agentworkshop 0.5.5 → 0.5.6

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 (46) hide show
  1. package/.AgentWorkShop/prompts/compaction-hints.md +9 -0
  2. package/.AgentWorkShop/prompts/host-tools.json +664 -630
  3. package/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -0
  4. package/.AgentWorkShop/prompts/system-manual.md +6 -1
  5. package/.output/.AgentWorkShop/prompts/compaction-hints.md +9 -0
  6. package/.output/.AgentWorkShop/prompts/host-tools.json +664 -630
  7. package/.output/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -0
  8. package/.output/.AgentWorkShop/prompts/system-manual.md +6 -1
  9. package/.output/nitro.json +1 -1
  10. package/.output/public/_nuxt/builds/latest.json +1 -1
  11. package/.output/public/_nuxt/builds/meta/d50c6ec2-801c-4ba2-97d8-a4db401b702c.json +1 -0
  12. package/.output/server/chunks/_/nitro.mjs +1920 -1211
  13. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  14. package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs +25 -3
  15. package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs.map +1 -1
  16. package/.output/server/package.json +1 -1
  17. package/package.json +1 -1
  18. package/scripts/_dbg-cross-channel-e2e.mjs +157 -0
  19. package/scripts/test-context-governor.ts +176 -0
  20. package/scripts/test-memory-brief-chronicle.ts +180 -0
  21. package/scripts/test-memory-dynamic.ts +3 -1
  22. package/scripts/test-memory-rrf-mmr.ts +114 -0
  23. package/server/api/workshop/channels/[id]/messages/index.post.ts +26 -1
  24. package/server/api/workshop/ws.ts +1 -0
  25. package/server/data/daqs.json +35 -13
  26. package/server/data/dcw-lines.json +14 -0
  27. package/server/data/dcw-products.json +14 -0
  28. package/server/data/dcw-recipes.json +28 -0
  29. package/server/data/dcw-rollback.json +122 -1
  30. package/server/data/dcw-runs.json +54 -36
  31. package/server/data/dcw-writes.json +60 -0
  32. package/server/data/dcws.json +42 -0
  33. package/server/data/line-runs.json +32 -22
  34. package/server/services/workshop/agents/adapters/omp-rpc-client.ts +65 -0
  35. package/server/services/workshop/agents/agent-interface.ts +26 -1
  36. package/server/services/workshop/agents/mock-agent.ts +12 -0
  37. package/server/services/workshop/agents/omp-agent.ts +277 -8
  38. package/server/services/workshop/db/database.ts +16 -1
  39. package/server/services/workshop/db/memory.repo.ts +22 -2
  40. package/server/services/workshop/prompts/loader.ts +6 -5
  41. package/server/services/workshop/runtime/agent-runtime.ts +52 -6
  42. package/server/services/workshop/runtime/channel-runtime.ts +14 -6
  43. package/server/services/workshop/runtime/manager.ts +192 -3
  44. package/server/services/workshop/runtime/memory.ts +313 -78
  45. package/server/services/workshop/types/task.ts +14 -0
  46. package/.output/public/_nuxt/builds/meta/f2f649c5-fd3b-4507-a3a1-8b4a194de72d.json +0 -1
@@ -0,0 +1,180 @@
1
+ /**
2
+ * 记忆持久化面测试:L0 会话简报 / 压缩摘要入库 / 团队编年史 / 维护豁免与淘汰 /
3
+ * manager 任务终态事件 → 团队域沉淀总线接线。
4
+ * 运行: npx tsx --tsconfig .nuxt/tsconfig.server.json scripts/test-memory-brief-chronicle.ts
5
+ */
6
+ import type { DatabaseSync } from 'node:sqlite'
7
+ import { openWorkshopDb } from '../server/services/workshop/db/database'
8
+ import { createMemoryRepo, TEAM_AGENT_ID } from '../server/services/workshop/db/memory.repo'
9
+ import { AgentMemory, runMemoryMaintenance, unsegmentCJK } from '../server/services/workshop/runtime/memory'
10
+ import type { AgentInterface } from '../server/services/workshop/agents/agent-interface'
11
+ import { createChannelRepo } from '../server/services/workshop/db/channel.repo'
12
+ import { createAgentRepo } from '../server/services/workshop/db/agent.repo'
13
+ import { createTeamRepo } from '../server/services/workshop/db/team.repo'
14
+ import { createTeamMemberRepo } from '../server/services/workshop/db/team-member.repo'
15
+ import { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
16
+ import { createMessageRepo } from '../server/services/workshop/db/message.repo'
17
+ import { createTaskRepo } from '../server/services/workshop/db/task.repo'
18
+ import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
19
+ import { createAgentChannelManager } from '../server/services/workshop/runtime/manager'
20
+
21
+ let failures = 0
22
+ function check(name: string, ok: boolean, detail = ''): void {
23
+ console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
24
+ if (!ok) failures += 1
25
+ }
26
+ const sleep = (ms: number): Promise<void> => new Promise(r => setTimeout(r, ms))
27
+
28
+ const db: DatabaseSync = openWorkshopDb(':memory:')
29
+ const repo = createMemoryRepo(db)
30
+ const seg = (s: string): string => s.replace(/([\u4e00-\u9fff])/g, ' $1 ')
31
+
32
+ // ===== L0 会话简报:recordTaskOutcome / save(shared) 自动刷新,幂等单行 =====
33
+ console.log('\n--- L0 会话简报(brief)---')
34
+ {
35
+ const mem = new AgentMemory(repo, { channelId: 'ch1', agentId: 'w1' })
36
+ check('无完成记录时不产简报', repo.findByAgentDedup('ch1', 'w1', 'brief:w1') === null)
37
+ await mem.recordTaskOutcome({
38
+ id: 'tk1', channelId: 'ch1', assigneeId: 'w1', creatorId: 'lead', title: '网关配置任务',
39
+ state: 'COMPLETED', progress: 100, retryCount: 0,
40
+ artifacts: [{ artifactId: 'a1', name: 'deliverable', parts: [{ text: '网关路由表已生效' }] }],
41
+ history: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
42
+ } as Parameters<AgentMemory['recordTaskOutcome']>[0])
43
+ let brief = repo.findByAgentDedup('ch1', 'w1', 'brief:w1')
44
+ check('任务终态后简报生成', brief !== null)
45
+ check('简报含最近完成条目', brief !== null && unsegmentCJK(repo.getById(brief.id)!.content).includes('网关配置任务'))
46
+ check('简报为单行幂等(brief kind)', repo.listByAgentChannel('ch1', 'w1', 100).filter(r => r.kind === 'brief').length === 1)
47
+
48
+ await mem.save({ title: '限流约定', content: '全队统一令牌桶', scope: 'shared', dedupKey: 'conv:rl' })
49
+ brief = repo.findByAgentDedup('ch1', 'w1', 'brief:w1')
50
+ check('共享沉淀后简报含团队约定', brief !== null && unsegmentCJK(repo.getById(brief.id)!.content).includes('限流约定'))
51
+
52
+ // recall L0 置顶
53
+ const block = await mem.recall('任意查询')
54
+ check('recall 注入块含会话简报', block !== null && block.includes('会话简报'))
55
+ }
56
+
57
+ // ===== 压缩摘要入库:episodic-session,序号 dedupKey,可检索 =====
58
+ console.log('\n--- 压缩摘要(episodic-session)---')
59
+ {
60
+ const mem = new AgentMemory(repo, { channelId: 'ch1', agentId: 'w1' })
61
+ const s1 = await mem.recordSessionCompaction({ summary: '完成网关配置;待办:验证路由', tokensBefore: 90000, tokensAfter: 30000, reason: 'threshold' })
62
+ const s2 = await mem.recordSessionCompaction({ summary: '第二次压缩摘要', tokensBefore: 88000 })
63
+ check('dedupKey 序号递增不互踩', s1.dedupKey !== s2.dedupKey && repo.findByAgentDedup('ch1', 'w1', s1.dedupKey) !== null && repo.findByAgentDedup('ch1', 'w1', s2.dedupKey) !== null)
64
+ const found = await mem.recallRows('网关配置 验证路由')
65
+ check('压缩摘要可被 search_memory 检索', found.some(s => s.kind === 'episodic-session'), JSON.stringify(found.map(s => s.kind)))
66
+ const row = repo.listByAgentChannel('ch1', 'w1', 100).find(r => r.kind === 'episodic-session')
67
+ check('importance=0.75 元信息入库', row !== null && row.importance === 0.75)
68
+ }
69
+
70
+ // ===== 团队编年史:滚动重写幂等单行 =====
71
+ console.log('\n--- 团队编年史(chronicle)---')
72
+ {
73
+ const mem = new AgentMemory(repo, { channelId: 'ch1', agentId: TEAM_AGENT_ID })
74
+ mem.upsertChronicle(['09-02 10:00 [完成] 任务A — w1(交付 x)', '09-02 11:00 [失败] 任务B — w2'].join('\n'))
75
+ mem.upsertChronicle(['09-02 12:00 [完成] 任务C — w3'].join('\n'))
76
+ const rows = repo.listByAgentChannel('ch1', TEAM_AGENT_ID, 100).filter(r => r.kind === 'chronicle')
77
+ check('编年史幂等单行(重写不新增)', rows.length === 1)
78
+ const chron = rows[0] !== undefined ? unsegmentCJK(rows[0]!.content) : ''
79
+ check('编年史内容为最新滚动', chron.includes('w3') && !chron.includes('任务 A'), chron)
80
+ }
81
+
82
+ // ===== 维护:session 14d 过期 / 策展层豁免 / team-task 淘汰 =====
83
+ console.log('\n--- 维护规则 v2 ---')
84
+ {
85
+ const old = new Date(Date.now() - 20 * 86_400_000).toISOString()
86
+ repo.upsert({ channelId: 'ch1', agentId: 'w1', kind: 'episodic-session', title: '旧压缩摘要', titleFts: seg('旧压缩摘要'), content: seg('过期内容'), importance: 0.75, taskId: null, dedupKey: 'session:w1:c99' })
87
+ db.prepare('UPDATE agent_memories SET created_at = ? WHERE dedup_key = ?').run(old, 'session:w1:c99')
88
+
89
+ const res = runMemoryMaintenance(repo, { expireDays: 180, cap: 500 })
90
+ check('episodic-session 14d 过期删除', res.deletedExpired >= 1 && repo.findByAgentDedup('ch1', 'w1', 'session:w1:c99') === null, JSON.stringify(res))
91
+ check('brief 豁免保留', repo.findByAgentDedup('ch1', 'w1', 'brief:w1') !== null)
92
+ check('chronicle 豁免保留', repo.listByAgentChannel('ch1', TEAM_AGENT_ID, 100).some(r => r.kind === 'chronicle'))
93
+
94
+ // team-task 容量淘汰(cap=2 时仅保留 2 行)
95
+ for (let i = 0; i < 4; i++) {
96
+ repo.upsert({ channelId: 'ch1', agentId: TEAM_AGENT_ID, kind: 'episodic-team-task', title: `团队任务${i}`, titleFts: seg(`团队任务${i}`), content: seg('成果'), importance: i < 2 ? 0.9 : 0.5, taskId: `tt-${i}`, dedupKey: `team-task:tt-${i}` })
97
+ }
98
+ runMemoryMaintenance(repo, { expireDays: 180, cap: 2 })
99
+ const remaining = repo.listByAgentChannel('ch1', TEAM_AGENT_ID, 100).filter(r => r.kind === 'episodic-team-task')
100
+ check('team episodic 纳入容量淘汰(高分保留)', remaining.length === 2 && remaining.every(r => r.importance === 0.9), `left=${remaining.length}`)
101
+ }
102
+
103
+ // ===== manager 总线接线:任务终态 → team-task 行 + 编年史(单点不双写)=====
104
+ console.log('\n--- manager 任务终态团队沉淀 ---')
105
+ {
106
+ const tmRepos = {
107
+ channels: createChannelRepo(db),
108
+ agents: createAgentRepo(db),
109
+ teams: createTeamRepo(db),
110
+ teamMembers: createTeamMemberRepo(db),
111
+ channelAgents: createChannelAgentRepo(db),
112
+ messages: createMessageRepo(db),
113
+ subscriptions: createSubscriptionRepo(db),
114
+ tasks: createTaskRepo(db),
115
+ memories: repo,
116
+ }
117
+ // 空转 impl:run 即返回(lead 自派任务的回合立即落定,不产错误)
118
+ const stubImpl = (): AgentInterface => ({ async* run() {} })
119
+ const manager = createAgentChannelManager({ repos: tmRepos, implFactory: stubImpl, db })
120
+ const ch = tmRepos.channels.create({ name: 'ch-harvest' })
121
+ const lead = tmRepos.channelAgents.create({ channelId: ch.id, templateId: null, name: 'lead-h', harness: 'mock', role: 'lead' })
122
+ // 激活 channel runtime(创建 bus + 注册团队沉淀监听)
123
+ manager.subscribeTaskEvents(ch.id, () => {})
124
+
125
+ const task = await manager.dispatchTask(ch.id, lead.id, { assigneeId: lead.id, title: '数据迁移执行', description: '迁移并验证' })
126
+ await sleep(30)
127
+ await manager.completeTask(ch.id, lead.id, {
128
+ taskId: task.id,
129
+ artifacts: [{ artifactId: 'd1', name: 'deliverable', parts: [{ text: '迁移完成,校验和一致' }] }],
130
+ })
131
+ await sleep(30)
132
+
133
+ const teamRow = repo.findByAgentDedup(ch.id, TEAM_AGENT_ID, `team-task:${task.id}`)
134
+ check('任务完成 → team-task 共享行入库', teamRow !== null)
135
+ const teamRows = repo.listByAgentChannel(ch.id, TEAM_AGENT_ID, 100).filter(r => r.kind === 'episodic-team-task')
136
+ check('团队域单点写入(无双写)', teamRows.length === 1, `rows=${teamRows.length}`)
137
+ check('交付物进入共享行内容', teamRow !== null && unsegmentCJK(repo.getById(teamRow.id)!.content).includes('校验和一致'))
138
+ const chronicle = repo.listByAgentChannel(ch.id, TEAM_AGENT_ID, 100).find(r => r.kind === 'chronicle')
139
+ check('编年史随任务终态滚动更新', chronicle !== undefined && unsegmentCJK(repo.getById(chronicle.id)!.content).includes('数据迁移执行'))
140
+ await manager.shutdown()
141
+ }
142
+
143
+ // ===== 空闲反思:当月 episodic-task ≥ 阈值 → reflection 行聚合(月度幂等)=====
144
+ console.log('\n--- 空闲反思(reflection)---')
145
+ {
146
+ const month = new Date().toISOString().slice(0, 7)
147
+ for (let i = 0; i < 8; i++) {
148
+ repo.upsert({ channelId: 'ch1', agentId: 'rx', kind: 'episodic-task', title: `反思任务${i}`, titleFts: seg(`反思任务${i}`), content: seg('结论性内容'), importance: 0.8, taskId: `rx-${i}`, dedupKey: `task:rx-${i}` })
149
+ }
150
+ const stubImpl = (): AgentInterface => ({ async* run() {} })
151
+ const m2 = createAgentChannelManager({
152
+ repos: {
153
+ channels: createChannelRepo(db),
154
+ agents: createAgentRepo(db),
155
+ teams: createTeamRepo(db),
156
+ teamMembers: createTeamMemberRepo(db),
157
+ channelAgents: createChannelAgentRepo(db),
158
+ messages: createMessageRepo(db),
159
+ subscriptions: createSubscriptionRepo(db),
160
+ tasks: createTaskRepo(db),
161
+ memories: repo,
162
+ },
163
+ implFactory: stubImpl,
164
+ db,
165
+ })
166
+ ;(m2 as unknown as Record<string, () => void>).reflectIdleMemories()
167
+ const reflRef = repo.findByAgentDedup('ch1', 'rx', `reflection:rx:${month}`)
168
+ const refl = reflRef ? repo.getById(reflRef.id) : null
169
+ check('反思行入库(月度 dedupKey)', refl !== null)
170
+ check('反思行聚合任务结论', refl !== null && unsegmentCJK(refl.content).includes('反思任务'))
171
+
172
+ // 再跑一轮:无新增任务 → 不重复聚合(内容不变,幂等)
173
+ ;(m2 as unknown as Record<string, () => void>).reflectIdleMemories()
174
+ const reflCount = repo.listByAgentChannel('ch1', 'rx', 100).filter(r => r.kind === 'reflection').length
175
+ check('反思月度幂等(无增量不再重写)', reflCount === 1)
176
+ await m2.shutdown()
177
+ }
178
+
179
+ console.log(failures === 0 ? '\nALL PASS' : `\n${failures} FAILED`)
180
+ process.exit(failures === 0 ? 0 : 1)
@@ -89,7 +89,9 @@ console.log('\n--- 记忆引子 primer(小预算注入)---')
89
89
  const block = await memA1.recall('限流 令牌桶 部署', { touch: false })
90
90
  check('primer 返回记忆块', block !== null)
91
91
  check('primer 含按需抓取工具提示', block !== null && block.includes('search_memory'), block?.slice(-80))
92
- check('primer 小预算(默认300 tokens,行数受限)', block !== null && block.split('\n').length < 10, `lines=${block?.split('\n').length}`)
92
+ // v2 三层注入:L0 简报(save(shared) 后自动存在)+ L1 引子 + 提示,同在 300 tok 引子预算 + 500 tok 总预算内
93
+ check('primer 含 L0 会话简报', block !== null && block.includes('会话简报'))
94
+ check('primer 小预算(行数受限)', block !== null && block.split('\n').length < 16, `lines=${block?.split('\n').length}`)
93
95
 
94
96
  const memTiny = new AgentMemory(repo, { channelId: 'ch1', agentId: 'a1', budgetTokens: 5 })
95
97
  check('极小预算 primer 无行则 null', (await memTiny.recall('限流')) === null)
@@ -0,0 +1,114 @@
1
+ /**
2
+ * 记忆检索 v2 算法测试:RRF 融合 / MMR 多样性 / kind 感知半衰期 / 任务关联加权 / 策展层排除。
3
+ * 运行: npx tsx --tsconfig .nuxt/tsconfig.server.json scripts/test-memory-rrf-mmr.ts
4
+ */
5
+ import type { DatabaseSync } from 'node:sqlite'
6
+ import { openWorkshopDb } from '../server/services/workshop/db/database'
7
+ import { createMemoryRepo, TEAM_AGENT_ID } from '../server/services/workshop/db/memory.repo'
8
+ import { AgentMemory, buildMatchQuery, estimateTokens } from '../server/services/workshop/runtime/memory'
9
+
10
+ let failures = 0
11
+ function check(name: string, ok: boolean, detail = ''): void {
12
+ console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
13
+ if (!ok) failures += 1
14
+ }
15
+
16
+ const db: DatabaseSync = openWorkshopDb(':memory:')
17
+ const repo = createMemoryRepo(db)
18
+ const seg = (s: string): string => s.replace(/([\u4e00-\u9fff])/g, ' $1 ')
19
+
20
+ // ===== buildMatchQuery:CJK 词项扩容 + 去重 =====
21
+ console.log('\n--- buildMatchQuery(CJK 词项扩容)---')
22
+ {
23
+ const q = buildMatchQuery('网关限流方案讨论,部署脚本与令牌桶阈值约定')
24
+ const termCount = q ? q.split(' OR ').length : 0
25
+ check('CJK 单字词项上限提升(>12)', termCount > 12, `terms=${termCount}`)
26
+ const deduped = buildMatchQuery('限流 限流 限流')
27
+ check('词项去重', deduped !== null && deduped.split(' OR ').length === 2, `terms=${deduped}`) // 限/流
28
+ check('token 估算为正', estimateTokens('abc 中文') > 0)
29
+ }
30
+
31
+ // ===== RRF 融合:FTS+向量双榜融合(词袋 embedder)=====
32
+ console.log('\n--- RRF 融合(混合检索)---')
33
+ {
34
+ // hash 词袋 provider:同词集 → 相近向量(零网络)
35
+ const { createHashEmbeddingProvider } = await import('../server/services/workshop/runtime/embedding-provider')
36
+ const embedder = createHashEmbeddingProvider(64)
37
+
38
+ const rows = [
39
+ { dedup: 'task:a', title: 'Redis 缓存穿透方案', content: '布隆过滤器前置拦截,空值缓存五分钟' },
40
+ { dedup: 'task:b', title: 'Redis 雪崩预案', content: '过期时间加随机抖动,多级缓存兜底' },
41
+ { dedup: 'task:c', title: '部署脚本清单', content: 'docker compose 拉镜像,健康检查后切流量' },
42
+ ]
43
+ for (const r of rows) {
44
+ repo.upsert({ channelId: 'ch1', agentId: 'a1', kind: 'episodic-task', title: r.title, titleFts: seg(r.title), content: seg(r.content), importance: 0.8, taskId: r.dedup, dedupKey: `task:${r.dedup}` })
45
+ }
46
+ const mem = new AgentMemory(repo, { channelId: 'ch1', agentId: 'a1', embedder })
47
+ // 向量可命中(词袋相似)但 FTS 无词面匹配的查询:RRF 仍能通过向量榜给出结果
48
+ const hits = await mem.recallRows('cache 穿透 拦截')
49
+ check('向量榜独有命中可达(RRF 不丢向量结果)', hits.some(s => s.title === 'Redis 缓存穿透方案'), JSON.stringify(hits.map(s => s.title)))
50
+
51
+ // 双榜同时命中:融合排序应把两榜都命中的排前
52
+ const both = await mem.recallRows('Redis 雪崩')
53
+ check('双榜命中排序靠前', both.length > 0 && both[0]!.title === 'Redis 雪崩预案', JSON.stringify(both.map(s => `${s.title}:${s.score}`)))
54
+ }
55
+
56
+ // ===== kind 感知半衰期:semantic 不衰减,episodic-task 14d =====
57
+ console.log('\n--- kind 感知半衰期 ---')
58
+ {
59
+ const old = new Date(Date.now() - 60 * 86_400_000).toISOString()
60
+ repo.upsert({ channelId: 'ch1', agentId: 'old1', kind: 'semantic', title: '六十天前的架构决策', titleFts: seg('六十天前的架构决策'), content: seg('内核结论:单写多读'), importance: 0.9, taskId: null, dedupKey: 'manual:old-sem' })
61
+ repo.upsert({ channelId: 'ch1', agentId: 'old1', kind: 'episodic-task', title: '六十天前的任务', titleFts: seg('六十天前的任务'), content: seg('过程性记录,内容已过时'), importance: 0.9, taskId: 't-old', dedupKey: 'task:t-old' })
62
+ // 直接改 created_at 模拟老化(upsert ON CONFLICT 会刷新,故用参数化 UPDATE)
63
+ db.prepare('UPDATE agent_memories SET created_at = ? WHERE agent_id = ?').run(old, 'old1')
64
+
65
+ const mem = new AgentMemory(repo, { channelId: 'ch1', agentId: 'old1' })
66
+ const hits = await mem.recallRows('六十天前')
67
+ const sem = hits.find(s => s.title === '六十天前的架构决策')
68
+ const epi = hits.find(s => s.title === '六十天前的任务')
69
+ check('semantic 知识不随时近衰减(分高于 episodic)', sem !== undefined && epi !== undefined && sem.score > epi.score, `sem=${sem?.score} epi=${epi?.score}`)
70
+ }
71
+
72
+ // ===== 任务关联加权:兄弟/父任务记忆置顶 =====
73
+ console.log('\n--- 任务关联加权(related-task boost)---')
74
+ {
75
+ repo.upsert({ channelId: 'ch1', agentId: 'rel', kind: 'episodic-task', title: '无关旧任务', titleFts: seg('无关旧任务'), content: seg('数据库迁移完成,版本升到 v14'), importance: 0.8, taskId: 't-other', dedupKey: 'task:t-other' })
76
+ repo.upsert({ channelId: 'ch1', agentId: 'rel', kind: 'episodic-task', title: '兄弟任务结论', titleFts: seg('兄弟任务结论'), content: seg('API 网关路由表已生成'), importance: 0.55, taskId: 't-sib1', dedupKey: 'task:t-sib1' })
77
+ const mem = new AgentMemory(repo, { channelId: 'ch1', agentId: 'rel' })
78
+ // 查询词面同时弱命中两行;兄弟任务 id 加权后必须反超高重要度旧行
79
+ const hits = await mem.recallRows('任务 结论 数据库 网关', { relatedTaskIds: ['t-sib1', 't-parent'] })
80
+ check('兄弟任务记忆加权反超(置顶)', hits[0]?.title === '兄弟任务结论', JSON.stringify(hits.map(s => `${s.title}:${s.score}`)))
81
+ const hitsNoRel = await mem.recallRows('任务 结论 数据库 网关', { touch: false })
82
+ check('无关联集时高重要度旧行居前(对照)', hitsNoRel[0]?.title === '无关旧任务', JSON.stringify(hitsNoRel.map(s => `${s.title}:${s.score}`)))
83
+ }
84
+
85
+ // ===== 策展层:brief 走 L0 置顶,L1 引子不重复;chronicle 不进 L1 但 L2 可检索 =====
86
+ console.log('\n--- 策展层(L0/L1/L2 分层)---')
87
+ {
88
+ const mem = new AgentMemory(repo, { channelId: 'ch1', agentId: 'cur' })
89
+ await mem.save({ title: '普通结论', content: '普通检索应该能命中', scope: 'private', dedupKey: 'note:normal' })
90
+ mem.repo.upsert({ channelId: 'ch1', agentId: 'cur', kind: 'brief', title: '会话简报', titleFts: seg('会话简报'), content: seg('最近完成:普通结论'), importance: 0.6, taskId: null, dedupKey: 'brief:cur' })
91
+ mem.repo.upsert({ channelId: 'ch1', agentId: 'cur', kind: 'chronicle', title: '团队编年史', titleFts: seg('团队编年史'), content: seg('完成 普通结论'), importance: 0.9, taskId: null, dedupKey: 'chronicle:ch1' })
92
+
93
+ // L1(recall):brief 经 L0 路径恰好注入一次(去重靠 excludeCurated);chronicle 不进 L1
94
+ const primer = await mem.recall('普通')
95
+ const briefHits = (primer?.match(/会话简报/g) ?? []).length
96
+ check('brief 恰好注入一次(L0 路径)', briefHits === 1, `hits=${briefHits}`)
97
+ check('L1 引子不含 chronicle(防双份注入)', primer !== null && !primer.includes('编年史'))
98
+
99
+ // L2(search_memory/recallRows):策展层可被检索(特性)
100
+ const found = await mem.recallRows('编年史')
101
+ check('L2 search_memory 可命中 chronicle', found.some(s => s.title === '团队编年史'), JSON.stringify(found.map(s => s.title)))
102
+ }
103
+
104
+ // ===== team 域可见性:episodic-team-task 对全员 shared 可检索 =====
105
+ console.log('\n--- team-task 共享域 ---')
106
+ {
107
+ repo.upsert({ channelId: 'ch1', agentId: TEAM_AGENT_ID, kind: 'episodic-team-task', title: '团队任务成果', titleFts: seg('团队任务成果'), content: seg('数据迁移已全部完成并验证'), importance: 0.8, taskId: 't-team', dedupKey: 'team-task:t-team' })
108
+ const memB = new AgentMemory(repo, { channelId: 'ch1', agentId: 'member-b' })
109
+ const seen = await memB.recallRows('数据迁移', { scope: 'shared' })
110
+ check('其他成员 shared 域可命中 team-task 行', seen.some(s => s.title === '团队任务成果'), JSON.stringify(seen.map(s => s.title)))
111
+ }
112
+
113
+ console.log(failures === 0 ? '\nALL PASS' : `\n${failures} FAILED`)
114
+ process.exit(failures === 0 ? 0 : 1)
@@ -15,7 +15,7 @@ import { defineApiHandler } from '../../../../../utils/response'
15
15
  import { getWorkshopManager } from '../../../../../plugins/workshop'
16
16
 
17
17
  const sendMessageSchema = z.object({
18
- toAgentId: z.string().min(1, 'toAgentId 必填'),
18
+ toAgentId: z.string().min(1, 'toAgentId 必填').optional(),
19
19
  text: z.string().min(1, 'text 必填'),
20
20
  fromAgentId: z.string().optional(),
21
21
  /** 人类发送者显示名(时间线 a2a.message 的发送者归属 → x-aw-from-label) */
@@ -23,6 +23,11 @@ const sendMessageSchema = z.object({
23
23
  priority: z.enum(['immediate', 'task']).default('task'),
24
24
  /** 触发器:要求接收方回执(执行结果 + 所需内容,in_reply_to 关联) */
25
25
  requireReply: z.boolean().optional(),
26
+ /**
27
+ * 跨 Channel 通信(仅 lead):指定目标 channel(id 或名字)→ 消息直投其 lead mailbox。
28
+ * 与 toAgentId 二选一;fromAgentId 必填且必须是本 channel 的 lead(worker 拒绝)。
29
+ */
30
+ toChannelId: z.string().optional(),
26
31
  })
27
32
 
28
33
  export default defineApiHandler(async (event) => {
@@ -33,6 +38,26 @@ export default defineApiHandler(async (event) => {
33
38
  const channel = manager.getChannelForUser(channelId, user.id)
34
39
  manager.requireOwned(channel.ownerUserId, user.id, 'channel')
35
40
 
41
+ // 跨 Channel 分支:lead 专属;目标解析(id/名字)与投递在 manager 内完成
42
+ if (body.toChannelId) {
43
+ if (body.toAgentId) {
44
+ throw new AppError(400, 'BAD_REQUEST', 'toChannelId 与 toAgentId 二选一,不可同时指定')
45
+ }
46
+ if (!body.fromAgentId) {
47
+ throw new AppError(403, 'ROLE_FORBIDDEN', '跨 Channel 通信必须以本 channel Leader 身份发送(fromAgentId 必填)')
48
+ }
49
+ const fromAgentId = manager.resolveChannelMember(channelId, body.fromAgentId).id
50
+ return manager.sendCrossChannelMessage(channelId, fromAgentId, {
51
+ toChannelId: body.toChannelId,
52
+ parts: [{ text: body.text }],
53
+ requireReply: body.requireReply,
54
+ })
55
+ }
56
+
57
+ if (!body.toAgentId) {
58
+ throw new AppError(400, 'BAD_REQUEST', 'toAgentId 必填(跨 Channel 请改用 toChannelId)')
59
+ }
60
+
36
61
  // 目标寻址(容错:精确实例 id / 模板 id / 名字 / 唯一名字前缀)
37
62
  const target = manager.resolveChannelMember(channelId, body.toAgentId)
38
63
  // 发送方身份校验:显式 fromAgentId 必须可解析到本 channel 成员 ——
@@ -379,6 +379,7 @@ function bindStreamSubscriptions(manager: AgentChannelManager, stream: ChannelSt
379
379
  currentTaskProgress: e.currentTaskProgress ?? null,
380
380
  queued: e.queuedCount ?? 0,
381
381
  completed: e.completedCount ?? 0,
382
+ context: e.context ?? null,
382
383
  }, { agentId: e.agentId })))
383
384
  // harness 事件流(message/artifact/status.message/error)
384
385
  stream.unsubs.push(manager.subscribeChannelEvents(channelId, (event, source) => mapAgentEvent(manager, stream, event, source)))
@@ -1245,7 +1245,7 @@
1245
1245
  "driverConfig": {},
1246
1246
  "lineId": "ln-c377a6bf",
1247
1247
  "value": 169.8,
1248
- "state": "alarm",
1248
+ "state": "ok",
1249
1249
  "lastAt": "2026-09-02T10:43:19.807Z",
1250
1250
  "createdAt": "2026-09-01T03:23:18.165Z"
1251
1251
  },
@@ -1533,7 +1533,7 @@
1533
1533
  },
1534
1534
  "lineId": "ln-9224d3d7",
1535
1535
  "value": 66,
1536
- "state": "offline",
1536
+ "state": "alarm",
1537
1537
  "lastAt": "2026-09-01T09:07:35.996Z",
1538
1538
  "createdAt": "2026-09-01T05:59:24.304Z"
1539
1539
  },
@@ -1613,7 +1613,7 @@
1613
1613
  },
1614
1614
  "lineId": "ln-386c53ed",
1615
1615
  "value": 66,
1616
- "state": "offline",
1616
+ "state": "alarm",
1617
1617
  "lastAt": "2026-09-01T09:07:35.996Z",
1618
1618
  "createdAt": "2026-09-01T06:05:53.246Z"
1619
1619
  },
@@ -1693,7 +1693,7 @@
1693
1693
  },
1694
1694
  "lineId": "ln-5957f6db",
1695
1695
  "value": 66,
1696
- "state": "offline",
1696
+ "state": "alarm",
1697
1697
  "lastAt": "2026-09-01T09:07:35.996Z",
1698
1698
  "createdAt": "2026-09-01T06:09:02.297Z"
1699
1699
  },
@@ -1773,7 +1773,7 @@
1773
1773
  },
1774
1774
  "lineId": "ln-9ec42f08",
1775
1775
  "value": 66,
1776
- "state": "offline",
1776
+ "state": "alarm",
1777
1777
  "lastAt": "2026-09-01T09:07:35.996Z",
1778
1778
  "createdAt": "2026-09-01T06:13:44.550Z"
1779
1779
  },
@@ -1853,7 +1853,7 @@
1853
1853
  },
1854
1854
  "lineId": "ln-69e37924",
1855
1855
  "value": 66,
1856
- "state": "offline",
1856
+ "state": "alarm",
1857
1857
  "lastAt": "2026-09-01T09:07:35.996Z",
1858
1858
  "createdAt": "2026-09-01T06:16:47.641Z"
1859
1859
  },
@@ -1933,7 +1933,7 @@
1933
1933
  },
1934
1934
  "lineId": "ln-c269e692",
1935
1935
  "value": 66,
1936
- "state": "offline",
1936
+ "state": "alarm",
1937
1937
  "lastAt": "2026-09-01T09:07:35.996Z",
1938
1938
  "createdAt": "2026-09-01T06:56:06.636Z"
1939
1939
  },
@@ -2013,7 +2013,7 @@
2013
2013
  },
2014
2014
  "lineId": "ln-229d1637",
2015
2015
  "value": 66,
2016
- "state": "offline",
2016
+ "state": "alarm",
2017
2017
  "lastAt": "2026-09-01T09:07:35.996Z",
2018
2018
  "createdAt": "2026-09-01T06:57:29.508Z"
2019
2019
  },
@@ -2093,7 +2093,7 @@
2093
2093
  },
2094
2094
  "lineId": "ln-9546f91f",
2095
2095
  "value": 66,
2096
- "state": "offline",
2096
+ "state": "alarm",
2097
2097
  "lastAt": "2026-09-01T09:07:35.996Z",
2098
2098
  "createdAt": "2026-09-01T07:00:25.309Z"
2099
2099
  },
@@ -2173,7 +2173,7 @@
2173
2173
  },
2174
2174
  "lineId": "ln-087aad5b",
2175
2175
  "value": 66,
2176
- "state": "offline",
2176
+ "state": "alarm",
2177
2177
  "lastAt": "2026-09-01T09:07:35.996Z",
2178
2178
  "createdAt": "2026-09-01T07:09:07.518Z"
2179
2179
  },
@@ -2281,7 +2281,7 @@
2281
2281
  },
2282
2282
  "lineId": "ln-97d44f8a",
2283
2283
  "value": 66,
2284
- "state": "offline",
2284
+ "state": "alarm",
2285
2285
  "lastAt": "2026-09-01T09:01:12.950Z",
2286
2286
  "createdAt": "2026-09-01T07:13:45.697Z"
2287
2287
  },
@@ -2361,7 +2361,7 @@
2361
2361
  },
2362
2362
  "lineId": "ln-a9551770",
2363
2363
  "value": 66,
2364
- "state": "offline",
2364
+ "state": "alarm",
2365
2365
  "lastAt": "2026-09-01T09:01:12.950Z",
2366
2366
  "createdAt": "2026-09-01T07:15:40.329Z"
2367
2367
  },
@@ -2627,7 +2627,7 @@
2627
2627
  },
2628
2628
  "lineId": "ln-844ad82f",
2629
2629
  "value": 66,
2630
- "state": "offline",
2630
+ "state": "alarm",
2631
2631
  "lastAt": "2026-09-01T09:07:36.508Z",
2632
2632
  "createdAt": "2026-09-01T09:06:55.959Z"
2633
2633
  },
@@ -2984,5 +2984,27 @@
2984
2984
  "state": "ok",
2985
2985
  "lastAt": "2026-09-02T10:43:19.556Z",
2986
2986
  "createdAt": "2026-09-02T05:37:50.702Z"
2987
+ },
2988
+ {
2989
+ "id": "dn-833816c5",
2990
+ "templateRef": "daq-temp-tc",
2991
+ "name": "̽���¶�",
2992
+ "driver": "mock",
2993
+ "enabled": true,
2994
+ "intervalMs": 500,
2995
+ "publishIntervalMs": null,
2996
+ "unit": "℃",
2997
+ "decimals": 1,
2998
+ "min": 150,
2999
+ "max": 185,
3000
+ "warnLow": 152.8,
3001
+ "warnHigh": 182.2,
3002
+ "deviceBindingId": null,
3003
+ "driverConfig": {},
3004
+ "lineId": "ln-c1ef6c0d",
3005
+ "value": null,
3006
+ "state": "offline",
3007
+ "lastAt": null,
3008
+ "createdAt": "2026-09-02T11:55:28.275Z"
2987
3009
  }
2988
3010
  ]
@@ -1034,5 +1034,19 @@
1034
1034
  "color": "#41c8f4",
1035
1035
  "description": "",
1036
1036
  "createdAt": "2026-09-02T10:37:43.626Z"
1037
+ },
1038
+ {
1039
+ "id": "ln-af1e04a3",
1040
+ "name": "全功能验证线-3lqwow",
1041
+ "color": "#b58cff",
1042
+ "description": "",
1043
+ "createdAt": "2026-09-02T11:40:28.452Z"
1044
+ },
1045
+ {
1046
+ "id": "ln-c1ef6c0d",
1047
+ "name": "���̽����",
1048
+ "color": "#ff8a5c",
1049
+ "description": "",
1050
+ "createdAt": "2026-09-02T11:55:27.943Z"
1037
1051
  }
1038
1052
  ]
@@ -936,5 +936,19 @@
936
936
  "name": "全功能产品-41bdm2",
937
937
  "description": "",
938
938
  "createdAt": "2026-09-02T10:37:43.637Z"
939
+ },
940
+ {
941
+ "id": "pd-765f8dfb",
942
+ "lineId": "ln-af1e04a3",
943
+ "name": "全功能产品-3lqwow",
944
+ "description": "",
945
+ "createdAt": "2026-09-02T11:40:28.474Z"
946
+ },
947
+ {
948
+ "id": "pd-efb05c40",
949
+ "lineId": "ln-c1ef6c0d",
950
+ "name": "���̽��Ʒ",
951
+ "description": "",
952
+ "createdAt": "2026-09-02T11:55:28.108Z"
939
953
  }
940
954
  ]
@@ -1886,5 +1886,33 @@
1886
1886
  "lastGoodRunId": null,
1887
1887
  "createdAt": "2026-09-02T05:37:46.125Z",
1888
1888
  "updatedAt": "2026-09-02T05:37:46.125Z"
1889
+ },
1890
+ {
1891
+ "id": "rc-ed3e066e",
1892
+ "productId": "pd-efb05c40",
1893
+ "lineId": "ln-c1ef6c0d",
1894
+ "name": "̽���䷽",
1895
+ "description": "",
1896
+ "params": [
1897
+ {
1898
+ "nodeId": "dw-4bcbc922",
1899
+ "templateRef": "dcw-temp-sp",
1900
+ "value": 180,
1901
+ "min": 176,
1902
+ "max": 188
1903
+ }
1904
+ ],
1905
+ "daqWindows": [
1906
+ {
1907
+ "nodeId": "dn-833816c5",
1908
+ "min": 100,
1909
+ "max": 260
1910
+ }
1911
+ ],
1912
+ "version": 1,
1913
+ "paramsHistory": [],
1914
+ "lastGoodRunId": null,
1915
+ "createdAt": "2026-09-02T11:55:28.651Z",
1916
+ "updatedAt": "2026-09-02T11:55:28.651Z"
1889
1917
  }
1890
1918
  ]