zeitlich 0.2.21 → 0.2.23

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 (129) hide show
  1. package/README.md +303 -105
  2. package/dist/adapters/sandbox/daytona/index.cjs +7 -1
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  4. package/dist/adapters/sandbox/daytona/index.d.cts +3 -1
  5. package/dist/adapters/sandbox/daytona/index.d.ts +3 -1
  6. package/dist/adapters/sandbox/daytona/index.js +7 -1
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  8. package/dist/adapters/sandbox/daytona/workflow.cjs +33 -0
  9. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -0
  10. package/dist/adapters/sandbox/daytona/workflow.d.cts +27 -0
  11. package/dist/adapters/sandbox/daytona/workflow.d.ts +27 -0
  12. package/dist/adapters/sandbox/daytona/workflow.js +31 -0
  13. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -0
  14. package/dist/adapters/sandbox/inmemory/index.cjs +18 -1
  15. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  16. package/dist/adapters/sandbox/inmemory/index.d.cts +4 -2
  17. package/dist/adapters/sandbox/inmemory/index.d.ts +4 -2
  18. package/dist/adapters/sandbox/inmemory/index.js +18 -1
  19. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  20. package/dist/adapters/sandbox/inmemory/workflow.cjs +33 -0
  21. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -0
  22. package/dist/adapters/sandbox/inmemory/workflow.d.cts +25 -0
  23. package/dist/adapters/sandbox/inmemory/workflow.d.ts +25 -0
  24. package/dist/adapters/sandbox/inmemory/workflow.js +31 -0
  25. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -0
  26. package/dist/adapters/sandbox/virtual/index.cjs +36 -9
  27. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  28. package/dist/adapters/sandbox/virtual/index.d.cts +8 -5
  29. package/dist/adapters/sandbox/virtual/index.d.ts +8 -5
  30. package/dist/adapters/sandbox/virtual/index.js +36 -9
  31. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  32. package/dist/adapters/sandbox/virtual/workflow.cjs +33 -0
  33. package/dist/adapters/sandbox/virtual/workflow.cjs.map +1 -0
  34. package/dist/adapters/sandbox/virtual/workflow.d.cts +27 -0
  35. package/dist/adapters/sandbox/virtual/workflow.d.ts +27 -0
  36. package/dist/adapters/sandbox/virtual/workflow.js +31 -0
  37. package/dist/adapters/sandbox/virtual/workflow.js.map +1 -0
  38. package/dist/adapters/thread/google-genai/index.cjs +9 -1
  39. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  40. package/dist/adapters/thread/google-genai/index.d.cts +31 -19
  41. package/dist/adapters/thread/google-genai/index.d.ts +31 -19
  42. package/dist/adapters/thread/google-genai/index.js +9 -1
  43. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  44. package/dist/adapters/thread/google-genai/workflow.cjs +33 -0
  45. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -0
  46. package/dist/adapters/thread/google-genai/workflow.d.cts +32 -0
  47. package/dist/adapters/thread/google-genai/workflow.d.ts +32 -0
  48. package/dist/adapters/thread/google-genai/workflow.js +31 -0
  49. package/dist/adapters/thread/google-genai/workflow.js.map +1 -0
  50. package/dist/adapters/thread/langchain/index.cjs +9 -1
  51. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  52. package/dist/adapters/thread/langchain/index.d.cts +27 -16
  53. package/dist/adapters/thread/langchain/index.d.ts +27 -16
  54. package/dist/adapters/thread/langchain/index.js +9 -1
  55. package/dist/adapters/thread/langchain/index.js.map +1 -1
  56. package/dist/adapters/thread/langchain/workflow.cjs +33 -0
  57. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -0
  58. package/dist/adapters/thread/langchain/workflow.d.cts +32 -0
  59. package/dist/adapters/thread/langchain/workflow.d.ts +32 -0
  60. package/dist/adapters/thread/langchain/workflow.js +31 -0
  61. package/dist/adapters/thread/langchain/workflow.js.map +1 -0
  62. package/dist/index.cjs +282 -90
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +38 -16
  65. package/dist/index.d.ts +38 -16
  66. package/dist/index.js +281 -87
  67. package/dist/index.js.map +1 -1
  68. package/dist/queries-DModcWRy.d.cts +44 -0
  69. package/dist/queries-byD0jr1Y.d.ts +44 -0
  70. package/dist/{types-BkAYmc96.d.ts → types-B50pBPEV.d.ts} +190 -38
  71. package/dist/{types-YbL7JpEA.d.cts → types-Bll19FZJ.d.cts} +7 -0
  72. package/dist/{types-YbL7JpEA.d.ts → types-Bll19FZJ.d.ts} +7 -0
  73. package/dist/{queries-6Avfh74U.d.ts → types-BuXdFhaZ.d.cts} +7 -48
  74. package/dist/{types-BMRzfELQ.d.cts → types-ChAMwU3q.d.cts} +17 -1
  75. package/dist/{types-BMRzfELQ.d.ts → types-ChAMwU3q.d.ts} +17 -1
  76. package/dist/{types-CES_30qx.d.cts → types-DQW8l7pY.d.cts} +190 -38
  77. package/dist/{queries-CHa2iv_I.d.cts → types-GZ76HZSj.d.ts} +7 -48
  78. package/dist/workflow.cjs +244 -86
  79. package/dist/workflow.cjs.map +1 -1
  80. package/dist/workflow.d.cts +54 -65
  81. package/dist/workflow.d.ts +54 -65
  82. package/dist/workflow.js +243 -83
  83. package/dist/workflow.js.map +1 -1
  84. package/package.json +54 -2
  85. package/src/adapters/sandbox/daytona/filesystem.ts +1 -1
  86. package/src/adapters/sandbox/daytona/index.ts +8 -0
  87. package/src/adapters/sandbox/daytona/proxy.ts +56 -0
  88. package/src/adapters/sandbox/e2b/filesystem.ts +147 -0
  89. package/src/adapters/sandbox/e2b/index.ts +164 -0
  90. package/src/adapters/sandbox/e2b/types.ts +23 -0
  91. package/src/adapters/sandbox/inmemory/index.ts +27 -3
  92. package/src/adapters/sandbox/inmemory/proxy.ts +53 -0
  93. package/src/adapters/sandbox/virtual/filesystem.ts +41 -17
  94. package/src/adapters/sandbox/virtual/provider.ts +9 -1
  95. package/src/adapters/sandbox/virtual/proxy.ts +53 -0
  96. package/src/adapters/sandbox/virtual/types.ts +9 -4
  97. package/src/adapters/thread/google-genai/activities.ts +51 -17
  98. package/src/adapters/thread/google-genai/index.ts +1 -0
  99. package/src/adapters/thread/google-genai/proxy.ts +61 -0
  100. package/src/adapters/thread/langchain/activities.ts +47 -14
  101. package/src/adapters/thread/langchain/index.ts +1 -0
  102. package/src/adapters/thread/langchain/proxy.ts +61 -0
  103. package/src/lib/lifecycle.ts +57 -0
  104. package/src/lib/sandbox/manager.ts +52 -6
  105. package/src/lib/sandbox/sandbox.test.ts +12 -11
  106. package/src/lib/sandbox/types.ts +31 -4
  107. package/src/lib/session/index.ts +4 -5
  108. package/src/lib/session/session-edge-cases.integration.test.ts +491 -66
  109. package/src/lib/session/session.integration.test.ts +92 -80
  110. package/src/lib/session/session.ts +108 -96
  111. package/src/lib/session/types.ts +87 -17
  112. package/src/lib/subagent/define.ts +6 -5
  113. package/src/lib/subagent/handler.ts +148 -16
  114. package/src/lib/subagent/index.ts +4 -0
  115. package/src/lib/subagent/register.ts +10 -3
  116. package/src/lib/subagent/signals.ts +8 -0
  117. package/src/lib/subagent/subagent.integration.test.ts +893 -128
  118. package/src/lib/subagent/tool.ts +2 -2
  119. package/src/lib/subagent/types.ts +84 -21
  120. package/src/lib/subagent/workflow.ts +83 -12
  121. package/src/lib/tool-router/router-edge-cases.integration.test.ts +4 -1
  122. package/src/lib/tool-router/router.integration.test.ts +141 -5
  123. package/src/lib/tool-router/router.ts +13 -3
  124. package/src/lib/tool-router/types.ts +7 -0
  125. package/src/lib/workflow.test.ts +104 -27
  126. package/src/lib/workflow.ts +37 -19
  127. package/src/tools/bash/bash.test.ts +16 -7
  128. package/src/workflow.ts +11 -14
  129. package/tsup.config.ts +6 -0
package/README.md CHANGED
@@ -28,20 +28,41 @@ Temporal solves these problems for workflows. Zeitlich brings these guarantees t
28
28
  - **Type-safe tools** — Define tools with Zod schemas, get full TypeScript inference
29
29
  - **Lifecycle hooks** — Pre/post tool execution, session start/end
30
30
  - **Subagent support** — Spawn child agents as Temporal child workflows
31
+ - **Skills** — First-class [agentskills.io](https://agentskills.io) support with progressive disclosure
31
32
  - **Filesystem utilities** — In-memory or custom providers for file operations
32
- - **Model flexibility** — Framework-agnostic model invocation with adapters for LangChain (and more coming)
33
+ - **Model flexibility** — Framework-agnostic model invocation with adapters for LangChain, Vercel AI SDK, or provider-specific SDKs
33
34
 
34
35
  ## LLM Integration
35
36
 
36
- Zeitlich's core is framework-agnostic — it defines generic interfaces (`ModelInvoker`, `ThreadOps`, `MessageContent`) that work with any LLM SDK. Concrete implementations are provided via adapter packages.
37
+ Zeitlich's core is framework-agnostic — it defines generic interfaces (`ModelInvoker`, `ThreadOps`, `MessageContent`) that work with any LLM SDK. You choose a **thread adapter** (for conversation storage and model invocation) and a **sandbox adapter** (for filesystem operations), then wire them together.
37
38
 
38
- ### LangChain Adapter (`zeitlich/adapters/thread/langchain`)
39
+ ### Thread Adapters
39
40
 
40
- The built-in LangChain adapter gives you:
41
+ A thread adapter bundles two concerns:
42
+ 1. **Thread management** — Storing and retrieving conversation messages in Redis
43
+ 2. **Model invocation** — Calling the LLM with the conversation history and tools
41
44
 
42
- - **Provider flexibility** Use Anthropic, OpenAI, Google, Azure, AWS Bedrock, or any LangChain-supported provider
43
- - **Consistent interface** — Same tool calling and message format regardless of provider
44
- - **Easy model swapping** Change models without rewriting agent logic
45
+ Each adapter exposes the same shape: `createActivities(scope)` for Temporal worker registration, and an `invoker` for model calls. Pick the one matching your preferred SDK:
46
+
47
+ | Adapter | Import | SDK |
48
+ |---------|--------|-----|
49
+ | LangChain | `zeitlich/adapters/thread/langchain` | `@langchain/core` + any provider package |
50
+ | Google GenAI | `zeitlich/adapters/thread/google-genai` | `@google/genai` |
51
+
52
+ Vercel AI SDK and other provider-specific adapters can be built by implementing the `ThreadOps` and `ModelInvoker` interfaces.
53
+
54
+ ### Sandbox Adapters
55
+
56
+ A sandbox adapter provides filesystem access for tools like `Bash`, `Read`, `Write`, and `Edit`:
57
+
58
+ | Adapter | Import | Use case |
59
+ |---------|--------|----------|
60
+ | In-memory | `zeitlich/adapters/sandbox/inmemory` | Tests and lightweight agents |
61
+ | Virtual | `zeitlich/adapters/sandbox/virtual` | Custom resolvers with path-only ops |
62
+ | Daytona | `zeitlich/adapters/sandbox/daytona` | Remote Daytona workspaces |
63
+ | E2B | `zeitlich/adapters/sandbox/e2b` | E2B cloud sandboxes |
64
+
65
+ ### Example: LangChain Adapter
45
66
 
46
67
  ```typescript
47
68
  import { ChatAnthropic } from "@langchain/anthropic";
@@ -55,19 +76,13 @@ const adapter = createLangChainAdapter({
55
76
 
56
77
  export function createActivities(client: WorkflowClient) {
57
78
  return {
58
- ...adapter.threadOps,
79
+ ...adapter.createActivities("myAgentWorkflow"),
59
80
  runAgent: createRunAgentActivity(client, adapter.invoker),
60
81
  };
61
82
  }
62
83
  ```
63
84
 
64
- Install the LangChain package for your chosen provider:
65
-
66
- ```bash
67
- npm install @langchain/core @langchain/anthropic # Anthropic
68
- npm install @langchain/core @langchain/openai # OpenAI
69
- npm install @langchain/core @langchain/google-genai # Google
70
- ```
85
+ All adapters follow the same pattern — `createActivities(scope)` for worker registration and `invoker` for model calls.
71
86
 
72
87
  ## Installation
73
88
 
@@ -78,7 +93,8 @@ npm install zeitlich ioredis
78
93
  **Peer dependencies:**
79
94
 
80
95
  - `ioredis` >= 5.0.0
81
- - `@langchain/core` >= 1.0.0 (optional — only needed when using `zeitlich/adapters/thread/langchain`)
96
+ - `@langchain/core` >= 1.0.0 (optional — only when using the LangChain adapter)
97
+ - `@google/genai` >= 1.0.0 (optional — only when using the Google GenAI adapter)
82
98
 
83
99
  **Required infrastructure:**
84
100
 
@@ -87,40 +103,40 @@ npm install zeitlich ioredis
87
103
 
88
104
  ## Import Paths
89
105
 
90
- Zeitlich provides three entry points:
106
+ Zeitlich uses separate entry points for workflow-side and activity-side code:
91
107
 
92
108
  ```typescript
93
- // In workflow files — no external dependencies (Redis, LangChain, etc.)
109
+ // In workflow files — no external dependencies (Redis, LLM SDKs, etc.)
94
110
  import {
95
111
  createSession,
96
112
  createAgentStateManager,
97
- askUserQuestionTool,
98
- bashTool,
99
113
  defineTool,
100
- defineSubagentWorkflow,
101
- defineSubagent,
102
- type ModelInvoker,
114
+ bashTool,
103
115
  } from "zeitlich/workflow";
104
116
 
117
+ // Adapter workflow proxies (auto-scoped to current workflow)
118
+ import { proxyLangChainThreadOps } from "zeitlich/adapters/thread/langchain/workflow";
119
+ import { proxyInMemorySandboxOps } from "zeitlich/adapters/sandbox/inmemory/workflow";
120
+
105
121
  // In activity files and worker setup — framework-agnostic core
106
122
  import {
107
123
  createRunAgentActivity,
108
- withParentWorkflowState,
124
+ SandboxManager,
109
125
  withSandbox,
110
126
  bashHandler,
111
- createAskUserQuestionHandler,
112
- toTree,
113
127
  } from "zeitlich";
114
128
 
115
- // LangChain adapter — unified adapter for LLM invocation and thread management
129
+ // Thread adapter — activity-side
116
130
  import { createLangChainAdapter } from "zeitlich/adapters/thread/langchain";
117
131
  ```
118
132
 
119
- **Why three entry points?**
133
+ **Entry points:**
120
134
 
121
135
  - `zeitlich/workflow` — Pure TypeScript, safe for Temporal's V8 sandbox
136
+ - `zeitlich/adapters/*/workflow` — Workflow-side proxies that auto-scope activities to the current workflow
122
137
  - `zeitlich` — Activity-side utilities (Redis, filesystem), framework-agnostic
123
- - `zeitlich/adapters/thread/langchain`LangChain-specific adapter (model invocation + thread management)
138
+ - `zeitlich/adapters/thread/*`Activity-side adapters (thread management + model invocation)
139
+ - `zeitlich/adapters/sandbox/*` — Activity-side sandbox providers
124
140
 
125
141
  ## Examples
126
142
 
@@ -145,13 +161,14 @@ export const searchTool: ToolDefinition<"Search", typeof searchSchema> = {
145
161
 
146
162
  ### 2. Create the Workflow
147
163
 
148
- The system prompt is set via `createAgentStateManager`'s `initialState`, and agent config fields (`agentName`, `maxTurns`, etc.) are spread into `createSession`.
164
+ The workflow wires together a **thread adapter** (for conversation storage / model calls) and a **sandbox adapter** (for filesystem tools). Both are pluggable swap the proxy import to switch providers.
149
165
 
150
166
  ```typescript
151
167
  import { proxyActivities, workflowInfo } from "@temporalio/workflow";
152
168
  import {
153
169
  createAgentStateManager,
154
170
  createSession,
171
+ defineWorkflow,
155
172
  askUserQuestionTool,
156
173
  bashTool,
157
174
  defineTool,
@@ -159,6 +176,9 @@ import {
159
176
  import { searchTool } from "./tools";
160
177
  import type { MyActivities } from "./activities";
161
178
 
179
+ import { proxyLangChainThreadOps } from "zeitlich/adapters/thread/langchain/workflow";
180
+ import { proxyInMemorySandboxOps } from "zeitlich/adapters/sandbox/inmemory/workflow";
181
+
162
182
  const {
163
183
  runAgentActivity,
164
184
  searchHandlerActivity,
@@ -175,64 +195,76 @@ const {
175
195
  heartbeatTimeout: "5m",
176
196
  });
177
197
 
178
- export async function myAgentWorkflow({ prompt }: { prompt: string }) {
179
- const { runId } = workflowInfo();
198
+ export const myAgentWorkflow = defineWorkflow(
199
+ { name: "myAgentWorkflow" },
200
+ async ({ prompt }: { prompt: string }, sessionInput) => {
201
+ const { runId } = workflowInfo();
180
202
 
181
- const stateManager = createAgentStateManager({
182
- initialState: {
183
- systemPrompt: "You are a helpful assistant.",
184
- },
185
- agentName: "my-agent",
186
- });
203
+ const stateManager = createAgentStateManager({
204
+ initialState: {
205
+ systemPrompt: "You are a helpful assistant.",
206
+ },
207
+ agentName: "my-agent",
208
+ });
187
209
 
188
- const session = await createSession({
189
- agentName: "my-agent",
190
- maxTurns: 20,
191
- threadId: runId,
192
- runAgent: runAgentActivity,
193
- buildContextMessage: () => [{ type: "text", text: prompt }],
194
- tools: {
195
- Search: defineTool({
196
- ...searchTool,
197
- handler: searchHandlerActivity,
198
- }),
199
- AskUserQuestion: defineTool({
200
- ...askUserQuestionTool,
201
- handler: askUserQuestionHandlerActivity,
202
- hooks: {
203
- onPostToolUse: () => {
204
- stateManager.waitForInput();
210
+ const session = await createSession({
211
+ agentName: "my-agent",
212
+ maxTurns: 20,
213
+ thread: { mode: "new", threadId: runId },
214
+ threadOps: proxyLangChainThreadOps(),
215
+ sandboxOps: proxyInMemorySandboxOps(),
216
+ runAgent: runAgentActivity,
217
+ buildContextMessage: () => [{ type: "text", text: prompt }],
218
+ tools: {
219
+ Search: defineTool({
220
+ ...searchTool,
221
+ handler: searchHandlerActivity,
222
+ }),
223
+ AskUserQuestion: defineTool({
224
+ ...askUserQuestionTool,
225
+ handler: askUserQuestionHandlerActivity,
226
+ hooks: {
227
+ onPostToolUse: () => {
228
+ stateManager.waitForInput();
229
+ },
205
230
  },
206
- },
207
- }),
208
- Bash: defineTool({
209
- ...bashTool,
210
- handler: bashHandlerActivity,
211
- }),
212
- },
213
- });
231
+ }),
232
+ Bash: defineTool({
233
+ ...bashTool,
234
+ handler: bashHandlerActivity,
235
+ }),
236
+ },
237
+ ...sessionInput,
238
+ });
214
239
 
215
- const result = await session.runSession({ stateManager });
216
- return result;
217
- }
240
+ const result = await session.runSession({ stateManager });
241
+ return result;
242
+ }
243
+ );
218
244
  ```
219
245
 
220
246
  ### 3. Create Activities
221
247
 
222
- Activities are factory functions that receive infrastructure dependencies (`redis`, `client`). Each returns an object of activity functions registered with the Temporal worker.
248
+ Activities are factory functions that receive infrastructure dependencies (`redis`, `client`). The thread adapter and sandbox provider are configured here swap imports to change LLM or sandbox backend.
223
249
 
224
250
  ```typescript
225
251
  import type Redis from "ioredis";
226
252
  import type { WorkflowClient } from "@temporalio/client";
227
253
  import { ChatAnthropic } from "@langchain/anthropic";
228
254
  import {
255
+ SandboxManager,
229
256
  withSandbox,
230
257
  bashHandler,
231
258
  createAskUserQuestionHandler,
232
259
  createRunAgentActivity,
233
260
  } from "zeitlich";
261
+ import { InMemorySandboxProvider } from "zeitlich/adapters/sandbox/inmemory";
262
+
234
263
  import { createLangChainAdapter } from "zeitlich/adapters/thread/langchain";
235
264
 
265
+ const sandboxProvider = new InMemorySandboxProvider();
266
+ const sandboxManager = new SandboxManager(sandboxProvider);
267
+
236
268
  export const createActivities = ({
237
269
  redis,
238
270
  client,
@@ -240,7 +272,7 @@ export const createActivities = ({
240
272
  redis: Redis;
241
273
  client: WorkflowClient;
242
274
  }) => {
243
- const { threadOps, invoker } = createLangChainAdapter({
275
+ const adapter = createLangChainAdapter({
244
276
  redis,
245
277
  model: new ChatAnthropic({
246
278
  model: "claude-sonnet-4-20250514",
@@ -249,8 +281,9 @@ export const createActivities = ({
249
281
  });
250
282
 
251
283
  return {
252
- ...threadOps,
253
- runAgentActivity: createRunAgentActivity(client, invoker),
284
+ ...adapter.createActivities("myAgentWorkflow"),
285
+ ...sandboxManager.createActivities("myAgentWorkflow"),
286
+ runAgentActivity: createRunAgentActivity(client, adapter.invoker),
254
287
  searchHandlerActivity: async (args: { query: string }) => ({
255
288
  toolResponse: JSON.stringify(await performSearch(args.query)),
256
289
  data: null,
@@ -382,6 +415,7 @@ import {
382
415
  createSession,
383
416
  defineSubagentWorkflow,
384
417
  } from "zeitlich/workflow";
418
+ import { proxyLangChainThreadOps } from "zeitlich/adapters/thread/langchain/workflow";
385
419
  import type { createResearcherActivities } from "./activities";
386
420
 
387
421
  const { runResearcherActivity } = proxyActivities<
@@ -400,7 +434,8 @@ export const researcherWorkflow = defineSubagentWorkflow(
400
434
  });
401
435
 
402
436
  const session = await createSession({
403
- ...sessionInput, // spreads agentName, threadId, continueThread, sandboxId
437
+ ...sessionInput, // spreads agentName, thread, sandbox, sandboxShutdown
438
+ threadOps: proxyLangChainThreadOps(), // auto-scoped to "Researcher"
404
439
  runAgent: runResearcherActivity,
405
440
  buildContextMessage: () => [{ type: "text", text: prompt }],
406
441
  });
@@ -427,7 +462,7 @@ export const researcherSubagent = defineSubagent(researcherWorkflow);
427
462
 
428
463
  // Optionally override parent-specific config
429
464
  export const researcherSubagent = defineSubagent(researcherWorkflow, {
430
- allowThreadContinuation: true,
465
+ thread: "fork",
431
466
  sandbox: "own",
432
467
  hooks: {
433
468
  onPostExecution: ({ result }) => console.log("researcher done", result),
@@ -442,38 +477,186 @@ const session = await createSession({
442
477
 
443
478
  The `Subagent` tool is automatically added when subagents are configured, allowing the LLM to spawn child workflows.
444
479
 
445
- ### Thread Continuation
480
+ ### Skills
481
+
482
+ Zeitlich has first-class support for the [agentskills.io](https://agentskills.io) specification. Skills are reusable instruction sets that an agent can load on-demand via the built-in `ReadSkill` tool — progressive disclosure keeps token usage low while giving agents access to rich, domain-specific guidance.
483
+
484
+ #### Defining a Skill
485
+
486
+ Each skill lives in its own directory as a `SKILL.md` file with YAML frontmatter:
487
+
488
+ ```
489
+ skills/
490
+ ├── code-review/
491
+ │ └── SKILL.md
492
+ ├── pdf-processing/
493
+ │ └── SKILL.md
494
+ ```
495
+
496
+ ```markdown
497
+ ---
498
+ name: code-review
499
+ description: Review pull requests for correctness, style, and security issues
500
+ allowed-tools: Bash Grep Read
501
+ license: MIT
502
+ ---
503
+
504
+ ## Instructions
505
+
506
+ When reviewing code, follow these steps:
507
+ 1. Read the diff with `Bash`
508
+ 2. Search for related tests with `Grep`
509
+ 3. ...
510
+ ```
511
+
512
+ Required fields: `name` and `description`. Optional: `license`, `compatibility`, `allowed-tools` (space-delimited), `metadata` (key-value map).
513
+
514
+ #### Loading Skills
515
+
516
+ Use `FileSystemSkillProvider` to load skills from a directory (works with any sandbox filesystem):
517
+
518
+ ```typescript
519
+ import { FileSystemSkillProvider } from "zeitlich";
520
+ import { InMemorySandboxProvider } from "zeitlich/adapters/sandbox/inmemory";
521
+
522
+ const provider = new InMemorySandboxProvider();
523
+ const { sandbox } = await provider.create({});
524
+
525
+ const skillProvider = new FileSystemSkillProvider(sandbox.fs, "/skills");
526
+ const skills = await skillProvider.loadAll();
527
+ ```
446
528
 
447
- By default, each session initializes a fresh thread. To continue from an existing thread (e.g., resuming a conversation after a workflow completes), pass `continueThread: true` along with the previous `threadId`:
529
+ Or parse a single file directly:
530
+
531
+ ```typescript
532
+ import { parseSkillFile } from "zeitlich/workflow";
533
+
534
+ const { frontmatter, body } = parseSkillFile(rawMarkdown);
535
+ // frontmatter: SkillMetadata, body: instruction text
536
+ ```
537
+
538
+ #### Passing Skills to a Session
539
+
540
+ Pass loaded skills to `createSession`. Zeitlich automatically registers a `ReadSkill` tool whose description lists all available skills — the agent discovers them through the tool definition and loads instructions on demand:
448
541
 
449
542
  ```typescript
450
543
  import { createSession } from "zeitlich/workflow";
451
544
 
452
- // First run — threadId defaults to getShortId() if omitted
453
545
  const session = await createSession({
454
- // threadId is optional, auto-generated if not provided
455
546
  // ... other config
547
+ skills, // Skill[] — loaded via FileSystemSkillProvider or manually
456
548
  });
549
+ ```
550
+
551
+ The `ReadSkill` tool accepts a `skill_name` parameter (constrained to an enum of available names) and returns the full instruction body. The handler runs directly in the workflow — no activity needed.
457
552
 
458
- // Later new workflow forks the previous thread
553
+ #### Building Skills Manually
554
+
555
+ For advanced use cases, you can construct the tool and handler independently:
556
+
557
+ ```typescript
558
+ import { createReadSkillTool, createReadSkillHandler } from "zeitlich/workflow";
559
+
560
+ const tool = createReadSkillTool(skills); // ToolDefinition with enum schema
561
+ const handler = createReadSkillHandler(skills); // Returns skill instructions
562
+ ```
563
+
564
+ ### Thread & Sandbox Lifecycle
565
+
566
+ Every session has a **thread** (conversation history) and an optional **sandbox** (filesystem environment). Both are configured with explicit lifecycle types that control how they are initialized and torn down.
567
+
568
+ #### Thread Initialization (`ThreadInit`)
569
+
570
+ The `thread` field on `SessionConfig` (and `WorkflowInput`) accepts one of three modes:
571
+
572
+ | Mode | Description |
573
+ |------|-------------|
574
+ | `{ mode: "new" }` | Start a fresh thread (default). Optionally pass `threadId` to choose the ID. |
575
+ | `{ mode: "fork", threadId }` | Copy all messages from an existing thread into a new one and continue there. The original is never mutated. |
576
+ | `{ mode: "continue", threadId }` | Append directly to an existing thread in-place. |
577
+
578
+ ```typescript
579
+ import { createSession } from "zeitlich/workflow";
580
+
581
+ // First run — fresh thread
582
+ const session = await createSession({
583
+ thread: { mode: "new" },
584
+ // ... other config
585
+ });
586
+
587
+ // Later — fork the previous conversation
459
588
  const resumedSession = await createSession({
460
- threadId: savedThreadId, // the thread to continue from
461
- continueThread: true, // fork into a new thread with the old messages
589
+ thread: { mode: "fork", threadId: savedThreadId },
462
590
  // ... other config
463
591
  });
464
- ```
465
592
 
466
- When `continueThread` is true the session **forks** the provided thread — it copies all messages into a new thread and operates on the copy. The original thread is never mutated, so multiple sessions can safely continue from the same thread in parallel.
593
+ // Or append directly to the existing thread
594
+ const continuedSession = await createSession({
595
+ thread: { mode: "continue", threadId: savedThreadId },
596
+ // ... other config
597
+ });
598
+ ```
467
599
 
468
600
  `getShortId()` produces compact, workflow-deterministic IDs (~12 base-62 chars) that are more token-efficient than UUIDs.
469
601
 
470
- #### Subagent Thread Continuation
602
+ #### Sandbox Initialization (`SandboxInit`)
603
+
604
+ The `sandbox` field controls how a sandbox is created or reused:
605
+
606
+ | Mode | Description |
607
+ |------|-------------|
608
+ | `{ mode: "new" }` | Create a fresh sandbox (default when `sandboxOps` is provided). |
609
+ | `{ mode: "continue", sandboxId }` | Resume a previously-paused sandbox. This session takes ownership. |
610
+ | `{ mode: "fork", sandboxId }` | Fork from an existing sandbox. A new sandbox is created and owned by this session. |
611
+ | `{ mode: "inherit", sandboxId }` | Use a sandbox owned by someone else (e.g. a parent agent). Shutdown policy is ignored. |
612
+
613
+ #### Sandbox Shutdown (`SandboxShutdown`)
614
+
615
+ The `sandboxShutdown` field controls what happens to the sandbox when the session exits:
471
616
 
472
- Subagents can opt in to thread continuation via `allowThreadContinuation`. When enabled, the parent agent can pass a `threadId` to resume a previous subagent conversation:
617
+ | Value | Description |
618
+ |-------|-------------|
619
+ | `"destroy"` | Tear down the sandbox entirely (default). |
620
+ | `"pause"` | Pause the sandbox so it can be resumed later. |
621
+ | `"keep"` | Leave the sandbox running (no-op on exit). |
622
+
623
+ Subagents also support `"pause-until-parent-close"` — pause on exit, then wait for the parent workflow to signal when to destroy it.
624
+
625
+ #### Subagent Thread & Sandbox Config
626
+
627
+ Subagents configure thread and sandbox strategies via `defineSubagent`:
473
628
 
474
629
  ```typescript
475
- import { defineSubagentWorkflow, defineSubagent } from "zeitlich/workflow";
630
+ import { defineSubagent } from "zeitlich/workflow";
631
+ import { researcherWorkflow } from "./researcher.workflow";
632
+
633
+ // Fresh thread each time, no sandbox (defaults)
634
+ export const researcherSubagent = defineSubagent(researcherWorkflow);
476
635
 
636
+ // Allow the parent to continue a previous conversation via fork
637
+ export const researcherSubagent = defineSubagent(researcherWorkflow, {
638
+ thread: "fork",
639
+ });
640
+
641
+ // Own sandbox with pause-on-exit
642
+ export const researcherSubagent = defineSubagent(researcherWorkflow, {
643
+ thread: "fork",
644
+ sandbox: { source: "own", shutdown: "pause" },
645
+ });
646
+
647
+ // Inherit the parent's sandbox
648
+ export const researcherSubagent = defineSubagent(researcherWorkflow, {
649
+ sandbox: "inherit",
650
+ });
651
+ ```
652
+
653
+ The `thread` field accepts `"new"` (default), `"fork"`, or `"continue"`. When set to `"fork"` or `"continue"`, the parent agent can pass a `threadId` in a subsequent `Task` tool call to resume the conversation. The subagent returns its `threadId` in the response (surfaced as `[Thread ID: ...]`), which the parent can use for continuation.
654
+
655
+ The `sandbox` field accepts `"none"` (default), `"inherit"`, `"own"`, or `{ source: "own", shutdown }` for explicit shutdown policy.
656
+
657
+ The subagent workflow receives lifecycle fields via `sessionInput`:
658
+
659
+ ```typescript
477
660
  export const researcherWorkflow = defineSubagentWorkflow(
478
661
  {
479
662
  name: "Researcher",
@@ -481,27 +664,17 @@ export const researcherWorkflow = defineSubagentWorkflow(
481
664
  },
482
665
  async (prompt, sessionInput) => {
483
666
  const session = await createSession({
484
- ...sessionInput, // threadId/continueThread are provided by parent when resuming
667
+ ...sessionInput, // spreads agentName, thread, sandbox, sandboxShutdown
668
+ threadOps: proxyLangChainThreadOps(),
485
669
  // ... other config
486
670
  });
487
671
 
488
672
  const { threadId, finalMessage } = await session.runSession({ stateManager });
489
- return {
490
- toolResponse: finalMessage ? extractText(finalMessage) : "No response",
491
- data: null,
492
- threadId,
493
- };
673
+ return { toolResponse: extractText(finalMessage), data: null, threadId };
494
674
  },
495
675
  );
496
-
497
- // Enable thread continuation in the parent registration
498
- export const researcherSubagent = defineSubagent(researcherWorkflow, {
499
- allowThreadContinuation: true,
500
- });
501
676
  ```
502
677
 
503
- The subagent returns its `threadId` in the response, which the handler surfaces to the parent LLM as `[Thread ID: ...]`. The parent can then pass that ID back in a subsequent `Subagent` tool call to continue the conversation.
504
-
505
678
  ### Filesystem Utilities
506
679
 
507
680
  Built-in support for file operations with in-memory or custom filesystem providers (e.g. from [`just-bash`](https://github.com/nicholasgasior/just-bash)).
@@ -547,7 +720,8 @@ import {
547
720
  const sandboxManager = new SandboxManager(provider);
548
721
 
549
722
  export const createActivities = ({ redis, client }) => ({
550
- ...sandboxManager.createActivities(),
723
+ // scope auto-prepends the provider id (e.g. "inMemory", "virtual")
724
+ ...sandboxManager.createActivities("MyAgentWorkflow"),
551
725
  globHandlerActivity: withSandbox(sandboxManager, globHandler),
552
726
  editHandlerActivity: withSandbox(sandboxManager, editHandler),
553
727
  bashHandlerActivity: withSandbox(sandboxManager, bashHandler),
@@ -617,6 +791,7 @@ Zeitlich provides ready-to-use tool definitions and handlers for common agent op
617
791
  | `Grep` | Search file contents with regex patterns |
618
792
  | `Bash` | Execute shell commands |
619
793
  | `AskUserQuestion` | Ask the user questions during execution with structured options |
794
+ | `ReadSkill` | Load skill instructions on demand (see [Skills](#skills)) |
620
795
  | `Task` | Launch subagents as child workflows (see [Subagents](#subagents)) |
621
796
 
622
797
  ```typescript
@@ -676,7 +851,10 @@ Safe for use in Temporal workflow files:
676
851
  | `getShortId` | Generate a compact, workflow-deterministic identifier (base-62, 12 chars) |
677
852
  | Tool definitions | `askUserQuestionTool`, `globTool`, `grepTool`, `readFileTool`, `writeFileTool`, `editTool`, `bashTool` |
678
853
  | Task tools | `taskCreateTool`, `taskGetTool`, `taskListTool`, `taskUpdateTool` for workflow task management |
679
- | Types | `SubagentDefinition`, `SubagentConfig`, `ToolDefinition`, `ToolWithHandler`, `RouterContext`, `SessionConfig`, etc. |
854
+ | Skill utilities | `parseSkillFile`, `createReadSkillTool`, `createReadSkillHandler` |
855
+ | `defineWorkflow` | Wraps a main workflow function, translating `WorkflowInput` into session-compatible fields |
856
+ | Lifecycle types | `ThreadInit`, `SandboxInit`, `SandboxShutdown`, `SubagentSandboxShutdown`, `SubagentSandboxConfig` |
857
+ | Types | `Skill`, `SkillMetadata`, `SkillProvider`, `SubagentDefinition`, `SubagentConfig`, `ToolDefinition`, `ToolWithHandler`, `RouterContext`, `SessionConfig`, `WorkflowConfig`, `WorkflowInput`, etc. |
680
858
 
681
859
  ### Activity Entry Point (`zeitlich`)
682
860
 
@@ -689,19 +867,29 @@ Framework-agnostic utilities for activities, worker setup, and Node.js code:
689
867
  | `createThreadManager` | Generic Redis-backed thread manager factory |
690
868
  | `toTree` | Generate file tree string from an `IFileSystem` instance |
691
869
  | `withSandbox` | Wraps a handler to auto-resolve sandbox from context (pairs with `withAutoAppend`) |
870
+ | `FileSystemSkillProvider` | Load skills from a directory following the agentskills.io layout |
692
871
  | Tool handlers | `bashHandler`, `editHandler`, `globHandler`, `readFileHandler`, `writeFileHandler`, `createAskUserQuestionHandler` |
693
872
 
694
- ### LangChain Adapter Entry Point (`zeitlich/adapters/thread/langchain`)
873
+ ### Thread Adapter Entry Points
695
874
 
696
- LangChain-specific implementations:
875
+ **LangChain** (`zeitlich/adapters/thread/langchain`):
697
876
 
698
877
  | Export | Description |
699
878
  | ----------------------------------- | ---------------------------------------------------------------------- |
700
- | `createLangChainAdapter` | Unified adapter returning `threadOps`, `invoker`, `createModelInvoker` |
879
+ | `createLangChainAdapter` | Unified adapter returning `createActivities`, `invoker`, `createModelInvoker` |
701
880
  | `createLangChainModelInvoker` | Factory that returns a `ModelInvoker` backed by a LangChain chat model |
702
881
  | `invokeLangChainModel` | One-shot model invocation convenience function |
703
882
  | `createLangChainThreadManager` | Thread manager with LangChain `StoredMessage` helpers |
704
883
 
884
+ **Google GenAI** (`zeitlich/adapters/thread/google-genai`):
885
+
886
+ | Export | Description |
887
+ | ----------------------------------- | ---------------------------------------------------------------------- |
888
+ | `createGoogleGenAIAdapter` | Unified adapter returning `createActivities`, `invoker`, `createModelInvoker` |
889
+ | `createGoogleGenAIModelInvoker` | Factory that returns a `ModelInvoker` backed by the `@google/genai` SDK |
890
+ | `invokeGoogleGenAIModel` | One-shot model invocation convenience function |
891
+ | `createGoogleGenAIThreadManager` | Thread manager with Google GenAI `Content` helpers |
892
+
705
893
  ### Types
706
894
 
707
895
  | Export | Description |
@@ -716,6 +904,11 @@ LangChain-specific implementations:
716
904
  | `RouterContext` | Base context every tool handler receives (`threadId`, `toolCallId`, `toolName`, `sandboxId?`) |
717
905
  | `Hooks` | Combined session lifecycle + tool execution hooks |
718
906
  | `ToolRouterHooks` | Narrowed hook interface for tool execution only (pre/post/failure) |
907
+ | `ThreadInit` | Thread initialization strategy: `"new"`, `"continue"`, or `"fork"` |
908
+ | `SandboxInit` | Sandbox initialization strategy: `"new"`, `"continue"`, `"fork"`, or `"inherit"` |
909
+ | `SandboxShutdown` | Sandbox exit policy: `"destroy" \| "pause" \| "keep"` |
910
+ | `SubagentSandboxShutdown` | Extended shutdown with `"pause-until-parent-close"` |
911
+ | `SubagentSandboxConfig` | Subagent sandbox strategy: `"none" \| "inherit" \| "own" \| { source, shutdown }` |
719
912
  | `SubagentDefinition` | Callable subagent workflow with embedded metadata (from `defineSubagentWorkflow`) |
720
913
  | `SubagentConfig` | Resolved subagent configuration consumed by `createSession` |
721
914
  | `AgentState` | Generic agent state type |
@@ -743,9 +936,14 @@ LangChain-specific implementations:
743
936
  │ └──────────────────────────────────────────────────────────┘ │
744
937
  │ │ │
745
938
  │ ┌──────────────────────────────────────────────────────────┐ │
746
- │ │ LLM Adapter (zeitlich/adapters/thread/langchain) │ │
747
- │ │ • createLangChainAdapter (thread ops + model invoker) │ │
748
- │ │ • createLangChainThreadManager (message helpers) │ │
939
+ │ │ Thread Adapter (zeitlich/adapters/thread/*) │ │
940
+ │ │ • LangChain, Google GenAI, or custom │ │
941
+ │ │ • Thread ops (message storage) + model invoker │ │
942
+ │ └──────────────────────────────────────────────────────────┘ │
943
+ │ ┌──────────────────────────────────────────────────────────┐ │
944
+ │ │ Sandbox Adapter (zeitlich/adapters/sandbox/*) │ │
945
+ │ │ • In-memory, Virtual, Daytona, E2B, or custom │ │
946
+ │ │ • Filesystem ops for agent tools │ │
749
947
  │ └──────────────────────────────────────────────────────────┘ │
750
948
  └─────────────────────────────────────────────────────────────────┘
751
949
 
@@ -47,7 +47,7 @@ var DaytonaSandboxFileSystem = class {
47
47
  await this.sandbox.fs.uploadFiles(
48
48
  files.map((f) => ({
49
49
  source: Buffer.from(f.content),
50
- destination: f.path
50
+ destination: this.normalisePath(f.path)
51
51
  }))
52
52
  );
53
53
  }
@@ -231,6 +231,12 @@ var DaytonaSandboxProvider = class {
231
231
  } catch {
232
232
  }
233
233
  }
234
+ async pause(_sandboxId, _ttlSeconds) {
235
+ throw new SandboxNotSupportedError("pause");
236
+ }
237
+ async fork(_sandboxId) {
238
+ throw new Error("Not implemented");
239
+ }
234
240
  async snapshot(_sandboxId) {
235
241
  throw new SandboxNotSupportedError(
236
242
  "snapshot (use Daytona's native snapshot API directly)"