mirror of
https://github.com/openai/codex.git
synced 2026-05-19 10:43:38 +00:00
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`](1fe2d73890/codex-rs/ext/goal/src/events.rs (L19-L32)) - [`GoalToolExecutor` emission points](1fe2d73890/codex-rs/ext/goal/src/tool.rs (L161-L190)) ## Testing - `cargo test -p codex-goal-extension`
This commit is contained in:
34
codex-rs/ext/goal/src/events.rs
Normal file
34
codex-rs/ext/goal/src/events.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use codex_extension_api::ConfigContributor;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_extension_api::ExtensionEventSink;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::NoopExtensionEventSink;
|
||||
use codex_extension_api::ThreadLifecycleContributor;
|
||||
use codex_extension_api::ThreadStartInput;
|
||||
use codex_extension_api::TokenUsageContributor;
|
||||
@@ -18,6 +20,7 @@ use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
|
||||
use crate::accounting::GoalAccountingState;
|
||||
use crate::events::GoalEventEmitter;
|
||||
use crate::tool::CreateGoalRequest;
|
||||
use crate::tool::GoalToolExecutor;
|
||||
|
||||
@@ -35,6 +38,7 @@ impl GoalExtensionConfig {
|
||||
#[derive(Clone)]
|
||||
pub struct GoalExtension<C> {
|
||||
backend: Arc<dyn GoalToolBackend>,
|
||||
event_emitter: GoalEventEmitter,
|
||||
goals_enabled: Arc<dyn Fn(&C) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
@@ -48,9 +52,18 @@ impl<C> GoalExtension<C> {
|
||||
pub fn new(
|
||||
backend: Arc<dyn GoalToolBackend>,
|
||||
goals_enabled: impl Fn(&C) -> bool + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self::new_with_event_sink(backend, Arc::new(NoopExtensionEventSink), goals_enabled)
|
||||
}
|
||||
|
||||
pub fn new_with_event_sink(
|
||||
backend: Arc<dyn GoalToolBackend>,
|
||||
event_sink: Arc<dyn ExtensionEventSink>,
|
||||
goals_enabled: impl Fn(&C) -> bool + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
backend,
|
||||
event_emitter: GoalEventEmitter::new(event_sink),
|
||||
goals_enabled: Arc::new(goals_enabled),
|
||||
}
|
||||
}
|
||||
@@ -231,14 +244,20 @@ where
|
||||
return Vec::new();
|
||||
};
|
||||
vec![
|
||||
Arc::new(GoalToolExecutor::get(thread_id, Arc::clone(&self.backend))),
|
||||
Arc::new(GoalToolExecutor::get(
|
||||
thread_id,
|
||||
Arc::clone(&self.backend),
|
||||
self.event_emitter.clone(),
|
||||
)),
|
||||
Arc::new(GoalToolExecutor::create(
|
||||
thread_id,
|
||||
Arc::clone(&self.backend),
|
||||
self.event_emitter.clone(),
|
||||
)),
|
||||
Arc::new(GoalToolExecutor::update(
|
||||
thread_id,
|
||||
Arc::clone(&self.backend),
|
||||
self.event_emitter.clone(),
|
||||
)),
|
||||
]
|
||||
}
|
||||
@@ -260,7 +279,11 @@ pub fn install_with_backend<C>(
|
||||
) where
|
||||
C: Send + Sync + 'static,
|
||||
{
|
||||
let extension = Arc::new(GoalExtension::new(backend, goals_enabled));
|
||||
let extension = Arc::new(GoalExtension::new_with_event_sink(
|
||||
backend,
|
||||
registry.event_sink(),
|
||||
goals_enabled,
|
||||
));
|
||||
registry.thread_lifecycle_contributor(extension.clone());
|
||||
registry.config_contributor(extension.clone());
|
||||
registry.turn_lifecycle_contributor(extension.clone());
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! accounting that can be represented with today's extension API.
|
||||
|
||||
mod accounting;
|
||||
mod events;
|
||||
mod extension;
|
||||
mod spec;
|
||||
mod tool;
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_protocol::protocol::validate_thread_goal_objective;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::events::GoalEventEmitter;
|
||||
use crate::extension::GoalToolBackend;
|
||||
use crate::spec::CREATE_GOAL_TOOL_NAME;
|
||||
use crate::spec::GET_GOAL_TOOL_NAME;
|
||||
@@ -28,6 +29,7 @@ pub(crate) struct GoalToolExecutor {
|
||||
kind: GoalToolKind,
|
||||
thread_id: ThreadId,
|
||||
backend: Arc<dyn GoalToolBackend>,
|
||||
event_emitter: GoalEventEmitter,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -65,27 +67,42 @@ enum CompletionBudgetReport {
|
||||
}
|
||||
|
||||
impl GoalToolExecutor {
|
||||
pub(crate) fn get(thread_id: ThreadId, backend: Arc<dyn GoalToolBackend>) -> Self {
|
||||
pub(crate) fn get(
|
||||
thread_id: ThreadId,
|
||||
backend: Arc<dyn GoalToolBackend>,
|
||||
event_emitter: GoalEventEmitter,
|
||||
) -> Self {
|
||||
Self {
|
||||
kind: GoalToolKind::Get,
|
||||
thread_id,
|
||||
backend,
|
||||
event_emitter,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create(thread_id: ThreadId, backend: Arc<dyn GoalToolBackend>) -> Self {
|
||||
pub(crate) fn create(
|
||||
thread_id: ThreadId,
|
||||
backend: Arc<dyn GoalToolBackend>,
|
||||
event_emitter: GoalEventEmitter,
|
||||
) -> Self {
|
||||
Self {
|
||||
kind: GoalToolKind::Create,
|
||||
thread_id,
|
||||
backend,
|
||||
event_emitter,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update(thread_id: ThreadId, backend: Arc<dyn GoalToolBackend>) -> Self {
|
||||
pub(crate) fn update(
|
||||
thread_id: ThreadId,
|
||||
backend: Arc<dyn GoalToolBackend>,
|
||||
event_emitter: GoalEventEmitter,
|
||||
) -> Self {
|
||||
Self {
|
||||
kind: GoalToolKind::Update,
|
||||
thread_id,
|
||||
backend,
|
||||
event_emitter,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +163,7 @@ impl GoalToolExecutor {
|
||||
.create_goal(self.thread_id, request)
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
self.emit_goal_updated_from_tool_call(&invocation, goal.clone());
|
||||
goal_response(Some(goal), CompletionBudgetReport::Omit)
|
||||
}
|
||||
|
||||
@@ -168,8 +186,20 @@ impl GoalToolExecutor {
|
||||
.complete_goal(self.thread_id)
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
self.emit_goal_updated_from_tool_call(&invocation, goal.clone());
|
||||
goal_response(Some(goal), CompletionBudgetReport::Include)
|
||||
}
|
||||
|
||||
fn emit_goal_updated_from_tool_call(&self, invocation: &ToolCall, goal: ThreadGoal) {
|
||||
// TODO: ToolCall should expose the current turn submission id so goal
|
||||
// tool events can set ThreadGoalUpdatedEvent.turn_id exactly as core
|
||||
// does today. Until then, correlate the event with the tool call id.
|
||||
self.event_emitter.thread_goal_updated(
|
||||
invocation.call_id.clone(),
|
||||
/*turn_id*/ None,
|
||||
goal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_arguments<T>(arguments: &str) -> Result<T, FunctionCallError>
|
||||
|
||||
Reference in New Issue
Block a user