From e033038a2b0ad5702c2c7aa1981df384a5e53570 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 13 Apr 2026 20:22:27 +0000 Subject: [PATCH] refactor(sdk,workflows): rename Copilot sendAndWait to send and simplify workflows Migrate Copilot session API from sendAndWait() to send() across all workflows, skill references, and documentation. Simplify builtin workflow implementations (ralph, deep-research-codebase) and example workflows for clarity and reduced boilerplate. --- .agents/skills/workflow-creator/SKILL.md | 10 +- .../references/agent-sessions.md | 87 +++++++---------- .../references/control-flow.md | 22 ++--- .../references/failure-modes.md | 20 ++-- .../references/getting-started.md | 25 +---- .../references/session-config.md | 6 +- .../workflow-creator/references/user-input.md | 8 +- .../workflows/hello-world/copilot/index.ts | 4 +- .../parallel-hello-world/copilot/index.ts | 31 ++---- README.md | 2 +- src/sdk/define-workflow.ts | 2 +- .../deep-research-codebase/copilot/index.ts | 96 +++++++------------ .../workflows/builtin/ralph/copilot/index.ts | 83 ++++++---------- 13 files changed, 147 insertions(+), 249 deletions(-) diff --git a/.agents/skills/workflow-creator/SKILL.md b/.agents/skills/workflow-creator/SKILL.md index 4252bf89b..2704e2d55 100644 --- a/.agents/skills/workflow-creator/SKILL.md +++ b/.agents/skills/workflow-creator/SKILL.md @@ -18,7 +18,7 @@ Load the topic-specific reference files from `references/` based on priority. ** | **1** | `getting-started.md` | **Always** — quick-start examples for all 3 SDKs, SDK exports, `SessionContext` reference | | **1** | `failure-modes.md` | **Always for multi-session workflows** — 15 catalogued failures (silent + loud) with wrong-vs-right patterns and a pre-ship design checklist | | **1** | `workflow-inputs.md` | **Always when declaring structured inputs or documenting how a workflow is invoked** — `WorkflowInput` schema, field-type selection, picker + CLI flag semantics, builtin-protection rules, invocation cheat sheet | -| **2** | `agent-sessions.md` | When writing SDK calls — `s.session.query()` (Claude), `s.session.sendAndWait()` (Copilot), `s.client.session.prompt()` (OpenCode); includes critical pitfalls on timeouts and session lifecycle | +| **2** | `agent-sessions.md` | When writing SDK calls — `s.session.query()` (Claude), `s.session.send()` (Copilot), `s.client.session.prompt()` (OpenCode); includes critical pitfalls on session lifecycle and when to use `sendAndWait` with explicit timeouts | | **2** | `control-flow.md` | When using loops, conditionals, parallel execution, or review/fix patterns | | **2** | `state-and-data-flow.md` | When passing data between sessions — `s.save()`, `s.transcript()`, `s.getMessages()`, file persistence, transcript compression | | **3** | `computation-and-validation.md` | When adding deterministic computation, response parsing, validation, quality gates, or file I/O | @@ -160,7 +160,7 @@ Hard constraints enforced by the builder, loader, and runtime: 4. **Unique session names** — every `ctx.stage()` call must use a unique `name` across the workflow run. 5. **Completed-only reads** — `transcript()` and `getMessages()` only access sessions whose callback has returned and saves have flushed. Attempting to read a still-running session throws. 6. **Graph topology is auto-inferred** — the runtime derives parent-child edges from `await`/`Promise.all` patterns. Sequential `await` creates a chain; `Promise.all([...])` branches from the same parent; a stage after `Promise.all` receives all parallel stages as parents. See `references/control-flow.md` for full details. -7. **Do not manually create clients or sessions** — the runtime auto-creates `s.client` and `s.session` from `clientOpts` and `sessionOpts`. Use `s.session.query()`, `s.session.sendAndWait()`, and `s.client.session.prompt()` instead. +7. **Do not manually create clients or sessions** — the runtime auto-creates `s.client` and `s.session` from `clientOpts` and `sessionOpts`. Use `s.session.query()`, `s.session.send()`, and `s.client.session.prompt()` instead. ## Concept-to-Code Mapping @@ -220,7 +220,7 @@ Pass a type parameter to `defineWorkflow<"agent">()` to narrow all context types | Agent | Type Parameter | Primary Session API | |-------|---------------|---------------------| | Claude | `defineWorkflow<"claude">` | `s.session.query(prompt)` — sends prompt to the Claude TUI pane | -| Copilot | `defineWorkflow<"copilot">` | `s.session.sendAndWait({ prompt }, TIMEOUT_MS)` — **explicit timeout required** (default 60s throws) | +| Copilot | `defineWorkflow<"copilot">` | `s.session.send({ prompt })` — fire-and-forget; use `sendAndWait({ prompt }, timeoutMs)` only when the user explicitly requests timeout-based waiting | | OpenCode | `defineWorkflow<"opencode">` | `s.client.session.prompt({ sessionID: s.session.id, parts: [...] })` | The runtime manages client/session lifecycle automatically. For native SDK types and advanced APIs, import directly from the provider packages (`@github/copilot-sdk`, `@anthropic-ai/claude-agent-sdk`, `@opencode-ai/sdk/v2`). @@ -246,9 +246,9 @@ Per-SDK cheat sheet: | Concern | Claude | Copilot | OpenCode | |---------|--------|---------|----------| -| Send prompt | `s.session.query(prompt)` | `s.session.sendAndWait({ prompt }, TIMEOUT)` | `s.client.session.prompt({ sessionID: s.session.id, parts: [{ type: "text", text: prompt }] })` | +| Send prompt | `s.session.query(prompt)` | `s.session.send({ prompt })` | `s.client.session.prompt({ sessionID: s.session.id, parts: [{ type: "text", text: prompt }] })` | | Save output | `s.save(s.sessionId)` | `s.save(await s.session.getMessages())` | `s.save(result.data!)` | -| Timeout | Per-query defaults via sessionOpts | **Mandatory** 2nd arg (default 60s throws!) | N/A | +| Timeout | Per-query defaults via sessionOpts | N/A (`send` has no timeout; `sendAndWait` accepts optional timeout, default 60s) | N/A | | Context model | Tmux pane (accumulates across turns) | Fresh per `ctx.stage()` | Fresh per `ctx.stage()` | | Extract text | `result.output` (string) | `getAssistantText(messages)` (see `failure-modes.md` F1) | `extractResponseText(result.data!.parts)` (see `failure-modes.md` F3) | diff --git a/.agents/skills/workflow-creator/references/agent-sessions.md b/.agents/skills/workflow-creator/references/agent-sessions.md index 9864bf3fd..cc438e035 100644 --- a/.agents/skills/workflow-creator/references/agent-sessions.md +++ b/.agents/skills/workflow-creator/references/agent-sessions.md @@ -210,9 +210,6 @@ Copilot uses a client-server architecture. The runtime auto-creates a `CopilotCl ```ts import { defineWorkflow } from "@bastani/atomic/workflows"; -// Always pass an explicit timeout to sendAndWait — see the pitfall note below. -const SEND_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes - export default defineWorkflow<"copilot">({ name: "implement" }) .run(async (ctx) => { await ctx.stage( @@ -223,7 +220,7 @@ export default defineWorkflow<"copilot">({ name: "implement" }) // s.client — CopilotClient (already started by runtime) // s.session — CopilotSession (already created, foreground session set) - await s.session.sendAndWait({ prompt: (s.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (s.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }, @@ -232,47 +229,32 @@ export default defineWorkflow<"copilot">({ name: "implement" }) .compile(); ``` -### Critical pitfall: `sendAndWait` has a 60-second default timeout - -`session.sendAndWait(options, timeout?)` accepts an optional second `timeout` -parameter that **defaults to 60000 ms**. When the timeout elapses it **throws** -`Timeout after 60000ms waiting for session.idle` — it does NOT abort the -in-flight agent, and it does NOT silently return. The throw propagates out of -the session callback, so: - -1. The current stage fails. -2. Every subsequent session step never executes (e.g. a `planner → orchestrator → reviewer` pipeline stops dead after the planner). -3. The agent may still be churning in the background. +### `send` vs `sendAndWait`: choosing the right method -This is deadly for real work — planner, reviewer, and orchestrator sub-agents -routinely need more than 60 seconds of wall-clock time. Source: -`@github/copilot-sdk/dist/session.js` — the `sendAndWait` implementation races -the idle promise against `setTimeout(..., timeout ?? 6e4)`. - -**Always pass an explicit, generous timeout** when calling `sendAndWait` inside -a workflow. Define it as a named constant so it's obvious and tunable: +**Default to `send`** for all Copilot workflow stages. `send` dispatches the +prompt and returns the `messageId` immediately — no timeout to guess, no +constants to tune, no risk of aborting a long-running agent mid-work. ```ts -// Buggy — silently inherits the 60s default and crashes long stages -await session.sendAndWait({ prompt }); - -// Correct — explicit 30-minute budget -const SEND_TIMEOUT_MS = 30 * 60 * 1000; -await session.sendAndWait({ prompt }, SEND_TIMEOUT_MS); +// Default pattern — clean, no timeout guessing +await s.session.send({ prompt }); ``` -Pick a timeout that fits the expected work: +**Use `sendAndWait` only when the user explicitly requests timeout-based +waiting.** `sendAndWait` blocks until the session emits `session.idle` or the +timeout fires. If you must use it, ask the user for a reasonable timeout. If +they aren't sure, default to **5 minutes** (300 000 ms). -| Session type | Suggested timeout | -|---|---| -| Short, bounded prompts (summaries, classification) | 5-10 minutes | -| Sub-agents doing file-wide analysis (planner, reviewer) | 30 minutes | -| Long implementation or multi-file refactors | 60 minutes | +```ts +// Only when the user explicitly wants timeout-gated waiting +const SEND_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes default; ask the user +await s.session.sendAndWait({ prompt }, SEND_TIMEOUT_MS); +``` -The timeout controls how long the SDK waits for `session.idle`; it does not -cap the agent itself. Err on the generous side — a truly hung session will -still surface as a clear error message rather than silently breaking -downstream stages. +**Why not `sendAndWait` by default?** The 60-second default timeout is almost +never correct for real agent work — planners, reviewers, and orchestrators +routinely exceed it. Guessing large timeouts (30-60 min) adds messy constants +and still risks aborting work prematurely. `send` avoids the problem entirely. ### Critical pitfall: session lifecycle controls what context is available @@ -293,7 +275,7 @@ state determines what context the model sees on its next turn: | State | How you get there | Context available | Action needed | |---|---|---|---| | **Fresh** | `client.createSession(...)` | **None** — empty conversation | You MUST inject everything the agent needs in the first prompt | -| **Continued** | Same session, additional `sendAndWait` calls | All prior turns in this session | Nothing — but watch total token usage | +| **Continued** | Same session, additional `send` calls | All prior turns in this session | Nothing — but watch total token usage | | **Resumed** | `client.resumeSession(sessionId)` | All persisted turns from the prior session of the SAME agent | Nothing — full history is reattached | | **Closed** | `session.disconnect()` or `client.stop()` | **Gone** from the live client; persisted on disk if the host enables it | Either resume by ID (same agent) or start fresh and re-inject context | @@ -328,7 +310,7 @@ Simplest and most common fix: ```ts async function runAgent(agent: string, prompt: string): Promise { const session = await client.createSession({ agent, onPermissionRequest: approveAll }); - await session.sendAndWait({ prompt }, SEND_TIMEOUT_MS); + await session.send({ prompt }); const messages = await session.getMessages(); await session.disconnect(); return getAssistantText(messages); // concatenate every top-level turn — see failure-modes.md §F1 @@ -354,8 +336,8 @@ history across related steps. ```ts // Same stage, multi-turn — full history stays attached -await s.session.sendAndWait({ prompt: "Plan the implementation." }, SEND_TIMEOUT_MS); -await s.session.sendAndWait({ prompt: "Follow up on the plan above." }, SEND_TIMEOUT_MS); +await s.session.send({ prompt: "Plan the implementation." }); +await s.session.send({ prompt: "Follow up on the plan above." }); ``` If you deliberately drop down to provider-specific resume/fork APIs, keep them @@ -407,20 +389,17 @@ them first.** ### Multi-turn conversations -Send multiple prompts to the same session. Remember: every `sendAndWait` call -needs its own explicit timeout. +Send multiple prompts to the same session: ```ts -const SEND_TIMEOUT_MS = 30 * 60 * 1000; - .run(async (ctx) => { await ctx.stage({ name: "implement" }, {}, {}, async (s) => { // Turn 1 - await s.session.sendAndWait({ prompt: "Plan the implementation." }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: "Plan the implementation." }); // Turn 2 - await s.session.sendAndWait({ prompt: "Now implement the plan." }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: "Now implement the plan." }); // Turn 3 - await s.session.sendAndWait({ prompt: "Run the tests." }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: "Run the tests." }); s.save(await s.session.getMessages()); }); @@ -446,7 +425,7 @@ await ctx.stage( }, }, // sessionOpts async (s) => { - await s.session.sendAndWait({ prompt: (s.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (s.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }, ); @@ -473,7 +452,7 @@ await ctx.stage( {}, { tools: [myTool] }, async (s) => { - await s.session.sendAndWait({ prompt: (s.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (s.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }, ); @@ -524,15 +503,13 @@ s.session.on("assistant.reasoning_delta", (event) => { Pass the `agent` parameter in `sessionOpts` (3rd arg to `ctx.stage()`) to bind the session to a named sub-agent: ```ts -const SEND_TIMEOUT_MS = 30 * 60 * 1000; // planner can take a while - .run(async (ctx) => { await ctx.stage( { name: "plan" }, {}, { agent: "planner" }, // sessionOpts — binds the session to the "planner" agent async (s) => { - await s.session.sendAndWait({ prompt: (s.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (s.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }, ); @@ -590,7 +567,7 @@ substitute the OpenCode API equivalents: | Concept | Copilot API | OpenCode API | |---|---|---| | Fresh session (auto-created) | `s.session` (runtime creates via `createSession`) | `s.session` (runtime creates via `session.create`) | -| Send a turn | `s.session.sendAndWait({ prompt }, timeout)` | `s.client.session.prompt({ sessionID: s.session.id, parts })` | +| Send a turn | `s.session.send({ prompt })` | `s.client.session.prompt({ sessionID: s.session.id, parts })` | | Close / disconnect | Auto-handled by runtime | session lifecycle managed via server; no explicit disconnect in typical flow | | Continue prior conversation | `s.client.resumeSession(sessionId)` (provider API; advanced) | Reuse the same `sessionID` with `s.client.session.prompt()` inside the same logical conversation. `ctx.stage()` itself still creates a fresh session every time | | Extract final text | `getAssistantText(messages)` (see `failure-modes.md` §F1) | `extractResponseText(result.data!.parts)` | diff --git a/.agents/skills/workflow-creator/references/control-flow.md b/.agents/skills/workflow-creator/references/control-flow.md index 79aba6f52..b1f9e5cc2 100644 --- a/.agents/skills/workflow-creator/references/control-flow.md +++ b/.agents/skills/workflow-creator/references/control-flow.md @@ -166,25 +166,16 @@ The inter-session pattern is the right fit here: every review and every fix beco ### Same pattern with Copilot -Loops amplify the `sendAndWait` 60-second timeout pitfall — any iteration -whose agent response takes longer than the default throws and kills the -entire surrounding session. Always pass an explicit timeout. See the -"Critical pitfall" section in `agent-sessions.md`. - ```ts -// Explicit per-call timeout — see agent-sessions.md pitfall note. -const SEND_TIMEOUT_MS = 30 * 60 * 1000; - .run(async (ctx) => { const MAX_CYCLES = 10; let consecutiveClean = 0; for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) { const review = await ctx.stage({ name: `review-${cycle}` }, {}, {}, async (s) => { - await s.session.sendAndWait( - { prompt: buildReviewPrompt((ctx.inputs.prompt ?? "")) }, - SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildReviewPrompt((ctx.inputs.prompt ?? "")), + }); const reviewRaw = getAssistantText(await s.session.getMessages()); // see failure-modes.md §F1 s.save(await s.session.getMessages()); @@ -206,10 +197,9 @@ const SEND_TIMEOUT_MS = 30 * 60 * 1000; : buildFixSpecFromRawReview(reviewRaw, (ctx.inputs.prompt ?? "")); await ctx.stage({ name: `fix-${cycle}` }, {}, {}, async (s) => { - await s.session.sendAndWait( - { prompt: fixPrompt || "Fix remaining issues." }, - SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: fixPrompt || "Fix remaining issues.", + }); s.save(await s.session.getMessages()); }); diff --git a/.agents/skills/workflow-creator/references/failure-modes.md b/.agents/skills/workflow-creator/references/failure-modes.md index 9024a2d44..0ad309bc8 100644 --- a/.agents/skills/workflow-creator/references/failure-modes.md +++ b/.agents/skills/workflow-creator/references/failure-modes.md @@ -39,7 +39,7 @@ Silent failures are catalogued first below. Loud failures are grouped at the end | [F7](#f7-continued-sessions-accumulate-state-across-loop-iterations) | Continued sessions accumulate state across loop iterations (lost-in-middle) | all | silent | | [F8](#f8-fenced-block-parsers-break-when-the-model-adds-prose) | Fenced-block parsers break when the model adds prose before/after | all | silent | | [F9](#f9-ssave-receives-the-wrong-shape) | `s.save()` receives the wrong shape for the SDK | all | silent | -| [F10](#f10-copilot-sendandwait-default-60s-timeout-throws) | Copilot: `sendAndWait` default 60s timeout throws | Copilot | loud | +| [F10](#f10-copilot-sendandwait-default-60s-timeout-throws) | Copilot: `sendAndWait` default 60s timeout throws (use `send` by default) | Copilot | loud | | [F11](#f11-manual-claude-session-initialization-resolved-by-runtime) | ~~Manual Claude session initialization~~ (resolved by runtime) | Claude | N/A | | [F12](#f12-resume-session-tries-to-swap-agents) | Resume session tries to swap agents | Copilot, OpenCode | loud | | [F13](#f13-parallel-siblings-read-each-others-transcripts) | Parallel siblings read each other's transcripts | all | loud | @@ -457,7 +457,7 @@ expects, and the runtime doesn't type-check the argument beyond "anything". // Claude — saves the wrong thing s.save(result.output); -// Copilot — saves an empty array if called before sendAndWait +// Copilot — saves an empty array if called before send s.save(await s.session.getMessages()); // Or saves one message object instead of the array s.save((await s.session.getMessages()).at(-1)); @@ -484,13 +484,17 @@ log the length. A 0-length or JSON-that-isn't-prose signature = F9. subsequent `ctx.stage()` call never executes — the throw propagates out of `run()` and halts the workflow. -**Full write-up.** `agent-sessions.md` §"Critical pitfall: `sendAndWait` has -a 60-second default timeout". - -**Fix.** Always pass an explicit timeout as the second argument. +**Fix.** Use `send` instead of `sendAndWait` — it has no timeout and avoids +the problem entirely. Only use `sendAndWait` when the user explicitly +requests timeout-based waiting, and always pass an explicit timeout (default +to 5 minutes if the user is unsure). ```ts -const SEND_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes +// Default pattern — no timeout, no risk +await s.session.send({ prompt }); + +// Only when the user explicitly wants timeout-gated waiting +const SEND_TIMEOUT_MS = 5 * 60 * 1000; // ask the user for a value await s.session.sendAndWait({ prompt }, SEND_TIMEOUT_MS); ``` @@ -662,7 +666,7 @@ accessing `.result` without awaiting, the type will be `Promise`, not `T`. Before shipping a multi-session workflow, walk the list: -- [ ] Every `s.session.sendAndWait` call passes an explicit timeout (F10) +- [ ] Copilot stages use `s.session.send` by default; `sendAndWait` only with an explicit user-requested timeout (F10) - [ ] Every fresh-session handoff forwards context explicitly (F5) - [ ] Every prompt whose output feeds a downstream stage explicitly requests trailing commentary (F6) - [ ] Response-text extraction uses the per-SDK correct pattern (F1-F4) diff --git a/.agents/skills/workflow-creator/references/getting-started.md b/.agents/skills/workflow-creator/references/getting-started.md index 8c7ad3813..abadbb863 100644 --- a/.agents/skills/workflow-creator/references/getting-started.md +++ b/.agents/skills/workflow-creator/references/getting-started.md @@ -51,35 +51,23 @@ export default defineWorkflow<"claude">({ ### Copilot -Note the `SEND_TIMEOUT_MS` constant passed as the second argument to every -`sendAndWait` call. The Copilot SDK's default timeout is **60 seconds**, and -when it fires it throws — which aborts the current session. Always pass an -explicit, generous timeout. See the "Critical pitfall" section in -`agent-sessions.md` for the full explanation. - ```ts // .atomic/workflows/my-workflow/copilot/index.ts import { defineWorkflow } from "@bastani/atomic/workflows"; -// Explicit 30-minute timeout — required; see agent-sessions.md pitfall note. -const SEND_TIMEOUT_MS = 30 * 60 * 1000; - export default defineWorkflow<"copilot">({ name: "my-workflow", description: "A two-session pipeline", }) .run(async (ctx) => { - // `userPromptText` rather than `prompt` because Copilot's - // sendAndWait uses `{ prompt: ... }` as an object key — the local - // name avoids visual collision inside those send calls. - const userPromptText = ctx.inputs.prompt ?? ""; + const prompt = ctx.inputs.prompt ?? ""; const describe = await ctx.stage( { name: "describe", description: "Ask the agent to describe the project" }, {}, {}, async (s) => { - await s.session.sendAndWait({ prompt: userPromptText }, SEND_TIMEOUT_MS); + await s.session.send({ prompt }); s.save(await s.session.getMessages()); }, ); @@ -90,12 +78,9 @@ export default defineWorkflow<"copilot">({ {}, async (s) => { const research = await s.transcript(describe); - await s.session.sendAndWait( - { - prompt: `Summarize the following in 2-3 bullet points:\n\n${research.content}`, - }, - SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: `Summarize the following in 2-3 bullet points:\n\n${research.content}`, + }); s.save(await s.session.getMessages()); }, ); diff --git a/.agents/skills/workflow-creator/references/session-config.md b/.agents/skills/workflow-creator/references/session-config.md index 203cf11a4..500ab0e74 100644 --- a/.agents/skills/workflow-creator/references/session-config.md +++ b/.agents/skills/workflow-creator/references/session-config.md @@ -207,7 +207,7 @@ await ctx.stage({ name: "plan" }, {}, { // Advanced infiniteSessions: true, // Auto-manage context via compaction }, async (s) => { - await s.session.sendAndWait({ prompt: (ctx.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (ctx.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }); ``` @@ -217,7 +217,7 @@ await ctx.stage({ name: "plan" }, {}, { ```ts // Approve everything (autonomous) — this is the default await ctx.stage({ name: "plan" }, {}, { onPermissionRequest: approveAll }, async (s) => { - await s.session.sendAndWait({ prompt: (ctx.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (ctx.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }); @@ -237,7 +237,7 @@ await ctx.stage({ name: "plan" }, {}, { } }, }, async (s) => { - await s.session.sendAndWait({ prompt: (ctx.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (ctx.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }); ``` diff --git a/.agents/skills/workflow-creator/references/user-input.md b/.agents/skills/workflow-creator/references/user-input.md index 013f119da..4da578a48 100644 --- a/.agents/skills/workflow-creator/references/user-input.md +++ b/.agents/skills/workflow-creator/references/user-input.md @@ -77,7 +77,7 @@ await ctx.stage({ name: "plan" }, {}, { return answer; }, }, async (s) => { - await s.session.sendAndWait({ prompt: (ctx.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (ctx.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }); ``` @@ -97,7 +97,7 @@ await ctx.stage({ name: "plan" }, {}, { }; }, }, async (s) => { - await s.session.sendAndWait({ prompt: (ctx.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (ctx.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }); ``` @@ -112,7 +112,7 @@ import { approveAll } from "@github/copilot-sdk"; // Explicit (same as the default): await ctx.stage({ name: "plan" }, {}, { onPermissionRequest: approveAll }, async (s) => { - await s.session.sendAndWait({ prompt: (ctx.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (ctx.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }); ``` @@ -131,7 +131,7 @@ await ctx.stage({ name: "plan" }, {}, { return { kind: "approved" }; }, }, async (s) => { - await s.session.sendAndWait({ prompt: (ctx.inputs.prompt ?? "") }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: (ctx.inputs.prompt ?? "") }); s.save(await s.session.getMessages()); }); ``` diff --git a/.atomic/workflows/hello-world/copilot/index.ts b/.atomic/workflows/hello-world/copilot/index.ts index 759bb9fdc..46a7f428f 100644 --- a/.atomic/workflows/hello-world/copilot/index.ts +++ b/.atomic/workflows/hello-world/copilot/index.ts @@ -1,7 +1,5 @@ import { defineWorkflow } from "@bastani/atomic/workflows"; -const SEND_TIMEOUT_MS = 30 * 60 * 1000; - /** * Build the greeting prompt from the structured inputs. The picker and * CLI flag parser both populate `ctx.inputs` — this workflow exercises @@ -49,7 +47,7 @@ export default defineWorkflow<"copilot">({ {}, {}, async (s) => { - await s.session.sendAndWait({ prompt }, SEND_TIMEOUT_MS); + await s.session.send({ prompt }); s.save(await s.session.getMessages()); }, ); diff --git a/.atomic/workflows/parallel-hello-world/copilot/index.ts b/.atomic/workflows/parallel-hello-world/copilot/index.ts index 8352f8057..a341ca755 100644 --- a/.atomic/workflows/parallel-hello-world/copilot/index.ts +++ b/.atomic/workflows/parallel-hello-world/copilot/index.ts @@ -1,7 +1,5 @@ import { defineWorkflow } from "@bastani/atomic/workflows"; -const SEND_TIMEOUT_MS = 30 * 60 * 1000; - /** Compose the initial greeting prompt from the structured inputs. */ function buildGreetPrompt(inputs: Record): string { const topic = inputs.topic ?? "the world"; @@ -37,7 +35,7 @@ export default defineWorkflow<"copilot">({ {}, {}, async (s) => { - await s.session.sendAndWait({ prompt: seedPrompt }, SEND_TIMEOUT_MS); + await s.session.send({ prompt: seedPrompt }); s.save(await s.session.getMessages()); }, ); @@ -49,12 +47,9 @@ export default defineWorkflow<"copilot">({ {}, async (s) => { const prior = await s.transcript(greet); - await s.session.sendAndWait( - { - prompt: `Rewrite the following as a formal greeting:\n\n${prior.content}`, - }, - SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: `Rewrite the following as a formal greeting:\n\n${prior.content}`, + }); s.save(await s.session.getMessages()); }, ), @@ -64,12 +59,9 @@ export default defineWorkflow<"copilot">({ {}, async (s) => { const prior = await s.transcript(greet); - await s.session.sendAndWait( - { - prompt: `Rewrite the following as a casual greeting:\n\n${prior.content}`, - }, - SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: `Rewrite the following as a casual greeting:\n\n${prior.content}`, + }); s.save(await s.session.getMessages()); }, ), @@ -82,12 +74,9 @@ export default defineWorkflow<"copilot">({ async (s) => { const formalText = await s.transcript(formal); const casualText = await s.transcript(casual); - await s.session.sendAndWait( - { - prompt: `Combine these two greetings into a single message:\n\n## Formal\n${formalText.content}\n\n## Casual\n${casualText.content}`, - }, - SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: `Combine these two greetings into a single message:\n\n## Formal\n${formalText.content}\n\n## Casual\n${casualText.content}`, + }); s.save(await s.session.getMessages()); }, ); diff --git a/README.md b/README.md index a8b3d258d..5e12ed350 100644 --- a/README.md +++ b/README.md @@ -572,7 +572,7 @@ The runtime auto-creates `s.client` and `s.session` — use them directly inside | Agent | How to send a prompt | | ----- | -------------------- | | **Claude** | `await s.session.query(prompt)` | -| **Copilot** | `await s.session.sendAndWait({ prompt }, TIMEOUT_MS)` — explicit timeout required (default 60s throws) | +| **Copilot** | `await s.session.send({ prompt })` | | **OpenCode** | `await s.client.session.prompt({ sessionID: s.session.id, parts: [{ type: "text", text: prompt }] })` | #### Key Rules diff --git a/src/sdk/define-workflow.ts b/src/sdk/define-workflow.ts index 9d269509e..76faaf41e 100644 --- a/src/sdk/define-workflow.ts +++ b/src/sdk/define-workflow.ts @@ -150,7 +150,7 @@ export class WorkflowBuilder { * {}, * async (s) => { * // s.client: CopilotClient, s.session: CopilotSession - * await s.session.sendAndWait({ prompt: s.inputs.prompt ?? "" }); + * await s.session.send({ prompt: s.inputs.prompt ?? "" }); * s.save(await s.session.getMessages()); * }, * ); diff --git a/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts b/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts index 3b67447f8..1ed9cde6a 100644 --- a/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +++ b/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts @@ -26,9 +26,6 @@ * * Copilot-specific concerns baked in: * - * • F10 — every `sendAndWait` passes an explicit 30-minute timeout. The SDK - * default is 60 seconds; a timeout THROWS and aborts the entire stage. - * Explorers can easily exceed 60s on large partitions. * * • F5 — every `ctx.stage()` call is a FRESH session with no memory of prior * stages. We forward the scout overview, history overview, and partition @@ -66,15 +63,6 @@ import { slugifyPrompt, } from "../helpers/prompts.ts"; -// ── Timeouts ──────────────────────────────────────────────────────────────── -// Every sendAndWait call passes one of these explicitly — never relying on -// the 60-second default (F10). Pick generously; a hung session still surfaces -// as a clear error rather than silently breaking downstream stages. -const SCOUT_TIMEOUT_MS = 15 * 60 * 1000; // 15 min — short orientation call -const HISTORY_TIMEOUT_MS = 20 * 60 * 1000; // 20 min — reads research/ docs -const EXPLORER_TIMEOUT_MS = 45 * 60 * 1000; // 45 min — multi-step locate/analyze -const AGGREGATOR_TIMEOUT_MS = 45 * 60 * 1000; // 45 min — reads N explorer reports - export default defineWorkflow<"copilot">({ name: "deep-research-codebase", description: @@ -127,19 +115,16 @@ export default defineWorkflow<"copilot">({ // 4. Short LLM call: architectural orientation for downstream // explorers. The prompt forbids the agent from answering the // research question — its only job here is to orient. - await s.session.sendAndWait( - { - prompt: buildScoutPrompt({ - question: prompt, - tree: data.tree, - totalLoc: data.totalLoc, - totalFiles: data.totalFiles, - explorerCount: actualCount, - partitionPreview: partitions, - }), - }, - SCOUT_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildScoutPrompt({ + question: prompt, + tree: data.tree, + totalLoc: data.totalLoc, + totalFiles: data.totalFiles, + explorerCount: actualCount, + partitionPreview: partitions, + }), + }); // F9: Copilot takes SessionEvent[], not a session ID. s.save(await s.session.getMessages()); @@ -166,10 +151,9 @@ export default defineWorkflow<"copilot">({ // The generic history prompt drives a single default-agent session // through locate → analyze → synthesize inline, instead of Claude's // sub-agent dispatch. - await s.session.sendAndWait( - { prompt: buildHistoryPromptGeneric({ question: prompt, root }) }, - HISTORY_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildHistoryPromptGeneric({ question: prompt, root }), + }); s.save(await s.session.getMessages()); }, ), @@ -210,21 +194,18 @@ export default defineWorkflow<"copilot">({ {}, {}, async (s) => { - await s.session.sendAndWait( - { - prompt: buildExplorerPromptGeneric({ - question: prompt, - index: i, - total: explorerCount, - partition, - scoutOverview, - historyOverview, - scratchPath, - root, - }), - }, - EXPLORER_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildExplorerPromptGeneric({ + question: prompt, + index: i, + total: explorerCount, + partition, + scoutOverview, + historyOverview, + scratchPath, + root, + }), + }); s.save(await s.session.getMessages()); // Returning structured metadata lets the aggregator stage reach @@ -254,21 +235,18 @@ export default defineWorkflow<"copilot">({ {}, {}, async (s) => { - await s.session.sendAndWait( - { - prompt: buildAggregatorPrompt({ - question: prompt, - totalLoc, - totalFiles, - explorerCount, - explorerFiles: explorerHandles.map((h) => h.result), - finalPath, - scoutOverview, - historyOverview, - }), - }, - AGGREGATOR_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildAggregatorPrompt({ + question: prompt, + totalLoc, + totalFiles, + explorerCount, + explorerFiles: explorerHandles.map((h) => h.result), + finalPath, + scoutOverview, + historyOverview, + }), + }); s.save(await s.session.getMessages()); }, ); diff --git a/src/sdk/workflows/builtin/ralph/copilot/index.ts b/src/sdk/workflows/builtin/ralph/copilot/index.ts index 410de34c5..0221a30ee 100644 --- a/src/sdk/workflows/builtin/ralph/copilot/index.ts +++ b/src/sdk/workflows/builtin/ralph/copilot/index.ts @@ -26,14 +26,6 @@ import { safeGitStatusS } from "../helpers/git.ts"; const MAX_LOOPS = 10; const CONSECUTIVE_CLEAN_THRESHOLD = 2; -/** - * Per-agent send timeout. `CopilotSession.sendAndWait` defaults to 60s, which - * is far too short for real planner/orchestrator/reviewer/debugger work — a - * timeout there throws and aborts the whole workflow before the next stage - * can run. 30 minutes gives each sub-agent ample headroom while still - * surfacing truly hung sessions. - */ -const AGENT_SEND_TIMEOUT_MS = 30 * 60 * 1000; /** * Concatenate the text content of every top-level assistant message in the @@ -90,15 +82,12 @@ export default defineWorkflow<"copilot">({ {}, { agent: "planner" }, async (s) => { - await s.session.sendAndWait( - { - prompt: buildPlannerPrompt(userPromptText, { - iteration, - debuggerReport: debuggerReport || undefined, - }), - }, - AGENT_SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildPlannerPrompt(userPromptText, { + iteration, + debuggerReport: debuggerReport || undefined, + }), + }); const messages = await s.session.getMessages(); s.save(messages); return getAssistantText(messages); @@ -113,14 +102,11 @@ export default defineWorkflow<"copilot">({ {}, { agent: "orchestrator" }, async (s) => { - await s.session.sendAndWait( - { - prompt: buildOrchestratorPrompt(userPromptText, { - plannerNotes: planner.result, - }), - }, - AGENT_SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildOrchestratorPrompt(userPromptText, { + plannerNotes: planner.result, + }), + }); s.save(await s.session.getMessages()); }, ); @@ -134,15 +120,12 @@ export default defineWorkflow<"copilot">({ {}, { agent: "reviewer" }, async (s) => { - await s.session.sendAndWait( - { - prompt: buildReviewPrompt(userPromptText, { - gitStatus, - iteration, - }), - }, - AGENT_SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildReviewPrompt(userPromptText, { + gitStatus, + iteration, + }), + }); const messages = await s.session.getMessages(); s.save(messages); return getAssistantText(messages); @@ -165,16 +148,13 @@ export default defineWorkflow<"copilot">({ {}, { agent: "reviewer" }, async (s) => { - await s.session.sendAndWait( - { - prompt: buildReviewPrompt(userPromptText, { - gitStatus, - iteration, - isConfirmationPass: true, - }), - }, - AGENT_SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildReviewPrompt(userPromptText, { + gitStatus, + iteration, + isConfirmationPass: true, + }), + }); const messages = await s.session.getMessages(); s.save(messages); return getAssistantText(messages); @@ -203,15 +183,12 @@ export default defineWorkflow<"copilot">({ {}, { agent: "debugger" }, async (s) => { - await s.session.sendAndWait( - { - prompt: buildDebuggerReportPrompt(parsed, reviewRaw, { - iteration, - gitStatus, - }), - }, - AGENT_SEND_TIMEOUT_MS, - ); + await s.session.send({ + prompt: buildDebuggerReportPrompt(parsed, reviewRaw, { + iteration, + gitStatus, + }), + }); const messages = await s.session.getMessages(); s.save(messages); return getAssistantText(messages);