Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/execution/src/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool, PythonExecutionError> {
self.inner
.handle_kernel_stdin_sync_rpc(request)
.map_err(map_javascript_error)
}

pub fn poll_event_blocking(
&mut self,
timeout: Duration,
Expand Down
13 changes: 11 additions & 2 deletions crates/execution/tests/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,14 +549,20 @@ 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:?}"
);
}
other => panic!(
"streaming-stdin execution should stay alive until stdin closes, got {other:?}"
),
}
if Instant::now() >= idle_deadline {
break;
}
}

execution
Expand All @@ -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:?}"
);
}
Expand Down
37 changes: 12 additions & 25 deletions packages/core/tests/pi-vanilla-bash.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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 {
createAnthropicFixture,
startLlmock,
stopLlmock,
} from "./helpers/llmock-helper.js";
import { moduleAccessMounts } from "./helpers/node-modules-mount.js";

const MODULE_ACCESS_CWD = resolve(import.meta.dirname, "..");

Expand Down Expand Up @@ -55,7 +56,9 @@ async function createPiVm(mockUrl: string): Promise<AgentOs> {
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],
});
}

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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(
{
Expand Down Expand Up @@ -368,7 +355,7 @@ describe("vanilla Pi bash tool inside the VM", () => {
const activeSessionId = sessionId;
const sawInProgress = new Promise<void>((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")
Expand Down
17 changes: 10 additions & 7 deletions packages/core/tests/session-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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", {
Expand All @@ -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);
Expand Down
Loading