zeitlich 0.2.49 → 0.2.51
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 +26 -23
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +3 -3
- package/dist/adapters/sandbox/daytona/index.d.ts +3 -3
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/daytona/workflow.d.ts +2 -2
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
- package/dist/adapters/sandbox/e2b/index.js.map +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
- package/dist/adapters/thread/anthropic/index.cjs +60 -55
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +20 -15
- package/dist/adapters/thread/anthropic/index.d.ts +20 -15
- package/dist/adapters/thread/anthropic/index.js +60 -55
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +7 -7
- package/dist/adapters/thread/anthropic/workflow.d.ts +7 -7
- package/dist/adapters/thread/google-genai/index.cjs +135 -66
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +200 -26
- package/dist/adapters/thread/google-genai/index.d.ts +200 -26
- package/dist/adapters/thread/google-genai/index.js +135 -66
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +8 -8
- package/dist/adapters/thread/google-genai/workflow.d.ts +8 -8
- package/dist/adapters/thread/langchain/index.cjs +67 -55
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +20 -15
- package/dist/adapters/thread/langchain/index.d.ts +20 -15
- package/dist/adapters/thread/langchain/index.js +67 -55
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +7 -7
- package/dist/adapters/thread/langchain/workflow.d.ts +7 -7
- package/dist/{cold-store-DKMAO1Dd.d.ts → cold-store-DyHodfAB.d.ts} +1 -1
- package/dist/{cold-store-CkWoNtMh.d.cts → cold-store-YOx9nmgR.d.cts} +1 -1
- package/dist/index.cjs +15050 -420
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -83
- package/dist/index.d.ts +79 -83
- package/dist/index.js +15051 -417
- package/dist/index.js.map +1 -1
- package/dist/{proxy-B7CWEV-T.d.cts → proxy-2htgGQrc.d.cts} +1 -1
- package/dist/{proxy-ByFHMVRX.d.ts → proxy-CmiTP4pp.d.ts} +1 -1
- package/dist/{thread-manager-nK-WcFzM.d.ts → thread-manager-BJ5pz5Cx.d.cts} +6 -7
- package/dist/{thread-manager-7AW4rhfu.d.ts → thread-manager-BQAbrYXH.d.cts} +6 -7
- package/dist/{thread-manager-Cibe0X5m.d.cts → thread-manager-CcvltOuq.d.ts} +6 -7
- package/dist/{thread-manager-B9rtMEVn.d.cts → thread-manager-DHAbncHX.d.ts} +6 -7
- package/dist/{types-gVa5XCWD.d.ts → types-BQvXWcft.d.ts} +1 -1
- package/dist/{types-XUUFvrJ9.d.cts → types-BjdqxKYp.d.cts} +709 -709
- package/dist/{types-CJ7tCdl6.d.ts → types-D8W5TnSa.d.cts} +3 -3
- package/dist/{types-CJ7tCdl6.d.cts → types-D8W5TnSa.d.ts} +3 -3
- package/dist/{types-DO4Tkwxo.d.ts → types-DEbkLA06.d.ts} +3 -3
- package/dist/{types-DeVNWqlb.d.ts → types-DiI7mZhI.d.ts} +709 -709
- package/dist/{types-BR-k7h0e.d.cts → types-N_LTWe4b.d.cts} +3 -3
- package/dist/{types-CjY93AWZ.d.cts → types-OEN1xrFg.d.cts} +1 -1
- package/dist/{workflow-uhOIj9D-.d.ts → workflow-CcgD6EUB.d.cts} +34 -3
- package/dist/{workflow-KbGsxpfh.d.cts → workflow-DBjPOKBr.d.ts} +34 -3
- package/dist/workflow.cjs +15008 -377
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +3 -3
- package/dist/workflow.d.ts +3 -3
- package/dist/workflow.js +15009 -374
- package/dist/workflow.js.map +1 -1
- package/package.json +10 -37
- package/src/adapters/thread/anthropic/activities.test.ts +115 -0
- package/src/adapters/thread/anthropic/activities.ts +11 -19
- package/src/adapters/thread/anthropic/fork-transform.test.ts +17 -11
- package/src/adapters/thread/anthropic/model-invoker.test.ts +54 -3
- package/src/adapters/thread/anthropic/model-invoker.ts +11 -1
- package/src/adapters/thread/anthropic/thread-manager.test.ts +2 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +3 -4
- package/src/adapters/thread/google-genai/activities.test.ts +162 -0
- package/src/adapters/thread/google-genai/activities.ts +38 -15
- package/src/adapters/thread/google-genai/fork-transform.test.ts +17 -11
- package/src/adapters/thread/google-genai/model-invoker.test.ts +386 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +118 -23
- package/src/adapters/thread/google-genai/thread-manager.test.ts +2 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +3 -4
- package/src/adapters/thread/langchain/activities.test.ts +88 -0
- package/src/adapters/thread/langchain/activities.ts +15 -12
- package/src/adapters/thread/langchain/fork-transform.test.ts +17 -11
- package/src/adapters/thread/langchain/model-invoker.test.ts +74 -0
- package/src/adapters/thread/langchain/model-invoker.ts +16 -3
- package/src/adapters/thread/langchain/thread-manager.test.ts +2 -2
- package/src/adapters/thread/langchain/thread-manager.ts +3 -4
- package/src/index.ts +2 -2
- package/src/lib/sandbox/capability-types.test.ts +2 -2
- package/src/lib/sandbox/manager.ts +2 -6
- package/src/lib/sandbox/sandbox.test.ts +1 -1
- package/src/lib/sandbox/types.ts +2 -2
- package/src/lib/session/session.integration.test.ts +92 -0
- package/src/lib/session/session.ts +23 -11
- package/src/lib/thread/keys.test.ts +9 -9
- package/src/lib/thread/keys.ts +1 -1
- package/src/lib/thread/manager.test.ts +24 -14
- package/src/lib/thread/manager.ts +19 -23
- package/src/lib/thread/snapshot.test.ts +51 -43
- package/src/lib/thread/snapshot.ts +54 -32
- package/src/lib/thread/test-utils.ts +106 -59
- package/src/lib/thread/tiered.test.ts +1 -1
- package/src/lib/thread/types.ts +2 -2
- package/src/lib/tool-router/router.integration.test.ts +44 -0
- package/src/lib/tool-router/router.ts +140 -32
- package/src/lib/workflow.ts +49 -0
- package/src/{adapters/sandbox/inmemory/proxy.ts → test-utils/in-memory-sandbox-proxy.ts} +5 -16
- package/src/{adapters/sandbox/inmemory/index.ts → test-utils/in-memory-sandbox.ts} +11 -3
- package/src/tools/bash/bash.test.ts +1 -1
- package/src/tools/edit/handler.test.ts +1 -1
- package/tsup.config.ts +2 -4
- package/dist/activities-7OcT_vdR.d.cts +0 -162
- package/dist/activities-zG_FBoY2.d.ts +0 -162
- package/dist/adapters/sandbox/inmemory/index.cjs +0 -214
- package/dist/adapters/sandbox/inmemory/index.cjs.map +0 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +0 -40
- package/dist/adapters/sandbox/inmemory/index.d.ts +0 -40
- package/dist/adapters/sandbox/inmemory/index.js +0 -211
- package/dist/adapters/sandbox/inmemory/index.js.map +0 -1
- package/dist/adapters/sandbox/inmemory/workflow.cjs +0 -36
- package/dist/adapters/sandbox/inmemory/workflow.cjs.map +0 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.cts +0 -27
- package/dist/adapters/sandbox/inmemory/workflow.d.ts +0 -27
- package/dist/adapters/sandbox/inmemory/workflow.js +0 -34
- package/dist/adapters/sandbox/inmemory/workflow.js.map +0 -1
|
@@ -7,19 +7,35 @@
|
|
|
7
7
|
* picks it up directly.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type
|
|
10
|
+
import type { RedisClientType } from "redis";
|
|
11
11
|
import type { ColdThreadStore, ThreadSnapshot } from "./cold-store";
|
|
12
12
|
|
|
13
13
|
type Value = string | string[];
|
|
14
14
|
|
|
15
|
+
/** node-redis `SetOptions` subset the stub understands. */
|
|
16
|
+
interface FakeSetOptions {
|
|
17
|
+
EX?: number;
|
|
18
|
+
NX?: boolean;
|
|
19
|
+
expiration?: { type: "EX" | "PX" | "EXAT" | "PXAT"; value: number } | "KEEPTTL";
|
|
20
|
+
condition?: "NX" | "XX";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** node-redis accepts a single key or an array (`RedisVariadicArgument`). */
|
|
24
|
+
type Keys = string | string[];
|
|
25
|
+
const toKeys = (keys: Keys): string[] => (Array.isArray(keys) ? keys : [keys]);
|
|
26
|
+
|
|
15
27
|
/**
|
|
16
|
-
* Minimal in-memory
|
|
28
|
+
* Minimal in-memory node-redis stub covering the commands the thread
|
|
17
29
|
* manager + snapshot helpers use: get/set/del/exists/expire,
|
|
18
|
-
*
|
|
19
|
-
* script.
|
|
20
|
-
*
|
|
30
|
+
* lRange/rPush/lLen/lTrim, and the `eval`-based idempotent-append Lua
|
|
31
|
+
* script. Mirrors the node-redis (`redis`) v4+ API surface — camelCase
|
|
32
|
+
* commands, an options object for `set`, variadic-or-array keys for
|
|
33
|
+
* `del`/`exists`, and a `multi().execAsPipeline()` pipeline that rejects
|
|
34
|
+
* with a `MultiErrorReply`-shaped error when a queued command fails.
|
|
35
|
+
* Behaviour matches Redis closely enough for unit tests; TTLs are stored
|
|
36
|
+
* but never expire automatically.
|
|
21
37
|
*/
|
|
22
|
-
export function createFakeRedis():
|
|
38
|
+
export function createFakeRedis(): RedisClientType & {
|
|
23
39
|
_store: Map<string, Value>;
|
|
24
40
|
_ttls: Map<string, number>;
|
|
25
41
|
} {
|
|
@@ -48,56 +64,60 @@ export function createFakeRedis(): Redis & {
|
|
|
48
64
|
async set(
|
|
49
65
|
key: string,
|
|
50
66
|
value: string,
|
|
51
|
-
|
|
52
|
-
): Promise<"OK"> {
|
|
53
|
-
// NX guard: when the
|
|
67
|
+
options?: FakeSetOptions
|
|
68
|
+
): Promise<"OK" | null> {
|
|
69
|
+
// NX guard: when the condition is NX and the key already exists,
|
|
54
70
|
// Redis returns null. We follow the same contract for tests that
|
|
55
|
-
// need it
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
return null
|
|
71
|
+
// need it.
|
|
72
|
+
const nx = options?.NX === true || options?.condition === "NX";
|
|
73
|
+
if (nx && store.has(key)) {
|
|
74
|
+
return null;
|
|
59
75
|
}
|
|
60
76
|
store.set(key, String(value));
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
const ttl =
|
|
78
|
+
options?.EX ??
|
|
79
|
+
(options?.expiration && options.expiration !== "KEEPTTL"
|
|
80
|
+
? options.expiration.value
|
|
81
|
+
: undefined);
|
|
82
|
+
if (typeof ttl === "number") {
|
|
83
|
+
ttls.set(key, ttl);
|
|
64
84
|
}
|
|
65
85
|
return "OK";
|
|
66
86
|
},
|
|
67
|
-
async del(
|
|
87
|
+
async del(keys: Keys): Promise<number> {
|
|
68
88
|
let removed = 0;
|
|
69
|
-
for (const k of keys) {
|
|
89
|
+
for (const k of toKeys(keys)) {
|
|
70
90
|
if (store.delete(k)) removed++;
|
|
71
91
|
ttls.delete(k);
|
|
72
92
|
}
|
|
73
93
|
return removed;
|
|
74
94
|
},
|
|
75
|
-
async exists(
|
|
76
|
-
return keys.reduce((acc, k) => acc + (store.has(k) ? 1 : 0), 0);
|
|
95
|
+
async exists(keys: Keys): Promise<number> {
|
|
96
|
+
return toKeys(keys).reduce((acc, k) => acc + (store.has(k) ? 1 : 0), 0);
|
|
77
97
|
},
|
|
78
98
|
async expire(key: string, ttl: number): Promise<number> {
|
|
79
99
|
if (!store.has(key)) return 0;
|
|
80
100
|
ttls.set(key, ttl);
|
|
81
101
|
return 1;
|
|
82
102
|
},
|
|
83
|
-
async
|
|
103
|
+
async lRange(key: string, start: number, end: number): Promise<string[]> {
|
|
84
104
|
if (!store.has(key)) return [];
|
|
85
105
|
if (!isList(key)) return [];
|
|
86
106
|
const list = store.get(key) as string[];
|
|
87
107
|
const last = end === -1 ? list.length - 1 : end;
|
|
88
108
|
return list.slice(start, last + 1);
|
|
89
109
|
},
|
|
90
|
-
async
|
|
110
|
+
async rPush(key: string, element: Keys): Promise<number> {
|
|
91
111
|
const list = ensureList(key);
|
|
92
|
-
list.push(...
|
|
112
|
+
list.push(...toKeys(element));
|
|
93
113
|
return list.length;
|
|
94
114
|
},
|
|
95
|
-
async
|
|
115
|
+
async lLen(key: string): Promise<number> {
|
|
96
116
|
if (!store.has(key)) return 0;
|
|
97
117
|
const list = store.get(key) as string[];
|
|
98
118
|
return list.length;
|
|
99
119
|
},
|
|
100
|
-
async
|
|
120
|
+
async lTrim(key: string, start: number, end: number): Promise<"OK"> {
|
|
101
121
|
if (!store.has(key)) return "OK";
|
|
102
122
|
const list = store.get(key) as string[];
|
|
103
123
|
const last = end === -1 ? list.length - 1 : end;
|
|
@@ -106,12 +126,11 @@ export function createFakeRedis(): Redis & {
|
|
|
106
126
|
},
|
|
107
127
|
async eval(
|
|
108
128
|
_script: string,
|
|
109
|
-
|
|
110
|
-
...args: (string | number)[]
|
|
129
|
+
options: { keys?: string[]; arguments?: string[] }
|
|
111
130
|
): Promise<number> {
|
|
112
131
|
// Mirrors APPEND_IDEMPOTENT_SCRIPT in src/lib/thread/manager.ts.
|
|
113
|
-
const keys =
|
|
114
|
-
const argv =
|
|
132
|
+
const keys = options.keys ?? [];
|
|
133
|
+
const argv = options.arguments ?? [];
|
|
115
134
|
const dedupKey = keys[0];
|
|
116
135
|
const listKey = keys[1];
|
|
117
136
|
const ttl = Number(argv[0]);
|
|
@@ -127,54 +146,64 @@ export function createFakeRedis(): Redis & {
|
|
|
127
146
|
ttls.set(dedupKey, ttl);
|
|
128
147
|
return 1;
|
|
129
148
|
},
|
|
130
|
-
// Chainable
|
|
131
|
-
// sync fake methods on `.
|
|
132
|
-
// semantics stay identical to the non-pipelined path.
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
149
|
+
// Chainable `multi()` stub. Defers each command to the underlying
|
|
150
|
+
// sync fake methods on `.execAsPipeline()`, so TTL tracking and store
|
|
151
|
+
// semantics stay identical to the non-pipelined path. Mirrors
|
|
152
|
+
// node-redis: per-command failures reject the pipeline with a
|
|
153
|
+
// `MultiErrorReply`-shaped error (`{ replies, errorIndexes }`).
|
|
154
|
+
multi(): FakeMulti {
|
|
136
155
|
const impl = fake as unknown as {
|
|
137
|
-
set: (
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
set: (
|
|
157
|
+
key: string,
|
|
158
|
+
value: string,
|
|
159
|
+
options?: FakeSetOptions
|
|
160
|
+
) => Promise<"OK" | null>;
|
|
161
|
+
del: (keys: Keys) => Promise<number>;
|
|
162
|
+
rPush: (key: string, element: Keys) => Promise<number>;
|
|
140
163
|
expire: (key: string, ttl: number) => Promise<number>;
|
|
141
164
|
};
|
|
142
165
|
const ops: Array<() => Promise<unknown>> = [];
|
|
143
|
-
const chain:
|
|
144
|
-
set: (
|
|
145
|
-
|
|
146
|
-
ops.push(() => impl.set(key, value, ...rest));
|
|
166
|
+
const chain: FakeMulti = {
|
|
167
|
+
set: (key, value, options) => {
|
|
168
|
+
ops.push(() => impl.set(key, value, options));
|
|
147
169
|
return chain;
|
|
148
170
|
},
|
|
149
|
-
del: (
|
|
150
|
-
ops.push(() => impl.del(
|
|
171
|
+
del: (keys) => {
|
|
172
|
+
ops.push(() => impl.del(keys));
|
|
151
173
|
return chain;
|
|
152
174
|
},
|
|
153
|
-
|
|
154
|
-
ops.push(() => impl.
|
|
175
|
+
rPush: (key, element) => {
|
|
176
|
+
ops.push(() => impl.rPush(key, element));
|
|
155
177
|
return chain;
|
|
156
178
|
},
|
|
157
179
|
expire: (key, ttl) => {
|
|
158
180
|
ops.push(() => impl.expire(key, ttl));
|
|
159
181
|
return chain;
|
|
160
182
|
},
|
|
161
|
-
|
|
162
|
-
const
|
|
183
|
+
execAsPipeline: async () => {
|
|
184
|
+
const replies: unknown[] = [];
|
|
185
|
+
const errorIndexes: number[] = [];
|
|
186
|
+
let i = 0;
|
|
163
187
|
for (const op of ops) {
|
|
164
188
|
try {
|
|
165
|
-
|
|
189
|
+
replies.push(await op());
|
|
166
190
|
} catch (e) {
|
|
167
|
-
|
|
191
|
+
replies.push(e);
|
|
192
|
+
errorIndexes.push(i);
|
|
168
193
|
}
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
if (errorIndexes.length > 0) {
|
|
197
|
+
throw makeMultiError(replies, errorIndexes);
|
|
169
198
|
}
|
|
170
|
-
return
|
|
199
|
+
return replies;
|
|
171
200
|
},
|
|
172
201
|
};
|
|
173
202
|
return chain;
|
|
174
203
|
},
|
|
175
204
|
_store: store,
|
|
176
205
|
_ttls: ttls,
|
|
177
|
-
} as unknown as
|
|
206
|
+
} as unknown as RedisClientType & {
|
|
178
207
|
_store: Map<string, Value>;
|
|
179
208
|
_ttls: Map<string, number>;
|
|
180
209
|
};
|
|
@@ -182,13 +211,31 @@ export function createFakeRedis(): Redis & {
|
|
|
182
211
|
return fake;
|
|
183
212
|
}
|
|
184
213
|
|
|
185
|
-
/** Minimal chainable surface used by the fake-redis
|
|
186
|
-
interface
|
|
187
|
-
set: (
|
|
188
|
-
del: (
|
|
189
|
-
|
|
190
|
-
expire: (key: string, ttl: number) =>
|
|
191
|
-
|
|
214
|
+
/** Minimal chainable surface used by the fake-redis `multi()` stub. */
|
|
215
|
+
interface FakeMulti {
|
|
216
|
+
set: (key: string, value: string, options?: FakeSetOptions) => FakeMulti;
|
|
217
|
+
del: (keys: Keys) => FakeMulti;
|
|
218
|
+
rPush: (key: string, element: Keys) => FakeMulti;
|
|
219
|
+
expire: (key: string, ttl: number) => FakeMulti;
|
|
220
|
+
execAsPipeline: () => Promise<unknown[]>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build a node-redis `MultiErrorReply`-shaped error: an `Error` carrying
|
|
225
|
+
* `replies` (per-command results, with failures as `Error`s) and
|
|
226
|
+
* `errorIndexes`. `applySnapshot` unwraps this to surface the first real
|
|
227
|
+
* error.
|
|
228
|
+
*/
|
|
229
|
+
export function makeMultiError(
|
|
230
|
+
replies: unknown[],
|
|
231
|
+
errorIndexes: number[]
|
|
232
|
+
): Error & { replies: unknown[]; errorIndexes: number[] } {
|
|
233
|
+
return Object.assign(
|
|
234
|
+
new Error(
|
|
235
|
+
`${errorIndexes.length} commands failed, see .replies and .errorIndexes for more information`
|
|
236
|
+
),
|
|
237
|
+
{ replies, errorIndexes }
|
|
238
|
+
);
|
|
192
239
|
}
|
|
193
240
|
|
|
194
241
|
/**
|
package/src/lib/thread/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type { RedisClientType } from "redis";
|
|
2
2
|
import type { JsonValue, PersistedThreadState } from "../state/types";
|
|
3
3
|
export interface ThreadManagerConfig<T> {
|
|
4
|
-
redis:
|
|
4
|
+
redis: RedisClientType;
|
|
5
5
|
threadId: string;
|
|
6
6
|
/** Thread key, defaults to 'messages' */
|
|
7
7
|
key?: string;
|
|
@@ -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,10 +211,20 @@ 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
|
-
| {
|
|
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>,
|
|
@@ -222,7 +232,8 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
222
232
|
sandboxId?: string,
|
|
223
233
|
onRewindRequested?: (signal: RewindSignal) => void,
|
|
224
234
|
assistantMessageId?: string,
|
|
225
|
-
persistThreadState?: () => Promise<void
|
|
235
|
+
persistThreadState?: () => Promise<void>,
|
|
236
|
+
deferAppend?: boolean
|
|
226
237
|
): Promise<ProcessedToolCall> {
|
|
227
238
|
const startTime = Date.now();
|
|
228
239
|
const tool = toolMap.get(toolCall.name);
|
|
@@ -230,15 +241,26 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
230
241
|
// --- Pre-hooks: may skip or modify args ---
|
|
231
242
|
const preResult = await runPreHooks(toolCall, tool, turn);
|
|
232
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
|
+
}
|
|
233
258
|
await appendToolResult(uuid4(), {
|
|
234
259
|
threadId: options.threadId,
|
|
235
260
|
threadKey: options.threadKey,
|
|
236
261
|
toolCallId: toolCall.id,
|
|
237
262
|
toolName: toolCall.name,
|
|
238
|
-
content:
|
|
239
|
-
skipped: true,
|
|
240
|
-
reason: "Skipped by PreToolUse hook",
|
|
241
|
-
}),
|
|
263
|
+
content: skipContent,
|
|
242
264
|
});
|
|
243
265
|
return { kind: "skipped" };
|
|
244
266
|
}
|
|
@@ -314,19 +336,22 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
314
336
|
}
|
|
315
337
|
|
|
316
338
|
// --- Append result to thread (unless handler already did) ---
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
threadId: options.threadId,
|
|
320
|
-
threadKey: options.threadKey,
|
|
321
|
-
toolCallId: toolCall.id,
|
|
322
|
-
toolName: toolCall.name,
|
|
323
|
-
content,
|
|
324
|
-
};
|
|
339
|
+
const needsAppend = !resultAppended;
|
|
340
|
+
if (needsAppend && !deferAppend) {
|
|
325
341
|
await appendToolResult.executeWithOptions(
|
|
326
342
|
{
|
|
327
343
|
summary: `Append ${toolCall.name} result`,
|
|
328
344
|
},
|
|
329
|
-
[
|
|
345
|
+
[
|
|
346
|
+
uuid4(),
|
|
347
|
+
{
|
|
348
|
+
threadId: options.threadId,
|
|
349
|
+
threadKey: options.threadKey,
|
|
350
|
+
toolCallId: toolCall.id,
|
|
351
|
+
toolName: toolCall.name,
|
|
352
|
+
content,
|
|
353
|
+
},
|
|
354
|
+
]
|
|
330
355
|
);
|
|
331
356
|
}
|
|
332
357
|
|
|
@@ -356,7 +381,18 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
356
381
|
durationMs
|
|
357
382
|
);
|
|
358
383
|
|
|
359
|
-
return {
|
|
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
|
+
};
|
|
360
396
|
}
|
|
361
397
|
|
|
362
398
|
return {
|
|
@@ -409,7 +445,7 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
409
445
|
): Promise<ProcessToolCallsResult<TResults>> {
|
|
410
446
|
const attachRewind = (
|
|
411
447
|
arr: ToolCallResultUnion<TResults>[],
|
|
412
|
-
rewind: RewindSignal | undefined
|
|
448
|
+
rewind: RewindSignal | undefined
|
|
413
449
|
): ProcessToolCallsResult<TResults> => {
|
|
414
450
|
if (rewind) {
|
|
415
451
|
(arr as ProcessToolCallsResult<TResults>).rewind = rewind;
|
|
@@ -447,19 +483,55 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
447
483
|
sandboxId,
|
|
448
484
|
onRewindRequested,
|
|
449
485
|
assistantMessageId,
|
|
450
|
-
persistThreadState
|
|
486
|
+
persistThreadState,
|
|
487
|
+
true
|
|
451
488
|
)
|
|
452
489
|
)
|
|
453
490
|
)
|
|
454
491
|
);
|
|
455
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
|
+
|
|
456
531
|
const results: ToolCallResultUnion<TResults>[] = [];
|
|
457
532
|
for (const outcome of outcomes) {
|
|
458
533
|
if (outcome.status === "rejected") {
|
|
459
|
-
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
throw outcome.reason;
|
|
534
|
+
continue;
|
|
463
535
|
}
|
|
464
536
|
if (outcome.value.kind === "result") {
|
|
465
537
|
results.push(outcome.value.value);
|
|
@@ -502,8 +574,12 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
502
574
|
}
|
|
503
575
|
|
|
504
576
|
const processOne = async (
|
|
505
|
-
toolCall: ParsedToolCallUnion<T
|
|
506
|
-
|
|
577
|
+
toolCall: ParsedToolCallUnion<T>,
|
|
578
|
+
deferAppend?: boolean
|
|
579
|
+
): Promise<{
|
|
580
|
+
result: ToolCallResult<TName, TResult>;
|
|
581
|
+
pendingAppend?: PendingAppend;
|
|
582
|
+
}> => {
|
|
507
583
|
const routerContext: RouterContext = {
|
|
508
584
|
threadId: options.threadId,
|
|
509
585
|
...(options.threadKey && { threadKey: options.threadKey }),
|
|
@@ -524,7 +600,8 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
524
600
|
routerContext as Parameters<typeof handler>[1]
|
|
525
601
|
);
|
|
526
602
|
|
|
527
|
-
|
|
603
|
+
const needsAppend = !response.resultAppended;
|
|
604
|
+
if (needsAppend && !deferAppend) {
|
|
528
605
|
await appendToolResult.executeWithOptions(
|
|
529
606
|
{
|
|
530
607
|
summary: `Append ${toolCall.name} result`,
|
|
@@ -543,20 +620,51 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
543
620
|
}
|
|
544
621
|
|
|
545
622
|
return {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
+
}),
|
|
550
637
|
};
|
|
551
638
|
};
|
|
552
639
|
|
|
553
640
|
if (options.parallel) {
|
|
554
|
-
|
|
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);
|
|
555
662
|
}
|
|
556
663
|
|
|
557
664
|
const results: ToolCallResult<TName, TResult>[] = [];
|
|
558
665
|
for (const toolCall of matchingCalls) {
|
|
559
|
-
|
|
666
|
+
const { result } = await processOne(toolCall);
|
|
667
|
+
results.push(result);
|
|
560
668
|
}
|
|
561
669
|
return results;
|
|
562
670
|
},
|
package/src/lib/workflow.ts
CHANGED
|
@@ -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
|
};
|