use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::domain::{ToolCallId, TurnId};
use crate::models::tool_call::ToolCall as ModelToolCall;
use crate::models::{ChatMessage, FinishReason, ReasoningChunk, TokenUsage};
use crate::runtime::SafetyMode;
use super::approval::ApprovalBroker;
use super::auto_classifier::AutoClassifier;
#[derive(Debug)]
pub struct StreamContext {
pub token: CancellationToken,
pub sink: mpsc::Sender<StreamEvent>,
pub turn: TurnId,
}
impl StreamContext {
pub fn new(token: CancellationToken, sink: mpsc::Sender<StreamEvent>, turn: TurnId) -> Self {
Self { token, sink, turn }
}
}
#[derive(Debug, Clone)]
pub enum StreamEvent {
Text(String),
Reasoning(ReasoningChunk),
ToolCall(ModelToolCall),
ThinkingSignature(String),
Done {
usage: Option<TokenUsage>,
thinking_signature: Option<String>,
stop_reason: Option<FinishReason>,
},
}
#[derive(Debug, Clone)]
pub struct FinalResponse {
pub usage: Option<TokenUsage>,
pub thinking_signature: Option<String>,
pub tool_calls: Vec<ModelToolCall>,
pub stop_reason: Option<FinishReason>,
}
pub struct ExecContext {
pub token: CancellationToken,
pub background: CancellationToken,
pub progress: mpsc::Sender<ProgressEvent>,
pub call_id: ToolCallId,
pub turn: TurnId,
pub workdir: PathBuf,
pub config: Arc<crate::app::Config>,
pub model_id: String,
pub task_id: Option<String>,
pub safety_mode: SafetyMode,
pub intent: Option<String>,
pub classifier: Option<Arc<dyn AutoClassifier>>,
pub approval: Option<ApprovalBroker>,
}
impl std::fmt::Debug for ExecContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExecContext")
.field("call_id", &self.call_id)
.field("turn", &self.turn)
.field("workdir", &self.workdir)
.field("model_id", &self.model_id)
.field("task_id", &self.task_id)
.field("safety_mode", &self.safety_mode)
.field("intent", &self.intent)
.field(
"classifier",
&self.classifier.as_ref().map(|_| "<dyn AutoClassifier>"),
)
.field(
"approval",
&self.approval.as_ref().map(|_| "<ApprovalBroker>"),
)
.finish_non_exhaustive()
}
}
impl ExecContext {
#[allow(clippy::too_many_arguments)]
pub fn new(
token: CancellationToken,
progress: mpsc::Sender<ProgressEvent>,
call_id: ToolCallId,
turn: TurnId,
workdir: PathBuf,
config: Arc<crate::app::Config>,
model_id: String,
task_id: Option<String>,
safety_mode: SafetyMode,
intent: Option<String>,
classifier: Option<Arc<dyn AutoClassifier>>,
approval: Option<ApprovalBroker>,
) -> Self {
Self {
token,
background: CancellationToken::new(),
progress,
call_id,
turn,
workdir,
config,
model_id,
task_id,
safety_mode,
intent,
classifier,
approval,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ProgressEvent {
Output(String),
Status(String),
Bytes { done: u64, total: Option<u64> },
Artifact {
mime: String,
#[serde(with = "crate::utils::serde_base64")]
data: Vec<u8>,
caption: Option<String>,
},
SubagentToolCall {
child_call_id: ToolCallId,
tool_name: String,
phase: SubagentPhase,
},
SubagentText(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SubagentPhase {
Started,
Finished,
Errored,
}
pub fn clone_messages(msgs: &[ChatMessage]) -> Vec<ChatMessage> {
msgs.to_vec()
}
pub fn test_stream_context(turn: TurnId) -> (StreamContext, mpsc::Receiver<StreamEvent>) {
let token = CancellationToken::new();
let (tx, rx) = mpsc::channel(64);
(StreamContext::new(token, tx, turn), rx)
}
pub fn test_exec_context(
turn: TurnId,
call_id: ToolCallId,
workdir: PathBuf,
) -> (ExecContext, mpsc::Receiver<ProgressEvent>) {
let token = CancellationToken::new();
let (tx, rx) = mpsc::channel(64);
let mut config = crate::app::Config::default();
config.safety.mode = crate::runtime::SafetyMode::FullAccess;
let config = Arc::new(config);
(
ExecContext::new(
token,
tx,
call_id,
turn,
workdir,
config,
String::new(),
None,
crate::runtime::SafetyMode::FullAccess,
None,
None,
None,
),
rx,
)
}
pub fn arc_sink(tx: mpsc::Sender<StreamEvent>) -> Arc<mpsc::Sender<StreamEvent>> {
Arc::new(tx)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[tokio::test]
async fn stream_context_carries_token_and_turn() {
let (ctx, _rx) = test_stream_context(TurnId(5));
assert_eq!(ctx.turn, TurnId(5));
assert!(!ctx.token.is_cancelled());
}
#[tokio::test]
async fn exec_context_propagates_cancel_signal() {
let (ctx, _rx) = test_exec_context(TurnId(1), ToolCallId(2), PathBuf::from("/tmp"));
let token = ctx.token.clone();
tokio::spawn(async move {
token.cancel();
});
ctx.token.cancelled().await;
assert!(ctx.token.is_cancelled());
}
#[tokio::test]
async fn progress_event_round_trips_through_channel() {
let (ctx, mut rx) = test_exec_context(TurnId(1), ToolCallId(2), PathBuf::from("/tmp"));
ctx.progress
.send(ProgressEvent::Status("halfway".to_string()))
.await
.expect("send");
match rx.recv().await.expect("recv") {
ProgressEvent::Status(s) => assert_eq!(s, "halfway"),
_ => panic!("wrong variant"),
}
}
}