Skip to content

Commit 91ec205

Browse files
feat: AG-UI core interop - spec-compliant event types (#411)
* feat(ai): add @ag-ui/core as dependency for spec-compliant event types * feat(ai): redefine AG-UI event types extending @ag-ui/core Replace custom AG-UI event types with interfaces that extend @ag-ui/core types for spec compliance. This is the foundational type change for the AG-UI protocol alignment. - Import all event types from @ag-ui/core with AGUI* aliases - Replace BaseAGUIEvent to extend @ag-ui/core BaseEvent - Replace each event interface to extend its @ag-ui/core equivalent - Add TanStack-internal extension fields (model, deprecated aliases) - Add new event types: ToolCallResultEvent, Reasoning* events - Deprecate AGUIEventType in favor of EventType enum - Re-export EventType enum from @ag-ui/core - Add threadId/runId to TextOptions interface - Update AGUIEvent union and StreamChunk type alias * feat(ai): add stripToSpec middleware to strip non-spec fields from stream events Creates a middleware that removes TanStack-internal extension fields (model, rawEvent, deprecated aliases) from StreamChunk events so the yielded stream is @ag-ui/core spec-compliant. Registered as the last middleware in the chat activity chain so devtools and user middleware still see the full extended events. * feat(ai): plumb threadId and runId through chat() to adapters Add threadId/runId to TextActivityOptions interface and TextEngine class so they flow from user-facing chat() options through to adapter.chatStream(). ThreadId is auto-generated if not provided. Adapters will consume these in subsequent tasks to include them in RUN_STARTED/RUN_FINISHED events. * feat(ai-openai): update text adapter for AG-UI spec compliance Add threadId to RUN_STARTED/RUN_FINISHED events, toolCallName to TOOL_CALL_START/TOOL_CALL_END, stepName to STEP_STARTED/STEP_FINISHED, flatten RUN_ERROR with top-level message/code fields, and emit REASONING_START/MESSAGE_START/CONTENT/MESSAGE_END/END events alongside legacy STEP events for reasoning content. * fix: update tests and fix type errors for AG-UI spec compliance Update test utilities and tests to use AG-UI spec field names: - Add threadId to RUN_STARTED/RUN_FINISHED events - Add toolCallName alongside deprecated toolName on tool events - Add stepName alongside deprecated stepId on step events - Use flat message field on RUN_ERROR (with deprecated error nested form) Fix critical bugs discovered during testing: - StreamProcessor: prefer chunk.message over chunk.error?.message for RUN_ERROR - TextEngine: process original chunks for internal state before middleware strips fields - Remove auto-applied stripToSpecMiddleware from chat() (breaks internal state since it strips finishReason, delta, content needed by TextEngine and StreamProcessor) - Fix type compatibility issues with @ag-ui/core EventType enum vs string literals Also fix type errors in: - stream-generation-result.ts: use EventType enum and add threadId - generateVideo/index.ts: add StreamChunk casts and threadId - tool-calls.ts: cast TOOL_CALL_END yield to ToolCallEndEvent - devtools-middleware.ts: handle toolCallName fallback and RUN_ERROR message field - processor.ts: handle developer role, Messages snapshot type cast, finishReason undefined * fix(ai): re-add stripToSpec middleware, process raw chunks internally, fix test assertions * fix(ai): pipe tool-phase events through middleware, strip toolCallName, fix type errors - Fix EventType enum vs string literal type errors in test files by relaxing chunk helper type params and adding cast helpers - Pipe tool-phase events (TOOL_CALL_END, TOOL_CALL_RESULT, CUSTOM) through the middleware pipeline so strip-to-spec and devtools middleware observe all events, not just model-stream events - Add toolCallName to TOOL_CALL_END strip set in strip-to-spec middleware since AG-UI spec ToolCallEndEvent only has toolCallId - Update test assertions to use TOOL_CALL_RESULT (spec event) instead of checking stripped fields on TOOL_CALL_END * style: format test files * test(ai): add tests for REASONING events, TOOL_CALL_RESULT, threadId, and strip compliance * fix: resolve eslint, type, and test failures across all packages - Fix 5 ESLint errors in @tanstack/ai (array-type, no-unnecessary-condition, no-unnecessary-type-assertion, sort-imports) - Fix ESLint error in @tanstack/ai-event-client (no-unnecessary-condition) - Fix string literal vs EventType enum type errors across all 7 adapter packages by adding asChunk helper that casts event objects to StreamChunk - Fix @tanstack/ai-client source type errors (chunk.error possibly undefined, runId access on RUN_ERROR events, connection-adapters push calls) - Fix @tanstack/ai-client and @tanstack/ai-openrouter test type errors - Fix tool-call-manager tests to use toolCallName instead of deprecated toolName * fix: resolve eslint and test failures from strip-to-spec middleware Remove assertions for fields (content, finishReason, usage) that the stripToSpec middleware now strips from emitted events. Fix unnecessary nullish coalescing in ai-openai and add type casts in ai-vue tests. * fix: honor caller runId, prevent duplicate thinking, add error IDs, fix reasoning ordering - Honor caller-provided runId/threadId in all 7 adapters using ?? fallback - Prevent duplicate thinking content from dual STEP_FINISHED/REASONING_MESSAGE_CONTENT events - Assert exact threadId value in chat test instead of just toBeDefined - Add runId/threadId to RUN_ERROR in generateVideo and stream-generation-result - Move reasoning processing before content processing in OpenRouter adapter * fix(smoke-tests): update harness to read spec-compliant event fields TOOL_CALL_START now uses toolCallName (spec) instead of toolName (deprecated). TOOL_CALL_END fields (toolName, input, result) are stripped by spec middleware; harness now falls back to data captured during START/ARGS phases. Added TOOL_CALL_RESULT handler for spec-compliant tool result delivery. RUN_FINISHED finishReason/usage are optional extensions. * ci: apply automated fixes * fix(smoke-tests): remove unnecessary as-any casts, use proper type narrowing * ci: apply automated fixes * fix(ai-ollama): emit TOOL_CALL_ARGS before TOOL_CALL_END for spec compliance Ollama doesn't stream tool args incrementally — it delivers them all at once in TOOL_CALL_END.input. Since the strip middleware removes input from TOOL_CALL_END, consumers had no way to get the args. Now emits a TOOL_CALL_ARGS event with the full args as delta before TOOL_CALL_END. * fix(ai): hide strip-to-spec middleware from devtools instrumentation * fix(ai): handle TOOL_CALL_RESULT in StreamProcessor to create tool-result parts Root cause: The strip middleware removes 'result' from TOOL_CALL_END events. The StreamProcessor's TOOL_CALL_END handler only creates tool-result parts when chunk.result is present. With it stripped, no tool-result parts were created on the client side. TOOL_CALL_RESULT events (spec-compliant tool result delivery) were received but ignored (no-op). Without tool-result parts, areAllToolsComplete() behaved incorrectly, and the client could not detect server tool completion. Fix: Handle TOOL_CALL_RESULT by creating tool-result parts and updating tool-call output, mirroring TOOL_CALL_END's result handling logic. * fix(ai): stop stripping finishReason from RUN_FINISHED events finishReason is essential for client-side continuation logic. Without it, the chat-client cannot distinguish 'stop' (no continuation needed) from 'tool_calls' (client tools need execution), causing infinite request loops when server-side tool results leave tool-call parts as the last message part. * fix(ai): only strip deprecated aliases and rawEvent, keep all extras @ag-ui/core BaseEventSchema uses .passthrough(), so extra fields are allowed and won't break spec validation. Only strip: - Deprecated aliases: toolName, stepId, state (nudge toward spec names) - Deprecated nested error object on RUN_ERROR - rawEvent (debug payload, potentially large) Keep everything else: model, content, args, usage, finishReason, input, result, index, providerMetadata, stepType, delta, etc. * ci: apply automated fixes * fix(ai): stop stripping fields — passthrough allows all extras @ag-ui/core BaseEventSchema uses .passthrough() so extra fields are allowed. Only strip the deprecated nested error object from RUN_ERROR (conflicts with spec's flat message/code). Everything else passes through: model, content, toolName, stepId, usage, finishReason, result, input, args, etc. * fix: resolve type errors from @ag-ui/core Zod passthrough types Zod passthrough adds `& { [k: string]: unknown }` to inferred types, preventing TypeScript from narrowing the `type` discriminant in switch statements. Add explicit casts where needed. Also fix toolCallName -> toolName rename in realtime types to match consumer code. * ci: apply automated fixes * chore(ai): bump @ag-ui/core from 0.0.48 to 0.0.49 Removes rxjs from the transitive dependency tree. All exported types and EventType enum values are identical between versions. * fix: CR fixes for AG-UI core interop - Use this.threadId in createSyntheticFinishedEvent instead of regenerating a new ID on each call - Add defensive delta guard in handleReasoningMessageContentEvent matching sibling handler patterns - Prefer spec chunk.message over deprecated chunk.error in devtools middleware, generation client, and video generation client - Add flat message field to synthesized RUN_ERROR in connection adapters - Fix processChunk JSDoc listing RUN_STARTED as ignored (it has a handler) - Fix comment referencing toolName when code uses toolCallName - Document RUN_ERROR in stream-generation-result @returns - Add meaningful assertions to TOOL_CALL_RESULT processor test - Clarify threadId test describes adapter passthrough behavior * ci: apply automated fixes * fix(ai-client): use cast for RUN_ERROR message to satisfy eslint chunk.message is typed as required string by @ag-ui/core but may be absent at runtime from events constructed via as-unknown casts. Cast to string|undefined to allow the || fallback chain while keeping the no-unnecessary-condition rule happy. * fix(ai): add StreamChunk casts for TOOL_CALL_START/ARGS in continuation re-executions The merge from main brought in #372 which emits these events but used string literals instead of the AGUIEvent enum, breaking the build. * fix(ai-openrouter): prevent duplicate TEXT_MESSAGE_END and RUN_FINISHED events OpenAI-compatible APIs often send two chunks with finishReason — one for the finish signal and a separate trailing chunk carrying usage data. The adapter had no guard against this, causing TEXT_MESSAGE_END and RUN_FINISHED to be emitted twice per run. Root cause: processChoice emitted finish events on every finishReason occurrence without tracking whether they had already been sent. Fix: - Add hasEmittedRunFinished / hasEmittedTextMessageEnd guards to AGUIState - Accumulate usage from any finishReason chunk into deferredUsage - Move RUN_FINISHED emission to after the stream loop so it always carries the latest usage data (even when it arrives on a later chunk) Adds tests for duplicate-finish-chunk scenarios, usage preservation, and event ordering. * fix(ai-openrouter, ai): emit single STEP_FINISHED per reasoning block, remove [DONE] sentinel STEP_FINISHED was emitted on every reasoning delta (N events for N deltas) but only one STEP_STARTED was emitted, causing verifiers to report orphan STEP_FINISHED events. Move the single STEP_FINISHED to the point where reasoning closes (before text starts or at stream end) so every STEP_STARTED has exactly one matching STEP_FINISHED. Remove the `data: [DONE]\n\n` sentinel from toServerSentEventsStream. The AG-UI protocol already uses RUN_FINISHED as the terminal event, so the [DONE] marker is redundant and forces every client to special-case non-JSON data in the SSE stream. Client-side parsers still tolerate [DONE] for backward compatibility with external servers. * fix(ai-client): warn when receiving deprecated [DONE] sentinel Old servers still emit `data: [DONE]\n\n` after the stream. The client already skips it, but now logs a deprecation warning so users know to upgrade their @tanstack/ai server package. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 243ae32 commit 91ec205

51 files changed

Lines changed: 3061 additions & 1125 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/typescript/ai-anthropic/src/adapters/summarize.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import type {
1212
} from '@tanstack/ai'
1313
import type { AnthropicClientConfig } from '../utils'
1414

15+
/** Cast an event object to StreamChunk. */
16+
const asChunk = (chunk: Record<string, unknown>) =>
17+
chunk as unknown as StreamChunk
18+
1519
/**
1620
* Configuration for Anthropic summarize adapter
1721
*/
@@ -103,18 +107,18 @@ export class AnthropicSummarizeAdapter<
103107
if (event.delta.type === 'text_delta') {
104108
const delta = event.delta.text
105109
accumulatedContent += delta
106-
yield {
110+
yield asChunk({
107111
type: 'TEXT_MESSAGE_CONTENT',
108112
messageId: id,
109113
model,
110114
timestamp: Date.now(),
111115
delta,
112116
content: accumulatedContent,
113-
}
117+
})
114118
}
115119
} else if (event.type === 'message_delta') {
116120
outputTokens = event.usage.output_tokens
117-
yield {
121+
yield asChunk({
118122
type: 'RUN_FINISHED',
119123
runId: id,
120124
model,
@@ -129,7 +133,7 @@ export class AnthropicSummarizeAdapter<
129133
completionTokens: outputTokens,
130134
totalTokens: inputTokens + outputTokens,
131135
},
132-
}
136+
})
133137
}
134138
}
135139
}

0 commit comments

Comments
 (0)