Skip to content

Concepts

Felix is a managed agents harness you run yourself. You describe an agent declaratively; Felix compiles that description into a governed, observable, resumable runtime and serves it over several protocols at once.

The through-line is: the manifest is the program, and everything else is machinery that enforces it.

Manifest

A YAML or JSON document with apiVersion: felix/v1 and kind: Agent. The schema is Pydantic v2 with extra="forbid", so an unknown key is a hard error rather than a silently ignored typo.

A manifest declares everything an agent needs: pattern, model, system prompt, tools, skills, MCP servers, A2A peers, client tools, memory backend, session strategy, auth requirements, policies, limits, guardrails, screening, approvals, and framework governance.

apiVersion: felix/v1
kind: Agent
metadata:
name: quick
spec:
pattern: react
model:
id: claude-sonnet
tools: [calculator]

Every field is catalogued in the Manifest reference.

Where manifests come from

The request-path resolver walks four layers and returns the first hit:

  1. Tenant Postgres — the tenant’s active pointer (source: tenant_postgres)
  2. Tenant object storemanifests/{tenant_id}/{name}.json (source: tenant_object)
  3. Global object storemanifests/{name}.json, affecting every tenant (source: global_object)
  4. Bundled — the YAML shipped in the image (source: bundled)

So a tenant manifest named quick shadows the bundled quick for that tenant alone. Active pointers are cached for 30 seconds. Eight manifests ship in the box: quick, deep, router, oss-only, hybrid-router, support, cowork and governed.

Writes go through the Management API, which is append-only and version-pinned: publishing inserts a new version row and flips a pointer, and rolling back flips the pointer again rather than rewriting content.

Canary rollouts

An active pointer can carry a canary_version and a canary_weight. Selection is deterministic, not random — the harness hashes tenant, thread, manifest name and both version numbers, then takes that modulo 100. A thread therefore stays on one side for the whole rollout instead of flapping between versions mid-conversation.

Compile pinning

With spec.governance.pin_compile, the manifest version a thread started on is frozen for that thread. If the active pointer moves underneath a live conversation, the next request fails with 409 rather than silently switching behaviour mid-run.

Tenant

Every row Felix writes is scoped to a tenant id, taken from the authenticated principal — never from a request field, so a caller cannot address another tenant’s data. Under FELIX_AUTH_MODE=none everything is the anonymous principal in tenant default.

Postgres row-level security is available as opt-in defence in depth. The policies themselves are not optional to install — 0006_tenant_rls is part of the migration chain, so any database at that revision or later has them. FELIX_DATABASE_RLS is the runtime half: set it true and the tenant id is applied per transaction and the policy enforces it; leave it false and Felix declares a bypass, so the policies are inert and query-layer scoping remains the isolation.

One caveat is worth checking rather than assuming, because it fails silently in the direction that matters: the policies are FORCEd, and a superuser or BYPASSRLS connection skips them entirely — so RLS can be switched on and enforcing nothing while everything appears to work. felix doctor reports whether it is actually in effect.

Thread

A thread is one conversation. Its id is always {tenant_id}:{suffix}, where clients supply only the suffix — a suffix containing : or # is rejected with 400 invalid_thread_id.

Session

The session is the append-only event log behind a thread. It lives outside the model’s context window: the log is the durable record, and a strategy decides what the model actually sees each turn.

Event kinds: message, tool_call, tool_result, thinking, audit, compaction, model_change, thinking_level_change, branch_summary, custom, label, session_info.

Strategies (spec.session.strategy):

Strategy Behaviour
full_replay Send the whole transcript. The default.
windowed:N Send the last N events.
summarizing:N Summarize everything older than the last N.
semantic:N Retrieve the N most relevant events (needs the embeddings extra).
compacting Compact on a token threshold, keeping keep_recent_tokens intact.

Because the log is authoritative and server-side, a client is a view, not the source of truth. It hydrates from GET /chat/sessions/{id}, whose snapshot carries the transcript, phase, thinking level, leaf, lease state and any queued steering.

A session moves through phases idle, turn, compaction, branch_summary, retry and aborted.

Branching

A thread is a tree, not a line. Fork copies a session into a new thread up to a chosen event. Rewind moves the active leaf back to an earlier event in the same thread; unless you opt out, the abandoned branch is summarized into a branch_summary event so the model does not lose what happened there.

Leases

Two browser tabs on one session would race. Each client mints a holder id and takes a lease — exclusive (reported as locked) or shared (reported as attached). Contention returns 409. Leases are Redis-backed, so they hold across replicas.

Pattern

The pattern is the control loop. Felix ships seven:

Pattern Kind Behaviour
react single Think → call tools → observe → repeat. The workhorse.
deep single Plans first, records steps as a plan document, then executes.
reflect single Generates, critiques against criteria, and retries below threshold.
plan_execute single Separate planner and executor models.
router multi Classifies the request and delegates to one sub-agent.
parallel multi Fans out to sub-agents and aggregates.
groupchat multi Sub-agents converse to a joint answer.

The registry is open — register_pattern(name, builder) adds your own, and nothing in core enumerates the built-in list. Multi-agent patterns require sub_agents and reject the direct outbound blocks (peers, containers, queues, sandboxes, browser_tools); a sub-agent’s own manifest declares those instead. See Patterns.

Skill

A SKILL.md file with YAML frontmatter, living under skills/<name>/. The frontmatter can fold extra tools, MCP servers and peers into any manifest that lists the skill; the body is appended to the system prompt under an ## Active Skills header.

Activation is per-tenant and restriction-only: a tenant overlay can disable skills the manifest declares, but can never enable ones it did not. Agents can manage their own overlay through list_skills, activate_skill and deactivate_skill.

Tool

Every tool, wherever it runs, is compiled to the same interface and tagged with a transport that shows up in audit rows:

Transport Runs where Declared by
local In the API process spec.tools
mcp A remote MCP server spec.mcp_servers{server}__{tool}
a2a Another Felix peer spec.peerspeer__{name}
container An HTTPS container gateway spec.containers
sandbox A Docker sandbox (sandbox extra) spec.sandboxes
browser Playwright (browser extra) spec.browser_tools
queue A separate consumer, via a Redis list spec.queues
client The caller’s own machine spec.client_tools

Tool failures come back as strings the model can read and react to, prefixed [error/invalid_arguments], [error/timeout], [error/blocked] or [error/internal]. A ref with fatal: true aborts the batch instead.

Client-executed tools

spec.client_tools inverts the usual direction: the harness emits a tool_request frame and blocks, and the browser or CLI runs the tool and answers with POST /chat/tool_result. This is a real round trip inside the model loop — a client that never answers stalls the run until the tool’s timeout. It is how the web clients run local shell commands and open local files against a workspace the server cannot see.

Queue tools

A queue tool pushes onto a Redis list and returns immediately; a separate consumer does the work and writes the result back with POST /internal/sessions/{session_id}/events, authenticated with X-Felix-Consumer-Secret. Use it for work that outlives a request.

Governance

Every tool is cloned through a fixed wrapper stack before the model can call it. The order is load-bearing and enforced by tests:

secret masking → policies → command screening → content screening → limits → guardrails → judges → approvals → artifact spill

Layer Manifest block Question it answers
Secret masking Is a resolved credential about to leak into output?
Policies spec.policies Does the caller hold the scopes this tool requires?
Command screening spec.command_screening Is this command allowed, denied, or does it need a human?
Content screening spec.content_screening Is this tool output trying to hijack the model?
Limits spec.limits Has this run exceeded its budget?
Guardrails spec.guardrails Does the text match a blocked pattern (e.g. PII)?
Judges spec.guardrails.judges Does a model score this as acceptable?
Approvals spec.approvals Has a human said yes?
Artifacts spec.artifacts Is this output too large to put in context?

A denial is stamped with a module-private marker, so a model cannot forge one by emitting a convincing-looking string. Governance fails closed — a missing store or an internal error denies rather than allows. See Governance.

spec.governance.frameworks additionally maps a manifest onto soc2 or eu_ai_act requirements and refuses to compile without the corresponding controls. That is a compile-time check, not a certification.

Memory

Two independent things share the word.

Session memory is the transcript, above — the checkpointer (spec.memory.checkpointer, Postgres) plus a strategy.

Durable memory is facts that outlive a thread. With spec.memory.capture.enabled, a small model extracts facts from each turn into memory_vectors (pgvector, HNSW) under kinds fact, preference, episode and procedural. spec.procedural_memory recalls stored how-tos and adds a remember_procedure tool. Semantic recall and tools_retrieval need the embeddings extra.

Durable execution

By default a run lives and dies with the request. With spec.execution.mode: durable, POST /chat enqueues a fiber and returns 202 with a resume_token; the fiber checkpoints into Postgres and the worker’s fiber_scheduler cron resumes it, so a process restart does not lose the run. Poll GET /chat/runs/{resume_token} for the result.

Setting FELIX_DURABILITY=temporal additionally drives a Temporal workflow on the felix-fibers task queue, falling back to the fiber scheduler if that fails.

POST /chat/stream ignores execution.mode: durable and always runs inline.

Steering

A run is not a black box once it starts. POST /chat/steer with kind: "steer" interrupts the remaining tool batch mid-run; kind: "follow_up" queues a message delivered once the run goes idle. POST /chat/abort stops it, and POST /chat/continue picks it back up with no new user message. The queues are Redis lists, so any replica can accept the steer.

Interrupts

Three things pause a run until something answers out of band: an approval_required frame waiting on a human decision, a tool_request frame waiting on the client, and a ui_request frame asking the user to choose, confirm or type something. Each has a timeout and fails closed. See sticky interrupts.

Surfaces

The same compiled agent is reachable four ways, which is the point of compiling from a manifest rather than wiring a framework by hand:

Surface Entry point
Native REST + SSE POST /chat, POST /chat/stream
OpenAI-compatible POST /v1/chat/completions (model = manifest name)
A2A JSON-RPC POST /a2a
MCP POST /mcp

Auth

Three modes, set by FELIX_AUTH_MODE:

  • none — everything is anonymous in tenant default, and management scope checks are skipped. Legal only on a loopback bind; the harness refuses to start otherwise, in every environment.
  • api_keyAuthorization: Bearer, Authorization: ApiKey or X-Api-Key, compared in constant time against FELIX_AUTH_API_KEYS, which supplies the subject, tenant and scopes.
  • jwt — verified against FELIX_JWT_VERIFIERS and FELIX_JWKS_PUBLIC. Mint tokens locally with felix mint-jwt.

A manifest adds its own inbound requirements through spec.auth.inboundallow_anonymous and required_scopes are enforced per request. Management routes layer scopes on top; see Scopes. A second credential, X-Felix-Consumer-Secret, gates POST /internal/*.

Details in Auth.

Observability

Prometheus metrics at GET /metrics, counters prefixed felix_*. Audit rows are buffered by the process that emits them and flushed to Postgres on a timer plus a drain on shutdown — the agent loop runs in the API process, so the API flushes too, not only the worker. Optional OTLP tracing with FELIX_OTEL_ENABLED, and an optional append-only analytics spill (FELIX_WAREHOUSE: DuckDB, ClickHouse or Doris).

See Observability.

Request lifecycle

Putting it together, one POST /chat:

  1. Middleware — request id, then body limit (1 MiB), then per-tenant rate limit, then authentication. All four are pure ASGI, so a streamed response is not copied between layers.

  2. Resolve — walk the four manifest layers, pick the canary variant, check the compile pin.

  3. Authorize — apply spec.auth.inbound; screen the inbound user turn.

  4. Compilebuild_agent resolves the prompt and sub-agents, binds outbound tools, wires skills and memory, and wraps every tool in the governance stack.

  5. Run — the pattern loop calls the model and dispatches tools, emitting audit rows and stream frames, and pausing on any interrupt.

  6. Persist — append events to the session log; buffer audit and usage for flush.

For the mechanism at each step, start at Architecture.