Releases: cubeplexai/cubepi
Release list
CubePi v0.13.5
What's Changed
Changed
FallbackBoundModelretries the active model before hopping, then sticks to the first successful leg for the rest of the agent run. TransientRateLimited/ProviderUnavailableretry up tomax_retries_per_model=3(4 attempts) and honour a cappedretry_after. ResidualProviderBadRequestandModelNotFoundhop without same-model retry;ProviderAuthFailedandContentFilteredstay fail-closed. Stream first-event errors now carry typed fields so the same predicate applies to exceptions and error events. Exhaustion raisesProviderUnavailablewith.errorslisting every leg. New subclassesModelNotFoundandContentFilteredinheritProviderBadRequest.ProviderError.error_codeis extracted from vendor bodies when present.
Full Changelog: v0.13.4...v0.13.5
CubePi v0.13.4
What's Changed
Changed
cubepi.run_idnow follows the agent business run id. OnAgentStart, the recorder stampsagent.state.active_run_id(the same string asprompt(run_id=…)/Message.run_id) onto every span. Hosts can filter traces with the same id used for SSE, messages, and billing.
Fixed
- Durable HITL pauses finalize traces as suspended rather than aborted.
- Raw tracing diagnostics follow the content-recording opt-in.
- One-shot cancellation matches Agent/MCP abort semantics.
Full Changelog: v0.13.3...v0.13.4
CubePi 0.13.3
What's fixed
OpenAI reasoning_effort no longer emits stale "minimal"
OpenAI renamed the lowest reasoning-effort tier to "none" (gpt-5.5+), and some OpenAI-compatible proxies reject the now-stale "minimal" value outright. The built-in "off" mode payload and the minimal → wire-value mapping in _OPENAI_EFFORT_VALUES both now emit "none".
Live ToolResultMessages carry the owning turn's run_id
On a run-aware Agent, tool results emitted during the live turn (events, hooks, live context, and the next provider call) previously had run_id=None while the checkpointer copy was stamped correctly. The assistant message is now stamped in place at the message-end seam so tool-result construction can inherit the active run ID for every sequential, parallel, salvage, and HITL-sibling outcome. Direct execute_tool_calls with an unstamped assistant still produces run_id=None.
Full changelog: https://github.com/cubeplexai/cubepi/blob/main/CHANGELOG.md#0133--2026-08-02
CubePi 0.13.2
What's fixed
gen_ai.output.messages on the chat span
Per the tracing design spec (§10.3/§10.5), the provider-level chat span now records gen_ai.output.messages reflecting what the provider actually returned - independent of the turn/agent-level rollup.
A code comment in _on_provider_response already claimed it would record "the normalized output messages where derivable", but only cubepi.llm.raw_response was ever set on this span; gen_ai.output.messages was written only on invoke_agent and cubepi.turn.
The recorder now reconstructs the semconv output-message parts (text / reasoning / tool_call) directly from the assembled response body via a new _derive_output_message_from_body() helper, mirroring the existing three-way provider-shape dispatch (Anthropic-shaped, OpenAI chat.completion-shaped, OpenAI Responses-shaped). Unrecognized shapes or empty content set nothing - identical to prior behavior, no regression.
Full changelog: https://github.com/cubeplexai/cubepi/blob/main/CHANGELOG.md#0132--2026-07-23
CubePi 0.13.1
What's fixed
Non-blocking force_flush
Tracer.force_flush and Meter.force_flush now run the synchronous OTel provider flush in a worker thread via asyncio.to_thread. A slow or backlogged OTLP collector can no longer stall the event loop while awaiting either method.
Background flush mode for trace()
The trace() context manager accepts a new flush parameter:
from cubepi.tracing import trace
# Default — block until spans are exported (unchanged behaviour).
async with trace(tracer, agent):
await agent.prompt("…")
# New — exit immediately, export as a supervised background task.
async with trace(tracer, agent, flush="background"):
await agent.prompt("…")Use flush="background" on request-serving paths (FastAPI endpoints, gRPC handlers, …) where span export to a remote collector must not gate a user-visible response. The tracer holds strong references to in-flight flush tasks so they survive GC; await tracer.shutdown() settles any pending flushes before closing exporters.
Full changelog
See CHANGELOG.md for the complete entry.
CubePi 0.13.0
What's New
Reasoning Capabilities
- Reasoning capability primitives with
ReasoningProfile,ThinkingControl, andAppliedReasoningControltypes - Extended thinking support across all providers (Anthropic, OpenAI)
- Reasoning state per-turn with thinking block inspection
Compaction & Performance
- Run-scoped compaction enables compression within a single long agentic run
- Real-token triggering based on actual usage (input + cache_read + cache_write)
- Accurate context fill detection without prompt caching blind spots
Reliability & Durability
- Tool-batch fault isolation — parallel tool exceptions now preserve all checkpoint state
- Parallel HITL approval replay — durable ledger for mixed approved/pending tool batches
- Checkpointer corruption detection — wrap deserialization errors with context
See CHANGELOG for full details.
CubePi 0.12.0
Added
CompactionMiddleware(tool_result_compressor=...)— a
Callable[[ToolResultMessage], str | None]callback for selective tool
result preservation during compaction. Return astrto preserve that
text verbatim in the summary (for grounding/citation); returnNoneto
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
CompactionStatepersistence.- Sender attribution at the provider boundary.
UserMessage.metadata
can now carrysender_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 emittool_namebefore arguments, smoothing streaming UX for
dispatch-mode deferred tools.
Fixed
- Removed stale
(latest)labels from Chinese 0.7 and 0.8 version docs.
Full Changelog: v0.11.0...v0.12.0
CubePi v0.11.0
Highlights
CubePi 0.11 ships the dispatch strategy for deferred tool groups — prompt-cache-stable MCP tool expansion that keeps the tools array and system prompt byte-stable across turns.
Breaking Changes
- Deferred tool groups default to
dispatchstrategy. Tool schemas are delivered throughload_toolsresults and invoked via adeferred_tool_calldispatcher. Restore the v0.10 behavior withAgent(deferred_tool_strategy="inject")/DeferredToolsMiddleware(strategy="inject"). DeferredToolsMiddleware(resumed_schemas=...)andResumedState.expanded_schemasare removed;prepare_resumed_statetakes a requiredstrategykeyword.- Inject mode no longer renders expanded schemas into the system prompt — the duplicate rendering (double token billing per turn) is gone.
Added
resolve_tool_callmiddleware hook — rewrite a tool call before validation,before_tool_call, execution, events, and tracing. Composition is first-non-None-wins.AgentTool.expose_to_model— whenFalse, the tool is resolvable and executable but its definition is never sent to the provider.Agent(deferred_tool_strategy=...)andDeferredToolsMiddleware(strategy=...)— choose"dispatch"(default) or"inject".- Resolved dispatcher calls that fail argument validation get the tool's full schema appended to the error result for self-correction.
Fixed
- HITL resume short-circuit now emits
HitlAnswerEvent— previously the event was skipped on the resume path. - Explicit
resolve_tool_callcomposes with middleware resolvers instead of replacing the chain.
Full changelog: v0.10.0...v0.11.0
Docs: https://cubepi.ai/docs/
v0.10.0
CubePi 0.10.0
Full changelog: see CHANGELOG.md
Removed (BREAKING)
"minimal"removed fromThinkingLevel.ThinkingLevelnow reads
Literal["off", "low", "medium", "high", "xhigh"]; the.minimalfield
is gone fromThinkingBudgets;THINKING_LEVELSno longer contains it;
Anthropic's defaultlevel_budgetsand OpenAI Responses'_THINKING_TO_EFFORT
no longer map it. Callers that previously passedthinking="minimal"
must switch tothinking="low"(or"off"). Rationale: DeepSeek's
Anthropic-shape endpoint rejectseffort=minimalonoutput_config,
and OpenAI'sreasoning.effortpath rewrote it to"low"downstream
anyway — keeping it was a footgun that surfaced as a 400 + fallback.
Added
-
synthetic_user_message(text, *, source) -> UserMessageand
is_synthetic_message(message) -> bool— public marker for
framework-injected user-role messages. Middleware-injected nudges
(todo guard errors, goal continuations, compaction summaries,
generate_structuredretry feedback) now stamp
metadata["synthetic"] = Trueso downstream UIs can tell internal
scaffolding apart from real human input. RealAgent.prompt()/
Agent.steer()messages remain unmarked. Closes #171. Exported from
cubepiandcubepi.providers. Use this factory (not bare
UserMessage) when returning messages fromTurnAction.inject_messages
oron_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-inload_toolstool (full or
selective). Key properties:- Catalog sorted by
group_idfor 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 withextra_refbound toself._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.deferredasDeferredToolGroup,
DeferredToolsMiddleware, andResumedState.
- Catalog sorted by
-
tool_choiceon Provider — newtool_choice: ToolChoice | None
parameter onBoundModel.stream(),BoundModel.generate(), and the
Providerprotocol. 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.).FauxProvideraccepts and ignores the
parameter. Type alias:ToolChoice = Literal["auto", "required", "none"] | str, exported fromcubepi.providers.base. -
BoundModel.generate_structured()— tool-based structured output.
Pass a PydanticBaseModelsubclass 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
viatool_choice, and validates the response with
output_type.model_validate(). Retries on validation failure (configurable
max_retries, default 1). RaisesStructuredOutputErroron no tool call
or validation exhaustion. -
GoalMiddleware— autonomous goal-driven agent runs. A separate
evaluator model judges whether a/goalcondition 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_evaluationsis hit. Outcome inagent.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.middlewareasGoalMiddleware.
Changed
on_run_endfires on every outer-loop iteration instead of once per
prompt()call. The_reflection_firedsingle-fire guard has been
removed. Existing middlewares that returnNoneafter one injection are
unaffected. This enables evaluation loops likeGoalMiddleware.
Changed
- Internal logging now uses stdlib
loggingexclusively. Previously
FallbackBoundModeland the provider listener-exception path tried to
importlogurufirst and fell back to stdlib. The loguru path was
silently incorrect — loguru does not perform%sargument substitution,
so failover warnings rendered literal%splaceholders 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
-
FallbackBoundModelfailover log line now substitutes its placeholders.
Before the loguru removal above, the WARNING emitted on every failover
readfailed=%s → next=%s reason=%s attempt=%s/%sliterally 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()andMeter.attach()now subscribe to every provider in
aFallbackBoundModelchain (closes #167). Previously they only listened
tochain[0].provider, so post-failover calls executed againstchain[1..]
were invisible to provider-level observability — chat spans, token usage,
cache metrics, and cost telemetry were missing for fallback legs. Adds a
newcubepi.providers.fallback.chain_providers()helper used by both
attach paths to walk and dedupe the chain. Agent-event-driven observability
(e.g. cost middleware readingMessageEvent) was already correct and is
unchanged.
v0.9.0
[0.9.0] - 2026-06-08
Added
-
TodoListMiddleware— built-in task-tracking middleware for multi-step
agents. Adds awrite_todostool 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
UserMessageis injected after several
turns without awrite_todoscall, prompting the model to keep the list
in sync without blocking. - Parallel-call guard — if the model calls
write_todosmore 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 inAgentContext.extraand survives
checkpointing. - Constructor:
TodoListMiddleware(extra_ref=..., tool_description=..., system_prompt=...).extra_refmust return the liveAgentContext.extra
dict (same object, not a copy) so the tool executor can write into it. - Exported from
cubepi.middlewareasTodoListMiddleware,Todo,
WriteTodosInput, andTodoGuardBlocked.
- Finalization guard — if the model delivers a plain-text final response
-
FallbackBoundModel— built-in failover chain at theBoundModellevel.
Wrap an orderedchainofBoundModelinstances; onRateLimited,
ProviderUnavailable, orContextLengthExceeded(configurable via
trigger_errors), or on a first-event stream error, the next model in the
chain is tried transparently. Optionalon_failovercallback for
billing/metrics hooks. Exported fromcubepiandcubepi.providers. -
DEFAULT_TRIGGER_ERRORS—frozenset({RateLimited, ProviderUnavailable, ContextLengthExceeded}). The default set of error types that trigger failover
inFallbackBoundModel. -
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 aBoundModel
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.specand
mirror theProvider.generate/Provider.streamsignatures exactly.
Breaking
Middleware.extra_llm_calls()returnsIterable[BoundModel]instead
ofIterable[tuple[Provider, Model]]. Third-party middleware overriding
this hook must update the return shape (see Migration). The recorder
consumer incubepi.tracingwas adapted in lock-step; built-in
CompactionMiddlewarealready updated.cubepi.middleware.compaction.summarizer.summarize()takes
model: BoundModelinstead of separateprovider: Provider, model: Model
kwargs. Direct callers (rare — this is internal toCompactionMiddleware)
must wrap the pair. The publicCompactionMiddleware(summary_model=...)
API is unchanged.cubepi.run_agent_loopandcubepi.run_agent_loop_continuetake
model: BoundModelinstead of separateprovider: Provider, model: Model
kwargs. Stateless-loop callers driving the loop outside ofAgentmust
update. TheAgentAPI is unchanged — it already tookmodel: 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
-
StructuredValuefields now preserveBaseModelpayloads on
serialization. Fields typedStructuredValue(the
JsonPrimitive | BaseModel | list | dictunion used by tool-result
details,AgentToolResult,HitlAnswerEvent.answer, and compaction
message-ref hashing) silently serializedBaseModelinstances to{}
onmodel_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
BaseModelbranch withSerializeAsAny[BaseModel]fixes the silent loss
across all five affected sites: checkpointer save, compaction state,
ToolExecutionEndEvent,ToolExecutionUpdateEvent, andHitlAnswerEvent. -
SubagentMiddlewarenow strips checkpointed-HITL elements from a child
agent's inherited tools / middleware. Previously, passing the parent
agent'sask_user_tool(channel)inshared_tools(the common pattern
when the host wants tools shared between parent and children) caused
the child's firstprompt()to raiseAgent has checkpointed HITL elements bound to run_ids ...because the binding's parentrun_id
didn't match the child's freshrun_id. The middleware now drops any
element whose.hitlis a checkpointedHitlBindingbefore
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.