Skip to main content

Ever-evolving prompting and context engineering for LLM agents through active memory and result analysis.

Project description

fabri

Ever-evolving prompting and context engineering for LLM agents through active memory and result analysis.

fabri is source-available under the Business Source License 1.1. You can install it from PyPI, build agents with it, and rely on the CLI and config surface. Individuals and organizations under US $1M in annual revenue can use it in production for free; larger organizations and anyone embedding fabri in a hosted/distributed product need a commercial license — see COMMERCIAL.md. Every version automatically converts to Apache 2.0 on 2030-06-23. The internals and the direction of the project are not open for contribution.

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 whole product.

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_subagent re-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_exec script (or one batch call) 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_file over 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.

Install

pip install fabri                       # the `fabri` command lands on PATH
docker run -p 6333:6333 qdrant/qdrant   # vector store for memory
export ANTHROPIC_API_KEY=...

For OpenAI models: pip install "fabri[openai]" and set llm.provider: openai in your config.

Embeddings run locally via sentence-transformers/all-MiniLM-L6-v2 — no embedding API calls.

Quickstart

fabri init demo && cd demo
fabri --config agent.yaml run "greet Ada with the hello tool"

fabri init writes an agent.yaml, an example tool under tools/agent_tools/, and a docker-compose.yml. You edit those, not the library.

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

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.

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)

llm:
  provider: anthropic            # or "openai"
  model: claude-sonnet-4-6
  max_tokens: 1024
  api_key_env: ANTHROPIC_API_KEY

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

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

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.

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

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"],
)

License

Business Source License 1.1 © Rushikesh Patade. Free for individuals and organizations under US $1M in annual revenue; commercial license required above that or for hosted/embedded redistribution — see COMMERCIAL.md. Auto-converts to Apache 2.0 on 2030-06-23. Not open for contribution.

Versions ≤ 0.4.6 were released under Apache 2.0 and remain so.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fabri-0.6.0.tar.gz (130.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fabri-0.6.0-py3-none-any.whl (104.9 kB view details)

Uploaded Python 3

File details

Details for the file fabri-0.6.0.tar.gz.

File metadata

  • Download URL: fabri-0.6.0.tar.gz
  • Upload date:
  • Size: 130.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for fabri-0.6.0.tar.gz
Algorithm Hash digest
SHA256 ab830151c3f0ecd693e8f81e80f7ff346eb3ec795a82ecd04c338f45f7cf0f9f
MD5 204ca32e1ad0f294ff4d61e93f134a62
BLAKE2b-256 58844775c0170459b86afe5ca3d5be4059725786f99ffbf2c60346cbe052b704

See more details on using hashes here.

Provenance

The following attestation bundles were made for fabri-0.6.0.tar.gz:

Publisher: release.yml on Rushour0/fabri

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fabri-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: fabri-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 104.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for fabri-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f2ffc0076ce87409a3ab97c5ea035b9ceab03a45f5fb5e4027174ade62100b37
MD5 3847492ea4b5e110de085ca4bcb186e6
BLAKE2b-256 3c3d5ec630354a27a6aa0db8ebfb2950ea0ed49fe736a39159e312e81696d0fe

See more details on using hashes here.

Provenance

The following attestation bundles were made for fabri-0.6.0-py3-none-any.whl:

Publisher: release.yml on Rushour0/fabri

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page