agentworkshop 0.5.5 → 0.5.7

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 (50) hide show
  1. package/.AgentWorkShop/prompts/compaction-hints.md +9 -0
  2. package/.AgentWorkShop/prompts/host-tools.json +695 -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 +695 -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/02278199-eeea-4f96-882a-94c5408c6cb6.json +1 -0
  12. package/.output/server/chunks/_/nitro.mjs +2188 -1324
  13. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  14. package/.output/server/chunks/routes/api/workshop/agent-tools/invoke.post.mjs +5 -2
  15. package/.output/server/chunks/routes/api/workshop/agent-tools/invoke.post.mjs.map +1 -1
  16. package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs +25 -3
  17. package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs.map +1 -1
  18. package/.output/server/package.json +1 -1
  19. package/package.json +1 -1
  20. package/scripts/_dbg-cross-channel-e2e.mjs +157 -0
  21. package/scripts/_dbg-cross-team-visibility-e2e.mjs +129 -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/agent-tools/invoke.post.ts +8 -1
  27. package/server/api/workshop/channels/[id]/messages/index.post.ts +26 -1
  28. package/server/api/workshop/ws.ts +1 -0
  29. package/server/data/daqs.json +35 -13
  30. package/server/data/dcw-lines.json +14 -0
  31. package/server/data/dcw-products.json +14 -0
  32. package/server/data/dcw-recipes.json +28 -0
  33. package/server/data/dcw-rollback.json +122 -1
  34. package/server/data/dcw-runs.json +54 -36
  35. package/server/data/dcw-writes.json +60 -0
  36. package/server/data/dcws.json +42 -0
  37. package/server/data/line-runs.json +32 -22
  38. package/server/services/workshop/agents/adapters/omp-rpc-client.ts +65 -0
  39. package/server/services/workshop/agents/agent-interface.ts +44 -1
  40. package/server/services/workshop/agents/mock-agent.ts +12 -0
  41. package/server/services/workshop/agents/omp-agent.ts +304 -8
  42. package/server/services/workshop/db/database.ts +16 -1
  43. package/server/services/workshop/db/memory.repo.ts +40 -2
  44. package/server/services/workshop/prompts/loader.ts +6 -5
  45. package/server/services/workshop/runtime/agent-runtime.ts +52 -6
  46. package/server/services/workshop/runtime/channel-runtime.ts +14 -6
  47. package/server/services/workshop/runtime/manager.ts +314 -3
  48. package/server/services/workshop/runtime/memory.ts +313 -78
  49. package/server/services/workshop/types/task.ts +14 -0
  50. package/.output/public/_nuxt/builds/meta/f2f649c5-fd3b-4507-a3a1-8b4a194de72d.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,9 @@ 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),
981
+ listOtherTeams: () => this.listOtherTeamsOverview(channelId, agent.id),
982
+ searchOtherTeamsMemory: input => this.recallOtherTeamsMemory(channelId, agent.id, input),
871
983
  refuseTask: (taskId, reason) => this.refuseTask(channelId, agent.id, taskId, reason),
872
984
  pollMailbox: limit => this.pollMailbox(channelId, agent.id, limit),
873
985
  waitMailbox: (limit, waitMs) => this.waitMailbox(channelId, agent.id, limit ?? 10, waitMs ?? 0),
@@ -885,6 +997,11 @@ export class AgentChannelManager {
885
997
  this.buses.get(channelId)?.notifyMemory({ agentId: agent.id, scope: input.scope, title: input.title, dedupKey: saved.dedupKey })
886
998
  return saved
887
999
  },
1000
+ // 会话压缩摘要入库(harvest 桥):omp 压缩产出 → 本成员 episodic-session 记忆
1001
+ recordSessionMemory: async (input) => {
1002
+ const saved = await memory.recordSessionCompaction(input)
1003
+ this.buses.get(channelId)?.notifyMemory({ agentId: agent.id, scope: 'private', title: `会话压缩摘要(${input.summary.length}字)`, dedupKey: saved.dedupKey })
1004
+ },
888
1005
  // 团队成员管理(仅 lead;manager 内二次校验角色,工具面与决策面共用)
889
1006
  createTeamMember: input => this.createTeamMember(channelId, agent.id, input),
890
1007
  updateTeamMember: (agentId, patch) => this.updateTeamMember(channelId, agent.id, agentId, patch),
@@ -2349,6 +2466,200 @@ export class AgentChannelManager {
2349
2466
  return message
2350
2467
  }
2351
2468
 
2469
+ /**
2470
+ * 跨 Channel 点对点通信(仅 lead):把消息直投目标 channel 的 lead mailbox。
2471
+ * 沿用既有 mailbox/route 机制(投递/唤醒/消费/回执全复用),仅新增跨 channel 路由:
2472
+ * - 权限:发送方必须是本 channel 启用成员中的 lead(worker/系统拒绝);
2473
+ * - 同主约束:两个 channel 必须属于同一 owner(用户级隔离不破);
2474
+ * - 守卫通行:x-aw-cross-channel 标记由服务端在发送时盖章(REST/工具入参不可伪造),
2475
+ * 目标 channel 信箱守卫据此放行非成员发送方的跨 channel 消息;
2476
+ * - 回执:目标 lead 经同一机制回信(其 send_message_to_agent / 本方法均可,
2477
+ * in_reply_to 关联触发实时推送)。
2478
+ */
2479
+ async sendCrossChannelMessage(
2480
+ fromChannelId: string,
2481
+ fromAgentId: string,
2482
+ input: { toChannelId: string, parts: Part[], requireReply?: boolean, inReplyTo?: string },
2483
+ ): Promise<{ messageId: string, toChannelId: string, toChannelName: string, toLeadAgentId: string }> {
2484
+ // 权限闸:发送方 = 本 channel 启用成员中的 lead
2485
+ const sender = this.deps.repos.channelAgents.findByChannelAgent(fromChannelId, fromAgentId)
2486
+ if (!sender || sender.enabled !== 1) {
2487
+ throw new AppError(403, 'SCOPE_VIOLATION', '跨 Channel 通信的发送方必须是本 channel 的启用成员')
2488
+ }
2489
+ if (sender.role !== 'lead') {
2490
+ throw new AppError(403, 'ROLE_FORBIDDEN', `跨 Channel 通信仅限 Leader 发起(${sender.name} 是 ${sender.role})`)
2491
+ }
2492
+
2493
+ // 目标 channel 解析(id 精确 > 名字精确 > 名字唯一包含);禁自指/禁停用/须有 lead
2494
+ const needle = input.toChannelId.trim().toLowerCase()
2495
+ const rows = this.deps.repos.channels.list()
2496
+ const target = rows.find(c => c.id === input.toChannelId)
2497
+ ?? rows.find(c => c.name.toLowerCase() === needle)
2498
+ ?? (() => {
2499
+ const partial = rows.filter(c => c.name.toLowerCase().includes(needle))
2500
+ return partial.length === 1 ? partial[0] : undefined
2501
+ })()
2502
+ if (!target || target.id === fromChannelId) {
2503
+ throw new AppError(404, 'CHANNEL_NOT_FOUND', `目标 channel 未命中(且不可为本 channel): ${input.toChannelId}`)
2504
+ }
2505
+ if (target.enabled !== 1) throw new AppError(400, 'CHANNEL_DISABLED', `目标 channel 已停用: ${target.name}`)
2506
+ if (!target.leadAgentId) throw new AppError(400, 'NO_LEAD', `目标 channel 没有 lead,无法接收跨 Channel 消息: ${target.name}`)
2507
+
2508
+ // 同主约束:跨团队协作不越用户边界
2509
+ const sourceChannel = this.deps.repos.channels.findById(fromChannelId)
2510
+ if (sourceChannel && target.ownerUserId !== null && sourceChannel.ownerUserId !== null
2511
+ && sourceChannel.ownerUserId !== target.ownerUserId) {
2512
+ throw new AppError(403, 'SCOPE_VIOLATION', '跨 Channel 通信仅限同一用户的 channel 之间')
2513
+ }
2514
+
2515
+ // 目标 lead 必须是目标 channel 的启用成员(防悬空 leadAgentId)
2516
+ const targetLead = this.deps.repos.channelAgents.findByChannelAgent(target.id, target.leadAgentId)
2517
+ if (!targetLead || targetLead.enabled !== 1) {
2518
+ throw new AppError(400, 'NO_LEAD', `目标 channel 的 lead 成员缺失或已停用: ${target.name}`)
2519
+ }
2520
+
2521
+ const message = buildMessage(target.id, 'ROLE_AGENT', input.parts, {
2522
+ 'x-aw-target-agent': targetLead.id,
2523
+ 'x-aw-from-agent': fromAgentId,
2524
+ 'x-aw-from-channel': fromChannelId,
2525
+ 'x-aw-cross-channel': 'true',
2526
+ 'x-aw-from-label': `跨Channel:${sourceChannel?.name ?? fromChannelId} / ${sender.name}`,
2527
+ ...(input.requireReply ? { 'x-aw-require-reply': 'true' } : {}),
2528
+ // in_reply_to 关联:channel-runtime 将其视为 realtime,回复即时推送对端
2529
+ ...(input.inReplyTo ? { 'x-aw-in-reply-to': input.inReplyTo } : {}),
2530
+ })
2531
+ const delivered = this.route(target.id, message)
2532
+ if (!delivered.includes(targetLead.id)) {
2533
+ throw new AppError(502, 'DELIVERY_FAILED', `跨 Channel 消息未能投递到「${target.name}」的 lead 信箱,请重试`)
2534
+ }
2535
+ return {
2536
+ messageId: message.messageId,
2537
+ toChannelId: target.id,
2538
+ toChannelName: target.name,
2539
+ toLeadAgentId: targetLead.id,
2540
+ }
2541
+ }
2542
+
2543
+ /**
2544
+ * 跨团队可见性(仅 lead):其他 channel 的场景任务概览(同主、启用)。
2545
+ * 协同工作流的第一步——先看别的团队在做什么/做过什么,再决定是否发信请求协作。
2546
+ */
2547
+ listOtherTeamsOverview(channelId: string, agentId: string): Array<{
2548
+ channelId: string, name: string, description: string, leadName: string | null,
2549
+ activeTasks: Array<{ id: string, title: string, state: string }>,
2550
+ recentCompleted: Array<{ title: string }>,
2551
+ }> {
2552
+ this.requireMember(channelId, agentId)
2553
+ const sender = this.deps.repos.channelAgents.findByChannelAgent(channelId, agentId)
2554
+ if (!sender || sender.role !== 'lead') {
2555
+ throw new AppError(403, 'ROLE_FORBIDDEN', `团队任务概览仅限 Leader(${sender?.name ?? agentId} 是 ${sender?.role ?? '非成员'})`)
2556
+ }
2557
+ const terminal = new Set(['COMPLETED', 'FAILED', 'CANCELED'])
2558
+ return this.otherSameOwnerChannels(channelId).map((c) => {
2559
+ const tasks = this.deps.repos.tasks.listByChannel(c.id)
2560
+ const leadRow = c.leadAgentId ? this.deps.repos.channelAgents.findById(c.leadAgentId) : undefined
2561
+ return {
2562
+ channelId: c.id,
2563
+ name: c.name,
2564
+ description: c.description ?? '',
2565
+ leadName: leadRow?.name ?? null,
2566
+ activeTasks: tasks
2567
+ .filter(t => !terminal.has(t.state))
2568
+ .map(t => ({ id: t.id, title: t.title, state: t.state })),
2569
+ recentCompleted: tasks
2570
+ .filter(t => t.state === 'COMPLETED')
2571
+ .slice(0, 3)
2572
+ .map(t => ({ title: t.title })),
2573
+ }
2574
+ })
2575
+ }
2576
+
2577
+ /**
2578
+ * 跨团队共享记忆检索(lead/worker 皆可,只读):其他 channel 的 __team__ 公共域。
2579
+ * 全员可见——worker 由此知道别的团队做过什么;发现相关经验/结论可直接复用,
2580
+ * 无需跨 channel 发信;查无所得而有协作必要时,再由 lead 发起跨 channel 通信。
2581
+ */
2582
+ recallOtherTeamsMemory(channelId: string, agentId: string, input: { query: string, limit?: number }): Array<{
2583
+ channelId: string, channelName: string, title: string, content: string, importance: number, createdAt: string,
2584
+ }> {
2585
+ this.requireMember(channelId, agentId)
2586
+ const q = input.query.trim()
2587
+ if (!q) return []
2588
+ const others = this.otherSameOwnerChannels(channelId)
2589
+ if (others.length === 0) return []
2590
+ const matchQuery = segmentCJK(q).split(/\s+/).filter(Boolean).join(' OR ')
2591
+ const rows = this.deps.repos.memories.searchTeamSharedAcross(
2592
+ matchQuery,
2593
+ others.map(c => c.id),
2594
+ Math.min(Math.max(input.limit ?? 5, 1), 20),
2595
+ )
2596
+ return rows.map(r => ({
2597
+ channelId: r.channelId,
2598
+ channelName: others.find(c => c.id === r.channelId)?.name ?? r.channelId,
2599
+ title: r.title,
2600
+ content: unsegmentCJK(r.content).slice(0, 400),
2601
+ importance: r.importance,
2602
+ createdAt: r.createdAt,
2603
+ }))
2604
+ }
2605
+
2606
+ /** 同主且启用的其他 channel 列表(跨团队可见性的作用域) */
2607
+ private otherSameOwnerChannels(channelId: string): ChannelRow[] {
2608
+ const self = this.deps.repos.channels.findById(channelId)
2609
+ return this.deps.repos.channels.list().filter(c =>
2610
+ c.id !== channelId
2611
+ && c.enabled === 1
2612
+ && (self?.ownerUserId == null || c.ownerUserId == null || c.ownerUserId === self.ownerUserId))
2613
+ }
2614
+
2615
+ /**
2616
+ * 工具桥(协作工具族):按 agentId 定位 channel 并调用其 workspace 方法。
2617
+ * 与工业工具桥同一服务层,供 REST 直调(测试/运维/控制台)。
2618
+ */
2619
+ invokeAgentWorkspaceTool(agentId: string, tool: string, args: Record<string, unknown> = {}): { text: string, isError?: boolean } {
2620
+ const row = this.deps.repos.channelAgents.findById(agentId)
2621
+ if (!row || row.enabled !== 1) throw new AppError(404, 'NOT_FOUND', `agent 不存在或已停用: ${agentId}`)
2622
+ const ws = this.ensureAgentRuntime(row.channelId, agentId)?.workspace
2623
+ if (!ws) throw new AppError(400, 'AGENT_NOT_READY', `agent 运行时未装配: ${agentId}`)
2624
+ switch (tool) {
2625
+ case 'list_other_teams': {
2626
+ const teams = ws.listOtherTeams()
2627
+ if (teams.length === 0) return { text: '当前没有其他团队(或均未启用)。' }
2628
+ const text = teams.map((t) => {
2629
+ const active = t.activeTasks.length > 0
2630
+ ? t.activeTasks.map(x => `「${x.title}」(${x.state})`).join('、')
2631
+ : '无进行中任务'
2632
+ const done = t.recentCompleted.length > 0
2633
+ ? t.recentCompleted.map(x => `「${x.title}」`).join('、')
2634
+ : '无'
2635
+ return `- ${t.name}${t.description ? `(${t.description})` : ''} · lead=${t.leadName ?? '?'}\n 进行中: ${active}\n 近期完成: ${done}\n channel_id: ${t.channelId}`
2636
+ }).join('\n')
2637
+ return { text: `其他团队概览:\n${text}\n(需要协作时用 send_cross_channel_message 向对应团队 Leader 发信;查具体知识用 search_other_teams_memory)` }
2638
+ }
2639
+ case 'search_other_teams_memory': {
2640
+ const query = String(args.query ?? '')
2641
+ if (!query) return { text: '缺少 query', isError: true }
2642
+ const rows = ws.searchOtherTeamsMemory({ query, limit: Number(args.limit ?? 5) })
2643
+ if (rows.length === 0) return { text: `其他团队的共享记忆中没有命中「${query}」的内容。` }
2644
+ const text = rows.map(r =>
2645
+ `- [${r.channelName}] 「${r.title}」(${r.createdAt.slice(0, 10)}): ${r.content}`).join('\n')
2646
+ return { text: `其他团队共享记忆命中 ${rows.length} 条:\n${text}` }
2647
+ }
2648
+ case 'send_cross_channel_message': {
2649
+ if (!args.to_channel_id || !args.message) return { text: '缺少 to_channel_id 或 message', isError: true }
2650
+ const r = ws.sendCrossChannelMessage({
2651
+ toChannelId: String(args.to_channel_id),
2652
+ parts: [{ text: String(args.message) }],
2653
+ requireReply: args.require_reply === true,
2654
+ inReplyTo: typeof args.in_reply_to === 'string' ? args.in_reply_to : undefined,
2655
+ })
2656
+ return { text: `跨 Channel 消息已送达「${r.toChannelName}」的 Leader(channel=${r.toChannelId}, lead=${r.toLeadAgentId.slice(0, 8)}),消息 id=${r.messageId.slice(0, 8)}。` }
2657
+ }
2658
+ default:
2659
+ throw new AppError(400, 'BAD_REQUEST', `工具桥不支持该协作工具: ${tool}`)
2660
+ }
2661
+ }
2662
+
2352
2663
  /** 阻塞长轮询(poll_messages):到信即时唤醒,250ms 兜底重查 */
2353
2664
  async waitMailbox(channelId: string, callerAgentId: string, limit: number, waitMs: number): Promise<A2AMessage[]> {
2354
2665
  this.requireMember(channelId, callerAgentId)