zeitlich 0.2.23 → 0.2.25
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 +44 -8
- package/dist/adapters/sandbox/bedrock/index.cjs +452 -0
- package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -0
- package/dist/adapters/sandbox/bedrock/index.d.cts +23 -0
- package/dist/adapters/sandbox/bedrock/index.d.ts +23 -0
- package/dist/adapters/sandbox/bedrock/index.js +449 -0
- package/dist/adapters/sandbox/bedrock/index.js.map +1 -0
- package/dist/adapters/sandbox/bedrock/workflow.cjs +33 -0
- package/dist/adapters/sandbox/bedrock/workflow.cjs.map +1 -0
- package/dist/adapters/sandbox/bedrock/workflow.d.cts +29 -0
- package/dist/adapters/sandbox/bedrock/workflow.d.ts +29 -0
- package/dist/adapters/sandbox/bedrock/workflow.js +31 -0
- package/dist/adapters/sandbox/bedrock/workflow.js.map +1 -0
- package/dist/adapters/sandbox/virtual/index.cjs +12 -2
- package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
- package/dist/adapters/sandbox/virtual/index.d.cts +4 -4
- package/dist/adapters/sandbox/virtual/index.d.ts +4 -4
- package/dist/adapters/sandbox/virtual/index.js +12 -2
- package/dist/adapters/sandbox/virtual/index.js.map +1 -1
- package/dist/adapters/sandbox/virtual/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/virtual/workflow.d.ts +2 -2
- package/dist/adapters/thread/google-genai/index.d.cts +2 -2
- package/dist/adapters/thread/google-genai/index.d.ts +2 -2
- package/dist/adapters/thread/google-genai/workflow.d.cts +2 -2
- package/dist/adapters/thread/google-genai/workflow.d.ts +2 -2
- package/dist/adapters/thread/langchain/index.d.cts +2 -2
- package/dist/adapters/thread/langchain/index.d.ts +2 -2
- package/dist/adapters/thread/langchain/workflow.d.cts +2 -2
- package/dist/adapters/thread/langchain/workflow.d.ts +2 -2
- package/dist/index.cjs +202 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -10
- package/dist/index.d.ts +63 -10
- package/dist/index.js +203 -21
- package/dist/index.js.map +1 -1
- package/dist/{queries-DModcWRy.d.cts → queries-BYGBImeC.d.cts} +1 -1
- package/dist/{queries-byD0jr1Y.d.ts → queries-DwBe2CAA.d.ts} +1 -1
- package/dist/{types-DQW8l7pY.d.cts → types-7PeMi1bD.d.cts} +9 -2
- package/dist/types-BdCdR41N.d.ts +74 -0
- package/dist/{types-BuXdFhaZ.d.cts → types-Bf8KV0Ci.d.cts} +1 -1
- package/dist/{types-Bll19FZJ.d.ts → types-D_igp10o.d.cts} +4 -0
- package/dist/{types-Bll19FZJ.d.cts → types-D_igp10o.d.ts} +4 -0
- package/dist/{types-GZ76HZSj.d.ts → types-LVKmCNds.d.ts} +1 -1
- package/dist/types-ZHs2v9Ap.d.cts +74 -0
- package/dist/{types-B50pBPEV.d.ts → types-hmferhc2.d.ts} +9 -2
- package/dist/workflow.cjs +73 -11
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +12 -7
- package/dist/workflow.d.ts +12 -7
- package/dist/workflow.js +73 -11
- package/dist/workflow.js.map +1 -1
- package/package.json +26 -1
- package/src/adapters/sandbox/bedrock/filesystem.ts +346 -0
- package/src/adapters/sandbox/bedrock/index.ts +259 -0
- package/src/adapters/sandbox/bedrock/proxy.ts +56 -0
- package/src/adapters/sandbox/bedrock/types.ts +24 -0
- package/src/adapters/sandbox/virtual/filesystem.ts +5 -3
- package/src/adapters/sandbox/virtual/provider.ts +9 -0
- package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +26 -0
- package/src/index.ts +2 -1
- package/src/lib/lifecycle.ts +1 -1
- package/src/lib/sandbox/node-fs.ts +115 -0
- package/src/lib/session/session.integration.test.ts +97 -0
- package/src/lib/session/session.ts +33 -2
- package/src/lib/session/types.ts +1 -1
- package/src/lib/skills/fs-provider.ts +65 -4
- package/src/lib/skills/handler.ts +43 -1
- package/src/lib/skills/index.ts +0 -1
- package/src/lib/skills/register.ts +17 -1
- package/src/lib/skills/skills.integration.test.ts +308 -24
- package/src/lib/skills/types.ts +6 -0
- package/src/lib/subagent/handler.ts +10 -4
- package/src/lib/subagent/subagent.integration.test.ts +36 -4
- package/src/lib/tool-router/router.ts +6 -3
- package/src/lib/tool-router/types.ts +4 -0
- package/tsup.config.ts +3 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow-safe proxy for Bedrock sandbox operations.
|
|
3
|
+
*
|
|
4
|
+
* Uses longer timeouts than in-memory providers since Bedrock
|
|
5
|
+
* sandboxes are remote and creation involves provisioning.
|
|
6
|
+
*
|
|
7
|
+
* Import this from `zeitlich/adapters/sandbox/bedrock/workflow`
|
|
8
|
+
* in your Temporal workflow files.
|
|
9
|
+
*
|
|
10
|
+
* By default the scope is derived from `workflowInfo().workflowType`,
|
|
11
|
+
* so activities are automatically namespaced per workflow.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { proxyBedrockSandboxOps } from 'zeitlich/adapters/sandbox/bedrock/workflow';
|
|
16
|
+
*
|
|
17
|
+
* const sandbox = proxyBedrockSandboxOps();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import { proxyActivities, workflowInfo } from "@temporalio/workflow";
|
|
21
|
+
import type { SandboxOps } from "../../../lib/sandbox/types";
|
|
22
|
+
import type { BedrockSandboxCreateOptions } from "./types";
|
|
23
|
+
|
|
24
|
+
const ADAPTER_PREFIX = "bedrock";
|
|
25
|
+
|
|
26
|
+
export function proxyBedrockSandboxOps(
|
|
27
|
+
scope?: string,
|
|
28
|
+
options?: Parameters<typeof proxyActivities>[0]
|
|
29
|
+
): SandboxOps<BedrockSandboxCreateOptions> {
|
|
30
|
+
const resolvedScope = scope ?? workflowInfo().workflowType;
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const acts = proxyActivities<Record<string, (...args: any[]) => any>>(
|
|
34
|
+
options ?? {
|
|
35
|
+
startToCloseTimeout: "120s",
|
|
36
|
+
retry: {
|
|
37
|
+
maximumAttempts: 3,
|
|
38
|
+
initialInterval: "5s",
|
|
39
|
+
maximumInterval: "60s",
|
|
40
|
+
backoffCoefficient: 3,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const prefix = `${ADAPTER_PREFIX}${resolvedScope.charAt(0).toUpperCase()}${resolvedScope.slice(1)}`;
|
|
46
|
+
const p = (key: string): string =>
|
|
47
|
+
`${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
createSandbox: acts[p("createSandbox")],
|
|
51
|
+
destroySandbox: acts[p("destroySandbox")],
|
|
52
|
+
pauseSandbox: acts[p("pauseSandbox")],
|
|
53
|
+
snapshotSandbox: acts[p("snapshotSandbox")],
|
|
54
|
+
forkSandbox: acts[p("forkSandbox")],
|
|
55
|
+
} as SandboxOps<BedrockSandboxCreateOptions>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Sandbox, SandboxCreateOptions } from "../../../lib/sandbox/types";
|
|
2
|
+
import type { BedrockSandboxFileSystem } from "./filesystem";
|
|
3
|
+
import type { BedrockAgentCoreClientConfig } from "@aws-sdk/client-bedrock-agentcore";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A Bedrock-backed {@link Sandbox} with its typed filesystem.
|
|
7
|
+
*/
|
|
8
|
+
export type BedrockSandbox = Sandbox & { fs: BedrockSandboxFileSystem };
|
|
9
|
+
|
|
10
|
+
export interface BedrockSandboxConfig {
|
|
11
|
+
/** ARN or name of the Code Interpreter resource. */
|
|
12
|
+
codeInterpreterIdentifier: string;
|
|
13
|
+
/** AWS SDK client configuration (region, credentials, etc.). */
|
|
14
|
+
clientConfig?: BedrockAgentCoreClientConfig;
|
|
15
|
+
/** Default base path for resolving relative filesystem paths. */
|
|
16
|
+
workspaceBase?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BedrockSandboxCreateOptions extends SandboxCreateOptions {
|
|
20
|
+
/** Session name (human-readable, does not need to be unique). */
|
|
21
|
+
name?: string;
|
|
22
|
+
/** Session timeout in seconds. Default 900 (15 min). Max 28 800 (8 h). */
|
|
23
|
+
sessionTimeoutSeconds?: number;
|
|
24
|
+
}
|
|
@@ -64,7 +64,7 @@ export class VirtualSandboxFileSystem<
|
|
|
64
64
|
tree: FileEntry<TMeta>[],
|
|
65
65
|
private resolver: FileResolver<TCtx, TMeta>,
|
|
66
66
|
private ctx: TCtx,
|
|
67
|
-
workspaceBase = "/"
|
|
67
|
+
workspaceBase = "/",
|
|
68
68
|
) {
|
|
69
69
|
this.workspaceBase = normalisePath(workspaceBase);
|
|
70
70
|
this.entries = new Map(
|
|
@@ -88,13 +88,15 @@ export class VirtualSandboxFileSystem<
|
|
|
88
88
|
// --------------------------------------------------------------------------
|
|
89
89
|
|
|
90
90
|
async readFile(path: string): Promise<string> {
|
|
91
|
-
const
|
|
91
|
+
const norm = normalisePath(path, this.workspaceBase);
|
|
92
|
+
const entry = this.entries.get(norm);
|
|
92
93
|
if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
|
|
93
94
|
return this.resolver.readFile(entry.id, this.ctx, entry.metadata);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
async readFileBuffer(path: string): Promise<Uint8Array> {
|
|
97
|
-
const
|
|
98
|
+
const norm = normalisePath(path, this.workspaceBase);
|
|
99
|
+
const entry = this.entries.get(norm);
|
|
98
100
|
if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
|
|
99
101
|
return this.resolver.readFileBuffer(entry.id, this.ctx, entry.metadata);
|
|
100
102
|
}
|
|
@@ -69,6 +69,15 @@ export class VirtualSandboxProvider<
|
|
|
69
69
|
workspaceBase,
|
|
70
70
|
);
|
|
71
71
|
|
|
72
|
+
if (options.initialFiles) {
|
|
73
|
+
for (const [path, content] of Object.entries(options.initialFiles)) {
|
|
74
|
+
await sandbox.fs.writeFile(path, content);
|
|
75
|
+
}
|
|
76
|
+
for (const m of sandbox.fs.getMutations()) {
|
|
77
|
+
if (m.type === "add") fileTree.push(m.entry);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
return {
|
|
73
82
|
sandbox,
|
|
74
83
|
stateUpdate: {
|
|
@@ -428,6 +428,32 @@ describe("VirtualSandboxProvider", () => {
|
|
|
428
428
|
"requires resolverContext",
|
|
429
429
|
);
|
|
430
430
|
});
|
|
431
|
+
|
|
432
|
+
it("create seeds initialFiles via writeFile into sandbox and fileTree", async () => {
|
|
433
|
+
const { resolver } = createMockResolver();
|
|
434
|
+
const provider = new VirtualSandboxProvider(resolver);
|
|
435
|
+
const { sandbox, stateUpdate } = await provider.create({
|
|
436
|
+
resolverContext: ctx,
|
|
437
|
+
initialFiles: {
|
|
438
|
+
"/skills/my-skill/SKILL.md": "---\nname: my-skill\n---\nDo things.",
|
|
439
|
+
"/skills/my-skill/references/guide.md": "# Guide\nStep 1...",
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(await sandbox.fs.readFile("/skills/my-skill/SKILL.md")).toBe(
|
|
444
|
+
"---\nname: my-skill\n---\nDo things.",
|
|
445
|
+
);
|
|
446
|
+
expect(await sandbox.fs.readFile("/skills/my-skill/references/guide.md")).toBe(
|
|
447
|
+
"# Guide\nStep 1...",
|
|
448
|
+
);
|
|
449
|
+
expect(await sandbox.fs.exists("/skills/my-skill")).toBe(true);
|
|
450
|
+
expect(await sandbox.fs.exists("/skills/my-skill/references")).toBe(true);
|
|
451
|
+
|
|
452
|
+
const tree = stateUpdate?.fileTree as FileEntry[];
|
|
453
|
+
expect(tree.find((e) => e.path === "/skills/my-skill/SKILL.md")).toBeDefined();
|
|
454
|
+
expect(tree.find((e) => e.path === "/skills/my-skill/references/guide.md")).toBeDefined();
|
|
455
|
+
expect(stateUpdate).not.toHaveProperty("localFiles");
|
|
456
|
+
});
|
|
431
457
|
});
|
|
432
458
|
|
|
433
459
|
// ============================================================================
|
package/src/index.ts
CHANGED
|
@@ -51,8 +51,9 @@ export {
|
|
|
51
51
|
} from "./lib/activity";
|
|
52
52
|
export type { AgentStateContext } from "./lib/activity";
|
|
53
53
|
|
|
54
|
-
// Sandbox (activity-side: manager)
|
|
54
|
+
// Sandbox (activity-side: manager + Node.js filesystem adapter)
|
|
55
55
|
export { SandboxManager } from "./lib/sandbox/manager";
|
|
56
|
+
export { NodeFsSandboxFileSystem } from "./lib/sandbox/node-fs";
|
|
56
57
|
|
|
57
58
|
// Tool handlers (activity implementations)
|
|
58
59
|
// Wrap sandbox handlers with withSandbox(manager, handler) at registration time
|
package/src/lib/lifecycle.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type SandboxInit =
|
|
|
34
34
|
| { mode: "new" }
|
|
35
35
|
| { mode: "continue"; sandboxId: string }
|
|
36
36
|
| { mode: "fork"; sandboxId: string }
|
|
37
|
-
| { mode: "inherit"; sandboxId: string };
|
|
37
|
+
| { mode: "inherit"; sandboxId: string; stateUpdate?: Record<string, unknown> };
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* What to do with the sandbox when the session exits.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { promises as fsp } from "node:fs";
|
|
2
|
+
import { resolve, posix } from "node:path";
|
|
3
|
+
import type { SandboxFileSystem, DirentEntry, FileStat } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thin adapter from Node.js `fs` to {@link SandboxFileSystem}.
|
|
7
|
+
*
|
|
8
|
+
* All paths are resolved relative to {@link workspaceBase} using
|
|
9
|
+
* `node:path.resolve` (OS-native). Useful for loading skills from the
|
|
10
|
+
* worker's local disk inside a Temporal activity.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { NodeFsSandboxFileSystem, FileSystemSkillProvider } from 'zeitlich';
|
|
15
|
+
*
|
|
16
|
+
* const fs = new NodeFsSandboxFileSystem('/path/to/skills-root');
|
|
17
|
+
* const provider = new FileSystemSkillProvider(fs, '/');
|
|
18
|
+
* const skills = await provider.loadAll();
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class NodeFsSandboxFileSystem implements SandboxFileSystem {
|
|
22
|
+
readonly workspaceBase: string;
|
|
23
|
+
|
|
24
|
+
constructor(workspaceBase: string) {
|
|
25
|
+
this.workspaceBase = workspaceBase;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private abs(path: string): string {
|
|
29
|
+
return resolve(this.workspaceBase, path);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async readFile(path: string): Promise<string> {
|
|
33
|
+
return fsp.readFile(this.abs(path), "utf-8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async readFileBuffer(path: string): Promise<Uint8Array> {
|
|
37
|
+
return fsp.readFile(this.abs(path));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async writeFile(path: string, content: string | Uint8Array): Promise<void> {
|
|
41
|
+
await fsp.writeFile(this.abs(path), content);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async appendFile(path: string, content: string | Uint8Array): Promise<void> {
|
|
45
|
+
await fsp.appendFile(this.abs(path), content);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async exists(path: string): Promise<boolean> {
|
|
49
|
+
try {
|
|
50
|
+
await fsp.access(this.abs(path));
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async stat(path: string): Promise<FileStat> {
|
|
58
|
+
const s = await fsp.stat(this.abs(path));
|
|
59
|
+
return {
|
|
60
|
+
isFile: s.isFile(),
|
|
61
|
+
isDirectory: s.isDirectory(),
|
|
62
|
+
isSymbolicLink: s.isSymbolicLink(),
|
|
63
|
+
size: s.size,
|
|
64
|
+
mtime: s.mtime,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async mkdir(
|
|
69
|
+
path: string,
|
|
70
|
+
options?: { recursive?: boolean },
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
await fsp.mkdir(this.abs(path), options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async readdir(path: string): Promise<string[]> {
|
|
76
|
+
return fsp.readdir(this.abs(path));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async readdirWithFileTypes(path: string): Promise<DirentEntry[]> {
|
|
80
|
+
const entries = await fsp.readdir(this.abs(path), { withFileTypes: true });
|
|
81
|
+
return entries.map((e) => ({
|
|
82
|
+
name: e.name,
|
|
83
|
+
isFile: e.isFile(),
|
|
84
|
+
isDirectory: e.isDirectory(),
|
|
85
|
+
isSymbolicLink: e.isSymbolicLink(),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async rm(
|
|
90
|
+
path: string,
|
|
91
|
+
options?: { recursive?: boolean; force?: boolean },
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
await fsp.rm(this.abs(path), options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async cp(
|
|
97
|
+
src: string,
|
|
98
|
+
dest: string,
|
|
99
|
+
options?: { recursive?: boolean },
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
await fsp.cp(this.abs(src), this.abs(dest), options);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async mv(src: string, dest: string): Promise<void> {
|
|
105
|
+
await fsp.rename(this.abs(src), this.abs(dest));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async readlink(path: string): Promise<string> {
|
|
109
|
+
return fsp.readlink(this.abs(path));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
resolvePath(base: string, path: string): string {
|
|
113
|
+
return posix.resolve(base, path);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -829,6 +829,103 @@ describe("createSession integration", () => {
|
|
|
829
829
|
expect(stateManager.get("customField")).toBe("from-sandbox");
|
|
830
830
|
});
|
|
831
831
|
|
|
832
|
+
// --- Skill resourceContents seeded as initialFiles ---
|
|
833
|
+
|
|
834
|
+
it("passes skill resourceContents as initialFiles to createSandbox", async () => {
|
|
835
|
+
const { ops } = createMockThreadOps();
|
|
836
|
+
let capturedOptions: Record<string, unknown> | undefined;
|
|
837
|
+
|
|
838
|
+
const sandboxOps: SandboxOps = {
|
|
839
|
+
createSandbox: async (options) => {
|
|
840
|
+
capturedOptions = options as Record<string, unknown>;
|
|
841
|
+
return { sandboxId: "sb-skill" };
|
|
842
|
+
},
|
|
843
|
+
destroySandbox: async () => {},
|
|
844
|
+
snapshotSandbox: async () => ({
|
|
845
|
+
sandboxId: "sb-skill",
|
|
846
|
+
providerId: "test",
|
|
847
|
+
data: null,
|
|
848
|
+
createdAt: new Date().toISOString(),
|
|
849
|
+
}),
|
|
850
|
+
forkSandbox: async () => "forked-sandbox-id",
|
|
851
|
+
pauseSandbox: async () => {},
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const session = await createSession({
|
|
855
|
+
agentName: "TestAgent",
|
|
856
|
+
thread: { mode: "new", threadId: "thread-1" },
|
|
857
|
+
runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
|
|
858
|
+
threadOps: ops,
|
|
859
|
+
buildContextMessage: () => "go",
|
|
860
|
+
sandboxOps,
|
|
861
|
+
skills: [
|
|
862
|
+
{
|
|
863
|
+
name: "test-skill",
|
|
864
|
+
description: "Test",
|
|
865
|
+
instructions: "Do test",
|
|
866
|
+
location: "/skills/test-skill",
|
|
867
|
+
resourceContents: { "references/guide.md": "# Guide content" },
|
|
868
|
+
},
|
|
869
|
+
],
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
const stateManager = createAgentStateManager({
|
|
873
|
+
initialState: { systemPrompt: "test" },
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
await session.runSession({ stateManager });
|
|
877
|
+
|
|
878
|
+
expect(capturedOptions).toBeDefined();
|
|
879
|
+
expect(capturedOptions?.initialFiles).toEqual({
|
|
880
|
+
"/skills/test-skill/references/guide.md": "# Guide content",
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it("does not pass initialFiles when skills have no resourceContents", async () => {
|
|
885
|
+
const { ops } = createMockThreadOps();
|
|
886
|
+
let capturedOptions: Record<string, unknown> | undefined;
|
|
887
|
+
|
|
888
|
+
const sandboxOps: SandboxOps = {
|
|
889
|
+
createSandbox: async (options) => {
|
|
890
|
+
capturedOptions = options as Record<string, unknown>;
|
|
891
|
+
return { sandboxId: "sb-no-rc" };
|
|
892
|
+
},
|
|
893
|
+
destroySandbox: async () => {},
|
|
894
|
+
snapshotSandbox: async () => ({
|
|
895
|
+
sandboxId: "sb-no-rc",
|
|
896
|
+
providerId: "test",
|
|
897
|
+
data: null,
|
|
898
|
+
createdAt: new Date().toISOString(),
|
|
899
|
+
}),
|
|
900
|
+
forkSandbox: async () => "forked-sandbox-id",
|
|
901
|
+
pauseSandbox: async () => {},
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
const session = await createSession({
|
|
905
|
+
agentName: "TestAgent",
|
|
906
|
+
thread: { mode: "new", threadId: "thread-1" },
|
|
907
|
+
runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
|
|
908
|
+
threadOps: ops,
|
|
909
|
+
buildContextMessage: () => "go",
|
|
910
|
+
sandboxOps,
|
|
911
|
+
skills: [
|
|
912
|
+
{
|
|
913
|
+
name: "test-skill",
|
|
914
|
+
description: "Test",
|
|
915
|
+
instructions: "Do test",
|
|
916
|
+
},
|
|
917
|
+
],
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
const stateManager = createAgentStateManager({
|
|
921
|
+
initialState: { systemPrompt: "test" },
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
await session.runSession({ stateManager });
|
|
925
|
+
|
|
926
|
+
expect(capturedOptions).toBeUndefined();
|
|
927
|
+
});
|
|
928
|
+
|
|
832
929
|
// --- Tool usage tracking from tool results ---
|
|
833
930
|
|
|
834
931
|
it("accumulates usage from tool call results", async () => {
|
|
@@ -13,8 +13,28 @@ import type { ParsedToolCallUnion, ToolMap } from "../tool-router/types";
|
|
|
13
13
|
import { getShortId } from "../thread/id";
|
|
14
14
|
import { buildSubagentRegistration } from "../subagent/register";
|
|
15
15
|
import { buildSkillRegistration } from "../skills/register";
|
|
16
|
+
import type { Skill } from "../skills/types";
|
|
16
17
|
import { uuid4 } from "@temporalio/workflow";
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Collects resource file contents from all skills into a flat map
|
|
21
|
+
* keyed by absolute path (location + relative resource path).
|
|
22
|
+
* Returns undefined when no skills carry resource contents.
|
|
23
|
+
*/
|
|
24
|
+
function collectSkillFiles(
|
|
25
|
+
skills: Skill[],
|
|
26
|
+
): Record<string, string> | undefined {
|
|
27
|
+
let files: Record<string, string> | undefined;
|
|
28
|
+
for (const skill of skills) {
|
|
29
|
+
if (!skill.resourceContents || !skill.location) continue;
|
|
30
|
+
for (const [relPath, content] of Object.entries(skill.resourceContents)) {
|
|
31
|
+
files ??= {};
|
|
32
|
+
files[`${skill.location}/${relPath}`] = content;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/**
|
|
19
39
|
* Creates an agent session that manages the agent loop: LLM invocation,
|
|
20
40
|
* tool routing, subagent coordination, and lifecycle hooks.
|
|
@@ -169,9 +189,15 @@ export async function createSession<T extends ToolMap, M = unknown>({
|
|
|
169
189
|
const sandboxMode = sandboxInit?.mode;
|
|
170
190
|
let sandboxId: string | undefined;
|
|
171
191
|
let sandboxOwned = false;
|
|
192
|
+
let sandboxStateUpdate: Record<string, unknown> | undefined;
|
|
172
193
|
|
|
173
194
|
if (sandboxMode === "inherit") {
|
|
174
|
-
|
|
195
|
+
const inheritInit = sandboxInit as { mode: "inherit"; sandboxId: string; stateUpdate?: Record<string, unknown> };
|
|
196
|
+
sandboxId = inheritInit.sandboxId;
|
|
197
|
+
if (inheritInit.stateUpdate) {
|
|
198
|
+
sandboxStateUpdate = inheritInit.stateUpdate;
|
|
199
|
+
stateManager.mergeUpdate(inheritInit.stateUpdate as Partial<TState>);
|
|
200
|
+
}
|
|
175
201
|
if (!sandboxOps) {
|
|
176
202
|
throw ApplicationFailure.create({
|
|
177
203
|
message: "sandboxId provided but no sandboxOps — cannot manage sandbox lifecycle",
|
|
@@ -199,10 +225,14 @@ export async function createSession<T extends ToolMap, M = unknown>({
|
|
|
199
225
|
);
|
|
200
226
|
sandboxOwned = true;
|
|
201
227
|
} else if (sandboxOps) {
|
|
202
|
-
const
|
|
228
|
+
const skillFiles = skills ? collectSkillFiles(skills) : undefined;
|
|
229
|
+
const result = await sandboxOps.createSandbox(
|
|
230
|
+
skillFiles ? { initialFiles: skillFiles } : undefined,
|
|
231
|
+
);
|
|
203
232
|
sandboxId = result.sandboxId;
|
|
204
233
|
sandboxOwned = true;
|
|
205
234
|
if (result.stateUpdate) {
|
|
235
|
+
sandboxStateUpdate = result.stateUpdate;
|
|
206
236
|
stateManager.mergeUpdate(result.stateUpdate as Partial<TState>);
|
|
207
237
|
}
|
|
208
238
|
}
|
|
@@ -293,6 +323,7 @@ export async function createSession<T extends ToolMap, M = unknown>({
|
|
|
293
323
|
{
|
|
294
324
|
turn: currentTurn,
|
|
295
325
|
...(sandboxId !== undefined && { sandboxId }),
|
|
326
|
+
...(sandboxStateUpdate && { sandboxStateUpdate }),
|
|
296
327
|
}
|
|
297
328
|
);
|
|
298
329
|
|
package/src/lib/session/types.ts
CHANGED
|
@@ -94,7 +94,7 @@ export interface SessionConfig<T extends ToolMap, M = unknown> {
|
|
|
94
94
|
tools?: T;
|
|
95
95
|
/** Subagent configurations */
|
|
96
96
|
subagents?: SubagentConfig[];
|
|
97
|
-
/** Skills available to this agent (metadata + instructions, loaded
|
|
97
|
+
/** Skills available to this agent (metadata + instructions, loaded before session creation) */
|
|
98
98
|
skills?: Skill[];
|
|
99
99
|
/** Session lifecycle hooks */
|
|
100
100
|
hooks?: Hooks<T, ToolCallResultUnion<InferToolResults<T>>>;
|
|
@@ -11,7 +11,11 @@ import { parseSkillFile } from "./parse";
|
|
|
11
11
|
* ├── code-review/
|
|
12
12
|
* │ └── SKILL.md
|
|
13
13
|
* ├── pdf-processing/
|
|
14
|
-
* │
|
|
14
|
+
* │ ├── SKILL.md
|
|
15
|
+
* │ ├── references/
|
|
16
|
+
* │ │ └── spec-summary.md
|
|
17
|
+
* │ └── scripts/
|
|
18
|
+
* │ └── extract.py
|
|
15
19
|
* ```
|
|
16
20
|
*
|
|
17
21
|
* Uses the sandbox filesystem abstraction — works with any backend
|
|
@@ -30,7 +34,11 @@ export class FileSystemSkillProvider implements SkillProvider {
|
|
|
30
34
|
for (const dir of dirs) {
|
|
31
35
|
const raw = await this.fs.readFile(join(this.baseDir, dir, "SKILL.md"));
|
|
32
36
|
const { frontmatter } = parseSkillFile(raw);
|
|
33
|
-
|
|
37
|
+
const location = join(this.baseDir, dir);
|
|
38
|
+
skills.push({
|
|
39
|
+
...frontmatter,
|
|
40
|
+
location,
|
|
41
|
+
});
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
return skills;
|
|
@@ -48,7 +56,15 @@ export class FileSystemSkillProvider implements SkillProvider {
|
|
|
48
56
|
);
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
const location = join(this.baseDir, name);
|
|
60
|
+
const resourcePaths = await this.discoverResources(name);
|
|
61
|
+
const resourceContents = await this.readResourceContents(location, resourcePaths);
|
|
62
|
+
return {
|
|
63
|
+
...frontmatter,
|
|
64
|
+
instructions: body,
|
|
65
|
+
location,
|
|
66
|
+
...(resourceContents && { resourceContents }),
|
|
67
|
+
};
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
/**
|
|
@@ -62,12 +78,57 @@ export class FileSystemSkillProvider implements SkillProvider {
|
|
|
62
78
|
for (const dir of dirs) {
|
|
63
79
|
const raw = await this.fs.readFile(join(this.baseDir, dir, "SKILL.md"));
|
|
64
80
|
const { frontmatter, body } = parseSkillFile(raw);
|
|
65
|
-
|
|
81
|
+
const location = join(this.baseDir, dir);
|
|
82
|
+
const resourcePaths = await this.discoverResources(dir);
|
|
83
|
+
const resourceContents = await this.readResourceContents(location, resourcePaths);
|
|
84
|
+
skills.push({
|
|
85
|
+
...frontmatter,
|
|
86
|
+
instructions: body,
|
|
87
|
+
location,
|
|
88
|
+
...(resourceContents && { resourceContents }),
|
|
89
|
+
});
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
return skills;
|
|
69
93
|
}
|
|
70
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Recursively discovers all non-SKILL.md files inside the skill directory
|
|
97
|
+
* and returns their paths relative to the skill root.
|
|
98
|
+
*/
|
|
99
|
+
private async discoverResources(skillDir: string): Promise<string[]> {
|
|
100
|
+
const skillRoot = join(this.baseDir, skillDir);
|
|
101
|
+
const resources: string[] = [];
|
|
102
|
+
|
|
103
|
+
const walk = async (dir: string, prefix: string): Promise<void> => {
|
|
104
|
+
const entries = await this.fs.readdirWithFileTypes(dir);
|
|
105
|
+
for (const e of entries) {
|
|
106
|
+
if (e.name.startsWith(".")) continue;
|
|
107
|
+
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
108
|
+
if (e.isDirectory) {
|
|
109
|
+
await walk(join(dir, e.name), relPath);
|
|
110
|
+
} else if (e.isFile && e.name !== "SKILL.md") {
|
|
111
|
+
resources.push(relPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
await walk(skillRoot, "");
|
|
117
|
+
return resources;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async readResourceContents(
|
|
121
|
+
location: string,
|
|
122
|
+
resources: string[],
|
|
123
|
+
): Promise<Record<string, string> | undefined> {
|
|
124
|
+
if (resources.length === 0) return undefined;
|
|
125
|
+
const contents: Record<string, string> = {};
|
|
126
|
+
for (const r of resources) {
|
|
127
|
+
contents[r] = await this.fs.readFile(join(location, r));
|
|
128
|
+
}
|
|
129
|
+
return contents;
|
|
130
|
+
}
|
|
131
|
+
|
|
71
132
|
private async discoverSkillDirs(): Promise<string[]> {
|
|
72
133
|
const entries = await this.fs.readdirWithFileTypes(this.baseDir);
|
|
73
134
|
const dirs: string[] = [];
|
|
@@ -2,9 +2,51 @@ import type { Skill } from "./types";
|
|
|
2
2
|
import type { ToolHandlerResponse } from "../tool-router";
|
|
3
3
|
import type { ReadSkillArgs } from "./tool";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Formats the skill activation response with structured wrapping.
|
|
7
|
+
*
|
|
8
|
+
* Follows the agentskills.io pattern: the instructions are wrapped in
|
|
9
|
+
* identifying tags and bundled resources are listed so the agent can
|
|
10
|
+
* load them on demand via its file-read tool.
|
|
11
|
+
*/
|
|
12
|
+
function formatSkillResponse(skill: Skill): string {
|
|
13
|
+
const parts: string[] = [];
|
|
14
|
+
|
|
15
|
+
parts.push(`<skill_content name="${skill.name}">`);
|
|
16
|
+
parts.push(skill.instructions);
|
|
17
|
+
|
|
18
|
+
if (skill.location) {
|
|
19
|
+
parts.push(`\nSkill directory: ${skill.location}`);
|
|
20
|
+
parts.push(
|
|
21
|
+
"Relative paths in this skill resolve against the skill directory above.",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const resources = skill.resourceContents
|
|
26
|
+
? Object.keys(skill.resourceContents)
|
|
27
|
+
: [];
|
|
28
|
+
if (resources.length > 0) {
|
|
29
|
+
parts.push("");
|
|
30
|
+
parts.push("<skill_resources>");
|
|
31
|
+
for (const r of resources) {
|
|
32
|
+
parts.push(` <file>${r}</file>`);
|
|
33
|
+
}
|
|
34
|
+
parts.push("</skill_resources>");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
parts.push("</skill_content>");
|
|
38
|
+
|
|
39
|
+
return parts.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
5
42
|
/**
|
|
6
43
|
* Creates a ReadSkill handler that looks up skills from an in-memory array.
|
|
7
44
|
* Runs directly in the workflow (like task tools) — no activity needed.
|
|
45
|
+
*
|
|
46
|
+
* The response uses structured wrapping per the agentskills.io spec:
|
|
47
|
+
* instructions are enclosed in `<skill_content>` tags, the skill directory
|
|
48
|
+
* is included, and bundled resources are listed so the agent can load them
|
|
49
|
+
* individually via its file-read tool.
|
|
8
50
|
*/
|
|
9
51
|
export function createReadSkillHandler(
|
|
10
52
|
skills: Skill[]
|
|
@@ -24,7 +66,7 @@ export function createReadSkillHandler(
|
|
|
24
66
|
}
|
|
25
67
|
|
|
26
68
|
return {
|
|
27
|
-
toolResponse: skill
|
|
69
|
+
toolResponse: formatSkillResponse(skill),
|
|
28
70
|
data: null,
|
|
29
71
|
};
|
|
30
72
|
};
|
package/src/lib/skills/index.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import type { ToolMap } from "../tool-router/types";
|
|
2
|
-
import type { Skill } from "./types";
|
|
2
|
+
import type { Skill, SkillMetadata } from "./types";
|
|
3
3
|
import { createReadSkillTool } from "./tool";
|
|
4
4
|
import { createReadSkillHandler } from "./handler";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Validates that all skill names are unique. Throws immediately if duplicates
|
|
8
|
+
* are found so misconfiguration is caught at session wiring time.
|
|
9
|
+
*/
|
|
10
|
+
function validateSkillNames(skills: SkillMetadata[]): void {
|
|
11
|
+
const names = skills.map((s) => s.name);
|
|
12
|
+
const dupes = names.filter((n, i) => names.indexOf(n) !== i);
|
|
13
|
+
if (dupes.length > 0) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Duplicate skill names: ${[...new Set(dupes)].join(", ")}`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* Builds a fully wired tool entry for the ReadSkill tool.
|
|
8
22
|
*
|
|
@@ -13,6 +27,8 @@ export function buildSkillRegistration(
|
|
|
13
27
|
): ToolMap[string] | null {
|
|
14
28
|
if (skills.length === 0) return null;
|
|
15
29
|
|
|
30
|
+
validateSkillNames(skills);
|
|
31
|
+
|
|
16
32
|
return {
|
|
17
33
|
...createReadSkillTool(skills),
|
|
18
34
|
handler: createReadSkillHandler(skills),
|