@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
@@ -1,6 +1,10 @@
1
1
  import { compactTokens } from "../core/format.ts";
2
+ import { effortFullName, effortSigla } from "../core/leg-thinking.ts";
2
3
  import { formatDuration } from "../core/time.ts";
4
+ import type { ThinkingLevel } from "../core/types.ts";
5
+ import type { ChildUsage } from "../engine/stream.ts";
3
6
  import { compactInlineText } from "./presentation.ts";
7
+ import { formatUsage } from "./usage.ts";
4
8
 
5
9
  /**
6
10
  * The unified agent tree — a single live model of *every* agent in flight,
@@ -11,7 +15,7 @@ import { compactInlineText } from "./presentation.ts";
11
15
  */
12
16
 
13
17
  export type AgentNodeStatus = "running" | "done" | "failed" | "stopped";
14
- export type AgentNodeKind = "subagent" | "delegate" | "council" | "flow" | "phase";
18
+ export type AgentNodeKind = "subagent" | "delegate" | "council" | "flow" | "phase" | "monitor" | "timer";
15
19
 
16
20
  export interface AgentNode {
17
21
  id: string;
@@ -23,6 +27,13 @@ export interface AgentNode {
23
27
  /** Configured agent definition (operator/reviewer/…), if known. */
24
28
  agent?: string;
25
29
  model?: string;
30
+ /** Requested per-leg thinking effort, known at launch (the spec-level value the launch row
31
+ * carried). Rendered as the compact sigla right after the model; absent/off render nothing. */
32
+ thinking?: ThinkingLevel;
33
+ /** Observed effective Pi effort from the terminal result. Set ONLY from a real
34
+ * AgentResult.thinkingEffective (never invented); when it diverges from `thinking`, the row
35
+ * renders the honest `xh→h` form. */
36
+ thinkingEffective?: ThinkingLevel;
26
37
  /** A short trailing annotation (usage, current activity), shown after the label. */
27
38
  detail: string | undefined;
28
39
  /** The agent's output buffer, shown when the user drills into the node. */
@@ -31,6 +42,8 @@ export interface AgentNode {
31
42
  * Excluded from change notifications — the live clock paints them. */
32
43
  startedAt?: number;
33
44
  lastAdvanceAt?: number;
45
+ /** Epoch ms when a wake source is due: a monitor's deadline or a timer's fire time. Its countdown replaces the elapsed/stall clock. Set once by the source; a change notifies (it is not a clock key). */
46
+ dueAt?: number;
34
47
  }
35
48
 
36
49
  export type AgentTreeChange =
@@ -60,6 +73,21 @@ export function flattenTree(nodes: AgentNode[]): FlatRow[] {
60
73
  /** Status → glyph, shared by every agent surface (tree, overlay). */
61
74
  export const GLYPH: Record<AgentNodeStatus, string> = { running: "⏳", done: "✓", failed: "✗", stopped: "■" };
62
75
 
76
+ /** A wake source is a row that is not an agent (never counted, steered, or stopped in bulk). */
77
+ export function isWakeSource(node: Pick<AgentNode, "kind">): boolean {
78
+ return node.kind === "monitor" || node.kind === "timer";
79
+ }
80
+
81
+ /** The kind-aware glyph shared by every surface (tree, overlay): a running timer is ⏰, a running
82
+ * monitor ◉; everything else, settled wake rows included, uses the ordinary status glyph. */
83
+ export function nodeGlyph(node: Pick<AgentNode, "status" | "kind">): string {
84
+ if (node.status === "running") {
85
+ if (node.kind === "timer") return "⏰";
86
+ if (node.kind === "monitor") return "◉";
87
+ }
88
+ return GLYPH[node.status];
89
+ }
90
+
63
91
  export interface RenderOptions {
64
92
  /** Current time; omitted ⇒ no elapsed/stall annotation (pure renders stay byte-identical). */
65
93
  now?: number;
@@ -69,6 +97,28 @@ export interface RenderOptions {
69
97
  waitingForSupervisor?: ReadonlySet<string>;
70
98
  }
71
99
 
100
+ /** The frozen compact effort tag for a tree/overlay row: ` · xh` after the model, or ` · xh→h`
101
+ * when the terminal result reported a DIFFERENT observed effective level. `off`/absent render
102
+ * nothing — absence means off, and an observation is never invented: the divergence form shows
103
+ * only when both sides are known and genuinely differ (an observed `off` never leaves a dangling
104
+ * `→`). Pure. */
105
+ export function effortTag(requested: ThinkingLevel | undefined, effective?: ThinkingLevel): string {
106
+ const req = requested !== undefined ? effortSigla(requested) : "";
107
+ if (!req) return "";
108
+ const obs = effective !== undefined ? effortSigla(effective) : "";
109
+ return obs && obs !== req ? ` · ${req}→${obs}` : ` · ${req}`;
110
+ }
111
+
112
+ /** The F9 drill-in line for a leg's effort — FULL names, the drill-in has the room (the compact
113
+ * siglas live only on the rows): `thinking: requested xhigh → observed high`. Same honesty rules
114
+ * as {@link effortTag}; the line is omitted entirely when nothing was requested. Pure. */
115
+ export function thinkingDetailLine(requested: ThinkingLevel | undefined, effective?: ThinkingLevel): string {
116
+ const req = requested !== undefined ? effortFullName(requested) : "";
117
+ if (!req) return "";
118
+ const obs = effective !== undefined && effective !== requested ? effortFullName(effective) : "";
119
+ return obs ? `thinking: requested ${req} → observed ${obs}` : `thinking: requested ${req}`;
120
+ }
121
+
72
122
  /** Whether `detail` is the literal "queued" marker a seeded-but-not-yet-live leg carries — shared
73
123
  * by `runningAnnotation` (suppresses the clock) and `telemetryStatus` (`extension.ts`, reports the
74
124
  * `queued` state). One predicate so the two readings of the marker cannot drift apart. */
@@ -79,11 +129,20 @@ export function isQueuedMarker(detail: string | undefined): boolean {
79
129
  /** "1m 15s" while a running node keeps advancing; "⚠ stalled 1m 30s" once it has been quiet
80
130
  * for `stallMs`. Undefined for settled nodes, nodes without clock data, or a node still carrying
81
131
  * the "queued" marker — it was seeded ahead of the concurrency gate and has not started running
82
- * yet, so its clock (stamped at seed time) has nothing true to report. Pure. */
83
- export function runningAnnotation(node: Pick<AgentNode, "status" | "startedAt" | "lastAdvanceAt" | "detail">, now: number, stallMs: number, waitingForSupervisor = false): string | undefined {
132
+ * yet, so its clock (stamped at seed time) has nothing true to report. A wake-source row (one with
133
+ * a `dueAt`) counts down instead — "fires in 49m 12s" for a timer, "12m 3s · deadline in 1h 47m"
134
+ * for a monitor — and NEVER shows the stall badge: a quiet monitor is waiting for its event,
135
+ * not stalled. Pure. */
136
+ export function runningAnnotation(node: Pick<AgentNode, "status" | "startedAt" | "lastAdvanceAt" | "detail" | "kind" | "dueAt">, now: number, stallMs: number, waitingForSupervisor = false): string | undefined {
84
137
  if (node.status !== "running" || node.startedAt === undefined) return undefined;
85
138
  if (isQueuedMarker(node.detail)) return undefined;
86
139
  if (waitingForSupervisor) return "waiting for supervisor";
140
+ if (node.dueAt !== undefined) {
141
+ const left = node.dueAt - now;
142
+ if (node.kind === "timer") return left > 0 ? `fires in ${formatDuration(left)}` : "firing…";
143
+ const elapsed = formatDuration(now - node.startedAt);
144
+ return left > 0 ? `${elapsed} · deadline in ${formatDuration(left)}` : `${elapsed} · deadline reached`;
145
+ }
87
146
  const quietSince = node.lastAdvanceAt ?? node.startedAt;
88
147
  if (stallMs > 0 && now - quietSince >= stallMs) return `⚠ stalled ${formatDuration(now - quietSince)}`;
89
148
  return formatDuration(now - node.startedAt);
@@ -118,12 +177,13 @@ export function deriveWaitingNodeIds(nodes: readonly AgentNode[], isWaiting: (id
118
177
  }
119
178
 
120
179
  /** The tree patch for one progress snapshot: every snapshot is an advance; activity wins over
121
- * a bare token count as the visible detail. One helper for every progress site. */
122
- export function progressPatch(snap: { output?: string; activity?: string; tokens?: number }, now: number): AgentNodePatch {
180
+ * usage, formatted like a completed row. Legacy aggregate-only progress stays a neutral count. */
181
+ export function progressPatch(snap: { output?: string; activity?: string; tokens?: number; usage?: ChildUsage }, now: number): AgentNodePatch {
123
182
  const patch: AgentNodePatch = { lastAdvanceAt: now };
124
183
  if (snap.output) patch.output = snap.output;
125
184
  if (snap.activity) patch.detail = snap.activity;
126
- else if (snap.tokens) patch.detail = `${compactTokens(snap.tokens)} tok`;
185
+ else if (snap.usage) patch.detail = formatUsage(snap.usage);
186
+ else if (snap.tokens) patch.detail = compactTokens(snap.tokens);
127
187
  return patch;
128
188
  }
129
189
 
@@ -150,12 +210,15 @@ export function renderAgentTree(nodes: AgentNode[], opts: RenderOptions = {}): s
150
210
  const isRoot = parentId === undefined;
151
211
  const isLast = i === kids.length - 1;
152
212
  const branch = isRoot ? "" : isLast ? "└─ " : "├─ ";
153
- const label = safeInline(node.label) || safeInline(node.id) || "agent";
213
+ const base = safeInline(node.label) || safeInline(node.id) || "agent";
214
+ // The compact effort sigla rides AFTER the label clamp — the existing per-part clipping is
215
+ // untouched; the appended field is at most ` · xh→h` wide.
216
+ const label = `${base}${effortTag(node.thinking, node.thinkingEffective)}`;
154
217
  const detailText = node.detail ? safeInline(node.detail) : "";
155
218
  const clock = opts.now !== undefined ? runningAnnotation(node, opts.now, opts.stallMs ?? 0, opts.waitingForSupervisor?.has(node.id) ?? false) : undefined;
156
219
  const tail = [detailText, clock].filter((s): s is string => Boolean(s)).join(" · ");
157
220
  const detail = tail ? ` ${tail}` : "";
158
- lines.push(`${prefix}${branch}${GLYPH[node.status]} ${label}${detail}`);
221
+ lines.push(`${prefix}${branch}${nodeGlyph(node)} ${label}${detail}`);
159
222
  const childPrefix = isRoot ? "" : `${prefix}${isLast ? " " : "│ "}`;
160
223
  walk(node.id, childPrefix);
161
224
  });
@@ -165,14 +228,41 @@ export function renderAgentTree(nodes: AgentNode[], opts: RenderOptions = {}): s
165
228
  return lines;
166
229
  }
167
230
 
231
+ /** One digest line for the running wake sources (monitors, timers) that fell below the summary
232
+ * fold: counts by kind ("◉ 1 monitor · ⏰ 2 timers"), plus the countdown to the soonest finite
233
+ * `dueAt` when a clock is given. A monitor armed after a fan-out would otherwise hide behind the
234
+ * "… +N more" line — exactly the "armed but nothing visible" case wake rows exist to fix. Pure. */
235
+ function wakeDigestLine(hidden: readonly AgentNode[], now: number | undefined): string {
236
+ const monitors = hidden.filter((node) => node.kind === "monitor").length;
237
+ const timers = hidden.filter((node) => node.kind === "timer").length;
238
+ const parts: string[] = [];
239
+ if (monitors > 0) parts.push(`${nodeGlyph({ status: "running", kind: "monitor" })} ${monitors} monitor${monitors === 1 ? "" : "s"}`);
240
+ if (timers > 0) parts.push(`${nodeGlyph({ status: "running", kind: "timer" })} ${timers} timer${timers === 1 ? "" : "s"}`);
241
+ if (now !== undefined) {
242
+ const due = hidden.map((node) => node.dueAt).filter((at): at is number => at !== undefined && Number.isFinite(at));
243
+ if (due.length > 0) {
244
+ const left = Math.min(...due) - now;
245
+ parts.push(left > 0 ? `next due in ${formatDuration(left)}` : "next due now");
246
+ }
247
+ }
248
+ return parts.join(" · ");
249
+ }
250
+
168
251
  /**
169
252
  * Bounded above-editor digest. Large fan-outs belong in the scrollable F9
170
253
  * overlay; they must not push the editor and conversation off the screen.
254
+ *
255
+ * Below the fold, two kinds of row stay visible as one line each, because an operator must not
256
+ * have to open F9 to learn them: failures (`✗ N failed: …`) and RUNNING wake sources (`◉ 1 monitor
257
+ * · ⏰ 2 timers · next due in 4m 12s`). The layout when overflowing is
258
+ * `[...rows, failureLine?, wakeDigest?, overflowLine]`, every line inside `maxRows`; when the budget
259
+ * cannot hold them all the lowest-priority line goes first (failure > wake digest > overflow).
171
260
  */
172
261
  export function renderAgentTreeSummary(nodes: AgentNode[], maxRows = 8, opts: RenderOptions = {}): string[] {
173
262
  const rows = renderAgentTree(nodes, opts);
174
263
  const limit = Math.max(1, Math.floor(maxRows));
175
264
  if (rows.length <= limit) return rows;
265
+ let failureLine: string | undefined;
176
266
  const failed = nodes.filter((node) => node.status === "failed");
177
267
  if (failed.length > 0) {
178
268
  const names = failed.slice(0, 2).map((node) => {
@@ -181,20 +271,26 @@ export function renderAgentTreeSummary(nodes: AgentNode[], maxRows = 8, opts: Re
181
271
  return detail ? `${label} (${detail})` : label;
182
272
  });
183
273
  const extra = failed.length - names.length;
184
- const failureLine = `✗ ${failed.length} failed: ${names.join(", ")}${extra > 0 ? `, +${extra} more` : ""}`;
185
- if (limit === 1) return [failureLine];
186
- const visibleCount = Math.max(0, limit - 2);
187
- return [
188
- ...rows.slice(0, visibleCount),
189
- failureLine,
190
- `… +${rows.length - visibleCount} more · F9 or /agents for the full live tree`,
191
- ];
274
+ failureLine = `✗ ${failed.length} failed: ${names.join(", ")}${extra > 0 ? `, +${extra} more` : ""}`;
275
+ }
276
+ // `flattenTree(nodes)[i]` is the node behind `rows[i]`: `renderAgentTree` and `flattenTree` walk the
277
+ // same parent→children order, so a row's flat index doubles as its position in the rendered list.
278
+ const flat = flattenTree(nodes);
279
+ const hiddenWake = (from: number): AgentNode[] =>
280
+ flat.slice(from).map((row) => row.node).filter((node) => isWakeSource(node) && node.status === "running");
281
+ let visibleCount = Math.max(0, limit - (failureLine !== undefined ? 2 : 1));
282
+ let hidden = hiddenWake(visibleCount);
283
+ if (hidden.length > 0) {
284
+ // The digest takes a slot of its own; one fewer visible row can only hide more wake rows.
285
+ visibleCount = Math.max(0, visibleCount - 1);
286
+ hidden = hiddenWake(visibleCount);
192
287
  }
193
- const visibleCount = Math.max(0, limit - 1);
194
- return [
195
- ...rows.slice(0, visibleCount),
288
+ const tail = [
289
+ ...(failureLine !== undefined ? [failureLine] : []),
290
+ ...(hidden.length > 0 ? [wakeDigestLine(hidden, opts.now)] : []),
196
291
  `… +${rows.length - visibleCount} more · F9 or /agents for the full live tree`,
197
292
  ];
293
+ return [...rows.slice(0, visibleCount), ...tail.slice(0, Math.max(0, limit - visibleCount))];
198
294
  }
199
295
 
200
296
  export interface AddNodeInput {
@@ -205,10 +301,13 @@ export interface AddNodeInput {
205
301
  kind?: AgentNodeKind;
206
302
  agent?: string;
207
303
  model?: string;
304
+ thinking?: ThinkingLevel;
305
+ thinkingEffective?: ThinkingLevel;
208
306
  detail?: string;
209
307
  output?: string;
210
308
  startedAt?: number;
211
309
  lastAdvanceAt?: number;
310
+ dueAt?: number;
212
311
  }
213
312
 
214
313
  export interface AgentNodePatch {
@@ -216,10 +315,13 @@ export interface AgentNodePatch {
216
315
  kind?: AgentNodeKind;
217
316
  agent?: string;
218
317
  model?: string;
318
+ thinking?: ThinkingLevel;
319
+ thinkingEffective?: ThinkingLevel;
219
320
  detail?: string;
220
321
  output?: string;
221
322
  startedAt?: number;
222
323
  lastAdvanceAt?: number;
324
+ dueAt?: number;
223
325
  }
224
326
 
225
327
  /** Apply the fields `add`'s upsert branch and `update` both accept, in place — the one place
@@ -229,10 +331,13 @@ function applyPatch(node: AgentNode, patch: AgentNodePatch): void {
229
331
  if (patch.kind !== undefined) node.kind = patch.kind;
230
332
  if (patch.agent !== undefined) node.agent = patch.agent;
231
333
  if (patch.model !== undefined) node.model = patch.model;
334
+ if (patch.thinking !== undefined) node.thinking = patch.thinking;
335
+ if (patch.thinkingEffective !== undefined) node.thinkingEffective = patch.thinkingEffective;
232
336
  if (patch.detail !== undefined) node.detail = patch.detail;
233
337
  if (patch.output !== undefined) node.output = patch.output;
234
338
  if (patch.startedAt !== undefined) node.startedAt = patch.startedAt;
235
339
  if (patch.lastAdvanceAt !== undefined) node.lastAdvanceAt = patch.lastAdvanceAt;
340
+ if (patch.dueAt !== undefined) node.dueAt = patch.dueAt;
236
341
  }
237
342
 
238
343
  /** A small mutable registry with change notification. The extension owns one. */
@@ -268,7 +373,10 @@ export class AgentTree {
268
373
  ...(input.kind !== undefined ? { kind: input.kind } : {}),
269
374
  ...(input.agent !== undefined ? { agent: input.agent } : {}),
270
375
  ...(input.model !== undefined ? { model: input.model } : {}),
376
+ ...(input.thinking !== undefined ? { thinking: input.thinking } : {}),
377
+ ...(input.thinkingEffective !== undefined ? { thinkingEffective: input.thinkingEffective } : {}),
271
378
  ...(input.output !== undefined ? { output: input.output } : {}),
379
+ ...(input.dueAt !== undefined ? { dueAt: input.dueAt } : {}),
272
380
  };
273
381
  this.nodes.push(node);
274
382
  this.emit({ type: "added", node: { ...node } });
@@ -0,0 +1,25 @@
1
+ import { type Component, sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
2
+ import { sanitizeTerminalText } from "./presentation.ts";
3
+
4
+ /**
5
+ * Public setWidget factory for already-budgeted, raw-text rows. Unlike the host's
6
+ * string[]/Text path this never wraps: one source row occupies at most one terminal
7
+ * row. Right-edge clipping preserves tree indentation and leading semantic markers;
8
+ * F9/exocom_list remain the full-detail surfaces.
9
+ *
10
+ * Snapshot and sanitize once per publication, without trimming/collapsing indentation.
11
+ * No theme colors are captured; each render uses its supplied width (including resize).
12
+ */
13
+ export function compactWidget(rows: readonly string[]): () => Component {
14
+ const lines = rows.map((row) => sanitizeTerminalText(row.replace(/\t/g, " ")).replace(/[\n\u2028\u2029]/g, " "));
15
+ return () => ({
16
+ render(width: number): string[] {
17
+ const columns = Math.max(0, Math.floor(width));
18
+ if (columns === 0) return [];
19
+ return lines.map((line) => visibleWidth(line) <= columns
20
+ ? line
21
+ : `${sliceByColumn(line, 0, columns - 1, true)}…`);
22
+ },
23
+ invalidate(): void { /* No width/theme-dependent cache. */ },
24
+ });
25
+ }
@@ -0,0 +1,55 @@
1
+ import { compactInlineText, sanitizeTerminalText } from "./presentation.ts";
2
+
3
+ const MAX_INSPECTED_CHARS = 16_384;
4
+ const MAX_SUMMARY_COLUMNS = 140;
5
+
6
+ /** Display-only cause preview. The caller retains the original error for diagnostics. */
7
+ export function summarizeFailure(error: string): string {
8
+ let source = sanitizeTerminalText(error.slice(0, MAX_INSPECTED_CHARS)).trim();
9
+ // Omit only the runtime's identity/model wrapper, not meaningful tags such as [BLOCKED].
10
+ const wrapperEnd = source.startsWith("[") ? source.indexOf("]") : -1;
11
+ if (wrapperEnd > 0 && wrapperEnd <= 512 && source.slice(1, wrapperEnd).includes(" · ")) source = source.slice(wrapperEnd + 1).trim();
12
+ const reason = sanitizeTerminalText(jsonCause(source) ?? source).replace(/\s+/g, " ").trim();
13
+ if (/^(?:(?:the|this)\s+)?(?:['"`][^'"`]{1,200}['"`]\s+)?model\s+is\s+not\s+supported\s+when\s+using\s+Codex\s+with\s+a\s+ChatGPT\s+account[.!]?$/i.test(reason)) {
14
+ return "Model not supported by this ChatGPT account.";
15
+ }
16
+ return compactInlineText(reason, { maxChars: MAX_SUMMARY_COLUMNS });
17
+ }
18
+
19
+ /** Scan one bounded object, respecting JSON strings; never retry parsing at every brace. */
20
+ function jsonCause(source: string): string | undefined {
21
+ const start = source.indexOf("{");
22
+ if (start < 0) return undefined;
23
+ let depth = 0;
24
+ let quoted = false;
25
+ let escaped = false;
26
+ for (let index = start; index < source.length; index++) {
27
+ const char = source[index];
28
+ if (quoted) {
29
+ if (escaped) escaped = false;
30
+ else if (char === "\\") escaped = true;
31
+ else if (char === '"') quoted = false;
32
+ } else if (char === '"') quoted = true;
33
+ else if (char === "{") depth++;
34
+ else if (char === "}" && --depth === 0) {
35
+ try { return objectCause(JSON.parse(source.slice(start, index + 1)) as unknown); }
36
+ catch { return undefined; }
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ function objectCause(value: unknown): string | undefined {
43
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
44
+ const object = value as Record<string, unknown>;
45
+ for (const field of [object.detail, object.message, object.error]) {
46
+ if (typeof field === "string" && field.trim()) return field;
47
+ }
48
+ if (object.error !== null && typeof object.error === "object" && !Array.isArray(object.error)) {
49
+ const error = object.error as Record<string, unknown>;
50
+ for (const field of [error.message, error.detail]) {
51
+ if (typeof field === "string" && field.trim()) return field;
52
+ }
53
+ }
54
+ return undefined;
55
+ }
@@ -0,0 +1,24 @@
1
+ /** One compact renderer for literal control receipts and diagnostic fallbacks. Pi owns the shell. */
2
+ import { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { compactVisibleText, sanitizeTerminalText } from "./presentation.ts";
5
+
6
+ interface LiteralResult {
7
+ content: readonly { type: string; text?: string }[];
8
+ /** Direct internal renderer calls carry this; Pi passes it in the render context. */
9
+ isError?: boolean;
10
+ }
11
+
12
+ export function renderLiteralToolResult(
13
+ result: LiteralResult,
14
+ { expanded }: { expanded: boolean },
15
+ theme: Theme,
16
+ context?: { isError: boolean },
17
+ ): Text {
18
+ const full = sanitizeTerminalText(result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n")) || "(no output)";
19
+ const failed = context?.isError ?? result.isError ?? false;
20
+ const preview = expanded ? undefined : compactVisibleText(full, { maxLines: 4, maxLineChars: 100 });
21
+ const heading = failed ? `${theme.fg("error", theme.bold("failed"))}\n` : "";
22
+ const hint = preview?.truncated ? `\n${theme.fg("dim", keyHint("app.tools.expand", "to expand"))}` : "";
23
+ return new Text(`${heading}${theme.fg("toolOutput", preview?.text ?? full)}${hint}`, 0, 0);
24
+ }