diff --git a/codex-rs/wasm-harness/src/harness.rs b/codex-rs/wasm-harness/src/harness.rs deleted file mode 100644 index a57f09be75..0000000000 --- a/codex-rs/wasm-harness/src/harness.rs +++ /dev/null @@ -1,374 +0,0 @@ -use crate::HarnessError; -use crate::responses::HarnessEvent; -use crate::responses::ResponsesFunctionCall; -use crate::responses::ResponsesRequest; -use crate::responses::ResponsesResponse; -use crate::responses::ResponsesTool; -use crate::responses::build_browser_instructions; -use crate::responses::tool_output_item; -use async_trait::async_trait; -use serde_json::Value; - -const DEFAULT_MODEL: &str = "gpt-5.1"; -const DEFAULT_MAX_TOOL_ROUNDS: usize = 8; - -#[derive(Clone, Debug)] -pub struct HarnessConfig { - pub model: String, - pub instructions: String, - pub max_tool_rounds: usize, -} - -impl Default for HarnessConfig { - fn default() -> Self { - Self { - model: DEFAULT_MODEL.to_string(), - instructions: build_browser_instructions(), - max_tool_rounds: DEFAULT_MAX_TOOL_ROUNDS, - } - } -} - -pub trait EventSink { - fn emit(&self, event: &HarnessEvent) -> Result<(), HarnessError>; -} - -#[async_trait(?Send)] -pub trait ResponsesClient { - async fn create_response( - &self, - request: ResponsesRequest, - ) -> Result; -} - -#[async_trait(?Send)] -pub trait ToolExecutor { - fn tools(&self) -> Vec; - - async fn execute(&self, function_call: &ResponsesFunctionCall) -> Result; -} - -pub struct EmbeddedHarness { - config: HarnessConfig, - responses_client: C, - tool_executor: T, - next_turn_id: u32, -} - -impl EmbeddedHarness { - #[must_use] - pub fn new(config: HarnessConfig, responses_client: C, tool_executor: T) -> Self { - Self { - config, - responses_client, - tool_executor, - next_turn_id: 0, - } - } - - pub fn responses_client_mut(&mut self) -> &mut C { - &mut self.responses_client - } - - pub fn tool_executor_mut(&mut self) -> &mut T { - &mut self.tool_executor - } - - pub fn config_mut(&mut self) -> &mut HarnessConfig { - &mut self.config - } -} - -impl EmbeddedHarness -where - C: ResponsesClient, - T: ToolExecutor, -{ - pub async fn submit_turn( - &mut self, - prompt: impl Into, - event_sink: &E, - ) -> Result { - self.next_turn_id += 1; - let turn_id = format!("browser-turn-{}", self.next_turn_id); - let prompt = prompt.into(); - - event_sink.emit(&HarnessEvent::TurnStarted { - turn_id: turn_id.clone(), - model_context_window: None, - collaboration_mode_kind: "default".to_string(), - })?; - event_sink.emit(&HarnessEvent::UserMessage { - turn_id: turn_id.clone(), - message: prompt.clone(), - })?; - - let result = self.run_turn(&turn_id, &prompt, event_sink).await; - match result { - Ok(last_agent_message) => { - event_sink.emit(&HarnessEvent::TurnComplete { - turn_id, - last_agent_message: Some(last_agent_message.clone()), - })?; - Ok(last_agent_message) - } - Err(err) => { - event_sink.emit(&HarnessEvent::TurnError { - turn_id: turn_id.clone(), - message: err.to_string(), - })?; - event_sink.emit(&HarnessEvent::TurnComplete { - turn_id, - last_agent_message: None, - })?; - Err(err) - } - } - } - - async fn run_turn( - &self, - turn_id: &str, - prompt: &str, - event_sink: &E, - ) -> Result { - let mut previous_response_id: Option = None; - let mut input = Value::String(prompt.to_string()); - let mut last_agent_message: Option = None; - - for round in 0..self.config.max_tool_rounds { - let tools = self.tool_executor.tools(); - let request = ResponsesRequest { - model: self.config.model.clone(), - instructions: self.config.instructions.clone(), - input, - previous_response_id: previous_response_id.clone(), - tools: (!tools.is_empty()).then_some(tools), - parallel_tool_calls: false, - }; - let response = self.responses_client.create_response(request).await?; - - previous_response_id = response.id.clone(); - - let agent_message = response.response_text(); - if !agent_message.is_empty() { - event_sink.emit(&HarnessEvent::AgentMessageDelta { - turn_id: turn_id.to_string(), - delta: agent_message.clone(), - })?; - event_sink.emit(&HarnessEvent::AgentMessage { - turn_id: turn_id.to_string(), - message: agent_message.clone(), - })?; - last_agent_message = Some(agent_message); - } - - let function_calls = response.function_calls()?; - if function_calls.is_empty() { - return Ok(last_agent_message.unwrap_or_else(|| { - "Responses API returned no assistant message.".to_string() - })); - } - - let response_id = previous_response_id.clone().ok_or_else(|| { - HarnessError::new("Responses API omitted response.id for a tool-calling turn") - })?; - - let mut tool_outputs = Vec::with_capacity(function_calls.len()); - for function_call in function_calls { - event_sink.emit(&HarnessEvent::ToolCallStarted { - turn_id: turn_id.to_string(), - response_id: response_id.clone(), - call_id: function_call.call_id.clone(), - name: function_call.name.clone(), - arguments: function_call.arguments.clone(), - })?; - - let output = self.tool_executor.execute(&function_call).await?; - event_sink.emit(&HarnessEvent::ToolCallCompleted { - turn_id: turn_id.to_string(), - response_id: response_id.clone(), - call_id: function_call.call_id.clone(), - name: function_call.name.clone(), - output: output.clone(), - })?; - tool_outputs.push(tool_output_item(&function_call.call_id, output)); - } - - input = Value::Array(tool_outputs); - - if round + 1 == self.config.max_tool_rounds { - return Err(HarnessError::new( - "turn exceeded the browser tool-round limit", - )); - } - } - - Err(HarnessError::new("browser turn loop exited unexpectedly")) - } -} - -#[cfg(test)] -mod tests { - use super::EmbeddedHarness; - use super::EventSink; - use super::HarnessConfig; - use super::ResponsesClient; - use super::ToolExecutor; - use crate::HarnessError; - use crate::responses::HarnessEvent; - use crate::responses::ResponsesFunctionCall; - use crate::responses::ResponsesRequest; - use crate::responses::ResponsesResponse; - use crate::responses::ResponsesTool; - use async_trait::async_trait; - use pretty_assertions::assert_eq; - use std::cell::RefCell; - use std::collections::VecDeque; - - struct RecordingEventSink { - events: RefCell>, - } - - impl RecordingEventSink { - fn new() -> Self { - Self { - events: RefCell::new(Vec::new()), - } - } - - fn events(&self) -> Vec { - self.events.borrow().clone() - } - } - - impl EventSink for RecordingEventSink { - fn emit(&self, event: &HarnessEvent) -> Result<(), HarnessError> { - self.events.borrow_mut().push(event.clone()); - Ok(()) - } - } - - struct FakeResponsesClient { - responses: RefCell>, - requests: RefCell>, - } - - impl FakeResponsesClient { - fn new(responses: Vec) -> Self { - Self { - responses: RefCell::new(responses.into()), - requests: RefCell::new(Vec::new()), - } - } - } - - #[async_trait(?Send)] - impl ResponsesClient for FakeResponsesClient { - async fn create_response( - &self, - request: ResponsesRequest, - ) -> Result { - self.requests.borrow_mut().push(request); - self.responses - .borrow_mut() - .pop_front() - .ok_or_else(|| HarnessError::new("no fake response available")) - } - } - - struct FakeToolExecutor; - - #[async_trait(?Send)] - impl ToolExecutor for FakeToolExecutor { - fn tools(&self) -> Vec { - vec![ResponsesTool::exec_js()] - } - - async fn execute( - &self, - function_call: &ResponsesFunctionCall, - ) -> Result { - assert_eq!(function_call.name, "exec_js"); - Ok("Hello, world!".to_string()) - } - } - - #[tokio::test] - async fn embedded_harness_completes_tool_turn() { - let responses = vec![ - serde_json::from_str( - r#"{ - "id": "resp_1", - "output": [ - { - "type": "function_call", - "call_id": "call_1", - "name": "exec_js", - "arguments": "{\"code\":\"console.log('Hello, world!')\"}" - } - ] - }"#, - ) - .expect("response should deserialize"), - serde_json::from_str( - r#"{ - "id": "resp_2", - "output_text": "Done." - }"#, - ) - .expect("response should deserialize"), - ]; - let client = FakeResponsesClient::new(responses); - let tool_executor = FakeToolExecutor; - let sink = RecordingEventSink::new(); - let mut harness = EmbeddedHarness::new(HarnessConfig::default(), client, tool_executor); - - let result = harness - .submit_turn("write hello world", &sink) - .await - .expect("turn should succeed"); - - assert_eq!(result, "Done."); - assert_eq!( - sink.events(), - vec![ - HarnessEvent::TurnStarted { - turn_id: "browser-turn-1".to_string(), - model_context_window: None, - collaboration_mode_kind: "default".to_string(), - }, - HarnessEvent::UserMessage { - turn_id: "browser-turn-1".to_string(), - message: "write hello world".to_string(), - }, - HarnessEvent::ToolCallStarted { - turn_id: "browser-turn-1".to_string(), - response_id: "resp_1".to_string(), - call_id: "call_1".to_string(), - name: "exec_js".to_string(), - arguments: r#"{"code":"console.log('Hello, world!')"}"#.to_string(), - }, - HarnessEvent::ToolCallCompleted { - turn_id: "browser-turn-1".to_string(), - response_id: "resp_1".to_string(), - call_id: "call_1".to_string(), - name: "exec_js".to_string(), - output: "Hello, world!".to_string(), - }, - HarnessEvent::AgentMessageDelta { - turn_id: "browser-turn-1".to_string(), - delta: "Done.".to_string(), - }, - HarnessEvent::AgentMessage { - turn_id: "browser-turn-1".to_string(), - message: "Done.".to_string(), - }, - HarnessEvent::TurnComplete { - turn_id: "browser-turn-1".to_string(), - last_agent_message: Some("Done.".to_string()), - }, - ] - ); - } -}