mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 12:04:56 -07:00
859c7c3a70
fix(cli): Write shell command output to a file and limit memory buffered in UI Fixes. Checkpoint. fix(core, cli): await outputStream.end() to prevent race conditions This commit fixes a critical race condition where was called synchronously without being awaited. This led to potential file truncation or EBUSY errors on Windows when attempting to manipulate the file immediately after the call. Additionally, this change removes fixed wait times (`setTimeout`) that were previously used in test files as a band-aid. fix(core): stream processed xterm output to file to remove spurious escape codes test(core): update shell regression tests to use file_data events
251 lines
6.4 KiB
TypeScript
251 lines
6.4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type {
|
|
GenerateContentResponse,
|
|
Part,
|
|
FunctionCall,
|
|
PartListUnion,
|
|
} from '@google/genai';
|
|
import { getResponseText } from './partUtils.js';
|
|
import { supportsMultimodalFunctionResponse } from '../config/models.js';
|
|
import { debugLogger } from './debugLogger.js';
|
|
import type { Config } from '../config/config.js';
|
|
|
|
/**
|
|
* Formats tool output for a Gemini FunctionResponse.
|
|
*/
|
|
function createFunctionResponsePart(
|
|
callId: string,
|
|
toolName: string,
|
|
output: string,
|
|
outputFile?: string,
|
|
): Part {
|
|
return {
|
|
functionResponse: {
|
|
id: callId,
|
|
name: toolName,
|
|
response: { output, outputFile },
|
|
},
|
|
};
|
|
}
|
|
|
|
function toParts(input: PartListUnion): Part[] {
|
|
const parts: Part[] = [];
|
|
for (const part of Array.isArray(input) ? input : [input]) {
|
|
if (typeof part === 'string') {
|
|
parts.push({ text: part });
|
|
} else if (part) {
|
|
parts.push(part);
|
|
}
|
|
}
|
|
return parts;
|
|
}
|
|
|
|
export function convertToFunctionResponse(
|
|
toolName: string,
|
|
callId: string,
|
|
llmContent: PartListUnion,
|
|
model: string,
|
|
config?: Config,
|
|
outputFile?: string,
|
|
): Part[] {
|
|
if (typeof llmContent === 'string') {
|
|
return [
|
|
createFunctionResponsePart(callId, toolName, llmContent, outputFile),
|
|
];
|
|
}
|
|
|
|
const parts = toParts(llmContent);
|
|
|
|
// Separate text from binary types
|
|
const textParts: string[] = [];
|
|
const inlineDataParts: Part[] = [];
|
|
const fileDataParts: Part[] = [];
|
|
|
|
for (const part of parts) {
|
|
if (part.text !== undefined) {
|
|
textParts.push(part.text);
|
|
} else if (part.inlineData) {
|
|
inlineDataParts.push(part);
|
|
} else if (part.fileData) {
|
|
fileDataParts.push(part);
|
|
} else if (part.functionResponse) {
|
|
if (parts.length > 1) {
|
|
debugLogger.warn(
|
|
'convertToFunctionResponse received multiple parts with a functionResponse. Only the functionResponse will be used, other parts will be ignored',
|
|
);
|
|
}
|
|
// Handle passthrough case
|
|
return [
|
|
{
|
|
functionResponse: {
|
|
id: callId,
|
|
name: toolName,
|
|
response: part.functionResponse.response,
|
|
},
|
|
},
|
|
];
|
|
}
|
|
// Ignore other part types
|
|
}
|
|
|
|
// Build the primary response part
|
|
const part: Part = {
|
|
functionResponse: {
|
|
id: callId,
|
|
name: toolName,
|
|
response: {
|
|
...(textParts.length > 0 ? { output: textParts.join('\n') } : {}),
|
|
outputFile,
|
|
},
|
|
},
|
|
};
|
|
|
|
const isMultimodalFRSupported = supportsMultimodalFunctionResponse(
|
|
model,
|
|
config,
|
|
);
|
|
const siblingParts: Part[] = [...fileDataParts];
|
|
|
|
if (inlineDataParts.length > 0) {
|
|
if (isMultimodalFRSupported) {
|
|
// Nest inlineData if supported by the model
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
(part.functionResponse as unknown as { parts: Part[] }).parts =
|
|
inlineDataParts;
|
|
} else {
|
|
// Otherwise treat as siblings
|
|
siblingParts.push(...inlineDataParts);
|
|
}
|
|
}
|
|
|
|
// Add descriptive text if the response object is empty but we have binary content
|
|
if (
|
|
textParts.length === 0 &&
|
|
(inlineDataParts.length > 0 || fileDataParts.length > 0)
|
|
) {
|
|
const totalBinaryItems = inlineDataParts.length + fileDataParts.length;
|
|
part.functionResponse!.response = {
|
|
output: `Binary content provided (${totalBinaryItems} item(s)).`,
|
|
};
|
|
}
|
|
|
|
if (siblingParts.length > 0) {
|
|
return [part, ...siblingParts];
|
|
}
|
|
|
|
return [part];
|
|
}
|
|
|
|
export function getResponseTextFromParts(parts: Part[]): string | undefined {
|
|
if (!parts) {
|
|
return undefined;
|
|
}
|
|
const textSegments = parts
|
|
.map((part) => part.text)
|
|
.filter((text): text is string => typeof text === 'string');
|
|
|
|
if (textSegments.length === 0) {
|
|
return undefined;
|
|
}
|
|
return textSegments.join('');
|
|
}
|
|
|
|
export function getFunctionCalls(
|
|
response: GenerateContentResponse,
|
|
): FunctionCall[] | undefined {
|
|
const parts = response.candidates?.[0]?.content?.parts;
|
|
if (!parts) {
|
|
return undefined;
|
|
}
|
|
const functionCallParts = parts
|
|
.filter((part) => !!part.functionCall)
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
.map((part) => part.functionCall as FunctionCall);
|
|
return functionCallParts.length > 0 ? functionCallParts : undefined;
|
|
}
|
|
|
|
export function getFunctionCallsFromParts(
|
|
parts: Part[],
|
|
): FunctionCall[] | undefined {
|
|
if (!parts) {
|
|
return undefined;
|
|
}
|
|
const functionCallParts = parts
|
|
.filter((part) => !!part.functionCall)
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
.map((part) => part.functionCall as FunctionCall);
|
|
return functionCallParts.length > 0 ? functionCallParts : undefined;
|
|
}
|
|
|
|
export function getFunctionCallsAsJson(
|
|
response: GenerateContentResponse,
|
|
): string | undefined {
|
|
const functionCalls = getFunctionCalls(response);
|
|
if (!functionCalls) {
|
|
return undefined;
|
|
}
|
|
return JSON.stringify(functionCalls, null, 2);
|
|
}
|
|
|
|
export function getFunctionCallsFromPartsAsJson(
|
|
parts: Part[],
|
|
): string | undefined {
|
|
const functionCalls = getFunctionCallsFromParts(parts);
|
|
if (!functionCalls) {
|
|
return undefined;
|
|
}
|
|
return JSON.stringify(functionCalls, null, 2);
|
|
}
|
|
|
|
export function getStructuredResponse(
|
|
response: GenerateContentResponse,
|
|
): string | undefined {
|
|
const textContent = getResponseText(response);
|
|
const functionCallsJson = getFunctionCallsAsJson(response);
|
|
|
|
if (textContent && functionCallsJson) {
|
|
return `${textContent}\n${functionCallsJson}`;
|
|
}
|
|
if (textContent) {
|
|
return textContent;
|
|
}
|
|
if (functionCallsJson) {
|
|
return functionCallsJson;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function getStructuredResponseFromParts(
|
|
parts: Part[],
|
|
): string | undefined {
|
|
const textContent = getResponseTextFromParts(parts);
|
|
const functionCallsJson = getFunctionCallsFromPartsAsJson(parts);
|
|
|
|
if (textContent && functionCallsJson) {
|
|
return `${textContent}\n${functionCallsJson}`;
|
|
}
|
|
if (textContent) {
|
|
return textContent;
|
|
}
|
|
if (functionCallsJson) {
|
|
return functionCallsJson;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function getCitations(resp: GenerateContentResponse): string[] {
|
|
return (resp.candidates?.[0]?.citationMetadata?.citations ?? [])
|
|
.filter((citation) => citation.uri !== undefined)
|
|
.map((citation) => {
|
|
if (citation.title) {
|
|
return `(${citation.title}) ${citation.uri}`;
|
|
}
|
|
return citation.uri!;
|
|
});
|
|
}
|