@deepstrike/sdk 0.2.42 → 0.2.44

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.
@@ -1,5 +1,6 @@
1
1
  import type { RuntimeRunner } from "../runtime/runner.js";
2
2
  import type { SessionEvent } from "../runtime/session-log.js";
3
+ import type { ContentPart } from "../types.js";
3
4
  import type { WorkflowNodeSpec } from "../types/agent.js";
4
5
  import type { Criterion, Verdict } from "../runtime/eval.js";
5
6
  import type { AttemptJudge, JudgeResult } from "./judge.js";
@@ -8,6 +9,12 @@ export interface AttemptRequest {
8
9
  sessionId?: string;
9
10
  goal: string;
10
11
  criteria?: Criterion[];
12
+ /**
13
+ * Multimodal inputs (images / audio) attached to the task. Forwarded to every attempt
14
+ * unconditionally; the runner seeds them per session idempotently, so fresh-session carries
15
+ * re-seed while same-session carries do not double.
16
+ */
17
+ attachments?: ContentPart[];
11
18
  extensions?: Record<string, unknown>;
12
19
  /** Parent transcript inherited by the first attempt only. */
13
20
  inheritEvents?: Array<{
@@ -14,6 +14,7 @@ export class RuntimeAttemptBody {
14
14
  sessionId: context.sessionId,
15
15
  goal: context.goal,
16
16
  criteria: (context.criteria ?? []).map(criterion => criterion.text),
17
+ ...(context.attachments?.length ? { attachments: context.attachments } : {}),
17
18
  extensions: context.extensions,
18
19
  ...(context.attempt === 1 && context.inheritEvents
19
20
  ? { inheritEvents: context.inheritEvents }
@@ -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-${nextOperationSequence++}`,
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
- return this.hashContent(callId).slice(0, 32);
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;
@@ -1208,6 +1208,12 @@ export class RuntimeRunner {
1208
1208
  const runId = midRun && resumedStart?.event.kind === "run_started"
1209
1209
  ? resumedStart.event.run_id
1210
1210
  : crypto.randomUUID();
1211
+ // Idempotent per session: an earlier run's `run_started` already carries these attachments
1212
+ // (same-session retry attempt), so replay reconstructs them — recording and seeding again
1213
+ // would double them in history. Deduping at the append keeps live and replay in agreement.
1214
+ const attachments = req.attachments?.length && !attachmentsAlreadySeeded(prior, req.attachments)
1215
+ ? req.attachments
1216
+ : undefined;
1211
1217
  if (!midRun) {
1212
1218
  await this.opts.sessionLog.append(req.sessionId, {
1213
1219
  kind: "run_started",
@@ -1216,10 +1222,10 @@ export class RuntimeRunner {
1216
1222
  criteria: req.criteria ?? [],
1217
1223
  agent_id: this.opts.agentId,
1218
1224
  system_prompt: this.opts.systemPrompt,
1219
- ...(req.attachments?.length ? { attachments: req.attachments } : {}),
1225
+ ...(attachments ? { attachments } : {}),
1220
1226
  });
1221
1227
  }
1222
- yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, req.attachments, runId);
1228
+ yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, attachments, runId);
1223
1229
  }
1224
1230
  async *wake(sessionId, extensions) {
1225
1231
  const events = await this.opts.sessionLog.read(sessionId);
@@ -1334,7 +1340,7 @@ export class RuntimeRunner {
1334
1340
  let full;
1335
1341
  const spool = this.opts.resultSpool ?? new LargeResultSpool();
1336
1342
  try {
1337
- full = await spool.findByCallId(callId);
1343
+ full = await spool.findByCallId(sessionId, callId);
1338
1344
  }
1339
1345
  catch {
1340
1346
  full = undefined;
@@ -1850,7 +1856,7 @@ export class RuntimeRunner {
1850
1856
  let spoolRef;
1851
1857
  let error;
1852
1858
  try {
1853
- spoolRef = await spool.persistOutput(action.callId, action.output);
1859
+ spoolRef = await spool.persistOutput(sessionId, action.callId, action.output);
1854
1860
  }
1855
1861
  catch (cause) {
1856
1862
  error = formatToolError(cause);
@@ -2467,6 +2473,15 @@ function isMidRun(events) {
2467
2473
  }
2468
2474
  return lastStarted >= 0 && lastStarted > lastTerminal;
2469
2475
  }
2476
+ /**
2477
+ * True when an earlier run in this session already seeded the same attachments. Replay
2478
+ * reconstructs the attachment message from that run's `run_started`, so recording and
2479
+ * live-seeding them again (a same-session retry attempt) would double them in history.
2480
+ */
2481
+ function attachmentsAlreadySeeded(prior, attachments) {
2482
+ const wanted = JSON.stringify(attachments);
2483
+ return prior.some(({ event }) => event.kind === "run_started" && JSON.stringify(event.attachments ?? []) === wanted);
2484
+ }
2470
2485
  /**
2471
2486
  * Build a kernel `add_history_message` payload from user attachments: a `user`
2472
2487
  * message whose content is the multimodal parts in the kernel's serde shape
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/sdk",
3
- "version": "0.2.42",
3
+ "version": "0.2.44",
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.42",
75
+ "@deepstrike/core": "0.2.44",
76
76
  "@google/generative-ai": "^0.24.1",
77
77
  "openai": "^5.23.2"
78
78
  },