@aeondave/pi-persona 1.17.3 → 1.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +44 -36
- package/CHANGELOG.md +76 -1
- package/README.md +23 -13
- package/docs/ARCHITECTURE.md +97 -37
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +33 -8
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +65 -14
- package/package.json +12 -6
- package/src/agents/agent.ts +6 -0
- package/src/bridge.ts +25 -8
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +14 -4
- package/src/core/capabilities.ts +1 -1
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/session-runs.ts +1 -1
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +83 -19
- package/src/engine/async.ts +49 -10
- package/src/engine/child.ts +10 -2
- package/src/engine/inproc.ts +87 -46
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +6 -0
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +45 -17
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +20 -21
- package/src/extension/hooks.ts +952 -938
- package/src/extension/identity.ts +41 -75
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +17 -0
- package/src/extension.ts +2963 -2873
- package/src/loader.ts +11 -3
- package/src/orchestration/flow-journal.ts +7 -1
- package/src/orchestration/flow-run.ts +122 -19
- package/src/orchestration/flow.ts +11 -0
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +44 -6
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +1 -1
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/council.ts +40 -23
- package/src/tools/delegate-tool.ts +50 -14
- package/src/tools/delegate.ts +13 -6
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom.ts +20 -3
- package/src/tools/flow.ts +24 -8
- package/src/tools/intercom-tool.ts +98 -14
- package/src/tools/models.ts +15 -0
- package/src/tools/monitor.ts +20 -1
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +23 -2
- package/src/ui/agent-overlay.ts +11 -3
- package/src/ui/agent-tree.ts +43 -1
- package/src/ui/literal-tool-result.ts +1 -1
- package/src/ui/model-picker.ts +1 -1
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/session-cards.ts +191 -14
package/src/loader.ts
CHANGED
|
@@ -120,8 +120,12 @@ export function loadDefinitions(dirs: ScopedDir[]): LoadResult {
|
|
|
120
120
|
const agents: AgentConfig[] = [];
|
|
121
121
|
for (const f of agentMerge.resolved) {
|
|
122
122
|
if (collisionNames.has(f.name)) continue;
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
try {
|
|
124
|
+
const agent = parseAgent(content.get(f.path) ?? "", f.path);
|
|
125
|
+
if (agent) agents.push(agent);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
warnings.push(`${f.path}: ${error instanceof Error ? error.message : String(error)} — skipped`);
|
|
128
|
+
}
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
return { personas, agents, shadowed: [...personaMerge.shadowed, ...agentMerge.shadowed], collisions, warnings };
|
|
@@ -212,7 +216,11 @@ export function loadTeams(files: string[]): Record<string, RosterMember[]> {
|
|
|
212
216
|
} catch {
|
|
213
217
|
continue;
|
|
214
218
|
}
|
|
215
|
-
|
|
219
|
+
try {
|
|
220
|
+
Object.assign(merged, parseTeams(content));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw new Error(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
}
|
|
216
224
|
}
|
|
217
225
|
return merged;
|
|
218
226
|
}
|
|
@@ -59,7 +59,13 @@ export function parseJournal(content: string, hash: string): Record<string, Resu
|
|
|
59
59
|
if (e.hash !== hash || typeof e.phase !== "string") continue;
|
|
60
60
|
if (e.ok) {
|
|
61
61
|
const prior = out[e.phase];
|
|
62
|
-
out[e.phase] = {
|
|
62
|
+
out[e.phase] = {
|
|
63
|
+
agent: e.phase, output: e.output ?? "", usage: emptyUsage(), ok: true,
|
|
64
|
+
...(typeof e.resultRef === "string" ? { resultRef: e.resultRef } : {}),
|
|
65
|
+
...(e.resultVersion === 1 ? { resultVersion: 1 as const } :
|
|
66
|
+
e.resultRef !== undefined || e.resultVersion !== undefined
|
|
67
|
+
? { refUnavailable: "unsupported or missing archived result journal version" } : {}),
|
|
68
|
+
};
|
|
63
69
|
// Preserve an approval from either this line or an earlier line for the same phase.
|
|
64
70
|
if (e.gateApproved || prior?.gateApproved) out[e.phase]!.gateApproved = true;
|
|
65
71
|
} else delete out[e.phase];
|
|
@@ -15,11 +15,14 @@ import type { AgentResult, FailureKind } from "./types.ts";
|
|
|
15
15
|
|
|
16
16
|
export type PhaseStatus = "running" | "done" | "failed";
|
|
17
17
|
|
|
18
|
-
/** One phase ready to run: its spec, the built task,
|
|
18
|
+
/** One phase ready to run: its spec, the built task, its upstream phases' results, and — in
|
|
19
|
+
* reference mode — the DIRECT upstream refs granted to this phase's legs (nothing transitive). */
|
|
19
20
|
export interface FlowPhaseRun {
|
|
20
21
|
phase: FlowPhase;
|
|
21
22
|
task: string;
|
|
22
23
|
upstream: Record<string, AgentResult>;
|
|
24
|
+
/** Empty in inline mode; the phase's direct needs' archived refs in reference mode. */
|
|
25
|
+
resultRefs: readonly string[];
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export type RunPhaseFn = (run: FlowPhaseRun) => Promise<AgentResult>;
|
|
@@ -32,10 +35,23 @@ export interface FlowJournalEntry {
|
|
|
32
35
|
output: string;
|
|
33
36
|
/** A gated phase whose checkpoint was approved — so a resume doesn't re-prompt. */
|
|
34
37
|
gateApproved?: boolean;
|
|
38
|
+
/** Reference handoff: the archived `result:<UUID>` this phase's output lives under — the
|
|
39
|
+
* body itself is NOT journaled in that mode (resume reads it back via the archive). */
|
|
40
|
+
resultRef?: string;
|
|
41
|
+
/** The archive-record schema version the `resultRef` line was written against. */
|
|
42
|
+
resultVersion?: 1;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/** A resume map value: a phase result plus whether its checkpoint gate was already approved. */
|
|
38
|
-
export type ResumedResult = AgentResult & {
|
|
46
|
+
export type ResumedResult = AgentResult & {
|
|
47
|
+
gateApproved?: boolean;
|
|
48
|
+
resultVersion?: 1;
|
|
49
|
+
/** Reference handoff: the archived ref this phase was journaled under (from the journal). */
|
|
50
|
+
resultRef?: string;
|
|
51
|
+
/** The archived body for a phase's ref could not be read back (resume, or a live capture
|
|
52
|
+
* that produced no ref) — dependents block on this need; nothing re-runs or falls back. */
|
|
53
|
+
refUnavailable?: string;
|
|
54
|
+
};
|
|
39
55
|
|
|
40
56
|
export interface FlowRunDeps {
|
|
41
57
|
/** Run one phase's strategy → its result (wraps `runPersonaStrategy` in prod). */
|
|
@@ -43,6 +59,10 @@ export interface FlowRunDeps {
|
|
|
43
59
|
/** The pinned run identity (I3: `flowRunHash` = flow + task + cwd) — recorded in every journal line. */
|
|
44
60
|
hash: string;
|
|
45
61
|
signal?: AbortSignal;
|
|
62
|
+
/** Reference handoff: read a journaled ref's stored body back from the terminal archive —
|
|
63
|
+
* how a RESUME rebuilds a done phase's output without re-running it. Required for a
|
|
64
|
+
* reference-mode resume that has refs to restore; absent ⇒ every ref reads unavailable. */
|
|
65
|
+
readArchivedResult?: (ref: string) => { ok: true; body: string; status: "done" | "failed" | "stopped"; truncated?: boolean } | { ok: false; error: string };
|
|
46
66
|
/** Grace an aborted wave gets to unwind cooperatively before its still-running phases are
|
|
47
67
|
* force-settled (defaults to {@link ABORT_GRACE_MS}); shrinkable in tests. */
|
|
48
68
|
abortGraceMs?: number;
|
|
@@ -123,37 +143,94 @@ function within(p: Promise<unknown>, ms: number): Promise<void> {
|
|
|
123
143
|
});
|
|
124
144
|
}
|
|
125
145
|
|
|
126
|
-
/**
|
|
146
|
+
/** Reference handoff: how much of an upstream body is embedded as a PREVIEW (contract 2).
|
|
147
|
+
* The full report stays in the archive; the dependent pages it with `read_result`. */
|
|
148
|
+
export const RESULT_PREVIEW_CHARS = 1200;
|
|
149
|
+
|
|
150
|
+
/** Build a phase's task. Inline (the default, first-class): the base task plus each upstream
|
|
151
|
+
* phase's FULL fenced output. Reference: per upstream phase ONLY a bounded fenced preview +
|
|
152
|
+
* the archived ref + its status + how to read more — never the full body. */
|
|
127
153
|
function buildPhaseTask(baseTask: string, spec: FlowSpec, phase: FlowPhase, upstream: Record<string, AgentResult>): string {
|
|
128
154
|
const parts = [`Flow "${spec.name}" — phase "${phase.id}" (strategy: ${phase.strategy}).`, baseTask];
|
|
129
155
|
const ups = Object.entries(upstream);
|
|
130
|
-
if (ups.length
|
|
131
|
-
|
|
132
|
-
|
|
156
|
+
if (ups.length === 0) return parts.join("\n");
|
|
157
|
+
if (spec.handoff === "reference") {
|
|
158
|
+
parts.push("\nUpstream results (archived references — read more with the read_result tool):");
|
|
159
|
+
for (const [id, r] of ups) {
|
|
160
|
+
const ref = r.resultReceipt?.ref;
|
|
161
|
+
parts.push(`\n--- ${id} ---`);
|
|
162
|
+
parts.push(`status: ${r.ok ? "done" : "failed"}`);
|
|
163
|
+
if (ref === undefined) {
|
|
164
|
+
parts.push(`result reference unavailable: ${r.output.slice(0, 200) || "(no output)"}`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
parts.push(`result ref: ${ref}`);
|
|
168
|
+
if (r.resultReceipt?.truncated) parts.push("Archive retention truncated the original report; the stored body is partial even after reading every page.");
|
|
169
|
+
parts.push(`preview (first ${RESULT_PREVIEW_CHARS} of ${r.output.length} chars):\n${fenceUntrusted(r.output.slice(0, RESULT_PREVIEW_CHARS))}${r.output.length > RESULT_PREVIEW_CHARS ? "\n(preview truncated — use read_result for the rest)" : ""}`);
|
|
170
|
+
parts.push(`Read the full report with the read_result tool, e.g. {"ref":"${ref}","offset":0,"limit":8000}; follow each reply's nextOffset until endOfStoredBody.`);
|
|
171
|
+
}
|
|
172
|
+
return parts.join("\n");
|
|
133
173
|
}
|
|
174
|
+
parts.push("\nUpstream results to build on:");
|
|
175
|
+
for (const [id, r] of ups) parts.push(`\n--- ${id} ---\n${fenceUntrusted(r.output)}`);
|
|
134
176
|
return parts.join("\n");
|
|
135
177
|
}
|
|
136
178
|
|
|
137
179
|
export async function runFlow(spec: FlowSpec, baseTask: string, deps: FlowRunDeps): Promise<FlowOutcome> {
|
|
138
180
|
const byId = new Map(spec.phases.map((p) => [p.id, p]));
|
|
181
|
+
const reference = spec.handoff === "reference";
|
|
182
|
+
/** One journal write, mode-aware: reference lines carry the ref (+version marker) and never
|
|
183
|
+
* the body; inline lines stay byte-identical to the historical format. */
|
|
184
|
+
const journalSettled = (id: string, r: AgentResult, gateApproved = false): void => {
|
|
185
|
+
if (!deps.journal) return;
|
|
186
|
+
const ref = r.resultReceipt?.ref;
|
|
187
|
+
deps.journal(reference
|
|
188
|
+
? { phase: id, hash: deps.hash, ok: r.ok, output: "", ...(r.ok && ref !== undefined ? { resultRef: ref, resultVersion: 1 as const } : {}), ...(gateApproved ? { gateApproved: true } : {}) }
|
|
189
|
+
: { phase: id, hash: deps.hash, ok: r.ok, output: r.output, ...(gateApproved ? { gateApproved: true } : {}) });
|
|
190
|
+
};
|
|
191
|
+
/** Reference resume: rebuild a done phase's output from the terminal archive. A ref that
|
|
192
|
+
* cannot be read back marks the phase `refUnavailable` — its dependents BLOCK (no respawn,
|
|
193
|
+
* no inline fallback, no re-run of the done phase); the error says why. */
|
|
194
|
+
const restoreResumed = (r: ResumedResult): ResumedResult => {
|
|
195
|
+
if (r.refUnavailable) return { ...r, output: "" };
|
|
196
|
+
const ref = r.resultRef;
|
|
197
|
+
if (ref === undefined) {
|
|
198
|
+
return { ...r, output: "", refUnavailable: "the phase was journaled without an archived result reference" };
|
|
199
|
+
}
|
|
200
|
+
if (r.resultVersion !== 1) return { ...r, output: "", refUnavailable: "unsupported or missing archived result journal version" };
|
|
201
|
+
const read = deps.readArchivedResult?.(ref);
|
|
202
|
+
if (!read) return { ...r, output: "", refUnavailable: `archived result ${ref} is unreadable (no archive reader is wired for this run)` };
|
|
203
|
+
if (!read.ok) return { ...r, output: "", refUnavailable: `archived result ${ref} is unreadable (${read.error})` };
|
|
204
|
+
if (read.status !== "done") return { ...r, output: "", refUnavailable: `archived result ${ref} has status ${read.status}; expected done` };
|
|
205
|
+
return { ...r, output: read.body, resultReceipt: { ...(r.resultReceipt ?? {}), ref, ...(read.truncated === undefined ? {} : { truncated: read.truncated }) } };
|
|
206
|
+
};
|
|
139
207
|
// A gated phase only satisfies its dependents once its checkpoint is "passed" (or it blocks
|
|
140
208
|
// them once "rejected"). Resume pre-seeds gates the journal already recorded as approved.
|
|
141
209
|
const gateState = new Map<string, "passed" | "rejected">();
|
|
142
|
-
const done = new Map<string,
|
|
143
|
-
for (const [id,
|
|
210
|
+
const done = new Map<string, ResumedResult>();
|
|
211
|
+
for (const [id, r0] of Object.entries(deps.resume ?? {})) {
|
|
144
212
|
const gated = byId.get(id)?.gate === true;
|
|
145
213
|
// A gated phase resumed as ok but WITHOUT a recorded approval (the process died between
|
|
146
214
|
// journaling its completion and its gate decision) must RE-RUN, not be treated as done —
|
|
147
215
|
// otherwise it sits in `done` with an unresolved gate and its dependents can never run.
|
|
148
|
-
|
|
216
|
+
let r: ResumedResult = r0;
|
|
217
|
+
if (reference && r0.ok) {
|
|
218
|
+
const restored = restoreResumed(r0);
|
|
219
|
+
r = restored.resultReceipt?.ref !== undefined ? restored : { ...restored, refUnavailable: restored.refUnavailable ?? "the phase has no archived result reference" };
|
|
220
|
+
}
|
|
221
|
+
// An invalid reference cannot turn this historical checkpoint into newly paid work.
|
|
222
|
+
if (gated && r.ok && !r.gateApproved && !r.refUnavailable) continue;
|
|
149
223
|
done.set(id, r);
|
|
150
|
-
if (gated &&
|
|
224
|
+
if (gated && r0.gateApproved) gateState.set(id, "passed");
|
|
151
225
|
}
|
|
152
226
|
// A FAILED gated phase has no checkpoint to resolve — its gate must not keep dependents
|
|
153
227
|
// from being considered (needFailed then records them "blocked", exactly like a failed
|
|
154
228
|
// non-gated need; otherwise they'd never become ready and silently vanish, stuck ⏳).
|
|
155
229
|
const gateResolved = (id: string): boolean => !byId.get(id)?.gate || gateState.has(id) || done.get(id)?.ok === false;
|
|
156
230
|
const needFailed = (n: string): boolean => (done.has(n) && !done.get(n)?.ok) || gateState.get(n) === "rejected";
|
|
231
|
+
/** An OK need whose archived reference cannot serve dependents (unreadable at resume, or a
|
|
232
|
+
* live capture that produced no ref). The need stays DONE — only its dependents block. */
|
|
233
|
+
const needUnavailable = (n: string): boolean => (done.get(n)?.refUnavailable ?? "") !== "";
|
|
157
234
|
let remaining = spec.phases.filter((p) => !done.has(p.id));
|
|
158
235
|
let cancelled = false;
|
|
159
236
|
// Keep blocked dependents distinct from phases that actually ran and failed. The flow's
|
|
@@ -170,12 +247,18 @@ export async function runFlow(spec: FlowSpec, baseTask: string, deps: FlowRunDep
|
|
|
170
247
|
const ready = remaining.filter((p) => (p.needs ?? []).every((n) => done.has(n) && gateResolved(n)));
|
|
171
248
|
if (ready.length === 0) break; // acyclic + progress guarantees this can't happen
|
|
172
249
|
|
|
173
|
-
// A phase whose any need FAILED
|
|
174
|
-
|
|
250
|
+
// A phase whose any need FAILED, whose gate was REJECTED, or (reference mode) whose need's
|
|
251
|
+
// archived ref cannot be read — record, don't run.
|
|
252
|
+
const blocked = ready.filter((p) => (p.needs ?? []).some((n) => needFailed(n) || needUnavailable(n)));
|
|
175
253
|
for (const p of blocked) {
|
|
176
254
|
blockedPhaseIds.add(p.id);
|
|
177
255
|
const gateReject = (p.needs ?? []).some((n) => gateState.get(n) === "rejected");
|
|
178
|
-
const
|
|
256
|
+
const unavailableNeed = (p.needs ?? []).find((n) => needUnavailable(n));
|
|
257
|
+
const error = gateReject
|
|
258
|
+
? "blocked: a checkpoint gate was not approved"
|
|
259
|
+
: unavailableNeed !== undefined
|
|
260
|
+
? `blocked: upstream phase "${unavailableNeed}" result reference is unavailable (${done.get(unavailableNeed)?.refUnavailable})`
|
|
261
|
+
: "blocked: an upstream phase failed";
|
|
179
262
|
const r: AgentResult = { agent: p.id, output: "", usage: emptyUsage(), ok: false, error };
|
|
180
263
|
done.set(p.id, r);
|
|
181
264
|
deps.onPhase?.(p.id, "failed", r);
|
|
@@ -196,28 +279,38 @@ export async function runFlow(spec: FlowSpec, baseTask: string, deps: FlowRunDep
|
|
|
196
279
|
const u = done.get(n);
|
|
197
280
|
if (u) upstream[n] = u;
|
|
198
281
|
}
|
|
282
|
+
// Reference mode: the phase's legs are granted EXACTLY its direct needs' refs —
|
|
283
|
+
// never a transitive ancestor's (the reader scope mirrors the task's previews).
|
|
284
|
+
const resultRefs: readonly string[] = reference
|
|
285
|
+
? (p.needs ?? []).map((n) => done.get(n)?.resultReceipt?.ref).filter((x): x is string => x !== undefined)
|
|
286
|
+
: [];
|
|
199
287
|
const task = buildPhaseTask(baseTask, spec, p, upstream);
|
|
200
288
|
// A throwing phase (e.g. a misconfigured strategy) is a phase FAILURE, not a
|
|
201
289
|
// crashed DAG — record it failed so its dependents block and the flow finishes.
|
|
202
|
-
let r:
|
|
290
|
+
let r: ResumedResult;
|
|
203
291
|
try {
|
|
204
|
-
r = await deps.runPhase({ phase: p, task, upstream });
|
|
292
|
+
r = await deps.runPhase({ phase: p, task, upstream, resultRefs });
|
|
205
293
|
} catch (err) {
|
|
206
294
|
const error = err instanceof Error ? err.message : String(err);
|
|
207
295
|
r = { agent: p.id, output: "", usage: emptyUsage(), ok: false, error };
|
|
208
296
|
}
|
|
297
|
+
// Reference mode is a durability contract: a phase that finished without an archived
|
|
298
|
+
// ref cannot serve its dependents. It stays done — dependents BLOCK on it below.
|
|
299
|
+
if (reference && r.ok && r.resultReceipt?.ref === undefined) {
|
|
300
|
+
r = { ...r, refUnavailable: "the phase completed without an archived result reference" };
|
|
301
|
+
}
|
|
209
302
|
// The run already gave up on this phase and moved on. Its UI node is torn down and
|
|
210
303
|
// its outcome is already reported, so re-drive neither — but work that DID complete
|
|
211
304
|
// is still worth keeping: journal it so a RESUME reuses the output instead of paying
|
|
212
305
|
// for the same phase a second time. A late FAILURE is not journaled — there is
|
|
213
306
|
// nothing to reuse, and a resume has to re-run it anyway.
|
|
214
307
|
if (forced) {
|
|
215
|
-
if (r.ok)
|
|
308
|
+
if (r.ok) journalSettled(p.id, r);
|
|
216
309
|
return;
|
|
217
310
|
}
|
|
218
311
|
settled.set(p.id, r);
|
|
219
312
|
deps.onPhase?.(p.id, r.ok ? "done" : "failed", r);
|
|
220
|
-
|
|
313
|
+
journalSettled(p.id, r);
|
|
221
314
|
}),
|
|
222
315
|
);
|
|
223
316
|
|
|
@@ -257,16 +350,26 @@ export async function runFlow(spec: FlowSpec, baseTask: string, deps: FlowRunDep
|
|
|
257
350
|
if (!phase?.gate || !r.ok || gateState.has(id)) continue;
|
|
258
351
|
const approved = deps.approveGate ? await deps.approveGate(phase, r) : true;
|
|
259
352
|
gateState.set(id, approved ? "passed" : "rejected");
|
|
260
|
-
if (approved)
|
|
353
|
+
if (approved) journalSettled(id, r, true);
|
|
261
354
|
}
|
|
262
355
|
remaining = remaining.filter((p) => !done.has(p.id));
|
|
263
356
|
}
|
|
264
357
|
|
|
358
|
+
// Missing reference evidence is a failed resume, even when the saved phase is a sink
|
|
359
|
+
// with no dependent to expose the problem. Retain it in done to prevent any respawn.
|
|
360
|
+
for (const [id, result] of done) {
|
|
361
|
+
if (result.refUnavailable) done.set(id, {
|
|
362
|
+
...result, ok: false, error: `Archived result reference is unavailable: ${result.refUnavailable}`, failureKind: "contract",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
265
365
|
const results: Record<string, AgentResult> = Object.fromEntries(done);
|
|
266
366
|
const needed = new Set(spec.phases.flatMap((p) => p.needs ?? []));
|
|
267
367
|
const terminals = spec.phases.filter((p) => !needed.has(p.id));
|
|
268
368
|
const terminalOutput = terminals
|
|
269
|
-
.map((p) =>
|
|
369
|
+
.map((p) => {
|
|
370
|
+
const result = done.get(p.id);
|
|
371
|
+
return `${result?.output ?? ""}${result?.resultReceipt?.truncated ? "\n[pi-persona] Archive retention truncated this report; this is the partial stored body." : ""}`;
|
|
372
|
+
})
|
|
270
373
|
.filter((s) => s.trim())
|
|
271
374
|
.join("\n\n---\n\n");
|
|
272
375
|
const ok = spec.phases.every((p) => done.get(p.id)?.ok ?? false);
|
|
@@ -22,6 +22,11 @@ export interface FlowPhase {
|
|
|
22
22
|
export interface FlowSpec {
|
|
23
23
|
name: string;
|
|
24
24
|
description?: string;
|
|
25
|
+
/** How a dependent phase receives its upstream phases' output. `inline` (the default,
|
|
26
|
+
* and still first-class) embeds each upstream result's full fenced body in the dependent's
|
|
27
|
+
* task; `reference` embeds only a bounded preview plus the archived result reference — the
|
|
28
|
+
* dependent reads the rest through the scoped `read_result` tool (see flow-run.ts). */
|
|
29
|
+
handoff?: "inline" | "reference";
|
|
25
30
|
phases: FlowPhase[];
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -111,6 +116,12 @@ export function parseFlow(content: string): FlowParse {
|
|
|
111
116
|
|
|
112
117
|
const flow: FlowSpec = { name: raw.name, phases };
|
|
113
118
|
if (typeof raw.description === "string") flow.description = raw.description;
|
|
119
|
+
if (raw.handoff !== undefined) {
|
|
120
|
+
if (raw.handoff !== "inline" && raw.handoff !== "reference") {
|
|
121
|
+
return { ok: false, error: `flow.handoff must be "inline" or "reference" (got ${JSON.stringify(raw.handoff)})` };
|
|
122
|
+
}
|
|
123
|
+
flow.handoff = raw.handoff;
|
|
124
|
+
}
|
|
114
125
|
return { ok: true, flow };
|
|
115
126
|
}
|
|
116
127
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Referenced by a persona's `orchestration.roster` or `council.roster`.
|
|
4
4
|
*
|
|
5
5
|
* A member is either a bare agent name (`- scout`) OR a map that specialises ONE
|
|
6
|
-
* agent inline — `{ agent, role?, model?, skills? }` — so an ensemble of distinct
|
|
6
|
+
* agent inline — `{ agent, role?, model?, thinking?, skills? }` — so an ensemble of distinct
|
|
7
7
|
* perspectives can be built from a SINGLE agent (e.g. one `reviewer` run three times
|
|
8
8
|
* with different `role`s and models) instead of one .md file per perspective. That is
|
|
9
9
|
* the "skills + role provide specialisation, not files" principle applied to rosters.
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { asBoolean, asStringArray, parseYamlSubset } from "../core/frontmatter.ts";
|
|
17
|
+
import { stripTerminalControls } from "../core/fence.ts";
|
|
18
|
+
import { parseLegThinking } from "../core/leg-thinking.ts";
|
|
19
|
+
import { isThinkingLevel, THINKING_LEVELS, type ThinkingLevel } from "../core/types.ts";
|
|
17
20
|
import type { Roster } from "./sdk.ts";
|
|
18
21
|
|
|
19
22
|
/** A roster member: a bare agent name, or an inline specialisation of one agent. `tools`/
|
|
@@ -21,13 +24,14 @@ import type { Roster } from "./sdk.ts";
|
|
|
21
24
|
* knobs (see `tools/delegate.ts`'s `DelegateTask`). */
|
|
22
25
|
export type RosterMember =
|
|
23
26
|
| string
|
|
24
|
-
|
|
|
27
|
+
| RosterSpec;
|
|
25
28
|
|
|
26
29
|
/** The normalised run-spec fields a member contributes (agent + any specialisation). */
|
|
27
30
|
export interface RosterSpec {
|
|
28
31
|
agent: string;
|
|
29
32
|
role?: string;
|
|
30
33
|
model?: string;
|
|
34
|
+
thinking?: ThinkingLevel;
|
|
31
35
|
skills?: string[];
|
|
32
36
|
tools?: string[];
|
|
33
37
|
isolation?: "none" | "worktree";
|
|
@@ -43,6 +47,8 @@ export function rosterSpec(member: RosterMember): RosterSpec {
|
|
|
43
47
|
const spec: RosterSpec = { agent: member.agent };
|
|
44
48
|
if (member.role?.trim()) spec.role = member.role.trim();
|
|
45
49
|
if (member.model?.trim()) spec.model = member.model.trim();
|
|
50
|
+
const thinking = parseLegThinking(member.thinking);
|
|
51
|
+
if (thinking !== undefined) spec.thinking = thinking;
|
|
46
52
|
if (member.skills && member.skills.length > 0) spec.skills = member.skills;
|
|
47
53
|
if (member.tools !== undefined) spec.tools = member.tools;
|
|
48
54
|
if (member.isolation !== undefined) spec.isolation = member.isolation;
|
|
@@ -57,19 +63,56 @@ const ROLE_STOPWORDS = new Set([
|
|
|
57
63
|
"ON", "IN", "OF", "TO", "IS", "IT", "BE", "DO", "AS", "AT", "BY", "AN", "A",
|
|
58
64
|
]);
|
|
59
65
|
|
|
66
|
+
const GIT_REF = /^(?:HEAD|ORIG_HEAD|FETCH_HEAD|MERGE_HEAD|CHERRY_PICK_HEAD|REBASE_HEAD)(?:[~^].*)?$/i;
|
|
67
|
+
const HASH = /^(?:0x)?[\da-f]{7,40}$/i;
|
|
68
|
+
|
|
69
|
+
function technicalRoleToken(token: string): boolean {
|
|
70
|
+
const trimmed = token.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
|
|
71
|
+
return token.includes("_")
|
|
72
|
+
|| token.includes("/")
|
|
73
|
+
|| token.includes("\\")
|
|
74
|
+
|| token.includes("$")
|
|
75
|
+
|| token.includes("=")
|
|
76
|
+
|| (trimmed.includes(":") && !trimmed.endsWith(":"))
|
|
77
|
+
|| GIT_REF.test(trimmed)
|
|
78
|
+
|| HASH.test(trimmed)
|
|
79
|
+
|| /^[\p{L}\p{N}_.-]+\.[\p{L}\p{N}]{1,8}$/u.test(trimmed);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function roleWords(role: string): string[] {
|
|
83
|
+
// Inline/fenced code is not a role label. An unmatched opening fence also hides the
|
|
84
|
+
// remainder instead of leaking its contents into the fallback label.
|
|
85
|
+
const withoutCode = role.replace(/(`{1,3})[\s\S]*?\1/g, " ").replace(/`{1,3}[\s\S]*$/g, " ");
|
|
86
|
+
return withoutCode.split(/\s+/).flatMap((raw) => {
|
|
87
|
+
const word = raw.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
|
|
88
|
+
return word && !technicalRoleToken(raw) && !technicalRoleToken(word) ? [word] : [];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function boundedRoleHint(text: string): string {
|
|
93
|
+
const points = Array.from(text);
|
|
94
|
+
return points.length > 20 ? `${points.slice(0, 19).join("")}…` : text;
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
/** A short emphasis hint from a member's role text — the first salient ALL-CAPS keyword
|
|
61
|
-
* (e.g. "SECURITY" from "Focus ONLY on the SECURITY lens…"), else
|
|
62
|
-
*
|
|
98
|
+
* (e.g. "SECURITY" from "Focus ONLY on the SECURITY lens…"), else a short human-readable
|
|
99
|
+
* prefix. Technical identifiers, code, paths, hashes and generic Git refs are omitted; a
|
|
100
|
+
* role with no readable words gets the neutral "role" hint. Used by presentation and
|
|
101
|
+
* run-unique UI keys, with roster seeding and SDK keys sharing this exact function. Pure. */
|
|
63
102
|
export function roleHint(role: string): string {
|
|
64
|
-
|
|
103
|
+
// Roles become picker/tree chrome. Strip terminal sequences before token parsing so their
|
|
104
|
+
// numeric parameters cannot be mistaken for visible prose, then remove bidi formatting
|
|
105
|
+
// controls that could visually reorder or disguise the resulting label.
|
|
106
|
+
const readable = stripTerminalControls(role).replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u206f]/gu, "");
|
|
107
|
+
const words = roleWords(readable);
|
|
65
108
|
for (const w of words) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return clean;
|
|
109
|
+
if (/^\p{L}{3,}$/u.test(w) && w === w.toUpperCase() && !ROLE_STOPWORDS.has(w)) {
|
|
110
|
+
return boundedRoleHint(w);
|
|
69
111
|
}
|
|
70
112
|
}
|
|
71
|
-
const brief = words.slice(0, 4)
|
|
72
|
-
|
|
113
|
+
const brief = words.slice(0, 4);
|
|
114
|
+
while (brief.length && ROLE_STOPWORDS.has(brief[brief.length - 1]!.toUpperCase())) brief.pop();
|
|
115
|
+
return boundedRoleHint(brief.join(" ")) || "role";
|
|
73
116
|
}
|
|
74
117
|
|
|
75
118
|
/** The base UI label for a member: `agent`, or `agent · HINT` when it carries a role —
|
|
@@ -99,6 +142,7 @@ export function rosterNodeKeys(members: RosterMember[]): string[] {
|
|
|
99
142
|
* LENIENT, and deliberately so: this is the STATIC `teams.yaml` path (authored, trusted project
|
|
100
143
|
* data), where a member with a badly-typed field is normalised, not rejected — a bad `model`
|
|
101
144
|
* is dropped and a numeric `tools` entry is coerced to its string, so the panel still runs.
|
|
145
|
+
* Explicit `thinking` is always strict: a typo must never inherit a different effort.
|
|
102
146
|
* Anything RUNTIME-supplied (a `council` call's `members`, an aux actor param) must go through
|
|
103
147
|
* {@link parseRuntimeRosterMember} instead: there, a dropped `tools` silently WIDENS a leg to
|
|
104
148
|
* the agent's default tool permissions, so a malformed field has to be refused, not normalised.
|
|
@@ -108,11 +152,13 @@ export function parseRosterMember(raw: unknown): RosterMember | undefined {
|
|
|
108
152
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
109
153
|
const o = raw as Record<string, unknown>;
|
|
110
154
|
if (typeof o.agent !== "string" || !o.agent.trim()) return undefined;
|
|
111
|
-
const m:
|
|
155
|
+
const m: RosterSpec = {
|
|
112
156
|
agent: o.agent.trim(),
|
|
113
157
|
};
|
|
114
158
|
if (typeof o.role === "string" && o.role.trim()) m.role = o.role.trim();
|
|
115
159
|
if (typeof o.model === "string" && o.model.trim()) m.model = o.model.trim();
|
|
160
|
+
const thinking = parseLegThinking(o.thinking);
|
|
161
|
+
if (thinking !== undefined) m.thinking = thinking;
|
|
116
162
|
const skills = asStringArray(o.skills);
|
|
117
163
|
if (skills) m.skills = skills;
|
|
118
164
|
const tools = asStringArray(o.tools);
|
|
@@ -126,7 +172,7 @@ export function parseRosterMember(raw: unknown): RosterMember | undefined {
|
|
|
126
172
|
}
|
|
127
173
|
|
|
128
174
|
/** Every field a member map may carry — anything else is a typo, not a silent no-op. */
|
|
129
|
-
const MEMBER_FIELDS = new Set(["agent", "role", "model", "skills", "tools", "isolation", "mcp"]);
|
|
175
|
+
const MEMBER_FIELDS = new Set(["agent", "role", "model", "thinking", "skills", "tools", "isolation", "mcp"]);
|
|
130
176
|
|
|
131
177
|
export type RuntimeMember = { ok: true; member: RosterMember } | { ok: false; error: string };
|
|
132
178
|
|
|
@@ -155,17 +201,13 @@ export function parseRuntimeRosterMember(raw: unknown): RuntimeMember {
|
|
|
155
201
|
}
|
|
156
202
|
const unknownKeys = Object.keys(o).filter((k) => !MEMBER_FIELDS.has(k));
|
|
157
203
|
if (unknownKeys.length > 0) {
|
|
158
|
-
return { ok: false, error: `unknown member field(s) ${unknownKeys.join(", ")} — expected agent, role, model, skills, tools, isolation, mcp` };
|
|
204
|
+
return { ok: false, error: `unknown member field(s) ${unknownKeys.join(", ")} — expected agent, role, model, thinking, skills, tools, isolation, mcp` };
|
|
205
|
+
}
|
|
206
|
+
const member: RosterSpec = { agent: o.agent.trim() };
|
|
207
|
+
if (o.thinking !== undefined) {
|
|
208
|
+
if (!isThinkingLevel(o.thinking)) return { ok: false, error: `"thinking" must be one of: ${THINKING_LEVELS.join(", ")}` };
|
|
209
|
+
member.thinking = o.thinking;
|
|
159
210
|
}
|
|
160
|
-
const member: {
|
|
161
|
-
agent: string;
|
|
162
|
-
role?: string;
|
|
163
|
-
model?: string;
|
|
164
|
-
skills?: string[];
|
|
165
|
-
tools?: string[];
|
|
166
|
-
isolation?: "none" | "worktree";
|
|
167
|
-
mcp?: boolean;
|
|
168
|
-
} = { agent: o.agent.trim() };
|
|
169
211
|
// A blank string is "unset" (the lenient path's own reading); a non-string is a type error.
|
|
170
212
|
for (const key of ["role", "model"] as const) {
|
|
171
213
|
const value = o[key];
|
|
@@ -199,7 +241,13 @@ export function parseTeams(yaml: string): Record<string, RosterMember[]> {
|
|
|
199
241
|
const teams: Record<string, RosterMember[]> = {};
|
|
200
242
|
for (const [name, value] of Object.entries(raw)) {
|
|
201
243
|
const items: unknown[] = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
|
|
202
|
-
const members = items.map(
|
|
244
|
+
const members = items.map((item, index) => {
|
|
245
|
+
try {
|
|
246
|
+
return parseRosterMember(item);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
throw new Error(`team ${name}[${index}]: ${error instanceof Error ? error.message : String(error)}`);
|
|
249
|
+
}
|
|
250
|
+
}).filter((m): m is RosterMember => m !== undefined);
|
|
203
251
|
if (members.length > 0) teams[name] = members;
|
|
204
252
|
}
|
|
205
253
|
return teams;
|
package/src/orchestration/sdk.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* exact same surface as future user-authored strategy files.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
8
9
|
import type { RunLimits } from "../core/capabilities.ts";
|
|
9
10
|
import { type ChildUsage, emptyUsage, type ToolEvent } from "../engine/stream.ts";
|
|
10
11
|
import { type JudgePrep, prepareJudge } from "./judge.ts";
|
|
@@ -22,6 +23,8 @@ export interface AgentRunSpec {
|
|
|
22
23
|
name?: string;
|
|
23
24
|
task: string;
|
|
24
25
|
model?: string;
|
|
26
|
+
/** Requested per-leg effort, before Pi applies the selected model's capability clamp. */
|
|
27
|
+
thinking?: ThinkingLevel;
|
|
25
28
|
tools?: string[];
|
|
26
29
|
/** Skills the sub-agent must load first (dynamic specialisation). */
|
|
27
30
|
skills?: string[];
|
|
@@ -49,6 +52,22 @@ export interface AgentRunSpec {
|
|
|
49
52
|
peers?: boolean;
|
|
50
53
|
/** Declared ownership (from delegate `writeSet`), used only to annotate the files-changed report. */
|
|
51
54
|
writeSet?: string[];
|
|
55
|
+
/** INTERNAL (flow reference handoff): the archived `result:<UUID>` refs granted to THIS leg
|
|
56
|
+
* — exactly its phase's DIRECT upstream needs, nothing more. Not a public delegate
|
|
57
|
+
* argument: the SDK strips any strategy-supplied value (strategies cannot enlarge a
|
|
58
|
+
* grant); only the trusted engine-seam wrapper (`withResultRefs`, set by the flow wiring)
|
|
59
|
+
* or an equivalently trusted host path may set it. Both engines preflight + scope the
|
|
60
|
+
* `read_result` tool on it. */
|
|
61
|
+
resultRefs?: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Pin a leg's granted result refs at the ENGINE seam — the trusted wiring path for flow
|
|
65
|
+
* reference mode. Overrides (never unions) whatever the caller passed, so a strategy cannot
|
|
66
|
+
* smuggle extra refs past the SDK's strip. An empty array is an explicit "none". */
|
|
67
|
+
export function withResultRefs(engine: StrategyEngine, resultRefs: readonly string[]): StrategyEngine {
|
|
68
|
+
return {
|
|
69
|
+
run: (spec, onProgress, signal, onSteer) => engine.run({ ...spec, resultRefs }, onProgress, signal, onSteer),
|
|
70
|
+
};
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
/** Is `x` a genuine positive, finite `timeoutMs` override — vs. absent/junk (undefined, NaN,
|
|
@@ -183,6 +202,8 @@ export interface SDKDeps {
|
|
|
183
202
|
* runs several times in one roster) — key the UI node by it, not by `agent`, or three
|
|
184
203
|
* same-agent roster-role members collapse into one node. Falls back to `agent`. */
|
|
185
204
|
onAgentStatus?: (agent: string, status: AgentStatus, result?: AgentResult, key?: string) => void;
|
|
205
|
+
/** One final logical leg after model recovery, never intermediate attempts. Storage is non-fatal. */
|
|
206
|
+
onAgentResult?: (result: AgentResult, key: string) => void;
|
|
186
207
|
/** Per-agent streaming progress (rolling output), for live UI. See `onAgentStatus.key`. */
|
|
187
208
|
onAgentProgress?: (agent: string, progress: AgentProgress, key?: string) => void;
|
|
188
209
|
/** Called as each agent starts with a handle to abort just that agent (for UI stop). */
|
|
@@ -241,6 +262,13 @@ function abortedLeg(agent: string): AgentResult {
|
|
|
241
262
|
return { agent, output: "", usage: emptyUsage(), ok: false, error: "agent aborted", failureKind: "abort" };
|
|
242
263
|
}
|
|
243
264
|
|
|
265
|
+
/** Copy of a spec with its `resultRefs` removed — the SDK's one strip point, so a strategy's
|
|
266
|
+
* own value never reaches an engine (only `withResultRefs`-style trusted wiring sets it). */
|
|
267
|
+
function stripResultRefs(spec: AgentRunSpec): AgentRunSpec {
|
|
268
|
+
const { resultRefs: _untrusted, ...rest } = spec;
|
|
269
|
+
return rest;
|
|
270
|
+
}
|
|
271
|
+
|
|
244
272
|
export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
245
273
|
// Run-scoped enforcement of the declared limits — applied here so NO strategy can
|
|
246
274
|
// exceed them, however it calls agent() (I2: safety from runtime limits, not isolation).
|
|
@@ -269,18 +297,26 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
269
297
|
};
|
|
270
298
|
|
|
271
299
|
return {
|
|
272
|
-
agent: async (
|
|
300
|
+
agent: async (requested, options) => {
|
|
301
|
+
// Reference-handoff grants are trusted wiring, never strategy input: drop any
|
|
302
|
+
// strategy-supplied `resultRefs` here (the flow path re-pins them at the engine
|
|
303
|
+
// seam via `withResultRefs`, which runs BELOW this layer).
|
|
304
|
+
const spec = requested.resultRefs === undefined ? requested : stripResultRefs(requested);
|
|
305
|
+
const key = uiKeyFor(spec);
|
|
306
|
+
const finish = (result: AgentResult): AgentResult => {
|
|
307
|
+
try { deps.onAgentResult?.(result, key); } catch { /* storage must not discard a completed leg */ }
|
|
308
|
+
return result;
|
|
309
|
+
};
|
|
273
310
|
if (deps.canSpawn && !deps.canSpawn(spec.agent)) {
|
|
274
|
-
return {
|
|
311
|
+
return finish({
|
|
275
312
|
agent: spec.agent,
|
|
276
313
|
output: "",
|
|
277
314
|
usage: emptyUsage(),
|
|
278
315
|
ok: false,
|
|
279
316
|
error: `persona may not spawn "${spec.agent}"`,
|
|
280
317
|
failureKind: "contract",
|
|
281
|
-
};
|
|
318
|
+
});
|
|
282
319
|
}
|
|
283
|
-
const key = uiKeyFor(spec);
|
|
284
320
|
const pending = Symbol();
|
|
285
321
|
pendingAgents.add(pending);
|
|
286
322
|
let started = false;
|
|
@@ -415,11 +451,13 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
415
451
|
}
|
|
416
452
|
};
|
|
417
453
|
try {
|
|
418
|
-
return await agentSlots.with(run, deps.signal);
|
|
454
|
+
return finish(await agentSlots.with(run, deps.signal));
|
|
419
455
|
} catch (error) {
|
|
420
456
|
// A queued leg has no engine result to report. Settle it as an abort when the
|
|
421
457
|
// run signal removed its waiter; all other errors retain their terminal semantics.
|
|
422
|
-
if (!started && deps.signal?.aborted)
|
|
458
|
+
if (!started && deps.signal?.aborted) {
|
|
459
|
+
return finish(abortedLeg(spec.agent));
|
|
460
|
+
}
|
|
423
461
|
throw error;
|
|
424
462
|
} finally {
|
|
425
463
|
pendingAgents.delete(pending);
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Orchestration-layer types shared by the SDK, strategies, and reducers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
5
6
|
import type { ChildUsage } from "../engine/stream.ts";
|
|
7
|
+
import type { ResultReceipt } from "../results/retrieval.ts";
|
|
6
8
|
|
|
7
9
|
export type { ChildUsage };
|
|
8
10
|
|
|
@@ -33,11 +35,19 @@ export interface AgentResult {
|
|
|
33
35
|
output: string;
|
|
34
36
|
/** Validated structured fields (when the agent ran against a contract). */
|
|
35
37
|
structured?: Record<string, unknown>;
|
|
38
|
+
/** Terminal archive observation; memory-only is not a durable receipt. */
|
|
39
|
+
resultReceipt?: ResultReceipt;
|
|
36
40
|
usage: ChildUsage;
|
|
37
41
|
ok: boolean;
|
|
38
42
|
error?: string;
|
|
39
43
|
/** The canonical `provider/id` the run actually used (drives provider fallback + UI). */
|
|
40
44
|
modelUsed?: string;
|
|
45
|
+
/** Selected effort before Pi's model-capability clamp, when a selection is known. */
|
|
46
|
+
thinkingRequested?: ThinkingLevel;
|
|
47
|
+
/** Observed Pi effort (session readback or final assistant-message thinkingLevel).
|
|
48
|
+
* This is Pi's requested effort, NOT evidence of the provider's actual cognition.
|
|
49
|
+
* Absent when the host exposes no validated observation; never inferred from the request. */
|
|
50
|
+
thinkingEffective?: ThinkingLevel;
|
|
41
51
|
/** Why it failed (set only when `ok` is false) — drives the model-fallback decision. */
|
|
42
52
|
failureKind?: FailureKind;
|
|
43
53
|
/** Set when this result comes from a recovered retry (see {@link ModelRecovery}). Its `usage`
|