refactor(agent): finalize harness resource config

This commit is contained in:
Mario Zechner
2026-05-10 00:49:00 +02:00
parent 79db9d62ef
commit e25415dd5f
10 changed files with 224 additions and 138 deletions

View File

@@ -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<TSkill, TPromptTemplate>;
private systemPrompt: AgentHarnessOptions<TSkill, TPromptTemplate, TTool>["systemPrompt"];
private getApiKeyAndHeaders?: AgentHarnessOptions["getApiKeyAndHeaders"];
private tools = new Map<string, AgentTool>();
private listeners = new Set<(event: AgentHarnessEvent, signal?: AbortSignal) => Promise<void> | void>();
private tools = new Map<string, TTool>();
private listeners = new Set<
(event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal) => Promise<void> | void
>();
private hooks = new Map<keyof AgentHarnessEventResultMap, Set<(event: any) => Promise<any> | any>>();
constructor(options: AgentHarnessOptions) {
constructor(options: AgentHarnessOptions<TSkill, TPromptTemplate, TTool>) {
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<void> {
private async emitOwn(event: AgentHarnessOwnEvent<TSkill, TPromptTemplate>, signal?: AbortSignal): Promise<void> {
for (const listener of this.listeners) {
await listener(event, signal);
}
}
private async emitAny(event: AgentHarnessEvent, signal?: AbortSignal): Promise<void> {
private async emitAny(event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal): Promise<void> {
for (const listener of this.listeners) {
await listener(event, signal);
}
@@ -163,13 +171,13 @@ export class AgentHarness {
});
}
private async createTurnState(): Promise<AgentHarnessTurnState> {
private async createTurnState(): Promise<AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>> {
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<TSkill, TPromptTemplate, TTool>): 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<TSkill, TPromptTemplate, TTool>,
text: string,
options?: { images?: ImageContent[] },
): Promise<AssistantMessage> {
@@ -578,22 +586,23 @@ export class AgentHarness {
this.agent.followUpMode = mode;
}
getResources(): AgentHarnessResources {
getResources(): AgentHarnessResources<TSkill, TPromptTemplate> {
return {
skills: this.resources.skills?.slice(),
promptTemplates: this.resources.promptTemplates?.slice(),
};
}
async setResources(resources: AgentHarnessResources): Promise<void> {
async setResources(resources: AgentHarnessResources<TSkill, TPromptTemplate>): Promise<void> {
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<void> {
async setTools(tools: TTool[], activeToolNames?: string[]): Promise<void> {
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): () => void {
subscribe(
listener: (event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal) => Promise<void> | void,
): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}

View File

@@ -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<TMetadata extends SessionMetadata>(
storage: SessionStorage<TMetadata>,
): Session<TMetadata> {
return new Session(storage);
}
export function createAgentHarness(options: AgentHarnessOptions): AgentHarness {
return new AgentHarness(options);
}

View File

@@ -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<TSource>(
export async function loadSourcedPromptTemplates<TSource, TPromptTemplate extends PromptTemplate = PromptTemplate>(
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<PromptTemplateDiagnostic & { source: TSource }>;
}> {
const promptTemplates: Array<{ promptTemplate: PromptTemplate; source: TSource }> = [];
const promptTemplates: Array<{ promptTemplate: TPromptTemplate; source: TSource }> = [];
const diagnostics: Array<PromptTemplateDiagnostic & { source: TSource }> = [];
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 };

View File

@@ -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<TSource>(
export async function loadSourcedSkills<TSource, TSkill extends Skill = Skill>(
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<SkillDiagnostic & { source: TSource }>;
}> {
const skills: Array<{ skill: Skill; source: TSource }> = [];
const skills: Array<{ skill: TSkill; source: TSource }> = [];
const diagnostics: Array<SkillDiagnostic & { source: TSource }> = [];
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 };

View File

@@ -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<TSkill, TPromptTemplate>;
systemPrompt: string;
model: Model<any>;
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<TSkill, TPromptTemplate>;
}
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<TSkill, TPromptTemplate>;
previousResources: AgentHarnessResources<TSkill, TPromptTemplate>;
}
export type AgentHarnessOwnEvent =
export type AgentHarnessOwnEvent<
TSkill extends Skill = Skill,
TPromptTemplate extends PromptTemplate = PromptTemplate,
> =
| QueueUpdateEvent
| SavePointEvent
| AbortEvent
| SettledEvent
| BeforeAgentStartEvent
| BeforeAgentStartEvent<TSkill, TPromptTemplate>
| ContextEvent
| BeforeProviderRequestEvent
| AfterProviderResponseEvent
@@ -432,9 +449,11 @@ export type AgentHarnessOwnEvent =
| SessionTreeEvent
| ModelSelectEvent
| ThinkingLevelSelectEvent
| ResourcesUpdateEvent;
| ResourcesUpdateEvent<TSkill, TPromptTemplate>;
export type AgentHarnessEvent = AgentEvent | AgentHarnessOwnEvent;
export type AgentHarnessEvent<TSkill extends Skill = Skill, TPromptTemplate extends PromptTemplate = PromptTemplate> =
| AgentEvent
| AgentHarnessOwnEvent<TSkill, TPromptTemplate>;
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<TSkill, TPromptTemplate>;
systemPrompt?:
| string
| ((context: {
@@ -584,8 +607,8 @@ export interface AgentHarnessOptions {
session: Session;
model: Model<any>;
thinkingLevel: ThinkingLevel;
activeTools: AgentTool[];
resources: AgentHarnessResources;
activeTools: TTool[];
resources: AgentHarnessResources<TSkill, TPromptTemplate>;
}) => string | Promise<string>);
getApiKeyAndHeaders?: (
model: Model<any>,

View File

@@ -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";