refactor(agent): expose concrete harness
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { readFileSync } from "node:fs";
|
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 { Agent } from "../agent.js";
|
||||||
import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../types.js";
|
import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../types.js";
|
||||||
import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction/branch-summarization.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 { expandPromptTemplate } from "./prompt-templates.js";
|
||||||
import type {
|
import type {
|
||||||
AbortResult,
|
AbortResult,
|
||||||
AgentHarness,
|
|
||||||
AgentHarnessContext,
|
AgentHarnessContext,
|
||||||
AgentHarnessConversationState,
|
AgentHarnessConversationState,
|
||||||
AgentHarnessEvent,
|
AgentHarnessEvent,
|
||||||
@@ -50,7 +49,7 @@ function createUserMessage(text: string, images?: ImageContent[]): AgentMessage
|
|||||||
return { role: "user", content, timestamp: Date.now() };
|
return { role: "user", content, timestamp: Date.now() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultAgentHarness implements AgentHarness {
|
export class AgentHarness {
|
||||||
readonly agent: Agent;
|
readonly agent: Agent;
|
||||||
readonly env: ExecutionEnv;
|
readonly env: ExecutionEnv;
|
||||||
readonly conversation: AgentHarnessConversationState;
|
readonly conversation: AgentHarnessConversationState;
|
||||||
@@ -284,8 +283,9 @@ export class DefaultAgentHarness implements AgentHarness {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async prompt(text: string, options?: { images?: ImageContent[] }): Promise<void> {
|
async prompt(text: string, options?: { images?: ImageContent[] }): Promise<AssistantMessage> {
|
||||||
if (!this.operation.idle) throw new Error("AgentHarness is busy");
|
if (!this.operation.idle) throw new Error("AgentHarness is busy");
|
||||||
|
const beforeLength = this.agent.state.messages.length;
|
||||||
this.operation.idle = false;
|
this.operation.idle = false;
|
||||||
this.operation.liveOperationId = randomUUID();
|
this.operation.liveOperationId = randomUUID();
|
||||||
const expanded = this.expandSkillCommand(expandPromptTemplate(text, this.promptTemplates));
|
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?.messages) messages = [...beforeResult.messages, ...messages];
|
||||||
if (beforeResult?.systemPrompt) this.agent.state.systemPrompt = beforeResult.systemPrompt;
|
if (beforeResult?.systemPrompt) this.agent.state.systemPrompt = beforeResult.systemPrompt;
|
||||||
await this.agent.prompt(messages);
|
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<void> {
|
async skill(name: string, args?: string): Promise<AssistantMessage> {
|
||||||
const skill = this.skills.find((candidate) => candidate.name === name);
|
const skill = this.skills.find((candidate) => candidate.name === name);
|
||||||
if (!skill) throw new Error(`Unknown skill: ${name}`);
|
if (!skill) throw new Error(`Unknown skill: ${name}`);
|
||||||
let content = readFileSync(skill.filePath, "utf8");
|
let content = readFileSync(skill.filePath, "utf8");
|
||||||
content = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "").trim();
|
content = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "").trim();
|
||||||
const prompt = args ? `${content}\n\n${args}` : content;
|
const prompt = args ? `${content}\n\n${args}` : content;
|
||||||
await this.prompt(prompt);
|
return await this.prompt(prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
steer(message: AgentMessage): void {
|
steer(message: AgentMessage): void {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DefaultAgentHarness } from "./agent-harness.js";
|
import { AgentHarness } from "./agent-harness.js";
|
||||||
import { Session } from "./session/session.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<TMetadata extends SessionMetadata>(
|
export function createSession<TMetadata extends SessionMetadata>(
|
||||||
storage: SessionStorage<TMetadata>,
|
storage: SessionStorage<TMetadata>,
|
||||||
@@ -9,5 +9,5 @@ export function createSession<TMetadata extends SessionMetadata>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createAgentHarness(options: AgentHarnessOptions): AgentHarness {
|
export function createAgentHarness(options: AgentHarnessOptions): AgentHarness {
|
||||||
return new DefaultAgentHarness(options);
|
return new AgentHarness(options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ImageContent, Model, TextContent } from "@mariozechner/pi-ai";
|
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";
|
import type { Session } from "./session/session.js";
|
||||||
|
|
||||||
export type SourceScope = "user" | "project" | "temporary";
|
export type SourceScope = "user" | "project" | "temporary";
|
||||||
@@ -524,60 +524,10 @@ export interface AgentHarnessOptions {
|
|||||||
promptTemplates?: PromptTemplate[];
|
promptTemplates?: PromptTemplate[];
|
||||||
skills?: Skill[];
|
skills?: Skill[];
|
||||||
requestAuth?: (model: Model<any>) => Promise<{ apiKey: string; headers?: Record<string, string> } | undefined>;
|
requestAuth?: (model: Model<any>) => Promise<{ apiKey: string; headers?: Record<string, string> } | undefined>;
|
||||||
initialModel?: Model<any>;
|
initialModel: Model<any>;
|
||||||
initialThinkingLevel?: ThinkingLevel;
|
initialThinkingLevel?: ThinkingLevel;
|
||||||
initialActiveToolNames?: string[];
|
initialActiveToolNames?: string[];
|
||||||
initialSystemPromptInputs?: SystemPromptInputs;
|
initialSystemPromptInputs?: SystemPromptInputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentHarness {
|
export type { AgentHarness } from "./agent-harness.js";
|
||||||
readonly agent: Agent;
|
|
||||||
readonly env: ExecutionEnv;
|
|
||||||
readonly conversation: AgentHarnessConversationState;
|
|
||||||
readonly operation: AgentHarnessOperationState;
|
|
||||||
|
|
||||||
prompt(text: string, options?: AgentHarnessPromptOptions): Promise<void>;
|
|
||||||
skill(name: string, args?: string): Promise<void>;
|
|
||||||
|
|
||||||
steer(message: AgentMessage): void;
|
|
||||||
followUp(message: AgentMessage): void;
|
|
||||||
nextTurn(message: AgentMessage): void;
|
|
||||||
|
|
||||||
appendMessage(message: AgentMessage): Promise<void>;
|
|
||||||
|
|
||||||
shell(
|
|
||||||
command: string,
|
|
||||||
options?: {
|
|
||||||
cwd?: string;
|
|
||||||
env?: Record<string, string>;
|
|
||||||
timeout?: number;
|
|
||||||
signal?: AbortSignal;
|
|
||||||
onStdout?: (chunk: string) => void;
|
|
||||||
onStderr?: (chunk: string) => void;
|
|
||||||
},
|
|
||||||
): Promise<{ stdout: string; stderr: string; exitCode: number }>;
|
|
||||||
|
|
||||||
compact(customInstructions?: string): Promise<CompactResult>;
|
|
||||||
navigateTree(
|
|
||||||
targetId: string,
|
|
||||||
options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
|
|
||||||
): Promise<NavigateTreeResult>;
|
|
||||||
|
|
||||||
setModel(model: Model<any>): Promise<void>;
|
|
||||||
setThinkingLevel(level: ThinkingLevel): Promise<void>;
|
|
||||||
setActiveTools(toolNames: string[]): Promise<void>;
|
|
||||||
setSystemPromptInputs(inputs: SystemPromptInputs): Promise<void>;
|
|
||||||
|
|
||||||
abort(): Promise<AbortResult>;
|
|
||||||
waitForIdle(): Promise<void>;
|
|
||||||
|
|
||||||
subscribe(listener: (event: AgentHarnessEvent, signal?: AbortSignal) => Promise<void> | void): () => void;
|
|
||||||
|
|
||||||
on<TType extends keyof AgentHarnessEventResultMap>(
|
|
||||||
type: TType,
|
|
||||||
handler: (
|
|
||||||
event: Extract<AgentHarnessEvent, { type: TType }>,
|
|
||||||
ctx: AgentHarnessContext,
|
|
||||||
) => Promise<AgentHarnessEventResultMap[TType]> | AgentHarnessEventResultMap[TType],
|
|
||||||
): () => void;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getModel } from "@mariozechner/pi-ai";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { NodeExecutionEnv } from "../../src/harness/execution-env.js";
|
import { NodeExecutionEnv } from "../../src/harness/execution-env.js";
|
||||||
import { createAgentHarness, createSession } from "../../src/harness/factory.js";
|
import { createAgentHarness, createSession } from "../../src/harness/factory.js";
|
||||||
@@ -16,7 +17,7 @@ describe("harness factories", () => {
|
|||||||
it("creates agent harnesses", () => {
|
it("creates agent harnesses", () => {
|
||||||
const session = createSession(new InMemorySessionStorage());
|
const session = createSession(new InMemorySessionStorage());
|
||||||
const env = new NodeExecutionEnv({ cwd: process.cwd() });
|
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.env).toBe(env);
|
||||||
expect(harness.conversation.session).toBe(session);
|
expect(harness.conversation.session).toBe(session);
|
||||||
});
|
});
|
||||||
|
|||||||
13
packages/agent/test/scratch/simple.ts
Normal file
13
packages/agent/test/scratch/simple.ts
Normal file
@@ -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);
|
||||||
Reference in New Issue
Block a user