@qwen-code/arena-agent 0.0.7 → 0.0.8-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/SDK.md ADDED
@@ -0,0 +1,521 @@
1
+ # Arena TypeScript SDK 使用指南
2
+
3
+ Arena SDK 是 `arena serve` 的 TypeScript 客户端,支持 Node.js 20+ 和现代
4
+ 浏览器。它封装了 session 创建与恢复、token 刷新、SSE 流式事件、附件、
5
+ `ask_user`、取消和历史读取。
6
+
7
+ ## 1. 启动 Arena 服务
8
+
9
+ SDK 不直接在客户端运行模型和工具;需要先启动 Arena 服务:
10
+
11
+ ```bash
12
+ npm install
13
+ npm run build
14
+ arena serve
15
+ ```
16
+
17
+ 默认地址是 `http://127.0.0.1:3000`。模型和工具密钥配置在 Arena 服务端,
18
+ 不要放进使用 SDK 的浏览器代码。
19
+
20
+ ## 2. 安装与导入
21
+
22
+ 发布到 npm 后:
23
+
24
+ ```bash
25
+ npm install arena-codeagent-harness
26
+ ```
27
+
28
+ 从本地 tarball 安装:
29
+
30
+ ```bash
31
+ npm pack
32
+ npm install /path/to/arena-codeagent-harness-0.2.0.tgz
33
+ ```
34
+
35
+ SDK 使用 ESM:
36
+
37
+ ```ts
38
+ import {
39
+ ArenaClient,
40
+ ArenaApiError,
41
+ type ArenaCredentials,
42
+ type ArenaInputRequest,
43
+ type ArenaServerEvent,
44
+ } from "arena-codeagent-harness";
45
+ ```
46
+
47
+ ## 3. 三个核心概念
48
+
49
+ - `ArenaClient`:连接一个 Arena 服务。
50
+ - `ArenaSession`:一段持久对话及其独立 workspace。
51
+ - Turn:从一条用户消息开始,到 Agent 成功、失败或被取消为止。
52
+
53
+ 同一个 session 可以运行多个 Turn。历史、文件和 workspace 会在这些 Turn
54
+ 之间保留。
55
+
56
+ ## 4. 最短可用示例
57
+
58
+ ```ts
59
+ import { ArenaClient } from "arena-codeagent-harness";
60
+
61
+ const arena = new ArenaClient({
62
+ baseUrl: "http://127.0.0.1:3000",
63
+ });
64
+
65
+ const session = await arena.sessions.create();
66
+ const result = await session.run({
67
+ input: "创建一个 hello.html landing page",
68
+ });
69
+
70
+ console.log(result.status);
71
+ console.log(result.text);
72
+ console.log(result.files);
73
+ ```
74
+
75
+ `run()` 会收集完整 Turn,适合脚本、服务端任务和不需要逐字渲染的场景。
76
+
77
+ 返回值主要包含:
78
+
79
+ ```ts
80
+ {
81
+ status: "success" | "error" | "unknown";
82
+ text: string;
83
+ reasoning: string;
84
+ messages: Message[];
85
+ files: Record<string, unknown>[];
86
+ events: ArenaServerEvent[];
87
+ error?: string;
88
+ }
89
+ ```
90
+
91
+ 如果只需要一次调用,也可以让 client 自动创建 session:
92
+
93
+ ```ts
94
+ const result = await arena.run({
95
+ input: "分析这个项目",
96
+ });
97
+
98
+ console.log(result.text);
99
+ console.log(result.session.credentials);
100
+ ```
101
+
102
+ ## 5. Credential、token 与 resume
103
+
104
+ 创建 session 后,服务端返回一组 session credential:
105
+
106
+ ```ts
107
+ {
108
+ sessionId: string;
109
+ token: string;
110
+ expiresAt?: string;
111
+ }
112
+ ```
113
+
114
+ 这里的 token 只用于访问对应 Arena session,不是模型 API Key。
115
+
116
+ ### 不需要跨进程恢复
117
+
118
+ 什么都不用保存:
119
+
120
+ ```ts
121
+ const arena = new ArenaClient({
122
+ baseUrl: "http://127.0.0.1:3000",
123
+ });
124
+
125
+ const session = await arena.sessions.create();
126
+ ```
127
+
128
+ ### 保存最新 credential
129
+
130
+ SDK 创建 session 或刷新 token 后会调用 `onCredentials`。应用应该始终覆盖
131
+ 旧值:
132
+
133
+ ```ts
134
+ let savedCredentials: ArenaCredentials | undefined;
135
+
136
+ const arena = new ArenaClient({
137
+ baseUrl: "http://127.0.0.1:3000",
138
+ onCredentials(credentials) {
139
+ savedCredentials = credentials;
140
+ },
141
+ });
142
+ ```
143
+
144
+ Node.js 服务通常应把它保存到数据库或自己的加密存储。浏览器演示可以保存到
145
+ `localStorage`,但生产环境需要评估 XSS 风险:
146
+
147
+ ```ts
148
+ const arena = new ArenaClient({
149
+ baseUrl: "http://127.0.0.1:3000",
150
+ onCredentials(credentials) {
151
+ localStorage.setItem(
152
+ "arena-session",
153
+ JSON.stringify(credentials),
154
+ );
155
+ },
156
+ });
157
+ ```
158
+
159
+ ### 恢复 session
160
+
161
+ ```ts
162
+ const credentials = JSON.parse(
163
+ localStorage.getItem("arena-session")!,
164
+ ) as ArenaCredentials;
165
+
166
+ const session = arena.sessions.resume(credentials);
167
+ const history = await session.getHistory();
168
+
169
+ console.log(history.messages);
170
+ console.log(history.activePrompt);
171
+ ```
172
+
173
+ `getHistory()` 同时会设置 SSE 续传需要的 `lastEventId`。恢复后再调用
174
+ `run()`、`stream()` 或 `connect()` 即可继续。
175
+
176
+ 当前服务端还没有用户账号维度的 `sessions.list()`/`sessions.open()`。
177
+ 跨设备登录应由服务端把 session 绑定到可信的 `userId`,并在用户登录后重新
178
+ 签发短期 session token,而不是在设备之间同步旧 token。
179
+
180
+ ## 6. 流式模式
181
+
182
+ ### 单个 Turn:`stream()`
183
+
184
+ `stream()` 会自动建立 SSE、上传附件、发送消息,并在本次 Turn 完成后停止:
185
+
186
+ ```ts
187
+ for await (const event of session.stream({
188
+ input: "创建一个 landing page",
189
+ })) {
190
+ switch (event.type) {
191
+ case "text-delta":
192
+ process.stdout.write(String(event.delta || ""));
193
+ break;
194
+
195
+ case "reasoning-delta":
196
+ console.log("Reasoning:", event.delta);
197
+ break;
198
+
199
+ case "tool-input-start":
200
+ console.log("Tool:", event.toolName);
201
+ break;
202
+
203
+ case "tool-output-available":
204
+ console.log("Tool output:", event.output);
205
+ break;
206
+
207
+ case "data-workspace":
208
+ console.log("Workspace file:", event.data);
209
+ break;
210
+
211
+ case "trigger:turn-complete":
212
+ console.log("Turn status:", event.status);
213
+ break;
214
+ }
215
+ }
216
+ ```
217
+
218
+ `stream()` 默认在连接断开后使用最近的 SSE event ID 续传。
219
+
220
+ ### 多个 Turn 共用连接:`connect()` + `send()`
221
+
222
+ Web UI 一般建立一条长期连接,然后发送多个 Turn:
223
+
224
+ ```ts
225
+ const events = session.connect({
226
+ onEvent(event) {
227
+ renderEvent(event);
228
+ },
229
+ onStateChange(state) {
230
+ console.log("SSE:", state);
231
+ },
232
+ onError(error) {
233
+ console.error("SSE error:", error);
234
+ },
235
+ });
236
+
237
+ await events.ready;
238
+
239
+ await session.send({ input: "第一个问题" });
240
+ // 收到 trigger:turn-complete 后才能发送下一条。
241
+
242
+ events.close();
243
+ ```
244
+
245
+ 也可以把持久连接作为 async iterable 消费:
246
+
247
+ ```ts
248
+ const events = session.connect();
249
+ await events.ready;
250
+
251
+ void session.send({ input: "分析 workspace" });
252
+
253
+ for await (const event of events) {
254
+ console.log(event);
255
+ if (event.type === "trigger:turn-complete") break;
256
+ }
257
+
258
+ events.close();
259
+ ```
260
+
261
+ `events.close()` 只关闭本地 SSE,不会取消服务端正在运行的 Turn。
262
+
263
+ ### 常用事件
264
+
265
+ | 事件 | 含义 |
266
+ |---|---|
267
+ | `start` | Turn 已开始 |
268
+ | `reasoning-start/delta/end` | reasoning 流 |
269
+ | `text-start/delta/end` | 最终文本流 |
270
+ | `tool-input-start/delta/available` | 模型正在生成工具调用 |
271
+ | `tool-output-available` | 工具执行结果 |
272
+ | `data-agent-status` | 当前模型或工具阶段 |
273
+ | `data-user-input-request` | `ask_user` 正在等待回答 |
274
+ | `data-user-input-submitted` | 回答已经提交 |
275
+ | `data-workspace` | workspace 文件可用 |
276
+ | `data-present-file` | 应立即展示的文件 |
277
+ | `data-messages` | 最新完整消息历史 |
278
+ | `trigger:turn-complete` | Turn 已结束 |
279
+ | `error` | Agent 或协议错误 |
280
+
281
+ ## 7. `ask_user`
282
+
283
+ ### 回调自动回答
284
+
285
+ 适合 CLI、后端服务或已有交互回调的应用:
286
+
287
+ ```ts
288
+ const result = await session.run({
289
+ input: "帮我设计一个网站",
290
+ onInputRequired: async (request) => {
291
+ const question = request.questions[0];
292
+ if (!question) return [];
293
+ const option = question.options[0];
294
+ if (!option) return [];
295
+
296
+ return [
297
+ {
298
+ questionId: question.id,
299
+ optionId: option.id,
300
+ },
301
+ ];
302
+ },
303
+ });
304
+ ```
305
+
306
+ 如果要提交自由文本:
307
+
308
+ ```ts
309
+ return [
310
+ {
311
+ questionId: question.id,
312
+ customResponse: "使用深色极简风格",
313
+ },
314
+ ];
315
+ ```
316
+
317
+ ### UI 手动回答
318
+
319
+ 不传 `onInputRequired`,收到事件后渲染自己的 UI:
320
+
321
+ ```ts
322
+ for await (const event of session.stream({
323
+ input: "帮我设计一个网站",
324
+ })) {
325
+ if (event.type !== "data-user-input-request") continue;
326
+
327
+ const request = event.data as ArenaInputRequest;
328
+ const answers = await showQuestionDialog(request.questions);
329
+
330
+ await session.respondToInput(request.requestId, answers);
331
+ }
332
+ ```
333
+
334
+ 答案格式:
335
+
336
+ ```ts
337
+ type ArenaAnswer = {
338
+ questionId: string;
339
+ optionId?: string;
340
+ customResponse?: string;
341
+ };
342
+ ```
343
+
344
+ ## 8. 取消
345
+
346
+ 取消服务端正在运行的 Turn:
347
+
348
+ ```ts
349
+ const cancelled = await session.cancel();
350
+ console.log(cancelled);
351
+ ```
352
+
353
+ 使用 `AbortController`:
354
+
355
+ ```ts
356
+ const controller = new AbortController();
357
+
358
+ setTimeout(() => controller.abort(), 5_000);
359
+
360
+ for await (const event of session.stream({
361
+ input: "执行一个复杂任务",
362
+ signal: controller.signal,
363
+ })) {
364
+ console.log(event);
365
+ }
366
+ ```
367
+
368
+ `stream()` 的 signal 被 abort 时,SDK 默认也会调用服务端取消接口。设置
369
+ `cancelOnAbort: false` 可以只停止本地消费:
370
+
371
+ ```ts
372
+ for await (const event of session.stream({
373
+ input: "继续在服务端运行",
374
+ signal: controller.signal,
375
+ cancelOnAbort: false,
376
+ })) {
377
+ console.log(event);
378
+ }
379
+ ```
380
+
381
+ 被取消的 partial assistant/tool 历史会由服务端保存。下次发送消息时,服务端
382
+ 会注入一次 interruption notice,Agent 可以继续处理。
383
+
384
+ ## 9. 附件与 workspace 文件
385
+
386
+ ### 浏览器 `File`
387
+
388
+ ```ts
389
+ const file = inputElement.files![0]!;
390
+
391
+ const result = await session.run({
392
+ input: "总结这个文件",
393
+ files: [file],
394
+ });
395
+ ```
396
+
397
+ ### Node.js
398
+
399
+ ```ts
400
+ import { readFile } from "node:fs/promises";
401
+
402
+ const bytes = await readFile("./report.pdf");
403
+
404
+ const result = await session.run({
405
+ input: "总结这份报告",
406
+ files: [
407
+ {
408
+ name: "report.pdf",
409
+ data: new Uint8Array(bytes),
410
+ type: "application/pdf",
411
+ },
412
+ ],
413
+ });
414
+ ```
415
+
416
+ `run()`、`stream()` 和 `send()` 都会先上传 `files`,再把附件引用放入消息。
417
+
418
+ ### 分步上传
419
+
420
+ ```ts
421
+ const attachment = await session.upload({
422
+ name: "notes.txt",
423
+ data: new TextEncoder().encode("Project notes"),
424
+ type: "text/plain",
425
+ });
426
+
427
+ await session.send({
428
+ input: "整理这份笔记",
429
+ attachments: [attachment],
430
+ });
431
+ ```
432
+
433
+ ### 下载 workspace 文件
434
+
435
+ ```ts
436
+ const blob = await session.download("hello.html");
437
+ console.log(await blob.text());
438
+ ```
439
+
440
+ `workspaceUrl(path)` 可以生成文件 URL。跨域浏览器场景下,直接打开 URL
441
+ 不一定携带 Authorization header,应优先使用 `download()`,或让应用服务端
442
+ 代理下载。
443
+
444
+ ## 10. 错误处理
445
+
446
+ 非 2xx API 响应会抛出 `ArenaApiError`:
447
+
448
+ ```ts
449
+ try {
450
+ await session.send({ input: "hello" });
451
+ } catch (error) {
452
+ if (error instanceof ArenaApiError) {
453
+ console.error(error.status);
454
+ console.error(error.body);
455
+ } else {
456
+ throw error;
457
+ }
458
+ }
459
+ ```
460
+
461
+ 流式 Agent 错误也可能作为 `error` 和 `trigger:turn-complete` 事件出现。
462
+ `run()` 会把最终状态放在 `result.status` 和 `result.error`。
463
+
464
+ ## 11. 浏览器直接导入
465
+
466
+ `arena serve` 会在 `/sdk.js` 提供相同的 ESM:
467
+
468
+ ```html
469
+ <script type="module">
470
+ import { ArenaClient } from "/sdk.js";
471
+
472
+ const arena = new ArenaClient();
473
+ const session = await arena.sessions.create();
474
+ console.log(await session.run({ input: "你好" }));
475
+ </script>
476
+ ```
477
+
478
+ 同源使用时 `baseUrl` 可以省略。跨域时传入完整 Arena 服务地址,并正确配置
479
+ CORS 和认证策略。
480
+
481
+ ## 12. API 速查
482
+
483
+ ### `ArenaClient`
484
+
485
+ ```ts
486
+ arena.getConfig()
487
+ arena.createSession(options?)
488
+ arena.sessions.create(options?)
489
+ arena.resumeSession(credentials)
490
+ arena.sessions.resume(credentials)
491
+ arena.run(streamOptions, createSessionOptions?)
492
+ ```
493
+
494
+ ### `ArenaSession`
495
+
496
+ ```ts
497
+ session.credentials
498
+ session.getHistory()
499
+ session.refreshToken()
500
+ session.run(options)
501
+ session.stream(options)
502
+ session.connect(options?)
503
+ session.send(options)
504
+ session.upload(file)
505
+ session.download(path)
506
+ session.workspaceUrl(path)
507
+ session.respondToInput(requestId, answers)
508
+ session.cancel()
509
+ ```
510
+
511
+ ### `ArenaEventStream`
512
+
513
+ ```ts
514
+ await events.ready
515
+ await events.closed
516
+ events.close()
517
+
518
+ for await (const event of events) {
519
+ // ...
520
+ }
521
+ ```
package/package.json CHANGED
@@ -1,17 +1,30 @@
1
1
  {
2
2
  "name": "@qwen-code/arena-agent",
3
- "version": "0.0.7",
3
+ "version": "0.0.8-dev.0",
4
4
  "description": "A TypeScript CodeAgent harness reconstructed from Arena message and tool traces.",
5
5
  "type": "module",
6
- "types": "./dist/sdk.d.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
7
8
  "exports": {
8
9
  ".": {
9
- "types": "./dist/sdk.d.ts",
10
- "import": "./dist/sdk.js"
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
11
12
  },
12
13
  "./sdk": {
13
14
  "types": "./dist/sdk.d.ts",
14
15
  "import": "./dist/sdk.js"
16
+ },
17
+ "./agent": {
18
+ "types": "./dist/agent.d.ts",
19
+ "import": "./dist/agent.js"
20
+ },
21
+ "./tools": {
22
+ "types": "./dist/tools.d.ts",
23
+ "import": "./dist/tools.js"
24
+ },
25
+ "./types": {
26
+ "types": "./dist/types.d.ts",
27
+ "import": "./dist/types.js"
15
28
  }
16
29
  },
17
30
  "bin": {
@@ -19,6 +32,7 @@
19
32
  },
20
33
  "files": [
21
34
  "dist",
35
+ "docs/SDK.md",
22
36
  "web",
23
37
  "README.md",
24
38
  ".env.example"
@@ -28,13 +42,13 @@
28
42
  },
29
43
  "scripts": {
30
44
  "generate:canonical-data": "node scripts/generate-canonical-data.mjs",
31
- "prebuild": "npm run generate:canonical-data",
45
+ "prebuild": "pnpm run generate:canonical-data",
32
46
  "build": "tsc -p tsconfig.json && tsc -p tsconfig.web.json",
33
47
  "dev": "tsx src/cli.ts serve",
34
48
  "tui": "tsx src/cli.ts tui",
35
- "prepack": "npm run build",
49
+ "prepack": "pnpm run build",
36
50
  "start": "node dist/cli.js serve",
37
- "pretest": "npm run build",
51
+ "pretest": "pnpm run build",
38
52
  "test": "tsx --test test/*.test.ts",
39
53
  "test:tool-replay": "ARENA_TOOL_REPLAY_TESTS=1 tsx --test test/tool-replay.test.ts",
40
54
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.web.json --noEmit"