Skip to content

docs: research AG-UI and MCP Apps frameworks and showcase opportunities - #259

Open
NickB03 wants to merge 10 commits into
mainfrom
claude/agui-mcp-framework-research-85jxvl
Open

docs: research AG-UI and MCP Apps frameworks and showcase opportunities#259
NickB03 wants to merge 10 commits into
mainfrom
claude/agui-mcp-framework-research-85jxvl

Conversation

@NickB03

@NickB03 NickB03 commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Add research doc mapping the AG-UI (Agent-User Interaction) protocol and the
MCP Apps (SEP-1865) / mcp-ui frameworks against Polymorph's existing
generative-UI, streaming, and tool-UI architecture. Identifies three
prioritized showcase initiatives (AG-UI server endpoint, MCP Apps host,
MCP Apps server) with concrete codebase integration points and a sequencing
recommendation.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6

Summary by CodeRabbit

  • New Features
    • Added an AG-UI demo page to run and observe the agent flow in the browser.
    • Added a streaming AG-UI endpoint that delivers live agent output via Server-Sent Events.
    • Added AG-UI client rendering that displays tool calls and generative UI components using a tool-UI registry.
  • Bug Fixes
    • Improved resilience to stream errors/invalid responses with safer, graceful failure.
    • Added labeled fallback rendering when a generative UI component can’t be rendered.
  • Documentation / Chores
    • Documented AG-UI endpoint behavior and configuration; updated graph refresh workflow scripts and tests.

claude added 7 commits June 28, 2026 17:11
Add research doc mapping the AG-UI (Agent-User Interaction) protocol and the
MCP Apps (SEP-1865) / mcp-ui frameworks against Polymorph's existing
generative-UI, streaming, and tool-UI architecture. Identifies three
prioritized showcase initiatives (AG-UI server endpoint, MCP Apps host,
MCP Apps server) with concrete codebase integration points and a sequencing
recommendation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6
Add POST /api/agui — an AG-UI (Agent-User Interaction protocol) endpoint that
runs Polymorph's existing chat agent and streams the result as AG-UI events
over SSE, letting any AG-UI-compatible frontend (e.g. CopilotKit) drive the
agent. This is Initiative B from docs/research/AGUI-MCP-APPS.md.

- lib/streaming/agui/adapter.ts: pure mapping — AG-UI RunAgentInput messages to
  AI SDK ModelMessage[], and AI SDK fullStream parts to AG-UI events
  (text/tool/step), with tool-call lifecycle dedup.
- lib/streaming/agui/response.ts: builds the agent statelessly (no DB/canvas),
  wraps the stream in RUN_STARTED/RUN_FINISHED/RUN_ERROR, encodes via
  @ag-ui/encoder for spec-compliant SSE framing.
- app/api/agui/route.ts: validates RunAgentInput with @ag-ui/core; gated behind
  ENABLE_AGUI_ENDPOINT (off by default — runs unauthenticated/stateless).
- Unit tests for the mapping layer; docs at docs/architecture/AGUI-ENDPOINT.md;
  env flag documented in .env.local.example.

Adds @ag-ui/core and @ag-ui/encoder dependencies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6
…rror mapping

- Split the agent-free SSE/lifecycle encoder into lib/streaming/agui/sse.ts
  (aguiSseResponse) so it can be unit-tested without loading the model/agent
  stack (importing the registry chain broke vitest collection via esbuild).
- Add sse.test.ts: verifies RUN_STARTED/RUN_FINISHED wrapping, event ordering,
  tool-call lifecycle dedup, and RUN_ERROR on both start-time and mid-stream
  failures, decoding real SSE frames.
- Fix: safeStringify now surfaces Error.message instead of '{}', so tool-error
  parts carry the real error text in TOOL_CALL_RESULT (caught by a new test).
- Add tool-error mapping test; update endpoint doc file map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6
Research-backed implementation plan for hosting MCP Apps in Polymorph. Key
finding: AI SDK v6 ships stable first-party MCP (@ai-sdk/mcp) plus an official
host renderer (experimental_MCPAppRenderer), so the sandboxed iframe + JSON-RPC
bridge are provided rather than hand-rolled. Recommends a dedicated runtime
allow-list of remote Streamable-HTTP MCP App servers (not the dev-only
.mcp.json, which is stdio), maps the integration onto existing seams
(createChatAgentTools, DynamicToolDisplay, useChat), and lays out a gated,
phased plan with an explicit security model. No code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6
… mode

Make the AG-UI endpoint demonstrably fully working end-to-end without an API key.

- sse.ts: an 'error' fullStream part is now terminal — emit RUN_ERROR and stop,
  with no trailing RUN_FINISHED (a run ends in exactly one terminal event). The
  catch-based RUN_ERROR for thrown errors is unchanged.
- agent.test.ts: end-to-end test driving a real ToolLoopAgent backed by
  MockLanguageModelV3 (no API key) through aguiSseResponse, asserting the full
  ordered lifecycle incl. a complete tool call + TOOL_CALL_RESULT.
- demo.ts + response.ts: gated AGUI_DEMO mode (AGUI_DEMO=true and not a
  production target) streams a scripted, model-free lifecycle so the endpoint
  can be exercised with no credentials. Verified in a browser: the full
  RUN_STARTED -> text -> TOOL_CALL_* + result -> RUN_FINISHED sequence renders
  from the live SSE stream.
- sse.test.ts: terminal-error test; docs updated for both behaviors + demo mode.

Gates: 15/15 agui tests, bun typecheck, bun lint all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6
Two directions completing the AG-UI integration:

MAPPING (display tools -> AG-UI generative UI):
- adapter.ts: a tool-call for a Polymorph display tool (name derived from
  TOOL_UI_TOOL_METADATA, the single source of truth) now also emits a CUSTOM
  event { name: 'GenerativeUI', value: { component, toolCallId, kind, props } }
  alongside the TOOL_CALL_* lifecycle, so AG-UI frontends can render the matching
  component. Emitted even when the tool-call input was streamed (deduped),
  without re-emitting the lifecycle. Non-UI tools emit no such event.
- demo.ts: scripted stream now also performs a displayPlan call so the demo
  exercises generative UI (browser-confirmed: CUSTOM GenerativeUI event appears).

CONSUMING (Polymorph as an AG-UI client):
- client.ts: consumeAguiStream(source) decodes an AG-UI SSE stream (Response /
  ReadableStream / async-iterable) via @ag-ui/core (no new dep) and reduces it
  into normalized assistant messages, tool calls, generative-UI components, and
  run status. The inverse of aguiSseResponse.
- client.test.ts: loopback round-trip (aguiSseResponse(demoFullStream()) ->
  consumeAguiStream) reconstructs text, tool call, result, the displayPlan
  generative-UI component, status 'finished'; plus a terminal-error case.

docs/architecture/AGUI-ENDPOINT.md updated (GenerativeUI mapping + Consuming
section). Gates: 20/20 agui tests, bun typecheck, bun lint all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6
Close the loop: a consumed AG-UI stream now renders as real Polymorph display
components, not raw event data.

- components/agui/agui-generative-ui.tsx: renders an AguiConsumeResult —
  assistant text, reconstructed tool calls, and each generativeUI item mapped
  through the tool-UI registry (isRegisteredToolUI + tryRenderToolUIByName), with
  a labeled fallback card when a component is unregistered or its props fail the
  registry's schema validation (no crash).
- components/agui/use-agui-agent.ts: useAguiAgent({ endpoint }) — POSTs a
  RunAgentInput and pipes the Response through consumeAguiStream into React state.
- demo.ts: displayPlan payload is now a valid SerializablePlan ({ id, title,
  todos[...] } with real status values) so the registry renders the actual Plan;
  client.test.ts assertions updated to match.
- components/agui/agui-generative-ui.test.tsx: consume the demo stream and assert
  the REAL Plan renders (title + a todo label) and the fallback is absent; plus
  an unregistered-component fallback case.
- app/agui-demo/page.tsx: gated dev demo page driving useAguiAgent against
  /api/agui (browser-verified: the live Plan renders from the consumed stream).
- docs updated: Frontend wiring section now documents the implemented components.

Gates: 22/22 agui+component tests, bun typecheck, bun lint all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6
@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
polymorph Ready Ready Preview, Comment Jun 29, 2026 2:32am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@NickB03, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 9 minutes. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 414d5296-c4f0-4f16-ad96-f6e8060d3788

📥 Commits

Reviewing files that changed from the base of the PR and between 90bed2f and 0a2dce7.

📒 Files selected for processing (1)
  • docs/research/AGUI-PORTFOLIO-PLAN.md
📝 Walkthrough

Walkthrough

Adds an AG-UI protocol pipeline: a gated POST /api/agui SSE endpoint, stream translation and consumption utilities, a client hook and renderer, a demo page and demo stream mode, plus related documentation, graph metadata, and tests.

Changes

AG-UI Protocol Integration

Layer / File(s) Summary
Stream translation
lib/streaming/agui/adapter.ts, lib/streaming/agui/adapter.test.ts
mapFullStreamPart translates AI SDK stream parts into AG-UI events, including text lifecycle, tool-call lifecycle with deduplication, generative UI custom events, tool results/errors, step events, and run errors. aguiMessagesToModelMessages maps AG-UI messages to AI SDK model messages.
SSE response streaming
lib/streaming/agui/sse.ts, lib/streaming/agui/sse.test.ts, lib/streaming/agui/demo.ts, lib/streaming/agui/response.ts, lib/streaming/agui/agent.test.ts
aguiSseResponse emits RUN_STARTED, streams mapped events, handles terminal errors, and finishes with RUN_FINISHED. demoFullStream() provides a scripted stream, and createAguiRunResponse() selects demo mode or live agent execution before returning the SSE response.
SSE consumption and rendering
lib/streaming/agui/client.ts, lib/streaming/agui/client.test.ts, components/agui/use-agui-agent.ts, components/agui/agui-generative-ui.tsx, components/agui/agui-generative-ui.test.tsx, app/agui-demo/page.tsx
consumeAguiStream parses AG-UI SSE frames into normalized messages, tool calls, and generative UI entries. useAguiAgent posts runs and tracks status/error/result, AguiGenerativeUI renders the result, and AguiDemoPage wires the flow into a dev page.
API route and demo entrypoint
app/api/agui/route.ts, .env.local.example, package.json
POST /api/agui gates execution with ENABLE_AGUI_ENDPOINT, validates RunAgentInput, and returns AG-UI SSE or JSON errors. The env template documents the endpoint, and the new AG-UI encoder/core dependencies are added.
Docs, graph freshness, and tooling
docs/architecture/AGUI-ENDPOINT.md, docs/research/AGUI-MCP-APPS.md, docs/research/MCP-APPS-HOST-PLAN.md, graphify-out/graph.json, .graphifyignore, .husky/pre-commit, AGENTS.md, scripts/refresh-graph.py, scripts/refresh-graph.sh
Adds endpoint, protocol, and host-plan documentation, plus graph nodes and refresh tooling for the new AG-UI integration surface.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop, hop—events now glide,
Through SSE streams they race and side by side,
A demo button sets them free,
With tool calls twinkling merrily,
The rabbit grins at AG-UI! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the PR’s focus on AG-UI and MCP Apps research plus showcase opportunities.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/agui-mcp-framework-research-85jxvl

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea1a83a54f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// assistant message (the one the model was emitting when it called the tool),
// falling back to a synthetic message if the stream was tool-calls-only.
if (toolCalls.size > 0) {
let host = messages.at(-1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve each tool call's owning message

When a run emits more than one assistant message, such as the multi-step ToolLoopAgent flow added in agent.test.ts where the first message calls a tool and a later message gives the final answer, this end-of-stream lookup attaches every reconstructed tool call to only the final message. That makes earlier assistant turns lose their tool calls and renders unrelated calls under the final answer; track the current/parent message when TOOL_CALL_START arrives instead of assigning all calls to messages.at(-1).

Useful? React with 👍 / 👎.


function* drain(final: boolean): Generator<BaseEvent> {
let index: number
while ((index = buffer.indexOf('\n\n')) !== -1) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept CRLF-delimited SSE frames

For AG-UI streams emitted with CRLF separators (\r\n\r\n), which are valid SSE and common with HTTP libraries, this delimiter search never drains frames until EOF. At EOF, multiple data: payloads get concatenated into one string and JSON.parse fails, so consumeAguiStream silently drops the whole run and returns an error result. Normalize line endings or detect \r\n\r\n as a frame boundary before parsing.

Useful? React with 👍 / 👎.

@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: 6

🧹 Nitpick comments (2)
.env.local.example (1)

123-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document AGUI_DEMO next to the endpoint toggle.

lib/streaming/agui/response.ts adds a second env gate for the scripted demo path, but this example only shows ENABLE_AGUI_ENDPOINT. Adding the dev-only AGUI_DEMO=true knob here will make the new demo flow much easier to discover.

🤖 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 @.env.local.example around lines 123 - 128, The AG-UI env example currently
documents only the endpoint toggle, but the demo path is also gated by AGUI_DEMO
in lib/streaming/agui/response.ts. Update the example next to
ENABLE_AGUI_ENDPOINT to also mention AGUI_DEMO=true as a dev-only knob, so the
scripted demo flow is easy to discover and configure.
lib/streaming/agui/response.ts (1)

26-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive VALID_USER_MODES from the shared source of truth.

This list duplicates UserMode from lib/types/search.ts:14. If a new mode is added there, resolveUserMode() will silently downgrade valid clients to 'search'. Export a shared constant/type guard from @/lib/types/search and reuse it here.

🤖 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 `@lib/streaming/agui/response.ts` around lines 26 - 45, VALID_USER_MODES is
duplicated in response.ts and can drift from the shared UserMode definition,
causing valid forwardedProps.userMode values to fall back to search. Update
resolveUserMode to use the shared source of truth from lib/types/search by
exporting and reusing a common constant or type guard, and keep the UserMode
check centralized so new modes are automatically accepted here.
🤖 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 `@app/api/agui/route.ts`:
- Around line 26-33: The request handler in route.ts parses JSON with req.json()
before any size check, which can buffer an oversized unauthenticated payload in
memory. Add a hard request-size guard in the AGUI route before the req.json()
call in the handler that uses RunAgentInputSchema, and return BAD_REQUEST when
the body exceeds the limit. Also make sure the same limit is enforced at the
gateway so oversized POSTs are rejected before reaching this endpoint.

In `@components/agui/agui-generative-ui.tsx`:
- Around line 95-99: The render path is too restrictive because
`AguiGenerativeUI` only calls `tryRenderToolUIByName()` when
`isRegisteredToolUI(component)` is true, which prevents the registry from
handling additional generic renderers. Update this branch so the registry helper
can make the renderability decision itself, likely by delegating directly
through `tryRenderToolUIByName()` (or the related registry entry point) for
`component`/`props`/`partId` without the extra `isRegisteredToolUI` guard, while
keeping the fallback to `null` only when the registry truly cannot render.

In `@components/agui/use-agui-agent.ts`:
- Around line 44-46: The new run flow in useAguiAgent resets error but leaves
the previous result visible, so clear the stored result at the same time you
call setStatus('running') and setError(null). Update both run paths referenced
by the comment so the state is reset before invoking the AG-UI request,
preventing app/agui-demo/page.tsx from rendering a stale payload after a failed
retry.

In `@lib/streaming/agui/client.ts`:
- Around line 113-132: The SSE frame splitting in drain() only matches "\n\n",
so CRLF-delimited AG-UI frames are not separated correctly. Update the frame
detection in drain() (and any related buffer trimming in the streaming loop) to
handle "\r\n\r\n" as well as "\n\n", so parseFrame() receives one complete
payload per event instead of concatenated JSON.
- Around line 192-254: The tool-call buffering in the AG-UI client is attaching
all calls to messages.at(-1) at the end, which can misattribute earlier calls to
the wrong assistant message. Update the event reducer in client.ts to track
ownership at TOOL_CALL_START by recording the current assistant message (or a
message identifier/host reference) alongside each toolCallId, then use that
stored association when appending calls instead of always using the last
message. Keep the change localized to the toolCalls assembly logic and the final
attachment block so each call is pushed onto the message that emitted it.

In `@lib/streaming/agui/sse.ts`:
- Around line 34-69: The SSE ReadableStream in sse.ts does not stop the upstream
run when the client disconnects because it only defines start() and never
handles cancel(). Add a cancel(reason) handler on the ReadableStream to abort
the same signal passed into startRun(options.abortSignal), and make sure the
stream cleanup prevents further send() calls after cancellation. Use the
existing startRun and controller logic in the stream setup to wire the
disconnect through so the model stream stops as soon as the SSE connection is
dropped.

---

Nitpick comments:
In @.env.local.example:
- Around line 123-128: The AG-UI env example currently documents only the
endpoint toggle, but the demo path is also gated by AGUI_DEMO in
lib/streaming/agui/response.ts. Update the example next to ENABLE_AGUI_ENDPOINT
to also mention AGUI_DEMO=true as a dev-only knob, so the scripted demo flow is
easy to discover and configure.

In `@lib/streaming/agui/response.ts`:
- Around line 26-45: VALID_USER_MODES is duplicated in response.ts and can drift
from the shared UserMode definition, causing valid forwardedProps.userMode
values to fall back to search. Update resolveUserMode to use the shared source
of truth from lib/types/search by exporting and reusing a common constant or
type guard, and keep the UserMode check centralized so new modes are
automatically accepted here.
🪄 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: d986dbc3-7c29-48e2-9d07-a1427ba11128

📥 Commits

Reviewing files that changed from the base of the PR and between 2bbf2a0 and ea1a83a.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .env.local.example
  • app/agui-demo/page.tsx
  • app/api/agui/route.ts
  • components/agui/agui-generative-ui.test.tsx
  • components/agui/agui-generative-ui.tsx
  • components/agui/use-agui-agent.ts
  • docs/architecture/AGUI-ENDPOINT.md
  • docs/research/AGUI-MCP-APPS.md
  • docs/research/MCP-APPS-HOST-PLAN.md
  • lib/streaming/agui/adapter.test.ts
  • lib/streaming/agui/adapter.ts
  • lib/streaming/agui/agent.test.ts
  • lib/streaming/agui/client.test.ts
  • lib/streaming/agui/client.ts
  • lib/streaming/agui/demo.ts
  • lib/streaming/agui/response.ts
  • lib/streaming/agui/sse.test.ts
  • lib/streaming/agui/sse.ts
  • package.json

Comment thread app/api/agui/route.ts
Comment on lines +26 to +33
let body: unknown
try {
body = await req.json()
} catch {
return jsonError('BAD_REQUEST', 'Invalid JSON body', 400)
}

const parsed = RunAgentInputSchema.safeParse(body)

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

Reject oversized bodies before buffering JSON.

await req.json() loads the entire unauthenticated conversation history into memory before any validation. On this endpoint, one oversized POST can tie up a worker or exhaust memory on self-hosted deployments. Add a hard size guard before calling req.json() and mirror that limit at the gateway.

Suggested guard
+const MAX_AGUI_BODY_BYTES = 1024 * 1024
+
 export async function POST(req: Request) {
   if (process.env.ENABLE_AGUI_ENDPOINT !== 'true') {
     return jsonError('NOT_FOUND', 'AG-UI endpoint is not enabled', 404)
   }
 
+  const contentLength = Number(req.headers.get('content-length') ?? '0')
+  if (Number.isFinite(contentLength) && contentLength > MAX_AGUI_BODY_BYTES) {
+    return jsonError('PAYLOAD_TOO_LARGE', 'Request body is too large', 413)
+  }
+
   let body: unknown
   try {
     body = await req.json()
📝 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
let body: unknown
try {
body = await req.json()
} catch {
return jsonError('BAD_REQUEST', 'Invalid JSON body', 400)
}
const parsed = RunAgentInputSchema.safeParse(body)
const MAX_AGUI_BODY_BYTES = 1024 * 1024
let body: unknown
try {
const contentLength = Number(req.headers.get('content-length') ?? '0')
if (Number.isFinite(contentLength) && contentLength > MAX_AGUI_BODY_BYTES) {
return jsonError('PAYLOAD_TOO_LARGE', 'Request body is too large', 413)
}
body = await req.json()
} catch {
return jsonError('BAD_REQUEST', 'Invalid JSON body', 400)
}
const parsed = RunAgentInputSchema.safeParse(body)
🤖 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 `@app/api/agui/route.ts` around lines 26 - 33, The request handler in route.ts
parses JSON with req.json() before any size check, which can buffer an oversized
unauthenticated payload in memory. Add a hard request-size guard in the AGUI
route before the req.json() call in the handler that uses RunAgentInputSchema,
and return BAD_REQUEST when the body exceeds the limit. Also make sure the same
limit is enforced at the gateway so oversized POSTs are rejected before reaching
this endpoint.

Comment on lines +95 to +99
let rendered: ReactNode = null
if (isRegisteredToolUI(component)) {
rendered = tryRenderToolUIByName(component, props, partId)
}

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

Let the registry decide whether a component is renderable.

This guard bypasses tryRenderToolUIByName() for anything not in isRegisteredToolUI(), but the registry helper also supports additional and generic renderers. Those components will incorrectly fall back even though components/tool-ui/registry.tsx can render them.

Suggested fix
-  let rendered: ReactNode = null
-  if (isRegisteredToolUI(component)) {
-    rendered = tryRenderToolUIByName(component, props, partId)
-  }
+  const rendered: ReactNode = tryRenderToolUIByName(component, props, partId)
📝 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
let rendered: ReactNode = null
if (isRegisteredToolUI(component)) {
rendered = tryRenderToolUIByName(component, props, partId)
}
const rendered: ReactNode = tryRenderToolUIByName(component, props, partId)
🤖 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 `@components/agui/agui-generative-ui.tsx` around lines 95 - 99, The render path
is too restrictive because `AguiGenerativeUI` only calls
`tryRenderToolUIByName()` when `isRegisteredToolUI(component)` is true, which
prevents the registry from handling additional generic renderers. Update this
branch so the registry helper can make the renderability decision itself, likely
by delegating directly through `tryRenderToolUIByName()` (or the related
registry entry point) for `component`/`props`/`partId` without the extra
`isRegisteredToolUI` guard, while keeping the fallback to `null` only when the
registry truly cannot render.

Comment on lines +44 to +46
setStatus('running')
setError(null)

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

Clear the previous result before starting a new run.

This resets error but not result. If a second run fails, the UI keeps rendering the last successful AG-UI payload because app/agui-demo/page.tsx shows result independently of status.

Suggested fix
     async (input: unknown): Promise<AguiConsumeResult | null> => {
       setStatus('running')
       setError(null)
+      setResult(null)
@@
       } catch (caught) {
         const message =
           caught instanceof Error ? caught.message : 'AG-UI run failed'
+        setResult(null)
         setError(message)
         setStatus('error')
         return null
       }

Also applies to: 65-70

🤖 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 `@components/agui/use-agui-agent.ts` around lines 44 - 46, The new run flow in
useAguiAgent resets error but leaves the previous result visible, so clear the
stored result at the same time you call setStatus('running') and setError(null).
Update both run paths referenced by the comment so the state is reset before
invoking the AG-UI request, preventing app/agui-demo/page.tsx from rendering a
stale payload after a failed retry.

Comment on lines +113 to +132
function* drain(final: boolean): Generator<BaseEvent> {
let index: number
while ((index = buffer.indexOf('\n\n')) !== -1) {
const frame = buffer.slice(0, index)
buffer = buffer.slice(index + 2)
const event = parseFrame(frame)
if (event) yield event
}
if (final && buffer.trim().length > 0) {
const event = parseFrame(buffer)
if (event) yield event
buffer = ''
}
}

for await (const chunk of sourceToTextChunks(source)) {
buffer += chunk
yield* drain(false)
}
yield* drain(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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle CRLF-delimited SSE frames.

drain() only looks for '\n\n'. If an AG-UI server sends blank lines as '\r\n\r\n', the buffer never splits into frames; drain(true) then hands multiple JSON payloads to one JSON.parse() and drops the whole stream.

Suggested fix
 async function* decodeAguiEvents(
   source: AguiStreamSource
 ): AsyncIterable<BaseEvent> {
   let buffer = ''

   function* drain(final: boolean): Generator<BaseEvent> {
     let index: number
-    while ((index = buffer.indexOf('\n\n')) !== -1) {
+    while ((index = buffer.search(/\r?\n\r?\n/)) !== -1) {
       const frame = buffer.slice(0, index)
-      buffer = buffer.slice(index + 2)
+      const separator = buffer.slice(index).match(/^\r?\n\r?\n/)?.[0] ?? '\n\n'
+      buffer = buffer.slice(index + separator.length)
       const event = parseFrame(frame)
       if (event) yield event
     }
📝 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* drain(final: boolean): Generator<BaseEvent> {
let index: number
while ((index = buffer.indexOf('\n\n')) !== -1) {
const frame = buffer.slice(0, index)
buffer = buffer.slice(index + 2)
const event = parseFrame(frame)
if (event) yield event
}
if (final && buffer.trim().length > 0) {
const event = parseFrame(buffer)
if (event) yield event
buffer = ''
}
}
for await (const chunk of sourceToTextChunks(source)) {
buffer += chunk
yield* drain(false)
}
yield* drain(true)
function* drain(final: boolean): Generator<BaseEvent> {
let index: number
while ((index = buffer.search(/\r?\n\r?\n/)) !== -1) {
const frame = buffer.slice(0, index)
const separator = buffer.slice(index).match(/^\r?\n\r?\n/)?.[0] ?? '\n\n'
buffer = buffer.slice(index + separator.length)
const event = parseFrame(frame)
if (event) yield event
}
if (final && buffer.trim().length > 0) {
const event = parseFrame(buffer)
if (event) yield event
buffer = ''
}
}
for await (const chunk of sourceToTextChunks(source)) {
buffer += chunk
yield* drain(false)
}
yield* drain(true)
🤖 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 `@lib/streaming/agui/client.ts` around lines 113 - 132, The SSE frame splitting
in drain() only matches "\n\n", so CRLF-delimited AG-UI frames are not separated
correctly. Update the frame detection in drain() (and any related buffer
trimming in the streaming loop) to handle "\r\n\r\n" as well as "\n\n", so
parseFrame() receives one complete payload per event instead of concatenated
JSON.

Comment on lines +192 to +254
case EventType.TOOL_CALL_START: {
const start = event as ToolCallStartEvent
toolCalls.set(start.toolCallId, {
toolCallId: start.toolCallId,
name: start.toolCallName,
args: ''
})
break
}
case EventType.TOOL_CALL_ARGS: {
const argsEvent = event as ToolCallArgsEvent
const call = toolCalls.get(argsEvent.toolCallId)
if (call) call.args += argsEvent.delta
break
}
case EventType.TOOL_CALL_RESULT: {
const result = event as ToolCallResultEvent
const call = toolCalls.get(result.toolCallId)
if (call) call.result = result.content
break
}

case EventType.CUSTOM: {
const custom = event as CustomEvent
if (custom.name === 'GenerativeUI') {
const value = (custom.value ?? {}) as Partial<AguiGenerativeUi>
generativeUI.push({
component: String(value.component ?? ''),
toolCallId: String(value.toolCallId ?? ''),
kind: value.kind,
props: value.props
})
}
break
}

case EventType.RUN_FINISHED:
status = 'finished'
break
case EventType.RUN_ERROR:
status = 'error'
error = (event as RunErrorEvent).message
break

// TOOL_CALL_END, TEXT_MESSAGE_END, lifecycle/step events carry no state
// we need to reduce here.
default:
break
}
}

// Attach assembled tool calls to their owning assistant message. AG-UI tool
// calls aren't tagged with a parent messageId, so attach to the most recent
// assistant message (the one the model was emitting when it called the tool),
// falling back to a synthetic message if the stream was tool-calls-only.
if (toolCalls.size > 0) {
let host = messages.at(-1)
if (!host) {
host = { id: 'assistant', role: 'assistant', text: '', toolCalls: [] }
messages.push(host)
}
host.toolCalls.push(...toolCalls.values())
}

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

Track tool-call ownership when each call starts.

All tool calls are buffered globally and attached once to messages.at(-1) at the end of the run. In any stream with multiple assistant messages, earlier tool calls will be rendered under the last message instead of the message that emitted them.

Suggested fix
   const messages: AguiAssistantMessage[] = []
   const messagesById = new Map<string, AguiAssistantMessage>()
   const toolCalls = new Map<string, AguiToolCall>()
+  const toolCallHosts = new Map<string, AguiAssistantMessage>()
   const generativeUI: AguiGenerativeUi[] = []

   let status: AguiConsumeResult['status'] = 'error'
   let error: string | undefined
+  let currentMessage: AguiAssistantMessage | undefined
@@
       case EventType.TEXT_MESSAGE_START:
-        messageFor((event as TextMessageStartEvent).messageId)
+        currentMessage = messageFor((event as TextMessageStartEvent).messageId)
         break
       case EventType.TEXT_MESSAGE_CONTENT: {
         const content = event as TextMessageContentEvent
-        messageFor(content.messageId).text += content.delta
+        currentMessage = messageFor(content.messageId)
+        currentMessage.text += content.delta
         break
       }
@@
       case EventType.TOOL_CALL_START: {
         const start = event as ToolCallStartEvent
-        toolCalls.set(start.toolCallId, {
+        const call = {
           toolCallId: start.toolCallId,
           name: start.toolCallName,
           args: ''
-        })
+        }
+        toolCalls.set(start.toolCallId, call)
+        const host =
+          currentMessage ??
+          messages.at(-1) ??
+          messageFor(`assistant-${messages.length + 1}`)
+        host.toolCalls.push(call)
+        toolCallHosts.set(start.toolCallId, host)
         break
       }
@@
-  if (toolCalls.size > 0) {
-    let host = messages.at(-1)
-    if (!host) {
-      host = { id: 'assistant', role: 'assistant', text: '', toolCalls: [] }
-      messages.push(host)
-    }
-    host.toolCalls.push(...toolCalls.values())
-  }
-
   return { status, error, messages, generativeUI }
 }
🤖 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 `@lib/streaming/agui/client.ts` around lines 192 - 254, The tool-call buffering
in the AG-UI client is attaching all calls to messages.at(-1) at the end, which
can misattribute earlier calls to the wrong assistant message. Update the event
reducer in client.ts to track ownership at TOOL_CALL_START by recording the
current assistant message (or a message identifier/host reference) alongside
each toolCallId, then use that stored association when appending calls instead
of always using the last message. Keep the change localized to the toolCalls
assembly logic and the final attachment block so each call is pushed onto the
message that emitted it.

Comment thread lib/streaming/agui/sse.ts
Comment on lines +34 to +69
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
const send = (event: BaseEvent) =>
controller.enqueue(textEncoder.encode(encoder.encodeSSE(event)))

send({ type: EventType.RUN_STARTED, threadId, runId } as BaseEvent)

try {
const fullStream = await startRun(options.abortSignal)
const state = createAguiMapState()
let terminalError = false
outer: for await (const part of fullStream) {
for (const event of mapFullStreamPart(part, state)) {
send(event)
// An `error` fullStream part maps to RUN_ERROR, which terminates
// the run: stop consuming and do not emit RUN_FINISHED.
if (event.type === EventType.RUN_ERROR) {
terminalError = true
break outer
}
}
}

if (!terminalError) {
send({ type: EventType.RUN_FINISHED, threadId, runId } as BaseEvent)
}
} catch (error) {
send({
type: EventType.RUN_ERROR,
message: getErrorMessage(error)
} as BaseEvent)
} finally {
controller.close()
}
}
})

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

Abort the upstream run when the SSE client disconnects.

This ReadableStream never implements cancel(), so a dropped SSE connection does not abort startRun. The model stream can keep running after the client is gone, and later writes can hit a canceled controller.

Proposed fix
 export function aguiSseResponse(
   startRun: FullStreamFactory,
   ids: { threadId: string; runId: string },
   options: { abortSignal?: AbortSignal } = {}
 ): Response {
   const { threadId, runId } = ids
   const encoder = new EventEncoder()
   const textEncoder = new TextEncoder()
+  const upstreamAbortController = new AbortController()
+
+  const forwardAbort = () => {
+    upstreamAbortController.abort(options.abortSignal?.reason)
+  }
+
+  if (options.abortSignal) {
+    if (options.abortSignal.aborted) {
+      forwardAbort()
+    } else {
+      options.abortSignal.addEventListener('abort', forwardAbort, {
+        once: true
+      })
+    }
+  }

   const stream = new ReadableStream<Uint8Array>({
     async start(controller) {
       const send = (event: BaseEvent) =>
         controller.enqueue(textEncoder.encode(encoder.encodeSSE(event)))

       send({ type: EventType.RUN_STARTED, threadId, runId } as BaseEvent)

       try {
-        const fullStream = await startRun(options.abortSignal)
+        const fullStream = await startRun(upstreamAbortController.signal)
         const state = createAguiMapState()
         let terminalError = false
         outer: for await (const part of fullStream) {
           for (const event of mapFullStreamPart(part, state)) {
             send(event)
             if (event.type === EventType.RUN_ERROR) {
               terminalError = true
               break outer
             }
           }
         }

         if (!terminalError) {
           send({ type: EventType.RUN_FINISHED, threadId, runId } as BaseEvent)
         }
       } catch (error) {
         send({
           type: EventType.RUN_ERROR,
           message: getErrorMessage(error)
         } as BaseEvent)
       } finally {
+        options.abortSignal?.removeEventListener('abort', forwardAbort)
         controller.close()
       }
+    },
+    cancel() {
+      upstreamAbortController.abort()
     }
   })
📝 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 stream = new ReadableStream<Uint8Array>({
async start(controller) {
const send = (event: BaseEvent) =>
controller.enqueue(textEncoder.encode(encoder.encodeSSE(event)))
send({ type: EventType.RUN_STARTED, threadId, runId } as BaseEvent)
try {
const fullStream = await startRun(options.abortSignal)
const state = createAguiMapState()
let terminalError = false
outer: for await (const part of fullStream) {
for (const event of mapFullStreamPart(part, state)) {
send(event)
// An `error` fullStream part maps to RUN_ERROR, which terminates
// the run: stop consuming and do not emit RUN_FINISHED.
if (event.type === EventType.RUN_ERROR) {
terminalError = true
break outer
}
}
}
if (!terminalError) {
send({ type: EventType.RUN_FINISHED, threadId, runId } as BaseEvent)
}
} catch (error) {
send({
type: EventType.RUN_ERROR,
message: getErrorMessage(error)
} as BaseEvent)
} finally {
controller.close()
}
}
})
const upstreamAbortController = new AbortController()
const forwardAbort = () => {
upstreamAbortController.abort(options.abortSignal?.reason)
}
if (options.abortSignal) {
if (options.abortSignal.aborted) {
forwardAbort()
} else {
options.abortSignal.addEventListener('abort', forwardAbort, {
once: true
})
}
}
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
const send = (event: BaseEvent) =>
controller.enqueue(textEncoder.encode(encoder.encodeSSE(event)))
send({ type: EventType.RUN_STARTED, threadId, runId } as BaseEvent)
try {
const fullStream = await startRun(upstreamAbortController.signal)
const state = createAguiMapState()
let terminalError = false
outer: for await (const part of fullStream) {
for (const event of mapFullStreamPart(part, state)) {
send(event)
// An `error` fullStream part maps to RUN_ERROR, which terminates
// the run: stop consuming and do not emit RUN_FINISHED.
if (event.type === EventType.RUN_ERROR) {
terminalError = true
break outer
}
}
}
if (!terminalError) {
send({ type: EventType.RUN_FINISHED, threadId, runId } as BaseEvent)
}
} catch (error) {
send({
type: EventType.RUN_ERROR,
message: getErrorMessage(error)
} as BaseEvent)
} finally {
options.abortSignal?.removeEventListener('abort', forwardAbort)
controller.close()
}
},
cancel() {
upstreamAbortController.abort()
}
})
🤖 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 `@lib/streaming/agui/sse.ts` around lines 34 - 69, The SSE ReadableStream in
sse.ts does not stop the upstream run when the client disconnects because it
only defines start() and never handles cancel(). Add a cancel(reason) handler on
the ReadableStream to abort the same signal passed into
startRun(options.abortSignal), and make sure the stream cleanup prevents further
send() calls after cancellation. Use the existing startRun and controller logic
in the stream setup to wire the disconnect through so the model stream stops as
soon as the SSE connection is dropped.

The graph-freshness CI guard (scripts/check-graph-freshness.py) failed because
the AG-UI work added 14 code files + 2 dependencies without updating the
committed knowledge graph. Append the 60 new AST symbols (lib/streaming/agui/*,
components/agui/*, app/agui-demo, app/api/agui, and the @ag-ui/core /
@ag-ui/encoder package.json deps) to graphify-out/graph.json so its AST symbol
set matches the code. Purely additive; existing nodes, links, and the curated
report/labels are unchanged. Verified the freshness check passes in a simulated
bare checkout (the only local diffs were gitignored generated files —
next-env.d.ts and .husky/_/husky.sh — which CI's checkout does not contain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 911f95999c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// AG-UI assistant content may be string | InputContent[]; only forward
// plain-text turns. system/user content is already string-typed.
const content = message.content
if (typeof content !== 'string' || content.length === 0) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve text parts from multimodal AG-UI messages

When an AG-UI client sends a valid user message whose content is an InputContent[] — for example text represented as [{ type: 'text', text: '...' }] alongside image/file parts — this guard drops the entire turn before calling the chat agent. The endpoint then runs with an empty or truncated history even though the request is protocol-valid, so compatible AG-UI frontends lose the user's prompt; concatenate text parts and map supported media instead of skipping all non-string content.

Useful? React with 👍 / 👎.

Comment on lines +236 to +239
// TOOL_CALL_END, TEXT_MESSAGE_END, lifecycle/step events carry no state
// we need to reduce here.
default:
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle AG-UI chunk events in the reducer

When an external AG-UI agent emits TEXT_MESSAGE_CHUNK or TOOL_CALL_CHUNK events, those standard @ag-ui/core events fall through this default branch because this hand-rolled consumer only handles the expanded start/content/end lifecycle. That yields a finished run with missing assistant text or tool calls for agents that use the chunk convenience events; expand these events here or run the stream through the AG-UI client normalizer before reducing it.

Useful? React with 👍 / 👎.

…stops blocking PRs

Make the CI 'Graph Freshness' guard self-maintaining instead of a recurring
manual chore, and fix two reproducibility traps that made it fail even after a
naive refresh.

- scripts/refresh-graph.py: deterministic AST-only refresh that mirrors the
  guard's own detect()+extract() and reconciles graph.json's AST nodes on
  (source_file, label) — the exact key the guard compares (NOT node id, which is
  path-derived and environment-dependent). Passes the guard by construction;
  preserves semantic nodes, links, and the curated report. Deliberately not
  bare Re-extracting code files in . (no LLM needed)...
  AST extraction: 100/847 uncached files (11%) [4 workers]
  AST extraction: 200/847 uncached files (23%) [4 workers]
  AST extraction: 300/847 uncached files (35%) [4 workers]
  AST extraction: 400/847 uncached files (47%) [4 workers]
  AST extraction: 500/847 uncached files (59%) [4 workers]
  AST extraction: 600/847 uncached files (70%) [4 workers]
  AST extraction: 700/847 uncached files (82%) [4 workers]
  AST extraction: 800/847 uncached files (94%) [4 workers]
  AST extraction: 987/987 files (100%) [4 workers]
[graphify] backed up curated graph (4 files) -> 2026-06-29/
[graphify watch] Skipped graph.html: Graph has 5312 nodes - too large for HTML viz (limit: 5000). Use --no-viz, raise GRAPHIFY_VIZ_NODE_LIMIT, or reduce input size.
[graphify watch] Rebuilt: 5312 nodes, 10891 edges, 332 communities
[graphify watch] graph.json and GRAPH_REPORT.md updated in graphify-out
Code graph updated. For doc/paper/image changes run /graphify --update in your AI assistant.
Tip: set GEMINI_API_KEY or GOOGLE_API_KEY to use Gemini for semantic extraction., which does a full rebuild that re-ASTs docs and
  diverges from the curated graph.
- scripts/refresh-graph.sh: wrapper that resolves a graphify-capable interpreter
  and skips gracefully (exit 0) when graphify isn't installed, so it never
  blocks contributors without the Python tooling — CI stays the backstop.
- .husky/pre-commit: run the refresh and stage graph.json when code is staged.
- .graphifyignore: exclude next-env.d.ts and .husky/_/ (generated files that
  exist locally after install/build but not in CI's bare checkout — graphing
  them made the guard environment-dependent). Also exclude the new scripts.
- AGENTS.md: point the 'after modifying code' step at scripts/refresh-graph.sh
  and warn off the divergent Re-extracting code files in . (no LLM needed)...
  AST extraction: 100/847 uncached files (11%) [4 workers]
  AST extraction: 200/847 uncached files (23%) [4 workers]
  AST extraction: 300/847 uncached files (35%) [4 workers]
  AST extraction: 400/847 uncached files (47%) [4 workers]
  AST extraction: 500/847 uncached files (59%) [4 workers]
  AST extraction: 600/847 uncached files (70%) [4 workers]
  AST extraction: 700/847 uncached files (82%) [4 workers]
  AST extraction: 800/847 uncached files (94%) [4 workers]
  AST extraction: 987/987 files (100%) [4 workers]
[graphify watch] No code-graph topology changes detected; outputs left untouched.
Code graph updated. For doc/paper/image changes run /graphify --update in your AI assistant.
Tip: set GEMINI_API_KEY or GOOGLE_API_KEY to use Gemini for semantic extraction..

Verified end-to-end: adding/removing a code symbol refreshes the graph and the
guard passes both ways; the no-graphify path skips cleanly; format:check passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6

@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: 2

🤖 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 `@scripts/refresh-graph.py`:
- Around line 104-108: The new-node append loop in refresh-graph.py uses
add_sigs directly, which is a set and produces nondeterministic ordering. Update
the add_sigs handling in the node-building block so the entries are processed in
a stable sorted order before appending AST nodes to g["nodes"], preserving
deterministic graph.json output and minimal diffs across runs.

In `@scripts/refresh-graph.sh`:
- Around line 6-8: The local graph refresh flow in refresh-graph.sh is using an
unpinned graphifyy install, which can diverge from CI. Update the script’s
install/run guidance so it explicitly uses graphifyy version 0.8.38, matching
the freshness check in CI, and make sure both the fallback local tool invocation
and any install instructions reference that same pinned version.
🪄 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: 448e7312-6b43-4d26-b8e1-851669b75b52

📥 Commits

Reviewing files that changed from the base of the PR and between 911f959 and 90bed2f.

📒 Files selected for processing (5)
  • .graphifyignore
  • .husky/pre-commit
  • AGENTS.md
  • scripts/refresh-graph.py
  • scripts/refresh-graph.sh
✅ Files skipped from review due to trivial changes (3)
  • .husky/pre-commit
  • .graphifyignore
  • AGENTS.md

Comment thread scripts/refresh-graph.py
Comment on lines +104 to +108
if add_sigs:
new_community = (
max((n.get("community", 0) for n in g["nodes"]), default=0) + 1
)
for sig in add_sigs:

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

Sort add_sigs before appending new nodes.

add_sigs is a set, so Line 108 appends new AST nodes in hash order. That makes graphify-out/graph.json ordering vary across runs and undercuts the “diff stays minimal” goal in Lines 124-125.

Proposed fix
-        for sig in add_sigs:
+        for sig in sorted(add_sigs):
             n = fresh_by_sig[sig]
             g["nodes"].append(
📝 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 add_sigs:
new_community = (
max((n.get("community", 0) for n in g["nodes"]), default=0) + 1
)
for sig in add_sigs:
if add_sigs:
new_community = (
max((n.get("community", 0) for n in g["nodes"]), default=0) + 1
)
for sig in sorted(add_sigs):
🤖 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 `@scripts/refresh-graph.py` around lines 104 - 108, The new-node append loop in
refresh-graph.py uses add_sigs directly, which is a set and produces
nondeterministic ordering. Update the add_sigs handling in the node-building
block so the entries are processed in a stable sorted order before appending AST
nodes to g["nodes"], preserving deterministic graph.json output and minimal
diffs across runs.

Comment thread scripts/refresh-graph.sh
Comment on lines +6 to +8
# Skips gracefully (exit 0) when graphify isn't installed, so it never blocks a
# commit/push for contributors without the Python tooling — CI remains the
# backstop. Install graphify with: uv tool install graphifyy

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:

#!/usr/bin/env bash
set -euo pipefail

rg -n "graphifyy|graphify" .github/workflows/ci.yml scripts/refresh-graph.sh

python - <<'PY'
from pathlib import Path
import re

ci = Path(".github/workflows/ci.yml").read_text()
sh = Path("scripts/refresh-graph.sh").read_text()

ci_pin = re.search(r"graphifyy==([0-9.]+)", ci)
print("CI graphifyy pin:", ci_pin.group(1) if ci_pin else "<missing>")
print("refresh-graph.sh uses exact pin:", "graphifyy==" in sh)
PY

Repository: NickB03/polymorph

Length of output: 1678


Pin graphifyy to 0.8.38. scripts/refresh-graph.sh can use any local graphifyy install, or uv tool run --from graphifyy, while CI uses graphifyy==0.8.38. That mismatch can make a local refresh pass and still fail the freshness check in CI.

🤖 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 `@scripts/refresh-graph.sh` around lines 6 - 8, The local graph refresh flow in
refresh-graph.sh is using an unpinned graphifyy install, which can diverge from
CI. Update the script’s install/run guidance so it explicitly uses graphifyy
version 0.8.38, matching the freshness check in CI, and make sure both the
fallback local tool invocation and any install instructions reference that same
pinned version.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90bed2f657

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{ type: EventType.TEXT_MESSAGE_END, messageId: part.id } as BaseEvent
]

case 'tool-input-start':

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle AI SDK tool-call streaming parts

When tool-call input is streamed by the AI SDK, the fullStream parts are emitted as tool-call-streaming-start / tool-call-delta with toolCallId and argsTextDelta (checked the AI SDK v6 streamText fullStream reference), so these tool-input-* branches never run for real streamed tool arguments. In that case AG-UI clients get no live TOOL_CALL_START/TOOL_CALL_ARGS updates and only see the expanded final tool-call after input parsing completes, which defeats the endpoint's advertised streaming tool-call lifecycle.

Useful? React with 👍 / 👎.

Cross-repo plan (Polymorph + nickb.net) to replace the portfolio's basic Q&A
chat with the live Polymorph agent over AG-UI, so the site itself demos
Polymorph. Grounded in this repo: the gating prerequisite is a secured public
/api/agui (bearer auth, Upstash rate-limiting + a global daily budget, a
bounded/cheap public agent, AGUI_DEMO as the $0 fallback). Recommends a
server-side proxy on nickb.net (keeps the key server-side, no CORS) and a
3-phase rollout (secure+embed -> demo generative UI -> agent drives the site).
No code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w4vB2hXtTZ9hViYxqeTE6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a2dce77ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

intent
})

const modelMessages = aguiMessagesToModelMessages(input.messages)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prune AG-UI history before streaming

When an AG-UI client sends the full thread history on each stateless run, this passes every converted message directly into agent.stream; unlike the existing chat and guest streaming paths, it never calls pruneMessages or maybeTruncateMessages, so a normal long conversation can exceed the selected model's context window and end as RUN_ERROR instead of producing a response. Apply the same pruning/truncation step before starting the agent stream.

Useful? React with 👍 / 👎.

Comment thread app/api/agui/route.ts
* environments where that cost/abuse surface is acceptable (e.g. behind a
* gateway or for a controlled demo).
*/
export async function POST(req: Request) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add CORS handling for external AG-UI clients

If the AG-UI frontend is served from a different origin, which this endpoint advertises by targeting external frontends such as CopilotKit/Dojo, the browser will preflight the JSON POST and require Access-Control-Allow-* headers. This route only implements POST and returns no CORS headers, and there is no shared CORS handling elsewhere in the app, so those clients are blocked before the SSE stream can start; add an OPTIONS handler and a configurable allowed-origin policy for this endpoint.

Useful? React with 👍 / 👎.

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