aab950f1c3
Stand up the executus/run kernel foundation, decoupled from mort: - runengine.go: the shared run-loop scaffolding (MergeCancellation, CleanupContextTimeout, RunFinalizer/FireFinalizers, RunStateAccessor) moved from mort. The accessor's *skillaudit.Writer dependency is inverted to a narrow run.RunTally interface (TokenStats + ToolCallsCount) — the kernel reads live tallies without importing the audit battery. - submit.go: the legacy submit-capture compat tool (stdlib + majordomo/llm). - agent.go: RunnableAgent DTO — the kernel's view of "a thing to run" (tier, prompt, caps, palette, phases, critic config). The persona Agent and saved Skill will LOWER into this DTO so the kernel never imports a noun battery. This is the spine of the agentexec.Run(*agents.Agent) inversion. run/ builds with only majordomo + executus/tool. The executor merge (agentexec+skillexec -> run.Executor) and the nil-safe run.Ports (Audit/Critic/Budget/Checkpointer/PaletteSource) are the next P2 block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
3.1 KiB
Go
85 lines
3.1 KiB
Go
package run
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
|
|
llm "gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
|
)
|
|
|
|
// SubmitCapture records the output a run's `submit` tool received.
|
|
//
|
|
// Why this exists: legacy agentkit injected a synthetic `submit` tool and
|
|
// ended the loop when it fired; years of mort system prompts (agent
|
|
// YAMLs, skill manifests, the executors' platform headers) teach the
|
|
// model to "call submit with your final answer". majordomo's agent loop
|
|
// has no submit concept — it ends when the model replies WITHOUT tool
|
|
// calls. Dropping submit cold would make every prompt-trained model
|
|
// burn turns on "unknown tool \"submit\"" errors.
|
|
//
|
|
// The compatibility shape: the executors add NewSubmitTool's tool to
|
|
// every run's toolset (unless the palette already defines a `submit`).
|
|
// The handler records the FIRST submitted answer and tells the model
|
|
// the answer was accepted so its next turn is a bare reply (which ends
|
|
// the loop naturally). After the run, the executor consults
|
|
// Output(loopOutput, runErr): a captured submission wins over an empty
|
|
// or budget-exhausted ending, so a model that submits on its final
|
|
// allowed step still produces its answer instead of ErrMaxSteps.
|
|
type SubmitCapture struct {
|
|
mu sync.Mutex
|
|
output string
|
|
called bool
|
|
}
|
|
|
|
// Record stores the first submitted answer; later calls are ignored
|
|
// (matching legacy agentkit's "multiple calls keep the first" contract).
|
|
func (c *SubmitCapture) Record(output string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.called {
|
|
return
|
|
}
|
|
c.called = true
|
|
c.output = output
|
|
}
|
|
|
|
// Submitted returns the captured answer and whether submit fired.
|
|
func (c *SubmitCapture) Submitted() (string, bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.output, c.called
|
|
}
|
|
|
|
// Output resolves the run's final output: the submitted answer when the
|
|
// model called submit (parity with legacy agentkit, where submit's argument
|
|
// WAS the run output), otherwise the loop's own final text. resolvedErr
|
|
// is nil when a submission exists — a run that submitted its answer and
|
|
// then ran out of steps (or timed out composing the courtesy
|
|
// confirmation turn) is a SUCCESS, not an error.
|
|
func (c *SubmitCapture) Output(loopOutput string, runErr error) (output string, resolvedErr error) {
|
|
if out, ok := c.Submitted(); ok {
|
|
return out, nil
|
|
}
|
|
return loopOutput, runErr
|
|
}
|
|
|
|
// submitArgs mirrors legacy agentkit's synthetic submit tool schema so
|
|
// models prompted under the old contract emit compatible calls.
|
|
type submitArgs struct {
|
|
Output string `json:"output" description:"The final answer, summary, or output for this task."`
|
|
}
|
|
|
|
// NewSubmitTool builds the compatibility `submit` tool bound to the
|
|
// given capture. Both executors (skill + agent) install one per run.
|
|
func NewSubmitTool(capture *SubmitCapture) llm.Tool {
|
|
return llm.DefineTool[submitArgs](
|
|
"submit",
|
|
"Submit your final answer or output to end this task. Call exactly once when you are done.",
|
|
func(_ context.Context, args submitArgs) (any, error) {
|
|
capture.Record(strings.TrimSpace(args.Output))
|
|
return "Final answer recorded. Do not call any more tools; reply now with a brief closing message.", nil
|
|
},
|
|
)
|
|
}
|