Skip to content

Commit 6f77116

Browse files
committed
feat(runtime): pass through REASONING_* chunks in TanStack converter
1 parent d07c1d1 commit 6f77116

2 files changed

Lines changed: 74 additions & 3 deletions

File tree

packages/runtime/src/agent/__tests__/converter-tanstack.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import {
1212
tanstackToolCallArgs,
1313
tanstackToolCallEnd,
1414
tanstackToolCallResult,
15+
tanstackReasoningStart,
16+
tanstackReasoningMessageStart,
17+
tanstackReasoningMessageContent,
18+
tanstackReasoningMessageEnd,
19+
tanstackReasoningEnd,
1520
} from "./agent-test-helpers";
1621

1722
describe("TanStack AI converter (via Agent)", () => {
@@ -387,3 +392,28 @@ describe("TanStack AI converter — state tools", () => {
387392
).toBeDefined();
388393
});
389394
});
395+
396+
describe("TanStack AI converter — reasoning", () => {
397+
it("emits the full REASONING lifecycle for reasoning chunks", async () => {
398+
const agent = createAgent("tanstack", [
399+
tanstackReasoningStart("r1"),
400+
tanstackReasoningMessageStart("r1"),
401+
tanstackReasoningMessageContent("r1", "thinking"),
402+
tanstackReasoningMessageEnd("r1"),
403+
tanstackReasoningEnd("r1"),
404+
]);
405+
const events = await collectEvents(agent.run(createDefaultInput()));
406+
407+
expectLifecycleWrapped(events);
408+
409+
// Strip the lifecycle wrap and inspect the inner sequence.
410+
const inner = events.slice(1, -1).map((e) => e.type);
411+
expect(inner).toEqual([
412+
EventType.REASONING_START,
413+
EventType.REASONING_MESSAGE_START,
414+
EventType.REASONING_MESSAGE_CONTENT,
415+
EventType.REASONING_MESSAGE_END,
416+
EventType.REASONING_END,
417+
]);
418+
});
419+
});

packages/runtime/src/agent/converters/tanstack.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import {
1010
ToolCallResultEvent,
1111
StateSnapshotEvent,
1212
StateDeltaEvent,
13+
ReasoningStartEvent,
14+
ReasoningMessageStartEvent,
15+
ReasoningMessageContentEvent,
16+
ReasoningMessageEndEvent,
17+
ReasoningEndEvent,
1318
} from "@ag-ui/client";
1419
import { randomUUID } from "@copilotkit/shared";
1520

@@ -232,6 +237,8 @@ export async function* convertTanStackStream(
232237
): AsyncGenerator<BaseEvent> {
233238
const messageId = randomUUID();
234239
const toolNamesById = new Map<string, string>();
240+
let isInReasoning = false;
241+
let reasoningMessageId = randomUUID();
235242

236243
for await (const chunk of stream) {
237244
if (abortSignal.aborted) break;
@@ -323,11 +330,45 @@ export async function* convertTanStackStream(
323330
};
324331
yield resultEvent;
325332
toolNamesById.delete(toolCallId);
333+
} else if (type === "REASONING_START") {
334+
isInReasoning = true;
335+
reasoningMessageId = (raw.messageId as string) ?? randomUUID();
336+
const startEvt: ReasoningStartEvent = {
337+
type: EventType.REASONING_START,
338+
messageId: reasoningMessageId,
339+
};
340+
yield startEvt;
341+
} else if (type === "REASONING_MESSAGE_START") {
342+
const evt: ReasoningMessageStartEvent = {
343+
type: EventType.REASONING_MESSAGE_START,
344+
messageId: reasoningMessageId,
345+
role: "reasoning",
346+
};
347+
yield evt;
348+
} else if (type === "REASONING_MESSAGE_CONTENT") {
349+
const evt: ReasoningMessageContentEvent = {
350+
type: EventType.REASONING_MESSAGE_CONTENT,
351+
messageId: reasoningMessageId,
352+
delta: raw.delta as string,
353+
};
354+
yield evt;
355+
} else if (type === "REASONING_MESSAGE_END") {
356+
const evt: ReasoningMessageEndEvent = {
357+
type: EventType.REASONING_MESSAGE_END,
358+
messageId: reasoningMessageId,
359+
};
360+
yield evt;
361+
} else if (type === "REASONING_END") {
362+
isInReasoning = false;
363+
const evt: ReasoningEndEvent = {
364+
type: EventType.REASONING_END,
365+
messageId: reasoningMessageId,
366+
};
367+
yield evt;
326368
}
327369
// Unhandled chunk types are silently ignored.
328-
// Known gap: REASONING events are not yet converted from TanStack streams.
329-
// Reasoning will not surface when using the TanStack backend.
330-
// Use the AI SDK backend if reasoning is required.
370+
// Reasoning auto-close (closing an open reasoning lifecycle when text/tool
371+
// arrives without explicit close events) is handled in the next task.
331372
}
332373
}
333374

0 commit comments

Comments
 (0)