arena-agent
v0.0.1
Published
A TypeScript CodeAgent harness reconstructed from Arena message and tool traces.
Readme
Arena CodeAgent Harness
这是一个根据 message.json 与 tools.json 逆向实现的 TypeScript CodeAgent
harness。运行时只依赖 Node.js,不再依赖 Python:
arena -p ...:headless 单轮入口,支持文本、完整 JSON 和 NDJSON 流。arena tui:终端多轮对话,实时输出并自动保存、恢复历史。arena serve:HTTP daemon、Web UI 与双通道 SSE 实时接口。- Agent loop:流式 assistant → tool call → tool result → assistant。
- 11 个工具逐字使用
tools.json的 Draft-07 schema。
安装
要求 Node.js 20.16+:
npm install
npm run build
npm linknpm run build 编译 src/**/*.ts;npm link 将本项目的 arena CLI
加入当前用户的 PATH。开发时可用:
npm run build
npm run typecheck
npm test模型
TypeScript 运行时通过 OpenAI-compatible 流协议访问模型。LiteLLM 官方提供 Python SDK 或独立 Proxy,因此纯 TypeScript 版本使用 LiteLLM Proxy 作为多 provider 网关:
export LITELLM_PROXY_URL=http://127.0.0.1:4000/v1
export LITELLM_API_KEY=sk-litellm
export ARENA_MODEL=anthropic/claude-sonnet-4-20250514设置 LITELLM_PROXY_URL 时,模型名会原样发送给 LiteLLM。没有 Proxy 时,
也支持直连 OpenAI、Anthropic OpenAI-compatibility 与 Gemini
OpenAI-compatibility 接口:
# OpenAI
export ARENA_MODEL=openai/gpt-4.1-mini
export OPENAI_API_KEY=...
# Anthropic
export ARENA_MODEL=anthropic/claude-sonnet-4-20250514
export ANTHROPIC_API_KEY=...
# Gemini
export ARENA_MODEL=gemini/gemini-2.5-pro
export GEMINI_API_KEY=...任何 OpenAI-compatible 服务都可通过 ARENA_API_BASE、ARENA_API_KEY
直连。当前 DashScope 配置示例:
export ARENA_MODEL=openai/qwen-latest-series-invite-beta-v92
export ARENA_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1
export ARENA_API_KEY=...
export ARENA_ENABLE_THINKING=true
export ARENA_THINKING_BUDGET=81920
export ARENA_MAX_TOKENS=131072
export ARENA_CONTEXT_WINDOW=1000000
export ARENA_CONTEXT_COMPACTION_RATIO=0.8
export ARENA_COMPACTION_SUMMARY_MAX_TOKENS=8192Harness 会在每次模型调用前估算消息、reasoning、工具 schema 和图片占用;达到
ARENA_CONTEXT_WINDOW × ARENA_CONTEXT_COMPACTION_RATIO(默认 80%)时,调用
当前模型把已完成的旧回合生成结构化续作摘要。旧前缀会被替换为与 Arena trace
一致的 leading <arena-system-message>,当前用户回合及其后的 tool-call / result
链保持原样。摘要重点保留目标、硬约束、发现、已完成/未完成事项、下一步和相关
文件路径;摘要调用默认关闭 thinking,输出上限由
ARENA_COMPACTION_SUMMARY_MAX_TOKENS 控制(默认 8192)。
ARENA_CONTEXT_WINDOW 应设置为所用模型的真实上下文窗口;默认 1000000 是
当前 trace 模型的暂定值。token 数目前是本地保守估算,并非 provider tokenizer
的精确计数。如果摘要请求失败,Harness 会保留原历史并继续正常模型请求,避免
摘要服务的瞬时故障直接中断任务。
模型请求遇到网络错误、HTTP 408/409/429 或 5xx 时会自动重试,默认最多重试
5 次。没有 Retry-After 时使用带随机抖动的指数退避;可通过
ARENA_MODEL_MAX_RETRIES、ARENA_MODEL_RETRY_BASE_MS 和
ARENA_MODEL_RETRY_MAX_MS 调整。设 ARENA_MODEL_MAX_RETRIES=0 可关闭。
流式响应开始产出之后发生的网络中断(如 socket 断开、连接重置、
fetch failed/terminated)同样会整轮重试:请求本身是幂等的,会用相同的
消息历史重新发起,累加器每次重试都会重置,因此写入 history.json 的是一次
干净完整的 assistant 输出。重试沿用上面同一组配置,并在事件流中先发出一个
model.retry 事件(TUI 会显示「reconnecting after a network error」提示)。
用户主动取消(abort)不会被当作可重试的网络故障。
Agent turn 没有 step 数或整轮墙钟时长上限,会持续执行到模型正常结束、
请求用户输入、发生不可恢复错误,或用户主动取消。工具自身仍可定义单次调用
的超时。每个完整的 assistant/tool step 都会原子保存到 history.json;
进程被外部超时终止时,已完成步骤仍可恢复,最多丢失当时正在执行的一步。
Headless
arena -p 'create a hello.html landing page'
arena -p 'create a hello.html landing page' --output stream-json每次 headless 执行都会生成一个 UUID session ID,并将完整历史自动写入与 daemon 相同的目录层级和 JSON 格式:
.arena/tenants/<tenantId>/sessions/<sessionId>/history.json输出事件及 JSON 结果中的 session_id 就是路径里的 <sessionId>。
history.json 的格式为 {"messages":[...]},即使结果为 needs_input
或 error 也会保存。需要跨命令续写同一份显式历史时,可另外传入
--history-file path;该文件会被加载并在执行结束后更新,同时本次自动
session 目录中的历史快照仍会保留。
需要让多次 -p 作为同一个 session 连续对话时,重复传入相同的
--session-id:
arena -p '先分析项目结构' --session-id my-session
arena -p '继续实现刚才的方案' --session-id my-session首次执行会创建对应的 history.json;后续执行会先加载已有消息,追加本轮
消息,并在初始 user 消息及每个完整 assistant/tool step 后原子重写完整
JSON 文件。--session-id 与 --history-file 不能同时使用。
需要附带用户上传文件(尤其是参考图片)时,使用可重复的 --attach
(别名 --image),值可以是本地路径或 http(s) URL:
arena -p 'redraw this photo with the reference face' \
--attach ./my_gf_reference.png \
--attach 'https://example.com/uploads/photo.jpg'每个附件会被保存到工作区 uploads/(模型内可通过
/home/user/uploads/<name> 访问;重名自动加 -2、-3 后缀,URL 无扩展名
时按图片字节特征补全扩展名)。本轮 user message 与 daemon 附件上传共用同
一构造逻辑,并使用与 Arena trace 相同的原生部件格式:图片部件
{"image": ...} 在前,随后是 prompt 文本 {"text": ...},最后追加一条
<arena-system-message>,列出保存到 /home/user/uploads/ 的文件名。单条
消息最多 10 个附件,单个附件上限 20 MB、总量上限 50 MB,与 daemon 一致。
多模态 user 消息(附件、图片 read_file 回灌、中断通知)在历史与事件流中
一律使用 Arena 原生部件 {"image": ...} / {"text": ...},与 baseline
trace 逐字段同构;仅在向模型发起 chat-completions 请求的边界处转换为
OpenAI 风格的 image_url 部件。--attach 传 URL 时 image 直接记录该
URL(与 baseline 引用附件的方式一致,模型服务端会自行下载;重放历史要求
该 URL 仍可访问);本地文件没有可引用的 URL,image 存自包含的 base64
data-URI。
stream-json 每行一个 JSON 对象。start、message、result 事件中的
messages 与输入样例保持相同契约:
- system/user:
role+content;图片读取后会持久化一条 multimodal user message - assistant:
role+content+reasoning_content,工具轮含tool_calls - tool:
role+tool_call_id+ JSON 字符串content
assistant.delta 实时提供 content、reasoning 与 tool-call 参数碎片;
model.start、model.complete、model.retry、tool.start、tool.complete
标记阶段。最后的 result 带完整可重放消息。
ask_user 需要交互式前端(TUI/WebUI)来收集选项,因此 headless(-p)与
无会话的 /query 端点不会把 ask_user 提供给模型——只有 TUI 和 WebUI
交互会话(都注入了 ask-user handler)才会开放该工具。这样 headless 不会再
卡在 needs_input 上阻塞训练 worker。作为兜底,若续写的历史或模型幻觉在无
handler 的会话里仍调用了 ask_user,工具会返回一个 error 结果(提示模型用
合理默认值继续),而不是 needs_input。
图片 read_file 的 tool result 使用占位文本;真实图片作为紧随工具结果组的
multimodal user message 注入并写入历史,因此后续重放不依赖原图片仍然存在。
TUI
构建后直接进入终端多轮对话:
arena tui
# 指定 workspace、模型和可恢复的 session
arena tui --workspace . --model mock/ask-user --session-id my-sessionworkspace 默认是启动命令时的当前目录(可用 --workspace 覆盖)。TUI 会把
实际 workspace root 和 cwd 动态注入 system prompt 与工具 schema;因此本机
运行时使用本机路径,部署进沙箱后会自然使用沙箱内的实际路径,不需要切换模式。
TUI 没有内置文件 viewer,因此不会向模型提供 present_file;模型会直接报告
生成文件的实际路径。
assistant 的 reasoning、正文和工具调用状态会边生成边输出。每轮结束都会把
完整消息原子写入与 headless、daemon 相同的 history.json;再次传入相同的
--session-id 会恢复上下文。也可改用工作区内的显式
--history-file path,两者不能同时使用。
TUI 会直接接管 ask_user 工具,在当前轮中显示选项并继续执行。内置命令:
/help:显示帮助/history:打印当前会话/session:打印 session ID 和历史文件位置/exit(或/quit):保存并退出
Ctrl-C 在生成期间取消当前轮;在输入提示符处按下则退出。开发模式也可用
npm run tui -- --model mock/landing-page。
TypeScript SDK
SDK 同时支持 Node.js 20+ 与浏览器。它调用 arena serve 的 HTTP 接口,并
负责 session credential、token 刷新、SSE 解析与断线续传:
import {
ArenaClient,
type ArenaInputRequest,
} from "arena-codeagent-harness";
const arena = new ArenaClient({
baseUrl: "http://127.0.0.1:3000",
onCredentials(credentials) {
// 应用负责把 credentials 持久化到自己的安全存储。
saveCredentials(credentials);
},
});
const session = await arena.sessions.create();
const result = await session.run({
input: "创建一个 hello.html",
files: [
{
name: "notes.txt",
data: new Blob(["project notes"], { type: "text/plain" }),
},
],
onInputRequired: async (request) => [
{
questionId: request.questions[0].id,
optionId: request.questions[0].options[0].id,
},
],
});
console.log(result.text, result.files);stream() 返回一个 async iterable,适合逐事件消费;没有
onInputRequired 时,可以在收到 data-user-input-request 后显式调用
respondToInput():
for await (const event of session.stream({ input: "设计一个网站" })) {
if (event.type === "text-delta") process.stdout.write(String(event.delta));
if (event.type === "data-user-input-request") {
const request = event.data as ArenaInputRequest;
await session.respondToInput(request.requestId, [
{ questionId: "style", optionId: "minimal" },
]);
}
}持久连接、恢复和取消分别使用:
const events = session.connect({
onEvent: (event) => render(event),
onStateChange: (state) => showConnectionState(state),
});
await events.ready;
await session.send({ input: "分析 workspace" });
await session.cancel(); // 只取消服务端当前 turn,不销毁 session
events.close(); // 只关闭本地 SSE 连接
const resumed = arena.sessions.resume(savedCredentials);
const history = await resumed.getHistory();
const next = await resumed.run({ input: "继续" });也可以用 arena.run() 创建临时 session 并一次性收集结果。SDK 导出完整
TypeScript 类型,服务端还会在 /sdk.js 提供同一份浏览器 ESM。Web UI
本身也通过这套 SDK 调用服务端。
完整说明见 docs/SDK.md。
Daemon 与 Web UI
arena serve
# 或
npm start浏览器打开 http://127.0.0.1:3000。页面实时展示 reasoning、工具输入输出、交互问题及 workspace 文件。session ID 由服务端 生成;打开页面或点击 New Chat 时只保留浏览器内的空白草稿,首次发送消息 (包括带附件的消息)时才创建 session。每个 session 的凭证哈希、历史、 workspace 和 artifacts 分别保存在:
.arena/tenants/<tenantId>/sessions/<sessionId>/
session.json
history.json
workspace/
artifacts/ARENA_TENANT_ID 默认是 local,部署到已有认证网关时应设置为网关绑定的
tenant 标识。客户端不能通过传入 session ID 重新领取会话;刷新 token 必须
同时持有该 session 当前的 bearer credential。新 session 默认从基础
--workspace 初始化独立副本(文件系统支持时优先使用 copy-on-write),
并排除 .arena、.git、依赖/构建目录和 .env* 密钥文件;若需要空
workspace,可设置 ARENA_SESSION_WORKSPACE_MODE=empty。
| 接口 | 作用 |
|---|---|
| POST /api/chat/trigger-session | 创建会话并返回短期 token |
| POST /api/chat/trigger-token | 刷新 token |
| POST /ai-proxy/realtime/v1/sessions/:id/in/append | 写入用户消息 |
| GET /ai-proxy/realtime/v1/sessions/:id/out | SSE 输出实时事件 |
| GET /api/chat/sessions/:id/messages | 恢复完整消息历史 |
| POST /api/chat/sessions/:id/uploads | 上传当前消息的附件 |
| POST /api/chat/sessions/:id/input-response | 回答 ask_user |
| POST /api/chat/sessions/:id/cancel | 取消当前 turn |
| GET /api/chat/sessions/:id/workspace/:path | 鉴权获取该 session 的 workspace 文件 |
创建会话时,客户端需要提供用于初始 system message 的本地日期与 IANA 时区; 位置为可选的客户端上下文:
{
"currentDate": "2026-07-23",
"timezone": "Asia/Singapore",
"location": {
"city": "Singapore",
"region": "Singapore",
"country": "SG"
}
}这些值会随 session metadata 持久化,后续 turn 继续复用首次生成的 system message。未提供位置时,prompt 会明确标记位置不可用,不会使用样例中的位置。
附件采用两阶段提交。客户端先将每个文件作为原始请求体上传到 uploads
接口,并通过 URI 编码的 x-file-name header 传递文件名;再把返回的
attachment.path 作为 { "type": "file", "path": "uploads/..." }
part 放入实时消息。每个文件最大 20 MB,每条消息最多 10 个文件、合计
50 MB。服务端会把文件写入该 session 的 /home/user/uploads/ 映射目录,
图片作为 multimodal content 注入,并在 user message 最后追加
arena-system-message 附件清单。附件引用只能指向本 session 的
uploads/,不能读取其他 workspace 路径或其他 session 的文件。
SSE 支持 Last-Event-ID 断线续传。ask_user 会暂停同一个 Agent Promise;
用户提交答案后生成标准 role=tool 消息并继续循环。
取消正在执行的 turn 时,daemon 会保存已经完成的 assistant/tool 消息以及已
流式生成的 partial assistant 内容。下一条用户消息会在最前面注入标准
arena-system-message interruption notice,且仅注入一次;如果取消发生在
ask_user 等待期间,则记录 {"answers":[],"skipped":true} tool result。
该待注入状态写入 session metadata,daemon 重启后仍可恢复。
present_file 会输出独立的 data-present-file 事件并自动打开前端 viewer。
HTML/SVG、图片、音频、视频、PDF 和文本可内嵌预览;Office 文件提供打开或
下载入口。HTML viewer 使用 sandbox="allow-scripts" 与 CSP 网络隔离,
外部脚本、样式和图片不会被加载。
兼容的简单流式接口:
curl -N -X POST \
-H 'Content-Type: text/plain' \
--data 'create a hello.html landing page' \
http://127.0.0.1:3000/query也支持 JSON POST 和 GET /query?q=...;健康检查是 GET /health。
工具
原始规范逐字保存在:
spec/message.jsonspec/tools.json
运行时加载这两个文件,并在执行前使用 AJV 校验原始 Draft-07 schema:
bash、ask_user、edit_file、read_file、fetch_page、web_search、
write_file、image_search、present_file、generate_image、
generate_speech。
web_search、image_search、generate_image、generate_speech 都使用
阿里云百炼 DashScope,并从工作区 .env 读取 DASHSCOPE_API_KEY。默认
调用公共北京地域地址;若使用业务空间专属域名或新加坡地域,可设置
DASHSCOPE_API_BASE(可带或不带 /api/v1)。
fetch_page 使用 Defuddle 将 HTML 正文转换为 Markdown,并使用
pdf-parse 提取 PDF 的前 30 页。响应按 Markdown 边界稳定分块,同一轮内
重复读取后续 chunkIndex 不会重新下载;抓取过程限制重定向、超时与响应
大小,并拒绝 localhost、私网和 link-local 地址。
read_file 可直接提取本地 PDF(前 30 页)、DOCX、PPTX、XLSX 的文字和
表格内容;旧版 DOC、PPT、XLS 会先通过 LibreOffice 转换。默认从 PATH
查找 soffice,也可用 ARENA_SOFFICE_PATH 指定其路径。文档解压和输出
均设有大小上限;当前不做 OCR,扫描版 PDF 只会返回无可提取文字的提示。
可用 ARENA_WEB_SEARCH_MODEL、ARENA_IMAGE_SEARCH_MODEL、
ARENA_IMAGE_MODEL、ARENA_SPEECH_MODEL 覆盖默认模型。图片生成结果和
搜索到的图片会立即下载到工作区;每次图片搜索的结果位于
assets/image-search/<tool-call-id>/,并作为可预览文件显示在 Web UI。
语音生成目前按 DashScope HTTP API 支持 .mp3、.wav 和 .opus 输出。
所有路径工具都限制在 --workspace 内并检查符号链接逃逸。bash 是本机
子进程,不是操作系统安全沙箱;多租户或不可信模型部署应使用容器或 VM。
测试
内置 mock 模型只用于无密钥集成测试:
ARENA_MODEL=mock/landing-page arena -p \
'create a hello.html landing page' --output stream-json
npm test测试覆盖消息契约、工具校验、headless、/query、实时双通道、
ask_user、present_file 和 daemon 优雅退出。
