mirror of
https://github.com/openai/codex.git
synced 2026-04-27 18:01:04 +03:00
core: preserve js_repl tool interrupt metadata
This commit is contained in:
@@ -69,6 +69,13 @@ pub enum ToolOutput {
|
||||
body: FunctionCallOutputBody,
|
||||
success: Option<bool>,
|
||||
},
|
||||
FunctionWithControl {
|
||||
// Canonical output body for function-style tools plus internal control
|
||||
// metadata consumed by core dispatch (not exposed on the wire).
|
||||
body: FunctionCallOutputBody,
|
||||
success: Option<bool>,
|
||||
interrupt_turn: bool,
|
||||
},
|
||||
Mcp {
|
||||
result: Result<CallToolResult, String>,
|
||||
},
|
||||
@@ -83,7 +90,7 @@ pub struct ToolDispatchOutput {
|
||||
impl ToolOutput {
|
||||
pub fn log_preview(&self) -> String {
|
||||
match self {
|
||||
ToolOutput::Function { body, .. } => {
|
||||
ToolOutput::Function { body, .. } | ToolOutput::FunctionWithControl { body, .. } => {
|
||||
telemetry_preview(&body.to_text().unwrap_or_default())
|
||||
}
|
||||
ToolOutput::Mcp { result } => format!("{result:?}"),
|
||||
@@ -92,14 +99,23 @@ impl ToolOutput {
|
||||
|
||||
pub fn success_for_logging(&self) -> bool {
|
||||
match self {
|
||||
ToolOutput::Function { success, .. } => success.unwrap_or(true),
|
||||
ToolOutput::Function { success, .. }
|
||||
| ToolOutput::FunctionWithControl { success, .. } => success.unwrap_or(true),
|
||||
ToolOutput::Mcp { result } => result.is_ok(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_turn_hint(&self) -> bool {
|
||||
match self {
|
||||
ToolOutput::FunctionWithControl { interrupt_turn, .. } => *interrupt_turn,
|
||||
ToolOutput::Function { .. } | ToolOutput::Mcp { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_response(self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
|
||||
match self {
|
||||
ToolOutput::Function { body, success } => {
|
||||
ToolOutput::Function { body, success }
|
||||
| ToolOutput::FunctionWithControl { body, success, .. } => {
|
||||
// `custom_tool_call` is the Responses API item type for freeform
|
||||
// tools (`ToolSpec::Freeform`, e.g. freeform `apply_patch` or
|
||||
// `js_repl`).
|
||||
@@ -216,6 +232,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_with_control_interrupt_hint_is_internal_only() {
|
||||
let payload = ToolPayload::Function {
|
||||
arguments: "{}".to_string(),
|
||||
};
|
||||
let output = ToolOutput::FunctionWithControl {
|
||||
body: FunctionCallOutputBody::Text("ok".to_string()),
|
||||
success: Some(true),
|
||||
interrupt_turn: true,
|
||||
};
|
||||
|
||||
assert!(output.interrupt_turn_hint());
|
||||
|
||||
let response = output.into_response("fn-ctrl", &payload);
|
||||
match response {
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
||||
assert_eq!(call_id, "fn-ctrl");
|
||||
assert_eq!(output.text_content(), Some("ok"));
|
||||
assert_eq!(output.success, Some(true));
|
||||
}
|
||||
other => panic!("expected FunctionCallOutput, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_tool_calls_can_derive_text_from_content_items() {
|
||||
let payload = ToolPayload::Custom {
|
||||
|
||||
@@ -155,6 +155,7 @@ impl ToolHandler for JsReplHandler {
|
||||
};
|
||||
|
||||
let content = result.output;
|
||||
let interrupt_turn = result.interrupt_turn;
|
||||
let mut items = Vec::with_capacity(result.content_items.len() + 1);
|
||||
if !content.is_empty() {
|
||||
items.push(FunctionCallOutputContentItem::InputText {
|
||||
@@ -173,14 +174,24 @@ impl ToolHandler for JsReplHandler {
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
body: if items.is_empty() {
|
||||
FunctionCallOutputBody::Text(content)
|
||||
} else {
|
||||
FunctionCallOutputBody::ContentItems(items)
|
||||
},
|
||||
success: Some(true),
|
||||
})
|
||||
let body = if items.is_empty() {
|
||||
FunctionCallOutputBody::Text(content)
|
||||
} else {
|
||||
FunctionCallOutputBody::ContentItems(items)
|
||||
};
|
||||
|
||||
if interrupt_turn {
|
||||
Ok(ToolOutput::FunctionWithControl {
|
||||
body,
|
||||
success: Some(true),
|
||||
interrupt_turn: true,
|
||||
})
|
||||
} else {
|
||||
Ok(ToolOutput::Function {
|
||||
body,
|
||||
success: Some(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ pub struct JsReplArgs {
|
||||
pub struct JsExecResult {
|
||||
pub output: String,
|
||||
pub content_items: Vec<FunctionCallOutputContentItem>,
|
||||
pub interrupt_turn: bool,
|
||||
}
|
||||
|
||||
struct KernelState {
|
||||
@@ -127,6 +128,7 @@ struct ExecContext {
|
||||
struct ExecToolCalls {
|
||||
in_flight: usize,
|
||||
content_items: Vec<FunctionCallOutputContentItem>,
|
||||
interrupted: bool,
|
||||
notify: Arc<Notify>,
|
||||
cancel: CancellationToken,
|
||||
}
|
||||
@@ -411,6 +413,24 @@ impl JsReplManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fn mark_exec_tool_call_interrupted(
|
||||
exec_tool_calls: &Arc<Mutex<HashMap<String, ExecToolCalls>>>,
|
||||
exec_id: &str,
|
||||
) {
|
||||
let mut calls = exec_tool_calls.lock().await;
|
||||
if let Some(state) = calls.get_mut(exec_id) {
|
||||
state.interrupted = true;
|
||||
}
|
||||
}
|
||||
|
||||
async fn exec_tool_calls_interrupted(
|
||||
exec_tool_calls: &Arc<Mutex<HashMap<String, ExecToolCalls>>>,
|
||||
exec_id: &str,
|
||||
) -> bool {
|
||||
let calls = exec_tool_calls.lock().await;
|
||||
calls.get(exec_id).is_some_and(|state| state.interrupted)
|
||||
}
|
||||
|
||||
async fn wait_for_exec_tool_calls_map(
|
||||
exec_tool_calls: &Arc<Mutex<HashMap<String, ExecToolCalls>>>,
|
||||
exec_id: &str,
|
||||
@@ -794,11 +814,15 @@ impl JsReplManager {
|
||||
};
|
||||
|
||||
match response {
|
||||
ExecResultMessage::Ok { content_items } => {
|
||||
ExecResultMessage::Ok {
|
||||
content_items,
|
||||
interrupt_turn,
|
||||
} => {
|
||||
let (output, content_items) = split_exec_result_content_items(content_items);
|
||||
Ok(JsExecResult {
|
||||
output,
|
||||
content_items,
|
||||
interrupt_turn,
|
||||
})
|
||||
}
|
||||
ExecResultMessage::Err { message } => Err(FunctionCallError::RespondToModel(message)),
|
||||
@@ -1111,6 +1135,8 @@ impl JsReplManager {
|
||||
.map(|state| state.content_items.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let interrupt_turn =
|
||||
JsReplManager::exec_tool_calls_interrupted(&exec_tool_calls, &id).await;
|
||||
let mut pending = pending_execs.lock().await;
|
||||
if let Some(tx) = pending.remove(&id) {
|
||||
let payload = if ok {
|
||||
@@ -1119,6 +1145,7 @@ impl JsReplManager {
|
||||
output,
|
||||
content_items,
|
||||
),
|
||||
interrupt_turn,
|
||||
}
|
||||
} else {
|
||||
ExecResultMessage::Err {
|
||||
@@ -1142,6 +1169,7 @@ impl JsReplManager {
|
||||
ok: false,
|
||||
response: None,
|
||||
error: Some("js_repl exec context not found".to_string()),
|
||||
interrupt_turn: false,
|
||||
});
|
||||
if let Err(err) = JsReplManager::write_message(&stdin, &payload).await {
|
||||
let snapshot =
|
||||
@@ -1175,6 +1203,7 @@ impl JsReplManager {
|
||||
ok: false,
|
||||
response: None,
|
||||
error: Some("js_repl execution reset".to_string()),
|
||||
interrupt_turn: false,
|
||||
},
|
||||
result = JsReplManager::run_tool_request(
|
||||
ctx,
|
||||
@@ -1188,8 +1217,16 @@ impl JsReplManager {
|
||||
ok: false,
|
||||
response: None,
|
||||
error: Some("js_repl exec context not found".to_string()),
|
||||
interrupt_turn: false,
|
||||
},
|
||||
};
|
||||
if result.interrupt_turn {
|
||||
JsReplManager::mark_exec_tool_call_interrupted(
|
||||
&exec_tool_calls_for_task,
|
||||
&exec_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
JsReplManager::finish_exec_tool_call(&exec_tool_calls_for_task, &exec_id)
|
||||
.await;
|
||||
let payload = HostToKernel::RunToolResult(result);
|
||||
@@ -1288,6 +1325,7 @@ impl JsReplManager {
|
||||
ok: false,
|
||||
response: None,
|
||||
error: Some(error),
|
||||
interrupt_turn: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1369,25 +1407,7 @@ impl JsReplManager {
|
||||
ok: true,
|
||||
response: Some(value),
|
||||
error: None,
|
||||
=======
|
||||
Ok(output) => {
|
||||
if let ResponseInputItem::FunctionCallOutput { output, .. } = &output.response_input
|
||||
&& let Some(items) = output.content_items()
|
||||
{
|
||||
let mut has_image = false;
|
||||
let mut content = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
match item {
|
||||
FunctionCallOutputContentItem::InputText { text } => {
|
||||
content.push(ContentItem::InputText { text: text.clone() });
|
||||
}
|
||||
FunctionCallOutputContentItem::InputImage { image_url } => {
|
||||
has_image = true;
|
||||
content.push(ContentItem::InputImage {
|
||||
image_url: image_url.clone(),
|
||||
});
|
||||
}
|
||||
>>>>>>> 2fbcb2ec4 (core: generalize interrupted tool-result handling)
|
||||
interrupt_turn: output.interrupt_turn,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1399,6 +1419,7 @@ impl JsReplManager {
|
||||
ok: false,
|
||||
response: None,
|
||||
error: Some(error),
|
||||
interrupt_turn: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1412,6 +1433,7 @@ impl JsReplManager {
|
||||
ok: false,
|
||||
response: None,
|
||||
error: Some(error),
|
||||
interrupt_turn: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1546,12 +1568,15 @@ struct RunToolResult {
|
||||
response: Option<JsonValue>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
interrupt_turn: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ExecResultMessage {
|
||||
Ok {
|
||||
content_items: Vec<FunctionCallOutputContentItem>,
|
||||
interrupt_turn: bool,
|
||||
},
|
||||
Err {
|
||||
message: String,
|
||||
|
||||
@@ -186,7 +186,8 @@ impl ToolRegistry {
|
||||
Ok(output) => {
|
||||
let preview = output.log_preview();
|
||||
let success = output.success_for_logging();
|
||||
let interrupt_turn = handler.should_interrupt_turn(&output);
|
||||
let interrupt_turn = output.interrupt_turn_hint()
|
||||
|| handler.should_interrupt_turn(&output);
|
||||
let mut guard = output_cell.lock().await;
|
||||
*guard = Some((output, interrupt_turn));
|
||||
Ok((preview, success))
|
||||
|
||||
Reference in New Issue
Block a user