Skip to content

Configuration

The system is configured by a single layered YAML file (agent-config.yaml): providers → llms / mcp_servers → agents. Cross-layer ${ref} resolution lets each layer reference the one below it, so adding an agent is a short stanza with no other file changes.

For the machine-readable dispatch overrides applied at invocation time, see reference.md — AGENT_RUN_CONFIG.


┌─────────────────────────────────────────────────────────────┐
│ agents architect / dev / context / review / issue / ... │
│ model: "${llms.qwen-3-6-27b}" │
└──────────────────────────────┬──────────────────────────────┘
│ ${ref}
┌──────────────────────────────▼──────────────────────────────┐
│ llms qwen-3-6-27b / deepseek-v4-pro / ... │
│ provider: "${providers.openrouter}" │
│ mcp_servers brave-search / ... (agent opt-in via ${ref}) │
└──────────────────────────────┬──────────────────────────────┘
│ ${ref}
┌──────────────────────────────▼──────────────────────────────┐
│ providers openrouter / local_vllm │
│ api_url + api_token_env │
└─────────────────────────────────────────────────────────────┘

References resolve bottom-up: a provider has no dependencies; an LLM points at a provider; a mcp_servers catalog entry is standalone; an agent points at an LLM (and, optionally, at one or more catalog entries). A ref that does not resolve is an error at load time (fail-fast, before any agent runs).

The config is loaded and validated by the config/ package (load_config, load_workflow, resolve_refs — see architecture_code.md). agent-config.yaml is the sole source of LLM configuration: the workflow no longer reads the legacy LLM_API_KEY / LLM_MODEL / LLM_BASE_URL / LLM_TEMPERATURE environment variables. If the file is missing, a role is absent, or a provider’s api_token_env is unset, the run fails fast with an actionable LLMConfigError naming the path (or the env var to set).

Where a relative AGENT_CONFIG_PATH resolves

Section titled “Where a relative AGENT_CONFIG_PATH resolves”

AGENT_CONFIG_PATH (the workflow’s agent_config_path input) is relative to the caller’s repository — the checked-out workspace, not the Nebuko checkout. The base is $GITHUB_WORKSPACE when set, otherwise the working directory the engine started in. An absolute value is used unchanged, and a bare file name (no separator) is looked up in <base>/.nebuko/agents/ first, then in Nebuko’s shipped defaults/agents/. This is what makes a cross-repo uses: nebuko/nebuko/... caller’s .nebuko/agents/agent-config.yaml resolve against the caller’s own workspace rather than the imported engine checkout.

How a cross-repo caller’s changes are published

Section titled “How a cross-repo caller’s changes are published”

The caller workspace anchors more than config lookup: the branch scaffold, the git session guards and the agent’s commit/push/PR wind-down all run there too (issue #1205). So a cross-repo caller needs its repository checked out at $GITHUB_WORKSPACE — the shipped workflow’s checkout step does this — with an origin remote configured and a git token present (NEBUKO_GIT_TOKEN, GH_TOKEN or GITHUB_TOKEN with contents write access). The feature branch, the commits and the pull request then land in the caller’s repository, not in the .nebuko-engine checkout that holds the engine’s source.

Only the git operations anchor at the caller workspace. The agent’s code — the agent_script entry point and the assembly it imports — always resolves from the imported .nebuko-engine checkout, never from the caller workspace (issue #1208). That is why a caller repository needs no src/nebuko/ tree of its own: the run borrows the engine’s code and writes its results into the caller’s checkout.


A bare /nebuko run workflow (no --name) starts the repo’s default workflow (issue #1094). Set it in the Nebuko manifest .nebuko/nebuko.yaml:

default_workflow: default

When the manifest is absent or does not declare default_workflow, the shipped default workflow is used. The manifest is read from the caller’s workspace (the same .nebuko/ directory that holds workflow and agent overrides).


FieldTypeRequiredDescription
api_urlstringyesChat-completions endpoint URL, or the name of an environment variable holding that URL (e.g. LOCAL_VLLM_API_URL), resolved at config-load time — mirrors api_token_env.
api_token_envstringyesName of the environment variable holding the API token.
cache_key_paramstringnoName of the JSON request-body field that carries the prompt-cache key (e.g. prompt_cache_key for Mistral). Config declares only the name; the value is the engine-stamped issue id (LLM_SESSION_ID) at runtime and is sent via litellm_extra_body — never as an HTTP header. Omit to send nothing.
providers:
openrouter:
api_url: "https://openrouter.ai/api/v1"
api_token_env: "OPENROUTER_API_KEY"
local_vllm:
# api_url may also be an env var name (resolved at load time):
api_url: "LOCAL_VLLM_API_URL"
api_token_env: "LOCAL_VLLM_API_TOKEN"

FieldTypeDescription
model_pathstringModel identifier on the provider (e.g., openrouter/anthropic/claude-sonnet-5).
provider${ref}Provider entry from the providers: layer.
default_temperaturefloatBase temperature for the model.
input_price / output_pricefloatCost per 1M tokens (USD) — seeds metrics/cost accounting.
cache_read_price / cache_write_pricefloatCost per 1M tokens (USD) for cached prompt tokens; falls back to input_price when unset.
thinking_enabledboolEnables the model’s thinking mode (e.g., Qwen flexible thinking). false strips all reasoning-enabling request params.
preserve_thinkingboolKeeps thinking content across turns.
reasoning_effortstringExplicit top-level reasoning effort (none/low/medium/xhigh); unset keeps the SDK default.
max_output_tokensintMax output tokens per completion.
top_p / top_k / min_pfloat/intSampling parameters (see table below).
presence_penalty / repetition_penaltyfloatRepetition control (see table below).
stop_sequencesstring[]Tokens that stop generation.

Cost precedence. A provider/SDK-reported cost (a genuinely billed amount, cache-aware) always wins over local estimation, so for a model litellm already knows the SDK’s cost is used and these configured prices do not override it. The *_price fields only seed the local four-tier estimate, which applies when no provider/SDK cost is reported — e.g. a subscription backend, or a self-hosted model. Where the estimate applies, each tier falls back per field to the default price service (an OpenRouter-backed table; see features.md — Metrics) and then to input_price.

The issue-close agent-metrics job additionally fills entries whose recorded cost is still 0 at report time, pricing their stored tokens against that same default price service by the model identity each run recorded (issue #928). The job has no agent-config, so this report-time fill uses the table only; configured *_price precedence applies at run time (the local estimate written into state). State is never rewritten — rerunning the job recomputes.

When any cost is estimated, a folded Price details used for the estimation block under the summary table lists Input price ($/1M), Output price ($/1M) and Cache hit price ($/1M) — the exact per-1M USD prices the estimate used, so the number can be audited and tuned (issue #1048). The * marker’s note is that block’s caption. Its rows are the model names the estimates were priced with, not the agent roles: the price is configured per model, and roles sharing a model share a row. Its columns are the price tiers Input price ($/1M), Output price ($/1M) and Cache hit price ($/1M), so a model’s three rates read straight across one line. Only models with an estimated run are listed. The values are the report-time table prices (config prices are not persisted into metrics). A table entry without a cache-read price bills cache hits at the input rate, so the Cache hit price ($/1M) column shows that input price rather than —.

How to use. Set input_price and output_price (USD per 1M tokens) on an llms entry in the agent config YAML, and optionally cache_read_price where the provider publishes one:

llms:
mistral-small-4:
model_path: "mistral/mistral-small-2603"
provider: "${providers.mistral}"
input_price: 0.15
output_price: 0.6
cache_read_price: 0.015

These prices apply to every agent role that references the llm entry.

How it works. Pricing merges two sources, per field, with the price-table value winning where both are set:

  • Run time — when the provider reports no cost, the local four-tier estimate prices tokens with the configured *_price fields, completed from the default price table where unset.
  • Report time — the issue-close agent-metrics job estimates zero-cost entries with the same merge: the price table first, then the agent config’s *_price fields as fallback (loaded from defaults/agents/ or the path pinned by AGENT_CONFIG_PATH). An estimated cell carries the * marker with the footnote, and the folded Price details used for the estimation block lists the per-model prices actually used.

Fail-open. When no price resolves for a model from either source — unlisted model, missing table, unpriced config — its cost shows $0.0000 with no marker and no price row (unchanged behaviour).

Sampling parameters and their typical ranges:

ParameterTypeDescriptionTypical range
top_pfloatNucleus sampling — lower = more deterministic0.1–1.0
top_kintLimits the candidate token pool10–100
min_pfloatFilters tokens below an absolute probability threshold0.0–1.0
presence_penaltyfloatDiscourages repeating tokens based on presence-2.0–2.0
repetition_penaltyfloat> 1.0 discourages repetition1.0–1.5

Note: presence_penalty must match the model’s operating mode: 0.0 for thinking-mode operation, ~1.5 for instruct-only (per official Qwen model card). PR #225 originally set 1.5 on the Qwen entries while they ran with thinking_enabled: true, i.e., under the wrong regime.

Example:

llms:
claude-sonnet-5:
model_path: "openrouter/anthropic/claude-sonnet-5"
provider: "${providers.openrouter}"

FieldTypeDescription
model${ref}LLM entry from the llms: layer.
rolestringOne-line role description, rendered into every other agent’s “Your Team” section. Optional; a missing role renders a placeholder and raises the C8-missing-role lint warning.
temperaturefloatPer-agent temperature override (floor for complexity adjustment).
prompt_filestringPath to the agent’s system prompt (convention: defaults/prompts/{agent}.md). Only the basename is used at run time — the layered lookup re-resolves it, so deployments can override a prompt by name.
skillsentry[]Skill attachments (issue #955). Each entry is a bare skill name (legacy, means inject), - inject: [names] (the body is rendered into the agent’s prompt) or - discover: [names] (name + one-line summary only, read on demand with the SDK’s invoke_skill tool). See agents-and-skills.md.
complexity_overridesmapping<low | medium | high, object>Per-tier partial overrides of model/sampling/reasoning. See Complexity Overrides.
toolsstring[]Tool whitelist: terminal, file_editor, task_tracker (SDK tools) plus think, finish, dispatch_next (built-in defaults). See agents-and-skills.md.
mcp_serversobject[]MCP servers attached to the agent’s conversation. Each entry is either an inline spec or a ${mcp_servers.<name>} ref into the root catalog (see MCP Servers).
token_budgetobjectCompaction / prompt-leanness policy (see Token Budget).

Example:

agents:
dev:
model: "${llms.qwen-3-6-27b}"
role: "Implementation engineer — writes code, runs tests, manages branches, commits, and PRs."
prompt_file: "defaults/prompts/dev.md"
skills:
- inject: ["posting-contract"]
- discover: ["complexity"]
tools: ["terminal", "file_editor", "task_tracker", "think", "finish", "dispatch_next"]

token_budget controls how an agent’s context is compacted and how lean its per-call prompt surface is (issue #851). Compaction is a late safety valve that runs on the agent’s own model: it is never triggered early by a small event count, and the summarizer’s spend is metered under usage_id="condenser".

FieldTypeDefaultDescription
compaction_max_sizeint | null240Event count that triggers compaction.
compaction_max_tokensint | null80% of the route input limitToken count that triggers compaction.
compaction_keep_firstint4Leading events compaction always preserves.
compaction_modelstring | nullagent’s modelSummarizer model (default shares the conversation’s prompt cache).
prompt_profilefull | leanfulllean trims per-call tool/prompt overhead.
require_batched_reconboolfalseRequire reconnaissance to be batched into single multi-action turns.

Example:

agents:
dev:
model: "${llms.qwen-3-6-27b}"
token_budget:
prompt_profile: "lean"
require_batched_recon: true

mcp_servers attaches Model Context Protocol servers to an agent’s conversation. Each entry is a stdio process spec; the server’s tools become available to the agent alongside its tools whitelist.

There are two levels, mirroring providers/llms:

  • Root catalog — a top-level mcp_servers: mapping, keyed by server name, holding the reusable server specs. The key must equal the entry’s name.
  • Agent opt-in — each agent’s mcp_servers: list is a list of refs into the catalog (- "${mcp_servers.<name>}"). Inline specs are still accepted for a genuinely one-off server.

A ref resolves to the referenced entry dict at load time, so the runtime wiring is unchanged: the agent still ends up with plain MCPServerConfig instances.

Catalog/server spec fields:

FieldTypeDescription
namestringStable handle for the server (must equal its catalog key).
commandstringExecutable to launch (npx, uvx, docker, or a binary path).
argsstring[]Command arguments (default []).
envmapping<string, string>Child-process env var → name of the OS env var holding the value (never the literal secret).
transportstringAlways stdio for now (default stdio).

Unresolved env references (the named OS env var is unset) cause the server to be skipped with a warning — a missing key degrades that one capability rather than failing the run.

Example — one Brave Search MCP definition reused by the context agent:

mcp_servers:
brave-search:
name: "brave-search"
command: "npx"
args:
- "-y"
- "@brave/[email protected]"
- "--enabled-tools"
- "brave_web_search"
- "brave_news_search"
env:
BRAVE_API_KEY: "BRAVE_API_KEY" # child var ← OS env var name
agents:
context:
tools: ["terminal", "file_editor", "task_tracker", "think", "finish"]
mcp_servers:
- "${mcp_servers.brave-search}"

Any number of agents can reference the same catalog entry. Set BRAVE_API_KEY in the environment (or as a CI secret) to enable it.


Syntax: ${layer.key} where layer is providers, llms, or mcp_servers.

Rules:

  • A value that is exactly a single ref (e.g., "${llms.qwen-3-6-27b}") is replaced by the full referenced object (dict).
  • Inline partial refs also work (e.g., "prefix_${llms.x.model_path}_suffix"): the ref path may descend into a field (layer.key.subkey), and its scalar value is substituted in place of the ref expression.
  • Resolution is recursive and runs at load time; an undefined ref is a hard error at load time.
  • Refs can only point at a lower layer (agents → llms / mcp_servers → providers).

Inline partial example:

agents:
dev:
model: "${llms.qwen-3-6-27b}" # full-string ref → the whole LLM dict
# A hypothetical inline form would be written as:
# model_tag: "gfa-${llms.qwen-3-6-27b.model_path}"
# → resolves to "gfa-hosted_vllm/qwen-3-6-27b"

Chain example (as found in this repo’s agent-config.yaml):

agents:
architect:
model: "${llms.qwen-3-6-27b}" # → resolves to the qwen-3-6-27b LLM dict
temperature: 1.0
prompt_file: "defaults/prompts/architect.md"
tools: ["think", "finish"]
llms:
qwen-3-6-27b:
model_path: "hosted_vllm/qwen-3-6-27b"
provider: "${providers.local_vllm}" # → resolves to the local_vllm provider dict
default_temperature: 0.7
providers:
local_vllm:
api_url: "http://192.168.1.64:31728/v1"
api_token_env: "LOCAL_VLLM_API_TOKEN"

# ── Layer 1: Providers — API endpoints + auth env var name ──
providers:
openrouter:
api_url: "https://openrouter.ai/api/v1"
api_token_env: "OPENROUTER_API_KEY"
# ── Layer 2: LLMs — models referencing providers, with sampling params ──
llms:
qwen-3-6-30b:
model_path: "hosted_vllm/qwen-3-6-30b-a3b"
provider: "${providers.local_vllm}"
default_temperature: 1.0
thinking_enabled: true
max_output_tokens: 32768
top_p: 0.95
top_k: 20
# ── Layer 3: Agents — role → model + prompt + tools ──
agents:
architect:
model: "${llms.qwen-3-6-30b}"
temperature: 1.0
prompt_file: "defaults/prompts/architect.md"
skills: []
tools: ["think", "finish"] # planners never touch files
dev:
model: "${llms.qwen-3-6-30b}"
prompt_file: "defaults/prompts/dev.md"
complexity_overrides:
low:
temperature: 0.0
high:
temperature: 0.9
reasoning: "xhigh"
tools: ["terminal", "file_editor", "task_tracker", "think", "finish"]

See this repo’s agent-config.yaml for the full 5-agent configuration.


Per-invocation dispatch fields override the YAML cascade. Precedence (highest first):

  1. Per-invocation — dispatch fields in the AGENT_RUN_CONFIG sentinel (model, temperature, top_p, top_k, min_p, presence_penalty, repetition_penalty, complexity).
  2. Complexity tier override — agents.<name>.complexity_overrides.<tier> for the dispatch complexity tier (low/medium/high). See Complexity Overrides.
  3. Per-agent — agents.<name> stanza in agent-config.yaml.
  4. Per-LLM — llms.<name> stanza in agent-config.yaml.

The complexity tier used for a run is resolved as explicit dispatch complexity > steps[].complexity (workflow YAML) > "medium" (issue #787) and exported as CONFIG_COMPLEXITY. A workflow step can therefore declare a default tier for its own invocation — see workflows.md.

When complexity is set in the dispatch and no explicit temperature override is provided, temperature is auto-adjusted:

ComplexityTemperature
low0.0 (most deterministic)
medium0.1
high0.3 (more creative)

The agent’s base temperature from agent-config.yaml serves as a floor — complexity only increases temperature, never decreases it.

Reasoning effort is independent of complexity: the reasoning dispatch field (none/low/medium/xhigh) maps to the provider’s thinking-mode parameter and only applies while thinking_enabled is true for the LLM.

Complexity Overrides (per-tier configuration)

Section titled “Complexity Overrides (per-tier configuration)”

An agent can swap its model, temperature, reasoning effort, and sampling parameters per resolved complexity tier — an explicit dispatch complexity, else the workflow step’s steps[].complexity, else medium. This enables cost/quality tiering — cheap settings for low, escalate to a stronger model for high.

Keys are exactly low, medium, or high; each value is a partial override reusing the agent-stanza field names (model, temperature, reasoning, reasoning_effort, thinking_enabled, top_p, top_k, min_p, presence_penalty, repetition_penalty). Unset fields fall through to the per-agent value; an unknown key is a load error, never silently ignored.

agents:
dev:
model: "${llms.qwen-3-6-27b}"
temperature: 0.7
complexity_overrides:
low:
temperature: 0.0
high:
model: "${llms.claude-sonnet-5}"
temperature: 0.9
reasoning: "xhigh"
top_p: 0.8

Precedence is per-invocation (CONFIG_* / AGENT_RUN_CONFIG) > tier override

per-agent > per-LLM. A tier temperature replaces the agent’s base temperature, and the per-tier floor from Dynamic Complexity-Based Temperature is then applied on top with max(); so a high override of 0.1 still resolves to 0.3. reasoning only applies while thinking is enabled for the resolved model.

thinking_enabled may also be set per tier, so an agent can re-enable thinking for one tier while its LLM defaults it off (the tier value wins). When thinking resolves to false, the resolver strips every reasoning-enabling request parameter and sends top-level reasoning_effort: "none" — an explicit kill switch, because a missing param is read by the provider as its own default (reasoning on) — so thinking_enabled: false genuinely disables reasoning. reasoning_effort sets the top-level effort explicitly; leave it unset to keep the SDK default.

Repo Complexity Policy (complexity_policy)

Section titled “Repo Complexity Policy (complexity_policy)”

The app config may carry an optional repo layer of the complexity policy — the same config source that already carries complexity_overrides. It merges on top of the builtin layer (builtin -> repo); unset sections inherit the lower layer. The resolved policy is pinned into run state for auditability.

complexity_policy:
scope: repo
target_distribution: { low: 0.70, medium: 0.25, high: 0.05 }
max_auto_delta: 1
auto_adjust: true
# Optional: retune the signal thresholds the governor evaluates.
signal_rules:
- signal: multi-issue-linkage
family: context
field: linked_issues
threshold: 2
direction: gte

The base tier chain is unchanged (dispatch > step > "medium", ADR-0005). On top of it the ComplexityGovernor overlays a clamped drift adjustment (effective = clamp(base + delta), |delta| <= max_auto_delta), computed from a typed EvidenceBundle (LLM metrics + human signals + context signals). Agents may request a re-tier via the complexity_review tool; the request requires a populated evidence bundle and a non-blank feeling. The drift check is evaluated against the recorded base tier, so sustained evidence yields the same clamped correction each boundary (no ratchet) and decays once evidence stops. The context signal family (linked_issues, reopened) reads an optional COMPLEXITY_CONTEXT_SIGNALS blob from the intake surface; no call-site publishes it yet, so those rules stay dormant until a publisher lands. See ADR-0012.