mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Add experimental thread/sample requests to the app server (#55339)
## What changed Add `thread/sample` to collect one model response on an ephemeral fork that inherits a loaded thread's context and configuration. Return the source turn ID, response ID, assistant text, and available token usage without modifying the source conversation. Require an idle, persistent root thread and an `expectedTurnId` matching its latest completed turn. Reject results if the source history changes during sampling. Keep the parent responsive while sampling, hide sample forks from `thread/loaded/list`, and reclaim them after completion or timeout. Intercept responses before tool execution or Stop hooks can run, returning null text for tool calls or responses without text. Apply optional reasoning-effort updates only when the provider and model support cache-preserving updates. ## Testing Add integration coverage for inherited context and reasoning effort, source preservation, tool and Stop hook suppression, invalid requests, stream errors, parent responsiveness, and timeout cleanup. GitOrigin-RevId: 08f4867e53ec7f6c8f4428d5542eeea4e6dadd0b
This commit is contained in:
@@ -11,7 +11,11 @@ use super::thread_fork_goal::inherit_thread_goal_snapshot;
|
||||
use super::thread_input::can_accept_direct_input;
|
||||
use super::thread_input::ensure_direct_input_allowed;
|
||||
use super::*;
|
||||
use crate::connection_rpc_gate::ConnectionRpcGate;
|
||||
use crate::error_code::method_not_found;
|
||||
use crate::thread_sample::SampleTask;
|
||||
use crate::thread_sample::SampleThread;
|
||||
use crate::thread_sample::generate_sample;
|
||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||
use codex_app_server_protocol::ThreadHistoryMode as ApiThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItemsListAnchor;
|
||||
@@ -19,6 +23,8 @@ use codex_app_server_protocol::ThreadItemsListCursor;
|
||||
use codex_app_server_protocol::ThreadRevertParams;
|
||||
use codex_app_server_protocol::ThreadRevertResponse;
|
||||
use codex_app_server_protocol::ThreadRevertedNotification;
|
||||
use codex_app_server_protocol::ThreadSampleParams;
|
||||
use codex_app_server_protocol::ThreadSampleResponse;
|
||||
use codex_app_server_protocol::ThreadSection;
|
||||
use codex_app_server_protocol::ThreadSectionAppearance;
|
||||
use codex_app_server_protocol::ThreadSectionMoveParams;
|
||||
@@ -463,6 +469,7 @@ pub(crate) struct ThreadRequestProcessor {
|
||||
pub(super) state_db: Option<StateDbHandle>,
|
||||
pub(super) log_db: Option<LogDbLayer>,
|
||||
pub(super) background_tasks: TaskTracker,
|
||||
pub(super) sample_responses: Arc<crate::thread_sample::SampleResponseRegistry>,
|
||||
pub(super) skills_watcher: Arc<SkillsWatcher>,
|
||||
pub(super) turn_cost_worker: Option<crate::turn_cost_worker::TurnCostWorkerHandle>,
|
||||
pub(super) initial_config_warnings: Arc<Vec<ConfigWarningNotification>>,
|
||||
@@ -505,6 +512,7 @@ impl ThreadRequestProcessor {
|
||||
skills_watcher: Arc<SkillsWatcher>,
|
||||
turn_cost_worker: Option<crate::turn_cost_worker::TurnCostWorkerHandle>,
|
||||
initial_config_warnings: Vec<ConfigWarningNotification>,
|
||||
sample_responses: Arc<crate::thread_sample::SampleResponseRegistry>,
|
||||
) -> Self {
|
||||
Self {
|
||||
auth_manager,
|
||||
@@ -522,6 +530,7 @@ impl ThreadRequestProcessor {
|
||||
state_db,
|
||||
log_db,
|
||||
background_tasks: TaskTracker::new(),
|
||||
sample_responses,
|
||||
skills_watcher,
|
||||
turn_cost_worker,
|
||||
initial_config_warnings: Arc::new(initial_config_warnings),
|
||||
@@ -603,6 +612,38 @@ impl ThreadRequestProcessor {
|
||||
.map(|()| None)
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_sample(
|
||||
&self,
|
||||
params: ThreadSampleParams,
|
||||
rpc_gate: Arc<ConnectionRpcGate>,
|
||||
context: RequestContext,
|
||||
) -> Result<ThreadSampleResponse, JSONRPCErrorError> {
|
||||
let id = ThreadId::from_string(¶ms.thread_id)
|
||||
.map_err(|error| invalid_request(error.to_string()))?;
|
||||
let parent = self
|
||||
.thread_manager
|
||||
.get_thread(id)
|
||||
.await
|
||||
.map_err(|_| invalid_request("thread/sample requires a loaded source"))?;
|
||||
self.config_manager
|
||||
.check_thread_model_provider(parent.config().await.as_ref())
|
||||
.await
|
||||
.map_err(|error| config_load_error(&error))?;
|
||||
generate_sample(
|
||||
SampleTask {
|
||||
manager: Arc::clone(&self.thread_manager),
|
||||
store: Arc::clone(&self.thread_store),
|
||||
parent,
|
||||
samples: Arc::clone(&self.sample_responses),
|
||||
params,
|
||||
trace: context.request_trace(),
|
||||
},
|
||||
&self.background_tasks,
|
||||
rpc_gate,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_revert(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -2887,13 +2928,17 @@ impl ThreadRequestProcessor {
|
||||
params: ThreadLoadedListParams,
|
||||
) -> Result<ThreadLoadedListResponse, JSONRPCErrorError> {
|
||||
let ThreadLoadedListParams { cursor, limit } = params;
|
||||
let mut data: Vec<String> = self
|
||||
.thread_manager
|
||||
.list_thread_ids()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|thread_id| thread_id.to_string())
|
||||
.collect();
|
||||
let mut data = Vec::new();
|
||||
for thread_id in self.thread_manager.list_thread_ids().await {
|
||||
if let Ok(thread) = self.thread_manager.get_thread(thread_id).await
|
||||
&& thread
|
||||
.thread_extension_data()
|
||||
.get::<SampleThread>()
|
||||
.is_none()
|
||||
{
|
||||
data.push(thread_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if data.is_empty() {
|
||||
return Ok(ThreadLoadedListResponse {
|
||||
|
||||
Reference in New Issue
Block a user