Skip to content

Release notes

0.23.34 — Native tools can confirm a failed mutation (#402)

Section titled “0.23.34 — Native tools can confirm a failed mutation (#402)”

ToolExecutionResult accepts an optional failure_confirmed: bool = False. A ledgered native tool that returns succeeded=False, failure_confirmed=True settles failed (mutation_failed) instead of outcome_unknown; set it only after verifying the write had no effect. Failures without the flag, plain-string results and handler exceptions are unchanged. failure_confirmed=True with succeeded=True raises ValueError. See the skills reference.

0.23.33 — Approved-write execution start event (#397)

Section titled “0.23.33 — Approved-write execution start event (#397)”

Approval-v2 streams emit tool_execution_started once after approval_resolved for a human-approved call, before post-approval admission, mutation checkpointing or transport work. It uses the existing v2 envelope and identities and means local execution started, not remote commitment. tool_started, tool_completed and terminal tool_execution keep their meaning. Post-approval admission and storage waits honor cancellation and the effective request ceiling: unsent calls report not_executed, dispatched ambiguous failures stay outcome_unknown and are never retried. Tool budgets are not extended. See the approved execution stream contract and its producer fixtures.

0.23.33 — Preserve observability queries in presentation copies (#399)

Section titled “0.23.33 — Preserve observability queries in presentation copies (#399)”

Receipts, approval requests and API tool events now show query fields, including PromQL and nested log/trace pipeline stages, with secret-like substrings scrubbed; they were previously fully redacted. Keys containing a whole sql or script token stay opaque; incidental names such as javascript no longer are. Secret-field redaction and the 2,500-character argument cap are unchanged. Hosts that relied on tool_started hiding queries should read the skills reference.

Section titled “0.23.33 — Forward MCP resource links (#395)”

tool_completed events forward well-formed _meta.links {label, url} pairs from MCP results, alongside reference_url, including on error results. Malformed entries and oversized strings are dropped individually; tool outcomes and adapter signatures are unchanged. Hosts validate URL suitability; see the skills reference.

0.23.32 — Persist finalizer coverage (#384)

Section titled “0.23.32 — Persist finalizer coverage (#384)”

Session history now saves the trusted turn finalizer’s coverage, including limitations, for model answers, recovered narration failures and deadline wrap-up. Saved coverage matches TurnResult, API run_completed events and coalesced results, as it already did for locally handled turns. Evidence receipts, cancellation and erasure fencing retain their existing behavior. Public entry coverage is verified across failed recovery, wrap-up, concurrent sessions and erasure controls (#386).

0.23.32 — Preserve observed MCP reference URLs (#385)

Section titled “0.23.32 — Preserve observed MCP reference URLs (#385)”

Public tool results keep producer-supplied reference_url metadata when present and well-formed. Missing, malformed or oversized values stay absent. Hosts that already ignore the field retain existing behavior.

0.23.31 — Bounded approved MCP execution (#375)

Section titled “0.23.31 — Bounded approved MCP execution (#375)”

Hosts can register ApprovedMutationBudget to give selected, freshly human-approved MCP writes a finite execution allowance within a fixed request ceiling. Session, approval, transport, cancellation drain, coalescing and wrap-up share that bound. Unconfigured hosts retain existing timing defaults; auto-approved calls receive no additional allowance. See the host API contract.

MCPExecutionTimeout distinguishes pre-dispatch rejection from dispatched uncertainty. mutation_outcome/v1 adds an optional timeout reason; unknown dispatched writes remain blocked from replay until authoritative reconciliation. API, Slack, CLI and Telegram approval waits honor an explicit request expiry.

0.23.31 — Completed evidence survives narration failure (#374)

Section titled “0.23.31 — Completed evidence survives narration failure (#374)”

After a successful non-setup tool call, known nonretryable provider narration failures now produce a bounded deterministic partial answer. This applies to post-tool synthesis, recovery synthesis, summary nudges, truncated-response recovery, and deadline wrap-up. No additional provider retry is made for a nonretryable error. The answer retains tool results, evidence receipts, coverage, degraded="deterministic_fallback", and time_boxed when the deadline expired.

The registered trusted turn finalizer runs before persistence and delivery, including coalesced callers. Loading a skill, asking a human, paging an earlier result, or a failed read alone does not qualify for this new recovery path. Authentication, permission, unknown provider failures, and failures before successful evidence remain errors on this path. Internal and persistence errors are not converted to completed turns. Explicit cancellation now propagates as cancellation instead of becoming a retryable narration timeout.

Unrecovered API runs retain the fixed agent run failed message and add a content-free failure diagnostic object to the terminal event and warning log. See Terminal answers and failures for the field contract.

0.23.30 — Recover timeouts before the first investigation read (#373)

Section titled “0.23.30 — Recover timeouts before the first investigation read (#373)”

After setup-only operations such as load_skill (including a failed load), a retryable provider timeout can now recover with tools enabled once per turn. The retry retains the setup acknowledgment and uses at most the smaller of the original deadline’s remaining time and engine.session.wrapup_timeout_seconds. It does not extend the transport deadline or retry a cancelled turn. A successful recovery can dispatch the first read through normal approval and budget checks.

If recovery fails, the retry has already been used, or the deadline expires before investigation starts, the terminal response says that the investigation did not start. Setup-only deadline expiry does not make an additional synthesis call. load_skill, human__ask, and read_tool_result are excluded from new investigation findings and attempted-check counts; skill prompts are not evidence.

After any non-setup tool attempt, recovery remains synthesis-only: attempted reads and writes, including outcome-unknown mutations, are never replayed by provider recovery. Existing approval outcomes and tool/result pairing are retained.

Hosts adopting this fix (including last9/ai#929) need no configuration or wire changes. Successful setup recovery returns the normal result (degraded=null); terminal fallback uses degraded=deterministic_fallback, with time_boxed=true when the original deadline expired. Provider-recovery telemetry distinguishes stage=setup from the existing stage=synthesis.

0.23.30 — MCP servers that refuse the startup connect recover (#370)

Section titled “0.23.30 — MCP servers that refuse the startup connect recover (#370)”

MCPManager.start() retries every started server that has no tools in the background (1s doubling to 30s, jittered) until it connects or stop() runs, logging ERROR while it has none and INFO once on recovery. is_healthy(), and so /healthz, now fails while any started server has no healthy session or an empty tool list, so a rollout does not mark such a pod Ready. Hosts whose servers legitimately expose no tools will see /healthz return 503.

ToolRouter.lock_allowlist() admits the tools of a server that was toolless at lock time once, on its first discovery; tools other servers added after the lock stay outside it. Engine dispatch calls the new ToolRouter.discover_surface(), which makes one connect attempt via the new MCPManager.connect_if_toolless(server), bounded by 5s, the turn deadline and its cancellation, before a miss on such a server becomes Unknown tool. It returns the snapshot the tool was found in, and approval binds to that snapshot’s tool revision and definition; a tool published after the lookup is not executed by that call. get_definition() stays a local lookup.

0.23.30 — Bedrock in-turn calls read the previous call’s cache (#364)

Section titled “0.23.30 — Bedrock in-turn calls read the previous call’s cache (#364)”

Converse requests with prompt caching now also put a cachePoint on the message before the last assistant reply, which is where the previous call of a tool loop ended. Bedrock finds a cached prefix only at a cachePoint or within about 20 content blocks before one, so after a round of many parallel tools the next call used to write the whole prefix again. A request still carries at most four cachePoints: when the loaded-skills section takes the fourth, the tool list gets none, as the system cachePoints already cover it.

0.23.29 — Opt-in Bedrock heartbeat cache TTL (#305)

Section titled “0.23.29 — Opt-in Bedrock heartbeat cache TTL (#305)”

llm.bedrock.heartbeat_cache_ttl (5m or 1h, unset by default) adds that ttl to every Converse cachePoint on heartbeat calls. Unset, requests are unchanged: no ttl field, the default 5-minute checkpoint. BedrockProvider takes it as the keyword argument heartbeat_cache_ttl; other values raise ValueError at construction.

The bedrock extra now requires boto3>=1.42.31 (was >=1.37.24): older botocore rejects the ttl field client-side with Unknown parameter ... cachePoint: "ttl". Hosts that pin boto3 below 1.42.31 must upgrade before setting heartbeat_cache_ttl.

0.23.28 — Forced tools on streams; deadline-cut input from blocking calls (#360)

Section titled “0.23.28 — Forced tools on streams; deadline-cut input from blocking calls (#360)”

create_message_stream accepts required_tool

Section titled “create_message_stream accepts required_tool”

LLMProvider.create_message_stream(required_tool=...) forces one call of the named tool, exactly as create_message does: Anthropic sends tool_choice {type: tool}, OpenAI tool_choice {type: function}, and Bedrock (through the base fallback) toolChoice.tool. An empty name or one that does not match exactly one tool raises ValueError before any network call. The base fallback forwards required_tool only when it is set, so host providers whose create_message predates the parameter keep working.

Blocking calls keep deadline-cut tool input

Section titled “Blocking calls keep deadline-cut tool input”

Anthropic and OpenAI (Responses, including Bedrock Mantle) now run a guarded blocking create_message (one with a deadline or cancel event) over the SDK stream. When the guard cuts it mid tool call, TurnResult.interrupted_tool_inputs carries the input streamed so far, as it already did for streaming calls (#354), so adapters without supports_streaming get it too. Responses, usage, cost and span attributes are unchanged. Unguarded calls still use the non-streaming endpoint. Bedrock Converse does not stream, so its field stays ().

0.23.27 — Per-turn session deadline hook (#355)

Section titled “0.23.27 — Per-turn session deadline hook (#355)”

Hosts can set the turn deadline per request

Section titled “Hosts can set the turn deadline per request”

Engine.register_turn_deadline(resolver) is called once per request as resolver(message, adapter), before coalescing. It returns positive, finite seconds, or None to use the configured engine.session.deadline_seconds. Invalid values (0, negative, NaN, inf, bool, str) are rejected before any model call. Only one resolver can be registered, and a skill whose bind() fails has its resolver rolled back. The resolved deadline drives the turn, the recovery call and the coalescing TTL and attach wait; the wrap-up budget is unchanged. In-flight owners are judged stale by the TTL they started with, so a default-deadline request no longer duplicates a still-running longer turn.

Hosts that register no resolver see no change.

0.23.27 — Deadline-cut streaming tool input (#354)

Section titled “0.23.27 — Deadline-cut streaming tool input (#354)”

A tool call cut by the deadline keeps its streamed input

Section titled “A tool call cut by the deadline keeps its streamed input”

When the turn deadline or cancel guard cuts a streaming model call while it is emitting a tool call, the input streamed so far is returned as TurnResult.interrupted_tool_inputs instead of being lost. The cut call is never dispatched. Anthropic and OpenAI (Responses) streams report tool-input deltas through the new StreamEmitter.

Limits: non-streaming calls cannot produce partial input, so the field is () for adapters without supports_streaming (including the API wait=true path without stream=true) and for Bedrock Converse, which does not stream.

0.23.26 — Slack runtime readiness (#334)

Section titled “0.23.26 — Slack runtime readiness (#334)”

Startup membership waits for Slack readiness

Section titled “Startup membership waits for Slack readiness”

New bearer-authenticated GET /api/runtime-readiness returns the loaded workspace, agent, connection, revision and generation once the Slack adapter is initialized, and 503 before. Hosts must invite the bot only after all five fields match the launch. Startup and queued join-event onboarding now wait for the same readiness, and managed /slack/events returns 503 until it. See runtime readiness.

Unreleased — Interrupted tool input (#353)

Section titled “Unreleased — Interrupted tool input (#353)”

Time-boxed turns keep the tool input the cut call had streamed

Section titled “Time-boxed turns keep the tool input the cut call had streamed”

When the session deadline cuts a streaming model call, TurnResult.interrupted_tool_inputs holds one {"name", "input_json"} per tool_use block the call had started, in stream order. input_json is the raw input received so far and is usually truncated JSON. The engine never dispatches these calls; the time-boxed wrap-up is unchanged. Every other turn returns (), as do turns whose cut call was not streamed: adapters without supports_streaming (including API wait=true without stream=true) and Bedrock Converse, which has no streaming path. Hosts read the field in a turn finalizer or from handle_with_results.

Custom providers built on GuardedAsyncStream now receive a StreamEmitter instead of a text callback: replace emit(chunk) with emit.text(chunk), and report tool_use input with emit.tool_input(block, delta, name=...) to support interrupted_tool_inputs.

0.23.26 — Mutation outcome history and live events (#341)

Section titled “0.23.26 — Mutation outcome history and live events (#341)”

Mutation outcomes are readable after the fact

Section titled “Mutation outcomes are readable after the fact”

Settled guarded mutations are now projected from the logical-turn ledger as a mutation_outcome/v1 record: {schema, logical_turn_id, mutation_id, tool, status, resource_id, url, title}. Nothing new is stored; every view is derived from the existing ledger entries, so history, live events and replay agree.

  • History: SessionManager.mutation_outcomes(key) and the authenticated GET /api/mutation-outcomes?session_id=.
  • Live events: after a settlement is durable the engine calls Adapter.on_mutation_outcome(outcome, mutation_revision=n); the API adapter emits a mutation_outcome run event, and GET /api/mutation-events replays them over SSE with id: / Last-Event-ID.
  • Replay cursor: each logical turn keeps a durable revision counter. Replay returns entries changed after the cursor, and an outcome that is still outcome_unknown is re-sent until it settles or is reconciled.

Hosts that do not read these views see no behaviour change.

MCP tools withdraw while their server is unavailable

Section titled “MCP tools withdraw while their server is unavailable”

This applies to every MCP host, including hosts without a resolve_surface hook. Quarantine, transport cleanup failure, degraded sessions and reconnect withdraw that server’s direct mcp__* tools and aliases from new model tool lists; earlier versions kept the cached definitions while disconnected. Successful rediscovery restores eligible tools. In-flight calls to an unchanged tool on another server remain admissible. See skills reference.

lock_allowlist() freezes the allowed names permanently, including when called again. A startup tool can be withdrawn and later restored; a tool first discovered after startup cannot enter the locked set.

0.23.24 — Durable mutation recovery (#342)

Section titled “0.23.24 — Durable mutation recovery (#342)”

Guarded mutations refuse exact replay by default

Section titled “Guarded mutations refuse exact replay by default”

Calls that require approval, and tools registered with register_mutation_tools, now checkpoint intent before dispatch and their outcome before narration. An exact replay within the same delivery or logical turn returns not_executed with mutation_completed, mutation_failed or mutation_outcome_unknown instead of executing again, including after a restart. Prepared MCP calls are checkpointed after approval, so preparation and approval may repeat but the write is admitted once.

New refusal reasons: mutation_arguments_changed (approved arguments changed before dispatch) and mutation_checkpoint_unavailable (state storage failed). Host error handling and eval assertions should recognize them.

Live-request coalescing now honors explicit delivery IDs: distinct deliveries each reach durable admission; identical deliveries still coalesce. MCP tools that execute without approval, registration or a canonical read-only declaration log a warning (without arguments). See durable mutation recovery for the host contract.

0.23.24 — MCP approval preparation (#343)

Section titled “0.23.24 — MCP approval preparation (#343)”

Preparation runs before approval by default

Section titled “Preparation runs before approval by default”

All hosts now run the owning skill’s before_mcp_call hook before human approval, including hooks that return no preview. This is a default behavior change with no opt-in flag: read-only probes run even if the human later denies the call or the approval wait times out. Prerequisite and correction decisions short-circuit before the parent is approved; prerequisite reads follow their own policy, and resumed parents require fresh preparation and approval.

Audit existing hooks before upgrading: keep probes read-only, honor cancellation and deadlines, and do not rely on prior human approval. Successful preparation is bound to the arguments and preview by a single-use receipt and is not repeated after approval. Existing approval overrides and auto-approval policies still apply.

Ordinary unguarded turns now also suppress native and MCP dispatch when their cancellation event is set. Previously this dispatch check applied only to guarded turns. Cancellation does not roll back work already executed.

Hook execution/admission timeouts now return preflight_timeout instead of preflight_failed. Update host error handling and eval assertions that match the old reason to recognize preflight_timeout; other hook failures still use preflight_failed. Request deadline expiry and cancellation remain distinct deadline and cancelled outcomes.

Reserved argument and API presentation shapes

Section titled “Reserved argument and API presentation shapes”

A top-level executable MCP argument named approval_preview is now rejected with reserved_approval_preview, including legitimate server-defined arguments. Rename that argument before using the tool with mithai.

API approval events keep flat tool_input arguments when no preview exists (e.g. {"title": "Services"}). When a preview exists, even an empty object, tool_input becomes {"arguments": {"title": "Services"}, "approval_preview": {}}. Both shapes contain redacted presentation data; clients must support both. Slack labels the evidence “Host-supplied preview”: mithai binds the evidence to the call but does not verify its claims. Hosts must accurately describe the arguments being applied.

See the skills reference for the preparation contract.