@earendil-works/pi-agent-core 0.76.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,7 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
36
36
  private createStreamFn;
37
37
  private drainQueuedMessages;
38
38
  private createLoopConfig;
39
+ private validateUniqueNames;
39
40
  private validateToolNames;
40
41
  private flushPendingSessionWrites;
41
42
  private handleAgentEvent;
@@ -69,9 +70,12 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
69
70
  label?: string;
70
71
  }): Promise<NavigateTreeResult>;
71
72
  getModel(): Model<any>;
72
- getThinkingLevel(): ThinkingLevel;
73
73
  setModel(model: Model<any>): Promise<void>;
74
+ getThinkingLevel(): ThinkingLevel;
74
75
  setThinkingLevel(level: ThinkingLevel): Promise<void>;
76
+ getTools(): TTool[];
77
+ setTools(tools: TTool[], activeToolNames?: string[]): Promise<void>;
78
+ getActiveTools(): TTool[];
75
79
  setActiveTools(toolNames: string[]): Promise<void>;
76
80
  getSteeringMode(): QueueMode;
77
81
  setSteeringMode(mode: QueueMode): Promise<void>;
@@ -81,7 +85,6 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
81
85
  setResources(resources: AgentHarnessResources<TSkill, TPromptTemplate>): Promise<void>;
82
86
  getStreamOptions(): AgentHarnessStreamOptions;
83
87
  setStreamOptions(streamOptions: AgentHarnessStreamOptions): Promise<void>;
84
- setTools(tools: TTool[], activeToolNames?: string[]): Promise<void>;
85
88
  abort(): Promise<AbortResult>;
86
89
  waitForIdle(): Promise<void>;
87
90
  subscribe(listener: (event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal) => Promise<void> | void): () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"agent-harness.d.ts","sourceRoot":"","sources":["../../src/harness/agent-harness.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype AssistantMessage,\n\ttype ImageContent,\n\ttype Model,\n\tstreamSimple,\n\ttype UserMessage,\n} from \"@earendil-works/pi-ai\";\nimport { runAgentLoop } from \"../agent-loop.ts\";\nimport type {\n\tAgentContext,\n\tAgentEvent,\n\tAgentLoopConfig,\n\tAgentMessage,\n\tAgentTool,\n\tQueueMode,\n\tStreamFn,\n\tThinkingLevel,\n} from \"../types.ts\";\nimport { collectEntriesForBranchSummary, generateBranchSummary } from \"./compaction/branch-summarization.ts\";\nimport { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from \"./compaction/compaction.ts\";\nimport { convertToLlm } from \"./messages.ts\";\nimport { formatPromptTemplateInvocation } from \"./prompt-templates.ts\";\nimport { formatSkillInvocation } from \"./skills.ts\";\nimport type {\n\tAbortResult,\n\tAgentHarnessEvent,\n\tAgentHarnessEventResultMap,\n\tAgentHarnessOptions,\n\tAgentHarnessOwnEvent,\n\tAgentHarnessPhase,\n\tAgentHarnessResources,\n\tAgentHarnessStreamOptions,\n\tAgentHarnessStreamOptionsPatch,\n\tExecutionEnv,\n\tNavigateTreeResult,\n\tPendingSessionWrite,\n\tPromptTemplate,\n\tSession,\n\tSkill,\n} from \"./types.ts\";\nimport { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from \"./types.ts\";\n\nfunction createUserMessage(text: string, images?: ImageContent[]): UserMessage {\n\tconst content: Array<{ type: \"text\"; text: string } | ImageContent> = [{ type: \"text\", text }];\n\tif (images) content.push(...images);\n\treturn { role: \"user\", content, timestamp: Date.now() };\n}\n\nfunction createFailureMessage(model: Model<any>, error: unknown, aborted: boolean): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [{ type: \"text\", text: \"\" }],\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tstopReason: aborted ? \"aborted\" : \"error\",\n\t\terrorMessage: error instanceof Error ? error.message : String(error),\n\t\ttimestamp: Date.now(),\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t};\n}\n\nfunction cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHarnessStreamOptions {\n\treturn {\n\t\t...streamOptions,\n\t\theaders: streamOptions?.headers ? { ...streamOptions.headers } : undefined,\n\t\tmetadata: streamOptions?.metadata ? { ...streamOptions.metadata } : undefined,\n\t};\n}\n\nfunction mergeHeaders(...headers: Array<Record<string, string> | undefined>): Record<string, string> | undefined {\n\tconst merged: Record<string, string> = {};\n\tlet hasHeaders = false;\n\tfor (const entry of headers) {\n\t\tif (!entry) continue;\n\t\tObject.assign(merged, entry);\n\t\thasHeaders = true;\n\t}\n\treturn hasHeaders ? merged : undefined;\n}\n\nfunction applyStreamOptionsPatch(\n\tbase: AgentHarnessStreamOptions,\n\tpatch?: AgentHarnessStreamOptionsPatch,\n): AgentHarnessStreamOptions {\n\tconst result = cloneStreamOptions(base);\n\tif (!patch) return result;\n\n\tif (Object.hasOwn(patch, \"transport\")) result.transport = patch.transport;\n\tif (Object.hasOwn(patch, \"timeoutMs\")) result.timeoutMs = patch.timeoutMs;\n\tif (Object.hasOwn(patch, \"maxRetries\")) result.maxRetries = patch.maxRetries;\n\tif (Object.hasOwn(patch, \"maxRetryDelayMs\")) result.maxRetryDelayMs = patch.maxRetryDelayMs;\n\tif (Object.hasOwn(patch, \"cacheRetention\")) result.cacheRetention = patch.cacheRetention;\n\n\tif (Object.hasOwn(patch, \"headers\")) {\n\t\tif (patch.headers === undefined) {\n\t\t\tresult.headers = undefined;\n\t\t} else {\n\t\t\tconst headers = { ...(result.headers ?? {}) };\n\t\t\tfor (const [key, value] of Object.entries(patch.headers)) {\n\t\t\t\tif (value === undefined) delete headers[key];\n\t\t\t\telse headers[key] = value;\n\t\t\t}\n\t\t\tresult.headers = Object.keys(headers).length > 0 ? headers : undefined;\n\t\t}\n\t}\n\n\tif (Object.hasOwn(patch, \"metadata\")) {\n\t\tif (patch.metadata === undefined) {\n\t\t\tresult.metadata = undefined;\n\t\t} else {\n\t\t\tconst metadata = { ...(result.metadata ?? {}) };\n\t\t\tfor (const [key, value] of Object.entries(patch.metadata)) {\n\t\t\t\tif (value === undefined) delete metadata[key];\n\t\t\t\telse metadata[key] = value;\n\t\t\t}\n\t\t\tresult.metadata = Object.keys(metadata).length > 0 ? metadata : undefined;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nconst SUBSCRIBER_EVENT_TYPE = \"*\";\n\ntype AgentHarnessHandler = (event: any, signal?: AbortSignal) => Promise<any> | any;\n\nfunction normalizeHarnessError(error: unknown, fallbackCode: AgentHarnessError[\"code\"]): AgentHarnessError {\n\tif (error instanceof AgentHarnessError) return error;\n\tconst cause = toError(error);\n\tif (cause instanceof SessionError) return new AgentHarnessError(\"session\", cause.message, cause);\n\tif (cause instanceof CompactionError) return new AgentHarnessError(\"compaction\", cause.message, cause);\n\tif (cause instanceof BranchSummaryError) return new AgentHarnessError(\"branch_summary\", cause.message, cause);\n\treturn new AgentHarnessError(fallbackCode, cause.message, cause);\n}\n\nfunction normalizeHookError(error: unknown): AgentHarnessError {\n\treturn normalizeHarnessError(error, \"hook\");\n}\n\ninterface AgentHarnessTurnState<\n\tTSkill extends Skill = Skill,\n\tTPromptTemplate extends PromptTemplate = PromptTemplate,\n\tTTool extends AgentTool = AgentTool,\n> {\n\tmessages: AgentMessage[];\n\tresources: AgentHarnessResources<TSkill, TPromptTemplate>;\n\tstreamOptions: AgentHarnessStreamOptions;\n\tsessionId: string;\n\tsystemPrompt: string;\n\tmodel: Model<any>;\n\tthinkingLevel: ThinkingLevel;\n\ttools: TTool[];\n\tactiveTools: TTool[];\n}\n\nexport class AgentHarness<\n\tTSkill extends Skill = Skill,\n\tTPromptTemplate extends PromptTemplate = PromptTemplate,\n\tTTool extends AgentTool = AgentTool,\n> {\n\treadonly env: ExecutionEnv;\n\tprivate session: Session;\n\tprivate phase: AgentHarnessPhase = \"idle\";\n\tprivate runAbortController?: AbortController;\n\tprivate runPromise?: Promise<void>;\n\tprivate pendingSessionWrites: PendingSessionWrite[] = [];\n\tprivate model: Model<any>;\n\tprivate thinkingLevel: ThinkingLevel;\n\tprivate systemPrompt: AgentHarnessOptions<TSkill, TPromptTemplate, TTool>[\"systemPrompt\"];\n\tprivate streamOptions: AgentHarnessStreamOptions;\n\tprivate getApiKeyAndHeaders?: AgentHarnessOptions[\"getApiKeyAndHeaders\"];\n\tprivate resources: AgentHarnessResources<TSkill, TPromptTemplate>;\n\tprivate tools = new Map<string, TTool>();\n\tprivate activeToolNames: string[];\n\tprivate steerQueue: UserMessage[] = [];\n\tprivate steeringQueueMode: QueueMode;\n\tprivate followUpQueue: UserMessage[] = [];\n\tprivate followUpQueueMode: QueueMode;\n\tprivate nextTurnQueue: AgentMessage[] = [];\n\tprivate handlers = new Map<string, Set<AgentHarnessHandler>>();\n\n\tconstructor(options: AgentHarnessOptions<TSkill, TPromptTemplate, TTool>) {\n\t\tthis.env = options.env;\n\t\tthis.session = options.session;\n\t\tthis.resources = options.resources ?? {};\n\t\tthis.streamOptions = cloneStreamOptions(options.streamOptions);\n\t\tthis.systemPrompt = options.systemPrompt;\n\t\tthis.getApiKeyAndHeaders = options.getApiKeyAndHeaders;\n\t\tfor (const tool of options.tools ?? []) {\n\t\t\tthis.tools.set(tool.name, tool);\n\t\t}\n\t\tthis.model = options.model;\n\t\tthis.thinkingLevel = options.thinkingLevel ?? \"off\";\n\t\tthis.activeToolNames = options.activeToolNames ?? (options.tools ?? []).map((tool) => tool.name);\n\t\tthis.steeringQueueMode = options.steeringMode ?? \"one-at-a-time\";\n\t\tthis.followUpQueueMode = options.followUpMode ?? \"one-at-a-time\";\n\t}\n\n\tprivate getHandlers(type: string): Set<AgentHarnessHandler> | undefined {\n\t\treturn this.handlers.get(type);\n\t}\n\n\tprivate async emitOwn(event: AgentHarnessOwnEvent<TSkill, TPromptTemplate>, signal?: AbortSignal): Promise<void> {\n\t\tfor (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) {\n\t\t\ttry {\n\t\t\t\tawait listener(event, signal);\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async emitAny(event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal): Promise<void> {\n\t\tfor (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) {\n\t\t\ttry {\n\t\t\t\tawait listener(event, signal);\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async emitHook<TType extends keyof AgentHarnessEventResultMap>(\n\t\tevent: Extract<AgentHarnessOwnEvent, { type: TType }>,\n\t): Promise<AgentHarnessEventResultMap[TType] | undefined> {\n\t\tconst handlers = this.getHandlers(event.type as TType);\n\t\tif (!handlers || handlers.size === 0) return undefined;\n\t\tlet lastResult: AgentHarnessEventResultMap[TType] | undefined;\n\t\tfor (const handler of handlers) {\n\t\t\ttry {\n\t\t\t\tconst result = await handler(event);\n\t\t\t\tif (result !== undefined) {\n\t\t\t\t\tlastResult = result;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t\treturn lastResult;\n\t}\n\n\tprivate async emitBeforeProviderRequest(\n\t\tmodel: Model<any>,\n\t\tsessionId: string,\n\t\tstreamOptions: AgentHarnessStreamOptions,\n\t): Promise<AgentHarnessStreamOptions> {\n\t\tconst handlers = this.getHandlers(\"before_provider_request\");\n\t\tlet current = cloneStreamOptions(streamOptions);\n\t\tif (!handlers || handlers.size === 0) return current;\n\t\tfor (const handler of handlers) {\n\t\t\ttry {\n\t\t\t\tconst result = await handler({\n\t\t\t\t\ttype: \"before_provider_request\",\n\t\t\t\t\tmodel,\n\t\t\t\t\tsessionId,\n\t\t\t\t\tstreamOptions: cloneStreamOptions(current),\n\t\t\t\t});\n\t\t\t\tif (result?.streamOptions) {\n\t\t\t\t\tcurrent = applyStreamOptionsPatch(current, result.streamOptions);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t\treturn current;\n\t}\n\n\tprivate async emitBeforeProviderPayload(model: Model<any>, payload: unknown): Promise<unknown> {\n\t\tconst handlers = this.getHandlers(\"before_provider_payload\");\n\t\tlet current = payload;\n\t\tif (!handlers || handlers.size === 0) return current;\n\t\tfor (const handler of handlers) {\n\t\t\ttry {\n\t\t\t\tconst result = await handler({ type: \"before_provider_payload\", model, payload: current });\n\t\t\t\tif (result !== undefined) {\n\t\t\t\t\tcurrent = result.payload;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t\treturn current;\n\t}\n\n\tprivate async emitQueueUpdate(): Promise<void> {\n\t\tawait this.emitOwn({\n\t\t\ttype: \"queue_update\",\n\t\t\tsteer: [...this.steerQueue],\n\t\t\tfollowUp: [...this.followUpQueue],\n\t\t\tnextTurn: [...this.nextTurnQueue],\n\t\t});\n\t}\n\n\tprivate startRunPromise(): () => void {\n\t\tlet finish = () => {};\n\t\tthis.runPromise = new Promise<void>((resolve) => {\n\t\t\tfinish = resolve;\n\t\t});\n\t\treturn () => {\n\t\t\tthis.runPromise = undefined;\n\t\t\tfinish();\n\t\t};\n\t}\n\n\tprivate async createTurnState(): Promise<AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>> {\n\t\tconst context = await this.session.buildContext();\n\t\tconst resources = this.getResources();\n\t\tconst sessionMetadata = await this.session.getMetadata();\n\t\tconst tools = [...this.tools.values()];\n\t\tconst activeTools = this.activeToolNames\n\t\t\t.map((name) => this.tools.get(name))\n\t\t\t.filter((tool): tool is TTool => tool !== undefined);\n\t\tlet systemPrompt = \"You are a helpful assistant.\";\n\t\tif (typeof this.systemPrompt === \"string\") {\n\t\t\tsystemPrompt = this.systemPrompt;\n\t\t} else if (this.systemPrompt) {\n\t\t\tsystemPrompt = await this.systemPrompt({\n\t\t\t\tenv: this.env,\n\t\t\t\tsession: this.session,\n\t\t\t\tmodel: this.model,\n\t\t\t\tthinkingLevel: this.thinkingLevel,\n\t\t\t\tactiveTools,\n\t\t\t\tresources,\n\t\t\t});\n\t\t}\n\t\treturn {\n\t\t\tmessages: context.messages,\n\t\t\tresources,\n\t\t\tstreamOptions: cloneStreamOptions(this.streamOptions),\n\t\t\tsessionId: sessionMetadata.id,\n\t\t\tsystemPrompt,\n\t\t\tmodel: this.model,\n\t\t\tthinkingLevel: this.thinkingLevel,\n\t\t\ttools,\n\t\t\tactiveTools,\n\t\t};\n\t}\n\n\tprivate createContext(\n\t\tturnState: AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>,\n\t\tsystemPrompt?: string,\n\t): AgentContext {\n\t\treturn {\n\t\t\tsystemPrompt: systemPrompt ?? turnState.systemPrompt,\n\t\t\tmessages: turnState.messages.slice(),\n\t\t\ttools: turnState.activeTools.slice(),\n\t\t};\n\t}\n\n\tprivate createStreamFn(getTurnState: () => AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>): StreamFn {\n\t\treturn async (model, context, streamOptions) => {\n\t\t\tconst turnState = getTurnState();\n\t\t\tconst auth = await this.getApiKeyAndHeaders?.(model);\n\t\t\tconst snapshotOptions: AgentHarnessStreamOptions = {\n\t\t\t\t...turnState.streamOptions,\n\t\t\t\theaders: mergeHeaders(turnState.streamOptions.headers, auth?.headers),\n\t\t\t};\n\t\t\tconst requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\tcacheRetention: requestOptions.cacheRetention,\n\t\t\t\theaders: requestOptions.headers,\n\t\t\t\tmaxRetries: requestOptions.maxRetries,\n\t\t\t\tmaxRetryDelayMs: requestOptions.maxRetryDelayMs,\n\t\t\t\tmetadata: requestOptions.metadata,\n\t\t\t\tonPayload: async (payload) => await this.emitBeforeProviderPayload(model, payload),\n\t\t\t\tonResponse: async (response) => {\n\t\t\t\t\tconst headers = { ...(response.headers as Record<string, string>) };\n\t\t\t\t\tawait this.emitOwn(\n\t\t\t\t\t\t{ type: \"after_provider_response\", status: response.status, headers },\n\t\t\t\t\t\tstreamOptions?.signal,\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\treasoning: streamOptions?.reasoning,\n\t\t\t\tsignal: streamOptions?.signal,\n\t\t\t\tsessionId: turnState.sessionId,\n\t\t\t\ttimeoutMs: requestOptions.timeoutMs,\n\t\t\t\ttransport: requestOptions.transport,\n\t\t\t\tapiKey: auth?.apiKey,\n\t\t\t});\n\t\t};\n\t}\n\n\tprivate async drainQueuedMessages(queue: AgentMessage[], mode: QueueMode): Promise<AgentMessage[]> {\n\t\tconst messages = mode === \"all\" ? queue.splice(0) : queue.splice(0, 1);\n\t\tif (messages.length === 0) return messages;\n\t\ttry {\n\t\t\tawait this.emitQueueUpdate();\n\t\t\treturn messages;\n\t\t} catch (error) {\n\t\t\tqueue.unshift(...messages);\n\t\t\tthrow normalizeHookError(error);\n\t\t}\n\t}\n\n\tprivate createLoopConfig(\n\t\tgetTurnState: () => AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>,\n\t\tsetTurnState: (turnState: AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>) => void,\n\t): AgentLoopConfig {\n\t\tconst turnState = getTurnState();\n\t\treturn {\n\t\t\tmodel: turnState.model,\n\t\t\treasoning: turnState.thinkingLevel === \"off\" ? undefined : turnState.thinkingLevel,\n\t\t\tconvertToLlm,\n\t\t\ttransformContext: async (messages) => {\n\t\t\t\tconst result = await this.emitHook({ type: \"context\", messages: [...messages] });\n\t\t\t\treturn result?.messages ?? messages;\n\t\t\t},\n\t\t\tbeforeToolCall: async ({ toolCall, args }) => {\n\t\t\t\tconst result = await this.emitHook({\n\t\t\t\t\ttype: \"tool_call\",\n\t\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\t\ttoolName: toolCall.name,\n\t\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\t});\n\t\t\t\treturn result ? { block: result.block, reason: result.reason } : undefined;\n\t\t\t},\n\t\t\tafterToolCall: async ({ toolCall, args, result, isError }) => {\n\t\t\t\tconst patch = await this.emitHook({\n\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\t\ttoolName: toolCall.name,\n\t\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\t\tcontent: result.content,\n\t\t\t\t\tdetails: result.details,\n\t\t\t\t\tisError,\n\t\t\t\t});\n\t\t\t\treturn patch\n\t\t\t\t\t? { content: patch.content, details: patch.details, isError: patch.isError, terminate: patch.terminate }\n\t\t\t\t\t: undefined;\n\t\t\t},\n\t\t\tprepareNextTurn: async () => {\n\t\t\t\tawait this.flushPendingSessionWrites();\n\t\t\t\tconst nextTurnState = await this.createTurnState();\n\t\t\t\tsetTurnState(nextTurnState);\n\t\t\t\treturn {\n\t\t\t\t\tcontext: this.createContext(nextTurnState),\n\t\t\t\t\tmodel: nextTurnState.model,\n\t\t\t\t\tthinkingLevel: nextTurnState.thinkingLevel,\n\t\t\t\t};\n\t\t\t},\n\t\t\tgetSteeringMessages: async () => this.drainQueuedMessages(this.steerQueue, this.steeringQueueMode),\n\t\t\tgetFollowUpMessages: async () => this.drainQueuedMessages(this.followUpQueue, this.followUpQueueMode),\n\t\t};\n\t}\n\n\tprivate validateToolNames(toolNames: string[], tools: Map<string, TTool> = this.tools): void {\n\t\tconst missing = toolNames.filter((name) => !tools.has(name));\n\t\tif (missing.length > 0) throw new AgentHarnessError(\"invalid_argument\", `Unknown tool(s): ${missing.join(\", \")}`);\n\t}\n\n\tprivate async flushPendingSessionWrites(): Promise<void> {\n\t\twhile (this.pendingSessionWrites.length > 0) {\n\t\t\tconst write = this.pendingSessionWrites[0]!;\n\t\t\tif (write.type === \"message\") {\n\t\t\t\tawait this.session.appendMessage(write.message);\n\t\t\t} else if (write.type === \"model_change\") {\n\t\t\t\tawait this.session.appendModelChange(write.provider, write.modelId);\n\t\t\t} else if (write.type === \"thinking_level_change\") {\n\t\t\t\tawait this.session.appendThinkingLevelChange(write.thinkingLevel);\n\t\t\t} else if (write.type === \"custom\") {\n\t\t\t\tawait this.session.appendCustomEntry(write.customType, write.data);\n\t\t\t} else if (write.type === \"custom_message\") {\n\t\t\t\tawait this.session.appendCustomMessageEntry(write.customType, write.content, write.display, write.details);\n\t\t\t} else if (write.type === \"label\") {\n\t\t\t\tawait this.session.appendLabel(write.targetId, write.label);\n\t\t\t} else if (write.type === \"session_info\") {\n\t\t\t\tawait this.session.appendSessionName(write.name ?? \"\");\n\t\t\t} else if (write.type === \"leaf\") {\n\t\t\t\tawait this.session.getStorage().setLeafId(write.targetId);\n\t\t\t}\n\t\t\tthis.pendingSessionWrites.shift();\n\t\t}\n\t}\n\n\tprivate async handleAgentEvent(event: AgentEvent, signal?: AbortSignal): Promise<void> {\n\t\tif (event.type === \"message_end\") {\n\t\t\tawait this.session.appendMessage(event.message);\n\t\t\tawait this.emitAny(event, signal);\n\t\t\treturn;\n\t\t}\n\t\tif (event.type === \"turn_end\") {\n\t\t\tlet eventError: unknown;\n\t\t\ttry {\n\t\t\t\tawait this.emitAny(event, signal);\n\t\t\t} catch (error) {\n\t\t\t\teventError = error;\n\t\t\t}\n\t\t\tconst hadPendingMutations = this.pendingSessionWrites.length > 0;\n\t\t\tawait this.flushPendingSessionWrites();\n\t\t\tif (eventError) throw eventError;\n\t\t\tawait this.emitOwn({ type: \"save_point\", hadPendingMutations });\n\t\t\treturn;\n\t\t}\n\t\tif (event.type === \"agent_end\") {\n\t\t\tawait this.flushPendingSessionWrites();\n\t\t\tthis.phase = \"idle\";\n\t\t\tawait this.emitAny(event, signal);\n\t\t\tawait this.emitOwn({ type: \"settled\", nextTurnCount: this.nextTurnQueue.length }, signal);\n\t\t\treturn;\n\t\t}\n\t\tawait this.emitAny(event, signal);\n\t}\n\n\tprivate async emitRunFailure(\n\t\tmodel: Model<any>,\n\t\terror: unknown,\n\t\taborted: boolean,\n\t\tsignal: AbortSignal,\n\t): Promise<AgentMessage[]> {\n\t\tconst failureMessage = createFailureMessage(model, error, aborted);\n\t\tawait this.handleAgentEvent({ type: \"message_start\", message: failureMessage }, signal);\n\t\tawait this.handleAgentEvent({ type: \"message_end\", message: failureMessage }, signal);\n\t\tawait this.handleAgentEvent({ type: \"turn_end\", message: failureMessage, toolResults: [] }, signal);\n\t\tawait this.handleAgentEvent({ type: \"agent_end\", messages: [failureMessage] }, signal);\n\t\treturn [failureMessage];\n\t}\n\n\tprivate async executeTurn(\n\t\tturnState: AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>,\n\t\ttext: string,\n\t\toptions?: { images?: ImageContent[] },\n\t): Promise<AssistantMessage> {\n\t\tlet activeTurnState = turnState;\n\t\tlet messages: AgentMessage[] = [createUserMessage(text, options?.images)];\n\t\tif (this.nextTurnQueue.length > 0) {\n\t\t\tconst queuedMessages = this.nextTurnQueue.splice(0);\n\t\t\ttry {\n\t\t\t\tawait this.emitQueueUpdate();\n\t\t\t} catch (error) {\n\t\t\t\tthis.nextTurnQueue.unshift(...queuedMessages);\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t\tmessages = [...queuedMessages, messages[0]!];\n\t\t}\n\t\tconst beforeResult = await this.emitHook({\n\t\t\ttype: \"before_agent_start\",\n\t\t\tprompt: text,\n\t\t\timages: options?.images,\n\t\t\tsystemPrompt: turnState.systemPrompt,\n\t\t\tresources: turnState.resources,\n\t\t});\n\t\tif (beforeResult?.messages) messages = [...messages, ...beforeResult.messages];\n\n\t\tconst abortController = new AbortController();\n\t\tconst getTurnState = () => activeTurnState;\n\t\tconst setTurnState = (nextTurnState: AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>) => {\n\t\t\tactiveTurnState = nextTurnState;\n\t\t};\n\t\tthis.runAbortController = abortController;\n\t\tconst runResultPromise = (async () => {\n\t\t\ttry {\n\t\t\t\treturn await runAgentLoop(\n\t\t\t\t\tmessages,\n\t\t\t\t\tthis.createContext(turnState, beforeResult?.systemPrompt),\n\t\t\t\t\tthis.createLoopConfig(getTurnState, setTurnState),\n\t\t\t\t\t(event) => this.handleAgentEvent(event, abortController.signal),\n\t\t\t\t\tabortController.signal,\n\t\t\t\t\tthis.createStreamFn(getTurnState),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await this.emitRunFailure(\n\t\t\t\t\t\tactiveTurnState.model,\n\t\t\t\t\t\terror,\n\t\t\t\t\t\tabortController.signal.aborted,\n\t\t\t\t\t\tabortController.signal,\n\t\t\t\t\t);\n\t\t\t\t} catch (failureError) {\n\t\t\t\t\tconst cause = new AggregateError(\n\t\t\t\t\t\t[toError(error), toError(failureError)],\n\t\t\t\t\t\t\"Agent run failed and failure reporting failed\",\n\t\t\t\t\t);\n\t\t\t\t\tthrow new AgentHarnessError(\"unknown\", cause.message, cause);\n\t\t\t\t}\n\t\t\t}\n\t\t})();\n\t\ttry {\n\t\t\tconst newMessages = await runResultPromise;\n\t\t\tfor (let i = newMessages.length - 1; i >= 0; i--) {\n\t\t\t\tconst message = newMessages[i]!;\n\t\t\t\tif (message.role === \"assistant\") {\n\t\t\t\t\treturn message;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", \"AgentHarness prompt completed without an assistant message\");\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tawait this.flushPendingSessionWrites();\n\t\t\t} finally {\n\t\t\t\tthis.runAbortController = undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync prompt(text: string, options?: { images?: ImageContent[] }): Promise<AssistantMessage> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"AgentHarness is busy\");\n\t\tthis.phase = \"turn\";\n\t\tconst finishRunPromise = this.startRunPromise();\n\t\ttry {\n\t\t\tconst turnState = await this.createTurnState();\n\t\t\treturn await this.executeTurn(turnState, text, options);\n\t\t} catch (error) {\n\t\t\tthis.phase = \"idle\";\n\t\t\tthrow normalizeHarnessError(error, \"unknown\");\n\t\t} finally {\n\t\t\tfinishRunPromise();\n\t\t}\n\t}\n\n\tasync skill(name: string, additionalInstructions?: string): Promise<AssistantMessage> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"AgentHarness is busy\");\n\t\tthis.phase = \"turn\";\n\t\tconst finishRunPromise = this.startRunPromise();\n\t\ttry {\n\t\t\tconst turnState = await this.createTurnState();\n\t\t\tconst skill = (turnState.resources.skills ?? []).find((candidate) => candidate.name === name);\n\t\t\tif (!skill) throw new AgentHarnessError(\"invalid_argument\", `Unknown skill: ${name}`);\n\t\t\treturn await this.executeTurn(turnState, formatSkillInvocation(skill, additionalInstructions));\n\t\t} catch (error) {\n\t\t\tthis.phase = \"idle\";\n\t\t\tthrow normalizeHarnessError(error, \"unknown\");\n\t\t} finally {\n\t\t\tfinishRunPromise();\n\t\t}\n\t}\n\n\tasync promptFromTemplate(name: string, args: string[] = []): Promise<AssistantMessage> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"AgentHarness is busy\");\n\t\tthis.phase = \"turn\";\n\t\tconst finishRunPromise = this.startRunPromise();\n\t\ttry {\n\t\t\tconst turnState = await this.createTurnState();\n\t\t\tconst template = (turnState.resources.promptTemplates ?? []).find((candidate) => candidate.name === name);\n\t\t\tif (!template) throw new AgentHarnessError(\"invalid_argument\", `Unknown prompt template: ${name}`);\n\t\t\treturn await this.executeTurn(turnState, formatPromptTemplateInvocation(template, args));\n\t\t} catch (error) {\n\t\t\tthis.phase = \"idle\";\n\t\t\tthrow normalizeHarnessError(error, \"unknown\");\n\t\t} finally {\n\t\t\tfinishRunPromise();\n\t\t}\n\t}\n\n\tasync steer(text: string, options?: { images?: ImageContent[] }): Promise<void> {\n\t\tif (this.phase === \"idle\") throw new AgentHarnessError(\"invalid_state\", \"Cannot steer while idle\");\n\t\tthis.steerQueue.push(createUserMessage(text, options?.images));\n\t\tawait this.emitQueueUpdate();\n\t}\n\n\tasync followUp(text: string, options?: { images?: ImageContent[] }): Promise<void> {\n\t\tif (this.phase === \"idle\") throw new AgentHarnessError(\"invalid_state\", \"Cannot follow up while idle\");\n\t\tthis.followUpQueue.push(createUserMessage(text, options?.images));\n\t\tawait this.emitQueueUpdate();\n\t}\n\n\tasync nextTurn(text: string, options?: { images?: ImageContent[] }): Promise<void> {\n\t\tthis.nextTurnQueue.push(createUserMessage(text, options?.images));\n\t\tawait this.emitQueueUpdate();\n\t}\n\n\tasync appendMessage(message: AgentMessage): Promise<void> {\n\t\ttry {\n\t\t\tif (this.phase === \"idle\") {\n\t\t\t\tawait this.session.appendMessage(message);\n\t\t\t} else {\n\t\t\t\tthis.pendingSessionWrites.push({ type: \"message\", message });\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"session\");\n\t\t}\n\t}\n\n\tasync compact(\n\t\tcustomInstructions?: string,\n\t): Promise<{ summary: string; firstKeptEntryId: string; tokensBefore: number; details?: unknown }> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"compact() requires idle harness\");\n\t\tthis.phase = \"compaction\";\n\t\ttry {\n\t\t\tconst model = this.model;\n\t\t\tif (!model) throw new AgentHarnessError(\"invalid_state\", \"No model set for compaction\");\n\t\t\tconst auth = await this.getApiKeyAndHeaders?.(model);\n\t\t\tif (!auth) throw new AgentHarnessError(\"auth\", \"No auth available for compaction\");\n\t\t\tconst branchEntries = await this.session.getBranch();\n\t\t\tconst preparationResult = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS);\n\t\t\tif (!preparationResult.ok) throw preparationResult.error;\n\t\t\tconst preparation = preparationResult.value;\n\t\t\tif (!preparation) throw new AgentHarnessError(\"compaction\", \"Nothing to compact\");\n\t\t\tconst hookResult = await this.emitHook({\n\t\t\t\ttype: \"session_before_compact\",\n\t\t\t\tpreparation,\n\t\t\t\tbranchEntries,\n\t\t\t\tcustomInstructions,\n\t\t\t\tsignal: new AbortController().signal,\n\t\t\t});\n\t\t\tif (hookResult?.cancel) throw new AgentHarnessError(\"compaction\", \"Compaction cancelled\");\n\t\t\tconst provided = hookResult?.compaction;\n\t\t\tconst compactResult = provided\n\t\t\t\t? { ok: true as const, value: provided }\n\t\t\t\t: await compact(\n\t\t\t\t\t\tpreparation,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tauth.apiKey,\n\t\t\t\t\t\tauth.headers,\n\t\t\t\t\t\tcustomInstructions,\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tthis.thinkingLevel,\n\t\t\t\t\t);\n\t\t\tif (!compactResult.ok) throw compactResult.error;\n\t\t\tconst result = compactResult.value;\n\t\t\tconst entryId = await this.session.appendCompaction(\n\t\t\t\tresult.summary,\n\t\t\t\tresult.firstKeptEntryId,\n\t\t\t\tresult.tokensBefore,\n\t\t\t\tresult.details,\n\t\t\t\tprovided !== undefined,\n\t\t\t);\n\t\t\tconst entry = await this.session.getEntry(entryId);\n\t\t\tif (entry?.type === \"compaction\") {\n\t\t\t\tawait this.emitOwn({ type: \"session_compact\", compactionEntry: entry, fromHook: provided !== undefined });\n\t\t\t}\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"compaction\");\n\t\t} finally {\n\t\t\tthis.phase = \"idle\";\n\t\t}\n\t}\n\n\tasync navigateTree(\n\t\ttargetId: string,\n\t\toptions?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },\n\t): Promise<NavigateTreeResult> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"navigateTree() requires idle harness\");\n\t\tthis.phase = \"branch_summary\";\n\t\ttry {\n\t\t\tconst oldLeafId = await this.session.getLeafId();\n\t\t\tif (oldLeafId === targetId) return { cancelled: false };\n\t\t\tconst targetEntry = await this.session.getEntry(targetId);\n\t\t\tif (!targetEntry) throw new AgentHarnessError(\"invalid_argument\", `Entry ${targetId} not found`);\n\t\t\tconst { entries, commonAncestorId } = await collectEntriesForBranchSummary(this.session, oldLeafId, targetId);\n\t\t\tconst preparation = {\n\t\t\t\ttargetId,\n\t\t\t\toldLeafId,\n\t\t\t\tcommonAncestorId,\n\t\t\t\tentriesToSummarize: entries,\n\t\t\t\tuserWantsSummary: options?.summarize ?? false,\n\t\t\t\tcustomInstructions: options?.customInstructions,\n\t\t\t\treplaceInstructions: options?.replaceInstructions,\n\t\t\t\tlabel: options?.label,\n\t\t\t};\n\t\t\tconst signal = new AbortController().signal;\n\t\t\tconst hookResult = await this.emitHook({ type: \"session_before_tree\", preparation, signal });\n\t\t\tif (hookResult?.cancel) return { cancelled: true };\n\t\t\tlet summaryEntry: NavigateTreeResult[\"summaryEntry\"];\n\t\t\tlet summaryText: string | undefined = hookResult?.summary?.summary;\n\t\t\tlet summaryDetails: unknown = hookResult?.summary?.details;\n\t\t\tif (!summaryText && options?.summarize && entries.length > 0) {\n\t\t\t\tconst model = this.model;\n\t\t\t\tif (!model) throw new AgentHarnessError(\"invalid_state\", \"No model set for branch summary\");\n\t\t\t\tconst auth = await this.getApiKeyAndHeaders?.(model);\n\t\t\t\tif (!auth) throw new AgentHarnessError(\"auth\", \"No auth available for branch summary\");\n\t\t\t\tconst branchSummary = await generateBranchSummary(entries, {\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\t\theaders: auth.headers,\n\t\t\t\t\tsignal: new AbortController().signal,\n\t\t\t\t\tcustomInstructions: hookResult?.customInstructions ?? options?.customInstructions,\n\t\t\t\t\treplaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions,\n\t\t\t\t});\n\t\t\t\tif (!branchSummary.ok) {\n\t\t\t\t\tif (branchSummary.error.code === \"aborted\") return { cancelled: true };\n\t\t\t\t\tthrow new AgentHarnessError(\"branch_summary\", branchSummary.error.message, branchSummary.error);\n\t\t\t\t}\n\t\t\t\tsummaryText = branchSummary.value.summary;\n\t\t\t\tsummaryDetails = {\n\t\t\t\t\treadFiles: branchSummary.value.readFiles,\n\t\t\t\t\tmodifiedFiles: branchSummary.value.modifiedFiles,\n\t\t\t\t};\n\t\t\t}\n\t\t\tlet editorText: string | undefined;\n\t\t\tlet newLeafId: string | null;\n\t\t\tif (targetEntry.type === \"message\" && targetEntry.message.role === \"user\") {\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\tconst content = targetEntry.message.content;\n\t\t\t\teditorText =\n\t\t\t\t\ttypeof content === \"string\"\n\t\t\t\t\t\t? content\n\t\t\t\t\t\t: content\n\t\t\t\t\t\t\t\t.filter((c): c is { readonly type: \"text\"; readonly text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t\t.join(\"\");\n\t\t\t} else if (targetEntry.type === \"custom_message\") {\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\teditorText =\n\t\t\t\t\ttypeof targetEntry.content === \"string\"\n\t\t\t\t\t\t? targetEntry.content\n\t\t\t\t\t\t: targetEntry.content\n\t\t\t\t\t\t\t\t.filter((c): c is { readonly type: \"text\"; readonly text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t\t.join(\"\");\n\t\t\t} else {\n\t\t\t\tnewLeafId = targetId;\n\t\t\t}\n\t\t\tconst summaryId = await this.session.moveTo(\n\t\t\t\tnewLeafId,\n\t\t\t\tsummaryText\n\t\t\t\t\t? { summary: summaryText, details: summaryDetails, fromHook: hookResult?.summary !== undefined }\n\t\t\t\t\t: undefined,\n\t\t\t);\n\t\t\tif (summaryId) {\n\t\t\t\tconst entry = await this.session.getEntry(summaryId);\n\t\t\t\tif (entry?.type === \"branch_summary\") summaryEntry = entry;\n\t\t\t}\n\t\t\tawait this.emitOwn({\n\t\t\t\ttype: \"session_tree\",\n\t\t\t\tnewLeafId: await this.session.getLeafId(),\n\t\t\t\toldLeafId,\n\t\t\t\tsummaryEntry,\n\t\t\t\tfromHook: hookResult?.summary !== undefined,\n\t\t\t});\n\t\t\treturn { cancelled: false, editorText, summaryEntry };\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"branch_summary\");\n\t\t} finally {\n\t\t\tthis.phase = \"idle\";\n\t\t}\n\t}\n\n\tgetModel(): Model<any> {\n\t\treturn this.model;\n\t}\n\n\tgetThinkingLevel(): ThinkingLevel {\n\t\treturn this.thinkingLevel;\n\t}\n\n\tasync setModel(model: Model<any>): Promise<void> {\n\t\ttry {\n\t\t\tconst previousModel = this.model;\n\t\t\tif (this.phase === \"idle\") {\n\t\t\t\tawait this.session.appendModelChange(model.provider, model.id);\n\t\t\t} else {\n\t\t\t\tthis.pendingSessionWrites.push({ type: \"model_change\", provider: model.provider, modelId: model.id });\n\t\t\t}\n\t\t\tthis.model = model;\n\t\t\tawait this.emitOwn({ type: \"model_select\", model, previousModel, source: \"set\" });\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"session\");\n\t\t}\n\t}\n\n\tasync setThinkingLevel(level: ThinkingLevel): Promise<void> {\n\t\ttry {\n\t\t\tconst previousLevel = this.thinkingLevel;\n\t\t\tif (this.phase === \"idle\") {\n\t\t\t\tawait this.session.appendThinkingLevelChange(level);\n\t\t\t} else {\n\t\t\t\tthis.pendingSessionWrites.push({ type: \"thinking_level_change\", thinkingLevel: level });\n\t\t\t}\n\t\t\tthis.thinkingLevel = level;\n\t\t\tawait this.emitOwn({ type: \"thinking_level_select\", level, previousLevel });\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"session\");\n\t\t}\n\t}\n\n\tasync setActiveTools(toolNames: string[]): Promise<void> {\n\t\ttry {\n\t\t\tthis.validateToolNames(toolNames);\n\t\t\tthis.activeToolNames = [...toolNames];\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"invalid_argument\");\n\t\t}\n\t}\n\n\tgetSteeringMode(): QueueMode {\n\t\treturn this.steeringQueueMode;\n\t}\n\n\tasync setSteeringMode(mode: QueueMode): Promise<void> {\n\t\tthis.steeringQueueMode = mode;\n\t}\n\n\tgetFollowUpMode(): QueueMode {\n\t\treturn this.followUpQueueMode;\n\t}\n\n\tasync setFollowUpMode(mode: QueueMode): Promise<void> {\n\t\tthis.followUpQueueMode = mode;\n\t}\n\n\tgetResources(): AgentHarnessResources<TSkill, TPromptTemplate> {\n\t\treturn {\n\t\t\tskills: this.resources.skills?.slice(),\n\t\t\tpromptTemplates: this.resources.promptTemplates?.slice(),\n\t\t};\n\t}\n\n\tasync setResources(resources: AgentHarnessResources<TSkill, TPromptTemplate>): Promise<void> {\n\t\tconst previousResources = this.getResources();\n\t\tthis.resources = {\n\t\t\tskills: resources.skills?.slice(),\n\t\t\tpromptTemplates: resources.promptTemplates?.slice(),\n\t\t};\n\t\tawait this.emitOwn({ type: \"resources_update\", resources: this.getResources(), previousResources });\n\t}\n\n\tgetStreamOptions(): AgentHarnessStreamOptions {\n\t\treturn cloneStreamOptions(this.streamOptions);\n\t}\n\n\tasync setStreamOptions(streamOptions: AgentHarnessStreamOptions): Promise<void> {\n\t\tthis.streamOptions = cloneStreamOptions(streamOptions);\n\t}\n\n\tasync setTools(tools: TTool[], activeToolNames?: string[]): Promise<void> {\n\t\ttry {\n\t\t\tconst nextTools = new Map(tools.map((tool) => [tool.name, tool]));\n\t\t\tconst nextActiveToolNames = activeToolNames ? [...activeToolNames] : this.activeToolNames;\n\t\t\tthis.validateToolNames(nextActiveToolNames, nextTools);\n\t\t\tthis.tools = nextTools;\n\t\t\tthis.activeToolNames = [...nextActiveToolNames];\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"invalid_argument\");\n\t\t}\n\t}\n\n\tasync abort(): Promise<AbortResult> {\n\t\tconst clearedSteer = [...this.steerQueue];\n\t\tconst clearedFollowUp = [...this.followUpQueue];\n\t\tthis.steerQueue = [];\n\t\tthis.followUpQueue = [];\n\t\tthis.runAbortController?.abort();\n\t\tconst errors: Error[] = [];\n\t\ttry {\n\t\t\tawait this.emitQueueUpdate();\n\t\t} catch (error) {\n\t\t\terrors.push(toError(error));\n\t\t}\n\t\ttry {\n\t\t\tawait this.waitForIdle();\n\t\t} catch (error) {\n\t\t\terrors.push(toError(error));\n\t\t}\n\t\ttry {\n\t\t\tawait this.emitOwn({ type: \"abort\", clearedSteer, clearedFollowUp });\n\t\t} catch (error) {\n\t\t\terrors.push(toError(error));\n\t\t}\n\t\tif (errors.length > 0) {\n\t\t\tconst cause = errors.length === 1 ? errors[0]! : new AggregateError(errors, \"Abort completed with errors\");\n\t\t\tthrow normalizeHarnessError(cause, \"hook\");\n\t\t}\n\t\treturn { clearedSteer, clearedFollowUp };\n\t}\n\n\tasync waitForIdle(): Promise<void> {\n\t\tawait this.runPromise;\n\t}\n\n\tsubscribe(\n\t\tlistener: (event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal) => Promise<void> | void,\n\t): () => void {\n\t\tlet handlers = this.handlers.get(SUBSCRIBER_EVENT_TYPE);\n\t\tif (!handlers) {\n\t\t\thandlers = new Set();\n\t\t\tthis.handlers.set(SUBSCRIBER_EVENT_TYPE, handlers);\n\t\t}\n\t\thandlers.add(listener as AgentHarnessHandler);\n\t\treturn () => handlers!.delete(listener as AgentHarnessHandler);\n\t}\n\n\ton<TType extends keyof AgentHarnessEventResultMap>(\n\t\ttype: TType,\n\t\thandler: (\n\t\t\tevent: Extract<AgentHarnessOwnEvent, { type: TType }>,\n\t\t) => Promise<AgentHarnessEventResultMap[TType]> | AgentHarnessEventResultMap[TType],\n\t): () => void {\n\t\tlet handlers = this.handlers.get(type);\n\t\tif (!handlers) {\n\t\t\thandlers = new Set();\n\t\t\tthis.handlers.set(type, handlers);\n\t\t}\n\t\thandlers.add(handler as AgentHarnessHandler);\n\t\treturn () => handlers!.delete(handler as AgentHarnessHandler);\n\t}\n}\n"]}
1
+ {"version":3,"file":"agent-harness.d.ts","sourceRoot":"","sources":["../../src/harness/agent-harness.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype AssistantMessage,\n\ttype ImageContent,\n\ttype Model,\n\tstreamSimple,\n\ttype UserMessage,\n} from \"@earendil-works/pi-ai\";\nimport { runAgentLoop } from \"../agent-loop.ts\";\nimport type {\n\tAgentContext,\n\tAgentEvent,\n\tAgentLoopConfig,\n\tAgentMessage,\n\tAgentTool,\n\tQueueMode,\n\tStreamFn,\n\tThinkingLevel,\n} from \"../types.ts\";\nimport { collectEntriesForBranchSummary, generateBranchSummary } from \"./compaction/branch-summarization.ts\";\nimport { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from \"./compaction/compaction.ts\";\nimport { convertToLlm } from \"./messages.ts\";\nimport { formatPromptTemplateInvocation } from \"./prompt-templates.ts\";\nimport { formatSkillInvocation } from \"./skills.ts\";\nimport type {\n\tAbortResult,\n\tAgentHarnessEvent,\n\tAgentHarnessEventResultMap,\n\tAgentHarnessOptions,\n\tAgentHarnessOwnEvent,\n\tAgentHarnessPhase,\n\tAgentHarnessResources,\n\tAgentHarnessStreamOptions,\n\tAgentHarnessStreamOptionsPatch,\n\tExecutionEnv,\n\tNavigateTreeResult,\n\tPendingSessionWrite,\n\tPromptTemplate,\n\tSession,\n\tSkill,\n} from \"./types.ts\";\nimport { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from \"./types.ts\";\n\nfunction createUserMessage(text: string, images?: ImageContent[]): UserMessage {\n\tconst content: Array<{ type: \"text\"; text: string } | ImageContent> = [{ type: \"text\", text }];\n\tif (images) content.push(...images);\n\treturn { role: \"user\", content, timestamp: Date.now() };\n}\n\nfunction createFailureMessage(model: Model<any>, error: unknown, aborted: boolean): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [{ type: \"text\", text: \"\" }],\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tstopReason: aborted ? \"aborted\" : \"error\",\n\t\terrorMessage: error instanceof Error ? error.message : String(error),\n\t\ttimestamp: Date.now(),\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t};\n}\n\nfunction cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHarnessStreamOptions {\n\treturn {\n\t\t...streamOptions,\n\t\theaders: streamOptions?.headers ? { ...streamOptions.headers } : undefined,\n\t\tmetadata: streamOptions?.metadata ? { ...streamOptions.metadata } : undefined,\n\t};\n}\n\nfunction mergeHeaders(...headers: Array<Record<string, string> | undefined>): Record<string, string> | undefined {\n\tconst merged: Record<string, string> = {};\n\tlet hasHeaders = false;\n\tfor (const entry of headers) {\n\t\tif (!entry) continue;\n\t\tObject.assign(merged, entry);\n\t\thasHeaders = true;\n\t}\n\treturn hasHeaders ? merged : undefined;\n}\n\nfunction findDuplicateNames(names: string[]): string[] {\n\tconst seen = new Set<string>();\n\tconst duplicates = new Set<string>();\n\tfor (const name of names) {\n\t\tif (seen.has(name)) duplicates.add(name);\n\t\tseen.add(name);\n\t}\n\treturn [...duplicates];\n}\n\nfunction applyStreamOptionsPatch(\n\tbase: AgentHarnessStreamOptions,\n\tpatch?: AgentHarnessStreamOptionsPatch,\n): AgentHarnessStreamOptions {\n\tconst result = cloneStreamOptions(base);\n\tif (!patch) return result;\n\n\tif (Object.hasOwn(patch, \"transport\")) result.transport = patch.transport;\n\tif (Object.hasOwn(patch, \"timeoutMs\")) result.timeoutMs = patch.timeoutMs;\n\tif (Object.hasOwn(patch, \"maxRetries\")) result.maxRetries = patch.maxRetries;\n\tif (Object.hasOwn(patch, \"maxRetryDelayMs\")) result.maxRetryDelayMs = patch.maxRetryDelayMs;\n\tif (Object.hasOwn(patch, \"cacheRetention\")) result.cacheRetention = patch.cacheRetention;\n\n\tif (Object.hasOwn(patch, \"headers\")) {\n\t\tif (patch.headers === undefined) {\n\t\t\tresult.headers = undefined;\n\t\t} else {\n\t\t\tconst headers = { ...(result.headers ?? {}) };\n\t\t\tfor (const [key, value] of Object.entries(patch.headers)) {\n\t\t\t\tif (value === undefined) delete headers[key];\n\t\t\t\telse headers[key] = value;\n\t\t\t}\n\t\t\tresult.headers = Object.keys(headers).length > 0 ? headers : undefined;\n\t\t}\n\t}\n\n\tif (Object.hasOwn(patch, \"metadata\")) {\n\t\tif (patch.metadata === undefined) {\n\t\t\tresult.metadata = undefined;\n\t\t} else {\n\t\t\tconst metadata = { ...(result.metadata ?? {}) };\n\t\t\tfor (const [key, value] of Object.entries(patch.metadata)) {\n\t\t\t\tif (value === undefined) delete metadata[key];\n\t\t\t\telse metadata[key] = value;\n\t\t\t}\n\t\t\tresult.metadata = Object.keys(metadata).length > 0 ? metadata : undefined;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nconst SUBSCRIBER_EVENT_TYPE = \"*\";\n\ntype AgentHarnessHandler = (event: any, signal?: AbortSignal) => Promise<any> | any;\n\nfunction normalizeHarnessError(error: unknown, fallbackCode: AgentHarnessError[\"code\"]): AgentHarnessError {\n\tif (error instanceof AgentHarnessError) return error;\n\tconst cause = toError(error);\n\tif (cause instanceof SessionError) return new AgentHarnessError(\"session\", cause.message, cause);\n\tif (cause instanceof CompactionError) return new AgentHarnessError(\"compaction\", cause.message, cause);\n\tif (cause instanceof BranchSummaryError) return new AgentHarnessError(\"branch_summary\", cause.message, cause);\n\treturn new AgentHarnessError(fallbackCode, cause.message, cause);\n}\n\nfunction normalizeHookError(error: unknown): AgentHarnessError {\n\treturn normalizeHarnessError(error, \"hook\");\n}\n\ninterface AgentHarnessTurnState<\n\tTSkill extends Skill = Skill,\n\tTPromptTemplate extends PromptTemplate = PromptTemplate,\n\tTTool extends AgentTool = AgentTool,\n> {\n\tmessages: AgentMessage[];\n\tresources: AgentHarnessResources<TSkill, TPromptTemplate>;\n\tstreamOptions: AgentHarnessStreamOptions;\n\tsessionId: string;\n\tsystemPrompt: string;\n\tmodel: Model<any>;\n\tthinkingLevel: ThinkingLevel;\n\ttools: TTool[];\n\tactiveTools: TTool[];\n}\n\nexport class AgentHarness<\n\tTSkill extends Skill = Skill,\n\tTPromptTemplate extends PromptTemplate = PromptTemplate,\n\tTTool extends AgentTool = AgentTool,\n> {\n\treadonly env: ExecutionEnv;\n\tprivate session: Session;\n\tprivate phase: AgentHarnessPhase = \"idle\";\n\tprivate runAbortController?: AbortController;\n\tprivate runPromise?: Promise<void>;\n\tprivate pendingSessionWrites: PendingSessionWrite[] = [];\n\tprivate model: Model<any>;\n\tprivate thinkingLevel: ThinkingLevel;\n\tprivate systemPrompt: AgentHarnessOptions<TSkill, TPromptTemplate, TTool>[\"systemPrompt\"];\n\tprivate streamOptions: AgentHarnessStreamOptions;\n\tprivate getApiKeyAndHeaders?: AgentHarnessOptions[\"getApiKeyAndHeaders\"];\n\tprivate resources: AgentHarnessResources<TSkill, TPromptTemplate>;\n\tprivate tools = new Map<string, TTool>();\n\tprivate activeToolNames: string[];\n\tprivate steerQueue: UserMessage[] = [];\n\tprivate steeringQueueMode: QueueMode;\n\tprivate followUpQueue: UserMessage[] = [];\n\tprivate followUpQueueMode: QueueMode;\n\tprivate nextTurnQueue: AgentMessage[] = [];\n\tprivate handlers = new Map<string, Set<AgentHarnessHandler>>();\n\n\tconstructor(options: AgentHarnessOptions<TSkill, TPromptTemplate, TTool>) {\n\t\tthis.env = options.env;\n\t\tthis.session = options.session;\n\t\tthis.resources = options.resources ?? {};\n\t\tthis.streamOptions = cloneStreamOptions(options.streamOptions);\n\t\tthis.systemPrompt = options.systemPrompt;\n\t\tthis.getApiKeyAndHeaders = options.getApiKeyAndHeaders;\n\t\tthis.validateUniqueNames(\n\t\t\t(options.tools ?? []).map((tool) => tool.name),\n\t\t\t\"Duplicate tool name(s)\",\n\t\t);\n\t\tfor (const tool of options.tools ?? []) {\n\t\t\tthis.tools.set(tool.name, tool);\n\t\t}\n\t\tthis.model = options.model;\n\t\tthis.thinkingLevel = options.thinkingLevel ?? \"off\";\n\t\tthis.activeToolNames = options.activeToolNames\n\t\t\t? [...options.activeToolNames]\n\t\t\t: (options.tools ?? []).map((tool) => tool.name);\n\t\tthis.validateUniqueNames(this.activeToolNames, \"Duplicate active tool name(s)\");\n\t\tthis.validateToolNames(this.activeToolNames);\n\t\tthis.steeringQueueMode = options.steeringMode ?? \"one-at-a-time\";\n\t\tthis.followUpQueueMode = options.followUpMode ?? \"one-at-a-time\";\n\t}\n\n\tprivate getHandlers(type: string): Set<AgentHarnessHandler> | undefined {\n\t\treturn this.handlers.get(type);\n\t}\n\n\tprivate async emitOwn(event: AgentHarnessOwnEvent<TSkill, TPromptTemplate>, signal?: AbortSignal): Promise<void> {\n\t\tfor (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) {\n\t\t\ttry {\n\t\t\t\tawait listener(event, signal);\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async emitAny(event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal): Promise<void> {\n\t\tfor (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) {\n\t\t\ttry {\n\t\t\t\tawait listener(event, signal);\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async emitHook<TType extends keyof AgentHarnessEventResultMap>(\n\t\tevent: Extract<AgentHarnessOwnEvent, { type: TType }>,\n\t): Promise<AgentHarnessEventResultMap[TType] | undefined> {\n\t\tconst handlers = this.getHandlers(event.type as TType);\n\t\tif (!handlers || handlers.size === 0) return undefined;\n\t\tlet lastResult: AgentHarnessEventResultMap[TType] | undefined;\n\t\tfor (const handler of handlers) {\n\t\t\ttry {\n\t\t\t\tconst result = await handler(event);\n\t\t\t\tif (result !== undefined) {\n\t\t\t\t\tlastResult = result;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t\treturn lastResult;\n\t}\n\n\tprivate async emitBeforeProviderRequest(\n\t\tmodel: Model<any>,\n\t\tsessionId: string,\n\t\tstreamOptions: AgentHarnessStreamOptions,\n\t): Promise<AgentHarnessStreamOptions> {\n\t\tconst handlers = this.getHandlers(\"before_provider_request\");\n\t\tlet current = cloneStreamOptions(streamOptions);\n\t\tif (!handlers || handlers.size === 0) return current;\n\t\tfor (const handler of handlers) {\n\t\t\ttry {\n\t\t\t\tconst result = await handler({\n\t\t\t\t\ttype: \"before_provider_request\",\n\t\t\t\t\tmodel,\n\t\t\t\t\tsessionId,\n\t\t\t\t\tstreamOptions: cloneStreamOptions(current),\n\t\t\t\t});\n\t\t\t\tif (result?.streamOptions) {\n\t\t\t\t\tcurrent = applyStreamOptionsPatch(current, result.streamOptions);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t\treturn current;\n\t}\n\n\tprivate async emitBeforeProviderPayload(model: Model<any>, payload: unknown): Promise<unknown> {\n\t\tconst handlers = this.getHandlers(\"before_provider_payload\");\n\t\tlet current = payload;\n\t\tif (!handlers || handlers.size === 0) return current;\n\t\tfor (const handler of handlers) {\n\t\t\ttry {\n\t\t\t\tconst result = await handler({ type: \"before_provider_payload\", model, payload: current });\n\t\t\t\tif (result !== undefined) {\n\t\t\t\t\tcurrent = result.payload;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t}\n\t\treturn current;\n\t}\n\n\tprivate async emitQueueUpdate(): Promise<void> {\n\t\tawait this.emitOwn({\n\t\t\ttype: \"queue_update\",\n\t\t\tsteer: [...this.steerQueue],\n\t\t\tfollowUp: [...this.followUpQueue],\n\t\t\tnextTurn: [...this.nextTurnQueue],\n\t\t});\n\t}\n\n\tprivate startRunPromise(): () => void {\n\t\tlet finish = () => {};\n\t\tthis.runPromise = new Promise<void>((resolve) => {\n\t\t\tfinish = resolve;\n\t\t});\n\t\treturn () => {\n\t\t\tthis.runPromise = undefined;\n\t\t\tfinish();\n\t\t};\n\t}\n\n\tprivate async createTurnState(): Promise<AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>> {\n\t\tconst context = await this.session.buildContext();\n\t\tconst resources = this.getResources();\n\t\tconst sessionMetadata = await this.session.getMetadata();\n\t\tconst tools = [...this.tools.values()];\n\t\tconst activeTools = this.activeToolNames\n\t\t\t.map((name) => this.tools.get(name))\n\t\t\t.filter((tool): tool is TTool => tool !== undefined);\n\t\tlet systemPrompt = \"You are a helpful assistant.\";\n\t\tif (typeof this.systemPrompt === \"string\") {\n\t\t\tsystemPrompt = this.systemPrompt;\n\t\t} else if (this.systemPrompt) {\n\t\t\tsystemPrompt = await this.systemPrompt({\n\t\t\t\tenv: this.env,\n\t\t\t\tsession: this.session,\n\t\t\t\tmodel: this.model,\n\t\t\t\tthinkingLevel: this.thinkingLevel,\n\t\t\t\tactiveTools,\n\t\t\t\tresources,\n\t\t\t});\n\t\t}\n\t\treturn {\n\t\t\tmessages: context.messages,\n\t\t\tresources,\n\t\t\tstreamOptions: cloneStreamOptions(this.streamOptions),\n\t\t\tsessionId: sessionMetadata.id,\n\t\t\tsystemPrompt,\n\t\t\tmodel: this.model,\n\t\t\tthinkingLevel: this.thinkingLevel,\n\t\t\ttools,\n\t\t\tactiveTools,\n\t\t};\n\t}\n\n\tprivate createContext(\n\t\tturnState: AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>,\n\t\tsystemPrompt?: string,\n\t): AgentContext {\n\t\treturn {\n\t\t\tsystemPrompt: systemPrompt ?? turnState.systemPrompt,\n\t\t\tmessages: turnState.messages.slice(),\n\t\t\ttools: turnState.activeTools.slice(),\n\t\t};\n\t}\n\n\tprivate createStreamFn(getTurnState: () => AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>): StreamFn {\n\t\treturn async (model, context, streamOptions) => {\n\t\t\tconst turnState = getTurnState();\n\t\t\tconst auth = await this.getApiKeyAndHeaders?.(model);\n\t\t\tconst snapshotOptions: AgentHarnessStreamOptions = {\n\t\t\t\t...turnState.streamOptions,\n\t\t\t\theaders: mergeHeaders(turnState.streamOptions.headers, auth?.headers),\n\t\t\t};\n\t\t\tconst requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\tcacheRetention: requestOptions.cacheRetention,\n\t\t\t\theaders: requestOptions.headers,\n\t\t\t\tmaxRetries: requestOptions.maxRetries,\n\t\t\t\tmaxRetryDelayMs: requestOptions.maxRetryDelayMs,\n\t\t\t\tmetadata: requestOptions.metadata,\n\t\t\t\tonPayload: async (payload) => await this.emitBeforeProviderPayload(model, payload),\n\t\t\t\tonResponse: async (response) => {\n\t\t\t\t\tconst headers = { ...(response.headers as Record<string, string>) };\n\t\t\t\t\tawait this.emitOwn(\n\t\t\t\t\t\t{ type: \"after_provider_response\", status: response.status, headers },\n\t\t\t\t\t\tstreamOptions?.signal,\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\treasoning: streamOptions?.reasoning,\n\t\t\t\tsignal: streamOptions?.signal,\n\t\t\t\tsessionId: turnState.sessionId,\n\t\t\t\ttimeoutMs: requestOptions.timeoutMs,\n\t\t\t\ttransport: requestOptions.transport,\n\t\t\t\tapiKey: auth?.apiKey,\n\t\t\t});\n\t\t};\n\t}\n\n\tprivate async drainQueuedMessages(queue: AgentMessage[], mode: QueueMode): Promise<AgentMessage[]> {\n\t\tconst messages = mode === \"all\" ? queue.splice(0) : queue.splice(0, 1);\n\t\tif (messages.length === 0) return messages;\n\t\ttry {\n\t\t\tawait this.emitQueueUpdate();\n\t\t\treturn messages;\n\t\t} catch (error) {\n\t\t\tqueue.unshift(...messages);\n\t\t\tthrow normalizeHookError(error);\n\t\t}\n\t}\n\n\tprivate createLoopConfig(\n\t\tgetTurnState: () => AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>,\n\t\tsetTurnState: (turnState: AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>) => void,\n\t): AgentLoopConfig {\n\t\tconst turnState = getTurnState();\n\t\treturn {\n\t\t\tmodel: turnState.model,\n\t\t\treasoning: turnState.thinkingLevel === \"off\" ? undefined : turnState.thinkingLevel,\n\t\t\tconvertToLlm,\n\t\t\ttransformContext: async (messages) => {\n\t\t\t\tconst result = await this.emitHook({ type: \"context\", messages: [...messages] });\n\t\t\t\treturn result?.messages ?? messages;\n\t\t\t},\n\t\t\tbeforeToolCall: async ({ toolCall, args }) => {\n\t\t\t\tconst result = await this.emitHook({\n\t\t\t\t\ttype: \"tool_call\",\n\t\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\t\ttoolName: toolCall.name,\n\t\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\t});\n\t\t\t\treturn result ? { block: result.block, reason: result.reason } : undefined;\n\t\t\t},\n\t\t\tafterToolCall: async ({ toolCall, args, result, isError }) => {\n\t\t\t\tconst patch = await this.emitHook({\n\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\t\ttoolName: toolCall.name,\n\t\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\t\tcontent: result.content,\n\t\t\t\t\tdetails: result.details,\n\t\t\t\t\tisError,\n\t\t\t\t});\n\t\t\t\treturn patch\n\t\t\t\t\t? { content: patch.content, details: patch.details, isError: patch.isError, terminate: patch.terminate }\n\t\t\t\t\t: undefined;\n\t\t\t},\n\t\t\tprepareNextTurn: async () => {\n\t\t\t\tawait this.flushPendingSessionWrites();\n\t\t\t\tconst nextTurnState = await this.createTurnState();\n\t\t\t\tsetTurnState(nextTurnState);\n\t\t\t\treturn {\n\t\t\t\t\tcontext: this.createContext(nextTurnState),\n\t\t\t\t\tmodel: nextTurnState.model,\n\t\t\t\t\tthinkingLevel: nextTurnState.thinkingLevel,\n\t\t\t\t};\n\t\t\t},\n\t\t\tgetSteeringMessages: async () => this.drainQueuedMessages(this.steerQueue, this.steeringQueueMode),\n\t\t\tgetFollowUpMessages: async () => this.drainQueuedMessages(this.followUpQueue, this.followUpQueueMode),\n\t\t};\n\t}\n\n\tprivate validateUniqueNames(names: string[], message: string): void {\n\t\tconst duplicates = findDuplicateNames(names);\n\t\tif (duplicates.length > 0)\n\t\t\tthrow new AgentHarnessError(\"invalid_argument\", `${message}: ${duplicates.join(\", \")}`);\n\t}\n\n\tprivate validateToolNames(toolNames: string[], tools: Map<string, TTool> = this.tools): void {\n\t\tthis.validateUniqueNames(toolNames, \"Duplicate active tool name(s)\");\n\t\tconst missing = toolNames.filter((name) => !tools.has(name));\n\t\tif (missing.length > 0) throw new AgentHarnessError(\"invalid_argument\", `Unknown tool(s): ${missing.join(\", \")}`);\n\t}\n\n\tprivate async flushPendingSessionWrites(): Promise<void> {\n\t\twhile (this.pendingSessionWrites.length > 0) {\n\t\t\tconst write = this.pendingSessionWrites[0]!;\n\t\t\tif (write.type === \"message\") {\n\t\t\t\tawait this.session.appendMessage(write.message);\n\t\t\t} else if (write.type === \"model_change\") {\n\t\t\t\tawait this.session.appendModelChange(write.provider, write.modelId);\n\t\t\t} else if (write.type === \"thinking_level_change\") {\n\t\t\t\tawait this.session.appendThinkingLevelChange(write.thinkingLevel);\n\t\t\t} else if (write.type === \"active_tools_change\") {\n\t\t\t\tawait this.session.appendActiveToolsChange(write.activeToolNames);\n\t\t\t} else if (write.type === \"custom\") {\n\t\t\t\tawait this.session.appendCustomEntry(write.customType, write.data);\n\t\t\t} else if (write.type === \"custom_message\") {\n\t\t\t\tawait this.session.appendCustomMessageEntry(write.customType, write.content, write.display, write.details);\n\t\t\t} else if (write.type === \"label\") {\n\t\t\t\tawait this.session.appendLabel(write.targetId, write.label);\n\t\t\t} else if (write.type === \"session_info\") {\n\t\t\t\tawait this.session.appendSessionName(write.name ?? \"\");\n\t\t\t} else if (write.type === \"leaf\") {\n\t\t\t\tawait this.session.getStorage().setLeafId(write.targetId);\n\t\t\t}\n\t\t\tthis.pendingSessionWrites.shift();\n\t\t}\n\t}\n\n\tprivate async handleAgentEvent(event: AgentEvent, signal?: AbortSignal): Promise<void> {\n\t\tif (event.type === \"message_end\") {\n\t\t\tawait this.session.appendMessage(event.message);\n\t\t\tawait this.emitAny(event, signal);\n\t\t\treturn;\n\t\t}\n\t\tif (event.type === \"turn_end\") {\n\t\t\tlet eventError: unknown;\n\t\t\ttry {\n\t\t\t\tawait this.emitAny(event, signal);\n\t\t\t} catch (error) {\n\t\t\t\teventError = error;\n\t\t\t}\n\t\t\tconst hadPendingMutations = this.pendingSessionWrites.length > 0;\n\t\t\tawait this.flushPendingSessionWrites();\n\t\t\tif (eventError) throw eventError;\n\t\t\tawait this.emitOwn({ type: \"save_point\", hadPendingMutations });\n\t\t\treturn;\n\t\t}\n\t\tif (event.type === \"agent_end\") {\n\t\t\tawait this.flushPendingSessionWrites();\n\t\t\tthis.phase = \"idle\";\n\t\t\tawait this.emitAny(event, signal);\n\t\t\tawait this.emitOwn({ type: \"settled\", nextTurnCount: this.nextTurnQueue.length }, signal);\n\t\t\treturn;\n\t\t}\n\t\tawait this.emitAny(event, signal);\n\t}\n\n\tprivate async emitRunFailure(\n\t\tmodel: Model<any>,\n\t\terror: unknown,\n\t\taborted: boolean,\n\t\tsignal: AbortSignal,\n\t): Promise<AgentMessage[]> {\n\t\tconst failureMessage = createFailureMessage(model, error, aborted);\n\t\tawait this.handleAgentEvent({ type: \"message_start\", message: failureMessage }, signal);\n\t\tawait this.handleAgentEvent({ type: \"message_end\", message: failureMessage }, signal);\n\t\tawait this.handleAgentEvent({ type: \"turn_end\", message: failureMessage, toolResults: [] }, signal);\n\t\tawait this.handleAgentEvent({ type: \"agent_end\", messages: [failureMessage] }, signal);\n\t\treturn [failureMessage];\n\t}\n\n\tprivate async executeTurn(\n\t\tturnState: AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>,\n\t\ttext: string,\n\t\toptions?: { images?: ImageContent[] },\n\t): Promise<AssistantMessage> {\n\t\tlet activeTurnState = turnState;\n\t\tlet messages: AgentMessage[] = [createUserMessage(text, options?.images)];\n\t\tif (this.nextTurnQueue.length > 0) {\n\t\t\tconst queuedMessages = this.nextTurnQueue.splice(0);\n\t\t\ttry {\n\t\t\t\tawait this.emitQueueUpdate();\n\t\t\t} catch (error) {\n\t\t\t\tthis.nextTurnQueue.unshift(...queuedMessages);\n\t\t\t\tthrow normalizeHookError(error);\n\t\t\t}\n\t\t\tmessages = [...queuedMessages, messages[0]!];\n\t\t}\n\t\tconst beforeResult = await this.emitHook({\n\t\t\ttype: \"before_agent_start\",\n\t\t\tprompt: text,\n\t\t\timages: options?.images,\n\t\t\tsystemPrompt: turnState.systemPrompt,\n\t\t\tresources: turnState.resources,\n\t\t});\n\t\tif (beforeResult?.messages) messages = [...messages, ...beforeResult.messages];\n\n\t\tconst abortController = new AbortController();\n\t\tconst getTurnState = () => activeTurnState;\n\t\tconst setTurnState = (nextTurnState: AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>) => {\n\t\t\tactiveTurnState = nextTurnState;\n\t\t};\n\t\tthis.runAbortController = abortController;\n\t\tconst runResultPromise = (async () => {\n\t\t\ttry {\n\t\t\t\treturn await runAgentLoop(\n\t\t\t\t\tmessages,\n\t\t\t\t\tthis.createContext(turnState, beforeResult?.systemPrompt),\n\t\t\t\t\tthis.createLoopConfig(getTurnState, setTurnState),\n\t\t\t\t\t(event) => this.handleAgentEvent(event, abortController.signal),\n\t\t\t\t\tabortController.signal,\n\t\t\t\t\tthis.createStreamFn(getTurnState),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await this.emitRunFailure(\n\t\t\t\t\t\tactiveTurnState.model,\n\t\t\t\t\t\terror,\n\t\t\t\t\t\tabortController.signal.aborted,\n\t\t\t\t\t\tabortController.signal,\n\t\t\t\t\t);\n\t\t\t\t} catch (failureError) {\n\t\t\t\t\tconst cause = new AggregateError(\n\t\t\t\t\t\t[toError(error), toError(failureError)],\n\t\t\t\t\t\t\"Agent run failed and failure reporting failed\",\n\t\t\t\t\t);\n\t\t\t\t\tthrow new AgentHarnessError(\"unknown\", cause.message, cause);\n\t\t\t\t}\n\t\t\t}\n\t\t})();\n\t\ttry {\n\t\t\tconst newMessages = await runResultPromise;\n\t\t\tfor (let i = newMessages.length - 1; i >= 0; i--) {\n\t\t\t\tconst message = newMessages[i]!;\n\t\t\t\tif (message.role === \"assistant\") {\n\t\t\t\t\treturn message;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", \"AgentHarness prompt completed without an assistant message\");\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tawait this.flushPendingSessionWrites();\n\t\t\t} finally {\n\t\t\t\tthis.runAbortController = undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync prompt(text: string, options?: { images?: ImageContent[] }): Promise<AssistantMessage> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"AgentHarness is busy\");\n\t\tthis.phase = \"turn\";\n\t\tconst finishRunPromise = this.startRunPromise();\n\t\ttry {\n\t\t\tconst turnState = await this.createTurnState();\n\t\t\treturn await this.executeTurn(turnState, text, options);\n\t\t} catch (error) {\n\t\t\tthis.phase = \"idle\";\n\t\t\tthrow normalizeHarnessError(error, \"unknown\");\n\t\t} finally {\n\t\t\tfinishRunPromise();\n\t\t}\n\t}\n\n\tasync skill(name: string, additionalInstructions?: string): Promise<AssistantMessage> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"AgentHarness is busy\");\n\t\tthis.phase = \"turn\";\n\t\tconst finishRunPromise = this.startRunPromise();\n\t\ttry {\n\t\t\tconst turnState = await this.createTurnState();\n\t\t\tconst skill = (turnState.resources.skills ?? []).find((candidate) => candidate.name === name);\n\t\t\tif (!skill) throw new AgentHarnessError(\"invalid_argument\", `Unknown skill: ${name}`);\n\t\t\treturn await this.executeTurn(turnState, formatSkillInvocation(skill, additionalInstructions));\n\t\t} catch (error) {\n\t\t\tthis.phase = \"idle\";\n\t\t\tthrow normalizeHarnessError(error, \"unknown\");\n\t\t} finally {\n\t\t\tfinishRunPromise();\n\t\t}\n\t}\n\n\tasync promptFromTemplate(name: string, args: string[] = []): Promise<AssistantMessage> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"AgentHarness is busy\");\n\t\tthis.phase = \"turn\";\n\t\tconst finishRunPromise = this.startRunPromise();\n\t\ttry {\n\t\t\tconst turnState = await this.createTurnState();\n\t\t\tconst template = (turnState.resources.promptTemplates ?? []).find((candidate) => candidate.name === name);\n\t\t\tif (!template) throw new AgentHarnessError(\"invalid_argument\", `Unknown prompt template: ${name}`);\n\t\t\treturn await this.executeTurn(turnState, formatPromptTemplateInvocation(template, args));\n\t\t} catch (error) {\n\t\t\tthis.phase = \"idle\";\n\t\t\tthrow normalizeHarnessError(error, \"unknown\");\n\t\t} finally {\n\t\t\tfinishRunPromise();\n\t\t}\n\t}\n\n\tasync steer(text: string, options?: { images?: ImageContent[] }): Promise<void> {\n\t\tif (this.phase === \"idle\") throw new AgentHarnessError(\"invalid_state\", \"Cannot steer while idle\");\n\t\tthis.steerQueue.push(createUserMessage(text, options?.images));\n\t\tawait this.emitQueueUpdate();\n\t}\n\n\tasync followUp(text: string, options?: { images?: ImageContent[] }): Promise<void> {\n\t\tif (this.phase === \"idle\") throw new AgentHarnessError(\"invalid_state\", \"Cannot follow up while idle\");\n\t\tthis.followUpQueue.push(createUserMessage(text, options?.images));\n\t\tawait this.emitQueueUpdate();\n\t}\n\n\tasync nextTurn(text: string, options?: { images?: ImageContent[] }): Promise<void> {\n\t\tthis.nextTurnQueue.push(createUserMessage(text, options?.images));\n\t\tawait this.emitQueueUpdate();\n\t}\n\n\tasync appendMessage(message: AgentMessage): Promise<void> {\n\t\ttry {\n\t\t\tif (this.phase === \"idle\") {\n\t\t\t\tawait this.session.appendMessage(message);\n\t\t\t} else {\n\t\t\t\tthis.pendingSessionWrites.push({ type: \"message\", message });\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"session\");\n\t\t}\n\t}\n\n\tasync compact(\n\t\tcustomInstructions?: string,\n\t): Promise<{ summary: string; firstKeptEntryId: string; tokensBefore: number; details?: unknown }> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"compact() requires idle harness\");\n\t\tthis.phase = \"compaction\";\n\t\ttry {\n\t\t\tconst model = this.model;\n\t\t\tif (!model) throw new AgentHarnessError(\"invalid_state\", \"No model set for compaction\");\n\t\t\tconst auth = await this.getApiKeyAndHeaders?.(model);\n\t\t\tif (!auth) throw new AgentHarnessError(\"auth\", \"No auth available for compaction\");\n\t\t\tconst branchEntries = await this.session.getBranch();\n\t\t\tconst preparationResult = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS);\n\t\t\tif (!preparationResult.ok) throw preparationResult.error;\n\t\t\tconst preparation = preparationResult.value;\n\t\t\tif (!preparation) throw new AgentHarnessError(\"compaction\", \"Nothing to compact\");\n\t\t\tconst hookResult = await this.emitHook({\n\t\t\t\ttype: \"session_before_compact\",\n\t\t\t\tpreparation,\n\t\t\t\tbranchEntries,\n\t\t\t\tcustomInstructions,\n\t\t\t\tsignal: new AbortController().signal,\n\t\t\t});\n\t\t\tif (hookResult?.cancel) throw new AgentHarnessError(\"compaction\", \"Compaction cancelled\");\n\t\t\tconst provided = hookResult?.compaction;\n\t\t\tconst compactResult = provided\n\t\t\t\t? { ok: true as const, value: provided }\n\t\t\t\t: await compact(\n\t\t\t\t\t\tpreparation,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tauth.apiKey,\n\t\t\t\t\t\tauth.headers,\n\t\t\t\t\t\tcustomInstructions,\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tthis.thinkingLevel,\n\t\t\t\t\t);\n\t\t\tif (!compactResult.ok) throw compactResult.error;\n\t\t\tconst result = compactResult.value;\n\t\t\tconst entryId = await this.session.appendCompaction(\n\t\t\t\tresult.summary,\n\t\t\t\tresult.firstKeptEntryId,\n\t\t\t\tresult.tokensBefore,\n\t\t\t\tresult.details,\n\t\t\t\tprovided !== undefined,\n\t\t\t);\n\t\t\tconst entry = await this.session.getEntry(entryId);\n\t\t\tif (entry?.type === \"compaction\") {\n\t\t\t\tawait this.emitOwn({ type: \"session_compact\", compactionEntry: entry, fromHook: provided !== undefined });\n\t\t\t}\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"compaction\");\n\t\t} finally {\n\t\t\tthis.phase = \"idle\";\n\t\t}\n\t}\n\n\tasync navigateTree(\n\t\ttargetId: string,\n\t\toptions?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },\n\t): Promise<NavigateTreeResult> {\n\t\tif (this.phase !== \"idle\") throw new AgentHarnessError(\"busy\", \"navigateTree() requires idle harness\");\n\t\tthis.phase = \"branch_summary\";\n\t\ttry {\n\t\t\tconst oldLeafId = await this.session.getLeafId();\n\t\t\tif (oldLeafId === targetId) return { cancelled: false };\n\t\t\tconst targetEntry = await this.session.getEntry(targetId);\n\t\t\tif (!targetEntry) throw new AgentHarnessError(\"invalid_argument\", `Entry ${targetId} not found`);\n\t\t\tconst { entries, commonAncestorId } = await collectEntriesForBranchSummary(this.session, oldLeafId, targetId);\n\t\t\tconst preparation = {\n\t\t\t\ttargetId,\n\t\t\t\toldLeafId,\n\t\t\t\tcommonAncestorId,\n\t\t\t\tentriesToSummarize: entries,\n\t\t\t\tuserWantsSummary: options?.summarize ?? false,\n\t\t\t\tcustomInstructions: options?.customInstructions,\n\t\t\t\treplaceInstructions: options?.replaceInstructions,\n\t\t\t\tlabel: options?.label,\n\t\t\t};\n\t\t\tconst signal = new AbortController().signal;\n\t\t\tconst hookResult = await this.emitHook({ type: \"session_before_tree\", preparation, signal });\n\t\t\tif (hookResult?.cancel) return { cancelled: true };\n\t\t\tlet summaryEntry: NavigateTreeResult[\"summaryEntry\"];\n\t\t\tlet summaryText: string | undefined = hookResult?.summary?.summary;\n\t\t\tlet summaryDetails: unknown = hookResult?.summary?.details;\n\t\t\tif (!summaryText && options?.summarize && entries.length > 0) {\n\t\t\t\tconst model = this.model;\n\t\t\t\tif (!model) throw new AgentHarnessError(\"invalid_state\", \"No model set for branch summary\");\n\t\t\t\tconst auth = await this.getApiKeyAndHeaders?.(model);\n\t\t\t\tif (!auth) throw new AgentHarnessError(\"auth\", \"No auth available for branch summary\");\n\t\t\t\tconst branchSummary = await generateBranchSummary(entries, {\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\t\theaders: auth.headers,\n\t\t\t\t\tsignal: new AbortController().signal,\n\t\t\t\t\tcustomInstructions: hookResult?.customInstructions ?? options?.customInstructions,\n\t\t\t\t\treplaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions,\n\t\t\t\t});\n\t\t\t\tif (!branchSummary.ok) {\n\t\t\t\t\tif (branchSummary.error.code === \"aborted\") return { cancelled: true };\n\t\t\t\t\tthrow new AgentHarnessError(\"branch_summary\", branchSummary.error.message, branchSummary.error);\n\t\t\t\t}\n\t\t\t\tsummaryText = branchSummary.value.summary;\n\t\t\t\tsummaryDetails = {\n\t\t\t\t\treadFiles: branchSummary.value.readFiles,\n\t\t\t\t\tmodifiedFiles: branchSummary.value.modifiedFiles,\n\t\t\t\t};\n\t\t\t}\n\t\t\tlet editorText: string | undefined;\n\t\t\tlet newLeafId: string | null;\n\t\t\tif (targetEntry.type === \"message\" && targetEntry.message.role === \"user\") {\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\tconst content = targetEntry.message.content;\n\t\t\t\teditorText =\n\t\t\t\t\ttypeof content === \"string\"\n\t\t\t\t\t\t? content\n\t\t\t\t\t\t: content\n\t\t\t\t\t\t\t\t.filter((c): c is { readonly type: \"text\"; readonly text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t\t.join(\"\");\n\t\t\t} else if (targetEntry.type === \"custom_message\") {\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\teditorText =\n\t\t\t\t\ttypeof targetEntry.content === \"string\"\n\t\t\t\t\t\t? targetEntry.content\n\t\t\t\t\t\t: targetEntry.content\n\t\t\t\t\t\t\t\t.filter((c): c is { readonly type: \"text\"; readonly text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t\t.join(\"\");\n\t\t\t} else {\n\t\t\t\tnewLeafId = targetId;\n\t\t\t}\n\t\t\tconst summaryId = await this.session.moveTo(\n\t\t\t\tnewLeafId,\n\t\t\t\tsummaryText\n\t\t\t\t\t? { summary: summaryText, details: summaryDetails, fromHook: hookResult?.summary !== undefined }\n\t\t\t\t\t: undefined,\n\t\t\t);\n\t\t\tif (summaryId) {\n\t\t\t\tconst entry = await this.session.getEntry(summaryId);\n\t\t\t\tif (entry?.type === \"branch_summary\") summaryEntry = entry;\n\t\t\t}\n\t\t\tawait this.emitOwn({\n\t\t\t\ttype: \"session_tree\",\n\t\t\t\tnewLeafId: await this.session.getLeafId(),\n\t\t\t\toldLeafId,\n\t\t\t\tsummaryEntry,\n\t\t\t\tfromHook: hookResult?.summary !== undefined,\n\t\t\t});\n\t\t\treturn { cancelled: false, editorText, summaryEntry };\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"branch_summary\");\n\t\t} finally {\n\t\t\tthis.phase = \"idle\";\n\t\t}\n\t}\n\n\tgetModel(): Model<any> {\n\t\treturn this.model;\n\t}\n\n\tasync setModel(model: Model<any>): Promise<void> {\n\t\ttry {\n\t\t\tconst previousModel = this.model;\n\t\t\tif (this.phase === \"idle\") {\n\t\t\t\tawait this.session.appendModelChange(model.provider, model.id);\n\t\t\t} else {\n\t\t\t\tthis.pendingSessionWrites.push({ type: \"model_change\", provider: model.provider, modelId: model.id });\n\t\t\t}\n\t\t\tthis.model = model;\n\t\t\tawait this.emitOwn({ type: \"model_update\", model, previousModel, source: \"set\" });\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"session\");\n\t\t}\n\t}\n\n\tgetThinkingLevel(): ThinkingLevel {\n\t\treturn this.thinkingLevel;\n\t}\n\n\tasync setThinkingLevel(level: ThinkingLevel): Promise<void> {\n\t\ttry {\n\t\t\tconst previousLevel = this.thinkingLevel;\n\t\t\tif (this.phase === \"idle\") {\n\t\t\t\tawait this.session.appendThinkingLevelChange(level);\n\t\t\t} else {\n\t\t\t\tthis.pendingSessionWrites.push({ type: \"thinking_level_change\", thinkingLevel: level });\n\t\t\t}\n\t\t\tthis.thinkingLevel = level;\n\t\t\tawait this.emitOwn({ type: \"thinking_level_update\", level, previousLevel });\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"session\");\n\t\t}\n\t}\n\n\tgetTools(): TTool[] {\n\t\treturn [...this.tools.values()];\n\t}\n\n\tasync setTools(tools: TTool[], activeToolNames?: string[]): Promise<void> {\n\t\ttry {\n\t\t\tthis.validateUniqueNames(\n\t\t\t\ttools.map((tool) => tool.name),\n\t\t\t\t\"Duplicate tool name(s)\",\n\t\t\t);\n\t\t\tconst nextTools = new Map(tools.map((tool) => [tool.name, tool]));\n\t\t\tconst nextActiveToolNames = activeToolNames ? [...activeToolNames] : this.activeToolNames;\n\t\t\tthis.validateToolNames(nextActiveToolNames, nextTools);\n\t\t\tconst previousToolNames = [...this.tools.keys()];\n\t\t\tconst previousActiveToolNames = [...this.activeToolNames];\n\t\t\tif (this.phase === \"idle\") {\n\t\t\t\tawait this.session.appendActiveToolsChange(nextActiveToolNames);\n\t\t\t} else {\n\t\t\t\tthis.pendingSessionWrites.push({ type: \"active_tools_change\", activeToolNames: [...nextActiveToolNames] });\n\t\t\t}\n\t\t\tthis.tools = nextTools;\n\t\t\tthis.activeToolNames = [...nextActiveToolNames];\n\t\t\tawait this.emitOwn({\n\t\t\t\ttype: \"tools_update\",\n\t\t\t\ttoolNames: [...this.tools.keys()],\n\t\t\t\tpreviousToolNames,\n\t\t\t\tactiveToolNames: [...this.activeToolNames],\n\t\t\t\tpreviousActiveToolNames,\n\t\t\t\tsource: \"set\",\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"invalid_argument\");\n\t\t}\n\t}\n\n\tgetActiveTools(): TTool[] {\n\t\treturn this.activeToolNames.map((name) => this.tools.get(name)!);\n\t}\n\n\tasync setActiveTools(toolNames: string[]): Promise<void> {\n\t\ttry {\n\t\t\tthis.validateToolNames(toolNames);\n\t\t\tconst previousToolNames = [...this.tools.keys()];\n\t\t\tconst previousActiveToolNames = [...this.activeToolNames];\n\t\t\tif (this.phase === \"idle\") {\n\t\t\t\tawait this.session.appendActiveToolsChange(toolNames);\n\t\t\t} else {\n\t\t\t\tthis.pendingSessionWrites.push({ type: \"active_tools_change\", activeToolNames: [...toolNames] });\n\t\t\t}\n\t\t\tthis.activeToolNames = [...toolNames];\n\t\t\tawait this.emitOwn({\n\t\t\t\ttype: \"tools_update\",\n\t\t\t\ttoolNames: [...this.tools.keys()],\n\t\t\t\tpreviousToolNames,\n\t\t\t\tactiveToolNames: [...this.activeToolNames],\n\t\t\t\tpreviousActiveToolNames,\n\t\t\t\tsource: \"set\",\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthrow normalizeHarnessError(error, \"invalid_argument\");\n\t\t}\n\t}\n\n\tgetSteeringMode(): QueueMode {\n\t\treturn this.steeringQueueMode;\n\t}\n\n\tasync setSteeringMode(mode: QueueMode): Promise<void> {\n\t\tthis.steeringQueueMode = mode;\n\t}\n\n\tgetFollowUpMode(): QueueMode {\n\t\treturn this.followUpQueueMode;\n\t}\n\n\tasync setFollowUpMode(mode: QueueMode): Promise<void> {\n\t\tthis.followUpQueueMode = mode;\n\t}\n\n\tgetResources(): AgentHarnessResources<TSkill, TPromptTemplate> {\n\t\treturn {\n\t\t\tskills: this.resources.skills?.slice(),\n\t\t\tpromptTemplates: this.resources.promptTemplates?.slice(),\n\t\t};\n\t}\n\n\tasync setResources(resources: AgentHarnessResources<TSkill, TPromptTemplate>): Promise<void> {\n\t\tconst previousResources = this.getResources();\n\t\tthis.resources = {\n\t\t\tskills: resources.skills?.slice(),\n\t\t\tpromptTemplates: resources.promptTemplates?.slice(),\n\t\t};\n\t\tawait this.emitOwn({ type: \"resources_update\", resources: this.getResources(), previousResources });\n\t}\n\n\tgetStreamOptions(): AgentHarnessStreamOptions {\n\t\treturn cloneStreamOptions(this.streamOptions);\n\t}\n\n\tasync setStreamOptions(streamOptions: AgentHarnessStreamOptions): Promise<void> {\n\t\tthis.streamOptions = cloneStreamOptions(streamOptions);\n\t}\n\n\tasync abort(): Promise<AbortResult> {\n\t\tconst clearedSteer = [...this.steerQueue];\n\t\tconst clearedFollowUp = [...this.followUpQueue];\n\t\tthis.steerQueue = [];\n\t\tthis.followUpQueue = [];\n\t\tthis.runAbortController?.abort();\n\t\tconst errors: Error[] = [];\n\t\ttry {\n\t\t\tawait this.emitQueueUpdate();\n\t\t} catch (error) {\n\t\t\terrors.push(toError(error));\n\t\t}\n\t\ttry {\n\t\t\tawait this.waitForIdle();\n\t\t} catch (error) {\n\t\t\terrors.push(toError(error));\n\t\t}\n\t\ttry {\n\t\t\tawait this.emitOwn({ type: \"abort\", clearedSteer, clearedFollowUp });\n\t\t} catch (error) {\n\t\t\terrors.push(toError(error));\n\t\t}\n\t\tif (errors.length > 0) {\n\t\t\tconst cause = errors.length === 1 ? errors[0]! : new AggregateError(errors, \"Abort completed with errors\");\n\t\t\tthrow normalizeHarnessError(cause, \"hook\");\n\t\t}\n\t\treturn { clearedSteer, clearedFollowUp };\n\t}\n\n\tasync waitForIdle(): Promise<void> {\n\t\tawait this.runPromise;\n\t}\n\n\tsubscribe(\n\t\tlistener: (event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal) => Promise<void> | void,\n\t): () => void {\n\t\tlet handlers = this.handlers.get(SUBSCRIBER_EVENT_TYPE);\n\t\tif (!handlers) {\n\t\t\thandlers = new Set();\n\t\t\tthis.handlers.set(SUBSCRIBER_EVENT_TYPE, handlers);\n\t\t}\n\t\thandlers.add(listener as AgentHarnessHandler);\n\t\treturn () => handlers!.delete(listener as AgentHarnessHandler);\n\t}\n\n\ton<TType extends keyof AgentHarnessEventResultMap>(\n\t\ttype: TType,\n\t\thandler: (\n\t\t\tevent: Extract<AgentHarnessOwnEvent, { type: TType }>,\n\t\t) => Promise<AgentHarnessEventResultMap[TType]> | AgentHarnessEventResultMap[TType],\n\t): () => void {\n\t\tlet handlers = this.handlers.get(type);\n\t\tif (!handlers) {\n\t\t\thandlers = new Set();\n\t\t\tthis.handlers.set(type, handlers);\n\t\t}\n\t\thandlers.add(handler as AgentHarnessHandler);\n\t\treturn () => handlers!.delete(handler as AgentHarnessHandler);\n\t}\n}\n"]}
@@ -50,6 +50,16 @@ function mergeHeaders(...headers) {
50
50
  }
51
51
  return hasHeaders ? merged : undefined;
52
52
  }
53
+ function findDuplicateNames(names) {
54
+ const seen = new Set();
55
+ const duplicates = new Set();
56
+ for (const name of names) {
57
+ if (seen.has(name))
58
+ duplicates.add(name);
59
+ seen.add(name);
60
+ }
61
+ return [...duplicates];
62
+ }
53
63
  function applyStreamOptionsPatch(base, patch) {
54
64
  const result = cloneStreamOptions(base);
55
65
  if (!patch)
@@ -140,12 +150,17 @@ export class AgentHarness {
140
150
  this.streamOptions = cloneStreamOptions(options.streamOptions);
141
151
  this.systemPrompt = options.systemPrompt;
142
152
  this.getApiKeyAndHeaders = options.getApiKeyAndHeaders;
153
+ this.validateUniqueNames((options.tools ?? []).map((tool) => tool.name), "Duplicate tool name(s)");
143
154
  for (const tool of options.tools ?? []) {
144
155
  this.tools.set(tool.name, tool);
145
156
  }
146
157
  this.model = options.model;
147
158
  this.thinkingLevel = options.thinkingLevel ?? "off";
148
- this.activeToolNames = options.activeToolNames ?? (options.tools ?? []).map((tool) => tool.name);
159
+ this.activeToolNames = options.activeToolNames
160
+ ? [...options.activeToolNames]
161
+ : (options.tools ?? []).map((tool) => tool.name);
162
+ this.validateUniqueNames(this.activeToolNames, "Duplicate active tool name(s)");
163
+ this.validateToolNames(this.activeToolNames);
149
164
  this.steeringQueueMode = options.steeringMode ?? "one-at-a-time";
150
165
  this.followUpQueueMode = options.followUpMode ?? "one-at-a-time";
151
166
  }
@@ -379,7 +394,13 @@ export class AgentHarness {
379
394
  getFollowUpMessages: async () => this.drainQueuedMessages(this.followUpQueue, this.followUpQueueMode),
380
395
  };
381
396
  }
397
+ validateUniqueNames(names, message) {
398
+ const duplicates = findDuplicateNames(names);
399
+ if (duplicates.length > 0)
400
+ throw new AgentHarnessError("invalid_argument", `${message}: ${duplicates.join(", ")}`);
401
+ }
382
402
  validateToolNames(toolNames, tools = this.tools) {
403
+ this.validateUniqueNames(toolNames, "Duplicate active tool name(s)");
383
404
  const missing = toolNames.filter((name) => !tools.has(name));
384
405
  if (missing.length > 0)
385
406
  throw new AgentHarnessError("invalid_argument", `Unknown tool(s): ${missing.join(", ")}`);
@@ -396,6 +417,9 @@ export class AgentHarness {
396
417
  else if (write.type === "thinking_level_change") {
397
418
  await this.session.appendThinkingLevelChange(write.thinkingLevel);
398
419
  }
420
+ else if (write.type === "active_tools_change") {
421
+ await this.session.appendActiveToolsChange(write.activeToolNames);
422
+ }
399
423
  else if (write.type === "custom") {
400
424
  await this.session.appendCustomEntry(write.customType, write.data);
401
425
  }
@@ -756,9 +780,6 @@ export class AgentHarness {
756
780
  getModel() {
757
781
  return this.model;
758
782
  }
759
- getThinkingLevel() {
760
- return this.thinkingLevel;
761
- }
762
783
  async setModel(model) {
763
784
  try {
764
785
  const previousModel = this.model;
@@ -769,12 +790,15 @@ export class AgentHarness {
769
790
  this.pendingSessionWrites.push({ type: "model_change", provider: model.provider, modelId: model.id });
770
791
  }
771
792
  this.model = model;
772
- await this.emitOwn({ type: "model_select", model, previousModel, source: "set" });
793
+ await this.emitOwn({ type: "model_update", model, previousModel, source: "set" });
773
794
  }
774
795
  catch (error) {
775
796
  throw normalizeHarnessError(error, "session");
776
797
  }
777
798
  }
799
+ getThinkingLevel() {
800
+ return this.thinkingLevel;
801
+ }
778
802
  async setThinkingLevel(level) {
779
803
  try {
780
804
  const previousLevel = this.thinkingLevel;
@@ -785,16 +809,67 @@ export class AgentHarness {
785
809
  this.pendingSessionWrites.push({ type: "thinking_level_change", thinkingLevel: level });
786
810
  }
787
811
  this.thinkingLevel = level;
788
- await this.emitOwn({ type: "thinking_level_select", level, previousLevel });
812
+ await this.emitOwn({ type: "thinking_level_update", level, previousLevel });
789
813
  }
790
814
  catch (error) {
791
815
  throw normalizeHarnessError(error, "session");
792
816
  }
793
817
  }
818
+ getTools() {
819
+ return [...this.tools.values()];
820
+ }
821
+ async setTools(tools, activeToolNames) {
822
+ try {
823
+ this.validateUniqueNames(tools.map((tool) => tool.name), "Duplicate tool name(s)");
824
+ const nextTools = new Map(tools.map((tool) => [tool.name, tool]));
825
+ const nextActiveToolNames = activeToolNames ? [...activeToolNames] : this.activeToolNames;
826
+ this.validateToolNames(nextActiveToolNames, nextTools);
827
+ const previousToolNames = [...this.tools.keys()];
828
+ const previousActiveToolNames = [...this.activeToolNames];
829
+ if (this.phase === "idle") {
830
+ await this.session.appendActiveToolsChange(nextActiveToolNames);
831
+ }
832
+ else {
833
+ this.pendingSessionWrites.push({ type: "active_tools_change", activeToolNames: [...nextActiveToolNames] });
834
+ }
835
+ this.tools = nextTools;
836
+ this.activeToolNames = [...nextActiveToolNames];
837
+ await this.emitOwn({
838
+ type: "tools_update",
839
+ toolNames: [...this.tools.keys()],
840
+ previousToolNames,
841
+ activeToolNames: [...this.activeToolNames],
842
+ previousActiveToolNames,
843
+ source: "set",
844
+ });
845
+ }
846
+ catch (error) {
847
+ throw normalizeHarnessError(error, "invalid_argument");
848
+ }
849
+ }
850
+ getActiveTools() {
851
+ return this.activeToolNames.map((name) => this.tools.get(name));
852
+ }
794
853
  async setActiveTools(toolNames) {
795
854
  try {
796
855
  this.validateToolNames(toolNames);
856
+ const previousToolNames = [...this.tools.keys()];
857
+ const previousActiveToolNames = [...this.activeToolNames];
858
+ if (this.phase === "idle") {
859
+ await this.session.appendActiveToolsChange(toolNames);
860
+ }
861
+ else {
862
+ this.pendingSessionWrites.push({ type: "active_tools_change", activeToolNames: [...toolNames] });
863
+ }
797
864
  this.activeToolNames = [...toolNames];
865
+ await this.emitOwn({
866
+ type: "tools_update",
867
+ toolNames: [...this.tools.keys()],
868
+ previousToolNames,
869
+ activeToolNames: [...this.activeToolNames],
870
+ previousActiveToolNames,
871
+ source: "set",
872
+ });
798
873
  }
799
874
  catch (error) {
800
875
  throw normalizeHarnessError(error, "invalid_argument");
@@ -832,18 +907,6 @@ export class AgentHarness {
832
907
  async setStreamOptions(streamOptions) {
833
908
  this.streamOptions = cloneStreamOptions(streamOptions);
834
909
  }
835
- async setTools(tools, activeToolNames) {
836
- try {
837
- const nextTools = new Map(tools.map((tool) => [tool.name, tool]));
838
- const nextActiveToolNames = activeToolNames ? [...activeToolNames] : this.activeToolNames;
839
- this.validateToolNames(nextActiveToolNames, nextTools);
840
- this.tools = nextTools;
841
- this.activeToolNames = [...nextActiveToolNames];
842
- }
843
- catch (error) {
844
- throw normalizeHarnessError(error, "invalid_argument");
845
- }
846
- }
847
910
  async abort() {
848
911
  const clearedSteer = [...this.steerQueue];
849
912
  const clearedFollowUp = [...this.followUpQueue];