@deepstrike/sdk 0.2.35 → 0.2.37

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.
Files changed (35) hide show
  1. package/README.md +6 -6
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +2 -0
  4. package/dist/os/public.d.ts +1 -1
  5. package/dist/os/public.js +1 -1
  6. package/dist/runtime/facade.js +11 -11
  7. package/dist/runtime/kernel-event-log.d.ts +0 -6
  8. package/dist/runtime/kernel-event-log.js +38 -62
  9. package/dist/runtime/kernel-step.d.ts +11 -0
  10. package/dist/runtime/kernel-step.js +12 -0
  11. package/dist/runtime/large-result-spool.d.ts +7 -0
  12. package/dist/runtime/large-result-spool.js +25 -0
  13. package/dist/runtime/loop-driver.d.ts +108 -0
  14. package/dist/runtime/loop-driver.js +198 -0
  15. package/dist/runtime/os-snapshot.d.ts +0 -1
  16. package/dist/runtime/os-snapshot.js +0 -19
  17. package/dist/runtime/reactive-session.d.ts +5 -2
  18. package/dist/runtime/reactive-session.js +17 -4
  19. package/dist/runtime/run-group.d.ts +9 -0
  20. package/dist/runtime/run-group.js +18 -4
  21. package/dist/runtime/runner.d.ts +151 -12
  22. package/dist/runtime/runner.js +568 -155
  23. package/dist/runtime/session-log.d.ts +31 -54
  24. package/dist/runtime/session-repair.d.ts +29 -7
  25. package/dist/runtime/session-repair.js +37 -9
  26. package/dist/runtime/sub-agent-orchestrator.d.ts +12 -0
  27. package/dist/runtime/sub-agent-orchestrator.js +54 -30
  28. package/dist/runtime/workflow-control-flow.d.ts +10 -2
  29. package/dist/runtime/workflow-control-flow.js +27 -6
  30. package/dist/signals/gateway.d.ts +4 -2
  31. package/dist/signals/gateway.js +8 -1
  32. package/dist/types/agent.d.ts +40 -2
  33. package/dist/types/agent.js +25 -1
  34. package/dist/types.d.ts +2 -0
  35. 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
+ }
@@ -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,12 +1,13 @@
1
1
  import type { LLMProvider, Message, ContentPart, ToolSchema, StreamEvent, ToolSuspendEvent, PermissionRequestEvent, PermissionResponse, AsyncSummarizer, DreamSummarizer } 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
- import type { SignalSource } from "../signals/types.js";
4
+ import type { SignalSource, RuntimeSignalUrgency } from "../signals/types.js";
5
5
  import type { SessionLog, SessionEvent } from "./session-log.js";
6
6
  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";
@@ -50,6 +51,18 @@ export interface TurnMetrics {
50
51
  /** Tokens written to the prompt cache this turn (Anthropic `cache_creation_input_tokens`). */
51
52
  cacheCreationTokens: number;
52
53
  }
54
+ /** O5: decision returned by `onToolCall` — `block: true` denies this call before it executes; the
55
+ * `reason` is fed back to the model as a governance-denied tool result (so it can redirect). */
56
+ export interface ToolCallHookDecision {
57
+ block?: boolean;
58
+ reason?: string;
59
+ }
60
+ /** O5: decision returned by `onToolResult` — `replaceOutput` swaps the result the model (and the
61
+ * session log) sees; `note` is injected into the signal stream (see `injectNote`). */
62
+ export interface ToolResultHookDecision {
63
+ replaceOutput?: string;
64
+ note?: string;
65
+ }
53
66
  export interface RuntimeOptions {
54
67
  provider: LLMProvider;
55
68
  /** M4/G5: cumulative token cap for this run (the kernel's `max_total_tokens`). A workflow node's
@@ -73,14 +86,19 @@ export interface RuntimeOptions {
73
86
  agentId?: string;
74
87
  /** I4: optional run-start memory pre-fetch hook. The runner calls this ONCE per run, before the
75
88
  * first LLM turn, with the request's goal and (optional) run-spec. Each returned query string
76
- * becomes a `dreamStore.search(agentId, q, 5)` and the resulting hits are paged into the
77
- * 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
78
92
  * `undefined` / empty array is a no-op. Requires `dreamStore` + `agentId`; missing either ⇒
79
- * 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
80
94
  * relevant memories land on turn 1 instead of being discovered via the meta-tool on turn 3+. */
81
95
  preQueryMemory?: (ctx: {
82
96
  goal: string;
83
97
  runSpec?: AgentRunSpec;
98
+ /** K4: `"initial"` = the once-per-run pre-turn-1 fetch; `"renewal"` = re-fired after a sprint
99
+ * renewal (renewal drops the old history INCLUDING earlier memory hits, so the new sprint
100
+ * gets a fresh recall pass). Hooks that ignore it keep the pre-K4 behavior. */
101
+ phase?: "initial" | "renewal";
84
102
  }) => Promise<string[] | undefined> | string[] | undefined;
85
103
  systemPrompt?: string;
86
104
  initialMemory?: string[];
@@ -118,6 +136,63 @@ export interface RuntimeOptions {
118
136
  * and memory-write syscalls are admitted unconditionally (pre-M2 behavior).
119
137
  */
120
138
  resourceQuota?: ResourceQuota;
139
+ /**
140
+ * O6: the in-kernel repeat fuse — the hard rungs above the soft no-progress STOP. When the model
141
+ * re-issues the IDENTICAL tool call (same name AND args) `denyAfter` turns in a row, the kernel
142
+ * denies it and feeds a directive note back; at `terminateAfter` the run ends `no_progress`.
143
+ * Same-tool/different-args loops never trip it. Defaults: enabled, denyAfter 5, terminateAfter 8.
144
+ * Pass `false` to disable (e.g. legit fixed-argument polling loops).
145
+ */
146
+ repeatFuse?: {
147
+ denyAfter?: number;
148
+ terminateAfter?: number;
149
+ } | false;
150
+ /**
151
+ * O4: the turn-end criteria gate (the Stop-hook analog). When the model tries to finish while the
152
+ * run's `criteria` stand, the kernel injects ONE self-check turn ("verify each criterion; continue
153
+ * if any is unmet") before accepting completion. Fires at most once per run; runs without criteria
154
+ * are untouched. Default enabled — set `false` to accept the first finish unconditionally.
155
+ */
156
+ criteriaGate?: boolean;
157
+ /**
158
+ * K2: max share of `maxTokens` the durable knowledge partition may occupy. Exceeding it emits a
159
+ * `knowledge_budget_exceeded` observation (once per cache generation) and evicts the OLDEST
160
+ * unpinned, non-skill entries at the next compaction/renewal boundary until usage fits. Pinned
161
+ * entries and skill pins are never budget-evicted. `0` disables. Default: kernel's 0.25.
162
+ */
163
+ knowledgeBudgetRatio?: number;
164
+ /**
165
+ * K3: default lease (in turns) for every skill activation. After that many turns the kernel
166
+ * auto-deactivates the skill — toolset re-widens, knowledge pin boundary-swept — exactly like
167
+ * an explicit `deactivateSkill()`. Absent ⇒ activations are permanent (default). A repeat
168
+ * `skill(name)` call refreshes the lease.
169
+ */
170
+ skillLeaseTurns?: number;
171
+ /**
172
+ * O5 (the PreToolUse-hook analog): called for each kernel-APPROVED tool call just before it
173
+ * executes. Return `{ block: true, reason }` to veto — the call never runs and the reason is fed
174
+ * back to the model as a denied tool result. This is the seam for STATEFUL host policy (count
175
+ * repeats, budget writes per resource, project-specific rules); keep static allow/deny in
176
+ * `governancePolicy`. Errs-open: a throwing hook never blocks the run.
177
+ */
178
+ onToolCall?: (call: {
179
+ callId: string;
180
+ name: string;
181
+ arguments: string;
182
+ }) => Promise<ToolCallHookDecision | undefined | void> | ToolCallHookDecision | undefined | void;
183
+ /**
184
+ * O5 (the PostToolUse-hook analog): called for each executed tool result before it reaches the
185
+ * kernel. Return `{ replaceOutput }` to swap the result the model sees (redact / annotate), and/or
186
+ * `{ note }` to push a contextual note into the signal stream (same channel as `injectNote` —
187
+ * e.g. "that write was a no-op, stop repeating it"). Errs-open: a throwing hook changes nothing.
188
+ */
189
+ onToolResult?: (result: {
190
+ callId: string;
191
+ name: string;
192
+ arguments: string;
193
+ output: string;
194
+ isError: boolean;
195
+ }) => Promise<ToolResultHookDecision | undefined | void> | ToolResultHookDecision | undefined | void;
121
196
  /**
122
197
  * L1 (RunGroup): bind this runner to a governance domain shared by N peer sessions of one logical
123
198
  * run. Members pass the same `id` + `budgetStore`; the kernel's run-level token cap is then enforced
@@ -217,11 +292,17 @@ export declare class RuntimeRunner {
217
292
  private activeKernel;
218
293
  private pendingObservations;
219
294
  private currentSessionId;
295
+ /** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
296
+ private injectedSignals;
297
+ /** Skill names whose content has already been pushed into the durable `knowledge` slot this
298
+ * run — guards against re-pushing a duplicate entry if the model calls `skill(name)` again for
299
+ * an already-active skill (loading is idempotent; the knowledge push should be too). */
300
+ private knowledgePushedSkills;
220
301
  private nextArchiveStart;
302
+ /** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
303
+ private currentGoal;
221
304
  /** Full tool outputs keyed by call_id until Layer-1 spool observations are logged. */
222
305
  private pendingSpoolOutputs;
223
- /** Local cache of paged-out/archived messages for priority memory retrieval. */
224
- private localPageOutCache;
225
306
  /** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
226
307
  * at the next safe point (after the tool turn resolves, kernel back in Reason — not suspended). */
227
308
  private pendingAuthoredWorkflows;
@@ -256,10 +337,22 @@ export declare class RuntimeRunner {
256
337
  mountMarker(kind: string, id: string, description: string): void;
257
338
  /** Unmount a capability by kind + id from the active run. No-op if not running. */
258
339
  unmountCapability(kind: string, id: string): void;
259
- /** Phase 4: satisfy kernel page-in requests before meta-tool execution. */
260
- private applyKernelPageIn;
261
- /** Push content into the Knowledge slot (memory retrievals, skill definitions, artifacts). */
262
- pushKnowledge(message: Message, tokens?: number): void;
340
+ /** Push content into the Knowledge slot (memory retrievals, skill definitions, artifacts).
341
+ * K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
342
+ * compaction/renewal boundary, where the cached system[1] block is rewritten anyway) instead
343
+ * of appending a duplicate. `opts.pinned` exempts the entry from the knowledge-budget sweep. */
344
+ pushKnowledge(message: Message, tokens?: number, opts?: {
345
+ key?: string;
346
+ pinned?: boolean;
347
+ }): void;
348
+ /** K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
349
+ * Errs-open: an unknown key is a kernel-side no-op. */
350
+ removeKnowledge(key: string): void;
351
+ /** K3: host-driven skill deactivation (there is deliberately no model-facing unload — it
352
+ * invites thrash). The toolset re-widens at the next provider call; the skill's knowledge pin
353
+ * drops at the next compaction/renewal boundary. A later `skill(name)` call re-activates and
354
+ * re-pins fresh content. Errs-open: not-active is a kernel-side no-op. */
355
+ deactivateSkill(name: string): void;
263
356
  /**
264
357
  * Spawn an isolated sub-agent via the kernel, run it on the host, and feed the result back.
265
358
  * Requires an active parent run (`run()` / `wake()` in progress or paused at milestone).
@@ -289,7 +382,15 @@ export declare class RuntimeRunner {
289
382
  */
290
383
  runWorkflow(spec: WorkflowSpec, opts?: {
291
384
  resumedCompleted?: string[];
385
+ /** W-1: recovered completions WITH control signals (classify branch / loop stop) — lowered to
386
+ * the kernel's `resumed_results` so control flow replays faithfully. Supersedes
387
+ * `resumedCompleted` for ids present in both. */
388
+ resumedResults?: RecoveredNodeCompletion[];
292
389
  resumedSubmissions?: Record<string, unknown>[][];
390
+ /** R3-1: original base index per submission batch (parallel to resumedSubmissions). */
391
+ resumedSubmissionBases?: number[];
392
+ /** W-1: recovered node outputs (agent id → output text) to pre-seed the driver's outputs map. */
393
+ resumedOutputs?: Map<string, string>;
293
394
  /** Standalone session id when bootstrapping (no active parent run). Defaults to a fresh uuid. */
294
395
  sessionId?: string;
295
396
  }): Promise<{
@@ -349,8 +450,9 @@ export declare class RuntimeRunner {
349
450
  private driveWorkflow;
350
451
  /**
351
452
  * Resume a workflow from the parent session's completed nodes.
352
- * Reads the session log, extracts completed workflow node agent_ids, and
353
- * calls runWorkflow with resumedCompleted so the kernel skips those nodes.
453
+ * Reads the session log, extracts completed workflow node records (with their W-1 control
454
+ * signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
455
+ * flow (classify prune / loop stop), and the driver re-seeds its outputs map.
354
456
  */
355
457
  resumeWorkflow(spec: WorkflowSpec, opts?: {
356
458
  sessionId?: string;
@@ -359,6 +461,16 @@ export declare class RuntimeRunner {
359
461
  failed: string[];
360
462
  }>;
361
463
  interrupt(): void;
464
+ /** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
465
+ * the next turn boundary, routes through the kernel attention policy, and — once acted on — renders
466
+ * as a `[SIGNAL] <text>` line in the volatile state turn plus a durable directive. Use it to feed
467
+ * host-detected events back to the model mid-run (e.g. "that write was a no-op — stop repeating it")
468
+ * without wiring a full `SignalSource`. `urgency` maps to the kernel disposition ladder: `"normal"`
469
+ * queues for the next boundary (default), `"high"` soft-interrupts, `"critical"` preempts. */
470
+ injectNote(text: string, urgency?: RuntimeSignalUrgency): void;
471
+ /** Injected-note drain shared by the main loop's per-turn poll: injected notes first (FIFO), then
472
+ * the configured `signalSource`. Keeps the two inbound channels on one code path so they never drift. */
473
+ private nextInboundSignal;
362
474
  run(req: {
363
475
  sessionId: string;
364
476
  goal: string;
@@ -376,11 +488,38 @@ export declare class RuntimeRunner {
376
488
  dream(agentId: string, nowMs?: number): AsyncIterable<StreamEvent>;
377
489
  /** Resolve in-kernel AskUser suspend; returns resume lists and stream events to yield. */
378
490
  private resolveKernelSuspend;
491
+ /**
492
+ * O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
493
+ * output. Resolution order: (a) this turn's in-memory `pendingSpoolOutputs` map (a call spooled
494
+ * earlier in the SAME tool-turn, before the session-log write lands), (b) the on-disk result
495
+ * spool (persisted once the kernel observation `large_result_spooled` was processed), (c) a
496
+ * session-log scan for the original `tool_completed` event carrying that `call_id`. Slices the
497
+ * resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
498
+ */
499
+ private resolveReadResult;
379
500
  private execute;
501
+ /** I4 + K4: fetch long-term memory hits for the current goal and land them in `history` as an
502
+ * ordinary user turn — single-use retrieval content that decays with the compression pyramid,
503
+ * never pinned into `knowledge`. Called once before turn 1 (`phase: "initial"`) and re-fired
504
+ * after each sprint renewal (`phase: "renewal"`): renewal drops the old history INCLUDING the
505
+ * earlier memory hits, so the new sprint gets a fresh recall pass. Errs-open throughout. */
506
+ private prefetchMemoryIntoHistory;
380
507
  private appendObservations;
381
508
  private archiveSemanticPageOut;
382
509
  private upgradeCompressedSummary;
383
510
  }
511
+ /** Kernel-consumed meta-tools (e.g. `pace`) are answered by a synthetic tool result the kernel keeps
512
+ * in its OWN history but never emits as a `tool_completed` session event (they never reach the
513
+ * execution plane). On replay that leaves an assistant `tool_call` with no following tool result —
514
+ * which strict OpenAI-compatible providers reject ("every tool_call must be answered by a tool
515
+ * message"). This pass re-pairs any such orphan by inserting a synthetic tool-result message right
516
+ * after its assistant message, reproducing the pair the kernel had all along.
517
+ *
518
+ * Discriminator: only pair an orphan when the run **continued past it** — i.e. a later non-tool
519
+ * message exists. A tail assistant tool_call with nothing after it is a genuinely PENDING tool the
520
+ * run stopped in front of (the wake/recovery case), which must stay unpaired so wake executes it.
521
+ * Pure. */
522
+ export declare function pairOrphanToolCalls(messages: Message[]): Message[];
384
523
  export declare function replayMessages(events: Array<{
385
524
  seq: number;
386
525
  event: SessionEvent;