@aeondave/pi-persona 1.17.3 → 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 +44 -36
- package/CHANGELOG.md +76 -1
- package/README.md +23 -13
- package/docs/ARCHITECTURE.md +97 -37
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +33 -8
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +65 -14
- package/package.json +12 -6
- package/src/agents/agent.ts +6 -0
- package/src/bridge.ts +25 -8
- 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 +14 -4
- package/src/core/capabilities.ts +1 -1
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/session-runs.ts +1 -1
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +83 -19
- package/src/engine/async.ts +49 -10
- package/src/engine/child.ts +10 -2
- package/src/engine/inproc.ts +87 -46
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +6 -0
- 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 +45 -17
- 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 +20 -21
- package/src/extension/hooks.ts +952 -938
- package/src/extension/identity.ts +41 -75
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +17 -0
- package/src/extension.ts +2963 -2873
- package/src/loader.ts +11 -3
- package/src/orchestration/flow-journal.ts +7 -1
- package/src/orchestration/flow-run.ts +122 -19
- package/src/orchestration/flow.ts +11 -0
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +44 -6
- 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 +1 -1
- 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/council.ts +40 -23
- package/src/tools/delegate-tool.ts +50 -14
- package/src/tools/delegate.ts +13 -6
- 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.ts +20 -3
- package/src/tools/flow.ts +24 -8
- package/src/tools/intercom-tool.ts +98 -14
- package/src/tools/models.ts +15 -0
- package/src/tools/monitor.ts +20 -1
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +23 -2
- package/src/ui/agent-overlay.ts +11 -3
- package/src/ui/agent-tree.ts +43 -1
- package/src/ui/literal-tool-result.ts +1 -1
- package/src/ui/model-picker.ts +1 -1
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/session-cards.ts +191 -14
package/src/engine/adapter.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import type { AgentConfig } from "../agents/agent.ts";
|
|
11
11
|
import { type ContractDef, contractInstructions, parseAndValidate, pinContract, type PinnedContract } from "../core/contract.ts";
|
|
12
12
|
import { sanitizeDisplayLabel } from "../core/display-label.ts";
|
|
13
|
+
import { resolveLegThinking } from "../core/leg-thinking.ts";
|
|
14
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
13
15
|
import { assignedIdentityPrompt } from "../core/naming.ts";
|
|
14
16
|
import { roleHint } from "../orchestration/roster.ts";
|
|
15
17
|
import { type AgentProgress, type AgentRunSpec, isPositiveFiniteMs, type SteerFn, type StrategyEngine } from "../orchestration/sdk.ts";
|
|
@@ -18,8 +20,13 @@ import { type ChildEngineOptions, type ChildRunSpec, runChildAgent } from "./chi
|
|
|
18
20
|
import { looksLikeProviderError } from "./errors.ts";
|
|
19
21
|
import { piHostModuleHint } from "./host-module.ts";
|
|
20
22
|
import { nextChildHandle } from "./handles.ts";
|
|
23
|
+
import { admitResultReadTool, referenceHandoffFailure } from "./result-grant.ts";
|
|
24
|
+
import { makeScopedResultReader, type ResultArchiveView } from "../results/retrieval.ts";
|
|
25
|
+
import type { PreparedResultReader, ScopedReader } from "../bus/result-reader.ts";
|
|
21
26
|
import { combineSignals } from "./signals.ts";
|
|
27
|
+
import { emptyUsage } from "./stream.ts";
|
|
22
28
|
import { toolModifierFailure, unknownAgentFailure, unknownContractFailure } from "./spec-preflight.ts";
|
|
29
|
+
import { mergeSessionToolAllowlist } from "./session-tools.ts";
|
|
23
30
|
|
|
24
31
|
/** The engine-side face of the cross-process broker (spec B3/B4/B7) — a child-engine
|
|
25
32
|
* spawn's connection to the supervisor-hosted relay. The concrete implementation
|
|
@@ -40,6 +47,10 @@ export interface EngineAdapterBroker {
|
|
|
40
47
|
* parent-side idle/startup watchdogs consult this before killing a silent leg (R9): a leg
|
|
41
48
|
* blocked on the supervisor is re-armed, not killed. */
|
|
42
49
|
hasPendingAskFrom(handle: string): boolean;
|
|
50
|
+
/** Strict, AWAITED result-reader preflight for a reference-handoff leg (the broker host binds
|
|
51
|
+
* a single-use token to this handle). Must complete BEFORE `register` and the spawn — a
|
|
52
|
+
* missing broker/binding fails the leg closed with zero usage instead of degrading bus-less. */
|
|
53
|
+
prepareResultReader(handle: string, reader: ScopedReader, signal?: AbortSignal): Promise<PreparedResultReader>;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export interface EngineAdapterDeps {
|
|
@@ -55,8 +66,7 @@ export interface EngineAdapterDeps {
|
|
|
55
66
|
* the on-the-fly role this run carries, when it has one, so a member's own assignment key
|
|
56
67
|
* (`agent#hash`) resolves ahead of the bare agent's. */
|
|
57
68
|
modelFor?: (agent: string, role?: string) => string | undefined;
|
|
58
|
-
/**
|
|
59
|
-
* fall into a model's default "adaptive" mode, which some models reject. */
|
|
69
|
+
/** Configured/inherited child effort, below explicit leg and agent selections. */
|
|
60
70
|
childThinking?: string;
|
|
61
71
|
/** The shared behavioral layer (docs/SPINE.md), prepended to every leg's prompt. This is the
|
|
62
72
|
* WORKER variant (`prompts/spine.worker.md`), not the supervisor text — a leg runs headless
|
|
@@ -80,6 +90,11 @@ export interface EngineAdapterDeps {
|
|
|
80
90
|
* blocking would deadlock: default false, mirroring `inproc.ts`'s `allowBlocking`
|
|
81
91
|
* default. Forwarded to the child as `PI_PERSONA_ALLOW_BLOCKING` (bridge.ts). */
|
|
82
92
|
allowBlocking?: boolean;
|
|
93
|
+
/** The owning session's terminal archive (a GETTER — the owner can change across sessions).
|
|
94
|
+
* Required by any leg carrying `resultRefs` (flow reference handoff): the scoped reader
|
|
95
|
+
* handed to the broker pages THIS archive, after a zero-usage preflight verified every
|
|
96
|
+
* granted ref is durably stored. Absent ⇒ such legs fail closed before any spawn. */
|
|
97
|
+
resultArchive?: () => ResultArchiveView | undefined;
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
@@ -113,10 +128,38 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
|
113
128
|
): Promise<AgentResult> {
|
|
114
129
|
const cfg = deps.resolveAgent(spec.agent);
|
|
115
130
|
if (!cfg) return unknownAgentFailure(spec.agent, deps.listAgents?.() ?? []);
|
|
131
|
+
let thinkingRequested: ThinkingLevel | undefined;
|
|
132
|
+
try {
|
|
133
|
+
thinkingRequested = resolveLegThinking(spec, cfg, deps.childThinking);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return { agent: spec.agent, output: "", usage: emptyUsage(), ok: false, failureKind: "contract", error: `[${spec.agent}] ${error instanceof Error ? error.message : String(error)}` };
|
|
136
|
+
}
|
|
116
137
|
const tools = spec.tools ?? cfg.tools;
|
|
117
138
|
const invalidTools = toolModifierFailure(spec.agent, tools);
|
|
118
139
|
if (invalidTools) return invalidTools;
|
|
119
140
|
|
|
141
|
+
// ── reference handoff preflight (contract 3/4) — BEFORE any broker work or spawn ──
|
|
142
|
+
// Admission uses the SAME shared rule as the in-process engine; storage is verified
|
|
143
|
+
// durable (session-entry); the broker binding is AWAITED before registration/spawn.
|
|
144
|
+
// Any miss is a zero-usage contract failure and the child is never invoked.
|
|
145
|
+
let admittedTools = tools;
|
|
146
|
+
let resultReader: ScopedReader | undefined;
|
|
147
|
+
if (spec.resultRefs !== undefined && spec.resultRefs.length > 0) {
|
|
148
|
+
const grant = admitResultReadTool({
|
|
149
|
+
specTools: spec.tools, cfgTools: cfg.tools, excludeTools: cfg.excludeTools,
|
|
150
|
+
canUseBus: deps.canUseBus ?? true,
|
|
151
|
+
});
|
|
152
|
+
if (!grant.ok) return referenceHandoffFailure(spec.agent, grant.error);
|
|
153
|
+
const archive = deps.resultArchive?.();
|
|
154
|
+
if (archive === undefined) return referenceHandoffFailure(spec.agent, "terminal result archive unavailable for reference handoff");
|
|
155
|
+
const scoped = makeScopedResultReader(archive, spec.resultRefs);
|
|
156
|
+
const { missing } = scoped.preflight();
|
|
157
|
+
if (missing.length > 0) return referenceHandoffFailure(spec.agent, `archived results unavailable: ${missing.join(", ")}`);
|
|
158
|
+
if (deps.broker === undefined) return referenceHandoffFailure(spec.agent, "cross-process broker reader unavailable for reference handoff");
|
|
159
|
+
resultReader = scoped;
|
|
160
|
+
admittedTools = grant.tools;
|
|
161
|
+
}
|
|
162
|
+
|
|
120
163
|
// Resolved (and pinned) up front: the SAME def both instructs the member and
|
|
121
164
|
// validates its output — an instruction/validation drift is impossible. A named
|
|
122
165
|
// contract that cannot be resolved is a hard configuration error: running the leg
|
|
@@ -130,12 +173,10 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
|
130
173
|
: spec.task;
|
|
131
174
|
const task = contractDef ? `${withSkills}\n\n${contractInstructions(contractDef)}` : withSkills;
|
|
132
175
|
const childSpec: ChildRunSpec = { task };
|
|
133
|
-
|
|
134
|
-
// Append an explicit thinking level (model:level) unless one is already present —
|
|
135
|
-
// a child without it defaults to "adaptive", which some models reject.
|
|
136
|
-
if (model && deps.childThinking && !model.includes(":")) model = `${model}:${deps.childThinking}`;
|
|
176
|
+
const model = spec.model ?? deps.modelFor?.(spec.agent, spec.role) ?? cfg.model;
|
|
137
177
|
if (model) childSpec.model = model;
|
|
138
|
-
if (
|
|
178
|
+
if (thinkingRequested !== undefined) childSpec.thinking = thinkingRequested;
|
|
179
|
+
if (admittedTools !== undefined) childSpec.tools = admittedTools;
|
|
139
180
|
if (cfg.excludeTools && cfg.excludeTools.length > 0) childSpec.excludeTools = cfg.excludeTools;
|
|
140
181
|
// The shared behavioral layer (docs/SPINE.md) + the agent's own persona + any
|
|
141
182
|
// on-the-fly `role` specialisation from the spec. All three go out as
|
|
@@ -160,27 +201,46 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
|
160
201
|
|
|
161
202
|
// Cross-process comm plane (spec B1-B7): absent broker ⇒ none of this runs, so
|
|
162
203
|
// the spawn env + behaviour stay byte-identical to pre-broker pi-persona.
|
|
163
|
-
// Present ⇒ mint a handle,
|
|
164
|
-
//
|
|
204
|
+
// Present ⇒ mint a handle, (for a reference-handoff leg) AWAIT the strict reader
|
|
205
|
+
// binding BEFORE registration, register the handle with the host BEFORE spawn, wire
|
|
206
|
+
// the child's env, and give the caller a steer function.
|
|
165
207
|
let handle: string | undefined;
|
|
208
|
+
let preparedReader: PreparedResultReader | undefined;
|
|
166
209
|
if (deps.broker) {
|
|
167
|
-
|
|
210
|
+
const broker = deps.broker;
|
|
211
|
+
handle = nextChildHandle(spec.agent);
|
|
168
212
|
const wantsPeers = spec.peers === true && (deps.canUseBus ?? true);
|
|
169
|
-
const
|
|
213
|
+
const busTools = (deps.canUseBus ?? true) ? ["contact_supervisor", ...(wantsPeers ? ["contact_peer"] : [])] : [];
|
|
214
|
+
const mergedTools = mergeSessionToolAllowlist(admittedTools, busTools);
|
|
215
|
+
if (mergedTools !== undefined) childSpec.tools = mergedTools;
|
|
216
|
+
const label = spec.name?.trim()
|
|
170
217
|
? sanitizeDisplayLabel(spec.name, handle)
|
|
171
218
|
: spec.role ? `${handle} (${roleHint(spec.role)})` : handle;
|
|
172
|
-
|
|
219
|
+
if (resultReader !== undefined) {
|
|
220
|
+
try {
|
|
221
|
+
preparedReader = await broker.prepareResultReader(handle, resultReader, signal);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
return referenceHandoffFailure(spec.agent, `broker result reader preparation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
224
|
+
}
|
|
225
|
+
childOptions.env = {
|
|
226
|
+
...childOptions.env,
|
|
227
|
+
// Private spawn credential for the child's bridge (bridge.ts installs read_result
|
|
228
|
+
// only when this is set); scrubbed from the inherited env inside child.ts.
|
|
229
|
+
PI_PERSONA_RESULT_READER_TOKEN: preparedReader.token,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
broker.register({ handle, label, group: peerGroup, ...(wantsPeers ? { peers: true } : {}) });
|
|
173
233
|
childOptions.env = {
|
|
174
234
|
...childOptions.env,
|
|
175
|
-
PI_PERSONA_BUS:
|
|
235
|
+
PI_PERSONA_BUS: broker.endpoint,
|
|
176
236
|
PI_PERSONA_HANDLE: handle,
|
|
237
|
+
PI_PERSONA_BUS_TOOLS: (deps.canUseBus ?? true) ? "on" : "off",
|
|
177
238
|
...(wantsPeers ? { PI_PERSONA_PEERS: "1" } : {}),
|
|
178
239
|
...(deps.allowBlocking ? { PI_PERSONA_ALLOW_BLOCKING: "1" } : {}),
|
|
179
240
|
};
|
|
180
241
|
// The parent-side watchdogs must not kill a leg that is genuinely waiting on the
|
|
181
242
|
// supervisor's answer (R9, inproc parity) — ask the bus, don't guess from a static flag.
|
|
182
|
-
childOptions.isBlocked = () =>
|
|
183
|
-
const broker = deps.broker;
|
|
243
|
+
childOptions.isBlocked = () => broker.hasPendingAskFrom(handle!);
|
|
184
244
|
const h = handle;
|
|
185
245
|
const steer: SteerFn = Object.assign((text: string) => broker.steerFrame(h, text), {
|
|
186
246
|
isWaitingForSupervisor: () => broker.hasPendingAskFrom(h),
|
|
@@ -192,17 +252,21 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
|
192
252
|
try {
|
|
193
253
|
child = await runChildAgent(childSpec, signal, childOptions);
|
|
194
254
|
} finally {
|
|
195
|
-
//
|
|
196
|
-
// a leaked
|
|
255
|
+
// Revoke the reader grant and unregister on EVERY settle path (success, failure, or
|
|
256
|
+
// an unexpected throw) — a leaked grant would outlive its leg; a leaked handle would
|
|
257
|
+
// otherwise linger in the host's registries forever.
|
|
258
|
+
preparedReader?.revoke();
|
|
197
259
|
if (deps.broker && handle) deps.broker.unregister(handle);
|
|
198
260
|
}
|
|
199
261
|
|
|
200
262
|
const result: AgentResult = { agent: spec.agent, output: child.output, usage: child.usage, ok: child.ok };
|
|
201
|
-
|
|
263
|
+
if (thinkingRequested !== undefined) result.thinkingRequested = thinkingRequested;
|
|
264
|
+
if (child.thinkingEffective !== undefined) result.thinkingEffective = child.thinkingEffective;
|
|
265
|
+
// The opaque provider/id the child ran on — for the UI and
|
|
202
266
|
// as the seed of the provider-fallback chain on a provider failure. The child's
|
|
203
267
|
// STREAM-REPORTED model wins over the requested spec: a resolved alias or a
|
|
204
268
|
// provider-side redirect must seed the fallback chain with what actually ran.
|
|
205
|
-
const modelUsed =
|
|
269
|
+
const modelUsed = child.model ?? childSpec.model ?? spec.model ?? cfg.model;
|
|
206
270
|
if (modelUsed) result.modelUsed = modelUsed;
|
|
207
271
|
// Diagnostic tag: agent · model ref · dynamic overrides. Same shape as inproc engine,
|
|
208
272
|
// so failed follow-ups always say WHICH agent+model died and WHY.
|
package/src/engine/async.ts
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
import type { ProgressSnapshot, ToolEvent } from "./stream.ts";
|
|
13
13
|
import type { AgentResult } from "../orchestration/types.ts";
|
|
14
|
+
import { resultReceiptHint, type ResultReceipt } from "../results/retrieval.ts";
|
|
14
15
|
import { DEFAULT_ASYNC_RETAIN } from "../core/config.ts";
|
|
15
16
|
import { MAX_DISPLAY_LABEL_CHARS, sanitizeDisplayLabel, sanitizeStatusLabel } from "../core/display-label.ts";
|
|
16
17
|
import { compactTokens } from "../core/format.ts";
|
|
18
|
+
import { isThinkingLevel } from "../core/types.ts";
|
|
17
19
|
import { formatDuration } from "../core/time.ts";
|
|
18
20
|
|
|
19
|
-
export { MAX_DISPLAY_LABEL_CHARS } from "../core/display-label.ts";
|
|
20
|
-
|
|
21
21
|
/** "possibly stuck" — the soft stall signal shared by the operator card, the peek digest and the UI
|
|
22
22
|
* badge. It is deliberately patient: a long scan, a big generation, or a blocking command shows no
|
|
23
23
|
* visible progress yet is perfectly healthy, so every surface flags a leg only after a genuinely long quiet
|
|
@@ -33,6 +33,7 @@ export interface AsyncRun {
|
|
|
33
33
|
status: "running" | "done" | "failed" | "stopped";
|
|
34
34
|
progress: ProgressSnapshot;
|
|
35
35
|
result?: AgentResult;
|
|
36
|
+
resultReceipt?: ResultReceipt;
|
|
36
37
|
error?: string;
|
|
37
38
|
/** The codename the launcher gave this run (e.g. "atlas-static") — the SAME name shown on the
|
|
38
39
|
* agent-tree node for this run. When set, it's the canonical display name (over the bare
|
|
@@ -115,6 +116,7 @@ export class AsyncRunTracker {
|
|
|
115
116
|
/** Ids whose payload was evicted while STILL UNCOLLECTED, oldest first (insertion-ordered Set). */
|
|
116
117
|
private readonly droppedIds = new Set<string>();
|
|
117
118
|
private readonly completeListeners: Array<(run: AsyncRun) => void> = [];
|
|
119
|
+
private readonly createListeners: Array<(run: AsyncRun) => void> = [];
|
|
118
120
|
private seq = 0;
|
|
119
121
|
private readonly maxRetained: number;
|
|
120
122
|
private readonly now: () => number;
|
|
@@ -159,6 +161,9 @@ export class AsyncRunTracker {
|
|
|
159
161
|
if (meta.label !== undefined) entry.label = sanitizeDisplayLabel(meta.label);
|
|
160
162
|
if (meta.model !== undefined) entry.model = sanitizeDisplayLabel(meta.model, "model");
|
|
161
163
|
this.runs.set(id, entry);
|
|
164
|
+
for (const cb of [...this.createListeners]) {
|
|
165
|
+
try { cb(entry); } catch { /* archival observation cannot prevent launch */ }
|
|
166
|
+
}
|
|
162
167
|
|
|
163
168
|
const settle = (): void => this.settleOnce(entry);
|
|
164
169
|
// `Promise.resolve(run(...))` routes BOTH a returned rejection AND a SYNCHRONOUS throw from
|
|
@@ -176,7 +181,9 @@ export class AsyncRunTracker {
|
|
|
176
181
|
.then((result) => {
|
|
177
182
|
if (entry.settled) return; // force-settled by the supervisor — drop the late natural result
|
|
178
183
|
entry.status = result.ok ? "done" : result.failureKind === "abort" ? "stopped" : "failed";
|
|
179
|
-
|
|
184
|
+
// Retain scalar metadata by value: an engine reusing/mutating its result object
|
|
185
|
+
// after settlement must not rewrite this run's requested/observed effort.
|
|
186
|
+
entry.result = { ...result, usage: { ...result.usage }, ...(result.resultReceipt ? { resultReceipt: { ...result.resultReceipt } } : {}) };
|
|
180
187
|
if (!result.ok && result.error) entry.error = result.error;
|
|
181
188
|
})
|
|
182
189
|
.catch((err: unknown) => {
|
|
@@ -347,6 +354,14 @@ export class AsyncRunTracker {
|
|
|
347
354
|
return batch === undefined || !this.running().some((r) => r.batch === batch);
|
|
348
355
|
}
|
|
349
356
|
|
|
357
|
+
onCreate(cb: (run: AsyncRun) => void): () => void {
|
|
358
|
+
this.createListeners.push(cb);
|
|
359
|
+
return () => {
|
|
360
|
+
const i = this.createListeners.indexOf(cb);
|
|
361
|
+
if (i >= 0) this.createListeners.splice(i, 1);
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
350
365
|
onComplete(cb: (run: AsyncRun) => void): () => void {
|
|
351
366
|
this.completeListeners.push(cb);
|
|
352
367
|
return () => {
|
|
@@ -363,10 +378,18 @@ export function dedupeRunsById(runs: AsyncRun[]): AsyncRun[] {
|
|
|
363
378
|
return [...byId.values()];
|
|
364
379
|
}
|
|
365
380
|
|
|
381
|
+
/** A terminal engine observation supersedes the launch choice; running/launch rows retain their snapshot. */
|
|
382
|
+
export function observedRunModel(run: Partial<Pick<AsyncRun, "status" | "result">>): string | undefined {
|
|
383
|
+
const model = run.status !== undefined && run.status !== "running" ? run.result?.modelUsed : undefined;
|
|
384
|
+
return model?.trim() ? model.split("/").map((part) => sanitizeDisplayLabel(part, "model", 256)).join("/").slice(0, 256) : undefined;
|
|
385
|
+
}
|
|
386
|
+
|
|
366
387
|
/** One identity everywhere a tracked run is rendered: the launcher's codename when present,
|
|
367
|
-
* otherwise the agent type, with the
|
|
368
|
-
export function runDisplayName(run: Pick<AsyncRun, "agent" | "label" | "model">): string {
|
|
369
|
-
const
|
|
388
|
+
* otherwise the agent type, with the observed terminal model or launch model appended once. */
|
|
389
|
+
export function runDisplayName(run: Pick<AsyncRun, "agent" | "label" | "model"> & Partial<Pick<AsyncRun, "status" | "result">>): string {
|
|
390
|
+
const observed = observedRunModel(run);
|
|
391
|
+
const selected = observed ? (observed.split("/").pop() ?? observed).replace(/^claude-/, "") : run.model;
|
|
392
|
+
const model = selected === undefined ? undefined : sanitizeDisplayLabel(selected, "model", 24);
|
|
370
393
|
if (model === undefined) return sanitizeDisplayLabel(run.label ?? run.agent);
|
|
371
394
|
const suffix = ` · ${model}`;
|
|
372
395
|
const nameLimit = Math.max(1, MAX_DISPLAY_LABEL_CHARS - suffix.length);
|
|
@@ -618,6 +641,25 @@ export function buildCompletionReport(runs: AsyncRun[], fence: (text: string) =>
|
|
|
618
641
|
const stoppedCount = stopped.length > 0 ? `, ${stopped.length} stopped` : "";
|
|
619
642
|
const head = `[pi-persona] ${runs.length} async run${runs.length === 1 ? "" : "s"} settled — ${done.length} done, ${failed.length} failed${stoppedCount}`;
|
|
620
643
|
const blocks: string[] = [head];
|
|
644
|
+
const models = runs.flatMap((run) => {
|
|
645
|
+
const model = observedRunModel(run);
|
|
646
|
+
return model ? [`${run.id}: Observed model: ${model}`] : [];
|
|
647
|
+
});
|
|
648
|
+
if (models.length > 0) blocks.push(`\n${models.join("\n")}`);
|
|
649
|
+
const receipts = runs.slice(0, MAX_ASYNC_STATUS_ROWS).flatMap((run) => {
|
|
650
|
+
const hint = resultReceiptHint(run.resultReceipt);
|
|
651
|
+
return hint ? [`${run.id}: ${hint}`] : [];
|
|
652
|
+
});
|
|
653
|
+
if (receipts.length > 0) blocks.push(`\nTerminal reports (intercom results lists all retained references):\n${receipts.join("\n")}`);
|
|
654
|
+
// Common result metadata, outside untrusted worker prose, on inline and background paths.
|
|
655
|
+
// A host effort observation is a Pi request setting, not a measure of provider cognition.
|
|
656
|
+
const efforts = runs.flatMap((r) => {
|
|
657
|
+
const requested = r.result?.thinkingRequested;
|
|
658
|
+
const observed = r.result?.thinkingEffective;
|
|
659
|
+
if (!isThinkingLevel(requested) && !isThinkingLevel(observed)) return [];
|
|
660
|
+
return [`${r.id}: requested ${isThinkingLevel(requested) ? requested : "unknown"}; observed Pi effort ${isThinkingLevel(observed) ? observed : "unknown"}`];
|
|
661
|
+
});
|
|
662
|
+
if (efforts.length > 0) blocks.push(`\nThinking (not provider cognition):\n${efforts.join("\n")}`);
|
|
621
663
|
for (const r of done) {
|
|
622
664
|
// Only fence REAL output — an empty/whitespace result gets a plain "(no output)", never an
|
|
623
665
|
// empty <fence></fence> shell (there's nothing untrusted to guard, and the empty block reads
|
|
@@ -660,7 +702,7 @@ export function buildCompletionReport(runs: AsyncRun[], fence: (text: string) =>
|
|
|
660
702
|
// The cap cuts whole legs from the middle of a wide fan-out. Name every run in the tail, which the
|
|
661
703
|
// cut keeps, so a cut leg is still fetchable by id instead of silently missing.
|
|
662
704
|
const named = report.length > MAX_COMPLETION_REPORT_CHARS
|
|
663
|
-
? `${report}\n\nRun ids in this report: ${runs.slice(0, MAX_ASYNC_STATUS_ROWS).map((r) => r.id).join(", ")}
|
|
705
|
+
? `${report}\n\nRun ids in this report: ${runs.slice(0, MAX_ASYNC_STATUS_ROWS).map((r) => r.id).join(", ")}. Saved reports: intercom { action:"results" }; use result { ref, offset, limit } even if live handles were dropped.`
|
|
664
706
|
: report;
|
|
665
707
|
return boundCompletionSurface(named);
|
|
666
708
|
}
|
|
@@ -697,9 +739,6 @@ export const MAX_EMPTY_RENDER_RETRIES = 3;
|
|
|
697
739
|
export interface IdleNotifierDeps<T> {
|
|
698
740
|
/** Whether the supervisor is idle (not streaming a turn). */
|
|
699
741
|
isIdle: () => boolean;
|
|
700
|
-
/** Legacy host-queue probe. Retained for source compatibility, but deliberately ignored: when
|
|
701
|
-
* the supervisor is idle, starting a turn is what drains any orphaned host follow-ups. */
|
|
702
|
-
hasPending?: () => boolean;
|
|
703
742
|
/** Deliver the coalesced message. Async rejection is treated exactly like a synchronous throw:
|
|
704
743
|
* the batch is restored at the front of the queue and retried. */
|
|
705
744
|
deliver: (message: string) => void | Promise<void>;
|
package/src/engine/child.ts
CHANGED
|
@@ -10,11 +10,13 @@ import * as fs from "node:fs";
|
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
|
|
13
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
13
14
|
import { applyEvent, type ChildUsage, createStreamState, feedLines, type ProgressSnapshot, snapshot } from "./stream.ts";
|
|
14
15
|
|
|
15
16
|
export interface ChildRunSpec {
|
|
16
17
|
task: string;
|
|
17
18
|
model?: string;
|
|
19
|
+
thinking?: ThinkingLevel;
|
|
18
20
|
tools?: string[];
|
|
19
21
|
excludeTools?: string[];
|
|
20
22
|
systemPrompt?: string;
|
|
@@ -32,6 +34,7 @@ export interface ChildRunResult {
|
|
|
32
34
|
/** True when the idle watchdog OR the hard wall-clock cap fired. Disjoint from `aborted`. */
|
|
33
35
|
timedOut: boolean;
|
|
34
36
|
model?: string;
|
|
37
|
+
thinkingEffective?: ThinkingLevel;
|
|
35
38
|
stopReason?: string;
|
|
36
39
|
errorMessage?: string;
|
|
37
40
|
}
|
|
@@ -166,6 +169,7 @@ export async function runChildAgent(
|
|
|
166
169
|
|
|
167
170
|
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
168
171
|
if (spec.model) args.push("--model", spec.model);
|
|
172
|
+
if (spec.thinking !== undefined) args.push("--thinking", spec.thinking);
|
|
169
173
|
if (spec.tools !== undefined) {
|
|
170
174
|
if (spec.tools.length > 0) args.push("--tools", spec.tools.join(","));
|
|
171
175
|
else args.push("--no-tools");
|
|
@@ -201,13 +205,16 @@ export async function runChildAgent(
|
|
|
201
205
|
const inv = resolveInvocation(args);
|
|
202
206
|
// A stale broker wiring in the SUPERVISOR's own environment must not leak into a
|
|
203
207
|
// plain child: PI_PERSONA_BUS would boot the child's bridge against a dead (or
|
|
204
|
-
// foreign) endpoint
|
|
205
|
-
//
|
|
208
|
+
// foreign) endpoint, and a stale reader token would arm a read_result tool nobody
|
|
209
|
+
// granted. The adapter re-adds these deliberately via opts.env when a broker (and a
|
|
210
|
+
// result-reader grant) is actually live for THIS run.
|
|
206
211
|
const inherited = { ...process.env };
|
|
207
212
|
delete inherited.PI_PERSONA_BUS;
|
|
208
213
|
delete inherited.PI_PERSONA_HANDLE;
|
|
209
214
|
delete inherited.PI_PERSONA_PEERS;
|
|
210
215
|
delete inherited.PI_PERSONA_ALLOW_BLOCKING;
|
|
216
|
+
delete inherited.PI_PERSONA_BUS_TOOLS;
|
|
217
|
+
delete inherited.PI_PERSONA_RESULT_READER_TOKEN;
|
|
211
218
|
const proc = spawn(inv.command, inv.args, {
|
|
212
219
|
cwd: spec.cwd ?? process.cwd(),
|
|
213
220
|
shell: false,
|
|
@@ -496,6 +503,7 @@ export async function runChildAgent(
|
|
|
496
503
|
timedOut: timedOut || hardTimedOut || startupTimedOut,
|
|
497
504
|
};
|
|
498
505
|
if (state.model !== undefined) result.model = state.model;
|
|
506
|
+
if (state.thinkingEffective !== undefined) result.thinkingEffective = state.thinkingEffective;
|
|
499
507
|
if (state.stopReason !== undefined) result.stopReason = state.stopReason;
|
|
500
508
|
// errorMessage precedence: a timeout/abort is the *cause of death*, so label
|
|
501
509
|
// it explicitly and fold in any stream-level error rather than letting the
|