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
RunAgentInputgoes 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.
| Date | What happened | Source |
|---|---|---|
| 9 April 2025 | The docs changelog records "AG-UI repositories are now public" and the initial release | docs.ag-ui.com, What's New |
| 30 April 2025 | First packages published: @ag-ui/core, @ag-ui/client and @ag-ui/encoder on npm, and ag-ui-protocol 0.1.4 on PyPI | npm and PyPI registry metadata |
| 7 May 2025 | Creation timestamp of the current ag-ui-protocol/ag-ui GitHub repository | GitHub repository metadata |
| 17 September 2026 | Version 1.0.0 published to npm and to PyPI | npm and PyPI registry metadata |
| 16 October 2026 | @ag-ui/client 1.0.3, the latest npm release at the time of writing | npm registry metadata |
| 21 October 2026 | Most recent dated release tag in the repository at the time of writing | GitHub 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
| Family | Events | Pattern | Consumed by |
|---|---|---|---|
| Runs and steps | RUN_STARTED, RUN_FINISHED, RUN_ERROR, STEP_STARTED, STEP_FINISHED | lifecycle | the client itself |
| Text messages | TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END, TEXT_MESSAGE_CHUNK | streaming | the UI |
| Tool calls | TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END, TOOL_CALL_CHUNK, TOOL_CALL_RESULT | streaming | the application |
| Reasoning | REASONING_START, REASONING_END, REASONING_MESSAGE_START, REASONING_MESSAGE_CONTENT, REASONING_MESSAGE_END, REASONING_MESSAGE_CHUNK, REASONING_ENCRYPTED_VALUE | streaming | the UI |
| State | STATE_SNAPSHOT, STATE_DELTA, MESSAGES_SNAPSHOT | snapshot and delta | the application store |
| Activity | ACTIVITY_SNAPSHOT, ACTIVITY_DELTA | snapshot and delta | the UI |
| Subagents | SUBAGENT_STARTED, SUBAGENT_FINISHED, SUBAGENT_ERROR | lifecycle | the client itself |
| Passthrough | RAW, CUSTOM | standalone | the 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_SNAPSHOTreplaces the consumer's state wholesale. No merging.STATE_DELTAamends the current state with an RFC 6902 JSON Patch: an array of operations such asadd,remove,replace,move,copyandtest, applied in order.- At the start of a run, the baseline is the
statein 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:
- Ordered, complete delivery of a run's events in the order the producer emitted them.
- Delivery of the
RunAgentInputbefore any events. - A termination signal a consumer can tell apart from truncation.
- 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:
| Field | Required | What it carries | Security note |
|---|---|---|---|
threadId | Yes | The conversation, minted by the application | Bind it to a tenant and user on the server |
runId | Yes | This run; must not be reused on the thread | Reject reuse to stop replay |
messages | Yes | The whole conversation so far, in order | Client-controlled; may include roles you did not expect |
protocolVersion | No in schema; consumers implementing 1.0 must declare it | The version the consumer speaks | Log it; the spec lets a producer reject only a major version it does not implement |
tools | No | Frontend tools the application offers, with parameter schemas | Your server decides which tools a user may be offered |
context | No | Description and value pairs meant to be injected into the model's context | Treat as prompt input, not configuration |
state | No | The last agreed state | Client-controlled; never authoritative |
forwardedProps | No | Application-specific JSON passed through untouched | Intermediaries must not alter it; validate it yourself |
resume | No | Answers to interrupts from the previous run | Client-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:
- The producer streams
TOOL_CALL_START,TOOL_CALL_ARGSandTOOL_CALL_END. - The producer must not answer the call itself: no
TOOL_CALL_RESULTand no fabricated tool message. - The producer finishes the run with the success outcome, optionally listing the unanswered calls in
pendingToolCallIds. It must not report the run as interrupted. - The application disposes of each call. It executes it, or declines it, with whatever consent its own rules require.
- If the thread continues, the next run's
messagesmust include a tool message for every pending call, keyed bytoolCallId. A failure is still an answer, sent as a tool message witherrorset, 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
| Situation | Use | Why |
|---|---|---|
| The browser itself must do something (navigate, render, pick a file) | Frontend tool | The application executes it; the result rides back as a tool message |
| A server-side action needs a human yes or no first | Interrupt with toolCallId and responseSchema | The 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 responseSchema | The UI can build a form from the schema |
| The user hits Stop | Cancelled outcome | Nothing is waited for; the next run is a fresh run |
| The agent needs no human input | Agent-side tool with in-stream TOOL_CALL_RESULT | Keep 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
messagescome from the client, which can insert tool calls the agent never made, results the tool never returned, or asystemmessage. - Forged approvals. A
resumeentry withstatus: resolvedis 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 bythreadIdalone 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.
contextentries 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
filesource 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
| Risk | Where it enters | Main control |
|---|---|---|
| Script or Markdown injection in the UI | Text, tool args and results, activity, custom events, interrupt messages | Render as text or sanitized Markdown; CSP; no raw HTML |
| Data exfiltration through rendered URLs | Images and links in Markdown or content parts | Allow-list image and link hosts; no auto-loading of remote images |
| State delta injection | STATE_DELTA, STATE_SNAPSHOT | Path allow-list, schema for state, prototype-safe patching |
| Client-forged history | messages in the next run input | Server-side history store; reject client system and developer roles |
| Forged approvals | resume entries, tool messages | Server-side interrupt records bound to user, call and argument hash |
| Unadvertised or altered tool calls | TOOL_CALL_START, reopened calls | Handler allow-list per run; freeze arguments at approval |
| Missing authentication | Every request | Authenticate at the binding; short-lived tokens; CSRF and Origin checks |
| Cross-tenant leakage | threadId lookups, replay, logs | Tenant-scoped keys; ownership check per request; log redaction |
| Resource exhaustion | Long streams, large deltas | Size, 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: falseand length limits on strings. - State delta paths match an allow-list of prefixes, and no segment equals
__proto__,constructororprototype. - 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:
- Render text deltas as text, through a text node or framework text binding, never an HTML-parsing API.
- Render Markdown safely: raw HTML disabled, output through a maintained sanitizer, and the whole buffer re-rendered on each delta.
- Control links:
httpsonly, real destination shown, nojavascript:ordata:URLs. - Control images: no auto-loading of remote images from model output without a host allow-list or a click.
- Show tool arguments as escaped JSON in confirmation cards, with invisible characters made visible.
- Never execute streamed content with
eval,Functionor an expression-evaluating template engine. - Set a strict Content Security Policy, as the OWASP CSP cheat sheet recommends, with no inline script and tight
img-srcandconnect-src.
Gate side effects behind real approval
Consent has to be real on both sides of the wire. A pattern that works:
- The agent decides an action needs approval and ends the run with an interrupt whose
toolCallIdnames the call. - 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.
- The UI shows the tool name and exact arguments, built from the stored record, not from text the model wrote about the action.
- The user approves. The client sends a resume entry with the interrupt id.
- 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
| MCP | A2A | AG-UI | Plain streaming chat API | |
|---|---|---|---|---|
| Who talks to whom | Agent host (client) to tool or data server | Agent to agent, often across organisations | Agent backend to user-facing application | Your code to a model provider |
| Unit of work | JSON-RPC requests for tools, resources and prompts | Messages and tasks with artifacts | Runs on a thread, carrying events | One completion or response |
| Standard transports | stdio and Streamable HTTP (spec 2026-07-28) | JSON-RPC 2.0 over HTTP with SSE streaming, gRPC, HTTP+JSON/REST | HTTP+SSE (required for HTTP), HTTP+Protobuf (optional); custom allowed | Usually HTTP with SSE streaming |
| Discovery | Per-request capability negotiation over stateless requests (spec 2026-07-28) | Agent Card, optionally an authenticated extended card | Optional capability declaration; the stream is authoritative | Model list endpoints |
| Auth model | Optional; when used, OAuth 2.1 with the server as resource server | Security schemes declared in the Agent Card; credentials out of band | None in the protocol; the binding and application provide it | Provider API keys or tokens, server side |
| Human in the loop | Elicitation (server asks the user through the client) plus host-side approval | input-required and auth-required task states | Frontend tools, interrupts and resume | None; build it yourself |
| Shared state | No shared state model | Task status and artifacts | Snapshots and JSON Patch deltas, round-tripped each run | None; you resend history |
| Main risks | Tool poisoning, over-broad tokens, confused deputy | Spoofed agents, data leaving the trust boundary, task hijack | Output rendered as markup, state injection, forged history and approvals, missing auth, tenant leaks | Prompt 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 asRUN_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
systemmessages. The schema allows the role. Your server should not accept it from a user. - Reading authorization from
stateorresume. Both come from the client. - Keying threads without tenants.
threadIdis 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.1until the endpoint has authentication. - Skipping checks on a custom WebSocket transport. Check
Originand 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 situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull 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
- AG-UI: Overview
- AG-UI: MCP, A2A, and AG-UI
- AG-UI Specification 1.0
- AG-UI 1.0: Key Changes
- AG-UI 1.0: The Event Model
- AG-UI 1.0: Run Input
- AG-UI 1.0: Snapshots and Deltas
- AG-UI 1.0: Interrupts and Resume
- AG-UI 1.0: Transports
- AG-UI 1.0: HTTP + Server-Sent Events
- AG-UI 1.0: HTTP + Protobuf
- AG-UI 1.0: Processing Model
- AG-UI 1.0: Event Streams
- AG-UI 1.0: Runs and Steps
- AG-UI 1.0: Tool Calls
- AG-UI 1.0: State
- AG-UI 1.0: Raw and Custom Events
- AG-UI 1.0: JSON Schema
- AG-UI: Core architecture (concept page)
- AG-UI: Server quickstart
- AG-UI: Build clients quickstart
- AG-UI: What's New
- AG-UI GitHub repository
- AG-UI repository SECURITY.md
- PyPI: ag-ui-protocol
- npm: @ag-ui/client
- Model Context Protocol specification 2026-07-28
- Model Context Protocol: Transports (2026-07-28)
- Model Context Protocol: Authorization (2026-07-28)
- A2A Protocol Specification
- WHATWG HTML Living Standard: Server-sent events
- RFC 6455: The WebSocket Protocol
- RFC 6902: JavaScript Object Notation (JSON) Patch
- OWASP Top 10 for LLM Applications 2025: LLM05 Improper Output Handling
- OWASP Top 10 for LLM Applications 2025: LLM01 Prompt Injection
- OWASP ASVS 5.0: V1 Encoding and Sanitization
- OWASP Cheat Sheet: Cross Site Scripting Prevention
- OWASP Cheat Sheet: Content Security Policy
- Amazon Bedrock AgentCore: Deploy AG-UI servers in AgentCore Runtime
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.