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,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆系统端到端验证 — 真实 AgentChannelManager(:memory: db)+ echo harness 全链:
|
|
3
|
+
* ① channel + lead/2worker 装配,runtimes started
|
|
4
|
+
* ② 任务完成 → lead/worker 各自记忆落库
|
|
5
|
+
* ③ lead addTeamMemory('团队统一用 pnpm')
|
|
6
|
+
* ④ 相关任务下发 → echo 捕获 worker request.memory 含任务1记忆 + 团队行
|
|
7
|
+
* ⑤ worker 间 peer 消息(require_reply)→ 双方 peer 记忆落库
|
|
8
|
+
* ⑥ REST(h3 toWebHandler 挂真实路由 handler):GET memories / POST team(lead)/ POST agent / DELETE 全 2xx;非 lead 写 team → 403
|
|
9
|
+
* ⑦ runMemoryMaintenanceNow() → 回拨老数据被清(团队行豁免)
|
|
10
|
+
* ⑧ await manager.shutdown() → 干净退出
|
|
11
|
+
* 向量链(hash embedder)已由 test-memory-vector.ts 覆盖;本 E2E 走纯 FTS 路径。
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from 'node:crypto'
|
|
14
|
+
import { createChannelEventRepo } from '../server/services/workshop/db/channel-event.repo'
|
|
15
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
16
|
+
import { tmpdir } from 'node:os'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { createApp, createRouter, toWebHandler } from 'h3'
|
|
19
|
+
import { openWorkshopDb } from '../server/services/workshop/db/database'
|
|
20
|
+
import type { MemoryRow } from '../server/services/workshop/db/database'
|
|
21
|
+
import { createChannelRepo } from '../server/services/workshop/db/channel.repo'
|
|
22
|
+
import { createAgentRepo } from '../server/services/workshop/db/agent.repo'
|
|
23
|
+
import { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
|
|
24
|
+
import { createTaskRepo } from '../server/services/workshop/db/task.repo'
|
|
25
|
+
import { createMemoryRepo, TEAM_AGENT_ID } from '../server/services/workshop/db/memory.repo'
|
|
26
|
+
import { createTeamRepo } from '../server/services/workshop/db/team.repo'
|
|
27
|
+
import { createTeamMemberRepo } from '../server/services/workshop/db/team-member.repo'
|
|
28
|
+
import { createMessageRepo } from '../server/services/workshop/db/message.repo'
|
|
29
|
+
import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
|
|
30
|
+
import { createAgentChannelManager } from '../server/services/workshop/runtime/manager'
|
|
31
|
+
|
|
32
|
+
import type { AgentInfo, AgentInterface, AgentRunContext, AgentRunRequest, AgentEvent } from '../server/services/workshop/agents/agent-interface'
|
|
33
|
+
import type { A2AArtifact, Part } from '../server/services/workshop/types/a2a'
|
|
34
|
+
// 真实 REST 路由 handler(生产同源;经 globalThis.__workshopManager 接测试 manager)
|
|
35
|
+
import teamMemGet from '../server/api/workshop/channels/[id]/memories/index.get'
|
|
36
|
+
import teamMemPost from '../server/api/workshop/channels/[id]/memories/index.post'
|
|
37
|
+
import teamMemDelete from '../server/api/workshop/channels/[id]/memories/[memoryId].delete'
|
|
38
|
+
import agentMemPost from '../server/api/workshop/channels/[id]/agents/[agentId]/memories/index.post'
|
|
39
|
+
import agentMemDelete from '../server/api/workshop/channels/[id]/agents/[agentId]/memories/[memoryId].delete'
|
|
40
|
+
|
|
41
|
+
let failures = 0
|
|
42
|
+
function check(name: string, ok: boolean, detail = ''): void {
|
|
43
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
44
|
+
if (!ok) failures += 1
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms))
|
|
48
|
+
|
|
49
|
+
async function waitUntil(cond: () => boolean | Promise<boolean>, timeoutMs: number): Promise<boolean> {
|
|
50
|
+
const deadline = Date.now() + timeoutMs
|
|
51
|
+
while (Date.now() < deadline) {
|
|
52
|
+
if (await cond()) return true
|
|
53
|
+
await sleep(50)
|
|
54
|
+
}
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function partsText(parts: Part[]): string {
|
|
59
|
+
return parts.map(p => ('text' in p ? p.text : '')).filter(Boolean).join('\n')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ═══════════ echo harness:捕获 request.memory + 驱动任务/peer 剧本 ═══════════
|
|
63
|
+
|
|
64
|
+
/** worker echo:assign → 产 deliverable + completeTask;peer(require-reply)→ 回执(message 事件 + sendMessage) */
|
|
65
|
+
class EchoWorkerImpl implements AgentInterface {
|
|
66
|
+
readonly captured: AgentRunRequest[] = []
|
|
67
|
+
|
|
68
|
+
async* run(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
|
|
69
|
+
this.captured.push(request)
|
|
70
|
+
const kind = request.message.metadata?.['x-aw-task-kind']
|
|
71
|
+
if (kind === 'assign' && request.taskId) {
|
|
72
|
+
yield { kind: 'status', status: { state: 'WORKING', timestamp: new Date().toISOString() } }
|
|
73
|
+
const artifact: A2AArtifact = {
|
|
74
|
+
artifactId: randomUUID(),
|
|
75
|
+
name: 'deliverable',
|
|
76
|
+
parts: [{ text: `echo 完成(${ctx.agentId}):${partsText(request.message.parts).slice(0, 80)}` }],
|
|
77
|
+
}
|
|
78
|
+
yield { kind: 'artifact', artifact, lastChunk: true, totalChunks: 1 }
|
|
79
|
+
await ctx.workspace.completeTask(request.taskId, [artifact])
|
|
80
|
+
yield { kind: 'done', final: { taskId: request.taskId } }
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
const fromId = request.fromAgentId
|
|
84
|
+
if (!kind && fromId) {
|
|
85
|
+
if (request.message.metadata?.['x-aw-require-reply'] === 'true') {
|
|
86
|
+
const text = partsText(request.message.parts)
|
|
87
|
+
const reply = `echo 回复(${ctx.agentId}):已处理「${text.slice(0, 60)}」。执行结果:完成。本回复不需要再响应。`
|
|
88
|
+
yield { kind: 'message', message: { messageId: randomUUID(), contextId: ctx.channelId, role: 'ROLE_AGENT', parts: [{ text: reply }] } }
|
|
89
|
+
await ctx.workspace.sendMessage({
|
|
90
|
+
toAgentId: fromId,
|
|
91
|
+
parts: [{ text: reply }],
|
|
92
|
+
metadata: { 'x-aw-in-reply-to': request.message.messageId, 'x-aw-require-reply': 'false' },
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
yield { kind: 'done' }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** lead echo:child-completed → 汇总 summary + completeTask 父任务(终态后 runtime 自动沉淀 lead 记忆);调度走规则引擎兜底 */
|
|
101
|
+
class EchoLeadImpl implements AgentInterface {
|
|
102
|
+
readonly captured: AgentRunRequest[] = []
|
|
103
|
+
|
|
104
|
+
async* run(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
|
|
105
|
+
this.captured.push(request)
|
|
106
|
+
if (request.message.metadata?.['x-aw-task-kind'] === 'child-completed' && request.taskId) {
|
|
107
|
+
// 规则引擎兜底可能已先收口父任务(调度轮与 child-completed 投递并发)→ 幂等:仅非终态时汇总收口
|
|
108
|
+
const parent = await ctx.workspace.getTask(request.taskId)
|
|
109
|
+
if (parent && parent.state !== 'COMPLETED' && parent.state !== 'FAILED' && parent.state !== 'CANCELED') {
|
|
110
|
+
const summary: A2AArtifact = {
|
|
111
|
+
artifactId: randomUUID(),
|
|
112
|
+
name: 'summary',
|
|
113
|
+
parts: [{ text: `汇总:子任务完成(${partsText(request.message.parts).slice(0, 80)})` }],
|
|
114
|
+
}
|
|
115
|
+
await ctx.workspace.completeTask(request.taskId, [summary])
|
|
116
|
+
}
|
|
117
|
+
yield { kind: 'done' }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ═══════════ 装配 ═══════════
|
|
123
|
+
|
|
124
|
+
const db = openWorkshopDb(':memory:')
|
|
125
|
+
const repos = {
|
|
126
|
+
channels: createChannelRepo(db),
|
|
127
|
+
agents: createAgentRepo(db),
|
|
128
|
+
channelAgents: createChannelAgentRepo(db),
|
|
129
|
+
messages: createMessageRepo(db),
|
|
130
|
+
subscriptions: createSubscriptionRepo(db),
|
|
131
|
+
tasks: createTaskRepo(db),
|
|
132
|
+
memories: createMemoryRepo(db),
|
|
133
|
+
|
|
134
|
+
channelEvents: createChannelEventRepo(db),
|
|
135
|
+
teams: createTeamRepo(db),
|
|
136
|
+
teamMembers: createTeamMemberRepo(db),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const echoByAgent = new Map<string, EchoWorkerImpl | EchoLeadImpl>()
|
|
140
|
+
const implFactory = (agent: AgentInfo): AgentInterface => {
|
|
141
|
+
const impl = agent.role === 'lead' ? new EchoLeadImpl() : new EchoWorkerImpl()
|
|
142
|
+
echoByAgent.set(agent.id, impl)
|
|
143
|
+
return impl
|
|
144
|
+
}
|
|
145
|
+
// C1:env 向量 provider 接线验证(127.0.0.1:1 立即拒连 → 熔断 → 全链 FTS 降级不崩)
|
|
146
|
+
const savedEmbedBase = process.env.AW_MEMORY_EMBED_BASE_URL
|
|
147
|
+
const savedEmbedModel = process.env.AW_MEMORY_EMBED_MODEL
|
|
148
|
+
process.env.AW_MEMORY_EMBED_BASE_URL = 'http://127.0.0.1:1'
|
|
149
|
+
process.env.AW_MEMORY_EMBED_MODEL = 'e2e-unreachable'
|
|
150
|
+
const manager = createAgentChannelManager({ repos, implFactory, db })
|
|
151
|
+
// REST 路由 handler 经 getWorkshopManager() 读进程级单例 → 指向测试 manager
|
|
152
|
+
globalThis.__workshopManager = manager
|
|
153
|
+
|
|
154
|
+
const tmpWorkspace = mkdtempSync(join(tmpdir(), 'aw-e2e-memory-'))
|
|
155
|
+
|
|
156
|
+
async function main(): Promise<void> {
|
|
157
|
+
// ── ① channel + lead + 2 worker 装配,runtimes started ──
|
|
158
|
+
console.log('\n=== ① channel + agents 装配 ===')
|
|
159
|
+
const ch = await manager.createChannel({ name: 'memory-e2e', workspace: tmpWorkspace })
|
|
160
|
+
const channelId = ch.channelId
|
|
161
|
+
const leadTpl = await manager.createAgent({ name: 'lead', harness: 'echo' })
|
|
162
|
+
const lead = await manager.addAgentToChannel({ channelId, agentId: leadTpl.id, role: 'lead' })
|
|
163
|
+
const mkWorker = async (name: string) => {
|
|
164
|
+
const tpl = await manager.createAgent({ name, harness: 'echo' })
|
|
165
|
+
return manager.addAgentToChannel({ channelId, agentId: tpl.id, role: 'worker' })
|
|
166
|
+
}
|
|
167
|
+
const w1 = await mkWorker('w1')
|
|
168
|
+
const w2 = await mkWorker('w2')
|
|
169
|
+
|
|
170
|
+
manager.ensureChannelActive(channelId, { tickMs: 50 })
|
|
171
|
+
const status1 = manager.runtimeStatus()
|
|
172
|
+
check('channel 装配 3 实例', (await manager.listChannelAgents(channelId)).length === 3)
|
|
173
|
+
check('lead runtime started(activeChannels + wired)', status1.activeChannels.includes(channelId) && status1.wiredAgents.includes(lead.id))
|
|
174
|
+
|
|
175
|
+
// ── ② 登录任务完成 → lead/worker 各自记忆落库 ──
|
|
176
|
+
console.log('\n=== ② 任务完成记忆落库(lead/worker) ===')
|
|
177
|
+
const submit = (title: string, description: string) => manager.submitChannelTask({ channelId, title, description })
|
|
178
|
+
const waitDone = (title: string) => waitUntil(async () =>
|
|
179
|
+
(await manager.listTasks(channelId, lead.id)).some(t => t.title === title && t.state === 'COMPLETED'), 10_000)
|
|
180
|
+
const rowsOf = (agentId: string): MemoryRow[] => repos.memories.listByAgent(agentId, 50)
|
|
181
|
+
|
|
182
|
+
await submit('实现登录页面', '为管理后台实现登录页面,含表单校验与错误提示')
|
|
183
|
+
check('任务1(登录)闭环 COMPLETED', await waitDone('实现登录页面'))
|
|
184
|
+
|
|
185
|
+
// 两个登录族任务保证两个 worker 各自有可召回的任务记忆(分发由规则引擎按负载/空闲决定)
|
|
186
|
+
await submit('实现登录页面表单校验', '补充登录页面表单校验规则')
|
|
187
|
+
check('任务1b(登录族)闭环 COMPLETED', await waitDone('实现登录页面表单校验'))
|
|
188
|
+
|
|
189
|
+
const leadRows = await waitUntil(() => rowsOf(lead.id).some(r => r.kind === 'episodic-task' && r.title === '实现登录页面'), 3000)
|
|
190
|
+
? rowsOf(lead.id)
|
|
191
|
+
: []
|
|
192
|
+
const leadRow = leadRows.find(r => r.kind === 'episodic-task' && r.title === '实现登录页面')
|
|
193
|
+
// 汇总 summary 与规则引擎兜底收口并发(谁先收口均可)→ 断言落库本身,不强求 content 来源
|
|
194
|
+
check('lead 记忆落库(父任务终态 harvest)', leadRow !== undefined && leadRow.importance === 0.8, leadRow?.title ?? '-')
|
|
195
|
+
const w1Row = rowsOf(w1.id).find(r => r.kind === 'episodic-task' && r.title.includes('登录页面'))
|
|
196
|
+
const w2Row = rowsOf(w2.id).find(r => r.kind === 'episodic-task' && r.title.includes('登录页面'))
|
|
197
|
+
check('worker 各自记忆落库(两 worker 均有登录族任务记忆)', w1Row !== undefined && w2Row !== undefined,
|
|
198
|
+
`w1=${w1Row?.title ?? '-'} w2=${w2Row?.title ?? '-'}`)
|
|
199
|
+
|
|
200
|
+
// ── ③ lead 策展团队共享记忆 ──
|
|
201
|
+
console.log('\n=== ③ lead addTeamMemory ===')
|
|
202
|
+
const teamList = manager.addTeamMemory(channelId, lead.id, {
|
|
203
|
+
title: '团队工程规范',
|
|
204
|
+
content: '团队统一用 pnpm 管理依赖,禁止 npm/yarn',
|
|
205
|
+
dedupKey: 'team:pnpm',
|
|
206
|
+
})
|
|
207
|
+
check('团队记忆入列(__team__ 域)', teamList.length === 1 && teamList[0].agentId === TEAM_AGENT_ID && teamList[0].channelId === channelId)
|
|
208
|
+
|
|
209
|
+
// ── ④ 相关任务 → worker request.memory 含任务1记忆 + 团队行 ──
|
|
210
|
+
console.log('\n=== ④ 相关任务召回注入 ===')
|
|
211
|
+
await submit('统一登录交互规范', '把统一用 pnpm 的工程规范落实到登录页面交互')
|
|
212
|
+
check('任务2(相关)闭环 COMPLETED', await waitDone('统一登录交互规范'))
|
|
213
|
+
const executorReq = [w1, w2]
|
|
214
|
+
.flatMap(w => (echoByAgent.get(w.id) as EchoWorkerImpl).captured)
|
|
215
|
+
.reverse()
|
|
216
|
+
.find(r => partsText(r.message.parts).includes('统一登录交互规范') && r.memory !== undefined)
|
|
217
|
+
check('worker request.memory 含任务1记忆 + 团队行',
|
|
218
|
+
executorReq !== undefined && executorReq.memory!.includes('登录页面') && executorReq.memory!.includes('pnpm'),
|
|
219
|
+
executorReq?.memory?.slice(0, 100))
|
|
220
|
+
|
|
221
|
+
// ── ④b env provider 接线:embed 全失败(拒连)→ 熔断 → FTS 降级不裸奔 ──
|
|
222
|
+
console.log('\n=== ④b env provider 接线降级(C1)===')
|
|
223
|
+
await submit('登录错误提示规范', '登录错误提示统一红字展示并联动表单校验')
|
|
224
|
+
check('embedder 不可达不阻任务闭环', await waitDone('登录错误提示规范'))
|
|
225
|
+
const degradedReq = [w1, w2]
|
|
226
|
+
.flatMap(w => (echoByAgent.get(w.id) as EchoWorkerImpl).captured)
|
|
227
|
+
.reverse()
|
|
228
|
+
.find(r => partsText(r.message.parts).includes('登录错误提示规范') && r.memory !== undefined)
|
|
229
|
+
check('熔断后召回仍注入记忆(纯 FTS 降级)', degradedReq !== undefined && degradedReq.memory!.includes('登录'),
|
|
230
|
+
degradedReq?.memory?.slice(0, 80))
|
|
231
|
+
|
|
232
|
+
// ── ⑤ worker 间 peer 消息(require_reply)→ 双方 peer 记忆落库 ──
|
|
233
|
+
console.log('\n=== ⑤ peer 消息双向记忆 ===')
|
|
234
|
+
await manager.sendA2A(channelId, w1.id, {
|
|
235
|
+
toAgentId: w2.id,
|
|
236
|
+
parts: [{ text: '登录页面的错误提示请统一文案,处理完回我' }],
|
|
237
|
+
metadata: { 'x-aw-require-reply': 'true' },
|
|
238
|
+
})
|
|
239
|
+
const peerDone = await waitUntil(() =>
|
|
240
|
+
rowsOf(w1.id).some(r => r.kind === 'episodic-peer') && rowsOf(w2.id).some(r => r.kind === 'episodic-peer'), 5000)
|
|
241
|
+
const w1Peer = rowsOf(w1.id).find(r => r.kind === 'episodic-peer')
|
|
242
|
+
const w2Peer = rowsOf(w2.id).find(r => r.kind === 'episodic-peer')
|
|
243
|
+
check('双方 peer 记忆落库', peerDone && w1Peer !== undefined && w2Peer !== undefined,
|
|
244
|
+
`w1=${w1Peer?.title ?? '-'} w2=${w2Peer?.title ?? '-'}`)
|
|
245
|
+
check('peer 记忆含问答内容(回执文本进 content;存储侧已 CJK 切分,断言用未切分片段)',
|
|
246
|
+
w2Peer !== undefined && w2Peer.content.includes('echo') && w2Peer.content.includes('答'),
|
|
247
|
+
w2Peer?.content.slice(0, 60))
|
|
248
|
+
|
|
249
|
+
// ── ⑥ REST 端点(h3 toWebHandler + 生产路由 handler) ──
|
|
250
|
+
console.log('\n=== ⑥ REST 记忆端点 ===')
|
|
251
|
+
const router = createRouter()
|
|
252
|
+
router.get('/api/workshop/channels/:id/memories', teamMemGet)
|
|
253
|
+
router.post('/api/workshop/channels/:id/memories', teamMemPost)
|
|
254
|
+
router.delete('/api/workshop/channels/:id/memories/:memoryId', teamMemDelete)
|
|
255
|
+
router.post('/api/workshop/channels/:id/agents/:agentId/memories', agentMemPost)
|
|
256
|
+
router.delete('/api/workshop/channels/:id/agents/:agentId/memories/:memoryId', agentMemDelete)
|
|
257
|
+
const app = createApp()
|
|
258
|
+
app.use(router)
|
|
259
|
+
const webHandler = toWebHandler(app)
|
|
260
|
+
const rest = async (method: string, path: string, token: string | null, body?: unknown): Promise<{ status: number, code: number | string }> => {
|
|
261
|
+
const res = await webHandler(new Request(`http://localhost/api/workshop${path}`, {
|
|
262
|
+
method,
|
|
263
|
+
headers: token ? { authorization: `Bearer ${token}` } : {},
|
|
264
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
265
|
+
}))
|
|
266
|
+
const json = await res.json().catch(() => ({ code: -1 })) as { code: number | string }
|
|
267
|
+
return { status: res.status, code: json.code }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const leadToken = lead.token!
|
|
271
|
+
const w1Token = w1.token!
|
|
272
|
+
check('REST token 有效(findByToken)', manager.findByToken(leadToken)?.id === lead.id && manager.findByToken(w1Token)?.id === w1.id)
|
|
273
|
+
|
|
274
|
+
const get1 = await rest('GET', `/channels/${channelId}/memories`, w1Token)
|
|
275
|
+
check('GET team memories(worker token)→ 200 + code 0', get1.status === 200 && get1.code === 0, `status=${get1.status} code=${get1.code}`)
|
|
276
|
+
const postTeam = await rest('POST', `/channels/${channelId}/memories`, leadToken,
|
|
277
|
+
{ title: 'REST 策展行', content: 'REST 写入的团队记忆', dedupKey: 'team:rest' })
|
|
278
|
+
check('POST team memory(lead token)→ 200', postTeam.status === 200 && postTeam.code === 0, `status=${postTeam.status}`)
|
|
279
|
+
const postTeamDenied = await rest('POST', `/channels/${channelId}/memories`, w1Token, { title: 'x', content: 'y' })
|
|
280
|
+
check('POST team memory(非 lead)→ 403 SCOPE_VIOLATION', postTeamDenied.status === 403 && postTeamDenied.code === 'SCOPE_VIOLATION',
|
|
281
|
+
`status=${postTeamDenied.status} code=${postTeamDenied.code}`)
|
|
282
|
+
const postAgent = await rest('POST', `/channels/${channelId}/agents/${w1.id}/memories`, leadToken,
|
|
283
|
+
{ title: 'REST 代写规范', content: '提交信息用中文', dedupKey: 'agent:rest' })
|
|
284
|
+
check('POST agent memory(lead 代写)→ 200', postAgent.status === 200 && postAgent.code === 0, `status=${postAgent.status}`)
|
|
285
|
+
const restTeamRow = manager.listTeamMemories(channelId, 50).find(r => r.title === 'REST 策展行')
|
|
286
|
+
const delTeam = await rest('DELETE', `/channels/${channelId}/memories/${restTeamRow!.id}`, leadToken)
|
|
287
|
+
check('DELETE team memory(lead)→ 200', delTeam.status === 200 && manager.listTeamMemories(channelId, 50).every(r => r.id !== restTeamRow!.id),
|
|
288
|
+
`status=${delTeam.status}`)
|
|
289
|
+
const restAgentRow = rowsOf(w1.id).find(r => r.title === 'REST 代写规范')!
|
|
290
|
+
const delAgent = await rest('DELETE', `/channels/${channelId}/agents/${w1.id}/memories/${restAgentRow.id}`, leadToken)
|
|
291
|
+
check('DELETE agent memory(lead)→ 200', delAgent.status === 200 && !rowsOf(w1.id).some(r => r.id === restAgentRow.id), `status=${delAgent.status}`)
|
|
292
|
+
|
|
293
|
+
// ── ⑦ 维护:回拨老数据 → runMemoryMaintenanceNow 清理(团队行豁免) ──
|
|
294
|
+
console.log('\n=== ⑦ 记忆维护清理 ===')
|
|
295
|
+
const oldIso = new Date(Date.now() - 400 * 86_400_000).toISOString()
|
|
296
|
+
const agedLeadRow = rowsOf(lead.id).find(r => r.kind === 'episodic-task')!
|
|
297
|
+
db.prepare(`UPDATE agent_memories SET created_at = ?, last_accessed_at = ? WHERE id = ?`).run(oldIso, oldIso, agedLeadRow.id)
|
|
298
|
+
const before = manager.listTeamMemories(channelId, 50)
|
|
299
|
+
const maint = manager.runMemoryMaintenanceNow()
|
|
300
|
+
check('老数据(400 天前)被过期清理', maint.deletedExpired >= 1 && !rowsOf(lead.id).some(r => r.id === agedLeadRow.id),
|
|
301
|
+
`deletedExpired=${maint.deletedExpired} evicted=${maint.evicted}`)
|
|
302
|
+
check('团队行(策展)豁免清理', before.length > 0 && manager.listTeamMemories(channelId, 50).length === before.length)
|
|
303
|
+
|
|
304
|
+
// ── ⑧ shutdown 干净退出 ──
|
|
305
|
+
console.log('\n=== ⑧ shutdown ===')
|
|
306
|
+
await manager.shutdown()
|
|
307
|
+
const status8 = manager.runtimeStatus()
|
|
308
|
+
check('shutdown 后 runtime 全卸载', status8.wiredAgents.length === 0 && status8.activeChannels.length === 0)
|
|
309
|
+
globalThis.__workshopManager = undefined // 还原 REST 单例,不泄漏测试 manager
|
|
310
|
+
const restoreEmbedEnv = (name: string, value: string | undefined): void => {
|
|
311
|
+
if (value === undefined) Reflect.deleteProperty(process.env, name)
|
|
312
|
+
else process.env[name] = value
|
|
313
|
+
}
|
|
314
|
+
restoreEmbedEnv('AW_MEMORY_EMBED_BASE_URL', savedEmbedBase)
|
|
315
|
+
restoreEmbedEnv('AW_MEMORY_EMBED_MODEL', savedEmbedModel)
|
|
316
|
+
|
|
317
|
+
rmSync(tmpWorkspace, { recursive: true, force: true })
|
|
318
|
+
db.close()
|
|
319
|
+
console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
|
|
320
|
+
process.exit(failures === 0 ? 0 : 1)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
main().catch(async (e) => {
|
|
324
|
+
console.error('E2E 异常:', e)
|
|
325
|
+
try {
|
|
326
|
+
await manager.shutdown()
|
|
327
|
+
}
|
|
328
|
+
catch { /* 尽力清理 */
|
|
329
|
+
}
|
|
330
|
+
rmSync(tmpWorkspace, { recursive: true, force: true })
|
|
331
|
+
process.exit(1)
|
|
332
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审计实验 4 —— DCW 落盘放大成本(只读:仅读真实 data/*.json,写入 scripts/_audit/.tmp)。
|
|
3
|
+
*
|
|
4
|
+
* 运行:node --experimental-transform-types scripts/_audit/run.mjs scripts/_audit/dcw-flush-cost.ts
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, readFileSync, statSync } from 'node:fs'
|
|
7
|
+
import { saveJsonFileAtomic } from '../../server/services/workshop/json-store.mjs'
|
|
8
|
+
|
|
9
|
+
const TMP = new URL('./.tmp/', import.meta.url).pathname.replace(/^\//, '')
|
|
10
|
+
mkdirSync(TMP, { recursive: true })
|
|
11
|
+
|
|
12
|
+
const rows = (f: string): unknown => JSON.parse(readFileSync(`server/data/${f}`, 'utf-8'))
|
|
13
|
+
|
|
14
|
+
const nodes = rows('dcws.json') as Array<Record<string, unknown>>
|
|
15
|
+
const writes = rows('dcw-writes.json') as Array<Record<string, unknown>>
|
|
16
|
+
|
|
17
|
+
function bench(label: string, data: unknown, target: string, iters: number): void {
|
|
18
|
+
const t0 = performance.now()
|
|
19
|
+
for (let i = 0; i < iters; i++) saveJsonFileAtomic(target, data)
|
|
20
|
+
const dt = performance.now() - t0
|
|
21
|
+
const size = statSync(target).size
|
|
22
|
+
// JSON.stringify 单独计量(纯 CPU,事件循环阻塞部分)
|
|
23
|
+
const t1 = performance.now()
|
|
24
|
+
for (let i = 0; i < iters; i++) JSON.stringify(data, null, 2)
|
|
25
|
+
const ds = performance.now() - t1
|
|
26
|
+
console.log(
|
|
27
|
+
` ${label}: flushNow 全量落盘 per-op=${(dt / iters).toFixed(1)}ms`
|
|
28
|
+
+ ` (其中 JSON.stringify(pretty) ${(ds / iters).toFixed(1)}ms), 文件=${(size / 1024).toFixed(1)}KB,`
|
|
29
|
+
+ ` iters=${iters}`,
|
|
30
|
+
)
|
|
31
|
+
console.log(` 记录数=${Array.isArray(data) ? data.length : Object.keys(data as object).length}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log('=== 真实数据规模下的落盘成本(与生产 server/data/*.json 同形) ===')
|
|
35
|
+
bench('dcws.json(节点快照;每次写值/读值后防抖落盘 dcw-controller.ts:179,235)', nodes, `${TMP}/dcws.json`, 20)
|
|
36
|
+
bench('dcw-writes.json(写历史;每次下发后防抖落盘 dcw-controller.ts:193)', writes, `${TMP}/dcw-writes.json`, 20)
|
|
37
|
+
|
|
38
|
+
const readInt = nodes.map(n => Number(n.readIntervalMs)).filter(v => Number.isFinite(v) && v > 0)
|
|
39
|
+
const perSec = readInt.reduce((a, v) => a + (1000 / v), 0)
|
|
40
|
+
console.log('\n=== 生产规模推算 ===')
|
|
41
|
+
console.log(` 配置了周期读的节点=${readInt.length},合计周期读频率=${perSec.toFixed(1)} 次/秒`)
|
|
42
|
+
console.log(` 每次周期读 executeRead 尾部调用 repo.flushDebounced()(dcw-controller.ts:235)`)
|
|
43
|
+
console.log(` 防抖窗 1.5s(dcw-node.repo.ts:60-66)→ 稳态下 dcws.json 全量重写≈每 1.5s 一次`)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 向量记忆管线测试:repo vec 方法(vec0 分区隔离/BigInt 绑定/刷新语义)+ embedding provider。
|
|
3
|
+
* 用确定性 hash provider(零网络);真实 provider 由 env 配置,e2e 可选验证。
|
|
4
|
+
* AgentMemory 混合召回融合断言由后续 task 追加(embedder 注入 AgentMemory 后)。
|
|
5
|
+
* 运行: npx tsx scripts/test-memory-vector.ts
|
|
6
|
+
*/
|
|
7
|
+
import type { DatabaseSync } from 'node:sqlite'
|
|
8
|
+
import { openWorkshopDb } from '../server/services/workshop/db/database'
|
|
9
|
+
import { createMemoryRepo, TEAM_AGENT_ID } from '../server/services/workshop/db/memory.repo'
|
|
10
|
+
import { AgentMemory, segmentCJK, vectorizeMemory } from '../server/services/workshop/runtime/memory'
|
|
11
|
+
import {
|
|
12
|
+
createEnvEmbeddingProvider,
|
|
13
|
+
createHashEmbeddingProvider,
|
|
14
|
+
} from '../server/services/workshop/runtime/embedding-provider'
|
|
15
|
+
|
|
16
|
+
let failures = 0
|
|
17
|
+
function check(name: string, ok: boolean, detail = ''): void {
|
|
18
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
19
|
+
if (!ok) failures += 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DIMS = 64 // hash 词袋维度:过小(如 8)会哈希碰撞扰乱 top-1 语义
|
|
23
|
+
const db: DatabaseSync = openWorkshopDb(':memory:')
|
|
24
|
+
const repo = createMemoryRepo(db)
|
|
25
|
+
const embedder = createHashEmbeddingProvider(DIMS)
|
|
26
|
+
const embed = async (text: string): Promise<Float32Array> => (await embedder.embed([text]))[0]!
|
|
27
|
+
|
|
28
|
+
// ---- embedding provider:确定性 hash 词袋 ----
|
|
29
|
+
console.log('--- hash provider ---')
|
|
30
|
+
check('dims 返回构造维度', embedder.dims() === DIMS)
|
|
31
|
+
const [va, vb] = await embedder.embed(['登录 鉴权', '登录 鉴权'])
|
|
32
|
+
check('相同词集向量一致(确定性)', va!.every((x, i) => x === vb![i]))
|
|
33
|
+
const norm = Math.sqrt(va!.reduce((s, x) => s + x * x, 0))
|
|
34
|
+
check('向量归一化(L2=1)', Math.abs(norm - 1) < 1e-5, `norm=${norm.toFixed(5)}`)
|
|
35
|
+
|
|
36
|
+
// ---- embedding provider:env 驱动 + 熔断 ----
|
|
37
|
+
console.log('--- env provider ---')
|
|
38
|
+
const savedBase = process.env.AW_MEMORY_EMBED_BASE_URL
|
|
39
|
+
const savedModel = process.env.AW_MEMORY_EMBED_MODEL
|
|
40
|
+
const savedKey = process.env.AW_MEMORY_EMBED_API_KEY
|
|
41
|
+
delete process.env.AW_MEMORY_EMBED_BASE_URL
|
|
42
|
+
delete process.env.AW_MEMORY_EMBED_MODEL
|
|
43
|
+
delete process.env.AW_MEMORY_EMBED_API_KEY
|
|
44
|
+
check('env 未配置返回 null(纯 FTS 降级)', createEnvEmbeddingProvider() === null)
|
|
45
|
+
process.env.AW_MEMORY_EMBED_BASE_URL = 'http://127.0.0.1:1' // 立即拒绝,零等待
|
|
46
|
+
process.env.AW_MEMORY_EMBED_MODEL = 'test-model'
|
|
47
|
+
const breaker = createEnvEmbeddingProvider()!
|
|
48
|
+
for (let i = 0; i < 3; i++) await breaker.embed(['x']).catch(() => {})
|
|
49
|
+
const cooled = await breaker.embed(['x']).then(() => false, (e: Error) => e.message.includes('冷却'))
|
|
50
|
+
check('连续 3 次失败进入冷却熔断', cooled)
|
|
51
|
+
const restoreEnv = (name: string, value: string | undefined): void => {
|
|
52
|
+
if (value === undefined) Reflect.deleteProperty(process.env, name)
|
|
53
|
+
else process.env[name] = value
|
|
54
|
+
}
|
|
55
|
+
restoreEnv('AW_MEMORY_EMBED_BASE_URL', savedBase)
|
|
56
|
+
restoreEnv('AW_MEMORY_EMBED_MODEL', savedModel)
|
|
57
|
+
restoreEnv('AW_MEMORY_EMBED_API_KEY', savedKey)
|
|
58
|
+
|
|
59
|
+
// ---- repo:vec 延迟建表(首次 embed 维度)----
|
|
60
|
+
console.log('--- repo vec 方法 ---')
|
|
61
|
+
check('vecInit 首次建表', repo.vecInit(DIMS) === true)
|
|
62
|
+
check('vecInit 幂等(同维度)', repo.vecInit(DIMS) === true)
|
|
63
|
+
check('vecInit 维度不符拒绝(不重建)', repo.vecInit(4) === false)
|
|
64
|
+
check('vecReady 状态只读', repo.vecReady === true)
|
|
65
|
+
|
|
66
|
+
// rowid → agentId 映射(kNN 分区隔离断言用)
|
|
67
|
+
const rowAgent = new Map<number, string>()
|
|
68
|
+
const seed = async (agentId: string, title: string, content: string, key: string): Promise<void> => {
|
|
69
|
+
repo.upsert({ channelId: 'ch1', agentId, kind: 'episodic-task', title, titleFts: segmentCJK(title), content: segmentCJK(content), importance: 0.8, taskId: key, dedupKey: `task:${key}` })
|
|
70
|
+
const at = repo.findByAgentDedup('ch1', agentId, `task:${key}`)!
|
|
71
|
+
rowAgent.set(at.rowid, agentId)
|
|
72
|
+
repo.vecSet(at.rowid, agentId, await embed(segmentCJK(content)))
|
|
73
|
+
}
|
|
74
|
+
await seed('a1', '登录鉴权', '用OAuth2做了登录鉴权组件', 't1')
|
|
75
|
+
await seed('a1', '数据库调研', '选择SQLite作为存储', 't2')
|
|
76
|
+
await seed('a2', '他人登录杂事', '别的agent的登录内容', 't3')
|
|
77
|
+
await seed(TEAM_AGENT_ID, '团队规范', '本团队代码必须写测试', 'g1')
|
|
78
|
+
|
|
79
|
+
// ---- 域隔离 kNN(agent_id 分区;本人/team 各自命中,不串他人)----
|
|
80
|
+
const a1hits = repo.vecSearch('a1', await embed(segmentCJK('登录鉴权')), 5)
|
|
81
|
+
check('a1 向量命中本人记忆', a1hits.length >= 1 && a1hits.every(h => rowAgent.get(h.memRowid) === 'a1'), `hits=${JSON.stringify(a1hits)}`)
|
|
82
|
+
check('a1 top1 为登录鉴权记忆', a1hits[0]?.memRowid === repo.findByAgentDedup('ch1', 'a1', 'task:t1')?.rowid)
|
|
83
|
+
check('kNN 距离非负', a1hits.every(h => h.distance >= 0))
|
|
84
|
+
const teamHits = repo.vecSearch(TEAM_AGENT_ID, await embed(segmentCJK('团队规范') + ' 测试'), 5)
|
|
85
|
+
check('team 向量域命中(恒不串私有域)', teamHits.length >= 1 && teamHits.every(h => rowAgent.get(h.memRowid) === TEAM_AGENT_ID))
|
|
86
|
+
|
|
87
|
+
// ---- 刷新语义:重写 t1 内容后 vecSet 覆盖旧向量 ----
|
|
88
|
+
repo.upsert({ channelId: 'ch1', agentId: 'a1', kind: 'episodic-task', title: '登录鉴权', titleFts: segmentCJK('登录鉴权'), content: segmentCJK('重写为JWT方案'), importance: 0.8, taskId: 't1', dedupKey: 'task:t1' })
|
|
89
|
+
const t1at = repo.findByAgentDedup('ch1', 'a1', 'task:t1')!
|
|
90
|
+
repo.vecSet(t1at.rowid, 'a1', await embed(segmentCJK('重写为JWT方案')))
|
|
91
|
+
const refreshed = repo.vecSearch('a1', await embed(segmentCJK('JWT方案')), 5)
|
|
92
|
+
check('vecSet 刷新后新向量命中', refreshed.length >= 1 && refreshed[0]!.memRowid === t1at.rowid && refreshed[0]!.distance < 1.0, `top=${JSON.stringify(refreshed[0])}`)
|
|
93
|
+
const oldQ = repo.vecSearch('a1', await embed(segmentCJK('OAuth2登录鉴权组件')), 5)
|
|
94
|
+
check('旧向量不再零距命中(已替换)', oldQ.every(h => h.distance > 0.5), `hits=${JSON.stringify(oldQ)}`)
|
|
95
|
+
|
|
96
|
+
// ---- vecDelete(删除记忆联动;不炸未向量化的 rowid)----
|
|
97
|
+
const t2at = repo.findByAgentDedup('ch1', 'a1', 'task:t2')!
|
|
98
|
+
repo.vecDelete(t2at.rowid)
|
|
99
|
+
check('vecDelete 后不命中', repo.vecSearch('a1', await embed(segmentCJK('SQLite存储')), 5).every(h => h.memRowid !== t2at.rowid))
|
|
100
|
+
repo.vecDelete(999_999) // 不存在的 rowid:静默
|
|
101
|
+
check('vecDelete 不存在的 rowid 静默', true)
|
|
102
|
+
|
|
103
|
+
// ---- AgentMemory 混合召回融合 + 写入自动向量化(T8)----
|
|
104
|
+
console.log('--- AgentMemory 混合召回 ---')
|
|
105
|
+
const mem = new AgentMemory(repo, { channelId: 'ch1', agentId: 'a1', embedder })
|
|
106
|
+
|
|
107
|
+
// 融合语义:FTS 词面 miss(英文词不进 CJK 切分索引)但向量补足
|
|
108
|
+
repo.upsert({ channelId: 'ch1', agentId: 'a1', kind: 'episodic-task', title: '登陆', titleFts: segmentCJK('登陆'), content: segmentCJK('用户登陆入口跳转处理'), importance: 0.8, taskId: 'tf1', dedupKey: 'task:tf1' })
|
|
109
|
+
const fusedAt = repo.findByAgentDedup('ch1', 'a1', 'task:tf1')!
|
|
110
|
+
repo.vecSet(fusedAt.rowid, 'a1', (await embedder.embed(['用户登陆入口跳转处理']))[0])
|
|
111
|
+
const fused = await mem.recall('entry login 用户')
|
|
112
|
+
check('向量兜底语义召回(FTS 弱命中时 vector 补足)', fused !== null && fused.includes('登陆'))
|
|
113
|
+
|
|
114
|
+
// 写入向量化:recordTaskOutcome 后向量自动就位(fakeTask 按 WorkspaceTask 全字段)
|
|
115
|
+
const fakeTask: WorkspaceTask = {
|
|
116
|
+
id: 'tv1', channelId: 'ch1', assigneeId: 'a1', creatorId: 'lead',
|
|
117
|
+
title: '向量写入验证', state: 'COMPLETED', progress: 100, retryCount: 0,
|
|
118
|
+
artifacts: [{ artifactId: 'x', name: 'deliverable', parts: [{ text: '记忆写入后应自动向量化' }] }],
|
|
119
|
+
history: [],
|
|
120
|
+
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
121
|
+
}
|
|
122
|
+
await mem.recordTaskOutcome(fakeTask)
|
|
123
|
+
const vat = repo.findByAgentDedup('ch1', 'a1', 'task:tv1')!
|
|
124
|
+
const vhit = repo.vecSearch('a1', (await embedder.embed(['记忆写入后应自动向量化']))[0], 3)
|
|
125
|
+
check('recordTaskOutcome 自动向量化', vhit.some(h => h.memRowid === vat.rowid))
|
|
126
|
+
|
|
127
|
+
// ---- M2:模块级 vectorizeMemory(策展行向量化;manager addTeamMemory/addAgentMemory 直调)----
|
|
128
|
+
console.log('--- vectorizeMemory 模块导出(M2)---')
|
|
129
|
+
repo.upsert({ channelId: 'ch1', agentId: TEAM_AGENT_ID, kind: 'semantic', title: '策展团队规范', titleFts: segmentCJK('策展团队规范'), content: segmentCJK('团队统一使用pnpm管理依赖'), importance: 0.9, taskId: null, dedupKey: 'team:vec1' })
|
|
130
|
+
await vectorizeMemory(repo, embedder, 'ch1', TEAM_AGENT_ID, 'team:vec1', '团队统一使用pnpm管理依赖')
|
|
131
|
+
const curatedAt = repo.findByAgentDedup('ch1', TEAM_AGENT_ID, 'team:vec1')
|
|
132
|
+
const curatedHits = curatedAt ? repo.vecSearch(TEAM_AGENT_ID, await embed('团队统一使用pnpm管理依赖'), 5) : []
|
|
133
|
+
check('vectorizeMemory 为策展行建向量(vecSearch 命中)', curatedAt !== null && curatedHits.some(h => h.memRowid === curatedAt.rowid),
|
|
134
|
+
`hits=${JSON.stringify(curatedHits)}`)
|
|
135
|
+
await vectorizeMemory(repo, null, 'ch1', TEAM_AGENT_ID, 'team:vec1', 'x')
|
|
136
|
+
check('vectorizeMemory null embedder 静默 no-op(不抛)', true)
|
|
137
|
+
|
|
138
|
+
// ---- I1:向量融合所有权守卫(a1 分区混入他人 rowid → 反查主表后必须过滤)----
|
|
139
|
+
console.log('--- 融合所有权守卫(I1)---')
|
|
140
|
+
const ownPlain = '所有权守卫独占水印内容'
|
|
141
|
+
repo.upsert({ channelId: 'ch1', agentId: 'a1', kind: 'episodic-task', title: '本人守卫行', titleFts: segmentCJK('本人守卫行'), content: segmentCJK(ownPlain), importance: 0.8, taskId: 'own1', dedupKey: 'task:own1' })
|
|
142
|
+
const ownAt = repo.findByAgentDedup('ch1', 'a1', 'task:own1')!
|
|
143
|
+
repo.vecSet(ownAt.rowid, 'a1', await embed(ownPlain))
|
|
144
|
+
repo.upsert({ channelId: 'ch1', agentId: 'intruder', kind: 'episodic-task', title: '入侵者守卫行', titleFts: segmentCJK('入侵者守卫行'), content: segmentCJK(ownPlain), importance: 0.8, taskId: 'intr1', dedupKey: 'task:intr1' })
|
|
145
|
+
const intrAt = repo.findByAgentDedup('ch1', 'intruder', 'task:intr1')!
|
|
146
|
+
repo.vecSet(intrAt.rowid, 'a1', await embed(ownPlain)) // 脏数据:他人 rowid 被写进 a1 向量分区
|
|
147
|
+
const dirtyHits = repo.vecSearch('a1', await embed(ownPlain), 10)
|
|
148
|
+
check('前置:a1 向量域确有脏 rowid(无守卫则泄漏)', dirtyHits.some(h => h.memRowid === intrAt.rowid), `hits=${JSON.stringify(dirtyHits)}`)
|
|
149
|
+
const guardBlock = await new AgentMemory(repo, { channelId: 'ch1', agentId: 'a1', embedder }).recall(ownPlain)
|
|
150
|
+
check('recall 融合过滤他人行(只见本人行)', guardBlock !== null && guardBlock.includes('本人守卫行') && !guardBlock.includes('入侵者守卫行'),
|
|
151
|
+
guardBlock?.slice(0, 100))
|
|
152
|
+
|
|
153
|
+
console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`}`)
|
|
154
|
+
process.exit(failures === 0 ? 0 : 1)
|