@deepstrike/sdk 0.2.43 → 0.2.45
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/runtime/execution-plane.js +7 -5
- package/dist/runtime/kernel-step.js +5 -2
- package/dist/runtime/large-result-spool.d.ts +2 -2
- package/dist/runtime/large-result-spool.js +9 -6
- package/dist/runtime/runner.js +2 -2
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +80 -3
- package/package.json +2 -2
|
@@ -124,9 +124,11 @@ export class LocalExecutionPlane {
|
|
|
124
124
|
},
|
|
125
125
|
};
|
|
126
126
|
try {
|
|
127
|
-
const
|
|
128
|
-
const originalArgsStr = JSON.stringify(
|
|
129
|
-
|
|
127
|
+
const rawArgs = JSON.parse(call.arguments || "{}");
|
|
128
|
+
const originalArgsStr = JSON.stringify(rawArgs);
|
|
129
|
+
// validation.args, not rawArgs, from here on: a oneOf/anyOf ROOT accepts a repaired probe
|
|
130
|
+
// CLONE — the original reference never sees those repairs (auto-casts, strips, defaults).
|
|
131
|
+
const validation = validateToolArguments(registered.schema.parameters, rawArgs);
|
|
130
132
|
if (validation.error)
|
|
131
133
|
return { callId: call.id, output: `invalid arguments: ${validation.error}`, isError: true };
|
|
132
134
|
if (validation.repaired) {
|
|
@@ -135,13 +137,13 @@ export class LocalExecutionPlane {
|
|
|
135
137
|
callId: call.id,
|
|
136
138
|
name: call.name,
|
|
137
139
|
originalArguments: originalArgsStr,
|
|
138
|
-
repairedArguments: JSON.stringify(args),
|
|
140
|
+
repairedArguments: JSON.stringify(validation.args),
|
|
139
141
|
};
|
|
140
142
|
}
|
|
141
143
|
// M3/G4: pass the run context (incl. `cwd`) so cwd-aware tools scope their work to the
|
|
142
144
|
// sub-agent's worktree. `RunContext` is structurally assignable to the tool's `ToolExecContext`.
|
|
143
145
|
// The per-call `audit` helper (above) layers best-effort side-effect handling on top.
|
|
144
|
-
const output = await registered.execute(args, callCtx);
|
|
146
|
+
const output = await registered.execute(validation.args, callCtx);
|
|
145
147
|
if (isAsyncIterable(output)) {
|
|
146
148
|
let combined = "";
|
|
147
149
|
const iterator = output[Symbol.asyncIterator]();
|
|
@@ -19,7 +19,6 @@ export function restoreKernelRuntime(runtime, snapshot) {
|
|
|
19
19
|
}, 1);
|
|
20
20
|
kernelWireStates.set(runtime, { operationId, nextEventSequence });
|
|
21
21
|
}
|
|
22
|
-
let nextOperationSequence = 1;
|
|
23
22
|
const kernelWireStates = new WeakMap();
|
|
24
23
|
function tryParseJson(s) {
|
|
25
24
|
try {
|
|
@@ -370,8 +369,12 @@ function mapKernelAction(raw) {
|
|
|
370
369
|
function stepInput(runtime, event) {
|
|
371
370
|
let state = kernelWireStates.get(runtime);
|
|
372
371
|
if (!state) {
|
|
372
|
+
// Globally unique, never a process-local counter: durable session logs key the kernel
|
|
373
|
+
// genesis/transaction chains by (sessionId, operationId) and outlive this process, so a
|
|
374
|
+
// counter that restarts at 1 collides with yesterday's chain on the same session (genesis
|
|
375
|
+
// digest conflict, or step_seq successor violation when the policy digest happens to match).
|
|
373
376
|
state = {
|
|
374
|
-
operationId: `node-operation-${
|
|
377
|
+
operationId: `node-operation-${crypto.randomUUID()}`,
|
|
375
378
|
nextEventSequence: 1,
|
|
376
379
|
};
|
|
377
380
|
kernelWireStates.set(runtime, state);
|
|
@@ -74,7 +74,7 @@ export declare class LargeResultSpool {
|
|
|
74
74
|
/**
|
|
75
75
|
* Persist a kernel-spooled tool output to disk. Returns the on-disk path ref.
|
|
76
76
|
*/
|
|
77
|
-
persistOutput(callId: string, content: string): Promise<string>;
|
|
77
|
+
persistOutput(sessionId: string, callId: string, content: string): Promise<string>;
|
|
78
78
|
/**
|
|
79
79
|
* Read a spooled result back from disk.
|
|
80
80
|
*/
|
|
@@ -85,7 +85,7 @@ export declare class LargeResultSpool {
|
|
|
85
85
|
* for the hashed call-key prefix; returns `undefined` if nothing was ever spooled
|
|
86
86
|
* for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
|
|
87
87
|
*/
|
|
88
|
-
findByCallId(callId: string): Promise<string | undefined>;
|
|
88
|
+
findByCallId(sessionId: string, callId: string): Promise<string | undefined>;
|
|
89
89
|
/**
|
|
90
90
|
* Clean up old spool files (optional maintenance).
|
|
91
91
|
*/
|
|
@@ -46,8 +46,11 @@ export class LargeResultSpool {
|
|
|
46
46
|
getSpoolPath(hash) {
|
|
47
47
|
return path.join(this.spoolDir, `${hash}.txt`);
|
|
48
48
|
}
|
|
49
|
-
callKey(callId) {
|
|
50
|
-
|
|
49
|
+
callKey(sessionId, callId) {
|
|
50
|
+
// Session-scoped: the spool dir is shared across sessions and outlives runs, while vendor
|
|
51
|
+
// call ids can be index-style ("call_0") and repeat — an unscoped key lets read_result in
|
|
52
|
+
// one session fetch another session's spooled output.
|
|
53
|
+
return this.hashContent(`${sessionId}\u0000${callId}`).slice(0, 32);
|
|
51
54
|
}
|
|
52
55
|
async atomicWrite(spoolPath, content) {
|
|
53
56
|
const tempPath = `${spoolPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
@@ -128,9 +131,9 @@ omitted: ${omitted} chars
|
|
|
128
131
|
/**
|
|
129
132
|
* Persist a kernel-spooled tool output to disk. Returns the on-disk path ref.
|
|
130
133
|
*/
|
|
131
|
-
async persistOutput(callId, content) {
|
|
134
|
+
async persistOutput(sessionId, callId, content) {
|
|
132
135
|
const hash = this.hashContent(content);
|
|
133
|
-
const spoolPath = this.getSpoolPath(`${this.callKey(callId)}-${hash.slice(0, 16)}`);
|
|
136
|
+
const spoolPath = this.getSpoolPath(`${this.callKey(sessionId, callId)}-${hash.slice(0, 16)}`);
|
|
134
137
|
let promise = this.activeWrites.get(spoolPath);
|
|
135
138
|
if (!promise) {
|
|
136
139
|
promise = (async () => {
|
|
@@ -165,7 +168,7 @@ omitted: ${omitted} chars
|
|
|
165
168
|
* for the hashed call-key prefix; returns `undefined` if nothing was ever spooled
|
|
166
169
|
* for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
|
|
167
170
|
*/
|
|
168
|
-
async findByCallId(callId) {
|
|
171
|
+
async findByCallId(sessionId, callId) {
|
|
169
172
|
let files;
|
|
170
173
|
try {
|
|
171
174
|
files = await fs.readdir(this.spoolDir);
|
|
@@ -173,7 +176,7 @@ omitted: ${omitted} chars
|
|
|
173
176
|
catch {
|
|
174
177
|
return undefined;
|
|
175
178
|
}
|
|
176
|
-
const prefix = `${this.callKey(callId)}-`;
|
|
179
|
+
const prefix = `${this.callKey(sessionId, callId)}-`;
|
|
177
180
|
const match = files.find(f => f.startsWith(prefix) && f.endsWith('.txt'));
|
|
178
181
|
if (!match)
|
|
179
182
|
return undefined;
|
package/dist/runtime/runner.js
CHANGED
|
@@ -1340,7 +1340,7 @@ export class RuntimeRunner {
|
|
|
1340
1340
|
let full;
|
|
1341
1341
|
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
1342
1342
|
try {
|
|
1343
|
-
full = await spool.findByCallId(callId);
|
|
1343
|
+
full = await spool.findByCallId(sessionId, callId);
|
|
1344
1344
|
}
|
|
1345
1345
|
catch {
|
|
1346
1346
|
full = undefined;
|
|
@@ -1856,7 +1856,7 @@ export class RuntimeRunner {
|
|
|
1856
1856
|
let spoolRef;
|
|
1857
1857
|
let error;
|
|
1858
1858
|
try {
|
|
1859
|
-
spoolRef = await spool.persistOutput(action.callId, action.output);
|
|
1859
|
+
spoolRef = await spool.persistOutput(sessionId, action.callId, action.output);
|
|
1860
1860
|
}
|
|
1861
1861
|
catch (cause) {
|
|
1862
1862
|
error = formatToolError(cause);
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare function toolChunkText(chunk: ToolChunk): string;
|
|
|
26
26
|
export declare function validateToolArguments(schemaJson: string, args: Record<string, unknown>): {
|
|
27
27
|
error?: string;
|
|
28
28
|
repaired: boolean;
|
|
29
|
+
args: Record<string, unknown>;
|
|
29
30
|
};
|
|
30
31
|
export declare function executeTools(calls: {
|
|
31
32
|
id: string;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { formatToolError } from "./errors.js";
|
|
2
|
+
/** Fail at registration, not as a vendor 400 at call time: every major provider (OpenAI-compat,
|
|
3
|
+
* Anthropic, Gemini) rejects a tool whose parameters root is not `type: "object"` — the wire
|
|
4
|
+
* error ("schema must be a JSON Schema of 'type: \"object\"'") surfaces far from the tool that
|
|
5
|
+
* caused it. Union roots must be wrapped: object root + flattened properties + `oneOf` sibling. */
|
|
6
|
+
function assertObjectRootSchema(name, parameters) {
|
|
7
|
+
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters) || parameters.type !== "object") {
|
|
8
|
+
throw new Error(`tool "${name}": parameters must be a JSON Schema with root type "object" `
|
|
9
|
+
+ `(got type: ${JSON.stringify(parameters?.type ?? null)}); `
|
|
10
|
+
+ `providers reject any other root — wrap union variants as an object root with a oneOf sibling`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
2
13
|
export function tool(name, description, parameters, fn) {
|
|
14
|
+
assertObjectRootSchema(name, parameters);
|
|
3
15
|
return {
|
|
4
16
|
schema: { name, description, parameters: JSON.stringify(parameters) },
|
|
5
17
|
async execute(args, ctx) { return fn(args, ctx); },
|
|
6
18
|
};
|
|
7
19
|
}
|
|
8
20
|
export function streamingTool(name, description, parameters, fn) {
|
|
21
|
+
assertObjectRootSchema(name, parameters);
|
|
9
22
|
return {
|
|
10
23
|
schema: { name, description, parameters: JSON.stringify(parameters) },
|
|
11
24
|
execute(args, ctx) { return fn(args, ctx); },
|
|
@@ -27,12 +40,14 @@ export function validateToolArguments(schemaJson, args) {
|
|
|
27
40
|
schema = JSON.parse(schemaJson);
|
|
28
41
|
}
|
|
29
42
|
catch {
|
|
30
|
-
return { error: "invalid tool schema", repaired: false };
|
|
43
|
+
return { error: "invalid tool schema", repaired: false, args };
|
|
31
44
|
}
|
|
32
45
|
const state = { repaired: false };
|
|
33
46
|
const wrapper = { root: args };
|
|
34
47
|
const error = validateValue(schema, wrapper, "root", "$", state);
|
|
35
|
-
|
|
48
|
+
// A oneOf/anyOf ROOT replaces the value with its accepted probe clone — in-place mutation of
|
|
49
|
+
// the caller's object only covers non-union roots. Callers must use the returned `args`.
|
|
50
|
+
return { error, repaired: state.repaired, args: wrapper.root };
|
|
36
51
|
}
|
|
37
52
|
function validateValue(schema, parent, key, path, state) {
|
|
38
53
|
let value = parent[key];
|
|
@@ -188,12 +203,72 @@ function validateValue(schema, parent, key, path, state) {
|
|
|
188
203
|
if (typeof value !== "boolean")
|
|
189
204
|
return `${path} must be boolean`;
|
|
190
205
|
}
|
|
206
|
+
else if (expectedType === "null") {
|
|
207
|
+
if (value !== null)
|
|
208
|
+
return `${path} must be null`;
|
|
209
|
+
}
|
|
191
210
|
}
|
|
192
211
|
else if (path === "$" && (!value || typeof value !== "object" || Array.isArray(value))) {
|
|
193
212
|
return `${path} must be object`;
|
|
194
213
|
}
|
|
195
214
|
if (Array.isArray(schema.enum) && !schema.enum.includes(value))
|
|
196
215
|
return `${path} must be one of enum values`;
|
|
216
|
+
// `const` is THE discriminator convention for oneOf variants (kind: {const: "edit"}). Without
|
|
217
|
+
// it, union branches match on required+type alone and the WRONG branch can win — then its
|
|
218
|
+
// allow-list strips keys the right branch declared.
|
|
219
|
+
if ("const" in schema) {
|
|
220
|
+
const want = schema.const;
|
|
221
|
+
const matches = want !== null && typeof want === "object"
|
|
222
|
+
? JSON.stringify(value) === JSON.stringify(want)
|
|
223
|
+
: value === want;
|
|
224
|
+
if (!matches)
|
|
225
|
+
return `${path} must equal the const value ${JSON.stringify(want)}`;
|
|
226
|
+
}
|
|
227
|
+
// Constraint keywords, checked per the value's actual type (JSON Schema semantics: string
|
|
228
|
+
// constraints ignore non-strings, etc.). Keywords outside this set (allOf, multipleOf,
|
|
229
|
+
// uniqueItems, format, if/then/else, …) are ignored, not rejected.
|
|
230
|
+
if (typeof value === "string") {
|
|
231
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
232
|
+
return `${path} must be at least ${schema.minLength} characters`;
|
|
233
|
+
}
|
|
234
|
+
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
|
|
235
|
+
return `${path} must be at most ${schema.maxLength} characters`;
|
|
236
|
+
}
|
|
237
|
+
if (typeof schema.pattern === "string") {
|
|
238
|
+
let re;
|
|
239
|
+
try {
|
|
240
|
+
re = new RegExp(schema.pattern);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
re = undefined;
|
|
244
|
+
} // author-side bad regex: skip, never fail the call
|
|
245
|
+
if (re && !re.test(value))
|
|
246
|
+
return `${path} must match pattern ${schema.pattern}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (typeof value === "number") {
|
|
250
|
+
if (typeof schema.minimum === "number" && value < schema.minimum)
|
|
251
|
+
return `${path} must be >= ${schema.minimum}`;
|
|
252
|
+
if (typeof schema.maximum === "number" && value > schema.maximum)
|
|
253
|
+
return `${path} must be <= ${schema.maximum}`;
|
|
254
|
+
if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum)
|
|
255
|
+
return `${path} must be > ${schema.exclusiveMinimum}`;
|
|
256
|
+
if (typeof schema.exclusiveMaximum === "number" && value >= schema.exclusiveMaximum)
|
|
257
|
+
return `${path} must be < ${schema.exclusiveMaximum}`;
|
|
258
|
+
}
|
|
259
|
+
else if (Array.isArray(value)) {
|
|
260
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems)
|
|
261
|
+
return `${path} must have at least ${schema.minItems} items`;
|
|
262
|
+
if (typeof schema.maxItems === "number" && value.length > schema.maxItems)
|
|
263
|
+
return `${path} must have at most ${schema.maxItems} items`;
|
|
264
|
+
}
|
|
265
|
+
// `not`: probe on a clone so a matching (= rejected) subschema's repairs never leak out.
|
|
266
|
+
if (schema.not && typeof schema.not === "object" && !Array.isArray(schema.not)) {
|
|
267
|
+
const probe = { v: structuredClone(value) };
|
|
268
|
+
if (!validateValue(schema.not, probe, "v", path, { repaired: false })) {
|
|
269
|
+
return `${path} must not match the disallowed shape`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
197
272
|
return undefined;
|
|
198
273
|
}
|
|
199
274
|
export async function executeTools(calls, registry) {
|
|
@@ -206,7 +281,9 @@ export async function executeTools(calls, registry) {
|
|
|
206
281
|
const validation = validateToolArguments(t.schema.parameters, args);
|
|
207
282
|
if (validation.error)
|
|
208
283
|
return { callId: c.id, output: `invalid arguments: ${validation.error}`, isError: true };
|
|
209
|
-
|
|
284
|
+
// validation.args, not args: a oneOf/anyOf ROOT accepts a repaired probe CLONE — the
|
|
285
|
+
// original reference never sees those repairs (auto-casts, strips, defaults).
|
|
286
|
+
const output = await t.execute(validation.args);
|
|
210
287
|
if (isAsyncIterable(output)) {
|
|
211
288
|
let combined = "";
|
|
212
289
|
for await (const chunk of output)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.45",
|
|
4
4
|
"description": "DeepStrike Node.js SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@anthropic-ai/sdk": "^0.99.0",
|
|
75
|
-
"@deepstrike/core": "0.2.
|
|
75
|
+
"@deepstrike/core": "0.2.45",
|
|
76
76
|
"@google/generative-ai": "^0.24.1",
|
|
77
77
|
"openai": "^5.23.2"
|
|
78
78
|
},
|