Why: Old tool results were filling the window, compaction could split a tool group or commit a stale summary, and reminders arrived as extra user messages. Evidence: Compaction retries a conflict and opens the circuit after three failures; clearing keeps the last five tool results; a stale file write is rejected; RPC reports started, queued, or handled. Impact: Requests over the context window replace older tool results with a placeholder. Rewriting an existing file now requires a read in the same run. Subagent text over 32000 characters is capped. Refs: feature/context-engineering Co-authored-by: Cursor <cursoragent@cursor.com>
125 lines
4.2 KiB
Go
125 lines
4.2 KiB
Go
// Package subagent 定义委派端口:内核不内置嵌套执行(那是编排层职责),
|
|
// 只提供工具面与信息集契约;宿主(产品 SpawnSubagent、小队引擎等)
|
|
// 注入 Factory 实现真实派生执行。未注入则 delegate_task 不注册(fail-closed)。
|
|
//
|
|
// 信息集对齐 agent-execution 标准的委派条款:目标、输入、写范围、非目标、
|
|
// 完成判据、预算与回传格式——子任务不能从父任务目标推导额外权限。
|
|
package subagent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"agent-native/runtime/tools"
|
|
)
|
|
|
|
// Request 是一次委派的完整信息集(模型经工具参数逐项填写)。
|
|
type Request struct {
|
|
Goal string `json:"goal"`
|
|
Inputs string `json:"inputs,omitempty"`
|
|
WriteScope string `json:"write_scope,omitempty"`
|
|
NonGoals string `json:"non_goals,omitempty"`
|
|
DoneCriteria string `json:"done_criteria,omitempty"`
|
|
Deliverables []string `json:"deliverables,omitempty"`
|
|
// Deadline / MaxToolCalls 是宿主注入的预算余量(模型不可自行扩权)。
|
|
Deadline time.Time `json:"-"`
|
|
MaxToolCalls int `json:"-"`
|
|
}
|
|
|
|
// Result 是子代理的回传:至少包含结论文本;文件由宿主按写范围物化。
|
|
type Result struct {
|
|
Success bool
|
|
Text string
|
|
Files map[string][]byte
|
|
Err string
|
|
}
|
|
|
|
// Factory 由宿主注入;ctx 取消必须传播到子执行。
|
|
type Factory func(ctx context.Context, req Request) (Result, error)
|
|
|
|
type delegateTool struct{ factory Factory }
|
|
|
|
// Register 把 delegate_task 工具注册进注册表(factory 为 nil 时不注册)。
|
|
func Register(r *tools.Registry, factory Factory) {
|
|
if factory == nil {
|
|
return
|
|
}
|
|
r.Register(&delegateTool{factory: factory})
|
|
}
|
|
|
|
func (t *delegateTool) Name() string { return "delegate_task" }
|
|
func (t *delegateTool) Description() string {
|
|
return "Delegate a self-contained subtask to a fresh sub-agent. Fill every field: goal, inputs (with versions), write_scope (directory the sub-agent may write), non_goals, done_criteria, deliverables. The sub-agent cannot exceed your permissions; report back its conclusion and files."
|
|
}
|
|
func (t *delegateTool) Parameters() json.RawMessage {
|
|
return json.RawMessage(`{"type":"object","required":["goal","done_criteria"],"properties":{
|
|
"goal":{"type":"string"},
|
|
"inputs":{"type":"string"},
|
|
"write_scope":{"type":"string"},
|
|
"non_goals":{"type":"string"},
|
|
"done_criteria":{"type":"string"},
|
|
"deliverables":{"type":"array","items":{"type":"string"}}
|
|
}}`)
|
|
}
|
|
|
|
func (t *delegateTool) Execute(ctx context.Context, args json.RawMessage) (tools.Result, error) {
|
|
var req Request
|
|
if err := json.Unmarshal(args, &req); err != nil {
|
|
return tools.Result{}, err
|
|
}
|
|
if strings.TrimSpace(req.Goal) == "" {
|
|
return tools.Result{Content: "goal is empty", IsError: true}, nil
|
|
}
|
|
if ctx.Err() != nil {
|
|
return tools.Result{Content: "delegation canceled before start: " + ctx.Err().Error(), IsError: true}, nil
|
|
}
|
|
res, err := t.factory(ctx, req)
|
|
if err != nil {
|
|
return tools.Result{Content: fmt.Sprintf("sub-agent failed: %v", err), IsError: true}, nil
|
|
}
|
|
shown, full := CapParentText(res.Text)
|
|
if full != nil {
|
|
if res.Files == nil {
|
|
res.Files = map[string][]byte{}
|
|
}
|
|
res.Files["subagent-result.txt"] = full
|
|
shown += "\n完整结果见 subagent-result.txt"
|
|
}
|
|
res.Text = shown
|
|
var b strings.Builder
|
|
if res.Success {
|
|
b.WriteString("[sub-agent succeeded]\n")
|
|
} else {
|
|
b.WriteString("[sub-agent failed]\n")
|
|
}
|
|
if res.Err != "" {
|
|
b.WriteString("error: " + res.Err + "\n")
|
|
}
|
|
b.WriteString(res.Text)
|
|
if len(res.Files) > 0 {
|
|
names := make([]string, 0, len(res.Files))
|
|
for name := range res.Files {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
b.WriteString("\nfiles: " + strings.Join(names, ", "))
|
|
}
|
|
return tools.Result{Content: b.String(), IsError: !res.Success}, nil
|
|
}
|
|
|
|
// ParentResultCap 是回传父会话的默认字符上限。
|
|
const ParentResultCap = 32000
|
|
|
|
// CapParentText 超出上限时返回截断文本和全文,由调用方把全文写成工件或文件。
|
|
func CapParentText(text string) (shown string, full []byte) {
|
|
if utf8.RuneCountInString(text) <= ParentResultCap {
|
|
return text, nil
|
|
}
|
|
return string([]rune(text)[:ParentResultCap]), []byte(text)
|
|
}
|