Repository navigation
Releases: civitas-io/fabrica
Release list
v0.6.0 — RecencyCompactor validation gate
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
RecencyCompactornow scores every summary via a new, cheap, non-LLMscore_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).CompactionResultgained two fields:validation_score: float | None(Nonewhen nothing was summarized or validation is disabled) anddegraded: bool(belowvalidation_threshold, default0.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, soRecencyCompactordoes not invent that behavior by default. - The retry always uses the same
target_tokensas the first attempt, never a larger one — a caller'sbudget_tokensis 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=Trueif 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-unbuiltfabrica-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),ruffclean,mypy --strictclean. - 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-contextNo 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)
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 realcivitas.plugins.tools.ToolProviderwrapping one MCP tool. One instance per(server, tool)pair, matchingconnect_mcp()'s own, already-shipped construction loop exactly — this fix required no changes toconnect_mcp(),civitas.plugins.tools.ToolRegistry, orpresidium.providers.civitas_adapters.GovernedToolAdapterat all. All three were already correctly built; they just had nothing real to wrap.MCPToolis not a replacement forMCPToolNamespace— the two are legitimate, parallel wrappers around the sameMCPClient, for two different consumers:MCPToolNamespaceis one object per server, multiplexing every tool through.call(name, params), for this repo's own code-mode/ToolManagerpath.MCPToolis one object per tool, for any agent registering tools directly intocivitas'sself.tools.- Registers under the already-documented
mcp://server_name/tool_nameaddressing scheme, emits a realcivitas.mcp.callOTEL span, and emits a realmcp.tool.callaudit event (success and failure both recorded) — closing every piecepython-civitas's owndocs/milestones.mdhad claimed as shipped.
Fixed (found while scoping, not part of the original report)
SandboxConfig.enabledhad opposite fail-open/fail-closed defaults between this repo's own, independently-definedSandboxConfig(enabled=True) andcivitas.sandbox.config.SandboxConfig(enabled=False) — a real, security-relevant divergence, not cosmetic. Fixed by unifying:fabrica.mcp.types.MCPServerConfig/SandboxConfig/FilesystemMount/MCPToolSchemaare now re-exports of civitas's own canonical types (civitas 0.12.0 gained theallow_unsandboxedfield this repo's version had and civitas's didn't), not a second, divergence-prone copy.fabrica.mcp.client.MCPClient's ownAuditSinkProtocol 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 declaredemit(event: str, details: dict); civitas's realcivitas.audit.types.AuditSink.emit()takes one argument, a wholeAuditEventTypedDict, plusflush()/close().connect_mcp()passes a realcivitas.audit.types.AuditSinkstraight through asaudit_sink=— any agent with real auditing configured would have hit a genuineTypeErrorthe momentMCPClient.connect()tried to emit its first"mcp.connect"event. Fixed by importing civitas's realAuditSink/AuditEventdirectly.
Dependency change
civitas>=0.11.0→civitas>=0.12.0— a real, needed bump, not cosmetic. TheSandboxConfig/MCPServerConfig/MCPToolSchemaunification above only exists as of that release; a floor below it would silently resolve acivitasthat still diverges on theSandboxConfig.enableddefault.
Verified
- New
tests/mcp/test_connect_mcp_integration.pyconstructs a realcivitas.process.AgentProcess, callsconnect_mcp()against the realecho_server.pyfixture, and asserts tools genuinely register intoself.toolsand execute for real — the exact scenario the bug report described. This test failed withModuleNotFoundErrorbefore this release; it passes now. - New
tests/mcp/test_tool.py(9 tests):ToolProvidershape, themcp://server/toolnaming scheme, span emission, and the corrected audit-event shape including on failure. - New regression test in
test_client.pyfor theAuditSinkbug. - 326 tests pass (35 skipped, hardware-gated),
ruffclean,mypy --strictclean — verified against the real, publishedcivitas0.12.0, not a local-path override.
Upgrading
pip install --upgrade fabrica-contextThis 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
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
feat: Scope gains an additive extra field, closing a real client/serv…
fabrica-context v0.2.0 -- RestPresidiumClient
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— implementsdocs/contracts/managers.md'sPresidiumClient.check_grantcontract exactly:- Real
POST /v1/check_grantREST 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-configuredssl.SSLContext, not httpx's deprecatedcert=/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.
- Real
- New
fabrica[presidium]extra (httpx) — deliberately not a core dependency.PresidiumClientstays a duck-typed Protocol; a caller who wants a different transport, or no Presidium at all (NullPresidiumClient), never needshttpxinstalled. fabrica.presidiumis now a package (was a single module) —__init__.pykeeps the unchanged Protocol +GrantResult;rest_client.pyholds 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-contribv0.2.x server (realGovernedRuntime, realHTTPGateway, real self-signed mTLS certs) and hitting it with the realRestPresidiumClient— not mocks. - 100% coverage on both new files.
ruff/mypy --strictclean.
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
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'ssrt) — 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 ownjailerbinary. Solves a genuinely hard problem (a vsock socket usable inside a directoryjailerlocks down to700) with a real, validated fix, not a workaround.
- Tier 0 (
ToolManager/SkillManager— tools-as-code plus afind()fallback; skills conforming to the openSKILL.mdstandard, validated against the real 81-skillbigpowerscatalog.MemoryManager— three separate facets (working memory, compaction, long-term recall via aMemoryStorewrapping Mem0), deliberately not one blended score.PromptManager— a versioned, addressablePromptStore; 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—TailscaleTunnelProviderandCloudflareTunnelProvider, 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/SpanProtocol 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
uvthroughout.
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
- README — what this is
docs/architecture.md— visual tourdocs/contracts/— implementation-ready signaturesspecs/archive/spikes/— every empirical validation, in order