Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
}
},
"properties": {
"approvedArguments": {
"description": "Optional client-approved replacement arguments. When present, this must fully match the existing registered schema for the same dynamic tool."
},
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
Expand All @@ -63,4 +66,4 @@
],
"title": "DynamicToolCallResponse",
"type": "object"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,9 @@
"DynamicToolCallResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"approvedArguments": {
"description": "Optional client-approved replacement arguments. When present, this must fully match the existing registered schema for the same dynamic tool."
},
"contentItems": {
"items": {
"$ref": "#/definitions/v2/DynamicToolCallOutputContentItem"
Expand Down Expand Up @@ -14491,4 +14494,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!

// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { JsonValue } from "../serde_json/JsonValue";
import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";

export type DynamicToolCallResponse = { contentItems: Array<DynamicToolCallOutputContentItem>, success: boolean, };
export type DynamicToolCallResponse = { contentItems: Array<DynamicToolCallOutputContentItem>, success: boolean,
/**
* Optional client-approved replacement arguments. When present, this must
* fully match the existing registered schema for the same dynamic tool.
*/
approvedArguments: JsonValue | null, };
59 changes: 59 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/thread_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1802,6 +1802,65 @@ mod tests {
);
}

#[test]
fn reconstructs_dynamic_tool_items_using_authoritative_response_arguments() {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
message: "run dynamic tool".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::DynamicToolCallRequest(
codex_protocol::dynamic_tools::DynamicToolCallRequest {
call_id: "dyn-1".into(),
turn_id: "turn-1".into(),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
},
),
EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
call_id: "dyn-1".into(),
turn_id: "turn-1".into(),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-456"}),
content_items: vec![CoreDynamicToolCallOutputContentItem::InputText {
text: "Ticket is open".into(),
}],
success: true,
error: None,
duration: Duration::from_millis(42),
}),
];

let items = events
.into_iter()
.map(RolloutItem::EventMsg)
.collect::<Vec<_>>();
let turns = build_turns_from_rollout_items(&items);
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].items.len(), 2);
assert_eq!(
turns[0].items[1],
ThreadItem::DynamicToolCall {
id: "dyn-1".into(),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-456"}),
status: DynamicToolCallStatus::Completed,
content_items: Some(vec![DynamicToolCallOutputContentItem::InputText {
text: "Ticket is open".into(),
}]),
success: Some(true),
duration_ms: Some(42),
}
);
}

#[test]
fn reconstructs_declined_exec_and_patch_items() {
let events = vec![
Expand Down
46 changes: 46 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5634,6 +5634,9 @@ pub struct PermissionsRequestApprovalResponse {
pub struct DynamicToolCallResponse {
pub content_items: Vec<DynamicToolCallOutputContentItem>,
pub success: bool,
/// Optional client-approved replacement arguments. When present, this must
/// fully match the existing registered schema for the same dynamic tool.
pub approved_arguments: Option<JsonValue>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
Expand Down Expand Up @@ -7640,6 +7643,7 @@ mod tests {
text: "dynamic-ok".to_string(),
}],
success: true,
approved_arguments: None,
})
.unwrap();

Expand All @@ -7652,6 +7656,7 @@ mod tests {
"text": "dynamic-ok"
}
],
"approvedArguments": null,
"success": true,
})
);
Expand All @@ -7669,6 +7674,7 @@ mod tests {
},
],
success: true,
approved_arguments: None,
})
.unwrap();

Expand All @@ -7685,11 +7691,51 @@ mod tests {
"imageUrl": "data:image/png;base64,AAA"
}
],
"approvedArguments": null,
"success": true,
})
);
}

#[test]
fn dynamic_tool_response_round_trips_non_null_approved_arguments() {
let value = serde_json::to_value(DynamicToolCallResponse {
content_items: vec![DynamicToolCallOutputContentItem::InputText {
text: "dynamic-ok".to_string(),
}],
success: true,
approved_arguments: Some(json!({ "city": "Tokyo" })),
})
.unwrap();

assert_eq!(
value,
json!({
"contentItems": [
{
"type": "inputText",
"text": "dynamic-ok"
}
],
"approvedArguments": {
"city": "Tokyo"
},
"success": true,
})
);

assert_eq!(
serde_json::from_value::<DynamicToolCallResponse>(value).unwrap(),
DynamicToolCallResponse {
content_items: vec![DynamicToolCallOutputContentItem::InputText {
text: "dynamic-ok".to_string(),
}],
success: true,
approved_arguments: Some(json!({ "city": "Tokyo" })),
}
);
}

#[test]
fn dynamic_tool_spec_deserializes_defer_loading() {
let value = json!({
Expand Down
12 changes: 11 additions & 1 deletion codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1027,11 +1027,21 @@ The client must respond with content items. Use `inputText` for text and `inputI
{ "type": "inputText", "text": "Ticket ABC-123 is open." },
{ "type": "inputImage", "imageUrl": "data:image/png;base64,AAA" }
],
"success": true
"success": true,
"approvedArguments": { "id": "ABC-456" }
}
}
```

`approvedArguments` is optional. When omitted or `null`, the original model-proposed `arguments` remain authoritative. When present, it must be a full replacement object/value that still matches the same registered dynamic-tool schema; clients cannot rename the tool or change its schema.

Event semantics stay narrow:

1. `item/started` and `item/tool/call` always show the model-proposed arguments.
2. If the client returns valid `approvedArguments`, Codex treats them as the authoritative executed arguments.
3. `item/completed` uses the authoritative final arguments.
4. When the authoritative arguments differ from the proposed ones, Codex records a developer-role note so the next model step reasons from the approved arguments instead of the earlier proposal.

## Skills

Invoke a skill by including `$<skill-name>` in the text input. Add a `skill` input item (recommended) so the backend injects full skill instructions instead of relying on the model to resolve the name.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/bespoke_event_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,7 @@ pub(crate) async fn apply_bespoke_event_handling(
text: "dynamic tool calls require api v2".to_string(),
}],
success: false,
approved_arguments: None,
},
})
.await;
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/app-server/src/dynamic_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ pub(crate) async fn on_call_response(
let DynamicToolCallResponse {
content_items,
success,
approved_arguments,
} = response.clone();
let core_response = CoreDynamicToolResponse {
content_items: content_items
.into_iter()
.map(CoreDynamicToolCallOutputContentItem::from)
.collect(),
success,
approved_arguments,
};
if let Err(err) = conversation
.submit(Op::DynamicToolResponse {
Expand Down Expand Up @@ -69,6 +71,7 @@ fn fallback_response(message: &str) -> (DynamicToolCallResponse, Option<String>)
text: message.to_string(),
}],
success: false,
approved_arguments: None,
},
Some(message.to_string()),
)
Expand Down
Loading
Loading