@aeondave/pi-persona 1.17.1 → 1.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +47 -18
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
package/src/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,
|
|
@@ -242,7 +249,7 @@ export async function runChildAgent(
|
|
|
242
249
|
const onLine = (line: string) => {
|
|
243
250
|
if (!line.trim()) return;
|
|
244
251
|
try {
|
|
245
|
-
const toolEvent = applyEvent(state, JSON.parse(line)
|
|
252
|
+
const toolEvent = applyEvent(state, JSON.parse(line));
|
|
246
253
|
// Ordinary stream progress stays coalesced to one snapshot per stdout chunk below.
|
|
247
254
|
// Tool lifecycle transitions are the exception: several can share one chunk and each
|
|
248
255
|
// authoritative transition must cross the progress seam exactly once.
|
|
@@ -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
|
|
@@ -509,7 +517,7 @@ export async function runChildAgent(
|
|
|
509
517
|
// start and was waiting on a slow first response.
|
|
510
518
|
result.errorMessage = `agent produced no progress within the ${opts.startupTimeoutMs}ms startup window — either it never started (a stalled init, e.g. an MCP adapter hanging) or its first provider response was slower than the window (a queued or rate-limited provider, a cold local model); raise or disable the window with PI_PERSONA_AGENT_STARTUP_MS (0 disables)${streamErr ? ` (last error: ${streamErr})` : ""}`;
|
|
511
519
|
} else if (timedOut) {
|
|
512
|
-
result.errorMessage = `agent timed out — no output for ${opts.timeoutMs}ms${streamErr ? ` (last error: ${streamErr})` : ""}`;
|
|
520
|
+
result.errorMessage = `agent timed out — no output for ${opts.timeoutMs}ms (raise this leg's timeoutMs if its tool is legitimately silent)${streamErr ? ` (last error: ${streamErr})` : ""}`;
|
|
513
521
|
} else if (aborted) {
|
|
514
522
|
result.errorMessage = `agent aborted${streamErr ? ` (last error: ${streamErr})` : ""}`;
|
|
515
523
|
} else if (oversizedLine) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A missing hashed chunk in Pi's own bundle is a local host problem, not a provider outage.
|
|
2
|
+
* Pure and deliberately narrow: ordinary missing packages or a worker's own imports get no hint. */
|
|
3
|
+
export function piHostModuleHint(error: string): string | undefined {
|
|
4
|
+
const normalized = error.replaceAll("\\", "/");
|
|
5
|
+
const match = /Cannot find module (["'])([^"'\r\n]+\/@earendil-works\/pi-coding-agent\/dist\/bundle\/chunks\/[A-Za-z0-9_-]+-[A-Z0-9]{8}\.js)\1 imported from ([^\r\n]+)/.exec(normalized);
|
|
6
|
+
const missing = match?.[2];
|
|
7
|
+
const importer = match?.[3]?.trim();
|
|
8
|
+
if (!missing || !importer) return undefined;
|
|
9
|
+
const directory = missing.slice(0, missing.lastIndexOf("/") + 1);
|
|
10
|
+
if (!importer.startsWith(directory) || !/^[^/]+\.js$/.test(importer.slice(directory.length))) return undefined;
|
|
11
|
+
return "A local Pi host bundle module could not load; the host may have been updated in place while this session was running. Retry in a fresh Pi process with the same provider/model; only if the fresh process also fails, repair the Pi installation. Switching provider/model does not repair a missing local module.";
|
|
12
|
+
}
|
package/src/engine/inproc.ts
CHANGED
|
@@ -26,20 +26,28 @@ import { makeContactSupervisorTool } from "../bus/contact.ts";
|
|
|
26
26
|
import { makeContactPeerTool } from "../bus/peers.ts";
|
|
27
27
|
import { type ContractDef, contractInstructions, parseAndValidate, pinContract, type PinnedContract } from "../core/contract.ts";
|
|
28
28
|
import { sanitizeDisplayLabel } from "../core/display-label.ts";
|
|
29
|
+
import { resolveLegThinking } from "../core/leg-thinking.ts";
|
|
29
30
|
import { assignedIdentityPrompt } from "../core/naming.ts";
|
|
30
31
|
import { attributeInbound } from "../core/fence.ts";
|
|
31
32
|
import { isThinkingLevel, type ThinkingLevel } from "../core/types.ts";
|
|
32
33
|
import { roleHint } from "../orchestration/roster.ts";
|
|
33
34
|
import { type AgentRunSpec, isPositiveFiniteMs, type SteerFn, type StrategyEngine } from "../orchestration/sdk.ts";
|
|
34
35
|
import type { AgentResult } from "../orchestration/types.ts";
|
|
36
|
+
import { makeReadResultTool } from "../tools/read-result.ts";
|
|
37
|
+
import { admitResultReadTool, referenceHandoffFailure } from "./result-grant.ts";
|
|
38
|
+
import { makeScopedResultReader, type ResultArchiveView } from "../results/retrieval.ts";
|
|
35
39
|
import { looksLikeProviderError } from "./errors.ts";
|
|
40
|
+
import { piHostModuleHint } from "./host-module.ts";
|
|
36
41
|
import { nextChildHandle } from "./handles.ts";
|
|
37
42
|
import { combineSignals } from "./signals.ts";
|
|
38
43
|
import { toolModifierFailure, unknownAgentFailure, unknownContractFailure } from "./spec-preflight.ts";
|
|
44
|
+
import { mergeSessionToolAllowlist } from "./session-tools.ts";
|
|
39
45
|
import { applyEvent, createStreamState, emptyUsage, type ProgressSnapshot, snapshot } from "./stream.ts";
|
|
40
46
|
|
|
41
47
|
/** The minimal slice of Pi's AgentSession the engine drives — also the test seam. */
|
|
42
48
|
export interface InProcSession {
|
|
49
|
+
/** Host readback after Pi applies its capability clamp; injected test sessions may omit it. */
|
|
50
|
+
readonly thinkingLevel?: unknown;
|
|
43
51
|
subscribe(listener: (event: unknown) => void): () => void;
|
|
44
52
|
prompt(input: string): Promise<void>;
|
|
45
53
|
agent: { abort(): void; waitForIdle(): Promise<void>; steer(message: unknown): void };
|
|
@@ -48,12 +56,11 @@ export interface InProcSession {
|
|
|
48
56
|
|
|
49
57
|
export interface CreateSessionOptions {
|
|
50
58
|
model: unknown;
|
|
51
|
-
modelRegistry: ModelRegistry;
|
|
52
59
|
/** Cooperative cancellation for resource/session initialization. Pi's loader does not
|
|
53
60
|
* currently accept a signal itself, but injected factories can stop promptly and the
|
|
54
61
|
* default factory checks it before/after each uncancellable Pi operation. */
|
|
55
62
|
signal?: AbortSignal;
|
|
56
|
-
/** The host's canonical runtime
|
|
63
|
+
/** The host's canonical runtime; shared so auth/provider registrations survive. */
|
|
57
64
|
modelRuntime?: unknown;
|
|
58
65
|
cwd: string;
|
|
59
66
|
/** Pi's global agent dir (~/.pi/agent) for the sub-agent's resource loader. */
|
|
@@ -70,12 +77,9 @@ export interface CreateSessionOptions {
|
|
|
70
77
|
export type CreateInProcSession = (opts: CreateSessionOptions) => Promise<InProcSession>;
|
|
71
78
|
|
|
72
79
|
/**
|
|
73
|
-
* Pi
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* private in its public TypeScript API. Read the runtime only when the expected
|
|
77
|
-
* methods are present; old Pi versions and test facades then retain the legacy
|
|
78
|
-
* modelRegistry path.
|
|
80
|
+
* The current Pi ModelRegistry facade owns the canonical ModelRuntime privately.
|
|
81
|
+
* Recover that runtime only when its expected methods are present, so native provider
|
|
82
|
+
* registrations and in-memory credentials remain shared with delegated sessions.
|
|
79
83
|
*/
|
|
80
84
|
function sharedModelRuntime(registry: ModelRegistry): unknown {
|
|
81
85
|
const candidate = (registry as unknown as { runtime?: unknown }).runtime;
|
|
@@ -156,6 +160,11 @@ export interface InProcessDeps {
|
|
|
156
160
|
* there, and this deadline aborts such a leg outright (failureKind "timeout"); the idle
|
|
157
161
|
* watchdog would not, because any event at all re-arms it. */
|
|
158
162
|
startupTimeoutMs?: number;
|
|
163
|
+
/** The owning session's terminal archive (a GETTER — the owner can change across sessions).
|
|
164
|
+
* Required by any leg carrying `resultRefs` (flow reference handoff): the scoped
|
|
165
|
+
* `read_result` tool pages THIS archive, after a zero-usage preflight verified every
|
|
166
|
+
* granted ref is durably stored. Absent ⇒ such legs fail closed before the session factory. */
|
|
167
|
+
resultArchive?: () => ResultArchiveView | undefined;
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
// The sub-agent must never re-enter the supervisor's orchestration surface. This is the SECOND
|
|
@@ -171,15 +180,6 @@ export function withOrchestrationToolsExcluded(extra: readonly string[] | undefi
|
|
|
171
180
|
return [...new Set([...ORCHESTRATION_TOOLS, ...(extra ?? [])])];
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
/** Preserve the semantic difference between an absent allowlist (Pi defaults) and an explicit
|
|
175
|
-
* empty one (no tools). Custom communication tools join only a non-empty allowlist; an empty
|
|
176
|
-
* grant must not quietly grow capabilities during session construction. */
|
|
177
|
-
export function mergeSessionToolAllowlist(tools: readonly string[] | undefined, customNames: readonly string[]): string[] | undefined {
|
|
178
|
-
if (tools === undefined) return undefined;
|
|
179
|
-
if (tools.length === 0) return [];
|
|
180
|
-
return [...new Set([...tools, ...customNames])];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
183
|
/** Default factory: a real in-process Pi session. The resource loader appends the
|
|
184
184
|
* agent's persona to the base system prompt (the in-process `--append-system-prompt`).
|
|
185
185
|
* Extensions DO load (so auth providers like pi-claude work) — the fork-bomb guard is
|
|
@@ -198,14 +198,8 @@ const createPiSession: CreateInProcSession = async (opts) => {
|
|
|
198
198
|
// otherwise the allowlist would filter them out of the child's active set.
|
|
199
199
|
const customNames = (opts.customTools ?? []).map((t) => t.name);
|
|
200
200
|
const tools = mergeSessionToolAllowlist(opts.tools, customNames);
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
// sub-session; newer pi takes the canonical `modelRuntime` instead. The extension-facing
|
|
204
|
-
// ModelRegistry keeps that runtime private in its type surface, so `sharedModelRuntime()` above
|
|
205
|
-
// recovers it only when the runtime shape is present. Passing it is important even when auth is
|
|
206
|
-
// on disk: native providers and runtime credentials can exist only in the supervisor's memory.
|
|
207
|
-
// `agentDir` remains a valid fallback for older/standalone SDKs, and the legacy key stays outside
|
|
208
|
-
// the object literal so current SDK types do not reject it.
|
|
201
|
+
// Current Pi consumes the canonical modelRuntime. Sharing it also preserves native providers
|
|
202
|
+
// and credentials that exist only in the supervisor's memory.
|
|
209
203
|
const sessionOptions: NonNullable<Parameters<typeof createAgentSession>[0]> = {
|
|
210
204
|
model: opts.model as NonNullable<NonNullable<Parameters<typeof createAgentSession>[0]>["model"]>,
|
|
211
205
|
sessionManager: SessionManager.inMemory(opts.cwd),
|
|
@@ -218,14 +212,9 @@ const createPiSession: CreateInProcSession = async (opts) => {
|
|
|
218
212
|
excludeTools: withOrchestrationToolsExcluded(opts.excludeTools),
|
|
219
213
|
...(tools !== undefined ? { tools } : {}),
|
|
220
214
|
...(opts.customTools && opts.customTools.length > 0 ? { customTools: opts.customTools } : {}),
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
// here on a pi that supports it, and pi clamps an unknown level regardless).
|
|
224
|
-
...(opts.thinkingLevel
|
|
225
|
-
? { thinkingLevel: opts.thinkingLevel as NonNullable<NonNullable<Parameters<typeof createAgentSession>[0]>["thinkingLevel"]> }
|
|
226
|
-
: {}),
|
|
215
|
+
// Pi owns capability clamping (which can move effort up as well as down).
|
|
216
|
+
...(opts.thinkingLevel !== undefined ? { thinkingLevel: opts.thinkingLevel } : {}),
|
|
227
217
|
};
|
|
228
|
-
(sessionOptions as { modelRegistry?: unknown }).modelRegistry = opts.modelRegistry;
|
|
229
218
|
const { session } = await createAgentSession(sessionOptions);
|
|
230
219
|
if (opts.signal?.aborted) {
|
|
231
220
|
try {
|
|
@@ -236,6 +225,10 @@ const createPiSession: CreateInProcSession = async (opts) => {
|
|
|
236
225
|
throw new DOMException("Session initialization aborted", "AbortError");
|
|
237
226
|
}
|
|
238
227
|
return {
|
|
228
|
+
get thinkingLevel() {
|
|
229
|
+
const level: unknown = session.thinkingLevel;
|
|
230
|
+
return isThinkingLevel(level) ? level : undefined;
|
|
231
|
+
},
|
|
239
232
|
subscribe: (l) => session.subscribe(l as Parameters<typeof session.subscribe>[0]),
|
|
240
233
|
prompt: async (input) => {
|
|
241
234
|
await session.prompt(input);
|
|
@@ -315,6 +308,13 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
315
308
|
async run(spec: AgentRunSpec, onProgress?, callSignal?, onSteerable?): Promise<AgentResult> {
|
|
316
309
|
const cfg = deps.resolveAgent(spec.agent);
|
|
317
310
|
if (!cfg) return unknownAgentFailure(spec.agent, deps.listAgents?.() ?? []);
|
|
311
|
+
let thinkingRequested: ThinkingLevel | undefined;
|
|
312
|
+
try {
|
|
313
|
+
thinkingRequested = resolveLegThinking(spec, cfg, deps.childThinking);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
return { agent: spec.agent, output: "", usage: emptyUsage(), ok: false, failureKind: "contract", error: `[${spec.agent}] ${error instanceof Error ? error.message : String(error)}` };
|
|
316
|
+
}
|
|
317
|
+
const thinkingMetadata = thinkingRequested !== undefined ? { thinkingRequested } : {};
|
|
318
318
|
const tools = spec.tools ?? cfg.tools;
|
|
319
319
|
const invalidTools = toolModifierFailure(spec.agent, tools);
|
|
320
320
|
if (invalidTools) return invalidTools;
|
|
@@ -332,7 +332,7 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
332
332
|
// Make the failure self-correcting: name a few real registry refs (nearest
|
|
333
333
|
// matches first) so the supervisor's retry can pick a valid model at once.
|
|
334
334
|
const all = deps.modelRegistry.getAll().map((m) => `${m.provider}/${m.id}`);
|
|
335
|
-
const frag = (ref ?? "").split("/").pop()?.
|
|
335
|
+
const frag = (ref ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
336
336
|
const near = frag ? all.filter((r) => r.toLowerCase().includes(frag)) : [];
|
|
337
337
|
const hint = (near.length > 0 ? near : all).slice(0, 5);
|
|
338
338
|
return {
|
|
@@ -341,16 +341,40 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
341
341
|
usage: emptyUsage(),
|
|
342
342
|
ok: false,
|
|
343
343
|
error: `[${spec.agent} · ${ref ?? "(no model)"}] model not found in registry (from ${src})${hint.length > 0 ? ` — try e.g.: ${hint.join(", ")}` : ""}`,
|
|
344
|
-
|
|
344
|
+
failureKind: "unknown-model",
|
|
345
|
+
...thinkingMetadata,
|
|
345
346
|
};
|
|
346
347
|
}
|
|
347
348
|
// The canonical provider/id this run resolved to — surfaced on every result (for the
|
|
348
349
|
// UI/cost) and, on a provider failure, used as the seed of the fallback chain.
|
|
349
350
|
const rm = model as { provider?: string; id?: string };
|
|
350
351
|
const resolvedRef = rm.provider && rm.id ? `${rm.provider}/${rm.id}` : ref;
|
|
351
|
-
const thinkingLevel = isThinkingLevel(deps.childThinking) ? deps.childThinking : undefined;
|
|
352
352
|
const excludeTools = cfg.excludeTools;
|
|
353
353
|
|
|
354
|
+
// ── reference handoff preflight (contract 3/4) — BEFORE the session factory, after every
|
|
355
|
+
// cheap zero-usage rejection above it. Admission uses the SAME shared rule as the child
|
|
356
|
+
// engine; storage is verified durable (session-entry); a failure is a zero-usage
|
|
357
|
+
// contract failure and the factory is never called. The revoke rides `releaseHandle()`
|
|
358
|
+
// (every exit path from here on drains it), so the scoped reader dies with the leg.
|
|
359
|
+
const legCleanups: Array<() => void> = [];
|
|
360
|
+
let resultReadTool: ToolDefinition | undefined;
|
|
361
|
+
let admittedTools = tools;
|
|
362
|
+
if (spec.resultRefs !== undefined && spec.resultRefs.length > 0) {
|
|
363
|
+
const grant = admitResultReadTool({
|
|
364
|
+
specTools: spec.tools, cfgTools: cfg.tools, excludeTools: cfg.excludeTools,
|
|
365
|
+
canUseBus: deps.canUseBus ?? true,
|
|
366
|
+
});
|
|
367
|
+
if (!grant.ok) return referenceHandoffFailure(spec.agent, grant.error);
|
|
368
|
+
const archive = deps.resultArchive?.();
|
|
369
|
+
if (archive === undefined) return referenceHandoffFailure(spec.agent, "terminal result archive unavailable for reference handoff");
|
|
370
|
+
const scoped = makeScopedResultReader(archive, spec.resultRefs);
|
|
371
|
+
const { missing } = scoped.preflight();
|
|
372
|
+
if (missing.length > 0) return referenceHandoffFailure(spec.agent, `archived results unavailable: ${missing.join(", ")}`);
|
|
373
|
+
resultReadTool = makeReadResultTool(scoped);
|
|
374
|
+
admittedTools = grant.tools;
|
|
375
|
+
legCleanups.push(() => scoped.revoke());
|
|
376
|
+
}
|
|
377
|
+
|
|
354
378
|
// Diagnostic tag: which agent + which model ref + whether it was dynamically
|
|
355
379
|
// specialised (skills / model / tools override on the spec). Users need this
|
|
356
380
|
// to tell "the delegate typo'd the model" from "the provider rejected it".
|
|
@@ -367,20 +391,19 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
367
391
|
// Already cancelled (e.g. a queued async run stopped before its slot came up):
|
|
368
392
|
// don't build a session or burn a model call — settle as aborted right away.
|
|
369
393
|
if (signal?.aborted) {
|
|
370
|
-
return { agent: spec.agent, output: "", usage: emptyUsage(), ok: false, error: `${tag} agent aborted`, ...(resolvedRef ? { modelUsed: resolvedRef } : {}), failureKind: "abort" };
|
|
394
|
+
return { agent: spec.agent, output: "", usage: emptyUsage(), ok: false, error: `${tag} agent aborted`, ...(resolvedRef ? { modelUsed: resolvedRef } : {}), ...thinkingMetadata, failureKind: "abort" };
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
const modelRuntime = sharedModelRuntime(deps.modelRegistry);
|
|
374
398
|
const sessionOpts: CreateSessionOptions = {
|
|
375
399
|
model,
|
|
376
|
-
modelRegistry: deps.modelRegistry,
|
|
377
400
|
...(modelRuntime !== undefined ? { modelRuntime } : {}),
|
|
378
401
|
cwd: deps.cwd,
|
|
379
402
|
agentDir: deps.agentDir ?? deps.cwd,
|
|
380
403
|
};
|
|
381
|
-
if (
|
|
404
|
+
if (admittedTools !== undefined) sessionOpts.tools = admittedTools;
|
|
382
405
|
if (excludeTools && excludeTools.length > 0) sessionOpts.excludeTools = excludeTools;
|
|
383
|
-
if (
|
|
406
|
+
if (thinkingRequested !== undefined) sessionOpts.thinkingLevel = thinkingRequested;
|
|
384
407
|
// The shared behavioral layer (docs/SPINE.md) + the agent's own persona + any
|
|
385
408
|
// on-the-fly `role` specialisation from the spec. All three reach the session as the
|
|
386
409
|
// loader's `appendSystemPrompt`, so a leg keeps Pi's full base prompt either way — the
|
|
@@ -397,20 +420,21 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
397
420
|
if (spec.peers === true && !wantsPeers && process.env.PI_PERSONA_DEBUG) {
|
|
398
421
|
process.stderr.write(`[pi-persona] peers requested for ${spec.agent} but bus/capability unavailable — running without contact_peer\n`);
|
|
399
422
|
}
|
|
400
|
-
|
|
423
|
+
const customTools: ToolDefinition[] = [];
|
|
424
|
+
const wantsCoaching = deps.coaching === true && (deps.canUseBus ?? true);
|
|
425
|
+
if (deps.bus && (wantsCoaching || wantsPeers)) {
|
|
401
426
|
childHandle = nextChildHandle(spec.agent);
|
|
402
427
|
deps.bus.register(supervisorHandle);
|
|
403
428
|
deps.bus.register(childHandle);
|
|
404
|
-
|
|
405
|
-
if (deps.coaching) {
|
|
429
|
+
if (wantsCoaching) {
|
|
406
430
|
customTools.push(
|
|
407
431
|
makeContactSupervisorTool(deps.bus, childHandle, supervisorHandle, { allowBlocking: deps.allowBlocking ?? false }),
|
|
408
432
|
);
|
|
409
433
|
}
|
|
410
434
|
if (wantsPeers) {
|
|
411
435
|
const label = spec.name?.trim()
|
|
412
|
-
|
|
413
|
-
|
|
436
|
+
? sanitizeDisplayLabel(spec.name, childHandle)
|
|
437
|
+
: spec.role ? `${childHandle} (${roleHint(spec.role)})` : childHandle;
|
|
414
438
|
peerLabels.set(childHandle, label);
|
|
415
439
|
const self = childHandle;
|
|
416
440
|
customTools.push(
|
|
@@ -419,8 +443,12 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
419
443
|
}),
|
|
420
444
|
);
|
|
421
445
|
}
|
|
422
|
-
sessionOpts.customTools = customTools;
|
|
423
446
|
}
|
|
447
|
+
// A granted reference-handoff reader joins the SAME custom-tools surface (contract 3):
|
|
448
|
+
// literal `read_result` already admitted into a nonempty allowlist above; an absent
|
|
449
|
+
// allowlist stays absent and the injected tool rides Pi's session defaults.
|
|
450
|
+
if (resultReadTool !== undefined) customTools.push(resultReadTool);
|
|
451
|
+
if (customTools.length > 0) sessionOpts.customTools = customTools;
|
|
424
452
|
|
|
425
453
|
// The handle is live on the SESSION-LONG bus from here on, while the only unregister sits
|
|
426
454
|
// in the run's own finally below. Anything that throws before we reach it must release
|
|
@@ -428,6 +456,11 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
428
456
|
// for the rest of the supervisor's session.
|
|
429
457
|
let handleReleased = false;
|
|
430
458
|
const releaseHandle = (): void => {
|
|
459
|
+
// Leg-scoped cleanups FIRST — the reference reader revoke must run even on a leg that
|
|
460
|
+
// never owned a bus handle (non-coaching, peer-less).
|
|
461
|
+
for (const cleanup of legCleanups.splice(0)) {
|
|
462
|
+
try { cleanup(); } catch { /* a throwing cleanup must never strand its siblings */ }
|
|
463
|
+
}
|
|
431
464
|
if (!childHandle || handleReleased) return;
|
|
432
465
|
handleReleased = true;
|
|
433
466
|
peerLabels.delete(childHandle);
|
|
@@ -577,6 +610,7 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
577
610
|
error: `${tag} agent aborted`,
|
|
578
611
|
...(resolvedRef ? { modelUsed: resolvedRef } : {}),
|
|
579
612
|
failureKind: "abort",
|
|
613
|
+
...thinkingMetadata,
|
|
580
614
|
};
|
|
581
615
|
}
|
|
582
616
|
|
|
@@ -648,6 +682,7 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
648
682
|
error: `${initError}${cleanupHint}`,
|
|
649
683
|
...(resolvedRef ? { modelUsed: resolvedRef } : {}),
|
|
650
684
|
failureKind: hardTimedOut || timedOut || startupTimedOut ? "timeout" : "abort",
|
|
685
|
+
...thinkingMetadata,
|
|
651
686
|
};
|
|
652
687
|
}
|
|
653
688
|
session = initializationOutcome.session;
|
|
@@ -666,7 +701,7 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
666
701
|
// One progress tick, routed exactly as the subscription routes them: the per-call sink
|
|
667
702
|
// takes the AgentProgress shape (no `turns`), else the construction-time sink.
|
|
668
703
|
const emitProgress = (snap: ProgressSnapshot): void => {
|
|
669
|
-
if (onProgress) onProgress({ output: snap.output, tokens: snap.tokens, ...(snap.
|
|
704
|
+
if (onProgress) onProgress({ output: snap.output, tokens: snap.tokens, ...(snap.usage ? { usage: snap.usage } : {}), ...(snap.activity ? { activity: snap.activity } : {}), ...(snap.toolEvent ? { toolEvent: snap.toolEvent } : {}) });
|
|
670
705
|
else if (deps.onProgress) deps.onProgress(snap);
|
|
671
706
|
};
|
|
672
707
|
let contractDef: ContractDef | undefined = requestedDef;
|
|
@@ -674,7 +709,7 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
674
709
|
try {
|
|
675
710
|
unsub = session.subscribe((ev) => {
|
|
676
711
|
armIdle(); // any event = activity → reset the idle clock
|
|
677
|
-
const toolEvent = applyEvent(state, ev
|
|
712
|
+
const toolEvent = applyEvent(state, ev);
|
|
678
713
|
// Track the open calls so an abandoned one can be closed once the run settles.
|
|
679
714
|
if (toolEvent) {
|
|
680
715
|
if (toolEvent.phase === "start") openToolCalls.set(toolEvent.callId, toolEvent.name);
|
|
@@ -735,7 +770,7 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
735
770
|
// launch path (extension.ts) only has the latter.
|
|
736
771
|
const snap = snapshot(state);
|
|
737
772
|
const activity = `✉ from ${env.from}`;
|
|
738
|
-
if (onProgress) onProgress({ output: snap.output, tokens: snap.tokens, ...(snap.
|
|
773
|
+
if (onProgress) onProgress({ output: snap.output, tokens: snap.tokens, ...(snap.usage ? { usage: snap.usage } : {}), activity });
|
|
739
774
|
else if (deps.onProgress) deps.onProgress({ ...snap, activity });
|
|
740
775
|
}
|
|
741
776
|
};
|
|
@@ -773,6 +808,7 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
773
808
|
// folded into the AgentResult below. Otherwise the stream state stays empty
|
|
774
809
|
// and the user sees an opaque "agent failed (unknown)".
|
|
775
810
|
let thrownError: string | undefined;
|
|
811
|
+
let thinkingEffective: ThinkingLevel | undefined;
|
|
776
812
|
try {
|
|
777
813
|
armIdle(); // start the idle clock (reset on every session event)
|
|
778
814
|
armHard(); // start the lifetime ceiling (never reset)
|
|
@@ -795,6 +831,8 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
795
831
|
releaseHandle();
|
|
796
832
|
unsub?.();
|
|
797
833
|
steerable = false;
|
|
834
|
+
const observed = session.thinkingLevel;
|
|
835
|
+
if (isThinkingLevel(observed)) thinkingEffective = observed;
|
|
798
836
|
session.dispose();
|
|
799
837
|
}
|
|
800
838
|
|
|
@@ -823,9 +861,13 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
823
861
|
}
|
|
824
862
|
}
|
|
825
863
|
|
|
826
|
-
const result: AgentResult = { agent: spec.agent, output: state.output, usage: state.usage, ok, ...(resolvedRef ? { modelUsed: resolvedRef } : {}) };
|
|
864
|
+
const result: AgentResult = { agent: spec.agent, output: state.output, usage: state.usage, ok, ...(resolvedRef ? { modelUsed: resolvedRef } : {}), ...thinkingMetadata };
|
|
865
|
+
// The final message describes the effort of that request; session readback is the
|
|
866
|
+
// observed setting when a host emits no effort field. Neither proves provider cognition.
|
|
867
|
+
const observed = state.thinkingEffective ?? thinkingEffective;
|
|
868
|
+
if (observed !== undefined) result.thinkingEffective = observed;
|
|
827
869
|
// A timeout/abort is the *cause of death* — label it first (mirrors the child engine).
|
|
828
|
-
if (timedOut) result.error = `${tag} agent timed out — no events for ${watchdogMs}ms${state.errorMessage ? ` (last error: ${state.errorMessage})` : ""}`;
|
|
870
|
+
if (timedOut) result.error = `${tag} agent timed out — no events for ${watchdogMs}ms (raise this leg's timeoutMs if its tool is legitimately silent)${state.errorMessage ? ` (last error: ${state.errorMessage})` : ""}`;
|
|
829
871
|
// The deadline cannot tell the two causes apart, so the message names both: claiming
|
|
830
872
|
// "it never started" would misdirect the operator of a leg that did start and was
|
|
831
873
|
// simply waiting on a slow first response (mirrors the child engine's wording).
|
|
@@ -836,6 +878,8 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
836
878
|
else if (state.errorMessage) result.error = `${tag} ${state.errorMessage}`;
|
|
837
879
|
else if (thrownError) result.error = `${tag} ${thrownError}`;
|
|
838
880
|
else if (!ok) result.error = `${tag} agent failed (${state.stopReason ?? "unknown"})`;
|
|
881
|
+
const hostModuleHint = !ok ? piHostModuleHint(state.errorMessage ?? thrownError ?? "") : undefined;
|
|
882
|
+
if (hostModuleHint && result.error) result.error += ` — ${hostModuleHint}`;
|
|
839
883
|
// Classify the failure so the fallback decorator can reroute ONLY provider errors,
|
|
840
884
|
// never an abort/timeout/hard-cap/agent miss. A stream `error` stop reason is pi's
|
|
841
885
|
// own report of an API failure → provider. A THROWN error is not automatically a
|
|
@@ -846,9 +890,11 @@ export function makeInProcessEngine(deps: InProcessDeps): StrategyEngine {
|
|
|
846
890
|
? "timeout"
|
|
847
891
|
: aborted
|
|
848
892
|
? "abort"
|
|
849
|
-
:
|
|
850
|
-
? "
|
|
851
|
-
: "
|
|
893
|
+
: hostModuleHint
|
|
894
|
+
? "agent"
|
|
895
|
+
: state.stopReason === "error" || (thrownError !== undefined && looksLikeProviderError(thrownError))
|
|
896
|
+
? "provider"
|
|
897
|
+
: "agent";
|
|
852
898
|
|
|
853
899
|
if (contractDef) {
|
|
854
900
|
const v = parseAndValidate(state.output, contractDef);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frozen `read_result` admission rule (flow reference handoff, contract 4) — ONE helper
|
|
3
|
+
* shared by BOTH engine backends, so the child-process and in-process paths can never drift.
|
|
4
|
+
*
|
|
5
|
+
* effective tools = spec.tools ?? cfg.tools. An explicit `[]` denies. A nonempty allowlist
|
|
6
|
+
* must already include `read` OR `read_result` (the grant builds on a leg that can read —
|
|
7
|
+
* it never widens a write-only specialist into a reader). Any deny (agent `excludeTools`,
|
|
8
|
+
* wildcards included) matching either name denies. The parent's `canUseBus` capability must
|
|
9
|
+
* be true for reference mode at all; explicit parent denies always win. Only after a grant:
|
|
10
|
+
* the literal `read_result` joins a nonempty allowlist; an absent allowlist stays absent
|
|
11
|
+
* (session defaults + the injected custom tool).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isAllowed } from "../core/permissions.ts";
|
|
15
|
+
import type { AgentResult } from "../orchestration/types.ts";
|
|
16
|
+
import { emptyUsage } from "./stream.ts";
|
|
17
|
+
|
|
18
|
+
/** The literal, never-renamed tool a reference-handoff leg reads archived results through. */
|
|
19
|
+
export const RESULT_READ_TOOL = "read_result";
|
|
20
|
+
|
|
21
|
+
export interface ResultToolAdmission {
|
|
22
|
+
specTools: readonly string[] | undefined;
|
|
23
|
+
cfgTools: readonly string[] | undefined;
|
|
24
|
+
/** The agent's own deny list (glob patterns allowed — the same matcher Pi's permission
|
|
25
|
+
* layer uses, so a `read*` deny here means what it means everywhere else). */
|
|
26
|
+
excludeTools: readonly string[] | undefined;
|
|
27
|
+
/** The persona-level bus capability (`EffectiveCapabilities.canUseBus`). */
|
|
28
|
+
canUseBus: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ResultToolGrant =
|
|
32
|
+
| { ok: false; error: string }
|
|
33
|
+
/** `tools` is the effective allowlist to run the leg with: undefined = an absent
|
|
34
|
+
* allowlist stays absent (session default + custom-tool injection). */
|
|
35
|
+
| { ok: true; tools: string[] | undefined };
|
|
36
|
+
|
|
37
|
+
export function admitResultReadTool(input: ResultToolAdmission): ResultToolGrant {
|
|
38
|
+
if (!input.canUseBus) {
|
|
39
|
+
return { ok: false, error: "reference handoff requires the bus capability (this persona denies it)" };
|
|
40
|
+
}
|
|
41
|
+
const tools = input.specTools ?? input.cfgTools;
|
|
42
|
+
if (tools !== undefined && tools.length === 0) {
|
|
43
|
+
return { ok: false, error: "reference handoff requires the read tool; this leg's tool allowlist is empty" };
|
|
44
|
+
}
|
|
45
|
+
// Denies are evaluated by NAME against both spellings, wildcards included, before any
|
|
46
|
+
// allowlist reasoning — an explicit deny always wins over the grant below.
|
|
47
|
+
const deny = { deny: [...(input.excludeTools ?? [])] };
|
|
48
|
+
if (!isAllowed("read", deny) || !isAllowed(RESULT_READ_TOOL, deny)) {
|
|
49
|
+
return { ok: false, error: "reference handoff requires the read tool; read/read_result is denied for this leg" };
|
|
50
|
+
}
|
|
51
|
+
if (tools !== undefined && !tools.includes("read") && !tools.includes(RESULT_READ_TOOL)) {
|
|
52
|
+
return { ok: false, error: `reference handoff requires the read or ${RESULT_READ_TOOL} tool; the allowlist names neither` };
|
|
53
|
+
}
|
|
54
|
+
if (tools === undefined) return { ok: true, tools: undefined };
|
|
55
|
+
return { ok: true, tools: [...new Set([...tools, RESULT_READ_TOOL])] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A reference-handoff preflight rejection: ZERO usage (nothing ran), terminal for the leg,
|
|
59
|
+
* listing what is missing so the caller can report it honestly. */
|
|
60
|
+
export function referenceHandoffFailure(agent: string, error: string): AgentResult {
|
|
61
|
+
return { agent, output: "", usage: emptyUsage(), ok: false, error: `[${agent}] ${error}`, failureKind: "contract" };
|
|
62
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Preserve absent/default and explicit empty grants when adding runtime-owned custom tools. */
|
|
2
|
+
export function mergeSessionToolAllowlist(tools: readonly string[] | undefined, customNames: readonly string[]): string[] | undefined {
|
|
3
|
+
if (tools === undefined) return undefined;
|
|
4
|
+
if (tools.length === 0) return [];
|
|
5
|
+
return [...new Set([...tools, ...customNames])];
|
|
6
|
+
}
|
package/src/engine/stream.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* Mirrors the canonical recipe in Pi's reference subagent example.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { isThinkingLevel, type ThinkingLevel } from "../core/types.ts";
|
|
13
|
+
|
|
12
14
|
export interface ChildUsage {
|
|
13
15
|
input: number;
|
|
14
16
|
output: number;
|
|
@@ -53,6 +55,8 @@ export interface StreamState {
|
|
|
53
55
|
deltaTail: number;
|
|
54
56
|
usage: ChildUsage;
|
|
55
57
|
model?: string;
|
|
58
|
+
/** Final assistant-message Pi effort, not provider cognition. Unknown stays absent. */
|
|
59
|
+
thinkingEffective?: ThinkingLevel;
|
|
56
60
|
stopReason?: string;
|
|
57
61
|
errorMessage?: string;
|
|
58
62
|
sawAssistant: boolean;
|
|
@@ -61,8 +65,6 @@ export interface StreamState {
|
|
|
61
65
|
/** Calls whose start was surfaced but whose end has not arrived. This is not a retention
|
|
62
66
|
* cache: active ids are never evicted merely because completed calls churn. */
|
|
63
67
|
activeToolCalls?: Set<string>;
|
|
64
|
-
/** Timestamp of the latest textual tool_execution_update, used only as a liveness signal. */
|
|
65
|
-
toolOutputAt?: number;
|
|
66
68
|
/** Recently completed call ids whose start/end phase was surfaced. Both caches are bounded;
|
|
67
69
|
* they suppress duplicate wire events without growing for the lifetime of a long run. */
|
|
68
70
|
seenToolStarts?: Set<string>;
|
|
@@ -238,7 +240,7 @@ function appendTranscript(state: StreamState, chunk: string, sep: string): void
|
|
|
238
240
|
|
|
239
241
|
/** Fold one parsed stream event into the accumulating state (in place), returning a
|
|
240
242
|
* deduplicated structured lifecycle transition when the runtime supplied a stable call id. */
|
|
241
|
-
export function applyEvent(state: StreamState, event: unknown
|
|
243
|
+
export function applyEvent(state: StreamState, event: unknown): ToolEvent | undefined {
|
|
242
244
|
if (!isObject(event)) return undefined;
|
|
243
245
|
|
|
244
246
|
// Track the current tool so the UI can show "running: grep src/…" while a
|
|
@@ -273,20 +275,6 @@ export function applyEvent(state: StreamState, event: unknown, observedAt?: numb
|
|
|
273
275
|
return toolEvent;
|
|
274
276
|
}
|
|
275
277
|
|
|
276
|
-
if (event.type === "tool_execution_update") {
|
|
277
|
-
const callId = event.toolCallId;
|
|
278
|
-
const toolName = event.toolName;
|
|
279
|
-
const partialResult = event.partialResult;
|
|
280
|
-
const hasText = isObject(partialResult) && Array.isArray(partialResult.content)
|
|
281
|
-
&& partialResult.content.some((part) => isObject(part) && part.type === "text" && typeof part.text === "string" && part.text.length > 0);
|
|
282
|
-
if (typeof callId === "string" && callId.length > 0
|
|
283
|
-
&& typeof toolName === "string" && toolName.length > 0
|
|
284
|
-
&& state.activeToolCalls?.has(callId) === true && hasText && observedAt !== undefined) {
|
|
285
|
-
state.toolOutputAt = observedAt;
|
|
286
|
-
}
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
278
|
// Live partial: pi streams the growing message as `message_update` events. Surface
|
|
291
279
|
// its text (or reasoning) so a long-thinking agent shows progress, not just tokens.
|
|
292
280
|
// ONLY the assistant's own message counts: pi also emits message_start/message_end for
|
|
@@ -322,6 +310,8 @@ export function applyEvent(state: StreamState, event: unknown, observedAt?: numb
|
|
|
322
310
|
resetDeltaParts(state); // the next message's deltas start from an empty slate
|
|
323
311
|
state.sawAssistant = true;
|
|
324
312
|
state.usage.turns++;
|
|
313
|
+
if (isThinkingLevel(msg.thinkingLevel)) state.thinkingEffective = msg.thinkingLevel;
|
|
314
|
+
else delete state.thinkingEffective;
|
|
325
315
|
|
|
326
316
|
const text = firstText(msg.content);
|
|
327
317
|
if (text !== undefined) state.output = text;
|
|
@@ -355,10 +345,10 @@ export interface ProgressSnapshot {
|
|
|
355
345
|
output: string;
|
|
356
346
|
turns: number;
|
|
357
347
|
tokens: number;
|
|
348
|
+
/** Copied cumulative counters for display; optional for legacy/custom progress sources. */
|
|
349
|
+
usage?: ChildUsage;
|
|
358
350
|
/** The tool currently running (e.g. "grep src/…"), if any. */
|
|
359
351
|
activity?: string;
|
|
360
|
-
/** Timestamp of the latest textual tool output update, when available. */
|
|
361
|
-
toolOutputAt?: number;
|
|
362
352
|
/** One authoritative runtime tool lifecycle transition, when this snapshot is event-driven. */
|
|
363
353
|
toolEvent?: ToolEvent;
|
|
364
354
|
}
|
|
@@ -370,9 +360,9 @@ export function snapshot(state: StreamState, toolEvent?: ToolEvent): ProgressSna
|
|
|
370
360
|
output: [state.transcript, state.partial].filter((s) => s).join("\n\n") || state.output,
|
|
371
361
|
turns: state.usage.turns,
|
|
372
362
|
tokens: state.usage.input + state.usage.output,
|
|
363
|
+
usage: { ...state.usage },
|
|
373
364
|
};
|
|
374
365
|
if (state.activity !== undefined) snap.activity = state.activity;
|
|
375
|
-
if (state.toolOutputAt !== undefined) snap.toolOutputAt = state.toolOutputAt;
|
|
376
366
|
if (toolEvent !== undefined) snap.toolEvent = toolEvent;
|
|
377
367
|
return snap;
|
|
378
368
|
}
|