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
@@ -0,0 +1,345 @@
1
+ import type {
2
+ SandboxFileSystem,
3
+ DirentEntry,
4
+ FileStat,
5
+ } from "../../../lib/sandbox/types";
6
+ import { SandboxNotSupportedError } from "../../../lib/sandbox/types";
7
+ import type {
8
+ FileEntry,
9
+ FileEntryMetadata,
10
+ FileResolver,
11
+ TreeMutation,
12
+ } from "./types";
13
+
14
+ /**
15
+ * Normalise a virtual path to a canonical form: absolute, no trailing slash
16
+ * (except root), no double slashes.
17
+ */
18
+ function normalisePath(p: string): string {
19
+ if (!p.startsWith("/")) p = "/" + p;
20
+ p = p.replace(/\/+/g, "/");
21
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
22
+ return p;
23
+ }
24
+
25
+ /** Return the parent directory of a normalised path ("/a/b" → "/a"). */
26
+ function parentDir(p: string): string {
27
+ const idx = p.lastIndexOf("/");
28
+ return idx <= 0 ? "/" : p.slice(0, idx);
29
+ }
30
+
31
+ /**
32
+ * Collect the set of implicit directory paths from a flat file list.
33
+ * E.g. "/a/b/c.ts" contributes "/a/b", "/a", "/".
34
+ */
35
+ function inferDirectories(entries: { path: string }[]): Set<string> {
36
+ const dirs = new Set<string>();
37
+ dirs.add("/");
38
+ for (const entry of entries) {
39
+ let dir = parentDir(normalisePath(entry.path));
40
+ while (dir !== "/" && !dirs.has(dir)) {
41
+ dirs.add(dir);
42
+ dir = parentDir(dir);
43
+ }
44
+ dirs.add("/");
45
+ }
46
+ return dirs;
47
+ }
48
+
49
+ /**
50
+ * Ephemeral {@link SandboxFileSystem} backed by a {@link FileResolver}.
51
+ *
52
+ * Created fresh for each tool invocation from the current workflow file tree.
53
+ * Directory structure is inferred from file paths. All mutations are tracked
54
+ * and can be retrieved via {@link getMutations} after the handler completes.
55
+ */
56
+ export class VirtualSandboxFileSystem<
57
+ TCtx = unknown,
58
+ TMeta = FileEntryMetadata,
59
+ > implements SandboxFileSystem
60
+ {
61
+ private entries: Map<string, FileEntry<TMeta>>;
62
+ private directories: Set<string>;
63
+ private mutations: TreeMutation<TMeta>[] = [];
64
+
65
+ constructor(
66
+ tree: FileEntry<TMeta>[],
67
+ private resolver: FileResolver<TCtx, TMeta>,
68
+ private ctx: TCtx,
69
+ ) {
70
+ this.entries = new Map(
71
+ tree.map((e) => [normalisePath(e.path), e]),
72
+ );
73
+ this.directories = inferDirectories(tree);
74
+ }
75
+
76
+ /** Return all mutations accumulated during this invocation. */
77
+ getMutations(): TreeMutation<TMeta>[] {
78
+ return this.mutations;
79
+ }
80
+
81
+ /** Look up a file entry by virtual path. */
82
+ getEntry(path: string): FileEntry<TMeta> | undefined {
83
+ return this.entries.get(normalisePath(path));
84
+ }
85
+
86
+ // --------------------------------------------------------------------------
87
+ // Read operations — delegate to resolver lazily
88
+ // --------------------------------------------------------------------------
89
+
90
+ async readFile(path: string): Promise<string> {
91
+ const entry = this.entries.get(normalisePath(path));
92
+ if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
93
+ return this.resolver.readFile(entry.id, this.ctx);
94
+ }
95
+
96
+ async readFileBuffer(path: string): Promise<Uint8Array> {
97
+ const entry = this.entries.get(normalisePath(path));
98
+ if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
99
+ return this.resolver.readFileBuffer(entry.id, this.ctx);
100
+ }
101
+
102
+ // --------------------------------------------------------------------------
103
+ // Metadata operations — pure, resolved from the tree
104
+ // --------------------------------------------------------------------------
105
+
106
+ async exists(path: string): Promise<boolean> {
107
+ const norm = normalisePath(path);
108
+ return this.entries.has(norm) || this.directories.has(norm);
109
+ }
110
+
111
+ async stat(path: string): Promise<FileStat> {
112
+ const norm = normalisePath(path);
113
+ const entry = this.entries.get(norm);
114
+ if (entry) {
115
+ return {
116
+ isFile: true,
117
+ isDirectory: false,
118
+ isSymbolicLink: false,
119
+ size: entry.size,
120
+ mtime: new Date(entry.mtime),
121
+ };
122
+ }
123
+ if (this.directories.has(norm)) {
124
+ return {
125
+ isFile: false,
126
+ isDirectory: true,
127
+ isSymbolicLink: false,
128
+ size: 0,
129
+ mtime: new Date(),
130
+ };
131
+ }
132
+ throw new Error(`ENOENT: no such file or directory: ${path}`);
133
+ }
134
+
135
+ async readdir(path: string): Promise<string[]> {
136
+ const norm = normalisePath(path);
137
+ if (!this.directories.has(norm)) {
138
+ throw new Error(`ENOENT: no such directory: ${path}`);
139
+ }
140
+ const prefix = norm === "/" ? "/" : norm + "/";
141
+ const names = new Set<string>();
142
+
143
+ for (const p of this.entries.keys()) {
144
+ if (p.startsWith(prefix)) {
145
+ const rest = p.slice(prefix.length);
146
+ const seg = rest.split("/")[0];
147
+ if (seg) names.add(seg);
148
+ }
149
+ }
150
+ for (const d of this.directories) {
151
+ if (d.startsWith(prefix) && d !== norm) {
152
+ const rest = d.slice(prefix.length);
153
+ const seg = rest.split("/")[0];
154
+ if (seg) names.add(seg);
155
+ }
156
+ }
157
+ return [...names].sort();
158
+ }
159
+
160
+ async readdirWithFileTypes(path: string): Promise<DirentEntry[]> {
161
+ const names = await this.readdir(path);
162
+ const norm = normalisePath(path);
163
+ const prefix = norm === "/" ? "/" : norm + "/";
164
+
165
+ return names.map((name) => {
166
+ const full = prefix + name;
167
+ const isFile = this.entries.has(full);
168
+ const isDirectory = this.directories.has(full);
169
+ return { name, isFile, isDirectory, isSymbolicLink: false };
170
+ });
171
+ }
172
+
173
+ // --------------------------------------------------------------------------
174
+ // Write operations — delegate to resolver, record mutations
175
+ // --------------------------------------------------------------------------
176
+
177
+ async writeFile(path: string, content: string | Uint8Array): Promise<void> {
178
+ const norm = normalisePath(path);
179
+ const existing = this.entries.get(norm);
180
+
181
+ if (existing) {
182
+ await this.resolver.writeFile(existing.id, content, this.ctx);
183
+ const size =
184
+ typeof content === "string"
185
+ ? new TextEncoder().encode(content).byteLength
186
+ : content.byteLength;
187
+ const updated: FileEntry<TMeta> = {
188
+ ...existing,
189
+ size,
190
+ mtime: new Date().toISOString(),
191
+ };
192
+ this.entries.set(norm, updated);
193
+ this.mutations.push({ type: "update", path: norm, entry: updated });
194
+ } else {
195
+ const entry = await this.resolver.createFile(norm, content, this.ctx);
196
+ const normalised = { ...entry, path: norm };
197
+ this.entries.set(norm, normalised);
198
+ this.addParentDirectories(norm);
199
+ this.mutations.push({ type: "add", entry: normalised });
200
+ }
201
+ }
202
+
203
+ async appendFile(path: string, content: string | Uint8Array): Promise<void> {
204
+ const norm = normalisePath(path);
205
+ const existing = this.entries.get(norm);
206
+
207
+ if (!existing) {
208
+ return this.writeFile(path, content);
209
+ }
210
+
211
+ const current = await this.resolver.readFile(existing.id, this.ctx);
212
+ const appended =
213
+ typeof content === "string"
214
+ ? current + content
215
+ : current + new TextDecoder().decode(content);
216
+ await this.resolver.writeFile(existing.id, appended, this.ctx);
217
+
218
+ const size = new TextEncoder().encode(appended).byteLength;
219
+ const updated: FileEntry<TMeta> = {
220
+ ...existing,
221
+ size,
222
+ mtime: new Date().toISOString(),
223
+ };
224
+ this.entries.set(norm, updated);
225
+ this.mutations.push({ type: "update", path: norm, entry: updated });
226
+ }
227
+
228
+ async mkdir(_path: string, _options?: { recursive?: boolean }): Promise<void> {
229
+ const norm = normalisePath(_path);
230
+ if (this.directories.has(norm)) return;
231
+
232
+ if (_options?.recursive) {
233
+ this.addParentDirectories(norm + "/placeholder");
234
+ this.directories.add(norm);
235
+ } else {
236
+ const parent = parentDir(norm);
237
+ if (!this.directories.has(parent)) {
238
+ throw new Error(`ENOENT: no such directory: ${parent}`);
239
+ }
240
+ this.directories.add(norm);
241
+ }
242
+ }
243
+
244
+ async rm(
245
+ path: string,
246
+ options?: { recursive?: boolean; force?: boolean },
247
+ ): Promise<void> {
248
+ const norm = normalisePath(path);
249
+ const entry = this.entries.get(norm);
250
+
251
+ if (entry) {
252
+ await this.resolver.deleteFile(entry.id, this.ctx);
253
+ this.entries.delete(norm);
254
+ this.mutations.push({ type: "remove", path: norm });
255
+ return;
256
+ }
257
+
258
+ if (this.directories.has(norm)) {
259
+ if (!options?.recursive) {
260
+ throw new Error(`EISDIR: is a directory (use recursive): ${path}`);
261
+ }
262
+ const prefix = norm === "/" ? "/" : norm + "/";
263
+ for (const [p, e] of this.entries) {
264
+ if (p.startsWith(prefix)) {
265
+ await this.resolver.deleteFile(e.id, this.ctx);
266
+ this.entries.delete(p);
267
+ this.mutations.push({ type: "remove", path: p });
268
+ }
269
+ }
270
+ for (const d of this.directories) {
271
+ if (d.startsWith(prefix)) this.directories.delete(d);
272
+ }
273
+ this.directories.delete(norm);
274
+ return;
275
+ }
276
+
277
+ if (!options?.force) {
278
+ throw new Error(`ENOENT: no such file or directory: ${path}`);
279
+ }
280
+ }
281
+
282
+ async cp(
283
+ src: string,
284
+ dest: string,
285
+ _options?: { recursive?: boolean },
286
+ ): Promise<void> {
287
+ const normSrc = normalisePath(src);
288
+ const normDest = normalisePath(dest);
289
+
290
+ const entry = this.entries.get(normSrc);
291
+ if (entry) {
292
+ const content = await this.resolver.readFile(entry.id, this.ctx);
293
+ await this.writeFile(normDest, content);
294
+ return;
295
+ }
296
+
297
+ if (!this.directories.has(normSrc)) {
298
+ throw new Error(`ENOENT: no such file or directory: ${src}`);
299
+ }
300
+ if (!_options?.recursive) {
301
+ throw new Error(`EISDIR: is a directory (use recursive): ${src}`);
302
+ }
303
+
304
+ const prefix = normSrc === "/" ? "/" : normSrc + "/";
305
+ for (const [p, e] of this.entries) {
306
+ if (p.startsWith(prefix)) {
307
+ const relative = p.slice(normSrc.length);
308
+ const content = await this.resolver.readFile(e.id, this.ctx);
309
+ await this.writeFile(normDest + relative, content);
310
+ }
311
+ }
312
+ }
313
+
314
+ async mv(src: string, dest: string): Promise<void> {
315
+ await this.cp(src, dest, { recursive: true });
316
+ await this.rm(src, { recursive: true });
317
+ }
318
+
319
+ // --------------------------------------------------------------------------
320
+ // Unsupported
321
+ // --------------------------------------------------------------------------
322
+
323
+ async readlink(_path: string): Promise<string> {
324
+ throw new SandboxNotSupportedError("readlink");
325
+ }
326
+
327
+ resolvePath(base: string, path: string): string {
328
+ if (path.startsWith("/")) return normalisePath(path);
329
+ const combined =
330
+ base.endsWith("/") ? base + path : base + "/" + path;
331
+ return normalisePath(combined);
332
+ }
333
+
334
+ // --------------------------------------------------------------------------
335
+ // Helpers
336
+ // --------------------------------------------------------------------------
337
+
338
+ private addParentDirectories(filePath: string): void {
339
+ let dir = parentDir(normalisePath(filePath));
340
+ while (!this.directories.has(dir)) {
341
+ this.directories.add(dir);
342
+ dir = parentDir(dir);
343
+ }
344
+ }
345
+ }
@@ -0,0 +1,88 @@
1
+ import type {
2
+ Sandbox,
3
+ SandboxCapabilities,
4
+ ExecOptions,
5
+ ExecResult,
6
+ } from "../../../lib/sandbox/types";
7
+ import { SandboxNotSupportedError } from "../../../lib/sandbox/types";
8
+ import { VirtualSandboxFileSystem } from "./filesystem";
9
+ import type {
10
+ FileEntry,
11
+ FileEntryMetadata,
12
+ FileResolver,
13
+ VirtualSandbox,
14
+ } from "./types";
15
+
16
+ // ============================================================================
17
+ // VirtualSandbox
18
+ // ============================================================================
19
+
20
+ class VirtualSandboxImpl<
21
+ TCtx = unknown,
22
+ TMeta = FileEntryMetadata,
23
+ > implements Sandbox
24
+ {
25
+ readonly capabilities: SandboxCapabilities = {
26
+ filesystem: true,
27
+ execution: false,
28
+ persistence: true,
29
+ };
30
+
31
+ readonly fs: VirtualSandboxFileSystem<TCtx, TMeta>;
32
+
33
+ constructor(
34
+ readonly id: string,
35
+ tree: FileEntry<TMeta>[],
36
+ resolver: FileResolver<TCtx, TMeta>,
37
+ ctx: TCtx
38
+ ) {
39
+ this.fs = new VirtualSandboxFileSystem(tree, resolver, ctx);
40
+ }
41
+
42
+ async exec(_command: string, _options?: ExecOptions): Promise<ExecResult> {
43
+ throw new SandboxNotSupportedError("exec");
44
+ }
45
+
46
+ async destroy(): Promise<void> {
47
+ // Ephemeral — nothing to clean up
48
+ }
49
+ }
50
+
51
+ // ============================================================================
52
+ // Factory
53
+ // ============================================================================
54
+
55
+ /**
56
+ * Create an ephemeral {@link Sandbox} from a file tree and resolver.
57
+ *
58
+ * Used internally by {@link withVirtualSandbox} and
59
+ * {@link VirtualSandboxProvider}; consumers can also call this directly
60
+ * if they need a sandbox outside the wrapper pattern.
61
+ */
62
+ export function createVirtualSandbox<
63
+ TCtx,
64
+ TMeta = FileEntryMetadata,
65
+ >(
66
+ id: string,
67
+ tree: FileEntry<TMeta>[],
68
+ resolver: FileResolver<TCtx, TMeta>,
69
+ ctx: TCtx,
70
+ ): VirtualSandbox<TCtx, TMeta> {
71
+ return new VirtualSandboxImpl(id, tree, resolver, ctx);
72
+ }
73
+
74
+ // Re-exports for convenience
75
+ export { VirtualSandboxFileSystem } from "./filesystem";
76
+ export { VirtualSandboxProvider } from "./provider";
77
+ export { withVirtualSandbox } from "./with-virtual-sandbox";
78
+ export type {
79
+ FileEntry,
80
+ FileEntryMetadata,
81
+ FileResolver,
82
+ VirtualFileTree,
83
+ VirtualSandboxCreateOptions,
84
+ VirtualSandboxState,
85
+ VirtualSandboxContext,
86
+ VirtualSandbox,
87
+ TreeMutation,
88
+ } from "./types";
@@ -0,0 +1,38 @@
1
+ import type { FileEntryMetadata, VirtualFileTree, TreeMutation } from "./types";
2
+
3
+ /**
4
+ * Apply a list of {@link TreeMutation}s to the `fileTree` stored in a state
5
+ * manager instance, updating it in place and returning the new tree.
6
+ *
7
+ * The `stateManager` parameter is structurally typed so any
8
+ * {@link AgentStateManager} whose custom state includes
9
+ * `fileTree: VirtualFileTree<TMeta>` will satisfy it.
10
+ */
11
+ export function applyVirtualTreeMutations<TMeta = FileEntryMetadata>(
12
+ stateManager: {
13
+ get(key: "fileTree"): VirtualFileTree<TMeta>;
14
+ set(key: "fileTree", value: VirtualFileTree<TMeta>): void;
15
+ },
16
+ mutations: TreeMutation<TMeta>[],
17
+ ): VirtualFileTree<TMeta> {
18
+ let tree = [...stateManager.get("fileTree")];
19
+
20
+ for (const m of mutations) {
21
+ switch (m.type) {
22
+ case "add":
23
+ tree.push(m.entry);
24
+ break;
25
+ case "remove":
26
+ tree = tree.filter((e) => e.path !== m.path);
27
+ break;
28
+ case "update":
29
+ tree = tree.map((e) =>
30
+ e.path === m.path ? { ...e, ...m.entry } : e
31
+ );
32
+ break;
33
+ }
34
+ }
35
+
36
+ stateManager.set("fileTree", tree);
37
+ return tree;
38
+ }
@@ -0,0 +1,101 @@
1
+ import type {
2
+ SandboxCapabilities,
3
+ SandboxCreateResult,
4
+ SandboxProvider,
5
+ } from "../../../lib/sandbox/types";
6
+ import { SandboxNotSupportedError } from "../../../lib/sandbox/types";
7
+ import { getShortId } from "../../../lib/thread/id";
8
+ import { createVirtualSandbox } from "./index";
9
+ import type {
10
+ FileEntryMetadata,
11
+ FileResolver,
12
+ VirtualSandboxCreateOptions,
13
+ } from "./types";
14
+
15
+ /**
16
+ * Stateless {@link SandboxProvider} backed by a {@link FileResolver}.
17
+ *
18
+ * The provider holds **no internal state**. All sandbox state (sandboxId,
19
+ * fileTree, resolverContext) is returned as a `stateUpdate` from
20
+ * {@link create} and merged into the workflow's `AgentState` by the session.
21
+ * {@link withVirtualSandbox} reads this state back on every tool invocation.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const provider = new VirtualSandboxProvider(resolver);
26
+ * const manager = new SandboxManager(provider);
27
+ *
28
+ * export const activities = {
29
+ * ...manager.createActivities(),
30
+ * readFile: withVirtualSandbox(client, provider, readHandler),
31
+ * };
32
+ * ```
33
+ */
34
+ export class VirtualSandboxProvider<
35
+ TCtx = unknown,
36
+ TMeta = FileEntryMetadata,
37
+ > implements SandboxProvider<VirtualSandboxCreateOptions<TCtx>> {
38
+ readonly id = "virtual";
39
+ readonly capabilities: SandboxCapabilities = {
40
+ filesystem: true,
41
+ execution: false,
42
+ persistence: true,
43
+ };
44
+
45
+ readonly resolver: FileResolver<TCtx, TMeta>;
46
+
47
+ constructor(resolver: FileResolver<TCtx, TMeta>) {
48
+ this.resolver = resolver;
49
+ }
50
+
51
+ async create(
52
+ options?: VirtualSandboxCreateOptions<TCtx>
53
+ ): Promise<SandboxCreateResult> {
54
+ if (!options || !("resolverContext" in options)) {
55
+ throw new Error("VirtualSandboxProvider.create requires resolverContext");
56
+ }
57
+
58
+ const sandboxId = options.id ?? getShortId();
59
+ const fileTree = await this.resolver.resolveEntries(
60
+ options.resolverContext
61
+ );
62
+
63
+ const sandbox = createVirtualSandbox(
64
+ sandboxId,
65
+ fileTree,
66
+ this.resolver,
67
+ options.resolverContext
68
+ );
69
+
70
+ return {
71
+ sandbox,
72
+ stateUpdate: {
73
+ sandboxId,
74
+ fileTree,
75
+ resolverContext: options.resolverContext,
76
+ },
77
+ };
78
+ }
79
+
80
+ async get(): Promise<never> {
81
+ throw new SandboxNotSupportedError(
82
+ "get (virtual sandbox state lives in workflow AgentState)"
83
+ );
84
+ }
85
+
86
+ async destroy(): Promise<void> {
87
+ // No-op — no internal state to clean up
88
+ }
89
+
90
+ async snapshot(): Promise<never> {
91
+ throw new SandboxNotSupportedError(
92
+ "snapshot (virtual sandbox state lives in workflow AgentState)"
93
+ );
94
+ }
95
+
96
+ async restore(): Promise<never> {
97
+ throw new SandboxNotSupportedError(
98
+ "restore (virtual sandbox state lives in workflow AgentState)"
99
+ );
100
+ }
101
+ }
@@ -0,0 +1,82 @@
1
+ import type { FileEntry } from "./types";
2
+
3
+ interface TreeNode {
4
+ name: string;
5
+ children: Map<string, TreeNode>;
6
+ isFile: boolean;
7
+ }
8
+
9
+ const buildTree = <T>(entries: FileEntry<T>[]): TreeNode => {
10
+ const root: TreeNode = { name: "/", children: new Map(), isFile: false };
11
+
12
+ for (const entry of entries) {
13
+ const parts = entry.path.split("/").filter(Boolean);
14
+ let current = root;
15
+ for (const part of parts) {
16
+ let child = current.children.get(part);
17
+ if (!child) {
18
+ child = { name: part, children: new Map(), isFile: false };
19
+ current.children.set(part, child);
20
+ }
21
+ current = child;
22
+ }
23
+ current.isFile = current.children.size === 0;
24
+ }
25
+
26
+ return root;
27
+ };
28
+
29
+ const printNode = (node: TreeNode, tab: string, sort: boolean): string => {
30
+ const entries = [...node.children.values()];
31
+ if (sort) {
32
+ entries.sort((a, b) => {
33
+ if (!a.isFile && !b.isFile) return a.name.localeCompare(b.name);
34
+ if (!a.isFile) return -1;
35
+ if (!b.isFile) return 1;
36
+ return a.name.localeCompare(b.name);
37
+ });
38
+ }
39
+
40
+ let str = "";
41
+ for (const [i, entry] of entries.entries()) {
42
+ const isLast = i === entries.length - 1;
43
+ const branch = isLast ? "└─" : "├─";
44
+ const childTab = tab + (isLast ? " " : "│ ");
45
+
46
+ if (entry.isFile) {
47
+ str += "\n" + tab + branch + " " + entry.name;
48
+ } else {
49
+ const subtree = printNode(entry, childTab, sort);
50
+ str += "\n" + tab + branch + " " + entry.name + "/" + subtree;
51
+ }
52
+ }
53
+ return str;
54
+ };
55
+
56
+ /**
57
+ * Generates a formatted file tree string from a flat {@link FileEntry} list.
58
+ * Directories are inferred from file paths — no filesystem access needed.
59
+ *
60
+ * @param entries - Flat list of file entries
61
+ * @param opts - Optional configuration
62
+ * @param opts.sort - Sort entries alphabetically with directories first (defaults to true)
63
+ * @returns Formatted file tree string
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const tree = formatVirtualFileTree(state.fileTree);
68
+ * // /
69
+ * // ├─ src/
70
+ * // │ ├─ index.ts
71
+ * // │ └─ utils.ts
72
+ * // └─ package.json
73
+ * ```
74
+ */
75
+ export function formatVirtualFileTree<T>(
76
+ entries: FileEntry<T>[],
77
+ opts: { sort?: boolean } = {}
78
+ ): string {
79
+ const sort = opts.sort ?? true;
80
+ const root = buildTree(entries);
81
+ return "/" + printNode(root, "", sort);
82
+ }