feat: implement experimental dynamic tools documentation-only pattern

This commit is contained in:
Michael Bleigh
2026-04-09 17:39:16 -07:00
parent f387e456be
commit d7ecbb072f
12 changed files with 430 additions and 31 deletions
+1
View File
@@ -990,6 +990,7 @@ export async function loadCliConfig(
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
experimentalMemoryManager: settings.experimental?.memoryManager,
experimentalDynamicTools: settings.experimental?.dynamicTools,
contextManagement,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
+10
View File
@@ -2207,6 +2207,16 @@ const SETTINGS_SCHEMA = {
'Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.',
showInDialog: true,
},
dynamicTools: {
type: 'boolean',
label: 'Dynamic Tools Documentation',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable documentation-injected dynamic tools experiment. This hides native tools and exposes a single execute() function.',
showInDialog: true,
},
},
},
extensions: {
+74 -5
View File
@@ -83,6 +83,12 @@ import {
COMPLETE_TASK_TOOL_NAME,
ACTIVATE_SKILL_TOOL_NAME,
} from '../tools/definitions/base-declarations.js';
import {
getDynamicToolsDocumentation,
EXECUTE_FUNCTION_DECLARATION,
unwrapExecuteArgs,
wrapAsExecute,
} from '../utils/dynamicToolsUtils.js';
/** A callback function to report on agent activity. */
export type ActivityCallback = (activity: SubagentActivityEvent) => void;
@@ -1074,7 +1080,58 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
for (const [index, functionCall] of functionCalls.entries()) {
const callId = functionCall.id ?? `${promptId}-${index}`;
const { args, error: parseError } = this.parseToolArguments(functionCall);
let { args, error: parseError } = this.parseToolArguments(functionCall);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
let effectiveToolName = functionCall.name as string;
let effectiveArgs = args;
let originalRequestName: string | undefined;
let originalRequestArgs: Record<string, unknown> | undefined;
// Handle experimental dynamic tools
if (
!parseError &&
this.context.config.getExperimentalDynamicTools()
) {
if (functionCall.name === 'execute') {
const unwrapped = unwrapExecuteArgs(functionCall.args);
if (unwrapped) {
debugLogger.log(
`[LocalAgentExecutor] Unwrapping execute call for tool: ${unwrapped.name}`,
);
effectiveToolName = unwrapped.name;
effectiveArgs = unwrapped.args;
args = unwrapped.args;
originalRequestName = 'execute';
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
originalRequestArgs = functionCall.args as Record<string, unknown>;
} else {
parseError =
'Invalid execute arguments. Expected {name: string, args: object}';
}
} else if (functionCall.name) {
// Model called a tool directly despite documentation.
// Recovery: Wrap it as an execute call internally.
debugLogger.log(
`[LocalAgentExecutor] Model called tool '${functionCall.name}' directly. Wrapping as 'execute' for experiment compatibility.`,
);
const wrapped = wrapAsExecute(functionCall.name, args);
const unwrapped = unwrapExecuteArgs(wrapped.args); // Still unwrap to get resilient parameter mapping
if (unwrapped) {
// IMPORTANT: Rewrite the original functionCall so history recording sees it as "execute"
functionCall.name = wrapped.name;
functionCall.args = wrapped.args;
effectiveToolName = unwrapped.name;
effectiveArgs = unwrapped.args;
args = unwrapped.args;
originalRequestName = 'execute';
originalRequestArgs = wrapped.args as Record<string, unknown>;
}
}
}
if (parseError) {
debugLogger.warn(`[LocalAgentExecutor] ${parseError}`);
@@ -1097,8 +1154,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
continue;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const toolName = functionCall.name as string;
const toolName = effectiveToolName;
let displayName = toolName;
let description: string | undefined = undefined;
@@ -1129,7 +1185,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
syncResults.set(callId, {
functionResponse: {
name: toolName,
name: functionCall.name,
id: callId,
response: { error },
},
@@ -1149,7 +1205,9 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
toolRequests.push({
callId,
name: toolName,
args,
args: effectiveArgs,
originalRequestName,
originalRequestArgs,
isClientInitiated: false, // These are coming from the subagent (the "model")
prompt_id: promptId,
});
@@ -1285,6 +1343,9 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
* Prepares the list of tool function declarations to be sent to the model.
*/
private prepareToolsList(): FunctionDeclaration[] {
if (this.context.config.getExperimentalDynamicTools()) {
return [EXECUTE_FUNCTION_DECLARATION];
}
const toolsList: FunctionDeclaration[] = [];
const { toolConfig } = this.definition;
@@ -1341,6 +1402,14 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
const dirContext = await getDirectoryContextString(this.context.config);
finalPrompt += `\n\n# Environment Context\n${dirContext}`;
if (this.context.config.getExperimentalDynamicTools()) {
const toolDeclarations = this.toolRegistry.getFunctionDeclarations(
this.definition.modelConfig.model,
);
const toolDocumentation = getDynamicToolsDocumentation(toolDeclarations);
finalPrompt += `\n\n${toolDocumentation}`;
}
// Append standard rules for non-interactive execution.
finalPrompt += `
Important Rules:
+12
View File
@@ -699,6 +699,7 @@ export interface ConfigParameters {
experimentalJitContext?: boolean;
autoDistillation?: boolean;
experimentalMemoryManager?: boolean;
experimentalDynamicTools?: boolean;
experimentalAgentHistoryTruncation?: boolean;
experimentalAgentHistoryTruncationThreshold?: number;
experimentalAgentHistoryRetainedMessages?: number;
@@ -941,6 +942,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly adminSkillsEnabled: boolean;
private readonly experimentalJitContext: boolean;
private readonly experimentalMemoryManager: boolean;
private readonly experimentalDynamicTools: boolean;
private readonly memoryBoundaryMarkers: readonly string[];
private readonly topicUpdateNarration: boolean;
private readonly disableLLMCorrection: boolean;
@@ -1152,6 +1154,12 @@ export class Config implements McpContext, AgentLoopContext {
this.experimentalJitContext = params.experimentalJitContext ?? false;
this.experimentalMemoryManager = params.experimentalMemoryManager ?? false;
this.experimentalDynamicTools =
params.experimentalDynamicTools === true ||
process.env['GEMINI_CLI_EXP_DYNAMIC_TOOLS'] === 'true';
if (this.experimentalDynamicTools) {
debugLogger.log('[Config] Experimental Dynamic Tools enabled.');
}
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
this.contextManagement = {
enabled: params.contextManagement?.enabled ?? false,
@@ -2426,6 +2434,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.experimentalMemoryManager;
}
getExperimentalDynamicTools(): boolean {
return this.experimentalDynamicTools;
}
getContextManagementConfig(): ContextManagementConfig {
return this.contextManagement;
}
+14 -10
View File
@@ -27,6 +27,7 @@ import {
import type { Config } from '../config/config.js';
import { type AgentLoopContext } from '../config/agent-loop-context.js';
import { getCoreSystemPrompt } from './prompts.js';
import { EXECUTE_FUNCTION_DECLARATION } from '../utils/dynamicToolsUtils.js';
import { checkNextSpeaker } from '../utils/nextSpeakerChecker.js';
import { reportError } from '../utils/errorReporting.js';
import { GeminiChat } from './geminiChat.js';
@@ -301,12 +302,20 @@ export class GeminiClient {
}
this.lastUsedModelId = modelId;
const toolRegistry = this.context.toolRegistry;
const toolDeclarations = toolRegistry.getFunctionDeclarations(modelId);
const tools: Tool[] = [{ functionDeclarations: toolDeclarations }];
const tools = this.getChatTools(modelId);
this.getChat().setTools(tools);
}
private getChatTools(modelId?: string): Tool[] {
const toolRegistry = this.context.toolRegistry;
if (this.config.getExperimentalDynamicTools()) {
debugLogger.log('[GeminiClient] Experimental Dynamic Tools enabled.');
return [{ functionDeclarations: [EXECUTE_FUNCTION_DECLARATION] }];
}
const toolDeclarations = toolRegistry.getFunctionDeclarations(modelId);
return [{ functionDeclarations: toolDeclarations }];
}
async resetChat(): Promise<void> {
this.chat = await this.startChat();
this.updateTelemetryTokenCount();
@@ -369,9 +378,7 @@ export class GeminiClient {
this.hasFailedCompressionAttempt = false;
this.lastUsedModelId = undefined;
const toolRegistry = this.context.toolRegistry;
const toolDeclarations = toolRegistry.getFunctionDeclarations();
const tools: Tool[] = [{ functionDeclarations: toolDeclarations }];
const tools = this.getChatTools();
const history = await getInitialChatHistory(this.config, extraHistory);
@@ -386,10 +393,7 @@ export class GeminiClient {
resumedSessionData,
async (modelId: string) => {
this.lastUsedModelId = modelId;
const toolRegistry = this.context.toolRegistry;
const toolDeclarations =
toolRegistry.getFunctionDeclarations(modelId);
return [{ functionDeclarations: toolDeclarations }];
return this.getChatTools(modelId);
},
);
} catch (error) {
+27 -4
View File
@@ -49,12 +49,14 @@ import { isFunctionResponse } from '../utils/messageInspectors.js';
import { partListUnionToString } from './geminiRequest.js';
import type { ModelConfigKey } from '../services/modelConfigService.js';
import { estimateTokenCountSync } from '../utils/tokenCalculation.js';
import { wrapAsExecute } from '../utils/dynamicToolsUtils.js';
import {
applyModelSelection,
createAvailabilityContextProvider,
} from '../availability/policyHelpers.js';
import { coreEvents } from '../utils/events.js';
import type { AgentLoopContext } from '../config/agent-loop-context.js';
import { debugLogger } from '../utils/debugLogger.js';
export enum StreamEventType {
/** A regular content chunk from the API. */
@@ -892,11 +894,30 @@ export class GeminiChat {
if (isValidResponse(chunk)) {
const content = chunk.candidates?.[0]?.content;
if (content?.parts) {
if (content.parts.some((part) => part.thought)) {
// Record thoughts
hasThoughts = true;
this.recordThoughtFromContent(content);
for (const part of content.parts) {
if (part.thought) {
// Record thoughts
hasThoughts = true;
this.recordThoughtFromContent(content);
}
if (
part.functionCall &&
part.functionCall.name &&
part.functionCall.name !== 'execute' &&
this.context.config.getExperimentalDynamicTools()
) {
debugLogger.log(
`[GeminiChat] Rewriting hallucinated tool call '${part.functionCall.name}' to 'execute' wrapper.`,
);
const wrapped = wrapAsExecute(
part.functionCall.name,
part.functionCall.args,
);
part.functionCall.name = wrapped.name;
part.functionCall.args = wrapped.args;
}
}
if (content.parts.some((part) => part.functionCall)) {
hasToolCall = true;
}
@@ -1048,6 +1069,8 @@ export class GeminiChat {
resultDisplay,
description:
'invocation' in call ? call.invocation?.getDescription() : undefined,
originalRequestName: call.request.originalRequestName,
originalRequestArgs: call.request.originalRequestArgs,
};
});
+78 -10
View File
@@ -29,6 +29,11 @@ import { parseThought, type ThoughtSummary } from '../utils/thoughtUtils.js';
import type { ModelConfigKey } from '../services/modelConfigService.js';
import { getCitations } from '../utils/generateContentResponseUtilities.js';
import { LlmRole } from '../telemetry/types.js';
import { debugLogger } from '../utils/debugLogger.js';
import {
unwrapExecuteArgs,
wrapAsExecute,
} from '../utils/dynamicToolsUtils.js';
import {
type ToolCallRequestInfo,
@@ -237,6 +242,7 @@ export type ServerGeminiStreamEvent =
// A turn manages the agentic loop turn within the server context.
export class Turn {
private callCounter = 0;
private handledCallIds = new Set<string>();
readonly pendingToolCalls: ToolCallRequestInfo[] = [];
private debugResponses: GenerateContentResponse[] = [];
@@ -409,7 +415,74 @@ export class Turn {
): ServerGeminiStreamEvent | null {
const name = fnCall.name || 'undefined_tool_name';
const args = fnCall.args || {};
const callId = fnCall.id ?? `${name}_${Date.now()}_${this.callCounter++}`;
const callId = fnCall.id || `${name}_${Date.now()}_${this.callCounter++}`;
if (this.handledCallIds.has(callId)) {
return null;
}
this.handledCallIds.add(callId);
// Handle experimental dynamic tools
if (
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
(this.chat as any).context.config.getExperimentalDynamicTools()
) {
if (name === 'execute') {
const unwrapped = unwrapExecuteArgs(fnCall.args);
if (unwrapped) {
debugLogger.log(
`[Turn] Unwrapping execute call for tool: ${unwrapped.name}`,
);
const toolCallRequest: ToolCallRequestInfo = {
callId,
name: unwrapped.name,
args: unwrapped.args,
originalRequestName: 'execute',
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-type-assertion
originalRequestArgs: fnCall.args as any,
isClientInitiated: false,
prompt_id: this.prompt_id,
traceId,
};
this.pendingToolCalls.push(toolCallRequest);
return {
type: GeminiEventType.ToolCallRequest,
value: toolCallRequest,
};
}
} else if (name !== 'undefined_tool_name') {
// Model called a tool directly despite documentation.
// It should have been wrapped by GeminiChat.makeApiCallAndProcessStream,
// but as a failsafe we wrap it here too.
debugLogger.log(
`[Turn] Model called tool '${name}' directly. Recovery wrapping as 'execute'.`,
);
const unwrapped = unwrapExecuteArgs(wrapAsExecute(name, args).args);
if (unwrapped) {
const toolCallRequest: ToolCallRequestInfo = {
callId,
name: unwrapped.name,
args: unwrapped.args,
originalRequestName: 'execute',
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-type-assertion
originalRequestArgs: wrapAsExecute(name, args).args as any,
isClientInitiated: false,
prompt_id: this.prompt_id,
traceId,
};
this.pendingToolCalls.push(toolCallRequest);
return {
type: GeminiEventType.ToolCallRequest,
value: toolCallRequest,
};
}
}
}
const toolCallRequest: ToolCallRequestInfo = {
callId,
@@ -426,15 +499,6 @@ export class Turn {
return { type: GeminiEventType.ToolCallRequest, value: toolCallRequest };
}
getDebugResponses(): GenerateContentResponse[] {
return this.debugResponses;
}
/**
* Get the concatenated response text from all responses in this turn.
* This extracts and joins all text content from the model's responses.
* The result is cached since this is called multiple times per turn.
*/
getResponseText(): string {
if (this.cachedResponseText === undefined) {
this.cachedResponseText = this.debugResponses
@@ -444,4 +508,8 @@ export class Turn {
}
return this.cachedResponseText;
}
getDebugResponses(): GenerateContentResponse[] {
return this.debugResponses;
}
}
@@ -32,6 +32,9 @@ import { resolveModel, supportsModernFeatures } from '../config/models.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { getAllGeminiMdFilenames } from '../tools/memoryTool.js';
import type { AgentLoopContext } from '../config/agent-loop-context.js';
import {
getDynamicToolsDocumentation,
} from '../utils/dynamicToolsUtils.js';
/**
* Orchestrates prompt generation by gathering context and building options.
@@ -96,6 +99,13 @@ export class PromptProvider {
.join('\n');
}
let dynamicToolsDocumentation: string | undefined;
if (context.config.getExperimentalDynamicTools()) {
dynamicToolsDocumentation = getDynamicToolsDocumentation(
context.toolRegistry.getFunctionDeclarations(),
);
}
let basePrompt: string;
// --- Template File Override ---
@@ -141,6 +151,7 @@ export class PromptProvider {
contextFilenames,
topicUpdateNarration: context.config.isTopicUpdateNarrationEnabled(),
})),
dynamicToolsDocumentation,
subAgents: this.withSection(
'agentContexts',
() =>
@@ -282,6 +293,7 @@ export class PromptProvider {
);
const isModernModel = supportsModernFeatures(desiredModel);
const activeSnippets = isModernModel ? snippets : legacySnippets;
return activeSnippets.getCompressionPrompt(
context.config.getApprovedPlanPath(),
);
+3
View File
@@ -43,6 +43,7 @@ import { DEFAULT_CONTEXT_FILENAME } from '../tools/memoryTool.js';
export interface SystemPromptOptions {
preamble?: PreambleOptions;
coreMandates?: CoreMandatesOptions;
dynamicToolsDocumentation?: string;
subAgents?: SubAgentOptions[];
agentSkills?: AgentSkillOptions[];
hookContext?: boolean;
@@ -127,6 +128,8 @@ ${renderPreamble(options.preamble)}
${renderCoreMandates(options.coreMandates)}
${options.dynamicToolsDocumentation ? `\n<tools>\n${options.dynamicToolsDocumentation}\n</tools>\n` : ''}
${renderSubAgents(options.subAgents)}
${renderAgentSkills(options.agentSkills)}
@@ -72,6 +72,10 @@ export interface ToolCallRecord {
description?: string;
resultDisplay?: ToolResultDisplay;
renderOutputAsMarkdown?: boolean;
/** Experimental: original name of the tool requested by the model before unwrapping */
originalRequestName?: string;
/** Experimental: original arguments of the tool requested by the model before unwrapping */
originalRequestArgs?: Record<string, unknown>;
}
/**
@@ -108,6 +112,8 @@ export interface ConversationRecord {
directories?: string[];
/** The kind of conversation (main agent or subagent) */
kind?: 'main' | 'subagent';
/** Experimental: Whether dynamic tools documentation-injection was enabled */
experimentalDynamicTools?: boolean;
}
/**
@@ -235,6 +241,7 @@ export class ChatRecordingService {
messages: [],
directories,
kind: this.kind,
experimentalDynamicTools: this.context.config.getExperimentalDynamicTools(),
});
}
@@ -487,7 +494,10 @@ export class ChatRecordingService {
this.cachedLastConvData = fs.readFileSync(this.conversationFile!, 'utf8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.cachedConversation = JSON.parse(this.cachedLastConvData);
if (!this.cachedConversation) {
if (this.cachedConversation) {
this.cachedConversation.experimentalDynamicTools =
this.context.config.getExperimentalDynamicTools();
} else {
// File is corrupt or contains "null". Fallback to an empty conversation.
this.cachedConversation = {
sessionId: this.sessionId,
@@ -496,6 +506,7 @@ export class ChatRecordingService {
lastUpdated: new Date().toISOString(),
messages: [],
kind: this.kind,
experimentalDynamicTools: this.context.config.getExperimentalDynamicTools(),
};
}
return this.cachedConversation;
@@ -514,6 +525,7 @@ export class ChatRecordingService {
lastUpdated: new Date().toISOString(),
messages: [],
kind: this.kind,
experimentalDynamicTools: this.context.config.getExperimentalDynamicTools(),
};
return this.cachedConversation;
}
@@ -0,0 +1,184 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { FunctionDeclaration, Schema } from '@google/genai';
import { Type } from '@google/genai';
import { debugLogger } from './debugLogger.js';
/**
* Generates documentation for tools to be injected into the system prompt.
*/
export function getDynamicToolsDocumentation(
tools: FunctionDeclaration[],
): string {
let doc =
'# Tools\n\nYou have a variety of tools available to you that can be called via the `execute` function. ALWAYS use `execute` for all tool calls. Additional tools may be loaded dynamically as you continue to work.\n\n## Initial Tools\n\n';
for (const tool of tools) {
if (!tool.name) continue;
const argsName = `${tool.name
.split(/_|-/)
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join('')}Args`;
doc += `<tool name="${tool.name}">\n`;
doc += `Description: ${tool.description ?? ''}\n\n`;
doc += `Usage: execute({name: "${tool.name}", args: ...})\n\n`;
if (tool.parameters) {
doc += 'Arguments:\n';
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const properties =
((tool.parameters.properties as unknown) as Record<string, unknown>) ||
{};
for (const [name, propRaw] of Object.entries(properties)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const prop = propRaw as Schema;
const isRequired = tool.parameters.required?.includes(name);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const typeStr = (prop.type as string) || 'any';
doc += `- ${name} (${typeStr}${isRequired ? ', REQUIRED' : ', OPTIONAL'}): ${prop.description ?? ''}\n`;
}
doc += '\n```ts\n';
doc += `interface ${argsName} ${schemaToTypeScript(tool.parameters)}\n`;
doc += '```\n';
}
doc += '</tool>\n\n';
}
return doc;
}
/**
* Converts a JSON schema to a TypeScript interface-like string.
*/
export function schemaToTypeScript(schema: Schema, indent = ''): string {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const type = (schema.type as string) || 'any';
if (
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
schema.type === (Type.OBJECT as unknown as string) ||
schema.type === Type.OBJECT
) {
let ts = '{\n';
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const properties =
((schema.properties as unknown) as Record<string, unknown>) || {};
for (const [name, propRaw] of Object.entries(properties)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const prop = propRaw as Schema;
const isRequired = schema.required?.includes(name);
if (prop.description) {
ts += `${indent} /** ${prop.description.replace(/\n/g, ' ')} */\n`;
}
const optional = isRequired ? '' : '?';
ts += `${indent} ${name}${optional}: ${schemaToTypeScript(
prop,
indent + ' ',
)};${isRequired ? ' // REQUIRED' : ''}\n`;
}
ts += `${indent}}`;
return ts;
} else if (
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
schema.type === (Type.ARRAY as unknown as string) ||
schema.type === Type.ARRAY
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const items = schema.items as Schema;
return `${schemaToTypeScript(items, indent)}[]`;
} else {
return type;
}
}
/**
* The single `execute` function declaration used when dynamic tools experiment is active.
*/
export const EXECUTE_FUNCTION_DECLARATION: FunctionDeclaration = {
name: 'execute',
description:
'Executes a tool by its name. Check the <tools> documentation in the system prompt for available tools and their arguments.',
parameters: {
type: Type.OBJECT,
properties: {
name: {
type: Type.STRING,
description: 'The name of the tool to execute.',
},
args: {
type: Type.OBJECT,
description: 'The arguments for the tool.',
},
},
required: ['name', 'args'],
},
};
/**
* Wraps a direct tool call into the `execute` pattern.
* This is used to recover when the model calls a tool directly despite instructions.
*/
export function wrapAsExecute(
name: string,
args: unknown,
): {
name: string;
args: { name: string; args: unknown };
} {
return {
name: 'execute',
args: {
name,
args,
},
};
}
/**
* Unwraps the arguments for an `execute` call, applying resilient parameter mapping
* to help the model succeed during the documentation-only tools experiment.
*/
export function unwrapExecuteArgs(args: unknown): {
name: string;
args: Record<string, unknown>;
} | null {
if (typeof args !== 'object' || args === null) {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-type-assertion
const name = (args as any).name;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-type-assertion
let innerArgs = (args as any).args;
if (typeof name !== 'string' || !innerArgs || typeof innerArgs !== 'object') {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
innerArgs = { ...innerArgs };
// Resilient parameter mapping for common model hallucinations/priors
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const anyArgs: any = innerArgs;
if (name === 'read_file' && anyArgs.path && !anyArgs.file_path) {
debugLogger.log(`[DynamicTools] Mapping path -> file_path for read_file: ${anyArgs.path}`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
anyArgs.file_path = anyArgs.path;
}
if (
(name === 'list_directory' || name === 'grep_search' || name === 'glob') &&
anyArgs.path &&
!anyArgs.dir_path
) {
debugLogger.log(`[DynamicTools] Mapping path -> dir_path for ${name}: ${anyArgs.path}`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
anyArgs.dir_path = anyArgs.path;
}
return {
name,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
args: innerArgs as Record<string, unknown>,
};
}
+2 -1
View File
@@ -659,7 +659,8 @@ export class TestRig {
key !== 'GEMINI_DEBUG' &&
key !== 'GEMINI_CLI_TEST_VAR' &&
key !== 'GEMINI_CLI_INTEGRATION_TEST' &&
!key.startsWith('GEMINI_CLI_ACTIVITY_LOG')
!key.startsWith('GEMINI_CLI_ACTIVITY_LOG') &&
!key.startsWith('GEMINI_CLI_EXP_')
) {
delete cleanEnv[key];
}