feat(teams): improve team activation and resilience

- context.go: add Rule #5 'Team delegation' to system prompt — agents now
  instructed to proactively use 'team' for multi-step/multi-concern tasks
  instead of handling them inline
- team.go: add 'WHEN TO USE THIS TOOL' activation triggers to tool description;
  strengthen decomposition rules with domain-agnostic project-manager framing
- loop.go: call subagentManager.SetTools(agent.Tools) after full registry is
  built so sub-agents inherit 'team' tool for recursive hierarchical decomposition
- toolloop.go: replace hard token budget failure with soft graceful degradation
  (50% advisory warning, 0% wrap-up signal + final summary call);
  add truncation recovery for max_tokens cutoff (finish_reason=truncated)
- openai_compat/provider.go: detect truncated JSON tool calls and set
  FinishReason='truncated' instead of silently storing malformed raw args
This commit is contained in:
Administrator 2026-02-28 20:33:40 +08:00
parent efb9dc27ad
commit 33c759f70a
6 changed files with 94 additions and 11 deletions

View file

@ -78,7 +78,9 @@ Your workspace is at: %s
3. **Memory** - When interacting with me if something seems memorable, update %s/memory/MEMORY.md
4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content.`,
4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content.
5. **Team delegation** - For any task that is non-trivial, multi-step, or involves distinct concerns (e.g. "convert React to Vue", "build a feature", "analyze and report"), you MUST use the 'team' tool to delegate and parallelize. Do NOT attempt to handle complex tasks inline by calling tools one by one yourself. Decompose first, delegate second, then report the outcome.`,
workspacePath, workspacePath, workspacePath, workspacePath, workspacePath)
}

View file

@ -161,6 +161,12 @@ func registerSharedTools(
spawnSubAgentTool := tools.NewSpawnSubAgentTool(subagentManager)
agent.Tools.Register(spawnSubAgentTool)
// Direction 3: Hierarchical Decomposition.
// Share the fully-built registry (which includes team, spawn_sub_agent, etc.) back
// to the subagent manager so that all workers spawned by this agent also inherit
// the full toolset — enabling sub-agents to recursively call 'team' themselves.
subagentManager.SetTools(agent.Tools)
}
}

View file

@ -235,6 +235,7 @@ func parseResponse(body []byte) (*LLMResponse, error) {
choice := apiResponse.Choices[0]
toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))
truncated := false
for _, tc := range choice.Message.ToolCalls {
arguments := make(map[string]any)
name := ""
@ -249,8 +250,10 @@ func parseResponse(body []byte) (*LLMResponse, error) {
name = tc.Function.Name
if tc.Function.Arguments != "" {
if err := json.Unmarshal([]byte(tc.Function.Arguments), &arguments); err != nil {
// JSON is malformed (likely truncated due to max_tokens). Log and signal truncation.
log.Printf("openai_compat: failed to decode tool call arguments for %q: %v", name, err)
arguments["raw"] = tc.Function.Arguments
truncated = true
continue // Skip this malformed tool call entirely
}
}
}
@ -274,13 +277,19 @@ func parseResponse(body []byte) (*LLMResponse, error) {
toolCalls = append(toolCalls, toolCall)
}
finishReason := choice.FinishReason
// Propagate truncation: if finish_reason is "length" or we detected bad JSON, mark as truncated.
if truncated || finishReason == "length" {
finishReason = "truncated"
}
return &LLMResponse{
Content: choice.Message.Content,
ReasoningContent: choice.Message.ReasoningContent,
Reasoning: choice.Message.Reasoning,
ReasoningDetails: choice.Message.ReasoningDetails,
ToolCalls: toolCalls,
FinishReason: choice.FinishReason,
FinishReason: finishReason,
Usage: apiResponse.Usage,
}, nil
}

View file

@ -15,6 +15,7 @@ import (
// These are set via `"tags": ["vision", "code"]` under each model in the model list.
const (
ModelTagVision = "vision" // Supports image/screenshot input (multimodal)
ModelTagImageGen = "image-gen" // Supports image generation output (e.g. DALL-E, Stable Diffusion)
ModelTagCode = "code" // Specialized for code generation and analysis
ModelTagFast = "fast" // Low-latency model, suited for lightweight tasks
ModelTagLongContext = "long-context" // Supports very long context windows (>100k tokens)
@ -24,7 +25,8 @@ const (
// modelTagDescriptions provides LLM-readable explanations of each known tag,
// injected at runtime into the tool description to guide model selection.
var modelTagDescriptions = map[string]string{
ModelTagVision: "can analyze images and screenshots",
ModelTagVision: "can analyze images and screenshots (multimodal input)",
ModelTagImageGen: "can generate images from text descriptions (e.g. DALL-E, Stable Diffusion)",
ModelTagCode: "specialized in code generation and debugging",
ModelTagFast: "fast and lightweight, ideal for simple or high-frequency tasks",
ModelTagLongContext: "handles very long inputs (>100k tokens)",

View file

@ -38,7 +38,29 @@ func (t *TeamTool) Name() string {
}
func (t *TeamTool) Description() string {
base := "Compose and execute a team of distinct sub-agents. You (the main agent) should autonomously analyze the user's request, determine the necessary specialized roles, break down the work into sub-tasks, and assign them. Execute sequentially (passing output from one to the next) or concurrently in parallel."
base := `Compose and execute a team of specialized sub-agents to accomplish a complex task.
WHEN TO USE THIS TOOL (use proactively do not attempt to handle these alone):
- The task involves 2 or more distinct areas of concern (e.g. research + writing, coding + testing, data gathering + analysis).
- The task would require more than 5 consecutive tool calls if done alone.
- Any part of the task can be done in parallel to save time.
- The task is large enough that a single agent would likely lose context or quality midway.
- The user asks you to "build", "create", "generate", "analyze", or "convert" something non-trivial.
When in doubt, prefer delegation over doing everything yourself.
CRITICAL RULES FOR TASK PLANNING:
1. Think like a project manager: analyze the full task first, then design the team structure before spawning anyone.
2. Decompose the task into the smallest independently-ownable units of work. A member should own exactly ONE distinct concern not a broad compound goal.
3. Identify dependencies between units: if one member's output is required by another, declare it via 'depends_on'. Independent units should run concurrently.
4. Each member's 'task' must be precise and self-contained. Include relevant context (e.g. reference to outputs from dependencies) directly in the task description.
5. Sub-agents are full agents with access to the same tools, including this 'team' tool. If a member's sub-task is itself complex, it may recursively form its own team.
Strategy guide:
- sequential: each step depends on the full output of the previous step in a strict chain.
- parallel: all tasks are fully independent with no shared inputs or outputs.
- dag: most real-world tasks some tasks depend on others, some can run concurrently.
- evaluator_optimizer: the output needs iterative critique and revision cycles.`
if t.manager != nil {
if hint := t.manager.ModelCapabilityHint(); hint != "" {
return base + "\n\n" + hint

View file

@ -76,18 +76,60 @@ func RunToolLoop(
return nil, fmt.Errorf("LLM call failed: %w", err)
}
// 3.5 Token Budget Enforcement
// 3.5 Token Budget: Soft enforcement with graceful degradation.
// Budget exhaustion is NOT a hard error — workers get a chance to wrap up gracefully.
if response.Usage != nil && config.RemainingTokenBudget != nil {
newBudget := config.RemainingTokenBudget.Add(-int64(response.Usage.TotalTokens))
if newBudget < 0 {
logger.ErrorCF("toolloop", "Token budget exceeded", map[string]any{
"used_iteration": response.Usage.TotalTokens,
"deficit": newBudget,
originalBudget := newBudget + int64(response.Usage.TotalTokens)
if newBudget <= 0 {
// Budget exhausted: signal the worker to wrap up and return partial result.
logger.WarnCF("toolloop", "Token budget exhausted, injecting wrap-up signal",
map[string]any{
"deficit": -newBudget,
"iteration": iteration,
})
finalContent = response.Content
messages = append(messages, providers.Message{
Role: "assistant",
Content: response.Content,
})
messages = append(messages, providers.Message{
Role: "user",
Content: "[SYSTEM] Token budget has been exhausted. Stop all tool calls immediately and return the best result you have completed so far. Do not call any more tools.",
})
// One final LLM call to get a summary/wrap-up from the model
if finalResp, err := config.Provider.Chat(ctx, messages, nil, config.Model, config.LLMOptions); err == nil {
finalContent = finalResp.Content
}
break
} else if originalBudget > 0 && newBudget < originalBudget/2 {
// Budget below 50%: soft warning injected into next iteration's context.
logger.WarnCF("toolloop", "Token budget below 50%, injecting advisory",
map[string]any{"remaining": newBudget, "iteration": iteration})
messages = append(messages, providers.Message{
Role: "user",
Content: "[SYSTEM] Advisory: token budget is running low. Please prioritize completing the most critical parts of your task and avoid unnecessary tool calls.",
})
return nil, fmt.Errorf("Token Budget Exceeded: Team budget completely consumed")
}
}
// 3.6 Truncation Recovery: LLM response was cut off (max_tokens hit or malformed JSON).
// Inject a recovery message so the LLM knows to retry with a shorter, complete response.
if response.FinishReason == "truncated" {
logger.WarnCF("toolloop", "LLM response was truncated (max_tokens hit), injecting recovery message",
map[string]any{"iteration": iteration})
messages = append(messages, providers.Message{
Role: "assistant",
Content: response.Content,
})
messages = append(messages, providers.Message{
Role: "user",
Content: "[SYSTEM] Your previous response was cut off because it exceeded the token limit. Please retry by producing a shorter, complete response. If you were about to call a tool, make sure the full JSON arguments are included without truncation.",
})
continue
}
// 4. If no tool calls, we're done
if len(response.ToolCalls) == 0 {
finalContent = response.Content