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.
- package/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
- package/.AgentWorkShop/prompts/aml-playbook.md +49 -49
- package/.AgentWorkShop/prompts/host-tools.json +1238 -1238
- package/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
- package/.output/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
- package/.output/.AgentWorkShop/prompts/aml-playbook.md +49 -49
- package/.output/.AgentWorkShop/prompts/host-tools.json +1238 -1238
- package/.output/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{BG8sCcwx.js → CUDwzgLV.js} +4 -4
- package/.output/public/_nuxt/CmsOtOAA2.js +1 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/4457d63f-0866-4cea-b320-04a321f24795.json +1 -0
- package/.output/public/_nuxt/default.BXI_fZUi.css +1 -0
- package/.output/server/chunks/_/nitro.mjs +703 -704
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/build/{default-hseyOqBT.mjs → default-CZmZFpqH.mjs} +27 -42
- package/.output/server/chunks/build/{default-hseyOqBT.mjs.map → default-CZmZFpqH.mjs.map} +1 -1
- package/.output/server/chunks/routes/renderer.mjs +1 -1
- package/.output/server/chunks/virtual/entry.mjs +3 -1
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/package.json +1 -1
- package/README-zh.md +6 -6
- package/README.md +6 -6
- package/app/components/AppHeader.vue +0 -17
- package/app/components/AppSidebar.vue +1 -0
- package/docs/cli.en.md +2 -2
- package/docs/cli.md +2 -2
- package/docs/full-test-plan.md +5 -1
- package/docs/plugins.en.md +2 -1
- package/docs/plugins.md +2 -1
- package/docs/sdk.en.md +1 -1
- package/docs/sdk.md +1 -1
- package/i18n/locales/en.ts +1 -0
- package/i18n/locales/zh-CN.ts +1 -0
- package/package.json +1 -1
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +1 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +1 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +153 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +153 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +431 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +180 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +112 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +227 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +131 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +160 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +157 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +348 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +296 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +285 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +217 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +301 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +151 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +135 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +156 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +72 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +469 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +259 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +97 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +277 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +171 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +425 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +99 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +325 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +332 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +43 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +154 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +531 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +439 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +323 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +433 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +280 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +117 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +160 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +101 -0
- package/scripts/_audit/detached-start.mjs +103 -0
- package/scripts/_audit/guard-upstream-liveness.mjs +104 -0
- package/scripts/_dbg-perms-e2e.mjs +10 -3
- package/scripts/_dbg-protocol-matrix.mjs +453 -441
- package/scripts/_dbg-render-regression.mjs +151 -130
- package/scripts/_dbg-town-agents-probe.mjs +60 -0
- package/scripts/_dbg-visual-acceptance.mjs +63 -0
- package/scripts/e2e-agent-team.mjs +5 -3
- package/scripts/test-plugin-hardening.mjs +32 -0
- package/scripts/three-system-e2e.mjs +6 -6
- package/server/plugins/dev-stability-guard.ts +40 -63
- package/server/plugins-builtin/rag-bridge/index.mjs +9 -2
- package/server/utils/stability-severity.ts +66 -0
- package/.output/public/_nuxt/DVsjtsvZ2.js +0 -1
- package/.output/public/_nuxt/builds/meta/a66fdef7-dcfd-453b-b1f6-543c7dfd98d5.json +0 -1
- package/.output/public/_nuxt/default.DE_vT238.css +0 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SchedulerLoop 调度循环测试(node + tsx 直跑,无浏览器)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* 1. 定时 tick:SUBMITTED 任务自动 dispatch 给 worker → worker 自动接取完成 → child-completed → 父任务完成(端到端)
|
|
6
|
+
* 2. wake() 立即触发一轮(大 tickMs 下无定时 tick 仍能 dispatch)
|
|
7
|
+
* 3. supervise 抛错 → 内置规则引擎兜底 dispatch
|
|
8
|
+
* 4. stop() 后不再调度
|
|
9
|
+
* 5. createAgentImpl 工厂(mock/claude/omp/未知 harness)
|
|
10
|
+
*
|
|
11
|
+
* 装配:真实 :memory: repo + 真实 TaskEngine + 真实 ChannelRuntime + 真实 Mailbox
|
|
12
|
+
* + 真实 AgentRuntime(MockAgentImpl lead/worker)+ 真实 SchedulerLoop。
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from 'node:crypto'
|
|
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 { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
|
|
19
|
+
import { createTaskRepo } from '../server/services/workshop/db/task.repo'
|
|
20
|
+
import type { TaskPatch } from '../server/services/workshop/db/task.repo'
|
|
21
|
+
import { createMessageRepo } from '../server/services/workshop/db/message.repo'
|
|
22
|
+
import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
|
|
23
|
+
import { TaskEngine } from '../server/services/workshop/runtime/task-engine'
|
|
24
|
+
import type { WorkspaceTask } from '../server/services/workshop/types/task'
|
|
25
|
+
import { ChannelRuntime } from '../server/services/workshop/runtime/channel-runtime'
|
|
26
|
+
import { AgentRuntime } from '../server/services/workshop/runtime/agent-runtime'
|
|
27
|
+
import type { ChannelBus } from '../server/services/workshop/runtime/agent-runtime'
|
|
28
|
+
import { Mailbox, rowToMessage } from '../server/services/workshop/runtime/mailbox'
|
|
29
|
+
import { SchedulerLoop } from '../server/services/workshop/runtime/scheduler-loop'
|
|
30
|
+
import { MockAgentImpl } from '../server/services/workshop/agents/mock-agent'
|
|
31
|
+
import { ClaudeSdkAgentImpl } from '../server/services/workshop/agents/claude-agent'
|
|
32
|
+
import { createAgentImpl } from '../server/services/workshop/agents/factory'
|
|
33
|
+
import { encodeTaskMode, type ModeConfig } from '../server/services/workshop/runtime/execution-mode'
|
|
34
|
+
import type {
|
|
35
|
+
AgentEvent,
|
|
36
|
+
AgentInfo,
|
|
37
|
+
AgentInterface,
|
|
38
|
+
AgentWorkspace,
|
|
39
|
+
SupervisionDecision,
|
|
40
|
+
} from '../server/services/workshop/agents/agent-interface'
|
|
41
|
+
import type { A2AMessage, ChannelMail } from '../server/services/workshop/types/a2a'
|
|
42
|
+
import { AppError } from '../server/utils/errors'
|
|
43
|
+
|
|
44
|
+
let failures = 0
|
|
45
|
+
function check(name: string, ok: boolean, detail = ''): void {
|
|
46
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
47
|
+
if (!ok) failures += 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sleep(ms: number): Promise<void> {
|
|
51
|
+
return new Promise(r => setTimeout(r, ms))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function waitUntil(cond: () => boolean, timeoutMs = 5000): Promise<boolean> {
|
|
55
|
+
const start = Date.now()
|
|
56
|
+
for (;;) {
|
|
57
|
+
if (cond()) return true
|
|
58
|
+
if (Date.now() - start >= timeoutMs) return cond()
|
|
59
|
+
await sleep(10)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** AgentRow(模板)与 ChannelAgentRow(实例)共有字段;实例 id 才是运行时身份 */
|
|
64
|
+
type RowWithConfig = { id: string, name: string, harness: string, configJson: string }
|
|
65
|
+
|
|
66
|
+
function rowToAgentInfo(row: RowWithConfig, channelId: string, role: 'lead' | 'worker'): AgentInfo {
|
|
67
|
+
return {
|
|
68
|
+
id: row.id,
|
|
69
|
+
channelId,
|
|
70
|
+
name: row.name,
|
|
71
|
+
harness: row.harness,
|
|
72
|
+
role,
|
|
73
|
+
config: JSON.parse(row.configJson) as Record<string, unknown>,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface WorkspaceDeps {
|
|
78
|
+
engine: TaskEngine
|
|
79
|
+
cr: ChannelRuntime
|
|
80
|
+
tasks: ReturnType<typeof createTaskRepo>
|
|
81
|
+
messages: ReturnType<typeof createMessageRepo>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 进程内 AgentWorkspace(委托真实 TaskEngine/ChannelRuntime/repo,模拟 manager 的最小能力面) */
|
|
85
|
+
function buildWorkspace(agent: AgentInfo, deps: WorkspaceDeps): AgentWorkspace {
|
|
86
|
+
const { engine, cr, tasks, messages } = deps
|
|
87
|
+
return {
|
|
88
|
+
listAgents: async () => [],
|
|
89
|
+
dispatchTask: async () => {
|
|
90
|
+
throw new AppError(400, 'BAD_REQUEST', '测试 workspace 不使用 dispatchTask')
|
|
91
|
+
},
|
|
92
|
+
listTasks: async () => engine.list(agent.channelId),
|
|
93
|
+
getTask: async (taskId) => {
|
|
94
|
+
const task = engine.get(taskId)
|
|
95
|
+
if (!task) throw new AppError(404, 'NOT_FOUND', `任务不存在: ${taskId}`)
|
|
96
|
+
return task
|
|
97
|
+
},
|
|
98
|
+
reportTask: async ({ taskId, progress, artifact, message }) => {
|
|
99
|
+
const task = engine.get(taskId)
|
|
100
|
+
if (!task) throw new AppError(404, 'NOT_FOUND', `任务不存在: ${taskId}`)
|
|
101
|
+
const patch: TaskPatch = {}
|
|
102
|
+
if (progress !== undefined) patch.progress = progress
|
|
103
|
+
if (artifact) patch.artifacts = [...task.artifacts, artifact]
|
|
104
|
+
if (message) {
|
|
105
|
+
patch.history = [
|
|
106
|
+
...task.history,
|
|
107
|
+
{ messageId: randomUUID(), contextId: task.channelId, role: 'ROLE_AGENT' as const, parts: [{ text: message }] },
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
tasks.update(taskId, patch)
|
|
111
|
+
return engine.get(taskId)!
|
|
112
|
+
},
|
|
113
|
+
completeTask: async (taskId, artifacts) => {
|
|
114
|
+
const completed = engine.complete(taskId, artifacts)
|
|
115
|
+
if (completed.parentId) {
|
|
116
|
+
engine.onChildCompleted(completed)
|
|
117
|
+
const parent = engine.get(completed.parentId)
|
|
118
|
+
if (parent) cr.getAgents().find(a => a.agentId === parent.assigneeId)?.wakeMailbox()
|
|
119
|
+
}
|
|
120
|
+
return completed
|
|
121
|
+
},
|
|
122
|
+
cancelTask: async taskId => engine.cancel(taskId, agent.id),
|
|
123
|
+
myQueue: async () => engine.queueViewOf(agent.channelId, agent.id),
|
|
124
|
+
queueOverview: async () => [],
|
|
125
|
+
updateTask: async (taskId, patch) => engine.updateTask(taskId, patch, agent.id),
|
|
126
|
+
reassignTask: async (taskId, toAgentId) => engine.reassign(taskId, toAgentId),
|
|
127
|
+
sendMessage: async ({ toAgentId, parts, metadata }) => {
|
|
128
|
+
const message: A2AMessage = {
|
|
129
|
+
messageId: randomUUID(),
|
|
130
|
+
contextId: agent.channelId,
|
|
131
|
+
role: 'ROLE_AGENT',
|
|
132
|
+
parts,
|
|
133
|
+
metadata: { ...(metadata ?? {}), 'x-aw-target-agent': toAgentId, 'x-aw-from-agent': agent.id },
|
|
134
|
+
}
|
|
135
|
+
cr.route(message)
|
|
136
|
+
return message
|
|
137
|
+
},
|
|
138
|
+
pollMailbox: async (limit = 100) =>
|
|
139
|
+
messages.listPendingByChannelAgent(agent.channelId, agent.id).slice(0, limit).map(rowToMessage),
|
|
140
|
+
listMail: async (opts) => {
|
|
141
|
+
const limit = Math.max(1, Math.min(500, opts?.limit ?? 200))
|
|
142
|
+
const rows = messages.listRecentByChannel(agent.channelId, limit)
|
|
143
|
+
const mails: ChannelMail[] = rows.map(r => ({
|
|
144
|
+
messageId: r.id,
|
|
145
|
+
taskId: r.taskId,
|
|
146
|
+
fromAgentId: r.fromAgentId,
|
|
147
|
+
toAgentId: r.toAgentId,
|
|
148
|
+
role: r.role as 'ROLE_USER' | 'ROLE_AGENT',
|
|
149
|
+
parts: (JSON.parse(r.partsJson) ?? []) as ChannelMail['parts'],
|
|
150
|
+
metadata: (JSON.parse(r.metadataJson) ?? {}) as Record<string, unknown>,
|
|
151
|
+
state: r.state,
|
|
152
|
+
createdAt: r.createdAt,
|
|
153
|
+
consumedAt: r.consumedAt,
|
|
154
|
+
}))
|
|
155
|
+
return opts?.agentId
|
|
156
|
+
? mails.filter(m => m.fromAgentId === opts.agentId || m.toAgentId === opts.agentId)
|
|
157
|
+
: mails
|
|
158
|
+
},
|
|
159
|
+
subscribe: async () => {},
|
|
160
|
+
recallMemory: async () => [],
|
|
161
|
+
saveMemory: async () => { throw new Error('unused') },
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface Setup {
|
|
166
|
+
engine: TaskEngine
|
|
167
|
+
cr: ChannelRuntime
|
|
168
|
+
lead: AgentRuntime
|
|
169
|
+
worker: AgentRuntime
|
|
170
|
+
channelId: string
|
|
171
|
+
loop: SchedulerLoop
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface SetupOptions {
|
|
175
|
+
tickMs?: number
|
|
176
|
+
leadImpl?: AgentInterface
|
|
177
|
+
workerImpl?: AgentInterface
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function setup(opts: SetupOptions = {}): Setup {
|
|
181
|
+
const db = openWorkshopDb(':memory:')
|
|
182
|
+
const channels = createChannelRepo(db)
|
|
183
|
+
const agents = createAgentRepo(db)
|
|
184
|
+
const tasks = createTaskRepo(db)
|
|
185
|
+
const messages = createMessageRepo(db)
|
|
186
|
+
const subscriptions = createSubscriptionRepo(db)
|
|
187
|
+
const engine = new TaskEngine({ tasks, messages })
|
|
188
|
+
const channel = channels.create({ name: 'test-channel' })
|
|
189
|
+
|
|
190
|
+
const channelAgents = createChannelAgentRepo(db)
|
|
191
|
+
const leadRow = agents.create({ name: 'lead', harness: 'mock', config: { delayMs: 0 } })
|
|
192
|
+
const workerRow = agents.create({ name: 'worker', harness: 'mock', config: { delayMs: 0 } })
|
|
193
|
+
// 实例行(独立身份 id)才是运行时身份:lead 派发时 assignee 用的是实例 id
|
|
194
|
+
const leadInst = channelAgents.create({ channelId: channel.id, templateId: leadRow.id, name: leadRow.name, harness: leadRow.harness, config: { delayMs: 0 }, role: 'lead' })
|
|
195
|
+
const workerInst = channelAgents.create({ channelId: channel.id, templateId: workerRow.id, name: workerRow.name, harness: workerRow.harness, config: { delayMs: 0 }, role: 'worker' })
|
|
196
|
+
const leadInfo = rowToAgentInfo(leadInst, channel.id, 'lead')
|
|
197
|
+
const workerInfo = rowToAgentInfo(workerInst, channel.id, 'worker')
|
|
198
|
+
|
|
199
|
+
const cr = new ChannelRuntime(channel.id, { taskEngine: engine, subscriptionRepo: subscriptions, channelAgents })
|
|
200
|
+
const bus: ChannelBus = { emit: () => {}, onEvent: () => () => {}, notifyTask: () => {}, notifyAgent: () => {}, onAgentStatus: () => {}, onTaskEvent: () => {}, wakeScheduler: () => {} }
|
|
201
|
+
|
|
202
|
+
const lead = new AgentRuntime(leadInfo, opts.leadImpl ?? new MockAgentImpl(leadInfo.config), {
|
|
203
|
+
mailbox: new Mailbox(messages, leadInfo.channelId, leadInfo.id, () => cr.wakeScheduler()),
|
|
204
|
+
taskEngine: engine,
|
|
205
|
+
bus,
|
|
206
|
+
workspace: buildWorkspace(leadInfo, { engine, cr, tasks, messages }),
|
|
207
|
+
})
|
|
208
|
+
const worker = new AgentRuntime(workerInfo, opts.workerImpl ?? new MockAgentImpl(workerInfo.config), {
|
|
209
|
+
mailbox: new Mailbox(messages, workerInfo.channelId, workerInfo.id, () => cr.wakeScheduler()),
|
|
210
|
+
taskEngine: engine,
|
|
211
|
+
bus,
|
|
212
|
+
workspace: buildWorkspace(workerInfo, { engine, cr, tasks, messages }),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
cr.addAgent(lead)
|
|
216
|
+
cr.addAgent(worker)
|
|
217
|
+
lead.start()
|
|
218
|
+
worker.start()
|
|
219
|
+
|
|
220
|
+
const loop = new SchedulerLoop(cr, lead, { tickMs: opts.tickMs ?? 10 })
|
|
221
|
+
cr.scheduler = loop
|
|
222
|
+
|
|
223
|
+
return { engine, cr, lead, worker, channelId: channel.id, loop }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function teardown(s: Setup): Promise<void> {
|
|
227
|
+
s.loop.stop()
|
|
228
|
+
await Promise.all([s.lead.stop(), s.worker.stop()])
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function submitTask(s: Setup, title: string) {
|
|
232
|
+
return s.engine.create({
|
|
233
|
+
channelId: s.channelId,
|
|
234
|
+
creatorId: '',
|
|
235
|
+
assigneeId: s.lead.agentId,
|
|
236
|
+
title,
|
|
237
|
+
description: '统筹交付',
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 提交 loop 模式主任务(描述按 [mode:loop][interval:..][max:..] 编码,与 manager.submitChannelTask 同构) */
|
|
242
|
+
function submitLoopTask(s: Setup, title: string, config: ModeConfig) {
|
|
243
|
+
return s.engine.create({
|
|
244
|
+
channelId: s.channelId,
|
|
245
|
+
creatorId: '',
|
|
246
|
+
assigneeId: s.lead.agentId,
|
|
247
|
+
title,
|
|
248
|
+
description: encodeTaskMode('loop', config, '循环测试任务'),
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 模拟 manager 的 loop 重提交回调:原样创建新一轮主任务(经调度循环 dispatch 给 worker) */
|
|
253
|
+
function wireLoopResubmit(s: Setup): void {
|
|
254
|
+
s.loop.setLoopResubmitCallback((title, description) => {
|
|
255
|
+
s.engine.create({
|
|
256
|
+
channelId: s.channelId,
|
|
257
|
+
creatorId: '',
|
|
258
|
+
assigneeId: s.lead.agentId,
|
|
259
|
+
title,
|
|
260
|
+
description,
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ===== 场景 =====
|
|
266
|
+
|
|
267
|
+
async function testAutoDispatchAndComplete(): Promise<void> {
|
|
268
|
+
console.log('\n--- 1. 定时 tick 自动 dispatch + worker 完成 + 父完成 ---')
|
|
269
|
+
const s = setup({ tickMs: 10 })
|
|
270
|
+
s.loop.start()
|
|
271
|
+
const parent = submitTask(s, '主任务')
|
|
272
|
+
|
|
273
|
+
const dispatched = await waitUntil(() =>
|
|
274
|
+
s.engine.list(s.channelId).some(t => t.parentId === parent.id && t.assigneeId === s.worker.agentId),
|
|
275
|
+
)
|
|
276
|
+
check('定时 tick dispatch 子任务给 worker', dispatched)
|
|
277
|
+
|
|
278
|
+
const completed = await waitUntil(() => s.engine.get(parent.id)?.state === 'COMPLETED')
|
|
279
|
+
check('worker 完成子任务 → child-completed → 父任务完成', completed, `state=${s.engine.get(parent.id)?.state}`)
|
|
280
|
+
|
|
281
|
+
const child = s.engine.list(s.channelId).find(t => t.parentId === parent.id)
|
|
282
|
+
check('子任务 COMPLETED 且进度 100', child?.state === 'COMPLETED' && child?.progress === 100, `state=${child?.state} progress=${child?.progress}`)
|
|
283
|
+
|
|
284
|
+
await teardown(s)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function testWakeTriggersRound(): Promise<void> {
|
|
288
|
+
console.log('\n--- 2. wake 立即触发一轮(大 tickMs,无定时 tick) ---')
|
|
289
|
+
const s = setup({ tickMs: 100000 })
|
|
290
|
+
s.loop.start()
|
|
291
|
+
const parent = submitTask(s, '主任务')
|
|
292
|
+
s.loop.wake()
|
|
293
|
+
|
|
294
|
+
const dispatched = await waitUntil(
|
|
295
|
+
() => s.engine.list(s.channelId).some(t => t.parentId === parent.id && t.assigneeId === s.worker.agentId),
|
|
296
|
+
2000,
|
|
297
|
+
)
|
|
298
|
+
check('wake 立即触发 dispatch(无定时 tick)', dispatched)
|
|
299
|
+
|
|
300
|
+
await teardown(s)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
class ThrowingSuperviseImpl implements AgentInterface {
|
|
304
|
+
async* run(): AsyncIterable<AgentEvent> {
|
|
305
|
+
// 无消息处理(lead 仅测试 supervise 抛错)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async supervise(): Promise<SupervisionDecision[]> {
|
|
309
|
+
throw new Error('supervise boom')
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function testSuperviseThrowFallsBackToRules(): Promise<void> {
|
|
314
|
+
console.log('\n--- 3. supervise 抛错 → 规则引擎兜底 dispatch ---')
|
|
315
|
+
const s = setup({ tickMs: 10, leadImpl: new ThrowingSuperviseImpl() })
|
|
316
|
+
s.loop.start()
|
|
317
|
+
const parent = submitTask(s, '主任务')
|
|
318
|
+
|
|
319
|
+
const dispatched = await waitUntil(() =>
|
|
320
|
+
s.engine.list(s.channelId).some(t => t.parentId === parent.id && t.assigneeId === s.worker.agentId),
|
|
321
|
+
)
|
|
322
|
+
check('supervise 抛错后规则引擎兜底 dispatch', dispatched)
|
|
323
|
+
|
|
324
|
+
await teardown(s)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
class FlakyWorkerImpl implements AgentInterface {
|
|
328
|
+
private attempts = 0
|
|
329
|
+
private readonly fallback = new MockAgentImpl({ delayMs: 100 })
|
|
330
|
+
|
|
331
|
+
async* run(request: Parameters<NonNullable<AgentInterface['run']>>[0], ctx: Parameters<NonNullable<AgentInterface['run']>>[1]): AsyncIterable<AgentEvent> {
|
|
332
|
+
if (request.message.metadata?.['x-aw-task-kind'] === 'assign' && ctx.role === 'worker' && this.attempts++ === 0) {
|
|
333
|
+
yield { kind: 'error', error: { code: 'TEST_TRANSIENT', message: 'transient test failure' } }
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
yield* this.fallback.run(request, ctx)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function testRetryKeepsParentAlive(): Promise<void> {
|
|
341
|
+
console.log('\n--- 4. 可重试子任务失败时不提前取消父任务 ---')
|
|
342
|
+
const s = setup({ tickMs: 10, workerImpl: new FlakyWorkerImpl() })
|
|
343
|
+
s.loop.start()
|
|
344
|
+
const parent = submitTask(s, '可重试主任务')
|
|
345
|
+
|
|
346
|
+
const retried = await waitUntil(() => {
|
|
347
|
+
const child = s.engine.list(s.channelId).find(t => t.parentId === parent.id)
|
|
348
|
+
return child?.retryCount === 1
|
|
349
|
+
})
|
|
350
|
+
const retriedChild = s.engine.list(s.channelId).find(t => t.parentId === parent.id)
|
|
351
|
+
check('失败子任务被重派', retried, `parent=${s.engine.get(parent.id)?.state}`)
|
|
352
|
+
check(
|
|
353
|
+
'重试期间父任务保持 WAITING',
|
|
354
|
+
s.engine.get(parent.id)?.state === 'WAITING'
|
|
355
|
+
&& (retriedChild?.state === 'ASSIGNED' || retriedChild?.state === 'WORKING'),
|
|
356
|
+
`parent=${s.engine.get(parent.id)?.state} child=${retriedChild?.state}`,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
const completed = await waitUntil(() => s.engine.get(parent.id)?.state === 'COMPLETED')
|
|
360
|
+
check('重试成功后父任务完成', completed, `state=${s.engine.get(parent.id)?.state}`)
|
|
361
|
+
|
|
362
|
+
await teardown(s)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function testStopStopsScheduling(): Promise<void> {
|
|
366
|
+
console.log('\n--- 5. stop 后不再调度 ---')
|
|
367
|
+
const s = setup({ tickMs: 10 })
|
|
368
|
+
s.loop.start()
|
|
369
|
+
const first = submitTask(s, '任务一')
|
|
370
|
+
const firstDone = await waitUntil(() => s.engine.get(first.id)?.state === 'COMPLETED')
|
|
371
|
+
check('stop 前任务完整完成', firstDone)
|
|
372
|
+
|
|
373
|
+
s.loop.stop()
|
|
374
|
+
|
|
375
|
+
const second = submitTask(s, '任务二')
|
|
376
|
+
await sleep(200)
|
|
377
|
+
const state = s.engine.get(second.id)?.state
|
|
378
|
+
const hasChild = s.engine.list(s.channelId).some(t => t.parentId === second.id)
|
|
379
|
+
check('stop 后新任务不再被 dispatch', state === 'SUBMITTED' && !hasChild, `state=${state} hasChild=${hasChild}`)
|
|
380
|
+
|
|
381
|
+
await teardown(s)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function testLoopIntervalAndMaxIterations(): Promise<void> {
|
|
385
|
+
console.log('\n--- 6. loop 模式:间隔生效 + maxIterations 后停止 ---')
|
|
386
|
+
const s = setup({ tickMs: 10 })
|
|
387
|
+
wireLoopResubmit(s)
|
|
388
|
+
s.loop.start()
|
|
389
|
+
|
|
390
|
+
const title = '循环任务'
|
|
391
|
+
const intervalMs = 300
|
|
392
|
+
const maxIterations = 2
|
|
393
|
+
const first = submitLoopTask(s, title, { intervalMs, maxIterations })
|
|
394
|
+
// mock lead dispatch 的子任务会继承父任务标题,统计主任务需排除子任务(parentId 存在)
|
|
395
|
+
const parents = (): WorkspaceTask[] =>
|
|
396
|
+
s.engine.list(s.channelId).filter(t => t.title === title && !t.parentId)
|
|
397
|
+
|
|
398
|
+
// 首轮执行完成(此时才开始等待间隔)
|
|
399
|
+
await waitUntil(() => s.engine.get(first.id)?.state === 'COMPLETED')
|
|
400
|
+
check('loop 首轮执行完成', s.engine.get(first.id)?.state === 'COMPLETED')
|
|
401
|
+
|
|
402
|
+
// 间隔后重提交:出现第 2 轮主任务,且间隔 ≥ intervalMs(从首轮完成时刻起算)
|
|
403
|
+
const firstDoneAt = Date.now()
|
|
404
|
+
const resubmitted = await waitUntil(() => {
|
|
405
|
+
const same = parents()
|
|
406
|
+
return same.length >= 2 && same[1]!.state === 'COMPLETED'
|
|
407
|
+
}, 5000)
|
|
408
|
+
const waitMs = Date.now() - firstDoneAt
|
|
409
|
+
check('间隔后重提交并完成第 2 轮', resubmitted, `主任务数=${parents().length}`)
|
|
410
|
+
check('等待时长 ≥ 配置间隔', waitMs >= intervalMs, `wait=${waitMs}ms interval=${intervalMs}ms`)
|
|
411
|
+
|
|
412
|
+
// 达到 maxIterations=2 后不再重提交:再等数个间隔,主任务数仍为 2
|
|
413
|
+
await sleep(intervalMs * 3)
|
|
414
|
+
const finalCount = parents().length
|
|
415
|
+
check('达到 maxIterations 后停止重提交', finalCount === maxIterations, `主任务数=${finalCount}(期望 ${maxIterations})`)
|
|
416
|
+
|
|
417
|
+
// 全部主任务均完成(无残留半截循环)
|
|
418
|
+
const loopTasks = parents()
|
|
419
|
+
check('所有轮次主任务均完成', loopTasks.every(t => t.state === 'COMPLETED'), loopTasks.map(t => t.state).join(','))
|
|
420
|
+
|
|
421
|
+
await teardown(s)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function testFactory(): void {
|
|
425
|
+
console.log('\n--- 6. createAgentImpl 工厂 ---')
|
|
426
|
+
const mk = (harness: string): AgentInfo => ({
|
|
427
|
+
id: randomUUID(),
|
|
428
|
+
channelId: 'ch',
|
|
429
|
+
name: 'a',
|
|
430
|
+
harness,
|
|
431
|
+
role: 'worker',
|
|
432
|
+
config: {},
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
check('mock → MockAgentImpl 实例', createAgentImpl(mk('mock')) instanceof MockAgentImpl)
|
|
436
|
+
|
|
437
|
+
check('claude → ClaudeSdkAgentImpl 实例', createAgentImpl(mk('claude')) instanceof ClaudeSdkAgentImpl)
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
createAgentImpl(mk('unknown'))
|
|
441
|
+
check('未知 harness 抛 UNKNOWN_HARNESS', false, '未抛异常')
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
const err = e as AppError
|
|
445
|
+
check(
|
|
446
|
+
'未知 harness 抛 UNKNOWN_HARNESS',
|
|
447
|
+
err instanceof AppError && err.code === 'UNKNOWN_HARNESS' && err.status === 400,
|
|
448
|
+
`code=${err?.code} status=${err?.status}`,
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function main(): Promise<void> {
|
|
454
|
+
await testAutoDispatchAndComplete()
|
|
455
|
+
await testWakeTriggersRound()
|
|
456
|
+
await testSuperviseThrowFallsBackToRules()
|
|
457
|
+
await testRetryKeepsParentAlive()
|
|
458
|
+
await testStopStopsScheduling()
|
|
459
|
+
await testLoopIntervalAndMaxIterations()
|
|
460
|
+
testFactory()
|
|
461
|
+
|
|
462
|
+
console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
|
|
463
|
+
process.exit(failures === 0 ? 0 : 1)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
main().catch((e) => {
|
|
467
|
+
console.error('测试异常:', e)
|
|
468
|
+
process.exit(1)
|
|
469
|
+
})
|