@deepstrike/sdk 0.2.40 → 0.2.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,13 +88,13 @@ The root export is the **intent layer** — what you reach for to run an agent,
88
88
 
89
89
  | Import | Contains |
90
90
  |--------|----------|
91
- | `@deepstrike/sdk` | `runAgent` · `runFanout` · `RuntimeRunner` · `tool` · `LocalExecutionPlane` · `InMemorySessionLog`/`FileSessionLog` · `AnthropicProvider`/`OpenAIProvider`/`OpenAIResponsesProvider` · `createProvider` · `Governance` · `AgentPool` · core types |
91
+ | `@deepstrike/sdk` | `runAgent` · `runFanout` · `RuntimeRunner` · `tool` · `LocalExecutionPlane` · `InMemorySessionLog`/`FileSessionLog` · `AnthropicProvider`/`OpenAIProvider`/`OpenAIResponsesProvider` · `createProvider` · `Governance` · `AgentPool` · `operationAbortSignal` · core types |
92
92
  | `@deepstrike/sdk/providers` | backend factories (`deepseek`, `kimi`, `qwen`, `glm`, `minimax`, `gemini`, `ollama`), profiles, `CircuitBreaker` |
93
93
  | `@deepstrike/sdk/workflow` | `SubAgentOrchestrator`, `spawnStandalone`, reducers, contracts, handoff/modes, agent + spec types |
94
94
  | `@deepstrike/sdk/planes` | `WorktreeExecutionPlane`, `ProcessSandboxPlane`, `McpProxyPlane`, `RemoteVpcPlane`, archive/credential stores |
95
- | `@deepstrike/sdk/memory` | `DreamStore`, `WorkingMemory`, `InMemoryDreamStore`, `KnowledgeSource` |
95
+ | `@deepstrike/sdk/memory` | `DreamStore`, `WorkingMemory`, `InMemoryDreamStore`, `rankMemories`, `extractSessionMemories`, `KnowledgeSource` |
96
96
  | `@deepstrike/sdk/harness` | `AttemptLoop`, body/judge/carry policies, `judge` |
97
- | `@deepstrike/sdk/os` | profiles, `KernelPrimitivesDashboard`, signals, `PermissionManager`, replay-testing utilities |
97
+ | `@deepstrike/sdk/os` | profiles, `KernelPrimitivesDashboard`, `primitiveForKind` / `KernelPrimitive`, signals, `PermissionManager`, replay-testing utilities |
98
98
 
99
99
  > **Migration from 0.2.x:** the kernel-lowering converters (`*ToKernel`), low-level prompt/eval builders, and the `OpenAIChatProvider` alias are no longer exported from root; backend providers, planes, memory, harness, and OS utilities moved to the subpaths above. See [`MIGRATION-v0.2.300.md`](./MIGRATION-v0.2.300.md).
100
100
 
@@ -245,7 +245,7 @@ A node's `kind` selects the control-flow shape; the same executor drives them al
245
245
 
246
246
  ### 0.2.11 capabilities
247
247
 
248
- - **Runtime fan-out** — give a node the `submitWorkflowNodesTool` and its agent can append nodes to the live DAG mid-run (true loop-until-done; one verifier per claim it discovers). Recorded and replayed on `resumeWorkflow`.
248
+ - **Runtime fan-out** — give a node the `submitWorkflowNodesTool` and its agent can append nodes to the live DAG mid-run (true loop-until-done; one verifier per claim it discovers). Recorded and replayed on `resumeWorkflow`. Governance rejection fails the submitting node instead of acknowledging work that was never appended.
249
249
  - **Quarantine, no escape** — set `trust: "quarantined"` on a node that reads untrusted content; it's denied write-capable isolation in-kernel, and any nodes it submits are coerced to quarantined too (no privilege escalation).
250
250
  - **Structured output** — set `outputSchema` on a node; the runner instructs the agent, validates the result against the JSON-Schema subset, and re-runs once with the errors on mismatch. A node that never conforms fails (its dependents starve).
251
251
  - **Budget as signal** — with a `maxWorkflowNodes` / `maxConcurrentSubagents` quota installed, each spawned node's goal carries its remaining headroom so a coordinator can size its fan-out to fit.
@@ -719,7 +719,7 @@ for await (const evt of runner.spawnSubAgent({
719
719
  }
720
720
  ```
721
721
 
722
- Requires an active parent run (`run()` / `wake()` in progress). The kernel emits `agent_process_changed`; the default `SubAgentOrchestrator` runs the child with a filtered execution plane and feeds `sub_agent_completed` back.
722
+ Requires an active parent run (`run()` / `wake()` in progress). The kernel emits `agent_process_changed`; the default `SubAgentOrchestrator` runs the child with a filtered execution plane and feeds `sub_agent_completed` back. If governance rejects the spawn before execution, the iterator yields one `error` event containing the denial reason; the parent turn is not rolled back.
723
723
 
724
724
  ---
725
725
 
@@ -30,11 +30,10 @@ export interface GovernancePolicy {
30
30
  }[];
31
31
  constraints?: GovernanceConstraint[];
32
32
  /** I5: when true (default), the runner pre-filters denied tools out of the schema passed to the
33
- * provider — the model never sees them and never tries to call them, eliminating the rollback
34
- * turn the kernel would otherwise produce. The denied tool names are also surfaced as a single
35
- * line on the system slot so the model knows not to plan around them. Set to false to fall
36
- * back to the v0.2.22 rollback-based behavior (useful for measuring the delta or when the
37
- * agent should learn the denial via a real attempt). */
33
+ * provider — the model never sees them and never tries to call them. The denied tool names are
34
+ * also surfaced as a single line on the system slot so the model knows not to plan around them.
35
+ * Set to false when the agent should learn the denial through a real attempted call and its
36
+ * visible error tool result. */
38
37
  surfaceDeniedInSystem?: boolean;
39
38
  }
40
39
  /** I5: walk the tool list and bucket each tool into `allowed` / `denied` based on a declarative
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export type { RunGroup, GroupBudgetStore, GroupLedger, GroupCharge, GroupMember,
22
22
  export { InMemoryEventStream, isVisibleTo } from "./runtime/event-stream.js";
23
23
  export type { EventStream, EventStreamOptions, BlackboardEvent, EventViewer } from "./runtime/event-stream.js";
24
24
  export type { ObserverFailure, ObserverErrorHandler } from "./runtime/reliability.js";
25
- export { ManagedTaskScope } from "./runtime/reliability.js";
25
+ export { ManagedTaskScope, operationAbortSignal } from "./runtime/reliability.js";
26
26
  export type { OperationContext, BackgroundTaskFailure, BackgroundTaskErrorHandler } from "./runtime/reliability.js";
27
27
  export { reactByMention, directorDriven, roundRobin, firstNonEmpty, union } from "./runtime/turn-policy.js";
28
28
  export type { TurnPolicy, PeerView } from "./runtime/turn-policy.js";
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ export { InMemorySessionLog, FileSessionLog } from "./runtime/session-log.js";
24
24
  export { KERNEL_LOG_RECORD_VERSION, KernelLogConflictError, KernelLogIntegrityError, canonicalKernelJson, createKernelOperationGenesis, createKernelTransaction, kernelRecordDigest, verifyKernelOperationGenesis, verifyKernelTransaction, verifyKernelTransactionStream, verifyKernelTransactionSuccessor, } from "./runtime/kernel-transaction-log.js";
25
25
  export { InMemoryGroupBudgetStore, GroupBudgetScope } from "./runtime/run-group.js";
26
26
  export { InMemoryEventStream, isVisibleTo } from "./runtime/event-stream.js";
27
- export { ManagedTaskScope } from "./runtime/reliability.js";
27
+ export { ManagedTaskScope, operationAbortSignal } from "./runtime/reliability.js";
28
28
  export { reactByMention, directorDriven, roundRobin, firstNonEmpty, union } from "./runtime/turn-policy.js";
29
29
  export { ReactiveSession, readRecentTool } from "./runtime/reactive-session.js";
30
30
  export { InMemoryReactionCheckpointStore, ReactionInProgressError } from "./runtime/reaction-checkpoint.js";
package/dist/kernel.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface ResourceQuota {
25
25
  maxTotalSubagents?: number;
26
26
  /** Max sub-agent nesting depth (direct children of the root loop are depth 1). */
27
27
  maxSpawnDepth?: number;
28
+ /** Max nodes in one in-kernel workflow DAG, including dynamically submitted nodes. */
29
+ maxWorkflowNodes?: number;
28
30
  /** Rolling-window memory-write rate limit: at most `maxWrites` per any `windowMs` span. */
29
31
  memoryWritesPerWindow?: MemoryWriteRateLimit;
30
32
  }
@@ -50,6 +52,9 @@ export interface MemoryPolicy {
50
52
  maxContentBytes?: number;
51
53
  /** Override the kernel's `write_memory` name-length limit. */
52
54
  maxNameLength?: number;
55
+ /** M4: recall count at which the kernel emits an advisory (edge-triggered)
56
+ * `promotion_suggested` for a recalled record. Omitted = suggestions disabled. */
57
+ promotionRecallThreshold?: number;
53
58
  }
54
59
  export interface GovernanceInstance {
55
60
  setIdentity(agentId: string, sessionId: string): void;
@@ -2,5 +2,8 @@ export { WorkingMemory } from "./working.js";
2
2
  export { InMemoryDreamStore } from "./in-memory-store.js";
3
3
  export type { InMemoryDreamStoreOptions } from "./in-memory-store.js";
4
4
  export { memoryRetentionScore } from "./retention.js";
5
+ export { rankMemories } from "./ranking.js";
6
+ export type { RankableMemory, RankedMemory, RankOptions } from "./ranking.js";
7
+ export { extractSessionMemories, parseExtractedMemories } from "./extraction.js";
5
8
  export type { DreamStore, SessionData, SessionMessage, MemoryRecord, MemoryRecall, MemoryQuery, MemoryScope, MemoryProvenance, MemoryRecallLifecycle, MemoryKind, MemoryAuthor, MemoryTrustLevel, } from "./protocols.js";
6
9
  export type { KnowledgeSource } from "../knowledge/source.js";
@@ -2,3 +2,5 @@
2
2
  export { WorkingMemory } from "./working.js";
3
3
  export { InMemoryDreamStore } from "./in-memory-store.js";
4
4
  export { memoryRetentionScore } from "./retention.js";
5
+ export { rankMemories } from "./ranking.js";
6
+ export { extractSessionMemories, parseExtractedMemories } from "./extraction.js";
@@ -1,4 +1,4 @@
1
- interface RankableMemory<T> {
1
+ export interface RankableMemory<T> {
2
2
  value: T;
3
3
  searchableText: string;
4
4
  updatedAt: number;
@@ -30,4 +30,3 @@ export interface RankOptions {
30
30
  * The score is relevance, deliberately distinct from the record's stored confidence.
31
31
  */
32
32
  export declare function rankMemories<T>(query: string, candidates: Array<RankableMemory<T>>, topK: number, opts?: RankOptions): Array<RankedMemory<T>>;
33
- export {};
@@ -2,7 +2,8 @@ export { DEFAULT_NATIVE_SIGNAL_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, assertN
2
2
  export type { NativeOsProfile, OsProfileId, SignalPolicy } from "../runtime/os-profile.js";
3
3
  export { rebuildOsSnapshotFromSessionEvents } from "../runtime/os-snapshot.js";
4
4
  export type { OsSnapshot } from "../runtime/os-snapshot.js";
5
- export type { KernelEventCategory } from "../runtime/kernel-event-log.js";
5
+ export type { KernelEventCategory, KernelPrimitive } from "../runtime/kernel-event-log.js";
6
+ export { primitiveForCategory, primitiveForKind } from "../runtime/kernel-event-log.js";
6
7
  export { KernelPrimitivesDashboard } from "../runtime/kernel-primitives-dashboard.js";
7
8
  export type { MemoryPolicy, MemoryWriteRateLimit, ResourceQuota } from "../kernel.js";
8
9
  export type { PromptBudget, SchedulerPolicy } from "../runtime/runner.js";
package/dist/os/public.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // and the scheduler/quota/policy types referenced by advanced `RuntimeOptions` fields.
3
3
  export { DEFAULT_NATIVE_SIGNAL_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, assertNativeProfile, osProfile, } from "../runtime/os-profile.js";
4
4
  export { rebuildOsSnapshotFromSessionEvents } from "../runtime/os-snapshot.js";
5
+ export { primitiveForCategory, primitiveForKind } from "../runtime/kernel-event-log.js";
5
6
  export { KernelPrimitivesDashboard } from "../runtime/kernel-primitives-dashboard.js";
6
7
  // Signals + SDK-side permissions.
7
8
  export { ScheduledPrompt } from "../signals/scheduled.js";
@@ -157,6 +157,9 @@ export type KernelRunnerAction = {
157
157
  };
158
158
  export interface KernelObservation {
159
159
  kind: string;
160
+ /** control_request_rejected: stable control-plane operation name and optional subject id. */
161
+ operation?: string;
162
+ subject?: string;
160
163
  action?: string;
161
164
  rho_after?: number;
162
165
  sprint?: number;
@@ -374,6 +374,23 @@ export declare class RuntimeRunner {
374
374
  private commitKernelAction;
375
375
  private persistMemoryToStore;
376
376
  private retrieveMemoryFromStore;
377
+ /**
378
+ * T5: run one memory query through the kernel's `query_memory → memory_query_result`
379
+ * effect lifecycle on the given runtime. The kernel injects each routed hit into history
380
+ * itself and derives the recall lifecycle (`memory_recalled`, edge-triggered
381
+ * `promotion_suggested`) from the routed hits — the store stays a pure query.
382
+ *
383
+ * `seenRecordIds` is one dedupe horizon: a record hit by several queries of the same
384
+ * prefetch is routed (recalled, injected) once. The kernel derives counts statelessly from
385
+ * each hit's payload, so host-side pre-filtering is the only place duplicates can be stopped.
386
+ *
387
+ * The recall lifecycle is consumed immediately rather than via the per-turn drain: the store
388
+ * must be up to date before any same-turn re-query, and a renewal prefetch fires inside the
389
+ * drain loop where queued observations would not be consumed until the next boundary.
390
+ * Non-memory observations are forwarded to `leftovers` (the run's pending queue) when given,
391
+ * and discarded for detached syscall runtimes (their kernel is throwaway).
392
+ */
393
+ private queryMemoryThroughKernel;
377
394
  writeMemory(memory: MemoryRecord, opts?: {
378
395
  sessionId?: string;
379
396
  agentId?: string;
@@ -394,7 +411,17 @@ export declare class RuntimeRunner {
394
411
  * every policy from the first spawn. No config ⇒ the native-profile defaults (铁律: defaults only).
395
412
  */
396
413
  private applyKernelPolicies;
397
- private appendMemorySyscallObservations;
414
+ /**
415
+ * Mirror one kernel memory-lifecycle observation into the durable store / host callbacks.
416
+ * Shared by the main run drain, the prefetch path, and the host memory syscalls so every
417
+ * query route has identical recall + promotion semantics (T5).
418
+ *
419
+ * M3: `memory_recalled` carries the kernel-derived count — the runner never computes
420
+ * `recall_count + 1` itself. M4: `promotion_suggested` is advisory and already
421
+ * edge-triggered by the kernel; the runner surfaces it and never auto-pins.
422
+ */
423
+ private mirrorMemoryLifecycle;
424
+ private consumeMemoryLifecycleObservations;
398
425
  /** Mount a tool capability on the currently-running kernel runtime. No-op if not running. */
399
426
  mountTool(schema: ToolSchema): Promise<void>;
400
427
  /** Mount a skill capability on the currently-running kernel runtime. No-op if not running. */
@@ -34,6 +34,25 @@ export function schedulerPolicyToKernel(policy) {
34
34
  token_cost_weight: policy.tokenCostWeight,
35
35
  };
36
36
  }
37
+ /** Kernel observation kinds owned by the memory lifecycle consumer (journal + store mirror). */
38
+ function isMemoryLifecycleObservation(obs) {
39
+ return obs.kind === "memory_written"
40
+ || obs.kind === "memory_queried"
41
+ || obs.kind === "memory_validation_failed"
42
+ || obs.kind === "memory_recalled"
43
+ || obs.kind === "promotion_suggested";
44
+ }
45
+ function controlRequestRejection(observations, operation) {
46
+ const rejected = observations.find(observation => observation.kind === "control_request_rejected"
47
+ && (!operation || observation.operation === operation));
48
+ if (!rejected)
49
+ return undefined;
50
+ return {
51
+ operation: rejected.operation ?? operation ?? "control_request",
52
+ ...(rejected.subject ? { subject: rejected.subject } : {}),
53
+ reason: typeof rejected.reason === "string" ? rejected.reason : "request denied",
54
+ };
55
+ }
37
56
  function pendingCallIds(action) {
38
57
  switch (action.kind) {
39
58
  case "call_provider":
@@ -126,6 +145,66 @@ export class RuntimeRunner {
126
145
  return (await this.opts.dreamStore.search(agentId, { ...query, top_k: requestedK }))
127
146
  .slice(0, requestedK);
128
147
  }
148
+ /**
149
+ * T5: run one memory query through the kernel's `query_memory → memory_query_result`
150
+ * effect lifecycle on the given runtime. The kernel injects each routed hit into history
151
+ * itself and derives the recall lifecycle (`memory_recalled`, edge-triggered
152
+ * `promotion_suggested`) from the routed hits — the store stays a pure query.
153
+ *
154
+ * `seenRecordIds` is one dedupe horizon: a record hit by several queries of the same
155
+ * prefetch is routed (recalled, injected) once. The kernel derives counts statelessly from
156
+ * each hit's payload, so host-side pre-filtering is the only place duplicates can be stopped.
157
+ *
158
+ * The recall lifecycle is consumed immediately rather than via the per-turn drain: the store
159
+ * must be up to date before any same-turn re-query, and a renewal prefetch fires inside the
160
+ * drain loop where queued observations would not be consumed until the next boundary.
161
+ * Non-memory observations are forwarded to `leftovers` (the run's pending queue) when given,
162
+ * and discarded for detached syscall runtimes (their kernel is throwaway).
163
+ */
164
+ async queryMemoryThroughKernel(runtime, query, agentId, sessionId, seenRecordIds, leftovers) {
165
+ const observations = [];
166
+ const action = await this.commitKernelAction(runtime, observations, { kind: "query_memory", query }, sessionId);
167
+ if (action.kind !== "query_memory") {
168
+ throw new Error(`query_memory returned unexpected kernel effect: ${action.kind}`);
169
+ }
170
+ let hits = [];
171
+ let ioError;
172
+ try {
173
+ hits = await this.retrieveMemoryFromStore(query, action.requestedK, agentId);
174
+ if (seenRecordIds) {
175
+ hits = hits.filter(hit => {
176
+ const id = hit.record.record_id;
177
+ if (seenRecordIds.has(id))
178
+ return false;
179
+ seenRecordIds.add(id);
180
+ return true;
181
+ });
182
+ }
183
+ }
184
+ catch (cause) {
185
+ ioError = cause;
186
+ }
187
+ // Close the kernel effect even when the store failed — never leave a dangling query.
188
+ // The result commit resumes the kernel's reasoning path (`resume_after_preload`), which
189
+ // re-emits the next loop action with the routed hits now in history — callers on a live
190
+ // run kernel must adopt it (a query is a preload, not a detached side-channel).
191
+ const resumed = await this.commitKernelMaybeAction(runtime, observations, {
192
+ kind: "memory_query_result",
193
+ effect_id: action.effectId,
194
+ hits,
195
+ ...(ioError ? { error: formatToolError(ioError) } : {}),
196
+ }, sessionId);
197
+ await this.consumeMemoryLifecycleObservations(sessionId, observations);
198
+ if (leftovers) {
199
+ for (const obs of observations) {
200
+ if (!isMemoryLifecycleObservation(obs))
201
+ leftovers.push(obs);
202
+ }
203
+ }
204
+ if (ioError)
205
+ throw ioError;
206
+ return { hits, action: resumed };
207
+ }
129
208
  async writeMemory(memory, opts = {}) {
130
209
  const sessionId = opts.sessionId ?? this.currentSessionId;
131
210
  const agentId = opts.agentId ?? this.opts.agentId;
@@ -136,7 +215,7 @@ export class RuntimeRunner {
136
215
  const runtime = this.createSyscallRuntime();
137
216
  const action = await this.commitKernelMaybeAction(runtime, observations, { kind: "write_memory", memory }, durableSessionId);
138
217
  if (!action) {
139
- await this.appendMemorySyscallObservations(sessionId, observations);
218
+ await this.consumeMemoryLifecycleObservations(sessionId, observations);
140
219
  return;
141
220
  }
142
221
  if (action.kind !== "persist_memory") {
@@ -154,7 +233,7 @@ export class RuntimeRunner {
154
233
  effect_id: action.effectId,
155
234
  ...(ioError ? { error: formatToolError(ioError) } : {}),
156
235
  }, durableSessionId);
157
- await this.appendMemorySyscallObservations(durableSessionId, observations);
236
+ await this.consumeMemoryLifecycleObservations(durableSessionId, observations);
158
237
  if (ioError)
159
238
  throw ioError;
160
239
  }
@@ -164,29 +243,11 @@ export class RuntimeRunner {
164
243
  if (!this.opts.dreamStore || !agentId)
165
244
  return [];
166
245
  const durableSessionId = this.durableSessionId(sessionId);
167
- const observations = [];
246
+ // Detached syscall runtime: the kernel's history injection and resumed action are inert
247
+ // (the kernel is throwaway), but the recall lifecycle is identical to an in-run query
248
+ // (T5 parity).
168
249
  const runtime = this.createSyscallRuntime();
169
- const action = await this.commitKernelAction(runtime, observations, { kind: "query_memory", query }, durableSessionId);
170
- if (action.kind !== "query_memory") {
171
- throw new Error(`query_memory returned unexpected kernel effect: ${action.kind}`);
172
- }
173
- let hits = [];
174
- let ioError;
175
- try {
176
- hits = await this.retrieveMemoryFromStore(query, action.requestedK, agentId);
177
- }
178
- catch (cause) {
179
- ioError = cause;
180
- }
181
- await this.commitKernelApply(runtime, observations, {
182
- kind: "memory_query_result",
183
- effect_id: action.effectId,
184
- hits,
185
- ...(ioError ? { error: formatToolError(ioError) } : {}),
186
- }, durableSessionId);
187
- await this.appendMemorySyscallObservations(durableSessionId, observations);
188
- if (ioError)
189
- throw ioError;
250
+ const { hits } = await this.queryMemoryThroughKernel(runtime, query, agentId, durableSessionId);
190
251
  await this.logMemoryRetrievalResult(durableSessionId, hits);
191
252
  return hits;
192
253
  }
@@ -322,6 +383,7 @@ export class RuntimeRunner {
322
383
  ...(q.maxConcurrentSubagents !== undefined ? { max_concurrent_subagents: q.maxConcurrentSubagents } : {}),
323
384
  ...(q.maxTotalSubagents !== undefined ? { max_total_subagents: q.maxTotalSubagents } : {}),
324
385
  ...(q.maxSpawnDepth !== undefined ? { max_spawn_depth: q.maxSpawnDepth } : {}),
386
+ ...(q.maxWorkflowNodes !== undefined ? { max_workflow_nodes: q.maxWorkflowNodes } : {}),
325
387
  ...(q.memoryWritesPerWindow !== undefined
326
388
  ? { memory_writes_per_window: [q.memoryWritesPerWindow.maxWrites, q.memoryWritesPerWindow.windowMs] }
327
389
  : {}),
@@ -371,14 +433,36 @@ export class RuntimeRunner {
371
433
  }
372
434
  await this.commitKernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
373
435
  }
374
- async appendMemorySyscallObservations(sessionId, observations) {
375
- if (!sessionId)
376
- return;
436
+ /**
437
+ * Mirror one kernel memory-lifecycle observation into the durable store / host callbacks.
438
+ * Shared by the main run drain, the prefetch path, and the host memory syscalls so every
439
+ * query route has identical recall + promotion semantics (T5).
440
+ *
441
+ * M3: `memory_recalled` carries the kernel-derived count — the runner never computes
442
+ * `recall_count + 1` itself. M4: `promotion_suggested` is advisory and already
443
+ * edge-triggered by the kernel; the runner surfaces it and never auto-pins.
444
+ */
445
+ async mirrorMemoryLifecycle(obs) {
446
+ if (obs.kind === "memory_recalled" && obs.recalls?.length) {
447
+ const agentId = this.opts.agentId;
448
+ if (agentId && this.opts.dreamStore?.recordRecall) {
449
+ await this.opts.dreamStore.recordRecall(agentId, obs.recalls);
450
+ }
451
+ }
452
+ if (obs.kind === "promotion_suggested" && obs.record_id) {
453
+ this.opts.onPromotionSuggested?.({
454
+ recordId: obs.record_id,
455
+ recallCount: obs.recall_count ?? 0,
456
+ });
457
+ }
458
+ }
459
+ async consumeMemoryLifecycleObservations(sessionId, observations) {
377
460
  const turn = this.activeKernel?.turn() ?? 0;
378
461
  for (const obs of observations) {
379
- if (obs.kind !== "memory_written"
380
- && obs.kind !== "memory_queried"
381
- && obs.kind !== "memory_validation_failed")
462
+ if (!isMemoryLifecycleObservation(obs))
463
+ continue;
464
+ await this.mirrorMemoryLifecycle(obs);
465
+ if (!sessionId)
382
466
  continue;
383
467
  const event = kernelObservationToSessionEvent(obs, turn);
384
468
  if (event)
@@ -459,8 +543,14 @@ export class RuntimeRunner {
459
543
  });
460
544
  this.nextArchiveStart = await this.appendObservations(parentSessionId, runtime, this.nextArchiveStart);
461
545
  const spawned = findSpawnProcessObservation(observations);
462
- if (!spawned)
546
+ if (!spawned) {
547
+ const rejected = controlRequestRejection(observations, "spawn_sub_agent");
548
+ if (rejected) {
549
+ yield { type: "error", message: `spawn_sub_agent denied: ${rejected.reason}` };
550
+ return;
551
+ }
463
552
  throw new Error("spawn_sub_agent did not emit agent_process_changed");
553
+ }
464
554
  const manifest = spawnObservationToManifest(spawned, spec, parentSessionId);
465
555
  const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
466
556
  const result = await orchestrator.run({
@@ -856,6 +946,11 @@ export class RuntimeRunner {
856
946
  }
857
947
  if (!initialAction)
858
948
  return { nodeOutcomes: [], outputs: {} };
949
+ const workflowRejection = controlRequestRejection(observations);
950
+ if (initialAction.kind === "call_provider" && workflowRejection) {
951
+ this.workflowContinuation = initialAction;
952
+ return { nodeOutcomes: [], outputs: {}, rejection: workflowRejection };
953
+ }
859
954
  if (initialAction.kind !== "spawn_workflow") {
860
955
  throw new Error(`workflow load returned unexpected kernel effect: ${initialAction.kind}`);
861
956
  }
@@ -915,6 +1010,21 @@ export class RuntimeRunner {
915
1010
  const observationStart = this.pendingObservations.length;
916
1011
  const submitAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, submitEvent);
917
1012
  const subObs = this.pendingObservations.slice(observationStart);
1013
+ const rejected = controlRequestRejection(subObs, "submit_workflow_nodes")
1014
+ ?? (subObs.find(o => o.kind === "nodes_rejected")
1015
+ ? { operation: "submit_workflow_nodes", reason: String(subObs.find(o => o.kind === "nodes_rejected")?.reason ?? "request denied") }
1016
+ : undefined);
1017
+ if (rejected) {
1018
+ const denial = `workflow node submission denied: ${rejected.reason}`;
1019
+ result.result = {
1020
+ ...result.result,
1021
+ termination: "error",
1022
+ finalMessage: { role: "assistant", content: denial, toolCalls: [] },
1023
+ };
1024
+ outputs.set(result.agentId, denial);
1025
+ if (stableId !== result.agentId)
1026
+ outputs.set(stableId, denial);
1027
+ }
918
1028
  if (submitAction?.kind === "spawn_workflow") {
919
1029
  nextNodes.push(...submitAction.nodes);
920
1030
  budget = submitAction.budget ?? budget;
@@ -1356,6 +1466,9 @@ export class RuntimeRunner {
1356
1466
  ...(m.validationEnabled !== undefined ? { validation_enabled: m.validationEnabled } : {}),
1357
1467
  ...(m.maxContentBytes !== undefined ? { max_content_bytes: m.maxContentBytes } : {}),
1358
1468
  ...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
1469
+ ...(m.promotionRecallThreshold !== undefined
1470
+ ? { promotion_recall_threshold: m.promotionRecallThreshold }
1471
+ : {}),
1359
1472
  });
1360
1473
  }
1361
1474
  if (this.opts.knowledgeSource) {
@@ -1465,21 +1578,24 @@ export class RuntimeRunner {
1465
1578
  message: attachmentsToKernelMessage(attachments),
1466
1579
  });
1467
1580
  }
1468
- // I4: pre-fetch memory before the first LLM turn so the model sees it on turn 1 instead of
1469
- // discovering it via the `memory` tool on turn 3+. Skipped on resumes (already in prior
1470
- // context) and when dreamStore/agentId is absent.
1471
- //
1472
- // Strict dynamic context control: this is single-use retrieval content (facts relevant to
1473
- // THIS run's goal right now), not a stable method/skill — so it lands in `history` as an
1474
- // ordinary turn, exactly like a real `memory` tool result would, and decays with the
1475
- // compression pyramid over subsequent turns instead of pinning itself in `knowledge` forever.
1476
1581
  this.currentGoal = goal;
1477
- if (!resumeMidRun) {
1478
- await this.prefetchMemoryIntoHistory(runtime, "initial");
1479
- }
1480
1582
  let action = resumeMidRun
1481
1583
  ? await this.commitKernelAction(runtime, this.pendingObservations, { kind: "resume" })
1482
1584
  : await this.commitKernelAction(runtime, this.pendingObservations, startPayload);
1585
+ // I4/T5: pre-fetch memory before the first LLM turn so the model sees it on turn 1 instead
1586
+ // of discovering it via the `memory` tool on turn 3+. It routes through the kernel's
1587
+ // query_memory lifecycle and therefore runs AFTER start_run — a memory query is a kernel
1588
+ // preload whose result resumes the reasoning path, so issuing it pre-run would leave the
1589
+ // kernel Running and start_run would fault. The resumed action from the last query
1590
+ // supersedes start_run's (same pull contract; it renders the injected hits). Hits land in
1591
+ // `history` as ordinary turns — single-use retrieval content that decays with the
1592
+ // compression pyramid, never pinned into `knowledge`. Skipped on resumes (already in
1593
+ // prior context) and when dreamStore/agentId is absent.
1594
+ if (!resumeMidRun) {
1595
+ const resumed = await this.prefetchMemoryIntoHistory(runtime, "initial");
1596
+ if (resumed)
1597
+ action = resumed;
1598
+ }
1483
1599
  // P0-C: the skill loaded and in effect going into the current turn (updated when the model's
1484
1600
  // `skill` tool call resolves). Drives the per-turn `activeSkill` metric → dwell measurement.
1485
1601
  let activeSkill;
@@ -1842,7 +1958,7 @@ export class RuntimeRunner {
1842
1958
  const spec = parseStartWorkflowSpec(call.arguments);
1843
1959
  if (spec) {
1844
1960
  this.pendingAuthoredWorkflows.push(spec);
1845
- const out = "workflow authored; executing now";
1961
+ const out = "workflow submitted for governance adjudication";
1846
1962
  toolResults.push({ callId: call.id, output: out, isError: false });
1847
1963
  yield { type: "tool_result", callId: call.id, content: out, isError: false };
1848
1964
  continue;
@@ -1853,13 +1969,13 @@ export class RuntimeRunner {
1853
1969
  ? parseStartWorkflowArgs(call.arguments)
1854
1970
  : parseSubmitWorkflowNodesArgs(call.arguments);
1855
1971
  yield { type: "workflow_nodes_submitted", nodes };
1856
- const result = { callId: call.id, output: "submitted", isError: false };
1972
+ const result = { callId: call.id, output: "workflow nodes submitted for parent governance adjudication", isError: false };
1857
1973
  toolResults.push(result);
1858
- yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
1974
+ yield { type: "tool_result", callId: call.id, content: result.output, isError: false };
1859
1975
  }
1860
1976
  // O5 (PreToolUse-hook analog): give the host a STATEFUL veto over each kernel-approved
1861
- // call. A blocked call never executes; its reason reaches the model as a governance-denied
1862
- // tool result (the kernel rolls the turn back with the note). Decision failures are closed
1977
+ // call. A blocked call never executes; its reason reaches the model as a committed
1978
+ // governance-denied tool result. Decision failures are closed
1863
1979
  // unless the host explicitly marks this hook advisory with `onToolCallFailure: "open"`.
1864
1980
  let executableCalls = normalCalls;
1865
1981
  if (this.opts.onToolCall) {
@@ -2195,7 +2311,7 @@ export class RuntimeRunner {
2195
2311
  * earlier memory hits, so the new sprint gets a fresh recall pass. Errs-open throughout. */
2196
2312
  async prefetchMemoryIntoHistory(runtime, phase) {
2197
2313
  if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
2198
- return;
2314
+ return undefined;
2199
2315
  // P10: recall is default-on (CC session-start recall) — with no hook configured,
2200
2316
  // the goal itself is the query. preQueryMemory stays as the targeting override.
2201
2317
  const preQuery = this.opts.preQueryMemory
@@ -2211,23 +2327,27 @@ export class RuntimeRunner {
2211
2327
  runSpec: this.opts.runSpec,
2212
2328
  phase,
2213
2329
  });
2214
- const lines = [];
2330
+ // T5: route each query through the kernel's query_memory lifecycle instead of calling
2331
+ // the store directly — the kernel injects each routed hit into history itself and the
2332
+ // recall lifecycle (recordRecall / promotion) fires exactly like an in-run query.
2333
+ //
2334
+ // One prefetch = one dedupe horizon: a record hit by several short queries recalls and
2335
+ // injects once. A renewal prefetch starts a fresh horizon — renewal dropped the earlier
2336
+ // injection with the old history, so re-exposure is a genuine new recall.
2337
+ const seenRecordIds = new Set();
2338
+ let resumed;
2215
2339
  for (const q of queries ?? []) {
2216
2340
  if (!q.query.trim())
2217
2341
  continue;
2218
- const hits = await this.opts.dreamStore.search(this.opts.agentId, q);
2219
- for (const hit of hits) {
2220
- lines.push(`[memory record_id=${hit.record.record_id} trust=${hit.record.provenance.trust} score=${hit.score.toFixed(3)}] ${hit.record.content}`);
2221
- }
2222
- }
2223
- if (lines.length > 0) {
2224
- await this.commitKernelApply(runtime, this.pendingObservations, {
2225
- kind: "add_history_message",
2226
- message: { role: "user", content: lines.join("\n") },
2227
- });
2342
+ const { action } = await this.queryMemoryThroughKernel(runtime, q, this.opts.agentId, this.durableSessionId(this.currentSessionId), seenRecordIds, this.pendingObservations);
2343
+ resumed = action ?? resumed;
2228
2344
  }
2345
+ // Each memory_query_result resumes the reasoning path and re-emits the pending loop
2346
+ // action; the caller must continue from the LAST one (it renders the injected hits).
2347
+ return resumed;
2229
2348
  }
2230
2349
  catch { /* errs-open — a faulty pre-fetch never breaks the run */ }
2350
+ return undefined;
2231
2351
  }
2232
2352
  async appendObservations(sessionId, runtime, nextArchiveStart, _taskScope) {
2233
2353
  const turn = runtime.turn();
@@ -2248,22 +2368,9 @@ export class RuntimeRunner {
2248
2368
  });
2249
2369
  this.activeGroupBudgetScope = undefined;
2250
2370
  }
2251
- // M3: mirror the kernel's journaled recall lifecycle into the durable store so recall
2252
- // history (count + last-recalled turn) survives across sessions.
2253
- if (obs.kind === "memory_recalled" && obs.recalls?.length) {
2254
- const agentId = this.opts.agentId;
2255
- if (agentId && this.opts.dreamStore?.recordRecall) {
2256
- await this.opts.dreamStore.recordRecall(agentId, obs.recalls);
2257
- }
2258
- }
2259
- // M4: a recall crossed the promotion threshold. Advisory — surface it for the host/model to
2260
- // act on (pin or promote to knowledge); the runner does not auto-pin.
2261
- if (obs.kind === "promotion_suggested" && obs.record_id) {
2262
- this.opts.onPromotionSuggested?.({
2263
- recordId: obs.record_id,
2264
- recallCount: obs.recall_count ?? 0,
2265
- });
2266
- }
2371
+ // M3/M4: mirror the kernel's journaled recall lifecycle into the durable store /
2372
+ // host callbacks (shared consumer identical semantics on every query route).
2373
+ await this.mirrorMemoryLifecycle(obs);
2267
2374
  const latest = obs.kind === "compressed" ? await this.opts.sessionLog.latestSeq(sessionId) : undefined;
2268
2375
  const event = kernelObservationToSessionEvent(obs, turn, {
2269
2376
  nextArchiveStart,
@@ -210,9 +210,17 @@ export interface WorkflowNodeOutcome {
210
210
  termination?: TerminationReason;
211
211
  output?: Message;
212
212
  }
213
+ /** A control-plane request rejected before any workflow effect started. */
214
+ export interface ControlRequestRejection {
215
+ operation: string;
216
+ subject?: string;
217
+ reason: string;
218
+ }
213
219
  export interface WorkflowOutcome {
214
220
  nodeOutcomes: WorkflowNodeOutcome[];
215
221
  outputs: Record<string, string>;
222
+ /** Present when the workflow itself was rejected before any node ran. */
223
+ rejection?: ControlRequestRejection;
216
224
  }
217
225
  export declare function workflowNodeOutcomeFromKernel(raw: KernelWorkflowNodeOutcome): WorkflowNodeOutcome;
218
226
  /** Per-node spawn descriptor carried in the `workflow_batch_spawned` observation. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/sdk",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
4
4
  "description": "DeepStrike Node.js SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@anthropic-ai/sdk": "^0.99.0",
75
- "@deepstrike/core": "0.2.40",
75
+ "@deepstrike/core": "0.2.42",
76
76
  "@google/generative-ai": "^0.24.1",
77
77
  "openai": "^5.23.2"
78
78
  },