Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions cmd/late/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func main() {
logitBiasReq := flag.String("logit-bias", "", "Main-agent token bias: JSON object or comma-separated TOKEN_ID:BIAS pairs.")
suppressThinkingWordsReq := flag.Bool("suppress-thinking-words", false, "Bias anti-overthinking tokens (requires the same model for main agent and subagents).")
subagentLogitBiasReq := flag.String("subagent-logit-bias", "", "Subagent token bias: JSON object or comma-separated TOKEN_ID:BIAS pairs.")
maxAsyncSubagentsReq := flag.Int("max-async-subagents", 0, "Maximum number of concurrent subagents (default: 2, or from config)")

flag.Usage = func() {
writeHelp(os.Stderr, flag.CommandLine)
Expand Down Expand Up @@ -204,6 +205,13 @@ func main() {
}
}

// Load App configuration
appConfig, err := appconfig.LoadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to load app config: %v\n", err)
}
maxAsyncSubagents := appconfig.ResolveMaxAsyncSubagents(appConfig, *maxAsyncSubagentsReq)

// Plugin command handler — dispatches before TUI startup
var pluginManager *plugin.PluginManager
cwd, _ := os.Getwd()
Expand Down Expand Up @@ -247,6 +255,10 @@ func main() {
systemPrompt = string(content)
}

systemPrompt = common.ReplacePlaceholders(systemPrompt, map[string]string{
"${{MAX_ASYNC_SUBAGENTS}}": fmt.Sprintf("%d", maxAsyncSubagents),
})

if *injectCWDReq {
cwd, err := os.Getwd()
if err == nil {
Expand Down Expand Up @@ -362,11 +374,7 @@ func main() {
}
}
}
// Load App configuration
appConfig, err := appconfig.LoadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to load app config: %v\n", err)
}

enabledTools := make(map[string]bool)
if appConfig != nil {
for toolName, enabled := range appConfig.EnabledTools {
Expand Down Expand Up @@ -768,6 +776,13 @@ func main() {
sess.Registry.Register(tool.SpawnSubagentTool{
Runner: runner,
})

if enabledTools["batch_spawn_subagents"] {
sess.Registry.Register(tool.BatchSpawnSubagentsTool{
Runner: runner,
MaxConcurrent: maxAsyncSubagents,
})
}
}

if _, err := p.Run(); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Late splits planning from execution: the **Lead Orchestrator** retains only high
- Cannot directly edit files (`write_file` and `target_edit` are physically unregistered).
- Destructive shell operations are blocked (including output redirection `>`).
- Instructed not to perform broad direct codebase scans; delegates discovery to the Researcher.
- **Subagent Spawning:** Dispatches atomic implementation steps to specialized workers via `spawn_subagent`.
- **Subagent Spawning:** Dispatches atomic implementation steps to specialized workers sequentially via `spawn_subagent` or concurrently via `batch_spawn_subagents`.

### Researcher
- **Role:** Read-only codebase explorer and contextual investigator.
Expand All @@ -51,7 +51,7 @@ Late splits planning from execution: the **Lead Orchestrator** retains only high
2. **Context Discovery (Optional):** For non-trivial tasks, the Orchestrator invokes `spawn_subagent` (type: `researcher`) to inspect the codebase and report constraints, relevant files, and patterns.
3. **Plan Formulation:** The Orchestrator synthesizes findings and writes a formal plan to `./implementation_plan.md` via `write_implementation_plan`.
4. **Milestone Tracking:** The Orchestrator registers atomic phases using `create_todos`.
5. **Worker Delegation:** (If approved:) The Orchestrator invokes `spawn_subagent` (type: `coder`) for an individual atomic step.
5. **Worker Delegation:** (If approved:) The Orchestrator invokes `spawn_subagent` (type: `coder`) for an individual atomic step, or `batch_spawn_subagents` for concurrent execution of independent steps.
6. **Isolated Execution:** The Coder inspects designated files, applies modifications, and runs validation commands within its private context.
7. **Structured Handoff:** The Coder returns a structured summary of applied changes, test results, or blocking issues. Its ephemeral scratchpad is terminated.
8. **Verification & Advancement:** The Orchestrator evaluates the result, marks the task complete via `finish_todo`, and proceeds to the next step.
Expand All @@ -74,7 +74,7 @@ Unlike systems where subagent delegation is merely prompt-recommended or optiona

- **Physical Tool Namespace Pruning:**
- Write tools (`write_file`, `target_edit`) are not registered in the Orchestrator's tool registry.
- Planning and delegation tools (`spawn_subagent`, `write_implementation_plan`, `create_todos`, `list_todos`, `finish_todo`) are omitted when constructing subagent registries.
- Planning and delegation tools (`spawn_subagent`, `batch_spawn_subagents`, `write_implementation_plan`, `create_todos`, `list_todos`, `finish_todo`) are omitted when constructing subagent registries.
- **Shell-Level Enforcement:**
- Shell commands pass through an AST and policy engine. Shell output redirection (`>`) is blocked, preventing orchestrator workarounds to write files via shell scripts.
- Search commands (`grep`, `find`, `rg`) are gated with directions to use native `.gitignore`-aware search tools.
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Late 将规划与执行分离:**主编排器(Lead Orchestrator)**仅保留
- 不能直接编辑文件(在物理层面上未注册 `write_file` 和 `target_edit` 工具)。
- 拦截破坏性的 shell 操作(包括输出重定向 `>`)。
- 被要求不得直接进行大范围的代码库扫描;将探索发现的任务委托给研究员(Researcher)。
- **子智能体调度:** 通过 `spawn_subagent` 将原子化的实现步骤分发给专门的工作智能体。
- **子智能体调度:** 通过 `spawn_subagent`(顺序执行)或 `batch_spawn_subagents`(并发批量执行)将原子化的实现步骤分发给专门的工作智能体。

### 研究员 (Researcher)
- **角色:** 只读的代码库探索者和代码上下文研究员。
Expand All @@ -51,7 +51,7 @@ Late 将规划与执行分离:**主编排器(Lead Orchestrator)**仅保留
2. **上下文探索(可选):** 对于非简单的任务,编排器会调用 `spawn_subagent`(类型:`researcher`)来检查代码库并报告约束条件、相关文件和代码模式。
3. **计划制定:** 编排器综合调查结果,并通过 `write_implementation_plan` 将正式的计划写入 `./implementation_plan.md`。
4. **里程碑追踪:** 编排器使用 `create_todos` 注册原子化的阶段任务。
5. **工作委托:** (获得批准后)编排器调用 `spawn_subagent`(类型:`coder`)来执行单个原子步骤。
5. **工作委托:** (获得批准后)编排器调用 `spawn_subagent`(类型:`coder`)来执行单个原子步骤,或使用 `batch_spawn_subagents` 并发执行互不依赖的独立步骤。
6. **隔离执行:** 程序员在私有上下文中检查指定文件、应用修改并运行验证命令。
7. **结构化交接:** 程序员返回所做更改、测试结果或阻塞问题的结构化摘要。其临时上下文随后被销毁。
8. **验证与推进:** 编排器评估结果,通过 `finish_todo` 将任务标记为完成,然后继续下一步。
Expand All @@ -74,7 +74,7 @@ Late 将规划与执行分离:**主编排器(Lead Orchestrator)**仅保留

- **物理工具命名空间裁剪:**
- 写入工具(`write_file`、`target_edit`)不会注册在编排器的工具注册表中。
- 在构建子智能体注册表时,规划和委托工具(`spawn_subagent`、`write_implementation_plan`、`create_todos`、`list_todos`、`finish_todo`)会被省略。
- 在构建子智能体注册表时,规划和委托工具(`spawn_subagent`、`batch_spawn_subagents`、`write_implementation_plan`、`create_todos`、`list_todos`、`finish_todo`)会被省略。
- **Shell 级别的强制执行:**
- Shell 命令会经过 AST 和策略引擎的处理。阻止 Shell 输出重定向(`>`),以防止编排器通过 shell 脚本变相写入文件。
- 搜索命令(`grep`、`find`、`rg`)会被拦截,并指引使用能够感知 `.gitignore` 的原生搜索工具。
Expand Down
5 changes: 2 additions & 3 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ func NewSubagentOrchestrator(
// Inherit all tools from parent (including MCP tools)
if parent != nil && parent.Registry() != nil {
for _, t := range parent.Registry().All() {
// Skip spawn_subagent and write_implementation_plan to prevent recursion/confusion
// Skip spawn_subagent, batch_spawn_subagents and write_implementation_plan to prevent recursion/confusion
name := t.Name()
if name == "spawn_subagent" || name == "write_implementation_plan" ||
if name == "spawn_subagent" || name == "batch_spawn_subagents" || name == "write_implementation_plan" ||
name == "create_todos" || name == "list_todos" || name == "finish_todo" {
continue
}
Expand Down Expand Up @@ -136,7 +136,6 @@ func NewSubagentOrchestrator(
return nil, fmt.Errorf("failed to add initial message: %w", err)
}

// 4. Create Orchestrator
mws := parent.Middlewares()

if messenger != nil {
Expand Down
38 changes: 38 additions & 0 deletions internal/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,44 @@ func TestNewSubagentOrchestratorID(t *testing.T) {
t.Errorf("Expected child ID to contain 'coder', got %s", child.ID())
}
}
// TestNewSubagentOrchestrator_ToolExclusion verifies that batch_spawn_subagents,
// spawn_subagent, and write_implementation_plan are excluded from the subagent registry
func TestNewSubagentOrchestrator_ToolExclusion(t *testing.T) {
cfg := client.Config{BaseURL: "http://localhost:8080"}
c := client.NewClient(cfg)
mockSession := session.New(c, "/tmp/mock-session.json", []client.ChatMessage{}, "mock system prompt", true)
parent := orchestrator.NewBaseOrchestrator("parent", mockSession, nil, 100)

child, err := NewSubagentOrchestrator(
c,
"test goal",
[]string{},
"coder",
map[string]bool{"bash": true, "read_file": true},
false,
false,
100,
"",
false,
parent,
nil,
)
if err != nil {
t.Fatalf("Failed to create subagent: %v", err)
}

reg := child.Registry()
if reg.Get("spawn_subagent") != nil {
t.Errorf("expected spawn_subagent to NOT be registered in subagent")
}
if reg.Get("batch_spawn_subagents") != nil {
t.Errorf("expected batch_spawn_subagents to NOT be registered in subagent")
}
if reg.Get("write_implementation_plan") != nil {
t.Errorf("expected write_implementation_plan to NOT be registered in subagent")
}
}

// TestNewSubagentOrchestrator_ConcurrentSpawn is the FR2 regression test:
// concurrent spawns against a shared parent must never mint duplicate child IDs.
func TestNewSubagentOrchestrator_ConcurrentSpawn(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions internal/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type SubagentConfig struct {
Description string `json:"description"`
PromptFile string `json:"prompt_file"`
AllowedTools []string `json:"allowed_tools"`
Async bool `json:"async,omitempty"`
}

func GetSubagents() []SubagentConfig {
Expand Down
26 changes: 17 additions & 9 deletions internal/assets/prompts/instruction-orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,25 @@ Your goal is to analyze complex user requests, explore the existing codebase to
* **YOU MUST**: Use `search_content` (for text inside files) and `find_files` (for finding files/directories) instead of using the `bash_tool` with e.g. `grep`/`find`/`rg`.
* **YOU MUST**: Use `write_implementation_plan` to record your design before any execution.
* **YOU MUST**: Use `create_todos`, `list_todos`, and `finish_todo` to track high-level execution progress, but ONLY AFTER writing the implementation plan.
* **YOU MUST**: Use `spawn_subagent` (type `coder`) for **ALL** direct file modifications. **CRITICAL TOOL RULE: You MUST invoke the `spawn_subagent` tool MULTIPLE TIMES—exactly once for EVERY individual step in your Implementation Plan. You are strictly forbidden from passing multiple steps or the entire plan into a single `spawn_subagent` call.**
* **YOU MUST**: Use `coder` subagent(s) for direct file modifications using `spawn_subagent` or `batch_spawn_subagents`.
* For sequential steps or single tasks, invoke `spawn_subagent`.
* When multiple steps in your Implementation Plan are independent (operating on separate files without cross-dependencies), you can invoke `batch_spawn_subagents` to execute up to ${{MAX_ASYNC_SUBAGENTS}} subagents concurrently. This runs them in parallel and returns all outputs at once in a single turn, preserving your KV-cache context and accelerating execution significantly.
* You may run different types of subagents asynchronously if appropriate (e.g. investigating separate parts of the codebase using `researcher` subagents).
* **YOU CANNOT**: Edit files, create files (other than the plan), or run destructive bash commands.
* *Note: Direct file-editing tools (like `write_file` or `target_edit`) are physically removed from your toolset. You MUST delegate all coding to subagents.*
* *Even for requests to "implement", "add", "update", or "edit", you MUST follow the plan -> subagent pipeline. Direct edits are only for subagents.*

## 2. Your Workflow

You must not just "guess" the plan. You must **investigate** first (by using a `researcher` subagent) to ensure your plan is grounded in reality. If an `AGENTS.md` exists make sure to read it first.
You must not just "guess" the plan. You must initially **investigate** first (by using (a) `researcher` subagent(s)) to ensure your plan is grounded in reality.
If an `AGENTS.md` exists make sure to read it first. You may identify if one exists by checking the toplevel directory of the repository before spawning (a) researcher subagent(s).

### Phase 1: Exploration & Discovery

**YOU MUST NOT**: Start searching or reading files yourself immediately (except for `AGENTS.md`). Your first action for any new, non-trivial request MUST be gathering context via a `researcher` subagent.
1. **Instruct the Researcher**: You MUST use `spawn_subagent` (type `researcher`) for broad exploration of the codebase.
2. Provide the researcher with clear instructions on what to look out for based on the user's prompt.
3. The researcher will map the project geography, trace logic, identify constraints, and return a comprehensive repo summary to you.
Your first action (after potentially reading an `AGENTS.md`) for any new, non-trivial request MUST be gathering context via (a) `researcher` subagent(s). Follow the following plan to satisfy the constraints:
1. Spawn (a) `researcher` subagent(s) using `spawn_subagent` or `batch_spawn_subagents` for broad exploration of the codebase.
2. Provide the researcher(s) with clear instructions on what to look out for based on the user's prompt.
3. The researcher(s) will map the project geography, trace logic, identify constraints, and return a comprehensive repo summary for you.

### Phase 2: Strategic Thinking

Expand Down Expand Up @@ -57,7 +61,7 @@ Output a structured **Implementation Plan** in Markdown. This plan will be hande
If you identify relevant **Agent Skills** (available via `activate_skill` metadata), you should:

1. **Activate them yourself**: If you need the skill's instructions to formulate a grounding and accurate plan.
2. **Context Injection**: When spawning a `coder` subagent via `spawn_subagent`, you **MUST** explicitly instruct the coder in the `goal` parameter to activate the relevant skill(s) (e.g., "Use the `anthropic-guidelines` skill to ensure correct branding"). This ensures the coder accesses the necessary specialized instructions and script tools.
2. **Context Injection**: When spawning a `coder` subagent (via `spawn_subagent` or `batch_spawn_subagents`), you **MUST** explicitly instruct the coder in the `goal` parameter to activate the relevant skill(s) (e.g., "Use the `anthropic-guidelines` skill to ensure correct branding"). This ensures the coder accesses the necessary specialized instructions and script tools.

## 3. Output Format

Expand Down Expand Up @@ -97,9 +101,13 @@ Clarity is key. Group steps logically.

## 5. Implementation Workflow

You must not edit any files yourself. You must use `coder` subagents to edit files. You must use `spawn_subagent` to spawn a subagent. You must use atomic steps in your plan. Each step should be a single, atomic action that can be performed independently of other steps. Each `coder` subagent being invoked by you must implement one single step only of your plan.
You must not edit any files yourself. You must use subagents to perform tasks. You must use atomic steps in your plan. Each step should be a single, atomic action that can be performed independently of other steps.

* **Progress Tracking**: Before or after spawning subagents, use `list_todos` to review progress. As each high-level step or milestone from your plan is completed by a `coder` subagent, use `finish_todo` to mark it complete.
When executing your plan:
1. **Sequential Execution**: Use `spawn_subagent` (type `coder` or `researcher`) for individual steps, or for steps that depend sequentially on previous steps.
2. **Concurrent/Async Execution**: When your plan contains independent steps that do not conflict (e.g. creating different files or modifying independent modules), you can execute them concurrently using `batch_spawn_subagents` (up to ${{MAX_ASYNC_SUBAGENTS}} subagents). All spawned subagents run in parallel and their results are returned together in a single response. This preserves your KV-cache and optimizes execution speed. It is strictly your responsibility to ensure that tasks executed concurrently do not modify the same files.

* **Progress Tracking**: Before or after spawning subagents, use `list_todos` to review progress. As each high-level step or milestone from your plan is completed by (a) `coder` subagent(s), use `finish_todo` to mark it complete. Do not mark a todo as completed before running the respective subagent(s).

## Current working dir

Expand Down
1 change: 1 addition & 0 deletions internal/assets/subagents/subagent-coder.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "coder",
"description": "'coder' for writing/modifying code.",
"prompt_file": "prompts/instruction-coding.md",
"async": true,
"allowed_tools": [
"read_file",
"write_file",
Expand Down
1 change: 1 addition & 0 deletions internal/assets/subagents/subagent-researcher.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "researcher",
"description": "'researcher' for codebase analysis.",
"prompt_file": "prompts/instruction-researcher.md",
"async": true,
"allowed_tools": [
"read_file",
"search_content",
Expand Down
46 changes: 36 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,24 +83,30 @@ type Config struct {

SkillsDir string `json:"skills_dir,omitempty"`

MaxAsyncSubagents int `json:"max_async_subagents,omitempty"`

Theme string `json:"theme,omitempty"`
Models []ModelSetting `json:"models,omitempty"`
AgentModels map[string]string `json:"agent_models,omitempty"`
}

const DefaultMaxAsyncSubagents = 2

func defaultConfig() Config {
return Config{
MaxAsyncSubagents: DefaultMaxAsyncSubagents,
EnabledTools: map[string]bool{
"read_file": true,
"write_file": true,
"target_edit": true,
"spawn_subagent": true,
"bash": true,
"search_content": true,
"find_files": true,
"create_todos": true,
"list_todos": true,
"finish_todo": true,
"read_file": true,
"write_file": true,
"target_edit": true,
"spawn_subagent": true,
"batch_spawn_subagents": true,
"bash": true,
"search_content": true,
"find_files": true,
"create_todos": true,
"list_todos": true,
"finish_todo": true,
},
}
}
Expand Down Expand Up @@ -239,6 +245,26 @@ func ResolveSubagentSettingsWithEnv(cfg *Config, openAI OpenAISettings, lookup E
return resolved
}

func ResolveMaxAsyncSubagents(cfg *Config, flagVal int) int {
return ResolveMaxAsyncSubagentsWithEnv(cfg, flagVal, os.LookupEnv)
}

func ResolveMaxAsyncSubagentsWithEnv(cfg *Config, flagVal int, lookup EnvLookup) int {
if flagVal > 0 {
return flagVal
}
if value, ok := nonEmptyEnv(lookup, "LATE_MAX_ASYNC_SUBAGENTS"); ok {
var parsed int
if _, err := fmt.Sscanf(value, "%d", &parsed); err == nil && parsed > 0 {
return parsed
}
}
if cfg != nil && cfg.MaxAsyncSubagents > 0 {
return cfg.MaxAsyncSubagents
}
return DefaultMaxAsyncSubagents
}

// ResolveSaveSubagentHistories determines whether subagent history
// persistence is enabled. Precedence: explicit CLI flag > saved session
// preference > config file. There is intentionally no environment-variable
Expand Down
Loading
Loading