VoidBot/packages/providers/src/owner-codex-runtime.ts
2026-05-05 12:22:53 +01:00

662 lines
16 KiB
TypeScript

import { spawn } from "node:child_process";
import type {
CodexMcpServerConfig,
ContextBundle,
ProviderNotificationIntent,
ProviderNotificationReason,
RetrievalFilters,
} from "@voidbot/shared";
import {
DEFAULT_RETRIEVAL_RESULT_LIMIT,
MAX_RETRIEVAL_RESULT_LIMIT,
} from "@voidbot/shared";
import {
extractLatestUsage,
clamp,
type CodexRunResult,
type CodexTraceEvent,
type CodexUsageSnapshot,
HANDOFF_SENTINEL,
type HistoryLookupTool,
type HistoryToolRequest,
isRecord,
MAX_NOTIFICATION_MESSAGE_LENGTH,
OWNER_NOTIFY_SENTINEL,
readNumber,
readRecord,
readString,
readUsage,
summarizeToolResult,
summarizeUnknown,
TOOL_REQUEST_SENTINEL,
type NormalizedDiscordReply,
type ToolCallRecord,
} from "./owner-codex-shared";
const DEFAULT_HISTORY_LIMIT = DEFAULT_RETRIEVAL_RESULT_LIMIT;
const MAX_HISTORY_LIMIT = MAX_RETRIEVAL_RESULT_LIMIT;
export async function runCodexExec(input: {
executable: string;
executableArgs: string[];
model: string;
reasoningEffort: "low" | "medium" | "high" | "xhigh";
timeoutMs: number;
workingDirectory: string;
prompt: string;
mcpServers: CodexMcpServerConfig[];
}): Promise<CodexRunResult> {
return new Promise<CodexRunResult>((resolve) => {
const startedAtMs = Date.now();
const startedAt = new Date(startedAtMs).toISOString();
const args = [
...input.executableArgs,
"exec",
"-m",
input.model,
"-c",
'approval_policy="never"',
"-c",
`model_reasoning_effort=${JSON.stringify(input.reasoningEffort)}`,
...buildMcpConfigArguments(input.mcpServers),
"--json",
"--skip-git-repo-check",
"-s",
"read-only",
"-",
];
let stdout = "";
let stderr = "";
let resolved = false;
let handoffReason: string | undefined;
let stdoutBuffer = "";
const traceEvents: CodexTraceEvent[] = [];
const child = spawn(input.executable, args, {
cwd: input.workingDirectory,
env: process.env,
stdio: ["pipe", "pipe", "pipe"],
});
const finish = (
result: Omit<
CodexRunResult,
"startedAt" | "finishedAt" | "durationMs" | "traceEvents" | "usage"
>,
) => {
if (resolved) {
return;
}
resolved = true;
clearTimeout(timeoutHandle);
const finishedAtMs = Date.now();
resolve({
...result,
startedAt,
finishedAt: new Date(finishedAtMs).toISOString(),
durationMs: finishedAtMs - startedAtMs,
traceEvents: [...traceEvents],
usage: extractLatestUsage(traceEvents),
});
};
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdin.setDefaultEncoding("utf8");
child.stdin.on("error", () => {
// If the child exits before fully consuming stdin, the close/error path
// below already decides the real result. Ignore broken-pipe noise here.
});
child.stdin.end(input.prompt);
child.stdout.on("data", (chunk: string) => {
stdout += chunk;
stdoutBuffer += chunk;
const lines = stdoutBuffer.split(/\r?\n/);
stdoutBuffer = lines.pop() ?? "";
for (const line of lines) {
const traceEvent = tryParseTraceEvent(
line,
traceEvents.length + 1,
startedAtMs,
);
if (traceEvent) {
traceEvents.push(traceEvent);
}
const event = tryParseCodexLine(line);
if (!event) {
continue;
}
if (event.approvalRequested) {
handoffReason =
event.handoffReason ??
"The request needs tools or permissions outside the Discord-safe owner allowlist.";
child.kill();
return;
}
if (event.toolRequest) {
child.kill();
finish({
stdout,
stderr,
exitCode: null,
toolRequest: event.toolRequest,
timedOut: false,
});
return;
}
if (event.handoffReason && !handoffReason) {
handoffReason = event.handoffReason;
}
}
});
child.stderr.on("data", (chunk: string) => {
stderr += chunk;
});
child.on("error", (error) => {
finish({
stdout,
stderr: `${stderr}${error.message}\n`,
exitCode: null,
handoffReason:
"The local Codex executable was not available for automatic Discord replies.",
timedOut: false,
});
});
child.on("close", (code) => {
if (stdoutBuffer.trim().length > 0) {
const traceEvent = tryParseTraceEvent(
stdoutBuffer.trim(),
traceEvents.length + 1,
startedAtMs,
);
if (traceEvent) {
traceEvents.push(traceEvent);
}
const event = tryParseCodexLine(stdoutBuffer.trim());
if (event?.approvalRequested && !handoffReason) {
handoffReason =
event.handoffReason ??
"The request needs tools or permissions outside the Discord-safe owner allowlist.";
}
}
finish({
stdout,
stderr,
exitCode: code,
handoffReason,
timedOut: false,
});
});
const timeoutHandle = setTimeout(() => {
handoffReason = "The local Codex run exceeded the Discord reply timeout.";
child.kill();
finish({
stdout,
stderr,
exitCode: null,
handoffReason,
timedOut: true,
});
}, input.timeoutMs);
});
}
export function normalizeDiscordReply(stdout: string): NormalizedDiscordReply {
const messages: string[] = [];
for (const rawLine of stdout.split(/\r?\n/)) {
const trimmed = rawLine.trim();
if (trimmed.length === 0 || !trimmed.startsWith("{")) {
continue;
}
try {
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
const event = unwrapCodexEvent(parsed);
const type = typeof event.type === "string" ? event.type : "";
if (type.endsWith("_delta")) {
continue;
}
if (type === "agent_message") {
const message = readString(event, "message") ?? readString(event, "text");
if (message) {
messages.push(message.trim());
}
}
if (
type === "item.completed" &&
isRecord(event.item) &&
event.item.type === "agent_message"
) {
const message =
readString(event.item, "text") ?? readString(event.item, "message");
if (message) {
messages.push(message.trim());
}
}
} catch {
continue;
}
}
const reply = [...messages].reverse().find((message) => message.length > 0);
if (!reply) {
return { notifications: [] };
}
if (reply.startsWith(HANDOFF_SENTINEL)) {
return { notifications: [] };
}
if (reply.startsWith(TOOL_REQUEST_SENTINEL)) {
return { notifications: [] };
}
const notifications: ProviderNotificationIntent[] = [];
const replyLines: string[] = [];
for (const line of reply.split(/\r?\n/)) {
const trimmed = line.trim();
if (trimmed.startsWith(OWNER_NOTIFY_SENTINEL)) {
const notification = parseOwnerNotificationIntent(trimmed);
if (notification) {
notifications.push(notification);
}
continue;
}
replyLines.push(line);
}
const cleanedReply = replyLines.join("\n").trim();
return {
reply: cleanedReply.length > 0 ? cleanedReply : undefined,
notifications,
};
}
export async function executeHistoryLookup(
historyLookup: HistoryLookupTool,
context: ContextBundle,
request: HistoryToolRequest,
): Promise<ToolCallRecord> {
const filters: RetrievalFilters = {
corpusKind: "discord_history",
guildId: context.guildContext.guildId,
channelId: request.channelId,
authorId: request.authorId,
};
const results = await historyLookup.search(request.query, request.limit, filters);
return {
request,
results,
};
}
function buildMcpConfigArguments(mcpServers: CodexMcpServerConfig[]): string[] {
const argumentsList: string[] = [];
for (const server of mcpServers) {
argumentsList.push(
"-c",
`mcp_servers.${server.name}.command=${JSON.stringify(server.command)}`,
);
argumentsList.push(
"-c",
`mcp_servers.${server.name}.args=${JSON.stringify(server.args)}`,
);
if (server.cwd) {
argumentsList.push(
"-c",
`mcp_servers.${server.name}.cwd=${JSON.stringify(server.cwd)}`,
);
}
for (const [key, value] of Object.entries(server.env ?? {})) {
argumentsList.push(
"-c",
`mcp_servers.${server.name}.env.${key}=${JSON.stringify(value)}`,
);
}
}
return argumentsList;
}
function tryParseCodexLine(line: string):
| {
approvalRequested: boolean;
handoffReason?: string;
toolRequest?: HistoryToolRequest;
}
| undefined {
const trimmed = line.trim();
if (!trimmed.startsWith("{")) {
return undefined;
}
try {
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
const event = unwrapCodexEvent(parsed);
const type = typeof event?.type === "string" ? event.type : "";
if (type.includes("approval_request")) {
return {
approvalRequested: true,
handoffReason:
"The request needs tools or permissions outside the Discord-safe owner allowlist.",
};
}
if (type === "agent_message") {
const message = readString(event, "message") ?? readString(event, "text");
if (message?.trim().startsWith(HANDOFF_SENTINEL)) {
return {
approvalRequested: false,
handoffReason: message.trim().slice(HANDOFF_SENTINEL.length).trim(),
};
}
const toolRequest = parseHistoryToolRequest(message);
if (toolRequest) {
return {
approvalRequested: false,
toolRequest,
};
}
}
if (
type === "item.completed" &&
isRecord(event.item) &&
event.item.type === "agent_message"
) {
const message =
readString(event.item, "text") ?? readString(event.item, "message");
if (message?.trim().startsWith(HANDOFF_SENTINEL)) {
return {
approvalRequested: false,
handoffReason: message.trim().slice(HANDOFF_SENTINEL.length).trim(),
};
}
const toolRequest = parseHistoryToolRequest(message);
if (toolRequest) {
return {
approvalRequested: false,
toolRequest,
};
}
}
return undefined;
} catch {
return undefined;
}
}
function tryParseTraceEvent(
line: string,
sequence: number,
startedAtMs: number,
): CodexTraceEvent | undefined {
const trimmed = line.trim();
if (!trimmed.startsWith("{")) {
return undefined;
}
try {
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
const event = unwrapCodexEvent(parsed);
const type = typeof event.type === "string" ? event.type : "";
const timestampMs = Date.now();
const baseEvent = {
sequence,
timestamp: new Date(timestampMs).toISOString(),
elapsedMs: timestampMs - startedAtMs,
};
if (type === "thread.started") {
return {
...baseEvent,
kind: "thread_started",
};
}
if (type === "turn.started") {
return {
...baseEvent,
kind: "turn_started",
};
}
if (type === "turn.completed") {
return {
...baseEvent,
kind: "turn_completed",
usage: readUsage(event.usage),
};
}
if (type === "agent_message") {
const message = readString(event, "message") ?? readString(event, "text");
if (message) {
return {
...baseEvent,
kind: "agent_message",
message,
};
}
}
if (!isRecord(event.item)) {
return undefined;
}
const item = event.item;
const itemId = readString(item, "id");
if (type === "item.started" && item.type === "command_execution") {
return {
...baseEvent,
kind: "command_started",
itemId,
command: readString(item, "command") ?? "(unknown command)",
status: readString(item, "status") ?? "in_progress",
};
}
if (type === "item.completed" && item.type === "command_execution") {
return {
...baseEvent,
kind: "command_completed",
itemId,
command: readString(item, "command") ?? "(unknown command)",
status: readString(item, "status") ?? "(unknown)",
exitCode: readNumber(item.exit_code),
output: readString(item, "aggregated_output") ?? "",
};
}
if (type === "item.started" && item.type === "mcp_tool_call") {
return {
...baseEvent,
kind: "mcp_tool_started",
itemId,
server: readString(item, "server"),
tool: readString(item, "tool"),
status: readString(item, "status") ?? "in_progress",
arguments: readRecord(item.arguments),
};
}
if (type === "item.completed" && item.type === "mcp_tool_call") {
return {
...baseEvent,
kind: "mcp_tool_completed",
itemId,
server: readString(item, "server"),
tool: readString(item, "tool"),
status: readString(item, "status") ?? "(unknown)",
arguments: readRecord(item.arguments),
error: summarizeUnknown(item.error, 220),
resultPreview: summarizeToolResult(item.result),
};
}
if (type === "item.completed" && item.type === "agent_message") {
const message =
readString(item, "text") ?? readString(item, "message");
if (message) {
return {
...baseEvent,
kind: "agent_message",
itemId,
message,
};
}
}
return undefined;
} catch {
return undefined;
}
}
function unwrapCodexEvent(parsed: Record<string, unknown>): Record<string, unknown> {
if (isRecord(parsed.msg) && typeof parsed.msg.type === "string") {
return parsed.msg;
}
if (isRecord(parsed.payload) && typeof parsed.payload.type === "string") {
return parsed.payload;
}
return parsed;
}
function parseHistoryToolRequest(
message: string | undefined,
): HistoryToolRequest | undefined {
const trimmed = message?.trim();
if (!trimmed?.startsWith(TOOL_REQUEST_SENTINEL)) {
return undefined;
}
const payload = trimmed.slice(TOOL_REQUEST_SENTINEL.length).trim();
try {
const parsed = JSON.parse(payload) as Record<string, unknown>;
if (parsed.tool !== "search_history") {
return undefined;
}
const query = typeof parsed.query === "string" ? parsed.query.trim() : "";
if (query.length === 0) {
return undefined;
}
const rawLimit =
typeof parsed.limit === "number" ? parsed.limit : DEFAULT_HISTORY_LIMIT;
const limit = clamp(Math.trunc(rawLimit), 1, MAX_HISTORY_LIMIT);
return {
tool: "search_history",
query: query.slice(0, 240),
limit,
channelId:
typeof parsed.channelId === "string" ? parsed.channelId : undefined,
authorId: typeof parsed.authorId === "string" ? parsed.authorId : undefined,
};
} catch {
return undefined;
}
}
function parseOwnerNotificationIntent(
line: string,
): ProviderNotificationIntent | undefined {
const payload = line.slice(OWNER_NOTIFY_SENTINEL.length).trim();
if (payload.length === 0) {
return undefined;
}
try {
const parsed = JSON.parse(payload) as Record<string, unknown>;
const message =
typeof parsed.message === "string"
? parsed.message.trim().replace(/\s+/g, " ")
: "";
if (message.length === 0) {
return undefined;
}
return {
channel: "owner_dm",
reason: normalizeNotificationReason(parsed.reason),
message: message.slice(0, MAX_NOTIFICATION_MESSAGE_LENGTH),
};
} catch {
return undefined;
}
}
function normalizeNotificationReason(
value: unknown,
): ProviderNotificationReason {
if (value === "completion" || value === "failure" || value === "handoff") {
return value;
}
return "custom";
}