Files
Agent-Native/runtime/subagent/subagent.go
T
weichen 948ca36ca2
ci / vet-and-test (pull_request) Successful in 47s
ci / docker-checkout-hostgateway (pull_request) Successful in 0s
Add AgentSpec, three guardrail ports, subagent events, and ask passthrough
Why: N1 increment three (WP1/WP4) - agents need a canonical spec
format with plugin restrictions, hosts need typed input/output/tool
guardrails, delegation needs lifecycle events, and squad adapters
need to pass the permission ask port through.

Evidence: go vet clean; go test ./... 26 packages ok, including new
agentspec (12 tests), guardrails (5), loop_guardrails (5), and
subagent hook tests (3).

Impact: new runtime/agentspec and runtime/guardrails packages; loop
consumes UserPromptSubmit outcomes (deny skips the loop, modify
appends the rewrite); subagent gains RegisterWithHooks emitting
SubagentStart/Stop around delegation; GateOptions gains PermissionAsk
forwarded into the kernel config. All additions are opt-in: nil
ports preserve prior behavior exactly.

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md WP1/WP4
2026-10-07 05:04:58 +00:00

146 lines
4.9 KiB
Go
Raw 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"`
// 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
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"}}
}}`)
}
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)
}