diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 37658438ea..255fc6ddc2 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -535,25 +535,19 @@ export class ReactLoopAgent implements Agent { ): Promise<{ request: GenerateOptions; preparedCall?: PreparedLlmCall }> { const { session } = this - const loggedConfig = session.requestHeader()?.config - const initialProvider = this.options.provider ?? '' - const initialModel = this.options.model ?? '' - const initialConfig: LlmCallConfig = { - provider: initialProvider, - model: initialModel, - ...loggedConfig?.provider === initialProvider - && loggedConfig.model === initialModel - && loggedConfig.reasoningEffort !== undefined - ? { reasoningEffort: loggedConfig.reasoningEffort } - : {}, - } // A loop instance starts from its declared route, restoring only an opaque // effort owned by that exact model. Later steps fold the config it logged. - const seedConfig: LlmCallConfig = deepFreeze(structuredClone( + const persistedConfig = session.requestHeader()?.config + const route = { provider: this.options.provider ?? '', model: this.options.model ?? '' } + const reasoningEffort = persistedConfig?.provider === route.provider + && persistedConfig.model === route.model + ? persistedConfig.reasoningEffort + : undefined + const seedConfig = deepFreeze(structuredClone( this.requestHeaderLogged - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- a logged instance anchor guarantees the fold - ? session.requestHeader()!.config - : initialConfig, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the instance logged the header it now folds + ? persistedConfig! + : { ...route, ...reasoningEffort === undefined ? {} : { reasoningEffort } }, )) const proposedConfig = await this.loopCtx.waterfall( agentCarrier(this), 'agent/request', this, turn, step, signal, @@ -590,17 +584,10 @@ export class ReactLoopAgent implements Agent { } const request = markAgentLoopRequest(deepFreeze({ - provider: header.config.provider, - model: header.config.model, - ...header.config.reasoningEffort !== undefined - ? { reasoningEffort: header.config.reasoningEffort } - : {}, + ...header.config, messages: boundaryMessages, ...header.system !== undefined ? { system: header.system } : {}, ...header.tools !== undefined ? { tools: header.tools } : {}, - ...header.config.temperature !== undefined ? { temperature: header.config.temperature } : {}, - ...header.config.maxTokens !== undefined ? { maxTokens: header.config.maxTokens } : {}, - ...header.config.stop !== undefined ? { stop: header.config.stop } : {}, sessionId: session.id, signal, })) diff --git a/packages/core/agent-loop/tests/request-reconstruction.spec.ts b/packages/core/agent-loop/tests/request-reconstruction.spec.ts index 97acd75755..04c46159c4 100644 --- a/packages/core/agent-loop/tests/request-reconstruction.spec.ts +++ b/packages/core/agent-loop/tests/request-reconstruction.spec.ts @@ -136,20 +136,26 @@ describe('request stability across the loop', () => { ]) expect(headers.map(event => event.data.reason)).toEqual(['initial', 'change']) - const resumedAdapter = new MockAdapter([textResponse('three')], reasoning) - const resumedCtx = await harness(resumedAdapter) - const resumedHandle = await resumedCtx.agents.create({ - sessionId: SessionId('effort-resumed'), - seed: structuredClone(agent.session.events), - agentOptions: { provider: 'mock', model: 'mock' }, - }) - send(resumedHandle.agent, 'third') - await waitForIdle(resumedCtx, resumedHandle.agent) + for (const [model, effort] of [ + ['mock', ReasoningEffortId('max')], + ['replacement', ReasoningEffortId('high')], + ] as const) { + const resumedAdapter = new MockAdapter([textResponse('resumed')], reasoning) + const resumedCtx = await harness(resumedAdapter) + const resumedHandle = await resumedCtx.agents.create({ + sessionId: SessionId(`effort-${model}`), + seed: structuredClone(agent.session.events), + agentOptions: { provider: 'mock', model }, + }) + send(resumedHandle.agent, 'resumed') + await waitForIdle(resumedCtx, resumedHandle.agent) - expect(resumedAdapter.requests[0]?.reasoningEffort).toBe(ReasoningEffortId('max')) - const resumedHeaders = resumedHandle.agent.session.events.filter(event => event.type === 'request/header') - expect(resumedHeaders.at(-1)?.data.header.config.reasoningEffort).toBe(ReasoningEffortId('max')) - expect(resumedHeaders.at(-1)?.data.reason).toBe('resume') + expect(resumedAdapter.requests[0]?.model).toBe(model) + expect(resumedAdapter.requests[0]?.reasoningEffort).toBe(effort) + const resumedHeaders = resumedHandle.agent.session.events.filter(event => event.type === 'request/header') + expect(resumedHeaders.at(-1)?.data.header.config.reasoningEffort).toBe(effort) + expect(resumedHeaders.at(-1)?.data.reason).toBe('resume') + } }) it('keeps exact-model resolution, request logging, and dispatch on one adapter registration', async () => {