@rivet-dev/agentos-core 0.0.0-main.29b0797 → 0.0.0-main.5ac5a14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent-os.js CHANGED
@@ -3,14 +3,18 @@ import { randomUUID } from "node:crypto";
3
3
  import { existsSync, readdirSync, statSync } from "node:fs";
4
4
  import { join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import * as executionProtocol from "@rivet-dev/agentos-runtime-core/protocol";
7
+ import { SidecarRejectedError } from "@rivet-dev/agentos-runtime-core/sidecar-errors";
6
8
  import { validateBindings } from "./bindings.js";
7
9
  import { zodToJsonSchema } from "./bindings-zod.js";
8
10
  import { parseAgentOsOptions } from "./options-schema.js";
11
+ import { getSandboxDisposeHooks, resolveSandboxOptions, } from "./sandbox.js";
9
12
  import { resolvePublishedSidecarBinary } from "./sidecar/binary.js";
10
13
  import { findCargoBinary, resolveCargoBinary } from "./sidecar/cargo.js";
11
14
  const ACP_PROTOCOL_VERSION = 1;
12
15
  const ACP_EXTENSION_NAMESPACE = "dev.rivet.agent-os.acp";
13
16
  const SHELL_DISPOSE_TIMEOUT_MS = 5_000;
17
+ const PROCESS_OUTPUT_EVENT_LIMIT = 1_024;
14
18
  function safeWireU64(value) {
15
19
  const result = Number(value);
16
20
  if (!Number.isSafeInteger(result) || result < 0) {
@@ -1427,12 +1431,212 @@ async function registerBindingsOnSidecar(client, session, vm, bindings) {
1427
1431
  }
1428
1432
  return buildBindingReference(bindings);
1429
1433
  }
1434
+ function executionIdentity(options) {
1435
+ return {
1436
+ contextId: options.contextId ?? null,
1437
+ };
1438
+ }
1439
+ function executionOutput(options, background = false) {
1440
+ if (options.output?.retainEvents && !options.contextId && !background) {
1441
+ throw new Error("retainEvents is available only for spawned processes; use output.capture for attached runs");
1442
+ }
1443
+ const capture = options.output?.capture;
1444
+ return {
1445
+ capture: capture === "all"
1446
+ ? executionProtocol.ExecutionOutputCapture.All
1447
+ : capture === "stderr"
1448
+ ? executionProtocol.ExecutionOutputCapture.Stderr
1449
+ : capture === "none"
1450
+ ? executionProtocol.ExecutionOutputCapture.None
1451
+ : null,
1452
+ retainEvents: options.output?.retainEvents ?? null,
1453
+ };
1454
+ }
1455
+ function executionBytes(data) {
1456
+ if (data === undefined)
1457
+ return null;
1458
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
1459
+ return bytes.slice().buffer;
1460
+ }
1461
+ function processExecutionOptions(options = {}, internal) {
1462
+ const background = internal?.background ?? false;
1463
+ return {
1464
+ identity: executionIdentity(options),
1465
+ output: executionOutput(options, background),
1466
+ operationId: internal?.executionId ?? null,
1467
+ background,
1468
+ cwd: options.cwd ?? null,
1469
+ env: options.env ? new Map(Object.entries(options.env)) : null,
1470
+ args: options.args ?? [],
1471
+ stdin: executionBytes(options.stdin),
1472
+ timeoutMs: protocolTimeout(options.timeoutMs),
1473
+ pty: options.pty
1474
+ ? {
1475
+ cols: options.pty.cols === undefined
1476
+ ? null
1477
+ : protocolUnsigned(options.pty.cols, 0xffff, "pty.cols"),
1478
+ rows: options.pty.rows === undefined
1479
+ ? null
1480
+ : protocolUnsigned(options.pty.rows, 0xffff, "pty.rows"),
1481
+ }
1482
+ : null,
1483
+ };
1484
+ }
1485
+ function protocolUnsigned(value, maximum, name) {
1486
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
1487
+ throw new RangeError(`${name} must be an integer between 0 and ${maximum}`);
1488
+ }
1489
+ return value;
1490
+ }
1491
+ function protocolTimeout(value) {
1492
+ if (value === undefined)
1493
+ return null;
1494
+ if (!Number.isSafeInteger(value) || value < 0) {
1495
+ throw new RangeError("timeoutMs must be a non-negative safe integer");
1496
+ }
1497
+ return BigInt(value);
1498
+ }
1499
+ function safeProtocolNumber(value, name) {
1500
+ const result = Number(value);
1501
+ if (!Number.isSafeInteger(result)) {
1502
+ throw new RangeError(`${name} exceeds JavaScript's safe integer range`);
1503
+ }
1504
+ return result;
1505
+ }
1506
+ function mapExecutionState(state) {
1507
+ const mapped = state.toLowerCase();
1508
+ return (mapped === "creating" ? "idle" : mapped);
1509
+ }
1510
+ function mapExecutionOutcome(outcome) {
1511
+ return outcome === executionProtocol.ExecutionOutcome.TimedOut
1512
+ ? "timed_out"
1513
+ : outcome.toLowerCase();
1514
+ }
1515
+ function mapExecutionDescriptor(descriptor) {
1516
+ return {
1517
+ contextId: descriptor.executionId,
1518
+ state: mapExecutionState(descriptor.state),
1519
+ ...(descriptor.retainedLanguage
1520
+ ? {
1521
+ language: descriptor.retainedLanguage ===
1522
+ executionProtocol.RetainedExecutionLanguage.JavaScript
1523
+ ? "javascript"
1524
+ : "python",
1525
+ }
1526
+ : {}),
1527
+ createdAtMs: safeProtocolNumber(descriptor.createdAtMs, "createdAtMs"),
1528
+ ...(descriptor.lastStartedAtMs !== null
1529
+ ? {
1530
+ lastStartedAtMs: safeProtocolNumber(descriptor.lastStartedAtMs, "lastStartedAtMs"),
1531
+ }
1532
+ : {}),
1533
+ ...(descriptor.lastCompletedAtMs !== null
1534
+ ? {
1535
+ lastCompletedAtMs: safeProtocolNumber(descriptor.lastCompletedAtMs, "lastCompletedAtMs"),
1536
+ }
1537
+ : {}),
1538
+ };
1539
+ }
1540
+ function mapExecutionError(error) {
1541
+ if (!error)
1542
+ return undefined;
1543
+ return {
1544
+ code: error.code,
1545
+ name: error.name,
1546
+ message: error.message,
1547
+ ...(error.stack ? { stack: error.stack } : {}),
1548
+ ...(error.details
1549
+ ? { details: JSON.parse(error.details) }
1550
+ : {}),
1551
+ };
1552
+ }
1553
+ function mapExecutionResult(result) {
1554
+ const base = {
1555
+ ...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
1556
+ ...(result.stdout !== null
1557
+ ? { stdout: new TextDecoder().decode(result.stdout) }
1558
+ : {}),
1559
+ ...(result.stderr !== null
1560
+ ? { stderr: new TextDecoder().decode(result.stderr) }
1561
+ : {}),
1562
+ ...(result.stdoutTruncated !== null
1563
+ ? { stdoutTruncated: result.stdoutTruncated }
1564
+ : {}),
1565
+ ...(result.stderrTruncated !== null
1566
+ ? { stderrTruncated: result.stderrTruncated }
1567
+ : {}),
1568
+ ...(result.evaluationValue
1569
+ ? { evaluationValue: JSON.parse(result.evaluationValue) }
1570
+ : {}),
1571
+ ...(result.typeScriptCheckResult
1572
+ ? {
1573
+ typeScriptCheckResult: JSON.parse(result.typeScriptCheckResult),
1574
+ }
1575
+ : {}),
1576
+ };
1577
+ const outcome = mapExecutionOutcome(result.outcome);
1578
+ if (outcome === "succeeded") {
1579
+ return { ...base, outcome };
1580
+ }
1581
+ return {
1582
+ ...base,
1583
+ outcome,
1584
+ error: mapExecutionError(result.error) ?? {
1585
+ code: "execution_failed",
1586
+ name: "ExecutionError",
1587
+ message: `execution completed with ${outcome}`,
1588
+ },
1589
+ };
1590
+ }
1591
+ function mapTypeScriptCheckResult(result) {
1592
+ if (result.outcome !== "succeeded") {
1593
+ return { ...result, diagnostics: [] };
1594
+ }
1595
+ const data = result.typeScriptCheckResult;
1596
+ if (!data || Array.isArray(data) || typeof data !== "object") {
1597
+ throw new Error("TypeScript checker returned no diagnostic result");
1598
+ }
1599
+ const hasErrors = data.hasErrors;
1600
+ const diagnostics = data.diagnostics;
1601
+ if (typeof hasErrors !== "boolean" || !Array.isArray(diagnostics)) {
1602
+ throw new Error("TypeScript checker returned an invalid diagnostic result");
1603
+ }
1604
+ return {
1605
+ ...result,
1606
+ hasErrors,
1607
+ diagnostics: diagnostics,
1608
+ };
1609
+ }
1610
+ function mapExecutionOutputEvent(event) {
1611
+ return {
1612
+ executionId: event.executionId,
1613
+ generation: safeProtocolNumber(event.generation, "execution generation"),
1614
+ ...(event.processId ? { processId: event.processId } : {}),
1615
+ sequence: safeProtocolNumber(event.sequence, "execution output sequence"),
1616
+ channel: event.channel.toLowerCase(),
1617
+ chunk: new Uint8Array(event.chunk),
1618
+ timestampMs: safeProtocolNumber(event.timestampMs, "execution timestamp"),
1619
+ };
1620
+ }
1621
+ function mapExecutionCompletedEvent(event) {
1622
+ return {
1623
+ executionId: event.executionId,
1624
+ generation: safeProtocolNumber(event.generation, "execution generation"),
1625
+ outcome: mapExecutionOutcome(event.outcome),
1626
+ ...(event.exitCode !== null ? { exitCode: event.exitCode } : {}),
1627
+ ...(event.error ? { error: mapExecutionError(event.error) } : {}),
1628
+ };
1629
+ }
1430
1630
  export class AgentOs {
1431
1631
  #kernel;
1432
1632
  sidecar;
1433
1633
  _durableSessionEventHandlers = new Map();
1434
1634
  _agentExitHandlers = new Map();
1435
1635
  _processes = new Map();
1636
+ _languageProcesses = new Map();
1637
+ _languageProcessIds = new Map();
1638
+ _executionOutputHandlers = new Map();
1639
+ _executionCompletedHandlers = new Map();
1436
1640
  _shells = new Map();
1437
1641
  // Value is the recorded exit code (undefined until/unless the exit
1438
1642
  // resolves) so waitShell can still report it after the entry is dropped.
@@ -1457,6 +1661,120 @@ export class AgentOs {
1457
1661
  _agentStderrHandler;
1458
1662
  _agentExitHandler;
1459
1663
  _limitWarningHandler;
1664
+ _disposeHooks = [];
1665
+ /**
1666
+ * Execute commands and manage child processes in the VM.
1667
+ *
1668
+ * `exec` evaluates a command with the configured shell, while `execFile`
1669
+ * invokes an executable directly with an argv array.
1670
+ */
1671
+ process = {
1672
+ exec: this._exec.bind(this),
1673
+ execFile: this._execFile.bind(this),
1674
+ spawn: this._spawnProcess.bind(this),
1675
+ get: this._getProcess.bind(this),
1676
+ list: this._listProcesses.bind(this),
1677
+ tree: this._processTree.bind(this),
1678
+ wait: this._waitProcess.bind(this),
1679
+ signal: this._signalProcess.bind(this),
1680
+ kill: this._killProcess.bind(this),
1681
+ writeStdin: this._writeProcessStdin.bind(this),
1682
+ closeStdin: this._closeProcessStdin.bind(this),
1683
+ resizePty: this._resizeProcessPty.bind(this),
1684
+ readOutput: this._readProcessOutput.bind(this),
1685
+ };
1686
+ javascript = {
1687
+ execute: this._executeJavaScript.bind(this),
1688
+ evaluate: this._evaluateJavaScript.bind(this),
1689
+ executeFile: this._executeJavaScriptFile.bind(this),
1690
+ spawn: this._spawnJavaScript.bind(this),
1691
+ spawnFile: this._spawnJavaScriptFile.bind(this),
1692
+ npm: {
1693
+ install: this._installNpmPackages.bind(this),
1694
+ runScript: this._executeNpmScript.bind(this),
1695
+ runPackage: this._executeNpmPackage.bind(this),
1696
+ },
1697
+ };
1698
+ typescript = {
1699
+ execute: this._executeTypeScript.bind(this),
1700
+ evaluate: this._evaluateTypeScript.bind(this),
1701
+ executeFile: this._executeTypeScriptFile.bind(this),
1702
+ spawn: this._spawnTypeScript.bind(this),
1703
+ spawnFile: this._spawnTypeScriptFile.bind(this),
1704
+ check: this._checkTypeScript.bind(this),
1705
+ checkProject: this._checkTypeScriptProject.bind(this),
1706
+ };
1707
+ python = {
1708
+ execute: this._executePython.bind(this),
1709
+ evaluate: this._evaluatePython.bind(this),
1710
+ executeFile: this._executePythonFile.bind(this),
1711
+ executeModule: this._executePythonModule.bind(this),
1712
+ spawn: this._spawnPython.bind(this),
1713
+ spawnFile: this._spawnPythonFile.bind(this),
1714
+ spawnModule: this._spawnPythonModule.bind(this),
1715
+ install: this._installPythonPackages.bind(this),
1716
+ };
1717
+ contexts = {
1718
+ get: this._getContext.bind(this),
1719
+ list: this._listContexts.bind(this),
1720
+ reset: this._resetContext.bind(this),
1721
+ delete: this._deleteContext.bind(this),
1722
+ };
1723
+ terminal = {
1724
+ open: this._openTerminal.bind(this),
1725
+ write: this._writeTerminal.bind(this),
1726
+ resize: this._resizeTerminal.bind(this),
1727
+ wait: this._waitTerminal.bind(this),
1728
+ close: this._closeTerminal.bind(this),
1729
+ };
1730
+ filesystem = {
1731
+ readFile: this._readFile.bind(this),
1732
+ writeFile: this._writeFile.bind(this),
1733
+ readFiles: this._readFiles.bind(this),
1734
+ writeFiles: this._writeFiles.bind(this),
1735
+ stat: this._stat.bind(this),
1736
+ mkdir: this._mkdir.bind(this),
1737
+ readdir: this._readdir.bind(this),
1738
+ readdirEntries: this._readdirEntries.bind(this),
1739
+ readdirRecursive: this._readdirRecursive.bind(this),
1740
+ exists: this._exists.bind(this),
1741
+ move: this._move.bind(this),
1742
+ remove: this._remove.bind(this),
1743
+ export: this._exportRootFilesystem.bind(this),
1744
+ mount: this._mountFs.bind(this),
1745
+ unmount: this._unmountFs.bind(this),
1746
+ listMounts: this._listMounts.bind(this),
1747
+ };
1748
+ network = {
1749
+ httpRequest: this._httpRequest.bind(this),
1750
+ };
1751
+ software = {
1752
+ list: this._listSoftware.bind(this),
1753
+ link: this._linkSoftware.bind(this),
1754
+ };
1755
+ agents = {
1756
+ list: this._listAgents.bind(this),
1757
+ };
1758
+ sessions = {
1759
+ open: this._openSession.bind(this),
1760
+ get: this._getSession.bind(this),
1761
+ list: this._listSessions.bind(this),
1762
+ delete: this._deleteSession.bind(this),
1763
+ unload: this._unloadSession.bind(this),
1764
+ prompt: this._prompt.bind(this),
1765
+ cancelPrompt: this._cancelPrompt.bind(this),
1766
+ respondPermission: this._respondPermission.bind(this),
1767
+ readHistory: this._readHistory.bind(this),
1768
+ getConfig: this._getSessionConfig.bind(this),
1769
+ setConfigOption: this._setSessionConfigOption.bind(this),
1770
+ getCapabilities: this._getSessionCapabilities.bind(this),
1771
+ getAgentInfo: this._getSessionAgentInfo.bind(this),
1772
+ };
1773
+ cron = {
1774
+ schedule: this._scheduleCron.bind(this),
1775
+ list: this._listCronJobs.bind(this),
1776
+ cancel: this._cancelCronJob.bind(this),
1777
+ };
1460
1778
  constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, agentStderrHandler, agentExitHandler, limitWarningHandler) {
1461
1779
  this.#kernel = kernel;
1462
1780
  this.sidecar = sidecar;
@@ -1522,13 +1840,15 @@ export class AgentOs {
1522
1840
  // sidecar owns agent resolution, agent enumeration, and agent snapshot
1523
1841
  // bundle loading from the projected package dirs.
1524
1842
  const localMounts = await resolveCompatLocalMounts(options?.mounts);
1525
- const bindings = options?.bindings;
1526
- if (bindings && bindings.length > 0) {
1527
- validateBindings(bindings);
1843
+ if (options?.bindings && options.bindings.length > 0) {
1844
+ validateBindings(options.bindings);
1528
1845
  }
1529
- // Resolve the sidecar handle up front so every VM created here leases the
1530
- // one shared native sidecar process owned by that handle.
1846
+ // Resolve the sidecar handle before starting an external sandbox so option
1847
+ // validation failures cannot leak provider resources.
1531
1848
  const sidecar = resolveAgentOsSidecar(options?.sidecar);
1849
+ options = await resolveSandboxOptions(options);
1850
+ const sandboxDisposeHooks = getSandboxDisposeHooks(options);
1851
+ const bindings = options.bindings;
1532
1852
  const createVmAdmin = async () => {
1533
1853
  // The `/opt/agentos` projection is built by the sidecar from the
1534
1854
  // forwarded `packages` (it owns the staging dir + read-only mount, and
@@ -1736,27 +2056,688 @@ export class AgentOs {
1736
2056
  vm._bindings = vmAdmin.bindings;
1737
2057
  vm._bindingReference = vmAdmin.bindingReference;
1738
2058
  vm._permissions = vmAdmin.permissions;
2059
+ vm._disposeHooks.push(...sandboxDisposeHooks);
1739
2060
  vm._installSidecarRequestHandler();
1740
2061
  vm._cronManager = new CronManager(vm, options?.scheduleDriver ?? new TimerScheduleDriver());
1741
2062
  return vm;
1742
2063
  }
1743
2064
  catch (error) {
1744
- await sidecarLease?.dispose().catch(() => { });
2065
+ const cleanupErrors = [];
2066
+ try {
2067
+ await sidecarLease?.dispose();
2068
+ }
2069
+ catch (disposeError) {
2070
+ cleanupErrors.push(disposeError);
2071
+ }
2072
+ const sandboxCleanupResults = await Promise.allSettled(sandboxDisposeHooks.map((hook) => hook()));
2073
+ cleanupErrors.push(...sandboxCleanupResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
2074
+ if (cleanupErrors.length > 0) {
2075
+ throw new AggregateError([error, ...cleanupErrors], "AgentOS VM creation and cleanup failed");
2076
+ }
1745
2077
  throw error;
1746
2078
  }
1747
2079
  }
2080
+ async createContext(contextId) {
2081
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "create_context", request: { contextId } });
2082
+ if (response.type !== "execution_descriptor") {
2083
+ throw new Error(`unexpected createContext response: ${response.type}`);
2084
+ }
2085
+ }
2086
+ async _executionOperation(payload, options, background = false) {
2087
+ if (options.signal?.aborted) {
2088
+ throw options.signal.reason ?? new DOMException("Aborted", "AbortError");
2089
+ }
2090
+ let executionId;
2091
+ const earlyOutput = [];
2092
+ const completedBeforeAdmission = new Set();
2093
+ let resolveCompletion;
2094
+ const completion = new Promise((resolve) => {
2095
+ resolveCompletion = resolve;
2096
+ });
2097
+ let warnedAboutEarlyEvents = false;
2098
+ const warnAboutEarlyEvents = () => {
2099
+ if (warnedAboutEarlyEvents)
2100
+ return;
2101
+ warnedAboutEarlyEvents = true;
2102
+ console.warn("[agentos] execution admission received more than 1024 early events; use process.readOutput() to replay any omitted output");
2103
+ };
2104
+ const deliverOutput = (event) => {
2105
+ if (event.channel === "stderr")
2106
+ options.onStderr?.(event.chunk);
2107
+ else
2108
+ options.onStdout?.(event.chunk);
2109
+ };
2110
+ const unsubscribeOutput = this._onExecutionOutput("*", (event) => {
2111
+ if (executionId === undefined) {
2112
+ if (earlyOutput.length < 1_024)
2113
+ earlyOutput.push(event);
2114
+ else
2115
+ warnAboutEarlyEvents();
2116
+ return;
2117
+ }
2118
+ if (event.executionId === executionId)
2119
+ deliverOutput(event);
2120
+ });
2121
+ let completed = false;
2122
+ const unsubscribeCompleted = this._onExecutionCompleted("*", (event) => {
2123
+ if (executionId === undefined) {
2124
+ if (completedBeforeAdmission.size < 1_024) {
2125
+ completedBeforeAdmission.add(event.executionId);
2126
+ }
2127
+ else
2128
+ warnAboutEarlyEvents();
2129
+ return;
2130
+ }
2131
+ if (event.executionId === executionId) {
2132
+ completed = true;
2133
+ resolveCompletion?.();
2134
+ }
2135
+ });
2136
+ let response;
2137
+ try {
2138
+ response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, payload);
2139
+ }
2140
+ catch (error) {
2141
+ unsubscribeOutput();
2142
+ unsubscribeCompleted();
2143
+ throw error;
2144
+ }
2145
+ if (response.type !== "execution_accepted") {
2146
+ unsubscribeOutput();
2147
+ unsubscribeCompleted();
2148
+ throw new Error(`unexpected execution response: ${response.type}`);
2149
+ }
2150
+ const descriptor = response.response.execution;
2151
+ executionId = response.response.operationId;
2152
+ for (const event of earlyOutput) {
2153
+ if (event.executionId === executionId)
2154
+ deliverOutput(event);
2155
+ }
2156
+ completed = completedBeforeAdmission.has(executionId);
2157
+ if (completed)
2158
+ resolveCompletion?.();
2159
+ const abort = () => {
2160
+ // Admitted executions report cancellation through their structured result.
2161
+ void this._cancelExecution(response.response.operationId).catch((error) => {
2162
+ console.error("[agentos] failed to cancel aborted execution", error);
2163
+ });
2164
+ };
2165
+ options.signal?.addEventListener("abort", abort, { once: true });
2166
+ if (options.signal?.aborted)
2167
+ abort();
2168
+ const cleanup = () => {
2169
+ unsubscribeOutput();
2170
+ unsubscribeCompleted();
2171
+ options.signal?.removeEventListener("abort", abort);
2172
+ };
2173
+ if (background) {
2174
+ if (!descriptor || descriptor.pid === null) {
2175
+ cleanup();
2176
+ throw new Error("spawned execution admission returned no process id");
2177
+ }
2178
+ if (completed)
2179
+ cleanup();
2180
+ else {
2181
+ const unsubscribeBackgroundCompletion = this._onExecutionCompleted(descriptor.executionId, () => {
2182
+ unsubscribeBackgroundCompletion();
2183
+ cleanup();
2184
+ });
2185
+ }
2186
+ return {
2187
+ executionId: descriptor.executionId,
2188
+ pid: descriptor.pid,
2189
+ completed,
2190
+ completion,
2191
+ createdAtMs: safeProtocolNumber(descriptor.createdAtMs, "process startedAtMs"),
2192
+ };
2193
+ }
2194
+ try {
2195
+ await completion;
2196
+ return await this._waitExecutionResult(response.response.operationId);
2197
+ }
2198
+ finally {
2199
+ cleanup();
2200
+ }
2201
+ }
2202
+ async _spawnLanguageOperation(buildPayload, options, language) {
2203
+ if ("contextId" in options) {
2204
+ throw new Error("contextId is not supported for spawned processes; contexts are for attached operations");
2205
+ }
2206
+ const executionId = `process-${randomUUID()}`;
2207
+ const internalOptions = {
2208
+ ...options,
2209
+ output: options.output,
2210
+ };
2211
+ const admitted = (await this._executionOperation(buildPayload(internalOptions, executionId), internalOptions, true));
2212
+ const descriptor = {
2213
+ pid: admitted.pid,
2214
+ state: "running",
2215
+ language,
2216
+ startedAtMs: admitted.createdAtMs,
2217
+ };
2218
+ this._languageProcesses.set(admitted.pid, {
2219
+ executionId: admitted.executionId,
2220
+ descriptor,
2221
+ outputHandlers: new Set(),
2222
+ exitHandlers: new Set(),
2223
+ });
2224
+ this._languageProcessIds.set(admitted.executionId, admitted.pid);
2225
+ const reconcileCompletion = async () => {
2226
+ if (!this._languageProcesses.get(admitted.pid)?.exit) {
2227
+ await this._waitProcess(admitted.pid);
2228
+ }
2229
+ };
2230
+ if (admitted.completed)
2231
+ await reconcileCompletion();
2232
+ else {
2233
+ void admitted.completion
2234
+ .then(reconcileCompletion)
2235
+ .catch((error) => console.error("[agentos] failed to reconcile spawned process completion", error));
2236
+ }
2237
+ if (options.signal) {
2238
+ const abort = () => {
2239
+ void this._killProcess(admitted.pid);
2240
+ };
2241
+ options.signal.addEventListener("abort", abort, { once: true });
2242
+ }
2243
+ return this._languageProcesses.get(admitted.pid)?.descriptor ?? descriptor;
2244
+ }
2245
+ async _exec(command, options = {}) {
2246
+ return (await this._executionOperation({
2247
+ type: "shell_execution",
2248
+ request: { process: processExecutionOptions(options), command },
2249
+ }, options));
2250
+ }
2251
+ async _execFile(command, args = [], options = {}) {
2252
+ const operationOptions = { ...options, args: [...args] };
2253
+ return (await this._executionOperation({
2254
+ type: "argv_execution",
2255
+ request: {
2256
+ process: processExecutionOptions(operationOptions),
2257
+ command,
2258
+ },
2259
+ }, operationOptions));
2260
+ }
2261
+ /** @deprecated Use `process.exec()` for lifecycle-aware execution. */
1748
2262
  async exec(command, options) {
1749
2263
  return this.#kernel.exec(command, options);
1750
2264
  }
2265
+ /** @deprecated Use `process.execFile()` for lifecycle-aware execution. */
1751
2266
  async execArgv(command, args = [], options) {
1752
2267
  const kernel = this.#kernel;
1753
2268
  return kernel.execArgv(command, args, options);
1754
2269
  }
1755
- _trackProcess(proc, command, args, outputHandlers, exitHandlers) {
2270
+ async _executeJavaScript(source, options = {}) {
2271
+ return (await this._executionOperation({
2272
+ type: "javascript_execution",
2273
+ request: {
2274
+ process: processExecutionOptions(options),
2275
+ source,
2276
+ format: options.format === "commonjs"
2277
+ ? executionProtocol.JavaScriptModuleFormat.CommonJs
2278
+ : executionProtocol.JavaScriptModuleFormat.Module,
2279
+ filePath: options.filePath ?? null,
2280
+ inputs: options.inputs ? JSON.stringify(options.inputs) : null,
2281
+ },
2282
+ }, options));
2283
+ }
2284
+ async _evaluationOperation(payload, options) {
2285
+ const result = (await this._executionOperation(payload, options));
2286
+ if (result.outcome !== "succeeded")
2287
+ return result;
2288
+ const value = result.evaluationValue;
2289
+ if (value === undefined) {
2290
+ throw new Error("successful evaluation did not return a JSON-serializable value");
2291
+ }
2292
+ return { ...result, value: value };
2293
+ }
2294
+ async _evaluateJavaScript(expression, options = {}) {
2295
+ return this._evaluationOperation({
2296
+ type: "javascript_evaluation",
2297
+ request: {
2298
+ process: processExecutionOptions(options),
2299
+ expression,
2300
+ format: options.format === "commonjs"
2301
+ ? executionProtocol.JavaScriptModuleFormat.CommonJs
2302
+ : executionProtocol.JavaScriptModuleFormat.Module,
2303
+ filePath: options.filePath ?? null,
2304
+ inputs: options.inputs ? JSON.stringify(options.inputs) : null,
2305
+ },
2306
+ }, options);
2307
+ }
2308
+ async _executeJavaScriptFile(path, options = {}) {
2309
+ return (await this._executionOperation({
2310
+ type: "javascript_file_execution",
2311
+ request: { process: processExecutionOptions(options), path },
2312
+ }, options));
2313
+ }
2314
+ _spawnJavaScript(source, options = {}) {
2315
+ return this._spawnLanguageOperation((operationOptions, executionId) => ({
2316
+ type: "javascript_execution",
2317
+ request: {
2318
+ process: processExecutionOptions(operationOptions, {
2319
+ background: true,
2320
+ executionId,
2321
+ }),
2322
+ source,
2323
+ format: executionProtocol.JavaScriptModuleFormat.Module,
2324
+ filePath: null,
2325
+ inputs: null,
2326
+ },
2327
+ }), options, "javascript");
2328
+ }
2329
+ _spawnJavaScriptFile(path, options = {}) {
2330
+ return this._spawnLanguageOperation((operationOptions, executionId) => ({
2331
+ type: "javascript_file_execution",
2332
+ request: {
2333
+ process: processExecutionOptions(operationOptions, {
2334
+ background: true,
2335
+ executionId,
2336
+ }),
2337
+ path,
2338
+ },
2339
+ }), options, "javascript");
2340
+ }
2341
+ async _executeTypeScript(source, options = {}) {
2342
+ return (await this._executionOperation({
2343
+ type: "typescript_execution",
2344
+ request: {
2345
+ process: processExecutionOptions(options),
2346
+ source,
2347
+ filePath: options.filePath ?? null,
2348
+ tsconfigPath: options.tsconfigPath ?? null,
2349
+ compilerOptions: options.compilerOptions
2350
+ ? JSON.stringify(options.compilerOptions)
2351
+ : null,
2352
+ inputs: options.inputs ? JSON.stringify(options.inputs) : null,
2353
+ },
2354
+ }, options));
2355
+ }
2356
+ async _evaluateTypeScript(expression, options = {}) {
2357
+ return this._evaluationOperation({
2358
+ type: "typescript_evaluation",
2359
+ request: {
2360
+ process: processExecutionOptions(options),
2361
+ expression,
2362
+ filePath: options.filePath ?? null,
2363
+ tsconfigPath: options.tsconfigPath ?? null,
2364
+ compilerOptions: options.compilerOptions
2365
+ ? JSON.stringify(options.compilerOptions)
2366
+ : null,
2367
+ inputs: options.inputs ? JSON.stringify(options.inputs) : null,
2368
+ },
2369
+ }, options);
2370
+ }
2371
+ async _executeTypeScriptFile(path, options = {}) {
2372
+ return (await this._executionOperation({
2373
+ type: "typescript_file_execution",
2374
+ request: {
2375
+ process: processExecutionOptions(options),
2376
+ path,
2377
+ tsconfigPath: options.tsconfigPath ?? null,
2378
+ compilerOptions: options.compilerOptions
2379
+ ? JSON.stringify(options.compilerOptions)
2380
+ : null,
2381
+ },
2382
+ }, options));
2383
+ }
2384
+ _spawnTypeScript(source, options = {}) {
2385
+ return this._spawnLanguageOperation((operationOptions, executionId) => ({
2386
+ type: "typescript_execution",
2387
+ request: {
2388
+ process: processExecutionOptions(operationOptions, {
2389
+ background: true,
2390
+ executionId,
2391
+ }),
2392
+ source,
2393
+ filePath: null,
2394
+ tsconfigPath: null,
2395
+ compilerOptions: null,
2396
+ inputs: null,
2397
+ },
2398
+ }), options, "javascript");
2399
+ }
2400
+ _spawnTypeScriptFile(path, options = {}) {
2401
+ return this._spawnLanguageOperation((operationOptions, executionId) => ({
2402
+ type: "typescript_file_execution",
2403
+ request: {
2404
+ process: processExecutionOptions(operationOptions, {
2405
+ background: true,
2406
+ executionId,
2407
+ }),
2408
+ path,
2409
+ tsconfigPath: null,
2410
+ compilerOptions: null,
2411
+ },
2412
+ }), options, "javascript");
2413
+ }
2414
+ async _checkTypeScript(source, options = {}) {
2415
+ const result = (await this._executionOperation({
2416
+ type: "typescript_check",
2417
+ request: {
2418
+ identity: executionIdentity(options),
2419
+ output: executionOutput(options),
2420
+ source,
2421
+ cwd: options.cwd ?? null,
2422
+ filePath: options.filePath ?? null,
2423
+ tsconfigPath: options.tsconfigPath ?? null,
2424
+ compilerOptions: options.compilerOptions
2425
+ ? JSON.stringify(options.compilerOptions)
2426
+ : null,
2427
+ timeoutMs: protocolTimeout(options.timeoutMs),
2428
+ },
2429
+ }, options));
2430
+ return mapTypeScriptCheckResult(result);
2431
+ }
2432
+ async _checkTypeScriptProject(options = {}) {
2433
+ const result = (await this._executionOperation({
2434
+ type: "typescript_project_check",
2435
+ request: {
2436
+ identity: executionIdentity(options),
2437
+ output: executionOutput(options),
2438
+ cwd: options.cwd ?? null,
2439
+ tsconfigPath: options.tsconfigPath ?? null,
2440
+ timeoutMs: protocolTimeout(options.timeoutMs),
2441
+ },
2442
+ }, options));
2443
+ return mapTypeScriptCheckResult(result);
2444
+ }
2445
+ async _installNpmPackages(packagesOrOptions = {}, maybeOptions = {}) {
2446
+ if (typeof packagesOrOptions === "string" ||
2447
+ Array.isArray(packagesOrOptions)) {
2448
+ const packages = Array.isArray(packagesOrOptions)
2449
+ ? packagesOrOptions
2450
+ : [packagesOrOptions];
2451
+ return (await this._executionOperation({
2452
+ type: "npm_package_install",
2453
+ request: {
2454
+ identity: executionIdentity(maybeOptions),
2455
+ output: executionOutput(maybeOptions),
2456
+ cwd: maybeOptions.cwd ?? null,
2457
+ env: maybeOptions.env
2458
+ ? new Map(Object.entries(maybeOptions.env))
2459
+ : null,
2460
+ timeoutMs: protocolTimeout(maybeOptions.timeoutMs),
2461
+ packages,
2462
+ dev: maybeOptions.dev ?? null,
2463
+ global: maybeOptions.global ?? null,
2464
+ },
2465
+ }, maybeOptions));
2466
+ }
2467
+ const options = packagesOrOptions;
2468
+ return (await this._executionOperation({
2469
+ type: "npm_project_install",
2470
+ request: {
2471
+ identity: executionIdentity(options),
2472
+ output: executionOutput(options),
2473
+ cwd: options.cwd ?? null,
2474
+ env: options.env ? new Map(Object.entries(options.env)) : null,
2475
+ timeoutMs: protocolTimeout(options.timeoutMs),
2476
+ frozen: options.frozen ?? null,
2477
+ },
2478
+ }, options));
2479
+ }
2480
+ async _executeNpmScript(script, options = {}) {
2481
+ return (await this._executionOperation({
2482
+ type: "npm_script_execution",
2483
+ request: { process: processExecutionOptions(options), script },
2484
+ }, options));
2485
+ }
2486
+ async _executeNpmPackage(packageSpec, options = {}) {
2487
+ return (await this._executionOperation({
2488
+ type: "npm_package_execution",
2489
+ request: {
2490
+ process: processExecutionOptions(options),
2491
+ packageSpec,
2492
+ binary: options.binary ?? null,
2493
+ },
2494
+ }, options));
2495
+ }
2496
+ async _executePython(source, options = {}) {
2497
+ return (await this._executionOperation({
2498
+ type: "python_execution",
2499
+ request: {
2500
+ process: processExecutionOptions(options),
2501
+ source,
2502
+ inputs: options.inputs ? JSON.stringify(options.inputs) : null,
2503
+ },
2504
+ }, options));
2505
+ }
2506
+ async _evaluatePython(expression, options = {}) {
2507
+ return this._evaluationOperation({
2508
+ type: "python_evaluation",
2509
+ request: {
2510
+ process: processExecutionOptions(options),
2511
+ expression,
2512
+ inputs: options.inputs ? JSON.stringify(options.inputs) : null,
2513
+ },
2514
+ }, options);
2515
+ }
2516
+ async _executePythonFile(path, options = {}) {
2517
+ return (await this._executionOperation({
2518
+ type: "python_file_execution",
2519
+ request: { process: processExecutionOptions(options), path },
2520
+ }, options));
2521
+ }
2522
+ async _executePythonModule(module, options = {}) {
2523
+ return (await this._executionOperation({
2524
+ type: "python_module_execution",
2525
+ request: { process: processExecutionOptions(options), module },
2526
+ }, options));
2527
+ }
2528
+ _spawnPython(source, options = {}) {
2529
+ return this._spawnLanguageOperation((operationOptions, executionId) => ({
2530
+ type: "python_execution",
2531
+ request: {
2532
+ process: processExecutionOptions(operationOptions, {
2533
+ background: true,
2534
+ executionId,
2535
+ }),
2536
+ source,
2537
+ inputs: null,
2538
+ },
2539
+ }), options, "python");
2540
+ }
2541
+ _spawnPythonFile(path, options = {}) {
2542
+ return this._spawnLanguageOperation((operationOptions, executionId) => ({
2543
+ type: "python_file_execution",
2544
+ request: {
2545
+ process: processExecutionOptions(operationOptions, {
2546
+ background: true,
2547
+ executionId,
2548
+ }),
2549
+ path,
2550
+ },
2551
+ }), options, "python");
2552
+ }
2553
+ _spawnPythonModule(module, options = {}) {
2554
+ return this._spawnLanguageOperation((operationOptions, executionId) => ({
2555
+ type: "python_module_execution",
2556
+ request: {
2557
+ process: processExecutionOptions(operationOptions, {
2558
+ background: true,
2559
+ executionId,
2560
+ }),
2561
+ module,
2562
+ },
2563
+ }), options, "python");
2564
+ }
2565
+ async _installPythonPackages(packagesOrOptions = {}, maybeOptions = {}) {
2566
+ const packages = typeof packagesOrOptions === "string"
2567
+ ? [packagesOrOptions]
2568
+ : Array.isArray(packagesOrOptions)
2569
+ ? packagesOrOptions
2570
+ : [];
2571
+ const options = typeof packagesOrOptions === "string" || Array.isArray(packagesOrOptions)
2572
+ ? maybeOptions
2573
+ : packagesOrOptions;
2574
+ return (await this._executionOperation({
2575
+ type: "python_install",
2576
+ request: {
2577
+ identity: executionIdentity(options),
2578
+ output: executionOutput(options),
2579
+ cwd: options.cwd ?? null,
2580
+ env: options.env ? new Map(Object.entries(options.env)) : null,
2581
+ timeoutMs: protocolTimeout(options.timeoutMs),
2582
+ packages,
2583
+ upgrade: options.upgrade ?? null,
2584
+ requirementsFile: options.requirementsFile ?? null,
2585
+ indexUrl: options.indexUrl ?? null,
2586
+ extraIndexUrls: options.extraIndexUrls ?? [],
2587
+ },
2588
+ }, options));
2589
+ }
2590
+ async _getContext(contextId) {
2591
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "get_execution", request: { executionId: contextId } });
2592
+ if (response.type !== "execution_descriptor") {
2593
+ throw new Error(`unexpected getContext response: ${response.type}`);
2594
+ }
2595
+ return mapExecutionDescriptor(response.response.execution);
2596
+ }
2597
+ async _listContexts() {
2598
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "list_executions" });
2599
+ if (response.type !== "execution_list") {
2600
+ throw new Error(`unexpected listContexts response: ${response.type}`);
2601
+ }
2602
+ return response.response.executions.map(mapExecutionDescriptor);
2603
+ }
2604
+ async _waitExecutionResult(executionId) {
2605
+ let resolveCompletion;
2606
+ const completion = new Promise((resolve) => {
2607
+ resolveCompletion = resolve;
2608
+ });
2609
+ const unsubscribe = this._onExecutionCompleted(executionId, () => {
2610
+ resolveCompletion?.();
2611
+ });
2612
+ let response;
2613
+ try {
2614
+ try {
2615
+ response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "wait_execution", request: { executionId } });
2616
+ }
2617
+ catch (error) {
2618
+ if (!(error instanceof SidecarRejectedError) ||
2619
+ error.detail.code !== "execution_busy") {
2620
+ throw error;
2621
+ }
2622
+ await completion;
2623
+ response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "wait_execution", request: { executionId } });
2624
+ }
2625
+ }
2626
+ finally {
2627
+ unsubscribe();
2628
+ }
2629
+ if (response.type !== "execution_completed") {
2630
+ throw new Error(`unexpected waitExecution response: ${response.type}`);
2631
+ }
2632
+ return mapExecutionResult(response.response);
2633
+ }
2634
+ async _cancelExecution(executionId) {
2635
+ await this._descriptorLifecycleRequest("cancel_execution", executionId);
2636
+ }
2637
+ async _signalExecution(executionId, signal) {
2638
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "signal_execution", request: { executionId, signal } });
2639
+ if (response.type !== "execution_descriptor") {
2640
+ throw new Error(`unexpected signalExecution response: ${response.type}`);
2641
+ }
2642
+ }
2643
+ async _resetContext(contextId) {
2644
+ await this._descriptorLifecycleRequest("reset_execution", contextId);
2645
+ }
2646
+ async _descriptorLifecycleRequest(type, executionId) {
2647
+ const payload = type === "cancel_execution"
2648
+ ? { type, request: { executionId } }
2649
+ : { type, request: { executionId } };
2650
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, payload);
2651
+ if (response.type !== "execution_descriptor") {
2652
+ throw new Error(`unexpected ${type} response: ${response.type}`);
2653
+ }
2654
+ return mapExecutionDescriptor(response.response.execution);
2655
+ }
2656
+ async _deleteContext(contextId) {
2657
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "delete_execution", request: { executionId: contextId } });
2658
+ if (response.type !== "execution_deleted") {
2659
+ throw new Error(`unexpected deleteContext response: ${response.type}`);
2660
+ }
2661
+ }
2662
+ async _writeExecutionStdin(executionId, data) {
2663
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, {
2664
+ type: "write_execution_stdin",
2665
+ request: { executionId, chunk: executionBytes(data) },
2666
+ });
2667
+ if (response.type !== "execution_io") {
2668
+ throw new Error(`unexpected writeExecutionStdin response: ${response.type}`);
2669
+ }
2670
+ }
2671
+ async _closeExecutionStdin(executionId) {
2672
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "close_execution_stdin", request: { executionId } });
2673
+ if (response.type !== "execution_io") {
2674
+ throw new Error(`unexpected closeExecutionStdin response: ${response.type}`);
2675
+ }
2676
+ }
2677
+ async _resizeExecutionPty(executionId, size) {
2678
+ const cols = protocolUnsigned(size.cols, 0xffff, "size.cols");
2679
+ const rows = protocolUnsigned(size.rows, 0xffff, "size.rows");
2680
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, {
2681
+ type: "resize_execution_pty",
2682
+ request: { executionId, cols, rows },
2683
+ });
2684
+ if (response.type !== "execution_io") {
2685
+ throw new Error(`unexpected resizeExecutionPty response: ${response.type}`);
2686
+ }
2687
+ }
2688
+ async _readExecutionOutput(executionId, options = {}) {
2689
+ const limit = options.limit === undefined
2690
+ ? null
2691
+ : protocolUnsigned(options.limit, 0xffff_ffff, "limit");
2692
+ const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, {
2693
+ type: "read_execution_output",
2694
+ request: {
2695
+ executionId,
2696
+ cursor: options.cursor ?? null,
2697
+ limit,
2698
+ },
2699
+ });
2700
+ if (response.type !== "execution_output_page") {
2701
+ throw new Error(`unexpected readExecutionOutput response: ${response.type}`);
2702
+ }
2703
+ return {
2704
+ events: response.response.events.map(mapExecutionOutputEvent),
2705
+ nextCursor: response.response.nextCursor,
2706
+ hasMore: response.response.hasMore,
2707
+ truncated: response.response.truncated,
2708
+ };
2709
+ }
2710
+ _onExecutionOutput(executionId, handler) {
2711
+ const handlers = this._executionOutputHandlers.get(executionId) ?? new Set();
2712
+ handlers.add(handler);
2713
+ this._executionOutputHandlers.set(executionId, handlers);
2714
+ return () => {
2715
+ handlers.delete(handler);
2716
+ if (handlers.size === 0)
2717
+ this._executionOutputHandlers.delete(executionId);
2718
+ };
2719
+ }
2720
+ _onExecutionCompleted(executionId, handler) {
2721
+ const handlers = this._executionCompletedHandlers.get(executionId) ?? new Set();
2722
+ handlers.add(handler);
2723
+ this._executionCompletedHandlers.set(executionId, handlers);
2724
+ return () => {
2725
+ handlers.delete(handler);
2726
+ if (handlers.size === 0)
2727
+ this._executionCompletedHandlers.delete(executionId);
2728
+ };
2729
+ }
2730
+ _trackProcess(proc, command, args, retainEvents, outputHandlers, exitHandlers) {
1756
2731
  const entry = {
1757
2732
  proc,
1758
2733
  command,
1759
2734
  args,
2735
+ startedAtMs: Date.now(),
2736
+ retainEvents,
2737
+ events: [],
2738
+ nextSequence: 0,
2739
+ signal: undefined,
2740
+ exit: undefined,
1760
2741
  outputHandlers,
1761
2742
  exitHandlers,
1762
2743
  };
@@ -1767,38 +2748,84 @@ export class AgentOs {
1767
2748
  // `_processes` is a process table for this VM's lifetime; it is freed wholesale
1768
2749
  // in dispose(). (H5: the leak was that dispose() never cleared it.)
1769
2750
  void proc.wait().then((code) => {
2751
+ const exit = {
2752
+ pid: proc.pid,
2753
+ outcome: entry.signal ? "signalled" : "exited",
2754
+ exitCode: code,
2755
+ ...(entry.signal ? { signal: entry.signal } : {}),
2756
+ };
2757
+ entry.exit = exit;
1770
2758
  for (const h of exitHandlers)
1771
- h({ pid: proc.pid, exitCode: code });
2759
+ h(exit);
1772
2760
  });
1773
- return { pid: proc.pid };
2761
+ return {
2762
+ pid: proc.pid,
2763
+ state: "running",
2764
+ command,
2765
+ startedAtMs: entry.startedAtMs,
2766
+ };
1774
2767
  }
1775
- spawn(command, args, options) {
2768
+ async _spawnProcess(command, args = [], options = {}) {
1776
2769
  const outputHandlers = new Set();
1777
2770
  const exitHandlers = new Set();
2771
+ const recordOutput = (channel, data) => {
2772
+ const entry = this._processes.get(proc.pid);
2773
+ if (!entry?.retainEvents)
2774
+ return;
2775
+ if (entry.events.length >= PROCESS_OUTPUT_EVENT_LIMIT) {
2776
+ entry.events.shift();
2777
+ }
2778
+ entry.events.push({
2779
+ pid: proc.pid,
2780
+ sequence: entry.nextSequence++,
2781
+ channel,
2782
+ chunk: data,
2783
+ timestampMs: Date.now(),
2784
+ });
2785
+ };
1778
2786
  const proc = this.#kernel.spawn(command, args, {
1779
- ...options,
2787
+ cwd: options.cwd,
2788
+ env: options.env,
2789
+ stdin: options.stdin,
2790
+ timeout: options.timeoutMs,
2791
+ streamStdin: true,
1780
2792
  onStdout: (data) => {
2793
+ recordOutput("stdout", data);
2794
+ options?.onStdout?.(data);
1781
2795
  for (const h of outputHandlers) {
1782
2796
  h({ pid: proc.pid, stream: "stdout", data });
1783
2797
  }
1784
2798
  },
1785
2799
  onStderr: (data) => {
2800
+ recordOutput("stderr", data);
2801
+ options?.onStderr?.(data);
1786
2802
  for (const h of outputHandlers) {
1787
2803
  h({ pid: proc.pid, stream: "stderr", data });
1788
2804
  }
1789
2805
  },
1790
2806
  });
1791
- return this._trackProcess(proc, command, args, outputHandlers, exitHandlers);
2807
+ return this._trackProcess(proc, command, args, options.output?.retainEvents ?? false, outputHandlers, exitHandlers);
2808
+ }
2809
+ spawn(command, args = [], options) {
2810
+ return this.process.spawn(command, args, options);
1792
2811
  }
1793
2812
  /** Write data to a process's stdin. */
1794
- writeProcessStdin(pid, data) {
2813
+ _writeProcessStdin(pid, data) {
2814
+ const language = this._languageProcesses.get(pid);
2815
+ if (language) {
2816
+ return this._writeExecutionStdin(language.executionId, data);
2817
+ }
1795
2818
  const entry = this._processes.get(pid);
1796
2819
  if (!entry)
1797
2820
  throw new Error(`Process not found: ${pid}`);
1798
2821
  return entry.proc.writeStdin(data);
1799
2822
  }
1800
2823
  /** Close a process's stdin stream. */
1801
- closeProcessStdin(pid) {
2824
+ _closeProcessStdin(pid) {
2825
+ const language = this._languageProcesses.get(pid);
2826
+ if (language) {
2827
+ return this._closeExecutionStdin(language.executionId);
2828
+ }
1802
2829
  const entry = this._processes.get(pid);
1803
2830
  if (!entry)
1804
2831
  throw new Error(`Process not found: ${pid}`);
@@ -1806,7 +2833,7 @@ export class AgentOs {
1806
2833
  }
1807
2834
  /** Subscribe to stdout and stderr from a process. */
1808
2835
  onProcessOutput(pid, handler) {
1809
- const entry = this._processes.get(pid);
2836
+ const entry = this._processes.get(pid) ?? this._languageProcesses.get(pid);
1810
2837
  if (!entry)
1811
2838
  throw new Error(`Process not found: ${pid}`);
1812
2839
  entry.outputHandlers.add(handler);
@@ -1816,12 +2843,12 @@ export class AgentOs {
1816
2843
  }
1817
2844
  /** Subscribe to process exit. Returns an unsubscribe function. */
1818
2845
  onProcessExit(pid, handler) {
1819
- const entry = this._processes.get(pid);
2846
+ const entry = this._processes.get(pid) ?? this._languageProcesses.get(pid);
1820
2847
  if (!entry)
1821
2848
  throw new Error(`Process not found: ${pid}`);
1822
2849
  // If already exited, call immediately.
1823
- if (entry.proc.exitCode !== null) {
1824
- handler({ pid, exitCode: entry.proc.exitCode });
2850
+ if ("exit" in entry && entry.exit) {
2851
+ handler(entry.exit);
1825
2852
  return () => { };
1826
2853
  }
1827
2854
  entry.exitHandlers.add(handler);
@@ -1830,11 +2857,39 @@ export class AgentOs {
1830
2857
  };
1831
2858
  }
1832
2859
  /** Wait for a process to exit. Returns the exit code. */
1833
- waitProcess(pid) {
2860
+ async _waitProcess(pid) {
2861
+ const language = this._languageProcesses.get(pid);
2862
+ if (language) {
2863
+ if (language.exit)
2864
+ return language.exit;
2865
+ const result = await this._waitExecutionResult(language.executionId);
2866
+ const exit = {
2867
+ pid,
2868
+ outcome: result.outcome === "timed_out"
2869
+ ? "timed_out"
2870
+ : result.outcome === "cancelled"
2871
+ ? "signalled"
2872
+ : "exited",
2873
+ ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
2874
+ ...(language.signal ? { signal: language.signal } : {}),
2875
+ };
2876
+ language.exit = exit;
2877
+ language.descriptor = { ...language.descriptor, state: "exited" };
2878
+ for (const handler of language.exitHandlers)
2879
+ handler(exit);
2880
+ return exit;
2881
+ }
1834
2882
  const entry = this._processes.get(pid);
1835
2883
  if (!entry)
1836
2884
  throw new Error(`Process not found: ${pid}`);
1837
- return entry.proc.wait();
2885
+ if (entry.exit)
2886
+ return entry.exit;
2887
+ const exitCode = await entry.proc.wait();
2888
+ return (entry.exit ?? {
2889
+ pid,
2890
+ outcome: "exited",
2891
+ exitCode,
2892
+ });
1838
2893
  }
1839
2894
  _assertSafeAbsolutePath(path) {
1840
2895
  if (!path.startsWith("/")) {
@@ -1853,15 +2908,15 @@ export class AgentOs {
1853
2908
  _vfs() {
1854
2909
  return this.#kernel.vfs;
1855
2910
  }
1856
- async readFile(path) {
2911
+ async _readFile(path) {
1857
2912
  this._assertSafeAbsolutePath(path);
1858
2913
  return this.#kernel.readFile(path);
1859
2914
  }
1860
- async writeFile(path, content) {
2915
+ async _writeFile(path, content) {
1861
2916
  this._assertWritableAbsolutePath(path);
1862
2917
  return this.#kernel.writeFile(path, content);
1863
2918
  }
1864
- async writeFiles(entries) {
2919
+ async _writeFiles(entries) {
1865
2920
  const results = [];
1866
2921
  for (const entry of entries) {
1867
2922
  try {
@@ -1884,7 +2939,7 @@ export class AgentOs {
1884
2939
  }
1885
2940
  return results;
1886
2941
  }
1887
- async readFiles(paths) {
2942
+ async _readFiles(paths) {
1888
2943
  const results = [];
1889
2944
  for (const path of paths) {
1890
2945
  try {
@@ -1913,18 +2968,18 @@ export class AgentOs {
1913
2968
  // exists() loop makes `mkdir -p` cost O(components * tree).
1914
2969
  await this.#kernel.mkdir(path);
1915
2970
  }
1916
- async mkdir(path, options) {
2971
+ async _mkdir(path, options) {
1917
2972
  if (options?.recursive) {
1918
2973
  return this._mkdirp(path);
1919
2974
  }
1920
2975
  this._assertSafeAbsolutePath(path);
1921
2976
  return this.#kernel.mkdir(path);
1922
2977
  }
1923
- async readdir(path) {
2978
+ async _readdir(path) {
1924
2979
  this._assertSafeAbsolutePath(path);
1925
2980
  return this.#kernel.readdir(path);
1926
2981
  }
1927
- async readdirEntries(path) {
2982
+ async _readdirEntries(path) {
1928
2983
  this._assertSafeAbsolutePath(path);
1929
2984
  const entries = await this.#kernel.readdirRecursive(path, { maxDepth: 0 });
1930
2985
  return entries.map((entry) => ({
@@ -1933,7 +2988,7 @@ export class AgentOs {
1933
2988
  isSymbolicLink: entry.isSymbolicLink,
1934
2989
  }));
1935
2990
  }
1936
- async readdirRecursive(path, options) {
2991
+ async _readdirRecursive(path, options) {
1937
2992
  this._assertSafeAbsolutePath(path);
1938
2993
  const exclude = options?.exclude ? new Set(options.exclude) : undefined;
1939
2994
  const entries = await this.#kernel.readdirRecursive(path, {
@@ -1963,15 +3018,15 @@ export class AgentOs {
1963
3018
  }
1964
3019
  return results;
1965
3020
  }
1966
- async stat(path) {
3021
+ async _stat(path) {
1967
3022
  this._assertSafeAbsolutePath(path);
1968
3023
  return this.#kernel.stat(path);
1969
3024
  }
1970
- async exists(path) {
3025
+ async _exists(path) {
1971
3026
  this._assertSafeAbsolutePath(path);
1972
3027
  return this.#kernel.exists(path);
1973
3028
  }
1974
- async exportRootFilesystem(options) {
3029
+ async _exportRootFilesystem(options) {
1975
3030
  if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes <= 0) {
1976
3031
  throw new RangeError("maxBytes must be a positive safe integer");
1977
3032
  }
@@ -1991,7 +3046,7 @@ export class AgentOs {
1991
3046
  * after the returned promise settles; a delivery failure rejects instead of
1992
3047
  * leaving the mount silently host-only.
1993
3048
  */
1994
- async mountFs(descriptor) {
3049
+ async _mountFs(descriptor) {
1995
3050
  this._assertSafeAbsolutePath(descriptor.path);
1996
3051
  if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
1997
3052
  throw new Error("portable dynamic mounts require the native sidecar");
@@ -2005,30 +3060,94 @@ export class AgentOs {
2005
3060
  },
2006
3061
  });
2007
3062
  }
2008
- async unmountFs(path) {
3063
+ async _unmountFs(path) {
2009
3064
  this._assertSafeAbsolutePath(path);
2010
3065
  if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
2011
3066
  throw new Error("portable dynamic mounts require the native sidecar");
2012
3067
  }
2013
3068
  await this.#kernel.unmountDescriptor(path);
2014
3069
  }
2015
- async listMounts() {
3070
+ async _listMounts() {
2016
3071
  if (!(this.#kernel instanceof NativeSidecarKernelProxy))
2017
3072
  return [];
2018
3073
  return this.#kernel.listMounts();
2019
3074
  }
2020
- async move(from, to) {
3075
+ async _move(from, to) {
2021
3076
  this._assertWritableAbsolutePath(from);
2022
3077
  this._assertWritableAbsolutePath(to);
2023
3078
  await this.#kernel.movePath(from, to);
2024
3079
  }
2025
- async remove(path, options) {
3080
+ async _remove(path, options) {
2026
3081
  this._assertWritableAbsolutePath(path);
2027
3082
  await this.#kernel.removePath(path, {
2028
3083
  recursive: options?.recursive ?? false,
2029
3084
  });
2030
3085
  }
2031
- async httpRequest(request) {
3086
+ /** @deprecated Use `filesystem.readFile()`. */
3087
+ readFile(path) {
3088
+ return this.filesystem.readFile(path);
3089
+ }
3090
+ /** @deprecated Use `filesystem.writeFile()`. */
3091
+ writeFile(path, content) {
3092
+ return this.filesystem.writeFile(path, content);
3093
+ }
3094
+ /** @deprecated Use `filesystem.writeFiles()`. */
3095
+ writeFiles(entries) {
3096
+ return this.filesystem.writeFiles(entries);
3097
+ }
3098
+ /** @deprecated Use `filesystem.readFiles()`. */
3099
+ readFiles(paths) {
3100
+ return this.filesystem.readFiles(paths);
3101
+ }
3102
+ /** @deprecated Use `filesystem.mkdir()`. */
3103
+ mkdir(path, options) {
3104
+ return this.filesystem.mkdir(path, options);
3105
+ }
3106
+ /** @deprecated Use `filesystem.readdir()`. */
3107
+ readdir(path) {
3108
+ return this.filesystem.readdir(path);
3109
+ }
3110
+ /** @deprecated Use `filesystem.readdirEntries()`. */
3111
+ readdirEntries(path) {
3112
+ return this.filesystem.readdirEntries(path);
3113
+ }
3114
+ /** @deprecated Use `filesystem.readdirRecursive()`. */
3115
+ readdirRecursive(path, options) {
3116
+ return this.filesystem.readdirRecursive(path, options);
3117
+ }
3118
+ /** @deprecated Use `filesystem.stat()`. */
3119
+ stat(path) {
3120
+ return this.filesystem.stat(path);
3121
+ }
3122
+ /** @deprecated Use `filesystem.exists()`. */
3123
+ exists(path) {
3124
+ return this.filesystem.exists(path);
3125
+ }
3126
+ /** @deprecated Use `filesystem.export()`. */
3127
+ exportRootFilesystem(options) {
3128
+ return this.filesystem.export(options);
3129
+ }
3130
+ /** @deprecated Use `filesystem.mount()`. */
3131
+ mountFs(descriptor) {
3132
+ return this.filesystem.mount(descriptor);
3133
+ }
3134
+ /** @deprecated Use `filesystem.unmount()`. */
3135
+ unmountFs(path) {
3136
+ return this.filesystem.unmount(path);
3137
+ }
3138
+ /** @deprecated Use `filesystem.listMounts()`. */
3139
+ listMounts() {
3140
+ return this.filesystem.listMounts();
3141
+ }
3142
+ /** @deprecated Use `filesystem.move()`. */
3143
+ move(from, to) {
3144
+ return this.filesystem.move(from, to);
3145
+ }
3146
+ /** @deprecated Use `filesystem.remove()`. */
3147
+ remove(path, options) {
3148
+ return this.filesystem.remove(path, options);
3149
+ }
3150
+ async _httpRequest(request) {
2032
3151
  this._assertSafeAbsolutePath(request.path.split("?")[0] || "/");
2033
3152
  const method = request.method?.toUpperCase() ?? "GET";
2034
3153
  const body = typeof request.body === "string"
@@ -2056,7 +3175,79 @@ export class AgentOs {
2056
3175
  body: new Uint8Array(Buffer.from(responsePayload.body ?? "", "base64")),
2057
3176
  };
2058
3177
  }
2059
- openShell(options) {
3178
+ /** @deprecated Use `network.httpRequest()`. */
3179
+ httpRequest(request) {
3180
+ return this.network.httpRequest(request);
3181
+ }
3182
+ /**
3183
+ * Fetch an HTTP endpoint inside the VM while preserving duplicate response
3184
+ * headers and arbitrary binary bodies. agentOS Apps uses this compatibility
3185
+ * surface for its direct actor API.
3186
+ */
3187
+ async fetch(port, request) {
3188
+ const url = new URL(request.url);
3189
+ const responsePayload = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
3190
+ port,
3191
+ method: request.method,
3192
+ path: `${url.pathname}${url.search}`,
3193
+ headersJson: JSON.stringify(Object.fromEntries(request.headers.entries())),
3194
+ ...(request.method !== "GET" && request.method !== "HEAD"
3195
+ ? {
3196
+ bodyBase64: Buffer.from(await request.arrayBuffer()).toString("base64"),
3197
+ }
3198
+ : {}),
3199
+ }));
3200
+ const headers = new Headers();
3201
+ for (const [key, value] of responsePayload.headers ?? []) {
3202
+ headers.append(key, value);
3203
+ }
3204
+ return new Response(Buffer.from(responsePayload.body ?? "", "base64"), {
3205
+ status: responsePayload.status,
3206
+ statusText: responsePayload.statusText ?? "",
3207
+ headers,
3208
+ });
3209
+ }
3210
+ async fetchStreamStart(port, request) {
3211
+ const url = new URL(request.url);
3212
+ return JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
3213
+ port,
3214
+ method: request.method,
3215
+ path: `${url.pathname}${url.search}`,
3216
+ headersJson: JSON.stringify(Object.fromEntries(request.headers.entries())),
3217
+ ...(request.method !== "GET" && request.method !== "HEAD"
3218
+ ? {
3219
+ bodyBase64: Buffer.from(await request.arrayBuffer()).toString("base64"),
3220
+ }
3221
+ : {}),
3222
+ streamOperation: "start",
3223
+ }));
3224
+ }
3225
+ async fetchStreamRead(streamId, maxBytes = 64 * 1024) {
3226
+ const response = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
3227
+ port: 0,
3228
+ method: "GET",
3229
+ path: "/",
3230
+ headersJson: "{}",
3231
+ streamOperation: "read",
3232
+ streamId,
3233
+ maxBytes,
3234
+ }));
3235
+ return {
3236
+ body: new Uint8Array(Buffer.from(response.body, "base64")),
3237
+ done: response.done,
3238
+ };
3239
+ }
3240
+ async fetchStreamCancel(streamId) {
3241
+ await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
3242
+ port: 0,
3243
+ method: "GET",
3244
+ path: "/",
3245
+ headersJson: "{}",
3246
+ streamOperation: "cancel",
3247
+ streamId,
3248
+ });
3249
+ }
3250
+ _openTerminal(options) {
2060
3251
  const shellId = `shell-${++this._shellCounter}`;
2061
3252
  this._closedShellIds.delete(shellId);
2062
3253
  const dataHandlers = new Set();
@@ -2107,7 +3298,7 @@ export class AgentOs {
2107
3298
  return this.#kernel.connectTerminal(options);
2108
3299
  }
2109
3300
  /** Write data to a shell's PTY input. */
2110
- writeShell(shellId, data) {
3301
+ _writeTerminal(shellId, data) {
2111
3302
  const entry = this._shells.get(shellId);
2112
3303
  if (!entry)
2113
3304
  throw new Error(`Shell not found: ${shellId}`);
@@ -2150,7 +3341,7 @@ export class AgentOs {
2150
3341
  return () => entry.exitHandlers.delete(handler);
2151
3342
  }
2152
3343
  /** Notify a shell of terminal resize. */
2153
- resizeShell(shellId, cols, rows) {
3344
+ _resizeTerminal(shellId, cols, rows) {
2154
3345
  const entry = this._shells.get(shellId);
2155
3346
  if (!entry)
2156
3347
  throw new Error(`Shell not found: ${shellId}`);
@@ -2161,7 +3352,7 @@ export class AgentOs {
2161
3352
  * immediately for a shell that has already exited (within the closed-shell
2162
3353
  * retention window).
2163
3354
  */
2164
- waitShell(shellId) {
3355
+ _waitTerminal(shellId) {
2165
3356
  const entry = this._shells.get(shellId);
2166
3357
  if (!entry) {
2167
3358
  const exitCode = this._closedShellIds.get(shellId);
@@ -2172,7 +3363,7 @@ export class AgentOs {
2172
3363
  return entry.exitPromise;
2173
3364
  }
2174
3365
  /** Kill a shell process and remove it from tracking. */
2175
- closeShell(shellId) {
3366
+ _closeTerminal(shellId) {
2176
3367
  const entry = this._shells.get(shellId);
2177
3368
  if (!entry) {
2178
3369
  if (this._closedShellIds.has(shellId)) {
@@ -2184,6 +3375,26 @@ export class AgentOs {
2184
3375
  this._shells.delete(shellId);
2185
3376
  this._closedShellIds.add(shellId);
2186
3377
  }
3378
+ /** @deprecated Use `terminal.open()`. */
3379
+ openShell(options) {
3380
+ return this.terminal.open(options);
3381
+ }
3382
+ /** @deprecated Use `terminal.write()`. */
3383
+ writeShell(shellId, data) {
3384
+ return this.terminal.write(shellId, data);
3385
+ }
3386
+ /** @deprecated Use `terminal.resize()`. */
3387
+ resizeShell(shellId, cols, rows) {
3388
+ this.terminal.resize(shellId, cols, rows);
3389
+ }
3390
+ /** @deprecated Use `terminal.wait()`. */
3391
+ waitShell(shellId) {
3392
+ return this.terminal.wait(shellId);
3393
+ }
3394
+ /** @deprecated Use `terminal.close()`. */
3395
+ closeShell(shellId) {
3396
+ this.terminal.close(shellId);
3397
+ }
2187
3398
  _resolveVmPathToHostPath(vmPath) {
2188
3399
  const normalizedVmPath = posixPath.normalize(vmPath);
2189
3400
  for (const mount of this._hostMounts) {
@@ -2199,34 +3410,51 @@ export class AgentOs {
2199
3410
  return null;
2200
3411
  }
2201
3412
  /** Returns info about all processes spawned via spawn(). */
2202
- listProcesses() {
2203
- return [...this._processes.values()].map(({ proc, command, args }) => ({
2204
- pid: proc.pid,
2205
- command,
2206
- args,
2207
- running: proc.exitCode === null,
2208
- exitCode: proc.exitCode,
2209
- }));
3413
+ async _listProcesses() {
3414
+ return [
3415
+ ...[...this._processes.values()].map(({ proc, command, startedAtMs }) => ({
3416
+ pid: proc.pid,
3417
+ command,
3418
+ state: proc.exitCode === null ? "running" : "exited",
3419
+ startedAtMs,
3420
+ })),
3421
+ ...[...this._languageProcesses.values()].map((entry) => entry.descriptor),
3422
+ ];
2210
3423
  }
2211
3424
  /** Returns all kernel processes across all active runtimes (WASM and Node). */
2212
- allProcesses() {
3425
+ _listAllProcesses() {
2213
3426
  if (this.#kernel instanceof NativeSidecarKernelProxy) {
2214
3427
  return this.#kernel.snapshotProcesses();
2215
3428
  }
2216
3429
  return [...this.#kernel.processes.values()];
2217
3430
  }
2218
3431
  /** Returns processes organized as a tree using ppid relationships. */
2219
- processTree() {
2220
- const all = this.allProcesses();
3432
+ async _processTree() {
3433
+ const all = this._listAllProcesses();
2221
3434
  const nodeMap = new Map();
2222
3435
  // Index: create a tree node for each process
2223
3436
  for (const proc of all) {
2224
- nodeMap.set(proc.pid, { ...proc, children: [] });
3437
+ nodeMap.set(proc.pid, {
3438
+ pid: proc.pid,
3439
+ ppid: proc.ppid,
3440
+ command: proc.command,
3441
+ state: proc.status,
3442
+ startedAtMs: proc.startTime,
3443
+ children: [],
3444
+ });
3445
+ }
3446
+ for (const process of this._languageProcesses.values()) {
3447
+ if (!nodeMap.has(process.descriptor.pid)) {
3448
+ nodeMap.set(process.descriptor.pid, {
3449
+ ...process.descriptor,
3450
+ children: [],
3451
+ });
3452
+ }
2225
3453
  }
2226
3454
  // Wire: attach each node to its parent
2227
3455
  const roots = [];
2228
3456
  for (const node of nodeMap.values()) {
2229
- const parent = nodeMap.get(node.ppid);
3457
+ const parent = node.ppid === undefined ? undefined : nodeMap.get(node.ppid);
2230
3458
  if (parent) {
2231
3459
  parent.children.push(node);
2232
3460
  }
@@ -2237,7 +3465,10 @@ export class AgentOs {
2237
3465
  return roots;
2238
3466
  }
2239
3467
  /** Returns info about a specific process by PID. Throws if not found. */
2240
- getProcess(pid) {
3468
+ async _getProcess(pid) {
3469
+ const language = this._languageProcesses.get(pid);
3470
+ if (language)
3471
+ return language.descriptor;
2241
3472
  const entry = this._processes.get(pid);
2242
3473
  if (!entry) {
2243
3474
  throw new Error(`Process not found: ${pid}`);
@@ -2245,32 +3476,76 @@ export class AgentOs {
2245
3476
  return {
2246
3477
  pid: entry.proc.pid,
2247
3478
  command: entry.command,
2248
- args: entry.args,
2249
- running: entry.proc.exitCode === null,
2250
- exitCode: entry.proc.exitCode,
3479
+ state: entry.proc.exitCode === null ? "running" : "exited",
3480
+ startedAtMs: entry.startedAtMs,
2251
3481
  };
2252
3482
  }
2253
- /** Send SIGTERM to gracefully stop a process. No-op if already exited. */
2254
- stopProcess(pid) {
3483
+ async _signalProcess(pid, signal) {
3484
+ const language = this._languageProcesses.get(pid);
3485
+ if (language) {
3486
+ language.signal = signal;
3487
+ await this._signalExecution(language.executionId, signal);
3488
+ return;
3489
+ }
2255
3490
  const entry = this._processes.get(pid);
2256
3491
  if (!entry) {
2257
3492
  throw new Error(`Process not found: ${pid}`);
2258
3493
  }
2259
3494
  if (entry.proc.exitCode !== null)
2260
3495
  return;
2261
- entry.proc.kill();
3496
+ entry.signal = signal;
3497
+ const number = signal === "SIGKILL" ? 9 : signal === "SIGINT" ? 2 : 15;
3498
+ entry.proc.kill(number);
2262
3499
  }
2263
3500
  /** Send SIGKILL to force-kill a process. No-op if already exited. */
2264
- killProcess(pid) {
3501
+ async _killProcess(pid) {
3502
+ await this._signalProcess(pid, "SIGKILL");
3503
+ }
3504
+ async _resizeProcessPty(pid, size) {
3505
+ const language = this._languageProcesses.get(pid);
3506
+ if (!language) {
3507
+ throw new Error(`Process ${pid} does not have a managed PTY`);
3508
+ }
3509
+ await this._resizeExecutionPty(language.executionId, size);
3510
+ }
3511
+ async _readProcessOutput(pid, options = {}) {
3512
+ const language = this._languageProcesses.get(pid);
3513
+ if (language) {
3514
+ const page = await this._readExecutionOutput(language.executionId, {
3515
+ cursor: options.after === undefined ? undefined : `1:${options.after + 1}`,
3516
+ });
3517
+ return {
3518
+ pid,
3519
+ events: page.events.map((event) => ({
3520
+ pid,
3521
+ sequence: event.sequence,
3522
+ channel: event.channel,
3523
+ chunk: event.chunk,
3524
+ timestampMs: event.timestampMs,
3525
+ })),
3526
+ nextCursor: page.nextCursor,
3527
+ hasMore: page.hasMore,
3528
+ truncated: page.truncated,
3529
+ };
3530
+ }
2265
3531
  const entry = this._processes.get(pid);
2266
- if (!entry) {
3532
+ if (!entry)
2267
3533
  throw new Error(`Process not found: ${pid}`);
3534
+ if (!entry.retainEvents) {
3535
+ throw new Error(`Process ${pid} was not spawned with output.retainEvents enabled`);
2268
3536
  }
2269
- if (entry.proc.exitCode !== null)
2270
- return;
2271
- entry.proc.kill(9);
3537
+ const after = options.after ?? -1;
3538
+ const events = entry.events.filter((event) => event.sequence > after);
3539
+ return {
3540
+ pid,
3541
+ events,
3542
+ nextCursor: String(events.at(-1)?.sequence ?? after),
3543
+ hasMore: false,
3544
+ truncated: entry.events.length === PROCESS_OUTPUT_EVENT_LIMIT &&
3545
+ after < (entry.events[0]?.sequence ?? 0) - 1,
3546
+ };
2272
3547
  }
2273
- async openSession(input) {
3548
+ async _openSession(input) {
2274
3549
  const response = await this._sendAcpRequest({
2275
3550
  tag: "AcpOpenSessionRequest",
2276
3551
  val: {
@@ -2293,7 +3568,7 @@ export class AgentOs {
2293
3568
  throw new Error(`unexpected openSession response: ${response.tag}`);
2294
3569
  }
2295
3570
  }
2296
- async getSession(input) {
3571
+ async _getSession(input) {
2297
3572
  const response = await this._sendAcpRequest({
2298
3573
  tag: "AcpGetDurableSessionRequest",
2299
3574
  val: { sessionId: input?.sessionId ?? null },
@@ -2303,7 +3578,7 @@ export class AgentOs {
2303
3578
  }
2304
3579
  return decodeDurableSessionInfo(response.val.session);
2305
3580
  }
2306
- async listSessions(input) {
3581
+ async _listSessions(input) {
2307
3582
  const response = await this._sendAcpRequest({
2308
3583
  tag: "AcpListDurableSessionsRequest",
2309
3584
  val: { cursor: input?.cursor ?? null, limit: input?.limit ?? null },
@@ -2316,7 +3591,7 @@ export class AgentOs {
2316
3591
  nextCursor: response.val.nextCursor,
2317
3592
  };
2318
3593
  }
2319
- async deleteSession(input = {}) {
3594
+ async _deleteSession(input = {}) {
2320
3595
  const response = await this._sendAcpRequest({
2321
3596
  tag: "AcpDeleteSessionRequest",
2322
3597
  val: { sessionId: input.sessionId ?? null },
@@ -2325,7 +3600,7 @@ export class AgentOs {
2325
3600
  throw new Error(`unexpected deleteSession response: ${response.tag}`);
2326
3601
  }
2327
3602
  }
2328
- async unloadSession(input) {
3603
+ async _unloadSession(input) {
2329
3604
  const response = await this._sendAcpRequest({
2330
3605
  tag: "AcpUnloadSessionRequest",
2331
3606
  val: { sessionId: input?.sessionId ?? null },
@@ -2334,7 +3609,7 @@ export class AgentOs {
2334
3609
  throw new Error(`unexpected unloadSession response: ${response.tag}`);
2335
3610
  }
2336
3611
  }
2337
- async prompt(input) {
3612
+ async _prompt(input) {
2338
3613
  const response = await this._sendAcpRequest({
2339
3614
  tag: "AcpPromptRequest",
2340
3615
  val: {
@@ -2352,7 +3627,7 @@ export class AgentOs {
2352
3627
  stopReason: response.val.stopReason,
2353
3628
  };
2354
3629
  }
2355
- async cancelPrompt(input) {
3630
+ async _cancelPrompt(input) {
2356
3631
  const response = await this._sendAcpRequest({
2357
3632
  tag: "AcpCancelPromptRequest",
2358
3633
  val: { sessionId: input?.sessionId ?? null },
@@ -2366,7 +3641,7 @@ export class AgentOs {
2366
3641
  }
2367
3642
  return { status: response.val.status };
2368
3643
  }
2369
- async respondPermission(input) {
3644
+ async _respondPermission(input) {
2370
3645
  const response = await this._sendAcpRequest({
2371
3646
  tag: "AcpRespondPermissionRequest",
2372
3647
  val: {
@@ -2393,7 +3668,7 @@ export class AgentOs {
2393
3668
  reason: permissionTerminalReason(response.val.reason),
2394
3669
  };
2395
3670
  }
2396
- async readHistory(input) {
3671
+ async _readHistory(input) {
2397
3672
  const response = await this._sendAcpRequest({
2398
3673
  tag: "AcpReadHistoryRequest",
2399
3674
  val: {
@@ -2412,7 +3687,7 @@ export class AgentOs {
2412
3687
  hasMoreAfter: response.val.hasMoreAfter,
2413
3688
  };
2414
3689
  }
2415
- async getSessionConfig(input) {
3690
+ async _getSessionConfig(input) {
2416
3691
  const response = await this._sendAcpRequest({
2417
3692
  tag: "AcpGetSessionConfigRequest",
2418
3693
  val: { sessionId: input?.sessionId ?? null },
@@ -2425,7 +3700,7 @@ export class AgentOs {
2425
3700
  options: JSON.parse(response.val.options),
2426
3701
  };
2427
3702
  }
2428
- async setSessionConfigOption(input) {
3703
+ async _setSessionConfigOption(input) {
2429
3704
  const response = await this._sendAcpRequest({
2430
3705
  tag: "AcpSetSessionConfigOptionRequest",
2431
3706
  val: {
@@ -2442,7 +3717,7 @@ export class AgentOs {
2442
3717
  options: JSON.parse(response.val.options),
2443
3718
  };
2444
3719
  }
2445
- async getSessionCapabilities(input) {
3720
+ async _getSessionCapabilities(input) {
2446
3721
  const response = await this._sendAcpRequest({
2447
3722
  tag: "AcpGetSessionCapabilitiesRequest",
2448
3723
  val: { sessionId: input?.sessionId ?? null },
@@ -2454,7 +3729,7 @@ export class AgentOs {
2454
3729
  ? null
2455
3730
  : normalizeSessionCapabilities(JSON.parse(response.val.capabilities));
2456
3731
  }
2457
- async getSessionAgentInfo(input) {
3732
+ async _getSessionAgentInfo(input) {
2458
3733
  const response = await this._sendAcpRequest({
2459
3734
  tag: "AcpGetSessionAgentInfoRequest",
2460
3735
  val: { sessionId: input?.sessionId ?? null },
@@ -2466,6 +3741,58 @@ export class AgentOs {
2466
3741
  ? null
2467
3742
  : JSON.parse(response.val.agentInfo);
2468
3743
  }
3744
+ /** @deprecated Use `sessions.open()`. */
3745
+ openSession(input) {
3746
+ return this.sessions.open(input);
3747
+ }
3748
+ /** @deprecated Use `sessions.get()`. */
3749
+ getSession(input) {
3750
+ return this.sessions.get(input);
3751
+ }
3752
+ /** @deprecated Use `sessions.list()`. */
3753
+ listSessions(input) {
3754
+ return this.sessions.list(input);
3755
+ }
3756
+ /** @deprecated Use `sessions.delete()`. */
3757
+ deleteSession(input = {}) {
3758
+ return this.sessions.delete(input);
3759
+ }
3760
+ /** @deprecated Use `sessions.unload()`. */
3761
+ unloadSession(input) {
3762
+ return this.sessions.unload(input);
3763
+ }
3764
+ /** @deprecated Use `sessions.prompt()`. */
3765
+ prompt(input) {
3766
+ return this.sessions.prompt(input);
3767
+ }
3768
+ /** @deprecated Use `sessions.cancelPrompt()`. */
3769
+ cancelPrompt(input) {
3770
+ return this.sessions.cancelPrompt(input);
3771
+ }
3772
+ /** @deprecated Use `sessions.respondPermission()`. */
3773
+ respondPermission(input) {
3774
+ return this.sessions.respondPermission(input);
3775
+ }
3776
+ /** @deprecated Use `sessions.readHistory()`. */
3777
+ readHistory(input) {
3778
+ return this.sessions.readHistory(input);
3779
+ }
3780
+ /** @deprecated Use `sessions.getConfig()`. */
3781
+ getSessionConfig(input) {
3782
+ return this.sessions.getConfig(input);
3783
+ }
3784
+ /** @deprecated Use `sessions.setConfigOption()`. */
3785
+ setSessionConfigOption(input) {
3786
+ return this.sessions.setConfigOption(input);
3787
+ }
3788
+ /** @deprecated Use `sessions.getCapabilities()`. */
3789
+ getSessionCapabilities(input) {
3790
+ return this.sessions.getCapabilities(input);
3791
+ }
3792
+ /** @deprecated Use `sessions.getAgentInfo()`. */
3793
+ getSessionAgentInfo(input) {
3794
+ return this.sessions.getAgentInfo(input);
3795
+ }
2469
3796
  /**
2470
3797
  * Dynamically link a software package into the RUNNING VM. The package's
2471
3798
  * `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) and its `share/man`
@@ -2474,7 +3801,7 @@ export class AgentOs {
2474
3801
  * block registers the package for `openSession({ agent: name })`. Persists for the VM's
2475
3802
  * lifetime (and across a snapshot iff the volume persists).
2476
3803
  */
2477
- async linkSoftware(descriptor) {
3804
+ async _linkSoftware(descriptor) {
2478
3805
  // Forward to the sidecar, which owns the `/opt/agentos` projection and
2479
3806
  // appends the package to its live host-backed staging dir; the commands
2480
3807
  // appear under `/opt/agentos/bin` immediately. The sidecar rejects a
@@ -2494,7 +3821,7 @@ export class AgentOs {
2494
3821
  // picked up by the sidecar (it owns the projected `/opt/agentos` and answers
2495
3822
  // openSession/listAgents from it). Nothing to record client-side.
2496
3823
  }
2497
- async listSoftware() {
3824
+ async _listSoftware() {
2498
3825
  return this._sidecarClient.providedCommands(this._sidecarSession, this._sidecarVm);
2499
3826
  }
2500
3827
  /**
@@ -2503,7 +3830,7 @@ export class AgentOs {
2503
3830
  * projected `/opt/agentos` packages (the client parses no manifests). Every such
2504
3831
  * agent is a package materialized into the VM, so `installed` is always `true`.
2505
3832
  */
2506
- async listAgents() {
3833
+ async _listAgents() {
2507
3834
  const response = await this._sendAcpRequest({
2508
3835
  tag: "AcpListAgentsRequest",
2509
3836
  val: { reserved: false },
@@ -2516,6 +3843,18 @@ export class AgentOs {
2516
3843
  installed: agent.installed,
2517
3844
  }));
2518
3845
  }
3846
+ /** @deprecated Use `software.link()`. */
3847
+ linkSoftware(descriptor) {
3848
+ return this.software.link(descriptor);
3849
+ }
3850
+ /** @deprecated Use `software.list()`. */
3851
+ listSoftware() {
3852
+ return this.software.list();
3853
+ }
3854
+ /** @deprecated Use `agents.list()`. */
3855
+ listAgents() {
3856
+ return this.agents.list();
3857
+ }
2519
3858
  _recordAgentStderr(event) {
2520
3859
  if (!event.sessionId) {
2521
3860
  return;
@@ -2569,6 +3908,81 @@ export class AgentOs {
2569
3908
  }
2570
3909
  }
2571
3910
  _handleSidecarEvent(event) {
3911
+ if (event.payload.type === "execution_output") {
3912
+ const output = mapExecutionOutputEvent(event.payload.event);
3913
+ const pid = this._languageProcessIds.get(output.executionId);
3914
+ if (pid !== undefined) {
3915
+ const process = this._languageProcesses.get(pid);
3916
+ if (process) {
3917
+ const publicOutput = {
3918
+ pid,
3919
+ stream: output.channel === "stderr" ? "stderr" : "stdout",
3920
+ data: output.chunk,
3921
+ };
3922
+ for (const handler of process.outputHandlers) {
3923
+ try {
3924
+ handler(publicOutput);
3925
+ }
3926
+ catch (error) {
3927
+ console.error("AgentOS process output handler failed", error);
3928
+ }
3929
+ }
3930
+ }
3931
+ }
3932
+ for (const key of ["*", output.executionId]) {
3933
+ for (const handler of this._executionOutputHandlers.get(key) ?? []) {
3934
+ try {
3935
+ handler(output);
3936
+ }
3937
+ catch (error) {
3938
+ console.error("AgentOS execution output handler failed", error);
3939
+ }
3940
+ }
3941
+ }
3942
+ return;
3943
+ }
3944
+ if (event.payload.type === "execution_completed") {
3945
+ const completed = mapExecutionCompletedEvent(event.payload.event);
3946
+ const pid = this._languageProcessIds.get(completed.executionId);
3947
+ if (pid !== undefined) {
3948
+ const process = this._languageProcesses.get(pid);
3949
+ if (process && !process.exit) {
3950
+ const exit = {
3951
+ pid,
3952
+ outcome: completed.outcome === "timed_out"
3953
+ ? "timed_out"
3954
+ : completed.outcome === "cancelled"
3955
+ ? "signalled"
3956
+ : "exited",
3957
+ ...(completed.exitCode !== undefined
3958
+ ? { exitCode: completed.exitCode }
3959
+ : {}),
3960
+ ...(process.signal ? { signal: process.signal } : {}),
3961
+ };
3962
+ process.exit = exit;
3963
+ process.descriptor = { ...process.descriptor, state: "exited" };
3964
+ for (const handler of process.exitHandlers) {
3965
+ try {
3966
+ handler(exit);
3967
+ }
3968
+ catch (error) {
3969
+ console.error("AgentOS process exit handler failed", error);
3970
+ }
3971
+ }
3972
+ }
3973
+ }
3974
+ for (const key of ["*", completed.executionId]) {
3975
+ for (const handler of this._executionCompletedHandlers.get(key) ?? []) {
3976
+ try {
3977
+ handler(completed);
3978
+ }
3979
+ catch (error) {
3980
+ console.error("AgentOS execution completion handler failed", error);
3981
+ }
3982
+ }
3983
+ }
3984
+ return;
3985
+ }
2572
3986
  if (event.payload.type === "ext") {
2573
3987
  this._handleAcpExtEvent(event.payload.envelope);
2574
3988
  return;
@@ -2970,12 +4384,12 @@ export class AgentOs {
2970
4384
  this._acpTerminals.set(terminalId, terminal);
2971
4385
  return { terminalId };
2972
4386
  }
2973
- _handleAcpWriteTerminal(params) {
4387
+ async _handleAcpWriteTerminal(params) {
2974
4388
  const method = "terminal/write";
2975
4389
  const terminal = this._requireAcpTerminal(params, method);
2976
4390
  const data = this._requireAcpStringParam(params, "data", method);
2977
4391
  const encoding = this._optionalAcpStringParam(params, "encoding", method);
2978
- terminal.handle.write(encoding === "base64" ? Buffer.from(data, "base64") : data);
4392
+ await terminal.handle.write(encoding === "base64" ? Buffer.from(data, "base64") : data);
2979
4393
  return null;
2980
4394
  }
2981
4395
  _handleAcpReadTerminal(params) {
@@ -3065,17 +4479,29 @@ export class AgentOs {
3065
4479
  }
3066
4480
  // ── Cron ────────────────────────────────────────────────────
3067
4481
  /** Schedule a cron job. Returns a handle with the job ID and a cancel method. */
3068
- scheduleCron(options) {
4482
+ _scheduleCron(options) {
3069
4483
  return this._cronManager.schedule(options);
3070
4484
  }
3071
4485
  /** List all registered cron jobs. */
3072
- listCronJobs() {
4486
+ _listCronJobs() {
3073
4487
  return this._cronManager.list();
3074
4488
  }
3075
4489
  /** Cancel a cron job by ID. */
3076
- cancelCronJob(id) {
4490
+ _cancelCronJob(id) {
3077
4491
  this._cronManager.cancel(id);
3078
4492
  }
4493
+ /** @deprecated Use `cron.schedule()`. */
4494
+ scheduleCron(options) {
4495
+ return this.cron.schedule(options);
4496
+ }
4497
+ /** @deprecated Use `cron.list()`. */
4498
+ listCronJobs() {
4499
+ return this.cron.list();
4500
+ }
4501
+ /** @deprecated Use `cron.cancel()`. */
4502
+ cancelCronJob(id) {
4503
+ this.cron.cancel(id);
4504
+ }
3079
4505
  /** Subscribe to cron lifecycle events (fire, complete, error). */
3080
4506
  onCronEvent(handler) {
3081
4507
  this._cronManager.onEvent(handler);
@@ -3094,14 +4520,30 @@ export class AgentOs {
3094
4520
  }
3095
4521
  this._acpTerminals.clear();
3096
4522
  this._processes.clear();
4523
+ this._executionOutputHandlers.clear();
4524
+ this._executionCompletedHandlers.clear();
3097
4525
  await waitForTrackedExitPromises([...shellExitPromises, ...terminalExitPromises], SHELL_DISPOSE_TIMEOUT_MS);
3098
4526
  this._disposeSidecarEventListener();
3099
4527
  const sidecarLease = this._sidecarLease;
3100
4528
  this._sidecarLease = null;
3101
- if (sidecarLease) {
3102
- return sidecarLease.dispose();
4529
+ const disposeVm = sidecarLease
4530
+ ? sidecarLease.dispose()
4531
+ : this.#kernel.dispose();
4532
+ const hooks = this._disposeHooks.splice(0);
4533
+ const errors = [];
4534
+ try {
4535
+ await disposeVm;
4536
+ }
4537
+ catch (error) {
4538
+ errors.push(error);
4539
+ }
4540
+ const hookResults = await Promise.allSettled(hooks.map((hook) => hook()));
4541
+ errors.push(...hookResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
4542
+ if (errors.length === 1)
4543
+ throw errors[0];
4544
+ if (errors.length > 1) {
4545
+ throw new AggregateError(errors, "AgentOS VM disposal failed");
3103
4546
  }
3104
- return this.#kernel.dispose();
3105
4547
  }
3106
4548
  }
3107
4549
  const agentOsRuntimeAdmins = new WeakMap();