package engine // event_emitter.go — ctx-based event forwarding hook for sub-agent visibility. // // Problem: SubAgent.runLoop emits events onto its own private channel; the // existing consumers (agent_executor.RunSync / RunBackground / memory // extraction drain) either drop the events entirely or type-switch on a // handful of types that never match the wrapping SubAgentEvent. Result: // parent engines and their SDK / SSE consumers cannot see that a sub-agent // is doing anything — child activity is invisible to TUI tree views, SSE // streams, audit sinks, and session cost accounting. // // Fix: the parent engine's main tool dispatch wraps ctx with an EventEmitter // closure that pushes into the active Run channel. When a tool (Agent / // Skill / Team / Dream) spawns a sub-agent, sa.runLoop reads the emitter // from ctx and forwards each sub-agent event (SubAgentStartEvent, wrapped // SubAgentEvent, SubAgentEndEvent) upward. SDK consumers of parent // Engine.Run() and bridge.SSE subscribers see child activity with // SubAgentID attribution. // // Why ctx (not a public method): the emitter is scoped to a single Run — // the channel closes when Run ends, so the emitter becomes invalid after. // ctx expires with Run; the emitter naturally becomes unreachable. A public // Engine.EmitEvent method would invite callers to hold a reference across // Run boundaries and race with channel close. // // Why the private key type: external code cannot spoof the key via // context.WithValue, which prevents plugin tools or SDK user hooks from // injecting forged events into the parent Run channel — events flow only // from engine-controlled dispatch sites. // // Silent path (memory extraction): runMemoryExtraction spawns sub-agents // under rootCtx rather than the current tool-dispatch ctx, so there is no // emitter in the ctx and forwarding is a no-op. SubAgentConfig.SilentEvents // is a belt-and-braces flag for callers that need explicit opt-out even if // they accidentally propagate a ctx that does carry an emitter. // // event_emitter.go — 基于 ctx 的事件转发钩子, 让子 agent 活动对父引擎可见. // // 问题: SubAgent.runLoop 把事件推到自己的私有 channel; 已有消费者 // (agent_executor.RunSync / RunBackground / 记忆提取 drain) 要么整个丢, // 要么 type switch 只匹配一小批永远和 SubAgentEvent wrapper 不相符的类型. // 结果: 父引擎及其 SDK / SSE 消费者看不到子 agent 在干啥 — TUI 树形视图, // SSE 流, 审计 sink, 会话成本统计里完全无感. // // 修复: 父引擎主工具派发前用 EventEmitter 闭包包 ctx, 闭包推入当前 Run // channel. 工具 (Agent / Skill / Team / Dream) spawn 子 agent 时, // sa.runLoop 从 ctx 读 emitter 把每个子 agent 事件 (SubAgentStartEvent, // 包装 SubAgentEvent, SubAgentEndEvent) 往上送. 父 Engine.Run() SDK 消费 // 端和 bridge.SSE 订阅端能看到子活动并带 SubAgentID 归属. // // 为啥走 ctx (不提供公共方法): emitter 的有效期锁在单次 Run — Run 结束时 // channel 关闭, emitter 失效. ctx 随 Run 结束而失效, emitter 自然不可达. // 若提供公共 Engine.EmitEvent 方法, 调用方会持引用跨越 Run 边界, 和 channel // close 产生竞态. // // 为啥用私有 key 类型: 外部代码无法通过 context.WithValue 伪造 key, 阻止 // plugin 工具或 SDK 用户 hook 往父 Run channel 注入伪造事件 — 事件只从 // engine 控制的派发点流出. // // 静默路径 (记忆提取): runMemoryExtraction spawn 子 agent 走的是 rootCtx // 而非当前工具派发 ctx, 所以 ctx 里没 emitter, 转发自动是 no-op. // SubAgentConfig.SilentEvents 是兜底标志, 给那些不小心传了带 emitter 的 // ctx 但仍想静默的 caller 用. import "context" // EventEmitter is the callback signature for forwarding an Event into the // parent engine's active Run channel. Implementations wrap the underlying // send with backpressure handling (non-blocking select with default drop, // or full-block — up to the caller). // // EventEmitter 是把 Event 转发到父引擎当前 Run channel 的回调签名. 实现 // 方自己封装 backpressure (非阻塞 select 兜底丢弃 / 全阻塞, 由 caller 决定). type EventEmitter func(evt Event) // eventEmitterKey 是 WithEventEmitter / EventEmitterFromContext 使用的私 // 有 context key. 未导出 struct 阻止外部伪造. eventEmitterKey is the // private context key used by WithEventEmitter / EventEmitterFromContext. // The unexported struct type blocks external spoofing. type eventEmitterKey struct{} // WithEventEmitter 派生一个携带 emitter 的 context. nil emitter 视为 // "未设置" — 直接返回原 ctx 不污染调用链. // // WithEventEmitter derives a context carrying emit. A nil emit is treated // as "unset" — the original ctx is returned unchanged. func WithEventEmitter(ctx context.Context, emit EventEmitter) context.Context { if emit == nil { return ctx } return context.WithValue(ctx, eventEmitterKey{}, emit) } // EventEmitterFromContext 返回由 WithEventEmitter 设置的 emitter, 未设置 // 返回 nil. 使用模式: if emit := EventEmitterFromContext(ctx); emit != nil // { emit(evt) }. // // EventEmitterFromContext returns the emitter set by WithEventEmitter, or // nil if absent. Usage pattern: if emit := EventEmitterFromContext(ctx); // emit != nil { emit(evt) }. func EventEmitterFromContext(ctx context.Context) EventEmitter { v, _ := ctx.Value(eventEmitterKey{}).(EventEmitter) return v }