@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.
@@ -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
+ }
@@ -32,4 +32,3 @@ export interface OsSnapshot {
32
32
  memoryRetrievalResultCount: number;
33
33
  }
34
34
  export declare function rebuildOsSnapshotFromSessionEvents(events: SessionEvent[]): OsSnapshot;
35
- export declare function sessionLogHasRequiredCategories(events: SessionEvent[]): boolean;
@@ -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 blackboard
115
- * continuity comes from the (persistent) `EventStream`. Turn-policy cursor state is not restored.
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
- void this.opts.runGroup.budgetStore.join(this.opts.runGroup.id, { sessionId: personaId, role: spec.role });
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 blackboard
99
- * continuity comes from the (persistent) `EventStream`. Turn-policy cursor state is not restored.
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
- for (const member of await opts.runGroup.budgetStore.members(opts.runGroup.id)) {
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
- this.memberships.get(groupId).set(member.sessionId, member);
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, { sessionId: event.session_id, role: event.role });
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()];
@@ -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 are paged into the
89
- * context's knowledge partition before turn 1, so the model sees them on first call. Returning
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 agent_ids, and
444
- * calls runWorkflow with resumedCompleted so the kernel skips those nodes.
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;