zeitlich 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.
Files changed (58) hide show
  1. package/dist/{activities-CrN-ghLo.d.ts → activities-CPIB2v2C.d.ts} +4 -4
  2. package/dist/{activities-Coafq5zr.d.cts → activities-DnmNOnq4.d.cts} +4 -4
  3. package/dist/adapters/sandbox/daytona/index.d.cts +2 -2
  4. package/dist/adapters/sandbox/daytona/index.d.ts +2 -2
  5. package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
  6. package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
  7. package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
  8. package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
  9. package/dist/adapters/thread/anthropic/index.d.cts +4 -4
  10. package/dist/adapters/thread/anthropic/index.d.ts +4 -4
  11. package/dist/adapters/thread/anthropic/workflow.d.cts +4 -4
  12. package/dist/adapters/thread/anthropic/workflow.d.ts +4 -4
  13. package/dist/adapters/thread/google-genai/index.d.cts +4 -4
  14. package/dist/adapters/thread/google-genai/index.d.ts +4 -4
  15. package/dist/adapters/thread/google-genai/workflow.d.cts +4 -4
  16. package/dist/adapters/thread/google-genai/workflow.d.ts +4 -4
  17. package/dist/adapters/thread/langchain/index.cjs +11 -11
  18. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  19. package/dist/adapters/thread/langchain/index.d.cts +4 -4
  20. package/dist/adapters/thread/langchain/index.d.ts +4 -4
  21. package/dist/adapters/thread/langchain/index.js +8 -12
  22. package/dist/adapters/thread/langchain/index.js.map +1 -1
  23. package/dist/adapters/thread/langchain/workflow.d.cts +4 -4
  24. package/dist/adapters/thread/langchain/workflow.d.ts +4 -4
  25. package/dist/index.cjs +73 -50
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +6 -6
  28. package/dist/index.d.ts +6 -6
  29. package/dist/index.js +73 -51
  30. package/dist/index.js.map +1 -1
  31. package/dist/{proxy-COqA95FW.d.ts → proxy-B7Xi1znZ.d.ts} +1 -1
  32. package/dist/{proxy-Bf7uI-Hw.d.cts → proxy-DTnc5rqT.d.cts} +1 -1
  33. package/dist/{thread-manager-Bi1XlbpJ.d.ts → thread-manager-BAv340mi.d.ts} +3 -3
  34. package/dist/{thread-manager-wRVVBFgj.d.cts → thread-manager-BWv6ZXI3.d.cts} +4 -4
  35. package/dist/{thread-manager-BsLO3Fgc.d.cts → thread-manager-BlX2TwRN.d.cts} +3 -3
  36. package/dist/{thread-manager-BhkOyQ1I.d.ts → thread-manager-D2xorI-J.d.ts} +4 -4
  37. package/dist/{types-CdALEF3z.d.cts → types-4Wmk-wRq.d.cts} +1 -1
  38. package/dist/{types-CjY93AWZ.d.cts → types-C90VoEpt.d.cts} +1 -1
  39. package/dist/{types-BkX4HLzi.d.ts → types-Clhqautb.d.ts} +1 -1
  40. package/dist/{types-ChAy_jSP.d.ts → types-DKsCdAtQ.d.ts} +1 -1
  41. package/dist/{types-C66-BVBr.d.cts → types-DRJt1TMi.d.cts} +1 -1
  42. package/dist/{types-gVa5XCWD.d.ts → types-DpFD8ofR.d.ts} +1 -1
  43. package/dist/{workflow-BwT5EybR.d.ts → workflow-D32TRMr-.d.ts} +2 -2
  44. package/dist/{workflow-DMmiaw6w.d.cts → workflow-XVt0ww8K.d.cts} +2 -2
  45. package/dist/workflow.cjs +65 -39
  46. package/dist/workflow.cjs.map +1 -1
  47. package/dist/workflow.d.cts +2 -2
  48. package/dist/workflow.d.ts +2 -2
  49. package/dist/workflow.js +65 -39
  50. package/dist/workflow.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/lib/.env +1 -0
  53. package/src/lib/session/session-edge-cases.integration.test.ts +16 -10
  54. package/src/lib/session/session.ts +54 -40
  55. package/src/lib/state/manager.integration.test.ts +27 -0
  56. package/src/lib/state/manager.ts +39 -1
  57. package/src/lib/subagent/workflow.ts +2 -2
  58. package/src/tools/bash/.env +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.42",
3
+ "version": "0.2.44",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
package/src/lib/.env ADDED
@@ -0,0 +1 @@
1
+ E2B_API_KEY=e2b_39af116424059782e2aee6942fd70237cc2126c9
@@ -176,9 +176,18 @@ describe("createSession edge cases", () => {
176
176
  idCounter = 0;
177
177
  });
178
178
 
179
- // --- WAITING_FOR_INPUT flow (condition returns false = timeout) ---
180
-
181
- it("cancels session when WAITING_FOR_INPUT times out (condition returns false)", async () => {
179
+ // NOTE: a previous test here ("cancels session when WAITING_FOR_INPUT
180
+ // times out") covered the condition-based wait + cancel-on-timeout flow
181
+ // that was removed in 7ab652b ("fix: stop halting workflow when
182
+ // waiting"). The session loop now exits cleanly the moment a tool puts
183
+ // the agent into WAITING_FOR_INPUT — there is no internal timeout to
184
+ // assert against — so the test was deleted rather than rewritten. The
185
+ // assertion below covers the surviving contract: the exit reason
186
+ // reflects the WAITING_FOR_INPUT state instead of the default
187
+ // "completed", so callers can distinguish a finished session from a
188
+ // parked one.
189
+
190
+ it("exits with 'waiting_for_input' when a tool parks the session", async () => {
182
191
  const { ops } = createMockThreadOps();
183
192
  let endReason: string | undefined;
184
193
  const capturedRef: {
@@ -191,10 +200,7 @@ describe("createSession edge cases", () => {
191
200
  schema: z.object({}),
192
201
  handler: async (_args: Record<string, never>, _ctx: RouterContext) => {
193
202
  capturedRef.stateManager?.waitForInput();
194
- return {
195
- toolResponse: "Please provide input.",
196
- data: null,
197
- };
203
+ return { toolResponse: "Please provide input.", data: null };
198
204
  },
199
205
  });
200
206
 
@@ -224,9 +230,9 @@ describe("createSession edge cases", () => {
224
230
 
225
231
  const result = await session.runSession({ stateManager });
226
232
 
227
- expect(result.exitReason).toBe("cancelled");
228
- expect(result.finalMessage).toBeNull();
229
- expect(endReason).toBe("cancelled");
233
+ expect(result.exitReason).toBe("waiting_for_input");
234
+ expect(endReason).toBe("waiting_for_input");
235
+ expect(stateManager.getStatus()).toBe("WAITING_FOR_INPUT");
230
236
  });
231
237
 
232
238
  // --- All tool calls are invalid ---
@@ -383,46 +383,6 @@ export async function createSession<
383
383
  });
384
384
  }
385
385
 
386
- // --- Virtual filesystem init (independent of sandbox) ----------------
387
- if (virtualFsConfig) {
388
- if (!virtualFsOps) {
389
- throw ApplicationFailure.create({
390
- message: "No virtualFsOps provided — cannot resolve file tree",
391
- nonRetryable: true,
392
- });
393
- }
394
- const result = await virtualFsOps.resolveFileTree(virtualFsConfig.ctx);
395
- const skillFiles = skills ? collectSkillFiles(skills) : undefined;
396
- const fileTree = skillFiles
397
- ? [
398
- ...result.fileTree,
399
- ...Object.entries(skillFiles).map(([path, content]) => ({
400
- id: `skill:${path}`,
401
- path,
402
- size: content.length,
403
- mtime: new Date().toISOString(),
404
- metadata: {},
405
- // Carry the content directly on the entry so any handler that
406
- // constructs a VirtualFileSystem from `fileTree` can read it
407
- // without needing to also wire up `inlineFiles` from state.
408
- inlineContent: content,
409
- })),
410
- ]
411
- : result.fileTree;
412
- stateManager.mergeUpdate({
413
- fileTree,
414
- virtualFsCtx: virtualFsConfig.ctx,
415
- // `inlineFiles` is still the source of truth at read time:
416
- // VirtualFileSystem checks the inlineFiles map first and only
417
- // falls through to entry.inlineContent. Embedding the content on
418
- // the entry is the migration target so that handlers building a
419
- // VirtualFileSystem from `fileTree` alone (without forwarding
420
- // `inlineFiles` from state) can read skill resources. Until a
421
- // follow-up drops `inlineFiles`, both fields are populated.
422
- ...(skillFiles && { inlineFiles: skillFiles }),
423
- } as Partial<AgentState<TState>>);
424
- }
425
-
426
386
  if (hooks.onSessionStart) {
427
387
  await hooks.onSessionStart({
428
388
  threadId,
@@ -474,6 +434,52 @@ export async function createSession<
474
434
  await initializeThread(threadId, threadKey);
475
435
  }
476
436
  }
437
+
438
+ // --- Virtual filesystem init (independent of sandbox) ----------------
439
+ // Runs AFTER thread rehydration so the freshly resolved tree is
440
+ // authoritative. Otherwise a stale `fileTree` carried in a persisted
441
+ // slice (from a run on older code that didn't strip it) could
442
+ // overwrite entries that no longer exist in the backing store and
443
+ // cause subsequent FileWrite calls to fail with "not_found".
444
+ if (virtualFsConfig) {
445
+ if (!virtualFsOps) {
446
+ throw ApplicationFailure.create({
447
+ message: "No virtualFsOps provided — cannot resolve file tree",
448
+ nonRetryable: true,
449
+ });
450
+ }
451
+ const result = await virtualFsOps.resolveFileTree(virtualFsConfig.ctx);
452
+ const skillFiles = skills ? collectSkillFiles(skills) : undefined;
453
+ const fileTree = skillFiles
454
+ ? [
455
+ ...result.fileTree,
456
+ ...Object.entries(skillFiles).map(([path, content]) => ({
457
+ id: `skill:${path}`,
458
+ path,
459
+ size: content.length,
460
+ mtime: new Date().toISOString(),
461
+ metadata: {},
462
+ // Carry the content directly on the entry so any handler that
463
+ // constructs a VirtualFileSystem from `fileTree` can read it
464
+ // without needing to also wire up `inlineFiles` from state.
465
+ inlineContent: content,
466
+ })),
467
+ ]
468
+ : result.fileTree;
469
+ stateManager.mergeUpdate({
470
+ fileTree,
471
+ virtualFsCtx: virtualFsConfig.ctx,
472
+ // `inlineFiles` is still the source of truth at read time:
473
+ // VirtualFileSystem checks the inlineFiles map first and only
474
+ // falls through to entry.inlineContent. Embedding the content on
475
+ // the entry is the migration target so that handlers building a
476
+ // VirtualFileSystem from `fileTree` alone (without forwarding
477
+ // `inlineFiles` from state) can read skill resources. Until a
478
+ // follow-up drops `inlineFiles`, both fields are populated.
479
+ ...(skillFiles && { inlineFiles: skillFiles }),
480
+ } as Partial<AgentState<TState>>);
481
+ }
482
+
477
483
  await appendHumanMessage(
478
484
  threadId,
479
485
  uuid4(),
@@ -596,6 +602,14 @@ export async function createSession<
596
602
  threadId,
597
603
  maxTurns,
598
604
  });
605
+ } else if (stateManager.getStatus() === "WAITING_FOR_INPUT") {
606
+ // A tool put the agent into WAITING_FOR_INPUT (e.g. an AskUser
607
+ // tool). The loop's `isRunning()` guard then exited cleanly; we
608
+ // report a dedicated exit reason rather than the misleading
609
+ // default `"completed"`, so callers can distinguish a finished
610
+ // session from one parked waiting on external input.
611
+ exitReason = "waiting_for_input";
612
+ log.info("session waiting for input", { agentName, threadId });
599
613
  }
600
614
  } catch (error) {
601
615
  exitReason = "failed";
@@ -385,6 +385,33 @@ describe("createAgentStateManager integration", () => {
385
385
  expect("tools" in slice.custom).toBe(false);
386
386
  });
387
387
 
388
+ it("getPersistedSlice strips runtime fields injected via mergeUpdate", () => {
389
+ const sm = createAgentStateManager<{ label: string }>({
390
+ initialState: { systemPrompt: "test", label: "original" },
391
+ });
392
+
393
+ sm.mergeUpdate({
394
+ label: "updated",
395
+ fileTree: [
396
+ {
397
+ id: "f1",
398
+ path: "/a.txt",
399
+ size: 1,
400
+ mtime: "2026-01-01T00:00:00Z",
401
+ metadata: {},
402
+ },
403
+ ],
404
+ virtualFsCtx: { workspaceBase: "/tmp" },
405
+ inlineFiles: { "/a.txt": "hi" },
406
+ } as Parameters<typeof sm.mergeUpdate>[0]);
407
+
408
+ const slice = sm.getPersistedSlice();
409
+ expect(slice.custom).toEqual({ label: "updated" });
410
+ expect("fileTree" in slice.custom).toBe(false);
411
+ expect("virtualFsCtx" in slice.custom).toBe(false);
412
+ expect("inlineFiles" in slice.custom).toBe(false);
413
+ });
414
+
388
415
  it("mergeUpdate replaces the task map when given a tasks field", () => {
389
416
  const sm = createAgentStateManager<{ label: string; extra?: string }>({
390
417
  initialState: {
@@ -20,6 +20,32 @@ import type {
20
20
  } from "./types";
21
21
  import { z } from "zod";
22
22
 
23
+ /**
24
+ * Fields that live on `AgentState` at runtime but must NOT be persisted into
25
+ * `PersistedThreadState`. They're either managed by the state manager itself
26
+ * (status/version/turns/tasks/tools/usage), kept in their own slot
27
+ * (systemPrompt), or rebuilt on each run (the virtual-fs trio).
28
+ *
29
+ * Centralizing the list keeps the constructor's destructure-omit and
30
+ * `getPersistedSlice` in lockstep.
31
+ */
32
+ const RESERVED_STATE_KEYS = [
33
+ "status",
34
+ "version",
35
+ "turns",
36
+ "tasks",
37
+ "tools",
38
+ "systemPrompt",
39
+ "fileTree",
40
+ "inlineFiles",
41
+ "virtualFsCtx",
42
+ "totalInputTokens",
43
+ "totalOutputTokens",
44
+ "totalReasonTokens",
45
+ "cachedWriteTokens",
46
+ "cachedReadTokens",
47
+ ] as const;
48
+
23
49
  /**
24
50
  * Creates an agent state manager for tracking workflow state.
25
51
  * Automatically registers Temporal query and update handlers for the agent.
@@ -238,9 +264,21 @@ export function createAgentStateManager<
238
264
  },
239
265
 
240
266
  getPersistedSlice(): PersistedThreadState {
267
+ // `customState` can pick up reserved/runtime fields via `mergeUpdate`
268
+ // (e.g. `fileTree`, `virtualFsCtx`, `inlineFiles` written by the
269
+ // virtual-fs bootstrap on every run). Those are rebuilt per run and
270
+ // must never round-trip through the thread store, so strip them here
271
+ // rather than relying on callers to remember.
272
+ const source = customState as unknown as Record<string, JsonValue>;
273
+ const custom: Record<string, JsonValue> = {};
274
+ const reserved = new Set<string>(RESERVED_STATE_KEYS);
275
+ for (const [key, value] of Object.entries(source)) {
276
+ if (reserved.has(key)) continue;
277
+ custom[key] = value;
278
+ }
241
279
  return {
242
280
  tasks: Array.from(tasks.entries()),
243
- custom: { ...(customState as unknown as Record<string, JsonValue>) },
281
+ custom,
244
282
  };
245
283
  },
246
284
 
@@ -168,9 +168,8 @@ export function defineSubagentWorkflow(
168
168
 
169
169
  // Auto-forward sandbox outputs captured from the session so user code
170
170
  // never has to thread them through manually. Explicit values on the fn
171
- // result take precedence.
171
+ // result take precedence, so spread `result` LAST.
172
172
  return {
173
- ...result,
174
173
  ...(capturedThreadId !== undefined && { threadId: capturedThreadId }),
175
174
  ...(capturedSandboxId !== undefined && { sandboxId: capturedSandboxId }),
176
175
  ...(capturedSnapshot !== undefined && { snapshot: capturedSnapshot }),
@@ -178,6 +177,7 @@ export function defineSubagentWorkflow(
178
177
  baseSnapshot: capturedBaseSnapshot,
179
178
  }),
180
179
  ...(capturedUsage !== undefined && { usage: capturedUsage }),
180
+ ...result,
181
181
  };
182
182
  };
183
183
 
@@ -0,0 +1 @@
1
+ E2B_API_KEY=e2b_39af116424059782e2aee6942fd70237cc2126c9