zeitlich 0.2.48 → 0.2.50

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.
Files changed (126) hide show
  1. package/README.md +26 -23
  2. package/dist/{activities-DCaIPQBT.d.ts → activities-IuOIvPHO.d.ts} +6 -6
  3. package/dist/{activities-BlQR5gX4.d.cts → activities-cIlq1y1y.d.cts} +6 -6
  4. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  5. package/dist/adapters/sandbox/daytona/index.d.cts +3 -3
  6. package/dist/adapters/sandbox/daytona/index.d.ts +3 -3
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  8. package/dist/adapters/sandbox/daytona/workflow.d.cts +2 -2
  9. package/dist/adapters/sandbox/daytona/workflow.d.ts +2 -2
  10. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  11. package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
  12. package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
  13. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  14. package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
  15. package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
  16. package/dist/adapters/thread/anthropic/index.cjs +45 -42
  17. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  18. package/dist/adapters/thread/anthropic/index.d.cts +10 -10
  19. package/dist/adapters/thread/anthropic/index.d.ts +10 -10
  20. package/dist/adapters/thread/anthropic/index.js +45 -42
  21. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  22. package/dist/adapters/thread/anthropic/workflow.d.cts +7 -7
  23. package/dist/adapters/thread/anthropic/workflow.d.ts +7 -7
  24. package/dist/adapters/thread/google-genai/index.cjs +117 -54
  25. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  26. package/dist/adapters/thread/google-genai/index.d.cts +27 -23
  27. package/dist/adapters/thread/google-genai/index.d.ts +27 -23
  28. package/dist/adapters/thread/google-genai/index.js +117 -54
  29. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  30. package/dist/adapters/thread/google-genai/workflow.d.cts +8 -8
  31. package/dist/adapters/thread/google-genai/workflow.d.ts +8 -8
  32. package/dist/adapters/thread/langchain/index.cjs +45 -42
  33. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  34. package/dist/adapters/thread/langchain/index.d.cts +10 -10
  35. package/dist/adapters/thread/langchain/index.d.ts +10 -10
  36. package/dist/adapters/thread/langchain/index.js +45 -42
  37. package/dist/adapters/thread/langchain/index.js.map +1 -1
  38. package/dist/adapters/thread/langchain/workflow.d.cts +7 -7
  39. package/dist/adapters/thread/langchain/workflow.d.ts +7 -7
  40. package/dist/{cold-store-UL13Sstw.d.cts → cold-store-C0uvYTSi.d.cts} +1 -1
  41. package/dist/{cold-store-aD4TSKlU.d.ts → cold-store-CCnZYWjx.d.ts} +1 -1
  42. package/dist/index.cjs +15063 -405
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +79 -83
  45. package/dist/index.d.ts +79 -83
  46. package/dist/index.js +15064 -402
  47. package/dist/index.js.map +1 -1
  48. package/dist/{proxy-BAty3CWM.d.cts → proxy-BVznA2_p.d.cts} +1 -1
  49. package/dist/{proxy-mbnwBhHw.d.ts → proxy-C4J1pNUk.d.ts} +1 -1
  50. package/dist/{thread-manager-CICj68PI.d.ts → thread-manager-BqjzWsP7.d.ts} +4 -4
  51. package/dist/{thread-manager-R6c3lnJy.d.cts → thread-manager-CzIs47uG.d.cts} +4 -4
  52. package/dist/{thread-manager-DsXvJ5cJ.d.cts → thread-manager-Dzl1fHhV.d.cts} +4 -4
  53. package/dist/{thread-manager-DtEtbUkp.d.ts → thread-manager-SkSWRPRc.d.ts} +4 -4
  54. package/dist/{types-gVa5XCWD.d.ts → types-BQvXWcft.d.ts} +1 -1
  55. package/dist/{types-DF4wzWQG.d.ts → types-CbPnU4RM.d.ts} +3 -3
  56. package/dist/{types-CJ7tCdl6.d.cts → types-D8W5TnSa.d.cts} +3 -3
  57. package/dist/{types-CJ7tCdl6.d.ts → types-D8W5TnSa.d.ts} +3 -3
  58. package/dist/{types-DwBYd0ij.d.ts → types-DZnUqCAP.d.cts} +709 -686
  59. package/dist/{types-CjY93AWZ.d.cts → types-OEN1xrFg.d.cts} +1 -1
  60. package/dist/{types-DWeyCTYK.d.cts → types-YNesmGKV.d.ts} +709 -686
  61. package/dist/{types-DDLPnxBh.d.cts → types-d2RvEP6v.d.cts} +3 -3
  62. package/dist/{workflow-DdaU7_j4.d.ts → workflow-B3oTe2_D.d.cts} +34 -3
  63. package/dist/{workflow-DVNPR7eX.d.cts → workflow-Bkzg0cjB.d.ts} +34 -3
  64. package/dist/workflow.cjs +15021 -362
  65. package/dist/workflow.cjs.map +1 -1
  66. package/dist/workflow.d.cts +3 -3
  67. package/dist/workflow.d.ts +3 -3
  68. package/dist/workflow.js +15022 -359
  69. package/dist/workflow.js.map +1 -1
  70. package/package.json +10 -37
  71. package/src/adapters/thread/anthropic/activities.ts +1 -1
  72. package/src/adapters/thread/anthropic/fork-transform.test.ts +17 -11
  73. package/src/adapters/thread/anthropic/model-invoker.test.ts +4 -3
  74. package/src/adapters/thread/anthropic/model-invoker.ts +1 -1
  75. package/src/adapters/thread/anthropic/thread-manager.test.ts +2 -2
  76. package/src/adapters/thread/anthropic/thread-manager.ts +1 -1
  77. package/src/adapters/thread/google-genai/activities.ts +1 -1
  78. package/src/adapters/thread/google-genai/fork-transform.test.ts +17 -11
  79. package/src/adapters/thread/google-genai/model-invoker.test.ts +337 -0
  80. package/src/adapters/thread/google-genai/model-invoker.ts +107 -23
  81. package/src/adapters/thread/google-genai/thread-manager.test.ts +2 -2
  82. package/src/adapters/thread/google-genai/thread-manager.ts +1 -1
  83. package/src/adapters/thread/langchain/activities.ts +1 -1
  84. package/src/adapters/thread/langchain/fork-transform.test.ts +17 -11
  85. package/src/adapters/thread/langchain/model-invoker.ts +1 -1
  86. package/src/adapters/thread/langchain/thread-manager.test.ts +2 -2
  87. package/src/adapters/thread/langchain/thread-manager.ts +1 -1
  88. package/src/index.ts +2 -2
  89. package/src/lib/sandbox/capability-types.test.ts +2 -2
  90. package/src/lib/sandbox/manager.ts +2 -6
  91. package/src/lib/sandbox/sandbox.test.ts +1 -1
  92. package/src/lib/sandbox/types.ts +2 -2
  93. package/src/lib/session/session.integration.test.ts +92 -0
  94. package/src/lib/session/session.ts +23 -0
  95. package/src/lib/subagent/handler.ts +23 -0
  96. package/src/lib/subagent/subagent.integration.test.ts +198 -0
  97. package/src/lib/thread/keys.test.ts +9 -9
  98. package/src/lib/thread/keys.ts +1 -1
  99. package/src/lib/thread/manager.test.ts +24 -14
  100. package/src/lib/thread/manager.ts +19 -23
  101. package/src/lib/thread/snapshot.test.ts +51 -43
  102. package/src/lib/thread/snapshot.ts +54 -32
  103. package/src/lib/thread/test-utils.ts +106 -59
  104. package/src/lib/thread/tiered.test.ts +1 -1
  105. package/src/lib/thread/types.ts +2 -2
  106. package/src/lib/tool-router/router.integration.test.ts +44 -0
  107. package/src/lib/tool-router/router.ts +149 -33
  108. package/src/lib/tool-router/types.ts +23 -0
  109. package/src/lib/workflow.ts +49 -0
  110. package/src/{adapters/sandbox/inmemory/proxy.ts → test-utils/in-memory-sandbox-proxy.ts} +5 -16
  111. package/src/{adapters/sandbox/inmemory/index.ts → test-utils/in-memory-sandbox.ts} +11 -3
  112. package/src/tools/bash/bash.test.ts +1 -1
  113. package/src/tools/edit/handler.test.ts +1 -1
  114. package/tsup.config.ts +2 -4
  115. package/dist/adapters/sandbox/inmemory/index.cjs +0 -214
  116. package/dist/adapters/sandbox/inmemory/index.cjs.map +0 -1
  117. package/dist/adapters/sandbox/inmemory/index.d.cts +0 -40
  118. package/dist/adapters/sandbox/inmemory/index.d.ts +0 -40
  119. package/dist/adapters/sandbox/inmemory/index.js +0 -211
  120. package/dist/adapters/sandbox/inmemory/index.js.map +0 -1
  121. package/dist/adapters/sandbox/inmemory/workflow.cjs +0 -36
  122. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +0 -1
  123. package/dist/adapters/sandbox/inmemory/workflow.d.cts +0 -27
  124. package/dist/adapters/sandbox/inmemory/workflow.d.ts +0 -27
  125. package/dist/adapters/sandbox/inmemory/workflow.js +0 -34
  126. package/dist/adapters/sandbox/inmemory/workflow.js.map +0 -1
@@ -271,6 +271,50 @@ describe("createToolRouter integration", () => {
271
271
  expect(order[1]).toBe("start-echo-b");
272
272
  });
273
273
 
274
+ it("appends parallel results in original call order", async () => {
275
+ const slowEcho = defineTool({
276
+ name: "Echo" as const,
277
+ description: "slow echo with variable latency",
278
+ schema: z.object({ text: z.string(), delay: z.number() }),
279
+ handler: async (args: { text: string; delay: number }) => {
280
+ await new Promise((r) => setTimeout(r, args.delay));
281
+ return { toolResponse: args.text, data: { echoed: args.text } };
282
+ },
283
+ });
284
+
285
+ const router = createToolRouter({
286
+ tools: { Echo: slowEcho, Add: mathTool } as const,
287
+ threadId: "t-1",
288
+ appendToolResult: appendSpy.fn,
289
+ parallel: true,
290
+ });
291
+
292
+ const calls = [
293
+ router.parseToolCall({
294
+ id: "tc-1",
295
+ name: "Echo",
296
+ args: { text: "first", delay: 30 },
297
+ }),
298
+ router.parseToolCall({
299
+ id: "tc-2",
300
+ name: "Echo",
301
+ args: { text: "second", delay: 0 },
302
+ }),
303
+ router.parseToolCall({
304
+ id: "tc-3",
305
+ name: "Echo",
306
+ args: { text: "third", delay: 15 },
307
+ }),
308
+ ];
309
+
310
+ await router.processToolCalls(calls);
311
+
312
+ expect(appendSpy.calls).toHaveLength(3);
313
+ expect(at(appendSpy.calls, 0).toolCallId).toBe("tc-1");
314
+ expect(at(appendSpy.calls, 1).toolCallId).toBe("tc-2");
315
+ expect(at(appendSpy.calls, 2).toolCallId).toBe("tc-3");
316
+ });
317
+
274
318
  it("processes multiple tool calls sequentially", async () => {
275
319
  const order: string[] = [];
276
320
  const slowEcho = defineTool({
@@ -211,17 +211,29 @@ export function createToolRouter<T extends ToolMap>(
211
211
  * handler requested a session-level rewind; when present, the result is
212
212
  * not appended to the thread and siblings should be cancelled.
213
213
  */
214
+ interface PendingAppend {
215
+ toolCallId: string;
216
+ toolName: string;
217
+ content: JsonValue;
218
+ }
219
+
214
220
  type ProcessedToolCall =
215
- | { kind: "result"; value: ToolCallResultUnion<TResults> }
221
+ | {
222
+ kind: "result";
223
+ value: ToolCallResultUnion<TResults>;
224
+ pendingAppend?: PendingAppend;
225
+ }
216
226
  | { kind: "rewind"; signal: RewindSignal }
217
- | { kind: "skipped" };
227
+ | { kind: "skipped"; pendingAppend?: PendingAppend };
218
228
 
219
229
  async function processToolCall(
220
230
  toolCall: ParsedToolCallUnion<T>,
221
231
  turn: number,
222
232
  sandboxId?: string,
223
233
  onRewindRequested?: (signal: RewindSignal) => void,
224
- assistantMessageId?: string
234
+ assistantMessageId?: string,
235
+ persistThreadState?: () => Promise<void>,
236
+ deferAppend?: boolean
225
237
  ): Promise<ProcessedToolCall> {
226
238
  const startTime = Date.now();
227
239
  const tool = toolMap.get(toolCall.name);
@@ -229,15 +241,26 @@ export function createToolRouter<T extends ToolMap>(
229
241
  // --- Pre-hooks: may skip or modify args ---
230
242
  const preResult = await runPreHooks(toolCall, tool, turn);
231
243
  if (preResult.skip) {
244
+ const skipContent = JSON.stringify({
245
+ skipped: true,
246
+ reason: "Skipped by PreToolUse hook",
247
+ });
248
+ if (deferAppend) {
249
+ return {
250
+ kind: "skipped",
251
+ pendingAppend: {
252
+ toolCallId: toolCall.id,
253
+ toolName: toolCall.name,
254
+ content: skipContent,
255
+ },
256
+ };
257
+ }
232
258
  await appendToolResult(uuid4(), {
233
259
  threadId: options.threadId,
234
260
  threadKey: options.threadKey,
235
261
  toolCallId: toolCall.id,
236
262
  toolName: toolCall.name,
237
- content: JSON.stringify({
238
- skipped: true,
239
- reason: "Skipped by PreToolUse hook",
240
- }),
263
+ content: skipContent,
241
264
  });
242
265
  return { kind: "skipped" };
243
266
  }
@@ -265,6 +288,7 @@ export function createToolRouter<T extends ToolMap>(
265
288
  toolName: toolCall.name,
266
289
  ...(sandboxId !== undefined && { sandboxId }),
267
290
  ...(assistantMessageId !== undefined && { assistantMessageId }),
291
+ ...(persistThreadState !== undefined && { persistThreadState }),
268
292
  };
269
293
  const response = await tool.handler(
270
294
  effectiveArgs as Parameters<typeof tool.handler>[0],
@@ -312,19 +336,22 @@ export function createToolRouter<T extends ToolMap>(
312
336
  }
313
337
 
314
338
  // --- Append result to thread (unless handler already did) ---
315
- if (!resultAppended) {
316
- const config = {
317
- threadId: options.threadId,
318
- threadKey: options.threadKey,
319
- toolCallId: toolCall.id,
320
- toolName: toolCall.name,
321
- content,
322
- };
339
+ const needsAppend = !resultAppended;
340
+ if (needsAppend && !deferAppend) {
323
341
  await appendToolResult.executeWithOptions(
324
342
  {
325
343
  summary: `Append ${toolCall.name} result`,
326
344
  },
327
- [uuid4(), config]
345
+ [
346
+ uuid4(),
347
+ {
348
+ threadId: options.threadId,
349
+ threadKey: options.threadKey,
350
+ toolCallId: toolCall.id,
351
+ toolName: toolCall.name,
352
+ content,
353
+ },
354
+ ]
328
355
  );
329
356
  }
330
357
 
@@ -354,7 +381,18 @@ export function createToolRouter<T extends ToolMap>(
354
381
  durationMs
355
382
  );
356
383
 
357
- return { kind: "result", value: toolResult };
384
+ return {
385
+ kind: "result",
386
+ value: toolResult,
387
+ ...(needsAppend &&
388
+ deferAppend && {
389
+ pendingAppend: {
390
+ toolCallId: toolCall.id,
391
+ toolName: toolCall.name,
392
+ content,
393
+ },
394
+ }),
395
+ };
358
396
  }
359
397
 
360
398
  return {
@@ -407,7 +445,7 @@ export function createToolRouter<T extends ToolMap>(
407
445
  ): Promise<ProcessToolCallsResult<TResults>> {
408
446
  const attachRewind = (
409
447
  arr: ToolCallResultUnion<TResults>[],
410
- rewind: RewindSignal | undefined,
448
+ rewind: RewindSignal | undefined
411
449
  ): ProcessToolCallsResult<TResults> => {
412
450
  if (rewind) {
413
451
  (arr as ProcessToolCallsResult<TResults>).rewind = rewind;
@@ -422,6 +460,7 @@ export function createToolRouter<T extends ToolMap>(
422
460
  const turn = context?.turn ?? 0;
423
461
  const sandboxId = context?.sandboxId;
424
462
  const assistantMessageId = context?.assistantMessageId;
463
+ const persistThreadState = context?.persistThreadState;
425
464
 
426
465
  let rewindSignal: RewindSignal | undefined;
427
466
 
@@ -443,19 +482,56 @@ export function createToolRouter<T extends ToolMap>(
443
482
  turn,
444
483
  sandboxId,
445
484
  onRewindRequested,
446
- assistantMessageId
485
+ assistantMessageId,
486
+ persistThreadState,
487
+ true
447
488
  )
448
489
  )
449
490
  )
450
491
  );
451
492
 
493
+ // Fail fast on non-cancellation rejections before appending
494
+ // anything, so the thread stays clean for retry/truncation.
495
+ for (const outcome of outcomes) {
496
+ if (
497
+ outcome.status === "rejected" &&
498
+ !isCancellation(outcome.reason)
499
+ ) {
500
+ throw outcome.reason;
501
+ }
502
+ }
503
+
504
+ // Append deferred results in original call order so positional
505
+ // correlation between function calls and responses is preserved.
506
+ if (!rewindSignal) {
507
+ for (const outcome of outcomes) {
508
+ if (
509
+ outcome.status === "fulfilled" &&
510
+ outcome.value.kind !== "rewind" &&
511
+ outcome.value.pendingAppend
512
+ ) {
513
+ const pa = outcome.value.pendingAppend;
514
+ await appendToolResult.executeWithOptions(
515
+ { summary: `Append ${pa.toolName} result` },
516
+ [
517
+ uuid4(),
518
+ {
519
+ threadId: options.threadId,
520
+ threadKey: options.threadKey,
521
+ toolCallId: pa.toolCallId,
522
+ toolName: pa.toolName,
523
+ content: pa.content,
524
+ },
525
+ ]
526
+ );
527
+ }
528
+ }
529
+ }
530
+
452
531
  const results: ToolCallResultUnion<TResults>[] = [];
453
532
  for (const outcome of outcomes) {
454
533
  if (outcome.status === "rejected") {
455
- if (isCancellation(outcome.reason)) {
456
- continue;
457
- }
458
- throw outcome.reason;
534
+ continue;
459
535
  }
460
536
  if (outcome.value.kind === "result") {
461
537
  results.push(outcome.value.value);
@@ -471,7 +547,8 @@ export function createToolRouter<T extends ToolMap>(
471
547
  turn,
472
548
  sandboxId,
473
549
  undefined,
474
- assistantMessageId
550
+ assistantMessageId,
551
+ persistThreadState
475
552
  );
476
553
  if (outcome.kind === "rewind") {
477
554
  rewindSignal = outcome.signal;
@@ -497,8 +574,12 @@ export function createToolRouter<T extends ToolMap>(
497
574
  }
498
575
 
499
576
  const processOne = async (
500
- toolCall: ParsedToolCallUnion<T>
501
- ): Promise<ToolCallResult<TName, TResult>> => {
577
+ toolCall: ParsedToolCallUnion<T>,
578
+ deferAppend?: boolean
579
+ ): Promise<{
580
+ result: ToolCallResult<TName, TResult>;
581
+ pendingAppend?: PendingAppend;
582
+ }> => {
502
583
  const routerContext: RouterContext = {
503
584
  threadId: options.threadId,
504
585
  ...(options.threadKey && { threadKey: options.threadKey }),
@@ -510,13 +591,17 @@ export function createToolRouter<T extends ToolMap>(
510
591
  ...(context?.assistantMessageId !== undefined && {
511
592
  assistantMessageId: context.assistantMessageId,
512
593
  }),
594
+ ...(context?.persistThreadState !== undefined && {
595
+ persistThreadState: context.persistThreadState,
596
+ }),
513
597
  };
514
598
  const response = await handler(
515
599
  toolCall.args as ToolArgs<T, TName>,
516
600
  routerContext as Parameters<typeof handler>[1]
517
601
  );
518
602
 
519
- if (!response.resultAppended) {
603
+ const needsAppend = !response.resultAppended;
604
+ if (needsAppend && !deferAppend) {
520
605
  await appendToolResult.executeWithOptions(
521
606
  {
522
607
  summary: `Append ${toolCall.name} result`,
@@ -535,20 +620,51 @@ export function createToolRouter<T extends ToolMap>(
535
620
  }
536
621
 
537
622
  return {
538
- toolCallId: toolCall.id,
539
- name: toolCall.name as TName,
540
- data: response.data,
541
- ...(response.metadata && { metadata: response.metadata }),
623
+ result: {
624
+ toolCallId: toolCall.id,
625
+ name: toolCall.name as TName,
626
+ data: response.data,
627
+ ...(response.metadata && { metadata: response.metadata }),
628
+ },
629
+ ...(needsAppend &&
630
+ deferAppend && {
631
+ pendingAppend: {
632
+ toolCallId: toolCall.id,
633
+ toolName: toolCall.name,
634
+ content: response.toolResponse as JsonValue,
635
+ },
636
+ }),
542
637
  };
543
638
  };
544
639
 
545
640
  if (options.parallel) {
546
- return Promise.all(matchingCalls.map(processOne));
641
+ const outcomes = await Promise.all(
642
+ matchingCalls.map((tc) => processOne(tc, true))
643
+ );
644
+ for (const { pendingAppend } of outcomes) {
645
+ if (pendingAppend) {
646
+ await appendToolResult.executeWithOptions(
647
+ { summary: `Append ${pendingAppend.toolName} result` },
648
+ [
649
+ uuid4(),
650
+ {
651
+ threadId: options.threadId,
652
+ threadKey: options.threadKey,
653
+ toolCallId: pendingAppend.toolCallId,
654
+ toolName: pendingAppend.toolName,
655
+ content: pendingAppend.content,
656
+ },
657
+ ]
658
+ );
659
+ }
660
+ }
661
+ return outcomes.map((o) => o.result);
547
662
  }
548
663
 
549
664
  const results: ToolCallResult<TName, TResult>[] = [];
550
665
  for (const toolCall of matchingCalls) {
551
- results.push(await processOne(toolCall));
666
+ const { result } = await processOne(toolCall);
667
+ results.push(result);
552
668
  }
553
669
  return results;
554
670
  },
@@ -190,6 +190,20 @@ export interface RouterContext {
190
190
  * thread so the child's first model call sees a well-formed history.
191
191
  */
192
192
  assistantMessageId?: string;
193
+ /**
194
+ * Persist the parent session's current `PersistedThreadState` slice
195
+ * (tasks + custom state) to the durable thread store. Wired up by
196
+ * the session — absent for manually-driven routers (tests, custom
197
+ * orchestrators).
198
+ *
199
+ * Subagent handlers invoke this before spawning a child that will
200
+ * read the parent's thread (`newThreadSource: "from-parent"` or an
201
+ * explicit parent threadId): the parent's slice otherwise only
202
+ * lands in storage at session-exit time, so the child would load a
203
+ * stale (or empty) snapshot. Best-effort — failures are logged by
204
+ * the session but never thrown.
205
+ */
206
+ persistThreadState?: () => Promise<void>;
193
207
  }
194
208
 
195
209
  /**
@@ -314,6 +328,15 @@ export interface ProcessToolCallsContext {
314
328
  * out of a parent-forked thread).
315
329
  */
316
330
  assistantMessageId?: string;
331
+ /**
332
+ * Optional callback that flushes the session's in-memory
333
+ * `PersistedThreadState` slice to the durable thread store. The
334
+ * router forwards it into every handler's {@link RouterContext}
335
+ * verbatim. The session uses this to let mid-loop tool handlers
336
+ * (notably subagents that fork or continue the parent's thread)
337
+ * persist the parent's slice before the child reads it.
338
+ */
339
+ persistThreadState?: () => Promise<void>;
317
340
  }
318
341
 
319
342
  /**
@@ -1,4 +1,6 @@
1
1
  import type { ThreadInit, SandboxInit, SandboxShutdown } from "./lifecycle";
2
+ import type { SandboxSnapshot } from "./sandbox/types";
3
+ import type { TokenUsage } from "./types";
2
4
 
3
5
  /**
4
6
  * Session config fields derived from a main workflow input, ready to spread
@@ -13,6 +15,25 @@ export interface WorkflowSessionInput {
13
15
  sandbox?: SandboxInit;
14
16
  /** Sandbox shutdown policy (default: "destroy") */
15
17
  sandboxShutdown?: SandboxShutdown;
18
+ /**
19
+ * Called by the session right before `runSession` returns. Installed by
20
+ * `defineWorkflow` to capture sandbox / thread / usage outputs and forward
21
+ * them to the workflow's `onSessionExit` config hook. Spread into
22
+ * `createSession` via `...sessionInput`.
23
+ */
24
+ onSessionExit?: (result: {
25
+ sandboxId?: string;
26
+ snapshot?: SandboxSnapshot;
27
+ threadId: string;
28
+ usage: {
29
+ totalInputTokens: number;
30
+ totalOutputTokens: number;
31
+ totalCachedWriteTokens: number;
32
+ totalCachedReadTokens: number;
33
+ totalReasonTokens: number;
34
+ turns: number;
35
+ };
36
+ }) => void;
16
37
  }
17
38
 
18
39
  /** Raw workflow input fields that map into `WorkflowSessionInput`. */
@@ -34,6 +55,18 @@ export interface WorkflowConfig {
34
55
  * - `"keep"` — leave the sandbox running (no-op on exit).
35
56
  */
36
57
  sandboxShutdown?: SandboxShutdown;
58
+ /**
59
+ * Called right before the underlying session exits, with the sandbox /
60
+ * thread outputs and normalized token usage. Mirrors the capture logic in
61
+ * `defineSubagentWorkflow`; useful for emitting metrics or persisting
62
+ * sandbox / thread ids without threading them through the handler result.
63
+ */
64
+ onSessionExit?: (result: {
65
+ sandboxId?: string;
66
+ snapshot?: SandboxSnapshot;
67
+ threadId: string;
68
+ usage: TokenUsage;
69
+ }) => void;
37
70
  }
38
71
 
39
72
  /**
@@ -59,6 +92,22 @@ export function defineWorkflow<TInput, TResult>(
59
92
  sandboxShutdown: config.sandboxShutdown ?? "destroy",
60
93
  ...(workflowInput.thread && { thread: workflowInput.thread }),
61
94
  ...(workflowInput.sandbox && { sandbox: workflowInput.sandbox }),
95
+ ...(config.onSessionExit && {
96
+ onSessionExit: ({ sandboxId, snapshot, threadId, usage }): void => {
97
+ config.onSessionExit?.({
98
+ ...(sandboxId !== undefined && { sandboxId }),
99
+ ...(snapshot !== undefined && { snapshot }),
100
+ threadId,
101
+ usage: {
102
+ inputTokens: usage.totalInputTokens,
103
+ outputTokens: usage.totalOutputTokens,
104
+ cachedWriteTokens: usage.totalCachedWriteTokens,
105
+ cachedReadTokens: usage.totalCachedReadTokens,
106
+ reasonTokens: usage.totalReasonTokens,
107
+ },
108
+ });
109
+ },
110
+ }),
62
111
  };
63
112
  return fn(input, sessionInput);
64
113
  };
@@ -1,26 +1,15 @@
1
1
  /**
2
- * Workflow-safe proxy for in-memory sandbox operations.
2
+ * Test-only workflow-safe proxy for the in-memory sandbox fixture.
3
3
  *
4
- * Import this from `zeitlich/adapters/sandbox/inmemory/workflow`
5
- * in your Temporal workflow files.
6
- *
7
- * By default the scope is derived from `workflowInfo().workflowType`,
8
- * so activities are automatically namespaced per workflow.
9
- *
10
- * @example
11
- * ```typescript
12
- * import { proxyInMemorySandboxOps } from 'zeitlich/adapters/sandbox/inmemory/workflow';
13
- *
14
- * // Auto-scoped to the current workflow name
15
- * const sandbox = proxyInMemorySandboxOps();
16
- * ```
4
+ * Not part of the shipped public surface — used by the capability-type
5
+ * fixtures to exercise the full-capability proxy shape.
17
6
  */
18
7
  import { proxyActivities, workflowInfo } from "@temporalio/workflow";
19
8
  import type {
20
9
  SandboxCreateOptions,
21
10
  SandboxOps,
22
- } from "../../../lib/sandbox/types";
23
- import type { InMemoryCaps } from "./index";
11
+ } from "../lib/sandbox/types";
12
+ import type { InMemoryCaps } from "./in-memory-sandbox";
24
13
 
25
14
  const ADAPTER_PREFIX = "inMemory";
26
15
 
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Test-only in-memory {@link Sandbox} provider backed by `just-bash`.
3
+ *
4
+ * This is **not** part of the shipped public surface — it lives under
5
+ * `src/test-utils` purely so the sandbox manager, tool handlers, and
6
+ * capability-type fixtures have a lightweight, fully-featured backend to
7
+ * exercise against. `just-bash` is a dev dependency for the same reason.
8
+ */
1
9
  import {
2
10
  Bash,
3
11
  InMemoryFs,
@@ -18,9 +26,9 @@ import type {
18
26
  ExecResult,
19
27
  DirentEntry,
20
28
  FileStat,
21
- } from "../../../lib/sandbox/types";
22
- import { SandboxNotFoundError } from "../../../lib/sandbox/types";
23
- import { getShortId } from "../../../lib/thread/id";
29
+ } from "../lib/sandbox/types";
30
+ import { SandboxNotFoundError } from "../lib/sandbox/types";
31
+ import { getShortId } from "../lib/thread/id";
24
32
 
25
33
  // ============================================================================
26
34
  // Adapter: IFileSystem → SandboxFileSystem
@@ -2,7 +2,7 @@ import { describe, expect, it, beforeEach } from "vitest";
2
2
  import { bashHandler } from "./handler";
3
3
  import { withSandbox } from "../../lib/tool-router/with-sandbox";
4
4
  import { SandboxManager } from "../../lib/sandbox/manager";
5
- import { InMemorySandboxProvider } from "../../adapters/sandbox/inmemory/index";
5
+ import { InMemorySandboxProvider } from "../../test-utils/in-memory-sandbox";
6
6
  import type { RouterContext } from "../../lib/tool-router/types";
7
7
  import type { Sandbox, SandboxCreateOptions } from "../../lib/sandbox";
8
8
 
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
- import { InMemorySandboxProvider } from "../../adapters/sandbox/inmemory/index";
2
+ import { InMemorySandboxProvider } from "../../test-utils/in-memory-sandbox";
3
3
  import type { Sandbox, SandboxCreateOptions } from "../../lib/sandbox";
4
4
  import { SandboxManager } from "../../lib/sandbox/manager";
5
5
  import type { RouterContext } from "../../lib/tool-router/types";
package/tsup.config.ts CHANGED
@@ -15,9 +15,6 @@ export default defineConfig({
15
15
  "adapters/thread/anthropic/index": "src/adapters/thread/anthropic/index.ts",
16
16
  "adapters/thread/anthropic/workflow":
17
17
  "src/adapters/thread/anthropic/proxy.ts",
18
- "adapters/sandbox/inmemory/index": "src/adapters/sandbox/inmemory/index.ts",
19
- "adapters/sandbox/inmemory/workflow":
20
- "src/adapters/sandbox/inmemory/proxy.ts",
21
18
  "adapters/sandbox/daytona/index": "src/adapters/sandbox/daytona/index.ts",
22
19
  "adapters/sandbox/daytona/workflow":
23
20
  "src/adapters/sandbox/daytona/proxy.ts",
@@ -38,7 +35,8 @@ export default defineConfig({
38
35
  /^@anthropic-ai\//,
39
36
  /^@daytonaio\//,
40
37
  /^@e2b\//,
41
- "ioredis",
38
+ "redis",
39
+ /^@redis\//,
42
40
  "@mongodb-js/zstd",
43
41
  "node-liblzma",
44
42
  ],