agentworkshop 0.7.37 → 0.7.39

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 (91) hide show
  1. package/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
  2. package/.AgentWorkShop/prompts/aml-playbook.md +49 -49
  3. package/.AgentWorkShop/prompts/host-tools.json +1238 -1238
  4. package/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
  5. package/.output/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
  6. package/.output/.AgentWorkShop/prompts/aml-playbook.md +49 -49
  7. package/.output/.AgentWorkShop/prompts/host-tools.json +1238 -1238
  8. package/.output/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
  9. package/.output/nitro.json +1 -1
  10. package/.output/public/_nuxt/{BG8sCcwx.js → CUDwzgLV.js} +4 -4
  11. package/.output/public/_nuxt/CmsOtOAA2.js +1 -0
  12. package/.output/public/_nuxt/builds/latest.json +1 -1
  13. package/.output/public/_nuxt/builds/meta/4457d63f-0866-4cea-b320-04a321f24795.json +1 -0
  14. package/.output/public/_nuxt/default.BXI_fZUi.css +1 -0
  15. package/.output/server/chunks/_/nitro.mjs +703 -704
  16. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  17. package/.output/server/chunks/build/{default-hseyOqBT.mjs → default-CZmZFpqH.mjs} +27 -42
  18. package/.output/server/chunks/build/{default-hseyOqBT.mjs.map → default-CZmZFpqH.mjs.map} +1 -1
  19. package/.output/server/chunks/routes/renderer.mjs +1 -1
  20. package/.output/server/chunks/virtual/entry.mjs +3 -1
  21. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  22. package/.output/server/package.json +1 -1
  23. package/README-zh.md +6 -6
  24. package/README.md +6 -6
  25. package/app/components/AppHeader.vue +0 -17
  26. package/app/components/AppSidebar.vue +1 -0
  27. package/docs/cli.en.md +2 -2
  28. package/docs/cli.md +2 -2
  29. package/docs/full-test-plan.md +5 -1
  30. package/docs/plugins.en.md +2 -1
  31. package/docs/plugins.md +2 -1
  32. package/docs/sdk.en.md +1 -1
  33. package/docs/sdk.md +1 -1
  34. package/i18n/locales/en.ts +1 -0
  35. package/i18n/locales/zh-CN.ts +1 -0
  36. package/package.json +1 -1
  37. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +1 -0
  38. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +1 -0
  39. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +153 -0
  40. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +153 -0
  41. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +431 -0
  42. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +180 -0
  43. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +112 -0
  44. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +227 -0
  45. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +131 -0
  46. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +160 -0
  47. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +157 -0
  48. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +348 -0
  49. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +296 -0
  50. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +285 -0
  51. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +217 -0
  52. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +301 -0
  53. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +151 -0
  54. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +135 -0
  55. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +156 -0
  56. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +72 -0
  57. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +469 -0
  58. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +259 -0
  59. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +97 -0
  60. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +277 -0
  61. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +171 -0
  62. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +425 -0
  63. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +99 -0
  64. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +325 -0
  65. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +332 -0
  66. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +43 -0
  67. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +154 -0
  68. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +531 -0
  69. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +439 -0
  70. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +323 -0
  71. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +433 -0
  72. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +280 -0
  73. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +117 -0
  74. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +160 -0
  75. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +101 -0
  76. package/scripts/_audit/detached-start.mjs +103 -0
  77. package/scripts/_audit/guard-upstream-liveness.mjs +104 -0
  78. package/scripts/_dbg-perms-e2e.mjs +10 -3
  79. package/scripts/_dbg-protocol-matrix.mjs +453 -441
  80. package/scripts/_dbg-render-regression.mjs +151 -130
  81. package/scripts/_dbg-town-agents-probe.mjs +60 -0
  82. package/scripts/_dbg-visual-acceptance.mjs +63 -0
  83. package/scripts/e2e-agent-team.mjs +5 -3
  84. package/scripts/test-plugin-hardening.mjs +32 -0
  85. package/scripts/three-system-e2e.mjs +6 -6
  86. package/server/plugins/dev-stability-guard.ts +40 -63
  87. package/server/plugins-builtin/rag-bridge/index.mjs +9 -2
  88. package/server/utils/stability-severity.ts +66 -0
  89. package/.output/public/_nuxt/DVsjtsvZ2.js +0 -1
  90. package/.output/public/_nuxt/builds/meta/a66fdef7-dcfd-453b-b1f6-543c7dfd98d5.json +0 -1
  91. package/.output/public/_nuxt/default.DE_vT238.css +0 -1
@@ -0,0 +1,433 @@
1
+ /**
2
+ * TaskEngine 测试(node + tsx 直跑,无浏览器)
3
+ *
4
+ * 覆盖:
5
+ * 1. create/dispatch 父子关系(assignee/creator/state/parent)
6
+ * 2. dispatch 投递 assign 消息(parts + metadata 断言)
7
+ * 3. 状态机全合法迁移
8
+ * 4. 非法迁移(COMPLETED→WORKING、终态再迁等)抛 INVALID_TRANSITION
9
+ * 5. applyEvent artifact 分块 append + progress 折算 / totalChunks 折算
10
+ * 6. complete 置终态 + 进度 100
11
+ * 7. reassign retryCount+1 + 投递 assign 消息
12
+ * 8. cancel 置 CANCELED + 投递 cancel 消息
13
+ * 9. onChildCompleted 最后一个子任务完成 → 父恢复 WORKING + child-completed 消息
14
+ */
15
+ import { openWorkshopDb } from '../server/services/workshop/db/database'
16
+ import { createChannelRepo } from '../server/services/workshop/db/channel.repo'
17
+ import { createAgentRepo } from '../server/services/workshop/db/agent.repo'
18
+ import { createTaskRepo } from '../server/services/workshop/db/task.repo'
19
+ import { createMessageRepo } from '../server/services/workshop/db/message.repo'
20
+ import { TaskEngine } from '../server/services/workshop/runtime/task-engine'
21
+ import type { Part, A2AArtifact } from '../server/services/workshop/types/a2a'
22
+ import type { WorkspaceTask } from '../server/services/workshop/types/task'
23
+ import { AppError } from '../server/utils/errors'
24
+
25
+ let failures = 0
26
+ function check(name: string, ok: boolean, detail = ''): void {
27
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
28
+ if (!ok) failures += 1
29
+ }
30
+
31
+ /** 全新内存库 + engine + 一个 channel + lead/worker 两个 agent */
32
+ function setup() {
33
+ const db = openWorkshopDb(':memory:')
34
+ const channels = createChannelRepo(db)
35
+ const agents = createAgentRepo(db)
36
+ const tasks = createTaskRepo(db)
37
+ const messages = createMessageRepo(db)
38
+ const engine = new TaskEngine({ tasks, messages })
39
+ const channel = channels.create({ name: 'test-channel' })
40
+ const lead = agents.create({ name: 'lead', harness: 'mock' })
41
+ const worker = agents.create({ name: 'worker', harness: 'mock' })
42
+ return { db, engine, messages, channel, lead, worker, agents }
43
+ }
44
+
45
+ /** 拉取某 agent 未消费消息并解析 parts/metadata */
46
+ function pendingMessages(messages: ReturnType<typeof createMessageRepo>, channelId: string, agentId: string) {
47
+ return messages.listPendingByChannelAgent(channelId, agentId).map(row => ({
48
+ row,
49
+ parts: JSON.parse(row.partsJson) as Part[],
50
+ metadata: JSON.parse(row.metadataJson) as Record<string, unknown>,
51
+ }))
52
+ }
53
+
54
+ /** 断言抛 INVALID_TRANSITION(400) */
55
+ function expectInvalidTransition(name: string, fn: () => unknown): void {
56
+ try {
57
+ fn()
58
+ check(name, false, '未抛出异常')
59
+ }
60
+ catch (e) {
61
+ const err = e as AppError
62
+ check(
63
+ name,
64
+ err instanceof AppError && err.code === 'INVALID_TRANSITION' && err.status === 400,
65
+ `code=${err?.code} status=${err?.status}`,
66
+ )
67
+ }
68
+ }
69
+
70
+ function testCreateDispatch(): void {
71
+ const { engine, lead, worker, channel } = setup()
72
+ const parent = engine.create({
73
+ channelId: channel.id,
74
+ creatorId: lead.id,
75
+ assigneeId: lead.id,
76
+ title: '主任务',
77
+ description: '统筹交付',
78
+ })
79
+ check('create 初始 SUBMITTED', parent.state === 'SUBMITTED', `state=${parent.state}`)
80
+ check('create 落库可查', engine.get(parent.id)?.id === parent.id)
81
+
82
+ const working = engine.transition(parent.id, 'WORKING', lead.id)
83
+ check('create 后 WORKING 迁移', working.state === 'WORKING')
84
+
85
+ const child = engine.dispatch(parent, {
86
+ assigneeId: worker.id,
87
+ title: '子任务1',
88
+ description: '实现功能 A',
89
+ })
90
+ check('dispatch 子任务 ASSIGNED', child.state === 'ASSIGNED', `state=${child.state}`)
91
+ check('dispatch 子任务 parentId', child.parentId === parent.id)
92
+ check('dispatch 子任务 assignee', child.assigneeId === worker.id)
93
+ check('dispatch 子任务 creator=父 assignee', child.creatorId === parent.assigneeId)
94
+ check('dispatch 后父转 WAITING', engine.get(parent.id)?.state === 'WAITING')
95
+ }
96
+
97
+ function testDispatchAssignMessage(): void {
98
+ const { engine, lead, worker, messages, channel } = setup()
99
+ const parent = engine.create({
100
+ channelId: channel.id,
101
+ creatorId: lead.id,
102
+ assigneeId: lead.id,
103
+ title: '主任务',
104
+ })
105
+ engine.transition(parent.id, 'WORKING', lead.id)
106
+ const child = engine.dispatch(parent, {
107
+ assigneeId: worker.id,
108
+ title: '子任务1',
109
+ description: '实现功能 A',
110
+ })
111
+
112
+ const msgs = pendingMessages(messages, channel.id, worker.id)
113
+ check('dispatch 投递 1 条 assign 消息', msgs.length === 1, `count=${msgs.length}`)
114
+ const m = msgs[0]
115
+ check('assign 消息 title part', m.parts[0]?.text === '子任务1', JSON.stringify(m.parts))
116
+ check('assign 消息 description part', m.parts[1]?.text === '实现功能 A')
117
+ check('assign 消息 kind', m.metadata['x-aw-task-kind'] === 'assign')
118
+ check('assign 消息 task-id = 子任务 id', m.metadata['x-aw-task-id'] === child.id)
119
+ check('assign 消息投递对象', m.row.toAgentId === worker.id)
120
+ }
121
+
122
+ function testAllLegalTransitions(): void {
123
+ const { engine, lead, worker, channel } = setup()
124
+ // 每个迁移用独立任务,避免状态污染
125
+ const cases: Array<{ from: WorkspaceTask['state'], to: WorkspaceTask['state'] }> = [
126
+ { from: 'SUBMITTED', to: 'WORKING' },
127
+ { from: 'SUBMITTED', to: 'ASSIGNED' },
128
+ { from: 'SUBMITTED', to: 'CANCELED' },
129
+ { from: 'ASSIGNED', to: 'WORKING' },
130
+ { from: 'ASSIGNED', to: 'CANCELED' },
131
+ { from: 'WORKING', to: 'WAITING' },
132
+ { from: 'WORKING', to: 'COMPLETED' },
133
+ { from: 'WORKING', to: 'FAILED' },
134
+ { from: 'WORKING', to: 'CANCELED' },
135
+ { from: 'WAITING', to: 'WORKING' },
136
+ { from: 'WAITING', to: 'CANCELED' },
137
+ { from: 'FAILED', to: 'ASSIGNED' },
138
+ ]
139
+ for (const { from, to } of cases) {
140
+ const t = engine.create({
141
+ channelId: channel.id,
142
+ creatorId: lead.id,
143
+ assigneeId: worker.id,
144
+ title: `t-${from}-${to}`,
145
+ })
146
+ if (from === 'ASSIGNED') {
147
+ engine.transition(t.id, 'ASSIGNED', lead.id)
148
+ }
149
+ else if (from === 'WORKING') {
150
+ engine.transition(t.id, 'WORKING', worker.id)
151
+ }
152
+ else if (from === 'WAITING') {
153
+ engine.transition(t.id, 'WORKING', worker.id)
154
+ engine.transition(t.id, 'WAITING', lead.id)
155
+ }
156
+ else if (from === 'FAILED') {
157
+ engine.transition(t.id, 'WORKING', worker.id)
158
+ engine.transition(t.id, 'FAILED', worker.id)
159
+ }
160
+ const r = engine.transition(t.id, to, lead.id)
161
+ check(`合法迁移 ${from}→${to}`, r.state === to, `got=${r.state}`)
162
+ }
163
+ }
164
+
165
+ function testIllegalTransitions(): void {
166
+ const { engine, lead, worker, channel } = setup()
167
+ // COMPLETED → WORKING
168
+ {
169
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: 'x' })
170
+ engine.transition(t.id, 'WORKING', worker.id)
171
+ engine.transition(t.id, 'COMPLETED', worker.id)
172
+ expectInvalidTransition('COMPLETED→WORKING 拒绝', () => engine.transition(t.id, 'WORKING', lead.id))
173
+ }
174
+ // WORKING→COMPLETED 后再次 WORKING(终态不可迁)
175
+ {
176
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: 'y' })
177
+ engine.transition(t.id, 'WORKING', worker.id)
178
+ engine.transition(t.id, 'COMPLETED', worker.id)
179
+ expectInvalidTransition('COMPLETED(终态)→WORKING 拒绝', () => engine.transition(t.id, 'WORKING', lead.id))
180
+ }
181
+ // 终态 COMPLETED → FAILED / CANCELED
182
+ {
183
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: 'z' })
184
+ engine.transition(t.id, 'WORKING', worker.id)
185
+ engine.transition(t.id, 'COMPLETED', worker.id)
186
+ expectInvalidTransition('COMPLETED→FAILED 拒绝', () => engine.transition(t.id, 'FAILED', lead.id))
187
+ expectInvalidTransition('COMPLETED→CANCELED 拒绝', () => engine.transition(t.id, 'CANCELED', lead.id))
188
+ }
189
+ // 非法: SUBMITTED → WAITING / FAILED
190
+ {
191
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: 'w' })
192
+ expectInvalidTransition('SUBMITTED→WAITING 拒绝', () => engine.transition(t.id, 'WAITING', lead.id))
193
+ expectInvalidTransition('SUBMITTED→FAILED 拒绝', () => engine.transition(t.id, 'FAILED', lead.id))
194
+ }
195
+ // 非法: WORKING → ASSIGNED
196
+ {
197
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: 'v' })
198
+ engine.transition(t.id, 'WORKING', worker.id)
199
+ expectInvalidTransition('WORKING→ASSIGNED 拒绝', () => engine.transition(t.id, 'ASSIGNED', lead.id))
200
+ }
201
+ // 非法: FAILED → WORKING(只能 FAILED→ASSIGNED)
202
+ {
203
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: 'u' })
204
+ engine.transition(t.id, 'WORKING', worker.id)
205
+ engine.transition(t.id, 'FAILED', worker.id)
206
+ expectInvalidTransition('FAILED→WORKING 拒绝', () => engine.transition(t.id, 'WORKING', lead.id))
207
+ }
208
+ }
209
+
210
+ function testApplyEventArtifactChunking(): void {
211
+ const { engine, lead, worker, channel } = setup()
212
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: '分块任务' })
213
+ engine.transition(t.id, 'WORKING', worker.id)
214
+
215
+ const mkArtifact = (text: string): A2AArtifact => ({
216
+ artifactId: 'art-1',
217
+ name: 'report',
218
+ parts: [{ text }],
219
+ })
220
+
221
+ engine.applyEvent(t.id, { kind: 'artifact', artifact: mkArtifact('chunk-1'), append: true, totalChunks: 4 })
222
+ let task = engine.get(t.id)!
223
+ check('第 1 块 append 后 parts=1', task.artifacts[0].parts.length === 1)
224
+ check('第 1 块 progress=25', task.progress === 25, `progress=${task.progress}`)
225
+
226
+ engine.applyEvent(t.id, { kind: 'artifact', artifact: mkArtifact('chunk-2'), append: true, totalChunks: 4 })
227
+ task = engine.get(t.id)!
228
+ check('第 2 块 append 后 parts=2(同名合并)', task.artifacts[0].parts.length === 2)
229
+ check('第 2 块 progress=50', task.progress === 50, `progress=${task.progress}`)
230
+ check('同名 append 不新增 artifact', task.artifacts.length === 1)
231
+
232
+ engine.applyEvent(t.id, { kind: 'artifact', artifact: mkArtifact('chunk-3'), append: true, totalChunks: 4 })
233
+ engine.applyEvent(t.id, { kind: 'artifact', artifact: mkArtifact('chunk-4'), append: true, totalChunks: 4 })
234
+ task = engine.get(t.id)!
235
+ check('4 块收齐 parts=4', task.artifacts[0].parts.length === 4)
236
+ check('4 块 progress=100', task.progress === 100, `progress=${task.progress}`)
237
+
238
+ // 非 append 直接 push 新 artifact
239
+ const other: A2AArtifact = { artifactId: 'art-2', name: 'notes', parts: [{ text: '总结' }] }
240
+ engine.applyEvent(t.id, { kind: 'artifact', artifact: other })
241
+ task = engine.get(t.id)!
242
+ check('非 append 新增 artifact', task.artifacts.length === 2)
243
+ check('非 append 不影响 progress', task.progress === 100, `progress=${task.progress}`)
244
+ }
245
+
246
+ function testComplete(): void {
247
+ const { engine, lead, worker, channel } = setup()
248
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: '完成任务' })
249
+ engine.transition(t.id, 'WORKING', worker.id)
250
+ const done = engine.complete(t.id)
251
+ check('complete 置 COMPLETED', done.state === 'COMPLETED', `state=${done.state}`)
252
+ check('complete 进度 100', done.progress === 100, `progress=${done.progress}`)
253
+ }
254
+
255
+ function testReassign(): void {
256
+ const { engine, lead, worker, messages, agents, channel } = setup()
257
+ const other = agents.create({ name: 'worker2', harness: 'mock' })
258
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: '失败任务' })
259
+ engine.transition(t.id, 'WORKING', worker.id)
260
+ engine.transition(t.id, 'FAILED', worker.id)
261
+
262
+ const r = engine.reassign(t.id, other.id)
263
+ check('reassign 置 ASSIGNED', r.state === 'ASSIGNED', `state=${r.state}`)
264
+ check('reassign 换 assignee', r.assigneeId === other.id)
265
+ check('reassign retryCount+1', r.retryCount === 1, `retry=${r.retryCount}`)
266
+
267
+ const msgs = pendingMessages(messages, channel.id, other.id)
268
+ check('reassign 投递 assign 消息', msgs.length === 1, `count=${msgs.length}`)
269
+ check('reassign 消息 kind=assign', msgs[0]?.metadata['x-aw-task-kind'] === 'assign')
270
+ check('reassign 消息 task-id', msgs[0]?.metadata['x-aw-task-id'] === t.id)
271
+ }
272
+
273
+ function testCancel(): void {
274
+ const { engine, lead, worker, messages, channel } = setup()
275
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: '取消任务' })
276
+ engine.transition(t.id, 'WORKING', worker.id)
277
+
278
+ const c = engine.cancel(t.id, lead.id)
279
+ check('cancel 置 CANCELED', c.state === 'CANCELED', `state=${c.state}`)
280
+
281
+ const msgs = pendingMessages(messages, channel.id, worker.id)
282
+ check('cancel 投递 cancel 消息', msgs.length === 1, `count=${msgs.length}`)
283
+ check('cancel 消息 kind=cancel', msgs[0]?.metadata['x-aw-task-kind'] === 'cancel')
284
+ check('cancel 消息 task-id', msgs[0]?.metadata['x-aw-task-id'] === t.id)
285
+ }
286
+
287
+ function testOnChildCompleted(): void {
288
+ const { engine, lead, worker, messages, channel } = setup()
289
+ const parent = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: lead.id, title: '主任务' })
290
+ engine.transition(parent.id, 'WORKING', lead.id)
291
+
292
+ const child1 = engine.dispatch(parent, { assigneeId: worker.id, title: '子任务1' })
293
+ const child2 = engine.dispatch(parent, { assigneeId: worker.id, title: '子任务2' })
294
+ check('两个子任务后父保持 WAITING', engine.get(parent.id)?.state === 'WAITING')
295
+
296
+ // 完成子任务 1(ASSIGNED → WORKING → COMPLETED)
297
+ engine.transition(child1.id, 'WORKING', worker.id)
298
+ const done1 = engine.complete(child1.id)
299
+ engine.onChildCompleted(done1)
300
+ check('子任务 1 完成后父仍 WAITING', engine.get(parent.id)?.state === 'WAITING')
301
+
302
+ // 完成子任务 2(最后一个)
303
+ engine.transition(child2.id, 'WORKING', worker.id)
304
+ const done2 = engine.complete(child2.id)
305
+ engine.onChildCompleted(done2)
306
+ check('最后一个子任务完成后父恢复 WORKING', engine.get(parent.id)?.state === 'WORKING')
307
+
308
+ // child-completed 消息:metadata 断言(x-aw-task-id=父 id,x-aw-child-task-id=子 id)
309
+ const leadMsgs = pendingMessages(messages, channel.id, lead.id)
310
+ check('投递 2 条 child-completed 消息', leadMsgs.length === 2, `count=${leadMsgs.length}`)
311
+ const last = leadMsgs[leadMsgs.length - 1]
312
+ check('child-completed kind', last?.metadata['x-aw-task-kind'] === 'child-completed')
313
+ check('child-completed x-aw-task-id=父 id', last?.metadata['x-aw-task-id'] === parent.id)
314
+ check('child-completed x-aw-child-task-id=子2 id', last?.metadata['x-aw-child-task-id'] === child2.id)
315
+ }
316
+
317
+ function testApplyEventErrorOnTerminal(): void {
318
+ const { engine, lead, worker, channel } = setup()
319
+ // WORKING 收到错误事件 → FAILED(正常路径)
320
+ {
321
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: '正常失败' })
322
+ engine.transition(t.id, 'WORKING', worker.id)
323
+ engine.applyEvent(t.id, { kind: 'error', error: { code: 'OMP_LLM_ERROR', message: 'boom' } })
324
+ check('WORKING 错误事件 → FAILED', engine.get(t.id)?.state === 'FAILED', `state=${engine.get(t.id)?.state}`)
325
+ }
326
+ // 已 CANCELED 的任务(取消后 abort 回合报 "Interrupted by user")→ 幂等 no-op,不抛非法迁移
327
+ {
328
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: '取消后回合报错' })
329
+ engine.transition(t.id, 'WORKING', worker.id)
330
+ engine.cancel(t.id, lead.id)
331
+ let threw = false
332
+ try {
333
+ engine.applyEvent(t.id, { kind: 'error', error: { code: 'OMP_LLM_ERROR', message: 'Interrupted by user' } })
334
+ }
335
+ catch {
336
+ threw = true
337
+ }
338
+ check('CANCELED 错误事件不抛非法迁移', !threw)
339
+ check('CANCELED 错误事件不改变状态', engine.get(t.id)?.state === 'CANCELED', `state=${engine.get(t.id)?.state}`)
340
+ }
341
+ // 已 COMPLETED / FAILED → 同样幂等 no-op
342
+ {
343
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: '完成后回合报错' })
344
+ engine.transition(t.id, 'WORKING', worker.id)
345
+ engine.complete(t.id)
346
+ engine.applyEvent(t.id, { kind: 'error', error: { code: 'OMP_LLM_ERROR', message: 'late' } })
347
+ check('COMPLETED 错误事件保持 COMPLETED', engine.get(t.id)?.state === 'COMPLETED', `state=${engine.get(t.id)?.state}`)
348
+ }
349
+ }
350
+
351
+ function testSyncBroadcast(): void {
352
+ // 带 hooks 的 engine:验证 onTaskChange 正常收到 state 与 progress(前端同步链的事实源)
353
+ const db = openWorkshopDb(':memory:')
354
+ const channels = createChannelRepo(db)
355
+ const agents = createAgentRepo(db)
356
+ const tasks = createTaskRepo(db)
357
+ const messages = createMessageRepo(db)
358
+ const events: Array<{ state?: string, progress?: number }> = []
359
+ const engine = new TaskEngine({ tasks, messages }, {
360
+ onTaskChange: (e) => {
361
+ events.push({ state: e.state, progress: e.progress })
362
+ },
363
+ })
364
+ const channel = channels.create({ name: 'sync-channel' })
365
+ const lead = agents.create({ name: 'lead', harness: 'mock' })
366
+ const worker = agents.create({ name: 'worker', harness: 'mock' })
367
+
368
+ // ① 分块折算进度 → hook 广播 progress(不重复广播相同进度)
369
+ const t = engine.create({ channelId: channel.id, creatorId: lead.id, assigneeId: worker.id, title: '同步任务' })
370
+ engine.transition(t.id, 'WORKING', worker.id)
371
+ const before = events.length
372
+ engine.applyEvent(t.id, {
373
+ kind: 'artifact',
374
+ artifact: { artifactId: 'a1', name: 'report', parts: [{ text: 'chunk-1' }] },
375
+ append: true,
376
+ totalChunks: 4,
377
+ })
378
+ engine.applyEvent(t.id, {
379
+ kind: 'artifact',
380
+ artifact: { artifactId: 'a1', name: 'report', parts: [{ text: 'chunk-2' }] },
381
+ append: true,
382
+ totalChunks: 4,
383
+ })
384
+ const progressEvents = events.slice(before).filter(e => e.progress !== undefined)
385
+ check('分块折算进度触发 progress 广播', progressEvents.length === 2, `count=${progressEvents.length}`)
386
+ check('progress 数值递增正确', progressEvents[0]?.progress === 25 && progressEvents[1]?.progress === 50,
387
+ `p0=${progressEvents[0]?.progress} p1=${progressEvents[1]?.progress}`)
388
+ // 相同进度不重复广播(append 无折算变化)
389
+ engine.applyEvent(t.id, { kind: 'artifact', artifact: { artifactId: 'a2', name: 'notes', parts: [{ text: 'x' }] } })
390
+ const afterExtra = events.filter(e => e.progress !== undefined).length
391
+ check('无进度变化的 artifact 不触发 progress 广播', afterExtra === progressEvents.length, `count=${afterExtra}`)
392
+
393
+ // ② complete:先置 100 再迁移 → 状态广播时任务进度已是 100(状态帧携带完成态进度)
394
+ const stateEvents: Array<{ state?: string, progressAtBroadcast?: number }> = []
395
+ const engine2 = new TaskEngine({ tasks, messages }, {
396
+ onTaskChange: (e) => {
397
+ if (e.state === 'COMPLETED') {
398
+ // 状态广播时刻的 DB 进度(transition 内部读行构造 task.status → 直接可见)
399
+ stateEvents.push({ state: e.state, progressAtBroadcast: tasks.findById(t.id)?.progress })
400
+ }
401
+ },
402
+ })
403
+ engine2.complete(t.id)
404
+ const completed = stateEvents[0]
405
+ check('COMPLETED 状态广播时进度已为 100', completed?.state === 'COMPLETED' && completed?.progressAtBroadcast === 100,
406
+ `progress=${completed?.progressAtBroadcast}`)
407
+ }
408
+
409
+ function main(): void {
410
+ testCreateDispatch()
411
+ testDispatchAssignMessage()
412
+ testAllLegalTransitions()
413
+ testIllegalTransitions()
414
+ testApplyEventArtifactChunking()
415
+ testApplyEventErrorOnTerminal()
416
+ testSyncBroadcast()
417
+ testComplete()
418
+ testReassign()
419
+ testCancel()
420
+ testOnChildCompleted()
421
+
422
+ console.log('')
423
+ if (failures === 0) {
424
+ console.log('ALL PASS')
425
+ process.exit(0)
426
+ }
427
+ else {
428
+ console.log(`${failures} FAILED`)
429
+ process.exit(1)
430
+ }
431
+ }
432
+
433
+ main()