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.
- package/README.md +121 -13
- package/dist/{activities-DE3_q9yq.d.ts → activities-1xrWRrGJ.d.cts} +2 -3
- package/dist/{activities-p8PDlRIK.d.cts → activities-DOViDCTE.d.ts} +2 -3
- package/dist/adapters/sandbox/e2b/index.cjs +230 -0
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -0
- package/dist/adapters/sandbox/e2b/index.d.cts +77 -0
- package/dist/adapters/sandbox/e2b/index.d.ts +77 -0
- package/dist/adapters/sandbox/e2b/index.js +227 -0
- package/dist/adapters/sandbox/e2b/index.js.map +1 -0
- package/dist/adapters/sandbox/{virtual → e2b}/workflow.cjs +8 -8
- package/dist/adapters/sandbox/e2b/workflow.cjs.map +1 -0
- package/dist/adapters/sandbox/e2b/workflow.d.cts +27 -0
- package/dist/adapters/sandbox/e2b/workflow.d.ts +27 -0
- package/dist/adapters/sandbox/{virtual → e2b}/workflow.js +8 -8
- package/dist/adapters/sandbox/e2b/workflow.js.map +1 -0
- package/dist/adapters/thread/anthropic/index.d.cts +5 -6
- package/dist/adapters/thread/anthropic/index.d.ts +5 -6
- package/dist/adapters/thread/anthropic/workflow.d.cts +4 -5
- package/dist/adapters/thread/anthropic/workflow.d.ts +4 -5
- package/dist/adapters/thread/google-genai/index.d.cts +5 -6
- package/dist/adapters/thread/google-genai/index.d.ts +5 -6
- package/dist/adapters/thread/google-genai/workflow.d.cts +4 -5
- package/dist/adapters/thread/google-genai/workflow.d.ts +4 -5
- package/dist/adapters/thread/langchain/index.cjs +22 -10
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +12 -10
- package/dist/adapters/thread/langchain/index.d.ts +12 -10
- package/dist/adapters/thread/langchain/index.js +22 -10
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +4 -5
- package/dist/adapters/thread/langchain/workflow.d.ts +4 -5
- package/dist/index.cjs +526 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -15
- package/dist/index.d.ts +66 -15
- package/dist/index.js +522 -26
- package/dist/index.js.map +1 -1
- package/dist/{proxy-BK1ydQt0.d.ts → proxy-78nc985d.d.ts} +1 -1
- package/dist/{proxy-BMAsMHdp.d.cts → proxy-Bm2UTiO_.d.cts} +1 -1
- package/dist/{thread-manager-dzaJHQEA.d.ts → thread-manager-07BaYu_z.d.ts} +1 -1
- package/dist/{thread-manager-Bz8txKKj.d.cts → thread-manager-BRE5KkHB.d.cts} +1 -1
- package/dist/{thread-manager-BlHua5_v.d.cts → thread-manager-CatBkarc.d.cts} +1 -1
- package/dist/{thread-manager-Bh9x847n.d.ts → thread-manager-CxbWo7q_.d.ts} +1 -1
- package/dist/types-BkVoEyiH.d.ts +1211 -0
- package/dist/{types-CIkYBoF8.d.ts → types-DAv_SLN8.d.ts} +1 -1
- package/dist/{types-BfIQABzu.d.cts → types-Dpz2gXLk.d.cts} +1 -1
- package/dist/types-seDYom4M.d.cts +1211 -0
- package/dist/workflow-B4T3la0p.d.cts +750 -0
- package/dist/workflow-DCmaXLZ_.d.ts +750 -0
- package/dist/workflow.cjs +198 -21
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +5 -579
- package/dist/workflow.d.ts +5 -579
- package/dist/workflow.js +197 -23
- package/dist/workflow.js.map +1 -1
- package/package.json +23 -23
- package/src/adapters/sandbox/{virtual → e2b}/proxy.ts +16 -13
- package/src/adapters/thread/langchain/hooks.test.ts +195 -0
- package/src/adapters/thread/langchain/hooks.ts +29 -12
- package/src/index.ts +7 -0
- package/src/lib/observability/hooks.ts +117 -0
- package/src/lib/observability/index.ts +13 -0
- package/src/lib/observability/sinks.ts +88 -0
- package/src/lib/sandbox/manager.ts +3 -3
- package/src/lib/session/session-edge-cases.integration.test.ts +1 -0
- package/src/lib/session/session.integration.test.ts +1 -0
- package/src/lib/session/session.ts +79 -5
- package/src/lib/session/types.ts +21 -0
- package/src/lib/state/manager.integration.test.ts +1 -0
- package/src/lib/subagent/handler.ts +22 -2
- package/src/lib/subagent/register.ts +7 -2
- package/src/lib/subagent/subagent.integration.test.ts +1 -0
- package/src/lib/tool-router/router-edge-cases.integration.test.ts +2 -0
- package/src/lib/tool-router/router.integration.test.ts +2 -0
- package/src/lib/tool-router/router.ts +26 -7
- package/src/lib/tool-router/types.ts +0 -4
- package/src/{adapters/sandbox/virtual → lib/virtual-fs}/filesystem.ts +4 -4
- package/src/lib/virtual-fs/index.ts +18 -0
- package/src/lib/virtual-fs/manager.ts +48 -0
- package/src/lib/virtual-fs/proxy.ts +45 -0
- package/src/{adapters/sandbox/virtual → lib/virtual-fs}/types.ts +43 -33
- package/src/{adapters/sandbox/virtual/virtual-sandbox.test.ts → lib/virtual-fs/virtual-fs.test.ts} +15 -130
- package/src/lib/virtual-fs/with-virtual-fs.ts +94 -0
- package/src/workflow.ts +25 -8
- package/tsup.config.ts +3 -2
- package/dist/adapters/sandbox/virtual/index.cjs +0 -487
- package/dist/adapters/sandbox/virtual/index.cjs.map +0 -1
- package/dist/adapters/sandbox/virtual/index.d.cts +0 -90
- package/dist/adapters/sandbox/virtual/index.d.ts +0 -90
- package/dist/adapters/sandbox/virtual/index.js +0 -479
- package/dist/adapters/sandbox/virtual/index.js.map +0 -1
- package/dist/adapters/sandbox/virtual/workflow.cjs.map +0 -1
- package/dist/adapters/sandbox/virtual/workflow.d.cts +0 -28
- package/dist/adapters/sandbox/virtual/workflow.d.ts +0 -28
- package/dist/adapters/sandbox/virtual/workflow.js.map +0 -1
- package/dist/queries-BCgJ9Sr5.d.ts +0 -44
- package/dist/queries-DwnE2bu3.d.cts +0 -44
- package/dist/types-CvJyXDYt.d.ts +0 -490
- package/dist/types-DFUNSYbj.d.ts +0 -125
- package/dist/types-DRnz-OZp.d.cts +0 -125
- package/dist/types-DSOefLpY.d.cts +0 -490
- package/dist/types-mCVxKIZb.d.cts +0 -585
- package/dist/types-mCVxKIZb.d.ts +0 -585
- package/src/adapters/sandbox/virtual/index.ts +0 -92
- package/src/adapters/sandbox/virtual/provider.ts +0 -121
- package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +0 -97
- /package/src/{adapters/sandbox/virtual → lib/virtual-fs}/mutations.ts +0 -0
- /package/src/{adapters/sandbox/virtual → lib/virtual-fs}/queries.ts +0 -0
- /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.
|
|
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.
|
|
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": "^
|
|
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
|
|
2
|
+
* Workflow-safe proxy for E2B sandbox operations.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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 {
|
|
15
|
+
* import { proxyE2bSandboxOps } from 'zeitlich/adapters/sandbox/e2b/workflow';
|
|
13
16
|
*
|
|
14
|
-
* const sandbox =
|
|
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 {
|
|
22
|
+
import type { E2bSandboxCreateOptions } from "./types";
|
|
20
23
|
|
|
21
|
-
const ADAPTER_PREFIX = "
|
|
24
|
+
const ADAPTER_PREFIX = "e2b";
|
|
22
25
|
|
|
23
|
-
export function
|
|
26
|
+
export function proxyE2bSandboxOps(
|
|
24
27
|
scope?: string,
|
|
25
28
|
options?: Parameters<typeof proxyActivities>[0]
|
|
26
|
-
): SandboxOps
|
|
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: "
|
|
35
|
+
startToCloseTimeout: "120s",
|
|
33
36
|
retry: {
|
|
34
37
|
maximumAttempts: 3,
|
|
35
|
-
initialInterval: "
|
|
36
|
-
maximumInterval: "
|
|
37
|
-
backoffCoefficient:
|
|
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<
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 (
|
|
31
|
-
message.content =
|
|
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
|
|
84
|
-
*
|
|
85
|
-
* // registers:
|
|
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
|
|