@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/core/timer.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `TimerScheduler` — the pure kernel behind the supervisor-armable `timer` tool.
|
|
3
3
|
*
|
|
4
|
-
* Problem it solves: a supervisor
|
|
5
|
-
* a service window, a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* a fresh turn, so the supervisor resumes on its own).
|
|
4
|
+
* Problem it solves: a supervisor needs a wake at a fixed wall-clock moment the world decides (a
|
|
5
|
+
* scheduled event, a service window, a reminder). The MCP/transport layer caps a blocking tool
|
|
6
|
+
* call at ~20-60s, so a blocking sleep is not an option. The fix is an ALARM: arm a timer that,
|
|
7
|
+
* when it expires, WAKES the session by injecting a follow-up (the extension routes the fire
|
|
8
|
+
* through the same idle-delivery notifier that surfaces async-run completions — an idle delivery
|
|
9
|
+
* always starts a fresh turn, so the supervisor resumes on its own).
|
|
11
10
|
*
|
|
12
11
|
* This module is pure (injected clock + setTimer/clearTimer + an onFire sink), so scheduling,
|
|
13
12
|
* validation, cancellation and listing are unit-tested without a live model or real wall clock.
|
|
@@ -65,6 +64,11 @@ export interface TimerSchedulerDeps {
|
|
|
65
64
|
clearTimer: (handle: unknown) => void;
|
|
66
65
|
/** Called exactly once when a timer expires (never for a cancelled one). */
|
|
67
66
|
onFire: (entry: TimerEntry) => void;
|
|
67
|
+
/** Called once per admitted alarm, right after `arm` accepts it (never for a rejected request).
|
|
68
|
+
* Every announced alarm later reaches exactly one of `onFire` / `onCancelled`. */
|
|
69
|
+
onArmed?: (entry: TimerEntry) => void;
|
|
70
|
+
/** Called once per alarm dropped by `cancel` or `cancelAll` (never for one that fired). */
|
|
71
|
+
onCancelled?: (entry: TimerEntry) => void;
|
|
68
72
|
/** Minimum accepted delay (ms). Default 1000 — sub-second alarms are pointless here. */
|
|
69
73
|
minDelayMs?: number;
|
|
70
74
|
/** Maximum accepted delay (ms). Default 24h — guards against a runaway/typo alarm. */
|
|
@@ -75,12 +79,26 @@ export interface TimerSchedulerDeps {
|
|
|
75
79
|
/** Maximum follow-up message length (chars). Default 4000 — the stored message is
|
|
76
80
|
* re-injected into context at fire time, so it is a budget, not a document. */
|
|
77
81
|
maxMessageChars?: number;
|
|
82
|
+
/** Longest accepted label (code units). Default 120 — the label lands in the tree row, `timer list`
|
|
83
|
+
* and the fire message, so it is a caption, not a document. A longer label is CLIPPED (ending in
|
|
84
|
+
* "…"), not rejected: a verbose label must never cost the alarm. */
|
|
85
|
+
maxLabelChars?: number;
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
const DEFAULT_MIN_DELAY_MS = 1_000;
|
|
81
89
|
const DEFAULT_MAX_DELAY_MS = 24 * 60 * 60 * 1_000;
|
|
82
90
|
const DEFAULT_MAX_ARMED = 32;
|
|
83
91
|
const DEFAULT_MAX_MESSAGE_CHARS = 4_000;
|
|
92
|
+
const DEFAULT_MAX_LABEL_CHARS = 120;
|
|
93
|
+
|
|
94
|
+
/** At most `max` code units, ending in "…" when clipped; never cuts an astral character in half. */
|
|
95
|
+
function clipLabel(text: string, max: number): string {
|
|
96
|
+
if (text.length <= max) return text;
|
|
97
|
+
let end = Math.max(0, max - 1);
|
|
98
|
+
const last = text.charCodeAt(end - 1);
|
|
99
|
+
if (last >= 0xd800 && last <= 0xdbff) end -= 1;
|
|
100
|
+
return `${text.slice(0, end)}…`;
|
|
101
|
+
}
|
|
84
102
|
|
|
85
103
|
interface Scheduled {
|
|
86
104
|
entry: TimerEntry;
|
|
@@ -93,6 +111,7 @@ export class TimerScheduler {
|
|
|
93
111
|
private readonly maxDelayMs: number;
|
|
94
112
|
private readonly maxArmed: number;
|
|
95
113
|
private readonly maxMessageChars: number;
|
|
114
|
+
private readonly maxLabelChars: number;
|
|
96
115
|
private readonly timers = new Map<string, Scheduled>();
|
|
97
116
|
private seq = 0;
|
|
98
117
|
|
|
@@ -102,6 +121,7 @@ export class TimerScheduler {
|
|
|
102
121
|
this.maxDelayMs = deps.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
103
122
|
this.maxArmed = deps.maxArmed ?? DEFAULT_MAX_ARMED;
|
|
104
123
|
this.maxMessageChars = deps.maxMessageChars ?? DEFAULT_MAX_MESSAGE_CHARS;
|
|
124
|
+
this.maxLabelChars = deps.maxLabelChars ?? DEFAULT_MAX_LABEL_CHARS;
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
/** Arm a new alarm. Returns the entry on success, or a reason on rejection. */
|
|
@@ -137,13 +157,14 @@ export class TimerScheduler {
|
|
|
137
157
|
const id = `timer-${++this.seq}`;
|
|
138
158
|
const entry: TimerEntry = {
|
|
139
159
|
id,
|
|
140
|
-
label: (req.label ?? "").trim() || id,
|
|
160
|
+
label: clipLabel((req.label ?? "").trim(), this.maxLabelChars) || id,
|
|
141
161
|
fireAtEpochMs: fireAt,
|
|
142
162
|
armedAtEpochMs: now,
|
|
143
163
|
message,
|
|
144
164
|
};
|
|
145
165
|
const handle = this.deps.setTimer(() => this.fire(id), delay);
|
|
146
166
|
this.timers.set(id, { entry, handle });
|
|
167
|
+
this.deps.onArmed?.(entry);
|
|
147
168
|
return { ok: true, entry, remainingMs: delay };
|
|
148
169
|
}
|
|
149
170
|
|
|
@@ -153,13 +174,17 @@ export class TimerScheduler {
|
|
|
153
174
|
if (!scheduled) return false;
|
|
154
175
|
this.deps.clearTimer(scheduled.handle);
|
|
155
176
|
this.timers.delete(id);
|
|
177
|
+
this.deps.onCancelled?.(scheduled.entry);
|
|
156
178
|
return true;
|
|
157
179
|
}
|
|
158
180
|
|
|
159
181
|
/** Cancel every armed alarm (reload/dispose hygiene — never leak a timer across sessions). */
|
|
160
182
|
cancelAll(): void {
|
|
161
|
-
|
|
183
|
+
const dropped = [...this.timers.values()];
|
|
184
|
+
for (const scheduled of dropped) this.deps.clearTimer(scheduled.handle);
|
|
162
185
|
this.timers.clear();
|
|
186
|
+
// Announced after the map is empty, so a listener that re-enters (list/cancel) sees the truth.
|
|
187
|
+
for (const scheduled of dropped) this.deps.onCancelled?.(scheduled.entry);
|
|
163
188
|
}
|
|
164
189
|
|
|
165
190
|
/** Snapshot of all armed alarms, soonest first, with live remaining time. */
|
package/src/core/types.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type { Permission };
|
|
|
14
14
|
* new level here too — `pi.getThinkingLevel()` returns the upstream union, so a missing member here
|
|
15
15
|
* is a hard compile error where the host reads it (see `PersonaHost.getThinkingLevel`). */
|
|
16
16
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
17
|
-
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
17
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
18
18
|
|
|
19
19
|
export function isThinkingLevel(x: unknown): x is ThinkingLevel {
|
|
20
20
|
return typeof x === "string" && (THINKING_LEVELS as readonly string[]).includes(x);
|
package/src/engine/adapter.ts
CHANGED
|
@@ -10,16 +10,23 @@ 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
|
-
import { type AgentRunSpec, isPositiveFiniteMs, type SteerFn, type StrategyEngine } from "../orchestration/sdk.ts";
|
|
17
|
+
import { type AgentProgress, type AgentRunSpec, isPositiveFiniteMs, type SteerFn, type StrategyEngine } from "../orchestration/sdk.ts";
|
|
16
18
|
import type { AgentResult } from "../orchestration/types.ts";
|
|
17
19
|
import { type ChildEngineOptions, type ChildRunSpec, runChildAgent } from "./child.ts";
|
|
18
20
|
import { looksLikeProviderError } from "./errors.ts";
|
|
21
|
+
import { piHostModuleHint } from "./host-module.ts";
|
|
19
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";
|
|
20
26
|
import { combineSignals } from "./signals.ts";
|
|
27
|
+
import { emptyUsage } from "./stream.ts";
|
|
21
28
|
import { toolModifierFailure, unknownAgentFailure, unknownContractFailure } from "./spec-preflight.ts";
|
|
22
|
-
import
|
|
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 {
|
|
@@ -106,17 +121,45 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
|
106
121
|
return {
|
|
107
122
|
async run(
|
|
108
123
|
spec: AgentRunSpec,
|
|
109
|
-
onProgress?: (p:
|
|
124
|
+
onProgress?: (p: AgentProgress) => void,
|
|
110
125
|
callSignal?: AbortSignal,
|
|
111
126
|
/** Called once with a steer handle when the broker is on (in-process parity). */
|
|
112
127
|
onSteerable?: (steer: SteerFn) => void,
|
|
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
|
|
@@ -153,34 +194,53 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
|
153
194
|
if (isPositiveFiniteMs(spec.timeoutMs)) childOptions.timeoutMs = spec.timeoutMs;
|
|
154
195
|
if (onProgress) {
|
|
155
196
|
childOptions.onProgress = (snap) =>
|
|
156
|
-
onProgress({ output: snap.output, tokens: snap.tokens, ...(snap.
|
|
197
|
+
onProgress({ output: snap.output, tokens: snap.tokens, ...(snap.usage ? { usage: snap.usage } : {}), ...(snap.activity ? { activity: snap.activity } : {}), ...(snap.toolEvent ? { toolEvent: snap.toolEvent } : {}) });
|
|
157
198
|
}
|
|
158
199
|
// The run aborts if EITHER the whole-run signal or this agent's own (UI stop) fires.
|
|
159
200
|
const signal = combineSignals(deps.signal, callSignal);
|
|
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.
|
|
@@ -215,6 +279,8 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
|
215
279
|
const tag = `[${spec.agent} · ${modelRef}${dyn}]`;
|
|
216
280
|
if (child.errorMessage) result.error = `${tag} ${child.errorMessage}`;
|
|
217
281
|
else if (!child.ok) result.error = `${tag} ${child.stderr.trim() || `agent failed (exit ${child.exitCode})`}`;
|
|
282
|
+
const hostModuleHint = !child.ok ? piHostModuleHint(`${child.errorMessage ?? ""}\n${child.stderr}`) : undefined;
|
|
283
|
+
if (hostModuleHint && result.error) result.error += ` — ${hostModuleHint}`;
|
|
218
284
|
// MCP-leg diagnosis: an `mcp: true` leg is routed here (the child engine) precisely so
|
|
219
285
|
// `pi-mcp-adapter` initializes in a fresh headless `pi -p`. When such a leg fails without
|
|
220
286
|
// completing a single turn, the near-certain cause is the MCP adapter hanging in init —
|
|
@@ -222,7 +288,7 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
|
222
288
|
// Keyed on turns===0 (never really started), not empty output, so an early tool/log line
|
|
223
289
|
// can't mask it. Name the cause + the fix so the operator never has to guess (a real prod
|
|
224
290
|
// report: a dead `mcp: true` async delegation with an opaque timeout).
|
|
225
|
-
if (!child.ok && spec.mcp === true && child.usage.turns === 0) {
|
|
291
|
+
if (!child.ok && !hostModuleHint && spec.mcp === true && child.usage.turns === 0) {
|
|
226
292
|
result.error =
|
|
227
293
|
`${result.error ?? `${tag} agent failed`} — mcp:true leg never completed a turn: the MCP adapter likely hung initializing in the headless child ` +
|
|
228
294
|
`(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; ` +
|
|
@@ -239,9 +305,11 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
|
|
|
239
305
|
? "timeout"
|
|
240
306
|
: child.aborted
|
|
241
307
|
? "abort"
|
|
242
|
-
:
|
|
243
|
-
? "
|
|
244
|
-
: "
|
|
308
|
+
: hostModuleHint
|
|
309
|
+
? "agent"
|
|
310
|
+
: child.stopReason === "error" || looksLikeProviderError(`${child.errorMessage ?? ""}\n${child.stderr}`)
|
|
311
|
+
? "provider"
|
|
312
|
+
: "agent";
|
|
245
313
|
|
|
246
314
|
if (contractDef) {
|
|
247
315
|
// Shared parse+validate (unwraps ```json fences / prose first) — a member's
|