diff --git a/crates/execution/src/python.rs b/crates/execution/src/python.rs index 58822c9a94..b976b0feb5 100644 --- a/crates/execution/src/python.rs +++ b/crates/execution/src/python.rs @@ -684,6 +684,18 @@ impl PythonExecution { .map_err(map_javascript_error) } + /// Service the local stdin bridge for consumers that drive a standalone + /// Python execution without the sidecar's sync-RPC dispatcher. + #[doc(hidden)] + pub fn try_service_standalone_stdin_sync_rpc( + &mut self, + request: &JavascriptSyncRpcRequest, + ) -> Result { + self.inner + .handle_kernel_stdin_sync_rpc(request) + .map_err(map_javascript_error) + } + pub fn poll_event_blocking( &mut self, timeout: Duration, diff --git a/crates/execution/tests/python.rs b/crates/execution/tests/python.rs index 3636a976fd..5c19eba79d 100644 --- a/crates/execution/tests/python.rs +++ b/crates/execution/tests/python.rs @@ -549,7 +549,10 @@ export async function loadPyodide(options) { assert!( execution .try_service_standalone_module_sync_rpc(&request) - .expect("service module sync RPC"), + .expect("service module sync RPC") + || execution + .try_service_standalone_stdin_sync_rpc(&request) + .expect("service stdin sync RPC"), "unexpected JS sync RPC before stdin write: {request:?}" ); } @@ -557,6 +560,9 @@ export async function loadPyodide(options) { "streaming-stdin execution should stay alive until stdin closes, got {other:?}" ), } + if Instant::now() >= idle_deadline { + break; + } } execution @@ -580,7 +586,10 @@ export async function loadPyodide(options) { assert!( execution .try_service_standalone_module_sync_rpc(&request) - .expect("service module sync RPC"), + .expect("service module sync RPC") + || execution + .try_service_standalone_stdin_sync_rpc(&request) + .expect("service stdin sync RPC"), "unexpected JS sync RPC request during stdin test: {request:?}" ); } diff --git a/packages/core/tests/pi-vanilla-bash.test.ts b/packages/core/tests/pi-vanilla-bash.test.ts index a3f51edd7a..14453919d6 100644 --- a/packages/core/tests/pi-vanilla-bash.test.ts +++ b/packages/core/tests/pi-vanilla-bash.test.ts @@ -1,7 +1,7 @@ import { resolve } from "node:path"; -import type { Fixture, ToolCall } from "@copilotkit/llmock"; -import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; import common from "@agentos-software/common"; +import pi from "@agentos-software/pi"; +import type { Fixture, ToolCall } from "@copilotkit/llmock"; import { describe, expect, test } from "vitest"; import { AgentOs } from "../src/agent-os.js"; import { @@ -9,6 +9,7 @@ import { startLlmock, stopLlmock, } from "./helpers/llmock-helper.js"; +import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); @@ -55,7 +56,9 @@ async function createPiVm(mockUrl: string): Promise { return AgentOs.create({ loopbackExemptPorts: [Number(new URL(mockUrl).port)], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - software: [common], + // Default software ships no agents; project Pi explicitly together with + // the shell commands used by its unmodified bash backend. + software: [...common, pi], }); } @@ -109,12 +112,8 @@ function captureSessionEventText( * `detached: true` and streaming stdout/stderr), with no custom `operations` * override in the adapter. Everything stays inside the VM. * - * The file-write, timeout, and abort cases depend on runtime behavior that is - * still outstanding below the adapter layer (shell `>` redirect visibility - * through `vm.readFile`, and a blocking guest `sleep`). They are tracked in - * `~/.agents/todo/agentos-runtime-fixes.md` and registered as skipped - * placeholders here so the file documents the full vanilla contract without - * asserting behavior the runtime cannot yet deliver. + * The coverage includes shell output, filesystem side effects, timeout-driven + * process-tree termination, and cancellation of an in-flight command. */ describe("vanilla Pi bash tool inside the VM", () => { test("runs the vanilla bash backend in the session working directory", async () => { @@ -241,11 +240,7 @@ describe("vanilla Pi bash tool inside the VM", () => { } }, 120_000); - // Blocked on shell `>` redirect output being visible to `vm.readFile()`. - // The redirect runs inside the guest shell but the written bytes do not - // reconcile to the host read path yet. Tracked in - // ~/.agents/todo/agentos-runtime-fixes.md (shell-exec redirect visibility). - test.skip("writes a file through the default bash backend", async () => { + test("writes a file through the default bash backend", async () => { const fixtures = createBashFixtures( bashToolCall({ command: "printf 'ok' > out.txt", timeout: 10 }), "out.txt was written.", @@ -285,11 +280,7 @@ describe("vanilla Pi bash tool inside the VM", () => { } }, 120_000); - // Blocked on a blocking guest `sleep`. The WASM `sleep` command currently - // fails to spawn ("operation not supported on this platform") because the - // host `sleep_ms` WASI import is unimplemented, so the timeout/kill path - // cannot be exercised. Tracked in ~/.agents/todo/agentos-runtime-fixes.md. - test.skip("enforces the bash timeout by killing the process tree", async () => { + test("enforces the bash timeout by killing the process tree", async () => { const fixtures = createBashFixtures( bashToolCall({ command: "sleep 30", timeout: 1 }), "the command timed out.", @@ -333,11 +324,7 @@ describe("vanilla Pi bash tool inside the VM", () => { } }, 60_000); - // Blocked on the same blocking-guest-`sleep` gap as the timeout case: the - // in-flight bash command exits immediately instead of staying running, so - // the cancel-while-in-progress path cannot be observed. Tracked in - // ~/.agents/todo/agentos-runtime-fixes.md. - test.skip("aborts an in-flight bash command on session cancel", async () => { + test("aborts an in-flight bash command on session cancel", async () => { const fixtures: Fixture[] = [ createAnthropicFixture( { @@ -368,7 +355,7 @@ describe("vanilla Pi bash tool inside the VM", () => { const activeSessionId = sessionId; const sawInProgress = new Promise((resolveInProgress) => { const unsubscribe = vm.onSessionEvent(activeSessionId, (event) => { - const serialized = JSON.stringify(event.notification.params); + const serialized = JSON.stringify(event.params); if ( serialized.includes('"in_progress"') && serialized.includes("bash") diff --git a/packages/core/tests/session-cleanup.test.ts b/packages/core/tests/session-cleanup.test.ts index 21f0bc58a5..cd2b135a55 100644 --- a/packages/core/tests/session-cleanup.test.ts +++ b/packages/core/tests/session-cleanup.test.ts @@ -1,12 +1,14 @@ import { execFileSync } from "node:child_process"; +import { readdir, readlink } from "node:fs/promises"; import { createServer, type IncomingMessage, type ServerResponse, } from "node:http"; -import { readlink, readdir } from "node:fs/promises"; -import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; import { resolve } from "node:path"; +import claude from "@agentos-software/claude-code"; +import opencode from "@agentos-software/opencode"; +import pi from "@agentos-software/pi"; import piCli from "@agentos-software/pi-cli"; import { describe, expect, test } from "vitest"; import { AgentOs } from "../src/agent-os.js"; @@ -15,17 +17,18 @@ import { encodeAcpRequest, encodeAcpResponse, } from "../src/sidecar/agentos-protocol.js"; +import type { SidecarSessionState } from "../src/sidecar/rpc-client.js"; import { NativeSidecarKernelProxy } from "../src/sidecar/rpc-client.js"; import { getAgentOsKernel } from "../src/test/runtime.js"; -import type { SidecarSessionState } from "../src/sidecar/rpc-client.js"; import { createAnthropicFixture, startLlmock, stopLlmock, } from "./helpers/llmock-helper.js"; +import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; import { - createVmOpenCodeHome, createVmWorkspace as createOpenCodeWorkspace, + createVmOpenCodeHome, } from "./helpers/opencode-helper.js"; import { REGISTRY_SOFTWARE } from "./helpers/registry-commands.js"; @@ -60,7 +63,7 @@ const PI_AGENTS: SessionCleanupAgent[] = [ AgentOs.create({ loopbackExemptPorts: [Number(new URL(mockUrl).port)], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - software: [], // pi pre-packed (default) + software: [pi], }), createSession: async (vm, mockUrl) => { const homeDir = await createVmPiHome(vm, mockUrl); @@ -115,7 +118,7 @@ const REGISTRY_AGENTS: SessionCleanupAgent[] = [ AgentOs.create({ loopbackExemptPorts: [Number(new URL(mockUrl).port)], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - software: [...REGISTRY_SOFTWARE], // claude pre-packed (default) + software: [claude, ...REGISTRY_SOFTWARE], }), createSession: async (vm, mockUrl) => vm.createSession("claude", { @@ -136,7 +139,7 @@ const REGISTRY_AGENTS: SessionCleanupAgent[] = [ AgentOs.create({ loopbackExemptPorts: [Number(new URL(mockUrl).port)], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - software: [...REGISTRY_SOFTWARE], // opencode pre-packed (default) + software: [opencode, ...REGISTRY_SOFTWARE], }), createSession: async (vm, mockUrl) => { const homeDir = await createVmOpenCodeHome(vm, mockUrl);