@deepstrike/sdk 0.2.36 → 0.2.38
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/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/os/public.d.ts +1 -1
- package/dist/os/public.js +1 -1
- package/dist/runtime/facade.js +11 -11
- package/dist/runtime/kernel-event-log.d.ts +0 -6
- package/dist/runtime/kernel-event-log.js +57 -62
- package/dist/runtime/kernel-step.d.ts +20 -1
- package/dist/runtime/kernel-step.js +25 -0
- package/dist/runtime/loop-driver.d.ts +108 -0
- package/dist/runtime/loop-driver.js +198 -0
- package/dist/runtime/os-snapshot.d.ts +0 -1
- package/dist/runtime/os-snapshot.js +0 -19
- package/dist/runtime/reactive-session.d.ts +5 -2
- package/dist/runtime/reactive-session.js +17 -4
- package/dist/runtime/run-group.d.ts +9 -0
- package/dist/runtime/run-group.js +18 -4
- package/dist/runtime/runner.d.ts +43 -6
- package/dist/runtime/runner.js +305 -71
- package/dist/runtime/session-log.d.ts +46 -54
- package/dist/runtime/session-repair.d.ts +29 -7
- package/dist/runtime/session-repair.js +37 -9
- package/dist/runtime/sub-agent-orchestrator.d.ts +12 -0
- package/dist/runtime/sub-agent-orchestrator.js +48 -32
- package/dist/runtime/workflow-control-flow.d.ts +10 -2
- package/dist/runtime/workflow-control-flow.js +27 -6
- package/dist/signals/gateway.d.ts +4 -2
- package/dist/signals/gateway.js +8 -1
- package/dist/types/agent.d.ts +28 -1
- package/dist/types/agent.js +25 -1
- package/dist/types.d.ts +50 -0
- package/package.json +2 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/** Fold the loop's session log into resumable pacing state — zero new storage. DW-5: the judge's
|
|
2
|
+
* override budget folds too, so a crash/restart can't grant the verdictFn fresh overrides. */
|
|
3
|
+
export function foldLoopState(events) {
|
|
4
|
+
let roundsCompleted = 0;
|
|
5
|
+
let pendingWakeAtMs;
|
|
6
|
+
let lastPace;
|
|
7
|
+
let overridesUsed = 0;
|
|
8
|
+
for (const { event } of events) {
|
|
9
|
+
if (event.kind === "round_paced") {
|
|
10
|
+
roundsCompleted = Math.max(roundsCompleted, event.round);
|
|
11
|
+
lastPace = { action: event.action, reason: event.reason };
|
|
12
|
+
pendingWakeAtMs = event.action === "sleep" ? event.wake_at_ms : undefined;
|
|
13
|
+
if (event.reason.startsWith("verdict override"))
|
|
14
|
+
overridesUsed += 1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { roundsCompleted, pendingWakeAtMs, lastPace, overridesUsed };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* DW-6 completion→wake bridge, composed from two existing seams (zero new mechanism): a `sleeper`
|
|
21
|
+
* that races the timer against an L0 recipient-addressed signal on the shared gateway. Ingest a
|
|
22
|
+
* signal with `recipient: loopId` (a subagent/workflow completion, a webhook) and the sleeping loop
|
|
23
|
+
* wakes into its next round immediately — where the SAME queued signal then reaches the model
|
|
24
|
+
* through the kernel's normal signal path, so the wake reason is visible in-round.
|
|
25
|
+
*/
|
|
26
|
+
export function signalAwareSleeper(gateway, loopId) {
|
|
27
|
+
return (delayMs) => new Promise(resolve => {
|
|
28
|
+
let settled = false;
|
|
29
|
+
const settle = (v) => {
|
|
30
|
+
if (settled)
|
|
31
|
+
return;
|
|
32
|
+
settled = true;
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
unsubscribe();
|
|
35
|
+
resolve(v);
|
|
36
|
+
};
|
|
37
|
+
const unsubscribe = gateway.onSignal(sig => {
|
|
38
|
+
if (sig.recipient === loopId)
|
|
39
|
+
settle(true);
|
|
40
|
+
});
|
|
41
|
+
const timer = setTimeout(() => settle(true), Math.max(0, delayMs));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export class LoopDriver {
|
|
45
|
+
runner;
|
|
46
|
+
spec;
|
|
47
|
+
overridesUsed = 0;
|
|
48
|
+
constructor(runner, spec) {
|
|
49
|
+
this.runner = runner;
|
|
50
|
+
this.spec = spec;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Drive rounds until the loop stops or goes dormant. Resumable by construction:
|
|
54
|
+
* the round count and any pending wake are folded from the session log, so
|
|
55
|
+
* calling `run()` again after a crash / on a stateless host continues in place.
|
|
56
|
+
*/
|
|
57
|
+
async run() {
|
|
58
|
+
const { loopId } = this.spec;
|
|
59
|
+
const log = this.runner.hostOptions.sessionLog;
|
|
60
|
+
// Resume: fold prior rounds + pending wake + the judge's used overrides from the transcript
|
|
61
|
+
// (DW-5: a crash/restart must not refill the verdictFn's override budget).
|
|
62
|
+
const prior = foldLoopState(await log.read(loopId));
|
|
63
|
+
let round = prior.roundsCompleted;
|
|
64
|
+
this.overridesUsed = Math.max(this.overridesUsed, prior.overridesUsed);
|
|
65
|
+
if (prior.pendingWakeAtMs !== undefined) {
|
|
66
|
+
const remaining = prior.pendingWakeAtMs - Date.now();
|
|
67
|
+
if (remaining > 0) {
|
|
68
|
+
const slept = await this.sleep(remaining, prior.pendingWakeAtMs);
|
|
69
|
+
if (!slept) {
|
|
70
|
+
return {
|
|
71
|
+
loopId, roundsCompleted: round, stopped: false,
|
|
72
|
+
state: "dormant", wakeAtMs: prior.pendingWakeAtMs,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let feedback;
|
|
78
|
+
for (;;) {
|
|
79
|
+
round += 1;
|
|
80
|
+
// Driver-side round-cap backstop: with a RunGroup, the kernel trap coerces via the
|
|
81
|
+
// seeded ledger; without one, this is the only max_rounds enforcement point.
|
|
82
|
+
if (this.spec.maxRounds !== undefined && round > this.spec.maxRounds) {
|
|
83
|
+
return {
|
|
84
|
+
loopId, roundsCompleted: round - 1, stopped: true, state: "stopped",
|
|
85
|
+
lastPace: { action: "stop", reason: `max_rounds=${this.spec.maxRounds} exhausted` },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
await log.append(loopId, { kind: "round_started", round, goal: this.spec.goal });
|
|
89
|
+
const goal = feedback
|
|
90
|
+
? `${this.spec.goal}\n\n[LOOP FEEDBACK round ${round - 1}] ${feedback}`
|
|
91
|
+
: this.spec.goal;
|
|
92
|
+
feedback = undefined;
|
|
93
|
+
let pace;
|
|
94
|
+
let status;
|
|
95
|
+
// ONE round = one bounded kernel run under the stable loop session id. The
|
|
96
|
+
// kernel's pacing trap adjudicates the model's pace proposal; we consume it
|
|
97
|
+
// from the done event. runSpec.loopRound arms the trap + the pace tool.
|
|
98
|
+
const priorRunSpec = this.runner.hostOptions.runSpec;
|
|
99
|
+
this.runner.hostOptions.runSpec = {
|
|
100
|
+
identity: { agentId: this.runner.hostOptions.agentId ?? "loop", sessionId: loopId, isSubAgent: false },
|
|
101
|
+
role: "custom",
|
|
102
|
+
goal,
|
|
103
|
+
...(priorRunSpec ?? {}),
|
|
104
|
+
loopRound: {
|
|
105
|
+
maxRounds: this.spec.maxRounds,
|
|
106
|
+
minSleepMs: this.spec.minSleepMs,
|
|
107
|
+
maxSleepMs: this.spec.maxSleepMs,
|
|
108
|
+
defaultAction: this.spec.defaultAction,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
// With a RunGroup configured, run() seeds the kernel trap's round base from the
|
|
112
|
+
// group ledger (the driver charges rounds:1 per round below) — max_rounds coercion
|
|
113
|
+
// then happens IN-KERNEL; the check above is the ungrouped backstop.
|
|
114
|
+
try {
|
|
115
|
+
for await (const evt of this.runner.run({
|
|
116
|
+
sessionId: loopId,
|
|
117
|
+
goal,
|
|
118
|
+
criteria: this.spec.criteria,
|
|
119
|
+
})) {
|
|
120
|
+
this.spec.onEvent?.(round, evt);
|
|
121
|
+
if (evt.type === "done") {
|
|
122
|
+
const d = evt;
|
|
123
|
+
status = d.status;
|
|
124
|
+
pace = d.paceDecision;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
this.runner.hostOptions.runSpec = priorRunSpec;
|
|
130
|
+
}
|
|
131
|
+
// Missing pace (old kernel / hard failure): stop and surface — nothing nags.
|
|
132
|
+
const decision = pace ?? {
|
|
133
|
+
action: "stop",
|
|
134
|
+
reason: `round ended without a pace decision (status: ${status ?? "unknown"})`,
|
|
135
|
+
};
|
|
136
|
+
// Cross-round done-gate: a stop proposal may be overridden K times by the judge.
|
|
137
|
+
let finalDecision = decision;
|
|
138
|
+
if (finalDecision.action === "stop"
|
|
139
|
+
&& this.spec.verdictFn
|
|
140
|
+
&& this.overridesUsed < (this.spec.maxVerdictOverrides ?? 2)) {
|
|
141
|
+
try {
|
|
142
|
+
const verdict = await this.spec.verdictFn({ loopId, round, reason: finalDecision.reason });
|
|
143
|
+
if (!verdict.pass) {
|
|
144
|
+
this.overridesUsed += 1;
|
|
145
|
+
feedback = verdict.feedback ?? "verdict failed — keep iterating on the goal";
|
|
146
|
+
finalDecision = {
|
|
147
|
+
action: "continue",
|
|
148
|
+
reason: `verdict override ${this.overridesUsed}: ${verdict.feedback ?? "not done yet"}`,
|
|
149
|
+
coercedFrom: `stop (${finalDecision.reason})`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { /* judge errs-open: the stop stands */ }
|
|
154
|
+
}
|
|
155
|
+
const wakeAtMs = finalDecision.action === "sleep"
|
|
156
|
+
? Date.now() + (finalDecision.delayMs ?? 60_000)
|
|
157
|
+
: undefined;
|
|
158
|
+
await log.append(loopId, {
|
|
159
|
+
kind: "round_paced",
|
|
160
|
+
round,
|
|
161
|
+
action: finalDecision.action,
|
|
162
|
+
...(finalDecision.delayMs !== undefined ? { delay_ms: finalDecision.delayMs } : {}),
|
|
163
|
+
...(wakeAtMs !== undefined ? { wake_at_ms: wakeAtMs } : {}),
|
|
164
|
+
reason: finalDecision.reason,
|
|
165
|
+
...(finalDecision.coercedFrom ? { coerced_from: finalDecision.coercedFrom } : {}),
|
|
166
|
+
});
|
|
167
|
+
// Lifetime governance: one round = one group charge on the rounds axis.
|
|
168
|
+
const group = this.runner.hostOptions.runGroup;
|
|
169
|
+
if (group)
|
|
170
|
+
await group.budgetStore.charge(group.id, { rounds: 1 });
|
|
171
|
+
if (finalDecision.action === "stop") {
|
|
172
|
+
return {
|
|
173
|
+
loopId, roundsCompleted: round, stopped: true, state: "stopped",
|
|
174
|
+
lastPace: finalDecision, lastStatus: status,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (finalDecision.action === "sleep" && wakeAtMs !== undefined) {
|
|
178
|
+
const slept = await this.sleep(wakeAtMs - Date.now(), wakeAtMs);
|
|
179
|
+
if (!slept) {
|
|
180
|
+
return {
|
|
181
|
+
loopId, roundsCompleted: round, stopped: false, state: "dormant",
|
|
182
|
+
lastPace: finalDecision, lastStatus: status, wakeAtMs,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// continue → next round immediately
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
sleep(delayMs, wakeAtMs) {
|
|
190
|
+
if (this.spec.sleeper)
|
|
191
|
+
return Promise.resolve(this.spec.sleeper(delayMs, wakeAtMs));
|
|
192
|
+
return new Promise(resolve => setTimeout(() => resolve(true), Math.max(0, delayMs)));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Facade: run a self-pacing loop agent (joins runAgent/runFanout as an entry point). */
|
|
196
|
+
export async function runLoop(runner, spec) {
|
|
197
|
+
return new LoopDriver(runner, spec).run();
|
|
198
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { categoryForKind, primitiveForKind } from "./kernel-event-log.js";
|
|
2
1
|
const KERNEL_KINDS = new Set([
|
|
3
2
|
"compressed",
|
|
4
3
|
"page_out",
|
|
@@ -16,7 +15,6 @@ const KERNEL_KINDS = new Set([
|
|
|
16
15
|
"agent_process_changed",
|
|
17
16
|
"milestone_advanced",
|
|
18
17
|
"milestone_blocked",
|
|
19
|
-
"milestone_evidence",
|
|
20
18
|
"memory_written",
|
|
21
19
|
"memory_queried",
|
|
22
20
|
"memory_validation_failed",
|
|
@@ -109,20 +107,3 @@ export function rebuildOsSnapshotFromSessionEvents(events) {
|
|
|
109
107
|
}
|
|
110
108
|
return snap;
|
|
111
109
|
}
|
|
112
|
-
export function sessionLogHasRequiredCategories(events) {
|
|
113
|
-
for (const event of events) {
|
|
114
|
-
if (!KERNEL_KINDS.has(event.kind))
|
|
115
|
-
continue;
|
|
116
|
-
const cat = event.category;
|
|
117
|
-
if (!cat)
|
|
118
|
-
return false;
|
|
119
|
-
if (cat !== categoryForKind(event.kind))
|
|
120
|
-
return false;
|
|
121
|
-
const prim = event.primitive;
|
|
122
|
-
if (prim !== undefined) {
|
|
123
|
-
if (prim !== primitiveForKind(event.kind))
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
@@ -111,8 +111,11 @@ export declare class ReactiveSession {
|
|
|
111
111
|
private getRunner;
|
|
112
112
|
private driveTurn;
|
|
113
113
|
/**
|
|
114
|
-
* Rebuild a session from a persisted `RunGroup`: load its members (lineage) as peers. The
|
|
115
|
-
* continuity comes from the (persistent) `EventStream`. Turn-policy cursor state is not
|
|
114
|
+
* Rebuild a session from a persisted `RunGroup`: load its PEER members (lineage) as peers. The
|
|
115
|
+
* blackboard continuity comes from the (persistent) `EventStream`. Turn-policy cursor state is not
|
|
116
|
+
* restored. W-N5: vehicle members (workflow envelopes, `wf-node*` children, loop iterations) share
|
|
117
|
+
* the governance domain but are NOT personas — resuming them as peers would resurrect phantoms.
|
|
118
|
+
* A legacy membership with no kind tags falls back to resuming every member.
|
|
116
119
|
*/
|
|
117
120
|
static resume(opts: ReactiveSessionOptions & {
|
|
118
121
|
peerSpecs?: Record<string, ReactivePeerSpec>;
|
|
@@ -17,7 +17,13 @@ export class ReactiveSession {
|
|
|
17
17
|
/** Register a peer persona and record it in the group membership (lineage). */
|
|
18
18
|
addPeer(personaId, spec = {}) {
|
|
19
19
|
this.peerSpecs.set(personaId, spec);
|
|
20
|
-
|
|
20
|
+
// W-N5: tagged "peer" so resume() can tell personas apart from vehicle sessions (workflow
|
|
21
|
+
// envelopes / wf-node children / loop iterations) that share the same governance domain.
|
|
22
|
+
void this.opts.runGroup.budgetStore.join(this.opts.runGroup.id, {
|
|
23
|
+
sessionId: personaId,
|
|
24
|
+
role: spec.role,
|
|
25
|
+
kind: "peer",
|
|
26
|
+
});
|
|
21
27
|
}
|
|
22
28
|
peers() {
|
|
23
29
|
return [...this.peerSpecs.keys()];
|
|
@@ -95,12 +101,19 @@ export class ReactiveSession {
|
|
|
95
101
|
return collectText(runner.run({ sessionId: personaId, goal }));
|
|
96
102
|
}
|
|
97
103
|
/**
|
|
98
|
-
* Rebuild a session from a persisted `RunGroup`: load its members (lineage) as peers. The
|
|
99
|
-
* continuity comes from the (persistent) `EventStream`. Turn-policy cursor state is not
|
|
104
|
+
* Rebuild a session from a persisted `RunGroup`: load its PEER members (lineage) as peers. The
|
|
105
|
+
* blackboard continuity comes from the (persistent) `EventStream`. Turn-policy cursor state is not
|
|
106
|
+
* restored. W-N5: vehicle members (workflow envelopes, `wf-node*` children, loop iterations) share
|
|
107
|
+
* the governance domain but are NOT personas — resuming them as peers would resurrect phantoms.
|
|
108
|
+
* A legacy membership with no kind tags falls back to resuming every member.
|
|
100
109
|
*/
|
|
101
110
|
static async resume(opts) {
|
|
102
111
|
const session = new ReactiveSession(opts);
|
|
103
|
-
|
|
112
|
+
const members = await opts.runGroup.budgetStore.members(opts.runGroup.id);
|
|
113
|
+
const anyTagged = members.some(m => m.kind !== undefined);
|
|
114
|
+
for (const member of members) {
|
|
115
|
+
if (anyTagged && member.kind !== "peer")
|
|
116
|
+
continue;
|
|
104
117
|
session.peerSpecs.set(member.sessionId, opts.peerSpecs?.[member.sessionId] ?? { role: member.role });
|
|
105
118
|
}
|
|
106
119
|
return session;
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import type { SessionLog } from "./session-log.js";
|
|
19
19
|
/** Cumulative resources spent across a run group. */
|
|
20
20
|
export interface GroupLedger {
|
|
21
|
+
/** ③ loop-agent rounds completed across the group (seeds the pacing trap's max_rounds). */
|
|
22
|
+
roundsCompleted?: number;
|
|
21
23
|
/** Total tokens spent by all members. */
|
|
22
24
|
tokensSpent: number;
|
|
23
25
|
/** Total sub-agents spawned by all members (running + completed). */
|
|
@@ -27,11 +29,18 @@ export interface GroupLedger {
|
|
|
27
29
|
export interface GroupCharge {
|
|
28
30
|
tokens?: number;
|
|
29
31
|
subagents?: number;
|
|
32
|
+
/** ③ loop-agent: completed rounds to add to the group's round count. */
|
|
33
|
+
rounds?: number;
|
|
30
34
|
}
|
|
31
35
|
/** A persona session that participated in the logical run (process-table lineage). */
|
|
32
36
|
export interface GroupMember {
|
|
33
37
|
sessionId: string;
|
|
34
38
|
role?: string;
|
|
39
|
+
/** W-N5: what this member IS in the lineage — a `"peer"` persona (ReactiveSession.addPeer) vs a
|
|
40
|
+
* `"vehicle"` session (run()/runWorkflow envelopes, workflow-node children, loop iterations).
|
|
41
|
+
* `ReactiveSession.resume()` rebuilds the peer set from `"peer"` members only, so DAG-in-Peer
|
|
42
|
+
* usage can't resurrect phantom `wf-node*` personas. Absent (legacy) = unknown. */
|
|
43
|
+
kind?: "peer" | "vehicle";
|
|
35
44
|
}
|
|
36
45
|
export interface GroupBudgetStore {
|
|
37
46
|
/** Cumulative spend across the group so far. */
|
|
@@ -3,19 +3,25 @@ export class InMemoryGroupBudgetStore {
|
|
|
3
3
|
ledgers = new Map();
|
|
4
4
|
memberships = new Map();
|
|
5
5
|
read(groupId) {
|
|
6
|
-
return this.ledgers.get(groupId) ?? { tokensSpent: 0, subagentsSpawned: 0 };
|
|
6
|
+
return this.ledgers.get(groupId) ?? { tokensSpent: 0, subagentsSpawned: 0, roundsCompleted: 0 };
|
|
7
7
|
}
|
|
8
8
|
charge(groupId, delta) {
|
|
9
9
|
const cur = this.read(groupId);
|
|
10
10
|
this.ledgers.set(groupId, {
|
|
11
11
|
tokensSpent: cur.tokensSpent + Math.max(0, delta.tokens ?? 0),
|
|
12
12
|
subagentsSpawned: cur.subagentsSpawned + Math.max(0, delta.subagents ?? 0),
|
|
13
|
+
roundsCompleted: (cur.roundsCompleted ?? 0) + Math.max(0, delta.rounds ?? 0),
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
join(groupId, member) {
|
|
16
17
|
if (!this.memberships.has(groupId))
|
|
17
18
|
this.memberships.set(groupId, new Map());
|
|
18
|
-
|
|
19
|
+
// First join wins (idempotent by sessionId) — the same contract as SessionLogGroupBudgetStore.
|
|
20
|
+
// A persona registered as "peer" then re-joining through its own run() as "vehicle" must not
|
|
21
|
+
// lose its peer tag (W-N5), and the two stores must agree on which record survives.
|
|
22
|
+
const members = this.memberships.get(groupId);
|
|
23
|
+
if (!members.has(member.sessionId))
|
|
24
|
+
members.set(member.sessionId, member);
|
|
19
25
|
}
|
|
20
26
|
members(groupId) {
|
|
21
27
|
return [...(this.memberships.get(groupId)?.values() ?? [])];
|
|
@@ -34,17 +40,20 @@ export class SessionLogGroupBudgetStore {
|
|
|
34
40
|
async read(groupId) {
|
|
35
41
|
let tokensSpent = 0;
|
|
36
42
|
let subagentsSpawned = 0;
|
|
43
|
+
let roundsCompleted = 0;
|
|
37
44
|
for (const { event } of await this.log.read(groupId)) {
|
|
38
45
|
if (event.kind === "group_budget_charged") {
|
|
46
|
+
roundsCompleted += event.rounds ?? 0;
|
|
39
47
|
tokensSpent += event.tokens;
|
|
40
48
|
subagentsSpawned += event.subagents;
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
|
-
return { tokensSpent, subagentsSpawned };
|
|
51
|
+
return { tokensSpent, subagentsSpawned, roundsCompleted };
|
|
44
52
|
}
|
|
45
53
|
async charge(groupId, delta) {
|
|
46
54
|
await this.log.append(groupId, {
|
|
47
55
|
kind: "group_budget_charged",
|
|
56
|
+
...(delta.rounds !== undefined ? { rounds: delta.rounds } : {}),
|
|
48
57
|
tokens: Math.max(0, delta.tokens ?? 0),
|
|
49
58
|
subagents: Math.max(0, delta.subagents ?? 0),
|
|
50
59
|
});
|
|
@@ -58,13 +67,18 @@ export class SessionLogGroupBudgetStore {
|
|
|
58
67
|
kind: "group_member_joined",
|
|
59
68
|
session_id: member.sessionId,
|
|
60
69
|
...(member.role ? { role: member.role } : {}),
|
|
70
|
+
...(member.kind ? { member_kind: member.kind } : {}),
|
|
61
71
|
});
|
|
62
72
|
}
|
|
63
73
|
async members(groupId) {
|
|
64
74
|
const seen = new Map();
|
|
65
75
|
for (const { event } of await this.log.read(groupId)) {
|
|
66
76
|
if (event.kind === "group_member_joined") {
|
|
67
|
-
seen.set(event.session_id, {
|
|
77
|
+
seen.set(event.session_id, {
|
|
78
|
+
sessionId: event.session_id,
|
|
79
|
+
role: event.role,
|
|
80
|
+
...(event.member_kind ? { kind: event.member_kind } : {}),
|
|
81
|
+
});
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
84
|
return [...seen.values()];
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LLMProvider, Message, ContentPart, ToolSchema, StreamEvent, ToolSuspendEvent, PermissionRequestEvent, PermissionResponse, AsyncSummarizer, DreamSummarizer } from "../types.js";
|
|
1
|
+
import type { LLMProvider, Message, ContentPart, ToolSchema, StreamEvent, ToolSuspendEvent, PermissionRequestEvent, PermissionResponse, AsyncSummarizer, DreamSummarizer, EntropySample, EntropyWatchOptions } from "../types.js";
|
|
2
2
|
import type { DreamStore, MemoryEntry, MemoryQuery, MemoryWriteRequest } from "../memory/protocols.js";
|
|
3
3
|
import type { KnowledgeSource } from "../knowledge/source.js";
|
|
4
4
|
import type { SignalSource, RuntimeSignalUrgency } from "../signals/types.js";
|
|
@@ -7,6 +7,7 @@ import type { ArchiveStore } from "./archive.js";
|
|
|
7
7
|
import type { ExecutionPlane } from "./execution-plane.js";
|
|
8
8
|
import type { RunGroup } from "./run-group.js";
|
|
9
9
|
import { type MemoryPolicy, type ResourceQuota } from "../kernel.js";
|
|
10
|
+
import { type RecoveredNodeCompletion } from "./session-repair.js";
|
|
10
11
|
import type { AgentRunSpec, MilestoneCheckResult, MilestoneContract, MilestonePolicy, WorkflowSpec } from "../types/agent.js";
|
|
11
12
|
import { type SubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
12
13
|
import { type ReducerRegistry } from "./reducers.js";
|
|
@@ -85,10 +86,11 @@ export interface RuntimeOptions {
|
|
|
85
86
|
agentId?: string;
|
|
86
87
|
/** I4: optional run-start memory pre-fetch hook. The runner calls this ONCE per run, before the
|
|
87
88
|
* first LLM turn, with the request's goal and (optional) run-spec. Each returned query string
|
|
88
|
-
* becomes a `dreamStore.search(agentId, q, 5)` and the resulting hits
|
|
89
|
-
*
|
|
89
|
+
* becomes a `dreamStore.search(agentId, q, 5)` and the resulting hits land in decaying
|
|
90
|
+
* HISTORY as an ordinary user turn before turn 1 (single-use retrieval content — never a
|
|
91
|
+
* permanent knowledge pin; `initialMemory` is the curated CLAUDE.md-analog seed). Returning
|
|
90
92
|
* `undefined` / empty array is a no-op. Requires `dreamStore` + `agentId`; missing either ⇒
|
|
91
|
-
* silently skipped (errs-open). Bench memory-recall shows -57% turns / -55% dollars when
|
|
93
|
+
* silently skipped (errs-open). Default when unset: one query = the run goal (P10). Bench memory-recall shows -57% turns / -55% dollars when
|
|
92
94
|
* relevant memories land on turn 1 instead of being discovered via the meta-tool on turn 3+. */
|
|
93
95
|
preQueryMemory?: (ctx: {
|
|
94
96
|
goal: string;
|
|
@@ -159,6 +161,14 @@ export interface RuntimeOptions {
|
|
|
159
161
|
* entries and skill pins are never budget-evicted. `0` disables. Default: kernel's 0.25.
|
|
160
162
|
*/
|
|
161
163
|
knowledgeBudgetRatio?: number;
|
|
164
|
+
/**
|
|
165
|
+
* Opt-in kernel entropy watch: threshold alerting over the per-turn session-entropy score
|
|
166
|
+
* (`entropy_sample` events stream unconditionally regardless). When the score crosses
|
|
167
|
+
* `threshold` — armed via hysteresis and past the cooldown — the run emits an `entropy_alert`
|
|
168
|
+
* stream event (and session-log record); with `notifyModel` the kernel also feeds the model a
|
|
169
|
+
* durable `[SIGNAL]` directive. Absent ⇒ disabled (kernel default).
|
|
170
|
+
*/
|
|
171
|
+
entropyWatch?: EntropyWatchOptions;
|
|
162
172
|
/**
|
|
163
173
|
* K3: default lease (in turns) for every skill activation. After that many turns the kernel
|
|
164
174
|
* auto-deactivates the skill — toolset re-widens, knowledge pin boundary-swept — exactly like
|
|
@@ -305,6 +315,8 @@ export declare class RuntimeRunner {
|
|
|
305
315
|
* at the next safe point (after the tool turn resolves, kernel back in Reason — not suspended). */
|
|
306
316
|
private pendingAuthoredWorkflows;
|
|
307
317
|
private dashboard;
|
|
318
|
+
/** Most recent kernel entropy sample of the active/last run (see `latestEntropy`). */
|
|
319
|
+
private lastEntropySample;
|
|
308
320
|
constructor(opts: RuntimeOptions);
|
|
309
321
|
/** Host configuration (for coordinator / sub-agent spawn). */
|
|
310
322
|
get hostOptions(): RuntimeOptions;
|
|
@@ -380,7 +392,15 @@ export declare class RuntimeRunner {
|
|
|
380
392
|
*/
|
|
381
393
|
runWorkflow(spec: WorkflowSpec, opts?: {
|
|
382
394
|
resumedCompleted?: string[];
|
|
395
|
+
/** W-1: recovered completions WITH control signals (classify branch / loop stop) — lowered to
|
|
396
|
+
* the kernel's `resumed_results` so control flow replays faithfully. Supersedes
|
|
397
|
+
* `resumedCompleted` for ids present in both. */
|
|
398
|
+
resumedResults?: RecoveredNodeCompletion[];
|
|
383
399
|
resumedSubmissions?: Record<string, unknown>[][];
|
|
400
|
+
/** R3-1: original base index per submission batch (parallel to resumedSubmissions). */
|
|
401
|
+
resumedSubmissionBases?: number[];
|
|
402
|
+
/** W-1: recovered node outputs (agent id → output text) to pre-seed the driver's outputs map. */
|
|
403
|
+
resumedOutputs?: Map<string, string>;
|
|
384
404
|
/** Standalone session id when bootstrapping (no active parent run). Defaults to a fresh uuid. */
|
|
385
405
|
sessionId?: string;
|
|
386
406
|
}): Promise<{
|
|
@@ -440,8 +460,9 @@ export declare class RuntimeRunner {
|
|
|
440
460
|
private driveWorkflow;
|
|
441
461
|
/**
|
|
442
462
|
* Resume a workflow from the parent session's completed nodes.
|
|
443
|
-
* Reads the session log, extracts completed workflow node
|
|
444
|
-
* calls runWorkflow
|
|
463
|
+
* Reads the session log, extracts completed workflow node records (with their W-1 control
|
|
464
|
+
* signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
|
|
465
|
+
* flow (classify prune / loop stop), and the driver re-seeds its outputs map.
|
|
445
466
|
*/
|
|
446
467
|
resumeWorkflow(spec: WorkflowSpec, opts?: {
|
|
447
468
|
sessionId?: string;
|
|
@@ -457,6 +478,10 @@ export declare class RuntimeRunner {
|
|
|
457
478
|
* without wiring a full `SignalSource`. `urgency` maps to the kernel disposition ladder: `"normal"`
|
|
458
479
|
* queues for the next boundary (default), `"high"` soft-interrupts, `"critical"` preempts. */
|
|
459
480
|
injectNote(text: string, urgency?: RuntimeSignalUrgency): void;
|
|
481
|
+
/** The most recent kernel session-entropy sample (one per completed turn), or `null` before the
|
|
482
|
+
* first boundary. A pull companion to the streamed `entropy_sample` events — hosts polling from
|
|
483
|
+
* outside the stream (e.g. a heartbeat supervisor) read the latest measurement here. */
|
|
484
|
+
latestEntropy(): EntropySample | null;
|
|
460
485
|
/** Injected-note drain shared by the main loop's per-turn poll: injected notes first (FIFO), then
|
|
461
486
|
* the configured `signalSource`. Keeps the two inbound channels on one code path so they never drift. */
|
|
462
487
|
private nextInboundSignal;
|
|
@@ -497,6 +522,18 @@ export declare class RuntimeRunner {
|
|
|
497
522
|
private archiveSemanticPageOut;
|
|
498
523
|
private upgradeCompressedSummary;
|
|
499
524
|
}
|
|
525
|
+
/** Kernel-consumed meta-tools (e.g. `pace`) are answered by a synthetic tool result the kernel keeps
|
|
526
|
+
* in its OWN history but never emits as a `tool_completed` session event (they never reach the
|
|
527
|
+
* execution plane). On replay that leaves an assistant `tool_call` with no following tool result —
|
|
528
|
+
* which strict OpenAI-compatible providers reject ("every tool_call must be answered by a tool
|
|
529
|
+
* message"). This pass re-pairs any such orphan by inserting a synthetic tool-result message right
|
|
530
|
+
* after its assistant message, reproducing the pair the kernel had all along.
|
|
531
|
+
*
|
|
532
|
+
* Discriminator: only pair an orphan when the run **continued past it** — i.e. a later non-tool
|
|
533
|
+
* message exists. A tail assistant tool_call with nothing after it is a genuinely PENDING tool the
|
|
534
|
+
* run stopped in front of (the wake/recovery case), which must stay unpaired so wake executes it.
|
|
535
|
+
* Pure. */
|
|
536
|
+
export declare function pairOrphanToolCalls(messages: Message[]): Message[];
|
|
500
537
|
export declare function replayMessages(events: Array<{
|
|
501
538
|
seq: number;
|
|
502
539
|
event: SessionEvent;
|