mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add goal app-server API (2 / 5) (#18074)
Adds the app-server v2 goal API on top of the persisted goal state from PR 1. ## Why Clients need a stable app-server surface for reading and controlling materialized thread goals before the model tools and TUI can use them. Goal changes also need to be observable by app-server clients, including clients that resume an existing thread. ## What changed - Added v2 `thread/goal/get`, `thread/goal/set`, and `thread/goal/clear` RPCs for materialized threads. - Added `thread/goal/updated` and `thread/goal/cleared` notifications so clients can keep local goal state in sync. - Added resume/snapshot wiring so reconnecting clients see the current goal state for a thread. - Added app-server handlers that reconcile persisted rollout state before direct goal mutations. - Updated the app-server README plus generated JSON and TypeScript schema fixtures for the new API surface. ## Verification - Added app-server v2 coverage for goal get/set/clear behavior, notification emission, resume snapshots, and non-local thread-store interactions.
This commit is contained in:
committed by
GitHub
Unverified
parent
0ee737cea6
commit
6c874f9b34
@@ -150,6 +150,16 @@ use codex_app_server_protocol::ThreadDecrementElicitationParams;
|
||||
use codex_app_server_protocol::ThreadDecrementElicitationResponse;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadGoal;
|
||||
use codex_app_server_protocol::ThreadGoalClearParams;
|
||||
use codex_app_server_protocol::ThreadGoalClearResponse;
|
||||
use codex_app_server_protocol::ThreadGoalClearedNotification;
|
||||
use codex_app_server_protocol::ThreadGoalGetParams;
|
||||
use codex_app_server_protocol::ThreadGoalGetResponse;
|
||||
use codex_app_server_protocol::ThreadGoalSetParams;
|
||||
use codex_app_server_protocol::ThreadGoalSetResponse;
|
||||
use codex_app_server_protocol::ThreadGoalStatus;
|
||||
use codex_app_server_protocol::ThreadGoalUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadIncrementElicitationParams;
|
||||
use codex_app_server_protocol::ThreadIncrementElicitationResponse;
|
||||
use codex_app_server_protocol::ThreadInjectItemsParams;
|
||||
@@ -482,6 +492,9 @@ enum ThreadReadViewError {
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
mod thread_goal_handlers;
|
||||
use self::thread_goal_handlers::api_thread_goal_from_state;
|
||||
|
||||
impl Drop for ActiveLogin {
|
||||
fn drop(&mut self) {
|
||||
self.cancel();
|
||||
@@ -955,6 +968,18 @@ impl CodexMessageProcessor {
|
||||
self.thread_set_name(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadGoalSet { request_id, params } => {
|
||||
self.thread_goal_set(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadGoalGet { request_id, params } => {
|
||||
self.thread_goal_get(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadGoalClear { request_id, params } => {
|
||||
self.thread_goal_clear(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadMetadataUpdate { request_id, params } => {
|
||||
self.thread_metadata_update(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -4695,6 +4720,9 @@ impl CodexMessageProcessor {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if self.config.features.enabled(Feature::Goals) {
|
||||
self.emit_thread_goal_snapshot(thread_id).await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
@@ -4860,6 +4888,17 @@ impl CodexMessageProcessor {
|
||||
return true;
|
||||
};
|
||||
|
||||
let emit_thread_goal_update = self.config.features.enabled(Feature::Goals);
|
||||
let thread_goal_state_db = if emit_thread_goal_update {
|
||||
if let Some(state_db) = existing_thread.state_db() {
|
||||
Some(state_db)
|
||||
} else {
|
||||
open_state_db_for_direct_thread_lookup(&self.config).await
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let command = crate::thread_state::ThreadListenerCommand::SendThreadResumeResponse(
|
||||
Box::new(crate::thread_state::PendingThreadResumeRequest {
|
||||
request_id: request_id.clone(),
|
||||
@@ -4867,6 +4906,8 @@ impl CodexMessageProcessor {
|
||||
config_snapshot,
|
||||
instruction_sources,
|
||||
thread_summary,
|
||||
emit_thread_goal_update,
|
||||
thread_goal_state_db,
|
||||
include_turns: !params.exclude_turns,
|
||||
}),
|
||||
);
|
||||
@@ -4879,6 +4920,7 @@ impl CodexMessageProcessor {
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -8800,6 +8842,29 @@ async fn handle_thread_listener_command(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ThreadListenerCommand::EmitThreadGoalUpdated { goal } => {
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ThreadGoalUpdated(
|
||||
ThreadGoalUpdatedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: None,
|
||||
goal,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
}
|
||||
ThreadListenerCommand::EmitThreadGoalCleared => {
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ThreadGoalCleared(
|
||||
ThreadGoalClearedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
},
|
||||
))
|
||||
.await;
|
||||
}
|
||||
ThreadListenerCommand::EmitThreadGoalSnapshot { state_db } => {
|
||||
send_thread_goal_snapshot_notification(outgoing, conversation_id, &state_db).await;
|
||||
}
|
||||
ThreadListenerCommand::ResolveServerRequest {
|
||||
request_id,
|
||||
completion_tx,
|
||||
@@ -8964,11 +9029,56 @@ async fn handle_pending_thread_resume_request(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if pending.emit_thread_goal_update {
|
||||
if let Some(state_db) = pending.thread_goal_state_db {
|
||||
send_thread_goal_snapshot_notification(outgoing, conversation_id, &state_db).await;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
thread_id = %conversation_id,
|
||||
"state db unavailable when reading thread goal for running thread resume"
|
||||
);
|
||||
}
|
||||
}
|
||||
outgoing
|
||||
.replay_requests_to_connection_for_thread(connection_id, conversation_id)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send_thread_goal_snapshot_notification(
|
||||
outgoing: &Arc<OutgoingMessageSender>,
|
||||
thread_id: ThreadId,
|
||||
state_db: &StateDbHandle,
|
||||
) {
|
||||
match state_db.get_thread_goal(thread_id).await {
|
||||
Ok(Some(goal)) => {
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ThreadGoalUpdated(
|
||||
ThreadGoalUpdatedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: None,
|
||||
goal: api_thread_goal_from_state(goal),
|
||||
},
|
||||
))
|
||||
.await;
|
||||
}
|
||||
Ok(None) => {
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ThreadGoalCleared(
|
||||
ThreadGoalClearedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
},
|
||||
))
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
thread_id = %thread_id,
|
||||
"failed to read thread goal for resume snapshot: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ThreadTurnSource<'a> {
|
||||
HistoryItems(&'a [RolloutItem]),
|
||||
}
|
||||
@@ -9459,6 +9569,27 @@ async fn open_state_db_for_direct_thread_lookup(config: &Config) -> Option<State
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn invalid_request(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: message.into(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_error(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: message.into(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_thread_id_for_request(thread_id: &str) -> Result<ThreadId, JSONRPCErrorError> {
|
||||
ThreadId::from_string(thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))
|
||||
}
|
||||
|
||||
fn non_empty_title(metadata: &ThreadMetadata) -> Option<String> {
|
||||
let title = metadata.title.trim();
|
||||
(!title.is_empty()).then(|| title.to_string())
|
||||
|
||||
Reference in New Issue
Block a user