shellTool function
AgentTool
shellTool(
- ExecutionEnv env, {
- ShellJobRegistry? jobs,
- Duration retryBackoff = _bashRetryBackoff,
- PasswordPromptCallback? onPasswordPrompt,
- Duration passwordQuiet = _bashPasswordQuiet,
Creates the bash tool: executes a shell command via ExecutionEnv.exec
and returns stdout followed by stderr, truncated to the last
defaultToolMaxLines lines / defaultToolMaxBytes bytes. A non-zero
exit code, timeout, or abort throws (the loop turns it into an error
tool result, pi semantics).
With jobs (and an environment implementing BackgroundShell) the tool
gains two background behaviors:
background: truestarts the command detached and returns immediately with the job id; completion is reported back as a follow-up message.- Foreground runs still block, but a user steering message mid-run (the loop's soft-yield token, see currentYieldToken) moves the command into a background job WITHOUT killing it: the tool call answers with the job id and a partial-output tail, and the user message is delivered at the next step boundary.
Implementation
AgentTool shellTool(
ExecutionEnv env, {
ShellJobRegistry? jobs,
Duration retryBackoff = _bashRetryBackoff,
PasswordPromptCallback? onPasswordPrompt,
Duration passwordQuiet = _bashPasswordQuiet,
}) {
return AgentTool(
name: bashToolName,
label: 'bash',
tier: ApprovalTier.exec,
description:
'Execute a bash command in the current working directory. Returns '
'stdout and stderr. Output is truncated to the last '
'$defaultToolMaxLines lines or ${defaultToolMaxBytes ~/ 1024}KB '
'(whichever is hit first). Optionally provide a timeout in seconds. '
'Timeout-class failures are retried automatically '
'(${bashToolMaxRetries + 1} attempts total) — a hung network call '
'does not fail the call; retries are visible as [bash attempt N] '
'notices in the output. '
'For long-running commands (builds, servers, watchers) pass '
'background: true — the command keeps running as a job, you get its '
'id immediately and are notified when it finishes; check progress '
'with bash_job. A foreground command that is still running when the '
'user sends a message is moved to a background job untouched (never '
'killed) so the user gets an answer right away.',
parameters: const {
'type': 'object',
'properties': {
'command': {
'type': 'string',
'description': 'The bash command to execute',
},
'timeout': {
'type': 'number',
'description': 'Timeout in seconds (optional, no default timeout)',
},
'background': {
'type': 'boolean',
'description':
'Run detached and return the job id immediately (optional, '
'default false). Use for long-running commands.',
},
'stdin': {
'type': 'string',
'description':
'Optional text written to the command\'s stdin right after '
'start (a password/passphrase the USER supplied via the ask '
'tool, or "y\\n"). Use when the command prompts for input, '
'e.g. ssh-add or sudo. Never invent secrets — ask first.',
},
},
'required': ['command'],
},
execute: (arguments, cancelToken, onUpdate) async {
cancelToken?.throwIfCancelled();
final command = arguments['command'] as String;
final timeoutArg = arguments['timeout'] as num?;
final timeout = timeoutArg == null ? null : _resolveTimeout(timeoutArg);
final background = arguments['background'] as bool? ?? false;
final stdinData = arguments['stdin'] as String?;
final canJob = jobs != null && jobs.isSupported;
if (background) {
if (!canJob) {
return ToolExecutionResult.text(
'Background execution is not supported in this environment — '
'run the command in the foreground with an explicit timeout.',
);
}
final entry = await jobs.start(
command,
options: ShellExecOptions(
cwd: env.cwd,
timeout: timeout,
cancelToken: cancelToken,
stdinData: stdinData,
),
);
return ToolExecutionResult.text(
'Started background job ${entry.id}.\n'
'Log: ${entry.logPath}\n'
'You will be notified when it finishes; check progress with '
'bash_job (action: status | output | stop).',
);
}
// Yield-aware foreground run: executed as a job from the start so a
// steering message can move it to the background mid-flight without
// killing the process. Settled before any yield → identical result to
// the classic inline path.
if (canJob && currentYieldToken() != null) {
return _shellViaJob(
env,
jobs,
command,
stdinData: stdinData,
timeout: timeout,
timeoutArg: timeoutArg,
cancelToken: cancelToken,
yieldToken: currentYieldToken()!,
onPasswordPrompt: onPasswordPrompt,
passwordQuiet: passwordQuiet,
);
}
return _runForegroundBash(
env,
command,
timeout: timeout,
timeoutArg: timeoutArg,
cancelToken: cancelToken,
stdinData: stdinData,
retryBackoff: retryBackoff,
);
},
);
}