From e1ca501da8395cbbbcce0854a26a1d3a48f0e546 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 6 May 2026 13:21:08 +0200 Subject: [PATCH] refactor(agent): expose concrete harness --- packages/agent/src/harness/agent-harness.ts | 23 ++++++--- packages/agent/src/harness/factory.ts | 6 +-- packages/agent/src/harness/types.ts | 56 ++------------------- packages/agent/test/harness/factory.test.ts | 3 +- packages/agent/test/scratch/simple.ts | 13 +++++ 5 files changed, 38 insertions(+), 63 deletions(-) create mode 100644 packages/agent/test/scratch/simple.ts diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 67db5ea6..8d36d8c0 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; -import type { ImageContent, Model } from "@mariozechner/pi-ai"; +import type { AssistantMessage, ImageContent, Model } from "@mariozechner/pi-ai"; import { Agent } from "../agent.js"; import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../types.js"; import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction/branch-summarization.js"; @@ -8,7 +8,6 @@ import { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from "./compa import { expandPromptTemplate } from "./prompt-templates.js"; import type { AbortResult, - AgentHarness, AgentHarnessContext, AgentHarnessConversationState, AgentHarnessEvent, @@ -50,7 +49,7 @@ function createUserMessage(text: string, images?: ImageContent[]): AgentMessage return { role: "user", content, timestamp: Date.now() }; } -export class DefaultAgentHarness implements AgentHarness { +export class AgentHarness { readonly agent: Agent; readonly env: ExecutionEnv; readonly conversation: AgentHarnessConversationState; @@ -284,8 +283,9 @@ export class DefaultAgentHarness implements AgentHarness { } } - async prompt(text: string, options?: { images?: ImageContent[] }): Promise { + async prompt(text: string, options?: { images?: ImageContent[] }): Promise { if (!this.operation.idle) throw new Error("AgentHarness is busy"); + const beforeLength = this.agent.state.messages.length; this.operation.idle = false; this.operation.liveOperationId = randomUUID(); const expanded = this.expandSkillCommand(expandPromptTemplate(text, this.promptTemplates)); @@ -310,15 +310,26 @@ export class DefaultAgentHarness implements AgentHarness { if (beforeResult?.messages) messages = [...beforeResult.messages, ...messages]; if (beforeResult?.systemPrompt) this.agent.state.systemPrompt = beforeResult.systemPrompt; await this.agent.prompt(messages); + let response: AssistantMessage | undefined; + const newMessages = this.agent.state.messages.slice(beforeLength); + for (let i = newMessages.length - 1; i >= 0; i--) { + const message = newMessages[i]!; + if (message.role === "assistant") { + response = message; + break; + } + } + if (!response) throw new Error("AgentHarness prompt completed without an assistant message"); + return response; } - async skill(name: string, args?: string): Promise { + async skill(name: string, args?: string): Promise { const skill = this.skills.find((candidate) => candidate.name === name); if (!skill) throw new Error(`Unknown skill: ${name}`); let content = readFileSync(skill.filePath, "utf8"); content = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "").trim(); const prompt = args ? `${content}\n\n${args}` : content; - await this.prompt(prompt); + return await this.prompt(prompt); } steer(message: AgentMessage): void { diff --git a/packages/agent/src/harness/factory.ts b/packages/agent/src/harness/factory.ts index 2c10ccc5..aeaa3a49 100644 --- a/packages/agent/src/harness/factory.ts +++ b/packages/agent/src/harness/factory.ts @@ -1,6 +1,6 @@ -import { DefaultAgentHarness } from "./agent-harness.js"; +import { AgentHarness } from "./agent-harness.js"; import { Session } from "./session/session.js"; -import type { AgentHarness, AgentHarnessOptions, SessionMetadata, SessionStorage } from "./types.js"; +import type { AgentHarnessOptions, SessionMetadata, SessionStorage } from "./types.js"; export function createSession( storage: SessionStorage, @@ -9,5 +9,5 @@ export function createSession( } export function createAgentHarness(options: AgentHarnessOptions): AgentHarness { - return new DefaultAgentHarness(options); + return new AgentHarness(options); } diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 625a9ccb..12224706 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -1,5 +1,5 @@ import type { ImageContent, Model, TextContent } from "@mariozechner/pi-ai"; -import type { Agent, AgentEvent, AgentMessage, ThinkingLevel } from "../index.js"; +import type { AgentEvent, AgentMessage, ThinkingLevel } from "../index.js"; import type { Session } from "./session/session.js"; export type SourceScope = "user" | "project" | "temporary"; @@ -524,60 +524,10 @@ export interface AgentHarnessOptions { promptTemplates?: PromptTemplate[]; skills?: Skill[]; requestAuth?: (model: Model) => Promise<{ apiKey: string; headers?: Record } | undefined>; - initialModel?: Model; + initialModel: Model; initialThinkingLevel?: ThinkingLevel; initialActiveToolNames?: string[]; initialSystemPromptInputs?: SystemPromptInputs; } -export interface AgentHarness { - readonly agent: Agent; - readonly env: ExecutionEnv; - readonly conversation: AgentHarnessConversationState; - readonly operation: AgentHarnessOperationState; - - prompt(text: string, options?: AgentHarnessPromptOptions): Promise; - skill(name: string, args?: string): Promise; - - steer(message: AgentMessage): void; - followUp(message: AgentMessage): void; - nextTurn(message: AgentMessage): void; - - appendMessage(message: AgentMessage): Promise; - - shell( - command: string, - options?: { - cwd?: string; - env?: Record; - timeout?: number; - signal?: AbortSignal; - onStdout?: (chunk: string) => void; - onStderr?: (chunk: string) => void; - }, - ): Promise<{ stdout: string; stderr: string; exitCode: number }>; - - compact(customInstructions?: string): Promise; - navigateTree( - targetId: string, - options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string }, - ): Promise; - - setModel(model: Model): Promise; - setThinkingLevel(level: ThinkingLevel): Promise; - setActiveTools(toolNames: string[]): Promise; - setSystemPromptInputs(inputs: SystemPromptInputs): Promise; - - abort(): Promise; - waitForIdle(): Promise; - - subscribe(listener: (event: AgentHarnessEvent, signal?: AbortSignal) => Promise | void): () => void; - - on( - type: TType, - handler: ( - event: Extract, - ctx: AgentHarnessContext, - ) => Promise | AgentHarnessEventResultMap[TType], - ): () => void; -} +export type { AgentHarness } from "./agent-harness.js"; diff --git a/packages/agent/test/harness/factory.test.ts b/packages/agent/test/harness/factory.test.ts index e303477b..0033d1a3 100644 --- a/packages/agent/test/harness/factory.test.ts +++ b/packages/agent/test/harness/factory.test.ts @@ -1,3 +1,4 @@ +import { getModel } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; import { NodeExecutionEnv } from "../../src/harness/execution-env.js"; import { createAgentHarness, createSession } from "../../src/harness/factory.js"; @@ -16,7 +17,7 @@ describe("harness factories", () => { it("creates agent harnesses", () => { const session = createSession(new InMemorySessionStorage()); const env = new NodeExecutionEnv({ cwd: process.cwd() }); - const harness = createAgentHarness({ env, session }); + const harness = createAgentHarness({ env, session, initialModel: getModel("anthropic", "claude-sonnet-4-5") }); expect(harness.env).toBe(env); expect(harness.conversation.session).toBe(session); }); diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts new file mode 100644 index 00000000..9c3f7dcf --- /dev/null +++ b/packages/agent/test/scratch/simple.ts @@ -0,0 +1,13 @@ +import { getModel } from "@mariozechner/pi-ai"; +import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; +import { createAgentHarness, NodeExecutionEnv, Session } from "../../src/index.js"; + +const session = new Session(new InMemorySessionStorage()); +const agent = createAgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session, + initialModel: getModel("openai-codex", "gpt-5.5"), +}); + +const response = await agent.prompt("What is 2 + 2?"); +console.log(response);