mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(tui/auto-drive): retain raw coordinator transcript
This commit is contained in:
@@ -1614,9 +1614,9 @@ impl App<'_> {
|
||||
AppState::Chat { widget } => widget.submit_op(op),
|
||||
AppState::Onboarding { .. } => {}
|
||||
},
|
||||
AppEvent::AutoCoordinatorDecision { status, summary, prompt } => {
|
||||
AppEvent::AutoCoordinatorDecision { status, summary, prompt, transcript } => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.auto_handle_decision(status, summary, prompt);
|
||||
widget.auto_handle_decision(status, summary, prompt, transcript);
|
||||
}
|
||||
}
|
||||
AppEvent::AutoCoordinatorThinking { delta, summary_index } => {
|
||||
|
||||
@@ -150,6 +150,7 @@ pub(crate) enum AppEvent {
|
||||
status: AutoCoordinatorStatus,
|
||||
summary: String,
|
||||
prompt: Option<String>,
|
||||
transcript: Vec<ResponseItem>,
|
||||
},
|
||||
AutoCoordinatorThinking {
|
||||
delta: String,
|
||||
|
||||
@@ -3394,15 +3394,20 @@ impl ChatWidget<'_> {
|
||||
|
||||
fn rebuild_auto_history(&mut self) -> Vec<codex_protocol::models::ResponseItem> {
|
||||
let conversation = self.export_auto_drive_items();
|
||||
self.auto_history.replace(conversation.clone());
|
||||
conversation
|
||||
let tail = self
|
||||
.auto_history
|
||||
.replace_converted(conversation.clone());
|
||||
if !tail.is_empty() {
|
||||
self.auto_history.append_raw(&tail);
|
||||
}
|
||||
self.auto_history.raw_snapshot()
|
||||
}
|
||||
|
||||
fn current_auto_history(&mut self) -> Vec<codex_protocol::models::ResponseItem> {
|
||||
if self.auto_history.is_empty() {
|
||||
if self.auto_history.converted_is_empty() {
|
||||
return self.rebuild_auto_history();
|
||||
}
|
||||
self.auto_history.snapshot()
|
||||
self.auto_history.raw_snapshot()
|
||||
}
|
||||
|
||||
/// Export current user/assistant messages into ResponseItem list for forking.
|
||||
@@ -10089,12 +10094,16 @@ impl ChatWidget<'_> {
|
||||
status: AutoCoordinatorStatus,
|
||||
summary: String,
|
||||
prompt: Option<String>,
|
||||
transcript: Vec<codex_protocol::models::ResponseItem>,
|
||||
) {
|
||||
if !self.auto_state.active {
|
||||
return;
|
||||
}
|
||||
|
||||
let summary = summary.trim().to_string();
|
||||
if !transcript.is_empty() {
|
||||
self.auto_history.append_raw(&transcript);
|
||||
}
|
||||
self.auto_state.last_decision_summary = Some(summary.clone());
|
||||
self.auto_state.coordinator_waiting = false;
|
||||
self.auto_on_reasoning_final(&summary);
|
||||
|
||||
@@ -11,7 +11,7 @@ use codex_core::model_family::{derive_default_model_family, find_family_for_mode
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::{AuthManager, ModelClient, Prompt, ResponseEvent, TextFormat};
|
||||
use codex_core::error::CodexErr;
|
||||
use codex_protocol::models::{ContentItem, ResponseItem};
|
||||
use codex_protocol::models::{ContentItem, ReasoningItemContent, ResponseItem};
|
||||
use futures::StreamExt;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
@@ -227,12 +227,13 @@ fn run_auto_loop(
|
||||
&app_event_tx,
|
||||
&cancel_token,
|
||||
) {
|
||||
Ok((status, summary, prompt_opt)) => {
|
||||
Ok((status, summary, prompt_opt, transcript_items)) => {
|
||||
let prompt_clone = prompt_opt.clone();
|
||||
let event = AppEvent::AutoCoordinatorDecision {
|
||||
status,
|
||||
summary,
|
||||
prompt: prompt_opt,
|
||||
transcript: transcript_items.clone(),
|
||||
};
|
||||
app_event_tx.send(event);
|
||||
|
||||
@@ -269,6 +270,7 @@ fn run_auto_loop(
|
||||
status: AutoCoordinatorStatus::Failed,
|
||||
summary: format!("Coordinator error: {err}"),
|
||||
prompt: None,
|
||||
transcript: Vec::new(),
|
||||
};
|
||||
app_event_tx.send(event);
|
||||
stopped = true;
|
||||
@@ -409,6 +411,11 @@ fn build_schema() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
struct RequestStreamResult {
|
||||
output_text: String,
|
||||
response_items: Vec<ResponseItem>,
|
||||
}
|
||||
|
||||
fn request_coordinator_decision(
|
||||
runtime: &tokio::runtime::Runtime,
|
||||
client: &ModelClient,
|
||||
@@ -418,8 +425,8 @@ fn request_coordinator_decision(
|
||||
conversation: Vec<ResponseItem>,
|
||||
app_event_tx: &AppEventSender,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> Result<(AutoCoordinatorStatus, String, Option<String>)> {
|
||||
let raw = request_decision(
|
||||
) -> Result<(AutoCoordinatorStatus, String, Option<String>, Vec<ResponseItem>)> {
|
||||
let (raw, response_items) = request_decision(
|
||||
runtime,
|
||||
client,
|
||||
developer_intro,
|
||||
@@ -455,7 +462,12 @@ fn request_coordinator_decision(
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok((status, decision.summary.trim().to_string(), prompt_opt))
|
||||
Ok((
|
||||
status,
|
||||
decision.summary.trim().to_string(),
|
||||
prompt_opt,
|
||||
response_items,
|
||||
))
|
||||
}
|
||||
|
||||
fn request_decision(
|
||||
@@ -467,7 +479,7 @@ fn request_decision(
|
||||
conversation: &[ResponseItem],
|
||||
app_event_tx: &AppEventSender,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> Result<String> {
|
||||
) -> Result<(String, Vec<ResponseItem>)> {
|
||||
match request_decision_with_model(
|
||||
runtime,
|
||||
client,
|
||||
@@ -479,7 +491,7 @@ fn request_decision(
|
||||
cancel_token,
|
||||
MODEL_SLUG,
|
||||
) {
|
||||
Ok(output) => Ok(output),
|
||||
Ok(result) => Ok((result.output_text, result.response_items)),
|
||||
Err(err) => {
|
||||
let fallback_slug = client.default_model_slug().to_string();
|
||||
if fallback_slug != MODEL_SLUG && should_retry_with_default_model(&err) {
|
||||
@@ -500,6 +512,7 @@ fn request_decision(
|
||||
cancel_token,
|
||||
&fallback_slug,
|
||||
)
|
||||
.map(|res| (res.output_text, res.response_items))
|
||||
.map_err(|fallback_err| {
|
||||
fallback_err.context(format!(
|
||||
"coordinator fallback with model '{}' failed after original error: {}",
|
||||
@@ -522,7 +535,7 @@ fn request_decision_with_model(
|
||||
app_event_tx: &AppEventSender,
|
||||
cancel_token: &CancellationToken,
|
||||
model_slug: &str,
|
||||
) -> Result<String> {
|
||||
) -> Result<RequestStreamResult> {
|
||||
let developer_intro = developer_intro.to_string();
|
||||
let primary_goal = primary_goal.to_string();
|
||||
let schema = schema.clone();
|
||||
@@ -551,31 +564,43 @@ fn request_decision_with_model(
|
||||
}
|
||||
let mut stream = client.stream(&prompt).await?;
|
||||
let mut out = String::new();
|
||||
let mut response_items: Vec<ResponseItem> = Vec::new();
|
||||
let mut reasoning_delta_accumulator = String::new();
|
||||
while let Some(ev) = stream.next().await {
|
||||
match ev {
|
||||
Ok(ResponseEvent::OutputTextDelta { delta, .. }) => out.push_str(&delta),
|
||||
Ok(ResponseEvent::OutputTextDelta { delta, .. }) => {
|
||||
out.push_str(&delta);
|
||||
}
|
||||
Ok(ResponseEvent::OutputItemDone { item, .. }) => {
|
||||
if let ResponseItem::Message { content, .. } = item {
|
||||
if let ResponseItem::Message { content, .. } = &item {
|
||||
for c in content {
|
||||
if let ContentItem::OutputText { text } = c {
|
||||
out.push_str(&text);
|
||||
out.push_str(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(item, ResponseItem::Reasoning { .. }) {
|
||||
reasoning_delta_accumulator.clear();
|
||||
}
|
||||
response_items.push(item);
|
||||
}
|
||||
Ok(ResponseEvent::ReasoningSummaryDelta {
|
||||
delta,
|
||||
summary_index,
|
||||
..
|
||||
}) => {
|
||||
let message = strip_role_prefix(&delta).to_string();
|
||||
let cleaned = strip_role_prefix(&delta);
|
||||
reasoning_delta_accumulator.push_str(cleaned);
|
||||
let message = cleaned.to_string();
|
||||
tx_inner.send(AppEvent::AutoCoordinatorThinking {
|
||||
delta: message,
|
||||
summary_index,
|
||||
});
|
||||
}
|
||||
Ok(ResponseEvent::ReasoningContentDelta { delta, .. }) => {
|
||||
let message = strip_role_prefix(&delta).to_string();
|
||||
let cleaned = strip_role_prefix(&delta);
|
||||
reasoning_delta_accumulator.push_str(cleaned);
|
||||
let message = cleaned.to_string();
|
||||
tx_inner.send(AppEvent::AutoCoordinatorThinking {
|
||||
delta: message,
|
||||
summary_index: None,
|
||||
@@ -586,7 +611,24 @@ fn request_decision_with_model(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
if !reasoning_delta_accumulator.trim().is_empty()
|
||||
&& !response_items
|
||||
.iter()
|
||||
.any(|item| matches!(item, ResponseItem::Reasoning { .. }))
|
||||
{
|
||||
response_items.push(ResponseItem::Reasoning {
|
||||
id: String::new(),
|
||||
summary: Vec::new(),
|
||||
content: Some(vec![ReasoningItemContent::ReasoningText {
|
||||
text: reasoning_delta_accumulator.trim().to_string(),
|
||||
}]),
|
||||
encrypted_content: None,
|
||||
});
|
||||
}
|
||||
Ok(RequestStreamResult {
|
||||
output_text: out,
|
||||
response_items,
|
||||
})
|
||||
}
|
||||
},
|
||||
classify,
|
||||
|
||||
@@ -1,29 +1,78 @@
|
||||
use codex_protocol::models::ResponseItem;
|
||||
|
||||
/// Maintains the Auto Drive conversation transcript between coordinator turns.
|
||||
///
|
||||
/// `converted` mirrors what we previously derived from UI history and is used
|
||||
/// when re-seeding the coordinator conversation. `raw` captures the exact
|
||||
/// ResponseItems returned by the Auto Drive model so we can retain full
|
||||
/// reasoning output without depending on UI rendering.
|
||||
pub(crate) struct AutoDriveHistory {
|
||||
items: Vec<ResponseItem>,
|
||||
converted: Vec<ResponseItem>,
|
||||
raw: Vec<ResponseItem>,
|
||||
pending_skip: usize,
|
||||
}
|
||||
|
||||
impl AutoDriveHistory {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
Self {
|
||||
converted: Vec::new(),
|
||||
raw: Vec::new(),
|
||||
pending_skip: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the stored transcript with the provided items.
|
||||
pub(crate) fn replace(&mut self, items: Vec<ResponseItem>) {
|
||||
self.items = items;
|
||||
/// Replace the stored converted transcript. Returns any new tail items that
|
||||
/// were not present previously, preserving insertion order.
|
||||
pub(crate) fn replace_converted(&mut self, items: Vec<ResponseItem>) -> Vec<ResponseItem> {
|
||||
let prev_len = self.converted.len();
|
||||
self.converted = items;
|
||||
let mut tail: Vec<_> = if self.converted.len() <= prev_len {
|
||||
Vec::new()
|
||||
} else {
|
||||
self.converted
|
||||
.iter()
|
||||
.skip(prev_len)
|
||||
.cloned()
|
||||
.collect()
|
||||
};
|
||||
|
||||
if !tail.is_empty() && self.pending_skip > 0 {
|
||||
if self.pending_skip >= tail.len() {
|
||||
self.pending_skip -= tail.len();
|
||||
tail.clear();
|
||||
} else {
|
||||
tail.drain(0..self.pending_skip);
|
||||
self.pending_skip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
tail
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot(&self) -> Vec<ResponseItem> {
|
||||
self.items.clone()
|
||||
pub(crate) fn append_raw(&mut self, items: &[ResponseItem]) {
|
||||
if items.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.raw.extend(items.iter().cloned());
|
||||
self.pending_skip = self.pending_skip.saturating_add(items.len());
|
||||
}
|
||||
|
||||
pub(crate) fn raw_snapshot(&self) -> Vec<ResponseItem> {
|
||||
self.raw.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.items.clear();
|
||||
self.converted.clear();
|
||||
self.raw.clear();
|
||||
self.pending_skip = 0;
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.items.is_empty()
|
||||
pub(crate) fn converted_is_empty(&self) -> bool {
|
||||
self.converted.is_empty()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub(crate) fn raw_items(&self) -> &[ResponseItem] {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1038,6 +1038,7 @@ fn auto_decision_persists_summary_through_cli_cycle() {
|
||||
AutoCoordinatorStatus::Continue,
|
||||
"**Plan:** run tests".to_string(),
|
||||
Some("run tests".to_string()),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
assert_eq!(chat.auto_state.current_display_line.as_deref(), Some("Plan:"));
|
||||
@@ -1067,6 +1068,33 @@ fn auto_decision_persists_summary_through_cli_cycle() {
|
||||
assert_eq!(chat.auto_state.current_display_line.as_deref(), Some("Plan:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_history_captures_raw_transcript() {
|
||||
use codex_protocol::models::{ContentItem, ResponseItem};
|
||||
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
chat.auto_state.active = true;
|
||||
chat.auto_state.waiting_for_response = true;
|
||||
chat.auto_state.coordinator_waiting = true;
|
||||
|
||||
let transcript = vec![ResponseItem::Message {
|
||||
id: None,
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "{\n \"finish_status\": \"continue\"\n}".to_string(),
|
||||
}],
|
||||
}];
|
||||
|
||||
chat.auto_handle_decision(
|
||||
AutoCoordinatorStatus::Continue,
|
||||
"Next".to_string(),
|
||||
Some("do thing".to_string()),
|
||||
transcript.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(chat.auto_history.raw_items(), transcript.as_slice());
|
||||
}
|
||||
|
||||
fn pump_app_events(
|
||||
chat: &mut ChatWidget<'_>,
|
||||
rx: &std::sync::mpsc::Receiver<AppEvent>,
|
||||
|
||||
Reference in New Issue
Block a user