mirror of
https://github.com/langchain-ai/docs.git
synced 2026-07-19 16:33:35 -04:00
feat: document how to execute skill scripts in a sandbox (#3590)
This commit is contained in:
@@ -403,9 +403,15 @@ jobs:
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const mdFiles = files.filter(f =>
|
||||
f.filename.endsWith('.mdx') || f.filename.endsWith('.md')
|
||||
);
|
||||
const mdFiles = files.filter(f => {
|
||||
const name = f.filename;
|
||||
if (!(name.endsWith('.mdx') || name.endsWith('.md'))) return false;
|
||||
const segments = name.split('/');
|
||||
if (segments.includes('code-samples') || segments.includes('snippets')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const ranked = [...mdFiles].sort((a, b) => {
|
||||
const score = (x) =>
|
||||
|
||||
@@ -109,6 +109,9 @@ jobs:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
POSTGRES_URI: postgresql://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable
|
||||
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }}
|
||||
DAYTONA_API_URL: ${{ secrets.DAYTONA_API_URL }}
|
||||
run: |
|
||||
if [[ "${{ steps.files.outputs.run_all }}" == "true" ]]; then
|
||||
echo "Running all code samples..."
|
||||
|
||||
@@ -20,6 +20,8 @@ dependencies = [
|
||||
"httpx>=0.27.0",
|
||||
"markdownify>=0.13.0",
|
||||
"langchain-google-genai>=2.0.0",
|
||||
"langchain-daytona>=0.0.5",
|
||||
"daytona>=0.100.0",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# :snippet-start: skills-sandbox-py
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from daytona import Daytona
|
||||
from deepagents import create_deep_agent
|
||||
from deepagents.backends import CompositeBackend, StoreBackend
|
||||
from deepagents.backends.utils import create_file_data
|
||||
from langchain.agents.middleware import AgentMiddleware, AgentState
|
||||
|
||||
# :remove-start:
|
||||
from langchain.messages import HumanMessage
|
||||
|
||||
# :remove-end:
|
||||
from langchain_daytona import DaytonaSandbox
|
||||
from langgraph.runtime import Runtime
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
|
||||
# Identical skill bundles for every user: one shared store namespace.
|
||||
SKILLS_SHARED_NAMESPACE = ("skills", "builtin")
|
||||
|
||||
|
||||
class SkillSandboxSyncMiddleware(AgentMiddleware[AgentState, Any, Any]):
|
||||
"""Copy shared skill files from the store into the sandbox before each agent run."""
|
||||
|
||||
def __init__(self, backend: CompositeBackend) -> None:
|
||||
super().__init__()
|
||||
self.backend = backend
|
||||
|
||||
async def abefore_agent(self, state: AgentState, runtime: Runtime[Any]) -> None:
|
||||
store = runtime.store
|
||||
|
||||
files: list[tuple[str, bytes]] = []
|
||||
for item in await store.asearch(SKILLS_SHARED_NAMESPACE):
|
||||
key = str(item.key)
|
||||
if ".." in key or any(c in key for c in ("*", "?")):
|
||||
msg = f"Invalid key: {key}"
|
||||
raise ValueError(msg)
|
||||
normalized = key if key.startswith("/") else f"/{key}"
|
||||
# CompositeBackend routes paths and batches uploads to the right backend.
|
||||
files.append((f"/skills{normalized}", item.value["content"].encode()))
|
||||
|
||||
if files:
|
||||
await self.backend.aupload_files(files)
|
||||
|
||||
|
||||
async def seed_skill_store(store: InMemoryStore) -> None:
|
||||
"""Load canonical skill files from disk into the shared store namespace (run once at deploy).
|
||||
You can retrieve skills from any source (local filesystem, remote URL, etc.).
|
||||
"""
|
||||
skills_dir = Path(__file__).resolve().parent / "skills"
|
||||
for file_path in sorted(p for p in skills_dir.rglob("*") if p.is_file()):
|
||||
rel = file_path.relative_to(skills_dir).as_posix()
|
||||
key = f"/{rel}"
|
||||
await store.aput(
|
||||
SKILLS_SHARED_NAMESPACE,
|
||||
key,
|
||||
create_file_data(file_path.read_text(encoding="utf-8")),
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
store = InMemoryStore()
|
||||
await seed_skill_store(store)
|
||||
|
||||
daytona = Daytona()
|
||||
sandbox = daytona.create()
|
||||
sandbox_backend = DaytonaSandbox(sandbox=sandbox)
|
||||
|
||||
backend = CompositeBackend(
|
||||
default=sandbox_backend,
|
||||
routes={
|
||||
"/skills/": StoreBackend(
|
||||
store=store,
|
||||
namespace=lambda _rt: SKILLS_SHARED_NAMESPACE,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
agent = create_deep_agent(
|
||||
model="anthropic:claude-sonnet-4-6",
|
||||
backend=backend,
|
||||
skills=["/skills/"],
|
||||
store=store,
|
||||
middleware=[SkillSandboxSyncMiddleware(backend)],
|
||||
)
|
||||
|
||||
# :remove-start:
|
||||
result = await agent.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
HumanMessage(
|
||||
content=(
|
||||
"Use the write-timestamp skill to write the current date and time "
|
||||
"to a file, then tell me what you wrote."
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
config={"configurable": {"thread_id": "skills-sandbox-demo"}},
|
||||
)
|
||||
|
||||
messages = result.get("messages", [])
|
||||
if messages:
|
||||
print(getattr(messages[-1], "content", ""))
|
||||
# :remove-end:
|
||||
finally:
|
||||
sandbox.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
# :snippet-end:
|
||||
@@ -0,0 +1,160 @@
|
||||
// :snippet-start: skills-sandbox-js
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import { join, posix, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createMiddleware } from "langchain";
|
||||
import {
|
||||
CompositeBackend,
|
||||
createDeepAgent,
|
||||
type FileData,
|
||||
StoreBackend,
|
||||
} from "deepagents";
|
||||
import { InMemoryStore } from "@langchain/langgraph";
|
||||
|
||||
import { DaytonaSandbox } from "@langchain/daytona";
|
||||
|
||||
/** Identical skill bundles for every user: one shared store namespace. */
|
||||
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
|
||||
|
||||
function createFileData(content: string): FileData {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
content: content.split("\n"),
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSkillsStoreKey(key: string): string {
|
||||
const k = String(key);
|
||||
if (k.includes("..") || /[*?]/.test(k)) {
|
||||
throw new Error(`Invalid key: ${key}`);
|
||||
}
|
||||
return k.startsWith("/") ? k : `/${k}`;
|
||||
}
|
||||
|
||||
async function walkFiles(dir: string): Promise<string[]> {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await walkFiles(fullPath)));
|
||||
} else if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
/** Load canonical skill files from disk into the shared store namespace (run once at deploy).
|
||||
* You can retrieve skills from any source (local filesystem, remote URL, etc.).
|
||||
*/
|
||||
async function seedSkillStore(store: InMemoryStore) {
|
||||
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
||||
const skillsDir = resolve(moduleDir, "skills");
|
||||
const filePaths = await walkFiles(skillsDir);
|
||||
for (const filePath of filePaths) {
|
||||
const rel = relative(skillsDir, filePath);
|
||||
// StoreBackend keys are paths *relative to the routed backend root*.
|
||||
// CompositeBackend strips the route prefix (`/skills/`) before delegating,
|
||||
// so store keys should look like "/<skillname>/SKILL.md".
|
||||
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
|
||||
const content = await readFile(filePath, "utf8");
|
||||
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy shared skill files from the store into the sandbox before each agent run. */
|
||||
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
|
||||
return createMiddleware({
|
||||
name: "SkillSandboxSyncMiddleware",
|
||||
beforeAgent: async (state, runtime) => {
|
||||
const store = (runtime as any).store;
|
||||
if (!store) {
|
||||
throw new Error(
|
||||
"Store is required for syncing skills into the sandbox. " +
|
||||
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
|
||||
);
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const files: Array<[string, Uint8Array]> = [];
|
||||
|
||||
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
|
||||
const normalized = normalizeSkillsStoreKey(String(item.key));
|
||||
const data = item.value as FileData;
|
||||
// CompositeBackend routes paths and batches uploads to the right backend.
|
||||
files.push([
|
||||
`/skills${normalized}`,
|
||||
encoder.encode(data.content.join("\n")),
|
||||
]);
|
||||
}
|
||||
|
||||
if (files.length > 0) await backend.uploadFiles(files);
|
||||
|
||||
return state;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const store = new InMemoryStore();
|
||||
await seedSkillStore(store);
|
||||
|
||||
const sandbox = await DaytonaSandbox.create({
|
||||
language: "python",
|
||||
timeout: 300,
|
||||
});
|
||||
|
||||
const backend = new CompositeBackend(sandbox, {
|
||||
"/skills/": new StoreBackend({
|
||||
store,
|
||||
namespace: () => [...SKILLS_SHARED_NAMESPACE],
|
||||
} as any),
|
||||
});
|
||||
|
||||
try {
|
||||
const agent = await createDeepAgent({
|
||||
model: "anthropic:claude-sonnet-4-6",
|
||||
backend,
|
||||
skills: ["/skills/"],
|
||||
store,
|
||||
middleware: [createSkillSandboxSyncMiddleware(backend)],
|
||||
});
|
||||
|
||||
// :remove-start:
|
||||
const result = await agent.invoke(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Use the write-timestamp skill to write the current date and time to a file, then tell me what you wrote.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
configurable: { thread_id: "skills-sandbox-demo" },
|
||||
},
|
||||
);
|
||||
|
||||
const messages = result.messages ?? [];
|
||||
if (messages.length > 0) {
|
||||
const last = messages[messages.length - 1];
|
||||
console.log(
|
||||
typeof last.content === "string" ? last.content : String(last.content),
|
||||
);
|
||||
}
|
||||
// :remove-end:
|
||||
} finally {
|
||||
await sandbox.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
// :snippet-end:
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: write-timestamp
|
||||
description: Use when the user wants the current date and time written to a file via the bundled script inside the sandbox.
|
||||
---
|
||||
|
||||
# Write timestamp
|
||||
|
||||
## Instructions
|
||||
|
||||
1. The script is available in the sandbox at `/skills/write-timestamp/write_timestamp.py` after sync.
|
||||
2. Run it with the `execute` tool, for example: `python /skills/write-timestamp/write_timestamp.py`
|
||||
3. Read `/tmp/skill_timestamp.txt` with `read_file` and summarize the result for the user.
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Write the current UTC date and time to a file (runs inside the sandbox)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
stamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
out = Path("/tmp/skill_timestamp.txt")
|
||||
out.write_text(stamp + "\n", encoding="utf-8")
|
||||
print(stamp)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+3392
-2
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@langchain/anthropic": "^1.3.22",
|
||||
"@langchain/daytona": "^0.2.0",
|
||||
"@langchain/langgraph-checkpoint-postgres": "^1.0.1",
|
||||
"@langchain/openai": "^1.2.12",
|
||||
"deepagents": "^1.8.5",
|
||||
|
||||
@@ -5,6 +5,8 @@ description: Learn how to extend your deep agent's capabilities with skills
|
||||
|
||||
import SkillsUsageTabsPy from '/snippets/skills-usage-tabs-py.mdx';
|
||||
import SkillsUsageTabsJs from '/snippets/skills-usage-tabs-js.mdx';
|
||||
import SkillsSandboxPy from '/snippets/code-samples/skills-sandbox-py.mdx';
|
||||
import SkillsSandboxJs from '/snippets/code-samples/skills-sandbox-js.mdx';
|
||||
|
||||
Skills are reusable agent capabilities that provide specialized workflows and domain knowledge.
|
||||
|
||||
@@ -321,6 +323,28 @@ When skills are configured, a "Skills System" section is injected into the agent
|
||||
Write clear, specific descriptions in your `SKILL.md` frontmatter. The agent decides whether to use a skill based on the description alone—detailed descriptions lead to better skill matching.
|
||||
</Tip>
|
||||
|
||||
## Execute skill scripts in a sandbox
|
||||
|
||||
Skills can include scripts alongside the `SKILL.md` file, such as, for example, a Python file that performs a search or data transformation. The agent can _read_ these scripts from any backend, but to _execute_ them, the agent needs access to a shell — which only [sandbox backends](/oss/deepagents/sandboxes) provide.
|
||||
|
||||
When you use a @[CompositeBackend] that routes skills to a @[StoreBackend] for persistence while using a sandbox as the default backend, skill files live in the store rather than in the sandbox is where code runs. For sandboxes to be able to use the scripts, you must use [custom middleware](/oss/langchain/middleware/custom) to upload skill scripts into the sandbox before the agent starts:
|
||||
|
||||
:::python
|
||||
|
||||
<SkillsSandboxPy />
|
||||
|
||||
The middleware's `before_agent` hook runs before each agent invocation, reading skill files from that shared namespace and uploading them into the sandbox filesystem. Once synced, the agent can execute scripts with the `execute` tool just like any other file in the sandbox.
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
<SkillsSandboxJs />
|
||||
|
||||
The middleware's `beforeAgent` hook runs before each agent invocation, reading skill files from that shared namespace and uploading them into the sandbox filesystem. Once synced, the agent can execute scripts with the `execute` tool just like any other file in the sandbox.
|
||||
:::
|
||||
|
||||
For a more complete example that also syncs [memories](/oss/deepagents/memory) bidirectionally, see [syncing skills and memories with custom middleware](/oss/deepagents/going-to-production#example-syncing-skills-and-memories-with-custom-middleware).
|
||||
|
||||
## Skills vs. memory
|
||||
|
||||
Skills and [memory](/oss/deepagents/customization#memory) (`AGENTS.md` files) serve different purposes:
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
```ts
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import { join, posix, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createMiddleware } from "langchain";
|
||||
import {
|
||||
CompositeBackend,
|
||||
createDeepAgent,
|
||||
type FileData,
|
||||
StoreBackend,
|
||||
} from "deepagents";
|
||||
import { InMemoryStore } from "@langchain/langgraph";
|
||||
|
||||
import { DaytonaSandbox } from "@langchain/daytona";
|
||||
|
||||
/** Identical skill bundles for every user: one shared store namespace. */
|
||||
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
|
||||
|
||||
function createFileData(content: string): FileData {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
content: content.split("\n"),
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSkillsStoreKey(key: string): string {
|
||||
const k = String(key);
|
||||
if (k.includes("..") || /[*?]/.test(k)) {
|
||||
throw new Error(`Invalid key: ${key}`);
|
||||
}
|
||||
return k.startsWith("/") ? k : `/${k}`;
|
||||
}
|
||||
|
||||
async function walkFiles(dir: string): Promise<string[]> {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await walkFiles(fullPath)));
|
||||
} else if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
/** Load canonical skill files from disk into the shared store namespace (run once at deploy).
|
||||
* You can retrieve skills from any source (local filesystem, remote URL, etc.).
|
||||
*/
|
||||
async function seedSkillStore(store: InMemoryStore) {
|
||||
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
||||
const skillsDir = resolve(moduleDir, "skills");
|
||||
const filePaths = await walkFiles(skillsDir);
|
||||
for (const filePath of filePaths) {
|
||||
const rel = relative(skillsDir, filePath);
|
||||
// StoreBackend keys are paths *relative to the routed backend root*.
|
||||
// CompositeBackend strips the route prefix (`/skills/`) before delegating,
|
||||
// so store keys should look like "/<skillname>/SKILL.md".
|
||||
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
|
||||
const content = await readFile(filePath, "utf8");
|
||||
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy shared skill files from the store into the sandbox before each agent run. */
|
||||
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
|
||||
return createMiddleware({
|
||||
name: "SkillSandboxSyncMiddleware",
|
||||
beforeAgent: async (state, runtime) => {
|
||||
const store = (runtime as any).store;
|
||||
if (!store) {
|
||||
throw new Error(
|
||||
"Store is required for syncing skills into the sandbox. " +
|
||||
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
|
||||
);
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const files: Array<[string, Uint8Array]> = [];
|
||||
|
||||
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
|
||||
const normalized = normalizeSkillsStoreKey(String(item.key));
|
||||
const data = item.value as FileData;
|
||||
// CompositeBackend routes paths and batches uploads to the right backend.
|
||||
files.push([
|
||||
`/skills${normalized}`,
|
||||
encoder.encode(data.content.join("\n")),
|
||||
]);
|
||||
}
|
||||
|
||||
if (files.length > 0) await backend.uploadFiles(files);
|
||||
|
||||
return state;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const store = new InMemoryStore();
|
||||
await seedSkillStore(store);
|
||||
|
||||
const sandbox = await DaytonaSandbox.create({
|
||||
language: "python",
|
||||
timeout: 300,
|
||||
});
|
||||
|
||||
const backend = new CompositeBackend(sandbox, {
|
||||
"/skills/": new StoreBackend({
|
||||
store,
|
||||
namespace: () => [...SKILLS_SHARED_NAMESPACE],
|
||||
} as any),
|
||||
});
|
||||
|
||||
try {
|
||||
const agent = await createDeepAgent({
|
||||
model: "anthropic:claude-sonnet-4-6",
|
||||
backend,
|
||||
skills: ["/skills/"],
|
||||
store,
|
||||
middleware: [createSkillSandboxSyncMiddleware(backend)],
|
||||
});
|
||||
|
||||
} finally {
|
||||
await sandbox.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
```python
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from daytona import Daytona
|
||||
from deepagents import create_deep_agent
|
||||
from deepagents.backends import CompositeBackend, StoreBackend
|
||||
from deepagents.backends.utils import create_file_data
|
||||
from langchain.agents.middleware import AgentMiddleware, AgentState
|
||||
|
||||
from langchain_daytona import DaytonaSandbox
|
||||
from langgraph.runtime import Runtime
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
|
||||
# Identical skill bundles for every user: one shared store namespace.
|
||||
SKILLS_SHARED_NAMESPACE = ("skills", "builtin")
|
||||
|
||||
|
||||
class SkillSandboxSyncMiddleware(AgentMiddleware[AgentState, Any, Any]):
|
||||
"""Copy shared skill files from the store into the sandbox before each agent run."""
|
||||
|
||||
def __init__(self, backend: CompositeBackend) -> None:
|
||||
super().__init__()
|
||||
self.backend = backend
|
||||
|
||||
async def abefore_agent(self, state: AgentState, runtime: Runtime[Any]) -> None:
|
||||
store = runtime.store
|
||||
|
||||
files: list[tuple[str, bytes]] = []
|
||||
for item in await store.asearch(SKILLS_SHARED_NAMESPACE):
|
||||
key = str(item.key)
|
||||
if ".." in key or any(c in key for c in ("*", "?")):
|
||||
msg = f"Invalid key: {key}"
|
||||
raise ValueError(msg)
|
||||
normalized = key if key.startswith("/") else f"/{key}"
|
||||
# CompositeBackend routes paths and batches uploads to the right backend.
|
||||
files.append((f"/skills{normalized}", item.value["content"].encode()))
|
||||
|
||||
if files:
|
||||
await self.backend.aupload_files(files)
|
||||
|
||||
|
||||
async def seed_skill_store(store: InMemoryStore) -> None:
|
||||
"""Load canonical skill files from disk into the shared store namespace (run once at deploy).
|
||||
You can retrieve skills from any source (local filesystem, remote URL, etc.).
|
||||
"""
|
||||
skills_dir = Path(__file__).resolve().parent / "skills"
|
||||
for file_path in sorted(p for p in skills_dir.rglob("*") if p.is_file()):
|
||||
rel = file_path.relative_to(skills_dir).as_posix()
|
||||
key = f"/{rel}"
|
||||
await store.aput(
|
||||
SKILLS_SHARED_NAMESPACE,
|
||||
key,
|
||||
create_file_data(file_path.read_text(encoding="utf-8")),
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
store = InMemoryStore()
|
||||
await seed_skill_store(store)
|
||||
|
||||
daytona = Daytona()
|
||||
sandbox = daytona.create()
|
||||
sandbox_backend = DaytonaSandbox(sandbox=sandbox)
|
||||
|
||||
backend = CompositeBackend(
|
||||
default=sandbox_backend,
|
||||
routes={
|
||||
"/skills/": StoreBackend(
|
||||
store=store,
|
||||
namespace=lambda _rt: SKILLS_SHARED_NAMESPACE,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
agent = create_deep_agent(
|
||||
model="anthropic:claude-sonnet-4-6",
|
||||
backend=backend,
|
||||
skills=["/skills/"],
|
||||
store=store,
|
||||
middleware=[SkillSandboxSyncMiddleware(backend)],
|
||||
)
|
||||
|
||||
finally:
|
||||
sandbox.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
Reference in New Issue
Block a user