@aeondave/pi-persona 1.15.2
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 +249 -0
- package/CHANGELOG.md +260 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/agents/balthasar.md +32 -0
- package/agents/casper.md +33 -0
- package/agents/evidence-verifier.md +23 -0
- package/agents/melchior.md +32 -0
- package/agents/operator.md +63 -0
- package/agents/research.md +58 -0
- package/agents/reviewer.md +49 -0
- package/agents/scout.md +23 -0
- package/agents/verifier.md +21 -0
- package/assets/banner.png +0 -0
- package/assets/demo1.gif +0 -0
- package/assets/demo1.mp4 +0 -0
- package/assets/workflow.png +0 -0
- package/contracts/finding.contract.json +9 -0
- package/docs/ARCHITECTURE.md +727 -0
- package/docs/EXPERIENCE.md +96 -0
- package/docs/MONITORS.md +84 -0
- package/docs/README.md +53 -0
- package/docs/REFERENCE.md +408 -0
- package/docs/SPINE.md +152 -0
- package/docs/STRATEGIES.md +215 -0
- package/docs/TELEMETRY.md +9 -0
- package/flows/deep-review.flow.json +8 -0
- package/flows/gated-build.flow.json +9 -0
- package/package.json +106 -0
- package/personas/audit.md +29 -0
- package/personas/dev.md +52 -0
- package/personas/elite.md +256 -0
- package/personas/judge.md +32 -0
- package/personas/magi.md +41 -0
- package/personas/planner.md +37 -0
- package/personas/researcher.md +37 -0
- package/personas/swarm.md +38 -0
- package/personas/verify.md +31 -0
- package/presets/magi-rounds.preset.json +5 -0
- package/prompts/spine.md +11 -0
- package/prompts/spine.worker.md +9 -0
- package/src/agents/agent.ts +78 -0
- package/src/bridge.ts +209 -0
- package/src/bus/broker/client.ts +379 -0
- package/src/bus/broker/framing.ts +43 -0
- package/src/bus/broker/host.ts +407 -0
- package/src/bus/broker/messages.ts +87 -0
- package/src/bus/broker/paths.ts +31 -0
- package/src/bus/contact.ts +145 -0
- package/src/bus/inproc.ts +279 -0
- package/src/bus/peers.ts +114 -0
- package/src/core/brief.ts +356 -0
- package/src/core/capabilities.ts +114 -0
- package/src/core/config.ts +228 -0
- package/src/core/contract.ts +391 -0
- package/src/core/data-root.ts +260 -0
- package/src/core/discovery.ts +33 -0
- package/src/core/display-label.ts +47 -0
- package/src/core/fence.ts +151 -0
- package/src/core/format.ts +13 -0
- package/src/core/frontmatter.ts +202 -0
- package/src/core/models.ts +192 -0
- package/src/core/monitor.ts +415 -0
- package/src/core/naming.ts +42 -0
- package/src/core/nudge.ts +238 -0
- package/src/core/ownership.ts +102 -0
- package/src/core/permissions.ts +56 -0
- package/src/core/pi-compat.ts +50 -0
- package/src/core/seed.ts +770 -0
- package/src/core/session-identity.ts +89 -0
- package/src/core/time.ts +202 -0
- package/src/core/timer.ts +221 -0
- package/src/core/types.ts +28 -0
- package/src/engine/adapter.ts +257 -0
- package/src/engine/async.ts +898 -0
- package/src/engine/change-report.ts +143 -0
- package/src/engine/child.ts +532 -0
- package/src/engine/errors.ts +30 -0
- package/src/engine/fallback.ts +57 -0
- package/src/engine/handles.ts +21 -0
- package/src/engine/inproc.ts +857 -0
- package/src/engine/signals.ts +11 -0
- package/src/engine/spec-preflight.ts +33 -0
- package/src/engine/stream.ts +369 -0
- package/src/engine/worktree.ts +144 -0
- package/src/exocom/activation.ts +31 -0
- package/src/exocom/codes.ts +261 -0
- package/src/exocom/envelope.ts +180 -0
- package/src/exocom/gate.ts +6 -0
- package/src/exocom/guards.ts +45 -0
- package/src/exocom/inbound.ts +141 -0
- package/src/exocom/install.ts +970 -0
- package/src/exocom/ledger.ts +603 -0
- package/src/exocom/limits.ts +20 -0
- package/src/exocom/paths.ts +35 -0
- package/src/exocom/plane.ts +1116 -0
- package/src/exocom/registry.ts +307 -0
- package/src/exocom/scope.ts +42 -0
- package/src/exocom/status.ts +86 -0
- package/src/exocom/untrusted.ts +40 -0
- package/src/exocom/wait.ts +23 -0
- package/src/exocom/write-guard.ts +89 -0
- package/src/extension/broker-host.ts +181 -0
- package/src/extension/clock.ts +14 -0
- package/src/extension/engine.ts +321 -0
- package/src/extension/hooks.ts +796 -0
- package/src/extension/identity.ts +244 -0
- package/src/extension/shared.ts +175 -0
- package/src/extension.ts +2221 -0
- package/src/loader.ts +218 -0
- package/src/monitor/process.ts +116 -0
- package/src/monitor/session.ts +61 -0
- package/src/orchestration/flow-journal.ts +86 -0
- package/src/orchestration/flow-run.ts +301 -0
- package/src/orchestration/flow.ts +125 -0
- package/src/orchestration/judge.ts +78 -0
- package/src/orchestration/model-retry.ts +85 -0
- package/src/orchestration/parallel.ts +107 -0
- package/src/orchestration/params.ts +23 -0
- package/src/orchestration/reducers.ts +123 -0
- package/src/orchestration/render.ts +127 -0
- package/src/orchestration/roster.ts +133 -0
- package/src/orchestration/sdk.ts +326 -0
- package/src/orchestration/strategies/compete.ts +182 -0
- package/src/orchestration/strategies/council-rounds.ts +124 -0
- package/src/orchestration/strategies/critic-loop.ts +168 -0
- package/src/orchestration/strategies/debate.ts +126 -0
- package/src/orchestration/strategies/fanout.ts +20 -0
- package/src/orchestration/strategies/judge.ts +117 -0
- package/src/orchestration/strategies/magi.ts +239 -0
- package/src/orchestration/strategies/map.ts +327 -0
- package/src/orchestration/strategies/pair.ts +72 -0
- package/src/orchestration/strategies/pipeline.ts +91 -0
- package/src/orchestration/strategies/synthesize.ts +99 -0
- package/src/orchestration/strategy.ts +49 -0
- package/src/orchestration/types.ts +31 -0
- package/src/orchestration/voting.ts +126 -0
- package/src/persona/config-store.ts +215 -0
- package/src/persona/controller.ts +173 -0
- package/src/persona/gating.ts +82 -0
- package/src/persona/orchestrate.ts +83 -0
- package/src/persona/persona.ts +294 -0
- package/src/persona/spine.ts +221 -0
- package/src/persona/state.ts +28 -0
- package/src/telemetry/contract.ts +244 -0
- package/src/telemetry/paths.ts +14 -0
- package/src/telemetry/producer.ts +390 -0
- package/src/tools/council.ts +200 -0
- package/src/tools/delegate-tool.ts +637 -0
- package/src/tools/delegate.ts +661 -0
- package/src/tools/exocom-work.ts +328 -0
- package/src/tools/exocom.ts +476 -0
- package/src/tools/flow.ts +117 -0
- package/src/tools/intercom-tool.ts +292 -0
- package/src/tools/intercom.ts +181 -0
- package/src/tools/models.ts +39 -0
- package/src/tools/monitor.ts +66 -0
- package/src/tools/timer.ts +105 -0
- package/src/ui/agent-overlay.ts +526 -0
- package/src/ui/agent-tree.ts +307 -0
- package/src/ui/live-clock.ts +42 -0
- package/src/ui/model-picker.ts +160 -0
- package/src/ui/presentation.ts +170 -0
- package/src/ui/usage.ts +133 -0
- package/teams.yaml +27 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine adapter — bridges the Strategy SDK's `StrategyEngine` seam to the real
|
|
3
|
+
* ChildProcessEngine. Resolves an agent name to its config (model/tools/prompt),
|
|
4
|
+
* runs the child, and (when a contract is requested) validates the structured
|
|
5
|
+
* output, attaching it or failing the result.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
|
|
10
|
+
import type { AgentConfig } from "../agents/agent.ts";
|
|
11
|
+
import { type ContractDef, contractInstructions, parseAndValidate, pinContract, type PinnedContract } from "../core/contract.ts";
|
|
12
|
+
import { sanitizeDisplayLabel } from "../core/display-label.ts";
|
|
13
|
+
import { assignedIdentityPrompt } from "../core/naming.ts";
|
|
14
|
+
import { roleHint } from "../orchestration/roster.ts";
|
|
15
|
+
import { type AgentRunSpec, isPositiveFiniteMs, type StrategyEngine } from "../orchestration/sdk.ts";
|
|
16
|
+
import type { AgentResult } from "../orchestration/types.ts";
|
|
17
|
+
import { type ChildEngineOptions, type ChildRunSpec, runChildAgent } from "./child.ts";
|
|
18
|
+
import { looksLikeProviderError } from "./errors.ts";
|
|
19
|
+
import { nextChildHandle } from "./handles.ts";
|
|
20
|
+
import { combineSignals } from "./signals.ts";
|
|
21
|
+
import { unknownAgentFailure, unknownContractFailure } from "./spec-preflight.ts";
|
|
22
|
+
import type { ToolEvent } from "./stream.ts";
|
|
23
|
+
|
|
24
|
+
/** The engine-side face of the cross-process broker (spec B3/B4/B7) — a child-engine
|
|
25
|
+
* spawn's connection to the supervisor-hosted relay. The concrete implementation
|
|
26
|
+
* (wrapping `bus/broker/host.ts`) is built in the extension wiring; this adapter only
|
|
27
|
+
* consumes the seam. */
|
|
28
|
+
export interface EngineAdapterBroker {
|
|
29
|
+
/** The session-scoped endpoint (`PI_PERSONA_BUS`) passed to the child's env. */
|
|
30
|
+
endpoint: string;
|
|
31
|
+
/** Pre-registers the about-to-be-spawned child's handle (+ peer-roster data when
|
|
32
|
+
* `peers` is set) with the host, before the child process is spawned. */
|
|
33
|
+
register(info: { handle: string; label?: string; group?: string; peers?: boolean }): void;
|
|
34
|
+
/** Unregisters the handle once the run has settled (success, failure, or abort). */
|
|
35
|
+
unregister(handle: string): void;
|
|
36
|
+
/** Pushes a `steer` frame to the connected (or not-yet-connected) child.
|
|
37
|
+
* Returns false when the handle is unknown (not pre-registered / already forgotten). */
|
|
38
|
+
steerFrame(handle: string, text: string): boolean;
|
|
39
|
+
/** Whether `handle` currently has a live ask (`decision`/`interview`) awaiting a reply — the
|
|
40
|
+
* parent-side idle/startup watchdogs consult this before killing a silent leg (R9): a leg
|
|
41
|
+
* blocked on the supervisor is re-armed, not killed. */
|
|
42
|
+
hasPendingAskFrom(handle: string): boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EngineAdapterDeps {
|
|
46
|
+
resolveAgent: (name: string) => AgentConfig | undefined;
|
|
47
|
+
contracts?: (name: string) => ContractDef | undefined;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
/** Known agent names, for self-correcting unknown-agent errors (mirrors the model path). */
|
|
50
|
+
listAgents?: () => string[];
|
|
51
|
+
/** Installed output-contract names (built-in `default` + files) for the self-correcting hint. */
|
|
52
|
+
listContracts?: () => string[];
|
|
53
|
+
/** Per-agent model override (e.g. a persona's configured ensemble models).
|
|
54
|
+
* Precedence: explicit spec.model > modelFor(agent) > the agent's own default. */
|
|
55
|
+
modelFor?: (agent: string) => string | undefined;
|
|
56
|
+
/** Explicit thinking level appended to the child model (`model:level`) so it can't
|
|
57
|
+
* fall into a model's default "adaptive" mode, which some models reject. */
|
|
58
|
+
childThinking?: string;
|
|
59
|
+
/** The shared behavioral layer (docs/SPINE.md), prepended to every leg's prompt. This is the
|
|
60
|
+
* WORKER variant (`prompts/spine.worker.md`), not the supervisor text — a leg runs headless
|
|
61
|
+
* with no user to confirm anything with. Absent/empty (the default) ⇒ the composed prompt is
|
|
62
|
+
* byte-identical to the pre-spine one. */
|
|
63
|
+
spine?: string;
|
|
64
|
+
/** Forwarded to the child engine (e.g. a test invocation resolver). */
|
|
65
|
+
childOptions?: ChildEngineOptions;
|
|
66
|
+
cwd?: string;
|
|
67
|
+
/** Cross-process comm plane (spec B1-B7). Absent ⇒ the child spawns with no
|
|
68
|
+
* `PI_PERSONA_BUS`/`PI_PERSONA_HANDLE` env, no host, no steer frames. Present ⇒ every
|
|
69
|
+
* spawn is registered with the host and gets a live handle for `onSteerable`. */
|
|
70
|
+
broker?: EngineAdapterBroker;
|
|
71
|
+
/** Persona-level bus capability (`EffectiveCapabilities.canUseBus`). Default true.
|
|
72
|
+
* When false, a spec's `peers` request is dropped (no peer registration, no
|
|
73
|
+
* `PI_PERSONA_PEERS` env) — mirrors `inproc.ts`'s `wantsPeers` gate so the same
|
|
74
|
+
* persona-capability restriction holds on both engines (spec B7). */
|
|
75
|
+
canUseBus?: boolean;
|
|
76
|
+
/** Allow the broker-connected child's `contact_supervisor` `decision`/`interview` to
|
|
77
|
+
* BLOCK for a reply — async runs only. Sync runs hold the supervisor's turn, so
|
|
78
|
+
* blocking would deadlock: default false, mirroring `inproc.ts`'s `allowBlocking`
|
|
79
|
+
* default. Forwarded to the child as `PI_PERSONA_ALLOW_BLOCKING` (bridge.ts). */
|
|
80
|
+
allowBlocking?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
84
|
+
// Per-run contract pinning (I3): an engine instance is created per run, so the first
|
|
85
|
+
// time a contract is used we pin `name@hash` and reuse that frozen snapshot for the
|
|
86
|
+
// rest of the run — hot-reloading a contract file can't change an in-flight run.
|
|
87
|
+
const pinned = new Map<string, PinnedContract>();
|
|
88
|
+
const pinnedDef = (name: string): ContractDef | undefined => {
|
|
89
|
+
const cached = pinned.get(name);
|
|
90
|
+
if (cached) return cached.def;
|
|
91
|
+
const def = deps.contracts?.(name);
|
|
92
|
+
if (!def) return undefined;
|
|
93
|
+
const fresh = pinContract(def);
|
|
94
|
+
pinned.set(name, fresh);
|
|
95
|
+
return fresh.def;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Peer-scoping group (B7): ONE id per engine instance, mirroring `inproc.ts`'s
|
|
99
|
+
// per-instance `peerLabels` map — `buildEngine` makes a fresh engine per
|
|
100
|
+
// delegate/council/flow-phase/async-launch, so this naturally scopes the host's
|
|
101
|
+
// `list` roster to the members of ONE run.
|
|
102
|
+
const peerGroup = randomUUID();
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
async run(
|
|
106
|
+
spec: AgentRunSpec,
|
|
107
|
+
onProgress?: (p: { output: string; tokens?: number; activity?: string; toolEvent?: ToolEvent }) => void,
|
|
108
|
+
callSignal?: AbortSignal,
|
|
109
|
+
/** Called once with a steer handle when the broker is on (in-process parity). */
|
|
110
|
+
onSteerable?: (steer: (text: string) => void) => void,
|
|
111
|
+
): Promise<AgentResult> {
|
|
112
|
+
const cfg = deps.resolveAgent(spec.agent);
|
|
113
|
+
if (!cfg) return unknownAgentFailure(spec.agent, deps.listAgents?.() ?? []);
|
|
114
|
+
|
|
115
|
+
// Resolved (and pinned) up front: the SAME def both instructs the member and
|
|
116
|
+
// validates its output — an instruction/validation drift is impossible. A named
|
|
117
|
+
// contract that cannot be resolved is a hard configuration error: running the leg
|
|
118
|
+
// without its requested guard would silently turn a constrained call into prose.
|
|
119
|
+
const requestedContract = spec.outputContract?.trim();
|
|
120
|
+
const contractDef = requestedContract ? pinnedDef(requestedContract) : undefined;
|
|
121
|
+
if (requestedContract && !contractDef) return unknownContractFailure(spec.agent, requestedContract, deps.listContracts?.() ?? []);
|
|
122
|
+
const withSkills =
|
|
123
|
+
spec.skills && spec.skills.length > 0
|
|
124
|
+
? `Load these skills before starting (use the nearest affine if one is missing): ${spec.skills.join(", ")}.\n\n${spec.task}`
|
|
125
|
+
: spec.task;
|
|
126
|
+
const task = contractDef ? `${withSkills}\n\n${contractInstructions(contractDef)}` : withSkills;
|
|
127
|
+
const childSpec: ChildRunSpec = { task };
|
|
128
|
+
let model = spec.model ?? deps.modelFor?.(spec.agent) ?? cfg.model;
|
|
129
|
+
// Append an explicit thinking level (model:level) unless one is already present —
|
|
130
|
+
// a child without it defaults to "adaptive", which some models reject.
|
|
131
|
+
if (model && deps.childThinking && !model.includes(":")) model = `${model}:${deps.childThinking}`;
|
|
132
|
+
if (model) childSpec.model = model;
|
|
133
|
+
const tools = spec.tools ?? cfg.tools;
|
|
134
|
+
if (tools !== undefined) childSpec.tools = tools;
|
|
135
|
+
if (cfg.excludeTools && cfg.excludeTools.length > 0) childSpec.excludeTools = cfg.excludeTools;
|
|
136
|
+
// The shared behavioral layer (docs/SPINE.md) + the agent's own persona + any
|
|
137
|
+
// on-the-fly `role` specialisation from the spec. All three go out as
|
|
138
|
+
// `--append-system-prompt`, so a leg keeps Pi's full base prompt either way — the
|
|
139
|
+
// layer adds behavioral consistency, it does not fill a scaffolding gap.
|
|
140
|
+
const layer = cfg.spine === false ? undefined : deps.spine?.trim();
|
|
141
|
+
const personaPrompt = [layer, cfg.systemPrompt?.trim(), assignedIdentityPrompt(spec.name), spec.role?.trim()].filter(Boolean).join("\n\n");
|
|
142
|
+
if (personaPrompt) childSpec.systemPrompt = personaPrompt;
|
|
143
|
+
if (deps.cwd) childSpec.cwd = deps.cwd;
|
|
144
|
+
|
|
145
|
+
const childOptions: ChildEngineOptions = { ...deps.childOptions };
|
|
146
|
+
// NP2: a per-leg timeoutMs override raises (or shortens) just THIS leg's idle-timeout
|
|
147
|
+
// ceiling without touching the shared default other legs of the same run still see.
|
|
148
|
+
// Junk (non-finite/≤0) is ignored — the engine's existing childOptions.timeoutMs stands.
|
|
149
|
+
if (isPositiveFiniteMs(spec.timeoutMs)) childOptions.timeoutMs = spec.timeoutMs;
|
|
150
|
+
if (onProgress) {
|
|
151
|
+
childOptions.onProgress = (snap) =>
|
|
152
|
+
onProgress({ output: snap.output, tokens: snap.tokens, ...(snap.activity ? { activity: snap.activity } : {}), ...(snap.toolEvent ? { toolEvent: snap.toolEvent } : {}) });
|
|
153
|
+
}
|
|
154
|
+
// The run aborts if EITHER the whole-run signal or this agent's own (UI stop) fires.
|
|
155
|
+
const signal = combineSignals(deps.signal, callSignal);
|
|
156
|
+
|
|
157
|
+
// Cross-process comm plane (spec B1-B7): absent broker ⇒ none of this runs, so
|
|
158
|
+
// the spawn env + behaviour stay byte-identical to pre-broker pi-persona.
|
|
159
|
+
// Present ⇒ mint a handle, register it with the host BEFORE
|
|
160
|
+
// spawn, wire the child's env, and give the caller a steer function.
|
|
161
|
+
let handle: string | undefined;
|
|
162
|
+
if (deps.broker) {
|
|
163
|
+
handle = nextChildHandle(spec.agent);
|
|
164
|
+
const wantsPeers = spec.peers === true && (deps.canUseBus ?? true);
|
|
165
|
+
const label = spec.name?.trim()
|
|
166
|
+
? sanitizeDisplayLabel(spec.name, handle)
|
|
167
|
+
: spec.role ? `${handle} (${roleHint(spec.role)})` : handle;
|
|
168
|
+
deps.broker.register({ handle, label, group: peerGroup, ...(wantsPeers ? { peers: true } : {}) });
|
|
169
|
+
childOptions.env = {
|
|
170
|
+
...childOptions.env,
|
|
171
|
+
PI_PERSONA_BUS: deps.broker.endpoint,
|
|
172
|
+
PI_PERSONA_HANDLE: handle,
|
|
173
|
+
...(wantsPeers ? { PI_PERSONA_PEERS: "1" } : {}),
|
|
174
|
+
...(deps.allowBlocking ? { PI_PERSONA_ALLOW_BLOCKING: "1" } : {}),
|
|
175
|
+
};
|
|
176
|
+
// The parent-side watchdogs must not kill a leg that is genuinely waiting on the
|
|
177
|
+
// supervisor's answer (R9, inproc parity) — ask the bus, don't guess from a static flag.
|
|
178
|
+
childOptions.isBlocked = () => deps.broker!.hasPendingAskFrom(handle!);
|
|
179
|
+
const broker = deps.broker;
|
|
180
|
+
const h = handle;
|
|
181
|
+
onSteerable?.((text) => broker.steerFrame(h, text));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let child: Awaited<ReturnType<typeof runChildAgent>>;
|
|
185
|
+
try {
|
|
186
|
+
child = await runChildAgent(childSpec, signal, childOptions);
|
|
187
|
+
} finally {
|
|
188
|
+
// Unregister on EVERY settle path (success, failure, or an unexpected throw) —
|
|
189
|
+
// a leaked handle would otherwise linger in the host's registries forever.
|
|
190
|
+
if (deps.broker && handle) deps.broker.unregister(handle);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result: AgentResult = { agent: spec.agent, output: child.output, usage: child.usage, ok: child.ok };
|
|
194
|
+
// The provider/id the child ran on (drop any `:thinking` suffix) — for the UI and
|
|
195
|
+
// as the seed of the provider-fallback chain on a provider failure. The child's
|
|
196
|
+
// STREAM-REPORTED model wins over the requested spec: a resolved alias or a
|
|
197
|
+
// provider-side redirect must seed the fallback chain with what actually ran.
|
|
198
|
+
const modelUsed = (child.model ?? childSpec.model ?? spec.model ?? cfg.model)?.split(":")[0];
|
|
199
|
+
if (modelUsed) result.modelUsed = modelUsed;
|
|
200
|
+
// Diagnostic tag: agent · model ref · dynamic overrides. Same shape as inproc engine,
|
|
201
|
+
// so failed follow-ups always say WHICH agent+model died and WHY.
|
|
202
|
+
const overrides: string[] = [];
|
|
203
|
+
if (spec.model) overrides.push("model");
|
|
204
|
+
if (spec.skills && spec.skills.length > 0) overrides.push("skills");
|
|
205
|
+
if (spec.tools !== undefined) overrides.push("tools");
|
|
206
|
+
const dyn = overrides.length > 0 ? ` +dyn(${overrides.join(",")})` : "";
|
|
207
|
+
const modelRef = childSpec.model ?? "(no model)";
|
|
208
|
+
const tag = `[${spec.agent} · ${modelRef}${dyn}]`;
|
|
209
|
+
if (child.errorMessage) result.error = `${tag} ${child.errorMessage}`;
|
|
210
|
+
else if (!child.ok) result.error = `${tag} ${child.stderr.trim() || `agent failed (exit ${child.exitCode})`}`;
|
|
211
|
+
// MCP-leg diagnosis: an `mcp: true` leg is routed here (the child engine) precisely so
|
|
212
|
+
// `pi-mcp-adapter` initializes in a fresh headless `pi -p`. When such a leg fails without
|
|
213
|
+
// completing a single turn, the near-certain cause is the MCP adapter hanging in init —
|
|
214
|
+
// most often a server that needs INTERACTIVE OAuth, which a headless child can't perform.
|
|
215
|
+
// Keyed on turns===0 (never really started), not empty output, so an early tool/log line
|
|
216
|
+
// can't mask it. Name the cause + the fix so the operator never has to guess (a real prod
|
|
217
|
+
// report: a dead `mcp: true` async delegation with an opaque timeout).
|
|
218
|
+
if (!child.ok && spec.mcp === true && child.usage.turns === 0) {
|
|
219
|
+
result.error =
|
|
220
|
+
`${result.error ?? `${tag} agent failed`} — mcp:true leg never completed a turn: the MCP adapter likely hung initializing in the headless child ` +
|
|
221
|
+
`(a server needing interactive OAuth can't authenticate without a UI). Fix: authenticate the MCP server once in a normal \`pi\` session (\`/mcp auth\`) so its token is cached — the headless child reuses it; ` +
|
|
222
|
+
`or drop mcp:true if this leg doesn't need MCP tools. Tune the wait with PI_PERSONA_AGENT_STARTUP_MS (0 disables).`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Classify the failure so the fallback decorator reroutes ONLY provider errors
|
|
226
|
+
// (a stream `error`), never an abort/timeout/spawn-miss/agent failure. A child that
|
|
227
|
+
// dies BEFORE the stream starts (the provider rejected the request and `pi` exited
|
|
228
|
+
// non-zero) has no stop reason at all — the evidence sits in stderr/error text, so
|
|
229
|
+
// classify it by content or the fallback silently never reroutes pre-stream deaths.
|
|
230
|
+
if (!child.ok)
|
|
231
|
+
result.failureKind = child.timedOut
|
|
232
|
+
? "timeout"
|
|
233
|
+
: child.aborted
|
|
234
|
+
? "abort"
|
|
235
|
+
: child.stopReason === "error" || looksLikeProviderError(`${child.errorMessage ?? ""}\n${child.stderr}`)
|
|
236
|
+
? "provider"
|
|
237
|
+
: "agent";
|
|
238
|
+
|
|
239
|
+
if (contractDef) {
|
|
240
|
+
// Shared parse+validate (unwraps ```json fences / prose first) — a member's
|
|
241
|
+
// fenced output must not void its structured result.
|
|
242
|
+
const v = parseAndValidate(child.output, contractDef);
|
|
243
|
+
if (v.value) result.structured = v.value;
|
|
244
|
+
if (!v.ok) {
|
|
245
|
+
result.ok = false;
|
|
246
|
+
// Only NAME the contract miss when nothing else already killed the run: a
|
|
247
|
+
// timeout/abort/provider error is the cause of death, and "not valid JSON"
|
|
248
|
+
// over an empty output would send the operator after the wrong problem.
|
|
249
|
+
if (v.error && result.error === undefined) result.error = v.error;
|
|
250
|
+
if (result.failureKind === undefined) result.failureKind = "contract";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return result;
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|