Repository navigation
Expand file tree
/
Copy pathcore-runtime.ts
More file actions
215 lines (187 loc) · 7.77 KB
/
Copy pathcore-runtime.ts
File metadata and controls
215 lines (187 loc) · 7.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import { JsonLinesTransport } from "../ipc/json-lines-transport.js";
import { IpcProtocol } from "../ipc/protocol.js";
import { AgentCore, type AgentCoreConfig } from "./agentcore.js";
import { Logger, ObservabilityProvider, type LogSink } from "./observability.js";
import { ProcessManager } from "../worker/process-manager.js";
import { OpenCodeAdapter } from "../worker/adapters/opencode-adapter.js";
import { ClaudeCodeAdapter } from "../worker/adapters/claude-code-adapter.js";
import { CodexCliAdapter } from "../worker/adapters/codex-cli-adapter.js";
import { CustomShellAdapter } from "../worker/adapters/custom-shell-adapter.js";
import { WorkerKind } from "../types/index.js";
import { createConnection, type Socket } from "node:net";
import { Readable, Writable } from "node:stream";
export interface CoreRuntimeOptions {
config?: AgentCoreConfig;
loggerComponent?: string;
logSink?: LogSink;
}
function isNonInteractive(): boolean {
// Treat either stream being a TTY as interactive.
return !(process.stdout.isTTY || process.stderr.isTTY);
}
function parseEnvBool(value: string | undefined): boolean | undefined {
if (value === undefined) return undefined;
const v = value.trim().toLowerCase();
if (v === "1" || v === "true" || v === "yes" || v === "on") return true;
if (v === "0" || v === "false" || v === "no" || v === "off") return false;
return undefined;
}
function parseDurationMs(input: string): number {
const trimmed = input.trim();
if (trimmed === "") throw new Error(`Invalid duration string: "${input}"`);
const tokenRe = /(\d+)(ms|s|m|h)/g;
let totalMs = 0;
let matchedLen = 0;
for (const match of trimmed.matchAll(tokenRe)) {
const value = parseInt(match[1]!, 10);
const unit = match[2]!;
matchedLen += match[0]!.length;
switch (unit) {
case "h":
totalMs += value * 3600_000;
break;
case "m":
totalMs += value * 60_000;
break;
case "s":
totalMs += value * 1000;
break;
case "ms":
totalMs += value;
break;
}
}
if (matchedLen !== trimmed.length || totalMs <= 0) {
throw new Error(`Invalid duration string: "${input}"`);
}
return totalMs;
}
export function startCoreRuntime(options: CoreRuntimeOptions = {}): void {
const config = options.config;
const logger = new Logger(options.loggerComponent ?? "main", config?.logLevel ?? "info", options.logSink);
// Mark this process as the Core runtime for logging/diagnostics.
if (!process.env.ROBOPPI_COMPONENT) process.env.ROBOPPI_COMPONENT = "core";
// IPC transport: JSONL over either stdio or a supervised IPC socket.
const ipcSocketPath = process.env.ROBOPPI_IPC_SOCKET_PATH?.trim();
const ipcSocketHost = process.env.ROBOPPI_IPC_SOCKET_HOST?.trim();
const ipcSocketPortRaw = process.env.ROBOPPI_IPC_SOCKET_PORT?.trim();
let ipcSocket: Socket | null = null;
let ipcSocketKind: "unix" | "tcp" | null = null;
let resolvedIpcHost: string | undefined;
let resolvedIpcPort: number | undefined;
// Prefer TCP host/port when provided (useful for environments where Unix sockets are blocked).
if (ipcSocketPortRaw) {
const port = Number.parseInt(ipcSocketPortRaw, 10);
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
throw new Error(`Invalid ROBOPPI_IPC_SOCKET_PORT: "${ipcSocketPortRaw}"`);
}
const host = ipcSocketHost && ipcSocketHost !== "" ? ipcSocketHost : "127.0.0.1";
ipcSocket = createConnection({ host, port });
ipcSocketKind = "tcp";
resolvedIpcHost = host;
resolvedIpcPort = port;
} else if (ipcSocketPath) {
ipcSocket = createConnection(ipcSocketPath);
ipcSocketKind = "unix";
}
if (ipcSocket) {
try {
ipcSocket.setNoDelay(true);
} catch {
// ignore
}
// Prevent unhandled socket errors from crashing the Core process.
ipcSocket.on("error", (err) => {
logger.error("IPC socket error", {
name: err.name,
message: err.message,
});
});
}
const stdin = ipcSocket
? (Readable.toWeb(ipcSocket) as unknown as ReadableStream<Uint8Array>)
: (() => {
// Prefer Bun's native stdin stream, but fall back to Node's process.stdin stream
// when Bun stdin stream plumbing differs in some environments.
const bunStdin = Bun.stdin as { stream?: () => ReadableStream<Uint8Array> } | undefined;
if (bunStdin?.stream) {
return bunStdin.stream() as ReadableStream<Uint8Array>;
}
return Readable.toWeb(process.stdin) as unknown as ReadableStream<Uint8Array>;
})();
const stdout = ipcSocket
? (Writable.toWeb(ipcSocket) as unknown as WritableStream<Uint8Array>)
: (Writable.toWeb(process.stdout) as unknown as WritableStream<Uint8Array>);
const transport = new JsonLinesTransport(stdin, stdout);
const protocol = new IpcProtocol(transport);
const observability = new ObservabilityProvider(config?.logLevel ?? "info", options.logSink);
const core = new AgentCore(protocol, config, observability);
// Keepalive output for environments that kill silent subprocesses.
// Enabled by default when non-interactive; can be overridden via env.
const keepaliveEnabled = parseEnvBool(process.env.ROBOPPI_KEEPALIVE) ?? isNonInteractive();
const keepaliveIntervalMs = (() => {
const raw = process.env.ROBOPPI_KEEPALIVE_INTERVAL ?? "10s";
try {
return parseDurationMs(raw);
} catch {
return 10_000;
}
})();
const keepaliveStartedAt = Date.now();
let keepaliveTimer: ReturnType<typeof setInterval> | null = null;
if (keepaliveEnabled) {
keepaliveTimer = setInterval(() => {
const activeWorkers = core.getWorkerGateway().getActiveWorkerCount();
if (activeWorkers <= 0) return;
const elapsedS = Math.floor((Date.now() - keepaliveStartedAt) / 1000);
logger.info("Keepalive", { activeWorkers, elapsedS });
}, keepaliveIntervalMs);
}
// Register built-in worker adapters so WORKER_TASK delegation works.
{
const pm = new ProcessManager();
core.getWorkerGateway().registerAdapter(WorkerKind.OPENCODE, new OpenCodeAdapter(pm));
core.getWorkerGateway().registerAdapter(WorkerKind.CLAUDE_CODE, new ClaudeCodeAdapter({}, pm));
core.getWorkerGateway().registerAdapter(WorkerKind.CODEX_CLI, new CodexCliAdapter(pm));
core.getWorkerGateway().registerAdapter(WorkerKind.CUSTOM, new CustomShellAdapter(pm));
}
let shuttingDown = false;
async function shutdown(reason: string): Promise<void> {
if (shuttingDown) return;
shuttingDown = true;
logger.info(`Shutting down: ${reason}`);
if (keepaliveTimer) {
clearInterval(keepaliveTimer);
}
try {
await core.shutdown();
} catch (err) {
logger.error("Error during shutdown", err);
}
process.exit(0);
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("uncaughtException", (err) => {
logger.error("Uncaught exception", { name: err.name, message: err.message, stack: err.stack });
shutdown("uncaughtException").catch(() => process.exit(1));
});
process.on("unhandledRejection", (reason) => {
logger.error("Unhandled rejection", reason);
shutdown("unhandledRejection").catch(() => process.exit(1));
});
logger.info("Roboppi core starting", {
config: {
concurrency: config?.budget?.maxConcurrency ?? 10,
rps: config?.budget?.maxRps ?? 50,
logLevel: config?.logLevel ?? "info",
},
});
core.start();
logger.info("Roboppi core started, awaiting IPC messages", {
transport: ipcSocket ? (ipcSocketKind === "tcp" ? "tcp" : "socket") : "stdio",
socketPath: ipcSocketKind === "unix" ? (ipcSocketPath || undefined) : undefined,
socketHost: ipcSocketKind === "tcp" ? resolvedIpcHost : undefined,
socketPort: ipcSocketKind === "tcp" ? resolvedIpcPort : undefined,
});
}