zeitlich 0.2.37 → 0.2.39
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 +18 -0
- package/dist/{activities-Bb-nAjwQ.d.ts → activities-Bmu7XnaG.d.ts} +4 -4
- package/dist/{activities-vkI4_3CC.d.cts → activities-ByBFLvm2.d.cts} +4 -4
- package/dist/adapter-id-BB-mmrts.d.cts +17 -0
- package/dist/adapter-id-BB-mmrts.d.ts +17 -0
- package/dist/adapter-id-CMwVrVqv.d.cts +17 -0
- package/dist/adapter-id-CMwVrVqv.d.ts +17 -0
- package/dist/adapter-id-CbY2zeSt.d.cts +17 -0
- package/dist/adapter-id-CbY2zeSt.d.ts +17 -0
- package/dist/adapters/sandbox/bedrock/index.cjs +3 -3
- package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
- package/dist/adapters/sandbox/bedrock/index.d.cts +6 -6
- package/dist/adapters/sandbox/bedrock/index.d.ts +6 -6
- package/dist/adapters/sandbox/bedrock/index.js +3 -3
- package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
- package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
- package/dist/adapters/sandbox/daytona/index.cjs +3 -3
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +4 -4
- package/dist/adapters/sandbox/daytona/index.d.ts +4 -4
- package/dist/adapters/sandbox/daytona/index.js +3 -3
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
- package/dist/adapters/sandbox/e2b/index.cjs +26 -14
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +24 -4
- package/dist/adapters/sandbox/e2b/index.d.ts +24 -4
- package/dist/adapters/sandbox/e2b/index.js +26 -14
- 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/sandbox/inmemory/index.cjs +3 -3
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +4 -4
- package/dist/adapters/sandbox/inmemory/index.d.ts +4 -4
- package/dist/adapters/sandbox/inmemory/index.js +3 -3
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
- package/dist/adapters/thread/anthropic/index.cjs +150 -13
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +9 -8
- package/dist/adapters/thread/anthropic/index.d.ts +9 -8
- package/dist/adapters/thread/anthropic/index.js +150 -14
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.cjs +9 -3
- package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +6 -5
- package/dist/adapters/thread/anthropic/workflow.d.ts +6 -5
- package/dist/adapters/thread/anthropic/workflow.js +9 -4
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.cjs +154 -13
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +6 -5
- package/dist/adapters/thread/google-genai/index.d.ts +6 -5
- package/dist/adapters/thread/google-genai/index.js +154 -14
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.cjs +9 -3
- package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +6 -5
- package/dist/adapters/thread/google-genai/workflow.d.ts +6 -5
- package/dist/adapters/thread/google-genai/workflow.js +9 -4
- package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
- package/dist/adapters/thread/index.cjs +16 -0
- package/dist/adapters/thread/index.cjs.map +1 -0
- package/dist/adapters/thread/index.d.cts +34 -0
- package/dist/adapters/thread/index.d.ts +34 -0
- package/dist/adapters/thread/index.js +12 -0
- package/dist/adapters/thread/index.js.map +1 -0
- package/dist/adapters/thread/langchain/index.cjs +149 -14
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +9 -8
- package/dist/adapters/thread/langchain/index.d.ts +9 -8
- package/dist/adapters/thread/langchain/index.js +149 -15
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.cjs +9 -3
- package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +6 -5
- package/dist/adapters/thread/langchain/workflow.d.ts +6 -5
- package/dist/adapters/thread/langchain/workflow.js +9 -4
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/index.cjs +367 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +365 -61
- package/dist/index.js.map +1 -1
- package/dist/{proxy-DEtowJyd.d.cts → proxy-BAKzNGRq.d.cts} +1 -1
- package/dist/{proxy-0smGKvx8.d.ts → proxy-DO_MXbY4.d.ts} +1 -1
- package/dist/{thread-manager-C-C4pI2z.d.ts → thread-manager-CcRXasqs.d.ts} +2 -2
- package/dist/{thread-manager-D4vgzYrh.d.cts → thread-manager-ClwSaUnj.d.cts} +2 -2
- package/dist/{thread-manager-3fszQih4.d.ts → thread-manager-D-7lp1JK.d.ts} +2 -2
- package/dist/{thread-manager-CzYln2OC.d.cts → thread-manager-Y8Ucf0Tf.d.cts} +2 -2
- package/dist/{types-CPKDl-y_.d.ts → types-Bcbiq8iv.d.cts} +195 -22
- package/dist/{types-CNuWnvy9.d.ts → types-DAsQ21Rt.d.ts} +1 -1
- package/dist/{types-B37hKoWA.d.ts → types-DpHTX-iO.d.ts} +58 -1
- package/dist/{types-BO7Yju20.d.cts → types-Dt8-HBBT.d.ts} +195 -22
- package/dist/{types-D08CXPh8.d.cts → types-hFFi-Zd9.d.cts} +58 -1
- package/dist/{types-DWEUmYAJ.d.cts → types-lm8tMNJQ.d.cts} +1 -1
- package/dist/{types-tQL9njTu.d.cts → types-yx0LzPGn.d.cts} +21 -7
- package/dist/{types-tQL9njTu.d.ts → types-yx0LzPGn.d.ts} +21 -7
- package/dist/{workflow-CjXHbZZc.d.ts → workflow-Bmf9EtDW.d.ts} +83 -3
- package/dist/{workflow-Do_lzJpT.d.cts → workflow-Bx9utBwb.d.cts} +83 -3
- package/dist/workflow.cjs +266 -39
- 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 +264 -41
- package/dist/workflow.js.map +1 -1
- package/package.json +12 -2
- package/src/adapters/sandbox/bedrock/index.ts +12 -3
- package/src/adapters/sandbox/daytona/index.ts +12 -3
- package/src/adapters/sandbox/e2b/index.ts +36 -14
- package/src/adapters/sandbox/e2b/types.ts +16 -0
- package/src/adapters/sandbox/inmemory/index.ts +12 -3
- package/src/adapters/thread/adapter-id.test.ts +42 -0
- package/src/adapters/thread/anthropic/activities.ts +40 -5
- package/src/adapters/thread/anthropic/adapter-id.ts +16 -0
- package/src/adapters/thread/anthropic/fork-transform.test.ts +291 -0
- package/src/adapters/thread/anthropic/index.ts +3 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +7 -1
- package/src/adapters/thread/anthropic/proxy.ts +3 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +27 -1
- package/src/adapters/thread/google-genai/activities.ts +44 -5
- package/src/adapters/thread/google-genai/adapter-id.ts +16 -0
- package/src/adapters/thread/google-genai/fork-transform.test.ts +149 -0
- package/src/adapters/thread/google-genai/index.ts +3 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +8 -2
- package/src/adapters/thread/google-genai/proxy.ts +3 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +27 -1
- package/src/adapters/thread/index.ts +39 -0
- package/src/adapters/thread/langchain/activities.ts +40 -5
- package/src/adapters/thread/langchain/adapter-id.ts +16 -0
- package/src/adapters/thread/langchain/fork-transform.test.ts +142 -0
- package/src/adapters/thread/langchain/index.ts +3 -0
- package/src/adapters/thread/langchain/model-invoker.ts +7 -1
- package/src/adapters/thread/langchain/proxy.ts +3 -2
- package/src/adapters/thread/langchain/thread-manager.ts +27 -1
- package/src/lib/lifecycle.ts +14 -5
- package/src/lib/model/types.ts +7 -0
- package/src/lib/sandbox/manager.ts +26 -18
- package/src/lib/sandbox/types.ts +27 -7
- package/src/lib/session/session-edge-cases.integration.test.ts +336 -4
- package/src/lib/session/session.integration.test.ts +192 -2
- package/src/lib/session/session.ts +102 -8
- package/src/lib/session/types.ts +66 -3
- package/src/lib/state/index.ts +1 -0
- package/src/lib/state/manager.integration.test.ts +109 -0
- package/src/lib/state/manager.ts +38 -8
- package/src/lib/state/types.ts +25 -0
- package/src/lib/subagent/handler.ts +124 -11
- package/src/lib/subagent/index.ts +5 -1
- package/src/lib/subagent/subagent.integration.test.ts +628 -104
- package/src/lib/subagent/types.ts +63 -14
- package/src/lib/subagent/workflow.ts +29 -2
- package/src/lib/thread/index.ts +5 -0
- package/src/lib/thread/keys.test.ts +101 -0
- package/src/lib/thread/keys.ts +94 -0
- package/src/lib/thread/manager.test.ts +139 -0
- package/src/lib/thread/manager.ts +105 -9
- package/src/lib/thread/proxy.ts +3 -0
- package/src/lib/thread/types.ts +64 -1
- package/src/lib/tool-router/index.ts +2 -0
- package/src/lib/tool-router/router-edge-cases.integration.test.ts +92 -0
- package/src/lib/tool-router/router.integration.test.ts +12 -0
- package/src/lib/tool-router/router.ts +89 -16
- package/src/lib/tool-router/types.ts +42 -1
- package/src/lib/types.ts +12 -0
- package/src/workflow.ts +14 -1
- package/tsup.config.ts +1 -0
|
@@ -7,11 +7,16 @@ import {
|
|
|
7
7
|
} from "@temporalio/workflow";
|
|
8
8
|
import type { SessionExitReason } from "../types";
|
|
9
9
|
import type { SessionConfig, ZeitlichSession } from "./types";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
SandboxCreateOptions,
|
|
12
|
+
SandboxOps,
|
|
13
|
+
SandboxSnapshot,
|
|
14
|
+
} from "../sandbox/types";
|
|
11
15
|
import type {
|
|
12
16
|
AgentState,
|
|
13
17
|
AgentStateManager,
|
|
14
18
|
JsonSerializable,
|
|
19
|
+
PersistedThreadState,
|
|
15
20
|
} from "../state/types";
|
|
16
21
|
import { createToolRouter } from "../tool-router/router";
|
|
17
22
|
import type { ParsedToolCallUnion, ToolMap } from "../tool-router/types";
|
|
@@ -107,6 +112,7 @@ export async function createSession<
|
|
|
107
112
|
sandbox: sandboxInit,
|
|
108
113
|
sandboxShutdown = "destroy",
|
|
109
114
|
onSandboxReady,
|
|
115
|
+
onSessionExit,
|
|
110
116
|
virtualFs: virtualFsConfig,
|
|
111
117
|
virtualFsOps,
|
|
112
118
|
}: SessionConfig<T, M, TContent>): Promise<ZeitlichSession<M, boolean>> {
|
|
@@ -142,6 +148,8 @@ export async function createSession<
|
|
|
142
148
|
appendSystemMessage,
|
|
143
149
|
appendAgentMessage,
|
|
144
150
|
forkThread,
|
|
151
|
+
loadThreadState,
|
|
152
|
+
saveThreadState,
|
|
145
153
|
} = threadOps;
|
|
146
154
|
|
|
147
155
|
const plugins: ToolMap[string][] = [];
|
|
@@ -253,8 +261,14 @@ export async function createSession<
|
|
|
253
261
|
nonRetryable: true,
|
|
254
262
|
});
|
|
255
263
|
}
|
|
264
|
+
const forkInit = sandboxInit as {
|
|
265
|
+
mode: "fork";
|
|
266
|
+
sandboxId: string;
|
|
267
|
+
options?: SandboxCreateOptions;
|
|
268
|
+
};
|
|
256
269
|
sandboxId = await sandboxOps.forkSandbox(
|
|
257
|
-
|
|
270
|
+
forkInit.sandboxId,
|
|
271
|
+
forkInit.options
|
|
258
272
|
);
|
|
259
273
|
sandboxOwned = true;
|
|
260
274
|
} else if (sandboxMode === "from-snapshot") {
|
|
@@ -264,10 +278,15 @@ export async function createSession<
|
|
|
264
278
|
nonRetryable: true,
|
|
265
279
|
});
|
|
266
280
|
}
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
const restoreInit = sandboxInit as {
|
|
282
|
+
mode: "from-snapshot";
|
|
283
|
+
snapshot: SandboxSnapshot;
|
|
284
|
+
options?: SandboxCreateOptions;
|
|
285
|
+
};
|
|
286
|
+
sandboxId = await sandboxOps.restoreSandbox(
|
|
287
|
+
restoreInit.snapshot,
|
|
288
|
+
restoreInit.options
|
|
289
|
+
);
|
|
271
290
|
sandboxOwned = true;
|
|
272
291
|
} else if (sandboxOps) {
|
|
273
292
|
const skillFiles = skills ? collectSkillFiles(skills) : undefined;
|
|
@@ -298,7 +317,10 @@ export async function createSession<
|
|
|
298
317
|
}
|
|
299
318
|
|
|
300
319
|
if (sandboxId && sandboxOwned && onSandboxReady) {
|
|
301
|
-
onSandboxReady(
|
|
320
|
+
onSandboxReady({
|
|
321
|
+
sandboxId,
|
|
322
|
+
...(baseSnapshot && { baseSnapshot }),
|
|
323
|
+
});
|
|
302
324
|
}
|
|
303
325
|
|
|
304
326
|
// --- Virtual filesystem init (independent of sandbox) ----------------
|
|
@@ -350,10 +372,21 @@ export async function createSession<
|
|
|
350
372
|
const systemPrompt = stateManager.getSystemPrompt();
|
|
351
373
|
|
|
352
374
|
// --- Thread lifecycle: new, continue, or fork ----------------------
|
|
375
|
+
const rehydrateFromSlice = (slice: PersistedThreadState): void => {
|
|
376
|
+
stateManager.mergeUpdate({
|
|
377
|
+
tasks: new Map(slice.tasks),
|
|
378
|
+
...slice.custom,
|
|
379
|
+
} as Partial<AgentState<TState>>);
|
|
380
|
+
};
|
|
381
|
+
|
|
353
382
|
if (threadMode === "fork" && sourceThreadId) {
|
|
354
383
|
await forkThread(sourceThreadId, threadId, threadKey);
|
|
384
|
+
const forkedSlice = await loadThreadState(threadId, threadKey);
|
|
385
|
+
if (forkedSlice) rehydrateFromSlice(forkedSlice);
|
|
355
386
|
} else if (threadMode === "continue") {
|
|
356
387
|
// "continue" — thread already exists, just append the new message
|
|
388
|
+
const continuedSlice = await loadThreadState(threadId, threadKey);
|
|
389
|
+
if (continuedSlice) rehydrateFromSlice(continuedSlice);
|
|
357
390
|
} else {
|
|
358
391
|
if (appendSystemPrompt) {
|
|
359
392
|
if (
|
|
@@ -381,6 +414,13 @@ export async function createSession<
|
|
|
381
414
|
let finalMessage: M | null = null;
|
|
382
415
|
|
|
383
416
|
try {
|
|
417
|
+
// Per-turn assistant message id. Pre-generated in the workflow
|
|
418
|
+
// so the runAgent activity can truncate the thread from this id
|
|
419
|
+
// on entry (deterministic rewind + time-travel via Temporal
|
|
420
|
+
// workflow reset). On a rewind retry we keep the same id so the
|
|
421
|
+
// prior attempt's assistant + tool results are wiped by the next
|
|
422
|
+
// runAgent call.
|
|
423
|
+
let assistantId: string | undefined;
|
|
384
424
|
while (
|
|
385
425
|
stateManager.isRunning() &&
|
|
386
426
|
!stateManager.isTerminal() &&
|
|
@@ -393,14 +433,17 @@ export async function createSession<
|
|
|
393
433
|
|
|
394
434
|
stateManager.setTools(toolRouter.getToolDefinitions());
|
|
395
435
|
|
|
436
|
+
assistantId ??= uuid4();
|
|
437
|
+
|
|
396
438
|
const { message, rawToolCalls, usage } = await runAgent({
|
|
397
439
|
threadId,
|
|
398
440
|
threadKey,
|
|
399
441
|
agentName,
|
|
400
442
|
metadata,
|
|
443
|
+
assistantMessageId: assistantId,
|
|
401
444
|
});
|
|
402
445
|
|
|
403
|
-
await appendAgentMessage(threadId,
|
|
446
|
+
await appendAgentMessage(threadId, assistantId, message, threadKey);
|
|
404
447
|
|
|
405
448
|
if (usage) {
|
|
406
449
|
stateManager.updateUsage(usage);
|
|
@@ -452,6 +495,28 @@ export async function createSession<
|
|
|
452
495
|
}
|
|
453
496
|
}
|
|
454
497
|
|
|
498
|
+
const rewind = toolCallResults.rewind;
|
|
499
|
+
if (rewind) {
|
|
500
|
+
log.info("rewinding turn", {
|
|
501
|
+
agentName,
|
|
502
|
+
threadId,
|
|
503
|
+
turn: currentTurn,
|
|
504
|
+
toolCallId: rewind.toolCallId,
|
|
505
|
+
toolName: rewind.toolName,
|
|
506
|
+
});
|
|
507
|
+
// Keep the same assistantId for the retry. The next
|
|
508
|
+
// runAgent call will call truncateFromId(assistantId) on
|
|
509
|
+
// entry, wiping the bad assistant message + any already
|
|
510
|
+
// appended tool results before re-invoking the LLM. The
|
|
511
|
+
// turn counter is intentionally NOT rolled back — each
|
|
512
|
+
// rewind still consumes one of the `maxTurns` budget so a
|
|
513
|
+
// misbehaving tool cannot spin the session forever.
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Turn committed: fresh id for the next turn.
|
|
518
|
+
assistantId = undefined;
|
|
519
|
+
|
|
455
520
|
if (stateManager.getStatus() === "WAITING_FOR_INPUT") {
|
|
456
521
|
const conditionMet = await condition(
|
|
457
522
|
() => stateManager.getStatus() === "RUNNING",
|
|
@@ -485,6 +550,28 @@ export async function createSession<
|
|
|
485
550
|
});
|
|
486
551
|
throw ApplicationFailure.fromError(error);
|
|
487
552
|
} finally {
|
|
553
|
+
// Persist the task map + custom state slice alongside the thread so
|
|
554
|
+
// a future `continue` / `fork` run can rehydrate it. Runs on every
|
|
555
|
+
// exit path (completed, failed, cancelled, max_turns,
|
|
556
|
+
// waiting_for_input timeout). Best-effort: failures here must not
|
|
557
|
+
// mask the original exit reason / error.
|
|
558
|
+
try {
|
|
559
|
+
await saveThreadState(
|
|
560
|
+
threadId,
|
|
561
|
+
stateManager.getPersistedSlice(),
|
|
562
|
+
threadKey
|
|
563
|
+
);
|
|
564
|
+
} catch (persistError) {
|
|
565
|
+
log.warn("failed to persist thread state", {
|
|
566
|
+
agentName,
|
|
567
|
+
threadId,
|
|
568
|
+
error:
|
|
569
|
+
persistError instanceof Error
|
|
570
|
+
? persistError.message
|
|
571
|
+
: String(persistError),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
488
575
|
await callSessionEnd(exitReason, stateManager.getTurns());
|
|
489
576
|
|
|
490
577
|
if (sandboxOwned && sandboxId && sandboxOps) {
|
|
@@ -526,6 +613,13 @@ export async function createSession<
|
|
|
526
613
|
...(exitSnapshot && { hasExitSnapshot: true }),
|
|
527
614
|
});
|
|
528
615
|
|
|
616
|
+
if (onSessionExit) {
|
|
617
|
+
onSessionExit({
|
|
618
|
+
...(sandboxId && { sandboxId }),
|
|
619
|
+
...(exitSnapshot && { snapshot: exitSnapshot }),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
529
623
|
return {
|
|
530
624
|
threadId,
|
|
531
625
|
finalMessage,
|
package/src/lib/session/types.ts
CHANGED
|
@@ -11,7 +11,11 @@ import type { Skill } from "../skills/types";
|
|
|
11
11
|
import type { SandboxOps, SandboxSnapshot } from "../sandbox/types";
|
|
12
12
|
import type { VirtualFsOps } from "../virtual-fs/types";
|
|
13
13
|
import type { RunAgentActivity } from "../model/types";
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
AgentStateManager,
|
|
16
|
+
JsonSerializable,
|
|
17
|
+
PersistedThreadState,
|
|
18
|
+
} from "../state/types";
|
|
15
19
|
import type { ActivityInterfaceFor } from "@temporalio/workflow";
|
|
16
20
|
import type {
|
|
17
21
|
ThreadInit,
|
|
@@ -53,12 +57,54 @@ export interface ThreadOps<TContent = string> {
|
|
|
53
57
|
content: unknown,
|
|
54
58
|
threadKey?: string
|
|
55
59
|
): Promise<void>;
|
|
56
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Copy all messages AND the persisted state slice (tasks + custom
|
|
62
|
+
* state) from `sourceThreadId` into a new thread at `targetThreadId`.
|
|
63
|
+
* Adapters that have `onForkPrepareThread` and/or `onForkTransform`
|
|
64
|
+
* hooks configured apply them once to the new thread's messages
|
|
65
|
+
* before returning.
|
|
66
|
+
*/
|
|
57
67
|
forkThread(
|
|
58
68
|
sourceThreadId: string,
|
|
59
69
|
targetThreadId: string,
|
|
60
70
|
threadKey?: string
|
|
61
71
|
): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Truncate the thread starting at `messageId`: that message and every
|
|
74
|
+
* message after it are removed. If `messageId` is not present the call
|
|
75
|
+
* is a no-op.
|
|
76
|
+
*
|
|
77
|
+
* The `runAgent` activity invokes this on entry with the pre-generated
|
|
78
|
+
* `assistantMessageId`. On the happy path the id is not yet in the
|
|
79
|
+
* thread and the call is a no-op. On a rewind retry (same assistant
|
|
80
|
+
* id reused) or a Temporal workflow reset-to-this-activity the id is
|
|
81
|
+
* present, so the bad assistant + any tool results it produced are
|
|
82
|
+
* wiped and the call is then replayable.
|
|
83
|
+
*/
|
|
84
|
+
truncateThread(
|
|
85
|
+
threadId: string,
|
|
86
|
+
messageId: string,
|
|
87
|
+
threadKey?: string
|
|
88
|
+
): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Load the persisted state slice (tasks + custom state) associated with
|
|
91
|
+
* the thread, or `null` if none has been saved yet. Called on session
|
|
92
|
+
* start for `continue`/`fork` threads to rehydrate {@link AgentStateManager}.
|
|
93
|
+
*/
|
|
94
|
+
loadThreadState(
|
|
95
|
+
threadId: string,
|
|
96
|
+
threadKey?: string
|
|
97
|
+
): Promise<PersistedThreadState | null>;
|
|
98
|
+
/**
|
|
99
|
+
* Overwrite the persisted state slice for the thread. Called once from
|
|
100
|
+
* the session's `finally` block on every exit path so that "finish,
|
|
101
|
+
* store, continue later" works regardless of exit reason.
|
|
102
|
+
*/
|
|
103
|
+
saveThreadState(
|
|
104
|
+
threadId: string,
|
|
105
|
+
state: PersistedThreadState,
|
|
106
|
+
threadKey?: string
|
|
107
|
+
): Promise<void>;
|
|
62
108
|
}
|
|
63
109
|
|
|
64
110
|
/**
|
|
@@ -183,8 +229,25 @@ export interface SessionConfig<
|
|
|
183
229
|
/**
|
|
184
230
|
* Called as soon as the sandbox is created (or resumed/forked), before the
|
|
185
231
|
* agent loop starts. Useful for signalling sandbox readiness to a parent.
|
|
232
|
+
*
|
|
233
|
+
* `baseSnapshot` is only populated when the sandbox was freshly created
|
|
234
|
+
* this run and `sandboxShutdown === "snapshot"` — i.e. when the session
|
|
235
|
+
* captured a seed snapshot intended for reuse.
|
|
236
|
+
*/
|
|
237
|
+
onSandboxReady?: (args: {
|
|
238
|
+
sandboxId: string;
|
|
239
|
+
baseSnapshot?: SandboxSnapshot;
|
|
240
|
+
}) => void;
|
|
241
|
+
/**
|
|
242
|
+
* Called right before `runSession` returns, with the session's sandbox
|
|
243
|
+
* outputs. Useful for callers (e.g. `defineSubagentWorkflow`) that want to
|
|
244
|
+
* forward these fields to their own return value without requiring user
|
|
245
|
+
* code to manually thread them through.
|
|
186
246
|
*/
|
|
187
|
-
|
|
247
|
+
onSessionExit?: (result: {
|
|
248
|
+
sandboxId?: string;
|
|
249
|
+
snapshot?: SandboxSnapshot;
|
|
250
|
+
}) => void;
|
|
188
251
|
|
|
189
252
|
// ---------------------------------------------------------------------------
|
|
190
253
|
// Virtual filesystem
|
package/src/lib/state/index.ts
CHANGED
|
@@ -350,4 +350,113 @@ describe("createAgentStateManager integration", () => {
|
|
|
350
350
|
sm.incrementTurns();
|
|
351
351
|
expect(sm.getTotalUsage().turns).toBe(2);
|
|
352
352
|
});
|
|
353
|
+
|
|
354
|
+
// --- Persisted slice round-trip ---
|
|
355
|
+
|
|
356
|
+
it("getPersistedSlice snapshots tasks + custom state, omits runtime bookkeeping", () => {
|
|
357
|
+
const sm = createAgentStateManager<{ label: string; count: number }>({
|
|
358
|
+
initialState: {
|
|
359
|
+
systemPrompt: "test",
|
|
360
|
+
label: "original",
|
|
361
|
+
count: 1,
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const task: WorkflowTask = {
|
|
366
|
+
id: "task-1",
|
|
367
|
+
subject: "subject",
|
|
368
|
+
description: "description",
|
|
369
|
+
activeForm: "doing",
|
|
370
|
+
status: "pending",
|
|
371
|
+
metadata: {},
|
|
372
|
+
blockedBy: [],
|
|
373
|
+
blocks: [],
|
|
374
|
+
};
|
|
375
|
+
sm.setTask(task);
|
|
376
|
+
sm.incrementTurns();
|
|
377
|
+
sm.updateUsage({ inputTokens: 42 });
|
|
378
|
+
|
|
379
|
+
const slice = sm.getPersistedSlice();
|
|
380
|
+
expect(slice.tasks).toEqual([["task-1", task]]);
|
|
381
|
+
expect(slice.custom).toEqual({ label: "original", count: 1 });
|
|
382
|
+
expect("status" in slice.custom).toBe(false);
|
|
383
|
+
expect("version" in slice.custom).toBe(false);
|
|
384
|
+
expect("turns" in slice.custom).toBe(false);
|
|
385
|
+
expect("tools" in slice.custom).toBe(false);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("mergeUpdate replaces the task map when given a tasks field", () => {
|
|
389
|
+
const sm = createAgentStateManager<{ label: string; extra?: string }>({
|
|
390
|
+
initialState: {
|
|
391
|
+
systemPrompt: "test",
|
|
392
|
+
label: "original",
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const existing: WorkflowTask = {
|
|
397
|
+
id: "task-pre",
|
|
398
|
+
subject: "pre",
|
|
399
|
+
description: "pre",
|
|
400
|
+
activeForm: "pre",
|
|
401
|
+
status: "pending",
|
|
402
|
+
metadata: {},
|
|
403
|
+
blockedBy: [],
|
|
404
|
+
blocks: [],
|
|
405
|
+
};
|
|
406
|
+
sm.setTask(existing);
|
|
407
|
+
const versionBefore = sm.getVersion();
|
|
408
|
+
|
|
409
|
+
const incoming: WorkflowTask = {
|
|
410
|
+
id: "task-new",
|
|
411
|
+
subject: "new",
|
|
412
|
+
description: "new",
|
|
413
|
+
activeForm: "new",
|
|
414
|
+
status: "in_progress",
|
|
415
|
+
metadata: { foo: "bar" },
|
|
416
|
+
blockedBy: [],
|
|
417
|
+
blocks: [],
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
sm.mergeUpdate({
|
|
421
|
+
tasks: new Map([["task-new", incoming]]),
|
|
422
|
+
label: "restored",
|
|
423
|
+
extra: "extra-value",
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
expect(sm.getTasks()).toEqual([incoming]);
|
|
427
|
+
expect(sm.getTask("task-pre")).toBeUndefined();
|
|
428
|
+
expect(sm.get("label")).toBe("restored");
|
|
429
|
+
expect(sm.get("extra")).toBe("extra-value");
|
|
430
|
+
expect(sm.getVersion()).toBe(versionBefore + 1);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("getPersistedSlice + mergeUpdate round-trips tasks + custom state", () => {
|
|
434
|
+
const original = createAgentStateManager<{ answer: number }>({
|
|
435
|
+
initialState: { systemPrompt: "test", answer: 42 },
|
|
436
|
+
});
|
|
437
|
+
const task: WorkflowTask = {
|
|
438
|
+
id: "t",
|
|
439
|
+
subject: "s",
|
|
440
|
+
description: "d",
|
|
441
|
+
activeForm: "doing",
|
|
442
|
+
status: "completed",
|
|
443
|
+
metadata: { a: "b" },
|
|
444
|
+
blockedBy: ["x"],
|
|
445
|
+
blocks: ["y"],
|
|
446
|
+
};
|
|
447
|
+
original.setTask(task);
|
|
448
|
+
|
|
449
|
+
const slice = original.getPersistedSlice();
|
|
450
|
+
|
|
451
|
+
const restored = createAgentStateManager<{ answer: number }>({
|
|
452
|
+
initialState: { systemPrompt: "test", answer: 0 },
|
|
453
|
+
});
|
|
454
|
+
restored.mergeUpdate({
|
|
455
|
+
tasks: new Map(slice.tasks),
|
|
456
|
+
...slice.custom,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
expect(restored.getTasks()).toEqual([task]);
|
|
460
|
+
expect(restored.get("answer")).toBe(42);
|
|
461
|
+
});
|
|
353
462
|
});
|
package/src/lib/state/manager.ts
CHANGED
|
@@ -11,7 +11,13 @@ import {
|
|
|
11
11
|
isTerminalStatus,
|
|
12
12
|
} from "../types";
|
|
13
13
|
import type { ToolDefinition } from "../tool-router/types";
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
AgentState,
|
|
16
|
+
AgentStateManager,
|
|
17
|
+
JsonSerializable,
|
|
18
|
+
JsonValue,
|
|
19
|
+
PersistedThreadState,
|
|
20
|
+
} from "./types";
|
|
15
21
|
import { z } from "zod";
|
|
16
22
|
|
|
17
23
|
/**
|
|
@@ -63,11 +69,19 @@ export function createAgentStateManager<
|
|
|
63
69
|
const tasks = new Map<string, WorkflowTask>(initialState?.tasks);
|
|
64
70
|
|
|
65
71
|
const {
|
|
66
|
-
status:
|
|
67
|
-
version:
|
|
68
|
-
turns:
|
|
69
|
-
tasks:
|
|
70
|
-
tools:
|
|
72
|
+
status: _status,
|
|
73
|
+
version: _version,
|
|
74
|
+
turns: _turns,
|
|
75
|
+
tasks: _tasks,
|
|
76
|
+
tools: _tools,
|
|
77
|
+
systemPrompt: _systemPrompt,
|
|
78
|
+
fileTree: _fileTree,
|
|
79
|
+
inlineFiles: _inlineFiles,
|
|
80
|
+
virtualFsCtx: _virtualFsCtx,
|
|
81
|
+
totalInputTokens: _totalInputTokens,
|
|
82
|
+
totalOutputTokens: _totalOutputTokens,
|
|
83
|
+
cachedWriteTokens: _cachedWriteTokens,
|
|
84
|
+
cachedReadTokens: _cachedReadTokens,
|
|
71
85
|
...custom
|
|
72
86
|
} = initialState ?? {};
|
|
73
87
|
const customState = custom as TCustom;
|
|
@@ -166,8 +180,17 @@ export function createAgentStateManager<
|
|
|
166
180
|
version++;
|
|
167
181
|
},
|
|
168
182
|
|
|
169
|
-
mergeUpdate(update: Partial<TCustom
|
|
170
|
-
|
|
183
|
+
mergeUpdate(update: Partial<AgentState<TCustom>>): void {
|
|
184
|
+
const { tasks: nextTasks, ...rest } = update as Partial<
|
|
185
|
+
AgentState<TCustom>
|
|
186
|
+
> & { tasks?: Map<string, WorkflowTask> };
|
|
187
|
+
if (nextTasks) {
|
|
188
|
+
tasks.clear();
|
|
189
|
+
for (const [id, task] of nextTasks) {
|
|
190
|
+
tasks.set(id, task);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
Object.assign(customState as object, rest);
|
|
171
194
|
version++;
|
|
172
195
|
},
|
|
173
196
|
|
|
@@ -214,6 +237,13 @@ export function createAgentStateManager<
|
|
|
214
237
|
return deleted;
|
|
215
238
|
},
|
|
216
239
|
|
|
240
|
+
getPersistedSlice(): PersistedThreadState {
|
|
241
|
+
return {
|
|
242
|
+
tasks: Array.from(tasks.entries()),
|
|
243
|
+
custom: { ...(customState as unknown as Record<string, JsonValue>) },
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
|
|
217
247
|
updateUsage(usage: {
|
|
218
248
|
inputTokens?: number;
|
|
219
249
|
outputTokens?: number;
|
package/src/lib/state/types.ts
CHANGED
|
@@ -48,6 +48,23 @@ export type JsonSerializable<T> = {
|
|
|
48
48
|
export type AgentState<TCustom extends JsonSerializable<TCustom>> =
|
|
49
49
|
BaseAgentState & TCustom;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The slice of agent state that is persisted alongside the thread in the
|
|
53
|
+
* thread store (e.g. Redis) so that a workflow can terminate, store its
|
|
54
|
+
* state, and be continued or forked later with that state rehydrated.
|
|
55
|
+
*
|
|
56
|
+
* Only fields that make sense to carry across workflow runs belong here.
|
|
57
|
+
* Runtime bookkeeping like status, version, turns, tools, fileTree, token
|
|
58
|
+
* counters, and the system prompt is intentionally NOT persisted — each run
|
|
59
|
+
* rebuilds those from scratch.
|
|
60
|
+
*/
|
|
61
|
+
export interface PersistedThreadState {
|
|
62
|
+
/** Task map serialized as entries so it round-trips through JSON. */
|
|
63
|
+
tasks: [string, WorkflowTask][];
|
|
64
|
+
/** All custom state fields declared by the caller. */
|
|
65
|
+
custom: Record<string, JsonValue>;
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
/**
|
|
52
69
|
* Agent state manager interface
|
|
53
70
|
* Note: Temporal handlers must be set up in the workflow file due to
|
|
@@ -122,6 +139,14 @@ export interface AgentStateManager<TCustom extends JsonSerializable<TCustom>> {
|
|
|
122
139
|
/** Set the tools (converts Zod schemas to JSON Schema for serialization) */
|
|
123
140
|
setTools(newTools: ToolDefinition[]): void;
|
|
124
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Snapshot the fields that should survive across workflow runs
|
|
144
|
+
* (tasks + all custom state). Safe to pass directly to
|
|
145
|
+
* {@link ThreadOps.saveThreadState}. Rehydrate on the next run with
|
|
146
|
+
* `mergeUpdate({ tasks: new Map(slice.tasks), ...slice.custom })`.
|
|
147
|
+
*/
|
|
148
|
+
getPersistedSlice(): PersistedThreadState;
|
|
149
|
+
|
|
125
150
|
/** Update the usage */
|
|
126
151
|
updateUsage(usage: TokenUsage): void;
|
|
127
152
|
|