0c80679719
From the PR #8 review (all graded in the gadfly MCP): - skip empty palette names + dedupe by final tool name, instead of producing a "skill__" tool or an opaque box.Add duplicate error. - delegationResult: no trailing blank line when a non-ok child produced no output. - delegationErr: fold a child's partial output into the hard-failure error so it isn't silently dropped. Deferred to C0b (design-level, not trivial): route delegation through the tool.Registry gate/audit wrappers; expose the skill's real input schema to the LLM instead of a generic inputs map. typed-nil PaletteSource is left as a caller contract (the == nil guard catches the untyped-nil interface). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
3.5 KiB
Go
103 lines
3.5 KiB
Go
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__<name> invokes the named saved skill with structured inputs.
|
|
// - agent__<name> 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."`
|
|
}
|