Why: the host factory decides sync vs background spawning; the request must carry the declaration (AgentSpec.Background consumption point, mirroring the product delegate tool's background parameter). Evidence: GOWORK=off go vet + go test green; the delegate_task schema gains the background property. Impact: none for hosts that do not set the field. Refs: Agent-System-Documents#42 (N2 plan WP5)
150 lines
5.2 KiB
Go
150 lines
5.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/hooks"
|
||
"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:"-"`
|
||
// Background 为真时宿主以后台方式派生(语义同产品 delegate 的
|
||
// background 参数;AgentSpec.Background 声明经宿主写入本字段消费)。
|
||
Background bool `json:"background,omitempty"`
|
||
}
|
||
|
||
// 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
|
||
bus *hooks.Bus
|
||
}
|
||
|
||
// Register 把 delegate_task 工具注册进注册表(factory 为 nil 时不注册)。
|
||
func Register(r *tools.Registry, factory Factory) {
|
||
RegisterWithHooks(r, factory, nil)
|
||
}
|
||
|
||
// RegisterWithHooks 同 Register;bus 非空时,每次委派执行前后分别发出
|
||
// SubagentStart / SubagentStop 事件(Start 带目标摘要,Stop 带成败标记)。
|
||
func RegisterWithHooks(r *tools.Registry, factory Factory, bus *hooks.Bus) {
|
||
if factory == nil {
|
||
return
|
||
}
|
||
r.Register(&delegateTool{factory: factory, bus: bus})
|
||
}
|
||
|
||
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"}},
|
||
"background":{"type":"boolean"}
|
||
}}`)
|
||
}
|
||
|
||
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
|
||
}
|
||
if t.bus != nil {
|
||
goal := req.Goal
|
||
if len(goal) > 200 {
|
||
goal = goal[:200] + "…"
|
||
}
|
||
t.bus.Emit(hooks.Call{Event: hooks.SubagentStart, Tool: "delegate_task", LastText: goal})
|
||
}
|
||
res, err := t.factory(ctx, req)
|
||
if t.bus != nil {
|
||
failed := err != nil || !res.Success
|
||
t.bus.Emit(hooks.Call{Event: hooks.SubagentStop, Tool: "delegate_task", IsError: failed})
|
||
}
|
||
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)
|
||
}
|