before the call · pre-flight
contextkitassemble
squeezecompress
tokenguardbudget
guardrailsgate
acttraceguard
→
→
after · automatic, via the bus
guardrailsgate output
cassettetest
acttraceaudit
cendor-core — the instrument() seam + one event bus beneath every stage. Each library publishes and subscribes here; none imports another.
**A lifecycle, not a dependency chain.** Every library works alone; two act on *both* sides of the
call — **guardrails** gates the input and the output, **acttrace** guards before send and audits
after — and adding one changes nothing at your call site. Everything ships under the `cendor.*`
import namespace. The foundation, `cendor-core`, defines the common vocabulary — what an "LLM call"
is, how tokens are counted and priced, how events are emitted — that lets the tools interlock
without coupling.
| Role | When it acts | PyPI package | Import |
|---|---|---|---|
| Foundation | the seam + the bus | `cendor-core` | `cendor.core` |
| Assemble | before the call | `cendor-contextkit` | `cendor.contextkit` |
| Compress | before the call | `cendor-squeeze` | `cendor.squeeze` |
| Budget | before + after | `cendor-tokenguard` | `cendor.tokenguard` |
| Gate | before + after | `cendor-guardrails` | `cendor.guardrails` |
| Test | after (record / replay) | `cendor-cassette` | `cendor.cassette` |
| Guard + Audit | before + after | `cendor-acttrace` | `cendor.acttrace` |
| Umbrella (meta) | — | `cendor` | *(installs them all)* |
## How they connect (three layers)
The layering is what lets each tool stand alone *and* compose.
### Layer 1 — Shared types & protocols
`cendor.core` exports what everything agrees on:
- **Types:** `LLMCall`, `ToolCall`, `Usage`, `Money` (Decimal-backed).
- **Protocols** (structural / duck-typed): `Compressor`, `EvictionStrategy`, `Sink`, `Subscriber`,
`Handle`. A library satisfies one by *shape* — `squeeze` is a `Compressor` without importing
`contextkit`; `acttrace` is a `Subscriber` without importing the bus.
- **Primitives:** `tokens.count(...)`, a tokenizer registry, and a `prices` table.
### Layer 2 — One instrumentation point
`tokenguard`, `guardrails`, `cassette`, and `acttrace` all need to *see* LLM and tool calls — to
budget, gate, record, or audit them. Rather than each patching the provider client (and fighting
each other), `core` owns a single interception point:
```python
from cendor.core import instrument
client = instrument(openai_client) # also Anthropic, Hugging Face, Bedrock, Gemini, Ollama
```
```ts
import OpenAI from 'openai';
import { instrument } from '@cendor/core';
const client = instrument(new OpenAI()); // also Anthropic, Hugging Face, Bedrock, Gemini, Ollama
```
`instrument()` wraps the client once, publishes a normalized `LLMCall` onto the in-process **event
bus**, and optionally emits an OpenTelemetry span. Each sibling tool *subscribes* instead of
patching — one wrap, many listeners, no conflicts. It handles sync, async, and streaming calls;
wrapping is idempotent and additive (it coexists with OpenLLMetry / OpenInference), and the bus is
thread-safe within a process.
### Layer 3 — Cooperation via the shared stream
Because every instrumented call flows through the same bus, downstream tools get each other's work
for free:
- `tokenguard` prices the call and records spend by tag.
- `contextkit` attaches its assembly decisions (kept / dropped) to the stream.
- `guardrails` gates the input, tool calls, and output; each decision emits on the bus.
- `acttrace` subscribes and produces an audit log that **already knows** the context, the cost, the
tools, and the guardrail decisions — without importing `tokenguard`, `contextkit`, or `guardrails`.
```mermaid
sequenceDiagram
participant App as Your agent
participant C as instrument(client)
participant Bus as core.bus
participant GR as guardrails
participant TG as tokenguard
participant CS as cassette
participant AT as acttrace
App->>GR: gate input (block / redact / flag)
GR-->>Bus: emit(GuardrailDecision)
App->>C: chat.completions.create(...)
C->>C: build LLMCall, count + price usage
C-->>Bus: emit(LLMCall)
Bus-->>TG: record spend, enforce budget
Bus-->>CS: record (or replay) the call
Bus-->>AT: append to the hash-chained log (calls + guardrail decisions)
C-->>App: response
```
> Install the umbrella, call `instrument()` once, and the `acttrace` audit log auto-populates with
> the budget, the context decisions, and the tool calls. That's what "they compose" means — a shared
> event schema, not marketing.
### Dependency graph
```mermaid
%%{init: {"flowchart": {"htmlLabels": false}} }%%
graph TD
core["cendor-core