Files
executus/run/palette.go
T
steve 9d41987b0e
executus CI / test (pull_request) Failing after 1m2s
Adversarial Review (Gadfly) / review (pull_request) Successful in 3m47s
C0: wire Palette delegation into run.Executor (skill__/agent__ tools)
The first cutover prerequisite: the executor now turns an agent's SkillPalette /
SubAgentPalette into delegation tools so a mort agent that delegates works
through run.Executor (the piece the `.agent run` canary needs beyond the
already-wired audit/budget).

- run/palette.go: addDelegationTools builds a skill__<name> tool (structured
  inputs) per SkillPalette entry and an agent__<name> tool (prompt) per
  SubAgentPalette entry, each invoking run.Ports.Palette as a CHILD of the
  current run (parentRunID = inv.RunID, inheriting caller + channel). A non-ok
  child status is surfaced to the parent with the partial output. nil-safe: no
  PaletteSource or empty palette → no delegation tools (unchanged behavior).
- executor.go: call it right after building the low-level toolbox.

Tests: the model calls skill__helper → routed through Palette with the right
name/caller/inputs/parent; nil palette → run still works.

Deferred to C0b (the remaining run.Ports executor wiring): Critic (soft-timeout
monitor + deadline binding + steer), Delivery (output egress for surfaces that
need executor-side delivery), Checkpointer (needs a majordomo message-history
hook to snapshot resumable state). The `.agent run` canary delivers its returned
Result.Output itself, so these aren't on its critical path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 09:28:01 -04:00

79 lines
2.7 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
}
for _, name := range ra.SkillPalette {
name := name // capture
t := llm.DefineTool(
"skill__"+name,
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, fmt.Errorf("skill %q failed: %w", name, err)
}
return delegationResult(name, "skill", out, status), nil
},
)
if err := box.Add(t); err != nil {
return fmt.Errorf("add skill__%s: %w", name, err)
}
}
for _, name := range ra.SubAgentPalette {
name := name // capture
t := llm.DefineTool(
"agent__"+name,
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, fmt.Errorf("agent %q failed: %w", name, err)
}
return delegationResult(name, "agent", out, status), nil
},
)
if err := box.Add(t); err != nil {
return fmt.Errorf("add agent__%s: %w", name, 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" {
return fmt.Sprintf("[%s %q ended with status %q]\n%s", kind, name, status, out)
}
return out
}
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."`
}