feat: agent teams with task claiming, context management, and team monitoring
- TeamInboxReporter: per-tool-call progress reporting to team inbox
- TaskClaim tool: atomic claim/release/list with .clawd-agents/claims/ lock files
- Team-scoped task_ids to prevent cross-team claim collisions
- AgentSuggestion tool: propose AGENTS.md additions (human review required)
- ContextRequest tool: iterative retrieval with 3-cycle budget for sub-agents
- Context-window-aware auto-compaction (70% threshold) prevents overflow
- Model token limits for qwen/glm/generic models with 131K fallback
- Reviewer subagent_type: read-only tools, no bash/write
- Team mode presets: 1x-6x (tiny/small/medium/large/xlarge/mega)
- /team slash command + Ctrl+T toggle (off by default, CLAWD_AGENT_TEAMS=1)
- TeamDelete: disk-based deletion with inbox/claims cleanup
- TeamStatus: kill stuck agents, list AGENTS.md suggestions
- AGENTS.md: auto-loaded shared learnings in sub-agent system prompt
- Periodic git commits every 5 tool calls via TeamInboxReporter
- Claims released on failure/panic in spawn_agent_job
- Fixed doubled .clawd-agents/.clawd-agents/ paths (set CLAWD_AGENT_STORE abs)
- Fixed "unknown error" in team watcher (added error field to inbox messages)
💘 Generated with Crush
Assisted-by: GLM 5.1 FP8 via Crush <crush@charm.land>
This commit is contained in:
parent
1786c5eb2e
commit
1b6ef83f78
|
|
@ -28,8 +28,9 @@ pub use providers::openai_compat::{
|
|||
};
|
||||
pub use providers::{
|
||||
detect_provider_kind, max_tokens_for_model, max_tokens_for_model_with_override,
|
||||
model_family_identity_for, model_family_identity_for_kind, provider_diagnostics_for_model,
|
||||
resolve_model_alias, ProviderDiagnostics, ProviderKind,
|
||||
model_family_identity_for, model_family_identity_for_kind, model_token_limit,
|
||||
ModelTokenLimit, provider_diagnostics_for_model, resolve_model_alias, ProviderDiagnostics,
|
||||
ProviderKind,
|
||||
};
|
||||
pub use sse::{parse_frame, SseParser};
|
||||
pub use types::{
|
||||
|
|
|
|||
|
|
@ -679,15 +679,21 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
|
|||
max_output_tokens: 16_384,
|
||||
context_window_tokens: 256_000,
|
||||
}),
|
||||
"qwen-max" => Some(ModelTokenLimit {
|
||||
// Qwen models via DashScope / OpenAI-compat
|
||||
"qwen3.6-35b-fast" | "qwen3-235b-a22b" | "qwen-max" | "qwen-plus" | "qwen-turbo" | "qwen-qwq" => Some(ModelTokenLimit {
|
||||
max_output_tokens: 16_384,
|
||||
context_window_tokens: 131_072,
|
||||
}),
|
||||
"glm-5.1-fast" => Some(ModelTokenLimit {
|
||||
max_output_tokens: 16_384,
|
||||
context_window_tokens: 200_000,
|
||||
}),
|
||||
// Generic fallback for any model: assume 128K context, 8K output.
|
||||
// This prevents the "unknown model → no limit check → context overflow" bug.
|
||||
_ => Some(ModelTokenLimit {
|
||||
max_output_tokens: 8_192,
|
||||
context_window_tokens: 131_072,
|
||||
}),
|
||||
"qwen-plus" => Some(ModelTokenLimit {
|
||||
max_output_tokens: 8_192,
|
||||
context_window_tokens: 131_072,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1195,10 +1195,14 @@ pub enum SlashCommand {
|
|||
History {
|
||||
count: Option<String>,
|
||||
},
|
||||
Unknown(String),
|
||||
Lsp {
|
||||
action: Option<String>,
|
||||
target: Option<String>,
|
||||
},
|
||||
Team {
|
||||
action: Option<String>,
|
||||
},
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -5403,6 +5407,7 @@ pub fn handle_slash_command(
|
|||
| SlashCommand::OutputStyle { .. }
|
||||
| SlashCommand::AddDir { .. }
|
||||
| SlashCommand::History { .. }
|
||||
| SlashCommand::Lsp { .. }
|
||||
| SlashCommand::Team { .. }
|
||||
| SlashCommand::Setup
|
||||
| SlashCommand::Unknown(_) => None,
|
||||
|
|
|
|||
|
|
@ -163,6 +163,10 @@ pub struct RuntimeFeatureConfig {
|
|||
api_timeout: ApiTimeoutConfig,
|
||||
rules_import: RulesImportConfig,
|
||||
provider: RuntimeProviderConfig,
|
||||
/// Model override used when spawning sub-agents via the Agent tool.
|
||||
/// Read from `subagentModel` (or `subagent_model`) in settings; falls
|
||||
/// back to the default model when unset.
|
||||
subagent_model: Option<String>,
|
||||
}
|
||||
|
||||
/// Controls which external AI coding framework rules are imported into the system prompt.
|
||||
|
|
@ -801,6 +805,7 @@ fn build_runtime_config(
|
|||
api_timeout: parse_optional_api_timeout_config(&merged_value)?,
|
||||
rules_import: parse_optional_rules_import(&merged_value)?,
|
||||
provider: parse_optional_provider_config(&merged_value)?,
|
||||
subagent_model: parse_optional_subagent_model(&merged_value),
|
||||
};
|
||||
|
||||
Ok(RuntimeConfig {
|
||||
|
|
@ -880,6 +885,13 @@ impl RuntimeConfig {
|
|||
self.feature_config.model.as_deref()
|
||||
}
|
||||
|
||||
/// Model override used when spawning sub-agents via the Agent tool.
|
||||
/// Read from `subagentModel` in settings; `None` means "use default model".
|
||||
#[must_use]
|
||||
pub fn subagent_model(&self) -> Option<&str> {
|
||||
self.feature_config.subagent_model.as_deref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn aliases(&self) -> &BTreeMap<String, String> {
|
||||
&self.feature_config.aliases
|
||||
|
|
@ -1717,6 +1729,17 @@ fn parse_optional_model(root: &JsonValue) -> Option<String> {
|
|||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Reads `subagentModel` (or the snake_case `subagent_model` alias) from
|
||||
/// merged settings. Returns `None` when absent or blank so the Agent tool
|
||||
/// falls back to the default model.
|
||||
fn parse_optional_subagent_model(root: &JsonValue) -> Option<String> {
|
||||
root.as_object()
|
||||
.and_then(|object| object.get("subagentModel").or_else(|| object.get("subagent_model")))
|
||||
.and_then(JsonValue::as_str)
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
fn parse_optional_aliases(root: &JsonValue) -> Result<BTreeMap<String, String>, ConfigError> {
|
||||
let Some(object) = root.as_object() else {
|
||||
return Ok(BTreeMap::new());
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@ pub use config_validate::{
|
|||
};
|
||||
pub use conversation::{
|
||||
auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent,
|
||||
ConversationRuntime, PromptCacheEvent, RuntimeError, StaticToolExecutor, ToolError,
|
||||
ToolExecutor, TurnSummary,
|
||||
ConversationRuntime, PromptCacheEvent, RuntimeError, StaticToolExecutor, ToolCall, ToolError,
|
||||
ToolExecutor, ToolResult, TurnProgressReporter, TurnSummary,
|
||||
};
|
||||
pub use file_ops::{
|
||||
edit_file, edit_file_in_workspace, glob_search, glob_search_in_workspace, grep_search,
|
||||
|
|
|
|||
|
|
@ -6962,6 +6962,7 @@ fn run_resume_command(
|
|||
| SlashCommand::Tag { .. }
|
||||
| SlashCommand::OutputStyle { .. }
|
||||
| SlashCommand::AddDir { .. }
|
||||
| SlashCommand::Lsp { .. }
|
||||
| SlashCommand::Team { .. }
|
||||
| SlashCommand::Setup => Err("unsupported resumed slash command".into()),
|
||||
}
|
||||
|
|
@ -7145,6 +7146,17 @@ fn run_repl(
|
|||
cli.record_prompt_history(&trimmed);
|
||||
cli.run_turn(&trimmed)?;
|
||||
}
|
||||
input::ReadOutcome::TeamToggle => {
|
||||
// Ctrl+T toggles agent teams mode
|
||||
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 (TeamCreate now available)");
|
||||
}
|
||||
}
|
||||
input::ReadOutcome::Cancel => {}
|
||||
input::ReadOutcome::Exit => {
|
||||
cli.persist_session()?;
|
||||
|
|
@ -8614,6 +8626,49 @@ impl LiveCli {
|
|||
run_init(CliOutputFormat::Text)?;
|
||||
false
|
||||
}
|
||||
SlashCommand::Team { action } => {
|
||||
match action.as_deref().unwrap_or("") {
|
||||
"on" | "enable" => {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
|
||||
eprintln!("[team] Agent teams enabled (TeamCreate now available)");
|
||||
}
|
||||
"off" | "disable" => {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
|
||||
eprintln!("[team] Agent teams disabled");
|
||||
}
|
||||
"status" => {
|
||||
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
|
||||
if current == "1" {
|
||||
eprintln!("[team] Agent teams: ENABLED");
|
||||
} else {
|
||||
eprintln!("[team] Agent teams: DISABLED (use /team on or Ctrl+T to enable)");
|
||||
}
|
||||
}
|
||||
"" => {
|
||||
// Toggle
|
||||
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 (TeamCreate now available)");
|
||||
}
|
||||
}
|
||||
other => eprintln!("[team] unknown action: {other}. Use: /team [on|off|status]"),
|
||||
}
|
||||
false
|
||||
}
|
||||
SlashCommand::Setup => {
|
||||
setup_wizard::run_setup_wizard()?;
|
||||
// Reload the model from config after wizard saves
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
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)) {
|
||||
self.set_model(Some(new_model))?;
|
||||
}
|
||||
false
|
||||
}
|
||||
SlashCommand::Diff => {
|
||||
Self::print_diff()?;
|
||||
false
|
||||
|
|
@ -8662,12 +8717,6 @@ impl LiveCli {
|
|||
);
|
||||
false
|
||||
}
|
||||
SlashCommand::Setup => {
|
||||
if let Err(e) = setup_wizard::run_setup_wizard() {
|
||||
eprintln!("Setup wizard failed: {e}");
|
||||
}
|
||||
false
|
||||
}
|
||||
SlashCommand::History { count } => {
|
||||
self.print_prompt_history(count.as_deref());
|
||||
false
|
||||
|
|
@ -8714,15 +8763,12 @@ impl LiveCli {
|
|||
| SlashCommand::Ide { .. }
|
||||
| SlashCommand::Tag { .. }
|
||||
| SlashCommand::OutputStyle { .. }
|
||||
| SlashCommand::AddDir { .. } => {
|
||||
| SlashCommand::AddDir { .. }
|
||||
| SlashCommand::Lsp { .. } => {
|
||||
let cmd_name = command.slash_name();
|
||||
eprintln!("{cmd_name} is not yet implemented in this build.");
|
||||
false
|
||||
}
|
||||
SlashCommand::Team { action } => {
|
||||
self.print_team_command(action.as_deref());
|
||||
false
|
||||
}
|
||||
SlashCommand::Unknown(name) => {
|
||||
eprintln!("{}", format_unknown_slash_command(&name));
|
||||
false
|
||||
|
|
@ -8735,48 +8781,6 @@ impl LiveCli {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// `/team` — user-facing manager for agent teams.
|
||||
///
|
||||
/// The team registry and the `TeamCreate` / `TeamDelete` / `Cron*` tools live
|
||||
/// inside the tool executor, so the model invokes them directly during a
|
||||
/// turn. This slash command surfaces a readable view of the same teams and
|
||||
/// is the user's way to take action without driving the model.
|
||||
fn print_team_command(&self, action: Option<&str>) {
|
||||
match action.map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("help") => {
|
||||
println!("Agent teams — the model creates and manages teams via its tools.");
|
||||
println!();
|
||||
println!("Available team tools the model can call:");
|
||||
println!(" TeamCreate create a team from a set of tasks");
|
||||
println!(" TeamDelete mark a team deleted by team_id");
|
||||
println!(" CronCreate schedule a recurring prompt (cron)");
|
||||
println!(" CronDelete remove a scheduled cron by cron_id");
|
||||
println!(" CronList list scheduled crons");
|
||||
println!();
|
||||
println!("Slash commands:");
|
||||
println!(" /team list show teams in this session registry");
|
||||
println!(" /team help this message");
|
||||
println!();
|
||||
println!("Teams are held in-process for the lifetime of this claw session.");
|
||||
}
|
||||
Some("list") => {
|
||||
// The team registry is owned by the in-process tool executor
|
||||
// (tools::global_team_registry) and is not currently exposed to
|
||||
// the CLI layer, so we can't enumerate live teams from here.
|
||||
// Listing will arrive with tool-registry introspection.
|
||||
println!("Live team listing isn't wired into the CLI yet.");
|
||||
println!(
|
||||
"Teams are created/managed by the model via the TeamCreate and TeamDelete"
|
||||
);
|
||||
println!("tools during a turn. Ask the model to report on its teams.");
|
||||
}
|
||||
Some(other) => {
|
||||
println!("Unknown /team action: {other}");
|
||||
println!("Available: list, help");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_status(&self) {
|
||||
let cumulative = self.runtime.usage().cumulative_usage();
|
||||
let latest = self.runtime.usage().current_turn_usage();
|
||||
|
|
@ -14569,8 +14573,10 @@ impl ToolExecutor for CliToolExecutor {
|
|||
"LSP",
|
||||
"Agent",
|
||||
"AgentMessage",
|
||||
"TeamStatus",
|
||||
"TaskGet",
|
||||
"TeamStatus",
|
||||
"TaskClaim",
|
||||
"AgentSuggestion",
|
||||
"ContextRequest", "TaskGet",
|
||||
"TaskList",
|
||||
"TaskOutput",
|
||||
"GitStatus",
|
||||
|
|
|
|||
|
|
@ -116,8 +116,9 @@ mod tests {
|
|||
lane_events: vec![],
|
||||
derived_state: "working".to_string(),
|
||||
current_blocker: None,
|
||||
error: None,
|
||||
team_id: None,
|
||||
task_id: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue