mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Preserve steer input as user input (#23405)
## Why Steered input was queued as a `ResponseInputItem`, then parsed back into a user message before recording. That path loses information that only exists on `UserInput`, such as UI text elements. This change keeps turn-local pending input typed as either original `UserInput` or existing response items, so steered user input reaches user-message recording without being reconstructed from a response item. ## What changed - Add `TurnInput` for active-turn pending input. - Queue `Session::steer_input` as `TurnInput::UserInput`. - Run pending-input hook inspection only for `TurnInput::UserInput`. - Process drained pending input item by item: accepted items are recorded, blocked items append hook context and are skipped. - Remove the pending-input prepend/requeue path. ## Validation - `just fmt` - `just fix -p codex-core` - `RUST_MIN_STACK=16777216 cargo test -p codex-core --lib session::tests::task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input -- --nocapture` - `RUST_MIN_STACK=16777216 cargo test -p codex-core --lib steer_input` - `RUST_MIN_STACK=16777216 cargo test -p codex-core --lib pending_input` - `RUST_MIN_STACK=16777216 cargo test -p codex-core --test all pending_input` - `RUST_MIN_STACK=16777216 cargo test -p codex-core` (unit tests passed: 1835 passed, 0 failed, 4 ignored; integration `all` target failed due missing helper binaries such as `codex`/`test_stdio_server` plus unrelated MCP/search/code-mode expectations)
This commit is contained in:
committed by
GitHub
Unverified
parent
9289b7cea8
commit
f0663fd4fd
@@ -7,6 +7,7 @@
|
||||
use crate::StateDbHandle;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::GoalContext;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::state::ActiveTurn;
|
||||
@@ -1347,7 +1348,14 @@ impl Session {
|
||||
return;
|
||||
}
|
||||
self.input_queue
|
||||
.extend_pending_input_for_turn_state(turn_state.as_ref(), candidate.items)
|
||||
.extend_pending_input_for_turn_state(
|
||||
turn_state.as_ref(),
|
||||
candidate
|
||||
.items
|
||||
.into_iter()
|
||||
.map(TurnInput::ResponseInputItem)
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let turn_context = self
|
||||
|
||||
@@ -20,7 +20,7 @@ use codex_hooks::UserPromptSubmitRequest;
|
||||
use codex_otel::HOOK_RUN_DURATION_METRIC;
|
||||
use codex_otel::HOOK_RUN_METRIC;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -30,12 +30,12 @@ use codex_protocol::protocol::HookRunStatus;
|
||||
use codex_protocol::protocol::HookRunSummary;
|
||||
use codex_protocol::protocol::HookSource;
|
||||
use codex_protocol::protocol::HookStartedEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::HookAdditionalContext;
|
||||
use crate::event_mapping::parse_turn_item;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
@@ -51,22 +51,6 @@ pub(crate) enum PreToolUseHookResult {
|
||||
Blocked(String),
|
||||
}
|
||||
|
||||
pub(crate) enum PendingInputHookDisposition {
|
||||
Accepted(Box<PendingInputRecord>),
|
||||
Blocked { additional_contexts: Vec<String> },
|
||||
}
|
||||
|
||||
pub(crate) enum PendingInputRecord {
|
||||
UserMessage {
|
||||
content: Vec<UserInput>,
|
||||
response_item: ResponseItem,
|
||||
additional_contexts: Vec<String>,
|
||||
},
|
||||
ConversationItem {
|
||||
response_item: ResponseItem,
|
||||
},
|
||||
}
|
||||
|
||||
struct ContextInjectingHookOutcome {
|
||||
hook_events: Vec<HookCompletedEvent>,
|
||||
outcome: HookRuntimeOutcome,
|
||||
@@ -338,32 +322,6 @@ pub(crate) async fn run_post_compact_hooks(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run_user_prompt_submit_hooks(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
prompt: String,
|
||||
) -> HookRuntimeOutcome {
|
||||
let request = UserPromptSubmitRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
permission_mode: hook_permission_mode(turn_context),
|
||||
prompt,
|
||||
};
|
||||
let hooks = sess.hooks();
|
||||
let preview_runs = hooks.preview_user_prompt_submit(&request);
|
||||
run_context_injecting_hook(
|
||||
sess,
|
||||
turn_context,
|
||||
preview_runs,
|
||||
hooks.run_user_prompt_submit(request),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn run_stop_hooks(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
@@ -458,54 +416,55 @@ pub(crate) async fn run_legacy_after_agent_hook(
|
||||
pub(crate) async fn inspect_pending_input(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
pending_input_item: ResponseInputItem,
|
||||
) -> PendingInputHookDisposition {
|
||||
let response_item = ResponseItem::from(pending_input_item);
|
||||
if let Some(TurnItem::UserMessage(user_message)) = parse_turn_item(&response_item) {
|
||||
let user_prompt_submit_outcome =
|
||||
run_user_prompt_submit_hooks(sess, turn_context, user_message.message()).await;
|
||||
if user_prompt_submit_outcome.should_stop {
|
||||
PendingInputHookDisposition::Blocked {
|
||||
additional_contexts: user_prompt_submit_outcome.additional_contexts,
|
||||
}
|
||||
} else {
|
||||
PendingInputHookDisposition::Accepted(Box::new(PendingInputRecord::UserMessage {
|
||||
content: user_message.content,
|
||||
response_item,
|
||||
additional_contexts: user_prompt_submit_outcome.additional_contexts,
|
||||
}))
|
||||
pending_input_item: &TurnInput,
|
||||
) -> HookRuntimeOutcome {
|
||||
match pending_input_item {
|
||||
TurnInput::UserInput(content) => {
|
||||
let request = UserPromptSubmitRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
permission_mode: hook_permission_mode(turn_context),
|
||||
prompt: UserMessageItem::new(content).message(),
|
||||
};
|
||||
let hooks = sess.hooks();
|
||||
let preview_runs = hooks.preview_user_prompt_submit(&request);
|
||||
run_context_injecting_hook(
|
||||
sess,
|
||||
turn_context,
|
||||
preview_runs,
|
||||
hooks.run_user_prompt_submit(request),
|
||||
)
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
PendingInputHookDisposition::Accepted(Box::new(PendingInputRecord::ConversationItem {
|
||||
response_item,
|
||||
}))
|
||||
TurnInput::ResponseInputItem(_) => HookRuntimeOutcome {
|
||||
should_stop: false,
|
||||
additional_contexts: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn record_pending_input(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
pending_input: PendingInputRecord,
|
||||
pending_input: TurnInput,
|
||||
additional_contexts: Vec<String>,
|
||||
) {
|
||||
match pending_input {
|
||||
PendingInputRecord::UserMessage {
|
||||
content,
|
||||
response_item,
|
||||
additional_contexts,
|
||||
} => {
|
||||
sess.record_user_prompt_and_emit_turn_item(
|
||||
turn_context.as_ref(),
|
||||
content.as_slice(),
|
||||
response_item,
|
||||
)
|
||||
.await;
|
||||
record_additional_contexts(sess, turn_context, additional_contexts).await;
|
||||
TurnInput::UserInput(content) => {
|
||||
sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), content.as_slice())
|
||||
.await;
|
||||
}
|
||||
PendingInputRecord::ConversationItem { response_item } => {
|
||||
TurnInput::ResponseInputItem(input) => {
|
||||
let response_item = ResponseItem::from(input);
|
||||
sess.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
record_additional_contexts(sess, turn_context, additional_contexts).await;
|
||||
}
|
||||
|
||||
async fn run_context_injecting_hook<Fut, Outcome>(
|
||||
|
||||
@@ -3,15 +3,22 @@ use crate::state::MailboxDeliveryPhase;
|
||||
use crate::state::TurnState;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::watch;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum TurnInput {
|
||||
UserInput(Vec<UserInput>),
|
||||
ResponseInputItem(ResponseInputItem),
|
||||
}
|
||||
|
||||
/// Turn-local pending input storage owned by the input queue flow.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TurnInputQueue {
|
||||
items: Vec<ResponseInputItem>,
|
||||
items: Vec<TurnInput>,
|
||||
}
|
||||
|
||||
/// Session-scoped pending input storage and active-turn mailbox delivery coordination.
|
||||
@@ -151,7 +158,7 @@ impl InputQueue {
|
||||
pub(super) async fn push_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
&self,
|
||||
turn_state: &Mutex<TurnState>,
|
||||
input: ResponseInputItem,
|
||||
input: TurnInput,
|
||||
) {
|
||||
let mut turn_state = turn_state.lock().await;
|
||||
turn_state.pending_input.items.push(input);
|
||||
@@ -161,7 +168,7 @@ impl InputQueue {
|
||||
pub(crate) async fn extend_pending_input_for_turn_state(
|
||||
&self,
|
||||
turn_state: &Mutex<TurnState>,
|
||||
input: Vec<ResponseInputItem>,
|
||||
input: Vec<TurnInput>,
|
||||
) {
|
||||
turn_state.lock().await.pending_input.items.extend(input);
|
||||
}
|
||||
@@ -169,7 +176,7 @@ impl InputQueue {
|
||||
pub(crate) async fn take_pending_input_for_turn_state(
|
||||
&self,
|
||||
turn_state: &Mutex<TurnState>,
|
||||
) -> Vec<ResponseInputItem> {
|
||||
) -> Vec<TurnInput> {
|
||||
turn_state.lock().await.pending_input.items.split_off(0)
|
||||
}
|
||||
|
||||
@@ -185,43 +192,20 @@ impl InputQueue {
|
||||
let mut active = active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
Some(active_turn) => {
|
||||
active_turn
|
||||
.turn_state
|
||||
.lock()
|
||||
.await
|
||||
.pending_input
|
||||
.items
|
||||
.extend(input);
|
||||
self.extend_pending_input_for_turn_state(
|
||||
active_turn.turn_state.as_ref(),
|
||||
input
|
||||
.into_iter()
|
||||
.map(TurnInput::ResponseInputItem)
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(input),
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn checks and turn state updates must remain atomic"
|
||||
)]
|
||||
pub(crate) async fn prepend_pending_input(
|
||||
&self,
|
||||
active_turn: &Mutex<Option<ActiveTurn>>,
|
||||
mut input: Vec<ResponseInputItem>,
|
||||
) -> Result<(), ()> {
|
||||
let mut active = active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
Some(active_turn) => {
|
||||
let mut turn_state = active_turn.turn_state.lock().await;
|
||||
if !input.is_empty() {
|
||||
let pending_input = &mut turn_state.pending_input;
|
||||
input.append(&mut pending_input.items);
|
||||
pending_input.items = input;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn checks and turn state updates must remain atomic"
|
||||
@@ -229,7 +213,7 @@ impl InputQueue {
|
||||
pub(crate) async fn get_pending_input(
|
||||
&self,
|
||||
active_turn: &Mutex<Option<ActiveTurn>>,
|
||||
) -> Vec<ResponseInputItem> {
|
||||
) -> Vec<TurnInput> {
|
||||
let (pending_input, accepts_mailbox_delivery) = {
|
||||
let mut active = active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
@@ -246,11 +230,13 @@ impl InputQueue {
|
||||
if !accepts_mailbox_delivery {
|
||||
return pending_input;
|
||||
}
|
||||
let mailbox_items = self.drain_mailbox_input_items().await;
|
||||
let mailbox_items = self
|
||||
.drain_mailbox_input_items()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(TurnInput::ResponseInputItem);
|
||||
if pending_input.is_empty() {
|
||||
mailbox_items
|
||||
} else if mailbox_items.is_empty() {
|
||||
pending_input
|
||||
mailbox_items.collect()
|
||||
} else {
|
||||
let mut pending_input = pending_input;
|
||||
pending_input.extend(mailbox_items);
|
||||
|
||||
@@ -207,6 +207,7 @@ use self::config_lock::validate_config_lock_if_configured;
|
||||
#[cfg(test)]
|
||||
use self::handlers::submission_dispatch_span;
|
||||
use self::handlers::submission_loop;
|
||||
pub(crate) use self::input_queue::TurnInput;
|
||||
pub(crate) use self::input_queue::TurnInputQueue;
|
||||
use self::review::spawn_review_thread;
|
||||
use self::session::AppServerClientMetadata;
|
||||
@@ -3099,11 +3100,11 @@ impl Session {
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
input: &[UserInput],
|
||||
response_item: ResponseItem,
|
||||
) {
|
||||
// Persist the user message to history, but emit the turn item from `UserInput` so
|
||||
// UI-only `text_elements` are preserved. `ResponseItem::Message` does not carry
|
||||
// those spans, and `record_response_item_and_emit_turn_item` would drop them.
|
||||
let response_item = ResponseItem::from(ResponseInputItem::from(input.to_vec()));
|
||||
self.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
|
||||
.await;
|
||||
let turn_item = TurnItem::UserMessage(UserMessageItem::new(input));
|
||||
@@ -3192,7 +3193,7 @@ impl Session {
|
||||
self.input_queue
|
||||
.push_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
active_turn.turn_state.as_ref(),
|
||||
input.into(),
|
||||
TurnInput::UserInput(input),
|
||||
)
|
||||
.await;
|
||||
Ok(active_turn_id.clone())
|
||||
|
||||
@@ -7724,15 +7724,21 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
|
||||
sess.inject_response_items(vec![ResponseInputItem::Message {
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "late pending input".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
}])
|
||||
let text_element = codex_protocol::user_input::TextElement::new(
|
||||
codex_protocol::user_input::ByteRange { start: 5, end: 12 },
|
||||
Some("pending marker".to_string()),
|
||||
);
|
||||
let pending_user_input = vec![UserInput::Text {
|
||||
text: "late pending input".to_string(),
|
||||
text_elements: vec![text_element.clone()],
|
||||
}];
|
||||
sess.steer_input(
|
||||
pending_user_input.clone(),
|
||||
Some(&tc.sub_id),
|
||||
/*responsesapi_client_metadata*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("inject pending input into active turn");
|
||||
.expect("steer pending input into active turn");
|
||||
|
||||
sess.on_task_finished(Arc::clone(&tc), /*last_agent_message*/ None)
|
||||
.await;
|
||||
@@ -7766,10 +7772,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
|
||||
EventMsg::ItemStarted(ItemStartedEvent {
|
||||
item: TurnItem::UserMessage(UserMessageItem { content, .. }),
|
||||
..
|
||||
}) if content == vec![UserInput::Text {
|
||||
text: "late pending input".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
}) if content == pending_user_input
|
||||
));
|
||||
|
||||
let third = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
|
||||
@@ -7781,10 +7784,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
item: TurnItem::UserMessage(UserMessageItem { content, .. }),
|
||||
..
|
||||
}) if content == vec![UserInput::Text {
|
||||
text: "late pending input".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
}) if content == pending_user_input
|
||||
));
|
||||
|
||||
let fourth = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
|
||||
@@ -7801,7 +7801,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
|
||||
..
|
||||
}) if message == "late pending input"
|
||||
&& images == Some(Vec::new())
|
||||
&& text_elements.is_empty()
|
||||
&& text_elements == vec![text_element]
|
||||
&& local_images.is_empty()
|
||||
));
|
||||
|
||||
@@ -7954,69 +7954,6 @@ async fn steer_input_returns_active_turn_id() {
|
||||
assert!(sess.input_queue.has_pending_input(&sess.active_turn).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepend_pending_input_keeps_older_tail_ahead_of_newer_input() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
input,
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let blocked = ResponseInputItem::Message {
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "blocked queued prompt".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
};
|
||||
let later = ResponseInputItem::Message {
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "later queued prompt".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
};
|
||||
let newer = ResponseInputItem::Message {
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "newer queued prompt".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
};
|
||||
|
||||
sess.inject_response_items(vec![blocked.clone(), later.clone()])
|
||||
.await
|
||||
.expect("inject initial pending input into active turn");
|
||||
|
||||
let drained = sess.input_queue.get_pending_input(&sess.active_turn).await;
|
||||
assert_eq!(drained, vec![blocked, later.clone()]);
|
||||
|
||||
sess.inject_response_items(vec![newer.clone()])
|
||||
.await
|
||||
.expect("inject newer pending input into active turn");
|
||||
|
||||
let mut drained_iter = drained.into_iter();
|
||||
let _blocked = drained_iter.next().expect("blocked prompt should exist");
|
||||
sess.input_queue
|
||||
.prepend_pending_input(&sess.active_turn, drained_iter.collect())
|
||||
.await
|
||||
.expect("requeue later pending input at the front of the queue");
|
||||
|
||||
assert_eq!(
|
||||
sess.input_queue.get_pending_input(&sess.active_turn).await,
|
||||
vec![later, newer]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_response_items_for_next_turn_move_into_next_active_turn() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
@@ -8044,7 +7981,7 @@ async fn queued_response_items_for_next_turn_move_into_next_active_turn() {
|
||||
|
||||
assert_eq!(
|
||||
sess.input_queue.get_pending_input(&sess.active_turn).await,
|
||||
vec![queued_item]
|
||||
vec![TurnInput::ResponseInputItem(queued_item)]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8089,7 +8026,10 @@ async fn abort_empty_active_turn_preserves_pending_input() {
|
||||
Arc::clone(&active_turn.turn_state)
|
||||
};
|
||||
sess.input_queue
|
||||
.extend_pending_input_for_turn_state(turn_state.as_ref(), vec![pending_item.clone()])
|
||||
.extend_pending_input_for_turn_state(
|
||||
turn_state.as_ref(),
|
||||
vec![TurnInput::ResponseInputItem(pending_item.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
sess.abort_all_tasks(TurnAbortReason::Replaced).await;
|
||||
@@ -8099,7 +8039,7 @@ async fn abort_empty_active_turn_preserves_pending_input() {
|
||||
sess.input_queue
|
||||
.take_pending_input_for_turn_state(turn_state.as_ref())
|
||||
.await,
|
||||
vec![pending_item]
|
||||
vec![TurnInput::ResponseInputItem(pending_item)]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8523,7 +8463,9 @@ async fn budget_limited_accounting_steers_active_turn_without_aborting() -> anyh
|
||||
.await?;
|
||||
|
||||
let pending_input = sess.input_queue.get_pending_input(&sess.active_turn).await;
|
||||
let [ResponseInputItem::Message { role, content, .. }] = pending_input.as_slice() else {
|
||||
let [TurnInput::ResponseInputItem(ResponseInputItem::Message { role, content, .. })] =
|
||||
pending_input.as_slice()
|
||||
else {
|
||||
panic!("expected one budget-limit steering message, got {pending_input:#?}");
|
||||
};
|
||||
assert_eq!("user", role);
|
||||
@@ -8748,7 +8690,7 @@ async fn external_objective_change_steers_active_turn() -> anyhow::Result<()> {
|
||||
pending_input.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
ResponseInputItem::Message { role, content, .. }
|
||||
TurnInput::ResponseInputItem(ResponseInputItem::Message { role, content, .. })
|
||||
if role == "user"
|
||||
&& content.iter().any(|content| matches!(
|
||||
content,
|
||||
@@ -8971,7 +8913,9 @@ async fn queue_only_mailbox_mail_waits_for_next_turn_after_answer_boundary() {
|
||||
|
||||
assert_eq!(
|
||||
sess.input_queue.get_pending_input(&sess.active_turn).await,
|
||||
vec![communication.to_response_input_item()],
|
||||
vec![TurnInput::ResponseInputItem(
|
||||
communication.to_response_input_item()
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9051,11 +8995,11 @@ async fn steered_input_reopens_mailbox_delivery_for_current_turn() {
|
||||
assert_eq!(
|
||||
sess.input_queue.get_pending_input(&sess.active_turn).await,
|
||||
vec![
|
||||
ResponseInputItem::from(vec![UserInput::Text {
|
||||
TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "follow up".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]),
|
||||
communication.to_response_input_item(),
|
||||
TurnInput::ResponseInputItem(communication.to_response_input_item()),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -9104,11 +9048,11 @@ async fn stale_defer_mailbox_delivery_does_not_override_steered_input() {
|
||||
assert_eq!(
|
||||
sess.input_queue.get_pending_input(&sess.active_turn).await,
|
||||
vec![
|
||||
ResponseInputItem::from(vec![UserInput::Text {
|
||||
TurnInput::UserInput(vec![UserInput::Text {
|
||||
text: "follow up".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]),
|
||||
communication.to_response_input_item(),
|
||||
TurnInput::ResponseInputItem(communication.to_response_input_item()),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -9163,7 +9107,9 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() {
|
||||
assert!(output.tool_future.is_some());
|
||||
assert_eq!(
|
||||
sess.input_queue.get_pending_input(&sess.active_turn).await,
|
||||
vec![communication.to_response_input_item()],
|
||||
vec![TurnInput::ResponseInputItem(
|
||||
communication.to_response_input_item()
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,12 @@ use crate::connectors;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::feedback_tags;
|
||||
use crate::goals::GoalRuntimeEvent;
|
||||
use crate::hook_runtime::PendingInputHookDisposition;
|
||||
use crate::hook_runtime::inspect_pending_input;
|
||||
use crate::hook_runtime::record_additional_contexts;
|
||||
use crate::hook_runtime::record_pending_input;
|
||||
use crate::hook_runtime::run_legacy_after_agent_hook;
|
||||
use crate::hook_runtime::run_pending_session_start_hooks;
|
||||
use crate::hook_runtime::run_stop_hooks;
|
||||
use crate::hook_runtime::run_user_prompt_submit_hooks;
|
||||
use crate::injection::ToolMentionKind;
|
||||
use crate::injection::app_id_from_path;
|
||||
use crate::injection::tool_kind_for_path;
|
||||
@@ -38,6 +36,7 @@ use crate::mentions::collect_explicit_plugin_mentions;
|
||||
use crate::mentions::collect_tool_mentions_from_messages;
|
||||
use crate::plugins::build_plugin_injections;
|
||||
use crate::session::PreviousTurnSettings;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::stream_events_utils::HandleOutputCtx;
|
||||
@@ -78,7 +77,6 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::items::PlanItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -174,17 +172,10 @@ pub(crate) async fn run_turn(
|
||||
if run_pending_session_start_hooks(&sess, &turn_context).await {
|
||||
return None;
|
||||
}
|
||||
let additional_contexts = if input.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input.clone());
|
||||
let response_item: ResponseItem = initial_input_for_turn.clone().into();
|
||||
let user_prompt_submit_outcome = run_user_prompt_submit_hooks(
|
||||
&sess,
|
||||
&turn_context,
|
||||
UserMessageItem::new(&input).message(),
|
||||
)
|
||||
.await;
|
||||
if !input.is_empty() {
|
||||
let initial_turn_input = TurnInput::UserInput(input.clone());
|
||||
let user_prompt_submit_outcome =
|
||||
inspect_pending_input(&sess, &turn_context, &initial_turn_input).await;
|
||||
if user_prompt_submit_outcome.should_stop {
|
||||
record_additional_contexts(
|
||||
&sess,
|
||||
@@ -194,13 +185,17 @@ pub(crate) async fn run_turn(
|
||||
.await;
|
||||
return None;
|
||||
}
|
||||
sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), &input, response_item)
|
||||
.await;
|
||||
user_prompt_submit_outcome.additional_contexts
|
||||
};
|
||||
record_pending_input(
|
||||
&sess,
|
||||
&turn_context,
|
||||
initial_turn_input,
|
||||
user_prompt_submit_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
sess.merge_connector_selection(explicitly_enabled_connectors.clone())
|
||||
.await;
|
||||
record_additional_contexts(&sess, &turn_context, additional_contexts).await;
|
||||
sess.set_previous_turn_settings(Some(PreviousTurnSettings {
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
@@ -252,45 +247,26 @@ pub(crate) async fn run_turn(
|
||||
};
|
||||
|
||||
let mut blocked_pending_input = false;
|
||||
let mut blocked_pending_input_contexts = Vec::new();
|
||||
let mut requeued_pending_input = false;
|
||||
let mut accepted_pending_input = Vec::new();
|
||||
if !pending_input.is_empty() {
|
||||
let mut pending_input_iter = pending_input.into_iter();
|
||||
while let Some(pending_input_item) = pending_input_iter.next() {
|
||||
match inspect_pending_input(&sess, &turn_context, pending_input_item).await {
|
||||
PendingInputHookDisposition::Accepted(pending_input) => {
|
||||
accepted_pending_input.push(*pending_input);
|
||||
}
|
||||
PendingInputHookDisposition::Blocked {
|
||||
additional_contexts,
|
||||
} => {
|
||||
let remaining_pending_input = pending_input_iter.collect::<Vec<_>>();
|
||||
if !remaining_pending_input.is_empty() {
|
||||
let _ = sess
|
||||
.input_queue
|
||||
.prepend_pending_input(&sess.active_turn, remaining_pending_input)
|
||||
.await;
|
||||
requeued_pending_input = true;
|
||||
}
|
||||
blocked_pending_input_contexts = additional_contexts;
|
||||
blocked_pending_input = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut accepted_pending_input = false;
|
||||
for pending_input_item in pending_input {
|
||||
let hook_outcome =
|
||||
inspect_pending_input(&sess, &turn_context, &pending_input_item).await;
|
||||
if hook_outcome.should_stop {
|
||||
blocked_pending_input = true;
|
||||
record_additional_contexts(&sess, &turn_context, hook_outcome.additional_contexts)
|
||||
.await;
|
||||
} else {
|
||||
accepted_pending_input = true;
|
||||
record_pending_input(
|
||||
&sess,
|
||||
&turn_context,
|
||||
pending_input_item,
|
||||
hook_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let has_accepted_pending_input = !accepted_pending_input.is_empty();
|
||||
for pending_input in accepted_pending_input {
|
||||
record_pending_input(&sess, &turn_context, pending_input).await;
|
||||
}
|
||||
record_additional_contexts(&sess, &turn_context, blocked_pending_input_contexts).await;
|
||||
|
||||
if blocked_pending_input && !has_accepted_pending_input {
|
||||
if requeued_pending_input {
|
||||
continue;
|
||||
}
|
||||
if blocked_pending_input && !accepted_pending_input {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ use tracing::warn;
|
||||
use crate::config::Config;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::goals::GoalRuntimeEvent;
|
||||
use crate::hook_runtime::PendingInputHookDisposition;
|
||||
use crate::hook_runtime::inspect_pending_input;
|
||||
use crate::hook_runtime::record_additional_contexts;
|
||||
use crate::hook_runtime::record_pending_input;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::state::ActiveTurn;
|
||||
@@ -42,7 +42,6 @@ use codex_otel::TURN_MEMORY_METRIC;
|
||||
use codex_otel::TURN_NETWORK_PROXY_METRIC;
|
||||
use codex_otel::TURN_TOKEN_USAGE_METRIC;
|
||||
use codex_otel::TURN_TOOL_CALL_METRIC;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
@@ -359,7 +358,10 @@ impl Session {
|
||||
Arc::clone(&turn.turn_state)
|
||||
};
|
||||
turn_state.lock().await.token_usage_at_turn_start = token_usage_at_turn_start;
|
||||
let mut pending_items = queued_response_items;
|
||||
let mut pending_items = queued_response_items
|
||||
.into_iter()
|
||||
.map(TurnInput::ResponseInputItem)
|
||||
.collect::<Vec<_>>();
|
||||
pending_items.extend(mailbox_items);
|
||||
self.input_queue
|
||||
.extend_pending_input_for_turn_state(turn_state.as_ref(), pending_items)
|
||||
@@ -587,7 +589,7 @@ impl Session {
|
||||
.turn_metadata_state
|
||||
.cancel_git_enrichment_task();
|
||||
|
||||
let mut pending_input = Vec::<ResponseInputItem>::new();
|
||||
let mut pending_input = Vec::<TurnInput>::new();
|
||||
let mut should_clear_active_turn = false;
|
||||
let mut token_usage_at_turn_start = None;
|
||||
let mut turn_had_memory_citation = false;
|
||||
@@ -622,15 +624,23 @@ impl Session {
|
||||
}
|
||||
if !pending_input.is_empty() {
|
||||
for pending_input_item in pending_input {
|
||||
match inspect_pending_input(self, &turn_context, pending_input_item).await {
|
||||
PendingInputHookDisposition::Accepted(pending_input) => {
|
||||
record_pending_input(self, &turn_context, *pending_input).await;
|
||||
}
|
||||
PendingInputHookDisposition::Blocked {
|
||||
additional_contexts,
|
||||
} => {
|
||||
record_additional_contexts(self, &turn_context, additional_contexts).await;
|
||||
}
|
||||
let hook_outcome =
|
||||
inspect_pending_input(self, &turn_context, &pending_input_item).await;
|
||||
if hook_outcome.should_stop {
|
||||
record_additional_contexts(
|
||||
self,
|
||||
&turn_context,
|
||||
hook_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
record_pending_input(
|
||||
self,
|
||||
&turn_context,
|
||||
pending_input_item,
|
||||
hook_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user