@gt-fe/eap-runtime 0.0.8

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 (326) hide show
  1. package/README.md +323 -0
  2. package/dist/api/handlers/index.d.ts +3 -0
  3. package/dist/api/handlers/index.js +4 -0
  4. package/dist/api/handlers/invoke.handler.d.ts +77 -0
  5. package/dist/api/handlers/invoke.handler.js +411 -0
  6. package/dist/api/handlers/messages.handler.d.ts +53 -0
  7. package/dist/api/handlers/messages.handler.js +80 -0
  8. package/dist/api/handlers/stream.handler.d.ts +92 -0
  9. package/dist/api/handlers/stream.handler.js +432 -0
  10. package/dist/api/index.d.ts +3 -0
  11. package/dist/api/index.js +4 -0
  12. package/dist/api/middleware/auth.middleware.d.ts +96 -0
  13. package/dist/api/middleware/auth.middleware.js +158 -0
  14. package/dist/api/middleware/index.d.ts +1 -0
  15. package/dist/api/middleware/index.js +2 -0
  16. package/dist/api/routes/agent.routes.d.ts +44 -0
  17. package/dist/api/routes/agent.routes.js +204 -0
  18. package/dist/api/routes/index.d.ts +3 -0
  19. package/dist/api/routes/index.js +4 -0
  20. package/dist/api/routes/session.routes.d.ts +82 -0
  21. package/dist/api/routes/session.routes.js +169 -0
  22. package/dist/api/routes/task.routes.d.ts +75 -0
  23. package/dist/api/routes/task.routes.js +108 -0
  24. package/dist/checkpoint/checkpoint-serializer.d.ts +58 -0
  25. package/dist/checkpoint/checkpoint-serializer.js +81 -0
  26. package/dist/checkpoint/composite-checkpoint-saver.d.ts +42 -0
  27. package/dist/checkpoint/composite-checkpoint-saver.js +118 -0
  28. package/dist/checkpoint/contracts/checkpoint-saver.contract.d.ts +65 -0
  29. package/dist/checkpoint/contracts/checkpoint-saver.contract.js +12 -0
  30. package/dist/checkpoint/contracts/index.d.ts +1 -0
  31. package/dist/checkpoint/contracts/index.js +1 -0
  32. package/dist/checkpoint/file-checkpoint-saver.d.ts +19 -0
  33. package/dist/checkpoint/file-checkpoint-saver.js +98 -0
  34. package/dist/checkpoint/hybrid-checkpoint-saver.d.ts +130 -0
  35. package/dist/checkpoint/hybrid-checkpoint-saver.js +211 -0
  36. package/dist/checkpoint/in-memory-checkpoint-saver.d.ts +14 -0
  37. package/dist/checkpoint/in-memory-checkpoint-saver.js +46 -0
  38. package/dist/checkpoint/index.d.ts +16 -0
  39. package/dist/checkpoint/index.js +13 -0
  40. package/dist/checkpoint/mongo-checkpoint-saver.d.ts +38 -0
  41. package/dist/checkpoint/mongo-checkpoint-saver.js +56 -0
  42. package/dist/checkpoint/pg-checkpoint-saver.d.ts +59 -0
  43. package/dist/checkpoint/pg-checkpoint-saver.js +140 -0
  44. package/dist/checkpoint/redis-checkpoint-saver.d.ts +68 -0
  45. package/dist/checkpoint/redis-checkpoint-saver.js +173 -0
  46. package/dist/checkpoint/restore-checkpoint.d.ts +33 -0
  47. package/dist/checkpoint/restore-checkpoint.js +33 -0
  48. package/dist/common/agent-code.d.ts +10 -0
  49. package/dist/common/agent-code.js +12 -0
  50. package/dist/common/concurrency.d.ts +79 -0
  51. package/dist/common/concurrency.js +121 -0
  52. package/dist/common/health.d.ts +40 -0
  53. package/dist/common/health.js +37 -0
  54. package/dist/common/index.d.ts +5 -0
  55. package/dist/common/index.js +5 -0
  56. package/dist/common/logger.d.ts +34 -0
  57. package/dist/common/logger.js +38 -0
  58. package/dist/common/queue-consumer.d.ts +125 -0
  59. package/dist/common/queue-consumer.js +159 -0
  60. package/dist/common/trace-id.d.ts +2 -0
  61. package/dist/common/trace-id.js +8 -0
  62. package/dist/common/types.d.ts +104 -0
  63. package/dist/common/types.js +14 -0
  64. package/dist/contracts/index.d.ts +11 -0
  65. package/dist/contracts/index.js +10 -0
  66. package/dist/contracts/permission-checker.d.ts +10 -0
  67. package/dist/contracts/permission-checker.js +4 -0
  68. package/dist/contracts/trace-collector.d.ts +211 -0
  69. package/dist/contracts/trace-collector.js +13 -0
  70. package/dist/governance/governance-client.d.ts +38 -0
  71. package/dist/governance/governance-client.js +95 -0
  72. package/dist/governance/index.d.ts +7 -0
  73. package/dist/governance/index.js +5 -0
  74. package/dist/governance/permission-guard.d.ts +19 -0
  75. package/dist/governance/permission-guard.js +59 -0
  76. package/dist/graph/adapters/index.d.ts +3 -0
  77. package/dist/graph/adapters/index.js +3 -0
  78. package/dist/graph/adapters/langgraph-checkpointer.d.ts +40 -0
  79. package/dist/graph/adapters/langgraph-checkpointer.js +211 -0
  80. package/dist/graph/adapters/langgraph.adapter.d.ts +190 -0
  81. package/dist/graph/adapters/langgraph.adapter.js +1303 -0
  82. package/dist/graph/adapters/subgraph-compiler.d.ts +39 -0
  83. package/dist/graph/adapters/subgraph-compiler.js +248 -0
  84. package/dist/graph/condition-evaluator.d.ts +57 -0
  85. package/dist/graph/condition-evaluator.js +162 -0
  86. package/dist/graph/contracts/executable-graph.d.ts +340 -0
  87. package/dist/graph/contracts/executable-graph.js +10 -0
  88. package/dist/graph/contracts/index.d.ts +2 -0
  89. package/dist/graph/contracts/index.js +2 -0
  90. package/dist/graph/contracts/state-graph.contract.d.ts +76 -0
  91. package/dist/graph/contracts/state-graph.contract.js +11 -0
  92. package/dist/graph/hitl/hitl-handler.d.ts +199 -0
  93. package/dist/graph/hitl/hitl-handler.js +480 -0
  94. package/dist/graph/hitl/index.d.ts +1 -0
  95. package/dist/graph/hitl/index.js +1 -0
  96. package/dist/graph/index.d.ts +7 -0
  97. package/dist/graph/index.js +7 -0
  98. package/dist/graph/nodes/index.d.ts +2 -0
  99. package/dist/graph/nodes/index.js +2 -0
  100. package/dist/graph/nodes/llm-node.d.ts +182 -0
  101. package/dist/graph/nodes/llm-node.js +281 -0
  102. package/dist/graph/nodes/skill-bind-node.d.ts +32 -0
  103. package/dist/graph/nodes/skill-bind-node.js +116 -0
  104. package/dist/graph/nodes/tool-node.d.ts +75 -0
  105. package/dist/graph/nodes/tool-node.js +324 -0
  106. package/dist/graph/resolve-graph-prompt.d.ts +5 -0
  107. package/dist/graph/resolve-graph-prompt.js +75 -0
  108. package/dist/graph/retry/budget-aware-retry.d.ts +91 -0
  109. package/dist/graph/retry/budget-aware-retry.js +126 -0
  110. package/dist/graph/retry/index.d.ts +9 -0
  111. package/dist/graph/retry/index.js +8 -0
  112. package/dist/graph/retry/node-retry.d.ts +123 -0
  113. package/dist/graph/retry/node-retry.js +235 -0
  114. package/dist/graph/skill-audit-recorder.d.ts +126 -0
  115. package/dist/graph/skill-audit-recorder.js +114 -0
  116. package/dist/index.d.ts +21 -0
  117. package/dist/index.js +32 -0
  118. package/dist/memory/compression/compression-stats.d.ts +109 -0
  119. package/dist/memory/compression/compression-stats.js +144 -0
  120. package/dist/memory/compression/compression.repository.d.ts +62 -0
  121. package/dist/memory/compression/compression.repository.js +71 -0
  122. package/dist/memory/compression/index.d.ts +10 -0
  123. package/dist/memory/compression/index.js +15 -0
  124. package/dist/memory/compression/llm-summarize.d.ts +58 -0
  125. package/dist/memory/compression/llm-summarize.js +192 -0
  126. package/dist/memory/compression/sliding-window.compression.d.ts +60 -0
  127. package/dist/memory/compression/sliding-window.compression.js +175 -0
  128. package/dist/memory/index.d.ts +4 -0
  129. package/dist/memory/index.js +7 -0
  130. package/dist/memory/long-term-memory.service.d.ts +94 -0
  131. package/dist/memory/long-term-memory.service.js +120 -0
  132. package/dist/memory/message-compression.service.d.ts +128 -0
  133. package/dist/memory/message-compression.service.js +69 -0
  134. package/dist/memory/pg-memory.repository.d.ts +44 -0
  135. package/dist/memory/pg-memory.repository.js +108 -0
  136. package/dist/memory/semantic-retrieval.service.d.ts +148 -0
  137. package/dist/memory/semantic-retrieval.service.js +238 -0
  138. package/dist/providers/echo-llm-provider.d.ts +10 -0
  139. package/dist/providers/echo-llm-provider.js +87 -0
  140. package/dist/providers/http-llm-provider.d.ts +35 -0
  141. package/dist/providers/http-llm-provider.js +404 -0
  142. package/dist/providers/index.d.ts +7 -0
  143. package/dist/providers/index.js +5 -0
  144. package/dist/providers/local-llm-provider.d.ts +27 -0
  145. package/dist/providers/local-llm-provider.js +105 -0
  146. package/dist/providers/resolve-llm-model.d.ts +13 -0
  147. package/dist/providers/resolve-llm-model.js +30 -0
  148. package/dist/providers/routing-llm-provider.d.ts +28 -0
  149. package/dist/providers/routing-llm-provider.js +79 -0
  150. package/dist/registry/agent-models.d.ts +1 -0
  151. package/dist/registry/agent-models.js +1 -0
  152. package/dist/registry/agent-registry-client.d.ts +56 -0
  153. package/dist/registry/agent-registry-client.js +36 -0
  154. package/dist/registry/definition-cache.d.ts +37 -0
  155. package/dist/registry/definition-cache.js +82 -0
  156. package/dist/registry/deployment-snapshot.d.ts +99 -0
  157. package/dist/registry/deployment-snapshot.js +542 -0
  158. package/dist/registry/graph-loader.d.ts +134 -0
  159. package/dist/registry/graph-loader.js +985 -0
  160. package/dist/registry/index.d.ts +9 -0
  161. package/dist/registry/index.js +9 -0
  162. package/dist/registry/preload-errors.d.ts +13 -0
  163. package/dist/registry/preload-errors.js +35 -0
  164. package/dist/registry/preload-flags.d.ts +22 -0
  165. package/dist/registry/preload-flags.js +44 -0
  166. package/dist/registry/skill-io-guard.d.ts +25 -0
  167. package/dist/registry/skill-io-guard.js +98 -0
  168. package/dist/registry/skill-tree.d.ts +27 -0
  169. package/dist/registry/skill-tree.js +183 -0
  170. package/dist/runtime/agent-runtime.d.ts +106 -0
  171. package/dist/runtime/agent-runtime.js +777 -0
  172. package/dist/runtime/dev-runtime.d.ts +69 -0
  173. package/dist/runtime/dev-runtime.js +199 -0
  174. package/dist/runtime/index.d.ts +4 -0
  175. package/dist/runtime/index.js +3 -0
  176. package/dist/runtime/storage-profile.d.ts +50 -0
  177. package/dist/runtime/storage-profile.js +138 -0
  178. package/dist/runtime/types.d.ts +120 -0
  179. package/dist/runtime/types.js +9 -0
  180. package/dist/server.d.ts +12 -0
  181. package/dist/server.js +161 -0
  182. package/dist/session/compression-middleware.d.ts +126 -0
  183. package/dist/session/compression-middleware.js +230 -0
  184. package/dist/session/file-message.repository.d.ts +25 -0
  185. package/dist/session/file-message.repository.js +78 -0
  186. package/dist/session/file-session.repository.d.ts +72 -0
  187. package/dist/session/file-session.repository.js +212 -0
  188. package/dist/session/index.d.ts +13 -0
  189. package/dist/session/index.js +12 -0
  190. package/dist/session/message.service.d.ts +80 -0
  191. package/dist/session/message.service.js +128 -0
  192. package/dist/session/persist-turn-messages.d.ts +145 -0
  193. package/dist/session/persist-turn-messages.js +325 -0
  194. package/dist/session/pg-message.repository.d.ts +49 -0
  195. package/dist/session/pg-message.repository.js +149 -0
  196. package/dist/session/pg-session.repository.d.ts +46 -0
  197. package/dist/session/pg-session.repository.js +117 -0
  198. package/dist/session/session-context.service.d.ts +49 -0
  199. package/dist/session/session-context.service.js +66 -0
  200. package/dist/session/session.service.d.ts +194 -0
  201. package/dist/session/session.service.js +166 -0
  202. package/dist/sse/connection-event-logger.d.ts +21 -0
  203. package/dist/sse/connection-event-logger.js +33 -0
  204. package/dist/sse/contracts/index.d.ts +1 -0
  205. package/dist/sse/contracts/index.js +1 -0
  206. package/dist/sse/contracts/sse-emitter.contract.d.ts +74 -0
  207. package/dist/sse/contracts/sse-emitter.contract.js +11 -0
  208. package/dist/sse/event-buffer.d.ts +20 -0
  209. package/dist/sse/event-buffer.js +45 -0
  210. package/dist/sse/events/code-events.d.ts +19 -0
  211. package/dist/sse/events/code-events.js +9 -0
  212. package/dist/sse/events/collaboration-events.d.ts +37 -0
  213. package/dist/sse/events/collaboration-events.js +11 -0
  214. package/dist/sse/events/connection-events.d.ts +13 -0
  215. package/dist/sse/events/connection-events.js +9 -0
  216. package/dist/sse/events/file-events.d.ts +29 -0
  217. package/dist/sse/events/file-events.js +10 -0
  218. package/dist/sse/events/hitl-events.d.ts +33 -0
  219. package/dist/sse/events/hitl-events.js +10 -0
  220. package/dist/sse/events/index.d.ts +107 -0
  221. package/dist/sse/events/index.js +96 -0
  222. package/dist/sse/events/plan-events.d.ts +38 -0
  223. package/dist/sse/events/plan-events.js +11 -0
  224. package/dist/sse/events/reasoning-events.d.ts +35 -0
  225. package/dist/sse/events/reasoning-events.js +13 -0
  226. package/dist/sse/events/response-events.d.ts +25 -0
  227. package/dist/sse/events/response-events.js +12 -0
  228. package/dist/sse/events/session-events.d.ts +43 -0
  229. package/dist/sse/events/session-events.js +12 -0
  230. package/dist/sse/events/skill-events.d.ts +48 -0
  231. package/dist/sse/events/skill-events.js +11 -0
  232. package/dist/sse/events/stats-events.d.ts +44 -0
  233. package/dist/sse/events/stats-events.js +12 -0
  234. package/dist/sse/events/tool-events.d.ts +68 -0
  235. package/dist/sse/events/tool-events.js +14 -0
  236. package/dist/sse/index.d.ts +8 -0
  237. package/dist/sse/index.js +8 -0
  238. package/dist/sse/sse-connection-manager.d.ts +116 -0
  239. package/dist/sse/sse-connection-manager.js +185 -0
  240. package/dist/sse/sse-emitter.d.ts +108 -0
  241. package/dist/sse/sse-emitter.js +210 -0
  242. package/dist/sse/sse-heartbeat.d.ts +58 -0
  243. package/dist/sse/sse-heartbeat.js +76 -0
  244. package/dist/sse/trace-collector.d.ts +60 -0
  245. package/dist/sse/trace-collector.js +107 -0
  246. package/dist/task/contracts/index.d.ts +2 -0
  247. package/dist/task/contracts/index.js +1 -0
  248. package/dist/task/contracts/task-manager.contract.d.ts +160 -0
  249. package/dist/task/contracts/task-manager.contract.js +10 -0
  250. package/dist/task/index.d.ts +7 -0
  251. package/dist/task/index.js +5 -0
  252. package/dist/task/pg-task.repository.d.ts +47 -0
  253. package/dist/task/pg-task.repository.js +131 -0
  254. package/dist/task/task-progress-tracker.d.ts +67 -0
  255. package/dist/task/task-progress-tracker.js +80 -0
  256. package/dist/task/task-timeout.d.ts +70 -0
  257. package/dist/task/task-timeout.js +116 -0
  258. package/dist/task/task.service.d.ts +84 -0
  259. package/dist/task/task.service.js +233 -0
  260. package/dist/token/compression-budget-integration.d.ts +145 -0
  261. package/dist/token/compression-budget-integration.js +185 -0
  262. package/dist/token/context-window-budget.d.ts +113 -0
  263. package/dist/token/context-window-budget.js +134 -0
  264. package/dist/token/contracts/index.d.ts +1 -0
  265. package/dist/token/contracts/index.js +1 -0
  266. package/dist/token/contracts/token-budget.contract.d.ts +77 -0
  267. package/dist/token/contracts/token-budget.contract.js +11 -0
  268. package/dist/token/index.d.ts +12 -0
  269. package/dist/token/index.js +10 -0
  270. package/dist/token/pg-token-budget.repository.d.ts +39 -0
  271. package/dist/token/pg-token-budget.repository.js +116 -0
  272. package/dist/token/session-budget-checker.d.ts +54 -0
  273. package/dist/token/session-budget-checker.js +72 -0
  274. package/dist/token/token-budget-guard.d.ts +66 -0
  275. package/dist/token/token-budget-guard.js +97 -0
  276. package/dist/token/token-budget.service.d.ts +86 -0
  277. package/dist/token/token-budget.service.js +130 -0
  278. package/dist/token/token-detail-mode.d.ts +73 -0
  279. package/dist/token/token-detail-mode.js +107 -0
  280. package/dist/token/token-estimator.d.ts +80 -0
  281. package/dist/token/token-estimator.js +134 -0
  282. package/dist/token/token-stats.service.d.ts +88 -0
  283. package/dist/token/token-stats.service.js +94 -0
  284. package/dist/tool/create-tool-executor.d.ts +33 -0
  285. package/dist/tool/create-tool-executor.js +60 -0
  286. package/dist/tool/dev-http-executor.d.ts +18 -0
  287. package/dist/tool/dev-http-executor.js +96 -0
  288. package/dist/tool/dev-mcp-executor.d.ts +29 -0
  289. package/dist/tool/dev-mcp-executor.js +84 -0
  290. package/dist/tool/dev-tool-executor.d.ts +23 -0
  291. package/dist/tool/dev-tool-executor.js +55 -0
  292. package/dist/tool/file-artifact.d.ts +11 -0
  293. package/dist/tool/file-artifact.js +90 -0
  294. package/dist/tool/http-tool-executor.d.ts +17 -0
  295. package/dist/tool/http-tool-executor.js +158 -0
  296. package/dist/tool/index.d.ts +18 -0
  297. package/dist/tool/index.js +17 -0
  298. package/dist/tool/loaded-tool-definition.d.ts +35 -0
  299. package/dist/tool/loaded-tool-definition.js +9 -0
  300. package/dist/tool/local-mcp-client.d.ts +23 -0
  301. package/dist/tool/local-mcp-client.js +225 -0
  302. package/dist/tool/local-tool-executor.d.ts +37 -0
  303. package/dist/tool/local-tool-executor.js +249 -0
  304. package/dist/tool/mcp-tool-executor.d.ts +23 -0
  305. package/dist/tool/mcp-tool-executor.js +160 -0
  306. package/dist/tool/mock-tool-executor.d.ts +28 -0
  307. package/dist/tool/mock-tool-executor.js +65 -0
  308. package/dist/tool/system-prompt-builder.d.ts +114 -0
  309. package/dist/tool/system-prompt-builder.js +213 -0
  310. package/dist/tool/tool-confirmation.d.ts +29 -0
  311. package/dist/tool/tool-confirmation.js +81 -0
  312. package/dist/tool/tool-event-emitter.d.ts +58 -0
  313. package/dist/tool/tool-event-emitter.js +154 -0
  314. package/dist/tool/tool-execution-router.d.ts +25 -0
  315. package/dist/tool/tool-execution-router.js +50 -0
  316. package/dist/tool/tool-executor.interface.d.ts +85 -0
  317. package/dist/tool/tool-executor.interface.js +7 -0
  318. package/dist/tool/tool-orchestrator.d.ts +22 -0
  319. package/dist/tool/tool-orchestrator.js +104 -0
  320. package/dist/tool/tool-result-injector.d.ts +64 -0
  321. package/dist/tool/tool-result-injector.js +65 -0
  322. package/dist/utils/format-error.d.ts +4 -0
  323. package/dist/utils/format-error.js +4 -0
  324. package/dist/utils/http-error.d.ts +11 -0
  325. package/dist/utils/http-error.js +24 -0
  326. package/package.json +56 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * PostgresCheckpointSaver — PostgreSQL 存储(主存储)
3
+ *
4
+ * 使用接口抽象,不引入 pg 依赖。参数占位符用 $1/$2 替代 MySQL 的 ?。
5
+ * 支持审计字段和软删除。
6
+ *
7
+ * @see design.md §1.3 checkpoints 数据模型
8
+ * @see checkpoint-saver.contract.ts ICheckpointSaver 接口定义
9
+ */
10
+ // ─────────────────────────────────────────────
11
+ // PostgresCheckpointSaver
12
+ // ─────────────────────────────────────────────
13
+ export class PostgresCheckpointSaver {
14
+ pool;
15
+ constructor(pool) {
16
+ this.pool = pool;
17
+ }
18
+ /**
19
+ * 持久化 Checkpoint(INSERT or UPDATE)
20
+ */
21
+ async save(threadId, data) {
22
+ const sql = `
23
+ INSERT INTO checkpoints (id, thread_id, checkpoint_id, current_node_id, history_path, state, created_by, created_at, updated_by, updated_at)
24
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
25
+ ON CONFLICT (thread_id, checkpoint_id) DO UPDATE SET
26
+ current_node_id = EXCLUDED.current_node_id,
27
+ history_path = EXCLUDED.history_path,
28
+ state = EXCLUDED.state,
29
+ updated_by = EXCLUDED.updated_by,
30
+ updated_at = EXCLUDED.updated_at
31
+ `;
32
+ await this.pool.query(sql, [
33
+ data.checkpointId,
34
+ threadId,
35
+ data.checkpointId,
36
+ data.currentNodeId,
37
+ JSON.stringify(data.historyPath),
38
+ JSON.stringify(data.state),
39
+ data.createdBy || 'system',
40
+ data.createdAt,
41
+ data.updatedBy || 'system',
42
+ data.updatedAt,
43
+ ]);
44
+ }
45
+ /**
46
+ * 加载指定 thread 的最新 Checkpoint
47
+ * Excludes soft-deleted records
48
+ */
49
+ async load(threadId) {
50
+ const sql = `SELECT * FROM checkpoints WHERE thread_id = $1 AND is_deleted = FALSE ORDER BY created_at DESC LIMIT 1`;
51
+ const { rows } = await this.pool.query(sql, [threadId]);
52
+ return rows[0] ? this.toCheckpointData(rows[0]) : null;
53
+ }
54
+ /**
55
+ * 按 checkpointId 加载特定 Checkpoint
56
+ * Excludes soft-deleted records
57
+ */
58
+ async loadByCheckpointId(checkpointId) {
59
+ const sql = `SELECT * FROM checkpoints WHERE checkpoint_id = $1 AND is_deleted = FALSE LIMIT 1`;
60
+ const { rows } = await this.pool.query(sql, [checkpointId]);
61
+ return rows[0] ? this.toCheckpointData(rows[0]) : null;
62
+ }
63
+ /**
64
+ * 列出指定 thread 的所有 Checkpoint
65
+ * Excludes soft-deleted records
66
+ */
67
+ async list(threadId) {
68
+ const sql = `SELECT * FROM checkpoints WHERE thread_id = $1 AND is_deleted = FALSE ORDER BY created_at ASC`;
69
+ const { rows } = await this.pool.query(sql, [threadId]);
70
+ return rows.map(r => this.toCheckpointData(r));
71
+ }
72
+ /**
73
+ * Soft delete checkpoint
74
+ */
75
+ async softDelete(checkpointId, deletedBy) {
76
+ const sql = `
77
+ UPDATE checkpoints
78
+ SET is_deleted = TRUE, deleted_by = $1, deleted_at = NOW()
79
+ WHERE checkpoint_id = $2 AND is_deleted = FALSE
80
+ `;
81
+ await this.pool.query(sql, [deletedBy, checkpointId]);
82
+ }
83
+ /**
84
+ * Fork:从指定 Checkpoint 创建新的执行线程
85
+ *
86
+ * 用于用户调试场景,从某个 Checkpoint 恢复执行,支持修改参数后重新执行。
87
+ *
88
+ * @param sourceCheckpointId - 源 Checkpoint ID
89
+ * @param newThreadId - 新线程 ID
90
+ * @param overrides - 覆盖的参数(可选)
91
+ * @returns 新的 CheckpointData
92
+ */
93
+ async fork(sourceCheckpointId, newThreadId, overrides) {
94
+ // 1. 加载源 Checkpoint
95
+ const source = await this.loadByCheckpointId(sourceCheckpointId);
96
+ if (!source) {
97
+ throw new Error(`Source checkpoint not found: ${sourceCheckpointId}`);
98
+ }
99
+ // 2. 创建新的 CheckpointData
100
+ const now = new Date().toISOString();
101
+ const newCheckpointId = crypto.randomUUID();
102
+ const newData = {
103
+ checkpointId: newCheckpointId,
104
+ threadId: newThreadId,
105
+ currentNodeId: source.currentNodeId,
106
+ historyPath: source.historyPath,
107
+ state: overrides ? { ...source.state, ...overrides } : source.state,
108
+ createdBy: 'fork',
109
+ createdAt: now,
110
+ updatedBy: 'fork',
111
+ updatedAt: now,
112
+ isDeleted: false,
113
+ };
114
+ // 3. 保存新的 Checkpoint
115
+ await this.save(newThreadId, newData);
116
+ return newData;
117
+ }
118
+ // ─────────────────────────────────────────────
119
+ // Private Helpers
120
+ // ─────────────────────────────────────────────
121
+ /**
122
+ * 将 PostgreSQL 行数据转换为 CheckpointData
123
+ */
124
+ toCheckpointData(row) {
125
+ return {
126
+ checkpointId: row.checkpoint_id,
127
+ threadId: row.thread_id,
128
+ currentNodeId: row.current_node_id,
129
+ historyPath: typeof row.history_path === 'string' ? JSON.parse(row.history_path) : row.history_path,
130
+ state: typeof row.state === 'string' ? JSON.parse(row.state) : row.state,
131
+ createdBy: row.created_by,
132
+ createdAt: row.created_at.toISOString(),
133
+ updatedBy: row.updated_by,
134
+ updatedAt: row.updated_at.toISOString(),
135
+ isDeleted: row.is_deleted,
136
+ deletedBy: row.deleted_by,
137
+ deletedAt: row.deleted_at?.toISOString(),
138
+ };
139
+ }
140
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * RedisCheckpointSaver — 基于 Redis 的 Checkpoint 持久化实现
3
+ *
4
+ * Key 模式:
5
+ * - 单个 Checkpoint: `thread:{threadId}:cp:{checkpointId}` → JSON 序列化的 CheckpointData
6
+ * - Thread 索引(Sorted Set): `thread:{threadId}:checkpoints` → member=checkpointId, score=createdAt timestamp
7
+ * - 最新指针: `thread:{threadId}:latest` → 存储最新 checkpointId
8
+ * - 反向索引: `cp:{checkpointId}:thread` → 存储 threadId(供 loadByCheckpointId 使用)
9
+ *
10
+ * @see design.md §1.3 checkpoints 数据模型
11
+ */
12
+ import type { Redis } from 'ioredis';
13
+ import type { ICheckpointSaver } from './contracts/checkpoint-saver.contract.js';
14
+ import type { CheckpointData } from '../graph/contracts/executable-graph.js';
15
+ /**
16
+ * RedisCheckpointSaver 配置选项
17
+ */
18
+ export interface RedisCheckpointSaverOptions {
19
+ /** Key 前缀(默认无前缀) */
20
+ keyPrefix?: string;
21
+ /** Checkpoint TTL(秒),默认不过期 */
22
+ ttl?: number;
23
+ }
24
+ /**
25
+ * RedisCheckpointSaver — ICheckpointSaver 的 Redis 实现
26
+ *
27
+ * 通过构造函数注入 ioredis 实例(DI 友好),支持可选的 TTL 和 key 前缀配置。
28
+ */
29
+ export declare class RedisCheckpointSaver implements ICheckpointSaver {
30
+ private readonly redis;
31
+ private readonly keyPrefix;
32
+ private readonly ttl;
33
+ constructor(redis: Redis, options?: RedisCheckpointSaverOptions);
34
+ private checkpointKey;
35
+ private threadIndexKey;
36
+ private latestKey;
37
+ private reverseIndexKey;
38
+ /**
39
+ * 持久化 Checkpoint
40
+ *
41
+ * 原子操作:SET checkpoint JSON + ZADD 到 thread 索引 + SET latest 指针 + SET 反向索引
42
+ * 使用 Redis pipeline 保证原子性和性能。
43
+ */
44
+ save(threadId: string, data: CheckpointData): Promise<void>;
45
+ /**
46
+ * 加载指定 thread 的最新 Checkpoint
47
+ *
48
+ * 流程:GET latest 指针 → GET checkpoint JSON → 解析返回
49
+ */
50
+ load(threadId: string): Promise<CheckpointData | null>;
51
+ /**
52
+ * 按 checkpointId 加载特定 Checkpoint
53
+ *
54
+ * 流程:通过反向索引 `cp:{checkpointId}:thread` 找到 threadId → GET checkpoint JSON
55
+ */
56
+ loadByCheckpointId(checkpointId: string): Promise<CheckpointData | null>;
57
+ /**
58
+ * 列出指定 thread 的所有 Checkpoint
59
+ *
60
+ * 流程:ZRANGE thread 索引获取所有 checkpointId → 批量 GET → 解析返回
61
+ * 结果按 createdAt 升序排列(Sorted Set score 升序)。
62
+ */
63
+ list(threadId: string): Promise<CheckpointData[]>;
64
+ /**
65
+ * Fork: 从指定 Checkpoint 创建新的执行线程
66
+ */
67
+ fork(sourceCheckpointId: string, newThreadId: string, overrides?: Record<string, unknown>): Promise<import('../graph/contracts/executable-graph.js').CheckpointData>;
68
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * RedisCheckpointSaver — 基于 Redis 的 Checkpoint 持久化实现
3
+ *
4
+ * Key 模式:
5
+ * - 单个 Checkpoint: `thread:{threadId}:cp:{checkpointId}` → JSON 序列化的 CheckpointData
6
+ * - Thread 索引(Sorted Set): `thread:{threadId}:checkpoints` → member=checkpointId, score=createdAt timestamp
7
+ * - 最新指针: `thread:{threadId}:latest` → 存储最新 checkpointId
8
+ * - 反向索引: `cp:{checkpointId}:thread` → 存储 threadId(供 loadByCheckpointId 使用)
9
+ *
10
+ * @see design.md §1.3 checkpoints 数据模型
11
+ */
12
+ /**
13
+ * RedisCheckpointSaver — ICheckpointSaver 的 Redis 实现
14
+ *
15
+ * 通过构造函数注入 ioredis 实例(DI 友好),支持可选的 TTL 和 key 前缀配置。
16
+ */
17
+ export class RedisCheckpointSaver {
18
+ redis;
19
+ keyPrefix;
20
+ ttl;
21
+ constructor(redis, options) {
22
+ this.redis = redis;
23
+ this.keyPrefix = options?.keyPrefix ?? '';
24
+ this.ttl = options?.ttl;
25
+ }
26
+ // ─────────────────────────────────────────────
27
+ // Key 构建辅助
28
+ // ─────────────────────────────────────────────
29
+ checkpointKey(threadId, checkpointId) {
30
+ return `${this.keyPrefix}thread:${threadId}:cp:${checkpointId}`;
31
+ }
32
+ threadIndexKey(threadId) {
33
+ return `${this.keyPrefix}thread:${threadId}:checkpoints`;
34
+ }
35
+ latestKey(threadId) {
36
+ return `${this.keyPrefix}thread:${threadId}:latest`;
37
+ }
38
+ reverseIndexKey(checkpointId) {
39
+ return `${this.keyPrefix}cp:${checkpointId}:thread`;
40
+ }
41
+ // ─────────────────────────────────────────────
42
+ // ICheckpointSaver 实现
43
+ // ─────────────────────────────────────────────
44
+ /**
45
+ * 持久化 Checkpoint
46
+ *
47
+ * 原子操作:SET checkpoint JSON + ZADD 到 thread 索引 + SET latest 指针 + SET 反向索引
48
+ * 使用 Redis pipeline 保证原子性和性能。
49
+ */
50
+ async save(threadId, data) {
51
+ const cpKey = this.checkpointKey(threadId, data.checkpointId);
52
+ const idxKey = this.threadIndexKey(threadId);
53
+ const ltKey = this.latestKey(threadId);
54
+ const revKey = this.reverseIndexKey(data.checkpointId);
55
+ const json = JSON.stringify(data);
56
+ const score = new Date(data.createdAt).getTime();
57
+ const pipeline = this.redis.pipeline();
58
+ // 存储 Checkpoint JSON
59
+ if (this.ttl) {
60
+ pipeline.set(cpKey, json, 'EX', this.ttl);
61
+ }
62
+ else {
63
+ pipeline.set(cpKey, json);
64
+ }
65
+ // Thread 索引 (Sorted Set): checkpointId 作为 member, createdAt timestamp 作为 score
66
+ pipeline.zadd(idxKey, score, data.checkpointId);
67
+ // 最新指针
68
+ pipeline.set(ltKey, data.checkpointId);
69
+ // 反向索引: checkpointId → threadId
70
+ if (this.ttl) {
71
+ pipeline.set(revKey, threadId, 'EX', this.ttl);
72
+ }
73
+ else {
74
+ pipeline.set(revKey, threadId);
75
+ }
76
+ // 对索引和 latest 指针也设置 TTL(如果配置了)
77
+ if (this.ttl) {
78
+ pipeline.expire(idxKey, this.ttl);
79
+ pipeline.expire(ltKey, this.ttl);
80
+ }
81
+ await pipeline.exec();
82
+ }
83
+ /**
84
+ * 加载指定 thread 的最新 Checkpoint
85
+ *
86
+ * 流程:GET latest 指针 → GET checkpoint JSON → 解析返回
87
+ */
88
+ async load(threadId) {
89
+ const ltKey = this.latestKey(threadId);
90
+ const latestCheckpointId = await this.redis.get(ltKey);
91
+ if (!latestCheckpointId) {
92
+ return null;
93
+ }
94
+ const cpKey = this.checkpointKey(threadId, latestCheckpointId);
95
+ const json = await this.redis.get(cpKey);
96
+ if (!json) {
97
+ return null;
98
+ }
99
+ return JSON.parse(json);
100
+ }
101
+ /**
102
+ * 按 checkpointId 加载特定 Checkpoint
103
+ *
104
+ * 流程:通过反向索引 `cp:{checkpointId}:thread` 找到 threadId → GET checkpoint JSON
105
+ */
106
+ async loadByCheckpointId(checkpointId) {
107
+ const revKey = this.reverseIndexKey(checkpointId);
108
+ const threadId = await this.redis.get(revKey);
109
+ if (!threadId) {
110
+ return null;
111
+ }
112
+ const cpKey = this.checkpointKey(threadId, checkpointId);
113
+ const json = await this.redis.get(cpKey);
114
+ if (!json) {
115
+ return null;
116
+ }
117
+ return JSON.parse(json);
118
+ }
119
+ /**
120
+ * 列出指定 thread 的所有 Checkpoint
121
+ *
122
+ * 流程:ZRANGE thread 索引获取所有 checkpointId → 批量 GET → 解析返回
123
+ * 结果按 createdAt 升序排列(Sorted Set score 升序)。
124
+ */
125
+ async list(threadId) {
126
+ const idxKey = this.threadIndexKey(threadId);
127
+ // ZRANGE 默认升序(按 score 即 createdAt timestamp)
128
+ const checkpointIds = await this.redis.zrange(idxKey, 0, -1);
129
+ if (checkpointIds.length === 0) {
130
+ return [];
131
+ }
132
+ // 批量获取所有 Checkpoint JSON(使用 pipeline 减少 RTT)
133
+ const pipeline = this.redis.pipeline();
134
+ for (const cpId of checkpointIds) {
135
+ pipeline.get(this.checkpointKey(threadId, cpId));
136
+ }
137
+ const results = await pipeline.exec();
138
+ const checkpoints = [];
139
+ if (results) {
140
+ for (const [err, json] of results) {
141
+ if (!err && json && typeof json === 'string') {
142
+ checkpoints.push(JSON.parse(json));
143
+ }
144
+ }
145
+ }
146
+ return checkpoints;
147
+ }
148
+ /**
149
+ * Fork: 从指定 Checkpoint 创建新的执行线程
150
+ */
151
+ async fork(sourceCheckpointId, newThreadId, overrides) {
152
+ const source = await this.loadByCheckpointId(sourceCheckpointId);
153
+ if (!source) {
154
+ throw new Error(`Source checkpoint not found: ${sourceCheckpointId}`);
155
+ }
156
+ const now = new Date().toISOString();
157
+ const { randomUUID } = await import('node:crypto');
158
+ const newData = {
159
+ checkpointId: randomUUID(),
160
+ threadId: newThreadId,
161
+ currentNodeId: source.currentNodeId,
162
+ historyPath: source.historyPath,
163
+ state: overrides ? { ...source.state, ...overrides } : source.state,
164
+ createdBy: 'fork',
165
+ createdAt: now,
166
+ updatedBy: 'fork',
167
+ updatedAt: now,
168
+ isDeleted: false,
169
+ };
170
+ await this.save(newThreadId, newData);
171
+ return newData;
172
+ }
173
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * restoreCheckpoint — 供 05-Trace/Replay 模块使用的便捷函数
3
+ *
4
+ * 薄封装 ICheckpointSaver.loadByCheckpointId,提供明确的语义入口:
5
+ * 通过 checkpointId 恢复特定 Graph 状态,用于 Replay/Fork 场景。
6
+ *
7
+ * @see design.md §1.3 Checkpoint 数据模型
8
+ * @see requirements.md Requirement 2 AC4(按 checkpointId 加载 Replay/Fork)
9
+ */
10
+ import type { ICheckpointSaver } from './contracts/checkpoint-saver.contract.js';
11
+ import type { CheckpointData } from '../graph/contracts/executable-graph.js';
12
+ /**
13
+ * 通过 checkpointId 恢复特定的 Graph 状态快照
14
+ *
15
+ * 供 05-Trace/Replay 模块调用,实现 Replay(重放)和 Fork(分叉)功能。
16
+ * 内部委托给注入的 ICheckpointSaver 实例。
17
+ *
18
+ * @param checkpointId - 目标 Checkpoint 唯一 ID
19
+ * @param saver - ICheckpointSaver 实现实例(通常为 CompositeCheckpointSaver)
20
+ * @returns 对应的 CheckpointData,不存在时返回 null
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import { restoreCheckpoint, CompositeCheckpointSaver } from '@eap/runtime';
25
+ *
26
+ * const checkpoint = await restoreCheckpoint('cp-abc-123', compositeCheckpointSaver);
27
+ * if (checkpoint) {
28
+ * // 使用 checkpoint.state 恢复 Graph 执行
29
+ * // 使用 checkpoint.currentNodeId 确定恢复起点
30
+ * }
31
+ * ```
32
+ */
33
+ export declare function restoreCheckpoint(checkpointId: string, saver: ICheckpointSaver): Promise<CheckpointData | null>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * restoreCheckpoint — 供 05-Trace/Replay 模块使用的便捷函数
3
+ *
4
+ * 薄封装 ICheckpointSaver.loadByCheckpointId,提供明确的语义入口:
5
+ * 通过 checkpointId 恢复特定 Graph 状态,用于 Replay/Fork 场景。
6
+ *
7
+ * @see design.md §1.3 Checkpoint 数据模型
8
+ * @see requirements.md Requirement 2 AC4(按 checkpointId 加载 Replay/Fork)
9
+ */
10
+ /**
11
+ * 通过 checkpointId 恢复特定的 Graph 状态快照
12
+ *
13
+ * 供 05-Trace/Replay 模块调用,实现 Replay(重放)和 Fork(分叉)功能。
14
+ * 内部委托给注入的 ICheckpointSaver 实例。
15
+ *
16
+ * @param checkpointId - 目标 Checkpoint 唯一 ID
17
+ * @param saver - ICheckpointSaver 实现实例(通常为 CompositeCheckpointSaver)
18
+ * @returns 对应的 CheckpointData,不存在时返回 null
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { restoreCheckpoint, CompositeCheckpointSaver } from '@eap/runtime';
23
+ *
24
+ * const checkpoint = await restoreCheckpoint('cp-abc-123', compositeCheckpointSaver);
25
+ * if (checkpoint) {
26
+ * // 使用 checkpoint.state 恢复 Graph 执行
27
+ * // 使用 checkpoint.currentNodeId 确定恢复起点
28
+ * }
29
+ * ```
30
+ */
31
+ export async function restoreCheckpoint(checkpointId, saver) {
32
+ return saver.loadByCheckpointId(checkpointId);
33
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Agent 对外主键解析(迁移期双读)
3
+ *
4
+ * 正式字段为 agentCode;历史 agentId 仅作同义兜底。
5
+ */
6
+ export declare function resolveAgentCode(input: {
7
+ agentCode?: string | null;
8
+ agentId?: string | null;
9
+ fallback?: string;
10
+ }): string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Agent 对外主键解析(迁移期双读)
3
+ *
4
+ * 正式字段为 agentCode;历史 agentId 仅作同义兜底。
5
+ */
6
+ export function resolveAgentCode(input) {
7
+ const code = input.agentCode ?? input.agentId ?? input.fallback;
8
+ if (code == null || code === '') {
9
+ throw new Error('agentCode is required (agentId synonym accepted during migration)');
10
+ }
11
+ return String(code);
12
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 并发控制模块
3
+ *
4
+ * 提供两种并发限制机制:
5
+ * 1. ConcurrencyLimiter — 基于 amqplib channel.prefetch 的 Agent 级并发控制
6
+ * 通过 RabbitMQ consumer prefetch 限制单 Agent 实例同时处理的任务数
7
+ * 2. Semaphore — 纯内存信号量,用于本地并发控制(不依赖 RabbitMQ)
8
+ *
9
+ * @see Requirement 14 AC2: 系统通过消息消费者 prefetch 参数控制单 Agent 实例的并发上限(默认 prefetch=5)
10
+ */
11
+ import type { Channel } from 'amqplib';
12
+ /** ConcurrencyLimiter 默认 prefetch 值 */
13
+ export declare const DEFAULT_PREFETCH = 5;
14
+ /**
15
+ * 基于 amqplib consumer prefetch 的 Agent 级并发限制器
16
+ *
17
+ * RabbitMQ 的 channel.prefetch(n) 表示该 Channel 上的消费者最多同时
18
+ * 持有 n 条未确认消息。超出限制的消息留在 Queue 中排队等待。
19
+ *
20
+ * 整体 QPS = Pod 数 × prefetch。
21
+ */
22
+ export declare class ConcurrencyLimiter {
23
+ private readonly prefetchCount;
24
+ /**
25
+ * @param prefetchCount 单 Agent 实例最大并发任务数,默认 5
26
+ */
27
+ constructor(prefetchCount?: number);
28
+ /**
29
+ * 配置 amqplib Channel 的 prefetch
30
+ *
31
+ * 调用后该 Channel 上的消费者最多同时处理 prefetchCount 条消息。
32
+ * 超出限制的消息在 RabbitMQ Queue 中排队等待。
33
+ *
34
+ * @param channel amqplib Channel 实例
35
+ */
36
+ configure(channel: Channel): Promise<void>;
37
+ /**
38
+ * 获取配置的最大并发数
39
+ */
40
+ getMaxConcurrency(): number;
41
+ }
42
+ /**
43
+ * 内存信号量 — 用于本地并发控制
44
+ *
45
+ * 当无需依赖 RabbitMQ 时(如单进程内限流、测试场景),
46
+ * 使用 Semaphore 控制并发执行数。
47
+ *
48
+ * 用法:
49
+ * ```ts
50
+ * const sem = new Semaphore(3);
51
+ * await sem.acquire(); // 等待可用槽位
52
+ * try {
53
+ * // 执行并发受限的操作
54
+ * } finally {
55
+ * sem.release(); // 释放槽位
56
+ * }
57
+ * ```
58
+ */
59
+ export declare class Semaphore {
60
+ private readonly maxSlots;
61
+ private currentSlots;
62
+ private readonly waitQueue;
63
+ /**
64
+ * @param maxConcurrency 最大并发数
65
+ */
66
+ constructor(maxConcurrency: number);
67
+ /**
68
+ * 获取一个可用槽位。如果当前无可用槽位,则等待直到有释放。
69
+ */
70
+ acquire(): Promise<void>;
71
+ /**
72
+ * 释放一个槽位。如果有等待者,唤醒队列中最先等待的。
73
+ */
74
+ release(): void;
75
+ /**
76
+ * 当前可用槽位数
77
+ */
78
+ get availableSlots(): number;
79
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 并发控制模块
3
+ *
4
+ * 提供两种并发限制机制:
5
+ * 1. ConcurrencyLimiter — 基于 amqplib channel.prefetch 的 Agent 级并发控制
6
+ * 通过 RabbitMQ consumer prefetch 限制单 Agent 实例同时处理的任务数
7
+ * 2. Semaphore — 纯内存信号量,用于本地并发控制(不依赖 RabbitMQ)
8
+ *
9
+ * @see Requirement 14 AC2: 系统通过消息消费者 prefetch 参数控制单 Agent 实例的并发上限(默认 prefetch=5)
10
+ */
11
+ // ─────────────────────────────────────────────
12
+ // ConcurrencyLimiter — RabbitMQ prefetch 并发控制
13
+ // ─────────────────────────────────────────────
14
+ /** ConcurrencyLimiter 默认 prefetch 值 */
15
+ export const DEFAULT_PREFETCH = 5;
16
+ /**
17
+ * 基于 amqplib consumer prefetch 的 Agent 级并发限制器
18
+ *
19
+ * RabbitMQ 的 channel.prefetch(n) 表示该 Channel 上的消费者最多同时
20
+ * 持有 n 条未确认消息。超出限制的消息留在 Queue 中排队等待。
21
+ *
22
+ * 整体 QPS = Pod 数 × prefetch。
23
+ */
24
+ export class ConcurrencyLimiter {
25
+ prefetchCount;
26
+ /**
27
+ * @param prefetchCount 单 Agent 实例最大并发任务数,默认 5
28
+ */
29
+ constructor(prefetchCount = DEFAULT_PREFETCH) {
30
+ if (prefetchCount < 1) {
31
+ throw new Error(`prefetchCount must be >= 1, got ${prefetchCount}`);
32
+ }
33
+ this.prefetchCount = prefetchCount;
34
+ }
35
+ /**
36
+ * 配置 amqplib Channel 的 prefetch
37
+ *
38
+ * 调用后该 Channel 上的消费者最多同时处理 prefetchCount 条消息。
39
+ * 超出限制的消息在 RabbitMQ Queue 中排队等待。
40
+ *
41
+ * @param channel amqplib Channel 实例
42
+ */
43
+ async configure(channel) {
44
+ await channel.prefetch(this.prefetchCount);
45
+ }
46
+ /**
47
+ * 获取配置的最大并发数
48
+ */
49
+ getMaxConcurrency() {
50
+ return this.prefetchCount;
51
+ }
52
+ }
53
+ // ─────────────────────────────────────────────
54
+ // Semaphore — 内存级信号量
55
+ // ─────────────────────────────────────────────
56
+ /**
57
+ * 内存信号量 — 用于本地并发控制
58
+ *
59
+ * 当无需依赖 RabbitMQ 时(如单进程内限流、测试场景),
60
+ * 使用 Semaphore 控制并发执行数。
61
+ *
62
+ * 用法:
63
+ * ```ts
64
+ * const sem = new Semaphore(3);
65
+ * await sem.acquire(); // 等待可用槽位
66
+ * try {
67
+ * // 执行并发受限的操作
68
+ * } finally {
69
+ * sem.release(); // 释放槽位
70
+ * }
71
+ * ```
72
+ */
73
+ export class Semaphore {
74
+ maxSlots;
75
+ currentSlots;
76
+ waitQueue = [];
77
+ /**
78
+ * @param maxConcurrency 最大并发数
79
+ */
80
+ constructor(maxConcurrency) {
81
+ if (maxConcurrency < 1) {
82
+ throw new Error(`maxConcurrency must be >= 1, got ${maxConcurrency}`);
83
+ }
84
+ this.maxSlots = maxConcurrency;
85
+ this.currentSlots = maxConcurrency;
86
+ }
87
+ /**
88
+ * 获取一个可用槽位。如果当前无可用槽位,则等待直到有释放。
89
+ */
90
+ acquire() {
91
+ if (this.currentSlots > 0) {
92
+ this.currentSlots--;
93
+ return Promise.resolve();
94
+ }
95
+ return new Promise((resolve) => {
96
+ this.waitQueue.push(resolve);
97
+ });
98
+ }
99
+ /**
100
+ * 释放一个槽位。如果有等待者,唤醒队列中最先等待的。
101
+ */
102
+ release() {
103
+ if (this.waitQueue.length > 0) {
104
+ const next = this.waitQueue.shift();
105
+ // 使用 microtask 调度,确保调用者的 finally 块先完成
106
+ Promise.resolve().then(next);
107
+ }
108
+ else {
109
+ this.currentSlots++;
110
+ if (this.currentSlots > this.maxSlots) {
111
+ this.currentSlots = this.maxSlots;
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * 当前可用槽位数
117
+ */
118
+ get availableSlots() {
119
+ return this.currentSlots;
120
+ }
121
+ }