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