Skip to content

feat(trigger-chat-agent): AI chat agent that teaches Trigger.dev by drawing - #126

Open
D-K-P wants to merge 31 commits into
mainfrom
add-trigger-chat-agent-example
Open

feat(trigger-chat-agent): AI chat agent that teaches Trigger.dev by drawing#126
D-K-P wants to merge 31 commits into
mainfrom
add-trigger-chat-agent-example

Conversation

@D-K-P

@D-K-P D-K-P commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

A new example: a Trigger.dev AI Chat agent that teaches you Trigger.dev by drawing instead of dumping paragraphs. Ask how a fan-out with retries works and you get an interactive React Flow node-graph; ask about retries and you get a short explainer, a quiz, and a gotcha callout. Every turn ends with clickable next-step chips so the learning keeps going.

How it's built

The agent is a single chat.agent() task, so the conversation is a durable run that survives redeploys and crashes with no database. It answers by calling a renderVisualization tool with a json-render spec built from a fixed component catalog (FlowGraph, Quiz, Callout, CodeCard, and more), which the Next.js frontend renders live with React Flow and shadcn/ui. The model supplies data, not markup, so every card is cheap and always well-formed.

Facts are grounded on the live docs through a documentation MCP server, and retrieved docs are quarantined as untrusted input before the model ever sees them. The system prompt is a versioned AI Prompt editable from the dashboard, and model calls emit standard GenAI telemetry spans, so you get token, cost and latency observability for free.

Also updates the root examples table with the new entry.

Summary by CodeRabbit

  • New Features
    • Added a Trigger.dev chat tutor example with interactive conversations and streaming responses.
    • Added rich learning content, including flow diagrams, code cards, quizzes, statistics, prompts, glossaries, and step-by-step guides.
    • Added live documentation grounding and suggested follow-up questions.
    • Added responsive dark-themed presentation with animations, accessible error handling, and reduced-motion support.
  • Documentation
    • Added setup instructions, configuration guidance, example prompts, deployment requirements, and environment variable documentation.

D-K-P added 30 commits August 6, 2026 11:08
Fork of clickhouse-chat-agent, reworked from data analytics to learning
Trigger.dev. Strips ClickHouse (client, listTables/describeTable/runQuery
tools, PointMap/maplibre) and the chart cards; swaps in an interactive
React Flow + dagre FlowGraph plus code/diagram/prompt/stat cards rendered
from json-render specs.

The chat.agent() keeps the versioned AI Prompt and renderVisualization
validate/retry loop, and merges a docs MCP server (default Context7,
DOCS_MCP_URL) into its tool set each turn to ground answers on the live
docs without inventing API surface.
Replace the parked shadcn-neutral styling with the Trigger.dev Launch Week
look: charcoal surfaces, apple-green accent, Satoshi titles. Adds the brand
palette as real CSS variables (so the React Flow SVG edge layer can read
them) and remaps the shadcn semantic tokens so the chat shell and the
json-render primitives inherit the look with no per-component overrides.
Satoshi loads from the Fontshare CDN via a <link> in the layout.
Turn the agent into a tutor, not just a diagram renderer. Adds a sandboxed
HTML `Lesson` catalog component (model-authored Tufte-style lesson with an
interactive quiz, rendered in an allow-scripts-only iframe so its JS can't
reach the app origin or session token) and a `suggestNext` tool that ends
every turn with clickable deeper / sideways / practice / topic chips to keep
the learning flowing.

Rewrites the versioned prompt around a teaching method adapted from Matt
Pocock's "teach" skill: mission-first, one tangible win per turn, knowledge
then a retrieval quiz, everything grounded in the docs MCP. The empty state
seeds start-here / go-deeper goals plus a docs-grounded "suggest more topics".
…cted HTML

Adversarial review confirmed the sandbox (allow-scripts, no allow-same-origin)
already blocks app-origin XSS and reading the session token, but a lesson could
still beacon out or render a token-phishing form. Inject a strict CSP into the
lesson document — connect-src/form-action 'none', img-src data: only — closing
the exfiltration and phishing channels while keeping the quiz scripts, inline
styles, and Fontshare fonts working. Also harden the resize postMessage handler
against non-finite heights.
…ender

Add a pre-render screening layer for model-authored lesson HTML, on top of the
sandbox + CSP. In the renderVisualization tool (server-side, before anything
reaches the browser) every Lesson is vetted by a deterministic red-flag scan
(lesson-screen.ts — network calls, forms, credential inputs, cookie/storage
access, navigation/redirect, nested frames, eval) AND a fan-out of cheap
parallel LLM screeners with distinct adversarial lenses (exfiltration,
social-engineering). Any hit fails the tool, so the model regenerates a clean
lesson through the existing validate-and-retry loop. The static scan is unit
tested (attacks blocked, benign quizzes pass). LESSON_SCREENING=off disables
the LLM fan-out.
… source

The docs a lesson is grounded on are the upstream prompt-injection vector: a
poisoned page could steer the agent. Wrap every docs-MCP tool's output before
the model sees it (quarantine.ts) — coerce it to text and delimit it as
"untrusted reference material: data, not instructions", with an inline flag
when injection markers are present. Reinforce the same rule in the system
prompt. Unlike lessons, docs can't be regenerated, so this neutralizes rather
than blocks; the lesson screen remains the backstop for any malicious output an
injection might still induce. Quarantine + flag logic unit tested.
…t execute

A live run surfaced a TypeError: the docs-MCP tool's own toModelOutput does
`'content' in output`, which throws when the wrapper had replaced execute's
result with a plain string. Quarantine in toModelOutput instead (the layer that
decides what text the model sees), leaving the raw MCP result shape intact for
the SDK. Verified end to end against a real project: grounding, quarantine,
renderVisualization (FlowGraph + Lesson), lesson screening, and suggestNext all
complete cleanly.
…esign

Replace the generic shadcn shell with the Launch Week chat language: lavender
user bubbles and translucent charcoal assistant bubbles that grow from their
tail corner (bubbleIn), the chat.agent phosphor wordmark, a pill composer with
the masked gradient-border focus glow and apple send button, a breathing-dot
thinking indicator, word-by-word blur-in on streaming text, and newest-turn-to-
top scrolling with edge-fade masks. Adds the lavender token and a deeper
charcoal-1000 page background. Cards still render full-width (no bubble).
Bring in the real Launch Week card designs and a set of cheap, data-fill
components the model populates instead of authoring HTML. Ports HeroCard (icon
badge + kicker + display title) and the rich StatCard (count-up value, delta
badge, mini bars) from the marketing catalog, and adds Quiz (interactive
multiple-choice with immediate feedback), Callout (tip/warn/note), Steps,
Glossary, and Compare. Replaces the plain Stat. Each is a handful of tokens for
the model and renders identically every time.
Remove the model-authored HTML `Lesson` and everything it required — the
sandboxed iframe (lesson.tsx), the injected CSP, the deterministic scan
(lesson-screen.ts) and the LLM screening fan-out in the agent — now that the
teaching kit is all data-driven components. Nothing generates markup, so the
whole attack surface is gone; the docs-MCP quarantine stays (docs are still
untrusted input). Rewrites the prompt to keep responses short and valuable
(components carry the density) and to steer the model across the new kit.
… often

Drop the per-word blur-in reveal (it read laggy) — assistant text now just
renders markdown as tokens stream in, the standard AI SDK approach, which feels
snappier. Steer the agent to frequently offer a "paste-ready prompt to scaffold
this in my repo" next-step chip and answer it with a PromptCard.
Persist conversations to Postgres, gated on DATABASE_URL — unset, the app runs
exactly as before with no sidebar; set, you get history, resume-on-reload and
delete. Showcases where the chat.agent lifecycle hooks belong: onChatStart
creates the row (once per chat), onTurnStart awaits the message write so the
question is durable before streaming AND sets the prompt (it fires on
continuation runs, where onChatStart doesn't), and onTurnComplete writes the
turn plus the resume cursor in one transaction so a refresh can't replay it.

Messages live in a single JSON column, so AI SDK upgrades need no migration.
Chats are owned by an anonymous cookie id minted in proxy.ts and every query is
scoped by it. Drizzle + pg against Supabase; pg avoids the prepared-statement
limitation of the transaction pooler.
Dagre was laying out every node as 128px wide — a constant carried over from a
design that assumed one-word labels — while the nodes actually rendered 150-300px.
Dagre reserved too little room, so siblings on a rank drew on top of each other.

Measure each node's width from its content and use that same value for both the
layout and the rendered node, so the two can't diverge; the serpentine grid gets
one column pitch sized to the widest node. Labels and sublabels are capped in the
catalog (22/24 chars) and truncate, and the schema now asks for at most ~10 nodes
and 3 branches per node so graphs stay readable in a chat column.
…architecture

Nothing was shown when a turn failed — the commonest case being `pnpm dev:trigger`
not running, which failed silently. Add an inline error banner with a retry (a
banner, not a toast: these are usually setup problems worth leaving on screen)
that maps failures to actionable text, including the dev-server case. Sanitise
errors server-side via uiMessageStreamOptions.onError so the browser gets a
useful message and the full error stays in the run log.

Also adds "Explain how this app is built" as a starting question, plus a factual
section in the prompt describing this app's architecture — the docs it grounds on
don't cover it, so without this the showcase question got a vague answer.
…duplicate prose

Restores auto-follow while an answer streams — I'd removed it when the entrance
felt jerky, but the blur was the culprit, not the scrolling. It's driven by a
ResizeObserver (message count doesn't change while text streams, so a
messages-keyed effect never fired) and disarms the moment the reader scrolls up,
re-arming at the bottom.

The composer now floats over the thread: content scrolls underneath and the last
inch masks into the background instead of stopping at a hard edge. Side rules on
the column so the overflowing Next chips read as clipped by a boundary, plus a
fade on that row.

Prompt: say each thing once (prose must not restate a card's contents) and call
suggestNext exactly once at the end of a turn — calling it repeatedly was
producing duplicated paragraphs and orphaned chip sets. Card text no longer
carries its own max-width: inside a card the card is the measure, so a 55ch cap
just left half the card empty.
Trigger.dev already persists the conversation: each chat.agent chat is backed by
a durable Session that outlives its runs, and the agent rebuilds full history
from a snapshot after an idle timeout, crash or redeploy. So the Postgres tables
were storing a second copy of what the platform already holds. Removes Drizzle,
pg and the schema/migrations, and rebuilds the sidebar on sessions.list().

Owner and title are stamped into session metadata when the session is created,
and the list is filtered by owner server-side so a browser only ever receives
its own chats. Two API limitations shaped this, both verified against 4.5.9:
sessions.update() returns Unauthorized even with a secret key, and
triggerConfig.tags is dropped at create time — so tags (which list can filter on
server-side) aren't usable, and metadata has to be set up front.

Opening an old chat resumes it and the agent remembers everything, but earlier
messages aren't redrawn: session.out is trimmed to ~one turn and there's no
public API to read the stored transcript. The UI says so rather than looking
broken. Verified live: turn runs, metadata persists, owner-scoped list returns
only that user's chat.
…er-side

Session tags go in the top-level `tags` field on session create, not
`triggerConfig.tags` (which tags each run the session schedules). That lets
sessions.list({ tag }) do the owner filtering server-side instead of listing
everything and narrowing in memory. Verified against 4.5.9: tags persist on the
session row and the tag filter returns just that owner's chats.
Back to a single-conversation demo: no sidebar, no chat list, no anonymous-user
cookie, no /chat/[id] route, no clientData. The example is one static page again
and needs nothing but a Trigger project and an Anthropic key.

The conversation is still durable — that's the platform's job, not the app's —
but nothing in the UI depends on enumerating or reopening past chats, which is
what all the removed machinery existed for.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a complete trigger-chat-agent Next.js example. The project includes Trigger.dev session actions, a streamed Anthropic chat agent, documentation MCP grounding with quarantine handling, validated JSON visualizations, and suggested-next tools. The client renders chat messages, documentation provenance, interactive graphs, diagrams, teaching cards, quizzes, code blocks, and markdown. It also adds project configuration, styling, setup documentation, and deployment instructions.

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

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% 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 clearly summarizes the main change: a Trigger.dev AI chat agent that teaches through visual content.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-trigger-chat-agent-example

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 10

🧹 Nitpick comments (4)
trigger-chat-agent/trigger.config.ts (1)

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

Fail fast when TRIGGER_PROJECT_REF is missing.

The non-null assertion hides a missing environment variable. The config then resolves project to undefined, and the CLI error does not point at the cause. Add an explicit check so the setup step in the README is enforced.

♻️ Proposed refactor
 import { defineConfig } from "`@trigger.dev/sdk`";
 
+const projectRef = process.env.TRIGGER_PROJECT_REF;
+if (!projectRef) {
+  throw new Error("TRIGGER_PROJECT_REF is not set. Copy .env.example to .env and set it.");
+}
+
 export default defineConfig({
-  project: process.env.TRIGGER_PROJECT_REF!,
+  project: projectRef,
   runtime: "node-22",
🤖 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 `@trigger-chat-agent/trigger.config.ts` at line 4, Replace the non-null
assertion on project in the Trigger configuration with an explicit validation of
TRIGGER_PROJECT_REF that fails immediately with a clear
missing-environment-variable error, ensuring setup cannot continue with an
undefined project value.
trigger-chat-agent/package.json (1)

5-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a lint script and ESLint dependencies, or remove the ESLint directive.

src/components/chat.tsx line 172 contains // eslint-disable-next-line react-hooks/exhaustive-deps, but this package declares no ESLint dependency and no lint script. Users who copy the example get no lint feedback.

🤖 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 `@trigger-chat-agent/package.json` around lines 5 - 11, Add a package-level
lint script and the required ESLint dependencies/configuration in package.json
so the existing directive in Chat component remains effective, or remove that
directive if linting is intentionally unsupported; ensure the chosen approach
gives copied examples consistent lint behavior.
trigger-chat-agent/src/components/chat.tsx (1)

610-619: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This branch is unreachable; remove it or document why it stays.

groupMessageParts routes every part that is not text, tool-renderVisualization, or suggestNext into a docs group, and Message renders those groups with DocsToolChain. MessagePart therefore never receives a generic tool- or dynamic-tool part. The block duplicates the documentation-status presentation and will drift from DocsToolChain.

🤖 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 `@trigger-chat-agent/src/components/chat.tsx` around lines 610 - 619, The
generic tool/dynamic-tool branch in MessagePart is unreachable because
groupMessageParts routes those parts into the docs group. Remove the
startsWith("tool-")/dynamic-tool handling and rely on DocsToolChain for
documentation-status rendering, unless there is a demonstrated routing exception
that must be documented.
trigger-chat-agent/src/lib/quarantine.ts (1)

1-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the stale lesson-screen.ts reference.

No lesson-screen.ts file exists in the repository. Update the header comment so it does not reference removed code.

🤖 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 `@trigger-chat-agent/src/lib/quarantine.ts` around lines 1 - 12, Update the
header comment in the quarantine module to remove the stale lesson-screen.ts
reference, while preserving the remaining description of the downstream
mitigation.
🤖 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 `@trigger-chat-agent/.gitignore`:
- Line 2: Update the ignore rules in .gitignore to cover all local environment
variants, including .env.local, .env.development.local, and
.env.production.local, while retaining the existing .env rule.

In `@trigger-chat-agent/src/app/actions.ts`:
- Around line 12-20: Update mintChatAccessToken to require an authenticated
caller and verify that the caller owns or may access the supplied chatId before
invoking auth.createPublicToken; reject unauthenticated or unauthorized
requests. If anonymous sessions are intentionally supported, document that
security requirement explicitly in the action instead of leaving the endpoint
unprotected.

In `@trigger-chat-agent/src/components/chat.tsx`:
- Around line 496-556: Update DocsToolChain to treat both output-available and
output-error states as terminal when computing complete, so failed lookups show
“Complete” and stop the searching animation. Preserve a distinct output-error
row indicator rather than rendering the successful Check icon for failed parts.

In `@trigger-chat-agent/src/components/error-notice.tsx`:
- Around line 53-58: Add role="alert" to the root div returned by the error
notice component so asynchronously rendered failures are announced to assistive
technology, leaving the existing styling and content unchanged.

In `@trigger-chat-agent/src/components/flow-graph.tsx`:
- Around line 450-472: Stabilize the nodes and sequence inputs used by the
status animation useEffect so replaceMessage’s deep-cloned but unchanged data
does not reset statuses or recreate timers. Derive content-based keys or
memoized array values from nodes and sequence, and use those stable dependencies
while preserving updates when their actual contents change.

In `@trigger-chat-agent/src/components/quiz.tsx`:
- Around line 36-62: Update the option buttons rendered in the options.map
callback to remain focusable after answered: replace the disabled attribute with
aria-disabled and guard the onClick handler so setPicked only runs before
answering. Mark the conditional explanation paragraph as an assertive live
region so its appearance is announced to assistive technologies.

In `@trigger-chat-agent/src/components/stat-card.tsx`:
- Around line 73-93: Update the onUpdate formatting in AnimatedValue so animated
numbers retain digit grouping when numText contains commas. Format the
interpolated value with grouping separators before combining it with prefix and
suffix, while preserving decimal precision and existing ungrouped-number
behavior.

In `@trigger-chat-agent/src/components/visualization.tsx`:
- Around line 11-40: Update VisualizationErrorBoundary usage in Visualization to
key the boundary by spec, ensuring repaired specifications mount a fresh
boundary and reset failed state. Add componentDidCatch to
VisualizationErrorBoundary to log rendering errors during development without
changing the existing fallback UI.

In `@trigger-chat-agent/src/lib/catalog.ts`:
- Around line 304-311: Update validateSpec to perform a reachability walk from
the root element using a visited set, following each element’s children and
rejecting any node encountered more than once as a cyclic or repeated reference.
Preserve the existing missing-child-key validation and return the accumulated
validation errors through the current result shape.

In `@trigger-chat-agent/src/trigger/trigger-chat-agent.ts`:
- Around line 48-67: Update loadDocsTools to initialize createMCPClient with
initializationOptions.timeout set to 10,000 ms, and replace client.tools() with
listTools({ options: { timeout: 10_000 } }) followed by
toolsFromDefinitions(...). Clear docsToolsPromise only when loading throws so
failed loads can retry, while preserving a valid empty tool set as a successful
cached result.

---

Nitpick comments:
In `@trigger-chat-agent/package.json`:
- Around line 5-11: Add a package-level lint script and the required ESLint
dependencies/configuration in package.json so the existing directive in Chat
component remains effective, or remove that directive if linting is
intentionally unsupported; ensure the chosen approach gives copied examples
consistent lint behavior.

In `@trigger-chat-agent/src/components/chat.tsx`:
- Around line 610-619: The generic tool/dynamic-tool branch in MessagePart is
unreachable because groupMessageParts routes those parts into the docs group.
Remove the startsWith("tool-")/dynamic-tool handling and rely on DocsToolChain
for documentation-status rendering, unless there is a demonstrated routing
exception that must be documented.

In `@trigger-chat-agent/src/lib/quarantine.ts`:
- Around line 1-12: Update the header comment in the quarantine module to remove
the stale lesson-screen.ts reference, while preserving the remaining description
of the downstream mitigation.

In `@trigger-chat-agent/trigger.config.ts`:
- Line 4: Replace the non-null assertion on project in the Trigger configuration
with an explicit validation of TRIGGER_PROJECT_REF that fails immediately with a
clear missing-environment-variable error, ensuring setup cannot continue with an
undefined project value.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a2df0c9a-246a-4da6-a147-033700484457

📥 Commits

Reviewing files that changed from the base of the PR and between a7f6fc1 and f31b070.

⛔ Files ignored due to path filters (2)
  • trigger-chat-agent/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • trigger-chat-agent/src/app/icon.svg is excluded by !**/*.svg
📒 Files selected for processing (34)
  • README.md
  • trigger-chat-agent/.env.example
  • trigger-chat-agent/.gitignore
  • trigger-chat-agent/README.md
  • trigger-chat-agent/components.json
  • trigger-chat-agent/next.config.ts
  • trigger-chat-agent/package.json
  • trigger-chat-agent/postcss.config.mjs
  • trigger-chat-agent/src/app/actions.ts
  • trigger-chat-agent/src/app/globals.css
  • trigger-chat-agent/src/app/layout.tsx
  • trigger-chat-agent/src/app/page.tsx
  • trigger-chat-agent/src/components/chat.tsx
  • trigger-chat-agent/src/components/code-card.tsx
  • trigger-chat-agent/src/components/diagram-card.tsx
  • trigger-chat-agent/src/components/error-notice.tsx
  • trigger-chat-agent/src/components/flow-graph.tsx
  • trigger-chat-agent/src/components/hero-card.tsx
  • trigger-chat-agent/src/components/prompt-card.tsx
  • trigger-chat-agent/src/components/quiz.tsx
  • trigger-chat-agent/src/components/stat-card.tsx
  • trigger-chat-agent/src/components/streaming-text.tsx
  • trigger-chat-agent/src/components/teaching-cards.tsx
  • trigger-chat-agent/src/components/visualization.tsx
  • trigger-chat-agent/src/components/wordmark.tsx
  • trigger-chat-agent/src/lib/catalog.ts
  • trigger-chat-agent/src/lib/code-theme.ts
  • trigger-chat-agent/src/lib/motion.ts
  • trigger-chat-agent/src/lib/quarantine.ts
  • trigger-chat-agent/src/lib/registry.tsx
  • trigger-chat-agent/src/lib/utils.ts
  • trigger-chat-agent/src/trigger/trigger-chat-agent.ts
  • trigger-chat-agent/trigger.config.ts
  • trigger-chat-agent/tsconfig.json

@@ -0,0 +1,6 @@
node_modules
.env

Copy link
Copy Markdown

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

Ignore all local environment files.

Line 2 does not ignore .env.local, .env.development.local, or .env.production.local. A developer can commit TRIGGER_SECRET_KEY or other credentials when using a standard local environment filename.

Proposed fix
-.env
+.env*
+!.env.example
📝 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
.env
.env*
!.env.example
🤖 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 `@trigger-chat-agent/.gitignore` at line 2, Update the ignore rules in
.gitignore to cover all local environment variants, including .env.local,
.env.development.local, and .env.production.local, while retaining the existing
.env rule.

Comment on lines +12 to +20
export async function mintChatAccessToken(chatId: string) {
return auth.createPublicToken({
scopes: {
read: { sessions: chatId },
write: { sessions: chatId },
},
expirationTime: "1h",
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bind mintChatAccessToken to an authenticated caller, or document the gap.

mintChatAccessToken is an exported server action, so it is a public POST endpoint. It mints a read and write token for any chatId the caller supplies, with no ownership check. Anyone who learns or guesses a session id can read that conversation and send turns as that user for one hour.

The demo may accept anonymous sessions. Users copy this file into real applications, so state the requirement in code.

🔒 Suggested ownership check
 export async function mintChatAccessToken(chatId: string) {
+  // SECURITY: this server action is a public endpoint. In a real application,
+  // resolve the signed-in user and verify they own `chatId` before minting.
+  // const user = await getCurrentUser();
+  // if (!user || !(await userOwnsSession(user.id, chatId))) throw new Error("Forbidden");
   return auth.createPublicToken({
     scopes: {
       read: { sessions: chatId },
       write: { sessions: chatId },
     },
     expirationTime: "1h",
   });
 }
📝 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
export async function mintChatAccessToken(chatId: string) {
return auth.createPublicToken({
scopes: {
read: { sessions: chatId },
write: { sessions: chatId },
},
expirationTime: "1h",
});
}
export async function mintChatAccessToken(chatId: string) {
// SECURITY: this server action is a public endpoint. In a real application,
// resolve the signed-in user and verify they own `chatId` before minting.
// const user = await getCurrentUser();
// if (!user || !(await userOwnsSession(user.id, chatId))) throw new Error("Forbidden");
return auth.createPublicToken({
scopes: {
read: { sessions: chatId },
write: { sessions: chatId },
},
expirationTime: "1h",
});
}
🤖 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 `@trigger-chat-agent/src/app/actions.ts` around lines 12 - 20, Update
mintChatAccessToken to require an authenticated caller and verify that the
caller owns or may access the supplied chatId before invoking
auth.createPublicToken; reject unauthenticated or unauthorized requests. If
anonymous sessions are intentionally supported, document that security
requirement explicitly in the action instead of leaving the endpoint
unprotected.

Comment on lines +496 to +556
function DocsToolChain({ parts }: { parts: MessagePartValue[] }) {
const [expanded, setExpanded] = useState(false);
const complete = parts.every(
(part) => (part as { state?: string }).state === "output-available",
);

return (
<div
className="rounded-2xl border border-grid-dimmed bg-charcoal-950/60 px-4 py-3"
aria-label="Documentation lookups"
>
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpanded((value) => !value)}
className="mb-1 flex min-h-10 w-full items-center gap-2 rounded-lg text-left"
>
<BookOpen className="size-3.5 text-apple-500" />
<span className="font-mono text-2xs uppercase tracking-widest text-dimmed">
Grounding in the docs
</span>
<span className="ml-auto font-mono text-2xs text-charcoal-500">
{complete ? "Complete" : "Searching"}
</span>
<ChevronDown
className={`size-3.5 shrink-0 text-charcoal-500 transition-transform duration-150 ${expanded ? "rotate-180" : ""}`}
/>
</button>
<div className="relative space-y-3 before:absolute before:bottom-2 before:left-[0.4375rem] before:top-2 before:w-px before:bg-grid-bright">
{parts.map((part, i) => {
const done =
(part as { state?: string }).state === "output-available";
const label = docsToolLabel(part);
return (
<div key={i} className="relative flex min-w-0 items-start gap-3">
<span className="relative z-10 mt-0.5 flex size-3.5 shrink-0 items-center justify-center rounded-full bg-charcoal-950 text-dimmed">
<Globe2 className="size-3.5" />
</span>
<span
className={`min-w-0 flex-1 text-sm text-dimmed ${expanded ? "break-words leading-5" : "truncate"}`}
title={label}
>
{label}
</span>
{done ? (
<Check
className="mt-0.5 size-3.5 shrink-0 text-dimmed"
aria-label="Complete"
/>
) : (
<span
className="relative mt-1 flex size-2 shrink-0"
aria-label="Searching"
>
<span className="absolute inset-0 animate-ping rounded-full bg-apple-500/60 motion-reduce:animate-none" />
<span className="relative size-2 rounded-full bg-apple-500" />
</span>
)}
</div>
);
})}

Copy link
Copy Markdown

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

Treat terminal error states as finished in DocsToolChain.

complete requires state === "output-available" for every part. A docs tool that ends in output-error never reaches that state. The header then stays on "Searching" and the row keeps the pinging dot after the turn ends. The docs MCP is remote, so this state is reachable.

🐛 Proposed fix
+const TERMINAL_STATES = new Set(["output-available", "output-error"]);
+const isSettled = (part: MessagePartValue) =>
+  TERMINAL_STATES.has((part as { state?: string }).state ?? "");
+
 function DocsToolChain({ parts }: { parts: MessagePartValue[] }) {
   const [expanded, setExpanded] = useState(false);
-  const complete = parts.every(
-    (part) => (part as { state?: string }).state === "output-available",
-  );
+  const complete = parts.every(isSettled);
@@
-          const done =
-            (part as { state?: string }).state === "output-available";
+          const done = isSettled(part);

Consider also showing a distinct icon when state === "output-error", so a failed lookup is not reported as complete.

📝 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 DocsToolChain({ parts }: { parts: MessagePartValue[] }) {
const [expanded, setExpanded] = useState(false);
const complete = parts.every(
(part) => (part as { state?: string }).state === "output-available",
);
return (
<div
className="rounded-2xl border border-grid-dimmed bg-charcoal-950/60 px-4 py-3"
aria-label="Documentation lookups"
>
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpanded((value) => !value)}
className="mb-1 flex min-h-10 w-full items-center gap-2 rounded-lg text-left"
>
<BookOpen className="size-3.5 text-apple-500" />
<span className="font-mono text-2xs uppercase tracking-widest text-dimmed">
Grounding in the docs
</span>
<span className="ml-auto font-mono text-2xs text-charcoal-500">
{complete ? "Complete" : "Searching"}
</span>
<ChevronDown
className={`size-3.5 shrink-0 text-charcoal-500 transition-transform duration-150 ${expanded ? "rotate-180" : ""}`}
/>
</button>
<div className="relative space-y-3 before:absolute before:bottom-2 before:left-[0.4375rem] before:top-2 before:w-px before:bg-grid-bright">
{parts.map((part, i) => {
const done =
(part as { state?: string }).state === "output-available";
const label = docsToolLabel(part);
return (
<div key={i} className="relative flex min-w-0 items-start gap-3">
<span className="relative z-10 mt-0.5 flex size-3.5 shrink-0 items-center justify-center rounded-full bg-charcoal-950 text-dimmed">
<Globe2 className="size-3.5" />
</span>
<span
className={`min-w-0 flex-1 text-sm text-dimmed ${expanded ? "break-words leading-5" : "truncate"}`}
title={label}
>
{label}
</span>
{done ? (
<Check
className="mt-0.5 size-3.5 shrink-0 text-dimmed"
aria-label="Complete"
/>
) : (
<span
className="relative mt-1 flex size-2 shrink-0"
aria-label="Searching"
>
<span className="absolute inset-0 animate-ping rounded-full bg-apple-500/60 motion-reduce:animate-none" />
<span className="relative size-2 rounded-full bg-apple-500" />
</span>
)}
</div>
);
})}
const TERMINAL_STATES = new Set(["output-available", "output-error"]);
const isSettled = (part: MessagePartValue) =>
TERMINAL_STATES.has((part as { state?: string }).state ?? "");
function DocsToolChain({ parts }: { parts: MessagePartValue[] }) {
const [expanded, setExpanded] = useState(false);
const complete = parts.every(isSettled);
return (
<div
className="rounded-2xl border border-grid-dimmed bg-charcoal-950/60 px-4 py-3"
aria-label="Documentation lookups"
>
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpanded((value) => !value)}
className="mb-1 flex min-h-10 w-full items-center gap-2 rounded-lg text-left"
>
<BookOpen className="size-3.5 text-apple-500" />
<span className="font-mono text-2xs uppercase tracking-widest text-dimmed">
Grounding in the docs
</span>
<span className="ml-auto font-mono text-2xs text-charcoal-500">
{complete ? "Complete" : "Searching"}
</span>
<ChevronDown
className={`size-3.5 shrink-0 text-charcoal-500 transition-transform duration-150 ${expanded ? "rotate-180" : ""}`}
/>
</button>
<div className="relative space-y-3 before:absolute before:bottom-2 before:left-[0.4375rem] before:top-2 before:w-px before:bg-grid-bright">
{parts.map((part, i) => {
const done = isSettled(part);
const label = docsToolLabel(part);
return (
<div key={i} className="relative flex min-w-0 items-start gap-3">
<span className="relative z-10 mt-0.5 flex size-3.5 shrink-0 items-center justify-center rounded-full bg-charcoal-950 text-dimmed">
<Globe2 className="size-3.5" />
</span>
<span
className={`min-w-0 flex-1 text-sm text-dimmed ${expanded ? "break-words leading-5" : "truncate"}`}
title={label}
>
{label}
</span>
{done ? (
<Check
className="mt-0.5 size-3.5 shrink-0 text-dimmed"
aria-label="Complete"
/>
) : (
<span
className="relative mt-1 flex size-2 shrink-0"
aria-label="Searching"
>
<span className="absolute inset-0 animate-ping rounded-full bg-apple-500/60 motion-reduce:animate-none" />
<span className="relative size-2 rounded-full bg-apple-500" />
</span>
)}
</div>
);
})}
🤖 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 `@trigger-chat-agent/src/components/chat.tsx` around lines 496 - 556, Update
DocsToolChain to treat both output-available and output-error states as terminal
when computing complete, so failed lookups show “Complete” and stop the
searching animation. Preserve a distinct output-error row indicator rather than
rendering the successful Check icon for failed parts.

Comment on lines +53 to +58
return (
<div className="flex items-start gap-3 rounded-xl border border-error/40 bg-error/5 px-4 py-3">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-error" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-error">{title}</div>
<p className="mt-0.5 break-words text-xs leading-relaxed text-dimmed">{detail}</p>

Copy link
Copy Markdown

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

Announce the error to assistive technology.

The banner appears asynchronously after a failure. Without a live region, screen readers do not announce it. Add role="alert".

♿ Proposed fix
-    <div className="flex items-start gap-3 rounded-xl border border-error/40 bg-error/5 px-4 py-3">
+    <div
+      role="alert"
+      className="flex items-start gap-3 rounded-xl border border-error/40 bg-error/5 px-4 py-3"
+    >
📝 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
return (
<div className="flex items-start gap-3 rounded-xl border border-error/40 bg-error/5 px-4 py-3">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-error" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-error">{title}</div>
<p className="mt-0.5 break-words text-xs leading-relaxed text-dimmed">{detail}</p>
return (
<div
role="alert"
className="flex items-start gap-3 rounded-xl border border-error/40 bg-error/5 px-4 py-3"
>
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-error" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-error">{title}</div>
<p className="mt-0.5 break-words text-xs leading-relaxed text-dimmed">{detail}</p>
🤖 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 `@trigger-chat-agent/src/components/error-notice.tsx` around lines 53 - 58, Add
role="alert" to the root div returned by the error notice component so
asynchronously rendered failures are announced to assistive technology, leaving
the existing styling and content unchanged.

Comment on lines +450 to +472
useEffect(() => {
setStatuses(initialStatuses(nodes, sequence, reduceMotion));
if (reduceMotion || !sequence || sequence.length === 0) return;
const timers = sequence.map((s) =>
window.setTimeout(() => {
setStatuses((prev) => (s.nodeId in prev ? { ...prev, [s.nodeId]: s.status } : prev));
}, Math.max(0, s.atMs))
);
return () => timers.forEach((t) => window.clearTimeout(t));
}, [nodes, sequence, reduceMotion]);

// Edges fade in mid-cascade so they don't dangle off still-hidden nodes.
const [edgesVisible, setEdgesVisible] = useState(reduceMotion);
const revealSpan = useMemo(() => Math.max(0, ...Object.values(revealDelays)), [revealDelays]);
useEffect(() => {
if (reduceMotion) {
setEdgesVisible(true);
return;
}
setEdgesVisible(false);
const t = window.setTimeout(() => setEdgesVisible(true), revealSpan * 500 + 150);
return () => window.clearTimeout(t);
}, [reduceMotion, revealSpan]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find FlowGraph call sites and check whether the node/edge/sequence arrays are memoized.
fd -e tsx -e ts . trigger-chat-agent/src | xargs rg -n -C 10 '\bFlowGraph\b'

# Inspect how visualization specs are parsed and passed down.
fd -a 'visualization.tsx|registry.tsx|catalog.ts' trigger-chat-agent/src --exec cat -n {}

Repository: triggerdotdev/examples

Length of output: 49061


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Visualization call sites ---'
rg -n -C 20 '\bVisualization\s*\(|normalizeSpec|renderVisualization|tool.*input|toolInvocation' trigger-chat-agent/src trigger-chat-agent --glob '!**/node_modules/**'

printf '%s\n' '--- Relevant package versions and renderer implementation references ---'
rg -n -C 8 '"`@json-render/`(react|core)"|json-render' trigger-chat-agent/package.json package.json trigger-chat-agent/pnpm-lock.yaml trigger-chat-agent/package-lock.json trigger-chat-agent/yarn.lock 2>/dev/null || true
rg -n -C 10 'JSONUIProvider|Renderer' trigger-chat-agent --glob '!**/node_modules/**'

Repository: triggerdotdev/examples

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Chat render path ---'
sed -n '430,560p' trigger-chat-agent/src/components/chat.tsx
sed -n '560,625p' trigger-chat-agent/src/components/chat.tsx

printf '%s\n' '--- All Visualization rendering references ---'
rg -n 'Visualization|groupMessageParts|messages\.map|parts\.map|key=' trigger-chat-agent/src/components/chat.tsx trigger-chat-agent/src/components/visualization.tsx

printf '%s\n' '--- Package metadata ---'
for f in trigger-chat-agent/package.json package.json; do
  if [ -f "$f" ]; then
    sed -n '1,180p' "$f"
  fi
done

Repository: triggerdotdev/examples

Length of output: 9882


🌐 Web query:

AI SDK v6 @ai-sdk/react useChat source message parts input object identity immutable updates

💡 Result:

In AI SDK v6 (and since v5.0), the useChat hook has transitioned to a transport-based architecture and no longer manages input state internally [1][2]. You must now manage input state (such as text fields) manually using standard React hooks like useState, and utilize the sendMessage function to send data [3][4]. Regarding your questions on message structure and updates: 1. Source Message Parts: The UIMessage object includes a parts property that contains an array of UIMessagePart objects [1][5]. This property is the recommended way to render message content, including text, tool invocations, tool results, and sources [3][5]. Specifically, sources are categorized into types such as source-url and source-document [5]. You should filter and map over the parts array to render these specific source types dynamically [5]. 2. Object Identity and Immutable Updates: The AI SDK manages chat state internally, providing an immutable messages array that updates automatically as new messages are streamed from the AI provider [1][3]. Since the SDK controls this state object, you should treat the messages array and the UIMessage objects within it as immutable [1][5]. When updating or interacting with chat state, use the provided methods (like sendMessage or setMessages) rather than directly mutating the state [1]. Because the SDK handles state management, React will trigger re-renders when the messages array is updated, allowing your components to stay synchronized with the latest streaming data [3][5]. By offloading input management to your own components and consuming the messages state through the hook, you ensure your UI remains responsive to the immutable state updates provided by the SDK [3][4].

Citations:


🌐 Web query:

site:github.com/vercel/ai packages/react/src/use-chat.ts UIMessage parts input streaming source

💡 Result:

In the Vercel AI SDK, UIMessage is the primary interface used to represent chat messages within the UI [1][2]. It defines the structure of messages rendered in the chat interface, containing a unique id, a role (system, user, or assistant), metadata, and an array of parts [1][3]. The parts property is an array of UIMessagePart objects, which are designed specifically for flexible UI rendering [1]. These parts can represent various content types, including [1]: - Text: Standard text content (TextUIPart) - Reasoning: Thoughts or reasoning processes (ReasoningUIPart) - Tooling: Tool invocations or results (ToolUIPart, DynamicToolUIPart) - File support: Files or related metadata (FileUIPart, SourceUrlUIPart, etc.) - Data: Custom data structures (DataUIPart) The useChat hook, located in packages/react/src/use-chat.ts, manages the interaction and streaming state for these messages [4][5]. When a response is streamed, the SDK handles the incoming data stream by processing it into UIMessage objects [6][7]. The streaming process involves: 1. Request/Resume: useChat initiates a request or resumes an existing stream via the Chat class's transport layer [6][7][5]. 2. Streaming State: As chunks are received, the SDK creates or updates the message state [6]. The UIMessage content is progressively built using these chunks, allowing the UI to reactively display the streaming content (e.g., text, tool calls) [6][7]. 3. Updates: The useChat hook uses useSyncExternalStore to subscribe to the internal Chat state, ensuring the UI re-renders as messages are added or updated during the streaming process [4][8][5]. This architecture separates the core messaging logic (the Chat instance) from the React-specific hooks (useChat), ensuring that UIMessage parts provide a consistent way to handle multi-modal and interactive content during streaming [6][5][1].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
url='https://raw.githubusercontent.com/vercel/ai/main/packages/ai/src/ui/chat.ts'
tmp=$(mktemp)
curl -fsSL "$url" -o "$tmp"
printf '%s\n' '--- Chat state update paths ---'
rg -n -C 12 'setMessages|messages =|updateMessage|replace|process|onTool|tool' "$tmp" | head -n 260
rm -f "$tmp"

Repository: triggerdotdev/examples

Length of output: 8275


🏁 Script executed:

#!/bin/bash
set -e
for path in packages/ai/src/ui/process-ui-message-stream.ts packages/ai/src/ui/chat.ts; do
  url="https://raw.githubusercontent.com/vercel/ai/main/$path"
  tmp=$(mktemp)
  curl -fsSL "$url" -o "$tmp"
  printf '\n--- %s ---\n' "$path"
  rg -n -C 16 'messages|replaceMessage|parts|tool-input|tool-output|snapshot|append' "$tmp" | head -n 360
  rm -f "$tmp"
done

Repository: triggerdotdev/examples

Length of output: 25338


🏁 Script executed:

#!/bin/bash
set -e
url='https://raw.githubusercontent.com/vercel/ai/main/packages/ai/src/ui/chat.ts'
tmp=$(mktemp)
curl -fsSL "$url" -o "$tmp"
printf '%s\n' '--- Stream state publication and snapshot ---'
rg -n -C 24 'runUpdateMessageJob|createStreamingUIMessageState|state\.messages|replaceMessage|snapshot|activeResponse|processUIMessageStream' "$tmp" | tail -n 420
rm -f "$tmp"

Repository: triggerdotdev/examples

Length of output: 13545


🏁 Script executed:

#!/bin/bash
set -e
url='https://raw.githubusercontent.com/vercel/ai/main/packages/react/src/use-chat.ts'
tmp=$(mktemp)
curl -fsSL "$url" -o "$tmp"
printf '%s\n' '--- React state snapshot and subscriptions ---'
rg -n -C 24 'snapshot|replaceMessage|useSyncExternalStore|messages|pushMessage' "$tmp" | head -n 420
rm -f "$tmp"

Repository: triggerdotdev/examples

Length of output: 6754


🏁 Script executed:

#!/bin/bash
set -e
url='https://raw.githubusercontent.com/vercel/ai/main/packages/react/src/chat.react.ts'
tmp=$(mktemp)
curl -fsSL "$url" -o "$tmp"
printf '%s\n' '--- React Chat message update implementation ---'
rg -n -C 18 'replaceMessage|pushMessage|registerMessages|messages|snapshot' "$tmp" | head -n 360
rm -f "$tmp"

Repository: triggerdotdev/examples

Length of output: 4171


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

p = Path("trigger-chat-agent/src/components/flow-graph.tsx").read_text()
status = re.search(
    r"useEffect\(\(\) => \{.*?setStatuses\(initialStatuses\(nodes, sequence, reduceMotion\)\);.*?\}, \s*\[([^\]]+)\]\);",
    p,
    re.S,
)
edges = re.search(
    r"useEffect\(\(\) => \{.*?setEdgesVisible\(false\);.*?\}, \s*\[([^\]]+)\]\);",
    p,
    re.S,
)
print("status_effect_dependencies:", status.group(1).strip() if status else "not found")
print("edges_effect_dependencies:", edges.group(1).strip() if edges else "not found")
PY

node - <<'JS'
const message = {
  parts: [{
    type: "tool-renderVisualization",
    input: {
      spec: {
        elements: {
          graph: {
            type: "FlowGraph",
            props: {
              nodes: [{ id: "a", label: "A", kind: "task" }],
              sequence: [{ nodeId: "a", status: "running", atMs: 1000 }],
            },
          },
        },
      },
    },
  }],
};

const nextMessage = structuredClone(message);
const oldProps = message.parts[0].input.spec.elements.graph.props;
const newProps = nextMessage.parts[0].input.spec.elements.graph.props;
console.log("nodes_same_reference_after_replaceMessage:", oldProps.nodes === newProps.nodes);
console.log("sequence_same_reference_after_replaceMessage:", oldProps.sequence === newProps.sequence);
JS

Repository: triggerdotdev/examples

Length of output: 371


Stabilize the status-effect inputs.

replaceMessage deep-clones each streamed message, so nodes and sequence receive new references even when their contents do not change. The status effect then resets the animation and recreates its timers on every stream update. Derive stable content keys or memoize these arrays before using them in the effect dependencies.

🤖 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 `@trigger-chat-agent/src/components/flow-graph.tsx` around lines 450 - 472,
Stabilize the nodes and sequence inputs used by the status animation useEffect
so replaceMessage’s deep-cloned but unchanged data does not reset statuses or
recreate timers. Derive content-based keys or memoized array values from nodes
and sequence, and use those stable dependencies while preserving updates when
their actual contents change.

Comment on lines +36 to +62
<div className="space-y-2">
{options.map((o, i) => {
const isCorrect = Boolean(o.correct);
const reveal = answered && (i === picked || isCorrect);
return (
<button
key={i}
type="button"
disabled={answered}
onClick={() => setPicked(i)}
className={cn(
"flex min-h-11 w-full items-center gap-3 rounded-xl border px-4 py-2.5 text-left text-sm leading-5 transition-colors duration-150",
!reveal && "border-charcoal-700 bg-charcoal-800 text-bright enabled:hover:bg-charcoal-700",
reveal && isCorrect && "border-apple-500/60 bg-apple-500/10 text-apple-200",
reveal && !isCorrect && "border-error/60 bg-error/10 text-error"
)}
>
{reveal && isCorrect && <CheckCircle2 className="size-4 shrink-0 text-apple-500" />}
{reveal && !isCorrect && <XCircle className="size-4 shrink-0 text-error" />}
<span>{o.text}</span>
</button>
);
})}
</div>
{answered && explanation && (
<p className="mt-4 border-t border-grid-bright pt-3 text-sm leading-relaxed text-dimmed">{explanation}</p>
)}

Copy link
Copy Markdown

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

Keep the answered options focusable and announce the result.

After the user answers, line 44 sets disabled on every option. A disabled <button> leaves the tab order and cannot receive focus, so a keyboard user loses the focus position and cannot review the revealed states. The explanation at line 61 also appears without an announcement.

Use aria-disabled with a click guard instead of disabled, and mark the explanation as a live region.

♿ Proposed fix
-      <div className="space-y-2">
+      <div className="space-y-2" role="group" aria-label={question}>
         {options.map((o, i) => {
           const isCorrect = Boolean(o.correct);
           const reveal = answered && (i === picked || isCorrect);
           return (
             <button
               key={i}
               type="button"
-              disabled={answered}
-              onClick={() => setPicked(i)}
+              aria-disabled={answered}
+              onClick={() => {
+                if (!answered) setPicked(i);
+              }}
               className={cn(
                 "flex min-h-11 w-full items-center gap-3 rounded-xl border px-4 py-2.5 text-left text-sm leading-5 transition-colors duration-150",
-                !reveal && "border-charcoal-700 bg-charcoal-800 text-bright enabled:hover:bg-charcoal-700",
+                !reveal && "border-charcoal-700 bg-charcoal-800 text-bright aria-disabled:hover:bg-charcoal-800 [&:not([aria-disabled=true])]:hover:bg-charcoal-700",
                 reveal && isCorrect && "border-apple-500/60 bg-apple-500/10 text-apple-200",
                 reveal && !isCorrect && "border-error/60 bg-error/10 text-error"
               )}
             >
       {answered && explanation && (
-        <p className="mt-4 border-t border-grid-bright pt-3 text-sm leading-relaxed text-dimmed">{explanation}</p>
+        <p role="status" className="mt-4 border-t border-grid-bright pt-3 text-sm leading-relaxed text-dimmed">
+          {explanation}
+        </p>
       )}
📝 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
<div className="space-y-2">
{options.map((o, i) => {
const isCorrect = Boolean(o.correct);
const reveal = answered && (i === picked || isCorrect);
return (
<button
key={i}
type="button"
disabled={answered}
onClick={() => setPicked(i)}
className={cn(
"flex min-h-11 w-full items-center gap-3 rounded-xl border px-4 py-2.5 text-left text-sm leading-5 transition-colors duration-150",
!reveal && "border-charcoal-700 bg-charcoal-800 text-bright enabled:hover:bg-charcoal-700",
reveal && isCorrect && "border-apple-500/60 bg-apple-500/10 text-apple-200",
reveal && !isCorrect && "border-error/60 bg-error/10 text-error"
)}
>
{reveal && isCorrect && <CheckCircle2 className="size-4 shrink-0 text-apple-500" />}
{reveal && !isCorrect && <XCircle className="size-4 shrink-0 text-error" />}
<span>{o.text}</span>
</button>
);
})}
</div>
{answered && explanation && (
<p className="mt-4 border-t border-grid-bright pt-3 text-sm leading-relaxed text-dimmed">{explanation}</p>
)}
<div className="space-y-2" role="group" aria-label={question}>
{options.map((o, i) => {
const isCorrect = Boolean(o.correct);
const reveal = answered && (i === picked || isCorrect);
return (
<button
key={i}
type="button"
aria-disabled={answered}
onClick={() => {
if (!answered) setPicked(i);
}}
className={cn(
"flex min-h-11 w-full items-center gap-3 rounded-xl border px-4 py-2.5 text-left text-sm leading-5 transition-colors duration-150",
!reveal && "border-charcoal-700 bg-charcoal-800 text-bright aria-disabled:hover:bg-charcoal-800 [&:not([aria-disabled=true])]:hover:bg-charcoal-700",
reveal && isCorrect && "border-apple-500/60 bg-apple-500/10 text-apple-200",
reveal && !isCorrect && "border-error/60 bg-error/10 text-error"
)}
>
{reveal && isCorrect && <CheckCircle2 className="size-4 shrink-0 text-apple-500" />}
{reveal && !isCorrect && <XCircle className="size-4 shrink-0 text-error" />}
<span>{o.text}</span>
</button>
);
})}
</div>
{answered && explanation && (
<p role="status" className="mt-4 border-t border-grid-bright pt-3 text-sm leading-relaxed text-dimmed">
{explanation}
</p>
)}
🤖 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 `@trigger-chat-agent/src/components/quiz.tsx` around lines 36 - 62, Update the
option buttons rendered in the options.map callback to remain focusable after
answered: replace the disabled attribute with aria-disabled and guard the
onClick handler so setPicked only runs before answering. Mark the conditional
explanation paragraph as an assertive live region so its appearance is announced
to assistive technologies.

Comment on lines +73 to +93
function AnimatedValue({ value, active, reduceMotion }: { value: string; active: boolean; reduceMotion: boolean }) {
const match = value.match(/^([^\d]*)([\d,]*\.?\d+)(.*)$/);
const [display, setDisplay] = useState(value);

useEffect(() => {
if (!match || !active || reduceMotion) {
setDisplay(value);
return;
}
const [, prefix, numText, suffix] = match;
const target = Number(numText.replace(/,/g, ""));
const decimals = numText.includes(".") ? numText.split(".")[1].length : 0;
const controls = animate(0, target, {
duration: 1,
delay: 0.25,
ease: easings.outExpo,
onUpdate: (v) => setDisplay(`${prefix}${v.toFixed(decimals)}${suffix}`),
});
return () => controls.stop();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, reduceMotion, value]);

Copy link
Copy Markdown

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

Preserve digit grouping during the animation.

Line 89 removes commas from a formatted value. If value is "1,234", the final animated value is "1234".

Format the animated number when numText contains grouping separators.

Proposed fix
-      onUpdate: (v) => setDisplay(`${prefix}${v.toFixed(decimals)}${suffix}`),
+      onUpdate: (v) => {
+        const numeric = v.toFixed(decimals);
+        const formatted = numText.includes(",")
+          ? numeric.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
+          : numeric;
+        setDisplay(`${prefix}${formatted}${suffix}`);
+      },
📝 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 AnimatedValue({ value, active, reduceMotion }: { value: string; active: boolean; reduceMotion: boolean }) {
const match = value.match(/^([^\d]*)([\d,]*\.?\d+)(.*)$/);
const [display, setDisplay] = useState(value);
useEffect(() => {
if (!match || !active || reduceMotion) {
setDisplay(value);
return;
}
const [, prefix, numText, suffix] = match;
const target = Number(numText.replace(/,/g, ""));
const decimals = numText.includes(".") ? numText.split(".")[1].length : 0;
const controls = animate(0, target, {
duration: 1,
delay: 0.25,
ease: easings.outExpo,
onUpdate: (v) => setDisplay(`${prefix}${v.toFixed(decimals)}${suffix}`),
});
return () => controls.stop();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, reduceMotion, value]);
function AnimatedValue({ value, active, reduceMotion }: { value: string; active: boolean; reduceMotion: boolean }) {
const match = value.match(/^([^\d]*)([\d,]*\.?\d+)(.*)$/);
const [display, setDisplay] = useState(value);
useEffect(() => {
if (!match || !active || reduceMotion) {
setDisplay(value);
return;
}
const [, prefix, numText, suffix] = match;
const target = Number(numText.replace(/,/g, ""));
const decimals = numText.includes(".") ? numText.split(".")[1].length : 0;
const controls = animate(0, target, {
duration: 1,
delay: 0.25,
ease: easings.outExpo,
onUpdate: (v) => {
const numeric = v.toFixed(decimals);
const formatted = numText.includes(",")
? numeric.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
: numeric;
setDisplay(`${prefix}${formatted}${suffix}`);
},
});
return () => controls.stop();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, reduceMotion, value]);
🤖 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 `@trigger-chat-agent/src/components/stat-card.tsx` around lines 73 - 93, Update
the onUpdate formatting in AnimatedValue so animated numbers retain digit
grouping when numText contains commas. Format the interpolated value with
grouping separators before combining it with prefix and suffix, while preserving
decimal precision and existing ungrouped-number behavior.

Comment on lines +11 to +40
class VisualizationErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
state = { failed: false };

static getDerivedStateFromError() {
return { failed: true };
}

render() {
if (this.state.failed) {
return (
<div className="my-3 rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground">
Couldn&apos;t render this visualization.
</div>
);
}
return this.props.children;
}
}

export function Visualization({ spec }: { spec: VisualizationSpec }) {
return (
<div>
<VisualizationErrorBoundary>
<JSONUIProvider registry={registry}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
</VisualizationErrorBoundary>
</div>
);
}

Copy link
Copy Markdown

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

Reset the error boundary when the spec changes.

failed is never cleared. MessagePart in trigger-chat-agent/src/components/chat.tsx keys parts by group index, so React reuses this boundary instance when the model repairs a rejected spec and streams a new one. The slot then stays on "Couldn't render this visualization." for the rest of the session. Key the boundary on the spec so a new spec mounts a fresh instance. Add componentDidCatch logging so the cause is visible during development.

🐛 Proposed fix
 class VisualizationErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
   state = { failed: false };
 
   static getDerivedStateFromError() {
     return { failed: true };
   }
 
+  componentDidCatch(error: unknown) {
+    console.error("Visualization render failed", error);
+  }
+
   render() {
@@
 export function Visualization({ spec }: { spec: VisualizationSpec }) {
+  // Remount on a new spec so a repaired spec is not blocked by the previous failure.
+  const specKey = JSON.stringify(spec);
   return (
     <div>
-      <VisualizationErrorBoundary>
+      <VisualizationErrorBoundary key={specKey}>
         <JSONUIProvider registry={registry}>
           <Renderer spec={spec} registry={registry} />
         </JSONUIProvider>
       </VisualizationErrorBoundary>
     </div>
   );
 }
📝 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
class VisualizationErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
state = { failed: false };
static getDerivedStateFromError() {
return { failed: true };
}
render() {
if (this.state.failed) {
return (
<div className="my-3 rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground">
Couldn&apos;t render this visualization.
</div>
);
}
return this.props.children;
}
}
export function Visualization({ spec }: { spec: VisualizationSpec }) {
return (
<div>
<VisualizationErrorBoundary>
<JSONUIProvider registry={registry}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
</VisualizationErrorBoundary>
</div>
);
}
class VisualizationErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
state = { failed: false };
static getDerivedStateFromError() {
return { failed: true };
}
componentDidCatch(error: unknown) {
console.error("Visualization render failed", error);
}
render() {
if (this.state.failed) {
return (
<div className="my-3 rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground">
Couldn&apos;t render this visualization.
</div>
);
}
return this.props.children;
}
}
export function Visualization({ spec }: { spec: VisualizationSpec }) {
// Remount on a new spec so a repaired spec is not blocked by the previous failure.
const specKey = JSON.stringify(spec);
return (
<div>
<VisualizationErrorBoundary key={specKey}>
<JSONUIProvider registry={registry}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
</VisualizationErrorBoundary>
</div>
);
}
🤖 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 `@trigger-chat-agent/src/components/visualization.tsx` around lines 11 - 40,
Update VisualizationErrorBoundary usage in Visualization to key the boundary by
spec, ensuring repaired specifications mount a fresh boundary and reset failed
state. Add componentDidCatch to VisualizationErrorBoundary to log rendering
errors during development without changing the existing fallback UI.

Comment on lines +304 to +311
for (const child of element.children ?? []) {
if (!spec.elements[child]) {
errors.push(`elements.${key}: child "${child}" is not a key in elements`);
}
}
}

return errors.length > 0 ? { ok: false, errors } : { ok: true };

Copy link
Copy Markdown

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 cyclic children references.

validateSpec checks only that each child key exists. A spec where two elements list each other as children passes validation. The client Renderer then walks the graph without a depth bound, which hangs the tab or overflows the stack. The model produces these keys, so the input is not trusted.

Add a reachability walk with a visited set.

🐛 Proposed fix: detect cycles from the root
   for (const child of element.children ?? []) {
     if (!spec.elements[child]) {
       errors.push(`elements.${key}: child "${child}" is not a key in elements`);
     }
   }
 }
 
+  // Guard the renderer: a cycle in `children` makes the client recurse forever.
+  const visiting = new Set<string>();
+  const done = new Set<string>();
+  const walk = (key: string) => {
+    if (done.has(key)) return;
+    if (visiting.has(key)) {
+      errors.push(`elements.${key}: children form a cycle`);
+      return;
+    }
+    visiting.add(key);
+    for (const child of spec.elements[key]?.children ?? []) {
+      if (spec.elements[child]) walk(child);
+    }
+    visiting.delete(key);
+    done.add(key);
+  };
+  if (spec.elements[spec.root]) walk(spec.root);
+
   return errors.length > 0 ? { ok: false, errors } : { ok: true };
📝 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
for (const child of element.children ?? []) {
if (!spec.elements[child]) {
errors.push(`elements.${key}: child "${child}" is not a key in elements`);
}
}
}
return errors.length > 0 ? { ok: false, errors } : { ok: true };
for (const child of element.children ?? []) {
if (!spec.elements[child]) {
errors.push(`elements.${key}: child "${child}" is not a key in elements`);
}
}
}
// Guard the renderer: a cycle in `children` makes the client recurse forever.
const visiting = new Set<string>();
const done = new Set<string>();
const walk = (key: string) => {
if (done.has(key)) return;
if (visiting.has(key)) {
errors.push(`elements.${key}: children form a cycle`);
return;
}
visiting.add(key);
for (const child of spec.elements[key]?.children ?? []) {
if (spec.elements[child]) walk(child);
}
visiting.delete(key);
done.add(key);
};
if (spec.elements[spec.root]) walk(spec.root);
return errors.length > 0 ? { ok: false, errors } : { ok: 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 `@trigger-chat-agent/src/lib/catalog.ts` around lines 304 - 311, Update
validateSpec to perform a reachability walk from the root element using a
visited set, following each element’s children and rejecting any node
encountered more than once as a cyclic or repeated reference. Preserve the
existing missing-child-key validation and return the accumulated validation
errors through the current result shape.

Comment on lines +48 to +67
async function loadDocsTools(): Promise<ToolSet> {
try {
const client = await createMCPClient({
transport: { type: "http", url: DOCS_MCP_URL },
});
// Kept open for the life of the run process — the returned tools close over
// the client to execute, so we never call client.close() here.
return quarantineDocsTools(await client.tools());
} catch (error) {
logger.warn("Docs MCP unavailable — continuing without doc grounding", {
url: DOCS_MCP_URL,
error: error instanceof Error ? error.message : String(error),
});
return {};
}
}

function getDocsTools(): Promise<ToolSet> {
return (docsToolsPromise ??= loadDocsTools());
}

Copy link
Copy Markdown

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

🧩 Analysis chain

🌐 Web query:

ai-sdk MCP createMCPClient http transport options abortSignal timeout

💡 Result:

In the AI SDK's MCP implementation, timeout and AbortSignal support are managed through RequestOptions and initializationOptions to control the lifecycle of the client and individual requests [1][2]. Initialization Timeouts and AbortSignals You can bound the initialization process of an MCP client by providing initializationOptions in the createMCPClient configuration [2]. This configuration accepts RequestOptions, which include timeout (in milliseconds) and an AbortSignal [1][2]. - When a timeout is specified, the client will automatically trigger an abort if the initialization does not complete within the given duration [2]. - If an AbortSignal is provided (either directly or via AbortSignal.any if combining with a timeout), the initialization will respect that signal [2]. Request-Level Timeouts and AbortSignals For individual operations such as tool execution (e.g., callTool), you can pass options that support AbortSignal [2]. Recent updates to the AI SDK have addressed previous limitations regarding request cancellation [1][3]. - The RequestOptions object used in client operations allows for defining timeout and maxTotalTimeout [1]. - These settings ensure that if a server fails to respond or hangs, the client will clean up internal handlers and reject the promise, preventing memory leaks and orphaned requests [1][3]. Implementation Note When using the HTTP transport, the AI SDK's transport layer propagates these cancellation signals to the underlying fetch or stream requests [1][2]. If you are using a custom transport, ensure it is designed to respect the cancellation signals propagated from the client's request methods [1]. Always ensure that the client is properly closed (e.g., using client.close()) to free up transport resources, even if an initialization or request error occurs [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -i 'trigger-chat-agent.ts|package.json|pnpm-lock.yaml|package-lock.json|yarn.lock' . | head -80
printf '%s\n' '--- relevant source references ---'
rg -n -C 8 'createMCPClient|docsToolsPromise|loadDocsTools|getDocsTools|DOCS_MCP_URL|`@ai-sdk/mcp`|abortSignal|initializationOptions' . \
  -g '*.ts' -g '*.tsx' -g '*.json' -g '*lock*' | head -300

Repository: triggerdotdev/examples

Length of output: 19857


🏁 Script executed:

#!/bin/bash
set -eu
base='https://unpkg.com/@ai-sdk/mcp@1.0.66'
for path in /dist/index.d.ts /dist/index.js /dist/tool/mcp-client.d.ts /dist/tool/mcp-client.js /package.json; do
  printf '\n--- %s ---\n' "$path"
  curl -fsSL "$base$path" | head -240
done

Repository: triggerdotdev/examples

Length of output: 22187


🏁 Script executed:

#!/bin/bash
set -eu
base='https://unpkg.com/@ai-sdk/mcp@1.0.66'
for path in /dist/index.d.ts /src/index.ts /src/tool/mcp-client.ts /README.md; do
  printf '\n--- %s ---\n' "$path"
  curl -fsSL "$base$path" |
    grep -n -E -C 12 'createMCPClient|initializationOptions|RequestOptions|abortSignal|timeout|tools\(' |
    head -320 || true
done

Repository: triggerdotdev/examples

Length of output: 20543


🏁 Script executed:

#!/bin/bash
set -eu
curl -fsSL 'https://unpkg.com/@ai-sdk/mcp@1.0.66/src/tool/mcp-client.ts' |
  sed -n '440,760p'

Repository: triggerdotdev/examples

Length of output: 8740


🏁 Script executed:

#!/bin/bash
set -eu
curl -fsSL 'https://unpkg.com/@ai-sdk/mcp@1.0.66/src/tool/mcp-client.ts' |
  grep -n -A90 -B12 -E 'async tools|toolsFromDefinitions|listTools\(' |
  head -260
printf '\n--- runtime and project metadata ---\n'
cat trigger-chat-agent/package.json
printf '\n--- relevant source ---\n'
cat -n trigger-chat-agent/src/trigger/trigger-chat-agent.ts | sed -n '44,70p;245,290p'

Repository: triggerdotdev/examples

Length of output: 12790


Bound MCP initialization and tool discovery, and retry failed loads.

loadDocsTools returns {} on failure, and docsToolsPromise caches that result. A transient outage then disables documentation grounding for the rest of the process. Clear the cached promise only when loading fails. Do not treat a valid empty tool set as a failure.

createMCPClient accepts timeout settings through initializationOptions, not top-level abortSignal. Also, client.tools() performs an unbounded tools/list request. Bound initialization with initializationOptions: { timeout: 10_000 }, and bound tool discovery with listTools({ options: { timeout: 10_000 } }) followed by toolsFromDefinitions(...).

🤖 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 `@trigger-chat-agent/src/trigger/trigger-chat-agent.ts` around lines 48 - 67,
Update loadDocsTools to initialize createMCPClient with
initializationOptions.timeout set to 10,000 ms, and replace client.tools() with
listTools({ options: { timeout: 10_000 } }) followed by
toolsFromDefinitions(...). Clear docsToolsPromise only when loading throws so
failed loads can retry, while preserving a valid empty tool set as a successful
cached result.

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.

1 participant