mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(auto-drive): surface plan updates to coordinator
This commit is contained in:
@@ -137,6 +137,14 @@ impl ModelClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn default_model_slug(&self) -> &str {
|
||||
self.config.model.as_str()
|
||||
}
|
||||
|
||||
pub fn default_model_family(&self) -> &ModelFamily {
|
||||
&self.config.model_family
|
||||
}
|
||||
|
||||
/// Dispatches to either the Responses or Chat implementation depending on
|
||||
/// the provider config. Public callers always invoke `stream()` – the
|
||||
/// specialised helpers are private to avoid accidental misuse.
|
||||
|
||||
@@ -3221,6 +3221,48 @@ impl ChatWidget<'_> {
|
||||
content: vec![content],
|
||||
});
|
||||
}
|
||||
crate::history_cell::HistoryCellType::PlanUpdate => {
|
||||
if let Some(plan) = cell
|
||||
.as_any()
|
||||
.downcast_ref::<crate::history_cell::PlanUpdateCell>()
|
||||
{
|
||||
let state = plan.state();
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
if !state.name.trim().is_empty() {
|
||||
lines.push(format!("Plan update: {}", state.name.trim()));
|
||||
} else {
|
||||
lines.push("Plan update".to_string());
|
||||
}
|
||||
|
||||
if state.progress.total > 0 {
|
||||
lines.push(format!(
|
||||
"Progress: {}/{}",
|
||||
state.progress.completed, state.progress.total
|
||||
));
|
||||
}
|
||||
|
||||
if state.steps.is_empty() {
|
||||
lines.push("(no steps recorded)".to_string());
|
||||
} else {
|
||||
for step in &state.steps {
|
||||
let status_label = match step.status {
|
||||
StepStatus::Completed => "[completed]",
|
||||
StepStatus::InProgress => "[in_progress]",
|
||||
StepStatus::Pending => "[pending]",
|
||||
};
|
||||
lines.push(format!("- {} {}", status_label, step.description));
|
||||
}
|
||||
}
|
||||
|
||||
let text = lines.join("\n");
|
||||
let content = ContentItem::OutputText { text };
|
||||
items.push(ResponseItem::Message {
|
||||
id: None,
|
||||
role: "assistant".to_string(),
|
||||
content: vec![content],
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +454,61 @@ fn request_decision(
|
||||
conversation: &[ResponseItem],
|
||||
app_event_tx: &AppEventSender,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> Result<String> {
|
||||
match request_decision_with_model(
|
||||
runtime,
|
||||
client,
|
||||
developer_intro,
|
||||
primary_goal,
|
||||
schema,
|
||||
conversation,
|
||||
app_event_tx,
|
||||
cancel_token,
|
||||
MODEL_SLUG,
|
||||
) {
|
||||
Ok(output) => Ok(output),
|
||||
Err(err) => {
|
||||
let fallback_slug = client.default_model_slug().to_string();
|
||||
if fallback_slug != MODEL_SLUG && should_retry_with_default_model(&err) {
|
||||
debug!(
|
||||
preferred = %MODEL_SLUG,
|
||||
fallback = %fallback_slug,
|
||||
"auto coordinator falling back to configured model after invalid model error"
|
||||
);
|
||||
let original_error = err.to_string();
|
||||
return request_decision_with_model(
|
||||
runtime,
|
||||
client,
|
||||
developer_intro,
|
||||
primary_goal,
|
||||
schema,
|
||||
conversation,
|
||||
app_event_tx,
|
||||
cancel_token,
|
||||
&fallback_slug,
|
||||
)
|
||||
.map_err(|fallback_err| {
|
||||
fallback_err.context(format!(
|
||||
"coordinator fallback with model '{}' failed after original error: {}",
|
||||
fallback_slug, original_error
|
||||
))
|
||||
});
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn request_decision_with_model(
|
||||
runtime: &tokio::runtime::Runtime,
|
||||
client: &ModelClient,
|
||||
developer_intro: &str,
|
||||
primary_goal: &str,
|
||||
schema: &Value,
|
||||
conversation: &[ResponseItem],
|
||||
app_event_tx: &AppEventSender,
|
||||
cancel_token: &CancellationToken,
|
||||
model_slug: &str,
|
||||
) -> Result<String> {
|
||||
let developer_intro = developer_intro.to_string();
|
||||
let primary_goal = primary_goal.to_string();
|
||||
@@ -467,11 +522,12 @@ fn request_decision(
|
||||
let result = runtime.block_on(async move {
|
||||
retry_with_backoff(
|
||||
|| {
|
||||
let prompt = build_prompt(
|
||||
let prompt = build_prompt_for_model(
|
||||
&developer_intro,
|
||||
&primary_goal,
|
||||
&schema,
|
||||
&conversation,
|
||||
model_slug,
|
||||
);
|
||||
let tx_inner = tx.clone();
|
||||
async move {
|
||||
@@ -576,11 +632,12 @@ fn request_decision(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_prompt(
|
||||
fn build_prompt_for_model(
|
||||
developer_intro: &str,
|
||||
primary_goal: &str,
|
||||
schema: &Value,
|
||||
conversation: &[ResponseItem],
|
||||
model_slug: &str,
|
||||
) -> Prompt {
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.store = true;
|
||||
@@ -597,13 +654,31 @@ fn build_prompt(
|
||||
strict: Some(true),
|
||||
schema: Some(schema.clone()),
|
||||
});
|
||||
prompt.model_override = Some(MODEL_SLUG.to_string());
|
||||
let family = find_family_for_model(MODEL_SLUG)
|
||||
.unwrap_or_else(|| derive_default_model_family(MODEL_SLUG));
|
||||
prompt.model_override = Some(model_slug.to_string());
|
||||
let family = find_family_for_model(model_slug)
|
||||
.unwrap_or_else(|| derive_default_model_family(model_slug));
|
||||
prompt.model_family_override = Some(family);
|
||||
prompt
|
||||
}
|
||||
|
||||
fn should_retry_with_default_model(err: &anyhow::Error) -> bool {
|
||||
err.chain().any(|cause| {
|
||||
if let Some(codex_err) = cause.downcast_ref::<CodexErr>() {
|
||||
if let CodexErr::UnexpectedStatus(status, body) = codex_err {
|
||||
if !status.is_client_error() {
|
||||
return false;
|
||||
}
|
||||
let body_lower = body.to_lowercase();
|
||||
return body_lower.contains("invalid model")
|
||||
|| body_lower.contains("unknown model")
|
||||
|| body_lower.contains("model_not_found")
|
||||
|| body_lower.contains("model does not exist");
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_model_error(error: &anyhow::Error) -> RetryDecision {
|
||||
if let Some(codex_err) = find_in_chain::<CodexErr>(error) {
|
||||
match codex_err {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use codex_core::{ModelClient, Prompt, ResponseEvent, TextFormat};
|
||||
use codex_core::{error::CodexErr, ModelClient, Prompt, ResponseEvent, TextFormat};
|
||||
use codex_core::model_family::{derive_default_model_family, find_family_for_model};
|
||||
use codex_protocol::models::{ContentItem, ResponseItem};
|
||||
use futures::StreamExt;
|
||||
@@ -148,27 +148,36 @@ fn evaluate_observer(
|
||||
client: Arc<ModelClient>,
|
||||
trigger: ObserverTrigger,
|
||||
) -> Result<(AutoObserverStatus, Option<String>, Option<String>)> {
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.store = true;
|
||||
|
||||
let instructions = build_observer_instructions(&trigger.environment_details);
|
||||
prompt.input.push(make_message("developer", instructions));
|
||||
let goal = format!("Primary Goal\n{}", trigger.goal_text);
|
||||
prompt.input.push(make_message("developer", goal));
|
||||
prompt.input.extend(trigger.conversation);
|
||||
|
||||
let schema = build_observer_schema();
|
||||
prompt.text_format = Some(TextFormat {
|
||||
r#type: "json_schema".to_string(),
|
||||
name: Some(OBSERVER_SCHEMA_NAME.to_string()),
|
||||
strict: Some(true),
|
||||
schema: Some(schema),
|
||||
});
|
||||
prompt.model_override = Some(MODEL_SLUG.to_string());
|
||||
let family = find_family_for_model(MODEL_SLUG)
|
||||
.unwrap_or_else(|| derive_default_model_family(MODEL_SLUG));
|
||||
prompt.model_family_override = Some(family);
|
||||
let prompt = build_observer_prompt(&trigger, MODEL_SLUG);
|
||||
match run_observer_prompt(runtime, client.clone(), prompt) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(err) => {
|
||||
let fallback_slug = client.default_model_slug().to_string();
|
||||
if should_retry_with_default_model(&err) && fallback_slug != MODEL_SLUG {
|
||||
debug!(
|
||||
preferred = %MODEL_SLUG,
|
||||
fallback = %fallback_slug,
|
||||
"auto observer falling back to configured model after invalid model error"
|
||||
);
|
||||
let fallback_prompt = build_observer_prompt(&trigger, &fallback_slug);
|
||||
let original_error = err.to_string();
|
||||
return run_observer_prompt(runtime, client, fallback_prompt).map_err(|fallback_err| {
|
||||
fallback_err.context(format!(
|
||||
"observer fallback with model '{}' failed after original error: {}",
|
||||
fallback_slug, original_error
|
||||
))
|
||||
});
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_observer_prompt(
|
||||
runtime: &tokio::runtime::Runtime,
|
||||
client: Arc<ModelClient>,
|
||||
prompt: Prompt,
|
||||
) -> Result<(AutoObserverStatus, Option<String>, Option<String>)> {
|
||||
let raw = runtime.block_on(async {
|
||||
request_observer_response(client.clone(), &prompt).await
|
||||
})?;
|
||||
@@ -206,6 +215,48 @@ fn evaluate_observer(
|
||||
Ok((status, replace_message, additional_instructions))
|
||||
}
|
||||
|
||||
fn build_observer_prompt(trigger: &ObserverTrigger, model_slug: &str) -> Prompt {
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.store = true;
|
||||
|
||||
let instructions = build_observer_instructions(&trigger.environment_details);
|
||||
prompt.input.push(make_message("developer", instructions));
|
||||
let goal = format!("Primary Goal\n{}", trigger.goal_text);
|
||||
prompt.input.push(make_message("developer", goal));
|
||||
prompt.input.extend(trigger.conversation.clone());
|
||||
|
||||
let schema = build_observer_schema();
|
||||
prompt.text_format = Some(TextFormat {
|
||||
r#type: "json_schema".to_string(),
|
||||
name: Some(OBSERVER_SCHEMA_NAME.to_string()),
|
||||
strict: Some(true),
|
||||
schema: Some(schema),
|
||||
});
|
||||
prompt.model_override = Some(model_slug.to_string());
|
||||
let family = find_family_for_model(model_slug)
|
||||
.unwrap_or_else(|| derive_default_model_family(model_slug));
|
||||
prompt.model_family_override = Some(family);
|
||||
prompt
|
||||
}
|
||||
|
||||
fn should_retry_with_default_model(err: &anyhow::Error) -> bool {
|
||||
err.chain().any(|cause| {
|
||||
if let Some(codex_err) = cause.downcast_ref::<CodexErr>() {
|
||||
if let CodexErr::UnexpectedStatus(status, body) = codex_err {
|
||||
if !status.is_client_error() {
|
||||
return false;
|
||||
}
|
||||
let body_lower = body.to_lowercase();
|
||||
return body_lower.contains("invalid model")
|
||||
|| body_lower.contains("unknown model")
|
||||
|| body_lower.contains("model_not_found")
|
||||
|| body_lower.contains("model does not exist");
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
async fn request_observer_response(
|
||||
client: Arc<ModelClient>,
|
||||
prompt: &Prompt,
|
||||
|
||||
Reference in New Issue
Block a user