Compare commits

...

12 Commits

Author SHA1 Message Date
gemini-cli-robot b0c7a1722a chore(release): v0.41.2 2026-05-06 17:56:24 +00:00
gemini-cli-robot 0da6569b62 fix(patch): cherry-pick 02995ba to release/v0.41.1-pr-26568 to patch version v0.41.1 and create version 0.41.2 (#26589)
Co-authored-by: Keith Schaab <keith.schaab@gmail.com>
2026-05-06 10:09:46 -07:00
gemini-cli-robot e1a54496dd chore(release): v0.41.1 2026-05-05 22:39:08 +00:00
gemini-cli-robot 7d5b3584f3 fix(patch): cherry-pick 3627f47 to release/v0.41.0-pr-26542 to patch version v0.41.0 and create version 0.41.1 (#26545)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
2026-05-05 21:57:00 +00:00
gemini-cli-robot 03fd454b31 chore(release): v0.41.0 2026-05-05 20:40:56 +00:00
gemini-cli-robot 6339fd95fa chore(release): v0.41.0-preview.3 2026-05-05 19:37:08 +00:00
gemini-cli-robot c0c1df12f9 fix(patch): cherry-pick 7cc19c2 to release/v0.41.0-preview.2-pr-26507 to patch version v0.41.0-preview.2 and create version 0.41.0-preview.3 (#26530)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-05-05 11:55:46 -07:00
gemini-cli-robot 3e31372710 chore(release): v0.41.0-preview.2 2026-05-05 18:43:45 +00:00
gemini-cli-robot c6a695d1b5 fix(patch): cherry-pick 1d72a12 to release/v0.41.0-preview.1-pr-26479 to patch version v0.41.0-preview.1 and create version 0.41.0-preview.2 (#26508)
Co-authored-by: Keith Schaab <keith.schaab@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-05-05 10:58:59 -07:00
gemini-cli-robot 78f3d705dd chore(release): v0.41.0-preview.1 2026-04-30 21:04:16 +00:00
gemini-cli-robot 7d233ddd5f fix(patch): cherry-pick 2194da2 to release/v0.41.0-preview.0-pr-26153 to patch version v0.41.0-preview.0 and create version 0.41.0-preview.1 (#26269)
Co-authored-by: lp-peg <35035802+lp-peg@users.noreply.github.com>
Co-authored-by: David Pierce <davidapierce@google.com>
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
2026-04-30 10:32:41 -07:00
gemini-cli-robot a5befa04f5 chore(release): v0.41.0-preview.0 2026-04-28 18:57:17 +00:00
24 changed files with 1240 additions and 176 deletions
+9 -9
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"workspaces": [
"packages/*"
],
@@ -18077,7 +18077,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
"@google-cloud/storage": "^7.16.0",
@@ -18206,7 +18206,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
@@ -18354,7 +18354,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
@@ -18665,7 +18665,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -18680,7 +18680,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18711,7 +18711,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18743,7 +18743,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.0-nightly.20260423.gaa05b4583"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.2"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -0,0 +1,173 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { Task } from './task.js';
import {
MessageBusType,
CoreToolCallStatus,
type Config,
type MessageBus,
} from '@google/gemini-cli-core';
import { createMockConfig } from '../utils/testing_utils.js';
import type { RequestContext } from '@a2a-js/sdk/server';
describe('Task Race Condition', () => {
let mockConfig: Config;
let messageBus: MessageBus;
beforeEach(() => {
messageBus = {
subscribe: vi.fn(),
unsubscribe: vi.fn(),
publish: vi.fn(),
} as unknown as MessageBus;
mockConfig = createMockConfig({
messageBus,
}) as Config;
});
it('should not hang when multiple tool confirmations are processed while waiting', async () => {
// @ts-expect-error - private constructor
const task = new Task('task-id', 'context-id', mockConfig);
// 1. Register two tools as scheduled
task['_registerToolCall']('tool-1', 'scheduled');
task['_registerToolCall']('tool-2', 'scheduled');
// 2. Both transition to awaiting_approval
const updateHandler = (messageBus.subscribe as Mock).mock.calls.find(
(c: unknown[]) => c[0] === MessageBusType.TOOL_CALLS_UPDATE,
)?.[1];
updateHandler({
type: MessageBusType.TOOL_CALLS_UPDATE,
schedulerId: 'task-id',
toolCalls: [
{
request: { callId: 'tool-1', name: 't1' },
status: CoreToolCallStatus.AwaitingApproval,
correlationId: 'corr-1',
confirmationDetails: { type: 'info' },
},
{
request: { callId: 'tool-2', name: 't2' },
status: CoreToolCallStatus.AwaitingApproval,
correlationId: 'corr-2',
confirmationDetails: { type: 'info' },
},
],
});
// 3. Confirm Tool 1. This makes isAwaitingApprovalOnly() return false.
for await (const _ of task.acceptUserMessage(
{
userMessage: {
parts: [
{
kind: 'data',
data: { callId: 'tool-1', outcome: 'proceed_once' },
},
],
},
} as unknown as RequestContext,
new AbortController().signal,
)) {
// consume generator
}
// 4. Start waiting. This should now block because Tool 1 is confirmed (so we are waiting for its execution).
const waitPromise = task.waitForPendingTools();
// 5. Confirm Tool 2 while waiting.
for await (const _ of task.acceptUserMessage(
{
userMessage: {
parts: [
{
kind: 'data',
data: { callId: 'tool-2', outcome: 'proceed_once' },
},
],
},
} as unknown as RequestContext,
new AbortController().signal,
)) {
// consume generator
}
// 6. Both tools complete successfully
updateHandler({
type: MessageBusType.TOOL_CALLS_UPDATE,
schedulerId: 'task-id',
toolCalls: [
{
request: { callId: 'tool-1', name: 't1' },
status: CoreToolCallStatus.Success,
response: { responseParts: [] },
},
{
request: { callId: 'tool-2', name: 't2' },
status: CoreToolCallStatus.Success,
response: { responseParts: [] },
},
],
});
// 7. Verify that the original waitPromise resolves.
await expect(waitPromise).resolves.toBeUndefined();
});
it('should reject waitForPendingTools when tools are cancelled', async () => {
// @ts-expect-error - private constructor
const task = new Task('task-id', 'context-id', mockConfig);
// 1. Register a tool
task['_registerToolCall']('tool-1', 'scheduled');
// 2. Start waiting
const waitPromise = task.waitForPendingTools();
// 3. Cancel pending tools
task.cancelPendingTools('User requested cancellation');
// 4. Verify waitPromise rejects with the reason
await expect(waitPromise).rejects.toThrow('User requested cancellation');
});
it('should handle concurrent tool scheduling correctly', async () => {
// @ts-expect-error - private constructor
const task = new Task('task-id', 'context-id', mockConfig);
// 1. Register a tool and start waiting
task['_registerToolCall']('tool-1', 'scheduled');
const waitPromise = task.waitForPendingTools();
// 2. Schedule another tool concurrently (e.g. from a secondary user message)
// This should NOT resolve the current waitPromise until both are done
await task.scheduleToolCalls(
[{ callId: 'tool-2', name: 't2', args: {} }],
new AbortController().signal,
);
expect(task['pendingToolCalls'].size).toBe(2);
// 3. Resolve tool 1
task['_resolveToolCall']('tool-1');
// 4. Verify waitPromise is still pending
let resolved = false;
waitPromise.then(() => (resolved = true));
await new Promise((resolve) => setTimeout(resolve, 10));
expect(resolved).toBe(false);
// 5. Resolve tool 2
task['_resolveToolCall']('tool-2');
// 6. Now it should resolve
await expect(waitPromise).resolves.toBeUndefined();
});
});
@@ -12,6 +12,7 @@ import {
ApprovalMode,
Scheduler,
type MessageBus,
type ToolLiveOutput,
} from '@google/gemini-cli-core';
import { createMockConfig } from '../utils/testing_utils.js';
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
@@ -66,6 +67,7 @@ describe('Task Event-Driven Scheduler', () => {
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
expect(mockEventBus.publish).toHaveBeenCalledWith(
@@ -106,6 +108,7 @@ describe('Task Event-Driven Scheduler', () => {
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
// Simulate A2A client confirmation
@@ -148,7 +151,11 @@ describe('Task Event-Driven Scheduler', () => {
const handler = (messageBus.subscribe as Mock).mock.calls.find(
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
)?.[1];
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
// Simulate Rejection (Cancel)
const handled = await (
@@ -174,7 +181,11 @@ describe('Task Event-Driven Scheduler', () => {
correlationId: 'corr-2',
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
};
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall2] });
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall2],
schedulerId: 'task-id',
});
// Simulate ModifyWithEditor
const handled2 = await (
@@ -215,7 +226,11 @@ describe('Task Event-Driven Scheduler', () => {
const handler = (messageBus.subscribe as Mock).mock.calls.find(
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
)?.[1];
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
// Simulate ProceedOnce for MCP
const handled = await (
@@ -255,7 +270,11 @@ describe('Task Event-Driven Scheduler', () => {
const handler = (messageBus.subscribe as Mock).mock.calls.find(
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
)?.[1];
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
const handled = await (
task as unknown as {
@@ -294,7 +313,11 @@ describe('Task Event-Driven Scheduler', () => {
const handler = (messageBus.subscribe as Mock).mock.calls.find(
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
)?.[1];
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
const handled = await (
task as unknown as {
@@ -333,7 +356,11 @@ describe('Task Event-Driven Scheduler', () => {
const handler = (messageBus.subscribe as Mock).mock.calls.find(
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
)?.[1];
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
const handled = await (
task as unknown as {
@@ -376,7 +403,11 @@ describe('Task Event-Driven Scheduler', () => {
const handler = (yoloMessageBus.subscribe as Mock).mock.calls.find(
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
)?.[1];
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
// Should NOT auto-publish ProceedOnce anymore, because PolicyEngine handles it directly
expect(yoloMessageBus.publish).not.toHaveBeenCalledWith(
@@ -419,6 +450,7 @@ describe('Task Event-Driven Scheduler', () => {
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
// Should publish artifact update for output
@@ -453,7 +485,11 @@ describe('Task Event-Driven Scheduler', () => {
const handler = (messageBus.subscribe as Mock).mock.calls.find(
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
)?.[1];
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall],
schedulerId: 'task-id',
});
// The tool should be complete and registered appropriately, eventually
// triggering the toolCompletionPromise resolution when all clear.
@@ -533,6 +569,7 @@ describe('Task Event-Driven Scheduler', () => {
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall1, toolCall2],
schedulerId: 'task-id',
});
// Confirm first tool call
@@ -572,6 +609,74 @@ describe('Task Event-Driven Scheduler', () => {
);
});
it('should handle multi-turn tool resolution correctly', async () => {
// @ts-expect-error - Calling private constructor
const task = new Task('task-id', 'context-id', mockConfig);
task['_registerToolCall']('1', 'scheduled');
task['_registerToolCall']('2', 'scheduled');
const handler = (messageBus.subscribe as Mock).mock.calls.find(
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
)?.[1];
// Turn 1: Resolve tool 1
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [
{
request: { callId: '1', name: 't1' },
status: 'success',
response: { responseParts: [] },
},
],
schedulerId: 'task-id',
});
expect(task['pendingToolCalls'].size).toBe(1);
expect(task['pendingToolCalls'].has('2')).toBe(true);
// Turn 2: Resolve tool 2
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [
{
request: { callId: '2', name: 't2' },
status: 'success',
response: { responseParts: [] },
},
],
schedulerId: 'task-id',
});
expect(task['pendingToolCalls'].size).toBe(0);
});
it('should handle subagent progress events from the scheduler', async () => {
// @ts-expect-error - Calling private constructor
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
// Trigger _schedulerOutputUpdate with subagent progress
task['_schedulerOutputUpdate']('tool-1', {
isSubagentProgress: true,
agentName: 'researcher',
recentActivity: [],
} as ToolLiveOutput);
expect(mockEventBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'artifact-update',
artifact: expect.objectContaining({
parts: [
expect.objectContaining({
text: expect.stringContaining('researcher'),
}),
],
}),
}),
);
});
it('should wait for executing tools before transitioning to input-required state', async () => {
// @ts-expect-error - Calling private constructor
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
@@ -600,6 +705,7 @@ describe('Task Event-Driven Scheduler', () => {
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall1, toolCall2],
schedulerId: 'task-id',
});
// Should NOT transition to input-required yet
@@ -621,6 +727,7 @@ describe('Task Event-Driven Scheduler', () => {
handler({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [toolCall1Complete, toolCall2],
schedulerId: 'task-id',
});
// Now it should transition
+203
View File
@@ -12,6 +12,9 @@ import {
type ToolCallRequestInfo,
type GitService,
type CompletedToolCall,
type ToolCall,
type ToolCallsUpdateMessage,
MessageBusType,
} from '@google/gemini-cli-core';
import { createMockConfig } from '../utils/testing_utils.js';
import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
@@ -460,4 +463,204 @@ describe('Task', () => {
expect(task.currentPromptId).toBe(expectedPromptId2);
});
});
describe('Race Condition Fix', () => {
const mockConfig = createMockConfig();
const mockEventBus: ExecutionEventBus = {
publish: vi.fn(),
on: vi.fn(),
off: vi.fn(),
once: vi.fn(),
removeAllListeners: vi.fn(),
finished: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
it('should NOT transition to input-required if a tool is still validating', async () => {
// @ts-expect-error - Calling private constructor
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
// Manually register two tool calls
task['_registerToolCall']('tool-1', 'awaiting_approval');
task['_registerToolCall']('tool-2', 'validating');
// Call checkInputRequiredState (private)
task['checkInputRequiredState']();
// Verify task state did NOT change to input-required
expect(task.taskState).not.toBe('input-required');
expect(mockEventBus.publish).not.toHaveBeenCalledWith(
expect.objectContaining({
status: expect.objectContaining({ state: 'input-required' }),
}),
);
});
it('should transition to input-required if all active tools are awaiting approval', async () => {
// @ts-expect-error - Calling private constructor
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
// Transition from submitted to working first to simulate normal flow
task.taskState = 'working';
// Manually register tool calls
task['_registerToolCall']('tool-1', 'awaiting_approval');
// Call checkInputRequiredState
task['checkInputRequiredState']();
// Verify task state changed to input-required
expect(task.taskState).toBe('input-required');
expect(mockEventBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
status: expect.objectContaining({ state: 'input-required' }),
}),
);
});
it('handleEventDrivenToolCallsUpdate should ignore events for other schedulers', async () => {
// @ts-expect-error - Calling private constructor
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
const handleEventDrivenToolCallSpy = vi.spyOn(
task as unknown as {
handleEventDrivenToolCall: Task['handleEventDrivenToolCall'];
},
'handleEventDrivenToolCall',
);
const otherEvent: ToolCallsUpdateMessage = {
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [
{ request: { callId: '1' }, status: 'executing' } as ToolCall,
],
schedulerId: 'other-task-id',
};
task['handleEventDrivenToolCallsUpdate'](otherEvent);
expect(handleEventDrivenToolCallSpy).not.toHaveBeenCalled();
const ownEvent: ToolCallsUpdateMessage = {
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [
{ request: { callId: '1' }, status: 'executing' } as ToolCall,
],
schedulerId: 'task-id',
};
task['handleEventDrivenToolCallsUpdate'](ownEvent);
expect(handleEventDrivenToolCallSpy).toHaveBeenCalled();
});
});
describe('Serialization and Mapping', () => {
it('should map internal "validating" status to "scheduled" for the client and include outcome', async () => {
const mockConfig = createMockConfig();
const mockEventBus: ExecutionEventBus = {
publish: vi.fn(),
on: vi.fn(),
off: vi.fn(),
once: vi.fn(),
removeAllListeners: vi.fn(),
finished: vi.fn(),
};
// @ts-expect-error - Calling private constructor
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
const mockToolCall = {
request: { callId: 'tool-1' },
status: 'validating',
outcome: 'accepted',
tool: { name: 'test-tool' },
};
const message = task['toolStatusMessage'](
mockToolCall as unknown as ToolCall,
'task-id',
'context-id',
);
const serialized = (
message.parts![0] as {
data: { status: string; outcome: string };
}
).data;
expect(serialized.status).toBe('scheduled');
expect(serialized.outcome).toBe('accepted');
});
it('should correctly detect changes when status or outcome changes', async () => {
const mockConfig = createMockConfig();
const mockEventBus: ExecutionEventBus = {
publish: vi.fn(),
on: vi.fn(),
off: vi.fn(),
once: vi.fn(),
removeAllListeners: vi.fn(),
finished: vi.fn(),
};
// @ts-expect-error - Calling private constructor
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
const toolCall1 = {
request: { callId: 'tool-1' },
status: 'awaiting_approval',
};
// First update - should trigger change
const changed1 = task['handleEventDrivenToolCall'](
toolCall1 as unknown as ToolCall,
);
expect(changed1).toBe(true);
// Second update with same status - should NOT trigger change
const changed2 = task['handleEventDrivenToolCall'](
toolCall1 as unknown as ToolCall,
);
expect(changed2).toBe(false);
// Update with new outcome - SHOULD trigger change
const toolCall2 = {
request: { callId: 'tool-1' },
status: 'awaiting_approval',
outcome: 'accepted',
};
const changed3 = task['handleEventDrivenToolCall'](
toolCall2 as unknown as ToolCall,
);
expect(changed3).toBe(true);
});
});
});
+74 -49
View File
@@ -11,6 +11,7 @@ import {
GeminiEventType,
ToolConfirmationOutcome,
ApprovalMode,
CoreToolCallStatus,
getAllMCPServerStatuses,
MCPServerStatus,
isNodeError,
@@ -51,6 +52,7 @@ import type {
Artifact,
} from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';
import { EventEmitter } from 'node:events';
import { logger } from '../utils/logger.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
@@ -95,12 +97,11 @@ export class Task {
// For tool waiting logic
private pendingToolCalls: Map<string, string> = new Map(); //toolCallId --> status
private pendingOutcomes: Map<string, ToolConfirmationOutcome | undefined> =
new Map(); // toolCallId --> outcome
private toolsAlreadyConfirmed: Set<string> = new Set();
private toolCompletionPromise?: Promise<void>;
private toolCompletionNotifier?: {
resolve: () => void;
reject: (reason?: Error) => void;
};
private toolUpdateEmitter = new EventEmitter();
private cancellationError?: Error;
private constructor(
id: string,
@@ -121,7 +122,6 @@ export class Task {
this.taskState = 'submitted';
this.eventBus = eventBus;
this.completedToolCalls = [];
this._resetToolCompletionPromise();
this.autoExecute = autoExecute;
this.config.setFallbackModelHandler(
// For a2a-server, we want to automatically switch to the fallback model
@@ -176,22 +176,9 @@ export class Task {
return metadata;
}
private _resetToolCompletionPromise(): void {
this.toolCompletionPromise = new Promise((resolve, reject) => {
this.toolCompletionNotifier = { resolve, reject };
});
// If there are no pending calls when reset, resolve immediately.
if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) {
this.toolCompletionNotifier.resolve();
}
}
private _registerToolCall(toolCallId: string, status: string): void {
const wasEmpty = this.pendingToolCalls.size === 0;
this.pendingToolCalls.set(toolCallId, status);
if (wasEmpty) {
this._resetToolCompletionPromise();
}
this.toolUpdateEmitter.emit('update');
logger.info(
`[Task] Registered tool call: ${toolCallId}. Pending: ${this.pendingToolCalls.size}`,
);
@@ -200,23 +187,47 @@ export class Task {
private _resolveToolCall(toolCallId: string): void {
if (this.pendingToolCalls.has(toolCallId)) {
this.pendingToolCalls.delete(toolCallId);
this.toolUpdateEmitter.emit('update');
logger.info(
`[Task] Resolved tool call: ${toolCallId}. Pending: ${this.pendingToolCalls.size}`,
);
if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) {
this.toolCompletionNotifier.resolve();
}
}
}
async waitForPendingTools(): Promise<void> {
private isAwaitingApprovalOnly(): boolean {
if (this.pendingToolCalls.size === 0) {
return Promise.resolve();
return false;
}
for (const [callId, status] of this.pendingToolCalls.entries()) {
if (
status !== CoreToolCallStatus.AwaitingApproval ||
this.toolsAlreadyConfirmed.has(callId)
) {
return false;
}
}
return true;
}
async waitForPendingTools(): Promise<void> {
while (this.pendingToolCalls.size > 0 && !this.isAwaitingApprovalOnly()) {
if (this.cancellationError) {
const error = this.cancellationError;
this.cancellationError = undefined;
throw error;
}
logger.info(
`[Task] Waiting for ${this.pendingToolCalls.size} pending tool(s)...`,
);
await new Promise((resolve) =>
this.toolUpdateEmitter.once('update', resolve),
);
}
if (this.cancellationError) {
const error = this.cancellationError;
this.cancellationError = undefined;
throw error;
}
logger.info(
`[Task] Waiting for ${this.pendingToolCalls.size} pending tool(s)...`,
);
await this.toolCompletionPromise;
}
cancelPendingTools(reason: string): void {
@@ -225,15 +236,13 @@ export class Task {
`[Task] Cancelling all ${this.pendingToolCalls.size} pending tool calls. Reason: ${reason}`,
);
}
if (this.toolCompletionNotifier) {
this.toolCompletionNotifier.reject(new Error(reason));
}
this.cancellationError = new Error(reason);
this.pendingToolCalls.clear();
this.pendingCorrelationIds.clear();
this.toolsAlreadyConfirmed.clear();
this.scheduler.cancelAll();
// Reset the promise for any future operations, ensuring it's in a clean state.
this._resetToolCompletionPromise();
this.toolUpdateEmitter.emit('update');
}
private _createTextMessage(
@@ -413,7 +422,10 @@ export class Task {
private handleEventDrivenToolCallsUpdate(
event: ToolCallsUpdateMessage,
): void {
if (event.type !== MessageBusType.TOOL_CALLS_UPDATE) {
if (
event.type !== MessageBusType.TOOL_CALLS_UPDATE ||
event.schedulerId !== this.id
) {
return;
}
@@ -426,7 +438,7 @@ export class Task {
this.checkInputRequiredState();
}
private handleEventDrivenToolCall(tc: ToolCall): void {
private handleEventDrivenToolCall(tc: ToolCall): boolean {
const callId = tc.request.callId;
// Do not process events for tools that have already been finalized.
@@ -436,11 +448,16 @@ export class Task {
this.processedToolCallIds.has(callId) ||
this.completedToolCalls.some((c) => c.request.callId === callId)
) {
return;
return false;
}
const previousStatus = this.pendingToolCalls.get(callId);
const hasChanged = previousStatus !== tc.status;
const previousOutcome = this.pendingOutcomes.get(callId);
const hasChanged =
previousStatus !== tc.status || previousOutcome !== tc.outcome;
// Update outcome tracking
this.pendingOutcomes.set(callId, tc.outcome);
// 1. Handle Output
if (tc.status === 'executing' && tc.liveOutput) {
@@ -454,6 +471,7 @@ export class Task {
tc.status === 'cancelled'
) {
this.toolsAlreadyConfirmed.delete(callId);
this.pendingOutcomes.delete(callId);
if (hasChanged) {
logger.info(
`[Task] Tool call ${callId} completed with status: ${tc.status}`,
@@ -496,6 +514,8 @@ export class Task {
);
this.eventBus?.publish(statusUpdate);
}
return hasChanged;
}
private checkInputRequiredState(): void {
@@ -508,12 +528,14 @@ export class Task {
let isExecuting = false;
for (const [callId, status] of this.pendingToolCalls.entries()) {
if (status === 'executing' || status === 'scheduled') {
isExecuting = true;
} else if (
status === 'awaiting_approval' &&
!this.toolsAlreadyConfirmed.has(callId)
if (
status === CoreToolCallStatus.Executing ||
status === CoreToolCallStatus.Scheduled ||
status === CoreToolCallStatus.Validating ||
this.toolsAlreadyConfirmed.has(callId)
) {
isExecuting = true;
} else if (status === CoreToolCallStatus.AwaitingApproval) {
isAwaitingApproval = true;
}
}
@@ -536,8 +558,8 @@ export class Task {
// Unblock waitForPendingTools to correctly end the executor loop and release the HTTP response stream.
// The IDE client will open a new stream with the confirmation reply.
if (!wasAlreadyInputRequired && this.toolCompletionNotifier) {
this.toolCompletionNotifier.resolve();
if (!wasAlreadyInputRequired) {
this.toolUpdateEmitter.emit('update');
}
}
}
@@ -574,8 +596,14 @@ export class Task {
'confirmationDetails',
'liveOutput',
'response',
'outcome',
);
// Map internal 'validating' status to 'scheduled' for the client
if (serializableToolCall.status === CoreToolCallStatus.Validating) {
serializableToolCall.status = CoreToolCallStatus.Scheduled;
}
if (tc.tool) {
const toolFields = this._pickFields(
tc.tool,
@@ -895,6 +923,7 @@ export class Task {
const outcomeString = part.data['outcome'];
this.toolsAlreadyConfirmed.add(callId);
this.toolUpdateEmitter.emit('update');
let confirmationOutcome: ToolConfirmationOutcome | undefined;
@@ -1108,10 +1137,6 @@ export class Task {
if (confirmationHandled) {
anyConfirmationHandled = true;
// If a confirmation was handled, the scheduler will now run the tool (or cancel it).
// We resolve the toolCompletionPromise manually in checkInputRequiredState
// to break the original execution loop, so we must reset it here so the
// new loop correctly awaits the tool's final execution.
this._resetToolCompletionPromise();
// We don't send anything to the LLM for this part.
// The subsequent tool execution will eventually lead to resolveToolCall.
continue;
+79 -37
View File
@@ -228,7 +228,7 @@ describe('E2E Tests', () => {
expect(toolCallUpdateEvent.status.message?.parts).toMatchObject([
{
data: {
status: 'validating',
status: 'scheduled',
request: { callId: 'test-call-id' },
},
},
@@ -330,7 +330,7 @@ describe('E2E Tests', () => {
expect(toolCallValidateEvent1.status.message?.parts).toMatchObject([
{
data: {
status: 'validating',
status: 'scheduled',
request: { callId: 'test-call-id-1' },
},
},
@@ -352,7 +352,7 @@ describe('E2E Tests', () => {
kind: 'state-change',
});
// 4. Tool 1 is validating.
// 4. Tool 1 is scheduled.
const toolCallUpdate1 = events[3].result as TaskStatusUpdateEvent;
expect(toolCallUpdate1.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
@@ -361,12 +361,12 @@ describe('E2E Tests', () => {
{
data: {
request: { callId: 'test-call-id-1' },
status: 'validating',
status: 'scheduled',
},
},
]);
// 5. Tool 2 is validating.
// 5. Tool 2 is scheduled.
const toolCallUpdate2 = events[4].result as TaskStatusUpdateEvent;
expect(toolCallUpdate2.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
@@ -375,17 +375,17 @@ describe('E2E Tests', () => {
{
data: {
request: { callId: 'test-call-id-2' },
status: 'validating',
status: 'scheduled',
},
},
]);
// 6. Tool 1 is awaiting approval.
const toolCallAwaitEvent = events[5].result as TaskStatusUpdateEvent;
expect(toolCallAwaitEvent.metadata?.['coderAgent']).toMatchObject({
const toolCallAwaitEvent1 = events[5].result as TaskStatusUpdateEvent;
expect(toolCallAwaitEvent1.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-confirmation',
});
expect(toolCallAwaitEvent.status.message?.parts).toMatchObject([
expect(toolCallAwaitEvent1.status.message?.parts).toMatchObject([
{
data: {
request: { callId: 'test-call-id-1' },
@@ -394,14 +394,28 @@ describe('E2E Tests', () => {
},
]);
// 7. The final event is "input-required".
const finalEvent = events[6].result as TaskStatusUpdateEvent;
// 7. Tool 2 is awaiting approval.
const toolCallAwaitEvent2 = events[6].result as TaskStatusUpdateEvent;
expect(toolCallAwaitEvent2.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-confirmation',
});
expect(toolCallAwaitEvent2.status.message?.parts).toMatchObject([
{
data: {
request: { callId: 'test-call-id-2' },
status: 'awaiting_approval',
},
},
]);
// 8. The final event is "input-required".
const finalEvent = events[7].result as TaskStatusUpdateEvent;
expect(finalEvent.final).toBe(true);
expect(finalEvent.status.state).toBe('input-required');
// The scheduler now waits for approval, so no more events are sent.
assertUniqueFinalEventIsLast(events);
expect(events.length).toBe(7);
expect(events.length).toBe(8);
});
it('should handle multiple tool calls sequentially in YOLO mode', async () => {
@@ -499,7 +513,7 @@ describe('E2E Tests', () => {
// Tool 1 Lifecycle
{
kind: 'tool-call-update',
status: 'validating',
status: 'scheduled',
callId: 'test-call-id-1',
},
{
@@ -520,7 +534,7 @@ describe('E2E Tests', () => {
// Tool 2 Lifecycle
{
kind: 'tool-call-update',
status: 'validating',
status: 'scheduled',
callId: 'test-call-id-2',
},
{
@@ -603,26 +617,40 @@ describe('E2E Tests', () => {
expect(workingEvent2.kind).toBe('status-update');
expect(workingEvent2.status.state).toBe('working');
// Status update: tool-call-update (validating)
const validatingEvent = events[3].result as TaskStatusUpdateEvent;
expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({
// Status update: tool-call-update (scheduled)
const scheduledEvent1 = events[3].result as TaskStatusUpdateEvent;
expect(scheduledEvent1.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
});
expect(validatingEvent.status.message?.parts).toMatchObject([
expect(scheduledEvent1.status.message?.parts).toMatchObject([
{
data: {
status: 'validating',
status: 'scheduled',
request: { callId: 'test-call-id-no-approval' },
},
},
]);
// Status update: tool-call-update (scheduled)
const scheduledEvent = events[4].result as TaskStatusUpdateEvent;
expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({
const scheduledEvent2 = events[4].result as TaskStatusUpdateEvent;
expect(scheduledEvent2.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
});
expect(scheduledEvent.status.message?.parts).toMatchObject([
expect(scheduledEvent2.status.message?.parts).toMatchObject([
{
data: {
status: 'scheduled',
request: { callId: 'test-call-id-no-approval' },
},
},
]);
// Status update: tool-call-update (scheduled)
const scheduledEvent3 = events[5].result as TaskStatusUpdateEvent;
expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
});
expect(scheduledEvent3.status.message?.parts).toMatchObject([
{
data: {
status: 'scheduled',
@@ -632,7 +660,7 @@ describe('E2E Tests', () => {
]);
// Status update: tool-call-update (executing)
const executingEvent = events[5].result as TaskStatusUpdateEvent;
const executingEvent = events[6].result as TaskStatusUpdateEvent;
expect(executingEvent.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
});
@@ -646,7 +674,7 @@ describe('E2E Tests', () => {
]);
// Status update: tool-call-update (success)
const successEvent = events[6].result as TaskStatusUpdateEvent;
const successEvent = events[7].result as TaskStatusUpdateEvent;
expect(successEvent.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
});
@@ -660,12 +688,12 @@ describe('E2E Tests', () => {
]);
// Status update: working (before sending tool result to LLM)
const workingEvent3 = events[7].result as TaskStatusUpdateEvent;
const workingEvent3 = events[8].result as TaskStatusUpdateEvent;
expect(workingEvent3.kind).toBe('status-update');
expect(workingEvent3.status.state).toBe('working');
// Status update: text-content (final LLM response)
const textContentEvent = events[8].result as TaskStatusUpdateEvent;
const textContentEvent = events[9].result as TaskStatusUpdateEvent;
expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({
kind: 'text-content',
});
@@ -674,7 +702,7 @@ describe('E2E Tests', () => {
]);
assertUniqueFinalEventIsLast(events);
expect(events.length).toBe(10);
expect(events.length).toBe(11);
});
it('should bypass tool approval in YOLO mode', async () => {
@@ -734,15 +762,15 @@ describe('E2E Tests', () => {
expect(workingEvent2.kind).toBe('status-update');
expect(workingEvent2.status.state).toBe('working');
// Status update: tool-call-update (validating)
const validatingEvent = events[3].result as TaskStatusUpdateEvent;
expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({
// Status update: tool-call-update (scheduled)
const scheduledEvent = events[3].result as TaskStatusUpdateEvent;
expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
});
expect(validatingEvent.status.message?.parts).toMatchObject([
expect(scheduledEvent.status.message?.parts).toMatchObject([
{
data: {
status: 'validating',
status: 'scheduled',
request: { callId: 'test-call-id-yolo' },
},
},
@@ -762,8 +790,22 @@ describe('E2E Tests', () => {
},
]);
// Status update: tool-call-update (scheduled)
const scheduledEvent3 = events[5].result as TaskStatusUpdateEvent;
expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
});
expect(scheduledEvent3.status.message?.parts).toMatchObject([
{
data: {
status: 'scheduled',
request: { callId: 'test-call-id-yolo' },
},
},
]);
// Status update: tool-call-update (executing)
const executingEvent = events[5].result as TaskStatusUpdateEvent;
const executingEvent = events[6].result as TaskStatusUpdateEvent;
expect(executingEvent.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
});
@@ -777,7 +819,7 @@ describe('E2E Tests', () => {
]);
// Status update: tool-call-update (success)
const successEvent = events[6].result as TaskStatusUpdateEvent;
const successEvent = events[7].result as TaskStatusUpdateEvent;
expect(successEvent.metadata?.['coderAgent']).toMatchObject({
kind: 'tool-call-update',
});
@@ -791,12 +833,12 @@ describe('E2E Tests', () => {
]);
// Status update: working (before sending tool result to LLM)
const workingEvent3 = events[7].result as TaskStatusUpdateEvent;
const workingEvent3 = events[8].result as TaskStatusUpdateEvent;
expect(workingEvent3.kind).toBe('status-update');
expect(workingEvent3.status.state).toBe('working');
// Status update: text-content (final LLM response)
const textContentEvent = events[8].result as TaskStatusUpdateEvent;
const textContentEvent = events[9].result as TaskStatusUpdateEvent;
expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({
kind: 'text-content',
});
@@ -805,7 +847,7 @@ describe('E2E Tests', () => {
]);
assertUniqueFinalEventIsLast(events);
expect(events.length).toBe(10);
expect(events.length).toBe(11);
});
it('should include traceId in status updates when available', async () => {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -27,7 +27,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.0-nightly.20260423.gaa05b4583"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.2"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
@@ -326,6 +326,36 @@ describe('SettingsDialog', () => {
});
unmount();
});
it('should render the bottom border correctly when height is constrained', async () => {
const settings = createMockSettings();
const onSelect = vi.fn();
const constrainedHeight = 15;
const renderResult = await renderDialog(settings, onSelect, {
availableTerminalHeight: constrainedHeight,
});
await renderResult.waitUntilReady();
await waitFor(() => {
const output = renderResult.lastFrame();
const lines = output.trim().split('\n');
// Verify height constraint
expect(lines.length).toBeLessThanOrEqual(constrainedHeight);
// Verify bottom border existence in the last line of the output
const lastLine = lines[lines.length - 1];
// 'round' border characters: ─, ╰, ╯
expect(lastLine).toMatch(/[─╰╯]/);
});
// SVG snapshot ensures visual layout and border rendering are preserved
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
});
});
describe('Setting Descriptions', () => {
@@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="275" viewBox="0 0 920 275">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="275" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#878787" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="19" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">&gt; Settings </text>
<text x="891" y="36" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="53" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#d7ffd7" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="891" y="70" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="87" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="87" fill="#d7ffd7" textLength="18" lengthAdjust="spacingAndGlyphs">╰─</text>
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
<text x="45" y="87" fill="#afafaf" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
<text x="180" y="87" fill="#d7ffd7" textLength="702" lengthAdjust="spacingAndGlyphs">─────────────────────────────────────────────────────────────────────────────╯</text>
<text x="891" y="87" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="104" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="104" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="119" width="9" height="17" fill="#005f00" />
<text x="27" y="121" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="36" y="119" width="9" height="17" fill="#005f00" />
<rect x="45" y="119" width="72" height="17" fill="#005f00" />
<text x="45" y="121" fill="#d7ffd7" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<rect x="117" y="119" width="711" height="17" fill="#005f00" />
<rect x="828" y="119" width="45" height="17" fill="#005f00" />
<text x="828" y="121" fill="#d7ffd7" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="121" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="138" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="45" y="136" width="198" height="17" fill="#005f00" />
<text x="45" y="138" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<text x="891" y="138" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="155" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="172" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
<text x="891" y="172" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="187" width="9" height="17" fill="#005f00" />
<text x="27" y="189" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="36" y="187" width="9" height="17" fill="#005f00" />
<rect x="45" y="187" width="117" height="17" fill="#005f00" />
<text x="45" y="189" fill="#d7ffd7" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<rect x="162" y="187" width="711" height="17" fill="#005f00" />
<text x="891" y="189" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="206" fill="#afafaf" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="206" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="223" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#878787" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -46,6 +46,24 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Initial Rendering > should render the bottom border correctly when height is constrained 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ > Settings │
│ │
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
│ ╰─Search to filter─────────────────────────────────────────────────────────────────────────────╯ │
│ ▲ │
│ ● Vim Mode false │
│ ▼ Enable Vim keybindings │
│ │
│ Apply To │
│ ● User Settings │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings enabled' correctly 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
@@ -425,7 +425,7 @@ export function BaseSettingsDialog({
flexDirection="row"
padding={1}
width="100%"
height="100%"
maxHeight={availableHeight}
>
<Box flexDirection="column" flexGrow={1}>
{/* Title */}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"description": "Gemini CLI Core",
"license": "Apache-2.0",
"repository": {
@@ -1898,6 +1898,30 @@ describe('PolicyEngine', () => {
expect(result.decision).toBe(PolicyDecision.ALLOW);
});
it('should NOT downgrade to ASK_USER for redirected commands in YOLO mode even without sandbox', async () => {
const rules: PolicyRule[] = [
{
toolName: 'run_shell_command',
decision: PolicyDecision.ALLOW,
priority: 10,
},
];
engine = new PolicyEngine({
rules,
approvalMode: ApprovalMode.YOLO,
sandboxManager: new NoopSandboxManager(),
});
const command = 'npm test 2>&1 | tail -80';
const { decision } = await engine.check(
{ name: 'run_shell_command', args: { command } },
undefined,
);
expect(decision).toBe(PolicyDecision.ALLOW);
});
it('should return ALLOW in YOLO mode even if shell command parsing fails', async () => {
const { splitCommands } = await import('../utils/shell-utils.js');
const rules: PolicyRule[] = [
+4 -5
View File
@@ -288,12 +288,11 @@ export class PolicyEngine {
if (allowRedirection) return false;
if (!hasRedirection(command)) return false;
// Do not downgrade (do not ask user) if sandboxing is enabled and in AUTO_EDIT or YOLO
const sandboxEnabled = !(this.sandboxManager instanceof NoopSandboxManager);
// Do not downgrade (do not ask user) if in AUTO_EDIT or YOLO mode.
// These modes trust the agent's actions (YOLO) or specific task (AUTO_EDIT).
if (
sandboxEnabled &&
(this.approvalMode === ApprovalMode.AUTO_EDIT ||
this.approvalMode === ApprovalMode.YOLO)
this.approvalMode === ApprovalMode.AUTO_EDIT ||
this.approvalMode === ApprovalMode.YOLO
) {
return false;
}
@@ -19,6 +19,7 @@ import {
import type { Config } from '../config/config.js';
import * as sdk from './sdk.js';
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
import { EventMetadataKey } from './clearcut-logger/event-metadata-key.js';
vi.mock('@opentelemetry/api-logs');
vi.mock('./sdk.js');
@@ -144,4 +145,174 @@ describe('conseca-logger', () => {
expect(mockLogger.emit).not.toHaveBeenCalled();
});
it('should omit user_prompt/trusted_content/policy from OTEL when logPrompts is disabled', () => {
const configNoPrompts = {
getTelemetryEnabled: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
isInteractive: vi.fn().mockReturnValue(true),
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
} as unknown as Config;
const event = new ConsecaPolicyGenerationEvent(
'sensitive prompt',
'sensitive content',
'sensitive policy',
);
logConsecaPolicyGeneration(configNoPrompts, event);
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
string,
unknown
>;
expect(attrs['user_prompt']).toBeUndefined();
expect(attrs['trusted_content']).toBeUndefined();
expect(attrs['policy']).toBeUndefined();
expect(attrs['event.name']).toBe(EVENT_CONSECA_POLICY_GENERATION);
});
it('should omit user_prompt/trusted_content/policy from Clearcut when logPrompts is disabled', () => {
const configNoPrompts = {
getTelemetryEnabled: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
isInteractive: vi.fn().mockReturnValue(true),
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
} as unknown as Config;
const event = new ConsecaPolicyGenerationEvent(
'sensitive prompt',
'sensitive content',
'sensitive policy',
'some error',
);
logConsecaPolicyGeneration(configNoPrompts, event);
expect(mockClearcutLogger.createLogEvent).toHaveBeenCalledWith(
expect.anything(),
[
{
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
value: 'some error',
},
],
);
});
it('should include user_prompt/trusted_content/policy in OTEL when logPrompts is enabled', () => {
const event = new ConsecaPolicyGenerationEvent(
'visible prompt',
'visible content',
'visible policy',
);
logConsecaPolicyGeneration(mockConfig, event);
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
string,
unknown
>;
expect(attrs['user_prompt']).toBe('visible prompt');
expect(attrs['trusted_content']).toBe('visible content');
expect(attrs['policy']).toBe('visible policy');
});
it('should omit sensitive fields from verdict OTEL when logPrompts is disabled', () => {
const configNoPrompts = {
getTelemetryEnabled: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
isInteractive: vi.fn().mockReturnValue(true),
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
} as unknown as Config;
const event = new ConsecaVerdictEvent(
'sensitive prompt',
'sensitive policy',
'sensitive tool call',
'allow',
'sensitive rationale',
);
logConsecaVerdict(configNoPrompts, event);
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
string,
unknown
>;
expect(attrs['user_prompt']).toBeUndefined();
expect(attrs['policy']).toBeUndefined();
expect(attrs['tool_call']).toBeUndefined();
expect(attrs['verdict_rationale']).toBeUndefined();
// verdict (the allow/deny result) is not sensitive and should be present
expect(attrs['verdict']).toBe('allow');
});
it('should omit sensitive fields from verdict Clearcut when logPrompts is disabled', () => {
const configNoPrompts = {
getTelemetryEnabled: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
isInteractive: vi.fn().mockReturnValue(true),
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
} as unknown as Config;
const event = new ConsecaVerdictEvent(
'sensitive prompt',
'sensitive policy',
'sensitive tool call',
'allow',
'sensitive rationale',
'some error',
);
logConsecaVerdict(configNoPrompts, event);
expect(mockClearcutLogger.createLogEvent).toHaveBeenCalledWith(
expect.anything(),
[
{
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RESULT,
value: '"allow"',
},
{
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
value: 'some error',
},
],
);
});
it('should include sensitive fields in verdict OTEL when logPrompts is enabled', () => {
const event = new ConsecaVerdictEvent(
'visible prompt',
'visible policy',
'visible tool call',
'deny',
'visible rationale',
);
logConsecaVerdict(mockConfig, event);
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
string,
unknown
>;
expect(attrs['user_prompt']).toBe('visible prompt');
expect(attrs['policy']).toBe('visible policy');
expect(attrs['tool_call']).toBe('visible tool call');
expect(attrs['verdict_rationale']).toBe('visible rationale');
expect(attrs['verdict']).toBe('deny');
});
});
+41 -31
View File
@@ -11,6 +11,7 @@ import { isTelemetrySdkInitialized } from './sdk.js';
import {
ClearcutLogger,
EventNames,
type EventValue,
} from './clearcut-logger/clearcut-logger.js';
import { EventMetadataKey } from './clearcut-logger/event-metadata-key.js';
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
@@ -27,20 +28,24 @@ export function logConsecaPolicyGeneration(
debugLogger.debug('Conseca Policy Generation Event:', event);
const clearcutLogger = ClearcutLogger.getInstance(config);
if (clearcutLogger) {
const data = [
{
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
value: safeJsonStringify(event.user_prompt),
},
{
gemini_cli_key: EventMetadataKey.CONSECA_TRUSTED_CONTENT,
value: safeJsonStringify(event.trusted_content),
},
{
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
value: safeJsonStringify(event.policy),
},
];
const data: EventValue[] = [];
if (config.getTelemetryLogPromptsEnabled()) {
data.push(
{
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
value: safeJsonStringify(event.user_prompt),
},
{
gemini_cli_key: EventMetadataKey.CONSECA_TRUSTED_CONTENT,
value: safeJsonStringify(event.trusted_content),
},
{
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
value: safeJsonStringify(event.policy),
},
);
}
if (event.error) {
data.push({
@@ -71,29 +76,34 @@ export function logConsecaVerdict(
debugLogger.debug('Conseca Verdict Event:', event);
const clearcutLogger = ClearcutLogger.getInstance(config);
if (clearcutLogger) {
const data = [
{
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
value: safeJsonStringify(event.user_prompt),
},
{
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
value: safeJsonStringify(event.policy),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
value: safeJsonStringify(event.tool_call),
},
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RESULT,
value: safeJsonStringify(event.verdict),
},
{
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RATIONALE,
value: event.verdict_rationale,
},
];
if (config.getTelemetryLogPromptsEnabled()) {
data.push(
{
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
value: safeJsonStringify(event.user_prompt),
},
{
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
value: safeJsonStringify(event.policy),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
value: safeJsonStringify(event.tool_call),
},
{
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RATIONALE,
value: event.verdict_rationale,
},
);
}
if (event.error) {
data.push({
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
+168 -15
View File
@@ -642,6 +642,54 @@ describe('loggers', () => {
}),
});
});
it('should not include response_text when logPrompts is disabled', () => {
const mockConfigNoPrompts = {
getSessionId: () => 'test-session-id',
getTargetDir: () => 'target-dir',
getUsageStatisticsEnabled: () => true,
getTelemetryEnabled: () => true,
getTelemetryLogPromptsEnabled: () => false,
getTelemetryTracesEnabled: () => false,
isInteractive: () => false,
getExperiments: () => undefined,
getExperimentsAsync: async () => undefined,
getContentGeneratorConfig: () => undefined,
} as unknown as Config;
const event = new ApiResponseEvent(
'test-model',
100,
{ prompt_id: 'prompt-id-noprompts', contents: [] },
{ candidates: [] },
AuthType.LOGIN_WITH_GOOGLE,
{},
'this response should be hidden',
);
logApiResponse(mockConfigNoPrompts, event);
const firstEmitCall = mockLogger.emit.mock.calls[0][0];
expect(firstEmitCall.attributes['response_text']).toBeUndefined();
});
it('should include response_text when logPrompts is enabled', () => {
const event = new ApiResponseEvent(
'test-model',
100,
{ prompt_id: 'prompt-id-withprompts', contents: [] },
{ candidates: [] },
AuthType.LOGIN_WITH_GOOGLE,
{},
'this response should be visible',
);
logApiResponse(mockConfig, event);
const firstEmitCall = mockLogger.emit.mock.calls[0][0];
expect(firstEmitCall.attributes['response_text']).toBe(
'this response should be visible',
);
});
});
describe('logApiError', () => {
@@ -1076,6 +1124,10 @@ describe('loggers', () => {
expect(attributes['gen_ai.provider.name']).toBe('gcp.vertex_ai');
// Ensure prompt messages are NOT included
expect(attributes['gen_ai.input.messages']).toBeUndefined();
// Ensure request_text is also NOT included in the first (toLogRecord) log
const firstLogCall = mockLogger.emit.mock.calls[0][0];
expect(firstLogCall.attributes['request_text']).toBeUndefined();
});
it('should correctly derive model from prompt details if available in semantic log', () => {
@@ -1373,16 +1425,20 @@ describe('loggers', () => {
error_type: undefined,
mcp_server_name: undefined,
extension_id: undefined,
metadata: {
model_added_lines: 1,
model_removed_lines: 2,
model_added_chars: 3,
model_removed_chars: 4,
user_added_lines: 5,
user_removed_lines: 6,
user_added_chars: 7,
user_removed_chars: 8,
},
metadata: JSON.stringify(
{
model_added_lines: 1,
model_removed_lines: 2,
model_added_chars: 3,
model_removed_chars: 4,
user_added_lines: 5,
user_removed_lines: 6,
user_added_chars: 7,
user_removed_chars: 8,
},
null,
2,
),
content_length: 13,
},
});
@@ -1455,12 +1511,16 @@ describe('loggers', () => {
body: 'Tool call: ask_user. Decision: accept. Success: true. Duration: 100ms.',
attributes: expect.objectContaining({
function_name: 'ask_user',
metadata: expect.objectContaining({
ask_user: {
question_types: ['choice'],
dismissed: false,
metadata: JSON.stringify(
{
ask_user: {
question_types: ['choice'],
dismissed: false,
},
},
}),
null,
2,
),
}),
});
});
@@ -1867,6 +1927,99 @@ describe('loggers', () => {
});
});
describe('logToolCall — logPrompts flag', () => {
it('should omit function_args when logPrompts is disabled', () => {
const mockConfigNoPrompts = {
getSessionId: () => 'test-session-id',
getTargetDir: () => 'target-dir',
getUsageStatisticsEnabled: () => true,
getTelemetryEnabled: () => true,
getTelemetryLogPromptsEnabled: () => false,
getTelemetryTracesEnabled: () => false,
isInteractive: () => false,
getExperiments: () => undefined,
getExperimentsAsync: async () => undefined,
getContentGeneratorConfig: () => undefined,
} as unknown as Config;
const call: CompletedToolCall = {
status: CoreToolCallStatus.Success,
request: {
name: 'run_bash',
args: { command: 'echo sensitive' },
callId: 'call-1',
isClientInitiated: false,
prompt_id: 'prompt-noprompts',
},
response: {
callId: 'call-1',
responseParts: [],
resultDisplay: undefined,
error: undefined,
errorType: undefined,
contentLength: undefined,
},
tool: undefined as unknown as AnyDeclarativeTool,
invocation: {} as AnyToolInvocation,
durationMs: 50,
};
const event = new ToolCallEvent(call);
logToolCall(mockConfigNoPrompts, event);
const emitted = mockLogger.emit.mock.calls[0][0] as {
attributes: Record<string, unknown>;
};
expect(emitted.attributes['function_args']).toBeUndefined();
expect(emitted.attributes['function_name']).toBe('run_bash');
});
it('should include function_args when logPrompts is enabled', () => {
const mockConfigWithPrompts = {
getSessionId: () => 'test-session-id',
getTargetDir: () => 'target-dir',
getUsageStatisticsEnabled: () => true,
getTelemetryEnabled: () => true,
getTelemetryLogPromptsEnabled: () => true,
getTelemetryTracesEnabled: () => false,
isInteractive: () => false,
getExperiments: () => undefined,
getExperimentsAsync: async () => undefined,
getContentGeneratorConfig: () => undefined,
} as unknown as Config;
const call: CompletedToolCall = {
status: CoreToolCallStatus.Success,
request: {
name: 'run_bash',
args: { command: 'echo visible' },
callId: 'call-2',
isClientInitiated: false,
prompt_id: 'prompt-withprompts',
},
response: {
callId: 'call-2',
responseParts: [],
resultDisplay: undefined,
error: undefined,
errorType: undefined,
contentLength: undefined,
},
tool: undefined as unknown as AnyDeclarativeTool,
invocation: {} as AnyToolInvocation,
durationMs: 50,
};
const event = new ToolCallEvent(call);
logToolCall(mockConfigWithPrompts, event);
const emitted = mockLogger.emit.mock.calls[0][0] as {
attributes: Record<string, unknown>;
};
expect(emitted.attributes['function_args']).toBe(
JSON.stringify({ command: 'echo visible' }, null, 2),
);
});
});
describe('logMalformedJsonResponse', () => {
beforeEach(() => {
vi.spyOn(ClearcutLogger.prototype, 'logMalformedJsonResponseEvent');
+57 -11
View File
@@ -231,6 +231,17 @@ export class UserPromptEvent implements BaseTelemetryEvent {
}
export const EVENT_TOOL_CALL = 'gemini_cli.tool_call';
const TOOL_CALL_METADATA_SAFE_KEYS = [
'model_added_lines',
'model_removed_lines',
'model_added_chars',
'model_removed_chars',
'user_added_lines',
'user_removed_lines',
'user_added_chars',
'user_removed_chars',
] as const;
export class ToolCallEvent implements BaseTelemetryEvent {
'event.name': 'tool_call';
'event.timestamp': string;
@@ -355,7 +366,6 @@ export class ToolCallEvent implements BaseTelemetryEvent {
'event.name': EVENT_TOOL_CALL,
'event.timestamp': this['event.timestamp'],
function_name: this.function_name,
function_args: safeJsonStringify(this.function_args, 2),
duration_ms: this.duration_ms,
success: this.success,
decision: this.decision,
@@ -367,8 +377,22 @@ export class ToolCallEvent implements BaseTelemetryEvent {
extension_id: this.extension_id,
start_time: this.start_time,
end_time: this.end_time,
metadata: this.metadata,
};
if (config.getTelemetryLogPromptsEnabled() && this.function_args) {
attributes['function_args'] = safeJsonStringify(this.function_args, 2);
}
if (this.metadata) {
const metadata = config.getTelemetryLogPromptsEnabled()
? this.metadata
: Object.fromEntries(
Object.entries(this.metadata).filter(([k]) =>
(TOOL_CALL_METADATA_SAFE_KEYS as readonly string[]).includes(k),
),
);
if (Object.keys(metadata).length > 0) {
attributes['metadata'] = safeJsonStringify(metadata, 2);
}
}
if (this.error) {
attributes[CoreToolCallStatus.Error] = this.error;
@@ -423,8 +447,10 @@ export class ApiRequestEvent implements BaseTelemetryEvent {
'event.timestamp': this['event.timestamp'],
model: this.model,
prompt_id: this.prompt.prompt_id,
request_text: this.request_text,
};
if (config.getTelemetryLogPromptsEnabled() && this.request_text) {
attributes['request_text'] = this.request_text;
}
if (this.role) {
attributes['role'] = this.role;
}
@@ -692,7 +718,7 @@ export class ApiResponseEvent implements BaseTelemetryEvent {
if (this.role) {
attributes['role'] = this.role;
}
if (this.response_text) {
if (config.getTelemetryLogPromptsEnabled() && this.response_text) {
attributes['response_text'] = this.response_text;
}
if (this.status_code) {
@@ -954,11 +980,20 @@ export class ConsecaPolicyGenerationEvent implements BaseTelemetryEvent {
...getCommonAttributes(config),
'event.name': EVENT_CONSECA_POLICY_GENERATION,
'event.timestamp': this['event.timestamp'],
user_prompt: this.user_prompt,
trusted_content: this.trusted_content,
policy: this.policy,
};
if (config.getTelemetryLogPromptsEnabled()) {
if (this.user_prompt) {
attributes['user_prompt'] = this.user_prompt;
}
if (this.trusted_content) {
attributes['trusted_content'] = this.trusted_content;
}
if (this.policy) {
attributes['policy'] = this.policy;
}
}
if (this.error) {
attributes['error'] = this.error;
}
@@ -1005,13 +1040,24 @@ export class ConsecaVerdictEvent implements BaseTelemetryEvent {
...getCommonAttributes(config),
'event.name': EVENT_CONSECA_VERDICT,
'event.timestamp': this['event.timestamp'],
user_prompt: this.user_prompt,
policy: this.policy,
tool_call: this.tool_call,
verdict: this.verdict,
verdict_rationale: this.verdict_rationale,
};
if (config.getTelemetryLogPromptsEnabled()) {
if (this.user_prompt) {
attributes['user_prompt'] = this.user_prompt;
}
if (this.policy) {
attributes['policy'] = this.policy;
}
if (this.tool_call) {
attributes['tool_call'] = this.tool_call;
}
if (this.verdict_rationale) {
attributes['verdict_rationale'] = this.verdict_rationale;
}
}
if (this.error) {
attributes['error'] = this.error;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-devtools",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"license": "Apache-2.0",
"type": "module",
"main": "dist/src/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-sdk",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"description": "Gemini CLI SDK",
"license": "Apache-2.0",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI Companion",
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.41.2",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {