agentworkshop 0.7.37 → 0.7.38

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 (59) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/builds/latest.json +1 -1
  3. package/.output/public/_nuxt/builds/meta/44b66249-54d0-4c8a-9057-9755a32329e2.json +1 -0
  4. package/.output/server/chunks/_/nitro.mjs +798 -799
  5. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  6. package/.output/server/package.json +1 -1
  7. package/README-zh.md +2 -2
  8. package/README.md +2 -2
  9. package/docs/cli.en.md +2 -2
  10. package/docs/cli.md +2 -2
  11. package/docs/full-test-plan.md +1 -1
  12. package/docs/plugins.en.md +1 -1
  13. package/docs/plugins.md +1 -1
  14. package/docs/sdk.en.md +1 -1
  15. package/docs/sdk.md +1 -1
  16. package/package.json +1 -1
  17. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +1 -0
  18. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +1 -0
  19. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +153 -0
  20. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +153 -0
  21. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +431 -0
  22. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +180 -0
  23. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +112 -0
  24. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +227 -0
  25. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +131 -0
  26. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +160 -0
  27. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +157 -0
  28. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +348 -0
  29. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +296 -0
  30. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +285 -0
  31. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +217 -0
  32. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +301 -0
  33. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +151 -0
  34. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +135 -0
  35. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +156 -0
  36. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +72 -0
  37. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +469 -0
  38. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +259 -0
  39. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +97 -0
  40. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +277 -0
  41. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +171 -0
  42. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +425 -0
  43. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +99 -0
  44. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +325 -0
  45. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +332 -0
  46. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +43 -0
  47. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +154 -0
  48. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +531 -0
  49. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +439 -0
  50. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +323 -0
  51. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +433 -0
  52. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +280 -0
  53. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +117 -0
  54. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +160 -0
  55. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +101 -0
  56. package/scripts/test-plugin-hardening.mjs +32 -0
  57. package/server/plugins/dev-stability-guard.ts +40 -63
  58. package/server/utils/stability-severity.ts +66 -0
  59. package/.output/public/_nuxt/builds/meta/a66fdef7-dcfd-453b-b1f6-543c7dfd98d5.json +0 -1
@@ -0,0 +1,157 @@
1
+ /**
2
+ * 审计实验 1 —— DCW 单节点写在飞互斥 + 周期读互斥(只读:不碰任何源码/数据文件)。
3
+ *
4
+ * 驱动真实 DcwNodeRuntime(dcw-runtime.ts),host 全部打桩,统计同一节点的
5
+ * 并发在飞写/读数;断言心跳重下发(tick)与手动 write() 共享同一 writing 闸门。
6
+ *
7
+ * 运行:node --experimental-transform-types scripts/_audit/run.mjs scripts/_audit/dcw-write-mutex.ts
8
+ */
9
+ import { DcwNode } from '../../server/services/workshop/dcw/dcw-node'
10
+ import { DcwNodeRuntime } from '../../server/services/workshop/dcw/dcw-runtime'
11
+
12
+ let failures = 0
13
+ function check(name: string, cond: boolean, extra = ''): void {
14
+ if (cond) {
15
+ console.log(` PASS ${name}${extra ? ` (${extra})` : ''}`)
16
+ }
17
+ else {
18
+ failures++
19
+ console.log(` FAIL ${name}${extra ? ` (${extra})` : ''}`)
20
+ }
21
+ }
22
+
23
+ const statusOf = (e: unknown): string => {
24
+ const s = (e as { status?: number }).status
25
+ return s ? String(s) : `no-status:${(e as Error)?.message ?? String(e)}`
26
+ }
27
+
28
+ interface Stats {
29
+ writeCalls: number
30
+ readCalls: number
31
+ inFlightWrites: number
32
+ maxInFlightWrites: number
33
+ inFlightReads: number
34
+ maxInFlightReads: number
35
+ writeEng: number[]
36
+ concurrentReadPairs: number
37
+ }
38
+
39
+ function makeNode(): DcwNode {
40
+ return DcwNode.fromRow({
41
+ id: 'n-audit-1',
42
+ templateRef: 'cw-temp',
43
+ name: 'audit-node',
44
+ driver: 'mock',
45
+ min: 0,
46
+ max: 200,
47
+ decimals: 2,
48
+ holdIntervalMs: 10,
49
+ readIntervalMs: 1000,
50
+ enabled: true,
51
+ lineId: 'ln-audit',
52
+ })
53
+ }
54
+
55
+ function makeHarness(opts: { writeDelayMs: number, readDelayMs: number }) {
56
+ const stats: Stats = {
57
+ writeCalls: 0,
58
+ readCalls: 0,
59
+ inFlightWrites: 0,
60
+ maxInFlightWrites: 0,
61
+ inFlightReads: 0,
62
+ maxInFlightReads: 0,
63
+ writeEng: [],
64
+ concurrentReadPairs: 0,
65
+ }
66
+ const sleep = (ms: number): Promise<void> => new Promise(r => setTimeout(r, ms))
67
+ const host = {
68
+ running: () => true,
69
+ defaults: () => ({ holdIntervalMs: 10, readIntervalMs: 1000 }),
70
+ async executeWrite(node: DcwNode, eng: number) {
71
+ stats.writeCalls++
72
+ stats.writeEng.push(eng)
73
+ stats.inFlightWrites++
74
+ stats.maxInFlightWrites = Math.max(stats.maxInFlightWrites, stats.inFlightWrites)
75
+ await sleep(opts.writeDelayMs)
76
+ stats.inFlightWrites--
77
+ node.applyWriteResult(eng, true, 'stub ok', new Date().toISOString())
78
+ return { ok: true, message: 'stub ok', raw: eng, readback: eng }
79
+ },
80
+ async executeRead() {
81
+ stats.readCalls++
82
+ stats.inFlightReads++
83
+ stats.maxInFlightReads = Math.max(stats.maxInFlightReads, stats.inFlightReads)
84
+ if (stats.inFlightReads > 1)
85
+ stats.concurrentReadPairs++
86
+ await sleep(opts.readDelayMs)
87
+ stats.inFlightReads--
88
+ return { ok: true, value: 1, raw: 1, message: 'stub read', at: new Date().toISOString() }
89
+ },
90
+ }
91
+ return { host, stats }
92
+ }
93
+
94
+ async function main(): Promise<void> {
95
+ // ---------- 场景 A:手动写在飞时,保写心跳必须跳过本拍(不得并发) ----------
96
+ console.log('\n[A] manual write in flight -> tick keep-alive must skip')
97
+ {
98
+ const node = makeNode()
99
+ const { host, stats } = makeHarness({ writeDelayMs: 120, readDelayMs: 0 })
100
+ const rt = new DcwNodeRuntime(node, host as never)
101
+ const p = rt.write(50)
102
+ let tickAt = 1_000_000
103
+ for (let i = 0; i < 20; i++) {
104
+ tickAt += 50 // 远超 holdIntervalMs=10,心跳到点
105
+ rt.tick(tickAt)
106
+ }
107
+ const concurrentSecondWrite = await rt.write(60).then(() => 'resolved', e => `rejected:${statusOf(e)}`)
108
+ const outcome = await p
109
+ check('manual write ok', outcome.ok === true)
110
+ check('heartbeat skipped while manual write in flight (executeWrite called once)', stats.writeCalls === 1, `calls=${stats.writeCalls}`)
111
+ check('max concurrent in-flight writes == 1', stats.maxInFlightWrites === 1, `max=${stats.maxInFlightWrites}`)
112
+ check('second concurrent manual write rejected with 409', concurrentSecondWrite === 'rejected:409', concurrentSecondWrite)
113
+ }
114
+
115
+ // ---------- 场景 B:手动写结束后,心跳按节奏重下发且仍与手动写互斥 ----------
116
+ console.log('\n[B] heartbeat re-write shares the same gate')
117
+ {
118
+ const node = makeNode()
119
+ const { host, stats } = makeHarness({ writeDelayMs: 60, readDelayMs: 0 })
120
+ const rt = new DcwNodeRuntime(node, host as never)
121
+ await rt.write(80)
122
+ const callsAfterManual = stats.writeCalls
123
+ const t = 2_000_100
124
+ rt.tick(t) // 到点 -> 心跳下发 node.value
125
+ rt.tick(t) // 同一拍再次 tick:心跳在飞,必须跳过
126
+ await new Promise(r => setTimeout(r, 5))
127
+ const second = await rt.write(90).then(() => 'resolved', e => `rejected:${statusOf(e)}`)
128
+ await new Promise(r => setTimeout(r, 150))
129
+ check('heartbeat issued at least one write', stats.writeCalls > callsAfterManual, `calls=${stats.writeCalls}`)
130
+ check('heartbeat wrote current setpoint 80', stats.writeEng.includes(80), JSON.stringify(stats.writeEng))
131
+ check('manual write during heartbeat in flight rejected with 409', second === 'rejected:409', second)
132
+ check('max concurrent in-flight writes == 1 (whole scenario)', stats.maxInFlightWrites === 1, `max=${stats.maxInFlightWrites}`)
133
+ }
134
+
135
+ // ---------- 场景 C:周期读的在飞互斥(readNow 与 tick 读) ----------
136
+ console.log('\n[C] readNow() while periodic read in flight')
137
+ {
138
+ const node = makeNode()
139
+ const { host, stats } = makeHarness({ writeDelayMs: 0, readDelayMs: 120 })
140
+ const rt = new DcwNodeRuntime(node, host as never)
141
+ rt.tick(3_000_000) // 触发周期读(不 await)
142
+ await new Promise(r => setTimeout(r, 10)) // 让周期读进入在飞
143
+ const res = await rt.readNow().then(() => 'resolved', e => `rejected:${statusOf(e)}`)
144
+ await new Promise(r => setTimeout(r, 250))
145
+ check('periodic read started', stats.readCalls >= 1, `readCalls=${stats.readCalls}`)
146
+ check(
147
+ 'readNow() rejected (expect rejected:409) while periodic read in flight',
148
+ res === 'rejected:409',
149
+ `actual=${res}; concurrent read pairs=${stats.concurrentReadPairs}, maxInFlightReads=${stats.maxInFlightReads}`,
150
+ )
151
+ }
152
+
153
+ console.log(`\nresult: ${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
154
+ process.exit(failures === 0 ? 0 : 1)
155
+ }
156
+
157
+ void main()
@@ -0,0 +1,348 @@
1
+ /**
2
+ * AgentRuntime 运行时核心测试(node + tsx 直跑,无浏览器)。
3
+ *
4
+ * 覆盖:
5
+ * 1. idle 自动消费
6
+ * 2. busy 时 enqueue 不打断(排队)
7
+ * 3. 结束后续消费积压
8
+ * 4. assign 消息触发 transition('WORKING')
9
+ * 5. 单条 run 抛错不阻塞下一条
10
+ * 6. abort 终止事件流
11
+ * 7. stop 优雅(等当前事件流结束)
12
+ *
13
+ * 注入 fake TaskEngine(与契约相同签名),真实引擎装配留到集成阶段。
14
+ */
15
+ import { randomUUID } from 'node:crypto'
16
+ import type { DatabaseSync } from 'node:sqlite'
17
+ import { openWorkshopDb } from '../server/services/workshop/db/database'
18
+ import { createMessageRepo } from '../server/services/workshop/db/message.repo'
19
+ import type { MessageRepo } from '../server/services/workshop/db/message.repo'
20
+ import { Mailbox } from '../server/services/workshop/runtime/mailbox'
21
+ import { AgentRuntime } from '../server/services/workshop/runtime/agent-runtime'
22
+ import type { ChannelBus, TaskEngine } from '../server/services/workshop/runtime/agent-runtime'
23
+ import type { A2AMessage, Part } from '../server/services/workshop/types/a2a'
24
+ import type { AgentTaskQueueView, TaskState, WorkspaceTask } from '../server/services/workshop/types/task'
25
+ import type {
26
+ AgentEvent,
27
+ AgentInfo,
28
+ AgentInterface,
29
+ AgentRunContext,
30
+ AgentRunRequest,
31
+ AgentWorkspace,
32
+ } from '../server/services/workshop/agents/agent-interface'
33
+
34
+ let failures = 0
35
+ function check(name: string, ok: boolean, detail = ''): void {
36
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
37
+ if (!ok) {
38
+ failures += 1
39
+ }
40
+ }
41
+
42
+ function sleep(ms: number): Promise<void> {
43
+ return new Promise(r => setTimeout(r, ms))
44
+ }
45
+
46
+ async function waitUntil(cond: () => boolean, timeoutMs = 3000): Promise<void> {
47
+ const start = Date.now()
48
+ while (!cond()) {
49
+ if (Date.now() - start > timeoutMs) throw new Error('等待超时')
50
+ await sleep(5)
51
+ }
52
+ }
53
+
54
+ /** 落库一个 channel(messages.channel_id 外键依赖) */
55
+ function seedChannel(db: DatabaseSync, id: string): void {
56
+ const now = new Date().toISOString()
57
+ db.prepare(
58
+ `INSERT INTO channels (id, name, description, lead_agent_id, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
59
+ ).run(id, id, '', null, 1, now, now)
60
+ }
61
+
62
+ function mkAgent(id: string, role: 'lead' | 'worker', channelId: string): AgentInfo {
63
+ return { id, channelId, name: id, harness: 'mock', role, config: {} }
64
+ }
65
+
66
+ function mkMessage(channelId: string, parts: Part[], metadata?: Record<string, unknown>): A2AMessage {
67
+ return { messageId: randomUUID(), contextId: channelId, role: 'ROLE_USER', parts, metadata }
68
+ }
69
+
70
+ function mkTask(channelId: string, assigneeId: string, title: string): WorkspaceTask {
71
+ return {
72
+ id: randomUUID(),
73
+ channelId,
74
+ assigneeId,
75
+ creatorId: '',
76
+ title,
77
+ state: 'SUBMITTED',
78
+ progress: 0,
79
+ retryCount: 0,
80
+ artifacts: [],
81
+ history: [],
82
+ createdAt: new Date().toISOString(),
83
+ updatedAt: new Date().toISOString(),
84
+ }
85
+ }
86
+
87
+ /** fake TaskEngine:记录 transition/applyEvent,提供 get/create 等(与契约相同签名) */
88
+ function makeFakeEngine() {
89
+ const transitions: { taskId: string, state: TaskState, by: string }[] = []
90
+ const applied: { taskId: string, event: AgentEvent }[] = []
91
+ const tasks = new Map<string, WorkspaceTask>()
92
+ const engine = {
93
+ create(input: {
94
+ channelId: string
95
+ creatorId: string
96
+ assigneeId: string
97
+ title: string
98
+ description?: string
99
+ parentId?: string
100
+ parts?: Part[]
101
+ }): WorkspaceTask {
102
+ const t = mkTask(input.channelId, input.assigneeId, input.title)
103
+ tasks.set(t.id, t)
104
+ return t
105
+ },
106
+ dispatch(): WorkspaceTask {
107
+ throw new Error('dispatch 未在测试中使用')
108
+ },
109
+ transition(taskId: string, state: TaskState, by: string): WorkspaceTask {
110
+ transitions.push({ taskId, state, by })
111
+ const t = tasks.get(taskId)
112
+ if (t) t.state = state
113
+ return t ?? mkTask('', '', '')
114
+ },
115
+ applyEvent(taskId: string, event: AgentEvent): void {
116
+ applied.push({ taskId, event })
117
+ },
118
+ list(): WorkspaceTask[] {
119
+ return [...tasks.values()]
120
+ },
121
+ get(taskId: string): WorkspaceTask | undefined {
122
+ return tasks.get(taskId)
123
+ },
124
+ complete(): WorkspaceTask {
125
+ throw new Error('complete 未在测试中使用')
126
+ },
127
+ reassign(): WorkspaceTask {
128
+ throw new Error('reassign 未在测试中使用')
129
+ },
130
+ cancel(): WorkspaceTask {
131
+ throw new Error('cancel 未在测试中使用')
132
+ },
133
+ onChildCompleted(): void {},
134
+ queueViewOf(channelId: string, agentId: string): AgentTaskQueueView {
135
+ const mine = [...tasks.values()].filter(t => t.channelId === channelId && t.assigneeId === agentId)
136
+ return {
137
+ agentId,
138
+ channelId,
139
+ queued: mine.filter(t => t.state === 'SUBMITTED' || t.state === 'ASSIGNED'),
140
+ current: mine.find(t => t.state === 'WORKING'),
141
+ completed: mine.filter(t => t.state === 'COMPLETED'),
142
+ }
143
+ },
144
+ }
145
+ return { engine: engine as TaskEngine, transitions, applied, tasks }
146
+ }
147
+
148
+ /** 可配置 fake impl:记录 run 调用,按注入的生成器产出事件流 */
149
+ class FakeImpl implements AgentInterface {
150
+ calls: AgentRunRequest[] = []
151
+ constructor(
152
+ private behavior: (req: AgentRunRequest, ctx: AgentRunContext) => AsyncGenerator<AgentEvent, void, unknown>,
153
+ ) {}
154
+
155
+ run(req: AgentRunRequest, ctx: AgentRunContext): AsyncIterable<AgentEvent> {
156
+ this.calls.push(req)
157
+ return this.behavior(req, ctx)
158
+ }
159
+ }
160
+
161
+ interface Setup {
162
+ messages: MessageRepo
163
+ busEvents: { event: AgentEvent, source: A2AMessage }[]
164
+ bus: ChannelBus
165
+ engine: ReturnType<typeof makeFakeEngine>
166
+ }
167
+
168
+ function setup(): Setup {
169
+ const db = openWorkshopDb(':memory:')
170
+ seedChannel(db, 'ch1')
171
+ const messages = createMessageRepo(db)
172
+ const busEvents: { event: AgentEvent, source: A2AMessage }[] = []
173
+ const bus: ChannelBus = {
174
+ emit: (event, source) => busEvents.push({ event, source }),
175
+ onEvent: () => () => {},
176
+ notifyTask: () => {},
177
+ notifyAgent: () => {},
178
+ onAgentStatus: () => {},
179
+ onTaskEvent: () => {},
180
+ wakeScheduler: () => {},
181
+ }
182
+ return { messages, busEvents, bus, engine: makeFakeEngine() }
183
+ }
184
+
185
+ function makeRuntime(s: Setup, agent: AgentInfo, impl: AgentInterface): AgentRuntime {
186
+ const mailbox = new Mailbox(s.messages, agent.channelId, agent.id, () => {})
187
+ const workspace = {} as AgentWorkspace
188
+ return new AgentRuntime(agent, impl, { mailbox, taskEngine: s.engine.engine, bus: s.bus, workspace })
189
+ }
190
+
191
+ async function testAutoConsume(): Promise<void> {
192
+ console.log('\n--- 1. idle 自动消费 ---')
193
+ const s = setup()
194
+ const impl = new FakeImpl(async function* () {
195
+ yield { kind: 'status', status: { state: 'working', timestamp: new Date().toISOString() } }
196
+ yield { kind: 'done' }
197
+ })
198
+ const rt = makeRuntime(s, mkAgent('a1', 'worker', 'ch1'), impl)
199
+ rt.start()
200
+ const msg = mkMessage('ch1', [])
201
+ rt.enqueue(msg)
202
+ await waitUntil(() => impl.calls.length === 1)
203
+ await waitUntil(() => rt.getState() === 'idle')
204
+ check('enqueue 后自动消费:run 被调用', impl.calls.length === 1)
205
+ check('自动消费:事件逐条广播', s.busEvents.length === 2, `收到 ${s.busEvents.length} 个事件`)
206
+ check('自动消费:消息标记已消费', s.messages.listPendingByChannelAgent('ch1', 'a1').length === 0)
207
+ await rt.stop()
208
+ }
209
+
210
+ async function testBusyQueue(): Promise<void> {
211
+ console.log('\n--- 2/3. busy 不打断 + 积压消费 ---')
212
+ const s = setup()
213
+ let release!: () => void
214
+ const hold = new Promise<void>((r) => {
215
+ release = r
216
+ })
217
+ const impl = new FakeImpl(async function* () {
218
+ yield { kind: 'status', status: { state: 'started', timestamp: new Date().toISOString() } }
219
+ await hold
220
+ yield { kind: 'done' }
221
+ })
222
+ const rt = makeRuntime(s, mkAgent('a1', 'worker', 'ch1'), impl)
223
+ rt.start()
224
+ const msgA = mkMessage('ch1', [])
225
+ const msgB = mkMessage('ch1', [])
226
+ rt.enqueue(msgA)
227
+ await waitUntil(() => impl.calls.length === 1)
228
+ check('busy 时不打断:state=busy', rt.getState() === 'busy')
229
+ rt.enqueue(msgB)
230
+ await sleep(30)
231
+ check('busy 时不打断:B 排队未处理', impl.calls.length === 1)
232
+ release()
233
+ await waitUntil(() => impl.calls.length === 2)
234
+ await waitUntil(() => rt.getState() === 'idle')
235
+ check('结束后续消费积压:B 被消费', impl.calls.length === 2)
236
+ check('积压消费:两条消息均已消费', s.messages.listPendingByChannelAgent('ch1', 'a1').length === 0)
237
+ await rt.stop()
238
+ }
239
+
240
+ async function testAssignTransition(): Promise<void> {
241
+ console.log('\n--- 4. assign 消息触发 transition WORKING ---')
242
+ const s = setup()
243
+ const impl = new FakeImpl(async function* () {
244
+ yield { kind: 'done' }
245
+ })
246
+ const rt = makeRuntime(s, mkAgent('a1', 'worker', 'ch1'), impl)
247
+ rt.start()
248
+ const task = mkTask('ch1', 'a1', '任务A')
249
+ s.engine.tasks.set(task.id, task)
250
+ const msg = mkMessage('ch1', [], { 'x-aw-task-kind': 'assign', 'x-aw-task-id': task.id })
251
+ msg.taskId = task.id
252
+ rt.enqueue(msg)
253
+ await waitUntil(() => impl.calls.length === 1)
254
+ await waitUntil(() => rt.getState() === 'idle')
255
+ check(
256
+ 'assign 消息触发 transition("WORKING", agentId)',
257
+ s.engine.transitions.some(t => t.taskId === task.id && t.state === 'WORKING' && t.by === 'a1'),
258
+ )
259
+ await rt.stop()
260
+ }
261
+
262
+ async function testErrorDoesNotBlock(): Promise<void> {
263
+ console.log('\n--- 5. 单条 run 抛错不阻塞下一条 ---')
264
+ const s = setup()
265
+ let callCount = 0
266
+ const impl = new FakeImpl(async function* () {
267
+ callCount += 1
268
+ if (callCount === 1) {
269
+ yield { kind: 'status', status: { state: 'boom', timestamp: new Date().toISOString() } }
270
+ throw new Error('boom')
271
+ }
272
+ yield { kind: 'done' }
273
+ })
274
+ const rt = makeRuntime(s, mkAgent('a1', 'worker', 'ch1'), impl)
275
+ rt.start()
276
+ rt.enqueue(mkMessage('ch1', []))
277
+ rt.enqueue(mkMessage('ch1', []))
278
+ await waitUntil(() => impl.calls.length >= 3 && rt.getState() === 'idle')
279
+ check('单条抛错重投重试:失败消息重试成功且不阻塞下一条', impl.calls.length === 3)
280
+ check('重试后消息仍标记消费', s.messages.listPendingByChannelAgent('ch1', 'a1').length === 0)
281
+ await rt.stop()
282
+ }
283
+
284
+ async function testAbort(): Promise<void> {
285
+ console.log('\n--- 6. abort 终止事件流 ---')
286
+ const s = setup()
287
+ let events = 0
288
+ const impl = new FakeImpl(async function* (_req, ctx) {
289
+ for (let i = 0; i < 300; i++) {
290
+ if (ctx.signal.aborted) break
291
+ events += 1
292
+ yield { kind: 'status', status: { state: `step${i}`, timestamp: new Date().toISOString() } }
293
+ await sleep(2)
294
+ }
295
+ yield { kind: 'done' }
296
+ })
297
+ const rt = makeRuntime(s, mkAgent('a1', 'worker', 'ch1'), impl)
298
+ rt.start()
299
+ rt.enqueue(mkMessage('ch1', []))
300
+ await waitUntil(() => events >= 3)
301
+ rt.abortCurrent()
302
+ await waitUntil(() => rt.getState() === 'idle')
303
+ check('abort 后事件流终止', events < 300, `终止于第 ${events} 个事件`)
304
+ await rt.stop()
305
+ }
306
+
307
+ async function testStop(): Promise<void> {
308
+ console.log('\n--- 7. stop 优雅 ---')
309
+ const s = setup()
310
+ let release!: () => void
311
+ const hold = new Promise<void>((r) => {
312
+ release = r
313
+ })
314
+ const impl = new FakeImpl(async function* () {
315
+ yield { kind: 'status', status: { state: 'working', timestamp: new Date().toISOString() } }
316
+ await hold
317
+ yield { kind: 'done' }
318
+ })
319
+ const rt = makeRuntime(s, mkAgent('a1', 'worker', 'ch1'), impl)
320
+ rt.start()
321
+ rt.enqueue(mkMessage('ch1', []))
322
+ await waitUntil(() => impl.calls.length === 1)
323
+ let stopResolved = false
324
+ const _stopPromise = rt.stop().then(() => {
325
+ stopResolved = true
326
+ })
327
+ await sleep(30)
328
+ check('stop 等当前事件流结束(未提前返回)', !stopResolved)
329
+ release()
330
+ await waitUntil(() => stopResolved)
331
+ check('stop 优雅:state=stopped', rt.getState() === 'stopped')
332
+ }
333
+
334
+ async function main(): Promise<void> {
335
+ await testAutoConsume()
336
+ await testBusyQueue()
337
+ await testAssignTransition()
338
+ await testErrorDoesNotBlock()
339
+ await testAbort()
340
+ await testStop()
341
+ console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`}`)
342
+ process.exit(failures === 0 ? 0 : 1)
343
+ }
344
+
345
+ main().catch((e) => {
346
+ console.error('测试异常:', e)
347
+ process.exit(1)
348
+ })