mirror of
https://github.com/openai/codex.git
synced 2026-05-15 08:42:34 +00:00
## Why Tool families already disagree on what their existing `duration` fields mean, so lifecycle latency should live on the shared item envelope instead of being inferred from per-tool execution fields. Carrying that envelope through app-server notifications gives downstream consumers one reusable timing signal without pretending every tool has the same execution semantics. ## What changed - Adds `started_at_ms` to core `ItemStartedEvent` values and `completed_at_ms` to core `ItemCompletedEvent` values. - Populates those timestamps in the shared session lifecycle emitters, so protocol-native items get timing without each producer tracking its own clock state. - Exposes `startedAtMs` on app-server `item/started` notifications and `completedAtMs` on `item/completed` notifications. - Maps the lifecycle timestamps through the app-server boundary while leaving legacy-converted notifications nullable when no lifecycle timestamp exists. - Regenerates the app-server JSON schema and TypeScript fixtures for the notification-envelope change and updates downstream fixtures that construct those notifications directly. - Extends the existing web-search and image-generation integration flows to assert the new lifecycle timestamps on the native item events. ## Verification - `cargo check -p codex-protocol -p codex-core -p codex-app-server-protocol -p codex-app-server -p codex-tui -p codex-exec -p codex-app-server-client` - `cargo test -p codex-core --test all web_search_item_is_emitted` - `cargo test -p codex-core --test all image_generation_call_event_is_emitted` - `cargo test -p codex-app-server-protocol` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/20514). * #18748 * #18747 * #17090 * #17089 * __->__ #20514
142 lines
3.9 KiB
Rust
142 lines
3.9 KiB
Rust
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Deserializer;
|
|
use serde::Serialize;
|
|
use serde_json::Value as JsonValue;
|
|
use ts_rs::TS;
|
|
|
|
#[derive(Debug, Clone, Serialize, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DynamicToolSpec {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub namespace: Option<String>,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub input_schema: JsonValue,
|
|
#[serde(default)]
|
|
pub defer_loading: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DynamicToolCallRequest {
|
|
pub call_id: String,
|
|
pub turn_id: String,
|
|
#[serde(default)]
|
|
pub started_at_ms: i64,
|
|
#[serde(default)]
|
|
pub namespace: Option<String>,
|
|
pub tool: String,
|
|
pub arguments: JsonValue,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DynamicToolResponse {
|
|
pub content_items: Vec<DynamicToolCallOutputContentItem>,
|
|
pub success: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
|
#[serde(tag = "type", rename_all = "camelCase")]
|
|
#[ts(tag = "type")]
|
|
pub enum DynamicToolCallOutputContentItem {
|
|
#[serde(rename_all = "camelCase")]
|
|
InputText { text: String },
|
|
#[serde(rename_all = "camelCase")]
|
|
InputImage { image_url: String },
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct DynamicToolSpecDe {
|
|
namespace: Option<String>,
|
|
name: String,
|
|
description: String,
|
|
input_schema: JsonValue,
|
|
defer_loading: Option<bool>,
|
|
expose_to_context: Option<bool>,
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for DynamicToolSpec {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let DynamicToolSpecDe {
|
|
namespace,
|
|
name,
|
|
description,
|
|
input_schema,
|
|
defer_loading,
|
|
expose_to_context,
|
|
} = DynamicToolSpecDe::deserialize(deserializer)?;
|
|
|
|
Ok(Self {
|
|
namespace,
|
|
name,
|
|
description,
|
|
input_schema,
|
|
defer_loading: defer_loading
|
|
.unwrap_or_else(|| expose_to_context.map(|visible| !visible).unwrap_or(false)),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::DynamicToolSpec;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn dynamic_tool_spec_deserializes_defer_loading() {
|
|
let value = json!({
|
|
"name": "lookup_ticket",
|
|
"description": "Fetch a ticket",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": { "type": "string" }
|
|
}
|
|
},
|
|
"deferLoading": true,
|
|
});
|
|
|
|
let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize");
|
|
|
|
assert_eq!(
|
|
actual,
|
|
DynamicToolSpec {
|
|
namespace: None,
|
|
name: "lookup_ticket".to_string(),
|
|
description: "Fetch a ticket".to_string(),
|
|
input_schema: json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"id": { "type": "string" }
|
|
}
|
|
}),
|
|
defer_loading: true,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_tool_spec_legacy_expose_to_context_inverts_to_defer_loading() {
|
|
let value = json!({
|
|
"name": "lookup_ticket",
|
|
"description": "Fetch a ticket",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
},
|
|
"exposeToContext": false,
|
|
});
|
|
|
|
let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize");
|
|
|
|
assert!(actual.defer_loading);
|
|
}
|
|
}
|