@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.
@@ -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) },
@@ -169,10 +170,44 @@ export async function executeTools(calls, registry) {
169
170
  return { callId: c.id, output, isError: false };
170
171
  }
171
172
  catch (err) {
172
- return { callId: c.id, output: String(err), isError: true };
173
+ return { callId: c.id, output: formatToolError(err), isError: true };
173
174
  }
174
175
  }));
175
176
  }
177
+ /**
178
+ * One-shot heuristic: detect when a streaming tool yielded text that *looks* like a failure
179
+ * envelope. The runtime cannot block the tool from doing it, but we warn (once per tool) so
180
+ * the author migrates to throwing — the canonical "streaming tool fails" path. Aligns with
181
+ * the non-streaming tool() / safeTool() contract: failures throw, successes return data.
182
+ */
183
+ const _warnedFailureShapes = new Set();
184
+ export function maybeWarnFailureShapedChunk(toolName, deltaText) {
185
+ if (!deltaText || _warnedFailureShapes.has(toolName))
186
+ return;
187
+ const trimmed = deltaText.trim();
188
+ if (trimmed.length < 2 || trimmed[0] !== "{")
189
+ return;
190
+ let parsed;
191
+ try {
192
+ parsed = JSON.parse(trimmed);
193
+ }
194
+ catch {
195
+ return;
196
+ }
197
+ if (typeof parsed !== "object" || parsed === null)
198
+ return;
199
+ const obj = parsed;
200
+ const looksLikeFailure = obj.success === false ||
201
+ obj.isError === true ||
202
+ obj.is_error === true;
203
+ if (!looksLikeFailure)
204
+ return;
205
+ _warnedFailureShapes.add(toolName);
206
+ console.warn(`[deepstrike] streaming tool "${toolName}" yielded a failure-shaped chunk ` +
207
+ `(success:false / isError:true). Streaming tools should fail by throwing; ` +
208
+ `the runtime will catch and surface the error consistently. ` +
209
+ `Returning a failure-shaped chunk is a foot-gun: the kernel still sees isError:false.`);
210
+ }
176
211
  export const readFile = tool("read_file", "Read the contents of a file.", { type: "object", properties: { path: { type: "string" } }, required: ["path"] }, async ({ path }) => {
177
212
  const { readFile: fsRead } = await import("fs/promises");
178
213
  return fsRead(String(path), "utf8");
package/dist/types.d.ts CHANGED
@@ -84,6 +84,15 @@ export interface UsageEvent extends StreamEvent {
84
84
  cacheReadInputTokens?: number;
85
85
  /** Prompt tokens written to cache this request (billed ~1.25x). Subset of inputTokens. */
86
86
  cacheCreationInputTokens?: number;
87
+ /** I1: per-slot pro-rata attribution of `cacheReadInputTokens`. Estimated, not authoritative —
88
+ * Anthropic returns a single cache-read total, so the SDK divides it evenly across the slots
89
+ * that carried a `cache_control` breakpoint on the request. Missing when the provider doesn't
90
+ * honor `cache_control` (OpenAI-family auto-cache) or when no breakpoints were placed. */
91
+ cacheReadInputTokensBySlot?: {
92
+ system?: number;
93
+ tools?: number;
94
+ messages?: number;
95
+ };
87
96
  }
88
97
  export type ToolChunk = string | {
89
98
  type: "text";
@@ -180,6 +189,16 @@ export interface ToolDeniedEvent extends StreamEvent {
180
189
  toolName: string;
181
190
  reason: string;
182
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
+ }
183
202
  export interface TokenUsage {
184
203
  /** Full prompt size: uncached input + cache reads + cache writes. */
185
204
  inputTokens: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/sdk",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
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.22",
23
+ "@deepstrike/core": "0.2.24",
24
24
  "@google/generative-ai": "^0.24.1",
25
25
  "openai": "^5.23.2"
26
26
  },