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
@@ -4,14 +4,14 @@
4
4
  * route 仅在本 channel 内路由消息;订阅关系经 subscriptionRepo。
5
5
  * 懒加载:agents map 不含目标时经 loader 回调按需装配(DB 行 → AgentRuntime);
6
6
  */
7
- import { createLogger } from '../logger'
7
+ import { createLogger } from '../logger'
8
8
  import type { A2AMessage } from '../types/a2a'
9
9
  import type { SubscriptionRepo } from '../db/subscription.repo'
10
10
  import type { ChannelAgentRepo } from '../db/channel-agent.repo'
11
11
  import type { AgentRuntimeLike, TaskEngine } from './agent-runtime'
12
12
 
13
- const log = createLogger('workshop.channel')
14
-
13
+ const log = createLogger('workshop.channel')
14
+
15
15
  /** 调度循环结构契约(实现方 T5 SchedulerLoop) */
16
16
  export interface SchedulerLoopLike {
17
17
  start(): void
@@ -85,10 +85,13 @@ export class ChannelRuntime {
85
85
  }
86
86
 
87
87
  /**
88
- * 信箱守卫(垃圾信息拦截):三类消息放行,其余直接丢弃(不落库、不投递)——
88
+ * 信箱守卫(垃圾信息拦截):四类消息放行,其余直接丢弃(不落库、不投递)——
89
89
  * ① 平台任务消息(task-kind + 真实存在的任务);
90
- * ② 声明发送人为本 channel 成员的消息(伪造/已移除成员 → 拒);
91
- * ③ 人类经 REST 注入的消息(x-aw-from-label 必须非空)。
90
+ * ② 跨 Channel 消息(x-aw-cross-channel 标记 + 源 channel 注明)——
91
+ * manager.sendCrossChannelMessage 发送时已校验「发送方=源 channel lead +
92
+ * 双方同主 + 目标 lead 在册」并盖服务端标记;REST/工具入参无法伪造;
93
+ * ③ 声明发送人为本 channel 成员的消息(伪造/已移除成员 → 拒);
94
+ * ④ 人类经 REST 注入的消息(x-aw-from-label 必须非空)。
92
95
  * 防脏信息注入:无主消息与未知发送人的消息没有回执对象,处理它们只会
93
96
  * 污染 Agent 上下文,因此拦截并删除。
94
97
  */
@@ -101,6 +104,11 @@ export class ChannelRuntime {
101
104
  log.warn(`[ChannelRuntime:${this.channelId.slice(0, 8)}] 拦截无效任务消息(kind=${String(taskKind)} task=${String(taskId)}),已丢弃`)
102
105
  return false
103
106
  }
107
+ if (meta['x-aw-cross-channel'] === 'true'
108
+ && typeof meta['x-aw-from-channel'] === 'string'
109
+ && meta['x-aw-from-channel'] !== this.channelId) {
110
+ return true
111
+ }
104
112
  const from = meta['x-aw-from-agent']
105
113
  if (typeof from === 'string' && from.length > 0) {
106
114
  const member = this.deps.channelAgents.findByChannelAgent(this.channelId, from)
@@ -9,7 +9,7 @@
9
9
  * - 每个实例在各自 channel 独立装配 AgentRuntime(独立 mailbox/impl 子进程/状态)
10
10
  * 权威契约见 docs/superpowers/plans/2026-08-13-agent-workshop-multi-agent.md 核心契约块 T3。
11
11
  */
12
- import { createLogger } from '../logger'
12
+ import { createLogger } from '../logger'
13
13
  import { mkdirSync } from 'node:fs'
14
14
  import { resolve } from 'node:path'
15
15
  import { randomUUID } from 'node:crypto'
@@ -46,8 +46,8 @@ import type { UserRepo } from '../db/user.repo'
46
46
  import type { ChannelEventRepo } from '../db/channel-event.repo'
47
47
  import type { ChannelTemplateRepo, ChannelTemplateMember } from '../db/channel-template.repo'
48
48
 
49
- const log = createLogger('workshop.manager')
50
-
49
+ const log = createLogger('workshop.manager')
50
+
51
51
  /** 全部仓储(依赖注入) */
52
52
  export interface AllRepos {
53
53
  users: UserRepo
@@ -304,6 +304,8 @@ export class AgentChannelManager {
304
304
  private memoryTimer: NodeJS.Timeout | null = null
305
305
  /** 全 manager 共享的 env 向量 provider(未配置 → null 纯 FTS;熔断/维度全体实例共享) */
306
306
  private readonly memoryEmbedder = createEnvEmbeddingProvider()
307
+ /** 反思游标:每 agent 上次聚合的当月任务数(月度幂等增量判定) */
308
+ private readonly reflectCounts = new Map<string, number>()
307
309
 
308
310
  constructor(private deps: ManagerDeps) {
309
311
  // 记忆衰减清理定时器(失败只记日志,绝不抛出;unref 不阻进程退出;非法/非正 env 回退默认)
@@ -314,6 +316,13 @@ export class AgentChannelManager {
314
316
  catch (err) {
315
317
  log.error('[memory] 维护任务异常', err)
316
318
  }
319
+ // 空闲反思(sleep-time compute):busy runtime 存在即整轮跳过,绝不与作业争抢
320
+ try {
321
+ this.reflectIdleMemories()
322
+ }
323
+ catch (err) {
324
+ log.error('[memory] 反思任务异常', err)
325
+ }
317
326
  }, envNum('AW_MEMORY_MAINTENANCE_MS', 6 * 3600_000))
318
327
  this.memoryTimer.unref?.()
319
328
  }
@@ -355,10 +364,110 @@ export class AgentChannelManager {
355
364
  cr.setLoader(id => this.ensureAgentRuntime(channelId, id))
356
365
  this.channels.set(channelId, cr)
357
366
  this.buses.set(channelId, this.buildBus(cr))
367
+ // 团队记忆沉淀(任务终态事件单点收口):team-task 共享行 + 编年史滚动重写。
368
+ // 事件总线保证每次状态迁移恰好一次通知——worker/lead 各自私有域 harvest 之外
369
+ // 的团队域写入不存在双写;失败仅记日志,绝不影响任务流。
370
+ this.buses.get(channelId)!.onTaskEvent(e => this.recordTeamTaskTerminal(channelId, e))
358
371
  }
359
372
  return cr
360
373
  }
361
374
 
375
+ /**
376
+ * 团队历史沉淀(任务终态):① team-task 共享行(成果/失败原因,全员可检索);
377
+ * ② 团队编年史滚动重写(最近 12 条终态任务轨迹)。
378
+ */
379
+ private recordTeamTaskTerminal(channelId: string, e: { taskId: string, state?: TaskState, task?: TaskEventTask }): void {
380
+ if (!e.state || !TERMINAL_TASK_STATES[e.state]) return
381
+ try {
382
+ const completed = e.state === 'COMPLETED'
383
+ const task = e.task
384
+ const title = task?.title ?? `任务 ${e.taskId.slice(0, 8)}`
385
+ // 成果提取与 AgentMemory.recordTaskOutcome 同口径(deliverable/summary 优先)
386
+ const artifacts = (task?.artifacts ?? []) as A2AArtifact[]
387
+ const preferred = artifacts.filter(a => a.name === 'deliverable' || a.name === 'summary')
388
+ const source = preferred.length > 0 ? preferred : artifacts
389
+ const deliverable = source
390
+ .flatMap(a => a.parts)
391
+ .map(p => ('text' in p ? p.text : ''))
392
+ .join(' ')
393
+ .trim()
394
+ const content = completed
395
+ ? (deliverable || title)
396
+ : `任务「${title}」${e.state === 'CANCELED' ? '已取消' : '未完成'}${deliverable ? `;已有进展: ${deliverable.slice(0, 200)}` : ''}`
397
+ const mem = new AgentMemory(this.deps.repos.memories, { channelId, agentId: TEAM_AGENT_ID, embedder: this.memoryEmbedder ?? undefined })
398
+ void mem.appendTeamTaskRecord({
399
+ taskId: e.taskId,
400
+ title,
401
+ content,
402
+ importance: completed ? 0.8 : 0.55,
403
+ }).catch((err) => { log.error('[memory] team-task 沉淀失败:', err) })
404
+ this.refreshChronicle(channelId)
405
+ }
406
+ catch (err) {
407
+ log.error('[memory] 团队任务沉淀异常:', err)
408
+ }
409
+ }
410
+
411
+ /** 团队编年史滚动重写(最近 12 条终态任务;chronicle:<channelId> 幂等单行;免向量化) */
412
+ private refreshChronicle(channelId: string): void {
413
+ try {
414
+ const terminal = this.getTaskEngine().list(channelId)
415
+ .filter(t => TERMINAL_TASK_STATES[t.state])
416
+ .sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1))
417
+ .slice(0, 12)
418
+ if (terminal.length === 0) return
419
+ const entries = terminal.map((t) => {
420
+ const stamp = t.updatedAt.slice(5, 16).replace('T', ' ')
421
+ const deliverable = t.artifacts
422
+ .flatMap(a => a.parts)
423
+ .map(p => ('text' in p ? p.text : ''))
424
+ .join(' ')
425
+ .replace(/\s+/g, ' ')
426
+ .trim()
427
+ .slice(0, 60)
428
+ const state = t.state === 'COMPLETED' ? '完成' : t.state === 'FAILED' ? '失败' : '取消'
429
+ return `${stamp} [${state}] ${t.title} — ${t.assigneeId.slice(0, 8)}${deliverable ? `(${deliverable})` : ''}`
430
+ })
431
+ new AgentMemory(this.deps.repos.memories, { channelId, agentId: TEAM_AGENT_ID })
432
+ .upsertChronicle(entries.join('\n'))
433
+ }
434
+ catch (err) {
435
+ log.error('[memory] 编年史刷新失败:', err)
436
+ }
437
+ }
438
+
439
+ /**
440
+ * 空闲反思(sleep-time compute,零 LLM):当月 episodic-task 增量 ≥ 阈值时,
441
+ * 聚合标题+结论成 semantic 反思行(月度 dedupKey 幂等;FTS 可检索,免向量化)。
442
+ */
443
+ private reflectIdleMemories(): void {
444
+ for (const rt of this.agentIndex.values()) {
445
+ if (rt.getState() === 'busy') return
446
+ }
447
+ const trigger = envNum('AW_MEMORY_REFLECT_TRIGGER', 8)
448
+ const month = new Date().toISOString().slice(0, 7)
449
+ for (const agentId of this.deps.repos.memories.listMemoryAgentIds()) {
450
+ const rows = this.deps.repos.memories.listByAgentWithRowid(agentId, 1_000_000)
451
+ .filter(r => r.kind === 'episodic-task' && r.createdAt.slice(0, 7) === month)
452
+ const prev = this.reflectCounts.get(agentId) ?? 0
453
+ if (rows.length < trigger || rows.length <= prev) continue
454
+ // 行可能跨 channel(历史迁移):按 channel 分组,各组建反思
455
+ const byChannel = new Map<string, typeof rows>()
456
+ for (const r of rows) {
457
+ const list = byChannel.get(r.channelId) ?? []
458
+ list.push(r)
459
+ byChannel.set(r.channelId, list)
460
+ }
461
+ for (const [channelId, list] of byChannel) {
462
+ const lines = list.slice(0, 20).map(r => `- ${r.title}:${unsegmentCJK(r.content).slice(0, 60)}`)
463
+ new AgentMemory(this.deps.repos.memories, { channelId, agentId })
464
+ .upsertReflection({ month, content: [`当月 ${list.length} 项任务经验聚合:`, ...lines].join('\n') })
465
+ }
466
+ this.reflectCounts.set(agentId, rows.length)
467
+ log.info(`[memory] 反思蒸馏:agent=${agentId.slice(0, 8)} 当月 ${rows.length} 项任务已聚合`)
468
+ }
469
+ }
470
+
362
471
  private buildBus(cr: ChannelRuntime): ChannelBus {
363
472
  const eventListeners = new Set<(event: AgentEvent, source: A2AMessage) => void>()
364
473
  const taskListeners = new Set<(e: { taskId: string, state?: TaskState, progress?: number }) => void>()
@@ -868,6 +977,7 @@ export class AgentChannelManager {
868
977
  updateTask: (taskId, patch) => this.updateTask(channelId, agent.id, taskId, patch),
869
978
  reassignTask: (taskId, toAgentId) => this.reassignTask(channelId, agent.id, taskId, toAgentId),
870
979
  sendMessage: input => this.sendA2A(channelId, agent.id, input),
980
+ sendCrossChannelMessage: input => this.sendCrossChannelMessage(channelId, agent.id, input),
871
981
  refuseTask: (taskId, reason) => this.refuseTask(channelId, agent.id, taskId, reason),
872
982
  pollMailbox: limit => this.pollMailbox(channelId, agent.id, limit),
873
983
  waitMailbox: (limit, waitMs) => this.waitMailbox(channelId, agent.id, limit ?? 10, waitMs ?? 0),
@@ -885,6 +995,11 @@ export class AgentChannelManager {
885
995
  this.buses.get(channelId)?.notifyMemory({ agentId: agent.id, scope: input.scope, title: input.title, dedupKey: saved.dedupKey })
886
996
  return saved
887
997
  },
998
+ // 会话压缩摘要入库(harvest 桥):omp 压缩产出 → 本成员 episodic-session 记忆
999
+ recordSessionMemory: async (input) => {
1000
+ const saved = await memory.recordSessionCompaction(input)
1001
+ this.buses.get(channelId)?.notifyMemory({ agentId: agent.id, scope: 'private', title: `会话压缩摘要(${input.summary.length}字)`, dedupKey: saved.dedupKey })
1002
+ },
888
1003
  // 团队成员管理(仅 lead;manager 内二次校验角色,工具面与决策面共用)
889
1004
  createTeamMember: input => this.createTeamMember(channelId, agent.id, input),
890
1005
  updateTeamMember: (agentId, patch) => this.updateTeamMember(channelId, agent.id, agentId, patch),
@@ -2349,6 +2464,80 @@ export class AgentChannelManager {
2349
2464
  return message
2350
2465
  }
2351
2466
 
2467
+ /**
2468
+ * 跨 Channel 点对点通信(仅 lead):把消息直投目标 channel 的 lead mailbox。
2469
+ * 沿用既有 mailbox/route 机制(投递/唤醒/消费/回执全复用),仅新增跨 channel 路由:
2470
+ * - 权限:发送方必须是本 channel 启用成员中的 lead(worker/系统拒绝);
2471
+ * - 同主约束:两个 channel 必须属于同一 owner(用户级隔离不破);
2472
+ * - 守卫通行:x-aw-cross-channel 标记由服务端在发送时盖章(REST/工具入参不可伪造),
2473
+ * 目标 channel 信箱守卫据此放行非成员发送方的跨 channel 消息;
2474
+ * - 回执:目标 lead 经同一机制回信(其 send_message_to_agent / 本方法均可,
2475
+ * in_reply_to 关联触发实时推送)。
2476
+ */
2477
+ async sendCrossChannelMessage(
2478
+ fromChannelId: string,
2479
+ fromAgentId: string,
2480
+ input: { toChannelId: string, parts: Part[], requireReply?: boolean, inReplyTo?: string },
2481
+ ): Promise<{ messageId: string, toChannelId: string, toChannelName: string, toLeadAgentId: string }> {
2482
+ // 权限闸:发送方 = 本 channel 启用成员中的 lead
2483
+ const sender = this.deps.repos.channelAgents.findByChannelAgent(fromChannelId, fromAgentId)
2484
+ if (!sender || sender.enabled !== 1) {
2485
+ throw new AppError(403, 'SCOPE_VIOLATION', '跨 Channel 通信的发送方必须是本 channel 的启用成员')
2486
+ }
2487
+ if (sender.role !== 'lead') {
2488
+ throw new AppError(403, 'ROLE_FORBIDDEN', `跨 Channel 通信仅限 Leader 发起(${sender.name} 是 ${sender.role})`)
2489
+ }
2490
+
2491
+ // 目标 channel 解析(id 精确 > 名字精确 > 名字唯一包含);禁自指/禁停用/须有 lead
2492
+ const needle = input.toChannelId.trim().toLowerCase()
2493
+ const rows = this.deps.repos.channels.list()
2494
+ const target = rows.find(c => c.id === input.toChannelId)
2495
+ ?? rows.find(c => c.name.toLowerCase() === needle)
2496
+ ?? (() => {
2497
+ const partial = rows.filter(c => c.name.toLowerCase().includes(needle))
2498
+ return partial.length === 1 ? partial[0] : undefined
2499
+ })()
2500
+ if (!target || target.id === fromChannelId) {
2501
+ throw new AppError(404, 'CHANNEL_NOT_FOUND', `目标 channel 未命中(且不可为本 channel): ${input.toChannelId}`)
2502
+ }
2503
+ if (target.enabled !== 1) throw new AppError(400, 'CHANNEL_DISABLED', `目标 channel 已停用: ${target.name}`)
2504
+ if (!target.leadAgentId) throw new AppError(400, 'NO_LEAD', `目标 channel 没有 lead,无法接收跨 Channel 消息: ${target.name}`)
2505
+
2506
+ // 同主约束:跨团队协作不越用户边界
2507
+ const sourceChannel = this.deps.repos.channels.findById(fromChannelId)
2508
+ if (sourceChannel && target.ownerUserId !== null && sourceChannel.ownerUserId !== null
2509
+ && sourceChannel.ownerUserId !== target.ownerUserId) {
2510
+ throw new AppError(403, 'SCOPE_VIOLATION', '跨 Channel 通信仅限同一用户的 channel 之间')
2511
+ }
2512
+
2513
+ // 目标 lead 必须是目标 channel 的启用成员(防悬空 leadAgentId)
2514
+ const targetLead = this.deps.repos.channelAgents.findByChannelAgent(target.id, target.leadAgentId)
2515
+ if (!targetLead || targetLead.enabled !== 1) {
2516
+ throw new AppError(400, 'NO_LEAD', `目标 channel 的 lead 成员缺失或已停用: ${target.name}`)
2517
+ }
2518
+
2519
+ const message = buildMessage(target.id, 'ROLE_AGENT', input.parts, {
2520
+ 'x-aw-target-agent': targetLead.id,
2521
+ 'x-aw-from-agent': fromAgentId,
2522
+ 'x-aw-from-channel': fromChannelId,
2523
+ 'x-aw-cross-channel': 'true',
2524
+ 'x-aw-from-label': `跨Channel:${sourceChannel?.name ?? fromChannelId} / ${sender.name}`,
2525
+ ...(input.requireReply ? { 'x-aw-require-reply': 'true' } : {}),
2526
+ // in_reply_to 关联:channel-runtime 将其视为 realtime,回复即时推送对端
2527
+ ...(input.inReplyTo ? { 'x-aw-in-reply-to': input.inReplyTo } : {}),
2528
+ })
2529
+ const delivered = this.route(target.id, message)
2530
+ if (!delivered.includes(targetLead.id)) {
2531
+ throw new AppError(502, 'DELIVERY_FAILED', `跨 Channel 消息未能投递到「${target.name}」的 lead 信箱,请重试`)
2532
+ }
2533
+ return {
2534
+ messageId: message.messageId,
2535
+ toChannelId: target.id,
2536
+ toChannelName: target.name,
2537
+ toLeadAgentId: targetLead.id,
2538
+ }
2539
+ }
2540
+
2352
2541
  /** 阻塞长轮询(poll_messages):到信即时唤醒,250ms 兜底重查 */
2353
2542
  async waitMailbox(channelId: string, callerAgentId: string, limit: number, waitMs: number): Promise<A2AMessage[]> {
2354
2543
  this.requireMember(channelId, callerAgentId)