diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index 674a0d4a..f87575d6 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -16,13 +16,17 @@ Harness config is the latest runtime configuration set by the application or ext - thinking level - tools - active tool names -- resources or resource provider +- resources - system prompt or system prompt provider Getters return harness config. They do not return the snapshot used by an in-flight provider request. 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. + +`getResources()` returns shallow-copied current resources. It is a live config read, not the last turn snapshot. + ### Turn snapshot A turn snapshot is the concrete state used for one LLM turn. It is created by `createTurnState()` and contains: @@ -35,7 +39,9 @@ A turn snapshot is the concrete state used for one LLM turn. It is created by `c - all tools - active tools -Static option values are used directly. Provider callbacks are invoked once per `createTurnState()` call. All logic for that turn uses the same snapshot. +Static option values are used directly. System-prompt provider callbacks are invoked once per `createTurnState()` call. All logic for that turn uses the same snapshot. + +Resource arrays are shallow-copied when a snapshot is created. Individual skill and prompt-template objects are not deep-copied. ### Session @@ -116,6 +122,8 @@ The low-level loop converts harness `ThinkingLevel` to provider `reasoning` at t No state refresh is needed on `agent_end` except flushing leftover pending session writes and clearing the operation phase. The exact `settled` event timing is still under review. +If the system-prompt callback throws while starting `prompt`, `skill`, or `promptFromTemplate`, the operation throws and the harness returns to idle. If it throws from the save-point snapshot created by `prepareNextTurn`, the low-level agent run records an assistant error message. + ## Hooks and events Current hooks receive only the event payload. There is no extension context object yet. diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index dd7bf90b..f7476435 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -38,7 +38,7 @@ export class AgentHarness { private steerQueue: UserMessage[] = []; private followUpQueue: UserMessage[] = []; private pendingSessionWrites: PendingSessionWrite[] = []; - private resources?: AgentHarnessOptions["resources"]; + private resources: AgentHarnessResources; private systemPrompt: AgentHarnessOptions["systemPrompt"]; private getApiKeyAndHeaders?: AgentHarnessOptions["getApiKeyAndHeaders"]; private tools = new Map(); @@ -57,7 +57,7 @@ export class AgentHarness { }); this.env = options.env; this.session = options.session; - this.resources = options.resources; + this.resources = options.resources ?? {}; this.systemPrompt = options.systemPrompt; this.getApiKeyAndHeaders = options.getApiKeyAndHeaders; for (const tool of this.agent.state.tools) { @@ -165,7 +165,7 @@ export class AgentHarness { private async createTurnState(): Promise { const context = await this.session.buildContext(); - const resources = typeof this.resources === "function" ? await this.resources() : (this.resources ?? {}); + const resources = this.getResources(); const tools = [...this.tools.values()]; const activeTools = this.activeToolNames .map((name) => this.tools.get(name)) @@ -578,8 +578,19 @@ export class AgentHarness { this.agent.followUpMode = mode; } + getResources(): AgentHarnessResources { + return { + skills: this.resources.skills?.slice(), + promptTemplates: this.resources.promptTemplates?.slice(), + }; + } + async setResources(resources: AgentHarnessResources): Promise { - this.resources = resources; + this.resources = { + skills: resources.skills?.slice(), + promptTemplates: resources.promptTemplates?.slice(), + }; + await this.emitOwn({ type: "resources_update", resources: this.getResources() }); } async setTools(tools: AgentTool[], activeToolNames?: string[]): Promise { diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 4c368ce0..afb2e75b 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -410,6 +410,11 @@ export interface ThinkingLevelSelectEvent { previousLevel: ThinkingLevel; } +export interface ResourcesUpdateEvent { + type: "resources_update"; + resources: AgentHarnessResources; +} + export type AgentHarnessOwnEvent = | QueueUpdateEvent | SavePointEvent @@ -426,7 +431,8 @@ export type AgentHarnessOwnEvent = | SessionBeforeTreeEvent | SessionTreeEvent | ModelSelectEvent - | ThinkingLevelSelectEvent; + | ThinkingLevelSelectEvent + | ResourcesUpdateEvent; export type AgentHarnessEvent = AgentEvent | AgentHarnessOwnEvent; @@ -481,6 +487,7 @@ export type AgentHarnessEventResultMap = { session_tree: undefined; model_select: undefined; thinking_level_select: undefined; + resources_update: undefined; queue_update: undefined; save_point: undefined; abort: undefined; @@ -565,7 +572,11 @@ export interface AgentHarnessOptions { env: ExecutionEnv; session: Session; tools?: AgentTool[]; - resources?: AgentHarnessResources | (() => AgentHarnessResources | Promise); + /** + * 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; systemPrompt?: | string | ((context: { diff --git a/packages/agent/test/harness/factory.test.ts b/packages/agent/test/harness/factory.test.ts index d8863482..20e5cd95 100644 --- a/packages/agent/test/harness/factory.test.ts +++ b/packages/agent/test/harness/factory.test.ts @@ -35,4 +35,30 @@ describe("harness factories", () => { 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); + }); });