diff --git a/Cargo.lock b/Cargo.lock index d4129a33e..972f4e882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1510,9 +1510,9 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -7351,9 +7351,9 @@ dependencies = [ [[package]] name = "rig-core" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437fa2a15825caf2505411bbe55b05c8eb122e03934938b38f9ecaa1d6ded7c8" +checksum = "a529b9b72f51a46a9ea87c9ba8031e36593de7a89461983f01d8c24462f9eb06" dependencies = [ "as-any", "async-stream", @@ -7377,6 +7377,7 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", + "tokio-tungstenite 0.23.1", "tracing", "tracing-futures", "url", @@ -7384,11 +7385,11 @@ dependencies = [ [[package]] name = "rig-derive" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f4b48f1449fa214d5cb11d0d0d952fd4c13b7ca5d1eaac64c87ce03cfb9e24" +checksum = "3b6d9818c9cb13d00664b52fd3e47b0554bc2d5c59cfb90340dd9411b09553bc" dependencies = [ - "convert_case 0.8.0", + "convert_case 0.10.0", "deluxe", "indoc", "proc-macro2", @@ -9469,6 +9470,22 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.36", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tungstenite 0.23.0", + "webpki-roots 0.26.11", +] + [[package]] name = "tokio-tungstenite" version = "0.28.0" @@ -9823,6 +9840,26 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.23.36", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.28.0" diff --git a/Cargo.toml b/Cargo.toml index eb157b1c9..f04c84646 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # LLM / Rig framework -rig = { version = "0.31", package = "rig-core", features = ["derive"] } +rig = { version = "0.33", package = "rig-core", features = ["derive"] } # HTTP clients for LLM providers reqwest = { version = "0.13", features = ["json", "stream", "form", "query", "gzip"] } diff --git a/src/agent/compactor.rs b/src/agent/compactor.rs index ea2856b45..5f7efc4b7 100644 --- a/src/agent/compactor.rs +++ b/src/agent/compactor.rs @@ -284,6 +284,9 @@ pub fn estimate_history_tokens(history: &[Message]) -> usize { chars += estimate_assistant_content_chars(item); } } + Message::System { content } => { + chars += content.len(); + } } } @@ -381,6 +384,11 @@ fn render_messages_as_transcript(messages: &[Message]) -> String { } } } + Message::System { content } => { + output.push_str("System: "); + output.push_str(content); + output.push('\n'); + } } } diff --git a/src/agent/worker.rs b/src/agent/worker.rs index bfaf89170..58c21d8e3 100644 --- a/src/agent/worker.rs +++ b/src/agent/worker.rs @@ -938,6 +938,10 @@ impl Worker { } } } + rig::message::Message::System { content } => { + let _ = writeln!(log, "[{index}] System:"); + let _ = writeln!(log, " {content}"); + } } } @@ -1130,6 +1134,7 @@ fn build_worker_recap(messages: &[rig::message::Message]) -> String { } } } + rig::message::Message::System { .. } => {} } } diff --git a/src/conversation/worker_transcript.rs b/src/conversation/worker_transcript.rs index 09d6aa9f9..23ec340f2 100644 --- a/src/conversation/worker_transcript.rs +++ b/src/conversation/worker_transcript.rs @@ -26,6 +26,11 @@ pub enum TranscriptStep { /// the correct `Message::User` role instead of treating everything as /// `Message::Assistant`. UserText { text: String }, + /// System-originated text (preamble, system prompt). + /// + /// Distinct from `UserText` so that system messages preserve their role + /// when round-tripped through the transcript. + SystemText { text: String }, /// Tool execution result. ToolResult { call_id: String, @@ -363,6 +368,13 @@ pub fn transcript_to_history(steps: &[TranscriptStep]) -> Vec { + if !text.is_empty() { + messages.push(Message::System { + content: text.clone(), + }); + } + } TranscriptStep::ToolResult { call_id, name: _, @@ -462,6 +474,11 @@ fn convert_history(history: &[rig::message::Message]) -> Vec { } } } + rig::message::Message::System { content } => { + steps.push(TranscriptStep::SystemText { + text: content.clone(), + }); + } } } diff --git a/src/llm/model.rs b/src/llm/model.rs index a40a1d313..ddaef3cd2 100644 --- a/src/llm/model.rs +++ b/src/llm/model.rs @@ -1306,6 +1306,10 @@ pub fn convert_messages_to_anthropic(messages: &OneOrMany) -> Vec Some(serde_json::json!({ + "role": "user", + "content": content, + })), }) .collect() } @@ -1367,6 +1371,12 @@ fn convert_messages_to_openai(messages: &OneOrMany) -> Vec { + result.push(serde_json::json!({ + "role": "user", + "content": content, + })); + } Message::Assistant { content, .. } => { let mut text_parts = Vec::new(); let mut reasoning_parts = Vec::new(); @@ -1486,6 +1496,12 @@ fn convert_messages_to_openai_responses(messages: &OneOrMany) -> Vec { + result.push(serde_json::json!({ + "role": "user", + "content": content, + })); + } Message::Assistant { content, .. } => { let mut text_parts = Vec::new(); let mut reasoning_parts = Vec::new(); @@ -3068,6 +3084,39 @@ mod tests { assert!(error.to_string().contains("stop_reason: max_tokens")); } + #[test] + fn convert_messages_to_anthropic_maps_system_to_user_role() { + let messages = OneOrMany::one(Message::System { + content: "You are a helpful assistant".to_string(), + }); + let converted = convert_messages_to_anthropic(&messages); + assert_eq!(converted.len(), 1); + assert_eq!(converted[0]["role"], "user"); + assert_eq!(converted[0]["content"], "You are a helpful assistant"); + } + + #[test] + fn convert_messages_to_openai_maps_system_to_user_role() { + let messages = OneOrMany::one(Message::System { + content: "You are a helpful assistant".to_string(), + }); + let converted = convert_messages_to_openai(&messages); + assert_eq!(converted.len(), 1); + assert_eq!(converted[0]["role"], "user"); + assert_eq!(converted[0]["content"], "You are a helpful assistant"); + } + + #[test] + fn convert_messages_to_openai_responses_maps_system_to_user_role() { + let messages = OneOrMany::one(Message::System { + content: "You are a helpful assistant".to_string(), + }); + let converted = convert_messages_to_openai_responses(&messages); + assert_eq!(converted.len(), 1); + assert_eq!(converted[0]["role"], "user"); + assert_eq!(converted[0]["content"], "You are a helpful assistant"); + } + #[test] fn parse_openai_response_handles_content_array_parts() { let body = serde_json::json!({ diff --git a/src/tools/worker_inspect.rs b/src/tools/worker_inspect.rs index edac9db8e..428bf26e5 100644 --- a/src/tools/worker_inspect.rs +++ b/src/tools/worker_inspect.rs @@ -135,6 +135,9 @@ impl Tool for WorkerInspectTool { worker_transcript::TranscriptStep::UserText { text } => { summary.push_str(&format!("**User:** {text}\n\n")); } + worker_transcript::TranscriptStep::SystemText { text } => { + summary.push_str(&format!("**System:** {text}\n\n")); + } worker_transcript::TranscriptStep::ToolResult { name, text, .. } => {