@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
package/README.md CHANGED
@@ -187,7 +187,7 @@ The mechanisms above are not internal refactors — they change what you can bui
187
187
  Tool calls, spawns, compression, and signals pass through one kernel gate with an explicit lifecycle (Ready / Running / Blocked / Suspended). You implement I/O; the kernel decides *when* and *whether*. Node, Python, and Rust share the same decision path, so `wake(sessionId)` and cross-language tooling see consistent behavior.
188
188
 
189
189
  **Longer, sturdier sessions (Layer-1 spool + semantic page-out)**
190
- Oversized tool results (> 50 KB) stay in context as a preview plus a `.spool/` reference — the model reads the full payload on demand via ordinary file tools. When pressure triggers semantic eviction, the SDK summarizes archived content into `DreamStore` and satisfies `page_in_requested` on the way back in. Long tasks survive token pressure instead of failing mid-run.
190
+ Oversized tool results (> 50 KB) stay in context as a preview plus a `.spool/` reference — the model reads the full payload on demand via ordinary file tools. When pressure triggers semantic eviction, the SDK summarizes archived content into `DreamStore`. Long tasks survive token pressure instead of failing mid-run.
191
191
 
192
192
  **Safety and governance by default (OS native profile)**
193
193
  Every run loads declarative `governancePolicy` (deny / ask_user / rate-limit / param rules) and in-kernel signal routing (`attentionPolicy`, default queue 64). Dangerous tools, external interrupts, and approval flows are policy — not ad-hoc `if` checks in your handlers.
@@ -311,7 +311,7 @@ The kernel renders context as four LLM API slots — only **history** is compres
311
311
  | Slot | Source | Role |
312
312
  |------|--------|------|
313
313
  | `systemStable` | system partition | Identity, rules — never changes within a run |
314
- | `systemKnowledge` | knowledge partition | Preloaded memory, skill defslow frequency |
314
+ | `systemKnowledge` | knowledge partition | Skill bodies, `initialMemory`, host-pinned durable refs keyed, boundary-evicted, budgeted |
315
315
  | `turns[0]` | `task_state` + signals | Goal, plan, progress, compression log, runtime signals |
316
316
  | `turns[1..N]` | history | Conversation transcript |
317
317
 
@@ -495,15 +495,15 @@ effort: 1
495
495
  2. Express each as a concise bullet
496
496
  ```
497
497
 
498
+ A loaded skill's body is pinned into the durable `knowledge` slot (keyed `skill:<name>`) and its `allowed_tools` narrow the exposed toolset. Activation is **not** permanent: `runner.deactivateSkill(name)` re-widens the toolset at the next provider call and unpins the body at the next boundary, and `skillLeaseTurns` auto-deactivates every skill N turns after it loads — so a long multi-phase run doesn't monotonically accumulate early-phase skills. There is deliberately no model-facing unload (deactivation is host-driven only).
499
+
498
500
  ---
499
501
 
500
502
  ## Knowledge
501
503
 
502
- Implement `KnowledgeSource` to connect any RAG system. The kernel injects a `knowledge` meta-tool that the LLM calls on demand. Runtime retrieval results land in **history** as tool results.
503
-
504
- To inject durable knowledge at startup (Slot 2, cacheable on Anthropic), use `initialMemory` or `runner.pushKnowledge()`.
504
+ Implement `KnowledgeSource` to connect any RAG system. The kernel injects a `knowledge` meta-tool that the LLM calls on demand. Runtime retrieval results land in **history** as tool results (single-use fact content that decays with the compression pyramid) — not in the durable `knowledge` partition.
505
505
 
506
- Before tool execution the kernel may emit `page_in_requested`; the SDK satisfies it from `DreamStore`, `KnowledgeSource`, and a local semantic page-out cache, then feeds `page_in` back to the kernel.
506
+ To inject durable knowledge at startup (Slot 2, cacheable on Anthropic), use `initialMemory` or `runner.pushKnowledge(content, tokens?, { key, pinned })`. A **keyed** entry upserts on a repeated key and can be removed with `runner.removeKnowledge(key)`; both take effect at the next compaction/renewal boundary (where the `system[1]` cache prefix is rewritten anyway). Set `knowledgeBudgetRatio` (default 0.25 of `maxTokens`, 0 disables) to cap the partition — over budget, the oldest unpinned, non-skill entries are evicted at boundaries while `pinned: true` entries survive.
507
507
 
508
508
  ```typescript
509
509
  const runner = new RuntimeRunner({
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { runAgent, runFanout } from "./runtime/facade.js";
2
+ export { runLoop, LoopDriver, foldLoopState } from "./runtime/loop-driver.js";
3
+ export type { LoopSpec, LoopOutcome } from "./runtime/loop-driver.js";
2
4
  export type { RunAgentOptions, RunFanoutOptions } from "./runtime/facade.js";
3
5
  export { RuntimeRunner, collectText } from "./runtime/runner.js";
4
6
  export type { RuntimeOptions } from "./runtime/runner.js";
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@
12
12
  // ╚══════════════════════════════════════════════════════════════════════════╝
13
13
  // ── Start here: the canonical entry points ─────────────────────────────────
14
14
  export { runAgent, runFanout } from "./runtime/facade.js";
15
+ // ③ dynamic loop agents: self-pacing rounds over the kernel pacing trap.
16
+ export { runLoop, LoopDriver, foldLoopState } from "./runtime/loop-driver.js";
15
17
  export { RuntimeRunner, collectText } from "./runtime/runner.js";
16
18
  // ── Execution plane + session log (the defaults) ────────────────────────────
17
19
  export { LocalExecutionPlane } from "./runtime/execution-plane.js";
@@ -1,6 +1,6 @@
1
1
  export { DEFAULT_NATIVE_ATTENTION_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, assertNativeProfile, osProfile, } from "../runtime/os-profile.js";
2
2
  export type { NativeOsProfile, OsProfileId } from "../runtime/os-profile.js";
3
- export { rebuildOsSnapshotFromSessionEvents, sessionLogHasRequiredCategories } from "../runtime/os-snapshot.js";
3
+ export { rebuildOsSnapshotFromSessionEvents } from "../runtime/os-snapshot.js";
4
4
  export type { OsSnapshot } from "../runtime/os-snapshot.js";
5
5
  export type { KernelEventCategory } from "../runtime/kernel-event-log.js";
6
6
  export { KernelPrimitivesDashboard } from "../runtime/kernel-primitives-dashboard.js";
package/dist/os/public.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // `@deepstrike/sdk/os` — Agent-OS diagnostics, profiles, signal/permission machinery, replay-testing,
2
2
  // and the scheduler/quota/policy types referenced by advanced `RuntimeOptions` fields.
3
3
  export { DEFAULT_NATIVE_ATTENTION_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, assertNativeProfile, osProfile, } from "../runtime/os-profile.js";
4
- export { rebuildOsSnapshotFromSessionEvents, sessionLogHasRequiredCategories } from "../runtime/os-snapshot.js";
4
+ export { rebuildOsSnapshotFromSessionEvents } from "../runtime/os-snapshot.js";
5
5
  export { KernelPrimitivesDashboard } from "../runtime/kernel-primitives-dashboard.js";
6
6
  // Signals + SDK-side permissions.
7
7
  export { ScheduledPrompt } from "../signals/scheduled.js";
@@ -15,6 +15,7 @@
15
15
  import { RuntimeRunner, collectText } from "./runner.js";
16
16
  import { LocalExecutionPlane } from "./execution-plane.js";
17
17
  import { InMemorySessionLog } from "./session-log.js";
18
+ import { fanoutSynthesize } from "../types/agent.js";
18
19
  /**
19
20
  * Run a single agent to completion and return its final text — `RuntimeRunner` + `run` + `collectText`
20
21
  * in one call. Register tools by passing `tools`; everything else has a working default.
@@ -46,17 +47,16 @@ export async function runFanout(opts) {
46
47
  maxTokens: opts.maxTokens ?? 32_000,
47
48
  ...(opts.maxTurns !== undefined ? { maxTurns: opts.maxTurns } : {}),
48
49
  });
49
- const workerRole = opts.workerRole ?? "explore";
50
- const spec = {
51
- nodes: [
52
- ...opts.tasks.map(task => ({ task, role: workerRole })),
53
- {
54
- task: opts.synthesize,
55
- role: opts.synthesisRole ?? "plan",
56
- dependsOn: opts.tasks.map((_, i) => i),
57
- },
58
- ],
59
- };
50
+ // W-N8: build the spec via the ONE fanout template (it pins the pattern's isolation /
51
+ // context-inheritance choices — read-only system-only workers, full-context synthesizer — which
52
+ // this facade used to silently drop), then apply the caller's role overrides on top.
53
+ const spec = fanoutSynthesize(opts.tasks, opts.synthesize);
54
+ if (opts.workerRole) {
55
+ for (const node of spec.nodes.slice(0, opts.tasks.length))
56
+ node.role = opts.workerRole;
57
+ }
58
+ if (opts.synthesisRole)
59
+ spec.nodes[spec.nodes.length - 1].role = opts.synthesisRole;
60
60
  const outcome = await runner.runWorkflow(spec, opts.sessionId ? { sessionId: opts.sessionId } : undefined);
61
61
  // The synthesis node is the last spec node; the kernel ids nodes `wf-node{index}`. Prefer that id,
62
62
  // but fall back to the last completed node's output so a kernel id-scheme change can't silently
@@ -3,12 +3,6 @@ import type { SessionEvent } from "./session-log.js";
3
3
  /** Agent OS kernel event category (Phase 5). */
4
4
  export type KernelEventCategory = "syscall" | "sched" | "mm" | "proc" | "ipc";
5
5
  export declare function categoryForKind(kind: string): KernelEventCategory;
6
- export declare function withCategory<T extends {
7
- kind: string;
8
- }>(event: T): T & {
9
- category: KernelEventCategory;
10
- primitive: KernelPrimitive;
11
- };
12
6
  type CompressionAction = Extract<SessionEvent, {
13
7
  kind: "compressed";
14
8
  }>["action"];
@@ -22,33 +22,16 @@ export function categoryForKind(kind) {
22
22
  return "sched";
23
23
  }
24
24
  }
25
- export function withCategory(event) {
26
- const category = categoryForKind(event.kind);
27
- return {
28
- ...event,
29
- category,
30
- primitive: primitiveForCategory(category),
31
- };
32
- }
33
25
  export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
34
26
  const t = obs.turn ?? turn;
35
27
  const compressionAction = opts.compressionAction ?? (() => undefined);
36
28
  switch (obs.kind) {
37
- case "page_out":
38
- return withCategory({
39
- kind: "page_out",
40
- turn: t,
41
- action: compressionAction(obs.action),
42
- summary: obs.summary,
43
- tier_hint: obs.tier_hint ?? "durable",
44
- message_count: Array.isArray(obs.archived) ? obs.archived.length : 0,
45
- });
46
29
  case "compressed": {
47
30
  const latest = opts.latestSeq ?? -1;
48
31
  const start = opts.nextArchiveStart ?? 0;
49
32
  if (latest < start)
50
33
  return null;
51
- return withCategory({
34
+ return {
52
35
  kind: "compressed",
53
36
  turn: t,
54
37
  archived_seq_range: [start, latest],
@@ -57,24 +40,24 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
57
40
  summary_tokens: obs.summary ? Math.max(1, Math.ceil(obs.summary.length / 4)) : undefined,
58
41
  archive_ref: opts.archiveRef,
59
42
  preserved_refs: opts.preservedRefs ?? [],
60
- });
43
+ };
61
44
  }
62
45
  case "renewed":
63
- return withCategory({
46
+ return {
64
47
  kind: "context_renewed",
65
48
  turn: t,
66
49
  sprint: obs.sprint ?? 0,
67
50
  handoff_ref: "",
68
- });
51
+ };
69
52
  case "rollbacked":
70
- return withCategory({
53
+ return {
71
54
  kind: "rollbacked",
72
55
  turn: t,
73
56
  checkpoint_history_len: obs.checkpoint_history_len ?? 0,
74
57
  reason: obs.reason,
75
- });
58
+ };
76
59
  case "capability_changed":
77
- return withCategory({
60
+ return {
78
61
  kind: "capability_changed",
79
62
  turn: t,
80
63
  added: obs.added ?? [],
@@ -84,36 +67,29 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
84
67
  ...(obs.version != null && { version: obs.version }),
85
68
  ...(obs.mounted_by != null && { mounted_by: obs.mounted_by }),
86
69
  ...(obs.mount_reason != null && { mount_reason: obs.mount_reason }),
87
- });
70
+ };
88
71
  case "milestone_advanced":
89
- return withCategory({
72
+ return {
90
73
  kind: "milestone_advanced",
91
74
  turn: t,
92
75
  phase_id: obs.phase_id ?? "",
93
76
  capabilities_unlocked: obs.capabilities_unlocked ?? [],
94
- });
77
+ };
95
78
  case "milestone_blocked":
96
- return withCategory({
79
+ return {
97
80
  kind: "milestone_blocked",
98
81
  turn: t,
99
82
  phase_id: obs.phase_id ?? "",
100
83
  reason: typeof obs.reason === "string" ? obs.reason : "",
101
- });
102
- case "milestone_evidence":
103
- return withCategory({
104
- kind: "milestone_evidence",
105
- turn: t,
106
- phase_id: obs.phase_id ?? "",
107
- evidence: obs.evidence ?? [],
108
- });
84
+ };
109
85
  case "checkpoint_taken":
110
- return withCategory({
86
+ return {
111
87
  kind: "checkpoint_taken",
112
88
  turn: t,
113
89
  history_len: obs.history_len ?? 0,
114
- });
90
+ };
115
91
  case "agent_process_changed":
116
- return withCategory({
92
+ return {
117
93
  kind: "agent_process_changed",
118
94
  turn: t,
119
95
  agent_id: obs.agent_id ?? "",
@@ -126,47 +102,47 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
126
102
  ...(obs.result_termination
127
103
  ? { result_termination: obs.result_termination }
128
104
  : {}),
129
- });
105
+ };
130
106
  case "tool_gated":
131
- return withCategory({
107
+ return {
132
108
  kind: "tool_gated",
133
109
  turn: t,
134
110
  call_id: obs.call_id ?? "",
135
111
  tool: obs.tool ?? "",
136
112
  reason: typeof obs.reason === "string" ? obs.reason : "",
137
- });
113
+ };
138
114
  case "signal_disposed":
139
- return withCategory({
115
+ return {
140
116
  kind: "signal_disposed",
141
117
  turn: t,
142
118
  signal_id: obs.signal_id ?? "",
143
119
  disposition: obs.disposition ?? "",
144
120
  queue_depth: obs.queue_depth ?? 0,
145
- });
121
+ };
146
122
  case "budget_exceeded":
147
- return withCategory({
123
+ return {
148
124
  kind: "budget_exceeded",
149
125
  turn: t,
150
126
  budget: obs.budget ?? "",
151
- });
127
+ };
152
128
  case "suspended":
153
- return withCategory({
129
+ return {
154
130
  kind: "suspended",
155
131
  turn: t,
156
132
  reason: typeof obs.reason === "string" ? obs.reason : "",
157
133
  pending_calls: obs.pending_calls ?? [],
158
- });
134
+ };
159
135
  case "resumed":
160
- return withCategory({
136
+ return {
161
137
  kind: "resumed",
162
138
  turn: t,
163
139
  approved: obs.approved ?? [],
164
140
  denied: obs.denied ?? [],
165
- });
141
+ };
166
142
  case "page_in_requested":
167
143
  return null;
168
144
  case "large_result_spooled":
169
- return withCategory({
145
+ return {
170
146
  kind: "large_result_spooled",
171
147
  turn: t,
172
148
  call_id: obs.call_id ?? "",
@@ -174,51 +150,51 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
174
150
  original_size: obs.original_size ?? 0,
175
151
  preview_size: obs.preview_size ?? 0,
176
152
  spool_ref: opts.spoolRef,
177
- });
153
+ };
178
154
  case "memory_written":
179
- return withCategory({
155
+ return {
180
156
  kind: "memory_written",
181
157
  turn: t,
182
158
  memory_id: obs.memory_id ?? "",
183
159
  memory_kind: obs.memory_kind ?? "",
184
160
  size_bytes: obs.size_bytes ?? 0,
185
- });
161
+ };
186
162
  case "memory_queried":
187
- return withCategory({
163
+ return {
188
164
  kind: "memory_queried",
189
165
  turn: t,
190
166
  query_context: obs.query_context ?? "",
191
167
  requested_k: obs.requested_k ?? 0,
192
168
  requires_async_response: obs.requires_async_response ?? false,
193
- });
169
+ };
194
170
  case "memory_validation_failed":
195
- return withCategory({
171
+ return {
196
172
  kind: "memory_validation_failed",
197
173
  turn: t,
198
174
  memory_id: obs.memory_id ?? "",
199
175
  error: obs.error ?? "",
200
- });
176
+ };
201
177
  case "workflow_batch_spawned": {
202
178
  // Batch metadata persisted for resume recovery; individual nodes are
203
179
  // recorded when they complete (via workflow_node_completed).
204
180
  const nodes = obs.nodes ?? [];
205
- return withCategory({
181
+ return {
206
182
  kind: "workflow_batch_spawned",
207
183
  turn: t,
208
184
  node_count: nodes.length,
209
185
  node_ids: nodes.map((n) => n.agent_id ?? ""),
210
- });
186
+ };
211
187
  }
212
188
  case "workflow_completed": {
213
189
  const completed = obs.completed ?? [];
214
190
  const failed = obs.failed ?? [];
215
- return withCategory({
191
+ return {
216
192
  kind: "workflow_completed",
217
193
  turn: t,
218
194
  completed,
219
195
  failed,
220
196
  total_nodes: completed.length + failed.length,
221
- });
197
+ };
222
198
  }
223
199
  default:
224
200
  return null;
@@ -11,10 +11,19 @@ export interface KernelRuntimeHandle {
11
11
  drainNewMessages(): Message[];
12
12
  preservedRefs(): string[];
13
13
  }
14
+ export interface PaceDecision {
15
+ action: "continue" | "sleep" | "stop";
16
+ delayMs?: number;
17
+ reason: string;
18
+ /** Set when the kernel trap coerced the model's proposal (clamped delay / forced stop). */
19
+ coercedFrom?: string;
20
+ }
14
21
  export interface KernelLoopResult {
15
22
  termination: string;
16
23
  turnsUsed: number;
17
24
  totalTokensUsed: number;
25
+ /** ③ loop-agent: the kernel-adjudicated after-round decision (absent on non-loop runs). */
26
+ paceDecision?: PaceDecision;
18
27
  }
19
28
  export type MilestoneVerifierKind = {
20
29
  kind: "machine_check";
@@ -65,6 +74,8 @@ export interface KernelObservation {
65
74
  phase_id?: string;
66
75
  capabilities_unlocked?: string[];
67
76
  evidence?: string[];
77
+ removed_keys?: string[];
78
+ tokens_freed?: number;
68
79
  reason?: RollbackReason | string;
69
80
  agent_id?: string;
70
81
  parent_session_id?: string;
@@ -240,12 +240,24 @@ function mapKernelAction(raw) {
240
240
  };
241
241
  case "done": {
242
242
  const result = raw.result ?? {};
243
+ const pace = result.pace_decision;
243
244
  return {
244
245
  kind: "done",
245
246
  result: {
246
247
  termination: String(result.termination ?? "error"),
247
248
  turnsUsed: Number(result.turns_used ?? 0),
248
249
  totalTokensUsed: Number(result.total_tokens_used ?? 0),
250
+ // ③ loop-agent: the kernel-adjudicated after-round decision (absent on non-loop runs).
251
+ ...(pace
252
+ ? {
253
+ paceDecision: {
254
+ action: (pace.action ?? "stop"),
255
+ delayMs: pace.delay_ms,
256
+ reason: pace.reason ?? "",
257
+ coercedFrom: pace.coerced_from,
258
+ },
259
+ }
260
+ : {}),
249
261
  },
250
262
  };
251
263
  }
@@ -77,6 +77,13 @@ export declare class LargeResultSpool {
77
77
  * Read a spooled result back from disk.
78
78
  */
79
79
  readSpooledResult(spoolRef: string): Promise<string>;
80
+ /**
81
+ * O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
82
+ * `call_id`, not the content-hashed file name `persistOutput` chose). Scans the spool directory
83
+ * for the `${callId}-*.txt` naming convention; returns `undefined` if nothing was ever spooled
84
+ * for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
85
+ */
86
+ findByCallId(callId: string): Promise<string | undefined>;
80
87
  /**
81
88
  * Clean up old spool files (optional maintenance).
82
89
  */
@@ -140,6 +140,31 @@ omitted: ${omitted} chars
140
140
  throw new Error(`Failed to read spooled result: ${error}`);
141
141
  }
142
142
  }
143
+ /**
144
+ * O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
145
+ * `call_id`, not the content-hashed file name `persistOutput` chose). Scans the spool directory
146
+ * for the `${callId}-*.txt` naming convention; returns `undefined` if nothing was ever spooled
147
+ * for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
148
+ */
149
+ async findByCallId(callId) {
150
+ let files;
151
+ try {
152
+ files = await fs.readdir(this.spoolDir);
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ const prefix = `${callId}-`;
158
+ const match = files.find(f => f.startsWith(prefix) && f.endsWith('.txt'));
159
+ if (!match)
160
+ return undefined;
161
+ try {
162
+ return await fs.readFile(path.join(this.spoolDir, match), 'utf-8');
163
+ }
164
+ catch {
165
+ return undefined;
166
+ }
167
+ }
143
168
  /**
144
169
  * Clean up old spool files (optional maintenance).
145
170
  */
@@ -0,0 +1,108 @@
1
+ /**
2
+ * ③ Dynamic loop-agent engineering system — the SDK driver.
3
+ *
4
+ * A loop agent is NOT a new execution engine:
5
+ * - a ROUND is exactly one bounded `RuntimeRunner.run()` (compaction, RepeatFuse,
6
+ * criteria gate, and budget verdicts all apply per round for free);
7
+ * - CONTINUITY is the session log replayed under ONE stable sessionId;
8
+ * - LIFETIME GOVERNANCE is the RunGroup the rounds are members of;
9
+ * - the only new decision — what happens AFTER a round — is the model-proposed,
10
+ * kernel-adjudicated `pace` verb (see the kernel pacing trap). The kernel never
11
+ * sleeps; all timers and judge calls live here, in SDK I/O land.
12
+ *
13
+ * Durable pacing: every round appends `round_started` / `round_paced` to the loop's
14
+ * session log, so `LoopDriver.resume()`-style recovery is a fold over the log
15
+ * (the SessionLogGroupBudgetStore pattern) — zero new storage. A stateless host
16
+ * reads `wake_at_ms` from the fold and re-arms via its own cron/queue; an
17
+ * in-process host lets `run()` sleep inline.
18
+ */
19
+ import type { RuntimeRunner } from "./runner.js";
20
+ import type { SessionEvent } from "./session-log.js";
21
+ import type { PaceDecision } from "./kernel-step.js";
22
+ import type { StreamEvent } from "../types.js";
23
+ export interface LoopSpec {
24
+ /** Stable loop id = the ONE session id every round replays (transcript continuity). */
25
+ loopId: string;
26
+ goal: string;
27
+ criteria?: string[];
28
+ /** Hard round cap; the kernel coerces continue/sleep to stop at the cap. */
29
+ maxRounds?: number;
30
+ /** Sleep clamp bounds (ms), enforced in-kernel. */
31
+ minSleepMs?: number;
32
+ maxSleepMs?: number;
33
+ /** "stop" (goal loop, default) | "sleep" (cron loop) when a round never calls pace. */
34
+ defaultAction?: "stop" | "sleep";
35
+ /** Cross-round done-gate: judges a stop proposal; a failing verdict overrides
36
+ * stop→continue at most `maxVerdictOverrides` times, its feedback becoming the
37
+ * next round's steering note. The in-kernel O4 criteria gate is the per-round rung
38
+ * of the same ladder — this is the cross-round rung. */
39
+ verdictFn?: (ctx: {
40
+ loopId: string;
41
+ round: number;
42
+ reason: string;
43
+ }) => Promise<{
44
+ pass: boolean;
45
+ feedback?: string;
46
+ }> | {
47
+ pass: boolean;
48
+ feedback?: string;
49
+ };
50
+ maxVerdictOverrides?: number;
51
+ /** Sleep implementation (injectable for tests / stateless hosts). Default: setTimeout.
52
+ * Return `false` to hand the wake to an external scheduler and end `run()` dormant. */
53
+ sleeper?: (delayMs: number, wakeAtMs: number) => Promise<boolean>;
54
+ /** Per-round event tap (streaming passthrough). */
55
+ onEvent?: (round: number, event: StreamEvent) => void;
56
+ }
57
+ export interface LoopOutcome {
58
+ loopId: string;
59
+ roundsCompleted: number;
60
+ stopped: boolean;
61
+ /** "stopped" | "dormant" (sleeper handed off to an external scheduler) */
62
+ state: "stopped" | "dormant";
63
+ lastPace?: PaceDecision;
64
+ lastStatus?: string;
65
+ /** Absolute wake time when dormant. */
66
+ wakeAtMs?: number;
67
+ }
68
+ /** Fold the loop's session log into resumable pacing state — zero new storage. DW-5: the judge's
69
+ * override budget folds too, so a crash/restart can't grant the verdictFn fresh overrides. */
70
+ export declare function foldLoopState(events: Array<{
71
+ seq: number;
72
+ event: SessionEvent;
73
+ }>): {
74
+ roundsCompleted: number;
75
+ pendingWakeAtMs?: number;
76
+ lastPace?: {
77
+ action: string;
78
+ reason: string;
79
+ };
80
+ overridesUsed: number;
81
+ };
82
+ /**
83
+ * DW-6 completion→wake bridge, composed from two existing seams (zero new mechanism): a `sleeper`
84
+ * that races the timer against an L0 recipient-addressed signal on the shared gateway. Ingest a
85
+ * signal with `recipient: loopId` (a subagent/workflow completion, a webhook) and the sleeping loop
86
+ * wakes into its next round immediately — where the SAME queued signal then reaches the model
87
+ * through the kernel's normal signal path, so the wake reason is visible in-round.
88
+ */
89
+ export declare function signalAwareSleeper(gateway: {
90
+ onSignal(listener: (sig: {
91
+ recipient?: string;
92
+ }) => void): () => void;
93
+ }, loopId: string): NonNullable<LoopSpec["sleeper"]>;
94
+ export declare class LoopDriver {
95
+ private readonly runner;
96
+ private readonly spec;
97
+ private overridesUsed;
98
+ constructor(runner: RuntimeRunner, spec: LoopSpec);
99
+ /**
100
+ * Drive rounds until the loop stops or goes dormant. Resumable by construction:
101
+ * the round count and any pending wake are folded from the session log, so
102
+ * calling `run()` again after a crash / on a stateless host continues in place.
103
+ */
104
+ run(): Promise<LoopOutcome>;
105
+ private sleep;
106
+ }
107
+ /** Facade: run a self-pacing loop agent (joins runAgent/runFanout as an entry point). */
108
+ export declare function runLoop(runner: RuntimeRunner, spec: LoopSpec): Promise<LoopOutcome>;