mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-28 10:41:03 -07:00
Compare commits
12 Commits
wrong-model
...
v0.41.2
| Author | SHA1 | Date | |
|---|---|---|---|
| b0c7a1722a | |||
| 0da6569b62 | |||
| e1a54496dd | |||
| 7d5b3584f3 | |||
| 03fd454b31 | |||
| 6339fd95fa | |||
| c0c1df12f9 | |||
| 3e31372710 | |||
| c6a695d1b5 | |||
| 78f3d705dd | |||
| 7d233ddd5f | |||
| a5befa04f5 |
Generated
+9
-9
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
+63
@@ -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">> 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,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[] = [
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,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,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,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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user