mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Merge remote-tracking branch 'origin/main' into codex/pr23585-merge-fix
# Conflicts: # codex-rs/core/src/session/input_queue.rs # codex-rs/core/src/session/mod.rs # codex-rs/core/src/session/turn.rs
This commit is contained in:
Generated
+23
-1
@@ -1859,6 +1859,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
@@ -1927,6 +1928,7 @@ dependencies = [
|
||||
"codex-utils-cli",
|
||||
"codex-utils-json-to-toml",
|
||||
"codex-utils-pty",
|
||||
"codex-web-search-extension",
|
||||
"core_test_support",
|
||||
"flate2",
|
||||
"futures",
|
||||
@@ -2968,6 +2970,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"codex-core",
|
||||
"codex-extension-api",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-state",
|
||||
"codex-tools",
|
||||
@@ -3787,7 +3790,6 @@ dependencies = [
|
||||
"codex-app-server-client",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-chatgpt",
|
||||
"codex-cli",
|
||||
"codex-cloud-requirements",
|
||||
"codex-config",
|
||||
@@ -4116,6 +4118,26 @@ dependencies = [
|
||||
"v8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-web-search-extension"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-api",
|
||||
"codex-core",
|
||||
"codex-extension-api",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
"codex-protocol",
|
||||
"codex-tools",
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-windows-sandbox"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -48,6 +48,7 @@ members = [
|
||||
"ext/goal",
|
||||
"ext/guardian",
|
||||
"ext/memories",
|
||||
"ext/web-search",
|
||||
"external-agent-migration",
|
||||
"external-agent-sessions",
|
||||
"keyring-store",
|
||||
@@ -180,6 +181,7 @@ codex-lmstudio = { path = "lmstudio" }
|
||||
codex-login = { path = "login" }
|
||||
codex-message-history = { path = "message-history" }
|
||||
codex-memories-extension = { path = "ext/memories" }
|
||||
codex-web-search-extension = { path = "ext/web-search" }
|
||||
codex-memories-read = { path = "memories/read" }
|
||||
codex-memories-write = { path = "memories/write" }
|
||||
codex-mcp = { path = "codex-mcp" }
|
||||
|
||||
@@ -401,6 +401,7 @@ fn sample_turn_steer_request(
|
||||
},
|
||||
],
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ fn sample_turn_steer_request() -> ClientRequest {
|
||||
expected_turn_id: "turn-1".to_string(),
|
||||
input: Vec::new(),
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,28 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AdditionalContextEntry": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"$ref": "#/definitions/AdditionalContextKind"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalContextKind": {
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"application"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ApprovalsReviewer": {
|
||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
|
||||
"enum": [
|
||||
|
||||
+22
@@ -5768,6 +5768,28 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AdditionalContextEntry": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"$ref": "#/definitions/v2/AdditionalContextKind"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalContextKind": {
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"application"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
|
||||
+22
@@ -164,6 +164,28 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AdditionalContextEntry": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"$ref": "#/definitions/AdditionalContextKind"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalContextKind": {
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"application"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
|
||||
@@ -5,6 +5,28 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"AdditionalContextEntry": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"$ref": "#/definitions/AdditionalContextKind"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalContextKind": {
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"application"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ApprovalsReviewer": {
|
||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
|
||||
"enum": [
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AdditionalContextEntry": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"$ref": "#/definitions/AdditionalContextKind"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalContextKind": {
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"application"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// 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.
|
||||
import type { AdditionalContextKind } from "./AdditionalContextKind";
|
||||
|
||||
export type AdditionalContextEntry = { value: string, kind: AdditionalContextKind, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// 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.
|
||||
|
||||
export type AdditionalContextKind = "untrusted" | "application";
|
||||
@@ -7,6 +7,8 @@ export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { ActivePermissionProfile } from "./ActivePermissionProfile";
|
||||
export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType";
|
||||
export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus";
|
||||
export type { AdditionalContextEntry } from "./AdditionalContextEntry";
|
||||
export type { AdditionalContextKind } from "./AdditionalContextKind";
|
||||
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
|
||||
export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
|
||||
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
|
||||
|
||||
@@ -3428,6 +3428,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
|
||||
thread_id: "thread_123".to_string(),
|
||||
input: vec![],
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
environments: None,
|
||||
cwd: None,
|
||||
runtime_workspace_roots: None,
|
||||
|
||||
@@ -41,6 +41,23 @@ pub struct TurnEnvironmentParams {
|
||||
pub cwd: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AdditionalContextKind {
|
||||
Untrusted,
|
||||
Application,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AdditionalContextEntry {
|
||||
pub value: String,
|
||||
pub kind: AdditionalContextKind,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
|
||||
)]
|
||||
@@ -53,6 +70,10 @@ pub struct TurnStartParams {
|
||||
#[experimental("turn/start.responsesapiClientMetadata")]
|
||||
#[ts(optional = nullable)]
|
||||
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
/// Optional client-provided context fragments keyed by an opaque source identifier.
|
||||
#[experimental("turn/start.additionalContext")]
|
||||
#[ts(optional = nullable)]
|
||||
pub additional_context: Option<HashMap<String, AdditionalContextEntry>>,
|
||||
/// Optional turn-scoped environments.
|
||||
///
|
||||
/// Omitted uses the thread sticky environments. Empty disables
|
||||
@@ -141,6 +162,10 @@ pub struct TurnSteerParams {
|
||||
#[experimental("turn/steer.responsesapiClientMetadata")]
|
||||
#[ts(optional = nullable)]
|
||||
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
/// Optional client-provided context fragments keyed by an opaque source identifier.
|
||||
#[experimental("turn/steer.additionalContext")]
|
||||
#[ts(optional = nullable)]
|
||||
pub additional_context: Option<HashMap<String, AdditionalContextEntry>>,
|
||||
/// Required active turn id precondition. The request fails when it does not
|
||||
/// match the currently active turn.
|
||||
pub expected_turn_id: String,
|
||||
|
||||
@@ -15,13 +15,21 @@ use codex_app_server_protocol::JSONRPCMessage;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::watch;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
use tokio::time::timeout;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::info;
|
||||
use tracing::warn;
|
||||
|
||||
const REMOTE_CONTROL_CLIENT_IDLE_TIMEOUT: Duration = Duration::from_secs(10 * 60);
|
||||
pub(crate) const REMOTE_CONTROL_IDLE_SWEEP_INTERVAL: Duration = Duration::from_secs(30);
|
||||
#[cfg(not(test))]
|
||||
const REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
#[cfg(test)]
|
||||
const REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT: Duration = Duration::from_millis(10);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Stopped;
|
||||
@@ -138,9 +146,6 @@ impl ClientTracker {
|
||||
|
||||
if let Some(connection_id) = self.clients.get_mut(&client_key).map(|client| {
|
||||
client.last_activity_at = Instant::now();
|
||||
if let Some(seq_id) = seq_id {
|
||||
client.last_inbound_seq_id = Some(seq_id);
|
||||
}
|
||||
client.connection_id
|
||||
}) {
|
||||
self.send_transport_event(TransportEvent::IncomingMessage {
|
||||
@@ -148,6 +153,7 @@ impl ClientTracker {
|
||||
message,
|
||||
})
|
||||
.await?;
|
||||
self.record_inbound_message_delivery(&client_key, seq_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -177,23 +183,37 @@ impl ClientTracker {
|
||||
disconnect_token.clone(),
|
||||
));
|
||||
self.clients.insert(
|
||||
client_key,
|
||||
client_key.clone(),
|
||||
ClientState {
|
||||
connection_id,
|
||||
disconnect_token,
|
||||
last_activity_at: Instant::now(),
|
||||
last_inbound_seq_id: if is_legacy_stream_id { None } else { seq_id },
|
||||
last_inbound_seq_id: None,
|
||||
status_tx,
|
||||
},
|
||||
);
|
||||
if is_legacy_stream_id {
|
||||
self.legacy_stream_ids.insert(client_id.clone(), stream_id);
|
||||
}
|
||||
self.send_transport_event(TransportEvent::IncomingMessage {
|
||||
connection_id,
|
||||
message,
|
||||
})
|
||||
.await
|
||||
if let Err(err) = self
|
||||
.send_transport_event(TransportEvent::IncomingMessage {
|
||||
connection_id,
|
||||
message,
|
||||
})
|
||||
.await
|
||||
{
|
||||
if let Some(client) = self.remove_client(&client_key) {
|
||||
client.disconnect_token.cancel();
|
||||
// The initialize send already timed out on this queue; preserve close
|
||||
// delivery without blocking reconnect on the same backpressure.
|
||||
drop(self.spawn_connection_closed(client.connection_id));
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
if !is_legacy_stream_id {
|
||||
self.record_inbound_message_delivery(&client_key, seq_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ClientEvent::ClientMessageChunk { .. } | ClientEvent::Ack { .. } => Ok(()),
|
||||
ClientEvent::Ping => {
|
||||
@@ -290,16 +310,9 @@ impl ClientTracker {
|
||||
&mut self,
|
||||
client_key: &(ClientId, StreamId),
|
||||
) -> Result<(), Stopped> {
|
||||
let Some(client) = self.clients.remove(client_key) else {
|
||||
let Some(client) = self.remove_client(client_key) else {
|
||||
return Ok(());
|
||||
};
|
||||
if self
|
||||
.legacy_stream_ids
|
||||
.get(&client_key.0)
|
||||
.is_some_and(|stream_id| stream_id == &client_key.1)
|
||||
{
|
||||
self.legacy_stream_ids.remove(&client_key.0);
|
||||
}
|
||||
client.disconnect_token.cancel();
|
||||
self.send_transport_event(TransportEvent::ConnectionClosed {
|
||||
connection_id: client.connection_id,
|
||||
@@ -307,11 +320,109 @@ impl ClientTracker {
|
||||
.await
|
||||
}
|
||||
|
||||
fn remove_client(&mut self, client_key: &(ClientId, StreamId)) -> Option<ClientState> {
|
||||
let client = self.clients.remove(client_key)?;
|
||||
if self
|
||||
.legacy_stream_ids
|
||||
.get(&client_key.0)
|
||||
.is_some_and(|stream_id| stream_id == &client_key.1)
|
||||
{
|
||||
self.legacy_stream_ids.remove(&client_key.0);
|
||||
}
|
||||
Some(client)
|
||||
}
|
||||
|
||||
async fn send_transport_event(&self, event: TransportEvent) -> Result<(), Stopped> {
|
||||
self.transport_event_tx
|
||||
.send(event)
|
||||
.await
|
||||
.map_err(|_| Stopped)
|
||||
let event = match event {
|
||||
TransportEvent::ConnectionClosed { connection_id } => {
|
||||
return self.send_connection_closed(connection_id).await;
|
||||
}
|
||||
event => event,
|
||||
};
|
||||
|
||||
let event_name = transport_event_name(&event);
|
||||
match timeout(
|
||||
REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT,
|
||||
self.transport_event_tx.send(event),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Ok(Err(_)) => {
|
||||
warn!(
|
||||
transport_event = event_name,
|
||||
"remote control transport event receiver dropped"
|
||||
);
|
||||
Err(Stopped)
|
||||
}
|
||||
Err(_) => {
|
||||
warn!(
|
||||
transport_event = event_name,
|
||||
timeout = ?REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT,
|
||||
"timed out forwarding remote control transport event"
|
||||
);
|
||||
Err(Stopped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_inbound_message_delivery(
|
||||
&mut self,
|
||||
client_key: &(ClientId, StreamId),
|
||||
seq_id: Option<u64>,
|
||||
) {
|
||||
// Timed forwarding can fail, so only dedupe retries after app-server receives it.
|
||||
if let Some(seq_id) = seq_id
|
||||
&& let Some(client) = self.clients.get_mut(client_key)
|
||||
{
|
||||
client.last_inbound_seq_id = Some(seq_id);
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_connection_closed(&self, connection_id: ConnectionId) -> Result<(), Stopped> {
|
||||
// Worker shutdown can abort the caller; detach the cleanup event before awaiting it.
|
||||
match self.spawn_connection_closed(connection_id).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
transport_event = "connection_closed",
|
||||
?err,
|
||||
"remote control transport event forwarding task failed"
|
||||
);
|
||||
Err(Stopped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_connection_closed(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
) -> JoinHandle<Result<(), Stopped>> {
|
||||
info!(
|
||||
connection_id = ?connection_id,
|
||||
"forwarding remote control connection closed transport event"
|
||||
);
|
||||
let transport_event_tx = self.transport_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
transport_event_tx
|
||||
.send(TransportEvent::ConnectionClosed { connection_id })
|
||||
.await
|
||||
.map_err(|_| {
|
||||
warn!(
|
||||
transport_event = "connection_closed",
|
||||
"remote control transport event receiver dropped"
|
||||
);
|
||||
Stopped
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn transport_event_name(event: &TransportEvent) -> &'static str {
|
||||
match event {
|
||||
TransportEvent::ConnectionOpened { .. } => "connection_opened",
|
||||
TransportEvent::ConnectionClosed { .. } => "connection_closed",
|
||||
TransportEvent::IncomingMessage { .. } => "incoming_message",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +481,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn initialized_notification() -> JSONRPCMessage {
|
||||
JSONRPCMessage::Notification(codex_app_server_protocol::JSONRPCNotification {
|
||||
method: "initialized".to_string(),
|
||||
params: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancelled_outbound_task_emits_connection_closed() {
|
||||
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
@@ -487,6 +605,256 @@ mod tests {
|
||||
.expect("shutdown should not hang on blocked server forwarding");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_close_transport_event_send_times_out_when_queue_stays_full() {
|
||||
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let (transport_event_tx, _transport_event_rx) = mpsc::channel(1);
|
||||
let shutdown_token = CancellationToken::new();
|
||||
let client_tracker =
|
||||
ClientTracker::new(server_event_tx, transport_event_tx.clone(), &shutdown_token);
|
||||
|
||||
transport_event_tx
|
||||
.send(TransportEvent::ConnectionClosed {
|
||||
connection_id: next_connection_id(),
|
||||
})
|
||||
.await
|
||||
.expect("transport event queue should accept prefill");
|
||||
|
||||
let send_result = client_tracker
|
||||
.send_transport_event(TransportEvent::IncomingMessage {
|
||||
connection_id: next_connection_id(),
|
||||
message: initialized_notification(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(send_result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn incoming_message_timeout_does_not_advance_seq_id() {
|
||||
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(2);
|
||||
let shutdown_token = CancellationToken::new();
|
||||
let mut client_tracker =
|
||||
ClientTracker::new(server_event_tx, transport_event_tx.clone(), &shutdown_token);
|
||||
|
||||
client_tracker
|
||||
.handle_message(initialize_envelope_with_stream_id(
|
||||
"client-1",
|
||||
Some("stream-1"),
|
||||
))
|
||||
.await
|
||||
.expect("initialize should open client");
|
||||
let connection_id = match transport_event_rx.recv().await.expect("open event") {
|
||||
TransportEvent::ConnectionOpened { connection_id, .. } => connection_id,
|
||||
other => panic!("expected connection opened, got {other:?}"),
|
||||
};
|
||||
let _ = transport_event_rx.recv().await.expect("initialize event");
|
||||
|
||||
for _ in 0..2 {
|
||||
transport_event_tx
|
||||
.send(TransportEvent::ConnectionClosed {
|
||||
connection_id: next_connection_id(),
|
||||
})
|
||||
.await
|
||||
.expect("transport event queue should accept prefill");
|
||||
}
|
||||
|
||||
let retry_envelope = ClientEnvelope {
|
||||
event: ClientEvent::ClientMessage {
|
||||
message: initialized_notification(),
|
||||
},
|
||||
client_id: ClientId("client-1".to_string()),
|
||||
stream_id: Some(StreamId("stream-1".to_string())),
|
||||
seq_id: Some(1),
|
||||
cursor: None,
|
||||
};
|
||||
assert!(
|
||||
client_tracker
|
||||
.handle_message(retry_envelope.clone())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
for _ in 0..2 {
|
||||
let _ = transport_event_rx.recv().await.expect("prefilled event");
|
||||
}
|
||||
|
||||
client_tracker
|
||||
.handle_message(retry_envelope)
|
||||
.await
|
||||
.expect("retry should forward after timeout");
|
||||
match transport_event_rx.recv().await.expect("retried event") {
|
||||
TransportEvent::IncomingMessage {
|
||||
connection_id: queued_connection_id,
|
||||
..
|
||||
} => assert_eq!(queued_connection_id, connection_id),
|
||||
other => panic!("expected incoming message, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_timeout_closes_open_connection() {
|
||||
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(1);
|
||||
let shutdown_token = CancellationToken::new();
|
||||
let client_tracker =
|
||||
ClientTracker::new(server_event_tx, transport_event_tx, &shutdown_token);
|
||||
let mut handle_message = tokio::spawn(async move {
|
||||
let mut client_tracker = client_tracker;
|
||||
client_tracker
|
||||
.handle_message(initialize_envelope_with_stream_id(
|
||||
"client-1",
|
||||
Some("stream-1"),
|
||||
))
|
||||
.await
|
||||
});
|
||||
|
||||
assert!(
|
||||
timeout(Duration::from_millis(50), &mut handle_message)
|
||||
.await
|
||||
.expect("initialize timeout rollback should not wait for close delivery")
|
||||
.expect("handle message task should not panic")
|
||||
.is_err()
|
||||
);
|
||||
let connection_id = match transport_event_rx.recv().await.expect("open event") {
|
||||
TransportEvent::ConnectionOpened { connection_id, .. } => connection_id,
|
||||
other => panic!("expected connection opened, got {other:?}"),
|
||||
};
|
||||
|
||||
match transport_event_rx.recv().await.expect("close event") {
|
||||
TransportEvent::ConnectionClosed {
|
||||
connection_id: closed_connection_id,
|
||||
} => assert_eq!(closed_connection_id, connection_id),
|
||||
other => panic!("expected connection closed, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_client_waits_for_transport_event_queue_capacity() {
|
||||
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(2);
|
||||
let shutdown_token = CancellationToken::new();
|
||||
let mut client_tracker =
|
||||
ClientTracker::new(server_event_tx, transport_event_tx, &shutdown_token);
|
||||
|
||||
client_tracker
|
||||
.handle_message(initialize_envelope_with_stream_id(
|
||||
"client-1",
|
||||
Some("stream-1"),
|
||||
))
|
||||
.await
|
||||
.expect("initialize should open client");
|
||||
let connection_id = match transport_event_rx.recv().await.expect("open event") {
|
||||
TransportEvent::ConnectionOpened { connection_id, .. } => connection_id,
|
||||
other => panic!("expected connection opened, got {other:?}"),
|
||||
};
|
||||
let _ = transport_event_rx.recv().await.expect("initialize event");
|
||||
|
||||
for _ in 0..2 {
|
||||
client_tracker
|
||||
.transport_event_tx
|
||||
.send(TransportEvent::IncomingMessage {
|
||||
connection_id,
|
||||
message: initialized_notification(),
|
||||
})
|
||||
.await
|
||||
.expect("transport event queue should accept prefill");
|
||||
}
|
||||
|
||||
let client_key = (
|
||||
ClientId("client-1".to_string()),
|
||||
StreamId("stream-1".to_string()),
|
||||
);
|
||||
let close_client = client_tracker.close_client(&client_key);
|
||||
tokio::pin!(close_client);
|
||||
assert!(
|
||||
timeout(Duration::from_millis(20), &mut close_client)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
for _ in 0..2 {
|
||||
match transport_event_rx.recv().await.expect("prefilled event") {
|
||||
TransportEvent::IncomingMessage {
|
||||
connection_id: queued_connection_id,
|
||||
..
|
||||
} => assert_eq!(queued_connection_id, connection_id),
|
||||
other => panic!("expected incoming message, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
close_client
|
||||
.await
|
||||
.expect("close should forward after queue drains");
|
||||
match transport_event_rx.recv().await.expect("close event") {
|
||||
TransportEvent::ConnectionClosed {
|
||||
connection_id: closed_connection_id,
|
||||
} => assert_eq!(closed_connection_id, connection_id),
|
||||
other => panic!("expected connection closed, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_client_keeps_forwarding_after_caller_is_aborted() {
|
||||
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(2);
|
||||
let shutdown_token = CancellationToken::new();
|
||||
let mut client_tracker =
|
||||
ClientTracker::new(server_event_tx, transport_event_tx, &shutdown_token);
|
||||
|
||||
client_tracker
|
||||
.handle_message(initialize_envelope_with_stream_id(
|
||||
"client-1",
|
||||
Some("stream-1"),
|
||||
))
|
||||
.await
|
||||
.expect("initialize should open client");
|
||||
let connection_id = match transport_event_rx.recv().await.expect("open event") {
|
||||
TransportEvent::ConnectionOpened { connection_id, .. } => connection_id,
|
||||
other => panic!("expected connection opened, got {other:?}"),
|
||||
};
|
||||
let _ = transport_event_rx.recv().await.expect("initialize event");
|
||||
|
||||
for _ in 0..2 {
|
||||
client_tracker
|
||||
.transport_event_tx
|
||||
.send(TransportEvent::IncomingMessage {
|
||||
connection_id,
|
||||
message: initialized_notification(),
|
||||
})
|
||||
.await
|
||||
.expect("transport event queue should accept prefill");
|
||||
}
|
||||
|
||||
let client_key = (
|
||||
ClientId("client-1".to_string()),
|
||||
StreamId("stream-1".to_string()),
|
||||
);
|
||||
let mut close_client =
|
||||
tokio::spawn(async move { client_tracker.close_client(&client_key).await });
|
||||
assert!(
|
||||
timeout(Duration::from_millis(20), &mut close_client)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
close_client.abort();
|
||||
let _ = close_client.await;
|
||||
|
||||
for _ in 0..2 {
|
||||
let _ = transport_event_rx.recv().await.expect("prefilled event");
|
||||
}
|
||||
match timeout(Duration::from_secs(1), transport_event_rx.recv())
|
||||
.await
|
||||
.expect("close should be delivered")
|
||||
.expect("close event")
|
||||
{
|
||||
TransportEvent::ConnectionClosed {
|
||||
connection_id: closed_connection_id,
|
||||
} => assert_eq!(closed_connection_id, connection_id),
|
||||
other => panic!("expected connection closed, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_with_new_stream_id_opens_new_connection_for_same_client() {
|
||||
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
|
||||
@@ -19,16 +19,20 @@ use codex_app_server_protocol::RemoteControlConnectionStatus;
|
||||
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
|
||||
use codex_login::AuthManager;
|
||||
use codex_state::StateRuntime;
|
||||
use futures::FutureExt;
|
||||
use gethostname::gethostname;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::watch;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
use tracing::warn;
|
||||
|
||||
pub struct RemoteControlStartConfig {
|
||||
@@ -73,13 +77,21 @@ impl RemoteControlHandle {
|
||||
return Err(RemoteControlUnavailable);
|
||||
}
|
||||
|
||||
self.enabled_tx.send_if_modified(|state| {
|
||||
let enabled_changed = self.enabled_tx.send_if_modified(|state| {
|
||||
let changed = !*state;
|
||||
*state = true;
|
||||
changed
|
||||
});
|
||||
|
||||
let status = self.status();
|
||||
info!(
|
||||
enabled_changed,
|
||||
current_status = ?status.status,
|
||||
environment_id = ?status.environment_id,
|
||||
installation_id = %status.installation_id,
|
||||
server_name = %status.server_name,
|
||||
"remote control enable requested"
|
||||
);
|
||||
if matches!(
|
||||
status.status,
|
||||
RemoteControlConnectionStatus::Connected | RemoteControlConnectionStatus::Connecting
|
||||
@@ -91,12 +103,21 @@ impl RemoteControlHandle {
|
||||
}
|
||||
|
||||
pub fn disable(&self) -> RemoteControlStatusChangedNotification {
|
||||
self.enabled_tx.send_if_modified(|state| {
|
||||
let enabled_changed = self.enabled_tx.send_if_modified(|state| {
|
||||
let changed = *state;
|
||||
*state = false;
|
||||
changed
|
||||
});
|
||||
|
||||
let status = self.status();
|
||||
info!(
|
||||
enabled_changed,
|
||||
current_status = ?status.status,
|
||||
environment_id = ?status.environment_id,
|
||||
installation_id = %status.installation_id,
|
||||
server_name = %status.server_name,
|
||||
"remote control disable requested"
|
||||
);
|
||||
self.publish_status(RemoteControlConnectionStatus::Disabled)
|
||||
}
|
||||
|
||||
@@ -112,6 +133,7 @@ impl RemoteControlHandle {
|
||||
&self,
|
||||
connection_status: RemoteControlConnectionStatus,
|
||||
) -> RemoteControlStatusChangedNotification {
|
||||
let mut status_change = None;
|
||||
self.status_tx.send_if_modified(|status| {
|
||||
let next_status =
|
||||
remote_control_status_with_connection_status(status, connection_status);
|
||||
@@ -119,9 +141,21 @@ impl RemoteControlHandle {
|
||||
return false;
|
||||
}
|
||||
|
||||
status_change = Some((status.clone(), next_status.clone()));
|
||||
*status = next_status;
|
||||
true
|
||||
});
|
||||
if let Some((previous_status, next_status)) = status_change {
|
||||
info!(
|
||||
previous_status = ?previous_status.status,
|
||||
next_status = ?next_status.status,
|
||||
previous_environment_id = ?previous_status.environment_id,
|
||||
next_environment_id = ?next_status.environment_id,
|
||||
installation_id = %next_status.installation_id,
|
||||
server_name = %next_status.server_name,
|
||||
"remote control handle status changed"
|
||||
);
|
||||
}
|
||||
self.status()
|
||||
}
|
||||
}
|
||||
@@ -165,6 +199,8 @@ pub async fn start_remote_control(
|
||||
|
||||
let (enabled_tx, enabled_rx) = watch::channel(initial_enabled);
|
||||
let server_name = gethostname().to_string_lossy().trim().to_string();
|
||||
let remote_control_url = config.remote_control_url;
|
||||
let installation_id = config.installation_id;
|
||||
let initial_status = RemoteControlStatusChangedNotification {
|
||||
status: if initial_enabled {
|
||||
RemoteControlConnectionStatus::Connecting
|
||||
@@ -172,16 +208,35 @@ pub async fn start_remote_control(
|
||||
RemoteControlConnectionStatus::Disabled
|
||||
},
|
||||
server_name: server_name.clone(),
|
||||
installation_id: config.installation_id.clone(),
|
||||
installation_id: installation_id.clone(),
|
||||
environment_id: None,
|
||||
};
|
||||
let (status_tx, _status_rx) = watch::channel(initial_status);
|
||||
let status_publisher = RemoteControlStatusPublisher::new(status_tx.clone());
|
||||
info!(
|
||||
remote_control_url = %remote_control_url,
|
||||
installation_id = %installation_id,
|
||||
server_name = %server_name,
|
||||
state_db_available,
|
||||
initial_enabled,
|
||||
"starting app-server remote control websocket task"
|
||||
);
|
||||
let remote_control_url_for_log = remote_control_url.clone();
|
||||
let installation_id_for_log = installation_id.clone();
|
||||
let server_name_for_log = server_name.clone();
|
||||
let shutdown_token_for_log = shutdown_token.clone();
|
||||
let join_handle = tokio::spawn(async move {
|
||||
RemoteControlWebsocket::new(
|
||||
info!(
|
||||
remote_control_url = %remote_control_url_for_log,
|
||||
installation_id = %installation_id_for_log,
|
||||
server_name = %server_name_for_log,
|
||||
initial_enabled,
|
||||
"app-server remote control websocket task started"
|
||||
);
|
||||
let websocket_task = RemoteControlWebsocket::new(
|
||||
websocket::RemoteControlWebsocketConfig {
|
||||
remote_control_url: config.remote_control_url,
|
||||
installation_id: config.installation_id,
|
||||
remote_control_url,
|
||||
installation_id,
|
||||
remote_control_target,
|
||||
server_name,
|
||||
},
|
||||
@@ -194,8 +249,38 @@ pub async fn start_remote_control(
|
||||
shutdown_token,
|
||||
enabled_rx,
|
||||
)
|
||||
.run(app_server_client_name_rx)
|
||||
.await;
|
||||
.run(app_server_client_name_rx);
|
||||
match AssertUnwindSafe(websocket_task).catch_unwind().await {
|
||||
Ok(()) => {
|
||||
let shutdown_requested = shutdown_token_for_log.is_cancelled();
|
||||
if shutdown_requested {
|
||||
info!(
|
||||
remote_control_url = %remote_control_url_for_log,
|
||||
installation_id = %installation_id_for_log,
|
||||
server_name = %server_name_for_log,
|
||||
shutdown_requested,
|
||||
"app-server remote control websocket task exited"
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
remote_control_url = %remote_control_url_for_log,
|
||||
installation_id = %installation_id_for_log,
|
||||
server_name = %server_name_for_log,
|
||||
shutdown_requested,
|
||||
"app-server remote control websocket task exited without shutdown"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(panic) => {
|
||||
error!(
|
||||
remote_control_url = %remote_control_url_for_log,
|
||||
installation_id = %installation_id_for_log,
|
||||
server_name = %server_name_for_log,
|
||||
"app-server remote control websocket task panicked"
|
||||
);
|
||||
std::panic::resume_unwind(panic);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok((
|
||||
|
||||
@@ -66,6 +66,10 @@ const REMOTE_CONTROL_ACCOUNT_ID_RETRY_INTERVAL: std::time::Duration =
|
||||
std::time::Duration::from_secs(1);
|
||||
const REMOTE_CONTROL_RECONNECT_BACKOFF_CAP: std::time::Duration =
|
||||
std::time::Duration::from_secs(30);
|
||||
const REMOTE_CONTROL_WEBSOCKET_CONNECT_TIMEOUT: std::time::Duration =
|
||||
std::time::Duration::from_secs(30);
|
||||
const REMOTE_CONTROL_CONNECTION_SHUTDOWN_TIMEOUT: std::time::Duration =
|
||||
std::time::Duration::from_secs(5);
|
||||
|
||||
struct BoundedOutboundBuffer {
|
||||
buffer_by_stream: HashMap<(ClientId, StreamId), VecDeque<ServerEnvelope>>,
|
||||
@@ -179,14 +183,32 @@ impl WebsocketState {
|
||||
return ClientSegmentObservation::Dropped;
|
||||
}
|
||||
|
||||
let observation = self.client_segment_reassembler.observe(client_envelope);
|
||||
if matches!(observation, ClientSegmentObservation::Forward(_))
|
||||
&& let Some((key, seq_id)) = client_message_key
|
||||
{
|
||||
self.client_segment_reassembler.observe(client_envelope)
|
||||
}
|
||||
|
||||
fn record_client_message_delivery(
|
||||
&mut self,
|
||||
client_envelope: &ClientEnvelope,
|
||||
client_message_key: Option<((ClientId, Option<StreamId>), u64)>,
|
||||
) {
|
||||
if let Some(cursor) = client_envelope.cursor.as_deref() {
|
||||
self.subscribe_cursor = Some(cursor.to_string());
|
||||
}
|
||||
if let Some((key, seq_id)) = client_message_key {
|
||||
self.last_completed_client_chunk_seq_id_by_stream
|
||||
.insert(key, seq_id);
|
||||
}
|
||||
observation
|
||||
if let ClientEvent::Ack { segment_id } = &client_envelope.event
|
||||
&& let Some(acked_seq_id) = client_envelope.seq_id
|
||||
&& let Some(stream_id) = client_envelope.stream_id.as_ref()
|
||||
{
|
||||
self.outbound_buffer.ack(
|
||||
&client_envelope.client_id,
|
||||
stream_id,
|
||||
acked_seq_id,
|
||||
*segment_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn invalidate_client_message_stream(&mut self, client_id: &ClientId, stream_id: &StreamId) {
|
||||
@@ -255,6 +277,14 @@ enum ConnectOutcome {
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ConnectionEndReason {
|
||||
Shutdown,
|
||||
Disabled,
|
||||
EnabledWatchClosed,
|
||||
ConnectionWorkerStopped,
|
||||
}
|
||||
|
||||
pub(super) struct RemoteControlChannels {
|
||||
pub(super) transport_event_tx: mpsc::Sender<TransportEvent>,
|
||||
pub(super) status_publisher: RemoteControlStatusPublisher,
|
||||
@@ -270,7 +300,12 @@ impl RemoteControlStatusPublisher {
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
fn status(&self) -> RemoteControlStatusChangedNotification {
|
||||
self.tx.borrow().clone()
|
||||
}
|
||||
|
||||
fn publish_status(&self, connection_status: RemoteControlConnectionStatus) {
|
||||
let mut status_change = None;
|
||||
self.tx.send_if_modified(|status| {
|
||||
let next_status =
|
||||
remote_control_status_with_connection_status(status, connection_status);
|
||||
@@ -278,12 +313,25 @@ impl RemoteControlStatusPublisher {
|
||||
return false;
|
||||
}
|
||||
|
||||
status_change = Some((status.clone(), next_status.clone()));
|
||||
*status = next_status;
|
||||
true
|
||||
});
|
||||
if let Some((previous_status, next_status)) = status_change {
|
||||
info!(
|
||||
previous_status = ?previous_status.status,
|
||||
next_status = ?next_status.status,
|
||||
previous_environment_id = ?previous_status.environment_id,
|
||||
next_environment_id = ?next_status.environment_id,
|
||||
installation_id = %next_status.installation_id,
|
||||
server_name = %next_status.server_name,
|
||||
"remote control websocket status changed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn publish_environment_id(&self, environment_id: Option<String>) {
|
||||
let mut status_change = None;
|
||||
self.tx.send_if_modified(|status| {
|
||||
if status.status == RemoteControlConnectionStatus::Disabled {
|
||||
return false;
|
||||
@@ -298,9 +346,20 @@ impl RemoteControlStatusPublisher {
|
||||
return false;
|
||||
}
|
||||
|
||||
status_change = Some((status.clone(), next_status.clone()));
|
||||
*status = next_status;
|
||||
true
|
||||
});
|
||||
if let Some((previous_status, next_status)) = status_change {
|
||||
info!(
|
||||
status = ?next_status.status,
|
||||
previous_environment_id = ?previous_status.environment_id,
|
||||
next_environment_id = ?next_status.environment_id,
|
||||
installation_id = %next_status.installation_id,
|
||||
server_name = %next_status.server_name,
|
||||
"remote control websocket environment changed"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,12 +426,26 @@ impl RemoteControlWebsocket {
|
||||
mut self,
|
||||
app_server_client_name_rx: Option<oneshot::Receiver<String>>,
|
||||
) {
|
||||
info!(
|
||||
remote_control_url = %self.remote_control_url,
|
||||
installation_id = %self.installation_id,
|
||||
server_name = %self.server_name,
|
||||
initial_enabled = *self.enabled_rx.borrow(),
|
||||
"app-server remote control websocket loop started"
|
||||
);
|
||||
let app_server_client_name = match self
|
||||
.wait_for_app_server_client_name(app_server_client_name_rx)
|
||||
.await
|
||||
{
|
||||
Ok(app_server_client_name) => app_server_client_name,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
remote_control_url = %self.remote_control_url,
|
||||
installation_id = %self.installation_id,
|
||||
server_name = %self.server_name,
|
||||
shutdown_requested = self.shutdown_token.is_cancelled(),
|
||||
"app-server remote control websocket loop stopped before client name was ready"
|
||||
);
|
||||
self.client_tracker.lock().await.shutdown().await;
|
||||
return;
|
||||
}
|
||||
@@ -380,9 +453,27 @@ impl RemoteControlWebsocket {
|
||||
|
||||
loop {
|
||||
if !self.wait_until_enabled().await {
|
||||
info!(
|
||||
remote_control_url = %self.remote_control_url,
|
||||
installation_id = %self.installation_id,
|
||||
server_name = %self.server_name,
|
||||
shutdown_requested = self.shutdown_token.is_cancelled(),
|
||||
current_status = ?self.status_publisher.status().status,
|
||||
"app-server remote control websocket loop exiting while waiting for enablement"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let status = self.status_publisher.status();
|
||||
info!(
|
||||
remote_control_url = %self.remote_control_url,
|
||||
installation_id = %self.installation_id,
|
||||
server_name = %self.server_name,
|
||||
reconnect_attempt = self.reconnect_attempt.saturating_add(1),
|
||||
current_status = ?status.status,
|
||||
environment_id = ?status.environment_id,
|
||||
"starting app-server remote control websocket connection cycle"
|
||||
);
|
||||
let shutdown_token = self.shutdown_token.child_token();
|
||||
let websocket_connection = match self
|
||||
.connect(&shutdown_token, app_server_client_name.as_deref())
|
||||
@@ -397,11 +488,30 @@ impl RemoteControlWebsocket {
|
||||
ConnectOutcome::Shutdown => break,
|
||||
};
|
||||
|
||||
self.run_connection(websocket_connection, shutdown_token)
|
||||
let connection_end_reason = self
|
||||
.run_connection(websocket_connection, shutdown_token)
|
||||
.await;
|
||||
let status = self.status_publisher.status();
|
||||
info!(
|
||||
remote_control_url = %self.remote_control_url,
|
||||
installation_id = %self.installation_id,
|
||||
server_name = %self.server_name,
|
||||
connection_end_reason = ?connection_end_reason,
|
||||
current_status = ?status.status,
|
||||
environment_id = ?status.environment_id,
|
||||
enabled = *self.enabled_rx.borrow(),
|
||||
"app-server remote control websocket connection cycle ended"
|
||||
);
|
||||
}
|
||||
|
||||
self.client_tracker.lock().await.shutdown().await;
|
||||
info!(
|
||||
remote_control_url = %self.remote_control_url,
|
||||
installation_id = %self.installation_id,
|
||||
server_name = %self.server_name,
|
||||
shutdown_requested = self.shutdown_token.is_cancelled(),
|
||||
"app-server remote control websocket loop exited"
|
||||
);
|
||||
}
|
||||
|
||||
async fn wait_for_app_server_client_name(
|
||||
@@ -462,6 +572,19 @@ impl RemoteControlWebsocket {
|
||||
|
||||
loop {
|
||||
let subscribe_cursor = self.state.lock().await.subscribe_cursor.clone();
|
||||
let enrollment = self.enrollment.as_ref();
|
||||
info!(
|
||||
websocket_url = %remote_control_target.websocket_url,
|
||||
installation_id = %self.installation_id,
|
||||
server_name = %self.server_name,
|
||||
reconnect_attempt = self.reconnect_attempt.saturating_add(1),
|
||||
has_enrollment = enrollment.is_some(),
|
||||
server_id = ?enrollment.map(|enrollment| enrollment.server_id.as_str()),
|
||||
environment_id = ?enrollment.map(|enrollment| enrollment.environment_id.as_str()),
|
||||
subscribe_cursor_present = subscribe_cursor.is_some(),
|
||||
app_server_client_name = ?app_server_client_name,
|
||||
"connecting to app-server remote control websocket"
|
||||
);
|
||||
let connect_options = RemoteControlConnectOptions {
|
||||
installation_id: &self.installation_id,
|
||||
server_name: &self.server_name,
|
||||
@@ -500,10 +623,16 @@ impl RemoteControlWebsocket {
|
||||
self.auth_recovery = self.auth_manager.unauthorized_recovery();
|
||||
self.status_publisher
|
||||
.publish_status(RemoteControlConnectionStatus::Connected);
|
||||
let enrollment = self.enrollment.as_ref();
|
||||
info!(
|
||||
"connected to app-server remote control websocket: {}, {}",
|
||||
remote_control_target.websocket_url,
|
||||
format_headers(response.headers())
|
||||
websocket_url = %remote_control_target.websocket_url,
|
||||
installation_id = %self.installation_id,
|
||||
server_name = %self.server_name,
|
||||
server_id = ?enrollment.map(|enrollment| enrollment.server_id.as_str()),
|
||||
environment_id = ?enrollment.map(|enrollment| enrollment.environment_id.as_str()),
|
||||
subscribe_cursor_present = subscribe_cursor.is_some(),
|
||||
response_headers = %format_headers(response.headers()),
|
||||
"connected to app-server remote control websocket"
|
||||
);
|
||||
return ConnectOutcome::Connected(Box::new(websocket_connection));
|
||||
}
|
||||
@@ -519,12 +648,20 @@ impl RemoteControlWebsocket {
|
||||
let reconnect_attempt = self.reconnect_attempt.saturating_add(1);
|
||||
let (reconnect_delay, reconnect_backoff_reset) =
|
||||
next_reconnect_delay(&mut self.reconnect_attempt);
|
||||
let enrollment = self.enrollment.as_ref();
|
||||
warn!(
|
||||
websocket_url = %remote_control_target.websocket_url,
|
||||
installation_id = %self.installation_id,
|
||||
server_name = %self.server_name,
|
||||
error = %err,
|
||||
error_kind = ?err.kind(),
|
||||
reconnect_attempt,
|
||||
reconnect_delay = ?reconnect_delay,
|
||||
reconnect_backoff_reset,
|
||||
has_enrollment = enrollment.is_some(),
|
||||
server_id = ?enrollment.map(|enrollment| enrollment.server_id.as_str()),
|
||||
environment_id = ?enrollment.map(|enrollment| enrollment.environment_id.as_str()),
|
||||
subscribe_cursor_present = subscribe_cursor.is_some(),
|
||||
"failed to connect to app-server remote control websocket"
|
||||
);
|
||||
if reconnect_backoff_reset {
|
||||
@@ -562,7 +699,7 @@ impl RemoteControlWebsocket {
|
||||
&self,
|
||||
websocket_connection: WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
shutdown_token: CancellationToken,
|
||||
) {
|
||||
) -> ConnectionEndReason {
|
||||
let (websocket_writer, websocket_reader) = websocket_connection.split();
|
||||
let mut join_set = tokio::task::JoinSet::new();
|
||||
|
||||
@@ -583,19 +720,48 @@ impl RemoteControlWebsocket {
|
||||
));
|
||||
|
||||
let mut enabled_rx = self.enabled_rx.clone();
|
||||
tokio::select! {
|
||||
_ = shutdown_token.cancelled() => {}
|
||||
let connection_end_reason = tokio::select! {
|
||||
_ = shutdown_token.cancelled() => ConnectionEndReason::Shutdown,
|
||||
changed = enabled_rx.wait_for(|enabled| !*enabled) => {
|
||||
if changed.is_ok() {
|
||||
self.status_publisher
|
||||
.publish_status(RemoteControlConnectionStatus::Disabled);
|
||||
ConnectionEndReason::Disabled
|
||||
} else {
|
||||
ConnectionEndReason::EnabledWatchClosed
|
||||
}
|
||||
}
|
||||
_ = join_set.join_next() => {}
|
||||
_ = join_set.join_next() => ConnectionEndReason::ConnectionWorkerStopped,
|
||||
};
|
||||
shutdown_token.cancel();
|
||||
|
||||
join_set.join_all().await;
|
||||
Self::join_connection_workers(&mut join_set, REMOTE_CONTROL_CONNECTION_SHUTDOWN_TIMEOUT)
|
||||
.await;
|
||||
connection_end_reason
|
||||
}
|
||||
|
||||
async fn join_connection_workers(
|
||||
join_set: &mut tokio::task::JoinSet<()>,
|
||||
shutdown_timeout: std::time::Duration,
|
||||
) {
|
||||
if tokio::time::timeout(shutdown_timeout, Self::drain_join_set(join_set))
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
warn!(
|
||||
shutdown_timeout = ?shutdown_timeout,
|
||||
remaining_workers = join_set.len(),
|
||||
"timed out waiting for remote control connection workers to stop; aborting"
|
||||
);
|
||||
join_set.abort_all();
|
||||
Self::drain_join_set(join_set).await;
|
||||
}
|
||||
|
||||
async fn drain_join_set(join_set: &mut tokio::task::JoinSet<()>) {
|
||||
while join_set.join_next().await.is_some() {}
|
||||
}
|
||||
|
||||
async fn run_server_writer(
|
||||
@@ -675,12 +841,14 @@ impl RemoteControlWebsocket {
|
||||
let queued_server_envelope = tokio::select! {
|
||||
_ = shutdown_token.cancelled() => return Ok(()),
|
||||
_ = ping_interval.tick() => {
|
||||
if let Err(err) = websocket_writer
|
||||
.send(tungstenite::Message::Ping(Vec::new().into()))
|
||||
.await
|
||||
{
|
||||
return Err(io::Error::other(err));
|
||||
}
|
||||
tokio::select! {
|
||||
_ = shutdown_token.cancelled() => return Ok(()),
|
||||
send_result = websocket_writer.send(tungstenite::Message::Ping(Vec::new().into())) => {
|
||||
if let Err(err) = send_result {
|
||||
return Err(io::Error::other(err));
|
||||
}
|
||||
}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
wait_result = used_rx.changed(), if !outbound_has_capacity =>
|
||||
@@ -886,6 +1054,7 @@ impl RemoteControlWebsocket {
|
||||
}
|
||||
};
|
||||
|
||||
let client_message_key = WebsocketState::client_message_key(&client_envelope);
|
||||
let observation = {
|
||||
let mut websocket_state = state.lock().await;
|
||||
websocket_state.observe_client_message(client_envelope, wire_size_bytes)
|
||||
@@ -895,24 +1064,6 @@ impl RemoteControlWebsocket {
|
||||
ClientSegmentObservation::Pending | ClientSegmentObservation::Dropped => continue,
|
||||
};
|
||||
|
||||
{
|
||||
let mut websocket_state = state.lock().await;
|
||||
if let Some(cursor) = client_envelope.cursor.as_deref() {
|
||||
websocket_state.subscribe_cursor = Some(cursor.to_string());
|
||||
}
|
||||
if let ClientEvent::Ack { segment_id } = &client_envelope.event
|
||||
&& let Some(acked_seq_id) = client_envelope.seq_id
|
||||
&& let Some(stream_id) = client_envelope.stream_id.as_ref()
|
||||
{
|
||||
websocket_state.outbound_buffer.ack(
|
||||
&client_envelope.client_id,
|
||||
stream_id,
|
||||
acked_seq_id,
|
||||
*segment_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let closed_client =
|
||||
matches!(&client_envelope.event, ClientEvent::ClientClosed).then(|| {
|
||||
(
|
||||
@@ -920,6 +1071,7 @@ impl RemoteControlWebsocket {
|
||||
client_envelope.stream_id.clone(),
|
||||
)
|
||||
});
|
||||
let delivered_client_envelope = client_envelope.clone();
|
||||
if client_tracker
|
||||
.handle_message(client_envelope)
|
||||
.await
|
||||
@@ -927,6 +1079,10 @@ impl RemoteControlWebsocket {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
state
|
||||
.lock()
|
||||
.await
|
||||
.record_client_message_delivery(&delivered_client_envelope, client_message_key);
|
||||
if let Some((client_id, stream_id)) = closed_client {
|
||||
let mut websocket_state = state.lock().await;
|
||||
if let Some(stream_id) = stream_id {
|
||||
@@ -1190,7 +1346,22 @@ pub(super) async fn connect_remote_control_websocket(
|
||||
connect_options.subscribe_cursor,
|
||||
)?;
|
||||
|
||||
match connect_async(request).await {
|
||||
let websocket_connect_result = tokio::time::timeout(
|
||||
REMOTE_CONTROL_WEBSOCKET_CONNECT_TIMEOUT,
|
||||
connect_async(request),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
io::Error::new(
|
||||
ErrorKind::TimedOut,
|
||||
format!(
|
||||
"timed out connecting to remote control websocket at `{}` after {:?}",
|
||||
remote_control_target.websocket_url, REMOTE_CONTROL_WEBSOCKET_CONNECT_TIMEOUT
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
match websocket_connect_result {
|
||||
Ok((websocket_stream, response)) => Ok((websocket_stream, response.map(|_| ()))),
|
||||
Err(err) => {
|
||||
match &err {
|
||||
@@ -2013,6 +2184,17 @@ mod tests {
|
||||
.expect("writer should stop cleanly");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn join_connection_workers_aborts_stuck_worker_after_timeout() {
|
||||
let mut join_set = tokio::task::JoinSet::new();
|
||||
join_set.spawn(futures::future::pending::<()>());
|
||||
|
||||
RemoteControlWebsocket::join_connection_workers(&mut join_set, Duration::from_millis(10))
|
||||
.await;
|
||||
|
||||
assert!(join_set.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_server_writer_inner_assigns_contiguous_seq_ids_per_stream() {
|
||||
let (client_stream, mut server_stream) = connected_websocket_pair().await;
|
||||
@@ -2350,6 +2532,65 @@ mod tests {
|
||||
&raw[split..],
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
observe_client_message(&mut state, first_chunk.clone()),
|
||||
ClientSegmentObservation::Pending
|
||||
));
|
||||
let completed_envelope = match observe_client_message(&mut state, second_chunk) {
|
||||
ClientSegmentObservation::Forward(client_envelope) => *client_envelope,
|
||||
_ => panic!("expected completed client message"),
|
||||
};
|
||||
state.record_client_message_delivery(
|
||||
&completed_envelope,
|
||||
Some((
|
||||
(
|
||||
ClientId("client-1".to_string()),
|
||||
Some(StreamId("stream-1".to_string())),
|
||||
),
|
||||
4,
|
||||
)),
|
||||
);
|
||||
assert!(matches!(
|
||||
observe_client_message(&mut state, first_chunk),
|
||||
ClientSegmentObservation::Dropped
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_state_allows_replay_before_completed_chunk_delivery() {
|
||||
let (outbound_buffer, _used_rx) = BoundedOutboundBuffer::new();
|
||||
let mut state = WebsocketState {
|
||||
outbound_buffer,
|
||||
subscribe_cursor: None,
|
||||
next_seq_id_by_stream: HashMap::new(),
|
||||
last_completed_client_chunk_seq_id_by_stream: HashMap::new(),
|
||||
client_segment_reassembler: ClientSegmentReassembler::default(),
|
||||
};
|
||||
let message = JSONRPCMessage::Notification(JSONRPCNotification {
|
||||
method: "initialized".to_string(),
|
||||
params: None,
|
||||
});
|
||||
let raw = serde_json::to_vec(&message).expect("message should serialize");
|
||||
let split = raw.len() / 2;
|
||||
let first_chunk = client_chunk_envelope(
|
||||
"client-1",
|
||||
"stream-1",
|
||||
/*seq_id*/ 4,
|
||||
/*segment_id*/ 0,
|
||||
/*segment_count*/ 2,
|
||||
raw.len(),
|
||||
&raw[..split],
|
||||
);
|
||||
let second_chunk = client_chunk_envelope(
|
||||
"client-1",
|
||||
"stream-1",
|
||||
/*seq_id*/ 4,
|
||||
/*segment_id*/ 1,
|
||||
/*segment_count*/ 2,
|
||||
raw.len(),
|
||||
&raw[split..],
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
observe_client_message(&mut state, first_chunk.clone()),
|
||||
ClientSegmentObservation::Pending
|
||||
@@ -2360,7 +2601,7 @@ mod tests {
|
||||
));
|
||||
assert!(matches!(
|
||||
observe_client_message(&mut state, first_chunk),
|
||||
ClientSegmentObservation::Dropped
|
||||
ClientSegmentObservation::Pending
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ codex-file-search = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-memories-extension = { workspace = true }
|
||||
codex-web-search-extension = { workspace = true }
|
||||
codex-memories-write = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_extension_api::AgentSpawner;
|
||||
use codex_extension_api::ExtensionEventSink;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_login::AuthManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::protocol::Event;
|
||||
@@ -22,6 +23,7 @@ use crate::outgoing_message::OutgoingMessageSender;
|
||||
pub(crate) fn thread_extensions<S>(
|
||||
guardian_agent_spawner: S,
|
||||
event_sink: Arc<dyn ExtensionEventSink>,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
) -> Arc<ExtensionRegistry<Config>>
|
||||
where
|
||||
S: AgentSpawner<StartThreadOptions, Spawned = NewThread, Error = CodexErr> + 'static,
|
||||
@@ -29,6 +31,7 @@ where
|
||||
let mut builder = ExtensionRegistryBuilder::<Config>::with_event_sink(event_sink);
|
||||
codex_guardian::install(&mut builder, guardian_agent_spawner);
|
||||
codex_memories_extension::install(&mut builder, codex_otel::global());
|
||||
codex_web_search_extension::install(&mut builder, auth_manager);
|
||||
Arc::new(builder.build())
|
||||
}
|
||||
|
||||
|
||||
@@ -184,15 +184,16 @@ mod tests {
|
||||
let thread_manager = Arc::new_cyclic(|thread_manager| {
|
||||
ThreadManager::new(
|
||||
&good_config,
|
||||
auth_manager,
|
||||
auth_manager.clone(),
|
||||
SessionSource::Exec,
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
thread_extensions(
|
||||
guardian_agent_spawner(thread_manager.clone()),
|
||||
Arc::new(NoopExtensionEventSink),
|
||||
auth_manager.clone(),
|
||||
),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store,
|
||||
Arc::clone(&thread_store),
|
||||
Some(state_db.clone()),
|
||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
|
||||
@@ -314,6 +314,7 @@ impl MessageProcessor {
|
||||
thread_extensions(
|
||||
guardian_agent_spawner(thread_manager.clone()),
|
||||
app_server_extension_event_sink(outgoing.clone()),
|
||||
auth_manager.clone(),
|
||||
),
|
||||
Some(analytics_events_client.clone()),
|
||||
Arc::clone(&thread_store),
|
||||
|
||||
@@ -658,6 +658,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
cwd: None,
|
||||
runtime_workspace_roots: None,
|
||||
approval_policy: None,
|
||||
|
||||
@@ -25,6 +25,8 @@ use codex_app_server_protocol::AccountLoginCompletedNotification;
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||
use codex_app_server_protocol::AdditionalContextEntry;
|
||||
use codex_app_server_protocol::AdditionalContextKind;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppListUpdatedNotification;
|
||||
use codex_app_server_protocol::AppSummary;
|
||||
@@ -421,6 +423,7 @@ use codex_thread_store::ThreadStore;
|
||||
use codex_thread_store::ThreadStoreError;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::io::Error as IoError;
|
||||
|
||||
@@ -1513,9 +1513,13 @@ impl ThreadRequestProcessor {
|
||||
.clone()
|
||||
.ok_or_else(|| internal_error("sqlite state db unavailable for memory reset"))?;
|
||||
|
||||
state_db.clear_memory_data().await.map_err(|err| {
|
||||
internal_error(format!("failed to clear memory rows in state db: {err}"))
|
||||
})?;
|
||||
state_db
|
||||
.memories()
|
||||
.clear_memory_data()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to clear memory rows in memories db: {err}"))
|
||||
})?;
|
||||
|
||||
clear_memory_roots_contents(&self.config.codex_home)
|
||||
.await
|
||||
@@ -2783,15 +2787,20 @@ impl ThreadRequestProcessor {
|
||||
/*include_history*/ true,
|
||||
)
|
||||
.await?;
|
||||
if source_thread.thread_id != existing_thread_id {
|
||||
return Err(invalid_request(format!(
|
||||
"cannot resume running thread {existing_thread_id} from source thread {}",
|
||||
source_thread.thread_id
|
||||
)));
|
||||
}
|
||||
Some((existing_thread_id, existing_thread, source_thread))
|
||||
} else {
|
||||
None
|
||||
let source_thread = self
|
||||
.read_stored_thread_for_resume(
|
||||
¶ms.thread_id,
|
||||
params.path.as_ref(),
|
||||
/*include_history*/ true,
|
||||
)
|
||||
.await?;
|
||||
let existing_thread_id = source_thread.thread_id;
|
||||
match self.thread_manager.get_thread(existing_thread_id).await {
|
||||
Ok(existing_thread) => Some((existing_thread_id, existing_thread, source_thread)),
|
||||
Err(_) => None,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((existing_thread_id, existing_thread, source_thread)) = running_thread {
|
||||
@@ -2808,6 +2817,48 @@ impl ThreadRequestProcessor {
|
||||
active_path.display()
|
||||
)));
|
||||
}
|
||||
let config_snapshot = existing_thread.config_snapshot().await;
|
||||
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
|
||||
if !mismatch_details.is_empty() {
|
||||
let has_subscribers = !self
|
||||
.thread_state_manager
|
||||
.subscribed_connection_ids(existing_thread_id)
|
||||
.await
|
||||
.is_empty();
|
||||
let loaded_status = self
|
||||
.thread_watch_manager
|
||||
.loaded_status_for_thread(&existing_thread_id.to_string())
|
||||
.await;
|
||||
let is_running =
|
||||
matches!(existing_thread.agent_status().await, AgentStatus::Running);
|
||||
|
||||
if !has_subscribers && matches!(loaded_status, ThreadStatus::Idle) && !is_running {
|
||||
// A loaded idle thread is only a cache entry. Shut it down
|
||||
// before removing it so cold resume cannot duplicate a
|
||||
// thread that timed out during shutdown.
|
||||
match wait_for_thread_shutdown(&existing_thread).await {
|
||||
ThreadShutdownResult::Complete => {
|
||||
self.thread_manager.remove_thread(&existing_thread_id).await;
|
||||
self.finalize_thread_teardown(existing_thread_id).await;
|
||||
return Ok(false);
|
||||
}
|
||||
ThreadShutdownResult::SubmitFailed => {
|
||||
warn!("failed to submit Shutdown to thread {existing_thread_id}");
|
||||
}
|
||||
ThreadShutdownResult::TimedOut => {
|
||||
warn!("thread {existing_thread_id} shutdown timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve rejoin semantics when another client can still observe
|
||||
// the loaded thread or shutdown did not complete.
|
||||
tracing::warn!(
|
||||
"thread/resume overrides ignored for loaded thread {}: {}",
|
||||
existing_thread_id,
|
||||
mismatch_details.join("; ")
|
||||
);
|
||||
}
|
||||
let redact_resume_payloads =
|
||||
should_redact_thread_resume_payloads(app_server_client_name.as_deref());
|
||||
let history_items = source_thread
|
||||
@@ -2837,15 +2888,6 @@ impl ThreadRequestProcessor {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let config_snapshot = existing_thread.config_snapshot().await;
|
||||
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
|
||||
if !mismatch_details.is_empty() {
|
||||
tracing::warn!(
|
||||
"thread/resume overrides ignored for running thread {}: {}",
|
||||
existing_thread_id,
|
||||
mismatch_details.join("; ")
|
||||
);
|
||||
}
|
||||
let mut summary_source_thread = source_thread;
|
||||
summary_source_thread.history = None;
|
||||
let mut thread_summary = self.stored_thread_to_api_thread(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
|
||||
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TurnRequestProcessor {
|
||||
@@ -30,6 +32,29 @@ fn resolve_runtime_workspace_roots(
|
||||
resolved_roots
|
||||
}
|
||||
|
||||
fn map_additional_context(
|
||||
additional_context: Option<HashMap<String, AdditionalContextEntry>>,
|
||||
) -> BTreeMap<String, CoreAdditionalContextEntry> {
|
||||
additional_context
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(key, entry)| {
|
||||
(
|
||||
key,
|
||||
CoreAdditionalContextEntry {
|
||||
value: entry.value,
|
||||
kind: match entry.kind {
|
||||
AdditionalContextKind::Untrusted => CoreAdditionalContextKind::Untrusted,
|
||||
AdditionalContextKind::Application => {
|
||||
CoreAdditionalContextKind::Application
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct ThreadSettingsBuildParams {
|
||||
method: &'static str,
|
||||
cwd: Option<PathBuf>,
|
||||
@@ -391,6 +416,7 @@ impl TurnRequestProcessor {
|
||||
.into_iter()
|
||||
.map(V2UserInput::into_core)
|
||||
.collect();
|
||||
let additional_context = map_additional_context(params.additional_context);
|
||||
let turn_has_input = !mapped_items.is_empty();
|
||||
let thread_settings = self
|
||||
.build_thread_settings_overrides(
|
||||
@@ -419,6 +445,7 @@ impl TurnRequestProcessor {
|
||||
environments: environment_selections,
|
||||
final_output_json_schema: params.output_schema,
|
||||
responsesapi_client_metadata: params.responsesapi_client_metadata,
|
||||
additional_context,
|
||||
thread_settings,
|
||||
};
|
||||
let turn_id = self
|
||||
@@ -746,10 +773,12 @@ impl TurnRequestProcessor {
|
||||
.into_iter()
|
||||
.map(V2UserInput::into_core)
|
||||
.collect();
|
||||
let additional_context = map_additional_context(params.additional_context);
|
||||
|
||||
let turn_id = thread
|
||||
.steer_input(
|
||||
mapped_items,
|
||||
additional_context,
|
||||
Some(¶ms.expected_turn_id),
|
||||
params.responsesapi_client_metadata,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_fake_rollout;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewDelivery;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
use codex_app_server_protocol::ReviewStartResponse;
|
||||
use codex_app_server_protocol::ReviewTarget;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadSource;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -60,6 +68,7 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
|
||||
let client_metadata = HashMap::from([
|
||||
("fiber_run_id".to_string(), "fiber-start-123".to_string()),
|
||||
("origin".to_string(), "gaas".to_string()),
|
||||
("thread_source".to_string(), "client-supplied".to_string()),
|
||||
]);
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
@@ -93,12 +102,188 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
|
||||
.unwrap_or_else(|| panic!("missing x-codex-turn-metadata header"));
|
||||
assert_eq!(metadata["fiber_run_id"].as_str(), Some("fiber-start-123"));
|
||||
assert_eq!(metadata["origin"].as_str(), Some("gaas"));
|
||||
assert_eq!(metadata["thread_source"].as_str(), Some("client-supplied"));
|
||||
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
|
||||
assert!(metadata.get("session_id").is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
/*supports_websockets*/ false,
|
||||
)?;
|
||||
|
||||
let source_thread_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-05T12-00-00",
|
||||
"2025-01-05T12:00:00Z",
|
||||
"Saved user message",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let ThreadForkResponse { thread, .. } =
|
||||
fork_fake_rollout_thread(&mut mcp, source_thread_id.clone()).await?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Continue".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let request = response_mock.single_request();
|
||||
let metadata = request
|
||||
.header("x-codex-turn-metadata")
|
||||
.as_deref()
|
||||
.map(parse_json_header)
|
||||
.unwrap_or_else(|| panic!("missing x-codex-turn-metadata header"));
|
||||
assert_eq!(
|
||||
metadata["forked_from_thread_id"].as_str(),
|
||||
Some(source_thread_id.as_str())
|
||||
);
|
||||
assert_eq!(metadata["thread_id"].as_str(), Some(thread.id.as_str()));
|
||||
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn review_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let review_payload = serde_json::json!({
|
||||
"findings": [],
|
||||
"overall_correctness": "good",
|
||||
"overall_explanation": "Done",
|
||||
"overall_confidence_score": 0.5
|
||||
})
|
||||
.to_string();
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", &review_payload),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
/*supports_websockets*/ false,
|
||||
)?;
|
||||
|
||||
let source_thread_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-05T12-00-00",
|
||||
"2025-01-05T12:00:00Z",
|
||||
"Saved user message",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let ThreadForkResponse { thread, .. } =
|
||||
fork_fake_rollout_thread(&mut mcp, source_thread_id.clone()).await?;
|
||||
|
||||
let review_req = mcp
|
||||
.send_review_start_request(ReviewStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
delivery: Some(ReviewDelivery::Inline),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "Review the fork".to_string(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
let review_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(review_req)),
|
||||
)
|
||||
.await??;
|
||||
let ReviewStartResponse {
|
||||
review_thread_id, ..
|
||||
} = to_response::<ReviewStartResponse>(review_resp)?;
|
||||
assert_eq!(review_thread_id, thread.id);
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let request = response_mock.single_request();
|
||||
let metadata = request
|
||||
.header("x-codex-turn-metadata")
|
||||
.as_deref()
|
||||
.map(parse_json_header)
|
||||
.unwrap_or_else(|| panic!("missing x-codex-turn-metadata header"));
|
||||
assert_eq!(
|
||||
request.header("x-openai-subagent").as_deref(),
|
||||
Some("review")
|
||||
);
|
||||
assert_eq!(
|
||||
metadata["forked_from_thread_id"].as_str(),
|
||||
Some(review_thread_id.as_str())
|
||||
);
|
||||
let review_request_thread_id = metadata["thread_id"]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| panic!("missing review request thread_id"));
|
||||
assert!(review_request_thread_id != review_thread_id.as_str());
|
||||
assert_eq!(
|
||||
request
|
||||
.header("x-codex-window-id")
|
||||
.as_deref()
|
||||
.and_then(|window_id| window_id.split_once(':').map(|(thread_id, _)| thread_id)),
|
||||
Some(review_request_thread_id)
|
||||
);
|
||||
assert!(metadata["turn_id"].as_str().is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -179,6 +364,7 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: Some(steer_metadata.clone()),
|
||||
additional_context: None,
|
||||
expected_turn_id: turn_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
@@ -344,6 +530,25 @@ supports_websockets = {supports_websockets}
|
||||
)
|
||||
}
|
||||
|
||||
async fn fork_fake_rollout_thread(
|
||||
mcp: &mut McpProcess,
|
||||
source_thread_id: String,
|
||||
) -> Result<ThreadForkResponse> {
|
||||
let fork_req = mcp
|
||||
.send_thread_fork_request(ThreadForkParams {
|
||||
thread_id: source_thread_id,
|
||||
thread_source: Some(ThreadSource::User),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let fork_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(fork_req)),
|
||||
)
|
||||
.await??;
|
||||
to_response::<ThreadForkResponse>(fork_resp)
|
||||
}
|
||||
|
||||
fn parse_json_header(value: &str) -> serde_json::Value {
|
||||
match serde_json::from_str(value) {
|
||||
Ok(value) => value,
|
||||
|
||||
@@ -49,7 +49,10 @@ async fn memory_reset_clears_memory_files_and_rows_preserves_threads() -> Result
|
||||
.await??;
|
||||
let _: MemoryResetResponse = to_response::<MemoryResetResponse>(response)?;
|
||||
|
||||
let stage1_outputs = state_db.list_stage1_outputs_for_global(/*n*/ 10).await?;
|
||||
let stage1_outputs = state_db
|
||||
.memories()
|
||||
.list_stage1_outputs_for_global(/*n*/ 10)
|
||||
.await?;
|
||||
assert_eq!(stage1_outputs, Vec::new());
|
||||
assert_eq!(
|
||||
state_db.get_thread_memory_mode(thread_id).await?.as_deref(),
|
||||
@@ -81,6 +84,7 @@ async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) ->
|
||||
state_db.upsert_thread(&metadata).await?;
|
||||
|
||||
let claim = state_db
|
||||
.memories()
|
||||
.try_claim_stage1_job(
|
||||
thread_id,
|
||||
worker_id,
|
||||
@@ -94,6 +98,7 @@ async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) ->
|
||||
};
|
||||
assert!(
|
||||
state_db
|
||||
.memories()
|
||||
.mark_stage1_job_succeeded(
|
||||
thread_id,
|
||||
ownership_token.as_str(),
|
||||
@@ -106,6 +111,7 @@ async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) ->
|
||||
"stage1 success should be recorded"
|
||||
);
|
||||
state_db
|
||||
.memories()
|
||||
.enqueue_global_consolidation(now.timestamp())
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -68,4 +68,5 @@ mod turn_interrupt;
|
||||
mod turn_start;
|
||||
mod turn_start_zsh_fork;
|
||||
mod turn_steer;
|
||||
mod web_search;
|
||||
mod windows_sandbox_setup;
|
||||
|
||||
@@ -43,6 +43,7 @@ use codex_app_server_protocol::ThreadSource;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::ThreadUnsubscribeParams;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
@@ -1929,7 +1930,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -> Result<()> {
|
||||
async fn thread_resume_defers_updated_at_until_turn_start() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
let rollout = setup_rollout_fixture(codex_home.path(), &server.uri())?;
|
||||
@@ -1973,6 +1974,33 @@ async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -
|
||||
let after_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?;
|
||||
assert_eq!(after_modified, rollout.before_modified);
|
||||
|
||||
let unsubscribe_id = mcp
|
||||
.send_thread_unsubscribe_request(ThreadUnsubscribeParams {
|
||||
thread_id: thread_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(unsubscribe_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let resume_id = mcp
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: "not-a-valid-thread-id".to_string(),
|
||||
path: Some(normalized_existing_path(&rollout.rollout_file_path)?),
|
||||
cwd: Some(codex_home.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let resume_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadResumeResponse { cwd, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
assert_eq!(cwd, AbsolutePathBuf::from_absolute_path(codex_home.path())?);
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id,
|
||||
@@ -2291,9 +2319,10 @@ async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result
|
||||
)
|
||||
.await??;
|
||||
|
||||
let stale_path = rollout_path(codex_home.path(), "2025-01-01T00-00-00", &thread_id);
|
||||
let stale_thread_id = Uuid::new_v4().to_string();
|
||||
let stale_path = rollout_path(codex_home.path(), "2025-01-01T00-00-00", &stale_thread_id);
|
||||
std::fs::create_dir_all(stale_path.parent().expect("stale path parent"))?;
|
||||
let thread_uuid = Uuid::parse_str(&thread_id)?;
|
||||
let thread_uuid = Uuid::parse_str(&stale_thread_id)?;
|
||||
let mut stale_file = std::fs::File::create(&stale_path)?;
|
||||
let stale_meta = json!({
|
||||
"timestamp": "2025-01-01T00:00:00Z",
|
||||
|
||||
@@ -317,6 +317,7 @@ async fn thread_unsubscribe_preserves_cached_status_before_idle_unload() -> Resu
|
||||
let resume_id = mcp
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id,
|
||||
cwd: Some(codex_home.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -16,6 +16,8 @@ use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url;
|
||||
use app_test_support::write_models_cache;
|
||||
use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE;
|
||||
use codex_app_server::INVALID_PARAMS_ERROR_CODE;
|
||||
use codex_app_server_protocol::AdditionalContextEntry;
|
||||
use codex_app_server_protocol::AdditionalContextKind;
|
||||
use codex_app_server_protocol::ByteRange;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::CollabAgentStatus;
|
||||
@@ -301,6 +303,82 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_additional_context_flows_to_model_input() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::default(),
|
||||
)?;
|
||||
|
||||
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)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "inspect tab".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
additional_context: Some(HashMap::from([(
|
||||
"custom_source".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "source value".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
)])),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.context("failed to fetch received requests")?;
|
||||
let request = requests
|
||||
.iter()
|
||||
.find(|request| request.url.path().ends_with("/responses"))
|
||||
.context("expected model request")?;
|
||||
let body = request
|
||||
.body_json::<Value>()
|
||||
.context("request body should be JSON")?;
|
||||
assert!(
|
||||
body.to_string()
|
||||
.contains("<external_custom_source>source value</external_custom_source>")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_sends_originator_header() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
@@ -2069,6 +2147,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
cwd: Some(first_cwd.clone()),
|
||||
runtime_workspace_roots: None,
|
||||
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
|
||||
@@ -2111,6 +2190,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
cwd: Some(second_cwd.clone()),
|
||||
runtime_workspace_roots: None,
|
||||
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
@@ -9,6 +10,8 @@ use app_test_support::to_response;
|
||||
use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url;
|
||||
use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE;
|
||||
use codex_app_server::INVALID_PARAMS_ERROR_CODE;
|
||||
use codex_app_server_protocol::AdditionalContextEntry;
|
||||
use codex_app_server_protocol::AdditionalContextKind;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
@@ -21,6 +24,8 @@ use codex_app_server_protocol::TurnSteerParams;
|
||||
use codex_app_server_protocol::TurnSteerResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -67,6 +72,7 @@ async fn turn_steer_requires_active_turn() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
expected_turn_id: "turn-does-not-exist".to_string(),
|
||||
})
|
||||
.await?;
|
||||
@@ -176,6 +182,7 @@ async fn turn_steer_rejects_oversized_text_input() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
expected_turn_id: turn.id.clone(),
|
||||
})
|
||||
.await?;
|
||||
@@ -284,6 +291,7 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
expected_turn_id: turn.id.clone(),
|
||||
})
|
||||
.await?;
|
||||
@@ -312,3 +320,118 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_steer_rejects_context_only_input_without_merging_context() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
let working_directory = tmp.path().join("workdir");
|
||||
std::fs::create_dir(&working_directory)?;
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(vec![
|
||||
create_shell_command_sse_response(
|
||||
vec!["sleep".to_string(), "1".to_string()],
|
||||
Some(&working_directory),
|
||||
Some(10_000),
|
||||
"call_sleep",
|
||||
)?,
|
||||
app_test_support::create_final_assistant_message_sse_response("Done")?,
|
||||
])
|
||||
.await;
|
||||
write_mock_responses_config_toml_with_chatgpt_base_url(
|
||||
&codex_home,
|
||||
&server.uri(),
|
||||
&server.uri(),
|
||||
)?;
|
||||
mount_analytics_capture(&server, &codex_home).await?;
|
||||
|
||||
let mut mcp = McpProcess::new_without_managed_config(&codex_home).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)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "run sleep".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: Some(working_directory),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/started"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let additional_context = Some(HashMap::from([(
|
||||
"browser_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "tab one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
)]));
|
||||
let steer_req = mcp
|
||||
.send_turn_steer_request(TurnSteerParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: Vec::new(),
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context,
|
||||
expected_turn_id: turn.id,
|
||||
})
|
||||
.await?;
|
||||
let steer_error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(steer_req)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(steer_error.error.code, -32600);
|
||||
assert_eq!(steer_error.error.message, "input must not be empty");
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.context("failed to fetch received requests")?;
|
||||
let response_requests = requests
|
||||
.iter()
|
||||
.filter(|request| request.url.path().ends_with("/responses"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(response_requests.len(), 2);
|
||||
let body = response_requests[1]
|
||||
.body_json::<Value>()
|
||||
.context("request body should be JSON")?;
|
||||
assert!(
|
||||
!body
|
||||
.to_string()
|
||||
.contains("<external_browser_info>tab one</external_browser_info>")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
// macOS and Windows Bazel CI can spend tens of seconds starting app-server
|
||||
// subprocesses or processing test RPCs under load.
|
||||
#[cfg(any(target_os = "macos", windows))]
|
||||
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_web_search_round_trips_encrypted_output() -> Result<()> {
|
||||
let call_id = "web-run-1";
|
||||
let server = responses::start_mock_server().await;
|
||||
mount_search_response(&server).await;
|
||||
|
||||
let response_mock = responses::mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_function_call_with_namespace(
|
||||
call_id,
|
||||
"web",
|
||||
"run",
|
||||
&json!({
|
||||
"search_query": [{"q": "standalone web search"}],
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams::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)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Search the web".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let _turn: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
|
||||
let first_response = requests[0].body_json();
|
||||
assert!(
|
||||
requests[0].tool_by_name("web", "run").is_some(),
|
||||
"web.run should be sent to the model"
|
||||
);
|
||||
assert!(
|
||||
!has_hosted_web_search(&first_response),
|
||||
"standalone web search should replace hosted web search"
|
||||
);
|
||||
|
||||
let search_body = search_request_body(&server).await?;
|
||||
assert_eq!(
|
||||
search_body["commands"],
|
||||
json!({
|
||||
"search_query": [{"q": "standalone web search"}],
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
search_body["settings"]["allowed_callers"],
|
||||
json!(["direct"])
|
||||
);
|
||||
assert_eq!(
|
||||
search_body["input"]
|
||||
.as_array()
|
||||
.context("search input should be an array")?
|
||||
.last(),
|
||||
Some(&json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "Search the web"}],
|
||||
}))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
requests[1].function_call_output(call_id),
|
||||
json!({
|
||||
"type": "function_call_output",
|
||||
"call_id": call_id,
|
||||
"output": [{
|
||||
"type": "encrypted_content",
|
||||
"encrypted_content": "ciphertext",
|
||||
}],
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mount_search_response(server: &MockServer) {
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/codex/alpha/search"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"encrypted_output": "ciphertext",
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn has_hosted_web_search(body: &Value) -> bool {
|
||||
body.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|tools| {
|
||||
tools
|
||||
.iter()
|
||||
.any(|tool| tool.get("type").and_then(Value::as_str) == Some("web_search"))
|
||||
})
|
||||
}
|
||||
|
||||
async fn search_request_body(server: &MockServer) -> Result<Value> {
|
||||
server
|
||||
.received_requests()
|
||||
.await
|
||||
.context("failed to fetch received requests")?
|
||||
.into_iter()
|
||||
.find(|request| request.url.path() == "/api/codex/alpha/search")
|
||||
.context("expected standalone search request")?
|
||||
.body_json()
|
||||
.context("search request body should be JSON")
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
model_provider = "openai-custom"
|
||||
chatgpt_base_url = "{server_uri}"
|
||||
|
||||
[features]
|
||||
standalone_web_search = true
|
||||
|
||||
[model_providers.openai-custom]
|
||||
name = "OpenAI"
|
||||
base_url = "{server_uri}/api/codex"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
supports_websockets = false
|
||||
requires_openai_auth = true
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -702,6 +702,7 @@ fn state_summary(check: &DoctorCheck) -> String {
|
||||
"state DB integrity",
|
||||
"log DB integrity",
|
||||
"goals DB integrity",
|
||||
"memories DB integrity",
|
||||
]
|
||||
.into_iter()
|
||||
.all(|label| detail::detail_value(check, label).is_some_and(|value| value == "ok"));
|
||||
@@ -1363,6 +1364,37 @@ Run codex doctor without --summary for detailed diagnostics.
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_human_report_includes_memories_db_in_state_health_summary() {
|
||||
let report = DoctorReport {
|
||||
schema_version: 1,
|
||||
generated_at: "0s since unix epoch".to_string(),
|
||||
overall_status: CheckStatus::Ok,
|
||||
codex_version: "0.0.0".to_string(),
|
||||
checks: vec![
|
||||
DoctorCheck::new(
|
||||
"state.paths",
|
||||
"state",
|
||||
CheckStatus::Ok,
|
||||
"state paths inspectable",
|
||||
)
|
||||
.detail("state DB: /tmp/state.sqlite")
|
||||
.detail("state DB integrity: ok")
|
||||
.detail("log DB: /tmp/logs.sqlite")
|
||||
.detail("log DB integrity: ok")
|
||||
.detail("goals DB: /tmp/goals.sqlite")
|
||||
.detail("goals DB integrity: ok")
|
||||
.detail("memories DB: /tmp/memories.sqlite")
|
||||
.detail("memories DB integrity: ok"),
|
||||
],
|
||||
};
|
||||
|
||||
let rendered = render_human_report(&report, detailed_no_color_unicode_options());
|
||||
|
||||
assert!(rendered.contains("✓ state databases healthy"));
|
||||
assert!(rendered.contains("memories DB /tmp/memories.sqlite · integrity ok"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_human_report_supports_ascii_output() {
|
||||
let rendered = render_human_report(
|
||||
|
||||
@@ -413,6 +413,7 @@ fn state_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail> {
|
||||
push_database_row(&mut out, parsed, "state DB");
|
||||
push_database_row(&mut out, parsed, "log DB");
|
||||
push_database_row(&mut out, parsed, "goals DB");
|
||||
push_database_row(&mut out, parsed, "memories DB");
|
||||
|
||||
for (source, label) in [
|
||||
("active rollout files", "active rollouts"),
|
||||
@@ -440,6 +441,8 @@ fn state_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail> {
|
||||
"state DB integrity",
|
||||
"log DB integrity",
|
||||
"goals DB integrity",
|
||||
"memories DB",
|
||||
"memories DB integrity",
|
||||
"active rollout files",
|
||||
"archived rollout files",
|
||||
],
|
||||
|
||||
@@ -27,7 +27,7 @@ use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
|
||||
use codex_rollout_trace::REDUCED_STATE_FILE_NAME;
|
||||
use codex_rollout_trace::replay_bundle;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_state::state_db_path;
|
||||
use codex_state::memories_db_path;
|
||||
use codex_tui::AppExitInfo;
|
||||
use codex_tui::Cli as TuiCli;
|
||||
use codex_tui::ExitReason;
|
||||
@@ -1751,22 +1751,16 @@ async fn run_debug_clear_memories_command(
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let state_path = state_db_path(config.sqlite_home.as_path());
|
||||
let mut cleared_state_db = false;
|
||||
if tokio::fs::try_exists(&state_path).await? {
|
||||
let state_db =
|
||||
StateRuntime::init(config.sqlite_home.clone(), config.model_provider_id.clone())
|
||||
.await?;
|
||||
state_db.clear_memory_data().await?;
|
||||
cleared_state_db = true;
|
||||
}
|
||||
let memories_path = memories_db_path(config.sqlite_home.as_path());
|
||||
let cleared_memories_db =
|
||||
StateRuntime::clear_memory_data_in_sqlite_home(config.sqlite_home.as_path()).await?;
|
||||
|
||||
clear_memory_roots_contents(&config.codex_home).await?;
|
||||
|
||||
let mut message = if cleared_state_db {
|
||||
format!("Cleared memory state from {}.", state_path.display())
|
||||
let mut message = if cleared_memories_db {
|
||||
format!("Cleared memory state from {}.", memories_path.display())
|
||||
} else {
|
||||
format!("No state db found at {}.", state_path.display())
|
||||
format!("No memories db found at {}.", memories_path.display())
|
||||
};
|
||||
message.push_str(&format!(
|
||||
" Cleared memory directories under {}.",
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_state::memories_db_path;
|
||||
use codex_state::state_db_path;
|
||||
use predicates::str::contains;
|
||||
use sqlx::SqlitePool;
|
||||
@@ -23,6 +24,9 @@ async fn debug_clear_memories_resets_state_and_removes_memory_dir() -> Result<()
|
||||
let thread_id = "00000000-0000-0000-0000-000000000123";
|
||||
let db_path = state_db_path(codex_home.path());
|
||||
let pool = SqlitePool::connect(&format!("sqlite://{}", db_path.display())).await?;
|
||||
let memories_db_path = memories_db_path(codex_home.path());
|
||||
let memories_pool =
|
||||
SqlitePool::connect(&format!("sqlite://{}", memories_db_path.display())).await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -74,7 +78,7 @@ INSERT INTO stage1_outputs (
|
||||
"#,
|
||||
)
|
||||
.bind(thread_id)
|
||||
.execute(&pool)
|
||||
.execute(&memories_pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
@@ -99,13 +103,14 @@ INSERT INTO jobs (
|
||||
"#,
|
||||
)
|
||||
.bind(thread_id)
|
||||
.execute(&pool)
|
||||
.execute(&memories_pool)
|
||||
.await?;
|
||||
|
||||
let memory_root = codex_home.path().join("memories");
|
||||
std::fs::create_dir_all(&memory_root)?;
|
||||
std::fs::write(memory_root.join("memory_summary.md"), "stale memory")?;
|
||||
pool.close().await;
|
||||
memories_pool.close().await;
|
||||
|
||||
let mut cmd = codex_command(codex_home.path())?;
|
||||
cmd.args(["debug", "clear-memories"])
|
||||
@@ -113,7 +118,7 @@ INSERT INTO jobs (
|
||||
.success()
|
||||
.stdout(contains("Cleared memory state"));
|
||||
|
||||
let pool = SqlitePool::connect(&format!("sqlite://{}", db_path.display())).await?;
|
||||
let pool = SqlitePool::connect(&format!("sqlite://{}", memories_db_path.display())).await?;
|
||||
let stage1_outputs_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stage1_outputs")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
@@ -131,3 +136,54 @@ INSERT INTO jobs (
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_clear_memories_resets_memories_db_without_state_db() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let runtime =
|
||||
StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string()).await?;
|
||||
drop(runtime);
|
||||
|
||||
let db_path = state_db_path(codex_home.path());
|
||||
let memories_db_path = memories_db_path(codex_home.path());
|
||||
let memories_pool =
|
||||
SqlitePool::connect(&format!("sqlite://{}", memories_db_path.display())).await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO stage1_outputs (
|
||||
thread_id,
|
||||
source_updated_at,
|
||||
raw_memory,
|
||||
rollout_summary,
|
||||
generated_at,
|
||||
rollout_slug,
|
||||
usage_count,
|
||||
last_usage,
|
||||
selected_for_phase2,
|
||||
selected_for_phase2_source_updated_at
|
||||
) VALUES ('00000000-0000-0000-0000-000000000123', 1, 'raw', 'summary', 1, NULL, 0, NULL, 0, NULL)
|
||||
"#,
|
||||
)
|
||||
.execute(&memories_pool)
|
||||
.await?;
|
||||
|
||||
memories_pool.close().await;
|
||||
std::fs::remove_file(&db_path)?;
|
||||
|
||||
let mut cmd = codex_command(codex_home.path())?;
|
||||
cmd.args(["debug", "clear-memories"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Cleared memory state"));
|
||||
|
||||
let pool = SqlitePool::connect(&format!("sqlite://{}", memories_db_path.display())).await?;
|
||||
let stage1_outputs_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stage1_outputs")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
assert_eq!(stage1_outputs_count, 0);
|
||||
pool.close().await;
|
||||
assert!(!db_path.exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ codex-utils-rustls-provider = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
http = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "stream"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::common::Reasoning;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -27,75 +28,100 @@ pub enum SearchInput {
|
||||
Items(Vec<ResponseItem>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, JsonSchema)]
|
||||
pub struct SearchCommands {
|
||||
/// Query the internet search engine for a given list of queries.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub search_query: Option<Vec<SearchQuery>>,
|
||||
/// Query the image search engine for a given list of queries.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_query: Option<Vec<SearchQuery>>,
|
||||
/// Open pages by reference id or URL.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub open: Option<Vec<OpenOperation>>,
|
||||
/// Open links from previously opened pages.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub click: Option<Vec<ClickOperation>>,
|
||||
/// Find text patterns in pages.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub find: Option<Vec<FindOperation>>,
|
||||
/// Take screenshots of PDF pages.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub screenshot: Option<Vec<ScreenshotOperation>>,
|
||||
/// Look up prices for the given stock symbols.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub finance: Option<Vec<FinanceOperation>>,
|
||||
/// Look up weather forecasts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub weather: Option<Vec<WeatherOperation>>,
|
||||
/// Look up sports schedules and standings.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sports: Option<Vec<SportsOperation>>,
|
||||
/// Get time for the given UTC offsets.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub time: Option<Vec<TimeOperation>>,
|
||||
/// Set the length of the response to be returned.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub response_length: Option<SearchResponseLength>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
|
||||
pub struct SearchQuery {
|
||||
/// Search query.
|
||||
pub q: String,
|
||||
/// Whether to filter by recency, as a number of recent days.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub recency: Option<u64>,
|
||||
/// Whether to filter by a specific list of domains.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domains: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
|
||||
pub struct OpenOperation {
|
||||
/// Reference id or URL to open.
|
||||
pub ref_id: String,
|
||||
/// Line number to position the page at.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lineno: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct ClickOperation {
|
||||
/// Reference id containing the numbered link.
|
||||
pub ref_id: String,
|
||||
/// Numbered link id to open.
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct FindOperation {
|
||||
/// Reference id or URL to search within.
|
||||
pub ref_id: String,
|
||||
/// Text pattern to find.
|
||||
pub pattern: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct ScreenshotOperation {
|
||||
/// Reference id or URL to screenshot.
|
||||
pub ref_id: String,
|
||||
/// Zero-indexed PDF page number.
|
||||
pub pageno: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
|
||||
pub struct FinanceOperation {
|
||||
/// Ticker symbol to look up.
|
||||
pub ticker: String,
|
||||
/// Asset type to look up.
|
||||
pub r#type: FinanceAssetType,
|
||||
/// ISO 3166-1 alpha-3 country code, "OTC", or "" for cryptocurrency.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub market: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FinanceAssetType {
|
||||
Equity,
|
||||
@@ -104,49 +130,61 @@ pub enum FinanceAssetType {
|
||||
Index,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
|
||||
pub struct WeatherOperation {
|
||||
/// Location in "Country, Area, City" format.
|
||||
pub location: String,
|
||||
/// Start date in YYYY-MM-DD format. Defaults to today.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub start: Option<String>,
|
||||
/// Number of days to return. Defaults to 7.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
|
||||
pub struct SportsOperation {
|
||||
/// Tool name for sports requests.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool: Option<SportsToolName>,
|
||||
/// Sports function to call.
|
||||
pub r#fn: SportsFunction,
|
||||
/// League to look up.
|
||||
pub league: SportsLeague,
|
||||
/// Team to look up, using the common 3 or 4 letter alias used in broadcasts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub team: Option<String>,
|
||||
/// Opponent to use with `team` when narrowing the lookup.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub opponent: Option<String>,
|
||||
/// Start date in YYYY-MM-DD format.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub date_from: Option<String>,
|
||||
/// End date in YYYY-MM-DD format.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub date_to: Option<String>,
|
||||
/// Number of games to return.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_games: Option<u64>,
|
||||
/// Locale for the lookup.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub locale: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SportsToolName {
|
||||
Sports,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SportsFunction {
|
||||
Schedule,
|
||||
Standings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SportsLeague {
|
||||
Nba,
|
||||
@@ -160,12 +198,13 @@ pub enum SportsLeague {
|
||||
Ipl,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct TimeOperation {
|
||||
/// UTC offset formatted like "+03:00".
|
||||
pub utc_offset: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SearchResponseLength {
|
||||
Short,
|
||||
|
||||
@@ -6,6 +6,7 @@ use self::activation::installed_marketplace_metadata_matches;
|
||||
use self::activation::write_installed_marketplace_metadata;
|
||||
use self::git::clone_git_source;
|
||||
use self::git::git_remote_revision;
|
||||
use crate::marketplace::find_marketplace_manifest_path;
|
||||
use crate::marketplace::validate_marketplace_root;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerStack;
|
||||
@@ -176,9 +177,7 @@ fn upgrade_configured_git_marketplace(
|
||||
MARKETPLACE_UPGRADE_GIT_TIMEOUT,
|
||||
)?;
|
||||
let destination = install_root.join(&marketplace.name);
|
||||
if destination
|
||||
.join(".agents/plugins/marketplace.json")
|
||||
.is_file()
|
||||
if find_marketplace_manifest_path(&destination).is_some()
|
||||
&& marketplace.last_revision.as_deref() == Some(remote_revision.as_str())
|
||||
&& installed_marketplace_metadata_matches(&destination, marketplace, &remote_revision)
|
||||
{
|
||||
|
||||
@@ -575,6 +575,9 @@
|
||||
"sqlite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"standalone_web_search": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"steer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -4505,6 +4508,9 @@
|
||||
"sqlite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"standalone_web_search": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"steer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -262,10 +262,12 @@ impl AgentControl {
|
||||
.await?
|
||||
}
|
||||
(Some(session_source), None) => {
|
||||
let forked_from_thread_id = thread_spawn_parent_thread_id(&session_source);
|
||||
Box::pin(state.spawn_new_thread_with_source(
|
||||
config.clone(),
|
||||
self.clone(),
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
/*thread_source*/ Some(ThreadSource::Subagent),
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
@@ -487,6 +489,7 @@ impl AgentControl {
|
||||
self.clone(),
|
||||
session_source,
|
||||
/*thread_source*/ Some(ThreadSource::Subagent),
|
||||
/*forked_from_thread_id*/ Some(parent_thread_id),
|
||||
/*persist_extended_history*/ false,
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
|
||||
@@ -444,6 +444,7 @@ async fn send_input_submits_user_message() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
},
|
||||
);
|
||||
@@ -598,6 +599,7 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
},
|
||||
);
|
||||
@@ -770,6 +772,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -86,6 +86,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
extensions: Arc::clone(&parent_session.services.extensions),
|
||||
conversation_history: initial_history.unwrap_or(InitialHistory::New),
|
||||
session_source: SessionSource::SubAgent(subagent_source.clone()),
|
||||
forked_from_thread_id: Some(parent_session.conversation_id),
|
||||
thread_source: Some(ThreadSource::Subagent),
|
||||
agent_control: parent_session.services.agent_control.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
@@ -192,6 +193,7 @@ pub(crate) async fn run_codex_thread_one_shot(
|
||||
items: input,
|
||||
final_output_json_schema,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AdditionalContextEntry;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::Op;
|
||||
@@ -41,6 +42,7 @@ use codex_thread_store::ThreadStoreError;
|
||||
use codex_thread_store::ThreadStoreResult;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use rmcp::model::ReadResourceRequestParams;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -236,11 +238,17 @@ impl CodexThread {
|
||||
pub async fn steer_input(
|
||||
&self,
|
||||
input: Vec<UserInput>,
|
||||
additional_context: BTreeMap<String, AdditionalContextEntry>,
|
||||
expected_turn_id: Option<&str>,
|
||||
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
) -> Result<String, SteerInputError> {
|
||||
self.codex
|
||||
.steer_input(input, expected_turn_id, responsesapi_client_metadata)
|
||||
.steer_input(
|
||||
input,
|
||||
additional_context,
|
||||
expected_turn_id,
|
||||
responsesapi_client_metadata,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use codex_protocol::items::HookPromptItem;
|
||||
use codex_protocol::items::parse_hook_prompt_fragment;
|
||||
use codex_protocol::models::ContentItem;
|
||||
|
||||
use super::AdditionalContextUserFragment;
|
||||
use super::EnvironmentContext;
|
||||
use super::FragmentRegistration;
|
||||
use super::FragmentRegistrationProxy;
|
||||
@@ -19,6 +20,8 @@ static USER_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy<UserInstruction
|
||||
FragmentRegistrationProxy::new();
|
||||
static ENVIRONMENT_CONTEXT_REGISTRATION: FragmentRegistrationProxy<EnvironmentContext> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static ADDITIONAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy<AdditionalContextUserFragment> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static SKILL_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy<SkillInstructions> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static USER_SHELL_COMMAND_REGISTRATION: FragmentRegistrationProxy<UserShellCommand> =
|
||||
@@ -42,6 +45,7 @@ static LEGACY_MODEL_MISMATCH_WARNING_REGISTRATION: FragmentRegistrationProxy<
|
||||
static CONTEXTUAL_USER_FRAGMENTS: &[&dyn FragmentRegistration] = &[
|
||||
&USER_INSTRUCTIONS_REGISTRATION,
|
||||
&ENVIRONMENT_CONTEXT_REGISTRATION,
|
||||
&ADDITIONAL_CONTEXT_REGISTRATION,
|
||||
&SKILL_INSTRUCTIONS_REGISTRATION,
|
||||
&USER_SHELL_COMMAND_REGISTRATION,
|
||||
&TURN_ABORTED_REGISTRATION,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
@@ -81,6 +82,19 @@ pub trait ContextualUserFragment {
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_response_input_item(self) -> ResponseInputItem
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
ResponseInputItem::Message {
|
||||
role: Self::role().to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: self.render(),
|
||||
}],
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_marked_text(start_marker: &str, end_marker: &str, text: &str) -> bool {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
use super::ContextualUserFragment;
|
||||
use codex_utils_string::truncate_middle_with_token_budget;
|
||||
|
||||
const MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS: usize = 1_000;
|
||||
const ADDITIONAL_CONTEXT_END_MARKER_SUFFIX: &str = ">";
|
||||
const ADDITIONAL_CONTEXT_START_MARKER_PREFIX: &str = "<external_";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct AdditionalContextUserFragment {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl AdditionalContextUserFragment {
|
||||
pub(crate) fn new(key: String, value: String) -> Self {
|
||||
Self { key, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for AdditionalContextUserFragment {
|
||||
fn role() -> &'static str {
|
||||
"user"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
(
|
||||
ADDITIONAL_CONTEXT_START_MARKER_PREFIX,
|
||||
ADDITIONAL_CONTEXT_END_MARKER_SUFFIX,
|
||||
)
|
||||
}
|
||||
|
||||
fn matches_text(text: &str) -> bool {
|
||||
let trimmed = text.trim();
|
||||
let Some(rest) = trimmed.strip_prefix(ADDITIONAL_CONTEXT_START_MARKER_PREFIX) else {
|
||||
return false;
|
||||
};
|
||||
let Some((key, value_and_close)) = rest.split_once(ADDITIONAL_CONTEXT_END_MARKER_SUFFIX)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
value_and_close.ends_with(&format!("</external_{key}>"))
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
additional_context_body(&self.key, &self.value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct AdditionalContextDeveloperFragment {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl AdditionalContextDeveloperFragment {
|
||||
pub(crate) fn new(key: String, value: String) -> Self {
|
||||
Self { key, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for AdditionalContextDeveloperFragment {
|
||||
fn role() -> &'static str {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
("", "")
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
additional_context_developer_body(&self.key, &self.value)
|
||||
}
|
||||
}
|
||||
|
||||
fn additional_context_body(key: &str, value: &str) -> String {
|
||||
let value = truncate_middle_with_token_budget(value, MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS).0;
|
||||
format!("{key}>{value}</external_{key}")
|
||||
}
|
||||
|
||||
fn additional_context_developer_body(key: &str, value: &str) -> String {
|
||||
let value = truncate_middle_with_token_budget(value, MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS).0;
|
||||
format!("<{key}>{value}</{key}>")
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
//! Hidden user-context fragment for runtime-owned goal steering prompts.
|
||||
|
||||
use super::ContextualUserFragment;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
|
||||
/// Hidden runtime-owned goal steering context injected into model input.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -17,17 +15,6 @@ impl GoalContext {
|
||||
prompt: prompt.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the registered fragment into an active-turn injectable item.
|
||||
pub fn into_response_input_item(self) -> ResponseInputItem {
|
||||
ResponseInputItem::Message {
|
||||
role: <Self as ContextualUserFragment>::role().to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: self.render(),
|
||||
}],
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for GoalContext {
|
||||
|
||||
@@ -8,6 +8,7 @@ mod collaboration_mode_instructions;
|
||||
mod contextual_user_message;
|
||||
mod environment_context;
|
||||
mod fragment;
|
||||
mod fragments;
|
||||
mod goal_context;
|
||||
mod guardian_followup_review_reminder;
|
||||
mod hook_additional_context;
|
||||
@@ -40,6 +41,8 @@ pub(crate) use environment_context::EnvironmentContext;
|
||||
pub use fragment::ContextualUserFragment;
|
||||
pub(crate) use fragment::FragmentRegistration;
|
||||
pub(crate) use fragment::FragmentRegistrationProxy;
|
||||
pub(crate) use fragments::AdditionalContextDeveloperFragment;
|
||||
pub(crate) use fragments::AdditionalContextUserFragment;
|
||||
pub use goal_context::GoalContext;
|
||||
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
|
||||
pub(crate) use hook_additional_context::HookAdditionalContext;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! events, and owns helper hooks used by goal lifecycle behavior.
|
||||
|
||||
use crate::StateDbHandle;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::GoalContext;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
|
||||
@@ -714,6 +714,7 @@ async fn run_review_on_session(
|
||||
environments: None,
|
||||
final_output_json_schema: Some(params.schema.clone()),
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
#[allow(deprecated)]
|
||||
cwd: Some(params.parent_turn.cwd.to_path_buf()),
|
||||
|
||||
@@ -10,6 +10,7 @@ use tracing::debug_span;
|
||||
use tracing::info_span;
|
||||
|
||||
use crate::session::SteerInputError;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::session::SessionSettingsUpdate;
|
||||
|
||||
@@ -194,6 +195,7 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
environments,
|
||||
final_output_json_schema,
|
||||
responsesapi_client_metadata,
|
||||
additional_context,
|
||||
thread_settings,
|
||||
} = op
|
||||
else {
|
||||
@@ -224,6 +226,7 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
let accepted_items = match sess
|
||||
.steer_input(
|
||||
items.clone(),
|
||||
additional_context.clone(),
|
||||
/*expected_turn_id*/ None,
|
||||
responsesapi_client_metadata.clone(),
|
||||
)
|
||||
@@ -254,9 +257,20 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
)
|
||||
.await;
|
||||
let accepted_items = items.clone();
|
||||
let additional_context_input = {
|
||||
let mut state = sess.state.lock().await;
|
||||
state.additional_context.merge(additional_context)
|
||||
};
|
||||
let mut task_input = additional_context_input
|
||||
.into_iter()
|
||||
.map(TurnInput::ResponseInputItem)
|
||||
.collect::<Vec<_>>();
|
||||
if !items.is_empty() {
|
||||
task_input.push(TurnInput::UserInput(items));
|
||||
}
|
||||
sess.spawn_task(
|
||||
Arc::clone(¤t_context),
|
||||
items,
|
||||
task_input,
|
||||
crate::tasks::RegularTask::new(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -166,16 +166,16 @@ impl InputQueue {
|
||||
.accept_mailbox_delivery_for_current_turn();
|
||||
}
|
||||
|
||||
pub(super) async fn push_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
pub(super) async fn extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
&self,
|
||||
turn_state: &Mutex<TurnState>,
|
||||
input: TurnInput,
|
||||
) -> Result<(), TurnInput> {
|
||||
input: Vec<TurnInput>,
|
||||
) -> Result<(), Vec<TurnInput>> {
|
||||
let mut turn_state = turn_state.lock().await;
|
||||
if turn_state.is_terminal() {
|
||||
return Err(input);
|
||||
}
|
||||
turn_state.pending_input.items.push(input);
|
||||
turn_state.pending_input.items.extend(input);
|
||||
turn_state.accept_mailbox_delivery_for_current_turn();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Debug;
|
||||
@@ -97,6 +98,7 @@ use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AdditionalContextEntry;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::HasLegacyEvent;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
@@ -398,6 +400,7 @@ pub(crate) struct CodexSpawnArgs {
|
||||
pub(crate) extensions: Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>>,
|
||||
pub(crate) conversation_history: InitialHistory,
|
||||
pub(crate) session_source: SessionSource,
|
||||
pub(crate) forked_from_thread_id: Option<ThreadId>,
|
||||
pub(crate) thread_source: Option<ThreadSource>,
|
||||
pub(crate) agent_control: AgentControl,
|
||||
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
|
||||
@@ -462,6 +465,7 @@ impl Codex {
|
||||
extensions,
|
||||
conversation_history,
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
agent_control,
|
||||
dynamic_tools,
|
||||
@@ -617,6 +621,7 @@ impl Codex {
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
dynamic_tools,
|
||||
persist_extended_history,
|
||||
@@ -742,11 +747,17 @@ impl Codex {
|
||||
pub async fn steer_input(
|
||||
&self,
|
||||
input: Vec<UserInput>,
|
||||
additional_context: BTreeMap<String, AdditionalContextEntry>,
|
||||
expected_turn_id: Option<&str>,
|
||||
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
) -> Result<String, SteerInputError> {
|
||||
self.session
|
||||
.steer_input(input, expected_turn_id, responsesapi_client_metadata)
|
||||
.steer_input(
|
||||
input,
|
||||
additional_context,
|
||||
expected_turn_id,
|
||||
responsesapi_client_metadata,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1086,6 +1097,7 @@ impl Session {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
},
|
||||
/*mirror_user_text_to_realtime*/ None,
|
||||
@@ -3152,6 +3164,7 @@ impl Session {
|
||||
pub async fn steer_input(
|
||||
&self,
|
||||
input: Vec<UserInput>,
|
||||
additional_context: BTreeMap<String, AdditionalContextEntry>,
|
||||
expected_turn_id: Option<&str>,
|
||||
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
) -> Result<String, SteerInputError> {
|
||||
@@ -3192,16 +3205,10 @@ impl Session {
|
||||
return Err(SteerInputError::EmptyInput);
|
||||
}
|
||||
|
||||
self.input_queue
|
||||
.push_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
active_turn.turn_state.as_ref(),
|
||||
TurnInput::UserInput(input),
|
||||
)
|
||||
.await
|
||||
.map_err(|input| match input {
|
||||
TurnInput::UserInput(input) => SteerInputError::NoActiveTurn(input),
|
||||
TurnInput::ResponseInputItem(_) => unreachable!("steer input must be user input"),
|
||||
})?;
|
||||
let additional_context_input = {
|
||||
let mut state = self.state.lock().await;
|
||||
state.additional_context.merge(additional_context)
|
||||
};
|
||||
|
||||
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
|
||||
active_task
|
||||
@@ -3210,6 +3217,19 @@ impl Session {
|
||||
.set_responsesapi_client_metadata(responsesapi_client_metadata);
|
||||
}
|
||||
|
||||
let mut pending_input = additional_context_input
|
||||
.into_iter()
|
||||
.map(TurnInput::ResponseInputItem)
|
||||
.collect::<Vec<_>>();
|
||||
let rejected_input = input.clone();
|
||||
pending_input.push(TurnInput::UserInput(input));
|
||||
self.input_queue
|
||||
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
active_turn.turn_state.as_ref(),
|
||||
pending_input,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| SteerInputError::NoActiveTurn(rejected_input))?;
|
||||
Ok(active_turn_id.clone())
|
||||
}
|
||||
|
||||
|
||||
@@ -69,12 +69,17 @@ pub(super) async fn spawn_review_thread(
|
||||
.model_reasoning_summary
|
||||
.unwrap_or(model_info.default_reasoning_summary);
|
||||
let session_source = parent_turn_context.session_source.clone();
|
||||
let forked_from_thread_id = {
|
||||
let state = sess.state.lock().await;
|
||||
state.session_configuration.forked_from_thread_id
|
||||
};
|
||||
|
||||
let per_turn_config = Arc::new(per_turn_config);
|
||||
let review_turn_id = sub_id.to_string();
|
||||
let turn_metadata_state = Arc::new(TurnMetadataState::new(
|
||||
sess.session_id().to_string(),
|
||||
sess.thread_id().to_string(),
|
||||
forked_from_thread_id,
|
||||
parent_turn_context.thread_source,
|
||||
review_turn_id.clone(),
|
||||
#[allow(deprecated)]
|
||||
@@ -132,11 +137,11 @@ pub(super) async fn spawn_review_thread(
|
||||
};
|
||||
|
||||
// Seed the child task with the review prompt as the initial user message.
|
||||
let input: Vec<UserInput> = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: review_prompt,
|
||||
// Review prompt is synthesized; no UI element ranges to preserve.
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
let tc = Arc::new(review_turn_context);
|
||||
tc.turn_metadata_state.spawn_git_enrichment_task();
|
||||
// TODO(ccunningham): Review turns currently rely on `spawn_task` for TurnComplete but do not
|
||||
|
||||
@@ -95,6 +95,8 @@ pub(crate) struct SessionConfiguration {
|
||||
pub(super) app_server_client_version: Option<String>,
|
||||
/// Source of the session (cli, vscode, exec, mcp, ...)
|
||||
pub(super) session_source: SessionSource,
|
||||
/// Immediate history source copied into this thread, when this thread was forked.
|
||||
pub(super) forked_from_thread_id: Option<ThreadId>,
|
||||
/// Optional analytics source classification for this thread.
|
||||
pub(super) thread_source: Option<ThreadSource>,
|
||||
pub(super) dynamic_tools: Vec<DynamicToolSpec>,
|
||||
@@ -505,7 +507,10 @@ impl Session {
|
||||
session_configuration.collaboration_mode.model(),
|
||||
session_configuration.provider
|
||||
);
|
||||
let forked_from_id = initial_history.forked_from_id();
|
||||
let forked_from_id = session_configuration
|
||||
.forked_from_thread_id
|
||||
.or_else(|| initial_history.forked_from_id());
|
||||
session_configuration.forked_from_thread_id = forked_from_id;
|
||||
|
||||
let event_persistence_mode = if session_configuration.persist_extended_history {
|
||||
ThreadEventPersistenceMode::Extended
|
||||
|
||||
@@ -2287,6 +2287,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2333,6 +2334,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: ThreadSettingsOverrides {
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
collaboration_mode: Some(collaboration_mode),
|
||||
@@ -2999,6 +3001,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -3103,6 +3106,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -3630,6 +3634,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -4373,6 +4378,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -4482,6 +4488,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -4716,6 +4723,7 @@ async fn make_session_with_config_and_rx(
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -4819,6 +4827,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: session_source.clone(),
|
||||
forked_from_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -5430,6 +5439,7 @@ fn op_kind_for_input_and_context_ops() {
|
||||
items: vec![],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
}
|
||||
.kind(),
|
||||
@@ -5460,6 +5470,7 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
approval_policy: Some(config.permissions.approval_policy.value()),
|
||||
@@ -5781,10 +5792,10 @@ async fn spawn_task_turn_span_inherits_dispatch_trace_context() {
|
||||
async {
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
vec![UserInput::Text {
|
||||
vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}])],
|
||||
TraceCaptureTask {
|
||||
captured_trace: Arc::clone(&captured_trace),
|
||||
},
|
||||
@@ -6312,6 +6323,7 @@ where
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools,
|
||||
persist_extended_history: false,
|
||||
@@ -6592,10 +6604,10 @@ async fn spawn_task_does_not_update_previous_turn_settings_for_non_run_turn_task
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
sess.set_previous_turn_settings(/*previous_turn_settings*/ None)
|
||||
.await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
@@ -7861,10 +7873,10 @@ impl SessionTask for GuardianDeniedApprovalTask {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn guardian_auto_review_interrupts_after_three_consecutive_denials() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "trigger guardian denials".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
sess.spawn_task(Arc::clone(&tc), input, GuardianDeniedApprovalTask)
|
||||
.await;
|
||||
|
||||
@@ -7892,10 +7904,10 @@ async fn guardian_auto_review_interrupts_after_three_consecutive_denials() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn guardian_helper_review_interrupts_after_three_consecutive_denials() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "keep turn active for helper reviews".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
input,
|
||||
@@ -7952,10 +7964,10 @@ async fn guardian_helper_review_interrupts_after_three_consecutive_denials() {
|
||||
#[test_log::test]
|
||||
async fn abort_regular_task_emits_turn_aborted_only() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
input,
|
||||
@@ -7985,10 +7997,10 @@ async fn abort_regular_task_emits_turn_aborted_only() {
|
||||
#[tokio::test]
|
||||
async fn abort_gracefully_emits_turn_aborted_only() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
input,
|
||||
@@ -8018,10 +8030,10 @@ async fn abort_gracefully_emits_turn_aborted_only() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
input,
|
||||
@@ -8044,6 +8056,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
|
||||
}];
|
||||
sess.steer_input(
|
||||
pending_user_input.clone(),
|
||||
/*additional_context*/ Default::default(),
|
||||
Some(&tc.sub_id),
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
@@ -8140,7 +8153,10 @@ async fn steer_input_requires_active_turn() {
|
||||
|
||||
let err = sess
|
||||
.steer_input(
|
||||
input, /*expected_turn_id*/ None, /*responsesapi_client_metadata*/ None,
|
||||
input,
|
||||
/*additional_context*/ Default::default(),
|
||||
/*expected_turn_id*/ None,
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("steering without active turn should fail");
|
||||
@@ -8151,10 +8167,10 @@ async fn steer_input_requires_active_turn() {
|
||||
#[tokio::test]
|
||||
async fn terminal_active_turn_rejects_new_pending_input() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
input,
|
||||
@@ -8174,6 +8190,7 @@ async fn terminal_active_turn_rejects_new_pending_input() {
|
||||
}];
|
||||
sess.steer_input(
|
||||
pending_user_input.clone(),
|
||||
/*additional_context*/ Default::default(),
|
||||
Some(&tc.sub_id),
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
@@ -8209,6 +8226,7 @@ async fn terminal_active_turn_rejects_new_pending_input() {
|
||||
let err = sess
|
||||
.steer_input(
|
||||
steer_input,
|
||||
/*additional_context*/ Default::default(),
|
||||
Some(&tc.sub_id),
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
@@ -8237,10 +8255,10 @@ async fn terminal_active_turn_rejects_new_pending_input() {
|
||||
#[tokio::test]
|
||||
async fn steer_input_enforces_expected_turn_id() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
input,
|
||||
@@ -8258,6 +8276,7 @@ async fn steer_input_enforces_expected_turn_id() {
|
||||
let err = sess
|
||||
.steer_input(
|
||||
steer_input,
|
||||
/*additional_context*/ Default::default(),
|
||||
Some("different-turn-id"),
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
@@ -8282,10 +8301,10 @@ async fn steer_input_rejects_non_regular_turns() {
|
||||
(TaskKind::Compact, NonSteerableTurnKind::Compact),
|
||||
] {
|
||||
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
let turn_context = sess.new_default_turn_with_sub_id("turn".to_string()).await;
|
||||
sess.spawn_task(
|
||||
turn_context,
|
||||
@@ -8304,6 +8323,7 @@ async fn steer_input_rejects_non_regular_turns() {
|
||||
let err = sess
|
||||
.steer_input(
|
||||
steer_input,
|
||||
/*additional_context*/ Default::default(),
|
||||
/*expected_turn_id*/ None,
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
@@ -8319,10 +8339,10 @@ async fn steer_input_rejects_non_regular_turns() {
|
||||
#[tokio::test]
|
||||
async fn steer_input_returns_active_turn_id() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
input,
|
||||
@@ -8340,6 +8360,7 @@ async fn steer_input_returns_active_turn_id() {
|
||||
let turn_id = sess
|
||||
.steer_input(
|
||||
steer_input,
|
||||
/*additional_context*/ Default::default(),
|
||||
Some(&tc.sub_id),
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
@@ -8564,6 +8585,7 @@ async fn active_goal_continuation_runs_again_after_no_tool_turn() -> anyhow::Res
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -8669,6 +8691,7 @@ async fn pending_request_user_input_does_not_spawn_extra_goal_continuation() ->
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -9216,6 +9239,7 @@ async fn completed_goal_accounts_current_turn_tokens_before_tool_response() -> a
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -9382,6 +9406,7 @@ async fn steered_input_reopens_mailbox_delivery_for_current_turn() {
|
||||
text: "follow up".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
/*additional_context*/ Default::default(),
|
||||
Some(&tc.sub_id),
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
@@ -9431,6 +9456,7 @@ async fn stale_defer_mailbox_delivery_does_not_override_steered_input() {
|
||||
text: "follow up".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
/*additional_context*/ Default::default(),
|
||||
Some(&tc.sub_id),
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
@@ -9512,10 +9538,10 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn abort_review_task_emits_exited_then_aborted_and_records_history() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "start review".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
}])];
|
||||
sess.spawn_task(Arc::clone(&tc), input, ReviewTask::new())
|
||||
.await;
|
||||
|
||||
|
||||
@@ -692,6 +692,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
session_source: SessionSource::SubAgent(SubAgentSource::Other(
|
||||
GUARDIAN_REVIEWER_NAME.to_string(),
|
||||
)),
|
||||
forked_from_thread_id: None,
|
||||
thread_source: None,
|
||||
agent_control: AgentControl::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
|
||||
@@ -466,7 +466,7 @@ async fn run_hooks_and_record_inputs(
|
||||
record_additional_contexts(sess, turn_context, hook_outcome.additional_contexts).await;
|
||||
} else {
|
||||
accepted_input = true;
|
||||
if matches!(input_item, TurnInput::UserInput(_)) {
|
||||
if matches!(input_item, TurnInput::UserInput(items) if !items.is_empty()) {
|
||||
accepted_user_input = true;
|
||||
}
|
||||
record_pending_input(
|
||||
|
||||
@@ -482,6 +482,7 @@ impl Session {
|
||||
let turn_metadata_state = Arc::new(TurnMetadataState::new(
|
||||
session_id.to_string(),
|
||||
thread_id.to_string(),
|
||||
session_configuration.forked_from_thread_id,
|
||||
session_configuration.thread_source,
|
||||
sub_id.clone(),
|
||||
cwd.clone(),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::context::AdditionalContextDeveloperFragment;
|
||||
use crate::context::AdditionalContextUserFragment;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::protocol::AdditionalContextEntry;
|
||||
use codex_protocol::protocol::AdditionalContextKind;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub(crate) struct AdditionalContextStore {
|
||||
values: BTreeMap<String, AdditionalContextEntry>,
|
||||
}
|
||||
|
||||
impl AdditionalContextStore {
|
||||
pub(crate) fn merge(
|
||||
&mut self,
|
||||
values: BTreeMap<String, AdditionalContextEntry>,
|
||||
) -> Vec<ResponseInputItem> {
|
||||
let fragments = values
|
||||
.iter()
|
||||
.filter(|(key, value)| self.values.get(*key) != Some(*value))
|
||||
.map(|(key, entry)| match entry.kind {
|
||||
AdditionalContextKind::Untrusted => {
|
||||
AdditionalContextUserFragment::new(key.clone(), entry.value.clone())
|
||||
.into_response_input_item()
|
||||
}
|
||||
AdditionalContextKind::Application => {
|
||||
AdditionalContextDeveloperFragment::new(key.clone(), entry.value.clone())
|
||||
.into_response_input_item()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
self.values = values;
|
||||
fragments
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
mod additional_context;
|
||||
mod auto_compact_window;
|
||||
mod service;
|
||||
mod session;
|
||||
mod turn;
|
||||
|
||||
pub(crate) use additional_context::AdditionalContextStore;
|
||||
pub(crate) use auto_compact_window::AutoCompactWindowSnapshot;
|
||||
pub(crate) use service::SessionServices;
|
||||
pub(crate) use session::SessionState;
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::AdditionalContextStore;
|
||||
use super::auto_compact_window::AutoCompactWindow;
|
||||
use super::auto_compact_window::AutoCompactWindowSnapshot;
|
||||
use crate::context_manager::ContextManager;
|
||||
@@ -25,6 +26,7 @@ pub(crate) struct SessionState {
|
||||
pub(crate) latest_rate_limits: Option<RateLimitSnapshot>,
|
||||
pub(crate) server_reasoning_included: bool,
|
||||
pub(crate) mcp_dependency_prompted: HashSet<String>,
|
||||
pub(crate) additional_context: AdditionalContextStore,
|
||||
/// Settings used by the latest regular user turn, used for turn-to-turn
|
||||
/// model/realtime handling on subsequent regular turns (including full-context
|
||||
/// reinjection after resume or `/compact`).
|
||||
@@ -49,6 +51,7 @@ impl SessionState {
|
||||
latest_rate_limits: None,
|
||||
server_reasoning_included: false,
|
||||
mcp_dependency_prompted: HashSet::new(),
|
||||
additional_context: AdditionalContextStore::default(),
|
||||
previous_turn_settings: None,
|
||||
auto_compact_window: AutoCompactWindow::new(),
|
||||
startup_prewarm: None,
|
||||
|
||||
@@ -233,7 +233,7 @@ async fn record_stage1_output_usage_for_memory_citation(
|
||||
}
|
||||
|
||||
if let Some(db) = state_db_ctx {
|
||||
let _ = db.record_stage1_output_usage(&thread_ids).await;
|
||||
let _ = db.memories().record_stage1_output_usage(&thread_ids).await;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnCompleteEvent;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -303,7 +302,7 @@ impl Session {
|
||||
pub async fn spawn_task<T: SessionTask>(
|
||||
self: &Arc<Self>,
|
||||
turn_context: Arc<TurnContext>,
|
||||
input: Vec<UserInput>,
|
||||
input: Vec<TurnInput>,
|
||||
task: T,
|
||||
) {
|
||||
self.abort_all_tasks(TurnAbortReason::Replaced).await;
|
||||
@@ -314,7 +313,7 @@ impl Session {
|
||||
pub(crate) async fn start_task<T: SessionTask>(
|
||||
self: &Arc<Self>,
|
||||
turn_context: Arc<TurnContext>,
|
||||
input: Vec<UserInput>,
|
||||
input: Vec<TurnInput>,
|
||||
task: T,
|
||||
) {
|
||||
let task: Arc<dyn AnySessionTask> = Arc::new(task);
|
||||
@@ -382,11 +381,7 @@ impl Session {
|
||||
));
|
||||
let ctx = Arc::clone(&turn_context);
|
||||
let task_for_run = Arc::clone(&task);
|
||||
let task_input = if input.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![TurnInput::UserInput(input)]
|
||||
};
|
||||
let task_input = input;
|
||||
let task_cancellation_token = cancellation_token.child_token();
|
||||
// Task-owned turn spans keep a core-owned span open for the
|
||||
// full task lifecycle after the submission dispatch span ends.
|
||||
|
||||
@@ -581,6 +581,15 @@ impl ThreadManager {
|
||||
pub async fn start_thread_with_options(
|
||||
&self,
|
||||
options: StartThreadOptions,
|
||||
) -> CodexResult<NewThread> {
|
||||
self.start_thread_with_options_and_fork_source(options, /*forked_from_thread_id*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn start_thread_with_options_and_fork_source(
|
||||
&self,
|
||||
options: StartThreadOptions,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let session_source = options
|
||||
.session_source
|
||||
@@ -594,6 +603,7 @@ impl ThreadManager {
|
||||
Arc::clone(&self.state.auth_manager),
|
||||
self.agent_control(),
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
options.dynamic_tools,
|
||||
options.persist_extended_history,
|
||||
@@ -634,7 +644,8 @@ impl ThreadManager {
|
||||
history,
|
||||
InterruptedTurnHistoryMarker::from_config(&options.config),
|
||||
);
|
||||
self.start_thread_with_options(options).await
|
||||
self.start_thread_with_options_and_fork_source(options, Some(forked_from_thread_id))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn resume_thread_from_rollout(
|
||||
@@ -673,6 +684,7 @@ impl ThreadManager {
|
||||
initial_history,
|
||||
auth_manager,
|
||||
self.agent_control(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
persist_extended_history,
|
||||
@@ -698,6 +710,7 @@ impl ThreadManager {
|
||||
InitialHistory::New,
|
||||
Arc::clone(&self.state.auth_manager),
|
||||
self.agent_control(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*thread_source*/ None,
|
||||
Vec::new(),
|
||||
/*persist_extended_history*/ false,
|
||||
@@ -727,6 +740,7 @@ impl ThreadManager {
|
||||
initial_history,
|
||||
auth_manager,
|
||||
self.agent_control(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
/*persist_extended_history*/ false,
|
||||
@@ -876,6 +890,13 @@ impl ThreadManager {
|
||||
persist_extended_history: bool,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
) -> CodexResult<NewThread> {
|
||||
// `forked_from_id()` describes this history's existing lineage. When
|
||||
// forking a resumed thread, the child copies the resumed thread itself.
|
||||
let forked_from_thread_id = match &history {
|
||||
InitialHistory::Resumed(resumed) => Some(resumed.conversation_id),
|
||||
InitialHistory::Forked(_) => history.forked_from_id(),
|
||||
InitialHistory::New | InitialHistory::Cleared => None,
|
||||
};
|
||||
let interrupted_marker = InterruptedTurnHistoryMarker::from_config(&config);
|
||||
let history = fork_history_from_snapshot(snapshot, history, interrupted_marker);
|
||||
let environments = default_thread_environment_selections(
|
||||
@@ -887,6 +908,7 @@ impl ThreadManager {
|
||||
history,
|
||||
Arc::clone(&self.state.auth_manager),
|
||||
self.agent_control(),
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
persist_extended_history,
|
||||
@@ -1020,6 +1042,7 @@ impl ThreadManagerState {
|
||||
config,
|
||||
agent_control,
|
||||
self.session_source.clone(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*thread_source*/ None,
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
@@ -1036,6 +1059,7 @@ impl ThreadManagerState {
|
||||
config: Config,
|
||||
agent_control: AgentControl,
|
||||
session_source: SessionSource,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
thread_source: Option<ThreadSource>,
|
||||
persist_extended_history: bool,
|
||||
metrics_service_name: Option<String>,
|
||||
@@ -1052,6 +1076,7 @@ impl ThreadManagerState {
|
||||
Arc::clone(&self.auth_manager),
|
||||
agent_control,
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
persist_extended_history,
|
||||
@@ -1086,6 +1111,7 @@ impl ThreadManagerState {
|
||||
Arc::clone(&self.auth_manager),
|
||||
agent_control,
|
||||
session_source,
|
||||
/*forked_from_thread_id*/ None,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
/*persist_extended_history*/ false,
|
||||
@@ -1107,6 +1133,7 @@ impl ThreadManagerState {
|
||||
agent_control: AgentControl,
|
||||
session_source: SessionSource,
|
||||
thread_source: Option<ThreadSource>,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
persist_extended_history: bool,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||
@@ -1121,6 +1148,7 @@ impl ThreadManagerState {
|
||||
Arc::clone(&self.auth_manager),
|
||||
agent_control,
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
persist_extended_history,
|
||||
@@ -1142,6 +1170,7 @@ impl ThreadManagerState {
|
||||
initial_history: InitialHistory,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
agent_control: AgentControl,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
thread_source: Option<ThreadSource>,
|
||||
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
||||
persist_extended_history: bool,
|
||||
@@ -1156,6 +1185,7 @@ impl ThreadManagerState {
|
||||
auth_manager,
|
||||
agent_control,
|
||||
self.session_source.clone(),
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
dynamic_tools,
|
||||
persist_extended_history,
|
||||
@@ -1177,6 +1207,7 @@ impl ThreadManagerState {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
agent_control: AgentControl,
|
||||
session_source: SessionSource,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
thread_source: Option<ThreadSource>,
|
||||
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
||||
persist_extended_history: bool,
|
||||
@@ -1229,6 +1260,7 @@ impl ThreadManagerState {
|
||||
extensions: Arc::clone(&self.extensions),
|
||||
conversation_history: initial_history,
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
agent_control,
|
||||
dynamic_tools,
|
||||
|
||||
@@ -2592,6 +2592,7 @@ async fn send_input_accepts_structured_items() {
|
||||
],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
};
|
||||
let captured = manager
|
||||
|
||||
@@ -40,7 +40,7 @@ pub fn create_view_image_tool(options: ViewImageToolOptions) -> ToolSpec {
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: VIEW_IMAGE_TOOL_NAME.to_string(),
|
||||
description: "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within <image ...> tags)."
|
||||
description: "View a local image file from the filesystem when visual inspection is needed. Use this for images already available on disk."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
|
||||
@@ -238,11 +238,12 @@ fn spec_for_model_request(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hosted_model_tool_specs(turn_context: &TurnContext) -> Vec<ToolSpec> {
|
||||
fn hosted_model_tool_specs(context: &CoreToolPlanContext<'_>) -> Vec<ToolSpec> {
|
||||
let turn_context = context.turn_context;
|
||||
let mut specs = Vec::new();
|
||||
let provider_capabilities = turn_context.provider.capabilities();
|
||||
let web_search_mode = provider_capabilities
|
||||
.web_search
|
||||
let web_search_mode = (!standalone_web_run_available(context.extension_tool_executors)
|
||||
&& provider_capabilities.web_search)
|
||||
.then_some(turn_context.config.web_search_mode.value());
|
||||
let web_search_config = if provider_capabilities.web_search {
|
||||
turn_context.config.web_search_config.as_ref()
|
||||
@@ -504,11 +505,20 @@ fn add_tool_sources(context: &CoreToolPlanContext<'_>, planned_tools: &mut Plann
|
||||
add_mcp_runtime_tools(context, planned_tools);
|
||||
add_dynamic_tools(context, planned_tools);
|
||||
add_extension_tools(context, planned_tools);
|
||||
for spec in hosted_model_tool_specs(context.turn_context) {
|
||||
for spec in hosted_model_tool_specs(context) {
|
||||
planned_tools.add_hosted_spec(spec);
|
||||
}
|
||||
}
|
||||
|
||||
fn standalone_web_run_available(
|
||||
extension_tools: &[Arc<dyn ToolExecutor<ExtensionToolCall>>],
|
||||
) -> bool {
|
||||
let web_run = ToolName::namespaced("web", "run");
|
||||
extension_tools
|
||||
.iter()
|
||||
.any(|executor| executor.tool_name() == web_run)
|
||||
}
|
||||
|
||||
fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
|
||||
let turn_context = context.turn_context;
|
||||
let features = turn_context.features.get();
|
||||
|
||||
@@ -20,8 +20,11 @@ use codex_tools::DiscoverablePluginInfo;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use codex_tools::ToolCall as ExtensionToolCall;
|
||||
use codex_tools::ToolExecutor;
|
||||
use codex_tools::ToolExposure;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolOutput;
|
||||
use codex_tools::ToolSpec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
@@ -37,6 +40,7 @@ struct ToolPlanInputs {
|
||||
mcp_tools: Option<Vec<ToolInfo>>,
|
||||
deferred_mcp_tools: Option<Vec<ToolInfo>>,
|
||||
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
extension_tool_executors: Vec<Arc<dyn ToolExecutor<ExtensionToolCall>>>,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
}
|
||||
|
||||
@@ -176,7 +180,7 @@ async fn probe_with(
|
||||
mcp_tools: inputs.mcp_tools,
|
||||
deferred_mcp_tools: inputs.deferred_mcp_tools,
|
||||
discoverable_tools: inputs.discoverable_tools,
|
||||
extension_tool_executors: Vec::new(),
|
||||
extension_tool_executors: inputs.extension_tool_executors,
|
||||
dynamic_tools: inputs.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
@@ -253,6 +257,37 @@ fn use_bedrock_provider(turn: &mut TurnContext) {
|
||||
turn.provider = create_model_provider(provider_info, turn.auth_manager.clone());
|
||||
}
|
||||
|
||||
struct WebRunExtensionTool;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ToolExecutor<ExtensionToolCall> for WebRunExtensionTool {
|
||||
fn tool_name(&self) -> ToolName {
|
||||
ToolName::namespaced("web", "run")
|
||||
}
|
||||
|
||||
fn spec(&self) -> ToolSpec {
|
||||
ToolSpec::Namespace(codex_tools::ResponsesApiNamespace {
|
||||
name: "web".to_string(),
|
||||
description: "Test web namespace.".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "run".to_string(),
|
||||
description: "Test standalone web search tool.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: codex_tools::JsonSchema::default(),
|
||||
output_schema: None,
|
||||
})],
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
_call: ExtensionToolCall,
|
||||
) -> Result<Box<dyn ToolOutput>, codex_tools::FunctionCallError> {
|
||||
Ok(Box::new(codex_tools::JsonToolOutput::new(json!({}))))
|
||||
}
|
||||
}
|
||||
|
||||
fn duplicate_primary_environment(turn: &mut TurnContext) {
|
||||
let mut second_environment = turn.environments.turn_environments[0].clone();
|
||||
second_environment.environment_id = "secondary".to_string();
|
||||
@@ -947,6 +982,26 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() {
|
||||
}
|
||||
);
|
||||
|
||||
let standalone_web_search_without_web_run = probe(|turn| {
|
||||
set_feature(turn, Feature::StandaloneWebSearch, /*enabled*/ true);
|
||||
set_web_search_mode(turn, WebSearchMode::Live);
|
||||
})
|
||||
.await;
|
||||
standalone_web_search_without_web_run.assert_visible_contains(&["web_search"]);
|
||||
|
||||
let standalone_web_search = probe_with(
|
||||
|turn| {
|
||||
set_feature(turn, Feature::StandaloneWebSearch, /*enabled*/ true);
|
||||
set_web_search_mode(turn, WebSearchMode::Live);
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![Arc::new(WebRunExtensionTool)],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
standalone_web_search.assert_visible_lacks(&["web_search"]);
|
||||
|
||||
let unsupported_provider = probe(|turn| {
|
||||
set_web_search_mode(turn, WebSearchMode::Live);
|
||||
use_bedrock_provider(turn);
|
||||
|
||||
@@ -16,6 +16,7 @@ use codex_git_utils::get_git_remote_urls_assume_git_repo;
|
||||
use codex_git_utils::get_git_repo_root;
|
||||
use codex_git_utils::get_has_changes;
|
||||
use codex_git_utils::get_head_commit_hash;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
@@ -74,6 +75,8 @@ pub(crate) struct TurnMetadataBag {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thread_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thread_source: Option<ThreadSource>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
turn_id: Option<String>,
|
||||
@@ -107,7 +110,14 @@ fn merge_turn_metadata(
|
||||
}
|
||||
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
|
||||
for (key, value) in responsesapi_client_metadata {
|
||||
if key == TURN_STARTED_AT_UNIX_MS_KEY {
|
||||
if matches!(
|
||||
key.as_str(),
|
||||
"session_id"
|
||||
| "thread_id"
|
||||
| "turn_id"
|
||||
| TURN_STARTED_AT_UNIX_MS_KEY
|
||||
| "forked_from_thread_id"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
metadata
|
||||
@@ -118,32 +128,6 @@ fn merge_turn_metadata(
|
||||
to_ascii_json_string(&metadata).ok()
|
||||
}
|
||||
|
||||
fn build_turn_metadata_bag(
|
||||
session_id: Option<String>,
|
||||
thread_id: Option<String>,
|
||||
thread_source: Option<ThreadSource>,
|
||||
turn_id: Option<String>,
|
||||
sandbox: Option<String>,
|
||||
repo_root: Option<String>,
|
||||
workspace_git_metadata: Option<WorkspaceGitMetadata>,
|
||||
) -> TurnMetadataBag {
|
||||
let mut workspaces = BTreeMap::new();
|
||||
if let (Some(repo_root), Some(workspace_git_metadata)) = (repo_root, workspace_git_metadata)
|
||||
&& !workspace_git_metadata.is_empty()
|
||||
{
|
||||
workspaces.insert(repo_root, workspace_git_metadata.into());
|
||||
}
|
||||
|
||||
TurnMetadataBag {
|
||||
session_id,
|
||||
thread_id,
|
||||
thread_source,
|
||||
turn_id,
|
||||
workspaces,
|
||||
sandbox,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_turn_metadata_header(
|
||||
cwd: &AbsolutePathBuf,
|
||||
sandbox: Option<&str>,
|
||||
@@ -164,20 +148,24 @@ pub async fn build_turn_metadata_header(
|
||||
return None;
|
||||
}
|
||||
|
||||
build_turn_metadata_bag(
|
||||
/*session_id*/ None,
|
||||
/*thread_id*/ None,
|
||||
/*thread_source*/ None,
|
||||
/*turn_id*/ None,
|
||||
sandbox.map(ToString::to_string),
|
||||
repo_root,
|
||||
Some(WorkspaceGitMetadata {
|
||||
associated_remote_urls,
|
||||
latest_git_commit_hash,
|
||||
has_changes,
|
||||
}),
|
||||
)
|
||||
.to_header_value()
|
||||
let workspace_git_metadata = WorkspaceGitMetadata {
|
||||
associated_remote_urls,
|
||||
latest_git_commit_hash,
|
||||
has_changes,
|
||||
};
|
||||
let mut metadata = TurnMetadataBag {
|
||||
sandbox: sandbox.map(ToString::to_string),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(repo_root) = repo_root
|
||||
&& !workspace_git_metadata.is_empty()
|
||||
{
|
||||
metadata
|
||||
.workspaces
|
||||
.insert(repo_root, workspace_git_metadata.into());
|
||||
}
|
||||
|
||||
metadata.to_header_value()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -198,6 +186,7 @@ impl TurnMetadataState {
|
||||
pub(crate) fn new(
|
||||
session_id: String,
|
||||
thread_id: String,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
thread_source: Option<ThreadSource>,
|
||||
turn_id: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
@@ -214,15 +203,15 @@ impl TurnMetadataState {
|
||||
)
|
||||
.to_string(),
|
||||
);
|
||||
let base_metadata = build_turn_metadata_bag(
|
||||
Some(session_id),
|
||||
Some(thread_id),
|
||||
let base_metadata = TurnMetadataBag {
|
||||
session_id: Some(session_id),
|
||||
thread_id: Some(thread_id),
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
Some(turn_id),
|
||||
turn_id: Some(turn_id),
|
||||
sandbox,
|
||||
/*repo_root*/ None,
|
||||
/*workspace_git_metadata*/ None,
|
||||
);
|
||||
..Default::default()
|
||||
};
|
||||
let base_header = base_metadata
|
||||
.to_header_value()
|
||||
.unwrap_or_else(|| "{}".to_string());
|
||||
@@ -346,19 +335,13 @@ impl TurnMetadataState {
|
||||
let Some(repo_root) = state.repo_root.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let enriched_metadata = build_turn_metadata_bag(
|
||||
state.base_metadata.session_id.clone(),
|
||||
state.base_metadata.thread_id.clone(),
|
||||
state.base_metadata.thread_source,
|
||||
state.base_metadata.turn_id.clone(),
|
||||
state.base_metadata.sandbox.clone(),
|
||||
Some(repo_root),
|
||||
Some(workspace_git_metadata),
|
||||
);
|
||||
if enriched_metadata.workspaces.is_empty() {
|
||||
if workspace_git_metadata.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut enriched_metadata = state.base_metadata.clone();
|
||||
enriched_metadata
|
||||
.workspaces
|
||||
.insert(repo_root, workspace_git_metadata.into());
|
||||
|
||||
if let Some(header_value) = enriched_metadata.to_header_value() {
|
||||
*state
|
||||
|
||||
@@ -93,6 +93,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() {
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
@@ -128,6 +129,7 @@ fn turn_metadata_state_uses_explicit_subagent_thread_source() {
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
Some(ThreadSource::Subagent),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
@@ -143,6 +145,35 @@ fn turn_metadata_state_uses_explicit_subagent_thread_source() {
|
||||
assert!(json.get("session_source").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_includes_root_fork_lineage() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = temp_dir.path().abs();
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
let source_thread_id =
|
||||
ThreadId::from_string("11111111-1111-4111-8111-111111111111").expect("thread id");
|
||||
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
Some(source_thread_id),
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert_eq!(
|
||||
json["forked_from_thread_id"].as_str(),
|
||||
Some("11111111-1111-4111-8111-111111111111")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
@@ -152,6 +183,7 @@ fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
@@ -179,6 +211,7 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta(
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*thread_source*/ None,
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
@@ -224,6 +257,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*thread_source*/ None,
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
@@ -266,7 +300,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_ignores_client_turn_started_at_unix_ms_before_start() {
|
||||
fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = temp_dir.path().abs();
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
@@ -274,6 +308,7 @@ fn turn_metadata_state_ignores_client_turn_started_at_unix_ms_before_start() {
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
@@ -281,15 +316,22 @@ fn turn_metadata_state_ignores_client_turn_started_at_unix_ms_before_start() {
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
state.set_responsesapi_client_metadata(HashMap::from([(
|
||||
"turn_started_at_unix_ms".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
)]));
|
||||
state.set_responsesapi_client_metadata(HashMap::from([
|
||||
(
|
||||
"turn_started_at_unix_ms".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
),
|
||||
(
|
||||
"forked_from_thread_id".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
),
|
||||
]));
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert!(json.get("turn_started_at_unix_ms").is_none());
|
||||
assert!(json.get("forked_from_thread_id").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -297,10 +339,13 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = temp_dir.path().abs();
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
let source_thread_id =
|
||||
ThreadId::from_string("44444444-4444-4444-8444-444444444444").expect("thread id");
|
||||
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
Some(source_thread_id),
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
@@ -318,6 +363,10 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
),
|
||||
("session_id".to_string(), "client-supplied".to_string()),
|
||||
("thread_id".to_string(), "client-supplied".to_string()),
|
||||
(
|
||||
"forked_from_thread_id".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
),
|
||||
("thread_source".to_string(), "client-supplied".to_string()),
|
||||
(
|
||||
"turn_started_at_unix_ms".to_string(),
|
||||
@@ -337,6 +386,10 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
assert_eq!(json["reasoning_effort"].as_str(), Some("client-supplied"));
|
||||
assert_eq!(json["session_id"].as_str(), Some("session-a"));
|
||||
assert_eq!(json["thread_id"].as_str(), Some("thread-a"));
|
||||
assert_eq!(
|
||||
json["forked_from_thread_id"].as_str(),
|
||||
Some("44444444-4444-4444-8444-444444444444")
|
||||
);
|
||||
assert_eq!(json["thread_source"].as_str(), Some("user"));
|
||||
assert_eq!(json["turn_id"].as_str(), Some("turn-a"));
|
||||
assert_eq!(
|
||||
|
||||
@@ -767,6 +767,7 @@ impl TestCodex {
|
||||
environments,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(self.config.cwd.to_path_buf()),
|
||||
approval_policy: Some(approval_policy),
|
||||
|
||||
@@ -53,6 +53,7 @@ async fn interrupt_long_running_tool_emits_turn_aborted() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -110,6 +111,7 @@ async fn interrupt_tool_records_history_entries() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -131,6 +133,7 @@ async fn interrupt_tool_records_history_entries() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -214,6 +217,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -235,6 +239,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -0,0 +1,581 @@
|
||||
use anyhow::Result;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::protocol::AdditionalContextEntry;
|
||||
use codex_protocol::protocol::AdditionalContextKind;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::context_snapshot;
|
||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||
use core_test_support::context_snapshot::ContextSnapshotRenderMode;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event_match;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let request = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| config.include_environment_context = false)
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "inspect the active tab".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: BTreeMap::from([
|
||||
(
|
||||
"browser_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "tab one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
(
|
||||
"automation_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "run one".to_string(),
|
||||
kind: AdditionalContextKind::Application,
|
||||
},
|
||||
),
|
||||
]),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let user_item = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
item: TurnItem::UserMessage(item),
|
||||
..
|
||||
}) => Some(item.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
user_item.content,
|
||||
vec![UserInput::Text {
|
||||
text: "inspect the active tab".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
);
|
||||
wait_for_event_match(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_)).then_some(())
|
||||
})
|
||||
.await;
|
||||
|
||||
let request = request.single_request();
|
||||
insta::assert_snapshot!(
|
||||
"additional_context_simple_input",
|
||||
context_snapshot::format_labeled_requests_snapshot(
|
||||
"additional context is inserted before the user turn input.",
|
||||
&[("Request", &request)],
|
||||
&ContextSnapshotOptions::default()
|
||||
.strip_capability_instructions()
|
||||
.render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 160 }),
|
||||
)
|
||||
);
|
||||
let developer_context_texts = request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<automation_info>"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
developer_context_texts,
|
||||
vec!["<automation_info>run one</automation_info>"]
|
||||
);
|
||||
assert_eq!(
|
||||
request.message_input_texts("user"),
|
||||
vec![
|
||||
"<external_browser_info>tab one</external_browser_info>",
|
||||
"inspect the active tab",
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn external_context_like_user_text_remains_a_user_message_item() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let request = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| config.include_environment_context = false)
|
||||
.build(&server)
|
||||
.await?;
|
||||
let user_input = UserInput::Text {
|
||||
text: "<external_api>".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
};
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![user_input.clone()],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: BTreeMap::new(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let user_item = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
item: TurnItem::UserMessage(item),
|
||||
..
|
||||
}) => Some(item.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(user_item.content, vec![user_input]);
|
||||
wait_for_event_match(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_)).then_some(())
|
||||
})
|
||||
.await;
|
||||
|
||||
let request = request.single_request();
|
||||
assert_eq!(request.message_input_texts("user"), vec!["<external_api>"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn additional_context_trust_controls_message_role() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let request = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| config.include_environment_context = false)
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "inspect context".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: BTreeMap::from([
|
||||
(
|
||||
"browser_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "tab one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
(
|
||||
"automation_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "run one".to_string(),
|
||||
kind: AdditionalContextKind::Application,
|
||||
},
|
||||
),
|
||||
]),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event_match(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_)).then_some(())
|
||||
})
|
||||
.await;
|
||||
|
||||
let request = request.single_request();
|
||||
let developer_context_texts = request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<automation_info>"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
developer_context_texts,
|
||||
vec!["<automation_info>run one</automation_info>"]
|
||||
);
|
||||
assert_eq!(
|
||||
request.message_input_texts("user"),
|
||||
vec![
|
||||
"<external_browser_info>tab one</external_browser_info>",
|
||||
"inspect context",
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn additional_context_is_deduplicated_between_turns_while_retained() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let first_request = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let second_request = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| config.include_environment_context = false)
|
||||
.build(&server)
|
||||
.await?;
|
||||
let additional_context = BTreeMap::from([(
|
||||
"browser_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "same tab".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
)]);
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "first turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: additional_context.clone(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event_match(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_)).then_some(())
|
||||
})
|
||||
.await;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "second turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context,
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event_match(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_)).then_some(())
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
first_request.single_request().message_input_texts("user"),
|
||||
vec![
|
||||
"<external_browser_info>same tab</external_browser_info>",
|
||||
"first turn",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
second_request.single_request().message_input_texts("user"),
|
||||
vec![
|
||||
"<external_browser_info>same tab</external_browser_info>",
|
||||
"first turn",
|
||||
"second turn",
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn additional_context_removes_one_value_while_adding_another() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let first_request = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let second_request = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
|
||||
)
|
||||
.await;
|
||||
let third_request = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-3"), ev_completed("resp-3")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| config.include_environment_context = false)
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "first turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: BTreeMap::from([
|
||||
(
|
||||
"automation_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "run one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
(
|
||||
"browser_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "tab one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
]),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event_match(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_)).then_some(())
|
||||
})
|
||||
.await;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "second turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: BTreeMap::from([
|
||||
(
|
||||
"automation_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "run one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
(
|
||||
"terminal_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "pty one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
]),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event_match(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_)).then_some(())
|
||||
})
|
||||
.await;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "third turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: BTreeMap::from([
|
||||
(
|
||||
"automation_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "run one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
(
|
||||
"browser_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "tab one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
(
|
||||
"terminal_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: "pty one".to_string(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
]),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event_match(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_)).then_some(())
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
first_request.single_request().message_input_texts("user"),
|
||||
vec![
|
||||
"<external_automation_info>run one</external_automation_info>",
|
||||
"<external_browser_info>tab one</external_browser_info>",
|
||||
"first turn",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
second_request.single_request().message_input_texts("user"),
|
||||
vec![
|
||||
"<external_automation_info>run one</external_automation_info>",
|
||||
"<external_browser_info>tab one</external_browser_info>",
|
||||
"first turn",
|
||||
"<external_terminal_info>pty one</external_terminal_info>",
|
||||
"second turn",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
third_request.single_request().message_input_texts("user"),
|
||||
vec![
|
||||
"<external_automation_info>run one</external_automation_info>",
|
||||
"<external_browser_info>tab one</external_browser_info>",
|
||||
"first turn",
|
||||
"<external_terminal_info>pty one</external_terminal_info>",
|
||||
"second turn",
|
||||
"<external_browser_info>tab one</external_browser_info>",
|
||||
"third turn",
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn additional_context_values_are_truncated_before_model_input() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
const MAX_EXPECTED_EXTERNAL_CONTEXT_TEXT_BYTES: usize = 5 * 1024;
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let request = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| config.include_environment_context = false)
|
||||
.build(&server)
|
||||
.await?;
|
||||
let long_browser_value = format!("browser-head-{}browser-tail", "b".repeat(40_000));
|
||||
let long_automation_value = format!("automation-head-{}automation-tail", "a".repeat(40_000));
|
||||
let untruncated_browser_fragment =
|
||||
format!("<external_browser_info>{long_browser_value}</external_browser_info>");
|
||||
let untruncated_automation_fragment =
|
||||
format!("<automation_info>{long_automation_value}</automation_info>");
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "summarize context".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: BTreeMap::from([
|
||||
(
|
||||
"automation_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: long_automation_value.clone(),
|
||||
kind: AdditionalContextKind::Application,
|
||||
},
|
||||
),
|
||||
(
|
||||
"browser_info".to_string(),
|
||||
AdditionalContextEntry {
|
||||
value: long_browser_value.clone(),
|
||||
kind: AdditionalContextKind::Untrusted,
|
||||
},
|
||||
),
|
||||
]),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event_match(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_)).then_some(())
|
||||
})
|
||||
.await;
|
||||
|
||||
let request = request.single_request();
|
||||
let developer_texts = request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<automation_info>"))
|
||||
.collect::<Vec<_>>();
|
||||
let [automation_text] = developer_texts.as_slice() else {
|
||||
panic!("expected application additional context, got {developer_texts:?}");
|
||||
};
|
||||
assert!(automation_text.starts_with(&format!(
|
||||
"<automation_info>automation-head-{}",
|
||||
"a".repeat(1024)
|
||||
)));
|
||||
assert!(automation_text.contains("tokens truncated"));
|
||||
assert!(automation_text.ends_with("automation-tail</automation_info>"));
|
||||
assert!(automation_text.len() < untruncated_automation_fragment.len());
|
||||
assert!(
|
||||
automation_text.len() <= MAX_EXPECTED_EXTERNAL_CONTEXT_TEXT_BYTES,
|
||||
"application additional context was not capped before model input: {} bytes",
|
||||
automation_text.len()
|
||||
);
|
||||
|
||||
let user_texts = request.message_input_texts("user");
|
||||
let [external_text, user_text] = user_texts.as_slice() else {
|
||||
panic!("expected external context plus user input, got {user_texts:?}");
|
||||
};
|
||||
assert_eq!(user_text, "summarize context");
|
||||
assert!(external_text.starts_with(&format!(
|
||||
"<external_browser_info>browser-head-{}",
|
||||
"b".repeat(1024)
|
||||
)));
|
||||
assert!(external_text.contains("tokens truncated"));
|
||||
assert!(external_text.ends_with("browser-tail</external_browser_info>"));
|
||||
assert!(external_text.len() < untruncated_browser_fragment.len());
|
||||
assert!(
|
||||
external_text.len() <= MAX_EXPECTED_EXTERNAL_CONTEXT_TEXT_BYTES,
|
||||
"untrusted additional context was not capped before model input: {} bytes",
|
||||
external_text.len()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -91,6 +91,7 @@ async fn submit_without_wait_with_turn_permissions(
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(harness.cwd().to_path_buf()),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
|
||||
@@ -653,6 +653,7 @@ async fn submit_turn(
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.cwd.path().to_path_buf()),
|
||||
approval_policy: Some(approval_policy),
|
||||
@@ -2598,6 +2599,7 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> {
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.cwd.path().to_path_buf()),
|
||||
approval_policy: Some(approval_policy),
|
||||
|
||||
@@ -392,6 +392,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -758,6 +759,7 @@ async fn includes_session_id_thread_id_and_model_headers_in_request() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -969,6 +971,7 @@ async fn includes_base_instructions_override_in_request() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1026,6 +1029,7 @@ async fn chatgpt_auth_sends_correct_request() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1150,6 +1154,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1189,6 +1194,7 @@ async fn includes_user_instructions_message_in_request() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1277,6 +1283,7 @@ async fn includes_apps_guidance_as_developer_message_for_chatgpt_auth() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1340,6 +1347,7 @@ async fn omits_apps_guidance_for_api_key_auth_even_when_feature_enabled() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1399,6 +1407,7 @@ async fn omits_apps_guidance_when_configured_off() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1441,6 +1450,7 @@ async fn omits_environment_context_when_configured_off() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1498,6 +1508,7 @@ async fn skills_append_to_developer_message() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1581,6 +1592,7 @@ async fn skills_use_aliases_in_developer_message_under_budget_pressure() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1642,6 +1654,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1684,6 +1697,7 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1727,6 +1741,7 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1778,6 +1793,7 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
approval_policy: Some(config.permissions.approval_policy.value()),
|
||||
@@ -1834,6 +1850,7 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1899,6 +1916,7 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default()
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
approval_policy: Some(config.permissions.approval_policy.value()),
|
||||
@@ -1959,6 +1977,7 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2018,6 +2037,7 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2057,6 +2077,7 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2105,6 +2126,7 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2152,6 +2174,7 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2204,6 +2227,7 @@ async fn includes_developer_instructions_message_in_request() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2500,6 +2524,7 @@ async fn token_count_includes_rate_limits_snapshot() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2639,6 +2664,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2716,6 +2742,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2731,6 +2758,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2816,6 +2844,7 @@ async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Res
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2927,6 +2956,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3016,6 +3046,7 @@ async fn env_var_overrides_loaded_auth() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3073,6 +3104,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3089,6 +3121,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3105,6 +3138,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -1319,6 +1319,7 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1408,6 +1409,7 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -2861,7 +2861,7 @@ text(JSON.stringify(tool));
|
||||
parsed,
|
||||
serde_json::json!({
|
||||
"name": "view_image",
|
||||
"description": "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within <image ...> tags).\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: {\n // Local filesystem path to an image file\n path: string;\n}): Promise<{\n // Image detail hint returned by view_image. Returns `high` for default resized behavior or `original` when original resolution is preserved.\n detail: \"high\" | \"original\";\n // Data URL for the loaded image.\n image_url: string;\n}>; };\n```",
|
||||
"description": "View a local image file from the filesystem when visual inspection is needed. Use this for images already available on disk.\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: {\n // Local filesystem path to an image file\n path: string;\n}): Promise<{\n // Image detail hint returned by view_image. Returns `high` for default resized behavior or `original` when original resolution is preserved.\n detail: \"high\" | \"original\";\n // Data URL for the loaded image.\n image_url: string;\n}>; };\n```",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -2991,6 +2991,7 @@ text(
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
|
||||
@@ -79,6 +79,7 @@ async fn no_collaboration_instructions_by_default() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -133,6 +134,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -170,6 +172,7 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> {
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.config.cwd.to_path_buf()),
|
||||
approval_policy: Some(test.config.permissions.approval_policy.value()),
|
||||
@@ -220,6 +223,7 @@ async fn collaboration_instructions_omitted_when_disabled() -> Result<()> {
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.config.cwd.to_path_buf()),
|
||||
approval_policy: Some(test.config.permissions.approval_policy.value()),
|
||||
@@ -279,6 +283,7 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -327,6 +332,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.config.cwd.to_path_buf()),
|
||||
approval_policy: Some(test.config.permissions.approval_policy.value()),
|
||||
@@ -391,6 +397,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -414,6 +421,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -466,6 +474,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -489,6 +498,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -543,6 +553,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -569,6 +580,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -624,6 +636,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -650,6 +663,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -708,6 +722,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -724,6 +739,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -776,6 +792,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -98,6 +98,7 @@ fn disabled_permission_user_turn(text: impl Into<String>, cwd: PathBuf, model: S
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
@@ -409,6 +410,7 @@ async fn summarize_context_three_requests_and_instructions() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -434,6 +436,7 @@ async fn summarize_context_three_requests_and_instructions() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -611,6 +614,7 @@ async fn manual_pre_compact_block_decision_does_not_block_compaction() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -684,6 +688,7 @@ async fn compact_hooks_respect_matchers_and_post_runs_after_compaction() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -754,6 +759,7 @@ async fn manual_compact_uses_custom_prompt() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -901,6 +907,7 @@ async fn manual_compact_emits_context_compaction_items() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1067,6 +1074,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1566,6 +1574,7 @@ async fn auto_compact_stops_after_consecutive_no_progress_compactions() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1654,6 +1663,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1670,6 +1680,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1686,6 +1697,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1857,6 +1869,7 @@ async fn auto_compact_emits_context_compaction_items() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1938,6 +1951,7 @@ async fn auto_compact_starts_after_turn_started() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1953,6 +1967,7 @@ async fn auto_compact_starts_after_turn_started() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1968,6 +1983,7 @@ async fn auto_compact_starts_after_turn_started() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2496,6 +2512,7 @@ async fn auto_compact_persists_rollout_entries() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2511,6 +2528,7 @@ async fn auto_compact_persists_rollout_entries() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2526,6 +2544,7 @@ async fn auto_compact_persists_rollout_entries() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2615,6 +2634,7 @@ async fn manual_compact_retries_after_context_window_error() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2719,6 +2739,7 @@ async fn manual_compact_non_context_failure_retries_then_emits_task_error() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2814,6 +2835,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2832,6 +2854,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -2850,6 +2873,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3014,6 +3038,7 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3119,6 +3144,7 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3550,6 +3576,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3669,6 +3696,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3731,6 +3759,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3763,6 +3792,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
|
||||
],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3952,6 +3982,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -3967,6 +3998,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -4040,6 +4072,7 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -326,6 +326,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -343,6 +344,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -518,6 +520,7 @@ async fn assert_remote_manual_compact_request_parity(
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -538,6 +541,7 @@ async fn assert_remote_manual_compact_request_parity(
|
||||
],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -552,6 +556,7 @@ async fn assert_remote_manual_compact_request_parity(
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -572,6 +577,7 @@ async fn assert_remote_manual_compact_request_parity(
|
||||
],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -586,6 +592,7 @@ async fn assert_remote_manual_compact_request_parity(
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -754,6 +761,7 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<(
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -771,6 +779,7 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<(
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -874,6 +883,7 @@ async fn remote_compact_v2_retries_failures_with_stream_retry_budget() -> Result
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -891,6 +901,7 @@ async fn remote_compact_v2_retries_failures_with_stream_retry_budget() -> Result
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -977,6 +988,7 @@ async fn remote_compact_v2_accepts_additional_output_items_before_compaction() -
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -994,6 +1006,7 @@ async fn remote_compact_v2_accepts_additional_output_items_before_compaction() -
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1085,6 +1098,7 @@ async fn remote_compact_filters_deferred_dynamic_tools() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1157,6 +1171,7 @@ async fn remote_compact_runs_automatically() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1238,6 +1253,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1252,6 +1268,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1370,6 +1387,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1384,6 +1402,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1404,6 +1423,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1504,6 +1524,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1518,6 +1539,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1612,6 +1634,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1629,6 +1652,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1720,6 +1744,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1737,6 +1762,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1808,6 +1834,7 @@ async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1889,6 +1916,7 @@ async fn remote_manual_compact_failure_emits_task_error_event() -> Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1973,6 +2001,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2116,6 +2145,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2134,6 +2164,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2159,6 +2190,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2255,6 +2287,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2272,6 +2305,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2344,6 +2378,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2358,6 +2393,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2425,6 +2461,7 @@ async fn remote_request_uses_custom_experimental_realtime_start_instructions() -
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2486,6 +2523,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2502,6 +2540,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2577,6 +2616,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2594,6 +2634,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2677,6 +2718,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2693,6 +2735,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2784,6 +2827,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() ->
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2814,6 +2858,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() ->
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2903,6 +2948,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -2990,6 +3036,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -3012,6 +3059,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -3131,6 +3179,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -3145,6 +3194,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -3230,6 +3280,7 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -3308,6 +3359,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -3394,6 +3446,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -3411,6 +3464,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -3493,6 +3547,7 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -606,6 +606,7 @@ async fn submit_user_input(codex: &codex_core::CodexThread, items: Vec<UserInput
|
||||
items,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -783,6 +783,7 @@ async fn user_turn(conversation: &Arc<CodexThread>, text: &str) {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -54,6 +54,7 @@ async fn submit_user_turn(
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.cwd_path().to_path_buf()),
|
||||
approval_policy: Some(approval_policy),
|
||||
@@ -141,6 +142,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.cwd_path().to_path_buf()),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
|
||||
@@ -57,6 +57,7 @@ async fn fork_thread_twice_drops_to_first_message() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -181,6 +182,7 @@ async fn fork_thread_from_history_does_not_require_source_rollout_path() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -1850,6 +1850,7 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1869,6 +1870,7 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -127,6 +127,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
@@ -223,6 +224,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
|
||||
@@ -58,6 +58,7 @@ fn disabled_plan_turn(
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
@@ -119,6 +120,7 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> {
|
||||
items: vec![expected_input.clone()],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -178,6 +180,7 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -239,6 +242,7 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -301,6 +305,7 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -381,6 +386,7 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -468,6 +474,7 @@ async fn image_generation_call_event_is_emitted_when_image_save_fails() -> anyho
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -524,6 +531,7 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1108,6 +1116,7 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -1163,6 +1172,7 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -83,6 +83,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> {
|
||||
environments: None,
|
||||
final_output_json_schema: Some(serde_json::from_str(SCHEMA)?),
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
|
||||
@@ -76,6 +76,7 @@ async fn submit_user_turn(
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.cwd.path().to_path_buf()),
|
||||
approval_policy: Some(approval_policy),
|
||||
|
||||
@@ -29,6 +29,7 @@ pub static CODEX_ALIASES_TEMP_DIR: Option<TestBinaryDispatchGuard> = {
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod abort_tasks;
|
||||
mod additional_context;
|
||||
mod agent_jobs;
|
||||
mod agent_websocket;
|
||||
mod agents_md;
|
||||
|
||||
@@ -48,6 +48,7 @@ fn read_only_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.cwd_path().to_path_buf()),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
|
||||
@@ -125,6 +125,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(first_turn_cwd),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
@@ -160,6 +161,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(preturn_context_diff_cwd),
|
||||
approval_policy: Some(AskForApproval::OnRequest),
|
||||
@@ -249,6 +251,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd_one.clone()),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
@@ -282,6 +285,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd_two),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
@@ -365,6 +369,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -406,6 +411,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(resume_override_cwd),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
@@ -479,6 +485,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -520,6 +527,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -100,6 +100,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> {
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.cwd_path().to_path_buf()),
|
||||
approval_policy: Some(codex_protocol::protocol::AskForApproval::Never),
|
||||
|
||||
@@ -112,6 +112,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd_path),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
|
||||
@@ -122,6 +122,7 @@ async fn responses_api_emits_api_request_event() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -168,6 +169,7 @@ async fn process_sse_emits_tracing_for_output_item() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -214,6 +216,7 @@ async fn process_sse_emits_failed_event_on_parse_error() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -261,6 +264,7 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -328,6 +332,7 @@ async fn process_sse_failed_event_records_response_error_message() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -393,6 +398,7 @@ async fn process_sse_failed_event_logs_parse_error() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -445,6 +451,7 @@ async fn process_sse_failed_event_logs_missing_error() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -506,6 +513,7 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -561,6 +569,7 @@ async fn process_sse_emits_completed_telemetry() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -640,6 +649,7 @@ async fn turn_and_completed_response_spans_record_token_usage() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -728,6 +738,7 @@ async fn handle_responses_span_records_response_kind_and_tool_name() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -822,6 +833,7 @@ async fn record_responses_sets_span_fields_for_response_events() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -911,6 +923,7 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -987,6 +1000,7 @@ async fn handle_response_item_records_tool_result_for_function_call() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1064,6 +1078,7 @@ async fn handle_response_item_records_tool_result_for_shell_command_call() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1173,6 +1188,7 @@ async fn handle_shell_command_autoapprove_from_config_records_tool_decision() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1228,6 +1244,7 @@ async fn handle_shell_command_user_approved_records_tool_decision() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1298,6 +1315,7 @@ async fn handle_shell_command_user_approved_for_session_records_tool_decision()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1368,6 +1386,7 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1438,6 +1457,7 @@ async fn handle_shell_command_user_denies_records_tool_decision() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1508,6 +1528,7 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision()
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -1579,6 +1600,7 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -166,6 +166,7 @@ async fn submit_user_input(codex: &CodexThread, text: &str) {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -184,6 +185,7 @@ async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) {
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.config.cwd.to_path_buf()),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
@@ -211,6 +213,7 @@ async fn steer_user_input(codex: &CodexThread, text: &str) {
|
||||
text: text.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
/*additional_context*/ Default::default(),
|
||||
/*expected_turn_id*/ None,
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
@@ -354,6 +357,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
@@ -373,6 +377,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user