refactor: normalize inbound channel context

This commit is contained in:
xiaoxixi 2026-07-19 13:42:43 +08:00
parent 355244a3d6
commit 76685c3983
8 changed files with 122 additions and 81 deletions

View File

@ -79,6 +79,7 @@ Scheduler → SessionManager scheduled execution → AgentLoop → Scheduler del
### Functional Boundaries ### Functional Boundaries
- **Channels** publish inbound messages through `MessageBus`; outbound writes arrive through `OutboundDispatcher` or a per-turn `TurnSink`. They know nothing about sessions or LLM - **Channels** publish inbound messages through `MessageBus`; outbound writes arrive through `OutboundDispatcher` or a per-turn `TurnSink`. They know nothing about sessions or LLM
- **Inbound contract** carries normalized sender/time/media plus `ChannelContext`; core routing may interpret `reply_to` but must treat platform-private context as opaque reply data
- **MessageBus** owns bounded inbound/outbound/control queues; outbound routing and retries belong to `OutboundDispatcher`, not the queue - **MessageBus** owns bounded inbound/outbound/control queues; outbound routing and retries belong to `OutboundDispatcher`, not the queue
- **SessionManager** owns session state, dialog operations, context construction, per-session work queues, and persistence coordination - **SessionManager** owns session state, dialog operations, context construction, per-session work queues, and persistence coordination
- **TurnController** is the only owner of active Turn state; snapshots are complete latest-wins values, not token queues, and `Completed` is published only after atomic persistence succeeds - **TurnController** is the only owner of active Turn state; snapshots are complete latest-wins values, not token queues, and `Completed` is published only after atomic persistence succeeds

View File

@ -125,6 +125,7 @@ sequenceDiagram
- 每个 session 有一条容量为 32 的队列,同一 session 串行处理,不同 session 的 worker 可并发执行。 - 每个 session 有一条容量为 32 的队列,同一 session 串行处理,不同 session 的 worker 可并发执行。
- 队列满时明确拒绝新消息,不允许无界积压。 - 队列满时明确拒绝新消息,不允许无界积压。
- Slash command 不进入 Agent 队列,由 `SessionManager` 直接执行,因此 `/stop` 等控制操作不会排在长模型调用之后。 - Slash command 不进入 Agent 队列,由 `SessionManager` 直接执行,因此 `/stop` 等控制操作不会排在长模型调用之后。
- `InboundMessage` 只保存规范化输入:`sender_id``received_at`、媒体和一个 `ChannelContext`。核心只解释其中的 `reply_to`reaction/message ID 等平台字段作为 `private` 不透明传到对应 Turn/普通回复,不能散落为核心层 magic key。持久化的用户消息保留真实接收时间和 `UserInput` 来源,客户端历史投影不暴露来源中的平台用户 ID。
- Session 为每个 Agent 请求创建一个 `TurnController`。Provider 向 AgentLoop 发 deltaAgentLoop 发结构化 TurnEvent只有 TurnController 能把事件归约为有序 block 和单调 revision 的完整快照。 - Session 为每个 Agent 请求创建一个 `TurnController`。Provider 向 AgentLoop 发 deltaAgentLoop 发结构化 TurnEvent只有 TurnController 能把事件归约为有序 block 和单调 revision 的完整快照。
- Turn 快照经 Tokio `watch` 发布,语义为 latest-wins慢客户端或慢渠道跳过中间状态不反压 Provider。终态明确编码在快照中不依赖 sender 关闭。 - Turn 快照经 Tokio `watch` 发布,语义为 latest-wins慢客户端或慢渠道跳过中间状态不反压 Provider。终态明确编码在快照中不依赖 sender 关闭。
- Agent 本轮消息原子持久化成功后才发布 `Completed`。取消或失败若已有可见正文,则保存为 `cancelled`/`interrupted` partial只有 reasoning 时不创建 assistant 历史。 - Agent 本轮消息原子持久化成功后才发布 `Completed`。取消或失败若已有可见正文,则保存为 `cancelled`/`interrupted` partial只有 reasoning 时不创建 assistant 历史。

View File

@ -154,6 +154,8 @@ pub struct ChatMessage {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SourceKind { pub enum SourceKind {
#[serde(rename = "user_input")]
UserInput,
#[serde(rename = "system_notification")] #[serde(rename = "system_notification")]
SystemNotification, SystemNotification,
#[serde(rename = "cross_channel")] #[serde(rename = "cross_channel")]
@ -364,18 +366,23 @@ mod conversation_message_tests {
// InboundMessage - Message from Channel to Bus (user input) // InboundMessage - Message from Channel to Bus (user input)
// ============================================================================ // ============================================================================
/// Opaque channel-owned context that may be carried to the corresponding reply.
/// Core routing understands `reply_to`; all other platform data remains private.
#[derive(Debug, Clone, Default)]
pub struct ChannelContext {
pub reply_to: Option<String>,
pub private: HashMap<String, String>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct InboundMessage { pub struct InboundMessage {
pub channel: String, pub channel: String,
pub sender_id: String, pub sender_id: String,
pub chat_id: String, pub chat_id: String,
pub content: String, pub content: String,
pub timestamp: i64, pub received_at: i64,
pub media: Vec<MediaItem>, pub media: Vec<MediaItem>,
/// Channel-specific data used internally by the channel (not forwarded). pub channel_context: ChannelContext,
pub metadata: HashMap<String, String>,
/// Data forwarded from inbound to outbound (copied to OutboundMessage.metadata by gateway).
pub forwarded_metadata: HashMap<String, String>,
} }
// ============================================================================ // ============================================================================

View File

@ -3,8 +3,8 @@ pub mod message;
pub use dispatcher::OutboundDispatcher; pub use dispatcher::OutboundDispatcher;
pub use message::{ pub use message::{
ChatMessage, CompletionStatus, ContentBlock, ControlMessage, InboundMessage, MediaItem, ChannelContext, ChatMessage, CompletionStatus, ContentBlock, ControlMessage, InboundMessage,
MediaRef, MessageSource, OutboundMessage, ProviderReasoningState, SourceKind, MediaItem, MediaRef, MessageSource, OutboundMessage, ProviderReasoningState, SourceKind,
}; };
use std::sync::Arc; use std::sync::Arc;

View File

@ -211,10 +211,9 @@ impl CliChatChannel {
sender_id: "cli".to_string(), sender_id: "cli".to_string(),
chat_id: target_chat_id, chat_id: target_chat_id,
content, content,
timestamp: crate::bus::message::current_timestamp(), received_at: crate::bus::message::current_timestamp(),
media, media,
metadata: Default::default(), channel_context: Default::default(),
forwarded_metadata: Default::default(),
}; };
if let Err(error) = bus.publish_inbound(msg).await { if let Err(error) = bus.publish_inbound(msg).await {
self.uploads.restore(uploads).await; self.uploads.restore(uploads).await;

View File

@ -1252,14 +1252,10 @@ impl FeishuChannel {
} }
}; };
// forwarded_metadata is copied to OutboundMessage.metadata by the gateway. let mut private_context = std::collections::HashMap::new();
let mut forwarded_metadata = std::collections::HashMap::new(); private_context.insert("feishu.message_id".to_string(), message_id.clone());
forwarded_metadata.insert("feishu.message_id".to_string(), message_id.clone());
if let Some(ref rid) = reaction_id { if let Some(ref rid) = reaction_id {
forwarded_metadata.insert("feishu.reaction_id".to_string(), rid.clone()); private_context.insert("feishu.reaction_id".to_string(), rid.clone());
}
if let Some(ref pid) = parsed.parent_id {
forwarded_metadata.insert("feishu.parent_id".to_string(), pid.clone());
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -1269,10 +1265,12 @@ impl FeishuChannel {
sender_id: parsed.open_id.clone(), sender_id: parsed.open_id.clone(),
chat_id: parsed.chat_id.clone(), chat_id: parsed.chat_id.clone(),
content: parsed.content.clone(), content: parsed.content.clone(),
timestamp: crate::bus::message::current_timestamp(), received_at: crate::bus::message::current_timestamp(),
media: parsed.media.clone(), media: parsed.media.clone(),
metadata: std::collections::HashMap::new(), channel_context: crate::bus::ChannelContext {
forwarded_metadata, reply_to: parsed.parent_id.clone(),
private: private_context,
},
}; };
if let Err(e) = self.handle_and_publish(&bus, &msg).await { if let Err(e) = self.handle_and_publish(&bus, &msg).await {
tracing::error!(error = %e, open_id = %parsed.open_id, chat_id = %parsed.chat_id, "Failed to publish Feishu message to bus"); tracing::error!(error = %e, open_id = %parsed.open_id, chat_id = %parsed.chat_id, "Failed to publish Feishu message to bus");

View File

@ -154,16 +154,7 @@ async fn process_inbound(
session_manager: Arc<SessionManager>, session_manager: Arc<SessionManager>,
inbound: InboundMessage, inbound: InboundMessage,
) { ) {
let result = session_manager let result = session_manager.handle_message(&inbound).await;
.handle_message(
&inbound.channel,
&inbound.sender_id,
&inbound.chat_id,
&inbound.content,
inbound.media.clone(),
inbound.forwarded_metadata.clone(),
)
.await;
match result { match result {
Ok(crate::session::session::HandleResult::AgentResponse(content)) => { Ok(crate::session::session::HandleResult::AgentResponse(content)) => {
@ -189,7 +180,7 @@ async fn publish_command_output(bus: &MessageBus, inbound: InboundMessage, conte
} }
async fn publish_output(bus: &MessageBus, inbound: InboundMessage, content: String, command: bool) { async fn publish_output(bus: &MessageBus, inbound: InboundMessage, content: String, command: bool) {
let mut metadata = inbound.forwarded_metadata; let mut metadata = inbound.channel_context.private;
if command { if command {
metadata.insert("_type".to_string(), "command".to_string()); metadata.insert("_type".to_string(), "command".to_string());
} }
@ -197,7 +188,7 @@ async fn publish_output(bus: &MessageBus, inbound: InboundMessage, content: Stri
channel: inbound.channel, channel: inbound.channel,
chat_id: inbound.chat_id, chat_id: inbound.chat_id,
content, content,
reply_to: None, reply_to: inbound.channel_context.reply_to,
media: vec![], media: vec![],
metadata, metadata,
delivery: None, delivery: None,
@ -353,6 +344,7 @@ fn is_priority_stop(content: &str) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::bus::ChannelContext;
use std::collections::HashSet; use std::collections::HashSet;
use tokio::sync::Notify; use tokio::sync::Notify;
@ -374,6 +366,36 @@ mod tests {
assert_eq!(keys.len(), 3); assert_eq!(keys.len(), 3);
} }
#[tokio::test]
async fn routed_output_preserves_reply_target_and_private_context() {
let bus = MessageBus::new(2);
let inbound = InboundMessage {
channel: "test".to_string(),
sender_id: "user".to_string(),
chat_id: "chat".to_string(),
content: "hello".to_string(),
received_at: 123,
media: vec![],
channel_context: ChannelContext {
reply_to: Some("parent".to_string()),
private: HashMap::from([("opaque".to_string(), "value".to_string())]),
},
};
publish_command_output(&bus, inbound, "done".to_string()).await;
let output = bus.consume_outbound().await.unwrap();
assert_eq!(output.reply_to.as_deref(), Some("parent"));
assert_eq!(
output.metadata.get("opaque").map(String::as_str),
Some("value")
);
assert_eq!(
output.metadata.get("_type").map(String::as_str),
Some("command")
);
}
#[tokio::test] #[tokio::test]
async fn slow_conversation_lane_does_not_block_another_lane() { async fn slow_conversation_lane_does_not_block_another_lane() {
let (slow_tx, slow_rx) = mpsc::channel(2); let (slow_tx, slow_rx) = mpsc::channel(2);

View File

@ -7,7 +7,8 @@ use super::persistence::{append_persisted_messages, finalize_turn_after_persiste
use super::turn::{TurnBlock, TurnController, TurnSnapshot}; use super::turn::{TurnBlock, TurnController, TurnSnapshot};
use super::turn_input::prepare_turn_input; use super::turn_input::prepare_turn_input;
use crate::bus::{ use crate::bus::{
ChatMessage, CompletionStatus, MediaItem, MediaRef, MessageSource, OutboundMessage, SourceKind, ChannelContext, ChatMessage, CompletionStatus, InboundMessage, MediaItem, MediaRef,
MessageSource, OutboundMessage, SourceKind,
}; };
use crate::mcp::get_mcp_status; use crate::mcp::get_mcp_status;
use crate::storage::{Storage, StorageError}; use crate::storage::{Storage, StorageError};
@ -28,9 +29,9 @@ fn outbound_session_metadata(session_id: &str) -> HashMap<String, String> {
fn outbound_turn_metadata( fn outbound_turn_metadata(
session_id: &str, session_id: &str,
forwarded: &HashMap<String, String>, private_context: &HashMap<String, String>,
) -> HashMap<String, String> { ) -> HashMap<String, String> {
let mut metadata = forwarded.clone(); let mut metadata = private_context.clone();
metadata.insert("_session_id".to_string(), session_id.to_string()); metadata.insert("_session_id".to_string(), session_id.to_string());
metadata metadata
} }
@ -115,31 +116,28 @@ fn terminal_fallback_content(snapshot: &TurnSnapshot) -> Option<String> {
async fn deliver_terminal_fallback( async fn deliver_terminal_fallback(
handle: TurnDeliveryHandle, handle: TurnDeliveryHandle,
bus: &MessageBus, bus: &MessageBus,
channel: &str, target: &crate::channels::TurnTarget,
chat_id: &str,
session_id: &str,
forwarded_metadata: &HashMap<String, String>,
controller: &TurnController, controller: &TurnController,
) { ) {
let Err(error) = handle.wait().await else { let Err(error) = handle.wait().await else {
return; return;
}; };
tracing::error!(channel, chat_id, error = %error, "Turn sink terminal delivery failed; using ordinary outbound fallback"); tracing::error!(channel = %target.channel, chat_id = %target.chat_id, error = %error, "Turn sink terminal delivery failed; using ordinary outbound fallback");
let snapshot = controller.snapshot(); let snapshot = controller.snapshot();
let Some(content) = terminal_fallback_content(&snapshot) else { let Some(content) = terminal_fallback_content(&snapshot) else {
return; return;
}; };
let outbound = OutboundMessage { let outbound = OutboundMessage {
channel: channel.to_string(), channel: target.channel.clone(),
chat_id: chat_id.to_string(), chat_id: target.chat_id.clone(),
content, content,
reply_to: None, reply_to: target.reply_to.clone(),
media: vec![], media: vec![],
metadata: outbound_turn_metadata(session_id, forwarded_metadata), metadata: target.metadata.clone(),
delivery: None, delivery: None,
}; };
if let Err(fallback_error) = bus.deliver_outbound(outbound).await { if let Err(fallback_error) = bus.deliver_outbound(outbound).await {
tracing::error!(channel, chat_id, error = %fallback_error, "Ordinary terminal fallback delivery failed"); tracing::error!(channel = %target.channel, chat_id = %target.chat_id, error = %fallback_error, "Ordinary terminal fallback delivery failed");
} }
} }
@ -298,13 +296,17 @@ mod cancelled_partial_tests {
let (sender, completion) = oneshot::channel(); let (sender, completion) = oneshot::channel();
sender.send(Err(DeliveryError::FinalTimedOut)).unwrap(); sender.send(Err(DeliveryError::FinalTimedOut)).unwrap();
let target = crate::channels::TurnTarget {
channel: "recording".to_string(),
chat_id: "chat".to_string(),
session_id: "session".to_string(),
reply_to: None,
metadata: HashMap::new(),
};
deliver_terminal_fallback( deliver_terminal_fallback(
TurnDeliveryHandle { completion }, TurnDeliveryHandle { completion },
&bus, &bus,
"recording", &target,
"chat",
"session",
&HashMap::new(),
&controller, &controller,
) )
.await; .await;
@ -399,10 +401,12 @@ struct ActiveTurnEmitter {
/// A task to be processed by the per-session agent worker /// A task to be processed by the per-session agent worker
struct AgentTask { struct AgentTask {
channel: String, channel: String,
sender_id: String,
chat_id: String, chat_id: String,
content: String, content: String,
received_at: i64,
media: Vec<MediaItem>, media: Vec<MediaItem>,
forwarded_metadata: HashMap<String, String>, channel_context: ChannelContext,
} }
#[derive(Clone)] #[derive(Clone)]
@ -2323,13 +2327,12 @@ impl SessionManager {
pub async fn handle_message( pub async fn handle_message(
&self, &self,
channel: &str, inbound: &InboundMessage,
_sender_id: &str,
chat_id: &str,
content: &str,
media: Vec<MediaItem>,
forwarded_metadata: HashMap<String, String>,
) -> Result<HandleResult, AgentError> { ) -> Result<HandleResult, AgentError> {
let channel = inbound.channel.as_str();
let sender_id = inbound.sender_id.as_str();
let chat_id = inbound.chat_id.as_str();
let content = inbound.content.as_str();
let unified_id = self.resolve_dialog_id(channel, chat_id).await?; let unified_id = self.resolve_dialog_id(channel, chat_id).await?;
tracing::debug!(unified_id = %unified_id, "handle_message resolved unified_id"); tracing::debug!(unified_id = %unified_id, "handle_message resolved unified_id");
let session = self.get_or_create_session(&unified_id).await?; let session = self.get_or_create_session(&unified_id).await?;
@ -2363,10 +2366,12 @@ impl SessionManager {
// Normal message: enqueue to per-session worker for serial processing. // Normal message: enqueue to per-session worker for serial processing.
let task = AgentTask { let task = AgentTask {
channel: channel.to_string(), channel: channel.to_string(),
sender_id: sender_id.to_string(),
chat_id: chat_id.to_string(), chat_id: chat_id.to_string(),
content: content.to_string(), content: content.to_string(),
media, received_at: inbound.received_at,
forwarded_metadata, media: inbound.media.clone(),
channel_context: inbound.channel_context.clone(),
}; };
let session_clone = session.clone(); let session_clone = session.clone();
let unified_str = unified_id.to_string(); let unified_str = unified_id.to_string();
@ -2534,7 +2539,8 @@ fn spawn_agent_worker(
'tasks: while let Some(task) = task_rx.recv().await { 'tasks: while let Some(task) = task_rx.recv().await {
let task_chan = task.channel.clone(); let task_chan = task.channel.clone();
let task_cid = task.chat_id.clone(); let task_cid = task.chat_id.clone();
let task_metadata = task.forwarded_metadata.clone(); let task_metadata = task.channel_context.private.clone();
let task_reply_to = task.channel_context.reply_to.clone();
// Phase 1: capture a stable session snapshot under lock. // Phase 1: capture a stable session snapshot under lock.
// Memory recall and compression happen outside this block so // Memory recall and compression happen outside this block so
// /stop and other commands are not blocked behind slow I/O or // /stop and other commands are not blocked behind slow I/O or
@ -2547,7 +2553,18 @@ fn spawn_agent_worker(
} }
let media_refs: Vec<MediaRef> = let media_refs: Vec<MediaRef> =
task.media.iter().map(MediaItem::to_media_ref).collect(); task.media.iter().map(MediaItem::to_media_ref).collect();
guard.create_user_message(&task.content, media_refs) let source = MessageSource {
kind: SourceKind::UserInput,
from_channel: Some(task.channel.clone()),
from_session: None,
from_user_id: Some(task.sender_id.clone()),
system_name: None,
task_id: None,
};
let mut message =
guard.create_user_message_with_source(&task.content, media_refs, source);
message.timestamp = task.received_at;
message
}; };
if let Err(e) = append_persisted_messages(&session, vec![user_message]).await { if let Err(e) = append_persisted_messages(&session, vec![user_message]).await {
tracing::error!(error = %e, "Failed to persist user message"); tracing::error!(error = %e, "Failed to persist user message");
@ -2555,7 +2572,7 @@ fn spawn_agent_worker(
channel: task_chan.clone(), channel: task_chan.clone(),
chat_id: task_cid.clone(), chat_id: task_cid.clone(),
content: "Failed to save your message, please try again.".to_string(), content: "Failed to save your message, please try again.".to_string(),
reply_to: None, reply_to: task_reply_to.clone(),
media: vec![], media: vec![],
metadata: outbound_turn_metadata(&unified_str, &task_metadata), metadata: outbound_turn_metadata(&unified_str, &task_metadata),
delivery: None, delivery: None,
@ -2589,7 +2606,7 @@ fn spawn_agent_worker(
chat_id: task_cid.clone(), chat_id: task_cid.clone(),
content: "Agent creation failed, please try again." content: "Agent creation failed, please try again."
.to_string(), .to_string(),
reply_to: None, reply_to: task_reply_to.clone(),
media: vec![], media: vec![],
metadata: outbound_turn_metadata(&unified_str, &task_metadata), metadata: outbound_turn_metadata(&unified_str, &task_metadata),
delivery: None, delivery: None,
@ -2660,17 +2677,15 @@ fn spawn_agent_worker(
); );
let initial_turn = turn_controller.snapshot(); let initial_turn = turn_controller.snapshot();
let active_turn_id = initial_turn.id.0.clone(); let active_turn_id = initial_turn.id.0.clone();
let turn_target = crate::channels::TurnTarget {
channel: task_chan.clone(),
chat_id: task_cid.clone(),
session_id: unified_str.clone(),
reply_to: task_reply_to.clone(),
metadata: outbound_turn_metadata(&unified_str, &task_metadata),
};
let delivery_handle = match turn_delivery let delivery_handle = match turn_delivery
.start( .start(turn_target.clone(), turn_receiver)
crate::channels::TurnTarget {
channel: task_chan.clone(),
chat_id: task_cid.clone(),
session_id: unified_str.clone(),
reply_to: None,
metadata: outbound_turn_metadata(&unified_str, &task_metadata),
},
turn_receiver,
)
.await .await
{ {
Ok(handle) => Some(handle), Ok(handle) => Some(handle),
@ -2712,6 +2727,7 @@ fn spawn_agent_worker(
let cid2 = task_cid.clone(); let cid2 = task_cid.clone();
let unified_str2 = unified_str.clone(); let unified_str2 = unified_str.clone();
let task_metadata2 = task_metadata.clone(); let task_metadata2 = task_metadata.clone();
let task_reply_to2 = task_reply_to.clone();
let title_supervisor = worker_supervisor.clone(); let title_supervisor = worker_supervisor.clone();
let turn_lifecycle = &turn_controller; let turn_lifecycle = &turn_controller;
let process_future = async move { let process_future = async move {
@ -2763,7 +2779,7 @@ fn spawn_agent_worker(
chat_id: cid2, chat_id: cid2,
content: "Context overflow handling failed." content: "Context overflow handling failed."
.to_string(), .to_string(),
reply_to: None, reply_to: task_reply_to2.clone(),
media: vec![], media: vec![],
metadata: outbound_turn_metadata( metadata: outbound_turn_metadata(
&response_session_id, &response_session_id,
@ -2826,7 +2842,7 @@ fn spawn_agent_worker(
channel: chan2, channel: chan2,
chat_id: cid2, chat_id: cid2,
content: format!("Processing error: {}", e), content: format!("Processing error: {}", e),
reply_to: None, reply_to: task_reply_to2.clone(),
media: vec![], media: vec![],
metadata: outbound_turn_metadata( metadata: outbound_turn_metadata(
&response_session_id, &response_session_id,
@ -2853,7 +2869,7 @@ fn spawn_agent_worker(
channel: chan2, channel: chan2,
chat_id: cid2, chat_id: cid2,
content: format!("Processing error: {}", e), content: format!("Processing error: {}", e),
reply_to: None, reply_to: task_reply_to2.clone(),
media: vec![], media: vec![],
metadata: outbound_turn_metadata( metadata: outbound_turn_metadata(
&response_session_id, &response_session_id,
@ -2907,7 +2923,7 @@ fn spawn_agent_worker(
chat_id: cid2, chat_id: cid2,
content: "Failed to save the agent response, please try again." content: "Failed to save the agent response, please try again."
.to_string(), .to_string(),
reply_to: None, reply_to: task_reply_to2.clone(),
media: vec![], media: vec![],
metadata: outbound_turn_metadata( metadata: outbound_turn_metadata(
&response_session_id, &response_session_id,
@ -2933,7 +2949,7 @@ fn spawn_agent_worker(
channel: chan2, channel: chan2,
chat_id: cid2, chat_id: cid2,
content: response, content: response,
reply_to: None, reply_to: task_reply_to2.clone(),
media: vec![], media: vec![],
metadata: outbound_turn_metadata( metadata: outbound_turn_metadata(
&response_session_id, &response_session_id,
@ -2976,10 +2992,7 @@ fn spawn_agent_worker(
deliver_terminal_fallback( deliver_terminal_fallback(
handle, handle,
&bus, &bus,
&task_chan, &turn_target,
&task_cid,
&unified_str,
&task_metadata,
&turn_controller, &turn_controller,
) )
.await; .await;