@deepstrike/sdk 0.2.22 → 0.2.24

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.
@@ -29,7 +29,28 @@ export interface GovernancePolicy {
29
29
  windowMs: number;
30
30
  }[];
31
31
  constraints?: GovernanceConstraint[];
32
+ /** I5: when true (default), the runner pre-filters denied tools out of the schema passed to the
33
+ * provider — the model never sees them and never tries to call them, eliminating the rollback
34
+ * turn the kernel would otherwise produce. The denied tool names are also surfaced as a single
35
+ * line on the system slot so the model knows not to plan around them. Set to false to fall
36
+ * back to the v0.2.22 rollback-based behavior (useful for measuring the delta or when the
37
+ * agent should learn the denial via a real attempt). */
38
+ surfaceDeniedInSystem?: boolean;
32
39
  }
40
+ /** I5: walk the tool list and bucket each tool into `allowed` / `denied` based on a declarative
41
+ * policy. A tool is denied when:
42
+ * - the tool name appears in `vetoes`
43
+ * - a `rules[i].pattern` matches the tool name and the rule's `action === "deny"`
44
+ * - or `defaultAction === "deny"` and no `allow` rule matches
45
+ * `ask_user` is treated as allowed at the schema layer — the runtime decides at call time.
46
+ * Pattern matching is exact match or a glob with a single trailing `*` (so `"write_*"` denies
47
+ * `write_file` and `write_db`). Pure — no side effects. */
48
+ export declare function governanceFilterSchema<T extends {
49
+ name: string;
50
+ }>(tools: T[], policy: GovernancePolicy | undefined): {
51
+ allowed: T[];
52
+ denied: string[];
53
+ };
33
54
  export type GovernanceConstraint = {
34
55
  kind: "required";
35
56
  tool: string;
@@ -32,6 +32,38 @@ export class Governance {
32
32
  return this.inner.evaluate(toolName, argsJson);
33
33
  }
34
34
  }
35
+ /** I5: walk the tool list and bucket each tool into `allowed` / `denied` based on a declarative
36
+ * policy. A tool is denied when:
37
+ * - the tool name appears in `vetoes`
38
+ * - a `rules[i].pattern` matches the tool name and the rule's `action === "deny"`
39
+ * - or `defaultAction === "deny"` and no `allow` rule matches
40
+ * `ask_user` is treated as allowed at the schema layer — the runtime decides at call time.
41
+ * Pattern matching is exact match or a glob with a single trailing `*` (so `"write_*"` denies
42
+ * `write_file` and `write_db`). Pure — no side effects. */
43
+ export function governanceFilterSchema(tools, policy) {
44
+ if (!policy)
45
+ return { allowed: tools, denied: [] };
46
+ const vetoes = new Set(policy.vetoes ?? []);
47
+ const allowed = [];
48
+ const denied = [];
49
+ const matches = (pat, name) => pat === name || (pat.endsWith("*") && name.startsWith(pat.slice(0, -1)));
50
+ for (const tool of tools) {
51
+ if (vetoes.has(tool.name)) {
52
+ denied.push(tool.name);
53
+ continue;
54
+ }
55
+ let action = policy.defaultAction ?? "allow";
56
+ for (const r of policy.rules ?? []) {
57
+ if (matches(r.pattern, tool.name))
58
+ action = r.action;
59
+ }
60
+ if (action === "deny")
61
+ denied.push(tool.name);
62
+ else
63
+ allowed.push(tool);
64
+ }
65
+ return { allowed, denied };
66
+ }
35
67
  /**
36
68
  * Convert a declarative {@link GovernancePolicy} into the `load_governance_policy`
37
69
  * kernel event payload (snake_case wire fields). Pure — no side effects.
@@ -5,6 +5,14 @@ export interface Criterion {
5
5
  text: string;
6
6
  required: boolean;
7
7
  weight?: number;
8
+ /** I3.3 (A4): optional stable identifier from the host's contract layer (e.g. an
9
+ * `acceptance[].id` field). The harness does not interpret it; it just threads it through to
10
+ * `verdictFn` so the host can dispatch per-criterion deterministic checks by id. */
11
+ id?: string;
12
+ /** I3.3 (A4): host hint — when true, the host has a deterministic check for this criterion
13
+ * and would short-circuit the LLM eval. The harness still defers to the host's `verdictFn`
14
+ * for the actual decision; this is purely a transparency field on the request. */
15
+ machineCheckable?: boolean;
8
16
  }
9
17
  export interface CriterionResult {
10
18
  criterion: string;
@@ -83,6 +91,14 @@ export declare class SinglePassHarness {
83
91
  run(request: HarnessRequest): Promise<HarnessOutcome>;
84
92
  stream(request: HarnessRequest): AsyncIterable<StreamEvent>;
85
93
  }
94
+ /**
95
+ * @deprecated I3.4 (A1): prefer {@link HarnessLoop} with `verdictFn` for host-defined judgment.
96
+ * `EvalLoopHarness.stream()` does NOT honor the `gate` passed via `request.gate` (only `.run()`
97
+ * does), which is a long-standing footgun for streaming hosts. `HarnessLoop` runs the eval loop
98
+ * uniformly across both stream and run, accepts an optional `verdictFn` for short-circuiting the
99
+ * built-in LLM eval, and otherwise mirrors `EvalLoopHarness`'s behavior. New code should use
100
+ * `HarnessLoop`; existing call sites can migrate by switching the class + (if applicable)
101
+ * passing a `verdictFn` for the same gate logic. Slated for removal in a future major. */
86
102
  export declare class EvalLoopHarness {
87
103
  private runner;
88
104
  private gate;
@@ -91,15 +107,29 @@ export declare class EvalLoopHarness {
91
107
  run(request: HarnessRequest): Promise<HarnessOutcome>;
92
108
  stream(request: HarnessRequest): AsyncIterable<StreamEvent>;
93
109
  }
110
+ /** I3.2 (A2/A3): host-supplied judgment for each attempt's result. Returning a `Verdict` short-
111
+ * circuits the built-in LLM eval (no `evalProvider.stream` call); returning `undefined` defers to
112
+ * the built-in eval (enables hybrid judgment: machine-checkable items deterministic, subjective
113
+ * items LLM). Pure addition — when not set, HarnessLoop.stream() is byte-equivalent to its prior
114
+ * behavior. The closure owns its own context (doc reader, deterministic checks, etc.); the SDK
115
+ * is intentionally agnostic about what it inspects. */
116
+ export type VerdictFn = (ctx: {
117
+ goal: string;
118
+ criteria: Criterion[];
119
+ attempt: number;
120
+ result: string;
121
+ }) => Verdict | undefined | Promise<Verdict | undefined>;
94
122
  export interface HarnessLoopOptions {
95
123
  maxAttempts?: number;
96
124
  skillDir?: string;
125
+ verdictFn?: VerdictFn;
97
126
  }
98
127
  export declare class HarnessLoop {
99
128
  private runner;
100
129
  private evalProvider;
101
130
  private maxAttempts;
102
131
  private skillDir?;
132
+ private verdictFn?;
103
133
  constructor(runner: RuntimeRunner, evalProvider: import("../types.js").LLMProvider, options?: HarnessLoopOptions);
104
134
  run(request: HarnessRequest): Promise<HarnessOutcome>;
105
135
  stream(request: HarnessRequest): AsyncIterable<HarnessEvent>;
@@ -26,6 +26,14 @@ export class SinglePassHarness {
26
26
  yield* this.runner.run({ sessionId: crypto.randomUUID(), goal: request.goal, criteria: request.criteria?.map(c => c.text), extensions: request.extensions });
27
27
  }
28
28
  }
29
+ /**
30
+ * @deprecated I3.4 (A1): prefer {@link HarnessLoop} with `verdictFn` for host-defined judgment.
31
+ * `EvalLoopHarness.stream()` does NOT honor the `gate` passed via `request.gate` (only `.run()`
32
+ * does), which is a long-standing footgun for streaming hosts. `HarnessLoop` runs the eval loop
33
+ * uniformly across both stream and run, accepts an optional `verdictFn` for short-circuiting the
34
+ * built-in LLM eval, and otherwise mirrors `EvalLoopHarness`'s behavior. New code should use
35
+ * `HarnessLoop`; existing call sites can migrate by switching the class + (if applicable)
36
+ * passing a `verdictFn` for the same gate logic. Slated for removal in a future major. */
29
37
  export class EvalLoopHarness {
30
38
  runner;
31
39
  gate;
@@ -53,11 +61,13 @@ export class HarnessLoop {
53
61
  evalProvider;
54
62
  maxAttempts;
55
63
  skillDir;
64
+ verdictFn;
56
65
  constructor(runner, evalProvider, options = {}) {
57
66
  this.runner = runner;
58
67
  this.evalProvider = evalProvider;
59
68
  this.maxAttempts = options.maxAttempts ?? 3;
60
69
  this.skillDir = options.skillDir;
70
+ this.verdictFn = options.verdictFn;
61
71
  }
62
72
  async run(request) {
63
73
  let last;
@@ -125,28 +135,38 @@ export class HarnessLoop {
125
135
  }
126
136
  }
127
137
  yield { type: "supervising" };
128
- // #6 (0.5.0): the eval/verdict compute is the kernel's stateless free functions (was the
129
- // EvalPipeline state machine). Build the eval prompt, call the eval LLM, parse the verdict.
130
- const evalMsgs = kernel.buildEvalMessages(request.goal, criteria, lastResult, attempt, true);
131
- let evalText = "";
132
- const evalContext = {
133
- systemText: evalMsgs.filter((m) => m.role === "system").map((m) => m.content).join("\n\n"),
134
- turns: evalMsgs.filter((m) => m.role !== "system"),
135
- };
136
- for await (const evt of this.evalProvider.stream(evalContext, [], undefined)) {
137
- if (evt.type === "text_delta")
138
- evalText += evt.delta;
138
+ // I3.2 (A2/A3): host-supplied `verdictFn` short-circuits the LLM eval. When it returns a
139
+ // Verdict, use it; when it returns undefined, defer to the built-in eval (hybrid path).
140
+ let verdict;
141
+ let skillCandidate;
142
+ if (this.verdictFn) {
143
+ verdict = await this.verdictFn({ goal: request.goal, criteria, attempt, result: lastResult });
144
+ }
145
+ if (!verdict) {
146
+ // #6 (0.5.0): the eval/verdict compute is the kernel's stateless free functions (was the
147
+ // EvalPipeline state machine). Build the eval prompt, call the eval LLM, parse the verdict.
148
+ const evalMsgs = kernel.buildEvalMessages(request.goal, criteria, lastResult, attempt, true);
149
+ let evalText = "";
150
+ const evalContext = {
151
+ systemText: evalMsgs.filter((m) => m.role === "system").map((m) => m.content).join("\n\n"),
152
+ turns: evalMsgs.filter((m) => m.role !== "system"),
153
+ };
154
+ for await (const evt of this.evalProvider.stream(evalContext, [], undefined)) {
155
+ if (evt.type === "text_delta")
156
+ evalText += evt.delta;
157
+ }
158
+ const parsed = kernel.parseVerdict(evalText);
159
+ verdict = {
160
+ passed: parsed.passed,
161
+ overallScore: parsed.overallScore,
162
+ feedback: parsed.feedback,
163
+ details: parsed.details ?? [],
164
+ };
165
+ skillCandidate = parsed.skillCandidate;
139
166
  }
140
- const parsed = kernel.parseVerdict(evalText);
141
- const verdict = {
142
- passed: parsed.passed,
143
- overallScore: parsed.overallScore,
144
- feedback: parsed.feedback,
145
- details: parsed.details ?? [],
146
- };
147
167
  if (verdict.passed) {
148
- if (parsed.skillCandidate && this.skillDir) {
149
- const { name, description, whenToUse, content } = parsed.skillCandidate;
168
+ if (skillCandidate && this.skillDir) {
169
+ const { name, description, whenToUse, content } = skillCandidate;
150
170
  const fm = ["---", `name: ${name}`, `description: ${description}`,
151
171
  whenToUse ? `when_to_use: ${whenToUse}` : null, "---", ""]
152
172
  .filter(Boolean).join("\n");
package/dist/index.d.ts CHANGED
@@ -56,7 +56,9 @@ export type { CreateProviderOptions, EndpointProfileId } from "./providers/catal
56
56
  export { ProviderReplayValidationError, DEGRADED_REASONING_PLACEHOLDER } from "./providers/replay-validator.js";
57
57
  export { assessProviderReplayability, peekProviderReplay, seedProviderReplayFromEvents, isReplayCompatibleWithProvider, } from "./runtime/provider-replay.js";
58
58
  export { tool, streamingTool, executeTools, readFile, validateToolArguments } from "./tools/index.js";
59
- export type { RegisteredTool } from "./tools/index.js";
59
+ export type { RegisteredTool, ToolExecContext } from "./tools/index.js";
60
+ export { safeTool, ok, fail, ToolError, formatToolError } from "./tools/errors.js";
61
+ export type { ToolEnvelope, ToolEnvelopeOk, ToolEnvelopeFail } from "./tools/errors.js";
60
62
  export { scanSkillDir, readSkillFile } from "./skills/loader.js";
61
63
  export type { SkillMetadata } from "./skills/loader.js";
62
64
  export { WorkingMemory } from "./memory/working.js";
@@ -71,8 +73,8 @@ export type { PermissionDecision, Permission } from "./safety/permissions.js";
71
73
  export { Governance, governancePolicyToKernelEvent } from "./governance.js";
72
74
  export type { GovernanceVerdict, GovernancePolicy, GovernanceConstraint } from "./governance.js";
73
75
  export { SinglePassHarness, EvalLoopHarness, HarnessLoop } from "./harness/harness.js";
74
- export type { HarnessRequest, HarnessOutcome, HarnessLoopOptions, QualityGate } from "./harness/harness.js";
75
- export type { Message, ToolCall, ToolResult, ToolSchema, ContentPart, TextPart, ImagePart, AudioPart, StreamEvent, TextDelta, ThinkingDelta, ToolCallEvent, ToolChunk, ToolDeltaEvent, ToolSuspendEvent, ToolResultEvent, DoneEvent, ErrorEvent, PermissionRequestEvent, PermissionResolvedEvent, PermissionResponse, LLMProvider, RetryConfig, TokenUsage, ProviderToolSpec, ProviderRunState, ProviderReplay, RenderedContext, ReplayabilityAssessment, CacheBreakpointStrategy, } from "./types.js";
76
+ export type { HarnessRequest, HarnessOutcome, HarnessLoopOptions, QualityGate, CriterionResult, HarnessEvent, VerdictFn } from "./harness/harness.js";
77
+ export type { Message, ToolCall, ToolResult, ToolSchema, ContentPart, TextPart, ImagePart, AudioPart, StreamEvent, TextDelta, ThinkingDelta, ToolCallEvent, ToolChunk, ToolDeltaEvent, ToolSuspendEvent, ToolResultEvent, ToolAuditFailedEvent, DoneEvent, ErrorEvent, PermissionRequestEvent, PermissionResolvedEvent, PermissionResponse, LLMProvider, RetryConfig, TokenUsage, ProviderToolSpec, ProviderRunState, ProviderReplay, RenderedContext, ReplayabilityAssessment, CacheBreakpointStrategy, } from "./types.js";
76
78
  export type { AgentCapabilityFilter, AgentIdentity, AgentIsolation, AgentRunSpec, AgentProcessChangedObservation, ContextInheritance, KernelAgentRole, LoopResult, MilestoneCheckResult, MilestoneContract, MilestonePhase, MilestonePolicy, SubAgentResult, TerminationReason, WorkflowSpec, WorkflowNodeSpec, WorkflowTaskSpec, WorkflowSpawnInfo, } from "./types/agent.js";
77
79
  export { agentIdentitySub, agentRunSpecToKernel, milestoneCheckFail, milestoneCheckPass, milestoneCheckResultToKernel, subAgentResultToKernel, workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, submitWorkflowNodesTool, startWorkflowTool, fanoutSynthesize, generateAndFilter, verifyRules, genEval, } from "./types/agent.js";
78
80
  export type { AcceptanceCriterion, VerificationContract, ContractCheckResult, } from "./collaboration/contract.js";
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ export { ProviderReplayValidationError, DEGRADED_REASONING_PLACEHOLDER } from ".
39
39
  export { assessProviderReplayability, peekProviderReplay, seedProviderReplayFromEvents, isReplayCompatibleWithProvider, } from "./runtime/provider-replay.js";
40
40
  // ── Tools & Skills ─────────────────────────────────────────────────────────
41
41
  export { tool, streamingTool, executeTools, readFile, validateToolArguments } from "./tools/index.js";
42
+ export { safeTool, ok, fail, ToolError, formatToolError } from "./tools/errors.js";
42
43
  export { scanSkillDir, readSkillFile } from "./skills/loader.js";
43
44
  // ── Memory ─────────────────────────────────────────────────────────────────
44
45
  export { WorkingMemory } from "./memory/working.js";
@@ -139,6 +139,11 @@ export class AnthropicProvider {
139
139
  const msgs = this.buildMessages(context, strategy);
140
140
  assertCacheBudget(system, tools.length);
141
141
  const requestExtensions = this.requestExtensions(extensions);
142
+ const builtTools = tools.length ? this.buildTools(tools, !Array.isArray(system), strategy) : undefined;
143
+ // I1: capture which slots will carry cache_control so cache_read_input_tokens can be
144
+ // attributed pro-rata when the response arrives. Honest annotation: Anthropic returns one
145
+ // scalar (no per-slot breakdown), so this is an estimate, not authoritative.
146
+ const slotBp = countCacheControlSlots(system, builtTools, msgs);
142
147
  const toolBlocks = {};
143
148
  const nativeBlocks = {};
144
149
  let finalText = "";
@@ -149,7 +154,7 @@ export class AnthropicProvider {
149
154
  max_tokens: typeof extensions?.max_tokens === "number" ? extensions.max_tokens : 8096,
150
155
  ...(system ? { system } : {}),
151
156
  messages: msgs,
152
- ...(tools.length ? { tools: this.buildTools(tools, !Array.isArray(system), strategy) } : {}),
157
+ ...(builtTools ? { tools: builtTools } : {}),
153
158
  }, extensions, signal);
154
159
  let uncachedInput = 0;
155
160
  let cacheReadTokens = 0;
@@ -171,6 +176,7 @@ export class AnthropicProvider {
171
176
  // context-pressure/compaction — excluding cached tokens would make a
172
177
  // cache-heavy turn look tiny and suppress compaction until a 413.
173
178
  const inputTokens = uncachedInput + cacheReadTokens + cacheCreationTokens;
179
+ const bySlot = estimateCacheReadBySlot(cacheReadTokens, slotBp);
174
180
  yield {
175
181
  type: "usage",
176
182
  totalTokens: inputTokens + outputTokens,
@@ -178,6 +184,7 @@ export class AnthropicProvider {
178
184
  outputTokens,
179
185
  cacheReadInputTokens: cacheReadTokens,
180
186
  cacheCreationInputTokens: cacheCreationTokens,
187
+ ...(bySlot ? { cacheReadInputTokensBySlot: bySlot } : {}),
181
188
  };
182
189
  }
183
190
  }
@@ -304,6 +311,58 @@ function resolveCacheBreakpointStrategy(extensions) {
304
311
  }
305
312
  return "default";
306
313
  }
314
+ /**
315
+ * I1: count which slots of the outgoing request carry a `cache_control` breakpoint. Used to
316
+ * pro-rata-attribute the response's `cache_read_input_tokens` (a single scalar with no per-slot
317
+ * breakdown) across the slots that contributed to the cache hit. Returns whether each slot has
318
+ * any breakpoint — not the actual count, since pro-rata only needs the contributing slot set.
319
+ */
320
+ function countCacheControlSlots(system, builtTools, msgs) {
321
+ const sysBp = Array.isArray(system) && system.some(b => b?.cache_control != null);
322
+ const toolBp = !!builtTools && builtTools.some(t => t?.cache_control != null);
323
+ let msgBp = false;
324
+ for (const m of msgs) {
325
+ if (Array.isArray(m.content)) {
326
+ if (m.content.some(b => b?.cache_control != null)) {
327
+ msgBp = true;
328
+ break;
329
+ }
330
+ }
331
+ }
332
+ return { system: sysBp, tools: toolBp, messages: msgBp };
333
+ }
334
+ /**
335
+ * I1: split the response's `cache_read_input_tokens` evenly across the slots that carried a
336
+ * cache_control breakpoint on the request. Returns undefined when there's no cache read or no
337
+ * contributing slot — in those cases the consumer is better off seeing the field absent than
338
+ * seeing all zeros. The remainder (if the total doesn't divide evenly) lands on the first
339
+ * contributing slot to keep the sum exact.
340
+ */
341
+ function estimateCacheReadBySlot(cacheRead, slotBp) {
342
+ if (cacheRead <= 0)
343
+ return undefined;
344
+ const count = (slotBp.system ? 1 : 0) + (slotBp.tools ? 1 : 0) + (slotBp.messages ? 1 : 0);
345
+ if (count === 0)
346
+ return undefined;
347
+ const share = Math.floor(cacheRead / count);
348
+ const remainder = cacheRead - share * count;
349
+ const out = {};
350
+ let firstDone = false;
351
+ const give = () => {
352
+ if (!firstDone) {
353
+ firstDone = true;
354
+ return share + remainder;
355
+ }
356
+ return share;
357
+ };
358
+ if (slotBp.system)
359
+ out.system = give();
360
+ if (slotBp.tools)
361
+ out.tools = give();
362
+ if (slotBp.messages)
363
+ out.messages = give();
364
+ return out;
365
+ }
307
366
  /** Anthropic accepts at most this many cache_control breakpoints per request. */
308
367
  const MAX_CACHE_BREAKPOINTS = 4;
309
368
  /**
@@ -1,4 +1,5 @@
1
- import { isAsyncIterable, normalizeToolChunk, toolChunkText, validateToolArguments } from "../tools/index.js";
1
+ import { isAsyncIterable, maybeWarnFailureShapedChunk, normalizeToolChunk, toolChunkText, validateToolArguments } from "../tools/index.js";
2
+ import { formatToolError } from "../tools/errors.js";
2
3
  import { readSkillFile } from "../skills/loader.js";
3
4
  import { LargeResultSpool } from "./large-result-spool.js";
4
5
  export class LocalExecutionPlane {
@@ -102,6 +103,20 @@ export class LocalExecutionPlane {
102
103
  const registered = this.tools.get(call.name);
103
104
  if (!registered)
104
105
  return { callId: call.id, output: `unknown tool: ${call.name}`, isError: true };
106
+ // `audit` failure buffer is hoisted above the try-block so the catch path can flush any
107
+ // best-effort failures recorded before the main throw.
108
+ const auditFailures = [];
109
+ const callCtx = {
110
+ ...(ctx.cwd !== undefined ? { cwd: ctx.cwd } : {}),
111
+ audit: async (label, fn) => {
112
+ try {
113
+ await fn();
114
+ }
115
+ catch (err) {
116
+ auditFailures.push({ label, error: formatToolError(err) });
117
+ }
118
+ },
119
+ };
105
120
  try {
106
121
  const args = JSON.parse(call.arguments || "{}");
107
122
  const originalArgsStr = JSON.stringify(args);
@@ -119,7 +134,8 @@ export class LocalExecutionPlane {
119
134
  }
120
135
  // M3/G4: pass the run context (incl. `cwd`) so cwd-aware tools scope their work to the
121
136
  // sub-agent's worktree. `RunContext` is structurally assignable to the tool's `ToolExecContext`.
122
- const output = await registered.execute(args, ctx);
137
+ // The per-call `audit` helper (above) layers best-effort side-effect handling on top.
138
+ const output = await registered.execute(args, callCtx);
123
139
  if (isAsyncIterable(output)) {
124
140
  let combined = "";
125
141
  const iterator = output[Symbol.asyncIterator]();
@@ -145,16 +161,28 @@ export class LocalExecutionPlane {
145
161
  }
146
162
  const delta = toolChunkText(next.value);
147
163
  combined += delta;
164
+ if (delta)
165
+ maybeWarnFailureShapedChunk(call.name, delta);
148
166
  yield { type: "tool_delta", callId: call.id, name: call.name, ...(delta ? { delta } : {}), chunk };
149
167
  }
168
+ for (const f of auditFailures) {
169
+ yield { type: "tool_audit_failed", callId: call.id, name: call.name, label: f.label, error: f.error };
170
+ }
150
171
  return { callId: call.id, output: combined, isError: false };
151
172
  }
173
+ for (const f of auditFailures) {
174
+ yield { type: "tool_audit_failed", callId: call.id, name: call.name, label: f.label, error: f.error };
175
+ }
152
176
  return { callId: call.id, output, isError: false };
153
177
  }
154
178
  catch (err) {
179
+ // Audit failures recorded before the main throw are still informational; surface them.
180
+ for (const f of auditFailures) {
181
+ yield { type: "tool_audit_failed", callId: call.id, name: call.name, label: f.label, error: f.error };
182
+ }
155
183
  return {
156
184
  callId: call.id,
157
- output: String(err),
185
+ output: formatToolError(err),
158
186
  isError: true,
159
187
  isFatal: Boolean(err?.isFatal),
160
188
  errorKind: err?.errorKind,
@@ -185,7 +213,7 @@ export async function resolvePermissionRequest(request, ctx) {
185
213
  return {
186
214
  approved: false,
187
215
  responder: "permission_handler",
188
- reason: `permission handler failed: ${String(err)}`,
216
+ reason: `permission handler failed: ${formatToolError(err)}`,
189
217
  };
190
218
  }
191
219
  }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
  import { LocalExecutionPlane } from "./execution-plane.js";
4
+ import { formatToolError } from "../tools/errors.js";
4
5
  class McpConnection {
5
6
  serverName;
6
7
  config;
@@ -97,7 +98,7 @@ class McpConnection {
97
98
  return { output: text || JSON.stringify(result), isError: result.isError ?? false };
98
99
  }
99
100
  catch (err) {
100
- return { output: String(err), isError: true };
101
+ return { output: formatToolError(err), isError: true };
101
102
  }
102
103
  }
103
104
  async stop() {
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { tool } from "../tools/index.js";
4
4
  import { LocalExecutionPlane } from "./execution-plane.js";
5
+ import { formatToolError } from "../tools/errors.js";
5
6
  /**
6
7
  * ExecutionPlane that runs subprocesses with a sandbox directory as cwd.
7
8
  * Extends LocalExecutionPlane with two built-in tools:
@@ -73,7 +74,7 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
73
74
  child.stdout.on("data", capture);
74
75
  child.stderr.on("data", capture);
75
76
  child.on("close", code => settle(Buffer.concat(chunks).toString("utf8"), code !== 0));
76
- child.on("error", err => settle(String(err), true));
77
+ child.on("error", err => settle(formatToolError(err), true));
77
78
  });
78
79
  }
79
80
  makeBashTool() {
@@ -1,4 +1,5 @@
1
1
  import { LocalExecutionPlane } from "./execution-plane.js";
2
+ import { formatToolError } from "../tools/errors.js";
2
3
  /**
3
4
  * ExecutionPlane that forwards tool calls over HTTP to a worker inside a customer VPC.
4
5
  *
@@ -76,7 +77,7 @@ export class RemoteVpcPlane {
76
77
  return { output: result.output, isError: result.isError ?? false };
77
78
  }
78
79
  catch (err) {
79
- return { output: String(err), isError: true };
80
+ return { output: formatToolError(err), isError: true };
80
81
  }
81
82
  }
82
83
  }
@@ -36,6 +36,16 @@ export interface TurnMetrics {
36
36
  inputTokens: number;
37
37
  /** Tokens served from the prompt cache this turn (Anthropic `cache_read_input_tokens`). */
38
38
  cacheReadTokens: number;
39
+ /** I1: per-slot attribution of `cacheReadTokens`. Anthropic reports a single cache-read total,
40
+ * not a per-block breakdown — this field is a pro-rata estimate over the slots that actually
41
+ * carried a `cache_control` breakpoint on the request. Missing / empty when the provider does
42
+ * not honor `cache_control` (OpenAI-family auto-cache) or when no breakpoints were placed.
43
+ * Useful for diagnosing which slot is buying the cache hit when comparing strategies. */
44
+ cacheReadTokensBySlot?: {
45
+ system?: number;
46
+ tools?: number;
47
+ messages?: number;
48
+ };
39
49
  /** Tokens written to the prompt cache this turn (Anthropic `cache_creation_input_tokens`). */
40
50
  cacheCreationTokens: number;
41
51
  }
@@ -60,6 +70,17 @@ export interface RuntimeOptions {
60
70
  maxTurns?: number;
61
71
  timeoutMs?: number;
62
72
  agentId?: string;
73
+ /** I4: optional run-start memory pre-fetch hook. The runner calls this ONCE per run, before the
74
+ * first LLM turn, with the request's goal and (optional) run-spec. Each returned query string
75
+ * becomes a `dreamStore.search(agentId, q, 5)` and the resulting hits are paged into the
76
+ * context's knowledge partition before turn 1, so the model sees them on first call. Returning
77
+ * `undefined` / empty array is a no-op. Requires `dreamStore` + `agentId`; missing either ⇒
78
+ * silently skipped (errs-open). Bench memory-recall shows -57% turns / -55% dollars when
79
+ * relevant memories land on turn 1 instead of being discovered via the meta-tool on turn 3+. */
80
+ preQueryMemory?: (ctx: {
81
+ goal: string;
82
+ runSpec?: AgentRunSpec;
83
+ }) => Promise<string[] | undefined> | string[] | undefined;
63
84
  systemPrompt?: string;
64
85
  initialMemory?: string[];
65
86
  skillDir?: string;