Ever-evolving prompting and context engineering for LLM agents through active memory and result analysis.
Project description
fabri
The self-improving agent engine you build products on. Most agent frameworks ship prompts that never change. fabri's agents mine their own run traces into memory, so they stop repeating their mistakes — measurably fewer steps and lower cost on tasks they can learn from (benchmarks).
Evidence snapshot
Fabri publishes completed measurements and leaves pending work visibly pending:
| Evidence | Result | Scope |
|---|---|---|
| Company setup qualification | Support HQ passed a small-sample 3/3 gate, but a follow-up 10-replica confirmation scored 9/10 (~90%) — the larger sample overturned the small-sample pass, so it does not clear the 100% reliability bar. Reliability Labs (2/3) and Revenue Ops (0/3, plus 1 truncation failure) also did not qualify. | At adequate sample size, none of the three evaluated company setups clears the 100% reliability bar; it is not a memory-benefit result |
| Offline retrieval evaluation | Shipped hybrid retrieval reached 0.938 recall@5 and 0.844 MRR, versus dense at 0.792 and 0.790 | Deterministic, labeled retrieval fixture; no live model |
| Session-N+1 recovery pilot | Steps 5 → 4, guideline reuse 0% → 67%, cost ↓7.8% | Six-run constructed task on gpt-4o-mini; not a general workload claim |
| LongMemEval | Runner shipped; result pending | No score claimed yet |
See BENCHMARKS.md for reproduction commands and caveats, the live benchmark page for the latest run, and the reviewed results for per-replica costs, failed hypotheses, and the release decision: Support HQ 3-replica, Support HQ 10-replica confirmation, Reliability Labs, and Revenue Ops.
The benchmarks/ directory holds the reproducible inputs behind
those numbers: datasets/ (frozen fixtures + protocol), fixtures/, reviewed
results/, and agreement/ (blind-labeling tooling for scorer agreement). See
benchmarks/README.md for the runners and what's
public versus provisional.
fabri is split into two layers: an engine (a frugal agent loop, per-role LLMs, polyglot tools, and a memory loop that grows the prompt from the agent's own traces) and a builder that turns intent into a running agent fast. Read docs/vision.md for the full why.
fabri is open source under the Apache License, 2.0. Install it from PyPI, build agents with it, embed it in a hosted product, fork it — no revenue threshold, no commercial license required. The core (agent loop, memory pipeline, orchestration, config surface) is open to contribution too — see CONTRIBUTING.md for how core changes get reviewed.
Philosophy
An agent's prompt should not be written by hand and frozen. It should grow from what the agent actually does.
┌──────────────────────────┐
│ task arrives │
└────────────┬─────────────┘
│
▼
┌───────────────────────────────────────────┐
│ retrieve relevant guidelines from memory │
│ (top-k by similarity, plus tool-tagged │
│ hits guaranteed when a tool is named) │
└────────────────────┬──────────────────────┘
│ injected into system prompt
▼
┌────────────────┐
│ agent loop │ ── tool calls ──▶ subprocess tools
│ (ReAct) │ ◀── results ────
└────────┬───────┘
│ JSONL trace
▼
┌───────────────────────────────────────────┐
│ analyze trace: compress each failure │
│ into a short, generalized guideline │
└────────────────────┬──────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ dedup vs existing tactical guidelines │
│ → near-duplicate? bump recurrence count │
│ → recurred across N sessions? promote │
│ from tactical to strategic │
└────────────────────┬──────────────────────┘
│
▼
back into the memory store,
retrievable on the next task
A failure in session N becomes retrievable context in session N+1, without anyone editing the prompt by hand. That loop — trace → analyze → compress → dedup → promote → retrieve — is the heart of the engine. On top of it, a builder layer (ideator, tool-writer, prompt-kit, skills, service) scaffolds a new product onto the engine so building one is faster, not slower — see docs/vision.md and Track B in docs/ROADMAP.md.
Memory isn't a flat bag of guidelines. Entries can be classified into
severity tiers (core / retrieve / quarantine) — a quarantined entry
(contradicted, or an unverified generic pattern) is excluded from retrieval
unconditionally, whether or not tiering is turned on. On top of the
retrieval loop above, fabri also has an experimental, default-off
action channel (ActionMemory) that lets a recovered failure — e.g. a token
cap that was too low — propose a fail-closed, allowlisted config fix instead
of just a retrievable note. It's shadow-only (proposals logged, nothing
applied) unless explicitly opted into. Status, honestly: the mechanism has
been live-demonstrated recovering a real truncation failure, but its benefit
over a frozen control is not yet proven, and a related idea — a hard
AGENT_MEMORY output contract — was reverted in 0.19.4 after it didn't hold
up. Tiering and ActionMemory are both off by default; see the memory:
config block below. For the full mechanics — retrieval fusion, mining,
dedup, promotion, tiering, and the ActionMemory allowlist — see the deep
dive in docs/memory.md.
Two operating principles fall out of that:
- Context over prompt. Keep retrieved context compact and just-in-time. Each tool gets one clear job. Tool results enter the context in a compact TOON encoding, not raw JSON.
- Polyglot tools behind a uniform contract. A tool is a JSON manifest next to an executable in any language. Stdin gets JSON args, stdout returns JSON, the runner normalizes errors. Agents can be composed as tools of other agents through the same contract.
Frugality by default
A token spent is a token billed. The base system prompt steers every run toward fewer, better-aimed actions, and the defaults make the cheap path the default path:
- Be sure before you call. The agent states what it expects a call to return before making it; if it can already act, it acts instead of probing. One decisive call beats many exploratory ones — every round-trip re-sends the whole context. (TALE, arXiv:2412.18547.)
- Single-threaded by default; delegate as the exception.
spawn_subagentre-runs the entire loop, so it's reserved for subtasks that are independent, parallelizable, and large enough to overflow the parent's context — never sequential steps, never "because the tool exists." A multi-agent run costs ~15× a single agent; coordination is a top failure source. (Anthropic, Building a multi-agent research system; Cognition, Don't Build Multi-Agents.) - Code as action. When a job needs several operations, the agent does
them in one
python_execscript (or onebatchcall) that branches over the results, instead of narrating each step as its own tool call. (CodeAct, arXiv:2402.01030: −30% steps; smolagents: −28% tokens.) - Surgical edits, windowed reads, prompt caching, TOON results. Prefer
edit_fileover whole-file rewrites; read only the slice you need; the static system+tools prefix is cached; tool results enter context in compact TOON.
Every run emits a usage trace event carrying token totals and
cost_usd (priced per model), plus subagent_cost_usd and total_cost_usd
— the end-to-end cost of the run and its whole sub-agent subtree — so a host
service can track COGS without parsing logs. See fabri.pricing.
Run it in your repo — self-improving, in one file
Easy setup is a design goal, not an afterthought. fabri agents learn from their
own runs (a lesson that recurs across ≥3 sessions is promoted to a permanent
guideline), and fabri repo surfaces that learning as a reviewable GitHub
issue or PR, right in your repo — with only the built-in GITHUB_TOKEN, no
model API key, no server to run.
Drop one workflow file in and the agent proposes its own prompt improvements on a schedule:
# .github/workflows/self-improve.yml
permissions: { issues: write }
jobs:
suggest-prompt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install "fabri[sqlite]"
- run: fabri repo suggest-prompt --config agent.yaml
env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }
Live reference: Rushour0/fabri-repo-demo
runs this weekly — see its issues
for what the agent learned from its own runs. Full guide:
docs/repo-agent.md.
Install
pip install fabri # lean base install; agencies run without memory learning
pip install "fabri[self-improving]" # ONE command: memory + hybrid retrieval so agencies learn
# from their own runs and avoid repeat mistakes — local sqlite-vec store, no docker
export ANTHROPIC_API_KEY=...
Or, to skip docker entirely (in-process sqlite-vec memory backend):
pip install 'fabri[sqlite]'
export ANTHROPIC_API_KEY=...
fabri --config configs/example.yaml run "your task" # or `fabri init` to scaffold
See configs/ for the canonical example and benchmark configs,
and BENCHMARKS.md for the methodology + how to reproduce
published numbers.
Default provider is Google Gemini (lowest cost + generous free tier): a
fresh fabri run needs only GEMINI_API_KEY. All provider SDKs ship by
default — no extra install — so switching is a one-line config change:
- Gemini (default):
llm.provider: gemini, exportGEMINI_API_KEY - Anthropic / Claude:
llm.provider: anthropic, exportANTHROPIC_API_KEY - OpenAI:
llm.provider: openai, exportOPENAI_API_KEY - OpenRouter:
llm.provider: openrouter, exportOPENROUTER_API_KEY - AWS Bedrock:
llm.provider: bedrock, setllm.aws_region(orAWS_REGION). Credentials come from the standard AWS chain — env keys, a shared profile, an IAM role, or a Bedrock API key (AWS_BEARER_TOKEN_BEDROCK) — so there's noapi_key_env. One Bedrock backend serves every Converse-capable model: Claude (us.anthropic.claude-…), OpenAIgpt-oss(openai.gpt-oss-…), Llama, Mistral, etc. (Gemini is not on Bedrock — use thegeminiprovider for that.)
Provider can be set per-role too (e.g. a Gemini Pro orchestrator with a
Flash-Lite narrator), so a run can mix vendors. Note that switching the parent
llm.provider repoints every model-only role (one that sets just a model id)
onto the new provider — give such roles their own provider, or set them to
null, when the model id isn't valid on the new provider.
Embeddings run locally via sentence-transformers/all-MiniLM-L6-v2 —
no embedding API calls.
Quickstart
Scaffold a multi-agent agency, run it on a task, and watch it learn — no docker:
pip install "fabri[self-improving]" # memory loop on; local sqlite-vec, no docker
export OPENAI_API_KEY=... # the bug-crew template ships OpenAI configs
fabri new agency demo # scaffolds a bug-triage crew: triager → fixer → tester
fabri --config demo/agent.openai.yaml run "triage and fix the failing test in workspace/"
# or pull an agency from a catalog instead of the bundled template:
fabri new agency demo-rosters --from gh:Rushour0/fabri-rosters/agencies/bug-triage-crew
# or watch it live in the browser:
fabri serve --config demo/agent.openai.yaml
fabri studio
--from takes a local directory path or a gh:owner/repo/subpath[@ref]
GitHub reference; fabri-rosters is
the catalog repo of ready-made agencies, and its gallery is hosted at
fabri.rushour0.com.
Each run mines guidelines from its own trace, so the crew avoids its own repeat
mistakes on the next run. Prefer a single agent? fabri init demo scaffolds one
(an agent.yaml, an example tool under tools/agent_tools/, and a
docker-compose.yml) — you edit those, not the library.
fabri studio serves the bundled UI with four surfaces: Conversation (a
live thread with follow-ups), Company (an org chart with live per-agent
handoffs and COGS), Fleet (parallel batch runs with rolled-up status and
summed COGS), and History (a persisted, read-only run index). See
examples/studio/README.md for details.
Commands
fabri run "some task description"
fabri --config agent.yaml run "..." # config-driven agent
fabri --verbose run "..." # DEBUG logging to console
fabri inspect-memory "a query" # test retrieval
fabri ingest-traces <session-id> # re-mine a past trace
fabri ingest app.log --adapter regex # mine an EXTERNAL log (the Improver)
fabri ingest --list-adapters # available log adapters
Each run returns an outcome: success, success_with_recovery
(finished but a tool call failed along the way), or incomplete (hit
the step limit).
Every run writes two records keyed by session_id:
.fabri/traces/<session_id>.jsonl— machine-readable trace used by the memory pipeline..fabri/logs/<session_id>.log— always DEBUG-level, with LLM call latency/token usage, tool dispatch latency, and every dedup / promotion decision.
Both land under .fabri/ in the directory you run from (override with
$FABRI_HOME). Add .fabri/ to your project's .gitignore.
Getting the most out of memory: see docs/using-fabri-well.md for the cross-run learning loop, backend choice, reading the
retrievaltrace, and memory hygiene. For how fabri's memory compares to other agent-memory projects (Hermes Agent, OpenClaw) and where it's headed, see docs/design/external-memory-patterns.md.
Configuring an agent
Every field has a default, so you only override what you need:
agent:
name: my-agent
max_steps: 10 # loop budget; raise for multi-tool tasks
output_format: json # what the model is asked to emit (decompose):
# json (reliable) or toon (fewer output tokens)
response_schema: null # O1: a JSON Schema for typed output. When set,
# the final answer is validated against it and
# the validated value is returned as
# `structured_output`; a mismatch re-prompts the
# model (response_retries), then error_strategy
# (strict | warn | fallback) resolves it.
llm:
provider: gemini # gemini / anthropic / openai / openrouter / bedrock
model: gemini-2.5-pro
max_tokens: 1024
api_key_env: GEMINI_API_KEY
# AWS Bedrock only (provider: bedrock): no api_key_env — creds come from the
# AWS chain; just set the region. See configs/bedrock.yaml for a full example.
# aws_region: us-east-1
tools:
manifest_dir: # one path or a list, merged into one registry
- builtin # bundled tools (read_file/write_file/...)
- tools/agent_tools # your project's own tools, relative to cwd
enabled: [read_file, write_file] # null = every discovered tool
sandbox_root: project # read_file/write_file refuse paths outside
result_format: toon # how tool results enter the model's context:
# toon (fewer input tokens) or json
decompose:
enabled: false # turn on for research-shaped tasks
max_subquestions: 5
mcp_servers: # optional: pull tools from MCP servers
- name: fs # stdio transport
command: ["npx", "@modelcontextprotocol/server-filesystem", "/srv/data"]
- name: web # http transport
url: "https://mcp.example.com/jsonrpc"
headers: {Authorization: "Bearer ..."}
# Remote tools are wrapped as `mcp_<server>_<remote_tool>`. A server
# that fails to start is logged and skipped, not fatal.
memory:
collection: my_fabri # separate Qdrant collection per agent
qdrant_url: http://localhost:6333
top_k: 5
similarity_threshold: 0.85 # dedup threshold for guideline merging
promotion_threshold_sessions: 3
guideline_max_tokens: 30
tiering_enabled: false # off by default; classifies entries into core/retrieve/quarantine
memory_action_enabled: false # off by default; surfaces proposed executable actions (shadow-only)
memory_action_apply_enabled: false # off by default; opt-in to actually apply proposed actions
Recommended default: the evolving profile
For an agent that should get better with every run, start from the evolving profile instead of tuning switches by hand:
memory:
profile: evolving
One line turns on the guarded self-improvement bundle: 120-token mined lessons (the stock 30-token cap truncates protocol-shaped knowledge), severity tiering, convention mining (declared multi-branch protocols captured engine-side, applied through a deterministic branch-selection validator), postmortems, evidence-gated success patterns, and shadow-only ActionMemory. Anything you set explicitly wins over the profile, and the human-authority gates are never profile-set: convention approvals stay exact-hash and human, action-apply stays opt-in, core prompt placement stays off. This is the configuration behind the convention-loop benchmark result (see BENCHMARKS.md); exact switches and fail-closed gates are in docs/memory.md.
Paths in manifest_dir and sandbox_root resolve relative to the
directory you run the command from, not the config file's location —
run from your project root. builtin resolves to the framework's
bundled tools wherever the package is installed.
Writing a tool
A tool is a JSON manifest next to an executable in any language. The
manifest is auto-discovered by globbing *.json in each manifest_dir.
{
"name": "hello",
"description": "One sentence the LLM uses to decide when to call this.",
"command": ["python3", "hello.py"],
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}}},
"output_schema": {"type": "object"},
"timeout_s": 10
}
The executable reads one JSON object from stdin, prints one JSON object to stdout, and uses its exit code to signal success/failure:
import json, sys
args = json.loads(sys.stdin.read())
print(json.dumps({"greeting": f"hello, {args['name']}"}))
# exit 0 -> ok=true, wrapped as {"ok": true, "result": ...}
# exit != 0 -> ok=false, wrapped as {"ok": false, "error": ..., "result": ...}
The runner normalizes timeouts, nonzero exits, and malformed-JSON
output into the same {ok, error?, result?, stderr?} shape — your
script never needs to worry about how the agent loop reports failure.
Sandboxing. read_file / write_file resolve every path against
$FABRI_SANDBOX_ROOT (set from tools.sandbox_root) and reject
anything that escapes it. If you write your own file-touching tool,
follow the same pattern.
Orchestration
fabri runs one agent loop at a time by default. There is no global planner, no message bus, and no coordinator process. The model in the loop is the orchestrator: at every step it sees the retrieved guidelines, the running tool-result tape, and decides the next call.
The loop
retrieve top-k guidelines for the task ──► system prompt
│
┌──────────────────────────────────────────────┘
▼
┌─────────────┐ tool_use ┌──────────────┐
│ LLM step │ ───────────► │ tool runner │ stdin JSON ─► subprocess
│ (ReAct) │ ◄─────────── │ │ ◄── stdout JSON
└──────┬──────┘ tool_result └──────────────┘
│ writes JSONL trace event per step
▼
stop on `final` / max_steps / hard error
│
┌──────────────────────────────────────────────┘
▼
analyze trace → compress failures → dedup → promote → memory store
The two cost-shaped knobs are the system prefix (cached;
guidelines + tool defs) and the rolling tape of tool results
(re-sent every step). Tool results enter the tape in TOON, not raw
JSON. Every step emits a usage event with token totals and
cost_usd so a host service can attribute COGS without parsing logs.
Picking a composition primitive
Three ways to do more work per LLM round-trip. Try them in this order — the cheap path is the default path.
| Primitive | Use when | Cost |
|---|---|---|
batch |
N known calls, no branching between them | 1× |
python_exec |
N calls with branching, loops, or aggregation | 1× |
spawn_subagent |
Independent subtask that would overflow context | ~15× |
Rule of thumb: if batch or python_exec can do it, do not reach for
spawn_subagent.
Agents as tools
A tools.agents entry in agent.yaml exposes another agent as a tool
of this one. Each sub-agent is just another tool call in the parent's
normal loop. A sub-agent entry may carry model / max_tokens
overrides, so a parent on Sonnet can call a Haiku classifier without
duplicating the full config:
tools:
agents:
- name: classify
description: Classify a snippet into one of N labels.
config: tools/agent_tools/classifier.yaml
model: claude-haiku-4-5
max_tokens: 256
A child invoked this way inherits the parent's memory collection by
default, so guidelines learned by either side accumulate in the same
store. Use memory_collection_suffix on the call (or a separate
memory.collection in the child config) when you want isolation —
e.g. a generic classify child shared by many parents.
Parallel fan-out
spawn_subagent calls that share a parallel_group tag are
dispatched concurrently by the parent loop, instead of sequentially.
The parent step that decides to fan out emits one tool_use block per
child; each parallel_group event in the trace marks the wall-clock
boundary. Cost is additive across children (token usage rolls up
through usage.total_cost_usd), but wall-clock collapses to the
slowest child.
The cheap path is always: do it inline. Reach for parallel_group
when (a) each child has its own large context to chew through, and
(b) the children genuinely don't need each other's outputs.
Multi-agent examples
Runnable versions of these shapes live in
examples/— custom tools (01), parallel sub-agent fan-out (02), a draft→verify pipeline (03), and sandboxing (04), each annotated with the optimization methodology it demonstrates (docs/optimization-methodologies.md). The YAML in this section is illustrative; theexamples/folders run as-is.
Three shapes that actually pay for the ~15× sub-agent overhead. Anything outside these is almost always cheaper inline.
| Shape | Use when | Example below |
|---|---|---|
| Fan-out + synthesize | One question splits into N independent legs | §1 |
| Specialist-as-tool | Some calls deserve a cheaper model or tighter prompt | §2 |
| Generator + verifier | One context can't hold both roles cleanly | §3 |
1. Fan-out research, single synthesizer
A planner agent decomposes a question, fans out one researcher-per-subquestion in parallel, then synthesizes. Each researcher burns its own context window on raw pages; the planner's context only ever sees their short summaries.
# planner.yaml
agent: { name: planner, max_steps: 12 }
llm: { provider: anthropic, model: claude-sonnet-4-6 }
tools:
manifest_dir: [builtin, tools/agent_tools]
enabled: [spawn_subagent, write_file]
agents:
- name: research_one
description: Answer ONE focused subquestion using the web. Returns ≤200 words + citations.
config: tools/agent_tools/researcher.yaml
model: claude-haiku-4-5 # cheap per-leg; planner stays on Sonnet
Planner's natural action becomes:
spawn_subagent(name=research_one, task="...subq A...", parallel_group="fanout-1")
spawn_subagent(name=research_one, task="...subq B...", parallel_group="fanout-1")
spawn_subagent(name=research_one, task="...subq C...", parallel_group="fanout-1")
→ synthesize from the three short returns
Wall-clock = slowest leg. Cost = sum of legs + planner. Crucially, the planner never loads raw pages into its context.
2. Specialist behind a uniform tool contract
A parent agent on Sonnet, two specialists exposed as tools:
# parent.yaml
tools:
agents:
- name: classify_intent # tiny Haiku classifier, 256-token cap
description: Classify a user message into {bug, feature, billing, other}.
config: tools/agent_tools/classifier.yaml
model: claude-haiku-4-5
max_tokens: 256
- name: sql_writer # SQL-only Sonnet, schema-aware
description: Turn a natural-language metric request into one SELECT.
config: tools/agent_tools/sqlwriter.yaml
The parent learns when to call each child through normal memory
guidelines ("for billing-shaped questions, call classify_intent
first"). Each child has its own tight prompt and its own memory
collection, so its guidelines don't pollute the parent's retrieval.
3. Pipeline with a verifier
Generator → verifier, two agents wired by the parent. Use when a single agent confuses itself by holding both roles in one context:
tools:
agents:
- name: draft
description: Produce a candidate answer. May be wrong.
config: tools/agent_tools/drafter.yaml
- name: verify
description: Check a candidate answer against the source. Returns {ok, reasons[]}.
config: tools/agent_tools/verifier.yaml
Parent loop: draft → verify → if not ok, draft again with the
verifier's reasons appended. The verifier never sees the drafter's
chain-of-thought, only its output — which is the point.
Designing tools for low token cost
Tools shape the bill more than prompts do. The system prefix is cached; tool results are not, and they ride in the context every step. The short version, as a table:
| Rule | What it means |
|---|---|
| One job, one tool | A mode enum is two tools badly fused. Split them. |
| Description is the contract | Manifest description is the only thing the LLM reads. 1–3 sentences. |
| Return the minimum | Slice, truncate, summarize. Every byte rides every subsequent step. |
| TOON-friendly shapes | Flat arrays of records with consistent keys encode much smaller. |
| Compose at the tool layer | If 3 calls always happen together, ship 1 tool. |
| Idempotent or explicit | Side-effecting tools must be safe to retry, or fail loudly. |
| Paths, not payloads | Write big results to .fabri/scratch/<id>.json, return {path, size}. |
| Code-as-action for loops | No for_each tool. python_exec covers it in one LLM step. |
| Cap your manifests | Keep tools.enabled tight — every entry sits in the cached prefix. |
Smell test: if "what would the agent do with the result of this tool?" has a one-sentence answer, the tool is probably shaped right. If the answer is "it depends what mode you called it in", split it.
The Improver — learn from logs you already have
The memory loop doesn't only learn from fabri's own runs. Point it at any log —
app logs, CI output, OTel/OpenAI traces — and the failures and successes in it
mine into the same memory the agent retrieves from. Feeding logs is
plug-and-play, and the default path is deterministic and $0 (no LLM call);
add synthesize=True for LLM-compressed guidelines.
import fabri
# Deterministic ($0): mine an external log straight into the agent's memory.
summary = fabri.readlogs("prod/app.jsonl", adapter="jsonl")
print(summary.sessions, summary.by_kind, f"${summary.llm_cost_usd:.4f}")
# The knowledge is now retrievable — the next run starts ahead of where the
# logs left off. Omit store/config and it resolves them from agent.yaml, so the
# guideline lands in the collection the agent already reads.
Teach fabri a new log format three ways — pick per format:
# 1) a native Python adapter (decorator)
from fabri.ingest import Session, tool_event, start_event, final_event
@fabri.adapter("mytool")
def parse(source, options):
for trace_id, records in source.group_by("trace_id"):
events = [start_event(records[0]["prompt"])]
for r in records:
events.append(tool_event(r["cmd"], ok=r["exit"] == 0, error=r.get("stderr")))
events.append(final_event("success" if records[-1]["exit"] == 0 else "failed"))
yield Session(f"mytool-{trace_id}", events)
# 2) a declarative field-map in agent.yaml — zero code
ingest:
adapters:
- name: prodlogs
kind: configmap
mapping: {session_key: trace_id, task_field: prompt,
tool_field: tool.name, ok_field: tool.ok, error_field: tool.error}
# 3) a polyglot executable (any language) via the tool contract, shippable as a
# skill — see the bundled `syslog-adapter` skill.
fabri skills install syslog-adapter
fabri ingest app.log --adapter syslog # $0; --synthesize for LLM guidelines
tail -f app.log | fabri ingest - --adapter syslog # or stream it live
Built-in adapters: jsonl (native), regex (plaintext), otel/openai
(structured traces), and auto to sniff. Third-party adapters install as pip
packages via the fabri.adapters entry-point group. fabri ingest --list-adapters shows what's available.
Skills — scaffold an agency from a bounded deliverable
The links and marketplace commands below resolve once this is merged to
main; on a branch or unmerged fork, use the local install/validate commands inskills/agency-builder/README.mdinstead.
skills/agency-builder
turns one bounded deliverable into a small, reviewable fabri agency — an
orchestrator plus a fixed set of specialists, each an ordinary agents-as-tools
config, with a deterministic verifier standing in for "trust me, it worked."
See docs/agency-kernel.md
for what stays fixed versus what varies per agency, and
examples/agencies/changelog-release-notes
for a worked, runnable example.
Install for Claude Code:
/plugin marketplace add Rushour0/fabri
/plugin install agency-builder@fabri-skills
Install for Codex CLI:
codex plugin marketplace add Rushour0/fabri
codex plugin add agency-builder@fabri-skills
The skill asks for the target persona, the one deliverable, specialist roles,
proof-bar metric, and approval gate before it builds anything — a bare prompt
like "build an AI agency for X" gets you a clarifying question back, not an
immediate scaffold. See
skills/agency-builder/README.md
for local (pre-publish) install/validate commands and common usage cases.
Using it as a library
Everything the CLI does is composition over the public API:
from fabri import (
run_agent, QdrantMemoryStore, build_llm, build_tool_defs, build_tools,
)
from fabri.config import load_config
config = load_config("agent.yaml")
store = QdrantMemoryStore(
url=config["memory"]["qdrant_url"],
collection=config["memory"]["collection"],
)
tools = build_tools(config["tools"])
llm = build_llm(config, build_tool_defs(tools, config["tools"]["decompose"]))
result = run_agent(
"do the task", llm, tools, store, max_steps=config["agent"]["max_steps"],
)
To skip Qdrant entirely, swap the store for the in-process sqlite-vec backend — same interface, no other code changes:
from fabri import SqliteMemoryStore
store = SqliteMemoryStore(
path=".fabri/memory.db",
collection=config["memory"]["collection"],
)
License
Apache License, 2.0 © Rushikesh Patade. Free for any use, including commercial and hosted/embedded redistribution. Contributions welcome — see CONTRIBUTING.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fabri-0.23.4.tar.gz.
File metadata
- Download URL: fabri-0.23.4.tar.gz
- Upload date:
- Size: 777.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0be355f2bf7c8c077d966bfddc1da182b7df9b14c715cf931c4d1773ad573a8e
|
|
| MD5 |
8b97d6065ede0ab37e75caa323c051ee
|
|
| BLAKE2b-256 |
8e2b680d115251c660bdb7440d34aec3efa3a62126c3dcc371a7f3377cbd51ce
|
Provenance
The following attestation bundles were made for fabri-0.23.4.tar.gz:
Publisher:
release.yml on Rushour0/fabri
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fabri-0.23.4.tar.gz -
Subject digest:
0be355f2bf7c8c077d966bfddc1da182b7df9b14c715cf931c4d1773ad573a8e - Sigstore transparency entry: 2339772713
- Sigstore integration time:
-
Permalink:
Rushour0/fabri@f53745a50a78baea1e724e12024b8a8e1e8c3a6a -
Branch / Tag:
refs/tags/v0.23.4 - Owner: https://github.com/Rushour0
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f53745a50a78baea1e724e12024b8a8e1e8c3a6a -
Trigger Event:
push
-
Statement type:
File details
Details for the file fabri-0.23.4-py3-none-any.whl.
File metadata
- Download URL: fabri-0.23.4-py3-none-any.whl
- Upload date:
- Size: 607.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95206bb4cd1cadc406fec169c59c9abcadce48d4ff0bb2d154c60f9f48c4b708
|
|
| MD5 |
0d563f653681259cac31adffe53c0e94
|
|
| BLAKE2b-256 |
64662bd355bfcc4a42da9cb6a4551d2519861e10a0589a53dbd1521098464b5c
|
Provenance
The following attestation bundles were made for fabri-0.23.4-py3-none-any.whl:
Publisher:
release.yml on Rushour0/fabri
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fabri-0.23.4-py3-none-any.whl -
Subject digest:
95206bb4cd1cadc406fec169c59c9abcadce48d4ff0bb2d154c60f9f48c4b708 - Sigstore transparency entry: 2339772729
- Sigstore integration time:
-
Permalink:
Rushour0/fabri@f53745a50a78baea1e724e12024b8a8e1e8c3a6a -
Branch / Tag:
refs/tags/v0.23.4 - Owner: https://github.com/Rushour0
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f53745a50a78baea1e724e12024b8a8e1e8c3a6a -
Trigger Event:
push
-
Statement type: