Skip to content

Commit 5e71f83

Browse files
committed
Merge remote-tracking branch 'upstream/main' into main-api-integration
2 parents 76442f6 + 83d7567 commit 5e71f83

11 files changed

Lines changed: 427 additions & 52 deletions

File tree

.github/workflows/release-nightly.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
release:
4040
if: "github.repository == 'google-gemini/gemini-cli'"
4141
needs: ['build-mac']
42-
environment: "${{ github.event.inputs.environment || 'prod' }}"
42+
environment: "${{ github.event_name == 'schedule' && 'internal' || github.event.inputs.environment || 'prod' }}"
4343
runs-on: 'ubuntu-latest'
4444
permissions:
4545
contents: 'write'

packages/a2a-server/src/agent/executor.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ vi.mock('../config/config.js', () => ({
2222
getCheckpointingEnabled: () => false,
2323
}),
2424
loadEnvironment: vi.fn(),
25+
setIsTrusted: vi.fn().mockReturnValue(false),
2526
setTargetDir: vi.fn().mockReturnValue('/tmp'),
2627
}));
2728

@@ -62,6 +63,12 @@ vi.mock('./task.js', () => {
6263
scheduleToolCalls: vi.fn().mockResolvedValue(undefined),
6364
waitForPendingTools: vi.fn().mockResolvedValue(undefined),
6465
getAndClearCompletedTools: vi.fn().mockReturnValue([]),
66+
get hasPendingTools() {
67+
return false;
68+
},
69+
get pendingToolsCount() {
70+
return 0;
71+
},
6572
addToolResponsesToHistory: vi.fn(),
6673
sendCompletedToolsToLlm: vi.fn().mockImplementation(async function* () {}),
6774
cancelPendingTools: vi.fn(),
@@ -245,4 +252,52 @@ describe('CoderAgentExecutor', () => {
245252
expect(executor.getTask(taskId)).toBeUndefined();
246253
expect(wrapper.task.dispose).toHaveBeenCalled();
247254
});
255+
256+
it('should yield the turn and transition to input-required if tools are pending', async () => {
257+
const taskId = 'test-task-pending-tools';
258+
const contextId = 'test-context';
259+
260+
const mockSocket = new EventEmitter();
261+
(requestStorage.getStore as Mock).mockReturnValue({
262+
req: { socket: mockSocket },
263+
});
264+
265+
// Pre-create the task to safely modify its mocked methods before execution
266+
const wrapper = await executor.createTask(
267+
taskId,
268+
contextId,
269+
undefined,
270+
mockEventBus,
271+
);
272+
const hasPendingToolsSpy = vi
273+
.spyOn(wrapper.task, 'hasPendingTools', 'get')
274+
.mockReturnValue(true);
275+
vi.spyOn(wrapper.task, 'pendingToolsCount', 'get').mockReturnValue(1);
276+
277+
const requestContext = {
278+
userMessage: {
279+
messageId: 'msg-1',
280+
taskId,
281+
contextId,
282+
parts: [{ kind: 'confirmation', callId: '1', outcome: 'proceed' }],
283+
metadata: {
284+
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
285+
},
286+
},
287+
} as unknown as RequestContext;
288+
289+
await executor.execute(requestContext, mockEventBus);
290+
291+
// Assert that the executor yielded the turn correctly without further progression
292+
expect(hasPendingToolsSpy).toHaveBeenCalled();
293+
expect(wrapper.task.getAndClearCompletedTools).not.toHaveBeenCalled();
294+
expect(wrapper.task.sendCompletedToolsToLlm).not.toHaveBeenCalled();
295+
expect(wrapper.task.setTaskStateAndPublishUpdate).toHaveBeenCalledWith(
296+
'input-required',
297+
expect.any(Object),
298+
undefined,
299+
undefined,
300+
true,
301+
);
302+
});
248303
});

packages/a2a-server/src/agent/executor.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ import {
3131
getContextIdFromMetadata,
3232
getAgentSettingsFromMetadata,
3333
} from '../types.js';
34-
import { loadConfig, loadEnvironment, setTargetDir } from '../config/config.js';
34+
import {
35+
loadConfig,
36+
loadEnvironment,
37+
setIsTrusted,
38+
setTargetDir,
39+
} from '../config/config.js';
3540
import { loadSettings } from '../config/settings.js';
3641
import { loadExtensions } from '../config/extension.js';
3742
import { Task } from './task.js';
@@ -93,8 +98,8 @@ export class CoderAgentExecutor implements AgentExecutor {
9398
taskId: string,
9499
): Promise<Config> {
95100
const workspaceRoot = setTargetDir(agentSettings);
96-
const isTrusted = agentSettings.isTrusted ?? false;
97101
loadEnvironment(); // Will override any global env with workspace envs
102+
const isTrusted = setIsTrusted(agentSettings);
98103
const settings = loadSettings(workspaceRoot, isTrusted);
99104
const extensions = loadExtensions(workspaceRoot);
100105
return loadConfig(
@@ -541,42 +546,49 @@ export class CoderAgentExecutor implements AgentExecutor {
541546

542547
if (abortSignal.aborted) throw new Error('Execution aborted');
543548

544-
const completedTools = currentTask.getAndClearCompletedTools();
545-
546-
if (completedTools.length > 0) {
547-
// If all completed tool calls were canceled, manually add them to history and set state to input-required, final:true
548-
if (completedTools.every((tool) => tool.status === 'cancelled')) {
549-
logger.info(
550-
`[CoderAgentExecutor] Task ${taskId}: All tool calls were cancelled. Updating history and ending agent turn.`,
551-
);
552-
currentTask.addToolResponsesToHistory(completedTools);
553-
agentTurnActive = false;
554-
const stateChange: StateChange = {
555-
kind: CoderAgentEvent.StateChangeEvent,
556-
};
557-
currentTask.setTaskStateAndPublishUpdate(
558-
'input-required',
559-
stateChange,
560-
undefined,
561-
undefined,
562-
true,
563-
);
549+
if (currentTask.hasPendingTools) {
550+
logger.info(
551+
`[CoderAgentExecutor] Task ${taskId}: There are still ${currentTask.pendingToolsCount} pending tools waiting for approval. Yielding to user.`,
552+
);
553+
agentTurnActive = false;
554+
} else {
555+
const completedTools = currentTask.getAndClearCompletedTools();
556+
557+
if (completedTools.length > 0) {
558+
// If all completed tool calls were canceled, manually add them to history and set state to input-required, final:true
559+
if (completedTools.every((tool) => tool.status === 'cancelled')) {
560+
logger.info(
561+
`[CoderAgentExecutor] Task ${taskId}: All tool calls were cancelled. Updating history and ending agent turn.`,
562+
);
563+
currentTask.addToolResponsesToHistory(completedTools);
564+
agentTurnActive = false;
565+
const stateChange: StateChange = {
566+
kind: CoderAgentEvent.StateChangeEvent,
567+
};
568+
currentTask.setTaskStateAndPublishUpdate(
569+
'input-required',
570+
stateChange,
571+
undefined,
572+
undefined,
573+
true,
574+
);
575+
} else {
576+
logger.info(
577+
`[CoderAgentExecutor] Task ${taskId}: Found ${completedTools.length} completed tool calls. Sending results back to LLM.`,
578+
);
579+
580+
agentEvents = currentTask.sendCompletedToolsToLlm(
581+
completedTools,
582+
abortSignal,
583+
);
584+
// Continue the loop to process the LLM response to the tool results.
585+
}
564586
} else {
565587
logger.info(
566-
`[CoderAgentExecutor] Task ${taskId}: Found ${completedTools.length} completed tool calls. Sending results back to LLM.`,
567-
);
568-
569-
agentEvents = currentTask.sendCompletedToolsToLlm(
570-
completedTools,
571-
abortSignal,
588+
`[CoderAgentExecutor] Task ${taskId}: No more tool calls to process. Ending agent turn.`,
572589
);
573-
// Continue the loop to process the LLM response to the tool results.
590+
agentTurnActive = false;
574591
}
575-
} else {
576-
logger.info(
577-
`[CoderAgentExecutor] Task ${taskId}: No more tool calls to process. Ending agent turn.`,
578-
);
579-
agentTurnActive = false;
580592
}
581593
}
582594

packages/a2a-server/src/agent/task.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,35 @@ describe('Task', () => {
631631

632632
expect(handleEventDrivenToolCallSpy).toHaveBeenCalled();
633633
});
634+
635+
describe('Pending Tools state', () => {
636+
it('should correctly report pending tools presence and count', () => {
637+
const mockConfig = createMockConfig();
638+
const mockEventBus: ExecutionEventBus = {
639+
publish: vi.fn(),
640+
on: vi.fn(),
641+
off: vi.fn(),
642+
once: vi.fn(),
643+
removeAllListeners: vi.fn(),
644+
finished: vi.fn(),
645+
};
646+
647+
// @ts-expect-error - Calling private constructor
648+
const task = new Task(
649+
'task-id',
650+
'context-id',
651+
mockConfig as Config,
652+
mockEventBus,
653+
);
654+
655+
expect(task.hasPendingTools).toBe(false);
656+
expect(task.pendingToolsCount).toBe(0);
657+
658+
task['_registerToolCall']('tool-1', 'scheduled');
659+
expect(task.hasPendingTools).toBe(true);
660+
expect(task.pendingToolsCount).toBe(1);
661+
});
662+
});
634663
});
635664

636665
describe('Serialization and Mapping', () => {

packages/a2a-server/src/agent/task.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ export class Task {
137137
);
138138
}
139139

140+
get hasPendingTools(): boolean {
141+
return this.pendingToolCalls.size > 0;
142+
}
143+
144+
get pendingToolsCount(): number {
145+
return this.pendingToolCalls.size;
146+
}
147+
140148
static async create(
141149
id: string,
142150
contextId: string,

packages/a2a-server/src/config/config.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
PRIORITY_YOLO_ALLOW_ALL,
2424
createPolicyEngineConfig,
2525
} from '@google/gemini-cli-core';
26+
import type { AgentSettings } from '../types.js';
2627

2728
// Mock dependencies
2829
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
@@ -612,3 +613,34 @@ describe('loadConfig', () => {
612613
});
613614
});
614615
});
616+
617+
describe('setIsTrusted', () => {
618+
beforeEach(() => {
619+
vi.resetModules();
620+
});
621+
622+
afterEach(() => {
623+
vi.unstubAllEnvs();
624+
});
625+
626+
it('should return true when GEMINI_FOLDER_TRUST env var is true', async () => {
627+
vi.stubEnv('GEMINI_FOLDER_TRUST', 'true');
628+
const { setIsTrusted } = await import('./config.js');
629+
expect(setIsTrusted(undefined)).toBe(true);
630+
expect(setIsTrusted({ isTrusted: false } as AgentSettings)).toBe(true);
631+
});
632+
633+
it('should return false when GEMINI_FOLDER_TRUST env var is false', async () => {
634+
vi.stubEnv('GEMINI_FOLDER_TRUST', 'false');
635+
const { setIsTrusted } = await import('./config.js');
636+
expect(setIsTrusted(undefined)).toBe(false);
637+
expect(setIsTrusted({ isTrusted: true } as AgentSettings)).toBe(false);
638+
});
639+
640+
it('should fallback to agentSettings.isTrusted if env var is undefined', async () => {
641+
const { setIsTrusted } = await import('./config.js');
642+
expect(setIsTrusted({ isTrusted: true } as AgentSettings)).toBe(true);
643+
expect(setIsTrusted({ isTrusted: false } as AgentSettings)).toBe(false);
644+
expect(setIsTrusted(undefined)).toBe(false);
645+
});
646+
});

packages/a2a-server/src/config/config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { logger } from '../utils/logger.js';
3434
import type { Settings } from './settings.js';
3535
import { type AgentSettings, CoderAgentEvent } from '../types.js';
3636

37+
const INITIAL_FOLDER_TRUST = process.env['GEMINI_FOLDER_TRUST'];
38+
3739
export async function loadConfig(
3840
settings: Settings,
3941
extensionLoader: ExtensionLoader,
@@ -182,6 +184,15 @@ export async function loadConfig(
182184
return config;
183185
}
184186

187+
export function setIsTrusted(
188+
agentSettings: AgentSettings | undefined,
189+
): boolean {
190+
if (INITIAL_FOLDER_TRUST !== undefined) {
191+
return INITIAL_FOLDER_TRUST === 'true';
192+
}
193+
return !!agentSettings?.isTrusted;
194+
}
195+
185196
export function setTargetDir(agentSettings: AgentSettings | undefined): string {
186197
const originalCWD = process.cwd();
187198
const targetDir =

0 commit comments

Comments
 (0)