···11+package agent
22+33+import (
44+ "context"
55+ "encoding/json"
66+ "fmt"
77+ "strings"
88+99+ "github.com/quailyquaily/mistermorph/tools"
1010+)
1111+1212+const spawnToolName = "spawn"
1313+1414+type spawnTool struct {
1515+ engine *Engine
1616+}
1717+1818+func (t *spawnTool) Name() string { return spawnToolName }
1919+2020+func (t *spawnTool) Description() string {
2121+ return "Spawn a sub-agent to handle a self-contained sub-task. " +
2222+ "The sub-agent runs with its own context and a restricted set of tools you specify. " +
2323+ "This call blocks until the sub-agent completes and returns its final output. " +
2424+ "Use this to parallelise independent work items."
2525+}
2626+2727+func (t *spawnTool) ParameterSchema() string {
2828+ s := map[string]any{
2929+ "type": "object",
3030+ "properties": map[string]any{
3131+ "task": map[string]any{
3232+ "type": "string",
3333+ "description": "Task prompt for the sub-agent.",
3434+ },
3535+ "tools": map[string]any{
3636+ "type": "array",
3737+ "items": map[string]any{"type": "string"},
3838+ "description": "Whitelist of tool names the sub-agent can use. Cannot include 'spawn'.",
3939+ },
4040+ "model": map[string]any{
4141+ "type": "string",
4242+ "description": "Optional model override for the sub-agent. Defaults to the parent's model.",
4343+ },
4444+ },
4545+ "required": []string{"task", "tools"},
4646+ }
4747+ b, _ := json.MarshalIndent(s, "", " ")
4848+ return string(b)
4949+}
5050+5151+func (t *spawnTool) Execute(ctx context.Context, params map[string]any) (string, error) {
5252+ task, _ := params["task"].(string)
5353+ task = strings.TrimSpace(task)
5454+ if task == "" {
5555+ return "", fmt.Errorf("missing required param: task")
5656+ }
5757+5858+ rawTools, _ := params["tools"].([]any)
5959+ if len(rawTools) == 0 {
6060+ return "", fmt.Errorf("missing required param: tools (must be a non-empty array of tool names)")
6161+ }
6262+6363+ subRegistry := tools.NewRegistry()
6464+ for _, raw := range rawTools {
6565+ name, ok := raw.(string)
6666+ if !ok {
6767+ continue
6868+ }
6969+ name = strings.TrimSpace(name)
7070+ if name == "" || strings.EqualFold(name, spawnToolName) {
7171+ continue
7272+ }
7373+ if tool, found := t.engine.registry.Get(name); found {
7474+ subRegistry.Register(tool)
7575+ }
7676+ }
7777+ if len(subRegistry.All()) == 0 {
7878+ return "", fmt.Errorf("none of the requested tools are available in the parent registry")
7979+ }
8080+8181+ model, _ := params["model"].(string)
8282+ model = strings.TrimSpace(model)
8383+ if model == "" {
8484+ model = strings.TrimSpace(t.engine.config.DefaultModel)
8585+ }
8686+8787+ client := t.engine.client
8888+ var cleanup func()
8989+ if t.engine.subClientFactory != nil {
9090+ client, cleanup = t.engine.subClientFactory("spawn")
9191+ }
9292+ if cleanup != nil {
9393+ defer cleanup()
9494+ }
9595+9696+ subOpts := []Option{WithLogger(t.engine.log)}
9797+ if t.engine.guard != nil {
9898+ subOpts = append(subOpts, WithGuard(t.engine.guard))
9999+ }
100100+101101+ subEngine := New(client, subRegistry, Config{
102102+ MaxSteps: t.engine.config.MaxSteps,
103103+ MaxTokenBudget: t.engine.config.MaxTokenBudget,
104104+ ParseRetries: t.engine.config.ParseRetries,
105105+ ToolRepeatLimit: t.engine.config.ToolRepeatLimit,
106106+ DefaultModel: model,
107107+ ToolCallTimeout: t.engine.config.ToolCallTimeout,
108108+ }, t.engine.spec, subOpts...)
109109+110110+ final, _, err := subEngine.Run(ctx, task, RunOptions{Model: model})
111111+ if err != nil {
112112+ return "", fmt.Errorf("sub-agent failed: %w", err)
113113+ }
114114+ if final == nil {
115115+ return "{}", nil
116116+ }
117117+118118+ b, err := json.Marshal(final.Output)
119119+ if err != nil {
120120+ return fmt.Sprintf("%v", final.Output), nil
121121+ }
122122+ return string(b), nil
123123+}
+2
assets/config/config.example.yaml
···482482max_token_budget: 0
483483# - tool_repeat_limit: maximum executions per tool name within one run.
484484tool_repeat_limit: 3
485485+# - spawn_enabled: enable the spawn tool to start sub-agents (default false).
486486+spawn_enabled: false
485487# Overall run timeout.
486488timeout: "10m"
487489# Base directory for local state (memory/skills/heartbeat).