YAML schema

A complete reference for every field in coulisse.yaml.

Top-level

agents: [ ... ]               # required, non-empty
auth: { ... }                 # optional; per-scope auth for /v1/* and /admin/*
default_user_id: <string>     # optional, unset by default
experiments: [ ... ]          # optional; A/B test groups over agents
judges: [ ... ]               # optional; empty/omitted = no evaluation
mcp: { ... }                  # optional
memory: { ... }               # optional; defaults to sqlite + hash embedder
providers: { ... }            # required
smoke_tests: [ ... ]          # optional; synthetic-user evaluation runs
telemetry: { ... }            # optional; fmt + sqlite on by default, OTLP opt-in

auth

  • Type: object
  • Optional. Omit to leave both surfaces unauthenticated (fine for local dev, never for anything exposed beyond loopback).

Two independent scopes:

  • auth.proxy guards the OpenAI-compatible /v1/* surface that SDK clients call.
  • auth.admin guards the /admin/* surface (the studio UI).

Each scope is itself optional and accepts the same shape: exactly one of basic or oidc when present. They are mutually exclusive within a scope — the server rejects a scope block that has both or neither. The two scopes are independent, so you can enable Basic on one and OIDC on the other.

auth.<scope>.basic

Static HTTP Basic credentials. Best for local dev or a single-operator deployment.

FieldTypeRequiredDefaultNotes
passwordstringyesNon-empty. Rotate if suspected leaked — there's no token revocation.
usernamestringnoadminNon-empty when set.
auth:
  admin:
    basic:
      password: choose-something-strong
      username: admin

auth.<scope>.oidc

Authorization-code-with-PKCE login against an OIDC-compliant IdP (Authentik, Keycloak, Auth0, Google, etc.). Access control is delegated to the IdP's application policy — Coulisse accepts any successfully authenticated user.

FieldTypeRequiredDefaultNotes
client_idstringyesMust match the client registered at the IdP.
client_secretstringnoRequired for confidential clients (Authentik's default); omit for public clients using PKCE only.
issuer_urlstringyesIdP issuer. For Authentik: https://<host>/application/o/<app-slug>/.
redirect_urlstringyesPublic base URL inside the protected scope. Must be registered as the redirect URI at the IdP. axum-oidc allows every subpath of this URL as a valid redirect.
scopeslist<string>no[email, profile]Extra OAuth2 scopes. openid is added automatically.
auth:
  admin:
    oidc:
      issuer_url:    https://authentik.example.com/application/o/coulisse/
      client_id:     coulisse-admin
      client_secret: <secret>
      redirect_url:  http://localhost:8421/admin/

default_user_id

  • Type: string
  • Default: unset
  • Purpose: fallback identifier for requests that don't supply safety_identifier (or the deprecated user).

Leave it unset for multi-tenant deployments — unidentified requests will be rejected. Set it to something like "main" for local or single-user setups so memory still works whether or not the client bothers to send an id. See User identification.

providers

  • Type: map of provider_kind → provider_config
  • Required. At least one provider must be declared.

Supported keys

anthropic, cohere, deepseek, gemini, groq, openai.

Per-provider fields

FieldTypeRequiredNotes
api_keystringyesProvider API key.
providers:
  anthropic:
    api_key: sk-ant-...
  openai:
    api_key: sk-...

mcp

  • Type: map of server_name → server_config
  • Optional. Omit if you don't use tools.

Server names are arbitrary — they're what agents refer to under mcp_tools.

Common fields

FieldTypeRequiredNotes
transportenumyesstdio or http.

transport: stdio

FieldTypeRequiredNotes
commandstringyesExecutable to run.
argslist<str>noCommand-line arguments.
envmap<str,str>noEnvironment variables for the child.

transport: http

FieldTypeRequiredNotes
urlstringyesStreamable-HTTP MCP endpoint.

Examples

mcp:
  hello:
    transport: stdio
    command: uvx
    args: [--from, git+https://..., hello-mcp-server]

  calculator:
    transport: http
    url: http://localhost:8080

memory

  • Type: object
  • Optional. Omit for defaults (sqlite at ./coulisse-memory.db, offline hash embedder, no auto-extraction).

See Memory configuration for the full walkthrough and examples.

Sub-fields

FieldTypeRequiredDefault
backend.kindenumnosqlite
backend.pathstringno./coulisse-memory.db
embedder.providerenumnohash
embedder.modelstringdependsrequired for openai/voyage
embedder.api_keystringnofalls back to providers.<provider>
embedder.dimsintno32 (hash only)
extractor.providerstringyes*— (* required when extractor is set)
extractor.modelstringyes*
extractor.dedup_thresholdfloatno0.9
extractor.max_facts_per_turnintno5
context_budgetintno8000
memory_budget_fractionfloatno0.1
recall_kintno5

agents

  • Type: list of agent configs
  • Required. At least one agent must be defined.

Per-agent fields

FieldTypeRequiredNotes
namestringyesUnique agent identifier; clients pass this as model.
providerstringyesKey under providers.
modelstringyesUpstream model identifier.
preamblestringnoSystem prompt. Default: empty.
judgeslist<string>noNames of judges (from top-level judges:) that evaluate this agent's replies. Empty = no evaluation.
mcp_toolslist<mcp_tool_access>noTools this agent may use.
purposestringnoTool description when this agent is exposed via another agent's subagents. Omit for standalone agents; add a concrete one-line description when this agent is meant to be called as a specialist.
subagentslist<string>noNames of other agents exposed as callable tools. Each entry must refer to another entry under agents. Self-reference and duplicates are rejected at startup.

mcp_tools entry

FieldTypeRequiredNotes
serverstringyesKey under mcp.
onlylist<str>noAllowed tool names. Omit for full access.

Complete agent example

agents:
  - name: code-reviewer
    provider: anthropic
    model: claude-sonnet-4-5-20250929
    preamble: |
      You are a thorough code reviewer.
    mcp_tools:
      - server: filesystem
        only:
          - read_file
      - server: hello

Subagent example

agents:
  - name: resume_critic
    provider: anthropic
    model: claude-sonnet-4-5-20250929
    purpose: Critique and rewrite a resume for a target role.
    preamble: |
      Given a resume and a target role, return a revised resume
      and a bullet list of the biggest gaps.

  - name: coach
    provider: anthropic
    model: claude-sonnet-4-5-20250929
    subagents: [resume_critic]
    preamble: |
      Delegate resume work to `resume_critic` when relevant.

See Multi-agent routing for the full subagent walkthrough.

experiments

  • Type: list of experiment configs
  • Optional. Omit (or leave empty) to skip A/B testing.

An experiment wraps two or more agents under one addressable name. Clients send the experiment's name in the model field and the router picks a variant per request. Experiment names share the agent namespace — collisions are rejected at startup.

See Experiments for the end-to-end walkthrough.

Per-experiment fields

FieldTypeRequiredDefaultNotes
bandit_window_secondsintno (bandit)604800 (7 d)Bandit-only. Maximum age of scores included in mean-arm computations.
epsilonfloatno (bandit)0.1Bandit-only. Probability in [0.0, 1.0] of routing to a random arm instead of the leader.
metricstringyes (bandit)Bandit-only. judge.criterion to optimise. The judge must declare the criterion in its rubrics, and every variant must opt into the judge.
min_samplesintno (bandit)30Bandit-only. Each arm must accumulate this many scores before exploitation is allowed.
namestringyesAddressable name; must not collide with any agent name.
primarystringyes (shadow)Shadow-only. Variant agent that serves the user. Must be one of variants.
purposestringnoTool description when the experiment is exposed via another agent's subagents:.
sampling_ratefloatno (shadow)1.0Shadow-only. Probability in [0.0, 1.0] that a turn also runs the non-primary variants in the background.
sticky_by_userboolnotrueWhen true, the same user always lands on the same variant (deterministic hash, no DB writes).
strategyenumyessplit, shadow, or bandit.
variantslist<variant>yesNon-empty. Each entry references an agent.

variants entry

FieldTypeRequiredDefaultNotes
agentstringyesName of an agent declared under top-level agents:. Variants must reference concrete agents — nesting an experiment is rejected.
weightfloatno1.0Strictly positive. Normalised against the sum of all variant weights.

Example

agents:
  - name: assistant-sonnet
    provider: anthropic
    model: claude-sonnet-4-5-20250929
  - name: assistant-gpt
    provider: openai
    model: gpt-4o

experiments:
  - name: assistant
    strategy: split
    variants:
      - agent: assistant-sonnet
        weight: 0.5
      - agent: assistant-gpt
        weight: 0.5

judges

  • Type: list of judge configs
  • Optional. Omit (or leave empty) for no automatic evaluation.

Judges are background LLM-as-judge evaluators. An agent opts in by listing judge names in its own judges: field. See LLM-as-judge evaluation for the full walkthrough.

Per-judge fields

FieldTypeRequiredDefaultNotes
namestringyesUnique judge identifier; agents refer to it here.
providerstringyesMust match a key under providers.
modelstringyesUpstream model identifier for the judge call.
rubricsmap<string,string>yescriterion: short description of what to assess. One score row per criterion per scored turn. Must declare at least one entry.
sampling_ratefloatno1.0In [0.0, 1.0]. 1.0 = every turn, 0.1 ≈ 10%, 0.0 = never.

Rubric descriptions should say what to evaluate — don't include scale, JSON, or format instructions. Coulisse forces the output shape internally (integer 0-10 per criterion with a one-sentence reasoning).

Example

judges:
  - name: quality
    provider: openai
    model: gpt-4o-mini
    sampling_rate: 1.0
    rubrics:
      accuracy:     Factual accuracy. Flag hallucinations.
      helpfulness:  Whether the assistant answered the user's question.
      tone:         Politeness and tone.

smoke_tests

  • Type: list of smoke test configs
  • Optional. Omit (or leave empty) for no synthetic-user runs.

Each entry pairs a persona (an LLM that role-plays the user) with a target agent or experiment. Triggered from the studio at /admin/smoke/<name>. See Smoke tests for the workflow.

Per-test fields

FieldTypeRequiredDefaultNotes
namestringyesUnique within smoke_tests.
targetstringyesAgent or experiment name. Resolved per run via the experiment router.
personaobjectyesprovider, model, preamble for the role-played user.
initial_messagestringnoHard-coded first persona turn. Omit to let the persona open the conversation.
stop_markerstringnoSubstring that ends the run when emitted by either side.
max_turnsintegerno10Cap on persona-then-agent pairs per run.
repetitionsintegerno1Independent runs launched per click. Each gets a fresh synthetic user id.

Example

smoke_tests:
  - name: jobseeker_basic
    target: tremplin
    persona:
      provider: anthropic
      model: claude-haiku-4-5-20251001
      preamble: |
        You are a 28-year-old looking for a developer job in Paris.
        Reply like a real human; finish with "[FIN]" once satisfied.
    initial_message: "Hi, I'm looking for work."
    stop_marker: "[FIN]"
    max_turns: 10
    repetitions: 5

telemetry

  • Type: object
  • Optional. Omit and Coulisse runs with stderr fmt logs at info plus the SQLite mirror that drives the studio UI; no external traces.

The block has three sub-sections — fmt, sqlite, and otlp — each independently toggleable. See Telemetry configuration for the full schema and Telemetry & OpenTelemetry for span semantics and OTLP backend integration.

telemetry:
  fmt:
    enabled: true        # default
  sqlite:
    enabled: true        # default; powers the studio UI
  otlp:                  # absent = no external traces
    endpoint: "http://localhost:4317"
    protocol: grpc       # or http_binary
    service_name: coulisse
    headers:
      authorization: "Bearer ${OTEL_API_KEY}"

Validation

On startup, Coulisse checks:

  • Each present auth scope (proxy, admin) declares exactly one of basic or oidc.
  • auth.<scope>.basic.password and auth.<scope>.basic.username are non-empty.
  • auth.<scope>.oidc.client_id, issuer_url, and redirect_url are non-empty.
  • There is at least one agent.
  • Agent names are unique.
  • Every agent's provider is configured.
  • Every referenced MCP server is configured.
  • Every name in subagents refers to a defined agent or experiment.
  • No agent lists itself under subagents.
  • subagents entries are unique within an agent (no duplicates).
  • Experiment names are unique and do not collide with any agent name.
  • Each experiment declares at least one variant.
  • Each variant references a defined agent and has a strictly positive weight.
  • Variant agents within an experiment are unique.
  • Strategy-specific fields are only set on the matching strategy (e.g. primary only on shadow, metric only on bandit).
  • For shadow: primary is set and matches one of the variants; sampling_rate is in [0.0, 1.0].
  • For bandit: metric is judge.criterion; the judge exists, declares the criterion in its rubrics, and every variant opts into the judge; epsilon is in [0.0, 1.0].
  • Every referenced judge exists.
  • Judge names are unique.
  • Every judge's provider is configured and supported.
  • Every judge has at least one rubric.
  • Every judge's sampling_rate is in [0.0, 1.0].

Any violation fails fast with an error message that names the offending agent or judge and field.