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.
Files changed (46) 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 +1920 -1211
  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/package.json +1 -1
  18. package/scripts/_dbg-cross-channel-e2e.mjs +157 -0
  19. package/scripts/test-context-governor.ts +176 -0
  20. package/scripts/test-memory-brief-chronicle.ts +180 -0
  21. package/scripts/test-memory-dynamic.ts +3 -1
  22. package/scripts/test-memory-rrf-mmr.ts +114 -0
  23. package/server/api/workshop/channels/[id]/messages/index.post.ts +26 -1
  24. package/server/api/workshop/ws.ts +1 -0
  25. package/server/data/daqs.json +35 -13
  26. package/server/data/dcw-lines.json +14 -0
  27. package/server/data/dcw-products.json +14 -0
  28. package/server/data/dcw-recipes.json +28 -0
  29. package/server/data/dcw-rollback.json +122 -1
  30. package/server/data/dcw-runs.json +54 -36
  31. package/server/data/dcw-writes.json +60 -0
  32. package/server/data/dcws.json +42 -0
  33. package/server/data/line-runs.json +32 -22
  34. package/server/services/workshop/agents/adapters/omp-rpc-client.ts +65 -0
  35. package/server/services/workshop/agents/agent-interface.ts +26 -1
  36. package/server/services/workshop/agents/mock-agent.ts +12 -0
  37. package/server/services/workshop/agents/omp-agent.ts +277 -8
  38. package/server/services/workshop/db/database.ts +16 -1
  39. package/server/services/workshop/db/memory.repo.ts +22 -2
  40. package/server/services/workshop/prompts/loader.ts +6 -5
  41. package/server/services/workshop/runtime/agent-runtime.ts +52 -6
  42. package/server/services/workshop/runtime/channel-runtime.ts +14 -6
  43. package/server/services/workshop/runtime/manager.ts +192 -3
  44. package/server/services/workshop/runtime/memory.ts +313 -78
  45. package/server/services/workshop/types/task.ts +14 -0
  46. package/.output/public/_nuxt/builds/meta/f2f649c5-fd3b-4507-a3a1-8b4a194de72d.json +0 -1
@@ -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,
@@ -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)