@esso0428/pi-subagents 0.17.28 → 0.17.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.17.29] - 2026-10-02
11
+
12
+ > **Breaking: Agent calls are always detached.** `Agent` now returns an ID immediately for every call; retrieve results after completion notifications with `get_subagent_result`. `run_in_background` remains accepted but has no effect.
13
+
14
+ ### Changed
15
+ - **Detached every Agent invocation and removed inline results**: fresh calls bypass the background queue and resumed calls run asynchronously; both return immediately and use completion notifications plus `get_subagent_result` for retrieval. `wait`, `wait_group`, and `wait_group_done` no longer depend on `run_in_background`, so a notification group can be used from any call rather than only a background one.
16
+ - **Made the live conversation viewer show and restore queued steering messages**: pending steers now appear as dim timeline rows until delivery, `Alt+Up` recalls the full pending queue into the composer joined by newlines, and the multiline composer accepts `Ctrl+J` with one rendered row per line.
17
+
10
18
  ## [0.17.28] - 2026-10-02
11
19
 
12
20
  ### Fixed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @esso0428/pi-subagents
2
2
 
3
- A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in foreground or background, steer them mid-run, resume completed sessions, and define your own custom agent types.
3
+ A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Agent calls always detach, steer them mid-run, resume completed sessions, and define your own custom agent types.
4
4
 
5
5
  > **Fork of [`@tintinweb/pi-subagents`](https://github.com/tintinweb/pi-subagents) integrating `npm:pi-subagents`-style JSON agent overrides — configure agents via `settings.json` without writing `.md` files.**
6
6
 
@@ -55,15 +55,15 @@ Agent({
55
55
  subagent_type: "Explore",
56
56
  prompt: "Find all files that handle authentication",
57
57
  description: "Find auth files",
58
- run_in_background: true,
58
+ run_in_background: true, // accepted for compatibility; has no effect
59
59
  })
60
60
  ```
61
61
 
62
- Foreground agents block until complete and return results inline. Background agents return an ID immediately and notify you on completion.
62
+ Agent calls always return an ID immediately and notify you on completion. Results arrive via the completion notification; call `get_subagent_result` with the ID to retrieve the result. `run_in_background` is retained for compatibility but has no effect.
63
63
 
64
64
  ### Wait groups
65
65
 
66
- Use `wait: true` on background agents to suppress individual completion notifications and receive one grouped notification instead:
66
+ Use `wait: true` to suppress an individual completion notification and receive one grouped notification instead:
67
67
 
68
68
  ```
69
69
  const group = subagent_wait_group({
@@ -75,7 +75,6 @@ Agent({
75
75
  subagent_type: "Explore",
76
76
  prompt: "Design a group-handle API",
77
77
  description: "Design group handle",
78
- run_in_background: true,
79
78
  wait: true,
80
79
  wait_group: group.group_id,
81
80
  })
@@ -84,7 +83,6 @@ Agent({
84
83
  subagent_type: "Plan",
85
84
  prompt: "Design a batch API",
86
85
  description: "Design batch API",
87
- run_in_background: true,
88
86
  wait: true,
89
87
  wait_group: group.group_id,
90
88
  wait_group_done: true,
@@ -121,13 +119,13 @@ Schedules are **session-scoped**: they reset on `/new` and restore on `/resume`.
121
119
 
122
120
  Restrictions:
123
121
  - `schedule` cannot be combined with `inherit_context` (no parent conversation exists at fire time) or `resume` (schedules create fresh agents).
124
- - `run_in_background` is forced to `true`.
122
+ - `run_in_background` is accepted for compatibility and has no effect; scheduled agents always run detached.
125
123
  - Scheduled fires bypass the `maxConcurrent` queue so a 5-minute interval cannot be deferred behind long-running manual agents.
126
124
  - **Headless `pi -p` doesn't wait for scheduled subagents.**
127
125
 
128
126
  ## UI
129
127
 
130
- The extension renders a persistent Agents widget above the editor. It initializes during TUI `session_start`, after the session branch restores completed-agent history, so openable agents are visible immediately when a session starts or resumes. By default it shows all agents (`widgetMode: all`), including foreground and background runs. Switch to `background` or `off` via `/agents → Settings → Widget`:
128
+ The extension renders a persistent Agents widget above the editor. It initializes during TUI `session_start`, after the session branch restores completed-agent history, so openable agents are visible immediately when a session starts or resumes. By default it shows all agents (`widgetMode: all`), including every managed run. Switch to `background` or `off` via `/agents → Settings → Widget`:
131
129
 
132
130
  ```
133
131
  ● Agents
@@ -171,7 +169,7 @@ Individual agent results render Claude Code-style in the conversation:
171
169
 
172
170
  Completed results can be expanded (ctrl+o in pi) to show the full agent output inline. The viewer's tool preview is read-only; its scrollbar and `j`/`k`/wheel/page/top/bottom controls never modify the agent transcript.
173
171
 
174
- By default, foreground and background agents each stream their full conversation to a per-subagent transcript — a JSON-lines file at `<os-tmpdir>/pi-subagents-<uid>/<cwd>/<session>/tasks/<agent-id>.output` (owner-only `0700`, cleared on reboot). Set `output_transcript: false` on a custom agent to write no transcript path or file for it, or set `outputTranscript: false` in `subagents.json` to make transcripts opt-in for the whole project (frontmatter overrides the project default). This governs **only** the transcript: it is independent of `persist_session` (the pi session on disk), and it does not affect `isolation: worktree` (which commits the agent's work to a git branch) or `memory:` (durable files) — set those accordingly if the goal is to keep a run off disk entirely. Background agent completion notifications render as styled boxes:
172
+ By default, agents stream their full conversation to a per-subagent transcript — a JSON-lines file at `<os-tmpdir>/pi-subagents-<uid>/<cwd>/<session>/tasks/<agent-id>.output` (owner-only `0700`, cleared on reboot). Set `output_transcript: false` on a custom agent to write no transcript path or file for it, or set `outputTranscript: false` in `subagents.json` to make transcripts opt-in for the whole project (frontmatter overrides the project default). This governs **only** the transcript: it is independent of `persist_session` (the pi session on disk), and it does not affect `isolation: worktree` (which commits the agent's work to a git branch) or `memory:` (durable files) — set those accordingly if the goal is to keep a run off disk entirely. Background agent completion notifications render as styled boxes:
175
173
 
176
174
  ```
177
175
  ✓ Find auth files completed
@@ -257,11 +255,11 @@ All fields are optional — sensible defaults for everything.
257
255
  | `session_dir` | pi default | Optional session directory when `persist_session: true`; omitted uses pi's normal session location, and relative paths resolve from the agent cwd |
258
256
  | `prompt_mode` | `replace` | `replace`: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). `append`: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
259
257
  | `inherit_context` | `false` | Fork parent conversation into agent |
260
- | `run_in_background` | `false` | Run in background by default |
258
+ | `run_in_background` | `false` | Compatibility parameter; has no effect because Agent calls always detach |
261
259
  | `isolated` | `false` | Hermetic specialist mode: forces `extensions: false` + `skills: false` + drops `ext:` selectors. Only built-in tools. Distinct from `isolation: worktree` (filesystem) |
262
260
  | `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
263
261
 
264
- Frontmatter is authoritative. If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `run_in_background`, `isolated`, or `isolation`, those values are locked for that agent. `Agent` tool parameters only fill fields the agent config leaves unspecified.
262
+ Frontmatter is authoritative. If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `isolated`, or `isolation`, those values are locked for that agent. `run_in_background` is accepted for compatibility but has no effect. `Agent` tool parameters only fill fields the agent config leaves unspecified.
265
263
 
266
264
  **Forgiving `model:` resolution.** A `model:` pin is matched against pi's model registry tolerantly, so cosmetic id variations don't silently drop the agent back to the parent's model: `.` and `-` are treated as equivalent in version numbers (`claude-haiku-4.5` ≡ `claude-haiku-4-5`), a trailing `-YYYYMMDD` date stamp is optional (`anthropic/claude-haiku-4-5-20251001` matches an undated registry id and vice-versa), and a `provider/modelId` whose named provider doesn't carry that model retries the bare id against every provider. Precedence is **exact → fuzzy under the named provider → same model under any provider → unavailable**, so an exact match always wins and dated snapshots aren't conflated. If nothing resolves, the pin can't run and the agent inherits the parent model — `/agents → Agent types` flags this case as `(unavailable, fallback: inherit)` and shows the resolved target `(→ provider/id)` when resolution lands on a different provider or version than configured. (This is distinct from [Model Scope](#model-scope) enforcement, which matches the `enabledModels` allowlist by *exact* entry.)
267
265
 
@@ -419,7 +417,7 @@ Launch a sub-agent.
419
417
  | `model` | string | no | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`). Resolved tolerantly (`.`/`-` and a trailing date stamp interchangeable) with provider fallback |
420
418
  | `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh, max (availability depends on pi version and model) |
421
419
  | `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
422
- | `run_in_background` | boolean | no | Run without blocking |
420
+ | `run_in_background` | boolean | no | Compatibility parameter; has no effect because Agent calls never block |
423
421
  | `resume` | string | no | Agent ID to resume a previous session |
424
422
  | `isolated` | boolean | no | No extension/MCP tools |
425
423
  | `isolation` | `"worktree"` | no | Run in an isolated git worktree |
@@ -461,7 +459,7 @@ Create new agent ← manual wizard or AI-generated
461
459
  Settings ← max concurrency, max turns, grace turns, join mode
462
460
  ```
463
461
 
464
- - **Running agents** — select one to open its live conversation viewer. While it's still running, press `Enter` to open the steering composer, then `Enter` again to send a message that redirects the agent (same mechanism as the `steer_subagent` tool; `Esc` or an empty submit returns), or press `x` (then `x` again to confirm) to stop/abort it — including **background** agents, which a global Esc can't unambiguously target (Esc still stops a blocking foreground `Agent` call). A stopped agent reports its partial output flagged as incomplete, not as a completion.
462
+ - **Running agents** — select one to open its live conversation viewer. While it's still running, press `Enter` to open the steering composer, then `Enter` again to send a message that redirects the agent (same mechanism as the `steer_subagent` tool; `Esc` or an empty submit returns), or press `x` (then `x` again to confirm) to stop/abort it. A stopped agent reports its partial output flagged as incomplete, not as a completion.
465
463
  - **Agent types** — unified list with source indicators: `•` (project), `◦` (global), `✕` (disabled). Each row shows the agent's model, and the highlighted agent's full description appears below the list. The model column flags `(unavailable, fallback: inherit)` when a configured model can't be resolved (it would silently inherit the parent model), and shows `(→ provider/id)` when it resolves to a different provider or version than configured. Select an agent to manage it:
466
464
  - **Default agents** (no override): Eject (export as `.md`), Disable
467
465
  - **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
@@ -489,9 +487,7 @@ Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
489
487
 
490
488
  ## Concurrency
491
489
 
492
- Background agents are subject to a configurable concurrency limit (default: 4). Excess agents are automatically queued and start as running agents complete. The widget shows queued agents as a collapsed count.
493
-
494
- Foreground agents bypass the queue — they block the parent anyway.
490
+ Agent tool calls bypass the configurable background concurrency queue (default: 4), preserving immediate detached execution. Other programmatic background spawns may still be queued; the widget shows queued agents as a collapsed count.
495
491
 
496
492
  ## Join Strategies
497
493
 
@@ -14,23 +14,22 @@ If the target is already known, use a direct tool — `read` for a known path, `
14
14
  ## Usage notes
15
15
 
16
16
  - Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
17
- - When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently. If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Agent tool use content blocks.
18
- - When the agent is done, it returns a single message back to you. The result is not visible to the user — to show the user, send a text message with a concise summary.
19
- - Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting the work as done.
20
- - Agents run in the background by default. When an agent runs in the background, you will be automatically notified when it completes — do NOT sleep, poll, or proactively check on its progress. Continue with other work or respond to the user instead.
21
- - **Foreground vs background**: Pass `run_in_background: false` only when your very next action depends on the agent's result and nothing else could usefully happen while it runs — e.g., a research agent whose finding gates the edit you're about to make. Otherwise let it run in the background (the default) — this includes fire-and-forget work, independent investigations, and anything where the user might hand you something else in the meantime. Wanting the result "next" is not enough on its own.
22
- - **Don't race**: after launching a background agent, you know nothing about its results. Never fabricate or predict them in any format — not as prose, summary, or structured output. The completion notification arrives in a later turn; it is never something you write yourself. If the user asks before it lands, say the agent is still running — give status, not a guess.
23
- - Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
24
- - Use steer_subagent to send mid-run messages to a running background agent.
17
+ - Every Agent call is detached and returns an agent ID immediately. run_in_background is accepted for compatibility but has no effect. When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.
18
+ - Results arrive via the completion notification; call get_subagent_result once per completed task ID with wait omitted or false to render the native expandable result. Agent results are not returned inline — never poll or sleep waiting for them.
19
+ - For nonblocking grouped notification, set wait: true. Omit wait_group for a one-agent implicit group, or create an explicit group with subagent_wait_group and seal it (or set wait_group_done: true on the final Agent call).
20
+ - Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
21
+ - Use resume with an agent ID to continue a previous agent's work; resume is detached and also returns immediately.
22
+ - Use steer_subagent to send mid-run messages to a running agent.
25
23
  - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
26
24
  - If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
27
25
  - Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
28
26
  - Use thinking to control extended thinking level.
29
- - Use inherit_context if the agent needs the parent conversation history.{{isolationGuideline}}{{scheduleGuideline}}
27
+ - Use inherit_context if the agent needs the parent conversation history.
28
+ - Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.{{scheduleGuideline}}
30
29
 
31
30
  ## Writing the prompt
32
31
 
33
- Brief the agent like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.
32
+ Provide clear, detailed prompts so the agent can work autonomously. Brief it like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.
34
33
  - Explain what you're trying to accomplish and why.
35
34
  - Describe what you've already learned or ruled out.
36
35
  - Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esso0428/pi-subagents",
3
- "version": "0.17.28",
3
+ "version": "0.17.29",
4
4
  "description": "A pi extension that brings smart Claude Code-style autonomous sub-agents to pi, with npm:pi-subagents-style JSON agent overrides.",
5
5
  "author": "ESSO0428",
6
6
  "repository": {
@@ -638,32 +638,6 @@ export class AgentManager {
638
638
  }
639
639
  }
640
640
 
641
- /**
642
- * Spawn an agent and wait for completion (foreground use).
643
- * Foreground agents bypass the concurrency queue.
644
- * Returns { id, record } so callers can access the agent ID.
645
- *
646
- * @param onSpawned - Called synchronously after spawn(), before onSessionCreated fires.
647
- * Use this to set record.outputFile so streamToOutputFile can pick it up.
648
- */
649
- async spawnAndWait(
650
- pi: ExtensionAPI,
651
- ctx: ExtensionContext,
652
- type: SubagentType,
653
- prompt: string,
654
- options: Omit<SpawnOptions, "isBackground">,
655
- onSpawned?: (id: string) => void,
656
- ): Promise<{ id: string; record: AgentRecord }> {
657
- const id = this.spawn(pi, ctx, type, prompt, {
658
- ...options,
659
- isBackground: false,
660
- onSpawned,
661
- });
662
- const record = this.agents.get(id)!;
663
- await record.promise;
664
- return { id, record };
665
- }
666
-
667
641
  /**
668
642
  * Resume an existing agent session with a new prompt.
669
643
  */
@@ -680,11 +654,15 @@ export class AgentManager {
680
654
  record.completedAt = undefined;
681
655
  record.result = undefined;
682
656
  record.error = undefined;
657
+ record.resultConsumed = false;
658
+ record.isBackground = true;
659
+ this.onStart?.(record);
683
660
  const resumedModel = record.session.model;
684
661
  record.invocation = {
685
662
  ...(record.invocation ?? {}),
686
663
  ...(resumedModel && { effectiveModelName: resumedModel.name ?? resumedModel.id }),
687
664
  effectiveThinking: record.session.thinkingLevel,
665
+ runInBackground: true,
688
666
  };
689
667
  this.checkpoint(record);
690
668
 
@@ -709,11 +687,13 @@ export class AgentManager {
709
687
  record.result = text;
710
688
  record.completedAt = Date.now();
711
689
  this.checkpoint(record);
690
+ try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
712
691
  } catch (err) {
713
692
  record.status = "error";
714
693
  record.error = err instanceof Error ? err.message : String(err);
715
694
  record.completedAt = Date.now();
716
695
  this.checkpoint(record);
696
+ try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
717
697
  }
718
698
 
719
699
  return record;
package/src/index.ts CHANGED
@@ -38,7 +38,6 @@ import {
38
38
  type AgentDetails,
39
39
  AgentWidget,
40
40
  buildInvocationTags,
41
- describeActivity,
42
41
  fgPreservingNestedStyles,
43
42
  formatDuration,
44
43
  formatMs,
@@ -81,7 +80,7 @@ function formatLifetimeTokens(o: { lifetimeUsage: LifetimeUsage }): string {
81
80
 
82
81
  /**
83
82
  * Create an AgentActivity state and spawn callbacks for tracking tool usage.
84
- * Used by both foreground and background paths to avoid duplication.
83
+ * Used by the detached Agent path for activity and usage tracking.
85
84
  */
86
85
  function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
87
86
  const state: AgentActivity = {
@@ -189,27 +188,6 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number): stri
189
188
  ].filter(Boolean).join('\n');
190
189
  }
191
190
 
192
- /** Build AgentDetails from a base + record-specific fields. */
193
- function buildDetails(
194
- base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
195
- record: { toolUses: number; startedAt: number; completedAt?: number; status: string; error?: string; id?: string; session?: any; lifetimeUsage: LifetimeUsage },
196
- activity?: AgentActivity,
197
- overrides?: Partial<AgentDetails>,
198
- ): AgentDetails {
199
- return {
200
- ...base,
201
- toolUses: record.toolUses,
202
- tokens: formatLifetimeTokens(record),
203
- turnCount: activity?.turnCount,
204
- maxTurns: activity?.maxTurns,
205
- durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
206
- status: record.status as AgentDetails["status"],
207
- agentId: record.id,
208
- error: record.error,
209
- ...overrides,
210
- };
211
- }
212
-
213
191
  /** Build notification details for the custom message renderer. */
214
192
  function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, activity?: AgentActivity): NotificationDetails {
215
193
  const totalTokens = getLifetimeTotal(record.lifetimeUsage);
@@ -871,7 +849,7 @@ export default function (pi: ExtensionAPI) {
871
849
  description:
872
850
  'Opt-in only — fire later instead of now. Omit to run immediately (the default, almost always correct). ' +
873
851
  'Formats: 6-field cron ("0 0 9 * * 1" = 9am Mon), interval ("5m"/"1h"), one-shot ("+10m" or ISO). ' +
874
- 'Forces run_in_background; incompatible with inherit_context and resume. Returns job ID.',
852
+ 'Scheduled agents run detached; run_in_background has no effect. Incompatible with inherit_context and resume. Returns job ID.',
875
853
  }),
876
854
  ),
877
855
  };
@@ -892,10 +870,10 @@ Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.
892
870
 
893
871
  Notes:
894
872
  - description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
895
- - Parallel work: one message, multiple Agent calls, run_in_background: true on each. You are notified when background agents finish — never poll or sleep.
873
+ - Every Agent call is detached and returns an agent ID immediately. run_in_background is accepted for compatibility but has no effect. You are notified when agents finish — never poll or sleep.
896
874
  - For nonblocking grouped notification, add wait: true. Use subagent_wait_group to create/update/seal explicit groups; wait_group_done seals after the final spawn.
897
- - The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
898
- - resume continues a previous agent by ID; steer_subagent messages a running one.
875
+ - Results arrive via the completion notification; then call get_subagent_result once per task ID with wait omitted or false to render the native expandable result. The result is not shown inline. Verify an agent's claimed code changes before reporting work done.
876
+ - resume continues a previous agent by ID and is also detached; steer_subagent messages a running one.
899
877
  - isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.`;
900
878
 
901
879
  const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
@@ -914,15 +892,12 @@ If the target is already known, use a direct tool — \`read\` for a known path,
914
892
  ## Usage notes
915
893
 
916
894
  - Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
917
- - When you launch multiple agents for independent work, send them in a single message with multiple tool uses, with run_in_background: true on each, so they run concurrently. If the user specifies that they want agents run "in parallel", you MUST send a single message with multiple tool calls. Foreground calls run sequentially — only one executes at a time.
918
- - When the agent is done, it returns a single message back to you. The result is not visible to the user — to show the user, send a text message with a concise summary.
895
+ - Every Agent call is detached and returns an agent ID immediately. run_in_background is accepted for compatibility but has no effect. When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.
896
+ - Results arrive via the completion notification; call get_subagent_result once per completed task ID with wait omitted or false to render the native expandable result. Agent results are not returned inline — never poll or sleep waiting for them.
897
+ - For nonblocking grouped notification, set wait: true. Omit wait_group for a one-agent implicit group, or create an explicit group with subagent_wait_group and seal it (or set wait_group_done: true on the final Agent call).
919
898
  - Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
920
- - Use run_in_background for work you don't need immediately. You will be notified when it completes — do NOT poll or sleep waiting for it. Continue with other work or respond to the user instead.
921
- - For nonblocking grouped notification, set wait: true with run_in_background: true. Omit wait_group for a one-agent implicit group, or create an explicit group with subagent_wait_group and seal it (or set wait_group_done: true on the final Agent call).
922
- - When a background or wait-group completion notification arrives, call get_subagent_result once per completed task-id with wait omitted or false before summarizing if the user needs the outputs. This preserves native expandable Get Subagent Result UI without blocking.
923
- - Foreground vs background: use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
924
- - Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
925
- - Use steer_subagent to send mid-run messages to a running background agent.
899
+ - Use resume with an agent ID to continue a previous agent's work; resume is detached and also returns immediately.
900
+ - Use steer_subagent to send mid-run messages to a running agent.
926
901
  - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
927
902
  - If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
928
903
  - Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
@@ -998,9 +973,9 @@ Terse command-style prompts produce shallow, generic work.
998
973
  promptGuidelines: [
999
974
  "Use Agent with specialized agents when the task matches an agent type's description. Subagents are valuable for parallelizing independent queries or for protecting the main context window from excessive results, but should not be used excessively when not needed. Importantly, avoid duplicating work that subagents are already doing — if you delegate research to a subagent, do not also perform the same searches yourself.",
1000
975
  "For broad codebase exploration or research, spawn Agent with an appropriate subagent_type (e.g. Explore). Otherwise use direct tools (read, grep, find) when the target is already known.",
1001
- "When an agent runs in the background, you will be notified on completion — do not poll or sleep waiting for it. Continue with other work instead.",
1002
- "For a nonblocking grouped notification, use wait: true with run_in_background: true; create/update/seal explicit groups with subagent_wait_group.",
1003
- "When a background or wait-group completion notification arrives, call get_subagent_result once per completed task-id with wait omitted or false before summarizing if the user needs the outputs; this preserves native expandable result UI without blocking.",
976
+ "Every Agent call is detached and returns an agent ID immediately. run_in_background is accepted for compatibility but has no effect; continue with other work instead of waiting.",
977
+ "Use wait: true to receive one grouped completion notification; create/update/seal explicit groups with subagent_wait_group.",
978
+ "Results arrive via the completion notification. Then call get_subagent_result once per completed task ID with wait omitted or false before summarizing if the user needs the outputs; this preserves native expandable result UI without blocking.",
1004
979
  "Trust but verify: an agent's summary describes intent, not outcome. When an agent writes or edits code, check the actual changes before reporting work as done.",
1005
980
  ],
1006
981
  parameters: Type.Object({
@@ -1032,12 +1007,12 @@ Terse command-style prompts produce shallow, generic work.
1032
1007
  ),
1033
1008
  run_in_background: Type.Optional(
1034
1009
  Type.Boolean({
1035
- description: "Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
1010
+ description: "Accepted for compatibility; it has no effect. Agent calls always detach, return an agent ID immediately, and notify on completion.",
1036
1011
  }),
1037
1012
  ),
1038
1013
  wait: Type.Optional(
1039
1014
  Type.Boolean({
1040
- description: "With run_in_background: true, suppress individual completion notification and wait for a sealed group notification. This never blocks execution.",
1015
+ description: "Suppress the individual completion notification and wait for a sealed group notification. This never blocks execution.",
1041
1016
  }),
1042
1017
  ),
1043
1018
  wait_group: Type.Optional(
@@ -1164,7 +1139,7 @@ Terse command-style prompts produce shallow, generic work.
1164
1139
 
1165
1140
  // ---- Execute ----
1166
1141
 
1167
- execute: async (toolCallId, params, signal, onUpdate, ctx) => {
1142
+ execute: async (toolCallId, params, _signal, _onUpdate, ctx) => {
1168
1143
  // Ensure we have UI context for widget rendering
1169
1144
  widget.setUICtx(ctx.ui as UICtx);
1170
1145
 
@@ -1174,7 +1149,6 @@ Terse command-style prompts produce shallow, generic work.
1174
1149
  const rawType = params.subagent_type as SubagentType;
1175
1150
  const resolved = resolveType(rawType);
1176
1151
  const subagentType = resolved ?? "general-purpose";
1177
- const fellBack = resolved === undefined;
1178
1152
 
1179
1153
  const displayName = getDisplayName(subagentType);
1180
1154
 
@@ -1226,7 +1200,6 @@ Terse command-style prompts produce shallow, generic work.
1226
1200
 
1227
1201
  const thinking = resolvedConfig.thinking;
1228
1202
  const inheritContext = resolvedConfig.inheritContext;
1229
- const runInBackground = resolvedConfig.runInBackground;
1230
1203
  const wait = params.wait === true;
1231
1204
  const waitGroup = typeof params.wait_group === "string" ? params.wait_group.trim() : undefined;
1232
1205
  const waitGroupDone = params.wait_group_done === true;
@@ -1274,7 +1247,8 @@ Terse command-style prompts produce shallow, generic work.
1274
1247
  maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
1275
1248
  isolated,
1276
1249
  inheritContext,
1277
- runInBackground,
1250
+ // Agent calls are always detached; run_in_background is a compatibility no-op.
1251
+ runInBackground: true,
1278
1252
  isolation,
1279
1253
  };
1280
1254
  // Tool-result render shows the mode label too; viewer's header already does.
@@ -1292,9 +1266,6 @@ Terse command-style prompts produce shallow, generic work.
1292
1266
  if ((waitGroup || waitGroupDone) && !wait) {
1293
1267
  return textResult("wait_group and wait_group_done require wait: true.");
1294
1268
  }
1295
- if (wait && !runInBackground) {
1296
- return textResult("wait: true requires run_in_background: true; it controls nonblocking background notifications.");
1297
- }
1298
1269
  if (wait && params.schedule) {
1299
1270
  return textResult("Cannot combine wait: true with schedule — scheduled jobs are separate future runs.");
1300
1271
  }
@@ -1313,9 +1284,6 @@ Terse command-style prompts produce shallow, generic work.
1313
1284
  if (params.inherit_context) {
1314
1285
  return textResult("Cannot combine `schedule` with `inherit_context` — there is no parent conversation at fire time.");
1315
1286
  }
1316
- if (params.run_in_background === false) {
1317
- return textResult("Cannot combine `schedule` with `run_in_background: false` — scheduled jobs always run in background.");
1318
- }
1319
1287
  if (!scheduler.isActive()) {
1320
1288
  return textResult("Scheduler is not active in this session yet. Try again after the session has fully started.");
1321
1289
  }
@@ -1343,7 +1311,7 @@ Terse command-style prompts produce shallow, generic work.
1343
1311
  }
1344
1312
  }
1345
1313
 
1346
- // Resume existing agent
1314
+ // Resume existing agent without blocking the parent tool call.
1347
1315
  if (params.resume) {
1348
1316
  const existing = manager.getRecord(params.resume);
1349
1317
  if (!existing) {
@@ -1352,241 +1320,133 @@ Terse command-style prompts produce shallow, generic work.
1352
1320
  if (!existing.session) {
1353
1321
  return textResult(`Agent "${params.resume}" has no active session to resume.`);
1354
1322
  }
1355
- const record = await manager.resume(params.resume, params.prompt, signal);
1356
- if (!record) {
1357
- return textResult(`Failed to resume agent "${params.resume}".`);
1358
- }
1359
- // A failed resume surfaces the error, plus any partial output THIS
1360
- // resume produced (never the previous turn's answer, #144).
1361
- if (record.status === "error") {
1362
- return textResult(`Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(detailBase, record));
1363
- }
1364
- return textResult(
1365
- record.result?.trim() || "No output.",
1366
- buildDetails(detailBase, record),
1367
- );
1368
- }
1369
-
1370
- // Background execution
1371
- if (runInBackground) {
1372
- const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(effectiveMaxTurns);
1373
-
1374
- // Wrap onSessionCreated to wire output file streaming.
1375
- // The callback reads the transcript paths installed synchronously by
1376
- // onSpawned before the agent can queue or start.
1377
- let id = "";
1378
- let effectiveWaitGroupId: string | undefined;
1379
- let implicitWaitGroupId: string | undefined;
1380
- if (wait) {
1381
- if (waitGroup) {
1382
- if (!waitGroups.hasGroup(waitGroup)) {
1383
- return textResult(`Wait group not found: "${waitGroup}". Create it with subagent_wait_group first.`);
1384
- }
1385
- effectiveWaitGroupId = waitGroup;
1386
- } else {
1387
- implicitWaitGroupId = waitGroups.create(params.description);
1388
- effectiveWaitGroupId = implicitWaitGroupId;
1389
- }
1390
- }
1391
- const joinMode = wait ? undefined : resolveJoinMode(defaultJoinMode, true);
1392
- const origBgOnSession = bgCallbacks.onSessionCreated;
1393
- bgCallbacks.onSessionCreated = (session: any) => {
1394
- origBgOnSession(session);
1395
- const rec = manager.getRecord(id);
1396
- if (rec?.outputFile) {
1397
- rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd, rec.historyFile);
1398
- }
1399
- };
1400
-
1401
- try {
1402
- id = manager.spawn(pi, ctx, subagentType, params.prompt, {
1403
- description: params.description,
1404
- model,
1405
- maxTurns: effectiveMaxTurns,
1406
- isolated,
1407
- inheritContext,
1408
- thinkingLevel: thinking,
1409
- isBackground: true,
1410
- isolation,
1411
- invocation: agentInvocation,
1412
- waitGroupId: effectiveWaitGroupId,
1413
- onSpawned: (spawnedId) => {
1414
- id = spawnedId;
1415
- attachTranscript(manager.getRecord(spawnedId), spawnedId);
1416
- if (effectiveWaitGroupId) waitGroups.addAgent(effectiveWaitGroupId, spawnedId);
1417
- },
1418
- ...bgCallbacks,
1419
- });
1420
- } catch (err) {
1421
- if (effectiveWaitGroupId && id) waitGroups.removeAgent(effectiveWaitGroupId, id);
1422
- if (implicitWaitGroupId) waitGroups.discard(implicitWaitGroupId);
1423
- return textResult(err instanceof Error ? err.message : String(err));
1424
- }
1425
-
1426
- // Set join metadata after spawn. Transcript metadata was installed by
1427
- // the manager's synchronous onSpawned callback before this point.
1428
- const record = manager.getRecord(id);
1429
- if (record && joinMode) {
1430
- record.joinMode = joinMode;
1431
- record.toolCallId = toolCallId;
1432
- }
1433
1323
 
1434
- if (effectiveWaitGroupId) {
1435
- if (implicitWaitGroupId || waitGroupDone) waitGroups.seal(effectiveWaitGroupId);
1436
- } else if (joinMode == null || joinMode === 'async') {
1437
- // Foreground/no join mode or explicit async — not part of any batch
1438
- } else {
1439
- // smart or group — add to current batch
1440
- currentBatchAgents.push({ id, joinMode });
1441
- // Debounce: reset timer on each new agent so parallel tool calls
1442
- // dispatched across multiple event loop ticks are captured together
1443
- if (batchFinalizeTimer) clearTimeout(batchFinalizeTimer);
1444
- batchFinalizeTimer = setTimeout(finalizeBatch, 100);
1445
- }
1446
-
1447
- agentActivity.set(id, bgState);
1324
+ // A resumed turn has a new unread result and follows the same detached
1325
+ // completion-notification flow as a fresh Agent call.
1326
+ existing.resultConsumed = false;
1327
+ existing.toolCallId = toolCallId;
1328
+ void manager.resume(params.resume, params.prompt);
1448
1329
  widget.ensureTimer();
1449
1330
  widget.update();
1450
1331
 
1451
- // Emit created event
1452
- pi.events.emit("subagents:created", {
1453
- id,
1454
- type: subagentType,
1455
- description: params.description,
1456
- isBackground: true,
1457
- });
1458
-
1459
- const isQueued = record?.status === "queued";
1460
1332
  return textResult(
1461
- `Agent ${isQueued ? "queued" : "started"} in background.\n` +
1462
- `Agent ID: ${id}\n` +
1333
+ `Agent resumed in background.\n` +
1334
+ `Agent ID: ${params.resume}\n` +
1463
1335
  `Type: ${displayName}\n` +
1464
- `Description: ${params.description}\n` +
1465
- (record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
1466
- (isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
1467
- (effectiveWaitGroupId
1468
- ? `\nWait group: ${effectiveWaitGroupId}${implicitWaitGroupId || waitGroupDone ? " (sealed)" : " (open — seal it with subagent_wait_group)"}.\n` +
1469
- `You will receive one grouped notification when the sealed wait group completes.\n`
1470
- : `\nYou will be notified when this agent completes.\n`) +
1471
- `After the completion notification, call get_subagent_result with wait omitted or false to render native expandable results; use steer_subagent to send messages while it is running.\n` +
1336
+ `Description: ${params.description}\n\n` +
1337
+ `You will receive a completion notification when this agent finishes.\n` +
1338
+ `Results arrive via that notification; after it, call get_subagent_result with wait omitted or false to render native expandable results.\n` +
1472
1339
  `Do not duplicate this agent's work.`,
1473
- { ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId: id },
1340
+ { ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId: params.resume },
1474
1341
  );
1475
1342
  }
1476
1343
 
1477
- // Foreground (synchronous) execution — stream progress via onUpdate
1478
- let spinnerFrame = 0;
1479
- const startedAt = Date.now();
1480
- let fgId: string | undefined;
1481
-
1482
- const streamUpdate = () => {
1483
- const details: AgentDetails = {
1484
- ...detailBase,
1485
- toolUses: fgState.toolUses,
1486
- tokens: formatLifetimeTokens(fgState),
1487
- turnCount: fgState.turnCount,
1488
- maxTurns: fgState.maxTurns,
1489
- durationMs: Date.now() - startedAt,
1490
- status: "running",
1491
- activity: describeActivity(fgState.activeTools, fgState.responseText),
1492
- spinnerFrame: spinnerFrame % SPINNER.length,
1493
- };
1494
- onUpdate?.({
1495
- content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
1496
- details: details as any,
1497
- });
1498
- };
1499
-
1500
- const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(effectiveMaxTurns, streamUpdate);
1501
-
1502
- // Wire session creation: register in widget + stream to output file.
1503
- // The output file path is set synchronously after spawn (below),
1504
- // before onSessionCreated fires — same pattern as background agents.
1505
- const origOnSession = fgCallbacks.onSessionCreated;
1506
- fgCallbacks.onSessionCreated = (session: any) => {
1507
- origOnSession(session);
1508
- for (const a of manager.listAgents()) {
1509
- if (a.session === session) {
1510
- fgId = a.id;
1511
- agentActivity.set(a.id, fgState);
1512
- widget.ensureTimer();
1513
- widget.update();
1514
- break;
1344
+ // All fresh Agent calls use the detached path. `run_in_background` is
1345
+ // intentionally ignored; bypassQueue preserves the historical behavior
1346
+ // where foreground calls started immediately despite the background cap.
1347
+ const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(effectiveMaxTurns);
1348
+
1349
+ // Wrap onSessionCreated to wire output file streaming.
1350
+ // The callback reads the transcript paths installed synchronously by
1351
+ // onSpawned before the agent can queue or start.
1352
+ let id = "";
1353
+ let effectiveWaitGroupId: string | undefined;
1354
+ let implicitWaitGroupId: string | undefined;
1355
+ if (wait) {
1356
+ if (waitGroup) {
1357
+ if (!waitGroups.hasGroup(waitGroup)) {
1358
+ return textResult(`Wait group not found: "${waitGroup}". Create it with subagent_wait_group first.`);
1515
1359
  }
1360
+ effectiveWaitGroupId = waitGroup;
1361
+ } else {
1362
+ implicitWaitGroupId = waitGroups.create(params.description);
1363
+ effectiveWaitGroupId = implicitWaitGroupId;
1516
1364
  }
1517
- // Stream conversation to output file (foreground agent logging)
1518
- if (fgId) {
1519
- const rec = manager.getRecord(fgId);
1520
- if (rec?.outputFile) {
1521
- rec.outputCleanup = streamToOutputFile(session, rec.outputFile, fgId, ctx.cwd, rec.historyFile);
1522
- }
1365
+ }
1366
+ const joinMode = wait ? undefined : resolveJoinMode(defaultJoinMode, true);
1367
+ const origBgOnSession = bgCallbacks.onSessionCreated;
1368
+ bgCallbacks.onSessionCreated = (session: any) => {
1369
+ origBgOnSession(session);
1370
+ const rec = manager.getRecord(id);
1371
+ if (rec?.outputFile) {
1372
+ rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd, rec.historyFile);
1523
1373
  }
1524
1374
  };
1525
1375
 
1526
- // Animate spinner at ~80ms (smooth rotation through 10 braille frames)
1527
- const spinnerInterval = setInterval(() => {
1528
- spinnerFrame++;
1529
- streamUpdate();
1530
- }, 80);
1531
-
1532
- streamUpdate();
1533
-
1534
- let record: AgentRecord;
1535
1376
  try {
1536
- const fgResult = await manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
1377
+ id = manager.spawn(pi, ctx, subagentType, params.prompt, {
1537
1378
  description: params.description,
1538
1379
  model,
1539
1380
  maxTurns: effectiveMaxTurns,
1540
1381
  isolated,
1541
1382
  inheritContext,
1542
1383
  thinkingLevel: thinking,
1384
+ isBackground: true,
1385
+ bypassQueue: true,
1543
1386
  isolation,
1544
1387
  invocation: agentInvocation,
1545
- signal,
1546
- ...fgCallbacks,
1547
- }, (fgAgentId) => {
1548
- // onSpawned: called synchronously after spawn, before onSessionCreated fires.
1549
- // Set up the output file so streamToOutputFile can pick it up.
1550
- const fgRec = manager.getRecord(fgAgentId);
1551
- attachTranscript(fgRec, fgAgentId);
1388
+ waitGroupId: effectiveWaitGroupId,
1389
+ onSpawned: (spawnedId) => {
1390
+ id = spawnedId;
1391
+ attachTranscript(manager.getRecord(spawnedId), spawnedId);
1392
+ if (effectiveWaitGroupId) waitGroups.addAgent(effectiveWaitGroupId, spawnedId);
1393
+ },
1394
+ ...bgCallbacks,
1552
1395
  });
1553
- record = fgResult.record;
1554
1396
  } catch (err) {
1555
- clearInterval(spinnerInterval);
1397
+ if (effectiveWaitGroupId && id) waitGroups.removeAgent(effectiveWaitGroupId, id);
1398
+ if (implicitWaitGroupId) waitGroups.discard(implicitWaitGroupId);
1556
1399
  return textResult(err instanceof Error ? err.message : String(err));
1557
1400
  }
1558
1401
 
1559
- clearInterval(spinnerInterval);
1560
-
1561
- // Clean up foreground agent from widget
1562
- if (fgId) {
1563
- agentActivity.delete(fgId);
1564
- widget.markFinished(fgId);
1402
+ // Set join metadata after spawn. Transcript metadata was installed by
1403
+ // the manager's synchronous onSpawned callback before this point.
1404
+ const record = manager.getRecord(id);
1405
+ if (record && joinMode) {
1406
+ record.joinMode = joinMode;
1407
+ record.toolCallId = toolCallId;
1565
1408
  }
1566
1409
 
1567
- // Get final token count
1568
- const tokenText = formatLifetimeTokens(fgState);
1569
-
1570
- const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
1410
+ if (effectiveWaitGroupId) {
1411
+ if (implicitWaitGroupId || waitGroupDone) waitGroups.seal(effectiveWaitGroupId);
1412
+ } else if (joinMode == null || joinMode === 'async') {
1413
+ // No join mode or explicit async — not part of any batch.
1414
+ } else {
1415
+ // Smart or group — add to current batch.
1416
+ currentBatchAgents.push({ id, joinMode });
1417
+ // Debounce: reset timer on each new agent so parallel tool calls
1418
+ // dispatched across multiple event loop ticks are captured together.
1419
+ if (batchFinalizeTimer) clearTimeout(batchFinalizeTimer);
1420
+ batchFinalizeTimer = setTimeout(finalizeBatch, 100);
1421
+ }
1571
1422
 
1572
- // "general-purpose" may itself be unregistered (defaults disabled, no
1573
- // user override) — getConfig then uses the hardcoded fallback config.
1574
- const fallbackNote = fellBack
1575
- ? `Note: Unknown agent type "${rawType}" — using ${resolveType("general-purpose") ? "general-purpose" : "the fallback agent config"}.\n\n`
1576
- : "";
1423
+ agentActivity.set(id, bgState);
1424
+ widget.ensureTimer();
1425
+ widget.update();
1577
1426
 
1578
- if (record.status === "error") {
1579
- // Error headline + any partial output the run produced before failing.
1580
- return textResult(`${fallbackNote}Agent failed: ${record.error}${partialOutputSuffix(record)}`, details);
1581
- }
1427
+ // Emit created event.
1428
+ pi.events.emit("subagents:created", {
1429
+ id,
1430
+ type: subagentType,
1431
+ description: params.description,
1432
+ isBackground: true,
1433
+ });
1582
1434
 
1583
- const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
1584
- const statsParts = [`${record.toolUses} tool uses`];
1585
- if (tokenText) statsParts.push(tokenText);
1435
+ const isQueued = record?.status === "queued";
1586
1436
  return textResult(
1587
- `${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
1588
- (record.result?.trim() || "No output."),
1589
- details,
1437
+ `Agent ${isQueued ? "queued" : "started"} in background.\n` +
1438
+ `Agent ID: ${id}\n` +
1439
+ `Type: ${displayName}\n` +
1440
+ `Description: ${params.description}\n` +
1441
+ (record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
1442
+ (isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
1443
+ (effectiveWaitGroupId
1444
+ ? `\nWait group: ${effectiveWaitGroupId}${implicitWaitGroupId || waitGroupDone ? " (sealed)" : " (open — seal it with subagent_wait_group)"}.\n` +
1445
+ `You will receive one grouped completion notification when the sealed wait group completes.\n`
1446
+ : `\nYou will receive a completion notification when this agent completes.\n`) +
1447
+ `Results arrive via the completion notification; after it, call get_subagent_result with wait omitted or false to render native expandable results.\n` +
1448
+ `Do not duplicate this agent's work.`,
1449
+ { ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId: id },
1590
1450
  );
1591
1451
  },
1592
1452
  }));
@@ -1597,7 +1457,7 @@ Terse command-style prompts produce shallow, generic work.
1597
1457
  name: SUBAGENT_TOOL_NAMES.WAIT_GROUP,
1598
1458
  label: "Subagent Wait Group",
1599
1459
  description:
1600
- "Create, update, or seal a nonblocking wait group for background Agent calls. " +
1460
+ "Create, update, or seal a nonblocking wait group for detached Agent calls. " +
1601
1461
  "A sealed group sends one completion notification after all member agents finish.",
1602
1462
  promptSnippet: "Create, update, or seal a grouped subagent completion notification",
1603
1463
  parameters: Type.Object({
@@ -1626,8 +1486,8 @@ Terse command-style prompts produce shallow, generic work.
1626
1486
  `Created subagent wait group.\n` +
1627
1487
  `Group ID: ${createdId}\n` +
1628
1488
  `Summary: ${summary}\n\n` +
1629
- `Use Agent with run_in_background: true, wait: true, wait_group: "${createdId}". ` +
1630
- `Seal the group after adding members.`,
1489
+ `Use Agent with wait: true, wait_group: "${createdId}". ` +
1490
+ `Seal the group after adding members. Agent calls are detached automatically.`,
1631
1491
  );
1632
1492
  }
1633
1493
  if (action === "update") {
@@ -1666,7 +1526,7 @@ Terse command-style prompts produce shallow, generic work.
1666
1526
  name: SUBAGENT_TOOL_NAMES.GET_RESULT,
1667
1527
  label: "Get Agent Result",
1668
1528
  description:
1669
- "Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background.",
1529
+ "Check status and retrieve results from a detached agent. Use the agent ID returned by Agent.",
1670
1530
  promptSnippet: "Check status and retrieve results from a background agent",
1671
1531
  parameters: Type.Object({
1672
1532
  agent_id: Type.String({
@@ -2341,7 +2201,7 @@ extensions: <true (inherit all MCP/extension tools), false (none), or comma-sepa
2341
2201
  skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
2342
2202
  disallowed_tools: <comma-separated tool names to block, even if otherwise available. Omit for none>
2343
2203
  inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
2344
- run_in_background: <true to run in background by default. Default: false>
2204
+ run_in_background: <accepted for compatibility; has no effect>
2345
2205
  output_transcript: <false to write no transcript file or path for this agent. Independent of persist_session. Default: true>
2346
2206
  isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
2347
2207
  memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none>
@@ -2363,23 +2223,33 @@ Guidelines for choosing settings:
2363
2223
 
2364
2224
  Write the file using the write tool. Only write the file, nothing else.`;
2365
2225
 
2366
- const { record } = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
2367
- description: `Generate ${name} agent`,
2368
- maxTurns: 5,
2369
- });
2370
-
2371
- if (record.status === "error") {
2372
- ctx.ui.notify(`Generation failed: ${record.error}`, "warning");
2226
+ let id: string;
2227
+ try {
2228
+ id = manager.spawn(pi, ctx, "general-purpose", generatePrompt, {
2229
+ description: `Generate ${name} agent`,
2230
+ maxTurns: 5,
2231
+ isBackground: true,
2232
+ bypassQueue: true,
2233
+ });
2234
+ } catch (err) {
2235
+ ctx.ui.notify(`Generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
2373
2236
  return;
2374
2237
  }
2375
2238
 
2376
- reloadCustomAgents();
2239
+ const record = manager.getRecord(id);
2240
+ void record?.promise?.then(() => {
2241
+ if (record.status === "error") {
2242
+ ctx.ui.notify(`Generation failed: ${record.error}`, "warning");
2243
+ return;
2244
+ }
2377
2245
 
2378
- if (existsSync(targetPath)) {
2379
- ctx.ui.notify(`Created ${targetPath}`, "info");
2380
- } else {
2381
- ctx.ui.notify("Agent generation completed but file was not created. Check the agent output.", "warning");
2382
- }
2246
+ reloadCustomAgents();
2247
+ if (existsSync(targetPath)) {
2248
+ ctx.ui.notify(`Created ${targetPath}`, "info");
2249
+ } else {
2250
+ ctx.ui.notify("Agent generation completed but file was not created. Check the agent output.", "warning");
2251
+ }
2252
+ });
2383
2253
  }
2384
2254
 
2385
2255
  async function showManualWizard(ctx: ExtensionCommandContext, targetDir: string) {
@@ -2548,7 +2418,7 @@ ${systemPrompt}
2548
2418
  {
2549
2419
  id: "widgetMode",
2550
2420
  label: "Widget",
2551
- description: "Above-editor agent widget: all = every agent; background = hide foreground (they already render inline); off = hide the widget.",
2421
+ description: "Above-editor agent widget: all = every agent; background = hide explicitly foreground programmatic runs; off = hide the widget.",
2552
2422
  currentValue: getWidgetMode(),
2553
2423
  values: ["all", "background", "off"],
2554
2424
  },
package/src/types.ts CHANGED
@@ -51,7 +51,7 @@ export interface AgentConfig {
51
51
  promptMode: "replace" | "append";
52
52
  /** Default for spawn: fork parent conversation. undefined = caller decides. */
53
53
  inheritContext?: boolean;
54
- /** Default for spawn: run in background. undefined = caller decides. */
54
+ /** Compatibility field; Agent calls always run detached regardless of this value. */
55
55
  runInBackground?: boolean;
56
56
  /** Default for spawn: no extension tools. undefined = caller decides. */
57
57
  isolated?: boolean;
@@ -147,6 +147,7 @@ export interface AgentInvocation {
147
147
  maxTurns?: number;
148
148
  isolated?: boolean;
149
149
  inheritContext?: boolean;
150
+ /** Effective detached execution mode; retained for persisted UI compatibility. */
150
151
  runInBackground?: boolean;
151
152
  isolation?: IsolationMode;
152
153
  }
@@ -33,6 +33,8 @@ export interface ConversationBlock {
33
33
  /** Compatibility-facing short alias used by the timeline renderer. */
34
34
  result?: ConversationToolResultSnapshot;
35
35
  toolStatus?: "pending" | "success" | "error";
36
+ /** Message is queued for delivery and has not yet entered the transcript. */
37
+ pending?: boolean;
36
38
  }
37
39
 
38
40
  export interface ConversationFormatOptions {
@@ -21,15 +21,17 @@ export function renderConversationRoleHeader(
21
21
  theme: ConversationRoleTheme,
22
22
  ): string {
23
23
  const label = conversationRoleLabel(block);
24
- const color = block.kind === "tool"
25
- ? "toolTitle"
26
- : block.role === "user"
27
- ? "userMessageText"
28
- : block.role === "assistant"
29
- ? "accent"
30
- : block.role === "custom"
31
- ? "customMessageLabel"
32
- : "muted";
24
+ const color = block.pending
25
+ ? "dim"
26
+ : block.kind === "tool"
27
+ ? "toolTitle"
28
+ : block.role === "user"
29
+ ? "userMessageText"
30
+ : block.role === "assistant"
31
+ ? "accent"
32
+ : block.role === "custom"
33
+ ? "customMessageLabel"
34
+ : "muted";
33
35
  const detail = block.kind === "tool" && block.toolCallId
34
36
  ? theme.fg("dim", ` · ${block.toolCallId}`)
35
37
  : "";
@@ -326,7 +326,7 @@ export class ConversationTimeline implements Component {
326
326
  try {
327
327
  const markdown = new Markdown(block.markdown || "∅", 2, 0, getMarkdownTheme(), {
328
328
  color: (text) => this.theme.fg(
329
- block.role === "user" ? "userMessageText" : block.role === "meta" ? "muted" : "text",
329
+ block.pending ? "dim" : block.role === "user" ? "userMessageText" : block.role === "meta" ? "muted" : "text",
330
330
  text,
331
331
  ),
332
332
  });
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import { copyToClipboard } from "@earendil-works/pi-coding-agent";
10
- import { type Component, Input, Key, matchesKey, type TUI, type TuiMouseEvent, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { type Component, Editor, Key, matchesKey, type TUI, type TuiMouseEvent, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
11
  import type { AgentRecord } from "../types.js";
12
12
  import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
13
13
  import type { Theme } from "./agent-widget.js";
@@ -68,7 +68,10 @@ function renderScrollMoreRule(
68
68
  }
69
69
 
70
70
  /** The live fields needed by the viewer; historical viewers use a static source. */
71
- export type ConversationSource = Pick<AgentSession, "messages" | "subscribe">;
71
+ export type ConversationSource = Pick<AgentSession, "messages" | "subscribe"> & {
72
+ getSteeringMessages?: () => readonly string[];
73
+ clearQueue?: () => { steering: string[]; followUp: string[] };
74
+ };
72
75
 
73
76
  export function createStaticConversationSource(
74
77
  messages: AgentSession["messages"],
@@ -80,6 +83,22 @@ type RenderLine = TimelineRenderLine;
80
83
 
81
84
  const FULL_TOOL_PREVIEW_MAX_CHARS = 500_000;
82
85
 
86
+ function pendingSteerBlockId(text: string, occurrence: number, usedIds: Set<string>): string {
87
+ let hash = 0;
88
+ for (let index = 0; index < text.length; index++) {
89
+ hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
90
+ }
91
+ const preferred = `pending-steer-${hash.toString(36)}-${occurrence}`;
92
+ let id = preferred;
93
+ let suffix = 2;
94
+ while (usedIds.has(id)) {
95
+ id = `${preferred}-${suffix}`;
96
+ suffix++;
97
+ }
98
+ usedIds.add(id);
99
+ return id;
100
+ }
101
+
83
102
  class FullToolPreview implements Component {
84
103
  private scrollOffset = 0;
85
104
  private pageSize = 1;
@@ -216,6 +235,7 @@ export class ConversationViewer implements Component {
216
235
  private scrollOffset = 0;
217
236
  private autoScroll = true;
218
237
  private unsubscribe: (() => void) | undefined;
238
+ private pendingSteers: string[] = [];
219
239
  private lastInnerW = 0;
220
240
  private closed = false;
221
241
  private stopArmed = false;
@@ -223,7 +243,7 @@ export class ConversationViewer implements Component {
223
243
  private hoveredPreview = false;
224
244
  private pressedHeaderAction: "preview" | "close" | undefined;
225
245
  private keys: ViewerKeys;
226
- private composer: Input | undefined;
246
+ private composer: Editor | undefined;
227
247
  private searchMode = false;
228
248
  private searchQuery = "";
229
249
  private searchMatches: ConversationMatch[] = [];
@@ -252,14 +272,18 @@ export class ConversationViewer implements Component {
252
272
  private operations?: ConversationViewerOperations,
253
273
  ) {
254
274
  this.keys = createViewerKeys(keybindings);
255
- this.blocks = formatConversationMessages(session.messages);
275
+ this.pendingSteers = this.readPendingSteers();
276
+ this.blocks = this.formatBlocks();
256
277
  this.timeline = new ConversationTimeline(tui, theme, {
257
278
  cwd: operations?.ctx.cwd,
258
279
  record,
259
280
  onChange: (change) => this.handleTimelineChange(change),
260
281
  });
261
282
  this.timeline.setBlocks(this.blocks);
262
- this.unsubscribe = session.subscribe(() => this.scheduleSessionRefresh());
283
+ this.unsubscribe = session.subscribe((event) => {
284
+ if (event.type === "queue_update") this.pendingSteers = [...event.steering];
285
+ this.scheduleSessionRefresh();
286
+ });
263
287
  }
264
288
 
265
289
  handleInput(data: string): void {
@@ -270,10 +294,14 @@ export class ConversationViewer implements Component {
270
294
  if (this.composer) {
271
295
  if (matchesKey(data, Key.alt("up")) || data === "a-up" || data === "alt+up") {
272
296
  this.recallSteerDraft();
297
+ } else if (matchesKey(data, Key.escape)) {
298
+ this.composer = undefined;
299
+ this.steerHistoryIndex = -1;
300
+ this.steerHistoryDraft = "";
273
301
  } else {
274
- const before = this.composer.getValue();
302
+ const before = this.composer.getText();
275
303
  this.composer.handleInput(data);
276
- if (this.composer && before !== this.composer.getValue()) {
304
+ if (this.composer && before !== this.composer.getText()) {
277
305
  this.steerHistoryIndex = -1;
278
306
  this.steerHistoryDraft = "";
279
307
  }
@@ -562,8 +590,8 @@ export class ConversationViewer implements Component {
562
590
 
563
591
  lines.push(hrMid);
564
592
  if (this.composer) {
565
- lines.push(row(this.composer.render(innerW)[0] ?? ""));
566
- const hint = th.fg("dim", "Enter send · Esc cancel · Alt+Up recall");
593
+ for (const composerLine of this.composer.render(innerW)) lines.push(row(composerLine));
594
+ const hint = th.fg("dim", "Enter send · Ctrl+J newline · Esc cancel · Alt+Up recall");
567
595
  const label = th.fg("accent", "✎ steer");
568
596
  lines.push(row(label + " ".repeat(Math.max(1, innerW - visibleWidth(label) - visibleWidth(hint))) + hint));
569
597
  } else if (this.searchMode) {
@@ -615,6 +643,31 @@ export class ConversationViewer implements Component {
615
643
  this.cachedVersion = -1;
616
644
  }
617
645
 
646
+ private readPendingSteers(): string[] {
647
+ return this.session.getSteeringMessages ? [...this.session.getSteeringMessages()] : [];
648
+ }
649
+
650
+ private formatBlocks(): ConversationBlock[] {
651
+ const blocks = formatConversationMessages(this.session.messages);
652
+ const usedIds = new Set(blocks.map((block) => block.id));
653
+ const occurrences = new Map<string, number>();
654
+ for (const text of this.pendingSteers) {
655
+ const occurrence = occurrences.get(text) ?? 0;
656
+ occurrences.set(text, occurrence + 1);
657
+ blocks.push({
658
+ id: pendingSteerBlockId(text, occurrence, usedIds),
659
+ kind: "text",
660
+ role: "user",
661
+ header: "User",
662
+ markdown: text,
663
+ copyText: text,
664
+ fullText: text,
665
+ pending: true,
666
+ });
667
+ }
668
+ return blocks;
669
+ }
670
+
618
671
  private scheduleSessionRefresh(): void {
619
672
  if (this.closed || this.sessionRefreshQueued) return;
620
673
  this.sessionRefreshQueued = true;
@@ -622,7 +675,8 @@ export class ConversationViewer implements Component {
622
675
  this.sessionRefreshQueued = false;
623
676
  if (this.closed) return;
624
677
  if (this.hasFocusedBlock) this.selectedBlockId = this.currentBlock()?.id;
625
- this.blocks = formatConversationMessages(this.session.messages);
678
+ this.pendingSteers = this.readPendingSteers();
679
+ this.blocks = this.formatBlocks();
626
680
  this.timeline.setBlocks(this.blocks);
627
681
  this.cacheVersion++;
628
682
  this.invalidate();
@@ -665,7 +719,8 @@ export class ConversationViewer implements Component {
665
719
  }
666
720
 
667
721
  private chromeLines(): number {
668
- return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + (this.composer ? 1 : 0);
722
+ const composerLines = this.composer?.render(this.lastInnerW || 80).length ?? 0;
723
+ return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + composerLines;
669
724
  }
670
725
 
671
726
  private invocationLine(): string | undefined {
@@ -987,7 +1042,16 @@ export class ConversationViewer implements Component {
987
1042
  }
988
1043
 
989
1044
  private openComposer(): void {
990
- const input = new Input();
1045
+ const input = new Editor(this.tui, {
1046
+ borderColor: (text) => this.theme.fg("borderMuted", text),
1047
+ selectList: {
1048
+ selectedPrefix: (text) => this.theme.fg("accent", text),
1049
+ selectedText: (text) => this.theme.fg("accent", text),
1050
+ description: (text) => this.theme.fg("muted", text),
1051
+ scrollInfo: (text) => this.theme.fg("muted", text),
1052
+ noMatch: (text) => this.theme.fg("muted", text),
1053
+ },
1054
+ });
991
1055
  input.focused = true;
992
1056
  this.steerHistoryIndex = -1;
993
1057
  this.steerHistoryDraft = "";
@@ -1003,25 +1067,31 @@ export class ConversationViewer implements Component {
1003
1067
  }
1004
1068
  this.tui.requestRender();
1005
1069
  };
1006
- input.onEscape = () => {
1007
- this.composer = undefined;
1008
- this.steerHistoryIndex = -1;
1009
- this.steerHistoryDraft = "";
1010
- this.tui.requestRender();
1011
- };
1012
1070
  this.composer = input;
1013
1071
  this.tui.requestRender();
1014
1072
  }
1015
1073
 
1016
- /** Recall the newest prior steer, preserving the draft for future editing. */
1074
+ /** Recall queued messages before local history so an undelivered steer is never hidden. */
1017
1075
  private recallSteerDraft(): void {
1018
- if (!this.composer || this.steerHistory.length === 0) return;
1076
+ if (!this.composer) return;
1077
+ if (this.pendingSteers.length > 0) {
1078
+ if (!this.session.clearQueue) return;
1079
+ const { steering, followUp } = this.session.clearQueue();
1080
+ const queuedText = [...steering, ...followUp].join("\n");
1081
+ const currentText = this.composer.getText();
1082
+ this.composer.setText([queuedText, currentText].filter((text) => text.trim()).join("\n"));
1083
+ this.pendingSteers = [];
1084
+ this.steerHistoryIndex = -1;
1085
+ this.steerHistoryDraft = "";
1086
+ return;
1087
+ }
1088
+ if (this.steerHistory.length === 0) return;
1019
1089
  if (this.steerHistoryIndex < 0) {
1020
- this.steerHistoryDraft = this.composer.getValue();
1090
+ this.steerHistoryDraft = this.composer.getText();
1021
1091
  this.steerHistoryIndex = 0;
1022
1092
  } else if (this.steerHistoryIndex < this.steerHistory.length - 1) {
1023
1093
  this.steerHistoryIndex++;
1024
1094
  }
1025
- this.composer.setValue(this.steerHistory[this.steerHistoryIndex] ?? this.steerHistoryDraft);
1095
+ this.composer.setText(this.steerHistory[this.steerHistoryIndex] ?? this.steerHistoryDraft);
1026
1096
  }
1027
1097
  }