ai-ml

AG-UI 1.0 Migration: What Breaks, Tested (2026)

October 1, 2026

AG-UI 1.0 Migration: What Breaks, Tested (2026)

TL;DR: The AG-UI 1.0 migration is mostly safe, but not in every direction. In my tests on October 1, 2026, a 1.0.1 client accepted every 0.x-style stream I sent it, translating old shapes and logging a warning for each one.1

The other direction failed once. When a 1.0-style stream returned a tool result with an image part, the 0.0.59 client threw a ZodError and the run produced no messages. A short protocolVersion check on the agent fixes it.

Two quieter changes need a look before you upgrade. Custom fields you put directly on events are now removed, and old THINKING_* streams now add a reasoning message to your chat history.

AG-UI 1.0 migration: the short answer

AG-UI is "an open, lightweight, event-based protocol that standardizes how AI agents connect to user-facing applications." It is the agent-to-user layer, next to MCP for tools and A2A for agent-to-agent work.2

CopilotKit announced AG-UI 1.0 on September 30, 2026, saying "AG-UI 1.0 is backwards compatible, so your existing agents and apps keep working."3 The official migration guide says "A 0.x agent keeps working against a 1.0 client" and "A 1.0 agent keeps working against a 0.x client."4

I tested that claim against the real npm packages instead of repeating it. The table below is the result.

Terminal output of the AG-UI compatibility matrix: six scripted streams replayed into @ag-ui/client 0.0.59 and 1.0.1, with all 1.0.1 runs ok and the 0.0.59 multimodal-tool-result run marked FAIL

Real output of the test harness, run on October 1, 2026 with Node 22.22.2. Each row is one stream replayed into one client version.1

What you'll learn

  • What AG-UI 1.0 changed and when the packages shipped
  • How I tested AG-UI backwards compatibility with a replay harness
  • What a 1.0 client does with THINKING to REASONING events, nulls and unknown fields
  • Why AG-UI multimodal tool results can crash a 0.x client, and the protocolVersion fix
  • The TypeScript and Python breaking changes, including @ag-ui/core/schemas
  • An upgrade checklist and the limits of these tests

What changed in AG-UI 1.0?

AG-UI 1.0 is the first release with a written specification behind it, and every SDK's protocol types are now generated from its schema.4 The announcement, by Anmol Baranwal and Eli Berman, lists five headline features: subagent support, metadata, multimodal tool results, human-in-the-loop interrupts and token usage.3

CopilotKit says the protocol is "adopted by Google, Microsoft, Amazon and Oracle, and supported by most agent frameworks, including LangChain, Mastra and Anthropic's Claude Managed Agents." That is the vendor's own claim; I did not test any of those integrations.3

The packages landed before the announcement. @ag-ui/core, @ag-ui/client and @ag-ui/encoder 1.0.0 were published to npm on September 17, 2026, and 1.0.1 on September 29. The Python package ag-ui-protocol 1.0.0 was also published on September 17.5

The spec's changelog lists the rules that matter most for a migration:6

  • "optional fields are omitted, never null"
  • Unknown events are dropped and unknown members are stripped, "warning as it goes; a malformed known value is fatal"
  • "The 0.x THINKING_* events are retired in favour of the reasoning family"
  • A tool "can return a document, an image or a search hit without encoding it into a string"

The old shapes are translated for now, not forever. The migration guide says each compatibility shim expires, provisionally twelve months after it was written, "after which the retired shapes stop working entirely."4

How I tested AG-UI backwards compatibility

I installed two clients side by side: @ag-ui/client 0.0.59, the last 0.x release (published August 27, 2026), and 1.0.1.5 Then I wrote a fake agent that replays a scripted server-sent event stream into the real HttpAgent.

mkdir v0 v1 harness
(cd v0 && npm init -y && npm i @ag-ui/client@0.0.59 @ag-ui/core@0.0.59 @ag-ui/encoder@0.0.59)
(cd v1 && npm init -y && npm i @ag-ui/client@1.0.1 @ag-ui/core@1.0.1 @ag-ui/encoder@1.0.1 zod)

Each stream lives in harness/streams.json. Here are two of the six: an old agent that still emits THINKING_* events, and one that puts custom fields straight on its events.

{
  "legacy-thinking": [
    {"type":"RUN_STARTED","threadId":"t1","runId":"r1"},
    {"type":"THINKING_START"},
    {"type":"THINKING_TEXT_MESSAGE_START"},
    {"type":"THINKING_TEXT_MESSAGE_CONTENT","delta":"Checking the weather tool..."},
    {"type":"THINKING_TEXT_MESSAGE_END"},
    {"type":"THINKING_END"},
    {"type":"TEXT_MESSAGE_START","messageId":"m1","role":"assistant"},
    {"type":"TEXT_MESSAGE_CONTENT","messageId":"m1","delta":"Sunny."},
    {"type":"TEXT_MESSAGE_END","messageId":"m1"},
    {"type":"RUN_FINISHED","threadId":"t1","runId":"r1"}
  ],
  "unknown-prop": [
    {"type":"RUN_STARTED","threadId":"t1","runId":"r1"},
    {"type":"TEXT_MESSAGE_START","messageId":"m1","role":"assistant","traceId":"abc-123"},
    {"type":"TEXT_MESSAGE_CONTENT","messageId":"m1","delta":"hi","costUsd":0.0004},
    {"type":"TEXT_MESSAGE_END","messageId":"m1"},
    {"type":"RUN_FINISHED","threadId":"t1","runId":"r1"}
  ]
}

The other four streams are legacy-nulls (rawEvent, parentMessageId and result set to null), metadata (the same custom fields moved under metadata), modern-reasoning (REASONING_* events) and multimodal-tool-result (a TOOL_CALL_RESULT whose content is a text part plus an image part).

The harness starts the fake agent, runs the client against it and prints what the client ended up with:

// compat.mjs: replay a scripted SSE stream into an @ag-ui/client install.
// Usage: node compat.mjs <client-folder> <stream-name>
import http from "node:http";
import fs from "node:fs";
import { createRequire } from "node:module";

const [folder, name] = process.argv.slice(2);
const streams = JSON.parse(fs.readFileSync(new URL("./streams.json", import.meta.url)));
const require = createRequire(new URL(`../${folder}/package.json`, import.meta.url));
const { HttpAgent } = require("@ag-ui/client");

// 1. A fake agent that replays one stream and remembers what the client sent.
let lastInput = null;
const server = http.createServer(async (req, res) => {
  let body = "";
  for await (const chunk of req) body += chunk;
  lastInput = JSON.parse(body);
  res.writeHead(200, { "content-type": "text/event-stream" });
  for (const event of streams[name]) res.write(`data: ${JSON.stringify(event)}\n\n`);
  res.end();
});
await new Promise((resolve) => server.listen(0, resolve));

// 2. Run the real client against it, counting warnings instead of printing them.
const warnings = [];
console.warn = (...args) => warnings.push(args.join(" "));
const agent = new HttpAgent({ url: `http://localhost:${server.address().port}`, threadId: "t1" });
let status = "ok";
try {
  await agent.runAgent({ runId: "r1" });
} catch (err) {
  const issue = err.issues?.[0];
  status = issue ? `FAIL: ${err.name} at ${issue.path.join(".")}: ${issue.message}` : `FAIL: ${err.message}`;
}
server.close();

console.log({
  client: require("@ag-ui/client/package.json").version,
  stream: name,
  status,
  sentProtocolVersion: lastInput.protocolVersion ?? "(absent)",
  warnings: warnings.length,
  messages: agent.messages.map((m) => `${m.role}: ${JSON.stringify(m.content)}`),
});

Run it as node harness/compat.mjs v1 legacy-thinking, and so on for each stream and client folder.

What does a 1.0 client do with an old 0.x agent?

It keeps working. All six streams completed with the 1.0.1 client, and every retired shape was translated with a console warning instead of an error.1

Stream from the agent0.0.59 client1.0.1 client
THINKING_* eventsok, no reasoning message keptok, 5 warnings, converted to REASONING_*
Optional fields set to nullok, nulls passed throughok, 3 warnings, nulls removed
Custom fields on eventsok, fields keptok, 2 warnings, fields removed
Custom fields under metadataokok, kept
REASONING_* eventsokok
Image part in a tool resultZodError, run failsok

One detail in the output: 1.0.1 also sent protocolVersion: "1.0" on its run input, while 0.0.59 sent no version at all. That one field drives the fix later in this post.

THINKING to REASONING events: your history changes

The 1.0.1 client converted each of the five THINKING_* events to its REASONING_* equivalent. The first warning read: "[ag-ui][compat] Converting deprecated THINKING_START to REASONING_START."

The side effect is in agent.messages. With 0.0.59, the thinking text was not kept, so the history held only assistant: "Sunny.". With 1.0.1, the history also held reasoning: "Checking the weather tool...".

That reasoning message then goes back to the agent. On a second turn, the 0.0.59 client sent message roles assistant, user, while the 1.0.1 client sent reasoning, assistant, user.1

The Python model in ag-ui-protocol 0.1.22 accepted that input. If your agent maps the history onto a model provider's message format, though, check that it handles a reasoning role before you upgrade the frontend.

AG-UI unknown properties are now stripped

This is the change I would check first, because it fails quietly. My stream put traceId and costUsd directly on text message events. The 0.0.59 client passed them to subscribers; 1.0.1 removed them.

The warning was: "[ag-ui][enforce] Removed unrecognised material at '/traceId' on TEXT_MESSAGE_START." The run still succeeded, so a UI that read event.traceId would simply get undefined.

The migration guide says "The sanctioned channel for extra data is metadata," which "is never stripped." The same rule applies to unknown keys you send on the run input; the guide points to forwardedProps for that.4 When I moved the same two fields under metadata, both clients kept them, with no warnings.

Nulls become absent fields

The 1.0.1 client turned rawEvent: null, parentMessageId: null and result: null into absent fields, with one warning each.1 The 1.0.1 encoder also leaves nulls out on the way out: encoding RUN_FINISHED with result: null produced no result key, while the 0.0.59 encoder wrote "result":null.

If your frontend checks event.result === null, switch it to event.result == null or !("result" in event).

Why can AG-UI multimodal tool results crash a 0.x client?

Because the 0.0.59 client still expects tool result content to be a string. When my stream returned a text part and an image part, the 0.0.59 client failed with ZodError at content: Expected string, received array, and no messages were kept.1

It was the only stream in my tests where a 1.0-shaped agent broke an older client. Multimodal tool results are one of 1.0's headline features, so it is an easy case to hit.3

The migration guide's list of 1.0 additions that a 0.x client can ignore names RUN_FINISHED.outcome, the subagent events, protocolVersion and activity events. It does not mention tool result content.4

The versioning spec covers it. Both version fields are optional "because absence means something: a peer from before the protocol carried a version," and "An implementation of this version MUST send its declaration."7

It also says "A party that knows its peer is older MAY translate the stream into a shape the peer understands," and "A lossy downgrade MUST emit a warning that names what was lost and why."7

The protocolVersion fix for AG-UI multimodal tool results

Check protocolVersion on the incoming run input. If it is missing, flatten the tool result with contentToText from @ag-ui/core before you send it, and log what you dropped. Save this as v1/agent.mjs so it uses the 1.0.1 packages.

// agent.mjs: a minimal AG-UI 1.0 agent that stays safe for pre-1.0 clients.
import http from "node:http";
import { EventEncoder } from "@ag-ui/encoder";
import { EventType, PROTOCOL_VERSION, contentToText, contentHasMedia } from "@ag-ui/core";

const chartResult = [
  { type: "text", text: "Chart attached" },
  { type: "image", source: { type: "url", value: "https://example.com/c.png", mimeType: "image/png" } },
];

export function startAgent(port = 0) {
  const server = http.createServer(async (req, res) => {
    let body = "";
    for await (const chunk of req) body += chunk;
    const input = JSON.parse(body);

    // A 1.0 client declares protocolVersion on every RunAgentInput. Absent = pre-1.0 peer.
    const peerSpeaks1 = typeof input.protocolVersion === "string";

    const enc = new EventEncoder({ accept: req.headers.accept });
    res.writeHead(200, { "content-type": enc.getContentType() });
    const send = (e) => res.write(enc.encode(e));

    send({ type: EventType.RUN_STARTED, threadId: input.threadId, runId: input.runId, protocolVersion: PROTOCOL_VERSION });
    send({ type: EventType.TOOL_CALL_START, toolCallId: "c1", toolCallName: "render_chart" });
    send({ type: EventType.TOOL_CALL_END, toolCallId: "c1" });

    let content = chartResult;
    if (!peerSpeaks1 && contentHasMedia(content)) {
      // Spec: a lossy downgrade MUST warn, naming what was lost and why.
      const dropped = content.filter((part) => part.type !== "text").map((part) => part.type);
      console.warn(`[agent] client sent no protocolVersion (pre-1.0): dropped ${dropped.join(", ")} part(s) from tool result c1`);
      content = contentToText(content);
    }
    send({ type: EventType.TOOL_CALL_RESULT, messageId: "tm1", toolCallId: "c1", role: "tool", content });
    send({ type: EventType.RUN_FINISHED, threadId: input.threadId, runId: input.runId });
    res.end();
  });
  return new Promise((resolve) => server.listen(port, () => resolve(server)));
}

@ag-ui/encoder 1.0.1 will not do this for you: its published code never calls contentToText, so a hand-written agent has to.1

The agent also sets protocolVersion on RUN_STARTED. The Python SDK's own notes say it "does not set it for you," so pass PROTOCOL_VERSION yourself in either language.8

To test it, I pointed each client at this agent:

// run-fixed.mjs, in the harness folder. Usage: node run-fixed.mjs <v0|v1>
import { createRequire } from "node:module";
import { startAgent } from "../v1/agent.mjs";
const ver = process.argv[2];
const require = createRequire(new URL(`../${ver}/package.json`, import.meta.url));
const { HttpAgent } = require("@ag-ui/client");
const server = await startAgent();
const agent = new HttpAgent({ url: `http://localhost:${server.address().port}/`, threadId: "t1" });
try {
  await agent.runAgent({ runId: "r1" });
  const tool = agent.messages.find((m) => m.role === "tool");
  console.log(`${ver} client -> tool message content:`, JSON.stringify(tool.content));
} catch (e) {
  console.log(`${ver} client -> FAILED:`, e.message.split("\n")[0]);
}
server.close();

The 0.0.59 client now got the plain string "Chart attached", and the agent logged that it dropped the image part. The 1.0.1 client still got the full text-plus-image array.1

Terminal output of run-fixed.mjs: the agent warns it dropped an image part for a client with no protocolVersion, the 0.0.59 client receives the string Chart attached, and the 1.0.1 client receives the text and image parts

Real output of the fixed agent with both client versions, October 1, 2026.1

TypeScript breaking changes: @ag-ui/core/schemas and friends

The client side of the wire is forgiving. The package APIs are not, so expect compile and import errors in code that used the validators or old type names.

Validators moved to @ag-ui/core/schemas

The root of @ag-ui/core 1.0.1 exports 12 names, down from 102 in 0.0.59. The Zod schemas moved to a subpath.41 In an ES module, the old import fails before any code runs:

SyntaxError: The requested module '@ag-ui/core' does not provide an export named 'EventSchemas'

The fix is one line:

import { EventSchemas } from "@ag-ui/core/schemas";

const ok = EventSchemas.safeParse({ type: "TEXT_MESSAGE_CONTENT", messageId: "m1", delta: "hi" });
const old = EventSchemas.safeParse({ type: "THINKING_START" });
console.log("valid event:", ok.success, "| THINKING_START:", old.success);
// valid event: true | THINKING_START: false

Zod also moved. In @ag-ui/core 0.0.59 it was a regular dependency (^3.22.4); in 1.0.1 it is a peer dependency (^3.25.18 || ^4.0.0).1 The guide says importing the validators "requires zod," so add zod yourself if you use @ag-ui/core/schemas. @ag-ui/client still depends on zod directly.4

Renamed and retired names

The EventType enum went from 36 members to 31: the five THINKING_* values are gone.1 The guide lists the renames, including:4

  • SubAgentInfo → SubagentInfo, and multiAgent.subAgents → subagents
  • InputContent → ContentPart, TextInputContent → TextPart, ImageInputContent → ImagePart
  • AbstractAgent.maxVersion → maxProtocolVersion (the old name is deprecated)
  • BinaryInputContent retired in favor of image, audio, video and document parts

Python breaking change: typed JSON Patch operations

In ag-ui-protocol 1.0.0, entries in StateDeltaEvent.delta are typed operation models, not dicts.4 I built the same event with 0.1.22 and 1.0.0:

from ag_ui.core import StateDeltaEvent, EventType

event = StateDeltaEvent(type=EventType.STATE_DELTA, delta=[{"op": "replace", "path": "/temp", "value": 21}])
op = event.delta[0]
print(type(op).__name__)
for label, read in [("op.path", lambda: op.path), ('op["path"]', lambda: op["path"])]:
    try:
        print(label, "->", read())
    except Exception as err:
        print(label, "->", type(err).__name__, err)

On 0.1.22, op was a dict and op.path raised AttributeError. On 1.0.0, op was a ReplaceOperation, and op["path"] raised TypeError: 'ReplaceOperation' object is not subscriptable.1 Search your agent for delta[ and patch[ before upgrading. If a library needs plain dicts, the guide shows converting each entry with model_dump(by_alias=True).4

Two things did not break. The 0.1.22 RunAgentInput accepted the new protocolVersion field (and dropped it), and its encoder already left out unset optional fields. The guide says absent-field handling shipped in Python 0.1.20.41

AG-UI 1.0 migration checklist

  1. Upgrade the frontend client first. In my tests, a 1.0.1 client handled every 0.x stream, with warnings.
  2. Move custom event fields under metadata. The 1.0 client removes unknown fields.
  3. Check your history handling for reasoning messages if your agent still emits THINKING_* events.
  4. Replace === null checks on optional fields with absence checks.
  5. Change validator imports to @ag-ui/core/schemas and add zod as your own dependency if you use them.
  6. In Python, switch patch["path"] to patch.path.
  7. Before an agent returns image, audio, video or document parts, check protocolVersion and flatten for clients that do not send it.
  8. Send protocolVersion on RUN_STARTED yourself.

If your agent runs behind a human approval step, the new interrupt outcome is worth a look too. My human-in-the-loop guide for the Claude Agent SDK covers the approval pattern on the agent side.

Limits of these tests

These are protocol-level tests with a scripted fake agent, not a real model. I tested one 0.x client (0.0.59, the last 0.x release) and one 1.0 client (1.0.1). Older 0.x clients may behave differently.

I did not test the .NET SDK, framework integrations, subagent events or interrupts across versions. The adoption claims in this post are CopilotKit's, not mine.

Bottom line

The AG-UI 1.0 migration is easiest frontend first. A 1.0.1 client translated every old stream I sent it, and the warnings show you exactly what to clean up.

Two risks remain. Custom fields that are not under metadata disappear without an error, and a 1.0 agent that sends media parts can break a 0.x client. Move the fields, and add the protocolVersion check before you ship multimodal tool results.

Footnotes

  1. Author's measurement, October 1, 2026: Node 22.22.2 and Python 3.11.15; @ag-ui/client, @ag-ui/core and @ag-ui/encoder 0.0.59 and 1.0.1 installed from npm; ag-ui-protocol 0.1.22 and 1.0.0 installed from PyPI into separate virtual environments. Export counts, the EventType member count and the Zod dependency entries were read from the installed packages. All code in this post was run as shown, after extracting it from the finished post into a fresh folder. The second-turn history check and the null-encoding check used two small extra scripts in the same setup. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19

  2. AG-UI Overview — Agent User Interaction Protocol docs, fetched 2026-10-01. ↩ ↩2

  3. Introducing AG-UI 1.0: a stable spec for connecting any agent to any application — CopilotKit, by Anmol Baranwal and Eli Berman, published September 30, 2026, fetched 2026-10-01. ↩ ↩2 ↩3 ↩4

  4. Migrating to 1.0 — Agent User Interaction Protocol docs, fetched 2026-10-01; quotations checked against the page source, docs/migrating-to-1-0.mdx on the ag-ui-protocol/ag-ui GitHub main branch, read the same day. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13

  5. Publish dates from the npm registry (npm view @ag-ui/core time, and the same for @ag-ui/client and @ag-ui/encoder) and from PyPI's JSON API for ag-ui-protocol, checked 2026-10-01. ↩ ↩2

  6. Key Changes — AG-UI 1.0 spec, fetched 2026-10-01; quotations checked against docs/spec/1.0/changelog.mdx on GitHub main. ↩ ↩2

  7. Versioning and Compatibility — AG-UI 1.0 spec, fetched 2026-10-01; quotations checked against docs/spec/1.0/basic/versioning.mdx on GitHub main. ↩ ↩2 ↩3

  8. ag_ui/core/version.py in ag-ui-protocol 1.0.0, read after installing from PyPI on 2026-10-01. ↩

Frequently Asked Questions

Mostly. In my tests, a 1.0.1 client accepted every 0.x-style stream with warnings. A 0.0.59 client failed on a 1.0 multimodal tool result, so 1.0 agents should check protocolVersion before sending media parts.1