!feat(cli): harden non-interactive agent session handling

This commit is contained in:
Adam Weidman
2026-03-20 14:33:57 -04:00
parent 2dfe237738
commit 25c5d44678
4 changed files with 300 additions and 52 deletions
+58 -17
View File
@@ -180,6 +180,8 @@ export async function runNonInteractive({
};
let errorToHandle: unknown | undefined;
let terminalProcessExitHandled = false;
let abortSession = () => {};
try {
consolePatcher.patch();
@@ -288,14 +290,23 @@ export async function runNonInteractive({
});
// Wire Ctrl+C to session abort
abortController.signal.addEventListener('abort', () => {
abortSession = () => {
void session.abort();
});
};
abortController.signal.addEventListener('abort', abortSession);
if (abortController.signal.aborted) {
return handleCancellationError(config);
}
// Start the agentic loop (runs in background)
const { streamId } = await session.send({
message: geminiPartsToContentParts(query),
});
if (streamId === null) {
throw new Error(
'LegacyAgentSession.send() unexpectedly returned no stream for a message send.',
);
}
const getTextContent = (parts?: ContentPart[]): string | undefined => {
const text = parts
@@ -346,11 +357,23 @@ export async function runNonInteractive({
enumerable: true,
});
}
if (errorMeta?.['status'] !== undefined) {
Object.defineProperty(errToThrow, 'status', {
value: errorMeta['status'],
enumerable: true,
});
}
return errToThrow;
};
const runTerminalExitHandler = (handler: () => never): never => {
terminalProcessExitHandled = true;
return handler();
};
// Consume AgentEvents for output formatting
let responseText = '';
let preToolResponseText: string | undefined;
let streamEnded = false;
for await (const event of session.stream({ streamId })) {
if (streamEnded) break;
@@ -383,6 +406,12 @@ export async function runNonInteractive({
break;
}
case 'tool_request': {
if (config.getOutputFormat() === OutputFormat.JSON) {
// Final JSON output should reflect the last assistant answer after
// any tool orchestration, not intermediate pre-tool text.
preToolResponseText = responseText || preToolResponseText;
responseText = '';
}
if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.TOOL_USE,
@@ -421,12 +450,33 @@ export async function runNonInteractive({
const errorMsg = getTextContent(event.content) ?? 'Tool error';
if (event.data?.['errorType'] === ToolErrorType.STOP_EXECUTION) {
if (
config.getOutputFormat() === OutputFormat.JSON &&
!responseText &&
preToolResponseText
) {
responseText = preToolResponseText;
}
const stopMessage = `Agent execution stopped: ${errorMsg}`;
if (config.getOutputFormat() === OutputFormat.TEXT) {
process.stderr.write(`${stopMessage}\n`);
}
}
if (event.data?.['errorType'] === ToolErrorType.NO_SPACE_LEFT) {
runTerminalExitHandler(() =>
handleToolError(
event.name,
new Error(errorMsg),
config,
typeof event.data?.['errorType'] === 'string'
? event.data['errorType']
: undefined,
displayText,
),
);
break;
}
handleToolError(
event.name,
new Error(errorMsg),
@@ -470,22 +520,9 @@ export async function runNonInteractive({
}
case 'agent_end': {
if (event.reason === 'aborted') {
handleCancellationError(config);
runTerminalExitHandler(() => handleCancellationError(config));
} else if (event.reason === 'max_turns') {
const isSessionLimit =
typeof event.data?.['maxTurns'] === 'number' &&
typeof event.data?.['turnCount'] === 'number';
if (isSessionLimit) {
handleMaxTurnsExceededError(config);
}
if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.ERROR,
timestamp: new Date().toISOString(),
severity: 'error',
message: 'Maximum session turns exceeded',
});
}
runTerminalExitHandler(() => handleMaxTurnsExceededError(config));
}
const stopMessage =
@@ -522,12 +559,16 @@ export async function runNonInteractive({
} finally {
// Cleanup stdin cancellation before other cleanup
cleanupStdinCancellation();
abortController.signal.removeEventListener('abort', abortSession);
consolePatcher.cleanup();
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
}
if (errorToHandle) {
if (terminalProcessExitHandled) {
throw errorToHandle;
}
handleError(errorToHandle, config);
}
});