diff --git a/CLAUDE.md b/CLAUDE.md index f9b1dca..0c43131 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,10 +43,11 @@ CORE (majordomo + stdlib): fanout/ programmatic N×M swarm [P0 ✓] deliver/ output egress seam (+ Discard/Stdout) [P0 ✓] identity/ caller identity seams [P0 ✓] - run/ run-loop mechanics (cancel-merge, finalizers, [P2 wip] - RunStateAccessor via RunTally seam, submit, - progress bridge) + RunnableAgent DTO done; - executor merge + nil-safe run.Ports next [P2] + run/ run-loop mechanics + RunnableAgent DTO + [P2 wip] + nil-safe run.Ports (Audit/Budget/Critic/ + Checkpointer/PaletteSource) defined; the + agentexec+skillexec -> run.Executor MERGE + (consuming Ports) is the remaining P2 work [P2] dispatchguard/ loop/depth/fan-out caps [P0 ✓] pendingattach/ attachment dedupe [P0 ✓] tool/ registry + 3-stage permissions + ssrf [P1 ✓] diff --git a/run/ports.go b/run/ports.go new file mode 100644 index 0000000..e18ac74 --- /dev/null +++ b/run/ports.go @@ -0,0 +1,168 @@ +package run + +import ( + "context" + "time" + + "gitea.stevedudenhoeffer.com/steve/majordomo/llm" + + "gitea.stevedudenhoeffer.com/steve/executus/deliver" +) + +// Ports are the host seams the run executor consumes. Every field is nil-safe: +// a light host passes the zero Ports and gets a bounded, in-memory run with no +// persistence, audit, budget, critic, delegation, or delivery — which is +// exactly a gadfly swarm task. A heavy host (mort) wires each one to a battery. +// +// This struct IS the inversion: in mort, agentexec imports agents / +// agentcritic / skillaudit and skillexec imports skills / paste directly; here +// the kernel depends only on these interfaces, and the batteries implement +// them. The mort_*_adapters.go wall becomes the set of impls. +type Ports struct { + // Audit records the run trace (start, per-step/per-tool events, final + // stats). nil = no audit. + Audit Audit + // Budget gates and meters per-caller resource use. nil = unbounded. + Budget Budget + // Critic optionally monitors a long run for hangs/runaways. nil = none. + Critic Critic + // Checkpointer persists resumable progress for durable recovery. nil = no + // checkpointing (a run interrupted by shutdown is simply lost). + Checkpointer Checkpointer + // Palette resolves SkillPalette / SubAgentPalette entries into delegation + // tools (skill__ / agent__). nil = those entries are inert. + Palette PaletteSource + // Delivery is where the run's output + artifacts go. nil = the caller + // reads the Result in-process (the light-host default). + Delivery deliver.Delivery +} + +// RunInfo describes a run at start time — the attribution a recorder/critic +// needs. Host-neutral rename of mort's SkillRun start fields. +type RunInfo struct { + RunID string + SubjectID string // the agent/skill id being run (audit "skill_id") + Name string + CallerID string + ChannelID string + ParentRunID string + Inputs map[string]any + StartedAt time.Time +} + +// RunStats is the terminal roll-up a recorder's Close writes. Mirrors mort's +// skillaudit/skillexec RunStats. +type RunStats struct { + Status string // ok | error | timeout | budget_exceeded | cancelled | dry_run + Output string + Error string + ToolCalls int + RuntimeSeconds float64 + InputTokens int64 + OutputTokens int64 + ThinkingTokens int64 +} + +// --- Audit --- + +// Audit begins recording a run. StartRun returns a per-run RunRecorder (or nil +// to skip recording this run). The audit battery wires its Storage behind this. +type Audit interface { + StartRun(ctx context.Context, info RunInfo) RunRecorder +} + +// RunRecorder records the events of one in-flight run and its final stats. It +// satisfies RunTally so the kernel can surface live token/tool counts to the +// self-status tool. Mirrors mort's skillaudit.Writer. +type RunRecorder interface { + RunTally + // OnStep records one completed agent-loop iteration's model response. + OnStep(iter int, resp *llm.Response) + // OnTool records one executed tool call + its result. + OnTool(call llm.ToolCall, result string) + // LogEvent / LogError append structured events to the run log. + LogEvent(eventType string, payload map[string]any) + LogError(msg string) + // Close writes the terminal roll-up. Detaches from the caller's context + // internally so a cancelled run still records. + Close(ctx context.Context, stats RunStats) +} + +// --- Budget --- + +// Budget gates and meters per-caller resource use. Mirrors mort's +// skillexec.BudgetTracker. +type Budget interface { + // Check reports whether the caller has remaining budget (nil = allowed). + Check(ctx context.Context, callerID string) error + // Commit records that the caller spent runtimeSeconds on this run. + Commit(ctx context.Context, callerID string, runtimeSeconds float64) +} + +// --- Critic --- + +// Critic optionally monitors a long-running run (the two-tier soft/hard +// timeout). Monitor returns a handle the executor feeds progress into and +// queries for steer/deadline decisions; a nil handle means "not monitored". +// +// The exact wiring (how the handle's Steer/Deadline bind into majordomo's +// agent.WithSteer / agent.WithMaxStepsFunc / run-context cancellation) is +// finalized in the executor; this is the seam the agentcritic battery adapts. +type Critic interface { + Monitor(ctx context.Context, info RunInfo, softTimeout time.Duration) CriticHandle +} + +// CriticHandle is the executor's live link to a run's critic. +type CriticHandle interface { + // RecordStep / RecordToolStart keep the critic's activity clock fresh so a + // healthy-but-slow run is not mistaken for a hang. + RecordStep(iter int) + RecordToolStart(name, args string) + // Steer returns any messages the critic wants injected into the loop (a + // nudge), drained before each step — matches majordomo agent.WithSteer. + Steer() []llm.Message + // Deadline returns the current hard-kill deadline (the critic may extend + // it); the executor binds the run context to it. Zero = no hard deadline. + Deadline() time.Time + // Stop ends monitoring when the run finishes. + Stop() +} + +// --- Checkpointer --- + +// Checkpointer persists a run's resumable progress for durable recovery. +// Mirrors mort's agentexec.RunCheckpointer. +type Checkpointer interface { + // Save persists the run's current resumable progress (throttled). + Save(ctx context.Context, st RunCheckpointState) error + // Complete clears the checkpoint on success. + Complete(ctx context.Context) error + // Fail clears the checkpoint on terminal failure. A run interrupted by + // shutdown is left untouched so boot recovery picks it up. + Fail(ctx context.Context, err error) error +} + +// RunCheckpointState is the resumable snapshot a Checkpointer persists. Kept +// minimal here; the executor extends what it records during the merge. +type RunCheckpointState struct { + Messages []llm.Message + Iteration int +} + +// --- PaletteSource --- + +// PaletteSource resolves a RunnableAgent's SkillPalette / SubAgentPalette names +// into delegation tools and invokes them. Mirrors mort's +// SkillInvokerForPalette + AgentInvokerForPalette. nil Palette => palette +// entries are inert ("not configured" at first call). +type PaletteSource interface { + ResolveSkill(ctx context.Context, callerID, name string) (skillID string, err error) + InvokeSkill(ctx context.Context, callerID, channelID, name string, + inputs map[string]any, parentRunID string) (output, runID, status string, err error) + + ResolveAgent(ctx context.Context, callerID, name string) (agentID string, err error) + InvokeAgent(ctx context.Context, callerID, channelID, name string, + prompt, parentRunID, modelTierOverride, promptPrepend string, + toolsSubset []string, + onEvent func(ctx context.Context, event, emoji string)) (output, runID, status string, err error) +}