feat: add exec pty support
This commit is contained in:
@@ -17,7 +17,7 @@ Key parameters:
|
|||||||
- `background` (bool): background immediately
|
- `background` (bool): background immediately
|
||||||
- `timeout` (seconds, default 1800): kill the process after this timeout
|
- `timeout` (seconds, default 1800): kill the process after this timeout
|
||||||
- `elevated` (bool): run on host if elevated mode is enabled/allowed
|
- `elevated` (bool): run on host if elevated mode is enabled/allowed
|
||||||
- Need a real TTY? Use the tmux skill.
|
- Need a real TTY? Set `pty: true`.
|
||||||
- `workdir`, `env`
|
- `workdir`, `env`
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ Background sessions are scoped per agent; `process` only sees sessions from the
|
|||||||
- `yieldMs` (default 10000): auto-background after delay
|
- `yieldMs` (default 10000): auto-background after delay
|
||||||
- `background` (bool): background immediately
|
- `background` (bool): background immediately
|
||||||
- `timeout` (seconds, default 1800): kill on expiry
|
- `timeout` (seconds, default 1800): kill on expiry
|
||||||
|
- `pty` (bool): run in a pseudo-terminal when available (TTY-only CLIs, coding agents, terminal UIs)
|
||||||
- `elevated` (bool): run on host if elevated mode is enabled/allowed (only changes behavior when the agent is sandboxed)
|
- `elevated` (bool): run on host if elevated mode is enabled/allowed (only changes behavior when the agent is sandboxed)
|
||||||
- Need a real TTY? Use the tmux skill.
|
- Need a fully interactive session? Use `pty: true` and the `process` tool for stdin/output.
|
||||||
Note: `elevated` is ignored when sandboxing is off (exec already runs on the host).
|
Note: `elevated` is ignored when sandboxing is off (exec already runs on the host).
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ Core parameters:
|
|||||||
- `background` (immediate background)
|
- `background` (immediate background)
|
||||||
- `timeout` (seconds; kills the process if exceeded, default 1800)
|
- `timeout` (seconds; kills the process if exceeded, default 1800)
|
||||||
- `elevated` (bool; run on host if elevated mode is enabled/allowed; only changes behavior when the agent is sandboxed)
|
- `elevated` (bool; run on host if elevated mode is enabled/allowed; only changes behavior when the agent is sandboxed)
|
||||||
- Need a real TTY? Use the tmux skill.
|
- Need a real TTY? Set `pty: true`.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
||||||
|
|||||||
@@ -138,6 +138,7 @@
|
|||||||
"@grammyjs/runner": "^2.0.3",
|
"@grammyjs/runner": "^2.0.3",
|
||||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||||
"@homebridge/ciao": "^1.3.4",
|
"@homebridge/ciao": "^1.3.4",
|
||||||
|
"@lydell/node-pty": "1.2.0-beta.3",
|
||||||
"@mariozechner/pi-agent-core": "0.46.0",
|
"@mariozechner/pi-agent-core": "0.46.0",
|
||||||
"@mariozechner/pi-ai": "0.46.0",
|
"@mariozechner/pi-ai": "0.46.0",
|
||||||
"@mariozechner/pi-coding-agent": "^0.46.0",
|
"@mariozechner/pi-coding-agent": "^0.46.0",
|
||||||
|
|||||||
63
pnpm-lock.yaml
generated
63
pnpm-lock.yaml
generated
@@ -28,6 +28,9 @@ importers:
|
|||||||
'@homebridge/ciao':
|
'@homebridge/ciao':
|
||||||
specifier: ^1.3.4
|
specifier: ^1.3.4
|
||||||
version: 1.3.4
|
version: 1.3.4
|
||||||
|
'@lydell/node-pty':
|
||||||
|
specifier: 1.2.0-beta.3
|
||||||
|
version: 1.2.0-beta.3
|
||||||
'@mariozechner/pi-agent-core':
|
'@mariozechner/pi-agent-core':
|
||||||
specifier: 0.46.0
|
specifier: 0.46.0
|
||||||
version: 0.46.0(ws@8.19.0)(zod@4.3.5)
|
version: 0.46.0(ws@8.19.0)(zod@4.3.5)
|
||||||
@@ -935,6 +938,39 @@ packages:
|
|||||||
'@lit/reactive-element@2.1.2':
|
'@lit/reactive-element@2.1.2':
|
||||||
resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==}
|
resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==}
|
||||||
|
|
||||||
|
'@lydell/node-pty-darwin-arm64@1.2.0-beta.3':
|
||||||
|
resolution: {integrity: sha512-owcv+e1/OSu3bf9ZBdUQqJsQF888KyuSIiPYFNn0fLhgkhm9F3Pvha76Kj5mCPnodf7hh3suDe7upw7GPRXftQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@lydell/node-pty-darwin-x64@1.2.0-beta.3':
|
||||||
|
resolution: {integrity: sha512-k38O+UviWrWdxtqZBBc/D8NJU11Rey8Y2YMwSWNxLv3eXZZdF5IVpbBkI/2RmLsV5nCcciqLPbukxeZnEfPlwA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@lydell/node-pty-linux-arm64@1.2.0-beta.3':
|
||||||
|
resolution: {integrity: sha512-HUwRpGu3O+4sv9DAQFKnyW5LYhyYu2SDUa/bdFO/t4dIFCM4uDJEq47wfRM7+aYtJTi1b3lakN8SlWeuFQqJQQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@lydell/node-pty-linux-x64@1.2.0-beta.3':
|
||||||
|
resolution: {integrity: sha512-+RRY0PoCUeQaCvPR7/UnkGbxulwbFtoTWJfe+o4T1RcNtngrgaI55I9nl8CD8uqhGrB3smKuyvPM5UtwGhASUw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@lydell/node-pty-win32-arm64@1.2.0-beta.3':
|
||||||
|
resolution: {integrity: sha512-UEDd9ASp2M3iIYpIzfmfBlpyn4+K1G4CAjYcHWStptCkefoSVXWTiUBIa1KjBjZi3/xmsHIDpBEYTkGWuvLt2Q==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@lydell/node-pty-win32-x64@1.2.0-beta.3':
|
||||||
|
resolution: {integrity: sha512-TpdqSFYx7/Rj+68tuP6F/lkRYrHCYAIJgaS1bx3SctTkb5QAQCFwOKHd4xlsivmEOMT2LdhkJggPxwX9PAO5pQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@lydell/node-pty@1.2.0-beta.3':
|
||||||
|
resolution: {integrity: sha512-ngGAItlRhmJXrhspxt8kX13n1dVFqzETOq0m/+gqSkO8NJBvNMwP7FZckMwps2UFySdr4yxCXNGu/bumg5at6A==}
|
||||||
|
|
||||||
'@mariozechner/clipboard-darwin-arm64@0.3.0':
|
'@mariozechner/clipboard-darwin-arm64@0.3.0':
|
||||||
resolution: {integrity: sha512-7i4bitLzRSij0fj6q6tPmmf+JrwHqfBsBmf8mOcLVv0LVexD+4gEsyMait4i92exKYmCfna6uHKVS84G4nqehg==}
|
resolution: {integrity: sha512-7i4bitLzRSij0fj6q6tPmmf+JrwHqfBsBmf8mOcLVv0LVexD+4gEsyMait4i92exKYmCfna6uHKVS84G4nqehg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -5142,6 +5178,33 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@lit-labs/ssr-dom-shim': 1.5.1
|
'@lit-labs/ssr-dom-shim': 1.5.1
|
||||||
|
|
||||||
|
'@lydell/node-pty-darwin-arm64@1.2.0-beta.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@lydell/node-pty-darwin-x64@1.2.0-beta.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@lydell/node-pty-linux-arm64@1.2.0-beta.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@lydell/node-pty-linux-x64@1.2.0-beta.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@lydell/node-pty-win32-arm64@1.2.0-beta.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@lydell/node-pty-win32-x64@1.2.0-beta.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@lydell/node-pty@1.2.0-beta.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@lydell/node-pty-darwin-arm64': 1.2.0-beta.3
|
||||||
|
'@lydell/node-pty-darwin-x64': 1.2.0-beta.3
|
||||||
|
'@lydell/node-pty-linux-arm64': 1.2.0-beta.3
|
||||||
|
'@lydell/node-pty-linux-x64': 1.2.0-beta.3
|
||||||
|
'@lydell/node-pty-win32-arm64': 1.2.0-beta.3
|
||||||
|
'@lydell/node-pty-win32-x64': 1.2.0-beta.3
|
||||||
|
|
||||||
'@mariozechner/clipboard-darwin-arm64@0.3.0':
|
'@mariozechner/clipboard-darwin-arm64@0.3.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,18 @@ let jobTtlMs = clampTtl(Number.parseInt(process.env.PI_BASH_JOB_TTL_MS ?? "", 10
|
|||||||
|
|
||||||
export type ProcessStatus = "running" | "completed" | "failed" | "killed";
|
export type ProcessStatus = "running" | "completed" | "failed" | "killed";
|
||||||
|
|
||||||
|
export type SessionStdin = {
|
||||||
|
write: (data: string, cb?: (err?: Error | null) => void) => void;
|
||||||
|
end: () => void;
|
||||||
|
destroyed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ProcessSession {
|
export interface ProcessSession {
|
||||||
id: string;
|
id: string;
|
||||||
command: string;
|
command: string;
|
||||||
scopeKey?: string;
|
scopeKey?: string;
|
||||||
child?: ChildProcessWithoutNullStreams;
|
child?: ChildProcessWithoutNullStreams;
|
||||||
|
stdin?: SessionStdin;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
|
|||||||
20
src/agents/bash-tools.exec.pty.test.ts
Normal file
20
src/agents/bash-tools.exec.pty.test.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { afterEach, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { createExecTool } from "./bash-tools.exec";
|
||||||
|
import { resetProcessRegistryForTests } from "./bash-process-registry";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resetProcessRegistryForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("exec supports pty output", async () => {
|
||||||
|
const tool = createExecTool({ allowBackground: false });
|
||||||
|
const result = await tool.execute("toolcall", {
|
||||||
|
command: "node -e 'process.stdout.write(\"ok\")'",
|
||||||
|
pty: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.details.status).toBe("completed");
|
||||||
|
const text = result.content?.[0]?.text ?? "";
|
||||||
|
expect(text).toContain("ok");
|
||||||
|
});
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
import { logInfo } from "../logger.js";
|
import { logInfo } from "../logger.js";
|
||||||
import { addSession, appendOutput, markBackgrounded, markExited } from "./bash-process-registry.js";
|
import {
|
||||||
|
type SessionStdin,
|
||||||
|
addSession,
|
||||||
|
appendOutput,
|
||||||
|
markBackgrounded,
|
||||||
|
markExited,
|
||||||
|
} from "./bash-process-registry.js";
|
||||||
import type { BashSandboxConfig } from "./bash-tools.shared.js";
|
import type { BashSandboxConfig } from "./bash-tools.shared.js";
|
||||||
import {
|
import {
|
||||||
buildDockerExecArgs,
|
buildDockerExecArgs,
|
||||||
@@ -29,6 +35,26 @@ const DEFAULT_MAX_OUTPUT = clampNumber(
|
|||||||
const DEFAULT_PATH =
|
const DEFAULT_PATH =
|
||||||
process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||||
|
|
||||||
|
type PtyExitEvent = { exitCode: number; signal?: number };
|
||||||
|
type PtyListener<T> = (event: T) => void;
|
||||||
|
type PtyHandle = {
|
||||||
|
pid: number;
|
||||||
|
write: (data: string | Buffer) => void;
|
||||||
|
onData: (listener: PtyListener<string>) => void;
|
||||||
|
onExit: (listener: PtyListener<PtyExitEvent>) => void;
|
||||||
|
};
|
||||||
|
type PtySpawn = (
|
||||||
|
file: string,
|
||||||
|
args: string[] | string,
|
||||||
|
options: {
|
||||||
|
name?: string;
|
||||||
|
cols?: number;
|
||||||
|
rows?: number;
|
||||||
|
cwd?: string;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
},
|
||||||
|
) => PtyHandle;
|
||||||
|
|
||||||
export type ExecToolDefaults = {
|
export type ExecToolDefaults = {
|
||||||
backgroundMs?: number;
|
backgroundMs?: number;
|
||||||
timeoutSec?: number;
|
timeoutSec?: number;
|
||||||
@@ -62,6 +88,11 @@ const execSchema = Type.Object({
|
|||||||
description: "Timeout in seconds (optional, kills process on expiry)",
|
description: "Timeout in seconds (optional, kills process on expiry)",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
pty: Type.Optional(
|
||||||
|
Type.Boolean({
|
||||||
|
description: "Run in a pseudo-terminal (PTY) when available (TTY-required CLIs, coding agents)",
|
||||||
|
}),
|
||||||
|
),
|
||||||
elevated: Type.Optional(
|
elevated: Type.Optional(
|
||||||
Type.Boolean({
|
Type.Boolean({
|
||||||
description: "Run on the host with elevated permissions (if allowed)",
|
description: "Run on the host with elevated permissions (if allowed)",
|
||||||
@@ -106,7 +137,7 @@ export function createExecTool(
|
|||||||
name: "exec",
|
name: "exec",
|
||||||
label: "exec",
|
label: "exec",
|
||||||
description:
|
description:
|
||||||
"Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. For real TTY mode, use the tmux skill.",
|
"Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. Use pty=true for TTY-required commands (terminal UIs, coding agents).",
|
||||||
parameters: execSchema,
|
parameters: execSchema,
|
||||||
execute: async (_toolCallId, args, signal, onUpdate) => {
|
execute: async (_toolCallId, args, signal, onUpdate) => {
|
||||||
const params = args as {
|
const params = args as {
|
||||||
@@ -116,6 +147,7 @@ export function createExecTool(
|
|||||||
yieldMs?: number;
|
yieldMs?: number;
|
||||||
background?: boolean;
|
background?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
pty?: boolean;
|
||||||
elevated?: boolean;
|
elevated?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,15 +234,20 @@ export function createExecTool(
|
|||||||
containerWorkdir: containerWorkdir ?? sandbox.containerWorkdir,
|
containerWorkdir: containerWorkdir ?? sandbox.containerWorkdir,
|
||||||
})
|
})
|
||||||
: mergedEnv;
|
: mergedEnv;
|
||||||
const child = sandbox
|
const usePty = params.pty === true && !sandbox;
|
||||||
? spawn(
|
let child: ChildProcessWithoutNullStreams | null = null;
|
||||||
|
let pty: PtyHandle | null = null;
|
||||||
|
let stdin: SessionStdin | undefined;
|
||||||
|
|
||||||
|
if (sandbox) {
|
||||||
|
child = spawn(
|
||||||
"docker",
|
"docker",
|
||||||
buildDockerExecArgs({
|
buildDockerExecArgs({
|
||||||
containerName: sandbox.containerName,
|
containerName: sandbox.containerName,
|
||||||
command: params.command,
|
command: params.command,
|
||||||
workdir: containerWorkdir ?? sandbox.containerWorkdir,
|
workdir: containerWorkdir ?? sandbox.containerWorkdir,
|
||||||
env,
|
env,
|
||||||
tty: false,
|
tty: params.pty === true,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
@@ -219,21 +256,61 @@ export function createExecTool(
|
|||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
},
|
},
|
||||||
)
|
) as ChildProcessWithoutNullStreams;
|
||||||
: spawn(shell, [...shellArgs, params.command], {
|
stdin = child.stdin;
|
||||||
|
} else if (usePty) {
|
||||||
|
const ptyModule = (await import("@lydell/node-pty")) as unknown as {
|
||||||
|
spawn?: PtySpawn;
|
||||||
|
default?: { spawn?: PtySpawn };
|
||||||
|
};
|
||||||
|
const spawnPty = ptyModule.spawn ?? ptyModule.default?.spawn;
|
||||||
|
if (!spawnPty) {
|
||||||
|
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
||||||
|
}
|
||||||
|
pty = spawnPty(shell, [...shellArgs, params.command], {
|
||||||
|
cwd: workdir,
|
||||||
|
env,
|
||||||
|
name: process.env.TERM ?? "xterm-256color",
|
||||||
|
cols: 120,
|
||||||
|
rows: 30,
|
||||||
|
});
|
||||||
|
stdin = {
|
||||||
|
destroyed: false,
|
||||||
|
write: (data, cb) => {
|
||||||
|
try {
|
||||||
|
pty?.write(data);
|
||||||
|
cb?.(null);
|
||||||
|
} catch (err) {
|
||||||
|
cb?.(err as Error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
end: () => {
|
||||||
|
try {
|
||||||
|
const eof = process.platform === "win32" ? "\x1a" : "\x04";
|
||||||
|
pty?.write(eof);
|
||||||
|
} catch {
|
||||||
|
// ignore EOF errors
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
child = spawn(shell, [...shellArgs, params.command], {
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
env,
|
env,
|
||||||
detached: process.platform !== "win32",
|
detached: process.platform !== "win32",
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
});
|
}) as ChildProcessWithoutNullStreams;
|
||||||
|
stdin = child.stdin;
|
||||||
|
}
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
command: params.command,
|
command: params.command,
|
||||||
scopeKey: defaults?.scopeKey,
|
scopeKey: defaults?.scopeKey,
|
||||||
child,
|
child: child ?? undefined,
|
||||||
pid: child?.pid,
|
stdin,
|
||||||
|
pid: child?.pid ?? pty?.pid,
|
||||||
startedAt,
|
startedAt,
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
maxOutputChars: maxOutput,
|
maxOutputChars: maxOutput,
|
||||||
@@ -321,21 +398,28 @@ export function createExecTool(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
const handleStdout = (data: string) => {
|
||||||
const str = sanitizeBinaryOutput(data.toString());
|
const str = sanitizeBinaryOutput(data.toString());
|
||||||
for (const chunk of chunkString(str)) {
|
for (const chunk of chunkString(str)) {
|
||||||
appendOutput(session, "stdout", chunk);
|
appendOutput(session, "stdout", chunk);
|
||||||
emitUpdate();
|
emitUpdate();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
child.stderr.on("data", (data) => {
|
const handleStderr = (data: string) => {
|
||||||
const str = sanitizeBinaryOutput(data.toString());
|
const str = sanitizeBinaryOutput(data.toString());
|
||||||
for (const chunk of chunkString(str)) {
|
for (const chunk of chunkString(str)) {
|
||||||
appendOutput(session, "stderr", chunk);
|
appendOutput(session, "stderr", chunk);
|
||||||
emitUpdate();
|
emitUpdate();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (pty) {
|
||||||
|
pty.onData(handleStdout);
|
||||||
|
} else if (child) {
|
||||||
|
child.stdout.on("data", handleStdout);
|
||||||
|
child.stderr.on("data", handleStderr);
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise<AgentToolResult<ExecToolDetails>>((resolve, reject) => {
|
return new Promise<AgentToolResult<ExecToolDetails>>((resolve, reject) => {
|
||||||
rejectFn = reject;
|
rejectFn = reject;
|
||||||
@@ -393,6 +477,9 @@ export function createExecTool(
|
|||||||
const isSuccess = code === 0 && !wasSignal && !signal?.aborted && !timedOut;
|
const isSuccess = code === 0 && !wasSignal && !signal?.aborted && !timedOut;
|
||||||
const status: "completed" | "failed" = isSuccess ? "completed" : "failed";
|
const status: "completed" | "failed" = isSuccess ? "completed" : "failed";
|
||||||
markExited(session, code, exitSignal, status);
|
markExited(session, code, exitSignal, status);
|
||||||
|
if (!session.child && session.stdin) {
|
||||||
|
session.stdin.destroyed = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (yielded || session.backgrounded) return;
|
if (yielded || session.backgrounded) return;
|
||||||
|
|
||||||
@@ -433,17 +520,25 @@ export function createExecTool(
|
|||||||
|
|
||||||
// `exit` can fire before stdio fully flushes (notably on Windows).
|
// `exit` can fire before stdio fully flushes (notably on Windows).
|
||||||
// `close` waits for streams to close, so aggregated output is complete.
|
// `close` waits for streams to close, so aggregated output is complete.
|
||||||
child.once("close", (code, exitSignal) => {
|
if (pty) {
|
||||||
handleExit(code, exitSignal);
|
pty.onExit((event) => {
|
||||||
});
|
const rawSignal = event.signal ?? null;
|
||||||
|
const normalizedSignal = rawSignal === 0 ? null : rawSignal;
|
||||||
|
handleExit(event.exitCode ?? null, normalizedSignal);
|
||||||
|
});
|
||||||
|
} else if (child) {
|
||||||
|
child.once("close", (code, exitSignal) => {
|
||||||
|
handleExit(code, exitSignal);
|
||||||
|
});
|
||||||
|
|
||||||
child.once("error", (err) => {
|
child.once("error", (err) => {
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
if (yieldTimer) clearTimeout(yieldTimer);
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||||
if (timeoutFinalizeTimer) clearTimeout(timeoutFinalizeTimer);
|
if (timeoutFinalizeTimer) clearTimeout(timeoutFinalizeTimer);
|
||||||
markExited(session, null, null, "failed");
|
markExited(session, null, null, "failed");
|
||||||
settle(() => reject(err));
|
settle(() => reject(err));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -302,7 +302,8 @@ export function createProcessTool(
|
|||||||
details: { status: "failed" },
|
details: { status: "failed" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!scopedSession.child?.stdin || scopedSession.child.stdin.destroyed) {
|
const stdin = scopedSession.stdin ?? scopedSession.child?.stdin;
|
||||||
|
if (!stdin || stdin.destroyed) {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -314,13 +315,13 @@ export function createProcessTool(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
scopedSession.child?.stdin.write(params.data ?? "", (err) => {
|
stdin.write(params.data ?? "", (err) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (params.eof) {
|
if (params.eof) {
|
||||||
scopedSession.child.stdin.end();
|
stdin.end();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
grep: "Search file contents for patterns",
|
grep: "Search file contents for patterns",
|
||||||
find: "Find files by glob pattern",
|
find: "Find files by glob pattern",
|
||||||
ls: "List directory contents",
|
ls: "List directory contents",
|
||||||
exec: "Run shell commands",
|
exec: "Run shell commands (pty available for TTY-required CLIs)",
|
||||||
process: "Manage background exec sessions",
|
process: "Manage background exec sessions",
|
||||||
web_search: "Search the web (Brave API)",
|
web_search: "Search the web (Brave API)",
|
||||||
web_fetch: "Fetch and extract readable content from a URL",
|
web_fetch: "Fetch and extract readable content from a URL",
|
||||||
|
|||||||
24
src/types/lydell-node-pty.d.ts
vendored
Normal file
24
src/types/lydell-node-pty.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
declare module "@lydell/node-pty" {
|
||||||
|
export type PtyExitEvent = { exitCode: number; signal?: number };
|
||||||
|
export type PtyListener<T> = (event: T) => void;
|
||||||
|
export type PtyHandle = {
|
||||||
|
pid: number;
|
||||||
|
write: (data: string | Buffer) => void;
|
||||||
|
onData: (listener: PtyListener<string>) => void;
|
||||||
|
onExit: (listener: PtyListener<PtyExitEvent>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PtySpawn = (
|
||||||
|
file: string,
|
||||||
|
args: string[] | string,
|
||||||
|
options: {
|
||||||
|
name?: string;
|
||||||
|
cols?: number;
|
||||||
|
rows?: number;
|
||||||
|
cwd?: string;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
},
|
||||||
|
) => PtyHandle;
|
||||||
|
|
||||||
|
export const spawn: PtySpawn;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user