mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
feat(auto-drive): include AUTO_AGENTS guidance
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Project-level documentation discovery.
|
||||
//!
|
||||
//! Project-level documentation can be stored in files named `AGENTS.md`.
|
||||
//! Project-level documentation can be stored in files named `AGENTS.md` and
|
||||
//! Auto Drive can use specialised guidance stored in `AUTO_AGENTS.md`.
|
||||
//! We include the concatenation of all files found along the path from the
|
||||
//! repository root to the current working directory as follows:
|
||||
//!
|
||||
@@ -17,8 +18,11 @@ use std::path::PathBuf;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tracing::error;
|
||||
|
||||
/// Currently, we only match the filename `AGENTS.md` exactly.
|
||||
const CANDIDATE_FILENAMES: &[&str] = &["AGENTS.md"];
|
||||
/// Filenames recognised for standard agent instructions.
|
||||
const AGENT_FILENAMES: &[&str] = &["AGENTS.md"];
|
||||
|
||||
/// Filenames recognised for Auto Drive instructions.
|
||||
const AUTO_AGENT_FILENAMES: &[&str] = &["AUTO_AGENTS.md"];
|
||||
|
||||
/// When both `Config::instructions` and the project doc are present, they will
|
||||
/// be concatenated with the following separator.
|
||||
@@ -48,14 +52,17 @@ pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
|
||||
/// concatenation of all discovered docs. If no documentation file is found the
|
||||
/// function returns `Ok(None)`. Unexpected I/O failures bubble up as `Err` so
|
||||
/// callers can decide how to handle them.
|
||||
pub async fn read_project_docs(config: &Config) -> std::io::Result<Option<String>> {
|
||||
async fn read_project_docs_with_candidates(
|
||||
config: &Config,
|
||||
candidate_filenames: &[&str],
|
||||
) -> std::io::Result<Option<String>> {
|
||||
let max_total = config.project_doc_max_bytes;
|
||||
|
||||
if max_total == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let paths = discover_project_doc_paths(config)?;
|
||||
let paths = discover_project_doc_paths_with_candidates(config, candidate_filenames)?;
|
||||
if paths.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -101,12 +108,23 @@ pub async fn read_project_docs(config: &Config) -> std::io::Result<Option<String
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_project_docs(config: &Config) -> std::io::Result<Option<String>> {
|
||||
read_project_docs_with_candidates(config, AGENT_FILENAMES).await
|
||||
}
|
||||
|
||||
pub async fn read_auto_drive_docs(config: &Config) -> std::io::Result<Option<String>> {
|
||||
read_project_docs_with_candidates(config, AUTO_AGENT_FILENAMES).await
|
||||
}
|
||||
|
||||
/// Discover the list of AGENTS.md files using the same search rules as
|
||||
/// `read_project_docs`, but return the file paths instead of concatenated
|
||||
/// contents. The list is ordered from repository root to the current working
|
||||
/// directory (inclusive). Symlinks are allowed. When `project_doc_max_bytes`
|
||||
/// is zero, returns an empty list.
|
||||
pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBuf>> {
|
||||
fn discover_project_doc_paths_with_candidates(
|
||||
config: &Config,
|
||||
candidate_filenames: &[&str],
|
||||
) -> std::io::Result<Vec<PathBuf>> {
|
||||
let mut dir = config.cwd.clone();
|
||||
if let Ok(canon) = dir.canonicalize() {
|
||||
dir = canon;
|
||||
@@ -153,7 +171,7 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBu
|
||||
|
||||
let mut found: Vec<PathBuf> = Vec::new();
|
||||
for d in search_dirs {
|
||||
for name in CANDIDATE_FILENAMES {
|
||||
for name in candidate_filenames {
|
||||
let candidate = d.join(name);
|
||||
match std::fs::symlink_metadata(&candidate) {
|
||||
Ok(md) => {
|
||||
@@ -173,6 +191,14 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBu
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBuf>> {
|
||||
discover_project_doc_paths_with_candidates(config, AGENT_FILENAMES)
|
||||
}
|
||||
|
||||
pub fn discover_auto_drive_doc_paths(config: &Config) -> std::io::Result<Vec<PathBuf>> {
|
||||
discover_project_doc_paths_with_candidates(config, AUTO_AGENT_FILENAMES)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -347,4 +373,39 @@ mod tests {
|
||||
let res = get_user_instructions(&cfg).await.expect("doc expected");
|
||||
assert_eq!(res, "root doc\n\ncrate doc");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auto_drive_doc_missing_returns_none() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let cfg = make_config(&tmp, 4096, None);
|
||||
let res = read_auto_drive_docs(&cfg).await;
|
||||
assert!(res.expect("auto docs read").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auto_drive_docs_follow_same_lookup_rules() {
|
||||
let repo = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
std::fs::write(
|
||||
repo.path().join(".git"),
|
||||
"gitdir: /path/to/actual/git/dir\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
fs::write(repo.path().join("AUTO_AGENTS.md"), "root auto").unwrap();
|
||||
|
||||
let nested = repo.path().join("workspace/crate_a");
|
||||
std::fs::create_dir_all(&nested).unwrap();
|
||||
fs::write(nested.join("AUTO_AGENTS.md"), "nested auto").unwrap();
|
||||
|
||||
let mut cfg = make_config(&repo, 4096, None);
|
||||
cfg.cwd = nested;
|
||||
|
||||
let res = read_auto_drive_docs(&cfg)
|
||||
.await
|
||||
.expect("auto doc expected")
|
||||
.expect("auto doc should be present");
|
||||
|
||||
assert_eq!(res, "root auto\n\nnested auto");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_core::config::Config;
|
||||
use codex_core::config_types::ReasoningEffort;
|
||||
use codex_core::debug_logger::DebugLogger;
|
||||
use codex_core::model_family::{derive_default_model_family, find_family_for_model};
|
||||
use codex_core::project_doc::read_auto_drive_docs;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::{AuthManager, ModelClient, Prompt, ResponseEvent, TextFormat};
|
||||
use codex_core::error::CodexErr;
|
||||
@@ -17,7 +18,7 @@ use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{self, json, Value};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::app_event::{AppEvent, AutoCoordinatorStatus};
|
||||
@@ -167,6 +168,27 @@ fn run_auto_loop(
|
||||
)),
|
||||
);
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("creating runtime for auto coordinator")?;
|
||||
|
||||
let auto_instructions = match runtime.block_on(read_auto_drive_docs(config.as_ref())) {
|
||||
Ok(Some(text)) => {
|
||||
let trimmed = text.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed)
|
||||
}
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
warn!("failed to read AUTO_AGENTS.md instructions: {err:#}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let sandbox_label = if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
|
||||
"full access"
|
||||
} else {
|
||||
@@ -179,11 +201,6 @@ fn run_auto_loop(
|
||||
let platform = std::env::consts::OS;
|
||||
debug!("[Auto coordinator] starting: goal={goal_text} platform={platform}");
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("creating runtime for auto coordinator")?;
|
||||
|
||||
let mut pending_conversation = Some(initial_conversation);
|
||||
let mut stopped = false;
|
||||
let mut requests_completed: u64 = 0;
|
||||
@@ -224,6 +241,7 @@ fn run_auto_loop(
|
||||
&primary_goal_message,
|
||||
&schema,
|
||||
conv,
|
||||
auto_instructions.as_deref(),
|
||||
&app_event_tx,
|
||||
&cancel_token,
|
||||
) {
|
||||
@@ -416,6 +434,7 @@ fn request_coordinator_decision(
|
||||
primary_goal: &str,
|
||||
schema: &Value,
|
||||
conversation: Vec<ResponseItem>,
|
||||
auto_instructions: Option<&str>,
|
||||
app_event_tx: &AppEventSender,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> Result<(AutoCoordinatorStatus, String, Option<String>)> {
|
||||
@@ -426,6 +445,7 @@ fn request_coordinator_decision(
|
||||
primary_goal,
|
||||
schema,
|
||||
&conversation,
|
||||
auto_instructions,
|
||||
app_event_tx,
|
||||
cancel_token,
|
||||
)?;
|
||||
@@ -465,6 +485,7 @@ fn request_decision(
|
||||
primary_goal: &str,
|
||||
schema: &Value,
|
||||
conversation: &[ResponseItem],
|
||||
auto_instructions: Option<&str>,
|
||||
app_event_tx: &AppEventSender,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> Result<String> {
|
||||
@@ -475,6 +496,7 @@ fn request_decision(
|
||||
primary_goal,
|
||||
schema,
|
||||
conversation,
|
||||
auto_instructions,
|
||||
app_event_tx,
|
||||
cancel_token,
|
||||
MODEL_SLUG,
|
||||
@@ -496,6 +518,7 @@ fn request_decision(
|
||||
primary_goal,
|
||||
schema,
|
||||
conversation,
|
||||
auto_instructions,
|
||||
app_event_tx,
|
||||
cancel_token,
|
||||
&fallback_slug,
|
||||
@@ -519,6 +542,7 @@ fn request_decision_with_model(
|
||||
primary_goal: &str,
|
||||
schema: &Value,
|
||||
conversation: &[ResponseItem],
|
||||
auto_instructions: Option<&str>,
|
||||
app_event_tx: &AppEventSender,
|
||||
cancel_token: &CancellationToken,
|
||||
model_slug: &str,
|
||||
@@ -527,6 +551,7 @@ fn request_decision_with_model(
|
||||
let primary_goal = primary_goal.to_string();
|
||||
let schema = schema.clone();
|
||||
let conversation: Vec<ResponseItem> = conversation.to_vec();
|
||||
let auto_instructions = auto_instructions.map(|text| text.to_string());
|
||||
let tx = app_event_tx.clone();
|
||||
let cancel = cancel_token.clone();
|
||||
let classify = |error: &anyhow::Error| classify_model_error(error);
|
||||
@@ -535,12 +560,14 @@ fn request_decision_with_model(
|
||||
let result = runtime.block_on(async move {
|
||||
retry_with_backoff(
|
||||
|| {
|
||||
let instructions = auto_instructions.clone();
|
||||
let prompt = build_prompt_for_model(
|
||||
&developer_intro,
|
||||
&primary_goal,
|
||||
&schema,
|
||||
&conversation,
|
||||
model_slug,
|
||||
instructions.as_deref(),
|
||||
);
|
||||
let tx_inner = tx.clone();
|
||||
async move {
|
||||
@@ -651,9 +678,18 @@ fn build_prompt_for_model(
|
||||
schema: &Value,
|
||||
conversation: &[ResponseItem],
|
||||
model_slug: &str,
|
||||
auto_instructions: Option<&str>,
|
||||
) -> Prompt {
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.store = true;
|
||||
if let Some(instructions) = auto_instructions {
|
||||
let trimmed = instructions.trim();
|
||||
if !trimmed.is_empty() {
|
||||
prompt
|
||||
.input
|
||||
.push(make_message("developer", trimmed.to_string()));
|
||||
}
|
||||
}
|
||||
prompt
|
||||
.input
|
||||
.push(make_message("developer", developer_intro.to_string()));
|
||||
|
||||
Reference in New Issue
Block a user