@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/orchestration/sdk.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
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
|
-
import { emptyUsage, type ToolEvent } from "../engine/stream.ts";
|
|
10
|
+
import { type ChildUsage, emptyUsage, type ToolEvent } from "../engine/stream.ts";
|
|
10
11
|
import { type JudgePrep, prepareJudge } from "./judge.ts";
|
|
11
12
|
import { planModelRecovery } from "./model-retry.ts";
|
|
12
13
|
import { mapWithConcurrency, Semaphore } from "./parallel.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,11 +52,27 @@ 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,
|
|
55
74
|
* Infinity, ≤0), which means "no override, fall back to the run's existing default"? The
|
|
56
|
-
* ONE predicate every `timeoutMs` site gates on (`specOf`, the single-
|
|
75
|
+
* ONE predicate every `timeoutMs` site gates on (`specOf`, the single-leg delegate path,
|
|
57
76
|
* and both engine adapters), so they can't drift into checking it differently. */
|
|
58
77
|
export function isPositiveFiniteMs(x: number | undefined): x is number {
|
|
59
78
|
return x !== undefined && Number.isFinite(x) && x > 0;
|
|
@@ -65,10 +84,10 @@ export interface AgentProgress {
|
|
|
65
84
|
output: string;
|
|
66
85
|
/** Tokens consumed so far (best-effort). */
|
|
67
86
|
tokens?: number;
|
|
87
|
+
/** Copied cumulative counters for display, when supplied by the engine. */
|
|
88
|
+
usage?: ChildUsage;
|
|
68
89
|
/** The tool the agent is currently running (e.g. "grep src/…"), if any. */
|
|
69
90
|
activity?: string;
|
|
70
|
-
/** Timestamp of newly streamed tool output; no tool output text crosses this seam. */
|
|
71
|
-
toolOutputAt?: number;
|
|
72
91
|
/** One authoritative runtime tool lifecycle transition; never contains tool args or output. */
|
|
73
92
|
toolEvent?: ToolEvent;
|
|
74
93
|
}
|
|
@@ -107,7 +126,8 @@ export interface StrategySDK {
|
|
|
107
126
|
agent(spec: AgentRunSpec, options?: AgentCallOptions): Promise<AgentResult>;
|
|
108
127
|
parallel<T>(thunks: Array<() => Promise<T>>, opts?: { concurrency?: number }): Promise<T[]>;
|
|
109
128
|
reduce: {
|
|
110
|
-
|
|
129
|
+
/** `labels` (aligned to `results`) name each section by lens or item instead of the agent. */
|
|
130
|
+
aggregate(results: AgentResult[], labels?: readonly string[]): AgentResult;
|
|
111
131
|
vote(candidates: AgentResult[], opts: VoteOpts): ReducerResult;
|
|
112
132
|
/** Anonymise + label candidates for an impartial judge (run `agent(judge, …)` on
|
|
113
133
|
* the returned ballot, then map the verdict via `pick`). */
|
|
@@ -146,6 +166,17 @@ export interface StrategyParam {
|
|
|
146
166
|
|
|
147
167
|
export interface Strategy {
|
|
148
168
|
name: string;
|
|
169
|
+
/** The problem class this method answers, in one line — rendered into the council tool's
|
|
170
|
+
* description from the registry, so the guidance cannot drift from the code. */
|
|
171
|
+
when?: string;
|
|
172
|
+
/** What the roster positions MEAN, where order or role matters (`[splitter, worker]`). */
|
|
173
|
+
roster?: string;
|
|
174
|
+
/** The team this method is built for. A call that switches TO this method and names no roster seats it,
|
|
175
|
+
* instead of the profile's roster (the wrong panel for it) or the `magi` fallback (read-only voters). */
|
|
176
|
+
team?: string;
|
|
177
|
+
/** Param keys the run cannot proceed without; a call that omits one is refused up front,
|
|
178
|
+
* before any run, tree row or model picker exists. Each must also be declared in `params`. */
|
|
179
|
+
required?: readonly string[];
|
|
149
180
|
/** Declared, discoverable params (name → type/default/doc). Consumed by the council tool
|
|
150
181
|
* (warn on unknown keys) and `/doctor`. A strategy with no params omits this. */
|
|
151
182
|
params?: Record<string, StrategyParam>;
|
|
@@ -171,6 +202,8 @@ export interface SDKDeps {
|
|
|
171
202
|
* runs several times in one roster) — key the UI node by it, not by `agent`, or three
|
|
172
203
|
* same-agent roster-role members collapse into one node. Falls back to `agent`. */
|
|
173
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;
|
|
174
207
|
/** Per-agent streaming progress (rolling output), for live UI. See `onAgentStatus.key`. */
|
|
175
208
|
onAgentProgress?: (agent: string, progress: AgentProgress, key?: string) => void;
|
|
176
209
|
/** Called as each agent starts with a handle to abort just that agent (for UI stop). */
|
|
@@ -229,6 +262,13 @@ function abortedLeg(agent: string): AgentResult {
|
|
|
229
262
|
return { agent, output: "", usage: emptyUsage(), ok: false, error: "agent aborted", failureKind: "abort" };
|
|
230
263
|
}
|
|
231
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
|
+
|
|
232
272
|
export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
233
273
|
// Run-scoped enforcement of the declared limits — applied here so NO strategy can
|
|
234
274
|
// exceed them, however it calls agent() (I2: safety from runtime limits, not isolation).
|
|
@@ -257,18 +297,26 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
257
297
|
};
|
|
258
298
|
|
|
259
299
|
return {
|
|
260
|
-
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
|
+
};
|
|
261
310
|
if (deps.canSpawn && !deps.canSpawn(spec.agent)) {
|
|
262
|
-
return {
|
|
311
|
+
return finish({
|
|
263
312
|
agent: spec.agent,
|
|
264
313
|
output: "",
|
|
265
314
|
usage: emptyUsage(),
|
|
266
315
|
ok: false,
|
|
267
316
|
error: `persona may not spawn "${spec.agent}"`,
|
|
268
317
|
failureKind: "contract",
|
|
269
|
-
};
|
|
318
|
+
});
|
|
270
319
|
}
|
|
271
|
-
const key = uiKeyFor(spec);
|
|
272
320
|
const pending = Symbol();
|
|
273
321
|
pendingAgents.add(pending);
|
|
274
322
|
let started = false;
|
|
@@ -403,11 +451,13 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
403
451
|
}
|
|
404
452
|
};
|
|
405
453
|
try {
|
|
406
|
-
return await agentSlots.with(run, deps.signal);
|
|
454
|
+
return finish(await agentSlots.with(run, deps.signal));
|
|
407
455
|
} catch (error) {
|
|
408
456
|
// A queued leg has no engine result to report. Settle it as an abort when the
|
|
409
457
|
// run signal removed its waiter; all other errors retain their terminal semantics.
|
|
410
|
-
if (!started && deps.signal?.aborted)
|
|
458
|
+
if (!started && deps.signal?.aborted) {
|
|
459
|
+
return finish(abortedLeg(spec.agent));
|
|
460
|
+
}
|
|
411
461
|
throw error;
|
|
412
462
|
} finally {
|
|
413
463
|
pendingAgents.delete(pending);
|
|
@@ -62,8 +62,12 @@ function clip(diff: string, max: number): string {
|
|
|
62
62
|
|
|
63
63
|
export const compete: Strategy = {
|
|
64
64
|
name: "compete",
|
|
65
|
+
when: "rival implementations in worktrees, blind-judged; returns the winning diff (clean git repo)",
|
|
66
|
+
roster: "competitors",
|
|
67
|
+
team: "build",
|
|
68
|
+
required: ["judge"],
|
|
65
69
|
params: {
|
|
66
|
-
judge: { type: "agent", doc: "
|
|
70
|
+
judge: { type: "agent", doc: "the arbiter agent, or an inline { agent, role?, model?, skills? }" },
|
|
67
71
|
ballotDiffChars: { type: "number", default: 6000, doc: "ballot-only diff clip length; the winner's diff is always full" },
|
|
68
72
|
},
|
|
69
73
|
async run(input, sdk) {
|
|
@@ -78,6 +78,8 @@ function cancelled(rounds: number, usages: AgentResult["usage"][]): AgentResult
|
|
|
78
78
|
|
|
79
79
|
export const councilRounds: Strategy = {
|
|
80
80
|
name: "council-rounds",
|
|
81
|
+
when: "hard fork needing iterated debate: re-vote each round to a bestOf majority",
|
|
82
|
+
team: "magi",
|
|
81
83
|
params: {
|
|
82
84
|
rounds: { type: "number", default: 3, doc: "max deliberation rounds" },
|
|
83
85
|
bestOf: { type: "number", doc: "default: majority of the roster" },
|
|
@@ -46,6 +46,9 @@ function cancelled(rounds: number, output: string, usages: AgentResult["usage"][
|
|
|
46
46
|
|
|
47
47
|
export const criticLoop: Strategy = {
|
|
48
48
|
name: "critic-loop",
|
|
49
|
+
when: "ONE draft + adversarial critique, revised until the critic approves",
|
|
50
|
+
roster: "[generator, critic]",
|
|
51
|
+
team: "repair",
|
|
49
52
|
params: {
|
|
50
53
|
// `default` must name an agent that ACTUALLY runs (the collector resolves the participant
|
|
51
54
|
// as: the provided override, else roster[rosterIndex], else this agent) — never a phrase
|
|
@@ -84,6 +84,8 @@ function cancelled(usages: AgentResult["usage"][]): AgentResult {
|
|
|
84
84
|
|
|
85
85
|
export const debate: Strategy = {
|
|
86
86
|
name: "debate",
|
|
87
|
+
when: "2+ members argue live with each other, then a majority vote",
|
|
88
|
+
team: "magi",
|
|
87
89
|
params: {
|
|
88
90
|
bestOf: { type: "number", doc: "default: majority of the roster" },
|
|
89
91
|
aggregate: { type: "string", default: "majority", doc: '"majority" | "unanimity"' },
|
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
* No peers: fanout is deliberately independent parallel work — use `debate` for a live exchange.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { rosterSpec } from "../roster.ts";
|
|
8
|
+
import { rosterNodeKeys, rosterSpec } from "../roster.ts";
|
|
9
9
|
import type { Strategy } from "../sdk.ts";
|
|
10
10
|
|
|
11
11
|
export const fanout: Strategy = {
|
|
12
12
|
name: "fanout",
|
|
13
|
+
when: "ONE question to a panel of distinct lenses/models, answers side by side, no vote (separate tasks → delegate)",
|
|
13
14
|
async run(input, sdk) {
|
|
14
15
|
const team = input.roster ? sdk.roster.team(input.roster) : [];
|
|
15
16
|
if (team.length === 0) throw new Error("fanout: a non-empty roster is required");
|
|
16
17
|
sdk.log(`fanout: ${team.length} agents on "${input.task.slice(0, 40)}"`);
|
|
17
18
|
const results = await sdk.parallel(team.map((m) => () => sdk.agent({ ...rosterSpec(m), task: input.task })));
|
|
18
|
-
return { ...sdk.reduce.aggregate(results), agent: "fanout" };
|
|
19
|
+
return { ...sdk.reduce.aggregate(results, rosterNodeKeys(team)), agent: "fanout" };
|
|
19
20
|
},
|
|
20
21
|
};
|
|
@@ -44,8 +44,12 @@ function cancelled(arbiter: string, display: AgentResult[], usage: AgentResult["
|
|
|
44
44
|
|
|
45
45
|
export const judge: Strategy = {
|
|
46
46
|
name: "judge",
|
|
47
|
+
when: "rival COMPLETE answers: independent panel, one impartial arbiter picks",
|
|
48
|
+
roster: "the panel",
|
|
49
|
+
team: "magi",
|
|
50
|
+
required: ["judge"],
|
|
47
51
|
params: {
|
|
48
|
-
judge: { type: "agent", doc: "
|
|
52
|
+
judge: { type: "agent", doc: "the arbiter agent, or an inline { agent, role?, model?, skills? }" },
|
|
49
53
|
contract: { type: "string", doc: "optional output contract the panel runs against" },
|
|
50
54
|
},
|
|
51
55
|
async run(input, sdk) {
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* comes from the roster's personas; bias guards live in the vote reducer.
|
|
6
6
|
*
|
|
7
7
|
* Reflection round (params.reflect, default ON): after the independent round, each
|
|
8
|
-
* core sees the
|
|
9
|
-
*
|
|
8
|
+
* core sees the PANEL's positions **anonymised** — its own among them, unmarked, because the
|
|
9
|
+
* reflecting leg is a fresh session and that list is its only memory of round 1 — and casts a
|
|
10
|
+
* FINAL vote, revising only if a genuinely new consideration moves it (holding is fine). This
|
|
10
11
|
* lets a core catch a blind spot without turning MAGI into groupthink: the positions
|
|
11
12
|
* are anonymised (no "defer to Casper" authority bias), it is exactly ONE round (not
|
|
12
|
-
* iterate-to-consensus — that is `council-rounds`), and dissent is always preserved.
|
|
13
|
+
* iterate-to-consensus — that is `council-rounds`), and dissent is always preserved. A core whose
|
|
14
|
+
* reflection leg fails keeps the valid vote it already cast (disclosed in the ruling footer).
|
|
13
15
|
* Set `reflect: false` for a pure independent poll (uncorrelated errors, cheapest).
|
|
14
16
|
*/
|
|
15
17
|
|
|
@@ -55,6 +57,8 @@ function rescuedModel(c: AgentResult | undefined): string | undefined {
|
|
|
55
57
|
|
|
56
58
|
export const magi: Strategy = {
|
|
57
59
|
name: "magi",
|
|
60
|
+
when: "contested fork: voters pick on a closed-choice question (yes/no, A/B); majority plus dissent",
|
|
61
|
+
team: "magi",
|
|
58
62
|
params: {
|
|
59
63
|
aggregate: { type: "string", default: "majority", doc: '"majority" | "unanimity"' },
|
|
60
64
|
reflect: { type: "boolean", default: true, doc: "one anonymised reflection round" },
|
|
@@ -88,7 +92,6 @@ export const magi: Strategy = {
|
|
|
88
92
|
return cancelled(round1.map((c) => c.usage));
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
let candidates = opening;
|
|
92
95
|
const okCount = opening.filter((c) => c.ok).length;
|
|
93
96
|
const tokensBeforeReflection = round1.reduce((total, result) => total + result.usage.input + result.usage.output, 0);
|
|
94
97
|
// A reflection is meaningful only when every core can participate. Under an active token
|
|
@@ -97,19 +100,19 @@ export const magi: Strategy = {
|
|
|
97
100
|
// goes for children: a rescue already spent a child per recovered core, and exceeding
|
|
98
101
|
// `maxChildren` THROWS out of sdk.agent() — turning a poll that has a ruling into no ruling.
|
|
99
102
|
const childrenUsed = round1.length + round1.filter((c) => c.modelRecovery).length;
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
103
|
+
const wantsReflection = reflect && okCount >= 2;
|
|
104
|
+
const skip = !wantsReflection ? undefined
|
|
105
|
+
: sdk.limits.budgetTokens > 0 && tokensBeforeReflection + team.length > sdk.limits.budgetTokens
|
|
106
|
+
? { why: "token budget", detail: `cannot fund the full panel (${sdk.limits.budgetTokens})` }
|
|
107
|
+
: childrenUsed + team.length > sdk.limits.maxChildren
|
|
108
|
+
? { why: "child budget", detail: `cannot fund the full panel (maxChildren ${sdk.limits.maxChildren}, ${childrenUsed} used)` }
|
|
109
|
+
: undefined;
|
|
110
|
+
const didReflect = wantsReflection && !skip;
|
|
111
|
+
if (skip) sdk.log(`magi: reflection skipped — ${skip.why} ${skip.detail}`);
|
|
110
112
|
|
|
113
|
+
let reflected: AgentResult[] = [];
|
|
111
114
|
if (didReflect) {
|
|
112
|
-
// Round 2 — each core sees the
|
|
115
|
+
// Round 2 — each core sees the panel's positions ANONYMISED (no author identity, so a
|
|
113
116
|
// core can't defer to a "senior" peer) and casts its FINAL vote. Instructed to hold
|
|
114
117
|
// unless genuinely moved, so this informs without manufacturing false consensus.
|
|
115
118
|
const positions = opening
|
|
@@ -123,7 +126,7 @@ export const magi: Strategy = {
|
|
|
123
126
|
// The recovered model must survive into this round: rebuilding from `rosterSpec(m)` alone
|
|
124
127
|
// would hand the rescued core back the model that just broke, so it fails again, is
|
|
125
128
|
// quarantined as invalid, and the recovery buys nothing but tokens.
|
|
126
|
-
|
|
129
|
+
reflected = await sdk.parallel(
|
|
127
130
|
team.map((m, i) => () => {
|
|
128
131
|
const model = rescuedModel(opening[i]);
|
|
129
132
|
return sdk.agent({ ...rosterSpec(m), ...(model ? { model } : {}), task: reflectTask, outputContract: "default" });
|
|
@@ -134,11 +137,23 @@ export const magi: Strategy = {
|
|
|
134
137
|
// DURING the reflection round is visible here only as every core settling
|
|
135
138
|
// `failureKind: "abort"` — as is one on a caller that passed no signal. Without the
|
|
136
139
|
// second clause a stopped run reports a normal-looking "no ruling" from a killed poll.
|
|
137
|
-
if (sdk.signal?.aborted ||
|
|
138
|
-
return cancelled([...round1, ...
|
|
140
|
+
if (sdk.signal?.aborted || reflected.every((c) => c.failureKind === "abort")) {
|
|
141
|
+
return cancelled([...round1, ...reflected].map((c) => c.usage));
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
|
-
|
|
144
|
+
// A reflection leg that FAILED (timeout, provider, contract...) is a missing second opinion, not a
|
|
145
|
+
// missing voter: the core keeps the valid vote it already cast, so a 3-core poll stays a 3-core
|
|
146
|
+
// poll. A STOPPED leg is never substituted. The substituted vote is not billed again (`allRuns`).
|
|
147
|
+
const ballots = didReflect
|
|
148
|
+
? reflected.map((r, i) => {
|
|
149
|
+
const first = opening[i];
|
|
150
|
+
return !r.ok && r.failureKind !== "abort" && first?.ok ? first : r;
|
|
151
|
+
})
|
|
152
|
+
: opening;
|
|
153
|
+
const kept = didReflect ? ballots.filter((b, i) => b !== reflected[i]).length : 0;
|
|
154
|
+
// "Reflected" means at least one core cast a second-round vote; a round that failed everywhere changed nothing.
|
|
155
|
+
const reflectedAny = didReflect && kept < ballots.length;
|
|
156
|
+
const decision = sdk.reduce.vote(ballots, { aggregate, keepBestFallback: true });
|
|
142
157
|
|
|
143
158
|
// Lead with the ruling (the answer); the decision/tally plumbing is a compact footer,
|
|
144
159
|
// not the headline. The collapsed council card shows `headline`; the supervisor still
|
|
@@ -156,8 +171,10 @@ export const magi: Strategy = {
|
|
|
156
171
|
const rescuedLine = rescued.length > 0
|
|
157
172
|
? ` · ${rescued.length} core${rescued.length === 1 ? "" : "s"} recovered on ${[...new Set(rescued.map((c) => c.modelRecovery?.to ?? ""))].filter(Boolean).join(", ")}`
|
|
158
173
|
: "";
|
|
174
|
+
const keptLine = kept > 0 ? ` · ${kept} core${kept === 1 ? "" : "s"} kept their round-1 vote (reflection failed)` : "";
|
|
175
|
+
const skipLine = skip ? ` · reflection skipped (${skip.why})` : "";
|
|
159
176
|
lines.push(
|
|
160
|
-
`\n— magi: ${decision.status}${
|
|
177
|
+
`\n— magi: ${decision.status}${reflectedAny ? " (after 1 reflection round)" : ""}${decision.usedFallback ? " · fell back to best-by-confidence" : ""} · tally ${tally}${invalid}${rescuedLine}${keptLine}${skipLine}`,
|
|
161
178
|
);
|
|
162
179
|
|
|
163
180
|
const winnerResult = decision.winner?.structured?.result;
|
|
@@ -170,16 +187,16 @@ export const magi: Strategy = {
|
|
|
170
187
|
// Usage sums EVERY run the poll actually paid for: the opening round (each logical result
|
|
171
188
|
// already carries its rescue's tokens exactly once) and the reflection round, kept separate
|
|
172
189
|
// from the opening one when reflection ran.
|
|
173
|
-
const allRuns: AgentResult[] =
|
|
190
|
+
const allRuns: AgentResult[] = [...round1, ...reflected];
|
|
174
191
|
const result: AgentResult = {
|
|
175
192
|
agent: "magi",
|
|
176
193
|
output: lines.join("\n"),
|
|
177
|
-
structured: { status: decision.status, tally: decision.tally, usedFallback: decision.usedFallback, reflected:
|
|
194
|
+
structured: { status: decision.status, tally: decision.tally, usedFallback: decision.usedFallback, reflected: reflectedAny, headline },
|
|
178
195
|
usage: sumUsage(allRuns.map((c) => c.usage)),
|
|
179
196
|
ok: decision.winner !== undefined,
|
|
180
197
|
};
|
|
181
198
|
if (!result.ok) {
|
|
182
|
-
const cause = summarizeFailedResults(
|
|
199
|
+
const cause = summarizeFailedResults(ballots, "magi produced no ruling");
|
|
183
200
|
result.error = cause.error;
|
|
184
201
|
result.failureKind = cause.failureKind;
|
|
185
202
|
}
|
|
@@ -16,6 +16,7 @@ import { fenceUntrusted } from "../../core/fence.ts";
|
|
|
16
16
|
import { cappedList } from "../../core/format.ts";
|
|
17
17
|
import { validateParallelWriteSets } from "../../core/ownership.ts";
|
|
18
18
|
import type { ChildUsage } from "../../engine/stream.ts";
|
|
19
|
+
import { compactInlineText } from "../../ui/presentation.ts";
|
|
19
20
|
import { itemLedger, type ItemLedgerEntry, sumUsage } from "../reducers.ts";
|
|
20
21
|
import { parseAuxActor } from "../params.ts";
|
|
21
22
|
import { rosterSpec, type RosterMember, type RosterSpec } from "../roster.ts";
|
|
@@ -144,10 +145,16 @@ async function splitIntoItems(
|
|
|
144
145
|
task: string,
|
|
145
146
|
maxItems: number,
|
|
146
147
|
reserveChildren: number,
|
|
148
|
+
declareWriteSets: boolean,
|
|
147
149
|
): Promise<SplitOutcome> {
|
|
150
|
+
// With ownership on, the splitter is the only party that can declare each item's write set, so it is asked
|
|
151
|
+
// for the object shape itself: one instruction, not a string array here and objects in the caller's prose.
|
|
152
|
+
const shape = declareWriteSets
|
|
153
|
+
? 'a JSON array of {"item": "<short description>", "writeSet": ["<every path a worker would edit>"]} objects (use [] for a read-only item)'
|
|
154
|
+
: "a JSON array of short strings";
|
|
148
155
|
const split = await sdk.agent({
|
|
149
156
|
...splitter,
|
|
150
|
-
task: `Break this task into independent sub-items. Return ONLY
|
|
157
|
+
task: `Break this task into independent sub-items. Return ONLY ${shape} — one per sub-item, nothing else.\n\nTask: ${task}`,
|
|
151
158
|
}, { reserveChildren });
|
|
152
159
|
if (!split.ok) {
|
|
153
160
|
return {
|
|
@@ -263,6 +270,9 @@ function buildItemLedger(
|
|
|
263
270
|
|
|
264
271
|
export const map: Strategy = {
|
|
265
272
|
name: "map",
|
|
273
|
+
when: "N items found at runtime: splitter lists, worker handles each in parallel",
|
|
274
|
+
roster: "[splitter, worker]",
|
|
275
|
+
team: "swarm",
|
|
266
276
|
params: {
|
|
267
277
|
maxItems: { type: "number", doc: "default AND ceiling: the run's maxChildren, less the splitter's own slot" },
|
|
268
278
|
peers: { type: "boolean", default: false, doc: "workers share load-bearing cross-item discoveries live" },
|
|
@@ -304,7 +314,7 @@ export const map: Strategy = {
|
|
|
304
314
|
// conservative upper bound; the cap above already guarantees the reserved waves fit.
|
|
305
315
|
const maxWorkerItems = Math.max(0, Math.floor(maxItems));
|
|
306
316
|
const splitReserve = maxWorkerItems * (reviewer ? 2 : 1);
|
|
307
|
-
const split = await splitIntoItems(sdk, splitter, input.task, maxItems, splitReserve);
|
|
317
|
+
const split = await splitIntoItems(sdk, splitter, input.task, maxItems, splitReserve, ownership !== "off");
|
|
308
318
|
if (!split.ok) return split.result;
|
|
309
319
|
const { allItems, items, dropped, usage: splitUsage } = split;
|
|
310
320
|
sdk.log(`map: ${items.length} items → ${worker.agent}${peers ? " (cross-talk on)" : ""}`);
|
|
@@ -317,7 +327,7 @@ export const map: Strategy = {
|
|
|
317
327
|
if (ownership === "enforce") enforceOwnershipOrThrow(items);
|
|
318
328
|
|
|
319
329
|
const results = await runWorkers(sdk, worker, input.task, items, peers, reviewer ? items.length : 0);
|
|
320
|
-
const agg = sdk.reduce.aggregate(results);
|
|
330
|
+
const agg = sdk.reduce.aggregate(results, items.map((it, i) => `item ${i + 1}: ${compactInlineText(it.item, { maxChars: 60 })}`));
|
|
321
331
|
// Say what was left out. The clamp is right — a worker per item past the cap would trip the
|
|
322
332
|
// pre-spawn guard and lose the whole fan-out — but an aggregate that silently covers part of
|
|
323
333
|
// the splitter's list reads as a complete answer over an incomplete input set.
|
|
@@ -35,6 +35,8 @@ const NAVIGATOR_PROTOCOL = [
|
|
|
35
35
|
|
|
36
36
|
export const pair: Strategy = {
|
|
37
37
|
name: "pair",
|
|
38
|
+
when: "one member works, a navigator reviews live",
|
|
39
|
+
roster: "[driver, navigator]",
|
|
38
40
|
async run(input, sdk) {
|
|
39
41
|
const team = input.roster ? sdk.roster.team(input.roster) : [];
|
|
40
42
|
if (team.length < 2) throw new Error("pair: a roster of [driver, navigator] is required");
|
|
@@ -44,6 +44,8 @@ function failed(results: AgentResult[], upstream: string, step: AgentResult): Ag
|
|
|
44
44
|
|
|
45
45
|
export const pipeline: Strategy = {
|
|
46
46
|
name: "pipeline",
|
|
47
|
+
when: "ad-hoc ordered stages you name (no installed flow needed): each member refines the previous output; the last answer is the result",
|
|
48
|
+
roster: "stages, in order",
|
|
47
49
|
async run(input, sdk) {
|
|
48
50
|
const team = input.roster ? sdk.roster.team(input.roster) : [];
|
|
49
51
|
if (team.length === 0) throw new Error("pipeline: a non-empty roster is required");
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* peers?: boolean (gatherers share contradictions/corroborations live — default off) }
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { rosterSpec } from "../roster.ts";
|
|
13
|
+
import { rosterNodeKeys, rosterSpec } from "../roster.ts";
|
|
14
14
|
import { fenceUntrusted } from "../../core/fence.ts";
|
|
15
|
+
import { sanitizeStatusLabel } from "../../core/display-label.ts";
|
|
15
16
|
import { parseAuxActor } from "../params.ts";
|
|
16
17
|
import { sumUsage, summarizeFailedResults } from "../reducers.ts";
|
|
17
18
|
import type { Strategy } from "../sdk.ts";
|
|
@@ -28,6 +29,9 @@ const CROSS_TALK = [
|
|
|
28
29
|
|
|
29
30
|
export const synthesize: Strategy = {
|
|
30
31
|
name: "synthesize",
|
|
32
|
+
when: "many angles into ONE deliverable: gatherers in parallel, one synthesiser merges",
|
|
33
|
+
roster: "gatherers",
|
|
34
|
+
team: "review",
|
|
31
35
|
params: {
|
|
32
36
|
// The default IS roster[0] — declared as `rosterIndex`, never as an agent name: the
|
|
33
37
|
// participant collector resolves "override, else roster[rosterIndex], else `default`",
|
|
@@ -61,10 +65,12 @@ export const synthesize: Strategy = {
|
|
|
61
65
|
}, { reserveChildren: 1 }), // leave the mandatory merge slot; SDK reserves queued gatherers
|
|
62
66
|
),
|
|
63
67
|
);
|
|
64
|
-
|
|
68
|
+
// Lens-aware labels (`reviewer · SECURITY`): the synthesizer must see WHICH lens found what.
|
|
69
|
+
const labels = rosterNodeKeys(team);
|
|
70
|
+
const usable = results.flatMap((r, i) => (r.ok && r.output.trim() ? [{ label: labels[i], output: r.output.trim() }] : []));
|
|
65
71
|
if (usable.length === 0) {
|
|
66
72
|
const cause = summarizeFailedResults(results, "no gatherer produced output");
|
|
67
|
-
const reasons = results.map((r) => `[${
|
|
73
|
+
const reasons = results.map((r, i) => `[${labels[i]}] ${r.error ?? "(no output)"}`).join("; ");
|
|
68
74
|
return {
|
|
69
75
|
agent: "synthesize",
|
|
70
76
|
output: `(no gatherer produced output: ${reasons})`,
|
|
@@ -74,13 +80,27 @@ export const synthesize: Strategy = {
|
|
|
74
80
|
failureKind: cause.failureKind,
|
|
75
81
|
};
|
|
76
82
|
}
|
|
83
|
+
// A partial merge is useful, but a missing review lens is not a clean review. Carry the
|
|
84
|
+
// runtime coverage into BOTH the merger's input and its returned output, even if it omits it.
|
|
85
|
+
const missing = results.flatMap((r, i) => r.ok && r.output.trim() ? [] : [{
|
|
86
|
+
member: sanitizeStatusLabel(labels[i] ?? r.agent),
|
|
87
|
+
status: r.ok ? "empty" : "failed",
|
|
88
|
+
detail: sanitizeStatusLabel(r.ok ? "no output" : r.error ?? "no failure detail", "no failure detail", 200),
|
|
89
|
+
}]);
|
|
90
|
+
const coverage = missing.length > 0 ? { expected: team.length, usable: usable.length, missing } : undefined;
|
|
91
|
+
const coverageNote = coverage ? [
|
|
92
|
+
`Incomplete coverage: ${usable.length}/${team.length} gatherers returned usable findings. Missing members were not reviewed; do not present this as a complete review.`,
|
|
93
|
+
fenceUntrusted(missing.slice(0, 16).map((m) => `${m.member} — ${m.status}: ${m.detail}`).join("\n")),
|
|
94
|
+
...(missing.length > 16 ? [`${missing.length - 16} additional missing members are recorded in the result's coverage metadata.`] : []),
|
|
95
|
+
].join("\n") : "";
|
|
77
96
|
|
|
78
|
-
const sections = usable.map((
|
|
97
|
+
const sections = usable.map((u) => `--- [${u.label}] ---\n${fenceUntrusted(u.output)}`).join("\n\n");
|
|
79
98
|
const final = await sdk.agent({
|
|
80
99
|
...synthesizer,
|
|
81
100
|
task:
|
|
82
101
|
`Synthesise the findings below into ONE coherent, de-duplicated answer to the task. ` +
|
|
83
102
|
`Resolve contradictions explicitly (say which finding you trusted and why) and keep every load-bearing detail.\n\n` +
|
|
103
|
+
(coverageNote ? `${coverageNote}\n\n` : "") +
|
|
84
104
|
`Task: ${input.task}\n\nFindings:\n${sections}`,
|
|
85
105
|
});
|
|
86
106
|
|
|
@@ -88,12 +108,12 @@ export const synthesize: Strategy = {
|
|
|
88
108
|
// the synthesiser's `result` field if it emitted one, else the first non-empty line.
|
|
89
109
|
const firstLine = final.output.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
|
|
90
110
|
const structuredResult = final.structured?.result;
|
|
91
|
-
const headline =
|
|
111
|
+
const headline = `${coverage ? `Incomplete coverage: ${usable.length}/${team.length} · ` : ""}${typeof structuredResult === "string" && structuredResult.trim() ? structuredResult : firstLine}`.slice(0, 120);
|
|
92
112
|
|
|
93
113
|
const result: AgentResult = {
|
|
94
114
|
agent: "synthesize",
|
|
95
|
-
output: final.output,
|
|
96
|
-
structured: { ...(final.structured ?? {}), gatherers: usable.length, headline },
|
|
115
|
+
output: coverageNote ? `${coverageNote}\n\n${final.output}` : final.output,
|
|
116
|
+
structured: { ...(final.structured ?? {}), gatherers: usable.length, headline, ...(coverage ? { coverage } : {}) },
|
|
97
117
|
usage: sumUsage([...results, final].map((r) => r.usage)),
|
|
98
118
|
ok: final.ok,
|
|
99
119
|
};
|
|
@@ -47,3 +47,34 @@ export function knownParams(name: string): Record<string, StrategyParam> | undef
|
|
|
47
47
|
const strategy = BUILTINS[name];
|
|
48
48
|
return strategy ? (strategy.params ?? {}) : undefined;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
/** One line per strategy for the council tool's description, generated from the registry:
|
|
52
|
+
* `- name(params, required*) — when; roster: positions; default team X.` Guidance on which method fits a
|
|
53
|
+
* problem class lives with the method, so it cannot drift from what the code does. */
|
|
54
|
+
export function strategyCatalog(): string {
|
|
55
|
+
return strategyNames()
|
|
56
|
+
.map((name) => {
|
|
57
|
+
const s = BUILTINS[name] as Strategy;
|
|
58
|
+
const keys = Object.keys(s.params ?? {}).map((k) => (s.required?.includes(k) ? `${k}*` : k));
|
|
59
|
+
const head = `- ${name}${keys.length > 0 ? `(${keys.join(", ")})` : ""}`;
|
|
60
|
+
// `magi` is the fallback every call already gets, so only a different default team is worth a mention.
|
|
61
|
+
const team = s.team && s.team !== "magi" ? `; default team ${s.team}` : "";
|
|
62
|
+
return `${head}${s.when ? ` — ${s.when}` : ""}${s.roster ? `; roster: ${s.roster}` : ""}${team}.`;
|
|
63
|
+
})
|
|
64
|
+
.join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Refuse a council call that cannot run, BEFORE any run, tree row or model picker exists: a
|
|
68
|
+
* `required` param that is missing or blank (blank counts as missing, exactly as the strategies'
|
|
69
|
+
* own `parseAuxActor` reads it). `undefined` means the call is sound. An UNKNOWN strategy is not
|
|
70
|
+
* judged here: the run itself fails with the installed list (an interactive council reports that in
|
|
71
|
+
* its completion follow-up, not in the launching call). */
|
|
72
|
+
export function missingRequiredParams(name: string, params: Record<string, unknown>): string | undefined {
|
|
73
|
+
const strategy = BUILTINS[name];
|
|
74
|
+
const missing = (strategy?.required ?? []).filter((key) => {
|
|
75
|
+
const value = params[key];
|
|
76
|
+
return value === undefined || value === null || (typeof value === "string" && !value.trim());
|
|
77
|
+
});
|
|
78
|
+
if (missing.length === 0) return undefined;
|
|
79
|
+
return `"${name}" requires ${missing.map((key) => `params.${key} — ${strategy?.params?.[key]?.doc ?? "required"}`).join("; ")}`;
|
|
80
|
+
}
|
|
@@ -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`
|