From bb837f88948d1a8c4b1c0cf85c6dc33771c507cd Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Thu, 24 Sep 2026 14:40:33 +0000 Subject: [PATCH] Rename Sessioned to WithChat It pairs a planning result with the ephemeral Chat that produced it; the old name suggested a durable session. --- AGENTS.md | 4 +- README.md | 16 +++--- flow/src/main/scala/orca/plan/Plan.scala | 49 +++++++++---------- .../main/scala/orca/plan/PlanPrompts.scala | 4 +- flow/src/main/scala/orca/plan/Triage.scala | 2 +- .../plan/{Sessioned.scala => WithChat.scala} | 7 ++- .../test/scala/orca/plan/PlanGridTest.scala | 10 ++-- runner/src/main/scala/orca/exports.scala | 2 +- .../scala/flowtests/ChatAdoptionTest.scala | 8 +-- .../scala/flowtests/FlowCompilesTest.scala | 33 ++++++------- 10 files changed, 66 insertions(+), 69 deletions(-) rename flow/src/main/scala/orca/plan/{Sessioned.scala => WithChat.scala} (75%) diff --git a/AGENTS.md b/AGENTS.md index 604ca2a17..114c7f0ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -159,9 +159,9 @@ most easily broken: the `AgentCall` modes' `runWithSession` — so ephemeral continuation is only reachable through a `Chat` handle). Agent/conversation safety comes from bundling, not from types a flow - script combines: `Chat`, `FlowSession` and `Sessioned` carry their agent and + script combines: `Chat`, `FlowSession` and `WithChat` carry their agent and conversation from creation, and every way to pair them (`Chat`'s - constructor, `Agent.chat(continueFrom)`, `Sessioned`'s `apply`/`copy`, + constructor, `Agent.chat(continueFrom)`, `WithChat`'s `apply`/`copy`, `SessionId`, `WireSessionId`) is `private[orca]` or narrower. `Chat.withAgent` swaps in a variant of the chat's agent and refuses one on another backend instance. An adopted chat refuses a turn diff --git a/README.md b/README.md index 15314b98e..005f278ea 100644 --- a/README.md +++ b/README.md @@ -712,7 +712,7 @@ Every way to talk to an agent, by what the conversation must do: | `agent.chat()` → `chat.run(prompt)` / `chat.resultAs[O].{autonomous,interactive}.run(input)` | new, then continued by every turn | no | both | text or `O` | `InStage` | yes* | | `agent.session(name, seed)` → `session.run(prompt)` / `session.resultAs[O].run(input)` | named; continued, or re-seeded if lost | yes | autonomous | text or `O` | `FlowContext`, `FlowControl`, `InStage`, `WorkspaceWrite` | no | | `session.chat` → as `Chat` | the session's; refused while the backend doesn't hold it (never run, or lost on resume) | no (turns not recorded) | both | text or `O` | `InStage` | yes* | -| `Plan.{autonomous,interactive}.*` → `Sessioned`; `.reviewed()`, `.chat` | new planning conversation, continued by `.reviewed()` and `.chat` | no | as named | `O` | `FlowContext`, `InStage` | yes* | +| `Plan.{autonomous,interactive}.*` → `WithChat`; `.reviewed()`, `.chat` | new planning conversation, continued by `.reviewed()` and `.chat` | no | as named | `O` | `FlowContext`, `InStage` | yes* | | `reviewAndFixLoop` / `reviewThenFix` | new reviewer chats; continues `coderSession` | the coder session does | autonomous | findings | `FlowContext`, `FlowControl`, `InStage`, `WorkspaceWrite` | no | | `lint(commands, agent)` | new (or continues a `Lint.summariser`) | no | autonomous | `ReviewResult` (`LintReport` with a summariser) | `FlowContext`, `InStage` | yes | @@ -905,15 +905,15 @@ splits `autonomous` / `interactive`: | `assessThenPlan(userPrompt, agent, instructions?)` | `Verdict[Plan]` | assess, then `Proceed(plan)` or `Rejection(kind, body)` | same, but can ask the reporter to clarify instead of rejecting | | `triage(report, agent, instructions?)` | `Triage` | classify a bug report (not-a-bug / untestable / testable) | same, with clarifying questions | -Every cell returns `Sessioned[]` — the result paired with the +Every cell returns `WithChat[]` — the result paired with the (ephemeral) `Chat` that produced it. Continue that conversation in-run (`chat.run(task)`; continuations have write access), or `.value` it and start a fresh, durable implementer session via `agent.session("implementer", seed = plan.brief)` — the chat does not survive a crash/resume, so every shipped example takes `.value`. Destructure when you want both: `val -Sessioned(chat, plan) = Plan.autonomous.from(...)`. +WithChat(chat, plan) = Plan.autonomous.from(...)`. -From a `Sessioned[Plan]`, an optional `.reviewed()` step refines the plan +From a `WithChat[Plan]`, an optional `.reviewed()` step refines the plan before implementing — the planner critiques its own draft, read-only, producing an improved `Plan`. Chain it: `Plan.autonomous.from(...).reviewed().value`. `.reviewed(variant = _.cheap)` runs the review on a variant of the read-only @@ -1110,7 +1110,7 @@ layer — replace the whole set via `flow(prompts = ...)`. See [ADR Common types you'll see in flow scripts. Most `derives JsonData`, making them valid stage results (the progress log can record and replay them) and usable as -structured LLM output via `claude.resultAs[T]`. Exceptions: `Sessioned` and +structured LLM output via `claude.resultAs[T]`. Exceptions: `WithChat` and `Verdict` do not derive `JsonData` — they are intermediate values, not stage results. @@ -1128,11 +1128,11 @@ results. description. - **`orca.plan.Task(title, description)`** — `title` is the human-readable label shown in the event log. -- **`orca.plan.Sessioned(chat, value)`** — every `Plan.{autonomous, +- **`orca.plan.WithChat(chat, value)`** — every `Plan.{autonomous, interactive}.*` operation returns one: the result paired with the (ephemeral) `Chat` that produced it, so the caller can continue that conversation in-run or `.value` it and start fresh. Only the library builds one; destructure it - with `val Sessioned(chat, plan) = ...`. + with `val WithChat(chat, plan) = ...`. - **`orca.plan.Verdict[A]`** — `Verdict.Proceed(value)` or `Verdict.Rejection(kind, body)` (kind ∈ Question / Critique / Rebuff). Returned by `assessThenPlan` as `Verdict[Plan]`. @@ -1150,7 +1150,7 @@ results. - **`orca.agents.Chat[B]`** — ephemeral multi-turn conversation handle from `agent.chat()`: tool-using and workspace-editing like any agent turn ("chat" names its lifetime, not its powers), in-run only, fork-safe. Also carried by - `Sessioned` for planning-conversation continuations. + `WithChat` for planning-conversation continuations. - **`orca.Title`** — opaque `String` alias for short labels (`Task.title`, `ReviewFinding.title`); `Title("…")` to construct, `.value` to read. - **`orca.tools.PrHandle`** — handle to an open pull request (`host`, `owner`, diff --git a/flow/src/main/scala/orca/plan/Plan.scala b/flow/src/main/scala/orca/plan/Plan.scala index 22e90dc82..c12a1a738 100644 --- a/flow/src/main/scala/orca/plan/Plan.scala +++ b/flow/src/main/scala/orca/plan/Plan.scala @@ -28,10 +28,9 @@ import scala.annotation.unused * with a plan or rejects), or `triage` (classify a bug report into a * [[Triage]] verdict). * - * Every cell returns a [[Sessioned]] — the result plus the agent session that - * produced it. A `Sessioned[Plan]` can be continued read-only into - * [[Plan.reviewed]] (self-critique), or discarded for a fresh implementer - * session. + * Every cell returns a [[WithChat]] — the result plus the chat that produced + * it. A `WithChat[Plan]` can be continued read-only into [[Plan.reviewed]] + * (self-critique), or discarded for a fresh implementer session. * * As a single case class it is a valid stage result (ADR 0018 §2.3) — the * progress log, not a plan file, is what resume reads. @@ -57,7 +56,7 @@ object Plan: * network (issues/PRs/web) but can't edit during the planning turn (see * [[autonomousResult]] for the per-backend guarantee). * - * Each operation returns a [[Sessioned]]: the read-only planning session is + * Each operation returns a [[WithChat]]: the read-only planning chat is * still resumable by a later writable call, so the caller can continue it * into implementation or discard it for a fresh session. */ @@ -67,7 +66,7 @@ object Plan: userPrompt: String, agent: Agent[?], instructions: String = PlanPrompts.Planning - )(using FlowContext, InStage): Sessioned[Plan] = + )(using FlowContext, InStage): WithChat[Plan] = autonomousResult[Plan, Plan](agent, userPrompt, instructions)(identity) /** Skeptically assess `userPrompt` (typically a bug/feature report) and @@ -78,7 +77,7 @@ object Plan: userPrompt: String, agent: Agent[?], instructions: String = PlanPrompts.AssessThenPlan - )(using FlowContext, InStage): Sessioned[Verdict[Plan]] = + )(using FlowContext, InStage): WithChat[Verdict[Plan]] = autonomousResult[AssessedPlan, Verdict[Plan]]( agent, userPrompt, @@ -92,7 +91,7 @@ object Plan: report: String, agent: Agent[?], instructions: String = PlanPrompts.Triage - )(using FlowContext, InStage): Sessioned[Triage] = + )(using FlowContext, InStage): WithChat[Triage] = autonomousResult[BugTriage, Triage](agent, report, instructions)(b => getOrFail(b.toTriage) ) @@ -102,9 +101,9 @@ object Plan: * read-only: the prompt is what forbids edits here, and the user sees any * violation. The tier was picked when read-only was believed to cost the * `ask_user` MCP tool; claude's `--tools` allowlist does not, so this is - * revisitable. Use [[autonomous]] when no mid-session questions are needed. + * revisitable. Use [[autonomous]] when no clarifying questions are needed. * - * Each operation returns a [[Sessioned]] so the conversation can carry into + * Each operation returns a [[WithChat]] so the conversation can carry into * implementation. */ object interactive: @@ -113,7 +112,7 @@ object Plan: userPrompt: String, agent: Agent[?], instructions: String = PlanPrompts.Planning - )(using FlowContext, InStage): Sessioned[Plan] = + )(using FlowContext, InStage): WithChat[Plan] = interactiveResult[Plan, Plan](agent, userPrompt, instructions)( identity ) @@ -126,7 +125,7 @@ object Plan: userPrompt: String, agent: Agent[?], instructions: String = PlanPrompts.AssessThenPlan - )(using FlowContext, InStage): Sessioned[Verdict[Plan]] = + )(using FlowContext, InStage): WithChat[Verdict[Plan]] = interactiveResult[AssessedPlan, Verdict[Plan]]( agent, userPrompt, @@ -140,7 +139,7 @@ object Plan: report: String, agent: Agent[?], instructions: String = PlanPrompts.Triage - )(using FlowContext, InStage): Sessioned[Triage] = + )(using FlowContext, InStage): WithChat[Triage] = interactiveResult[BugTriage, Triage](agent, report, instructions)(b => getOrFail(b.toTriage) ) @@ -157,7 +156,7 @@ object Plan: result.fold(msg => throw OrcaFlowException(msg), identity) /** Run one autonomous turn producing wire type `O`, convert it to the public - * result `A`, and pair it with the session. Shared by every `autonomous.*` + * result `A`, and pair it with the chat. Shared by every `autonomous.*` * operation. * * Runs `NetworkOnly`: reads plus read-only network, so the planner can fetch @@ -173,7 +172,7 @@ object Plan: )(convert: O => A)(using @unused ctx: FlowContext, ev: InStage - ): Sessioned[A] = + ): WithChat[A] = // The planning turn runs on the restricted (NetworkOnly) sibling, but the // chat handed out is bound to the BASE agent, so a continuation regains the // caller's full capability — the restriction stays per-turn. @@ -183,7 +182,7 @@ object Plan: .resultAs[O] .autonomous .run(withInstructions(input, instructions)) - Sessioned(chat, convert(raw)) + WithChat(chat, convert(raw)) /** Interactive counterpart to [[autonomousResult]] — no per-turn restriction * (interactive planning runs with normal permissions, see [[interactive]]), @@ -196,19 +195,19 @@ object Plan: )(convert: O => A)(using @unused ctx: FlowContext, ev: InStage - ): Sessioned[A] = + ): WithChat[A] = val chat = agent.chat() val raw = chat .resultAs[O] .interactive .run(withInstructions(input, instructions)) - Sessioned(chat, convert(raw)) + WithChat(chat, convert(raw)) - // `reviewed` resumes the planning session read-only, reusing the planner's + // `reviewed` resumes the planning chat read-only, reusing the planner's // exploration. Defined here to keep it in the implicit scope of - // `Sessioned[Plan]`. + // `WithChat[Plan]`. - extension (sp: Sessioned[Plan]) + extension (planned: WithChat[Plan]) /** Resume the planning conversation for a critical self-review, returning * the improved plan (brief included) paired with the (same) chat. The * review turn runs on `variant` of the read-only chat agent — the one the @@ -218,13 +217,13 @@ object Plan: def reviewed( instructions: String = PlanPrompts.Review, variant: Agent[?] => Agent[?] = identity - )(using @unused ctx: FlowContext, ev: InStage): Sessioned[Plan] = - val improved = sp.chat + )(using @unused ctx: FlowContext, ev: InStage): WithChat[Plan] = + val improved = planned.chat .withAgent(agent => variant(agent.withReadOnly)) .resultAs[Plan] .autonomous - .run(s"$instructions\n\n${render(sp.value)}") - Sessioned(sp.chat, improved) + .run(s"$instructions\n\n${render(planned.value)}") + WithChat(planned.chat, improved) /** Empty plans render as nothing — surfacing "0 tasks planned" muddies the * picture; a planning failure is more useful as an explicit `fail(...)` from diff --git a/flow/src/main/scala/orca/plan/PlanPrompts.scala b/flow/src/main/scala/orca/plan/PlanPrompts.scala index 1babdae48..47f07ea36 100644 --- a/flow/src/main/scala/orca/plan/PlanPrompts.scala +++ b/flow/src/main/scala/orca/plan/PlanPrompts.scala @@ -39,8 +39,8 @@ object PlanPrompts: val Triage: String = PromptResource.load("/orca/plan/prompts/triage.md") - /** Used by `Sessioned[Plan].reviewed`. The current plan is appended after - * this block; the agent returns an improved plan, brief included. + /** Used by `WithChat[Plan].reviewed`. The current plan is appended after this + * block; the agent returns an improved plan, brief included. */ val Review: String = PromptResource.load("/orca/plan/prompts/review.md") diff --git a/flow/src/main/scala/orca/plan/Triage.scala b/flow/src/main/scala/orca/plan/Triage.scala index fbac7e58d..da1425064 100644 --- a/flow/src/main/scala/orca/plan/Triage.scala +++ b/flow/src/main/scala/orca/plan/Triage.scala @@ -18,7 +18,7 @@ import orca.agents.{Announce, JsonData} * with runtime `Option#get` / empty-string checks. * * Produced by [[Plan.autonomous.triage]] / [[Plan.interactive.triage]], - * wrapped in a [[Sessioned]]. Flows typically discard the triage session + * wrapped in a [[WithChat]]. Flows typically discard the triage chat * (`.value`) and seed a fresh implementer session from the issue body. * * A `stage` can record and replay a `Triage` result — the triage stage is a diff --git a/flow/src/main/scala/orca/plan/Sessioned.scala b/flow/src/main/scala/orca/plan/WithChat.scala similarity index 75% rename from flow/src/main/scala/orca/plan/Sessioned.scala rename to flow/src/main/scala/orca/plan/WithChat.scala index 7e2ed3964..64404bd32 100644 --- a/flow/src/main/scala/orca/plan/Sessioned.scala +++ b/flow/src/main/scala/orca/plan/WithChat.scala @@ -3,8 +3,7 @@ package orca.plan import orca.agents.Chat /** A planning-phase result paired with the (ephemeral) [[orca.agents.Chat]] - * that produced it. "Sessioned" names this ephemeral pairing specifically — it - * is not a durable session; see `agent.session(name, seed)` for that. + * that produced it. * * Every `Plan.{autonomous,interactive}.*` operation returns one of these, so * the caller can continue the same conversation into the implementation phase @@ -21,7 +20,7 @@ import orca.agents.Chat * site: * * {{{ - * val Sessioned(chat, plan) = Plan.autonomous.from(userPrompt, claude) + * val WithChat(chat, plan) = Plan.autonomous.from(userPrompt, claude) * }}} */ -final case class Sessioned[+A] private[orca] (chat: Chat[?], value: A) +final case class WithChat[+A] private[orca] (chat: Chat[?], value: A) diff --git a/flow/src/test/scala/orca/plan/PlanGridTest.scala b/flow/src/test/scala/orca/plan/PlanGridTest.scala index bf6f7d8a1..f7d2fcd73 100644 --- a/flow/src/test/scala/orca/plan/PlanGridTest.scala +++ b/flow/src/test/scala/orca/plan/PlanGridTest.scala @@ -3,8 +3,8 @@ package orca.plan import orca.events.EventDispatcher /** Runtime wiring of the autonomous planning grid: each operation pairs its - * result with the producing session, and `triage` converts the wire - * [[BugTriage]] into a [[Triage]]. The conversions themselves are covered by + * result with the producing chat, and `triage` converts the wire [[BugTriage]] + * into a [[Triage]]. The conversions themselves are covered by * [[AssessThenPlanTest]] (toVerdict) and [[BugTriageTest]] (toTriage); the * interactive cells share the same helper and are pinned at compile time by * `flowtests.FlowCompilesTest`. @@ -70,15 +70,15 @@ class PlanGridTest extends munit.FunSuite: "the planning turn must run on the restricted sibling" ) - // --- post-planning step (reviewed) on the planning session --- + // --- post-planning step (reviewed) on the planning chat --- /** `samplePlan` on a planning chat whose agent answers `reply`, after the * planning turn that opened its conversation. */ - private def planned(reply: CannedResult[Plan]): Sessioned[Plan] = + private def planned(reply: CannedResult[Plan]): WithChat[Plan] = val chat = reply.agent.chat() val _ = chat.resultAs[Plan].autonomous.run("plan") - Sessioned(chat, samplePlan) + WithChat(chat, samplePlan) test("reviewed returns the improved plan on the original chat binding"): val improved = samplePlan.copy(description = "tighter", brief = "sharper") diff --git a/runner/src/main/scala/orca/exports.scala b/runner/src/main/scala/orca/exports.scala index 28ff6e372..2a08aed90 100644 --- a/runner/src/main/scala/orca/exports.scala +++ b/runner/src/main/scala/orca/exports.scala @@ -44,7 +44,7 @@ export orca.agents.{ schemaFromJsonData, codecFromJsonData } -export orca.plan.{BugReportMatch, Plan, Sessioned, Task, Triage, Verdict} +export orca.plan.{BugReportMatch, Plan, Task, Triage, Verdict, WithChat} // PrSummary is the result type of openPrFromBranch and summarisePr; // orcaCommentMarker is the idempotency marker gh.upsertComment keys on; // recordOpenedPr is for a flow that opens its PR with a bare gh.createPr, and diff --git a/runner/src/test/scala/flowtests/ChatAdoptionTest.scala b/runner/src/test/scala/flowtests/ChatAdoptionTest.scala index b30260bac..85fbfd2f5 100644 --- a/runner/src/test/scala/flowtests/ChatAdoptionTest.scala +++ b/runner/src/test/scala/flowtests/ChatAdoptionTest.scala @@ -23,20 +23,20 @@ class ChatAdoptionTest extends munit.FunSuite: test("a flow script cannot pair a value with a chat"): val errors = compileErrors( """ - def pair(c: orca.Chat[?], p: orca.plan.Plan) = orca.plan.Sessioned(c, p) + def pair(c: orca.Chat[?], p: orca.plan.Plan) = orca.plan.WithChat(c, p) """ ) assert( errors.contains( - "Sessioned in package orca.plan does not take parameters" + "WithChat in package orca.plan does not take parameters" ), errors ) - test("a flow script cannot swap a Sessioned's chat"): + test("a flow script cannot swap a WithChat's chat"): val errors = compileErrors( """ - def swap(s: orca.plan.Sessioned[orca.plan.Plan], c: orca.Chat[?]) = + def swap(s: orca.plan.WithChat[orca.plan.Plan], c: orca.Chat[?]) = s.copy(chat = c) """ ) diff --git a/runner/src/test/scala/flowtests/FlowCompilesTest.scala b/runner/src/test/scala/flowtests/FlowCompilesTest.scala index e916acb06..f358e569b 100644 --- a/runner/src/test/scala/flowtests/FlowCompilesTest.scala +++ b/runner/src/test/scala/flowtests/FlowCompilesTest.scala @@ -358,23 +358,23 @@ object FlowCanary: case Right(_) => () /** Planning grid surface; exercised across `flows/`. Pins the full `mode × - * operation` grid: every cell returns `Sessioned[]` where the result + * operation` grid: every cell returns `WithChat[]` where the result * is `Plan` (`from`), `Verdict[Plan]` (`assessThenPlan`), or `Triage` * (`triage`). */ def planningGridSurface(): Unit = flow(OrcaArgs()): stage("grid"): - // --- from → Sessioned[Plan], both modes --- - val autoFrom: Sessioned[Plan] = + // --- from → WithChat[Plan], both modes --- + val autoFrom: WithChat[Plan] = Plan.autonomous.from(userPrompt, claude.opus) - val intFrom: Sessioned[Plan] = + val intFrom: WithChat[Plan] = Plan.interactive.from(userPrompt, claude) // Codex and Pi also expose ask_user, so the interactive cells // compile against them too. - val intFromCodex: Sessioned[Plan] = + val intFromCodex: WithChat[Plan] = Plan.interactive.from(userPrompt, codex) - val intFromPi: Sessioned[Plan] = + val intFromPi: WithChat[Plan] = Plan.interactive.from(userPrompt, pi) val _ = ( autoFrom.value, @@ -383,10 +383,10 @@ object FlowCanary: intFromPi.value ) - // --- assessThenPlan → Sessioned[Verdict[Plan]], both modes --- - val autoAssess: Sessioned[Verdict[Plan]] = + // --- assessThenPlan → WithChat[Verdict[Plan]], both modes --- + val autoAssess: WithChat[Verdict[Plan]] = Plan.autonomous.assessThenPlan(userPrompt, claude.opus) - val intAssess: Sessioned[Verdict[Plan]] = + val intAssess: WithChat[Verdict[Plan]] = Plan.interactive.assessThenPlan(userPrompt, claude) val _ = intAssess autoAssess.value match @@ -395,13 +395,12 @@ object FlowCanary: case Verdict.Rejection(Verdict.RejectionKind.Critique, _) => () case Verdict.Rejection(Verdict.RejectionKind.Rebuff, _) => () - // --- triage → Sessioned[Triage], both modes --- - val autoTriage: Sessioned[Triage] = + // --- triage → WithChat[Triage], both modes --- + val autoTriage: WithChat[Triage] = Plan.autonomous.triage(userPrompt, claude.opus) val _ = autoTriage.value - // Destructure the concretely-typed interactive result, as the bugfix - // plan does (`val Sessioned(session, triage) = ...`). - val Sessioned(_, triage) = Plan.interactive.triage(userPrompt, claude) + // Destructure the concretely-typed interactive result. + val WithChat(_, triage) = Plan.interactive.triage(userPrompt, claude) triage match case Triage.NotABug(_) => () case Triage.Untestable(_, _) => () @@ -427,9 +426,9 @@ object FlowCanary: ) /** Post-planning step (`reviewed`) plus the per-task stage loop — exercised - * by `flows/implement-enhanced.sc`. Pins that the `Sessioned[Plan]` - * extension resolves through `import orca.*` alone. Plans are always - * briefed: the `brief` rides in the structured output, so `plan.brief` / + * by `flows/implement-enhanced.sc`. Pins that the `WithChat[Plan]` extension + * resolves through `import orca.*` alone. Plans are always briefed: the + * `brief` rides in the structured output, so `plan.brief` / * `plan.taskPrompt` are always available. Resume is the progress log (ADR * 0018 §2.8), and the task loop is a plain per-task `stage(...)`. */