Upgrading¶
What changes between releases that you might have to act on, with the code before and after. Each release lists who is affected; if none of it applies to you, upgrading is a version bump.
The kaboo libraries can be upgraded in any order. Each one reads what the older versions of the others send, and nothing an older piece sends can be mistaken for an approval. Features that span the stack light up once every piece is upgraded — see Upgrading the stack.
0.31¶
0.31 passes the shared state to working views. Nothing is removed, and the other kaboo libraries need no change.
New¶
- Views read the shared state.
ViewRequest.stateis the shared state as of the call, state effects included, so a view can render what the effects keep (see Shared state).
0.30¶
0.30 adds working views and actor stamps for the services agents call, and state effects read structured results. Nothing is removed, and the other kaboo libraries need no change.
State effects read structuredContent¶
Who: state effects, with a path or a mapper, on MCP tools that return
structuredContent.
An effect's result is the tool's structuredContent when the server sends one,
and its text otherwise, parsed as JSON when it is JSON. A mapper written against
such a tool's text now receives the structured value, so the tool can answer the
model in prose and the effect in data (see
Shared state).
New¶
- Working views. An agent's
views:names callables that return aView: a base rendered at the invocation's first model call and kept on the latest person message, where a prompt cache keeps applying, and the changes since, rendered at every call. A visualizer sees the dashboard it is building without reading it again (see Shared state). - Actor stamps.
MCPCallMetaHookwithstamp_actor: trueputskaboo/actor(agent,thread,run) in each MCP call's_meta, so a service can attribute every change to the agent and conversation that made it (see Hooks).
0.29¶
0.29 moves kaboo-workflows onto AG-UI's own events and state: sub-agents, questions, approvals and a conversation's memory all use the protocol's standard fields. Upgrade it together with kaboo-runtime 0.8 and kaboo-react 0.12; older clients don't read the new events. It needs ag-ui-strands 1.0.2 or later:
- "ag-ui-strands>=1.0.1,<2",
+ "ag-ui-strands>=1.0.2,<2",
Sub-agents are SUBAGENT_* events¶
Who: everyone serving through create_agui_app, and clients that read the
kaboo.activity activity.
Every agent a delegate runs streams as SUBAGENT_STARTED, its tagged text,
reasoning and tool events, and SUBAGENT_FINISHED or SUBAGENT_ERROR (see
Sub-agents in a chat UI). The kaboo.activity
snapshots and deltas are gone, and with them the option that tuned them:
- create_agui_app("config.yaml", activity_deltas=True)
+ create_agui_app("config.yaml")
A structured output's schema name is in SUBAGENT_FINISHED.metadata.outputSchema,
a tool label in TOOL_CALL_START.metadata.label, the run's trace id in
RUN_FINISHED.metadata.traceId and each sub-agent's in
SUBAGENT_STARTED.metadata.traceId. RUN_FINISHED.usage counts every agent.
The browser's tools replace the built-in ask_user¶
Who: agents with interrupt: true or interrupt.ask_user, and code that
imports kaboo_workflows.tools.ask_user.
A question is now a tool the browser runs, like a file request. Allow it with
client_tools, on any agent, delegates included, and register it in the
browser (kaboo-react's useAskUserTool):
agents:
field_agent:
- interrupt: true
+ client_tools: [ask_user]
interrupt:
tools: [submit_report]
- ask_user: false
The run ends with the call in RUN_FINISHED.outcome.pendingToolCallIds, and the
browser's tool message answers it. Questions are no longer interrupts; only
approvals are. See Human-in-the-loop.
Approvals use ag-ui-strands' native interrupts¶
Who: clients that render approvals.
An approval is published by ag-ui-strands and filled in by kaboo:
reason: "kaboo:approval", the call's toolCallId (a delegate's own call),
the delegate's subagentRunId and a responseSchema. metadata holds the
same fields as before. Resume entries are unchanged.
A conversation's memory moves to kaboo_sessions¶
Who: hosts that store or inspect kaboo_history or kaboo_session.
- state.kaboo_history # sub-agent transcripts
- state.kaboo_session # a pending gate
+ state.kaboo_sessions.history # every agent's transcript
+ state.kaboo_sessions.paused # the paused checkpoint
+ state.kaboo_sessions.approvals # the approvals it waits on
A run writes it back as one STATE_DELTA instead of in a STATE_SNAPSHOT.
kaboo-runtime 0.8 keeps it on the server. Conversations stored before 0.29 go on,
but their sub-agents start without the old memory, and a pause opened before the
upgrade can't be resumed: answer waiting approvals before upgrading.
No more MESSAGES_SNAPSHOT, and no continuation pause¶
Who: clients that rebuilt their transcript from MESSAGES_SNAPSHOT, and
hosts that pass response_budget.
A run no longer re-sends the whole conversation after every step, so a response grows with its turn, not with the thread (Response size). Build the transcript from the events, as CopilotKit does. With responses that small, the continuation pause is gone:
- create_agui_app("config.yaml", response_budget=CONTINUATION_BUDGET)
+ create_agui_app("config.yaml")
ContinuationHook, CONTINUATION_TYPE, ResponseBudget,
set_response_budget and get_response_budget are removed, and no run pauses
with kaboo:continuation. Keep a host-side backstop that closes a response just
under the host's cap (Serve on AgentCore).
Swarm and Graph entries run on ag-ui-strands' bridge¶
Who: services whose entry is a swarm or a graph.
kaboo's own multi-agent bridge is removed. A Swarm or Graph entry streams its
nodes as steps, with their text in the main transcript, and a Graph's
chat_output no longer picks the one node that speaks. A delegate entry is
unaffected; see Swarm + graph.
New¶
client_toolson any agent: the browser's tools it may call, with their context. See Ask the user.state_effectsand per-agentworkspaceviews. See Shared state.render_a2uifor the agents allowed it, and surface actions as the person's turn. See Generative UI.GET /invocations/capabilitiesanswers AG-UIAgentCapabilities.
Testing¶
Who: tests that read TurnResult.
TurnResult.groupsholds the sub-agent groups folded from the events, keyed bysubagentRunId. The entry agent has no group (noinlineChatOwner), and a structured output isresult, withoutputSchemaName.TurnResult.messages()builds the transcript from what the run sent and streamed, instead of reading the lastMESSAGES_SNAPSHOT.TurnResult.text()is the entry agent's reply alone.- Answer a browser tool with
turn(tools=[...], messages=[*r.messages(), tool_message])(Testing).
0.28.1¶
Who: agents with attachments: { tool: true } whose model opens a picture,
PDF or video with fetch_attachment, and code that reads that tool's result.
fetch_attachment now returns the file in its own result. Before, the tool
returned only text and the ReferenceHook added the file to the latest user
message, which is the one holding the tool's result, so the file came before
it. Bedrock refuses a tool call that isn't followed directly by its result, and
OpenAI's Chat Completions API got the file as a user message between the call
and its result, so the run failed right after the fetch. strands formats a file
in a tool result for each provider: Bedrock takes pictures, PDFs and video in
the result, and on OpenAI and Ollama pictures reach the model too.
fetch_attachment("img_1")
- {"status": "success", "content": [{"text": "Loaded 'a.png' (image/png). It is now included in the conversation — read it directly to answer."}]}
+ {"status": "success", "content": [{"text": "Loaded 'a.png' (image/png)."}, {"image": {"format": "png", "source": {"bytes": b"..."}}}]}
Two providers can't show the model a file in a tool result: strands sends one
to Gemini as JSON inside the function response, and keeps a PDF inside the
tool message on OpenAI's Chat Completions API, which takes only text there. An
agent on either that has to look at files should use attachments: { inline: true },
which puts them in the person's message instead.
0.28¶
kaboo-workflows moves to the AG-UI 1.0 generation of ag-ui-protocol and
ag-ui-strands, and to mcp 2.
Dependencies¶
Who: apps that pin any of these packages themselves.
- "strands-agents>=1.44.0,<2.0.0",
- "mcp>=1.24.0,<2",
- "ag-ui-strands>=0.2.0,<0.3",
- "ag-ui-protocol>=0.1.19,<1",
+ "strands-agents>=1.55.0,<2.0.0",
+ "mcp>=2.0.0,<3",
+ "httpx2>=2.5.0,<3",
+ "ag-ui-strands>=1.0.1,<2",
+ "ag-ui-protocol>=1.0.0,<2",
Tested with strands-agents 1.61, mcp 2.1, ag-ui-strands 1.0.1 and ag-ui-protocol 1.0.0. Approvals and questions resume exactly as before: kaboo-workflows answers them itself rather than through ag-ui-strands' own interrupt protocol.
Clients need no change: kaboo-runtime 0.6 and kaboo-react 0.10 work with 0.28,
and kaboo-runtime 0.7 and kaboo-react 0.11 work with 0.27. Runs now also carry
ag-ui-strands' RAW events, one per model call with its usage and latency.
AG-UI activity deltas carry typed patch operations¶
Who: code that reads ag_ui.core events in Python, such as tests that
inspect the activity stream.
ag-ui-protocol 1.0 types ActivityDeltaEvent.patch as JSON Patch operation
models instead of dicts. What goes over the wire is unchanged; read the patch as
a client receives it:
- ops = event.patch
+ ops = event.model_dump(mode="json", by_alias=True, exclude_none=True)["patch"]
Custom MCP servers get the SDK's MCPServer¶
Who: anyone who subclasses kaboo_workflows.mcp.MCPServer or sets
server_params.
mcp 2 renamed FastMCP to MCPServer in mcp.server.mcpserver, and
_register_tools receives that class. Its tool() decorator is unchanged.
- from mcp.server.fastmcp import FastMCP
+ from mcp.server.mcpserver import MCPServer as SdkMCPServer
from kaboo_workflows.mcp import MCPServer
class CalculatorServer(MCPServer):
- def _register_tools(self, mcp: FastMCP) -> None:
+ def _register_tools(self, mcp: SdkMCPServer) -> None:
@mcp.tool()
def add(a: float, b: float) -> float: ...
server_params still reach the server's constructor, which no longer takes
host, port, stateless_http or json_response. kaboo-workflows passes the
host and port from the server's definition, and serves streamable-http
stateless with JSON responses as before.
MCP clients take httpx2¶
Who: anyone who passes transport_options.http_client or
transport_options.auth in code, or writes their own auth for an MCP client.
YAML auth: settings need no change.
mcp 2 makes its HTTP requests with httpx2, so a client or auth you pass must
come from it. kaboo-workflows' RelayTokenAuth, StaticTokenAuth and
M2MClientCredentialsAuth are httpx2.Auth strategies now.
- import httpx
+ import httpx2
- transport_options={"http_client": httpx.AsyncClient(auth=RelayTokenAuth())}
+ transport_options={"http_client": httpx2.AsyncClient(auth=RelayTokenAuth())}
Other code of yours that uses the mcp package directly follows mcp's own 2.0
migration.
Bedrock prompt caching settings¶
Who: Bedrock models that set cache_prompt or cache_tools.
strands deprecates both in favor of cache_config fields. They still work, with
a warning:
params:
- cache_prompt: default
- cache_tools: default
- cache_config: {strategy: auto}
+ cache_config: {strategy: auto, system_prompt_ttl: true, tools_ttl: true}
Question input_type is removed in 0.29¶
The deprecated input_type and question type (see 0.27) still work in 0.28,
and are removed in 0.29.
0.27.1¶
Who: anyone who installed 0.27.0 fresh, or pins one of these packages themselves.
0.27.0 allowed dependency versions it was never tested with, and a fresh
install picked up ag-ui-strands 1.0, whose own interrupt tracking refuses a
resumed run (PENDING_INTERRUPTS), so every approval and question failed on
resume. The ranges now stop at the versions kaboo-workflows is tested with:
- "ag-ui-strands>=0.2.0",
- "ag-ui-protocol>=0.1.19",
- "mcp>=1.24.0",
- "langfuse>=3.0.0",
+ "ag-ui-strands>=0.2.0,<0.3",
+ "ag-ui-protocol>=0.1.19,<1",
+ "mcp>=1.24.0,<2",
+ "langfuse>=3.0.0,<5",
strands-agents keeps <2.0.0 and is tested up to 1.61. If your app pins a
newer version of one of these, pip now reports the conflict instead of
installing a combination that doesn't work.
0.27¶
Questions are multiple choice only, tool approvals gain opt-in remembered decisions, and every answer the person can give reaches the model as approved, denied with a reason, or skipped.
ask_user asks multiple-choice questions only¶
Who: every agent with interrupt: true or interrupt.ask_user, and any
prompt that teaches the model input_type or free-text questions.
Every question now offers two or more options, multiple makes it
multi-select, and the person can always pick Other and answer in their own
words. The agent never lists "Other" itself; the tool strips it. A call
without enough options gets a tool error that teaches the rule, so the model
asks again with options instead of reaching the person.
- ask_user(question="What should I focus on?")
- ask_user(question="Which sectors?", input_type="checkbox", options=["B2B", "B2C"])
+ ask_user(question="What should I focus on?", options=["Revenue", "Churn", "Costs"])
+ ask_user(question="Which sectors?", options=["B2B", "B2C"], multiple=True)
Update prompts the same way:
system_prompt: |
- Use input_type "radio" when one option applies and "checkbox" when several do.
+ Ask with ask_user, offering options; set multiple when several can apply.
Deprecated, removed in 0.29: input_type and a question's type of
radio or checkbox still work, as single and multi-select, and log a
deprecation warning. text is refused. Question requests also still carry
type for kaboo-react before 0.10.
The answer reaches the model as before — a {question: answer} map — with a
list for multi-select and the person's own words for Other. A skipped question
returns "The person skipped this question."
Custom requests are their own kind, not text questions¶
Who: tools that raise a form interrupt with a text question and a
discriminator such as kind or kaboo_kind, to make a UI show a file or data
source picker.
Raise your own request kind instead. kaboo-react renders it with the renderer
you register for that type, and shows a minimal card with Skip when nothing is
registered.
response = tool_context.interrupt(
name="request_datasources",
reason={
- "type": "form",
- "kind": "datasources",
- "kinds": kinds,
- "questions": [{"question": reason, "type": "text"}],
+ "type": "datasources",
+ "message": reason,
+ "kinds": kinds,
},
)
A skipped request answers {"status": "cancelled", "skipped": True}, so a tool
that already treats "cancelled" as "no answer" needs no change. See
Ask the user.
Only an approval runs a gated call¶
Who: clients and custom approval cards that resume tool gates.
A gated call runs only when the answer's status is "approved". Before, any
answer other than "cancelled" ran it.
resume = [{"interruptId": gate_id, "status": "resolved", "payload": {"status": "approved"}}]
- resume = [{"interruptId": gate_id, "status": "resolved", "payload": {"status": "rejected"}}]
+ resume = [{"interruptId": gate_id, "status": "resolved", "payload": {"status": "cancelled", "reason": "Use the final title"}}]
- Deny with
{"status": "cancelled", "reason"?}. The model reads "Declined:". - Skip with a plain AG-UI
cancelledentry, or by sending a message. The model reads "Skipped: …". - Approve with
{"status": "approved", "scope"?, "tool_input"?}.
The model's wording changed: "User rejected this action." is now "Declined.", and a call closed by a new message reads "Skipped: the person sent a message instead of answering."
A message sent while something waits skips it¶
Who: hosts and clients; no config change.
A run that cancels every open request and ends with a new user message — the
AG-UI form of "the person moved on" — is a new turn: the waiting calls close as
skipped, their activity cards close, and the agent answers the message. A plain
new message (no resume) did this already. kaboo-react 0.10 sends it for you.
The deadline moved into metadata, and the agent enforces it¶
Who: agents with interrupt.ttl_seconds, and clients that read the AG-UI
interrupt's expiresAt.
The AG-UI client refuses to resume a thread, or to send anything on it, once an
open interrupt's own expiresAt has passed — the chat was stuck. The deadline
now travels only in metadata.expiresAt, and an approval answered after it does
not run.
- const deadline = interrupt.expiresAt
+ const deadline = interrupt.metadata?.expiresAt
Without ttl_seconds, approvals wait indefinitely, like questions.
Remembered approvals (opt-in)¶
Who: anyone who wants "Allow for this chat" or "Always allow".
interrupt:
tools:
- "merchant___*"
- { name: sql___run_query, remember: once }
skip_read_only: true
remember: thread
toolstakes names, globs, or{name, remember, subject}objects.skip_read_onlylets tools annotatedreadOnlyHint: truerun without asking.rememberis the widest scope a person may choose:once(the default),threadoralways.subjectis an import spec deciding what a remembered decision covers.
Wider scopes are offered only when the host remembers decisions: it sends
forwardedProps.kaboo_grants and the run publishes its open requests in
kaboo_session.approvals. kaboo-runtime 0.6 does both when given a grant store.
See Human-in-the-loop.
Tools can ask for approval themselves (opt-in)¶
Who: anyone whose tools are generic (one tool that does many things, such
as a GraphQL run_mutation) and whose service already knows what each call
needs.
interrupt:
tool_approvals: true
A tool that won't run a call yet returns a kaboo_approval reply instead of a
result. The run pauses with an approval request carrying the tool's request
ID, and the tool is called once more if the person approves. The host must
tell the tool's service about the decision before the run continues
(kaboo-runtime 0.6's onDecision). Nothing changes until you turn it on. See
Tools that enforce their own approvals,
including the checklist for the service.
For hosts¶
- Open approval requests are published in
state.kaboo_session.approvals, from both the single-agent and the Swarm/Graph adapters:interruptId,toolCallId,tool,subject,scopes,input,expiresAt, andrequestwhen a tool asked for the approval. Forward a decision with arequestto that tool's service, and don't store it as a grant. - The tool's activity entry records how a gated call was decided
(
approval: {decision, by, scope?, reason?}). - The private
_build_resume_responseshelpers are replaced by one shared builder inkaboo_workflows.adapters._interrupts.