Merge pull request #286607 from microsoft/tyriar/286598

Add enforceModelTimeout setting
This commit is contained in:
Daniel Imms
2026-01-08 12:16:54 -08:00
committed by GitHub
2 changed files with 49 additions and 5 deletions

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import type { IMarker as IXtermMarker } from '@xterm/xterm';
import { timeout } from '../../../../../../base/common/async.js';
import { timeout, type CancelablePromise } from '../../../../../../base/common/async.js';
import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { CancellationError } from '../../../../../../base/common/errors.js';
@@ -220,11 +220,16 @@ export async function createRunInTerminalToolData(
type: 'boolean',
description: 'Whether the command starts a background process. If true, the command will run in the background and you will not see the output. If false, the tool call will block on the command finishing, and then you will get the output. Examples of background processes: building in watch mode, starting a server. You can check the output of a background process later on by using get_terminal_output.'
},
timeout: {
type: 'number',
description: 'An optional timeout in milliseconds. When provided, the tool will stop tracking the command after this duration and return the output collected so far. Be conservative with the timeout duration, give enough time that the command would complete on a low-end machine. Use 0 for no timeout. If it\'s not clear how long the command will take then use 0 to avoid prematurely terminating it, never guess too low.',
},
},
required: [
'command',
'explanation',
'isBackground',
'timeout',
]
}
};
@@ -249,6 +254,7 @@ export interface IRunInTerminalInputParams {
command: string;
explanation: string;
isBackground: boolean;
timeout?: number;
}
/**
@@ -662,7 +668,24 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
let outputLineCount = -1;
let exitCode: number | undefined;
let altBufferResult: IToolResult | undefined;
let didTimeout = false;
let timeoutPromise: CancelablePromise<void> | undefined;
const executeCancellation = store.add(new CancellationTokenSource(token));
// Set up timeout if provided and the setting is enabled
if (args.timeout !== undefined && args.timeout > 0) {
const shouldEnforceTimeout = this._configurationService.getValue(TerminalChatAgentToolsSettingId.EnforceTimeoutFromModel) === true;
if (shouldEnforceTimeout) {
timeoutPromise = timeout(args.timeout);
timeoutPromise.then(() => {
if (!executeCancellation.token.isCancellationRequested) {
didTimeout = true;
executeCancellation.cancel();
}
});
}
}
try {
let strategy: ITerminalExecuteStrategy;
switch (toolTerminal.shellIntegrationQuality) {
@@ -740,11 +763,21 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
}
} catch (e) {
this._logService.debug(`RunInTerminalTool: Threw exception`);
toolTerminal.instance.dispose();
error = e instanceof CancellationError ? 'canceled' : 'unexpectedException';
throw e;
// Handle timeout case - get output collected so far and return it
if (didTimeout && e instanceof CancellationError) {
this._logService.debug(`RunInTerminalTool: Timeout reached, returning output collected so far`);
error = 'timeout';
const timeoutOutput = getOutput(toolTerminal.instance, undefined);
outputLineCount = timeoutOutput ? count(timeoutOutput.trim(), '\n') + 1 : 0;
terminalResult = timeoutOutput ?? '';
} else {
this._logService.debug(`RunInTerminalTool: Threw exception`);
toolTerminal.instance.dispose();
error = e instanceof CancellationError ? 'canceled' : 'unexpectedException';
throw e;
}
} finally {
timeoutPromise?.cancel();
store.dispose();
const timingExecuteMs = Date.now() - timingStart;
this._telemetry.logInvoke(toolTerminal.instance, {

View File

@@ -22,6 +22,7 @@ export const enum TerminalChatAgentToolsSettingId {
AutoReplyToPrompts = 'chat.tools.terminal.autoReplyToPrompts',
OutputLocation = 'chat.tools.terminal.outputLocation',
PreventShellHistory = 'chat.tools.terminal.preventShellHistory',
EnforceTimeoutFromModel = 'chat.tools.terminal.enforceTimeoutFromModel',
TerminalProfileLinux = 'chat.tools.terminal.terminalProfile.linux',
TerminalProfileMacOs = 'chat.tools.terminal.terminalProfile.osx',
@@ -470,6 +471,16 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
`- \`fish\`: ${localize('preventShellHistory.description.fish', "Sets `fish_private_mode` to prevent any command from entering history")}`,
`- \`pwsh\`: ${localize('preventShellHistory.description.pwsh', "Sets a custom history handler via PSReadLine's `AddToHistoryHandler` to prevent any command from entering history")}`,
].join('\n'),
},
[TerminalChatAgentToolsSettingId.EnforceTimeoutFromModel]: {
restricted: true,
type: 'boolean',
default: false,
tags: ['experimental'],
experiment: {
mode: 'auto'
},
markdownDescription: localize('enforceTimeoutFromModel.description', "Whether to enforce the timeout value provided by the model in the run in terminal tool. When enabled, if the model provides a timeout parameter, the tool will stop tracking the command after that duration and return the output collected so far."),
}
};