@deepstrike/wasm 0.2.21 → 0.2.25

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.
@@ -37,7 +37,16 @@ export interface GovernancePolicy {
37
37
  windowMs: number;
38
38
  }[];
39
39
  constraints?: GovernanceConstraint[];
40
+ /** I5: when true (default), the runner pre-filters denied tools out of the schema. */
41
+ surfaceDeniedInSystem?: boolean;
40
42
  }
43
+ /** I5: bucket tools into allowed/denied per the policy. Pure. Mirrors Node. */
44
+ export declare function governanceFilterSchema<T extends {
45
+ name: string;
46
+ }>(tools: T[], policy: GovernancePolicy | undefined): {
47
+ allowed: T[];
48
+ denied: string[];
49
+ };
41
50
  export type GovernanceConstraint = {
42
51
  kind: "required";
43
52
  tool: string;
@@ -51,6 +51,31 @@ export class Governance {
51
51
  return this._inner.evaluate(toolName, argsJson);
52
52
  }
53
53
  }
54
+ /** I5: bucket tools into allowed/denied per the policy. Pure. Mirrors Node. */
55
+ export function governanceFilterSchema(tools, policy) {
56
+ if (!policy)
57
+ return { allowed: tools, denied: [] };
58
+ const vetoes = new Set(policy.vetoes ?? []);
59
+ const allowed = [];
60
+ const denied = [];
61
+ const matches = (pat, name) => pat === name || (pat.endsWith("*") && name.startsWith(pat.slice(0, -1)));
62
+ for (const tool of tools) {
63
+ if (vetoes.has(tool.name)) {
64
+ denied.push(tool.name);
65
+ continue;
66
+ }
67
+ let action = policy.defaultAction ?? "allow";
68
+ for (const r of policy.rules ?? []) {
69
+ if (matches(r.pattern, tool.name))
70
+ action = r.action;
71
+ }
72
+ if (action === "deny")
73
+ denied.push(tool.name);
74
+ else
75
+ allowed.push(tool);
76
+ }
77
+ return { allowed, denied };
78
+ }
54
79
  export function governancePolicyToKernelEvent(policy) {
55
80
  return {
56
81
  kind: "load_governance_policy",
@@ -3,6 +3,10 @@ export interface Criterion {
3
3
  text: string;
4
4
  required: boolean;
5
5
  weight?: number;
6
+ /** I3.3 (A4): optional stable id from the host's contract layer; threaded to verdictFn. */
7
+ id?: string;
8
+ /** I3.3 (A4): host hint — host has a deterministic check for this criterion. */
9
+ machineCheckable?: boolean;
6
10
  }
7
11
  export interface CriterionResult {
8
12
  criterion: string;
@@ -62,13 +66,22 @@ export declare class SinglePassHarness {
62
66
  constructor(runner: RuntimeRunner);
63
67
  run(request: HarnessRequest): Promise<HarnessOutcome>;
64
68
  }
69
+ /** I3.2 (A2/A3): host-supplied judgment — see Node `VerdictFn` for the full contract. Mirrors. */
70
+ export type VerdictFn = (ctx: {
71
+ goal: string;
72
+ criteria: Criterion[];
73
+ attempt: number;
74
+ result: string;
75
+ }) => Verdict | undefined | Promise<Verdict | undefined>;
65
76
  export interface HarnessLoopOptions {
66
77
  maxAttempts?: number;
78
+ verdictFn?: VerdictFn;
67
79
  }
68
80
  export declare class HarnessLoop {
69
81
  private runner;
70
82
  private evalProvider;
71
83
  private maxAttempts;
84
+ private verdictFn?;
72
85
  constructor(runner: RuntimeRunner, evalProvider: import("../types.js").LLMProvider, options?: HarnessLoopOptions);
73
86
  runStreaming(request: HarnessRequest): AsyncIterable<HarnessEvent>;
74
87
  }
@@ -23,10 +23,12 @@ export class HarnessLoop {
23
23
  runner;
24
24
  evalProvider;
25
25
  maxAttempts;
26
+ verdictFn;
26
27
  constructor(runner, evalProvider, options = {}) {
27
28
  this.runner = runner;
28
29
  this.evalProvider = evalProvider;
29
30
  this.maxAttempts = options.maxAttempts ?? 3;
31
+ this.verdictFn = options.verdictFn;
30
32
  }
31
33
  async *runStreaming(request) {
32
34
  const kernel = await import("@deepstrike/wasm-kernel");
@@ -61,24 +63,31 @@ export class HarnessLoop {
61
63
  }
62
64
  }
63
65
  yield { type: "supervising" };
64
- // #6 (0.5.0): eval/verdict compute is the kernel's stateless free functions (was EvalPipeline).
65
- const evalMsgs = kernel.buildEvalMessages(request.goal, criteria, lastResult, attempt, true);
66
- let evalText = "";
67
- const evalContext = {
68
- systemText: "",
69
- turns: evalMsgs,
70
- };
71
- for await (const evt of this.evalProvider.stream(evalContext, [], undefined)) {
72
- if (evt.type === "text_delta")
73
- evalText += evt.delta;
66
+ // I3.2 (A2/A3): host-supplied verdictFn short-circuits the LLM eval. Undefined defer.
67
+ let verdict;
68
+ if (this.verdictFn) {
69
+ verdict = await this.verdictFn({ goal: request.goal, criteria, attempt, result: lastResult });
70
+ }
71
+ if (!verdict) {
72
+ // #6 (0.5.0): eval/verdict compute is the kernel's stateless free functions (was EvalPipeline).
73
+ const evalMsgs = kernel.buildEvalMessages(request.goal, criteria, lastResult, attempt, true);
74
+ let evalText = "";
75
+ const evalContext = {
76
+ systemText: "",
77
+ turns: evalMsgs,
78
+ };
79
+ for await (const evt of this.evalProvider.stream(evalContext, [], undefined)) {
80
+ if (evt.type === "text_delta")
81
+ evalText += evt.delta;
82
+ }
83
+ const parsed = kernel.parseVerdict(evalText);
84
+ verdict = {
85
+ passed: parsed.passed,
86
+ overallScore: parsed.overallScore,
87
+ feedback: parsed.feedback,
88
+ details: (parsed.details ?? []),
89
+ };
74
90
  }
75
- const parsed = kernel.parseVerdict(evalText);
76
- const verdict = {
77
- passed: parsed.passed,
78
- overallScore: parsed.overallScore,
79
- feedback: parsed.feedback,
80
- details: (parsed.details ?? []),
81
- };
82
91
  if (verdict.passed) {
83
92
  yield { type: "done", verdict, iterations: lastIterations, totalTokens: lastTotalTokens, status: lastStatus };
84
93
  return;
package/dist/index.d.ts CHANGED
@@ -12,15 +12,17 @@ export type { GovernanceVerdict } from "./governance.js";
12
12
  export { AnthropicProvider } from "./providers/anthropic.js";
13
13
  export { OpenAIProvider, QwenProvider, DeepSeekProvider, MiniMaxProvider, KimiProvider } from "./providers/openai.js";
14
14
  export { tool, executeTools } from "./tools/index.js";
15
- export type { RegisteredTool } from "./tools/index.js";
15
+ export type { RegisteredTool, ToolExecContext } from "./tools/index.js";
16
+ export { safeTool, ok, fail, ToolError, formatToolError } from "./tools/errors.js";
17
+ export type { ToolEnvelope, ToolEnvelopeOk, ToolEnvelopeFail } from "./tools/errors.js";
16
18
  export { WorkingMemory } from "./memory/index.js";
17
19
  export { InMemoryDreamStore } from "./memory/in-memory-store.js";
18
20
  export type { DreamStore, DreamResult, SessionStore, SessionData, SessionMessage, MemoryEntry, CurationResult, CurationStats, } from "./memory/index.js";
19
21
  export type { KnowledgeSource } from "./knowledge/index.js";
20
22
  export { SinglePassHarness, HarnessLoop } from "./harness/index.js";
21
- export type { HarnessRequest, HarnessOutcome, HarnessLoopOptions } from "./harness/index.js";
23
+ export type { HarnessRequest, HarnessOutcome, HarnessLoopOptions, CriterionResult, HarnessEvent, VerdictFn } from "./harness/index.js";
22
24
  export { ScheduledPrompt } from "./signals/index.js";
23
25
  export type { RuntimeSignal, SignalSource } from "./signals/index.js";
24
26
  export { PermissionManager, PermissionMode } from "./safety/index.js";
25
27
  export type { PermissionDecision } from "./safety/index.js";
26
- export type { Message, ToolCall, ToolResult, ToolSchema, RenderedContext, ProviderRunState, StreamEvent, TextDelta, ThinkingDelta, ToolCallEvent, ToolResultEvent, DoneEvent, ErrorEvent, PermissionRequestEvent, PermissionResolvedEvent, PermissionResponse, LLMProvider, } from "./types.js";
28
+ export type { Message, ToolCall, ToolResult, ToolSchema, RenderedContext, ProviderRunState, StreamEvent, TextDelta, ThinkingDelta, ToolCallEvent, ToolResultEvent, ToolAuditFailedEvent, DoneEvent, ErrorEvent, PermissionRequestEvent, PermissionResolvedEvent, PermissionResponse, LLMProvider, CacheBreakpointStrategy, } from "./types.js";
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export { Governance } from "./governance.js";
7
7
  export { AnthropicProvider } from "./providers/anthropic.js";
8
8
  export { OpenAIProvider, QwenProvider, DeepSeekProvider, MiniMaxProvider, KimiProvider } from "./providers/openai.js";
9
9
  export { tool, executeTools } from "./tools/index.js";
10
+ export { safeTool, ok, fail, ToolError, formatToolError } from "./tools/errors.js";
10
11
  export { WorkingMemory } from "./memory/index.js";
11
12
  export { InMemoryDreamStore } from "./memory/in-memory-store.js";
12
13
  export { SinglePassHarness, HarnessLoop } from "./harness/index.js";
@@ -3,18 +3,66 @@ import { assistantReplayKey, collectStreamMessage, toAnthropicMessages } from ".
3
3
  const MAX_CACHE_BREAKPOINTS = 4;
4
4
  /** Rolling cache breakpoints reserved for the message history (system uses ≤2). */
5
5
  const MESSAGE_CACHE_BREAKPOINTS = 2;
6
- function buildAnthropicTools(tools, anchorCache) {
6
+ function buildAnthropicTools(tools, anchorCache, strategy) {
7
+ const emitOnLastTool = anchorCache && (strategy === "default" || strategy === "tools-only");
7
8
  return tools.map((t, i) => ({
8
9
  name: t.name,
9
10
  description: t.description,
10
11
  input_schema: JSON.parse(t.parameters),
11
- // Anchor a tool breakpoint only when the system blocks won't carry one;
12
- // otherwise systemStable already caches the tools prefix (tools render
13
- // first), and a redundant tool breakpoint would burn a slot the message
14
- // history needs to stay within the 4-breakpoint budget.
15
- ...(anchorCache && i === tools.length - 1 ? { cache_control: { type: "ephemeral" } } : {}),
12
+ ...(emitOnLastTool && i === tools.length - 1 ? { cache_control: { type: "ephemeral" } } : {}),
16
13
  }));
17
14
  }
15
+ /** Recognised values for the `cacheBreakpointStrategy` extension; unknown → "default". */
16
+ const CACHE_BREAKPOINT_STRATEGIES = new Set([
17
+ "default", "tools-only", "system-only", "frozen-prefix", "none",
18
+ ]);
19
+ function resolveCacheBreakpointStrategy(extensions) {
20
+ const raw = extensions?.cacheBreakpointStrategy;
21
+ if (typeof raw === "string" && CACHE_BREAKPOINT_STRATEGIES.has(raw)) {
22
+ return raw;
23
+ }
24
+ return "default";
25
+ }
26
+ /** I1: which slots of the outgoing request carry a `cache_control` breakpoint — used to pro-rata
27
+ * attribute the response's `cache_read_input_tokens` (a single scalar). Mirrors Node. */
28
+ function countCacheControlSlots(system, builtTools, msgs) {
29
+ const sysBp = Array.isArray(system) && system.some(b => b?.cache_control != null);
30
+ const toolBp = !!builtTools && builtTools.some(t => t?.cache_control != null);
31
+ let msgBp = false;
32
+ for (const m of msgs) {
33
+ if (Array.isArray(m.content)) {
34
+ if (m.content.some(b => b?.cache_control != null)) {
35
+ msgBp = true;
36
+ break;
37
+ }
38
+ }
39
+ }
40
+ return { system: sysBp, tools: toolBp, messages: msgBp };
41
+ }
42
+ /** I1: split `cache_read_input_tokens` evenly across contributing slots. Remainder lands on the
43
+ * first contributing slot to keep the sum exact. Mirrors Node. */
44
+ function estimateCacheReadBySlot(cacheRead, slotBp) {
45
+ if (cacheRead <= 0)
46
+ return undefined;
47
+ const count = (slotBp.system ? 1 : 0) + (slotBp.tools ? 1 : 0) + (slotBp.messages ? 1 : 0);
48
+ if (count === 0)
49
+ return undefined;
50
+ const share = Math.floor(cacheRead / count);
51
+ const remainder = cacheRead - share * count;
52
+ const out = {};
53
+ let firstDone = false;
54
+ const give = () => { if (!firstDone) {
55
+ firstDone = true;
56
+ return share + remainder;
57
+ } return share; };
58
+ if (slotBp.system)
59
+ out.system = give();
60
+ if (slotBp.tools)
61
+ out.tools = give();
62
+ if (slotBp.messages)
63
+ out.messages = give();
64
+ return out;
65
+ }
18
66
  /**
19
67
  * Roll cache breakpoints across the conversation tail so the message-history
20
68
  * prefix is written once and re-read on later turns (without this the cached
@@ -27,14 +75,16 @@ function buildAnthropicTools(tools, anchorCache) {
27
75
  * only the incremental `[frozen..tail]`. Otherwise fall back to the rolling
28
76
  * pair: final message + nearest preceding user turn.
29
77
  */
30
- function applyMessageCacheControl(msgs, frozenPrefixLen) {
78
+ function applyMessageCacheControl(msgs, frozenPrefixLen, strategy) {
31
79
  if (!msgs.length)
32
80
  return;
81
+ if (strategy === "tools-only" || strategy === "system-only" || strategy === "none")
82
+ return;
33
83
  const targets = new Set([msgs.length - 1]);
34
84
  if (typeof frozenPrefixLen === "number" && frozenPrefixLen >= 1 && frozenPrefixLen < msgs.length) {
35
85
  targets.add(frozenPrefixLen - 1);
36
86
  }
37
- else {
87
+ else if (strategy === "default") {
38
88
  for (let i = msgs.length - 2; i >= 0 && targets.size < MESSAGE_CACHE_BREAKPOINTS; i--) {
39
89
  if (msgs[i].role === "user")
40
90
  targets.add(i);
@@ -110,16 +160,19 @@ export class AnthropicProvider {
110
160
  return collectStreamMessage(this.stream(context, tools, extensions));
111
161
  }
112
162
  async *stream(context, tools, extensions, _state, signal) {
163
+ const strategy = resolveCacheBreakpointStrategy(extensions);
164
+ const emitOnSystemBlocks = strategy === "default" || strategy === "system-only";
165
+ const cc = { type: "ephemeral" };
113
166
  const systemBlocks = [];
114
167
  if (context.systemStable) {
115
- systemBlocks.push({ type: "text", text: context.systemStable, cache_control: { type: "ephemeral" } });
168
+ systemBlocks.push({ type: "text", text: context.systemStable, ...(emitOnSystemBlocks ? { cache_control: cc } : {}) });
116
169
  }
117
170
  if (context.systemKnowledge) {
118
- systemBlocks.push({ type: "text", text: context.systemKnowledge, cache_control: { type: "ephemeral" } });
171
+ systemBlocks.push({ type: "text", text: context.systemKnowledge, ...(emitOnSystemBlocks ? { cache_control: cc } : {}) });
119
172
  }
120
173
  const system = systemBlocks.length ? systemBlocks : (context.systemText || undefined);
121
174
  const msgs = toAnthropicMessages(context, message => this.nativeAssistantBlocks.get(assistantReplayKey(message)));
122
- applyMessageCacheControl(msgs, context.frozenPrefixLen);
175
+ applyMessageCacheControl(msgs, context.frozenPrefixLen, strategy);
123
176
  // Append the volatile State turn AFTER the cache breakpoints (uncached tail);
124
177
  // absent on un-rebuilt bindings, where the state is already inside `turns`.
125
178
  // Render through toAnthropicMessages so assistant tool_use blocks are
@@ -130,13 +183,16 @@ export class AnthropicProvider {
130
183
  msgs.push(...stateMsgs);
131
184
  }
132
185
  assertCacheBudget(system, tools.length);
186
+ const builtTools = tools.length ? buildAnthropicTools(tools, !Array.isArray(system), strategy) : undefined;
187
+ // I1: capture which slots will carry cache_control for pro-rata attribution at usage time.
188
+ const slotBp = countCacheControlSlots(system, builtTools, msgs);
133
189
  const body = {
134
190
  model: this.model,
135
191
  max_tokens: this.maxTokens,
136
192
  messages: msgs,
137
193
  stream: true,
138
194
  ...(system ? { system } : {}),
139
- ...(tools.length ? { tools: buildAnthropicTools(tools, !Array.isArray(system)) } : {}),
195
+ ...(builtTools ? { tools: builtTools } : {}),
140
196
  };
141
197
  if (extensions?.enable_thinking) {
142
198
  body.thinking = { type: "enabled", budget_tokens: 8000 };
@@ -193,6 +249,7 @@ export class AnthropicProvider {
193
249
  // the kernel reads it as the authoritative context size.
194
250
  const inputTokens = uncachedInput + cacheReadTokens + cacheCreationTokens;
195
251
  if (inputTokens > 0 || outputTokens > 0) {
252
+ const bySlot = estimateCacheReadBySlot(cacheReadTokens, slotBp);
196
253
  yield {
197
254
  type: "usage",
198
255
  totalTokens: inputTokens + outputTokens,
@@ -200,6 +257,7 @@ export class AnthropicProvider {
200
257
  outputTokens,
201
258
  cacheReadInputTokens: cacheReadTokens,
202
259
  cacheCreationInputTokens: cacheCreationTokens,
260
+ ...(bySlot ? { cacheReadInputTokensBySlot: bySlot } : {}),
203
261
  };
204
262
  }
205
263
  }
@@ -1,4 +1,5 @@
1
1
  import { LargeResultSpool } from "./large-result-spool.js";
2
+ import { formatToolError } from "../tools/errors.js";
2
3
  function stripFrontmatter(content) {
3
4
  const s = content.trimStart();
4
5
  if (!s.startsWith("---"))
@@ -71,19 +72,38 @@ export class LocalExecutionPlane {
71
72
  yield { type: "tool_result", callId: call.id, name: call.name, content: `unknown tool: ${call.name}`, isError: true, isFatal: false, errorKind: "recoverable" };
72
73
  continue;
73
74
  }
75
+ // Per-call `audit` helper: failures collected here are surfaced as `tool_audit_failed`
76
+ // events rather than flipping the main tool result to `isError: true`.
77
+ const auditFailures = [];
78
+ const callCtx = {
79
+ ...(ctx.cwd !== undefined ? { cwd: ctx.cwd } : {}),
80
+ audit: async (label, fn) => {
81
+ try {
82
+ await fn();
83
+ }
84
+ catch (err) {
85
+ auditFailures.push({ label, error: formatToolError(err) });
86
+ }
87
+ },
88
+ };
74
89
  try {
75
90
  const args = JSON.parse(call.arguments || "{}");
76
- // M3/G4: pass the run context for tool-ABI parity with Node/Python (`RunContext` is
77
- // structurally assignable to the tool's `ToolExecContext`).
78
- const output = await registered.execute(args, ctx);
91
+ // M3/G4: pass the run context (incl. `cwd`, `audit`) for tool-ABI parity with Node/Python.
92
+ const output = await registered.execute(args, callCtx);
93
+ for (const f of auditFailures) {
94
+ yield { type: "tool_audit_failed", callId: call.id, name: call.name, label: f.label, error: f.error };
95
+ }
79
96
  yield { type: "tool_result", callId: call.id, name: call.name, content: String(output), isError: false };
80
97
  }
81
98
  catch (err) {
99
+ for (const f of auditFailures) {
100
+ yield { type: "tool_audit_failed", callId: call.id, name: call.name, label: f.label, error: f.error };
101
+ }
82
102
  yield {
83
103
  type: "tool_result",
84
104
  callId: call.id,
85
105
  name: call.name,
86
- content: String(err),
106
+ content: formatToolError(err),
87
107
  isError: true,
88
108
  isFatal: Boolean(err?.isFatal),
89
109
  errorKind: err?.errorKind,
@@ -134,7 +154,7 @@ export async function resolvePermissionRequest(request, ctx) {
134
154
  return {
135
155
  approved: false,
136
156
  responder: "permission_handler",
137
- reason: `permission handler failed: ${String(err)}`,
157
+ reason: `permission handler failed: ${formatToolError(err)}`,
138
158
  };
139
159
  }
140
160
  }
@@ -51,6 +51,12 @@ export interface TurnMetrics {
51
51
  activeSkill?: string;
52
52
  inputTokens: number;
53
53
  cacheReadTokens: number;
54
+ /** I1: pro-rata per-slot attribution of `cacheReadTokens` (Anthropic only). Mirrors Node. */
55
+ cacheReadTokensBySlot?: {
56
+ system?: number;
57
+ tools?: number;
58
+ messages?: number;
59
+ };
54
60
  cacheCreationTokens: number;
55
61
  }
56
62
  export interface RuntimeOptions {
@@ -67,6 +73,12 @@ export interface RuntimeOptions {
67
73
  maxTurns?: number;
68
74
  timeoutMs?: number;
69
75
  agentId?: string;
76
+ /** I4: optional run-start memory pre-fetch hook (mirrors Node SDK). Called once per run before
77
+ * the first LLM turn; each returned query string becomes a dreamStore search; hits page into
78
+ * the knowledge partition before turn 1. Requires dreamStore + agentId. */
79
+ preQueryMemory?: (ctx: {
80
+ goal: string;
81
+ }) => Promise<string[] | undefined> | string[] | undefined;
70
82
  systemPrompt?: string;
71
83
  initialMemory?: string[];
72
84
  /** Skill name → markdown body (WASM has no filesystem). */