From 7c4f378313daeb637ea469beaa140bd8a22916cf Mon Sep 17 00:00:00 2001 From: Pavel Jbanov Date: Wed, 19 Nov 2025 11:12:05 -0500 Subject: [PATCH 1/2] fix(js/ai): fixed dynamic tools becoming non-dynamic once registered test case in question ```ts it('should remain dynamic after registration', () => { const dynamic = tool({ name: 'dynamic', description: 'test' }); assert.strictEqual(isDynamicTool(dynamic), true); registry.registerAction('tool', dynamic); assert.strictEqual(isDynamicTool(dynamic), true); }); ``` --- js/ai/src/tool.ts | 2 +- js/ai/tests/tool_test.ts | 55 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/js/ai/src/tool.ts b/js/ai/src/tool.ts index 19c9665676..0f8327bf96 100644 --- a/js/ai/src/tool.ts +++ b/js/ai/src/tool.ts @@ -384,7 +384,7 @@ export function isToolResponse(part: Part): part is ToolResponsePart { } export function isDynamicTool(t: unknown): t is ToolAction { - return isAction(t) && !t.__registry; + return isAction(t) && t.__action?.metadata?.dynamic === true; } export function interrupt( diff --git a/js/ai/tests/tool_test.ts b/js/ai/tests/tool_test.ts index 74a0194e91..e4fc6c6d12 100644 --- a/js/ai/tests/tool_test.ts +++ b/js/ai/tests/tool_test.ts @@ -14,12 +14,17 @@ * limitations under the License. */ -import { z } from '@genkit-ai/core'; +import { action, z } from '@genkit-ai/core'; import { initNodeFeatures } from '@genkit-ai/core/node'; import { Registry } from '@genkit-ai/core/registry'; import * as assert from 'assert'; import { afterEach, describe, it } from 'node:test'; -import { defineInterrupt, defineTool } from '../src/tool.js'; +import { + defineInterrupt, + defineTool, + isDynamicTool, + tool, +} from '../src/tool.js'; initNodeFeatures(); @@ -109,6 +114,52 @@ describe('defineInterrupt', () => { }); }); +describe('isDynamicTool', () => { + let registry = new Registry(); + registry.apiStability = 'beta'; + afterEach(() => { + registry = new Registry(); + registry.apiStability = 'beta'; + }); + + it('should return true for a dynamic tool', () => { + const dynamic = tool({ name: 'dynamic', description: 'test' }); + assert.strictEqual(isDynamicTool(dynamic), true); + }); + + it('should remain dynamic after registration', () => { + const dynamic = tool({ name: 'dynamic', description: 'test' }); + assert.strictEqual(isDynamicTool(dynamic), true); + + registry.registerAction('tool', dynamic); + + assert.strictEqual(isDynamicTool(dynamic), true); + }); + + it('should return false for a registered tool', () => { + const regular = defineTool( + registry, + { name: 'regular', description: 'test' }, + async () => {} + ); + assert.strictEqual(isDynamicTool(regular), false); + }); + + it('should return false for a non-tool action', () => { + const regularAction = action( + { actionType: 'util', name: 'regularAction', description: 'test' }, + async () => {} + ); + assert.strictEqual(isDynamicTool(regularAction), false); + }); + + it('should return false for a non-action', () => { + assert.strictEqual(isDynamicTool({}), false); + assert.strictEqual(isDynamicTool('tool'), false); + assert.strictEqual(isDynamicTool(123), false); + }); +}); + describe('defineTool', () => { let registry = new Registry(); registry.apiStability = 'beta'; From 04f02fb758a54c527ada6a65c353e90d708f4080 Mon Sep 17 00:00:00 2001 From: Pavel Jbanov Date: Wed, 19 Nov 2025 11:23:01 -0500 Subject: [PATCH 2/2] same for resources --- js/ai/src/resource.ts | 3 ++- js/ai/tests/resource/resource_test.ts | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/js/ai/src/resource.ts b/js/ai/src/resource.ts index 1ffb8192d1..a7964b4a87 100644 --- a/js/ai/src/resource.ts +++ b/js/ai/src/resource.ts @@ -138,6 +138,7 @@ export function defineResource( ): ResourceAction { const action = dynamicResource(opts, fn); action.matches = createMatcher(opts.uri, opts.template); + action.__action.metadata.dynamic = false; registry.registerAction('resource', action); return action; } @@ -193,7 +194,7 @@ export async function findMatchingResource( /** Checks whether provided object is a dynamic resource. */ export function isDynamicResourceAction(t: unknown): t is ResourceAction { - return isAction(t) && !t.__registry; + return isAction(t) && t.__action?.metadata?.dynamic === true; } /** diff --git a/js/ai/tests/resource/resource_test.ts b/js/ai/tests/resource/resource_test.ts index fff8fa88fd..558c6a2ea4 100644 --- a/js/ai/tests/resource/resource_test.ts +++ b/js/ai/tests/resource/resource_test.ts @@ -23,6 +23,7 @@ import { dynamicResource, findMatchingResource, isDynamicResourceAction, + resource, } from '../../src/resource.js'; import { defineEchoModel } from '../helpers.js'; @@ -58,7 +59,7 @@ describe('resource', () => { uri: 'foo://bar', }, type: 'resource', - dynamic: true, + dynamic: false, }); assert.strictEqual(testResource.matches({ uri: 'foo://bar' }), true); @@ -229,7 +230,7 @@ describe('resource', () => { uri: undefined, }, type: 'resource', - dynamic: true, + dynamic: false, }); const gotBaz = await findMatchingResource(registry, resList, { @@ -286,4 +287,15 @@ describe('isDynamicResourceAction', () => { true ); }); + + it('should remain dynamic after registration', () => { + const dynamic = resource({ uri: 'bar://baz' }, () => ({ + content: [{ text: `bar` }], + })); + assert.strictEqual(isDynamicResourceAction(dynamic), true); + + registry.registerAction('resource', dynamic); + + assert.strictEqual(isDynamicResourceAction(dynamic), true); + }); });