Two gaps surfaced by a live fan-out smoke in a downstream host (mort):
1) A single-loop run that exhausts its step budget (ErrMaxSteps / ErrToolLoop)
returned empty Output + a hard error, discarding all the reasoning it had
produced — unlike the phased path, which already salvages a partial
transcript and continues. Add opt-in single-loop salvage
(Defaults.SalvageSingleLoopMaxSteps, default off): on budget exhaustion with
empty Output, reconstruct a best-effort answer from the step narration
(reusing salvagePhaseTranscript) and downgrade the run to a successful
partial result. Off by default so a structured-output host keeps its clean
hard error; an interactive host opts in.
2) RunnableAgent carried no tool-call ceiling — MaxIterations bounds STEPS, but
one step can dispatch several tool calls, so a host's "max tool calls" cap
had nowhere to land. Add RunnableAgent.MaxToolCalls (<=0 = unlimited,
backward-compatible): the step observer counts executed calls and, once the
cap is reached, stepCeilingOption forces the loop to exit; the resulting
ErrMaxSteps is relabeled to the new ErrMaxToolCalls sentinel. Enforced
single-loop only for now (phased is a follow-up). A cap hit is budget
exhaustion, so salvage recovers its partial reasoning too.
isPhaseBudgetExhaustion becomes the shared isBudgetExhaustion (now also matching
ErrMaxToolCalls) so the phased and single-loop paths never drift, and the
RunStateAccessor now reports the real tool-call cap (was hardcoded 0).
Additive + backward-compatible: new optional DTO field, new opt-in Defaults
flag, new exported sentinel; unlimited/off reproduce prior behavior exactly. No
new dependencies (go mod tidy clean). Tests cover salvage on/off/no-prose, cap
enforcement + sentinel, cap+salvage, unlimited no-op, and cap-overrides-critic.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q1eJh5NVR11z6RzyxLANMK
Lifts the 'an agent uses a SKILL.md pack' concept out of a host and into the
harness:
- run.Ports.SkillPacks (SkillPackActivator) — nil-safe port; the executor folds
a loaded agent's pack catalog into the system prompt and adds a skill_use
loader tool to the toolbox (uses the existing ra.SystemPrompt + toolbox seams)
- run.RunnableAgent.SkillPacks + persona.Agent.SkillPacks (+ skill_packs YAML,
extends-inherit, ToRunnable) — the Agent noun is now pack-aware
- skillpack.Activator — the battery's default port impl (resolve names → packs →
catalog + skill_use), with a per-run BundleStager factory the host plumbs;
satisfies the port structurally (no import of run)
- agentbuiltins: ships gifsmith, a portable focused GIF/MP4 render agent that
uses the gif pack — references tool/tier/pack NAMES only, no host coupling
A host now wires run.Ports.SkillPacks instead of carrying its own activation
glue. Tests: Activator resolution + gifsmith loads through persona→RunnableAgent.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The kernel carried RunnableAgent.Phases as a DTO but never executed it —
Run always ran a single agent loop with ra.SystemPrompt, so a phased agent
(mort's deepresearch/research) silently ran one loop with the base prompt
instead of its pipeline. This implements the phase loop, ported from mort's
agentexec pipeline but reusing the kernel's own machinery.
- run/phases.go: runPhases / runOnePhase. Phases run sequentially; each is a
fresh agent loop (or a bare LLM call for IsRunFunc phases) with its own
template-expanded system prompt ({{.Query}} + {{.<PhaseName>}}), model
tier, step cap, and tool subset. Outputs thread into later phases; the
final phase's output is the run output. Optional phases swallow errors and
substitute FallbackMessage; a non-optional phase that merely exhausts its
step/tool budget salvages its partial transcript and continues (a hard
error still aborts); per-phase tier-resolve failures fall back with a WARN.
- run/agent.go: Phase gains IsRunFunc + FallbackMessage (the kernel Phase
struct previously omitted them).
- run/executor.go: Run factors the shared agent options (tool-error limits,
step observer, compactor) and branches — single loop (critic's dynamic
step ceiling) vs the phase runner (fixed per-phase caps; the run-level
critic's steer + hard deadline still apply across phases). systemPrompt
now delegates to systemPromptWithBody so each phase keeps the platform
header. The same step observer feeds audit/steps/critic across all phases.
Tests (run/phases_test.go): sequential output threading + template
expansion, Optional-failure → FallbackMessage continues, hard-error abort,
IsRunFunc bare call, per-phase SystemHeader, filterToolbox subset, template
expansion. Full ./... suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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) <[email protected]>