@deepstrike/sdk 0.2.23 → 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.
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- 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.js +5 -4
- 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 +67 -5
- package/dist/types.d.ts +10 -0
- package/package.json +2 -2
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";
|
|
@@ -72,7 +74,7 @@ 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
76
|
export type { HarnessRequest, HarnessOutcome, HarnessLoopOptions, QualityGate, CriterionResult, HarnessEvent, VerdictFn } 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";
|
|
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";
|
|
@@ -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.js
CHANGED
|
@@ -15,6 +15,7 @@ import { governancePolicyToKernelEvent, governanceFilterSchema } from "../govern
|
|
|
15
15
|
import { kernelObservationToSessionEvent, withCategory } from "./kernel-event-log.js";
|
|
16
16
|
import { assertNativeProfile } from "./os-profile.js";
|
|
17
17
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
18
|
+
import { formatToolError } from "../tools/errors.js";
|
|
18
19
|
export class RuntimeRunner {
|
|
19
20
|
opts;
|
|
20
21
|
interrupted = false;
|
|
@@ -381,7 +382,7 @@ export class RuntimeRunner {
|
|
|
381
382
|
return ok(reducer(inputs), "completed");
|
|
382
383
|
}
|
|
383
384
|
catch (err) {
|
|
384
|
-
return ok(`reducer "${node.reducer}" threw: ${
|
|
385
|
+
return ok(`reducer "${node.reducer}" threw: ${formatToolError(err)}`, "error");
|
|
385
386
|
}
|
|
386
387
|
}
|
|
387
388
|
/**
|
|
@@ -1114,7 +1115,7 @@ export class RuntimeRunner {
|
|
|
1114
1115
|
if (abortSignal?.aborted) {
|
|
1115
1116
|
this.interrupted = true;
|
|
1116
1117
|
}
|
|
1117
|
-
const errMsg =
|
|
1118
|
+
const errMsg = formatToolError(err).toLowerCase();
|
|
1118
1119
|
if ((errMsg.includes("413") || errMsg.includes("too long") || errMsg.includes("context length exceeded") || errMsg.includes("context_length_exceeded")) &&
|
|
1119
1120
|
!hasAttemptedReactiveCompact) {
|
|
1120
1121
|
hasAttemptedReactiveCompact = true;
|
|
@@ -1124,7 +1125,7 @@ export class RuntimeRunner {
|
|
|
1124
1125
|
}
|
|
1125
1126
|
}
|
|
1126
1127
|
if (!shouldRetry) {
|
|
1127
|
-
yield { type: "error", message:
|
|
1128
|
+
yield { type: "error", message: formatToolError(err) };
|
|
1128
1129
|
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
1129
1130
|
break;
|
|
1130
1131
|
}
|
|
@@ -1405,7 +1406,7 @@ export class RuntimeRunner {
|
|
|
1405
1406
|
// Classify by NAPI status code or message pattern — `invalid_arg` for surface-shape rejects,
|
|
1406
1407
|
// `error` for everything else — then emit run_terminal so observability sees a clean end.
|
|
1407
1408
|
// The yield-error path mirrors what the in-flight provider-stream catch does.
|
|
1408
|
-
const errMsg =
|
|
1409
|
+
const errMsg = formatToolError(err);
|
|
1409
1410
|
const code = err.code;
|
|
1410
1411
|
const isInvalidArg = code === "InvalidArg" ||
|
|
1411
1412
|
errMsg.toLowerCase().includes("invalidarg") ||
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ToolExecContext, RegisteredTool } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Stable JSON shape returned to the model by `safeTool`. The model can branch on `code` instead
|
|
4
|
+
* of pattern-matching a free-form string. `hint` is the self-correcting affordance — a short
|
|
5
|
+
* suggestion ("call document_outline first") that the agent can follow on its own.
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolEnvelopeOk<T = unknown> {
|
|
8
|
+
success: true;
|
|
9
|
+
data?: T;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolEnvelopeFail {
|
|
12
|
+
success: false;
|
|
13
|
+
code: string;
|
|
14
|
+
error: string;
|
|
15
|
+
hint?: string;
|
|
16
|
+
}
|
|
17
|
+
export type ToolEnvelope<T = unknown> = ToolEnvelopeOk<T> | ToolEnvelopeFail;
|
|
18
|
+
/**
|
|
19
|
+
* Error class understood by `safeTool` and by the runtime's error-aware serialization. A throw
|
|
20
|
+
* of `ToolError` produces `{ success:false, code, error, hint? }`; a plain `Error` (or anything
|
|
21
|
+
* with a string `.code`/`.hint`) is honored too, so existing code that already sets `code` on a
|
|
22
|
+
* custom Error keeps working without migration.
|
|
23
|
+
*/
|
|
24
|
+
export declare class ToolError extends Error {
|
|
25
|
+
code: string;
|
|
26
|
+
hint?: string;
|
|
27
|
+
constructor(message: string, opts?: {
|
|
28
|
+
code?: string;
|
|
29
|
+
hint?: string;
|
|
30
|
+
cause?: unknown;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export declare function ok<T>(data?: T): ToolEnvelopeOk<T>;
|
|
34
|
+
export declare function fail(code: string, error: string, hint?: string): ToolEnvelopeFail;
|
|
35
|
+
/**
|
|
36
|
+
* Error-aware serialization for tool-execution error paths. Replaces `String(err)` at the
|
|
37
|
+
* sites that hand the model (or the host's stream) a failure message.
|
|
38
|
+
*
|
|
39
|
+
* - `Error` with no extra fields → `err.message` (clean, no `"Error: "` prefix).
|
|
40
|
+
* - `Error` carrying `code` / `hint` / `cause` → JSON `{message, name?, code?, hint?, cause?}`.
|
|
41
|
+
* - Plain objects → `JSON.stringify(...)` (replaces the old `"[object Object]"`).
|
|
42
|
+
* - Primitives / null / undefined → `String(...)` (unchanged).
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatToolError(err: unknown): string;
|
|
45
|
+
/**
|
|
46
|
+
* `tool()` equivalent that wraps the body in a try/catch and returns a stable
|
|
47
|
+
* `{success, code, error, hint?}` JSON envelope to the model:
|
|
48
|
+
*
|
|
49
|
+
* - body returns plain data → `{success:true, data}`
|
|
50
|
+
* - body returns an envelope (via `ok()`/`fail()`) → passed through
|
|
51
|
+
* - body throws `ToolError` → `{success:false, code, error, hint?}`
|
|
52
|
+
* - body throws any other `Error` → `{success:false, code: error.code ?? "internal", error: error.message}`
|
|
53
|
+
* - body throws a non-Error → `{success:false, code:"internal", error: formatToolError(...)}`
|
|
54
|
+
*
|
|
55
|
+
* The classic `tool()` factory is unchanged. `safeTool` is opt-in: import and switch one tool at
|
|
56
|
+
* a time. Designed for the consumer-side pattern users had to hand-roll to escape the legacy
|
|
57
|
+
* `String(err)` foot-gun.
|
|
58
|
+
*/
|
|
59
|
+
export declare function safeTool<T = unknown>(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown>, ctx?: ToolExecContext) => Promise<ToolEnvelope<T> | T> | ToolEnvelope<T> | T): RegisteredTool;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { tool } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Error class understood by `safeTool` and by the runtime's error-aware serialization. A throw
|
|
4
|
+
* of `ToolError` produces `{ success:false, code, error, hint? }`; a plain `Error` (or anything
|
|
5
|
+
* with a string `.code`/`.hint`) is honored too, so existing code that already sets `code` on a
|
|
6
|
+
* custom Error keeps working without migration.
|
|
7
|
+
*/
|
|
8
|
+
export class ToolError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
hint;
|
|
11
|
+
constructor(message, opts = {}) {
|
|
12
|
+
super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);
|
|
13
|
+
this.name = "ToolError";
|
|
14
|
+
this.code = opts.code ?? "internal";
|
|
15
|
+
if (opts.hint !== undefined)
|
|
16
|
+
this.hint = opts.hint;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function ok(data) {
|
|
20
|
+
return data === undefined ? { success: true } : { success: true, data };
|
|
21
|
+
}
|
|
22
|
+
export function fail(code, error, hint) {
|
|
23
|
+
return hint === undefined ? { success: false, code, error } : { success: false, code, error, hint };
|
|
24
|
+
}
|
|
25
|
+
function isEnvelope(v) {
|
|
26
|
+
return typeof v === "object" && v !== null && "success" in v &&
|
|
27
|
+
typeof v.success === "boolean";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Error-aware serialization for tool-execution error paths. Replaces `String(err)` at the
|
|
31
|
+
* sites that hand the model (or the host's stream) a failure message.
|
|
32
|
+
*
|
|
33
|
+
* - `Error` with no extra fields → `err.message` (clean, no `"Error: "` prefix).
|
|
34
|
+
* - `Error` carrying `code` / `hint` / `cause` → JSON `{message, name?, code?, hint?, cause?}`.
|
|
35
|
+
* - Plain objects → `JSON.stringify(...)` (replaces the old `"[object Object]"`).
|
|
36
|
+
* - Primitives / null / undefined → `String(...)` (unchanged).
|
|
37
|
+
*/
|
|
38
|
+
export function formatToolError(err) {
|
|
39
|
+
if (err == null)
|
|
40
|
+
return String(err);
|
|
41
|
+
if (typeof err === "string")
|
|
42
|
+
return err;
|
|
43
|
+
if (err instanceof Error) {
|
|
44
|
+
const anyErr = err;
|
|
45
|
+
const code = anyErr.code;
|
|
46
|
+
const hint = anyErr.hint;
|
|
47
|
+
const cause = anyErr.cause;
|
|
48
|
+
if (code === undefined && hint === undefined && cause === undefined) {
|
|
49
|
+
return err.message || err.name || "Error";
|
|
50
|
+
}
|
|
51
|
+
const payload = { message: err.message };
|
|
52
|
+
if (err.name && err.name !== "Error")
|
|
53
|
+
payload.name = err.name;
|
|
54
|
+
if (code !== undefined)
|
|
55
|
+
payload.code = code;
|
|
56
|
+
if (hint !== undefined)
|
|
57
|
+
payload.hint = hint;
|
|
58
|
+
if (cause !== undefined)
|
|
59
|
+
payload.cause = cause instanceof Error ? cause.message : cause;
|
|
60
|
+
try {
|
|
61
|
+
return JSON.stringify(payload);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return err.message || err.name || "Error";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (typeof err === "object") {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.stringify(err);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return Object.prototype.toString.call(err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return String(err);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `tool()` equivalent that wraps the body in a try/catch and returns a stable
|
|
79
|
+
* `{success, code, error, hint?}` JSON envelope to the model:
|
|
80
|
+
*
|
|
81
|
+
* - body returns plain data → `{success:true, data}`
|
|
82
|
+
* - body returns an envelope (via `ok()`/`fail()`) → passed through
|
|
83
|
+
* - body throws `ToolError` → `{success:false, code, error, hint?}`
|
|
84
|
+
* - body throws any other `Error` → `{success:false, code: error.code ?? "internal", error: error.message}`
|
|
85
|
+
* - body throws a non-Error → `{success:false, code:"internal", error: formatToolError(...)}`
|
|
86
|
+
*
|
|
87
|
+
* The classic `tool()` factory is unchanged. `safeTool` is opt-in: import and switch one tool at
|
|
88
|
+
* a time. Designed for the consumer-side pattern users had to hand-roll to escape the legacy
|
|
89
|
+
* `String(err)` foot-gun.
|
|
90
|
+
*/
|
|
91
|
+
export function safeTool(name, description, parameters, fn) {
|
|
92
|
+
const wrapped = async (args, ctx) => {
|
|
93
|
+
try {
|
|
94
|
+
const result = await fn(args, ctx);
|
|
95
|
+
if (isEnvelope(result))
|
|
96
|
+
return JSON.stringify(result);
|
|
97
|
+
return JSON.stringify(ok(result));
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (err instanceof ToolError) {
|
|
101
|
+
return JSON.stringify(fail(err.code, err.message || err.name, err.hint));
|
|
102
|
+
}
|
|
103
|
+
if (err instanceof Error) {
|
|
104
|
+
const anyErr = err;
|
|
105
|
+
const code = typeof anyErr.code === "string" ? anyErr.code : "internal";
|
|
106
|
+
const hint = typeof anyErr.hint === "string" ? anyErr.hint : undefined;
|
|
107
|
+
return JSON.stringify(fail(code, err.message || err.name || "Error", hint));
|
|
108
|
+
}
|
|
109
|
+
return JSON.stringify(fail("internal", formatToolError(err)));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return tool(name, description, parameters, wrapped);
|
|
113
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { ToolChunk, ToolSchema, ToolResult } from "../types.js";
|
|
2
2
|
/** M3/G4: the runtime context a tool may read when executing. Carries the working directory the tool
|
|
3
3
|
* should operate in — set to a sub-agent's git worktree for `isolation: "worktree"` nodes. A narrow,
|
|
4
|
-
* dependency-free shape; the execution plane's `RunContext` is structurally assignable to it.
|
|
4
|
+
* dependency-free shape; the execution plane's `RunContext` is structurally assignable to it.
|
|
5
|
+
*
|
|
6
|
+
* `audit` is the "best-effort post-commit side-effect" channel: wrap an audit-log write,
|
|
7
|
+
* metrics emit, or any non-essential persistence in `await ctx.audit(label, () => store.write(...))`.
|
|
8
|
+
* If the side-effect throws, the failure is recorded as a `tool_audit_failed` stream event and
|
|
9
|
+
* the tool still completes successfully — avoiding the foot-gun where a transient audit-store
|
|
10
|
+
* outage flips an already-committed write into `isError: true` and triggers a duplicate retry. */
|
|
5
11
|
export interface ToolExecContext {
|
|
6
12
|
cwd?: string;
|
|
13
|
+
audit?: (label: string, fn: () => Promise<void> | void) => Promise<void>;
|
|
7
14
|
}
|
|
8
15
|
export interface RegisteredTool {
|
|
9
16
|
schema: ToolSchema;
|
|
@@ -23,4 +30,5 @@ export declare function executeTools(calls: {
|
|
|
23
30
|
name: string;
|
|
24
31
|
arguments: string;
|
|
25
32
|
}[], registry: Map<string, RegisteredTool>): Promise<ToolResult[]>;
|
|
33
|
+
export declare function maybeWarnFailureShapedChunk(toolName: string, deltaText: string): void;
|
|
26
34
|
export declare const readFile: RegisteredTool;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatToolError } from "./errors.js";
|
|
1
2
|
export function tool(name, description, parameters, fn) {
|
|
2
3
|
return {
|
|
3
4
|
schema: { name, description, parameters: JSON.stringify(parameters) },
|
|
@@ -36,6 +37,22 @@ export function validateToolArguments(schemaJson, args) {
|
|
|
36
37
|
function validateValue(schema, parent, key, path, state) {
|
|
37
38
|
let value = parent[key];
|
|
38
39
|
const expectedType = schema.type;
|
|
40
|
+
// 0. 多态联合 (oneOf / anyOf) —— 先于单一 type 分支匹配
|
|
41
|
+
const union = (schema.oneOf ?? schema.anyOf);
|
|
42
|
+
if (Array.isArray(union)) {
|
|
43
|
+
for (const sub of union) {
|
|
44
|
+
// 先克隆再试:避免某分支的 auto-cast/裁剪部分改写后又失败,污染后续分支
|
|
45
|
+
const probe = { v: structuredClone(parent[key]) };
|
|
46
|
+
const probeState = { repaired: false };
|
|
47
|
+
if (!validateValue(sub, probe, "v", path, probeState)) {
|
|
48
|
+
parent[key] = probe.v; // 接受首个匹配分支(连同它内部的 repair)
|
|
49
|
+
if (probeState.repaired)
|
|
50
|
+
state.repaired = true;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `${path} does not match any allowed shape`;
|
|
55
|
+
}
|
|
39
56
|
// 1. 类型自动规整 (Auto-cast)
|
|
40
57
|
if (typeof expectedType === "string") {
|
|
41
58
|
if (expectedType === "boolean") {
|
|
@@ -91,14 +108,25 @@ function validateValue(schema, parent, key, path, state) {
|
|
|
91
108
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
92
109
|
return `${path} must be object`;
|
|
93
110
|
const obj = value;
|
|
94
|
-
// 3a. 裁剪多余字段
|
|
111
|
+
// 3a. 裁剪多余字段 —— 尊重 additionalProperties。
|
|
112
|
+
// 缺省/false 维持旧的"裁剪"行为(所有现存工具都依赖它);只有显式 true 或子 schema 才放行。
|
|
95
113
|
const properties = schema.properties ?? {};
|
|
96
114
|
const allowedKeys = new Set(Object.keys(properties));
|
|
115
|
+
const additional = schema.additionalProperties;
|
|
97
116
|
for (const objKey of Object.keys(obj)) {
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
if (allowedKeys.has(objKey))
|
|
118
|
+
continue;
|
|
119
|
+
if (additional === true)
|
|
120
|
+
continue; // 任意键放行:不校验、不裁剪
|
|
121
|
+
if (additional && typeof additional === "object") {
|
|
122
|
+
// 用子 schema 递归校验每个额外键的值(也会 auto-cast / 补默认)
|
|
123
|
+
const err = validateValue(additional, obj, objKey, `${path}.${objKey}`, state);
|
|
124
|
+
if (err)
|
|
125
|
+
return err;
|
|
126
|
+
continue;
|
|
101
127
|
}
|
|
128
|
+
delete obj[objKey]; // additionalProperties 缺省/false → 维持旧行为
|
|
129
|
+
state.repaired = true;
|
|
102
130
|
}
|
|
103
131
|
for (const required of schema.required ?? []) {
|
|
104
132
|
if (!(required in obj))
|
|
@@ -169,10 +197,44 @@ export async function executeTools(calls, registry) {
|
|
|
169
197
|
return { callId: c.id, output, isError: false };
|
|
170
198
|
}
|
|
171
199
|
catch (err) {
|
|
172
|
-
return { callId: c.id, output:
|
|
200
|
+
return { callId: c.id, output: formatToolError(err), isError: true };
|
|
173
201
|
}
|
|
174
202
|
}));
|
|
175
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* One-shot heuristic: detect when a streaming tool yielded text that *looks* like a failure
|
|
206
|
+
* envelope. The runtime cannot block the tool from doing it, but we warn (once per tool) so
|
|
207
|
+
* the author migrates to throwing — the canonical "streaming tool fails" path. Aligns with
|
|
208
|
+
* the non-streaming tool() / safeTool() contract: failures throw, successes return data.
|
|
209
|
+
*/
|
|
210
|
+
const _warnedFailureShapes = new Set();
|
|
211
|
+
export function maybeWarnFailureShapedChunk(toolName, deltaText) {
|
|
212
|
+
if (!deltaText || _warnedFailureShapes.has(toolName))
|
|
213
|
+
return;
|
|
214
|
+
const trimmed = deltaText.trim();
|
|
215
|
+
if (trimmed.length < 2 || trimmed[0] !== "{")
|
|
216
|
+
return;
|
|
217
|
+
let parsed;
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(trimmed);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
225
|
+
return;
|
|
226
|
+
const obj = parsed;
|
|
227
|
+
const looksLikeFailure = obj.success === false ||
|
|
228
|
+
obj.isError === true ||
|
|
229
|
+
obj.is_error === true;
|
|
230
|
+
if (!looksLikeFailure)
|
|
231
|
+
return;
|
|
232
|
+
_warnedFailureShapes.add(toolName);
|
|
233
|
+
console.warn(`[deepstrike] streaming tool "${toolName}" yielded a failure-shaped chunk ` +
|
|
234
|
+
`(success:false / isError:true). Streaming tools should fail by throwing; ` +
|
|
235
|
+
`the runtime will catch and surface the error consistently. ` +
|
|
236
|
+
`Returning a failure-shaped chunk is a foot-gun: the kernel still sees isError:false.`);
|
|
237
|
+
}
|
|
176
238
|
export const readFile = tool("read_file", "Read the contents of a file.", { type: "object", properties: { path: { type: "string" } }, required: ["path"] }, async ({ path }) => {
|
|
177
239
|
const { readFile: fsRead } = await import("fs/promises");
|
|
178
240
|
return fsRead(String(path), "utf8");
|
package/dist/types.d.ts
CHANGED
|
@@ -189,6 +189,16 @@ export interface ToolDeniedEvent extends StreamEvent {
|
|
|
189
189
|
toolName: string;
|
|
190
190
|
reason: string;
|
|
191
191
|
}
|
|
192
|
+
/** A tool's `ctx.audit(label, fn)` best-effort side-effect threw. The tool itself completed
|
|
193
|
+
* successfully (no isError flip, no retry); this event lets the host log / monitor that an
|
|
194
|
+
* audit-store / metrics-emit / non-essential persistence step failed. */
|
|
195
|
+
export interface ToolAuditFailedEvent extends StreamEvent {
|
|
196
|
+
type: "tool_audit_failed";
|
|
197
|
+
callId: string;
|
|
198
|
+
name: string;
|
|
199
|
+
label: string;
|
|
200
|
+
error: string;
|
|
201
|
+
}
|
|
192
202
|
export interface TokenUsage {
|
|
193
203
|
/** Full prompt size: uncached input + cache reads + cache writes. */
|
|
194
204
|
inputTokens: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.25",
|
|
4
4
|
"description": "DeepStrike Node.js SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@anthropic-ai/sdk": "^0.99.0",
|
|
23
|
-
"@deepstrike/core": "0.2.
|
|
23
|
+
"@deepstrike/core": "0.2.25",
|
|
24
24
|
"@google/generative-ai": "^0.24.1",
|
|
25
25
|
"openai": "^5.23.2"
|
|
26
26
|
},
|