@migor/agentia 0.6.3 → 0.7.1
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/CHANGELOG.md +250 -1
- package/README.en.md +4 -1
- package/README.md +23 -1
- package/dist/AGENTS.md +211 -24
- package/dist/container/container.d.ts +8 -1
- package/dist/container/container.js +8 -1
- package/dist/container/container.js.map +1 -1
- package/dist/core/run.d.ts +5 -1
- package/dist/core/timeout.d.ts +8 -0
- package/dist/core/timeout.js +14 -3
- package/dist/core/timeout.js.map +1 -1
- package/dist/core/tool.d.ts +64 -0
- package/dist/engine/budget.d.ts +17 -4
- package/dist/engine/budget.js +3 -3
- package/dist/engine/budget.js.map +1 -1
- package/dist/engine/loop.d.ts +7 -0
- package/dist/engine/loop.js +102 -6
- package/dist/engine/loop.js.map +1 -1
- package/dist/engine/spec.d.ts +18 -2
- package/dist/engine/spec.js.map +1 -1
- package/dist/engine/tracer.d.ts +11 -0
- package/dist/engine/tracer.js +25 -12
- package/dist/engine/tracer.js.map +1 -1
- package/dist/engine/turn.d.ts +25 -3
- package/dist/engine/turn.js +63 -4
- package/dist/engine/turn.js.map +1 -1
- package/dist/engine/types.d.ts +28 -1
- package/dist/engine/types.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/anthropic.js +50 -5
- package/dist/integrations/anthropic.js.map +1 -1
- package/dist/integrations/mcp.d.ts +116 -11
- package/dist/integrations/mcp.js +523 -2
- package/dist/integrations/mcp.js.map +1 -1
- package/dist/integrations/metrics.d.ts +15 -0
- package/dist/integrations/metrics.js +114 -77
- package/dist/integrations/metrics.js.map +1 -1
- package/dist/integrations/openai.js +46 -0
- package/dist/integrations/openai.js.map +1 -1
- package/dist/integrations/otlp.js +15 -6
- package/dist/integrations/otlp.js.map +1 -1
- package/dist/runtime/run.d.ts +6 -0
- package/dist/runtime/run.js +23 -0
- package/dist/runtime/run.js.map +1 -1
- package/dist/store/fsStore.d.ts +2 -1
- package/dist/store/fsStore.js +15 -4
- package/dist/store/fsStore.js.map +1 -1
- package/dist/store/sqliteStore.js +7 -5
- package/dist/store/sqliteStore.js.map +1 -1
- package/dist/store/store.d.ts +17 -1
- package/dist/store/store.js +3 -2
- package/dist/store/store.js.map +1 -1
- package/dist/toolkit/asset.d.ts +3 -2
- package/dist/toolkit/asset.js +11 -2
- package/dist/toolkit/asset.js.map +1 -1
- package/dist/toolkit/module.d.ts +3 -2
- package/dist/toolkit/module.js +3 -0
- package/dist/toolkit/module.js.map +1 -1
- package/dist/toolkit/skill.js +46 -8
- package/dist/toolkit/skill.js.map +1 -1
- package/dist/toolkit/subagent.js +26 -1
- package/dist/toolkit/subagent.js.map +1 -1
- package/dist/toolkit/tool.d.ts +6 -0
- package/dist/toolkit/tool.js +1 -0
- package/dist/toolkit/tool.js.map +1 -1
- package/dist/transport/async.d.ts +74 -1
- package/dist/transport/async.js +257 -12
- package/dist/transport/async.js.map +1 -1
- package/dist/transport/http.d.ts +5 -0
- package/dist/transport/http.js +74 -2
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/scheduler.js +33 -2
- package/dist/transport/scheduler.js.map +1 -1
- package/package.json +8 -4
package/dist/AGENTS.md
CHANGED
|
@@ -104,6 +104,7 @@ npx @migor/cli doctor # 静态体检(未登记/悬空/命名/重复
|
|
|
104
104
|
| `schema` | 入参 JSON Schema;传 `fromZod<T>(...)` 可获得签名校验 |
|
|
105
105
|
| `name` | 模型可见的工具名,缺省取方法名(建议 snake_case) |
|
|
106
106
|
| `strict` | 透传给 Anthropic 的 strict 模式(**框架不校验 schema 合规性**) |
|
|
107
|
+
| `approval` | `'required'` = 每次调用先挂起等人工审批(HITL,见 §6.6「人工审批」) |
|
|
107
108
|
|
|
108
109
|
### `@Skill(spec: SkillSpec)`
|
|
109
110
|
|
|
@@ -205,8 +206,10 @@ npx @migor/cli doctor # 静态体检(未登记/悬空/命名/重复
|
|
|
205
206
|
| `session` | 会话持久化 `{ store, id }`:run 前拼历史、成功收尾追加本轮(见 `SessionStore`) |
|
|
206
207
|
| `memory` | 跨 run 记忆 `{ store, keys }`:run 前水合进 blackboard(用户种子优先)、收尾写回;与 `session` 正交(见 `MemoryStore`) |
|
|
207
208
|
| `beforeFlush` | `(trace, result) => void \| Promise<void>`:**sinks 冲刷之前**的最后一笔(run 正常收尾后调一次,抛错被吞)。给「**拿到结果才判得出**的结论」用的缝 —— 典型是 `defineEval` 的 score:等 `app.run` 返回再 `attachScore`,`metricsSink` 早在导出那一刻聚完账,分数就永远进不了指标。读 trace 就够的判断不必用它,写进 sinks 里即可(见 §6 判官配方) |
|
|
209
|
+
| `approvals` | HITL 审批决定(`Record<tool_use_id, ApprovalDecision>`):恢复 `awaiting_approval` 的 run 时传入(异步宿主会自动带,见 §6.6「人工审批」);手工续跑「assistant 结尾带 tool_use」的消息历史时也可直接给 |
|
|
208
210
|
|
|
209
|
-
返回 `AgentRunOutput`:`{ run, result }`。`result` 含 `trace` / `stopReason` / `finalText` / `iterations` / `error` / `typed
|
|
211
|
+
返回 `AgentRunOutput`:`{ run, result }`。`result` 含 `trace` / `stopReason` / `finalText` / `iterations` / `error` / `typed`;
|
|
212
|
+
`stopReason === 'awaiting_approval'`(HITL 挂起)时另有 `suspendedMessages`(完整消息历史,末尾是含未决 tool_use 的 assistant 消息)与 `pendingApprovals`(待决 tool_use_id 列表),未挂起时两者为 `undefined`。
|
|
210
213
|
|
|
211
214
|
---
|
|
212
215
|
|
|
@@ -359,7 +362,7 @@ const app = await createApp({ ... });
|
|
|
359
362
|
| API | 说明 |
|
|
360
363
|
|---|---|
|
|
361
364
|
| `createHttpHandler` | `(req,res)` handler:`POST /run` 同步(带 `Accept: text/event-stream` 则 SSE 流式)、`POST /tasks` 异步、`GET /tasks/:id`、`GET /healthz`;返回值另带 `drain()` 与 `runner` |
|
|
362
|
-
| `AsyncRunner` | 异步任务宿主(`submit` / `poll` / `awaitTask` / `resumePending` / `drain
|
|
365
|
+
| `AsyncRunner` | 异步任务宿主(`submit` / `poll` / `awaitTask` / `approve` / `resumePending` / `drain`);`approve(taskId, decisions, { decidedBy? })` 审批挂起任务(HITL,见 §6.6「人工审批」) |
|
|
363
366
|
| `TaskSink` | 任务完成回调 `{ onFinished(rec) }`,配 `AsyncRunner({ taskSinks })`;抛错被吞 |
|
|
364
367
|
| `HttpException` | 鉴权钩子抛出以自定 HTTP 状态与响应体(抛别的错误一律按 401 处理) |
|
|
365
368
|
| `Scheduler` | 定时触发(`every` / `at`) |
|
|
@@ -369,6 +372,78 @@ const app = await createApp({ ... });
|
|
|
369
372
|
| `SqliteTaskStore` | `node:sqlite` 耐久存储(WAL + busy_timeout) |
|
|
370
373
|
| `RedisTaskStore` | duck-typed Redis 存储(可设 `ttlSeconds`);客户端结构面 `get` / `set` / `del` / `keys`(或 `scanIterator`),外加设 TTL 时必需的 `expire`。`set` **只传两参** —— 尾参的选项形状两家相反:ioredis 认位置参数 `('EX', n)`、node-redis 认对象 `{ EX: n }`,取任何一种都会在另一家上失效(ioredis 会把对象字符串化成 `"[object Object]"` 报语法错;**node-redis 的 `SET` 只声明三个形参,位置参数被静默丢弃**)。所以 TTL 一律走 `expire(key, seconds)`(两家同名同形);设了 `ttlSeconds > 0` 却没给 `expire` 时**构造期抛错**,不静默丢掉 TTL |
|
|
371
374
|
|
|
375
|
+
#### gRPC 宿主(第 4 个宿主,**框架不内置**)
|
|
376
|
+
|
|
377
|
+
gRPC 不是「另一种 broker」,它与 HTTP 是同一档的东西:**触发宿主**。所以正确形状与 HTTP 宿主
|
|
378
|
+
同构 —— 只产服务实现、不 listen(监听/端口/信号都是宿主的职责):
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
const out = await app.run(normalizeMessages(req.input), {
|
|
382
|
+
signal: ac.signal, // ① 中断
|
|
383
|
+
...(traceContext !== undefined ? { traceContext } : {}), // ② 入站链路
|
|
384
|
+
rethrow: false, // 跑失败是业务结果,不是传输错误
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**为什么框架不内置 gRPC**(与「MCP 连接器内置」不矛盾,判别规则只有一条):
|
|
389
|
+
MCP stdio 只用标准库(`spawn` + 全局 `fetch`)⇒ **内置不新增一个第三方依赖**;
|
|
390
|
+
gRPC 必须引第三方客户端(`@grpc/grpc-js`)⇒ 落在「可选能力一律 duck-typed / peer」那一侧。
|
|
391
|
+
「零运行时依赖」是公开承诺(`package.json` 三个依赖字段全空),所以它只能以**配方 + 示例**存在。
|
|
392
|
+
真有第二个使用方要同一份逻辑时再考虑独立包,且粒度是**一个第三方客户端一个包** ——
|
|
393
|
+
不是把所有集成塞进一个「服务包」(理由见 spec §10 2026-09-18 ⑪)。
|
|
394
|
+
|
|
395
|
+
**宿主必须自己接上的四处**(漏掉任何一条都**不会报错**,只会静默丢东西):
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
① deadline / 客户端取消 → AbortSignal → options.signal
|
|
399
|
+
不接 = 客户端已经走了,服务端还把这次 run 跑完(token 照烧),trace 里也看不出「白跑了一轮」
|
|
400
|
+
② metadata `traceparent` → options.traceContext → run 根一条 link
|
|
401
|
+
不接 = 跨进程关联在服务边界上断掉(§6.4「跨进程关联」那条缝在 gRPC 宿主上同样成立)
|
|
402
|
+
③ 框架错误 → gRPC 状态码(照抄 classifyError 的分类,别自己 instanceof 厂商错误类)
|
|
403
|
+
不接 = 全部塌成一个 UNKNOWN,调用方的重试策略随之失效
|
|
404
|
+
④ trace → sink(落盘 / OTLP / 指标)
|
|
405
|
+
不接 = RPC 回了结果,但「这次为什么慢 / 贵 / 失败」没有证据 —— trace 决定你敢不敢上线
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**错误 → 状态码**照着引擎的分类口径写(`classifyError` 认的是**数据属性**:数值 `status` /
|
|
409
|
+
errno `code`,不是 `instanceof`):
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
const t = classifyError(e).type;
|
|
413
|
+
t === 'rate_limit' ? grpc.status.RESOURCE_EXHAUSTED
|
|
414
|
+
: t === 'server' || t === 'connection' ? grpc.status.UNAVAILABLE
|
|
415
|
+
: t === 'timeout' ? grpc.status.DEADLINE_EXCEEDED
|
|
416
|
+
: t === 'aborted' ? grpc.status.CANCELLED
|
|
417
|
+
: t === 'api' ? grpc.status.INVALID_ARGUMENT // 4xx:调用方写错了
|
|
418
|
+
: grpc.status.INTERNAL
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**run 失败 ≠ RPC 失败**:与 HTTP 宿主 200 + `status: failed` 同口径 —— run 的硬失败是**业务结果**
|
|
422
|
+
(`rethrow: false`,看 `status` / `error` 字段),别翻成 UNKNOWN 让调用方以为是基础设施故障。
|
|
423
|
+
只有宿主层面的失败(入参不可规整、停机中)才用非 OK 状态码。
|
|
424
|
+
|
|
425
|
+
**流式**走同一个 `onText` 缝(不是 gRPC 特例):`onText: (d) => call.write({ textDelta: d })`,
|
|
426
|
+
末帧下发整份结果 —— 与 HTTP 宿主的 SSE 事件一一对应(`text.delta` / `run.end`),
|
|
427
|
+
前端能把两套传输共用一套渲染逻辑。
|
|
428
|
+
|
|
429
|
+
**调用方怎么带上游链路**:metadata 里放 W3C `traceparent`(与 HTTP 头同格式、同一个
|
|
430
|
+
`parseTraceparent` 解析);**畸形头静默当作没有上游**,不打回失败 —— 链路是观测行为,
|
|
431
|
+
一个畸形头不该把业务请求打成错误。
|
|
432
|
+
|
|
433
|
+
**现成可跑**:仓库 `examples/grpc-host/`(proto + 宿主 + 客户端 + e2e;四个 RPC:一元 /
|
|
434
|
+
服务端流 / 异步投递 / 查任务态),README 里每条都是可执行命令:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
npm run build # 仓库根:先出框架 dist(示例以 file:../.. 依赖它)
|
|
438
|
+
cd examples/grpc-host && npm install && npm run build
|
|
439
|
+
npm run serve # 起宿主;PORT=0 时它打印实际端口(不靠外部探端口,没有抢占窗口)
|
|
440
|
+
npm run client # 另一个终端:把四个 RPC 跑一遍
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
它的 e2e(`npm run e2e:grpc`)守的正是上面四处语义:deadline 到期后**服务端的 run 真被 abort**
|
|
444
|
+
(trace 里 `error.type=aborted`,而不是跑完)、`traceparent` 落成 run 根 link、
|
|
445
|
+
同 `session_id` 的两轮 run 共享历史、同 `idempotency-key` 重投不重复执行。
|
|
446
|
+
|
|
372
447
|
#### HTTP 端点速查(`createHttpHandler` 的路由)
|
|
373
448
|
|
|
374
449
|
| 端点 | 请求 | 响应 |
|
|
@@ -376,6 +451,7 @@ const app = await createApp({ ... });
|
|
|
376
451
|
| `POST /run` | body 是 `RunInput`(string / messages / `{prompt\|text\|messages}`);带 `Accept: text/event-stream` 则走 SSE | 200 `{ runId, status, stopReason, finalText, typed?, trace, error? }` —— **`status=failed` 也照返 200**(`rethrow:false` 语义:硬失败以 `error` 字段表达,不用 HTTP 错误码) |
|
|
377
452
|
| `POST /tasks` | `{ input, idempotencyKey?, options? }` —— `input` 同 `RunInput`;`options` 是 `RunInvocationOptions` | 202 `TaskRecord`(`status: 'queued'`);同 `idempotencyKey` 未失败则去重,直接返回既有记录 |
|
|
378
453
|
| `GET /tasks/:id` | — | 200 `TaskRecord`;不存在 → 404。**停机中仍可轮询**(否则拿不到在飞任务的结果) |
|
|
454
|
+
| `POST /tasks/:id/approve` | `{ decisions: { <tool_use_id>: { approved, reason? } }, decidedBy? }` | 200 `TaskRecord`(HITL 审批:批准/拒绝挂起任务,见 §6.6「人工审批」);任务不存在 → 404;不在 `awaiting_approval` 状态 → 409;body 非法 → 400。**停机中仍可审批**(与 GET 轮询同理由) |
|
|
379
455
|
| `GET /healthz` | — | 200 `HealthResponse`;**不鉴权**,停机中也回 200 |
|
|
380
456
|
| `GET /metrics` | — | 200 Prometheus 文本(`text/plain; version=0.0.4`);**需在 `createHttpHandler` 里传 `metrics`**,**不鉴权**(与 `/healthz` 同档),停机中也回 |
|
|
381
457
|
|
|
@@ -452,7 +528,7 @@ process.on('SIGTERM', async () => {
|
|
|
452
528
|
- **取消**:`app.run(messages, { signal })` 传 `AbortSignal` —— 框架会 abort 在飞请求(内置 Anthropic / OpenAI 适配器都转发 `signal`),run 以 `stopReason='aborted'` 收尾(算失败)。`createHttpHandler` 已内置「客户端断开即中止」;`AsyncRunner.runTimeoutMs` 到点同样是**真中止**(构造期校验:必须 ≥ 0 的**有限**数 —— NaN/Infinity 会被 `setTimeout` 钳到 1ms,等于每个任务立即超时,故直接抛错;要「不限」传 0 或不设)。
|
|
453
529
|
- **重试**:缺省自动重试可重试失败(429 / 5xx / 连接失败),指数退避 + 抖动。`retry: false` 关闭,或 `retry: { maxAttempts, baseDelayMs, maxDelayMs, jitter, onRetry }` 调参。**只在本次尝试尚未产出任何文本时重试**(已吐出的字无法撤回)。⚠️ 与底层 client 的**内置重试**叠加 —— **两条内置适配器口径一致**(`createAnthropicClient` / `createOpenAIClient` 都有 `maxRetries`,缺省 2,重试同一状态码集合 408/409/429/5xx)—— 建议二选一调(这里 `maxAttempts: 1`,或 `<适配器>({ maxRetries: 0 })`)。
|
|
454
530
|
- **流式**:`POST /run` 带 `Accept: text/event-stream` → SSE 逐帧下发(`text.delta` / `run.end` / `error`);不带该头仍回一元 JSON。
|
|
455
|
-
- **工具超时 / 并发闸门**:`toolTimeoutMs` 超时**不杀 run**(该条 tool_result 记 `is_error`,模型可换路);`maxToolConcurrency` 给同回合的并行工具设上限(默认全并行)。⚠️ 超时 =
|
|
531
|
+
- **工具超时 / 并发闸门**:`toolTimeoutMs` 超时**不杀 run**(该条 tool_result 记 `is_error`,模型可换路);`maxToolConcurrency` 给同回合的并行工具设上限(默认全并行)。⚠️ 超时 = **放弃等待**:`AgentTool.run` 没有 signal 参数,**副作用可能已发生**;但引擎放弃等待时会 abort `ToolRunContext.abandoned` —— 想真停的工具监听它自行收尾(框架自带的 @SubAgent / @Skill 已这么做:超时即中止子循环,capability span 以 error 收尾)。**超时判定只有一个裁判**:`toolTimeoutMs` 是唯一判据 —— 工具自带的超时(如 MCP 桥的 `timeoutMs`)在设了本项时**不参与**判定;反过来说,工具自判的超时(抛 `code='timeout'` 的错误)与引擎判的记**同一类账**(`errorKind='timeout'`),并同样回 `is_error`。
|
|
456
532
|
|
|
457
533
|
### 6.3 上下文预算与成本
|
|
458
534
|
|
|
@@ -478,7 +554,7 @@ process.on('SIGTERM', async () => {
|
|
|
478
554
|
|
|
479
555
|
| API | 说明 |
|
|
480
556
|
|---|---|
|
|
481
|
-
| `createBudgetGuard` | 执行 `check(
|
|
557
|
+
| `createBudgetGuard` | 执行 `check({ totalUsage })` → `'tokens' \| 'cost' \| null` 的护栏(也可只用来自己记账) |
|
|
482
558
|
|
|
483
559
|
两者是**互补的两件事**,取舍点完全不同:
|
|
484
560
|
|
|
@@ -587,6 +663,8 @@ runner.submit(msg.value, {
|
|
|
587
663
|
```
|
|
588
664
|
|
|
589
665
|
异步任务可以在 `POST /tasks` 的 body 里显式给 `options.traceContext`,它优先于 `traceparent` 头。
|
|
666
|
+
换宿主这条缝不变:gRPC 宿主把 metadata 的 `traceparent` 翻进 `options.traceContext`,
|
|
667
|
+
见 §6.2「gRPC 宿主」。
|
|
590
668
|
|
|
591
669
|
> **只做入站**:框架**不生成**出站 `traceparent`(运行中没有「当前 span」这个概念,硬造一个会是假的
|
|
592
670
|
> spanId,比不做更糟)。要从 run 往外传,用结果里的 `runId`(== `traceId`)拼你自己的头,见 §7 已知边界。
|
|
@@ -606,6 +684,8 @@ runner.submit(msg.value, {
|
|
|
606
684
|
来自 `capability` span。**`@Prompt` 不建 span、无独立耗时,因此不产出能力指标**(如实缺省,不硬凑)。
|
|
607
685
|
- **模型级** —— 按模型(`llm.turn` 的 span name)归因 turn 数 / token / 成本 / 耗时,并单独给出
|
|
608
686
|
`model_unpriced_turns_total`(算不出成本的 turn 数 —— **成本护栏失效的显式信号**)。
|
|
687
|
+
同理,**标签基数折叠不是静默的**:`agentia_dropped_keys{kind="capability"|"model"|"score"}`
|
|
688
|
+
(恒定发三个样本,即使为 0 —— 「0 → N」这个变化本身就是要告警的信号)。
|
|
609
689
|
- **评分级** —— 来自 run 根 span 的 `score` 事件(`attachScore` 写入):
|
|
610
690
|
`agentia_score{name,source}` gauge 记**最近一次**值(分数不是累加量),`agentia_score_total{name,source}` counter 记条数;
|
|
611
691
|
`snapshot().scores` 以 `name@source` 为键(source 缺省时裸 name)暴露 `{ value, count, sum }`(平均 = sum/count),
|
|
@@ -654,6 +734,8 @@ runner.submit(msg.value, {
|
|
|
654
734
|
- 分位是**窗口内精确值**(最近 rank 法),只反映最近 `windowSize` 条样本;**直方图计数是累积的**(全历史),两者语义不同、各有各的用处。
|
|
655
735
|
- **内存上限** ≈ `(1 + 能力数 + 模型数) × windowSize` —— 三个维度都由基数上限封顶(`maxCapabilities` / `maxModels` / `maxScores`),长跑宿主不会被拖住。
|
|
656
736
|
- 超上限的键折叠进 `__other__`:**丢的只是标签粒度,量不丢** —— `__other__` 桶照常累加,`snapshot()` 里各维度的总数仍然对得上。被折叠的**不同**键数见 `droppedCapabilities` / `droppedModels` / `droppedScores`(各自最多记账 1024 个键,满了以后是下界)。
|
|
737
|
+
**同样的数在 `/metrics` 上也看得见**(`render()`):`agentia_dropped_keys{kind=…}` —— 只看 Prometheus
|
|
738
|
+
不看 `snapshot()` 的部署不会漏掉折叠。
|
|
657
739
|
- `costUsd` 依赖模型在价格表内(不在表里时不计、并计入 `unpricedTurns` 与 `usage.unpriced` 事件);根 span 未收尾(如失败路径的半截 trace)的 run 不进延迟样本。
|
|
658
740
|
|
|
659
741
|
#### 调用树面板(`agentia dev` 的本地面板 / 官网 Playground)
|
|
@@ -748,25 +830,40 @@ const app = await createApp({ /* … */ sinks: [jsonl] });
|
|
|
748
830
|
| `diffTraces` | 两条 trace 的 A/B 比对(prompt / 模型实验):run 级 summary + 逐 span 字段差;纯函数,llm.turn 配对**忽略模型名**,缺省忽略墙钟 |
|
|
749
831
|
| `applyMiddleware` | 手动包裹配置菜单(装配层已自动做) |
|
|
750
832
|
|
|
751
|
-
#### MCP
|
|
833
|
+
#### MCP 桥与连接器(MCP 是「工具来源」,不是新机制)
|
|
752
834
|
|
|
753
835
|
| API | 说明 |
|
|
754
836
|
|---|---|
|
|
755
837
|
| `mcpTools` | 把 MCP server 的 `tools/list` 映射成框架 `AgentTool[]`(进 `createApp({ tools })`) |
|
|
756
838
|
| `McpClientLike` | 最小结构面:`listTools()` + `callTool(name, args)`;框架**不 import** MCP SDK |
|
|
839
|
+
| `createStdioMcpConnector` | **出厂 stdio 连接器**:spawn 一个 MCP server 子进程,走换行分隔 JSON-RPC(只用 `node:child_process`) |
|
|
840
|
+
| `createStreamableHttpMcpConnector` | **出厂 StreamableHTTP 连接器**:一个 endpoint POST JSON-RPC;`application/json` 与 `text/event-stream` 两种响应都接(只用全局 `fetch`) |
|
|
841
|
+
| `McpConnector` | 两个连接器的公共面 = `McpClientLike` + `close()`。自己写传输时实现 `McpClientLike` 即可,不必碰它 |
|
|
757
842
|
| `MCP_DEFAULT_TIMEOUT_MS` | 桥的**兜底**单次调用超时(60000 ms)—— 引擎设了 `toolTimeoutMs` 时**不参与**判定 |
|
|
843
|
+
| `MCP_CLOSE_GRACE_MS` | `close()` 里 SIGTERM → SIGKILL 的宽限期(2000 ms) |
|
|
758
844
|
|
|
759
845
|
- **名字**:`prefix + 归一化原名`(MCP 名里的 `-` / `.` / 空格 → `_`)。归一化后**空名(原名不含任何 ASCII 字母/数字/下划线时产物为空,如全 emoji 名)/ 撞名 / 超 64 字符**一律**装配期抛错**(静默改名会得到一个调不回去的名字,比启动期报错难查得多)。
|
|
760
846
|
- **原名**:每次调用写进发起 turn 的两条 attribute —— `mcp.tool.<菜单名>`(每次调用各一条,并行调用互不覆盖,审计 / 回放靠它把菜单名还原成 server 认识的原名)与 `mcp.tool`(本次 turn **最近一次**的原名,兼容既有查询)。
|
|
761
847
|
- **入参 schema**:MCP 的 `inputSchema` 已是 JSON Schema → 原样透传,由 engine 的子集校验器在 `callTool` **之前**校验(非法入参根本不会发给 server,模型自己会改)。
|
|
762
|
-
- **失败**:`callTool` 抛错 → 该条 `tool_result` 记 `is_error`,**不杀 run**(与本地工具抛错同语义)。⚠️ **协议层的 `isError: true` 框架看不见** ——
|
|
848
|
+
- **失败**:`callTool` 抛错 → 该条 `tool_result` 记 `is_error`,**不杀 run**(与本地工具抛错同语义)。⚠️ **协议层的 `isError: true` 框架看不见** —— 连接器必须转成抛错,否则模型以为成功了(出厂连接器已代你处理)。
|
|
763
849
|
- **超时**:**只有一个裁判**。引擎设了 `toolTimeoutMs` 时,桥的 `timeoutMs`(缺省 `MCP_DEFAULT_TIMEOUT_MS` = 60000)**不参与判定**;它只在「桥脱离引擎单用」或「引擎没设 `toolTimeoutMs`」时作为兜底。两条路径共用同一判定(`core/timeout.ts`:**看实测耗时,不看竞速**),超时都记 `errorKind='timeout'` + `is_error` 回模型、**不杀 run**。你自己写的工具要报超时,抛一个 `code === 'timeout'` 的错误即可(不必 import 框架的类)。
|
|
764
|
-
-
|
|
850
|
+
- **连接器出厂自带,但仍只给缝**:两个连接器只用标准库(`node:child_process` + 全局 `fetch`)⇒ **不新增任何第三方依赖**。要接官方 SDK / 远程 server / 自研传输,实现 `McpClientLike` 两个方法即可(同 `RedisLike` 的形状)。
|
|
851
|
+
- **连接器替你兜住三件只有它能做的事**:① spawn 失败(命令不存在 → `ENOENT`)是**异步 `'error'` 事件**,不接住会把宿主进程带崩;② stdout 的分帧 —— 一条报文可能跨多个 chunk;③ **协议层 `isError: true` 转成抛错**。
|
|
852
|
+
- **连接器的 `timeoutMs` 只管装配期**(握手 + `tools/list`):那两步**没有任何别的裁判**,server 卡住会让 `createApp` 永久挂起;`callTool` 的裁判仍是引擎 / 桥(一次调用只有一个)。
|
|
765
853
|
|
|
766
854
|
```ts
|
|
767
|
-
//
|
|
768
|
-
const
|
|
769
|
-
const tools = await mcpTools(
|
|
855
|
+
// 出厂连接器:stdio(spawn 子进程)
|
|
856
|
+
const mcp = createStdioMcpConnector(['uvx', 'mcp-server-time']);
|
|
857
|
+
const tools = await mcpTools(mcp, { server: 'time' }); // → mcp_time_get_current_time …
|
|
858
|
+
|
|
859
|
+
// 远程 server:StreamableHTTP
|
|
860
|
+
const remote = createStreamableHttpMcpConnector('https://mcp.example/mcp', {
|
|
861
|
+
headers: { authorization: 'Bearer …' },
|
|
862
|
+
});
|
|
863
|
+
const remoteTools = await mcpTools(remote, { server: 'remote' });
|
|
864
|
+
|
|
865
|
+
// 也可以用你自己的传输:任意实现了 listTools/callTool 的对象都能接(duck-typed,无需继承)
|
|
866
|
+
const mine = await mcpTools(myOwnConnector, { server: 'time' });
|
|
770
867
|
|
|
771
868
|
// 与本地 @Tool 同池:同过中间件链、同进重名查重
|
|
772
869
|
const app = createApp({ system, providers: [...], tools });
|
|
@@ -780,6 +877,28 @@ const app = createApp({ system, providers: [...], tools });
|
|
|
780
877
|
| `server` | server 标识,只用于拼缺省前缀(不会发给 server) |
|
|
781
878
|
| `timeoutMs` | 单次 `callTool` 超时(毫秒);缺省 60000,非正数 = 不限 |
|
|
782
879
|
|
|
880
|
+
#### `StdioMcpConnectorOptions`(`createStdioMcpConnector` 的选项)
|
|
881
|
+
|
|
882
|
+
| 字段 | 说明 |
|
|
883
|
+
|---|---|
|
|
884
|
+
| `env` | 追加 / 覆盖的环境变量(缺省继承 `process.env`) |
|
|
885
|
+
| `cwd` | 子进程工作目录 |
|
|
886
|
+
| `stderr` | 子进程 stderr 去向:`'inherit'`(缺省,server 日志直通终端)或 `'ignore'` |
|
|
887
|
+
| `clientInfo` | `initialize` 握手要发的 `{ name, version }`(协议要求存在) |
|
|
888
|
+
| `protocolVersion` | 请求的协议版本,缺省 `2024-11-05` |
|
|
889
|
+
| `timeoutMs` | **装配期**超时(握手 + `tools/list`),缺省 60000,非正数 = 不限 |
|
|
890
|
+
|
|
891
|
+
#### `StreamableHttpMcpConnectorOptions`(`createStreamableHttpMcpConnector` 的选项)
|
|
892
|
+
|
|
893
|
+
| 字段 | 说明 |
|
|
894
|
+
|---|---|
|
|
895
|
+
| `headers` | 附加请求头(鉴权等);会覆盖缺省的 `content-type` / `accept` 同名字段 |
|
|
896
|
+
| `clientInfo` | 同 stdio |
|
|
897
|
+
| `protocolVersion` | 请求的协议版本,缺省 `2024-11-05`;协商结果以 server 回的为准 |
|
|
898
|
+
| `timeoutMs` | **装配期**超时(握手 + `tools/list`),缺省 60000,非正数 = 不限 |
|
|
899
|
+
| `onSessionExpired` | 会话过期自愈时被调一次(见「已知边界」)—— 要计数 / 告警 / 打日志就挂它 |
|
|
900
|
+
| `fetchImpl` | 注入 `fetch`(测试用;缺省全局 `fetch`,与 `createOpenAIClient` 同款) |
|
|
901
|
+
|
|
783
902
|
#### evals(把 mockClient 提升为一等能力)
|
|
784
903
|
|
|
785
904
|
| API | 说明 |
|
|
@@ -940,10 +1059,13 @@ createApp({ system, providers: [...], middleware: [quota], sinks: [billing] });
|
|
|
940
1059
|
- 拦下来的那次 run **仍然要记账**(模型的钱已经花了)—— 记账在 sink 里、拦截在 middleware 里,两者独立。
|
|
941
1060
|
- 被拦下的能力**不会执行**(副作用不发生),但 run 继续跑(模型可以换路)。
|
|
942
1061
|
|
|
943
|
-
####
|
|
1062
|
+
#### 人工审批
|
|
1063
|
+
|
|
1064
|
+
两种形态,按「人什么时候在场」选:
|
|
944
1065
|
|
|
945
|
-
|
|
946
|
-
|
|
1066
|
+
**A. 进程内闸门(middleware)** —— 审批方能被一次 `await` 等到(同进程回调 / 短等待)时用,
|
|
1067
|
+
不需要任何新机制 —— `middleware` 可以 `await` 决策再放行,引擎会等工具结果
|
|
1068
|
+
(`Promise.resolve(tool.run(...))`):
|
|
947
1069
|
|
|
948
1070
|
```ts
|
|
949
1071
|
const DANGEROUS = new Set(['send_email', 'deploy', 'delete_records']);
|
|
@@ -966,9 +1088,59 @@ createApp({ system, providers: [...], middleware: [requireApproval] });
|
|
|
966
1088
|
(ALS 传播,见上一节)。
|
|
967
1089
|
- 等待不会被默认掐断(`toolTimeoutMs` 缺省 0 = 不限);真设了它,注意别把人的思考时间算进去。
|
|
968
1090
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1091
|
+
**B. 挂起式审批(HITL,跨进程耐久)** —— 审批在**另一个系统**里发生(工单 / IM / 后台台帐),
|
|
1092
|
+
可能要等几小时甚至跨进程重启时用:给工具声明 `approval: 'required'`,模型每次调用它都会把
|
|
1093
|
+
任务**挂起**,等决定到达后从断点恢复。
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
class DeployTools {
|
|
1097
|
+
@Tool({ description: '部署到生产环境', schema: deploySchema, approval: 'required' })
|
|
1098
|
+
async deploy(input: DeployInput) { /* … */ }
|
|
1099
|
+
}
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
流程(异步任务宿主):
|
|
1103
|
+
|
|
1104
|
+
1. `POST /tasks` 提交任务;模型调到 `deploy` 时 run **挂起**:任务状态变
|
|
1105
|
+
`awaiting_approval`,**整个回合一个工具都不执行**(全有或全无 —— 协议要求每个
|
|
1106
|
+
tool_use 配对 tool_result,部分执行 + 部分挂起会产出配不平的历史)。
|
|
1107
|
+
2. 轮询 `GET /tasks/:id` 看到 `status: 'awaiting_approval'` + `pendingApprovals`
|
|
1108
|
+
(待决的 tool_use_id 列表)+ `spec.messages`(完整消息历史,末尾是含未决 tool_use
|
|
1109
|
+
的那条 assistant 消息)。挂起**不占并发槽**、不算终态(`awaitTask` 继续等)、
|
|
1110
|
+
`resumePending` 不会把它当孤儿捡走。
|
|
1111
|
+
3. 人批了:`POST /tasks/:id/approve`,body
|
|
1112
|
+
`{ decisions: { '<tool_use_id>': { approved: true } }, decidedBy: 'alice' }`
|
|
1113
|
+
(或直接 `runner.approve(taskId, decisions, { decidedBy })`)。**逐 tool_use_id 幂等**
|
|
1114
|
+
(第一次决定赢,重复提交不推翻)。决定**随任务落库**(`TaskRecord.approvals`,
|
|
1115
|
+
进程重启不丢);待决集合齐了任务自动恢复执行。
|
|
1116
|
+
4. 恢复时:**批准**的工具正常执行(工具体内经 `ToolRunContext.approval` 读到自己的决定 —
|
|
1117
|
+
谁批的、什么时候、什么理由);**拒绝**的工具得到 `tool_result(is_error: true,
|
|
1118
|
+
content: '审批被拒绝:…')` —— 理由回给模型,可自行换路。恢复后再遇未决审批 ⇒
|
|
1119
|
+
再次挂起(可等多轮)。
|
|
1120
|
+
|
|
1121
|
+
##### `ApprovalDecision`(审批决定)的字段
|
|
1122
|
+
|
|
1123
|
+
| 字段 | 说明 |
|
|
1124
|
+
|---|---|
|
|
1125
|
+
| `approved` | `true` = 批准执行;`false` = 拒绝(tool_result 记 `is_error`) |
|
|
1126
|
+
| `reason` | 拒绝理由 / 备注(回给模型) |
|
|
1127
|
+
| `decidedBy` | 审批人标识(审计用) |
|
|
1128
|
+
| `decidedAt` | 决定时刻;缺省由框架在收到决定时填 |
|
|
1129
|
+
| `requestedAt` | 挂起时刻(框架回填;trace 的 `approval.decided` 事件据此算 `waitedMs`) |
|
|
1130
|
+
|
|
1131
|
+
观测与兜底:
|
|
1132
|
+
|
|
1133
|
+
- trace:挂起段在发起回合的 turn span 记 `approval.requested`(带待决 id 列表);
|
|
1134
|
+
恢复执行时记 `approval.decided`(带 tool_use_id / approved / decidedBy / reason /
|
|
1135
|
+
waitedMs)。**挂起段与恢复段是两棵独立的 trace**,恢复段经根 span 的 `links` 挂到
|
|
1136
|
+
上一段 runId(跨段关联不断链);挂起段的 trace 照常投递 sinks(「任务为什么在等」
|
|
1137
|
+
必须可观测)。
|
|
1138
|
+
- 超时兜底:`new AsyncRunner(app, { approvalTimeoutMs })`(缺省 0 = 一直等)。**惰性判定,
|
|
1139
|
+
不起定时器**:`approve` / `poll` / `resumePending` 读到一个挂起已超过该值的任务时,
|
|
1140
|
+
自动把全部待决项写成「拒绝:审批超时」并恢复执行(模型收到理由、任务走向终态)。
|
|
1141
|
+
没人读的任务不会自己超时 —— 要定期扫就靠 `resumePending()`。
|
|
1142
|
+
|
|
1143
|
+
**已知边界**(也收录在 §7):见 §7 表的「审批」相关行。
|
|
972
1144
|
|
|
973
1145
|
#### 内容护栏
|
|
974
1146
|
|
|
@@ -998,8 +1170,8 @@ const callable = {
|
|
|
998
1170
|
在**工具实现内部**做(Docker / 子进程 / 微 VM 随你),框架不参与也不该参与 ——
|
|
999
1171
|
`AgentTool.run(input) → output` 这个契约把隔离整个挡在实现里。
|
|
1000
1172
|
|
|
1001
|
-
唯一沾边的一条:工具起了子进程,**取消时要自己 kill**。框架的 `toolTimeoutMs`
|
|
1002
|
-
|
|
1173
|
+
唯一沾边的一条:工具起了子进程,**取消时要自己 kill**。框架的 `toolTimeoutMs` 是「不等了」,
|
|
1174
|
+
放弃等待时会 abort `ToolRunContext.abandoned` —— 要能真停,让工具监听它(或读 `ToolRunContext.signal` 响应整条 run 的中止)。
|
|
1003
1175
|
|
|
1004
1176
|
---
|
|
1005
1177
|
|
|
@@ -1030,16 +1202,20 @@ const callable = {
|
|
|
1030
1202
|
| 停机不由框架触发 | 框架给 `drain()` 但**不订阅** `SIGTERM`/`SIGINT`(不做进程级决策);信号处理是宿主的 |
|
|
1031
1203
|
| 停机可能切断 SSE | `drain()` 超时后会强制关闭仍开着的 SSE 流,其 run 以 `stopReason='aborted'` 收尾 —— 客户端应把断流当作可重试 |
|
|
1032
1204
|
| 鉴权失败即断连 | 未通过鉴权时在读到 body 之前就回响应,连接**不可复用**(显式 `connection: close`);这是「不收body省资源」的代价 |
|
|
1033
|
-
| 预算护栏不是硬实时 |
|
|
1205
|
+
| 预算护栏不是硬实时 | 一回合记账完才判,实际用量可能超上限一个回合的量;并行子循环(一回合多个子 agent)各自过闸,超支上限是「**每个在飞分支**各一个回合」而非「总共一个回合」;模型自然收尾的那回合超限**不算失败**(只留 `budget.exceeded` 事件) |
|
|
1034
1206
|
| `maxCostUsd` 依赖价格表 | 模型不在价格表内(且未用 `priceOverrides` 覆盖)时成本恒为 0,这条护栏**不触发** —— 要无条件兜底用 `maxTotalTokens`。**失效会响**:turn 上会记 `usage.unpriced` 事件、指标有 `model_unpriced_turns_total`、可回调 `onUnpricedModel` |
|
|
1035
|
-
|
|
|
1207
|
+
| 工具超时**不强制取消**工具 | `AgentTool.run` 没有 signal 参数,超时首先是「不等了」;副作用可能已发生。想真停:监听 `ToolRunContext.abandoned`(引擎放弃等待时 abort 它)自行收尾。框架自带的 @SubAgent / @Skill 已这么做 —— 超时即中止子循环(在飞请求被掐、不再后台烧 token),capability span 立刻以 error 收尾 |
|
|
1036
1208
|
| 会话只存对话轮次 | `SessionStore` 存「用户输入 + 最终回复」,run 内部的 tool 往返**不进历史**(要完整过程用 `traceToMessages`);且只有**跑成功**的轮次才回写 |
|
|
1037
1209
|
| 同 session 并发 run 要自行串行化 | `SessionStore` 是 **append-only**:并发写不互相覆盖、不丢数据,但**不保证角色交替** —— 两个并发 run 共用同一 sessionId 时,各自追加的轮次可能交错成「连续两条 user」,下一轮 load 出来撞角色交替校验(400)。同一 session 的并发 run 请调用方自行串行化(每 session 一把锁 / 一条队列) |
|
|
1038
1210
|
| OpenAI 适配器听端点的话 | 请求发 `stream:true`,但**按响应形态解析**:端点回 JSON 就退回一次性(没有打字机效果),回 `event-stream` 才逐 token |
|
|
1039
1211
|
| OpenAI 流式的上游故障按失败处理 | 三种形态都**抛错**按失败处理:流中 `error` 分片(上游把故障塞进 200 的流;按 `type`/`code` 反推 status,限流能被引擎重试认出);**未收到 `[DONE]` 也无 `finish_reason`**(流被上游/代理截断 —— 哪怕已吐出半句、有累积文本,也按不完整响应抛错,不报 `end_turn`);正常终止却无任何文本与工具调用(与非流式空 `choices` 同一守卫)。**例外**:`finish_reason=content_filter` 的空流是合法 refusal,不抛 —— 与非流式路径同一个响应同一个结论 |
|
|
1040
|
-
|
|
|
1041
|
-
| MCP
|
|
1212
|
+
| OpenAI 兼容端点回 legacy `function_call` 形态时**不支持** | 适配器只认现代 `tool_calls`(请求侧也只发这个形态)。收到 `finish_reason=function_call` 会**响亮失败**(400,落 `api`/不可重试),**不**按 `end_turn` 收尾 —— 那种回法里的调用在 `message.function_call` 里、读不出来,报成正常收尾会让「模型要调工具、工具却没执行」记成成功。换支持 `tool_calls` 的端点或模型即可(legacy `functions` 形态 OpenAI 2023 已废弃) |
|
|
1213
|
+
| MCP 只做 tools | `sampling`(server 反向请求模型)/ `resources` / `prompts` 原语不做;出厂连接器同样只做 `tools/list` + `tools/call` |
|
|
1214
|
+
| MCP 的协议层错误框架看不见 | `isError: true` 只有连接器能看见 —— 它必须转成抛错,否则模型收到的是一条「成功」的结果(出厂连接器已代你处理) |
|
|
1042
1215
|
| MCP 超时同样是「不等了」 | 桥的 `timeoutMs` 取消不了 server 侧执行(拿不到取消句柄);它只是**兜底** —— 引擎设了 `toolTimeoutMs` 时**不参与**判定(一次调用只有一个裁判;**显式 `toolTimeoutMs: 0` 也算设了** —— 那是引擎表态「不限」,桥不会再自作主张判 60s),两条路径**同判定、同账**(`errorKind='timeout'`) |
|
|
1216
|
+
| MCP 连接器的超时只管装配期 | 连接器自带的 `timeoutMs` 只作用于**握手 + `tools/list`**(那两步**没有任何别的裁判** —— server 卡住会让 `createApp` 永久挂起);`callTool` 仍是引擎 / 桥那一个裁判 |
|
|
1217
|
+
| MCP 连接的 `close()` 保证子进程已终止 | stdio 先 `SIGTERM`、`MCP_CLOSE_GRACE_MS`(2000 ms)后 `SIGKILL`,然后**等真正的 `'exit'`** —— **返回即代表进程已被回收**(此前到点即返回,会留孤儿进程而调用方无从知晓);HTTP 尽力 `DELETE` 会话(server 不认也无所谓) |
|
|
1218
|
+
| StreamableHTTP 会话过期**自愈** | 带会话 id 收到 `404` = 会话已终止、**该请求未被 server 执行** ⇒ 丢会话 → 重新握手 → 把**这一次**重试一次(**只一次**,不再循环)。自愈本身是静默的 ⇒ 用 `onSessionExpired` 去计数 / 告警,否则它和「静默失效」在监控上看不出区别。`404` **之外**的失败仍按 `classifyError` 分流抛出,不重试 |
|
|
1043
1219
|
| MCP 名字可能被归一化 | 原名含 `-` / `.` / 空格 → 进菜单时变成 `_`;回调 server 用的仍是原名(`mcp.tool.<菜单名>` attribute 逐次可查;`mcp.tool` 是最近一次) |
|
|
1044
1220
|
| MCP 工具不能进 DI 容器 | 它没有 provider token,也不能被别的能力的 `tools` 引用(两种引用粒度都要先有 token) |
|
|
1045
1221
|
| 指标分位是窗口内精确值 | `*_last{quantile=...}` 只反映最近 `windowSize`(缺省 1024)条样本;要跨实例聚合请用直方图(`*_bucket` / `_sum` / `_count`,累积语义) |
|
|
@@ -1056,7 +1232,18 @@ const callable = {
|
|
|
1056
1232
|
| 评分来自 run 之外 | `Score` 走 run 根 `score` **事件**而非 span 字段(评分通常在 run 跑完后才产生);`attachScore` 找不到根 span 时静默忽略,多次调用即多条事件(不同维度各记各的) |
|
|
1057
1233
|
| 链路关联只做**入站** | `traceContext` / `traceparent` 头只把**上游**接进来(run 根的 `links`);框架**不生成**出站 `traceparent` —— 运行中没有「当前 span」可导出,硬造会给出假 spanId。要从 run 往外传,用 `result.trace.traceId`(== `runId`)自行拼头。另:link 只落在 run 根(子 span 不散),且**一进程内**不跨进程自动传播 —— 队列场景要自己把 `traceContext` 传下去(HTTP 头带走,或随 `TaskRecord.spec.options` 落库) |
|
|
1058
1234
|
| 配额不是框架子系统 | 只给缝(middleware + TraceSink + BudgetGuard),计数放哪(内存 / Redis / DB)与超限怎么办都是你的策略 |
|
|
1059
|
-
|
|
|
1235
|
+
| 人工审批两种形态 | 进程内闸门用 `middleware`(`await` 决策再放行);跨进程耐久审批用 `@Tool({ approval: 'required' })` + `POST /tasks/:id/approve`(挂起/恢复,见 §6.6「人工审批」) |
|
|
1236
|
+
| 审批超时是**惰性**判定 | `approvalTimeoutMs` 不起定时器:`approve` / `poll` / `resumePending` 读到过期挂起任务时才自动全拒并重派 —— 没人读的任务不会自己超时(要定期扫就调 `resumePending()`) |
|
|
1237
|
+
| 挂起段与恢复段是两棵 trace | 每段执行一棵独立的树(`traceId == runId`),恢复段经根 span 的 `links` 挂到上一段;指标按段计(挂起段算一次 ok 的 run —— 「等人」不算失败,区分看 `stop_reason` attribute) |
|
|
1238
|
+
| 审批是 at-least-once | 崩溃发生在「批准后、恢复执行中」时,副作用工具会重执行(与 `resumePending` 续跑同口径)—— 副作用工具自己保证幂等 |
|
|
1239
|
+
| 挂起/恢复间预算重新起算 | `maxTotalTokens` / `maxCostUsd` 在恢复段从 0 重新计(新树新账,与 `resumePending` 续跑同口径) |
|
|
1240
|
+
| 嵌套能力内的审批不支持挂起 | @SubAgent / @Skill 子循环里的 `approval: 'required'` 工具无法把整个 run 挂起 —— 子循环挂起会以 `is_error` 交回主 agent(要审批的能力请放主菜单) |
|
|
1241
|
+
| 同步 `/run` 撞上审批没人可批 | 同步 RPC 会带着 `stopReason: 'awaiting_approval'` 收尾返回 —— 但响应体(`toHttpBody`)**不含** `suspendedMessages`,也没有任务记录可审批(待决清单只能去 trace 的 `approval.requested` 事件里看)。**要审批请走 `POST /tasks` 异步宿主** |
|
|
1242
|
+
| Scheduler 调度表不落库 | `every` / `at` 的调度本身只在内存:已 submit 的任务记录能经 `resumePending` 续跑,但「未来某刻再触发」的调度在重启后不存在(远期单发由宿主自己的 cron 驱动)。另:`drain()` 不停 Scheduler —— 停机窗口内到点的 tick 会打一条触发失败日志(无害但吵),介意就 `scheduler.stop()` 先行 |
|
|
1243
|
+
| file store 的撕裂写只在启动时自愈 | 写入中途失败(磁盘满等)留下的残行由 `healTail` 在**构造期**修复;同进程内继续 append 会把新记录粘在残行尾部、下次启动时一起丢弃 —— 磁盘满告警后先恢复写入能力再继续依赖它 |
|
|
1244
|
+
| 终态落库失败无告警 | AsyncRunner 终态 save 失败被吞(「不击穿主流程」的代价):store 抖动时任务可能永远停在 `running`,重启后 `resumePending` 会重跑一个**实际已成功**(副作用已发生)的任务 —— 耐久 store 的故障告警是宿主的事 |
|
|
1245
|
+
| `contextPolicy` 不进子循环 | 应用级 `contextPolicy` / `onText` 只对主循环生效:@SubAgent / @Skill 的子运行不做上下文裁剪(长跑子 agent 撞上下文上限会以 api 错误收尾)。预算护栏(`maxTotalTokens` / `maxCostUsd`)正常透传 |
|
|
1246
|
+
| 记忆没有删除语义 | `MemoryStore` 只有 load/save:run 内 `ctx.delete` 掉的键回写时不会从 store 移除(下一轮水合会复活)。要真删请直接操作 store 实现 |
|
|
1060
1247
|
| 内容护栏不给实现 | 同「配额」:只给缝(入参包 `app.run` / 工具前 `middleware` / 出参包返回值或 `sinks`),策略(正则 / 分类器 / 外部 API)是你的 |
|
|
1061
1248
|
| 框架不执行模型生成的代码 | 无沙箱可言:`@Skill` 跑你写的方法、`@Tool` 是你写的函数,模型输出只成文本 / `tool_result`;代码执行工具的隔离是**工具实现内部**的事;工具起的子进程取消时要自己 kill |
|
|
1062
1249
|
|
|
@@ -1078,7 +1265,7 @@ const callable = {
|
|
|
1078
1265
|
| 鉴权钩子抛错,客户端只看到「未通过鉴权」 | 这是设计:非 `HttpException` 的错误原文只进服务端日志(要回给调用方就抛 `HttpException(status, body)`) |
|
|
1079
1266
|
| 停机后 `POST /tasks` 回 503 | `drain()` 已被调用(或注入的 runner 已 drain)—— 这是「拒新单」的正常行为,任务没丢 |
|
|
1080
1267
|
| `stopReason` 是 `budget_exceeded`、任务被判失败 | 这是设计(护栏拦下的 run **没跑完**)。只想「记一笔」不想改结局,就自己用 `createBudgetGuard` 读 trace |
|
|
1081
|
-
| 工具超时了,副作用却还是发生了 |
|
|
1268
|
+
| 工具超时了,副作用却还是发生了 | 超时是「放弃等待」不是强制取消。引擎放弃时会 abort `ToolRunContext.abandoned`(@SubAgent/@Skill 已靠它自中止)—— 你自己的工具要真停就监听它 |
|
|
1082
1269
|
| 用 OpenAI 端点没看到打字机效果 | 端点没按 `stream:true` 回 `event-stream`(回了一整份 JSON)—— 适配器按响应形态解析,此时退回一次性 |
|
|
1083
1270
|
| 改了框架源码却看不到效果 | 确认 import 的是同一份构建产物(`npm run build` 后跑 `dist`) |
|
|
1084
1271
|
| MCP 工具没出现在菜单里 | `mcpTools()` 的返回值没传进 `createApp({ tools })` —— 它不走装饰器收集,也不进 DI 容器(裸工具缝) |
|
|
@@ -37,7 +37,14 @@ export declare class Container {
|
|
|
37
37
|
private readonly cache;
|
|
38
38
|
/** 正在解析的 token 链,用于循环依赖检测 */
|
|
39
39
|
private resolving;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* 覆盖式注册(后注册覆盖先注册;同一 app 内同 token 重复注册视为升级)。
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ **注册不会刷新已装配的能力菜单**:`AgentApp.tools` 是**构建期**从容器解析的快照
|
|
44
|
+
* (见 `toolkit/module.ts` 的 `get tools()`)。`createApp()` 之后再 `register()` 一个新
|
|
45
|
+
* `@Tool` 的 provider,容器里确实有了,但主菜单**不会**多出这个工具 —— 菜单是静态稳定的。
|
|
46
|
+
* 要让新能力进菜单,得在装配**之前**注册(或重新 `createApp()`)。
|
|
47
|
+
*/
|
|
41
48
|
register(...providers: Provider[]): this;
|
|
42
49
|
/** token 的全部传递下游(直接或间接依赖它的 token),用于注册时传递失效缓存 */
|
|
43
50
|
private dependentsOf;
|
|
@@ -9,7 +9,14 @@ export class Container {
|
|
|
9
9
|
cache = new Map();
|
|
10
10
|
/** 正在解析的 token 链,用于循环依赖检测 */
|
|
11
11
|
resolving = [];
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* 覆盖式注册(后注册覆盖先注册;同一 app 内同 token 重复注册视为升级)。
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ **注册不会刷新已装配的能力菜单**:`AgentApp.tools` 是**构建期**从容器解析的快照
|
|
16
|
+
* (见 `toolkit/module.ts` 的 `get tools()`)。`createApp()` 之后再 `register()` 一个新
|
|
17
|
+
* `@Tool` 的 provider,容器里确实有了,但主菜单**不会**多出这个工具 —— 菜单是静态稳定的。
|
|
18
|
+
* 要让新能力进菜单,得在装配**之前**注册(或重新 `createApp()`)。
|
|
19
|
+
*/
|
|
13
20
|
register(...providers) {
|
|
14
21
|
for (const p of providers) {
|
|
15
22
|
if (!isProvider(p)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container.js","sourceRoot":"","sources":["../../src/container/container.ts"],"names":[],"mappings":"AAoCA,MAAM,UAAU,UAAU,CAAC,CAAU;IACnC,OAAO,CACL,OAAO,CAAC,KAAK,QAAQ;QACrB,CAAC,KAAK,IAAI;QACV,OAAQ,CAA2B,CAAC,OAAO,KAAK,QAAQ;QACxD,CAAC,UAAU,IAAK,CAAY,IAAI,UAAU,IAAK,CAAY,IAAI,YAAY,IAAK,CAAY,CAAC,CAC9F,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,SAAS;IACH,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAC;IACtC,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACnD,6BAA6B;IACrB,SAAS,GAAY,EAAE,CAAC;IAEhC
|
|
1
|
+
{"version":3,"file":"container.js","sourceRoot":"","sources":["../../src/container/container.ts"],"names":[],"mappings":"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"}
|
package/dist/core/run.d.ts
CHANGED
|
@@ -5,8 +5,12 @@ import type { SpanError } from './trace.js';
|
|
|
5
5
|
* 放在 core 的理由:`RunStatus` / `RunMeta` 是纯数据,只依赖 `core/trace`。
|
|
6
6
|
* store / transport / runtime 都要读它,下沉 core 后三者都不必再向上引 runtime
|
|
7
7
|
* (此前 `store → runtime` 是未声明的兄弟层依赖,见 AGENTS.md 分层约定)。
|
|
8
|
+
*
|
|
9
|
+
* `awaiting_approval`(HITL):run 在等人审批 —— **非终态**(awaitTask 继续轮询)、
|
|
10
|
+
* 不占并发槽(执行段落已收尾释放)、`resumePending` 不捡走(它不是孤儿,崩溃续跑语义
|
|
11
|
+
* 不适用于「在等人」);消息历史(含未决 tool_use)与待决清单随 TaskRecord 落库。
|
|
8
12
|
*/
|
|
9
|
-
export type RunStatus = 'queued' | 'running' | 'succeeded' | 'failed';
|
|
13
|
+
export type RunStatus = 'queued' | 'running' | 'awaiting_approval' | 'succeeded' | 'failed';
|
|
10
14
|
/**
|
|
11
15
|
* run 的运行记录(供持久化 / 读取)。字段**必填但可为 undefined**:它们是框架在 run
|
|
12
16
|
* 生命周期各阶段**总是写进对象**的状态(`toMeta()` 一次构造全量),「缺省」在这里不是
|
package/dist/core/timeout.d.ts
CHANGED
|
@@ -90,6 +90,14 @@ export declare function withTimeout<T>(p: Promise<T>, timeoutMs: number): Promis
|
|
|
90
90
|
*
|
|
91
91
|
* 抛的是 `name === 'AbortError'` 的错误(而非 `TimeoutError`):取消不是超时,
|
|
92
92
|
* `engine/errors.ts` 按 `name` 把它归进「已中止」,`loop` 据此以 `aborted` 收尾。
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ **`ms <= 0` = 「不睡」(立即 resolve),且这一判先于 aborted 检查** —— 别把它
|
|
95
|
+
* 「修」成「已中止就该 reject」。这是本仓库统一的「非正数 = 机制关掉」口径,与
|
|
96
|
+
* `withTimeout(p, 0)` 的「不设超时、原样透传」对称:预算 ≤0 时这次等待压根不存在,
|
|
97
|
+
* 也就没有「被中断」可言(取消会在**下一步**(下一个 fetch / 下一轮循环)照常浮出来)。
|
|
98
|
+
* 已有用例钉住这个语义:`tests/core/sse-text-stats.test.ts`(已中止 + `ms<=0` 仍 resolve)
|
|
99
|
+
* 与 `tests/core/timeout.test.ts`(`withTimeout` 预算非正数)。2026-09-19 的外部复核
|
|
100
|
+
* 曾把它当「一致性缺口」改反过一次,被这条用例拦住 —— 保留该用例,别删。
|
|
93
101
|
*/
|
|
94
102
|
export declare function interruptibleSleep(ms: number, signal?: AbortSignal, abortMessage?: string): Promise<void>;
|
|
95
103
|
/**
|
package/dist/core/timeout.js
CHANGED
|
@@ -82,16 +82,19 @@ export function isTimeoutError(e) {
|
|
|
82
82
|
export async function withTimeout(p, timeoutMs) {
|
|
83
83
|
if (!(timeoutMs > 0))
|
|
84
84
|
return p;
|
|
85
|
-
|
|
85
|
+
// 单调钟(performance.now):硬超时判定看的是**耗时**,墙钟(Date.now)会被
|
|
86
|
+
// NTP 回拨/跳变扭曲 —— 回拨让 settledAt - startedAt 变负,超时的工具被记成 ok,
|
|
87
|
+
// 恰是这段代码要防的静默失效。
|
|
88
|
+
const startedAt = performance.now();
|
|
86
89
|
// 在**工具 settle 的那个微任务里**记时间,而不是 `await` 恢复之后再记:
|
|
87
90
|
// 微任务紧跟在 resolve 它的那次回调之后跑,所以这个时刻最贴近「工具真正完成」。
|
|
88
91
|
// 若改成 await 恢复后再测,宿主在「工具完成 → 恢复」之间被饿住会把按时完成的工具误判成超时。
|
|
89
92
|
let settledAt = 0;
|
|
90
93
|
const tracked = p.then((v) => {
|
|
91
|
-
settledAt =
|
|
94
|
+
settledAt = performance.now();
|
|
92
95
|
return v;
|
|
93
96
|
}, (e) => {
|
|
94
|
-
settledAt =
|
|
97
|
+
settledAt = performance.now();
|
|
95
98
|
throw e;
|
|
96
99
|
});
|
|
97
100
|
let timer;
|
|
@@ -134,6 +137,14 @@ export async function withTimeout(p, timeoutMs) {
|
|
|
134
137
|
*
|
|
135
138
|
* 抛的是 `name === 'AbortError'` 的错误(而非 `TimeoutError`):取消不是超时,
|
|
136
139
|
* `engine/errors.ts` 按 `name` 把它归进「已中止」,`loop` 据此以 `aborted` 收尾。
|
|
140
|
+
*
|
|
141
|
+
* ⚠️ **`ms <= 0` = 「不睡」(立即 resolve),且这一判先于 aborted 检查** —— 别把它
|
|
142
|
+
* 「修」成「已中止就该 reject」。这是本仓库统一的「非正数 = 机制关掉」口径,与
|
|
143
|
+
* `withTimeout(p, 0)` 的「不设超时、原样透传」对称:预算 ≤0 时这次等待压根不存在,
|
|
144
|
+
* 也就没有「被中断」可言(取消会在**下一步**(下一个 fetch / 下一轮循环)照常浮出来)。
|
|
145
|
+
* 已有用例钉住这个语义:`tests/core/sse-text-stats.test.ts`(已中止 + `ms<=0` 仍 resolve)
|
|
146
|
+
* 与 `tests/core/timeout.test.ts`(`withTimeout` 预算非正数)。2026-09-19 的外部复核
|
|
147
|
+
* 曾把它当「一致性缺口」改反过一次,被这条用例拦住 —— 保留该用例,别删。
|
|
137
148
|
*/
|
|
138
149
|
export function interruptibleSleep(ms, signal, abortMessage = '已被取消') {
|
|
139
150
|
if (ms <= 0)
|
package/dist/core/timeout.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../src/core/timeout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,sCAAsC;AACtC,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAErD;;;;;;;;;;GAUG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC5B,IAAI,GAAc,SAAS,CAAC;IACrC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,CAAU;IACvC,IAAI,CAAC,YAAY,YAAY;QAAE,OAAO,IAAI,CAAC;IAC3C,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,CAAC,GAAG,CAAuC,CAAC;IAClD,OAAO,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,CAAa,EACb,SAAiB;IAEjB,IAAI,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,SAAS,GAAG,
|
|
1
|
+
{"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../src/core/timeout.ts"],"names":[],"mappings":"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"}
|