Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
9.3 kB · 284 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";import { tmpdir } from "node:os";import { dirname, join } from "node:path";import { fileURLToPath } from "node:url";import type { AgentEvent } from "@earendil-works/pi-agent-core";import { afterEach, beforeEach, describe, expect, test } from "vitest";import { RpcClient } from "../src/modes/rpc/rpc-client.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_TOKEN)("RPC mode", () => { let client: RpcClient; let sessionDir: string;
beforeEach(() => { sessionDir = join(tmpdir(), `pi-rpc-test-${Date.now()}`); client = new RpcClient({ cliPath: join(__dirname, "..", "dist", "cli.js"), cwd: join(__dirname, ".."), env: { PI_CODING_AGENT_DIR: sessionDir }, provider: "anthropic", model: "claude-sonnet-4-5", }); });
afterEach(async () => { await client.stop(); if (sessionDir && existsSync(sessionDir)) { rmSync(sessionDir, { recursive: true }); } });
test("should get state", async () => { await client.start(); const state = await client.getState();
expect(state.model).toBeDefined(); expect(state.model?.provider).toBe("anthropic"); expect(state.model?.id).toBe("claude-sonnet-4-5"); expect(state.isStreaming).toBe(false); expect(state.messageCount).toBe(0); }, 30000);
test("should save messages to session file", async () => { await client.start();
const events = await client.promptAndWait("Reply with just the word 'hello'");
const messageEndEvents = events.filter((e) => e.type === "message_end"); expect(messageEndEvents.length).toBeGreaterThanOrEqual(2);
// Wait for file writes await new Promise((resolve) => setTimeout(resolve, 200));
const sessionsPath = join(sessionDir, "sessions"); expect(existsSync(sessionsPath)).toBe(true);
const sessionDirs = readdirSync(sessionsPath); expect(sessionDirs.length).toBeGreaterThan(0);
const cwdSessionDir = join(sessionsPath, sessionDirs[0]); const sessionFiles = readdirSync(cwdSessionDir).filter((f) => f.endsWith(".jsonl")); expect(sessionFiles.length).toBe(1);
const sessionContent = readFileSync(join(cwdSessionDir, sessionFiles[0]), "utf8"); const entries = sessionContent .trim() .split("\n") .map((line) => JSON.parse(line));
expect(entries[0].type).toBe("session");
const messages = entries.filter((e: { type: string }) => e.type === "message"); expect(messages.length).toBeGreaterThanOrEqual(2);
const roles = messages.map((m: { message: { role: string } }) => m.message.role); expect(roles).toContain("user"); expect(roles).toContain("assistant"); }, 90000);
test("should handle manual compaction", async () => { await client.start();
await client.promptAndWait("Say hello");
const result = await client.compact(); expect(result.summary).toBeDefined(); expect(result.tokensBefore).toBeGreaterThan(0);
// Wait for file writes await new Promise((resolve) => setTimeout(resolve, 200));
const sessionsPath = join(sessionDir, "sessions"); const sessionDirs = readdirSync(sessionsPath); const cwdSessionDir = join(sessionsPath, sessionDirs[0]); const sessionFiles = readdirSync(cwdSessionDir).filter((f) => f.endsWith(".jsonl")); const sessionContent = readFileSync(join(cwdSessionDir, sessionFiles[0]), "utf8"); const entries = sessionContent .trim() .split("\n") .map((line) => JSON.parse(line));
const compactionEntries = entries.filter((e: { type: string }) => e.type === "compaction"); expect(compactionEntries.length).toBe(1); expect(compactionEntries[0].summary).toBeDefined(); }, 120000);
test("should execute bash command", async () => { await client.start();
const result = await client.bash("echo hello"); expect(result.output.trim()).toBe("hello"); expect(result.exitCode).toBe(0); expect(result.cancelled).toBe(false); }, 30000);
test("should add bash output to context", async () => { await client.start();
await client.promptAndWait("Say hi");
const uniqueValue = `test-${Date.now()}`; await client.bash(`echo ${uniqueValue}`);
// Wait for file writes await new Promise((resolve) => setTimeout(resolve, 200));
const sessionsPath = join(sessionDir, "sessions"); const sessionDirs = readdirSync(sessionsPath); const cwdSessionDir = join(sessionsPath, sessionDirs[0]); const sessionFiles = readdirSync(cwdSessionDir).filter((f) => f.endsWith(".jsonl")); const sessionContent = readFileSync(join(cwdSessionDir, sessionFiles[0]), "utf8"); const entries = sessionContent .trim() .split("\n") .map((line) => JSON.parse(line));
const bashMessages = entries.filter( (e: { type: string; message?: { role: string } }) => e.type === "message" && e.message?.role === "bashExecution", ); expect(bashMessages.length).toBe(1); expect(bashMessages[0].message.output).toContain(uniqueValue); }, 90000);
test("should include bash output in LLM context", async () => { await client.start();
const uniqueValue = `unique-${Date.now()}`; await client.bash(`echo ${uniqueValue}`);
const events = await client.promptAndWait( "What was the exact output of the echo command I just ran? Reply with just the value, nothing else.", );
const messageEndEvents = events.filter((e) => e.type === "message_end") as AgentEvent[]; const assistantMessage = messageEndEvents.find( (e) => e.type === "message_end" && e.message?.role === "assistant", ) as any;
expect(assistantMessage).toBeDefined();
const textContent = assistantMessage.message.content.find((c: any) => c.type === "text"); expect(textContent?.text).toContain(uniqueValue); }, 90000);
test("should set and get thinking level", async () => { await client.start();
await client.setThinkingLevel("high");
const state = await client.getState(); expect(state.thinkingLevel).toBe("high"); }, 30000);
test("should cycle thinking level", async () => { await client.start();
const initialState = await client.getState(); const initialLevel = initialState.thinkingLevel;
const result = await client.cycleThinkingLevel(); expect(result).toBeDefined(); expect(result!.level).not.toBe(initialLevel);
const newState = await client.getState(); expect(newState.thinkingLevel).toBe(result!.level); }, 30000);
test("should get available models", async () => { await client.start();
const models = await client.getAvailableModels(); expect(models.length).toBeGreaterThan(0);
for (const model of models) { expect(model.provider).toBeDefined(); expect(model.id).toBeDefined(); expect(model.contextWindow).toBeGreaterThan(0); expect(typeof model.reasoning).toBe("boolean"); } }, 30000);
test("should get session stats", async () => { await client.start();
await client.promptAndWait("Hello");
const stats = await client.getSessionStats(); expect(stats.sessionFile).toBeDefined(); expect(stats.sessionId).toBeDefined(); expect(stats.userMessages).toBeGreaterThanOrEqual(1); expect(stats.assistantMessages).toBeGreaterThanOrEqual(1); }, 90000);
test("should create new session", async () => { await client.start();
await client.promptAndWait("Hello");
let state = await client.getState(); expect(state.messageCount).toBeGreaterThan(0);
await client.newSession();
state = await client.getState(); expect(state.messageCount).toBe(0); }, 90000);
test("should export to HTML", async () => { await client.start();
await client.promptAndWait("Hello");
const result = await client.exportHtml(); expect(result.path).toBeDefined(); expect(result.path.endsWith(".html")).toBe(true); expect(existsSync(result.path)).toBe(true); }, 90000);
test("should get last assistant text", async () => { await client.start();
let text = await client.getLastAssistantText(); expect(text).toBeUndefined();
await client.promptAndWait("Reply with just: test123");
text = await client.getLastAssistantText(); expect(text).toContain("test123"); }, 90000);
test("should set and get session name", async () => { await client.start();
let state = await client.getState(); expect(state.sessionName).toBeUndefined();
// Send a prompt first - session files are only written after first assistant message await client.promptAndWait("Reply with just 'ok'");
await client.setSessionName("my-test-session");
state = await client.getState(); expect(state.sessionName).toBe("my-test-session");
// Wait for file writes await new Promise((resolve) => setTimeout(resolve, 200));
const sessionsPath = join(sessionDir, "sessions"); const sessionDirs = readdirSync(sessionsPath); const cwdSessionDir = join(sessionsPath, sessionDirs[0]); const sessionFiles = readdirSync(cwdSessionDir).filter((f) => f.endsWith(".jsonl")); const sessionContent = readFileSync(join(cwdSessionDir, sessionFiles[0]), "utf8"); const entries = sessionContent .trim() .split("\n") .map((line) => JSON.parse(line));
const sessionInfoEntries = entries.filter((e: { type: string }) => e.type === "session_info"); expect(sessionInfoEntries.length).toBe(1); expect(sessionInfoEntries[0].name).toBe("my-test-session"); }, 60000);});