@migor/agentia 0.7.0 → 0.7.2
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 +198 -1
- package/README.md +1 -1
- package/dist/AGENTS.md +160 -16
- 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 +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/anthropic.d.ts +5 -0
- package/dist/integrations/anthropic.js +57 -4
- package/dist/integrations/anthropic.js.map +1 -1
- package/dist/integrations/mcp-http.d.ts +43 -0
- package/dist/integrations/mcp-http.js +250 -0
- package/dist/integrations/mcp-http.js.map +1 -0
- package/dist/integrations/mcp-stdio.d.ts +46 -0
- package/dist/integrations/mcp-stdio.js +230 -0
- package/dist/integrations/mcp-stdio.js.map +1 -0
- package/dist/integrations/mcp.d.ts +46 -73
- package/dist/integrations/mcp.js +24 -421
- package/dist/integrations/mcp.js.map +1 -1
- package/dist/integrations/metrics-otlp.d.ts +42 -0
- package/dist/integrations/metrics-otlp.js +178 -0
- package/dist/integrations/metrics-otlp.js.map +1 -0
- package/dist/integrations/metrics-render.d.ts +2 -0
- package/dist/integrations/metrics-render.js +145 -0
- package/dist/integrations/metrics-render.js.map +1 -0
- package/dist/integrations/metrics-state.d.ts +191 -0
- package/dist/integrations/metrics-state.js +363 -0
- package/dist/integrations/metrics-state.js.map +1 -0
- package/dist/integrations/metrics.d.ts +3 -86
- package/dist/integrations/metrics.js +28 -650
- package/dist/integrations/metrics.js.map +1 -1
- package/dist/integrations/openai.js +25 -3
- 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 +3 -2
- package/dist/store/fsStore.js +20 -6
- 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 +81 -1
- package/dist/transport/async.js +323 -11
- 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 +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,201 @@
|
|
|
5
5
|
(0.x 阶段:minor 可含破坏性变更,每个破坏性变更都在对应版本的「迁移」小节里写明)。
|
|
6
6
|
决策的完整证据链在 `docs/spec.md` §10(带时间线的决策日志)。
|
|
7
7
|
|
|
8
|
+
## [0.7.2] - 2026-09-20
|
|
9
|
+
|
|
10
|
+
### 变更
|
|
11
|
+
|
|
12
|
+
> 本版主题(窗口 `0.7.1 → 0.7.2`,含 #77–#81):**第九轮评审收口** —— 一条高优先级的
|
|
13
|
+
> **HITL × `sessionStore` 组合破口**(挂起任务恢复后历史翻倍、成功后又把坏会话写回去),
|
|
14
|
+
> 外加六条中/低优先级的静默失效修复;其余是纯结构重构、回归测试补课与依赖升级。
|
|
15
|
+
> **无破坏性变更,不需要改代码**:所有修复都是「旧行为本来就不该那样」。
|
|
16
|
+
> **两处错误归类收紧(记账口径,不是行为变更)**:Anthropic 流内 4xx 不再落成可重试的
|
|
17
|
+
> `server`,异步宿主的 `runTimeoutMs` 超时不再落 `unknown`。宿主若按 `error.type` /
|
|
18
|
+
> `errorKind` / `retryable` 分支(重试、告警、看板),取值会更准 —— 见下方两条。
|
|
19
|
+
> 本条目的多数内容**是在本次发版时回填的**:窗口内 5 个 PR 都没写 CHANGELOG,
|
|
20
|
+
> 条目按各提交正文重建。
|
|
21
|
+
|
|
22
|
+
### 修复(第九轮评审收口)
|
|
23
|
+
|
|
24
|
+
- **HITL × `sessionStore`:挂起任务恢复后会话历史翻倍 + 成功后毒化会话**(本版最高优先级):
|
|
25
|
+
恢复段曾把 session 再注入 `app.run`,而挂起段落库的 `suspendedMessages` 已含完整历史 ⇒
|
|
26
|
+
run 层 `loadSession` 又 prepend 一遍(历史翻倍、token 复利);且成功后 `appendSession`
|
|
27
|
+
会把含未决 `tool_use` 的整段扩展历史写回会话 —— 该会话**下一轮直接撞 API 400**
|
|
28
|
+
(孤立 `tool_use` + 连续 assistant)。现在恢复段不再注入 session,会话回写改由
|
|
29
|
+
`AsyncRunner` 按「本轮用户输入 + 最终回复」补,口径与 `run.ts` 的三条不变量一致。
|
|
30
|
+
- **惰性审批超时 `#expireAndResume` 补重入闸 + 进闸后重读 store**:异步 store 下两个并发
|
|
31
|
+
`poll` 各拿到一份 `awaiting` 副本 ⇒ 双双填超时拒绝、**双双派发**(同一任务跑两遍)。
|
|
32
|
+
现在与 `approve` 共用同一把 per-taskId 在飞闸。
|
|
33
|
+
- **MCP StreamableHTTP 连接器不再丢弃 `abandoned`**:`tools/call` 把引擎的「放弃等待」
|
|
34
|
+
信号透传到在飞 `fetch`(含会话 404 自愈那次重试)—— 裁判放弃后真能掐掉请求,
|
|
35
|
+
而不是继续在后台烧。stdio 侧 #75 的 `pending` 出簿修复这次补上了回归用例。
|
|
36
|
+
- **Anthropic 流内 `error` 事件的 status 反推补 4xx 档**:`invalid_request_error` /
|
|
37
|
+
`authentication_error` / `permission_error` / `not_found_error` → 400(归 `api`、
|
|
38
|
+
**不可重试**),与 `openai.ts` 同口径。此前这些确定性病因落 500 + `retryable:true`,
|
|
39
|
+
引擎会白重试 3 次(3 次网络请求 + 3 倍等待)。
|
|
40
|
+
- **异步宿主 `runTimeoutMs` 的超时错误不再落 `unknown`**:改用 `core/timeout.ts` 的
|
|
41
|
+
`TimeoutError`(`code='timeout'`),与引擎工具超时 / MCP 桥兜底同口径。归类由
|
|
42
|
+
`unknown`(`retryable:false`)变为 `timeout`(`retryable:true`)—— 这只是**记账口径**:
|
|
43
|
+
超时按定义属可重试故障,框架**不会**因此自动重跑整个 run,要不要重试仍由宿主决定。
|
|
44
|
+
- **`FileTaskStore.save` 改为先落盘、成功后再更新内存**:落盘失败时内存不再推进,
|
|
45
|
+
避免内存与磁盘两本账、重启后静默回退(`get` / `byIdempotency` 查不到未落盘的记录)。
|
|
46
|
+
- **Anthropic SSE 组装拦畸形 `content_block_start.index`**:超大 index(如 `1e9`)会造出
|
|
47
|
+
稀疏数组、后续 `for...of` / `reduce` 按 `length` 空转;现在畸形 index(负数 / 非整数 /
|
|
48
|
+
超上限)响亮抛 `AnthropicApiError(500)`。
|
|
49
|
+
|
|
50
|
+
### 重构(纯结构,零行为变化)
|
|
51
|
+
|
|
52
|
+
- `integrations/metrics.ts`(1050 行)→ `metrics-state.ts` / `metrics-render.ts` /
|
|
53
|
+
`metrics-otlp.ts` / `metrics.ts`(只留选项校验、定时器与组装);
|
|
54
|
+
`integrations/mcp.ts`(897 行)→ `mcp-stdio.ts` / `mcp-http.ts` / `mcp.ts`
|
|
55
|
+
(桥 + 共享 helper + re-export 两个连接器工厂)。**公共面与运行时行为不变**。
|
|
56
|
+
|
|
57
|
+
### 测试
|
|
58
|
+
|
|
59
|
+
- 补上 #75 遗留的回归用例:stdio MCP 连接器「裁判放弃等待 ⇒ `pending` 记账出簿」的防泄漏
|
|
60
|
+
修复此前**没有任何用例守着**(夹具新增 `silentcall` 模式:握手 / `tools/list` 正常、
|
|
61
|
+
`tools/call` 永不回包)。
|
|
62
|
+
|
|
63
|
+
### 依赖
|
|
64
|
+
|
|
65
|
+
- 开发依赖:`@anthropic-ai/sdk` → 0.126.0、`@biomejs/biome` → 2.5.14、
|
|
66
|
+
`@types/node` → 26.6.1、`zod` → 4.6.5(均不影响运行时面)。
|
|
67
|
+
- 官网(不随包发布):`astro` → 7.3.3。
|
|
68
|
+
|
|
69
|
+
## [0.7.1] - 2026-09-19
|
|
70
|
+
|
|
71
|
+
### 变更
|
|
72
|
+
|
|
73
|
+
> 本版主题(窗口 `0.7.0 → 0.7.1`,含 #71–#75):**HITL 人工审批**(挂起 / 恢复、跨进程耐久)、
|
|
74
|
+
> **gRPC 宿主配方与可跑示例**,以及**连续四轮复审收口**(第八轮「时间维度」、外部四路复审、
|
|
75
|
+
> 外部复核的复核)。这批改动绝大多数是同一族病灶 —— **「不报错地不干活」**:超时不清簿记、
|
|
76
|
+
> 重试不排空响应体、非流式回落零校验、回调通道缺失、幂等键赢家跨重启易主、门禁被 `&&`
|
|
77
|
+
> 短路静默跳过。
|
|
78
|
+
> **两处使用者会遇到的行为变更**:① OpenAI 兼容端点返回 legacy `function_call` 形态时,
|
|
79
|
+
> 不再静默以 `end_turn` 收尾,而是抛 400(确定性不兼容 ⇒ 不重试),见下方
|
|
80
|
+
> 「legacy `function_call` 形态响亮失败」;② MCP / 预算护栏 / 异步宿主的若干语义收紧
|
|
81
|
+
> (会话过期自愈加互斥、`close()` 超时不挂死、未配 `sessionStore` 时 `submit` 响亮失败),
|
|
82
|
+
> **都不需要改代码**。
|
|
83
|
+
> **一处破坏性变更(仅类型面,运行时行为不变)**:`RecorderBackend` 新增必填成员 `usage()`;
|
|
84
|
+
> `BudgetGuard.check` 入参由 `Trace` 收窄为 `{ readonly totalUsage: Usage }` ——
|
|
85
|
+
> 迁移写法见本节末的「迁移」小节。
|
|
86
|
+
|
|
87
|
+
### 修复(外部四路复审收口:文档承诺了代码没做的事)
|
|
88
|
+
|
|
89
|
+
- **`AsyncRunner.approve` 补并发重入闸 + 真落库再派发**(HITL 补丁):并发 approve
|
|
90
|
+
(双击「批准」/两个审批人同时批)曾在 store 往返窗口内各自判「决定齐了」、
|
|
91
|
+
**各派发一次**(同一任务重复执行);且它用着吞错的 `#safeSave` 却在注释里承诺
|
|
92
|
+
「先落库再派发」。现在并发调用共享在飞那次(第一次决定赢),落库失败 ⇒
|
|
93
|
+
调用方收到 reject、**绝不派发**。
|
|
94
|
+
- **MCP StreamableHTTP 连接器:`tools/call` 不再起第二个计时器**(`timeoutMs` 只管
|
|
95
|
+
装配期的握手/`tools/list`,与 stdio 侧对称;工具调用的裁判仍是引擎的
|
|
96
|
+
`toolTimeoutMs`)。顺带修复:非 SSE 响应的 id 改为**等值配对**(原只验「id 是
|
|
97
|
+
number」,串包时会把别的请求的结果当本次的返回)。
|
|
98
|
+
- **gRPC 示例 `getTask` 补 try/catch**:grpc-js 不接管 async handler 的 Promise,
|
|
99
|
+
store 抛错会以 unhandledRejection 终止进程。
|
|
100
|
+
- **soak 脚本两处假绿**:采样不足时「跳过内存断言」却照打「内存有界」⇒ 采样间隔
|
|
101
|
+
随时长缩放、样本不足硬失败;宽区间失败率断言换成**逐笔对账**(每个不可重试故障
|
|
102
|
+
恰好杀死一个 run:`failed ≥ 注入数` 且超出部分 ≤ 请求的 0.1%)。
|
|
103
|
+
- 文档对齐:usage-guide 曾写同步 `/run` 返回「含 `suspendedMessages`」(响应体没有
|
|
104
|
+
该字段)—— 改文档:要审批请走 `POST /tasks` 异步宿主。
|
|
105
|
+
|
|
106
|
+
### 修复(第八轮复审:时间维度的三处破口)
|
|
107
|
+
|
|
108
|
+
- **子 agent / skill 被 `toolTimeoutMs` 超时后,capability span 在交付的 trace 里永不收尾,
|
|
109
|
+
且后台继续烧的 token 不进任何观测面**。新增 `ToolRunContext.abandoned`:引擎「放弃等待」
|
|
110
|
+
时 abort 它 —— @SubAgent / @Skill 收到信号即中止子循环(在飞请求被掐掉),capability
|
|
111
|
+
span 立刻以 `error`(`timeout`)收尾(trace 是浅拷交付的,等子循环自己 settle 再关
|
|
112
|
+
就进不了已交付的那份)。自定义工具要「真停」同样监听它。
|
|
113
|
+
- **Scheduler 的 `at()` / `every()` 挡 32 位定时器溢出**:超过 2³¹-1ms(约 24.86 天)的
|
|
114
|
+
延迟会被 Node **静默**钳到 1ms —— `at(30 天后)` 变成立即触发、`every(34 天)` 退化成
|
|
115
|
+
每毫秒空转。现在构造期抛错(与 `runTimeoutMs` 同款防线)。
|
|
116
|
+
- **skill 方法体 `try/catch` 掉 `ctx.llm()` 失败并降级时,capability span 不再被误标
|
|
117
|
+
error**(旧实现把「子运行失败」提前烙进 span,幂等守卫让整体成功的调用翻不了案)。
|
|
118
|
+
|
|
119
|
+
### 新增(HITL 人工审批:挂起/恢复,跨进程耐久)
|
|
120
|
+
|
|
121
|
+
- **审批 = 异步 tool_result**:`@Tool({ approval: 'required' })`(或裸 `AgentTool` 的 `approval`
|
|
122
|
+
字段)声明后,模型每次调用该工具都会把任务**挂起**:run 状态变为 `awaiting_approval`,
|
|
123
|
+
**整个回合一个工具都不执行**(回合级全有或全无 —— 协议要求每个 tool_use 配对 tool_result);
|
|
124
|
+
完整消息历史(末尾是含未决 tool_use 的 assistant 消息)与待决清单(`pendingApprovals`)
|
|
125
|
+
随 `TaskRecord` 落库,进程重启不丢。
|
|
126
|
+
- **恢复**:`runner.approve(taskId, decisions, { decidedBy? })` 或
|
|
127
|
+
`POST /tasks/:id/approve`(body `{ decisions: { <tool_use_id>: { approved, reason? } }, decidedBy? }`)。
|
|
128
|
+
**逐 tool_use_id 幂等**(第一次决定赢);决定齐了整个回合恢复执行:批准的工具正常执行
|
|
129
|
+
(工具体内经 `ToolRunContext.approval` 读到决定),拒绝的得 `tool_result(is_error,
|
|
130
|
+
'审批被拒绝:…')`(理由回给模型,可自行换路)。恢复后再遇未决审批 ⇒ 再次挂起(可等多轮)。
|
|
131
|
+
引擎侧的恢复是**通用**的:任何「assistant 结尾带 tool_use」的消息历史喂给
|
|
132
|
+
`runAgent` / `app.run` 都会先解决这些 tool_use 再调模型。
|
|
133
|
+
- **状态语义**:`awaiting_approval` 是**非终态**(`awaitTask` 继续等)、不占并发槽、
|
|
134
|
+
不触发 `TaskSink.onFinished`、`resumePending` 不捡(它不是孤儿,是在等人)、
|
|
135
|
+
InMemory 淘汰跳过、同幂等键重复 submit 返回等待中的任务。挂起段的 trace **照常投递
|
|
136
|
+
sinks**;恢复段是一棵新树,经根 span 的 `links` 挂到上一段 runId。
|
|
137
|
+
- **审批超时兜底**:`new AsyncRunner(app, { approvalTimeoutMs })`(缺省 0 = 一直等)。
|
|
138
|
+
**惰性判定、不起定时器**:`approve` / `poll` / `resumePending` 读到过期挂起任务时,
|
|
139
|
+
自动把全部待决项写成「拒绝:审批超时」并恢复执行。
|
|
140
|
+
- 新公共面:`ApprovalDecision` 类型;`RunAgentOptions.approvals` / `RunInvocationOptions.approvals`;
|
|
141
|
+
`AgentRunResult.suspendedMessages` / `pendingApprovals`(字段恒在场,未挂起为 `undefined`);
|
|
142
|
+
`AgentStopReason` 与 `RunStatus` 各增 `'awaiting_approval'`;`AsyncRunner.approve` /
|
|
143
|
+
`approvalTimeoutMs`;trace 事件 `approval.requested` / `approval.decided`(带 waitedMs)。
|
|
144
|
+
- 已知边界(详见 usage-guide §7):超时惰性判定;指标按段计;批准后崩溃 ⇒ at-least-once
|
|
145
|
+
重执行;预算口径在恢复段重新起算;嵌套能力(子 agent / skill 子循环)内的审批工具
|
|
146
|
+
不支持挂起整个 run;同步 `POST /run` 撞上审批会带 `awaiting_approval` 返回(要审批请走
|
|
147
|
+
`/tasks`)。
|
|
148
|
+
|
|
149
|
+
### 修复(外部复核的复核 + 三条一致性缺口收口)
|
|
150
|
+
|
|
151
|
+
对上一轮「四路复审」的修复(31 个文件)逐条复核并实测通过;另补三条「守卫没覆盖自己
|
|
152
|
+
声称范围」的缺口与一处护栏开销:
|
|
153
|
+
|
|
154
|
+
- **MCP HTTP `close()` 的 DELETE 现在过 `guard`**:此前直接 `await fetchImpl(...)`,外层
|
|
155
|
+
`catch` 只兜得住**抛错**、兜不住**挂死** —— server 接受连接后不回(半开 / 卡在代理后面),
|
|
156
|
+
`close()` 就永久挂住,而调用方是**宿主停机路径**(挂住比失败更糟)。fetch 与 body 排空
|
|
157
|
+
一起进 guard(只护住响应头,`readText` 照样能卡)。摘掉修复 ⇒ 新用例在 8s 测试预算下
|
|
158
|
+
被 `cancelledByParent` 取消。
|
|
159
|
+
- **`asset()` 拦绝对路径**:守卫此前只拦带 scheme 的 `rel`,但 `/etc/passwd` 与
|
|
160
|
+
`file:///etc/passwd` 是同一类(`new URL` 会把 base 的路径部分整个丢掉)——
|
|
161
|
+
「以为读了能力目录里的文件,实际读了别处」(macOS 上**真能读到**)。`../` 仍放行
|
|
162
|
+
(它是相对 base 解析的,base 没被忽略)。
|
|
163
|
+
- **预算护栏改走廉价 usage**:新增 `TraceRecorder.usage()`(只扫 spans 求和、不拷
|
|
164
|
+
attributes/events),`snapshot().totalUsage` 改为调它 ⇒ 两条路不可能漂移。护栏每回合
|
|
165
|
+
要判**两次**(回合入口 + 回合末,是不同决策点,**刻意不合并**),此前每次都全量
|
|
166
|
+
`snapshot()` ⇒ O(回合 × 累计事件量) 的白拷。
|
|
167
|
+
- 测试基建:`mcpConnector` 的 stdio 握手缺省预算 5s → 20s(`node --test` 按文件并行,
|
|
168
|
+
重负载下**子进程启动**本身就可能吃掉数秒;断超时行为的那条用例自带 `timeoutMs`)。
|
|
169
|
+
- 复核结论一条「查过但不改」:`interruptibleSleep(0, 已中止的 signal)` 仍 **resolve** ——
|
|
170
|
+
它与 `withTimeout(p, 0)` 的「非正数 = 机制关掉」是同一口径,不是缺口(本轮曾误改,
|
|
171
|
+
被既有用例拦下后回退;那条用例的断言已从隐式 `await` 改成显式 `assert.doesNotReject`
|
|
172
|
+
并写明理由,见 spec §10 2026-09-19 ④)。
|
|
173
|
+
|
|
174
|
+
**迁移(自己实现这两个结构面时)**
|
|
175
|
+
|
|
176
|
+
- `BudgetGuard.check` 的入参由 `Trace` 收窄为 `{ readonly totalUsage: Usage }`。
|
|
177
|
+
**传整份 `Trace` 的调用方不受影响**(结构上满足);自己实现 `BudgetGuard` 的代码需把
|
|
178
|
+
签名改宽,且**不得再读 `spans`**(类型上已读不到 —— 「只看 totalUsage」由注释变成约束)。
|
|
179
|
+
- `RecorderBackend` 新增必填成员 `usage(): Usage`。自己实现该结构面(或自建 recorder
|
|
180
|
+
替身)的代码需补上。
|
|
181
|
+
|
|
182
|
+
### 新增(gRPC 宿主配方与可跑示例)
|
|
183
|
+
|
|
184
|
+
- **不对 Kafka / gRPC 做「服务包」**,但把真缺的那一块做成配方 + 可跑示例:gRPC 宿主必须
|
|
185
|
+
自己接上的**四处**(deadline / 取消 → `signal`、`metadata` 的 `traceparent` → `traceContext`、
|
|
186
|
+
框架错误 → gRPC 状态码、trace → sink),四处漏掉**都不报错**。判别规则只有一条 ——
|
|
187
|
+
客户端是不是标准库(MCP 用 `spawn` + `fetch` 所以能内置,gRPC / Kafka 要引第三方客户端),
|
|
188
|
+
决策见 spec §10 2026-09-18 ⑪。
|
|
189
|
+
- `docs/usage-guide.md` §6.2 新增「gRPC 宿主」配方;`examples/grpc-host/` 给了 proto + 宿主 +
|
|
190
|
+
客户端 + README(一元 / 服务端流 / 异步投递 / 查任务,`PORT=0` 自报端口、无抢占窗口);
|
|
191
|
+
`scripts/e2e-grpc.ts` 真构建真起宿主、用它自带的客户端跑四个 RPC,并入 `npm run e2e` 第四步。
|
|
192
|
+
|
|
193
|
+
### 修复(OpenAI 兼容端点:legacy `function_call` 形态响亮失败)
|
|
194
|
+
|
|
195
|
+
- 兼容端点把工具调用放在 `message.function_call` 时,适配器只读 `tool_calls` ⇒ 此前落进
|
|
196
|
+
`default: return 'end_turn'`,**模型要调的工具被丢掉、run 却以成功收尾**(模块头写的正是
|
|
197
|
+
「上游故障绝不映射成成功」)。现在 `function_call` 抛 `OpenAICompatApiError(400)`:这是
|
|
198
|
+
**确定性不兼容** ⇒ 落 `classifyError` 的 `api` / 不可重试(用 500 会被引擎重试三次,每次
|
|
199
|
+
都重复丢弃同一个调用);也**不做 legacy 兼容**——请求侧只发 `tool_calls`,回灌的
|
|
200
|
+
`role:'tool'` legacy-only 端点同样吃不下,半吊子支持比不支持更糟。`default` 仍是有意的
|
|
201
|
+
`end_turn`(未知值**且有正文**),并补 `eos_token` 用例把这个有意默认钉住,防后人顺手改成抛错。
|
|
202
|
+
|
|
8
203
|
## [0.7.0] - 2026-09-18
|
|
9
204
|
|
|
10
205
|
### 变更
|
|
@@ -481,7 +676,9 @@
|
|
|
481
676
|
首个公开发布:`@migor/agentia` + `@migor/cli`(scope `@migor/*`),两包版本同步。
|
|
482
677
|
框架本体单包;CLI 独立成包(workspaces)。
|
|
483
678
|
|
|
484
|
-
[Unreleased]: https://github.com/retrychx/agentia/compare/v0.7.
|
|
679
|
+
[Unreleased]: https://github.com/retrychx/agentia/compare/v0.7.2...HEAD
|
|
680
|
+
[0.7.2]: https://github.com/retrychx/agentia/releases/tag/v0.7.2
|
|
681
|
+
[0.7.1]: https://github.com/retrychx/agentia/releases/tag/v0.7.1
|
|
485
682
|
[0.7.0]: https://github.com/retrychx/agentia/releases/tag/v0.7.0
|
|
486
683
|
[0.6.3]: https://github.com/retrychx/agentia/releases/tag/v0.6.3
|
|
487
684
|
[0.6.2]: https://github.com/retrychx/agentia/releases/tag/v0.6.2
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ $EDITOR my-app/.env # 填 ANTHROPIC_API_KEY(也可直接 expo
|
|
|
30
30
|
> 框架**不自动**读 `.env`:脚手架 `src/main.ts` 首行的 `loadEnvFile()` 负责把它读进 `process.env`。
|
|
31
31
|
> 真实环境变量优先(CI / docker / 命令行永远赢过文件),`loadEnvFile({ override: true })` 才反过来。
|
|
32
32
|
|
|
33
|
-
> **版本**:`0.7.
|
|
33
|
+
> **版本**:`0.7.2`(`@migor/agentia` 与 `@migor/cli` 均已发布到 npm)。`examples/` **刻意**用
|
|
34
34
|
> `file:../..` 指向本仓库而不是 npm 版本 —— 跑的是**工作区代码**,理由见
|
|
35
35
|
> [`examples/README.md`](examples/README.md) 的「依赖说明」。
|
|
36
36
|
|
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 已知边界。
|
|
@@ -981,10 +1059,13 @@ createApp({ system, providers: [...], middleware: [quota], sinks: [billing] });
|
|
|
981
1059
|
- 拦下来的那次 run **仍然要记账**(模型的钱已经花了)—— 记账在 sink 里、拦截在 middleware 里,两者独立。
|
|
982
1060
|
- 被拦下的能力**不会执行**(副作用不发生),但 run 继续跑(模型可以换路)。
|
|
983
1061
|
|
|
984
|
-
####
|
|
1062
|
+
#### 人工审批
|
|
1063
|
+
|
|
1064
|
+
两种形态,按「人什么时候在场」选:
|
|
985
1065
|
|
|
986
|
-
|
|
987
|
-
|
|
1066
|
+
**A. 进程内闸门(middleware)** —— 审批方能被一次 `await` 等到(同进程回调 / 短等待)时用,
|
|
1067
|
+
不需要任何新机制 —— `middleware` 可以 `await` 决策再放行,引擎会等工具结果
|
|
1068
|
+
(`Promise.resolve(tool.run(...))`):
|
|
988
1069
|
|
|
989
1070
|
```ts
|
|
990
1071
|
const DANGEROUS = new Set(['send_email', 'deploy', 'delete_records']);
|
|
@@ -1007,9 +1088,59 @@ createApp({ system, providers: [...], middleware: [requireApproval] });
|
|
|
1007
1088
|
(ALS 传播,见上一节)。
|
|
1008
1089
|
- 等待不会被默认掐断(`toolTimeoutMs` 缺省 0 = 不限);真设了它,注意别把人的思考时间算进去。
|
|
1009
1090
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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 表的「审批」相关行。
|
|
1013
1144
|
|
|
1014
1145
|
#### 内容护栏
|
|
1015
1146
|
|
|
@@ -1039,8 +1170,8 @@ const callable = {
|
|
|
1039
1170
|
在**工具实现内部**做(Docker / 子进程 / 微 VM 随你),框架不参与也不该参与 ——
|
|
1040
1171
|
`AgentTool.run(input) → output` 这个契约把隔离整个挡在实现里。
|
|
1041
1172
|
|
|
1042
|
-
唯一沾边的一条:工具起了子进程,**取消时要自己 kill**。框架的 `toolTimeoutMs`
|
|
1043
|
-
|
|
1173
|
+
唯一沾边的一条:工具起了子进程,**取消时要自己 kill**。框架的 `toolTimeoutMs` 是「不等了」,
|
|
1174
|
+
放弃等待时会 abort `ToolRunContext.abandoned` —— 要能真停,让工具监听它(或读 `ToolRunContext.signal` 响应整条 run 的中止)。
|
|
1044
1175
|
|
|
1045
1176
|
---
|
|
1046
1177
|
|
|
@@ -1071,13 +1202,14 @@ const callable = {
|
|
|
1071
1202
|
| 停机不由框架触发 | 框架给 `drain()` 但**不订阅** `SIGTERM`/`SIGINT`(不做进程级决策);信号处理是宿主的 |
|
|
1072
1203
|
| 停机可能切断 SSE | `drain()` 超时后会强制关闭仍开着的 SSE 流,其 run 以 `stopReason='aborted'` 收尾 —— 客户端应把断流当作可重试 |
|
|
1073
1204
|
| 鉴权失败即断连 | 未通过鉴权时在读到 body 之前就回响应,连接**不可复用**(显式 `connection: close`);这是「不收body省资源」的代价 |
|
|
1074
|
-
| 预算护栏不是硬实时 |
|
|
1205
|
+
| 预算护栏不是硬实时 | 一回合记账完才判,实际用量可能超上限一个回合的量;并行子循环(一回合多个子 agent)各自过闸,超支上限是「**每个在飞分支**各一个回合」而非「总共一个回合」;模型自然收尾的那回合超限**不算失败**(只留 `budget.exceeded` 事件) |
|
|
1075
1206
|
| `maxCostUsd` 依赖价格表 | 模型不在价格表内(且未用 `priceOverrides` 覆盖)时成本恒为 0,这条护栏**不触发** —— 要无条件兜底用 `maxTotalTokens`。**失效会响**:turn 上会记 `usage.unpriced` 事件、指标有 `model_unpriced_turns_total`、可回调 `onUnpricedModel` |
|
|
1076
|
-
|
|
|
1207
|
+
| 工具超时**不强制取消**工具 | `AgentTool.run` 没有 signal 参数,超时首先是「不等了」;副作用可能已发生。想真停:监听 `ToolRunContext.abandoned`(引擎放弃等待时 abort 它)自行收尾。框架自带的 @SubAgent / @Skill 已这么做 —— 超时即中止子循环(在飞请求被掐、不再后台烧 token),capability span 立刻以 error 收尾 |
|
|
1077
1208
|
| 会话只存对话轮次 | `SessionStore` 存「用户输入 + 最终回复」,run 内部的 tool 往返**不进历史**(要完整过程用 `traceToMessages`);且只有**跑成功**的轮次才回写 |
|
|
1078
1209
|
| 同 session 并发 run 要自行串行化 | `SessionStore` 是 **append-only**:并发写不互相覆盖、不丢数据,但**不保证角色交替** —— 两个并发 run 共用同一 sessionId 时,各自追加的轮次可能交错成「连续两条 user」,下一轮 load 出来撞角色交替校验(400)。同一 session 的并发 run 请调用方自行串行化(每 session 一把锁 / 一条队列) |
|
|
1079
1210
|
| OpenAI 适配器听端点的话 | 请求发 `stream:true`,但**按响应形态解析**:端点回 JSON 就退回一次性(没有打字机效果),回 `event-stream` 才逐 token |
|
|
1080
1211
|
| OpenAI 流式的上游故障按失败处理 | 三种形态都**抛错**按失败处理:流中 `error` 分片(上游把故障塞进 200 的流;按 `type`/`code` 反推 status,限流能被引擎重试认出);**未收到 `[DONE]` 也无 `finish_reason`**(流被上游/代理截断 —— 哪怕已吐出半句、有累积文本,也按不完整响应抛错,不报 `end_turn`);正常终止却无任何文本与工具调用(与非流式空 `choices` 同一守卫)。**例外**:`finish_reason=content_filter` 的空流是合法 refusal,不抛 —— 与非流式路径同一个响应同一个结论 |
|
|
1212
|
+
| OpenAI 兼容端点回 legacy `function_call` 形态时**不支持** | 适配器只认现代 `tool_calls`(请求侧也只发这个形态)。收到 `finish_reason=function_call` 会**响亮失败**(400,落 `api`/不可重试),**不**按 `end_turn` 收尾 —— 那种回法里的调用在 `message.function_call` 里、读不出来,报成正常收尾会让「模型要调工具、工具却没执行」记成成功。换支持 `tool_calls` 的端点或模型即可(legacy `functions` 形态 OpenAI 2023 已废弃) |
|
|
1081
1213
|
| MCP 只做 tools | `sampling`(server 反向请求模型)/ `resources` / `prompts` 原语不做;出厂连接器同样只做 `tools/list` + `tools/call` |
|
|
1082
1214
|
| MCP 的协议层错误框架看不见 | `isError: true` 只有连接器能看见 —— 它必须转成抛错,否则模型收到的是一条「成功」的结果(出厂连接器已代你处理) |
|
|
1083
1215
|
| MCP 超时同样是「不等了」 | 桥的 `timeoutMs` 取消不了 server 侧执行(拿不到取消句柄);它只是**兜底** —— 引擎设了 `toolTimeoutMs` 时**不参与**判定(一次调用只有一个裁判;**显式 `toolTimeoutMs: 0` 也算设了** —— 那是引擎表态「不限」,桥不会再自作主张判 60s),两条路径**同判定、同账**(`errorKind='timeout'`) |
|
|
@@ -1100,7 +1232,19 @@ const callable = {
|
|
|
1100
1232
|
| 评分来自 run 之外 | `Score` 走 run 根 `score` **事件**而非 span 字段(评分通常在 run 跑完后才产生);`attachScore` 找不到根 span 时静默忽略,多次调用即多条事件(不同维度各记各的) |
|
|
1101
1233
|
| 链路关联只做**入站** | `traceContext` / `traceparent` 头只把**上游**接进来(run 根的 `links`);框架**不生成**出站 `traceparent` —— 运行中没有「当前 span」可导出,硬造会给出假 spanId。要从 run 往外传,用 `result.trace.traceId`(== `runId`)自行拼头。另:link 只落在 run 根(子 span 不散),且**一进程内**不跨进程自动传播 —— 队列场景要自己把 `traceContext` 传下去(HTTP 头带走,或随 `TaskRecord.spec.options` 落库) |
|
|
1102
1234
|
| 配额不是框架子系统 | 只给缝(middleware + TraceSink + BudgetGuard),计数放哪(内存 / Redis / DB)与超限怎么办都是你的策略 |
|
|
1103
|
-
|
|
|
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
|
+
| 恢复段的会话回写是进程内快照 | 带 `sessionId` 的任务挂起时,「本轮用户输入」快照只存进程内存(恢复段由 AsyncRunner 自己补写「用户输入 + 最终回复」,不再经 run 层重复拼历史)—— 进程崩在「挂起 → 重启 → approve」之间会**丢这一次会话回写**(会话少一轮,但绝不写进坏历史;审批决定本身已落库) |
|
|
1241
|
+
| 嵌套能力内的审批不支持挂起 | @SubAgent / @Skill 子循环里的 `approval: 'required'` 工具无法把整个 run 挂起 —— 子循环挂起会以 `is_error` 交回主 agent(要审批的能力请放主菜单) |
|
|
1242
|
+
| 同步 `/run` 撞上审批没人可批 | 同步 RPC 会带着 `stopReason: 'awaiting_approval'` 收尾返回 —— 但响应体(`toHttpBody`)**不含** `suspendedMessages`,也没有任务记录可审批(待决清单只能去 trace 的 `approval.requested` 事件里看)。**要审批请走 `POST /tasks` 异步宿主** |
|
|
1243
|
+
| Scheduler 调度表不落库 | `every` / `at` 的调度本身只在内存:已 submit 的任务记录能经 `resumePending` 续跑,但「未来某刻再触发」的调度在重启后不存在(远期单发由宿主自己的 cron 驱动)。另:`drain()` 不停 Scheduler —— 停机窗口内到点的 tick 会打一条触发失败日志(无害但吵),介意就 `scheduler.stop()` 先行 |
|
|
1244
|
+
| file store 的撕裂写只在启动时自愈 | 写入中途失败(磁盘满等)留下的残行由 `healTail` 在**构造期**修复;同进程内继续 append 会把新记录粘在残行尾部、下次启动时一起丢弃 —— 磁盘满告警后先恢复写入能力再继续依赖它 |
|
|
1245
|
+
| 终态落库失败无告警 | AsyncRunner 终态 save 失败被吞(「不击穿主流程」的代价):store 抖动时任务可能永远停在 `running`,重启后 `resumePending` 会重跑一个**实际已成功**(副作用已发生)的任务 —— 耐久 store 的故障告警是宿主的事 |
|
|
1246
|
+
| `contextPolicy` 不进子循环 | 应用级 `contextPolicy` / `onText` 只对主循环生效:@SubAgent / @Skill 的子运行不做上下文裁剪(长跑子 agent 撞上下文上限会以 api 错误收尾)。预算护栏(`maxTotalTokens` / `maxCostUsd`)正常透传 |
|
|
1247
|
+
| 记忆没有删除语义 | `MemoryStore` 只有 load/save:run 内 `ctx.delete` 掉的键回写时不会从 store 移除(下一轮水合会复活)。要真删请直接操作 store 实现 |
|
|
1104
1248
|
| 内容护栏不给实现 | 同「配额」:只给缝(入参包 `app.run` / 工具前 `middleware` / 出参包返回值或 `sinks`),策略(正则 / 分类器 / 外部 API)是你的 |
|
|
1105
1249
|
| 框架不执行模型生成的代码 | 无沙箱可言:`@Skill` 跑你写的方法、`@Tool` 是你写的函数,模型输出只成文本 / `tool_result`;代码执行工具的隔离是**工具实现内部**的事;工具起的子进程取消时要自己 kill |
|
|
1106
1250
|
|
|
@@ -1122,7 +1266,7 @@ const callable = {
|
|
|
1122
1266
|
| 鉴权钩子抛错,客户端只看到「未通过鉴权」 | 这是设计:非 `HttpException` 的错误原文只进服务端日志(要回给调用方就抛 `HttpException(status, body)`) |
|
|
1123
1267
|
| 停机后 `POST /tasks` 回 503 | `drain()` 已被调用(或注入的 runner 已 drain)—— 这是「拒新单」的正常行为,任务没丢 |
|
|
1124
1268
|
| `stopReason` 是 `budget_exceeded`、任务被判失败 | 这是设计(护栏拦下的 run **没跑完**)。只想「记一笔」不想改结局,就自己用 `createBudgetGuard` 读 trace |
|
|
1125
|
-
| 工具超时了,副作用却还是发生了 |
|
|
1269
|
+
| 工具超时了,副作用却还是发生了 | 超时是「放弃等待」不是强制取消。引擎放弃时会 abort `ToolRunContext.abandoned`(@SubAgent/@Skill 已靠它自中止)—— 你自己的工具要真停就监听它 |
|
|
1126
1270
|
| 用 OpenAI 端点没看到打字机效果 | 端点没按 `stream:true` 回 `event-stream`(回了一整份 JSON)—— 适配器按响应形态解析,此时退回一次性 |
|
|
1127
1271
|
| 改了框架源码却看不到效果 | 确认 import 的是同一份构建产物(`npm run build` 后跑 `dist`) |
|
|
1128
1272
|
| 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
|
/**
|