@deepstrike/wasm 0.2.78 → 0.2.80

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/dist/index.d.ts CHANGED
@@ -58,3 +58,4 @@ export { memoryFetchRecall } from "./memory/recall-projection.js";
58
58
  export type { MemoryCandidate, MemoryCandidateReceipt, MemoryCandidateSink, MemoryCandidateSource } from "./memory/candidate-sink.js";
59
59
  export { resolveMemoryRuntimePolicy } from "./memory/runtime-policy.js";
60
60
  export type { MemoryRuntimePolicy, MemoryRetrievalPolicy, MemoryWritePolicy, ResolvedMemoryRuntimePolicy } from "./memory/runtime-policy.js";
61
+ export { DynamicWorkflowContextImpl, DynamicWorkflowLimitError, dynamicAgentTask, type DynamicWorkflowAgentOptions, type DynamicWorkflowAgentRequest, type DynamicWorkflowAgentResult, type DynamicWorkflowBlackboard, type DynamicWorkflowContext, type DynamicWorkflowLimits, type DynamicWorkflowProgress, type DynamicWorkflowRun, } from "./runtime/dynamic-workflow.js";
package/dist/index.js CHANGED
@@ -31,3 +31,4 @@ export { DurableContentError, decodeDurableContent, decodeDurableToolResult, enc
31
31
  export { InMemoryMemoryCandidateSink, memoryCandidateDuplicateReason, memoryCandidateId, memoryCandidatePayloadDigest } from "./memory/candidate-sink.js";
32
32
  export { memoryFetchRecall } from "./memory/recall-projection.js";
33
33
  export { resolveMemoryRuntimePolicy } from "./memory/runtime-policy.js";
34
+ export { DynamicWorkflowContextImpl, DynamicWorkflowLimitError, dynamicAgentTask, } from "./runtime/dynamic-workflow.js";
@@ -155,6 +155,12 @@ export declare class CanonicalRunnerRuntime {
155
155
  resumeAction(): KernelRunnerAction | null;
156
156
  startAgent(taskValue: Record<string, unknown>, runSpecValue?: Record<string, unknown>): Promise<KernelRunnerAction | null>;
157
157
  startWorkflow(specValue: Record<string, unknown>): Promise<KernelRunnerAction | null>;
158
+ /** Plan task 6.3 · open a dynamic workflow root: the host program appends nodes to it. */
159
+ startDynamicWorkflow(): Promise<KernelRunnerAction | null>;
160
+ /** Append host-program nodes to the active dynamic workflow root. */
161
+ appendWorkflowNodes(specValue: Record<string, unknown>, plan?: Record<string, unknown>): Promise<KernelRunnerAction | null>;
162
+ /** Close the dynamic workflow root so the kernel commits its terminal. */
163
+ completeDynamicWorkflow(): Promise<KernelRunnerAction | null>;
158
164
  private appendBase;
159
165
  applyHostEvent(event: Record<string, unknown>): Promise<KernelRunnerAction | null>;
160
166
  private ensureConfigured;
@@ -532,6 +532,7 @@ function canonicalProviderMessage(raw, appendBase = 0) {
532
532
  }
533
533
  : {}),
534
534
  ...(raw.token_count !== undefined ? { tokens: Number(raw.token_count) } : {}),
535
+ ...(raw.replay && typeof raw.replay === "object" ? { replay: raw.replay } : {}),
535
536
  };
536
537
  }
537
538
  function canonicalProviderToolArguments(name, argumentsValue, appendBase) {
@@ -913,6 +914,36 @@ export class CanonicalRunnerRuntime {
913
914
  this.workflowNodeCount = spec.nodes.length;
914
915
  return action;
915
916
  }
917
+ /** Plan task 6.3 · open a dynamic workflow root: the host program appends nodes to it. */
918
+ async startDynamicWorkflow() {
919
+ await this.ensureConfigured();
920
+ const action = await this.commit({
921
+ kind: "start_operation",
922
+ entry: { kind: "dynamic_workflow" },
923
+ initial_context: this.initialContext,
924
+ });
925
+ this.started = true;
926
+ this.workflowNodeCount = 0;
927
+ return action;
928
+ }
929
+ /** Append host-program nodes to the active dynamic workflow root. */
930
+ async appendWorkflowNodes(specValue, plan) {
931
+ await this.ensureConfigured();
932
+ if (!this.started)
933
+ throw new Error("cannot append dynamic workflow nodes before operation start");
934
+ return this.commit({
935
+ kind: "host_control",
936
+ command: {
937
+ kind: "append_workflow_nodes",
938
+ nodes: canonicalWorkflowSpec(specValue, true, this.appendBase()).nodes,
939
+ ...(plan ? { plan } : {}),
940
+ },
941
+ });
942
+ }
943
+ /** Close the dynamic workflow root so the kernel commits its terminal. */
944
+ async completeDynamicWorkflow() {
945
+ return this.commit({ kind: "host_control", command: { kind: "complete_dynamic_workflow" } });
946
+ }
916
947
  appendBase() {
917
948
  return this.workflowNodeCount ?? `wf-append-${crypto.randomUUID().slice(0, 8)}`;
918
949
  }
@@ -960,6 +991,13 @@ export class CanonicalRunnerRuntime {
960
991
  ...(providerStopReason(event.stop_reason)
961
992
  ? { stop_reason: providerStopReason(event.stop_reason) }
962
993
  : {}),
994
+ // Plan task 5.1 · what the provider did to the context server-side.
995
+ ...(event.provider_compaction && typeof event.provider_compaction === "object"
996
+ ? { provider_compaction: event.provider_compaction }
997
+ : {}),
998
+ ...(Array.isArray(event.cleared_tool_results) && event.cleared_tool_results.length
999
+ ? { cleared_tool_results: event.cleared_tool_results.map(String) }
1000
+ : {}),
963
1001
  },
964
1002
  },
965
1003
  },
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Plan task 6.3 · dynamic workflows: a host program decides, step by step, which agents to run.
3
+ *
4
+ * Each `agent` (or `parallelAgents` batch) becomes one submission the kernel admits onto the
5
+ * operation's dynamic workflow root — the kernel still owns the DAG, its budgets and every spawn —
6
+ * and the program awaits the typed result before deciding what comes next. The vocabulary matches
7
+ * the Node SDK's `runDynamicWorkflow`; replay of a saved run and the sandboxed script executors are
8
+ * Node-only (they need a filesystem and `node:vm`).
9
+ */
10
+ import type { KernelAgentRole, WorkflowNodeSpec, WorkflowOutcome, WorkflowSpec } from "./types/agent.js";
11
+ export interface DynamicWorkflowLimits {
12
+ maxAgentsPerRun?: number;
13
+ maxConcurrentAgents?: number;
14
+ maxBatchSize?: number;
15
+ }
16
+ export interface DynamicWorkflowAgentOptions {
17
+ label?: string;
18
+ role?: KernelAgentRole;
19
+ modelHint?: string;
20
+ trust?: WorkflowNodeSpec["trust"];
21
+ isolation?: WorkflowNodeSpec["isolation"];
22
+ outputSchema?: Record<string, unknown>;
23
+ tokenBudget?: number;
24
+ maxTurns?: number;
25
+ maxWallMs?: number;
26
+ }
27
+ export interface DynamicWorkflowAgentRequest {
28
+ prompt: string;
29
+ options?: DynamicWorkflowAgentOptions;
30
+ }
31
+ export interface DynamicWorkflowAgentResult<T = string> {
32
+ value: T;
33
+ text: string;
34
+ nodeId: string;
35
+ status: string;
36
+ termination?: string;
37
+ }
38
+ export interface DynamicWorkflowPhaseProgress {
39
+ name: string;
40
+ status: "running" | "completed" | "failed";
41
+ agentsStarted: number;
42
+ agentsCompleted: number;
43
+ }
44
+ export interface DynamicWorkflowProgress {
45
+ runId: string;
46
+ agentsStarted: number;
47
+ agentsCompleted: number;
48
+ activeAgents: number;
49
+ phase?: string;
50
+ phases: DynamicWorkflowPhaseProgress[];
51
+ logs: Array<{
52
+ message: string;
53
+ fields?: Record<string, unknown>;
54
+ }>;
55
+ }
56
+ export interface DynamicWorkflowRun<T> {
57
+ runId: string;
58
+ value: T;
59
+ progress: DynamicWorkflowProgress;
60
+ }
61
+ /** Plan task 6.2 · state the program shares with the agents it runs (see the Node SDK). */
62
+ export interface DynamicWorkflowBlackboard {
63
+ put(key: string, value: unknown, options?: {
64
+ scope?: "workflow" | "phase";
65
+ }): Promise<void>;
66
+ get(key: string): unknown;
67
+ delete(key: string): Promise<void>;
68
+ }
69
+ export interface DynamicWorkflowContext<TArgs extends Record<string, unknown> = Record<string, unknown>> {
70
+ readonly args: Readonly<TArgs>;
71
+ readonly progress: DynamicWorkflowProgress;
72
+ readonly blackboard: DynamicWorkflowBlackboard;
73
+ agent<T = string>(prompt: string, options?: DynamicWorkflowAgentOptions): Promise<DynamicWorkflowAgentResult<T> | null>;
74
+ parallelAgents<T>(items: readonly T[], worker: (item: T, index: number) => DynamicWorkflowAgentRequest): Promise<Array<DynamicWorkflowAgentResult | null>>;
75
+ parallel<T, R>(items: readonly T[], worker: (item: T, index: number) => Promise<R> | R): Promise<R[]>;
76
+ pipeline<T, R>(items: readonly T[], worker: (item: T, index: number) => Promise<R> | R): Promise<R[]>;
77
+ phase<T>(name: string, body: () => Promise<T> | T): Promise<T>;
78
+ log(message: string, fields?: Record<string, unknown>): void;
79
+ }
80
+ export declare class DynamicWorkflowLimitError extends Error {
81
+ readonly code: "dynamic_workflow_limit";
82
+ }
83
+ /** What the context needs from the runner: admit and drive one submission, write the blackboard. */
84
+ export interface DynamicWorkflowHost {
85
+ runWorkflow(spec: WorkflowSpec, plan: Record<string, unknown>): Promise<WorkflowOutcome>;
86
+ writeBlackboard(key: string, content: string | undefined): Promise<void>;
87
+ }
88
+ export declare function dynamicAgentTask(prompt: string, options?: DynamicWorkflowAgentOptions): DynamicWorkflowAgentRequest;
89
+ export declare class DynamicWorkflowContextImpl<TArgs extends Record<string, unknown>> implements DynamicWorkflowContext<TArgs> {
90
+ private readonly host;
91
+ readonly args: Readonly<TArgs>;
92
+ readonly progress: DynamicWorkflowProgress;
93
+ private readonly limits;
94
+ private readonly phaseStack;
95
+ private readonly board;
96
+ private nextNode;
97
+ private sequence;
98
+ private activeSlots;
99
+ private readonly waitingSlots;
100
+ readonly blackboard: DynamicWorkflowBlackboard;
101
+ constructor(host: DynamicWorkflowHost, runId: string, args: TArgs, limits?: DynamicWorkflowLimits);
102
+ agent<T = string>(prompt: string, options?: DynamicWorkflowAgentOptions): Promise<DynamicWorkflowAgentResult<T> | null>;
103
+ parallelAgents<T>(items: readonly T[], worker: (item: T, index: number) => DynamicWorkflowAgentRequest): Promise<Array<DynamicWorkflowAgentResult | null>>;
104
+ parallel<T, R>(items: readonly T[], worker: (item: T, index: number) => Promise<R> | R): Promise<R[]>;
105
+ pipeline<T, R>(items: readonly T[], worker: (item: T, index: number) => Promise<R> | R): Promise<R[]>;
106
+ phase<T>(name: string, body: () => Promise<T> | T): Promise<T>;
107
+ log(message: string, fields?: Record<string, unknown>): void;
108
+ private assertBatch;
109
+ private blackboardPut;
110
+ private blackboardDelete;
111
+ private acquireSlot;
112
+ private releaseSlot;
113
+ private runBatch;
114
+ }
@@ -0,0 +1,190 @@
1
+ import { sha256Hex } from "./sha256.js";
2
+ const DEFAULT_LIMITS = { maxAgentsPerRun: 64, maxConcurrentAgents: 8, maxBatchSize: 16 };
3
+ export class DynamicWorkflowLimitError extends Error {
4
+ code = "dynamic_workflow_limit";
5
+ }
6
+ export function dynamicAgentTask(prompt, options) {
7
+ if (!prompt.trim())
8
+ throw new Error("dynamic workflow agent prompt must not be empty");
9
+ return { prompt, ...(options ? { options } : {}) };
10
+ }
11
+ export class DynamicWorkflowContextImpl {
12
+ host;
13
+ args;
14
+ progress;
15
+ limits;
16
+ phaseStack = [];
17
+ board = new Map();
18
+ nextNode = 0;
19
+ sequence = 0;
20
+ activeSlots = 0;
21
+ waitingSlots = [];
22
+ blackboard = Object.freeze({
23
+ put: (key, value, options) => this.blackboardPut(key, value, options),
24
+ get: (key) => this.board.get(key)?.value,
25
+ delete: (key) => this.blackboardDelete(key),
26
+ });
27
+ constructor(host, runId, args, limits) {
28
+ this.host = host;
29
+ this.args = Object.freeze({ ...args });
30
+ this.limits = { ...DEFAULT_LIMITS, ...limits };
31
+ this.progress = { runId, agentsStarted: 0, agentsCompleted: 0, activeAgents: 0, phases: [], logs: [] };
32
+ }
33
+ async agent(prompt, options) {
34
+ return (await this.runBatch([dynamicAgentTask(prompt, options)]))[0];
35
+ }
36
+ async parallelAgents(items, worker) {
37
+ this.assertBatch(items.length, "parallelAgents");
38
+ return this.runBatch(items.map((item, index) => worker(item, index)));
39
+ }
40
+ async parallel(items, worker) {
41
+ this.assertBatch(items.length, "parallel");
42
+ return Promise.all(items.map((item, index) => worker(item, index)));
43
+ }
44
+ async pipeline(items, worker) {
45
+ const out = [];
46
+ for (let index = 0; index < items.length; index++)
47
+ out.push(await worker(items[index], index));
48
+ return out;
49
+ }
50
+ async phase(name, body) {
51
+ if (!name.trim())
52
+ throw new Error("dynamic workflow phase name must not be empty");
53
+ const phase = { name, status: "running", agentsStarted: 0, agentsCompleted: 0 };
54
+ this.progress.phases.push(phase);
55
+ this.phaseStack.push(phase);
56
+ this.progress.phase = name;
57
+ try {
58
+ const result = await body();
59
+ phase.status = "completed";
60
+ return result;
61
+ }
62
+ catch (error) {
63
+ phase.status = "failed";
64
+ throw error;
65
+ }
66
+ finally {
67
+ this.phaseStack.pop();
68
+ this.progress.phase = this.phaseStack.at(-1)?.name;
69
+ for (const [key, entry] of [...this.board]) {
70
+ if (entry.phase === phase)
71
+ await this.blackboardDelete(key);
72
+ }
73
+ }
74
+ }
75
+ log(message, fields) {
76
+ this.progress.logs.push({ message, ...(fields ? { fields } : {}) });
77
+ }
78
+ assertBatch(size, operation) {
79
+ if (size > this.limits.maxBatchSize) {
80
+ throw new DynamicWorkflowLimitError(`${operation} exceeded maxBatchSize (${this.limits.maxBatchSize})`);
81
+ }
82
+ }
83
+ async blackboardPut(key, value, options = {}) {
84
+ if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(key)) {
85
+ throw new Error("dynamic workflow blackboard keys are 1-128 characters of letters, digits, _ . : -");
86
+ }
87
+ const scope = options.scope ?? "workflow";
88
+ if (scope !== "workflow" && scope !== "phase")
89
+ throw new Error(`unknown blackboard scope ${String(scope)}`);
90
+ const phase = scope === "phase" ? this.phaseStack.at(-1) : undefined;
91
+ if (scope === "phase" && !phase)
92
+ throw new Error("a phase-scoped blackboard entry needs an enclosing phase");
93
+ const content = typeof value === "string" ? value : JSON.stringify(value);
94
+ if (content === undefined)
95
+ throw new Error("dynamic workflow blackboard values must be serializable");
96
+ this.board.set(key, { value: typeof value === "string" ? value : JSON.parse(content), ...(phase ? { phase } : {}) });
97
+ await this.host.writeBlackboard(key, content);
98
+ }
99
+ async blackboardDelete(key) {
100
+ if (!this.board.delete(key))
101
+ return;
102
+ await this.host.writeBlackboard(key, undefined);
103
+ }
104
+ async acquireSlot() {
105
+ if (this.activeSlots < this.limits.maxConcurrentAgents) {
106
+ this.activeSlots += 1;
107
+ return;
108
+ }
109
+ await new Promise(resolve => this.waitingSlots.push(resolve));
110
+ }
111
+ releaseSlot() {
112
+ const next = this.waitingSlots.shift();
113
+ if (next)
114
+ next();
115
+ else
116
+ this.activeSlots -= 1;
117
+ }
118
+ async runBatch(requests) {
119
+ if (requests.length === 0)
120
+ return [];
121
+ if (this.progress.agentsStarted + requests.length > this.limits.maxAgentsPerRun) {
122
+ throw new DynamicWorkflowLimitError(`dynamic workflow exceeded maxAgentsPerRun (${this.limits.maxAgentsPerRun})`);
123
+ }
124
+ const sequence = this.sequence++;
125
+ const entries = requests.map(request => {
126
+ const options = request.options ?? {};
127
+ const nodeId = options.label?.trim() || `dynamic-agent-${this.nextNode++}`;
128
+ const fingerprint = `sha256:${sha256Hex(JSON.stringify({ prompt: request.prompt, options }))}`;
129
+ const node = {
130
+ nodeId,
131
+ task: request.prompt,
132
+ role: options.role ?? "implement",
133
+ ...(options.modelHint ? { modelHint: options.modelHint } : {}),
134
+ ...(options.trust ? { trust: options.trust } : {}),
135
+ ...(options.isolation ? { isolation: options.isolation } : {}),
136
+ ...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
137
+ ...(options.tokenBudget !== undefined ? { tokenBudget: options.tokenBudget } : {}),
138
+ ...(options.maxTurns !== undefined ? { maxTurns: options.maxTurns } : {}),
139
+ ...(options.maxWallMs !== undefined ? { maxWallMs: options.maxWallMs } : {}),
140
+ };
141
+ return { nodeId, request, fingerprint, node };
142
+ });
143
+ const phase = this.phaseStack.at(-1);
144
+ this.progress.agentsStarted += entries.length;
145
+ this.progress.activeAgents += entries.length;
146
+ if (phase)
147
+ phase.agentsStarted += entries.length;
148
+ await this.acquireSlot();
149
+ let outcome;
150
+ try {
151
+ outcome = await this.host.runWorkflow({ nodes: entries.map(entry => entry.node) }, {
152
+ run_id: this.progress.runId,
153
+ sequence,
154
+ nodes: entries.map(entry => ({ node_id: entry.nodeId, depends_on: [], prompt_fingerprint: entry.fingerprint, replay: "executed" })),
155
+ });
156
+ }
157
+ finally {
158
+ this.releaseSlot();
159
+ this.progress.activeAgents -= entries.length;
160
+ }
161
+ if (outcome.rejection)
162
+ throw new Error(`dynamic workflow submission rejected: ${outcome.rejection.reason}`);
163
+ const byNode = new Map(outcome.nodeOutcomes.map(node => [node.nodeId, node]));
164
+ return entries.map(entry => {
165
+ this.progress.agentsCompleted += 1;
166
+ if (phase)
167
+ phase.agentsCompleted += 1;
168
+ const node = byNode.get(entry.nodeId);
169
+ const text = outcome.outputs[entry.nodeId];
170
+ if (text === undefined && (!node || node.status !== "completed"))
171
+ return null;
172
+ let value = text ?? "";
173
+ if (entry.request.options?.outputSchema) {
174
+ try {
175
+ value = JSON.parse(text ?? "");
176
+ }
177
+ catch {
178
+ value = null;
179
+ }
180
+ }
181
+ return {
182
+ value,
183
+ text: text ?? "",
184
+ nodeId: entry.nodeId,
185
+ status: node?.status ?? "completed",
186
+ ...(node?.termination ? { termination: node.termination } : {}),
187
+ };
188
+ });
189
+ }
190
+ }
@@ -82,6 +82,8 @@ function projectKernelObservation(obs, turn, opts = {}) {
82
82
  recall_expanded: obs.recall_expanded ?? 0,
83
83
  ...(obs.items && obs.items.length > 0 ? { items: obs.items } : {}),
84
84
  ...(obs.history && obs.history.length > 0 ? { history: obs.history } : {}),
85
+ ...(obs.history_items && obs.history_items.length > 0 ? { history_items: obs.history_items } : {}),
86
+ ...(obs.history_items_omitted ? { history_items_omitted: obs.history_items_omitted } : {}),
85
87
  };
86
88
  case "rollbacked":
87
89
  return {
@@ -1,5 +1,5 @@
1
1
  import type { ModelMessage, RenderedContext, ToolCall, ToolExecutionResult, ToolSchema } from "../types.js";
2
- import type { ContextItemRecord, ContextOriginTally, RollbackReason } from "./session-log.js";
2
+ import type { ContextHistoryItemRecord, ContextItemRecord, ContextOriginTally, RollbackReason } from "./session-log.js";
3
3
  interface TaskUpdate {
4
4
  plan?: string[];
5
5
  currentStep?: number;
@@ -137,6 +137,8 @@ export interface KernelObservation {
137
137
  recall_expanded?: number;
138
138
  items?: ContextItemRecord[];
139
139
  history?: ContextOriginTally[];
140
+ history_items?: ContextHistoryItemRecord[];
141
+ history_items_omitted?: number;
140
142
  archived_count?: number;
141
143
  turn?: number;
142
144
  checkpoint_history_len?: number;
@@ -220,6 +220,10 @@ export function kernelMessageToSdk(raw) {
220
220
  isError: raw.is_error === true,
221
221
  }];
222
222
  }
223
+ if (typeof raw.item_id === "number")
224
+ message.itemId = raw.item_id;
225
+ if (raw.replay && typeof raw.replay === "object")
226
+ message.providerReplay = raw.replay;
223
227
  return message;
224
228
  }
225
229
  export function renderedContextToSdk(raw) {
@@ -235,5 +239,8 @@ export function renderedContextToSdk(raw) {
235
239
  ctx.stateTurn = kernelMessageToSdk(rawStateTurn);
236
240
  if (typeof frozenLen === "number")
237
241
  ctx.frozenPrefixLen = frozenLen;
242
+ const estimated = raw.estimated_tokens ?? raw.estimatedTokens;
243
+ if (typeof estimated === "number" && estimated > 0)
244
+ ctx.estimatedTokens = estimated;
238
245
  return ctx;
239
246
  }
@@ -9,7 +9,8 @@ import type { SessionLog, SessionEvent } from "./session-log.js";
9
9
  import type { ExecutionPlane } from "./execution-plane.js";
10
10
  import { type GovernancePolicy, type LivePolicyPatch } from "../governance.js";
11
11
  import type { KernelJournal } from "./kernel-journal.js";
12
- import type { AgentRunSpec, MilestonePolicy, MilestoneContract, MilestoneCheckResult, WorkflowSpec, WorkflowOutcome } from "./types/agent.js";
12
+ import { type DynamicWorkflowContext, type DynamicWorkflowLimits, type DynamicWorkflowRun } from "./dynamic-workflow.js";
13
+ import type { AgentRunSpec, MilestonePolicy, MilestoneContract, MilestoneCheckResult, WorkflowSpec, InheritedKnowledge, WorkflowOutcome } from "./types/agent.js";
13
14
  import { type SubAgentOrchestrator } from "./sub-agent-orchestrator.js";
14
15
  import { type NativeOsProfile, type OsProfileId, type SignalPolicy } from "./os-profile.js";
15
16
  import { PayloadStore } from "./payload-store.js";
@@ -141,6 +142,9 @@ export interface RuntimeOptions {
141
142
  /** Plan task 3.1 · outputs of the workflow nodes this run depends on, by agent id. The kernel
142
143
  * holds them; the model reads one with `read_result(call_id="dep:<agent_id>")`. */
143
144
  dependencyOutputs?: Record<string, string>;
145
+ /** Plan task 1.1 · the parent knowledge a child run starts with, chosen by the kernel from the
146
+ * items' scope and the node's context inheritance. Set by the workflow driver. */
147
+ inheritedKnowledge?: InheritedKnowledge[];
144
148
  /** Skill name → markdown body (WASM has no filesystem). */
145
149
  skillContentMap?: Map<string, string>;
146
150
  memoryRuntimePolicy?: MemoryRuntimePolicy;
@@ -456,6 +460,18 @@ export declare class RuntimeRunner {
456
460
  /** Standalone session id when bootstrapping (no active parent run). Defaults to a fresh uuid. */
457
461
  sessionId?: string;
458
462
  }): Promise<WorkflowOutcome>;
463
+ /**
464
+ * Plan task 6.3 · run a host program that decides which agents to run as it goes. Each
465
+ * `agent` / `parallelAgents` call is admitted by the kernel onto this operation's dynamic
466
+ * workflow root and driven to its typed outcome before the call returns; submissions and
467
+ * blackboard writes reach the kernel one at a time, in program order. Requires a standalone runner.
468
+ */
469
+ runDynamicWorkflow<TArgs extends Record<string, unknown>, T>(program: (context: DynamicWorkflowContext<TArgs>) => Promise<T> | T, opts?: {
470
+ sessionId?: string;
471
+ runId?: string;
472
+ args?: TArgs;
473
+ limits?: DynamicWorkflowLimits;
474
+ }): Promise<DynamicWorkflowRun<T>>;
459
475
  /**
460
476
  * #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
461
477
  * routes through the kernel (root in `SubAgentAwait` → preempt → `AgentPreempted` + tears the
@@ -11,6 +11,7 @@ import { memoryFetchRecall } from "../memory/recall-projection.js";
11
11
  import { MemoryWriter } from "../memory/writer.js";
12
12
  import { CanonicalRunnerRuntime, canonicalKernelAction, canonicalKernelApply, isInvalidInputError, canonicalKernelMaybeAction, canonicalStartAgent, canonicalStartWorkflow, sha256 as canonicalSha256, } from "./canonical-kernel-step.js";
13
13
  import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-replay.js";
14
+ import { DynamicWorkflowContextImpl, } from "./dynamic-workflow.js";
14
15
  import { sanitizeReplayText } from "./replay-sanitize.js";
15
16
  import { decodeDurableContent, decodeDurableToolResult, durableBlocksToToolOutput, toolOutputBlocksToDurable } from "./durable-content.js";
16
17
  import { formatToolError } from "../tools/errors.js";
@@ -758,6 +759,15 @@ export class RuntimeRunner {
758
759
  content,
759
760
  });
760
761
  }
762
+ for (const entry of this.opts.inheritedKnowledge ?? []) {
763
+ await this.commitKernelApply(runtime, this.pendingObservations, {
764
+ kind: "add_knowledge_message",
765
+ content: entry.content,
766
+ ...(entry.key ? { key: entry.key } : {}),
767
+ tokens: entry.tokens ?? Math.max(1, Math.ceil(entry.content.length / 4)),
768
+ ...(entry.pinned ? { pinned: true } : {}),
769
+ });
770
+ }
761
771
  if (this.opts.initialMemory) {
762
772
  for (const mem of this.opts.initialMemory) {
763
773
  await this.commitKernelApply(runtime, this.pendingObservations, {
@@ -948,6 +958,12 @@ export class RuntimeRunner {
948
958
  // I5: the kernel itself withholds statically denied tools (`hide_denied_tools`) and renders
949
959
  // the denial note — the host sends exactly the context and surface the kernel committed.
950
960
  const context = action.context;
961
+ // Plan task 1.1c · the kernel keeps provider replay with each turn's item; hand it to the
962
+ // provider keyed by the turn exactly as rendered, so its own lookup finds it.
963
+ for (const turn of context.turns) {
964
+ if (turn.providerReplay)
965
+ this.opts.provider.seedProviderReplay?.(turn, turn.providerReplay);
966
+ }
951
967
  const tools = action.tools;
952
968
  let turnTokens = 0;
953
969
  let turnInputTokens = 0;
@@ -1152,6 +1168,21 @@ export class RuntimeRunner {
1152
1168
  // P4-S1: land the attempt evidence BEFORE the kernel resolution commits — the record
1153
1169
  // describes the transport fact, which exists regardless of what the kernel decides next.
1154
1170
  const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
1171
+ // Plan task 1.1c · the kernel keeps the replay with the turn's item from here on.
1172
+ if (providerReplay !== undefined) {
1173
+ providerEvent.message = { ...providerEvent.message, replay: providerReplay };
1174
+ }
1175
+ // Plan task 5.1 · server-side context edits become facts the kernel absorbs.
1176
+ const contextEdits = this.opts.provider.takeContextEdits?.();
1177
+ if (contextEdits?.compaction) {
1178
+ providerEvent.provider_compaction = {
1179
+ through_item: contextEdits.compaction.throughItem,
1180
+ replay: contextEdits.compaction.replay,
1181
+ summary_tokens: contextEdits.compaction.summaryTokens,
1182
+ };
1183
+ }
1184
+ if (contextEdits?.clearedToolResults?.length)
1185
+ providerEvent.cleared_tool_results = contextEdits.clearedToolResults;
1155
1186
  const wireEvidence = {
1156
1187
  protocol: this.providerRoute.protocol,
1157
1188
  request_fingerprint: providerPlan.fingerprint,
@@ -1674,6 +1705,7 @@ export class RuntimeRunner {
1674
1705
  ...(node.dependency_outputs
1675
1706
  ? { dependencyOutputs: node.dependency_outputs }
1676
1707
  : {}),
1708
+ ...(node.inherited_knowledge?.length ? { inheritedKnowledge: node.inherited_knowledge } : {}),
1677
1709
  manifest,
1678
1710
  sessionLog: this.opts.sessionLog,
1679
1711
  // This child is a workflow node, so capability resolution applies workflow-node quarantine
@@ -1878,6 +1910,86 @@ export class RuntimeRunner {
1878
1910
  }
1879
1911
  }
1880
1912
  }
1913
+ /**
1914
+ * Plan task 6.3 · run a host program that decides which agents to run as it goes. Each
1915
+ * `agent` / `parallelAgents` call is admitted by the kernel onto this operation's dynamic
1916
+ * workflow root and driven to its typed outcome before the call returns; submissions and
1917
+ * blackboard writes reach the kernel one at a time, in program order. Requires a standalone runner.
1918
+ */
1919
+ async runDynamicWorkflow(program, opts = {}) {
1920
+ if (this.activeKernel || this.currentSessionId) {
1921
+ throw new Error("dynamic workflows require a standalone RuntimeRunner");
1922
+ }
1923
+ const sessionId = opts.sessionId ?? `dw-${crypto.randomUUID()}`;
1924
+ const runId = opts.runId ?? crypto.randomUUID();
1925
+ try {
1926
+ await this.opts.sessionLog.append(sessionId, {
1927
+ kind: "run_started",
1928
+ run_id: runId,
1929
+ goal: "dynamic-workflow",
1930
+ criteria: [],
1931
+ ...(this.opts.agentId ? { agent_id: this.opts.agentId } : {}),
1932
+ route: this.providerRoute,
1933
+ });
1934
+ const runtime = await this.initializeWorkflowKernel(sessionId, runId);
1935
+ await runtime.startDynamicWorkflow();
1936
+ runtime.drainHostObservations();
1937
+ let tail = Promise.resolve();
1938
+ const exclusive = (work) => {
1939
+ const next = tail.then(work);
1940
+ tail = next.catch(() => undefined);
1941
+ return next;
1942
+ };
1943
+ const host = {
1944
+ runWorkflow: (spec, plan) => exclusive(async () => {
1945
+ const start = this.pendingObservations.length;
1946
+ const action = await runtime.appendWorkflowNodes(workflowSpecToKernel(spec), plan);
1947
+ this.pendingObservations.push(...runtime.drainHostObservations());
1948
+ const observations = this.pendingObservations.slice(start);
1949
+ const rejected = observations.find(o => o.kind === "control_request_rejected");
1950
+ if (!action || rejected) {
1951
+ return { nodeOutcomes: [], outputs: {}, rejection: { operation: "submit_workflow_nodes", reason: rejected?.reason ?? "the kernel admitted no nodes" } };
1952
+ }
1953
+ const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
1954
+ const base = Number.isInteger(submitted?.base) ? submitted.base : 0;
1955
+ const outcome = await this.driveWorkflow(action, observations, sessionId, runtime, new Map());
1956
+ // The kernel names appended nodes by position (wf-node<base+i>); the program by its own ids.
1957
+ const toHost = new Map(spec.nodes.map((node, index) => [`wf-node${base + index}`, node.nodeId ?? `wf-node${base + index}`]));
1958
+ return {
1959
+ ...outcome,
1960
+ nodeOutcomes: outcome.nodeOutcomes.map(node => ({ ...node, nodeId: toHost.get(node.nodeId) ?? node.nodeId })),
1961
+ outputs: Object.fromEntries(Object.entries(outcome.outputs).map(([id, output]) => [toHost.get(id) ?? id, output])),
1962
+ };
1963
+ }),
1964
+ // Plan task 6.2 · a keyed kernel item on this operation; agents spawned later inherit it.
1965
+ writeBlackboard: (key, content) => exclusive(async () => {
1966
+ await this.commitKernelApply(runtime, this.pendingObservations, content === undefined
1967
+ ? { kind: "remove_knowledge", key: `blackboard:${key}` }
1968
+ : { kind: "add_knowledge_message", key: `blackboard:${key}`, content, tokens: Math.max(1, Math.ceil(content.length / 4)) });
1969
+ }),
1970
+ };
1971
+ const context = new DynamicWorkflowContextImpl(host, runId, (opts.args ?? {}), opts.limits);
1972
+ const value = await program(context);
1973
+ const closing = await exclusive(async () => {
1974
+ const action = await runtime.completeDynamicWorkflow();
1975
+ this.pendingObservations.push(...runtime.drainHostObservations());
1976
+ return action;
1977
+ });
1978
+ const terminal = closing ?? runtime.resumeAction();
1979
+ if (!terminal || terminal.kind !== "done") {
1980
+ throw new Error("dynamic workflow close did not produce a terminal kernel action");
1981
+ }
1982
+ await this.appendObservations(sessionId, runtime, 0);
1983
+ return { runId, value, progress: context.progress };
1984
+ }
1985
+ finally {
1986
+ this.rejectPendingControl("the run ended before the control command was applied");
1987
+ this.activeKernel = null;
1988
+ this.currentSessionId = null;
1989
+ this.abortController = null;
1990
+ this.pendingObservations = [];
1991
+ }
1992
+ }
1881
1993
  /**
1882
1994
  * #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
1883
1995
  * routes through the kernel (root in `SubAgentAwait` → preempt → `AgentPreempted` + tears the
@@ -77,7 +77,7 @@ export declare const SESSION_EVENT_REGISTRY: {
77
77
  };
78
78
  readonly context_lifecycle_reported: {
79
79
  readonly required: readonly ["cache_loss_tokens", "knowledge_entries", "knowledge_referenced", "recall_expanded", "recalled", "regret", "turn", "waste_tokens"];
80
- readonly optional: readonly ["history", "items"];
80
+ readonly optional: readonly ["history", "history_items", "history_items_omitted", "items"];
81
81
  };
82
82
  readonly suspended: {
83
83
  readonly required: readonly ["reason", "turn"];
@@ -21,7 +21,7 @@ export const SESSION_EVENT_REGISTRY = {
21
21
  rollbacked: { required: ["checkpoint_history_len", "turn"], optional: ["reason"] },
22
22
  capability_changed: { required: ["added", "removed", "turn"], optional: ["capability_id", "change_kind", "mount_reason", "mounted_by", "version"] },
23
23
  context_renewed: { required: ["handoff_ref", "sprint", "turn"], optional: [] },
24
- context_lifecycle_reported: { required: ["cache_loss_tokens", "knowledge_entries", "knowledge_referenced", "recall_expanded", "recalled", "regret", "turn", "waste_tokens"], optional: ["history", "items"] },
24
+ context_lifecycle_reported: { required: ["cache_loss_tokens", "knowledge_entries", "knowledge_referenced", "recall_expanded", "recalled", "regret", "turn", "waste_tokens"], optional: ["history", "history_items", "history_items_omitted", "items"] },
25
25
  suspended: { required: ["reason", "turn"], optional: ["pending_calls"] },
26
26
  resumed: { required: ["turn"], optional: ["approved", "denied"] },
27
27
  tool_gated: { required: ["call_id", "reason", "tool", "turn"], optional: [] },
@@ -54,7 +54,7 @@ export interface ContextOriginTally {
54
54
  /** One knowledge item's lifecycle in a `context_lifecycle_reported` event (plan task 4.1). */
55
55
  export interface ContextItemRecord {
56
56
  key?: string;
57
- origin: "system" | "skill" | "memory_recall" | "reference" | "tool_result" | "dependency_output" | "user" | "assistant" | "task_state" | "signal" | "provider_summary";
57
+ origin: "system" | "skill" | "memory_recall" | "reference" | "blackboard" | "tool_result" | "dependency_output" | "user" | "assistant" | "task_state" | "signal" | "provider_summary";
58
58
  tokens: number;
59
59
  value: {
60
60
  injected_turn: number;
@@ -65,7 +65,18 @@ export interface ContextItemRecord {
65
65
  paged_in_after_evict?: boolean;
66
66
  };
67
67
  /** Why it left; absent while resident. */
68
- end?: "evicted" | "expired" | "removed";
68
+ end?: "evicted" | "expired" | "removed" | "compacted" | "renewed";
69
+ }
70
+ /** One history item's lifecycle in a `context_lifecycle_reported` event (plan task 4.1). */
71
+ export interface ContextHistoryItemRecord {
72
+ id: number;
73
+ origin: ContextItemRecord["origin"];
74
+ tokens: number;
75
+ injected_turn: number;
76
+ /** What it rendered when it left, or renders now; absent = full. */
77
+ fidelity?: "structured" | "excerpt" | "handle";
78
+ /** Why it left; absent while still in history. */
79
+ end?: "removed" | "compacted" | "renewed";
69
80
  }
70
81
  export type SessionEvent = {
71
82
  kind: "run_started";
@@ -202,6 +213,9 @@ export type SessionEvent = {
202
213
  items?: ContextItemRecord[];
203
214
  /** History items by origin: admitted, still in history at the end, read back with `read_result`. */
204
215
  history?: ContextOriginTally[];
216
+ /** The latest history items one by one (at most 128); older ones are only counted. */
217
+ history_items?: ContextHistoryItemRecord[];
218
+ history_items_omitted?: number;
205
219
  } | {
206
220
  kind: "suspended";
207
221
  turn: number;
@@ -1,4 +1,4 @@
1
- import type { AgentRunSpec, AgentProcessChangedObservation, SubAgentResult } from "./types/agent.js";
1
+ import type { AgentRunSpec, AgentProcessChangedObservation, SubAgentResult, InheritedKnowledge } from "./types/agent.js";
2
2
  import type { RuntimeOptions } from "./runner.js";
3
3
  import type { SessionLog } from "./session-log.js";
4
4
  export interface SubAgentRunContext {
@@ -22,6 +22,8 @@ export interface SubAgentRunContext {
22
22
  abortSignal?: AbortSignal;
23
23
  /** Plan task 3.1 · the outputs of the workflow nodes this child depends on, by agent id. */
24
24
  dependencyOutputs?: Record<string, string>;
25
+ /** Plan task 1.1 · the parent knowledge the kernel lets this child inherit. */
26
+ inheritedKnowledge?: InheritedKnowledge[];
25
27
  }
26
28
  /** M1/G3 intelligence routing: resolve the provider for a sub-agent from its spec's `modelHint`.
27
29
  * Falls back to the parent provider when there is no hint or no `providerFor` hook resolves it. */
@@ -128,6 +128,8 @@ export class SubAgentOrchestrator {
128
128
  : undefined,
129
129
  // A child reads only its own dependencies, never ones inherited from the parent's options.
130
130
  dependencyOutputs: ctx.dependencyOutputs,
131
+ // Likewise only the knowledge the kernel chose for this child, never the parent's own.
132
+ inheritedKnowledge: ctx.inheritedKnowledge,
131
133
  });
132
134
  // #2-B-ii: parent preempt → interrupt the child (cancels its in-flight LLM call).
133
135
  linkAbort(ctx.abortSignal, childRunner);
@@ -208,6 +208,13 @@ export interface WorkflowOutcome {
208
208
  rejection?: ControlRequestRejection;
209
209
  }
210
210
  export declare function workflowNodeOutcomeFromKernel(raw: KernelWorkflowNodeOutcome): WorkflowNodeOutcome;
211
+ /** Plan task 1.1 · one parent knowledge item a child run starts with (kernel-chosen by scope). */
212
+ export interface InheritedKnowledge {
213
+ content: string;
214
+ key?: string;
215
+ tokens?: number;
216
+ pinned?: boolean;
217
+ }
211
218
  /** Per-node spawn descriptor carried in the `workflow_batch_spawned` observation. */
212
219
  export interface WorkflowSpawnInfo {
213
220
  agent_id: string;
@@ -227,6 +234,8 @@ export interface WorkflowSpawnInfo {
227
234
  input_agent_ids?: string[];
228
235
  /** The kernel-composed dependency section that follows the node's goal. */
229
236
  dependency_brief?: string;
237
+ /** Plan task 1.1 · the parent knowledge the kernel lets this node inherit (by item scope). */
238
+ inherited_knowledge?: InheritedKnowledge[];
230
239
  /** M4/G5: the node's per-node cumulative token cap, if set — the runner caps the child run here. */
231
240
  token_budget?: number;
232
241
  /** O3: per-node turn cap → the child run's `maxTurns`. */
package/dist/types.d.ts CHANGED
@@ -46,6 +46,12 @@ export interface ModelMessage {
46
46
  /** Multimodal parts (text + image/audio). When present, providers render these
47
47
  * instead of the plain `content` string. */
48
48
  contentParts?: ContentPart[];
49
+ /** The kernel item this rendered turn is (plan task 1.1c): stable across compaction, renewal
50
+ * and restore. Absent on messages the kernel did not render. */
51
+ itemId?: number;
52
+ /** Provider state the kernel keeps with this turn's item (reasoning, native blocks) and renders
53
+ * back with it; the runner seeds it into the provider before a call (plan task 1.1c). */
54
+ providerReplay?: ProviderReplay;
49
55
  }
50
56
  export interface ToolCall {
51
57
  id: string;
@@ -79,6 +85,9 @@ export interface RenderedContext {
79
85
  * Anthropic pins a deep cache breakpoint here instead of the rolling pair. */
80
86
  frozenPrefixLen?: number;
81
87
  budgetOverflow?: ContextBudgetOverflow;
88
+ /** The kernel's estimate of this request's input tokens, tool schemas included, calibrated
89
+ * against what the provider counted for earlier requests (plan task 5.1). */
90
+ estimatedTokens?: number;
82
91
  }
83
92
  export interface ContextBudgetOverflow {
84
93
  kind: "fixed_context" | "protected_tail";
@@ -269,6 +278,15 @@ export interface PromptMeasurement {
269
278
  source: MeasurementSource;
270
279
  confidence: MeasurementConfidence;
271
280
  }
281
+ /** Server-side context edits a provider reports for one call (plan task 5.1). */
282
+ export interface ProviderContextEdits {
283
+ compaction?: {
284
+ throughItem: number;
285
+ replay: ProviderReplay;
286
+ summaryTokens: number;
287
+ };
288
+ clearedToolResults?: string[];
289
+ }
272
290
  export interface ProviderReplay {
273
291
  provider?: string;
274
292
  protocol: GenerationProtocol;
@@ -320,6 +338,10 @@ export interface LLMProvider {
320
338
  };
321
339
  };
322
340
  peekProviderReplay?(message: Pick<ModelMessage, "content" | "toolCalls">): ProviderReplay | undefined;
341
+ /** Plan task 5.1 · what the last call did to the context server-side, for the kernel to absorb:
342
+ * a summary of history through `throughItem` (the `itemId` of the last rendered turn it covers)
343
+ * and the call ids of tool results it cleared. Read once per completed call. */
344
+ takeContextEdits?(): ProviderContextEdits | undefined;
323
345
  seedProviderReplay?(message: Pick<ModelMessage, "content" | "toolCalls">, replay: ProviderReplay): void;
324
346
  /** P4-S1 mirror: transport facts of the most recent execution (host evidence only, B7). */
325
347
  peekTransportTelemetry?(): ProviderTransportTelemetry | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/wasm",
3
- "version": "0.2.78",
3
+ "version": "0.2.80",
4
4
  "description": "DeepStrike WASM SDK — browser, Cloudflare Workers, Deno Deploy",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -14,10 +14,10 @@
14
14
  "build:wasm": "wasm-pack build ../crates/deepstrike-wasm --target bundler --out-dir ../../wasm/pkg",
15
15
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
16
16
  "test": "node --experimental-vm-modules node_modules/.bin/jest",
17
- "test:canonical-binding": "wasm-pack build ../crates/deepstrike-wasm --target nodejs --dev --out-dir ../../wasm/pkg-node && node tests/canonical-binding.node.cjs && node tests/golden-fixtures-sweep.node.cjs && npm run build && node tests/boundary-p0-regressions.node.mjs && node tests/boundary-p1-regressions.node.mjs && node tests/boundary-p2-regressions.node.mjs && node tests/memory-candidate-binding.node.mjs && node tests/memory-recall-renewal.node.mjs"
17
+ "test:canonical-binding": "wasm-pack build ../crates/deepstrike-wasm --target nodejs --dev --out-dir ../../wasm/pkg-node && node tests/canonical-binding.node.cjs && node tests/golden-fixtures-sweep.node.cjs && npm run build && node tests/boundary-p0-regressions.node.mjs && node tests/boundary-p1-regressions.node.mjs && node tests/boundary-p2-regressions.node.mjs && node tests/memory-candidate-binding.node.mjs && node tests/memory-recall-renewal.node.mjs && node tests/dynamic-workflow.node.mjs"
18
18
  },
19
19
  "dependencies": {
20
- "@deepstrike/wasm-kernel": "0.2.78"
20
+ "@deepstrike/wasm-kernel": "0.2.80"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/jest": "^30.0.0",