Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/agentkit-agui-wire-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@agent-native/core": patch
Comment thread
builder-io-integration[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Release the Core wire change with an appropriate version

The changeset still classifies @agent-native/core as a patch even though the Core adapter now requires protocol v2 and emits/consumes the incompatible AG-UI wire format. Existing Core consumers can fail negotiation or decoding against v1 peers, so this wire-incompatible runtime change should use the appropriate pre-1.0 minor/breaking version or provide a compatibility release.

Fix in Builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 614404f. The changeset now marks both @agent-native/core and @agent-native/agentkit as pre-1.0 minor releases, matching the explicitly documented protocol-v2 wire break and coordinated migration requirement. The no-major-changeset guard and all 72 repository guards pass.

"@agent-native/agentkit": minor
---

**Breaking (pre-1.0):** AgentKit protocol v2 intentionally rejects v1-only
peers because the AG-UI envelope is not wire-compatible with the original
Builder envelope. Upgrade the AgentKit client and server together, then rerun
transport conformance before deploying a custom adapter. The deprecated
`resolveApproval` API remains only as a source-compatibility bridge after both
peers are on v2.

Carry AgentKit runs over the AG-UI wire format instead of a Builder-only
envelope. Overlapping events map onto native AG-UI event types, and the
Builder-specific events travel as a versioned typed extension profile over
`CUSTOM`, so a stock AG-UI client can read the stream while AgentKit consumers
still receive fully typed domain events. Sequencing, replay cursors, and profile
version negotiation are defined as explicit extensions because AG-UI specifies
none of them. Approvals now use AG-UI's interrupt model: the Core transport
exposes `resumeRun` with `resume` entries in place of `resolveApproval`. An
approval interrupt terminally closes its protocol run, and `resumeRun` returns
the distinct replacement run that carries the resolution and continued work.
15 changes: 15 additions & 0 deletions packages/agentkit/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,21 @@ distinguishes available, degraded, unavailable, unsupported, and omitted unknown
state. During the pre-1.0 period, consumers should review minor-release notes
before upgrading a custom adapter.

Protocol v2 is one such explicitly breaking pre-1.0 minor. Its AG-UI envelope
is not wire-compatible with v1, so clients and servers must upgrade together
and custom adapters must rerun conformance before deployment. V2-only peers
reject v1 at negotiation instead of guessing a fallback; the deprecated
`resolveApproval` API is only a source-compatibility bridge after both peers
have upgraded.

Approvals follow AG-UI's terminal interrupt lifecycle. The request closes the
interrupted protocol run, and `resumeRun()` returns a distinct replacement run
that contains the resolution and continued events.

Conformance enforces `resumeRun()` for transports that advertise protocol v2.
Unversioned compatibility transports may temporarily use the deprecated
`resolveApproval()` bridge while custom adapters migrate.

Conformance follows negotiated capabilities. A transport that declares
`resumableRuns` must prove cursor replay. A transport that declares
`durableThreadSnapshots` must return a value accepted by
Expand Down
18 changes: 18 additions & 0 deletions packages/agentkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,13 @@ where `true` means available, `false` means unsupported, and omission means
unknown. Breaking required wire changes add a protocol version instead of
guessing a fallback.

Protocol v2 is an explicitly breaking pre-1.0 minor: its AG-UI envelope is not
wire-compatible with v1. Upgrade AgentKit clients and servers together, then
rerun transport conformance before deploying a custom adapter. V2-only peers
reject v1 rather than silently decoding it, and the deprecated
`resolveApproval` API is only a source-compatibility bridge once both peers use
v2.

New transports implement `discoverCapabilities(input)` and return an
`AgentCapabilitiesDiscovery` descriptor for every requested capability.
`degraded` and `unavailable` descriptors carry a typed `capability_unavailable`
Expand All @@ -787,6 +794,17 @@ apps pin it through Core's dependency rather than resolving a `latest` tag. Read
release notes for minor updates, and run transport conformance after upgrading a
custom adapter.

Approval requests are terminal interrupts on the wire. The interrupted run
closes after the request; `resumeRun()` returns a distinct replacement run id
whose stream begins with the approval resolution and carries the continued
work. Consumers must subscribe to that returned run instead of waiting for more
events on the interrupted run.

Transport conformance requires `resumeRun()` when a transport advertises
protocol v2. An unversioned compatibility transport may temporarily advertise
approvals through the deprecated `resolveApproval()` bridge, allowing custom
adapters to migrate without weakening the v2 lifecycle contract.

## Migrate an existing Core chat surface

Keep the Core runtime and replace the presentation boundary in one pass:
Expand Down
1 change: 1 addition & 0 deletions packages/agentkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"@ag-ui/core": "0.0.59",
"@agent-native/toolkit": "workspace:*",
"@tabler/icons-react": "catalog:",
"react-markdown": "^10.1.0",
Expand Down
143 changes: 118 additions & 25 deletions packages/agentkit/src/adapters/http.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,29 @@ import { describe, expect, it, vi } from "vitest";
import type { AgentEvent, AgentTransport } from "../protocol/index.js";
import {
AgentKitProtocolError,
AGENTKIT_PROFILE_HEADER,
AGENTKIT_PROFILE_ID,
createAgentKitProtocolVersionOffer,
createAgentProtocolEnvelope,
encodeAgentEvent,
} from "../protocol/index.js";
import {
AgentKitHttpError,
createAgentKitHttpHandler,
createAgentKitHttpTransport,
} from "./http.js";

const sseEvent = (sequence: number): AgentEvent => ({
id: `event-${sequence}`,
threadId: "thread-1",
runId: "run-1",
sequence,
occurredAt: "2026-08-29T00:00:00.000Z",
type: "message.delta",
messageId: "message-1",
text: String(sequence),
});

describe("AgentKit HTTP adapter", () => {
it("round-trips commands and resumable SSE through Fetch primitives", async () => {
const events: AgentEvent[] = [
Expand Down Expand Up @@ -128,6 +142,47 @@ describe("AgentKit HTTP adapter", () => {
);
});

it("keeps the deprecated approval route as a resume bridge", async () => {
const resumeRun = vi.fn(async () => ({ runId: "run-1" }));
const handler = createAgentKitHttpHandler({
transport: {
capabilities: { approvals: true },
async startRun() {
return { runId: "run-1" };
},
async *subscribeToRun() {},
async cancelRun() {},
resumeRun,
},
});
const transport = createAgentKitHttpTransport({
baseUrl: "https://agentkit.test/agentkit",
fetch: (input, init) => handler(new Request(input, init)),
});

await transport.resolveApproval?.({
threadId: "thread-1",
runId: "run-1",
approvalId: "approval-1",
response: { decision: "deny" },
});

expect(resumeRun).toHaveBeenCalledWith(
{
threadId: "thread-1",
runId: "run-1",
resume: [
{
interruptId: "approval-1",
status: "resolved",
payload: { decision: "deny" },
},
],
},
expect.objectContaining({ signal: expect.any(AbortSignal) }),
);
});

it("rejects malformed commands and route identity mismatches", async () => {
const queueMessage = vi.fn();
const handler = createAgentKitHttpHandler({
Expand Down Expand Up @@ -356,7 +411,10 @@ describe("AgentKit HTTP adapter", () => {
const fetcher = vi.fn(
async () =>
new Response("not an event stream", {
headers: { "content-type": "application/json" },
headers: {
"content-type": "application/json",
[AGENTKIT_PROFILE_HEADER]: AGENTKIT_PROFILE_ID,
},
}),
);
const transport = createAgentKitHttpTransport({
Expand Down Expand Up @@ -388,23 +446,45 @@ describe("AgentKit HTTP adapter", () => {
});
});

it("rejects non-event envelopes inside an SSE response", async () => {
it("rejects a frame that is not a valid AG-UI event", async () => {
const transport = createAgentKitHttpTransport({
baseUrl: "https://agentkit.test/agentkit",
createCorrelationId: () => "request-42",
fetch: async () =>
new Response(`data: ${JSON.stringify({ runId: "run-1" })}\n\n`, {
headers: {
"content-type": "text/event-stream; charset=utf-8",
"x-agentkit-correlation-id": "request-42",
[AGENTKIT_PROFILE_HEADER]: AGENTKIT_PROFILE_ID,
},
}),
});

await expect(
transport
.subscribeToRun({ threadId: "thread-1", runId: "run-1" })
[Symbol.asyncIterator]()
.next(),
).rejects.toThrow("is not a valid AG-UI event");
});

it("skips a frame emitted by a producer outside the profile", async () => {
const foreign = JSON.stringify({
type: "CUSTOM",
name: "someone-else/thing",
value: {},
});
const transport = createAgentKitHttpTransport({
baseUrl: "https://agentkit.test/agentkit",
fetch: async () =>
new Response(
`data: ${JSON.stringify(
createAgentProtocolEnvelope(
"response",
{ runId: "run-1" },
"request-42",
),
`id: opaque-foreign\ndata: ${foreign}\n\nid: 1\ndata: ${JSON.stringify(
encodeAgentEvent(sseEvent(1)),
)}\n\n`,
{
headers: {
"content-type": "text/event-stream; charset=utf-8",
"x-agentkit-correlation-id": "request-42",
"content-type": "text/event-stream",
[AGENTKIT_PROFILE_HEADER]: AGENTKIT_PROFILE_ID,
},
},
),
Expand All @@ -415,27 +495,22 @@ describe("AgentKit HTTP adapter", () => {
.subscribeToRun({ threadId: "thread-1", runId: "run-1" })
[Symbol.asyncIterator]()
.next(),
).rejects.toThrow("expected an event envelope");
).resolves.toMatchObject({ value: { sequence: 1 } });
});

it("rejects an SSE sequence gap before advancing the replay cursor", async () => {
const event = (sequence: number) =>
createAgentProtocolEnvelope("event", {
id: `event-${sequence}`,
threadId: "thread-1",
runId: "run-1",
sequence,
occurredAt: "2026-08-29T00:00:00.000Z",
type: "message.delta",
messageId: "message-1",
text: String(sequence),
} satisfies AgentEvent);
const event = (sequence: number) => encodeAgentEvent(sseEvent(sequence));
const transport = createAgentKitHttpTransport({
baseUrl: "https://agentkit.test/agentkit",
fetch: async () =>
new Response(
`id: 1\ndata: ${JSON.stringify(event(1))}\n\nid: 3\ndata: ${JSON.stringify(event(3))}\n\n`,
{ headers: { "content-type": "text/event-stream" } },
{
headers: {
"content-type": "text/event-stream",
[AGENTKIT_PROFILE_HEADER]: AGENTKIT_PROFILE_ID,
},
},
),
});

Expand All @@ -450,6 +525,24 @@ describe("AgentKit HTTP adapter", () => {
);
});

it("rejects an event stream that omits the negotiated profile", async () => {
const transport = createAgentKitHttpTransport({
baseUrl: "https://agentkit.test/agentkit",
fetch: async () =>
new Response(
`id: 1\ndata: ${JSON.stringify(encodeAgentEvent(sseEvent(1)))}\n\n`,
{ headers: { "content-type": "text/event-stream" } },
),
});

await expect(
transport
.subscribeToRun({ threadId: "thread-1", runId: "run-1" })
[Symbol.asyncIterator]()
.next(),
).rejects.toMatchObject({ code: "unsupported_profile" });
});

it("rejects a backend sequence gap before writing an invalid SSE cursor", async () => {
const handler = createAgentKitHttpHandler({
transport: {
Expand Down Expand Up @@ -1155,12 +1248,12 @@ describe("AgentKit HTTP adapter", () => {
});

const discovery = await transport.discoverCapabilities?.({
protocol: { protocol: "agentkit", versions: [1] },
protocol: createAgentKitProtocolVersionOffer(),
});

expect(discovery?.protocol).toMatchObject({
status: "compatible",
selectedVersion: 1,
selectedVersion: 2,
});
expect(discovery?.capabilities).toEqual(
expect.arrayContaining([
Expand Down
Loading
Loading