Today we are releasing Hashbrown v0.8.
In this release, we are adding decision models alongside generative models. You can use a decision to route a question, classify input, or choose which components a chat can render. The answer is typed, and it includes the model's confidence so your application can decide when to act.
There is more in this release, too. We can now show the progress and outcome of server tools, enable web search with OpenAI and Azure, and read how a model finished along with its token usage. We have also been building atc, a live flight-tracking application that puts these features to work in both React and Angular.
Here is what we will cover:
- Decisions and UI selection, with TypeSafe's Jev as the first decision adapter.
- Component descriptions and rules, replacing few-shot UI examples.
- OpenAI and Azure on the Responses API, including reasoning and web search.
- Activities, server tool steps, and citations.
- Run results, token usage, finish reasons, and provider fixes.
- Updating your application to v0.8.
This is a breaking release. Update your client packages and server adapter together, and review the upgrade checklist below before you deploy.
The client examples show both React and Angular.
React hooks run inside a component; Angular resource fields run inside a component or another injection context.
Configure HashbrownProvider in React or provideHashbrown in Angular before using them.
The server examples are shared by both frameworks.
Decisions
The Use Case
Consider a flight-tracking application. A user can ask to filter the map, look up an aircraft, or ask a question that needs a written answer. Before we generate an answer, we need to know where to send the input.
That is a closed question: the answer must be one of the options our application understands. Other examples include classifying a support request, checking whether a message is urgent, or choosing a panel to open. A decision model answers these questions with an enumeration, a boolean, or a number on a bounded scale.
We could ask a generative model for structured output here. The benefit of a decision model is that it returns the answer together with probabilities. Our application can use both: act on a confident answer, or fall back to a chat when the model is unsure.
| Task | Use |
|---|---|
| Route a question to the right panel | Decision |
| Choose cards for a query | Decision |
| Classify, triage, or tag input | Decision |
| Write an answer or explanation | Generation |
| Fill in the props of a component | Generation |
Define the Questions
First, define the questions with a Skillet schema.
s.enumeration() gains per-option descriptions in this release, so the model knows what each option means:
import { s } from '@hashbrownai/core';
/** Routing questions shared by both client implementations. */
export const routeSchema = s.object('Where to send the question', {
intent: s.enumeration('What the user wants to do', {
filter: 'Narrow the list of flights on the map',
chat: 'Ask a question that needs a written answer',
other: 'Anything else',
}),
clear: s.boolean('The user wants to clear the current filter'),
});
import { useDecision } from '@hashbrownai/react';
import { routeSchema } from './route-schema';
function Search({ query }: { query: string }) {
const route = useDecision({
schema: routeSchema,
input: query === '' ? null : query,
system: 'You route questions in a flight-tracking app.',
debounceTime: 150,
});
if (route.value?.intent === 'filter') {
return <FilterPanel />;
}
return <Chat />;
}
Let's break that down:
routeSchemais aDecidableSchema. TypeScript checks it at compile time, so it can only ask what a decision model can answer: enumerations, booleans, bounded numbers and objects of those.inputis the thing to decide about. Passnullwhen there is nothing to ask yet.valueis typed from the schema.metamirrors the shape ofvalueand carries the model's confidence for each question.
In Angular, decisionResource() takes the shared schema and returns signals.
The panel and chat components below are your application's own components:
import { Component, computed, input } from '@angular/core';
import { decisionResource } from '@hashbrownai/angular';
import { routeSchema } from './route-schema';
import { FilterPanelComponent } from './filter-panel.component';
import { ChatComponent } from './chat.component';
@Component({
selector: 'app-search',
imports: [FilterPanelComponent, ChatComponent],
template: `
@if (route.value()?.intent === 'filter') {
<app-filter-panel />
} @else {
<app-chat />
}
`,
})
/** Routes the current query to the filter or chat panel. */
export class SearchComponent {
readonly query = input.required<string>();
readonly route = decisionResource({
schema: routeSchema,
input: computed(() => (this.query() === '' ? null : this.query())),
system: 'You route questions in a flight-tracking app.',
debounce: 150,
});
}
Decide which UI to show
Next, we can use a decision to choose which UI to offer.
useUiDecision() and uiDecisionResource() let a decision pick components.
There are two modes:
- Candidates. Your code builds every option, and the model picks which ones to show. The model never writes a prop.
- Chat. You keep your own chat, with its own system prompt, tools and transcript. A decision picks from the chat's components, the chat is narrowed to the chosen ones, and the message is sent.
Here is chat mode, using your existing flight and airport component definitions and getFlights tool.
Import the hooks from @hashbrownai/react, or the resources from @hashbrownai/angular:
const chat = useUiChat({
system: 'Answer questions about live flights. Use the cards to show results.',
tools: [getFlights],
components: [flightCard, airportCard],
});
const { sendMessage, isDeciding } = useUiDecision({
chat,
select: { max: 2 },
minConfidence: 0.7,
system: 'A flight-tracking app. The input is a chat message.',
});
readonly chat = uiChatResource({
system: 'Answer questions about live flights. Use the cards to show results.',
tools: [getFlights],
components: [flightCard, airportCard],
});
readonly selection = uiDecisionResource({
chat: this.chat,
select: { max: 2 },
minConfidence: 0.7,
system: 'A flight-tracking app. The input is a chat message.',
});
Let's break that down:
chatcomes fromuseUiChat()in React oruiChatResource()in Angular. Its system prompt, tools and transcript are used unchanged, so a card can still be filled from a tool result.systemhere is context for the decision model only: what the app is, and what the input means. Layout rules stay in the chat's own prompt, because they do not belong in a decision model's input.selectis the limit:'one', or{ max: 2 }.- The decision looks inside kits. Every component is an option, and a chosen parent brings its declared children along.
- Send through the React hook's
sendMessage()or Angular'sselection.sendMessage(). If the model is unsure, or the decision fails, the message goes out with every component. Nothing breaks; you get the behavior you had before. When the turn finishes, the chat's components are restored.
Some components should remain available on every turn.
For example, I want the chat to keep Markdown even when a decision selects only a flight card.
Put these components in a kit with selectable: false:
const prose = useUiKit({
components: [exposeMarkdown({ citations: true })],
selectable: false,
});
const chat = useUiChat({
system: 'Answer questions about live flights.',
components: [flightCard, airportCard, prose],
});
readonly prose = createUiKit({
components: [exposeMarkdown({ citations: true })],
selectable: false,
});
readonly chat = uiChatResource({
system: 'Answer questions about live flights.',
components: [flightCard, airportCard, this.prose],
});
The decision chooses among the cards.
The Markdown kit stays available, and its members are never included in the selection questions.
Use useUiKit() and exposeMarkdown() from the React package, or createUiKit() and exposeMarkdown() from the Angular package.
On the server, createSelectionSchema() from core produces exactly the questions the hook or resource sends, so you can pin them the same way you pin a prompt.
The UI Selection recipes for React and Angular walk through both modes, kits, children, fallback and pinning.
TypeSafe on the server
Decisions run on a server route that holds your key.
The default is /api/decide.
@hashbrownai/typesafe maps Hashbrown's decision protocol to Jev's API using fetch, without a separate vendor SDK:
import { HashbrownTypeSafe } from '@hashbrownai/typesafe';
app.post('/api/decide', async (req, res) => {
const decision = await HashbrownTypeSafe.decide({
apiKey: process.env.TYPESAFE_API_KEY!,
model: 'jev-1.14.0',
request: req.body,
});
res.json(decision);
});
A few things to know:
- It validates the client's request before any network call.
- It retries rate limits, and reports failures as a
DecisionErrorwith a typed code. - Pass
schemato pin the questions on the server, the same way you pin a prompt. - Pin the model version. There is no
jev-latest, and that is on purpose.
The protocol itself is plain JSON and vendor-neutral. You can implement another decision transport against the same protocol.
Read Decisions for React or Angular for the concept, and the TypeSafe platform page for React or Angular for the shared server setup.
Decisions inside your agent
Not every decision belongs in the browser. An agent that runs in LangGraph, or in your own server, has the same closed questions: what should I do next, and is this urgent?
decodeDecision() is public in core for exactly this.
Call HashbrownTypeSafe.decide with a schema, then decode the response into a typed value and meta:
import { decodeDecision, s } from '@hashbrownai/core';
import { HashbrownTypeSafe } from '@hashbrownai/typesafe';
const nextStepSchema = s.object('Next step for a support conversation', {
step: s.enumeration('What the agent should do next', {
refund: 'Start a refund for the order',
lookup: 'Look up the order before answering',
answer: 'Answer from what is already known',
human: 'Hand the conversation to a person',
}),
urgent: s.boolean('The customer says the problem is urgent'),
});
export async function decideNextStep(conversation: string) {
const response = await HashbrownTypeSafe.decide({
apiKey: process.env.TYPESAFE_API_KEY!,
model: 'jev-1.14.0',
schema: nextStepSchema,
request: { state: conversation },
});
const { value, meta } = decodeDecision(nextStepSchema, response);
return meta.step.confidence >= 0.7 ? value : null;
}
Return null when the model is not sure, and let the agent fall back to its language model.
The same recipe shows how to run decisions over AG-UI itself, in userland: the request travels in forwardedProps.hashbrown.decision, and the answer comes back in RUN_FINISHED.result.
That is the same field lastRunResult reads, which we will get to below.
And when a server does raise a run error, it now reaches error as an AgentRunError with a code, so your app can tell a rate limit from a bad request.
Read Decisions in Agents for React or Angular for both server recipes.
Describing Your Components
We are removing two ways of providing few-shot UI examples: <ui> blocks in the prompt tagged template, and the examples option on UI kits and UI hooks.
The problem I kept coming back to was maintenance. An example names components and supplies their props. When a component changes, that example needs to change with it. When a decision narrows the available components, the example may refer to a component the model can no longer use.
In v0.8, system is a plain string, and there is no compatibility layer for prompt or examples.
Guidance now lives where it travels with the component:
- Component descriptions say when to use the component, when not to, and how it differs from similar ones.
- Prop descriptions and schema constraints hold value conventions: units, formats, ranges, allowed values.
- A short plain-string
systemholds layout and composition rules.
Before, with a React hook or an Angular resource:
useUiChat({
system: prompt`
You are a smart home assistant.
<ui>
<LightCard lightId="light-1" brightness={80} />
</ui>
`,
components: [lightCard],
});
readonly chat = uiChatResource({
system: prompt`
You are a smart home assistant.
<ui>
<LightCard lightId="light-1" brightness={80} />
</ui>
`,
components: [lightCard],
});
After:
useUiChat({
system: `
You are a smart home assistant.
- Show one LightCard for each light the user asks about.
- Add at most one Markdown summary, after the cards.
`,
components: [
exposeComponent(LightCard, {
name: 'LightCard',
description:
'One light and its current state. Use it once per light the user asks about. For general explanations, use Markdown instead.',
props: {
lightId: s.string('The light ID from the getLights tool'),
brightness: s.number('Brightness from 0 (off) to 100 (full)'),
},
}),
exposeMarkdown(),
],
});
readonly chat = uiChatResource({
system: `
You are a smart home assistant.
- Show one LightCard for each light the user asks about.
- Add at most one Markdown summary, after the cards.
`,
components: [
exposeComponent(LightCardComponent, {
name: 'LightCard',
description:
'One light and its current state. Use it once per light the user asks about. For general explanations, use Markdown instead.',
input: {
lightId: s.string('The light ID from the getLights tool'),
brightness: s.number('Brightness from 0 (off) to 100 (full)'),
},
}),
exposeMarkdown(),
],
});
With this approach, the guidance lives alongside the component and its schema. When we remove a component from the choices, we remove its description too.
Two more kit changes ship alongside this:
setComponents()works the way you expected. Before,useStructuredChatfroze the schema at mount, so changing components onuseUiChatoruseUiCompletionchanged the system prompt but not what the model could return. Removing a component also broke earlier answers that used it, and replacing a component under the same name kept the old one. All three are fixed: the schema follows the components, past answers render with every component the chat has offered, and the newest definition wins. Angular'suiChatResourcegainscomponentsandsetComponentsto match.markdownComponent()moves to core.createUiJsonSchema()arrived in v0.7 so an agent you run yourself can build the schema your kit sends. The Markdown component's definition lived only in the React and Angular packages, so a server could not include it. Now it can, and the new Securing a Public Endpoint recipe shows a server pinning the system prompt, the tools and the full UI schema, using atc as the worked example.
OpenAI and Azure on the Responses API
The OpenAI and Azure adapters now stream from OpenAI's Responses API instead of Chat Completions. The adapters continue to handle chat history, tools, and structured output, and can now stream reasoning summaries.
For a reasoning model, Hashbrown asks for reasoning summaries by default and streams them as AG-UI reasoning events, so your app can show the model thinking.
Each reasoning item's encrypted content comes back too, and Hashbrown replays it on the next turn from the client's own history.
Every request sets store: false; the client carries the continuation data needed for later turns.
HashbrownOpenAI.stream.text({
apiKey: process.env.OPENAI_API_KEY!,
model: 'gpt-5',
input,
reasoning: { effort: 'low' },
});
A few things to know:
- Reasoning models are
oplus a digit,gpt-5and up, except the-chatvariants. Other models get no reasoning request. - Hashbrown never sends
effortunless you set it. Setreasoning: falseto stop requesting summaries. transformRequestOptionsnow receives Responses params.messagesbecomeinputitems, andmax_completion_tokensbecomesmax_output_tokens.@hashbrownai/openairequires a server that implements the Responses API. A Chat Completions-only endpoint needs to stay on v0.7 until you can update that endpoint.- Azure: remove
clientOptions.deployment, pass the deployment name asmodel, and use a Responses-capableapiVersion. If your deployment has a custom name, setreasoning: trueto request reasoning summaries without relying on model-name detection.
atc shows a reasoning row above each answer in both frameworks, if you want to see it in a real app.
Web Search and Server Tool Steps
A tool call is part of the answer's story. If an agent searches the web, I want to show what it searched for, whether the search succeeded, and which sources it used.
OpenAI and Azure now support the provider's built-in web search through builtInTools.webSearch:
import { HashbrownOpenAI } from '@hashbrownai/openai';
const events = HashbrownOpenAI.stream.text({
apiKey: process.env.OPENAI_API_KEY!,
model: 'gpt-4.1-mini',
input,
builtInTools: { webSearch: true },
});
The key takeaways are:
- The model provider runs the search. You do not register a browser tool for it.
- The search appears in the assistant message's
serverToolCalls, withkind: 'provider'andbuiltin: 'web_search'. - The call can include its label, queries, sources, timing, and outcome. The answer carries citations that refer to those sources.
- Use a model and endpoint that support the Responses API's
web_searchtool. The Azure adapter exposes the samebuiltInToolsoption.
This also improves tools run by your own agent server.
Hashbrown reads the shared b4.step contract from AG-UI tool-call metadata or a CUSTOM event named b4.step.
A server such as B4.run can stream a label, sources, and status into the matching serverToolCalls entry.
Servers that send ordinary AG-UI tool calls keep working without those optional fields.
The existing status values remain inProgress, executing, and complete.
The new outcome says how a settled call ended: success, failure, denied, or cancelled.
A failed call includes an error and a rejected result, so it no longer looks like a successful call to your UI.
Provider tool calls stay on the provider, even if your application has a client tool with the same name.
Assistant messages also expose citations, sources, and any required attribution.
A citation can refer to a source on a server tool call or on the message itself, so look in both places when you render it.
Show citations as visible, clickable links alongside the answer, and preserve required attribution.
Read the OpenAI, Azure, and server tool call guides for the rendering examples.
Activities
An agent often has more to show than its final answer: a plan, a progress bar, or the status of a background job. AG-UI calls these activities. In v0.8, Hashbrown receives them from any AG-UI backend, and every Hashbrown generative provider adapter can send them.
On the client, Chat.Message gains a fourth variant, role: 'activity', with an id, an activityType, untyped content, and links to the run, the assistant message and the tool call it belongs to.
Activities land in chat.messages in React or chat.value() in Angular, in the order they arrived.
The React example below is the activity branch of a message-role switch; the Angular example belongs in your component template.
In both, planSteps is your application's helper for validating the activity content and returning its steps as strings:
case 'activity':
return message.activityType === 'plan' ? (
<ul key={message.id}>
{planSteps(message.content).map((step) => (
<li key={step}>{step}</li>
))}
</ul>
) : null;
@for (message of chat.value(); track $index) { @if (message.role === 'activity'
&& message.activityType === 'plan') {
<ul>
@for (step of planSteps(message.content); track step) {
<li>{{ step }}</li>
}
</ul>
} }
When an activity belongs with the answer it describes, groupTurns() from core groups the flat list into turns and buckets each turn's activities by their links.
On the server, every provider takes an activities callback.
activity.snapshot() sends an activity and returns a handle you can patch() as the work moves along:
HashbrownOpenAI.stream.text({
apiKey: process.env.OPENAI_API_KEY!,
model: 'gpt-5-nano',
input,
activities: async ({ activity, model }) => {
const plan = activity.snapshot(`plan-${input.runId}`, 'plan', {
steps: ['Read the question', 'Answer'],
done: 0,
});
await model;
plan.patch([{ op: 'replace', path: '/done', value: 2 }]);
},
});
The run waits for the callback to settle, so you can send activities before, during and after the model call. The model never sees them, and Hashbrown never sends them back to the agent.
Two things come along with this:
- User and assistant messages now expose a stable
id, the canonical AG-UI message id. Activities link to it, and so doesgroupTurns(). If you save and restore a transcript, the ids and the links survive. - The provider packages now depend on
@hashbrownai/core.
Read Activities for React or Angular for the flat and nested rendering patterns.
A special shoutout to Manfred Steyer for extending the activity snapshot capabilities for developers. Thank you for helping move this work forward!
What Do I Need to Change?
Let's start with the part you need to act on. Start by updating the packages together, then apply the steps that match your application.
1. Update packages together
Every @hashbrownai/* package in your app must be on the same version, and that now includes the server's adapter:
npm install @hashbrownai/core@0.8.0 @hashbrownai/react@0.8.0
npm install @hashbrownai/openai@0.8.0
npm install @hashbrownai/core@0.8.0 @hashbrownai/angular@0.8.0
npm install @hashbrownai/openai@0.8.0
Replace openai with the provider adapter your server uses.
2. Read the extension from forwardedProps
Hashbrown sends a standard AG-UI run input plus an object of its own: the response schema for structured output and a flag for generative UI.
Since v0.6, that object rode along as a top-level hashbrown key.
AG-UI 1.0 has a proper home for it, forwardedProps, the field an agent reads and intermediaries pass through untouched.
A gateway that validates the input strictly can drop a key it does not know.
Using that standard field lets the extension pass through an AG-UI gateway without relying on an extra top-level key.
A v0.8 client sends only the new shape. If you use an official adapter and never read the extension yourself, step 1 already covered this. If your server reads it, to pin a schema or pick a model, make this change:
Before:
const responseSchema = input.hashbrown?.responseSchema;
After:
const responseSchema = input.forwardedProps?.hashbrown?.responseSchema;
Deploy the client and the server together. A server still on v0.7 sees no schema and answers in free text, and it will not tell you why. 😆
3. Replace examples with rules
If you pass examples to a kit, a UI hook, or a UI resource, or build a system with the prompt template, follow the section above.
Delete the option, describe each component, and write the system prompt as a plain string.
4. Move transformRequestOptions to the Responses API
If you use the OpenAI or Azure adapter and pass transformRequestOptions, rewrite it for the Responses API's ResponseCreateParamsStreaming type.
Messages, including system messages, become input items, and max_completion_tokens becomes max_output_tokens.
Both packages raise their openai peer dependency to ^6.45.0.
For Azure, also remove clientOptions.deployment and pass the deployment name as model.
Before:
transformRequestOptions: (options) => ({
...options,
max_completion_tokens: 2048,
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
...options.messages,
],
}),
After:
transformRequestOptions: (options) => ({
...options,
max_output_tokens: 2048,
instructions: ['You are a helpful assistant.', options.instructions]
.filter(Boolean)
.join('\n\n'),
}),
5. Stop expecting activity messages in run input
AG-UI activity messages are display material for the client, and the spec says a client must not send them back to the agent.
Hashbrown now strips them from input.messages before each run.
Hashbrown's own providers already ignored them, so this only matters if your server read role: 'activity' messages from the run input.
Keep that state on the server, or send it through AG-UI state.
6. Handle role: 'activity' where you switch on a message role
Most code that switches on message.role still type checks.
What breaks is an exhaustive check that assigns the leftover role to never, or an else branch that assumes the message is from the assistant.
Add an 'activity' case, even if you ignore it, so an activity is never mistaken for a user or assistant message.
7. Expect id on user and assistant messages
On input, id is optional, and Hashbrown keeps one you supply.
Code that builds messages by hand needs no change.
Code that compares whole message objects, for example tests using toEqual, now sees the extra field.
Match on the fields you care about.
8. Check your toolchain
Core v0.8 uses package imports entries.
Your bundler and test runner need to support package imports.
Node 20 and later, Vite, webpack 5, Next.js, the Angular CLI, Jest 29.4 and later, and Vitest do.
Older toolchains, including webpack 4 and Jest before 29.4, need to be upgraded before they can load core v0.8.
9. Check how you handle an incomplete answer
OpenAI and Azure now complete a truncated or filtered answer with a finish reason instead of raising RUN_ERROR.
If your app used error to detect an incomplete response, check lastRunFinishReason for length or content_filter as well.
The v0.8 guide for React and for Angular walk through the migration and include a prompt for your AI coding assistant.
The Run's Return Value
An AG-UI agent can attach a return value to the event that ends a run. It is separate from the messages and from shared state. A LangGraph graph returns its final state. A workflow returns a summary record. A judge returns a verdict.
In v0.8, read that value from lastRunResult:
const chat = useChat({ system, transport });
const verdict = chat.lastRunResult;
readonly chat = chatResource({ system, transport });
readonly verdict = computed(() => this.chat.lastRunResult());
A few rules:
- It is the return value of the last run that completed, as any JSON value, or
undefinedwhen the agent attached none. - It is cleared when the next run starts, so a value always belongs to the messages on screen.
- Interrupted, cancelled and failed runs never set it.
It is on useChat, useCompletion, the structured and UI hooks, and every Angular chat and completion resource.
Hashbrown's own provider adapters do not produce one.
Use it with an agent you run yourself.
Token Usage and Finish Reasons
An answer that hits its token limit needs different treatment from one that finishes normally. Before this release, the providers did not expose that distinction consistently, and token counts were not available through the chat APIs.
All six generative provider adapters now report a normalized finish reason, along with token usage when the provider supplies it.
Read them next to lastRunResult:
const chat = useChat({ system, transport });
const usage = chat.lastRunUsage;
const finishReason = chat.lastRunFinishReason;
readonly chat = chatResource({ system, transport });
readonly usage = computed(() => this.chat.lastRunUsage());
readonly finishReason = computed(() => this.chat.lastRunFinishReason());
lastRunFinishReason is one of stop, length, tool_calls, content_filter, refusal, or other.
For example, a length reason tells us that the output token limit or context window cut the answer off.
A content_filter reason tells us that a safety filter or guardrail blocked the prompt or stopped the answer.
There is a behavior change to be aware of here.
OpenAI and Azure previously raised RUN_ERROR for an incomplete response.
Now, as with the other providers, the run completes with the partial answer and its finish reason.
Check that reason if your application needs to explain a truncated or filtered answer to the user.
lastRunUsage is an array of TokenUsage entries, with a provider, model, and the counts that provider supplied.
Input and output tokens are totals; cache and reasoning token counts are parts of those totals, so do not add them again.
Usage travels in AG-UI's standard RUN_FINISHED.usage field.
The normalized finish reason travels in metadata.hashbrown.finishReason, with the provider's original reason kept in its own metadata.
Both values are cleared at the start of the next run and are never set by interrupted, cancelled, or failed runs. They describe the last completed run, not the total for a conversation or a turn with several tool-call runs. The APIs are available on the React chat, completion, structured and UI hooks, the matching Angular resources, and the core runtime.
See Token Usage and Finish Reason for React or Angular for the complete mapping.
Cancelled Runs
AG-UI 1.0 lets a server end a run with outcome: { type: 'cancelled' }.
The run was stopped before it completed, and it did not fail.
Previously, Hashbrown treated this as a malformed interrupt and raised an error.
In v0.8, a cancelled run settles the same way a local stop does.
The partial answer is rolled back, nothing is retried, and error stays empty.
The next message on the thread starts an ordinary new run.
Provider Fixes
We have also been testing the adapters against recorded responses from the real provider APIs. This lets us exercise the same text, tool, reasoning, and structured-output flows without requiring a live model key for every test run.
Several correctness fixes came out of that work:
- Anthropic: interleaved thinking can begin after a text or tool-use block without crashing the run.
- Bedrock: parallel tool results are kept in one user turn, as Converse requires. Server tool use is no longer mistaken for a client tool call.
- Google: a signature-only part no longer overwrites a function call's own thought signature.
- OpenAI, Azure, and Ollama: a client tool's
erroris sent back to the model as an error result instead of being dropped. - Azure:
reasoning: trueexplicitly enables reasoning summaries for deployments whose names do not identify the model. - TypeSafe: a score's value is its most probable defined level, while the weighted score remains in
meta. Request size limits account for Jev's long-context option, and errors preserve the vendor's error type.
Core's public declarations also stop exposing QuickJS types, so applications do not need those implementation types to consume the package.
Try It in atc
Our new flagship example is atc, a live flight-tracking application built in both React and Angular. It replaces the invoicing example. The two applications share component contracts, browser tools, and a state model, while each uses its framework's Hashbrown APIs.
You can ask a question about an aircraft or an airport, or type a request such as "show me arrivals at PDX" to filter the map. A decision routes the input, and another can narrow the cards offered to the chat. The example also includes airport and airspace overlays, filter starters and refinements, and a developer drawer for inspecting routing, card selection, streamed UI, and tool calls.
The atc post includes the demo video, and the source shows the full implementation. Use it alongside the migration guides when you want to see how these APIs fit together in an application.
Conclusion
For me, the useful part of this release is how the pieces work together. A decision routes the input and selects the available components. A chat generates the answer. Activities and tool steps show the work along the way, and the finish reason tells our application how the run ended.
I am thankful to everyone who filed an issue, tried a preview, or shared where they got stuck. Your feedback helps us decide what to build and what to fix.
The migration guides for React and Angular cover the upgrade. Have questions or hit a snag? Open an issue on GitHub and let me know!
