zeitlich 0.2.38 → 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-BKhMtKDd.d.ts → activities-Bmu7XnaG.d.ts} +4 -6
- package/dist/{activities-CDcwkRZs.d.cts → activities-ByBFLvm2.d.cts} +4 -6
- 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/thread/anthropic/index.cjs +140 -23
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +8 -7
- package/dist/adapters/thread/anthropic/index.d.ts +8 -7
- package/dist/adapters/thread/anthropic/index.js +140 -24
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.cjs +8 -3
- package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +5 -4
- package/dist/adapters/thread/anthropic/workflow.d.ts +5 -4
- package/dist/adapters/thread/anthropic/workflow.js +8 -4
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.cjs +140 -23
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +5 -4
- package/dist/adapters/thread/google-genai/index.d.ts +5 -4
- package/dist/adapters/thread/google-genai/index.js +140 -24
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.cjs +8 -3
- package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +5 -4
- package/dist/adapters/thread/google-genai/workflow.d.ts +5 -4
- package/dist/adapters/thread/google-genai/workflow.js +8 -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 +139 -24
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +8 -7
- package/dist/adapters/thread/langchain/index.d.ts +8 -7
- package/dist/adapters/thread/langchain/index.js +139 -25
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.cjs +8 -3
- package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +5 -4
- package/dist/adapters/thread/langchain/workflow.d.ts +5 -4
- package/dist/adapters/thread/langchain/workflow.js +8 -4
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/index.cjs +266 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +263 -49
- package/dist/index.js.map +1 -1
- package/dist/{proxy-D_3x7RN4.d.cts → proxy-BAKzNGRq.d.cts} +1 -1
- package/dist/{proxy-CUlKSvZS.d.ts → proxy-DO_MXbY4.d.ts} +1 -1
- package/dist/{thread-manager-CVu7o2cs.d.ts → thread-manager-CcRXasqs.d.ts} +2 -4
- package/dist/{thread-manager-HSwyh28L.d.cts → thread-manager-ClwSaUnj.d.cts} +2 -4
- package/dist/{thread-manager-c1gPopAG.d.ts → thread-manager-D-7lp1JK.d.ts} +2 -4
- package/dist/{thread-manager-wGi-LqIP.d.cts → thread-manager-Y8Ucf0Tf.d.cts} +2 -4
- package/dist/{types-C06FwR96.d.cts → types-Bcbiq8iv.d.cts} +162 -44
- package/dist/{types-BH_IRryz.d.ts → types-DpHTX-iO.d.ts} +54 -6
- package/dist/{types-DNr31FzL.d.ts → types-Dt8-HBBT.d.ts} +162 -44
- package/dist/{types-BaOw4hKI.d.cts → types-hFFi-Zd9.d.cts} +54 -6
- package/dist/{workflow-CSCkpwAL.d.ts → workflow-Bmf9EtDW.d.ts} +82 -2
- package/dist/{workflow-DuvMZ8Vm.d.cts → workflow-Bx9utBwb.d.cts} +82 -2
- package/dist/workflow.cjs +188 -37
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +2 -2
- package/dist/workflow.d.ts +2 -2
- package/dist/workflow.js +185 -38
- package/dist/workflow.js.map +1 -1
- package/package.json +11 -1
- package/src/adapters/thread/adapter-id.test.ts +42 -0
- package/src/adapters/thread/anthropic/activities.ts +33 -7
- 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 +8 -4
- package/src/adapters/thread/anthropic/proxy.ts +3 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +27 -4
- package/src/adapters/thread/google-genai/activities.ts +33 -7
- 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 +7 -3
- package/src/adapters/thread/google-genai/proxy.ts +3 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +27 -4
- package/src/adapters/thread/index.ts +39 -0
- package/src/adapters/thread/langchain/activities.ts +33 -7
- 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 +8 -3
- package/src/adapters/thread/langchain/proxy.ts +3 -2
- package/src/adapters/thread/langchain/thread-manager.ts +27 -4
- package/src/lib/lifecycle.ts +3 -1
- package/src/lib/model/types.ts +7 -10
- package/src/lib/session/session-edge-cases.integration.test.ts +131 -63
- package/src/lib/session/session.integration.test.ts +174 -5
- package/src/lib/session/session.ts +68 -28
- package/src/lib/session/types.ts +60 -9
- 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 +528 -0
- 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 +92 -14
- package/src/lib/thread/proxy.ts +2 -0
- package/src/lib/thread/types.ts +60 -6
- package/src/lib/tool-router/types.ts +16 -8
- package/src/lib/types.ts +12 -0
- package/src/workflow.ts +12 -1
- package/tsup.config.ts +1 -0
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
AgentState,
|
|
17
17
|
AgentStateManager,
|
|
18
18
|
JsonSerializable,
|
|
19
|
+
PersistedThreadState,
|
|
19
20
|
} from "../state/types";
|
|
20
21
|
import { createToolRouter } from "../tool-router/router";
|
|
21
22
|
import type { ParsedToolCallUnion, ToolMap } from "../tool-router/types";
|
|
@@ -111,6 +112,7 @@ export async function createSession<
|
|
|
111
112
|
sandbox: sandboxInit,
|
|
112
113
|
sandboxShutdown = "destroy",
|
|
113
114
|
onSandboxReady,
|
|
115
|
+
onSessionExit,
|
|
114
116
|
virtualFs: virtualFsConfig,
|
|
115
117
|
virtualFsOps,
|
|
116
118
|
}: SessionConfig<T, M, TContent>): Promise<ZeitlichSession<M, boolean>> {
|
|
@@ -146,7 +148,8 @@ export async function createSession<
|
|
|
146
148
|
appendSystemMessage,
|
|
147
149
|
appendAgentMessage,
|
|
148
150
|
forkThread,
|
|
149
|
-
|
|
151
|
+
loadThreadState,
|
|
152
|
+
saveThreadState,
|
|
150
153
|
} = threadOps;
|
|
151
154
|
|
|
152
155
|
const plugins: ToolMap[string][] = [];
|
|
@@ -314,7 +317,10 @@ export async function createSession<
|
|
|
314
317
|
}
|
|
315
318
|
|
|
316
319
|
if (sandboxId && sandboxOwned && onSandboxReady) {
|
|
317
|
-
onSandboxReady(
|
|
320
|
+
onSandboxReady({
|
|
321
|
+
sandboxId,
|
|
322
|
+
...(baseSnapshot && { baseSnapshot }),
|
|
323
|
+
});
|
|
318
324
|
}
|
|
319
325
|
|
|
320
326
|
// --- Virtual filesystem init (independent of sandbox) ----------------
|
|
@@ -366,10 +372,21 @@ export async function createSession<
|
|
|
366
372
|
const systemPrompt = stateManager.getSystemPrompt();
|
|
367
373
|
|
|
368
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
|
+
|
|
369
382
|
if (threadMode === "fork" && sourceThreadId) {
|
|
370
383
|
await forkThread(sourceThreadId, threadId, threadKey);
|
|
384
|
+
const forkedSlice = await loadThreadState(threadId, threadKey);
|
|
385
|
+
if (forkedSlice) rehydrateFromSlice(forkedSlice);
|
|
371
386
|
} else if (threadMode === "continue") {
|
|
372
387
|
// "continue" — thread already exists, just append the new message
|
|
388
|
+
const continuedSlice = await loadThreadState(threadId, threadKey);
|
|
389
|
+
if (continuedSlice) rehydrateFromSlice(continuedSlice);
|
|
373
390
|
} else {
|
|
374
391
|
if (appendSystemPrompt) {
|
|
375
392
|
if (
|
|
@@ -397,6 +414,13 @@ export async function createSession<
|
|
|
397
414
|
let finalMessage: M | null = null;
|
|
398
415
|
|
|
399
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;
|
|
400
424
|
while (
|
|
401
425
|
stateManager.isRunning() &&
|
|
402
426
|
!stateManager.isTerminal() &&
|
|
@@ -409,25 +433,17 @@ export async function createSession<
|
|
|
409
433
|
|
|
410
434
|
stateManager.setTools(toolRouter.getToolDefinitions());
|
|
411
435
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
usage,
|
|
416
|
-
threadLengthAtCall,
|
|
417
|
-
} = await runAgent({
|
|
436
|
+
assistantId ??= uuid4();
|
|
437
|
+
|
|
438
|
+
const { message, rawToolCalls, usage } = await runAgent({
|
|
418
439
|
threadId,
|
|
419
440
|
threadKey,
|
|
420
441
|
agentName,
|
|
421
442
|
metadata,
|
|
443
|
+
assistantMessageId: assistantId,
|
|
422
444
|
});
|
|
423
445
|
|
|
424
|
-
|
|
425
|
-
// so it already knows how many messages were stored at that
|
|
426
|
-
// point — we use that directly as the rewind snapshot instead
|
|
427
|
-
// of a separate activity round-trip.
|
|
428
|
-
const preAssistantLength = threadLengthAtCall;
|
|
429
|
-
|
|
430
|
-
await appendAgentMessage(threadId, uuid4(), message, threadKey);
|
|
446
|
+
await appendAgentMessage(threadId, assistantId, message, threadKey);
|
|
431
447
|
|
|
432
448
|
if (usage) {
|
|
433
449
|
stateManager.updateUsage(usage);
|
|
@@ -488,24 +504,19 @@ export async function createSession<
|
|
|
488
504
|
toolCallId: rewind.toolCallId,
|
|
489
505
|
toolName: rewind.toolName,
|
|
490
506
|
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
"support rewinds.",
|
|
497
|
-
nonRetryable: true,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
// Drop the assistant message + any already-saved tool results
|
|
501
|
-
// so the LLM call can be retried from the pre-assistant state.
|
|
502
|
-
// The turn counter is intentionally NOT rolled back — each
|
|
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
|
|
503
512
|
// rewind still consumes one of the `maxTurns` budget so a
|
|
504
513
|
// misbehaving tool cannot spin the session forever.
|
|
505
|
-
await truncateThread(threadId, preAssistantLength, threadKey);
|
|
506
514
|
continue;
|
|
507
515
|
}
|
|
508
516
|
|
|
517
|
+
// Turn committed: fresh id for the next turn.
|
|
518
|
+
assistantId = undefined;
|
|
519
|
+
|
|
509
520
|
if (stateManager.getStatus() === "WAITING_FOR_INPUT") {
|
|
510
521
|
const conditionMet = await condition(
|
|
511
522
|
() => stateManager.getStatus() === "RUNNING",
|
|
@@ -539,6 +550,28 @@ export async function createSession<
|
|
|
539
550
|
});
|
|
540
551
|
throw ApplicationFailure.fromError(error);
|
|
541
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
|
+
|
|
542
575
|
await callSessionEnd(exitReason, stateManager.getTurns());
|
|
543
576
|
|
|
544
577
|
if (sandboxOwned && sandboxId && sandboxOps) {
|
|
@@ -580,6 +613,13 @@ export async function createSession<
|
|
|
580
613
|
...(exitSnapshot && { hasExitSnapshot: true }),
|
|
581
614
|
});
|
|
582
615
|
|
|
616
|
+
if (onSessionExit) {
|
|
617
|
+
onSessionExit({
|
|
618
|
+
...(sandboxId && { sandboxId }),
|
|
619
|
+
...(exitSnapshot && { snapshot: exitSnapshot }),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
583
623
|
return {
|
|
584
624
|
threadId,
|
|
585
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,22 +57,52 @@ 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>;
|
|
62
72
|
/**
|
|
63
|
-
* Truncate the thread
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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.
|
|
68
83
|
*/
|
|
69
84
|
truncateThread(
|
|
70
85
|
threadId: string,
|
|
71
|
-
|
|
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,
|
|
72
106
|
threadKey?: string
|
|
73
107
|
): Promise<void>;
|
|
74
108
|
}
|
|
@@ -195,8 +229,25 @@ export interface SessionConfig<
|
|
|
195
229
|
/**
|
|
196
230
|
* Called as soon as the sandbox is created (or resumed/forked), before the
|
|
197
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.
|
|
198
246
|
*/
|
|
199
|
-
|
|
247
|
+
onSessionExit?: (result: {
|
|
248
|
+
sandboxId?: string;
|
|
249
|
+
snapshot?: SandboxSnapshot;
|
|
250
|
+
}) => void;
|
|
200
251
|
|
|
201
252
|
// ---------------------------------------------------------------------------
|
|
202
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
|
|