zeitlich 0.2.43 → 0.2.45
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/dist/index.cjs +61 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +61 -41
- package/dist/index.js.map +1 -1
- package/dist/workflow.cjs +61 -41
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.js +61 -41
- package/dist/workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/session/session.ts +53 -51
- package/src/lib/state/manager.integration.test.ts +27 -0
- package/src/lib/state/manager.ts +39 -1
package/package.json
CHANGED
|
@@ -163,8 +163,7 @@ export async function createSession<
|
|
|
163
163
|
unknown,
|
|
164
164
|
SandboxCapability
|
|
165
165
|
>;
|
|
166
|
-
const wideOps = (): WideSandboxOps =>
|
|
167
|
-
sandboxOps as unknown as WideSandboxOps;
|
|
166
|
+
const wideOps = (): WideSandboxOps => sandboxOps as unknown as WideSandboxOps;
|
|
168
167
|
// ---------------------------------------------------------------------------
|
|
169
168
|
// Thread resolution
|
|
170
169
|
// ---------------------------------------------------------------------------
|
|
@@ -276,10 +275,7 @@ export async function createSession<
|
|
|
276
275
|
// defaults are. Both surfaces consult `resolveSessionLifecycle`
|
|
277
276
|
// (or its type-level equivalent) before checking individual
|
|
278
277
|
// mode/shutdown values.
|
|
279
|
-
const lifecycle = resolveSessionLifecycle(
|
|
280
|
-
sandboxInit,
|
|
281
|
-
sandboxShutdown
|
|
282
|
-
);
|
|
278
|
+
const lifecycle = resolveSessionLifecycle(sandboxInit, sandboxShutdown);
|
|
283
279
|
const sandboxMode: SandboxInit["mode"] | undefined = lifecycle.mode;
|
|
284
280
|
const resolvedShutdown: SubagentSandboxShutdown = lifecycle.shutdown;
|
|
285
281
|
let sandboxId: string | undefined;
|
|
@@ -383,7 +379,48 @@ export async function createSession<
|
|
|
383
379
|
});
|
|
384
380
|
}
|
|
385
381
|
|
|
382
|
+
const sessionStartMs = Date.now();
|
|
383
|
+
const systemPrompt = stateManager.getSystemPrompt();
|
|
384
|
+
|
|
385
|
+
// --- Thread lifecycle: new, continue, or fork ----------------------
|
|
386
|
+
const rehydrateFromSlice = (slice: PersistedThreadState): void => {
|
|
387
|
+
stateManager.mergeUpdate({
|
|
388
|
+
tasks: new Map(slice.tasks),
|
|
389
|
+
...slice.custom,
|
|
390
|
+
} as Partial<AgentState<TState>>);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (threadMode === "fork" && sourceThreadId) {
|
|
394
|
+
await forkThread(sourceThreadId, threadId, threadKey);
|
|
395
|
+
const forkedSlice = await loadThreadState(threadId, threadKey);
|
|
396
|
+
if (forkedSlice) rehydrateFromSlice(forkedSlice);
|
|
397
|
+
} else if (threadMode === "continue") {
|
|
398
|
+
// "continue" — thread already exists, just append the new message
|
|
399
|
+
const continuedSlice = await loadThreadState(threadId, threadKey);
|
|
400
|
+
if (continuedSlice) rehydrateFromSlice(continuedSlice);
|
|
401
|
+
} else {
|
|
402
|
+
if (appendSystemPrompt) {
|
|
403
|
+
if (
|
|
404
|
+
systemPrompt == null ||
|
|
405
|
+
(typeof systemPrompt === "string" && systemPrompt.trim() === "")
|
|
406
|
+
) {
|
|
407
|
+
throw ApplicationFailure.create({
|
|
408
|
+
message: "No system prompt in state",
|
|
409
|
+
nonRetryable: true,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
await appendSystemMessage(threadId, uuid4(), systemPrompt, threadKey);
|
|
413
|
+
} else {
|
|
414
|
+
await initializeThread(threadId, threadKey);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
386
418
|
// --- Virtual filesystem init (independent of sandbox) ----------------
|
|
419
|
+
// Runs AFTER thread rehydration so the freshly resolved tree is
|
|
420
|
+
// authoritative. Otherwise a stale `fileTree` carried in a persisted
|
|
421
|
+
// slice (from a run on older code that didn't strip it) could
|
|
422
|
+
// overwrite entries that no longer exist in the backing store and
|
|
423
|
+
// cause subsequent FileWrite calls to fail with "not_found".
|
|
387
424
|
if (virtualFsConfig) {
|
|
388
425
|
if (!virtualFsOps) {
|
|
389
426
|
throw ApplicationFailure.create({
|
|
@@ -423,6 +460,16 @@ export async function createSession<
|
|
|
423
460
|
} as Partial<AgentState<TState>>);
|
|
424
461
|
}
|
|
425
462
|
|
|
463
|
+
await appendHumanMessage(
|
|
464
|
+
threadId,
|
|
465
|
+
uuid4(),
|
|
466
|
+
await buildContextMessage(),
|
|
467
|
+
threadKey
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
let exitReason: SessionExitReason = "completed";
|
|
471
|
+
let finalMessage: M | null = null;
|
|
472
|
+
|
|
426
473
|
if (hooks.onSessionStart) {
|
|
427
474
|
await hooks.onSessionStart({
|
|
428
475
|
threadId,
|
|
@@ -439,51 +486,6 @@ export async function createSession<
|
|
|
439
486
|
...(sandboxId && { sandboxId }),
|
|
440
487
|
});
|
|
441
488
|
|
|
442
|
-
const sessionStartMs = Date.now();
|
|
443
|
-
const systemPrompt = stateManager.getSystemPrompt();
|
|
444
|
-
|
|
445
|
-
// --- Thread lifecycle: new, continue, or fork ----------------------
|
|
446
|
-
const rehydrateFromSlice = (slice: PersistedThreadState): void => {
|
|
447
|
-
stateManager.mergeUpdate({
|
|
448
|
-
tasks: new Map(slice.tasks),
|
|
449
|
-
...slice.custom,
|
|
450
|
-
} as Partial<AgentState<TState>>);
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
if (threadMode === "fork" && sourceThreadId) {
|
|
454
|
-
await forkThread(sourceThreadId, threadId, threadKey);
|
|
455
|
-
const forkedSlice = await loadThreadState(threadId, threadKey);
|
|
456
|
-
if (forkedSlice) rehydrateFromSlice(forkedSlice);
|
|
457
|
-
} else if (threadMode === "continue") {
|
|
458
|
-
// "continue" — thread already exists, just append the new message
|
|
459
|
-
const continuedSlice = await loadThreadState(threadId, threadKey);
|
|
460
|
-
if (continuedSlice) rehydrateFromSlice(continuedSlice);
|
|
461
|
-
} else {
|
|
462
|
-
if (appendSystemPrompt) {
|
|
463
|
-
if (
|
|
464
|
-
systemPrompt == null ||
|
|
465
|
-
(typeof systemPrompt === "string" && systemPrompt.trim() === "")
|
|
466
|
-
) {
|
|
467
|
-
throw ApplicationFailure.create({
|
|
468
|
-
message: "No system prompt in state",
|
|
469
|
-
nonRetryable: true,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
await appendSystemMessage(threadId, uuid4(), systemPrompt, threadKey);
|
|
473
|
-
} else {
|
|
474
|
-
await initializeThread(threadId, threadKey);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
await appendHumanMessage(
|
|
478
|
-
threadId,
|
|
479
|
-
uuid4(),
|
|
480
|
-
await buildContextMessage(),
|
|
481
|
-
threadKey
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
let exitReason: SessionExitReason = "completed";
|
|
485
|
-
let finalMessage: M | null = null;
|
|
486
|
-
|
|
487
489
|
try {
|
|
488
490
|
// Per-turn assistant message id. Pre-generated in the workflow
|
|
489
491
|
// so the runAgent activity can truncate the thread from this id
|
|
@@ -385,6 +385,33 @@ describe("createAgentStateManager integration", () => {
|
|
|
385
385
|
expect("tools" in slice.custom).toBe(false);
|
|
386
386
|
});
|
|
387
387
|
|
|
388
|
+
it("getPersistedSlice strips runtime fields injected via mergeUpdate", () => {
|
|
389
|
+
const sm = createAgentStateManager<{ label: string }>({
|
|
390
|
+
initialState: { systemPrompt: "test", label: "original" },
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
sm.mergeUpdate({
|
|
394
|
+
label: "updated",
|
|
395
|
+
fileTree: [
|
|
396
|
+
{
|
|
397
|
+
id: "f1",
|
|
398
|
+
path: "/a.txt",
|
|
399
|
+
size: 1,
|
|
400
|
+
mtime: "2026-01-01T00:00:00Z",
|
|
401
|
+
metadata: {},
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
virtualFsCtx: { workspaceBase: "/tmp" },
|
|
405
|
+
inlineFiles: { "/a.txt": "hi" },
|
|
406
|
+
} as Parameters<typeof sm.mergeUpdate>[0]);
|
|
407
|
+
|
|
408
|
+
const slice = sm.getPersistedSlice();
|
|
409
|
+
expect(slice.custom).toEqual({ label: "updated" });
|
|
410
|
+
expect("fileTree" in slice.custom).toBe(false);
|
|
411
|
+
expect("virtualFsCtx" in slice.custom).toBe(false);
|
|
412
|
+
expect("inlineFiles" in slice.custom).toBe(false);
|
|
413
|
+
});
|
|
414
|
+
|
|
388
415
|
it("mergeUpdate replaces the task map when given a tasks field", () => {
|
|
389
416
|
const sm = createAgentStateManager<{ label: string; extra?: string }>({
|
|
390
417
|
initialState: {
|
package/src/lib/state/manager.ts
CHANGED
|
@@ -20,6 +20,32 @@ import type {
|
|
|
20
20
|
} from "./types";
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Fields that live on `AgentState` at runtime but must NOT be persisted into
|
|
25
|
+
* `PersistedThreadState`. They're either managed by the state manager itself
|
|
26
|
+
* (status/version/turns/tasks/tools/usage), kept in their own slot
|
|
27
|
+
* (systemPrompt), or rebuilt on each run (the virtual-fs trio).
|
|
28
|
+
*
|
|
29
|
+
* Centralizing the list keeps the constructor's destructure-omit and
|
|
30
|
+
* `getPersistedSlice` in lockstep.
|
|
31
|
+
*/
|
|
32
|
+
const RESERVED_STATE_KEYS = [
|
|
33
|
+
"status",
|
|
34
|
+
"version",
|
|
35
|
+
"turns",
|
|
36
|
+
"tasks",
|
|
37
|
+
"tools",
|
|
38
|
+
"systemPrompt",
|
|
39
|
+
"fileTree",
|
|
40
|
+
"inlineFiles",
|
|
41
|
+
"virtualFsCtx",
|
|
42
|
+
"totalInputTokens",
|
|
43
|
+
"totalOutputTokens",
|
|
44
|
+
"totalReasonTokens",
|
|
45
|
+
"cachedWriteTokens",
|
|
46
|
+
"cachedReadTokens",
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
23
49
|
/**
|
|
24
50
|
* Creates an agent state manager for tracking workflow state.
|
|
25
51
|
* Automatically registers Temporal query and update handlers for the agent.
|
|
@@ -238,9 +264,21 @@ export function createAgentStateManager<
|
|
|
238
264
|
},
|
|
239
265
|
|
|
240
266
|
getPersistedSlice(): PersistedThreadState {
|
|
267
|
+
// `customState` can pick up reserved/runtime fields via `mergeUpdate`
|
|
268
|
+
// (e.g. `fileTree`, `virtualFsCtx`, `inlineFiles` written by the
|
|
269
|
+
// virtual-fs bootstrap on every run). Those are rebuilt per run and
|
|
270
|
+
// must never round-trip through the thread store, so strip them here
|
|
271
|
+
// rather than relying on callers to remember.
|
|
272
|
+
const source = customState as unknown as Record<string, JsonValue>;
|
|
273
|
+
const custom: Record<string, JsonValue> = {};
|
|
274
|
+
const reserved = new Set<string>(RESERVED_STATE_KEYS);
|
|
275
|
+
for (const [key, value] of Object.entries(source)) {
|
|
276
|
+
if (reserved.has(key)) continue;
|
|
277
|
+
custom[key] = value;
|
|
278
|
+
}
|
|
241
279
|
return {
|
|
242
280
|
tasks: Array.from(tasks.entries()),
|
|
243
|
-
custom
|
|
281
|
+
custom,
|
|
244
282
|
};
|
|
245
283
|
},
|
|
246
284
|
|