Skip to content

Releases: cubeplexai/cubepi

CubePi v0.13.5

Choose a tag to compare

@xfgong xfgong released this 16 Aug 08:58

What's Changed

Changed

  • FallbackBoundModel retries the active model before hopping, then sticks to the first successful leg for the rest of the agent run. Transient RateLimited / ProviderUnavailable retry up to max_retries_per_model=3 (4 attempts) and honour a capped retry_after. Residual ProviderBadRequest and ModelNotFound hop without same-model retry; ProviderAuthFailed and ContentFiltered stay fail-closed. Stream first-event errors now carry typed fields so the same predicate applies to exceptions and error events. Exhaustion raises ProviderUnavailable with .errors listing every leg. New subclasses ModelNotFound and ContentFiltered inherit ProviderBadRequest. ProviderError.error_code is extracted from vendor bodies when present.

Full Changelog: v0.13.4...v0.13.5

CubePi v0.13.4

Choose a tag to compare

@xfgong xfgong released this 10 Aug 13:05

What's Changed

Changed

  • cubepi.run_id now follows the agent business run id. On AgentStart, the recorder stamps agent.state.active_run_id (the same string as prompt(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

Choose a tag to compare

@xfgong xfgong released this 02 Aug 15:21

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

Choose a tag to compare

@xfgong xfgong released this 23 Jul 14:47

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

Choose a tag to compare

@xfgong xfgong released this 16 Jul 03:17

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

Choose a tag to compare

@xfgong xfgong released this 06 Jul 03:13

What's New

Reasoning Capabilities

  • Reasoning capability primitives with ReasoningProfile, ThinkingControl, and AppliedReasoningControl types
  • 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

Choose a tag to compare

@xfgong xfgong released this 24 Jun 05:15

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.

Full Changelog: v0.11.0...v0.12.0

CubePi v0.11.0

Choose a tag to compare

@xfgong xfgong released this 17 Jun 09:26

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 dispatch strategy. Tool schemas are delivered through load_tools results and invoked via a deferred_tool_call dispatcher. 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.
  • Inject mode no longer renders expanded schemas into the system prompt — 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. Composition is first-non-None-wins.
  • AgentTool.expose_to_model — when False, the tool is resolvable and executable but its definition is never sent to the provider.
  • 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 for self-correction.

Fixed

  • HITL resume short-circuit now emits HitlAnswerEvent — previously the event was skipped on the resume path.
  • Explicit resolve_tool_call composes with middleware resolvers instead of replacing the chain.

Full changelog: v0.10.0...v0.11.0
Docs: https://cubepi.ai/docs/

v0.10.0

Choose a tag to compare

@xfgong xfgong released this 10 Jun 09:47

CubePi 0.10.0

Full changelog: see CHANGELOG.md

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.

v0.9.0

Choose a tag to compare

@xfgong xfgong released this 08 Jun 14:16

[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.