跳到主要内容

Changelog

All notable changes to CubePi are documented here.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

0.13.0 - 2026-07-05

Added

  • Reasoning capability primitivesReasoningProfile, ThinkingControl, and AppliedReasoningControl types for fine-grained reasoning configuration. Models that support extended thinking (Anthropic extended-thinking API) now expose budget, level, and token tracking through the provider layer.
  • Reasoning controls render in all providersBoundModel streams now carry reasoning state per-turn. Rendered thinking blocks are available in message content for inspection and tracing.
  • Run-scoped compaction and real-token triggeringCompactionMiddleware can now compress history within a single long agentic run (previously compression only occurred at run boundaries). Compaction triggers on true context fill (input + cache_read + cache_write from the last turn) instead of cache-blind character estimates, so prompt caching can't mask genuine over-limit context.

Fixed

  • Tool-batch fault isolation. One escaping exception in a parallel tool batch (HitlControlException, CancelledError, or BaseException from after_tool_call) used to abort the bare-await collection loop, dropping every ToolResultMessage in the batch — including succeeded siblings — and leaving dangling tool_calls in the checkpoint. Now _execute_parallel settles every task and synthesizes error results for failures, preserving all checkpoint state and emitting events in correct order on suspend/resume.
  • Parallel HITL approval replay. Fixed answer ledger replay to correctly handle parallel tool batches with mixed approved/pending tool calls. Added explicit answer ledger persistence across all checkpointer backends (sqlite/postgres/mysql) so HITL resume paths have a durable record of prior approvals.
  • Checkpointer corruption detection. Load paths now wrap per-row deserialization in CheckpointCorruptionError with thread_id/backend context. Unknown roles are now treated as corruption in all backends instead of silently passing through.
  • Reasoning field gating. Fixed rendering of reasoning fields to only appear when model.reasoning is enabled, preventing type mismatches on models that don't support extended thinking.
  • Checkpointer v5 schema support. Restored compatibility with v5 schema migrations across all backends.

0.12.0 - 2026-06-24

Added

  • CompactionMiddleware(tool_result_compressor=...) — a Callable[[ToolResultMessage], str | None] callback for selective tool result preservation during compaction. Return a str to preserve that text verbatim in the summary (for grounding/citation); return None to fall through to default pruning. Preserved results are appended to the summary as a labeled reference section, excluded from the summarizer input to save budget, and accumulated across compaction rounds via CompactionState persistence.
  • Sender attribution at the provider boundary. UserMessage.metadata can now carry sender_user_id / sender_display_name; providers prefix the first text block with [Name]: when converting to the API format. Keeps stored message content clean while letting the model know who sent each turn in group-chat scenarios.
  • Deferred tool ordering hint. The dispatcher description now hints models to emit tool_name before arguments, smoothing streaming UX for dispatch-mode deferred tools.

Fixed

  • Removed stale (latest) labels from Chinese 0.7 and 0.8 version docs.

0.11.0 - 2026-06-17

Changed (BREAKING)

  • Deferred tool groups default to the new dispatch strategy. Tool schemas are delivered through load_tools results and invoked via the deferred_tool_call dispatcher; the tools array and system prompt stay byte-stable, so expansions no longer invalidate the prompt cache. Restore the v0.10 behavior with Agent(deferred_tool_strategy="inject") / DeferredToolsMiddleware(strategy="inject").
  • DeferredToolsMiddleware(resumed_schemas=...) and ResumedState.expanded_schemas are removed; prepare_resumed_state takes a required strategy keyword (a default could silently resume an inject-mode host with hidden tools).
  • Inject mode no longer renders expanded schemas into the system prompt. The definitions were already in the tools array — the duplicate rendering (double token billing per turn) is gone.

Added

  • resolve_tool_call middleware hook — rewrite a tool call before validation, before_tool_call, execution, events, and tracing see it. Composition is first-non-None-wins. Powers the deferred dispatcher; also usable for tool aliasing/redirection.
  • AgentTool.expose_to_model — when False, the tool is resolvable and executable by the engine but its definition is never sent to the provider. Dispatch-mode deferred tools use this.
  • Agent(deferred_tool_strategy=...) and DeferredToolsMiddleware(strategy=...) — choose "dispatch" (default) or "inject".
  • Resolved dispatcher calls that fail argument validation get the tool's full schema appended to the error result, so the model can self-correct in one round trip.

Fixed

  • HITL resume short-circuit now emits HitlAnswerEvent. Previously, _await_answer's resume path returned the pre-loaded answer without emitting the event, so subscribers (e.g. IM outbound tailers) never learned the question was answered.
  • Explicit resolve_tool_call composes with middleware resolvers instead of replacing the chain. An explicit resolver passed to Agent(resolve_tool_call=...) becomes the chain head (first-non-None-wins) rather than silently disabling middleware-provided resolvers like the deferred dispatcher.

0.10.0 - 2026-06-10

Removed (BREAKING)

  • "minimal" removed from ThinkingLevel. ThinkingLevel now reads Literal["off", "low", "medium", "high", "xhigh"]; the .minimal field is gone from ThinkingBudgets; THINKING_LEVELS no longer contains it; Anthropic's default level_budgets and OpenAI Responses' _THINKING_TO_EFFORT no longer map it. Callers that previously passed thinking="minimal" must switch to thinking="low" (or "off"). Rationale: DeepSeek's Anthropic-shape endpoint rejects effort=minimal on output_config, and OpenAI's reasoning.effort path rewrote it to "low" downstream anyway — keeping it was a footgun that surfaced as a 400 + fallback.

Added

  • synthetic_user_message(text, *, source) -> UserMessage and is_synthetic_message(message) -> bool — public marker for framework-injected user-role messages. Middleware-injected nudges (todo guard errors, goal continuations, compaction summaries, generate_structured retry feedback) now stamp metadata["synthetic"] = True so downstream UIs can tell internal scaffolding apart from real human input. Real Agent.prompt() / Agent.steer() messages remain unmarked. Closes #171. Exported from cubepi and cubepi.providers. Use this factory (not bare UserMessage) when returning messages from TurnAction.inject_messages or on_run_end.

  • DeferredToolGroup / DeferredToolsMiddleware — progressive tool disclosure primitive. Hides MCP tool schemas from the model by default, injecting a compact catalog into the system prompt instead. The model expands groups on demand via the built-in load_tools tool (full or selective). Key properties:

    • Catalog sorted by group_id for byte-stable system prompt prefix.
    • Expanded schemas append-only (expansion order, never reordered) for prompt-cache prefix stability across turns.
    • Loader called once per group per run; selective expansions filter from the cached result.
    • Agent(deferred_tool_groups=[...]) — primary API. Middleware is auto-created internally with extra_ref bound to self._extra.
    • Cross-run replay via DeferredToolsMiddleware.prepare_resumed_state(), which returns pre-loaded tools, remaining groups, and expanded schemas for prompt-cache continuity.
    • Exported from cubepi.deferred as DeferredToolGroup, DeferredToolsMiddleware, and ResumedState.
  • tool_choice on Provider — new tool_choice: ToolChoice | None parameter on BoundModel.stream(), BoundModel.generate(), and the Provider protocol. Accepts "auto", "required", "none", or a specific tool name string. Each built-in provider maps the value to its native wire format (Anthropic: {"type": "any"} for "required", OpenAI: "required", etc.). FauxProvider accepts and ignores the parameter. Type alias: ToolChoice = Literal["auto", "required", "none"] | str, exported from cubepi.providers.base.

  • BoundModel.generate_structured() — tool-based structured output. Pass a Pydantic BaseModel subclass and get a validated instance back:

    from pydantic import BaseModel

    class Sentiment(BaseModel):
    label: str
    confidence: float

    result = await model.generate_structured(
    Sentiment,
    messages=[UserMessage(content=[TextContent(text="Great product!")])],
    )

    Injects a synthetic tool from the model's JSON schema, forces the call via tool_choice, and validates the response with output_type.model_validate(). Retries on validation failure (configurable max_retries, default 1). Raises StructuredOutputError on no tool call or validation exhaustion.

  • GoalMiddleware — autonomous goal-driven agent runs. A separate evaluator model judges whether a /goal condition has been met after each worker run (dual-model architecture — the agent isn't grading its own homework). Continues until the evaluator confirms or max_evaluations is hit. Outcome in agent.state.extra["goal"].

    from cubepi.middleware.goal import GoalMiddleware

    goal = GoalMiddleware(
    evaluator=provider.model("claude-haiku-4-5-20251001"),
    max_evaluations=10,
    )
    agent = Agent(model=provider.model("claude-sonnet-4-6"), middleware=[goal])
    await agent.prompt("/goal all tests pass")

    Exported from cubepi.middleware as GoalMiddleware.

Changed

  • on_run_end fires on every outer-loop iteration instead of once per prompt() call. The _reflection_fired single-fire guard has been removed. Existing middlewares that return None after one injection are unaffected. This enables evaluation loops like GoalMiddleware.

Changed

  • Internal logging now uses stdlib logging exclusively. Previously FallbackBoundModel and the provider listener-exception path tried to import loguru first and fell back to stdlib. The loguru path was silently incorrect — loguru does not perform %s argument substitution, so failover warnings rendered literal %s placeholders instead of the resolved labels. cubepi has never declared loguru as a dependency; hosts that prefer loguru should intercept stdlib logging records into it. No public API change.

Fixed

  • FallbackBoundModel failover log line now substitutes its placeholders. Before the loguru removal above, the WARNING emitted on every failover read failed=%s → next=%s reason=%s attempt=%s/%s literally because the loguru-backed logger ignored the positional args. Now renders as failed=anthropic/claude-opus-4-5 → next=openai/gpt-5 reason=… attempt=1/2.

  • Recorder.attach() and Meter.attach() now subscribe to every provider in a FallbackBoundModel chain (closes #167). Previously they only listened to chain[0].provider, so post-failover calls executed against chain[1..] were invisible to provider-level observability — chat spans, token usage, cache metrics, and cost telemetry were missing for fallback legs. Adds a new cubepi.providers.fallback.chain_providers() helper used by both attach paths to walk and dedupe the chain. Agent-event-driven observability (e.g. cost middleware reading MessageEvent) was already correct and is unchanged.

0.9.0 - 2026-06-08

Added

  • TodoListMiddleware — built-in task-tracking middleware for multi-step agents. Adds a write_todos tool that lets the model maintain a structured checklist (pending / in_progress / completed). Includes:

    • Finalization guard — if the model delivers a plain-text final response while items remain unfinished, it is looped back once to update the list before the run ends.
    • Stale-todo reminder — a soft UserMessage is injected after several turns without a write_todos call, prompting the model to keep the list in sync without blocking.
    • Parallel-call guard — if the model calls write_todos more than once in a single turn, the duplicates are rejected and the checklist is rolled back to its pre-turn state.
    • State (todos, guard counters) lives in AgentContext.extra and survives checkpointing.
    • Constructor: TodoListMiddleware(extra_ref=..., tool_description=..., system_prompt=...). extra_ref must return the live AgentContext.extra dict (same object, not a copy) so the tool executor can write into it.
    • Exported from cubepi.middleware as TodoListMiddleware, Todo, WriteTodosInput, and TodoGuardBlocked.
  • FallbackBoundModel — built-in failover chain at the BoundModel level. Wrap an ordered chain of BoundModel instances; on RateLimited, ProviderUnavailable, or ContextLengthExceeded (configurable via trigger_errors), or on a first-event stream error, the next model in the chain is tried transparently. Optional on_failover callback for billing/metrics hooks. Exported from cubepi and cubepi.providers.

  • DEFAULT_TRIGGER_ERRORSfrozenset({RateLimited, ProviderUnavailable, ContextLengthExceeded}). The default set of error types that trigger failover in FallbackBoundModel.

  • BoundModel.generate() / BoundModel.stream() — the handle returned by provider.model(...) now drives a provider call directly. Useful for utilities (summarizers, classifiers) where you already hold a BoundModel and want to skip the agent loop:

    bound = provider.model("claude-sonnet-4-6")
    reply = await bound.generate(
    messages=[UserMessage(content=[TextContent(text="hi")])],
    system_prompt="Be brief.",
    )

    Both methods forward to the bound provider with model=bound.spec and mirror the Provider.generate / Provider.stream signatures exactly.

Breaking

  • Middleware.extra_llm_calls() returns Iterable[BoundModel] instead of Iterable[tuple[Provider, Model]]. Third-party middleware overriding this hook must update the return shape (see Migration). The recorder consumer in cubepi.tracing was adapted in lock-step; built-in CompactionMiddleware already updated.
  • cubepi.middleware.compaction.summarizer.summarize() takes model: BoundModel instead of separate provider: Provider, model: Model kwargs. Direct callers (rare — this is internal to CompactionMiddleware) must wrap the pair. The public CompactionMiddleware(summary_model=...) API is unchanged.
  • cubepi.run_agent_loop and cubepi.run_agent_loop_continue take model: BoundModel instead of separate provider: Provider, model: Model kwargs. Stateless-loop callers driving the loop outside of Agent must update. The Agent API is unchanged — it already took model: BoundModel.

Migration

  • Middleware authors overriding extra_llm_calls():

    from cubepi.providers.base import BoundModel

    # Before
    def extra_llm_calls(self):
    return [(self._provider, self._model_spec)]

    # After — either build one explicitly…
    def extra_llm_calls(self):
    return [BoundModel(provider=self._provider, spec=self._model_spec)]

    # …or, if your middleware already holds a BoundModel (recommended),
    # just return it:
    def extra_llm_calls(self):
    return [self._bound_model]
  • Direct summarize() callers (uncommon):

    # Before
    await summarize(provider=provider, model=model_spec, ...)

    # After
    await summarize(model=BoundModel(provider=provider, spec=model_spec), ...)
  • Stateless-loop callers (uncommon — most users build an Agent):

    # Before
    await run_agent_loop(
    prompts=[...],
    context=ctx,
    provider=provider,
    model=model_spec,
    convert_to_llm=...,
    emit=...,
    )

    # After
    await run_agent_loop(
    prompts=[...],
    context=ctx,
    model=provider.model("id", ...),
    convert_to_llm=...,
    emit=...,
    )

Fixed

  • StructuredValue fields now preserve BaseModel payloads on serialization. Fields typed StructuredValue (the JsonPrimitive | BaseModel | list | dict union used by tool-result details, AgentToolResult, HitlAnswerEvent.answer, and compaction message-ref hashing) silently serialized BaseModel instances to {} on model_dump(). Pydantic's union dispatch picks the dump schema from the declared base, not the runtime subclass, so the concrete instance's fields were ignored with no error or warning — data gone. Annotating the BaseModel branch with SerializeAsAny[BaseModel] fixes the silent loss across all five affected sites: checkpointer save, compaction state, ToolExecutionEndEvent, ToolExecutionUpdateEvent, and HitlAnswerEvent.

  • SubagentMiddleware now strips checkpointed-HITL elements from a child agent's inherited tools / middleware. Previously, passing the parent agent's ask_user_tool(channel) in shared_tools (the common pattern when the host wants tools shared between parent and children) caused the child's first prompt() to raise Agent has checkpointed HITL elements bound to run_ids ... because the binding's parent run_id didn't match the child's fresh run_id. The middleware now drops any element whose .hitl is a checkpointed HitlBinding before constructing the child — the subagent runs autonomously without the parent's HITL channel, matching its "ephemeral and autonomous" design intent. Elements without .hitl, or with non-checkpointed bindings, are inherited as-is.

0.8.0 - 2026-06-06

Added

  • @tool decoratorcubepi.tool builds an AgentTool from a plain async function: the input schema is generated from the typed parameters (honouring Field(...) metadata), the docstring becomes the description, and the loop-supplied tool_call_id / signal / on_update are injected only when declared. Tools may return a str, a Content, a list of content, or a full AgentToolResult. The longhand AgentTool(...) remains fully supported.
  • Conversation fork — fork a thread at a completed-run boundary, or run a one-shot ephemeral continuation against a snapshot:
    • Agent.fork(src, new, *, after_run_id, metadata=None) — physical-copy fork at a completed-run boundary.
    • Agent.fork_once(src, message, *, after_run_id) -> ForkOnceResult — single-turn ephemeral continuation, no checkpointer writes.
    • Agent.prompt(message, *, run_id=None) -> str now accept-or-generates the run_id and returns it.
    • Agent.state.active_run_id exposes the in-flight run_id.
    • Agent(messages=...) constructor arg for ephemeral pre-seeded history.
    • Checkpointer.snapshot, fork, claim_run, mark_run_complete, load_pending Protocol methods.
    • cubepi_runs table per backend (Postgres / MySQL schema v3 → v4).
    • Message.run_id: str | None field on all three Message variants.
    • HitlBinding attribute on AgentTool / Middleware; ask_user_tool and ApprovalPolicyMiddleware populate it.

Fixed

  • Tool results that set is_error=True are now reported as errors. A tool body that returned AgentToolResult(is_error=True) without raising was surfaced to the model as a successful result; the flag is now honored on the execution path (affects both @tool and longhand AgentTool tools).

Breaking

  • Agent.prompt() return type changed from None to str. Callers ignoring the return value keep working.
  • Checkpointer Protocol gained 5 new methods. Third-party v3-only checkpointers continue to work for vanilla prompt() via degraded mode; fork APIs raise CheckpointerError on such backends.

Migration

  • Postgres / MySQL: run the new alembic helper (see backend guides).
  • SQLite: auto-migration at connect time.
  • Legacy run_id=NULL messages remain readable; threads with only such messages are not forkable.

0.7.0 - 2026-06-05

Added

  • Provider.generate(...) one-shot helper — providers now expose a non-streaming call that consumes stream() and returns the final AssistantMessage. It supports per-call overrides for max_output_tokens, temperature, thinking, and thinking_budgets.
  • Compaction middlewarecubepi.middleware.CompactionMiddleware summarizes older conversation turns into JSON-safe AgentContext.extra state and sends the model a compressed view while preserving full agent history.
  • Subagent middlewarecubepi.middleware.SubagentMiddleware adds a subagent tool that runs an ephemeral child Agent, supports shared tools and middleware inheritance, captures child events, and exposes host callbacks for application-specific event streaming.
  • Typed provider error taxonomy — built-in providers wrap SDK failures in ProviderError subclasses such as ContextLengthExceeded, RateLimited, ProviderAuthFailed, ProviderUnavailable, and ProviderBadRequest.
  • Tracer.oneshot(...) — trace a single background LLM call without a full Agent loop; the trace CLI can filter these runs via --meta oneshot_operation=....
  • Checkpointed HITL run IDs — checkpointers now persist the owning run_id atomically with pending HITL requests so hosts can resume the correct detached run after a restart.
  • on_run_end middleware hook — fires exactly once after all turns and tool calls complete, before AgentEndEvent. Return a list[Message] to inject additional messages and run one extra model turn (e.g. a memory-reflection pass); return None to do nothing.
    • _reflection_fired guard in the loop prevents the injected turn from re-triggering on_run_end.
    • should_stop_after_turn and turn_action.decision == "stop" paths now route through on_run_end before emitting AgentEndEvent. Error/aborted runs (stop_reason in ("error", "aborted")) skip the hook.
    • HITL-interrupted runs (HitlDetached / HitlAborted) also skip the hook — the conversation is paused, not finished.
  • Compaction summarizer tracingCompactionMiddleware's summary call is now first-class in the trace tree. A cubepi.compaction.summarize parent span (carrying cubepi.compaction.message_count) wraps the summarizer LLM call, and the recorder auto-subscribes the middleware's summary_provider so its chat span lands as a child:
    invoke_agent
    └── cubepi.turn
    ├── cubepi.compaction.summarize
    │ └── chat <summary-model>
    └── chat <main-model>
    Previously summarizer calls bypassed the trace entirely.
  • Middleware.providers() protocol — middleware authors can override this method to expose any extra BaseProvider instances the middleware owns; Recorder.attach() walks agent._middleware and wires its listener registry on each one (id-deduped against agent._provider). Default is empty, so existing custom middleware needs no change.

Changed

  • Breaking: agent construction now takes a bound model from provider.model(...). Replace Agent(provider=provider, model=Model(...)) with Agent(model=provider.model("model-id", ...)). provider_id now lives on provider constructors and is copied into model metadata used for tracing, response metadata, and error messages. Tracer.oneshot(...), CompactionMiddleware, and SubagentMiddleware use the same bound-model shape.

  • Breaking: pre-model middleware hooks now receive AgentContext directly. Update custom middleware and explicit hook callables from the old signatures:

    • transform_context(messages, *, signal=None)
    • transform_system_prompt(system_prompt, *, signal=None)
    • convert_to_llm(messages)

    to the new signatures:

    • transform_context(messages, *, ctx, signal=None)
    • transform_system_prompt(system_prompt, *, ctx, signal=None)
    • convert_to_llm(messages, *, ctx)

    Use ctx.extra for middleware state that should survive checkpointing. This release does not include old-signature compatibility shims.

  • Breaking: custom providers must implement Provider.generate(...) or inherit from BaseProvider, which supplies generate() by consuming stream().

  • Breaking: Postgres and MySQL checkpointer schemas are now version 3. Host applications must add the nullable run_id column to cubepi_threads and call write_schema_version_op() in their Alembic migration before using this release.

  • Breaking: the cubepi.providers.images surface has been redesigned to align with the chat-provider 0.7 conventions: providers now take provider_id and an optional ImagesCapabilityDescriptor; models are built via provider.model("id", ...) (renamed provider field to provider_id, added default_size/n/quality/output_format and cost metadata); ImagesContext is typed (size/n/quality/output_format/seed/ negative_prompt/steps/guidance/extra); per-call options live on a new ImagesOptions bag (signal, on_payload, on_response); failures raise cubepi.errors.ProviderError subclasses instead of in-band AssistantImages.error_message; the create_images_provider / register_images_provider_class registry is removed. The new shape reaches OpenAI, Doubao Seedream, SiliconFlow, and Together AI through a single OpenAIImagesProvider configured with the right ImagesCapabilityDescriptor.

  • CI now runs mypy cubepi in addition to pytest and ruff.

Fixed

  • Tool argument ValidationErrors are formatted as model-readable tool results, including literal and extra-field errors.
  • Provider stream-level SDK exceptions are classified into typed cubepi errors instead of leaking raw vendor exception types.
  • Tracer.oneshot() now closes chat spans on failure/cancellation, awaits stream completion and flushes, forwards abort signals, and surfaces silent producer failures.
  • Anthropic empty-assistant recovery — a persisted AssistantMessage(content=[], stop_reason="error") (e.g. from a transient provider failure that the checkpointer recorded) no longer poisons the conversation:
    • Trailing empty assistant turns are dropped before send (the next request regenerates from the preceding user prompt instead of treating the placeholder as an assistant prefill).
    • Mid-history empty assistant content is replaced with an [empty response] text block so the wire payload satisfies the Anthropic API's messages.N: all messages must have non-empty content rule.
    • The trim happens in stream() before the cache policy resolves breakpoints, so the user's last-message cache_control marker survives on retry paths.
  • Tracing root attribution under middleware-driven providers — when a middleware provider (e.g. CompactionMiddleware.summary_provider) issues the first chat span of a run before the agent's main call, the root invoke_agent span's gen_ai.provider.name / cubepi.agent.system_prompt_sha256 / cubepi.agent.tools are no longer overwritten by the middleware's values. The recorder now gates root attribution by model identity (not listener identity), and falls back to first-call-wins when a middleware declares the same (provider, model) pair as the agent's main.
  • The compaction summarizer's wrapper span now installs turn_span as the OTel current span, so cubepi.compaction.summarize lands as a child of cubepi.turn instead of becoming an orphan root.

Removed

  • Removed unused PyYAML from the default dependency set; the core dependency set is back to anthropic, openai, and pydantic.

0.6.0 - 2026-05-31

Added

  • Human-in-the-Loop (HITL) — first-class suspend/resume for agent runs that need a human decision mid-flight:
    • HitlChannel protocol with InMemoryChannel and CheckpointedChannel implementations. CheckpointedChannel persists pending requests so an agent can be resumed after a process restart.
    • Agent(channel=...) wiring; agent.respond(), agent.detach(), and agent.abort_pending() for external controllers.
    • ApprovalPolicyMiddleware and ConfirmToolCallMiddleware for declarative tool-call gating; ask_user built-in tool for open-ended prompts.
    • ScriptedChannel and NoopChannel for deterministic testing.
    • HitlRequestEvent, HitlAnswerEvent, AgentSuspendedEvent, AgentAbortedEvent stream events.
    • Lazy OTel hitl.ask / hitl.confirm spans with outcome attributes.
    • Pending-request persistence on Memory, SQLite, Postgres, and MySQL checkpointers (schema v2 — additive migration, backwards compatible).
  • MySQLCheckpointer — full-featured MySQL/MariaDB checkpointer with Alembic schema management, matching the Postgres implementation.
  • Stream recording + trace convertrecord_stream() captures a raw provider MessageStream to JSONL; cubepi trace convert replays the recording as a structured trace. Useful for offline debugging and testing without live API calls.
  • Trace CLI — run-metadata filtering (--meta / --show-meta): filter and display tracing_context key/value tags attached to a run.
  • Trace CLI — span IDs in trace view node labels for easier cross-referencing with external OTLP backends.
  • Agent steering by IDagent.steer(...) returns a steer_id; pass it to agent.cancel_steer(steer_id) to cancel a not-yet-drained steering message.

Fixed

  • OpenAI provider: deduplicate finish_reason processing that caused spurious extra events on streamed responses.
  • Agent: ToolExecutionStartEvent is now deferred until the tool coroutine is actually scheduled, preventing early events for tools that are never run.
  • Tracing: clear stream_tool_accumulated at turn start to avoid stale tool-call data leaking across turns.

0.5.0 - 2026-05-25

Added

  • Image generation subsystem (cubepi.providers.images): a pluggable, per-vendor model interface for image generation, with a class factory so new backends slot in without touching call sites.
  • CapabilityDescriptor: a declarative, per-model description of what a model supports — temperature mode (free / fixed / ignored), reasoning-level mapping (int budget / effort / enum), and the max_tokens field name. It now drives the OpenAI, OpenAI Responses, Anthropic, and DeepSeek providers, and is exported from the top-level package.
  • cubepi trace CLI (install with the trace-cli extra): discover, list, view, follow, and aggregate stats over local agent-run traces, with rich rendering and run-id prefix matching.
  • Tracing: an OTLP exporter and a best-effort trace() scope helper. The tracing package now imports cleanly without opentelemetry installed.
  • Self-describing provider errors that carry provider / model / cause context for easier debugging.

Changed

  • Provider reasoning/thinking and temperature handling is now driven by CapabilityDescriptor instead of per-provider ad-hoc payload quirks, giving consistent behavior across OpenAI, Anthropic, and DeepSeek.

Fixed

  • Anthropic: merge parallel tool results into a single user message; carry parsed tool arguments through the streaming toolcall_end event; compute max_tokens from the actual capability budget; honor per-request thinking_budgets overrides.
  • Agent loop / steering: drain steering at the turn boundary; after_model_response now injects after tool results; backfill tool results for tool calls orphaned by a cancel.
  • DeepSeek: correct reasoning-effort path and temperature range handling.

Earlier releases