src / preprocessor.ts
/**
* @file preprocessor.ts
* Prompt preprocessor — two jobs:
*
* 1. Resets the per-turn tool call budget every time the user sends a message.
* 2. Optionally injects computer state (OS, tools, network, active background
* processes) into the model's context so it knows what it's working with
* without spending a tool call to find out.
*/
import { configSchematics } from "./config";
import { advanceTurn } from "./toolsProvider";
import * as engine from "./container/engine";
import { MAX_INJECTED_CONTEXT_CHARS, CONTAINER_WORKDIR } from "./constants";
import type { PluginController } from "./pluginTypes";
function readConfig(ctl: PluginController) {
const c = ctl.getPluginConfig(configSchematics);
return {
autoInject: c.get("autoInjectContext") === "on",
maxToolCalls: c.get("maxToolCallsPerTurn") ?? 25,
internetAccess: c.get("internetAccess") === "on",
persistenceMode: c.get("persistenceMode") || "persistent",
baseImage: c.get("baseImage") || "ubuntu:24.04",
};
}
/**
* Build a concise context block about the computer's current state.
*/
async function buildContextBlock(
cfg: ReturnType<typeof readConfig>,
): Promise<string> {
if (!engine.isReady()) {
return [
`[Computer — Available]`,
`You have a dedicated Linux computer (${cfg.baseImage}) available via tools.`,
`Internet: ${cfg.internetAccess ? "enabled" : "disabled"}.`,
`Mode: ${cfg.persistenceMode}.`,
`The computer will start automatically when you first use a tool.`,
`Working directory: ${CONTAINER_WORKDIR}`,
].join("\n");
}
try {
const quickInfo = await engine.exec(
`echo "OS=$(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')" && ` +
`echo "TOOLS=$(which git curl wget python3 node gcc pip3 2>/dev/null | xargs -I{} basename {} | tr '\\n' ',')" && ` +
`echo "FILES=$(ls ${CONTAINER_WORKDIR} 2>/dev/null | head -10 | tr '\\n' ',')" && ` +
`echo "DISK=$(df -h ${CONTAINER_WORKDIR} 2>/dev/null | tail -1 | awk '{print $4 \" free / \" $2 \" total\"}')"`,
5,
MAX_INJECTED_CONTEXT_CHARS,
);
if (quickInfo.exitCode !== 0) {
return `[Computer — Running (${cfg.baseImage}), Internet: ${cfg.internetAccess ? "on" : "off"}]`;
}
const lines = quickInfo.stdout.split("\n");
const get = (prefix: string): string => {
const line = lines.find((l) => l.startsWith(prefix + "="));
return line?.slice(prefix.length + 1)?.trim() ?? "";
};
const os = get("OS");
const tools = get("TOOLS").split(",").filter(Boolean);
const files = get("FILES").split(",").filter(Boolean);
const disk = get("DISK");
const parts: string[] = [
`[Computer — Running]`,
`OS: ${os}`,
`Internet: ${cfg.internetAccess ? "enabled" : "disabled"}`,
`Mode: ${cfg.persistenceMode}`,
`Disk: ${disk}`,
];
if (tools.length > 0) {
parts.push(`Installed: ${tools.join(", ")}`);
}
if (files.length > 0) {
parts.push(
`Workspace (${CONTAINER_WORKDIR}): ${files.join(", ")}${files.length >= 10 ? "…" : ""}`,
);
} else {
parts.push(`Workspace (${CONTAINER_WORKDIR}): empty`);
}
try {
const bgProcs = engine.listBgProcesses().filter((p) => p.running);
if (bgProcs.length > 0) {
const bgSummary = bgProcs
.map((p) => ` [handleId:${p.handleId}] ${p.command} — running for ${p.runtimeSecs}s`)
.join("\n");
parts.push(
``,
`Active background processes (${bgProcs.length}):`,
bgSummary,
`Use ReadProcessLogs(handleId) to check output, KillBackground(handleId) to stop.`,
);
}
} catch {
/* bg process list is best-effort — never block the preprocessor */
}
parts.push(
``,
`Tools: Execute, WriteFile, AppendFile, ReadFile, StrReplace, InsertLines, ListDirectory, MoveFile, CopyFile, SearchInFiles, SetEnvVar, UploadFile, DownloadFile, ExecuteBackground, ReadProcessLogs, KillBackground, KillProcess, ComputerStatus, RestartComputer, RebuildComputer, ResetShell.`,
);
return parts.join("\n");
} catch {
return `[Computer — Running (${cfg.baseImage}), Internet: ${cfg.internetAccess ? "on" : "off"}]`;
}
}
export async function promptPreprocessor(
ctl: PluginController,
userMessage: string,
): Promise<string> {
const cfg = readConfig(ctl);
advanceTurn(cfg.maxToolCalls);
if (!cfg.autoInject) return userMessage;
if (userMessage.length < 5) return userMessage;
try {
const context = await buildContextBlock(cfg);
if (!context) return userMessage;
return `${context}\n\n---\n\n${userMessage}`;
} catch {
return userMessage;
}
}