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.
- package/README.md +49 -38
- package/dist/adapters/sandbox/daytona/index.cjs +205 -0
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -0
- package/dist/adapters/sandbox/daytona/index.d.cts +86 -0
- package/dist/adapters/sandbox/daytona/index.d.ts +86 -0
- package/dist/adapters/sandbox/daytona/index.js +202 -0
- package/dist/adapters/sandbox/daytona/index.js.map +1 -0
- package/dist/adapters/sandbox/inmemory/index.cjs +174 -0
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -0
- package/dist/adapters/sandbox/inmemory/index.d.cts +28 -0
- package/dist/adapters/sandbox/inmemory/index.d.ts +28 -0
- package/dist/adapters/sandbox/inmemory/index.js +172 -0
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -0
- package/dist/adapters/sandbox/virtual/index.cjs +405 -0
- package/dist/adapters/sandbox/virtual/index.cjs.map +1 -0
- package/dist/adapters/sandbox/virtual/index.d.cts +85 -0
- package/dist/adapters/sandbox/virtual/index.d.ts +85 -0
- package/dist/adapters/sandbox/virtual/index.js +400 -0
- package/dist/adapters/sandbox/virtual/index.js.map +1 -0
- package/dist/adapters/thread/google-genai/index.cjs +284 -0
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -0
- package/dist/adapters/thread/google-genai/index.d.cts +145 -0
- package/dist/adapters/thread/google-genai/index.d.ts +145 -0
- package/dist/adapters/thread/google-genai/index.js +278 -0
- package/dist/adapters/thread/google-genai/index.js.map +1 -0
- package/dist/adapters/{langchain → thread/langchain}/index.cjs +7 -9
- package/dist/adapters/thread/langchain/index.cjs.map +1 -0
- package/dist/adapters/{langchain → thread/langchain}/index.d.cts +17 -21
- package/dist/adapters/{langchain → thread/langchain}/index.d.ts +17 -21
- package/dist/adapters/{langchain → thread/langchain}/index.js +7 -9
- package/dist/adapters/thread/langchain/index.js.map +1 -0
- package/dist/index.cjs +816 -545
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +235 -74
- package/dist/index.d.ts +235 -74
- package/dist/index.js +804 -540
- package/dist/index.js.map +1 -1
- package/dist/types-B4C9txdq.d.ts +389 -0
- package/dist/{thread-manager-qc0g5Rvd.d.cts → types-B9ljZewB.d.cts} +1 -6
- package/dist/{thread-manager-qc0g5Rvd.d.ts → types-B9ljZewB.d.ts} +1 -6
- package/dist/types-BMXzv7TN.d.cts +476 -0
- package/dist/types-BMXzv7TN.d.ts +476 -0
- package/dist/types-BVP87m_W.d.cts +121 -0
- package/dist/types-CDubRtad.d.cts +115 -0
- package/dist/types-CDubRtad.d.ts +115 -0
- package/dist/types-CwwgQ_9H.d.ts +121 -0
- package/dist/types-GpMU4b0w.d.cts +389 -0
- package/dist/workflow.cjs +444 -318
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +271 -222
- package/dist/workflow.d.ts +271 -222
- package/dist/workflow.js +440 -316
- package/dist/workflow.js.map +1 -1
- package/package.json +59 -6
- package/src/adapters/sandbox/daytona/filesystem.ts +136 -0
- package/src/adapters/sandbox/daytona/index.ts +149 -0
- package/src/adapters/sandbox/daytona/types.ts +34 -0
- package/src/adapters/sandbox/inmemory/index.ts +213 -0
- package/src/adapters/sandbox/virtual/filesystem.ts +345 -0
- package/src/adapters/sandbox/virtual/index.ts +88 -0
- package/src/adapters/sandbox/virtual/mutations.ts +38 -0
- package/src/adapters/sandbox/virtual/provider.ts +101 -0
- package/src/adapters/sandbox/virtual/tree.ts +82 -0
- package/src/adapters/sandbox/virtual/types.ts +127 -0
- package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +523 -0
- package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +91 -0
- package/src/adapters/thread/google-genai/activities.ts +121 -0
- package/src/adapters/thread/google-genai/index.ts +41 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +154 -0
- package/src/adapters/thread/google-genai/thread-manager.ts +169 -0
- package/src/adapters/{langchain → thread/langchain}/activities.ts +11 -15
- package/src/adapters/{langchain → thread/langchain}/index.ts +1 -1
- package/src/adapters/{langchain → thread/langchain}/model-invoker.ts +15 -18
- package/src/adapters/{langchain → thread/langchain}/thread-manager.ts +1 -1
- package/src/index.ts +32 -24
- package/src/lib/activity.ts +87 -0
- package/src/lib/hooks/index.ts +11 -0
- package/src/lib/hooks/types.ts +98 -0
- package/src/lib/model/helpers.ts +6 -0
- package/src/lib/model/index.ts +13 -0
- package/src/lib/{model-invoker.ts → model/types.ts} +18 -1
- package/src/lib/sandbox/index.ts +19 -0
- package/src/lib/sandbox/manager.ts +76 -0
- package/src/lib/sandbox/sandbox.test.ts +158 -0
- package/src/lib/{fs.ts → sandbox/tree.ts} +6 -6
- package/src/lib/sandbox/types.ts +164 -0
- package/src/lib/session/index.ts +11 -0
- package/src/lib/{session.ts → session/session.ts} +76 -48
- package/src/lib/session/types.ts +93 -0
- package/src/lib/skills/fs-provider.ts +16 -15
- package/src/lib/skills/handler.ts +31 -0
- package/src/lib/skills/index.ts +5 -1
- package/src/lib/skills/register.ts +20 -0
- package/src/lib/skills/tool.ts +47 -0
- package/src/lib/state/index.ts +9 -0
- package/src/lib/{state-manager.ts → state/manager.ts} +10 -147
- package/src/lib/state/types.ts +134 -0
- package/src/lib/subagent/define.ts +71 -0
- package/src/lib/subagent/handler.ts +99 -0
- package/src/lib/subagent/index.ts +13 -0
- package/src/lib/subagent/register.ts +53 -0
- package/src/lib/subagent/tool.ts +80 -0
- package/src/lib/subagent/types.ts +92 -0
- package/src/lib/thread/index.ts +7 -0
- package/src/lib/{thread-manager.ts → thread/manager.ts} +1 -33
- package/src/lib/thread/types.ts +33 -0
- package/src/lib/tool-router/auto-append.ts +55 -0
- package/src/lib/tool-router/index.ts +41 -0
- package/src/lib/tool-router/router.ts +462 -0
- package/src/lib/tool-router/types.ts +478 -0
- package/src/lib/tool-router/with-sandbox.ts +70 -0
- package/src/lib/types.ts +5 -382
- package/src/tools/bash/bash.test.ts +53 -55
- package/src/tools/bash/handler.ts +23 -51
- package/src/tools/edit/handler.ts +67 -81
- package/src/tools/glob/handler.ts +60 -17
- package/src/tools/read-file/handler.ts +67 -0
- package/src/tools/read-skill/handler.ts +1 -31
- package/src/tools/read-skill/tool.ts +5 -47
- package/src/tools/subagent/handler.ts +1 -100
- package/src/tools/subagent/tool.ts +5 -93
- package/src/tools/task-create/handler.ts +1 -1
- package/src/tools/task-get/handler.ts +1 -1
- package/src/tools/task-list/handler.ts +1 -1
- package/src/tools/task-update/handler.ts +1 -1
- package/src/tools/write-file/handler.ts +47 -0
- package/src/workflow.ts +88 -47
- package/tsup.config.ts +8 -1
- package/dist/adapters/langchain/index.cjs.map +0 -1
- package/dist/adapters/langchain/index.js.map +0 -1
- package/dist/model-invoker-y_zlyMqu.d.cts +0 -892
- package/dist/model-invoker-y_zlyMqu.d.ts +0 -892
- package/src/lib/tool-router.ts +0 -977
- package/src/lib/workflow-helpers.ts +0 -50
- /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
|
+
}
|