zeitlich 0.2.13 → 0.2.15

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.
Files changed (135) hide show
  1. package/README.md +61 -50
  2. package/dist/adapters/sandbox/daytona/index.cjs +205 -0
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -0
  4. package/dist/adapters/sandbox/daytona/index.d.cts +86 -0
  5. package/dist/adapters/sandbox/daytona/index.d.ts +86 -0
  6. package/dist/adapters/sandbox/daytona/index.js +202 -0
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -0
  8. package/dist/adapters/sandbox/inmemory/index.cjs +174 -0
  9. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -0
  10. package/dist/adapters/sandbox/inmemory/index.d.cts +28 -0
  11. package/dist/adapters/sandbox/inmemory/index.d.ts +28 -0
  12. package/dist/adapters/sandbox/inmemory/index.js +172 -0
  13. package/dist/adapters/sandbox/inmemory/index.js.map +1 -0
  14. package/dist/adapters/sandbox/virtual/index.cjs +405 -0
  15. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -0
  16. package/dist/adapters/sandbox/virtual/index.d.cts +85 -0
  17. package/dist/adapters/sandbox/virtual/index.d.ts +85 -0
  18. package/dist/adapters/sandbox/virtual/index.js +400 -0
  19. package/dist/adapters/sandbox/virtual/index.js.map +1 -0
  20. package/dist/adapters/thread/google-genai/index.cjs +306 -0
  21. package/dist/adapters/thread/google-genai/index.cjs.map +1 -0
  22. package/dist/adapters/thread/google-genai/index.d.cts +145 -0
  23. package/dist/adapters/thread/google-genai/index.d.ts +145 -0
  24. package/dist/adapters/thread/google-genai/index.js +300 -0
  25. package/dist/adapters/thread/google-genai/index.js.map +1 -0
  26. package/dist/adapters/{langchain → thread/langchain}/index.cjs +29 -9
  27. package/dist/adapters/thread/langchain/index.cjs.map +1 -0
  28. package/dist/adapters/{langchain → thread/langchain}/index.d.cts +17 -21
  29. package/dist/adapters/{langchain → thread/langchain}/index.d.ts +17 -21
  30. package/dist/adapters/{langchain → thread/langchain}/index.js +29 -9
  31. package/dist/adapters/thread/langchain/index.js.map +1 -0
  32. package/dist/index.cjs +866 -567
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +235 -74
  35. package/dist/index.d.ts +235 -74
  36. package/dist/index.js +854 -562
  37. package/dist/index.js.map +1 -1
  38. package/dist/{thread-manager-qc0g5Rvd.d.cts → types-35POpVfa.d.cts} +7 -6
  39. package/dist/{thread-manager-qc0g5Rvd.d.ts → types-35POpVfa.d.ts} +7 -6
  40. package/dist/types-BMXzv7TN.d.cts +476 -0
  41. package/dist/types-BMXzv7TN.d.ts +476 -0
  42. package/dist/types-BVP87m_W.d.cts +121 -0
  43. package/dist/types-BWvIYK28.d.ts +391 -0
  44. package/dist/types-CDubRtad.d.cts +115 -0
  45. package/dist/types-CDubRtad.d.ts +115 -0
  46. package/dist/types-CwwgQ_9H.d.ts +121 -0
  47. package/dist/types-Dje1TdH6.d.cts +391 -0
  48. package/dist/workflow.cjs +460 -321
  49. package/dist/workflow.cjs.map +1 -1
  50. package/dist/workflow.d.cts +271 -222
  51. package/dist/workflow.d.ts +271 -222
  52. package/dist/workflow.js +456 -319
  53. package/dist/workflow.js.map +1 -1
  54. package/package.json +65 -8
  55. package/src/adapters/sandbox/daytona/filesystem.ts +136 -0
  56. package/src/adapters/sandbox/daytona/index.ts +149 -0
  57. package/src/adapters/sandbox/daytona/types.ts +34 -0
  58. package/src/adapters/sandbox/inmemory/index.ts +213 -0
  59. package/src/adapters/sandbox/virtual/filesystem.ts +345 -0
  60. package/src/adapters/sandbox/virtual/index.ts +88 -0
  61. package/src/adapters/sandbox/virtual/mutations.ts +38 -0
  62. package/src/adapters/sandbox/virtual/provider.ts +101 -0
  63. package/src/adapters/sandbox/virtual/tree.ts +82 -0
  64. package/src/adapters/sandbox/virtual/types.ts +127 -0
  65. package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +523 -0
  66. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +91 -0
  67. package/src/adapters/thread/google-genai/activities.ts +132 -0
  68. package/src/adapters/thread/google-genai/index.ts +41 -0
  69. package/src/adapters/thread/google-genai/model-invoker.ts +154 -0
  70. package/src/adapters/thread/google-genai/thread-manager.ts +169 -0
  71. package/src/adapters/{langchain → thread/langchain}/activities.ts +22 -15
  72. package/src/adapters/{langchain → thread/langchain}/index.ts +1 -1
  73. package/src/adapters/{langchain → thread/langchain}/model-invoker.ts +15 -18
  74. package/src/adapters/{langchain → thread/langchain}/thread-manager.ts +1 -1
  75. package/src/index.ts +32 -24
  76. package/src/lib/activity.ts +87 -0
  77. package/src/lib/hooks/index.ts +11 -0
  78. package/src/lib/hooks/types.ts +98 -0
  79. package/src/lib/model/helpers.ts +6 -0
  80. package/src/lib/model/index.ts +13 -0
  81. package/src/lib/{model-invoker.ts → model/types.ts} +18 -1
  82. package/src/lib/sandbox/index.ts +19 -0
  83. package/src/lib/sandbox/manager.ts +76 -0
  84. package/src/lib/sandbox/sandbox.test.ts +158 -0
  85. package/src/lib/{fs.ts → sandbox/tree.ts} +6 -6
  86. package/src/lib/sandbox/types.ts +164 -0
  87. package/src/lib/session/index.ts +11 -0
  88. package/src/lib/{session.ts → session/session.ts} +83 -50
  89. package/src/lib/session/types.ts +95 -0
  90. package/src/lib/skills/fs-provider.ts +16 -15
  91. package/src/lib/skills/handler.ts +31 -0
  92. package/src/lib/skills/index.ts +5 -1
  93. package/src/lib/skills/register.ts +20 -0
  94. package/src/lib/skills/tool.ts +47 -0
  95. package/src/lib/state/index.ts +9 -0
  96. package/src/lib/{state-manager.ts → state/manager.ts} +10 -147
  97. package/src/lib/state/types.ts +134 -0
  98. package/src/lib/subagent/define.ts +71 -0
  99. package/src/lib/subagent/handler.ts +99 -0
  100. package/src/lib/subagent/index.ts +13 -0
  101. package/src/lib/subagent/register.ts +68 -0
  102. package/src/lib/subagent/tool.ts +80 -0
  103. package/src/lib/subagent/types.ts +92 -0
  104. package/src/lib/thread/index.ts +7 -0
  105. package/src/lib/{thread-manager.ts → thread/manager.ts} +20 -33
  106. package/src/lib/thread/types.ts +39 -0
  107. package/src/lib/tool-router/auto-append.ts +55 -0
  108. package/src/lib/tool-router/index.ts +41 -0
  109. package/src/lib/tool-router/router.ts +462 -0
  110. package/src/lib/tool-router/types.ts +478 -0
  111. package/src/lib/tool-router/with-sandbox.ts +70 -0
  112. package/src/lib/types.ts +5 -382
  113. package/src/tools/bash/bash.test.ts +53 -55
  114. package/src/tools/bash/handler.ts +23 -51
  115. package/src/tools/edit/handler.ts +67 -81
  116. package/src/tools/glob/handler.ts +60 -17
  117. package/src/tools/read-file/handler.ts +67 -0
  118. package/src/tools/read-skill/handler.ts +1 -31
  119. package/src/tools/read-skill/tool.ts +5 -47
  120. package/src/tools/subagent/handler.ts +1 -100
  121. package/src/tools/subagent/tool.ts +5 -93
  122. package/src/tools/task-create/handler.ts +1 -1
  123. package/src/tools/task-get/handler.ts +1 -1
  124. package/src/tools/task-list/handler.ts +1 -1
  125. package/src/tools/task-update/handler.ts +1 -1
  126. package/src/tools/write-file/handler.ts +47 -0
  127. package/src/workflow.ts +88 -47
  128. package/tsup.config.ts +8 -1
  129. package/dist/adapters/langchain/index.cjs.map +0 -1
  130. package/dist/adapters/langchain/index.js.map +0 -1
  131. package/dist/model-invoker-y_zlyMqu.d.cts +0 -892
  132. package/dist/model-invoker-y_zlyMqu.d.ts +0 -892
  133. package/src/lib/tool-router.ts +0 -977
  134. package/src/lib/workflow-helpers.ts +0 -50
  135. /package/src/lib/{thread-id.ts → thread/id.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { IFileSystem } from "just-bash";
1
+ import type { SandboxFileSystem } from "./types";
2
2
 
3
3
  const basename = (path: string, separator: string): string => {
4
4
  if (path[path.length - 1] === separator) path = path.slice(0, -1);
@@ -25,10 +25,10 @@ const printTree = async (
25
25
  };
26
26
 
27
27
  /**
28
- * Generates a formatted file tree string from an `IFileSystem` instance.
28
+ * Generates a formatted file tree string from a {@link SandboxFileSystem}.
29
29
  * Useful for including filesystem context in agent prompts.
30
30
  *
31
- * @param fs - File system implementation (e.g. from `just-bash`)
31
+ * @param fs - Sandbox filesystem implementation
32
32
  * @param opts - Optional configuration for tree generation
33
33
  * @param opts.dir - Root directory to start from (defaults to `/`)
34
34
  * @param opts.separator - Path separator (`/` or `\\`, defaults to `/`)
@@ -40,7 +40,7 @@ const printTree = async (
40
40
  * ```typescript
41
41
  * import { toTree } from 'zeitlich';
42
42
  *
43
- * const fileTree = await toTree(inMemoryFileSystem);
43
+ * const fileTree = await toTree(sandbox.fs);
44
44
  * // Returns:
45
45
  * // /
46
46
  * // ├─ src/
@@ -50,7 +50,7 @@ const printTree = async (
50
50
  * ```
51
51
  */
52
52
  export const toTree = async (
53
- fs: IFileSystem,
53
+ fs: SandboxFileSystem,
54
54
  opts: {
55
55
  dir?: string;
56
56
  separator?: "/" | "\\";
@@ -67,7 +67,7 @@ export const toTree = async (
67
67
  const sort = opts.sort ?? true;
68
68
  let subtree = " (...)";
69
69
  if (depth > 0) {
70
- const list = (await fs.readdirWithFileTypes?.(dir)) || [];
70
+ const list = await fs.readdirWithFileTypes(dir);
71
71
  if (sort) {
72
72
  list.sort((a, b) => {
73
73
  if (a.isDirectory && b.isDirectory) {
@@ -0,0 +1,164 @@
1
+ // ============================================================================
2
+ // Sandbox Filesystem
3
+ // ============================================================================
4
+
5
+ export interface DirentEntry {
6
+ name: string;
7
+ isFile: boolean;
8
+ isDirectory: boolean;
9
+ isSymbolicLink: boolean;
10
+ }
11
+
12
+ export interface FileStat {
13
+ isFile: boolean;
14
+ isDirectory: boolean;
15
+ isSymbolicLink: boolean;
16
+ size: number;
17
+ mtime: Date;
18
+ }
19
+
20
+ /**
21
+ * Provider-agnostic filesystem interface.
22
+ *
23
+ * Implementations that don't support a method should throw
24
+ * {@link SandboxNotSupportedError}.
25
+ */
26
+ export interface SandboxFileSystem {
27
+ readFile(path: string): Promise<string>;
28
+ readFileBuffer(path: string): Promise<Uint8Array>;
29
+ writeFile(path: string, content: string | Uint8Array): Promise<void>;
30
+ appendFile(path: string, content: string | Uint8Array): Promise<void>;
31
+ exists(path: string): Promise<boolean>;
32
+ stat(path: string): Promise<FileStat>;
33
+ mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
34
+ readdir(path: string): Promise<string[]>;
35
+ readdirWithFileTypes(path: string): Promise<DirentEntry[]>;
36
+ rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
37
+ cp(src: string, dest: string, options?: { recursive?: boolean }): Promise<void>;
38
+ mv(src: string, dest: string): Promise<void>;
39
+ readlink(path: string): Promise<string>;
40
+ resolvePath(base: string, path: string): string;
41
+ }
42
+
43
+ // ============================================================================
44
+ // Execution
45
+ // ============================================================================
46
+
47
+ export interface ExecOptions {
48
+ timeout?: number;
49
+ cwd?: string;
50
+ env?: Record<string, string>;
51
+ }
52
+
53
+ export interface ExecResult {
54
+ exitCode: number;
55
+ stdout: string;
56
+ stderr: string;
57
+ }
58
+
59
+ // ============================================================================
60
+ // Capabilities
61
+ // ============================================================================
62
+
63
+ export interface SandboxCapabilities {
64
+ /** Sandbox supports filesystem operations */
65
+ filesystem: boolean;
66
+ /** Sandbox supports shell/command execution */
67
+ execution: boolean;
68
+ /** Sandbox state can be persisted and restored */
69
+ persistence: boolean;
70
+ }
71
+
72
+ // ============================================================================
73
+ // Sandbox
74
+ // ============================================================================
75
+
76
+ export interface Sandbox {
77
+ readonly id: string;
78
+ readonly capabilities: SandboxCapabilities;
79
+ readonly fs: SandboxFileSystem;
80
+
81
+ exec(command: string, options?: ExecOptions): Promise<ExecResult>;
82
+ destroy(): Promise<void>;
83
+ }
84
+
85
+ // ============================================================================
86
+ // Snapshots
87
+ // ============================================================================
88
+
89
+ export interface SandboxSnapshot {
90
+ sandboxId: string;
91
+ providerId: string;
92
+ /** Provider-specific serialised state */
93
+ data: unknown;
94
+ createdAt: string;
95
+ }
96
+
97
+ // ============================================================================
98
+ // Provider
99
+ // ============================================================================
100
+
101
+ export interface SandboxCreateOptions {
102
+ /** Preferred sandbox ID (provider may ignore) */
103
+ id?: string;
104
+ /** Seed the filesystem with these files */
105
+ initialFiles?: Record<string, string | Uint8Array>;
106
+ /** Environment variables available inside the sandbox */
107
+ env?: Record<string, string>;
108
+ }
109
+
110
+ export interface SandboxCreateResult {
111
+ sandbox: Sandbox;
112
+ /** Optional state to merge into the workflow's `AgentState` via the session. */
113
+ stateUpdate?: Record<string, unknown>;
114
+ }
115
+
116
+ export interface SandboxProvider<
117
+ TOptions extends SandboxCreateOptions = SandboxCreateOptions,
118
+ TSandbox extends Sandbox = Sandbox,
119
+ > {
120
+ readonly id: string;
121
+ readonly capabilities: SandboxCapabilities;
122
+
123
+ create(options?: TOptions): Promise<SandboxCreateResult>;
124
+ get(sandboxId: string): Promise<TSandbox>;
125
+ destroy(sandboxId: string): Promise<void>;
126
+ snapshot(sandboxId: string): Promise<SandboxSnapshot>;
127
+ restore(snapshot: SandboxSnapshot): Promise<Sandbox>;
128
+ }
129
+
130
+ // ============================================================================
131
+ // SandboxOps — workflow-side activity interface (like ThreadOps)
132
+ // ============================================================================
133
+
134
+ export interface SandboxOps<
135
+ TOptions extends SandboxCreateOptions = SandboxCreateOptions,
136
+ > {
137
+ createSandbox(
138
+ options?: TOptions,
139
+ ): Promise<{ sandboxId: string; stateUpdate?: Record<string, unknown> }>;
140
+ destroySandbox(sandboxId: string): Promise<void>;
141
+ snapshotSandbox(sandboxId: string): Promise<SandboxSnapshot>;
142
+ }
143
+
144
+ // ============================================================================
145
+ // Errors
146
+ // ============================================================================
147
+
148
+ import { ApplicationFailure } from "@temporalio/common";
149
+
150
+ export class SandboxNotSupportedError extends ApplicationFailure {
151
+ constructor(operation: string) {
152
+ super(
153
+ `Sandbox does not support: ${operation}`,
154
+ "SandboxNotSupportedError",
155
+ true,
156
+ );
157
+ }
158
+ }
159
+
160
+ export class SandboxNotFoundError extends ApplicationFailure {
161
+ constructor(sandboxId: string) {
162
+ super(`Sandbox not found: ${sandboxId}`, "SandboxNotFoundError", true);
163
+ }
164
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ createSession,
3
+ proxyDefaultThreadOps,
4
+ proxySandboxOps,
5
+ } from "./session";
6
+
7
+ export type {
8
+ ThreadOps,
9
+ SessionConfig,
10
+ ZeitlichSession,
11
+ } from "./types";
@@ -5,42 +5,15 @@ import {
5
5
  setHandler,
6
6
  ApplicationFailure,
7
7
  } from "@temporalio/workflow";
8
- import type {
9
- ThreadOps,
10
- AgentConfig,
11
- SessionStartHook,
12
- SessionEndHook,
13
- SessionExitReason,
14
- SessionConfig,
15
- } from "./types";
16
- import { type AgentStateManager, type JsonSerializable } from "./state-manager";
17
- import {
18
- createToolRouter,
19
- type ParsedToolCallUnion,
20
- type ToolMap,
21
- } from "./tool-router";
22
- import type { MessageContent } from "./types";
23
- import { getShortId } from "./thread-id";
24
-
25
- export interface ZeitlichSession<M = unknown> {
26
- runSession<T extends JsonSerializable<T>>(args: {
27
- stateManager: AgentStateManager<T>;
28
- }): Promise<{
29
- finalMessage: M | null;
30
- exitReason: SessionExitReason;
31
- usage: ReturnType<AgentStateManager<T>["getTotalUsage"]>;
32
- }>;
33
- }
34
-
35
- /**
36
- * Session-level hooks for lifecycle events
37
- */
38
- export interface SessionLifecycleHooks {
39
- /** Called when session starts */
40
- onSessionStart?: SessionStartHook;
41
- /** Called when session ends */
42
- onSessionEnd?: SessionEndHook;
43
- }
8
+ import type { SessionExitReason, MessageContent } from "../types";
9
+ import type { ThreadOps, SessionConfig, ZeitlichSession } from "./types";
10
+ import type { SandboxOps } from "../sandbox/types";
11
+ import { type AgentStateManager, type JsonSerializable } from "../state/types";
12
+ import { createToolRouter } from "../tool-router/router";
13
+ import type { ParsedToolCallUnion, ToolMap } from "../tool-router/types";
14
+ import { getShortId } from "../thread/id";
15
+ import { buildSubagentRegistration } from "../subagent/register";
16
+ import { buildSkillRegistration } from "../skills/register";
44
17
 
45
18
  /**
46
19
  * Creates an agent session that manages the agent loop: LLM invocation,
@@ -89,27 +62,40 @@ export const createSession = async <T extends ToolMap, M = unknown>({
89
62
  appendSystemPrompt = true,
90
63
  continueThread = false,
91
64
  waitForInputTimeout = "48h",
92
- }: SessionConfig<T, M> & AgentConfig): Promise<ZeitlichSession<M>> => {
93
- const threadId = providedThreadId ?? getShortId();
65
+ sandbox: sandboxOps,
66
+ sandboxId: inheritedSandboxId,
67
+ }: SessionConfig<T, M>): Promise<ZeitlichSession<M>> => {
68
+ const sourceThreadId = continueThread ? providedThreadId : undefined;
69
+ const threadId =
70
+ continueThread && providedThreadId ? getShortId() : (providedThreadId ?? getShortId());
94
71
 
95
72
  const {
96
73
  appendToolResult,
97
74
  appendHumanMessage,
98
75
  initializeThread,
99
76
  appendSystemMessage,
77
+ forkThread,
100
78
  } = threadOps ?? proxyDefaultThreadOps();
101
79
 
80
+ const plugins: ToolMap[string][] = [];
81
+ if (subagents) {
82
+ const reg = buildSubagentRegistration(subagents);
83
+ if (reg) plugins.push(reg);
84
+ }
85
+ if (skills) {
86
+ const reg = buildSkillRegistration(skills);
87
+ if (reg) plugins.push(reg);
88
+ }
89
+
102
90
  const toolRouter = createToolRouter({
103
91
  tools,
104
92
  appendToolResult,
105
93
  threadId,
106
94
  hooks,
107
- subagents,
108
- skills,
95
+ plugins,
109
96
  parallel: processToolsInParallel,
110
97
  });
111
98
 
112
- // Helper to call session end hook
113
99
  const callSessionEnd = async (
114
100
  exitReason: SessionExitReason,
115
101
  turns: number
@@ -126,12 +112,15 @@ export const createSession = async <T extends ToolMap, M = unknown>({
126
112
  };
127
113
 
128
114
  return {
129
- runSession: async ({
115
+ runSession: async <TState extends JsonSerializable<TState>>({
130
116
  stateManager,
117
+ }: {
118
+ stateManager: AgentStateManager<TState>;
131
119
  }): Promise<{
120
+ threadId: string;
132
121
  finalMessage: M | null;
133
122
  exitReason: SessionExitReason;
134
- usage: ReturnType<typeof stateManager.getTotalUsage>;
123
+ usage: ReturnType<AgentStateManager<TState>["getTotalUsage"]>;
135
124
  }> => {
136
125
  setHandler(
137
126
  defineUpdate<unknown, [MessageContent]>(`add${agentName}Message`),
@@ -153,6 +142,19 @@ export const createSession = async <T extends ToolMap, M = unknown>({
153
142
  }
154
143
  );
155
144
 
145
+ // --- Sandbox lifecycle: create or inherit ---
146
+ let sandboxId: string | undefined = inheritedSandboxId;
147
+ const ownsSandbox = !sandboxId && !!sandboxOps;
148
+ if (ownsSandbox) {
149
+ const result = await sandboxOps.createSandbox({ id: threadId });
150
+ sandboxId = result.sandboxId;
151
+ if (result.stateUpdate) {
152
+ stateManager.mergeUpdate(
153
+ result.stateUpdate as Partial<TState>,
154
+ );
155
+ }
156
+ }
157
+
156
158
  if (hooks.onSessionStart) {
157
159
  await hooks.onSessionStart({
158
160
  threadId,
@@ -163,7 +165,9 @@ export const createSession = async <T extends ToolMap, M = unknown>({
163
165
 
164
166
  const systemPrompt = stateManager.getSystemPrompt();
165
167
 
166
- if (!continueThread) {
168
+ if (continueThread && sourceThreadId) {
169
+ await forkThread(sourceThreadId, threadId);
170
+ } else {
167
171
  if (appendSystemPrompt) {
168
172
  if (!systemPrompt || systemPrompt.trim() === "") {
169
173
  throw ApplicationFailure.create({
@@ -201,18 +205,17 @@ export const createSession = async <T extends ToolMap, M = unknown>({
201
205
  stateManager.updateUsage(usage);
202
206
  }
203
207
 
204
- // No tools configured - treat any non-end_turn as completed
205
208
  if (!toolRouter.hasTools() || rawToolCalls.length === 0) {
206
209
  stateManager.complete();
207
210
  exitReason = "completed";
208
211
  return {
212
+ threadId,
209
213
  finalMessage: message,
210
214
  exitReason,
211
215
  usage: stateManager.getTotalUsage(),
212
216
  };
213
217
  }
214
218
 
215
- // Parse all tool calls uniformly through the router
216
219
  const parsedToolCalls: ParsedToolCallUnion<T>[] = [];
217
220
  for (const tc of rawToolCalls) {
218
221
  try {
@@ -229,11 +232,11 @@ export const createSession = async <T extends ToolMap, M = unknown>({
229
232
  }
230
233
  }
231
234
 
232
- // Hooks can call stateManager.waitForInput() to pause the session
233
235
  const toolCallResults = await toolRouter.processToolCalls(
234
236
  parsedToolCalls,
235
237
  {
236
238
  turn: currentTurn,
239
+ ...(sandboxId !== undefined && { sandboxId }),
237
240
  }
238
241
  );
239
242
 
@@ -250,14 +253,12 @@ export const createSession = async <T extends ToolMap, M = unknown>({
250
253
  );
251
254
  if (!conditionMet) {
252
255
  stateManager.cancel();
253
- // Wait briefly to allow pending waitForStateChange handlers to complete
254
256
  await condition(() => false, "2s");
255
257
  break;
256
258
  }
257
259
  }
258
260
  }
259
261
 
260
- // Check if we hit max turns
261
262
  if (stateManager.getTurns() >= maxTurns && stateManager.isRunning()) {
262
263
  exitReason = "max_turns";
263
264
  }
@@ -265,11 +266,15 @@ export const createSession = async <T extends ToolMap, M = unknown>({
265
266
  exitReason = "failed";
266
267
  throw ApplicationFailure.fromError(error);
267
268
  } finally {
268
- // SessionEnd hook - always called
269
269
  await callSessionEnd(exitReason, stateManager.getTurns());
270
+
271
+ if (ownsSandbox && sandboxId && sandboxOps) {
272
+ await sandboxOps.destroySandbox(sandboxId);
273
+ }
270
274
  }
271
275
 
272
276
  return {
277
+ threadId,
273
278
  finalMessage: null,
274
279
  exitReason,
275
280
  usage: stateManager.getTotalUsage(),
@@ -306,3 +311,31 @@ export function proxyDefaultThreadOps(
306
311
  }
307
312
  );
308
313
  }
314
+
315
+ /**
316
+ * Proxy sandbox lifecycle operations as Temporal activities.
317
+ * Call this in workflow code when the agent needs a sandbox.
318
+ *
319
+ * @example
320
+ * ```typescript
321
+ * const session = await createSession({
322
+ * sandbox: proxySandboxOps(),
323
+ * // ...
324
+ * });
325
+ * ```
326
+ */
327
+ export function proxySandboxOps(
328
+ options?: Parameters<typeof proxyActivities>[0]
329
+ ): SandboxOps {
330
+ return proxyActivities<SandboxOps>(
331
+ options ?? {
332
+ startToCloseTimeout: "30s",
333
+ retry: {
334
+ maximumAttempts: 3,
335
+ initialInterval: "2s",
336
+ maximumInterval: "30s",
337
+ backoffCoefficient: 2,
338
+ },
339
+ }
340
+ );
341
+ }
@@ -0,0 +1,95 @@
1
+ import type { Duration } from "@temporalio/common";
2
+ import type {
3
+ MessageContent,
4
+ ToolResultConfig,
5
+ SessionExitReason,
6
+ } from "../types";
7
+ import type {
8
+ ToolMap,
9
+ ToolCallResultUnion,
10
+ InferToolResults,
11
+ } from "../tool-router/types";
12
+ import type { Hooks } from "../hooks/types";
13
+ import type { SubagentConfig } from "../subagent/types";
14
+ import type { Skill } from "../skills/types";
15
+ import type { SandboxOps } from "../sandbox/types";
16
+ import type { RunAgentActivity } from "../model/types";
17
+ import type { AgentStateManager, JsonSerializable } from "../state/types";
18
+
19
+ /**
20
+ * Thread operations required by a session.
21
+ * Consumers provide these — typically by wrapping Temporal activities.
22
+ */
23
+ export interface ThreadOps {
24
+ /** Initialize an empty thread */
25
+ initializeThread(threadId: string): Promise<void>;
26
+ /** Append a human message to the thread */
27
+ appendHumanMessage(
28
+ threadId: string,
29
+ content: string | MessageContent
30
+ ): Promise<void>;
31
+ /** Append a tool result to the thread */
32
+ appendToolResult(config: ToolResultConfig): Promise<void>;
33
+ /** Append a system message to the thread */
34
+ appendSystemMessage(threadId: string, content: string): Promise<void>;
35
+ /** Copy all messages from sourceThreadId into a new thread at targetThreadId */
36
+ forkThread(sourceThreadId: string, targetThreadId: string): Promise<void>;
37
+ }
38
+
39
+ /**
40
+ * Configuration for a Zeitlich agent session
41
+ */
42
+ export interface SessionConfig<T extends ToolMap, M = unknown> {
43
+ /** The name of the agent, should be unique within the workflows */
44
+ agentName: string;
45
+ /** The thread ID to use for the session (defaults to a short generated ID) */
46
+ threadId?: string;
47
+ /** Metadata for the session */
48
+ metadata?: Record<string, unknown>;
49
+ /** Whether to append the system prompt as message to the thread */
50
+ appendSystemPrompt?: boolean;
51
+ /** How many turns to run the session for */
52
+ maxTurns?: number;
53
+ /** Workflow-specific runAgent activity (with tools pre-bound) */
54
+ runAgent: RunAgentActivity<M>;
55
+ /** Thread operations (initialize, append messages, parse tool calls) */
56
+ threadOps?: ThreadOps;
57
+ /** Tool router for processing tool calls (optional if agent has no tools) */
58
+ tools?: T;
59
+ /** Subagent configurations */
60
+ subagents?: SubagentConfig[];
61
+ /** Skills available to this agent (metadata + instructions, loaded activity-side) */
62
+ skills?: Skill[];
63
+ /** Session lifecycle hooks */
64
+ hooks?: Hooks<T, ToolCallResultUnion<InferToolResults<T>>>;
65
+ /** Whether to process tools in parallel */
66
+ processToolsInParallel?: boolean;
67
+ /**
68
+ * Build context message content from agent-specific context.
69
+ * Returns MessageContent array for the initial HumanMessage.
70
+ */
71
+ buildContextMessage: () => MessageContent | Promise<MessageContent>;
72
+ /** When true, skip thread initialization and system prompt — append only the new human message to the existing thread. */
73
+ continueThread?: boolean;
74
+ /** How long to wait for input before cancelling the workflow */
75
+ waitForInputTimeout?: Duration;
76
+ /** Sandbox lifecycle operations (optional — omit for agents that don't need a sandbox) */
77
+ sandbox?: SandboxOps;
78
+ /**
79
+ * Pre-existing sandbox ID to reuse (e.g. inherited from a parent agent).
80
+ * When set, the session skips `createSandbox` and will not destroy the
81
+ * sandbox on exit (the owner is responsible for cleanup).
82
+ */
83
+ sandboxId?: string;
84
+ }
85
+
86
+ export interface ZeitlichSession<M = unknown> {
87
+ runSession<T extends JsonSerializable<T>>(args: {
88
+ stateManager: AgentStateManager<T>;
89
+ }): Promise<{
90
+ threadId: string;
91
+ finalMessage: M | null;
92
+ exitReason: SessionExitReason;
93
+ usage: ReturnType<AgentStateManager<T>["getTotalUsage"]>;
94
+ }>;
95
+ }
@@ -1,10 +1,10 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
1
  import { join } from "node:path";
2
+ import type { SandboxFileSystem } from "../sandbox/types";
3
3
  import type { Skill, SkillMetadata, SkillProvider } from "./types";
4
4
  import { parseSkillFile } from "./parse";
5
5
 
6
6
  /**
7
- * Loads skills from a filesystem directory following the agentskills.io layout:
7
+ * Loads skills from a directory following the agentskills.io layout:
8
8
  *
9
9
  * ```
10
10
  * skills/
@@ -14,17 +14,21 @@ import { parseSkillFile } from "./parse";
14
14
  * │ └── SKILL.md
15
15
  * ```
16
16
  *
17
- * Activity-side only cannot be used in Temporal workflow code.
17
+ * Uses the sandbox filesystem abstraction works with any backend
18
+ * (in-memory, host FS, Wasmer, Daytona, etc.).
18
19
  */
19
20
  export class FileSystemSkillProvider implements SkillProvider {
20
- constructor(private readonly baseDir: string) {}
21
+ constructor(
22
+ private readonly fs: SandboxFileSystem,
23
+ private readonly baseDir: string,
24
+ ) {}
21
25
 
22
26
  async listSkills(): Promise<SkillMetadata[]> {
23
27
  const dirs = await this.discoverSkillDirs();
24
28
  const skills: SkillMetadata[] = [];
25
29
 
26
30
  for (const dir of dirs) {
27
- const raw = await readFile(join(this.baseDir, dir, "SKILL.md"), "utf-8");
31
+ const raw = await this.fs.readFile(join(this.baseDir, dir, "SKILL.md"));
28
32
  const { frontmatter } = parseSkillFile(raw);
29
33
  skills.push(frontmatter);
30
34
  }
@@ -33,15 +37,14 @@ export class FileSystemSkillProvider implements SkillProvider {
33
37
  }
34
38
 
35
39
  async getSkill(name: string): Promise<Skill> {
36
- const raw = await readFile(
40
+ const raw = await this.fs.readFile(
37
41
  join(this.baseDir, name, "SKILL.md"),
38
- "utf-8"
39
42
  );
40
43
  const { frontmatter, body } = parseSkillFile(raw);
41
44
 
42
45
  if (frontmatter.name !== name) {
43
46
  throw new Error(
44
- `Skill directory "${name}" contains SKILL.md with mismatched name "${frontmatter.name}"`
47
+ `Skill directory "${name}" contains SKILL.md with mismatched name "${frontmatter.name}"`,
45
48
  );
46
49
  }
47
50
 
@@ -57,7 +60,7 @@ export class FileSystemSkillProvider implements SkillProvider {
57
60
  const skills: Skill[] = [];
58
61
 
59
62
  for (const dir of dirs) {
60
- const raw = await readFile(join(this.baseDir, dir, "SKILL.md"), "utf-8");
63
+ const raw = await this.fs.readFile(join(this.baseDir, dir, "SKILL.md"));
61
64
  const { frontmatter, body } = parseSkillFile(raw);
62
65
  skills.push({ ...frontmatter, instructions: body });
63
66
  }
@@ -66,16 +69,14 @@ export class FileSystemSkillProvider implements SkillProvider {
66
69
  }
67
70
 
68
71
  private async discoverSkillDirs(): Promise<string[]> {
69
- const entries = await readdir(this.baseDir, { withFileTypes: true });
72
+ const entries = await this.fs.readdirWithFileTypes(this.baseDir);
70
73
  const dirs: string[] = [];
71
74
 
72
75
  for (const entry of entries) {
73
- if (!entry.isDirectory()) continue;
74
- try {
75
- await readFile(join(this.baseDir, entry.name, "SKILL.md"), "utf-8");
76
+ if (!entry.isDirectory) continue;
77
+ const skillPath = join(this.baseDir, entry.name, "SKILL.md");
78
+ if (await this.fs.exists(skillPath)) {
76
79
  dirs.push(entry.name);
77
- } catch {
78
- // No SKILL.md — skip
79
80
  }
80
81
  }
81
82
 
@@ -0,0 +1,31 @@
1
+ import type { Skill } from "./types";
2
+ import type { ToolHandlerResponse } from "../tool-router";
3
+ import type { ReadSkillArgs } from "./tool";
4
+
5
+ /**
6
+ * Creates a ReadSkill handler that looks up skills from an in-memory array.
7
+ * Runs directly in the workflow (like task tools) — no activity needed.
8
+ */
9
+ export function createReadSkillHandler(
10
+ skills: Skill[]
11
+ ): (args: ReadSkillArgs) => ToolHandlerResponse<null> {
12
+ const skillMap = new Map(skills.map((s) => [s.name, s]));
13
+
14
+ return (args: ReadSkillArgs): ToolHandlerResponse<null> => {
15
+ const skill = skillMap.get(args.skill_name);
16
+
17
+ if (!skill) {
18
+ return {
19
+ toolResponse: JSON.stringify({
20
+ error: `Skill "${args.skill_name}" not found`,
21
+ }),
22
+ data: null,
23
+ };
24
+ }
25
+
26
+ return {
27
+ toolResponse: skill.instructions,
28
+ data: null,
29
+ };
30
+ };
31
+ }
@@ -1,3 +1,7 @@
1
1
  export type { Skill, SkillMetadata, SkillProvider } from "./types";
2
2
  export { parseSkillFile } from "./parse";
3
- export { FileSystemSkillProvider } from "./fs-provider";
3
+ export { createReadSkillTool, READ_SKILL_TOOL_NAME } from "./tool";
4
+ export type { ReadSkillArgs } from "./tool";
5
+ export { createReadSkillHandler } from "./handler";
6
+ export { buildSkillRegistration } from "./register";
7
+