Why: complete agents delegate; the kernel must not nest executions itself (orchestration stays with the host per the agent-execution standard), so the missing piece is a port plus a tool face. Evidence: go vet ./... clean; go test ./... 14 packages ok. Cases: nil factory registers nothing, full delegation field round-trip (goal/inputs/write_scope/non_goals/done_criteria/deliverables) with sorted file list in the result text, sub-agent failure and factory error propagation, schema-required goal rejection, pre-canceled delegations never starting, and adapter end-to-end wiring through Options.Subagent. Impact: new runtime/subagent package (Request/Result/Factory); the delegation message set matches the agent-execution delegation clause (sub-tasks derive no extra permissions; budget fields are host-set); native.Options gains Subagent. Refs: plan B8; standards/agent-execution.md delegation section.
104 lines
3.6 KiB
Go
104 lines
3.6 KiB
Go
// Package subagent 定义委派端口:内核不内置嵌套执行(那是编排层职责),
|
|
// 只提供工具面与信息集契约;宿主(产品 SpawnSubagent、小队引擎等)
|
|
// 注入 Factory 实现真实派生执行。未注入则 delegate_task 不注册(fail-closed)。
|
|
//
|
|
// 信息集对齐 agent-execution 标准的委派条款:目标、输入、写范围、非目标、
|
|
// 完成判据、预算与回传格式——子任务不能从父任务目标推导额外权限。
|
|
package subagent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"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
|
|
}
|
|
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
|
|
}
|