@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.
- package/AGENTS.md +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +47 -18
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
package/src/tools/exocom.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
15
|
-
import type
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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:
|
|
79
|
-
isError:
|
|
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
|
|
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 =
|
|
109
|
-
? theme.fg("
|
|
110
|
-
:
|
|
111
|
-
|
|
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
|
}
|