feat(team): port subagent_model, parallel tool exec, team coordination layer
Port the team-enhancement feature layer from feat/multi-tool-exec onto
feat/team-test so the agent-teams stack is buildable and testable on top
of the current CLI/runtime.
Subagent model (config wiring):
- Add subagent_model field to RuntimeFeatureConfig + RuntimeConfig::subagent_model()
accessor + parse_optional_subagent_model() so the subagentModel setting
(already validated by config_validate.rs) is now actually read and stored.
- Agent tool's resolve_agent_model falls back to subagentModel from config
when no explicit model is passed.
Parallel tool execution:
- Re-export ToolCall/ToolResult/TurnProgressReporter from runtime so the
CliToolExecutor::execute_batch override type-checks.
- Override execute_batch to classify read-only tools (read_file, glob/grep
search, WebFetch/WebSearch, LSP, Git*, ToolSearch, Skill, Agent*,
TeamStatus, Task*) as parallel-safe and run them concurrently via
std:🧵:scope; everything else stays sequential. Results return in
original model order.
Team coordination layer (cherry-picked from feat/multi-tool-exec):
- AgentMessage, TaskClaim, TeamStatus tools + shared mailbox directory +
mode presets (tiny/1x ... mega/6x) + enriched TeamCreate/Agent tool
descriptions + background team watcher.
- /team slash command: on/off/status/toggle of CLAWD_AGENT_TEAMS (the flag
TeamCreate requires), replacing the info-only stub. Spec updated to
[on|off|status].
- resolve_conflicts resolved: de-duplicated SlashCommand::Team/Setup arms
and ReadOutcome::TeamToggle arms that the cherry-picks overlapped with
the existing /team parse fix.
Verification:
- cargo build --workspace OK
- cargo test -p tools --lib: 111 passed (1 env failure: repl_executes_python_code)
- cargo test -p runtime --lib team_cron: 17 passed
- cargo test -p commands --lib: 42 passed (incl /team parse regression)
- cargo test -p rusty-claude-cli --bin claw: 345 passed
- scripts/fmt.sh --check OK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1b6ef83f78
commit
3438fe8f2d
|
|
@ -29,7 +29,7 @@ pub use providers::openai_compat::{
|
||||||
pub use providers::{
|
pub use providers::{
|
||||||
detect_provider_kind, max_tokens_for_model, max_tokens_for_model_with_override,
|
detect_provider_kind, max_tokens_for_model, max_tokens_for_model_with_override,
|
||||||
model_family_identity_for, model_family_identity_for_kind, model_token_limit,
|
model_family_identity_for, model_family_identity_for_kind, model_token_limit,
|
||||||
ModelTokenLimit, provider_diagnostics_for_model, resolve_model_alias, ProviderDiagnostics,
|
provider_diagnostics_for_model, resolve_model_alias, ModelTokenLimit, ProviderDiagnostics,
|
||||||
ProviderKind,
|
ProviderKind,
|
||||||
};
|
};
|
||||||
pub use sse::{parse_frame, SseParser};
|
pub use sse::{parse_frame, SseParser};
|
||||||
|
|
|
||||||
|
|
@ -680,7 +680,8 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
|
||||||
context_window_tokens: 256_000,
|
context_window_tokens: 256_000,
|
||||||
}),
|
}),
|
||||||
// Qwen models via DashScope / OpenAI-compat
|
// Qwen models via DashScope / OpenAI-compat
|
||||||
"qwen3.6-35b-fast" | "qwen3-235b-a22b" | "qwen-max" | "qwen-plus" | "qwen-turbo" | "qwen-qwq" => Some(ModelTokenLimit {
|
"qwen3.6-35b-fast" | "qwen3-235b-a22b" | "qwen-max" | "qwen-plus" | "qwen-turbo"
|
||||||
|
| "qwen-qwq" => Some(ModelTokenLimit {
|
||||||
max_output_tokens: 16_384,
|
max_output_tokens: 16_384,
|
||||||
context_window_tokens: 131_072,
|
context_window_tokens: 131_072,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -815,7 +815,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||||
name: "team",
|
name: "team",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
summary: "Manage agent teams",
|
summary: "Manage agent teams",
|
||||||
argument_hint: Some("[list|create|delete]"),
|
argument_hint: Some("[on|off|status]"),
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
|
|
@ -5545,9 +5545,9 @@ mod tests {
|
||||||
Ok(Some(SlashCommand::Team { action: None }))
|
Ok(Some(SlashCommand::Team { action: None }))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SlashCommand::parse("/team list"),
|
SlashCommand::parse("/team on"),
|
||||||
Ok(Some(SlashCommand::Team {
|
Ok(Some(SlashCommand::Team {
|
||||||
action: Some("list".to_string())
|
action: Some("on".to_string())
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -1734,7 +1734,11 @@ fn parse_optional_model(root: &JsonValue) -> Option<String> {
|
||||||
/// falls back to the default model.
|
/// falls back to the default model.
|
||||||
fn parse_optional_subagent_model(root: &JsonValue) -> Option<String> {
|
fn parse_optional_subagent_model(root: &JsonValue) -> Option<String> {
|
||||||
root.as_object()
|
root.as_object()
|
||||||
.and_then(|object| object.get("subagentModel").or_else(|| object.get("subagent_model")))
|
.and_then(|object| {
|
||||||
|
object
|
||||||
|
.get("subagentModel")
|
||||||
|
.or_else(|| object.get("subagent_model"))
|
||||||
|
})
|
||||||
.and_then(JsonValue::as_str)
|
.and_then(JsonValue::as_str)
|
||||||
.filter(|s| !s.trim().is_empty())
|
.filter(|s| !s.trim().is_empty())
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
|
|
|
||||||
|
|
@ -284,10 +284,7 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_turn_progress_reporter(
|
pub fn with_turn_progress_reporter(mut self, reporter: Box<dyn TurnProgressReporter>) -> Self {
|
||||||
mut self,
|
|
||||||
reporter: Box<dyn TurnProgressReporter>,
|
|
||||||
) -> Self {
|
|
||||||
self.turn_progress_reporter = Some(reporter);
|
self.turn_progress_reporter = Some(reporter);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -578,7 +575,13 @@ where
|
||||||
// Phase 3: Post-hooks and session updates (sequential, original order).
|
// Phase 3: Post-hooks and session updates (sequential, original order).
|
||||||
for p in &pending {
|
for p in &pending {
|
||||||
// Capture progress data for the reporter.
|
// Capture progress data for the reporter.
|
||||||
let (progress_tool_name, progress_input, progress_output, progress_is_error, result_message) = if p.allowed {
|
let (
|
||||||
|
progress_tool_name,
|
||||||
|
progress_input,
|
||||||
|
progress_output,
|
||||||
|
progress_is_error,
|
||||||
|
result_message,
|
||||||
|
) = if p.allowed {
|
||||||
let batch_result = &batch_results[batch_index];
|
let batch_result = &batch_results[batch_index];
|
||||||
batch_index += 1;
|
batch_index += 1;
|
||||||
let (mut output, mut is_error) = match &batch_result.result {
|
let (mut output, mut is_error) = match &batch_result.result {
|
||||||
|
|
@ -621,16 +624,32 @@ where
|
||||||
output,
|
output,
|
||||||
is_error,
|
is_error,
|
||||||
);
|
);
|
||||||
(p.tool_name.clone(), p.effective_input.clone(), progress_output, is_error, result_message)
|
(
|
||||||
|
p.tool_name.clone(),
|
||||||
|
p.effective_input.clone(),
|
||||||
|
progress_output,
|
||||||
|
is_error,
|
||||||
|
result_message,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
let denied_output = merge_hook_feedback(&p.pre_hook_messages, p.deny_reason.clone().unwrap_or_default(), true);
|
let denied_output = merge_hook_feedback(
|
||||||
|
&p.pre_hook_messages,
|
||||||
|
p.deny_reason.clone().unwrap_or_default(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
let result_message = ConversationMessage::tool_result(
|
let result_message = ConversationMessage::tool_result(
|
||||||
p.tool_use_id.clone(),
|
p.tool_use_id.clone(),
|
||||||
p.tool_name.clone(),
|
p.tool_name.clone(),
|
||||||
denied_output,
|
denied_output,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
(p.tool_name.clone(), String::new(), String::new(), true, result_message)
|
(
|
||||||
|
p.tool_name.clone(),
|
||||||
|
String::new(),
|
||||||
|
String::new(),
|
||||||
|
true,
|
||||||
|
result_message,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
self.session
|
self.session
|
||||||
.push_message(result_message.clone())
|
.push_message(result_message.clone())
|
||||||
|
|
@ -642,7 +661,13 @@ where
|
||||||
} else {
|
} else {
|
||||||
Ok(progress_output.as_str())
|
Ok(progress_output.as_str())
|
||||||
};
|
};
|
||||||
reporter.on_tool_result(iterations, self.max_iterations, &progress_tool_name, &progress_input, report_result);
|
reporter.on_tool_result(
|
||||||
|
iterations,
|
||||||
|
self.max_iterations,
|
||||||
|
&progress_tool_name,
|
||||||
|
&progress_input,
|
||||||
|
report_result,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
tool_results.push(result_message);
|
tool_results.push(result_message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7173,16 +7173,6 @@ fn run_repl(
|
||||||
let _ = cli.set_model(Some(new_model));
|
let _ = cli.set_model(Some(new_model));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input::ReadOutcome::TeamToggle => {
|
|
||||||
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
|
|
||||||
if current == "1" {
|
|
||||||
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
|
|
||||||
eprintln!("[team] Agent teams disabled");
|
|
||||||
} else {
|
|
||||||
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
|
|
||||||
eprintln!("[team] Agent teams enabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -8641,7 +8631,9 @@ impl LiveCli {
|
||||||
if current == "1" {
|
if current == "1" {
|
||||||
eprintln!("[team] Agent teams: ENABLED");
|
eprintln!("[team] Agent teams: ENABLED");
|
||||||
} else {
|
} else {
|
||||||
eprintln!("[team] Agent teams: DISABLED (use /team on or Ctrl+T to enable)");
|
eprintln!(
|
||||||
|
"[team] Agent teams: DISABLED (use /team on or Ctrl+T to enable)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"" => {
|
"" => {
|
||||||
|
|
@ -8655,7 +8647,9 @@ impl LiveCli {
|
||||||
eprintln!("[team] Agent teams enabled (TeamCreate now available)");
|
eprintln!("[team] Agent teams enabled (TeamCreate now available)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
other => eprintln!("[team] unknown action: {other}. Use: /team [on|off|status]"),
|
other => {
|
||||||
|
eprintln!("[team] unknown action: {other}. Use: /team [on|off|status]")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -8664,7 +8658,10 @@ impl LiveCli {
|
||||||
// Reload the model from config after wizard saves
|
// Reload the model from config after wizard saves
|
||||||
let cwd = std::env::current_dir().unwrap_or_default();
|
let cwd = std::env::current_dir().unwrap_or_default();
|
||||||
let config = runtime::ConfigLoader::default_for(&cwd).load().ok();
|
let config = runtime::ConfigLoader::default_for(&cwd).load().ok();
|
||||||
if let Some(new_model) = config.as_ref().and_then(|c| c.provider().model().map(str::to_string)) {
|
if let Some(new_model) = config
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.provider().model().map(str::to_string))
|
||||||
|
{
|
||||||
self.set_model(Some(new_model))?;
|
self.set_model(Some(new_model))?;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
|
@ -14573,10 +14570,11 @@ impl ToolExecutor for CliToolExecutor {
|
||||||
"LSP",
|
"LSP",
|
||||||
"Agent",
|
"Agent",
|
||||||
"AgentMessage",
|
"AgentMessage",
|
||||||
"TeamStatus",
|
"TeamStatus",
|
||||||
"TaskClaim",
|
"TaskClaim",
|
||||||
"AgentSuggestion",
|
"AgentSuggestion",
|
||||||
"ContextRequest", "TaskGet",
|
"ContextRequest",
|
||||||
|
"TaskGet",
|
||||||
"TaskList",
|
"TaskList",
|
||||||
"TaskOutput",
|
"TaskOutput",
|
||||||
"GitStatus",
|
"GitStatus",
|
||||||
|
|
@ -14593,11 +14591,9 @@ impl ToolExecutor for CliToolExecutor {
|
||||||
|
|
||||||
// Classify calls as parallel-safe or sequential
|
// Classify calls as parallel-safe or sequential
|
||||||
for (i, call) in calls.iter().enumerate() {
|
for (i, call) in calls.iter().enumerate() {
|
||||||
if self
|
if self.allowed_tools.as_ref().is_some_and(|allowed| {
|
||||||
.allowed_tools
|
!allowed.contains(&canonical_allowed_tool_name(&call.tool_name))
|
||||||
.as_ref()
|
}) {
|
||||||
.is_some_and(|allowed| !allowed.contains(&canonical_allowed_tool_name(&call.tool_name)))
|
|
||||||
{
|
|
||||||
results[i] = Some(runtime::ToolResult {
|
results[i] = Some(runtime::ToolResult {
|
||||||
tool_use_id: call.tool_use_id.clone(),
|
tool_use_id: call.tool_use_id.clone(),
|
||||||
tool_name: call.tool_name.clone(),
|
tool_name: call.tool_name.clone(),
|
||||||
|
|
@ -14633,12 +14629,11 @@ impl ToolExecutor for CliToolExecutor {
|
||||||
let input = input.clone();
|
let input = input.clone();
|
||||||
let idx = *idx;
|
let idx = *idx;
|
||||||
handles.push(s.spawn(move || {
|
handles.push(s.spawn(move || {
|
||||||
let value = serde_json::from_str(&input)
|
let value = serde_json::from_str(&input).map_err(|error| {
|
||||||
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")));
|
ToolError::new(format!("invalid tool input JSON: {error}"))
|
||||||
|
});
|
||||||
let result = match value {
|
let result = match value {
|
||||||
Ok(v) => registry
|
Ok(v) => registry.execute(&tool_name, &v).map_err(ToolError::new),
|
||||||
.execute(&tool_name, &v)
|
|
||||||
.map_err(ToolError::new),
|
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
};
|
};
|
||||||
(idx, tool_use_id, tool_name, result)
|
(idx, tool_use_id, tool_name, result)
|
||||||
|
|
@ -14646,14 +14641,16 @@ impl ToolExecutor for CliToolExecutor {
|
||||||
}
|
}
|
||||||
handles
|
handles
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|h| h.join().unwrap_or_else(|_| {
|
.map(|h| {
|
||||||
(
|
h.join().unwrap_or_else(|_| {
|
||||||
0,
|
(
|
||||||
String::new(),
|
0,
|
||||||
String::new(),
|
String::new(),
|
||||||
Err(ToolError::new("parallel thread panicked")),
|
String::new(),
|
||||||
)
|
Err(ToolError::new("parallel thread panicked")),
|
||||||
}))
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ use aspect_macros::aspect;
|
||||||
use aspect_std::LoggingAspect;
|
use aspect_std::LoggingAspect;
|
||||||
|
|
||||||
use api::{
|
use api::{
|
||||||
max_tokens_for_model, model_family_identity_for, model_token_limit, ModelTokenLimit,
|
max_tokens_for_model, model_family_identity_for, model_token_limit, resolve_model_alias,
|
||||||
resolve_model_alias, ApiError, ContentBlockDelta, InputContentBlock, InputMessage,
|
ApiError, ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest, MessageResponse,
|
||||||
MessageRequest, MessageResponse, OutputContentBlock, ProviderClient,
|
OutputContentBlock, ProviderClient, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition,
|
||||||
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
|
ToolResultContentBlock,
|
||||||
};
|
};
|
||||||
use plugins::PluginTool;
|
use plugins::PluginTool;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
|
|
@ -1593,7 +1593,9 @@ fn execute_tool_with_enforcer(
|
||||||
"AgentMessage" => from_value::<AgentMessageInput>(input).and_then(run_agent_message),
|
"AgentMessage" => from_value::<AgentMessageInput>(input).and_then(run_agent_message),
|
||||||
"TeamStatus" => from_value::<TeamStatusInput>(input).and_then(run_team_status),
|
"TeamStatus" => from_value::<TeamStatusInput>(input).and_then(run_team_status),
|
||||||
"TaskClaim" => from_value::<TaskClaimInput>(input).and_then(run_task_claim),
|
"TaskClaim" => from_value::<TaskClaimInput>(input).and_then(run_task_claim),
|
||||||
"AgentSuggestion" => from_value::<AgentSuggestionInput>(input).and_then(run_agent_suggestion),
|
"AgentSuggestion" => {
|
||||||
|
from_value::<AgentSuggestionInput>(input).and_then(run_agent_suggestion)
|
||||||
|
}
|
||||||
"ContextRequest" => from_value::<ContextRequestInput>(input).and_then(run_context_request),
|
"ContextRequest" => from_value::<ContextRequestInput>(input).and_then(run_context_request),
|
||||||
"CronCreate" => from_value::<CronCreateInput>(input).and_then(run_cron_create),
|
"CronCreate" => from_value::<CronCreateInput>(input).and_then(run_cron_create),
|
||||||
"CronDelete" => from_value::<CronDeleteInput>(input).and_then(run_cron_delete),
|
"CronDelete" => from_value::<CronDeleteInput>(input).and_then(run_cron_delete),
|
||||||
|
|
@ -1888,10 +1890,13 @@ fn run_team_create(input: TeamCreateInput) -> Result<String, String> {
|
||||||
return Err("Agent teams is disabled. Use /team on or Ctrl+T to enable.".to_string());
|
return Err("Agent teams is disabled. Use /team on or Ctrl+T to enable.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let team_id = format!("team-{}", std::time::SystemTime::now()
|
let team_id = format!(
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
"team-{}",
|
||||||
.unwrap_or_default()
|
std::time::SystemTime::now()
|
||||||
.as_nanos());
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_nanos()
|
||||||
|
);
|
||||||
let output_dir = agent_store_dir()?;
|
let output_dir = agent_store_dir()?;
|
||||||
let team_dir = output_dir.join("teams");
|
let team_dir = output_dir.join("teams");
|
||||||
std::fs::create_dir_all(&team_dir).map_err(|e| e.to_string())?;
|
std::fs::create_dir_all(&team_dir).map_err(|e| e.to_string())?;
|
||||||
|
|
@ -1899,9 +1904,23 @@ fn run_team_create(input: TeamCreateInput) -> Result<String, String> {
|
||||||
// Expand mode preset into tasks, or use manual tasks.
|
// Expand mode preset into tasks, or use manual tasks.
|
||||||
// Default to "2x" when neither mode nor tasks are provided.
|
// Default to "2x" when neither mode nor tasks are provided.
|
||||||
let tasks = if let Some(mode) = &input.mode {
|
let tasks = if let Some(mode) = &input.mode {
|
||||||
expand_team_mode(mode, input.prompt.as_deref().unwrap_or("Explore the codebase and report findings"), &team_id)?
|
expand_team_mode(
|
||||||
|
mode,
|
||||||
|
input
|
||||||
|
.prompt
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("Explore the codebase and report findings"),
|
||||||
|
&team_id,
|
||||||
|
)?
|
||||||
} else if input.tasks.is_empty() {
|
} else if input.tasks.is_empty() {
|
||||||
expand_team_mode("2x", input.prompt.as_deref().unwrap_or("Explore the codebase and report findings"), &team_id)?
|
expand_team_mode(
|
||||||
|
"2x",
|
||||||
|
input
|
||||||
|
.prompt
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("Explore the codebase and report findings"),
|
||||||
|
&team_id,
|
||||||
|
)?
|
||||||
} else {
|
} else {
|
||||||
input.tasks.clone()
|
input.tasks.clone()
|
||||||
};
|
};
|
||||||
|
|
@ -1911,7 +1930,10 @@ fn run_team_create(input: TeamCreateInput) -> Result<String, String> {
|
||||||
|
|
||||||
for (i, task) in tasks.iter().enumerate() {
|
for (i, task) in tasks.iter().enumerate() {
|
||||||
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
|
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
let description = task.get("description").and_then(|v| v.as_str()).unwrap_or(&input.name);
|
let description = task
|
||||||
|
.get("description")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or(&input.name);
|
||||||
let subagent_type = task.get("subagent_type").and_then(|v| v.as_str());
|
let subagent_type = task.get("subagent_type").and_then(|v| v.as_str());
|
||||||
let model_override = task.get("model").and_then(|v| v.as_str());
|
let model_override = task.get("model").and_then(|v| v.as_str());
|
||||||
|
|
||||||
|
|
@ -1919,12 +1941,19 @@ fn run_team_create(input: TeamCreateInput) -> Result<String, String> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let task_id = task.get("task_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
let task_id = task
|
||||||
|
.get("task_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
let agent_input = AgentInput {
|
let agent_input = AgentInput {
|
||||||
description: description.to_string(),
|
description: description.to_string(),
|
||||||
prompt: prompt.to_string(),
|
prompt: prompt.to_string(),
|
||||||
subagent_type: subagent_type.map(|s| s.to_string()),
|
subagent_type: subagent_type.map(|s| s.to_string()),
|
||||||
name: Some(format!("{}-agent-{}", slugify_agent_name(&input.name), i + 1)),
|
name: Some(format!(
|
||||||
|
"{}-agent-{}",
|
||||||
|
slugify_agent_name(&input.name),
|
||||||
|
i + 1
|
||||||
|
)),
|
||||||
model: model_override.map(|s| s.to_string()),
|
model: model_override.map(|s| s.to_string()),
|
||||||
team_id: Some(team_id.clone()),
|
team_id: Some(team_id.clone()),
|
||||||
task_id,
|
task_id,
|
||||||
|
|
@ -1961,8 +1990,11 @@ fn run_team_create(input: TeamCreateInput) -> Result<String, String> {
|
||||||
"created_at": iso8601_now(),
|
"created_at": iso8601_now(),
|
||||||
});
|
});
|
||||||
let manifest_path = team_dir.join(format!("{team_id}.json"));
|
let manifest_path = team_dir.join(format!("{team_id}.json"));
|
||||||
std::fs::write(&manifest_path, serde_json::to_string_pretty(&team_manifest).map_err(|e| e.to_string())?)
|
std::fs::write(
|
||||||
.map_err(|e| e.to_string())?;
|
&manifest_path,
|
||||||
|
serde_json::to_string_pretty(&team_manifest).map_err(|e| e.to_string())?,
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Register in global registry
|
// Register in global registry
|
||||||
let team = global_team_registry().create(&input.name, agent_ids.clone());
|
let team = global_team_registry().create(&input.name, agent_ids.clone());
|
||||||
|
|
@ -1989,10 +2021,11 @@ fn run_team_status(input: TeamStatusInput) -> Result<String, String> {
|
||||||
if !manifest_path.exists() {
|
if !manifest_path.exists() {
|
||||||
return Err(format!("team {} not found", input.team_id));
|
return Err(format!("team {} not found", input.team_id));
|
||||||
}
|
}
|
||||||
let team_data: Value = serde_json::from_str(
|
let team_data: Value =
|
||||||
&std::fs::read_to_string(&manifest_path).map_err(|e| e.to_string())?
|
serde_json::from_str(&std::fs::read_to_string(&manifest_path).map_err(|e| e.to_string())?)
|
||||||
).map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let agent_ids = team_data.get("agent_ids")
|
let agent_ids = team_data
|
||||||
|
.get("agent_ids")
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
@ -2012,9 +2045,11 @@ fn run_team_status(input: TeamStatusInput) -> Result<String, String> {
|
||||||
if agent_json.exists() {
|
if agent_json.exists() {
|
||||||
if let Ok(data) = std::fs::read_to_string(&agent_json) {
|
if let Ok(data) = std::fs::read_to_string(&agent_json) {
|
||||||
if let Ok(parsed) = serde_json::from_str::<Value>(&data) {
|
if let Ok(parsed) = serde_json::from_str::<Value>(&data) {
|
||||||
detail["status"] = parsed.get("status").cloned().unwrap_or(json!("unknown"));
|
detail["status"] =
|
||||||
|
parsed.get("status").cloned().unwrap_or(json!("unknown"));
|
||||||
detail["name"] = parsed.get("name").cloned().unwrap_or(json!(null));
|
detail["name"] = parsed.get("name").cloned().unwrap_or(json!(null));
|
||||||
detail["subagent_type"] = parsed.get("subagent_type").cloned().unwrap_or(json!(null));
|
detail["subagent_type"] =
|
||||||
|
parsed.get("subagent_type").cloned().unwrap_or(json!(null));
|
||||||
if let Some(completed) = parsed.get("completed_at") {
|
if let Some(completed) = parsed.get("completed_at") {
|
||||||
detail["completed_at"] = completed.clone();
|
detail["completed_at"] = completed.clone();
|
||||||
}
|
}
|
||||||
|
|
@ -2026,7 +2061,8 @@ fn run_team_status(input: TeamStatusInput) -> Result<String, String> {
|
||||||
|
|
||||||
if agent_md.exists() {
|
if agent_md.exists() {
|
||||||
if let Ok(md_content) = std::fs::read_to_string(&agent_md) {
|
if let Ok(md_content) = std::fs::read_to_string(&agent_md) {
|
||||||
let summary = md_content.lines()
|
let summary = md_content
|
||||||
|
.lines()
|
||||||
.skip_while(|line| !line.starts_with("## Result"))
|
.skip_while(|line| !line.starts_with("## Result"))
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.take(10)
|
.take(10)
|
||||||
|
|
@ -2038,7 +2074,11 @@ fn run_team_status(input: TeamStatusInput) -> Result<String, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match detail.get("status").and_then(|v| v.as_str()).unwrap_or("unknown") {
|
match detail
|
||||||
|
.get("status")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
{
|
||||||
"completed" => completed_count += 1,
|
"completed" => completed_count += 1,
|
||||||
"failed" => failed_count += 1,
|
"failed" => failed_count += 1,
|
||||||
_ => running_count += 1,
|
_ => running_count += 1,
|
||||||
|
|
@ -2287,14 +2327,33 @@ fn get_agent_result_preview(store_dir: &std::path::Path, agent_id: &str) -> Stri
|
||||||
let md_path = store_dir.join(format!("{agent_id}.md"));
|
let md_path = store_dir.join(format!("{agent_id}.md"));
|
||||||
if let Ok(content) = std::fs::read_to_string(&md_path) {
|
if let Ok(content) = std::fs::read_to_string(&md_path) {
|
||||||
let lines: Vec<&str> = content.lines().collect();
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
let start = lines.iter().position(|l| l.starts_with("## Result")).map(|i| i + 1).unwrap_or(0);
|
let start = lines
|
||||||
lines[start..].iter().take(5).cloned().collect::<Vec<&str>>().join(" ").chars().take(200).collect()
|
.iter()
|
||||||
|
.position(|l| l.starts_with("## Result"))
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.unwrap_or(0);
|
||||||
|
lines[start..]
|
||||||
|
.iter()
|
||||||
|
.take(5)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(" ")
|
||||||
|
.chars()
|
||||||
|
.take(200)
|
||||||
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_team_event(events_path: &std::path::Path, team_id: &str, agent_id: &str, event_type: &str, name: &str, detail: Option<&str>) {
|
fn append_team_event(
|
||||||
|
events_path: &std::path::Path,
|
||||||
|
team_id: &str,
|
||||||
|
agent_id: &str,
|
||||||
|
event_type: &str,
|
||||||
|
name: &str,
|
||||||
|
detail: Option<&str>,
|
||||||
|
) {
|
||||||
let entry = json!({
|
let entry = json!({
|
||||||
"timestamp": std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis(),
|
"timestamp": std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis(),
|
||||||
"team_id": team_id,
|
"team_id": team_id,
|
||||||
|
|
@ -2305,13 +2364,16 @@ fn append_team_event(events_path: &std::path::Path, team_id: &str, agent_id: &st
|
||||||
});
|
});
|
||||||
if let Ok(line) = serde_json::to_string(&entry) {
|
if let Ok(line) = serde_json::to_string(&entry) {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(events_path) {
|
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(events_path)
|
||||||
|
{
|
||||||
let _ = writeln!(file, "{line}");
|
let _ = writeln!(file, "{line}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn run_team_delete(input: TeamDeleteInput) -> Result<String, String> {
|
fn run_team_delete(input: TeamDeleteInput) -> Result<String, String> {
|
||||||
// Delete from disk-based team storage
|
// Delete from disk-based team storage
|
||||||
let team_dir = agent_store_dir()?.join("teams");
|
let team_dir = agent_store_dir()?.join("teams");
|
||||||
|
|
@ -2381,8 +2443,11 @@ fn run_agent_message(input: AgentMessageInput) -> Result<String, String> {
|
||||||
"message": msg,
|
"message": msg,
|
||||||
"timestamp": ts,
|
"timestamp": ts,
|
||||||
});
|
});
|
||||||
std::fs::write(&msg_file, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?)
|
std::fs::write(
|
||||||
.map_err(|e| e.to_string())?;
|
&msg_file,
|
||||||
|
serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?,
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
to_pretty_json(json!({
|
to_pretty_json(json!({
|
||||||
"action": "sent",
|
"action": "sent",
|
||||||
"to": target,
|
"to": target,
|
||||||
|
|
@ -2404,9 +2469,7 @@ fn run_agent_message(input: AgentMessageInput) -> Result<String, String> {
|
||||||
let entries: Vec<_> = std::fs::read_dir(&inbox_dir)
|
let entries: Vec<_> = std::fs::read_dir(&inbox_dir)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
.filter(|e| {
|
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
|
||||||
e.path().extension().is_some_and(|ext| ext == "json")
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
for entry in &entries {
|
for entry in &entries {
|
||||||
if let Ok(content) = std::fs::read_to_string(entry.path()) {
|
if let Ok(content) = std::fs::read_to_string(entry.path()) {
|
||||||
|
|
@ -2443,9 +2506,11 @@ fn run_agent_message(input: AgentMessageInput) -> Result<String, String> {
|
||||||
return Err(format!("team {team_id} not found"));
|
return Err(format!("team {team_id} not found"));
|
||||||
}
|
}
|
||||||
let team_data: Value = serde_json::from_str(
|
let team_data: Value = serde_json::from_str(
|
||||||
&std::fs::read_to_string(&manifest_path).map_err(|e| e.to_string())?
|
&std::fs::read_to_string(&manifest_path).map_err(|e| e.to_string())?,
|
||||||
).map_err(|e| e.to_string())?;
|
)
|
||||||
let agent_ids = team_data.get("agent_ids")
|
.map_err(|e| e.to_string())?;
|
||||||
|
let agent_ids = team_data
|
||||||
|
.get("agent_ids")
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
@ -2457,7 +2522,9 @@ fn run_agent_message(input: AgentMessageInput) -> Result<String, String> {
|
||||||
let mut sent_to: Vec<String> = Vec::new();
|
let mut sent_to: Vec<String> = Vec::new();
|
||||||
for id_val in agent_ids {
|
for id_val in agent_ids {
|
||||||
if let Some(id) = id_val.as_str() {
|
if let Some(id) = id_val.as_str() {
|
||||||
if id == sender { continue; }
|
if id == sender {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let inbox_dir = mailbox_dir.join(id);
|
let inbox_dir = mailbox_dir.join(id);
|
||||||
std::fs::create_dir_all(&inbox_dir).map_err(|e| e.to_string())?;
|
std::fs::create_dir_all(&inbox_dir).map_err(|e| e.to_string())?;
|
||||||
let msg_file = inbox_dir.join(format!("msg-{ts}-{sender}.json"));
|
let msg_file = inbox_dir.join(format!("msg-{ts}-{sender}.json"));
|
||||||
|
|
@ -2467,8 +2534,11 @@ fn run_agent_message(input: AgentMessageInput) -> Result<String, String> {
|
||||||
"timestamp": ts,
|
"timestamp": ts,
|
||||||
"team_id": team_id,
|
"team_id": team_id,
|
||||||
});
|
});
|
||||||
std::fs::write(&msg_file, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?)
|
std::fs::write(
|
||||||
.map_err(|e| e.to_string())?;
|
&msg_file,
|
||||||
|
serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?,
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
sent_to.push(id.to_string());
|
sent_to.push(id.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2535,7 +2605,9 @@ fn run_task_claim(input: TaskClaimInput) -> Result<String, String> {
|
||||||
"count": claims.len(),
|
"count": claims.len(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
other => Err(format!("unknown TaskClaim action: {other}. Use claim, release, or list")),
|
other => Err(format!(
|
||||||
|
"unknown TaskClaim action: {other}. Use claim, release, or list"
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2557,8 +2629,11 @@ fn run_agent_suggestion(input: AgentSuggestionInput) -> Result<String, String> {
|
||||||
});
|
});
|
||||||
let filename = format!("suggestion-{agent_id}-{ts}.json");
|
let filename = format!("suggestion-{agent_id}-{ts}.json");
|
||||||
let path = suggestions_dir.join(&filename);
|
let path = suggestions_dir.join(&filename);
|
||||||
std::fs::write(&path, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?)
|
std::fs::write(
|
||||||
.map_err(|e| e.to_string())?;
|
&path,
|
||||||
|
serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?,
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
to_pretty_json(json!({
|
to_pretty_json(json!({
|
||||||
"action": "suggestion",
|
"action": "suggestion",
|
||||||
"file": filename,
|
"file": filename,
|
||||||
|
|
@ -2579,7 +2654,11 @@ fn run_context_request(input: ContextRequestInput) -> Result<String, String> {
|
||||||
match std::fs::read_to_string(&path) {
|
match std::fs::read_to_string(&path) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
let truncated = if content.len() > 10000 {
|
let truncated = if content.len() > 10000 {
|
||||||
format!("{}... (truncated, {} bytes total)", &content[..10000], content.len())
|
format!(
|
||||||
|
"{}... (truncated, {} bytes total)",
|
||||||
|
&content[..10000],
|
||||||
|
content.len()
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
content
|
content
|
||||||
};
|
};
|
||||||
|
|
@ -2610,7 +2689,15 @@ fn run_context_request(input: ContextRequestInput) -> Result<String, String> {
|
||||||
for symbol in &input.symbols {
|
for symbol in &input.symbols {
|
||||||
let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
|
let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
|
||||||
let output = std::process::Command::new("grep")
|
let output = std::process::Command::new("grep")
|
||||||
.args(["-rn", "--include=*.rs", "--include=*.ts", "--include=*.js", "--include=*.py", symbol, "."])
|
.args([
|
||||||
|
"-rn",
|
||||||
|
"--include=*.rs",
|
||||||
|
"--include=*.ts",
|
||||||
|
"--include=*.js",
|
||||||
|
"--include=*.py",
|
||||||
|
symbol,
|
||||||
|
".",
|
||||||
|
])
|
||||||
.current_dir(&cwd)
|
.current_dir(&cwd)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
@ -2646,7 +2733,11 @@ fn expand_team_mode(mode: &str, base_prompt: &str, team_id: &str) -> Result<Vec<
|
||||||
"4x" | "large" => 4,
|
"4x" | "large" => 4,
|
||||||
"5x" | "xlarge" => 5,
|
"5x" | "xlarge" => 5,
|
||||||
"6x" | "mega" => 6,
|
"6x" | "mega" => 6,
|
||||||
other => return Err(format!("unknown team mode '{other}'. Use 1x-6x or tiny/small/medium/large/xlarge/mega")),
|
other => {
|
||||||
|
return Err(format!(
|
||||||
|
"unknown team mode '{other}'. Use 1x-6x or tiny/small/medium/large/xlarge/mega"
|
||||||
|
))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let short_team_id = &team_id[team_id.len().saturating_sub(8)..];
|
let short_team_id = &team_id[team_id.len().saturating_sub(8)..];
|
||||||
let roles: &[&str] = &["Explore", "Plan", "Verification"];
|
let roles: &[&str] = &["Explore", "Plan", "Verification"];
|
||||||
|
|
@ -2721,8 +2812,11 @@ fn claim_task(task_id: &str, agent_id: &str, team_id: &str) -> Result<bool, Stri
|
||||||
});
|
});
|
||||||
// Atomic claim: write to temp file then rename
|
// Atomic claim: write to temp file then rename
|
||||||
let tmp_path = dir.join(format!("{task_id}.lock.tmp.{agent_id}"));
|
let tmp_path = dir.join(format!("{task_id}.lock.tmp.{agent_id}"));
|
||||||
std::fs::write(&tmp_path, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?)
|
std::fs::write(
|
||||||
.map_err(|e| e.to_string())?;
|
&tmp_path,
|
||||||
|
serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?,
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
match std::fs::rename(&tmp_path, &lock_path) {
|
match std::fs::rename(&tmp_path, &lock_path) {
|
||||||
Ok(()) => Ok(true),
|
Ok(()) => Ok(true),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
@ -2755,7 +2849,8 @@ fn list_claims(team_id: Option<&str>) -> Vec<serde_json::Value> {
|
||||||
if path.extension().map_or(false, |e| e == "lock") {
|
if path.extension().map_or(false, |e| e == "lock") {
|
||||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&content) {
|
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||||
if team_id.map_or(true, |tid| v.get("team_id").map_or(false, |t| t == tid)) {
|
if team_id.map_or(true, |tid| v.get("team_id").map_or(false, |t| t == tid))
|
||||||
|
{
|
||||||
claims.push(v);
|
claims.push(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2778,7 +2873,12 @@ impl TeamInboxReporter {
|
||||||
fn new(team_id: String, agent_id: String, agent_name: String) -> Self {
|
fn new(team_id: String, agent_id: String, agent_name: String) -> Self {
|
||||||
let inbox_dir = agent_mailbox_dir().join("team").join(&team_id);
|
let inbox_dir = agent_mailbox_dir().join("team").join(&team_id);
|
||||||
let _ = std::fs::create_dir_all(&inbox_dir);
|
let _ = std::fs::create_dir_all(&inbox_dir);
|
||||||
Self { team_id, agent_id, agent_name, inbox_dir }
|
Self {
|
||||||
|
team_id,
|
||||||
|
agent_id,
|
||||||
|
agent_name,
|
||||||
|
inbox_dir,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2812,10 +2912,9 @@ impl TurnProgressReporter for TeamInboxReporter {
|
||||||
"max_iterations": max_iterations,
|
"max_iterations": max_iterations,
|
||||||
"timestamp": ts,
|
"timestamp": ts,
|
||||||
});
|
});
|
||||||
let msg_file = self.inbox_dir.join(format!(
|
let msg_file = self
|
||||||
"tp-{}-{}-{ts}.json",
|
.inbox_dir
|
||||||
self.agent_id, iteration
|
.join(format!("tp-{}-{}-{ts}.json", self.agent_id, iteration));
|
||||||
));
|
|
||||||
if let Ok(line) = serde_json::to_string(&entry) {
|
if let Ok(line) = serde_json::to_string(&entry) {
|
||||||
let _ = std::fs::write(&msg_file, line);
|
let _ = std::fs::write(&msg_file, line);
|
||||||
}
|
}
|
||||||
|
|
@ -2830,13 +2929,19 @@ impl TurnProgressReporter for TeamInboxReporter {
|
||||||
.output();
|
.output();
|
||||||
if diff_check.map_or(true, |o| !o.status.success()) {
|
if diff_check.map_or(true, |o| !o.status.success()) {
|
||||||
let _ = std::process::Command::new("git")
|
let _ = std::process::Command::new("git")
|
||||||
.args(["commit", "-m", &format!("agent {} progress: iteration {iteration}", self.agent_id)])
|
.args([
|
||||||
|
"commit",
|
||||||
|
"-m",
|
||||||
|
&format!("agent {} progress: iteration {iteration}", self.agent_id),
|
||||||
|
])
|
||||||
.output();
|
.output();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for kill signal from team lead
|
// Check for kill signal from team lead
|
||||||
for entry in std::fs::read_dir(&self.inbox_dir).unwrap_or_else(|_| std::fs::read_dir(".").unwrap()) {
|
for entry in
|
||||||
|
std::fs::read_dir(&self.inbox_dir).unwrap_or_else(|_| std::fs::read_dir(".").unwrap())
|
||||||
|
{
|
||||||
if let Ok(e) = entry {
|
if let Ok(e) = entry {
|
||||||
let name = e.file_name();
|
let name = e.file_name();
|
||||||
let name_str = name.to_string_lossy();
|
let name_str = name.to_string_lossy();
|
||||||
|
|
@ -5352,14 +5457,25 @@ fn setup_agent_worktree(agent_id: &str) -> Result<std::path::PathBuf, String> {
|
||||||
// Create worktree on a new branch
|
// Create worktree on a new branch
|
||||||
let branch_name = format!("agent/{agent_id}");
|
let branch_name = format!("agent/{agent_id}");
|
||||||
let output = std::process::Command::new("git")
|
let output = std::process::Command::new("git")
|
||||||
.args(["worktree", "add", worktree_dir.to_str().unwrap_or(""), "-b", &branch_name])
|
.args([
|
||||||
|
"worktree",
|
||||||
|
"add",
|
||||||
|
worktree_dir.to_str().unwrap_or(""),
|
||||||
|
"-b",
|
||||||
|
&branch_name,
|
||||||
|
])
|
||||||
.current_dir(&cwd)
|
.current_dir(&cwd)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
// Branch might already exist, try with existing branch
|
// Branch might already exist, try with existing branch
|
||||||
let output2 = std::process::Command::new("git")
|
let output2 = std::process::Command::new("git")
|
||||||
.args(["worktree", "add", worktree_dir.to_str().unwrap_or(""), &branch_name])
|
.args([
|
||||||
|
"worktree",
|
||||||
|
"add",
|
||||||
|
worktree_dir.to_str().unwrap_or(""),
|
||||||
|
&branch_name,
|
||||||
|
])
|
||||||
.current_dir(&cwd)
|
.current_dir(&cwd)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
@ -5390,7 +5506,12 @@ fn teardown_agent_worktree(agent_id: &str, worktree_path: &std::path::Path) -> R
|
||||||
}
|
}
|
||||||
// Remove the worktree
|
// Remove the worktree
|
||||||
let _ = std::process::Command::new("git")
|
let _ = std::process::Command::new("git")
|
||||||
.args(["worktree", "remove", worktree_path.to_str().unwrap_or(""), "--force"])
|
.args([
|
||||||
|
"worktree",
|
||||||
|
"remove",
|
||||||
|
worktree_path.to_str().unwrap_or(""),
|
||||||
|
"--force",
|
||||||
|
])
|
||||||
.current_dir(&cwd)
|
.current_dir(&cwd)
|
||||||
.output();
|
.output();
|
||||||
// Delete the branch
|
// Delete the branch
|
||||||
|
|
@ -5405,8 +5526,7 @@ fn run_agent_job(job: &AgentJob) -> Result<(), String> {
|
||||||
// Claim task if task_id is set (prevents duplicate work)
|
// Claim task if task_id is set (prevents duplicate work)
|
||||||
if let Some(ref task_id) = job.task_id {
|
if let Some(ref task_id) = job.task_id {
|
||||||
if let Some(ref team_id) = job.team_id {
|
if let Some(ref team_id) = job.team_id {
|
||||||
let claimed = claim_task(task_id, &job.manifest.agent_id, team_id)
|
let claimed = claim_task(task_id, &job.manifest.agent_id, team_id).unwrap_or(false);
|
||||||
.unwrap_or(false);
|
|
||||||
if !claimed {
|
if !claimed {
|
||||||
return Err(format!("task {task_id} already claimed by another agent"));
|
return Err(format!("task {task_id} already claimed by another agent"));
|
||||||
}
|
}
|
||||||
|
|
@ -5732,8 +5852,11 @@ fn post_agent_completion_to_team_inbox(
|
||||||
"timestamp": ts,
|
"timestamp": ts,
|
||||||
});
|
});
|
||||||
let msg_file = mailbox_dir.join(format!("{}-{ts}.json", manifest.agent_id));
|
let msg_file = mailbox_dir.join(format!("{}-{ts}.json", manifest.agent_id));
|
||||||
std::fs::write(&msg_file, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?)
|
std::fs::write(
|
||||||
.map_err(|e| e.to_string())
|
&msg_file,
|
||||||
|
serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?,
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_LANE_SUMMARY_WORDS: usize = 7;
|
const MIN_LANE_SUMMARY_WORDS: usize = 7;
|
||||||
|
|
@ -10038,8 +10161,8 @@ mod tests {
|
||||||
subagent_type: Some("Explore".to_string()),
|
subagent_type: Some("Explore".to_string()),
|
||||||
name: Some("ship-audit".to_string()),
|
name: Some("ship-audit".to_string()),
|
||||||
model: None,
|
model: None,
|
||||||
team_id: None,
|
team_id: None,
|
||||||
task_id: None,
|
task_id: None,
|
||||||
},
|
},
|
||||||
move |job| {
|
move |job| {
|
||||||
*captured_for_spawn
|
*captured_for_spawn
|
||||||
|
|
@ -10121,8 +10244,8 @@ mod tests {
|
||||||
subagent_type: Some("Explore".to_string()),
|
subagent_type: Some("Explore".to_string()),
|
||||||
name: Some("complete-task".to_string()),
|
name: Some("complete-task".to_string()),
|
||||||
model: Some("claude-sonnet-4-6".to_string()),
|
model: Some("claude-sonnet-4-6".to_string()),
|
||||||
team_id: None,
|
team_id: None,
|
||||||
task_id: None,
|
task_id: None,
|
||||||
},
|
},
|
||||||
|job| {
|
|job| {
|
||||||
persist_agent_terminal_state(
|
persist_agent_terminal_state(
|
||||||
|
|
@ -10180,8 +10303,8 @@ mod tests {
|
||||||
subagent_type: Some("Verification".to_string()),
|
subagent_type: Some("Verification".to_string()),
|
||||||
name: Some("fail-task".to_string()),
|
name: Some("fail-task".to_string()),
|
||||||
model: None,
|
model: None,
|
||||||
team_id: None,
|
team_id: None,
|
||||||
task_id: None,
|
task_id: None,
|
||||||
},
|
},
|
||||||
|job| {
|
|job| {
|
||||||
persist_agent_terminal_state(
|
persist_agent_terminal_state(
|
||||||
|
|
@ -10229,8 +10352,8 @@ mod tests {
|
||||||
subagent_type: Some("Explore".to_string()),
|
subagent_type: Some("Explore".to_string()),
|
||||||
name: Some("summary-floor".to_string()),
|
name: Some("summary-floor".to_string()),
|
||||||
model: None,
|
model: None,
|
||||||
team_id: None,
|
team_id: None,
|
||||||
task_id: None,
|
task_id: None,
|
||||||
},
|
},
|
||||||
|job| {
|
|job| {
|
||||||
persist_agent_terminal_state(
|
persist_agent_terminal_state(
|
||||||
|
|
@ -10326,8 +10449,8 @@ mod tests {
|
||||||
subagent_type: Some("Verification".to_string()),
|
subagent_type: Some("Verification".to_string()),
|
||||||
name: Some("review-lane".to_string()),
|
name: Some("review-lane".to_string()),
|
||||||
model: None,
|
model: None,
|
||||||
team_id: None,
|
team_id: None,
|
||||||
task_id: None,
|
task_id: None,
|
||||||
},
|
},
|
||||||
|job| {
|
|job| {
|
||||||
persist_agent_terminal_state(
|
persist_agent_terminal_state(
|
||||||
|
|
@ -10488,8 +10611,8 @@ mod tests {
|
||||||
subagent_type: Some("Explore".to_string()),
|
subagent_type: Some("Explore".to_string()),
|
||||||
name: Some("cron-closeout".to_string()),
|
name: Some("cron-closeout".to_string()),
|
||||||
model: None,
|
model: None,
|
||||||
team_id: None,
|
team_id: None,
|
||||||
task_id: None,
|
task_id: None,
|
||||||
},
|
},
|
||||||
|job| {
|
|job| {
|
||||||
persist_agent_terminal_state(
|
persist_agent_terminal_state(
|
||||||
|
|
@ -10531,8 +10654,8 @@ mod tests {
|
||||||
subagent_type: None,
|
subagent_type: None,
|
||||||
name: Some("spawn-error".to_string()),
|
name: Some("spawn-error".to_string()),
|
||||||
model: None,
|
model: None,
|
||||||
team_id: None,
|
team_id: None,
|
||||||
task_id: None,
|
task_id: None,
|
||||||
},
|
},
|
||||||
|_| Err(String::from("thread creation failed")),
|
|_| Err(String::from("thread creation failed")),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue