mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
feat(app-server): add next prompt suggestion rpc
This commit is contained in:
@@ -3898,6 +3898,33 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSuggestNextPromptParams": {
|
||||
"description": "Requests a best-effort predicted next prompt for one loaded thread.\n\nThis is an internal interactive-client helper. The request does not advance the thread or persist a new item; it asks the server to sample from the thread's current prompt-visible history.",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"description": "When true, cancel the request identified by `cancellation_token` without sampling.\n\nThis requires `cancellation_token`.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cancellationToken": {
|
||||
"description": "Optional client token used to cancel an older in-flight suggestion request.\n\nReusing a token cancels the older request before this request samples.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"description": "Thread whose current completed history should be used for prediction.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadUnarchiveParams": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
@@ -4748,6 +4775,30 @@
|
||||
"title": "Thread/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/suggestNextPrompt"
|
||||
],
|
||||
"title": "Thread/suggestNextPromptRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadSuggestNextPromptParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/suggestNextPromptRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Append raw Responses API items to the thread history without starting a user turn.",
|
||||
"properties": {
|
||||
|
||||
+68
@@ -660,6 +660,30 @@
|
||||
"title": "Thread/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/suggestNextPrompt"
|
||||
],
|
||||
"title": "Thread/suggestNextPromptRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadSuggestNextPromptParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/suggestNextPromptRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Append raw Responses API items to the thread history without starting a user turn.",
|
||||
"properties": {
|
||||
@@ -18088,6 +18112,50 @@
|
||||
"title": "ThreadStatusChangedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSuggestNextPromptParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Requests a best-effort predicted next prompt for one loaded thread.\n\nThis is an internal interactive-client helper. The request does not advance the thread or persist a new item; it asks the server to sample from the thread's current prompt-visible history.",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"description": "When true, cancel the request identified by `cancellation_token` without sampling.\n\nThis requires `cancellation_token`.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cancellationToken": {
|
||||
"description": "Optional client token used to cancel an older in-flight suggestion request.\n\nReusing a token cancels the older request before this request samples.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"description": "Thread whose current completed history should be used for prediction.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadSuggestNextPromptParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSuggestNextPromptResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Returns the predicted prompt text, or silence when no suggestion is suitable.",
|
||||
"properties": {
|
||||
"suggestion": {
|
||||
"description": "Composer-ready prompt text after server-side suppression and filtering.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadSuggestNextPromptResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadTokenUsage": {
|
||||
"properties": {
|
||||
"last": {
|
||||
|
||||
+68
@@ -1419,6 +1419,30 @@
|
||||
"title": "Thread/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/suggestNextPrompt"
|
||||
],
|
||||
"title": "Thread/suggestNextPromptRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadSuggestNextPromptParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/suggestNextPromptRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Append raw Responses API items to the thread history without starting a user turn.",
|
||||
"properties": {
|
||||
@@ -15905,6 +15929,50 @@
|
||||
"title": "ThreadStatusChangedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSuggestNextPromptParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Requests a best-effort predicted next prompt for one loaded thread.\n\nThis is an internal interactive-client helper. The request does not advance the thread or persist a new item; it asks the server to sample from the thread's current prompt-visible history.",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"description": "When true, cancel the request identified by `cancellation_token` without sampling.\n\nThis requires `cancellation_token`.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cancellationToken": {
|
||||
"description": "Optional client token used to cancel an older in-flight suggestion request.\n\nReusing a token cancels the older request before this request samples.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"description": "Thread whose current completed history should be used for prediction.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadSuggestNextPromptParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSuggestNextPromptResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Returns the predicted prompt text, or silence when no suggestion is suitable.",
|
||||
"properties": {
|
||||
"suggestion": {
|
||||
"description": "Composer-ready prompt text after server-side suppression and filtering.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadSuggestNextPromptResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadTokenUsage": {
|
||||
"properties": {
|
||||
"last": {
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Requests a best-effort predicted next prompt for one loaded thread.\n\nThis is an internal interactive-client helper. The request does not advance the thread or persist a new item; it asks the server to sample from the thread's current prompt-visible history.",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"description": "When true, cancel the request identified by `cancellation_token` without sampling.\n\nThis requires `cancellation_token`.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cancellationToken": {
|
||||
"description": "Optional client token used to cancel an older in-flight suggestion request.\n\nReusing a token cancels the older request before this request samples.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"description": "Thread whose current completed history should be used for prediction.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadSuggestNextPromptParams",
|
||||
"type": "object"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Returns the predicted prompt text, or silence when no suggestion is suitable.",
|
||||
"properties": {
|
||||
"suggestion": {
|
||||
"description": "Composer-ready prompt text after server-side suppression and filtering.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadSuggestNextPromptResponse",
|
||||
"type": "object"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+28
@@ -0,0 +1,28 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Requests a best-effort predicted next prompt for one loaded thread.
|
||||
*
|
||||
* This is an internal interactive-client helper. The request does not advance
|
||||
* the thread or persist a new item; it asks the server to sample from the
|
||||
* thread's current prompt-visible history.
|
||||
*/
|
||||
export type ThreadSuggestNextPromptParams = {
|
||||
/**
|
||||
* Thread whose current completed history should be used for prediction.
|
||||
*/
|
||||
threadId: string,
|
||||
/**
|
||||
* Optional client token used to cancel an older in-flight suggestion request.
|
||||
*
|
||||
* Reusing a token cancels the older request before this request samples.
|
||||
*/
|
||||
cancellationToken?: string | null,
|
||||
/**
|
||||
* When true, cancel the request identified by `cancellation_token` without sampling.
|
||||
*
|
||||
* This requires `cancellation_token`.
|
||||
*/
|
||||
cancel?: boolean | null, };
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Returns the predicted prompt text, or silence when no suggestion is suitable.
|
||||
*/
|
||||
export type ThreadSuggestNextPromptResponse = {
|
||||
/**
|
||||
* Composer-ready prompt text after server-side suppression and filtering.
|
||||
*/
|
||||
suggestion: string | null, };
|
||||
@@ -422,6 +422,8 @@ export type { ThreadStartSource } from "./ThreadStartSource";
|
||||
export type { ThreadStartedNotification } from "./ThreadStartedNotification";
|
||||
export type { ThreadStatus } from "./ThreadStatus";
|
||||
export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification";
|
||||
export type { ThreadSuggestNextPromptParams } from "./ThreadSuggestNextPromptParams";
|
||||
export type { ThreadSuggestNextPromptResponse } from "./ThreadSuggestNextPromptResponse";
|
||||
export type { ThreadTokenUsage } from "./ThreadTokenUsage";
|
||||
export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification";
|
||||
export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams";
|
||||
|
||||
@@ -585,6 +585,13 @@ client_request_definitions! {
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadReadResponse,
|
||||
},
|
||||
ThreadSuggestNextPrompt => "thread/suggestNextPrompt" {
|
||||
params: v2::ThreadSuggestNextPromptParams,
|
||||
// Suggestions do not mutate thread state, and cancellation requests must not queue
|
||||
// behind the in-flight sampling request they are trying to stop.
|
||||
serialization: None,
|
||||
response: v2::ThreadSuggestNextPromptResponse,
|
||||
},
|
||||
#[experimental("thread/turns/list")]
|
||||
ThreadTurnsList => "thread/turns/list" {
|
||||
params: v2::ThreadTurnsListParams,
|
||||
|
||||
@@ -1168,6 +1168,38 @@ pub struct ThreadReadResponse {
|
||||
pub thread: Thread,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// Requests a best-effort predicted next prompt for one loaded thread.
|
||||
///
|
||||
/// This is an internal interactive-client helper. The request does not advance
|
||||
/// the thread or persist a new item; it asks the server to sample from the
|
||||
/// thread's current prompt-visible history.
|
||||
pub struct ThreadSuggestNextPromptParams {
|
||||
/// Thread whose current completed history should be used for prediction.
|
||||
pub thread_id: String,
|
||||
/// Optional client token used to cancel an older in-flight suggestion request.
|
||||
///
|
||||
/// Reusing a token cancels the older request before this request samples.
|
||||
#[ts(optional = nullable)]
|
||||
pub cancellation_token: Option<String>,
|
||||
/// When true, cancel the request identified by `cancellation_token` without sampling.
|
||||
///
|
||||
/// This requires `cancellation_token`.
|
||||
#[ts(optional = nullable)]
|
||||
pub cancel: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// Returns the predicted prompt text, or silence when no suggestion is suitable.
|
||||
pub struct ThreadSuggestNextPromptResponse {
|
||||
/// Composer-ready prompt text after server-side suppression and filtering.
|
||||
pub suggestion: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -137,6 +137,7 @@ Example with notification opt-out:
|
||||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate control/spawn parent is known.
|
||||
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
||||
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
- `thread/suggestNextPrompt` — sample a filtered next-prompt suggestion for a loaded thread without starting a turn. This is an internal TUI helper, not a general conversation mutation API.
|
||||
- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
|
||||
- `thread/turns/items/list` — experimental; reserved for paging full items for one turn. The API shape is present, but app-server currently returns an unsupported-method JSON-RPC error.
|
||||
- `thread/metadata/update` — patch stored thread metadata in sqlite; currently supports updating persisted `gitInfo` fields and returns the refreshed `thread`.
|
||||
|
||||
@@ -1103,6 +1103,9 @@ impl MessageProcessor {
|
||||
ClientRequest::ThreadRead { params, .. } => {
|
||||
self.thread_processor.thread_read(params).await
|
||||
}
|
||||
ClientRequest::ThreadSuggestNextPrompt { params, .. } => {
|
||||
self.turn_processor.thread_suggest_next_prompt(params).await
|
||||
}
|
||||
ClientRequest::ThreadTurnsList { params, .. } => {
|
||||
self.thread_processor.thread_turns_list(params).await
|
||||
}
|
||||
|
||||
@@ -234,6 +234,8 @@ use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::ThreadSuggestNextPromptParams;
|
||||
use codex_app_server_protocol::ThreadSuggestNextPromptResponse;
|
||||
use codex_app_server_protocol::ThreadTurnsItemsListParams;
|
||||
use codex_app_server_protocol::ThreadTurnsListParams;
|
||||
use codex_app_server_protocol::ThreadTurnsListResponse;
|
||||
|
||||
@@ -12,6 +12,7 @@ pub(crate) struct TurnRequestProcessor {
|
||||
config: Arc<Config>,
|
||||
config_manager: ConfigManager,
|
||||
pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
|
||||
pending_next_prompt_suggestions: Arc<Mutex<HashMap<String, Arc<CancellationToken>>>>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
thread_list_state_permit: Arc<Semaphore>,
|
||||
@@ -96,6 +97,7 @@ impl TurnRequestProcessor {
|
||||
config,
|
||||
config_manager,
|
||||
pending_thread_unloads,
|
||||
pending_next_prompt_suggestions: Arc::new(Mutex::new(HashMap::new())),
|
||||
thread_state_manager,
|
||||
thread_watch_manager,
|
||||
thread_list_state_permit,
|
||||
@@ -139,6 +141,21 @@ impl TurnRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
/// Handles the internal next-prompt RPC without mutating thread state.
|
||||
///
|
||||
/// The loaded thread owns sampling and filtering. This processor maps the
|
||||
/// typed RPC boundary onto that thread API, tracks optional client
|
||||
/// cancellation tokens, and converts unexpected core errors into the
|
||||
/// app-server error channel.
|
||||
pub(crate) async fn thread_suggest_next_prompt(
|
||||
&self,
|
||||
params: ThreadSuggestNextPromptParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_suggest_next_prompt_inner(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn turn_steer(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
@@ -700,6 +717,86 @@ impl TurnRequestProcessor {
|
||||
Ok(ThreadSettingsUpdateResponse {})
|
||||
}
|
||||
|
||||
async fn thread_suggest_next_prompt_inner(
|
||||
&self,
|
||||
params: ThreadSuggestNextPromptParams,
|
||||
) -> Result<ThreadSuggestNextPromptResponse, JSONRPCErrorError> {
|
||||
let ThreadSuggestNextPromptParams {
|
||||
thread_id,
|
||||
cancellation_token,
|
||||
cancel,
|
||||
} = params;
|
||||
let cancel = cancel.unwrap_or_default();
|
||||
let suggestion_cancellation = self
|
||||
.next_prompt_suggestion_cancellation(cancellation_token.as_deref(), cancel)
|
||||
.await?;
|
||||
let Some(suggestion_cancellation) = suggestion_cancellation else {
|
||||
return Ok(ThreadSuggestNextPromptResponse { suggestion: None });
|
||||
};
|
||||
|
||||
let result = async {
|
||||
let (_, thread) = self.load_thread(&thread_id).await?;
|
||||
thread
|
||||
.suggest_next_prompt(suggestion_cancellation.as_ref().clone())
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to suggest next prompt: {err}")))
|
||||
}
|
||||
.await;
|
||||
self.clear_next_prompt_suggestion_cancellation(
|
||||
cancellation_token.as_deref(),
|
||||
&suggestion_cancellation,
|
||||
)
|
||||
.await;
|
||||
let suggestion = result?;
|
||||
Ok(ThreadSuggestNextPromptResponse { suggestion })
|
||||
}
|
||||
|
||||
async fn next_prompt_suggestion_cancellation(
|
||||
&self,
|
||||
cancellation_token: Option<&str>,
|
||||
cancel: bool,
|
||||
) -> Result<Option<Arc<CancellationToken>>, JSONRPCErrorError> {
|
||||
let Some(cancellation_token) = cancellation_token else {
|
||||
if cancel {
|
||||
return Err(invalid_request(
|
||||
"cancellationToken is required when cancel is true",
|
||||
));
|
||||
}
|
||||
return Ok(Some(Arc::new(CancellationToken::new())));
|
||||
};
|
||||
|
||||
let mut pending_next_prompt_suggestions = self.pending_next_prompt_suggestions.lock().await;
|
||||
if let Some(existing) = pending_next_prompt_suggestions.remove(cancellation_token) {
|
||||
existing.cancel();
|
||||
}
|
||||
if cancel {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let cancellation = Arc::new(CancellationToken::new());
|
||||
pending_next_prompt_suggestions
|
||||
.insert(cancellation_token.to_string(), Arc::clone(&cancellation));
|
||||
Ok(Some(cancellation))
|
||||
}
|
||||
|
||||
async fn clear_next_prompt_suggestion_cancellation(
|
||||
&self,
|
||||
cancellation_token: Option<&str>,
|
||||
suggestion_cancellation: &Arc<CancellationToken>,
|
||||
) {
|
||||
let Some(cancellation_token) = cancellation_token else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut pending_next_prompt_suggestions = self.pending_next_prompt_suggestions.lock().await;
|
||||
if pending_next_prompt_suggestions
|
||||
.get(cancellation_token)
|
||||
.is_some_and(|current| Arc::ptr_eq(current, suggestion_cancellation))
|
||||
{
|
||||
pending_next_prompt_suggestions.remove(cancellation_token);
|
||||
}
|
||||
}
|
||||
|
||||
async fn thread_inject_items_response_inner(
|
||||
&self,
|
||||
params: ThreadInjectItemsParams,
|
||||
|
||||
@@ -63,6 +63,7 @@ mod thread_settings_update;
|
||||
mod thread_shell_command;
|
||||
mod thread_start;
|
||||
mod thread_status;
|
||||
mod thread_suggest_next_prompt;
|
||||
mod thread_unarchive;
|
||||
mod thread_unsubscribe;
|
||||
mod turn_interrupt;
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadInjectItemsParams;
|
||||
use codex_app_server_protocol::ThreadInjectItemsResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadSuggestNextPromptResponse;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use core_test_support::responses;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::sleep;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(/*secs*/ 10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_suggest_next_prompt_samples_loaded_history_without_tools() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "run the tests"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let response_mock = responses::mount_sse_once(&server, body).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
inject_suggestion_history(&mut mcp, &thread.id).await?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/suggestNextPrompt",
|
||||
Some(json!({ "threadId": thread.id })),
|
||||
)
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response = to_response::<ThreadSuggestNextPromptResponse>(response)?;
|
||||
|
||||
assert_eq!(response.suggestion.as_deref(), Some("run the tests"));
|
||||
let request = response_mock.single_request();
|
||||
assert_eq!(request.body_json()["tools"], json!([]));
|
||||
assert!(
|
||||
request
|
||||
.message_input_texts("user")
|
||||
.last()
|
||||
.is_some_and(|text| text.contains("[SUGGESTION MODE:")),
|
||||
"suggestion prompt should be appended as the final user message"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_suggest_next_prompt_cancel_stops_in_flight_sampling() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "run the tests"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let response_mock = responses::mount_response_once(
|
||||
&server,
|
||||
responses::sse_response(body).set_delay(std::time::Duration::from_secs(/*secs*/ 5)),
|
||||
)
|
||||
.await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
inject_suggestion_history(&mut mcp, &thread.id).await?;
|
||||
|
||||
let suggestion_req = mcp
|
||||
.send_raw_request(
|
||||
"thread/suggestNextPrompt",
|
||||
Some(json!({
|
||||
"threadId": thread.id,
|
||||
"cancellationToken": "next-prompt-suggestion-test",
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
wait_for_request_count(&response_mock, /*expected*/ 1).await?;
|
||||
let cancel_req = mcp
|
||||
.send_raw_request(
|
||||
"thread/suggestNextPrompt",
|
||||
Some(json!({
|
||||
"threadId": thread.id,
|
||||
"cancellationToken": "next-prompt-suggestion-test",
|
||||
"cancel": true,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cancel_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(cancel_req)),
|
||||
)
|
||||
.await??;
|
||||
let cancel_resp = to_response::<ThreadSuggestNextPromptResponse>(cancel_resp)?;
|
||||
assert_eq!(cancel_resp.suggestion, None);
|
||||
|
||||
let suggestion_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(suggestion_req)),
|
||||
)
|
||||
.await??;
|
||||
let suggestion_resp = to_response::<ThreadSuggestNextPromptResponse>(suggestion_resp)?;
|
||||
assert_eq!(suggestion_resp.suggestion, None);
|
||||
assert_eq!(response_mock.requests().len(), /*expected*/ 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn inject_suggestion_history(mcp: &mut McpProcess, thread_id: &str) -> Result<()> {
|
||||
let items = vec![
|
||||
message("user", "fix the bug"),
|
||||
message("assistant", "I fixed it"),
|
||||
message("user", "continue"),
|
||||
message("assistant", "The change is ready"),
|
||||
];
|
||||
let inject_req = mcp
|
||||
.send_thread_inject_items_request(ThreadInjectItemsParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
items: items
|
||||
.into_iter()
|
||||
.map(serde_json::to_value)
|
||||
.collect::<serde_json::Result<Vec<_>>>()?,
|
||||
})
|
||||
.await?;
|
||||
let inject_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(inject_req)),
|
||||
)
|
||||
.await??;
|
||||
let _response: ThreadInjectItemsResponse =
|
||||
to_response::<ThreadInjectItemsResponse>(inject_resp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_request_count(
|
||||
response_mock: &core_test_support::responses::ResponseMock,
|
||||
expected: usize,
|
||||
) -> Result<()> {
|
||||
timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
if response_mock.requests().len() >= expected {
|
||||
return;
|
||||
}
|
||||
sleep(std::time::Duration::from_millis(/*millis*/ 10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn message(role: &str, text: &str) -> ResponseItem {
|
||||
let content = match role {
|
||||
"assistant" => ContentItem::OutputText {
|
||||
text: text.to_string(),
|
||||
},
|
||||
_ => ContentItem::InputText {
|
||||
text: text.to_string(),
|
||||
},
|
||||
};
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: role.to_string(),
|
||||
content: vec![content],
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
config_toml,
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user