mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-09 01:27:41 -07:00
Merge remote-tracking branch 'origin/main' into st/chore/clean-up-memory
# Conflicts: # packages/cli/src/config/config.ts
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.42.0-nightly.20260428.g59b2dea0e",
|
||||
"version": "0.44.0-nightly.20260512.g022e8baef",
|
||||
"description": "Gemini CLI Core",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
AgentInterface,
|
||||
} from '@a2a-js/sdk';
|
||||
import type { SendMessageResult } from './a2a-client-manager.js';
|
||||
import type { SubagentActivityItem } from './types.js';
|
||||
import { type SubagentActivityItem, SubagentState } from './types.js';
|
||||
|
||||
export const AUTH_REQUIRED_MSG = `[Authorization Required] The agent has indicated it requires authorization to proceed. Please follow the agent's instructions.`;
|
||||
|
||||
@@ -143,7 +143,7 @@ export class A2AResultReassembler {
|
||||
id: 'auth-required',
|
||||
type: 'thought',
|
||||
content: AUTH_REQUIRED_MSG,
|
||||
status: 'running',
|
||||
status: SubagentState.RUNNING,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ export class A2AResultReassembler {
|
||||
id: `msg-${index}`,
|
||||
type: 'thought',
|
||||
content: msg.trim(),
|
||||
status: 'completed',
|
||||
status: SubagentState.COMPLETED,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ export class A2AResultReassembler {
|
||||
id: 'pending',
|
||||
type: 'thought',
|
||||
content: 'Working...',
|
||||
status: 'running',
|
||||
status: SubagentState.RUNNING,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
type SubagentActivityItem,
|
||||
AgentTerminateMode,
|
||||
isToolActivityError,
|
||||
SubagentState,
|
||||
} from '../types.js';
|
||||
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
|
||||
import { createBrowserAgentDefinition } from './browserAgentFactory.js';
|
||||
@@ -123,7 +124,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
isSubagentProgress: true,
|
||||
agentName: this.agentName,
|
||||
recentActivity: [],
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
};
|
||||
updateOutput(initialProgress);
|
||||
}
|
||||
@@ -137,7 +138,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
id: randomUUID(),
|
||||
type: 'thought',
|
||||
content: sanitizedMsg,
|
||||
status: 'completed',
|
||||
status: SubagentState.COMPLETED,
|
||||
});
|
||||
if (recentActivity.length > MAX_RECENT_ACTIVITY) {
|
||||
recentActivity = recentActivity.slice(-MAX_RECENT_ACTIVITY);
|
||||
@@ -146,7 +147,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
isSubagentProgress: true,
|
||||
agentName: this.agentName,
|
||||
recentActivity: [...recentActivity],
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
} as SubagentProgress);
|
||||
}
|
||||
: undefined;
|
||||
@@ -175,7 +176,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
if (
|
||||
lastItem &&
|
||||
lastItem.type === 'thought' &&
|
||||
lastItem.status === 'running'
|
||||
lastItem.status === SubagentState.RUNNING
|
||||
) {
|
||||
lastItem.content = sanitizeThoughtContent(text);
|
||||
} else {
|
||||
@@ -183,7 +184,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
id: randomUUID(),
|
||||
type: 'thought',
|
||||
content: sanitizeThoughtContent(text),
|
||||
status: 'running',
|
||||
status: SubagentState.RUNNING,
|
||||
});
|
||||
}
|
||||
updated = true;
|
||||
@@ -210,7 +211,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
displayName,
|
||||
description,
|
||||
args,
|
||||
status: 'running',
|
||||
status: SubagentState.RUNNING,
|
||||
});
|
||||
updated = true;
|
||||
break;
|
||||
@@ -227,9 +228,11 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
recentActivity[i].type === 'tool_call' &&
|
||||
callId != null &&
|
||||
recentActivity[i].id === callId &&
|
||||
recentActivity[i].status === 'running'
|
||||
recentActivity[i].status === SubagentState.RUNNING
|
||||
) {
|
||||
recentActivity[i].status = isError ? 'error' : 'completed';
|
||||
recentActivity[i].status = isError
|
||||
? SubagentState.ERROR
|
||||
: SubagentState.COMPLETED;
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
@@ -242,7 +245,9 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
const callId = activity.data['callId']
|
||||
? String(activity.data['callId'])
|
||||
: undefined;
|
||||
const newStatus = isCancellation ? 'cancelled' : 'error';
|
||||
const newStatus = isCancellation
|
||||
? SubagentState.CANCELLED
|
||||
: SubagentState.ERROR;
|
||||
|
||||
if (callId) {
|
||||
// Mark the specific tool as error/cancelled
|
||||
@@ -250,7 +255,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
if (
|
||||
recentActivity[i].type === 'tool_call' &&
|
||||
recentActivity[i].id === callId &&
|
||||
recentActivity[i].status === 'running'
|
||||
recentActivity[i].status === SubagentState.RUNNING
|
||||
) {
|
||||
recentActivity[i].status = newStatus;
|
||||
updated = true;
|
||||
@@ -260,7 +265,10 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
} else {
|
||||
// No specific tool — mark ALL running tool_call items
|
||||
for (const item of recentActivity) {
|
||||
if (item.type === 'tool_call' && item.status === 'running') {
|
||||
if (
|
||||
item.type === 'tool_call' &&
|
||||
item.status === SubagentState.RUNNING
|
||||
) {
|
||||
item.status = newStatus;
|
||||
updated = true;
|
||||
}
|
||||
@@ -293,7 +301,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
isSubagentProgress: true,
|
||||
agentName: this.agentName,
|
||||
recentActivity: [...recentActivity],
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
};
|
||||
updateOutput(progress);
|
||||
}
|
||||
@@ -330,13 +338,13 @@ ${output.result}`;
|
||||
// GOAL = agent completed its task normally.
|
||||
// ABORTED = user cancelled.
|
||||
// Others (ERROR, MAX_TURNS, ERROR_NO_COMPLETE_TASK_CALL) = error.
|
||||
let progressState: SubagentProgress['state'];
|
||||
let progressState: SubagentState;
|
||||
if (output.terminate_reason === AgentTerminateMode.ABORTED) {
|
||||
progressState = 'cancelled';
|
||||
progressState = SubagentState.CANCELLED;
|
||||
} else if (output.terminate_reason === AgentTerminateMode.GOAL) {
|
||||
progressState = 'completed';
|
||||
progressState = SubagentState.COMPLETED;
|
||||
} else {
|
||||
progressState = 'error';
|
||||
progressState = SubagentState.ERROR;
|
||||
}
|
||||
|
||||
const progress: SubagentProgress = {
|
||||
@@ -366,8 +374,8 @@ ${output.result}`;
|
||||
|
||||
// Mark any running items as error/cancelled
|
||||
for (const item of recentActivity) {
|
||||
if (item.status === 'running') {
|
||||
item.status = isAbort ? 'cancelled' : 'error';
|
||||
if (item.status === SubagentState.RUNNING) {
|
||||
item.status = isAbort ? SubagentState.CANCELLED : SubagentState.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +383,7 @@ ${output.result}`;
|
||||
isSubagentProgress: true,
|
||||
agentName: this.agentName,
|
||||
recentActivity: [...recentActivity],
|
||||
state: isAbort ? 'cancelled' : 'error',
|
||||
state: isAbort ? SubagentState.CANCELLED : SubagentState.ERROR,
|
||||
};
|
||||
|
||||
if (updateOutput) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type SubagentProgress,
|
||||
SubagentActivityErrorType,
|
||||
SUBAGENT_REJECTED_ERROR_PREFIX,
|
||||
SubagentState,
|
||||
} from './types.js';
|
||||
import { LocalSubagentInvocation } from './local-invocation.js';
|
||||
import { LocalAgentExecutor } from './local-executor.js';
|
||||
@@ -215,7 +216,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
]);
|
||||
const display = result.returnDisplay as SubagentProgress;
|
||||
expect(display.isSubagentProgress).toBe(true);
|
||||
expect(display.state).toBe('completed');
|
||||
expect(display.state).toBe(SubagentState.COMPLETED);
|
||||
expect(display.result).toBe('Analysis complete.');
|
||||
expect(display.terminateReason).toBe(AgentTerminateMode.GOAL);
|
||||
});
|
||||
@@ -234,7 +235,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
|
||||
const display = result.returnDisplay as SubagentProgress;
|
||||
expect(display.isSubagentProgress).toBe(true);
|
||||
expect(display.state).toBe('completed');
|
||||
expect(display.state).toBe(SubagentState.COMPLETED);
|
||||
expect(display.result).toBe('Partial progress...');
|
||||
expect(display.terminateReason).toBe(AgentTerminateMode.TIMEOUT);
|
||||
});
|
||||
@@ -340,7 +341,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
expect.objectContaining({
|
||||
type: 'thought',
|
||||
content: 'Error: Failed',
|
||||
status: 'error',
|
||||
status: SubagentState.ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -376,7 +377,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
expect.objectContaining({
|
||||
type: 'tool_call',
|
||||
content: 'ls',
|
||||
status: 'error',
|
||||
status: SubagentState.ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -418,7 +419,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
expect.objectContaining({
|
||||
type: 'tool_call',
|
||||
content: 'ls',
|
||||
status: 'cancelled',
|
||||
status: SubagentState.CANCELLED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -443,7 +444,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
expect(result.error).toBeUndefined();
|
||||
const display = result.returnDisplay as SubagentProgress;
|
||||
expect(display.isSubagentProgress).toBe(true);
|
||||
expect(display.state).toBe('completed');
|
||||
expect(display.state).toBe(SubagentState.COMPLETED);
|
||||
expect(display.result).toBe('Done');
|
||||
});
|
||||
|
||||
@@ -466,7 +467,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
expect.objectContaining({
|
||||
type: 'thought',
|
||||
content: `Error: ${error.message}`,
|
||||
status: 'error',
|
||||
status: SubagentState.ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -488,7 +489,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
expect(display.recentActivity).toContainEqual(
|
||||
expect.objectContaining({
|
||||
content: `Error: ${creationError.message}`,
|
||||
status: 'error',
|
||||
status: SubagentState.ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
SUBAGENT_REJECTED_ERROR_PREFIX,
|
||||
SUBAGENT_CANCELLED_ERROR_MESSAGE,
|
||||
isToolActivityError,
|
||||
SubagentState,
|
||||
} from './types.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { z } from 'zod';
|
||||
@@ -117,7 +118,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
isSubagentProgress: true,
|
||||
agentName: this.definition.name,
|
||||
recentActivity: [],
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
};
|
||||
updateOutput(initialProgress);
|
||||
}
|
||||
@@ -137,7 +138,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
if (
|
||||
lastItem &&
|
||||
lastItem.type === 'thought' &&
|
||||
lastItem.status === 'running'
|
||||
lastItem.status === SubagentState.RUNNING
|
||||
) {
|
||||
lastItem.content = sanitizeThoughtContent(text);
|
||||
} else {
|
||||
@@ -145,7 +146,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
id: randomUUID(),
|
||||
type: 'thought',
|
||||
content: sanitizeThoughtContent(text),
|
||||
status: 'running',
|
||||
status: SubagentState.RUNNING,
|
||||
});
|
||||
}
|
||||
updated = true;
|
||||
@@ -174,7 +175,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
displayName,
|
||||
description,
|
||||
args,
|
||||
status: 'running',
|
||||
status: SubagentState.RUNNING,
|
||||
});
|
||||
updated = true;
|
||||
|
||||
@@ -193,9 +194,11 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
if (
|
||||
recentActivity[i].type === 'tool_call' &&
|
||||
recentActivity[i].content === name &&
|
||||
recentActivity[i].status === 'running'
|
||||
recentActivity[i].status === SubagentState.RUNNING
|
||||
) {
|
||||
recentActivity[i].status = isError ? 'error' : 'completed';
|
||||
recentActivity[i].status = isError
|
||||
? SubagentState.ERROR
|
||||
: SubagentState.COMPLETED;
|
||||
updated = true;
|
||||
|
||||
this.publishActivity(recentActivity[i]);
|
||||
@@ -224,9 +227,9 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
if (
|
||||
recentActivity[i].type === 'tool_call' &&
|
||||
recentActivity[i].content === toolName &&
|
||||
recentActivity[i].status === 'running'
|
||||
recentActivity[i].status === SubagentState.RUNNING
|
||||
) {
|
||||
recentActivity[i].status = 'cancelled';
|
||||
recentActivity[i].status = SubagentState.CANCELLED;
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
@@ -237,9 +240,9 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
if (
|
||||
recentActivity[i].type === 'tool_call' &&
|
||||
recentActivity[i].content === toolName &&
|
||||
recentActivity[i].status === 'running'
|
||||
recentActivity[i].status === SubagentState.RUNNING
|
||||
) {
|
||||
recentActivity[i].status = 'error';
|
||||
recentActivity[i].status = SubagentState.ERROR;
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
@@ -253,7 +256,10 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
isCancellation || isRejection
|
||||
? sanitizedError
|
||||
: `Error: ${sanitizedError}`,
|
||||
status: isCancellation || isRejection ? 'cancelled' : 'error',
|
||||
status:
|
||||
isCancellation || isRejection
|
||||
? SubagentState.CANCELLED
|
||||
: SubagentState.ERROR,
|
||||
});
|
||||
updated = true;
|
||||
break;
|
||||
@@ -267,7 +273,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
isSubagentProgress: true,
|
||||
agentName: this.definition.name,
|
||||
recentActivity: [...recentActivity], // Copy to avoid mutation issues
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
};
|
||||
|
||||
updateOutput(progress);
|
||||
@@ -287,7 +293,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
isSubagentProgress: true,
|
||||
agentName: this.definition.name,
|
||||
recentActivity: [...recentActivity],
|
||||
state: 'cancelled',
|
||||
state: SubagentState.CANCELLED,
|
||||
};
|
||||
|
||||
if (updateOutput) {
|
||||
@@ -303,7 +309,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
isSubagentProgress: true,
|
||||
agentName: this.definition.name,
|
||||
recentActivity: [...recentActivity],
|
||||
state: 'completed',
|
||||
state: SubagentState.COMPLETED,
|
||||
result: output.result,
|
||||
terminateReason: output.terminate_reason,
|
||||
};
|
||||
@@ -334,8 +340,8 @@ ${output.result}`;
|
||||
|
||||
// Mark any running items as error/cancelled
|
||||
for (const item of recentActivity) {
|
||||
if (item.status === 'running') {
|
||||
item.status = isAbort ? 'cancelled' : 'error';
|
||||
if (item.status === SubagentState.RUNNING) {
|
||||
item.status = isAbort ? SubagentState.CANCELLED : SubagentState.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,12 +349,12 @@ ${output.result}`;
|
||||
// But only if it's NOT an abort, or if we want to show "Cancelled" as a thought
|
||||
if (!isAbort) {
|
||||
const lastActivity = recentActivity[recentActivity.length - 1];
|
||||
if (!lastActivity || lastActivity.status !== 'error') {
|
||||
if (!lastActivity || lastActivity.status !== SubagentState.ERROR) {
|
||||
recentActivity.push({
|
||||
id: randomUUID(),
|
||||
type: 'thought',
|
||||
content: `Error: ${errorMessage}`,
|
||||
status: 'error',
|
||||
status: SubagentState.ERROR,
|
||||
});
|
||||
// Maintain size limit
|
||||
// No limit on UI events sent via bus
|
||||
@@ -359,7 +365,7 @@ ${output.result}`;
|
||||
isSubagentProgress: true,
|
||||
agentName: this.definition.name,
|
||||
recentActivity: [...recentActivity],
|
||||
state: isAbort ? 'cancelled' : 'error',
|
||||
state: isAbort ? SubagentState.CANCELLED : SubagentState.ERROR,
|
||||
};
|
||||
|
||||
if (updateOutput) {
|
||||
|
||||
@@ -250,11 +250,11 @@ describe('AgentRegistry', () => {
|
||||
};
|
||||
|
||||
vi.mocked(tomlLoader.loadAgentsFromDirectory)
|
||||
.mockResolvedValueOnce({ agents: [userAgent], errors: [] }) // User dir
|
||||
.mockResolvedValueOnce({
|
||||
agents: [projectAgent, uniqueProjectAgent],
|
||||
errors: [],
|
||||
}); // Project dir
|
||||
}) // Project dir
|
||||
.mockResolvedValueOnce({ agents: [userAgent], errors: [] }); // User dir
|
||||
|
||||
await registry.initialize();
|
||||
|
||||
@@ -1011,7 +1011,7 @@ describe('AgentRegistry', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should overwrite an existing agent definition', async () => {
|
||||
it('should NOT overwrite an existing agent definition', async () => {
|
||||
await registry.testRegisterAgent(MOCK_AGENT_V1);
|
||||
expect(registry.getDefinition('MockAgent')?.description).toBe(
|
||||
'Mock Description V1',
|
||||
@@ -1019,36 +1019,22 @@ describe('AgentRegistry', () => {
|
||||
|
||||
await registry.testRegisterAgent(MOCK_AGENT_V2);
|
||||
expect(registry.getDefinition('MockAgent')?.description).toBe(
|
||||
'Mock Description V2 (Updated)',
|
||||
'Mock Description V1',
|
||||
);
|
||||
expect(registry.getAllDefinitions()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should log overwrites when in debug mode', async () => {
|
||||
const debugConfig = makeMockedConfig({ debugMode: true });
|
||||
const debugRegistry = new TestableAgentRegistry(debugConfig);
|
||||
const debugLogSpy = vi
|
||||
.spyOn(debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await debugRegistry.testRegisterAgent(MOCK_AGENT_V1);
|
||||
await debugRegistry.testRegisterAgent(MOCK_AGENT_V2);
|
||||
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(
|
||||
`[AgentRegistry] Overriding agent 'MockAgent'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not log overwrites when not in debug mode', async () => {
|
||||
const debugLogSpy = vi
|
||||
.spyOn(debugLogger, 'log')
|
||||
it('should emit warning on duplicate agent definition', async () => {
|
||||
const feedbackSpy = vi
|
||||
.spyOn(coreEvents, 'emitFeedback')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await registry.testRegisterAgent(MOCK_AGENT_V1);
|
||||
await registry.testRegisterAgent(MOCK_AGENT_V2);
|
||||
|
||||
expect(debugLogSpy).not.toHaveBeenCalledWith(
|
||||
`[AgentRegistry] Overriding agent 'MockAgent'`,
|
||||
expect(feedbackSpy).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
expect.stringContaining("Duplicate agent name 'MockAgent' detected"),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -169,31 +169,6 @@ export class AgentRegistry {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load user-level agents: ~/.gemini/agents/
|
||||
const userAgentsDir = Storage.getUserAgentsDir();
|
||||
const userAgents = await loadAgentsFromDirectory(userAgentsDir);
|
||||
for (const error of userAgents.errors) {
|
||||
debugLogger.warn(
|
||||
`[AgentRegistry] Error loading user agent: ${error.message}`,
|
||||
);
|
||||
const msg = `Agent loading error: ${error.message}`;
|
||||
errors?.push(msg);
|
||||
coreEvents.emitFeedback('error', msg);
|
||||
}
|
||||
await Promise.allSettled(
|
||||
userAgents.agents.map(async (agent) => {
|
||||
try {
|
||||
this.ensureRemoteAgentHash(agent);
|
||||
await this.registerAgent(agent, errors);
|
||||
} catch (e) {
|
||||
const msg = `Error registering user agent "${agent.name}": ${e instanceof Error ? e.message : String(e)}`;
|
||||
debugLogger.warn(`[AgentRegistry] ${msg}`, e);
|
||||
errors?.push(msg);
|
||||
coreEvents.emitFeedback('error', msg);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Load project-level agents: .gemini/agents/ (relative to Project Root)
|
||||
const folderTrustEnabled = this.config.getFolderTrust();
|
||||
const isTrustedFolder = this.config.isTrustedFolder();
|
||||
@@ -256,6 +231,31 @@ export class AgentRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
// Load user-level agents: ~/.gemini/agents/
|
||||
const userAgentsDir = Storage.getUserAgentsDir();
|
||||
const userAgents = await loadAgentsFromDirectory(userAgentsDir);
|
||||
for (const error of userAgents.errors) {
|
||||
debugLogger.warn(
|
||||
`[AgentRegistry] Error loading user agent: ${error.message}`,
|
||||
);
|
||||
const msg = `Agent loading error: ${error.message}`;
|
||||
errors?.push(msg);
|
||||
coreEvents.emitFeedback('error', msg);
|
||||
}
|
||||
await Promise.allSettled(
|
||||
userAgents.agents.map(async (agent) => {
|
||||
try {
|
||||
this.ensureRemoteAgentHash(agent);
|
||||
await this.registerAgent(agent, errors);
|
||||
} catch (e) {
|
||||
const msg = `Error registering user agent "${agent.name}": ${e instanceof Error ? e.message : String(e)}`;
|
||||
debugLogger.warn(`[AgentRegistry] ${msg}`, e);
|
||||
errors?.push(msg);
|
||||
coreEvents.emitFeedback('error', msg);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Load agents from extensions
|
||||
for (const extension of this.config.getExtensions()) {
|
||||
if (extension.isActive && extension.agents) {
|
||||
@@ -336,6 +336,17 @@ export class AgentRegistry {
|
||||
definition: AgentDefinition<TOutput>,
|
||||
errors?: string[],
|
||||
): Promise<void> {
|
||||
const existing = this.agents.get(definition.name);
|
||||
if (existing && existing !== definition) {
|
||||
coreEvents.emitFeedback(
|
||||
'warning',
|
||||
`Duplicate agent name '${definition.name}' detected. ` +
|
||||
`The later definition will be ignored. ` +
|
||||
`Rename one of the agents to avoid this conflict.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition.kind === 'local') {
|
||||
this.registerLocalAgent(definition);
|
||||
} else if (definition.kind === 'remote') {
|
||||
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
type A2AClientManager,
|
||||
} from './a2a-client-manager.js';
|
||||
|
||||
import type { RemoteAgentDefinition, SubagentProgress } from './types.js';
|
||||
import {
|
||||
type RemoteAgentDefinition,
|
||||
type SubagentProgress,
|
||||
SubagentState,
|
||||
} from './types.js';
|
||||
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
|
||||
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
|
||||
import type { A2AAuthProvider } from './auth-provider/types.js';
|
||||
@@ -268,7 +272,9 @@ describe('RemoteAgentInvocation', () => {
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
expect(result.returnDisplay).toMatchObject({
|
||||
state: SubagentState.ERROR,
|
||||
});
|
||||
expect((result.returnDisplay as SubagentProgress).result).toContain(
|
||||
"Failed to create auth provider for agent 'test-agent'",
|
||||
);
|
||||
@@ -461,7 +467,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
expect(updateOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isSubagentProgress: true,
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
recentActivity: expect.arrayContaining([
|
||||
expect.objectContaining({ content: 'Working...' }),
|
||||
]),
|
||||
@@ -470,7 +476,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
expect(updateOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isSubagentProgress: true,
|
||||
state: 'completed',
|
||||
state: SubagentState.COMPLETED,
|
||||
result: 'HelloHello World',
|
||||
}),
|
||||
);
|
||||
@@ -508,7 +514,9 @@ describe('RemoteAgentInvocation', () => {
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
expect(result.returnDisplay).toMatchObject({
|
||||
state: SubagentState.ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
@@ -533,7 +541,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
});
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({
|
||||
state: 'error',
|
||||
state: SubagentState.ERROR,
|
||||
result: expect.stringContaining('Network error'),
|
||||
});
|
||||
});
|
||||
@@ -616,7 +624,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
expect(updateOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isSubagentProgress: true,
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
recentActivity: expect.arrayContaining([
|
||||
expect.objectContaining({ content: 'Working...' }),
|
||||
]),
|
||||
@@ -625,7 +633,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
expect(updateOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isSubagentProgress: true,
|
||||
state: 'completed',
|
||||
state: SubagentState.COMPLETED,
|
||||
result: 'Thinking...Final Answer',
|
||||
}),
|
||||
);
|
||||
@@ -693,7 +701,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
expect(updateOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isSubagentProgress: true,
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
recentActivity: expect.arrayContaining([
|
||||
expect.objectContaining({ content: 'Working...' }),
|
||||
]),
|
||||
@@ -702,7 +710,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
expect(updateOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isSubagentProgress: true,
|
||||
state: 'completed',
|
||||
state: SubagentState.COMPLETED,
|
||||
result: 'Generating...\n\nArtifact (Result):\nPart 1 Part 2',
|
||||
}),
|
||||
);
|
||||
@@ -760,7 +768,9 @@ describe('RemoteAgentInvocation', () => {
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
expect(result.returnDisplay).toMatchObject({
|
||||
state: SubagentState.ERROR,
|
||||
});
|
||||
expect((result.returnDisplay as SubagentProgress).result).toContain(
|
||||
a2aError.userMessage,
|
||||
);
|
||||
@@ -782,7 +792,9 @@ describe('RemoteAgentInvocation', () => {
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
expect(result.returnDisplay).toMatchObject({
|
||||
state: SubagentState.ERROR,
|
||||
});
|
||||
expect((result.returnDisplay as SubagentProgress).result).toContain(
|
||||
'Error calling remote agent: something unexpected',
|
||||
);
|
||||
@@ -813,7 +825,9 @@ describe('RemoteAgentInvocation', () => {
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
expect(result.returnDisplay).toMatchObject({
|
||||
state: SubagentState.ERROR,
|
||||
});
|
||||
// Should contain both the partial output and the error message
|
||||
expect(result.returnDisplay).toMatchObject({
|
||||
result: expect.stringContaining('Partial response'),
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type RemoteAgentDefinition,
|
||||
type AgentInputs,
|
||||
type SubagentProgress,
|
||||
SubagentState,
|
||||
getAgentCardLoadOptions,
|
||||
getRemoteAgentTargetUrl,
|
||||
} from './types.js';
|
||||
@@ -138,13 +139,13 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
|
||||
updateOutput({
|
||||
isSubagentProgress: true,
|
||||
agentName,
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
recentActivity: [
|
||||
{
|
||||
id: 'pending',
|
||||
type: 'thought',
|
||||
content: 'Working...',
|
||||
status: 'running',
|
||||
status: SubagentState.RUNNING,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -193,7 +194,7 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
|
||||
updateOutput({
|
||||
isSubagentProgress: true,
|
||||
agentName,
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
recentActivity: reassembler.toActivityItems(),
|
||||
result: reassembler.toString(),
|
||||
});
|
||||
@@ -225,7 +226,7 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
|
||||
const finalProgress: SubagentProgress = {
|
||||
isSubagentProgress: true,
|
||||
agentName,
|
||||
state: 'completed',
|
||||
state: SubagentState.COMPLETED,
|
||||
result: finalOutput,
|
||||
recentActivity: reassembler.toActivityItems(),
|
||||
};
|
||||
@@ -249,7 +250,7 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
|
||||
const errorProgress: SubagentProgress = {
|
||||
isSubagentProgress: true,
|
||||
agentName,
|
||||
state: 'error',
|
||||
state: SubagentState.ERROR,
|
||||
result: fullDisplay,
|
||||
recentActivity: reassembler.toActivityItems(),
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
DEFAULT_QUERY_STRING,
|
||||
type RemoteAgentDefinition,
|
||||
type SubagentProgress,
|
||||
SubagentState,
|
||||
getRemoteAgentTargetUrl,
|
||||
getAgentCardLoadOptions,
|
||||
} from './types.js';
|
||||
@@ -233,7 +234,7 @@ class RemoteSubagentProtocol implements AgentProtocol {
|
||||
this._latestProgress = {
|
||||
isSubagentProgress: true,
|
||||
agentName: this._agentName,
|
||||
state: 'running',
|
||||
state: SubagentState.RUNNING,
|
||||
recentActivity: reassembler.toActivityItems(),
|
||||
result: currentText,
|
||||
};
|
||||
@@ -259,7 +260,7 @@ class RemoteSubagentProtocol implements AgentProtocol {
|
||||
const finalProgress: SubagentProgress = {
|
||||
isSubagentProgress: true,
|
||||
agentName: this._agentName,
|
||||
state: 'completed',
|
||||
state: SubagentState.COMPLETED,
|
||||
result: finalOutput,
|
||||
recentActivity: reassembler.toActivityItems(),
|
||||
};
|
||||
|
||||
@@ -88,6 +88,13 @@ export interface SubagentActivityEvent {
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export enum SubagentState {
|
||||
RUNNING = 'running',
|
||||
COMPLETED = 'completed',
|
||||
ERROR = 'error',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
export interface SubagentActivityItem {
|
||||
id: string;
|
||||
type: 'thought' | 'tool_call';
|
||||
@@ -95,14 +102,14 @@ export interface SubagentActivityItem {
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
args?: string;
|
||||
status: 'running' | 'completed' | 'error' | 'cancelled';
|
||||
status: SubagentState;
|
||||
}
|
||||
|
||||
export interface SubagentProgress {
|
||||
isSubagentProgress: true;
|
||||
agentName: string;
|
||||
recentActivity: SubagentActivityItem[];
|
||||
state?: 'running' | 'completed' | 'error' | 'cancelled';
|
||||
state?: SubagentState;
|
||||
result?: string;
|
||||
terminateReason?: AgentTerminateMode;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ const createMockConfig = (overrides: Partial<Config> = {}): Config => {
|
||||
return useGemini31 && authType === AuthType.USE_GEMINI;
|
||||
},
|
||||
getContentGeneratorConfig: () => ({ authType: undefined }),
|
||||
getHasAccessToPreviewModel: () => true,
|
||||
getMaxAttemptsPerTurn: () => 3,
|
||||
getExperimentalDynamicModelConfiguration: () => false,
|
||||
getReleaseChannel: () => 'preview',
|
||||
modelConfigService: new ModelConfigService(DEFAULT_MODEL_CONFIGS),
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
return config;
|
||||
@@ -187,6 +191,7 @@ describe('policyHelpers', () => {
|
||||
const testCases = [
|
||||
{ name: 'Default Auto', model: DEFAULT_GEMINI_MODEL_AUTO },
|
||||
{ name: 'Gemini 3 Auto', model: 'auto-gemini-3' },
|
||||
{ name: 'Unified Auto', model: 'auto' },
|
||||
{ name: 'Flash Lite', model: DEFAULT_GEMINI_FLASH_LITE_MODEL },
|
||||
{
|
||||
name: 'Gemini 3 Auto (3.1 Enabled)',
|
||||
@@ -215,7 +220,18 @@ describe('policyHelpers', () => {
|
||||
];
|
||||
|
||||
testCases.forEach(
|
||||
({ name, model, useGemini31, hasAccess, authType, wrapsAround }) => {
|
||||
({
|
||||
name,
|
||||
model,
|
||||
useGemini31,
|
||||
hasAccess,
|
||||
authType,
|
||||
wrapsAround,
|
||||
...rest
|
||||
}) => {
|
||||
const releaseChannel = (rest as Record<string, unknown>)[
|
||||
'releaseChannel'
|
||||
] as string | undefined;
|
||||
it(`achieves parity for: ${name}`, () => {
|
||||
const createBaseConfig = (dynamic: boolean) =>
|
||||
createMockConfig({
|
||||
@@ -225,6 +241,7 @@ describe('policyHelpers', () => {
|
||||
getGemini31FlashLiteLaunchedSync: () => false,
|
||||
getHasAccessToPreviewModel: () => hasAccess ?? true,
|
||||
getContentGeneratorConfig: () => ({ authType }),
|
||||
getReleaseChannel: () => releaseChannel ?? 'preview',
|
||||
modelConfigService: new ModelConfigService(DEFAULT_MODEL_CONFIGS),
|
||||
});
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ export function resolvePolicyChain(
|
||||
useGemini3_1: useGemini31,
|
||||
useGemini3_1FlashLite: useGemini31FlashLite,
|
||||
useCustomTools: useCustomToolModel,
|
||||
releaseChannel: config.getReleaseChannel?.(),
|
||||
};
|
||||
|
||||
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
|
||||
|
||||
@@ -679,8 +679,13 @@ async function fetchCachedCredentials(): Promise<
|
||||
for (const keyFile of pathsToTry) {
|
||||
try {
|
||||
const keyFileString = await fs.readFile(keyFile, 'utf-8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return JSON.parse(keyFileString);
|
||||
const parsed: unknown = JSON.parse(keyFileString);
|
||||
const isOAuthCreds = (val: unknown): val is Credentials | JWTInput =>
|
||||
typeof val === 'object' && val !== null;
|
||||
if (isOAuthCreds(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
throw new Error('Invalid credentials format');
|
||||
} catch (error) {
|
||||
// Log specific error for debugging, but continue trying other paths
|
||||
debugLogger.debug(
|
||||
|
||||
@@ -80,14 +80,11 @@ import { tokenLimit } from '../core/tokenLimits.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
isAutoModel,
|
||||
isPreviewModel,
|
||||
isGemini2Model,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
resolveModel,
|
||||
} from './models.js';
|
||||
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
|
||||
@@ -444,6 +441,7 @@ export interface ExtensionInstallMetadata {
|
||||
allowPreRelease?: boolean;
|
||||
}
|
||||
|
||||
import { getChannelFromVersion } from '../utils/channel.js';
|
||||
import { DEFAULT_MAX_ATTEMPTS } from '../utils/retry.js';
|
||||
import {
|
||||
DEFAULT_FILE_FILTERING_OPTIONS,
|
||||
@@ -759,7 +757,8 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private skillManager!: SkillManager;
|
||||
private _sessionId: string;
|
||||
private readonly clientName: string | undefined;
|
||||
private clientVersion: string;
|
||||
private _clientVersion: string;
|
||||
|
||||
private fileSystemService: FileSystemService;
|
||||
private trackerService?: TrackerService;
|
||||
readonly topicState = new TopicState();
|
||||
@@ -977,8 +976,9 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
constructor(params: ConfigParameters) {
|
||||
this._sessionId = params.sessionId;
|
||||
this.clientName = params.clientName;
|
||||
this.clientVersion = params.clientVersion ?? 'unknown';
|
||||
this._clientVersion = params.clientVersion ?? 'unknown';
|
||||
this.approvedPlanPath = undefined;
|
||||
|
||||
this.embeddingModel =
|
||||
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
|
||||
this.sandbox = params.sandbox
|
||||
@@ -2000,14 +2000,21 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
resetTime?: string;
|
||||
} {
|
||||
const model = this.getModel();
|
||||
if (!isAutoModel(model)) {
|
||||
if (!isAutoModel(model, this)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const isPreview =
|
||||
model === PREVIEW_GEMINI_MODEL_AUTO ||
|
||||
isPreviewModel(this.getActiveModel(), this);
|
||||
const proModel = isPreview ? PREVIEW_GEMINI_MODEL : DEFAULT_GEMINI_MODEL;
|
||||
const primaryModel = resolveModel(
|
||||
model,
|
||||
this.getGemini31LaunchedSync(),
|
||||
this.getGemini31FlashLiteLaunchedSync(),
|
||||
this.getUseCustomToolModelSync(),
|
||||
this.getHasAccessToPreviewModel(),
|
||||
this,
|
||||
);
|
||||
|
||||
const isPreview = isPreviewModel(primaryModel, this);
|
||||
const proModel = primaryModel;
|
||||
const flashModel = isPreview
|
||||
? PREVIEW_GEMINI_FLASH_MODEL
|
||||
: DEFAULT_GEMINI_FLASH_MODEL;
|
||||
@@ -2755,6 +2762,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.dynamicModelConfiguration;
|
||||
}
|
||||
|
||||
getReleaseChannel(): string {
|
||||
return getChannelFromVersion(this._clientVersion);
|
||||
}
|
||||
|
||||
getPendingIncludeDirectories(): string[] {
|
||||
return this.pendingIncludeDirectories;
|
||||
}
|
||||
@@ -3503,6 +3514,13 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client version.
|
||||
*/
|
||||
get clientVersion(): string {
|
||||
return this._clientVersion;
|
||||
}
|
||||
|
||||
private async ensureExperimentsLoaded(): Promise<void> {
|
||||
if (!this.experimentsPromise) {
|
||||
return;
|
||||
|
||||
@@ -362,9 +362,10 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
|
||||
// Aliases
|
||||
auto: {
|
||||
displayName: 'Auto',
|
||||
tier: 'auto',
|
||||
isPreview: true,
|
||||
isVisible: false,
|
||||
isVisible: true,
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
},
|
||||
pro: {
|
||||
@@ -386,22 +387,16 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'auto-gemini-3': {
|
||||
displayName: 'Auto (Gemini 3)',
|
||||
tier: 'auto',
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
isVisible: true,
|
||||
dialogDescription:
|
||||
'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
isVisible: false,
|
||||
},
|
||||
'auto-gemini-2.5': {
|
||||
displayName: 'Auto (Gemini 2.5)',
|
||||
tier: 'auto',
|
||||
family: 'gemini-2.5',
|
||||
isPreview: false,
|
||||
isVisible: true,
|
||||
dialogDescription:
|
||||
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
isVisible: false,
|
||||
},
|
||||
},
|
||||
modelIdResolutions: {
|
||||
@@ -451,23 +446,10 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
'auto-gemini-3': {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
|
||||
{
|
||||
condition: { useGemini3_1: true, useCustomTools: true },
|
||||
target: 'gemini-3.1-pro-preview-customtools',
|
||||
},
|
||||
{
|
||||
condition: { useGemini3_1: true },
|
||||
target: 'gemini-3.1-pro-preview',
|
||||
},
|
||||
],
|
||||
},
|
||||
auto: {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
{ condition: { releaseChannel: 'stable' }, target: 'gemini-2.5-pro' },
|
||||
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
|
||||
{
|
||||
condition: { useGemini3_1: true, useCustomTools: true },
|
||||
@@ -493,9 +475,6 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
'auto-gemini-2.5': {
|
||||
default: 'gemini-2.5-pro',
|
||||
},
|
||||
'gemini-3.1-flash-lite-preview': {
|
||||
default: 'gemini-3.1-flash-lite-preview',
|
||||
contexts: [
|
||||
@@ -523,20 +502,35 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
'auto-gemini-3': {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
|
||||
{
|
||||
condition: { useGemini3_1: true, useCustomTools: true },
|
||||
target: 'gemini-3.1-pro-preview-customtools',
|
||||
},
|
||||
{
|
||||
condition: { useGemini3_1: true },
|
||||
target: 'gemini-3.1-pro-preview',
|
||||
},
|
||||
],
|
||||
},
|
||||
'auto-gemini-2.5': {
|
||||
default: 'gemini-2.5-pro',
|
||||
},
|
||||
},
|
||||
classifierIdResolutions: {
|
||||
flash: {
|
||||
default: 'gemini-3-flash-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { requestedModels: ['auto-gemini-2.5', 'gemini-2.5-pro'] },
|
||||
condition: { hasAccessToPreview: false },
|
||||
target: 'gemini-2.5-flash',
|
||||
},
|
||||
{
|
||||
condition: {
|
||||
requestedModels: ['auto-gemini-3', 'gemini-3-pro-preview'],
|
||||
},
|
||||
target: 'gemini-3-flash-preview',
|
||||
condition: { requestedModels: ['gemini-2.5-pro', 'auto-gemini-2.5'] },
|
||||
target: 'gemini-2.5-flash',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -544,7 +538,15 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { requestedModels: ['auto-gemini-2.5', 'gemini-2.5-pro'] },
|
||||
condition: { hasAccessToPreview: false },
|
||||
target: 'gemini-2.5-pro',
|
||||
},
|
||||
{
|
||||
condition: { releaseChannel: 'stable', requestedModels: ['auto'] },
|
||||
target: 'gemini-2.5-pro',
|
||||
},
|
||||
{
|
||||
condition: { requestedModels: ['gemini-2.5-pro', 'auto-gemini-2.5'] },
|
||||
target: 'gemini-2.5-pro',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface ModelResolutionContext {
|
||||
useCustomTools?: boolean;
|
||||
hasAccessToPreview?: boolean;
|
||||
requestedModel?: string;
|
||||
releaseChannel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,7 @@ export interface IModelConfigService {
|
||||
export interface ModelCapabilityContext {
|
||||
readonly modelConfigService: IModelConfigService;
|
||||
getExperimentalDynamicModelConfiguration(): boolean;
|
||||
getReleaseChannel?(): string;
|
||||
}
|
||||
|
||||
export const PREVIEW_GEMINI_MODEL = 'gemini-3-pro-preview';
|
||||
@@ -78,7 +80,9 @@ export const VALID_GEMINI_MODELS = new Set([
|
||||
GEMMA_4_26B_A4B_IT_MODEL,
|
||||
]);
|
||||
|
||||
/** @deprecated Use GEMINI_MODEL_ALIAS_AUTO instead. */
|
||||
export const PREVIEW_GEMINI_MODEL_AUTO = 'auto-gemini-3';
|
||||
/** @deprecated Use GEMINI_MODEL_ALIAS_AUTO instead. */
|
||||
export const DEFAULT_GEMINI_MODEL_AUTO = 'auto-gemini-2.5';
|
||||
|
||||
// Model aliases for user convenience.
|
||||
@@ -92,8 +96,22 @@ export const DEFAULT_GEMINI_EMBEDDING_MODEL = 'gemini-embedding-001';
|
||||
// Cap the thinking at 8192 to prevent run-away thinking loops.
|
||||
export const DEFAULT_THINKING_MODE = 8192;
|
||||
|
||||
export function getAutoModelDescription(
|
||||
releaseChannel: string = 'stable',
|
||||
useGemini3_1: boolean = false,
|
||||
) {
|
||||
const isPreview = releaseChannel === 'preview';
|
||||
const proModel = isPreview
|
||||
? useGemini3_1
|
||||
? 'gemini-3.1-pro'
|
||||
: 'gemini-3-pro'
|
||||
: 'gemini-2.5-pro';
|
||||
const flashModel = isPreview ? 'gemini-3-flash' : 'gemini-2.5-flash';
|
||||
return `Let Gemini CLI decide the best model for the task: ${proModel}, ${flashModel}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the requested model alias (e.g., 'auto-gemini-3', 'pro', 'flash', 'flash-lite')
|
||||
* Resolves the requested model alias (e.g., 'auto', 'pro', 'flash', 'flash-lite')
|
||||
* to a concrete model name.
|
||||
*
|
||||
* @param requestedModel The model alias or concrete model name requested by the user.
|
||||
@@ -108,6 +126,7 @@ export function resolveModel(
|
||||
useCustomToolModel: boolean = false,
|
||||
hasAccessToPreview: boolean = true,
|
||||
config?: ModelCapabilityContext,
|
||||
releaseChannel?: string,
|
||||
): string {
|
||||
// Defensive check against non-string inputs at runtime
|
||||
const normalizedModel = Array.isArray(requestedModel)
|
||||
@@ -116,12 +135,15 @@ export function resolveModel(
|
||||
? String(requestedModel ?? '').trim() || ''
|
||||
: requestedModel.trim() || '';
|
||||
|
||||
const currentReleaseChannel = releaseChannel ?? config?.getReleaseChannel?.();
|
||||
|
||||
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
|
||||
const resolved = config.modelConfigService.resolveModelId(normalizedModel, {
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
releaseChannel: currentReleaseChannel,
|
||||
});
|
||||
|
||||
if (!hasAccessToPreview && isPreviewModel(resolved, config)) {
|
||||
@@ -140,10 +162,16 @@ export function resolveModel(
|
||||
|
||||
let resolved: string;
|
||||
switch (normalizedModel) {
|
||||
case PREVIEW_GEMINI_MODEL:
|
||||
case PREVIEW_GEMINI_MODEL_AUTO:
|
||||
case GEMINI_MODEL_ALIAS_AUTO:
|
||||
case GEMINI_MODEL_ALIAS_PRO: {
|
||||
if (currentReleaseChannel === 'stable') {
|
||||
resolved = DEFAULT_GEMINI_MODEL;
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
case PREVIEW_GEMINI_MODEL:
|
||||
case PREVIEW_GEMINI_MODEL_AUTO: {
|
||||
if (useGemini3_1) {
|
||||
resolved = useCustomToolModel
|
||||
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
|
||||
@@ -202,7 +230,7 @@ export function resolveModel(
|
||||
/**
|
||||
* Resolves the appropriate model based on the classifier's decision.
|
||||
*
|
||||
* @param requestedModel The current requested model (e.g. auto-gemini-2.5).
|
||||
* @param requestedModel The current requested model (e.g. auto).
|
||||
* @param modelAlias The alias selected by the classifier ('flash' or 'pro').
|
||||
* @param useGemini3_1 Whether to use Gemini 3.1 Pro Preview.
|
||||
* @param useCustomToolModel Whether to use the custom tool model.
|
||||
@@ -240,17 +268,27 @@ export function resolveClassifierModel(
|
||||
}
|
||||
if (
|
||||
requestedModel === PREVIEW_GEMINI_MODEL_AUTO ||
|
||||
requestedModel === PREVIEW_GEMINI_MODEL
|
||||
requestedModel === PREVIEW_GEMINI_MODEL ||
|
||||
requestedModel === GEMINI_MODEL_ALIAS_AUTO
|
||||
) {
|
||||
return PREVIEW_GEMINI_FLASH_MODEL;
|
||||
return hasAccessToPreview
|
||||
? PREVIEW_GEMINI_FLASH_MODEL
|
||||
: DEFAULT_GEMINI_FLASH_MODEL;
|
||||
}
|
||||
return resolveModel(GEMINI_MODEL_ALIAS_FLASH);
|
||||
return resolveModel(
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
hasAccessToPreview,
|
||||
);
|
||||
}
|
||||
return resolveModel(
|
||||
requestedModel,
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -266,6 +304,8 @@ export function getDisplayString(
|
||||
}
|
||||
|
||||
switch (model) {
|
||||
case GEMINI_MODEL_ALIAS_AUTO:
|
||||
return 'Auto';
|
||||
case PREVIEW_GEMINI_MODEL_AUTO:
|
||||
return 'Auto (Gemini 3)';
|
||||
case DEFAULT_GEMINI_MODEL_AUTO:
|
||||
@@ -345,7 +385,7 @@ export function isGemini3Model(
|
||||
): boolean {
|
||||
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
|
||||
// Legacy behavior resolves the model first.
|
||||
const resolved = resolveModel(model);
|
||||
const resolved = resolveModel(model, false, false, false, true, config);
|
||||
return (
|
||||
config.modelConfigService.getModelDefinition(resolved)?.family ===
|
||||
'gemini-3'
|
||||
|
||||
@@ -76,7 +76,7 @@ export class ProjectRegistry {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
return { projects: {} }; // Normal first run
|
||||
}
|
||||
if (error instanceof SyntaxError) {
|
||||
if (error instanceof SyntaxError || error instanceof z.ZodError) {
|
||||
debugLogger.warn(
|
||||
'Failed to load registry (JSON corrupted), resetting to empty: ',
|
||||
error,
|
||||
|
||||
@@ -177,10 +177,15 @@ export class BaseLlmClient {
|
||||
);
|
||||
|
||||
// If we are here, the content is valid (not empty and parsable).
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return JSON.parse(
|
||||
const parsed: unknown = JSON.parse(
|
||||
this.cleanJsonResponse(getResponseText(result)!.trim(), model),
|
||||
);
|
||||
const isRecord = (val: unknown): val is Record<string, unknown> =>
|
||||
typeof val === 'object' && val !== null && !Array.isArray(val);
|
||||
if (isRecord(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
throw new Error('Invalid JSON response format from LLM');
|
||||
}
|
||||
|
||||
async generateEmbedding(texts: string[]): Promise<number[][]> {
|
||||
|
||||
@@ -84,11 +84,12 @@ export class FakeContentGenerator implements ContentGenerator {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
role: LlmRole,
|
||||
): Promise<GenerateContentResponse> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return Object.setPrototypeOf(
|
||||
this.getNextResponse('generateContent', request),
|
||||
GenerateContentResponse.prototype,
|
||||
);
|
||||
const response: unknown = this.getNextResponse('generateContent', request);
|
||||
Object.setPrototypeOf(response, GenerateContentResponse.prototype);
|
||||
if (response instanceof GenerateContentResponse) {
|
||||
return response;
|
||||
}
|
||||
throw new Error('Failed to create GenerateContentResponse');
|
||||
}
|
||||
|
||||
async generateContentStream(
|
||||
@@ -118,10 +119,11 @@ export class FakeContentGenerator implements ContentGenerator {
|
||||
async embedContent(
|
||||
request: EmbedContentParameters,
|
||||
): Promise<EmbedContentResponse> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return Object.setPrototypeOf(
|
||||
this.getNextResponse('embedContent', request),
|
||||
EmbedContentResponse.prototype,
|
||||
);
|
||||
const response: unknown = this.getNextResponse('embedContent', request);
|
||||
Object.setPrototypeOf(response, EmbedContentResponse.prototype);
|
||||
if (response instanceof EmbedContentResponse) {
|
||||
return response;
|
||||
}
|
||||
throw new Error('Failed to create EmbedContentResponse');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +84,13 @@ export class LocalLiteRtLmClient {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return JSON.parse(result.text);
|
||||
const parsed: unknown = JSON.parse(result.text);
|
||||
const isRecord = (val: unknown): val is Record<string, unknown> =>
|
||||
typeof val === 'object' && val !== null && !Array.isArray(val);
|
||||
if (isRecord(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
throw new Error('Invalid JSON response format from Local LLM');
|
||||
} catch (error) {
|
||||
debugLogger.error(
|
||||
`[LocalLiteRtLmClient] Failed to generate content:`,
|
||||
|
||||
@@ -348,9 +348,11 @@ export class IdeClient {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const parsedJson = JSON.parse(textPart.text);
|
||||
if (parsedJson && typeof parsedJson.content === 'string') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return parsedJson.content;
|
||||
if (parsedJson) {
|
||||
const content: unknown = parsedJson.content;
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
if (parsedJson && parsedJson.content === null) {
|
||||
return undefined;
|
||||
|
||||
@@ -123,8 +123,17 @@ export async function getConnectionConfigFromFile(
|
||||
`gemini-ide-server-${pid}.json`,
|
||||
);
|
||||
const portFileContents = await fs.promises.readFile(portFile, 'utf8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return JSON.parse(portFileContents);
|
||||
const parsed: unknown = JSON.parse(portFileContents);
|
||||
type ConfigType = ConnectionConfig & {
|
||||
workspacePath?: string;
|
||||
ideInfo?: IdeInfo;
|
||||
};
|
||||
const isConfig = (val: unknown): val is ConfigType =>
|
||||
typeof val === 'object' && val !== null;
|
||||
if (isConfig(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
throw new Error('Invalid connection config format');
|
||||
} catch {
|
||||
// For newer extension versions, the file name matches the pattern
|
||||
// /^gemini-ide-server-${pid}-\d+\.json$/. If multiple IDE
|
||||
@@ -166,39 +175,59 @@ export async function getConnectionConfigFromFile(
|
||||
logger.debug('Failed to read IDE connection config file(s):', e);
|
||||
return undefined;
|
||||
}
|
||||
const parsedContents = fileContents.map((content) => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
logger.debug('Failed to parse JSON from config file: ', e);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
const parsedContents = fileContents.map(
|
||||
(
|
||||
content,
|
||||
):
|
||||
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
|
||||
| undefined => {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(content);
|
||||
type ConfigType = ConnectionConfig & {
|
||||
workspacePath?: string;
|
||||
ideInfo?: IdeInfo;
|
||||
};
|
||||
const isConfig = (val: unknown): val is ConfigType =>
|
||||
typeof val === 'object' && val !== null;
|
||||
if (isConfig(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
logger.debug('Failed to parse JSON from config file: ', e);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const validWorkspaces = parsedContents.filter((content) => {
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
const { isValid } = validateWorkspacePath(
|
||||
content.workspacePath,
|
||||
process.cwd(),
|
||||
);
|
||||
return isValid;
|
||||
});
|
||||
const validWorkspaces = parsedContents.filter(
|
||||
(
|
||||
content,
|
||||
): content is ConnectionConfig & {
|
||||
workspacePath?: string;
|
||||
ideInfo?: IdeInfo;
|
||||
} => {
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
const { isValid } = validateWorkspacePath(
|
||||
content.workspacePath,
|
||||
process.cwd(),
|
||||
);
|
||||
return isValid;
|
||||
},
|
||||
);
|
||||
|
||||
if (validWorkspaces.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (validWorkspaces.length === 1) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const selected = validWorkspaces[0];
|
||||
const fileIndex = parsedContents.indexOf(selected);
|
||||
if (fileIndex !== -1) {
|
||||
logger.debug(`Selected IDE connection file: ${matchingFiles[fileIndex]}`);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return selected;
|
||||
}
|
||||
|
||||
@@ -208,7 +237,6 @@ export async function getConnectionConfigFromFile(
|
||||
(content) => String(content.port) === portFromEnv,
|
||||
);
|
||||
if (matchingPortIndex !== -1) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const selected = validWorkspaces[matchingPortIndex];
|
||||
const fileIndex = parsedContents.indexOf(selected);
|
||||
if (fileIndex !== -1) {
|
||||
@@ -216,12 +244,10 @@ export async function getConnectionConfigFromFile(
|
||||
`Selected IDE connection file (matched port from env): ${matchingFiles[fileIndex]}`,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const selected = validWorkspaces[0];
|
||||
const fileIndex = parsedContents.indexOf(selected);
|
||||
if (fileIndex !== -1) {
|
||||
@@ -229,7 +255,6 @@ export async function getConnectionConfigFromFile(
|
||||
`Selected first valid IDE connection file: ${matchingFiles[fileIndex]}`,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return selected;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
isProModel,
|
||||
getAutoModelDescription,
|
||||
} from '../config/models.js';
|
||||
|
||||
// The primary key for the ModelConfig is the model string. However, we also
|
||||
@@ -101,6 +102,7 @@ export interface ResolutionContext {
|
||||
hasAccessToPreview?: boolean;
|
||||
hasAccessToProModel?: boolean;
|
||||
requestedModel?: string;
|
||||
releaseChannel?: string;
|
||||
}
|
||||
|
||||
/** The requirements defined in the registry. */
|
||||
@@ -111,6 +113,7 @@ export interface ResolutionCondition {
|
||||
hasAccessToPreview?: boolean;
|
||||
/** Matches if the current model is in this list. */
|
||||
requestedModels?: string[];
|
||||
releaseChannel?: string;
|
||||
}
|
||||
|
||||
export interface ModelConfigServiceConfig {
|
||||
@@ -156,6 +159,7 @@ export class ModelConfigService {
|
||||
const shouldShowPreviewModels = context.hasAccessToPreview ?? false;
|
||||
const useGemini31 = context.useGemini3_1 ?? false;
|
||||
const useGemini31FlashLite = context.useGemini3_1FlashLite ?? false;
|
||||
const releaseChannel = context.releaseChannel ?? 'stable';
|
||||
|
||||
const mainOptions = Object.entries(definitions)
|
||||
.filter(([_, m]) => {
|
||||
@@ -164,18 +168,21 @@ export class ModelConfigService {
|
||||
if (m.tier !== 'auto') return false;
|
||||
return true;
|
||||
})
|
||||
.map(([id, m]) => ({
|
||||
modelId: id,
|
||||
name: m.displayName ?? getDisplayString(id),
|
||||
description:
|
||||
id === 'auto-gemini-3' && useGemini31
|
||||
? (m.dialogDescription ?? '').replace(
|
||||
'gemini-3-pro',
|
||||
'gemini-3.1-pro',
|
||||
)
|
||||
: (m.dialogDescription ?? ''),
|
||||
tier: m.tier ?? 'auto',
|
||||
}));
|
||||
.map(([id, m]) => {
|
||||
let description = m.dialogDescription ?? '';
|
||||
if (id === 'auto') {
|
||||
description = getAutoModelDescription(releaseChannel, useGemini31);
|
||||
} else if (id === 'auto-gemini-3' && useGemini31) {
|
||||
description = description.replace('gemini-3-pro', 'gemini-3.1-pro');
|
||||
}
|
||||
|
||||
return {
|
||||
modelId: id,
|
||||
name: m.displayName ?? getDisplayString(id),
|
||||
description,
|
||||
tier: m.tier ?? 'auto',
|
||||
};
|
||||
});
|
||||
|
||||
const manualOptions = Object.entries(definitions)
|
||||
.filter(([id, m]) => {
|
||||
@@ -258,6 +265,8 @@ export class ModelConfigService {
|
||||
!!context.requestedModel &&
|
||||
value.includes(context.requestedModel)
|
||||
);
|
||||
case 'releaseChannel':
|
||||
return value === context.releaseChannel;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
+10
-4
@@ -319,6 +319,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
|
||||
},
|
||||
"context": {
|
||||
"description": "Show this many lines of context around each match (equivalent to grep -C). Defaults to 0 if omitted.",
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
},
|
||||
"dir_path": {
|
||||
@@ -416,7 +417,8 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
|
||||
"properties": {
|
||||
"end_line": {
|
||||
"description": "Optional: The 1-based line number to end reading at (inclusive).",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
"file_path": {
|
||||
"description": "The path to the file to read.",
|
||||
@@ -424,7 +426,8 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
|
||||
},
|
||||
"start_line": {
|
||||
"description": "Optional: The 1-based line number to start reading from.",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
@@ -1114,6 +1117,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
|
||||
},
|
||||
"context": {
|
||||
"description": "Show this many lines of context around each match (equivalent to grep -C). Defaults to 0 if omitted.",
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
},
|
||||
"dir_path": {
|
||||
@@ -1211,7 +1215,8 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
|
||||
"properties": {
|
||||
"end_line": {
|
||||
"description": "Optional: The 1-based line number to end reading at (inclusive).",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
"file_path": {
|
||||
"description": "The path to the file to read.",
|
||||
@@ -1219,7 +1224,8 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
|
||||
},
|
||||
"start_line": {
|
||||
"description": "Optional: The 1-based line number to start reading from.",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -94,12 +94,14 @@ export const DEFAULT_LEGACY_SET: CoreToolSet = {
|
||||
[READ_FILE_PARAM_START_LINE]: {
|
||||
description:
|
||||
'Optional: The 1-based line number to start reading from.',
|
||||
type: 'number',
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
},
|
||||
[READ_FILE_PARAM_END_LINE]: {
|
||||
description:
|
||||
'Optional: The 1-based line number to end reading at (inclusive).',
|
||||
type: 'number',
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
},
|
||||
},
|
||||
required: [PARAM_FILE_PATH],
|
||||
@@ -220,6 +222,7 @@ export const DEFAULT_LEGACY_SET: CoreToolSet = {
|
||||
description:
|
||||
'Show this many lines of context around each match (equivalent to grep -C). Defaults to 0 if omitted.',
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
},
|
||||
[GREP_PARAM_AFTER]: {
|
||||
description:
|
||||
|
||||
@@ -103,12 +103,14 @@ export const GEMINI_3_SET: CoreToolSet = {
|
||||
[READ_FILE_PARAM_START_LINE]: {
|
||||
description:
|
||||
'Optional: The 1-based line number to start reading from.',
|
||||
type: 'number',
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
},
|
||||
[READ_FILE_PARAM_END_LINE]: {
|
||||
description:
|
||||
'Optional: The 1-based line number to end reading at (inclusive).',
|
||||
type: 'number',
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
},
|
||||
},
|
||||
required: [PARAM_FILE_PATH],
|
||||
@@ -227,6 +229,7 @@ export const GEMINI_3_SET: CoreToolSet = {
|
||||
description:
|
||||
'Show this many lines of context around each match (equivalent to grep -C). Defaults to 0 if omitted.',
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
},
|
||||
[GREP_PARAM_AFTER]: {
|
||||
description:
|
||||
|
||||
@@ -148,18 +148,20 @@ describe('ReadFileTool', () => {
|
||||
|
||||
it('should throw error if start_line is less than 1', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
file_path: path.join(tempRootDir, 'test.txt'),
|
||||
file_path: 'test.txt',
|
||||
start_line: 0,
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow('start_line must be at least 1');
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
'params/start_line must be >= 1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if end_line is less than 1', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
file_path: path.join(tempRootDir, 'test.txt'),
|
||||
file_path: 'test.txt',
|
||||
end_line: 0,
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow('end_line must be at least 1');
|
||||
expect(() => tool.build(params)).toThrow('params/end_line must be >= 1');
|
||||
});
|
||||
|
||||
it('should throw error if start_line is greater than end_line', () => {
|
||||
|
||||
@@ -255,12 +255,6 @@ export class ReadFileTool extends BaseDeclarativeTool<
|
||||
return validationError;
|
||||
}
|
||||
|
||||
if (params.start_line !== undefined && params.start_line < 1) {
|
||||
return 'start_line must be at least 1';
|
||||
}
|
||||
if (params.end_line !== undefined && params.end_line < 1) {
|
||||
return 'end_line must be at least 1';
|
||||
}
|
||||
if (
|
||||
params.start_line !== undefined &&
|
||||
params.end_line !== undefined &&
|
||||
|
||||
@@ -9,7 +9,7 @@ import picomatch from 'picomatch';
|
||||
import { loadIgnoreRules, type Ignore } from './ignore.js';
|
||||
import { ResultCache } from './result-cache.js';
|
||||
import { crawl } from './crawler.js';
|
||||
import { AsyncFzf, type FzfResultItem } from 'fzf';
|
||||
import { AsyncFzf } from 'fzf';
|
||||
import { unescapePath } from '../paths.js';
|
||||
import type { FileDiscoveryService } from '../../services/fileDiscoveryService.js';
|
||||
import { FileWatcher, type FileWatcherEvent } from './fileWatcher.js';
|
||||
@@ -270,7 +270,7 @@ class RecursiveFileSearch implements FileSearch {
|
||||
|
||||
pattern = unescapePath(pattern) || '*';
|
||||
|
||||
let filteredCandidates;
|
||||
let filteredCandidates: string[];
|
||||
const { files: candidates, isExactMatch } =
|
||||
await this.resultCache.get(pattern);
|
||||
|
||||
@@ -282,17 +282,27 @@ class RecursiveFileSearch implements FileSearch {
|
||||
if (pattern.includes('*') || !this.fzf) {
|
||||
filteredCandidates = await filter(candidates, pattern, options.signal);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
filteredCandidates = await this.fzf
|
||||
.find(pattern)
|
||||
.then((results: Array<FzfResultItem<string>>) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
results.map((entry: FzfResultItem<string>) => entry.item),
|
||||
)
|
||||
.catch(() => {
|
||||
try {
|
||||
const fzfResult: unknown = await this.fzf.find(pattern);
|
||||
if (Array.isArray(fzfResult)) {
|
||||
filteredCandidates = fzfResult.map((entry: unknown) => {
|
||||
if (
|
||||
typeof entry === 'object' &&
|
||||
entry !== null &&
|
||||
'item' in entry
|
||||
) {
|
||||
return String((entry as { item: unknown }).item);
|
||||
}
|
||||
return String(entry);
|
||||
});
|
||||
} else {
|
||||
shouldCache = false;
|
||||
return [];
|
||||
});
|
||||
filteredCandidates = [];
|
||||
}
|
||||
} catch {
|
||||
shouldCache = false;
|
||||
filteredCandidates = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCache) {
|
||||
|
||||
@@ -27,8 +27,7 @@ export function safeJsonStringify(
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return value;
|
||||
return value as unknown;
|
||||
},
|
||||
space,
|
||||
);
|
||||
@@ -61,8 +60,7 @@ export function safeJsonStringifyBooleanValuesOnly(obj: any): string {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
if ((value as Config) !== null && !configSeen) {
|
||||
configSeen = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return value;
|
||||
return value as unknown;
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
|
||||
Reference in New Issue
Block a user