@deepstrike/wasm 0.2.79 → 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
+ }
@@ -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,6 +9,7 @@ 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 DynamicWorkflowContext, type DynamicWorkflowLimits, type DynamicWorkflowRun } from "./dynamic-workflow.js";
12
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";
@@ -459,6 +460,18 @@ export declare class RuntimeRunner {
459
460
  /** Standalone session id when bootstrapping (no active parent run). Defaults to a fresh uuid. */
460
461
  sessionId?: string;
461
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>>;
462
475
  /**
463
476
  * #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
464
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";
@@ -957,6 +958,12 @@ export class RuntimeRunner {
957
958
  // I5: the kernel itself withholds statically denied tools (`hide_denied_tools`) and renders
958
959
  // the denial note — the host sends exactly the context and surface the kernel committed.
959
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
+ }
960
967
  const tools = action.tools;
961
968
  let turnTokens = 0;
962
969
  let turnInputTokens = 0;
@@ -1161,6 +1168,21 @@ export class RuntimeRunner {
1161
1168
  // P4-S1: land the attempt evidence BEFORE the kernel resolution commits — the record
1162
1169
  // describes the transport fact, which exists regardless of what the kernel decides next.
1163
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;
1164
1186
  const wireEvidence = {
1165
1187
  protocol: this.providerRoute.protocol,
1166
1188
  request_fingerprint: providerPlan.fingerprint,
@@ -1888,6 +1910,86 @@ export class RuntimeRunner {
1888
1910
  }
1889
1911
  }
1890
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
+ }
1891
1993
  /**
1892
1994
  * #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
1893
1995
  * routes through the kernel (root in `SubAgentAwait` → preempt → `AgentPreempted` + tears the
@@ -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;
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.79",
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.79"
20
+ "@deepstrike/wasm-kernel": "0.2.80"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/jest": "^30.0.0",