shellTool function
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).
Implementation
AgentTool shellTool(ExecutionEnv env) {
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.',
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)',
},
},
'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 result = await env.exec(
command,
options: ShellExecOptions(timeout: timeout, cancelToken: cancelToken),
);
String outputOf(ShellExecResult execResult) {
final parts = <String>[
if (execResult.stdout.isNotEmpty) execResult.stdout,
if (execResult.stderr.isNotEmpty) execResult.stderr,
];
return parts.join('\n');
}
String truncate(String output) {
final truncation = _truncateTail(output);
if (!truncation.truncated) return output;
final startLine = truncation.totalLines - truncation.outputLines + 1;
final endLine = truncation.totalLines;
var notice =
'\n\n[Showing lines $startLine-$endLine of ${truncation.totalLines}';
if (truncation.truncatedBy == _TruncatedBy.bytes) {
notice += ' (${formatToolSize(defaultToolMaxBytes)} limit)';
}
return '${truncation.content}$notice.]';
}
if (result.isErr) {
final error = result.errorOrNull!;
throw switch (error.code) {
ExecutionErrorCode.aborted => StateError(
_appendStatus('', 'Command aborted'),
),
ExecutionErrorCode.timeout => StateError(
_appendStatus(
'',
'Command timed out after ${timeoutArg ?? 'unknown'} seconds',
),
),
_ => StateError('$error'),
};
}
final execResult = result.valueOrNull!;
final rawOutput = outputOf(execResult);
if (execResult.exitCode != 0) {
throw StateError(
_appendStatus(
truncate(rawOutput),
'Command exited with code ${execResult.exitCode}',
),
);
}
final output = truncate(rawOutput);
return ToolExecutionResult.text(output.isEmpty ? '(no output)' : output);
},
);
}