felix
A self-hostable agents harness. Agents are YAML, not code, and the runtime they compile into is governed by default.
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
A manifest
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/mcpThe 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.