Changelog
All notable changes to CubeLoop are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.15.2 - 2026-10-21
Added
- Messages carry a stable
id.UserMessage,AssistantMessage, andToolResultMessagehaveid: str, empty until the agent appends the message. A host may set it first; that value is kept. The id is written onto both the object the loop keeps and the object stored in the checkpointer. Rows saved before this field existed load as""and compaction keeps identifying them by content hash. New messages useid:, so a later rewrite ofrun_id,timestamp, ormetadatadoes not change which transcript entry a summary covers.
Fixed
- Compaction no longer goes blind when its persisted state is dropped. Three
defects compounded into
ContextLengthExceededon long conversations (#233):message_refhashed the whole message dump, including bookkeeping the framework and the host rewrite after the fact —run_id(re-stamped on messages the framework re-touches, e.g. the injectedtodo_stale_remindernudges) andtimestamp/metadata(host-owned; CubePlex writes a per-turnmemory_snapshot.captured_atinto it). On a 901-message conversation 38 of 877 refs drifted between compaction runs, so_state_matches_historyreturned False and the state was discarded. A message with anidis referenced by that id. A message without one is referenced by role, content, and tool linkage, ignoringrun_id,timestamp, andmetadata.- The trigger sized a raw-history send with
real_context_estimate, which is anchored to the most recent usage-bearing assistant and adds only what follows it. Every message before the anchor is assumed to be covered by that turn'sinput_tokens— true only while a summary covers the prefix. With the state dropped it reported 57,278 tokens for a 901-message history whose actual send was >= 256,001. When no summary is in effect the trigger now also consults the char estimate, which does see the whole list. - Dropping the state is logged at WARNING. It means the next send is the raw history; previously it was silent, so the failure presented as "the model got slow" until the provider returned 400.
0.15.1 - 2026-09-24
Added
ToolResultLimitMiddlewarecaps tool-result text inafter_tool_call. Oversized text is truncated tomax_chars(default 20,000) with a notice telling the model to narrow the call. The rewrite happens beforeToolExecutionEndEvent, so the model, checkpointer, and host consumers all see the truncated result. Image blocks are kept; tools inexclude_tool_namespass through (#229).TodoListMiddlewarecan wait on host-validated background tasks.write_todosaccepts an optionalwait_for_taskslist. A nonempty list requiresvalidate_task_wait; without that callback it is rejected. A successful declaration is checkpointed with the Todo snapshot, run ID, and input boundary. Before the unfinished-Todo guard forces another model call, CubeLoop asks the host again.valid, orcancelledafter the host proves the cancellation, lets the run finish with unfinished items still open. New input invalidates the declaration, including a new run's first message, a HITL answer, and human approval, denial, or editing of a tool call. Automatic policy approval does not. CubeLoop does not start, poll, cancel, or deliver the tasks (#231).
Changed
- Checkpoint
extrais saved at more boundaries.save_extraruns after a turn that produced tool results, when a run suspends, and atagent_end. A failed write is recorded as a checkpoint failure. The suspension or completion event is not published, and the run is not marked complete (#231). - Human approval records
hitl_trace["decision"]="human_approve". The tool still runs with its original arguments. Policy auto-approval stays a silent passthrough (#231).
Fixed
- HITL answers restored through the resume slot use the same typed
normalization as checkpoint-loaded answers, so a restored JSON answer
invalidates a task-wait declaration the same way a live answer does
(
#231).
0.15.0 - 2026-09-16
Added
- Public execution sessions for service hosts. Every
Agentexposesagent.sessionwith typed prompt, continue, and HITL response requests. Each attempt carries a run ID and attempt ID and returns a settledExecutionResultthat distinguishes completion, suspension, cancellation, failure, and incomplete history. ExistingAgent.prompt(),resume(), andrespond()use the same sequential admission gate. - Identified live input and session events. Hosts can submit steering or
follow-up messages with an input ID, observe when an input is committed to
the checkpoint, and subscribe to bounded, attempt-scoped event delivery.
TurnExecutionContextcaptures the effective model, prompt, and tool bindings for each model request.
Fixed
- Execution finalization and recovery are more consistent. Failed or cancelled attempts settle queued input and tool calls, preserve suspension state across durable retries, and report checkpoint and event-delivery failures without exposing a false successful outcome. Pending HITL cleanup on Postgres and MySQL is fenced by run ID to avoid clearing another run's request.
0.14.1 - 2026-09-10
Fixed
- The CubeLoop rename no longer renames the persistent Postgres/MySQL
schema. Physical tables, partitions, indexes, and the version marker remain
cubepi_*at schema version 5, so an existing 0.13.6 database opens without DDL and historical host Alembic revisions remain reproducible. - The
cubepipackage is now a fail-fast tombstone. It has no dependencies, exports, import aliases, or CLI proxy. Imports and commands fail with explicit instructions to install and migrate tocubeloop.
0.13.6 - 2026-09-04
Fixed
- Compaction no longer lets trailing synthetic user controls hide
current-turn evidence. Transform middleware can append a synthetic
UserMessageafter this turn's tool results. Compaction previously treated that control as the protected tail and a legal boundary, so the results immediately before it became eligible for size-only pruning ([tool] N chars) even though they were still in durable history. Trailing synthetic user messages are now split off before tail, boundary, prune, and summarize decisions; they still count toward the send-size threshold and are reattached unchanged (#217). - GitHub Actions workflows now declare least-privilege permissions. CI and publish workflows explicitly request read-only repository contents access.
0.13.5 - 2026-08-16
Changed
FallbackBoundModelretries the active model before hopping, then sticks to the first successful leg for the rest of the agent run (#211). 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.
0.13.4 - 2026-08-10
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 in the activation. A tracer-private uuid is minted only as a fallback whenactive_run_idis unset (e.g. oneshot still mints its own session id). Hosts can filter traces with the same id they use for SSE, messages, and billing. OTeltrace_id/span_idremain the tree identity; JSONL still shards bytrace_id.
Fixed
- Durable HITL pauses now finalize traces as suspended rather than aborted.
Agent.detach()commits the pending request first, then the owning activation emits a terminal suspension event after runtime-state cleanup. Open agent, turn, chat, and tool spans end withcubepi.run.outcome="suspended"; observer failures cannot hide the terminal event, cancellation still propagates, andrespond()opens a distinct correlated activation trace. - Raw tracing diagnostics now follow the content-recording opt-in. With
record_content=False, provider, turn, one-shot, and MCP spans retain typed error classification and ERROR status but use bounded generic descriptions and omit exception messages and stack traces. Stream logs retain structural timing/size fields while omitting tool-argument previews and raw error text.record_content=Truepreserves the prior diagnostic detail for failures. - One-shot cancellation now matches Agent and MCP cancellation semantics. It
records
cubepi.aborted=truewith status UNSET and does not emit an exception event, because cancellation is a control signal rather than a failure.
0.13.3 - 2026-08-02
Fixed
- OpenAI
chat_completions/responsescapability profiles no longer emitreasoning_effort: "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 theminimal→ wire-value mapping in_OPENAI_EFFORT_VALUESboth now emit"none". - Live
ToolResultMessages now carry the owning turn'srun_id. On a run-aware Agent, tool results emitted during the live turn (events, hooks, live context, and the next provider call) previously hadrun_id=Nonewhile 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. Directexecute_tool_callswith an unstamped assistant still producesrun_id=None.
0.13.2 - 2026-07-23
Fixed
gen_ai.output.messagesnow recorded on thechatspan. Per the tracing design spec (§10.3/§10.5), the provider-levelchatspan'sgen_ai.output.messagesshould reflect what the provider actually returned - independent of the turn/agent-level rollup. A code comment in_on_provider_responsealready claimed it would record "the normalized output messages where derivable", but onlycubepi.llm.raw_responsewas ever set on this span;gen_ai.output.messageswas written only oninvoke_agentandcubeloop.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, OpenAIchat.completion-shaped, OpenAI Responses-shaped). Unrecognized shapes or empty content set nothing, identical to prior behavior (no regression).
0.13.1 - 2026-07-15
Fixed
- Non-blocking tracing flush.
Tracer.force_flushandMeter.force_flushnow run the synchronous provider flush in a worker thread viaasyncio.to_thread, so awaiting either method never stalls the event loop on a slow or backlogged OTLP collector. - Background flush mode for
trace(). Thetrace()context manager accepts a newflushparameter ("await"|"background", default"await"). Withflush="background"the block exits immediately after detaching listeners while span export continues as a supervised background task — useful on request-serving paths where a caller is waiting on the block's completion. The tracer keeps a strong reference to in-flight flush tasks (_pending_flushes) so they can't be garbage-collected mid-export;await tracer.shutdown()settles any still-pending flushes before closing exporters, so no spans are lost on clean shutdown.
0.13.0 - 2026-07-05
Added
- Reasoning capability primitives —
ReasoningProfile,ThinkingControl, andAppliedReasoningControltypes 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 providers —
BoundModelstreams now carry reasoning state per-turn. Rendered thinking blocks are available in message content for inspection and tracing. - Run-scoped compaction and real-token triggering —
CompactionMiddlewarecan 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_parallelsettles 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.reasoningis 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=...)— aCallable[[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 viaCompactionStatepersistence.- Sender attribution at the provider boundary.
UserMessage.metadatacan 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 emit
tool_namebefore 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
dispatchstrategy. Tool schemas are delivered throughload_toolsresults and invoked via thedeferred_tool_calldispatcher; the tools array and system prompt stay byte-stable, so expansions no longer invalidate the prompt cache. 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 (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_callmiddleware 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— whenFalse, 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=...)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, 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_callcomposes with middleware resolvers instead of replacing the chain. An explicit resolver passed toAgent(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 fromThinkingLevel.ThinkingLevelnow readsLiteral["off", "low", "medium", "high", "xhigh"]; the.minimalfield is gone fromThinkingBudgets;THINKING_LEVELSno longer contains it; Anthropic's defaultlevel_budgetsand OpenAI Responses'_THINKING_TO_EFFORTno 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) -> UserMessageandis_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 stampmetadata["synthetic"] = Trueso downstream UIs can tell internal scaffolding apart from real human input. RealAgent.prompt()/Agent.steer()messages remain unmarked. Closes #171. Exported fromcubepiandcubeloop.providers. Use this factory (not bareUserMessage) when returning messages fromTurnAction.inject_messagesoron_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 | Noneparameter onBoundModel.stream(),BoundModel.generate(), and theProviderprotocol. 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 BaseModelclass Sentiment(BaseModel):label: strconfidence: floatresult = 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 withoutput_type.model_validate(). Retries on validation failure (configurablemax_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 ormax_evaluationsis hit. Outcome inagent.state.extra["goal"].from cubepi.middleware.goal import GoalMiddlewaregoal = 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 perprompt()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. PreviouslyFallbackBoundModeland 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 asfailed=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.
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.extradict (same object, not a copy) so the tool executor can write into it. - Exported from
cubepi.middlewareasTodoListMiddleware,Todo,WriteTodosInput, andTodoGuardBlocked.
-
FallbackBoundModel— built-in failover chain at theBoundModellevel. Wrap an orderedchainofBoundModelinstances; onRateLimited,ProviderUnavailable, orContextLengthExceeded(configurable viatrigger_errors), or on a first-event stream error, the next model in the chain is tried transparently. Optionalon_failovercallback for billing/metrics hooks. Exported fromcubepiandcubeloop.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 byprovider.model(...)now drives a provider call directly. Useful for utilities (summarizers, classifiers) where you already hold aBoundModeland 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 incubeloop.tracingwas adapted in lock-step; built-inCompactionMiddlewarealready updated.cubepi.middleware.compaction.summarizer.summarize()takesmodel: BoundModelinstead of separateprovider: Provider, model: Modelkwargs. 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_continuetakemodel: BoundModelinstead of separateprovider: Provider, model: Modelkwargs. 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# Beforedef 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):# Beforeawait summarize(provider=provider, model=model_spec, ...)# Afterawait summarize(model=BoundModel(provider=provider, spec=model_spec), ...) -
Stateless-loop callers (uncommon — most users build an
Agent):# Beforeawait run_agent_loop(prompts=[...],context=ctx,provider=provider,model=model_spec,convert_to_llm=...,emit=...,)# Afterawait run_agent_loop(prompts=[...],context=ctx,model=provider.model("id", ...),convert_to_llm=...,emit=...,)
Fixed
-
StructuredValuefields now preserveBaseModelpayloads on serialization. Fields typedStructuredValue(theJsonPrimitive | BaseModel | list | dictunion used by tool-resultdetails,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 theBaseModelbranch 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_iddidn'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.
0.8.0 - 2026-06-06
Added
@tooldecorator —cubepi.toolbuilds anAgentToolfrom a plain async function: the input schema is generated from the typed parameters (honouringField(...)metadata), the docstring becomes the description, and the loop-suppliedtool_call_id/signal/on_updateare injected only when declared. Tools may return astr, aContent, alistof content, or a fullAgentToolResult. The longhandAgentTool(...)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) -> strnow accept-or-generates therun_idand returns it.Agent.state.active_run_idexposes the in-flightrun_id.Agent(messages=...)constructor arg for ephemeral pre-seeded history.Checkpointer.snapshot,fork,claim_run,mark_run_complete,load_pendingProtocol methods.cubepi_runstable per backend (Postgres / MySQL schema v3 → v4).Message.run_id: str | Nonefield on all three Message variants.HitlBindingattribute onAgentTool/Middleware;ask_user_toolandApprovalPolicyMiddlewarepopulate it.
Fixed
- Tool results that set
is_error=Trueare now reported as errors. A tool body that returnedAgentToolResult(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@tooland longhandAgentTooltools).
Breaking
Agent.prompt()return type changed fromNonetostr. Callers ignoring the return value keep working.CheckpointerProtocol gained 5 new methods. Third-party v3-only checkpointers continue to work for vanillaprompt()via degraded mode; fork APIs raiseCheckpointerErroron such backends.
Migration
- Postgres / MySQL: run the new alembic helper (see backend guides).
- SQLite: auto-migration at connect time.
- Legacy
run_id=NULLmessages 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 consumesstream()and returns the finalAssistantMessage. It supports per-call overrides formax_output_tokens,temperature,thinking, andthinking_budgets.- Compaction middleware —
cubepi.middleware.CompactionMiddlewaresummarizes older conversation turns into JSON-safeAgentContext.extrastate and sends the model a compressed view while preserving full agent history. - Subagent middleware —
cubepi.middleware.SubagentMiddlewareadds asubagenttool that runs an ephemeral childAgent, 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
ProviderErrorsubclasses such asContextLengthExceeded,RateLimited,ProviderAuthFailed,ProviderUnavailable, andProviderBadRequest. Tracer.oneshot(...)— trace a single background LLM call without a fullAgentloop; the trace CLI can filter these runs via--meta oneshot_operation=....- Checkpointed HITL run IDs — checkpointers now persist the owning
run_idatomically with pending HITL requests so hosts can resume the correct detached run after a restart. on_run_endmiddleware hook — fires exactly once after all turns and tool calls complete, beforeAgentEndEvent. Return alist[Message]to inject additional messages and run one extra model turn (e.g. a memory-reflection pass); returnNoneto do nothing._reflection_firedguard in the loop prevents the injected turn from re-triggeringon_run_end.should_stop_after_turnandturn_action.decision == "stop"paths now route throughon_run_endbefore emittingAgentEndEvent. 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 tracing —
CompactionMiddleware's summary call is now first-class in the trace tree. Acubepi.compaction.summarizeparent span (carryingcubepi.compaction.message_count) wraps the summarizer LLM call, and the recorder auto-subscribes the middleware'ssummary_providerso itschatspan lands as a child:Previously summarizer calls bypassed the trace entirely.invoke_agent└── cubeloop.turn├── cubepi.compaction.summarize│ └── chat <summary-model>└── chat <main-model> Middleware.providers()protocol — middleware authors can override this method to expose any extraBaseProviderinstances the middleware owns;Recorder.attach()walksagent._middlewareand wires its listener registry on each one (id-deduped againstagent._provider). Default is empty, so existing custom middleware needs no change.
Changed
-
Breaking: agent construction now takes a bound model from
provider.model(...). ReplaceAgent(provider=provider, model=Model(...))withAgent(model=provider.model("model-id", ...)).provider_idnow lives on provider constructors and is copied into model metadata used for tracing, response metadata, and error messages.Tracer.oneshot(...),CompactionMiddleware, andSubagentMiddlewareuse the same bound-model shape. -
Breaking: pre-model middleware hooks now receive
AgentContextdirectly. 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.extrafor middleware state that should survive checkpointing. This release does not include old-signature compatibility shims. -
Breaking: custom providers must implement
Provider.generate(...)or inherit fromBaseProvider, which suppliesgenerate()by consumingstream(). -
Breaking: Postgres and MySQL checkpointer schemas are now version 3. Host applications must add the nullable
run_idcolumn tocubepi_threadsand callwrite_schema_version_op()in their Alembic migration before using this release. -
Breaking: the
cubepi.providers.imagessurface has been redesigned to align with the chat-provider 0.7 conventions: providers now takeprovider_idand an optionalImagesCapabilityDescriptor; models are built viaprovider.model("id", ...)(renamedproviderfield toprovider_id, addeddefault_size/n/quality/output_formatandcostmetadata);ImagesContextis typed (size/n/quality/output_format/seed/ negative_prompt/steps/guidance/extra); per-call options live on a newImagesOptionsbag (signal,on_payload,on_response); failures raisecubepi.errors.ProviderErrorsubclasses instead of in-bandAssistantImages.error_message; thecreate_images_provider/register_images_provider_classregistry is removed. The new shape reaches OpenAI, Doubao Seedream, SiliconFlow, and Together AI through a singleOpenAIImagesProviderconfigured with the rightImagesCapabilityDescriptor. -
CI now runs
mypy cubepiin 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'smessages.N: all messages must have non-empty contentrule. - The trim happens in
stream()before the cache policy resolves breakpoints, so the user's last-messagecache_controlmarker 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 rootinvoke_agentspan'sgen_ai.provider.name/cubepi.agent.system_prompt_sha256/cubepi.agent.toolsare 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_spanas the OTel current span, socubepi.compaction.summarizelands as a child ofcubeloop.turninstead of becoming an orphan root.
Removed
- Removed unused
PyYAMLfrom the default dependency set; the core dependency set is back toanthropic,openai, andpydantic.
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:
HitlChannelprotocol withInMemoryChannelandCheckpointedChannelimplementations.CheckpointedChannelpersists pending requests so an agent can be resumed after a process restart.Agent(channel=...)wiring;agent.respond(),agent.detach(), andagent.abort_pending()for external controllers.ApprovalPolicyMiddlewareandConfirmToolCallMiddlewarefor declarative tool-call gating;ask_userbuilt-in tool for open-ended prompts.ScriptedChannelandNoopChannelfor deterministic testing.HitlRequestEvent,HitlAnswerEvent,AgentSuspendedEvent,AgentAbortedEventstream events.- Lazy OTel
hitl.ask/hitl.confirmspans 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 convert—record_stream()captures a raw providerMessageStreamto JSONL;cubeloop trace convertreplays 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 displaytracing_contextkey/value tags attached to a run. - Trace CLI — span IDs in
trace viewnode labels for easier cross-referencing with external OTLP backends. - Agent steering by ID —
agent.steer(...)returns asteer_id; pass it toagent.cancel_steer(steer_id)to cancel a not-yet-drained steering message.
Fixed
- OpenAI provider: deduplicate
finish_reasonprocessing that caused spurious extra events on streamed responses. - Agent:
ToolExecutionStartEventis now deferred until the tool coroutine is actually scheduled, preventing early events for tools that are never run. - Tracing: clear
stream_tool_accumulatedat 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 themax_tokensfield name. It now drives the OpenAI, OpenAI Responses, Anthropic, and DeepSeek providers, and is exported from the top-level package.cubeloop traceCLI (install with thetrace-cliextra): 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 withoutopentelemetryinstalled. - Self-describing provider errors that carry provider / model / cause context for easier debugging.
Changed
- Provider reasoning/thinking and temperature handling is now driven by
CapabilityDescriptorinstead 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_endevent; computemax_tokensfrom the actual capability budget; honor per-requestthinking_budgetsoverrides. - Agent loop / steering: drain steering at the turn boundary;
after_model_responsenow 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
- 0.4.0 - 2026-05-19 — see the release notes.
- 0.3.0 - 2026-05-14 — see the release notes.
- 0.2.0 - 2026-05-10 — see the release notes.
- 0.1.0 - 2026-05-09 — initial release. See the release notes.