agentworkshop 0.5.4 → 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 (49) 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 +1936 -1195
  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/cli/commands/start.mjs +2 -2
  18. package/package.json +1 -1
  19. package/scripts/_dbg-cross-channel-e2e.mjs +157 -0
  20. package/scripts/_dbg-prompts-resolve-test.ts +39 -0
  21. package/scripts/start.mjs +2 -0
  22. package/scripts/test-context-governor.ts +176 -0
  23. package/scripts/test-memory-brief-chronicle.ts +180 -0
  24. package/scripts/test-memory-dynamic.ts +3 -1
  25. package/scripts/test-memory-rrf-mmr.ts +114 -0
  26. package/server/api/workshop/channels/[id]/messages/index.post.ts +26 -1
  27. package/server/api/workshop/ws.ts +1 -0
  28. package/server/data/daqs.json +110 -88
  29. package/server/data/dcw-lines.json +21 -0
  30. package/server/data/dcw-products.json +21 -0
  31. package/server/data/dcw-recipes.json +28 -0
  32. package/server/data/dcw-rollback.json +260 -1
  33. package/server/data/dcw-runs.json +81 -54
  34. package/server/data/dcw-writes.json +108 -0
  35. package/server/data/dcws.json +63 -0
  36. package/server/data/line-runs.json +32 -22
  37. package/server/services/workshop/agents/adapters/omp-rpc-client.ts +65 -0
  38. package/server/services/workshop/agents/agent-interface.ts +26 -1
  39. package/server/services/workshop/agents/mock-agent.ts +12 -0
  40. package/server/services/workshop/agents/omp-agent.ts +277 -8
  41. package/server/services/workshop/db/database.ts +16 -1
  42. package/server/services/workshop/db/memory.repo.ts +22 -2
  43. package/server/services/workshop/prompts/loader.ts +64 -10
  44. package/server/services/workshop/runtime/agent-runtime.ts +52 -6
  45. package/server/services/workshop/runtime/channel-runtime.ts +14 -6
  46. package/server/services/workshop/runtime/manager.ts +192 -3
  47. package/server/services/workshop/runtime/memory.ts +313 -78
  48. package/server/services/workshop/types/task.ts +14 -0
  49. package/.output/public/_nuxt/builds/meta/70bd10c7-55cf-4e49-94de-8aa1b9edf33a.json +0 -1
@@ -28,10 +28,11 @@ import type {
28
28
  SupervisionSnapshot,
29
29
  } from './agent-interface'
30
30
  import type { A2AArtifact, A2AMessage, Part } from '../types/a2a'
31
- import type { WorkspaceTask } from '../types/task'
31
+ import type { AgentContextStats, WorkspaceTask } from '../types/task'
32
32
  import {
33
33
  OmpRpcClient,
34
34
  type AgentSessionEvent,
35
+ type CompactionResult,
35
36
  type RpcHostToolDefinition,
36
37
  type HostToolCallRequest,
37
38
  } from './adapters/omp-rpc-client'
@@ -52,6 +53,7 @@ import {
52
53
  import { toolDaqQuery, toolDcwControl, toolDcwJudge, toolDcwJournal, toolDcwRollback, toolMyIndustrialNodes } from './industrial-tools'
53
54
  import { buildIndustrialContext, industrialLoopGuide } from './industrial-context'
54
55
  import { extractTaskMode } from '../runtime/execution-mode'
56
+ import { envNum } from '../runtime/memory'
55
57
 
56
58
  const log = createLogger('workshop.omp')
57
59
 
@@ -217,6 +219,194 @@ export class OmpRpcAgentImpl implements AgentInterface {
217
219
  */
218
220
  private replyContext: { fromId: string, messageId: string } | null = null
219
221
 
222
+ // ===== 上下文治理(70% 无中断压缩环)=====
223
+ /** 压缩进行中(平台 gate 与 omp 原生压缩共用互斥位) */
224
+ private compacting = false
225
+ private lastCompactAt = 0
226
+ /** omp 不支持 compact 命令(旧版)→ 永久停用平台压缩;原生 auto-compaction 兜底,harvest 照常 */
227
+ private compactLegacy = false
228
+ /** 模型上下文窗口(get_state/get_session_stats 探测;null = 未知,percent 不可算) */
229
+ private contextWindow: number | null = null
230
+ private sessionId: string | null = null
231
+ /** harvest 双路去重(compact 响应与 compaction_end 事件可能双达) */
232
+ private lastHarvestKey = ''
233
+ private lastHarvestAt = 0
234
+
235
+ /** 总开关(AW_OMP_COMPACT_ENABLED=0 显式关闭;envNum 拒 0,故单独解析) */
236
+ private compactEnabled(): boolean {
237
+ const v = Number(process.env.AW_OMP_COMPACT_ENABLED)
238
+ return !(Number.isFinite(v) && v === 0)
239
+ }
240
+
241
+ private compactThreshold(): number {
242
+ return Math.min(1, envNum('AW_OMP_COMPACT_THRESHOLD', 0.7))
243
+ }
244
+
245
+ private compactMinIntervalMs(): number {
246
+ return envNum('AW_OMP_COMPACT_MIN_INTERVAL_MS', 300_000)
247
+ }
248
+
249
+ private compactWaitMs(): number {
250
+ return envNum('AW_OMP_COMPACT_WAIT_MS', 120_000)
251
+ }
252
+
253
+ /** 被动上下文用量快照(无探测 RPC;getStatus 透出用) */
254
+ getContextStats(): AgentContextStats | null {
255
+ const usage = this.client?.getContextUsage() ?? null
256
+ const window = this.contextWindow ?? usage?.contextWindow ?? null
257
+ if (!usage && window === null) return null
258
+ const usedTokens = usage?.tokens ?? 0
259
+ return {
260
+ usedTokens,
261
+ contextWindow: window,
262
+ percent: window && window > 0 ? Math.min(1, usedTokens / window) : null,
263
+ compacting: this.compacting,
264
+ }
265
+ }
266
+
267
+ /**
268
+ * 上下文用量探测:get_session_stats 权威值;不可用退化为被动 usage + 已探测窗口。
269
+ * percent 归一化到 0-1(omp 可能返回 0-100)。
270
+ */
271
+ private async probeUsage(client: OmpRpcClient): Promise<{ tokens: number, contextWindow: number | null, percent: number | null } | null> {
272
+ try {
273
+ const resp = await client.send({ type: 'get_session_stats' })
274
+ const cu = ((resp.data ?? {}) as Record<string, unknown>).contextUsage as
275
+ | { tokens?: number, contextWindow?: number, percent?: number }
276
+ | undefined
277
+ if (cu && typeof cu.tokens === 'number' && cu.tokens > 0) {
278
+ if (typeof cu.contextWindow === 'number' && cu.contextWindow > 0) {
279
+ this.contextWindow = cu.contextWindow
280
+ client.setContextWindow(cu.contextWindow)
281
+ }
282
+ const window = this.contextWindow ?? cu.contextWindow ?? null
283
+ const rawPercent = typeof cu.percent === 'number' ? (cu.percent > 1 ? cu.percent / 100 : cu.percent) : null
284
+ const percent = rawPercent ?? (window && window > 0 ? Math.min(1, cu.tokens / window) : null)
285
+ return { tokens: cu.tokens, contextWindow: window, percent }
286
+ }
287
+ }
288
+ catch { /* get_session_stats 不可用 → 被动跟踪 */ }
289
+ return client.getContextUsage()
290
+ }
291
+
292
+ /**
293
+ * 上下文治理门控:三条 prompt 路径(worker/peer/supervise)在回合间隙统一调用;
294
+ * post-settle 经 onTurnSettled 复用同一守卫。任何失败路径都放行——压缩晚一轮
295
+ * 永远好过中断作业。三重防线:仅回合间隙发起 → get_state 复查 isStreaming/
296
+ * isCompacting → compacting 互斥位 + 最小间隔防振荡。
297
+ */
298
+ private async contextGate(reason: string): Promise<void> {
299
+ if (!this.compactEnabled() || this.compactLegacy || this.compacting) return
300
+ const client = this.client
301
+ if (!client || !client.alive) return
302
+ try {
303
+ const usage = await this.probeUsage(client)
304
+ if (!usage || usage.percent === null || usage.percent < this.compactThreshold()) return
305
+ if (Date.now() - this.lastCompactAt < this.compactMinIntervalMs()) return
306
+ // 回合间隙硬校验:流式中/压缩中绝不发起(不中断执行的硬约束)
307
+ const st = await client.send({ type: 'get_state' })
308
+ const s = (st.data ?? {}) as Record<string, unknown>
309
+ if (s.isStreaming === true || s.isCompacting === true) return
310
+ if (typeof s.sessionId === 'string' && s.sessionId) this.sessionId = s.sessionId
311
+ this.compacting = true
312
+ this.lastCompactAt = Date.now()
313
+ try {
314
+ const result = await this.runCompaction(client)
315
+ if (result?.summary) {
316
+ await this.harvestCompaction(
317
+ result.summary,
318
+ result.tokensBefore,
319
+ result.tokensAfter ?? result.estimatedTokensAfter,
320
+ reason,
321
+ )
322
+ }
323
+ }
324
+ finally {
325
+ this.compacting = false
326
+ }
327
+ }
328
+ catch (err) {
329
+ const msg = err instanceof Error ? err.message : String(err)
330
+ if (/compact/i.test(msg) && /unknown|unsupported|invalid|无效|未知|不支持/i.test(msg)) this.compactLegacy = true
331
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] contextGate(${reason}) 失败,放行: ${msg}`)
332
+ }
333
+ }
334
+
335
+ /**
336
+ * 发起 compact 并等待 compaction_end(响应内含 result 则立即收口;
337
+ * 超时放行——压缩在 harness 内后台继续,isCompacting 防后续双发)。
338
+ */
339
+ private runCompaction(client: OmpRpcClient): Promise<CompactionResult | null> {
340
+ return new Promise<CompactionResult | null>((resolve) => {
341
+ let settled = false
342
+ const finish = (r: CompactionResult | null): void => {
343
+ if (settled) return
344
+ settled = true
345
+ off()
346
+ clearTimeout(timer)
347
+ resolve(r)
348
+ }
349
+ const off = client.onEvent((event) => {
350
+ if (event.type === 'compaction_end') {
351
+ finish((event as { result?: CompactionResult }).result ?? null)
352
+ }
353
+ else if (event.type === '__process_exit__') {
354
+ finish(null)
355
+ }
356
+ })
357
+ const timer = setTimeout(() => {
358
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 等待超时(${this.compactWaitMs()}ms)→ 放行(压缩后台继续)`)
359
+ finish(null)
360
+ }, this.compactWaitMs())
361
+ void client.send({ type: 'compact', customInstructions: this.compactionHints() })
362
+ .then((resp) => {
363
+ const result = (resp.data as { result?: CompactionResult } | undefined)?.result
364
+ if (result?.summary) finish(result)
365
+ // 响应无 result:等 compaction_end 事件(协议权威语义)
366
+ })
367
+ .catch((err) => {
368
+ const msg = err instanceof Error ? err.message : String(err)
369
+ if (/compact/i.test(msg)) this.compactLegacy = true
370
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 发起失败: ${msg}`)
371
+ finish(null)
372
+ })
373
+ })
374
+ }
375
+
376
+ /** 压缩摘要指令(外置 prompts;缺失时省略,omp 用默认策略) */
377
+ private compactionHints(): string | undefined {
378
+ try {
379
+ return renderPrompt('compaction-hints')
380
+ }
381
+ catch {
382
+ return undefined
383
+ }
384
+ }
385
+
386
+ /**
387
+ * 压缩摘要 harvest(三路统一:平台主动 compact / omp 阈值自动 / overflow):
388
+ * 经 workspace.recordSessionMemory 落库为本人 episodic-session 记忆。
389
+ * 双路去重:同摘要 60s 内只入库一次。
390
+ */
391
+ private async harvestCompaction(summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string): Promise<void> {
392
+ const key = summary.slice(0, 120)
393
+ if (key === this.lastHarvestKey && Date.now() - this.lastHarvestAt < 60_000) return
394
+ this.lastHarvestKey = key
395
+ this.lastHarvestAt = Date.now()
396
+ try {
397
+ await this.workspace?.recordSessionMemory?.({ summary, tokensBefore, tokensAfter, reason })
398
+ log.info(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要已入记忆(${summary.length} 字,reason=${reason ?? 'auto'})`)
399
+ }
400
+ catch (err) {
401
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要入库失败(不影响会话):`, err instanceof Error ? err.message : err)
402
+ }
403
+ }
404
+
405
+ /** 回合落定钩子(AgentRuntime 在信箱空时调用):后台压缩检查,自守卫不抛错 */
406
+ async onTurnSettled(): Promise<void> {
407
+ await this.contextGate('post-settle')
408
+ }
409
+
220
410
  constructor(config: Record<string, unknown> = {}) {
221
411
  this.config = config as OmpAgentConfig
222
412
  // factory 注入的 agent 身份(无需等待 init())
@@ -449,6 +639,9 @@ export class OmpRpcAgentImpl implements AgentInterface {
449
639
  if (!this.client) return []
450
640
  if (this.supervising) return [] // 上一轮 supervise 未收口:跳过本拍(节流即正确)
451
641
 
642
+ // 上下文门控(≥70% 先压缩再调度;回合间隙发起,失败放行)
643
+ await this.contextGate('supervise')
644
+
452
645
  const prompt = await this.buildSupervisePrompt(snapshot, ctx.memory)
453
646
  // 150s 上界:supervise 持 lead.execLock 期间信箱消费停顿(更久会拖垮 worker 回执处理);
454
647
  // supervise 是一次真实 LLM 回合:omp 冷启动(插件/MCP 加载 30~90s)+ 慢 provider
@@ -589,9 +782,14 @@ export class OmpRpcAgentImpl implements AgentInterface {
589
782
  const msgText = partsToText(msg.parts)
590
783
  const requireReply = msg.metadata?.['x-aw-require-reply'] === 'true'
591
784
  const isReply = typeof msg.metadata?.['x-aw-in-reply-to'] === 'string'
785
+ // 跨 Channel 来信:发送方是其他 channel 的 lead,回执必须走 send_cross_channel_message
786
+ const crossChannel = msg.metadata?.['x-aw-cross-channel'] === 'true'
787
+ const fromChannel = typeof msg.metadata?.['x-aw-from-channel'] === 'string'
788
+ ? String(msg.metadata['x-aw-from-channel'])
789
+ : ''
592
790
  // 回执自动关联仅对真实 agent 发送者生效(人类无 agentId 可回投;
593
791
  // 对人类的回复经事件流/时间线可见)
594
- this.replyContext = requireReply && request.fromAgentId
792
+ this.replyContext = requireReply && request.fromAgentId && !crossChannel
595
793
  ? { fromId: request.fromAgentId, messageId: msg.messageId }
596
794
  : null
597
795
 
@@ -599,11 +797,13 @@ export class OmpRpcAgentImpl implements AgentInterface {
599
797
  ? `You are "${this.agentName}", the LEAD coordinator of a multi-agent team (Channel: ${this.channelId}). A team member sent you a direct message.`
600
798
  : `You are "${this.agentName}", a worker agent in a multi-agent team (Channel: ${this.channelId}).`
601
799
 
602
- // 消息应答指令:必复/可选两态 + lead 名册提示(全部外置 prompts/)
603
- const respondBlock = renderPrompt(requireReply ? 'peer-reply-required' : 'peer-reply-optional', {
604
- fromId,
605
- messageId: msg.messageId,
606
- })
800
+ // 消息应答指令:必复/可选/跨 Channel 三态 + lead 名册提示(全部外置 prompts/)
801
+ const respondBlock = crossChannel
802
+ ? renderPrompt('peer-reply-cross-channel', { fromId, messageId: msg.messageId, fromChannel })
803
+ : renderPrompt(requireReply ? 'peer-reply-required' : 'peer-reply-optional', {
804
+ fromId,
805
+ messageId: msg.messageId,
806
+ })
607
807
  const peerBody = renderPrompt('peer-message', {
608
808
  fromId,
609
809
  messageId: msg.messageId,
@@ -647,6 +847,8 @@ export class OmpRpcAgentImpl implements AgentInterface {
647
847
  if (!client) {
648
848
  throw new Error('omp 客户端未就绪')
649
849
  }
850
+ // 上下文门控(≥70% 时在回合间隙先压缩再发 prompt;等待有界,失败放行)
851
+ await this.contextGate('pre-prompt')
650
852
  const queue: AgentEvent[] = []
651
853
  let resolveWait: (() => void) | null = null
652
854
  let isDone = false
@@ -929,6 +1131,29 @@ export class OmpRpcAgentImpl implements AgentInterface {
929
1131
  error: { code: 'OMP_PROCESS_EXIT', message: 'omp 子进程意外退出' },
930
1132
  }]
931
1133
 
1134
+ // ===== 上下文治理事件(回合中到达的原生阈值/overflow 压缩;不污染事件流)=====
1135
+ case 'compaction_start':
1136
+ this.compacting = true
1137
+ return []
1138
+
1139
+ case 'compaction_end': {
1140
+ this.compacting = false
1141
+ const result = (event as { result?: CompactionResult }).result
1142
+ if (result?.summary) {
1143
+ void this.harvestCompaction(
1144
+ result.summary,
1145
+ result.tokensBefore,
1146
+ result.tokensAfter ?? result.estimatedTokensAfter,
1147
+ (event as { reason?: string }).reason,
1148
+ )
1149
+ }
1150
+ return []
1151
+ }
1152
+
1153
+ // 回合完全落定(无自动重试/压缩重试/排队续跑):后续压缩检查由 AgentRuntime 驱动
1154
+ case 'agent_settled':
1155
+ return []
1156
+
932
1157
  case '__error__':
933
1158
  return [{
934
1159
  kind: 'error',
@@ -1176,7 +1401,12 @@ export class OmpRpcAgentImpl implements AgentInterface {
1176
1401
  // 只配其一时也显式发送,避免 respawn 后静默回落默认模型)
1177
1402
  if (this.config.provider || this.config.model) {
1178
1403
  try {
1179
- await client.send({ type: 'set_model', provider: this.config.provider, modelId: this.config.model })
1404
+ // 未配置一侧保持 undefined(JSON 序列化丢键)→ omp 以当前已设值补全
1405
+ await client.send({
1406
+ type: 'set_model',
1407
+ provider: this.config.provider as string,
1408
+ modelId: this.config.model as string,
1409
+ })
1180
1410
  }
1181
1411
  catch {
1182
1412
  // 模型设置失败不致命(用 omp 默认模型)
@@ -1187,11 +1417,30 @@ export class OmpRpcAgentImpl implements AgentInterface {
1187
1417
  client.onHostToolCall(req => this.handleHostTool(req))
1188
1418
  await client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole) })
1189
1419
 
1420
+ // 上下文治理探测(feature-detect 一次;失败退化被动 usage 跟踪)+ 原生压缩兜底保持开启
1421
+ void this.probeContext(client).catch(() => {})
1422
+ void client.send({ type: 'set_auto_compaction', enabled: true }).catch(() => {})
1423
+
1190
1424
  this.client = client
1191
1425
  this.hostToolsRegistered = true
1192
1426
  }
1193
1427
  }
1194
1428
 
1429
+ /** 上下文状态探测:get_state 取 contextWindow/sessionId(一次;失败仅损失 percent 精度) */
1430
+ private async probeContext(client: OmpRpcClient): Promise<void> {
1431
+ try {
1432
+ const resp = await client.send({ type: 'get_state' })
1433
+ const data = (resp.data ?? {}) as Record<string, unknown>
1434
+ const model = data.model as { contextWindow?: number } | undefined
1435
+ if (typeof model?.contextWindow === 'number' && model.contextWindow > 0) {
1436
+ this.contextWindow = model.contextWindow
1437
+ client.setContextWindow(model.contextWindow)
1438
+ }
1439
+ if (typeof data.sessionId === 'string' && data.sessionId) this.sessionId = data.sessionId
1440
+ }
1441
+ catch { /* get_state 不可用:仅被动 usage */ }
1442
+ }
1443
+
1195
1444
  // ===== 内部:host tool handler(workspace 桥接) =====
1196
1445
 
1197
1446
  private async handleHostTool(req: HostToolCallRequest): Promise<{ text: string, isError?: boolean }> {
@@ -1309,6 +1558,26 @@ export class OmpRpcAgentImpl implements AgentInterface {
1309
1558
  return { text: `消息 ${sent.messageId.slice(0, 8)}… 已发送给 ${toAgentId}(priority=${priority})${triggerNote}` }
1310
1559
  }
1311
1560
 
1561
+ case 'send_cross_channel_message': {
1562
+ // 跨 Channel 通信(仅 lead):直投目标 channel 的 lead mailbox
1563
+ const toChannelId = req.arguments.to_channel_id as string
1564
+ const message = req.arguments.message as string
1565
+ if (!toChannelId || !message) return { text: '缺少 to_channel_id 或 message', isError: true }
1566
+ try {
1567
+ const r = await ws.sendCrossChannelMessage({
1568
+ toChannelId,
1569
+ parts: [{ text: message }],
1570
+ requireReply: req.arguments.require_reply === true,
1571
+ inReplyTo: req.arguments.in_reply_to as string | undefined,
1572
+ })
1573
+ const note = req.arguments.require_reply === true ? '(已要求对方 Leader 回复)' : ''
1574
+ return { text: `跨 Channel 消息 ${r.messageId.slice(0, 8)}… 已送达 channel「${r.toChannelName}」的 Leader(${r.toLeadAgentId.slice(0, 8)}…)${note}。对方将按你的信息需求处理;其回复会经 mailbox 到达你这里。` }
1575
+ }
1576
+ catch (err) {
1577
+ return { text: `跨 Channel 发送失败: ${err instanceof Error ? err.message : String(err)}(仅 Leader 可跨 Channel 通信)`, isError: true }
1578
+ }
1579
+ }
1580
+
1312
1581
  case 'refuse_task': {
1313
1582
  const taskId = req.arguments.task_id as string
1314
1583
  const reason = req.arguments.reason as string
@@ -549,12 +549,27 @@ export interface TeamMemberRow {
549
549
  createdAt: string
550
550
  }
551
551
 
552
+ /**
553
+ * 记忆类别:episodic-task/episodic-peer/episodic-session/episodic-team-task(harvest 族,
554
+ * 参与过期+淘汰)/ semantic(知识,免衰减)/ brief/chronicle/reflection(策展层,
555
+ * 免向量化免维护)。TEXT 自由列(无 CHECK),联合类型仅约束调用面。
556
+ */
557
+ export type MemoryKind
558
+ = | 'episodic-task'
559
+ | 'episodic-peer'
560
+ | 'episodic-session'
561
+ | 'episodic-team-task'
562
+ | 'semantic'
563
+ | 'brief'
564
+ | 'chronicle'
565
+ | 'reflection'
566
+
552
567
  /** agent_memories 表行(content 为已 CJK 切分存储文本;agentId='__team__' 为团队共享行) */
553
568
  export interface MemoryRow {
554
569
  id: string
555
570
  channelId: string
556
571
  agentId: string
557
- kind: 'episodic-task' | 'episodic-peer' | 'semantic'
572
+ kind: MemoryKind
558
573
  title: string
559
574
  content: string
560
575
  importance: number
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { randomUUID } from 'node:crypto'
8
8
  import type { DatabaseSync, StatementSync } from 'node:sqlite'
9
- import type { MemoryRow } from './database'
9
+ import type { MemoryKind, MemoryRow } from './database'
10
10
 
11
11
  /** 团队共享记忆域 sentinel(单 channel 内全员可读) */
12
12
  export const TEAM_AGENT_ID = '__team__'
@@ -14,7 +14,8 @@ export const TEAM_AGENT_ID = '__team__'
14
14
  export interface MemoryUpsertInput {
15
15
  channelId: string
16
16
  agentId: string
17
- kind: 'episodic-task' | 'episodic-peer' | 'semantic'
17
+ /** 语义见 database.ts MemoryKind(harvest 族参与维护;semantic 知识;策展层双免) */
18
+ kind: MemoryKind
18
19
  title: string
19
20
  /** title 的 CJK 切分副本(FTS 索引用;调用方经 AgentMemory.segmentCJK 处理,V8) */
20
21
  titleFts: string
@@ -75,6 +76,15 @@ export function createMemoryRepo(db: DatabaseSync) {
75
76
  const agentIdsStmt = db.prepare(
76
77
  `SELECT DISTINCT agent_id FROM agent_memories WHERE agent_id != ?`,
77
78
  )
79
+ const channelIdsStmt = db.prepare(
80
+ `SELECT DISTINCT channel_id AS channelId FROM agent_memories`,
81
+ )
82
+ const getByIdStmt = db.prepare(
83
+ `SELECT id, channel_id AS channelId, agent_id AS agentId, kind, title, content,
84
+ importance, task_id AS taskId, access_count AS accessCount,
85
+ last_accessed_at AS lastAccessedAt, created_at AS createdAt
86
+ FROM agent_memories WHERE id = ?`,
87
+ )
78
88
  // ===== 向量方法(P1;vec0 分区表 + mem_rowid 辅助列映射)=====
79
89
  // 已实测事实:
80
90
  // - vec0 分区表不支持显式 rowid 插入 → 以 +mem_rowid 辅助列映射 agent_memories.rowid
@@ -188,6 +198,16 @@ export function createMemoryRepo(db: DatabaseSync) {
188
198
  return (agentIdsStmt.all(TEAM_AGENT_ID) as Array<{ agent_id: string }>).map(r => r.agent_id)
189
199
  },
190
200
 
201
+ /** 有记忆的 channel 清单(team 域行按 channel 迭代维护用) */
202
+ listChannelIds(): string[] {
203
+ return (channelIdsStmt.all() as Array<{ channelId: string }>).map(r => r.channelId)
204
+ },
205
+
206
+ /** 按 id 取单行(brief 置顶注入等直取场景;不存在返回 null) */
207
+ getById(id: string): MemoryRow | null {
208
+ return (getByIdStmt.get(id) as MemoryRow | undefined) ?? null
209
+ },
210
+
191
211
  // ===== vec:延迟建表(vecInit)/ 刷新写入(vecSet)/ 删除(vecDelete)/ 分区 kNN(vecSearch)=====
192
212
 
193
213
  /** 首次调用按 dims 建表;同 dims 幂等 true;不同 dims 拒绝 false(禁用向量,绝不重建) */
@@ -7,11 +7,18 @@
7
7
  * - 缓存:mtime 感知(文件修改即刻生效,dev/HMR 友好;stat 开销可忽略)
8
8
  * - host-tools.json:结构化工具定义(name/label/description/parameters),
9
9
  * JSON 承载 schema,与叙事型 md 并置同一目录统一管理
10
- * - 目录解析:AW_PROMPTS_DIR 环境变量 → process.cwd()/.AgentWorkShop/prompts;
10
+ * - 目录解析(hardening 后的规约,与配置根优先级同族):
11
+ * ① AW_PROMPTS_DIR 环境变量(显式覆盖,兼容旧约定)
12
+ * ② 项目内 <cwd 向上>/.AgentWorkShop/prompts 真实存在 → 项目内优先
13
+ * ③ 否则 ~/.AgentWorkShop/prompts —— 首次解析时从包内资产播种初始文件
14
+ * (只补缺失文件,绝不覆盖已有内容 → 用户在 Home 的定制即权威版本);
15
+ * 播种源 = AW_PACKAGE_ROOT(启动器注入的包根)/.AgentWorkShop/prompts
11
16
  * 启动即校验(缺文件 fail-fast,防静默降级为无 prompt 裸跑)
12
17
  */
13
- import { readFileSync, statSync, existsSync, readdirSync } from 'node:fs'
14
- import { join, resolve } from 'node:path'
18
+ import { copyFileSync, existsSync, readdirSync, readFileSync, statSync, mkdirSync } from 'node:fs'
19
+ import { dirname, join, resolve } from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
21
+ import { awHome, findLocalConfigRoot } from '@/shared/config/home.mjs'
15
22
  import { AppError } from '../../../utils/errors'
16
23
 
17
24
  /** 模板变量值(字符串化注入) */
@@ -24,15 +31,62 @@ interface CacheEntry {
24
31
 
25
32
  const cache = new Map<string, CacheEntry>()
26
33
 
27
- /** prompts 目录(AW_PROMPTS_DIR 覆盖 → cwd 相对) */
34
+ /** 包内资产 prompts 目录(播种源;不存在返回 null) */
35
+ function packagedPromptsDir(): string | null {
36
+ const packageRoot = process.env.AW_PACKAGE_ROOT
37
+ ? resolve(process.env.AW_PACKAGE_ROOT)
38
+ : resolve(dirname(fileURLToPath(import.meta.url)), '../../../..')
39
+ const candidate = join(packageRoot, '.AgentWorkShop', 'prompts')
40
+ return existsSync(candidate) ? candidate : null
41
+ }
42
+
43
+ /** 首次/每次解析把包内初始 prompts 补种到 ~/.AgentWorkShop/prompts(逐文件只补缺,不覆盖)。
44
+ * 每次解析都执行:包升级新增的提示词文件能进入已存在的 Home(用户已定制文件不回写)。 */
45
+ function seedHomePrompts(homePrompts: string): void {
46
+ const src = packagedPromptsDir()
47
+ if (!src || resolve(src) === resolve(homePrompts)) return
48
+ mkdirSync(homePrompts, { recursive: true })
49
+ let seeded = 0
50
+ for (const f of readdirSync(src)) {
51
+ const dst = join(homePrompts, f)
52
+ if (existsSync(dst)) continue
53
+ try {
54
+ copyFileSync(join(src, f), dst)
55
+ seeded++
56
+ }
57
+ catch { /* 单文件失败不阻断播种 */ }
58
+ }
59
+ if (seeded > 0) console.log(`[prompts] 已向 ${homePrompts} 补种 ${seeded} 个提示词文件`)
60
+ }
61
+
62
+ /**
63
+ * prompts 目录解析:显式覆盖 > 项目内(存在即用) > ~/.AgentWorkShop/prompts(自动播种)。
64
+ * 失败 fail-fast —— 外置 prompt 为必需,防静默降级为无 prompt 裸跑。
65
+ */
28
66
  export function promptsDir(): string {
29
- const base = process.env.AW_PROMPTS_DIR
30
- ? resolve(process.env.AW_PROMPTS_DIR)
31
- : resolve(process.cwd(), '.AgentWorkShop', 'prompts')
32
- if (!existsSync(base)) {
33
- throw new AppError(500, 'PROMPTS_DIR_MISSING', `prompts 目录不存在: ${base}(外置 prompt 为必需;可用 AW_PROMPTS_DIR 指定)`)
67
+ // ① 显式覆盖(兼容既有部署约定)
68
+ if (process.env.AW_PROMPTS_DIR) {
69
+ const base = resolve(process.env.AW_PROMPTS_DIR)
70
+ if (!existsSync(base)) {
71
+ throw new AppError(500, 'PROMPTS_DIR_MISSING', `prompts 目录不存在: ${base}(AW_PROMPTS_DIR 指定)`)
72
+ }
73
+ return base
74
+ }
75
+
76
+ // ~/.AgentWorkShop/prompts:默认兜底目录,解析时自动补种初始文件(只补缺,不覆盖用户定制)
77
+ const homePrompts = join(awHome(process.env), 'prompts')
78
+ seedHomePrompts(homePrompts)
79
+
80
+ // ② 项目内 ./.AgentWorkShop/prompts 真实存在 → 项目内优先
81
+ const localConfigRoot = findLocalConfigRoot(process.cwd())
82
+ if (localConfigRoot) {
83
+ const candidate = join(localConfigRoot, 'prompts')
84
+ if (existsSync(candidate)) return candidate
34
85
  }
35
- return base
86
+
87
+ // ③ ~/.AgentWorkShop/prompts
88
+ if (existsSync(homePrompts)) return homePrompts
89
+ throw new AppError(500, 'PROMPTS_DIR_MISSING', `prompts 目录不存在(已尝试自动播种): ${homePrompts};可用 AW_PROMPTS_DIR 指定`)
36
90
  }
37
91
 
38
92
  /** 读取 md 原文(mtime 缓存;文件缺失 fail-fast) */
@@ -17,7 +17,7 @@ import type {
17
17
  SupervisionSnapshot,
18
18
  } from '../agents/agent-interface'
19
19
  import type { A2AMessage, A2AArtifact, Part } from '../types/a2a'
20
- import type { AgentStatusView, AgentTaskQueueView, TaskState, WorkspaceTask } from '../types/task'
20
+ import type { AgentContextStats, AgentStatusView, AgentTaskQueueView, TaskState, WorkspaceTask } from '../types/task'
21
21
  import { TERMINAL_TASK_STATES } from '../types/task'
22
22
  import type { AgentMemory } from './memory'
23
23
  import type { Mailbox } from './mailbox'
@@ -58,6 +58,8 @@ export interface ChannelBus {
58
58
  currentTaskProgress?: number | null
59
59
  queuedCount?: number
60
60
  completedCount?: number
61
+ /** harness 上下文用量(omp 有;进程内 harness 缺省) */
62
+ context?: AgentContextStats | null
61
63
  }): void
62
64
  onAgentStatus(fn: (e: {
63
65
  agentId: string
@@ -67,6 +69,7 @@ export interface ChannelBus {
67
69
  currentTaskProgress?: number | null
68
70
  queuedCount?: number
69
71
  completedCount?: number
72
+ context?: AgentContextStats | null
70
73
  }) => void): () => void
71
74
  /** channel 内消息投递通知(route 汇流点触发;AEP a2a.message 事件源) */
72
75
  notifyMessage(message: A2AMessage): void
@@ -205,10 +208,11 @@ export class AgentRuntime {
205
208
  return this.state
206
209
  }
207
210
 
208
- /** 实时状态视图:idle/busy/stopped + 当前任务 + 队列上下文(实时追踪的单一入口) */
211
+ /** 实时状态视图:idle/busy/stopped + 当前任务 + 队列 + harness 上下文用量 */
209
212
  getStatus(): AgentStatusView {
210
213
  const queue = this.getQueueView()
211
214
  const current = queue.current ?? (this.currentTaskId ? this.deps.taskEngine.get(this.currentTaskId) : undefined)
215
+ const context = this.impl.getContextStats?.() ?? undefined
212
216
  return {
213
217
  agentId: this.agentId,
214
218
  channelId: this.channelId,
@@ -221,6 +225,7 @@ export class AgentRuntime {
221
225
  currentTaskProgress: current?.progress != null ? current.progress : null,
222
226
  queuedCount: queue.queued.length,
223
227
  completedCount: queue.completed.length,
228
+ ...(context ? { context } : {}),
224
229
  }
225
230
  }
226
231
 
@@ -448,6 +453,8 @@ export class AgentRuntime {
448
453
  }
449
454
  finally {
450
455
  if (this.superviseController === controller) this.superviseController = null
456
+ // 调度回合落定:lead 的 supervise 是上下文膨胀大户,同样走 post-settle 压缩检查
457
+ this.maybePostSettle()
451
458
  }
452
459
  }
453
460
 
@@ -522,10 +529,12 @@ export class AgentRuntime {
522
529
  }
523
530
  // 每次 run 新建 AbortController;abort 后事件流终止
524
531
  this.abortController = new AbortController()
525
- // 记忆召回(异常不阻塞)
532
+ // 记忆召回(异常不阻塞):查询=消息原文(title 首词天然显著)+ 任务关联加权
526
533
  let memoryBlock: string | undefined
527
534
  try {
528
- memoryBlock = (await this.deps.memory?.recall(partsToText(msg.parts))) ?? undefined
535
+ memoryBlock = (await this.deps.memory?.recall(partsToText(msg.parts), {
536
+ relatedTaskIds: this.relatedTaskIdsOf(taskId),
537
+ })) ?? undefined
529
538
  }
530
539
  catch (err) {
531
540
  log.error(`[AgentRuntime:${this.agentId}] 记忆召回失败:`, err)
@@ -666,11 +675,13 @@ export class AgentRuntime {
666
675
  this.runErrorRetries.delete(msg.messageId)
667
676
  this.deps.mailbox.markConsumed(msg.messageId)
668
677
  }
678
+ // 回合落定:信箱空 → 后台压缩检查(post-settle 路径;impl 自守卫不阻塞)
679
+ this.maybePostSettle()
669
680
  }
670
681
  }
671
682
 
672
- /** 状态通知的队列上下文(实时:当前任务/待执行数/已完成数) */
673
- private queueContext(): Pick<AgentStatusView, 'currentTaskId' | 'currentTaskTitle' | 'currentTaskProgress' | 'queuedCount' | 'completedCount'> {
683
+ /** 状态通知的队列上下文(实时:当前任务/待执行数/已完成数/harness 上下文用量) */
684
+ private queueContext(): Pick<AgentStatusView, 'currentTaskId' | 'currentTaskTitle' | 'currentTaskProgress' | 'queuedCount' | 'completedCount'> & { context?: AgentContextStats | null } {
674
685
  const status = this.getStatus()
675
686
  return {
676
687
  currentTaskId: status.currentTaskId,
@@ -678,9 +689,44 @@ export class AgentRuntime {
678
689
  currentTaskProgress: status.currentTaskProgress,
679
690
  queuedCount: status.queuedCount,
680
691
  completedCount: status.completedCount,
692
+ context: status.context ?? null,
681
693
  }
682
694
  }
683
695
 
696
+ /**
697
+ * 任务关联集(自身+父+兄弟,≤20):related-task boost 数据源。
698
+ * 同父兄弟任务的记忆(前几个子任务做了什么)在引子中必然置顶——
699
+ * 纯内存 id 判断,零检索开销。
700
+ */
701
+ private relatedTaskIdsOf(taskId: string | undefined): string[] | undefined {
702
+ if (!taskId) return undefined
703
+ const task = this.deps.taskEngine.get(taskId)
704
+ if (!task) return undefined
705
+ const ids = new Set<string>([taskId])
706
+ if (task.parentId) {
707
+ ids.add(task.parentId)
708
+ for (const t of this.deps.taskEngine.list(this.channelId)) {
709
+ if (t.parentId === task.parentId) ids.add(t.id)
710
+ if (ids.size >= 20) break
711
+ }
712
+ }
713
+ return [...ids]
714
+ }
715
+
716
+ /**
717
+ * post-settle 压缩检查:仅信箱无排队消息时触发(排队消息的 pre-prompt gate 兜底)。
718
+ * impl 自守卫(回合间隙才压缩、异常不抛出);本方法自身也不阻塞消费循环。
719
+ */
720
+ private maybePostSettle(): void {
721
+ if (!this.impl.onTurnSettled) return
722
+ void this.deps.mailbox.peek(1)
723
+ .then((pending) => {
724
+ if (pending.length > 0) return undefined
725
+ return this.impl.onTurnSettled?.()
726
+ })
727
+ .catch(() => {})
728
+ }
729
+
684
730
  private toRequest(msg: A2AMessage, memory?: string): AgentRunRequest {
685
731
  return {
686
732
  message: msg,