Skip to content

Commit fc915f6

Browse files
committed
Add some stuff here
1 parent be71fea commit fc915f6

3 files changed

Lines changed: 119 additions & 3 deletions

File tree

references/ai-chat/src/app/actions.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
aiChatSession,
1010
upgradeTestAgent,
1111
cfTrustTestAgent,
12+
generatorToolTest,
13+
toolModelOutputTest,
1214
} from "@/trigger/chat";
1315
import type { ChatUiMessage } from "@/lib/chat-tools-schemas";
1416
import { prisma } from "@/lib/prisma";
@@ -22,7 +24,9 @@ export type ChatReferenceTaskId =
2224
| "ai-chat-raw"
2325
| "ai-chat-session"
2426
| "upgrade-test"
25-
| "cf-trust-test";
27+
| "cf-trust-test"
28+
| "generator-tool-test"
29+
| "tool-model-output-test";
2630

2731
function isChatReferenceTaskId(id: string): id is ChatReferenceTaskId {
2832
return (
@@ -31,7 +35,9 @@ function isChatReferenceTaskId(id: string): id is ChatReferenceTaskId {
3135
id === "ai-chat-raw" ||
3236
id === "ai-chat-session" ||
3337
id === "upgrade-test" ||
34-
id === "cf-trust-test"
38+
id === "cf-trust-test" ||
39+
id === "generator-tool-test" ||
40+
id === "tool-model-output-test"
3541
);
3642
}
3743

@@ -42,7 +48,9 @@ type TaskIdentifierForChat =
4248
| (typeof aiChatRaw)["id"]
4349
| (typeof aiChatSession)["id"]
4450
| (typeof upgradeTestAgent)["id"]
45-
| (typeof cfTrustTestAgent)["id"];
51+
| (typeof cfTrustTestAgent)["id"]
52+
| (typeof generatorToolTest)["id"]
53+
| (typeof toolModelOutputTest)["id"];
4654

4755
/**
4856
* Server-mediated start: creates the Session row + triggers the first
@@ -75,6 +83,8 @@ const startActionByTaskId: Record<
7583
"ai-chat-session": startChatSessionFor("ai-chat-session"),
7684
"upgrade-test": startChatSessionFor("upgrade-test"),
7785
"cf-trust-test": startChatSessionFor("cf-trust-test"),
86+
"generator-tool-test": startChatSessionFor("generator-tool-test"),
87+
"tool-model-output-test": startChatSessionFor("tool-model-output-test"),
7888
};
7989

8090
export async function startChatSession(input: {

references/ai-chat/src/components/chat-sidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export function ChatSidebar({
120120
<option value="stress-emit">stress-emit (UI stress test)</option>
121121
<option value="cf-trust-test">cf-trust-test (Cloudflare proxy trust)</option>
122122
<option value="tool-model-output-test">tool-model-output-test (toModelOutput cross-turn)</option>
123+
<option value="generator-tool-test">generator-tool-test (async-generator tool output)</option>
123124
</select>
124125
</div>
125126
<label

references/ai-chat/src/trigger/chat.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,3 +1193,108 @@ export const toolModelOutputFnTest = chat.agent({
11931193
});
11941194
},
11951195
});
1196+
1197+
// ============================================================================
1198+
// generator-tool-test: TRI-10306 repro
1199+
//
1200+
// GovSignals reported that an async-generator tool's final yielded object shows
1201+
// up as `{}` for BOTH the telemetry trace AND the tool result the model
1202+
// receives. On ai v6 + @ai-sdk/provider-utils 4.0.x (what this reference
1203+
// resolves) the traced `executeTool` consumes the generator correctly — the
1204+
// LAST yield becomes the output — so the expectation here is a NON-repro. This
1205+
// agent makes that falsifiable.
1206+
//
1207+
// The tool mirrors the customer's shape exactly: yield a progress chunk, then
1208+
// yield the final structured result (no explicit `return`). The final object
1209+
// carries GEN_MARKER so every observation point has a deterministic,
1210+
// model-independent signal:
1211+
//
1212+
// - onStepFinish({ toolResults }): exactly what `executeTool` produced. `{}`
1213+
// or a missing marker => Symptom A (model side) reproduced.
1214+
// - logGeneratorProbe(messages): on turn 2+, whether the prior-turn tool
1215+
// result still carries GEN_MARKER after the SDK re-converts history.
1216+
// - The `ai.toolCall.result` span attribute (inspect via dashboard / MCP):
1217+
// Symptom B (telemetry side). `{}` there while the probes are correct =>
1218+
// it's our OTel attribute flattening, not the model input.
1219+
// ============================================================================
1220+
1221+
const GEN_MARKER = "LIBRARY-MARKER-4731";
1222+
1223+
const searchLibrary = tool({
1224+
description:
1225+
"Search the library. You MUST call this tool to answer any library question. " +
1226+
"It streams a progress update, then returns the structured results.",
1227+
inputSchema: z.object({ query: z.string() }),
1228+
// Mirrors the customer's tool: a preliminary progress yield, then the final
1229+
// structured result as the LAST yield (no explicit `return`).
1230+
execute: async function* ({ query }) {
1231+
yield { text: `Searching library for "${query}"…` };
1232+
yield {
1233+
success: true,
1234+
marker: GEN_MARKER,
1235+
data: { results: [{ id: 1, title: "Durable agents 101" }] },
1236+
metadata: { totalFound: 1 },
1237+
};
1238+
},
1239+
});
1240+
1241+
/**
1242+
* Deterministic, model-independent verdict for the cross-turn path: does each
1243+
* incoming tool-result message still carry GEN_MARKER after the SDK's internal
1244+
* re-conversion of prior-turn history? `messages` is the literal output of the
1245+
* `toModelMessages` wrapper handed to `run()`.
1246+
*/
1247+
function logGeneratorProbe(messages: ModelMessage[]) {
1248+
for (const m of messages) {
1249+
if (m.role !== "tool") continue;
1250+
const serialized = JSON.stringify(m.content);
1251+
logger.info("generator-tool-test: incoming tool result", {
1252+
messageCount: messages.length,
1253+
containsMarker: serialized.includes(GEN_MARKER),
1254+
serialized: serialized.slice(0, 500),
1255+
});
1256+
}
1257+
}
1258+
1259+
export const generatorToolTest = chat.agent({
1260+
id: "generator-tool-test",
1261+
idleTimeoutInSeconds: 60,
1262+
// Declared on the config so the SDK threads them through its internal
1263+
// convertToModelMessages on turn 2+; handed back typed on the run payload.
1264+
tools: { searchLibrary },
1265+
run: async ({ messages, tools, signal }) => {
1266+
logGeneratorProbe(messages);
1267+
return streamText({
1268+
model: openai("gpt-4o-mini"),
1269+
system:
1270+
"You are a library assistant. For ANY user question you MUST first call " +
1271+
"the searchLibrary tool, then answer based on its result. If the user asks " +
1272+
"for the marker, report the `marker` field from the tool result verbatim.",
1273+
messages,
1274+
tools,
1275+
stopWhen: stepCountIs(5),
1276+
abortSignal: signal,
1277+
// Mirror the customer's telemetry config so the AI SDK emits an
1278+
// `ai.toolCall` span with `ai.toolCall.result` (Symptom B — does our
1279+
// OTel attribute flattening collapse the structured output to `{}`?).
1280+
experimental_telemetry: {
1281+
isEnabled: true,
1282+
recordInputs: true,
1283+
recordOutputs: true,
1284+
functionId: "generator-tool-test.tool-loop",
1285+
},
1286+
// Authoritative, model-independent capture of what executeTool produced
1287+
// for the model on this step (Symptom A).
1288+
onStepFinish: ({ toolResults }) => {
1289+
for (const tr of toolResults ?? []) {
1290+
const serialized = JSON.stringify((tr as { output?: unknown }).output);
1291+
logger.info("generator-tool-test: onStepFinish toolResult", {
1292+
toolName: (tr as { toolName?: string }).toolName,
1293+
containsMarker: serialized.includes(GEN_MARKER),
1294+
output: serialized.slice(0, 500),
1295+
});
1296+
}
1297+
},
1298+
});
1299+
},
1300+
});

0 commit comments

Comments
 (0)