// 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) }