zeitlich 0.2.40 → 0.2.42
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 +12 -1
- package/dist/{activities-CvUrG3YG.d.cts → activities-Coafq5zr.d.cts} +2 -2
- package/dist/{activities-CULxRzJ1.d.ts → activities-CrN-ghLo.d.ts} +2 -2
- package/dist/adapters/sandbox/daytona/index.cjs +4 -23
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +18 -86
- package/dist/adapters/sandbox/daytona/index.d.ts +18 -86
- package/dist/adapters/sandbox/daytona/index.js +4 -23
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.cjs +1 -7
- package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.cts +9 -2
- package/dist/adapters/sandbox/daytona/workflow.d.ts +9 -2
- package/dist/adapters/sandbox/daytona/workflow.js +1 -7
- package/dist/adapters/sandbox/daytona/workflow.js.map +1 -1
- package/dist/adapters/sandbox/e2b/index.cjs +21 -3
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +48 -7
- package/dist/adapters/sandbox/e2b/index.d.ts +48 -7
- package/dist/adapters/sandbox/e2b/index.js +22 -5
- package/dist/adapters/sandbox/e2b/index.js.map +1 -1
- package/dist/adapters/sandbox/e2b/workflow.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.cts +4 -2
- package/dist/adapters/sandbox/e2b/workflow.d.ts +4 -2
- package/dist/adapters/sandbox/e2b/workflow.js.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.cjs +11 -0
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +11 -3
- package/dist/adapters/sandbox/inmemory/index.d.ts +11 -3
- package/dist/adapters/sandbox/inmemory/index.js +11 -1
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.cts +4 -2
- package/dist/adapters/sandbox/inmemory/workflow.d.ts +4 -2
- package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -1
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +6 -6
- package/dist/adapters/thread/anthropic/index.d.ts +6 -6
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +6 -6
- package/dist/adapters/thread/anthropic/workflow.d.ts +6 -6
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +6 -6
- package/dist/adapters/thread/google-genai/index.d.ts +6 -6
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +6 -6
- package/dist/adapters/thread/google-genai/workflow.d.ts +6 -6
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +6 -6
- package/dist/adapters/thread/langchain/index.d.ts +6 -6
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +6 -6
- package/dist/adapters/thread/langchain/workflow.d.ts +6 -6
- package/dist/index.cjs +316 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -17
- package/dist/index.d.ts +93 -17
- package/dist/index.js +317 -120
- package/dist/index.js.map +1 -1
- package/dist/{proxy-5EbwzaY4.d.cts → proxy-Bf7uI-Hw.d.cts} +1 -1
- package/dist/{proxy-wZufFfBh.d.ts → proxy-COqA95FW.d.ts} +1 -1
- package/dist/{thread-manager-BqBAIsED.d.ts → thread-manager-BhkOyQ1I.d.ts} +2 -2
- package/dist/{thread-manager-BNiIt5r8.d.ts → thread-manager-Bi1XlbpJ.d.ts} +2 -2
- package/dist/{thread-manager-DF8WuCRs.d.cts → thread-manager-BsLO3Fgc.d.cts} +2 -2
- package/dist/{thread-manager-BoN5DOvG.d.cts → thread-manager-wRVVBFgj.d.cts} +2 -2
- package/dist/{types-C7OoY7h8.d.ts → types-BkX4HLzi.d.ts} +1 -1
- package/dist/{types-CuISs0Ub.d.cts → types-C66-BVBr.d.cts} +1 -1
- package/dist/types-CJ7tCdl6.d.cts +266 -0
- package/dist/types-CJ7tCdl6.d.ts +266 -0
- package/dist/{types-DeQH84C_.d.ts → types-CdALEF3z.d.cts} +342 -23
- package/dist/{types-Cn2r3ol3.d.cts → types-ChAy_jSP.d.ts} +342 -23
- package/dist/types-CjY93AWZ.d.cts +84 -0
- package/dist/types-gVa5XCWD.d.ts +84 -0
- package/dist/{workflow-DhplIN65.d.cts → workflow-BwT5EybR.d.ts} +7 -6
- package/dist/{workflow-C2MZZj5K.d.ts → workflow-DMmiaw6w.d.cts} +7 -6
- package/dist/workflow.cjs +138 -77
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +4 -4
- package/dist/workflow.d.ts +4 -4
- package/dist/workflow.js +139 -78
- package/dist/workflow.js.map +1 -1
- package/package.json +17 -33
- package/src/adapters/sandbox/daytona/index.ts +25 -48
- package/src/adapters/sandbox/daytona/proxy.ts +7 -8
- package/src/adapters/sandbox/e2b/README.md +81 -0
- package/src/adapters/sandbox/e2b/index.ts +53 -11
- package/src/adapters/sandbox/e2b/keep-alive.test.ts +115 -0
- package/src/adapters/sandbox/e2b/proxy.ts +3 -2
- package/src/adapters/sandbox/e2b/types.ts +34 -2
- package/src/adapters/sandbox/inmemory/index.ts +21 -1
- package/src/adapters/sandbox/inmemory/proxy.ts +7 -3
- package/src/index.ts +1 -1
- package/src/lib/activity.ts +5 -0
- package/src/lib/sandbox/capability-types.test.ts +859 -0
- package/src/lib/sandbox/index.ts +1 -0
- package/src/lib/sandbox/manager.ts +187 -31
- package/src/lib/sandbox/types.ts +189 -46
- package/src/lib/session/index.ts +1 -0
- package/src/lib/session/session.integration.test.ts +58 -0
- package/src/lib/session/session.ts +109 -50
- package/src/lib/session/types.ts +189 -8
- package/src/lib/subagent/handler.ts +66 -43
- package/src/lib/subagent/subagent.integration.test.ts +2 -0
- package/src/lib/subagent/types.ts +492 -16
- package/src/lib/subagent/workflow.ts +11 -1
- package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +158 -0
- package/src/lib/tool-router/index.ts +1 -1
- package/src/lib/tool-router/with-sandbox.ts +45 -2
- package/src/lib/virtual-fs/filesystem.ts +41 -16
- package/src/lib/virtual-fs/types.ts +19 -0
- package/src/lib/virtual-fs/virtual-fs.test.ts +204 -1
- package/src/tools/read-file/handler.test.ts +83 -0
- package/src/workflow.ts +3 -0
- package/tsup.config.ts +0 -4
- package/dist/adapters/sandbox/bedrock/index.cjs +0 -457
- package/dist/adapters/sandbox/bedrock/index.cjs.map +0 -1
- package/dist/adapters/sandbox/bedrock/index.d.cts +0 -25
- package/dist/adapters/sandbox/bedrock/index.d.ts +0 -25
- package/dist/adapters/sandbox/bedrock/index.js +0 -454
- package/dist/adapters/sandbox/bedrock/index.js.map +0 -1
- package/dist/adapters/sandbox/bedrock/workflow.cjs +0 -36
- package/dist/adapters/sandbox/bedrock/workflow.cjs.map +0 -1
- package/dist/adapters/sandbox/bedrock/workflow.d.cts +0 -29
- package/dist/adapters/sandbox/bedrock/workflow.d.ts +0 -29
- package/dist/adapters/sandbox/bedrock/workflow.js +0 -34
- package/dist/adapters/sandbox/bedrock/workflow.js.map +0 -1
- package/dist/types-DAsQ21Rt.d.ts +0 -74
- package/dist/types-lm8tMNJQ.d.cts +0 -74
- package/dist/types-yx0LzPGn.d.cts +0 -173
- package/dist/types-yx0LzPGn.d.ts +0 -173
- package/src/adapters/sandbox/bedrock/filesystem.ts +0 -340
- package/src/adapters/sandbox/bedrock/index.ts +0 -274
- package/src/adapters/sandbox/bedrock/proxy.ts +0 -59
- package/src/adapters/sandbox/bedrock/types.ts +0 -24
|
@@ -4,6 +4,7 @@ import { withSandbox } from "./with-sandbox";
|
|
|
4
4
|
import type { RouterContext, ToolHandlerResponse } from "./types";
|
|
5
5
|
import type { ToolResultConfig } from "../types";
|
|
6
6
|
import type { Sandbox } from "../sandbox/types";
|
|
7
|
+
import { SandboxNotFoundError } from "../sandbox/types";
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// withAutoAppend
|
|
@@ -332,6 +333,163 @@ describe("withSandbox", () => {
|
|
|
332
333
|
).rejects.toThrow("sandbox not found");
|
|
333
334
|
});
|
|
334
335
|
|
|
336
|
+
it("propagates SandboxNotFoundError by default (no translate option)", async () => {
|
|
337
|
+
const manager = {
|
|
338
|
+
getSandbox: async (): Promise<Sandbox> => {
|
|
339
|
+
throw new SandboxNotFoundError("sb-gone");
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const handler = async (): Promise<ToolHandlerResponse<null>> => ({
|
|
344
|
+
toolResponse: "ok",
|
|
345
|
+
data: null,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const wrapped = withSandbox(manager, handler);
|
|
349
|
+
|
|
350
|
+
await expect(
|
|
351
|
+
wrapped(
|
|
352
|
+
{},
|
|
353
|
+
{
|
|
354
|
+
threadId: "t",
|
|
355
|
+
toolCallId: "tc",
|
|
356
|
+
toolName: "Bash",
|
|
357
|
+
sandboxId: "sb-gone",
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
).rejects.toBeInstanceOf(SandboxNotFoundError);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("translates SandboxNotFoundError into a structured response when opted in", async () => {
|
|
364
|
+
const manager = {
|
|
365
|
+
getSandbox: async (): Promise<Sandbox> => {
|
|
366
|
+
throw new SandboxNotFoundError("sb-gone");
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const innerCalled = vi.fn();
|
|
371
|
+
const handler = async (): Promise<ToolHandlerResponse<null>> => {
|
|
372
|
+
innerCalled();
|
|
373
|
+
return { toolResponse: "ok", data: null };
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const wrapped = withSandbox(manager, handler, {
|
|
377
|
+
translateSandboxNotFound: true,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const result = await wrapped(
|
|
381
|
+
{},
|
|
382
|
+
{
|
|
383
|
+
threadId: "t",
|
|
384
|
+
toolCallId: "tc",
|
|
385
|
+
toolName: "Bash",
|
|
386
|
+
sandboxId: "sb-gone",
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
expect(result.toolResponse).toContain("Bash");
|
|
391
|
+
expect(result.toolResponse).toContain("execution environment");
|
|
392
|
+
expect(result.toolResponse).toContain("no longer available");
|
|
393
|
+
expect(result.toolResponse).toContain("could not be completed");
|
|
394
|
+
expect(result.toolResponse).not.toContain("session cannot continue");
|
|
395
|
+
expect(result.data).toBeNull();
|
|
396
|
+
expect(innerCalled).not.toHaveBeenCalled();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("uses sandboxNotFoundToolResponse as the tool response when set", async () => {
|
|
400
|
+
const manager = {
|
|
401
|
+
getSandbox: async (): Promise<Sandbox> => {
|
|
402
|
+
throw new SandboxNotFoundError("sb-gone");
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const handler = async (): Promise<ToolHandlerResponse<null>> => ({
|
|
407
|
+
toolResponse: "ok",
|
|
408
|
+
data: null,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const wrapped = withSandbox(manager, handler, {
|
|
412
|
+
translateSandboxNotFound: true,
|
|
413
|
+
sandboxNotFoundToolResponse:
|
|
414
|
+
"El entorno de ejecución ya no está disponible. Reinicia el agente.",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const result = await wrapped(
|
|
418
|
+
{},
|
|
419
|
+
{
|
|
420
|
+
threadId: "t",
|
|
421
|
+
toolCallId: "tc",
|
|
422
|
+
toolName: "Bash",
|
|
423
|
+
sandboxId: "sb-gone",
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
expect(result.toolResponse).toBe(
|
|
428
|
+
"El entorno de ejecución ya no está disponible. Reinicia el agente."
|
|
429
|
+
);
|
|
430
|
+
expect(result.toolResponse).not.toContain("execution environment");
|
|
431
|
+
expect(result.toolResponse).not.toContain("Bash");
|
|
432
|
+
expect(result.data).toBeNull();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("ignores sandboxNotFoundToolResponse when translateSandboxNotFound is not enabled", async () => {
|
|
436
|
+
const manager = {
|
|
437
|
+
getSandbox: async (): Promise<Sandbox> => {
|
|
438
|
+
throw new SandboxNotFoundError("sb-gone");
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const handler = async (): Promise<ToolHandlerResponse<null>> => ({
|
|
443
|
+
toolResponse: "ok",
|
|
444
|
+
data: null,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const wrapped = withSandbox(manager, handler, {
|
|
448
|
+
sandboxNotFoundToolResponse: "should not be used",
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await expect(
|
|
452
|
+
wrapped(
|
|
453
|
+
{},
|
|
454
|
+
{
|
|
455
|
+
threadId: "t",
|
|
456
|
+
toolCallId: "tc",
|
|
457
|
+
toolName: "Bash",
|
|
458
|
+
sandboxId: "sb-gone",
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
).rejects.toBeInstanceOf(SandboxNotFoundError);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("does not translate non-SandboxNotFoundError errors when translate option is set", async () => {
|
|
465
|
+
const manager = {
|
|
466
|
+
getSandbox: async (): Promise<Sandbox> => {
|
|
467
|
+
throw new Error("network down");
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const handler = async (): Promise<ToolHandlerResponse<null>> => ({
|
|
472
|
+
toolResponse: "ok",
|
|
473
|
+
data: null,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const wrapped = withSandbox(manager, handler, {
|
|
477
|
+
translateSandboxNotFound: true,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
await expect(
|
|
481
|
+
wrapped(
|
|
482
|
+
{},
|
|
483
|
+
{
|
|
484
|
+
threadId: "t",
|
|
485
|
+
toolCallId: "tc",
|
|
486
|
+
toolName: "Bash",
|
|
487
|
+
sandboxId: "sb-gone",
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
).rejects.toThrow("network down");
|
|
491
|
+
});
|
|
492
|
+
|
|
335
493
|
it("passes all RouterContext fields through to inner handler", async () => {
|
|
336
494
|
const mockSandbox = createMockSandbox();
|
|
337
495
|
const manager = { getSandbox: async () => mockSandbox };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createToolRouter, defineTool, hasNoOtherToolCalls } from "./router";
|
|
2
2
|
export { withAutoAppend } from "./auto-append";
|
|
3
3
|
export { withSandbox } from "./with-sandbox";
|
|
4
|
-
export type { SandboxContext } from "./with-sandbox";
|
|
4
|
+
export type { SandboxContext, WithSandboxOptions } from "./with-sandbox";
|
|
5
5
|
|
|
6
6
|
export type {
|
|
7
7
|
ToolDefinition,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Sandbox } from "../sandbox/types";
|
|
2
|
+
import { SandboxNotFoundError } from "../sandbox/types";
|
|
2
3
|
import type { JsonValue } from "../state/types";
|
|
3
4
|
import type {
|
|
4
5
|
ActivityToolHandler,
|
|
@@ -6,6 +7,33 @@ import type {
|
|
|
6
7
|
ToolHandlerResponse,
|
|
7
8
|
} from "./types";
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Options for {@link withSandbox}.
|
|
12
|
+
*/
|
|
13
|
+
export interface WithSandboxOptions {
|
|
14
|
+
/**
|
|
15
|
+
* If `true`, a {@link SandboxNotFoundError} thrown by `manager.getSandbox`
|
|
16
|
+
* is translated into a structured tool-handler response (instead of
|
|
17
|
+
* propagating). This lets the agent return a graceful error to the model
|
|
18
|
+
* rather than crashing the workflow when the backing sandbox has been
|
|
19
|
+
* killed mid-run (e.g. because the E2B `timeoutMs` lifetime elapsed).
|
|
20
|
+
*
|
|
21
|
+
* Off by default to preserve the existing contract for callers that rely
|
|
22
|
+
* on the error bubbling out. New callers should generally enable this in
|
|
23
|
+
* combination with the E2B `keepAliveMs` provider option.
|
|
24
|
+
*
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
translateSandboxNotFound?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Custom tool response returned to the agent when the backing sandbox
|
|
30
|
+
* is not found and `translateSandboxNotFound` is `true`. Defaults to a
|
|
31
|
+
* generic English message. Use this to localize, match agent persona,
|
|
32
|
+
* or give the model more specific recovery instructions.
|
|
33
|
+
*/
|
|
34
|
+
sandboxNotFoundToolResponse?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
/**
|
|
10
38
|
* Extended router context with a resolved {@link Sandbox} instance.
|
|
11
39
|
*
|
|
@@ -65,13 +93,15 @@ export function withSandbox<
|
|
|
65
93
|
handler: (
|
|
66
94
|
args: TArgs,
|
|
67
95
|
context: RouterContext & { sandbox: TSandbox; sandboxId: string }
|
|
68
|
-
) => Promise<ToolHandlerResponse<TResult, TToolResponse
|
|
96
|
+
) => Promise<ToolHandlerResponse<TResult, TToolResponse>>,
|
|
97
|
+
options?: WithSandboxOptions
|
|
69
98
|
): ActivityToolHandler<
|
|
70
99
|
TArgs,
|
|
71
100
|
TResult | null,
|
|
72
101
|
RouterContext,
|
|
73
102
|
TToolResponse | string
|
|
74
103
|
> {
|
|
104
|
+
const translateSandboxNotFound = options?.translateSandboxNotFound ?? false;
|
|
75
105
|
return async (args, context) => {
|
|
76
106
|
if (!context.sandboxId) {
|
|
77
107
|
return {
|
|
@@ -79,7 +109,20 @@ export function withSandbox<
|
|
|
79
109
|
data: null,
|
|
80
110
|
};
|
|
81
111
|
}
|
|
82
|
-
|
|
112
|
+
let sandbox: TSandbox;
|
|
113
|
+
try {
|
|
114
|
+
sandbox = await manager.getSandbox(context.sandboxId);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (translateSandboxNotFound && err instanceof SandboxNotFoundError) {
|
|
117
|
+
return {
|
|
118
|
+
toolResponse:
|
|
119
|
+
options?.sandboxNotFoundToolResponse ??
|
|
120
|
+
`Error: the execution environment for the ${context.toolName} tool is no longer available, so this tool call could not be completed.`,
|
|
121
|
+
data: null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
83
126
|
return handler(args, { ...context, sandbox, sandboxId: context.sandboxId });
|
|
84
127
|
};
|
|
85
128
|
}
|
|
@@ -104,7 +104,7 @@ export class VirtualFileSystem<
|
|
|
104
104
|
if (inline !== undefined) return inline;
|
|
105
105
|
const entry = this.entries.get(norm);
|
|
106
106
|
if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
|
|
107
|
-
return this.
|
|
107
|
+
return this.readEntryContent(entry);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
async readFileBuffer(path: string): Promise<Uint8Array> {
|
|
@@ -113,9 +113,25 @@ export class VirtualFileSystem<
|
|
|
113
113
|
if (inline !== undefined) return new TextEncoder().encode(inline);
|
|
114
114
|
const entry = this.entries.get(norm);
|
|
115
115
|
if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
|
|
116
|
+
if (entry.inlineContent !== undefined) {
|
|
117
|
+
return new TextEncoder().encode(entry.inlineContent);
|
|
118
|
+
}
|
|
116
119
|
return this.resolver.readFileBuffer(entry.id, this.ctx, entry.metadata);
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the string content for an entry, preferring inline content
|
|
124
|
+
* carried on the entry itself before consulting the resolver. Used by
|
|
125
|
+
* `readFile`, `appendFile`, and `cp` so all read paths agree on the
|
|
126
|
+
* lookup precedence.
|
|
127
|
+
*/
|
|
128
|
+
private readEntryContent(entry: FileEntry<TMeta>): Promise<string> {
|
|
129
|
+
if (entry.inlineContent !== undefined) {
|
|
130
|
+
return Promise.resolve(entry.inlineContent);
|
|
131
|
+
}
|
|
132
|
+
return this.resolver.readFile(entry.id, this.ctx, entry.metadata);
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
// --------------------------------------------------------------------------
|
|
120
136
|
// Metadata operations — pure, resolved from the tree
|
|
121
137
|
// --------------------------------------------------------------------------
|
|
@@ -196,6 +212,11 @@ export class VirtualFileSystem<
|
|
|
196
212
|
const existing = this.entries.get(norm);
|
|
197
213
|
|
|
198
214
|
if (existing) {
|
|
215
|
+
if (existing.inlineContent !== undefined) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`EROFS: cannot write to inline (read-only) entry: ${path}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
199
220
|
await this.resolver.writeFile(
|
|
200
221
|
existing.id,
|
|
201
222
|
content,
|
|
@@ -230,11 +251,13 @@ export class VirtualFileSystem<
|
|
|
230
251
|
return this.writeFile(path, content);
|
|
231
252
|
}
|
|
232
253
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
254
|
+
if (existing.inlineContent !== undefined) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`EROFS: cannot append to inline (read-only) entry: ${path}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const current = await this.readEntryContent(existing);
|
|
238
261
|
const appended =
|
|
239
262
|
typeof content === "string"
|
|
240
263
|
? current + content
|
|
@@ -283,6 +306,11 @@ export class VirtualFileSystem<
|
|
|
283
306
|
const entry = this.entries.get(norm);
|
|
284
307
|
|
|
285
308
|
if (entry) {
|
|
309
|
+
if (entry.inlineContent !== undefined) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`EROFS: cannot remove inline (read-only) entry: ${path}`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
286
314
|
await this.resolver.deleteFile(entry.id, this.ctx, entry.metadata);
|
|
287
315
|
this.entries.delete(norm);
|
|
288
316
|
this.mutations.push({ type: "remove", path: norm });
|
|
@@ -296,6 +324,11 @@ export class VirtualFileSystem<
|
|
|
296
324
|
const prefix = norm === "/" ? "/" : norm + "/";
|
|
297
325
|
for (const [p, e] of this.entries) {
|
|
298
326
|
if (p.startsWith(prefix)) {
|
|
327
|
+
if (e.inlineContent !== undefined) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`EROFS: cannot remove inline (read-only) entry: ${p}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
299
332
|
await this.resolver.deleteFile(e.id, this.ctx, e.metadata);
|
|
300
333
|
this.entries.delete(p);
|
|
301
334
|
this.mutations.push({ type: "remove", path: p });
|
|
@@ -323,11 +356,7 @@ export class VirtualFileSystem<
|
|
|
323
356
|
|
|
324
357
|
const entry = this.entries.get(normSrc);
|
|
325
358
|
if (entry) {
|
|
326
|
-
const content = await this.
|
|
327
|
-
entry.id,
|
|
328
|
-
this.ctx,
|
|
329
|
-
entry.metadata
|
|
330
|
-
);
|
|
359
|
+
const content = await this.readEntryContent(entry);
|
|
331
360
|
await this.writeFile(normDest, content);
|
|
332
361
|
return;
|
|
333
362
|
}
|
|
@@ -343,11 +372,7 @@ export class VirtualFileSystem<
|
|
|
343
372
|
for (const [p, e] of this.entries) {
|
|
344
373
|
if (p.startsWith(prefix)) {
|
|
345
374
|
const relative = p.slice(normSrc.length);
|
|
346
|
-
const content = await this.
|
|
347
|
-
e.id,
|
|
348
|
-
this.ctx,
|
|
349
|
-
e.metadata
|
|
350
|
-
);
|
|
375
|
+
const content = await this.readEntryContent(e);
|
|
351
376
|
await this.writeFile(normDest + relative, content);
|
|
352
377
|
}
|
|
353
378
|
}
|
|
@@ -20,6 +20,25 @@ export interface FileEntry<TMeta = FileEntryMetadata> {
|
|
|
20
20
|
/** ISO-8601 date string (JSON-safe) */
|
|
21
21
|
mtime: string;
|
|
22
22
|
metadata: TMeta;
|
|
23
|
+
/**
|
|
24
|
+
* Optional inline content carried directly on the entry. When present the
|
|
25
|
+
* {@link VirtualFileSystem} returns this string from `readFile` /
|
|
26
|
+
* `readFileBuffer` (and uses it as the source for `cp` / `appendFile`)
|
|
27
|
+
* without consulting the resolver.
|
|
28
|
+
*
|
|
29
|
+
* Use this for files that exist purely in workflow state and have no
|
|
30
|
+
* backing in the consumer's data layer (e.g. skill resources bundled at
|
|
31
|
+
* session creation time). Because the content travels with the entry,
|
|
32
|
+
* any tool handler that constructs a `VirtualFileSystem` from `fileTree`
|
|
33
|
+
* sees the same content — no separate `inlineFiles` plumbing required.
|
|
34
|
+
*
|
|
35
|
+
* Read-only: entries with `inlineContent` reject in-place mutations
|
|
36
|
+
* (`writeFile`, `appendFile`, `rm`, `mv` of the entry, `cp` over the
|
|
37
|
+
* entry as destination). The resolver has no contract for the synthetic
|
|
38
|
+
* `id` these entries carry, so attempting a mutation throws an `EROFS`
|
|
39
|
+
* error instead of silently routing a doomed call through the resolver.
|
|
40
|
+
*/
|
|
41
|
+
inlineContent?: string;
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
// ============================================================================
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it, beforeEach } from "vitest";
|
|
1
|
+
import { describe, expect, it, beforeEach, vi } from "vitest";
|
|
2
2
|
import type { FileEntry, FileResolver } from "./types";
|
|
3
3
|
import { VirtualFileSystem } from "./filesystem";
|
|
4
4
|
import { applyVirtualTreeMutations } from "./mutations";
|
|
@@ -387,6 +387,209 @@ describe("VirtualFileSystem — inlineFiles", () => {
|
|
|
387
387
|
});
|
|
388
388
|
});
|
|
389
389
|
|
|
390
|
+
// ============================================================================
|
|
391
|
+
// VirtualFileSystem — entry.inlineContent
|
|
392
|
+
// ============================================================================
|
|
393
|
+
|
|
394
|
+
describe("VirtualFileSystem — entry.inlineContent", () => {
|
|
395
|
+
const inlineEntry: FileEntry = {
|
|
396
|
+
id: "skill:code-review:checklist.md",
|
|
397
|
+
path: "/skills/code-review/checklist.md",
|
|
398
|
+
size: 18,
|
|
399
|
+
mtime: "2025-01-01T00:00:00.000Z",
|
|
400
|
+
metadata: {},
|
|
401
|
+
inlineContent: "# Review checklist",
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
function createFsWithInlineEntry(): VirtualFileSystem<TestCtx> {
|
|
405
|
+
const { resolver } = createMockResolver();
|
|
406
|
+
return new VirtualFileSystem([...sampleTree, inlineEntry], resolver, ctx);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
it("readFile returns entry.inlineContent without hitting the resolver", async () => {
|
|
410
|
+
const { resolver, store } = createMockResolver();
|
|
411
|
+
const readFileSpy = vi.spyOn(resolver, "readFile");
|
|
412
|
+
const fs = new VirtualFileSystem(
|
|
413
|
+
[...sampleTree, inlineEntry],
|
|
414
|
+
resolver,
|
|
415
|
+
ctx
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const content = await fs.readFile("/skills/code-review/checklist.md");
|
|
419
|
+
expect(content).toBe("# Review checklist");
|
|
420
|
+
expect(readFileSpy).not.toHaveBeenCalled();
|
|
421
|
+
expect(store.size).toBeGreaterThan(0);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("readFileBuffer returns entry.inlineContent encoded as Uint8Array", async () => {
|
|
425
|
+
const fs = createFsWithInlineEntry();
|
|
426
|
+
const buf = await fs.readFileBuffer("/skills/code-review/checklist.md");
|
|
427
|
+
expect(buf).toBeInstanceOf(Uint8Array);
|
|
428
|
+
expect(new TextDecoder().decode(buf)).toBe("# Review checklist");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("entry.inlineContent files participate in directory inference", async () => {
|
|
432
|
+
const fs = createFsWithInlineEntry();
|
|
433
|
+
expect(await fs.exists("/skills")).toBe(true);
|
|
434
|
+
expect(await fs.exists("/skills/code-review")).toBe(true);
|
|
435
|
+
expect(await fs.readdir("/skills/code-review")).toContain("checklist.md");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("inlineFiles map still wins over entry.inlineContent for the same path", async () => {
|
|
439
|
+
const { resolver } = createMockResolver();
|
|
440
|
+
const fs = new VirtualFileSystem(
|
|
441
|
+
[...sampleTree, inlineEntry],
|
|
442
|
+
resolver,
|
|
443
|
+
ctx,
|
|
444
|
+
"/",
|
|
445
|
+
{ "/skills/code-review/checklist.md": "OVERRIDE" }
|
|
446
|
+
);
|
|
447
|
+
expect(await fs.readFile("/skills/code-review/checklist.md")).toBe(
|
|
448
|
+
"OVERRIDE"
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("readFile resolves entry.inlineContent for non-normalized paths (no leading slash)", async () => {
|
|
453
|
+
const fs = createFsWithInlineEntry();
|
|
454
|
+
const content = await fs.readFile("skills/code-review/checklist.md");
|
|
455
|
+
expect(content).toBe("# Review checklist");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("empty-string entry.inlineContent is served without falling through to the resolver", async () => {
|
|
459
|
+
const { resolver } = createMockResolver();
|
|
460
|
+
const readFileSpy = vi.spyOn(resolver, "readFile");
|
|
461
|
+
const fs = new VirtualFileSystem(
|
|
462
|
+
[
|
|
463
|
+
...sampleTree,
|
|
464
|
+
{
|
|
465
|
+
id: "skill:empty",
|
|
466
|
+
path: "/skills/empty.md",
|
|
467
|
+
size: 0,
|
|
468
|
+
mtime: "2025-01-01T00:00:00.000Z",
|
|
469
|
+
metadata: {},
|
|
470
|
+
inlineContent: "",
|
|
471
|
+
} satisfies FileEntry,
|
|
472
|
+
],
|
|
473
|
+
resolver,
|
|
474
|
+
ctx
|
|
475
|
+
);
|
|
476
|
+
expect(await fs.readFile("/skills/empty.md")).toBe("");
|
|
477
|
+
expect(readFileSpy).not.toHaveBeenCalled();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("stat reports entry.inlineContent files as files", async () => {
|
|
481
|
+
const fs = createFsWithInlineEntry();
|
|
482
|
+
const stat = await fs.stat("/skills/code-review/checklist.md");
|
|
483
|
+
expect(stat.isFile).toBe(true);
|
|
484
|
+
expect(stat.isDirectory).toBe(false);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("non-inline entries in the same tree still go through the resolver", async () => {
|
|
488
|
+
const { resolver } = createMockResolver();
|
|
489
|
+
const readFileSpy = vi.spyOn(resolver, "readFile");
|
|
490
|
+
const fs = new VirtualFileSystem(
|
|
491
|
+
[...sampleTree, inlineEntry],
|
|
492
|
+
resolver,
|
|
493
|
+
ctx
|
|
494
|
+
);
|
|
495
|
+
const content = await fs.readFile("/src/index.ts");
|
|
496
|
+
expect(content).toBe('console.log("hello");');
|
|
497
|
+
expect(readFileSpy).toHaveBeenCalledTimes(1);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// ============================================================================
|
|
502
|
+
// VirtualFileSystem — entry.inlineContent read-only contract
|
|
503
|
+
// ============================================================================
|
|
504
|
+
|
|
505
|
+
describe("VirtualFileSystem — entry.inlineContent read-only", () => {
|
|
506
|
+
const inlineEntry: FileEntry = {
|
|
507
|
+
id: "skill:demo:notes.md",
|
|
508
|
+
path: "/skills/demo/notes.md",
|
|
509
|
+
size: 8,
|
|
510
|
+
mtime: "2025-01-01T00:00:00.000Z",
|
|
511
|
+
metadata: {},
|
|
512
|
+
inlineContent: "original",
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
function makeFs(): {
|
|
516
|
+
fs: VirtualFileSystem<TestCtx>;
|
|
517
|
+
resolver: FileResolver<TestCtx>;
|
|
518
|
+
spies: {
|
|
519
|
+
writeFile: ReturnType<typeof vi.spyOn>;
|
|
520
|
+
deleteFile: ReturnType<typeof vi.spyOn>;
|
|
521
|
+
};
|
|
522
|
+
} {
|
|
523
|
+
const { resolver } = createMockResolver();
|
|
524
|
+
const writeFile = vi.spyOn(resolver, "writeFile");
|
|
525
|
+
const deleteFile = vi.spyOn(resolver, "deleteFile");
|
|
526
|
+
const fs = new VirtualFileSystem(
|
|
527
|
+
[...sampleTree, inlineEntry],
|
|
528
|
+
resolver,
|
|
529
|
+
ctx
|
|
530
|
+
);
|
|
531
|
+
return { fs, resolver, spies: { writeFile, deleteFile } };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
it("writeFile on inline entry throws EROFS and never calls resolver.writeFile", async () => {
|
|
535
|
+
const { fs, spies } = makeFs();
|
|
536
|
+
await expect(fs.writeFile("/skills/demo/notes.md", "new")).rejects.toThrow(
|
|
537
|
+
/EROFS/
|
|
538
|
+
);
|
|
539
|
+
expect(spies.writeFile).not.toHaveBeenCalled();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("appendFile on inline entry throws EROFS and never calls resolver.writeFile", async () => {
|
|
543
|
+
const { fs, spies } = makeFs();
|
|
544
|
+
await expect(
|
|
545
|
+
fs.appendFile("/skills/demo/notes.md", "more")
|
|
546
|
+
).rejects.toThrow(/EROFS/);
|
|
547
|
+
expect(spies.writeFile).not.toHaveBeenCalled();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("rm on inline entry throws EROFS and never calls resolver.deleteFile", async () => {
|
|
551
|
+
const { fs, spies } = makeFs();
|
|
552
|
+
await expect(fs.rm("/skills/demo/notes.md")).rejects.toThrow(/EROFS/);
|
|
553
|
+
expect(spies.deleteFile).not.toHaveBeenCalled();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("rm recursive on a directory containing an inline entry throws EROFS", async () => {
|
|
557
|
+
const { fs, spies } = makeFs();
|
|
558
|
+
await expect(fs.rm("/skills", { recursive: true })).rejects.toThrow(
|
|
559
|
+
/EROFS/
|
|
560
|
+
);
|
|
561
|
+
expect(spies.deleteFile).not.toHaveBeenCalled();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("mv of an inline entry throws EROFS (rm step rejects after copy)", async () => {
|
|
565
|
+
const { fs } = makeFs();
|
|
566
|
+
await expect(fs.mv("/skills/demo/notes.md", "/copy.md")).rejects.toThrow(
|
|
567
|
+
/EROFS/
|
|
568
|
+
);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("cp from inline source to a fresh destination uses inlineContent and creates via resolver", async () => {
|
|
572
|
+
const { fs } = makeFs();
|
|
573
|
+
await fs.cp("/skills/demo/notes.md", "/copied.md");
|
|
574
|
+
expect(await fs.readFile("/copied.md")).toBe("original");
|
|
575
|
+
expect(await fs.exists("/copied.md")).toBe(true);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("appendFile to a missing path under an inline-only directory delegates to writeFile", async () => {
|
|
579
|
+
const { fs } = makeFs();
|
|
580
|
+
await fs.appendFile("/skills/demo/new.md", "hello");
|
|
581
|
+
expect(await fs.readFile("/skills/demo/new.md")).toBe("hello");
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("cp over an inline destination throws EROFS", async () => {
|
|
585
|
+
const { fs, spies } = makeFs();
|
|
586
|
+
await expect(
|
|
587
|
+
fs.cp("/src/index.ts", "/skills/demo/notes.md")
|
|
588
|
+
).rejects.toThrow(/EROFS/);
|
|
589
|
+
expect(spies.writeFile).not.toHaveBeenCalled();
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
390
593
|
// ============================================================================
|
|
391
594
|
// createVirtualFsActivities
|
|
392
595
|
// ============================================================================
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { readFileHandler } from "./handler";
|
|
3
|
+
import { VirtualFileSystem } from "../../lib/virtual-fs/filesystem";
|
|
4
|
+
import type { FileEntry, FileResolver } from "../../lib/virtual-fs/types";
|
|
5
|
+
|
|
6
|
+
interface TestCtx {
|
|
7
|
+
projectId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createNoopResolver(): FileResolver<TestCtx> {
|
|
11
|
+
return {
|
|
12
|
+
resolveEntries: async () => [],
|
|
13
|
+
readFile: vi.fn(async () => {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"resolver.readFile should not be called for inline entries"
|
|
16
|
+
);
|
|
17
|
+
}),
|
|
18
|
+
readFileBuffer: vi.fn(async () => {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"resolver.readFileBuffer should not be called for inline entries"
|
|
21
|
+
);
|
|
22
|
+
}),
|
|
23
|
+
writeFile: vi.fn(async () => {}),
|
|
24
|
+
createFile: vi.fn(async () => {
|
|
25
|
+
throw new Error("not implemented");
|
|
26
|
+
}),
|
|
27
|
+
deleteFile: vi.fn(async () => {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ctx: TestCtx = { projectId: "p" };
|
|
32
|
+
|
|
33
|
+
const skillEntry: FileEntry = {
|
|
34
|
+
id: "skill:demo:notes.md",
|
|
35
|
+
path: "/skills/demo/notes.md",
|
|
36
|
+
size: 9,
|
|
37
|
+
mtime: "2025-01-01T00:00:00.000Z",
|
|
38
|
+
metadata: {},
|
|
39
|
+
inlineContent: "# notes\n",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe("readFileHandler — entry.inlineContent (skill resources)", () => {
|
|
43
|
+
it("returns the inline content even when the resolver has no backing", async () => {
|
|
44
|
+
const resolver = createNoopResolver();
|
|
45
|
+
const fs = new VirtualFileSystem([skillEntry], resolver, ctx);
|
|
46
|
+
|
|
47
|
+
const response = await readFileHandler(
|
|
48
|
+
{ path: "/skills/demo/notes.md" },
|
|
49
|
+
{
|
|
50
|
+
threadId: "t",
|
|
51
|
+
toolCallId: "tc",
|
|
52
|
+
toolName: "FileRead",
|
|
53
|
+
// The handler signature requires SandboxContext, which expects a
|
|
54
|
+
// SandboxFileSystem — VirtualFileSystem implements that interface.
|
|
55
|
+
sandbox: { fs } as never,
|
|
56
|
+
sandboxId: "ignored",
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(typeof response.toolResponse).toBe("string");
|
|
61
|
+
expect(response.toolResponse).toContain("# notes");
|
|
62
|
+
expect(response.data).not.toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("works with non-normalized agent-supplied paths (no leading slash)", async () => {
|
|
66
|
+
const resolver = createNoopResolver();
|
|
67
|
+
const fs = new VirtualFileSystem([skillEntry], resolver, ctx);
|
|
68
|
+
|
|
69
|
+
const response = await readFileHandler(
|
|
70
|
+
{ path: "skills/demo/notes.md" },
|
|
71
|
+
{
|
|
72
|
+
threadId: "t",
|
|
73
|
+
toolCallId: "tc",
|
|
74
|
+
toolName: "FileRead",
|
|
75
|
+
sandbox: { fs } as never,
|
|
76
|
+
sandboxId: "ignored",
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(response.toolResponse).toContain("# notes");
|
|
81
|
+
expect(response.data).not.toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|