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.
Files changed (76) hide show
  1. package/README.md +44 -8
  2. package/dist/adapters/sandbox/bedrock/index.cjs +452 -0
  3. package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -0
  4. package/dist/adapters/sandbox/bedrock/index.d.cts +23 -0
  5. package/dist/adapters/sandbox/bedrock/index.d.ts +23 -0
  6. package/dist/adapters/sandbox/bedrock/index.js +449 -0
  7. package/dist/adapters/sandbox/bedrock/index.js.map +1 -0
  8. package/dist/adapters/sandbox/bedrock/workflow.cjs +33 -0
  9. package/dist/adapters/sandbox/bedrock/workflow.cjs.map +1 -0
  10. package/dist/adapters/sandbox/bedrock/workflow.d.cts +29 -0
  11. package/dist/adapters/sandbox/bedrock/workflow.d.ts +29 -0
  12. package/dist/adapters/sandbox/bedrock/workflow.js +31 -0
  13. package/dist/adapters/sandbox/bedrock/workflow.js.map +1 -0
  14. package/dist/adapters/sandbox/virtual/index.cjs +12 -2
  15. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  16. package/dist/adapters/sandbox/virtual/index.d.cts +4 -4
  17. package/dist/adapters/sandbox/virtual/index.d.ts +4 -4
  18. package/dist/adapters/sandbox/virtual/index.js +12 -2
  19. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  20. package/dist/adapters/sandbox/virtual/workflow.d.cts +2 -2
  21. package/dist/adapters/sandbox/virtual/workflow.d.ts +2 -2
  22. package/dist/adapters/thread/google-genai/index.d.cts +2 -2
  23. package/dist/adapters/thread/google-genai/index.d.ts +2 -2
  24. package/dist/adapters/thread/google-genai/workflow.d.cts +2 -2
  25. package/dist/adapters/thread/google-genai/workflow.d.ts +2 -2
  26. package/dist/adapters/thread/langchain/index.d.cts +2 -2
  27. package/dist/adapters/thread/langchain/index.d.ts +2 -2
  28. package/dist/adapters/thread/langchain/workflow.d.cts +2 -2
  29. package/dist/adapters/thread/langchain/workflow.d.ts +2 -2
  30. package/dist/index.cjs +202 -19
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +63 -10
  33. package/dist/index.d.ts +63 -10
  34. package/dist/index.js +203 -21
  35. package/dist/index.js.map +1 -1
  36. package/dist/{queries-DModcWRy.d.cts → queries-BYGBImeC.d.cts} +1 -1
  37. package/dist/{queries-byD0jr1Y.d.ts → queries-DwBe2CAA.d.ts} +1 -1
  38. package/dist/{types-DQW8l7pY.d.cts → types-7PeMi1bD.d.cts} +9 -2
  39. package/dist/types-BdCdR41N.d.ts +74 -0
  40. package/dist/{types-BuXdFhaZ.d.cts → types-Bf8KV0Ci.d.cts} +1 -1
  41. package/dist/{types-Bll19FZJ.d.ts → types-D_igp10o.d.cts} +4 -0
  42. package/dist/{types-Bll19FZJ.d.cts → types-D_igp10o.d.ts} +4 -0
  43. package/dist/{types-GZ76HZSj.d.ts → types-LVKmCNds.d.ts} +1 -1
  44. package/dist/types-ZHs2v9Ap.d.cts +74 -0
  45. package/dist/{types-B50pBPEV.d.ts → types-hmferhc2.d.ts} +9 -2
  46. package/dist/workflow.cjs +73 -11
  47. package/dist/workflow.cjs.map +1 -1
  48. package/dist/workflow.d.cts +12 -7
  49. package/dist/workflow.d.ts +12 -7
  50. package/dist/workflow.js +73 -11
  51. package/dist/workflow.js.map +1 -1
  52. package/package.json +26 -1
  53. package/src/adapters/sandbox/bedrock/filesystem.ts +346 -0
  54. package/src/adapters/sandbox/bedrock/index.ts +259 -0
  55. package/src/adapters/sandbox/bedrock/proxy.ts +56 -0
  56. package/src/adapters/sandbox/bedrock/types.ts +24 -0
  57. package/src/adapters/sandbox/virtual/filesystem.ts +5 -3
  58. package/src/adapters/sandbox/virtual/provider.ts +9 -0
  59. package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +26 -0
  60. package/src/index.ts +2 -1
  61. package/src/lib/lifecycle.ts +1 -1
  62. package/src/lib/sandbox/node-fs.ts +115 -0
  63. package/src/lib/session/session.integration.test.ts +97 -0
  64. package/src/lib/session/session.ts +33 -2
  65. package/src/lib/session/types.ts +1 -1
  66. package/src/lib/skills/fs-provider.ts +65 -4
  67. package/src/lib/skills/handler.ts +43 -1
  68. package/src/lib/skills/index.ts +0 -1
  69. package/src/lib/skills/register.ts +17 -1
  70. package/src/lib/skills/skills.integration.test.ts +308 -24
  71. package/src/lib/skills/types.ts +6 -0
  72. package/src/lib/subagent/handler.ts +10 -4
  73. package/src/lib/subagent/subagent.integration.test.ts +36 -4
  74. package/src/lib/tool-router/router.ts +6 -3
  75. package/src/lib/tool-router/types.ts +4 -0
  76. 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 entry = this.entries.get(normalisePath(path, this.workspaceBase));
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 entry = this.entries.get(normalisePath(path, this.workspaceBase));
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
@@ -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
- sandboxId = (sandboxInit as { mode: "inherit"; sandboxId: string }).sandboxId;
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 result = await sandboxOps.createSandbox();
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
 
@@ -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 activity-side) */
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
- * │ └── SKILL.md
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
- skills.push(frontmatter);
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
- return { ...frontmatter, instructions: body };
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
- skills.push({ ...frontmatter, instructions: body });
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.instructions,
69
+ toolResponse: formatSkillResponse(skill),
28
70
  data: null,
29
71
  };
30
72
  };
@@ -4,4 +4,3 @@ export { createReadSkillTool, READ_SKILL_TOOL_NAME } from "./tool";
4
4
  export type { ReadSkillArgs } from "./tool";
5
5
  export { createReadSkillHandler } from "./handler";
6
6
  export { buildSkillRegistration } from "./register";
7
-
@@ -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),