@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.
- package/dist/governance.d.ts +21 -0
- package/dist/governance.js +32 -0
- package/dist/harness/harness.d.ts +30 -0
- package/dist/harness/harness.js +40 -20
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -0
- package/dist/providers/anthropic.js +60 -1
- package/dist/runtime/execution-plane.js +32 -4
- package/dist/runtime/mcp-proxy-plane.js +2 -1
- package/dist/runtime/process-sandbox-plane.js +2 -1
- package/dist/runtime/remote-vpc-plane.js +2 -1
- package/dist/runtime/runner.d.ts +21 -0
- package/dist/runtime/runner.js +425 -333
- package/dist/tools/errors.d.ts +59 -0
- package/dist/tools/errors.js +113 -0
- package/dist/tools/index.d.ts +9 -1
- package/dist/tools/index.js +36 -1
- package/dist/types.d.ts +19 -0
- package/package.json +2 -2
package/dist/governance.d.ts
CHANGED
|
@@ -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;
|
package/dist/governance.js
CHANGED
|
@@ -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>;
|
package/dist/harness/harness.js
CHANGED
|
@@ -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
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
let
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 (
|
|
149
|
-
const { name, description, whenToUse, content } =
|
|
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
|
-
...(
|
|
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
|
-
|
|
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:
|
|
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: ${
|
|
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:
|
|
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(
|
|
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:
|
|
80
|
+
return { output: formatToolError(err), isError: true };
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
}
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -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;
|