Every version, both languages.
Cendor ships in Python (cendor.* on PyPI) and TypeScript (@cendor/* on npm). The two release on independent cadences — the parity matrix, not matching version numbers, is the contract.
Structured output is captured, a raw response can be recorded, and the SDK’s own helper methods survive instrumentation · cendor-core 1.14.2 / @cendor/core 0.16.2 · cendor-cassette 1.1.1 · cendor-guardrails 1.6.1 · cendor-acttrace 1.13.1 / @cendor/core 0.16.1 · cendor-acttrace 1.13.1 / @cendor/acttrace 0.14.1 · 2026-07-27
Six capture and integrity repairs, every one found by driving a real Microsoft 365 pro-code agent against the published shelf rather than by reading the code. Under cassette replay an await on an async OpenAI or Anthropic client used to raise TypeError, because those SDKs put an async def behind a sync decorator and the replay seam handed the recorded value straight back — a replayed stream was worse, neither awaitable nor async for-able, so no app-side shim could have covered it. A call made through with_raw_response — the documented way to read response headers, and the path Microsoft Agent Framework drives OpenAI through — was captured with no usage and no cost while the identical plain call priced exactly. responses.parse emitted nothing at all. An async tool behind a decorator recorded its own coroutine object, and a recorder persisted that into the cassette. In TypeScript, instrument() quietly removed the SDK's asResponse()/withResponse() accessors while the preserved type kept promising them — it type-checked, then threw. And two live AuditLogs on one chain file interleaved two hash chains into it, so verify() failed at the first divergence, discovered only when someone audited. All six are fixed; none adds public API.
Earlier — One scope, one trace (core 1.14.0 / 0.16.0 · acttrace 1.13.0 / 0.14.0 · sdk 1.20.0 / 0.24.0 · Cendor Monitor 0.14.1, 2026-07-26):
core.trace("id") now opens a real parent span, so a unit of work that spans several calls arrives as one trace instead of N. Before this it only stamped an ambient id, and every call inside still arrived as its own root span — measured against Cendor Monitor, a scope around a chat call and a tool call produced two unrelated rows sharing one id, with the governance fanned out across both, while the console cheerfully advised reaching for the very lever that could not move. It is a behaviour change with a one-env-var opt-out (CENDOR_TRACE_SPAN=off, or span=False per scope) for anyone whose backend groups by trace id today. Nothing is emitted when there is nobody to emit to, and no span is opened inside a cendor-sdk run — that run already owns its trace, so the calls attach to it rather than compete with it. Agent identity: Agent(id=…) rides the semconv gen_ai.agent.id, because a name is a label — two apps can share one, and a rename loses that agent's history — and three products that do own a real agent id (Bedrock Agents, OpenAI Assistants, Azure AI Foundry) now have adapter scopes that map it. Give no id and the attribute is omitted: never a hash, never a placeholder. Every governance row names its actor: a new read of core's ambient registry lets a governance.* span and the audit.* mirror name the acting agent even on the entry types that carry no agent field — a budget block above all. Measured before this release: 13 of 386 governance rows named their agent. Cendor Monitor 0.14.1 renders all of it: a trace() scope is one Calls row with STEPS = N, the Agents page shows an id beside a name, the Libraries door surfaces the agent dimension when your framework provided one, the Governance page leads with verdicts instead of trajectory and folds nine action spellings into five outcomes, and every spend table now says out loud that cost attribution is fleet-wide (a metric datapoint carries no run scope on the wire, so both doors show the same breakdown). 0.14.1 adds two corrections found by measuring the console against a real store rather than reasoning about it: a run's governance count now always equals what its own journey shows (28 of 235 measured rows said 0 in the header while the body listed three — the shared-scope events belong to the scope, and the chip that says so is the honest place to put them), and an MCP handshake no longer creates an empty run row (it fires at connect time, outside any run; it is still recorded on the MCP page). See the Observability guide.
Configure an OpenTelemetry provider the way you already would, and Cendor's telemetry flows — you write none. Governed calls become gen_ai.* spans the moment you call instrument(); spend becomes counters through an internal additive tap (your use_sink slot stays yours); run() opens its own agent.run tree with your session id as the conversation key; AuditLog(system=…) auto-attaches its mirror; and budget blocks and guardrail verdicts arrive as governance.* spans (cendor.gov.*) — so a monitoring user sees why a run stopped without adopting the evidence library. Cendor still has no endpoint, no exporter and no key: it emits into your provider, which is why on-by-default is safe. The off switch is one env var — CENDOR_TELEMETRY=off (process-wide, no code change), and CENDOR_DEBUG_TELEMETRY=1 prints one line saying what was detected. With OpenTelemetry absent, or no provider configured, nothing is emitted and nothing is even subscribed; prompt/response content stays opt-in. Explicit attachments still win, and the audit mirror always beats the ops spans — one decision, one rendering. Rule 6 holds by construction: governance.* spans carry no audit.* vocabulary and no reason string (a rule’s reason — an llm_judge verdict especially — can carry input-derived text). Also fixed on the way: @cendor/tokenguard's OTelSink bound a permanent no-op counter when constructed before your provider (the JS metrics API has no proxy), and @cendor/core's live-spans latch + the SDK's scope registry were module globals — one open scope silenced every concurrent flow, and concurrent runs mis-parented each other's steps. Cendor Monitor 0.12.0 maps the new spans into the same Governance board and inline run verdicts, keeps zero-correlation governance instead of dropping it (v0.11.0), and stores metric deltas so spend no longer inflates once per export cycle (measured 6×). Patched the same day — @cendor/core 0.15.1 / @cendor/sdk 0.23.1: the new latch used AsyncLocalStorage.enterWith, which only scopes as intended on node ≥ 24; on node 20 / 22 a closed scope left the emitter suppressed process-wide and two concurrent runs shared one scope. The automatic run scope now uses AsyncLocalStorage.run() — verified identical on node 20.20 / 22.23 / 24.18 — while a hand-closed liveSpans() handle stays process-wide while open, which is what that shape can honestly guarantee. Fixed 2026-07-26 — cendor-sdk 1.19.1 / @cendor/sdk 0.23.2: the automatic scope learned which run it belonged to from the first event on the process-wide bus, so two overlapping runs rendered one run’s call twice, dropped the other’s, and stamped both roots with one run id; and the TypeScript streamed scope was bound around the generator’s creation, which binds nothing. Both fixed, each pinned by a test verified failing first. The lesson is a test shape, not a code rule: a telemetry test with an instant stub finishes one run before the next starts, so it proves nothing about a server. Cendor Monitor 0.12.1 makes the metric-stream bound a real LRU (it evicted the first-seen stream — often the busiest — and re-baselined it), and 0.12.2 stores the money column as a bare decimal (a currency suffix from an older producer made the Postgres cost sort throw). @cendor/sdk 0.23.3 is the producer half of that: the cost span attribute is now the bare amount, matching @cendor/core and both Python paths. See the Observability guide.
SDK findings closure (cendor-sdk 1.17.0 / @cendor/sdk 0.21.1) — a fix and a parity extension, both backward-compatible. Python run.astream(checkpoint=…) was accepted and documented but never forwarded — streamed-async checkpointing was silently a no-op; fixed, now at parity with run.stream and the TypeScript twin (red-first). The multi-agent pipeline shapes sequential / parallel / parallel_async gain the honored per-run governance surface (retry / on_step / guardrails → Result.guardrail_decisions) and supervisor delegates to the full team runner with session / checkpoint, matching TypeScript 1:1 (session / checkpoint stay team-only for the pipe shapes; guardrail_mode is single-agent-only). The @cendor/sdk 0.21.1 patch is a truth-up: the README now states automatic token/cost capture is live for every provider and documents the full 0.21 surface (reaskOnOutputTrip / streamCheckWindow / streamed checkpoints / conversationId / the six telemetry domains), plus A2A serve() HTTP, SqliteSessionStore disk, and resilience-matrix test coverage. See the multi-agent guide + the parity matrix.
The SDK now emits structural telemetry, and the monitor splits into two doors — two stores, two UX modes — so libraries-only and SDK telemetry each read as their own surface. cendor-sdk 1.16 / 0.21 adds opt-in cendor.sdk child spans for RAG (rag.assemble / rag.compress), memory (memory.load / save), orchestration handoffs, checkpoints, a first-class tool domain (source local|mcp, outcome ok|error|blocked), and MCP server attribution — zero-core, both languages, content rules unchanged (labels/ids/counts, never bodies). The optional self-hosted Cendor Monitor 0.9 renders them: each door is its own store (/data/libs.db + /data/sdk.db, or Postgres per door — mixed mode supported), the console has two full UX modes with the door in the URL (/libs/…, /sdk/…) and a persistent switcher, the SDK mode gains Orchestration / Tools / MCP / RAG / Memory / Checkpoints pages, and a live channel streams updates over Server-Sent Events (live steps, not tokens). Same wire, same honesty — the SDK door adds identity and structure, never "more governance". Cendor Monitor stays optional dev tooling (like cendor-mcp); your own OTLP backend remains the default. See Cendor Monitor + the Observability guide.
Library patches — @cendor/core 0.12.2 · cendor-tokenguard 1.5.1 / @cendor/tokenguard 0.6.2 · @cendor/cassette 0.3.3 · cendor-squeeze 1.1.1 · 2026-07-24: a remediation wave — no new capability, all additive and backward-compatible. core instrument() now detects a boto-shaped converse_stream as an always-stream Bedrock target, closing the last undocumented instrument() detection gap with Python. tokenguard QueueSink gains drop observability — an on_drop_error / onDropError callback plus a dropped_rows / droppedRows() counter — so a row a failing durable sink throws on is counted and surfaced instead of silently swallowed. cassette anchors its rerecord drift buffer on the global symbol registry (two loaded copies share one buffer). squeeze now exports MemoryStore / SQLiteStore at the package top level (Python, matching the TypeScript index).
Earlier — App & agent identity (core 1.11.1 / 0.12.1 · @cendor/sdk 0.20.1 · Cendor Monitor 0.8.0, 2026-07-23): Framework agent-name adapters + a monitor "Apps" top level — identity surfaced where it is already true, never invented by core. core 1.11 / 0.12 adds two optional framework adapters that carry a third-party framework's agent identity onto the bus (so a monitor's Agents page fills for framework-driven stacks), mirroring the shipped langchain handler: cendor.core.openai_agents / @cendor/core/openai-agents for the OpenAI Agents SDK (the agent's model calls ride the standard OpenAI client, so instrument() still captures tokens/cost/streaming — the adapter supplies only the name), and cendor.core.foundry / @cendor/core/foundry for Azure AI Foundry Agents (stamps agent + conversation_id, attribution-only since the model runs server-side). Core carries no identity of its own; importing an adapter registers nothing until you attach. Cendor Monitor 0.8 adds the Apps page — the libs door's top level, grouping runs by their OTel service.name with a distinct-instance count from service.instance.id; an app's identity is its standard OTel resource (set OTEL_SERVICE_NAME), not anything Cendor invents. @cendor/sdk 0.20.1 re-pins @cendor/core ^0.12.0 so a fresh install resolves a single core. Additive and backward-compatible. See the core adapters + parity matrix.
Earlier — Streaming truth + mid-stream breaker (core 1.10.0 / 0.11.0 · tokenguard 1.5.0 / 0.6.0 · @cendor/sdk 1.15.0 / 0.20.0, 2026-07-23): SDK Phase-S follow-up (@cendor/sdk 1.15.0 / 0.20.0) closes the parity items the provider-capabilities wave deferred — both languages, on the same core 1.10 / tokenguard 1.5 shelf. Streamed and multi-agent runs now stamp conversation.id from a keyed session (a monitor groups the runs of one thread); run.stream / run.astream take a checkpoint (per-turn + per-segment saves; a done-resume replays a lone RunComplete, an unfinished resume continues without re-showing prior deltas); TypeScript gains bounded output-block re-ask + an incremental streaming output-window check; the TS span tree reaches Python parity (provider, latency, finish reason, streamed flag, error, tool arg-names; live children backdated by latency; a 3-level per-agent tree); and Bedrock gains forced-toolChoice structured output, gated to tool-less agents. Streaming re-ask is offered in neither language (a streamed answer’s deltas can’t be unshown). Additive and backward-compatible. See the parity matrix.
A budget can now stop a runaway stream mid-flight, streamed estimates see visible thinking, and Anthropic streams token-by-token in both languages. core 1.10 / 0.11 adds a per-chunk stream-observer seam (raising aborts the stream, finalizing once with the partial estimated usage) and counts visible thinking into streamed estimates (Anthropic thinking_delta, Ollama message.thinking, OpenAI-compat reasoning_content, Bedrock reasoningContent); it also captures Bedrock converse_stream (Py) and repairs a misdetected async client's usage. tokenguard 1.5 / 0.6 rides that seam for on_exceed="break" — cut a streamed call the instant its running output estimate crosses the cap (you keep the partial output; the provider bills to the cut — it stops the meter, it does not un-bill), and clamp now injects the ceiling on Bedrock / Ollama / dict-config Gemini too. The SDK 1.14 / 0.19 turns those into Anthropic incremental streaming + ThinkingDelta, native Anthropic structured output (output_config.format), and Ollama/Bedrock data-URL images — with Bedrock run.aio no longer blocking the loop. Every number is measured; the breaker stops the meter within ~one chunk + one RTT, it does not un-bill the provider. See the tokenguard and Observability guides.
Earlier — Ambient metadata seam (core 1.9.0 / 0.10.0 · @cendor/sdk 1.13.0 / 0.18.0, 2026-07-22): Run context is now stamped onto every event the moment it is built — not read back later, when the scope may already be gone. A new core-owned pre-emit capture point (add_ambient_provider() / addAmbientProvider()) attaches the ambient run context — agent, conversation id, budget frames, decision id, cassette session — to each event at construction, so acttrace, cassette, and live_spans read it from the event instead of a delivery-time ambient read that can arrive too late. This closes a class of "capture read too late" bugs: tokenguard streamed spend that drained out of scope now accrues, enforces, and attributes (cumulative caps hold even under block), and Python stream generators no longer leak a run scope into the consumer's next call. BudgetEvent gains a trace_id, and a new additive ThinkingDelta stream event surfaces model thinking token-by-token. Additive and backward-compatible; evidence, not a compliance guarantee. See the Observability guide.
Earlier — Governance→run linkage (core 0.9.0 · acttrace 1.9.0 / 0.10.0 · @cendor/sdk 0.17.0, 2026-07-22): A governance event now links back to the run that produced it. Inside live_spans() / liveSpans, the run's root span is the active context span for the whole run — so acttrace stamps each audit entry with the run's trace id (cendor.audit.otel_trace_id) and the audit.* mirror spans nest in the run's trace. TypeScript @cendor/sdk 0.17 makes liveSpans activate the run root (Python live_spans always did), closing a TS-only gap. acttrace 1.9 / 0.10 also stamps run_id → cendor.audit.run_id, the fallback a trace-aware monitor joins on when no OTel span was active (a post-hoc span_tree, or an app with no context manager). core 0.9's otel.span() activates its span too (TS parity). Additive and backward-compatible; the audit file stays the sole verify() evidence; a no-op without OpenTelemetry. See the Observability guide.
Earlier — Emission-truth wave (core 1.8.0 / 0.8.0 · sdk 1.12.0 / 0.16.0, 2026-07-21): Truth on a governed journey — time-to-first-token, estimated-vs-real streamed tokens, and the run's agents, so a monitor never over-claims. The SDK's span_tree / live_spans now stamp cendor.ttft_ms on a streamed chat span (TTFT inside a real governed journey, not just a bare libs-only call) and cendor.usage_estimated="true" when a streamed token count was recovered by an offline estimate rather than reported by the provider — so a monitor renders those tokens as est. instead of exact. live_spans stamps cendor.run.agents on the run root at close (parity with span_tree), so an Agents view fills for live-streamed runs. core 1.8 / 0.8's libs-only use_span_emitter() carries the same estimated flag. Additive and backward-compatible; a no-op without OpenTelemetry. See the Observability guide.
Earlier — Journey-view wave (core 1.7.0 / 0.7.0 · squeeze 1.1.0 / 0.3.0 · acttrace 1.8.0 / 0.9.0 · sdk 1.11.0 / 0.15.0, 2026-07-20):
Opt-in content on the wire — prompts, responses, thinking, tool values — OFF by default, standards-native, and never in the audit chain. Turn it on with otel.capture_content() / captureContent() (core 1.7 / 0.7) or the standard OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var, and live_spans / span_tree stamp gen_ai.input.messages / output.messages (including parsed thinking) + system_instructions + tool arg/result — masked (fail-closed) and byte-capped. Because it's the semconv standard, the same content renders in Langfuse or Braintrust too. run(session=…) auto-groups a multi-turn conversation (gen_ai.conversation.id from the session key); squeeze emits a metadata-only CompressionEvent so compression stops being dark; streamed calls stamp cendor.ttft_ms. Content lands only where your OTLP goes — Cendor never receives it, and audit.* spans stay content-free (rule 6). Additive and backward-compatible; a no-op without OpenTelemetry. See the Observability guide.
Earlier — Monitor-truth wave (tokenguard 1.3.0 / 0.4.1 · acttrace 1.7.0 · guardrails 1.6.0 / 0.7.1 · sdk 1.10.0, 2026-07-20):
The audit mirror now carries the answers, not just labels — so a monitor can show which budget acted, what it blocked, and which guardrail fired at which stage. budget(name=…, description=…) gives a budget a human identity (tokenguard 1.3 / 0.4), and acttrace 1.7 / 0.8 mirrors it as cendor.audit.budget alongside the numeric projected-vs-cap figures (money as strings), plus llm_call usage/latency, guardrail severity + policy version, and context-assembly block counts. Two native governance counters — cendor.tokenguard.budget.events and cendor.guardrails.decisions — make block/flag rates chartable. The SDK's live_spans / span_tree now name the agent + number the step on every call span, reach parity between live and post-hoc, and accept an opt-in label= → cendor.run.label (never derived from the prompt). Additive and backward-compatible; a no-op without OpenTelemetry — Cendor stays local-first. See the Observability guide.
Earlier — OpenTelemetry observability export (acttrace 1.6.0 / 0.7.0 · tokenguard 1.2.0 / 0.3.0 · sdk 1.8.0 / 0.12.0, 2026-07-19):
Governance events now reach your observability stack — Azure Monitor, CloudWatch, Datadog, Grafana, or any OTLP backend — with no Cendor-specific exporter. Attach AuditLog(mirror=OTelMirror()) and every chained entry — decisions, guardrail actions, budget breaches, human oversight — is also emitted as an audit.<type> OpenTelemetry span, an operational copy for monitoring and alerting; the hash-chained file stays the sole verify() evidence. A pre-flight budget action (blocked/downgraded/clamped) now rides the bus as a BudgetEvent — the only signal a refused call ever leaves — and OTelSink dimensions spend by your track() tags. Entries carry otel_trace_id so you can pivot from an APM trace to the audit entry. The SDK re-exports OTelMirror + BudgetEvent. All of it is opt-in and a no-op without OpenTelemetry — Cendor stays local-first. See the new Observability guide.
The SDK's promise is that its governance is the libraries, re-exported. This wave makes that literally true and pins it in CI. guard is now the identical acttrace object (sdk.guard is acttrace.guard — acttrace 1.5.0 / 0.6.0's return is dual-shape: the raw interceptor is also the scope form). embed() is governed pre-flight: core 1.6.0 / 0.6.0 captures openai-shaped embeddings.create, so a keyless budget(usd=…, on_exceed="block") refuses an over-budget embedding call before it fires, and the snapshot prices the text-embedding-* ids. The TypeScript rules namespace reaches full Python parity (spotlight, the detection-tier adapters, groundedness/denied-topics), and the pii/secrets bridge now honors per-category policy actions — a gdpr special_category finding blocks even under action="redact". A new parity/identity test suite in both SDKs pins every re-export, so the next drift fails the build instead of shipping. See Architecture.
Follow-up — sdk 1.9.0 / 0.13.0 · 2026-07-20: live_spans / span_tree now accept an optional conversation_id that stamps gen_ai.conversation.id on the root agent.run span — so a backend can group the runs of one multi-turn conversation. Opt-in and additive; a no-op without OpenTelemetry.
Libraries
| Package | PyPI (cendor-*) | npm (@cendor/*) |
|---|---|---|
| core | 1.21.0 (opens in a new tab) | 3.8.0 (opens in a new tab) |
| tokenguard | 1.8.0 (opens in a new tab) | 3.2.0 (opens in a new tab) |
| guardrails | 1.7.0 (opens in a new tab) | 3.1.1 (opens in a new tab) |
| contextkit | 1.1.0 (opens in a new tab) | 3.1.0 (opens in a new tab) |
| squeeze | 1.1.2 (opens in a new tab) | 3.1.0 (opens in a new tab) |
| cassette | 1.1.1 (opens in a new tab) | 3.0.0 (opens in a new tab) |
| acttrace | 1.14.0 (opens in a new tab) | 3.1.0 (opens in a new tab) |
| libs (umbrella) | 1.2.0 (opens in a new tab) | 3.0.0 (opens in a new tab) |
| cendor (alias) | 1.1.0 (opens in a new tab) | — |
SDK
| Package | PyPI (cendor-*) | npm (@cendor/*) |
|---|---|---|
| sdk | 1.22.2 (opens in a new tab) | 3.2.2 (opens in a new tab) |
Dev tooling (optional — no library depends on it)
| Package | PyPI (cendor-*) | npm (@cendor/*) |
|---|---|---|
| mcp | 0.1.7 (opens in a new tab) | 0.1.7 (opens in a new tab) |
| init | 0.3.0 (opens in a new tab) | 0.4.1 (opens in a new tab) |
Cendor Monitor ships as a Docker image, not a registry package: ghcr.io/cendorhq/cendor-monitor:0.15.0 — public, multi-arch (amd64 + arm64), no login needed. The optional, self-hosted journey view over standard OTLP — agents → sessions → runs, the full run journey (prompts/responses/thinking/tokens/cost, content opt-in) with governance verdicts inline, and 7 per-library proof pages. v0.15 adds the downgrade guard: an image that opens a store written by a newer image now refuses to start — naming the door, the schema version it found, and the one it supports — instead of silently stamping schema_version backwards while reading columns it does not know, which left the recorded version a lie for every later migration to misfire on. The check runs before any DDL, so a refusal leaves the store exactly as found, and a store with no version row is a normal first boot rather than a downgrade. Rolling an image back is a legitimate operator action; corrupting the version track while doing it is not — and because startup behaviour changes, it is a minor, not a patch. The operations docs gain the upgrade contract, leading with the answer to the question people actually ask: your audit evidence was never in this container — the hash-chained file on your app host is the only thing verify() reads. Upgrading is additive and lossless; retention is what removes data (7 days per door by default), and :latest moves, so pin a tag. v0.14.2 styles the sidebar’s pinned Settings / Docs pair, which had never matched the other nav items and collided into one word below 820px. v0.14 makes a libs scope a real call group and treats an Agent(id=…) as identity — emitted only when you give one, never invented (schema v2); v0.14.1 stopped a run row contradicting its own journey, and an MCP handshake no longer invents an empty run. v0.13 gives each governance outcome one word and puts the verdicts before the trajectory. v0.12 adds the agent edge, the libs agent dimension, and an honest money table (every spend surface says what it counts). v0.10 gives the container its own door-less page, so every setting belongs to exactly one door; v0.10.1 removed the demo app — the monitor ships no sample data and fabricates nothing. v0.9 split the two doors into two stores (Libraries / SDK), path-routed and never mixed. v0.4 added the filter bar + indexed full-text search over content, dependency-free trend charts, the "what Cendor saved" value strip, and cost attribution by feature/user. v0.3 dropped Tempo/Prometheus/Perses for a Cendor ingest + store (SQLite default / external Postgres); the image is Apache-2.0 (code) with OFL-1.1 fonts. No library depends on it. Full page: /monitor.
Per-release changelogs live with each package: Python on cendor-libs (opens in a new tab) / cendor-sdk (opens in a new tab), TypeScript on cendor-libs-js (opens in a new tab) / cendor-sdk-js (opens in a new tab). The unscoped cendor npm alias ships later.
How often each of these is installed: /downloads — per package, per registry, recorded daily. PyPI figures exclude index mirrors; npm publishes no mirror filter, so the two are never summed.