@aeondave/pi-persona 1.17.1 → 1.17.4

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.
Files changed (146) hide show
  1. package/AGENTS.md +103 -64
  2. package/CHANGELOG.md +310 -1
  3. package/README.md +37 -23
  4. package/agents/architect.md +21 -0
  5. package/agents/balthasar.md +17 -22
  6. package/agents/casper.md +17 -23
  7. package/agents/evidence-verifier.md +31 -16
  8. package/agents/melchior.md +17 -22
  9. package/agents/operator.md +15 -55
  10. package/agents/research.md +14 -48
  11. package/agents/reviewer.md +20 -39
  12. package/agents/scout.md +14 -15
  13. package/agents/verifier.md +15 -16
  14. package/docs/ARCHITECTURE.md +280 -92
  15. package/docs/COLLABORATION.md +110 -0
  16. package/docs/EXPERIENCE.md +52 -17
  17. package/docs/MONITORS.md +62 -3
  18. package/docs/README.md +12 -0
  19. package/docs/REFERENCE.md +139 -38
  20. package/docs/SPINE.md +2 -2
  21. package/docs/STRATEGIES.md +23 -8
  22. package/docs/TELEMETRY.md +1 -0
  23. package/flows/deep-review.flow.json +1 -1
  24. package/package.json +15 -8
  25. package/personas/audit.md +28 -17
  26. package/personas/dev.md +30 -32
  27. package/personas/elite.md +223 -221
  28. package/personas/judge.md +25 -19
  29. package/personas/magi.md +26 -29
  30. package/personas/planner.md +31 -24
  31. package/personas/researcher.md +25 -26
  32. package/personas/swarm.md +35 -25
  33. package/personas/verify.md +30 -19
  34. package/src/agents/agent.ts +26 -0
  35. package/src/bridge.ts +28 -10
  36. package/src/bus/broker/client.ts +119 -21
  37. package/src/bus/broker/host.ts +123 -8
  38. package/src/bus/broker/messages.ts +31 -4
  39. package/src/bus/contact.ts +113 -38
  40. package/src/bus/peers.ts +15 -0
  41. package/src/bus/result-reader.ts +88 -0
  42. package/src/core/brief.ts +436 -353
  43. package/src/core/capabilities.ts +1 -1
  44. package/src/core/config.ts +15 -10
  45. package/src/core/leg-thinking.ts +57 -0
  46. package/src/core/model-catalog.ts +171 -0
  47. package/src/core/model-tier.ts +89 -0
  48. package/src/core/models.ts +13 -4
  49. package/src/core/monitor-output.ts +85 -0
  50. package/src/core/monitor.ts +34 -2
  51. package/src/core/nudge.ts +176 -23
  52. package/src/core/ownership.ts +1 -2
  53. package/src/core/pi-compat.ts +5 -12
  54. package/src/core/seed.ts +99 -6
  55. package/src/core/session-runs.ts +87 -0
  56. package/src/core/settings.ts +1 -1
  57. package/src/core/timer.ts +34 -9
  58. package/src/core/types.ts +1 -1
  59. package/src/engine/adapter.ts +95 -27
  60. package/src/engine/async.ts +213 -155
  61. package/src/engine/child.ts +12 -4
  62. package/src/engine/host-module.ts +12 -0
  63. package/src/engine/inproc.ts +99 -53
  64. package/src/engine/result-grant.ts +62 -0
  65. package/src/engine/session-tools.ts +6 -0
  66. package/src/engine/stream.ts +10 -20
  67. package/src/engine/worktree.ts +17 -7
  68. package/src/exocom/collaboration-delivery.ts +29 -0
  69. package/src/exocom/collaboration.ts +148 -0
  70. package/src/exocom/envelope.ts +43 -4
  71. package/src/exocom/gate.ts +7 -2
  72. package/src/exocom/install.ts +47 -18
  73. package/src/exocom/ledger.ts +58 -8
  74. package/src/exocom/plane.ts +32 -1
  75. package/src/exocom/profile.ts +40 -0
  76. package/src/exocom/registry.ts +5 -0
  77. package/src/exocom/status.ts +11 -1
  78. package/src/exocom/write-guard.ts +7 -0
  79. package/src/extension/broker-host.ts +61 -9
  80. package/src/extension/engine.ts +54 -28
  81. package/src/extension/hooks.ts +952 -868
  82. package/src/extension/identity.ts +41 -75
  83. package/src/extension/persona-model-settings.ts +31 -13
  84. package/src/extension/results.ts +136 -0
  85. package/src/extension/shared.ts +32 -7
  86. package/src/extension.ts +2965 -2611
  87. package/src/loader.ts +11 -3
  88. package/src/monitor/archive.ts +235 -0
  89. package/src/monitor/session.ts +21 -1
  90. package/src/orchestration/flow-journal.ts +33 -6
  91. package/src/orchestration/flow-run.ts +160 -32
  92. package/src/orchestration/flow.ts +20 -2
  93. package/src/orchestration/reducers.ts +9 -5
  94. package/src/orchestration/render.ts +1 -1
  95. package/src/orchestration/roster.ts +71 -23
  96. package/src/orchestration/sdk.ts +61 -11
  97. package/src/orchestration/strategies/compete.ts +5 -1
  98. package/src/orchestration/strategies/council-rounds.ts +2 -0
  99. package/src/orchestration/strategies/critic-loop.ts +3 -0
  100. package/src/orchestration/strategies/debate.ts +2 -0
  101. package/src/orchestration/strategies/fanout.ts +3 -2
  102. package/src/orchestration/strategies/judge.ts +5 -1
  103. package/src/orchestration/strategies/magi.ts +40 -23
  104. package/src/orchestration/strategies/map.ts +13 -3
  105. package/src/orchestration/strategies/pair.ts +2 -0
  106. package/src/orchestration/strategies/pipeline.ts +2 -0
  107. package/src/orchestration/strategies/synthesize.ts +27 -7
  108. package/src/orchestration/strategy.ts +31 -0
  109. package/src/orchestration/types.ts +10 -0
  110. package/src/persona/model-participants.ts +20 -7
  111. package/src/persona/orchestrate.ts +2 -0
  112. package/src/persona/persona.ts +8 -4
  113. package/src/results/archive.ts +281 -0
  114. package/src/results/record.ts +166 -0
  115. package/src/results/retrieval.ts +142 -0
  116. package/src/results/types.ts +128 -0
  117. package/src/telemetry/contract.ts +1 -1
  118. package/src/tools/background-run.ts +151 -0
  119. package/src/tools/council.ts +108 -66
  120. package/src/tools/delegate-tool.ts +349 -283
  121. package/src/tools/delegate.ts +35 -216
  122. package/src/tools/exocom-collaboration-input.ts +168 -0
  123. package/src/tools/exocom-collaboration.ts +315 -0
  124. package/src/tools/exocom-profile.ts +102 -0
  125. package/src/tools/exocom-work.ts +18 -53
  126. package/src/tools/exocom.ts +35 -22
  127. package/src/tools/flow.ts +103 -30
  128. package/src/tools/intercom-tool.ts +202 -128
  129. package/src/tools/intercom.ts +1 -1
  130. package/src/tools/models.ts +34 -9
  131. package/src/tools/monitor.ts +58 -6
  132. package/src/tools/read-result.ts +52 -0
  133. package/src/tools/timer.ts +34 -15
  134. package/src/ui/agent-overlay.ts +65 -16
  135. package/src/ui/agent-tree.ts +127 -19
  136. package/src/ui/compact-widget.ts +25 -0
  137. package/src/ui/failure-summary.ts +55 -0
  138. package/src/ui/literal-tool-result.ts +24 -0
  139. package/src/ui/model-picker.ts +158 -81
  140. package/src/ui/orchestration-launch.ts +49 -0
  141. package/src/ui/persona-settings.ts +9 -3
  142. package/src/ui/presentation.ts +24 -0
  143. package/src/ui/report-body.ts +63 -0
  144. package/src/ui/session-cards.ts +366 -0
  145. package/src/ui/wake-rows.ts +262 -0
  146. package/teams.yaml +5 -1
@@ -5,13 +5,15 @@ import { failureDetails } from "../extension/shared.ts";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { compactInlineText, sanitizeTerminalText } from "../ui/presentation.ts";
7
7
  import { compactVisibleText } from "../ui/presentation.ts";
8
+ import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
9
+ import { createReportBody } from "../ui/report-body.ts";
8
10
  import { coachingDisabledHint, expandDetailHint, reconcileAnsweredAsk } from "../extension/shared.ts";
9
11
  import {
10
- type AsyncRun, AsyncRunTracker, boundCompletionSurface, buildPeekDigest, buildWaitTimeoutNote,
11
- MAX_ASYNC_STATUS_ROWS,
12
+ type AsyncRun, AsyncRunTracker, buildPeekDigest,
12
13
  dedupeRunsById, getFullRunOutput, IdleCoalescingNotifier,
13
- renderCompletion, runDisplayName, runDurationLabel,
14
+ isAsyncRunStalled, observedRunModel, runDisplayName, runDurationLabel,
14
15
  } from "../engine/async.ts";
16
+ import type { PollGuard } from "../core/nudge.ts";
15
17
  import { fenceUntrusted } from "../core/fence.ts";
16
18
  import { sanitizeDisplayLabel } from "../core/display-label.ts";
17
19
  import { type IntercomOutcome, type IntercomParams, MAX_INTERCOM_MESSAGE_CHARS, MAX_INTERCOM_REF_CHARS, runIntercom } from "./intercom.ts";
@@ -20,11 +22,14 @@ import type { InProcessBus } from "../bus/inproc.ts";
20
22
  import type { PersonaController } from "../persona/controller.ts";
21
23
  import { emptyUsage } from "../engine/stream.ts";
22
24
  import { toolUsageField, type ChildUsageLedger } from "../ui/usage.ts";
25
+ import type { TerminalResultArchive } from "../results/archive.ts";
26
+ import { pageLiveResult, projectResultPage, resultReceiptHint } from "../results/retrieval.ts";
23
27
 
24
28
  export interface IntercomToolDeps {
25
29
  get lastCtx(): ExtensionContext | undefined;
26
30
  set lastCtx(value: ExtensionContext | undefined);
27
31
  tracker: AsyncRunTracker;
32
+ archive(ctx: ExtensionContext): TerminalResultArchive;
28
33
  completionNotifier: IdleCoalescingNotifier<AsyncRun>;
29
34
  intercomNotifier: IdleCoalescingNotifier<PendingAsk>;
30
35
  controller: PersonaController;
@@ -32,35 +37,33 @@ export interface IntercomToolDeps {
32
37
  SUPERVISOR: string;
33
38
  STALL_FLAG_MS: number;
34
39
  waitingRunIds?: () => ReadonlySet<string>;
40
+ /** Refuses an unchanged repeat of peek/result on a running run. Built only when `config.nudge`. */
41
+ pollGuard?: PollGuard | undefined;
35
42
  missingRunMessage(id: string, display: string | undefined): string;
36
43
  stopAgent(nodeId: string): boolean;
37
44
  steerAgent(nodeId: string, text: string): boolean;
38
45
  steerRegistry: Map<string, unknown>;
39
46
  stopRequested: Set<string>;
40
- drainBusBlock(): string;
47
+ /** The premature-surrender note for a settled report (config-gated), else undefined. */
41
48
  scanForSurrender(text: string): string | undefined;
42
- get disposed(): boolean;
43
49
  childUsage: ChildUsageLedger;
44
50
  publishPersonaCost(): void;
45
51
  }
46
52
 
47
- /** Keep an aborted wait honest: its signal may end the join well before the configured window. */
48
- export function buildIntercomWaitStillNote(ids: readonly string[], timeoutMs: number, interrupted: boolean): string {
49
- if (!interrupted) return buildWaitTimeoutNote(ids, timeoutMs);
50
- const visibleIds = ids.slice(0, MAX_ASYNC_STATUS_ROWS).map((id) => sanitizeDisplayLabel(id, "run"));
51
- const omitted = ids.length - visibleIds.length;
52
- const idSummary = `${visibleIds.join(", ")}${omitted > 0 ? `, … +${omitted} more` : ""}`;
53
- return (
54
- `⏹ wait interrupted before the configured ${timeoutMs}ms window elapsed; still running: ${idSummary}. ` +
55
- "Continue useful supervisor work; completion will notify you automatically. Use intercom wait again when ready."
56
- );
57
- }
58
-
59
53
  /** Fence only child-authored payloads; missing-message diagnostics remain actionable tool prose. */
60
54
  export function fenceIntercomToolOutcome(out: IntercomOutcome, action: string, fence: (text: string) => string): string {
61
55
  return action === "message" && out.details.ok ? fence(out.text) : fenceIntercomOutcome(out, fence);
62
56
  }
63
57
 
58
+ /** `run-4 (alpha · haiku) running, run-3 (scout) done, … (+2 more)` — the one bounded run listing
59
+ * every id-to-call-sign teaching surface shares (a run miss, a bulk stop). `none` when empty. */
60
+ export function describeRuns(runs: readonly AsyncRun[], opts: { status?: boolean; max?: number } = {}): string {
61
+ if (runs.length === 0) return "none";
62
+ const max = opts.max ?? 8;
63
+ const shown = runs.slice(0, max).map((run) => `${run.id} (${runDisplayName(run)})${opts.status ? ` ${run.status}` : ""}`);
64
+ return `${shown.join(", ")}${runs.length > max ? `, … (+${runs.length - max} more)` : ""}`;
65
+ }
66
+
64
67
  /** Renderer-local state only. The persisted result, not this cache, owns display identity. */
65
68
  interface IntercomRenderState {
66
69
  target?: { id: string; label: string };
@@ -77,13 +80,62 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
77
80
  const collision = d.tracker.list().some((other) => other.id !== id && runDisplayName(other) === name);
78
81
  return collision ? `${name} [${sanitizeDisplayLabel(id, "run")}]` : name;
79
82
  }
83
+ /** An id the tracker holds nothing for: teach what exists (and why a stale id dead-ends) instead of
84
+ * letting steer/stop fall through to a false "already finished" or "no live steer handle". */
85
+ function missingRun(action: "result" | "steer" | "stop", to: string, display: string | undefined, snapshot: object) {
86
+ return { content: [{ type: "text" as const, text: d.missingRunMessage(to, display) }], details: failureDetails({ action, ok: false, ...snapshot }), isError: true };
87
+ }
88
+ /** `stop { to: "all" }`: request cancellation of every running run, each with the per-run semantics of a
89
+ * single stop — except that a run already stopping is reported, never force-cleared, in bulk. */
90
+ function stopAllRuns() {
91
+ const snapshot = { runId: "all", target: "all" };
92
+ const requested: AsyncRun[] = [];
93
+ const already: AsyncRun[] = [];
94
+ const unreachable: AsyncRun[] = [];
95
+ for (const run of d.tracker.running()) {
96
+ const nodeId = `async:${run.id}`;
97
+ if (d.stopRequested.has(nodeId)) already.push(run);
98
+ else (d.stopAgent(nodeId) ? requested : unreachable).push(run);
99
+ }
100
+ const noHandle = unreachable.length > 0 ? `No live stop handle: ${describeRuns(unreachable)}.` : "";
101
+ if (requested.length === 0 && already.length === 0) {
102
+ return { content: [{ type: "text" as const, text: noHandle || "intercom stop: no running runs to stop." }], details: failureDetails({ action: "stop", ok: false, ...snapshot }), isError: true };
103
+ }
104
+ if (requested.length > 0) d.pollGuard?.reset();
105
+ const parts = [
106
+ ...(requested.length > 0 ? [`Cancellation requested for ${requested.length} run${requested.length === 1 ? "" : "s"}: ${describeRuns(requested)}. Stopped reports arrive as completion follow-ups; partial output is kept; changes already made are not undone.`] : []),
107
+ ...(already.length > 0 ? [`Already stopping: ${describeRuns(already)}.${requested.length === 0 ? " Repeat stop { to: <run id> } on one that stays running to force-clear it." : ""}`] : []),
108
+ ...(noHandle ? [noHandle] : []),
109
+ ];
110
+ return { content: [{ type: "text" as const, text: parts.join(" ") }], details: { action: "stop", ok: true, ...snapshot }, isError: false };
111
+ }
112
+ /**
113
+ * An unchanged repeat of a status check learns nothing and holds the turn the idle-gated completion
114
+ * wake needs: refuse it, and on the second repeat end the turn. The signature is every shown run's
115
+ * state (status, queued, waiting, stalled), so anything newly informative passes.
116
+ */
117
+ function pollRefusal(action: "peek" | "result", scope: string, runs: readonly AsyncRun[], waiting: ReadonlySet<string> | undefined, snapshot: object) {
118
+ const running = runs.filter((r) => r.status === "running");
119
+ if (!d.pollGuard || running.length === 0) return undefined;
120
+ const now = Date.now();
121
+ const sig = runs.map((r) => [r.id, r.status, r.queued === true, waiting?.has(r.id) === true, isAsyncRunStalled(r, now, d.STALL_FLAG_MS)].join("|")).sort().join(",");
122
+ const verdict = d.pollGuard.check(scope, sig);
123
+ if (verdict === "ok") return undefined;
124
+ const text = `intercom ${action}: no run settled, started, queued, began waiting or stalled since your last ${action} (${running.map((r) => r.id).join(", ")} still running). Checking again blocks your own completion follow-ups and the operator's steering — end your turn; a finished run or batch wakes you. Use steer or stop only to intervene.`;
125
+ return {
126
+ content: [{ type: "text" as const, text }],
127
+ details: failureDetails({ action, ok: false, ...snapshot }),
128
+ isError: true,
129
+ ...(verdict === "terminate" ? { terminate: true } : {}),
130
+ };
131
+ }
80
132
  // ── intercom tool (supervisor side of the comm plane: read/answer children) ───
81
133
  const IntercomToolParams = Type.Object({
82
134
  action: Type.Union(
83
135
  [
84
136
  Type.Literal("peek"),
85
137
  Type.Literal("result"),
86
- Type.Literal("wait"),
138
+ Type.Literal("results"),
87
139
  Type.Literal("steer"),
88
140
  Type.Literal("stop"),
89
141
  Type.Literal("list"),
@@ -94,24 +146,28 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
94
146
  ],
95
147
  {
96
148
  description:
97
- "peek = watch async sub-agents · result = retrieve one complete settled result by run id · wait = collect current reports; sync:true joins (default: nonblocking with UI, blocking headless) · steer = soft redirect into one by run id (it may ignore it) · stop = request engine cancellation by run id · message = retrieve one drained bus message by message id · list/inbox/reply/send = coaching messages (needs a coaching persona)",
149
+ "peek = status of async sub-agents · results = bounded archive summaries (cursor/limit) · result = page a settled live result by to:run-id or a saved report by ref:result-UUID · steer = soft redirect into one by run id (it may ignore it) · stop = request engine cancellation by run id · message = retrieve one drained bus message by message id · list/inbox/reply/send = coaching messages (needs a coaching persona)",
98
150
  },
99
151
  ),
100
- to: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "result/steer/stop/peek: the async run id (e.g. 'run-1') · wait: an async run id; omit it to collect all current/pending completions · send: the child handle (from `list`)" })),
152
+ to: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "result/steer/stop/peek: the async run id (e.g. 'run-1'); stop: \"all\" cancels every running run · send: the child handle (from `list`)" })),
153
+ ref: Type.Optional(Type.String({ maxLength: 43, description: "result: opaque result:<UUID> archive reference; never a run id. Mutually exclusive with to." })),
154
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "result: stored-body UTF-16 offset (default 0); use nextOffset, never split a surrogate pair." })),
155
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000, description: "result: 2..16000 UTF-16 units (default 8000). results: 1..50 summaries (default 20)." })),
156
+ cursor: Type.Optional(Type.Integer({ minimum: 0, description: "results: nextCursor from the prior bounded summary page; omit for the first page." })),
157
+ collect: Type.Optional(Type.Boolean({ description: "result with live to only: true acknowledges the whole report and suppresses its pending completion. Paging alone does not collect." })),
101
158
  messageId: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "message: the drained bus message id shown by `inbox`" })),
102
159
  askId: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "reply: the message id of the child's pending question" })),
103
160
  message: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_MESSAGE_CHARS, description: "steer/reply/send: the text to deliver" })),
104
- sync: Type.Optional(Type.Boolean({ description: "wait: block for runs to settle; defaults to false in interactive/RPC sessions and true in headless sessions" })),
105
- timeoutMs: Type.Optional(Type.Number({ description: "wait with sync:true: max ms to hold your turn (default 600000; values below 1000 clamp to 1000, values above 600000 clamp to 600000) — on timeout you get what settled + what's still running" })),
106
161
  });
107
162
  pi.registerTool({
108
163
  name: "intercom",
109
164
  label: "Intercom",
110
165
  description: [
111
- "See, steer, message, and JOIN your running sub-agents.",
112
- "`peek` watches what your async sub-agents are doing; `result` retrieves one complete settled",
113
- "payload by run id; `wait` collects current reports and is nonblocking by default with UI (interactive/RPC); set `sync: true` for an intentional bounded join. Headless defaults to joining; set `sync: false` for a snapshot. Do not automatically wait for all children.",
114
- "Work independently while children run; when only children remain, end your turn and let completion follow-ups wake you; do not monitor or poll healthy children.",
166
+ "Steer, stop and inspect your background sub-agents; a finished run or batch wakes you with a completion follow-up.",
167
+ "Never `peek` healthy children; use it only for status you need. `result { to: runId }` pages a live settled payload by run id (8000 units default, 16000 maximum).",
168
+ "`results { cursor?, limit? }` lists saved terminal summaries across this session's branches (20 default, 50 maximum); pass nextCursor for the next page.",
169
+ "`result { ref: 'result:<UUID>', offset?, limit? }` reads a saved report, including after reload, without collecting, billing, restarting a worker or waking the model. Memory-only references last only for this activation. Run ids route only the current runtime; never guess a historical run by id.",
170
+ "Use nextOffset for more UTF-16 pages. Only a whole live body returned in one read, or explicit live collect:true, suppresses a pending completion. Archive ref reads reject collect.",
115
171
  "`steer` sends a soft course-correction to one (by run id); in-process async runs can receive it mid-run,",
116
172
  "while child-engine (MCP/worktree) async runs receive it as a brokered follow-up. After remaining tools return, Pi can consume steering; arbitrary active tool work is not promised to stop immediately.",
117
173
  "`message` retrieves a drained bus message by its message id; `list`/`inbox`/`reply`/`send` exchange coaching messages (a child reaches you via `contact_supervisor`)",
@@ -124,7 +180,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
124
180
  // interpolate it into trusted prose without reducing it to compact identifier metadata.
125
181
  const displayTarget = params.to === undefined ? undefined : sanitizeDisplayLabel(params.to, "run");
126
182
  const targetSnapshot = params.to === undefined ? {} : { runId: params.to, target: displayRunTarget(params.to) };
127
- // peek + wait + steer + stop are supervisor→child controls over the async d.tracker /
183
+ // peek + result + steer + stop are supervisor→child controls over the async tracker /
128
184
  // steer handles — available to EVERY persona (no dependency on the coaching bus).
129
185
  if (params.action === "peek") {
130
186
  // No `to` → running legs PLUS any settled-but-not-yet-delivered ones (the settle→deliver
@@ -132,112 +188,73 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
132
188
  const runs = params.to
133
189
  ? [d.tracker.peek(params.to)].filter((r): r is AsyncRun => !!r)
134
190
  : dedupeRunsById([...d.tracker.running(), ...d.completionNotifier.peekPending()]);
135
- return { content: [{ type: "text", text: buildPeekDigest(runs, { now: Date.now(), stallMs: d.STALL_FLAG_MS, ...(d.waitingRunIds ? { waitingForSupervisor: d.waitingRunIds() } : {}) }) }], details: { action: "peek", ok: true, ...targetSnapshot }, isError: false };
191
+ const waiting = d.waitingRunIds?.();
192
+ const refusal = pollRefusal("peek", params.to ?? "*", runs, waiting, targetSnapshot);
193
+ if (refusal) return refusal;
194
+ return { content: [{ type: "text", text: buildPeekDigest(runs, { now: Date.now(), stallMs: d.STALL_FLAG_MS, ...(waiting ? { waitingForSupervisor: waiting } : {}) }) }], details: { action: "peek", ok: true, ...targetSnapshot }, isError: false };
195
+ }
196
+ if (params.action === "results" || (params.action === "result" && params.ref !== undefined)) {
197
+ const invalid = params.to !== undefined || params.collect !== undefined || (params.action === "results" && (params.ref !== undefined || params.offset !== undefined)) || (params.action === "result" && params.cursor !== undefined);
198
+ if (invalid) return { content: [{ type: "text", text: "Archive reads accept ref/offset/limit; results accepts cursor/limit. to and collect are live-only." }], details: failureDetails({ action: params.action }), isError: true };
199
+ const archive = d.archive(ctx);
200
+ if (params.action === "results") {
201
+ const page = archive.list({ ...(params.cursor !== undefined ? { cursor: params.cursor } : {}), ...(params.limit !== undefined ? { limit: params.limit } : {}) });
202
+ const text = page.ok ? ["Saved terminal reports (not live workers):", ...page.results.map((r) => `${r.ref} · ${sanitizeDisplayLabel(r.label ?? r.agent)} · ${r.status} · ${r.storage}${r.truncated ? " · truncated" : ""}`), ...(page.nextCursor !== undefined ? [`nextCursor: ${page.nextCursor}`] : []), ...(page.warnings.length ? [`Storage warnings: ${page.warnings.join(", ")}`] : [])].join("\n") : page.message;
203
+ return { content: [{ type: "text", text }], details: { action: "results", ...page }, isError: !page.ok };
204
+ }
205
+ const page = archive.read({ ref: params.ref!, ...(params.offset !== undefined ? { offset: params.offset } : {}), ...(params.limit !== undefined ? { limit: params.limit } : {}) });
206
+ if (!page.ok) {
207
+ return { content: [{ type: "text", text: page.message }], details: { action: "result", ...page }, isError: true };
208
+ }
209
+ // Bounded-page rule: the fenced body is delivered once, in content; details stay
210
+ // bounded metadata exactly like the live result path (no whole page hidden there).
211
+ const { body: _pageBody, ...pageMeta } = page;
212
+ return {
213
+ content: [{ type: "text", text: `${page.summary.ref} · ${page.summary.status} · ${page.summary.storage}${page.summary.truncated ? " · saved body truncated" : ""}\n${projectResultPage(page, page.summary.error)}` }],
214
+ details: { action: "result", ...pageMeta }, isError: false,
215
+ };
136
216
  }
137
217
  if (params.action === "result") {
138
- if (!params.to) {
218
+ if (!params.to || params.cursor !== undefined) {
139
219
  return { content: [{ type: "text", text: "intercom result needs { to: <run id> }." }], details: failureDetails({ action: "result", ok: false }), isError: true };
140
220
  }
141
221
  const run = d.tracker.peek(params.to);
142
- if (!run) {
143
- return { content: [{ type: "text", text: d.missingRunMessage(params.to, displayTarget) }], details: failureDetails({ action: "result", ok: false, ...targetSnapshot }), isError: true };
144
- }
222
+ if (!run) return missingRun("result", params.to, displayTarget, targetSnapshot);
145
223
  if (run.status === "running") {
224
+ const refusal = pollRefusal("result", `result:${params.to}`, [run], d.waitingRunIds?.(), targetSnapshot);
225
+ if (refusal) return refusal;
146
226
  return {
147
- content: [{ type: "text", text: `${targetSnapshot.target} is still running. Use intercom peek/wait, or request result after it settles.` }],
227
+ content: [{ type: "text", text: `${targetSnapshot.target} is still running; a finished run or batch wakes you with a completion follow-up. End your turn if nothing else remains.` }],
148
228
  details: failureDetails({ action: "result", ok: false, status: run.status, ...targetSnapshot }),
149
229
  isError: true,
150
230
  };
151
231
  }
152
232
  // Explicit collection owns this delivery: remove a still-buffered passive completion so the
153
- // same result cannot appear again as a follow-up a moment later. Telling the d.tracker too
233
+ // same result cannot appear again as a follow-up a moment later. Telling the tracker too
154
234
  // makes this retained copy the first thing retention evicts — the supervisor has read it,
155
235
  // so it is the cheapest payload in the map to lose.
156
- d.completionNotifier.discard((pending) => pending.id === run.id);
157
- d.tracker.markCollected(run.id);
158
236
  const full = getFullRunOutput(run);
159
- const body = full === "(no output)" ? full : fenceUntrusted(full);
160
- // The cause is engine/child-authored text too. Keep the run id/status as trusted compact
161
- // metadata, but put the diagnostic inside the same untrusted fence as the payload.
162
- const cause = run.error ? `\nFailure detail:\n${fenceUntrusted(run.error)}` : "";
163
- const displayRun = sanitizeDisplayLabel(run.label ?? run.agent);
237
+ const page = pageLiveResult(full, params.offset, params.limit);
238
+ if (!page) return { content: [{ type: "text", text: "Invalid result offset/limit; use nextOffset and 2..16000 units." }], details: failureDetails({ action: "result", ...targetSnapshot }), isError: true };
239
+ if (params.collect === true || (page.offset === 0 && page.endOfStoredBody)) {
240
+ d.completionNotifier.discard((pending) => pending.id === run.id);
241
+ d.tracker.markCollected(run.id);
242
+ }
243
+ const body = projectResultPage(page, run.error);
244
+ const receipt = resultReceiptHint(run.resultReceipt);
245
+ const displayRun = runDisplayName(run);
246
+ const modelUsed = observedRunModel(run);
164
247
  // The wall time this leg took: an explicit collection is one of the paths a completion reaches
165
- // the supervisor through, so it carries the same reading the passive/join reports do.
248
+ // the supervisor through, so it carries the same reading the passive completion report does.
166
249
  const took = runDurationLabel(run);
250
+ // The follow-up discarded above is what would have carried the premature-surrender note, so
251
+ // a collected DONE report must carry it itself (a failed leg is already a failure report).
252
+ const note = run.status === "done" ? d.scanForSurrender(full) : undefined;
167
253
  const billed = d.childUsage.accountMany([{ key: run.id, usage: run.result?.usage ?? emptyUsage() }]);
168
254
  d.publishPersonaCost();
169
255
  return {
170
- content: [{ type: "text", text: `${run.id} (${displayRun}) · ${run.status}${took ? ` · ${took}` : ""}${cause}\n${body}` }],
171
- details: { action: "result", ok: true, status: run.status, ...targetSnapshot },
172
- isError: false,
173
- ...toolUsageField(billed),
174
- };
175
- }
176
- if (params.action === "wait") {
177
- // No `to` → wait on running legs AND collect settled legs still queued for follow-up
178
- // delivery, so a wait in the settle→deliver gap returns their results (not "nothing").
179
- if (params.to && !d.tracker.peek(params.to)) {
180
- return { content: [{ type: "text", text: d.missingRunMessage(params.to, displayTarget) }], details: failureDetails({ action: "wait", ok: false, ...targetSnapshot }), isError: true };
181
- }
182
- const ids = params.to
183
- ? [params.to]
184
- : dedupeRunsById([...d.tracker.running(), ...d.completionNotifier.peekPending()]).map((r) => r.id);
185
- if (ids.length === 0) {
186
- return { content: [{ type: "text", text: "No async runs to wait for." }], details: { action: "wait", ok: true }, isError: false };
187
- }
188
- // Interactive/RPC defaults to a snapshot: waiting inside a tool stalls queued user steering.
189
- // Headless retains its historical join; callers can override either default with sync.
190
- if (!(params.sync ?? ctx.hasUI !== true)) {
191
- const current = ids.map((id) => d.tracker.peek(id)).filter((run): run is AsyncRun => !!run);
192
- const settled = current.filter((run) => run.status !== "running");
193
- const still = current.filter((run) => run.status === "running");
194
- const settledIds = new Set(settled.map((run) => run.id));
195
- d.completionNotifier.discard((run) => settledIds.has(run.id));
196
- for (const id of settledIds) d.tracker.markCollected(id);
197
- const report = settled.length > 0 ? renderCompletion(settled, fenceUntrusted, (t) => d.scanForSurrender(t)) : "";
198
- const stillIds = still.map((run) => sanitizeDisplayLabel(run.id, "run"));
199
- const omitted = stillIds.length > MAX_ASYNC_STATUS_ROWS;
200
- const runningNote = still.length > 0
201
- ? `Background continues; automatic completion will notify you. End your turn when this is the only remaining work. Still running: ${stillIds.slice(0, MAX_ASYNC_STATUS_ROWS).join(", ")}${omitted ? `, … +${stillIds.length - MAX_ASYNC_STATUS_ROWS} more` : ""}.`
202
- : "";
203
- const text = boundCompletionSurface([report, runningNote].filter(Boolean).join("\n\n") || "Nothing to report (unknown run ids?).");
204
- const billed = d.childUsage.accountMany(settled.map((run) => ({ key: run.id, usage: run.result?.usage ?? emptyUsage() })));
205
- d.publishPersonaCost();
206
- return {
207
- content: [{ type: "text", text }],
208
- details: { action: "wait", ok: true, settled: [...settledIds], running: still.map((run) => run.id), ...targetSnapshot },
209
- isError: false,
210
- ...toolUsageField(billed),
211
- };
212
- }
213
- // Bounded join: never longer than a child's ask timeout (bus `ask` default 600s),
214
- // so a coaching child blocking on OUR reply can't deadlock us past its own timeout.
215
- // Default matches that ceiling — heavy sub-agents (30+ turns) routinely outlast a
216
- // short window, and a premature "still running" forces a needless re-wait.
217
- const timeoutMs = Math.min(Math.max(params.timeoutMs ?? 600_000, 1_000), 600_000);
218
- const runs = await d.tracker.waitFor(ids, timeoutMs, _signal);
219
- const settled = runs.filter((r) => r.status !== "running");
220
- const still = runs.filter((r) => r.status === "running");
221
- // These results are delivered HERE — drop them from the pending follow-up
222
- // notifier so they aren't reported a second time. Render through the SAME
223
- // renderCompletion the passive path uses, so a leg that came back BLOCKED still
224
- // carries the premature-surrender note when it is collected via `wait`.
225
- const settledIds = new Set(settled.map((r) => r.id));
226
- d.completionNotifier.discard((run) => settledIds.has(run.id));
227
- for (const id of settledIds) d.tracker.markCollected(id);
228
- const report = settled.length > 0 ? renderCompletion(settled, fenceUntrusted, (t) => d.scanForSurrender(t)) : "";
229
- const stillNote = still.length > 0
230
- ? buildIntercomWaitStillNote(still.map((r) => r.id), timeoutMs, _signal?.aborted === true)
231
- : "";
232
- const joined = [report, stillNote].filter(Boolean).join("\n\n") || "Nothing to report (unknown run ids?).";
233
- const text = boundCompletionSurface(joined);
234
- const billed = d.childUsage.accountMany(
235
- settled.map((r) => ({ key: r.id, usage: r.result?.usage ?? emptyUsage() })),
236
- );
237
- d.publishPersonaCost();
238
- return {
239
- content: [{ type: "text", text }],
240
- details: { action: "wait", ok: true, settled: [...settledIds], running: still.map((r) => r.id), ...targetSnapshot },
256
+ content: [{ type: "text", text: `${run.id} (${displayRun}) · ${run.status}${took ? ` · ${took}` : ""}${modelUsed ? `\nObserved model: ${modelUsed}` : ""}\n${body}${receipt ? `\n${receipt}` : ""}${note ? `\n\n${note}` : ""}` }],
257
+ details: { action: "result", ok: true, status: run.status, ...targetSnapshot, ...(modelUsed ? { modelUsed } : {}), page: { offset: page.offset, nextOffset: page.nextOffset, endOfStoredBody: page.endOfStoredBody }, ...(run.resultReceipt ? { resultReceipt: { ...run.resultReceipt } } : {}) },
241
258
  isError: false,
242
259
  ...toolUsageField(billed),
243
260
  };
@@ -248,7 +265,8 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
248
265
  }
249
266
  const nodeId = `async:${params.to}`;
250
267
  const knownRun = d.tracker.peek(params.to);
251
- if (knownRun && knownRun.status !== "running") {
268
+ if (!knownRun) return missingRun("steer", params.to, displayTarget, targetSnapshot);
269
+ if (knownRun.status !== "running") {
252
270
  return {
253
271
  content: [{ type: "text", text: `Cannot steer "${targetSnapshot.target}" — this run is already settled (${knownRun.status}). Use intercom result with to="${displayTarget}" to inspect its retained result.` }],
254
272
  details: failureDetails({ action: "steer", ok: false, status: knownRun.status, ...targetSnapshot }),
@@ -262,26 +280,31 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
262
280
  isError: true,
263
281
  };
264
282
  }
265
- // Routed through the guarded d.steerAgent so a just-finished/d.disposed handle can't throw.
283
+ // Routed through the guarded steerAgent so a just-finished/disposed handle can't throw.
266
284
  const steered = d.steerAgent(nodeId, params.message);
285
+ // An intervention changes the child's state without moving any flag the guard compares.
286
+ if (steered) d.pollGuard?.reset();
267
287
  return steered
268
288
  ? { content: [{ type: "text", text: `Steering queued for ${targetSnapshot.target} (${displayTarget}).` }], details: { action: "steer", ok: true, ...targetSnapshot, message: params.message }, isError: false }
269
289
  : { content: [{ type: "text", text: `Could not steer "${targetSnapshot.target}" — it may have just finished, or the message was empty.` }], details: failureDetails({ action: "steer", ok: false, ...targetSnapshot }), isError: true };
270
290
  }
271
291
  if (params.action === "stop") {
292
+ if (params.to === "all") return stopAllRuns();
272
293
  if (!params.to) {
273
294
  return { content: [{ type: "text", text: "intercom stop needs { to: <run id> }." }], details: failureDetails({ action: "stop", ok: false }), isError: true };
274
295
  }
296
+ if (!d.tracker.peek(params.to)) return missingRun("stop", params.to, displayTarget, targetSnapshot);
275
297
  // Abort the real run signal. The child backend can kill its process tree; the
276
298
  // in-process backend requests host cancellation and settles its lifecycle separately.
277
299
  const nodeId = `async:${params.to}`;
278
300
  const repeated = d.stopRequested.has(nodeId);
279
301
  const stopped = d.stopAgent(nodeId);
302
+ if (stopped) d.pollGuard?.reset();
280
303
  if (stopped && !repeated) {
281
- return { content: [{ type: "text", text: `Cancellation requested for ${targetSnapshot.target}. Check its terminal result; changes already made are not undone.` }], details: { action: "stop", ok: true, ...targetSnapshot }, isError: false };
304
+ return { content: [{ type: "text", text: `Cancellation requested for ${targetSnapshot.target}; its stopped report arrives with the completion follow-up (its batch's, if it has one). Changes already made are not undone.` }], details: { action: "stop", ok: true, ...targetSnapshot }, isError: false };
282
305
  }
283
306
  // A repeated stop has just invoked the REAL cancel handle again. Only now force-clear
284
- // d.tracker state if engine settlement is still lagging; the handle remains registered
307
+ // tracker state if engine settlement is still lagging; the handle remains registered
285
308
  // until onComplete so cancellation can never be replaced by UI-only bookkeeping.
286
309
  if (stopped && repeated && d.tracker.forceSettle(params.to, "force-stopped by supervisor after repeated engine cancellation")) {
287
310
  return {
@@ -304,7 +327,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
304
327
  if (params.action === "reply" && out.details.ok && params.askId) {
305
328
  reconcileAnsweredAsk(params.askId, d.intercomNotifier, d.bus, d.SUPERVISOR);
306
329
  }
307
- // Child-authored inbox bodies are untrusted, exactly like the d.drainBusBlock/peek copies.
330
+ // Child-authored inbox bodies are untrusted, exactly like the automatic drain and peek copies.
308
331
  // A retrieved body is child-authored even though it is no longer an inbox batch; keep the
309
332
  // same trust fence around it as inbox and automatic drain surfaces.
310
333
  let text = fenceIntercomToolOutcome(out, params.action, fenceUntrusted);
@@ -320,26 +343,28 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
320
343
  const runTarget = typeof args.to === "string"
321
344
  ? state?.target?.id === args.to ? state.target.label : displayRunTarget(args.to)
322
345
  : undefined;
323
- if (action === "wait" || action === "peek") target = runTarget ?? "all";
346
+ if (action === "peek") target = runTarget ?? "all";
324
347
  else if (["result", "steer", "stop"].includes(action)) target = runTarget ?? "?";
325
348
  else if (action === "send") target = compactInlineText(args.to ?? "?", { maxChars: 80 }) || "?";
326
349
  else if (action === "message") target = compactInlineText(args.messageId ?? "?", { maxChars: 80 }) || "?";
327
350
  else if (action === "reply") target = compactInlineText(args.askId ?? "?", { maxChars: 80 }) || "?";
328
- const timeout = action === "wait" && Number.isFinite(args.timeoutMs) && args.timeoutMs !== undefined
329
- ? ` · ${Math.max(0, Math.floor(args.timeoutMs))}ms`
330
- : "";
331
- const formatCall = (label: string) => `${theme.fg("toolTitle", theme.bold("intercom "))}${theme.fg("accent", action)}${label ? theme.fg("dim", ` ${compactInlineText(label, { maxChars: 80 })}${timeout}`) : ""}`;
351
+ const formatCall = (label: string) => `${theme.fg("toolTitle", theme.bold("intercom "))}${theme.fg("accent", action)}${label ? theme.fg("dim", ` ${compactInlineText(label, { maxChars: 80 })}`) : ""}`;
332
352
  const component = new Text(formatCall(target), 0, 0);
333
353
  if (state) { state.callText = component; state.formatCall = formatCall; }
334
354
  return component;
335
355
  },
336
356
  renderResult(result, { expanded }, theme, context) {
337
- const details = (result.details ?? {}) as { action?: string; ok?: boolean; runId?: string; target?: string; message?: string };
357
+ const details = (result.details ?? {}) as {
358
+ action?: string; ok?: boolean; runId?: string; target?: string; message?: string;
359
+ results?: Array<{ truncated?: boolean }>;
360
+ summary?: { ref?: string; storage?: string; storedChars?: number; truncated?: boolean };
361
+ nextCursor?: number; nextOffset?: number; warnings?: readonly string[];
362
+ };
338
363
  const state = context?.state as IntercomRenderState | undefined;
339
364
  const action = details.action ?? context?.args?.action;
340
365
  const savedId = details.runId ?? context?.args?.to;
341
366
  const legacyCompleted = !details.target && context !== undefined && context.isPartial !== true
342
- && ["steer", "stop", "result", "peek", "wait"].includes(action ?? "");
367
+ && ["steer", "stop", "result", "peek"].includes(action ?? "");
343
368
  if (state && typeof savedId === "string" && (details.target || legacyCompleted)) {
344
369
  // Old receipts have no display snapshot. A new session may reuse their run ID;
345
370
  // the current tracker cannot supply a historical alias for that unrelated execution.
@@ -350,9 +375,9 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
350
375
  }
351
376
  const first = result.content[0];
352
377
  const full = sanitizeTerminalText(first?.type === "text" ? first.text : "(no output)");
353
- const failed = details.ok === false;
378
+ const failed = (context?.isError ?? result.isError) || details.ok === false;
354
379
  const prefix = failed ? `${theme.fg("error", theme.bold("failed"))}\n` : "";
355
- if (details.action === "steer" && details.ok === true) {
380
+ if (!failed && details.action === "steer" && details.ok === true) {
356
381
  const target = compactInlineText(details.target ?? state?.target?.label ?? (legacyCompleted && typeof savedId === "string" ? sanitizeDisplayLabel(savedId, "run") : details.runId ? displayRunTarget(details.runId) : ""), { maxChars: 80 });
357
382
  const message = typeof details.message === "string" ? details.message : context?.args?.message;
358
383
  // Old persisted receipts may predate message snapshots. Keep their original content
@@ -370,7 +395,56 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
370
395
  const showTitle = !state?.callText || !preview;
371
396
  return new Text(`${showTitle ? theme.fg("success", title) : ""}${preview ? `${showTitle ? "\n" : ""}${theme.fg("toolOutput", preview.text)}${preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : ""}` : ""}`, 0, 0);
372
397
  }
373
- if (expanded) return new Text(`${prefix}${theme.fg("toolOutput", full)}`, 0, 0);
398
+ // Archive views are listings/diagnostics, never Markdown reports. A successful archive
399
+ // read carries no runId (live results do) and a summary (its own receipt); the invalid
400
+ // argument mix and failed reads fall through to the literal failure rendering below.
401
+ const listing = details.action === "results" && details.ok === true && Array.isArray(details.results) ? details.results : undefined;
402
+ const archivedPage = details.action === "result" && details.ok === true
403
+ && typeof details.summary === "object" && details.summary !== null && details.runId === undefined
404
+ ? details.summary
405
+ : undefined;
406
+ if (expanded) {
407
+ // Archive pages/listings stay literal via the shared renderer — complete sanitized
408
+ // content, no Markdown re-render of a quote-fenced page or a ref listing.
409
+ if (!failed && (listing || archivedPage)) {
410
+ return renderLiteralToolResult(result, { expanded }, theme, context);
411
+ }
412
+ // Explicit report/message actions only; missing/running diagnostics and control
413
+ // receipts remain literal. Older history with no action is not guessed from prose.
414
+ if (!failed && ["result", "message", "inbox"].includes(action ?? "")) {
415
+ return createReportBody(full, theme, {}, context?.lastComponent);
416
+ }
417
+ return new Text(`${prefix}${theme.fg("toolOutput", full)}`, 0, 0);
418
+ }
419
+ if (!failed && listing) {
420
+ // Purpose line first (count + paging/truncation/storage honesty), then a bounded
421
+ // listing preview; the payload's own header line is replaced by the purpose line.
422
+ const truncated = listing.filter((entry) => entry?.truncated === true).length;
423
+ const flags = [
424
+ ...(details.nextCursor !== undefined ? [`next page: cursor ${details.nextCursor}`] : []),
425
+ ...(truncated > 0 ? [`${truncated} truncated`] : []),
426
+ ...(details.warnings?.length ? [`warnings: ${details.warnings.join(", ")}`] : []),
427
+ ];
428
+ const purpose = compactInlineText(`results · ${listing.length} retained${flags.length ? ` · ${flags.join(" · ")}` : ""}`, { maxChars: 100 });
429
+ const preview = compactVisibleText(full.split("\n").slice(1).join("\n"), { maxLines: 3, maxLineChars: 100 });
430
+ return new Text(`${theme.fg("success", purpose)}\n${theme.fg("toolOutput", preview.text)}${preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : ""}`, 0, 0);
431
+ }
432
+ if (!failed && archivedPage) {
433
+ // Page position reads against the STORED body (originals may be truncated); honesty
434
+ // flags surface collapsed so a memory-only or truncated receipt is never mistaken
435
+ // for a durable whole report without expanding.
436
+ const end = details.nextOffset ?? archivedPage.storedChars ?? 0;
437
+ const flags = [
438
+ ...(archivedPage.truncated ? ["truncated"] : []),
439
+ ...(archivedPage.storage === "memory-only" ? ["memory-only"] : []),
440
+ ...(details.warnings?.length ? [`warnings: ${details.warnings.join(", ")}`] : []),
441
+ ];
442
+ const purpose = compactInlineText(`result · ${archivedPage.ref ?? "?"} · page ${end}/${archivedPage.storedChars ?? "?"}${flags.length ? ` · ${flags.join(" · ")}` : ""}`, { maxChars: 100 });
443
+ // The payload's receipt header and paging diagnostic line are replaced by the purpose
444
+ // line; the fence wrapper is dropped by the preview, leaving the page body quoted.
445
+ const preview = compactVisibleText(full.split("\n").slice(2).join("\n"), { maxLines: 3, maxLineChars: 100 });
446
+ return new Text(`${theme.fg("success", purpose)}\n${theme.fg("toolOutput", preview.text)}${preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : ""}`, 0, 0);
447
+ }
374
448
  const preview = compactVisibleText(full, { maxLines: 4, maxLineChars: 100 });
375
449
  const hint = preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : "";
376
450
  return new Text(`${prefix}${theme.fg("toolOutput", preview.text)}${hint}`, 0, 0);
@@ -120,7 +120,7 @@ export function runIntercom(params: IntercomParams, bus: InProcessBus, self: str
120
120
  }
121
121
  case "message": {
122
122
  if (!params.messageId) {
123
- return { text: "intercom message needs { messageId: <message id> }.", details: { action: "message", ok: false } };
123
+ return { text: "intercom message needs { messageId: <message id> }. To message a running child use steer { to: <run id>, message }.", details: { action: "message", ok: false } };
124
124
  }
125
125
  if (params.messageId.length > MAX_INTERCOM_REF_CHARS) {
126
126
  return { text: `intercom messageId exceeds the ${MAX_INTERCOM_REF_CHARS}-character limit.`, details: { action: "message", ok: false } };
@@ -3,11 +3,12 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
3
3
  import { Type } from "typebox";
4
4
  import { Text } from "@earendil-works/pi-tui";
5
5
  import { compactInlineText } from "../ui/presentation.ts";
6
+ import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
6
7
 
7
8
  export interface ModelsToolDeps {
8
9
  get lastCtx(): ExtensionContext | undefined;
9
10
  set lastCtx(value: ExtensionContext | undefined);
10
- searchModels(ctx: ExtensionContext, query?: string): { lines: string[]; total: number; capped: boolean };
11
+ searchModels(ctx: ExtensionContext, query?: string): { lines: string[]; total: number; capped: boolean; shortlist: boolean };
11
12
  }
12
13
 
13
14
  export function registerModelsTool(pi: ExtensionAPI, d: ModelsToolDeps): void {
@@ -16,24 +17,48 @@ export function registerModelsTool(pi: ExtensionAPI, d: ModelsToolDeps): void {
16
17
  name: "models",
17
18
  label: "Models",
18
19
  description: [
19
- "List or search the available model ids (provider/id). The same name exists under several",
20
- "providers — use an EXACT id from here as a delegate task's `model`. ★ marks your session",
21
- "provider; prefer it (it's the authenticated one).",
20
+ "Discover model candidates (provider/id). With no query: the current model first, then a compact",
21
+ "shortlist of nearby family/version candidates from the same provider, ordered cost:low → medium → high → unknown.",
22
+ "Family and version names are proximity hints, not verified release dates or account support. A query searches",
23
+ "the full eligible catalog, including older models and snapshots. Use exact refs; ★ marks your",
24
+ "session provider, not account support. Authentication does not guarantee access to every listed model.",
25
+ "cost:low/medium/high ranks advertised API prices relative to recent candidates of the same provider;",
26
+ "cost:unknown means insufficient price data. These are cost bands, not quality, speed or reasoning-effort ratings; choose task fit yourself.",
27
+ "Context, reasoning support and API prices are host catalog metadata; API prices do not describe subscription billing.",
28
+ "Prefer these nearby candidates before older or unrelated models merely because they are cheap. Omitting `model` preserves",
29
+ "saved per-persona role/agent assignments, then the agent frontmatter model and engine defaults; it does not necessarily select the supervisor model.",
30
+ "To intentionally choose the supervisor's current model over a configured choice, pass its exact provider/id as `model` only when explicitly authorized.",
31
+ "Never silently switch providers or billing routes.",
22
32
  ].join(" "),
23
33
  parameters: Type.Object({
24
- query: Type.Optional(Type.String({ description: "Filter by substring (provider or id), e.g. 'sonnet'" })),
34
+ query: Type.Optional(Type.String({ description: "Search the full catalog by substring (provider or id), e.g. 'sonnet'; omit for the current-first shortlist" })),
25
35
  }),
26
36
  async execute(_id, params, _signal, _onUpdate, ctx) {
27
37
  d.lastCtx = ctx;
28
- const { lines, total, capped } = d.searchModels(ctx, params.query);
38
+ const { lines, total, capped, shortlist } = d.searchModels(ctx, params.query);
39
+ const showing = shortlist || capped ? ` (showing ${lines.length} ${shortlist ? "nearby family/version candidates" : "matches"})` : "";
29
40
  const text = lines.length
30
- ? `${total} model(s)${capped ? " (showing 40 — refine with a query)" : ""}; ★ = session provider:\n${lines.join("\n")}`
41
+ ? `${total} catalog model(s)${showing}; ★ = session provider, (you) = current model. Account support may vary.${shortlist || capped ? " Query searches the full catalog." : ""}\n${lines.join("\n")}`
31
42
  : `no models match "${params.query ?? ""}"`;
32
- return { content: [{ type: "text", text }], details: { total }, isError: false };
43
+ return { content: [{ type: "text", text }], details: { total, shown: lines.length, shortlist }, isError: false };
33
44
  },
34
45
  renderCall(args, theme) {
35
- const query = compactInlineText(args.query ?? "(all)", { maxChars: 96 }) || "(all)";
46
+ const query = compactInlineText(args.query ?? "(shortlist)", { maxChars: 96 }) || "(shortlist)";
36
47
  return new Text(`${theme.fg("toolTitle", theme.bold("models "))}${theme.fg("dim", query)}`, 0, 0);
37
48
  },
49
+ renderResult(result, { expanded }, theme, context) {
50
+ // The catalog itself is literal data: the shared renderer bounds it collapsed and
51
+ // shows the complete sanitized listing expanded.
52
+ if (expanded) return renderLiteralToolResult(result, { expanded }, theme, context);
53
+ const failed = context?.isError ?? result.isError ?? false;
54
+ if (failed) return renderLiteralToolResult(result, { expanded: false }, theme, context);
55
+ const details = (result.details ?? {}) as { total?: number; shown?: number; shortlist?: boolean };
56
+ const total = typeof details.total === "number" && Number.isFinite(details.total) ? details.total : 0;
57
+ const shown = typeof details.shown === "number" && Number.isFinite(details.shown) ? details.shown : 0;
58
+ const summary = shown === 0
59
+ ? "no catalog matches"
60
+ : `${total} catalog models · showing ${shown}${details.shortlist ? " nearby candidates" : " matches"}`;
61
+ return new Text(`${theme.fg("toolTitle", theme.bold("models · "))}${theme.fg("accent", compactInlineText(summary, { maxChars: 96 }))}`, 0, 0);
62
+ },
38
63
  });
39
64
  }