feat(app-server): add next prompt suggestion rpc

This commit is contained in:
Felipe Coury
2026-06-03 16:02:36 -03:00
parent 768d0f1489
commit 2f8f0a149a
17 changed files with 653 additions and 1 deletions
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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"
}
@@ -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
@@ -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, };
@@ -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/")]
+1
View File
@@ -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
"#
),
)
}