Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions examples/skills_with_cube/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME
TRPC_AGENT_API_KEY=your-api-key
TRPC_AGENT_BASE_URL=your-base-url
TRPC_AGENT_MODEL_NAME=your-model-name


CUBE_TEMPLATE_ID=your-cube-template-id
E2B_API_URL=your-e2b-api-url
E2B_API_KEY=your-e2b-api-key
99 changes: 99 additions & 0 deletions examples/skills_with_cube/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Skills Cube 与 stage_inputs 示例

本示例演示在腾讯云 Agent 沙箱 Cube 工作区中执行 `skill_run`,并通过 `host://`、`workspace://`、`skill://` 等输入方案演示 `stage_inputs` 如何把本地输入上传/复制到远端沙箱。

## 关键特性

- `create_cube_sandbox_client(CubeClientConfig(auto_recover=True))`:创建远端 Cube 沙箱;底层 sandbox 过期/不存在时,`CubeSandboxClient` 会自动创建新 sandbox 并重试当前操作一次
- `agent/tools.py` 中 `build_cube_skill_run_payload` 生成固定形态的 `skill_run` 负载供模型调用
- `host://` 输入会从运行示例的本机路径上传到 Cube 沙箱,不依赖 Docker bind mount
- `run_agent.py` 会准备示例 `/tmp/skillrun-inputs/sales.csv`,demo 结束后销毁 Cube 沙箱

## Agent 层级结构说明

- 根节点:`LlmAgent`,挂载 `SkillToolSet`(Cube 运行时 + 技能仓库)
- 无子 Agent

## 关键代码解释

- `agent/tools.py`:通过 `CubeClientConfig(auto_recover=True)` 创建 `CubeSandboxClient`,再通过 `create_cube_workspace_runtime` 创建 workspace runtime
- `agent/agent.py`:异步创建 agent,并把 workspace runtime 返回给 runner 做最终销毁
- `run_agent.py`:组装含 `inputs` 数组的 JSON 提示词,驱动单次 `skill_run` 演示,并在 finally 中销毁沙箱

## 环境与运行

- Python 3.12;仓库根目录安装 Cube extra:`pip install -e '.[cube]'`
- 配置 `TRPC_AGENT_API_KEY`、`TRPC_AGENT_BASE_URL`、`TRPC_AGENT_MODEL_NAME`
- 配置 Cube 环境变量:`CUBE_TEMPLATE_ID`、`E2B_API_URL`、`E2B_API_KEY`
- 可选:`SKILLS_ROOT`、`CUBE_EXECUTE_TIMEOUT`(默认 `30`)、`CUBE_IDLE_TIMEOUT`(默认 `600`)

```bash
cd examples/skills_with_cube
python3 run_agent.py
```

### 验证 sandbox 重建后的 Skill runtime 恢复

为了验证业务主动重建 sandbox 后,Skill 工具链仍能继续使用当前 workspace runtime,可以开启下面的环境变量:

```bash
SKILLS_WITH_CUBE_RECREATE_BETWEEN_RUNS=1 python3 run_agent.py
```

该模式会连续发起两次相同的 `skill_run` 请求:第一次正常运行;第二次请求前通过 workspace runtime 主动重建 Cube sandbox。只要第二次请求也能正常完成,就说明 Skill 工具链可以继续使用新的 runtime,而不是继续访问过期 sandbox。

### 验证 sandbox 失效后的自动恢复

为了验证更接近真实场景的自动恢复路径,可以开启下面的临时测试开关:

```bash
SKILLS_WITH_CUBE_KILL_BETWEEN_RUNS=1 python3 run_agent.py
```

该模式同样会连续发起两次相同的 `skill_run` 请求。第一次请求成功后,示例会直接 kill 远端 Cube sandbox,但保留本地 `CubeSandboxClient` 中的旧句柄。第二次请求继续使用旧句柄访问远端 sandbox,此时 Cube 会返回类似 `Code.unknown: The requested resource does not exist` 的错误;如果 `auto_recover=True` 生效,日志中会出现:

```txt
Cube sandbox expired; recreating sandbox client: Code.unknown: The requested resource does not exist
Cube sandbox client using sandbox: <new-sandbox-id>
```

随后第二次 `skill_run` 仍应返回 `exit_code=0`,说明旧 sandbox 被平台/外部销毁后,`CubeSandboxClient` 已自动恢复并继续执行当前操作。

注意:`SKILLS_WITH_CUBE_KILL_BETWEEN_RUNS` 是为了验证恢复机制而加入的临时代码,会使用私有句柄直接 kill 远端 sandbox。正式提交示例或生产代码时可以删除该测试开关及对应 helper。

## 期望运行结果

```txt
[START] skills_with_cube
...
created Cube sandbox ...
...
🔧 [Invoke Tool:: skill_run({... 'inputs': [
'host:///tmp/skillrun-inputs/sales.csv',
'workspace://skills/python-math/SKILL.md',
'skill://python-math/scripts/fib.py',
], ...})
📊 [Tool Result: {
'stdout': '', 'stderr': '', 'exit_code': 0,
'output_files': [
{'name': 'out/fib.txt', 'content': '0\n1\n1\n2\n3\n5\n8\n13\n21\n34\n', ...},
{'name': 'out/staged_inputs_tree.txt', 'content':
'work/inputs:\nsales.csv\n---\nwork/staged_inputs:\nfib.py\npython-math_skill.md\n', ...},
],
...
}]
...
```

## 结果分析(是否符合要求)

符合本示例测试要求:Cube 沙箱成功创建并完成 `skill_run` 调用链;`host://` / `workspace://` / `skill://` 三种 input scheme 都成功落入远端工作区,输出文件 `out/fib.txt` 和 `out/staged_inputs_tree.txt` 正常产出,进程以 `exit_code=0` 结束。

如果使用 `SKILLS_WITH_CUBE_KILL_BETWEEN_RUNS=1`,还需要确认第二次请求中出现自动恢复日志,并且第二次 `skill_run` 仍然成功。这表示真实的“旧 sandbox 不存在 -> client 自动重建 -> 当前操作重试”链路通过。

## 适用场景建议

- 需要在远端 Cube 沙箱内执行技能、并验证本地输入上传到沙箱时参考本示例
- 调试 `workspace://` 时应确保源文件已存在于当前 workspace,再复制或链接到目标路径
- 长生命周期 agent 建议开启 `CubeClientConfig(auto_recover=True)`,避免 Cube sandbox 因超时或平台清理后导致后续技能调用持续失败
- 自动恢复会创建全新的 sandbox,远端 workspace 内容不会自动从旧 sandbox 迁移;Skill staging 和 workspace 创建逻辑需要能在新 sandbox 上重新执行
5 changes: 5 additions & 0 deletions examples/skills_with_cube/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
41 changes: 41 additions & 0 deletions examples/skills_with_cube/agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""Agent for Cube-backed skill runs."""

from trpc_agent_sdk.agents import LlmAgent
from trpc_agent_sdk.models import LLMModel
from trpc_agent_sdk.models import OpenAIModel

from .config import get_model_config
from .prompts import INSTRUCTION
from .tools import create_skill_tool_set


def _create_model() -> LLMModel:
""" Create a model"""
api_key, url, model_name = get_model_config()
model = OpenAIModel(model_name=model_name, api_key=api_key, base_url=url)
return model


async def create_agent():
"""Create a Cube-backed skill run agent and its workspace runtime."""

# Create tools
skill_tool_set, skill_repository, workspace_runtime = await create_skill_tool_set()
agent = LlmAgent(
name="skill_run_agent_with_cube",
description="A professional skill run assistant that can use Agent Skills.",
model=_create_model(),
# Use state variables for template replacement - Demonstration of the {var} syntax
instruction=INSTRUCTION,
tools=[skill_tool_set],
skill_repository=skill_repository,
)
return agent, workspace_runtime


root_agent = None
19 changes: 19 additions & 0 deletions examples/skills_with_cube/agent/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
""" Agent config module"""

import os


def get_model_config() -> tuple[str, str, str]:
"""Get model config from environment variables"""
api_key = os.getenv('TRPC_AGENT_API_KEY', '')
url = os.getenv('TRPC_AGENT_BASE_URL', '')
model_name = os.getenv('TRPC_AGENT_MODEL_NAME', '')
if not api_key or not url or not model_name:
raise ValueError('''TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL,
and TRPC_AGENT_MODEL_NAME must be set in environment variables''')
return api_key, url, model_name
36 changes: 36 additions & 0 deletions examples/skills_with_cube/agent/prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
""" prompts for agent"""

INSTRUCTION = """
Be a concise, helpful assistant that can use Agent Skills.

When a task may need tools, first ask to list skills or suggest one.
Load a skill only when needed, then run commands from its docs exactly.
Prefer safe defaults; ask clarifying questions if anything is ambiguous.
When running, include output_files patterns if files are expected.
Summarize results, note saved files, and propose next steps briefly.

Inside a Cube skill workspace, inputs staged from host:// are uploaded
copies in the remote sandbox. Treat inputs/ and work/inputs/ as input
data and write new results under out/ or $OUTPUT_DIR.

When chaining multiple skills, read previous results directly from
out/ (or $OUTPUT_DIR) and write new files back to out/. Prefer using
skill_run inputs/outputs fields to map files instead of shell commands
like cp or mv where possible.

When using a skill, follow this workflow:
1. First call skill_load to load the skill documentation
2. Always call skill_list_docs immediately after skill_load to verify what documents have been loaded,
including documents from subdirectories (e.g., references/ folder)
3. If needed, use skill_select_docs to add additional documents
4. Call skill_list_docs again after skill_select_docs to confirm the final set of loaded documents
5. Finally use skill_run to execute commands

This ensures you can verify that all relevant documentation files, including those in subdirectories,
are properly loaded before executing commands.
"""
116 changes: 116 additions & 0 deletions examples/skills_with_cube/agent/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""Tools for the Cube-backed skill agent."""
import os
from pathlib import Path
from typing import Any

from trpc_agent_sdk.code_executors import WorkspaceInputSpec
from trpc_agent_sdk.code_executors.cube import CubeClientConfig
from trpc_agent_sdk.code_executors.cube import CubeWorkspaceRuntime
from trpc_agent_sdk.code_executors.cube import CubeWorkspaceRuntimeConfig
from trpc_agent_sdk.code_executors.cube import create_cube_sandbox_client
from trpc_agent_sdk.code_executors.cube import create_cube_workspace_runtime
from trpc_agent_sdk.skills import ENV_SKILLS_ROOT
from trpc_agent_sdk.skills import SkillToolSet
from trpc_agent_sdk.skills import create_default_skill_repository


def _get_skill_paths() -> str:
"""Get the skill paths."""
skills_root = os.getenv(ENV_SKILLS_ROOT)
if skills_root:
return skills_root
current_path = Path(__file__).parent
path = str(current_path.parent / "skills")
# convert to file URL
# path = "file://" + path
# "http://{host}:{port}/{path}/{filename}.{extension}"
# path = "http://localhost:8000/skills/skills.tar.gz"
return path


def _cube_client_config() -> CubeClientConfig:
"""Build Cube executor config from environment variables."""
return CubeClientConfig(
execute_timeout=float(os.getenv("CUBE_EXECUTE_TIMEOUT", "30")),
idle_timeout=int(os.getenv("CUBE_IDLE_TIMEOUT", "600")),
auto_recover=True,
)


async def create_skill_tool_set() -> tuple[SkillToolSet, Any, CubeWorkspaceRuntime]:
"""Create a Cube-backed skill tool set and its Cube runtime."""
tool_kwargs = {
"save_as_artifacts": True,
"omit_inline_content": False,
}

cfg = _cube_client_config()
sandbox_client = await create_cube_sandbox_client(cfg)
workspace_runtime = create_cube_workspace_runtime(
sandbox_client=sandbox_client,
execute_timeout=cfg.execute_timeout,
workspace_cfg=CubeWorkspaceRuntimeConfig(),
)
print(f"[skills_with_cube] using Cube sandbox: {workspace_runtime.sandbox_id}", flush=True)
skill_paths = _get_skill_paths()
repository = create_default_skill_repository(skill_paths, workspace_runtime=workspace_runtime)
toolset = SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs)
return toolset, repository, workspace_runtime


def build_cube_stage_inputs_specs(inputs_host: str = "/tmp/skillrun-inputs") -> list[WorkspaceInputSpec]:
"""Build example input specs for Cube runtime.

The returned specs demonstrate the supported input schemes used by
``CubeWorkspaceFS.stage_inputs``:

- ``host://`` : upload from a host path into the remote Cube sandbox
- ``workspace://``: reuse a file already present in current workspace
- ``skill://`` : reference a file under workspace ``skills/``
"""
return [
WorkspaceInputSpec(
src=f"host://{inputs_host}/sales.csv",
dst="work/inputs/sales.csv",
mode="link",
),
WorkspaceInputSpec(
# This file exists after skill staging, so the workspace:// demo is stable.
src="workspace://skills/python-math/SKILL.md",
dst="work/staged_inputs/python-math_skill.md",
mode="copy",
),
WorkspaceInputSpec(
src="skill://python-math/scripts/fib.py",
dst="work/staged_inputs/fib.py",
mode="copy",
),
]


def build_cube_skill_run_payload(skill_name: str = "python-math",
inputs_host: str = "/tmp/skillrun-inputs") -> dict[str, Any]:
"""Build a full ``skill_run`` payload for Cube mode demonstration.

This payload can be used directly when invoking the ``skill_run`` tool:
it stages input schemes into the remote Cube workspace and writes outputs
under ``out/``.
"""
return {
"skill":
skill_name,
"cwd":
f"$SKILLS_DIR/{skill_name}",
"command": ("python scripts/fib.py 10 > out/fib.txt && "
"(ls -R work/inputs; echo '---'; ls -R work/staged_inputs) > out/staged_inputs_tree.txt"),
"inputs": [spec.model_dump() for spec in build_cube_stage_inputs_specs(inputs_host=inputs_host)],
"output_files": [
"out/fib.txt",
"out/staged_inputs_tree.txt",
],
}
Loading
Loading