diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index f87575d6..c88cf7aa 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -4,6 +4,20 @@ This document describes the current direction and implemented behavior. Some extension/session-facade details are planned and called out explicitly. +## Ultimate lifecycle goal + +Harness listeners and hooks should be able to close over the `AgentHarness` instance and call public harness APIs from any event where those APIs are documented as allowed. Those calls must not corrupt in-flight turn snapshots, reorder persisted transcript entries, lose pending writes, deadlock settlement, or leave the harness in the wrong phase. + +The intended rule is: + +- structural operations remain rejected while busy +- queue operations are accepted at documented turn-safe points +- runtime config setters update future snapshots without mutating the current provider request +- session writes made while busy are durably queued and flushed in deterministic order +- getters return latest harness config, not in-flight snapshots + +A final lifecycle hardening pass should prove these guarantees with a broad listener/hook reentrancy test suite. + ## State model The harness separates state into four categories. @@ -23,7 +37,7 @@ Getters return harness config. They do not return the snapshot used by an in-fli Setters update harness config immediately, including while a turn is in flight. Changes affect the next turn snapshot, not the currently running provider request. -`setResources()` accepts concrete resources and emits `resources_update` with shallow-copied resources. Applications own loading/reloading resources from disk or other sources and should call `setResources()` with new values. +`setResources()` accepts concrete resources and emits `resources_update` on every call with shallow-copied current and previous resources. Applications own loading/reloading resources from disk or other sources and should call `setResources()` with new values. `getResources()` returns shallow-copied current resources. It is a live config read, not the last turn snapshot. @@ -173,3 +187,18 @@ They are allowed only while idle and are not queued. They operate on persisted s Branch summary generation is part of the tree navigation operation. Auto-compaction and retry decision points are not implemented in `AgentHarness` yet. + +## Final lifecycle hardening todo + +Before treating `AgentHarness` as migration-ready, add a broad test suite that exercises listeners and hooks closing over the harness and calling public APIs during every relevant event: + +- runtime config setters from low-level lifecycle events and harness events +- resource/tool/model/thinking updates during active turns and save points +- session writes from listeners and hooks, including writes from `settled` +- queue operations from turn events, tool events, and provider hooks +- rejected structural operations while busy +- abort from listeners/hooks +- getter behavior during active operations +- deterministic ordering of agent-emitted messages and pending listener writes +- no deadlocks when async listeners call harness APIs and await them +- phase cleanup through success, provider error, hook error, abort, compaction, and tree navigation diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index f7476435..33b96b37 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -17,7 +17,9 @@ import type { ExecutionEnv, NavigateTreeResult, PendingSessionWrite, + PromptTemplate, Session, + Skill, } from "./types.js"; function createUserMessage(text: string, images?: ImageContent[]): UserMessage { @@ -26,7 +28,11 @@ function createUserMessage(text: string, images?: ImageContent[]): UserMessage { return { role: "user", content, timestamp: Date.now() }; } -export class AgentHarness { +export class AgentHarness< + TSkill extends Skill = Skill, + TPromptTemplate extends PromptTemplate = PromptTemplate, + TTool extends AgentTool = AgentTool, +> { readonly agent: Agent; readonly env: ExecutionEnv; private session: Session; @@ -38,14 +44,16 @@ export class AgentHarness { private steerQueue: UserMessage[] = []; private followUpQueue: UserMessage[] = []; private pendingSessionWrites: PendingSessionWrite[] = []; - private resources: AgentHarnessResources; - private systemPrompt: AgentHarnessOptions["systemPrompt"]; + private resources: AgentHarnessResources; + private systemPrompt: AgentHarnessOptions["systemPrompt"]; private getApiKeyAndHeaders?: AgentHarnessOptions["getApiKeyAndHeaders"]; - private tools = new Map(); - private listeners = new Set<(event: AgentHarnessEvent, signal?: AbortSignal) => Promise | void>(); + private tools = new Map(); + private listeners = new Set< + (event: AgentHarnessEvent, signal?: AbortSignal) => Promise | void + >(); private hooks = new Map Promise | any>>(); - constructor(options: AgentHarnessOptions) { + constructor(options: AgentHarnessOptions) { this.agent = new Agent({ initialState: { model: options.model, @@ -60,12 +68,12 @@ export class AgentHarness { this.resources = options.resources ?? {}; this.systemPrompt = options.systemPrompt; this.getApiKeyAndHeaders = options.getApiKeyAndHeaders; - for (const tool of this.agent.state.tools) { + for (const tool of options.tools ?? []) { this.tools.set(tool.name, tool); } this.model = options.model; this.thinkingLevel = options.thinkingLevel ?? this.agent.state.thinkingLevel; - this.activeToolNames = options.activeToolNames ?? this.agent.state.tools.map((tool) => tool.name); + this.activeToolNames = options.activeToolNames ?? (options.tools ?? []).map((tool) => tool.name); this.agent.state.model = this.model; this.agent.state.thinkingLevel = this.thinkingLevel; this.agent.getApiKey = async (provider) => { @@ -127,13 +135,13 @@ export class AgentHarness { }); } - private async emitOwn(event: AgentHarnessOwnEvent, signal?: AbortSignal): Promise { + private async emitOwn(event: AgentHarnessOwnEvent, signal?: AbortSignal): Promise { for (const listener of this.listeners) { await listener(event, signal); } } - private async emitAny(event: AgentHarnessEvent, signal?: AbortSignal): Promise { + private async emitAny(event: AgentHarnessEvent, signal?: AbortSignal): Promise { for (const listener of this.listeners) { await listener(event, signal); } @@ -163,13 +171,13 @@ export class AgentHarness { }); } - private async createTurnState(): Promise { + private async createTurnState(): Promise> { const context = await this.session.buildContext(); const resources = this.getResources(); const tools = [...this.tools.values()]; const activeTools = this.activeToolNames .map((name) => this.tools.get(name)) - .filter((tool): tool is AgentTool => tool !== undefined); + .filter((tool): tool is TTool => tool !== undefined); let systemPrompt = "You are a helpful assistant."; if (typeof this.systemPrompt === "string") { systemPrompt = this.systemPrompt; @@ -194,7 +202,7 @@ export class AgentHarness { }; } - private applyTurnState(turnState: AgentHarnessTurnState): void { + private applyTurnState(turnState: AgentHarnessTurnState): void { this.agent.state.messages = turnState.messages; this.agent.state.systemPrompt = turnState.systemPrompt; this.agent.state.model = turnState.model; @@ -263,7 +271,7 @@ export class AgentHarness { } private async executeTurn( - turnState: AgentHarnessTurnState, + turnState: AgentHarnessTurnState, text: string, options?: { images?: ImageContent[] }, ): Promise { @@ -578,22 +586,23 @@ export class AgentHarness { this.agent.followUpMode = mode; } - getResources(): AgentHarnessResources { + getResources(): AgentHarnessResources { return { skills: this.resources.skills?.slice(), promptTemplates: this.resources.promptTemplates?.slice(), }; } - async setResources(resources: AgentHarnessResources): Promise { + async setResources(resources: AgentHarnessResources): Promise { + const previousResources = this.getResources(); this.resources = { skills: resources.skills?.slice(), promptTemplates: resources.promptTemplates?.slice(), }; - await this.emitOwn({ type: "resources_update", resources: this.getResources() }); + await this.emitOwn({ type: "resources_update", resources: this.getResources(), previousResources }); } - async setTools(tools: AgentTool[], activeToolNames?: string[]): Promise { + async setTools(tools: TTool[], activeToolNames?: string[]): Promise { this.tools = new Map(tools.map((tool) => [tool.name, tool])); if (activeToolNames) { this.validateToolNames(activeToolNames); @@ -623,7 +632,9 @@ export class AgentHarness { await this.agent.waitForIdle(); } - subscribe(listener: (event: AgentHarnessEvent, signal?: AbortSignal) => Promise | void): () => void { + subscribe( + listener: (event: AgentHarnessEvent, signal?: AbortSignal) => Promise | void, + ): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); } diff --git a/packages/agent/src/harness/factory.ts b/packages/agent/src/harness/factory.ts deleted file mode 100644 index aeaa3a49..00000000 --- a/packages/agent/src/harness/factory.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AgentHarness } from "./agent-harness.js"; -import { Session } from "./session/session.js"; -import type { AgentHarnessOptions, SessionMetadata, SessionStorage } from "./types.js"; - -export function createSession( - storage: SessionStorage, -): Session { - return new Session(storage); -} - -export function createAgentHarness(options: AgentHarnessOptions): AgentHarness { - return new AgentHarness(options); -} diff --git a/packages/agent/src/harness/prompt-templates.ts b/packages/agent/src/harness/prompt-templates.ts index 988008e9..e77682b8 100644 --- a/packages/agent/src/harness/prompt-templates.ts +++ b/packages/agent/src/harness/prompt-templates.ts @@ -52,19 +52,26 @@ export async function loadPromptTemplates( * Source values are preserved exactly and attached to every loaded prompt template and diagnostic. The agent package does * not interpret source values; applications define their own provenance shape. */ -export async function loadSourcedPromptTemplates( +export async function loadSourcedPromptTemplates( env: ExecutionEnv, inputs: Array<{ path: string; source: TSource }>, + mapPromptTemplate?: (promptTemplate: PromptTemplate, source: TSource) => TPromptTemplate, ): Promise<{ - promptTemplates: Array<{ promptTemplate: PromptTemplate; source: TSource }>; + promptTemplates: Array<{ promptTemplate: TPromptTemplate; source: TSource }>; diagnostics: Array; }> { - const promptTemplates: Array<{ promptTemplate: PromptTemplate; source: TSource }> = []; + const promptTemplates: Array<{ promptTemplate: TPromptTemplate; source: TSource }> = []; const diagnostics: Array = []; for (const input of inputs) { const result = await loadPromptTemplates(env, input.path); - for (const promptTemplate of result.promptTemplates) - promptTemplates.push({ promptTemplate, source: input.source }); + for (const promptTemplate of result.promptTemplates) { + promptTemplates.push({ + promptTemplate: mapPromptTemplate + ? mapPromptTemplate(promptTemplate, input.source) + : (promptTemplate as TPromptTemplate), + source: input.source, + }); + } for (const diagnostic of result.diagnostics) diagnostics.push({ ...diagnostic, source: input.source }); } return { promptTemplates, diagnostics }; diff --git a/packages/agent/src/harness/skills.ts b/packages/agent/src/harness/skills.ts index 3fd6b7fb..db03d93c 100644 --- a/packages/agent/src/harness/skills.ts +++ b/packages/agent/src/harness/skills.ts @@ -59,18 +59,21 @@ export async function loadSkills( * Source values are preserved exactly and attached to every loaded skill and diagnostic. The agent package does not * interpret source values; applications define their own provenance shape. */ -export async function loadSourcedSkills( +export async function loadSourcedSkills( env: ExecutionEnv, inputs: Array<{ path: string; source: TSource }>, + mapSkill?: (skill: Skill, source: TSource) => TSkill, ): Promise<{ - skills: Array<{ skill: Skill; source: TSource }>; + skills: Array<{ skill: TSkill; source: TSource }>; diagnostics: Array; }> { - const skills: Array<{ skill: Skill; source: TSource }> = []; + const skills: Array<{ skill: TSkill; source: TSource }> = []; const diagnostics: Array = []; for (const input of inputs) { const result = await loadSkills(env, input.path); - for (const skill of result.skills) skills.push({ skill, source: input.source }); + for (const skill of result.skills) { + skills.push({ skill: mapSkill ? mapSkill(skill, input.source) : (skill as TSkill), source: input.source }); + } for (const diagnostic of result.diagnostics) diagnostics.push({ ...diagnostic, source: input.source }); } return { skills, diagnostics }; diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index afb2e75b..24b5feeb 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -33,11 +33,14 @@ export interface PromptTemplate { } /** Resources made available to explicit invocation methods and system-prompt callbacks. */ -export interface AgentHarnessResources { +export interface AgentHarnessResources< + TSkill extends Skill = Skill, + TPromptTemplate extends PromptTemplate = PromptTemplate, +> { /** Prompt templates available for explicit invocation. */ - promptTemplates?: PromptTemplate[]; + promptTemplates?: TPromptTemplate[]; /** Skills available to the model and explicit skill invocation. */ - skills?: Skill[]; + skills?: TSkill[]; } /** Kind of filesystem object as addressed by an {@link ExecutionEnv}. Symlinks are not followed automatically. */ @@ -295,14 +298,18 @@ export type PendingSessionWrite = SessionTreeEntry extends infer TEntry : never : never; -export interface AgentHarnessTurnState { +export interface AgentHarnessTurnState< + TSkill extends Skill = Skill, + TPromptTemplate extends PromptTemplate = PromptTemplate, + TTool extends AgentTool = AgentTool, +> { messages: AgentMessage[]; - resources: AgentHarnessResources; + resources: AgentHarnessResources; systemPrompt: string; model: Model; thinkingLevel: ThinkingLevel; - tools: AgentTool[]; - activeTools: AgentTool[]; + tools: TTool[]; + activeTools: TTool[]; } export interface QueueUpdateEvent { @@ -328,12 +335,15 @@ export interface SettledEvent { nextTurnCount: number; } -export interface BeforeAgentStartEvent { +export interface BeforeAgentStartEvent< + TSkill extends Skill = Skill, + TPromptTemplate extends PromptTemplate = PromptTemplate, +> { type: "before_agent_start"; prompt: string; images?: ImageContent[]; systemPrompt: string; - resources: AgentHarnessResources; + resources: AgentHarnessResources; } export interface ContextEvent { @@ -410,17 +420,24 @@ export interface ThinkingLevelSelectEvent { previousLevel: ThinkingLevel; } -export interface ResourcesUpdateEvent { +export interface ResourcesUpdateEvent< + TSkill extends Skill = Skill, + TPromptTemplate extends PromptTemplate = PromptTemplate, +> { type: "resources_update"; - resources: AgentHarnessResources; + resources: AgentHarnessResources; + previousResources: AgentHarnessResources; } -export type AgentHarnessOwnEvent = +export type AgentHarnessOwnEvent< + TSkill extends Skill = Skill, + TPromptTemplate extends PromptTemplate = PromptTemplate, +> = | QueueUpdateEvent | SavePointEvent | AbortEvent | SettledEvent - | BeforeAgentStartEvent + | BeforeAgentStartEvent | ContextEvent | BeforeProviderRequestEvent | AfterProviderResponseEvent @@ -432,9 +449,11 @@ export type AgentHarnessOwnEvent = | SessionTreeEvent | ModelSelectEvent | ThinkingLevelSelectEvent - | ResourcesUpdateEvent; + | ResourcesUpdateEvent; -export type AgentHarnessEvent = AgentEvent | AgentHarnessOwnEvent; +export type AgentHarnessEvent = + | AgentEvent + | AgentHarnessOwnEvent; export interface BeforeAgentStartResult { messages?: AgentMessage[]; @@ -568,15 +587,19 @@ export interface BranchSummaryResult { error?: string; } -export interface AgentHarnessOptions { +export interface AgentHarnessOptions< + TSkill extends Skill = Skill, + TPromptTemplate extends PromptTemplate = PromptTemplate, + TTool extends AgentTool = AgentTool, +> { env: ExecutionEnv; session: Session; - tools?: AgentTool[]; + tools?: TTool[]; /** * Concrete resources available to explicit invocation methods and system-prompt callbacks. * Applications own loading/reloading resources and should call `setResources()` with new values. */ - resources?: AgentHarnessResources; + resources?: AgentHarnessResources; systemPrompt?: | string | ((context: { @@ -584,8 +607,8 @@ export interface AgentHarnessOptions { session: Session; model: Model; thinkingLevel: ThinkingLevel; - activeTools: AgentTool[]; - resources: AgentHarnessResources; + activeTools: TTool[]; + resources: AgentHarnessResources; }) => string | Promise); getApiKeyAndHeaders?: ( model: Model, diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 256a7ce6..7b6018c1 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -23,7 +23,6 @@ export { shouldCompact, } from "./harness/compaction/compaction.js"; export * from "./harness/execution-env.js"; -export * from "./harness/factory.js"; export * from "./harness/messages.js"; export * from "./harness/prompt-templates.js"; export * from "./harness/session/repo/jsonl.js"; diff --git a/packages/agent/test/harness/agent-harness.test.ts b/packages/agent/test/harness/agent-harness.test.ts new file mode 100644 index 00000000..453b1297 --- /dev/null +++ b/packages/agent/test/harness/agent-harness.test.ts @@ -0,0 +1,82 @@ +import { getModel } from "@earendil-works/pi-ai"; +import { describe, expect, it } from "vitest"; +import { AgentHarness } from "../../src/harness/agent-harness.js"; +import { NodeExecutionEnv } from "../../src/harness/execution-env.js"; +import { Session } from "../../src/harness/session/session.js"; +import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; +import type { PromptTemplate, Skill } from "../../src/harness/types.js"; +import type { AgentTool } from "../../src/types.js"; + +interface AppSkill extends Skill { + source: "project" | "user"; +} + +interface AppPromptTemplate extends PromptTemplate { + source: "project" | "user"; +} + +interface AppTool extends AgentTool { + source: "builtin" | "extension"; +} + +describe("AgentHarness", () => { + it("constructs directly and exposes queue modes", () => { + const session = new Session(new InMemorySessionStorage()); + const env = new NodeExecutionEnv({ cwd: process.cwd() }); + const initialModel = getModel("anthropic", "claude-sonnet-4-5"); + const harness = new AgentHarness({ + env, + session, + model: initialModel, + systemPrompt: "You are helpful.", + steeringMode: "all", + followUpMode: "all", + }); + expect(harness.env).toBe(env); + expect(harness.agent.state.model).toBe(initialModel); + expect(harness.steeringMode).toBe("all"); + expect(harness.followUpMode).toBe("all"); + harness.steeringMode = "one-at-a-time"; + harness.followUpMode = "one-at-a-time"; + expect(harness.agent.steeringMode).toBe("one-at-a-time"); + expect(harness.agent.followUpMode).toBe("one-at-a-time"); + }); + + it("preserves app resource types for getters and update events", async () => { + const session = new Session(new InMemorySessionStorage()); + const env = new NodeExecutionEnv({ cwd: process.cwd() }); + const model = getModel("anthropic", "claude-sonnet-4-5"); + const harness = new AgentHarness({ env, session, model }); + const skill: AppSkill = { + name: "inspect", + description: "Inspect things", + content: "Use inspection tools.", + filePath: "/skills/inspect/SKILL.md", + source: "project", + }; + const promptTemplate: AppPromptTemplate = { name: "review", content: "Review $1", source: "user" }; + const resources = { skills: [skill], promptTemplates: [promptTemplate] }; + const updates: Array<{ resourcesSource?: string; previousSource?: string }> = []; + harness.subscribe((event) => { + if (event.type === "resources_update") { + updates.push({ + resourcesSource: event.resources.skills?.[0]?.source, + previousSource: event.previousResources.skills?.[0]?.source, + }); + } + }); + + await harness.setResources(resources); + await harness.setResources(resources); + const resolved = harness.getResources(); + + expect(updates).toEqual([ + { resourcesSource: "project", previousSource: undefined }, + { resourcesSource: "project", previousSource: "project" }, + ]); + expect(resolved.skills?.[0]?.source).toBe("project"); + expect(resolved.promptTemplates?.[0]?.source).toBe("user"); + expect(resolved.skills).not.toBe(resources.skills); + expect(resolved.promptTemplates).not.toBe(resources.promptTemplates); + }); +}); diff --git a/packages/agent/test/harness/factory.test.ts b/packages/agent/test/harness/factory.test.ts deleted file mode 100644 index 20e5cd95..00000000 --- a/packages/agent/test/harness/factory.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { getModel } from "@earendil-works/pi-ai"; -import { describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "../../src/harness/execution-env.js"; -import { createAgentHarness, createSession } from "../../src/harness/factory.js"; -import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; - -describe("harness factories", () => { - it("creates sessions from storage", async () => { - const storage = new InMemorySessionStorage({ - metadata: { id: "session-1", createdAt: "2026-01-01T00:00:00.000Z" }, - }); - const session = createSession(storage); - expect(session.getStorage()).toBe(storage); - expect(await session.getMetadata()).toEqual({ id: "session-1", createdAt: "2026-01-01T00:00:00.000Z" }); - }); - - it("creates agent harnesses", () => { - const session = createSession(new InMemorySessionStorage()); - const env = new NodeExecutionEnv({ cwd: process.cwd() }); - const initialModel = getModel("anthropic", "claude-sonnet-4-5"); - const harness = createAgentHarness({ - env, - session, - model: initialModel, - systemPrompt: "You are helpful.", - steeringMode: "all", - followUpMode: "all", - }); - expect(harness.env).toBe(env); - expect(harness.agent.state.model).toBe(initialModel); - expect(harness.steeringMode).toBe("all"); - expect(harness.followUpMode).toBe("all"); - harness.steeringMode = "one-at-a-time"; - harness.followUpMode = "one-at-a-time"; - expect(harness.agent.steeringMode).toBe("one-at-a-time"); - expect(harness.agent.followUpMode).toBe("one-at-a-time"); - }); - - it("updates and reads concrete resources", async () => { - const session = createSession(new InMemorySessionStorage()); - const env = new NodeExecutionEnv({ cwd: process.cwd() }); - const model = getModel("anthropic", "claude-sonnet-4-5"); - const harness = createAgentHarness({ env, session, model }); - const skill = { - name: "inspect", - description: "Inspect things", - content: "Use inspection tools.", - filePath: "/skills/inspect/SKILL.md", - }; - const resources = { skills: [skill], promptTemplates: [{ name: "review", content: "Review $1" }] }; - const updates: unknown[] = []; - harness.subscribe((event) => { - if (event.type === "resources_update") updates.push(event.resources); - }); - - await harness.setResources(resources); - const resolved = harness.getResources(); - - expect(updates).toEqual([resources]); - expect(resolved).toEqual(resources); - expect(resolved.skills).not.toBe(resources.skills); - expect(resolved.promptTemplates).not.toBe(resources.promptTemplates); - }); -}); diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts index 01cec2ec..c417740f 100644 --- a/packages/agent/test/scratch/simple.ts +++ b/packages/agent/test/scratch/simple.ts @@ -3,30 +3,39 @@ import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai"; import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; import { - createAgentHarness, + AgentHarness, formatSkillsForSystemPrompt, loadSourcedPromptTemplates, loadSourcedSkills, NodeExecutionEnv, + type PromptTemplate, Session, + type Skill, } from "../../src/index.js"; type Source = { type: "project" | "user" | "path"; dir: string }; +type SourcedSkill = Skill & { source: Source }; +type SourcedPromptTemplate = PromptTemplate & { source: Source }; const env = new NodeExecutionEnv({ cwd: process.cwd() }); const source = (type: Source["type"], dir: string) => ({ path: dir, source: { type, dir } }); -const { skills: sourcedSkills } = await loadSourcedSkills(env, [ - source("project", join(env.cwd, ".pi/skills")), - source("user", join(homedir(), ".pi/agent/skills")), - source("path", join(env.cwd, "../../../pi-skills")), -]); -const { promptTemplates: sourcedPromptTemplates } = await loadSourcedPromptTemplates(env, [ - source("project", join(env.cwd, ".pi/prompts")), - source("user", join(homedir(), ".pi/agent/prompts")), -]); +const { skills: sourcedSkills } = await loadSourcedSkills( + env, + [ + source("project", join(env.cwd, ".pi/skills")), + source("user", join(homedir(), ".pi/agent/skills")), + source("path", join(env.cwd, "../../../pi-skills")), + ], + (skill, source) => ({ ...skill, source }), +); +const { promptTemplates: sourcedPromptTemplates } = await loadSourcedPromptTemplates( + env, + [source("project", join(env.cwd, ".pi/prompts")), source("user", join(homedir(), ".pi/agent/prompts"))], + (promptTemplate, source) => ({ ...promptTemplate, source }), +); const session = new Session(new InMemorySessionStorage()); -const agent = createAgentHarness({ +const agent = new AgentHarness({ env, session, model: getModel("openai", "gpt-5.5"),