Safeguard
Supply Chain Security•

AG-UI Protocol Explained: How Agents Stream Events and How to Secure It

AG-UI streams typed agent events (text, tool calls, state) to your UI over HTTP and SSE. How the 1.0 spec works and how to validate and render it safely.

Safeguard Security Team
47 min read

AG-UI (the Agent User Interaction protocol) is an open, MIT licensed protocol for connecting an AI agent to the application a person is using. The application POSTs one JSON request, and the agent answers with an ordered stream of typed events: text, tool calls, reasoning, shared state and progress. Secure it by treating every event as untrusted input.

This post goes one level deeper than our earlier pieces on A2A and MCP on the wire, agent output handling, insecure inter-agent communication and computer-use agents. Those covered agent to tool, agent to agent and agent to screen. This one covers the last hop: agent to the person, through the user interface. Everything below is checked against the AG-UI 1.0 specification and its JSON Schema, and the walkthrough was run against the published 1.0 SDKs.

Key takeaways

  • AG-UI is the agent to user layer. MCP connects agents to tools, A2A connects agents to each other, and AG-UI connects an agent to a user-facing application.
  • One RunAgentInput goes in as an HTTP POST; one ordered stream of events comes back. Version 1.0 defines 31 event types in eight families.
  • The default binding is HTTP plus server-sent events. Protobuf is optional. WebSockets are allowed as a custom transport.
  • AG-UI defines no credential. Authentication and authorization are yours to add.
  • State and history round-trip through the client on every run, so the server must never treat them as proof of what happened.
  • Frontend tools and interrupts carry human approval: the agent proposes, the application decides.
  • Validate every event against the schema, render text as text, restrict state paths, gate side effects behind real consent, and bind every thread to an authenticated tenant.

What is the AG-UI protocol?

AG-UI stands for Agent User Interaction. The project's overview page describes it as an open, lightweight, event-based protocol that standardizes how AI agents connect to user-facing applications. The 1.0 specification is more precise: one request in, one ordered stream of typed events out, carrying everything the user sees of the agent.

A classic web request returns data once and ends. An agent run is long, streams partial output, calls tools, changes shared state, may stop for approval and may delegate to a subagent. Before a shared protocol, every framework invented its own events and every front end wrote an adapter. AG-UI gives both sides one vocabulary.

The specification names two roles. A producer emits the event stream: usually the agent, sometimes a proxy or bridge. A consumer reads it: usually a client SDK and the UI on top. Exactly one message flows the other way, RunAgentInput, sent once to open each exchange. There is no mid-run channel from the user back to the agent. If the agent needs an answer, the run ends and the next run carries it. That choice shapes both human in the loop and where your security checks must go.

Who maintains AG-UI and when did it launch?

The overview page says AG-UI "was born from CopilotKit's initial partnership with LangChain and CrewAI." CopilotKit runs the repository's security reporting: the SECURITY.md file in github.com/ag-ui-protocol/ag-ui points reporters to a CopilotKit security address, with a 48 hour acknowledgement target and a 30 day fix or mitigation target. The specification says AG-UI is maintained with three first-party SDKs, TypeScript, Python and .NET. Other language bindings, such as Go, Kotlin, Java, Rust, Dart and Ruby, are listed as community maintained. The repository is MIT licensed.

There is no single launch day in the sources. The dates below come from the project's own pages and the package registries.

DateWhat happenedSource
9 April 2025The docs changelog records "AG-UI repositories are now public" and the initial releasedocs.ag-ui.com, What's New
30 April 2025First packages published: @ag-ui/core, @ag-ui/client and @ag-ui/encoder on npm, and ag-ui-protocol 0.1.4 on PyPInpm and PyPI registry metadata
7 May 2025Creation timestamp of the current ag-ui-protocol/ag-ui GitHub repositoryGitHub repository metadata
17 September 2026Version 1.0.0 published to npm and to PyPInpm and PyPI registry metadata
16 October 2026@ag-ui/client 1.0.3, the latest npm release at the time of writingnpm registry metadata
21 October 2026Most recent dated release tag in the repository at the time of writingGitHub releases

The 7 May 2025 repository timestamp is later than the 9 April date in the changelog, probably because the repository moved to its current organisation, though no source says so. Treat 9 April 2025 as the stated public date and 30 April 2025 as the first installable release.

What changed in 1.0?

Before 1.0, AG-UI defined event shapes, and much of the behaviour lived in the TypeScript client. The 1.0 changelog lists the first major change simply as "the specification exists". Rules about ordering, unknown versus malformed events, warnings and attribution are now normative, written with the usual MUST and SHOULD keywords from BCP 14. The JSON Schema at docs.ag-ui.com/spec/1.0/schema.json is authoritative for structure. The specification text is authoritative for behaviour.

Other 1.0 changes that matter for security reviews: RUN_FINISHED gains an outcome (success, interrupt or cancelled); interrupts and resume entries join the protocol; subagent work is attributed with subagentRunId; reasoning events replace the old THINKING_* events; unknown material is stripped with a warning while malformed known values are fatal; an HTTP plus Protobuf binding is specified; tool results can carry images and documents; and the protocol version travels in band on RunAgentInput.protocolVersion and RUN_STARTED.protocolVersion.

One caution: some older concept pages still describe the pre-1.0 world. The core architecture page refers to 16 event types and lists webhooks as a transport. The 1.0 specification counts 31 event types and defines two standard bindings. This post follows the specification.

Where AG-UI sits next to MCP and A2A

The AG-UI docs present three complementary layers. MCP, which the docs describe as originated by Anthropic, connects agents to tools and data. A2A, which the docs describe as originated by Google, connects agents to other agents. AG-UI connects agents to users through applications. The docs call AG-UI the "kitchen sink" protocol, shaped by what real agent applications needed.

In practice the three stack. A support assistant might take the user's question over AG-UI, call a ticketing system through MCP, delegate a billing question to another team's agent over A2A, and stream the answer and an approval prompt back over AG-UI. Each hop has its own trust boundary. AG-UI's is the one where a human reads the output and clicks the buttons, which makes it the protocol where output handling, rendering safety and consent matter most.

The event types and how state is synchronised

Every AG-UI event is a JSON object with one envelope: a required type (one of 31 EventType values), an optional timestamp that must not be used for ordering (arrival order is the protocol's order), an optional rawEvent carrying the provider-native original, which must not drive behaviour, and optional metadata.

Five identifiers tie events together. threadId names a conversation and is minted by the application. runId must not be reused on a thread. messageId must be unique within the thread. toolCallId ties a call to its result and to any interrupt about it. subagentRunId identifies one subagent invocation. All are opaque strings that a consumer must not parse. One more rule worth copying: absent means absent. An optional field with no value must be omitted, not sent as null.

The eight event families

FamilyEventsPatternConsumed by
Runs and stepsRUN_STARTED, RUN_FINISHED, RUN_ERROR, STEP_STARTED, STEP_FINISHEDlifecyclethe client itself
Text messagesTEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END, TEXT_MESSAGE_CHUNKstreamingthe UI
Tool callsTOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END, TOOL_CALL_CHUNK, TOOL_CALL_RESULTstreamingthe application
ReasoningREASONING_START, REASONING_END, REASONING_MESSAGE_START, REASONING_MESSAGE_CONTENT, REASONING_MESSAGE_END, REASONING_MESSAGE_CHUNK, REASONING_ENCRYPTED_VALUEstreamingthe UI
StateSTATE_SNAPSHOT, STATE_DELTA, MESSAGES_SNAPSHOTsnapshot and deltathe application store
ActivityACTIVITY_SNAPSHOT, ACTIVITY_DELTAsnapshot and deltathe UI
SubagentsSUBAGENT_STARTED, SUBAGENT_FINISHED, SUBAGENT_ERRORlifecyclethe client itself
PassthroughRAW, CUSTOMstandalonethe application, opt-in

Only the run lifecycle is mandatory for a producer. Everything else is a feature the producer emits when it has something to say with it. A consumer, however, must accept all families, even ones it ignores.

Run lifecycle events

A run opens with RUN_STARTED, which may echo the run input, and closes with RUN_FINISHED or RUN_ERROR. In 1.0, RUN_FINISHED may carry an outcome: success (or none), which may list unanswered frontend tool calls in pendingToolCallIds; interrupt, carrying one or more Interrupt objects; or cancelled, for a run stopped on purpose. A late RUN_ERROR after RUN_FINISHED is allowed. STEP_STARTED and STEP_FINISHED mark phases a UI can show as progress.

The schema notes an edge case that matters for safety: an outcome a consumer does not recognise is stripped and read as success. Build your UI so that "success" never implies "a human approved something".

Text messages and the streaming pattern

Text uses an open, content, close discipline: TEXT_MESSAGE_START with a messageId and role, then TEXT_MESSAGE_CONTENT events each carrying a delta, then TEXT_MESSAGE_END. TEXT_MESSAGE_CHUNK is a compact form the consumer expands into the same sequence. Because text arrives in fragments, a boundary can fall in the middle of a Markdown link or an HTML tag, so a renderer that converts each partial buffer must be safe for incomplete input.

Tool call events

Tool calls follow the same pattern, matched by toolCallId: TOOL_CALL_START names the tool, TOOL_CALL_ARGS streams argument text, TOOL_CALL_END closes the call, and TOOL_CALL_RESULT carries a result as a tool message. Arguments are text, conventionally JSON, and the protocol deliberately does not validate them. A consumer must not act on them before TOOL_CALL_END, and closing establishes completeness, not validity.

A closed call may be reopened with the same toolCallId and more arguments appended. The reopening must agree with the original name and parent, but a consumer is not required to detect a disagreement. Close that gap yourself: freeze the name and arguments at the TOOL_CALL_END you act on, and reject later reopening of a call already shown to a user or executed.

Reasoning events

The reasoning family streams what the model is thinking, using spans and messages matched by messageId. REASONING_ENCRYPTED_VALUE carries provider artefacts that a consumer stores and returns on the next run without reading. The 1.0 specification says consumers should treat reasoning and encrypted artefacts with the same confidentiality as the conversation itself. If you log conversations with care but dump raw event streams into a debug bucket, reasoning is the part you will leak first.

State snapshots, state deltas and how state is synchronised

Shared state is the value an agent and an application keep in sync: a plan, a form, a document under construction. In 1.0 the schema leaves State deliberately unconstrained, so it can be any JSON value.

State travels by the snapshot and delta pattern:

  • STATE_SNAPSHOT replaces the consumer's state wholesale. No merging.
  • STATE_DELTA amends the current state with an RFC 6902 JSON Patch: an array of operations such as add, remove, replace, move, copy and test, applied in order.
  • At the start of a run, the baseline is the state in the run input, or the empty object if there was none. After that it is whatever the run's own snapshots and deltas have made of it.

RFC 6902 makes patch application atomic: if any operation fails, the document is left as it was. AG-UI adds that a malformed patch is fatal, an unrecognised op is dropped with a warning, and a well-formed patch that fails to apply (including a failing test) is skipped with a warning. The producer should then resynchronise with a snapshot, which the consumer must adopt.

The loop closes across runs. State persists on a thread, and the next run's input carries it back as the starting value. MESSAGES_SNAPSHOT restates the whole conversation the producer owns: listed messages replace the consumer's copy in place, unseen ones are appended, and unlisted ones are dropped, except client-only activity and reasoning messages.

Here is the security consequence in one sentence: the client holds the state and the history between runs, and sends them back. The specification's state page calls state events "remote writes into application state", and it asks producers not to put secrets in state because it round-trips through the consumer on every run.

Activity, subagent and passthrough events

Activity events carry structured progress that a UI renders as its own widget; a consumer must strip activity messages before sending history back. Subagent events attribute work with subagentRunId and bracket it with SUBAGENT_STARTED, SUBAGENT_FINISHED and SUBAGENT_ERROR, which is how a UI shows which agent said what. RAW passes a provider-native event through, and CUSTOM carries an application's own named event. A consumer must not derive protocol behaviour from RAW and must ignore unrecognised CUSTOM names. Both carry arbitrary JSON into your client.

Transports: server-sent events, Protobuf, WebSockets and webhooks

AG-UI separates what events mean from how they travel. A transport is a binding, and the 1.0 binding contract says every binding must provide four things:

  1. Ordered, complete delivery of a run's events in the order the producer emitted them.
  2. Delivery of the RunAgentInput before any events.
  3. A termination signal a consumer can tell apart from truncation.
  4. An error path for rejected input, outside the stream, before RUN_STARTED.

The same page states that authentication and authorization are properties of the binding and the application, not of the protocol.

HTTP plus server-sent events, the default

Any implementation that speaks HTTP must support this binding. The client POSTs the run input as one JSON object with Accept: text/event-stream. A run that starts answers 200 with Content-Type: text/event-stream, and each SSE data payload is exactly one event as a JSON object. Producers must use LF line endings; consumers must ignore SSE fields other than data and tolerate keep-alive comments.

Input rejected before the run starts, such as malformed JSON, failed validation or refused authentication, is an HTTP error status with no stream. A failure after the stream opens travels as RUN_ERROR, so a consumer must not infer success from 200 alone. There is no resumption: the WHATWG Last-Event-ID reconnect mechanism is not used. A stream that drops without a terminal event is truncated, and the consumer must not report it as successful.

One browser detail matters for authentication. The WHATWG EventSource interface takes a URL and a withCredentials flag; it sends no body and no custom headers, so it cannot send AG-UI's POST. Clients read the SSE format from a fetch response instead, which means your credential is usually an Authorization header or a cookie on a POST, with the usual cross-origin rules.

HTTP plus Protobuf, optional

The second standard binding uses the same POST and negotiates a binary response by media type. Events travel as protobuf frames with a 4-byte length prefix, generated from the same schema, and the project pins parity between implementations with a shared byte corpus. Semantics are identical. For security work, the main difference is that you cannot read the stream with curl and your eyes, so make sure your logging decodes it.

WebSockets

The 1.0 specification defines no WebSocket binding but allows custom transports, naming WebSockets as an example. A custom transport must keep the event model and processing rules, meet the binding contract, and frame JSON events one per frame as SSE does. Some hosts offer one: Amazon Bedrock AgentCore's documentation says an AG-UI runtime serves HTTP and SSE at /invocations and WebSockets at /ws on port 8080, with the platform handling SigV4 or OAuth 2.0 authentication. If you build your own, remember that RFC 6455 uses an origin-based security model. Check the Origin header on the handshake, or a cookie-authenticated socket can be driven from another site.

Webhooks

Older AG-UI concept pages list webhooks as a supported transport. The 1.0 specification does not define a webhook binding, and the binding contract asks for ordered, complete delivery with a clear termination signal, which a set of independent webhook calls does not provide on its own. If you need to notify a system after a run, treat that as an application feature outside the protocol, and sign and authenticate those calls as you would any webhook.

How a run is started

A run starts when the application sends RunAgentInput. The specification describes each field:

FieldRequiredWhat it carriesSecurity note
threadIdYesThe conversation, minted by the applicationBind it to a tenant and user on the server
runIdYesThis run; must not be reused on the threadReject reuse to stop replay
messagesYesThe whole conversation so far, in orderClient-controlled; may include roles you did not expect
protocolVersionNo in schema; consumers implementing 1.0 must declare itThe version the consumer speaksLog it; the spec lets a producer reject only a major version it does not implement
toolsNoFrontend tools the application offers, with parameter schemasYour server decides which tools a user may be offered
contextNoDescription and value pairs meant to be injected into the model's contextTreat as prompt input, not configuration
stateNoThe last agreed stateClient-controlled; never authoritative
forwardedPropsNoApplication-specific JSON passed through untouchedIntermediaries must not alter it; validate it yourself
resumeNoAnswers to interrupts from the previous runClient-controlled; verify against server records

Look closely at messages. The schema's Role values include developer, system, assistant, user, tool, activity and reasoning, and a producer must treat messages as the complete history. The docs' server quickstart converts user, system and assistant messages from the input straight into model messages. That is fine for a local demo and wrong for a multi-tenant product, because it accepts a system message typed into the browser's developer tools.

How frontend tools and human in the loop approvals work

Because there is no mid-run channel from the user, AG-UI offers two ways for a run to hand control back to a human. The tool calls page sums it up in one line: "the agent proposes, the application disposes."

Frontend tools

The tools list in the run input belongs to the application. Each entry has a name, a description and, where the tool takes arguments, a parameter schema. When the agent calls one of these tools:

  1. The producer streams TOOL_CALL_START, TOOL_CALL_ARGS and TOOL_CALL_END.
  2. The producer must not answer the call itself: no TOOL_CALL_RESULT and no fabricated tool message.
  3. The producer finishes the run with the success outcome, optionally listing the unanswered calls in pendingToolCallIds. It must not report the run as interrupted.
  4. The application disposes of each call. It executes it, or declines it, with whatever consent its own rules require.
  5. If the thread continues, the next run's messages must include a tool message for every pending call, keyed by toolCallId. A failure is still an answer, sent as a tool message with error set, and a declined call is answered by saying it was declined.

A call naming a tool that was not advertised is not by itself a protocol violation, and the specification leaves it to the consumer, saying it should not run without the scrutiny a new tool would get. In practice: never execute it. Frontend tools suit browser actions such as navigating, highlighting a row, rendering a chart or asking the user to confirm a plan.

Interrupts and resume

Interrupts are the explicit way to stop and ask. A run that needs outside input ends with RUN_FINISHED whose outcome is the interrupt outcome, carrying at least one Interrupt. Each interrupt has an id that is unique within the run, a free-form reason, an optional human-readable message, an optional toolCallId naming the tool call an approval concerns, an optional responseSchema describing the expected answer and an optional expiresAt.

The run that continues carries a resume list with one ResumeEntry per interrupt. Each entry names the interruptId, a status of resolved or cancelled, an optional payload with the answer and optional metadata, which the schema describes as envelope information such as signatures. The coverage rule is strict: the resume list must cover every interrupt from the run being continued, either answered or explicitly abandoned. Omission is not abandonment, and the consumer must reject an incomplete resume before anything is sent. An interrupt the consumer judges expired can no longer be answered, only abandoned.

On the producer side, if an agent can tell that an approval-gated action is still open and the resume list has no entry for it, it must not perform the action and must not treat the omission as abandonment. A producer that kept no state has nothing to notice and is still conformant. That is why you should keep state: a stateless producer takes whatever resume list arrives as the whole truth.

Which mechanism to use

SituationUseWhy
The browser itself must do something (navigate, render, pick a file)Frontend toolThe application executes it; the result rides back as a tool message
A server-side action needs a human yes or no firstInterrupt with toolCallId and responseSchemaThe run stops, names what it waits for, and resumes only with an answer
The agent needs a missing value (a date, an account)Interrupt with responseSchemaThe UI can build a form from the schema
The user hits StopCancelled outcomeNothing is waited for; the next run is a fresh run
The agent needs no human inputAgent-side tool with in-stream TOOL_CALL_RESULTKeep it on the server, under the server's authorization

The security risks

The 1.0 specification has a short security section with three principles: user consent and control, model output is untrusted input, and data flows both ways. It also says plainly that the protocol cannot enforce them at the wire level. The risks below follow from those principles. They map onto the OWASP Top 10 for LLM Applications 2025, especially LLM05 Improper Output Handling and LLM01 Prompt Injection.

Untrusted agent output rendered in the UI

What the agent streams is shaped by its inputs, including web pages, documents and tool results an attacker may control. OWASP's LLM05 entry lists, among its examples, model-generated JavaScript or Markdown interpreted by the browser, resulting in cross-site scripting, and its first mitigation is to treat the model as any other user, with zero trust.

In an AG-UI client the obvious channel is TEXT_MESSAGE_CONTENT. Less obvious ones are tool arguments shown in confirmation cards, tool result parts carrying URLs, activity content, CUSTOM values, interrupt message text and reasoning. The specification covers all of them: applications must not render streamed content as executable markup. The risk is not only script. A Markdown image pointing at an attacker's server leaks data in its URL the moment it renders. A link can say one domain and go to another. Invisible or direction-changing Unicode can make a confirmation card say something other than what will execute.

State delta injection

STATE_DELTA is a remote write into your application's state, from a model whose output can be steered by injected content. Three things can go wrong.

First, the patch can target paths your UI treats as trusted. If your state object mixes agent-editable content with fields such as user.role, approved or featureFlags, a delta can change them. The specification asks applications to validate what they read out of state before acting on it, rendering it or granting anything because of it.

Second, the patch application itself can be abused. JSON Pointer paths are just strings, and a naive patch applier in JavaScript that walks into __proto__, constructor or prototype can pollute object prototypes in the client. Use a patch library that rejects those segments, or reject them yourself before applying.

Third, state comes back. The next run's input carries state from the client to the agent, which means anyone who controls the client can edit it. A server that reads state.approved == true from the input and proceeds has handed authorization to the browser.

Tool call spoofing

Several paths make a tool call look like something it is not:

  • Unadvertised tool names. A call can name a tool not in tools, and a client with a shared handler registry may find one.
  • Reopened calls. Arguments can change between the confirmation you showed and the moment you execute.
  • Forged history. The next run's messages come from the client, which can insert tool calls the agent never made, results the tool never returned, or a system message.
  • Forged approvals. A resume entry with status: resolved is just client JSON. Without a server-side record of the interrupt, the agent cannot tell real approval from fabricated.
  • Misattributed output. A message can claim to come from a different subagent; the consumer has to check attribution.

The specification's rule is the anchor: an application must not represent a call as user-approved when it was not. Make it a server rule too. The server, not the client, decides what was approved.

Authentication and session handling

AG-UI defines no credential, so whether you check anything is up to you. Common gaps: cookie-authenticated agent endpoints with no cross-site request protection, so another site can start runs as the user; long-lived bearer tokens readable by page scripts, which turns any rendering bug into token theft; no check that the user owns the threadId; WebSocket transports that accept any Origin; and tokens in query strings that end up in logs. Streams are long, too, so decide whether revoking a token should end runs already in flight.

Cross-tenant leakage

Multi-tenant agent services share thread stores, state stores, caches and log pipelines. Leakage usually comes from four places:

  • Identifier trust. The client mints threadId. Looking up history by threadId alone lets a guessed or leaked identifier expose another tenant's conversation.
  • Shared agent memory. Per-thread memory in a global dictionary keyed without a tenant can mix sessions under load.
  • Replay. The SSE binding lets a response replay a thread's earlier runs. Without an ownership check, that streams someone else's history.
  • Logs. rawEvent, RAW, reasoning and encrypted values travel in the stream. Logging it verbatim copies them into a store with different access rules.

Other risks worth a line each

  • Context injection. context entries are injected into the model's context, so anything a user can put there is prompt input.
  • forwardedProps. Intermediaries must pass it untouched, which makes it an easy channel for flags a backend later trusts. Validate it at the agent.
  • Provider file handles. A file source is an opaque handle to bytes at a model provider. One tenant's handle should not work in another tenant's run.
  • Resource exhaustion. Limit event size, events per run, run duration and state size.
  • Downgrades. Content dropped between versions comes with a warning. Show it, so nobody approves what they could not fully see.

Threat summary

RiskWhere it entersMain control
Script or Markdown injection in the UIText, tool args and results, activity, custom events, interrupt messagesRender as text or sanitized Markdown; CSP; no raw HTML
Data exfiltration through rendered URLsImages and links in Markdown or content partsAllow-list image and link hosts; no auto-loading of remote images
State delta injectionSTATE_DELTA, STATE_SNAPSHOTPath allow-list, schema for state, prototype-safe patching
Client-forged historymessages in the next run inputServer-side history store; reject client system and developer roles
Forged approvalsresume entries, tool messagesServer-side interrupt records bound to user, call and argument hash
Unadvertised or altered tool callsTOOL_CALL_START, reopened callsHandler allow-list per run; freeze arguments at approval
Missing authenticationEvery requestAuthenticate at the binding; short-lived tokens; CSRF and Origin checks
Cross-tenant leakagethreadId lookups, replay, logsTenant-scoped keys; ownership check per request; log redaction
Resource exhaustionLong streams, large deltasSize, count and time limits on both sides

How to validate and render AG-UI events safely

The 1.0 specification lists four implementation guidelines: build consent flows for consequential actions, validate schema and semantics at every trust boundary, isolate rendering from execution, and log enough to audit what an agent did. Here is what each looks like in code.

Validate structure with the published schema

The 1.0 schema is published at https://docs.ag-ui.com/spec/1.0/schema.json (JSON Schema draft 2020-12), and the first-party TypeScript, Python and .NET models are generated from it. Use those models, or validate raw events with any 2020-12 validator in a gateway or test harness. The processing model sets the order: compatibility boundary, then middleware, then enforcement, and application code must not see what enforcement would remove. Put security checks in your application handlers, after enforcement, so you check what the application will actually use.

Structure is not semantics. A schema-valid STATE_DELTA can still write to a forbidden path, and a schema-valid tool call can still name a tool the user may not use. Add semantic checks:

  • Tool name is in this run's allow-list, which your server computed for this user.
  • Tool arguments parse as JSON and validate against that tool's parameter schema, with additionalProperties: false and length limits on strings.
  • State delta paths match an allow-list of prefixes, and no segment equals __proto__, constructor or prototype.
  • State after the patch still validates against your own state schema.
  • Event sizes, counts and nesting depth are under your limits.

Render with output encoding, not trust

OWASP's cross-site scripting cheat sheet starts with framework security: frameworks escape text by default, and the danger is in the escape hatches that insert raw HTML. ASVS 5.0 requirement 1.2.1 asks for output encoding relevant to the context, 1.3.1 for sanitizing untrusted HTML with a well-known library, and 1.3.2 for avoiding eval() and dynamic code execution. For an AG-UI chat surface:

  1. Render text deltas as text, through a text node or framework text binding, never an HTML-parsing API.
  2. Render Markdown safely: raw HTML disabled, output through a maintained sanitizer, and the whole buffer re-rendered on each delta.
  3. Control links: https only, real destination shown, no javascript: or data: URLs.
  4. Control images: no auto-loading of remote images from model output without a host allow-list or a click.
  5. Show tool arguments as escaped JSON in confirmation cards, with invisible characters made visible.
  6. Never execute streamed content with eval, Function or an expression-evaluating template engine.
  7. Set a strict Content Security Policy, as the OWASP CSP cheat sheet recommends, with no inline script and tight img-src and connect-src.

Gate side effects behind real approval

Consent has to be real on both sides of the wire. A pattern that works:

  1. The agent decides an action needs approval and ends the run with an interrupt whose toolCallId names the call.
  2. Before ending the run, the server stores a record: thread, user, tenant, interrupt id, tool name, a hash of the canonical arguments, and an expiry.
  3. The UI shows the tool name and exact arguments, built from the stored record, not from text the model wrote about the action.
  4. The user approves. The client sends a resume entry with the interrupt id.
  5. The server looks up the record, checks the user, tenant, expiry and argument hash, marks the record as used, and only then executes.

Steps 2 and 5 are what make the approval trustworthy. Without them, the approval is whatever the client says it is.

Keep authority on the server

Treat the client's history and state as a rendering cache. For authorization, use server records: store history per tenant and thread; on each run accept only new user messages and tool messages answering calls you know are pending; drop client system and developer messages and any assistant turn you did not produce; derive the tools offered to the model from the user's permissions, not from the input; and log inputs, emitted events, approvals and executed actions, redacted, so you can say what the agent did and on whose authority.

Step by step: run a minimal AG-UI server and client and inspect the event stream

This walkthrough builds a stub agent with the official Python package, ag-ui-protocol, and a client with the official TypeScript package, @ag-ui/client. It needs no model API key, because the point is the protocol, not the model. It was run against ag-ui-protocol 1.0.0 and @ag-ui/client 1.0.3. Pin versions in your own project and update them deliberately.

Step 1: create a project and install the server packages

mkdir agui-demo && cd agui-demo
python3 -m venv .venv
. .venv/bin/activate
pip install "ag-ui-protocol==1.0.0" fastapi uvicorn

The package installs the ag_ui.core module, which holds the event and input models, and ag_ui.encoder, which turns events into SSE frames.

Step 2: write a stub agent with authentication and input checks

Save this as server.py. It authenticates a bearer token, binds each thread to the user who first used it, refuses system and developer messages from the client, and then streams a text message, a state snapshot, a state delta and a call to a frontend tool.

import os
import uuid

from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import StreamingResponse

from ag_ui.core import (
    EventType, RunAgentInput, RunErrorEvent, RunFinishedEvent, RunStartedEvent,
    StateDeltaEvent, StateSnapshotEvent, TextMessageContentEvent,
    TextMessageEndEvent, TextMessageStartEvent, ToolCallArgsEvent,
    ToolCallEndEvent, ToolCallStartEvent,
)
from ag_ui.encoder import EventEncoder

app = FastAPI(title="Minimal AG-UI agent")

# Demo only. Use your identity provider and a database in production.
TOKENS = {os.environ.get("AGUI_DEMO_TOKEN", "dev-token-alice"): "alice"}
THREAD_OWNER = {}

ALLOWED_ROLES = {"user", "assistant", "tool"}  # no system or developer turns from the browser
MAX_MESSAGES = 50

def authenticate(authorization):
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="missing bearer token")
    user = TOKENS.get(authorization.removeprefix("Bearer ").strip())
    if user is None:
        raise HTTPException(status_code=401, detail="invalid token")
    return user

@app.post("/agent")
async def run_agent(input_data: RunAgentInput, request: Request,
                    authorization: str = Header(default=None)):
    user = authenticate(authorization)

    owner = THREAD_OWNER.setdefault(input_data.thread_id, user)
    if owner != user:
        raise HTTPException(status_code=403, detail="thread belongs to another user")

    if len(input_data.messages) > MAX_MESSAGES:
        raise HTTPException(status_code=413, detail="history too long")
    for m in input_data.messages:
        if m.role not in ALLOWED_ROLES:
            raise HTTPException(status_code=400, detail=f"role {m.role} not accepted")

    encoder = EventEncoder(accept=request.headers.get("accept"))

    async def events():
        rid, tid = input_data.run_id, input_data.thread_id
        yield encoder.encode(RunStartedEvent(type=EventType.RUN_STARTED,
                                             thread_id=tid, run_id=rid))
        try:
            msg_id = str(uuid.uuid4())
            yield encoder.encode(TextMessageStartEvent(
                type=EventType.TEXT_MESSAGE_START, message_id=msg_id, role="assistant"))
            for part in ["Drafting a plan ", "with two steps."]:
                yield encoder.encode(TextMessageContentEvent(
                    type=EventType.TEXT_MESSAGE_CONTENT, message_id=msg_id, delta=part))
            yield encoder.encode(TextMessageEndEvent(
                type=EventType.TEXT_MESSAGE_END, message_id=msg_id))

            yield encoder.encode(StateSnapshotEvent(
                type=EventType.STATE_SNAPSHOT,
                snapshot={"plan": {"steps": [], "status": "drafting"}}))
            yield encoder.encode(StateDeltaEvent(
                type=EventType.STATE_DELTA,
                delta=[{"op": "add", "path": "/plan/steps/0",
                        "value": {"title": "Collect invoices", "done": False}}]))

            call_id = str(uuid.uuid4())
            yield encoder.encode(ToolCallStartEvent(
                type=EventType.TOOL_CALL_START, tool_call_id=call_id,
                tool_call_name="confirm_plan", parent_message_id=msg_id))
            yield encoder.encode(ToolCallArgsEvent(
                type=EventType.TOOL_CALL_ARGS, tool_call_id=call_id,
                delta='{"summary": "2 steps"}'))
            yield encoder.encode(ToolCallEndEvent(
                type=EventType.TOOL_CALL_END, tool_call_id=call_id))

            yield encoder.encode(RunFinishedEvent(
                type=EventType.RUN_FINISHED, thread_id=tid, run_id=rid))
        except Exception:
            yield encoder.encode(RunErrorEvent(type=EventType.RUN_ERROR,
                                               message="agent failed"))

    return StreamingResponse(events(), media_type=encoder.get_content_type())

Notice the order. Authentication, ownership and input checks all happen before the stream opens, so a refusal is an HTTP status, exactly as the SSE binding describes. Once RUN_STARTED is sent, failures can only travel as RUN_ERROR.

Step 3: start the server

uvicorn server:app --host 127.0.0.1 --port 8765

Bind to 127.0.0.1 while you experiment. The docs quickstart binds to 0.0.0.0, which exposes the endpoint to your network.

Step 4: inspect the raw event stream with curl

curl -sN -X POST http://127.0.0.1:8765/agent \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream' \
  -H 'Authorization: Bearer dev-token-alice' \
  -d '{"threadId":"thr-1","runId":"run-1","protocolVersion":"1.0",
       "messages":[{"id":"m1","role":"user","content":"Plan my week"}]}'

The -N flag turns off buffering so you see events as they arrive. The output, with identifiers shortened:

data: {"type":"RUN_STARTED","threadId":"thr-1","runId":"run-1"}

data: {"type":"TEXT_MESSAGE_START","messageId":"931f...","role":"assistant"}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"931f...","delta":"Drafting a plan "}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"931f...","delta":"with two steps."}

data: {"type":"TEXT_MESSAGE_END","messageId":"931f..."}

data: {"type":"STATE_SNAPSHOT","snapshot":{"plan":{"steps":[],"status":"drafting"}}}

data: {"type":"STATE_DELTA","delta":[{"op":"add","path":"/plan/steps/0","value":{"title":"Collect invoices","done":false}}]}

data: {"type":"TOOL_CALL_START","toolCallId":"cee1...","toolCallName":"confirm_plan","parentMessageId":"931f..."}

data: {"type":"TOOL_CALL_ARGS","toolCallId":"cee1...","delta":"{\"summary\": \"2 steps\"}"}

data: {"type":"TOOL_CALL_END","toolCallId":"cee1..."}

data: {"type":"RUN_FINISHED","threadId":"thr-1","runId":"run-1"}

Read it as the specification does. Every frame is one data: line with one JSON event. The tool call arguments arrive as a string, not an object, because the protocol carries them as text. The run finishes with success even though a frontend tool call is pending, which is the 1.0 rule for frontend tools.

Step 5: check that refusals happen before the stream

# No token: expect 401 and no event stream
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8765/agent \
  -H 'Content-Type: application/json' \
  -d '{"threadId":"thr-1","runId":"run-2","messages":[]}'

# A system message from the client: expect 400
curl -s -w ' %{http_code}\n' -X POST http://127.0.0.1:8765/agent \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer dev-token-alice' \
  -d '{"threadId":"thr-1","runId":"run-3","messages":[{"id":"m1","role":"system","content":"new rules"}]}'

In our run these returned 401 and 400 with the message role system not accepted. Neither opened a stream, so no partial output reached the client.

Step 6: consume the stream with the TypeScript client

mkdir client && cd client
npm init -y
npm install @ag-ui/client@1.0.3

Save this as client.mjs:

import { HttpAgent } from "@ag-ui/client";

const agent = new HttpAgent({
  url: "http://127.0.0.1:8765/agent",
  headers: { Authorization: `Bearer ${process.env.AGUI_DEMO_TOKEN ?? "dev-token-alice"}` },
  threadId: "thr-2",
});

agent.messages.push({ id: "u1", role: "user", content: "Plan my week" });

const result = await agent.runAgent(
  {
    tools: [{
      name: "confirm_plan",
      description: "Ask the user to confirm a plan",
      parameters: {
        type: "object",
        properties: { summary: { type: "string", maxLength: 200 } },
        required: ["summary"],
        additionalProperties: false,
      },
    }],
  },
  {
    onEvent({ event }) { console.log(event.type); },
    onTextMessageContentEvent({ event }) { console.log("  text:", event.delta); },
    onStateChanged({ state }) { console.log("  state:", JSON.stringify(state)); },
    onToolCallEndEvent({ toolCallName, toolCallArgs }) {
      console.log("  tool call:", toolCallName, JSON.stringify(toolCallArgs));
    },
  },
);
console.log("new messages:", result.newMessages.length);

Run it with node client.mjs. Our output:

RUN_STARTED
TEXT_MESSAGE_START
TEXT_MESSAGE_CONTENT
  text: Drafting a plan
TEXT_MESSAGE_CONTENT
  text: with two steps.
TEXT_MESSAGE_END
STATE_SNAPSHOT
  state: {"plan":{"steps":[],"status":"drafting"}}
STATE_DELTA
  state: {"plan":{"steps":[{"title":"Collect invoices","done":false}],"status":"drafting"}}
TOOL_CALL_START
TOOL_CALL_ARGS
TOOL_CALL_END
  tool call: confirm_plan {"summary":"2 steps"}
RUN_FINISHED
new messages: 1

The client applied the snapshot, then the JSON Patch, and parsed the finished tool arguments into an object for the onToolCallEndEvent handler. That handler is where your frontend tool logic belongs, and where the checks from the previous section go.

Step 7: add the checks a real client needs

Extend the tool handler before you execute anything. In outline:

const ALLOWED_TOOLS = new Set(["confirm_plan"]);
const FORBIDDEN_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);

function checkToolCall(name, args, schemaFor) {
  if (!ALLOWED_TOOLS.has(name)) throw new Error(`tool ${name} was not offered`);
  const validate = schemaFor(name);           // compiled JSON Schema validator
  if (!validate(args)) throw new Error(`bad arguments for ${name}`);
  return Object.freeze(structuredClone(args)); // freeze what the user will approve
}

function checkPatch(ops, allowedPrefixes) {
  for (const op of ops) {
    for (const p of [op.path, op.from].filter(Boolean)) {
      const segments = p.split("/").slice(1);
      if (segments.some((s) => FORBIDDEN_SEGMENTS.has(s))) throw new Error("forbidden path");
      if (!allowedPrefixes.some((prefix) => p === prefix || p.startsWith(prefix + "/"))) {
        throw new Error(`path ${p} not writable by the agent`);
      }
    }
  }
}

Wire checkPatch into a middleware or a state handler that runs before the client applies deltas, and render any text with a text binding, never an HTML one. On the server, swap the in-memory dictionaries for a database keyed by tenant and thread, and store interrupt records as described above.

Step 8: watch a real stream when debugging

The AG-UI debugging guide recommends the AG-UI Dojo, a set of small demonstrations of each building block, as a checklist when bringing AG-UI to a new surface. Add a security pass to that checklist. Capture a few real runs in a test environment with curl -N, list every event type your agent emits, and confirm that each one has a handler and a rendering rule. Anything that reaches the browser without a rule is a finding.

MCP, A2A, AG-UI and plain streaming chat APIs compared

MCPA2AAG-UIPlain streaming chat API
Who talks to whomAgent host (client) to tool or data serverAgent to agent, often across organisationsAgent backend to user-facing applicationYour code to a model provider
Unit of workJSON-RPC requests for tools, resources and promptsMessages and tasks with artifactsRuns on a thread, carrying eventsOne completion or response
Standard transportsstdio and Streamable HTTP (spec 2026-07-28)JSON-RPC 2.0 over HTTP with SSE streaming, gRPC, HTTP+JSON/RESTHTTP+SSE (required for HTTP), HTTP+Protobuf (optional); custom allowedUsually HTTP with SSE streaming
DiscoveryPer-request capability negotiation over stateless requests (spec 2026-07-28)Agent Card, optionally an authenticated extended cardOptional capability declaration; the stream is authoritativeModel list endpoints
Auth modelOptional; when used, OAuth 2.1 with the server as resource serverSecurity schemes declared in the Agent Card; credentials out of bandNone in the protocol; the binding and application provide itProvider API keys or tokens, server side
Human in the loopElicitation (server asks the user through the client) plus host-side approvalinput-required and auth-required task statesFrontend tools, interrupts and resumeNone; build it yourself
Shared stateNo shared state modelTask status and artifactsSnapshots and JSON Patch deltas, round-tripped each runNone; you resend history
Main risksTool poisoning, over-broad tokens, confused deputySpoofed agents, data leaving the trust boundary, task hijackOutput rendered as markup, state injection, forged history and approvals, missing auth, tenant leaksPrompt injection, leaking the provider key to the browser

A plain model streaming API is a server-side integration whose key must never reach the browser. AG-UI sits between that stream and the browser, which is why it needs its own authentication. The three agent protocols are not alternatives; they are different hops with different boundaries.

Common mistakes

  • Rendering text deltas as HTML. It fails the first time a web page tells the model to output a script tag.
  • Trusting 200 OK. Failures after the stream opens arrive as RUN_ERROR, and a truncated stream has no outcome.
  • Treating success as approval. Successful runs can leave pending tool calls, and unknown outcomes read as success. Approval lives in your records.
  • Acting on tool arguments before TOOL_CALL_END. Until the call closes, the text is a prefix.
  • Executing tools the run did not offer. The protocol tolerates an unadvertised tool name. Your handler registry should not.
  • Letting the browser send system messages. The schema allows the role. Your server should not accept it from a user.
  • Reading authorization from state or resume. Both come from the client.
  • Keying threads without tenants. threadId is minted by the client. Scope every lookup by the authenticated tenant and user.
  • Logging raw streams. rawEvent, RAW, reasoning and encrypted values land in logs with weaker access controls. Redact before you store.
  • Binding the dev server to every interface. Use 127.0.0.1 until the endpoint has authentication.
  • Skipping checks on a custom WebSocket transport. Check Origin and authenticate the upgrade.

FAQ

What is AG-UI in simple terms?

A standard way for an AI agent to talk to an app's user interface. The app sends the conversation, and the agent streams back typed events such as text, tool calls and state changes.

Is AG-UI the same as A2UI?

No. The AG-UI docs describe A2UI as a generative UI specification for delivering widgets, and AG-UI as the interaction protocol between agent front end and backend. AG-UI can carry A2UI.

How is AG-UI different from MCP?

MCP connects an agent to tools and data. AG-UI connects an agent to the person using an application. One agent can use both at once.

Does AG-UI use WebSockets?

The 1.0 specification defines HTTP plus server-sent events as the required binding for HTTP implementations and HTTP plus Protobuf as optional. WebSockets are allowed as a custom transport that must follow the same binding contract. Some hosting platforms offer a WebSocket path.

Does AG-UI handle authentication?

No. The specification says AG-UI defines no credential, and that authentication and authorization belong to the binding and the application. Add your own, such as short-lived bearer tokens or same-site cookies with cross-site request protection, and check thread ownership on every run.

How does human in the loop approval work in AG-UI?

Two ways. A frontend tool call ends the run with the call pending, and the app executes or declines it and sends the result in the next run. An interrupt ends the run with an explicit request, and the next run carries a resume entry that answers it. Keep a server-side record so the approval cannot be forged.

Can a malicious page make an AG-UI agent attack my users?

It can try, through prompt injection: content the agent reads can steer what it streams. The protocol's answer is that model output is untrusted input. Render text safely, validate tool arguments, restrict state paths and require real consent for actions.

Is server-sent events secure enough for agent streams?

SSE is a framing over HTTP, so its security comes from HTTPS, authentication and your handling of the content. The AG-UI binding adds clear error and truncation rules. It does not resume broken streams, which avoids a class of replay confusion.

Where is the AG-UI specification?

The 1.0 specification and its JSON Schema are on docs.ag-ui.com under /spec/1.0/. The schema is authoritative for structure, and the specification text is authoritative for behaviour such as ordering and error handling.

Where Safeguard fits

Safeguard does not inspect AG-UI streams, render agent output or enforce approvals in your application. Those controls live in your server and client, as described above. The overlap is in the software you ship. An AG-UI deployment is a Python, TypeScript or .NET service plus a browser bundle, built from the protocol SDKs, a web framework, a Markdown renderer, an HTML sanitizer, a JSON Patch library and their transitive dependencies. Safeguard can scan those repositories and images (safeguard scan --dir ., safeguard scan --image <name>), match components against vulnerability databases, apply reachability analysis, show which projects carry a given package version, and gate builds with policies. For agents that reach your systems through tools, Safeguard's MCP server lets assistants such as Claude query risk with the tools you choose. MCP Guardrail and the AI Gateway runtime guardrail proxy are in early access, so ask before you plan around either.

The sanitizer and Markdown renderer in an AG-UI client are the code standing between model output and your users, and they are ordinary open source dependencies that get vulnerabilities and fixes like any other. Keeping them current is supply chain hygiene, and it applies here even though the protocol itself is not a supply chain tool.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

None of these demos shows an AG-UI client. Each shows a Safeguard surface that follows the same rules this post recommends: named tools, approval before action, and a human deciding at the point of risk. You can download Safeguard and follow the CLI docs to try them.

This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. It is the agent to tool hop from the comparison table, with the allow-list set by a person rather than by the model. The same idea applies to the tools list your AG-UI server offers each user.

Click through it at your own pace, or open it full screen.

This is a real CLI session: asked to fix a lodash CVE, the CLI reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, prints a summary and pushes nothing. It is the propose and dispose pattern in a terminal, and /permissions shows how modes from Ask to Strict change what runs without a prompt.

Click through it at your own pace, or open it full screen.

This real Android session sends work to a computer running the CLI and approves the command on the phone with Allow once, Allow for turn or Deny. It is an approval round trip across devices, which is the shape of an AG-UI interrupt: the work pauses, a person answers, and only then does the action run.

Click through it at your own pace, or open it full screen.

Quick check

Your AG-UI server reads the next run's input, finds a resume entry with status resolved for interrupt int-1, and executes the transfer. What is missing?

The resume entry is client-supplied JSON. The server should have stored a record when it raised int-1 (user, tenant, tool call, argument hash, expiry) and should check that record, mark it used, and only then execute. Without that, any modified client can approve its own actions.

A STATE_DELTA arrives with an add operation at the path /proto/isAdmin. The patch is valid JSON Patch. Should the client apply it?

No. Schema validity is not safety. Reject any path segment equal to proto, constructor or prototype, and only allow writes under the prefixes the agent is meant to edit. Then validate the resulting state against your own state schema.

Sources

Last reviewed: 23 October 2026. AG-UI 1.0 is recent and its SDKs release often; check the versioned specification and your SDK's changelog before you build on a specific behaviour.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.