@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
@@ -19,11 +19,13 @@ import { type Static, Type } from "typebox";
19
19
 
20
20
  import { keyHint, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
21
  import { Text } from "@earendil-works/pi-tui";
22
+ import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
22
23
 
23
24
  import { fencePeer } from "../core/fence.ts";
24
25
  import { inventedExocomNameHint } from "../core/naming.ts";
25
26
  import { MAX_IDENTITY_NAME_CHARS } from "../core/session-identity.ts";
26
27
  import { ExocomPeerRejection, type DisplayPeer, type ExocomPlane } from "../exocom/plane.ts";
28
+ import { normalizeExocomProfile, type ExocomProfile } from "../exocom/profile.ts";
27
29
  import { normalizeMetadataText, normalizePeerName } from "../exocom/registry.ts";
28
30
  import type { ExocomScope } from "../exocom/scope.ts";
29
31
  import { compactInlineText } from "../ui/presentation.ts";
@@ -85,6 +87,7 @@ type ExocomPeerSummary = Pick<DisplayPeer, "displayName" | "persona" | "model" |
85
87
  workspace_code?: string;
86
88
  workspace_label?: string;
87
89
  sameWorkspace?: boolean;
90
+ profile?: ExocomProfile;
88
91
  };
89
92
 
90
93
  function peerWord(count: number): string {
@@ -95,6 +98,14 @@ function peerWord(count: number): string {
95
98
  * model so it can retain useful detail without turning one peer into a 300-column wall. */
96
99
  function peerRows(peer: ExocomPeerSummary, expanded: boolean): string[] {
97
100
  const context = `${Math.max(0, Math.min(100, Number(peer.context_pct) || 0))}%`;
101
+ const availability = peer.profile?.availability ?? "unknown";
102
+ const capabilities = peer.profile?.capabilities ?? [];
103
+ const visibleCapabilities = capabilities.slice(0, expanded ? 8 : 2).join(" · ")
104
+ || "capabilities not reported";
105
+ const omittedCapabilities = Math.max(0, capabilities.length - (expanded ? 8 : 2));
106
+ const profileLine = expanded
107
+ ? ` self-reported availability: ${availability} · capabilities: ${compactInlineText(visibleCapabilities, { maxChars: 420 })}${omittedCapabilities ? ` · +${omittedCapabilities} more` : ""}`
108
+ : ` self-reported: ${availability} · ${compactInlineText(visibleCapabilities, { maxChars: 44 })}${omittedCapabilities ? ` · +${omittedCapabilities} more` : ""}`;
98
109
  const workspace = peer.workspace_code && peer.workspace_label
99
110
  ? ` workspace: ${compactInlineText(peer.workspace_label, { maxChars: expanded ? 64 : 40 }) || "workspace"} [${peer.workspace_code}] · ${peer.sameWorkspace === false ? "external" : "same"}`
100
111
  : undefined;
@@ -102,12 +113,12 @@ function peerRows(peer: ExocomPeerSummary, expanded: boolean): string[] {
102
113
  const name = compactInlineText(peer.displayName, { maxChars: 48 }) || "peer";
103
114
  const persona = compactInlineText(peer.persona, { maxChars: 32 }) || "unknown";
104
115
  const model = compactInlineText(peer.model, { maxChars: 88 }) || "unknown";
105
- return [`- ${name} · ${persona} · ctx ${context}`, ` model: ${model}`, ...(workspace ? [workspace] : [])];
116
+ return [`- ${name} · ${persona} · ctx ${context}`, ` model: ${model}`, profileLine, ...(workspace ? [workspace] : [])];
106
117
  }
107
118
  const name = compactInlineText(peer.displayName, { maxChars: 24 }) || "peer";
108
119
  const persona = compactInlineText(peer.persona, { maxChars: 18 }) || "unknown";
109
120
  const model = compactInlineText(peer.model, { maxChars: 36 }) || "unknown";
110
- return [`- ${name} · ${persona} · ${model} · ctx ${context}`, ...(workspace ? [workspace] : [])];
121
+ return [`- ${name} · ${persona} · ${model} · ctx ${context}`, profileLine, ...(workspace ? [workspace] : [])];
111
122
  }
112
123
 
113
124
  /** The model-facing row carries the ROUTE as well as the human label: a display name is
@@ -117,10 +128,15 @@ function modelPeerRow(peer: ExocomPeerSummary): string {
117
128
  const workspace = peer.workspace_code && peer.workspace_label
118
129
  ? ` · workspace: ${normalizeMetadataText(peer.workspace_label, 80, "workspace")} [${peer.workspace_code}] · ${peer.sameWorkspace === false ? "external workspace" : "same workspace"}`
119
130
  : "";
120
- return `- ${normalizeMetadataText(peer.displayName, 80, "peer")} · ${normalizeMetadataText(peer.persona, 64, "unknown")} · ${normalizeMetadataText(peer.model, 160, "unknown")} · ctx ${Math.max(0, Math.min(100, Number(peer.context_pct) || 0))}% · target: ${normalizeMetadataText(peer.target, 80, "peer")}${workspace}`;
131
+ const availability = peer.profile?.availability ?? "unknown";
132
+ const capabilities = peer.profile?.capabilities.length
133
+ ? peer.profile.capabilities.join(", ")
134
+ : "not reported";
135
+ return `- ${normalizeMetadataText(peer.displayName, 80, "peer")} · ${normalizeMetadataText(peer.persona, 64, "unknown")} · ${normalizeMetadataText(peer.model, 160, "unknown")} · ctx ${Math.max(0, Math.min(100, Number(peer.context_pct) || 0))}% · target: ${normalizeMetadataText(peer.target, 80, "peer")} · self-reported availability: ${availability} · capabilities: ${capabilities}${workspace}`;
121
136
  }
122
137
 
123
138
  function summarizePeer(peer: DisplayPeer, scope: ExocomScope | undefined): ExocomPeerSummary {
139
+ const profile = normalizeExocomProfile(peer.profile);
124
140
  if (!scope) {
125
141
  return {
126
142
  displayName: normalizeMetadataText(peer.displayName, 80, "peer"),
@@ -128,6 +144,7 @@ function summarizePeer(peer: DisplayPeer, scope: ExocomScope | undefined): Exoco
128
144
  persona: normalizeMetadataText(peer.persona, 64, "unknown"),
129
145
  model: normalizeMetadataText(peer.model, 160, "unknown"),
130
146
  context_pct: Math.max(0, Math.min(100, Number(peer.context_pct) || 0)),
147
+ ...(profile ? { profile } : {}),
131
148
  };
132
149
  }
133
150
  // Legacy peers predate workspace metadata. Their registry location still proves they belong
@@ -146,6 +163,7 @@ function summarizePeer(peer: DisplayPeer, scope: ExocomScope | undefined): Exoco
146
163
  ...(workspaceCode ? { workspace_code: workspaceCode } : {}),
147
164
  workspace_label: normalizeMetadataText(workspaceLabel, 80, "workspace"),
148
165
  sameWorkspace: workspaceId === scope.homeWorkspaceId,
166
+ ...(profile ? { profile } : {}),
149
167
  };
150
168
  }
151
169
 
@@ -227,12 +245,11 @@ function isSendDetails(details: unknown): details is ExocomSendDetails {
227
245
  return Array.isArray(shape.failed) || typeof shape.msg_id === "string";
228
246
  }
229
247
 
230
- function renderSendResult(details: ExocomSendDetails, expanded: boolean): string {
248
+ function renderSendResult(details: ExocomSendDetails): string {
231
249
  if ("failed" in details) {
232
250
  const failed = details.failedCount ?? details.failed.length;
233
251
  const queued = details.queuedCount ?? details.msg_ids.length;
234
- const summary = `${queued > 0 ? "✓" : "✗"} queued ${queued} peer${queued === 1 ? "" : "s"}${failed > 0 ? ` · ${failed} failed` : ""}`;
235
- if (!expanded) return `${summary} (${keyHint("app.tools.expand", "to expand")})`;
252
+ const summary = `queued ${queued} peer${queued === 1 ? "" : "s"}${failed > 0 ? ` · ${failed} failed` : ""}`;
236
253
  const lines = [summary];
237
254
  if (details.msg_ids.length > 0) lines.push(`msg_ids: ${details.msg_ids.join(", ")}`);
238
255
  for (const failure of details.failed) {
@@ -242,8 +259,7 @@ function renderSendResult(details: ExocomSendDetails, expanded: boolean): string
242
259
  if ((details.omittedFailures ?? 0) > 0) lines.push(`…and ${details.omittedFailures} more failures omitted; retry only the failed targets shown above.`);
243
260
  return lines.join("\n");
244
261
  }
245
- const summary = `✓ queued to ${details.target} · msg_id=${details.msg_id}`;
246
- return expanded ? summary : `${summary} (${keyHint("app.tools.expand", "to expand")})`;
262
+ return `queued to ${details.target} · msg_id=${details.msg_id}`;
247
263
  }
248
264
 
249
265
  /** `getPlane` is a LIVE accessor, not a captured value: a `canUseBus` downgrade (`stopExocom`,
@@ -294,7 +310,7 @@ export function registerExocomTools(
294
310
  renderCall(_args, theme) {
295
311
  return new Text(theme.fg("toolTitle", theme.bold("Exocom List")), 0, 0);
296
312
  },
297
- renderResult(result, { expanded }, theme) {
313
+ renderResult(result, { expanded }, theme, context) {
298
314
  const details = result.details as unknown as {
299
315
  peers?: ExocomPeerSummary[];
300
316
  total?: number;
@@ -303,9 +319,8 @@ export function registerExocomTools(
303
319
  scopeCode?: string;
304
320
  homeWorkspace?: { label?: string; code?: string; sameAsScope?: boolean };
305
321
  } | undefined;
306
- if (!details || !Array.isArray(details.peers)) {
307
- const first = result.content.find((item) => item.type === "text");
308
- return new Text(theme.fg("error", first?.type === "text" ? first.text : "Exocom list failed"), 0, 0);
322
+ if (context?.isError || !details || !Array.isArray(details.peers)) {
323
+ return renderLiteralToolResult(result, { expanded }, theme, { isError: true });
309
324
  }
310
325
  const peers = details.peers;
311
326
  const total = typeof details.total === "number" ? details.total : peers.length;
@@ -448,15 +463,14 @@ export function registerExocomTools(
448
463
  const suffix = preview.length < message.length ? "…" : "";
449
464
  return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Send "))}${theme.fg("accent", target)}${preview ? theme.fg("dim", ` ${preview}${suffix}`) : ""}`, 0, 0);
450
465
  },
451
- renderResult(result, { expanded }, theme) {
466
+ renderResult(result, { expanded }, theme, context) {
452
467
  const details = result.details as unknown as ExocomSendDetails | undefined;
453
468
  // pi renders this for a FAILED call too, handing us the error's empty details object — an
454
469
  // unrecognised shape is a failure to show, not a send to congratulate the user on.
455
- if (!isSendDetails(details)) {
456
- const text = result.content.find((item) => item.type === "text");
457
- return new Text(theme.fg("error", text?.type === "text" ? text.text : "(no result)"), 0, 0);
470
+ if (context?.isError || !isSendDetails(details)) {
471
+ return renderLiteralToolResult(result, { expanded }, theme, { isError: true });
458
472
  }
459
- return new Text(theme.fg(expanded ? "toolOutput" : "accent", renderSendResult(details, expanded)), 0, 0);
473
+ return renderLiteralToolResult({ ...result, content: [{ type: "text", text: renderSendResult(details) }] }, { expanded }, theme, context);
460
474
  },
461
475
  });
462
476
 
@@ -475,15 +489,14 @@ export function registerExocomTools(
475
489
  renderCall(args, theme) {
476
490
  return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Name "))}${theme.fg("accent", normalizeMetadataText(args.name, 96, "peer"))}`, 0, 0);
477
491
  },
478
- renderResult(result, { expanded }, theme) {
492
+ renderResult(result, { expanded }, theme, context) {
479
493
  const details = result.details as unknown as { name?: string } | undefined;
480
- if (!details?.name) {
481
- const first = result.content.find((item) => item.type === "text");
482
- return new Text(theme.fg("error", first?.type === "text" ? first.text : "Exocom name failed"), 0, 0);
494
+ if (context?.isError || !details?.name) {
495
+ return renderLiteralToolResult(result, { expanded }, theme, { isError: true });
483
496
  }
484
497
  const name = normalizeMetadataText(details.name, 96, "(unchanged)");
485
498
  const text = `exocom: you are now "${name}"`;
486
- return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
499
+ return renderLiteralToolResult({ ...result, content: [{ type: "text", text }] }, { expanded }, theme, context);
487
500
  },
488
501
  });
489
502
  }
package/src/tools/flow.ts CHANGED
@@ -1,27 +1,46 @@
1
1
  /** `flow` tool — run a *.flow.json DAG. */
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
- import { failureDetails } from "../extension/shared.ts";
4
+ import { expandDetailHint, failureDetails, isInline } from "../extension/shared.ts";
5
5
  import { fenceUntrusted } from "../core/fence.ts";
6
- import type { FlowOutcome } from "../orchestration/flow-run.ts";
6
+ import { resultReceiptHint } from "../results/retrieval.ts";
7
+ import { flowDisposition, type FlowOutcome } from "../orchestration/flow-run.ts";
7
8
  import type { FlowSpec } from "../orchestration/flow.ts";
8
- import { compactInlineText, sanitizeTerminalText } from "../ui/presentation.ts";
9
- import { compactVisibleText } from "../ui/presentation.ts";
10
- import { toolUsageField, type ChildUsageLedger } from "../ui/usage.ts";
9
+ import { compactInlineText, compactVisibleText, sanitizeTerminalText } from "../ui/presentation.ts";
10
+ import { toolUsageField } from "../ui/usage.ts";
11
11
  import { sumUsage } from "../orchestration/reducers.ts";
12
- import { emptyUsage } from "../engine/stream.ts";
13
12
  import { Text } from "@earendil-works/pi-tui";
14
- import { expandDetailHint } from "../extension/shared.ts";
15
- import type { FailureKind } from "../orchestration/types.ts";
13
+ import { createReportBody } from "../ui/report-body.ts";
14
+ import { orchestrationLaunchLines, type OrchestrationLaunchSnapshot } from "../ui/orchestration-launch.ts";
15
+ import type { AgentResult, FailureKind } from "../orchestration/types.ts";
16
+ import { launchReceipt, type BackgroundRuns, type TrackedRun } from "./background-run.ts";
16
17
 
17
18
  export interface FlowToolDeps {
18
19
  get lastCtx(): ExtensionContext | undefined;
19
20
  set lastCtx(value: ExtensionContext | undefined);
20
21
  loadFlow(cwd: string, name: string): { ok: true; flow: FlowSpec } | { ok: false; error: string } | undefined;
21
22
  listFlows(cwd: string): string[];
22
- runFlowVisible(ctx: ExtensionContext, spec: FlowSpec, task: string, signal?: AbortSignal): Promise<FlowOutcome>;
23
- childUsage: ChildUsageLedger;
24
- publishPersonaCost(): void;
23
+ launchSnapshot(ctx: ExtensionContext, spec: FlowSpec): OrchestrationLaunchSnapshot;
24
+ runFlowVisible(ctx: ExtensionContext, spec: FlowSpec, task: string, tracked?: TrackedRun): Promise<FlowOutcome>;
25
+ /** Launch/join: a flow is one tracked run, like a delegate leg (see background-run.ts). */
26
+ runs: BackgroundRuns;
27
+ }
28
+
29
+ /** A finished flow as the tracker sees it. A checkpoint the operator declined is a STOP (the run settles
30
+ * "stopped", never "failed"); a cancelled run is an abort; anything else that is not ok is a failure. */
31
+ function flowResult(name: string, outcome: FlowOutcome): AgentResult {
32
+ const base: AgentResult = {
33
+ agent: `flow ${name}`,
34
+ output: outcome.output || "(flow produced no output)",
35
+ usage: sumUsage(Object.values(outcome.results).map((r) => r.usage)),
36
+ ok: outcome.ok,
37
+ };
38
+ const disposition = flowDisposition(outcome);
39
+ if (disposition === "done") return base;
40
+ if (disposition === "stopped") {
41
+ return { ...base, error: `stopped at checkpoint "${outcome.stoppedAtGate}": not approved`, failureKind: "abort" };
42
+ }
43
+ return { ...base, error: outcome.error ?? "flow failed", failureKind: outcome.cancelled ? "abort" : (outcome.failureKind ?? "agent") };
25
44
  }
26
45
 
27
46
  export function registerFlowTool(pi: ExtensionAPI, d: FlowToolDeps): void {
@@ -36,9 +55,11 @@ export function registerFlowTool(pi: ExtensionAPI, d: FlowToolDeps): void {
36
55
  description: [
37
56
  "Run a named flow — a declarative DAG over strategies (`*.flow.json`): phases each run a",
38
57
  "strategy over a roster, wired by `needs`, fanning out where independent and threading each",
39
- "phase's output into its dependents. Journaled, so an interrupted flow resumes. Reach for it",
58
+ "phase's output into its dependents. Journaled: re-running the identical task resumes finished phases. Reach for it",
40
59
  "when a task has a fixed multi-stage shape (e.g. gather → critique → decide) you want run",
41
- "deterministically, end to end, rather than deciding each step yourself.",
60
+ "deterministically, end to end, rather than deciding each step yourself. Interactive sessions run it in",
61
+ "the BACKGROUND: you get a run id at once and ONE follow-up when it ends (a checkpoint waits for the",
62
+ "operator, not for you), so end your turn and never poll it; headless sessions block and return inline.",
42
63
  ].join(" "),
43
64
  parameters: FlowToolParams,
44
65
  async execute(_id, params, signal, _onUpdate, ctx) {
@@ -60,43 +81,88 @@ export function registerFlowTool(pi: ExtensionAPI, d: FlowToolDeps): void {
60
81
  isError: true,
61
82
  };
62
83
  }
84
+ let launch: OrchestrationLaunchSnapshot | undefined;
63
85
  try {
64
- const outcome = await d.runFlowVisible(ctx, parsed.flow, params.task, signal);
86
+ try {
87
+ const snapshot = d.launchSnapshot(ctx, parsed.flow);
88
+ launch = { ...snapshot, participants: snapshot.participants.map((participant) => ({ ...participant })) };
89
+ } catch {
90
+ // This is presentation metadata only; a settings/model lookup must not block the flow.
91
+ launch = { participants: [], unavailable: true };
92
+ }
93
+ // The same launch `delegate` uses: ONE tracked run. Interactive sessions get a receipt now and
94
+ // the outcome as ONE completion follow-up; a checkpoint is the OPERATOR's modal and never wakes
95
+ // the model. Headless sessions join the run before returning (and reject every checkpoint).
96
+ const inline = isInline(ctx);
97
+ const ran: { outcome?: FlowOutcome } = {}; // the run's full outcome, which the tracker's AgentResult cannot carry
98
+ const id = d.runs.launch({ kind: "flow", label: `flow ${parsed.flow.name}`, task: params.task, inline }, async (run) => {
99
+ const finished = await d.runFlowVisible(ctx, parsed.flow, params.task, run);
100
+ ran.outcome = finished;
101
+ return flowResult(parsed.flow.name, finished);
102
+ });
103
+ if (!inline) {
104
+ const name = compactInlineText(parsed.flow.name, { maxChars: 64 });
105
+ return {
106
+ content: [{ type: "text", text: launchReceipt("flow", id, name, "when it ends; a checkpoint waits for the operator, not for you") }],
107
+ details: { runId: id, flow: name, launch },
108
+ isError: false,
109
+ };
110
+ }
111
+ const { runs, usage } = await d.runs.settle([id], signal);
112
+ const run = runs[0];
113
+ const outcome = ran.outcome;
114
+ if (!outcome) {
115
+ const message = run?.error ?? run?.result?.error ?? run?.status ?? "the run record is gone";
116
+ return { content: [{ type: "text", text: `flow failed: ${message}` }], details: failureDetails({ ok: false, error: message, launch }), isError: true };
117
+ }
65
118
  const details = {
119
+ launch,
66
120
  ok: outcome.ok,
67
121
  failedPhase: outcome.failedPhase,
68
122
  failureKind: outcome.failureKind,
69
123
  error: outcome.error,
124
+ stoppedAtGate: outcome.stoppedAtGate,
125
+ ...(run?.resultReceipt ? { resultReceipt: { ...run.resultReceipt } } : {}),
70
126
  };
71
- const usage = Object.values(outcome.results).length > 0
72
- ? sumUsage(Object.values(outcome.results).map((r) => r.usage))
73
- : emptyUsage();
74
- d.childUsage.account(usage);
75
- d.publishPersonaCost();
127
+ // An unapproved checkpoint is the operator's choice: the flow stopped, it did not fail.
128
+ const failed = flowDisposition(outcome) === "failed";
76
129
  return {
77
- content: [{ type: "text", text: fenceUntrusted(outcome.output || "(flow produced no output)") }],
78
- details: outcome.ok ? details : failureDetails(details),
79
- isError: !outcome.ok,
130
+ content: [{ type: "text", text: `${fenceUntrusted(outcome.output || "(flow produced no output)")}${run?.resultReceipt ? `\n\n${resultReceiptHint(run.resultReceipt)}` : ""}` }],
131
+ details: failed ? failureDetails(details) : details,
132
+ isError: failed,
80
133
  ...toolUsageField(usage),
81
134
  };
82
135
  } catch (err) {
83
136
  const message = err instanceof Error ? err.message : String(err);
84
- return { content: [{ type: "text", text: `flow failed: ${message}` }], details: failureDetails({ ok: false, error: message }), isError: true };
137
+ return { content: [{ type: "text", text: `flow failed: ${message}` }], details: failureDetails({ ok: false, error: message, ...(launch ? { launch } : {}) }), isError: true };
85
138
  }
86
139
  },
87
140
  renderCall(args, theme) {
88
141
  return new Text(`${theme.fg("toolTitle", theme.bold("flow "))}${theme.fg("accent", compactInlineText(args.name ?? "?", { maxChars: 96 }) || "?")}`, 0, 0);
89
142
  },
90
- renderResult(result, { expanded }, theme) {
143
+ renderResult(result, { expanded }, theme, context) {
91
144
  const details = (result.details ?? {}) as {
92
145
  ok?: boolean;
93
146
  failedPhase?: string;
94
147
  failureKind?: FailureKind;
95
148
  error?: string;
149
+ stoppedAtGate?: string;
150
+ runId?: string;
151
+ flow?: string;
152
+ launch?: OrchestrationLaunchSnapshot;
96
153
  };
97
154
  const first = result.content[0];
98
155
  const full = sanitizeTerminalText(first?.type === "text" ? first.text : "(no output)");
99
- const failed = details.ok === false;
156
+ const hostError = context?.isError ?? result.isError;
157
+ if (details.runId) {
158
+ // Interactive launch receipt: the outcome arrives later as the completion follow-up.
159
+ const launched = hostError || details.ok === false ? theme.fg("error", theme.bold("flow failed"))
160
+ : `${theme.fg("toolTitle", theme.bold("flow "))}${theme.fg("accent", compactInlineText(`launched ${details.runId} · ${details.flow ?? "?"}`, { maxChars: 120 }))}`;
161
+ const metadata = orchestrationLaunchLines(details.launch, expanded).join("\n");
162
+ return new Text([launched, theme.fg("dim", metadata), expanded ? theme.fg("toolOutput", full) : ""].filter(Boolean).join("\n"), 0, 0);
163
+ }
164
+ const disposition = flowDisposition({ ok: !hostError && details.ok !== false, stoppedAtGate: details.stoppedAtGate });
165
+ const failed = disposition === "failed";
100
166
  const failureBits = failed
101
167
  ? compactInlineText(
102
168
  [details.failedPhase ? `phase ${details.failedPhase}` : undefined, details.failureKind, details.error]
@@ -105,13 +171,20 @@ export function registerFlowTool(pi: ExtensionAPI, d: FlowToolDeps): void {
105
171
  { maxChars: 160 },
106
172
  )
107
173
  : "";
108
- const title = failed
109
- ? theme.fg("error", theme.bold(`flow failed${failureBits ? ` · ${failureBits}` : ""}`))
110
- : theme.fg("success", theme.bold("flow complete"));
111
- if (expanded) return new Text(`${title}\n${theme.fg("toolOutput", full)}`, 0, 0);
174
+ const title = () => disposition === "stopped"
175
+ ? theme.fg("warning", theme.bold(`flow stopped · checkpoint ${compactInlineText(details.stoppedAtGate ?? "", { maxChars: 64 })}`))
176
+ : failed
177
+ ? theme.fg("error", theme.bold(`flow failed${failureBits ? ` · ${failureBits}` : ""}`))
178
+ : theme.fg("success", theme.bold("flow complete"));
179
+ const metadata = details.launch ? `\n\nLaunch request (Pi may clamp effort):\n${orchestrationLaunchLines(details.launch, expanded).join("\n")}` : "";
180
+ if (expanded) {
181
+ // Real flow outcomes are fenced even after a phase fails; host/preflight errors are not.
182
+ if (failed && !full.startsWith("Sub-agent output (untrusted data):\n")) return new Text(`${title()}\n${theme.fg("toolOutput", full)}${metadata}`, 0, 0);
183
+ return createReportBody(`${full}${metadata}`, theme, { header: title }, context?.lastComponent);
184
+ }
112
185
  const preview = compactVisibleText(full, { maxLines: 3, maxLineChars: 100 });
113
186
  const hint = preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : "";
114
- return new Text(`${title}\n${theme.fg("toolOutput", preview.text)}${hint}`, 0, 0);
187
+ return new Text(`${title()}\n${theme.fg("toolOutput", preview.text)}${hint}${theme.fg("dim", metadata)}`, 0, 0);
115
188
  },
116
189
  });
117
190
  }