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.
The 3-Layer Model
Section titled “The 3-Layer Model”┌─────────────────────────────────────────────────────────────┐│ 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.
The Default Workflow
Section titled “The Default Workflow”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: defaultWhen 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).
Provider Schema
Section titled “Provider Schema”| Field | Type | Required | Description |
|---|---|---|---|
api_url | string | yes | Chat-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_env | string | yes | Name of the environment variable holding the API token. |
cache_key_param | string | no | Name 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"LLM Schema
Section titled “LLM Schema”| Field | Type | Description |
|---|---|---|
model_path | string | Model identifier on the provider (e.g., openrouter/anthropic/claude-sonnet-5). |
provider | ${ref} | Provider entry from the providers: layer. |
default_temperature | float | Base temperature for the model. |
input_price / output_price | float | Cost per 1M tokens (USD) — seeds metrics/cost accounting. |
cache_read_price / cache_write_price | float | Cost per 1M tokens (USD) for cached prompt tokens; falls back to input_price when unset. |
thinking_enabled | bool | Enables the model’s thinking mode (e.g., Qwen flexible thinking). false strips all reasoning-enabling request params. |
preserve_thinking | bool | Keeps thinking content across turns. |
reasoning_effort | string | Explicit top-level reasoning effort (none/low/medium/xhigh); unset keeps the SDK default. |
max_output_tokens | int | Max output tokens per completion. |
top_p / top_k / min_p | float/int | Sampling parameters (see table below). |
presence_penalty / repetition_penalty | float | Repetition control (see table below). |
stop_sequences | string[] | 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
*_pricefields 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 toinput_price.The issue-close
agent-metricsjob additionally fills entries whose recorded cost is still0at 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*_priceprecedence 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 estimationblock under the summary table listsInput price ($/1M),Output price ($/1M)andCache 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 tiersInput price ($/1M),Output price ($/1M)andCache 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 theCache hit price ($/1M)column shows that input price rather than—.
Model pricing & cost estimation
Section titled “Model pricing & cost estimation”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.015These 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
*_pricefields, completed from the default price table where unset. - Report time — the issue-close
agent-metricsjob estimates zero-cost entries with the same merge: the price table first, then the agent config’s*_pricefields as fallback (loaded fromdefaults/agents/or the path pinned byAGENT_CONFIG_PATH). An estimated cell carries the*marker with the footnote, and the foldedPrice details used for the estimationblock 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:
| Parameter | Type | Description | Typical range |
|---|---|---|---|
top_p | float | Nucleus sampling — lower = more deterministic | 0.1–1.0 |
top_k | int | Limits the candidate token pool | 10–100 |
min_p | float | Filters tokens below an absolute probability threshold | 0.0–1.0 |
presence_penalty | float | Discourages repeating tokens based on presence | -2.0–2.0 |
repetition_penalty | float | > 1.0 discourages repetition | 1.0–1.5 |
Note:
presence_penaltymust match the model’s operating mode:0.0for thinking-mode operation, ~1.5for instruct-only (per official Qwen model card). PR #225 originally set 1.5 on the Qwen entries while they ran withthinking_enabled: true, i.e., under the wrong regime.
Example:
llms: claude-sonnet-5: model_path: "openrouter/anthropic/claude-sonnet-5" provider: "${providers.openrouter}"Agent Schema
Section titled “Agent Schema”| Field | Type | Description |
|---|---|---|
model | ${ref} | LLM entry from the llms: layer. |
role | string | One-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. |
temperature | float | Per-agent temperature override (floor for complexity adjustment). |
prompt_file | string | Path 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. |
skills | entry[] | 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_overrides | mapping<low | medium | high, object> | Per-tier partial overrides of model/sampling/reasoning. See Complexity Overrides. |
tools | string[] | Tool whitelist: terminal, file_editor, task_tracker (SDK tools) plus think, finish, dispatch_next (built-in defaults). See agents-and-skills.md. |
mcp_servers | object[] | 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_budget | object | Compaction / 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
Section titled “Token Budget”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".
| Field | Type | Default | Description |
|---|---|---|---|
compaction_max_size | int | null | 240 | Event count that triggers compaction. |
compaction_max_tokens | int | null | 80% of the route input limit | Token count that triggers compaction. |
compaction_keep_first | int | 4 | Leading events compaction always preserves. |
compaction_model | string | null | agent’s model | Summarizer model (default shares the conversation’s prompt cache). |
prompt_profile | full | lean | full | lean trims per-call tool/prompt overhead. |
require_batched_recon | bool | false | Require 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: trueMCP Servers
Section titled “MCP Servers”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’sname. - 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:
| Field | Type | Description |
|---|---|---|
name | string | Stable handle for the server (must equal its catalog key). |
command | string | Executable to launch (npx, uvx, docker, or a binary path). |
args | string[] | Command arguments (default []). |
env | mapping<string, string> | Child-process env var → name of the OS env var holding the value (never the literal secret). |
transport | string | Always 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" - "--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.
${ref} Cross-Layer Resolution
Section titled “${ref} Cross-Layer Resolution”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"Full Annotated Example (Abbreviated)
Section titled “Full Annotated Example (Abbreviated)”# ── 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.
Runtime Override
Section titled “Runtime Override”Per-invocation dispatch fields override the YAML cascade. Precedence (highest first):
- Per-invocation — dispatch fields in the
AGENT_RUN_CONFIGsentinel (model,temperature,top_p,top_k,min_p,presence_penalty,repetition_penalty,complexity). - Complexity tier override —
agents.<name>.complexity_overrides.<tier>for the dispatchcomplexitytier (low/medium/high). See Complexity Overrides. - Per-agent —
agents.<name>stanza inagent-config.yaml. - Per-LLM —
llms.<name>stanza inagent-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.
Dynamic Complexity-Based Temperature
Section titled “Dynamic Complexity-Based Temperature”When complexity is set in the dispatch and no explicit temperature override
is provided, temperature is auto-adjusted:
| Complexity | Temperature |
|---|---|
low | 0.0 (most deterministic) |
medium | 0.1 |
high | 0.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.8Precedence is per-invocation (CONFIG_* / AGENT_RUN_CONFIG) > tier override
per-agent > per-LLM. A tier
temperaturereplaces the agent’s base temperature, and the per-tier floor from Dynamic Complexity-Based Temperature is then applied on top withmax(); so ahighoverride of0.1still resolves to0.3.reasoningonly 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: gteThe 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.