mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(auto-drive): promote helper agents to write mode
This commit is contained in:
@@ -53,7 +53,7 @@ agent {
|
||||
"files": ["services/api", "services/api/src", "services/api/Cargo.toml"],
|
||||
"models": ["claude-sonnet-4.5","code-gpt-5-codex","`gemini-2.5-pro"],
|
||||
"output": "Middleware + passing tests + README snippet",
|
||||
"read_only": false // Allow changes - will launch every agent in a separate worktree
|
||||
"write": true // Allow changes - will launch every agent in a separate worktree
|
||||
}
|
||||
}
|
||||
agent {"action":"wait","wait":{"batch_id":"<batch_id>","return_all":true,"timeout_seconds":600}} // Long timeout or you can do separate work and check back later.
|
||||
|
||||
@@ -17,7 +17,7 @@ Progress
|
||||
|
||||
Planning for Long Missions
|
||||
- Establish a clear North Star (metric/acceptance check) and periodically re‑validate against it.
|
||||
- Early: fan out 2–3 independent avenues (research, repro/proto, market/metrics). Use agents for breadth; keep them read‑only unless isolated writes are needed.
|
||||
- Early: fan out 2–3 independent avenues (research, repro/proto, market/metrics). Use agents for breadth; in Full Auto default to launching them with `write: true` so they can prototype in isolated worktrees. Drop to `write: false` only when the task is strictly analysis or review.
|
||||
- Mid: converge—prioritize the avenue with the strongest evidence; keep one “scout” agent probing risk or upside.
|
||||
- Late: lock down—tests, focused review, and polish before declaring success.
|
||||
|
||||
@@ -28,7 +28,7 @@ Choosing Instructions
|
||||
|
||||
- Agents (background):
|
||||
- Use for repros, benchmarks, data/market scans, prototypes, or long-running checks.
|
||||
- Keep prompts outcome-oriented (what artifact/insight to produce). Enabling writes gives the agents isolated worktrees to use.
|
||||
- Keep prompts outcome-oriented (what artifact/insight to produce). Prefer `write: true` in Full Auto so agents can create commits safely in their own worktrees; flip to `write: false` when you only need research or review output.
|
||||
- Set `agents.timing`:
|
||||
- `parallel` when the CLI should continue its prompt while the agents run (it may call `agent.wait` later when convenient).
|
||||
- `blocking` when the CLI must wait on `agent.wait` before progressing with its own prompt.
|
||||
|
||||
@@ -1206,11 +1206,19 @@ pub fn create_agent_tool(allowed_models: &[String]) -> OpenAiTool {
|
||||
allowed_values: None,
|
||||
},
|
||||
);
|
||||
create_properties.insert(
|
||||
"write".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
description: Some(
|
||||
"Enable isolated write worktrees for each agent (default: true). Set false to keep the agent read-only.".to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
create_properties.insert(
|
||||
"read_only".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
description: Some(
|
||||
"When true, run in read-only mode (default: false)".to_string(),
|
||||
"Deprecated: inverse of `write`. Prefer setting `write` instead.".to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
@@ -1378,6 +1386,9 @@ pub struct RunAgentParams {
|
||||
pub context: Option<String>,
|
||||
pub output: Option<String>,
|
||||
pub files: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub write: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub read_only: Option<bool>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
@@ -1390,6 +1401,9 @@ pub struct AgentCreateOptions {
|
||||
pub context: Option<String>,
|
||||
pub output: Option<String>,
|
||||
pub files: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub write: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub read_only: Option<bool>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
+45
-15
@@ -6363,12 +6363,18 @@ fn to_exec_params(params: ShellToolCallParams, sess: &Session) -> ExecParams {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_agent_read_only(requested: Option<bool>, config: Option<&crate::config_types::AgentConfig>) -> bool {
|
||||
if let Some(flag) = requested {
|
||||
flag
|
||||
} else {
|
||||
config.map(|c| c.read_only).unwrap_or(false)
|
||||
fn resolve_agent_read_only(
|
||||
write: Option<bool>,
|
||||
read_only: Option<bool>,
|
||||
config: Option<&crate::config_types::AgentConfig>,
|
||||
) -> bool {
|
||||
if let Some(flag) = write {
|
||||
return !flag;
|
||||
}
|
||||
if let Some(flag) = read_only {
|
||||
return flag;
|
||||
}
|
||||
config.map(|c| c.read_only).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -6392,21 +6398,36 @@ mod resolve_read_only_tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_flag_overrides_config_true() {
|
||||
fn explicit_write_overrides_config_read_only() {
|
||||
let cfg = make_config(true);
|
||||
assert!(!resolve_agent_read_only(Some(false), Some(&cfg)));
|
||||
assert!(
|
||||
!resolve_agent_read_only(Some(true), None, Some(&cfg)),
|
||||
"write=true should allow writes even when config prefers read-only"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_read_only_flag_takes_precedence() {
|
||||
let cfg = make_config(false);
|
||||
assert!(
|
||||
resolve_agent_read_only(None, Some(true), Some(&cfg)),
|
||||
"read_only=true should force read-only even when config allows writes"
|
||||
);
|
||||
assert!(
|
||||
resolve_agent_read_only(Some(false), None, Some(&cfg)),
|
||||
"write=false should force read-only"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_to_config_when_request_absent() {
|
||||
let cfg = make_config(true);
|
||||
assert!(resolve_agent_read_only(None, Some(&cfg)));
|
||||
assert!(resolve_agent_read_only(None, None, Some(&cfg)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_to_false_without_config() {
|
||||
assert!(!resolve_agent_read_only(None, None));
|
||||
assert!(resolve_agent_read_only(Some(true), None));
|
||||
assert!(!resolve_agent_read_only(None, None, None));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6503,6 +6524,7 @@ pub(crate) async fn handle_agent_tool(
|
||||
let context = create_opts.context.take();
|
||||
let output = create_opts.output.take();
|
||||
let files = create_opts.files.take();
|
||||
let write = create_opts.write.take();
|
||||
let read_only = create_opts.read_only.take();
|
||||
let mut normalized_name = normalize_agent_name(create_opts.name.take());
|
||||
if normalized_name.is_none() {
|
||||
@@ -6515,6 +6537,7 @@ pub(crate) async fn handle_agent_tool(
|
||||
context: context.clone(),
|
||||
output: output.clone(),
|
||||
files: files.clone(),
|
||||
write,
|
||||
read_only,
|
||||
name: normalized_name.clone(),
|
||||
};
|
||||
@@ -6557,8 +6580,11 @@ pub(crate) async fn handle_agent_tool(
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(ro) = read_only {
|
||||
create_event.insert("read_only".to_string(), serde_json::Value::Bool(ro));
|
||||
if let Some(flag) = write {
|
||||
create_event.insert("write".to_string(), serde_json::Value::Bool(flag));
|
||||
}
|
||||
if let Some(flag) = read_only {
|
||||
create_event.insert("read_only".to_string(), serde_json::Value::Bool(flag));
|
||||
}
|
||||
if let Some(ref name_str) = normalized_name {
|
||||
if !name_str.is_empty() {
|
||||
@@ -6940,7 +6966,11 @@ pub(crate) async fn handle_run_agent(
|
||||
}
|
||||
|
||||
// Respect explicit read_only flag from the caller; otherwise fall back to the config default.
|
||||
let read_only = resolve_agent_read_only(params.read_only, Some(config));
|
||||
let read_only = resolve_agent_read_only(
|
||||
params.write,
|
||||
params.read_only,
|
||||
Some(config),
|
||||
);
|
||||
|
||||
let agent_id = manager
|
||||
.create_agent_with_config(
|
||||
@@ -6965,7 +6995,7 @@ pub(crate) async fn handle_run_agent(
|
||||
skipped.push(format!("{} (missing: {})", model, cmd_to_check));
|
||||
continue;
|
||||
}
|
||||
let read_only = resolve_agent_read_only(params.read_only, None);
|
||||
let read_only = resolve_agent_read_only(params.write, params.read_only, None);
|
||||
let agent_id = manager
|
||||
.create_agent(
|
||||
model.clone(),
|
||||
@@ -6986,7 +7016,7 @@ pub(crate) async fn handle_run_agent(
|
||||
|
||||
// If nothing runnable remains, fall back to a single built‑in Codex agent.
|
||||
if agent_ids.is_empty() {
|
||||
let read_only = resolve_agent_read_only(params.read_only, None);
|
||||
let read_only = resolve_agent_read_only(params.write, params.read_only, None);
|
||||
let agent_id = manager
|
||||
.create_agent(
|
||||
"code".to_string(),
|
||||
|
||||
@@ -82,7 +82,7 @@ pub fn default_instructions_for(name: &str) -> Option<String> {
|
||||
|
||||
1. If you do not fully understand the task, research it briefly. Do not attempt to code or solve it, just understand the task in the context of the current code base.
|
||||
2. Provide full context to the agents so they can work on the task themselves. You do not need to guide them on how to write the code - focus on describing the current task and desired outcome.
|
||||
3. Start agents with read-only: false - each agents will work in a separate worktree and can:
|
||||
3. Start agents with write: true - each agent will work in a separate worktree and can:
|
||||
- Read and analyze existing code
|
||||
- Create new files
|
||||
- Modify existing files
|
||||
@@ -133,11 +133,12 @@ pub fn format_subagent_command(
|
||||
.or_else(|| default_instructions_for(name))
|
||||
.unwrap_or_default();
|
||||
|
||||
let write_flag = !read_only;
|
||||
let prompt = format!(
|
||||
"Please perform /{name} using the <tools>, <instructions> and <task> below.\n<tools>\n To perform /{name} you must use `agent {{\"action\":\"create\",\"create\":{{\"models\":[{models}],\"read_only\":{ro}}}}}` to start a batch of agents.\n Provide a comprehensive description of the task and context. You may need to briefly research the code base first and to give the agents a head start of where to look. You can include one or two key files but also allow the models to look up the files they need themselves. Using the create action starts all agents at once and returns a `batch_id`.\n\n Each agent uses a different LLM which allows you to gather diverse results.\n Monitor progress using `agent {{\"action\":\"wait\",\"wait\":{{\"batch_id\":\"<batch_id>\",\"return_all\":true}}}}` to wait for all agents to complete.\n If an agent fails or times out, you can ignore it and continue with the other results.\n Use `agent {{\"action\":\"result\",\"result\":{{\"agent_id\":\"<agent_id>\"}}}}` to fetch detailed output, or inspect the worktree directly if `read_only` is false.\n</tools>\n<instructions>\n Instructions for /{name}:\n {instructions}\n</instructions>\n<task>\n Task for /{name}:\n {task}\n</task>",
|
||||
"Please perform /{name} using the <tools>, <instructions> and <task> below.\n<tools>\n To perform /{name} you must use `agent {{\"action\":\"create\",\"create\":{{\"models\":[{models}],\"write\":{write_flag}}}}}` to start a batch of agents.\n Provide a comprehensive description of the task and context. You may need to briefly research the code base first and to give the agents a head start of where to look. You can include one or two key files but also allow the models to look up the files they need themselves. Using the create action starts all agents at once and returns a `batch_id`.\n\n Each agent uses a different LLM which allows you to gather diverse results.\n Monitor progress using `agent {{\"action\":\"wait\",\"wait\":{{\"batch_id\":\"<batch_id>\",\"return_all\":true}}}}` to wait for all agents to complete.\n If an agent fails or times out, you can ignore it and continue with the other results.\n Use `agent {{\"action\":\"result\",\"result\":{{\"agent_id\":\"<agent_id>\"}}}}` to fetch detailed output, or inspect the worktree directly when `write` is true.\n</tools>\n<instructions>\n Instructions for /{name}:\n {instructions}\n</instructions>\n<task>\n Task for /{name}:\n {task}\n</task>",
|
||||
name = name,
|
||||
models = models_str,
|
||||
ro = read_only,
|
||||
write_flag = write_flag,
|
||||
instructions = instr_text,
|
||||
task = task,
|
||||
);
|
||||
|
||||
@@ -12797,15 +12797,48 @@ fi\n\
|
||||
|
||||
self.pending_turn_descriptor = None;
|
||||
self.pending_auto_turn_config = None;
|
||||
|
||||
let mut promoted_agents: Vec<String> = Vec::new();
|
||||
if matches!(status, AutoCoordinatorStatus::Continue) {
|
||||
self.auto_state.pending_agent_actions = agents.clone();
|
||||
self.auto_state.pending_agent_timing =
|
||||
agents_timing.filter(|_| !self.auto_state.pending_agent_actions.is_empty());
|
||||
let resolved_agents: Vec<AutoTurnAgentsAction> = agents
|
||||
.into_iter()
|
||||
.map(|mut action| {
|
||||
let original = action.write;
|
||||
let resolved = self.resolve_agent_write_flag(&action);
|
||||
if resolved && !original {
|
||||
promoted_agents.push(action.prompt.clone());
|
||||
}
|
||||
action.write = resolved;
|
||||
action
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.auto_state.pending_agent_actions = resolved_agents;
|
||||
self.auto_state.pending_agent_timing = agents_timing
|
||||
.filter(|_| !self.auto_state.pending_agent_actions.is_empty());
|
||||
} else {
|
||||
self.auto_state.pending_agent_actions.clear();
|
||||
self.auto_state.pending_agent_timing = None;
|
||||
}
|
||||
|
||||
if !promoted_agents.is_empty() {
|
||||
let joined = promoted_agents
|
||||
.into_iter()
|
||||
.map(|prompt| {
|
||||
let trimmed = prompt.trim();
|
||||
if trimmed.is_empty() {
|
||||
"<empty prompt>".to_string()
|
||||
} else {
|
||||
format!("\"{trimmed}\"")
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
self.push_background_tail(format!(
|
||||
"Auto Drive enabled write mode for agent prompt(s): {joined}"
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(review_action) = code_review {
|
||||
self.auto_trigger_review_action(review_action);
|
||||
}
|
||||
@@ -13564,9 +13597,9 @@ fi\n\
|
||||
.trim()
|
||||
.replace('\n', " ")
|
||||
.replace('"', "\\\"");
|
||||
let read_only_text = if action.write { "read_only: false" } else { "read_only: true" };
|
||||
let write_text = if action.write { "write: true" } else { "write: false" };
|
||||
let mut line = format!(
|
||||
"Please run agent.create with {read_only_text} and prompt like \"{prompt}\"."
|
||||
"Please run agent.create with {write_text} and prompt like \"{prompt}\"."
|
||||
);
|
||||
if let Some(ctx) = action
|
||||
.context
|
||||
@@ -13605,6 +13638,54 @@ fi\n\
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_agents_can_write(&self) -> bool {
|
||||
matches!(
|
||||
self.config.sandbox_policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::WorkspaceWrite { .. }
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_agent_write_flag(&self, action: &AutoTurnAgentsAction) -> bool {
|
||||
if !self.auto_state.subagents_enabled {
|
||||
return false;
|
||||
}
|
||||
if !self.auto_agents_can_write() {
|
||||
return false;
|
||||
}
|
||||
if action.write {
|
||||
return true;
|
||||
}
|
||||
if Self::agent_prompt_prefers_read_only(&action.prompt) {
|
||||
return false;
|
||||
}
|
||||
if let Some(ctx) = action.context.as_deref() {
|
||||
if Self::agent_prompt_prefers_read_only(ctx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn agent_prompt_prefers_read_only(text: &str) -> bool {
|
||||
const READ_ONLY_KEYWORDS: [&str; 10] = [
|
||||
"investigate",
|
||||
"research",
|
||||
"review",
|
||||
"analyze",
|
||||
"analysis",
|
||||
"audit",
|
||||
"assess",
|
||||
"summarize",
|
||||
"outline",
|
||||
"plan",
|
||||
];
|
||||
let lowered = text.to_ascii_lowercase();
|
||||
lowered
|
||||
.split(|ch: char| !ch.is_alphanumeric())
|
||||
.filter(|token| !token.is_empty())
|
||||
.any(|token| READ_ONLY_KEYWORDS.contains(&token))
|
||||
}
|
||||
|
||||
fn auto_stop(&mut self, message: Option<String>) {
|
||||
self.auto_flush_observer_banners();
|
||||
if let Some(handle) = self.auto_handle.take() {
|
||||
@@ -21462,7 +21543,20 @@ mod tests {
|
||||
);
|
||||
let action = &chat.auto_state.pending_agent_actions[0];
|
||||
assert_eq!(action.prompt, "Draft alternative fix");
|
||||
assert!(!action.write);
|
||||
assert!(action.write);
|
||||
|
||||
let notice = "Auto Drive enabled write mode";
|
||||
let write_notice_present = chat
|
||||
.history_cells
|
||||
.iter()
|
||||
.any(|cell| {
|
||||
cell.display_lines_trimmed().iter().any(|line| {
|
||||
line.spans
|
||||
.iter()
|
||||
.any(|span| span.content.contains(notice))
|
||||
})
|
||||
});
|
||||
assert!(write_notice_present);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -21568,7 +21662,7 @@ mod tests {
|
||||
assert!(message.contains("Workspace root: /tmp"));
|
||||
assert!(message.contains("Run diagnostics"));
|
||||
assert!(message.contains("Please run agent.create"));
|
||||
assert!(message.contains("read_only: true"));
|
||||
assert!(message.contains("write: false"));
|
||||
assert!(message.contains("Models: [claude-sonnet-4.5, gemini-2.5-pro]"));
|
||||
assert!(message.contains("Draft alternative fix"));
|
||||
assert!(message.contains("Focus on parser module"));
|
||||
|
||||
Reference in New Issue
Block a user