zeitlich 0.2.27 → 0.2.29

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 (109) hide show
  1. package/README.md +121 -13
  2. package/dist/{activities-DE3_q9yq.d.ts → activities-1xrWRrGJ.d.cts} +2 -3
  3. package/dist/{activities-p8PDlRIK.d.cts → activities-DOViDCTE.d.ts} +2 -3
  4. package/dist/adapters/sandbox/e2b/index.cjs +230 -0
  5. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -0
  6. package/dist/adapters/sandbox/e2b/index.d.cts +77 -0
  7. package/dist/adapters/sandbox/e2b/index.d.ts +77 -0
  8. package/dist/adapters/sandbox/e2b/index.js +227 -0
  9. package/dist/adapters/sandbox/e2b/index.js.map +1 -0
  10. package/dist/adapters/sandbox/{virtual → e2b}/workflow.cjs +8 -8
  11. package/dist/adapters/sandbox/e2b/workflow.cjs.map +1 -0
  12. package/dist/adapters/sandbox/e2b/workflow.d.cts +27 -0
  13. package/dist/adapters/sandbox/e2b/workflow.d.ts +27 -0
  14. package/dist/adapters/sandbox/{virtual → e2b}/workflow.js +8 -8
  15. package/dist/adapters/sandbox/e2b/workflow.js.map +1 -0
  16. package/dist/adapters/thread/anthropic/index.d.cts +5 -6
  17. package/dist/adapters/thread/anthropic/index.d.ts +5 -6
  18. package/dist/adapters/thread/anthropic/workflow.d.cts +4 -5
  19. package/dist/adapters/thread/anthropic/workflow.d.ts +4 -5
  20. package/dist/adapters/thread/google-genai/index.d.cts +5 -6
  21. package/dist/adapters/thread/google-genai/index.d.ts +5 -6
  22. package/dist/adapters/thread/google-genai/workflow.d.cts +4 -5
  23. package/dist/adapters/thread/google-genai/workflow.d.ts +4 -5
  24. package/dist/adapters/thread/langchain/index.cjs +22 -10
  25. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  26. package/dist/adapters/thread/langchain/index.d.cts +12 -10
  27. package/dist/adapters/thread/langchain/index.d.ts +12 -10
  28. package/dist/adapters/thread/langchain/index.js +22 -10
  29. package/dist/adapters/thread/langchain/index.js.map +1 -1
  30. package/dist/adapters/thread/langchain/workflow.d.cts +4 -5
  31. package/dist/adapters/thread/langchain/workflow.d.ts +4 -5
  32. package/dist/index.cjs +526 -24
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +66 -15
  35. package/dist/index.d.ts +66 -15
  36. package/dist/index.js +522 -26
  37. package/dist/index.js.map +1 -1
  38. package/dist/{proxy-BK1ydQt0.d.ts → proxy-78nc985d.d.ts} +1 -1
  39. package/dist/{proxy-BMAsMHdp.d.cts → proxy-Bm2UTiO_.d.cts} +1 -1
  40. package/dist/{thread-manager-dzaJHQEA.d.ts → thread-manager-07BaYu_z.d.ts} +1 -1
  41. package/dist/{thread-manager-Bz8txKKj.d.cts → thread-manager-BRE5KkHB.d.cts} +1 -1
  42. package/dist/{thread-manager-BlHua5_v.d.cts → thread-manager-CatBkarc.d.cts} +1 -1
  43. package/dist/{thread-manager-Bh9x847n.d.ts → thread-manager-CxbWo7q_.d.ts} +1 -1
  44. package/dist/types-BkVoEyiH.d.ts +1211 -0
  45. package/dist/{types-CIkYBoF8.d.ts → types-DAv_SLN8.d.ts} +1 -1
  46. package/dist/{types-BfIQABzu.d.cts → types-Dpz2gXLk.d.cts} +1 -1
  47. package/dist/types-seDYom4M.d.cts +1211 -0
  48. package/dist/workflow-B4T3la0p.d.cts +750 -0
  49. package/dist/workflow-DCmaXLZ_.d.ts +750 -0
  50. package/dist/workflow.cjs +198 -21
  51. package/dist/workflow.cjs.map +1 -1
  52. package/dist/workflow.d.cts +5 -579
  53. package/dist/workflow.d.ts +5 -579
  54. package/dist/workflow.js +197 -23
  55. package/dist/workflow.js.map +1 -1
  56. package/package.json +23 -23
  57. package/src/adapters/sandbox/{virtual → e2b}/proxy.ts +16 -13
  58. package/src/adapters/thread/langchain/hooks.test.ts +195 -0
  59. package/src/adapters/thread/langchain/hooks.ts +29 -12
  60. package/src/index.ts +7 -0
  61. package/src/lib/observability/hooks.ts +117 -0
  62. package/src/lib/observability/index.ts +13 -0
  63. package/src/lib/observability/sinks.ts +88 -0
  64. package/src/lib/sandbox/manager.ts +3 -3
  65. package/src/lib/session/session-edge-cases.integration.test.ts +1 -0
  66. package/src/lib/session/session.integration.test.ts +1 -0
  67. package/src/lib/session/session.ts +79 -5
  68. package/src/lib/session/types.ts +21 -0
  69. package/src/lib/state/manager.integration.test.ts +1 -0
  70. package/src/lib/subagent/handler.ts +22 -2
  71. package/src/lib/subagent/register.ts +7 -2
  72. package/src/lib/subagent/subagent.integration.test.ts +1 -0
  73. package/src/lib/tool-router/router-edge-cases.integration.test.ts +2 -0
  74. package/src/lib/tool-router/router.integration.test.ts +2 -0
  75. package/src/lib/tool-router/router.ts +26 -7
  76. package/src/lib/tool-router/types.ts +0 -4
  77. package/src/{adapters/sandbox/virtual → lib/virtual-fs}/filesystem.ts +4 -4
  78. package/src/lib/virtual-fs/index.ts +18 -0
  79. package/src/lib/virtual-fs/manager.ts +48 -0
  80. package/src/lib/virtual-fs/proxy.ts +45 -0
  81. package/src/{adapters/sandbox/virtual → lib/virtual-fs}/types.ts +43 -33
  82. package/src/{adapters/sandbox/virtual/virtual-sandbox.test.ts → lib/virtual-fs/virtual-fs.test.ts} +15 -130
  83. package/src/lib/virtual-fs/with-virtual-fs.ts +94 -0
  84. package/src/workflow.ts +25 -8
  85. package/tsup.config.ts +3 -2
  86. package/dist/adapters/sandbox/virtual/index.cjs +0 -487
  87. package/dist/adapters/sandbox/virtual/index.cjs.map +0 -1
  88. package/dist/adapters/sandbox/virtual/index.d.cts +0 -90
  89. package/dist/adapters/sandbox/virtual/index.d.ts +0 -90
  90. package/dist/adapters/sandbox/virtual/index.js +0 -479
  91. package/dist/adapters/sandbox/virtual/index.js.map +0 -1
  92. package/dist/adapters/sandbox/virtual/workflow.cjs.map +0 -1
  93. package/dist/adapters/sandbox/virtual/workflow.d.cts +0 -28
  94. package/dist/adapters/sandbox/virtual/workflow.d.ts +0 -28
  95. package/dist/adapters/sandbox/virtual/workflow.js.map +0 -1
  96. package/dist/queries-BCgJ9Sr5.d.ts +0 -44
  97. package/dist/queries-DwnE2bu3.d.cts +0 -44
  98. package/dist/types-CvJyXDYt.d.ts +0 -490
  99. package/dist/types-DFUNSYbj.d.ts +0 -125
  100. package/dist/types-DRnz-OZp.d.cts +0 -125
  101. package/dist/types-DSOefLpY.d.cts +0 -490
  102. package/dist/types-mCVxKIZb.d.cts +0 -585
  103. package/dist/types-mCVxKIZb.d.ts +0 -585
  104. package/src/adapters/sandbox/virtual/index.ts +0 -92
  105. package/src/adapters/sandbox/virtual/provider.ts +0 -121
  106. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +0 -97
  107. /package/src/{adapters/sandbox/virtual → lib/virtual-fs}/mutations.ts +0 -0
  108. /package/src/{adapters/sandbox/virtual → lib/virtual-fs}/queries.ts +0 -0
  109. /package/src/{adapters/sandbox/virtual → lib/virtual-fs}/tree.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -107,26 +107,6 @@
107
107
  "default": "./dist/adapters/sandbox/inmemory/workflow.js"
108
108
  }
109
109
  },
110
- "./adapters/sandbox/virtual": {
111
- "import": {
112
- "types": "./dist/adapters/sandbox/virtual/index.d.ts",
113
- "default": "./dist/adapters/sandbox/virtual/index.js"
114
- },
115
- "require": {
116
- "types": "./dist/adapters/sandbox/virtual/index.d.ts",
117
- "default": "./dist/adapters/sandbox/virtual/index.js"
118
- }
119
- },
120
- "./adapters/sandbox/virtual/workflow": {
121
- "import": {
122
- "types": "./dist/adapters/sandbox/virtual/workflow.d.ts",
123
- "default": "./dist/adapters/sandbox/virtual/workflow.js"
124
- },
125
- "require": {
126
- "types": "./dist/adapters/sandbox/virtual/workflow.d.ts",
127
- "default": "./dist/adapters/sandbox/virtual/workflow.js"
128
- }
129
- },
130
110
  "./adapters/sandbox/daytona": {
131
111
  "import": {
132
112
  "types": "./dist/adapters/sandbox/daytona/index.d.ts",
@@ -147,6 +127,26 @@
147
127
  "default": "./dist/adapters/sandbox/daytona/workflow.js"
148
128
  }
149
129
  },
130
+ "./adapters/sandbox/e2b": {
131
+ "import": {
132
+ "types": "./dist/adapters/sandbox/e2b/index.d.ts",
133
+ "default": "./dist/adapters/sandbox/e2b/index.js"
134
+ },
135
+ "require": {
136
+ "types": "./dist/adapters/sandbox/e2b/index.d.ts",
137
+ "default": "./dist/adapters/sandbox/e2b/index.js"
138
+ }
139
+ },
140
+ "./adapters/sandbox/e2b/workflow": {
141
+ "import": {
142
+ "types": "./dist/adapters/sandbox/e2b/workflow.d.ts",
143
+ "default": "./dist/adapters/sandbox/e2b/workflow.js"
144
+ },
145
+ "require": {
146
+ "types": "./dist/adapters/sandbox/e2b/workflow.d.ts",
147
+ "default": "./dist/adapters/sandbox/e2b/workflow.js"
148
+ }
149
+ },
150
150
  "./adapters/sandbox/bedrock": {
151
151
  "import": {
152
152
  "types": "./dist/adapters/sandbox/bedrock/index.d.ts",
@@ -212,7 +212,7 @@
212
212
  "devDependencies": {
213
213
  "@anthropic-ai/sdk": "^0.80.0",
214
214
  "@aws-sdk/client-bedrock-agentcore": "^3.900.0",
215
- "@daytonaio/sdk": "^0.149.0",
215
+ "@daytonaio/sdk": "^0.158.1",
216
216
  "@e2b/code-interpreter": "^2.3.3",
217
217
  "@eslint/js": "^10.0.1",
218
218
  "@google/genai": "^1.44.0",
@@ -226,7 +226,7 @@
226
226
  "prettier": "^3.8.1",
227
227
  "release-please": "^17.3.0",
228
228
  "tsup": "^8.5.1",
229
- "typescript": "^5.9.3",
229
+ "typescript": "^6.0.2",
230
230
  "typescript-eslint": "^8.56.1",
231
231
  "vitest": "^4.0.18"
232
232
  },
@@ -1,7 +1,10 @@
1
1
  /**
2
- * Workflow-safe proxy for virtual sandbox operations.
2
+ * Workflow-safe proxy for E2B sandbox operations.
3
3
  *
4
- * Import this from `zeitlich/adapters/sandbox/virtual/workflow`
4
+ * Uses longer timeouts than in-memory providers since E2B
5
+ * sandboxes are remote and creation involves provisioning.
6
+ *
7
+ * Import this from `zeitlich/adapters/sandbox/e2b/workflow`
5
8
  * in your Temporal workflow files.
6
9
  *
7
10
  * By default the scope is derived from `workflowInfo().workflowType`,
@@ -9,32 +12,32 @@
9
12
  *
10
13
  * @example
11
14
  * ```typescript
12
- * import { proxyVirtualSandboxOps } from 'zeitlich/adapters/sandbox/virtual/workflow';
15
+ * import { proxyE2bSandboxOps } from 'zeitlich/adapters/sandbox/e2b/workflow';
13
16
  *
14
- * const sandbox = proxyVirtualSandboxOps();
17
+ * const sandbox = proxyE2bSandboxOps();
15
18
  * ```
16
19
  */
17
20
  import { proxyActivities, workflowInfo } from "@temporalio/workflow";
18
21
  import type { SandboxOps } from "../../../lib/sandbox/types";
19
- import type { VirtualSandboxCreateOptions } from "./types";
22
+ import type { E2bSandboxCreateOptions } from "./types";
20
23
 
21
- const ADAPTER_PREFIX = "virtual";
24
+ const ADAPTER_PREFIX = "e2b";
22
25
 
23
- export function proxyVirtualSandboxOps(
26
+ export function proxyE2bSandboxOps(
24
27
  scope?: string,
25
28
  options?: Parameters<typeof proxyActivities>[0]
26
- ): SandboxOps<VirtualSandboxCreateOptions<unknown>> {
29
+ ): SandboxOps {
27
30
  const resolvedScope = scope ?? workflowInfo().workflowType;
28
31
 
29
32
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
33
  const acts = proxyActivities<Record<string, (...args: any[]) => any>>(
31
34
  options ?? {
32
- startToCloseTimeout: "30s",
35
+ startToCloseTimeout: "120s",
33
36
  retry: {
34
37
  maximumAttempts: 3,
35
- initialInterval: "2s",
36
- maximumInterval: "30s",
37
- backoffCoefficient: 2,
38
+ initialInterval: "5s",
39
+ maximumInterval: "60s",
40
+ backoffCoefficient: 3,
38
41
  },
39
42
  }
40
43
  );
@@ -49,5 +52,5 @@ export function proxyVirtualSandboxOps(
49
52
  pauseSandbox: acts[p("pauseSandbox")],
50
53
  snapshotSandbox: acts[p("snapshotSandbox")],
51
54
  forkSandbox: acts[p("forkSandbox")],
52
- } as SandboxOps<VirtualSandboxCreateOptions<unknown>>;
55
+ } as SandboxOps<E2bSandboxCreateOptions>;
53
56
  }
@@ -0,0 +1,195 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { HumanMessage, AIMessage } from "@langchain/core/messages";
3
+ import type { BaseMessage } from "@langchain/core/messages";
4
+ import { appendCachePoint } from "./hooks";
5
+
6
+ const cacheBlock = { type: "cache_control" as const, cache_control: { type: "ephemeral" as const } };
7
+
8
+ function applyHook(
9
+ messages: BaseMessage[],
10
+ hook: ReturnType<typeof appendCachePoint>,
11
+ ): BaseMessage[] {
12
+ return messages.map((m, i, arr) => hook(m, i, arr));
13
+ }
14
+
15
+ function countCacheBlocks(messages: BaseMessage[]): number {
16
+ return messages.reduce((n, m) => {
17
+ const c = m.content;
18
+ if (Array.isArray(c)) {
19
+ return n + (c.some((b) => b.type === cacheBlock.type) ? 1 : 0);
20
+ }
21
+ return n;
22
+ }, 0);
23
+ }
24
+
25
+ function messageAt(messages: BaseMessage[], idx: number): BaseMessage {
26
+ const m = messages[idx];
27
+ if (!m) throw new Error(`No message at index ${String(idx)}`);
28
+ return m;
29
+ }
30
+
31
+ describe("appendCachePoint", () => {
32
+ it("appends a cache block to the last message", () => {
33
+ const messages: BaseMessage[] = [
34
+ new HumanMessage("hello"),
35
+ new AIMessage("hi"),
36
+ new HumanMessage("bye"),
37
+ ];
38
+ const hook = appendCachePoint(cacheBlock);
39
+ const result = applyHook(messages, hook);
40
+
41
+ const last = messageAt(result, 2);
42
+ expect(Array.isArray(last.content)).toBe(true);
43
+ const blocks = last.content as Array<{ type: string }>;
44
+ expect(blocks.some((b) => b.type === cacheBlock.type)).toBe(true);
45
+ });
46
+
47
+ it("deduplicates within the last message", () => {
48
+ const messages: BaseMessage[] = [
49
+ new HumanMessage({
50
+ content: [
51
+ { type: "text", text: "hello" },
52
+ cacheBlock,
53
+ ],
54
+ }),
55
+ ];
56
+ const hook = appendCachePoint(cacheBlock);
57
+ const result = applyHook(messages, hook);
58
+
59
+ const blocks = (messageAt(result, 0).content as Array<{ type: string }>).filter(
60
+ (b) => b.type === cacheBlock.type,
61
+ );
62
+ expect(blocks).toHaveLength(1);
63
+ });
64
+
65
+ it("strips old cache blocks when total would exceed maxBlocks", () => {
66
+ const messages: BaseMessage[] = Array.from({ length: 6 }, (_, i) =>
67
+ new HumanMessage({
68
+ content: [
69
+ { type: "text", text: `msg ${i}` },
70
+ cacheBlock,
71
+ ],
72
+ }),
73
+ );
74
+ const hook = appendCachePoint(cacheBlock, { maxBlocks: 4 });
75
+ const result = applyHook(messages, hook);
76
+
77
+ expect(countCacheBlocks(result)).toBe(4);
78
+ });
79
+
80
+ it("keeps the most recent cache blocks", () => {
81
+ const messages: BaseMessage[] = Array.from({ length: 6 }, (_, i) =>
82
+ new HumanMessage({
83
+ content: [
84
+ { type: "text", text: `msg ${i}` },
85
+ cacheBlock,
86
+ ],
87
+ }),
88
+ );
89
+ const hook = appendCachePoint(cacheBlock, { maxBlocks: 4 });
90
+ const result = applyHook(messages, hook);
91
+
92
+ const hasCache = result.map((m) => {
93
+ const c = m.content;
94
+ return Array.isArray(c) && c.some((b) => b.type === cacheBlock.type);
95
+ });
96
+ expect(hasCache[0]).toBe(false);
97
+ expect(hasCache[1]).toBe(false);
98
+ expect(hasCache[3]).toBe(true);
99
+ expect(hasCache[4]).toBe(true);
100
+ expect(hasCache[5]).toBe(true);
101
+ });
102
+
103
+ it("respects a custom maxBlocks value", () => {
104
+ const messages: BaseMessage[] = Array.from({ length: 5 }, (_, i) =>
105
+ new HumanMessage({
106
+ content: [
107
+ { type: "text", text: `msg ${i}` },
108
+ cacheBlock,
109
+ ],
110
+ }),
111
+ );
112
+ const hook = appendCachePoint(cacheBlock, { maxBlocks: 2 });
113
+ const result = applyHook(messages, hook);
114
+
115
+ expect(countCacheBlocks(result)).toBe(2);
116
+ });
117
+
118
+ it("does nothing to non-last messages without cache blocks", () => {
119
+ const messages: BaseMessage[] = [
120
+ new HumanMessage("plain"),
121
+ new AIMessage("response"),
122
+ new HumanMessage("last"),
123
+ ];
124
+ const hook = appendCachePoint(cacheBlock);
125
+ const result = applyHook(messages, hook);
126
+
127
+ expect(messageAt(result, 0).content).toBe("plain");
128
+ expect(messageAt(result, 1).content).toBe("response");
129
+ expect(countCacheBlocks(result)).toBe(1);
130
+ });
131
+
132
+ it("handles string content on the last message", () => {
133
+ const messages: BaseMessage[] = [new HumanMessage("only")];
134
+ const hook = appendCachePoint(cacheBlock);
135
+ const result = applyHook(messages, hook);
136
+
137
+ const first = messageAt(result, 0);
138
+ expect(Array.isArray(first.content)).toBe(true);
139
+ const content = first.content as Array<{ type: string }>;
140
+ expect(content.some((b) => b.type === cacheBlock.type)).toBe(true);
141
+ });
142
+
143
+ it("defaults maxBlocks to 4", () => {
144
+ const messages: BaseMessage[] = Array.from({ length: 8 }, (_, i) =>
145
+ new HumanMessage({
146
+ content: [
147
+ { type: "text", text: `msg ${i}` },
148
+ cacheBlock,
149
+ ],
150
+ }),
151
+ );
152
+ const hook = appendCachePoint(cacheBlock);
153
+ const result = applyHook(messages, hook);
154
+
155
+ expect(countCacheBlocks(result)).toBe(4);
156
+ });
157
+
158
+ it("preserves non-cache content blocks when stripping", () => {
159
+ const messages: BaseMessage[] = [
160
+ new HumanMessage({
161
+ content: [
162
+ { type: "text", text: "keep me" },
163
+ cacheBlock,
164
+ { type: "image_url", image_url: { url: "http://example.com" } },
165
+ ],
166
+ }),
167
+ new HumanMessage({
168
+ content: [
169
+ { type: "text", text: "msg 1" },
170
+ cacheBlock,
171
+ ],
172
+ }),
173
+ new HumanMessage({
174
+ content: [
175
+ { type: "text", text: "msg 2" },
176
+ cacheBlock,
177
+ ],
178
+ }),
179
+ new HumanMessage({
180
+ content: [
181
+ { type: "text", text: "msg 3" },
182
+ cacheBlock,
183
+ ],
184
+ }),
185
+ new HumanMessage("last"),
186
+ ];
187
+ const hook = appendCachePoint(cacheBlock, { maxBlocks: 4 });
188
+ const result = applyHook(messages, hook);
189
+
190
+ const first = messageAt(result, 0).content as Array<{ type: string }>;
191
+ expect(first.some((b) => b.type === "text")).toBe(true);
192
+ expect(first.some((b) => b.type === "image_url")).toBe(true);
193
+ expect(first.some((b) => b.type === cacheBlock.type)).toBe(false);
194
+ });
195
+ });
@@ -4,32 +4,49 @@ type ContentBlock = MessageContent extends (infer U)[] | string ? U : never;
4
4
 
5
5
  /**
6
6
  * Creates an `onPreparedMessage` hook that appends a cache-point content
7
- * block to the last message in the thread.
7
+ * block to the last message in the thread, and strips excess cache-point
8
+ * blocks from earlier messages so the total never exceeds `maxBlocks`.
8
9
  *
9
- * Skips appending if the last message already contains a block with the
10
- * same `type`.
10
+ * Older cache-point blocks are removed first, keeping the most recent
11
+ * `maxBlocks - 1` positions plus the last message's block.
11
12
  */
12
13
  export function appendCachePoint(
13
14
  block: ContentBlock,
15
+ { maxBlocks = 4 }: { maxBlocks?: number } = {},
14
16
  ): (message: BaseMessage, index: number, messages: readonly BaseMessage[]) => BaseMessage {
15
17
  return (message, index, messages) => {
16
- if (index !== messages.length - 1) {
18
+ const isLast = index === messages.length - 1;
19
+
20
+ if (isLast) {
21
+ const { content } = message;
22
+ if (Array.isArray(content)) {
23
+ if (content.some((b) => b.type === block.type)) return message;
24
+ message.content = [...content, block];
25
+ } else if (typeof content === "string") {
26
+ message.content = [{ type: "text", text: content }, block] satisfies MessageContent;
27
+ }
17
28
  return message;
18
29
  }
19
30
 
20
31
  const { content } = message;
32
+ if (!Array.isArray(content) || !content.some((b) => b.type === block.type)) {
33
+ return message;
34
+ }
21
35
 
22
- if (Array.isArray(content)) {
23
- if (content.some((b) => b.type === block.type)) {
24
- return message;
36
+ // Count cache blocks in messages after this one (excluding the last,
37
+ // which always gets one) plus 1 for the last message itself.
38
+ let cacheBlocksAfter = 1;
39
+ for (let i = index + 1; i < messages.length - 1; i++) {
40
+ const msg = messages[i];
41
+ if (!msg) continue;
42
+ const c = msg.content;
43
+ if (Array.isArray(c) && c.some((b: ContentBlock) => b.type === block.type)) {
44
+ cacheBlocksAfter++;
25
45
  }
26
- message.content = [...content, block];
27
- return message;
28
46
  }
29
47
 
30
- if (typeof content === "string") {
31
- message.content = [{ type: "text", text: content }, block] satisfies MessageContent;
32
- return message;
48
+ if (cacheBlocksAfter >= maxBlocks) {
49
+ message.content = content.filter((b) => b.type !== block.type);
33
50
  }
34
51
 
35
52
  return message;
package/src/index.ts CHANGED
@@ -59,6 +59,13 @@ export type { AgentStateContext } from "./lib/activity";
59
59
  export { SandboxManager } from "./lib/sandbox/manager";
60
60
  export { NodeFsSandboxFileSystem } from "./lib/sandbox/node-fs";
61
61
 
62
+ // Virtual filesystem (activity-side)
63
+ export { VirtualFileSystem } from "./lib/virtual-fs/filesystem";
64
+ export { withVirtualFs } from "./lib/virtual-fs/with-virtual-fs";
65
+ export { createVirtualFsActivities } from "./lib/virtual-fs/manager";
66
+ export type { FileTreeAccessor } from "./lib/virtual-fs/queries";
67
+ export type { VirtualFsContext } from "./lib/virtual-fs/types";
68
+
62
69
  // Tool handlers (activity implementations)
63
70
  // Wrap sandbox handlers with withSandbox(manager, handler) at registration time
64
71
  export { bashHandler } from "./tools/bash/handler";
@@ -0,0 +1,117 @@
1
+ import { proxySinks } from "@temporalio/workflow";
2
+ import type { ZeitlichObservabilitySinks } from "./sinks";
3
+ import type {
4
+ SessionStartHook,
5
+ SessionEndHook,
6
+ } from "../hooks/types";
7
+ import type {
8
+ PostToolUseHook,
9
+ PostToolUseFailureHook,
10
+ } from "../tool-router/types";
11
+
12
+ export interface ObservabilityHooks {
13
+ onSessionStart: SessionStartHook;
14
+ onSessionEnd: SessionEndHook;
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ onPostToolUse: PostToolUseHook<any, any>;
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ onPostToolUseFailure: PostToolUseFailureHook<any>;
19
+ }
20
+
21
+ /**
22
+ * Creates session hooks that emit agent lifecycle events to
23
+ * {@link ZeitlichObservabilitySinks}.
24
+ *
25
+ * The returned hooks call `proxySinks()` once and forward each event to
26
+ * the `zeitlichMetrics` sink. If the sink is not registered on the Worker,
27
+ * calls are silently dropped by the Temporal runtime.
28
+ *
29
+ * Combine with your own hooks using spread or {@link composeHooks}:
30
+ *
31
+ * ```typescript
32
+ * const session = await createSession({
33
+ * hooks: {
34
+ * ...createObservabilityHooks("myAgent"),
35
+ * // additional hooks can be composed via composeHooks()
36
+ * },
37
+ * });
38
+ * ```
39
+ *
40
+ * @param agentName - Agent name attached to every emitted event
41
+ */
42
+ export function createObservabilityHooks(agentName: string): ObservabilityHooks {
43
+ const { zeitlichMetrics } = proxySinks<ZeitlichObservabilitySinks>();
44
+ let sessionStartMs = Date.now();
45
+
46
+ return {
47
+ onSessionStart: (ctx) => {
48
+ sessionStartMs = Date.now();
49
+ zeitlichMetrics.sessionStarted({
50
+ agentName,
51
+ threadId: ctx.threadId,
52
+ metadata: ctx.metadata,
53
+ });
54
+ },
55
+
56
+ onSessionEnd: (ctx) => {
57
+ zeitlichMetrics.sessionEnded({
58
+ agentName,
59
+ threadId: ctx.threadId,
60
+ exitReason: ctx.exitReason,
61
+ turns: ctx.turns,
62
+ usage: {},
63
+ durationMs: Date.now() - sessionStartMs,
64
+ });
65
+ },
66
+
67
+ onPostToolUse: (ctx) => {
68
+ zeitlichMetrics.toolExecuted({
69
+ agentName,
70
+ toolName: ctx.toolCall.name,
71
+ durationMs: ctx.durationMs,
72
+ success: true,
73
+ threadId: ctx.threadId,
74
+ turn: ctx.turn,
75
+ });
76
+ },
77
+
78
+ onPostToolUseFailure: (ctx) => {
79
+ zeitlichMetrics.toolExecuted({
80
+ agentName,
81
+ toolName: ctx.toolCall.name,
82
+ durationMs: 0,
83
+ success: false,
84
+ threadId: ctx.threadId,
85
+ turn: ctx.turn,
86
+ });
87
+ return {};
88
+ },
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Compose multiple hook functions for the same lifecycle event into one.
94
+ *
95
+ * Each hook is called sequentially in order. Return values from
96
+ * `onPreToolUse` / `onPostToolUseFailure` use the **last** non-undefined
97
+ * result (later hooks can override earlier ones).
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const obs = createObservabilityHooks("myAgent");
102
+ * const hooks = {
103
+ * onSessionEnd: composeHooks(obs.onSessionEnd, myCustomEndHook),
104
+ * };
105
+ * ```
106
+ */
107
+ export function composeHooks<TArgs extends unknown[], TReturn>(
108
+ ...fns: ((...args: TArgs) => TReturn | Promise<TReturn>)[]
109
+ ): (...args: TArgs) => Promise<TReturn> {
110
+ return async (...args: TArgs): Promise<TReturn> => {
111
+ let lastResult!: TReturn;
112
+ for (const fn of fns) {
113
+ lastResult = await fn(...args);
114
+ }
115
+ return lastResult;
116
+ };
117
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ createObservabilityHooks,
3
+ composeHooks,
4
+ } from "./hooks";
5
+ export type { ObservabilityHooks } from "./hooks";
6
+
7
+ export type {
8
+ ZeitlichObservabilitySinks,
9
+ SessionStartedEvent,
10
+ SessionEndedEvent,
11
+ TurnCompletedEvent,
12
+ ToolExecutedEvent,
13
+ } from "./sinks";
@@ -0,0 +1,88 @@
1
+ import type { Sinks } from "@temporalio/workflow";
2
+ import type { TokenUsage, SessionExitReason } from "../types";
3
+
4
+ // ============================================================================
5
+ // Sink Event Types
6
+ // ============================================================================
7
+
8
+ export interface SessionStartedEvent {
9
+ agentName: string;
10
+ threadId: string;
11
+ metadata: Record<string, unknown>;
12
+ }
13
+
14
+ export interface SessionEndedEvent {
15
+ agentName: string;
16
+ threadId: string;
17
+ exitReason: SessionExitReason;
18
+ turns: number;
19
+ usage: TokenUsage;
20
+ durationMs: number;
21
+ }
22
+
23
+ export interface TurnCompletedEvent {
24
+ agentName: string;
25
+ threadId: string;
26
+ turn: number;
27
+ toolCallCount: number;
28
+ usage?: TokenUsage;
29
+ }
30
+
31
+ export interface ToolExecutedEvent {
32
+ agentName: string;
33
+ toolName: string;
34
+ durationMs: number;
35
+ success: boolean;
36
+ threadId: string;
37
+ turn: number;
38
+ }
39
+
40
+ // ============================================================================
41
+ // Sink Interface
42
+ // ============================================================================
43
+
44
+ /**
45
+ * Temporal Sinks interface for zeitlich agent observability.
46
+ *
47
+ * Sinks bridge the workflow sandbox to the Node.js environment, allowing
48
+ * consumers to emit metrics (Prometheus, Datadog, OpenTelemetry, etc.)
49
+ * from agent lifecycle events without breaking determinism.
50
+ *
51
+ * Register on the Worker via `InjectedSinks<ZeitlichObservabilitySinks>`:
52
+ *
53
+ * ```typescript
54
+ * import { Worker, InjectedSinks } from "@temporalio/worker";
55
+ * import type { ZeitlichObservabilitySinks } from "zeitlich/workflow";
56
+ *
57
+ * const sinks: InjectedSinks<ZeitlichObservabilitySinks> = {
58
+ * zeitlichMetrics: {
59
+ * sessionStarted: {
60
+ * fn(workflowInfo, event) { counter.inc({ agent: event.agentName }); },
61
+ * callDuringReplay: false,
62
+ * },
63
+ * sessionEnded: {
64
+ * fn(workflowInfo, event) { histogram.observe(event.durationMs); },
65
+ * callDuringReplay: false,
66
+ * },
67
+ * turnCompleted: {
68
+ * fn(workflowInfo, event) { gauge.set(event.turn); },
69
+ * callDuringReplay: false,
70
+ * },
71
+ * toolExecuted: {
72
+ * fn(workflowInfo, event) { histogram.observe({ tool: event.toolName }, event.durationMs); },
73
+ * callDuringReplay: false,
74
+ * },
75
+ * },
76
+ * };
77
+ *
78
+ * const worker = await Worker.create({ sinks, ... });
79
+ * ```
80
+ */
81
+ export interface ZeitlichObservabilitySinks extends Sinks {
82
+ zeitlichMetrics: {
83
+ sessionStarted(event: SessionStartedEvent): void;
84
+ sessionEnded(event: SessionEndedEvent): void;
85
+ turnCompleted(event: TurnCompletedEvent): void;
86
+ toolExecuted(event: ToolExecutedEvent): void;
87
+ };
88
+ }
@@ -80,9 +80,9 @@ export class SandboxManager<
80
80
  * manager.createActivities("CodingAgent");
81
81
  * // registers: inMemoryCodingAgentCreateSandbox, inMemoryCodingAgentDestroySandbox, …
82
82
  *
83
- * const vmgr = new SandboxManager(new VirtualSandboxProvider(resolver));
84
- * vmgr.createActivities("CodingAgent");
85
- * // registers: virtualCodingAgentCreateSandbox, …
83
+ * const dmgr = new SandboxManager(new DaytonaSandboxProvider(config));
84
+ * dmgr.createActivities("CodingAgent");
85
+ * // registers: daytonaCodingAgentCreateSandbox, …
86
86
  * ```
87
87
  */
88
88
  createActivities<S extends string>(
@@ -42,6 +42,7 @@ vi.mock("@temporalio/workflow", () => {
42
42
  uuid4: () =>
43
43
  `00000000-0000-0000-0000-${String(++idCounter).padStart(12, "0")}`,
44
44
  ApplicationFailure: MockApplicationFailure,
45
+ log: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
45
46
  };
46
47
  });
47
48
 
@@ -46,6 +46,7 @@ vi.mock("@temporalio/workflow", () => {
46
46
  uuid4: () =>
47
47
  `00000000-0000-0000-0000-${String(++idCounter).padStart(12, "0")}`,
48
48
  ApplicationFailure: MockApplicationFailure,
49
+ log: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
49
50
  };
50
51
  });
51
52