← The record

felix

A self-hostable agents harness. Agents are YAML, not code, and the runtime they compile into is governed by default.

Year
2026
Language
Python 3.14
Licence
Apache-2.0
Release
v0.2.0
Status
Early

What it is

An agent is authored as a felix/v1 YAML manifest and compiled into a running, governed agent: durable fibers, memory, skills, evaluation, approvals and sandboxes, all declared rather than wired. Changing what an agent does is a config change.

The same agent is served over four surfaces at once. A REST and SSE endpoint, an OpenAI-compatible /v1 where the manifest name is the model id, A2A JSON-RPC for agent-to-agent calls, and MCP. A client that already speaks any one of those needs no adapter.

It is the third version of this idea. The first ran on AWS Bedrock and LangGraph, the second was TypeScript on Cloudflare Workers with an agentic commerce layer on top. This one dropped both the edge runtime and the commerce layer in exchange for running anywhere the operator controls.

Architecture

Felix request and execution pathsA client reaches felix-api over one of four surfaces: REST and SSE, an OpenAI-compatible v1 endpoint, A2A JSON-RPC, and MCP. The API resolves a YAML manifest through the harness package, which owns patterns, tools, session strategy, governance and auth. Durable runs are enqueued to a Taskiq worker; a separate scheduler enqueues cron tasks. All three processes share Postgres with pgvector as the system of record, Valkey as cache and queue transport, and a pluggable object store backed by the filesystem, S3 or GCS.clientREST / SSEOpenAI-compatible /v1A2A JSON-RPCMCPfelix-apiCPython 3.14 · Granian · FastAPIpackages/harnessmanifests · patterns · tools · session · governance · authworkerTaskiq consumerschedulercron enqueuePostgreSQL+ pgvector · system of recordValkeycache · queue transportobject storefs | S3 | GCSevery dependency reached through a Protocol, not a vendor SDKso the same code runs on a filesystem-only VM, on AWS, or on GCP
Three processes share one datastore triple. The scheduler is separate from the worker on purpose: without it running alongside, nothing periodic fires at all, which is the kind of failure that looks like a bug in the agent rather than a missing process.

A manifest

manifests/governed.yamlfelix/v1
apiVersion: felix/v1
kind: Agent
metadata:
  name: governed
spec:
  model:
    id: claude-sonnet
    thinking_budget: 4096
  session:
    strategy: compacting # or windowed:N, semantic:N, full_replay
  execution:
    mode: durable # 202 + resume_token; Temporal optional
  memory:
    capture: true
  mcp_servers: # bound in as server__tool
    - id: search
      url: https://mcp.internal/mcp

The parts that were actually hard

  • A run that dies mid-tool leaves a tool call with no result, and nothing outside the tool can tell whether the effect landed. The call is closed out with an interrupted result before the thread resumes, because a provider rejects the whole transcript over one unanswered call. Tools declare whether they are safe to re-run and the default is that they are not: re-running a search costs latency, re-running a payment charges twice.
  • Extended thinking is stateful once tools are involved. The provider signs each thinking block, and a later turn replaying a tool call has to replay the signed reasoning that produced it. Blocks are captured off the response and replayed ahead of the tool_use blocks. A block whose signature was not captured is dropped rather than sent, because an unverifiable signature rejects the entire turn.
  • Side requests poison the prompt cache. Compaction, memory extraction, inbound screening and branch summarisation each carry a completely different prefix. Sharing the conversation cache identity would churn the cached prefix the next real turn would have hit, and write an entry nothing ever reads. They opt out.
  • An unknown model id has to fail in two directions at once. The request shape assumes the current generation, because sending a parameter a model has removed is a hard 400 while omitting an optional one is not. The context window stays conservative, because over-advertising a window invites a request the model will reject.
  • A dropped stream is only partly recoverable, and the docs say so. Structural SSE frames carry an id cursor, so a reconnect can replay what was missed and tail the thread. Token-level frames deliberately do not, which per the SSE spec leaves the client on the last structural id. The run itself is still torn down on disconnect: what comes back is the thread, not the abandoned turn.

Where it actually is

Released Apache-2.0 at v0.2.0 in August 2026, with CI, a Helm chart, deploy notes for AWS and GCP, and a docs site. Around 290 Python modules and 97 test files.

It is not battle-tested and this page will not claim it is. The TypeScript version is the one that ran longest. What this version has is a smaller dependency surface and no cloud it cannot leave.

Ask about it

The assistant on the home page answers from this same record, and will tell you what is not on it.

Ask the recordTalk to me