Files
Agent-Native/runtime/subagent/subagent.go
weichen cdbdeb0b14
ci / vet-and-test (pull_request) Successful in 51s
ci / docker-checkout-hostgateway (pull_request) Successful in 2s
Carry output format and tool surface on delegation requests (Q3 WP3)
Why: the delegation task brief must state goal, output format, tools and
boundaries (Q3 acceptance); Request had no carriers for the two new
sections, so delegate_task callers could not declare them.

Evidence: TestDelegateToolRoundTrip extended to assert output_format and
tools survive the tool-call round trip; runtime suite green (SELF); CI
authoritative (GATE).

Impact: additive Request fields + delegate_task schema/description; empty
fields keep today's behavior (task brief sections omitted).

Refs: Q3 plan WP3; Dev card .task/feature-agent-stack-q3.md
2026-10-08 03:27:09 +00:00

159 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"`
// OutputFormat 交付物的结构/格式要求(Q3 WP3:委派模板四要素之一,
// 任务书「输出格式」节;空 = 由子代理自行决定)。
OutputFormat string `json:"output_format,omitempty"`
// Tools 允许子代理使用的工具面(Q3 WP3:任务书「工具面」节;空 = 沿用
// 子代理画像默认。宿主可按此收窄,不得用于扩权——子代理权限恒不超过
// 委派方)。
Tools []string `json:"tools,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, output_format (structure/format the deliverables must follow), tools (tool names the sub-agent should use). 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"}},
"output_format":{"type":"string"},
"tools":{"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)
}