Skip to content

feat(examples): add AG-UI polyglot chat backends#963

Merged
AlemTuzlak merged 6 commits into
mainfrom
ag-ui-example
Jul 20, 2026
Merged

feat(examples): add AG-UI polyglot chat backends#963
AlemTuzlak merged 6 commits into
mainfrom
ag-ui-example

Conversation

@jherr

@jherr jherr commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add an AG-UI polyglot React example with Go, Rust, PHP, Zig, Bash, and Python backends
  • stream OpenAI and Anthropic responses through the same AG-UI SSE client contract
  • detect installed toolchains, start available servers with dev:all, and show setup guidance for unavailable runtimes
  • add dependency-light Bash and standard-library Python implementations; Python runs on port 8006

Python support

The example now includes a Python 3.9+ backend built entirely with the standard library (ThreadingHTTPServer and urllib.request). It supports both OpenAI and Anthropic streaming and is available through pnpm dev:python or pnpm dev:all.

Python AG-UI backend

Test plan

  • pnpm test:pr
  • pnpm --filter @tanstack/ai-e2e test:e2e
  • pnpm test:docs
  • pnpm test:types && pnpm build in examples/ag-ui
  • verify Python /health and AG-UI SSE error flow locally
  • verify a streamed OpenAI response through the Python backend

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Added the AG-UI Polyglot Chat example with a React interface and selectable Go, Rust, PHP, Zig, Bash, and Python backends.
    • Added streaming chat responses through the AG-UI SSE protocol with OpenAI and Anthropic provider support.
    • Added automatic backend availability detection, setup guidance, and runtime-based server selection.
  • Documentation
    • Added quick-start instructions, architecture details, prerequisites, and per-backend setup documentation.

jherr and others added 5 commits July 19, 2026 11:45
…ends

Introduce examples/ag-ui, a React SPA using @tanstack/ai-react-ui against hand-rolled AG-UI SSE servers in Go and Rust that stream OpenAI and Anthropic chat completions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add PHP and Zig servers with runtime detection so the example can expose every available backend and guide setup for missing toolchains.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Expand the polyglot example with dependency-light servers and toolchain-aware local startup so the same AG-UI client can demonstrate both runtimes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep generated interpreter caches out of the AG-UI example sources.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jherr
jherr requested a review from a team as a code owner July 20, 2026 00:47
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an AG-UI Polyglot Echo example with a React/Vite chat client, runtime-aware development tooling, and Go, Rust, PHP, Zig, Bash, and Python SSE backends supporting OpenAI and Anthropic streaming.

Changes

AG-UI Polyglot Echo

Layer / File(s) Summary
Example setup and server discovery
examples/README.md, examples/ag-ui/package.json, examples/ag-ui/scripts/*, examples/ag-ui/public/servers.json, examples/ag-ui/README.md
Adds setup documentation, package scripts, runtime detection, generated server metadata, and parallel development orchestration.
React client and backend proxy
examples/ag-ui/src/*, examples/ag-ui/vite.config.ts, examples/ag-ui/index.html, examples/ag-ui/tsconfig.json
Adds backend/provider selection, setup instructions, SSE chat rendering, Vite proxying, and application bootstrap configuration.
Language-specific AG-UI servers
examples/ag-ui/servers/{go,rust,php,zig,bash,python}/*
Adds HTTP/SSE servers with message normalization, provider selection, lifecycle events, and OpenAI/Anthropic streaming implementations.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant App
  participant ViteProxy
  participant AGUIServer
  participant LLMProvider
  User->>App: select backend and provider
  App->>ViteProxy: POST /api/{backend}
  ViteProxy->>AGUIServer: rewrite request to /
  AGUIServer->>LLMProvider: stream completion
  LLMProvider-->>AGUIServer: text deltas
  AGUIServer-->>App: AG-UI SSE events
  App-->>User: render streamed response
Loading

Suggested reviewers: tombeckenham

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Concise and specific; it accurately summarizes the new AG-UI polyglot chat backends.
Description check ✅ Passed It provides the main change, Python support details, and a concrete test plan.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ag-ui-example

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

examples/ag-ui/vite.config.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): examples/ag-ui/vite.config.ts


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@963

@tanstack/ai-acp

npm i https://pkg.pr.new/@tanstack/ai-acp@963

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@963

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@963

@tanstack/ai-bedrock

npm i https://pkg.pr.new/@tanstack/ai-bedrock@963

@tanstack/ai-claude-code

npm i https://pkg.pr.new/@tanstack/ai-claude-code@963

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@963

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@963

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@963

@tanstack/ai-codex

npm i https://pkg.pr.new/@tanstack/ai-codex@963

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@963

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@963

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@963

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@963

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@963

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@963

@tanstack/ai-grok-build

npm i https://pkg.pr.new/@tanstack/ai-grok-build@963

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@963

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@963

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@963

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@963

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@963

@tanstack/ai-mistral

npm i https://pkg.pr.new/@tanstack/ai-mistral@963

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@963

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@963

@tanstack/ai-opencode

npm i https://pkg.pr.new/@tanstack/ai-opencode@963

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@963

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@963

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@963

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@963

@tanstack/ai-sandbox

npm i https://pkg.pr.new/@tanstack/ai-sandbox@963

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-sandbox-cloudflare@963

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/@tanstack/ai-sandbox-daytona@963

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/@tanstack/ai-sandbox-docker@963

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/@tanstack/ai-sandbox-local-process@963

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/@tanstack/ai-sandbox-sprites@963

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/@tanstack/ai-sandbox-vercel@963

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@963

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@963

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@963

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@963

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@963

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@963

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@963

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@963

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@963

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@963

commit: 348caf2

@nx-cloud

nx-cloud Bot commented Jul 20, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 348caf2

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-20 02:48:47 UTC

Apply the repository Markdown table formatting so the autofix check remains clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​tokio@​1.53.05810093100100
Addedcargo/​reqwest@​0.12.287910094100100
Addedcargo/​serde@​1.0.2298110093100100
Addedcargo/​serde_json@​1.0.1508210093100100
Addedcargo/​axum@​0.8.99710093100100
Addedcargo/​futures-util@​0.3.3310010093100100
Addedcargo/​tokio-stream@​0.1.1810010093100100
Addedcargo/​async-stream@​0.3.610010094100100

View full report

@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: cargo hyper-util is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/reqwest@0.12.28cargo/axum@0.8.9cargo/hyper-util@0.1.20

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/hyper-util@0.1.20. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo libc is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/reqwest@0.12.28cargo/axum@0.8.9cargo/tokio@1.53.0cargo/libc@0.2.186

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/libc@0.2.186. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo tokio is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: examples/ag-ui/servers/rust/Cargo.lockcargo/tokio@1.53.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/tokio@1.53.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo writeable is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/reqwest@0.12.28cargo/writeable@0.6.3

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/writeable@0.6.3. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (2)
examples/ag-ui/servers/go/openai.go (1)

73-73: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

bufio.Scanner uses the default 64 KB line limit.

bufio.NewScanner caps tokens at bufio.MaxScanTokenSize (64 KB). An SSE data: line longer than that makes scanner.Scan() stop and scanner.Err() return bufio.ErrTooLong, silently truncating the stream. Consider enlarging the buffer or switching to bufio.Reader.ReadString('\n').

♻️ Raise the scanner buffer
 	scanner := bufio.NewScanner(resp.Body)
+	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
 	for scanner.Scan() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ag-ui/servers/go/openai.go` at line 73, Increase the buffer capacity
for the bufio.Scanner created in the SSE response handling flow, using
Scanner.Buffer before scanning so data lines larger than the default 64 KB are
accepted. Preserve the existing scanning and error-handling behavior while
selecting a sufficiently larger maximum token size.
examples/ag-ui/servers/rust/src/agui.rs (1)

65-74: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Unknown providers are silently coerced to OpenAI (diverges from Python/Bash). Both the Rust and Zig backends treat any provider that isn't anthropic as openai, whereas the Python (provider_from_input) and Bash backends in this same example reject unsupported providers with an error. For a demo whose stated goal is a shared cross-language contract, consider aligning on erroring for unrecognized providers.

  • examples/ag-ui/servers/rust/src/agui.rs#L65-L74: replace the catch-all _ arm with an explicit "openai" match and error/fallback handling for other values.
  • examples/ag-ui/servers/zig/src/agui.zig#L54-L64: return an error (or otherwise signal RUN_ERROR) for providers that are neither openai nor anthropic instead of defaulting to OpenAI.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ag-ui/servers/rust/src/agui.rs` around lines 65 - 74, Align provider
validation with the shared contract: in examples/ag-ui/servers/rust/src/agui.rs
lines 65-74, update the provider match to handle "openai" explicitly and reject
all other values instead of defaulting to OpenAI; in
examples/ag-ui/servers/zig/src/agui.zig lines 54-64, return an error or signal
RUN_ERROR for providers other than "openai" and "anthropic".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/ag-ui/servers/go/agui.go`:
- Around line 89-102: Replace wildcard CORS with an allow-list for the known
Vite development origin. In examples/ag-ui/servers/go/agui.go lines 89-102,
update beginSSE to echo the origin only when it matches http://localhost:5173;
apply the same restriction in examples/ag-ui/servers/php/lib/Agui.php lines
107-131 for begin_sse() and cors_preflight(), with no wildcard responses.

In `@examples/ag-ui/servers/php/lib/Providers.php`:
- Around line 131-190: Update the curl options in stream_provider_sse to add a
finite CURLOPT_CONNECTTIMEOUT and CURLOPT_LOW_SPEED_LIMIT with
CURLOPT_LOW_SPEED_TIME, preventing handshake or stalled-stream hangs while
preserving long-running streams; do not add a blanket CURLOPT_TIMEOUT.

In `@examples/ag-ui/servers/rust/src/messages.rs`:
- Around line 59-77: Update the content parsing function around the array join
and final content.to_string() fallback so arrays without text items return an
empty string instead of raw JSON serialization. Preserve the existing joined
text behavior for arrays containing text parts, and retain raw serialization
only for non-array content.

In `@examples/ag-ui/servers/rust/src/providers.rs`:
- Around line 55-57: Update both streaming loops at
examples/ag-ui/servers/rust/src/providers.rs:55-57 and 138-140 to accumulate raw
byte chunks, split the byte buffer on newline boundaries, and decode only
complete lines. Remove per-chunk String::from_utf8_lossy usage while preserving
the existing line-processing behavior; handle any remaining unterminated bytes
after the stream ends as appropriate.

In `@examples/ag-ui/src/App.tsx`:
- Line 32: Replace the T[] array syntax with Array<T> in both ServerInfo
declarations: update servers in examples/ag-ui/src/App.tsx lines 32-32 and the
useState type in lines 119-119 to use Array<ServerInfo>, preserving the existing
behavior.
- Around line 160-162: Update the active server selection expression near
servers.find to use servers.at(0) instead of servers[0], preserving the trailing
default object fallback for empty server arrays and eliminating the
unnecessary-condition lint error.
- Around line 4-9: Reorder the imports in App.tsx so the value import from
`@tanstack/ai-react-ui` appears before the type import from `@tanstack/ai-react`,
satisfying the import/order rule without changing the imported symbols.

---

Nitpick comments:
In `@examples/ag-ui/servers/go/openai.go`:
- Line 73: Increase the buffer capacity for the bufio.Scanner created in the SSE
response handling flow, using Scanner.Buffer before scanning so data lines
larger than the default 64 KB are accepted. Preserve the existing scanning and
error-handling behavior while selecting a sufficiently larger maximum token
size.

In `@examples/ag-ui/servers/rust/src/agui.rs`:
- Around line 65-74: Align provider validation with the shared contract: in
examples/ag-ui/servers/rust/src/agui.rs lines 65-74, update the provider match
to handle "openai" explicitly and reject all other values instead of defaulting
to OpenAI; in examples/ag-ui/servers/zig/src/agui.zig lines 54-64, return an
error or signal RUN_ERROR for providers other than "openai" and "anthropic".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd0af346-a7ee-41af-94aa-8d58acfa0388

📥 Commits

Reviewing files that changed from the base of the PR and between bf870fa and 348caf2.

⛔ Files ignored due to path filters (4)
  • examples/ag-ui/bash-server.png is excluded by !**/*.png
  • examples/ag-ui/python-server.png is excluded by !**/*.png
  • examples/ag-ui/servers/rust/Cargo.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (44)
  • examples/README.md
  • examples/ag-ui/.env.example
  • examples/ag-ui/.gitignore
  • examples/ag-ui/README.md
  • examples/ag-ui/index.html
  • examples/ag-ui/package.json
  • examples/ag-ui/public/servers.json
  • examples/ag-ui/scripts/detect-servers.mjs
  • examples/ag-ui/scripts/dev-all.mjs
  • examples/ag-ui/scripts/servers.mjs
  • examples/ag-ui/servers/bash/README.md
  • examples/ag-ui/servers/bash/server.sh
  • examples/ag-ui/servers/go/README.md
  • examples/ag-ui/servers/go/agui.go
  • examples/ag-ui/servers/go/anthropic.go
  • examples/ag-ui/servers/go/go.mod
  • examples/ag-ui/servers/go/llm.go
  • examples/ag-ui/servers/go/main.go
  • examples/ag-ui/servers/go/messages.go
  • examples/ag-ui/servers/go/openai.go
  • examples/ag-ui/servers/php/README.md
  • examples/ag-ui/servers/php/lib/Agui.php
  • examples/ag-ui/servers/php/lib/Messages.php
  • examples/ag-ui/servers/php/lib/Providers.php
  • examples/ag-ui/servers/php/router.php
  • examples/ag-ui/servers/python/README.md
  • examples/ag-ui/servers/python/server.py
  • examples/ag-ui/servers/rust/Cargo.toml
  • examples/ag-ui/servers/rust/README.md
  • examples/ag-ui/servers/rust/src/agui.rs
  • examples/ag-ui/servers/rust/src/main.rs
  • examples/ag-ui/servers/rust/src/messages.rs
  • examples/ag-ui/servers/rust/src/providers.rs
  • examples/ag-ui/servers/zig/README.md
  • examples/ag-ui/servers/zig/build.zig
  • examples/ag-ui/servers/zig/src/agui.zig
  • examples/ag-ui/servers/zig/src/main.zig
  • examples/ag-ui/servers/zig/src/messages.zig
  • examples/ag-ui/servers/zig/src/providers.zig
  • examples/ag-ui/src/App.tsx
  • examples/ag-ui/src/main.tsx
  • examples/ag-ui/src/styles.css
  • examples/ag-ui/tsconfig.json
  • examples/ag-ui/vite.config.ts

Comment on lines +89 to +102
func beginSSE(w http.ResponseWriter) (*sseStream, bool) {
flusher, ok := w.(http.Flusher)
if !ok {
return nil, false
}

w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusOK)

return &sseStream{w: w, flusher: flusher}, true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Wildcard CORS repeated across the Go and PHP SSE servers. Both backends set Access-Control-Allow-Origin: * on their SSE/preflight responses. Since these bind to 127.0.0.1, any webpage open in the developer's browser could hit them and trigger LLM calls against the configured API keys, reading back the streamed output.

  • examples/ag-ui/servers/go/agui.go#L89-L102: in beginSSE, replace the wildcard with a check against the known Vite dev origin (e.g. http://localhost:5173) before echoing it back.
  • examples/ag-ui/servers/php/lib/Agui.php#L107-L131: in begin_sse() and cors_preflight(), apply the same origin allow-list instead of *.
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 97-97: Setting the 'Access-Control-Allow-Origin' response header to the wildcard '' disables the same-origin policy and lets any website read the response. This is especially dangerous when combined with 'Access-Control-Allow-Credentials', as it can expose authenticated data to attacker-controlled origins. Instead of '', reflect a request origin only after validating it against an allow-list of trusted origins.
Context: w.Header().Set("Access-Control-Allow-Origin", "*")
Note: [CWE-942] Permissive Cross-domain Policy with Untrusted Domains.

(cors-wildcard-origin-header-go)

📍 Affects 2 files
  • examples/ag-ui/servers/go/agui.go#L89-L102 (this comment)
  • examples/ag-ui/servers/php/lib/Agui.php#L107-L131
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ag-ui/servers/go/agui.go` around lines 89 - 102, Replace wildcard
CORS with an allow-list for the known Vite development origin. In
examples/ag-ui/servers/go/agui.go lines 89-102, update beginSSE to echo the
origin only when it matches http://localhost:5173; apply the same restriction in
examples/ag-ui/servers/php/lib/Agui.php lines 107-131 for begin_sse() and
cors_preflight(), with no wildcard responses.

Source: Linters/SAST tools

Comment on lines +131 to +190
function stream_provider_sse(
string $url,
array $headers,
string $payload,
callable $handleEvent
): void {
$buffer = '';

$ch = curl_init($url);
if ($ch === false) {
throw new RuntimeException('failed to initialize curl');
}

curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use (&$buffer, $handleEvent): int {
$buffer .= $chunk;

while (($lineEnd = strpos($buffer, "\n")) !== false) {
$line = rtrim(substr($buffer, 0, $lineEnd), "\r");
$buffer = substr($buffer, $lineEnd + 1);

if (!str_starts_with($line, 'data: ')) {
continue;
}

$data = substr($line, 6);
if ($data === '[DONE]') {
return strlen($chunk);
}

$parsed = json_decode($data, true);
if (!is_array($parsed)) {
continue;
}

$handleEvent($parsed);
}

return strlen($chunk);
},
]);

$ok = curl_exec($ch);
if ($ok === false) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException($error !== '' ? $error : 'provider request failed');
}

$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status >= 400) {
throw new RuntimeException('provider request failed (' . $status . ')');
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No connection/stall timeout on the outbound provider request.

curl_setopt_array sets no CURLOPT_CONNECTTIMEOUT or CURLOPT_LOW_SPEED_LIMIT/CURLOPT_LOW_SPEED_TIME. If api.openai.com/api.anthropic.com never completes the TCP/TLS handshake or stalls mid-stream, this call can hang indefinitely — and since PHP's built-in dev server typically serves one request at a time, that hang can block the entire demo server, including /health. A blanket CURLOPT_TIMEOUT would be wrong here since it would also cut off long legitimate streams, but a connect timeout plus a low-speed guard won't.

🔧 Proposed fix
     curl_setopt_array($ch, [
         CURLOPT_POST => true,
         CURLOPT_HTTPHEADER => $headers,
         CURLOPT_POSTFIELDS => $payload,
         CURLOPT_RETURNTRANSFER => false,
+        CURLOPT_CONNECTTIMEOUT => 10,
+        CURLOPT_LOW_SPEED_LIMIT => 1,
+        CURLOPT_LOW_SPEED_TIME => 30,
         CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use (&$buffer, $handleEvent): int {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function stream_provider_sse(
string $url,
array $headers,
string $payload,
callable $handleEvent
): void {
$buffer = '';
$ch = curl_init($url);
if ($ch === false) {
throw new RuntimeException('failed to initialize curl');
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use (&$buffer, $handleEvent): int {
$buffer .= $chunk;
while (($lineEnd = strpos($buffer, "\n")) !== false) {
$line = rtrim(substr($buffer, 0, $lineEnd), "\r");
$buffer = substr($buffer, $lineEnd + 1);
if (!str_starts_with($line, 'data: ')) {
continue;
}
$data = substr($line, 6);
if ($data === '[DONE]') {
return strlen($chunk);
}
$parsed = json_decode($data, true);
if (!is_array($parsed)) {
continue;
}
$handleEvent($parsed);
}
return strlen($chunk);
},
]);
$ok = curl_exec($ch);
if ($ok === false) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException($error !== '' ? $error : 'provider request failed');
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException('provider request failed (' . $status . ')');
}
}
function stream_provider_sse(
string $url,
array $headers,
string $payload,
callable $handleEvent
): void {
$buffer = '';
$ch = curl_init($url);
if ($ch === false) {
throw new RuntimeException('failed to initialize curl');
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_LOW_SPEED_LIMIT => 1,
CURLOPT_LOW_SPEED_TIME => 30,
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use (&$buffer, $handleEvent): int {
$buffer .= $chunk;
while (($lineEnd = strpos($buffer, "\n")) !== false) {
$line = rtrim(substr($buffer, 0, $lineEnd), "\r");
$buffer = substr($buffer, $lineEnd + 1);
if (!str_starts_with($line, 'data: ')) {
continue;
}
$data = substr($line, 6);
if ($data === '[DONE]') {
return strlen($chunk);
}
$parsed = json_decode($data, true);
if (!is_array($parsed)) {
continue;
}
$handleEvent($parsed);
}
return strlen($chunk);
},
]);
$ok = curl_exec($ch);
if ($ok === false) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException($error !== '' ? $error : 'provider request failed');
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException('provider request failed (' . $status . ')');
}
}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 149-149: Avoid unused parameters such as '$handle'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ag-ui/servers/php/lib/Providers.php` around lines 131 - 190, Update
the curl options in stream_provider_sse to add a finite CURLOPT_CONNECTTIMEOUT
and CURLOPT_LOW_SPEED_LIMIT with CURLOPT_LOW_SPEED_TIME, preventing handshake or
stalled-stream hangs while preserving long-running streams; do not add a blanket
CURLOPT_TIMEOUT.

Comment on lines +59 to +77
if let Some(items) = content.as_array() {
let joined = items
.iter()
.filter_map(|item| {
if item.get("type")?.as_str()? == "text" {
item.get("text")?.as_str().map(str::to_string)
} else {
None
}
})
.collect::<Vec<_>>()
.join("");
if !joined.is_empty() {
return joined;
}
}

content.to_string()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Non-text content falls through to raw JSON serialization.

When content is an array with no type=="text" items (e.g. image parts), the join at Line 70 is empty, the guard at Line 71 skips the early return, and control reaches Line 76 content.to_string(), which serializes the entire array as JSON text and injects it into the chat message. The Python/PHP equivalents yield an empty string here so the message is dropped downstream. Consider returning an empty string for the non-text fallback to match the cross-language contract.

🔧 Proposed fix
-    content.to_string()
+    String::new()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(items) = content.as_array() {
let joined = items
.iter()
.filter_map(|item| {
if item.get("type")?.as_str()? == "text" {
item.get("text")?.as_str().map(str::to_string)
} else {
None
}
})
.collect::<Vec<_>>()
.join("");
if !joined.is_empty() {
return joined;
}
}
content.to_string()
}
if let Some(items) = content.as_array() {
let joined = items
.iter()
.filter_map(|item| {
if item.get("type")?.as_str()? == "text" {
item.get("text")?.as_str().map(str::to_string)
} else {
None
}
})
.collect::<Vec<_>>()
.join("");
if !joined.is_empty() {
return joined;
}
}
String::new()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ag-ui/servers/rust/src/messages.rs` around lines 59 - 77, Update the
content parsing function around the array join and final content.to_string()
fallback so arrays without text items return an empty string instead of raw JSON
serialization. Preserve the existing joined text behavior for arrays containing
text parts, and retain raw serialization only for non-array content.

Comment on lines +55 to +57
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|error| error.to_string())?;
buffer.push_str(&String::from_utf8_lossy(&chunk));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail
sed -n '1,240p' examples/ag-ui/servers/rust/src/providers.rs

Repository: TanStack/ai

Length of output: 5655


Avoid lossy decoding before line assembly

  • examples/ag-ui/servers/rust/src/providers.rs#L55-L57
  • examples/ag-ui/servers/rust/src/providers.rs#L138-L140

String::from_utf8_lossy runs on each bytes_stream() chunk, so a multibyte UTF-8 character split across chunks can be replaced with U+FFFD. Buffer raw bytes, split on \n, and decode only complete lines.

📍 Affects 1 file
  • examples/ag-ui/servers/rust/src/providers.rs#L55-L57 (this comment)
  • examples/ag-ui/servers/rust/src/providers.rs#L138-L140
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ag-ui/servers/rust/src/providers.rs` around lines 55 - 57, Update
both streaming loops at examples/ag-ui/servers/rust/src/providers.rs:55-57 and
138-140 to accumulate raw byte chunks, split the byte buffer on newline
boundaries, and decode only complete lines. Remove per-chunk
String::from_utf8_lossy usage while preserving the existing line-processing
behavior; handle any remaining unterminated bytes after the stream ends as
appropriate.

Comment on lines +4 to +9
import {
Chat,
ChatInput,
ChatMessage,
ChatMessages,
} from '@tanstack/ai-react-ui'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix import ordering to satisfy import/order.

ESLint flags the value import from @tanstack/ai-react-ui as needing to precede the type import from @tanstack/ai-react. This fails lint in CI.

♻️ Reorder imports
 import { fetchServerSentEvents } from '`@tanstack/ai-react`'
-import type { UIMessage } from '`@tanstack/ai-react`'
 import {
   Chat,
   ChatInput,
   ChatMessage,
   ChatMessages,
 } from '`@tanstack/ai-react-ui`'
+import type { UIMessage } from '`@tanstack/ai-react`'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {
Chat,
ChatInput,
ChatMessage,
ChatMessages,
} from '@tanstack/ai-react-ui'
import { fetchServerSentEvents } from '`@tanstack/ai-react`'
import {
Chat,
ChatInput,
ChatMessage,
ChatMessages,
} from '`@tanstack/ai-react-ui`'
import type { UIMessage } from '`@tanstack/ai-react`'
🧰 Tools
🪛 ESLint

[error] 4-9: @tanstack/ai-react-ui import should occur before type import of @tanstack/ai-react

(import/order)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ag-ui/src/App.tsx` around lines 4 - 9, Reorder the imports in
App.tsx so the value import from `@tanstack/ai-react-ui` appears before the type
import from `@tanstack/ai-react`, satisfying the import/order rule without
changing the imported symbols.

Source: Linters/SAST tools


type ServersManifest = {
generatedAt: string
servers: ServerInfo[]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use Array<ServerInfo> generic syntax. The project's @typescript-eslint/array-type rule forbids the T[] shorthand, so both declarations fail lint.

  • examples/ag-ui/src/App.tsx#L32-L32: change servers: ServerInfo[] to servers: Array<ServerInfo>.
  • examples/ag-ui/src/App.tsx#L119-L119: change useState<ServerInfo[]>([]) to useState<Array<ServerInfo>>([]).
🧰 Tools
🪛 ESLint

[error] 32-32: Array type using 'ServerInfo[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)

📍 Affects 1 file
  • examples/ag-ui/src/App.tsx#L32-L32 (this comment)
  • examples/ag-ui/src/App.tsx#L119-L119
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ag-ui/src/App.tsx` at line 32, Replace the T[] array syntax with
Array<T> in both ServerInfo declarations: update servers in
examples/ag-ui/src/App.tsx lines 32-32 and the useState type in lines 119-119 to
use Array<ServerInfo>, preserving the existing behavior.

Source: Linters/SAST tools

Comment on lines +160 to +162
const active =
servers.find((item) => item.id === backend) ??
servers[0] ??

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

servers[0] fallback is defeated by the type system.

With the current tsconfig (no noUncheckedIndexedAccess), servers[0] is typed as non-nullable ServerInfo, so TS/ESLint treat the trailing ?? {...} default as dead code (the no-unnecessary-condition error at Lines 161-162), even though servers[0] is undefined at runtime when the array is empty. Use servers.at(0), which is typed ServerInfo | undefined, to keep the runtime fallback reachable and satisfy the linter.

🐛 Preserve the runtime fallback
   const active =
     servers.find((item) => item.id === backend) ??
-    servers[0] ??
+    servers.at(0) ??
     ({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const active =
servers.find((item) => item.id === backend) ??
servers[0] ??
const active =
servers.find((item) => item.id === backend) ??
servers.at(0) ??
🧰 Tools
🪛 ESLint

[error] 161-162: Unnecessary conditional, expected left-hand side of ?? operator to be possibly null or undefined.

(@typescript-eslint/no-unnecessary-condition)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ag-ui/src/App.tsx` around lines 160 - 162, Update the active server
selection expression near servers.find to use servers.at(0) instead of
servers[0], preserving the trailing default object fallback for empty server
arrays and eliminating the unnecessary-condition lint error.

Source: Linters/SAST tools

@AlemTuzlak
AlemTuzlak merged commit 994dc87 into main Jul 20, 2026
10 checks passed
@AlemTuzlak
AlemTuzlak deleted the ag-ui-example branch July 20, 2026 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants