mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 02:24:09 -07:00
cfc697a96d
- Also updated README.md accordingly. Part of https://b.corp.google.com/issues/411384603
236 lines
7.4 KiB
TypeScript
236 lines
7.4 KiB
TypeScript
import { Part } from '@google/genai';
|
|
import { toolRegistry } from '../tools/tool-registry.js';
|
|
import {
|
|
HistoryItem,
|
|
IndividualToolCallDisplay,
|
|
ToolCallEvent,
|
|
ToolCallStatus,
|
|
ToolConfirmationOutcome,
|
|
ToolEditConfirmationDetails,
|
|
ToolExecuteConfirmationDetails,
|
|
} from '../ui/types.js';
|
|
import { ToolResultDisplay } from '../tools/tools.js';
|
|
|
|
/**
|
|
* Processes a tool call chunk and updates the history state accordingly.
|
|
* Manages adding new tool groups or updating existing ones.
|
|
* Resides here as its primary effect is updating history based on tool events.
|
|
*/
|
|
export const handleToolCallChunk = (
|
|
chunk: ToolCallEvent,
|
|
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
|
|
submitQuery: (query: Part) => Promise<void>,
|
|
getNextMessageId: () => number,
|
|
currentToolGroupIdRef: React.MutableRefObject<number | null>,
|
|
): void => {
|
|
const toolDefinition = toolRegistry.getTool(chunk.name);
|
|
const description = toolDefinition?.getDescription
|
|
? toolDefinition.getDescription(chunk.args)
|
|
: '';
|
|
const toolDisplayName = toolDefinition?.displayName ?? chunk.name;
|
|
let confirmationDetails = chunk.confirmationDetails;
|
|
if (confirmationDetails) {
|
|
const originalConfirmationDetails = confirmationDetails;
|
|
const historyUpdatingConfirm = async (outcome: ToolConfirmationOutcome) => {
|
|
originalConfirmationDetails.onConfirm(outcome);
|
|
|
|
if (outcome === ToolConfirmationOutcome.Cancel) {
|
|
let resultDisplay: ToolResultDisplay | undefined;
|
|
if ('fileDiff' in originalConfirmationDetails) {
|
|
resultDisplay = {
|
|
fileDiff: (
|
|
originalConfirmationDetails as ToolEditConfirmationDetails
|
|
).fileDiff,
|
|
};
|
|
} else {
|
|
resultDisplay = `~~${(originalConfirmationDetails as ToolExecuteConfirmationDetails).command}~~`;
|
|
}
|
|
handleToolCallChunk(
|
|
{
|
|
...chunk,
|
|
status: ToolCallStatus.Canceled,
|
|
confirmationDetails: undefined,
|
|
resultDisplay,
|
|
},
|
|
setHistory,
|
|
submitQuery,
|
|
getNextMessageId,
|
|
currentToolGroupIdRef,
|
|
);
|
|
const functionResponse: Part = {
|
|
functionResponse: {
|
|
name: chunk.name,
|
|
response: { error: 'User rejected function call.' },
|
|
},
|
|
};
|
|
await submitQuery(functionResponse);
|
|
} else {
|
|
const tool = toolRegistry.getTool(chunk.name);
|
|
if (!tool) {
|
|
throw new Error(
|
|
`Tool "${chunk.name}" not found or is not registered.`,
|
|
);
|
|
}
|
|
handleToolCallChunk(
|
|
{
|
|
...chunk,
|
|
status: ToolCallStatus.Invoked,
|
|
resultDisplay: 'Executing...',
|
|
confirmationDetails: undefined,
|
|
},
|
|
setHistory,
|
|
submitQuery,
|
|
getNextMessageId,
|
|
currentToolGroupIdRef,
|
|
);
|
|
const result = await tool.execute(chunk.args);
|
|
handleToolCallChunk(
|
|
{
|
|
...chunk,
|
|
status: ToolCallStatus.Invoked,
|
|
resultDisplay: result.returnDisplay,
|
|
confirmationDetails: undefined,
|
|
},
|
|
setHistory,
|
|
submitQuery,
|
|
getNextMessageId,
|
|
currentToolGroupIdRef,
|
|
);
|
|
const functionResponse: Part = {
|
|
functionResponse: {
|
|
name: chunk.name,
|
|
id: chunk.callId,
|
|
response: { output: result.llmContent },
|
|
},
|
|
};
|
|
await submitQuery(functionResponse);
|
|
}
|
|
};
|
|
|
|
confirmationDetails = {
|
|
...originalConfirmationDetails,
|
|
onConfirm: historyUpdatingConfirm,
|
|
};
|
|
}
|
|
const toolDetail: IndividualToolCallDisplay = {
|
|
callId: chunk.callId,
|
|
name: toolDisplayName,
|
|
description,
|
|
resultDisplay: chunk.resultDisplay,
|
|
status: chunk.status,
|
|
confirmationDetails: confirmationDetails,
|
|
};
|
|
|
|
const activeGroupId = currentToolGroupIdRef.current;
|
|
setHistory((prev) => {
|
|
if (chunk.status === ToolCallStatus.Pending) {
|
|
if (activeGroupId === null) {
|
|
// Start a new tool group
|
|
const newGroupId = getNextMessageId();
|
|
currentToolGroupIdRef.current = newGroupId;
|
|
return [
|
|
...prev,
|
|
{
|
|
id: newGroupId,
|
|
type: 'tool_group',
|
|
tools: [toolDetail],
|
|
} as HistoryItem,
|
|
];
|
|
}
|
|
|
|
// Add to existing tool group
|
|
return prev.map((item) =>
|
|
item.id === activeGroupId && item.type === 'tool_group'
|
|
? item.tools.some((t) => t.callId === toolDetail.callId)
|
|
? item // Tool already listed as pending
|
|
: { ...item, tools: [...item.tools, toolDetail] }
|
|
: item,
|
|
);
|
|
}
|
|
|
|
// Update the status of a pending tool within the active group
|
|
if (activeGroupId === null) {
|
|
// Log if an invoked tool arrives without an active group context
|
|
console.warn(
|
|
'Received invoked tool status without an active tool group ID:',
|
|
chunk,
|
|
);
|
|
return prev;
|
|
}
|
|
|
|
return prev.map((item) =>
|
|
item.id === activeGroupId && item.type === 'tool_group'
|
|
? {
|
|
...item,
|
|
tools: item.tools.map((t) =>
|
|
t.callId === toolDetail.callId
|
|
? { ...t, ...toolDetail, status: chunk.status } // Update details & status
|
|
: t,
|
|
),
|
|
}
|
|
: item,
|
|
);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Appends an error or informational message to the history, attempting to attach
|
|
* it to the last non-user message or creating a new entry.
|
|
*/
|
|
export const addErrorMessageToHistory = (
|
|
error: any,
|
|
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
|
|
getNextMessageId: () => number,
|
|
): void => {
|
|
const isAbort = error.name === 'AbortError';
|
|
const errorType = isAbort ? 'info' : 'error';
|
|
const errorText = isAbort
|
|
? '[Request cancelled by user]'
|
|
: `[Error: ${error.message || 'Unknown error'}]`;
|
|
|
|
setHistory((prev) => {
|
|
const reversedHistory = [...prev].reverse();
|
|
// Find the last message that isn't from the user to append the error/info to
|
|
const lastBotMessageIndex = reversedHistory.findIndex(
|
|
(item) => item.type !== 'user',
|
|
);
|
|
const originalIndex =
|
|
lastBotMessageIndex !== -1 ? prev.length - 1 - lastBotMessageIndex : -1;
|
|
|
|
if (originalIndex !== -1) {
|
|
// Append error to the last relevant message
|
|
return prev.map((item, index) => {
|
|
if (index === originalIndex) {
|
|
let baseText = '';
|
|
// Determine base text based on item type
|
|
if (item.type === 'gemini') baseText = item.text ?? '';
|
|
else if (item.type === 'tool_group')
|
|
baseText = `Tool execution (${item.tools.length} calls)`;
|
|
else if (item.type === 'error' || item.type === 'info')
|
|
baseText = item.text ?? '';
|
|
// Safely handle potential undefined text
|
|
|
|
const updatedText = (
|
|
baseText +
|
|
(baseText && !baseText.endsWith('\n') ? '\n' : '') +
|
|
errorText
|
|
).trim();
|
|
// Reuse existing ID, update type and text
|
|
return { ...item, type: errorType, text: updatedText };
|
|
}
|
|
return item;
|
|
});
|
|
} else {
|
|
// No previous message to append to, add a new error item
|
|
return [
|
|
...prev,
|
|
{
|
|
id: getNextMessageId(),
|
|
type: errorType,
|
|
text: errorText,
|
|
} as HistoryItem,
|
|
];
|
|
}
|
|
});
|
|
};
|