@aeondave/pi-persona 1.17.0 → 1.17.3
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 +79 -29
- package/CHANGELOG.md +264 -0
- package/README.md +27 -12
- 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 +223 -57
- package/docs/EXPERIENCE.md +19 -9
- package/docs/MONITORS.md +62 -3
- package/docs/REFERENCE.md +189 -35
- 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 +9 -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 +20 -0
- package/src/bridge.ts +3 -2
- package/src/core/brief.ts +429 -356
- package/src/core/config.ts +16 -11
- 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/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +251 -0
- package/src/core/timer.ts +34 -9
- package/src/engine/adapter.ts +12 -8
- package/src/engine/async.ts +195 -136
- package/src/engine/child.ts +1 -1
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +11 -6
- package/src/engine/stream.ts +3 -0
- package/src/exocom/install.ts +2 -1
- package/src/extension/companion-settings.ts +29 -0
- package/src/extension/engine.ts +43 -16
- package/src/extension/hooks.ts +106 -36
- package/src/extension/persona-model-settings.ts +336 -0
- package/src/extension/persona-settings.ts +459 -0
- package/src/extension/shared.ts +15 -7
- package/src/extension.ts +642 -267
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +26 -5
- package/src/orchestration/flow-run.ts +38 -13
- package/src/orchestration/flow.ts +9 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/sdk.ts +17 -3
- 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/persona/config-store.ts +6 -195
- package/src/persona/model-participants.ts +1 -1
- package/src/persona/persona.ts +7 -3
- package/src/persona/settings-store.ts +636 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +77 -52
- package/src/tools/delegate-tool.ts +315 -285
- package/src/tools/delegate.ts +22 -210
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +15 -19
- package/src/tools/flow.ts +85 -28
- package/src/tools/intercom-tool.ts +108 -118
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +19 -9
- package/src/tools/monitor.ts +39 -6
- package/src/tools/timer.ts +11 -13
- package/src/ui/agent-overlay.ts +55 -14
- package/src/ui/agent-tree.ts +84 -18
- 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/persona-settings.ts +328 -0
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +189 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
- package/src/persona/state.ts +0 -28
package/src/tools/flow.ts
CHANGED
|
@@ -1,27 +1,43 @@
|
|
|
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
|
|
6
|
+
import { flowDisposition, type FlowOutcome } from "../orchestration/flow-run.ts";
|
|
7
7
|
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";
|
|
8
|
+
import { compactInlineText, compactVisibleText, sanitizeTerminalText } from "../ui/presentation.ts";
|
|
9
|
+
import { toolUsageField } from "../ui/usage.ts";
|
|
11
10
|
import { sumUsage } from "../orchestration/reducers.ts";
|
|
12
|
-
import { emptyUsage } from "../engine/stream.ts";
|
|
13
11
|
import { Text } from "@earendil-works/pi-tui";
|
|
14
|
-
import {
|
|
15
|
-
import type { FailureKind } from "../orchestration/types.ts";
|
|
12
|
+
import { createReportBody } from "../ui/report-body.ts";
|
|
13
|
+
import type { AgentResult, FailureKind } from "../orchestration/types.ts";
|
|
14
|
+
import { launchReceipt, type BackgroundRuns, type TrackedRun } from "./background-run.ts";
|
|
16
15
|
|
|
17
16
|
export interface FlowToolDeps {
|
|
18
17
|
get lastCtx(): ExtensionContext | undefined;
|
|
19
18
|
set lastCtx(value: ExtensionContext | undefined);
|
|
20
19
|
loadFlow(cwd: string, name: string): { ok: true; flow: FlowSpec } | { ok: false; error: string } | undefined;
|
|
21
20
|
listFlows(cwd: string): string[];
|
|
22
|
-
runFlowVisible(ctx: ExtensionContext, spec: FlowSpec, task: string,
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
runFlowVisible(ctx: ExtensionContext, spec: FlowSpec, task: string, tracked?: TrackedRun): Promise<FlowOutcome>;
|
|
22
|
+
/** Launch/join: a flow is one tracked run, like a delegate leg (see background-run.ts). */
|
|
23
|
+
runs: BackgroundRuns;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A finished flow as the tracker sees it. A checkpoint the operator declined is a STOP (the run settles
|
|
27
|
+
* "stopped", never "failed"); a cancelled run is an abort; anything else that is not ok is a failure. */
|
|
28
|
+
function flowResult(name: string, outcome: FlowOutcome): AgentResult {
|
|
29
|
+
const base: AgentResult = {
|
|
30
|
+
agent: `flow ${name}`,
|
|
31
|
+
output: outcome.output || "(flow produced no output)",
|
|
32
|
+
usage: sumUsage(Object.values(outcome.results).map((r) => r.usage)),
|
|
33
|
+
ok: outcome.ok,
|
|
34
|
+
};
|
|
35
|
+
const disposition = flowDisposition(outcome);
|
|
36
|
+
if (disposition === "done") return base;
|
|
37
|
+
if (disposition === "stopped") {
|
|
38
|
+
return { ...base, error: `stopped at checkpoint "${outcome.stoppedAtGate}": not approved`, failureKind: "abort" };
|
|
39
|
+
}
|
|
40
|
+
return { ...base, error: outcome.error ?? "flow failed", failureKind: outcome.cancelled ? "abort" : (outcome.failureKind ?? "agent") };
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
export function registerFlowTool(pi: ExtensionAPI, d: FlowToolDeps): void {
|
|
@@ -36,9 +52,11 @@ export function registerFlowTool(pi: ExtensionAPI, d: FlowToolDeps): void {
|
|
|
36
52
|
description: [
|
|
37
53
|
"Run a named flow — a declarative DAG over strategies (`*.flow.json`): phases each run a",
|
|
38
54
|
"strategy over a roster, wired by `needs`, fanning out where independent and threading each",
|
|
39
|
-
"phase's output into its dependents. Journaled
|
|
55
|
+
"phase's output into its dependents. Journaled: re-running the identical task resumes finished phases. Reach for it",
|
|
40
56
|
"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.",
|
|
57
|
+
"deterministically, end to end, rather than deciding each step yourself. Interactive sessions run it in",
|
|
58
|
+
"the BACKGROUND: you get a run id at once and ONE follow-up when it ends (a checkpoint waits for the",
|
|
59
|
+
"operator, not for you), so end your turn and never poll it; headless sessions block and return inline.",
|
|
42
60
|
].join(" "),
|
|
43
61
|
parameters: FlowToolParams,
|
|
44
62
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
@@ -61,22 +79,44 @@ export function registerFlowTool(pi: ExtensionAPI, d: FlowToolDeps): void {
|
|
|
61
79
|
};
|
|
62
80
|
}
|
|
63
81
|
try {
|
|
64
|
-
|
|
82
|
+
// The same launch `delegate` uses: ONE tracked run. Interactive sessions get a receipt now and
|
|
83
|
+
// the outcome as ONE completion follow-up; a checkpoint is the OPERATOR's modal and never wakes
|
|
84
|
+
// the model. Headless sessions join the run before returning (and reject every checkpoint).
|
|
85
|
+
const inline = isInline(ctx);
|
|
86
|
+
const ran: { outcome?: FlowOutcome } = {}; // the run's full outcome, which the tracker's AgentResult cannot carry
|
|
87
|
+
const id = d.runs.launch({ kind: "flow", label: `flow ${parsed.flow.name}`, task: params.task, inline }, async (run) => {
|
|
88
|
+
const finished = await d.runFlowVisible(ctx, parsed.flow, params.task, run);
|
|
89
|
+
ran.outcome = finished;
|
|
90
|
+
return flowResult(parsed.flow.name, finished);
|
|
91
|
+
});
|
|
92
|
+
if (!inline) {
|
|
93
|
+
const name = compactInlineText(parsed.flow.name, { maxChars: 64 });
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: launchReceipt("flow", id, name, "when it ends; a checkpoint waits for the operator, not for you") }],
|
|
96
|
+
details: { runId: id, flow: name },
|
|
97
|
+
isError: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const { runs, usage } = await d.runs.settle([id], signal);
|
|
101
|
+
const run = runs[0];
|
|
102
|
+
const outcome = ran.outcome;
|
|
103
|
+
if (!outcome) {
|
|
104
|
+
const message = run?.error ?? run?.result?.error ?? run?.status ?? "the run record is gone";
|
|
105
|
+
return { content: [{ type: "text", text: `flow failed: ${message}` }], details: failureDetails({ ok: false, error: message }), isError: true };
|
|
106
|
+
}
|
|
65
107
|
const details = {
|
|
66
108
|
ok: outcome.ok,
|
|
67
109
|
failedPhase: outcome.failedPhase,
|
|
68
110
|
failureKind: outcome.failureKind,
|
|
69
111
|
error: outcome.error,
|
|
112
|
+
stoppedAtGate: outcome.stoppedAtGate,
|
|
70
113
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
: emptyUsage();
|
|
74
|
-
d.childUsage.account(usage);
|
|
75
|
-
d.publishPersonaCost();
|
|
114
|
+
// An unapproved checkpoint is the operator's choice: the flow stopped, it did not fail.
|
|
115
|
+
const failed = flowDisposition(outcome) === "failed";
|
|
76
116
|
return {
|
|
77
117
|
content: [{ type: "text", text: fenceUntrusted(outcome.output || "(flow produced no output)") }],
|
|
78
|
-
details:
|
|
79
|
-
isError:
|
|
118
|
+
details: failed ? failureDetails(details) : details,
|
|
119
|
+
isError: failed,
|
|
80
120
|
...toolUsageField(usage),
|
|
81
121
|
};
|
|
82
122
|
} catch (err) {
|
|
@@ -87,16 +127,27 @@ export function registerFlowTool(pi: ExtensionAPI, d: FlowToolDeps): void {
|
|
|
87
127
|
renderCall(args, theme) {
|
|
88
128
|
return new Text(`${theme.fg("toolTitle", theme.bold("flow "))}${theme.fg("accent", compactInlineText(args.name ?? "?", { maxChars: 96 }) || "?")}`, 0, 0);
|
|
89
129
|
},
|
|
90
|
-
renderResult(result, { expanded }, theme) {
|
|
130
|
+
renderResult(result, { expanded }, theme, context) {
|
|
91
131
|
const details = (result.details ?? {}) as {
|
|
92
132
|
ok?: boolean;
|
|
93
133
|
failedPhase?: string;
|
|
94
134
|
failureKind?: FailureKind;
|
|
95
135
|
error?: string;
|
|
136
|
+
stoppedAtGate?: string;
|
|
137
|
+
runId?: string;
|
|
138
|
+
flow?: string;
|
|
96
139
|
};
|
|
97
140
|
const first = result.content[0];
|
|
98
141
|
const full = sanitizeTerminalText(first?.type === "text" ? first.text : "(no output)");
|
|
99
|
-
const
|
|
142
|
+
const hostError = context?.isError ?? result.isError;
|
|
143
|
+
if (details.runId) {
|
|
144
|
+
// Interactive launch receipt: the outcome arrives later as the completion follow-up.
|
|
145
|
+
const launched = hostError || details.ok === false ? theme.fg("error", theme.bold("flow failed"))
|
|
146
|
+
: `${theme.fg("toolTitle", theme.bold("flow "))}${theme.fg("accent", compactInlineText(`launched ${details.runId} · ${details.flow ?? "?"}`, { maxChars: 120 }))}`;
|
|
147
|
+
return new Text(expanded ? `${launched}\n${theme.fg("toolOutput", full)}` : launched, 0, 0);
|
|
148
|
+
}
|
|
149
|
+
const disposition = flowDisposition({ ok: !hostError && details.ok !== false, stoppedAtGate: details.stoppedAtGate });
|
|
150
|
+
const failed = disposition === "failed";
|
|
100
151
|
const failureBits = failed
|
|
101
152
|
? compactInlineText(
|
|
102
153
|
[details.failedPhase ? `phase ${details.failedPhase}` : undefined, details.failureKind, details.error]
|
|
@@ -105,13 +156,19 @@ export function registerFlowTool(pi: ExtensionAPI, d: FlowToolDeps): void {
|
|
|
105
156
|
{ maxChars: 160 },
|
|
106
157
|
)
|
|
107
158
|
: "";
|
|
108
|
-
const title =
|
|
109
|
-
? theme.fg("
|
|
110
|
-
:
|
|
111
|
-
|
|
159
|
+
const title = () => disposition === "stopped"
|
|
160
|
+
? theme.fg("warning", theme.bold(`flow stopped · checkpoint ${compactInlineText(details.stoppedAtGate ?? "", { maxChars: 64 })}`))
|
|
161
|
+
: failed
|
|
162
|
+
? theme.fg("error", theme.bold(`flow failed${failureBits ? ` · ${failureBits}` : ""}`))
|
|
163
|
+
: theme.fg("success", theme.bold("flow complete"));
|
|
164
|
+
if (expanded) {
|
|
165
|
+
// Real flow outcomes are fenced even after a phase fails; host/preflight errors are not.
|
|
166
|
+
if (failed && !full.startsWith("Sub-agent output (untrusted data):\n")) return new Text(`${title()}\n${theme.fg("toolOutput", full)}`, 0, 0);
|
|
167
|
+
return createReportBody(full, theme, { header: title }, context?.lastComponent);
|
|
168
|
+
}
|
|
112
169
|
const preview = compactVisibleText(full, { maxLines: 3, maxLineChars: 100 });
|
|
113
170
|
const hint = preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : "";
|
|
114
|
-
return new Text(`${title}\n${theme.fg("toolOutput", preview.text)}${hint}`, 0, 0);
|
|
171
|
+
return new Text(`${title()}\n${theme.fg("toolOutput", preview.text)}${hint}`, 0, 0);
|
|
115
172
|
},
|
|
116
173
|
});
|
|
117
174
|
}
|
|
@@ -5,13 +5,14 @@ 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 { createReportBody } from "../ui/report-body.ts";
|
|
8
9
|
import { coachingDisabledHint, expandDetailHint, reconcileAnsweredAsk } from "../extension/shared.ts";
|
|
9
10
|
import {
|
|
10
|
-
type AsyncRun, AsyncRunTracker,
|
|
11
|
-
MAX_ASYNC_STATUS_ROWS,
|
|
11
|
+
type AsyncRun, AsyncRunTracker, buildPeekDigest,
|
|
12
12
|
dedupeRunsById, getFullRunOutput, IdleCoalescingNotifier,
|
|
13
|
-
|
|
13
|
+
isAsyncRunStalled, runDisplayName, runDurationLabel,
|
|
14
14
|
} from "../engine/async.ts";
|
|
15
|
+
import type { PollGuard } from "../core/nudge.ts";
|
|
15
16
|
import { fenceUntrusted } from "../core/fence.ts";
|
|
16
17
|
import { sanitizeDisplayLabel } from "../core/display-label.ts";
|
|
17
18
|
import { type IntercomOutcome, type IntercomParams, MAX_INTERCOM_MESSAGE_CHARS, MAX_INTERCOM_REF_CHARS, runIntercom } from "./intercom.ts";
|
|
@@ -32,35 +33,33 @@ export interface IntercomToolDeps {
|
|
|
32
33
|
SUPERVISOR: string;
|
|
33
34
|
STALL_FLAG_MS: number;
|
|
34
35
|
waitingRunIds?: () => ReadonlySet<string>;
|
|
36
|
+
/** Refuses an unchanged repeat of peek/result on a running run. Built only when `config.nudge`. */
|
|
37
|
+
pollGuard?: PollGuard | undefined;
|
|
35
38
|
missingRunMessage(id: string, display: string | undefined): string;
|
|
36
39
|
stopAgent(nodeId: string): boolean;
|
|
37
40
|
steerAgent(nodeId: string, text: string): boolean;
|
|
38
41
|
steerRegistry: Map<string, unknown>;
|
|
39
42
|
stopRequested: Set<string>;
|
|
40
|
-
|
|
43
|
+
/** The premature-surrender note for a settled report (config-gated), else undefined. */
|
|
41
44
|
scanForSurrender(text: string): string | undefined;
|
|
42
|
-
get disposed(): boolean;
|
|
43
45
|
childUsage: ChildUsageLedger;
|
|
44
46
|
publishPersonaCost(): void;
|
|
45
47
|
}
|
|
46
48
|
|
|
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
49
|
/** Fence only child-authored payloads; missing-message diagnostics remain actionable tool prose. */
|
|
60
50
|
export function fenceIntercomToolOutcome(out: IntercomOutcome, action: string, fence: (text: string) => string): string {
|
|
61
51
|
return action === "message" && out.details.ok ? fence(out.text) : fenceIntercomOutcome(out, fence);
|
|
62
52
|
}
|
|
63
53
|
|
|
54
|
+
/** `run-4 (alpha · haiku) running, run-3 (scout) done, … (+2 more)` — the one bounded run listing
|
|
55
|
+
* every id-to-call-sign teaching surface shares (a run miss, a bulk stop). `none` when empty. */
|
|
56
|
+
export function describeRuns(runs: readonly AsyncRun[], opts: { status?: boolean; max?: number } = {}): string {
|
|
57
|
+
if (runs.length === 0) return "none";
|
|
58
|
+
const max = opts.max ?? 8;
|
|
59
|
+
const shown = runs.slice(0, max).map((run) => `${run.id} (${runDisplayName(run)})${opts.status ? ` ${run.status}` : ""}`);
|
|
60
|
+
return `${shown.join(", ")}${runs.length > max ? `, … (+${runs.length - max} more)` : ""}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
64
63
|
/** Renderer-local state only. The persisted result, not this cache, owns display identity. */
|
|
65
64
|
interface IntercomRenderState {
|
|
66
65
|
target?: { id: string; label: string };
|
|
@@ -77,13 +76,61 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
77
76
|
const collision = d.tracker.list().some((other) => other.id !== id && runDisplayName(other) === name);
|
|
78
77
|
return collision ? `${name} [${sanitizeDisplayLabel(id, "run")}]` : name;
|
|
79
78
|
}
|
|
79
|
+
/** An id the tracker holds nothing for: teach what exists (and why a stale id dead-ends) instead of
|
|
80
|
+
* letting steer/stop fall through to a false "already finished" or "no live steer handle". */
|
|
81
|
+
function missingRun(action: "result" | "steer" | "stop", to: string, display: string | undefined, snapshot: object) {
|
|
82
|
+
return { content: [{ type: "text" as const, text: d.missingRunMessage(to, display) }], details: failureDetails({ action, ok: false, ...snapshot }), isError: true };
|
|
83
|
+
}
|
|
84
|
+
/** `stop { to: "all" }`: request cancellation of every running run, each with the per-run semantics of a
|
|
85
|
+
* single stop — except that a run already stopping is reported, never force-cleared, in bulk. */
|
|
86
|
+
function stopAllRuns() {
|
|
87
|
+
const snapshot = { runId: "all", target: "all" };
|
|
88
|
+
const requested: AsyncRun[] = [];
|
|
89
|
+
const already: AsyncRun[] = [];
|
|
90
|
+
const unreachable: AsyncRun[] = [];
|
|
91
|
+
for (const run of d.tracker.running()) {
|
|
92
|
+
const nodeId = `async:${run.id}`;
|
|
93
|
+
if (d.stopRequested.has(nodeId)) already.push(run);
|
|
94
|
+
else (d.stopAgent(nodeId) ? requested : unreachable).push(run);
|
|
95
|
+
}
|
|
96
|
+
const noHandle = unreachable.length > 0 ? `No live stop handle: ${describeRuns(unreachable)}.` : "";
|
|
97
|
+
if (requested.length === 0 && already.length === 0) {
|
|
98
|
+
return { content: [{ type: "text" as const, text: noHandle || "intercom stop: no running runs to stop." }], details: failureDetails({ action: "stop", ok: false, ...snapshot }), isError: true };
|
|
99
|
+
}
|
|
100
|
+
if (requested.length > 0) d.pollGuard?.reset();
|
|
101
|
+
const parts = [
|
|
102
|
+
...(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.`] : []),
|
|
103
|
+
...(already.length > 0 ? [`Already stopping: ${describeRuns(already)}.${requested.length === 0 ? " Repeat stop { to: <run id> } on one that stays running to force-clear it." : ""}`] : []),
|
|
104
|
+
...(noHandle ? [noHandle] : []),
|
|
105
|
+
];
|
|
106
|
+
return { content: [{ type: "text" as const, text: parts.join(" ") }], details: { action: "stop", ok: true, ...snapshot }, isError: false };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* An unchanged repeat of a status check learns nothing and holds the turn the idle-gated completion
|
|
110
|
+
* wake needs: refuse it, and on the second repeat end the turn. The signature is every shown run's
|
|
111
|
+
* state (status, queued, waiting, stalled), so anything newly informative passes.
|
|
112
|
+
*/
|
|
113
|
+
function pollRefusal(action: "peek" | "result", scope: string, runs: readonly AsyncRun[], waiting: ReadonlySet<string> | undefined, snapshot: object) {
|
|
114
|
+
const running = runs.filter((r) => r.status === "running");
|
|
115
|
+
if (!d.pollGuard || running.length === 0) return undefined;
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
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(",");
|
|
118
|
+
const verdict = d.pollGuard.check(scope, sig);
|
|
119
|
+
if (verdict === "ok") return undefined;
|
|
120
|
+
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.`;
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text" as const, text }],
|
|
123
|
+
details: failureDetails({ action, ok: false, ...snapshot }),
|
|
124
|
+
isError: true,
|
|
125
|
+
...(verdict === "terminate" ? { terminate: true } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
80
128
|
// ── intercom tool (supervisor side of the comm plane: read/answer children) ───
|
|
81
129
|
const IntercomToolParams = Type.Object({
|
|
82
130
|
action: Type.Union(
|
|
83
131
|
[
|
|
84
132
|
Type.Literal("peek"),
|
|
85
133
|
Type.Literal("result"),
|
|
86
|
-
Type.Literal("wait"),
|
|
87
134
|
Type.Literal("steer"),
|
|
88
135
|
Type.Literal("stop"),
|
|
89
136
|
Type.Literal("list"),
|
|
@@ -94,24 +141,20 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
94
141
|
],
|
|
95
142
|
{
|
|
96
143
|
description:
|
|
97
|
-
"peek =
|
|
144
|
+
"peek = status of async sub-agents · result = retrieve one complete settled result by run id · 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
145
|
},
|
|
99
146
|
),
|
|
100
|
-
to: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "result/steer/stop/peek: the async run id (e.g. 'run-1')
|
|
147
|
+
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`)" })),
|
|
101
148
|
messageId: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "message: the drained bus message id shown by `inbox`" })),
|
|
102
149
|
askId: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "reply: the message id of the child's pending question" })),
|
|
103
150
|
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
151
|
});
|
|
107
152
|
pi.registerTool({
|
|
108
153
|
name: "intercom",
|
|
109
154
|
label: "Intercom",
|
|
110
155
|
description: [
|
|
111
|
-
"
|
|
112
|
-
"`peek`
|
|
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.",
|
|
156
|
+
"Steer, stop and inspect your background sub-agents; a finished run or batch wakes you with a completion follow-up.",
|
|
157
|
+
"Never `peek` healthy children; use it only for status you need. `result { to: runId }` retrieves one complete settled payload by run id.",
|
|
115
158
|
"`steer` sends a soft course-correction to one (by run id); in-process async runs can receive it mid-run,",
|
|
116
159
|
"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
160
|
"`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 +167,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
124
167
|
// interpolate it into trusted prose without reducing it to compact identifier metadata.
|
|
125
168
|
const displayTarget = params.to === undefined ? undefined : sanitizeDisplayLabel(params.to, "run");
|
|
126
169
|
const targetSnapshot = params.to === undefined ? {} : { runId: params.to, target: displayRunTarget(params.to) };
|
|
127
|
-
// peek +
|
|
170
|
+
// peek + result + steer + stop are supervisor→child controls over the async tracker /
|
|
128
171
|
// steer handles — available to EVERY persona (no dependency on the coaching bus).
|
|
129
172
|
if (params.action === "peek") {
|
|
130
173
|
// No `to` → running legs PLUS any settled-but-not-yet-delivered ones (the settle→deliver
|
|
@@ -132,25 +175,28 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
132
175
|
const runs = params.to
|
|
133
176
|
? [d.tracker.peek(params.to)].filter((r): r is AsyncRun => !!r)
|
|
134
177
|
: dedupeRunsById([...d.tracker.running(), ...d.completionNotifier.peekPending()]);
|
|
135
|
-
|
|
178
|
+
const waiting = d.waitingRunIds?.();
|
|
179
|
+
const refusal = pollRefusal("peek", params.to ?? "*", runs, waiting, targetSnapshot);
|
|
180
|
+
if (refusal) return refusal;
|
|
181
|
+
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 };
|
|
136
182
|
}
|
|
137
183
|
if (params.action === "result") {
|
|
138
184
|
if (!params.to) {
|
|
139
185
|
return { content: [{ type: "text", text: "intercom result needs { to: <run id> }." }], details: failureDetails({ action: "result", ok: false }), isError: true };
|
|
140
186
|
}
|
|
141
187
|
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
|
-
}
|
|
188
|
+
if (!run) return missingRun("result", params.to, displayTarget, targetSnapshot);
|
|
145
189
|
if (run.status === "running") {
|
|
190
|
+
const refusal = pollRefusal("result", `result:${params.to}`, [run], d.waitingRunIds?.(), targetSnapshot);
|
|
191
|
+
if (refusal) return refusal;
|
|
146
192
|
return {
|
|
147
|
-
content: [{ type: "text", text: `${targetSnapshot.target} is still running
|
|
193
|
+
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
194
|
details: failureDetails({ action: "result", ok: false, status: run.status, ...targetSnapshot }),
|
|
149
195
|
isError: true,
|
|
150
196
|
};
|
|
151
197
|
}
|
|
152
198
|
// 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
|
|
199
|
+
// same result cannot appear again as a follow-up a moment later. Telling the tracker too
|
|
154
200
|
// makes this retained copy the first thing retention evicts — the supervisor has read it,
|
|
155
201
|
// so it is the cheapest payload in the map to lose.
|
|
156
202
|
d.completionNotifier.discard((pending) => pending.id === run.id);
|
|
@@ -162,93 +208,28 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
162
208
|
const cause = run.error ? `\nFailure detail:\n${fenceUntrusted(run.error)}` : "";
|
|
163
209
|
const displayRun = sanitizeDisplayLabel(run.label ?? run.agent);
|
|
164
210
|
// 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
|
|
211
|
+
// the supervisor through, so it carries the same reading the passive completion report does.
|
|
166
212
|
const took = runDurationLabel(run);
|
|
213
|
+
// The follow-up discarded above is what would have carried the premature-surrender note, so
|
|
214
|
+
// a collected DONE report must carry it itself (a failed leg is already a failure report).
|
|
215
|
+
const note = run.status === "done" ? d.scanForSurrender(full) : undefined;
|
|
167
216
|
const billed = d.childUsage.accountMany([{ key: run.id, usage: run.result?.usage ?? emptyUsage() }]);
|
|
168
217
|
d.publishPersonaCost();
|
|
169
218
|
return {
|
|
170
|
-
content: [{ type: "text", text: `${run.id} (${displayRun}) · ${run.status}${took ? ` · ${took}` : ""}${cause}\n${body}` }],
|
|
219
|
+
content: [{ type: "text", text: `${run.id} (${displayRun}) · ${run.status}${took ? ` · ${took}` : ""}${cause}\n${body}${note ? `\n\n${note}` : ""}` }],
|
|
171
220
|
details: { action: "result", ok: true, status: run.status, ...targetSnapshot },
|
|
172
221
|
isError: false,
|
|
173
222
|
...toolUsageField(billed),
|
|
174
223
|
};
|
|
175
224
|
}
|
|
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 },
|
|
241
|
-
isError: false,
|
|
242
|
-
...toolUsageField(billed),
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
225
|
if (params.action === "steer") {
|
|
246
226
|
if (!params.to || params.message === undefined) {
|
|
247
227
|
return { content: [{ type: "text", text: "intercom steer needs { to: <run id>, message }." }], details: failureDetails({ action: "steer", ok: false }), isError: true };
|
|
248
228
|
}
|
|
249
229
|
const nodeId = `async:${params.to}`;
|
|
250
230
|
const knownRun = d.tracker.peek(params.to);
|
|
251
|
-
if (knownRun
|
|
231
|
+
if (!knownRun) return missingRun("steer", params.to, displayTarget, targetSnapshot);
|
|
232
|
+
if (knownRun.status !== "running") {
|
|
252
233
|
return {
|
|
253
234
|
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
235
|
details: failureDetails({ action: "steer", ok: false, status: knownRun.status, ...targetSnapshot }),
|
|
@@ -262,26 +243,31 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
262
243
|
isError: true,
|
|
263
244
|
};
|
|
264
245
|
}
|
|
265
|
-
// Routed through the guarded
|
|
246
|
+
// Routed through the guarded steerAgent so a just-finished/disposed handle can't throw.
|
|
266
247
|
const steered = d.steerAgent(nodeId, params.message);
|
|
248
|
+
// An intervention changes the child's state without moving any flag the guard compares.
|
|
249
|
+
if (steered) d.pollGuard?.reset();
|
|
267
250
|
return steered
|
|
268
251
|
? { content: [{ type: "text", text: `Steering queued for ${targetSnapshot.target} (${displayTarget}).` }], details: { action: "steer", ok: true, ...targetSnapshot, message: params.message }, isError: false }
|
|
269
252
|
: { 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
253
|
}
|
|
271
254
|
if (params.action === "stop") {
|
|
255
|
+
if (params.to === "all") return stopAllRuns();
|
|
272
256
|
if (!params.to) {
|
|
273
257
|
return { content: [{ type: "text", text: "intercom stop needs { to: <run id> }." }], details: failureDetails({ action: "stop", ok: false }), isError: true };
|
|
274
258
|
}
|
|
259
|
+
if (!d.tracker.peek(params.to)) return missingRun("stop", params.to, displayTarget, targetSnapshot);
|
|
275
260
|
// Abort the real run signal. The child backend can kill its process tree; the
|
|
276
261
|
// in-process backend requests host cancellation and settles its lifecycle separately.
|
|
277
262
|
const nodeId = `async:${params.to}`;
|
|
278
263
|
const repeated = d.stopRequested.has(nodeId);
|
|
279
264
|
const stopped = d.stopAgent(nodeId);
|
|
265
|
+
if (stopped) d.pollGuard?.reset();
|
|
280
266
|
if (stopped && !repeated) {
|
|
281
|
-
return { content: [{ type: "text", text: `Cancellation requested for ${targetSnapshot.target}
|
|
267
|
+
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
268
|
}
|
|
283
269
|
// A repeated stop has just invoked the REAL cancel handle again. Only now force-clear
|
|
284
|
-
//
|
|
270
|
+
// tracker state if engine settlement is still lagging; the handle remains registered
|
|
285
271
|
// until onComplete so cancellation can never be replaced by UI-only bookkeeping.
|
|
286
272
|
if (stopped && repeated && d.tracker.forceSettle(params.to, "force-stopped by supervisor after repeated engine cancellation")) {
|
|
287
273
|
return {
|
|
@@ -304,7 +290,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
304
290
|
if (params.action === "reply" && out.details.ok && params.askId) {
|
|
305
291
|
reconcileAnsweredAsk(params.askId, d.intercomNotifier, d.bus, d.SUPERVISOR);
|
|
306
292
|
}
|
|
307
|
-
// Child-authored inbox bodies are untrusted, exactly like the
|
|
293
|
+
// Child-authored inbox bodies are untrusted, exactly like the automatic drain and peek copies.
|
|
308
294
|
// A retrieved body is child-authored even though it is no longer an inbox batch; keep the
|
|
309
295
|
// same trust fence around it as inbox and automatic drain surfaces.
|
|
310
296
|
let text = fenceIntercomToolOutcome(out, params.action, fenceUntrusted);
|
|
@@ -320,15 +306,12 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
320
306
|
const runTarget = typeof args.to === "string"
|
|
321
307
|
? state?.target?.id === args.to ? state.target.label : displayRunTarget(args.to)
|
|
322
308
|
: undefined;
|
|
323
|
-
if (action === "
|
|
309
|
+
if (action === "peek") target = runTarget ?? "all";
|
|
324
310
|
else if (["result", "steer", "stop"].includes(action)) target = runTarget ?? "?";
|
|
325
311
|
else if (action === "send") target = compactInlineText(args.to ?? "?", { maxChars: 80 }) || "?";
|
|
326
312
|
else if (action === "message") target = compactInlineText(args.messageId ?? "?", { maxChars: 80 }) || "?";
|
|
327
313
|
else if (action === "reply") target = compactInlineText(args.askId ?? "?", { maxChars: 80 }) || "?";
|
|
328
|
-
const
|
|
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}`) : ""}`;
|
|
314
|
+
const formatCall = (label: string) => `${theme.fg("toolTitle", theme.bold("intercom "))}${theme.fg("accent", action)}${label ? theme.fg("dim", ` ${compactInlineText(label, { maxChars: 80 })}`) : ""}`;
|
|
332
315
|
const component = new Text(formatCall(target), 0, 0);
|
|
333
316
|
if (state) { state.callText = component; state.formatCall = formatCall; }
|
|
334
317
|
return component;
|
|
@@ -339,7 +322,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
339
322
|
const action = details.action ?? context?.args?.action;
|
|
340
323
|
const savedId = details.runId ?? context?.args?.to;
|
|
341
324
|
const legacyCompleted = !details.target && context !== undefined && context.isPartial !== true
|
|
342
|
-
&& ["steer", "stop", "result", "peek"
|
|
325
|
+
&& ["steer", "stop", "result", "peek"].includes(action ?? "");
|
|
343
326
|
if (state && typeof savedId === "string" && (details.target || legacyCompleted)) {
|
|
344
327
|
// Old receipts have no display snapshot. A new session may reuse their run ID;
|
|
345
328
|
// the current tracker cannot supply a historical alias for that unrelated execution.
|
|
@@ -350,9 +333,9 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
350
333
|
}
|
|
351
334
|
const first = result.content[0];
|
|
352
335
|
const full = sanitizeTerminalText(first?.type === "text" ? first.text : "(no output)");
|
|
353
|
-
const failed = details.ok === false;
|
|
336
|
+
const failed = (context?.isError ?? result.isError) || details.ok === false;
|
|
354
337
|
const prefix = failed ? `${theme.fg("error", theme.bold("failed"))}\n` : "";
|
|
355
|
-
if (details.action === "steer" && details.ok === true) {
|
|
338
|
+
if (!failed && details.action === "steer" && details.ok === true) {
|
|
356
339
|
const target = compactInlineText(details.target ?? state?.target?.label ?? (legacyCompleted && typeof savedId === "string" ? sanitizeDisplayLabel(savedId, "run") : details.runId ? displayRunTarget(details.runId) : ""), { maxChars: 80 });
|
|
357
340
|
const message = typeof details.message === "string" ? details.message : context?.args?.message;
|
|
358
341
|
// Old persisted receipts may predate message snapshots. Keep their original content
|
|
@@ -370,7 +353,14 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
370
353
|
const showTitle = !state?.callText || !preview;
|
|
371
354
|
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
355
|
}
|
|
373
|
-
if (expanded)
|
|
356
|
+
if (expanded) {
|
|
357
|
+
// Explicit report/message actions only; missing/running diagnostics and control
|
|
358
|
+
// receipts remain literal. Older history with no action is not guessed from prose.
|
|
359
|
+
if (!failed && ["result", "message", "inbox"].includes(action ?? "")) {
|
|
360
|
+
return createReportBody(full, theme, {}, context?.lastComponent);
|
|
361
|
+
}
|
|
362
|
+
return new Text(`${prefix}${theme.fg("toolOutput", full)}`, 0, 0);
|
|
363
|
+
}
|
|
374
364
|
const preview = compactVisibleText(full, { maxLines: 4, maxLineChars: 100 });
|
|
375
365
|
const hint = preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : "";
|
|
376
366
|
return new Text(`${prefix}${theme.fg("toolOutput", preview.text)}${hint}`, 0, 0);
|
package/src/tools/intercom.ts
CHANGED
|
@@ -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 } };
|