Skip to content

Commit b4fa1e1

Browse files
committed
feat(agents): organize remote sandbox internals and add server examples
1 parent d832127 commit b4fa1e1

21 files changed

Lines changed: 876 additions & 105 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Parent VeADK model. The sandbox's own model is configured on its Tool/image.
2+
MODEL_AGENT_NAME="doubao-seed-2-1-pro-260628"
3+
MODEL_AGENT_API_BASE="https://ark.cn-beijing.volces.com/api/v3/"
4+
MODEL_AGENT_API_KEY=""
5+
6+
# Change this ID to switch Skill/CodeEnv within the same region/account.
7+
AGENTKIT_TOOL_ID=""
8+
AGENTKIT_TOOL_REGION="cn-beijing"
9+
# Leave empty to discover via GetTool. Private tools MUST explicitly set
10+
# CodeEnv or Skill according to the actual image protocol.
11+
AGENTKIT_TOOL_TYPE=""
12+
VOLCENGINE_ACCESS_KEY=""
13+
VOLCENGINE_SECRET_KEY=""
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Remote Sandbox Agent
2+
3+
- `multi_agents/agent.py`:coordinator 将任务转交给远端 sandbox 子 agent。
4+
- `single_agent/agent.py`:直接使用远端 sandbox 作为 root agent。
5+
- `single_agent/server.py`:通过 `AgentkitAgentServerApp` 提供 HTTP/SSE 服务。
6+
- `single_agent/client.py`:创建会话、发送任务并展示流式结果,不需要导入 agent 或配置云端凭证。
7+
8+
## 配置
9+
10+
使用安装了当前仓库版本 VeADK 的 Python 环境。在 `examples/17_remote_sandbox_agent/.env` 中配置。两个 server 都会在导入 agent 前加载此文件,不依赖启动目录;已导出的环境变量优先:
11+
12+
```dotenv
13+
AGENTKIT_TOOL_ID=你的ToolId
14+
# 可选,不填写时通过 GetTool 自动发现;显式值必须为 Skill 或 CodeEnv。
15+
AGENTKIT_TOOL_TYPE=CodeEnv
16+
AGENTKIT_TOOL_REGION=cn-beijing
17+
VOLCENGINE_ACCESS_KEY=你的AccessKey
18+
VOLCENGINE_SECRET_KEY=你的SecretKey
19+
```
20+
21+
PPE / STG 可额外配置实际控制面地址和签名 service:
22+
23+
```dotenv
24+
AGENTKIT_TOOL_SERVICE_CODE=agentkit_ppe
25+
AGENTKIT_TOOL_HOST=实际控制面域名
26+
AGENTKIT_TOOL_SCHEME=https
27+
```
28+
29+
STG 使用 `agentkit_stg` 及对应域名、ToolId。修改 `.env` 后重启 server。
30+
CodeEnv 需要支持 Codex Worker 协议 v1 和 `tool_events` 的沙箱镜像;Skill 需要提供 A2A 服务。
31+
单 agent 示例没有本地 coordinator 模型;远端沙箱所需的模型和技能配置由对应 Tool/镜像提供。
32+
33+
## 启动单 agent 服务
34+
35+
以下命令均从仓库根目录执行:
36+
37+
```bash
38+
python examples/17_remote_sandbox_agent/single_agent/server.py
39+
```
40+
41+
默认监听 `127.0.0.1:8000`,可使用 `--host` 和 `--port` 调整。
42+
43+
另一个终端发送任务:
44+
45+
```bash
46+
python examples/17_remote_sandbox_agent/single_agent/client.py \
47+
"用 Python 计算 2 + 3,并展示执行结果"
48+
```
49+
50+
client 会打印 `session_id`,然后展示 `[tool]`、`[tool result]`、`[progress]`、`[delta]` 和 `[answer]`。
51+
继续同一会话:
52+
53+
```bash
54+
python examples/17_remote_sandbox_agent/single_agent/client.py \
55+
--session-id 上一次打印的session_id \
56+
"把刚才的结果乘以 10"
57+
```
58+
59+
可使用 `--url http://127.0.0.1:8000` 指定服务地址,`--user-id` 指定用户。
60+
复用会话时保持 user ID 一致;传入不存在的 session ID 会报错,不会自动创建替代会话。
61+
server 默认使用内存会话,重启后需创建新会话。client 不会自动重发执行任务。
62+
63+
## 启动多 agent 服务
64+
65+
`multi_agents` 同样提供独立的 `agent.py`、`server.py` 和 `client.py`。
66+
沿用上面的沙箱配置,并为 coordinator 配置模型,例如在 `.env` 中设置 `MODEL_AGENT_API_KEY` 和 `MODEL_AGENT_NAME`。
67+
68+
```bash
69+
python examples/17_remote_sandbox_agent/multi_agents/server.py --port 8001
70+
```
71+
72+
另一个终端调用:
73+
74+
```bash
75+
python examples/17_remote_sandbox_agent/multi_agents/client.py \
76+
--url http://127.0.0.1:8001 \
77+
"用 Python 计算 2 + 3,并展示执行结果"
78+
```
79+
80+
client 会额外打印事件所属的 agent,方便观察 coordinator 向 sandbox 的转交。
81+
同样支持 `--session-id` 继续对话;请使用同一个服务和 user ID。
82+
单 agent 和多 agent 服务默认端口都是 8000,同时运行时需要为其中一个指定不同端口。
83+
84+
## VeADK Web
85+
86+
也可以从仓库根目录运行:
87+
88+
```bash
89+
veadk web examples/17_remote_sandbox_agent
90+
```
91+
92+
选择 `single_agent` 或 `multi_agents`。多 agent 示例还需要 coordinator 模型凭证。
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Remote Sandbox Agent
2+
3+
- `multi_agents/agent.py`: a coordinator transfers tasks to a remote sandbox child.
4+
- `single_agent/agent.py`: the remote sandbox itself is the root agent.
5+
- `single_agent/server.py`: serves it with `AgentkitAgentServerApp` over HTTP/SSE.
6+
- `single_agent/client.py`: creates a session, submits text, and displays streamed events. The client needs no cloud credentials or agent import.
7+
8+
## Configuration
9+
10+
Use a Python environment with VeADK installed from this checkout. Add these settings to `examples/17_remote_sandbox_agent/.env`. Both servers load this file before importing the agent, regardless of the working directory; exported environment variables take precedence:
11+
12+
```dotenv
13+
AGENTKIT_TOOL_ID=your-tool-id
14+
# Optional: Skill or CodeEnv. Omit to discover the type via GetTool.
15+
AGENTKIT_TOOL_TYPE=CodeEnv
16+
AGENTKIT_TOOL_REGION=cn-beijing
17+
VOLCENGINE_ACCESS_KEY=your-access-key
18+
VOLCENGINE_SECRET_KEY=your-secret-key
19+
```
20+
21+
For PPE/STG, configure the actual control-plane host and signing service:
22+
23+
```dotenv
24+
AGENTKIT_TOOL_SERVICE_CODE=agentkit_ppe
25+
AGENTKIT_TOOL_HOST=actual-control-plane-host
26+
AGENTKIT_TOOL_SCHEME=https
27+
```
28+
29+
For STG use `agentkit_stg`, its host, and a ToolId from that environment. Restart the server after changing `.env`.
30+
CodeEnv requires an image supporting Codex Worker protocol v1 and `tool_events`; Skill requires an A2A service.
31+
The single-agent example has no local coordinator model. Configure remote models and skills through the Tool/image.
32+
33+
## Run the server and client
34+
35+
Run from the repository root:
36+
37+
```bash
38+
python examples/17_remote_sandbox_agent/single_agent/server.py
39+
```
40+
41+
The default address is `127.0.0.1:8000`; override with `--host` and `--port`.
42+
In another terminal:
43+
44+
```bash
45+
python examples/17_remote_sandbox_agent/single_agent/client.py \
46+
"Use Python to compute 2 + 3 and show the result"
47+
```
48+
49+
The client prints the session ID, tool calls/results, progress, text deltas, and the final answer.
50+
Continue with the printed session ID:
51+
52+
```bash
53+
python examples/17_remote_sandbox_agent/single_agent/client.py \
54+
--session-id previous-session-id "Multiply that result by 10"
55+
```
56+
57+
Use `--url` to change the server address and `--user-id` to change the user.
58+
Keep the same user ID when resuming. An unknown session ID produces an error; the client does not create a replacement or automatically resubmit tasks.
59+
The server uses in-memory sessions by default, so create a new session after restarting it.
60+
61+
## Run the multi-agent server
62+
63+
`multi_agents` also provides separate `agent.py`, `server.py`, and `client.py` files.
64+
Use the same sandbox settings and configure the coordinator model, for example with `MODEL_AGENT_API_KEY` and `MODEL_AGENT_NAME` in `.env`.
65+
66+
```bash
67+
python examples/17_remote_sandbox_agent/multi_agents/server.py --port 8001
68+
```
69+
70+
In another terminal:
71+
72+
```bash
73+
python examples/17_remote_sandbox_agent/multi_agents/client.py \
74+
--url http://127.0.0.1:8001 \
75+
"Use Python to compute 2 + 3 and show the result"
76+
```
77+
78+
The client also labels each event with its agent author so you can observe the transfer from coordinator to sandbox.
79+
Use `--session-id` to continue with the same server and user ID.
80+
Both servers default to port 8000; select a different port for one when running them together.
81+
82+
## VeADK Web
83+
84+
From the repository root:
85+
86+
```bash
87+
veadk web examples/17_remote_sandbox_agent
88+
```
89+
90+
Select `single_agent` or `multi_agents`. The multi-agent example also needs coordinator model credentials.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""VeADK Web: one coordinator and one remote sandbox sub-agent."""
2+
3+
import os
4+
5+
from veadk import Agent
6+
from veadk.agents.agentkit_remote_sandbox_agent import AgentkitRemoteSandboxAgent
7+
8+
sandbox = AgentkitRemoteSandboxAgent(
9+
name="sandbox",
10+
description="执行需要远端技能、Python、命令或文件操作的任务,并直接报告执行过程和结果。",
11+
tool_id=os.getenv("AGENTKIT_TOOL_ID"),
12+
tool_type=os.getenv("AGENTKIT_TOOL_TYPE") or None,
13+
request_timeout=900,
14+
expiry_buffer=90,
15+
)
16+
root_agent = Agent(
17+
name="remote_sandbox_demo",
18+
description="A coordinator with one remote sandbox agent.",
19+
instruction=(
20+
"理解用户的请求。需要运行代码、操作文件或使用沙箱技能时,"
21+
"使用 transfer_to_agent 将任务交给 sandbox。"
22+
"不要自己运行命令,不要编造远端执行结果。"
23+
"sandbox 会直接展示工具过程并回答用户,无需重复总结。"
24+
),
25+
sub_agents=[sandbox],
26+
)
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""Send a text task to server.py and display its ADK event stream."""
2+
3+
import argparse
4+
import asyncio
5+
import json
6+
import sys
7+
from urllib.parse import quote
8+
9+
import httpx
10+
11+
12+
async def session_id_for(client, app_name, user_id, session_id=None):
13+
path = f"/apps/{quote(app_name, safe='')}/users/{quote(user_id, safe='')}/sessions"
14+
if session_id:
15+
response = await client.get(f"{path}/{quote(session_id, safe='')}")
16+
response.raise_for_status()
17+
return response.json()["id"]
18+
response = await client.post(path, json={})
19+
response.raise_for_status()
20+
return response.json()["id"]
21+
22+
23+
async def stream_events(client, *, app_name, user_id, session_id, task):
24+
body = {
25+
"appName": app_name,
26+
"userId": user_id,
27+
"sessionId": session_id,
28+
"newMessage": {"role": "user", "parts": [{"text": task}]},
29+
"streaming": True,
30+
}
31+
async with client.stream("POST", "/run_sse", json=body) as response:
32+
response.raise_for_status()
33+
data = []
34+
async for line in response.aiter_lines():
35+
if line.startswith("data:"):
36+
data.append(line[5:].lstrip(" "))
37+
elif not line and data:
38+
yield json.loads("\n".join(data))
39+
data.clear()
40+
if data:
41+
yield json.loads("\n".join(data))
42+
43+
44+
class EventPrinter:
45+
def __init__(self):
46+
self.segment = None
47+
self.author = None
48+
49+
def finish(self):
50+
if self.segment is not None:
51+
print(flush=True)
52+
self.segment = None
53+
54+
def __call__(self, event):
55+
error = event.get("error") or event.get("errorMessage")
56+
if error:
57+
self.finish()
58+
raise RuntimeError(str(error))
59+
final = False
60+
parts = (event.get("content") or {}).get("parts", [])
61+
author = event.get("author", "unknown")
62+
if parts and author != self.author:
63+
self.finish()
64+
self.author = author
65+
print(f"[agent: {author}]", flush=True)
66+
for part in parts:
67+
if "functionCall" in part or "functionResponse" in part:
68+
self.finish()
69+
key = "functionCall" if "functionCall" in part else "functionResponse"
70+
label = "tool" if key == "functionCall" else "tool result"
71+
print(
72+
f"[{label}]", json.dumps(part[key], ensure_ascii=False), flush=True
73+
)
74+
elif part.get("text"):
75+
if part.get("thought"):
76+
label = "progress"
77+
elif event.get("partial"):
78+
label = "delta"
79+
else:
80+
label = "answer"
81+
final = True
82+
if label in {"progress", "delta"}:
83+
if self.segment != label:
84+
self.finish()
85+
print(f"[{label}] ", end="", flush=True)
86+
self.segment = label
87+
print(part["text"], end="", flush=True)
88+
else:
89+
self.finish()
90+
print(f"[{label}] {part['text']}", flush=True)
91+
return final
92+
93+
94+
async def run(args):
95+
# Match the agent's execution/readiness budget, allowing quiet long-running tasks.
96+
async with httpx.AsyncClient(
97+
base_url=args.url.rstrip("/"),
98+
timeout=httpx.Timeout(args.timeout, connect=10),
99+
trust_env=False,
100+
) as client:
101+
session_id = await session_id_for(
102+
client, args.app_name, args.user_id, args.session_id
103+
)
104+
print(f"session_id={session_id}", flush=True)
105+
print("Reuse this ID with --session-id for the next turn.", flush=True)
106+
final = False
107+
display_event = EventPrinter()
108+
try:
109+
async for event in stream_events(
110+
client,
111+
app_name=args.app_name,
112+
user_id=args.user_id,
113+
session_id=session_id,
114+
task=args.task,
115+
):
116+
final = display_event(event) or final
117+
finally:
118+
display_event.finish()
119+
if not final:
120+
raise RuntimeError(
121+
"Stream ended without a final answer; execution is unconfirmed. Do not automatically resubmit."
122+
)
123+
124+
125+
def main():
126+
parser = argparse.ArgumentParser(description=__doc__)
127+
parser.add_argument("task", help="Text task to execute in the sandbox")
128+
parser.add_argument("--url", default="http://127.0.0.1:8000")
129+
parser.add_argument("--app-name", default="remote_sandbox_demo")
130+
parser.add_argument("--user-id", default="demo_user")
131+
parser.add_argument("--session-id", help="Resume an existing session for this user")
132+
parser.add_argument("--timeout", type=float, default=1500)
133+
args = parser.parse_args()
134+
if not args.task.strip():
135+
parser.error("task must contain text")
136+
if args.timeout <= 0:
137+
parser.error("timeout must be positive")
138+
try:
139+
asyncio.run(run(args))
140+
except (httpx.HTTPError, RuntimeError, ValueError) as exc:
141+
print(f"Request failed: {exc}", file=sys.stderr)
142+
raise SystemExit(1) from None
143+
except KeyboardInterrupt:
144+
raise SystemExit(130) from None
145+
146+
147+
if __name__ == "__main__":
148+
main()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Serve the coordinator and remote sandbox child through AgentKit's HTTP/SSE APIs."""
2+
3+
import argparse
4+
5+
from pathlib import Path
6+
7+
from dotenv import load_dotenv
8+
9+
10+
def main():
11+
parser = argparse.ArgumentParser(description=__doc__)
12+
parser.add_argument("--host", default="127.0.0.1")
13+
parser.add_argument("--port", type=int, default=8000)
14+
args = parser.parse_args()
15+
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
16+
17+
from agent import root_agent
18+
from agentkit.apps import AgentkitAgentServerApp
19+
20+
server = AgentkitAgentServerApp(agent=root_agent)
21+
server.run(host=args.host, port=args.port)
22+
23+
24+
if __name__ == "__main__":
25+
main()

0 commit comments

Comments
 (0)