@aeondave/pi-persona 1.17.3 → 1.17.5
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 +57 -36
- package/CHANGELOG.md +106 -1
- package/README.md +32 -16
- package/docs/ARCHITECTURE.md +199 -48
- 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/docs/STRATEGIES.md +6 -0
- package/package.json +12 -6
- package/src/agents/agent.ts +6 -0
- package/src/bridge.ts +33 -23
- package/src/bus/broker/client.ts +151 -30
- package/src/bus/broker/host.ts +130 -11
- package/src/bus/broker/messages.ts +35 -5
- package/src/bus/contact-transport.ts +8 -0
- package/src/bus/contact.ts +118 -40
- package/src/bus/peers.ts +23 -6
- 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/companion.ts +64 -0
- 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 +100 -20
- package/src/engine/async.ts +53 -11
- package/src/engine/child.ts +11 -2
- package/src/engine/inproc.ts +98 -47
- 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/companion.ts +38 -0
- package/src/extension/engine.ts +40 -29
- package/src/extension/hooks.ts +954 -931
- package/src/extension/identity.ts +41 -75
- package/src/extension/memory-owner.ts +54 -0
- package/src/extension/mind-worker.ts +129 -0
- package/src/extension/results.ts +138 -0
- package/src/extension/shared.ts +17 -0
- package/src/extension.ts +2922 -2849
- package/src/loader.ts +11 -3
- package/src/orchestration/agent-attempt.ts +15 -0
- package/src/orchestration/flow-journal.ts +7 -1
- package/src/orchestration/flow-run.ts +125 -19
- package/src/orchestration/flow.ts +11 -0
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -19
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +6 -7
- 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/activity-rows.ts +34 -0
- package/src/ui/agent-overlay.ts +23 -13
- package/src/ui/agent-tree.ts +67 -6
- 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/ui/strategy-leg.ts +40 -0
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";
|
|
@@ -12,6 +13,7 @@ import { planModelRecovery } from "./model-retry.ts";
|
|
|
12
13
|
import { mapWithConcurrency, Semaphore } from "./parallel.ts";
|
|
13
14
|
import { aggregateResults, sumUsage } from "./reducers.ts";
|
|
14
15
|
import { roleHint, type RosterMember } from "./roster.ts";
|
|
16
|
+
import { agentAttemptIdentity, type AgentAttemptIdentity } from "./agent-attempt.ts";
|
|
15
17
|
import type { AgentResult, ModelRecovery } from "./types.ts";
|
|
16
18
|
import { type ReducerResult, type VoteOpts, voteReduce } from "./voting.ts";
|
|
17
19
|
|
|
@@ -22,6 +24,8 @@ export interface AgentRunSpec {
|
|
|
22
24
|
name?: string;
|
|
23
25
|
task: string;
|
|
24
26
|
model?: string;
|
|
27
|
+
/** Requested per-leg effort, before Pi applies the selected model's capability clamp. */
|
|
28
|
+
thinking?: ThinkingLevel;
|
|
25
29
|
tools?: string[];
|
|
26
30
|
/** Skills the sub-agent must load first (dynamic specialisation). */
|
|
27
31
|
skills?: string[];
|
|
@@ -49,6 +53,22 @@ export interface AgentRunSpec {
|
|
|
49
53
|
peers?: boolean;
|
|
50
54
|
/** Declared ownership (from delegate `writeSet`), used only to annotate the files-changed report. */
|
|
51
55
|
writeSet?: string[];
|
|
56
|
+
/** INTERNAL (flow reference handoff): the archived `result:<UUID>` refs granted to THIS leg
|
|
57
|
+
* — exactly its phase's DIRECT upstream needs, nothing more. Not a public delegate
|
|
58
|
+
* argument: the SDK strips any strategy-supplied value (strategies cannot enlarge a
|
|
59
|
+
* grant); only the trusted engine-seam wrapper (`withResultRefs`, set by the flow wiring)
|
|
60
|
+
* or an equivalently trusted host path may set it. Both engines preflight + scope the
|
|
61
|
+
* `read_result` tool on it. */
|
|
62
|
+
resultRefs?: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Pin a leg's granted result refs at the ENGINE seam — the trusted wiring path for flow
|
|
66
|
+
* reference mode. Overrides (never unions) whatever the caller passed, so a strategy cannot
|
|
67
|
+
* smuggle extra refs past the SDK's strip. An empty array is an explicit "none". */
|
|
68
|
+
export function withResultRefs(engine: StrategyEngine, resultRefs: readonly string[]): StrategyEngine {
|
|
69
|
+
return {
|
|
70
|
+
run: (spec, onProgress, signal, onSteer) => engine.run({ ...spec, resultRefs }, onProgress, signal, onSteer),
|
|
71
|
+
};
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
/** Is `x` a genuine positive, finite `timeoutMs` override — vs. absent/junk (undefined, NaN,
|
|
@@ -180,15 +200,17 @@ export interface SDKDeps {
|
|
|
180
200
|
/** Per-agent lifecycle, for live UI. The result is passed on done/failed so the
|
|
181
201
|
* UI can capture each agent's output/usage. `key` is a run-unique display id (the
|
|
182
202
|
* `agent` name for a solo member, disambiguated by role/occurrence when the same agent
|
|
183
|
-
* runs several times in one roster)
|
|
184
|
-
*
|
|
185
|
-
onAgentStatus?: (agent: string, status: AgentStatus, result
|
|
203
|
+
* runs several times in one roster). The frozen `attempt` carries exact full-role/model/effort
|
|
204
|
+
* identity: never recover those fields by parsing this shortened display key. */
|
|
205
|
+
onAgentStatus?: (agent: string, status: AgentStatus, result: AgentResult | undefined, key: string, attempt: AgentAttemptIdentity) => void;
|
|
206
|
+
/** One final logical leg after model recovery, never intermediate attempts. Storage is non-fatal. */
|
|
207
|
+
onAgentResult?: (result: AgentResult, key: string, attempt: AgentAttemptIdentity) => void;
|
|
186
208
|
/** Per-agent streaming progress (rolling output), for live UI. See `onAgentStatus.key`. */
|
|
187
|
-
onAgentProgress?: (agent: string, progress: AgentProgress, key
|
|
209
|
+
onAgentProgress?: (agent: string, progress: AgentProgress, key: string, attempt: AgentAttemptIdentity) => void;
|
|
188
210
|
/** Called as each agent starts with a handle to abort just that agent (for UI stop). */
|
|
189
|
-
onAgentStart?: (agent: string, abort: () => void, key
|
|
211
|
+
onAgentStart?: (agent: string, abort: () => void, key: string, attempt: AgentAttemptIdentity) => void;
|
|
190
212
|
/** Called once an agent is live with a handle to steer it (in-process engine only). */
|
|
191
|
-
onAgentSteerable?: (agent: string, steer: SteerFn, key
|
|
213
|
+
onAgentSteerable?: (agent: string, steer: SteerFn, key: string, attempt: AgentAttemptIdentity) => void;
|
|
192
214
|
/** Emitted immediately BEFORE a leg is re-run on the main model after a model-caused failure,
|
|
193
215
|
* so live UI can say so while it happens (`key` is the same logical node as the attempt it
|
|
194
216
|
* replaces). Purely cosmetic: a throw here is contained and can never discard the billed
|
|
@@ -241,6 +263,13 @@ function abortedLeg(agent: string): AgentResult {
|
|
|
241
263
|
return { agent, output: "", usage: emptyUsage(), ok: false, error: "agent aborted", failureKind: "abort" };
|
|
242
264
|
}
|
|
243
265
|
|
|
266
|
+
/** Copy of a spec with its `resultRefs` removed — the SDK's one strip point, so a strategy's
|
|
267
|
+
* own value never reaches an engine (only `withResultRefs`-style trusted wiring sets it). */
|
|
268
|
+
function stripResultRefs(spec: AgentRunSpec): AgentRunSpec {
|
|
269
|
+
const { resultRefs: _untrusted, ...rest } = spec;
|
|
270
|
+
return rest;
|
|
271
|
+
}
|
|
272
|
+
|
|
244
273
|
export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
245
274
|
// Run-scoped enforcement of the declared limits — applied here so NO strategy can
|
|
246
275
|
// exceed them, however it calls agent() (I2: safety from runtime limits, not isolation).
|
|
@@ -269,18 +298,27 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
269
298
|
};
|
|
270
299
|
|
|
271
300
|
return {
|
|
272
|
-
agent: async (
|
|
301
|
+
agent: async (requested, options) => {
|
|
302
|
+
// Reference-handoff grants are trusted wiring, never strategy input: drop any
|
|
303
|
+
// strategy-supplied `resultRefs` here (the flow path re-pins them at the engine
|
|
304
|
+
// seam via `withResultRefs`, which runs BELOW this layer).
|
|
305
|
+
const spec = requested.resultRefs === undefined ? requested : stripResultRefs(requested);
|
|
306
|
+
const key = uiKeyFor(spec);
|
|
307
|
+
let currentAttempt = agentAttemptIdentity(spec);
|
|
308
|
+
const finish = (result: AgentResult): AgentResult => {
|
|
309
|
+
try { deps.onAgentResult?.(result, key, currentAttempt); } catch { /* storage must not discard a completed leg */ }
|
|
310
|
+
return result;
|
|
311
|
+
};
|
|
273
312
|
if (deps.canSpawn && !deps.canSpawn(spec.agent)) {
|
|
274
|
-
return {
|
|
313
|
+
return finish({
|
|
275
314
|
agent: spec.agent,
|
|
276
315
|
output: "",
|
|
277
316
|
usage: emptyUsage(),
|
|
278
317
|
ok: false,
|
|
279
318
|
error: `persona may not spawn "${spec.agent}"`,
|
|
280
319
|
failureKind: "contract",
|
|
281
|
-
};
|
|
320
|
+
});
|
|
282
321
|
}
|
|
283
|
-
const key = uiKeyFor(spec);
|
|
284
322
|
const pending = Symbol();
|
|
285
323
|
pendingAgents.add(pending);
|
|
286
324
|
let started = false;
|
|
@@ -302,6 +340,8 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
302
340
|
const detachRoot: Array<() => void> = [];
|
|
303
341
|
let announcedStart = false;
|
|
304
342
|
const attempt = async (aSpec: AgentRunSpec): Promise<AgentResult> => {
|
|
343
|
+
const identity = agentAttemptIdentity(aSpec);
|
|
344
|
+
currentAttempt = identity;
|
|
305
345
|
const ac = current ?? new AbortController();
|
|
306
346
|
current = ac;
|
|
307
347
|
if (deps.signal) {
|
|
@@ -312,9 +352,9 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
312
352
|
}
|
|
313
353
|
if (!announcedStart) {
|
|
314
354
|
announcedStart = true;
|
|
315
|
-
deps.onAgentStart?.(aSpec.agent, () => current?.abort(), key);
|
|
355
|
+
deps.onAgentStart?.(aSpec.agent, () => current?.abort(), key, identity);
|
|
316
356
|
}
|
|
317
|
-
deps.onAgentStatus?.(aSpec.agent, "running", undefined, key);
|
|
357
|
+
deps.onAgentStatus?.(aSpec.agent, "running", undefined, key, identity);
|
|
318
358
|
// A slot grant is not an engine start. Root or local cancellation in a lifecycle
|
|
319
359
|
// callback must prevent the engine invocation, not merely hand it an aborted signal.
|
|
320
360
|
if (ac.signal.aborted) {
|
|
@@ -330,9 +370,9 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
330
370
|
try {
|
|
331
371
|
result = await deps.engine.run(
|
|
332
372
|
aSpec,
|
|
333
|
-
onProgress ? (p) => onProgress(aSpec.agent, p, key) : undefined,
|
|
373
|
+
onProgress ? (p) => onProgress(aSpec.agent, p, key, identity) : undefined,
|
|
334
374
|
ac.signal,
|
|
335
|
-
deps.onAgentSteerable ? (steer) => deps.onAgentSteerable?.(aSpec.agent, steer, key) : undefined,
|
|
375
|
+
deps.onAgentSteerable ? (steer) => deps.onAgentSteerable?.(aSpec.agent, steer, key, identity) : undefined,
|
|
336
376
|
);
|
|
337
377
|
} catch (err) {
|
|
338
378
|
// PER-LEG failure — contained, never rethrown: one blown engine call must not discard
|
|
@@ -349,7 +389,7 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
349
389
|
};
|
|
350
390
|
try {
|
|
351
391
|
const first = await attempt(spec);
|
|
352
|
-
deps.onAgentStatus?.(spec.agent, first.ok ? "done" : "failed", first, key);
|
|
392
|
+
deps.onAgentStatus?.(spec.agent, first.ok ? "done" : "failed", first, key, currentAttempt);
|
|
353
393
|
|
|
354
394
|
// ── Main-only model recovery ──────────────────────────────────────────────
|
|
355
395
|
// A model-caused failure (`provider`/`unknown-model`) gets exactly ONE re-run on the
|
|
@@ -391,7 +431,7 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
391
431
|
// a fresh controller or spend another child slot after it has been stopped.
|
|
392
432
|
if (deps.signal?.aborted || current?.signal.aborted) {
|
|
393
433
|
const stopped = { ...abortedLeg(spec.agent), usage: first.usage };
|
|
394
|
-
deps.onAgentStatus?.(spec.agent, "failed", stopped, key);
|
|
434
|
+
deps.onAgentStatus?.(spec.agent, "failed", stopped, key, currentAttempt);
|
|
395
435
|
return stopped;
|
|
396
436
|
}
|
|
397
437
|
// The retry is a REAL attempt: admitted and charged like any other child, but inside
|
|
@@ -408,18 +448,20 @@ export function makeSDK(deps: SDKDeps): StrategySDK {
|
|
|
408
448
|
modelRecovery: recovery,
|
|
409
449
|
...(!second.ok ? { error: `Initial model ${plan.from ?? "(unresolved)"} failed: ${first.error ?? first.failureKind ?? "unknown cause"}; main model ${plan.to} failed: ${second.error ?? second.failureKind ?? "unknown cause"}` } : {}),
|
|
410
450
|
};
|
|
411
|
-
deps.onAgentStatus?.(spec.agent, settled.ok ? "done" : "failed", settled, key);
|
|
451
|
+
deps.onAgentStatus?.(spec.agent, settled.ok ? "done" : "failed", settled, key, currentAttempt);
|
|
412
452
|
return settled;
|
|
413
453
|
} finally {
|
|
414
454
|
for (const detach of detachRoot) detach();
|
|
415
455
|
}
|
|
416
456
|
};
|
|
417
457
|
try {
|
|
418
|
-
return await agentSlots.with(run, deps.signal);
|
|
458
|
+
return finish(await agentSlots.with(run, deps.signal));
|
|
419
459
|
} catch (error) {
|
|
420
460
|
// A queued leg has no engine result to report. Settle it as an abort when the
|
|
421
461
|
// run signal removed its waiter; all other errors retain their terminal semantics.
|
|
422
|
-
if (!started && deps.signal?.aborted)
|
|
462
|
+
if (!started && deps.signal?.aborted) {
|
|
463
|
+
return finish(abortedLeg(spec.agent));
|
|
464
|
+
}
|
|
423
465
|
throw error;
|
|
424
466
|
} finally {
|
|
425
467
|
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`
|
|
@@ -29,6 +29,7 @@ import { createHash } from "node:crypto";
|
|
|
29
29
|
|
|
30
30
|
import { memberBaseLabel, parseRosterMember, type RosterMember, type RosterSpec } from "../orchestration/roster.ts";
|
|
31
31
|
import { knownParams } from "../orchestration/strategy.ts";
|
|
32
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
32
33
|
import type { StrategyParam } from "../orchestration/sdk.ts";
|
|
33
34
|
|
|
34
35
|
/** Where a participant's effective model came from — in precedence order. */
|
|
@@ -58,6 +59,12 @@ export interface ModelParticipant {
|
|
|
58
59
|
model?: string;
|
|
59
60
|
/** Which step of the precedence supplied `model`. */
|
|
60
61
|
source: ModelSource;
|
|
62
|
+
/** The member's SPEC-level effort — exactly the value that lands in `AgentRunSpec.thinking`
|
|
63
|
+
* when a strategy spreads `rosterSpec(member)`. Never resolved against agent defaults or the
|
|
64
|
+
* configured child effort (the engines own that precedence): the row shows what the launch
|
|
65
|
+
* spec carried, the same rule the delegate launch rows follow. Absent when none was declared
|
|
66
|
+
* (absence renders nothing); an explicit `off` is a declared value and is kept. */
|
|
67
|
+
thinking?: ThinkingLevel;
|
|
61
68
|
/** Someone already chose this participant's model (inline, a saved assignment, or the agent's own
|
|
62
69
|
* frontmatter) ⇒ never a picker candidate. The session default is deliberately NOT counted. */
|
|
63
70
|
assigned: boolean;
|
|
@@ -135,6 +142,7 @@ function fromSpec(spec: RosterSpec, origin: "roster" | "param", param: string |
|
|
|
135
142
|
...(param ? { param } : {}),
|
|
136
143
|
...(model ? { model } : {}),
|
|
137
144
|
source,
|
|
145
|
+
...(spec.thinking ? { thinking: spec.thinking } : {}),
|
|
138
146
|
assigned: source === "inline" || source === "assignment" || source === "agent",
|
|
139
147
|
};
|
|
140
148
|
}
|
|
@@ -191,16 +199,13 @@ function isAgentName(value: unknown): boolean {
|
|
|
191
199
|
return typeof value === "string" && value.trim().length > 0 && !/\s/.test(value.trim());
|
|
192
200
|
}
|
|
193
201
|
|
|
194
|
-
/** Every
|
|
195
|
-
export function
|
|
202
|
+
/** Every declared execution occurrence, including repeated assignment keys with different pins. */
|
|
203
|
+
export function collectModelParticipantOccurrences(input: ParticipantInput): ModelParticipant[] {
|
|
196
204
|
const out: ModelParticipant[] = [];
|
|
197
|
-
const seen = new Set<string>();
|
|
198
205
|
for (const raw of input.members ?? []) {
|
|
199
206
|
const spec = toSpec(raw);
|
|
200
207
|
if (!spec) continue;
|
|
201
208
|
const p = fromSpec(spec, "roster", undefined, input);
|
|
202
|
-
if (seen.has(p.key)) continue;
|
|
203
|
-
seen.add(p.key);
|
|
204
209
|
out.push(p);
|
|
205
210
|
}
|
|
206
211
|
const schema = input.schema ?? (input.strategy ? knownParams(input.strategy) : undefined);
|
|
@@ -210,14 +215,22 @@ export function collectModelParticipants(input: ParticipantInput): ModelParticip
|
|
|
210
215
|
const spec = auxActorSpec(input, paramName, param);
|
|
211
216
|
if (!spec) continue;
|
|
212
217
|
const p = fromSpec(spec, "param", paramName, input);
|
|
213
|
-
if (seen.has(p.key)) continue;
|
|
214
|
-
seen.add(p.key);
|
|
215
218
|
out.push(p);
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
221
|
return out;
|
|
219
222
|
}
|
|
220
223
|
|
|
224
|
+
/** One row per assignment key for model setup. Repeated execution occurrences share that choice. */
|
|
225
|
+
export function collectModelParticipants(input: ParticipantInput): ModelParticipant[] {
|
|
226
|
+
const seen = new Set<string>();
|
|
227
|
+
return collectModelParticipantOccurrences(input).filter((p) => {
|
|
228
|
+
if (seen.has(p.key)) return false;
|
|
229
|
+
seen.add(p.key);
|
|
230
|
+
return true;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
221
234
|
/** The participants a picker may still ask about: nobody has chosen a model for them, and this
|
|
222
235
|
* session has not already asked (and been dismissed) for that key. The caller records the keys it
|
|
223
236
|
* asks about BEFORE asking, so a dismissal is remembered too. */
|
|
@@ -7,11 +7,8 @@
|
|
|
7
7
|
import type { RunLimits } from "../core/capabilities.ts";
|
|
8
8
|
import { makeRoster, type RosterMember } from "../orchestration/roster.ts";
|
|
9
9
|
import {
|
|
10
|
-
type AgentProgress,
|
|
11
|
-
type AgentStatus,
|
|
12
10
|
makeSDK,
|
|
13
11
|
type SDKDeps,
|
|
14
|
-
type SteerFn,
|
|
15
12
|
type StrategyEngine,
|
|
16
13
|
type StrategyInput,
|
|
17
14
|
} from "../orchestration/sdk.ts";
|
|
@@ -72,13 +69,14 @@ export interface RunStrategyDeps {
|
|
|
72
69
|
log?: (message: string) => void;
|
|
73
70
|
/** Per-agent lifecycle, for live UI (which roster agent is running/done + its result).
|
|
74
71
|
* `key` is a run-unique display id (disambiguates same-agent roster-role members). */
|
|
75
|
-
onAgentStatus?:
|
|
72
|
+
onAgentStatus?: SDKDeps["onAgentStatus"];
|
|
73
|
+
onAgentResult?: SDKDeps["onAgentResult"];
|
|
76
74
|
/** Per-agent streaming progress (rolling output), for live UI. */
|
|
77
|
-
onAgentProgress?:
|
|
75
|
+
onAgentProgress?: SDKDeps["onAgentProgress"];
|
|
78
76
|
/** Called as each agent starts with a handle to abort just that agent (UI stop). */
|
|
79
|
-
onAgentStart?:
|
|
77
|
+
onAgentStart?: SDKDeps["onAgentStart"];
|
|
80
78
|
/** Called once an agent is live with a handle to steer it (in-process engine only). */
|
|
81
|
-
onAgentSteerable?:
|
|
79
|
+
onAgentSteerable?: SDKDeps["onAgentSteerable"];
|
|
82
80
|
/** See `SDKDeps.canSpawn` — every strategy spawn, not only `delegate`. */
|
|
83
81
|
canSpawn?: (agent: string) => boolean;
|
|
84
82
|
}
|
|
@@ -110,6 +108,7 @@ export async function runPersonaStrategy(
|
|
|
110
108
|
if (deps.onModelFallback) sdkDeps.onModelFallback = deps.onModelFallback;
|
|
111
109
|
if (deps.log) sdkDeps.log = deps.log;
|
|
112
110
|
if (deps.onAgentStatus) sdkDeps.onAgentStatus = deps.onAgentStatus;
|
|
111
|
+
if (deps.onAgentResult) sdkDeps.onAgentResult = deps.onAgentResult;
|
|
113
112
|
if (deps.onAgentProgress) sdkDeps.onAgentProgress = deps.onAgentProgress;
|
|
114
113
|
if (deps.onAgentStart) sdkDeps.onAgentStart = deps.onAgentStart;
|
|
115
114
|
if (deps.onAgentSteerable) sdkDeps.onAgentSteerable = deps.onAgentSteerable;
|
package/src/persona/persona.ts
CHANGED
|
@@ -244,7 +244,7 @@ export function expandCouncilPreset(draft: CouncilDraft, presets: Record<string,
|
|
|
244
244
|
/** Per-call council selection. `persona` borrows only that persona's expanded
|
|
245
245
|
* council declaration; it never activates the persona or imports its prompt,
|
|
246
246
|
* model, tools, or permissions. Explicit strategy/roster/params remain local
|
|
247
|
-
* overrides
|
|
247
|
+
* overrides of those defaults. */
|
|
248
248
|
export interface CouncilInvocation {
|
|
249
249
|
persona?: string | undefined;
|
|
250
250
|
strategy?: string | undefined;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
createTerminalResultRecord, isResultRef, pageStoredBody, parseTerminalResultInput,
|
|
4
|
+
parseTerminalResultRecord, payloadBytes,
|
|
5
|
+
} from "./record.ts";
|
|
6
|
+
import {
|
|
7
|
+
TERMINAL_RESULT_ENTRY, RESULT_MAX_ENTRIES, RESULT_MAX_PAYLOAD_BYTES, RESULT_LIST_DEFAULT, RESULT_LIST_MAX,
|
|
8
|
+
type ArchiveEvidence, type ArchiveFailure, type ResultListRequest, type ResultPage, type ResultReadRequest,
|
|
9
|
+
type ResultStorage, type ResultSummary, type ResultSummaryPage, type ResultWarning, type SaveOutcome,
|
|
10
|
+
type TerminalResultArchiveDeps, type TerminalResultInput, type TerminalResultRecord,
|
|
11
|
+
} from "./types.ts";
|
|
12
|
+
|
|
13
|
+
interface RetainedResult {
|
|
14
|
+
record: TerminalResultRecord;
|
|
15
|
+
storage: ResultStorage;
|
|
16
|
+
storageError?: string;
|
|
17
|
+
}
|
|
18
|
+
interface SourceResult { data: unknown; index: number; retained?: RetainedResult }
|
|
19
|
+
type Lookup = { kind: "missing" } | { kind: "ambiguous" | "corrupt" } | {
|
|
20
|
+
kind: "found"; record: TerminalResultRecord; index: number; retained?: RetainedResult;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function identity(data: unknown): { ref?: string; originKey?: string } {
|
|
24
|
+
try {
|
|
25
|
+
if (data === null || typeof data !== "object") return {};
|
|
26
|
+
const value = data as Record<string, unknown>;
|
|
27
|
+
return {
|
|
28
|
+
...(isResultRef(value.ref) ? { ref: value.ref } : {}),
|
|
29
|
+
...(typeof value.originKey === "string" && value.originKey.length <= 1024 ? { originKey: value.originKey } : {}),
|
|
30
|
+
};
|
|
31
|
+
} catch { return {}; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function summary(record: TerminalResultRecord, storage: ResultStorage): ResultSummary {
|
|
35
|
+
// Parse again to detach optional usage too; callers never receive our mutable state.
|
|
36
|
+
const copy = parseTerminalResultRecord(record)!;
|
|
37
|
+
const { body: _body, payloadHash: _hash, ...metadata } = copy;
|
|
38
|
+
return { ...metadata, storage };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validQuery(value: unknown, keys: readonly string[]): boolean {
|
|
42
|
+
try {
|
|
43
|
+
if (value === null || typeof value !== "object") return false;
|
|
44
|
+
const proto: unknown = Object.getPrototypeOf(value);
|
|
45
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
46
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
47
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
48
|
+
return typeof key === "string" && keys.includes(key) && descriptor?.enumerable === true && "value" in descriptor;
|
|
49
|
+
});
|
|
50
|
+
} catch { return false; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function errorDetail(error: unknown): string {
|
|
54
|
+
try {
|
|
55
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
56
|
+
return text.slice(0, 2048);
|
|
57
|
+
} catch { return "Storage callback threw an unreadable error"; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One session-file owner, fixed at construction by the adapter. No filesystem, Pi imports,
|
|
61
|
+
* replay, wake or worker lifecycle. Native history is scanned without caching report bodies;
|
|
62
|
+
* even over-quota/corrupt histories leave valid unrelated reports queryable. Local accepted
|
|
63
|
+
* records and uncertain append reservations are bounded by the SAME count/byte ceilings.
|
|
64
|
+
* The native entry array is a host-owned immutable snapshot: its initial length is pinned so
|
|
65
|
+
* a host that appends to that same array cannot double-charge our own reserved writes.
|
|
66
|
+
*/
|
|
67
|
+
export class TerminalResultArchive {
|
|
68
|
+
private readonly deps: TerminalResultArchiveDeps;
|
|
69
|
+
private hydrated = false;
|
|
70
|
+
private hydrating = false;
|
|
71
|
+
private history: readonly unknown[] = [];
|
|
72
|
+
private historyLength = 0;
|
|
73
|
+
private historyCount = 0;
|
|
74
|
+
private chargedEntries = 0;
|
|
75
|
+
private chargedPayloadBytes: number | null = 0;
|
|
76
|
+
private localBytes = 0;
|
|
77
|
+
private readonly retained: RetainedResult[] = [];
|
|
78
|
+
private readonly warnings = new Set<ResultWarning>();
|
|
79
|
+
private saving = false;
|
|
80
|
+
|
|
81
|
+
constructor(deps: TerminalResultArchiveDeps) {
|
|
82
|
+
// Copy callbacks/flags: mutating a caller's dependency object cannot retarget this owner.
|
|
83
|
+
this.deps = { ...deps };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
save(input: TerminalResultInput): SaveOutcome {
|
|
87
|
+
if (this.saving || this.hydrating) return this.failure("busy", "A terminal result save or history accounting is already in progress");
|
|
88
|
+
this.saving = true;
|
|
89
|
+
try { this.hydrate(); return this.saveInput(input); }
|
|
90
|
+
finally { this.saving = false; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
read(request: ResultReadRequest): ResultPage {
|
|
94
|
+
if (this.hydrating) return this.failure("busy", "Terminal result history accounting is in progress");
|
|
95
|
+
this.hydrate();
|
|
96
|
+
if (!validQuery(request, ["ref", "offset", "limit"]) || !isResultRef(request.ref)) {
|
|
97
|
+
return this.failure("invalid", "Expected result:<UUID> and an optional stored-body offset/limit");
|
|
98
|
+
}
|
|
99
|
+
const found = this.lookup("ref", request.ref);
|
|
100
|
+
if (found.kind === "missing") return this.failure("unavailable", "Result reference is unavailable in this owning session");
|
|
101
|
+
if (found.kind !== "found") return this.failure(found.kind, "Result identity is ambiguous or corrupt in this owning session");
|
|
102
|
+
const page = pageStoredBody(found.record, request.offset, request.limit);
|
|
103
|
+
if (!page) return this.failure("invalid", "Invalid stored-body UTF-16 offset or read limit (2..16000); surrogate pairs cannot be split");
|
|
104
|
+
return {
|
|
105
|
+
ok: true, summary: summary(found.record, this.storage(found.retained)), ...page,
|
|
106
|
+
...this.evidence(found.record, found.retained),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
list(request: ResultListRequest = {}): ResultSummaryPage {
|
|
111
|
+
if (this.hydrating) return this.failure("busy", "Terminal result history accounting is in progress");
|
|
112
|
+
this.hydrate();
|
|
113
|
+
if (!validQuery(request, ["cursor", "limit"])) return this.failure("invalid", "Expected a result-list query");
|
|
114
|
+
const cursor = request.cursor === undefined ? 0 : request.cursor;
|
|
115
|
+
const limit = request.limit === undefined ? RESULT_LIST_DEFAULT : request.limit;
|
|
116
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > this.historyCount + this.retained.length
|
|
117
|
+
|| !Number.isInteger(limit) || limit < 1 || limit > RESULT_LIST_MAX) {
|
|
118
|
+
return this.failure("invalid", "Invalid result list cursor or limit (1..50)");
|
|
119
|
+
}
|
|
120
|
+
const results: ResultSummary[] = [];
|
|
121
|
+
let nextCursor: number | undefined;
|
|
122
|
+
for (const source of this.sources()) {
|
|
123
|
+
if (source.index < cursor) continue;
|
|
124
|
+
const record = parseTerminalResultRecord(source.data);
|
|
125
|
+
if (!record) continue;
|
|
126
|
+
const found = this.lookup("ref", record.ref);
|
|
127
|
+
if (found.kind !== "found" || found.index !== source.index) continue;
|
|
128
|
+
if (results.length === limit) { nextCursor = source.index; break; }
|
|
129
|
+
results.push(summary(record, this.storage(found.retained)));
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, results, ...(nextCursor === undefined ? {} : { nextCursor }), ...this.evidence() };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private saveInput(value: unknown): SaveOutcome {
|
|
135
|
+
const input = parseTerminalResultInput(value);
|
|
136
|
+
if (!input) return this.failure("invalid", "Invalid terminal result projection or metadata limits");
|
|
137
|
+
const existing = this.lookup("originKey", input.originKey);
|
|
138
|
+
if (existing.kind === "ambiguous" || existing.kind === "corrupt") {
|
|
139
|
+
return this.failure(existing.kind, "The origin already has an ambiguous or corrupt result");
|
|
140
|
+
}
|
|
141
|
+
if (existing.kind === "found") {
|
|
142
|
+
const candidate = createTerminalResultRecord(input, existing.record.ref, input.createdAt ?? existing.record.createdAt);
|
|
143
|
+
if (JSON.stringify(parseTerminalResultRecord(candidate)) !== JSON.stringify(existing.record)) {
|
|
144
|
+
return this.failure("conflict", "The immutable origin already belongs to a different terminal payload");
|
|
145
|
+
}
|
|
146
|
+
return this.receipt(existing.record, existing.retained, true);
|
|
147
|
+
}
|
|
148
|
+
let record: TerminalResultRecord;
|
|
149
|
+
try {
|
|
150
|
+
const ref = this.deps.makeRef === undefined ? `result:${randomUUID()}` : this.deps.makeRef();
|
|
151
|
+
if (!isResultRef(ref)) return this.failure("invalid", "Reference factory must return result:<UUID>");
|
|
152
|
+
const collision = this.lookup("ref", ref);
|
|
153
|
+
if (collision.kind !== "missing") return this.failure(collision.kind === "found" ? "conflict" : collision.kind, "Generated result reference is already occupied");
|
|
154
|
+
record = createTerminalResultRecord(input, ref, input.createdAt ?? (this.deps.clock === undefined ? Date.now() : this.deps.clock()));
|
|
155
|
+
} catch (error) { return this.failure("invalid", errorDetail(error)); }
|
|
156
|
+
const cost = payloadBytes(record);
|
|
157
|
+
if (this.chargedEntries >= RESULT_MAX_ENTRIES || this.localBytes + cost > RESULT_MAX_PAYLOAD_BYTES
|
|
158
|
+
|| (this.chargedPayloadBytes !== null && this.chargedPayloadBytes + cost > RESULT_MAX_PAYLOAD_BYTES)) {
|
|
159
|
+
return this.failure("quota", "Terminal result lifetime payload/count quota exhausted; append-only storage is not reclaimed");
|
|
160
|
+
}
|
|
161
|
+
// Reserve before invoking host code. A throw may occur AFTER a physical/session commit.
|
|
162
|
+
// Never retry this origin into another entry or treat memory retention as proof of durability.
|
|
163
|
+
this.chargedEntries++;
|
|
164
|
+
if (this.chargedPayloadBytes !== null) this.chargedPayloadBytes += cost;
|
|
165
|
+
this.localBytes += cost;
|
|
166
|
+
const retained: RetainedResult = { record, storage: "memory-only" };
|
|
167
|
+
this.retained.push(retained);
|
|
168
|
+
if (this.deps.persistent && this.chargedPayloadBytes !== null) {
|
|
169
|
+
try {
|
|
170
|
+
this.deps.append(TERMINAL_RESULT_ENTRY, parseTerminalResultRecord(record)!);
|
|
171
|
+
retained.storage = "session-entry";
|
|
172
|
+
} catch (error) { retained.storageError = errorDetail(error); }
|
|
173
|
+
}
|
|
174
|
+
return this.receipt(record, retained, false);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private storage(retained: RetainedResult | undefined): ResultStorage {
|
|
178
|
+
return retained?.storage ?? (this.deps.persistent ? "session-entry" : "memory-only");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private receipt(record: TerminalResultRecord, retained: RetainedResult | undefined, idempotent: boolean): SaveOutcome {
|
|
182
|
+
const storage = this.storage(retained);
|
|
183
|
+
return {
|
|
184
|
+
ok: true, ref: record.ref, storage, summary: summary(record, storage), idempotent,
|
|
185
|
+
...(retained?.storageError === undefined ? {} : { storageError: retained.storageError }),
|
|
186
|
+
...this.evidence(record, retained),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private hydrate(): void {
|
|
191
|
+
if (this.hydrated) return;
|
|
192
|
+
this.hydrated = true;
|
|
193
|
+
this.hydrating = true;
|
|
194
|
+
if (!this.deps.persistent) this.warnings.add("no-session-storage");
|
|
195
|
+
try {
|
|
196
|
+
const entries = this.deps.entries();
|
|
197
|
+
if (!Array.isArray(entries)) throw new Error("Invalid owning-session entry list");
|
|
198
|
+
this.history = entries;
|
|
199
|
+
this.historyLength = entries.length;
|
|
200
|
+
for (const source of this.sources()) {
|
|
201
|
+
this.chargedEntries++;
|
|
202
|
+
try {
|
|
203
|
+
const cost = payloadBytes(source.data);
|
|
204
|
+
if (this.chargedPayloadBytes !== null) this.chargedPayloadBytes += cost;
|
|
205
|
+
if (!Number.isSafeInteger(this.chargedPayloadBytes)) this.disablePersistence();
|
|
206
|
+
} catch { this.disablePersistence(); }
|
|
207
|
+
if (!parseTerminalResultRecord(source.data)) this.warnings.add("invalid-history");
|
|
208
|
+
}
|
|
209
|
+
this.historyCount = this.chargedEntries;
|
|
210
|
+
} catch { this.disablePersistence(); }
|
|
211
|
+
finally { this.hydrating = false; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private *sources(): Generator<SourceResult> {
|
|
215
|
+
let index = 0;
|
|
216
|
+
for (let i = 0; i < this.historyLength; i++) {
|
|
217
|
+
try {
|
|
218
|
+
const raw = this.history[i];
|
|
219
|
+
if (raw === null || typeof raw !== "object") continue;
|
|
220
|
+
const entry = raw as Record<string, unknown>;
|
|
221
|
+
if (entry.type !== "custom" || entry.customType !== TERMINAL_RESULT_ENTRY) continue;
|
|
222
|
+
// Count the entry even when reading its data throws.
|
|
223
|
+
let data: unknown;
|
|
224
|
+
try { data = entry.data; } catch { this.disablePersistence(); }
|
|
225
|
+
yield { data, index: index++ };
|
|
226
|
+
} catch { this.disablePersistence(); }
|
|
227
|
+
}
|
|
228
|
+
for (const retained of this.retained) yield { data: retained.record, index: index++, retained };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Two bounded-memory scans, not a report cache or an unbounded identity index. */
|
|
232
|
+
private lookup(key: "ref" | "originKey", value: string): Lookup {
|
|
233
|
+
let first: { record: TerminalResultRecord; source: SourceResult } | undefined;
|
|
234
|
+
for (const source of this.sources()) {
|
|
235
|
+
if (identity(source.data)[key] !== value) continue;
|
|
236
|
+
const record = parseTerminalResultRecord(source.data);
|
|
237
|
+
if (!record) return { kind: "corrupt" };
|
|
238
|
+
first = { record, source };
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
if (!first) return { kind: "missing" };
|
|
242
|
+
const canonical = JSON.stringify(first.record);
|
|
243
|
+
for (const source of this.sources()) {
|
|
244
|
+
if (source.index === first.source.index) continue;
|
|
245
|
+
const id = identity(source.data);
|
|
246
|
+
if (id.ref !== first.record.ref && id.originKey !== first.record.originKey) continue;
|
|
247
|
+
const record = parseTerminalResultRecord(source.data);
|
|
248
|
+
if (!record) return { kind: "corrupt" };
|
|
249
|
+
if (JSON.stringify(record) !== canonical) {
|
|
250
|
+
this.warnings.add("ambiguous-history");
|
|
251
|
+
return { kind: "ambiguous" };
|
|
252
|
+
}
|
|
253
|
+
this.warnings.add("duplicate-history");
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
kind: "found", record: first.record, index: first.source.index,
|
|
257
|
+
...(first.source.retained === undefined ? {} : { retained: first.source.retained }),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private disablePersistence(): void {
|
|
262
|
+
this.chargedPayloadBytes = null;
|
|
263
|
+
this.warnings.add("accounting-unavailable");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private evidence(record?: TerminalResultRecord, retained?: RetainedResult): ArchiveEvidence {
|
|
267
|
+
const warnings = new Set(this.warnings);
|
|
268
|
+
if (record?.truncated) warnings.add("body-truncated");
|
|
269
|
+
if (retained?.storageError !== undefined) warnings.add("append-failed");
|
|
270
|
+
return {
|
|
271
|
+
accounting: {
|
|
272
|
+
chargedEntries: this.chargedEntries, chargedPayloadBytes: this.chargedPayloadBytes,
|
|
273
|
+
persistenceDisabled: !this.deps.persistent || this.chargedPayloadBytes === null,
|
|
274
|
+
}, warnings: [...warnings],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private failure(reason: ArchiveFailure["reason"], message: string): ArchiveFailure {
|
|
279
|
+
return { ok: false, reason, message, ...this.evidence() };
|
|
280
|
+
}
|
|
281
|
+
}
|