zeitlich 0.2.30 → 0.2.32

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 (84) hide show
  1. package/dist/{activities-BeveyY9b.d.cts → activities-DA-bQM12.d.cts} +2 -2
  2. package/dist/{activities-NT3rcw66.d.ts → activities-FIXVz7DT.d.ts} +2 -2
  3. package/dist/adapters/thread/anthropic/index.cjs +4 -48
  4. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  5. package/dist/adapters/thread/anthropic/index.d.cts +6 -6
  6. package/dist/adapters/thread/anthropic/index.d.ts +6 -6
  7. package/dist/adapters/thread/anthropic/index.js +4 -48
  8. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  9. package/dist/adapters/thread/anthropic/workflow.cjs +1 -0
  10. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  11. package/dist/adapters/thread/anthropic/workflow.d.cts +4 -4
  12. package/dist/adapters/thread/anthropic/workflow.d.ts +4 -4
  13. package/dist/adapters/thread/anthropic/workflow.js +1 -0
  14. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  15. package/dist/adapters/thread/google-genai/index.cjs +8 -48
  16. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  17. package/dist/adapters/thread/google-genai/index.d.cts +6 -6
  18. package/dist/adapters/thread/google-genai/index.d.ts +6 -6
  19. package/dist/adapters/thread/google-genai/index.js +8 -48
  20. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  21. package/dist/adapters/thread/google-genai/workflow.cjs +1 -0
  22. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  23. package/dist/adapters/thread/google-genai/workflow.d.cts +4 -4
  24. package/dist/adapters/thread/google-genai/workflow.d.ts +4 -4
  25. package/dist/adapters/thread/google-genai/workflow.js +1 -0
  26. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  27. package/dist/adapters/thread/langchain/index.cjs +5 -1
  28. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  29. package/dist/adapters/thread/langchain/index.d.cts +6 -5
  30. package/dist/adapters/thread/langchain/index.d.ts +6 -5
  31. package/dist/adapters/thread/langchain/index.js +5 -1
  32. package/dist/adapters/thread/langchain/index.js.map +1 -1
  33. package/dist/adapters/thread/langchain/workflow.cjs +1 -0
  34. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  35. package/dist/adapters/thread/langchain/workflow.d.cts +4 -4
  36. package/dist/adapters/thread/langchain/workflow.d.ts +4 -4
  37. package/dist/adapters/thread/langchain/workflow.js +1 -0
  38. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  39. package/dist/index.cjs +34 -7
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +6 -6
  42. package/dist/index.d.ts +6 -6
  43. package/dist/index.js +34 -7
  44. package/dist/index.js.map +1 -1
  45. package/dist/{proxy-BgswT47M.d.ts → proxy-Br4unLTC.d.ts} +1 -1
  46. package/dist/{proxy-OJihshQF.d.cts → proxy-CTCYWjkr.d.cts} +1 -1
  47. package/dist/{thread-manager-lfN0V-gH.d.cts → thread-manager-CUubPYPH.d.cts} +1 -1
  48. package/dist/{thread-manager-DH0zv05W.d.cts → thread-manager-Cv_BR28i.d.cts} +1 -1
  49. package/dist/{thread-manager-iUplxEZt.d.ts → thread-manager-DKWxHUzD.d.ts} +1 -1
  50. package/dist/{thread-manager-BS477gj8.d.ts → thread-manager-YJLoc1vH.d.ts} +1 -1
  51. package/dist/{types-DVdT5ybA.d.cts → types-Bpq5fDI5.d.cts} +13 -5
  52. package/dist/{types-CCIc7Eam.d.ts → types-BxiT8w9d.d.ts} +1 -1
  53. package/dist/{types-D90Q5aOh.d.ts → types-CheCTLeV.d.ts} +13 -5
  54. package/dist/{types-DgIVPOa1.d.cts → types-NJDyMyUx.d.cts} +1 -1
  55. package/dist/{workflow-Cj4DxGdM.d.cts → workflow-BWKQcz9d.d.cts} +1 -1
  56. package/dist/{workflow-CzrBdCcJ.d.ts → workflow-D8wK7TJY.d.ts} +1 -1
  57. package/dist/workflow.cjs +17 -2
  58. package/dist/workflow.cjs.map +1 -1
  59. package/dist/workflow.d.cts +2 -2
  60. package/dist/workflow.d.ts +2 -2
  61. package/dist/workflow.js +17 -2
  62. package/dist/workflow.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/adapters/thread/anthropic/activities.ts +10 -0
  65. package/src/adapters/thread/anthropic/model-invoker.ts +2 -5
  66. package/src/adapters/thread/google-genai/activities.ts +14 -0
  67. package/src/adapters/thread/google-genai/model-invoker.ts +2 -5
  68. package/src/adapters/thread/langchain/activities.ts +11 -0
  69. package/src/adapters/thread/langchain/model-invoker.ts +2 -3
  70. package/src/lib/.env +1 -0
  71. package/src/lib/model/types.ts +3 -2
  72. package/src/lib/session/session-edge-cases.integration.test.ts +6 -0
  73. package/src/lib/session/session.integration.test.ts +3 -0
  74. package/src/lib/session/session.ts +19 -1
  75. package/src/lib/session/types.ts +7 -0
  76. package/src/lib/thread/proxy.ts +1 -0
  77. package/src/lib/types.ts +3 -0
  78. package/src/lib/virtual-fs/filesystem.ts +15 -0
  79. package/src/lib/virtual-fs/manager.ts +3 -3
  80. package/src/lib/virtual-fs/proxy.ts +3 -3
  81. package/src/lib/virtual-fs/types.ts +3 -2
  82. package/src/lib/virtual-fs/virtual-fs.test.ts +64 -0
  83. package/src/lib/virtual-fs/with-virtual-fs.ts +4 -3
  84. package/src/tools/bash/.env +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -164,6 +164,16 @@ export function createAnthropicAdapter(
164
164
  await thread.appendToolResult(id, toolCallId, toolName, content);
165
165
  },
166
166
 
167
+ async appendAgentMessage(
168
+ threadId: string,
169
+ id: string,
170
+ message: Anthropic.Messages.Message,
171
+ threadKey?: string,
172
+ ): Promise<void> {
173
+ const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey });
174
+ await thread.appendAssistantMessage(id, message.content);
175
+ },
176
+
167
177
  async forkThread(
168
178
  sourceThreadId: string,
169
179
  targetThreadId: string,
@@ -3,7 +3,6 @@ import type Anthropic from "@anthropic-ai/sdk";
3
3
  import type { SerializableToolDefinition } from "../../../lib/types";
4
4
  import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
5
5
  import { createAnthropicThreadManager, type AnthropicThreadManagerHooks } from "./thread-manager";
6
- import { v4 as uuidv4 } from "uuid";
7
6
 
8
7
  export interface AnthropicModelInvokerConfig {
9
8
  redis: Redis;
@@ -29,8 +28,8 @@ function toAnthropicTools(
29
28
  * `ModelInvoker<Anthropic.Messages.Message>` contract.
30
29
  *
31
30
  * Loads the conversation thread from Redis, invokes the Claude model via
32
- * `client.messages.create`, appends the AI response, and returns
33
- * a normalised AgentResponse.
31
+ * `client.messages.create`, and returns a normalised AgentResponse.
32
+ * The caller is responsible for appending the response to the thread.
34
33
  *
35
34
  * @example
36
35
  * ```typescript
@@ -74,8 +73,6 @@ export function createAnthropicModelInvoker({
74
73
  ...(tools ? { tools } : {}),
75
74
  });
76
75
 
77
- await thread.appendAssistantMessage(uuidv4(), response.content);
78
-
79
76
  const toolCalls = response.content.filter(
80
77
  (block): block is Anthropic.Messages.ToolUseBlock =>
81
78
  block.type === "tool_use",
@@ -194,6 +194,20 @@ export function createGoogleGenAIAdapter(
194
194
  );
195
195
  },
196
196
 
197
+ async appendAgentMessage(
198
+ threadId: string,
199
+ id: string,
200
+ message: Content,
201
+ threadKey?: string,
202
+ ): Promise<void> {
203
+ const thread = createGoogleGenAIThreadManager({
204
+ redis,
205
+ threadId,
206
+ key: threadKey,
207
+ });
208
+ await thread.appendModelContent(id, message.parts ?? []);
209
+ },
210
+
197
211
  async forkThread(
198
212
  sourceThreadId: string,
199
213
  targetThreadId: string,
@@ -3,7 +3,6 @@ import type { GoogleGenAI, Content, FunctionDeclaration } from "@google/genai";
3
3
  import type { SerializableToolDefinition } from "../../../lib/types";
4
4
  import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
5
5
  import { createGoogleGenAIThreadManager, type GoogleGenAIThreadManagerHooks } from "./thread-manager";
6
- import { v4 as uuidv4 } from "uuid";
7
6
 
8
7
  export interface GoogleGenAIModelInvokerConfig {
9
8
  redis: Redis;
@@ -27,8 +26,8 @@ function toFunctionDeclarations(
27
26
  * `ModelInvoker<Content>` contract.
28
27
  *
29
28
  * Loads the conversation thread from Redis, invokes the Gemini model via
30
- * `client.models.generateContent`, appends the AI response, and returns
31
- * a normalised AgentResponse.
29
+ * `client.models.generateContent`, and returns a normalised AgentResponse.
30
+ * The caller is responsible for appending the response to the thread.
32
31
  *
33
32
  * @example
34
33
  * ```typescript
@@ -77,8 +76,6 @@ export function createGoogleGenAIModelInvoker({
77
76
  const responseParts = response.candidates?.[0]?.content?.parts ?? [];
78
77
  const modelContent: Content = { role: "model", parts: responseParts };
79
78
 
80
- await thread.appendModelContent(uuidv4(), responseParts);
81
-
82
79
  const functionCalls = response.functionCalls ?? [];
83
80
 
84
81
  return {
@@ -148,6 +148,17 @@ export function createLangChainAdapter(
148
148
  await thread.appendToolResult(id, toolCallId, "", content);
149
149
  },
150
150
 
151
+ async appendAgentMessage(
152
+ threadId: string,
153
+ id: string,
154
+ message: StoredMessage,
155
+ threadKey?: string,
156
+ ): Promise<void> {
157
+ const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
158
+ const patched = { ...message, data: { ...message.data, id } };
159
+ await thread.append([patched]);
160
+ },
161
+
151
162
  async forkThread(
152
163
  sourceThreadId: string,
153
164
  targetThreadId: string,
@@ -17,7 +17,8 @@ export interface LangChainModelInvokerConfig<TModel extends BaseChatModel<any> =
17
17
  * `ModelInvoker<StoredMessage>` contract.
18
18
  *
19
19
  * Loads the conversation thread from Redis, invokes a LangChain chat model,
20
- * appends the AI response, and returns a normalised AgentResponse.
20
+ * and returns a normalised AgentResponse.
21
+ * The caller is responsible for appending the response to the thread.
21
22
  *
22
23
  * @example
23
24
  * ```typescript
@@ -54,8 +55,6 @@ export function createLangChainModelInvoker<TModel extends BaseChatModel<any> =
54
55
  },
55
56
  );
56
57
 
57
- await thread.append([response.toDict()]);
58
-
59
58
  const toolCalls = response.tool_calls ?? [];
60
59
 
61
60
  return {
package/src/lib/.env ADDED
@@ -0,0 +1 @@
1
+ E2B_API_KEY=e2b_39af116424059782e2aee6942fd70237cc2126c9
@@ -33,8 +33,9 @@ export interface ModelInvokerConfig {
33
33
 
34
34
  /**
35
35
  * Generic model invocation contract.
36
- * Implementations load the thread, call the LLM, append the response,
37
- * and return a normalised AgentResponse.
36
+ * Implementations load the thread, call the LLM, and return a normalised
37
+ * AgentResponse. The caller (workflow) is responsible for appending the
38
+ * response to the thread with a deterministic ID.
38
39
  *
39
40
  * Framework adapters (e.g. `zeitlich/langchain`) provide concrete
40
41
  * implementations of this type.
@@ -88,6 +88,9 @@ function createMockThreadOps() {
88
88
  appendSystemMessage: async (threadId, id, content) => {
89
89
  log.push({ op: "appendSystemMessage", args: [threadId, id, content] });
90
90
  },
91
+ appendAgentMessage: async (threadId, id, message) => {
92
+ log.push({ op: "appendAgentMessage", args: [threadId, id, message] });
93
+ },
91
94
  forkThread: async (source, target) => {
92
95
  log.push({ op: "forkThread", args: [source, target] });
93
96
  },
@@ -752,6 +755,9 @@ describe("createSession edge cases", () => {
752
755
  appendSystemMessage: async (threadId, id, content) => {
753
756
  log.push({ op: "appendSystemMessage", args: [threadId, id, content] });
754
757
  },
758
+ appendAgentMessage: async (threadId, id, message) => {
759
+ log.push({ op: "appendAgentMessage", args: [threadId, id, message] });
760
+ },
755
761
  forkThread: async (source, target) => {
756
762
  log.push({ op: "forkThread", args: [source, target] });
757
763
  },
@@ -99,6 +99,9 @@ function createMockThreadOps() {
99
99
  appendSystemMessage: async (threadId, id, content) => {
100
100
  log.push({ op: "appendSystemMessage", args: [threadId, id, content] });
101
101
  },
102
+ appendAgentMessage: async (threadId, id, message) => {
103
+ log.push({ op: "appendAgentMessage", args: [threadId, id, message] });
104
+ },
102
105
  forkThread: async (source, target) => {
103
106
  log.push({ op: "forkThread", args: [source, target] });
104
107
  },
@@ -139,6 +139,7 @@ export async function createSession<
139
139
  appendHumanMessage,
140
140
  initializeThread,
141
141
  appendSystemMessage,
142
+ appendAgentMessage,
142
143
  forkThread,
143
144
  } = threadOps;
144
145
 
@@ -270,8 +271,23 @@ export async function createSession<
270
271
  });
271
272
  }
272
273
  const result = await virtualFsOps.resolveFileTree(virtualFsConfig.ctx);
274
+ const skillFiles = skills ? collectSkillFiles(skills) : undefined;
275
+ const fileTree = skillFiles
276
+ ? [
277
+ ...result.fileTree,
278
+ ...Object.entries(skillFiles).map(([path, content]) => ({
279
+ id: `skill:${path}`,
280
+ path,
281
+ size: content.length,
282
+ mtime: new Date().toISOString(),
283
+ metadata: {},
284
+ })),
285
+ ]
286
+ : result.fileTree;
273
287
  stateManager.mergeUpdate({
274
- fileTree: result.fileTree,
288
+ fileTree,
289
+ virtualFsCtx: virtualFsConfig.ctx,
290
+ ...(skillFiles && { inlineFiles: skillFiles }),
275
291
  } as Partial<AgentState<TState>>);
276
292
  }
277
293
 
@@ -341,6 +357,8 @@ export async function createSession<
341
357
  metadata,
342
358
  });
343
359
 
360
+ await appendAgentMessage(threadId, uuid4(), message, threadKey);
361
+
344
362
  if (usage) {
345
363
  stateManager.updateUsage(usage);
346
364
  }
@@ -39,6 +39,13 @@ export interface ThreadOps<TContent = string> {
39
39
  ): Promise<void>;
40
40
  /** Append a tool result to the thread */
41
41
  appendToolResult(id: string, config: ToolResultConfig): Promise<void>;
42
+ /** Append the model's response to the thread */
43
+ appendAgentMessage(
44
+ threadId: string,
45
+ id: string,
46
+ message: unknown,
47
+ threadKey?: string
48
+ ): Promise<void>;
42
49
  /** Append a system message to the thread */
43
50
  appendSystemMessage(
44
51
  threadId: string,
@@ -51,6 +51,7 @@ export function createThreadOpsProxy(
51
51
  initializeThread: acts[p("initializeThread")],
52
52
  appendHumanMessage: acts[p("appendHumanMessage")],
53
53
  appendToolResult: acts[p("appendToolResult")],
54
+ appendAgentMessage: acts[p("appendAgentMessage")],
54
55
  appendSystemMessage: acts[p("appendSystemMessage")],
55
56
  forkThread: acts[p("forkThread")],
56
57
  } as ActivityInterfaceFor<ThreadOps>;
package/src/lib/types.ts CHANGED
@@ -25,6 +25,9 @@ export interface BaseAgentState {
25
25
  turns: number;
26
26
  tasks: Map<string, WorkflowTask>;
27
27
  fileTree: FileEntry[];
28
+ /** In-memory file contents keyed by path, bypassing the resolver (e.g. skill resources). */
29
+ inlineFiles?: Record<string, string>;
30
+ virtualFsCtx?: unknown;
28
31
  systemPrompt?: string;
29
32
  totalInputTokens: number;
30
33
  totalOutputTokens: number;
@@ -60,17 +60,28 @@ export class VirtualFileSystem<
60
60
  private directories: Set<string>;
61
61
  private mutations: TreeMutation<TMeta>[] = [];
62
62
 
63
+ private inlineFiles: Map<string, string>;
64
+
63
65
  constructor(
64
66
  tree: FileEntry<TMeta>[],
65
67
  private resolver: FileResolver<TCtx, TMeta>,
66
68
  private ctx: TCtx,
67
69
  workspaceBase = "/",
70
+ inlineFiles?: Record<string, string>,
68
71
  ) {
69
72
  this.workspaceBase = normalisePath(workspaceBase);
70
73
  this.entries = new Map(
71
74
  tree.map((e) => [normalisePath(e.path, this.workspaceBase), e])
72
75
  );
73
76
  this.directories = inferDirectories(tree, this.workspaceBase);
77
+ this.inlineFiles = new Map(
78
+ inlineFiles
79
+ ? Object.entries(inlineFiles).map(([p, c]) => [
80
+ normalisePath(p, this.workspaceBase),
81
+ c,
82
+ ])
83
+ : []
84
+ );
74
85
  }
75
86
 
76
87
  /** Return all mutations accumulated during this invocation. */
@@ -89,6 +100,8 @@ export class VirtualFileSystem<
89
100
 
90
101
  async readFile(path: string): Promise<string> {
91
102
  const norm = normalisePath(path, this.workspaceBase);
103
+ const inline = this.inlineFiles.get(norm);
104
+ if (inline !== undefined) return inline;
92
105
  const entry = this.entries.get(norm);
93
106
  if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
94
107
  return this.resolver.readFile(entry.id, this.ctx, entry.metadata);
@@ -96,6 +109,8 @@ export class VirtualFileSystem<
96
109
 
97
110
  async readFileBuffer(path: string): Promise<Uint8Array> {
98
111
  const norm = normalisePath(path, this.workspaceBase);
112
+ const inline = this.inlineFiles.get(norm);
113
+ if (inline !== undefined) return new TextEncoder().encode(inline);
99
114
  const entry = this.entries.get(norm);
100
115
  if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
101
116
  return this.resolver.readFileBuffer(entry.id, this.ctx, entry.metadata);
@@ -30,7 +30,7 @@ export function createVirtualFsActivities<
30
30
  TMeta = FileEntryMetadata,
31
31
  >(
32
32
  resolver: FileResolver<TCtx, TMeta>,
33
- scope: S,
33
+ scope: S
34
34
  ): PrefixedVirtualFsOps<S, TCtx, TMeta> {
35
35
  const ops: VirtualFsOps<TCtx, TMeta> = {
36
36
  resolveFileTree: async (ctx: TCtx) => {
@@ -39,10 +39,10 @@ export function createVirtualFsActivities<
39
39
  },
40
40
  };
41
41
 
42
- const prefix = scope;
42
+ const prefix = `virtualFs${scope.charAt(0).toUpperCase()}${scope.slice(1)}`;
43
43
  const cap = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
44
44
 
45
45
  return Object.fromEntries(
46
- Object.entries(ops).map(([k, v]) => [`${prefix}${cap(k)}`, v]),
46
+ Object.entries(ops).map(([k, v]) => [`${prefix}${cap(k)}`, v])
47
47
  ) as PrefixedVirtualFsOps<S, TCtx, TMeta>;
48
48
  }
@@ -18,7 +18,7 @@ import type { VirtualFsOps } from "./types";
18
18
 
19
19
  export function proxyVirtualFsOps<TCtx = unknown>(
20
20
  scope?: string,
21
- options?: Parameters<typeof proxyActivities>[0],
21
+ options?: Parameters<typeof proxyActivities>[0]
22
22
  ): VirtualFsOps<TCtx> {
23
23
  const resolvedScope = scope ?? workflowInfo().workflowType;
24
24
 
@@ -32,10 +32,10 @@ export function proxyVirtualFsOps<TCtx = unknown>(
32
32
  maximumInterval: "30s",
33
33
  backoffCoefficient: 2,
34
34
  },
35
- },
35
+ }
36
36
  );
37
37
 
38
- const prefix = resolvedScope;
38
+ const prefix = `virtualFs${resolvedScope.charAt(0).toUpperCase()}${resolvedScope.slice(1)}`;
39
39
  const p = (key: string): string =>
40
40
  `${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
41
41
 
@@ -118,8 +118,9 @@ export type PrefixedVirtualFsOps<
118
118
  */
119
119
  export interface VirtualFsState<TCtx = unknown, TMeta = FileEntryMetadata> {
120
120
  fileTree: FileEntry<TMeta>[];
121
- ctx: TCtx;
122
- workspaceBase?: string;
121
+ virtualFsCtx: TCtx;
122
+ /** In-memory file contents keyed by path, bypassing the resolver (e.g. skill resources). */
123
+ inlineFiles?: Record<string, string>;
123
124
  }
124
125
 
125
126
  // ============================================================================
@@ -325,6 +325,70 @@ describe("VirtualFileSystem", () => {
325
325
  });
326
326
  });
327
327
 
328
+ // ============================================================================
329
+ // VirtualFileSystem — inlineFiles
330
+ // ============================================================================
331
+
332
+ describe("VirtualFileSystem — inlineFiles", () => {
333
+ const skillEntry: FileEntry = {
334
+ id: "skill:code-review:checklist.md",
335
+ path: "/skills/code-review/checklist.md",
336
+ size: 18,
337
+ mtime: "2025-01-01T00:00:00.000Z",
338
+ metadata: {},
339
+ };
340
+
341
+ const inlineFiles: Record<string, string> = {
342
+ "/skills/code-review/checklist.md": "# Review checklist",
343
+ };
344
+
345
+ function createFsWithInline() {
346
+ const { resolver } = createMockResolver();
347
+ return new VirtualFileSystem(
348
+ [...sampleTree, skillEntry],
349
+ resolver,
350
+ ctx,
351
+ "/",
352
+ inlineFiles,
353
+ );
354
+ }
355
+
356
+ it("readFile returns inline content instead of hitting the resolver", async () => {
357
+ const fs = createFsWithInline();
358
+ const content = await fs.readFile("/skills/code-review/checklist.md");
359
+ expect(content).toBe("# Review checklist");
360
+ });
361
+
362
+ it("readFileBuffer returns inline content as Uint8Array", async () => {
363
+ const fs = createFsWithInline();
364
+ const buf = await fs.readFileBuffer("/skills/code-review/checklist.md");
365
+ expect(new TextDecoder().decode(buf)).toBe("# Review checklist");
366
+ });
367
+
368
+ it("inline files are visible in readdir", async () => {
369
+ const fs = createFsWithInline();
370
+ const names = await fs.readdir("/skills/code-review");
371
+ expect(names).toContain("checklist.md");
372
+ });
373
+
374
+ it("inline file directories are inferred", async () => {
375
+ const fs = createFsWithInline();
376
+ expect(await fs.exists("/skills")).toBe(true);
377
+ expect(await fs.exists("/skills/code-review")).toBe(true);
378
+ });
379
+
380
+ it("readFile still falls back to resolver for non-inline files", async () => {
381
+ const fs = createFsWithInline();
382
+ const content = await fs.readFile("/src/index.ts");
383
+ expect(content).toBe('console.log("hello");');
384
+ });
385
+
386
+ it("readFile throws ENOENT for paths not in tree or inline", async () => {
387
+ const fs = createFsWithInline();
388
+ await expect(fs.readFile("/nope.txt")).rejects.toThrow("ENOENT");
389
+ });
390
+ });
391
+
328
392
  // ============================================================================
329
393
  // createVirtualFsActivities
330
394
  // ============================================================================
@@ -66,7 +66,7 @@ export function withVirtualFs<
66
66
  const state =
67
67
  await queryParentWorkflowState<VirtualFsState<TCtx, TMeta>>(client);
68
68
 
69
- const { fileTree, ctx, workspaceBase } = state;
69
+ const { fileTree, virtualFsCtx, inlineFiles } = state;
70
70
  if (!fileTree) {
71
71
  return {
72
72
  toolResponse: `Error: No fileTree in agent state. The ${context.toolName} tool requires a virtual filesystem.`,
@@ -77,8 +77,9 @@ export function withVirtualFs<
77
77
  const virtualFs = new VirtualFileSystem(
78
78
  fileTree,
79
79
  resolver,
80
- ctx,
81
- workspaceBase ?? "/"
80
+ virtualFsCtx,
81
+ "/",
82
+ inlineFiles
82
83
  );
83
84
  const response = await handler(args, { ...context, virtualFs });
84
85
  const mutations = virtualFs.getMutations();
@@ -0,0 +1 @@
1
+ E2B_API_KEY=e2b_39af116424059782e2aee6942fd70237cc2126c9