This PR does two things because after I got deep into the first one I started pulling on the thread to the second: - Makes `ConversationManager` the place where all in-memory conversations are created and stored. Previously, `MessageProcessor` in the `codex-mcp-server` crate was doing this via its `session_map`, but this is something that should be done in `codex-core`. - It unwinds the `ctrl_c: tokio::sync::Notify` that was threaded throughout our code. I think this made sense at one time, but now that we handle Ctrl-C within the TUI and have a proper `Op::Interrupt` event, I don't think this was quite right, so I removed it. For `codex exec` and `codex proto`, we now use `tokio::signal::ctrl_c()` directly, but we no longer make `Notify` a field of `Codex` or `CodexConversation`. Changes of note: - Adds the files `conversation_manager.rs` and `codex_conversation.rs` to `codex-core`. - `Codex` and `CodexSpawnOk` are no longer exported from `codex-core`: other crates must use `CodexConversation` instead (which is created via `ConversationManager`). - `core/src/codex_wrapper.rs` has been deleted in favor of `ConversationManager`. - `ConversationManager::new_conversation()` returns `NewConversation`, which is in line with the `new_conversation` tool we want to add to the MCP server. Note `NewConversation` includes `SessionConfiguredEvent`, so we eliminate checks in cases like `codex-rs/core/tests/client.rs` to verify `SessionConfiguredEvent` is the first event because that is now internal to `ConversationManager`. - Quite a bit of code was deleted from `codex-rs/mcp-server/src/message_processor.rs` since it no longer has to manage multiple conversations itself: it goes through `ConversationManager` instead. - `core/tests/live_agent.rs` has been deleted because I had to update a bunch of tests and all the tests in here were ignored, and I don't think anyone ever ran them, so this was just technical debt, at this point. - Removed `notify_on_sigint()` from `util.rs` (and in a follow-up, I hope to refactor the blandly-named `util.rs` into more descriptive files). - In general, I started replacing local variables named `codex` as `conversation`, where appropriate, though admittedly I didn't do it through all the integration tests because that would have added a lot of noise to this PR. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2240). * #2264 * #2263 * __->__ #2240
142 lines
3.7 KiB
Rust
142 lines
3.7 KiB
Rust
#![cfg(unix)]
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use async_channel::Receiver;
|
|
use codex_core::exec::ExecParams;
|
|
use codex_core::exec::SandboxType;
|
|
use codex_core::exec::StdoutStream;
|
|
use codex_core::exec::process_exec_tool_call;
|
|
use codex_core::protocol::Event;
|
|
use codex_core::protocol::EventMsg;
|
|
use codex_core::protocol::ExecCommandOutputDeltaEvent;
|
|
use codex_core::protocol::ExecOutputStream;
|
|
use codex_core::protocol::SandboxPolicy;
|
|
|
|
fn collect_stdout_events(rx: Receiver<Event>) -> Vec<u8> {
|
|
let mut out = Vec::new();
|
|
while let Ok(ev) = rx.try_recv() {
|
|
if let EventMsg::ExecCommandOutputDelta(ExecCommandOutputDeltaEvent {
|
|
stream: ExecOutputStream::Stdout,
|
|
chunk,
|
|
..
|
|
}) = ev.msg
|
|
{
|
|
out.extend_from_slice(&chunk);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_exec_stdout_stream_events_echo() {
|
|
let (tx, rx) = async_channel::unbounded::<Event>();
|
|
|
|
let stdout_stream = StdoutStream {
|
|
sub_id: "test-sub".to_string(),
|
|
call_id: "call-1".to_string(),
|
|
tx_event: tx,
|
|
};
|
|
|
|
let cmd = vec![
|
|
"/bin/sh".to_string(),
|
|
"-c".to_string(),
|
|
// Use printf for predictable behavior across shells
|
|
"printf 'hello-world\n'".to_string(),
|
|
];
|
|
|
|
let params = ExecParams {
|
|
command: cmd,
|
|
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
|
timeout_ms: Some(5_000),
|
|
env: HashMap::new(),
|
|
with_escalated_permissions: None,
|
|
justification: None,
|
|
};
|
|
|
|
let policy = SandboxPolicy::new_read_only_policy();
|
|
|
|
let result = process_exec_tool_call(
|
|
params,
|
|
SandboxType::None,
|
|
&policy,
|
|
&None,
|
|
Some(stdout_stream),
|
|
)
|
|
.await;
|
|
|
|
let result = match result {
|
|
Ok(r) => r,
|
|
Err(e) => panic!("process_exec_tool_call failed: {e}"),
|
|
};
|
|
|
|
assert_eq!(result.exit_code, 0);
|
|
assert_eq!(result.stdout.text, "hello-world\n");
|
|
|
|
let streamed = collect_stdout_events(rx);
|
|
// We should have received at least the same contents (possibly in one chunk)
|
|
assert_eq!(String::from_utf8_lossy(&streamed), "hello-world\n");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_exec_stderr_stream_events_echo() {
|
|
let (tx, rx) = async_channel::unbounded::<Event>();
|
|
|
|
let stdout_stream = StdoutStream {
|
|
sub_id: "test-sub".to_string(),
|
|
call_id: "call-2".to_string(),
|
|
tx_event: tx,
|
|
};
|
|
|
|
let cmd = vec![
|
|
"/bin/sh".to_string(),
|
|
"-c".to_string(),
|
|
// Write to stderr explicitly
|
|
"printf 'oops\n' 1>&2".to_string(),
|
|
];
|
|
|
|
let params = ExecParams {
|
|
command: cmd,
|
|
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
|
timeout_ms: Some(5_000),
|
|
env: HashMap::new(),
|
|
with_escalated_permissions: None,
|
|
justification: None,
|
|
};
|
|
|
|
let policy = SandboxPolicy::new_read_only_policy();
|
|
|
|
let result = process_exec_tool_call(
|
|
params,
|
|
SandboxType::None,
|
|
&policy,
|
|
&None,
|
|
Some(stdout_stream),
|
|
)
|
|
.await;
|
|
|
|
let result = match result {
|
|
Ok(r) => r,
|
|
Err(e) => panic!("process_exec_tool_call failed: {e}"),
|
|
};
|
|
|
|
assert_eq!(result.exit_code, 0);
|
|
assert_eq!(result.stdout.text, "");
|
|
assert_eq!(result.stderr.text, "oops\n");
|
|
|
|
// Collect only stderr delta events
|
|
let mut err = Vec::new();
|
|
while let Ok(ev) = rx.try_recv() {
|
|
if let EventMsg::ExecCommandOutputDelta(ExecCommandOutputDeltaEvent {
|
|
stream: ExecOutputStream::Stderr,
|
|
chunk,
|
|
..
|
|
}) = ev.msg
|
|
{
|
|
err.extend_from_slice(&chunk);
|
|
}
|
|
}
|
|
assert_eq!(String::from_utf8_lossy(&err), "oops\n");
|
|
}
|