@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 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
- 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
  }
@@ -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: ${err instanceof Error ? err.message : String(err)}`, "error");
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 = String(err).toLowerCase();
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: String(err) };
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 = err instanceof Error ? err.message : String(err);
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
+ }
@@ -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;
@@ -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 (!allowedKeys.has(objKey)) {
99
- delete obj[objKey];
100
- state.repaired = true;
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: String(err), isError: true };
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.23",
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",
23
+ "@deepstrike/core": "0.2.25",
24
24
  "@google/generative-ai": "^0.24.1",
25
25
  "openai": "^5.23.2"
26
26
  },