mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 20:14:44 -07:00
Revert "feat: Introduce an AI-driven interactive shell mode with new"
This reverts commit 651ad63ed6.
This commit is contained in:
@@ -1009,7 +1009,6 @@ export async function loadCliConfig(
|
|||||||
enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell,
|
enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell,
|
||||||
shellBackgroundCompletionBehavior: settings.tools?.shell
|
shellBackgroundCompletionBehavior: settings.tools?.shell
|
||||||
?.backgroundCompletionBehavior as string | undefined,
|
?.backgroundCompletionBehavior as string | undefined,
|
||||||
interactiveShellMode: settings.tools?.shell?.interactiveShellMode,
|
|
||||||
shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout,
|
shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout,
|
||||||
enableShellOutputEfficiency:
|
enableShellOutputEfficiency:
|
||||||
settings.tools?.shell?.enableShellOutputEfficiency ?? true,
|
settings.tools?.shell?.enableShellOutputEfficiency ?? true,
|
||||||
|
|||||||
@@ -1512,26 +1512,6 @@ const SETTINGS_SCHEMA = {
|
|||||||
{ label: 'Notify', value: 'notify' },
|
{ label: 'Notify', value: 'notify' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
interactiveShellMode: {
|
|
||||||
type: 'enum',
|
|
||||||
label: 'Interactive Shell Mode',
|
|
||||||
category: 'Tools',
|
|
||||||
requiresRestart: true,
|
|
||||||
default: undefined as 'human' | 'ai' | 'off' | undefined,
|
|
||||||
description: oneLine`
|
|
||||||
Controls who can interact with backgrounded shell processes.
|
|
||||||
"human": user can Tab-focus and type into shells (default).
|
|
||||||
"ai": model gets write_to_shell/read_shell tools for TUI interaction.
|
|
||||||
"off": no interactive shell.
|
|
||||||
When set, overrides enableInteractiveShell.
|
|
||||||
`,
|
|
||||||
showInDialog: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'human', label: 'Human (Tab to focus)' },
|
|
||||||
{ value: 'ai', label: 'AI (model-driven tools)' },
|
|
||||||
{ value: 'off', label: 'Off' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
pager: {
|
pager: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
label: 'Pager',
|
label: 'Pager',
|
||||||
|
|||||||
@@ -92,23 +92,7 @@ export function shellReducer(
|
|||||||
nextTasks.delete(action.pid);
|
nextTasks.delete(action.pid);
|
||||||
}
|
}
|
||||||
nextTasks.set(action.pid, updatedTask);
|
nextTasks.set(action.pid, updatedTask);
|
||||||
|
return { ...state, backgroundTasks: nextTasks };
|
||||||
// Auto-hide panel when all tasks have exited
|
|
||||||
let nextVisible = state.isBackgroundTaskVisible;
|
|
||||||
if (action.update.status === 'exited') {
|
|
||||||
const hasRunning = Array.from(nextTasks.values()).some(
|
|
||||||
(s) => s.status === 'running',
|
|
||||||
);
|
|
||||||
if (!hasRunning) {
|
|
||||||
nextVisible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
backgroundTasks: nextTasks,
|
|
||||||
isBackgroundTaskVisible: nextVisible,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case 'APPEND_TASK_OUTPUT': {
|
case 'APPEND_TASK_OUTPUT': {
|
||||||
const task = state.backgroundTasks.get(action.pid);
|
const task = state.backgroundTasks.get(action.pid);
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
||||||
import { type BackgroundTask } from './shellReducer.js';
|
|
||||||
|
|
||||||
export interface BackgroundShellManagerProps {
|
|
||||||
backgroundTasks: Map<number, BackgroundTask>;
|
|
||||||
backgroundTaskCount: number;
|
|
||||||
isBackgroundTaskVisible: boolean;
|
|
||||||
activePtyId: number | null | undefined;
|
|
||||||
embeddedShellFocused: boolean;
|
|
||||||
setEmbeddedShellFocused: (focused: boolean) => void;
|
|
||||||
terminalHeight: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBackgroundShellManager({
|
|
||||||
backgroundTasks,
|
|
||||||
backgroundTaskCount,
|
|
||||||
isBackgroundTaskVisible,
|
|
||||||
activePtyId,
|
|
||||||
embeddedShellFocused,
|
|
||||||
setEmbeddedShellFocused,
|
|
||||||
terminalHeight,
|
|
||||||
}: BackgroundShellManagerProps) {
|
|
||||||
const [isBackgroundShellListOpen, setIsBackgroundShellListOpen] =
|
|
||||||
useState(false);
|
|
||||||
const [activeBackgroundShellPid, setActiveBackgroundShellPid] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const prevShellCountRef = useRef(backgroundTaskCount);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (backgroundTasks.size === 0) {
|
|
||||||
if (activeBackgroundShellPid !== null) {
|
|
||||||
setActiveBackgroundShellPid(null);
|
|
||||||
}
|
|
||||||
if (isBackgroundShellListOpen) {
|
|
||||||
setIsBackgroundShellListOpen(false);
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
activeBackgroundShellPid === null ||
|
|
||||||
!backgroundTasks.has(activeBackgroundShellPid)
|
|
||||||
) {
|
|
||||||
// If active shell is closed or none selected, select the first one
|
|
||||||
setActiveBackgroundShellPid(backgroundTasks.keys().next().value ?? null);
|
|
||||||
} else if (backgroundTaskCount > prevShellCountRef.current) {
|
|
||||||
// A new shell was added — auto-switch to the newest one (last in the map)
|
|
||||||
const pids = Array.from(backgroundTasks.keys());
|
|
||||||
const newestPid = pids[pids.length - 1];
|
|
||||||
if (newestPid !== undefined && newestPid !== activeBackgroundShellPid) {
|
|
||||||
setActiveBackgroundShellPid(newestPid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prevShellCountRef.current = backgroundTaskCount;
|
|
||||||
}, [
|
|
||||||
backgroundTasks,
|
|
||||||
activeBackgroundShellPid,
|
|
||||||
backgroundTaskCount,
|
|
||||||
isBackgroundShellListOpen,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (embeddedShellFocused) {
|
|
||||||
const hasActiveForegroundShell = !!activePtyId;
|
|
||||||
const hasVisibleBackgroundShell =
|
|
||||||
isBackgroundTaskVisible && backgroundTasks.size > 0;
|
|
||||||
|
|
||||||
if (!hasActiveForegroundShell && !hasVisibleBackgroundShell) {
|
|
||||||
setEmbeddedShellFocused(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
isBackgroundTaskVisible,
|
|
||||||
backgroundTasks,
|
|
||||||
embeddedShellFocused,
|
|
||||||
backgroundTaskCount,
|
|
||||||
activePtyId,
|
|
||||||
setEmbeddedShellFocused,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const backgroundShellHeight = useMemo(
|
|
||||||
() =>
|
|
||||||
isBackgroundTaskVisible && backgroundTasks.size > 0
|
|
||||||
? Math.max(Math.floor(terminalHeight * 0.3), 5)
|
|
||||||
: 0,
|
|
||||||
[isBackgroundTaskVisible, backgroundTasks.size, terminalHeight],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isBackgroundShellListOpen,
|
|
||||||
setIsBackgroundShellListOpen,
|
|
||||||
activeBackgroundShellPid,
|
|
||||||
setActiveBackgroundShellPid,
|
|
||||||
backgroundShellHeight,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -661,10 +661,6 @@ export const useExecutionLifecycle = (
|
|||||||
(s: BackgroundTask) => s.status === 'running',
|
(s: BackgroundTask) => s.status === 'running',
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const showBackgroundShell = useCallback(() => {
|
|
||||||
dispatch({ type: 'SET_VISIBILITY', visible: true });
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleShellCommand,
|
handleShellCommand,
|
||||||
activeShellPtyId: state.activeShellPtyId,
|
activeShellPtyId: state.activeShellPtyId,
|
||||||
@@ -672,7 +668,6 @@ export const useExecutionLifecycle = (
|
|||||||
backgroundTaskCount,
|
backgroundTaskCount,
|
||||||
isBackgroundTaskVisible: state.isBackgroundTaskVisible,
|
isBackgroundTaskVisible: state.isBackgroundTaskVisible,
|
||||||
toggleBackgroundTasks,
|
toggleBackgroundTasks,
|
||||||
showBackgroundShell,
|
|
||||||
backgroundCurrentExecution,
|
backgroundCurrentExecution,
|
||||||
registerBackgroundTask,
|
registerBackgroundTask,
|
||||||
dismissBackgroundTask,
|
dismissBackgroundTask,
|
||||||
|
|||||||
@@ -390,7 +390,6 @@ export const useGeminiStream = (
|
|||||||
backgroundTaskCount,
|
backgroundTaskCount,
|
||||||
isBackgroundTaskVisible,
|
isBackgroundTaskVisible,
|
||||||
toggleBackgroundTasks,
|
toggleBackgroundTasks,
|
||||||
showBackgroundShell,
|
|
||||||
backgroundCurrentExecution,
|
backgroundCurrentExecution,
|
||||||
registerBackgroundTask,
|
registerBackgroundTask,
|
||||||
dismissBackgroundTask,
|
dismissBackgroundTask,
|
||||||
@@ -1918,7 +1917,6 @@ export const useGeminiStream = (
|
|||||||
backgroundedTool.command,
|
backgroundedTool.command,
|
||||||
backgroundedTool.initialOutput,
|
backgroundedTool.initialOutput,
|
||||||
);
|
);
|
||||||
showBackgroundShell();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2058,7 +2056,6 @@ export const useGeminiStream = (
|
|||||||
modelSwitchedFromQuotaError,
|
modelSwitchedFromQuotaError,
|
||||||
addItem,
|
addItem,
|
||||||
registerBackgroundTask,
|
registerBackgroundTask,
|
||||||
showBackgroundShell,
|
|
||||||
consumeUserHint,
|
consumeUserHint,
|
||||||
isLowErrorVerbosity,
|
isLowErrorVerbosity,
|
||||||
maybeAddSuppressedToolErrorNote,
|
maybeAddSuppressedToolErrorNote,
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ import { GlobTool } from '../tools/glob.js';
|
|||||||
import { ActivateSkillTool } from '../tools/activate-skill.js';
|
import { ActivateSkillTool } from '../tools/activate-skill.js';
|
||||||
import { EditTool } from '../tools/edit.js';
|
import { EditTool } from '../tools/edit.js';
|
||||||
import { ShellTool } from '../tools/shell.js';
|
import { ShellTool } from '../tools/shell.js';
|
||||||
import { WriteToShellTool } from '../tools/write-to-shell.js';
|
|
||||||
import { ReadShellTool } from '../tools/read-shell.js';
|
|
||||||
import { WriteFileTool } from '../tools/write-file.js';
|
import { WriteFileTool } from '../tools/write-file.js';
|
||||||
import { WebFetchTool } from '../tools/web-fetch.js';
|
import { WebFetchTool } from '../tools/web-fetch.js';
|
||||||
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
|
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
|
||||||
@@ -658,7 +656,6 @@ export interface ConfigParameters {
|
|||||||
useRipgrep?: boolean;
|
useRipgrep?: boolean;
|
||||||
enableInteractiveShell?: boolean;
|
enableInteractiveShell?: boolean;
|
||||||
shellBackgroundCompletionBehavior?: string;
|
shellBackgroundCompletionBehavior?: string;
|
||||||
interactiveShellMode?: 'human' | 'ai' | 'off';
|
|
||||||
skipNextSpeakerCheck?: boolean;
|
skipNextSpeakerCheck?: boolean;
|
||||||
shellExecutionConfig?: ShellExecutionConfig;
|
shellExecutionConfig?: ShellExecutionConfig;
|
||||||
extensionManagement?: boolean;
|
extensionManagement?: boolean;
|
||||||
@@ -871,7 +868,6 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
| 'inject'
|
| 'inject'
|
||||||
| 'notify'
|
| 'notify'
|
||||||
| 'silent';
|
| 'silent';
|
||||||
private readonly interactiveShellMode: 'human' | 'ai' | 'off';
|
|
||||||
private readonly skipNextSpeakerCheck: boolean;
|
private readonly skipNextSpeakerCheck: boolean;
|
||||||
private readonly useBackgroundColor: boolean;
|
private readonly useBackgroundColor: boolean;
|
||||||
private readonly useAlternateBuffer: boolean;
|
private readonly useAlternateBuffer: boolean;
|
||||||
@@ -1239,14 +1235,6 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
this.shellBackgroundCompletionBehavior = 'silent';
|
this.shellBackgroundCompletionBehavior = 'silent';
|
||||||
}
|
}
|
||||||
|
|
||||||
// interactiveShellMode takes precedence over enableInteractiveShell.
|
|
||||||
// If not set, derive from enableInteractiveShell for backward compat.
|
|
||||||
if (params.interactiveShellMode) {
|
|
||||||
this.interactiveShellMode = params.interactiveShellMode;
|
|
||||||
} else {
|
|
||||||
this.interactiveShellMode = this.enableInteractiveShell ? 'human' : 'off';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true;
|
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true;
|
||||||
this.shellExecutionConfig = {
|
this.shellExecutionConfig = {
|
||||||
terminalWidth: params.shellExecutionConfig?.terminalWidth ?? 80,
|
terminalWidth: params.shellExecutionConfig?.terminalWidth ?? 80,
|
||||||
@@ -3223,14 +3211,10 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
return (
|
return (
|
||||||
this.interactive &&
|
this.interactive &&
|
||||||
this.ptyInfo !== 'child_process' &&
|
this.ptyInfo !== 'child_process' &&
|
||||||
this.interactiveShellMode !== 'off'
|
this.enableInteractiveShell
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInteractiveShellMode(): 'human' | 'ai' | 'off' {
|
|
||||||
return this.interactiveShellMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSkillsSupportEnabled(): boolean {
|
isSkillsSupportEnabled(): boolean {
|
||||||
return this.skillsSupport;
|
return this.skillsSupport;
|
||||||
}
|
}
|
||||||
@@ -3591,15 +3575,6 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
new ReadBackgroundOutputTool(this, this.messageBus),
|
new ReadBackgroundOutputTool(this, this.messageBus),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Register AI-driven interactive shell tools when mode is 'ai'
|
|
||||||
if (this.getInteractiveShellMode() === 'ai') {
|
|
||||||
maybeRegister(WriteToShellTool, () =>
|
|
||||||
registry.registerTool(new WriteToShellTool(this.messageBus)),
|
|
||||||
);
|
|
||||||
maybeRegister(ReadShellTool, () =>
|
|
||||||
registry.registerTool(new ReadShellTool(this.messageBus)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!this.isMemoryManagerEnabled()) {
|
if (!this.isMemoryManagerEnabled()) {
|
||||||
maybeRegister(MemoryTool, () =>
|
maybeRegister(MemoryTool, () =>
|
||||||
registry.registerTool(new MemoryTool(this.messageBus, this.storage)),
|
registry.registerTool(new MemoryTool(this.messageBus, this.storage)),
|
||||||
|
|||||||
@@ -200,7 +200,6 @@ export class PromptProvider {
|
|||||||
enableShellEfficiency:
|
enableShellEfficiency:
|
||||||
context.config.getEnableShellOutputEfficiency(),
|
context.config.getEnableShellOutputEfficiency(),
|
||||||
interactiveShellEnabled: context.config.isInteractiveShellEnabled(),
|
interactiveShellEnabled: context.config.isInteractiveShellEnabled(),
|
||||||
interactiveShellMode: context.config.getInteractiveShellMode(),
|
|
||||||
topicUpdateNarration:
|
topicUpdateNarration:
|
||||||
context.config.isTopicUpdateNarrationEnabled(),
|
context.config.isTopicUpdateNarrationEnabled(),
|
||||||
memoryManagerEnabled: context.config.isMemoryManagerEnabled(),
|
memoryManagerEnabled: context.config.isMemoryManagerEnabled(),
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import {
|
|||||||
MEMORY_TOOL_NAME,
|
MEMORY_TOOL_NAME,
|
||||||
READ_FILE_TOOL_NAME,
|
READ_FILE_TOOL_NAME,
|
||||||
SHELL_TOOL_NAME,
|
SHELL_TOOL_NAME,
|
||||||
WRITE_TO_SHELL_TOOL_NAME,
|
|
||||||
READ_SHELL_TOOL_NAME,
|
|
||||||
WRITE_FILE_TOOL_NAME,
|
WRITE_FILE_TOOL_NAME,
|
||||||
WRITE_TODOS_TOOL_NAME,
|
WRITE_TODOS_TOOL_NAME,
|
||||||
GREP_PARAM_TOTAL_MAX_MATCHES,
|
GREP_PARAM_TOTAL_MAX_MATCHES,
|
||||||
@@ -83,7 +81,6 @@ export interface PrimaryWorkflowsOptions {
|
|||||||
export interface OperationalGuidelinesOptions {
|
export interface OperationalGuidelinesOptions {
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
interactiveShellEnabled: boolean;
|
interactiveShellEnabled: boolean;
|
||||||
interactiveShellMode?: 'human' | 'ai' | 'off';
|
|
||||||
topicUpdateNarration: boolean;
|
topicUpdateNarration: boolean;
|
||||||
memoryManagerEnabled: boolean;
|
memoryManagerEnabled: boolean;
|
||||||
}
|
}
|
||||||
@@ -394,7 +391,7 @@ export function renderOperationalGuidelines(
|
|||||||
- **Command Execution:** Use the ${formatToolName(SHELL_TOOL_NAME)} tool for running shell commands, remembering the safety rule to explain modifying commands first.${toolUsageInteractive(
|
- **Command Execution:** Use the ${formatToolName(SHELL_TOOL_NAME)} tool for running shell commands, remembering the safety rule to explain modifying commands first.${toolUsageInteractive(
|
||||||
options.interactive,
|
options.interactive,
|
||||||
options.interactiveShellEnabled,
|
options.interactiveShellEnabled,
|
||||||
)}${toolUsageRememberingFacts(options)}${toolUsageAiShell(options)}
|
)}${toolUsageRememberingFacts(options)}
|
||||||
- **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible.
|
- **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible.
|
||||||
|
|
||||||
## Interaction Details
|
## Interaction Details
|
||||||
@@ -803,17 +800,6 @@ function toolUsageInteractive(
|
|||||||
- **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim).`;
|
- **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toolUsageAiShell(options: OperationalGuidelinesOptions): string {
|
|
||||||
if (options.interactiveShellMode !== 'ai') return '';
|
|
||||||
return `
|
|
||||||
- **AI-Driven Interactive Shell:** Commands using \`wait_for_output_seconds\` auto-promote to background when they stall. Once promoted, use ${formatToolName(READ_SHELL_TOOL_NAME)} to see the terminal screen, then ${formatToolName(WRITE_TO_SHELL_TOOL_NAME)} to send text input and/or special keys (arrows, Enter, Ctrl-C, etc.).
|
|
||||||
- Set \`wait_for_output_seconds\` **low (2-5)** for commands that prompt for input (npx, installers, REPLs). Set **high (60+)** for long builds. Omit for instant commands.
|
|
||||||
- **Always read the screen before writing input.** The screen state tells you what the process is waiting for.
|
|
||||||
- When waiting for a command to finish (e.g. npm install), use ${formatToolName(READ_SHELL_TOOL_NAME)} with \`wait_seconds\` to delay before reading. Do NOT poll in a tight loop.
|
|
||||||
- **Clean up when done:** when your task is complete, kill background processes with ${formatToolName(WRITE_TO_SHELL_TOOL_NAME)} sending Ctrl-C, or note the PID for the user to clean up.
|
|
||||||
- You are the sole operator of promoted shells — the user cannot type into them.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toolUsageRememberingFacts(
|
function toolUsageRememberingFacts(
|
||||||
options: OperationalGuidelinesOptions,
|
options: OperationalGuidelinesOptions,
|
||||||
): string {
|
): string {
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ export interface ShellExecutionConfig {
|
|||||||
backgroundCompletionBehavior?: 'inject' | 'notify' | 'silent';
|
backgroundCompletionBehavior?: 'inject' | 'notify' | 'silent';
|
||||||
originalCommand?: string;
|
originalCommand?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
autoPromoteTimeoutMs?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -890,21 +889,6 @@ export class ShellExecutionService {
|
|||||||
sessionId: shellExecutionConfig.sessionId,
|
sessionId: shellExecutionConfig.sessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
let autoPromoteTimer: NodeJS.Timeout | undefined;
|
|
||||||
const resetAutoPromoteTimer = () => {
|
|
||||||
if (shellExecutionConfig.autoPromoteTimeoutMs !== undefined) {
|
|
||||||
if (autoPromoteTimer) clearTimeout(autoPromoteTimer);
|
|
||||||
autoPromoteTimer = setTimeout(() => {
|
|
||||||
ShellExecutionService.background(
|
|
||||||
ptyPid,
|
|
||||||
shellExecutionConfig.sessionId,
|
|
||||||
);
|
|
||||||
}, shellExecutionConfig.autoPromoteTimeoutMs);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
resetAutoPromoteTimer();
|
|
||||||
|
|
||||||
const result = ExecutionLifecycleService.attachExecution(ptyPid, {
|
const result = ExecutionLifecycleService.attachExecution(ptyPid, {
|
||||||
executionMethod: ptyInfo?.name ?? 'node-pty',
|
executionMethod: ptyInfo?.name ?? 'node-pty',
|
||||||
writeInput: (input) => {
|
writeInput: (input) => {
|
||||||
@@ -1082,7 +1066,6 @@ export class ShellExecutionService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleOutput = (data: Buffer) => {
|
const handleOutput = (data: Buffer) => {
|
||||||
resetAutoPromoteTimer();
|
|
||||||
processingChain = processingChain.then(
|
processingChain = processingChain.then(
|
||||||
() =>
|
() =>
|
||||||
new Promise<void>((resolveChunk) => {
|
new Promise<void>((resolveChunk) => {
|
||||||
@@ -1152,7 +1135,6 @@ export class ShellExecutionService {
|
|||||||
|
|
||||||
ptyProcess.onExit(
|
ptyProcess.onExit(
|
||||||
({ exitCode, signal }: { exitCode: number; signal?: number }) => {
|
({ exitCode, signal }: { exitCode: number; signal?: number }) => {
|
||||||
if (autoPromoteTimer) clearTimeout(autoPromoteTimer);
|
|
||||||
exited = true;
|
exited = true;
|
||||||
abortSignal.removeEventListener('abort', abortHandler);
|
abortSignal.removeEventListener('abort', abortHandler);
|
||||||
// Attempt to destroy the PTY to ensure FD is closed
|
// Attempt to destroy the PTY to ensure FD is closed
|
||||||
@@ -1238,7 +1220,6 @@ export class ShellExecutionService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const abortHandler = async () => {
|
const abortHandler = async () => {
|
||||||
if (autoPromoteTimer) clearTimeout(autoPromoteTimer);
|
|
||||||
if (ptyProcess.pid && !exited) {
|
if (ptyProcess.pid && !exited) {
|
||||||
await killProcessGroup({
|
await killProcessGroup({
|
||||||
pid: ptyPid,
|
pid: ptyPid,
|
||||||
@@ -1417,28 +1398,6 @@ export class ShellExecutionService {
|
|||||||
return ExecutionLifecycleService.subscribe(pid, listener);
|
return ExecutionLifecycleService.subscribe(pid, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the current rendered screen state of a running process.
|
|
||||||
* Returns the full terminal buffer text for PTY processes,
|
|
||||||
* or the accumulated output for child processes.
|
|
||||||
*
|
|
||||||
* @param pid The process ID of the target process.
|
|
||||||
* @returns The screen text, or null if the process is not found.
|
|
||||||
*/
|
|
||||||
static readScreen(pid: number): string | null {
|
|
||||||
const activePty = this.activePtys.get(pid);
|
|
||||||
if (activePty) {
|
|
||||||
return getFullBufferText(activePty.headlessTerminal);
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeChild = this.activeChildProcesses.get(pid);
|
|
||||||
if (activeChild) {
|
|
||||||
return activeChild.state.output;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes the pseudo-terminal (PTY) of a running process.
|
* Resizes the pseudo-terminal (PTY) of a running process.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -56,18 +56,6 @@ export const READ_FILE_PARAM_END_LINE = 'end_line';
|
|||||||
export const SHELL_TOOL_NAME = 'run_shell_command';
|
export const SHELL_TOOL_NAME = 'run_shell_command';
|
||||||
export const SHELL_PARAM_COMMAND = 'command';
|
export const SHELL_PARAM_COMMAND = 'command';
|
||||||
export const SHELL_PARAM_IS_BACKGROUND = 'is_background';
|
export const SHELL_PARAM_IS_BACKGROUND = 'is_background';
|
||||||
export const SHELL_PARAM_WAIT_SECONDS = 'wait_for_output_seconds';
|
|
||||||
|
|
||||||
// -- write_to_shell --
|
|
||||||
export const WRITE_TO_SHELL_TOOL_NAME = 'write_to_shell';
|
|
||||||
export const WRITE_TO_SHELL_PARAM_PID = 'pid';
|
|
||||||
export const WRITE_TO_SHELL_PARAM_INPUT = 'input';
|
|
||||||
export const WRITE_TO_SHELL_PARAM_SPECIAL_KEYS = 'special_keys';
|
|
||||||
|
|
||||||
// -- read_shell --
|
|
||||||
export const READ_SHELL_TOOL_NAME = 'read_shell';
|
|
||||||
export const READ_SHELL_PARAM_PID = 'pid';
|
|
||||||
export const READ_SHELL_PARAM_WAIT_SECONDS = 'wait_seconds';
|
|
||||||
|
|
||||||
// -- write_file --
|
// -- write_file --
|
||||||
export const WRITE_FILE_TOOL_NAME = 'write_file';
|
export const WRITE_FILE_TOOL_NAME = 'write_file';
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ export {
|
|||||||
LS_TOOL_NAME,
|
LS_TOOL_NAME,
|
||||||
READ_FILE_TOOL_NAME,
|
READ_FILE_TOOL_NAME,
|
||||||
SHELL_TOOL_NAME,
|
SHELL_TOOL_NAME,
|
||||||
WRITE_TO_SHELL_TOOL_NAME,
|
|
||||||
READ_SHELL_TOOL_NAME,
|
|
||||||
WRITE_FILE_TOOL_NAME,
|
WRITE_FILE_TOOL_NAME,
|
||||||
EDIT_TOOL_NAME,
|
EDIT_TOOL_NAME,
|
||||||
WEB_SEARCH_TOOL_NAME,
|
WEB_SEARCH_TOOL_NAME,
|
||||||
@@ -75,12 +73,6 @@ export {
|
|||||||
LS_PARAM_IGNORE,
|
LS_PARAM_IGNORE,
|
||||||
SHELL_PARAM_COMMAND,
|
SHELL_PARAM_COMMAND,
|
||||||
SHELL_PARAM_IS_BACKGROUND,
|
SHELL_PARAM_IS_BACKGROUND,
|
||||||
SHELL_PARAM_WAIT_SECONDS,
|
|
||||||
WRITE_TO_SHELL_PARAM_PID,
|
|
||||||
WRITE_TO_SHELL_PARAM_INPUT,
|
|
||||||
WRITE_TO_SHELL_PARAM_SPECIAL_KEYS,
|
|
||||||
READ_SHELL_PARAM_PID,
|
|
||||||
READ_SHELL_PARAM_WAIT_SECONDS,
|
|
||||||
WEB_SEARCH_PARAM_QUERY,
|
WEB_SEARCH_PARAM_QUERY,
|
||||||
WEB_FETCH_PARAM_PROMPT,
|
WEB_FETCH_PARAM_PROMPT,
|
||||||
READ_MANY_PARAM_INCLUDE,
|
READ_MANY_PARAM_INCLUDE,
|
||||||
@@ -257,21 +249,18 @@ export function getShellDefinition(
|
|||||||
enableInteractiveShell: boolean,
|
enableInteractiveShell: boolean,
|
||||||
enableEfficiency: boolean,
|
enableEfficiency: boolean,
|
||||||
enableToolSandboxing: boolean = false,
|
enableToolSandboxing: boolean = false,
|
||||||
interactiveShellMode?: string,
|
|
||||||
): ToolDefinition {
|
): ToolDefinition {
|
||||||
return {
|
return {
|
||||||
base: getShellDeclaration(
|
base: getShellDeclaration(
|
||||||
enableInteractiveShell,
|
enableInteractiveShell,
|
||||||
enableEfficiency,
|
enableEfficiency,
|
||||||
enableToolSandboxing,
|
enableToolSandboxing,
|
||||||
interactiveShellMode,
|
|
||||||
),
|
),
|
||||||
overrides: (modelId) =>
|
overrides: (modelId) =>
|
||||||
getToolSet(modelId).run_shell_command(
|
getToolSet(modelId).run_shell_command(
|
||||||
enableInteractiveShell,
|
enableInteractiveShell,
|
||||||
enableEfficiency,
|
enableEfficiency,
|
||||||
enableToolSandboxing,
|
enableToolSandboxing,
|
||||||
interactiveShellMode,
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
PARAM_DIR_PATH,
|
PARAM_DIR_PATH,
|
||||||
SHELL_PARAM_IS_BACKGROUND,
|
SHELL_PARAM_IS_BACKGROUND,
|
||||||
EXIT_PLAN_PARAM_PLAN_FILENAME,
|
EXIT_PLAN_PARAM_PLAN_FILENAME,
|
||||||
SHELL_PARAM_WAIT_SECONDS,
|
|
||||||
SKILL_PARAM_NAME,
|
SKILL_PARAM_NAME,
|
||||||
PARAM_ADDITIONAL_PERMISSIONS,
|
PARAM_ADDITIONAL_PERMISSIONS,
|
||||||
UPDATE_TOPIC_TOOL_NAME,
|
UPDATE_TOPIC_TOOL_NAME,
|
||||||
@@ -37,9 +36,7 @@ import {
|
|||||||
export function getShellToolDescription(
|
export function getShellToolDescription(
|
||||||
enableInteractiveShell: boolean,
|
enableInteractiveShell: boolean,
|
||||||
enableEfficiency: boolean,
|
enableEfficiency: boolean,
|
||||||
interactiveShellMode?: string,
|
|
||||||
): string {
|
): string {
|
||||||
const isAiMode = interactiveShellMode === 'ai';
|
|
||||||
const efficiencyGuidelines = enableEfficiency
|
const efficiencyGuidelines = enableEfficiency
|
||||||
? `
|
? `
|
||||||
|
|
||||||
@@ -59,11 +56,6 @@ export function getShellToolDescription(
|
|||||||
Background PIDs: Only included if background processes were started.
|
Background PIDs: Only included if background processes were started.
|
||||||
Process Group PGID: Only included if available.`;
|
Process Group PGID: Only included if available.`;
|
||||||
|
|
||||||
if (isAiMode) {
|
|
||||||
const autoPromoteInstructions = `Commands that do not complete within \`${SHELL_PARAM_WAIT_SECONDS}\` seconds are automatically promoted to background. Once promoted, use \`write_to_shell\` and \`read_shell\` to interact with the process. Do NOT use \`&\` to background commands.`;
|
|
||||||
return `This tool executes a given shell command as \`bash -c <command>\`. ${autoPromoteInstructions} Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`.${efficiencyGuidelines}${returnedInfo}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (os.platform() === 'win32') {
|
if (os.platform() === 'win32') {
|
||||||
const backgroundInstructions = enableInteractiveShell
|
const backgroundInstructions = enableInteractiveShell
|
||||||
? `To run a command in the background, set the \`${SHELL_PARAM_IS_BACKGROUND}\` parameter to true. Do NOT use PowerShell background constructs.`
|
? `To run a command in the background, set the \`${SHELL_PARAM_IS_BACKGROUND}\` parameter to true. Do NOT use PowerShell background constructs.`
|
||||||
@@ -94,33 +86,12 @@ export function getShellDeclaration(
|
|||||||
enableInteractiveShell: boolean,
|
enableInteractiveShell: boolean,
|
||||||
enableEfficiency: boolean,
|
enableEfficiency: boolean,
|
||||||
enableToolSandboxing: boolean = false,
|
enableToolSandboxing: boolean = false,
|
||||||
interactiveShellMode?: string,
|
|
||||||
): FunctionDeclaration {
|
): FunctionDeclaration {
|
||||||
const isAiMode = interactiveShellMode === 'ai';
|
|
||||||
|
|
||||||
// In AI mode, use wait_for_output_seconds instead of is_background
|
|
||||||
const backgroundParam = isAiMode
|
|
||||||
? {
|
|
||||||
[SHELL_PARAM_WAIT_SECONDS]: {
|
|
||||||
type: 'number' as const,
|
|
||||||
description:
|
|
||||||
'Max seconds to wait for command to complete before auto-promoting to background (default: 5). Set low (2-5) for commands likely to prompt for input (npx, installers, REPLs). Set high (60-300) for long builds or installs. Once promoted, use write_to_shell/read_shell to interact.',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
[SHELL_PARAM_IS_BACKGROUND]: {
|
|
||||||
type: 'boolean' as const,
|
|
||||||
description:
|
|
||||||
'Set to true if this command should be run in the background (e.g. for long-running servers or watchers). The command will be started, allowed to run for a brief moment to check for immediate errors, and then moved to the background.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: SHELL_TOOL_NAME,
|
name: SHELL_TOOL_NAME,
|
||||||
description: getShellToolDescription(
|
description: getShellToolDescription(
|
||||||
enableInteractiveShell,
|
enableInteractiveShell,
|
||||||
enableEfficiency,
|
enableEfficiency,
|
||||||
interactiveShellMode,
|
|
||||||
),
|
),
|
||||||
parametersJsonSchema: {
|
parametersJsonSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -149,7 +120,6 @@ export function getShellDeclaration(
|
|||||||
description:
|
description:
|
||||||
'Optional. Delay in milliseconds to wait after starting the process in the background. Useful to allow the process to start and generate initial output before returning.',
|
'Optional. Delay in milliseconds to wait after starting the process in the background. Useful to allow the process to start and generate initial output before returning.',
|
||||||
},
|
},
|
||||||
...backgroundParam,
|
|
||||||
...(enableToolSandboxing
|
...(enableToolSandboxing
|
||||||
? {
|
? {
|
||||||
[PARAM_ADDITIONAL_PERMISSIONS]: {
|
[PARAM_ADDITIONAL_PERMISSIONS]: {
|
||||||
|
|||||||
@@ -337,13 +337,11 @@ export const DEFAULT_LEGACY_SET: CoreToolSet = {
|
|||||||
enableInteractiveShell,
|
enableInteractiveShell,
|
||||||
enableEfficiency,
|
enableEfficiency,
|
||||||
enableToolSandboxing,
|
enableToolSandboxing,
|
||||||
interactiveShellMode,
|
|
||||||
) =>
|
) =>
|
||||||
getShellDeclaration(
|
getShellDeclaration(
|
||||||
enableInteractiveShell,
|
enableInteractiveShell,
|
||||||
enableEfficiency,
|
enableEfficiency,
|
||||||
enableToolSandboxing,
|
enableToolSandboxing,
|
||||||
interactiveShellMode,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
replace: {
|
replace: {
|
||||||
|
|||||||
@@ -344,13 +344,11 @@ export const GEMINI_3_SET: CoreToolSet = {
|
|||||||
enableInteractiveShell,
|
enableInteractiveShell,
|
||||||
enableEfficiency,
|
enableEfficiency,
|
||||||
enableToolSandboxing,
|
enableToolSandboxing,
|
||||||
interactiveShellMode,
|
|
||||||
) =>
|
) =>
|
||||||
getShellDeclaration(
|
getShellDeclaration(
|
||||||
enableInteractiveShell,
|
enableInteractiveShell,
|
||||||
enableEfficiency,
|
enableEfficiency,
|
||||||
enableToolSandboxing,
|
enableToolSandboxing,
|
||||||
interactiveShellMode,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
replace: {
|
replace: {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export interface CoreToolSet {
|
|||||||
enableInteractiveShell: boolean,
|
enableInteractiveShell: boolean,
|
||||||
enableEfficiency: boolean,
|
enableEfficiency: boolean,
|
||||||
enableToolSandboxing: boolean,
|
enableToolSandboxing: boolean,
|
||||||
interactiveShellMode?: string,
|
|
||||||
) => FunctionDeclaration;
|
) => FunctionDeclaration;
|
||||||
replace: FunctionDeclaration;
|
replace: FunctionDeclaration;
|
||||||
google_web_search: FunctionDeclaration;
|
google_web_search: FunctionDeclaration;
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2026 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
BaseDeclarativeTool,
|
|
||||||
BaseToolInvocation,
|
|
||||||
Kind,
|
|
||||||
type ToolInvocation,
|
|
||||||
type ToolResult,
|
|
||||||
} from './tools.js';
|
|
||||||
import { ShellExecutionService } from '../services/shellExecutionService.js';
|
|
||||||
import {
|
|
||||||
READ_SHELL_TOOL_NAME,
|
|
||||||
READ_SHELL_PARAM_PID,
|
|
||||||
READ_SHELL_PARAM_WAIT_SECONDS,
|
|
||||||
} from './tool-names.js';
|
|
||||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
|
||||||
|
|
||||||
export interface ReadShellParams {
|
|
||||||
pid: number;
|
|
||||||
wait_seconds?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReadShellToolInvocation extends BaseToolInvocation<
|
|
||||||
ReadShellParams,
|
|
||||||
ToolResult
|
|
||||||
> {
|
|
||||||
constructor(
|
|
||||||
params: ReadShellParams,
|
|
||||||
messageBus: MessageBus,
|
|
||||||
_toolName?: string,
|
|
||||||
_toolDisplayName?: string,
|
|
||||||
) {
|
|
||||||
super(params, messageBus, _toolName, _toolDisplayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
const waitPart =
|
|
||||||
this.params.wait_seconds !== undefined
|
|
||||||
? ` (after ${this.params.wait_seconds}s)`
|
|
||||||
: '';
|
|
||||||
return `read shell screen PID ${this.params.pid}${waitPart}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
|
||||||
const { pid, wait_seconds } = this.params;
|
|
||||||
|
|
||||||
// Wait before reading if requested
|
|
||||||
if (wait_seconds !== undefined && wait_seconds > 0) {
|
|
||||||
const waitMs = Math.min(wait_seconds, 30) * 1000; // Cap at 30s
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
const timer = setTimeout(resolve, waitMs);
|
|
||||||
const onAbort = () => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
signal.addEventListener('abort', onAbort, { once: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the PID is active
|
|
||||||
if (!ShellExecutionService.isPtyActive(pid)) {
|
|
||||||
return {
|
|
||||||
llmContent: `Error: No active process found with PID ${pid}. The process may have exited.`,
|
|
||||||
returnDisplay: `No active process with PID ${pid}.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const screen = ShellExecutionService.readScreen(pid);
|
|
||||||
if (screen === null) {
|
|
||||||
return {
|
|
||||||
llmContent: `Error: Could not read screen for PID ${pid}. The process may have exited.`,
|
|
||||||
returnDisplay: `Could not read screen for PID ${pid}.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
llmContent: screen,
|
|
||||||
returnDisplay: `Screen read from PID ${pid} (${screen.split('\n').length} lines).`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReadShellTool extends BaseDeclarativeTool<
|
|
||||||
ReadShellParams,
|
|
||||||
ToolResult
|
|
||||||
> {
|
|
||||||
static readonly Name = READ_SHELL_TOOL_NAME;
|
|
||||||
|
|
||||||
constructor(messageBus: MessageBus) {
|
|
||||||
super(
|
|
||||||
ReadShellTool.Name,
|
|
||||||
'ReadShell',
|
|
||||||
'Reads the current screen state of a running background shell process. Returns the rendered terminal screen as text, preserving the visual layout. Use after write_to_shell to see updated output, or to check progress of a running command.',
|
|
||||||
Kind.Read,
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
[READ_SHELL_PARAM_PID]: {
|
|
||||||
type: 'number',
|
|
||||||
description:
|
|
||||||
'The PID of the background process to read from. Obtained from a previous run_shell_command call that was auto-promoted to background or started with is_background=true.',
|
|
||||||
},
|
|
||||||
[READ_SHELL_PARAM_WAIT_SECONDS]: {
|
|
||||||
type: 'number',
|
|
||||||
description:
|
|
||||||
'Seconds to wait before reading the screen. Use this to let the process run for a while before checking output (e.g. wait for a build to finish). Max 30 seconds.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: [READ_SHELL_PARAM_PID],
|
|
||||||
},
|
|
||||||
messageBus,
|
|
||||||
false, // output is not markdown
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override validateToolParamValues(
|
|
||||||
params: ReadShellParams,
|
|
||||||
): string | null {
|
|
||||||
if (!params.pid || params.pid <= 0) {
|
|
||||||
return 'PID must be a positive number.';
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
params.wait_seconds !== undefined &&
|
|
||||||
(params.wait_seconds < 0 || params.wait_seconds > 30)
|
|
||||||
) {
|
|
||||||
return 'wait_seconds must be between 0 and 30.';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected createInvocation(
|
|
||||||
params: ReadShellParams,
|
|
||||||
messageBus: MessageBus,
|
|
||||||
_toolName?: string,
|
|
||||||
_toolDisplayName?: string,
|
|
||||||
): ToolInvocation<ReadShellParams, ToolResult> {
|
|
||||||
return new ReadShellToolInvocation(
|
|
||||||
params,
|
|
||||||
messageBus,
|
|
||||||
_toolName,
|
|
||||||
_toolDisplayName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -149,8 +149,6 @@ describe('ShellTool', () => {
|
|||||||
getShellBackgroundCompletionBehavior: vi.fn().mockReturnValue('silent'),
|
getShellBackgroundCompletionBehavior: vi.fn().mockReturnValue('silent'),
|
||||||
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
||||||
getSandboxEnabled: vi.fn().mockReturnValue(false),
|
getSandboxEnabled: vi.fn().mockReturnValue(false),
|
||||||
getInteractiveShellMode: vi.fn().mockReturnValue('off'),
|
|
||||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
|
||||||
sanitizationConfig: {},
|
sanitizationConfig: {},
|
||||||
get sandboxManager() {
|
get sandboxManager() {
|
||||||
return mockSandboxManager;
|
return mockSandboxManager;
|
||||||
@@ -424,7 +422,7 @@ describe('ShellTool', () => {
|
|||||||
|
|
||||||
expect(mockShellBackground).toHaveBeenCalledWith(
|
expect(mockShellBackground).toHaveBeenCalledWith(
|
||||||
12345,
|
12345,
|
||||||
'test-session-id',
|
'default',
|
||||||
'sleep 10',
|
'sleep 10',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -668,7 +666,7 @@ describe('ShellTool', () => {
|
|||||||
|
|
||||||
expect(mockShellBackground).toHaveBeenCalledWith(
|
expect(mockShellBackground).toHaveBeenCalledWith(
|
||||||
12345,
|
12345,
|
||||||
'test-session-id',
|
'default',
|
||||||
'sleep 10',
|
'sleep 10',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
|
|
||||||
import { getErrorMessage } from '../utils/errors.js';
|
import { getErrorMessage } from '../utils/errors.js';
|
||||||
import { summarizeToolOutput } from '../utils/summarizer.js';
|
import { summarizeToolOutput } from '../utils/summarizer.js';
|
||||||
import { formatShellOutput } from './shellOutputFormatter.js';
|
|
||||||
import {
|
import {
|
||||||
ShellExecutionService,
|
ShellExecutionService,
|
||||||
type ShellOutputEvent,
|
type ShellOutputEvent,
|
||||||
@@ -72,7 +71,6 @@ export interface ShellToolParams {
|
|||||||
is_background?: boolean;
|
is_background?: boolean;
|
||||||
delay_ms?: number;
|
delay_ms?: number;
|
||||||
[PARAM_ADDITIONAL_PERMISSIONS]?: SandboxPermissions;
|
[PARAM_ADDITIONAL_PERMISSIONS]?: SandboxPermissions;
|
||||||
wait_for_output_seconds?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ShellToolInvocation extends BaseToolInvocation<
|
export class ShellToolInvocation extends BaseToolInvocation<
|
||||||
@@ -80,7 +78,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
ToolResult
|
ToolResult
|
||||||
> {
|
> {
|
||||||
private proactivePermissionsConfirmed?: SandboxPermissions;
|
private proactivePermissionsConfirmed?: SandboxPermissions;
|
||||||
private _autoPromoteTimer?: NodeJS.Timeout;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly context: AgentLoopContext,
|
private readonly context: AgentLoopContext,
|
||||||
@@ -226,12 +223,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
}
|
}
|
||||||
|
|
||||||
override getExplanation(): string {
|
override getExplanation(): string {
|
||||||
let explanation = this.getContextualDetails().trim();
|
return this.getContextualDetails().trim();
|
||||||
const isAiMode = this.context.config.getInteractiveShellMode() === 'ai';
|
|
||||||
if (this.params.wait_for_output_seconds !== undefined || isAiMode) {
|
|
||||||
explanation += ` [auto-background after ${this.params.wait_for_output_seconds ?? 5}s]`;
|
|
||||||
}
|
|
||||||
return explanation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override getPolicyUpdateOptions(
|
override getPolicyUpdateOptions(
|
||||||
@@ -505,21 +497,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentPid: number | undefined;
|
|
||||||
const isAiMode = this.context.config.getInteractiveShellMode() === 'ai';
|
|
||||||
const shouldAutoPromote =
|
|
||||||
this.params.wait_for_output_seconds !== undefined || isAiMode;
|
|
||||||
const waitMs = (this.params.wait_for_output_seconds ?? 5) * 1000;
|
|
||||||
|
|
||||||
const resetAutoPromoteTimer = () => {
|
|
||||||
if (shouldAutoPromote && currentPid) {
|
|
||||||
if (this._autoPromoteTimer) clearTimeout(this._autoPromoteTimer);
|
|
||||||
this._autoPromoteTimer = setTimeout(() => {
|
|
||||||
ShellExecutionService.background(currentPid!);
|
|
||||||
}, waitMs);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
signal.addEventListener('abort', onAbort, { once: true });
|
signal.addEventListener('abort', onAbort, { once: true });
|
||||||
timeoutController.signal.addEventListener('abort', onAbort, {
|
timeoutController.signal.addEventListener('abort', onAbort, {
|
||||||
once: true,
|
once: true,
|
||||||
@@ -534,7 +511,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
cwd,
|
cwd,
|
||||||
(event: ShellOutputEvent) => {
|
(event: ShellOutputEvent) => {
|
||||||
resetTimeout(); // Reset timeout on any event
|
resetTimeout(); // Reset timeout on any event
|
||||||
resetAutoPromoteTimer(); // Reset auto-promote on any event
|
|
||||||
if (!updateOutput) {
|
if (!updateOutput) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -606,7 +582,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
backgroundCompletionBehavior:
|
backgroundCompletionBehavior:
|
||||||
this.context.config.getShellBackgroundCompletionBehavior(),
|
this.context.config.getShellBackgroundCompletionBehavior(),
|
||||||
originalCommand: strippedCommand,
|
originalCommand: strippedCommand,
|
||||||
autoPromoteTimeoutMs: shouldAutoPromote ? waitMs : undefined,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -643,11 +618,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In AI mode with wait_for_output_seconds, set up auto-promotion timer.
|
|
||||||
// When the timer fires, promote to background instead of cancelling.
|
|
||||||
currentPid = pid;
|
|
||||||
resetAutoPromoteTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await resultPromise;
|
const result = await resultPromise;
|
||||||
@@ -688,73 +658,95 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let data: BackgroundExecutionData | undefined;
|
||||||
|
|
||||||
|
let llmContent = '';
|
||||||
let timeoutMessage = '';
|
let timeoutMessage = '';
|
||||||
if (result.aborted) {
|
if (result.aborted) {
|
||||||
if (timeoutController.signal.aborted) {
|
if (timeoutController.signal.aborted) {
|
||||||
timeoutMessage = `Command was automatically cancelled because it exceeded the timeout of ${(
|
timeoutMessage = `Command was automatically cancelled because it exceeded the timeout of ${(
|
||||||
timeoutMs / 60000
|
timeoutMs / 60000
|
||||||
).toFixed(1)} minutes without output.`;
|
).toFixed(1)} minutes without output.`;
|
||||||
|
llmContent = timeoutMessage;
|
||||||
|
} else {
|
||||||
|
llmContent =
|
||||||
|
'Command was cancelled by user before it could complete.';
|
||||||
}
|
}
|
||||||
}
|
if (result.output.trim()) {
|
||||||
|
llmContent += ` Below is the output before it was cancelled:\n${result.output}`;
|
||||||
const formatterOutput = formatShellOutput({
|
} else {
|
||||||
params: this.params,
|
llmContent += ' There was no output before it was cancelled.';
|
||||||
result,
|
|
||||||
debugMode: this.context.config.getDebugMode(),
|
|
||||||
backgroundPIDs,
|
|
||||||
isAiMode,
|
|
||||||
timeoutMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
let data: BackgroundExecutionData | undefined;
|
|
||||||
data = formatterOutput.data as BackgroundExecutionData | undefined;
|
|
||||||
let returnDisplay: string | AnsiOutput = formatterOutput.returnDisplay;
|
|
||||||
let llmContent = formatterOutput.llmContent;
|
|
||||||
|
|
||||||
if (!this.context.config.getDebugMode()) {
|
|
||||||
if (
|
|
||||||
!this.params.is_background &&
|
|
||||||
!result.backgrounded &&
|
|
||||||
!result.aborted
|
|
||||||
) {
|
|
||||||
if (result.output.trim() || result.ansiOutput) {
|
|
||||||
returnDisplay =
|
|
||||||
result.ansiOutput && result.ansiOutput.length > 0
|
|
||||||
? result.ansiOutput
|
|
||||||
: result.output;
|
|
||||||
} else {
|
|
||||||
if (result.signal) {
|
|
||||||
returnDisplay = `Command terminated by signal: ${result.signal}`;
|
|
||||||
} else if (result.error) {
|
|
||||||
returnDisplay = `Command failed: ${getErrorMessage(result.error)}`;
|
|
||||||
} else if (result.exitCode !== null && result.exitCode !== 0) {
|
|
||||||
returnDisplay = `Command exited with code: ${result.exitCode}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
} else if (this.params.is_background || result.backgrounded) {
|
||||||
|
llmContent = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`;
|
||||||
// Replace wrapper command with actual command in error messages
|
|
||||||
if (result.error && !result.aborted) {
|
|
||||||
llmContent = llmContent.replaceAll(
|
|
||||||
commandToExecute,
|
|
||||||
this.params.command,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update data with specific things needed by ShellTool
|
|
||||||
if (this.params.is_background || result.backgrounded) {
|
|
||||||
data = {
|
data = {
|
||||||
...data,
|
pid: result.pid,
|
||||||
initialOutput: result.output,
|
|
||||||
pid: result.pid!,
|
|
||||||
command: this.params.command,
|
command: this.params.command,
|
||||||
|
initialOutput: result.output,
|
||||||
};
|
};
|
||||||
} else if (result.exitCode !== null && result.exitCode !== 0) {
|
} else {
|
||||||
data = {
|
// Create a formatted error string for display, replacing the wrapper command
|
||||||
exitCode: result.exitCode,
|
// with the user-facing command.
|
||||||
isError: true,
|
const llmContentParts = [`Output: ${result.output || '(empty)'}`];
|
||||||
} as BackgroundExecutionData;
|
|
||||||
|
if (result.error) {
|
||||||
|
const finalError = result.error.message.replaceAll(
|
||||||
|
commandToExecute,
|
||||||
|
this.params.command,
|
||||||
|
);
|
||||||
|
llmContentParts.push(`Error: ${finalError}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.exitCode !== null && result.exitCode !== 0) {
|
||||||
|
llmContentParts.push(`Exit Code: ${result.exitCode}`);
|
||||||
|
data = {
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.signal) {
|
||||||
|
llmContentParts.push(`Signal: ${result.signal}`);
|
||||||
|
}
|
||||||
|
if (backgroundPIDs.length) {
|
||||||
|
llmContentParts.push(`Background PIDs: ${backgroundPIDs.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (result.pid) {
|
||||||
|
llmContentParts.push(`Process Group PGID: ${result.pid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
llmContent = llmContentParts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
let returnDisplay: string | AnsiOutput = '';
|
||||||
|
if (this.context.config.getDebugMode()) {
|
||||||
|
returnDisplay = llmContent;
|
||||||
|
} else {
|
||||||
|
if (this.params.is_background || result.backgrounded) {
|
||||||
|
returnDisplay = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`;
|
||||||
|
} else if (result.aborted) {
|
||||||
|
const cancelMsg = timeoutMessage || 'Command cancelled by user.';
|
||||||
|
if (result.output.trim()) {
|
||||||
|
returnDisplay = `${cancelMsg}\n\nOutput before cancellation:\n${result.output}`;
|
||||||
|
} else {
|
||||||
|
returnDisplay = cancelMsg;
|
||||||
|
}
|
||||||
|
} else if (result.output.trim() || result.ansiOutput) {
|
||||||
|
returnDisplay =
|
||||||
|
result.ansiOutput && result.ansiOutput.length > 0
|
||||||
|
? result.ansiOutput
|
||||||
|
: result.output;
|
||||||
|
} else {
|
||||||
|
if (result.signal) {
|
||||||
|
returnDisplay = `Command terminated by signal: ${result.signal}`;
|
||||||
|
} else if (result.error) {
|
||||||
|
returnDisplay = `Command failed: ${getErrorMessage(result.error)}`;
|
||||||
|
} else if (result.exitCode !== null && result.exitCode !== 0) {
|
||||||
|
returnDisplay = `Command exited with code: ${result.exitCode}`;
|
||||||
|
}
|
||||||
|
// If output is empty and command succeeded (code 0, no error/signal/abort),
|
||||||
|
// returnDisplay will remain empty, which is fine.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heuristic Sandbox Denial Detection
|
// Heuristic Sandbox Denial Detection
|
||||||
@@ -937,8 +929,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||||
const autoTimer = this._autoPromoteTimer;
|
|
||||||
if (autoTimer) clearTimeout(autoTimer);
|
|
||||||
signal.removeEventListener('abort', onAbort);
|
signal.removeEventListener('abort', onAbort);
|
||||||
timeoutController.signal.removeEventListener('abort', onAbort);
|
timeoutController.signal.removeEventListener('abort', onAbort);
|
||||||
try {
|
try {
|
||||||
@@ -1017,7 +1007,6 @@ export class ShellTool extends BaseDeclarativeTool<
|
|||||||
this.context.config.getEnableInteractiveShell(),
|
this.context.config.getEnableInteractiveShell(),
|
||||||
this.context.config.getEnableShellOutputEfficiency(),
|
this.context.config.getEnableShellOutputEfficiency(),
|
||||||
this.context.config.getSandboxEnabled(),
|
this.context.config.getSandboxEnabled(),
|
||||||
this.context.config.getInteractiveShellMode(),
|
|
||||||
);
|
);
|
||||||
return resolveToolDeclaration(definition, modelId);
|
return resolveToolDeclaration(definition, modelId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2026 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type ShellExecutionResult } from '../services/shellExecutionService.js';
|
|
||||||
import { type ShellToolParams } from './shell.js';
|
|
||||||
|
|
||||||
export interface FormatShellOutputOptions {
|
|
||||||
params: ShellToolParams;
|
|
||||||
result: ShellExecutionResult;
|
|
||||||
debugMode: boolean;
|
|
||||||
timeoutMessage?: string;
|
|
||||||
backgroundPIDs: number[];
|
|
||||||
summarizedOutput?: string;
|
|
||||||
isAiMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FormattedShellOutput {
|
|
||||||
llmContent: string;
|
|
||||||
returnDisplay: string;
|
|
||||||
data: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatShellOutput(
|
|
||||||
options: FormatShellOutputOptions,
|
|
||||||
): FormattedShellOutput {
|
|
||||||
const {
|
|
||||||
params,
|
|
||||||
result,
|
|
||||||
debugMode,
|
|
||||||
timeoutMessage,
|
|
||||||
backgroundPIDs,
|
|
||||||
summarizedOutput,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
let llmContent = '';
|
|
||||||
let data: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
if (result.aborted) {
|
|
||||||
llmContent = timeoutMessage || 'Command cancelled by user.';
|
|
||||||
if (result.output.trim()) {
|
|
||||||
llmContent += ` Below is the output before it was cancelled:\n${result.output}`;
|
|
||||||
} else {
|
|
||||||
llmContent += ' There was no output before it was cancelled.';
|
|
||||||
}
|
|
||||||
} else if (params.is_background || result.backgrounded) {
|
|
||||||
const isAutoPromoted = result.backgrounded && !params.is_background;
|
|
||||||
if (isAutoPromoted) {
|
|
||||||
llmContent = `Command auto-promoted to background (PID: ${result.pid}). The process is still running. To check its screen state, call the read_shell tool with pid ${result.pid}. To send input or keystrokes, call the write_to_shell tool with pid ${result.pid}. If the process does not exit on its own when done, kill it with write_to_shell using special_keys=["Ctrl-C"].`;
|
|
||||||
} else {
|
|
||||||
llmContent = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`;
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
pid: result.pid,
|
|
||||||
command: params.command,
|
|
||||||
directory: params.dir_path,
|
|
||||||
backgrounded: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const llmContentParts: string[] = [];
|
|
||||||
|
|
||||||
let content = summarizedOutput ?? result.output.trim();
|
|
||||||
if (!content) {
|
|
||||||
content = '(empty)';
|
|
||||||
}
|
|
||||||
|
|
||||||
llmContentParts.push(`Output: ${content}`);
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
llmContentParts.push(`Error: ${result.error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.exitCode !== null && result.exitCode !== 0) {
|
|
||||||
llmContentParts.push(`Exit Code: ${result.exitCode}`);
|
|
||||||
}
|
|
||||||
if (result.signal !== null) {
|
|
||||||
llmContentParts.push(`Signal: ${result.signal}`);
|
|
||||||
}
|
|
||||||
if (backgroundPIDs.length) {
|
|
||||||
llmContentParts.push(`Background PIDs: ${backgroundPIDs.join(', ')}`);
|
|
||||||
}
|
|
||||||
if (result.pid) {
|
|
||||||
llmContentParts.push(`Process Group PGID: ${result.pid}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
llmContent = llmContentParts.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
let returnDisplay = '';
|
|
||||||
if (debugMode) {
|
|
||||||
returnDisplay = llmContent;
|
|
||||||
} else {
|
|
||||||
if (params.is_background || result.backgrounded) {
|
|
||||||
const isAutoPromotedDisplay =
|
|
||||||
result.backgrounded && !params.is_background;
|
|
||||||
if (isAutoPromotedDisplay) {
|
|
||||||
returnDisplay = `Command auto-promoted to background (PID: ${result.pid}).`;
|
|
||||||
} else {
|
|
||||||
returnDisplay = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`;
|
|
||||||
}
|
|
||||||
} else if (result.aborted) {
|
|
||||||
const cancelMsg = timeoutMessage || 'Command cancelled by user.';
|
|
||||||
if (result.output.trim()) {
|
|
||||||
returnDisplay = `${cancelMsg}\n\nOutput before cancellation:\n${result.output}`;
|
|
||||||
} else {
|
|
||||||
returnDisplay = cancelMsg;
|
|
||||||
}
|
|
||||||
} else if (result.error) {
|
|
||||||
returnDisplay = `Command failed: ${result.error.message}`;
|
|
||||||
} else if (result.exitCode !== 0 && result.exitCode !== null) {
|
|
||||||
returnDisplay = `Command exited with code ${result.exitCode}`;
|
|
||||||
if (result.output.trim()) {
|
|
||||||
returnDisplay += `\n\n${result.output}`;
|
|
||||||
}
|
|
||||||
} else if (summarizedOutput) {
|
|
||||||
returnDisplay = `Command succeeded. Output summarized:\n${summarizedOutput}`;
|
|
||||||
} else {
|
|
||||||
returnDisplay = `Command succeeded.`;
|
|
||||||
if (result.output.trim()) {
|
|
||||||
returnDisplay += `\n\n${result.output}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { llmContent, returnDisplay, data };
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,6 @@ import {
|
|||||||
LS_TOOL_NAME,
|
LS_TOOL_NAME,
|
||||||
READ_FILE_TOOL_NAME,
|
READ_FILE_TOOL_NAME,
|
||||||
SHELL_TOOL_NAME,
|
SHELL_TOOL_NAME,
|
||||||
WRITE_TO_SHELL_TOOL_NAME,
|
|
||||||
READ_SHELL_TOOL_NAME,
|
|
||||||
WRITE_FILE_TOOL_NAME,
|
WRITE_FILE_TOOL_NAME,
|
||||||
EDIT_TOOL_NAME,
|
EDIT_TOOL_NAME,
|
||||||
WEB_SEARCH_TOOL_NAME,
|
WEB_SEARCH_TOOL_NAME,
|
||||||
@@ -54,12 +52,6 @@ import {
|
|||||||
LS_PARAM_IGNORE,
|
LS_PARAM_IGNORE,
|
||||||
SHELL_PARAM_COMMAND,
|
SHELL_PARAM_COMMAND,
|
||||||
SHELL_PARAM_IS_BACKGROUND,
|
SHELL_PARAM_IS_BACKGROUND,
|
||||||
SHELL_PARAM_WAIT_SECONDS,
|
|
||||||
WRITE_TO_SHELL_PARAM_PID,
|
|
||||||
WRITE_TO_SHELL_PARAM_INPUT,
|
|
||||||
WRITE_TO_SHELL_PARAM_SPECIAL_KEYS,
|
|
||||||
READ_SHELL_PARAM_PID,
|
|
||||||
READ_SHELL_PARAM_WAIT_SECONDS,
|
|
||||||
WEB_SEARCH_PARAM_QUERY,
|
WEB_SEARCH_PARAM_QUERY,
|
||||||
WEB_FETCH_PARAM_PROMPT,
|
WEB_FETCH_PARAM_PROMPT,
|
||||||
READ_MANY_PARAM_INCLUDE,
|
READ_MANY_PARAM_INCLUDE,
|
||||||
@@ -98,8 +90,6 @@ export {
|
|||||||
LS_TOOL_NAME,
|
LS_TOOL_NAME,
|
||||||
READ_FILE_TOOL_NAME,
|
READ_FILE_TOOL_NAME,
|
||||||
SHELL_TOOL_NAME,
|
SHELL_TOOL_NAME,
|
||||||
WRITE_TO_SHELL_TOOL_NAME,
|
|
||||||
READ_SHELL_TOOL_NAME,
|
|
||||||
WRITE_FILE_TOOL_NAME,
|
WRITE_FILE_TOOL_NAME,
|
||||||
EDIT_TOOL_NAME,
|
EDIT_TOOL_NAME,
|
||||||
WEB_SEARCH_TOOL_NAME,
|
WEB_SEARCH_TOOL_NAME,
|
||||||
@@ -146,12 +136,6 @@ export {
|
|||||||
LS_PARAM_IGNORE,
|
LS_PARAM_IGNORE,
|
||||||
SHELL_PARAM_COMMAND,
|
SHELL_PARAM_COMMAND,
|
||||||
SHELL_PARAM_IS_BACKGROUND,
|
SHELL_PARAM_IS_BACKGROUND,
|
||||||
SHELL_PARAM_WAIT_SECONDS,
|
|
||||||
WRITE_TO_SHELL_PARAM_PID,
|
|
||||||
WRITE_TO_SHELL_PARAM_INPUT,
|
|
||||||
WRITE_TO_SHELL_PARAM_SPECIAL_KEYS,
|
|
||||||
READ_SHELL_PARAM_PID,
|
|
||||||
READ_SHELL_PARAM_WAIT_SECONDS,
|
|
||||||
WEB_SEARCH_PARAM_QUERY,
|
WEB_SEARCH_PARAM_QUERY,
|
||||||
WEB_FETCH_PARAM_PROMPT,
|
WEB_FETCH_PARAM_PROMPT,
|
||||||
READ_MANY_PARAM_INCLUDE,
|
READ_MANY_PARAM_INCLUDE,
|
||||||
@@ -195,7 +179,6 @@ export const TOOLS_REQUIRING_NARROWING = new Set([
|
|||||||
WRITE_FILE_TOOL_NAME,
|
WRITE_FILE_TOOL_NAME,
|
||||||
EDIT_TOOL_NAME,
|
EDIT_TOOL_NAME,
|
||||||
SHELL_TOOL_NAME,
|
SHELL_TOOL_NAME,
|
||||||
WRITE_TO_SHELL_TOOL_NAME,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const TRACKER_CREATE_TASK_TOOL_NAME = 'tracker_create_task';
|
export const TRACKER_CREATE_TASK_TOOL_NAME = 'tracker_create_task';
|
||||||
@@ -268,8 +251,6 @@ export const ALL_BUILTIN_TOOL_NAMES = [
|
|||||||
WEB_FETCH_TOOL_NAME,
|
WEB_FETCH_TOOL_NAME,
|
||||||
EDIT_TOOL_NAME,
|
EDIT_TOOL_NAME,
|
||||||
SHELL_TOOL_NAME,
|
SHELL_TOOL_NAME,
|
||||||
WRITE_TO_SHELL_TOOL_NAME,
|
|
||||||
READ_SHELL_TOOL_NAME,
|
|
||||||
GREP_TOOL_NAME,
|
GREP_TOOL_NAME,
|
||||||
READ_MANY_FILES_TOOL_NAME,
|
READ_MANY_FILES_TOOL_NAME,
|
||||||
READ_FILE_TOOL_NAME,
|
READ_FILE_TOOL_NAME,
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2026 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
type ToolConfirmationOutcome,
|
|
||||||
BaseDeclarativeTool,
|
|
||||||
BaseToolInvocation,
|
|
||||||
Kind,
|
|
||||||
type ToolInvocation,
|
|
||||||
type ToolResult,
|
|
||||||
type ToolCallConfirmationDetails,
|
|
||||||
type ToolExecuteConfirmationDetails,
|
|
||||||
} from './tools.js';
|
|
||||||
import { ShellExecutionService } from '../services/shellExecutionService.js';
|
|
||||||
import {
|
|
||||||
WRITE_TO_SHELL_TOOL_NAME,
|
|
||||||
WRITE_TO_SHELL_PARAM_PID,
|
|
||||||
WRITE_TO_SHELL_PARAM_INPUT,
|
|
||||||
WRITE_TO_SHELL_PARAM_SPECIAL_KEYS,
|
|
||||||
} from './tool-names.js';
|
|
||||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping of named special keys to their ANSI escape sequences.
|
|
||||||
*/
|
|
||||||
const SPECIAL_KEY_MAP: Record<string, string> = {
|
|
||||||
Enter: '\r',
|
|
||||||
Tab: '\t',
|
|
||||||
Up: '\x1b[A',
|
|
||||||
Down: '\x1b[B',
|
|
||||||
Left: '\x1b[D',
|
|
||||||
Right: '\x1b[C',
|
|
||||||
Escape: '\x1b',
|
|
||||||
Backspace: '\x7f',
|
|
||||||
'Ctrl-C': '\x03',
|
|
||||||
'Ctrl-D': '\x04',
|
|
||||||
'Ctrl-Z': '\x1a',
|
|
||||||
Space: ' ',
|
|
||||||
Delete: '\x1b[3~',
|
|
||||||
Home: '\x1b[H',
|
|
||||||
End: '\x1b[F',
|
|
||||||
};
|
|
||||||
|
|
||||||
const VALID_SPECIAL_KEYS = Object.keys(SPECIAL_KEY_MAP);
|
|
||||||
|
|
||||||
/** Delay in ms to wait after writing input for the process to react. */
|
|
||||||
const POST_INPUT_DELAY_MS = 150;
|
|
||||||
|
|
||||||
export interface WriteToShellParams {
|
|
||||||
pid: number;
|
|
||||||
input?: string;
|
|
||||||
special_keys?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WriteToShellToolInvocation extends BaseToolInvocation<
|
|
||||||
WriteToShellParams,
|
|
||||||
ToolResult
|
|
||||||
> {
|
|
||||||
constructor(
|
|
||||||
params: WriteToShellParams,
|
|
||||||
messageBus: MessageBus,
|
|
||||||
_toolName?: string,
|
|
||||||
_toolDisplayName?: string,
|
|
||||||
) {
|
|
||||||
super(params, messageBus, _toolName, _toolDisplayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
const parts: string[] = [`write to shell PID ${this.params.pid}`];
|
|
||||||
if (this.params.input) {
|
|
||||||
const display =
|
|
||||||
this.params.input.length > 50
|
|
||||||
? `${this.params.input.substring(0, 50)}...`
|
|
||||||
: this.params.input;
|
|
||||||
parts.push(`input: "${display}"`);
|
|
||||||
}
|
|
||||||
if (this.params.special_keys?.length) {
|
|
||||||
parts.push(`keys: [${this.params.special_keys.join(', ')}]`);
|
|
||||||
}
|
|
||||||
return parts.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async getConfirmationDetails(
|
|
||||||
_abortSignal: AbortSignal,
|
|
||||||
): Promise<ToolCallConfirmationDetails | false> {
|
|
||||||
const confirmationDetails: ToolExecuteConfirmationDetails = {
|
|
||||||
type: 'exec',
|
|
||||||
title: 'Confirm Shell Input',
|
|
||||||
command: this.getDescription(),
|
|
||||||
rootCommand: 'write_to_shell',
|
|
||||||
rootCommands: ['write_to_shell'],
|
|
||||||
onConfirm: async (_outcome: ToolConfirmationOutcome) => {
|
|
||||||
// Policy updates handled centrally
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return confirmationDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
|
||||||
const { pid, input, special_keys } = this.params;
|
|
||||||
|
|
||||||
// Validate the PID is active
|
|
||||||
if (!ShellExecutionService.isPtyActive(pid)) {
|
|
||||||
return {
|
|
||||||
llmContent: `Error: No active process found with PID ${pid}. The process may have exited.`,
|
|
||||||
returnDisplay: `No active process with PID ${pid}.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate special keys
|
|
||||||
if (special_keys?.length) {
|
|
||||||
const invalidKeys = special_keys.filter(
|
|
||||||
(k) => !VALID_SPECIAL_KEYS.includes(k),
|
|
||||||
);
|
|
||||||
if (invalidKeys.length > 0) {
|
|
||||||
return {
|
|
||||||
llmContent: `Error: Invalid special keys: ${invalidKeys.join(', ')}. Valid keys are: ${VALID_SPECIAL_KEYS.join(', ')}`,
|
|
||||||
returnDisplay: `Invalid special keys: ${invalidKeys.join(', ')}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send text input
|
|
||||||
if (input) {
|
|
||||||
ShellExecutionService.writeToPty(pid, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send special keys
|
|
||||||
if (special_keys?.length) {
|
|
||||||
for (const key of special_keys) {
|
|
||||||
const sequence = SPECIAL_KEY_MAP[key];
|
|
||||||
if (sequence) {
|
|
||||||
ShellExecutionService.writeToPty(pid, sequence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait briefly for the process to react
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, POST_INPUT_DELAY_MS));
|
|
||||||
|
|
||||||
// Read the screen after writing
|
|
||||||
const screen = ShellExecutionService.readScreen(pid);
|
|
||||||
if (screen === null) {
|
|
||||||
return {
|
|
||||||
llmContent: `Input sent, but the process (PID ${pid}) has exited.`,
|
|
||||||
returnDisplay: `Process exited after input.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
llmContent: `Input sent to PID ${pid}. Current screen:\n${screen}`,
|
|
||||||
returnDisplay: `Input sent to PID ${pid}.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WriteToShellTool extends BaseDeclarativeTool<
|
|
||||||
WriteToShellParams,
|
|
||||||
ToolResult
|
|
||||||
> {
|
|
||||||
static readonly Name = WRITE_TO_SHELL_TOOL_NAME;
|
|
||||||
|
|
||||||
constructor(messageBus: MessageBus) {
|
|
||||||
super(
|
|
||||||
WriteToShellTool.Name,
|
|
||||||
'WriteToShell',
|
|
||||||
'Sends input to a running background shell process. Use this to interact with TUI applications, REPLs, and interactive commands. After writing, the current screen state is returned. Works with processes that were auto-promoted to background via wait_for_output_seconds or started with is_background=true.',
|
|
||||||
Kind.Execute,
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
[WRITE_TO_SHELL_PARAM_PID]: {
|
|
||||||
type: 'number',
|
|
||||||
description:
|
|
||||||
'The PID of the background process to write to. Obtained from a previous run_shell_command call that was auto-promoted to background or started with is_background=true.',
|
|
||||||
},
|
|
||||||
[WRITE_TO_SHELL_PARAM_INPUT]: {
|
|
||||||
type: 'string',
|
|
||||||
description:
|
|
||||||
'(OPTIONAL) Text to send to the process. This is literal text typed into the terminal.',
|
|
||||||
},
|
|
||||||
[WRITE_TO_SHELL_PARAM_SPECIAL_KEYS]: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
enum: VALID_SPECIAL_KEYS,
|
|
||||||
},
|
|
||||||
description:
|
|
||||||
'(OPTIONAL) Named special keys to send after the input text. Each key is sent in sequence. Examples: ["Enter"], ["Tab"], ["Up", "Enter"], ["Ctrl-C"].',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: [WRITE_TO_SHELL_PARAM_PID],
|
|
||||||
},
|
|
||||||
messageBus,
|
|
||||||
false, // output is not markdown
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override validateToolParamValues(
|
|
||||||
params: WriteToShellParams,
|
|
||||||
): string | null {
|
|
||||||
if (!params.pid || params.pid <= 0) {
|
|
||||||
return 'PID must be a positive number.';
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!params.input &&
|
|
||||||
(!params.special_keys || !params.special_keys.length)
|
|
||||||
) {
|
|
||||||
return 'At least one of input or special_keys must be provided.';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected createInvocation(
|
|
||||||
params: WriteToShellParams,
|
|
||||||
messageBus: MessageBus,
|
|
||||||
_toolName?: string,
|
|
||||||
_toolDisplayName?: string,
|
|
||||||
): ToolInvocation<WriteToShellParams, ToolResult> {
|
|
||||||
return new WriteToShellToolInvocation(
|
|
||||||
params,
|
|
||||||
messageBus,
|
|
||||||
_toolName,
|
|
||||||
_toolDisplayName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user