@migor/agentia 0.9.4 → 0.10.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/CHANGELOG.md +1280 -4
- package/README.en.md +61 -3
- package/README.md +66 -4
- package/dist/AGENTS.md +330 -45
- package/dist/container/container.js +21 -0
- package/dist/container/container.js.map +1 -1
- package/dist/core/limits.d.ts +101 -14
- package/dist/core/limits.js +103 -13
- package/dist/core/limits.js.map +1 -1
- package/dist/core/line-framing.d.ts +36 -0
- package/dist/core/line-framing.js +39 -0
- package/dist/core/line-framing.js.map +1 -0
- package/dist/core/message.d.ts +11 -0
- package/dist/core/run.d.ts +68 -4
- package/dist/core/run.js +27 -1
- package/dist/core/run.js.map +1 -1
- package/dist/core/schema.js +59 -8
- package/dist/core/schema.js.map +1 -1
- package/dist/core/timeout.d.ts +43 -1
- package/dist/core/timeout.js +76 -3
- package/dist/core/timeout.js.map +1 -1
- package/dist/core/tool.d.ts +25 -1
- package/dist/core/trace.d.ts +40 -1
- package/dist/core/trace.js +26 -0
- package/dist/core/trace.js.map +1 -1
- package/dist/engine/budget.js +14 -0
- package/dist/engine/budget.js.map +1 -1
- package/dist/engine/defer.d.ts +49 -0
- package/dist/engine/defer.js +72 -0
- package/dist/engine/defer.js.map +1 -0
- package/dist/engine/errors.d.ts +23 -1
- package/dist/engine/errors.js +24 -0
- package/dist/engine/errors.js.map +1 -1
- package/dist/engine/forwarded.d.ts +14 -5
- package/dist/engine/forwarded.js +4 -1
- package/dist/engine/forwarded.js.map +1 -1
- package/dist/engine/loop-result.d.ts +65 -7
- package/dist/engine/loop-result.js +24 -5
- package/dist/engine/loop-result.js.map +1 -1
- package/dist/engine/loop.d.ts +2 -0
- package/dist/engine/loop.js +157 -17
- package/dist/engine/loop.js.map +1 -1
- package/dist/engine/mcp-server-http.d.ts +2 -0
- package/dist/engine/mcp-server-http.js +215 -0
- package/dist/engine/mcp-server-http.js.map +1 -0
- package/dist/engine/mcp-server-stdio.d.ts +15 -0
- package/dist/engine/mcp-server-stdio.js +106 -0
- package/dist/engine/mcp-server-stdio.js.map +1 -0
- package/dist/engine/mcp-server.d.ts +134 -0
- package/dist/engine/mcp-server.js +232 -0
- package/dist/engine/mcp-server.js.map +1 -0
- package/dist/engine/menu-drift.d.ts +34 -0
- package/dist/engine/menu-drift.js +81 -0
- package/dist/engine/menu-drift.js.map +1 -0
- package/dist/engine/policy.d.ts +2 -1
- package/dist/engine/policy.js +7 -1
- package/dist/engine/policy.js.map +1 -1
- package/dist/engine/replay.d.ts +2 -1
- package/dist/engine/replay.js +3 -2
- package/dist/engine/replay.js.map +1 -1
- package/dist/engine/resume-input.d.ts +10 -0
- package/dist/engine/resume-input.js +11 -0
- package/dist/engine/resume-input.js.map +1 -1
- package/dist/engine/run-config.d.ts +26 -2
- package/dist/engine/run-config.js +60 -0
- package/dist/engine/run-config.js.map +1 -1
- package/dist/engine/spec.d.ts +33 -4
- package/dist/engine/spec.js.map +1 -1
- package/dist/engine/tool-context.d.ts +10 -5
- package/dist/engine/tool-context.js +5 -1
- package/dist/engine/tool-context.js.map +1 -1
- package/dist/engine/tracer.d.ts +43 -5
- package/dist/engine/tracer.js +73 -10
- package/dist/engine/tracer.js.map +1 -1
- package/dist/engine/trimming.js +28 -2
- package/dist/engine/trimming.js.map +1 -1
- package/dist/engine/turn.d.ts +60 -3
- package/dist/engine/turn.js +180 -71
- package/dist/engine/turn.js.map +1 -1
- package/dist/engine/types.d.ts +89 -3
- package/dist/engine/types.js +38 -2
- package/dist/engine/types.js.map +1 -1
- package/dist/eval/export.d.ts +48 -0
- package/dist/eval/export.js +146 -0
- package/dist/eval/export.js.map +1 -0
- package/dist/eval/harvest.js +7 -2
- package/dist/eval/harvest.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/adapter-options.d.ts +22 -0
- package/dist/integrations/adapter-options.js +51 -0
- package/dist/integrations/adapter-options.js.map +1 -1
- package/dist/integrations/anthropic.d.ts +4 -2
- package/dist/integrations/anthropic.js +27 -27
- package/dist/integrations/anthropic.js.map +1 -1
- package/dist/integrations/file-sink.d.ts +20 -0
- package/dist/integrations/file-sink.js +27 -0
- package/dist/integrations/file-sink.js.map +1 -0
- package/dist/integrations/mcp-http.d.ts +3 -3
- package/dist/integrations/mcp-http.js +1 -1
- package/dist/integrations/mcp-http.js.map +1 -1
- package/dist/integrations/mcp-protocol.d.ts +115 -0
- package/dist/integrations/mcp-protocol.js +69 -0
- package/dist/integrations/mcp-protocol.js.map +1 -0
- package/dist/integrations/mcp-stdio.d.ts +3 -3
- package/dist/integrations/mcp-stdio.js +17 -3
- package/dist/integrations/mcp-stdio.js.map +1 -1
- package/dist/integrations/mcp.d.ts +6 -95
- package/dist/integrations/mcp.js +24 -66
- package/dist/integrations/mcp.js.map +1 -1
- package/dist/integrations/metrics-otlp.js +52 -7
- package/dist/integrations/metrics-otlp.js.map +1 -1
- package/dist/integrations/metrics-render.d.ts +15 -0
- package/dist/integrations/metrics-render.js +79 -2
- package/dist/integrations/metrics-render.js.map +1 -1
- package/dist/integrations/metrics-state.d.ts +99 -1
- package/dist/integrations/metrics-state.js +128 -4
- package/dist/integrations/metrics-state.js.map +1 -1
- package/dist/integrations/metrics.d.ts +62 -6
- package/dist/integrations/metrics.js +49 -8
- package/dist/integrations/metrics.js.map +1 -1
- package/dist/integrations/openai.js +9 -31
- package/dist/integrations/openai.js.map +1 -1
- package/dist/integrations/otlp.d.ts +3 -3
- package/dist/integrations/report.d.ts +7 -0
- package/dist/integrations/report.js +20 -1
- package/dist/integrations/report.js.map +1 -1
- package/dist/runtime/memory.d.ts +61 -1
- package/dist/runtime/memory.js +47 -7
- package/dist/runtime/memory.js.map +1 -1
- package/dist/runtime/run.d.ts +12 -2
- package/dist/runtime/run.js +126 -17
- package/dist/runtime/run.js.map +1 -1
- package/dist/store/fsStore.js +10 -4
- package/dist/store/fsStore.js.map +1 -1
- package/dist/store/record.d.ts +41 -0
- package/dist/store/record.js +54 -0
- package/dist/store/record.js.map +1 -0
- package/dist/store/redisStore.d.ts +24 -1
- package/dist/store/redisStore.js +42 -7
- package/dist/store/redisStore.js.map +1 -1
- package/dist/store/sqliteStore.d.ts +23 -0
- package/dist/store/sqliteStore.js +103 -8
- package/dist/store/sqliteStore.js.map +1 -1
- package/dist/store/store.d.ts +65 -2
- package/dist/store/store.js +5 -2
- package/dist/store/store.js.map +1 -1
- package/dist/toolkit/capability-cycles.d.ts +50 -0
- package/dist/toolkit/capability-cycles.js +88 -0
- package/dist/toolkit/capability-cycles.js.map +1 -0
- package/dist/toolkit/capability-slice.d.ts +86 -0
- package/dist/toolkit/capability-slice.js +98 -0
- package/dist/toolkit/capability-slice.js.map +1 -0
- package/dist/toolkit/module.d.ts +7 -1
- package/dist/toolkit/module.js +65 -32
- package/dist/toolkit/module.js.map +1 -1
- package/dist/transport/approval-policy.d.ts +1 -1
- package/dist/transport/approval-policy.js +8 -4
- package/dist/transport/approval-policy.js.map +1 -1
- package/dist/transport/approval-supervisor.d.ts +73 -0
- package/dist/transport/approval-supervisor.js +252 -0
- package/dist/transport/approval-supervisor.js.map +1 -0
- package/dist/transport/async.d.ts +159 -56
- package/dist/transport/async.js +627 -269
- package/dist/transport/async.js.map +1 -1
- package/dist/transport/drain-gate.d.ts +6 -2
- package/dist/transport/drain-gate.js +14 -2
- package/dist/transport/drain-gate.js.map +1 -1
- package/dist/transport/http-endpoints.d.ts +151 -0
- package/dist/transport/http-endpoints.js +480 -0
- package/dist/transport/http-endpoints.js.map +1 -0
- package/dist/transport/http-io.d.ts +66 -0
- package/dist/transport/http-io.js +145 -0
- package/dist/transport/http-io.js.map +1 -0
- package/dist/transport/http-route.d.ts +10 -3
- package/dist/transport/http-route.js +18 -3
- package/dist/transport/http-route.js.map +1 -1
- package/dist/transport/http-shapes.d.ts +11 -0
- package/dist/transport/http-shapes.js +31 -1
- package/dist/transport/http-shapes.js.map +1 -1
- package/dist/transport/http.d.ts +22 -35
- package/dist/transport/http.js +98 -476
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/owner-id.d.ts +29 -0
- package/dist/transport/owner-id.js +46 -0
- package/dist/transport/owner-id.js.map +1 -0
- package/dist/transport/owner-liveness.d.ts +8 -0
- package/dist/transport/owner-liveness.js +56 -0
- package/dist/transport/owner-liveness.js.map +1 -0
- package/dist/transport/resume-policy.d.ts +48 -10
- package/dist/transport/resume-policy.js +25 -10
- package/dist/transport/resume-policy.js.map +1 -1
- package/dist/transport/resume-scanner.d.ts +55 -0
- package/dist/transport/resume-scanner.js +219 -0
- package/dist/transport/resume-scanner.js.map +1 -0
- package/dist/transport/scheduler.js +9 -23
- package/dist/transport/scheduler.js.map +1 -1
- package/dist/transport/signal-supervisor.d.ts +80 -0
- package/dist/transport/signal-supervisor.js +129 -0
- package/dist/transport/signal-supervisor.js.map +1 -0
- package/dist/transport/slot-pool.d.ts +9 -1
- package/dist/transport/slot-pool.js +9 -1
- package/dist/transport/slot-pool.js.map +1 -1
- package/dist/transport/task-events.d.ts +34 -3
- package/dist/transport/task-events.js +93 -5
- package/dist/transport/task-events.js.map +1 -1
- package/dist/transport/wake-policy.d.ts +50 -0
- package/dist/transport/wake-policy.js +43 -0
- package/dist/transport/wake-policy.js.map +1 -0
- package/package.json +14 -4
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— 审批监督:HITL 挂起登记簿(读数)+ 在飞闸 + `approve` + 审批超时恢复。
|
|
3
|
+
*
|
|
4
|
+
* 从 `async.ts` 的 `AsyncRunner` 抽出的**方向正交**的一块(外部深评结构体检的建议②)。
|
|
5
|
+
* 它管的是「谁挂着、谁在等审批、审批超时了没」,与「认领 / 槽位 / 派发 / 执行」不是
|
|
6
|
+
* 一个方向 —— 而抽之前它被私字段(`#suspended` / `#inflightApprovals`)与私有方法缝在
|
|
7
|
+
* 1554 行的 `AsyncRunner` 里,横跨 5 处外部调用点(`cancel` / `signalTask` / 到期唤醒 /
|
|
8
|
+
* `resumePending` 扫描 / `#executeInner` 的挂起与终态出口)+ 对外 getter。
|
|
9
|
+
*
|
|
10
|
+
* **零行为变化**:本文件是对原成员的**逐字搬迁** + 依赖改为构造注入。判据、分支顺序、
|
|
11
|
+
* 错误类型、副作用次序(先落库再派发)一字未改。
|
|
12
|
+
*
|
|
13
|
+
* 为什么 `dispatch` 用**回调注入**而不是 import `async.ts`:`#dispatch` 是 `AsyncRunner`
|
|
14
|
+
* 的私有方法(停机闸 + 唯一派发入口 + 一次性告警),审批簇只**消费**它,不该拥有它、
|
|
15
|
+
* 也不该复制一份。注入让依赖保持单向(审批 → 派发),同时避免模块循环。
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ `TaskApproveError` / `ApprovalDecisions` 随本簇迁到这里;`async.ts` 仍 **re-export**
|
|
18
|
+
* 它们 ⇒ `http-endpoints.ts` / `http-shapes.ts` 的 import 路径不变。
|
|
19
|
+
*/
|
|
20
|
+
import { approvalExpired, approvalsComplete, fillTimeoutDenials } from './approval-policy.js';
|
|
21
|
+
import { summarizeSuspended } from './wake-policy.js';
|
|
22
|
+
import { isThenable } from '../store/store.js';
|
|
23
|
+
/**
|
|
24
|
+
* `approve` 的失败:带 HTTP 语义的状态码(404 = 任务不存在,409 = 任务当前不在
|
|
25
|
+
* `suspended` 状态),HTTP 宿主据此回对应响应。
|
|
26
|
+
* module 级 export —— 不进公共导出面(纯宿主内部实现细节)。
|
|
27
|
+
*/
|
|
28
|
+
export class TaskApproveError extends Error {
|
|
29
|
+
/** 400 = 入参语义不合法(decisions 里有本任务待决之外的 id,2026-09-28 深评 P1-2) */
|
|
30
|
+
status;
|
|
31
|
+
constructor(status, message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'TaskApproveError';
|
|
34
|
+
this.status = status;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class ApprovalSupervisor {
|
|
38
|
+
/**
|
|
39
|
+
* **本进程可见的挂起读数**(`/healthz` 的 `suspended` 段,配套 6)。
|
|
40
|
+
*
|
|
41
|
+
* 口径与 `inFlight` 同一张表:只数**本进程**经手的挂起 —— 跨进程要合并看板请自己聚合,
|
|
42
|
+
* 不假装是全局面(别的进程挂起的记录这里看不见,重启后由首次扫描对齐,见下)。
|
|
43
|
+
*
|
|
44
|
+
* 三条维护纪律(这条读数的唯一风险是**漂移**,所以每条出口都要写清):
|
|
45
|
+
* ① 进挂起时登记(`#executeInner` 的挂起分支,与落库同一个分支);
|
|
46
|
+
* ② 离开挂起时除名(`approve` / 两条恢复路径 / 终态分支);
|
|
47
|
+
* ③ **每次 `resumePending` 扫描按 store 重建**(`rebuild`)—— 扫描本来就 `list()` 了全表,
|
|
48
|
+
* 顺手对齐即可,把「漏了某个出口」从永久漂移降级成「下一次扫描前的偏差」。
|
|
49
|
+
*
|
|
50
|
+
* 为什么不留成 store 查询(每次 /healthz 扫全表):`/healthz` 是探针端点(秒级频率),
|
|
51
|
+
* 而 `list()` 要把**每条记录的完整 trace**取出来(sqlite/redis 下是全部反序列化)——
|
|
52
|
+
* 拿它当健康检查的代价比它回答的问题大得多。
|
|
53
|
+
*/
|
|
54
|
+
#suspended = new Map();
|
|
55
|
+
/** 同一任务的在飞审批(`approve` 的重入闸)。**与惰性超时恢复共用同一把闸** ——
|
|
56
|
+
* `approve` 与超时恢复都会「填决定 + 落库 + 派发」,不互斥就是同一任务跑两遍。 */
|
|
57
|
+
#inflight = new Map();
|
|
58
|
+
#deps;
|
|
59
|
+
constructor(deps) {
|
|
60
|
+
this.#deps = deps;
|
|
61
|
+
}
|
|
62
|
+
/** 挂起读数:按原因分组的条数 + 最早的目标时刻(无时间挂起时为 `null`) */
|
|
63
|
+
get summary() {
|
|
64
|
+
return summarizeSuspended(this.#suspended.values());
|
|
65
|
+
}
|
|
66
|
+
/** 登记一条挂起(纪律①)。原因缺失的记录不进表 —— 分不了组,宁可少数也不猜一个原因出来 */
|
|
67
|
+
markSuspended(rec) {
|
|
68
|
+
if (rec.status !== 'suspended' || rec.suspendedReason === undefined)
|
|
69
|
+
return;
|
|
70
|
+
this.#suspended.set(rec.taskId, { reason: rec.suspendedReason, wakeAt: rec.wakeAt });
|
|
71
|
+
}
|
|
72
|
+
/** 除名(纪律②):任务离开挂起态时调用(恢复、终态都算) */
|
|
73
|
+
unmarkSuspended(taskId) {
|
|
74
|
+
this.#suspended.delete(taskId);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 按 store 的**全表**重建挂起集(读数纪律③)—— `resumePending` 扫描时调用。
|
|
78
|
+
* 扫描本来就 `list()` 了全表,顺手把读数对齐一遍;顺带把**他进程**(或本进程上一世)
|
|
79
|
+
* 留下的挂起也算进本进程可见的那些(口径见 `summary` 的注释)。
|
|
80
|
+
*/
|
|
81
|
+
rebuild(recs) {
|
|
82
|
+
this.#suspended.clear();
|
|
83
|
+
for (const rec of recs) {
|
|
84
|
+
if (rec.status !== 'suspended' || rec.suspendedReason === undefined)
|
|
85
|
+
continue;
|
|
86
|
+
this.#suspended.set(rec.taskId, { reason: rec.suspendedReason, wakeAt: rec.wakeAt });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 惰性审批超时:`approvalExpired` 为真 ⇒ 进在飞闸 + 自动全拒 + 落库 + 派发。返回是否命中。
|
|
91
|
+
*
|
|
92
|
+
* 与原 `#lazyGates` / `#redispatch` 里那两句 `if (approvalExpired(...)) #expireAndResume(...)`
|
|
93
|
+
* **逐字等价** —— `approvalExpired` 内部已判 `status === 'suspended'` 与
|
|
94
|
+
* `reason === 'approval'`(见 approval-policy.ts),所以 `#redispatch` 原写法里那句额外的
|
|
95
|
+
* `rec.status !== 'suspended'` 短路是冗余的,合并到本方法不改变任何可观测行为。
|
|
96
|
+
*/
|
|
97
|
+
expireIfExpired(rec, now) {
|
|
98
|
+
if (!approvalExpired(rec, now, this.#deps.approvalTimeoutMs))
|
|
99
|
+
return false;
|
|
100
|
+
this.#expireAndResume(rec, now);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 审批一个处于 `suspended` 的任务(HITL)。
|
|
105
|
+
*
|
|
106
|
+
* - **逐 tool_use_id 幂等**:已存在的决定不覆盖(第一次决定赢)—— 重复提交 /
|
|
107
|
+
* 并发点击不会推翻已有决定,也不会让恢复段重复执行;
|
|
108
|
+
* - **并发重入共享在飞那次**:同一任务的并发 approve(双击「批准」/两个审批人
|
|
109
|
+
* 同时批)返回同一个 Promise —— 否则两个调用都在对方落库前读到
|
|
110
|
+
* `suspended`、各自判「决定齐了」、**各派发一次**(同一任务重复执行,
|
|
111
|
+
* 与 resumePending 的闸门同一 bug 类)。被共享的那次覆盖不到的决定不丢:
|
|
112
|
+
* 调用方从返回的记录看到任务仍在等待,重试即并入;
|
|
113
|
+
* - 决定齐了就恢复:`status` 回 `running`、**先落库再派发**(与 `#redispatch`
|
|
114
|
+
* 同一条纪律 —— 没落库就恢复,进程崩在窗口里会丢决定);恢复段带着
|
|
115
|
+
* `rec.approvals` 与扩展后的消息历史(`rec.spec.messages`,末尾是含未决
|
|
116
|
+
* tool_use 的那条 assistant 消息)重进引擎循环;
|
|
117
|
+
* - 决定**没**齐:只把本批决定落库,任务继续等(可能多轮审批);
|
|
118
|
+
* - 返回当前 TaskRecord(快照)。任务不存在抛 404 语义、状态不对抛 409 语义
|
|
119
|
+
* 的 `TaskApproveError`。
|
|
120
|
+
*/
|
|
121
|
+
async approve(taskId, decisions, opts = {}) {
|
|
122
|
+
const inflight = this.#inflight.get(taskId);
|
|
123
|
+
if (inflight)
|
|
124
|
+
return inflight;
|
|
125
|
+
const run = this.#approveInner(taskId, decisions, opts).finally(() => {
|
|
126
|
+
this.#inflight.delete(taskId);
|
|
127
|
+
});
|
|
128
|
+
this.#inflight.set(taskId, run);
|
|
129
|
+
return run;
|
|
130
|
+
}
|
|
131
|
+
async #approveInner(taskId, decisions, opts) {
|
|
132
|
+
const rec = await this.#deps.store.get(taskId);
|
|
133
|
+
if (!rec)
|
|
134
|
+
throw new TaskApproveError(404, `task 不存在: ${taskId}`);
|
|
135
|
+
if (rec.status !== 'suspended' || rec.suspendedReason !== 'approval') {
|
|
136
|
+
throw new TaskApproveError(409, rec.status === 'suspended'
|
|
137
|
+
? `task ${taskId} 的挂起原因是 ${rec.suspendedReason ?? '未知'},只有等人工审批的挂起能审批`
|
|
138
|
+
: `task ${taskId} 当前状态为 ${rec.status},只有挂起在等审批的任务才能审批`);
|
|
139
|
+
}
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
// 入参校验(2026-09-28 外部深评 P1-2):本批只许批**本任务待决的** id。
|
|
142
|
+
// 不校验会怎样(实证:一次 approve 塞 5000 个无关键 ⇒ 记录从 246 字节撑到 419 KB):
|
|
143
|
+
// 多出来的键照样写进 `rec.approvals`、随每次 save 全文重写落库、并随任务**永久保留**
|
|
144
|
+
// (终态也不清:审批记录是审计的一部分)⇒ 一个**认证调用方**单次请求就能把记录撑大,
|
|
145
|
+
// 反复调用可无限叠加。拒整批而不是挑着收:与 `parseApproveBody`(形状全有或全无)、
|
|
146
|
+
// `parseEventBody`(多一个字段即拒)同一条纪律 —— 调用方本来就该从记录的 `pendingApprovals`
|
|
147
|
+
// 里读要批哪些 id。
|
|
148
|
+
// ⚠️ 为什么**不需要**再给 `rec.approvals` 加常量上限:单次 approve 能新写的键 ⊆ 当前
|
|
149
|
+
// `pendingApprovals`(挂起那一刻固定)。跨多轮挂起它会累计**历次**待决的并集
|
|
150
|
+
// (决定是审计,终态也保留),但每一轮都对应真跑出来的模型回合与 tool_use ——
|
|
151
|
+
// 调用方的单次输入无法放大体积,增长只能随真实运行发生。再加常量上限是一段
|
|
152
|
+
// 永远触发不到的死代码(那不是护栏,是噪音)。
|
|
153
|
+
const pendingIds = new Set(rec.pendingApprovals ?? []);
|
|
154
|
+
const unknownIds = Object.keys(decisions).filter((id) => !pendingIds.has(id));
|
|
155
|
+
if (unknownIds.length > 0) {
|
|
156
|
+
throw new TaskApproveError(400, `decisions 里有 ${unknownIds.length} 个 id 不在本任务的待决列表里(如 ${unknownIds[0]})—— 整批拒掉,记录不动`);
|
|
157
|
+
}
|
|
158
|
+
rec.approvals ??= {};
|
|
159
|
+
for (const [id, d] of Object.entries(decisions)) {
|
|
160
|
+
if (rec.approvals[id])
|
|
161
|
+
continue; // 逐 id 幂等:第一次决定赢
|
|
162
|
+
rec.approvals[id] = {
|
|
163
|
+
approved: d.approved,
|
|
164
|
+
...(d.reason !== undefined ? { reason: d.reason } : {}),
|
|
165
|
+
...(opts.decidedBy !== undefined ? { decidedBy: opts.decidedBy } : {}),
|
|
166
|
+
decidedAt: now,
|
|
167
|
+
...(rec.suspendedSince !== undefined ? { requestedAt: rec.suspendedSince } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// 惰性审批超时:人的决定先并入(先到先赢),仍空着的待决项由超时兜底成 deny
|
|
171
|
+
if (approvalExpired(rec, now, this.#deps.approvalTimeoutMs))
|
|
172
|
+
fillTimeoutDenials(rec, now);
|
|
173
|
+
const complete = approvalsComplete(rec);
|
|
174
|
+
if (complete) {
|
|
175
|
+
rec.status = 'running'; // 由 #executeInner 接管(acquireSlot → 恢复执行)
|
|
176
|
+
rec.ownerId = this.#deps.ownerId;
|
|
177
|
+
}
|
|
178
|
+
// 先落库再派发 —— **真纪律,不是注释**:save 失败(同步抛 / 异步 reject)就
|
|
179
|
+
// 绝不恢复执行(落不了库的决定不算决定:进程崩在窗口里会丢决定,重启后把
|
|
180
|
+
// 同一件事再判一次、可能改判)。调用方拿到 reject,重试即可。
|
|
181
|
+
await this.#deps.store.save(rec);
|
|
182
|
+
if (complete) {
|
|
183
|
+
// 离开挂起态(读数纪律②):决定齐了这就是「醒来」那一刻
|
|
184
|
+
this.unmarkSuspended(taskId);
|
|
185
|
+
// 派发走**唯一入口**(闸在 #dispatch 里判一次 —— 2026-09-28 外部深评 P1-1 的结构性修法):
|
|
186
|
+
// 决定已经在记录里,停机窗口里放行的是「重新排期」(下次启动认领),不是「吞掉」。
|
|
187
|
+
this.#deps.dispatch(rec);
|
|
188
|
+
}
|
|
189
|
+
return { ...rec };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 惰性超时扫描(HITL):读到一个已超时的 awaiting 任务 ⇒ 自动全拒 + 落库 + 重派。
|
|
193
|
+
* 与 #redispatch 同一条纪律:**先落库再派发**(没落库就恢复,进程崩在窗口里会
|
|
194
|
+
* 丢掉超时决定、把同一件事再判一次)。
|
|
195
|
+
*
|
|
196
|
+
* 重入闸(2026-09-20,见 spec §10 当日条):与 `approve` **共用同一把** per-taskId
|
|
197
|
+
* 闸(`#inflight`)。异步 store 的 `get` 返回**新副本**且有网络往返 —— 两个
|
|
198
|
+
* 并发 poll(或 poll 与 approve)各自看到 awaiting 快照 ⇒ 双双填超时拒绝 + 双双
|
|
199
|
+
* `#execute`(同一任务跑两遍)。在飞即跳过:另一路径自己会兜底(`#approveInner`
|
|
200
|
+
* 里也有 approvalExpired 判定,见 approval-policy.ts),决定逐 id 幂等(第一次决定赢)。
|
|
201
|
+
* 反之 approve 撞上在飞的超时恢复时共享其结果 —— 与人的决定竞速,先到先得。
|
|
202
|
+
*/
|
|
203
|
+
#expireAndResume(rec, now) {
|
|
204
|
+
if (this.#inflight.has(rec.taskId))
|
|
205
|
+
return;
|
|
206
|
+
const run = this.#expireAndResumeInner(rec, now).finally(() => {
|
|
207
|
+
this.#inflight.delete(rec.taskId);
|
|
208
|
+
});
|
|
209
|
+
this.#inflight.set(rec.taskId, run);
|
|
210
|
+
// poll / resumePending 路径没有调用方接 reject —— 订阅掉,不得逃逸成 unhandled rejection
|
|
211
|
+
run.catch(() => undefined);
|
|
212
|
+
}
|
|
213
|
+
async #expireAndResumeInner(rec, now) {
|
|
214
|
+
// 进闸后**重读一遍**再判:闸只互斥「进入」,挡不住「进闸前已取到的旧副本」——
|
|
215
|
+
// 本记录的快照可能是在另一次恢复落库**之前**取的(异步 store 的 get 有往返),
|
|
216
|
+
// 凭陈旧快照放行会把同一任务再派发一次。
|
|
217
|
+
const fresh = await this.#deps.store.get(rec.taskId);
|
|
218
|
+
const target = fresh ?? rec;
|
|
219
|
+
if (target.status !== 'suspended' ||
|
|
220
|
+
!approvalExpired(target, now, this.#deps.approvalTimeoutMs)) {
|
|
221
|
+
return target;
|
|
222
|
+
}
|
|
223
|
+
fillTimeoutDenials(target, now);
|
|
224
|
+
target.status = 'running';
|
|
225
|
+
target.ownerId = this.#deps.ownerId;
|
|
226
|
+
let saved;
|
|
227
|
+
try {
|
|
228
|
+
saved = this.#deps.store.save(target);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return target; // 同步落库失败则不派发(与下一条异步分支同纪律:先落库再派发)
|
|
232
|
+
}
|
|
233
|
+
if (isThenable(saved)) {
|
|
234
|
+
// 落库失败则不派发(认领没落地就派发 = 重新打开重复执行窗口)
|
|
235
|
+
try {
|
|
236
|
+
await saved;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return target;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// 离开挂起态(读数纪律②)—— 落在**落库成功之后**:落不了库就不算恢复(没派发,读数也不动)
|
|
243
|
+
this.unmarkSuspended(target.taskId);
|
|
244
|
+
// 派发走唯一入口(闸在 #dispatch 里)。⚠️ **这一条正是外部深评抓到的第四条路径**:
|
|
245
|
+
// `#expireAndResume`(审批超时自动全拒并恢复)与 `approve` 不同触发源、同一形状,
|
|
246
|
+
// 由 `poll()` 的惰性闸驱动 —— 而「停机中照常可轮询」是 HTTP 宿主的明确承诺
|
|
247
|
+
// (LB / K8s preStop / 前端轮询)⇒ 停机后照样能起新 run。
|
|
248
|
+
this.#deps.dispatch(target);
|
|
249
|
+
return target;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=approval-supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-supervisor.js","sourceRoot":"","sources":["../../src/transport/approval-supervisor.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import type { MessageParam } from '../core/message.js';
|
|
2
2
|
import type { ModelClient } from '../core/tool.js';
|
|
3
3
|
import type { AgentRunResult } from '../engine/types.js';
|
|
4
|
-
import type { RunStatus } from '../core/run.js';
|
|
4
|
+
import type { RunStatus, TaskEvent } from '../core/run.js';
|
|
5
5
|
import type { RunInvocationOptions } from '../engine/spec.js';
|
|
6
6
|
import type { MaybePromise, TaskRecord, TaskStore } from '../store/store.js';
|
|
7
7
|
import type { TraceRecordEvent } from '../core/trace.js';
|
|
8
|
+
import type { ApprovalDecisions } from './approval-supervisor.js';
|
|
9
|
+
import type { ResumePendingOptions } from './resume-scanner.js';
|
|
10
|
+
import type { SuspendedSummary } from './wake-policy.js';
|
|
11
|
+
export { TaskApproveError } from './approval-supervisor.js';
|
|
12
|
+
export type { ApprovalDecisions } from './approval-supervisor.js';
|
|
13
|
+
export { TaskEventError } from './signal-supervisor.js';
|
|
14
|
+
export type { ResumePendingOptions } from './resume-scanner.js';
|
|
8
15
|
/**
|
|
9
16
|
* Agentia —— 异步任务宿主(spec §6.3 异步 / §6.5 确定性 / §6.6 换宿主不换语义)。
|
|
10
17
|
*
|
|
11
18
|
* - submit 即回(queued),后台驱动状态机 queued → running → succeeded/failed;
|
|
12
|
-
* 工具标了 `approval: 'required'` 时 run 可在回合间挂起为 **
|
|
19
|
+
* 工具标了 `approval: 'required'` 时 run 可在回合间挂起为 **suspended**
|
|
13
20
|
* (HITL:非终态、不占并发槽、不触发 TaskSink.onFinished),`approve()` 给齐决定后恢复;
|
|
21
|
+
* 工具调 `ctx.deferUntil(t)` 时同样挂起(durable timer:`suspendedReason: 'timer'`),
|
|
22
|
+
* 到点由 `resumePending` / `poll` 的**惰性**扫描唤醒(不起定时器,见 wake-policy.ts);
|
|
23
|
+
* 两条挂起路径的**闸按原因分开**(等谁的决定 vs 等到点了没);
|
|
14
24
|
* - **at-least-once 去重**:同一 idempotencyKey 重复 submit,若上一任务仍在
|
|
15
25
|
* queued/running/succeeded 则直接返回既有记录,不重复执行(失败可重试新任务);
|
|
16
26
|
* - run 记录落 TaskStore(v1 InMemoryTaskStore),trace 随 result 一同保留;
|
|
@@ -50,11 +60,6 @@ export interface PersistFailureInfo {
|
|
|
50
60
|
error: unknown;
|
|
51
61
|
phase: 'initial' | 'outcome';
|
|
52
62
|
}
|
|
53
|
-
/** `approve` 的入参:一批 tool_use_id → 批准/拒绝(理由可选) */
|
|
54
|
-
export type ApprovalDecisions = Record<string, {
|
|
55
|
-
approved: boolean;
|
|
56
|
-
reason?: string;
|
|
57
|
-
}>;
|
|
58
63
|
/**
|
|
59
64
|
* 任务事件流的一帧(`GET /tasks/:id/stream` 的帧形状,与 SSE 帧名一一对应)。
|
|
60
65
|
*
|
|
@@ -107,14 +112,35 @@ export declare class TaskStreamError extends Error {
|
|
|
107
112
|
constructor(message: string);
|
|
108
113
|
}
|
|
109
114
|
/**
|
|
110
|
-
* `approve`
|
|
111
|
-
* `
|
|
112
|
-
* module 级 export —— 不进公共导出面(纯宿主内部实现细节)。
|
|
115
|
+
* `TaskApproveError`(`approve` 的失败,带 HTTP 语义状态码)已随审批监督簇迁到
|
|
116
|
+
* `approval-supervisor.ts`,本文件顶部 re-export 保持导出面不变(定义与理由见那里)。
|
|
113
117
|
*/
|
|
114
|
-
|
|
118
|
+
/**
|
|
119
|
+
* `cancel` 的失败:带 HTTP 语义的状态码(404 = 任务不存在,409 = 已终态 / 在飞 run 不在本进程)。
|
|
120
|
+
* 形状与 `TaskApproveError` 同款(module 级 export,不进公共导出面)。
|
|
121
|
+
*/
|
|
122
|
+
export declare class TaskCancelError extends Error {
|
|
115
123
|
readonly status: 404 | 409;
|
|
116
124
|
constructor(status: 404 | 409, message: string);
|
|
117
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* `submit` 在**排队段已满**时的失败(`AsyncRunnerOptions.maxQueued`,2026-09-28 外部深评 T5)。
|
|
128
|
+
*
|
|
129
|
+
* 为什么单独一个类型、而不是复用 `TaskInputError`:两者在 HTTP 上的**含义不同** ——
|
|
130
|
+
* 入参不合法是 **400**(调用方改请求就能过),排队满**不是调用方的错**,是 **503
|
|
131
|
+
* + `Retry-After`**(同一次请求稍后重发就该过)。宿主若把它们混成一支,运维看到的就是
|
|
132
|
+
* 「你的请求有问题」,而实际是「服务该扩容 / 该退避」—— 与 `maxConcurrentRuns` 的 503
|
|
133
|
+
* 那支刻意分开的理由同款。
|
|
134
|
+
*
|
|
135
|
+
* `status` 是自陈字段(本仓 `TaskApproveError` 起的惯例):宿主按它选状态码,不靠
|
|
136
|
+
* `instanceof` 猜;本类目前只有 503 一种。
|
|
137
|
+
*
|
|
138
|
+
* module 级 export —— 不进公共导出面(与 TaskApproveError / TaskCancelError / TaskEventError 同档)。
|
|
139
|
+
*/
|
|
140
|
+
export declare class TaskQueueFullError extends Error {
|
|
141
|
+
readonly status = 503;
|
|
142
|
+
constructor(message: string);
|
|
143
|
+
}
|
|
118
144
|
export interface AsyncRunnerOptions {
|
|
119
145
|
client?: ModelClient;
|
|
120
146
|
store?: TaskStore;
|
|
@@ -133,6 +159,27 @@ export interface AsyncRunnerOptions {
|
|
|
133
159
|
};
|
|
134
160
|
/** 同时执行的任务上限;缺省不限。超出部分排队等槽位(状态保持 queued) */
|
|
135
161
|
concurrency?: number;
|
|
162
|
+
/**
|
|
163
|
+
* **在等槽位的任务数**上限(`concurrency` 之外的那一段);缺省 0 = 不限。
|
|
164
|
+
*
|
|
165
|
+
* 为什么需要它(2026-09-28 外部深评 T5):`concurrency` 只约束**同时在跑**几个,
|
|
166
|
+
* 而「跑不上、排在后面」的那一段**没有任何上限** —— `submit` 永远收单(POST /tasks
|
|
167
|
+
* 永远 202),排队段因此随调用方灌入无界增长:每条排队任务都是一条 `TaskRecord` +
|
|
168
|
+
* 一棵 `#execute` 的悬挂 promise + 一个 `#slots` 等待者,**全部在内存里**。
|
|
169
|
+
* `concurrency: 1` 挡不住它 —— 那只是让排队段更长。
|
|
170
|
+
*
|
|
171
|
+
* 语义:**「在等槽位」= 已受理(落库 + `#execute` 同步前段已计数)、尚未拿到槽位**。
|
|
172
|
+
* 拿到槽位即离开本计数(此后归 `concurrency` 管),所以两个旋钮管的是两段、可加:
|
|
173
|
+
* 进程内最多 `concurrency + maxQueued` 条任务同时在推进。
|
|
174
|
+
*
|
|
175
|
+
* ⚠️ 两个刻意的口径:
|
|
176
|
+
* - **只在 `submit`(新活入口)判**:恢复路径(approve / 到期唤醒 / 崩溃重投)**不受闸**
|
|
177
|
+
* —— 它们推进的是**已受理**的任务,拦下来等于把任务永久搁死在 store 里。
|
|
178
|
+
* - **超限是 503 不是 400**:这不是调用方把参数写错了,是「现在排不下、稍后再来」。
|
|
179
|
+
* HTTP 宿主回 503 + `Retry-After`(与 `maxConcurrentRuns` 同款),错误类型
|
|
180
|
+
* `TaskQueueFullError` 让宿主能把它与「入参不合法」分开。
|
|
181
|
+
*/
|
|
182
|
+
maxQueued?: number;
|
|
136
183
|
/** 任务完成回调(进程内);见 TaskSink */
|
|
137
184
|
taskSinks?: TaskSink[];
|
|
138
185
|
/**
|
|
@@ -166,7 +213,7 @@ export interface AsyncRunnerOptions {
|
|
|
166
213
|
* 审批等待超时(毫秒,HITL);缺省 0 = 不限(一直等人)。
|
|
167
214
|
*
|
|
168
215
|
* **惰性判定,不起定时器**:`approve` / `poll` / `resumePending` 读到一个
|
|
169
|
-
* `
|
|
216
|
+
* `suspended` 任务时,若它挂起已超过该值,框架自动把**全部待决项**写成
|
|
170
217
|
* 「denied,reason: '审批超时'」并恢复执行(模型收到拒绝理由,可自行换路)。
|
|
171
218
|
* 也就是说超时只在「有人读它」时生效 —— 没人读的任务不会自己动(进程里不养定时器,
|
|
172
219
|
* 崩溃/重启也不依赖任何在飞回调)。
|
|
@@ -180,19 +227,25 @@ export interface AsyncRunnerOptions {
|
|
|
180
227
|
* (既不是「关掉流」也不是「不限」),0 语义归类见 `core/limits.ts`。
|
|
181
228
|
*
|
|
182
229
|
* 内存量级:单条事件正文受 `maxEventChars` 约束(入参/成功出参缺省 2000 字符),
|
|
183
|
-
* 故 500 条 ≈ 1 MB/任务;终态流的保留条数是代码里的常量(最近 16
|
|
230
|
+
* 故 500 条 ≈ 1 MB/任务;终态流的保留条数是代码里的常量(最近 16 条),
|
|
231
|
+
* **非终态**流的缓冲配额见 `streamNonTerminalBuffers`(缺省 32 条流)。
|
|
184
232
|
* 超出条数上限时丢**最旧**的,并向订阅方发一帧 `stream.truncated`(**不静默**)。
|
|
185
233
|
*/
|
|
186
234
|
streamBufferEvents?: number;
|
|
187
|
-
}
|
|
188
|
-
/** resumePending 的启动扫描选项 */
|
|
189
|
-
export interface ResumePendingOptions {
|
|
190
235
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
236
|
+
* **非终态**流的**缓冲**配额(条数,不是事件数):本进程最多为多少条**还没跑完**的流
|
|
237
|
+
* 保留「可回放的事件缓冲」;缺省 32。超出即从最旧、**无订阅者**的流开始回收缓冲。
|
|
238
|
+
*
|
|
239
|
+
* 为什么需要它:`streamBufferEvents` 只钉住**每任务**的量,而「挂着的任务」(等审批 /
|
|
240
|
+
* 等事件 / 等定时器)永远走不到终态 ⇒ 表项与缓冲只增不减(2026-09-28 外部深评 T4)。
|
|
241
|
+
* 回收的是**回放历史**,不是流本身:表项与序号都留着,连上来的读者先收到一帧
|
|
242
|
+
* `stream.truncated`(明示缺口)再转实时推送 —— 所以「回放看不全」是**看得见**的降级。
|
|
243
|
+
*
|
|
244
|
+
* `0` = 不为**没人读的**非终态流留回放缓冲(只有实时转发)。必须是非负安全整数,坏值构造期抛 TypeError。
|
|
245
|
+
* ⚠️ **有订阅者的流不受配额约束**(不抽走正在读的东西);终态流的**表项**保留数仍是
|
|
246
|
+
* 内部常量(最近 16 条)—— 它本来就是有界的,没走旋钮。
|
|
194
247
|
*/
|
|
195
|
-
|
|
248
|
+
streamNonTerminalBuffers?: number;
|
|
196
249
|
}
|
|
197
250
|
/** 应用最小调用面(agent 装配无关,避免 run 层向上依赖 toolkit) */
|
|
198
251
|
export interface AppCallable {
|
|
@@ -209,11 +262,18 @@ export declare class AsyncRunner {
|
|
|
209
262
|
#private;
|
|
210
263
|
private readonly app;
|
|
211
264
|
readonly store: TaskStore;
|
|
212
|
-
/**
|
|
265
|
+
/**
|
|
266
|
+
* 本进程标识:写进认领的 `TaskRecord.ownerId`,供 resumePending 区分他我。
|
|
267
|
+
*
|
|
268
|
+
* 形状 `p<pid>@<host>-<8位十六进制>`(真源见 `owner-id.ts`)。为什么带主机名与 pid:
|
|
269
|
+
* 光有时间判不出「主人还在不在」(`staleAfterMs` 是**新鲜度**不是**租约**),
|
|
270
|
+
* 而「同主机 + pid 还在」是能直接问操作系统的**直接证据**(见 `owner-liveness.ts`)。
|
|
271
|
+
*/
|
|
213
272
|
readonly ownerId: string;
|
|
214
273
|
private readonly client;
|
|
215
274
|
private readonly sessionStore;
|
|
216
275
|
private readonly concurrency;
|
|
276
|
+
private readonly maxQueued;
|
|
217
277
|
private readonly runTimeoutMs;
|
|
218
278
|
private readonly approvalTimeoutMs;
|
|
219
279
|
/** 已受理但未达终态的任务数(queued + running)—— /healthz 与 drain 共用 */
|
|
@@ -252,7 +312,7 @@ export declare class AsyncRunner {
|
|
|
252
312
|
* 1. **先重放、后实时**:`opts.from`(= SSE 的 `Last-Event-ID` / `?from=`)之后的事件先补发,
|
|
253
313
|
* 再挂实时订阅 —— 「连上时已经跑了一半」的客户端因此也能拿到完整前缀。
|
|
254
314
|
* 2. **终态收口**:任务已终态 ⇒ 补发完缓冲 + 一帧 `end`,**不留**一条永远不会再产出事件的流。
|
|
255
|
-
* 挂起(`
|
|
315
|
+
* 挂起(`suspended`)**不是**终态:批了(或睡到点了)会接着跑,流必须开着(见 isTerminalTask)。
|
|
256
316
|
* 3. **跨进程不假装**:本进程没有这条流时(别的进程在跑 / 任务早于本进程),发一帧
|
|
257
317
|
* `unavailable` 后**立即收口**:终态补 `end`,非终态补 `closed`(流级收尾,不是
|
|
258
318
|
* 伪造终态)—— 非终态只发 `unavailable` 就返回的话,这条 SSE 只剩心跳永远挂着。
|
|
@@ -268,31 +328,65 @@ export declare class AsyncRunner {
|
|
|
268
328
|
byIdempotency(key: string): MaybePromise<TaskRecord | undefined>;
|
|
269
329
|
list(): MaybePromise<TaskRecord[]>;
|
|
270
330
|
/**
|
|
271
|
-
*
|
|
331
|
+
* 取消一个任务(durable 配套 5 的另一半;设计稿 `docs/plans/2026-09-28-cancel-api.md`)。
|
|
332
|
+
*
|
|
333
|
+
* 三种「在服」状态各自的语义 —— 都落在这一处,不分散:
|
|
334
|
+
* - **running**:中止在飞 signal ⇒ 引擎以 `stopReason: 'aborted'` 收尾(尊重 signal 的
|
|
335
|
+
* client 是**真中断**,token 不再烧;不尊重者等价「放弃等待」—— 与 `runTimeoutMs`
|
|
336
|
+
* 同一份契约)。等它真落库再返回:不等的话调用方拿到的是 `status` 仍为 running 的快照,
|
|
337
|
+
* 「取消了但状态还在跑」比不返回更误导。
|
|
338
|
+
* - **queued**(还没起跑):落终态;`#executeInner` 拿到槽位后会**重读**再判,
|
|
339
|
+
* 所以被取消的排队任务**绝不起跑**(这条是本次实现顺带补上的洞)。记账由本方法
|
|
340
|
+
* 自己做;那一趟还活着的 `#execute`(在等槽位)靠 `#cancelSettled` 在 finally 里
|
|
341
|
+
* 跳过它那一份(否则 onFinished 两次)。
|
|
342
|
+
* - **suspended**(两种原因都算):落终态 —— 两条唤醒闸都 gate 在 `status === 'suspended'`
|
|
343
|
+
* 上,「不唤醒」因此是翻转状态的**推论**,不需要第二处标志。它没有活着的 `#execute`
|
|
344
|
+
* (挂起时那趟就走完了),所以记账(sinks / 事件流 / 认领释放 / 等待者 / 会话清理)
|
|
345
|
+
* 由本方法自己做 —— 也因此它**不进** `#cancelSettled`(没有消费者,加了就是泄漏)。
|
|
346
|
+
*
|
|
347
|
+
* 状态按**意图**落,不按机制落:超时的收尾语义**不变**(仍是 `failed` + `error.type` 为
|
|
348
|
+
* `'timeout'`),取消带 `error = abortedError()`(取消不是失败,但原因要可查)。
|
|
349
|
+
*
|
|
350
|
+
* 不做静默 no-op:已终态 ⇒ `TaskCancelError(409)`(状态不对要说出来),不存在 ⇒ 404。
|
|
351
|
+
* 边界(如实写在这里):`running` 的 run **不在本进程**(多进程部署 / 上一世留下的 running)
|
|
352
|
+
* 时抛 409 —— 本进程没有句柄可中断,改状态假装取消只会在那条 run 跑完时被覆盖回去。
|
|
353
|
+
*
|
|
354
|
+
* ⚠️ 同一个 409 判据下还有**第二种**情形(2026-09-28 外部复核):任务已在本进程受理、
|
|
355
|
+
* 但还没走到 `#runAborts` 登记(在等并发槽位,或恢复路径刚写完 `running` 尚未派发完)。
|
|
356
|
+
* 它**不是**「不在本进程」—— 原文案一律那么说,对这一类是说反话。两种情形分开措辞
|
|
357
|
+
* (见 `#cancel` 的判据),但**闸不放开**:放开会让等槽位那趟 `#execute` 的 finally
|
|
358
|
+
* 双发 `onFinished`(spec §10 的变异 M23)。
|
|
359
|
+
*/
|
|
360
|
+
cancel(taskId: string): MaybePromise<TaskRecord>;
|
|
361
|
+
/**
|
|
362
|
+
* 审批一个处于 `suspended` 的任务(HITL)。
|
|
272
363
|
*
|
|
273
|
-
* -
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* `awaiting_approval`、各自判「决定齐了」、**各派发一次**(同一任务重复执行,
|
|
278
|
-
* 与 resumePending 的闸门同一 bug 类)。被共享的那次覆盖不到的决定不丢:
|
|
279
|
-
* 调用方从返回的记录看到任务仍在等待,重试即并入;
|
|
280
|
-
* - 决定齐了就恢复:`status` 回 `running`、**先落库再派发**(与 `#redispatch`
|
|
281
|
-
* 同一条纪律 —— 没落库就恢复,进程崩在窗口里会丢决定);恢复段带着
|
|
282
|
-
* `rec.approvals` 与扩展后的消息历史(`rec.spec.messages`,末尾是含未决
|
|
283
|
-
* tool_use 的那条 assistant 消息)重进引擎循环;
|
|
284
|
-
* - 决定**没**齐:只把本批决定落库,任务继续等(可能多轮审批);
|
|
285
|
-
* - 返回当前 TaskRecord(快照)。任务不存在抛 404 语义、状态不对抛 409 语义
|
|
286
|
-
* 的 `TaskApproveError`。
|
|
364
|
+
* 实现已外移到 `approval-supervisor.ts`(**逐字搬迁**,含四条语义与它们的理由:
|
|
365
|
+
* 逐 tool_use_id 幂等 / 并发重入共享在飞那次 / 决定齐了先落库再派发 / 状态不对抛
|
|
366
|
+
* `TaskApproveError`)。这里只留薄壳 —— `approve` 是 `AsyncRunner` 的公开 API,
|
|
367
|
+
* 签名与返回类型不变。
|
|
287
368
|
*/
|
|
288
369
|
approve(taskId: string, decisions: ApprovalDecisions, opts?: {
|
|
289
370
|
decidedBy?: string;
|
|
290
371
|
}): Promise<TaskRecord>;
|
|
291
|
-
/**
|
|
292
|
-
*
|
|
293
|
-
|
|
372
|
+
/**
|
|
373
|
+
* 投递一个事件给**挂起**的任务(HITL 与 durable timer 两种挂起都适用)。
|
|
374
|
+
*
|
|
375
|
+
* 实现已外移到 `signal-supervisor.ts`(**逐字搬迁**,含完整语义清单与并发闸的理由:
|
|
376
|
+
* 只对 `suspended` 生效 / 按 `eventId` 幂等 / 缓冲满了拒收并说出来 / 投完即续跑且
|
|
377
|
+
* **先落库再派发** / 并发时**串行成链**而不是像 approve 那样共享在飞那次)。
|
|
378
|
+
* 这里只留薄壳 —— `signalTask` 是 `AsyncRunner` 的公开 API,签名与返回类型不变。
|
|
379
|
+
*/
|
|
380
|
+
signalTask(taskId: string, event: TaskEvent): Promise<TaskRecord>;
|
|
294
381
|
/** 已受理但未达终态的任务数(queued + running)—— 健康检查与 drain 共用同一口径 */
|
|
295
382
|
get inFlight(): number;
|
|
383
|
+
/**
|
|
384
|
+
* **本进程可见的挂起读数**(`/healthz` 的 `suspended` 段,配套 6)。
|
|
385
|
+
*
|
|
386
|
+
* 口径与三条维护纪律(挂起时登记 / 离开时除名 / 每次扫描按 store 重建)已随审批监督簇
|
|
387
|
+
* 迁到 `approval-supervisor.ts` 的 `#suspended`(那张注释就写在那里);这里只留对外 getter。
|
|
388
|
+
*/
|
|
389
|
+
get suspendedSummary(): SuspendedSummary;
|
|
296
390
|
/** 是否已进入优雅停机(drain 之后为 true)—— HTTP 宿主据此对新单回 503 */
|
|
297
391
|
get isDraining(): boolean;
|
|
298
392
|
/**
|
|
@@ -302,11 +396,29 @@ export declare class AsyncRunner {
|
|
|
302
396
|
* `resumePending` 续跑(所以 drain 不是「丢弃」,是「不再往前推」)。
|
|
303
397
|
* - `timeoutMs` 缺省 0 = 一直等。
|
|
304
398
|
* - 等待的是**所有已受理**的任务(queued 的也在内),不只是正在占槽位的那些。
|
|
399
|
+
*
|
|
400
|
+
* ⚠️ **这是一道单向闩:置位之后本进程不再推进任何任务,且没有复位路径**(`DrainGate` 只有
|
|
401
|
+
* 置位,没有复位)。所以 `false`(排空超时)的含义不只是「还有在飞的」——它同时给宿主
|
|
402
|
+
* 加了一条义务:**必须让进程退出**。理由(2026-09-28 PR #164 复核 §1,有探针实证):
|
|
403
|
+
* 停机窗口里被 `#dispatch` 拒掉的记录**已经落库**(`running`/`queued` + **本进程的 ownerId**),
|
|
404
|
+
* 而本进程的 `resumePending` 会按 `own-process` 跳过它们(见 resume-policy.ts),`submit` 又
|
|
405
|
+
* 已整体关闭 ⇒ 本进程内**没有任何自愈路径**。宿主若在 `false` 之后继续服务,这些记录会永远
|
|
406
|
+
* 停在 `running`:`awaitTask` 不返回,`GET /tasks/:id/stream` 还会告诉客户端
|
|
407
|
+
* `not-in-this-process`(说不对话:记录里的 ownerId 正是本进程)。只有**下一次进程启动**
|
|
408
|
+
* 才会有人认领它们(ownerId 含 pid)。参考宿主(`examples/` 各示例的 `src/main.ts`)都是
|
|
409
|
+
* `await drain(...)` 之后 `process.exit(...)`,与本条契约一致。
|
|
305
410
|
*/
|
|
306
411
|
drain(opts?: {
|
|
307
412
|
timeoutMs?: number;
|
|
308
413
|
}): Promise<boolean>;
|
|
309
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* 当前**真正在排队**的任务数(`maxQueued` 的实时读数,诊断用)—— 与闸用的是**同一个**
|
|
416
|
+
* `#queueDepth`,所以「看到的数」就是「判据的数」,不会各说各话。
|
|
417
|
+
* 与 `inFlight` 的分工:`inFlight` 是「本进程已受理、未达终态」的全量(含在跑、含挂起),
|
|
418
|
+
* 本读数是其中「排不上、在等槽位」的那一段。
|
|
419
|
+
*/
|
|
420
|
+
get queued(): number;
|
|
421
|
+
/** 等到任务终态;超时抛错。`suspended` 不是终态 —— 继续等(人在路上 / 时刻未到)。 */
|
|
310
422
|
awaitTask(taskId: string, opts?: {
|
|
311
423
|
timeoutMs?: number;
|
|
312
424
|
intervalMs?: number;
|
|
@@ -316,22 +428,13 @@ export declare class AsyncRunner {
|
|
|
316
428
|
* (running 视为进程中断)。返回重派数量(异步 store 下返回 Promise<number>)。
|
|
317
429
|
* 幂等键去重照常生效。
|
|
318
430
|
*
|
|
319
|
-
* `
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* **认领先落库、再派发**(见 `#redispatch`);异步 store 的认领落库失败会让本方法
|
|
324
|
-
* reject —— 宁可让调用方看见「续跑没做」,也不要静默放出一批会被重复执行的任务。
|
|
431
|
+
* 实现在 `resume-scanner.ts`(**逐字搬迁**,含那四步扫描的**顺序**与各自理由、重入闸、
|
|
432
|
+
* 到期索引接线、多进程 `ownerId` 的判定分级)。这里只留薄壳 —— `resumePending` 是
|
|
433
|
+
* `AsyncRunner` 的公开 API,签名与返回类型不变。
|
|
325
434
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
* (大概正被那个进程执行)。缺省 0 = 不判断、一律重派(单进程旧语义)。
|
|
435
|
+
* 一句话提要(细节见实现):`suspended` **不捡**(它在等人、不是孤儿),但读到它会做
|
|
436
|
+
* 惰性判定 —— 审批超时自动全拒并重派、`wakeAt` 已到的唤醒并重派(`drain()` 之后不唤,
|
|
437
|
+
* 配套 5);认领**先落库再派发**;多进程靠 `ownerId` 区分他我(`staleAfterMs > 0` 时问 pid)。
|
|
330
438
|
*/
|
|
331
439
|
resumePending(opts?: ResumePendingOptions): number | Promise<number>;
|
|
332
|
-
/**
|
|
333
|
-
* 在飞的 `resumePending`(重入闸,见上)。不用 boolean 而用 Promise:
|
|
334
|
-
* 重入方要拿到**同一次扫描**的结果,而不是一个「你等着」的空数。
|
|
335
|
-
*/
|
|
336
|
-
private inflightResume;
|
|
337
440
|
}
|