mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Move item event mapping into app-server-protocol (#20299)
## Why Follow-up to #20291. The v2 item-event-to-notification translation had been embedded in `app-server/src/bespoke_event_handling.rs`, which made it hard to reuse anywhere else. This PR moves that stateless mapping into shared protocol code so other entry points can produce the same `ServerNotification` payloads without copying app-server logic. That also lets `thread-manager-sample` demonstrate the same notification surface that the app server exposes, instead of only printing the final assistant message. ## What changed - move `item_event_to_server_notification` into `codex-app-server-protocol::protocol::event_mapping` - keep the mapper tests next to the shared implementation in `codex-app-server-protocol` - re-export the mapper from `codex-core-api` so lightweight consumers can use it without reaching into `app-server-protocol` directly - simplify `app-server/src/bespoke_event_handling.rs` so it delegates the stateless event-to-notification projection to the shared helper - update `thread-manager-sample` to: - print mapped notifications as newline-delimited JSON - use the shared mapper through `codex-core-api` - enable the default feature set so the sample exposes the normal tool surface - use a `read_only` permission profile so shell commands can run in the sample without widening permissions ## Testing - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-core-api` - `cargo test -p codex-app-server bespoke_event_handling::tests` - `cargo test -p codex-thread-manager-sample` - `cargo run -p codex-thread-manager-sample -- "briefly explore the repo with pwd and ls, then summarize it"`
This commit is contained in:
committed by
GitHub
Unverified
parent
c70cdc108f
commit
5cc5f12efc
@@ -23,6 +23,7 @@ use codex_core_api::EnvironmentManager;
|
||||
use codex_core_api::EnvironmentManagerArgs;
|
||||
use codex_core_api::EventMsg;
|
||||
use codex_core_api::ExecServerRuntimePaths;
|
||||
use codex_core_api::Features;
|
||||
use codex_core_api::GhostSnapshotConfig;
|
||||
use codex_core_api::History;
|
||||
use codex_core_api::MemoriesConfig;
|
||||
@@ -53,13 +54,14 @@ use codex_core_api::WebSearchMode;
|
||||
use codex_core_api::arg0_dispatch_or_else;
|
||||
use codex_core_api::built_in_model_providers;
|
||||
use codex_core_api::find_codex_home;
|
||||
use codex_core_api::item_event_to_server_notification;
|
||||
use codex_core_api::set_default_originator;
|
||||
use codex_core_api::thread_store_from_config;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
name = "codex-thread-manager-sample",
|
||||
about = "Run one Codex turn through ThreadManager and print the final assistant output."
|
||||
about = "Run one Codex turn through ThreadManager and print mapped notifications as newline-delimited JSON."
|
||||
)]
|
||||
struct Args {
|
||||
/// Override the model for this run.
|
||||
@@ -125,19 +127,14 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
.await
|
||||
.context("start Codex thread")?;
|
||||
|
||||
let turn_output = run_turn(&thread, prompt).await;
|
||||
let thread_id_string = thread_id.to_string();
|
||||
let turn_output = run_turn(&thread, &thread_id_string, prompt).await;
|
||||
let shutdown_result = thread.shutdown_and_wait().await;
|
||||
let _ = thread_manager.remove_thread(&thread_id).await;
|
||||
|
||||
let output = turn_output?;
|
||||
turn_output?;
|
||||
shutdown_result.context("shut down Codex thread")?;
|
||||
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
stdout.write_all(output.as_bytes())?;
|
||||
if !output.ends_with('\n') {
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -151,7 +148,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
.context("OpenAI model provider should be available")?
|
||||
.clone();
|
||||
|
||||
Ok(Config {
|
||||
let mut config = Config {
|
||||
config_layer_stack: ConfigLayerStack::default(),
|
||||
startup_warnings: Vec::new(),
|
||||
model,
|
||||
@@ -164,7 +161,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
personality: None,
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::default()),
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
active_permission_profile: None,
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
@@ -261,10 +258,15 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
feedback_enabled: false,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
otel: OtelConfig::default(),
|
||||
})
|
||||
};
|
||||
config
|
||||
.features
|
||||
.set(Features::with_defaults())
|
||||
.context("configure default features")?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
async fn run_turn(thread: &CodexThread, prompt: String) -> anyhow::Result<String> {
|
||||
async fn run_turn(thread: &CodexThread, thread_id: &str, prompt: String) -> anyhow::Result<()> {
|
||||
thread
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
@@ -278,15 +280,62 @@ async fn run_turn(thread: &CodexThread, prompt: String) -> anyhow::Result<String
|
||||
.await
|
||||
.context("submit user input")?;
|
||||
|
||||
let mut last_agent_message = String::new();
|
||||
let mut current_turn_id: Option<String> = None;
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
loop {
|
||||
let event = thread.next_event().await.context("read Codex event")?;
|
||||
match event.msg {
|
||||
EventMsg::TurnComplete(event) => {
|
||||
return Ok(event.last_agent_message.unwrap_or(last_agent_message));
|
||||
let notification = match &event.msg {
|
||||
EventMsg::TurnStarted(event) => {
|
||||
current_turn_id = Some(event.turn_id.clone());
|
||||
None
|
||||
}
|
||||
EventMsg::AgentMessage(event) => {
|
||||
last_agent_message = event.message;
|
||||
EventMsg::DynamicToolCallResponse(_)
|
||||
| EventMsg::McpToolCallBegin(_)
|
||||
| EventMsg::McpToolCallEnd(_)
|
||||
| EventMsg::CollabAgentSpawnBegin(_)
|
||||
| EventMsg::CollabAgentSpawnEnd(_)
|
||||
| EventMsg::CollabAgentInteractionBegin(_)
|
||||
| EventMsg::CollabAgentInteractionEnd(_)
|
||||
| EventMsg::CollabWaitingBegin(_)
|
||||
| EventMsg::CollabWaitingEnd(_)
|
||||
| EventMsg::CollabCloseBegin(_)
|
||||
| EventMsg::CollabCloseEnd(_)
|
||||
| EventMsg::CollabResumeBegin(_)
|
||||
| EventMsg::CollabResumeEnd(_)
|
||||
| EventMsg::AgentMessageContentDelta(_)
|
||||
| EventMsg::PlanDelta(_)
|
||||
| EventMsg::ReasoningContentDelta(_)
|
||||
| EventMsg::ReasoningRawContentDelta(_)
|
||||
| EventMsg::AgentReasoningSectionBreak(_)
|
||||
| EventMsg::ItemStarted(_)
|
||||
| EventMsg::ItemCompleted(_)
|
||||
| EventMsg::PatchApplyBegin(_)
|
||||
| EventMsg::PatchApplyUpdated(_)
|
||||
| EventMsg::TerminalInteraction(_)
|
||||
| EventMsg::ExecCommandBegin(_)
|
||||
| EventMsg::ExecCommandOutputDelta(_)
|
||||
| EventMsg::ExecCommandEnd(_) => Some(item_event_to_server_notification(
|
||||
event.msg.clone(),
|
||||
thread_id,
|
||||
current_turn_id
|
||||
.as_deref()
|
||||
.context("mapped notification arrived before turn started")?,
|
||||
/*is_file_change_output*/ false,
|
||||
)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(notification) = notification {
|
||||
serde_json::to_writer(&mut stdout, ¬ification)
|
||||
.context("serialize mapped notification")?;
|
||||
stdout
|
||||
.write_all(b"\n")
|
||||
.context("write notification newline")?;
|
||||
stdout.flush().context("flush notification output")?;
|
||||
}
|
||||
|
||||
match event.msg {
|
||||
EventMsg::TurnComplete(_) => {
|
||||
return Ok(());
|
||||
}
|
||||
EventMsg::Error(event) => {
|
||||
bail!(event.message);
|
||||
|
||||
Reference in New Issue
Block a user