diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 06e8cad76..8aed74361 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -4,8 +4,8 @@ ### Breaking Changes -- `AgentHarnessOptions.models` is required: the harness streams turns, compaction, and branch summarization through the provided `Models` instance (`models.streamSimple()`/`completeSimple()`) instead of the pi-ai global stream functions. Build one with `createModels()` + provider factories (or `builtinModels()` from `@earendil-works/pi-ai/providers/all`); tests use `fauxProvider()`. `getApiKeyAndHeaders` still wins per field but is no longer required — without it, requests resolve auth through the providers. -- `compact()`, `generateSummary()`, and `generateBranchSummary()` take a `Models` parameter; the explicit `apiKey` is now optional. +- `AgentHarnessOptions.models` is required and is the only auth path: the harness streams turns, compaction, and branch summarization through the provided `Models` instance (`models.streamSimple()`/`completeSimple()`), resolving auth through the providers. `AgentHarnessOptions.getApiKeyAndHeaders` is removed — apps that resolved keys per request now express that as provider auth (`ApiKeyAuth`/`OAuthAuth`) on the providers in the `Models` collection. Build one with `createModels()` + provider factories (or `builtinModels()` from `@earendil-works/pi-ai/providers/all`); tests use `fauxProvider()`. +- `compact()`, `generateSummary()`, and `generateBranchSummary()` take a `Models` parameter and no longer accept explicit `apiKey`/`headers`. - `StreamFn` is defined structurally (`(model, context, options?) => AssistantMessageEventStream | Promise<...>`); `Models.streamSimple` satisfies it. ## [0.79.1] - 2026-06-09 diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index cff784ae3..f5cb00cea 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -821,7 +821,7 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 6 — AgentHarness - [x] `AgentHarnessOptions.models` required (`readonly models` on the harness); the harness stream path uses `models.streamSimple()`. `StreamFn` redefined structurally (no compat type dependency); `Models.streamSimple` satisfies it. -- [x] Compaction/branch-summarization take the harness `Models` instance; explicit `getApiKeyAndHeaders` auth stays and wins per-field, but is no longer required — requests resolve through provider auth otherwise (the hard "No auth available" throws are gone). +- [x] Compaction/branch-summarization take the harness `Models` instance. `getApiKeyAndHeaders` is removed entirely — `Models` is the only auth path; per-request key resolution becomes provider auth on the collection. `compact()`/`generateSummary()`/`generateBranchSummary()` lose their explicit `apiKey`/`headers` parameters. - [x] Harness tests use `createModels()` + `fauxProvider()` with unique per-fake provider ids; no global api-registry state, no unregister bookkeeping. ### Phase 7 — coding-agent bridge (minimal) diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 1afb30c95..1d09b054e 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -69,17 +69,6 @@ function cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHar }; } -function mergeHeaders(...headers: Array | undefined>): Record | undefined { - const merged: Record = {}; - let hasHeaders = false; - for (const entry of headers) { - if (!entry) continue; - Object.assign(merged, entry); - hasHeaders = true; - } - return hasHeaders ? merged : undefined; -} - function findDuplicateNames(names: string[]): string[] { const seen = new Set(); const duplicates = new Set(); @@ -181,7 +170,6 @@ export class AgentHarness< private thinkingLevel: ThinkingLevel; private systemPrompt: AgentHarnessOptions["systemPrompt"]; private streamOptions: AgentHarnessStreamOptions; - private getApiKeyAndHeaders?: AgentHarnessOptions["getApiKeyAndHeaders"]; private resources: AgentHarnessResources; private tools = new Map(); private activeToolNames: string[]; @@ -199,7 +187,6 @@ export class AgentHarness< this.resources = options.resources ?? {}; this.streamOptions = cloneStreamOptions(options.streamOptions); this.systemPrompt = options.systemPrompt; - this.getApiKeyAndHeaders = options.getApiKeyAndHeaders; this.validateUniqueNames( (options.tools ?? []).map((tool) => tool.name), "Duplicate tool name(s)", @@ -372,11 +359,7 @@ export class AgentHarness< private createStreamFn(getTurnState: () => AgentHarnessTurnState): StreamFn { return async (model, context, streamOptions) => { const turnState = getTurnState(); - const auth = await this.getApiKeyAndHeaders?.(model); - const snapshotOptions: AgentHarnessStreamOptions = { - ...turnState.streamOptions, - headers: mergeHeaders(turnState.streamOptions.headers, auth?.headers), - }; + const snapshotOptions: AgentHarnessStreamOptions = { ...turnState.streamOptions }; const requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions); return this.models.streamSimple(model, context, { cacheRetention: requestOptions.cacheRetention, @@ -397,7 +380,6 @@ export class AgentHarness< sessionId: turnState.sessionId, timeoutMs: requestOptions.timeoutMs, transport: requestOptions.transport, - apiKey: auth?.apiKey, }); }; } @@ -709,8 +691,6 @@ export class AgentHarness< try { const model = this.model; if (!model) throw new AgentHarnessError("invalid_state", "No model set for compaction"); - // Explicit auth wins; otherwise the request resolves through provider auth. - const auth = await this.getApiKeyAndHeaders?.(model); const branchEntries = await this.session.getBranch(); const preparationResult = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS); if (!preparationResult.ok) throw preparationResult.error; @@ -727,16 +707,7 @@ export class AgentHarness< const provided = hookResult?.compaction; const compactResult = provided ? { ok: true as const, value: provided } - : await compact( - preparation, - this.models, - model, - auth?.apiKey, - auth?.headers, - customInstructions, - undefined, - this.thinkingLevel, - ); + : await compact(preparation, this.models, model, customInstructions, undefined, this.thinkingLevel); if (!compactResult.ok) throw compactResult.error; const result = compactResult.value; const entryId = await this.session.appendCompaction( @@ -789,13 +760,9 @@ export class AgentHarness< if (!summaryText && options?.summarize && entries.length > 0) { const model = this.model; if (!model) throw new AgentHarnessError("invalid_state", "No model set for branch summary"); - // Explicit auth wins; otherwise the request resolves through provider auth. - const auth = await this.getApiKeyAndHeaders?.(model); const branchSummary = await generateBranchSummary(entries, { models: this.models, model, - apiKey: auth?.apiKey, - headers: auth?.headers, signal: new AbortController().signal, customInstructions: hookResult?.customInstructions ?? options?.customInstructions, replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions, diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index 1df47d7a4..fdf1df498 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -49,14 +49,10 @@ export interface CollectEntriesResult { /** Options for generating a branch summary. */ export interface GenerateBranchSummaryOptions { - /** Provider collection the summarization request goes through. */ + /** Provider collection the summarization request goes through; owns auth resolution. */ models: Models; /** Model used for summarization. */ model: Model; - /** Explicit API key; wins over provider-resolved auth. */ - apiKey?: string; - /** Optional request headers forwarded to the provider. */ - headers?: Record; /** Abort signal for the summarization request. */ signal: AbortSignal; /** Optional instructions appended to or replacing the default prompt. */ @@ -204,16 +200,7 @@ export async function generateBranchSummary( entries: SessionTreeEntry[], options: GenerateBranchSummaryOptions, ): Promise> { - const { - models, - model, - apiKey, - headers, - signal, - customInstructions, - replaceInstructions, - reserveTokens = 16384, - } = options; + const { models, model, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options; const contextWindow = model.contextWindow || 128000; const tokenBudget = contextWindow - reserveTokens; @@ -244,7 +231,7 @@ export async function generateBranchSummary( const response = await models.completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, - { apiKey, headers, signal, maxTokens: 2048 }, + { signal, maxTokens: 2048 }, ); if (response.stopReason === "aborted") { return err(new BranchSummaryError("aborted", response.errorMessage || "Branch summary aborted")); diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index 55ea5b989..d6874c330 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -457,8 +457,6 @@ export async function generateSummary( models: Models, model: Model, reserveTokens: number, - apiKey?: string, - headers?: Record, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, @@ -490,8 +488,8 @@ export async function generateSummary( const completionOptions = model.reasoning && thinkingLevel && thinkingLevel !== "off" - ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } - : { maxTokens, signal, apiKey, headers }; + ? { maxTokens, signal, reasoning: thinkingLevel } + : { maxTokens, signal }; const response = await models.completeSimple( model, @@ -628,8 +626,6 @@ export async function compact( preparation: CompactionPreparation, models: Models, model: Model, - apiKey?: string, - headers?: Record, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, @@ -659,24 +655,13 @@ export async function compact( models, model, settings.reserveTokens, - apiKey, - headers, signal, customInstructions, previousSummary, thinkingLevel, ) : Promise.resolve(ok("No prior history.")), - generateTurnPrefixSummary( - turnPrefixMessages, - models, - model, - settings.reserveTokens, - apiKey, - headers, - signal, - thinkingLevel, - ), + generateTurnPrefixSummary(turnPrefixMessages, models, model, settings.reserveTokens, signal, thinkingLevel), ]); if (!historyResult.ok) return err(historyResult.error); if (!turnPrefixResult.ok) return err(turnPrefixResult.error); @@ -687,8 +672,6 @@ export async function compact( models, model, settings.reserveTokens, - apiKey, - headers, signal, customInstructions, previousSummary, @@ -713,8 +696,6 @@ async function generateTurnPrefixSummary( models: Models, model: Model, reserveTokens: number, - apiKey?: string, - headers?: Record, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, ): Promise> { @@ -737,8 +718,8 @@ async function generateTurnPrefixSummary( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, model.reasoning && thinkingLevel && thinkingLevel !== "off" - ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } - : { maxTokens, signal, apiKey, headers }, + ? { maxTokens, signal, reasoning: thinkingLevel } + : { maxTokens, signal }, ); if (response.stopReason === "aborted") { return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted")); diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 0e0aeaaf6..f7bdf6dd5 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -805,7 +805,7 @@ export interface AgentHarnessOptions< /** * Provider collection used for all model requests (turn streaming, * compaction, branch summarization). Auth resolves through the providers' - * auth; explicit per-request values (`getApiKeyAndHeaders`) win per field. + * auth. */ models: Models; tools?: TTool[]; @@ -824,9 +824,6 @@ export interface AgentHarnessOptions< activeTools: TTool[]; resources: AgentHarnessResources; }) => string | Promise); - getApiKeyAndHeaders?: ( - model: Model, - ) => Promise<{ apiKey: string; headers?: Record } | undefined>; /** Curated stream/provider request options. Snapshotted at turn start. */ streamOptions?: AgentHarnessStreamOptions; model: Model; diff --git a/packages/agent/test/harness/agent-harness-stream.test.ts b/packages/agent/test/harness/agent-harness-stream.test.ts index b4ed38dcd..f5a4021d3 100644 --- a/packages/agent/test/harness/agent-harness-stream.test.ts +++ b/packages/agent/test/harness/agent-harness-stream.test.ts @@ -36,7 +36,7 @@ function captureOptions(options: StreamOptions | undefined): StreamOptions { } describe("AgentHarness stream configuration", () => { - it("snapshots stream options and merges auth headers before provider request hooks", async () => { + it("snapshots stream options before provider request hooks", async () => { let capturedOptions: StreamOptions | undefined; const registration = newFaux(); registration.setResponses([ @@ -60,12 +60,11 @@ describe("AgentHarness stream configuration", () => { metadata: { base: true }, cacheRetention: "none", }, - getApiKeyAndHeaders: async () => ({ apiKey: "secret", headers: { "x-auth": "auth" } }), }); harness.on("before_provider_request", (event) => { expect(event.sessionId).toBe("session-1"); - expect(event.streamOptions.headers).toEqual({ "x-base": "base", "x-auth": "auth" }); + expect(event.streamOptions.headers).toEqual({ "x-base": "base" }); return { streamOptions: { headers: { "x-hook": "hook" }, @@ -77,14 +76,13 @@ describe("AgentHarness stream configuration", () => { await harness.prompt("hello"); expect(capturedOptions).toMatchObject({ - apiKey: "secret", timeoutMs: 1000, maxRetries: 2, maxRetryDelayMs: 3000, sessionId: "session-1", cacheRetention: "none", }); - expect(capturedOptions?.headers).toEqual({ "x-base": "base", "x-auth": "auth", "x-hook": "hook" }); + expect(capturedOptions?.headers).toEqual({ "x-base": "base", "x-hook": "hook" }); expect(capturedOptions?.metadata).toEqual({ base: true, hook: true }); }); diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts index 540cb332c..95148c3b3 100644 --- a/packages/agent/test/harness/compaction.test.ts +++ b/packages/agent/test/harness/compaction.test.ts @@ -440,20 +440,9 @@ describe("harness compaction", () => { }, ]); getOrThrow( - await generateSummary( - messages, - models, - reasoningModel, - 2000, - "test-key", - undefined, - undefined, - undefined, - undefined, - "medium", - ), + await generateSummary(messages, models, reasoningModel, 2000, undefined, undefined, undefined, "medium"), ); - expect(seenOptions[0]).toMatchObject({ reasoning: "medium", apiKey: "test-key" }); + expect(seenOptions[0]).toMatchObject({ reasoning: "medium" }); const { faux: fauxOff, model: offModel } = createFauxModel(true); fauxOff.setResponses([ @@ -462,20 +451,7 @@ describe("harness compaction", () => { return fauxAssistantMessage("## Goal\nTest summary"); }, ]); - getOrThrow( - await generateSummary( - messages, - models, - offModel, - 2000, - "test-key", - undefined, - undefined, - undefined, - undefined, - "off", - ), - ); + getOrThrow(await generateSummary(messages, models, offModel, 2000, undefined, undefined, undefined, "off")); expect(seenOptions[1]).not.toHaveProperty("reasoning"); const { faux: fauxNonReasoning, model: nonReasoningModel } = createFauxModel(false); @@ -486,18 +462,7 @@ describe("harness compaction", () => { }, ]); getOrThrow( - await generateSummary( - messages, - models, - nonReasoningModel, - 2000, - "test-key", - undefined, - undefined, - undefined, - undefined, - "medium", - ), + await generateSummary(messages, models, nonReasoningModel, 2000, undefined, undefined, undefined, "medium"), ); expect(seenOptions[2]).not.toHaveProperty("reasoning"); }); @@ -516,17 +481,7 @@ describe("harness compaction", () => { ]); const summary = getOrThrow( - await generateSummary( - messages, - models, - model, - 2000, - "test-key", - { "x-test": "yes" }, - undefined, - "focus", - "old summary", - ), + await generateSummary(messages, models, model, 2000, undefined, "focus", "old summary"), ); expect(summary).toContain("Test summary"); @@ -538,7 +493,7 @@ describe("harness compaction", () => { const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; const { faux: errorFaux, model: errorModel } = createFauxModel(false); errorFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "boom" })]); - const errorResult = await generateSummary(messages, models, errorModel, 2000, "test-key"); + const errorResult = await generateSummary(messages, models, errorModel, 2000); expect(errorResult).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Summarization failed: boom" }, @@ -546,7 +501,7 @@ describe("harness compaction", () => { const { faux: abortedFaux, model: abortedModel } = createFauxModel(false); abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "stopped" })]); - const abortedResult = await generateSummary(messages, models, abortedModel, 2000, "test-key"); + const abortedResult = await generateSummary(messages, models, abortedModel, 2000); expect(abortedResult).toMatchObject({ ok: false, error: { code: "aborted", message: "stopped" } }); }); @@ -574,7 +529,7 @@ describe("harness compaction", () => { settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 }, }; - getOrThrow(await compact(preparation, models, model, "test-key")); + getOrThrow(await compact(preparation, models, model)); expect(seenOptions.map((options) => options?.maxTokens)).toEqual([128000, 128000]); }); @@ -592,7 +547,7 @@ describe("harness compaction", () => { }; const { faux: historyFaux, model: historyModel } = createFauxModel(false); historyFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "history failed" })]); - expect(await compact(preparation, models, historyModel, "test-key")).toMatchObject({ + expect(await compact(preparation, models, historyModel)).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Summarization failed: history failed" }, }); @@ -602,7 +557,6 @@ describe("harness compaction", () => { { ...preparation, messagesToSummarize: [], firstKeptEntryId: "" }, models, invalidModel, - "test-key", ); expect(invalidResult).toMatchObject({ ok: false, error: { code: "invalid_session" } }); }); @@ -627,7 +581,7 @@ describe("harness compaction", () => { settings: { enabled: true, reserveTokens: 2000, keepRecentTokens: 20 }, }; - getOrThrow(await compact(preparation, models, model, "test-key", undefined, undefined, undefined, "high")); + getOrThrow(await compact(preparation, models, model, undefined, undefined, "high")); expect(seenOptions[0]).toMatchObject({ reasoning: "high" }); }); @@ -646,14 +600,14 @@ describe("harness compaction", () => { const { faux, model } = createFauxModel(false); faux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "prefix failed" })]); - expect(await compact(preparation, models, model, "test-key")).toMatchObject({ + expect(await compact(preparation, models, model)).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Turn prefix summarization failed: prefix failed" }, }); const { faux: abortedFaux, model: abortedModel } = createFauxModel(false); abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "prefix stopped" })]); - expect(await compact(preparation, models, abortedModel, "test-key")).toMatchObject({ + expect(await compact(preparation, models, abortedModel)).toMatchObject({ ok: false, error: { code: "aborted", message: "prefix stopped" }, }); @@ -672,7 +626,7 @@ describe("harness compaction", () => { expect(preparation).toBeDefined(); const { faux, model } = createFauxModel(false); faux.setResponses([fauxAssistantMessage("## Goal\nTest summary")]); - const result = getOrThrow(await compact(preparation!, models, model, "test-key")); + const result = getOrThrow(await compact(preparation!, models, model)); expect(result.summary.length).toBeGreaterThan(0); expect(result.firstKeptEntryId).toBeTruthy(); expect(result.details).toBeDefined();