Configuration

September 4, 2026 · View on GitHub

Every configuration field for creatures, terrariums, LLM profiles, MCP servers, and package manifests. File formats: YAML (preferred), JSON, TOML. All files support ${VAR} / ${VAR:default} env-var interpolation, applied at load time.

For the model of how creatures and terrariums relate, see concepts/boundaries. For hands-on examples, see guides/configuration and guides/creatures.

Path resolution

Config fields referring to other files or packages resolve in this order:

  1. @<pkg>/<path-inside-pkg>~/.kohakuterrarium/packages/<pkg>/<path-inside-pkg> (following <pkg>.link for editable installs).
  2. creatures/<name> or similar project-relative forms → walk up from the current agent folder to the project root.
  3. Otherwise relative to the agent folder (falling back to the base-config folder when inherited).

Creature config (config.yaml)

Loaded by kohakuterrarium.core.config.load_agent_config. File lookup order: config.yamlconfig.ymlconfig.jsonconfig.toml.

Top-level fields

FieldTypeDefaultRequiredDescription
namestr(none)yesCreature name. Default session key if session_key unset.
versionstr"1.0"noInformational.
base_configstrnullnoParent config to inherit from (@package/path, creatures/<name>, or relative).
controllerdict{}noLLM/controller block. See Controller.
system_promptstr"You are a helpful assistant."noInline system prompt.
system_prompt_filestrnullnoPath to a markdown prompt file; relative to the agent folder. Concatenated through the inheritance chain.
prompt_context_filesdict[str,str]{}noJinja variable → file path; files are read and injected when the prompt is rendered.
tool_doc_modestr"standard"nobrief (name + description only, info gated), standard (plus full parameter schema), or full (usage tier inlined). Per-tool override: doc_mode on a tools: entry.
include_tools_in_promptbooltruenoInclude auto-generated tool list.
include_hints_in_promptbooltruenoInclude framework hints (tool-call syntax and info / read_job / jobs / wait command examples).
max_messagesint0noConversation cap. 0 = unlimited.
ephemeralboolfalsenoClear conversation after each turn (group-chat mode).
session_keystrnullnoOverride default session key (which is name).
inputdict{}noInput module config. See Input.
outputdict{}noOutput module config. See Output.
toolslist[]noTool entries. See Tools.
subagentslist[]noSub-agent entries. See Sub-agents.
triggerslist[]noTrigger entries. See Triggers.
compactdictnullnoCompaction config. See Compact.
startup_triggerdictnullnoOne-shot trigger fired on start. {prompt: "..."}.
terminationdictnullnoTermination conditions. See Termination.
max_subagent_depthint3noMax nested sub-agent depth. 0 = unlimited.
tool_formatstr | dict"bracket"nobracket, xml, native, or a custom dict format. native requires the configured LLM provider to support structured tool calling.
mcp_serverslist[]noPer-agent MCP servers. See MCP servers.
pluginslist[]noLifecycle plugins. See Plugins.
no_inheritlist[str][]noKeys that replace (not merge) base values. E.g. [tools, subagents].
memorydict{}nomemory.embedding.{provider,model}. See Memory.
output_wiringlist[]noPer-creature automatic round-output routing. See Output wiring.
skillslist[str][]noPackage-skill opt-in list. Package skills default disabled unless named here; "*" enables all discovered package skills.
skill_index_budget_bytesint4096noByte budget for the auto-invoke procedural-skill index in the system prompt.
framework_hint_overridesdict[str,str]{}noCreature-level override map for built-in framework-hint prose blocks.
disable_provider_toolslist[str][]noOpt out of provider-native tools auto-injected by the active backend.
max_iterationsint | nullnullnoShared iteration budget for the parent controller and inheriting sub-agents.
sanitize_orphan_tool_callsbooltruenoDrop orphan tool-call/tool-result fragments before sending history to the provider.

Controller block

All fields may also be set at the top level for backward compatibility.

FieldTypeDefaultDescription
llmstr""Profile reference in ~/.kohakuterrarium/llm_profiles.yaml (e.g. gpt-5.4, claude-opus-4.7). May carry an inline variation selector, e.g. claude-opus-4.7@reasoning=xhigh.
modelstr""Inline model id if llm unset. Also accepts a name@group=option selector.
providerstr""Disambiguator when model is set and the same model id is bound to multiple backends (e.g. openai vs openrouter).
variation_selectionsdict[str,str]{}Per-group variation overrides, {group_name: option_name}. See Variation selector.
variationstr""Shorthand for a single-option selection; resolved against the preset's groups.
auth_modestr""Blank (auto), codex-oauth, etc.
api_key_envstr""Env var holding the key.
base_urlstr""Override endpoint URL.
temperaturefloat0.7Sampling temperature.
max_tokensint | nullnullMaps onto the resolved profile's max_output (per-response output cap), not max_context (total window).
reasoning_effortstr"medium"none, minimal, low, medium, high, xhigh. Consumed directly by Codex; for other providers use extra_body (see Provider-specific extra_body notes).
service_tierstrnullpriority, flex.
extra_bodydict{}Deep-merged onto the resolved preset's extra_body (which may already carry variation patches).
tool_doc_mode, include_tools_in_prompt, include_hints_in_prompt, max_messages, ephemeral, tool_formatMirror top-level fields.

Canonical model identifiers are now provider/name[@group=option,...]. The runtime stores and surfaces this full identifier (for /model, session-info events, and UI display), so a round-trip like /model openai/gpt-5.4-api@reasoning=high is stable.

Resolution order per turn (see llm/profiles.py:resolve_controller_llm):

  1. --llm CLI flag wins over the YAML controller.llm.
  2. Otherwise controller.llm (preset name + optional @group=option selector).
  3. Otherwise controller.model: matched against the built-in and user preset registry by model id. controller.provider disambiguates cross-backend collisions; a name@group=option selector is also parsed out.
  4. If neither llm nor model was set, fall back to default_model from llm_profiles.yaml.
  5. After a profile is resolved, the controller's temperature, reasoning_effort, service_tier, max_tokens (remapped to max_output), and extra_body are layered on top. extra_body is deep-merged, every other override is a scalar replace.

Variation selector

A preset may expose variation groups: two-level dicts of {group_name: {option_name: patch}} that let one preset serve multiple knobs (reasoning effort, speed, thinking level) without duplicating the entry. Selection happens either inside the preset reference string or via explicit dict fields on the controller.

Shorthand forms (usable in --llm, controller.llm, or controller.model):

claude-opus-4.7@reasoning=xhigh                 # one group = option
claude-opus-4.7@reasoning=xhigh,speed=fast      # multiple groups, comma-separated
claude-opus-4.7@xhigh                           # bare option; auto-resolves
                                                # to the single matching group
                                                # (fails if ambiguous)

Explicit form (preferred when the selector is assembled in config):

controller:
  llm: claude-opus-4.7
  variation_selections:
    reasoning: xhigh
  # or, single-option shorthand:
  variation: xhigh

Rules:

  • The bare-shorthand form (@xhigh) is rejected when more than one group would match the option; disambiguate with @group=option.
  • Unknown groups or options raise at resolve time.
  • Variation patches may write to only these roots: temperature, reasoning_effort, service_tier, max_context, max_output, extra_body. Anything else is rejected.
  • Cross-group collisions on the same dotted path raise: two selections cannot both claim extra_body.reasoning.effort.

See Variation groups in builtins.md for the per-preset catalogue of groups and options.

Provider-specific extra_body notes

extra_body is deep-merged into the JSON request body. Each provider reads reasoning/effort knobs from a different path; set the knob the provider actually honours:

ProviderCanonical pathNotes
Codex (ChatGPT-OAuth)top-level reasoning_effort, service_tierreasoning_effort: none|low|medium|high|xhigh. Fast mode: use the speed=fast variation on gpt-5.4, which maps to service_tier: priority. Setting service_tier: fast literally is rejected by the OpenAI API.
OpenAI direct (-api presets)extra_body.reasoning.effortFull scale none|low|medium|high|xhigh.
OpenRouter (-or presets)extra_body.reasoning.effortUnified scale minimal|low|medium|high; xhigh only honoured by a handful of models (Opus 4.7, GPT-5.x).
Anthropic directextra_body.output_config.effortCompat endpoint silently drops top-level reasoning_effort / service_tier. Opus 4.7: low|medium|high|xhigh|max; Opus 4.6 / Sonnet 4.6: low|medium|high|max. Haiku 4.5 uses the older thinking.budget_tokens.
Gemini directextra_body.google.thinking_config.thinking_levelLOW|MEDIUM|HIGH (Pro) or MINIMAL|LOW|MEDIUM|HIGH (Flash / Flash-Lite).

Anthropic-via-OpenRouter (claude-*-or) presets ship with extra_body.cache_control: {type: ephemeral} pre-set; your inline controller.extra_body is deep-merged over it and can disable or replace it.

Anthropic-compatible endpoints also get automatic prompt-caching markers applied to the system message and the last three non-tool conversation messages unless you set extra_body.disable_prompt_caching: true.

Input

Dict fields: {type, module?, class?, options?, ...type-specific keys}.

FieldTypeDefaultDescription
typestr"cli"cli, cli_nonblocking, tui, none, custom, package. Audio/ASR inputs are custom/package modules.
modulestr(none)For custom (e.g. ./custom/input.py) or package (e.g. pkg.mod).
classstr(none)Class to instantiate. YAML key is class; the loader stores it on the class_name dataclass attribute.
optionsdict{}Module-specific options.
promptstr"> "CLI prompt (plain cli input only; ignored by the Rich CLI and TUI).
exit_commandslist[str][]Strings that trigger exit.

Output

Supports a default output plus optional named_outputs for side channels (e.g. a Discord webhook).

FieldTypeDefaultDescription
typestr"stdout"stdout, stdout_prefixed, console_tts, dummy_tts, tui, custom, package.
modulestr(none)For custom/package output modules.
classstr(none)Class to instantiate. YAML key is class; the loader stores it on the class_name dataclass attribute.
optionsdict{}Module-specific options.
controller_directbooltrueRoute controller text through the default output.
named_outputsdict[str, OutputConfigItem]{}Named side outputs. Each item has the same shape as the default.

Tools

List of tool entries. Each entry is a dict or a shorthand string (builtin by that name).

FieldTypeDefaultDescription
namestr(none)Tool name (required). For type: trigger, must match the trigger's setup_tool_name.
typestr"builtin"builtin, trigger, custom, package.
modulestr(none)For custom (e.g. ./custom/tools/my_tool.py) or package.
classstr(none)Class to instantiate for custom/package. YAML key is class; stored on the class_name dataclass attribute.
docstr(none)Override for the skill documentation file.
optionsdict{}Tool-specific options. For builtins, top-level keys such as timeout, max_output, working_dir, env, and notify_controller_on_background_complete are mapped into ToolConfig; remaining keys stay in config.extra.

Tool types:

  • builtin: resolved against the built-in tool catalog by name.
  • trigger: exposes a universal trigger class as an LLM-callable setup tool. name must match the trigger's setup_tool_name. Shipped setup tools: add_timer (TimerTrigger), watch_channel (ChannelTrigger), add_schedule (SchedulerTrigger).
  • custom / package: load the class at module + class.

Provider-native tools are auto-injected from the active backend's provider_native_tools declaration. The creature does not need to list such a tool under tools: unless it wants to override per-tool knobs. The shipped example is image_gen for Codex-backed creatures.

Shorthand:

tools:
  - bash
  - read
  - write

Sub-agents

FieldTypeDefaultDescription
namestr(none)Sub-agent identifier.
typestr"builtin"builtin, custom, package.
modulestr(none)For custom/package.
configstr(none)Named config object inside the module (e.g. MY_AGENT_CONFIG). YAML key is config; stored on the config_name dataclass attribute.
descriptionstr(none)Description used in the parent's prompt.
toolslist[str][]Tools this sub-agent is allowed to use.
can_modifyboolfalseWhether the sub-agent can perform mutating operations.
interactiveboolfalseStay alive across turns; receive context updates.
optionsdict{}Sub-agent-specific options. Inline sub-agent config fields such as notify_controller_on_background_complete are read from here when supported by SubAgentConfig.

Shorthand: a bare string is treated as a builtin sub-agent name:

subagents:
  - explore
  - worker

YAML-only inline config: use type: custom without module/config; unknown entry fields are forwarded into SubAgentConfig.from_dict:

subagents:
  - name: dependency_mapper
    type: custom
    system_prompt: "Map dependencies and return a compact summary."
    tools: [glob, grep, read, tree]
    default_plugins: ["auto-compact"]
    plugins:
      - name: budget
        options:
          turn_budget: [40, 60]
          tool_call_budget: [75, 100]

Builtin sub-agents already declare default_plugins: ["auto-compact"] and a budget plugin with turn_budget: [40, 60], tool_call_budget: [75, 100], and no walltime_budget.

Background completion note:

tools:
  - name: web_fetch
    type: builtin
    notify_controller_on_background_complete: false

subagents:
  - name: research
    type: builtin
    notify_controller_on_background_complete: false

With this flag set to false, the background job still emits normal activity/log/output updates, but its completion does not push a fresh event back into the controller loop.

Sub-agent option fields also include runtime and shared-budget controls:

  • default_plugins: ["auto-compact"]: expands to compact.auto; use it when the sub-agent has a compact: block that should auto-trigger.
  • plugins: [{name: budget, options: {...}}]: unified runtime budget plugin. Its options include turn_budget: [soft, hard], tool_call_budget: [soft, hard], and optional walltime_budget: [soft, hard] in seconds.
  • budget_inherit: true (default): child reuses the parent's shared legacy iteration budget if one exists.
  • budget_allocation: N: child gets a fresh isolated legacy budget of N turns.
  • budget_inherit: false with no allocation: child runs without the parent's shared legacy budget.

Triggers

FieldTypeDefaultDescription
typestr(none)timer, context, channel, custom, package.
modulestr(none)For custom/package.
classstr(none)Class to instantiate. YAML key is class; stored on the class_name dataclass attribute.
promptstr(none)Default prompt injection when the trigger fires.
optionsdict{}Trigger-specific options.

Common per-type options:

  • timer: interval (seconds), immediate (bool, default false).
  • context: debounce_ms (int, default 100); a debounced context-update trigger.
  • channel: channel (name), filter_sender (optional).

For a clock-aligned scheduler, expose SchedulerTrigger as an LLM-callable setup tool via a tools entry with type: trigger, name: add_schedule (see Tools) rather than declaring it in the triggers: list.

Compact

FieldTypeDefaultDescription
enabledbooltrueEnable automatic compaction. When false, manual /compact remains available.
max_tokensintprofile-defaultTarget token ceiling.
thresholdfloat0.8Fraction of max_tokens at which compaction starts.
targetfloat0.5Target fraction after compaction; recent turns remain verbatim when they fit.
keep_recent_turnsint8Turns preserved verbatim.
compact_modelstrcontroller's modelOverride LLM used for summarisation.

Output wiring

A list of framework-level routing entries. At each turn-end, the framework constructs a creature_output TriggerEvent and pushes it directly into each target creature's event queue, bypassing channels entirely. See output wiring in the terrariums guide and pattern 1b in patterns.md for discussion; this section is the config reference.

Entry fields:

FieldTypeDefaultDescription
tostr(none)Target creature name, or the magic string "root".
with_contentbooltrueIf false, the event carries an empty content (metadata-only ping).
promptstr | nullnullTemplate for the receiver's prompt override. When unset, a default template is used depending on with_content.
prompt_formatsimple | jinja"simple"simple uses str.format_map; jinja uses the prompt.template renderer for conditionals / filters.

Available template variables (both formats): source, target, content, turn_index, source_event_type, with_content.

Shorthand: a bare string is sugar for {to: <str>, with_content: true}:

output_wiring:
  - runner                                   # shorthand
  - { to: root, with_content: false }        # lifecycle ping
  - to: analyzer
    prompt: "[From coder] {content}"         # simple (default)
  - to: critic
    prompt: "{{ source | upper }}: {{ content }}"
    prompt_format: jinja

Notes:

  • Only meaningful when the creature runs inside a terrarium. Standalone creatures with output_wiring configured emit nothing (the resolver is attached by the terrarium runtime; a standalone agent gets a no-op resolver that logs once).
  • Unknown / stopped targets are logged and skipped; they never raise into the source creature's turn-finalisation.
  • The source's _finalize_processing runs to completion immediately; each target's _process_event runs in its own asyncio.Task so a slow receiver doesn't block the source.

Termination

Any non-zero threshold is enforced. Keyword match stops the agent when the output contains the keyword.

FieldTypeDefaultDescription
max_turnsint0
max_tokensint0
max_durationfloat0Seconds.
idle_timeoutfloat0Seconds with no events.
keywordslist[str][]Case-sensitive substring match.

Built-in termination checks run first. Plugins may then contribute additional termination voters programmatically; any positive vote stops the run.

MCP servers in agent config

Per-agent MCP servers. Connected on agent start. A global catalog at ~/.kohakuterrarium/mcp_servers.yaml (managed by kt config mcp) uses the same schema; agents declare the ones they want per-config.

FieldTypeDefaultDescription
namestr(none)Server identifier.
transportstdio | streamable_http | http | ssestdioTransport. streamable_http is preferred for modern HTTP MCP; http/sse are legacy SSE aliases.
commandstr(none)stdio executable.
argslist[str][]stdio args.
envdict[str,str]{}stdio env.
urlstr(none)URL for streamable_http, http, or sse transports.

Plugins

FieldTypeDefaultDescription
namestr(none)Plugin identifier.
typestr"builtin"builtin, custom, package.
modulestr(none)For custom (e.g. ./custom/plugins/my.py) or package.
class or class_namestr(none)Class to instantiate. Plugins accept both keys (see bootstrap/plugins.py); every other module kind uses class.
descriptionstr(none)Free-form metadata.
optionsdict{}Plugin-specific options.

Shorthand: a bare string is treated as a package-resolved plugin name.

Plugins may also add controller commands and termination voters at runtime; those are Python-level extension points, not YAML fields on the creature.

Memory

memory:
  embedding:
    provider: model2vec       # or sentence-transformer, api
    model: "@best"            # preset alias or HuggingFace path

Provider options:

  • model2vec (default, no torch dependency).
  • sentence-transformer (torch-based, higher quality).

Preset aliases: @tiny, @base, @retrieval, @best, @multilingual, @multilingual-best, @science, @nomic, @gemma.

Inheritance rules

base_config resolves via the path rules above. Merging follows one unified rule set for every field:

  • Scalars: child overrides.
  • Dicts (controller, input, output, memory, compact, …): shallow merge; child keys override at the top level.
  • Identity-keyed lists (tools, subagents, plugins, mcp_servers, triggers): union by name. On name collision child wins and replaces the base entry in place (preserving base order). Items without a name value concatenate.
  • Other lists: child replaces base.
  • Prompt files: system_prompt_file concatenates along the chain; inline system_prompt is appended last.

Two directives opt out of defaults:

DirectiveEffect
no_inherit: [field, …]Drops the inherited value for each listed field. Applies uniformly to scalars, dicts, identity lists, and the prompt chain.
prompt_mode: concat | replaceconcat (default) keeps inherited prompt file chain + inline. replace wipes inherited prompts, sugar for no_inherit: [system_prompt, system_prompt_file].

Examples.

Override an inherited tool without replacing the whole list:

base_config: "@kt-biome/creatures/swe"
tools:
  - { name: bash, type: custom, module: ./tools/safe_bash.py, class: SafeBash }

Start clean: drop inherited tools entirely.

base_config: "@kt-biome/creatures/general"
no_inherit: [tools]
tools:
  - { name: think, type: builtin }

Replace the prompt entirely for a specialised persona:

base_config: "@kt-biome/creatures/general"
prompt_mode: replace
system_prompt_file: prompts/niche.md

File convention

creatures/<name>/
  config.yaml           # required
  prompts/system.md     # if referenced
  tools/                # custom tool modules (by convention)
  memory/               # context files (by convention)
  subagents/            # custom sub-agent configs (by convention)

These subfolder names are conventions only. The loader resolves each module: path relative to the agent folder via ModuleLoader; there is no auto-scan of tools/ or subagents/, so every custom module must be declared in config.yaml.


Terrarium config (terrarium.yaml)

Loaded by kohakuterrarium.terrarium.config.load_terrarium_config.

terrarium:
  name: str
  root:                  # optional: designate the privileged user-facing node
    base_config: str     # or any AgentConfig field inline
    ...
  creatures:
    - name: str
      base_config: str   # legacy alias: `config:`
      channels:
        listen: [str]
        can_send: [str]
      output_log: bool         # default false
      output_log_size: int     # default 100
      ...                      # any AgentConfig override
  channels:
    <name>:
      description: str
    # or shorthand: string = description:
    # <name>: "description"

All graph channels are broadcast: every listener receives every send. The previously-supported type: field is ignored at the engine layer; new configs should omit it.

Recipes carry no Drive fields. A terrarium.yaml recipe is graph-construction glue only — creatures, channels, wiring. It never configures the Drive runtime, selects registrations, or seeds Drive records. Drive runtime configuration is an explicit Terrarium(...) constructor argument (managed products resolve it from drive-settings.yaml). Applying the same recipe to two engines can therefore produce different Drive capabilities. All existing recipes remain byte-for-byte valid with no Drive awareness.

Terrarium field summary:

FieldTypeDefaultDescription
namestr(none)Terrarium name.
rootobjectnullOptional inline agent config promoted to the privileged user-facing node; it receives the group tools and the standard report_to_root wiring.
creatureslist[]Creatures that run inside the terrarium.
channelsdict{}Shared channel declarations.

Creature entry fields (also accepts any AgentConfig field inline, e.g. system_prompt_file, controller, output_wiring, …):

FieldTypeDefaultDescription
namestr(none)Creature name.
base_config (or config)str(none)Config path (agent config).
channels.listenlist[str][]Channels the creature consumes.
channels.can_sendlist[str][]Channels the creature can publish to.
output_logboolfalseCapture stdout per creature.
output_log_sizeint100Max lines per creature's log buffer.
output_wiringlist[]Framework-level auto-delivery of this creature's turn-end output to other creatures. See Output wiring for the entry shape.

Channel entry fields:

FieldTypeDefaultDescription
descriptionstr""Documented in the channel topology prompt.

All graph channels are broadcast: every listener receives every send. A legacy type: key is still parsed but ignored at the engine layer; omit it in new configs.

Auto-created channels:

  • One channel per creature, named after the creature (direct message via send_channel).
  • report_to_root channel when root: is set, with every other creature wired to send on it and only root listening.

Root (privileged user-facing node):

  • Force-registered with the privileged group tools (group_add_node, group_remove_node, group_start_node, group_stop_node, group_channel, group_wire, group_status).
  • Auto-listens to every creature channel; receives report_to_root.
  • Inheritance / merge rules are the same as for creatures.

Drive settings (drive-settings.yaml)

The Drive runtime is configured by explicit Terrarium(...) constructor arguments (drive_config / drive_registrations / drive_store). The low-level engine never reads any file. For managed surfaces (web, TUI, kt, desktop), Studio owns a settings file and resolves it into those explicit arguments:

  • Canonical path: config_dir() / "drive-settings.yaml", normally ~/.kohakuterrarium/drive-settings.yaml (honours KT_CONFIG_DIR). It is not the launcher's app-settings.json.
  • Loaded/validated by kohakuterrarium.studio.identity.drive_settings.
  • Stores serializable selections and options only — never live Python objects and never Drive records (those live in the Drive repository / session sidecar).
  • An absent file resolves to a runtime-disabled default. A malformed file raises a typed validation error and the last valid file is left untouched — a bad setting never silently enables code.

Full schema (all fields optional; omitted fields use the documented defaults shown):

schema_version: 1
runtime:
  enabled: false                  # off by default; nothing runs until true
  max_active_per_creature: 8
  max_pending_per_graph: 100
  max_consecutive_drive_turns: 3
  dispatcher_concurrency: 4
  spec_max_bytes: 16384
  presentation_max_bytes: 8192
  metadata_max_bytes: 4096
  evidence_max_bytes: 16384
  retry:
    max_attempts: 5
    initial_backoff_s: 2.0
    max_backoff_s: 300.0          # must be >= initial_backoff_s
    jitter: 0.1                   # within [0, 1]
  retention:
    terminal_days: 90
    acknowledged_delivery_days: 30
    superseded_delivery_days: 7
    dead_letter_days: 90
    progress_max_count: 500
    progress_max_age_days: 90
registrations:
  generic:                        # a registration is keyed by its stable name
    enabled: true
    options: {}
  goal:
    enabled: false                # installed != enabled (see below)
    options: {}

runtime fields

runtime reuses the same validation as the DriveRuntimeConfig constructor argument — nonsense values fail at load, not after a Drive is already active.

FieldTypeDefaultMeaning
enabledboolfalseMaster switch. false = the engine owns no Drive manager, tools, prompt, or dispatcher.
max_active_per_creatureint ≥ 18Cap on concurrently active Drives per assignee.
max_pending_per_graphint ≥ 1100Backpressure cap on pending deliveries per graph.
max_consecutive_drive_turnsint ≥ 13After this many back-to-back Drive turns, dispatch yields one slot to queued user / channel / trigger work.
dispatcher_concurrencyint ≥ 14Max concurrent delivery claims.
spec_max_bytes / presentation_max_bytes / metadata_max_bytes / evidence_max_bytesint ≥ 116384 / 8192 / 4096 / 16384Independent per-payload byte limits.
retry.max_attemptsint ≥ 15Delivery attempts before dead-letter.
retry.initial_backoff_s / retry.max_backoff_snumber ≥ 02.0 / 300.0Exponential backoff bounds (maxinitial).
retry.jitternumber 0–10.1Backoff jitter fraction.
retention.terminal_daysint ≥ 090Days a terminal Drive is kept before it is eligible for retirement.
retention.acknowledged_delivery_daysint ≥ 030Retention for acknowledged deliveries.
retention.superseded_delivery_daysint ≥ 07Retention for superseded deliveries.
retention.dead_letter_daysint ≥ 090Retention for dead letters.
retention.progress_max_countint ≥ 1500Max retained progress records per Drive.
retention.progress_max_age_daysint ≥ 090Max age of retained progress records.

registrations: installed is not enabled

Each key under registrations is a registration's stable name with { enabled: bool, options: {...} }. A registration must be installed (declared by a package's drive_registrations: manifest slot, or passed directly to Terrarium) and enabled here before its kind can be created, validated, projected, or scheduled. Installation alone never enables anything. Enabling the runtime with no enabled registration is rejected. Only enabled registrations are imported and only they contribute prompt prose.

Save is not apply

Persisting settings and applying them to a running engine are two distinct operations, so a UI can never pass off "saved for next start" as "running now":

  • Save validates and atomically writes the file (save_settings). It is optimistic-concurrency protected by a content-hash revision; a stale write raises a conflict error and the caller must refetch.

  • Apply (apply_runtime) is separate and returns one of:

    ResultWhen
    applied_liveA registration-set change on an already-enabled runtime with unchanged tuning — loaded, validated, and swapped atomically.
    restart_requiredTurning the runtime on or off, or changing runtime tuning — the v1 conservative boundary. The file becomes the desired next-start config; the running engine is unchanged.
    rejectedSettings failed to resolve (e.g. an enabled registration that will not import).

The apply result reports both the desired settings revision and the running runtime revision, so surfaces can show desired-versus-running state honestly. Disabling a persisted registration never deletes its Drive records; those become non-deliverable and inspectable until a compatible registration is restored (see when a registration is disabled).

Node-targeted settings (Laboratory)

Settings scope is per execution node / config home, not per recipe and not per creature. In a Laboratory deployment each worker has its own drive-settings.yaml under its own KT_CONFIG_DIR, and a settings operation carries a target node so it routes to the worker that will run the Drives — never to the host's agent-free coordination engine. Availability is node-specific because package installation is node-specific. In L4 multi-user mode the operator policy is shared by default; each per-user engine receives a fresh immutable resolution of it while its Drive records stay per-user.


LLM profiles (~/.kohakuterrarium/llm_profiles.yaml)

version: 3
default_model: <preset name>

# Optional exact sub-agent name → default model selector mapping.
subagent_models:
  explore: openrouter/mimo-v2.5-pro
  worker: codex/gpt-5.5

backends:
  <provider-name>:
    backend_type: openai | anthropic | codex  # transport implementation
    base_url: str
    api_key_env: str
    provider_name: str                  # compatibility identity for native tools
    provider_native_tools: [str, ...]   # auto-injected native tools this backend serves

presets:
  <preset-name>:
    provider: <backend-name>   # reference to backends or built-in
    model: str                 # model id
    max_context: int           # default 256000
    max_output: int            # default 65536
    temperature: float         # optional
    reasoning_effort: str      # none | minimal | low | medium | high | xhigh
    service_tier: str          # priority | flex
    extra_body: dict
    variation_groups:          # optional; see Variation selector
      <group>:
        <option>:
          <dotted.path>: value

Canonical backend_type values are:

  • openai: OpenAI-compatible /chat/completions endpoints.
  • anthropic: Anthropic-compatible Messages API endpoints via the official anthropic Python package (Claude, MiniMax's /anthropic/v1/messages, and compatible proxies).
  • codex: OpenAI Responses-API transport. With no base_url it uses the ChatGPT-subscription Codex OAuth flow; set a base_url (a custom OpenAI-Responses-compatible endpoint) and it uses API-key auth instead — no OAuth. base_url is the single discriminator.

Legacy codex-oauth is accepted for back-compat and normalized to codex.

Built-in provider names (codex, openai, openrouter, anthropic, gemini, mimo, kimi-code, glm-coding) cannot be deleted; their base URLs and api_key_env values are fixed via built-in defaults. Per-agent overrides via controller.base_url / controller.api_key_env still work.

subagent_models supplies user-level defaults without editing packaged creature configs. Keys match the final SubAgentConfig.name exactly. A concrete model on the creature or sub-agent module still wins; parent, inherit, and default explicitly inherit the parent LLM. An absent model, subagent-default, or subagent_default uses the named entry when present, otherwise it inherits the parent. The mapping is read when a sub-agent is created, so edits affect future jobs only. Conversation and Inspector Trace rows record the model actually bound to each new job, preferring its canonical profile selector and falling back to the raw model id.

Adding a custom LLM backend provider

For most providers you only need a backend entry plus a preset:

backends:
  minimax-anthropic:
    backend_type: anthropic
    base_url: https://api.minimax.io/anthropic
    api_key_env: MINIMAX_API_KEY

presets:
  minimax-anthropic:
    minimax-m2.7:
      model: MiniMax-M2.7
      max_context: 200000
      max_output: 2048

Use backend_type: openai for providers exposing an OpenAI-compatible /chat/completions API, and backend_type: anthropic for providers exposing an Anthropic-compatible /v1/messages API. API keys are resolved first from ~/.kohakuterrarium/api_keys.yaml (kt login <provider-name> / kt config key set <provider-name>) and then from api_key_env. Anthropic backend presets can pass SDK request fields through extra_body; for provider beta headers, set extra_body.extra_headers on the preset.

To add a new transport implementation in code, create a BaseLLMProvider subclass under src/kohakuterrarium/llm/, implement _stream_chat() and _complete_chat() using KohakuTerrarium's OpenAI-shaped internal message dicts, add the backend type to validate_backend_type(), and extend bootstrap/llm.py so resolved LLMProfile.backend_type instantiates it. Keep provider-specific request/response conversion at that boundary; do not change controller or conversation storage for one provider.

Custom backends may also declare:

  • provider_name: the compatibility identity used when checking whether a provider-native tool supports this backend.
  • provider_native_tools: the built-in provider-native tools to auto-inject into creatures using this backend.

See LLM presets in builtins.md for every shipped preset, Variation groups in builtins.md for the per-preset catalogue, and Variation selector for how to pick a specific variation in a controller config.


MCP server catalog (~/.kohakuterrarium/mcp_servers.yaml)

Global MCP registry, an alternative to per-agent mcp_servers:.

- name: sqlite
  transport: stdio
  command: mcp-server-sqlite
  args: ["/path/to/db"]
  env: {}
- name: web_api
  transport: streamable_http
  url: https://mcp.example.com/mcp
  env: { API_KEY: ${MCP_API_KEY} }

Fields:

FieldTypeDefaultDescription
namestr(none)Unique identifier.
transportstdio | streamable_http | http | ssestdioTransport. streamable_http is preferred for modern HTTP MCP; http/sse are legacy SSE aliases.
commandstr(none)stdio executable.
argslist[str][]stdio args.
envdict[str,str]{}stdio env.
urlstr(none)URL for streamable_http, http, or sse transports.

Package manifest (kohaku.yaml)

name: my-package
version: "1.0.0"
description: "..."
creatures:
  - name: researcher
terrariums:
  - name: research_team
tools:
  - name: my_tool
    module: my_package.tools
    class: MyTool
plugins:
  - name: my_plugin
    module: my_package.plugins
    class: MyPlugin
io:
  - name: discord_input
    module: my_package.io.discord
    class: DiscordInput
triggers:
  - name: webhook
    module: my_package.triggers.webhook
    class: WebhookTrigger
skills:
  - name: repo-surgery
    path: skills/repo-surgery
commands:
  - name: handoff
    module: my_package.commands.handoff
    class: HandoffCommand
user_commands:
  - name: deploy
    module: my_package.user_commands.deploy
    class: DeployCommand
drive_registrations:
  - name: goal
    kind: goal
    module: my_package.drive.goal
    class: GoalDriveRegistration
    description: Durable objective pursuit policy
prompts:
  - name: git-safety
    path: prompts/git-safety.md
framework_hints:
  framework.execution_model.dynamic: "..."
llm_presets:
  - name: my_preset
python_dependencies:
  - requests>=2.28.0
FieldTypeDescription
namestrPackage name; installed as ~/.kohakuterrarium/packages/<name>/.
versionstrSemver.
descriptionstrFree-form.
creatureslist[{name}]: creature configs under creatures/<name>/.
terrariumslist[{name}]: terrarium configs under terrariums/<name>/.
toolslist[{name, module, class}]: contributed tool classes.
pluginslist[{name, module, class}]: contributed plugins.
iolist[{name, module, class}]: contributed input/output modules resolved by package name.
triggerslist[{name, module, class}]: contributed trigger classes.
skillslist[{name, path, description?}]: contributed procedural skill bundles.
commandslist[{name, module, class, override?}]: controller ##name## commands.
user_commandslist[{name, module, class}]: human-facing slash commands.
drive_registrationslist[{name, kind, module, class, description?}]: deterministic Drive kind/policy registrations. Discovered here but inert until enabled in drive-settings.yaml; the catalog scan lists them without importing the module.
prompts / templateslist[{name, path}]: reusable prompt fragments for Jinja {% include %}.
framework_hintsdict[str,str]Package-level override map for framework-hint prose blocks.
llm_presetslist[{name}]: contributed LLM presets (values live in the package).
python_dependencieslist[str]Pip requirement strings.

Install modes:

  • kt install <git_url>: clone.
  • kt install <path>: copy.
  • kt install <path> -e: write <name>.link pointer to the source.

API-key storage (~/.kohakuterrarium/api_keys.yaml)

Managed by kt login and kt config key set. Format:

openai: sk-...
openrouter: sk-or-...
anthropic: sk-ant-...

Resolution order: stored file → env var (api_key_env) → empty.


See also