Files
codex/codex-rs/ext/goal/src/events.rs
T
jif-oaiandGitHub 4ca60ef9ff Emit goal update events from goal extension tools (#23306)
## Why

Goal creation and completion are moving through the goal extension, but
the rest of Codex still observes goal state through `ThreadGoalUpdated`
events. Without an event from the extension-owned tool path, a
model-initiated `create_goal` or `update_goal` can mutate the backend
and return a tool result while app-server and TUI listeners miss the
goal state transition.

## What changed

- Added `GoalEventEmitter` as a small wrapper around the host
`ExtensionEventSink` to build `EventMsg::ThreadGoalUpdated` events for
goal updates.
- Threaded the registry event sink into `GoalExtension` and the
`GoalToolExecutor`s created by the extension. The public
`GoalExtension::new` constructor keeps a `NoopExtensionEventSink`
fallback for standalone use.
- Emitted a goal update after successful `create_goal` and `update_goal`
tool calls. Until `ToolCall` exposes the current turn submission id,
these events use the tool call id as the event id and leave `turn_id`
unset.

Relevant code:

-
[`GoalEventEmitter::thread_goal_updated`](https://github.com/openai/codex/blob/1fe2d73890df9a50996f67f705d4da4cc3d4b866/codex-rs/ext/goal/src/events.rs#L19-L32)
- [`GoalToolExecutor` emission
points](https://github.com/openai/codex/blob/1fe2d73890df9a50996f67f705d4da4cc3d4b866/codex-rs/ext/goal/src/tool.rs#L161-L190)

## Testing

- `cargo test -p codex-goal-extension`
2026-05-18 16:14:37 +02:00

35 lines
874 B
Rust

use std::sync::Arc;
use codex_extension_api::ExtensionEventSink;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ThreadGoal;
use codex_protocol::protocol::ThreadGoalUpdatedEvent;
#[derive(Clone)]
pub(crate) struct GoalEventEmitter {
sink: Arc<dyn ExtensionEventSink>,
}
impl GoalEventEmitter {
pub(crate) fn new(sink: Arc<dyn ExtensionEventSink>) -> Self {
Self { sink }
}
pub(crate) fn thread_goal_updated(
&self,
event_id: impl Into<String>,
turn_id: Option<String>,
goal: ThreadGoal,
) {
self.sink.emit(Event {
id: event_id.into(),
msg: EventMsg::ThreadGoalUpdated(ThreadGoalUpdatedEvent {
thread_id: goal.thread_id,
turn_id,
goal,
}),
});
}
}