Skip to content

Releases: civitas-io/fabrica

v0.6.0 — RecencyCompactor validation gate

Choose a tag to compare

@github-actions github-actions released this 26 Aug 14:11

Highlights

RecencyCompactor gains a real validation gate — closing the #1 memory-quality gap identified by an internal LLM council.

Triggered by a 5-advisor council (Contrarian / First Principles / Expansionist / Outsider / Executor, peer-reviewed) run on "what's the true path to SOTA memory management," cross-referenced against a recent survey paper on agentic context management (arXiv:2607.21503) and this org's own harness-engineering-wiki. All 5 advisors converged independently, and peer review confirmed it 5/5: RecencyCompactor had zero validation of its own summary output — it called the injected Summarizer once and trusted the result unconditionally. That's the exact failure mode the cited survey's sharpest data point describes: unvalidated compaction can drop task accuracy below the no-context baseline (a documented case: 66.7% → 57.1%).

Validated empirically before shipping — not assumed

SPIKE-recency-compactor-validation-gate.md extended the two prior RecencyCompactor spikes' trip-planning scenario to three competing hard facts in different domains (a budget ceiling, a severe allergy/safety constraint, a hard return-by date) under a genuinely tight token budget, with a generic, non-fact-aware summarizer prompt — the exact condition neither prior spike exercised (their own prompt was already fact-aware and never once failed, which is precisely why this spike had to change that variable to be informative).

Real Gemini 2.5 Flash calls via Vertex AI, real RecencyCompactor calls (not a reimplementation):

Condition Runs Both critical facts correct
FULL history (baseline) 3 3/3
NAIVE — today's real, pre-0.6.0 behavior 6 3/6
VALIDATED — this release's gate 6 6/6

Every naive failure showed up as a missing number in the validation heuristic's own diagnostic output — never as missing prose. A confirmatory run kept the retry's budget_tokens identical to the first attempt (not the spike's initial 1.5× bump, which would have silently let compaction exceed a caller's real budget) and still held 6/6 — that's the configuration actually shipped.

What's new

  • RecencyCompactor now scores every summary via a new, cheap, non-LLM score_compaction() heuristic (0.6× numeric-token overlap + 0.4× content-word overlap between the source messages and the summary). Deliberately not another LLM call — validating a summary with a second LLM judgment would reintroduce the same untrusted-inference risk one level up (a concern 2 of 5 independent peer reviewers raised in the council session).
  • CompactionResult gained two fields: validation_score: float | None (None when nothing was summarized or validation is disabled) and degraded: bool (below validation_threshold, default 0.55) — real visibility a caller never had before.
  • Retry is opt-in, not automatic. A caller supplies a retry_summarizer: Summarizer | None — typically the same model, a stricter, more fact-explicit prompt. Retrying the identical summarizer with the identical prompt was never validated and has no principled reason to help, so RecencyCompactor does not invent that behavior by default.
  • The retry always uses the same target_tokens as the first attempt, never a larger one — a caller's budget_tokens is a real ceiling (e.g. fitting a model's context window), not a suggestion to relax under pressure.
  • A failing retry degrades honestly rather than aborting. If the (optional) retry summarizer itself raises, the first summarizer's already-successful result is kept, degraded=True if it was below threshold — never a crash over a compaction that already produced something usable.

Named as still genuinely open — not glossed over

  • Retry-exhaustion path is implemented but not empirically exercised. Every retry in the spike happened to succeed; what happens when the retry also fails validation is real code (degraded=True, no third attempt invented) but wasn't hit by a real failing case in testing.
  • The threshold (0.55) and weighting (0.6/0.4) are a reasonable starting point, not independently tuned against a broader dataset — chosen because the observed separating gap (0.315 naive average vs. 0.674 validated average) was wide enough that the exact value likely doesn't matter much in the tested scenario, not because it was optimized.
  • The council's other, lower-priority findings are real future work, deliberately not addressed here: MemoryStore's BM25-only (keyword) retrieval with no semantic/vector search, unbounded growth with no capacity ceiling or freshness/decay policy, no entity resolution, and the still-unbuilt fabrica-contrib[mem0|zep|letta|cognee|langmem] adapters. Correctly sequenced behind this fix — these make the system "not yet best," this fix addressed the system being "actively worse than off."

Verified

  • 9 new tests (tests/memory/test_compactor.py), 334 tests pass total (was 326), ruff clean, mypy --strict clean.
  • Real fresh-venv install from PyPI, confirming the validation gate + opt-in retry work end to end exactly as shipped (not just in the dev checkout).

Upgrading

pip install --upgrade fabrica-context

No breaking changes — validate=True and retry_summarizer=None are the new defaults, meaning existing callers get free validation visibility (validation_score/degraded) with zero behavior change to summary output unless they explicitly opt into the retry path.

Full Changelog: v0.5.0...v0.6.0

v0.5.0 — MCPTool (the real, previously-missing piece connect_mcp() needed)

Choose a tag to compare

@github-actions github-actions released this 25 Aug 16:17

Highlights

MCPTool — the real, previously-missing piece civitas.process.AgentProcess.connect_mcp() has always needed.

connect_mcp() — the sanctioned, documented MCP integration point for any Civitas agent — has tried from fabrica.mcp.tool import MCPTool since it was written. That module never existed anywhere in this repo's history (git log -S "class MCPTool" only ever matches MCPToolNamespace, a substring hit, not a real one). Every real call raised ModuleNotFoundError immediately, despite python-civitas's own docs/mcp.md presenting it as a fully working, tutorial-documented feature.

Reported 2026-08-25 by a downstream project team building on this stack. Verified directly against source before agreeing — not taken on faith — then scoped and fixed the same day, alongside the corresponding real fix in civitas-io/python-civitas v0.12.0 (required — see Upgrading below).

What's new

  • fabrica.mcp.tool.MCPTool — a real civitas.plugins.tools.ToolProvider wrapping one MCP tool. One instance per (server, tool) pair, matching connect_mcp()'s own, already-shipped construction loop exactly — this fix required no changes to connect_mcp(), civitas.plugins.tools.ToolRegistry, or presidium.providers.civitas_adapters.GovernedToolAdapter at all. All three were already correctly built; they just had nothing real to wrap.
  • MCPTool is not a replacement for MCPToolNamespace — the two are legitimate, parallel wrappers around the same MCPClient, for two different consumers: MCPToolNamespace is one object per server, multiplexing every tool through .call(name, params), for this repo's own code-mode/ToolManager path. MCPTool is one object per tool, for any agent registering tools directly into civitas's self.tools.
  • Registers under the already-documented mcp://server_name/tool_name addressing scheme, emits a real civitas.mcp.call OTEL span, and emits a real mcp.tool.call audit event (success and failure both recorded) — closing every piece python-civitas's own docs/milestones.md had claimed as shipped.

Fixed (found while scoping, not part of the original report)

  • SandboxConfig.enabled had opposite fail-open/fail-closed defaults between this repo's own, independently-defined SandboxConfig (enabled=True) and civitas.sandbox.config.SandboxConfig (enabled=False) — a real, security-relevant divergence, not cosmetic. Fixed by unifying: fabrica.mcp.types.MCPServerConfig/SandboxConfig/FilesystemMount/MCPToolSchema are now re-exports of civitas's own canonical types (civitas 0.12.0 gained the allow_unsandboxed field this repo's version had and civitas's didn't), not a second, divergence-prone copy.
  • fabrica.mcp.client.MCPClient's own AuditSink Protocol didn't match civitas's real one at all — found by tracing the actual runtime call, not by comparing signatures side by side. The local Protocol declared emit(event: str, details: dict); civitas's real civitas.audit.types.AuditSink.emit() takes one argument, a whole AuditEvent TypedDict, plus flush()/close(). connect_mcp() passes a real civitas.audit.types.AuditSink straight through as audit_sink= — any agent with real auditing configured would have hit a genuine TypeError the moment MCPClient.connect() tried to emit its first "mcp.connect" event. Fixed by importing civitas's real AuditSink/AuditEvent directly.

Dependency change

  • civitas>=0.11.0 → civitas>=0.12.0 — a real, needed bump, not cosmetic. The SandboxConfig/MCPServerConfig/MCPToolSchema unification above only exists as of that release; a floor below it would silently resolve a civitas that still diverges on the SandboxConfig.enabled default.

Verified

  • New tests/mcp/test_connect_mcp_integration.py constructs a real civitas.process.AgentProcess, calls connect_mcp() against the real echo_server.py fixture, and asserts tools genuinely register into self.tools and execute for real — the exact scenario the bug report described. This test failed with ModuleNotFoundError before this release; it passes now.
  • New tests/mcp/test_tool.py (9 tests): ToolProvider shape, the mcp://server/tool naming scheme, span emission, and the corrected audit-event shape including on failure.
  • New regression test in test_client.py for the AuditSink bug.
  • 326 tests pass (35 skipped, hardware-gated), ruff clean, mypy --strict clean — verified against the real, published civitas 0.12.0, not a local-path override.

Upgrading

pip install --upgrade fabrica-context

This will also pull in civitas>=0.12.0, which includes a real, deliberate breaking change of its own (SandboxConfig.enabled now defaults to True, fail-closed) — see that release's own notes if you configure MCP server sandboxing.

Full Changelog: v0.4.0...v0.5.0

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 05:51
chore: bump to v0.4.0 for InProcessPresidiumClient

MINOR bump, not PATCH -- a genuinely new, additive capability (new
InProcessPresidiumClient class, new fabrica[presidium-inprocess]
extra), not a bug fix, matching this repo's own established semver
convention. HANDOFF.md updated with the real detail.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 02:31
feat: Scope gains an additive extra field, closing a real client/serv…

fabrica-context v0.2.0 -- RestPresidiumClient

Choose a tag to compare

@github-actions github-actions released this 24 Aug 03:23

fabrica-context v0.2.0 — RestPresidiumClient

The real PresidiumClient implementation — REST + mTLS, circuit-breaker protected, fail-closed. Closes the one item this project's own HANDOFF.md explicitly named as blocked, not just deferred: no real Presidium HTTP server existed to build or validate a client against. civitas-io/presidium shipped a real M7 server this cycle, so this now exists for real.

Added

  • fabrica.presidium.rest_client.RestPresidiumClient — implements docs/contracts/managers.md's PresidiumClient.check_grant contract exactly:
    • Real POST /v1/check_grant REST call, confirmed directly against Presidium's own shipped server source, not guessed.
    • RestPresidiumClient.from_endpoint(...) convenience factory for real mTLS (client cert, key, CA) — builds one fully-configured ssl.SSLContext, not httpx's deprecated cert=/verify=<str> combination (which has a real, confirmed bug found earlier this cycle).
    • A minimal, real circuit breaker (CLOSED/OPEN/HALF_OPEN) — trips after N consecutive failures, denies immediately without a fresh timeout wait until a cooldown elapses, then half-opens to test recovery with exactly one real attempt.
    • Never raises — every failure mode (breaker open, connection error, timeout, non-2xx, malformed JSON, an unrecognized decision value) returns a plain GrantResult(decision="deny", ...), matching the Protocol's own documented fail-closed contract.
  • New fabrica[presidium] extra (httpx) — deliberately not a core dependency. PresidiumClient stays a duck-typed Protocol; a caller who wants a different transport, or no Presidium at all (NullPresidiumClient), never needs httpx installed.
  • fabrica.presidium is now a package (was a single module) — __init__.py keeps the unchanged Protocol + GrantResult; rest_client.py holds the new implementation. Mirrors this codebase's own established Protocol-plus-concrete-provider(s) shape (fabrica.tunnel, fabrica.sandbox).

Verification

  • 24 deterministic unit tests against httpx.MockTransport — every real response shape, every fail-closed path, circuit breaker state transitions.
  • A real end-to-end test booting an actual presidium/presidium-contrib v0.2.x server (real GovernedRuntime, real HTTPGateway, real self-signed mTLS certs) and hitting it with the real RestPresidiumClient — not mocks.
  • 100% coverage on both new files. ruff/mypy --strict clean.

Known, honest gap (not silently omitted)

Presidium's own real server does not yet thread scope through to its policy layer (a known gap on Presidium's own roadmap). check_grant() serializes and sends scope on the wire regardless — forward-compatible with the day Presidium's server catches up, currently a no-op server-side.

v0.1.0 — first real release

Choose a tag to compare

@jerynmathew jerynmathew released this 21 Aug 19:11

Fabrica v0.1.0 — first real release

Fabrica is the context layer for Python agents built on the Civitas platform: it decides what an agent sees (tools, skills, memory, prompts) and where its code actually runs (a tiered, pluggable sandbox). This is the first published release — alpha, but substantially real: every component below is implemented and tested, most validated against real external hardware or services, not just designed.

pip install fabrica-context   # import fabrica

Headline: code-mode

The core mechanism this project exists to prove out. A model writes code against a tool namespace instead of making individual tool calls; that code runs in an isolated sandbox; only the final result crosses back into the model's context. Measured on real hardware: ~79% cheaper and more correct than traditional tool-calling (SPIKE-code-mode-execution.md).

What's in this release

The full object model — designed, built, and tested, not just planned:

  • Retriever — the shared engine behind both tool and skill discovery. Pure-Python BM25 (KeywordBackend) for v1; no performance evidence yet justifies Rust tooling.
  • Sandbox / SandboxPool — three real, isolation tiers, auto-detected per host, never a user-facing choice:
    • Tier 0 (SubprocessSandbox) — trusted code / local dev, all platforms.
    • Tier 1 (SrtSandbox, wraps Anthropic's srt) — real OS-level network/filesystem enforcement, validated live end to end on macOS (p50 152ms).
    • Tier 2 (FirecrackerSandbox) — Linux + KVM, microVM-per-agent isolation. Cold-boot by default; two real, opt-in hardenings, both validated on real bare-metal hardware:
      • use_snapshot_restore=True — a golden snapshot created once, every boot restores from it in ~8–11ms.
      • use_jailer=True — chroot + cgroups + uid/gid drop on top of the KVM boundary, via Firecracker's own jailer binary. Solves a genuinely hard problem (a vsock socket usable inside a directory jailer locks down to 700) with a real, validated fix, not a workaround.
  • ToolManager / SkillManager — tools-as-code plus a find() fallback; skills conforming to the open SKILL.md standard, validated against the real 81-skill bigpowers catalog.
  • MemoryManager — three separate facets (working memory, compaction, long-term recall via a MemoryStore wrapping Mem0), deliberately not one blended score.
  • PromptManager — a versioned, addressable PromptStore; storage and retrieval only, no rendering or compression.
  • CivitasBridge — the single integration seam into Civitas's real runtime (Runtime.spawn, StateStore), reconciled against Civitas's actual source, not guessed.
  • MCP, both directions — a real MCP client (MCPClient/MCPToolNamespace) and a real MCP server (FabricaMCPServer, stdio + HTTP transports), the server side preserving the same two-path token-efficiency thesis for external callers, not just internal ones.
  • TunnelProvider — TailscaleTunnelProvider and CloudflareTunnelProvider, both credential-free, both validated end to end (real public URL, real curl round trip, real access log confirming the request crossed the tunnel).
  • Real observability — all ten spans named in the system design emit for real via a Tracer/Span Protocol matching Civitas's actual tracer shape, not a generic OTEL call into a void.

Validated, not claimed:

  • Fifteen spikes, all real hardware or API evidence — no simulated results anywhere in this project's history.
  • Nine implementation-ready contracts (docs/contracts/), each with real corrections found while writing exact signatures, not just transcribed from design docs.
  • A real GitHub Actions CI pipeline (lint, test, build, verified install) on every push, using uv throughout.

Known gaps — named, not hidden

  • Managed-provider adapters (E2B, Modal, AWS, Azure, GCP) — interfaces fully designed, blocked on real cloud credentials this project doesn't have.
  • NgrokTunnelProvider — not built; needs a paid/free-tier account for anything beyond a very short session, unlike the two implemented backends.
  • macOS Tier 2 (libkrun) — not built. "Great to have, not a must."
  • Windows — deliberately deferred across every tier; a small segment, revisit only if a real gap forces it.
  • PresidiumClient's real REST+mTLS implementation — the Protocol is designed; no real Presidium server exists anywhere yet to validate a client against.

See HANDOFF.md and docs/PLAN.md for the full, current, ordered state.

Links