恢复室Recovery Room
凌晨三点,一笔退款争议进了队列。没有人在线。你的 agent 要自己查订单、读政策、决定退不退;超过阈值必须等人批;批了之后要写三处副作用;worker 中途被杀,第二天要能接着跑而不是重跑一遍——而且绝不能把已经退过的钱再退一次。
At 3 a.m. a refund dispute lands in the queue and nobody is online. Your agent looks up the order, reads the policy and decides. Past a threshold it must wait for a human. Afterwards it writes three side effects. The worker dies mid-turn; the next morning it must resume rather than restart — and must never refund the same money twice.
你要交付什么What you ship
一个真的能跑的 AgentScope 服务:FastAPI 会话、持久化状态、权限闸门、人工审批挂起与幂等恢复、预算封顶、轨迹回归评测,外加一层真实鉴权。每个模块都用一条会失败的断言验收,不是打勾。
A service that actually runs: FastAPI sessions, durable state, a permission gate, parked human approval with idempotent resume, a budget ceiling, trajectory regression, plus a real auth layer. Every module is graded by an assertion that can fail, not a tick.
这条轨道只做 Python:Agent 服务(agentscope.app)只存在于 Python 包里。所以这里每个模块都由真实断言判题,而不是造一个只能假装等价的 Java 镜像。This track is Python-only on purpose: the Agent Service (agentscope.app) lives only in the Python package, so every module here is graded by a real assertion instead of a Java mirror that would have to fake one.
模块阶梯The ladder
每个模块要么加一块积木,要么拆一次台;判题通过才会解锁下一个。Each module either adds one block or breaks one promise, and unlocks the next when its check passes.
为什么选这个项目Why this project
下面每一行都是 agent 论坛与事故复盘里反复出现的抱怨。右列标出由哪个模块用断言接住它。Every row is a complaint that recurs in agent forums and incident write-ups. The right column names the module that grades it.
| 真实问题Reported problem | 判题模块Graded in |
|---|---|
「agent 跑着跑着就没了」——长任务没有可恢复状态,重启等于从头再来,而副作用已经做了一半。 "the job just disappeared" — long tasks have no resumable state, so a restart means starting over while the side effects are already half done. | C1C2C5 |
「审批做成了一段话」——模型礼貌地回复"已转人工",系统里没有任何 pending 记录,于是没人真的被叫来批。 "approval is a paragraph" — the model replies "escalated to a human" while no pending record exists anywhere, so nobody is actually paged. | C2C4C5 |
「钱退了两遍」——工具重试、消息重放、worker 被杀后恢复,三条路径都会让同一个 tool_call 再执行一次,而幂等键没人存。 "the refund happened twice" — tool retries, stream replays and a resumed worker all re-run the same tool call, and nobody stored an idempotency key. | C5 |
「一个 tool 挂了 40 万 token」——失控循环没有上限,账单在第二天才被发现。 "one tool call burned 400k tokens" — the loop had no ceiling and the bill arrived the next day. | C6 |
「我们内部先随便部署了一下」——身份来自一个可伪造的请求头,沙箱就是宿主 shell,两者叠在一起是远程代码执行。 "we shipped it internally first" — identity came from a forgeable request header and the sandbox was the host shell; stacked, that is remote code execution. | C7C8 |
装配台The assembly bench
create_app 吃这八个槽位。每个槽挑一块积木,调用会自己拼出来。放错槽位会被拦;能装上但依然危险的那一类,也会被点名。create_app takes these eight slots. Pick a block per slot and the call builds itself. A block that does not match its contract gets flagged; so does a wiring that compiles and is still unsafe.
storage=
StorageBase
必填required
托管 agent、会话、凭据与消息。没有它,进程一重启,服务就没有任何记忆。Owns agents, sessions, credentials and messages. Without it the service has no memory across process restarts.
message_bus=
MessageBus
必填required
负责跨会话收件箱投递与空闲会话唤醒。它与存储刻意分离,所以传输后端和持久化后端可以是两套不同的系统。Carries cross-session inbox delivery and idle-session triggers. Deliberately separate from storage, so the transport backend can differ from the persistence backend.
workspace_manager=
WorkspaceManagerBase
必填required
决定工具到底在哪里执行、隔离到什么粒度。每次 chat 运行和每个 /workspace 接口都依赖它。Decides where a tool actually runs and how isolated it is. Every chat run and every /workspace route depends on it.
extra_credentials=
list[Type[CredentialBase]]
选填optional
注册服务可以解析的模型族。放入脚本化凭据后,整条链路可以离线、确定性地跑通——这正是评测能做真的前提。Registers the model families the service may resolve. A scripted credential makes the whole stack runnable offline and deterministic, which is what makes an evaluation honest.
extra_agent_tools=
AgentToolFactory
选填optional
给服务构造的每个 agent 追加自定义工具,业务能力从这里进来。Adds your own tools to every agent the service builds. This is where business capability enters.
resource_access_policy=
ResourceAccessPolicyBase
选填optional
决定调用方能用到哪些被共享的资源,多租户影响面由此收口。Decides which shared resources a caller may use. The multi-tenant blast radius lives here.
channels=
list[Type[ChannelBase]]
选填optional
把服务接到聊天平台上;每个渠道都额外带来依赖与审批面。Connects the service to chat platforms. Each channel brings its own dependency and its own approval surface.
extra_agent_middlewares=
AgentMiddlewareFactory
选填optional
工厂按 (user, agent, session) 返回一组 MiddlewareBase,拦截的是回复循环本身:token 预算、链路追踪、模型路由。它不像工具那样能被模型绕开——因为它从不向模型征求许可。A factory that returns MiddlewareBase objects per (user, agent, session). It intercepts the reply loop itself: token budgets, tracing, routing. Unlike a tool it cannot be talked around, because it never asks the model for permission.
extra_middlewares=
list[Middleware]
选填optional
ASGI 中间件:每个请求在 agent 存在之前都要经过的这一层。身份要在这里被校验,而不是被相信——这也是伪造 X-User-ID 唯一能被拦下的位置。ASGI middlewares, the layer every request passes through before an agent exists. This is where identity gets verified instead of believed — the one place a forged X-User-ID can be stopped.
create_app(…)
依赖阶梯Install ladder
pip install -e ".[service]"
FastAPI、uvicorn、apscheduler 与 ag-ui-protocol 都是可选依赖。不装 service,连 agentscope.app 都 import 不进来。
fastapi, uvicorn, apscheduler and ag-ui-protocol are optional extras. Without service you cannot even import agentscope.app.
pip install -e ".[service,storage-sql]"
sqlalchemy[asyncio] + alembic + greenlet。加上本项目自带的脚本化凭据,C1 到 C8 全部不需要 API key,也不需要 Redis。
sqlalchemy[asyncio], alembic and greenlet. With the scripted credential in this lab, C1 through C8 need neither an API key nor a Redis broker.
pip install -e ".[service,storage-sql,storage-redis,workspace-docker]"
只有做 C7 的容器替换实验时才需要;判题本身会在缺依赖时给出明确的跳过原因,不会假装通过。
Only needed for the C7 container swap. Checks that cannot run without it say so explicitly instead of pretending to pass.
判题全部离线:脚本化模型 + 一个 SQLite 文件,不需要 API key、Redis 或 Docker。缺依赖的判题会明确说明并停下,不会白送你一个「通过」。Checks run offline against a scripted model and one SQLite file: no API key, no broker, no Docker daemon. A check that needs a dependency you lack says so and stops; it never reports a pass it did not earn.