package run import ( "context" "fmt" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" "gitea.stevedudenhoeffer.com/steve/executus/tool" ) // addDelegationTools adds a delegation tool to the toolbox for each // SkillPalette / SubAgentPalette entry, backed by the PaletteSource: // // - skill__ invokes the named saved skill with structured inputs. // - agent__ invokes the named sub-agent with a prompt. // // Each delegated call runs as a CHILD of the current run (parentRunID = // inv.RunID), inheriting the caller + channel. No-op when palette is nil or both // palettes are empty — so an agent with no palette (or a host with no // PaletteSource) simply has no delegation tools, exactly as before. func addDelegationTools(box *llm.Toolbox, ra RunnableAgent, inv tool.Invocation, palette PaletteSource) error { if palette == nil { return nil } seen := map[string]bool{} // dedupe across both palettes by final tool name for _, name := range ra.SkillPalette { name := name // capture toolName := "skill__" + name if name == "" || seen[toolName] { // skip empty / duplicate palette entries continue } seen[toolName] = true t := llm.DefineTool( toolName, fmt.Sprintf("Delegate the task to the %q skill. Provide its declared inputs.", name), func(ctx context.Context, args skillDelegateArgs) (any, error) { out, _, status, err := palette.InvokeSkill(ctx, inv.CallerID, inv.ChannelID, name, args.Inputs, inv.RunID) if err != nil { return nil, delegationErr("skill", name, out, err) } return delegationResult(name, "skill", out, status), nil }, ) if err := box.Add(t); err != nil { return fmt.Errorf("add %s: %w", toolName, err) } } for _, name := range ra.SubAgentPalette { name := name // capture toolName := "agent__" + name if name == "" || seen[toolName] { continue } seen[toolName] = true t := llm.DefineTool( toolName, fmt.Sprintf("Delegate the task to the %q sub-agent with a natural-language prompt.", name), func(ctx context.Context, args agentDelegateArgs) (any, error) { out, _, status, err := palette.InvokeAgent(ctx, inv.CallerID, inv.ChannelID, name, args.Prompt, inv.RunID, "", "", nil, nil) if err != nil { return nil, delegationErr("agent", name, out, err) } return delegationResult(name, "agent", out, status), nil }, ) if err := box.Add(t); err != nil { return fmt.Errorf("add %s: %w", toolName, err) } } return nil } // delegationResult surfaces a non-ok child status to the parent agent (so it can // react to a timeout/cancel/budget stop) while still passing the partial output. func delegationResult(name, kind, out, status string) string { if status != "" && status != "ok" { header := fmt.Sprintf("[%s %q ended with status %q]", kind, name, status) if out == "" { // no trailing blank line when there's no body return header } return header + "\n" + out } return out } // delegationErr wraps a hard delegation failure, folding in any partial output // the child produced before failing (so it isn't silently lost). func delegationErr(kind, name, partial string, err error) error { if partial != "" { return fmt.Errorf("%s %q failed (partial output: %q): %w", kind, name, partial, err) } return fmt.Errorf("%s %q failed: %w", kind, name, err) } type skillDelegateArgs struct { Inputs map[string]any `json:"inputs" description:"Inputs for the skill, matching its declared input schema."` } type agentDelegateArgs struct { Prompt string `json:"prompt" description:"The task/prompt to hand the sub-agent."` }