cli-swarm 7.0.32 → 7.0.34

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/broker.mjs CHANGED
@@ -25,6 +25,19 @@ const EVALUATION_SCHEMA = 'skill-automatic-evaluation/1.0'
25
25
  const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
26
26
  const REQUEST_SCHEMA_PATTERN = /^([A-Za-z0-9.-]+\.skill)\.request\/([0-9]+\.[0-9]+)$/
27
27
  const TRANSPORT_ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]{1,63}$/
28
+ const NETWORK_TRANSPORT_ERROR = 'NETWORK_TRANSPORT'
29
+ const SKILL_INVOCATION_ERROR = 'SKILL_INVOCATION_FAILED'
30
+
31
+ class OfficialSkillInvocationError extends Error {
32
+ constructor(context, operation, transportCode) {
33
+ super(`${context.displayName} ${operation} invocation failed: network transport ${transportCode}`)
34
+ this.name = 'OfficialSkillInvocationError'
35
+ this.code = NETWORK_TRANSPORT_ERROR
36
+ this.operation = operation
37
+ this.retryable = false
38
+ this.transportCode = transportCode
39
+ }
40
+ }
28
41
 
29
42
  function asObject(value, label) {
30
43
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -237,6 +250,29 @@ export function transportFailureCode(error) {
237
250
  return 'UNKNOWN_TRANSPORT_ERROR'
238
251
  }
239
252
 
253
+ export function officialSkillFailureResponse(error) {
254
+ if (error instanceof OfficialSkillInvocationError) {
255
+ return {
256
+ ok: false,
257
+ error: {
258
+ code: error.code,
259
+ message: error.message,
260
+ operation: error.operation,
261
+ retryable: error.retryable,
262
+ transportCode: error.transportCode,
263
+ },
264
+ }
265
+ }
266
+ return {
267
+ ok: false,
268
+ error: {
269
+ code: SKILL_INVOCATION_ERROR,
270
+ message: error instanceof Error ? error.message : 'Skill invocation failed',
271
+ retryable: false,
272
+ },
273
+ }
274
+ }
275
+
240
276
  export async function invokeOfficialSkill(context, operation, input, dependencies) {
241
277
  const environment = asObject(dependencies.environment, 'broker environment')
242
278
  if (typeof dependencies.request !== 'function') {
@@ -253,9 +289,7 @@ export async function invokeOfficialSkill(context, operation, input, dependencie
253
289
  signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
254
290
  })
255
291
  } catch (error) {
256
- throw new Error(
257
- `${context.displayName} ${operation} invocation failed: network transport ${transportFailureCode(error)}`,
258
- )
292
+ throw new OfficialSkillInvocationError(context, operation, transportFailureCode(error))
259
293
  }
260
294
  const payload = await responsePayload(response, `${context.displayName} ${operation} response`)
261
295
  if (!response.ok || payload.ok !== true) {
package/installer.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  brokerCommandInput,
14
14
  invokeCommandInput,
15
15
  invokeOfficialSkill,
16
+ officialSkillFailureResponse,
16
17
  } from './broker.mjs'
17
18
 
18
19
  export {
@@ -25,6 +26,7 @@ export {
25
26
  callOfficialSkill,
26
27
  invokeCommandInput,
27
28
  invokeOfficialSkill,
29
+ officialSkillFailureResponse,
28
30
  } from './broker.mjs'
29
31
 
30
32
  const INSTALL_META = 'install-meta.json'
@@ -184,11 +186,19 @@ async function readBrokerSource(input) {
184
186
  }
185
187
 
186
188
  async function runBrokerInvocation(context, commandInput) {
187
- const invocation = await invokeOfficialSkill(
188
- context, commandInput.operation, commandInput.input, brokerDependencies(),
189
- )
190
- console.log(JSON.stringify(invocation))
191
- return invocation
189
+ try {
190
+ const invocation = await invokeOfficialSkill(
191
+ context, commandInput.operation, commandInput.input, brokerDependencies(),
192
+ )
193
+ console.log(JSON.stringify(invocation))
194
+ return invocation
195
+ } catch (error) {
196
+ const response = officialSkillFailureResponse(error)
197
+ console.log(JSON.stringify({ response }))
198
+ console.error(response.error.message)
199
+ process.exitCode = 1
200
+ return null
201
+ }
192
202
  }
193
203
 
194
204
  export async function runIntakeHandshake(context, spec) {
package/package.json CHANGED
@@ -17,6 +17,7 @@
17
17
  "skill/skill.json",
18
18
  "swarm-coordinator-fs.mjs",
19
19
  "swarm-coordinator-model.mjs",
20
+ "swarm-coordinator-waits.mjs",
20
21
  "swarm-coordinator.mjs",
21
22
  "swarm-runtime.mjs",
22
23
  "skill/references/org-chart.md",
@@ -33,5 +34,5 @@
33
34
  "url": "https://github.com/88208555/swarm-clitax.git"
34
35
  },
35
36
  "type": "module",
36
- "version": "7.0.32"
37
+ "version": "7.0.34"
37
38
  }
package/skill/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: '通过智能体大脑调度创建 N 个子智能体,用企业级
5
5
 
6
6
  # swarm
7
7
 
8
- Package version: v7.0.32
8
+ Package version: v7.0.34
9
9
 
10
10
  把「项目需求」编排为一支可观测、可自治、可安全运转的智能体蜂群。
11
11
 
@@ -19,15 +19,41 @@
19
19
  - 构建/部署锁:同资源排队;申请前必须完成当前基线握手。
20
20
  - 基线握手:观察哈希必须等于任务卡基线,否则任务转为等待并返回重取清单。
21
21
  - 所有租约 TTL 最长 3600 秒;续期签发新租约,旧租约不再处于活动状态。
22
+ - 锁 TTL 到期由协调扫描处理,锁释放事件可唤醒已声明该事件的依赖方。
23
+ - `queueOnConflict=true` 必须显式提供正整数 `queueTimeoutMs`;它是排队时限,与授锁后的 `ttlSeconds` 独立。请求保存 queueId/deadlineAt;提升时持久保存该队列对应的原始 grant。恢复先执行一次 tick,再只读调用 `lock-queue-status`,输入 queueId/taskId/agentId/chainId;不重复申请锁。身份、资源、路径、当前租约或期限失配均拒绝,旧版已授锁但缺少关联记录也不会推测归属。
24
+ - 队列到期后扫描记录 timed-out、向等待方写入 lock-denied 消息,并生成 need-human/Confirm 请求;宿主需呈现该请求。旧队列不再授锁,人工恢复任务后仍须提交带新明确时限的申请。旧版缺少 deadlineAt 的队列明确拒绝;不借用 eta 或租约 TTL。
25
+ - 排队期间任务终止或原构建/部署基线握手改变,后续扫描将该请求明确标记 rejected。已授锁但释放、续期或到期后,原队列不能被用作新授权。
22
26
 
23
27
  ## 依赖等待
24
28
 
25
- 等待前调用 `dependency-wait`,明确 `waiter`、`waitFor`、结构化 `event`、`expectedWithinMs`、到达/超时策略和 `refetchPaths`。活动等待会阻断同 chain 的 Aimlock 预算读取。
29
+ 等待前调用 `dependency-wait`,明确 `waiter`、`waitFor`、结构化 `event`、`expectedWithinMs`、到达/超时策略和 `refetchPaths`。活动等待及尚未完成的真人裁决必须阻断对应 chain 的受控工作,不能只发提醒后继续执行。
26
30
 
27
- - 事件到达:5 秒目标窗口内路由唤醒包,内容含事件与重取清单。
28
- - 对方失败或被回收:立即以 `dependency-terminated` 唤醒,不等超时。
29
- - 超时:按声明处置;`escalate-need-human` 和连续第二次超时返回 `status=blocked`、高风险 Confirm Protocol 请求与 `nextStep=confirm-protocol`。宿主必须先调用该请求并等待人类答案,不能把请求对象当作已确认。
30
- - 成环:登记时或 `tick` 检出后立即打断全部相关等待,并生成真人裁决请求。
31
- - 未登记等待:`task-status=waiting` 返回 `undeclaredWait=true` 和补登提示。
31
+ 宿主的等待流程:
32
32
 
33
- 等待必须由事件到达、终止通知、超时处置或死锁打断结束,不允许无限期静默挂起。
33
+ 1. 调用 `dependency-wait` 登记具体事件依赖;使用其返回状态判断是否还需要等待。
34
+ 2. 需要继续等待时,宿主显式调用 `wait-for-event`。该本地调用驱动协调扫描,检查事件、任务终止、租约到期、依赖超时与死锁,并返回对应唤醒包或确认请求。
35
+ 3. 收到事件后消费唤醒包和重取清单;同一任务仍有其他活动依赖时,继续保持等待。
36
+ 4. 收到高风险确认请求时,先调用 Confirm Protocol,再由宿主呈现请求、取得真实用户答案并回传裁决。请求对象和唤醒通知本身都不授予执行权限。
37
+ 5. 主动放弃等待时调用 `wait-cancel`,更新指定等待及审计台账;不得仅停止本地等待进程或在聊天中宣布放弃。
38
+
39
+ ### 事件与处置
40
+
41
+ - 事件到达:匹配等待并生成包含事件内容和重取清单的唤醒包;已在台账中的匹配事件应在登记等待时检查。
42
+ - 任务完成:对应完成事件参与依赖路由。
43
+ - 对方失败或被回收:立即产生终止通知;登记依赖时同样检查对方是否已经终止,不能等满超时才发现。
44
+ - 超时:按声明处置;`escalate-need-human` 和连续第二次超时产生高风险 Confirm Protocol 请求。成功收到依赖事件后,连续超时计数应重新开始。
45
+ - 成环:登记时或 `tick` 扫描识别后打断相关等待,生成死锁报告和真人裁决请求;裁决前双方保持阻断。
46
+ - 未登记等待:`task-status=waiting` 提示补登;宿主必须先补全依赖声明,才能进入跨任务挂起。
47
+ - 多项依赖:单个事件到达或单项等待取消,不等于该任务的全部依赖都已解决。
48
+
49
+ ## 宿主接入边界
50
+
51
+ - 公开 `tick` 的 `now` 仅是调用方观察时间,规范化后作为 `observedAt` 返回与审计;`scannedAt`、租约到期、排队期限和新签名租约一律使用协调器真实时钟。历史或未来观察值均不能改变授权时间;生产入口没有虚拟时钟注入。测试等待真实有界期限。
52
+ - `tick` 与 `lock-release` 都在 `decisions` 返回本次新产生的队列超时 Confirm 请求;同时保留台账和消息,后续扫描不重复生成。
53
+ - `tick` 是显式扫描入口;`wait-for-event` 仅在宿主实际调用并维持该进程时驱动扫描。该包不会自行安装常驻服务或全局定时器。
54
+ - 事件唤醒的 5 秒目标依赖活跃扫描和宿主及时消费结果;只写台账、只调用一次 `dependency-wait`,均不能保证事件已投递到其他 IDE 或任务。
55
+ - Confirm Protocol 返回结构化请求和答案协议;系统原生弹窗、跨任务消息投递与实际 callback 执行由宿主接入负责。
56
+ - Aimlock 的读写阻断仅适用于经过其受控入口的操作;不得声称拦截所有 IDE 或操作系统文件读写。
57
+ - 宿主退出、进程中断或结果尚未投递时,恢复后先读取台账并继续协调;不得将“未收到消息”解释为任务已完成。
58
+
59
+ 等待必须经事件到达、终止通知、超时处置、显式取消或死锁裁决退出。保留声明、处置和裁决证据,禁止无限期静默挂起。
package/skill/skill.json CHANGED
@@ -6,5 +6,5 @@
6
6
  "name": "swarm",
7
7
  "schemaVersion": "swarm.skill.request/1.0",
8
8
  "type": "Skill",
9
- "version": "v7.0.32"
9
+ "version": "v7.0.34"
10
10
  }
@@ -17,6 +17,17 @@ const WAIT_EVENT_ACTIONS = new Set(['wake-with-package'])
17
17
  const TASK_STATUSES = new Set(['active', 'waiting', 'completed', 'failed', 'reclaimed'])
18
18
  const SHA256_PATTERN = /^[0-9a-f]{64}$/
19
19
 
20
+ function findTask(state, input) {
21
+ const taskId = identifier(input.taskId, 'taskId')
22
+ const task = state.tasks.find((item) => item.taskId === taskId)
23
+ if (!task) coordinatorError('SWARM_COORD_TASK_NOT_FOUND', `task ${taskId} is not registered`)
24
+ if (task.agentId !== identifier(input.agentId, 'agentId')
25
+ || task.chainId !== identifier(input.chainId, 'chainId')) {
26
+ coordinatorError('SWARM_COORD_TASK_AUTHORITY_DENIED', 'task ownership does not match')
27
+ }
28
+ return task
29
+ }
30
+
20
31
  function requireString(value, label) {
21
32
  if (typeof value !== 'string' || !value.trim()) {
22
33
  coordinatorError('SWARM_COORD_FIELD_REQUIRED', `${label} is required`)
@@ -52,6 +63,7 @@ function normalizeTaskCard(input, now = new Date().toISOString()) {
52
63
  }
53
64
  return {
54
65
  schemaVersion: 'swarm.task-card/1.0',
66
+ supersedesTaskId: Object.hasOwn(input, 'supersedesTaskId') ? identifier(input.supersedesTaskId, 'supersedesTaskId') : null,
55
67
  taskId: identifier(input.taskId, 'taskId'),
56
68
  agentId: identifier(input.agentId, 'agentId'),
57
69
  chainId: identifier(input.chainId, 'chainId'),
@@ -116,7 +128,7 @@ function requirementConflicts(left, right) {
116
128
  function scanTaskConflicts(candidate, tasks) {
117
129
  const conflicts = []
118
130
  for (const task of tasks) {
119
- if (task.taskId === candidate.taskId || task.status === 'completed' || task.status === 'failed') continue
131
+ if (task.taskId === candidate.taskId || ['completed', 'failed', 'reclaimed'].includes(task.status)) continue
120
132
  const scopes = matchingScopes(candidate, task)
121
133
  if (scopes.length) conflicts.push({ type: 'file-range', risk: 'medium', taskId: task.taskId, evidence: scopes })
122
134
  const sameTarget = candidate.deployTarget !== null && candidate.deployTarget === task.deployTarget
@@ -143,6 +155,12 @@ function normalizeLockRequest(input) {
143
155
  if (!Number.isInteger(input.ttlSeconds) || input.ttlSeconds < 1 || input.ttlSeconds > 3_600) {
144
156
  coordinatorError('SWARM_COORD_LOCK_TTL_INVALID', 'ttlSeconds must be 1..3600')
145
157
  }
158
+ const hasQueueTimeout = Object.hasOwn(input, 'queueTimeoutMs')
159
+ if ((input.queueOnConflict || hasQueueTimeout)
160
+ && (!Number.isSafeInteger(input.queueTimeoutMs) || input.queueTimeoutMs < 1
161
+ || !Number.isFinite(new Date(Date.now() + input.queueTimeoutMs).getTime()))) {
162
+ coordinatorError('SWARM_COORD_QUEUE_TIMEOUT_REQUIRED', 'queueOnConflict requires an explicit positive queueTimeoutMs')
163
+ }
146
164
  const paths = lockType === 'file'
147
165
  ? requireStringArray(input.paths, 'paths', { nonEmpty: true }).map((path) => relativePath(path))
148
166
  : []
@@ -160,6 +178,7 @@ function normalizeLockRequest(input) {
160
178
  paths,
161
179
  ttlSeconds: input.ttlSeconds,
162
180
  queueOnConflict: input.queueOnConflict,
181
+ ...(hasQueueTimeout ? { queueTimeoutMs: input.queueTimeoutMs } : {}),
163
182
  baselineHandshakeId: input.baselineHandshakeId,
164
183
  }
165
184
  }
@@ -173,7 +192,8 @@ function locksConflict(request, lock) {
173
192
  }
174
193
 
175
194
  function normalizeWait(input, now = new Date().toISOString()) {
176
- if (!Number.isInteger(input.expectedWithinMs) || input.expectedWithinMs < 1) {
195
+ if (!Number.isSafeInteger(input.expectedWithinMs) || input.expectedWithinMs < 1
196
+ || !Number.isFinite(new Date(Date.parse(now) + input.expectedWithinMs).getTime())) {
177
197
  coordinatorError('SWARM_COORD_WAIT_DURATION_INVALID', 'expectedWithinMs must be a positive integer')
178
198
  }
179
199
  if (!WAIT_EVENT_ACTIONS.has(input.onEvent) || !WAIT_TIMEOUT_ACTIONS.has(input.onTimeout)) {
@@ -258,6 +278,7 @@ function validateInputShape(input, schema) {
258
278
  }
259
279
 
260
280
  export {
281
+ findTask,
261
282
  coordinationMessage,
262
283
  detectWaitCycles,
263
284
  locksConflict,
@@ -0,0 +1,285 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { coordinatorError, identifier, readCoordinationState, withCoordinationState } from './swarm-coordinator-fs.mjs'
3
+ import { coordinationMessage, detectWaitCycles, findTask, normalizeWait, requireString, requireTaskStatus } from './swarm-coordinator-model.mjs'
4
+
5
+ const LOCAL_SCHEMA = 'swarm.coordinator-local/1.0'
6
+ const WAIT_POLL_MS = 250
7
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'reclaimed'])
8
+
9
+ function pendingDecision(state, task) {
10
+ return state.decisions.some((decision) => decision.status === 'pending' && decision.agents.includes(task.agentId))
11
+ }
12
+
13
+ function hasActiveWait(state, task) {
14
+ return state.waits.some((wait) => wait.taskId === task.taskId && wait.status === 'active')
15
+ }
16
+
17
+ function assertTaskRunnable(state, task) {
18
+ if (task.status !== 'active' || hasActiveWait(state, task) || pendingDecision(state, task)) {
19
+ coordinatorError('SWARM_COORD_TASK_BLOCKED', 'task must resolve its dependencies and human decisions before execution')
20
+ }
21
+ }
22
+
23
+ function addMessage(state, type, from, to, payload, now) {
24
+ const message = coordinationMessage(type, from, to, payload, now)
25
+ state.messages.push(message)
26
+ return message
27
+ }
28
+
29
+ function eventRecord(publisher, event, payload, now) {
30
+ return { schemaVersion: 'swarm.coord-event/1.0', eventId: 'event-' + randomUUID(), publisher, event, payload, publishedAt: now }
31
+ }
32
+
33
+ function wakeWait(state, wait, resolution, event, now) {
34
+ wait.status = resolution
35
+ wait.resolvedAt = now
36
+ wait.resolution = event
37
+ const task = state.tasks.find((item) => item.taskId === wait.taskId)
38
+ if (task && task.status === 'waiting' && !hasActiveWait(state, task) && !pendingDecision(state, task)) {
39
+ task.status = 'active'
40
+ task.updatedAt = now
41
+ }
42
+ if (resolution === 'event-received') {
43
+ const entry = state.timeoutCounts.find((item) => item.waiter === wait.waiter)
44
+ if (entry) entry.count = 0
45
+ }
46
+ const wakePackage = { schemaVersion: 'swarm.wake-package/1.0', waitId: wait.waitId, reason: resolution,
47
+ event, refetchPaths: wait.refetchPaths, createdAt: now }
48
+ addMessage(state, 'dependency-wait', 'coordinator', wait.waiter, wakePackage, now)
49
+ return wakePackage
50
+ }
51
+
52
+ function routeEvent(state, event) {
53
+ return state.waits.filter((wait) => wait.status === 'active' && wait.waitFor === event.publisher && wait.event === event.event)
54
+ .map((wait) => wakeWait(state, wait, 'event-received', event, event.publishedAt))
55
+ }
56
+
57
+ function confirmationRequest(decision) {
58
+ const options = decision.agents.map((agentId) => ({ id: 'resume:' + agentId, label: '先恢复 ' + agentId, hint: '唤醒该智能体先解除依赖' }))
59
+ options.push({ id: 'abort', label: '终止等待', hint: '终止相关等待并保持任务阻塞' })
60
+ return { schemaVersion: 'confirm-protocol.skill.request/1.0', requestId: 'confirm-' + decision.decisionId,
61
+ operation: 'interaction-request', input: { interaction: {
62
+ schemaVersion: 'confirm.interaction/1.0', requestId: decision.decisionId, type: 'choice', question: decision.question,
63
+ options, default: null, timeout: null, timeoutAction: 'wait', risk: 'high', riskDescription: decision.riskDescription,
64
+ rememberable: false, memoryKey: '', callback: { operation: 'resolve-human', payload: { decisionId: decision.decisionId } },
65
+ } } }
66
+ }
67
+
68
+ function createDecision(state, kind, agents, waitIds, question, riskDescription, now) {
69
+ const decision = { schemaVersion: 'swarm.coord-decision/1.0', decisionId: 'decision-' + randomUUID(),
70
+ kind, agents: [...new Set(agents)], waitIds: [...new Set(waitIds)], question, riskDescription,
71
+ status: 'pending', answer: null, actorId: null, createdAt: now, resolvedAt: null }
72
+ state.decisions.push(decision)
73
+ for (const task of state.tasks.filter((item) => agents.includes(item.agentId) && !TERMINAL_STATUSES.has(item.status))) {
74
+ task.status = 'blocked'
75
+ task.blockedReason = 'human-decision'
76
+ task.updatedAt = now
77
+ }
78
+ const waits = state.waits.filter((wait) => waitIds.includes(wait.waitId))
79
+ .map(({ waitId, waiter, waitFor, event, purpose, deadlineAt }) => ({ waitId, waiter, waitFor, event, purpose, deadlineAt }))
80
+ addMessage(state, 'need-human', 'coordinator', 'human', { decisionId: decision.decisionId, kind, agents, waits }, now)
81
+ return { decision, status: 'blocked', confirmationRequired: true, confirmProtocolRequest: confirmationRequest(decision),
82
+ nextStep: { operation: 'confirm-protocol', instruction: 'Invoke Confirm Protocol and wait for the human answer.' } }
83
+ }
84
+
85
+ function applyCycles(state, now) {
86
+ const requests = []
87
+ for (const cycle of detectWaitCycles(state.waits)) {
88
+ const agents = cycle.slice(0, -1)
89
+ const waits = state.waits.filter((wait) => wait.status === 'active' && agents.includes(wait.waiter) && agents.includes(wait.waitFor))
90
+ if (!waits.length) continue
91
+ requests.push(createDecision(state, 'dependency-cycle', agents, waits.map((wait) => wait.waitId),
92
+ '检测到依赖等待成环:' + agents.join(' → ') + '。请选择先恢复的智能体。',
93
+ '依赖成环会导致全部相关任务无限等待,必须由真人决定执行顺序。', now))
94
+ const dependencies = waits.map(({ waiter, waitFor, event, purpose }) => ({ waiter, waitFor, event, purpose }))
95
+ for (const wait of waits) wakeWait(state, wait, 'deadlock-interrupted', { cycle, dependencies }, now)
96
+ }
97
+ return requests
98
+ }
99
+
100
+ function replayDependency(state, task, wait) {
101
+ const consumed = new Set(state.waits.filter((item) => item.taskId === task.taskId && item.resolution?.eventId)
102
+ .map((item) => item.resolution.eventId))
103
+ const event = state.events.findLast((item) => item.publisher === wait.waitFor && item.event === wait.event
104
+ && Date.parse(item.publishedAt) >= Date.parse(task.registeredAt) && !consumed.has(item.eventId))
105
+ if (event) return [wakeWait(state, wait, 'event-received', event, wait.startedAt)]
106
+ const targets = state.tasks.filter((item) => item.agentId === wait.waitFor)
107
+ if (targets.every((item) => TERMINAL_STATUSES.has(item.status))) {
108
+ return [wakeWait(state, wait, 'dependency-terminated', {
109
+ event: 'task-terminated', publisher: wait.waitFor, payload: { tasks: targets.map(({ taskId, status }) => ({ taskId, status })) },
110
+ publishedAt: wait.startedAt,
111
+ }, wait.startedAt)]
112
+ }
113
+ return []
114
+ }
115
+
116
+ async function dependencyWait(repositoryRoot, input) {
117
+ return withCoordinationState(repositoryRoot, async (state) => {
118
+ const task = findTask(state, { taskId: input.taskId, agentId: input.waiter, chainId: input.chainId })
119
+ if (TERMINAL_STATUSES.has(task.status) || pendingDecision(state, task)) {
120
+ coordinatorError('SWARM_COORD_TASK_BLOCKED', 'terminated tasks and pending human decisions cannot declare new dependencies')
121
+ }
122
+ const wait = normalizeWait(input)
123
+ if (!state.tasks.some((item) => item.agentId === wait.waitFor)) {
124
+ coordinatorError('SWARM_COORD_WAIT_TARGET_UNKNOWN', 'waitFor must identify a registered agent')
125
+ }
126
+ task.status = 'waiting'
127
+ task.updatedAt = wait.startedAt
128
+ state.waits.push(wait)
129
+ addMessage(state, 'dependency-wait', wait.waiter, wait.waitFor, {
130
+ waitId: wait.waitId, event: wait.event, deadlineAt: wait.deadlineAt, purpose: wait.purpose,
131
+ }, wait.startedAt)
132
+ const wakePackages = replayDependency(state, task, wait)
133
+ const decisions = applyCycles(state, wait.startedAt)
134
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, wait, suspended: wait.status === 'active', decisions, wakePackages,
135
+ nextStep: wait.status === 'active' ? { operation: 'wait-for-event', input: { taskId: task.taskId,
136
+ agentId: task.agentId, chainId: task.chainId, waitId: wait.waitId } } : null },
137
+ audit: [{ event: 'dependency-wait', waitId: wait.waitId, waiter: wait.waiter, waitFor: wait.waitFor }] }
138
+ })
139
+ }
140
+
141
+ async function publishEvent(repositoryRoot, input) {
142
+ return withCoordinationState(repositoryRoot, async (state) => {
143
+ const publisher = identifier(input.publisher, 'publisher')
144
+ if (!state.tasks.some((task) => task.agentId === publisher)) coordinatorError('SWARM_COORD_WAIT_TARGET_UNKNOWN', 'publisher must be registered')
145
+ const eventName = identifier(input.event, 'event')
146
+ if (!input.payload || typeof input.payload !== 'object' || Array.isArray(input.payload)) {
147
+ coordinatorError('SWARM_COORD_EVENT_PAYLOAD_INVALID', 'payload must be an object')
148
+ }
149
+ const event = eventRecord(publisher, eventName, input.payload, new Date().toISOString())
150
+ state.events.push(event)
151
+ const wakePackages = routeEvent(state, event)
152
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, event, wakePackages },
153
+ audit: [{ event: 'event-published', eventId: event.eventId, publisher, eventName }] }
154
+ })
155
+ }
156
+
157
+ function notifyDeadTask(state, task, now) {
158
+ const others = state.tasks.some((item) => item.agentId === task.agentId && !TERMINAL_STATUSES.has(item.status))
159
+ if (others) return []
160
+ return state.waits.filter((wait) => wait.status === 'active' && wait.waitFor === task.agentId)
161
+ .map((wait) => wakeWait(state, wait, 'dependency-terminated', {
162
+ event: 'task-terminated', publisher: task.agentId, payload: { taskId: task.taskId, status: task.status }, publishedAt: now,
163
+ }, now))
164
+ }
165
+
166
+ async function taskStatus(repositoryRoot, input) {
167
+ return withCoordinationState(repositoryRoot, async (state) => {
168
+ const task = findTask(state, input)
169
+ const status = requireTaskStatus(input.status)
170
+ if (TERMINAL_STATUSES.has(task.status) && task.status !== status) {
171
+ coordinatorError('SWARM_COORD_TASK_TERMINATED', 'terminal task cannot be restarted; register a new task')
172
+ }
173
+ if (status === 'active' && (pendingDecision(state, task) || hasActiveWait(state, task) || task.status === 'blocked')) {
174
+ coordinatorError('SWARM_COORD_TASK_BLOCKED', 'resolve dependencies or the human decision before activation')
175
+ }
176
+ const now = new Date().toISOString()
177
+ const undeclaredWait = status === 'waiting' && !hasActiveWait(state, task)
178
+ task.status = undeclaredWait ? 'blocked' : status
179
+ if (undeclaredWait) task.blockedReason = 'undeclared-wait'
180
+ task.updatedAt = now
181
+ const declarationMessage = undeclaredWait
182
+ ? addMessage(state, 'dependency-wait', 'coordinator', task.agentId, { status: 'declaration-required', taskId: task.taskId }, now) : null
183
+ const wakePackages = []
184
+ if (TERMINAL_STATUSES.has(status)) {
185
+ for (const wait of state.waits.filter((item) => item.taskId === task.taskId && item.status === 'active')) {
186
+ wakePackages.push(wakeWait(state, wait, 'waiter-terminated', { taskId: task.taskId, status }, now))
187
+ }
188
+ if (status === 'completed') {
189
+ const event = eventRecord(task.agentId, 'task-completed', { taskId: task.taskId, status }, now)
190
+ state.events.push(event)
191
+ wakePackages.push(...routeEvent(state, event))
192
+ }
193
+ wakePackages.push(...notifyDeadTask(state, task, now))
194
+ }
195
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, task, undeclaredWait, declarationMessage, wakePackages },
196
+ audit: [{ event: 'task-status', taskId: task.taskId, status: task.status }] }
197
+ })
198
+ }
199
+
200
+ function updateTimeoutCount(state, waiter) {
201
+ let entry = state.timeoutCounts.find((item) => item.waiter === waiter)
202
+ if (!entry) { entry = { waiter, count: 0 }; state.timeoutCounts.push(entry) }
203
+ entry.count += 1
204
+ return entry.count
205
+ }
206
+
207
+ function applyTimeouts(state, now) {
208
+ const timedOut = state.waits.filter((wait) => wait.status === 'active' && Date.parse(wait.deadlineAt) <= Date.parse(now))
209
+ const decisions = [], wakePackages = []
210
+ for (const wait of timedOut) {
211
+ const count = updateTimeoutCount(state, wait.waiter)
212
+ if (wait.onTimeout === 'escalate-need-human' || count >= 2) {
213
+ decisions.push(createDecision(state, 'dependency-timeout', [wait.waiter, wait.waitFor], [wait.waitId],
214
+ wait.waiter + ' 等待 ' + wait.waitFor + ' 的 ' + wait.event + ' 已超时,请选择后续动作。',
215
+ '依赖事件未在声明期限内到达,继续静默等待可能导致任务停滞。', now))
216
+ wakePackages.push(wakeWait(state, wait, 'timeout-interrupted', { timeoutCount: count }, now))
217
+ } else {
218
+ const resolution = wait.onTimeout === 'abandon-wait' ? 'timeout-abandoned' : 'timeout-continued'
219
+ wakePackages.push(wakeWait(state, wait, resolution, { timeoutCount: count }, now))
220
+ }
221
+ }
222
+ return { timedOut, decisions, wakePackages }
223
+ }
224
+
225
+ async function resolveHuman(repositoryRoot, input) {
226
+ return withCoordinationState(repositoryRoot, async (state) => {
227
+ const decisionId = identifier(input.decisionId, 'decisionId')
228
+ const decision = state.decisions.find((item) => item.decisionId === decisionId)
229
+ if (!decision || decision.status !== 'pending') coordinatorError('SWARM_COORD_DECISION_NOT_PENDING', 'decision is not pending')
230
+ const answer = requireString(input.answer, 'answer')
231
+ if (![...decision.agents.map((agent) => 'resume:' + agent), 'abort'].includes(answer)) {
232
+ coordinatorError('SWARM_COORD_DECISION_ANSWER_INVALID', 'answer is not a declared option')
233
+ }
234
+ const now = new Date().toISOString()
235
+ decision.status = 'resolved'
236
+ decision.answer = answer
237
+ decision.actorId = identifier(input.actorId, 'actorId')
238
+ decision.resolvedAt = now
239
+ for (const task of state.tasks.filter((item) => answer === 'resume:' + item.agentId && !TERMINAL_STATUSES.has(item.status))) {
240
+ if (!pendingDecision(state, task)) {
241
+ task.status = hasActiveWait(state, task) ? 'waiting' : 'active'
242
+ task.blockedReason = null
243
+ task.updatedAt = now
244
+ }
245
+ }
246
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, decision },
247
+ audit: [{ event: 'decision-resolved', decisionId, answer, actorId: decision.actorId }] }
248
+ })
249
+ }
250
+
251
+ async function cancelWait(repositoryRoot, input) {
252
+ return withCoordinationState(repositoryRoot, async (state) => {
253
+ const task = findTask(state, input)
254
+ const wait = state.waits.find((item) => item.waitId === identifier(input.waitId, 'waitId') && item.taskId === task.taskId)
255
+ if (!wait || wait.status !== 'active') coordinatorError('SWARM_COORD_WAIT_NOT_ACTIVE', 'wait is not active for this task')
256
+ const reason = requireString(input.reason, 'reason')
257
+ const wakePackage = wakeWait(state, wait, 'cancelled', { reason, actorId: task.agentId }, new Date().toISOString())
258
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, wait, wakePackage }, audit: [{ event: 'wait-cancelled', waitId: wait.waitId, reason }] }
259
+ })
260
+ }
261
+
262
+ async function waitForEvent(repositoryRoot, input, tick) {
263
+ const waitId = identifier(input.waitId, 'waitId')
264
+ while (true) {
265
+ const before = await readCoordinationState(repositoryRoot)
266
+ const task = findTask(before, input)
267
+ const wait = before.waits.find((item) => item.waitId === waitId && item.taskId === task.taskId)
268
+ if (!wait) coordinatorError('SWARM_COORD_WAIT_NOT_FOUND', 'wait is not registered for this task')
269
+ const pending = before.decisions.filter((item) => item.status === 'pending' && item.agents.includes(task.agentId))
270
+ if (wait.status === 'active' && pending.length) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked',
271
+ wait, wakePackage: null, decisions: pending, confirmProtocolRequests: pending.map(confirmationRequest) }
272
+ if (wait.status !== 'active') {
273
+ const message = before.messages.findLast((item) => item.payload.waitId === waitId && item.payload.schemaVersion === 'swarm.wake-package/1.0')
274
+ if (!message) coordinatorError('SWARM_COORD_WAKE_PACKAGE_MISSING', 'resolved wait has no wake package')
275
+ const decisions = before.decisions.filter((item) => item.status === 'pending' && item.agents.includes(task.agentId))
276
+ return { schemaVersion: LOCAL_SCHEMA, status: decisions.length || task.status !== 'active' ? 'blocked' : 'resolved',
277
+ wait, wakePackage: message.payload, decisions, confirmProtocolRequests: decisions.map(confirmationRequest) }
278
+ }
279
+ await tick(repositoryRoot, { now: new Date().toISOString() })
280
+ await new Promise((accept) => setTimeout(accept, Math.min(WAIT_POLL_MS, Math.max(1, Date.parse(wait.deadlineAt) - Date.now()))))
281
+ }
282
+ }
283
+
284
+ export { addMessage, eventRecord, routeEvent, createDecision, applyCycles, applyTimeouts, assertTaskRunnable,
285
+ pendingDecision, hasActiveWait, dependencyWait, publishEvent, taskStatus, resolveHuman, cancelWait, waitForEvent }