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.
- package/.AgentWorkShop/prompts/compaction-hints.md +9 -0
- package/.AgentWorkShop/prompts/host-tools.json +664 -630
- package/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -0
- package/.AgentWorkShop/prompts/system-manual.md +6 -1
- package/.output/.AgentWorkShop/prompts/compaction-hints.md +9 -0
- package/.output/.AgentWorkShop/prompts/host-tools.json +664 -630
- package/.output/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -0
- package/.output/.AgentWorkShop/prompts/system-manual.md +6 -1
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/d50c6ec2-801c-4ba2-97d8-a4db401b702c.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +1920 -1211
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs +25 -3
- package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs.map +1 -1
- package/.output/server/package.json +1 -1
- package/package.json +1 -1
- package/scripts/_dbg-cross-channel-e2e.mjs +157 -0
- package/scripts/test-context-governor.ts +176 -0
- package/scripts/test-memory-brief-chronicle.ts +180 -0
- package/scripts/test-memory-dynamic.ts +3 -1
- package/scripts/test-memory-rrf-mmr.ts +114 -0
- package/server/api/workshop/channels/[id]/messages/index.post.ts +26 -1
- package/server/api/workshop/ws.ts +1 -0
- package/server/data/daqs.json +35 -13
- package/server/data/dcw-lines.json +14 -0
- package/server/data/dcw-products.json +14 -0
- package/server/data/dcw-recipes.json +28 -0
- package/server/data/dcw-rollback.json +122 -1
- package/server/data/dcw-runs.json +54 -36
- package/server/data/dcw-writes.json +60 -0
- package/server/data/dcws.json +42 -0
- package/server/data/line-runs.json +32 -22
- package/server/services/workshop/agents/adapters/omp-rpc-client.ts +65 -0
- package/server/services/workshop/agents/agent-interface.ts +26 -1
- package/server/services/workshop/agents/mock-agent.ts +12 -0
- package/server/services/workshop/agents/omp-agent.ts +277 -8
- package/server/services/workshop/db/database.ts +16 -1
- package/server/services/workshop/db/memory.repo.ts +22 -2
- package/server/services/workshop/prompts/loader.ts +6 -5
- package/server/services/workshop/runtime/agent-runtime.ts +52 -6
- package/server/services/workshop/runtime/channel-runtime.ts +14 -6
- package/server/services/workshop/runtime/manager.ts +192 -3
- package/server/services/workshop/runtime/memory.ts +313 -78
- package/server/services/workshop/types/task.ts +14 -0
- package/.output/public/_nuxt/builds/meta/f2f649c5-fd3b-4507-a3a1-8b4a194de72d.json +0 -1
|
@@ -17,10 +17,31 @@ import type { EmbeddingProvider } from './embedding-provider'
|
|
|
17
17
|
const W_RELEVANCE = 0.5
|
|
18
18
|
const W_RECENCY = 0.3
|
|
19
19
|
const W_IMPORTANCE = 0.2
|
|
20
|
-
const RECENCY_DAYS = 7
|
|
21
20
|
const RECENT_FALLBACK = 5
|
|
22
|
-
const WEAK_HIT_KEEP = 3
|
|
23
21
|
const MAX_TERMS = 12
|
|
22
|
+
/** CJK 单字词项上限(单字区分度低,12 个易主题漂移;ascii 词项仍取 MAX_TERMS) */
|
|
23
|
+
const MAX_CJK_TERMS = 24
|
|
24
|
+
/** FTS 候选池深度(RRF 融合需要比最终引子更深的榜) */
|
|
25
|
+
const FTS_POOL = 20
|
|
26
|
+
/** Reciprocal Rank Fusion 常数(业界标准 k=60) */
|
|
27
|
+
const RRF_K = 60
|
|
28
|
+
/** MMR 多样性权重(λ×相关性 − (1−λ)×与已选集的最大相似) */
|
|
29
|
+
const MMR_LAMBDA = 0.7
|
|
30
|
+
/** 任务关联加权:候选行 taskId ∈ 兄弟/父任务集时终分加成 */
|
|
31
|
+
const RELATED_TASK_BOOST = 0.3
|
|
32
|
+
/** 策展层 kind(确定性 L0/按 key 直取;不进 L1 排名防双份注入,search_memory 仍可命中) */
|
|
33
|
+
const CURATED_KINDS = new Set(['brief', 'chronicle', 'reflection'])
|
|
34
|
+
/** kind 感知时近衰减半衰期(天;0 = 不衰减) */
|
|
35
|
+
const RECENCY_HALF_LIFE_DAYS: Record<string, number> = {
|
|
36
|
+
'episodic-peer': 3,
|
|
37
|
+
'episodic-task': 14,
|
|
38
|
+
'episodic-session': 14,
|
|
39
|
+
'episodic-team-task': 14,
|
|
40
|
+
'semantic': 0,
|
|
41
|
+
'brief': 0,
|
|
42
|
+
'chronicle': 0,
|
|
43
|
+
'reflection': 0,
|
|
44
|
+
}
|
|
24
45
|
const CONTENT_STORE_LIMIT = 800
|
|
25
46
|
|
|
26
47
|
/** 环境变量安全数值(非有限/非正数 → fallback;防 NaN/0 破坏预算与定时器) */
|
|
@@ -45,6 +66,8 @@ export interface RecallOptions {
|
|
|
45
66
|
budgetTokens?: number
|
|
46
67
|
/** 检索域:auto=私有+公共(默认) / private=仅私有 / shared=仅 Channel 公共 */
|
|
47
68
|
scope?: MemoryScope
|
|
69
|
+
/** 任务关联集(自身+父+兄弟 ≤20):命中行终分 +RELATED_TASK_BOOST(任务需求驱动的相关性) */
|
|
70
|
+
relatedTaskIds?: string[]
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
export type MemoryScope = 'auto' | 'private' | 'shared'
|
|
@@ -68,12 +91,18 @@ export function segmentCJK(text: string): string {
|
|
|
68
91
|
|
|
69
92
|
export function buildMatchQuery(text: string): string | null {
|
|
70
93
|
const cleaned = text.replace(/["'*().:+-]/g, ' ').toLowerCase()
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.
|
|
94
|
+
const words = segmentCJK(cleaned).split(/\s+/).filter(Boolean)
|
|
95
|
+
const cjkTerms: string[] = []
|
|
96
|
+
const otherTerms: string[] = []
|
|
97
|
+
for (const t of words) {
|
|
98
|
+
const isCjk = /[\u4e00-\u9fff]/.test(t)
|
|
99
|
+
if (!isCjk && (t.length < 2 || ['and', 'or', 'not', 'near'].includes(t))) continue
|
|
100
|
+
const pool = isCjk ? cjkTerms : otherTerms
|
|
101
|
+
const cap = isCjk ? MAX_CJK_TERMS : MAX_TERMS
|
|
102
|
+
if (pool.length >= cap) continue
|
|
103
|
+
if (!pool.includes(t)) pool.push(t)
|
|
104
|
+
}
|
|
105
|
+
const terms = [...otherTerms, ...cjkTerms]
|
|
77
106
|
// 词项一律引号包裹:残留特殊字符(FTS5 列过滤/短语语法)全部惰性化
|
|
78
107
|
return terms.length > 0 ? terms.map(t => `"${t}"`).join(' OR ') : null
|
|
79
108
|
}
|
|
@@ -137,27 +166,26 @@ export class AgentMemory {
|
|
|
137
166
|
: row.agentId === this.opts.agentId
|
|
138
167
|
}
|
|
139
168
|
|
|
140
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* 混合检索原始命中(RRF 融合;不排序不 touch)。
|
|
171
|
+
* FTS 榜与向量榜各按名次贡献 1/(k+rank) 累加,再按榜内最高分归一化到 0-1——
|
|
172
|
+
* 相比 v1 的 max 距离归一化,弱首命中/两榜分纲不一致时排序依然稳定。
|
|
173
|
+
*/
|
|
141
174
|
private async collectHits(query: string, scope: MemoryScope): Promise<Map<string, { row: MemoryRow, relevance: number }>> {
|
|
142
|
-
const
|
|
175
|
+
const rrf = new Map<string, { row: MemoryRow, score: number }>()
|
|
176
|
+
const addRank = (row: MemoryRow, rank: number): void => {
|
|
177
|
+
if (!this.inScope(row, scope)) return
|
|
178
|
+
const inc = 1 / (RRF_K + rank + 1)
|
|
179
|
+
const cur = rrf.get(row.id)
|
|
180
|
+
if (cur) cur.score += inc
|
|
181
|
+
else rrf.set(row.id, { row, score: inc })
|
|
182
|
+
}
|
|
143
183
|
const match = buildMatchQuery(query)
|
|
144
184
|
if (match) {
|
|
145
|
-
const found = this.repo.search(this.opts.agentId, match,
|
|
146
|
-
|
|
147
|
-
const best = first ? Math.max(-first.bm25, 0.001) : 1
|
|
148
|
-
// 弱命中保留按"过滤后保留数"计数(非原始下标):
|
|
149
|
-
// 跨 channel 的 team 残留行会占据排序下标,若按下标保留会把本 channel 有效弱命中挤出门外
|
|
150
|
-
let kept = 0
|
|
151
|
-
for (const row of found) {
|
|
152
|
-
if (!this.inScope(row, scope)) continue
|
|
153
|
-
const rel = Math.min(1, -row.bm25 / best)
|
|
154
|
-
if (rel >= 0.1 || kept < WEAK_HIT_KEEP) {
|
|
155
|
-
hits.set(row.id, { row, relevance: rel })
|
|
156
|
-
kept += 1
|
|
157
|
-
}
|
|
158
|
-
}
|
|
185
|
+
const found = this.repo.search(this.opts.agentId, match, FTS_POOL)
|
|
186
|
+
found.forEach((row, i) => addRank(row, i))
|
|
159
187
|
}
|
|
160
|
-
// 向量分支:查询向量 + 本人/team 双域 kNN
|
|
188
|
+
// 向量分支:查询向量 + 本人/team 双域 kNN,按距离名次参与 RRF(失败退化纯 FTS)
|
|
161
189
|
if (this.embedder) {
|
|
162
190
|
try {
|
|
163
191
|
const [qv] = await this.embedder.embed([query])
|
|
@@ -171,85 +199,157 @@ export class AgentMemory {
|
|
|
171
199
|
: [this.opts.agentId, TEAM_AGENT_ID]
|
|
172
200
|
const vAll = ownDomains.flatMap(d =>
|
|
173
201
|
this.repo.vecSearch(d, qv, d === TEAM_AGENT_ID ? 5 : 10))
|
|
174
|
-
// rowid → 最小距离(agent/team 分区不相交;防御性取 min)
|
|
202
|
+
// rowid → 最小距离(agent/team 分区不相交;防御性取 min),保持距离序作名次
|
|
175
203
|
const distByRowid = new Map<number, number>()
|
|
176
204
|
for (const { memRowid, distance } of vAll) {
|
|
177
205
|
const prev = distByRowid.get(memRowid)
|
|
178
206
|
if (prev === undefined || distance < prev) distByRowid.set(memRowid, distance)
|
|
179
207
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (row.agentId !== this.opts.agentId && row.agentId !== TEAM_AGENT_ID)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
hits.set(row.id, { row, relevance: prev ? Math.max(prev.relevance, sim) : sim })
|
|
187
|
-
}
|
|
208
|
+
const rowids = [...distByRowid.entries()].sort((a, b) => a[1] - b[1]).map(([rid]) => rid)
|
|
209
|
+
this.repo.listByRowids(rowids).forEach((row, i) => {
|
|
210
|
+
// 所有权守卫 + channel 隔离(脏 rowid/rowid 复用反查到他人行一律丢弃)
|
|
211
|
+
if (row.agentId !== this.opts.agentId && row.agentId !== TEAM_AGENT_ID) return
|
|
212
|
+
addRank(row, i)
|
|
213
|
+
})
|
|
188
214
|
}
|
|
189
215
|
}
|
|
190
216
|
catch { /* 向量不可用退化为 FTS */ }
|
|
191
217
|
}
|
|
192
|
-
// 时近兜底仅对含私有域的 scope 生效(shared 域无"本人最近"语义)
|
|
218
|
+
// 时近兜底仅对含私有域的 scope 生效(shared 域无"本人最近"语义);
|
|
219
|
+
// 兜底行不参与 RRF,固定弱相关度(策展层行排除——brief 恒 L0 注入,不占引子)
|
|
220
|
+
const hits = new Map<string, { row: MemoryRow, relevance: number }>()
|
|
221
|
+
let best = 0
|
|
222
|
+
for (const { score } of rrf.values()) {
|
|
223
|
+
if (score > best) best = score
|
|
224
|
+
}
|
|
225
|
+
for (const { row, score } of rrf.values()) {
|
|
226
|
+
hits.set(row.id, { row, relevance: best > 0 ? score / best : 0 })
|
|
227
|
+
}
|
|
193
228
|
if (scope !== 'shared') {
|
|
194
229
|
for (const row of this.repo.listRecent(this.opts.agentId, RECENT_FALLBACK)) {
|
|
230
|
+
if (CURATED_KINDS.has(row.kind)) continue
|
|
195
231
|
if (!hits.has(row.id)) hits.set(row.id, { row, relevance: 0.15 })
|
|
196
232
|
}
|
|
197
233
|
}
|
|
198
234
|
return hits
|
|
199
235
|
}
|
|
200
236
|
|
|
201
|
-
/** 排序(
|
|
202
|
-
private async rank(
|
|
237
|
+
/** 排序(综合分降序;策展层排除/任务关联加权;不含预算/touch 副作用) */
|
|
238
|
+
private async rank(
|
|
239
|
+
query: string,
|
|
240
|
+
scope: MemoryScope,
|
|
241
|
+
opts: { excludeCurated?: boolean, relatedTaskIds?: string[] } = {},
|
|
242
|
+
): Promise<Array<{ row: MemoryRow, relevance: number, score: number }>> {
|
|
203
243
|
const hits = await this.collectHits(query, scope)
|
|
244
|
+
const related = opts.relatedTaskIds && opts.relatedTaskIds.length > 0 ? new Set(opts.relatedTaskIds) : null
|
|
204
245
|
return [...hits.values()]
|
|
205
|
-
.
|
|
246
|
+
.filter(h => !opts.excludeCurated || !CURATED_KINDS.has(h.row.kind))
|
|
247
|
+
.map((h) => {
|
|
248
|
+
let score = this.score(h.row, h.relevance)
|
|
249
|
+
if (related && h.row.taskId && related.has(h.row.taskId)) {
|
|
250
|
+
score = Math.min(1, score + RELATED_TASK_BOOST)
|
|
251
|
+
}
|
|
252
|
+
return { ...h, score }
|
|
253
|
+
})
|
|
206
254
|
.sort((a, b) => b.score - a.score)
|
|
207
255
|
}
|
|
208
256
|
|
|
209
257
|
/**
|
|
210
|
-
* run/supervise
|
|
211
|
-
*
|
|
212
|
-
*
|
|
258
|
+
* run/supervise 前的记忆注入:三层装配(硬顶 AW_MEMORY_INJECT_TOTAL)。
|
|
259
|
+
* - L0 会话简报(brief 行,确定性置顶,不占检索);
|
|
260
|
+
* - L1 相关性引子(RRF 混合检索 + MMR 多样性 + 任务关联加权,小预算);
|
|
261
|
+
* - 完整内容由 Agent 经 search_memory 工具按需抓取(L2)。
|
|
262
|
+
* 预算内装配;使用提示计费(防提示挤占正文预算)。
|
|
213
263
|
*/
|
|
214
264
|
async recall(query: string, recallOpts: RecallOptions = {}): Promise<string | null> {
|
|
215
265
|
const doTouch = recallOpts.touch !== false
|
|
216
|
-
|
|
266
|
+
// 显式 budgetTokens(实例/调用级)= 整块记忆硬顶(L0 简报+L1 引子+提示全计入);
|
|
267
|
+
// 未显式设置 = 双预算:L1 引子 ≤ AW_MEMORY_PRIMER_TOKENS(300),整块 ≤ AW_MEMORY_INJECT_TOTAL(500)
|
|
268
|
+
const explicit = recallOpts.budgetTokens ?? this.opts.budgetTokens
|
|
269
|
+
const budget = explicit ?? envNum('AW_MEMORY_PRIMER_TOKENS', 300)
|
|
270
|
+
const totalBudget = explicit ?? envNum('AW_MEMORY_INJECT_TOTAL', 500)
|
|
217
271
|
const scope = recallOpts.scope ?? 'auto'
|
|
218
|
-
const scored = await this.rank(query, scope)
|
|
219
|
-
if (scored.length === 0) return null
|
|
272
|
+
const scored = await this.rank(query, scope, { excludeCurated: true, relatedTaskIds: recallOpts.relatedTaskIds })
|
|
220
273
|
|
|
221
274
|
const lines: string[] = []
|
|
222
275
|
const touched: string[] = []
|
|
223
|
-
let
|
|
224
|
-
|
|
225
|
-
|
|
276
|
+
let usedTotal = 0
|
|
277
|
+
// L0 会话简报置顶(幂等单行;确定性注入,不依赖检索命中;计费走总预算,不挤占 L1 引子预算)
|
|
278
|
+
const briefRef = this.repo.findByAgentDedup(this.opts.channelId, this.opts.agentId, `brief:${this.opts.agentId}`)
|
|
279
|
+
const briefRow = briefRef ? this.repo.getById(briefRef.id) : null
|
|
280
|
+
if (briefRow) {
|
|
281
|
+
const briefText = `## 会话简报(自动维护)\n${unsegmentCJK(briefRow.content)}`
|
|
282
|
+
const briefCost = estimateTokens(briefText)
|
|
283
|
+
if (usedTotal + briefCost <= totalBudget) {
|
|
284
|
+
usedTotal += briefCost
|
|
285
|
+
lines.push(briefText)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// 使用提示预留计费:正文与提示共享预算,提示永不超卖
|
|
289
|
+
const tipsText = [
|
|
290
|
+
`记忆使用提示:`,
|
|
291
|
+
`- 以上只是线索级摘要。开工前先判断是否需要更多上下文:历史结论/团队约定/相似任务经验 → 调 search_memory(query, scope) 主动检索(覆盖私有+共享域)。`,
|
|
292
|
+
`- 作业中发现可复用的结论、约定、教训 → 立即 save_memory 沉淀:scope=private 存个人经验,scope=shared 发布到 Channel 公共记忆供全员复用。`,
|
|
293
|
+
].join('\n')
|
|
294
|
+
const tipsCost = estimateTokens(tipsText)
|
|
295
|
+
// MMR 装配:逐条贪心选 λ×相关性 − (1−λ)×与已选集最大相似 的最大者(多样性,防近重复挤占)
|
|
296
|
+
const candidates = scored.slice()
|
|
297
|
+
const tokenSet = (row: MemoryRow): Set<string> =>
|
|
298
|
+
new Set(unsegmentCJK(`${row.title} ${row.content.slice(0, 200)}`).toLowerCase().split(/\s+/).filter(Boolean))
|
|
299
|
+
const selectedTokens: Set<string>[] = []
|
|
300
|
+
let usedPrimer = 0
|
|
301
|
+
while (candidates.length > 0 && lines.length < 12) {
|
|
302
|
+
let bestIdx = -1
|
|
303
|
+
let bestGain = -Infinity
|
|
304
|
+
let bestTokens: Set<string> | null = null
|
|
305
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
306
|
+
const c = candidates[i]!
|
|
307
|
+
const toks = tokenSet(c.row)
|
|
308
|
+
let maxSim = 0
|
|
309
|
+
for (const st of selectedTokens) {
|
|
310
|
+
if (st.size === 0 || toks.size === 0) continue
|
|
311
|
+
let inter = 0
|
|
312
|
+
for (const t of toks) if (st.has(t)) inter++
|
|
313
|
+
const sim = inter / (st.size + toks.size - inter)
|
|
314
|
+
if (sim > maxSim) maxSim = sim
|
|
315
|
+
}
|
|
316
|
+
const gain = MMR_LAMBDA * c.score - (1 - MMR_LAMBDA) * maxSim
|
|
317
|
+
if (gain > bestGain) {
|
|
318
|
+
bestGain = gain
|
|
319
|
+
bestIdx = i
|
|
320
|
+
bestTokens = toks
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (bestIdx < 0) break
|
|
324
|
+
const [c] = candidates.splice(bestIdx, 1)
|
|
325
|
+
const line = this.formatLine(c!.row)
|
|
226
326
|
const cost = estimateTokens(line)
|
|
227
|
-
|
|
228
|
-
|
|
327
|
+
// 双预算:L1 引子预算(budget)+ 注入总预算(total,含 brief);超预算跳过但继续找更短行
|
|
328
|
+
if (usedPrimer + tipsCost + cost > budget || usedTotal + cost + tipsCost > totalBudget) continue
|
|
329
|
+
usedPrimer += cost
|
|
330
|
+
usedTotal += cost
|
|
229
331
|
lines.push(line)
|
|
230
|
-
|
|
332
|
+
selectedTokens.push(bestTokens!)
|
|
333
|
+
if (doTouch) touched.push(c!.row.id)
|
|
231
334
|
}
|
|
232
335
|
if (lines.length === 0) return null
|
|
233
336
|
for (const id of touched) this.repo.touch(id)
|
|
234
337
|
return [
|
|
235
338
|
`## 相关记忆(自动召回的高相关/最近线索摘要;与当前任务冲突时,以当前任务为准)`,
|
|
236
339
|
...lines,
|
|
237
|
-
|
|
238
|
-
`记忆使用提示:`,
|
|
239
|
-
`- 以上只是线索级摘要。开工前先判断是否需要更多上下文:历史结论/团队约定/相似任务经验 → 调 search_memory(query, scope) 主动检索(覆盖私有+共享域)。`,
|
|
240
|
-
`- 作业中发现可复用的结论、约定、教训 → 立即 save_memory 沉淀:scope=private 存个人经验,scope=shared 发布到 Channel 公共记忆供全员复用。`,
|
|
241
|
-
].join('\n'),
|
|
340
|
+
tipsText,
|
|
242
341
|
].join('\n')
|
|
243
342
|
}
|
|
244
343
|
|
|
245
344
|
/**
|
|
246
345
|
* 工具按需抓取(search_memory):结构化片段返回,content 还原为未切分原文。
|
|
247
|
-
* 触发 touch(access_count 强化后续召回排序);limit
|
|
346
|
+
* 触发 touch(access_count 强化后续召回排序);limit 上限防御;
|
|
347
|
+
* relatedTaskIds 透传任务关联加权(与 recall 同源排序)。
|
|
248
348
|
*/
|
|
249
|
-
async recallRows(query: string, opts: { scope?: MemoryScope, limit?: number } = {}): Promise<MemorySnippet[]> {
|
|
349
|
+
async recallRows(query: string, opts: { scope?: MemoryScope, limit?: number, relatedTaskIds?: string[] } = {}): Promise<MemorySnippet[]> {
|
|
250
350
|
const scope = opts.scope ?? 'auto'
|
|
251
351
|
const limit = Math.min(Math.max(opts.limit ?? 5, 1), 20)
|
|
252
|
-
const scored = (await this.rank(query, scope)).slice(0, limit)
|
|
352
|
+
const scored = (await this.rank(query, scope, { relatedTaskIds: opts.relatedTaskIds })).slice(0, limit)
|
|
253
353
|
for (const s of scored) this.repo.touch(s.row.id)
|
|
254
354
|
return scored.map(s => ({
|
|
255
355
|
id: s.row.id,
|
|
@@ -286,10 +386,12 @@ export class AgentMemory {
|
|
|
286
386
|
dedupKey,
|
|
287
387
|
})
|
|
288
388
|
await vectorizeMemory(this.repo, this.embedder, this.opts.channelId, owner, dedupKey, input.content)
|
|
389
|
+
// 共享约定进入简报"团队共享约定"行
|
|
390
|
+
if (shared) await this.updateBrief()
|
|
289
391
|
return { scope: input.scope, dedupKey }
|
|
290
392
|
}
|
|
291
393
|
|
|
292
|
-
/** run 后(任务路径;仅终态调用):harvest TaskEngine 终态 + deliverable */
|
|
394
|
+
/** run 后(任务路径;仅终态调用):harvest TaskEngine 终态 + deliverable,并刷新 L0 简报 */
|
|
293
395
|
async recordTaskOutcome(task: WorkspaceTask): Promise<void> {
|
|
294
396
|
// 优先 deliverable/summary 命名成果;harness 任意命名(如 mock 的 result)兜底取全部成果
|
|
295
397
|
const preferred = task.artifacts.filter(a => a.name === 'deliverable' || a.name === 'summary')
|
|
@@ -312,6 +414,7 @@ export class AgentMemory {
|
|
|
312
414
|
dedupKey: `task:${task.id}`,
|
|
313
415
|
})
|
|
314
416
|
await this.vectorize(content, `task:${task.id}`)
|
|
417
|
+
await this.updateBrief()
|
|
315
418
|
}
|
|
316
419
|
|
|
317
420
|
/** run 后(点对点路径):请求 + 我方回复摘要 */
|
|
@@ -339,19 +442,141 @@ export class AgentMemory {
|
|
|
339
442
|
await vectorizeMemory(this.repo, this.embedder, this.opts.channelId, this.opts.agentId, dedupKey, plainContent)
|
|
340
443
|
}
|
|
341
444
|
|
|
445
|
+
// ===== 上下文治理/团队历史(P0-3 新增写入面;全部幂等 upsert)=====
|
|
446
|
+
|
|
447
|
+
/** 会话压缩摘要序号(每实例自增;dedupKey 防同幂键覆盖) */
|
|
448
|
+
private sessionSeq = 0
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* 压缩摘要入库(compaction harvest):kind=episodic-session,14d 保鲜,向量化一次。
|
|
452
|
+
* 三路统一入口(平台主动 compact / omp 阈值自动 / overflow),dedupKey 含序号防互踩。
|
|
453
|
+
*/
|
|
454
|
+
async recordSessionCompaction(input: { summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string }): Promise<{ dedupKey: string }> {
|
|
455
|
+
this.sessionSeq += 1
|
|
456
|
+
const dedupKey = `session:${this.opts.agentId}:c${this.sessionSeq}`
|
|
457
|
+
const meta: string[] = []
|
|
458
|
+
if (typeof input.tokensBefore === 'number') meta.push(`压缩前 ${input.tokensBefore} tok`)
|
|
459
|
+
if (typeof input.tokensAfter === 'number') meta.push(`压缩后约 ${input.tokensAfter} tok`)
|
|
460
|
+
if (input.reason) meta.push(`触发=${input.reason}`)
|
|
461
|
+
const head = meta.length > 0 ? `[会话压缩摘要 · ${meta.join(' · ')}]\n` : '[会话压缩摘要]\n'
|
|
462
|
+
this.repo.upsert({
|
|
463
|
+
channelId: this.opts.channelId,
|
|
464
|
+
agentId: this.opts.agentId,
|
|
465
|
+
kind: 'episodic-session',
|
|
466
|
+
title: `会话压缩摘要 #${this.sessionSeq}`,
|
|
467
|
+
titleFts: segmentCJK('会话压缩摘要'),
|
|
468
|
+
content: segmentCJK(`${head}${input.summary}`).slice(0, CONTENT_STORE_LIMIT),
|
|
469
|
+
importance: 0.75,
|
|
470
|
+
taskId: null,
|
|
471
|
+
dedupKey,
|
|
472
|
+
})
|
|
473
|
+
await this.vectorize(input.summary, dedupKey)
|
|
474
|
+
return { dedupKey }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* L0 会话简报刷新(零 LLM 模板拼装;brief:<agentId> 幂等单行;免向量化——
|
|
479
|
+
* 确定性层恒注入,无需被检索;FTS 仍可命中)。数据源=本人已有记忆行,零额外查询依赖。
|
|
480
|
+
*/
|
|
481
|
+
async updateBrief(): Promise<void> {
|
|
482
|
+
const own = this.repo.listByAgentChannel(this.opts.channelId, this.opts.agentId, 40)
|
|
483
|
+
.filter(r => r.kind === 'episodic-task')
|
|
484
|
+
const shared = this.repo.listByAgentChannel(this.opts.channelId, TEAM_AGENT_ID, 40)
|
|
485
|
+
.filter(r => r.kind === 'semantic')
|
|
486
|
+
const lines: string[] = []
|
|
487
|
+
const done = own.filter(r => r.importance >= 0.75).slice(0, 3)
|
|
488
|
+
if (done.length > 0) {
|
|
489
|
+
lines.push(`最近完成:`)
|
|
490
|
+
done.forEach((r, i) => {
|
|
491
|
+
lines.push(`${i + 1}) ${r.title} — ${unsegmentCJK(r.content).slice(0, 30)}`)
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
if (shared.length > 0) {
|
|
495
|
+
lines.push(`团队共享约定:${shared.slice(0, 2).map(r => r.title).join(';')}`)
|
|
496
|
+
}
|
|
497
|
+
if (lines.length === 0) return
|
|
498
|
+
this.repo.upsert({
|
|
499
|
+
channelId: this.opts.channelId,
|
|
500
|
+
agentId: this.opts.agentId,
|
|
501
|
+
kind: 'brief',
|
|
502
|
+
title: '会话简报',
|
|
503
|
+
titleFts: segmentCJK('会话简报'),
|
|
504
|
+
content: segmentCJK(lines.join('\n')),
|
|
505
|
+
importance: 0.6,
|
|
506
|
+
taskId: null,
|
|
507
|
+
dedupKey: `brief:${this.opts.agentId}`,
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 团队任务成果入共享域(挂 channel task 终态事件;team-task:<taskId> 幂等;向量化)。
|
|
513
|
+
* 全员 search_memory(scope=shared) 即可命中"谁做完了什么/为什么失败"。
|
|
514
|
+
*/
|
|
515
|
+
async appendTeamTaskRecord(input: { taskId: string, title: string, content: string, importance: number }): Promise<void> {
|
|
516
|
+
const dedupKey = `team-task:${input.taskId}`
|
|
517
|
+
this.repo.upsert({
|
|
518
|
+
channelId: this.opts.channelId,
|
|
519
|
+
agentId: TEAM_AGENT_ID,
|
|
520
|
+
kind: 'episodic-team-task',
|
|
521
|
+
title: input.title,
|
|
522
|
+
titleFts: segmentCJK(input.title),
|
|
523
|
+
content: segmentCJK(input.content).slice(0, CONTENT_STORE_LIMIT),
|
|
524
|
+
importance: input.importance,
|
|
525
|
+
taskId: input.taskId,
|
|
526
|
+
dedupKey,
|
|
527
|
+
})
|
|
528
|
+
await vectorizeMemory(this.repo, this.embedder, this.opts.channelId, TEAM_AGENT_ID, dedupKey, input.content)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** 团队编年史滚动重写(chronicle:<channelId> 幂等单行;免向量化;策展层豁免维护) */
|
|
532
|
+
upsertChronicle(entriesText: string): void {
|
|
533
|
+
this.repo.upsert({
|
|
534
|
+
channelId: this.opts.channelId,
|
|
535
|
+
agentId: TEAM_AGENT_ID,
|
|
536
|
+
kind: 'chronicle',
|
|
537
|
+
title: '团队编年史',
|
|
538
|
+
titleFts: segmentCJK('团队编年史'),
|
|
539
|
+
content: segmentCJK(entriesText).slice(0, 1500),
|
|
540
|
+
importance: 0.9,
|
|
541
|
+
taskId: null,
|
|
542
|
+
dedupKey: `chronicle:${this.opts.channelId}`,
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** 空闲反思行(reflection:<agentId>:<month> 幂等单行;免向量化;策展层豁免维护) */
|
|
547
|
+
upsertReflection(input: { month: string, content: string }): void {
|
|
548
|
+
this.repo.upsert({
|
|
549
|
+
channelId: this.opts.channelId,
|
|
550
|
+
agentId: this.opts.agentId,
|
|
551
|
+
kind: 'reflection',
|
|
552
|
+
title: `作业反思 ${input.month}`,
|
|
553
|
+
titleFts: segmentCJK(`作业反思 ${input.month}`),
|
|
554
|
+
content: segmentCJK(input.content).slice(0, CONTENT_STORE_LIMIT),
|
|
555
|
+
importance: 0.7,
|
|
556
|
+
taskId: null,
|
|
557
|
+
dedupKey: `reflection:${this.opts.agentId}:${input.month}`,
|
|
558
|
+
})
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** kind 感知综合分:时近衰减按 kind 半衰期(知识/策展层不随时间贬值) */
|
|
342
562
|
private score(row: MemoryRow, relevance: number): number {
|
|
563
|
+
const halfLife = RECENCY_HALF_LIFE_DAYS[row.kind] ?? 7
|
|
343
564
|
const ageDays = (Date.now() - Date.parse(row.createdAt)) / 86_400_000
|
|
344
|
-
const recency = Math.exp(-ageDays /
|
|
565
|
+
const recency = halfLife <= 0 ? 1 : Math.exp(-ageDays / halfLife)
|
|
345
566
|
const importance = Math.min(1, row.importance + row.accessCount * 0.05)
|
|
346
567
|
return W_RELEVANCE * relevance + W_RECENCY * recency + W_IMPORTANCE * importance
|
|
347
568
|
}
|
|
348
569
|
|
|
349
570
|
private formatLine(row: MemoryRow): string {
|
|
350
|
-
const tag =
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
571
|
+
const tag = ({
|
|
572
|
+
'episodic-task': '任务',
|
|
573
|
+
'episodic-team-task': '团队任务',
|
|
574
|
+
'episodic-peer': '协作',
|
|
575
|
+
'episodic-session': '会话',
|
|
576
|
+
'brief': '简报',
|
|
577
|
+
'chronicle': '编年史',
|
|
578
|
+
'reflection': '反思',
|
|
579
|
+
} as Record<string, string>)[row.kind] ?? '共享'
|
|
355
580
|
const content = unsegmentCJK(row.content)
|
|
356
581
|
const short = content.length > 240 ? `${content.slice(0, 240)}…` : content
|
|
357
582
|
return `- [${humanAgo(row.createdAt)}·${tag}] ${row.title}:${short}`
|
|
@@ -389,36 +614,37 @@ export interface MaintenanceResult {
|
|
|
389
614
|
}
|
|
390
615
|
|
|
391
616
|
/**
|
|
392
|
-
* 记忆维护:① episodic 过期删除(now - (lastAccessedAt ?? createdAt) >
|
|
393
|
-
*
|
|
394
|
-
*
|
|
617
|
+
* 记忆维护:① episodic 过期删除(now - (lastAccessedAt ?? createdAt) > 过期天数;
|
|
618
|
+
* kind 分离:episodic-session 14d / 其余 episodic 180d)
|
|
619
|
+
* ② 每 agent 容量淘汰(仅 episodic 族;effectiveScore = importance + accessCount×0.05 降序保留)
|
|
620
|
+
* ③ team 域行:episodic-team-task 同样过期+淘汰(防共享域膨胀);chronicle/semantic 策展行豁免
|
|
621
|
+
* ④ 孤儿 vec 清理。semantic / brief / chronicle / reflection 全程豁免(单行幂等自限)。
|
|
395
622
|
*/
|
|
396
623
|
export function runMemoryMaintenance(
|
|
397
624
|
repo: MemoryRepo,
|
|
398
625
|
opts: { expireDays?: number, cap?: number } = {},
|
|
399
626
|
): MaintenanceResult {
|
|
400
627
|
const expireDays = opts.expireDays ?? envNum('AW_MEMORY_EXPIRE_DAYS', 180)
|
|
628
|
+
const sessionExpireDays = envNum('AW_MEMORY_EXPIRE_SESSION_DAYS', 14)
|
|
401
629
|
const cap = opts.cap ?? envNum('AW_MEMORY_CAP', 500)
|
|
402
|
-
const expireMs = expireDays * 86_400_000
|
|
403
630
|
const now = Date.now()
|
|
404
631
|
let deletedExpired = 0
|
|
405
632
|
let evicted = 0
|
|
406
633
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
634
|
+
/** 过期天数按 kind 分离(session 摘要短保鲜) */
|
|
635
|
+
const expireMsFor = (kind: string): number =>
|
|
636
|
+
(kind === 'episodic-session' ? sessionExpireDays : expireDays) * 86_400_000
|
|
637
|
+
|
|
638
|
+
/** 对一组行执行 ①过期 ②容量淘汰(仅传入选定行;调用方负责 kind 圈定) */
|
|
639
|
+
const expireAndEvict = (rows: Array<MemoryRow & { rowid: number }>): void => {
|
|
411
640
|
for (const r of rows) {
|
|
412
|
-
if (
|
|
413
|
-
if (now - Date.parse(r.lastAccessedAt ?? r.createdAt) > expireMs) {
|
|
641
|
+
if (now - Date.parse(r.lastAccessedAt ?? r.createdAt) > expireMsFor(r.kind)) {
|
|
414
642
|
repo.vecDelete(r.rowid)
|
|
415
643
|
repo.delete(r.id)
|
|
416
644
|
deletedExpired++
|
|
417
645
|
}
|
|
418
646
|
}
|
|
419
|
-
|
|
420
|
-
const remaining = repo.listByAgentWithRowid(agentId, 1_000_000)
|
|
421
|
-
.filter(r => r.kind.startsWith('episodic'))
|
|
647
|
+
const remaining = rows.filter(r => repo.getById(r.id) !== null)
|
|
422
648
|
if (remaining.length > cap) {
|
|
423
649
|
const sorted = remaining
|
|
424
650
|
.map(r => ({ r, s: r.importance + r.accessCount * 0.05 }))
|
|
@@ -430,6 +656,15 @@ export function runMemoryMaintenance(
|
|
|
430
656
|
}
|
|
431
657
|
}
|
|
432
658
|
}
|
|
659
|
+
|
|
660
|
+
// 成员私有域(排除 team)
|
|
661
|
+
for (const agentId of repo.listMemoryAgentIds()) {
|
|
662
|
+
expireAndEvict(repo.listByAgentWithRowid(agentId, 1_000_000)
|
|
663
|
+
.filter(r => r.kind.startsWith('episodic')))
|
|
664
|
+
}
|
|
665
|
+
// team 共享域:仅 episodic-team-task 参与过期/淘汰(chronicle/semantic 策展行豁免)
|
|
666
|
+
expireAndEvict(repo.listByAgentWithRowid(TEAM_AGENT_ID, 1_000_000)
|
|
667
|
+
.filter(r => r.kind === 'episodic-team-task'))
|
|
433
668
|
let cleanedVec = 0
|
|
434
669
|
try {
|
|
435
670
|
cleanedVec = repo.vecCleanOrphans()
|
|
@@ -65,6 +65,18 @@ export interface AgentTaskQueueView {
|
|
|
65
65
|
completed: WorkspaceTask[]
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** harness 上下文用量快照(omp harness 有;进程内 harness 无 → 字段缺省) */
|
|
69
|
+
export interface AgentContextStats {
|
|
70
|
+
/** 最近已知上下文 tokens(≈ prompt 规模) */
|
|
71
|
+
usedTokens: number
|
|
72
|
+
/** 模型上下文窗口(未知为 null,percent 同为 null) */
|
|
73
|
+
contextWindow: number | null
|
|
74
|
+
/** 占窗口比例 0-1(窗口未知为 null) */
|
|
75
|
+
percent: number | null
|
|
76
|
+
/** harness 正在压缩会话 */
|
|
77
|
+
compacting: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
/** Agent 实时状态视图(状态管理机制:idle/busy/stopped + 队列上下文) */
|
|
69
81
|
export interface AgentStatusView {
|
|
70
82
|
agentId: string
|
|
@@ -82,4 +94,6 @@ export interface AgentStatusView {
|
|
|
82
94
|
queuedCount: number
|
|
83
95
|
/** 已完成任务数 */
|
|
84
96
|
completedCount: number
|
|
97
|
+
/** harness 上下文用量(omp 有;进程内 harness/未知时缺省) */
|
|
98
|
+
context?: AgentContextStats
|
|
85
99
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"id":"f2f649c5-fd3b-4507-a3a1-8b4a194de72d","timestamp":1788345881592,"prerendered":[]}
|