fix: clean agent bash lint
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { ProcessSession } from "./bash-process-registry.js";
|
||||
import {
|
||||
addSession,
|
||||
appendOutput,
|
||||
@@ -8,20 +10,16 @@ import {
|
||||
resetProcessRegistryForTests,
|
||||
} from "./bash-process-registry.js";
|
||||
|
||||
type DummyChild = {
|
||||
pid?: number;
|
||||
};
|
||||
|
||||
describe("bash process registry", () => {
|
||||
beforeEach(() => {
|
||||
resetProcessRegistryForTests();
|
||||
});
|
||||
|
||||
it("captures output and truncates", () => {
|
||||
const session = {
|
||||
const session: ProcessSession = {
|
||||
id: "sess",
|
||||
command: "echo test",
|
||||
child: { pid: 123 } as DummyChild,
|
||||
child: { pid: 123 } as ChildProcessWithoutNullStreams,
|
||||
startedAt: Date.now(),
|
||||
cwd: "/tmp",
|
||||
maxOutputChars: 10,
|
||||
@@ -37,19 +35,19 @@ describe("bash process registry", () => {
|
||||
backgrounded: false,
|
||||
};
|
||||
|
||||
addSession(session as any);
|
||||
appendOutput(session as any, "stdout", "0123456789");
|
||||
appendOutput(session as any, "stdout", "abcdef");
|
||||
addSession(session);
|
||||
appendOutput(session, "stdout", "0123456789");
|
||||
appendOutput(session, "stdout", "abcdef");
|
||||
|
||||
expect(session.aggregated).toBe("6789abcdef");
|
||||
expect(session.truncated).toBe(true);
|
||||
});
|
||||
|
||||
it("only persists finished sessions when backgrounded", () => {
|
||||
const session = {
|
||||
const session: ProcessSession = {
|
||||
id: "sess",
|
||||
command: "echo test",
|
||||
child: { pid: 123 } as DummyChild,
|
||||
child: { pid: 123 } as ChildProcessWithoutNullStreams,
|
||||
startedAt: Date.now(),
|
||||
cwd: "/tmp",
|
||||
maxOutputChars: 100,
|
||||
@@ -65,12 +63,12 @@ describe("bash process registry", () => {
|
||||
backgrounded: false,
|
||||
};
|
||||
|
||||
addSession(session as any);
|
||||
markExited(session as any, 0, null, "completed");
|
||||
addSession(session);
|
||||
markExited(session, 0, null, "completed");
|
||||
expect(listFinishedSessions()).toHaveLength(0);
|
||||
|
||||
markBackgrounded(session as any);
|
||||
markExited(session as any, 0, null, "completed");
|
||||
markBackgrounded(session);
|
||||
markExited(session, 0, null, "completed");
|
||||
expect(listFinishedSessions()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,12 +79,17 @@ export function appendOutput(
|
||||
) {
|
||||
session.pendingStdout ??= [];
|
||||
session.pendingStderr ??= [];
|
||||
const buffer = stream === "stdout" ? session.pendingStdout : session.pendingStderr;
|
||||
const buffer =
|
||||
stream === "stdout" ? session.pendingStdout : session.pendingStderr;
|
||||
buffer.push(chunk);
|
||||
session.totalOutputChars += chunk.length;
|
||||
const aggregated = trimWithCap(session.aggregated + chunk, session.maxOutputChars);
|
||||
const aggregated = trimWithCap(
|
||||
session.aggregated + chunk,
|
||||
session.maxOutputChars,
|
||||
);
|
||||
session.truncated =
|
||||
session.truncated || aggregated.length < session.aggregated.length + chunk.length;
|
||||
session.truncated ||
|
||||
aggregated.length < session.aggregated.length + chunk.length;
|
||||
session.aggregated = aggregated;
|
||||
session.tail = tail(session.aggregated, 2000);
|
||||
}
|
||||
@@ -175,6 +180,9 @@ function pruneFinishedSessions() {
|
||||
|
||||
function startSweeper() {
|
||||
if (sweeper) return;
|
||||
sweeper = setInterval(pruneFinishedSessions, Math.max(30_000, JOB_TTL_MS / 6));
|
||||
sweeper = setInterval(
|
||||
pruneFinishedSessions,
|
||||
Math.max(30_000, JOB_TTL_MS / 6),
|
||||
);
|
||||
sweeper.unref?.();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { bashTool, processTool } from "./bash-tools.js";
|
||||
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
|
||||
import { bashTool, processTool } from "./bash-tools.js";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -49,7 +49,9 @@ describe("bash tool backgrounding", () => {
|
||||
const sessionId = (result.details as { sessionId: string }).sessionId;
|
||||
|
||||
const list = await processTool.execute("call2", { action: "list" });
|
||||
const sessions = (list.details as { sessions: Array<{ sessionId: string }> }).sessions;
|
||||
const sessions = (
|
||||
list.details as { sessions: Array<{ sessionId: string }> }
|
||||
).sessions;
|
||||
expect(sessions.some((s) => s.sessionId === sessionId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai";
|
||||
import { StringEnum } from "@mariozechner/pi-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import {
|
||||
addSession,
|
||||
@@ -16,7 +16,11 @@ import {
|
||||
markBackgrounded,
|
||||
markExited,
|
||||
} from "./bash-process-registry.js";
|
||||
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "./shell-utils.js";
|
||||
import {
|
||||
getShellConfig,
|
||||
killProcessTree,
|
||||
sanitizeBinaryOutput,
|
||||
} from "./shell-utils.js";
|
||||
|
||||
const CHUNK_LIMIT = 8 * 1024;
|
||||
const DEFAULT_YIELD_MS = clampNumber(
|
||||
@@ -79,7 +83,7 @@ export const bashTool: AgentTool<typeof bashSchema, BashToolDetails> = {
|
||||
description:
|
||||
"Execute bash with background continuation. Use yieldMs/background to continue later via process tool.",
|
||||
parameters: bashSchema,
|
||||
execute: async (toolCallId, args, signal, onUpdate) => {
|
||||
execute: async (_toolCallId, args, signal, onUpdate) => {
|
||||
const params = args as {
|
||||
command: string;
|
||||
workdir?: string;
|
||||
@@ -106,12 +110,13 @@ export const bashTool: AgentTool<typeof bashSchema, BashToolDetails> = {
|
||||
const workdir = params.workdir?.trim() || process.cwd();
|
||||
|
||||
const { shell, args: shellArgs } = getShellConfig();
|
||||
const env = params.env ?? {};
|
||||
const child: ChildProcessWithoutNullStreams = spawn(
|
||||
shell,
|
||||
[...shellArgs, params.command],
|
||||
{
|
||||
cwd: workdir,
|
||||
env: { ...process.env, ...(params.env ?? {}) },
|
||||
env: { ...process.env, ...env },
|
||||
detached: true,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
},
|
||||
@@ -243,8 +248,11 @@ export const bashTool: AgentTool<typeof bashSchema, BashToolDetails> = {
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||
const durationMs = Date.now() - startedAt;
|
||||
const wasSignal = exitSignal != null;
|
||||
const isSuccess = code === 0 && !wasSignal && !signal?.aborted && !timedOut;
|
||||
const status: "completed" | "failed" = isSuccess ? "completed" : "failed";
|
||||
const isSuccess =
|
||||
code === 0 && !wasSignal && !signal?.aborted && !timedOut;
|
||||
const status: "completed" | "failed" = isSuccess
|
||||
? "completed"
|
||||
: "failed";
|
||||
markExited(session, code, exitSignal, status);
|
||||
|
||||
if (yielded || session.backgrounded) return;
|
||||
@@ -352,7 +360,10 @@ export const processTool: AgentTool<typeof processSchema> = {
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: lines.join("\n") || "No running or recent sessions." },
|
||||
{
|
||||
type: "text",
|
||||
text: lines.join("\n") || "No running or recent sessions.",
|
||||
},
|
||||
],
|
||||
details: { status: "completed", sessions: [...running, ...finished] },
|
||||
};
|
||||
@@ -360,7 +371,9 @@ export const processTool: AgentTool<typeof processSchema> = {
|
||||
|
||||
if (!params.sessionId) {
|
||||
return {
|
||||
content: [{ type: "text", text: "sessionId is required for this action." }],
|
||||
content: [
|
||||
{ type: "text", text: "sessionId is required for this action." },
|
||||
],
|
||||
details: { status: "failed" },
|
||||
};
|
||||
}
|
||||
@@ -389,7 +402,8 @@ export const processTool: AgentTool<typeof processSchema> = {
|
||||
},
|
||||
],
|
||||
details: {
|
||||
status: finished.status === "completed" ? "completed" : "failed",
|
||||
status:
|
||||
finished.status === "completed" ? "completed" : "failed",
|
||||
sessionId: params.sessionId,
|
||||
exitCode: finished.exitCode ?? undefined,
|
||||
aggregated: finished.aggregated,
|
||||
@@ -398,7 +412,10 @@ export const processTool: AgentTool<typeof processSchema> = {
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `No session found for ${params.sessionId}` },
|
||||
{
|
||||
type: "text",
|
||||
text: `No session found for ${params.sessionId}`,
|
||||
},
|
||||
],
|
||||
details: { status: "failed" },
|
||||
};
|
||||
@@ -421,7 +438,12 @@ export const processTool: AgentTool<typeof processSchema> = {
|
||||
if (exited) {
|
||||
const status =
|
||||
exitCode === 0 && exitSignal == null ? "completed" : "failed";
|
||||
markExited(session, session.exitCode ?? null, session.exitSignal ?? null, status);
|
||||
markExited(
|
||||
session,
|
||||
session.exitCode ?? null,
|
||||
session.exitSignal ?? null,
|
||||
status,
|
||||
);
|
||||
}
|
||||
const status = exited
|
||||
? exitCode === 0 && exitSignal == null
|
||||
@@ -470,9 +492,7 @@ export const processTool: AgentTool<typeof processSchema> = {
|
||||
const total = session.aggregated.length;
|
||||
const slice = session.aggregated.slice(
|
||||
params.offset ?? 0,
|
||||
params.limit
|
||||
? (params.offset ?? 0) + params.limit
|
||||
: undefined,
|
||||
params.limit ? (params.offset ?? 0) + params.limit : undefined,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: slice || "(no output yet)" }],
|
||||
@@ -488,15 +508,12 @@ export const processTool: AgentTool<typeof processSchema> = {
|
||||
const total = finished.aggregated.length;
|
||||
const slice = finished.aggregated.slice(
|
||||
params.offset ?? 0,
|
||||
params.limit
|
||||
? (params.offset ?? 0) + params.limit
|
||||
: undefined,
|
||||
params.limit ? (params.offset ?? 0) + params.limit : undefined,
|
||||
);
|
||||
const status = finished.status === "completed" ? "completed" : "failed";
|
||||
const status =
|
||||
finished.status === "completed" ? "completed" : "failed";
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: slice || "(no output recorded)" },
|
||||
],
|
||||
content: [{ type: "text", text: slice || "(no output recorded)" }],
|
||||
details: {
|
||||
status,
|
||||
sessionId: params.sessionId,
|
||||
@@ -519,7 +536,10 @@ export const processTool: AgentTool<typeof processSchema> = {
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `No active session found for ${params.sessionId}` },
|
||||
{
|
||||
type: "text",
|
||||
text: `No active session found for ${params.sessionId}`,
|
||||
},
|
||||
],
|
||||
details: { status: "failed" },
|
||||
};
|
||||
@@ -572,7 +592,10 @@ export const processTool: AgentTool<typeof processSchema> = {
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `No active session found for ${params.sessionId}` },
|
||||
{
|
||||
type: "text",
|
||||
text: `No active session found for ${params.sessionId}`,
|
||||
},
|
||||
],
|
||||
details: { status: "failed" },
|
||||
};
|
||||
|
||||
@@ -11,9 +11,20 @@ export function getShellConfig(): { shell: string; args: string[] } {
|
||||
}
|
||||
|
||||
export function sanitizeBinaryOutput(text: string): string {
|
||||
return text
|
||||
.replace(/[\p{Format}\p{Surrogate}]/gu, "")
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
||||
const scrubbed = text.replace(/[\p{Format}\p{Surrogate}]/gu, "");
|
||||
if (!scrubbed) return scrubbed;
|
||||
const chunks: string[] = [];
|
||||
for (const char of scrubbed) {
|
||||
const code = char.codePointAt(0);
|
||||
if (code == null) continue;
|
||||
if (code === 0x09 || code === 0x0a || code === 0x0d) {
|
||||
chunks.push(char);
|
||||
continue;
|
||||
}
|
||||
if (code < 0x20) continue;
|
||||
chunks.push(char);
|
||||
}
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
export function killProcessTree(pid: number): void {
|
||||
|
||||
Reference in New Issue
Block a user