Phase 7: nine runnable examples/ programs (parse, failover chains with trailing alias, tiers, LLM_* env providers, multimodal, tool loop, Generate[T], agent, skills); docs/mort-migration.md mapping mort's go-llm/go-agentkit usage onto majordomo APIs with the planned additive library extensions and conversion order; README finalized with the complete matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.2 KiB
mort → majordomo migration blueprint
The plan for converting mort off go-llm/go-agentkit and onto majordomo,
executed as the final phase of the build run. Inventory source: a full
sweep of mort's 160 importing files (2026-06-10).
Mandate (Steve, mid-run): this is not a 1:1 port. mort's agent / LLM /
tool code may be rewritten wholesale into a properly designed, extensible
system — the constraint is user-facing behavior: .query still does quick
research, .research / .deepresearch keep their roles, chatbot and
general agents keep broadly the same functionality.
Layering: what moves, what stays
mort features (.query, chatbot, cookbook, scaddy, ...) — stay, re-wired
mort orchestration (lanes, run-critic, budgets, delivery,
encryption, compaction policy, tracing/usage recording) — stay, re-based
go-llm + go-agentkit — DELETED
majordomo (Parse/tiers/chains/health, providers, media,
tools, agents, skills) — the new floor
mort-side systems survive because majordomo's seams are public: providers
and models are small interfaces (lane wrapping = an llm.Provider
decorator; the foreman 30-minute timeout = a Model decorator), the agent
loop exposes observers, and the registry accepts any provider.
Core mappings
| mort today (go-llm / agentkit) | majordomo |
|---|---|
llm.Parse(spec) / llms.ParseModelRequest |
Registry.Parse (same grammar; chains, aliases, LLM_* lazily) |
tier convars + llm.RegisterResolver |
RegisterResolver (library addition №1) backed by the same convar reads |
legacy aliases (sonnet, haiku, ...) |
RegisterAlias table, unchanged strings |
| comma failover chains + bench-on-failure | chain executor + health tracker (ADR-0006/0008) |
llm.SetFailoverDefaults/Observer, failoverlog |
WithHealthConfig/WithChainConfig + ChainConfig.Observer (addition №7) feeding the same llm_failover_events store |
.failover bench/unbench/list |
health.Tracker Bench/Unbench/Snapshot (addition №6) |
llm.Client + provider constructors + OPENAI_KEY etc. |
built-ins + env keys; mort's nonstandard OPENAI_KEY/GOOGLE_GEMINI_API_KEY bridged via WithAPIKey at wiring |
| deepseek/moonshot/xai/groq clients | openai.New(WithName(...), WithBaseURL(...)) compat presets registered at boot |
LLM_M1=foreman://... env DSN |
identical (ADR-0004 parity); foreman timeout via a Model-wrapping decorator |
llm.Define[Args] / DefineSimple (60+ sites) |
DefineTool[Args] (addition №2, schema via SchemaFor) |
llm.NewToolBox / ToolBox.Execute/AllTools |
llm.NewToolbox / Execute / Tools |
llm.Message/UserMessage/SystemMessage/UserMessageWithImages |
Message constructors + UserParts(Text, Image...) |
llm.Image{Base64/URL} |
ImagePart (bytes; URL images fetched by mort's imageutil at the edge — it already downloads Discord attachments) |
model.Complete(ctx, req) |
Model.Generate |
llms.GenerateWith[T] (hidden tool trick) |
majordomo.Generate[T] (native structured output; flexBool-style tolerance stays mort-side in the few schemas that need it) |
llm.WithPromptCaching/WithMaxTokens/WithTemperature |
request options; prompt caching = addition №4 (Anthropic cache_control) |
reasoning :high suffixes in specs |
stripped by mort's spec layer → WithReasoningEffort (ADR-0003) |
| usage detail keys (cached/reasoning tokens) | Usage detail fields (addition №3) |
agentkit.Agent{...}.Run |
agent.New(...).Run |
Observer/FuncObserver/MultiObserver |
WithStepObserver / OnStep (multiple registrations compose) |
MaxIter / MaxIterFunc (critic adjust_budget) |
WithMaxSteps / WithMaxStepsFunc (addition №5a) |
Steer mailbox (critic nudges) |
WithSteer per-step message injection (addition №5b) |
ContextCompactor |
WithCompactor pre-step hook (addition №5c); mort keeps its summarize-the-middle policy |
MaxConsecutiveToolErrors / MaxSameToolCallRepeats |
loop guards (addition №5d) |
ErrMaxIterExhausted |
agent.ErrMaxSteps |
SessionTools{Tools,PostRun,Cleanup} / AttachImages |
mort-side session-tool layer over AddToolbox + run history; artifacts/delivery were always mort code |
AgentDefinition/PhaseDefinition/RunPipeline |
mort-side phase runner: sequential agent.Run calls with per-phase model/tools/system + template vars (mort's executor owns this today anyway) |
skill manifests (skills/*/skill.yml) |
unchanged YAML → skill.New(name, WithInstructions(sys), WithToolbox(...)) + mort's executor bounds |
llm.Transcription types |
move into pkg/logic/transcribe (out of majordomo's scope) |
lane wrapping of provider.Provider |
laneProvider implementing llm.Provider, registered via RegisterProvider — no fork needed (this was inventory pain point №6) |
Library additions planned during conversion
Each is additive, general-purpose, and lands in majordomo main with tests and an ADR before mort consumes it:
Registry.RegisterResolver(Resolver)— dynamic alias resolution (DB-backed tiers); checked after static aliases during expansion, output re-expanded recursively with the same cycle guard.DefineTool[Args](name, desc, fn)— typed tools: schema fromSchemaFor[Args], arguments unmarshaled before the handler runs.Usage{CacheReadTokens, CacheWriteTokens, ReasoningTokens}populated by the providers that report them (Anthropic, OpenAI, Google).WithPromptCaching()→ Anthropiccache_control(system + last turn); no-op elsewhere.- Agent loop hooks: (a)
WithMaxStepsFunc, (b)WithSteer, (c)WithCompactor, (d)WithToolErrorLimits. health.Tracker.Bench(key, until)/Unbench(key)/Snapshot()for the.failoveradmin surface and web UI.ChainConfig.Observer— one callback per failover decision (attempt, classification, bench) feeding mort's event log.
Conversion order (Phase 9 execution plan)
- Library additions above (majordomo main, gated, committed).
pkg/logic/llmsrebuild — the choke point. One mort package that owns: amajordomo.Registrywith mort's env keys, compat providers, lane-wrapped registration, the convar-backed tier resolver, legacy aliases, the foreman timeout decorator, health observer wiring, and theParseModelRequest/ParseModelForContext/GenerateWith/CallAndExecute/ SimpleCallmort-facing API (now thin wrappers over majordomo). The reasoning-suffix dialect is stripped here.- Tool layer —
pkg/skilltoolsregistry keeps its gating/audit design;gated_tool.gore-basesNewGatedTool[Args]onDefineTool[Args]; ~96 tools recompile againstllm.Tool. - Executors —
skillexec+agentexecre-based onagent.Agent(loop guards, compactor, steer, observers from the additions). The no-tools direct path falls out naturally (empty toolset sends no tools array). Phased agents = mort-side sequential runner. - Call sites — chatbot, cookbook, recipe, summary, standingquery,
tasks, fitness, drawbot, lottery, reminder, epon, scaddy, clipper,
transcribe: mechanical re-pointing to the new
llmswrappers and canonical types. - Delete go-llm + go-agentkit from go.mod; repo-wide grep must be
clean;
go build ./...+ tests green. - Branch, push, PR (never touching mort's
main).
Behavioral deltas to verify after conversion
- Default chain knobs differ (go-llm: 3 retries/5m flat cooldown/jitter;
majordomo: 1 retry/threshold 2/5s→5m exponential). mort's convars
(
llms.failover.max_retries,llms.failover.cooldown_seconds) map ontoChainConfig.TransientRetries+health.Configso operators keep their dials; semantics ADR-0006 apply. - go-llm benched-everything chains ignore cooldowns; majordomo returns the joined exhaustion error. mort's executor already retries failed runs.
ErrRequestSpecific(400/413/422) failed over WITHOUT benching in go-llm; majordomo fails fast by default — mort setsAdvanceOnPermanent: trueto preserve availability behavior.- Image URLs: go-llm providers fetched
llm.Image{URL}; majordomo is bytes-only —imageutil.FromTextURLsnow downloads (it already has the HTTP plumbing and the 50MB cap).