zeitlich 0.2.13 → 0.2.14

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 +49 -38
  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 +284 -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 +278 -0
  25. package/dist/adapters/thread/google-genai/index.js.map +1 -0
  26. package/dist/adapters/{langchain → thread/langchain}/index.cjs +7 -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 +7 -9
  31. package/dist/adapters/thread/langchain/index.js.map +1 -0
  32. package/dist/index.cjs +816 -545
  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 +804 -540
  37. package/dist/index.js.map +1 -1
  38. package/dist/types-B4C9txdq.d.ts +389 -0
  39. package/dist/{thread-manager-qc0g5Rvd.d.cts → types-B9ljZewB.d.cts} +1 -6
  40. package/dist/{thread-manager-qc0g5Rvd.d.ts → types-B9ljZewB.d.ts} +1 -6
  41. package/dist/types-BMXzv7TN.d.cts +476 -0
  42. package/dist/types-BMXzv7TN.d.ts +476 -0
  43. package/dist/types-BVP87m_W.d.cts +121 -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-GpMU4b0w.d.cts +389 -0
  48. package/dist/workflow.cjs +444 -318
  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 +440 -316
  53. package/dist/workflow.js.map +1 -1
  54. package/package.json +59 -6
  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 +121 -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 +11 -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} +76 -48
  89. package/src/lib/session/types.ts +93 -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 +53 -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} +1 -33
  106. package/src/lib/thread/types.ts +33 -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,7 +62,9 @@ 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>> => {
65
+ sandbox: sandboxOps,
66
+ sandboxId: inheritedSandboxId,
67
+ }: SessionConfig<T, M>): Promise<ZeitlichSession<M>> => {
93
68
  const threadId = providedThreadId ?? getShortId();
94
69
 
95
70
  const {
@@ -99,17 +74,25 @@ export const createSession = async <T extends ToolMap, M = unknown>({
99
74
  appendSystemMessage,
100
75
  } = threadOps ?? proxyDefaultThreadOps();
101
76
 
77
+ const plugins: ToolMap[string][] = [];
78
+ if (subagents) {
79
+ const reg = buildSubagentRegistration(subagents);
80
+ if (reg) plugins.push(reg);
81
+ }
82
+ if (skills) {
83
+ const reg = buildSkillRegistration(skills);
84
+ if (reg) plugins.push(reg);
85
+ }
86
+
102
87
  const toolRouter = createToolRouter({
103
88
  tools,
104
89
  appendToolResult,
105
90
  threadId,
106
91
  hooks,
107
- subagents,
108
- skills,
92
+ plugins,
109
93
  parallel: processToolsInParallel,
110
94
  });
111
95
 
112
- // Helper to call session end hook
113
96
  const callSessionEnd = async (
114
97
  exitReason: SessionExitReason,
115
98
  turns: number
@@ -126,12 +109,15 @@ export const createSession = async <T extends ToolMap, M = unknown>({
126
109
  };
127
110
 
128
111
  return {
129
- runSession: async ({
112
+ runSession: async <TState extends JsonSerializable<TState>>({
130
113
  stateManager,
114
+ }: {
115
+ stateManager: AgentStateManager<TState>;
131
116
  }): Promise<{
117
+ threadId: string;
132
118
  finalMessage: M | null;
133
119
  exitReason: SessionExitReason;
134
- usage: ReturnType<typeof stateManager.getTotalUsage>;
120
+ usage: ReturnType<AgentStateManager<TState>["getTotalUsage"]>;
135
121
  }> => {
136
122
  setHandler(
137
123
  defineUpdate<unknown, [MessageContent]>(`add${agentName}Message`),
@@ -153,6 +139,19 @@ export const createSession = async <T extends ToolMap, M = unknown>({
153
139
  }
154
140
  );
155
141
 
142
+ // --- Sandbox lifecycle: create or inherit ---
143
+ let sandboxId: string | undefined = inheritedSandboxId;
144
+ const ownsSandbox = !sandboxId && !!sandboxOps;
145
+ if (ownsSandbox) {
146
+ const result = await sandboxOps.createSandbox({ id: threadId });
147
+ sandboxId = result.sandboxId;
148
+ if (result.stateUpdate) {
149
+ stateManager.mergeUpdate(
150
+ result.stateUpdate as Partial<TState>,
151
+ );
152
+ }
153
+ }
154
+
156
155
  if (hooks.onSessionStart) {
157
156
  await hooks.onSessionStart({
158
157
  threadId,
@@ -201,18 +200,17 @@ export const createSession = async <T extends ToolMap, M = unknown>({
201
200
  stateManager.updateUsage(usage);
202
201
  }
203
202
 
204
- // No tools configured - treat any non-end_turn as completed
205
203
  if (!toolRouter.hasTools() || rawToolCalls.length === 0) {
206
204
  stateManager.complete();
207
205
  exitReason = "completed";
208
206
  return {
207
+ threadId,
209
208
  finalMessage: message,
210
209
  exitReason,
211
210
  usage: stateManager.getTotalUsage(),
212
211
  };
213
212
  }
214
213
 
215
- // Parse all tool calls uniformly through the router
216
214
  const parsedToolCalls: ParsedToolCallUnion<T>[] = [];
217
215
  for (const tc of rawToolCalls) {
218
216
  try {
@@ -229,11 +227,11 @@ export const createSession = async <T extends ToolMap, M = unknown>({
229
227
  }
230
228
  }
231
229
 
232
- // Hooks can call stateManager.waitForInput() to pause the session
233
230
  const toolCallResults = await toolRouter.processToolCalls(
234
231
  parsedToolCalls,
235
232
  {
236
233
  turn: currentTurn,
234
+ ...(sandboxId !== undefined && { sandboxId }),
237
235
  }
238
236
  );
239
237
 
@@ -250,14 +248,12 @@ export const createSession = async <T extends ToolMap, M = unknown>({
250
248
  );
251
249
  if (!conditionMet) {
252
250
  stateManager.cancel();
253
- // Wait briefly to allow pending waitForStateChange handlers to complete
254
251
  await condition(() => false, "2s");
255
252
  break;
256
253
  }
257
254
  }
258
255
  }
259
256
 
260
- // Check if we hit max turns
261
257
  if (stateManager.getTurns() >= maxTurns && stateManager.isRunning()) {
262
258
  exitReason = "max_turns";
263
259
  }
@@ -265,11 +261,15 @@ export const createSession = async <T extends ToolMap, M = unknown>({
265
261
  exitReason = "failed";
266
262
  throw ApplicationFailure.fromError(error);
267
263
  } finally {
268
- // SessionEnd hook - always called
269
264
  await callSessionEnd(exitReason, stateManager.getTurns());
265
+
266
+ if (ownsSandbox && sandboxId && sandboxOps) {
267
+ await sandboxOps.destroySandbox(sandboxId);
268
+ }
270
269
  }
271
270
 
272
271
  return {
272
+ threadId,
273
273
  finalMessage: null,
274
274
  exitReason,
275
275
  usage: stateManager.getTotalUsage(),
@@ -306,3 +306,31 @@ export function proxyDefaultThreadOps(
306
306
  }
307
307
  );
308
308
  }
309
+
310
+ /**
311
+ * Proxy sandbox lifecycle operations as Temporal activities.
312
+ * Call this in workflow code when the agent needs a sandbox.
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * const session = await createSession({
317
+ * sandbox: proxySandboxOps(),
318
+ * // ...
319
+ * });
320
+ * ```
321
+ */
322
+ export function proxySandboxOps(
323
+ options?: Parameters<typeof proxyActivities>[0]
324
+ ): SandboxOps {
325
+ return proxyActivities<SandboxOps>(
326
+ options ?? {
327
+ startToCloseTimeout: "30s",
328
+ retry: {
329
+ maximumAttempts: 3,
330
+ initialInterval: "2s",
331
+ maximumInterval: "30s",
332
+ backoffCoefficient: 2,
333
+ },
334
+ }
335
+ );
336
+ }
@@ -0,0 +1,93 @@
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
+ }
36
+
37
+ /**
38
+ * Configuration for a Zeitlich agent session
39
+ */
40
+ export interface SessionConfig<T extends ToolMap, M = unknown> {
41
+ /** The name of the agent, should be unique within the workflows */
42
+ agentName: string;
43
+ /** The thread ID to use for the session (defaults to a short generated ID) */
44
+ threadId?: string;
45
+ /** Metadata for the session */
46
+ metadata?: Record<string, unknown>;
47
+ /** Whether to append the system prompt as message to the thread */
48
+ appendSystemPrompt?: boolean;
49
+ /** How many turns to run the session for */
50
+ maxTurns?: number;
51
+ /** Workflow-specific runAgent activity (with tools pre-bound) */
52
+ runAgent: RunAgentActivity<M>;
53
+ /** Thread operations (initialize, append messages, parse tool calls) */
54
+ threadOps?: ThreadOps;
55
+ /** Tool router for processing tool calls (optional if agent has no tools) */
56
+ tools?: T;
57
+ /** Subagent configurations */
58
+ subagents?: SubagentConfig[];
59
+ /** Skills available to this agent (metadata + instructions, loaded activity-side) */
60
+ skills?: Skill[];
61
+ /** Session lifecycle hooks */
62
+ hooks?: Hooks<T, ToolCallResultUnion<InferToolResults<T>>>;
63
+ /** Whether to process tools in parallel */
64
+ processToolsInParallel?: boolean;
65
+ /**
66
+ * Build context message content from agent-specific context.
67
+ * Returns MessageContent array for the initial HumanMessage.
68
+ */
69
+ buildContextMessage: () => MessageContent | Promise<MessageContent>;
70
+ /** When true, skip thread initialization and system prompt — append only the new human message to the existing thread. */
71
+ continueThread?: boolean;
72
+ /** How long to wait for input before cancelling the workflow */
73
+ waitForInputTimeout?: Duration;
74
+ /** Sandbox lifecycle operations (optional — omit for agents that don't need a sandbox) */
75
+ sandbox?: SandboxOps;
76
+ /**
77
+ * Pre-existing sandbox ID to reuse (e.g. inherited from a parent agent).
78
+ * When set, the session skips `createSandbox` and will not destroy the
79
+ * sandbox on exit (the owner is responsible for cleanup).
80
+ */
81
+ sandboxId?: string;
82
+ }
83
+
84
+ export interface ZeitlichSession<M = unknown> {
85
+ runSession<T extends JsonSerializable<T>>(args: {
86
+ stateManager: AgentStateManager<T>;
87
+ }): Promise<{
88
+ threadId: string;
89
+ finalMessage: M | null;
90
+ exitReason: SessionExitReason;
91
+ usage: ReturnType<AgentStateManager<T>["getTotalUsage"]>;
92
+ }>;
93
+ }
@@ -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
+
@@ -0,0 +1,20 @@
1
+ import type { ToolMap } from "../tool-router/types";
2
+ import type { Skill } from "./types";
3
+ import { createReadSkillTool } from "./tool";
4
+ import { createReadSkillHandler } from "./handler";
5
+
6
+ /**
7
+ * Builds a fully wired tool entry for the ReadSkill tool.
8
+ *
9
+ * Returns null if no skills are provided.
10
+ */
11
+ export function buildSkillRegistration(
12
+ skills: Skill[]
13
+ ): ToolMap[string] | null {
14
+ if (skills.length === 0) return null;
15
+
16
+ return {
17
+ ...createReadSkillTool(skills),
18
+ handler: createReadSkillHandler(skills),
19
+ };
20
+ }