@migor/agentia 0.9.5 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +593 -1
  2. package/README.en.md +61 -3
  3. package/README.md +66 -4
  4. package/dist/AGENTS.md +18 -13
  5. package/dist/container/container.js +17 -11
  6. package/dist/container/container.js.map +1 -1
  7. package/dist/core/limits.d.ts +12 -5
  8. package/dist/core/limits.js +4 -4
  9. package/dist/core/limits.js.map +1 -1
  10. package/dist/engine/budget.js +14 -0
  11. package/dist/engine/budget.js.map +1 -1
  12. package/dist/engine/errors.d.ts +23 -1
  13. package/dist/engine/errors.js +24 -0
  14. package/dist/engine/errors.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/runtime/run.js +14 -2
  19. package/dist/runtime/run.js.map +1 -1
  20. package/dist/store/redisStore.d.ts +21 -0
  21. package/dist/store/redisStore.js +21 -0
  22. package/dist/store/redisStore.js.map +1 -1
  23. package/dist/store/store.d.ts +1 -1
  24. package/dist/transport/approval-supervisor.d.ts +73 -0
  25. package/dist/transport/approval-supervisor.js +252 -0
  26. package/dist/transport/approval-supervisor.js.map +1 -0
  27. package/dist/transport/async.d.ts +29 -105
  28. package/dist/transport/async.js +85 -513
  29. package/dist/transport/async.js.map +1 -1
  30. package/dist/transport/resume-scanner.d.ts +55 -0
  31. package/dist/transport/resume-scanner.js +219 -0
  32. package/dist/transport/resume-scanner.js.map +1 -0
  33. package/dist/transport/signal-supervisor.d.ts +80 -0
  34. package/dist/transport/signal-supervisor.js +129 -0
  35. package/dist/transport/signal-supervisor.js.map +1 -0
  36. package/dist/transport/task-events.js +8 -1
  37. package/dist/transport/task-events.js.map +1 -1
  38. package/package.json +9 -1
@@ -5,7 +5,13 @@ 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';
8
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';
9
15
  /**
10
16
  * Agentia —— 异步任务宿主(spec §6.3 异步 / §6.5 确定性 / §6.6 换宿主不换语义)。
11
17
  *
@@ -54,11 +60,6 @@ export interface PersistFailureInfo {
54
60
  error: unknown;
55
61
  phase: 'initial' | 'outcome';
56
62
  }
57
- /** `approve` 的入参:一批 tool_use_id → 批准/拒绝(理由可选) */
58
- export type ApprovalDecisions = Record<string, {
59
- approved: boolean;
60
- reason?: string;
61
- }>;
62
63
  /**
63
64
  * 任务事件流的一帧(`GET /tasks/:id/stream` 的帧形状,与 SSE 帧名一一对应)。
64
65
  *
@@ -111,15 +112,9 @@ export declare class TaskStreamError extends Error {
111
112
  constructor(message: string);
112
113
  }
113
114
  /**
114
- * `approve` 的失败:带 HTTP 语义的状态码(404 = 任务不存在,409 = 任务当前不在
115
- * `suspended` 状态),HTTP 宿主据此回对应响应。
116
- * module 级 export —— 不进公共导出面(纯宿主内部实现细节)。
115
+ * `TaskApproveError`(`approve` 的失败,带 HTTP 语义状态码)已随审批监督簇迁到
116
+ * `approval-supervisor.ts`,本文件顶部 re-export 保持导出面不变(定义与理由见那里)。
117
117
  */
118
- export declare class TaskApproveError extends Error {
119
- /** 400 = 入参语义不合法(decisions 里有本任务待决之外的 id,2026-09-28 深评 P1-2) */
120
- readonly status: 400 | 404 | 409;
121
- constructor(status: 400 | 404 | 409, message: string);
122
- }
123
118
  /**
124
119
  * `cancel` 的失败:带 HTTP 语义的状态码(404 = 任务不存在,409 = 已终态 / 在飞 run 不在本进程)。
125
120
  * 形状与 `TaskApproveError` 同款(module 级 export,不进公共导出面)。
@@ -128,15 +123,6 @@ export declare class TaskCancelError extends Error {
128
123
  readonly status: 404 | 409;
129
124
  constructor(status: 404 | 409, message: string);
130
125
  }
131
- /**
132
- * `signalTask` 的失败:带 HTTP 语义的状态码(404 = 任务不存在,409 = 任务当前不在
133
- * `suspended` 状态 / 同 eventId 已投递)。形状与 `TaskCancelError` 同款
134
- * (module 级 export,不进公共导出面)。
135
- */
136
- export declare class TaskEventError extends Error {
137
- readonly status: 404 | 409;
138
- constructor(status: 404 | 409, message: string);
139
- }
140
126
  /**
141
127
  * `submit` 在**排队段已满**时的失败(`AsyncRunnerOptions.maxQueued`,2026-09-28 外部深评 T5)。
142
128
  *
@@ -261,24 +247,6 @@ export interface AsyncRunnerOptions {
261
247
  */
262
248
  streamNonTerminalBuffers?: number;
263
249
  }
264
- /** resumePending 的启动扫描选项 */
265
- export interface ResumePendingOptions {
266
- /**
267
- * 他进程任务的判定开关;缺省 0 = 不判断,一律重派(重启即续跑,单进程旧语义)。
268
- *
269
- * `> 0` 时按**两档**判他进程的记录(判据在 `resume-policy.ts`,这里只说取舍):
270
- * ① 记录里的主机名是本机 ⇒ **问 pid 还在不在**(`owner-liveness.ts`)——
271
- * 「主人在」的直接证据:**在的绝不抢**(不管它起跑多久),**不在的立刻可抢**
272
- * (不等保鲜期,崩溃孤儿不会饿死);
273
- * ② 判不了(异主机 / 升级前写下的旧格式 ownerId / 自定义串)⇒ 退回本参数名义上的
274
- * 那件事:`startedAt`(退化到 `createdAt`)距今不足该值就当他还在跑、先别抢。
275
- *
276
- * ⚠️ ②只是**启发式**(「记录看起来还新」),不是租约:单进程部署建议 0;多进程共库
277
- * 时它只是「问不到 pid」时的兜底,别再指望它单独把「重复执行」挡住。
278
- * 阈值取值要大于「一个进程从启动到跑满一条记录」的合理时间,否则长跑会被抢。
279
- */
280
- staleAfterMs?: number;
281
- }
282
250
  /** 应用最小调用面(agent 装配无关,避免 run 层向上依赖 toolkit) */
283
251
  export interface AppCallable {
284
252
  readonly name: string;
@@ -393,60 +361,31 @@ export declare class AsyncRunner {
393
361
  /**
394
362
  * 审批一个处于 `suspended` 的任务(HITL)。
395
363
  *
396
- * - **逐 tool_use_id 幂等**:已存在的决定不覆盖(第一次决定赢)—— 重复提交 /
397
- * 并发点击不会推翻已有决定,也不会让恢复段重复执行;
398
- * - **并发重入共享在飞那次**:同一任务的并发 approve(双击「批准」/两个审批人
399
- * 同时批)返回同一个 Promise —— 否则两个调用都在对方落库前读到
400
- * `suspended`、各自判「决定齐了」、**各派发一次**(同一任务重复执行,
401
- * 与 resumePending 的闸门同一 bug 类)。被共享的那次覆盖不到的决定不丢:
402
- * 调用方从返回的记录看到任务仍在等待,重试即并入;
403
- * - 决定齐了就恢复:`status` 回 `running`、**先落库再派发**(与 `#redispatch`
404
- * 同一条纪律 —— 没落库就恢复,进程崩在窗口里会丢决定);恢复段带着
405
- * `rec.approvals` 与扩展后的消息历史(`rec.spec.messages`,末尾是含未决
406
- * tool_use 的那条 assistant 消息)重进引擎循环;
407
- * - 决定**没**齐:只把本批决定落库,任务继续等(可能多轮审批);
408
- * - 返回当前 TaskRecord(快照)。任务不存在抛 404 语义、状态不对抛 409 语义
409
- * 的 `TaskApproveError`。
364
+ * 实现已外移到 `approval-supervisor.ts`(**逐字搬迁**,含四条语义与它们的理由:
365
+ * 逐 tool_use_id 幂等 / 并发重入共享在飞那次 / 决定齐了先落库再派发 / 状态不对抛
366
+ * `TaskApproveError`)。这里只留薄壳 —— `approve` 是 `AsyncRunner` 的公开 API,
367
+ * 签名与返回类型不变。
410
368
  */
411
369
  approve(taskId: string, decisions: ApprovalDecisions, opts?: {
412
370
  decidedBy?: string;
413
371
  }): Promise<TaskRecord>;
414
- /** 同一任务的在飞审批(approve 的重入闸,见上)。**与惰性超时恢复共用同一把闸** ——
415
- * approve 与 #expireAndResume 都会「填决定 + 落库 + 派发」,不互斥就是同一任务跑两遍。 */
416
- private readonly inflightApprovals;
417
372
  /**
418
- * 投递一个事件给**挂起**的任务(2026-09-28 ⑥,run 事件投入口;
419
- * `POST /tasks/:id/events` 的宿主方法侧,与 approve/cancel 对称)。
420
- *
421
- * 语义(设计稿 §3 逐条的落点):
422
- * - **只对 `suspended` 生效**:不存在 ⇒ 404;其余状态(含已终态)⇒ 409
423
- * (「不唤醒已终态」是这条状态闸的推论,与 approve 同款「状态不对要说出来」);
424
- * - **幂等**:`eventId` 给定时按 `rec.deliveredEventIds` 去重,重复 ⇒ 409
425
- * (簿记随记录落库,重启不丢;有界 FIFO,见 MAX_DELIVERED_EVENT_IDS)。
426
- * 不给 `eventId` 就**没有恰好一次**:重复投递 = 重复进历史(如实,不假装);
427
- * - **投完即续跑**(两种挂起原因都算 —— 对 timer 挂起这就是「提前醒」):
428
- * 事件进 `rec.pendingEvents`、状态回 `running`,**先落库再派发**
429
- * (与 approve / #wakeDueInner 同一条纪律:崩在窗口里不能丢事件)。
430
- * 事件**不**直接追加进 `rec.spec.messages` —— 续跑判定只认历史末尾一条
431
- * (`tailToolUses`),追加 user 消息会把续跑判成新对话;注入由引擎在
432
- * 未决 tool_use 解决之后做(见 engine/loop.ts 的 deliverTaskEvents);
433
- * - **不沿旧 wakeAt**:记录上的旧目标时刻不动(闸都 gate 在 suspended 上,
434
- * 状态一翻它就失效);醒来重跑那一批时工具若再次 `deferUntil`,
435
- * `wakeAt` 由新回合重新落定;
436
- * - drain 期间**仍允许**(与 approve 同理由:挂起的任务只有外部输入能推进)。
373
+ * 投递一个事件给**挂起**的任务(HITL 与 durable timer 两种挂起都适用)。
437
374
  *
438
- * 并发闸与 approve **不同款**:approve 共享在飞那次(决定是幂等合并),事件**串行成链**
439
- * (两条并发事件是两条不同的输入,共享 = 静默丢一条)。与其它恢复路径
440
- * (approve / 到期唤醒,各有自己的闸)的竞速:进闸后重读再判挡住绝大多数;
441
- * 剩下的窄窗口与既有「approve × wakeDue 分闸」同类(那两条按挂起原因天然互斥,
442
- * 事件两种原因都适用,是这个闸新盖的缝 —— 已记在 spec §10 2026-09-28 ⑥)。
375
+ * 实现已外移到 `signal-supervisor.ts`(**逐字搬迁**,含完整语义清单与并发闸的理由:
376
+ * 只对 `suspended` 生效 / 按 `eventId` 幂等 / 缓冲满了拒收并说出来 / 投完即续跑且
377
+ * **先落库再派发** / 并发时**串行成链**而不是像 approve 那样共享在飞那次)。
378
+ * 这里只留薄壳 —— `signalTask` 是 `AsyncRunner` 的公开 API,签名与返回类型不变。
443
379
  */
444
380
  signalTask(taskId: string, event: TaskEvent): Promise<TaskRecord>;
445
- /** 同一任务的在飞事件投递(signalTask 的重入闸,见上) */
446
- private readonly inflightSignals;
447
381
  /** 已受理但未达终态的任务数(queued + running)—— 健康检查与 drain 共用同一口径 */
448
382
  get inFlight(): number;
449
- /** 挂起读数:按原因分组的条数 + 最早的目标时刻(无时间挂起时为 `null`) */
383
+ /**
384
+ * **本进程可见的挂起读数**(`/healthz` 的 `suspended` 段,配套 6)。
385
+ *
386
+ * 口径与三条维护纪律(挂起时登记 / 离开时除名 / 每次扫描按 store 重建)已随审批监督簇
387
+ * 迁到 `approval-supervisor.ts` 的 `#suspended`(那张注释就写在那里);这里只留对外 getter。
388
+ */
450
389
  get suspendedSummary(): SuspendedSummary;
451
390
  /** 是否已进入优雅停机(drain 之后为 true)—— HTTP 宿主据此对新单回 503 */
452
391
  get isDraining(): boolean;
@@ -472,8 +411,6 @@ export declare class AsyncRunner {
472
411
  drain(opts?: {
473
412
  timeoutMs?: number;
474
413
  }): Promise<boolean>;
475
- /** 在飞的到期唤醒(同一任务的重入闸,理由与 inflightApprovals 同款:两条路径都「落库 + 派发」)。 */
476
- private readonly inflightWakes;
477
414
  /**
478
415
  * 当前**真正在排队**的任务数(`maxQueued` 的实时读数,诊断用)—— 与闸用的是**同一个**
479
416
  * `#queueDepth`,所以「看到的数」就是「判据的数」,不会各说各话。
@@ -491,26 +428,13 @@ export declare class AsyncRunner {
491
428
  * (running 视为进程中断)。返回重派数量(异步 store 下返回 Promise<number>)。
492
429
  * 幂等键去重照常生效。
493
430
  *
494
- * `suspended`(HITL)**不捡**:它在等人、不是孤儿(进程没死也可能挂着)。
495
- * 但读到它会做**惰性判定**,两条闸各管各的原因:
496
- * - `approval`:配了 `approvalTimeoutMs` 且已超时的挂起任务自动全拒
497
- * (`denied, reason: '审批超时'`)并重派;
498
- * - `timer`:`wakeAt` 已到的睡着的任务**唤醒并重派**(醒来重跑那一批)。
499
- * ⚠️ `drain()` 之后不唤醒(配套 5):停机窗口里天级的 run 被叫起来接着跑 = 部署卡死。
500
- *
501
- * **认领先落库、再派发**(见 `#redispatch`);异步 store 的认领落库失败会让本方法
502
- * reject —— 宁可让调用方看见「续跑没做」,也不要静默放出一批会被重复执行的任务。
431
+ * 实现在 `resume-scanner.ts`(**逐字搬迁**,含那四步扫描的**顺序**与各自理由、重入闸、
432
+ * 到期索引接线、多进程 `ownerId` 的判定分级)。这里只留薄壳 —— `resumePending` 是
433
+ * `AsyncRunner` 的公开 API,签名与返回类型不变。
503
434
  *
504
- * 多进程共用一个 store 时靠 `ownerId` 区分他我:
505
- * - 本进程的记录一律跳过(它还在本进程内存里跑,重派 = 跑两遍);
506
- * - `staleAfterMs > 0` 时启动他进程判定:**同主机就问 pid 还在不在**(在的不抢、
507
- * 不在的立刻抢),问不到(异主机 / 旧格式 ownerId)才退回保鲜期启发式。
508
- * 缺省 0 = 不判断、一律重派(单进程旧语义)。判定分级与边界见 `resume-policy.ts`。
435
+ * 一句话提要(细节见实现):`suspended` **不捡**(它在等人、不是孤儿),但读到它会做
436
+ * 惰性判定 —— 审批超时自动全拒并重派、`wakeAt` 已到的唤醒并重派(`drain()` 之后不唤,
437
+ * 配套 5);认领**先落库再派发**;多进程靠 `ownerId` 区分他我(`staleAfterMs > 0` 时问 pid)。
509
438
  */
510
439
  resumePending(opts?: ResumePendingOptions): number | Promise<number>;
511
- /**
512
- * 在飞的 `resumePending`(重入闸,见上)。不用 boolean 而用 Promise:
513
- * 重入方要拿到**同一次扫描**的结果,而不是一个「你等着」的空数。
514
- */
515
- private inflightResume;
516
440
  }