Skip to content

Commit dfbf01f

Browse files
committed
fix tool by name ref
1 parent db57138 commit dfbf01f

10 files changed

Lines changed: 509 additions & 80 deletions

File tree

e2e/scenarios/genkit-instrumentation/__snapshots__/genkit-v1-33-0-auto.span-events.json

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,25 @@
183183
],
184184
"type": "function"
185185
},
186+
{
187+
"has_input": true,
188+
"has_output": true,
189+
"metadata": {
190+
"genkit.action_name": "cityMarkerTool",
191+
"genkit.action_type": "tool",
192+
"provider": "genkit"
193+
},
194+
"metric_keys": [
195+
"duration"
196+
],
197+
"name": "genkit.tool: cityMarkerTool",
198+
"root_span_id": "<span:1>",
199+
"span_id": "<span:12>",
200+
"span_parents": [
201+
"<span:11>"
202+
],
203+
"type": "tool"
204+
},
186205
{
187206
"has_input": false,
188207
"has_output": false,
@@ -192,7 +211,7 @@
192211
"metric_keys": [],
193212
"name": "genkit-tool-operation",
194213
"root_span_id": "<span:1>",
195-
"span_id": "<span:12>",
214+
"span_id": "<span:13>",
196215
"span_parents": [
197216
"<span:3>"
198217
],
@@ -211,9 +230,9 @@
211230
],
212231
"name": "genkit.tool: summarizeCity",
213232
"root_span_id": "<span:1>",
214-
"span_id": "<span:13>",
233+
"span_id": "<span:14>",
215234
"span_parents": [
216-
"<span:12>"
235+
"<span:13>"
217236
],
218237
"type": "tool"
219238
}

e2e/scenarios/genkit-instrumentation/__snapshots__/genkit-v1-33-0-wrapped.span-events.json

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,25 @@
183183
],
184184
"type": "function"
185185
},
186+
{
187+
"has_input": true,
188+
"has_output": true,
189+
"metadata": {
190+
"genkit.action_name": "cityMarkerTool",
191+
"genkit.action_type": "tool",
192+
"provider": "genkit"
193+
},
194+
"metric_keys": [
195+
"duration"
196+
],
197+
"name": "genkit.tool: cityMarkerTool",
198+
"root_span_id": "<span:1>",
199+
"span_id": "<span:12>",
200+
"span_parents": [
201+
"<span:11>"
202+
],
203+
"type": "tool"
204+
},
186205
{
187206
"has_input": false,
188207
"has_output": false,
@@ -192,7 +211,7 @@
192211
"metric_keys": [],
193212
"name": "genkit-tool-operation",
194213
"root_span_id": "<span:1>",
195-
"span_id": "<span:12>",
214+
"span_id": "<span:13>",
196215
"span_parents": [
197216
"<span:3>"
198217
],
@@ -211,9 +230,9 @@
211230
],
212231
"name": "genkit.tool: summarizeCity",
213232
"root_span_id": "<span:1>",
214-
"span_id": "<span:13>",
233+
"span_id": "<span:14>",
215234
"span_parents": [
216-
"<span:12>"
235+
"<span:13>"
217236
],
218237
"type": "tool"
219238
}

e2e/scenarios/genkit-instrumentation/assertions.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ function buildSpanSummary(
6464
events,
6565
"genkit-model-tool-operation",
6666
);
67+
const modelToolGenerateSpan = findGenkitSpan(
68+
events,
69+
modelToolOperation?.span.id,
70+
"genkit.generate",
71+
);
72+
const modelTriggeredToolSpan = supportsActionSpans
73+
? findGenkitSpan(
74+
events,
75+
modelToolGenerateSpan?.span.id,
76+
"genkit.tool: cityMarkerTool",
77+
)
78+
: undefined;
6779

6880
const summary = [
6981
findLatestSpan(events, ROOT_NAME),
@@ -76,7 +88,8 @@ function buildSpanSummary(
7688
embedOperation,
7789
findGenkitSpan(events, embedOperation?.span.id, "genkit.embed"),
7890
modelToolOperation,
79-
findGenkitSpan(events, modelToolOperation?.span.id, "genkit.generate"),
91+
modelToolGenerateSpan,
92+
...(supportsActionSpans ? [modelTriggeredToolSpan] : []),
8093
];
8194

8295
if (supportsActionSpans) {
@@ -225,11 +238,24 @@ export function defineGenkitInstrumentationAssertions(options: {
225238
modelToolOperation?.span.id,
226239
"genkit.generate",
227240
);
241+
const toolSpan = findGenkitSpan(
242+
events,
243+
generateSpan?.span.id,
244+
"genkit.tool: cityMarkerTool",
245+
);
228246

229247
expect(generateSpan?.row.metadata).toMatchObject({
230248
provider: "genkit",
231249
});
232250
expect(generateSpan?.output).toBeDefined();
251+
expect(toolSpan?.row.metadata).toMatchObject({
252+
"genkit.action_name": "cityMarkerTool",
253+
"genkit.action_type": "tool",
254+
provider: "genkit",
255+
});
256+
expect(toolSpan?.output).toMatchObject({
257+
marker: MODEL_TOOL_MARKER,
258+
});
233259
expect(modelToolOperation?.output).toMatchObject({
234260
marker: MODEL_TOOL_MARKER,
235261
toolCalled: true,

e2e/scenarios/genkit-instrumentation/scenario.impl.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export async function runGenkitInstrumentationScenario(options = {}) {
130130
);
131131

132132
let modelToolCallCount = 0;
133-
const modelTool = ai.defineTool(
133+
ai.defineTool(
134134
{
135135
name: "cityMarkerTool",
136136
description:
@@ -184,7 +184,10 @@ export async function runGenkitInstrumentationScenario(options = {}) {
184184
maxOutputTokens: 32,
185185
},
186186
});
187-
await collectAsync(stream);
187+
const chunks = await collectAsync(stream);
188+
if (chunks.length === 0) {
189+
throw new Error("Expected Genkit stream to yield chunks");
190+
}
188191
await response;
189192
}, GOOGLE_GENAI_RETRY_OPTIONS);
190193
});
@@ -217,7 +220,7 @@ export async function runGenkitInstrumentationScenario(options = {}) {
217220
model,
218221
prompt:
219222
"Use the cityMarkerTool tool with city Vienna before answering.",
220-
tools: [modelTool],
223+
tools: ["cityMarkerTool"],
221224
maxTurns: 3,
222225
config: {
223226
temperature: 0,

js/src/instrumentation/plugins/ai-sdk-plugin.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
traceSyncStreamChannel,
66
unsubscribeAll,
77
} from "../core/channel-tracing";
8+
import { isAsyncIterable } from "../core/stream-patcher";
89
import { SpanTypeAttribute, isPromiseLike } from "../../../util/index";
910
import { getCurrentUnixTimestamp } from "../../util";
1011
import { Attachment, type Span, withCurrent } from "../../logger";
@@ -1530,24 +1531,14 @@ function isReadableStreamLike(value: unknown): value is {
15301531
);
15311532
}
15321533

1533-
function isAsyncIterableLike(value: unknown): value is AsyncIterable<unknown> {
1534-
return (
1535-
value != null &&
1536-
typeof value === "object" &&
1537-
typeof (value as { [Symbol.asyncIterator]?: unknown })[
1538-
Symbol.asyncIterator
1539-
] === "function"
1540-
);
1541-
}
1542-
15431534
function findAsyncIterableField(
15441535
result: Record<string, unknown>,
15451536
candidateFields: string[],
15461537
): { field: string; stream: AsyncIterable<unknown> } | null {
15471538
for (const field of candidateFields) {
15481539
try {
15491540
const stream = result[field];
1550-
if (isAsyncIterableLike(stream)) {
1541+
if (isAsyncIterable(stream)) {
15511542
return { field, stream };
15521543
}
15531544
} catch {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { afterEach, describe, expect, it } from "vitest";
2+
import { GenkitPlugin } from "./genkit-plugin";
3+
import { genkitChannels } from "./genkit-channels";
4+
5+
function singleQueueStream<T>(
6+
chunks: T[],
7+
): AsyncIterable<T> & AsyncIterator<T> {
8+
let index = 0;
9+
return {
10+
async next() {
11+
await Promise.resolve();
12+
if (index >= chunks.length) {
13+
return { done: true, value: undefined };
14+
}
15+
return { done: false, value: chunks[index++] };
16+
},
17+
[Symbol.asyncIterator]() {
18+
return this;
19+
},
20+
};
21+
}
22+
23+
async function drainMicrotasks(): Promise<void> {
24+
for (let index = 0; index < 5; index++) {
25+
await Promise.resolve();
26+
}
27+
}
28+
29+
async function collectAsync<T>(stream: AsyncIterable<T>): Promise<T[]> {
30+
const chunks: T[] = [];
31+
for await (const chunk of stream) {
32+
chunks.push(chunk);
33+
}
34+
return chunks;
35+
}
36+
37+
describe("GenkitPlugin stream patching", () => {
38+
const plugin = new GenkitPlugin();
39+
40+
afterEach(() => {
41+
plugin.disable();
42+
});
43+
44+
it("does not consume generateStream chunks before user code reads them", async () => {
45+
plugin.enable();
46+
const stream = singleQueueStream([{ text: "hello" }, { text: " world" }]);
47+
48+
const result = genkitChannels.generateStream.traceSync(
49+
() => ({
50+
response: Promise.resolve({
51+
text: "hello world",
52+
usage: {
53+
inputTokens: 1,
54+
outputTokens: 2,
55+
totalTokens: 3,
56+
},
57+
}),
58+
stream,
59+
}),
60+
{ arguments: [{ prompt: "Say hello world." }] } as Parameters<
61+
typeof genkitChannels.generateStream.traceSync
62+
>[1],
63+
);
64+
65+
await drainMicrotasks();
66+
67+
await expect(collectAsync(result.stream)).resolves.toEqual([
68+
{ text: "hello" },
69+
{ text: " world" },
70+
]);
71+
});
72+
73+
it("does not consume action.stream chunks before user code reads them", async () => {
74+
plugin.enable();
75+
const stream = singleQueueStream(["first", "second"]);
76+
const action = Object.assign(() => Promise.resolve(), {
77+
__action: {
78+
actionType: "tool",
79+
name: "streamTool",
80+
},
81+
});
82+
83+
const result = genkitChannels.actionStream.traceSync(
84+
() => ({
85+
output: Promise.resolve({ done: true }),
86+
stream,
87+
}),
88+
{
89+
arguments: [{ input: true }],
90+
self: action,
91+
} as Parameters<typeof genkitChannels.actionStream.traceSync>[1],
92+
);
93+
94+
await drainMicrotasks();
95+
96+
await expect(collectAsync(result.stream)).resolves.toEqual([
97+
"first",
98+
"second",
99+
]);
100+
});
101+
});

0 commit comments

Comments
 (0)