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
c3d3635924
commit
173588bd6a
|
|
@ -34,8 +34,12 @@ pub use providers::openai_compat::{
|
|||
};
|
||||
pub use providers::{
|
||||
detect_provider_kind, max_tokens_for_model, max_tokens_for_model_with_override,
|
||||
<<<<<<< HEAD
|
||||
model_family_identity_for, model_family_identity_for_kind, provider_diagnostics_for_model,
|
||||
resolve_model_alias, ProviderDiagnostics, ProviderKind,
|
||||
=======
|
||||
model_token_limit, ModelTokenLimit, resolve_model_alias, ProviderKind,
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
};
|
||||
pub use sse::{parse_frame, SseParser};
|
||||
pub use types::{
|
||||
|
|
|
|||
|
|
@ -722,6 +722,7 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
|
|||
max_output_tokens: 16_384,
|
||||
context_window_tokens: 256_000,
|
||||
}),
|
||||
<<<<<<< HEAD
|
||||
"qwen-max" => Some(ModelTokenLimit {
|
||||
max_output_tokens: 8_192,
|
||||
context_window_tokens: 131_072,
|
||||
|
|
@ -731,6 +732,23 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
|
|||
context_window_tokens: 131_072,
|
||||
}),
|
||||
_ => None,
|
||||
=======
|
||||
// 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,
|
||||
}),
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1199,6 +1199,9 @@ pub enum SlashCommand {
|
|||
action: Option<String>,
|
||||
target: Option<String>,
|
||||
},
|
||||
Team {
|
||||
action: Option<String>,
|
||||
},
|
||||
Unknown(String),
|
||||
Team {
|
||||
action: Option<String>,
|
||||
|
|
@ -1289,6 +1292,11 @@ impl SlashCommand {
|
|||
Self::Theme { .. } => "/theme",
|
||||
Self::Voice { .. } => "/voice",
|
||||
Self::Usage { .. } => "/usage",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
Self::Team { .. } => "/team",
|
||||
Self::Setup => "/setup",
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
Self::Rename { .. } => "/rename",
|
||||
Self::Copy { .. } => "/copy",
|
||||
Self::Hooks { .. } => "/hooks",
|
||||
|
|
@ -1513,6 +1521,11 @@ pub fn validate_slash_command_input(
|
|||
"theme" => SlashCommand::Theme { name: remainder },
|
||||
"voice" => SlashCommand::Voice { mode: remainder },
|
||||
"usage" => SlashCommand::Usage { scope: remainder },
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"team" => SlashCommand::Team { action: remainder },
|
||||
"setup" => SlashCommand::Setup,
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
"rename" => SlashCommand::Rename { name: remainder },
|
||||
"copy" => SlashCommand::Copy { target: remainder },
|
||||
"hooks" => SlashCommand::Hooks { args: remainder },
|
||||
|
|
@ -5421,6 +5434,7 @@ pub fn handle_slash_command(
|
|||
| SlashCommand::Team { .. }
|
||||
=======
|
||||
| SlashCommand::Lsp { .. }
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
| SlashCommand::Setup
|
||||
=======
|
||||
|
|
@ -5432,6 +5446,10 @@ pub fn handle_slash_command(
|
|||
=======
|
||||
>>>>>>> 0b227b62 (fix: resolve cherry-pick conflicts and remove non-LSP artifacts)
|
||||
| SlashCommand::Unknown(_) => None,
|
||||
=======
|
||||
| SlashCommand::Team { .. }
|
||||
| SlashCommand::Setup | SlashCommand::Unknown(_) => None,
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,22 @@ impl Display for ToolError {
|
|||
|
||||
impl std::error::Error for ToolError {}
|
||||
|
||||
/// Callback trait for reporting tool execution progress during a turn.
|
||||
/// Implementations can post progress to a team inbox, log to stderr, etc.
|
||||
/// Called after each tool call completes (success or failure).
|
||||
pub trait TurnProgressReporter: Send + Sync {
|
||||
/// Called after a tool execution completes.
|
||||
/// `iteration` is 1-based index of the tool call within this turn.
|
||||
fn on_tool_result(
|
||||
&self,
|
||||
iteration: usize,
|
||||
max_iterations: usize,
|
||||
tool_name: &str,
|
||||
input: &str,
|
||||
result: Result<&str, &str>,
|
||||
);
|
||||
}
|
||||
|
||||
/// Error returned when a conversation turn cannot be completed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RuntimeError {
|
||||
|
|
@ -174,6 +190,7 @@ pub struct ConversationRuntime<C, T> {
|
|||
hook_abort_signal: HookAbortSignal,
|
||||
hook_progress_reporter: Option<Box<dyn HookProgressReporter>>,
|
||||
session_tracer: Option<SessionTracer>,
|
||||
turn_progress_reporter: Option<Box<dyn TurnProgressReporter>>,
|
||||
}
|
||||
|
||||
impl<C, T> ConversationRuntime<C, T>
|
||||
|
|
@ -223,6 +240,7 @@ where
|
|||
hook_abort_signal: HookAbortSignal::default(),
|
||||
hook_progress_reporter: None,
|
||||
session_tracer: None,
|
||||
turn_progress_reporter: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -266,6 +284,14 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_turn_progress_reporter(
|
||||
mut self,
|
||||
reporter: Box<dyn TurnProgressReporter>,
|
||||
) -> Self {
|
||||
self.turn_progress_reporter = Some(reporter);
|
||||
self
|
||||
}
|
||||
|
||||
fn run_pre_tool_use_hook(&mut self, tool_name: &str, input: &str) -> HookRunResult {
|
||||
if let Some(reporter) = self.hook_progress_reporter.as_mut() {
|
||||
self.hook_runner.run_pre_tool_use_with_context(
|
||||
|
|
@ -551,7 +577,8 @@ where
|
|||
|
||||
// Phase 3: Post-hooks and session updates (sequential, original order).
|
||||
for p in &pending {
|
||||
let result_message = if p.allowed {
|
||||
// Capture progress data for the reporter.
|
||||
let (progress_tool_name, progress_input, progress_output, progress_is_error, result_message) = if p.allowed {
|
||||
let batch_result = &batch_results[batch_index];
|
||||
batch_index += 1;
|
||||
let (mut output, mut is_error) = match &batch_result.result {
|
||||
|
|
@ -587,25 +614,36 @@ where
|
|||
|| post_hook_result.is_failed()
|
||||
|| post_hook_result.is_cancelled(),
|
||||
);
|
||||
|
||||
ConversationMessage::tool_result(
|
||||
let progress_output = output.clone();
|
||||
let result_message = ConversationMessage::tool_result(
|
||||
p.tool_use_id.clone(),
|
||||
p.tool_name.clone(),
|
||||
output,
|
||||
is_error,
|
||||
)
|
||||
);
|
||||
(p.tool_name.clone(), p.effective_input.clone(), progress_output, is_error, result_message)
|
||||
} else {
|
||||
ConversationMessage::tool_result(
|
||||
let denied_output = merge_hook_feedback(&p.pre_hook_messages, p.deny_reason.clone().unwrap_or_default(), true);
|
||||
let result_message = ConversationMessage::tool_result(
|
||||
p.tool_use_id.clone(),
|
||||
p.tool_name.clone(),
|
||||
merge_hook_feedback(&p.pre_hook_messages, p.deny_reason.clone().unwrap_or_default(), true),
|
||||
denied_output,
|
||||
true,
|
||||
)
|
||||
);
|
||||
(p.tool_name.clone(), String::new(), String::new(), true, result_message)
|
||||
};
|
||||
self.session
|
||||
.push_message(result_message.clone())
|
||||
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
||||
self.record_tool_finished(iterations, &result_message);
|
||||
if let Some(ref reporter) = self.turn_progress_reporter {
|
||||
let report_result = if progress_is_error {
|
||||
Err(progress_output.as_str())
|
||||
} else {
|
||||
Ok(progress_output.as_str())
|
||||
};
|
||||
reporter.on_tool_result(iterations, self.max_iterations, &progress_tool_name, &progress_input, report_result);
|
||||
}
|
||||
tool_results.push(result_message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ pub use lsp_discovery::{
|
|||
pub use conversation::{
|
||||
auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent,
|
||||
ConversationRuntime, PromptCacheEvent, RuntimeError, StaticToolExecutor, ToolCall, ToolError,
|
||||
ToolExecutor, ToolResult, TurnSummary,
|
||||
ToolExecutor, ToolResult, TurnProgressReporter, TurnSummary,
|
||||
};
|
||||
pub use file_ops::{
|
||||
edit_file, edit_file_in_workspace, glob_search, glob_search_in_workspace, grep_search,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub enum ReadOutcome {
|
|||
Cancel,
|
||||
Exit,
|
||||
ProviderSwap,
|
||||
TeamToggle,
|
||||
}
|
||||
|
||||
struct SlashCommandHelper {
|
||||
|
|
@ -130,6 +131,11 @@ impl LineEditor {
|
|||
KeyEvent(KeyCode::Char('P'), Modifiers::CTRL),
|
||||
Cmd::SelfInsert(1, '\x01'),
|
||||
);
|
||||
// Ctrl+T inserts a sentinel character that toggles agent teams mode.
|
||||
editor.bind_sequence(
|
||||
KeyEvent(KeyCode::Char('T'), Modifiers::CTRL),
|
||||
Cmd::SelfInsert(1, '\x02'),
|
||||
);
|
||||
|
||||
Self {
|
||||
prompt: prompt.into(),
|
||||
|
|
@ -168,6 +174,10 @@ impl LineEditor {
|
|||
if line.contains('\x01') {
|
||||
return Ok(ReadOutcome::ProviderSwap);
|
||||
}
|
||||
// Ctrl+T inserts \x02 sentinel — toggles team mode.
|
||||
if line.contains('\x02') {
|
||||
return Ok(ReadOutcome::TeamToggle);
|
||||
}
|
||||
Ok(ReadOutcome::Submit(line))
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
|
|
|
|||
|
|
@ -6937,6 +6937,7 @@ fn run_resume_command(
|
|||
| SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
|
||||
| SlashCommand::AddDir { .. }
|
||||
| SlashCommand::Lsp { .. }
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
| SlashCommand::Setup => Err("unsupported resumed slash command".into()),
|
||||
=======
|
||||
|
|
@ -6944,6 +6945,10 @@ fn run_resume_command(
|
|||
| SlashCommand::Lsp { .. } => Err("unsupported resumed slash command".into()),
|
||||
>>>>>>> 0b227b62 (fix: resolve cherry-pick conflicts and remove non-LSP artifacts)
|
||||
}
|
||||
=======
|
||||
| SlashCommand::Team { .. }
|
||||
| SlashCommand::Setup => Err("unsupported resumed slash command".into()), }
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
}
|
||||
|
||||
/// Detect if the current working directory is "broad" (home directory or
|
||||
|
|
@ -7207,6 +7212,17 @@ fn run_repl(
|
|||
}
|
||||
println!("{}", format_connected_line(&cli.model));
|
||||
}
|
||||
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.shutdown_lsp_servers();
|
||||
|
|
@ -8322,6 +8338,52 @@ impl LiveCli {
|
|||
run_init(CliOutputFormat::Text)?;
|
||||
false
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
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
|
||||
}
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
SlashCommand::Diff => {
|
||||
Self::print_diff()?;
|
||||
false
|
||||
|
|
@ -14270,8 +14332,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