Skip to content

Commit 5198b62

Browse files
committed
feat: return text from session
1 parent 21ea076 commit 5198b62

8 files changed

Lines changed: 180 additions & 22 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ agentOS wraps the kernel and adds: a high-level filesystem/process API, ACP agen
3131
- **Registry types**: `@rivet-dev/agent-os-registry-types` in `packages/registry-types/` -- shared type definitions for WASM command package descriptors. The registry software packages link to this package. When changing descriptor types, update here and rebuild the registry.
3232
- **npm scope**: `@rivet-dev/agent-os-*`
3333
- **Actor integration** lives in the Rivet repo at `rivetkit-typescript/packages/rivetkit/src/agent-os/`, not as a separate package
34-
- **The actor layer must maintain 1:1 feature parity with AgentOs.** Every public method on the `AgentOs` class (`packages/core/src/agent-os.ts`) must have a corresponding actor action in the Rivet repo's `rivetkit-typescript/packages/rivetkit/src/agent-os/`. Subscription methods (onProcessStdout, onShellData, onCronEvent, etc.) are wired through actor events. Lifecycle methods (dispose) are handled by the actor's onSleep/onDestroy hooks. When adding a new public method to AgentOs, add the corresponding actor action in the same change.
34+
- **The actor layer must maintain 1:1 feature parity with AgentOs.** Every public method on the `AgentOs` class (`packages/core/src/agent-os.ts`) must have a corresponding actor action in the Rivet repo's `rivetkit-typescript/packages/rivetkit/src/agent-os/`. Subscription methods (onProcessStdout, onShellData, onCronEvent, etc.) are wired through actor events. Lifecycle methods (dispose) are handled by the actor's onSleep/onDestroy hooks. When adding a new public method to AgentOs, add the corresponding actor action in the same change. This includes changes to method signatures, option types, return types, and configuration interfaces -- any API surface change in AgentOs must be mirrored in the actor layer. **Always ask the user which Rivet repo/path to update** (e.g., `~/r-aos`, `~/r16`, etc.) before making changes there.
3535
- **The RivetKit driver test suite must have full feature coverage of all agent-os actor actions.** Tests live in the Rivet repo's `rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/`. When adding a new actor action, add a corresponding driver test in the same change.
3636
- **The core quickstart (`examples/quickstart/`) and the RivetKit example (in the Rivet repo at `examples/agent-os/`) must stay in sync.** Both cover the same set of features (hello-world, filesystem, processes, network, cron, tools, agent-session, sandbox) with identical behavior, just different APIs. Core uses `AgentOs.create()` directly; RivetKit uses `agentOs()` actor with client-server split. When adding or changing a quickstart example, update both.
3737

examples/quickstart/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"nodejs": "node --import tsx src/nodejs.ts",
1919
"python": "node --import tsx src/python.ts",
2020
"bash": "node --import tsx src/bash.ts",
21-
"s3-filesystem": "node --import tsx src/s3-filesystem.ts"
21+
"s3-filesystem": "node --import tsx src/s3-filesystem.ts",
22+
"pi-extensions": "node --import tsx src/pi-extensions.ts"
2223
},
2324
"dependencies": {
2425
"@rivet-dev/agent-os-core": "workspace:*",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Pi extensions: write a custom extension into the VM before creating a
2+
// session and verify Pi discovers and loads it.
3+
//
4+
// The adapter scans ~/.pi/agent/extensions/ and <cwd>/.pi/extensions/ for
5+
// .js files at session start. Each file exports a function that receives
6+
// Pi's ExtensionAPI, which can register tools, modify the system prompt,
7+
// subscribe to lifecycle events, and more.
8+
//
9+
// Extensions must use CommonJS syntax (module.exports = function(pi) { ... }).
10+
//
11+
// NOTE: Requires ANTHROPIC_API_KEY to be set.
12+
13+
import { AgentOs } from "@rivet-dev/agent-os-core";
14+
import common from "@rivet-dev/agent-os-common";
15+
import pi from "@rivet-dev/agent-os-pi";
16+
17+
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
18+
if (!ANTHROPIC_API_KEY) {
19+
console.error("Set ANTHROPIC_API_KEY to run this example.");
20+
process.exit(1);
21+
}
22+
23+
// ── Extension source code ──────────────────────────────────────────
24+
//
25+
// This extension hooks Pi's before_agent_start event to append a custom
26+
// instruction to the system prompt. No imports needed — the ExtensionAPI
27+
// is passed as a parameter.
28+
29+
const extensionSource = `
30+
module.exports = function(pi) {
31+
pi.on("before_agent_start", async (event) => {
32+
return {
33+
systemPrompt: event.systemPrompt +
34+
"\\n\\nCRITICAL INSTRUCTION: You MUST begin every response with " +
35+
"exactly the phrase 'EXTENSION_OK: ' followed by your answer. " +
36+
"This is mandatory and non-negotiable."
37+
};
38+
});
39+
};
40+
`;
41+
42+
// ── Create VM and write extension ──────────────────────────────────
43+
44+
const vm = await AgentOs.create({ software: [common, pi] });
45+
46+
// Write the extension into Pi's global extensions directory.
47+
// In the VM, HOME is /home/user, so ~/.pi/agent/extensions/ resolves there.
48+
const extensionsDir = "/home/user/.pi/agent/extensions";
49+
await vm.mkdir(extensionsDir, { recursive: true });
50+
await vm.writeFile(`${extensionsDir}/custom-greeting.js`, extensionSource);
51+
52+
console.log("Extension written. Creating Pi session...\n");
53+
54+
// ── Create session and prompt ──────────────────────────────────────
55+
56+
const { sessionId } = await vm.createSession("pi", {
57+
env: { ANTHROPIC_API_KEY },
58+
});
59+
console.log("Session created:", sessionId);
60+
61+
// Ask a simple question — if the extension loaded, the agent will
62+
// prefix its response with "EXTENSION_OK: "
63+
const { text } = await vm.prompt(sessionId, "What is 2 + 2? Reply with just the number.");
64+
console.log("Agent:", text);
65+
66+
// ── Verify ─────────────────────────────────────────────────────────
67+
68+
if (text.includes("EXTENSION_OK:")) {
69+
console.log("SUCCESS — Pi extension loaded and modified the system prompt.");
70+
} else {
71+
console.log("FAIL — Response did not include the expected prefix.");
72+
}
73+
74+
vm.closeSession(sessionId);
75+
await vm.dispose();

packages/core/src/agent-os.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,14 @@ export interface SessionInfo {
273273
agentType: string;
274274
}
275275

276+
/** Result from AgentOs.prompt(). */
277+
export interface PromptResult {
278+
/** Raw JSON-RPC response from the ACP adapter. */
279+
response: JsonRpcResponse;
280+
/** Accumulated agent text output from streamed message chunks. */
281+
text: string;
282+
}
283+
276284
/** Information about a process spawned via AgentOs.spawn(). */
277285
export interface SpawnedProcessInfo {
278286
pid: number;
@@ -978,7 +986,13 @@ export class AgentOs {
978986
}
979987
}
980988

981-
async mkdir(path: string): Promise<void> {
989+
async mkdir(
990+
path: string,
991+
options?: { recursive?: boolean },
992+
): Promise<void> {
993+
if (options?.recursive) {
994+
return this._mkdirp(path);
995+
}
982996
this._assertSafeAbsolutePath(path);
983997
return this.kernel.mkdir(path);
984998
}
@@ -1854,12 +1868,32 @@ export class AgentOs {
18541868

18551869
// ── Flat session API (ID-based) ───────────────────────────────
18561870

1857-
/** Send a prompt to the agent and wait for the final response. */
1871+
/** Send a prompt to the agent and wait for the final response.
1872+
* Returns the raw JSON-RPC response and the accumulated agent text. */
18581873
async prompt(
18591874
sessionId: string,
18601875
text: string,
1861-
): Promise<JsonRpcResponse> {
1862-
return this._requireSession(sessionId).prompt(text);
1876+
): Promise<PromptResult> {
1877+
const session = this._requireSession(sessionId);
1878+
1879+
// Collect streamed text while the prompt is running
1880+
let agentText = "";
1881+
const handler: SessionEventHandler = (event) => {
1882+
const params = event.params as Record<string, unknown> | undefined;
1883+
const update = params?.update as Record<string, unknown> | undefined;
1884+
if (update?.sessionUpdate === "agent_message_chunk") {
1885+
const content = update.content as { text?: string } | undefined;
1886+
if (content?.text) agentText += content.text;
1887+
}
1888+
};
1889+
session.onSessionEvent(handler);
1890+
1891+
try {
1892+
const response = await session.prompt(text);
1893+
return { response, text: agentText };
1894+
} finally {
1895+
session.removeSessionEventHandler(handler);
1896+
}
18631897
}
18641898

18651899
/** Cancel ongoing agent work for a session. */

packages/playground/vendor/monaco

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
/home/nathan/a2/packages/playground/node_modules/monaco-editor/min
1+
/home/nathan/a1/packages/playground/node_modules/monaco-editor/min

packages/playground/vendor/pyodide

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
/home/nathan/a2/packages/playground/node_modules/pyodide
1+
/home/nathan/a1/packages/playground/node_modules/pyodide
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
/home/nathan/a2/packages/playground/node_modules/typescript/lib/typescript.js
1+
/home/nathan/a1/packages/playground/node_modules/typescript/lib/typescript.js

registry/agent/pi/src/adapter.ts

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ import {
3737
createAgentSession,
3838
} from "@mariozechner/pi-coding-agent";
3939
import type { AgentSession } from "@mariozechner/pi-coding-agent";
40-
import { isAbsolute, resolve as resolvePath } from "node:path";
41-
import { readFileSync } from "node:fs";
40+
import { isAbsolute, join, resolve as resolvePath } from "node:path";
41+
import { existsSync, readFileSync, readdirSync } from "node:fs";
42+
import { homedir } from "node:os";
43+
import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
4244

4345
// ── CLI argument parsing ────────────────────────────────────────────
4446

@@ -51,6 +53,46 @@ for (let i = 0; i < argv.length; i++) {
5153
}
5254
}
5355

56+
// ── Extension discovery ────────────────────────────────────────────
57+
// Manually discover and load Pi extensions from standard directories.
58+
// Pi's built-in jiti loader requires performance.now() which the VM's
59+
// V8 runtime doesn't provide, so we load extensions ourselves via
60+
// require() and pass them as extensionFactories.
61+
62+
function discoverExtensionFactories(cwd: string): ExtensionFactory[] {
63+
const factories: ExtensionFactory[] = [];
64+
const dirs = [
65+
join(cwd, ".pi", "extensions"),
66+
join(homedir(), ".pi", "agent", "extensions"),
67+
];
68+
69+
for (const dir of dirs) {
70+
if (!existsSync(dir)) continue;
71+
let entries: string[];
72+
try {
73+
entries = readdirSync(dir);
74+
} catch {
75+
continue;
76+
}
77+
for (const name of entries) {
78+
if (!name.endsWith(".js") && !name.endsWith(".ts")) continue;
79+
const filePath = join(dir, name);
80+
try {
81+
// biome-ignore lint/security/noGlobalEval: needed to load extensions without jiti
82+
const mod = eval(`require(${JSON.stringify(filePath)})`);
83+
const factory = mod?.default ?? mod;
84+
if (typeof factory === "function") {
85+
factories.push(factory);
86+
}
87+
} catch {
88+
// Skip extensions that fail to load
89+
}
90+
}
91+
}
92+
93+
return factories;
94+
}
95+
5496
// ── Agent implementation ────────────────────────────────────────────
5597

5698
class PiSdkAgent implements Agent {
@@ -95,19 +137,25 @@ class PiSdkAgent implements Agent {
95137
): Promise<NewSessionResponse> {
96138
this.cwd = params.cwd;
97139

98-
const { session } = await createAgentSession({
140+
// Discover extensions from standard Pi directories and load them
141+
// manually (bypasses jiti which requires performance.now).
142+
const extensionFactories = discoverExtensionFactories(params.cwd);
143+
144+
const { DefaultResourceLoader } = await import(
145+
"@mariozechner/pi-coding-agent"
146+
);
147+
const resourceLoader = new DefaultResourceLoader({
148+
cwd: params.cwd,
149+
...(appendSystemPrompt ? { appendSystemPrompt } : {}),
150+
noExtensions: true, // skip jiti-based discovery
151+
extensionFactories,
152+
});
153+
await resourceLoader.reload();
154+
155+
const { session, extensionsResult } = await createAgentSession({
99156
cwd: params.cwd,
100157
sessionManager: SessionManager.inMemory(),
101-
...(appendSystemPrompt
102-
? {
103-
resourceLoader: new (
104-
await import("@mariozechner/pi-coding-agent")
105-
).DefaultResourceLoader({
106-
cwd: params.cwd,
107-
appendSystemPrompt,
108-
}),
109-
}
110-
: {}),
158+
resourceLoader,
111159
});
112160

113161
this.session = session;

0 commit comments

Comments
 (0)