mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 12:57:12 -07:00
fix(watcher): safely parse structured subagent output and clean up tests
This commit is contained in:
@@ -941,10 +941,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly adminSkillsEnabled: boolean;
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly experimentalMemoryManager: boolean;
|
||||
private readonly experimentalAgentHistoryTruncation: boolean;
|
||||
private readonly experimentalAgentHistoryTruncationThreshold: number;
|
||||
private readonly experimentalAgentHistoryRetainedMessages: number;
|
||||
private readonly experimentalAgentHistorySummarization: boolean;
|
||||
private readonly experimentalWatcher: boolean;
|
||||
private readonly experimentalWatcherInterval: number;
|
||||
private readonly memoryBoundaryMarkers: readonly string[];
|
||||
@@ -1158,14 +1154,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
|
||||
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||
this.experimentalMemoryManager = params.experimentalMemoryManager ?? false;
|
||||
this.experimentalAgentHistoryTruncation =
|
||||
params.experimentalAgentHistoryTruncation ?? false;
|
||||
this.experimentalAgentHistoryTruncationThreshold =
|
||||
params.experimentalAgentHistoryTruncationThreshold ?? 30;
|
||||
this.experimentalAgentHistoryRetainedMessages =
|
||||
params.experimentalAgentHistoryRetainedMessages ?? 15;
|
||||
this.experimentalAgentHistorySummarization =
|
||||
params.experimentalAgentHistorySummarization ?? false;
|
||||
this.experimentalWatcher = params.experimentalWatcher ?? false;
|
||||
this.experimentalWatcherInterval = params.experimentalWatcherInterval ?? 20;
|
||||
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
|
||||
@@ -2452,6 +2440,14 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.experimentalMemoryManager;
|
||||
}
|
||||
|
||||
isExperimentalWatcherEnabled(): boolean {
|
||||
return this.experimentalWatcher;
|
||||
}
|
||||
|
||||
getExperimentalWatcherInterval(): number {
|
||||
return this.experimentalWatcherInterval;
|
||||
}
|
||||
|
||||
getContextManagementConfig(): ContextManagementConfig {
|
||||
return this.contextManagement;
|
||||
}
|
||||
@@ -2468,30 +2464,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
};
|
||||
}
|
||||
|
||||
isExperimentalAgentHistoryTruncationEnabled(): boolean {
|
||||
return this.experimentalAgentHistoryTruncation;
|
||||
}
|
||||
|
||||
getExperimentalAgentHistoryTruncationThreshold(): number {
|
||||
return this.experimentalAgentHistoryTruncationThreshold;
|
||||
}
|
||||
|
||||
getExperimentalAgentHistoryRetainedMessages(): number {
|
||||
return this.experimentalAgentHistoryRetainedMessages;
|
||||
}
|
||||
|
||||
isExperimentalAgentHistorySummarizationEnabled(): boolean {
|
||||
return this.experimentalAgentHistorySummarization;
|
||||
}
|
||||
|
||||
isExperimentalWatcherEnabled(): boolean {
|
||||
return this.experimentalWatcher;
|
||||
}
|
||||
|
||||
getExperimentalWatcherInterval(): number {
|
||||
return this.experimentalWatcherInterval;
|
||||
}
|
||||
|
||||
isTopicUpdateNarrationEnabled(): boolean {
|
||||
return this.topicUpdateNarration;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import type { ContentGenerator } from './contentGenerator.js';
|
||||
import { LoopDetectionService } from '../services/loopDetectionService.js';
|
||||
import { ChatCompressionService } from '../context/chatCompressionService.js';
|
||||
import { AgentHistoryProvider } from '../context/agentHistoryProvider.js';
|
||||
import type { WatcherProgress } from '../agents/types.js';
|
||||
import { isSubagentProgress, type WatcherProgress } from '../agents/types.js';
|
||||
import { WatcherReportSchema } from '../agents/watcher-agent.js';
|
||||
import { ideContextStore } from '../ide/ideContext.js';
|
||||
import {
|
||||
@@ -1390,10 +1390,13 @@ export class GeminiClient {
|
||||
const invocation = watcherTool.build({ recentHistory });
|
||||
const result = await invocation.execute(signal);
|
||||
|
||||
if (result.llmContent) {
|
||||
if (
|
||||
isSubagentProgress(result.returnDisplay) &&
|
||||
result.returnDisplay.result
|
||||
) {
|
||||
try {
|
||||
const contentString = partListUnionToString(result.llmContent);
|
||||
const parsed = WatcherReportSchema.parse(JSON.parse(contentString));
|
||||
const rawOutput = result.returnDisplay.result;
|
||||
const parsed = WatcherReportSchema.parse(JSON.parse(rawOutput));
|
||||
|
||||
// Internally write the status report to avoid requiring user permission
|
||||
const projectTempDir = this.config.storage.getProjectTempDir();
|
||||
|
||||
@@ -11,7 +11,6 @@ import { GeminiClient } from './client.js';
|
||||
import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import { makeFakeConfig } from '../test-utils/config.js';
|
||||
import { ApprovalMode } from '../policy/types.js';
|
||||
import type { WatcherProgress } from '../agents/types.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
|
||||
describe('GeminiClient Watcher Integration', () => {
|
||||
@@ -57,25 +56,17 @@ describe('GeminiClient Watcher Integration', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should trigger watcher periodically when enabled', async () => {
|
||||
vi.spyOn(config, 'isExperimentalWatcherEnabled').mockReturnValue(true);
|
||||
vi.spyOn(config, 'getExperimentalWatcherInterval').mockReturnValue(2);
|
||||
vi.spyOn(config, 'getApprovalMode').mockReturnValue(ApprovalMode.DEFAULT);
|
||||
|
||||
// Mock toolRegistry before initialize calls startChat
|
||||
const mockWatcherTool = {
|
||||
const createMockWatcherTool = (resultData: unknown) => ({
|
||||
build: vi.fn().mockReturnValue({
|
||||
execute: vi.fn().mockResolvedValue({
|
||||
llmContent: [
|
||||
{
|
||||
text: JSON.stringify({
|
||||
userDirections: 'Keep testing',
|
||||
progressSummary: 'Test in progress',
|
||||
evaluation: 'Good',
|
||||
feedback: 'Keep going',
|
||||
} as WatcherProgress),
|
||||
},
|
||||
],
|
||||
llmContent: [{ text: 'Subagent finished' }],
|
||||
returnDisplay: {
|
||||
isSubagentProgress: true,
|
||||
agentName: 'watcher',
|
||||
recentActivity: [],
|
||||
state: 'completed',
|
||||
result: resultData ? JSON.stringify(resultData) : undefined,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
name: 'watcher',
|
||||
@@ -88,7 +79,19 @@ describe('GeminiClient Watcher Integration', () => {
|
||||
outputName: 'report',
|
||||
schema: {},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should trigger watcher periodically when enabled', async () => {
|
||||
vi.spyOn(config, 'isExperimentalWatcherEnabled').mockReturnValue(true);
|
||||
vi.spyOn(config, 'getExperimentalWatcherInterval').mockReturnValue(2);
|
||||
vi.spyOn(config, 'getApprovalMode').mockReturnValue(ApprovalMode.DEFAULT);
|
||||
|
||||
const mockWatcherTool = createMockWatcherTool({
|
||||
userDirections: 'Keep testing',
|
||||
progressSummary: 'Test in progress',
|
||||
evaluation: 'Good',
|
||||
feedback: 'Keep going',
|
||||
});
|
||||
|
||||
const mockToolRegistry = {
|
||||
getFunctionDeclarations: vi.fn().mockReturnValue([]),
|
||||
@@ -101,7 +104,6 @@ describe('GeminiClient Watcher Integration', () => {
|
||||
discoverAllTools: vi.fn(),
|
||||
};
|
||||
|
||||
// Use type assertion for testing purposes to access protected members
|
||||
const clientAccess = client as unknown as {
|
||||
context: AgentLoopContext;
|
||||
};
|
||||
@@ -130,7 +132,7 @@ describe('GeminiClient Watcher Integration', () => {
|
||||
promptId,
|
||||
);
|
||||
for await (const _ of generator) {
|
||||
// Intentionally consume
|
||||
// consume
|
||||
}
|
||||
|
||||
expect(mockWatcherTool.build).toHaveBeenCalled();
|
||||
@@ -141,7 +143,6 @@ describe('GeminiClient Watcher Integration', () => {
|
||||
vi.spyOn(config, 'getExperimentalWatcherInterval').mockReturnValue(2);
|
||||
vi.spyOn(config, 'getApprovalMode').mockReturnValue(ApprovalMode.DEFAULT);
|
||||
|
||||
// Mock toolRegistry before initialize calls startChat
|
||||
const mockWatcherTool = {
|
||||
build: vi.fn(),
|
||||
name: 'watcher',
|
||||
@@ -158,7 +159,6 @@ describe('GeminiClient Watcher Integration', () => {
|
||||
discoverAllTools: vi.fn(),
|
||||
};
|
||||
|
||||
// Use type assertion for testing purposes to access protected members
|
||||
const clientAccess = client as unknown as {
|
||||
context: AgentLoopContext;
|
||||
};
|
||||
@@ -187,7 +187,7 @@ describe('GeminiClient Watcher Integration', () => {
|
||||
promptId,
|
||||
);
|
||||
for await (const _ of generator) {
|
||||
// Intentionally consume
|
||||
// consume
|
||||
}
|
||||
|
||||
expect(mockWatcherTool.build).not.toHaveBeenCalled();
|
||||
@@ -201,35 +201,12 @@ describe('GeminiClient Watcher Integration', () => {
|
||||
);
|
||||
vi.spyOn(config, 'getApprovalMode').mockReturnValue(ApprovalMode.DEFAULT);
|
||||
|
||||
const mockWatcherTool = {
|
||||
build: vi.fn().mockReturnValue({
|
||||
execute: vi.fn().mockResolvedValue({
|
||||
llmContent: [
|
||||
{
|
||||
text: JSON.stringify({
|
||||
userDirections: 'Keep testing',
|
||||
progressSummary: 'Test in progress',
|
||||
evaluation: 'Good',
|
||||
feedback: 'Keep going',
|
||||
} as WatcherProgress),
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
name: 'watcher',
|
||||
displayName: 'Watcher',
|
||||
description: 'Watcher tool',
|
||||
inputConfig: {
|
||||
inputName: 'history',
|
||||
description: 'history',
|
||||
schema: {},
|
||||
},
|
||||
outputConfig: {
|
||||
outputName: 'report',
|
||||
description: 'report',
|
||||
schema: {},
|
||||
},
|
||||
};
|
||||
const mockWatcherTool = createMockWatcherTool({
|
||||
userDirections: 'Keep testing',
|
||||
progressSummary: 'Test in progress',
|
||||
evaluation: 'Good',
|
||||
feedback: 'Keep going',
|
||||
});
|
||||
|
||||
const mockToolRegistry = {
|
||||
getFunctionDeclarations: vi.fn().mockReturnValue([]),
|
||||
@@ -242,7 +219,6 @@ describe('GeminiClient Watcher Integration', () => {
|
||||
discoverAllTools: vi.fn(),
|
||||
};
|
||||
|
||||
// Use type assertion for testing purposes to access protected members
|
||||
const clientAccess = client as unknown as {
|
||||
context: AgentLoopContext;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user