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