@deepstrike/sdk 0.2.50 → 0.2.51

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 (39) hide show
  1. package/README.md +83 -60
  2. package/dist/index.d.ts +5 -7
  3. package/dist/index.js +3 -3
  4. package/dist/kernel.d.ts +61 -31
  5. package/dist/runtime/canonical-kernel-step.d.ts +143 -0
  6. package/dist/runtime/canonical-kernel-step.js +1444 -0
  7. package/dist/runtime/execution-plane.d.ts +0 -3
  8. package/dist/runtime/execution-plane.js +0 -24
  9. package/dist/runtime/facade.js +3 -0
  10. package/dist/runtime/kernel-event-log.js +7 -13
  11. package/dist/runtime/kernel-journal.d.ts +264 -0
  12. package/dist/runtime/kernel-journal.js +741 -0
  13. package/dist/runtime/kernel-primitives-dashboard.d.ts +0 -2
  14. package/dist/runtime/kernel-primitives-dashboard.js +1 -8
  15. package/dist/runtime/kernel-step.d.ts +29 -109
  16. package/dist/runtime/kernel-step.js +47 -317
  17. package/dist/runtime/os-snapshot.d.ts +2 -2
  18. package/dist/runtime/os-snapshot.js +2 -6
  19. package/dist/runtime/payload-store.d.ts +16 -0
  20. package/dist/runtime/payload-store.js +80 -0
  21. package/dist/runtime/runner.d.ts +31 -114
  22. package/dist/runtime/runner.js +689 -774
  23. package/dist/runtime/session-log.d.ts +34 -32
  24. package/dist/runtime/session-log.js +21 -131
  25. package/dist/runtime/session-repair.d.ts +2 -36
  26. package/dist/runtime/session-repair.js +2 -47
  27. package/dist/runtime/sub-agent-orchestrator.d.ts +1 -1
  28. package/dist/runtime/sub-agent-orchestrator.js +42 -40
  29. package/dist/types/agent.d.ts +22 -19
  30. package/dist/types/agent.js +26 -42
  31. package/dist/workflow/public.d.ts +1 -1
  32. package/dist/workflow/public.js +1 -1
  33. package/package.json +2 -2
  34. package/dist/runtime/kernel-rebuild.d.ts +0 -13
  35. package/dist/runtime/kernel-rebuild.js +0 -75
  36. package/dist/runtime/kernel-transaction-log.d.ts +0 -61
  37. package/dist/runtime/kernel-transaction-log.js +0 -149
  38. package/dist/runtime/large-result-spool.d.ts +0 -93
  39. package/dist/runtime/large-result-spool.js +0 -214
@@ -2,7 +2,6 @@ import type { ToolCall, ToolSchema, StreamEvent, ToolSuspendEvent, PermissionReq
2
2
  import type { RegisteredTool } from "../tools/index.js";
3
3
  import type { DreamStore, MemoryScope } from "../memory/protocols.js";
4
4
  import type { KnowledgeSource } from "../knowledge/source.js";
5
- import { LargeResultSpool } from "./large-result-spool.js";
6
5
  import type { OperationContext } from "./reliability.js";
7
6
  export interface RunContext {
8
7
  /** Immutable identity, deadline, and cancellation boundary for this operation. */
@@ -14,7 +13,6 @@ export interface RunContext {
14
13
  knowledgeSource?: KnowledgeSource;
15
14
  onToolSuspend?: (event: ToolSuspendEvent) => Promise<unknown> | unknown;
16
15
  onPermissionRequest?: (event: PermissionRequestEvent) => Promise<PermissionResponse | boolean> | PermissionResponse | boolean;
17
- resultSpool?: LargeResultSpool;
18
16
  /** M3/G4 worktree isolation: the working directory a sub-agent's tools should run in (the git
19
17
  * worktree created for an `isolation: "worktree"` node). Injected by `WorktreeExecutionPlane`; a
20
18
  * cwd-aware execution plane / tool reads it to scope filesystem + subprocess work. Undefined ⇒
@@ -38,7 +36,6 @@ export declare class LocalExecutionPlane implements ExecutionPlane {
38
36
  unregister(name: string): this;
39
37
  schemas(): ToolSchema[];
40
38
  executeAll(calls: ToolCall[], ctx: RunContext): AsyncIterable<StreamEvent>;
41
- private tryReadSpooledArgument;
42
39
  private executeSingle;
43
40
  }
44
41
  export declare function resolvePermissionRequest(request: PermissionRequestEvent, ctx: RunContext): Promise<PermissionResponse>;
@@ -1,7 +1,6 @@
1
1
  import { isAsyncIterable, maybeWarnFailureShapedChunk, normalizeToolChunk, toolChunkText, validateToolArguments } from "../tools/index.js";
2
2
  import { formatToolError } from "../tools/errors.js";
3
3
  import { readSkillFile } from "../skills/loader.js";
4
- import { LargeResultSpool } from "./large-result-spool.js";
5
4
  export class LocalExecutionPlane {
6
5
  tools = new Map();
7
6
  register(...tools) {
@@ -81,30 +80,7 @@ export class LocalExecutionPlane {
81
80
  }
82
81
  }
83
82
  }
84
- async tryReadSpooledArgument(call, ctx) {
85
- const isReadTool = ["read", "read_file", "view_file", "read_spooled_result"].includes(call.name);
86
- if (!isReadTool)
87
- return null;
88
- try {
89
- const args = JSON.parse(call.arguments || "{}");
90
- for (const val of Object.values(args)) {
91
- if (typeof val === "string" && (val.startsWith(".spool/") || val.includes("/.spool/"))) {
92
- const spool = ctx.resultSpool ?? new LargeResultSpool();
93
- const content = await spool.readSpooledResult(val);
94
- return content;
95
- }
96
- }
97
- }
98
- catch {
99
- // Ignore errors
100
- }
101
- return null;
102
- }
103
83
  async *executeSingle(call, ctx) {
104
- const spooledContent = await this.tryReadSpooledArgument(call, ctx);
105
- if (spooledContent !== null) {
106
- return { callId: call.id, output: spooledContent, isError: false };
107
- }
108
84
  const registered = this.tools.get(call.name);
109
85
  if (!registered)
110
86
  return { callId: call.id, output: `unknown tool: ${call.name}`, isError: true };
@@ -58,6 +58,9 @@ export async function runFanout(opts) {
58
58
  if (opts.synthesisRole)
59
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
+ if (outcome.rejection) {
62
+ throw new Error(`workflow ${outcome.rejection.operation} rejected: ${outcome.rejection.reason}`);
63
+ }
61
64
  // The synthesis node is the last spec node; the kernel ids nodes `wf-node{index}`. Prefer that id,
62
65
  // but fall back to the last completed node's output so a kernel id-scheme change can't silently
63
66
  // return an empty synthesis.
@@ -9,7 +9,6 @@ export function categoryForKind(kind) {
9
9
  case "page_in_requested":
10
10
  case "renewed":
11
11
  case "context_renewed":
12
- case "large_result_spooled":
13
12
  case "memory_written":
14
13
  case "memory_queried":
15
14
  case "memory_validation_failed":
@@ -111,7 +110,7 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
111
110
  kind: "agent_process_changed",
112
111
  turn: t,
113
112
  agent_id: obs.agent_id ?? "",
114
- parent_session_id: obs.parent_session_id ?? "",
113
+ parent_task_id: obs.parent_task_id ?? "",
115
114
  role: obs.role ?? "",
116
115
  isolation: obs.isolation ?? "",
117
116
  context_inheritance: obs.context_inheritance ?? "",
@@ -182,16 +181,6 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
182
181
  };
183
182
  case "page_in_requested":
184
183
  return null;
185
- case "large_result_spooled":
186
- return {
187
- kind: "large_result_spooled",
188
- turn: t,
189
- call_id: obs.call_id ?? "",
190
- tool: obs.tool ?? "",
191
- original_size: obs.original_size ?? 0,
192
- preview_size: obs.preview_size ?? 0,
193
- spool_ref: obs.spool_ref,
194
- };
195
184
  case "page_out_archived":
196
185
  return {
197
186
  kind: "page_out",
@@ -264,7 +253,12 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
264
253
  };
265
254
  }
266
255
  default:
267
- return null;
256
+ return {
257
+ kind: "kernel_observation",
258
+ turn: t,
259
+ observation_kind: obs.kind,
260
+ raw: { ...obs },
261
+ };
268
262
  }
269
263
  }
270
264
  export function primitiveForCategory(category) {
@@ -0,0 +1,264 @@
1
+ /**
2
+ * `KernelJournal` — the durable transaction capability of the Canonical Kernel ABI (spec §9.1).
3
+ *
4
+ * This is the *authoritative* interface shape for all four SDKs (Node first, then Python/WASM/Rust).
5
+ *
6
+ * Three rules give this file its shape:
7
+ *
8
+ * 1. **Records are opaque.** core owns canonical serialization and hashing
9
+ * (`KernelRecord::record_bytes()` / `record_digest()` / `expected_head()`). The host stores the
10
+ * bytes verbatim and indexes them by the digest core handed it. A journal that re-serializes a
11
+ * record to recompute its hash would make "the host recomputed and disagreed" a reachable state;
12
+ * it is not one here.
13
+ * 2. **CAS is a storage-layer primitive, not a read-compare-write sequence.** §9.1 requires a real
14
+ * atomic operation (file lock / `O_EXCL` chained naming / conditional database update).
15
+ * `InMemoryKernelJournal` is atomic only within one process and says so in its own type;
16
+ * `FileKernelJournal` is atomic across processes (see its class docs).
17
+ * 3. **The journal's sequence space is `step_seq`** — the operation's record-chain position — and is
18
+ * completely independent of `SessionLog`'s business event `seq`. Pruning a journal prefix can
19
+ * never punch a hole in business event numbering (spec Task 8b, criterion 4).
20
+ *
21
+ * Failures are typed so a caller can tell "retry after rebuild" from "this storage is broken" from
22
+ * "someone handed me a corrupt chain" — a durable-step wrapper must never publish effects on any of
23
+ * them, and must never collapse them into one opaque `Error`.
24
+ */
25
+ export declare const MAX_CHAIN_POSITION = 1000000000000;
26
+ /**
27
+ * The CAS precondition did not hold: the journal head (or checkpoint pointer) moved.
28
+ *
29
+ * Retryable — the protocol response is `abort(token)` → re-read head → rebuild → replay the input
30
+ * (spec §8.3, row "CAS conflict").
31
+ */
32
+ export declare class JournalCasConflictError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ /**
36
+ * The journal contents contradict themselves or the caller's claim: a broken digest chain, a
37
+ * `step_seq` that does not follow its predecessor, a checkpoint whose `covered_head` does not match
38
+ * the record at its `through_step_seq`. Never retryable — retrying replays the same contradiction.
39
+ */
40
+ export declare class JournalIntegrityError extends Error {
41
+ constructor(message: string);
42
+ }
43
+ /**
44
+ * The storage layer failed (disk full, permission denied, hard links unsupported). Distinct from
45
+ * both of the above: the journal state is *unknown*, not known-conflicting and not known-corrupt.
46
+ */
47
+ export declare class JournalIoError extends Error {
48
+ constructor(message: string, options?: {
49
+ cause?: unknown;
50
+ });
51
+ }
52
+ /** What a host hands the journal: core's opaque bytes plus the identity core assigned them. */
53
+ export interface JournalRecordInput {
54
+ /** Chain position. Genesis (`ConfigureOperation`) is 0; every later record is `head + 1`. */
55
+ step_seq: number;
56
+ /** core's `record_digest`. The journal indexes by it and never recomputes it. */
57
+ record_digest: string;
58
+ /** core's `record_bytes`, stored verbatim. */
59
+ record_bytes: Uint8Array;
60
+ }
61
+ /** A stored record. `previous_record_digest` is the CAS precondition it was appended under. */
62
+ export interface JournalEntry extends JournalRecordInput {
63
+ /** Absent on the genesis record only (spec §8.1: the first append's expected head is empty). */
64
+ previous_record_digest?: string;
65
+ }
66
+ /** The journal head: the last record of an operation's chain. */
67
+ export interface JournalHead {
68
+ step_seq: number;
69
+ record_digest: string;
70
+ }
71
+ export interface JournalAppendReceipt {
72
+ step_seq: number;
73
+ record_digest: string;
74
+ }
75
+ /** The host-persistable part of `kernel.checkpoint_candidate()` (spec §12.3). */
76
+ export interface CheckpointCandidate {
77
+ /** Stable identity of this checkpoint; the CAS token a later install names as its predecessor. */
78
+ checkpoint_id: string;
79
+ /** The chain position this checkpoint's logical state covers. */
80
+ through_step_seq: number;
81
+ /** core's digest of the logical state. Opaque to the journal. */
82
+ state_digest: string;
83
+ /** The serialized checkpoint blob, stored verbatim. */
84
+ checkpoint_bytes: Uint8Array;
85
+ }
86
+ export interface InstalledCheckpoint extends CheckpointCandidate {
87
+ /** Monotonic install ordinal. `previous_checkpoint_id === undefined` installs ordinal 0. */
88
+ ordinal: number;
89
+ /** The record digest at `through_step_seq` — verified at install, not required to still be head. */
90
+ covered_head: string;
91
+ previous_checkpoint_id?: string;
92
+ /**
93
+ * Whether `ackCheckpoint` has run. Prefix reclamation is gated on this: a checkpoint that is
94
+ * installed but not acknowledged is recoverable-from but not prune-authorising (spec §12.3 rule 5/6).
95
+ */
96
+ acknowledged: boolean;
97
+ }
98
+ export interface JournalPruneReceipt {
99
+ /** Highest `step_seq` no longer retained. `-1` when nothing has ever been pruned. */
100
+ pruned_through_step_seq: number;
101
+ pruned_count: number;
102
+ }
103
+ /**
104
+ * The durable transaction capability (spec §9.1). Deliberately *not* part of `SessionLog`: a custom
105
+ * business-projection log must never be forced to masquerade as a transactional journal (§9.4).
106
+ * One class may implement both — `InMemorySessionLog` does — but the interfaces stay separate.
107
+ *
108
+ * Guarantees an implementation owes (spec §9.1):
109
+ * - strict ordering within an operation;
110
+ * - a failed CAS never overwrites;
111
+ * - record bytes preserved verbatim;
112
+ * - checkpoint pointer advances monotonically;
113
+ * - the checkpoint store verifies `covered_head` against `through_step_seq` but does **not** require
114
+ * it to still be the current transaction head (§22.14);
115
+ * - checkpoint pointer and prefix pruning have an explicit acknowledgement boundary.
116
+ */
117
+ export interface KernelJournal {
118
+ /**
119
+ * Atomically append `record` iff the operation's head is exactly `expectedHead`.
120
+ *
121
+ * @param expectedHead `undefined` starts the chain (genesis; `record.step_seq` must be 0).
122
+ * @throws {JournalCasConflictError} head moved, or a genesis already exists.
123
+ * @throws {JournalIntegrityError} `step_seq` does not follow the head.
124
+ * @throws {JournalIoError} storage failure.
125
+ */
126
+ compareAndAppend(operationId: string, expectedHead: string | undefined, record: JournalRecordInput): Promise<JournalAppendReceipt>;
127
+ /** The current head, or `undefined` when the operation has no records and no pruned anchor. */
128
+ head(operationId: string): Promise<JournalHead | undefined>;
129
+ /** Records with `step_seq >= fromStepSeq` (default: the whole retained chain), in chain order. */
130
+ readFrom(operationId: string, fromStepSeq?: number): Promise<JournalEntry[]>;
131
+ /**
132
+ * Records strictly after the record whose digest is `afterHead` — the digest-anchored cursor
133
+ * §9.1 names `records_after(operation_id, checkpoint_head)`. `undefined` returns everything retained.
134
+ *
135
+ * @throws {JournalIntegrityError} `afterHead` names no retained record and no pruned anchor.
136
+ */
137
+ recordsAfter(operationId: string, afterHead?: string): Promise<JournalEntry[]>;
138
+ /**
139
+ * Atomically install `checkpoint` iff the operation's checkpoint pointer is exactly
140
+ * `previousCheckpointId`, and `coveredHead` is the record digest at `checkpoint.through_step_seq`.
141
+ *
142
+ * Per §22.14 this deliberately does **not** require `coveredHead` to still be the current
143
+ * transaction head: transactions appended after the candidate was taken stay as tail.
144
+ *
145
+ * @param previousCheckpointId `undefined` installs the operation's first checkpoint.
146
+ * @throws {JournalCasConflictError} the checkpoint pointer moved.
147
+ * @throws {JournalIntegrityError} `coveredHead`/`through_step_seq` disagree with the chain, or
148
+ * `through_step_seq` would move the pointer backwards.
149
+ */
150
+ compareAndInstallCheckpoint(operationId: string, previousCheckpointId: string | undefined, coveredHead: string, checkpoint: CheckpointCandidate): Promise<InstalledCheckpoint>;
151
+ /** The highest-ordinal installed checkpoint, acknowledged or not. */
152
+ latestCheckpoint(operationId: string): Promise<InstalledCheckpoint | undefined>;
153
+ /**
154
+ * Record the durable acknowledgement that opens the prefix-reclamation boundary. Idempotent.
155
+ *
156
+ * @throws {JournalIntegrityError} no checkpoint with that id is installed.
157
+ */
158
+ ackCheckpoint(operationId: string, checkpointId: string): Promise<InstalledCheckpoint>;
159
+ /**
160
+ * Reclaim the record prefix covered by the latest **acknowledged** checkpoint. A no-op while no
161
+ * checkpoint is acknowledged. The pruned boundary is retained as an anchor so `head()` and the
162
+ * next CAS still resolve on a fully-pruned chain.
163
+ */
164
+ pruneAckedPrefix(operationId: string): Promise<JournalPruneReceipt>;
165
+ /**
166
+ * Durably stage one outbound WireEnvelope JSON for `operationId` until the matching record is
167
+ * append-acked (or the attempt is abandoned). Overwrites any previous pending envelope.
168
+ *
169
+ * Required by Phase 6 host cutover (adjudication 5e.3): retries must replay byte-identical
170
+ * envelopes — reminting `observed_at_ms` after a crash produces `DuplicateInputConflict`.
171
+ */
172
+ stageOutboundEnvelope(operationId: string, envelopeJson: string): Promise<void>;
173
+ /** Read the staged outbound envelope, if any. */
174
+ readOutboundEnvelope(operationId: string): Promise<string | undefined>;
175
+ /** Clear the staged outbound envelope. Idempotent. */
176
+ clearOutboundEnvelope(operationId: string): Promise<void>;
177
+ }
178
+ /**
179
+ * **Single-process dev/test implementation.**
180
+ *
181
+ * CAS is genuinely atomic here — every check-then-mutate below runs to completion without an
182
+ * intervening `await`, so no other task can interleave on a single-threaded runtime — but that
183
+ * atomicity ends at the process boundary. Two processes sharing "the same" journal do not exist:
184
+ * each has its own `Map`. Production hosts must supply a `KernelJournal` whose CAS is a real
185
+ * storage-layer primitive (spec §9.1); `FileKernelJournal` is the reference for that.
186
+ */
187
+ export declare class InMemoryKernelJournal implements KernelJournal {
188
+ private readonly operations;
189
+ private state;
190
+ private headOf;
191
+ compareAndAppend(operationId: string, expectedHead: string | undefined, record: JournalRecordInput): Promise<JournalAppendReceipt>;
192
+ head(operationId: string): Promise<JournalHead | undefined>;
193
+ readFrom(operationId: string, fromStepSeq?: number): Promise<JournalEntry[]>;
194
+ recordsAfter(operationId: string, afterHead?: string): Promise<JournalEntry[]>;
195
+ compareAndInstallCheckpoint(operationId: string, previousCheckpointId: string | undefined, coveredHead: string, checkpoint: CheckpointCandidate): Promise<InstalledCheckpoint>;
196
+ latestCheckpoint(operationId: string): Promise<InstalledCheckpoint | undefined>;
197
+ ackCheckpoint(operationId: string, checkpointId: string): Promise<InstalledCheckpoint>;
198
+ pruneAckedPrefix(operationId: string): Promise<JournalPruneReceipt>;
199
+ stageOutboundEnvelope(operationId: string, envelopeJson: string): Promise<void>;
200
+ readOutboundEnvelope(operationId: string): Promise<string | undefined>;
201
+ clearOutboundEnvelope(operationId: string): Promise<void>;
202
+ }
203
+ /**
204
+ * **Cross-process atomic reference implementation** of `KernelJournal` (spec Task 8b).
205
+ *
206
+ * The atomicity primitive is POSIX `link(2)`: content is written to a private temp file and fsynced,
207
+ * then hard-linked into its final name. `link` fails with `EEXIST` if the name is taken, and it
208
+ * publishes already-complete content — so a crash can never leave a half-written `.rec`, only an
209
+ * orphan temp file that the naming rule ignores. The journal root must therefore live on a
210
+ * filesystem that supports hard links; that requirement is the price of real CAS.
211
+ *
212
+ * **Why the record filename is `<step_seq>.rec` and contains no digest.** The filename *is* the
213
+ * collision domain. Two writers racing on the same head both compute the same next `step_seq`, so
214
+ * they contend for one name and exactly one wins. Folding a per-writer value (the new record's
215
+ * digest) into the name would give the racers *different* names — both `link`s would succeed and the
216
+ * chain would fork. Only the predecessor-determined part of the identity may appear in the name.
217
+ *
218
+ * The pre-`link` head check is not a TOCTOU hole: it can only *reject* an append that `link` would
219
+ * have accepted (a stale `expectedHead` whose `step_seq` slot happens to be free), never accept one
220
+ * `link` would have rejected. Every acceptance is still decided by the atomic `link`.
221
+ *
222
+ * Checkpoint installs use the same primitive on a separate ordinal space
223
+ * (`<ordinal>.ckpt`), so two processes installing on the same predecessor also contend for one name.
224
+ */
225
+ export declare class FileKernelJournal implements KernelJournal {
226
+ private readonly root;
227
+ constructor(root: string);
228
+ private operationDir;
229
+ private recordsDir;
230
+ private checkpointsDir;
231
+ private tmpDir;
232
+ private prunedPath;
233
+ /**
234
+ * Write `payload` to a temp file, fsync it, then atomically claim `target` by hard link.
235
+ *
236
+ * @returns `false` when the name was already taken — i.e. a lost CAS race.
237
+ */
238
+ private publish;
239
+ private syncDir;
240
+ /** Sorted `step_seq` values of the retained records. Anything not matching the rule is residue. */
241
+ private recordSeqs;
242
+ private readRecord;
243
+ private prunedAnchor;
244
+ head(operationId: string): Promise<JournalHead | undefined>;
245
+ compareAndAppend(operationId: string, expectedHead: string | undefined, record: JournalRecordInput): Promise<JournalAppendReceipt>;
246
+ readFrom(operationId: string, fromStepSeq?: number): Promise<JournalEntry[]>;
247
+ recordsAfter(operationId: string, afterHead?: string): Promise<JournalEntry[]>;
248
+ /** Sorted ordinals of installed checkpoints. */
249
+ private checkpointOrdinals;
250
+ private ackedOrdinals;
251
+ private readCheckpoint;
252
+ latestCheckpoint(operationId: string): Promise<InstalledCheckpoint | undefined>;
253
+ compareAndInstallCheckpoint(operationId: string, previousCheckpointId: string | undefined, coveredHead: string, checkpoint: CheckpointCandidate): Promise<InstalledCheckpoint>;
254
+ ackCheckpoint(operationId: string, checkpointId: string): Promise<InstalledCheckpoint>;
255
+ pruneAckedPrefix(operationId: string): Promise<JournalPruneReceipt>;
256
+ /** The anchor only ever moves forward, so an atomic overwriting `rename` is the right primitive. */
257
+ private writeAnchor;
258
+ private outboundPath;
259
+ /** Overwriteable durable blob — same write-tmp→fsync→rename pattern as the pruned anchor. */
260
+ private writeReplaceable;
261
+ stageOutboundEnvelope(operationId: string, envelopeJson: string): Promise<void>;
262
+ readOutboundEnvelope(operationId: string): Promise<string | undefined>;
263
+ clearOutboundEnvelope(operationId: string): Promise<void>;
264
+ }