@@ -49,7 +49,13 @@ import type { ProbeContext, ProbeResult } from "../../types/index.js";
4949 * HTML error page, a JSON body lacking `version`, or any
5050 * 4xx FAILS. A real `{version}` info response proves the
5151 * v2 runtime is mounted and answering.
52- * - **chat** POST `${base}/api/copilotkit/agent/default/run` with
52+ * - **chat** POST `${base}/api/copilotkit/agent/<id>/run` where
53+ * `<id>` is resolved PER-STARTER from the `/info` `agents`
54+ * map (`Object.keys(info.agents)[0]`, the same `info`
55+ * response the health + agent rungs fetch) — `default` for
56+ * the 11 default-registering starters, a dynamic key (e.g.
57+ * `weatherAgent`) for mastra, falling back to `default`
58+ * only when the map is empty/unreadable — with
5359 * `Accept: text/event-stream` and the AG-UI run body
5460 * `{threadId, runId, messages, state, tools, context,
5561 * forwardedProps}`. Read the AG-UI SSE stream and require
@@ -154,6 +160,14 @@ export interface StarterSmokeLevelSignal {
154160 /** Keyed failure class (red rows only). */
155161 errorClass ?: StarterFailureClass ;
156162 latencyMs : number ;
163+ /**
164+ * The agent id this level resolved/targeted, for drilldown. Set on the chat
165+ * rung (the id read from `/info` `agents` and POSTed at
166+ * `/agent/<id>/run`); the EXPECTED value is `default` for the 11
167+ * default-registering starters and a dynamic key (e.g. `weatherAgent`) for
168+ * mastra. Absent on the other rungs.
169+ */
170+ agentId ?: string ;
157171}
158172
159173/**
@@ -171,14 +185,19 @@ const DEFAULT_TIMEOUT_MS = 30_000;
171185const TIMEOUT_ENV_VAR = "STARTER_SMOKE_TIMEOUT_MS" ;
172186
173187/**
174- * Canonical agent id every starter registers in its `agents` map
175- * (`examples/integrations/<slug>/src/app/api/copilotkit/[[...slug]]/route.ts`
176- * registers `{ default: new <Framework>Agent(...) }`). The path-based chat
177- * rung targets `/agent/${CHAT_AGENT_ID}/run`; `matchRoute` reads the id from
178- * the URL segment. Confirmed both in the starter routes and via a deployed
179- * starter's `info` `agents` map.
188+ * LAST-RESORT agent id for the path-based chat rung when `/info` advertises
189+ * no readable `agents` map. Most starters register `{ default: ... }`
190+ * (`examples/integrations/<slug>/src/app/api/copilotkit/[[...slug]]/route.ts`),
191+ * so `default` is the EXPECTED resolved value for those 11 starters — NOT an
192+ * error. But some starters register DYNAMIC non-`default` keys: mastra wires
193+ * `MastraAgent.getLocalAgents(...)`, whose keys are the Mastra agent names
194+ * (e.g. `weatherAgent`), so a hardcoded `/agent/default/run` 404s for it. The
195+ * driver therefore resolves the chat agent id PER-STARTER from the `/info`
196+ * `agents` map (`Object.keys(info.agents)[0]`, the first registered agent),
197+ * the same `info` response the health + agent rungs already fetch, and only
198+ * falls back to this constant when that map is empty/unreadable.
180199 */
181- const CHAT_AGENT_ID = "default" ;
200+ const FALLBACK_CHAT_AGENT_ID = "default" ;
182201
183202/**
184203 * The AG-UI run body for the chat rung. Shape matches
@@ -277,19 +296,33 @@ export function createStarterSmokeDriver(
277296 // path, so health/agent GET `/api/copilotkit/info` and chat POSTs the
278297 // per-agent run path `/api/copilotkit/agent/<id>/run`. The dead
279298 // single-route envelope POST to bare `/api/copilotkit` is never issued.
280- const levelUrls : Record < StarterLevel , string > = {
281- health : `${ base } /api/copilotkit/info` ,
282- agent : `${ base } /api/copilotkit/info` ,
283- chat : `${ base } /api/copilotkit/agent/${ encodeURIComponent ( CHAT_AGENT_ID ) } /run` ,
284- interaction : `${ base } /` ,
299+ // The chat rung's agent id is resolved PER-STARTER from the `/info`
300+ // `agents` map read by the agent rung (which runs before chat in
301+ // STARTER_LEVELS order). It starts at the last-resort fallback and is
302+ // overwritten once the agent rung reads a non-empty `agents` map, so the
303+ // chat POST targets the agent the deployed starter actually registered
304+ // (mastra registers a dynamic non-`default` key).
305+ let resolvedChatAgentId = FALLBACK_CHAT_AGENT_ID ;
306+ const chatUrlFor = ( agentId : string ) : string =>
307+ `${ base } /api/copilotkit/agent/${ encodeURIComponent ( agentId ) } /run` ;
308+ const urlForLevel = ( level : StarterLevel ) : string => {
309+ switch ( level ) {
310+ case "health" :
311+ case "agent" :
312+ return `${ base } /api/copilotkit/info` ;
313+ case "chat" :
314+ return chatUrlFor ( resolvedChatAgentId ) ;
315+ case "interaction" :
316+ return `${ base } /` ;
317+ }
285318 } ;
286319
287320 const failed : StarterLevel [ ] = [ ] ;
288321 let passed = 0 ;
289322 let worstClass : StarterFailureClass | undefined ;
290323
291324 for ( const level of STARTER_LEVELS ) {
292- const url = levelUrls [ level ] ;
325+ const url = urlForLevel ( level ) ;
293326 // Short-circuit on an external (outer-timeout) abort BEFORE issuing a
294327 // fresh fetch. Without this, an abort mid-tick still fires a cold-start
295328 // request for every remaining level — wasting wake requests and
@@ -317,6 +350,14 @@ export function createStarterSmokeDriver(
317350 } ) ;
318351 }
319352
353+ // Capture the agent id the agent rung resolved from `/info` so the
354+ // chat rung (next in STARTER_LEVELS order) POSTs the per-agent run path
355+ // the starter actually registered. A missing resolution leaves the
356+ // last-resort fallback in place.
357+ if ( level === "agent" && result . resolvedAgentId ) {
358+ resolvedChatAgentId = result . resolvedAgentId ;
359+ }
360+
320361 const state = result . ok ? "green" : "red" ;
321362 if ( result . ok ) {
322363 passed ++ ;
@@ -337,6 +378,10 @@ export function createStarterSmokeDriver(
337378 errorDesc : result . errorDesc ,
338379 errorClass : result . ok ? undefined : result . errorClass ,
339380 latencyMs : result . latencyMs ,
381+ // Surface the resolved chat agent id on the chat row for
382+ // drilldown (the EXPECTED value is `default` for the 11
383+ // default-registering starters, a dynamic key for mastra).
384+ ...( level === "chat" ? { agentId : resolvedChatAgentId } : { } ) ,
340385 } ,
341386 observedAt : ctx . now ( ) . toISOString ( ) ,
342387 } ) ;
@@ -367,6 +412,13 @@ interface LevelOutcome {
367412 errorDesc ?: string ;
368413 errorClass ?: StarterFailureClass ;
369414 latencyMs : number ;
415+ /**
416+ * Agent rung only: the agent id resolved from the `/info` `agents` map
417+ * (`Object.keys(info.agents)[0]`), used to drive the chat rung's per-agent
418+ * run path. Absent when the map is empty/unreadable (the caller then keeps
419+ * the last-resort fallback). Other rungs never set this.
420+ */
421+ resolvedAgentId ?: string ;
370422}
371423
372424/**
@@ -500,7 +552,16 @@ async function probeLevel(opts: {
500552 latencyMs,
501553 } ;
502554 }
503- return { ok : true , status : res . status , latencyMs } ;
555+ // Resolve the chat agent id from the SAME `/info` body the version check
556+ // just validated, so the chat rung targets the agent the starter
557+ // actually registered (mastra uses a dynamic non-`default` key) without a
558+ // second `info` fetch. Absent → caller keeps the last-resort fallback.
559+ return {
560+ ok : true ,
561+ status : res . status ,
562+ latencyMs,
563+ resolvedAgentId : resolveAgentId ( body ) ?? undefined ,
564+ } ;
504565 }
505566
506567 // chat: require an SSE stream carrying assistant text content.
@@ -709,6 +770,35 @@ function verifyInfoVersion(body: string): string | null {
709770 return null ;
710771}
711772
773+ /**
774+ * Resolve the chat agent id from an `info` response body: the FIRST key of its
775+ * `agents` map (a `Record<string, AgentDescription>`). Most starters register
776+ * `{ default: ... }` → `default`; mastra registers dynamic keys (e.g.
777+ * `weatherAgent`). Returns null when the body is unparseable, has no `agents`
778+ * map, or the map is empty — the caller then keeps the last-resort fallback.
779+ * Order follows JS object insertion order, matching the runtime's
780+ * registration order.
781+ */
782+ function resolveAgentId ( body : string ) : string | null {
783+ let parsed : unknown ;
784+ try {
785+ parsed = JSON . parse ( body ) ;
786+ } catch {
787+ return null ;
788+ }
789+ if ( parsed === null || typeof parsed !== "object" || Array . isArray ( parsed ) ) {
790+ return null ;
791+ }
792+ const agents = ( parsed as Record < string , unknown > ) [ "agents" ] ;
793+ if ( agents === null || typeof agents !== "object" || Array . isArray ( agents ) ) {
794+ return null ;
795+ }
796+ const keys = Object . keys ( agents as Record < string , unknown > ) ;
797+ if ( keys . length === 0 ) return null ;
798+ const first = keys [ 0 ] ;
799+ return first . trim ( ) . length > 0 ? first : null ;
800+ }
801+
712802/**
713803 * Validate a chat `agent/run` SSE body. A passing stream must satisfy ALL of:
714804 * - ≥1 `TEXT_MESSAGE_CONTENT`/`TEXT_MESSAGE_CHUNK` event with a non-empty
0 commit comments