From 7300fc6a8de7451f317a22bdb4e1c5ded3009a61 Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Sun, 19 Jul 2026 19:10:02 +0800 Subject: [PATCH 1/3] feat: add thinking_effort to turn telemetry events turn_started / turn_ended / turn_interrupted now carry the resolved effective thinking effort in both engines, so per-turn and per-session effort usage distribution becomes measurable. In agent-core-v2 the profile domain mirrors the effective effort (including the KIMI_MODEL_THINKING_EFFORT override) into the ambient telemetry context on model / thinking changes and after restore; the loop snapshots it at turn start. In agent-core the turn flow reads config.thinkingEffort at each emission point. --- .../src/agent/loop/loopService.ts | 12 +++++-- .../src/agent/profile/profileService.ts | 19 +++++++++-- .../app/telemetry/agentTelemetryContext.ts | 8 +++-- .../agent-core-v2/src/app/telemetry/events.ts | 6 ++++ .../test/agent/loop/loop.test.ts | 3 +- .../test/agent/profile/config-state.test.ts | 33 +++++++++++++++++++ packages/agent-core/src/agent/turn/index.ts | 4 ++- packages/agent-core/test/agent/turn.test.ts | 7 ++-- 8 files changed, 80 insertions(+), 12 deletions(-) diff --git a/packages/agent-core-v2/src/agent/loop/loopService.ts b/packages/agent-core-v2/src/agent/loop/loopService.ts index fce65bfecb..3fe5af724f 100644 --- a/packages/agent-core-v2/src/agent/loop/loopService.ts +++ b/packages/agent-core-v2/src/agent/loop/loopService.ts @@ -372,10 +372,16 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { this.telemetryContext.set({ turn_id: turn.id }); const telemetryContext = this.telemetryContext.get(); const turnTelemetry = this.telemetry.withContext(telemetryContext); - const { mode, provider_type, protocol } = telemetryContext; + const { mode, provider_type, protocol, thinking_effort } = telemetryContext; let result: TurnResult | undefined; try { - const started: TurnStartedTelemetryEvent = { turn_id: turn.id, mode, provider_type, protocol }; + const started: TurnStartedTelemetryEvent = { + turn_id: turn.id, + mode, + provider_type, + protocol, + thinking_effort, + }; turnTelemetry.track2('turn_started', started); result = await this.run({ turnId: turn.id, @@ -411,6 +417,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { interrupt_reason: interruptReasonFor(result), provider_type, protocol, + thinking_effort, trace_id: traceId, }; turnTelemetry.track2('turn_interrupted', interrupted); @@ -423,6 +430,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { mode, provider_type, protocol, + thinking_effort, trace_id: traceId, }; turnTelemetry.track2('turn_ended', ended); diff --git a/packages/agent-core-v2/src/agent/profile/profileService.ts b/packages/agent-core-v2/src/agent/profile/profileService.ts index bbb2e5e365..c4ca0902cc 100644 --- a/packages/agent-core-v2/src/agent/profile/profileService.ts +++ b/packages/agent-core-v2/src/agent/profile/profileService.ts @@ -19,11 +19,14 @@ * `emitStatusUpdated` run live-only after the dispatch, so `wire.replay` * rebuilds the Models silently; the same live-only path mirrors the resolved * model protocol into the ambient telemetry context (`provider_type` / - * `protocol`) whenever the model alias changes. + * `protocol`) whenever the model alias changes, and mirrors the effective + * thinking effort (`thinking_effort`) whenever the model alias or thinking + * level changes; an `onDidRestore` hook re-mirrors the effort after replay. * Bound at Agent scope. */ import { InstantiationType } from '#/_base/di/extensions'; +import { Disposable } from '#/_base/di/lifecycle'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import { UNKNOWN_CAPABILITY, type ModelCapability } from '#/app/llmProtocol/capability'; import { type GenerationKwargs } from '#/app/llmProtocol/kimiOptions'; @@ -92,7 +95,7 @@ declare module '#/app/event/eventBus' { } } -export class AgentProfileService implements IAgentProfileService { +export class AgentProfileService extends Disposable implements IAgentProfileService { declare readonly _serviceBrand: undefined; private optionsValue: ProfileServiceOptions = {}; @@ -124,7 +127,14 @@ export class AgentProfileService implements IAgentProfileService { @IAgentProfileCatalogService private readonly catalog: IAgentProfileCatalogService, @ISessionSkillCatalog private readonly skillCatalog: ISessionSkillCatalog, ) { + super(); this.configure({}); + this._register( + this.wire.hooks.onDidRestore.register('profile', async (_ctx, next) => { + this.mirrorTelemetryThinkingEffort(); + await next(); + }), + ); } configure(options: ProfileServiceOptions): void { @@ -426,6 +436,7 @@ export class AgentProfileService implements IAgentProfileService { this.telemetryContext.set({ provider_type: protocol, protocol }); } if (changed.modelAlias !== undefined || changed.thinkingLevel !== undefined) { + this.mirrorTelemetryThinkingEffort(); this.warnAboutAnthropicThinkingEffort(); } this.emitStatusUpdated( @@ -433,6 +444,10 @@ export class AgentProfileService implements IAgentProfileService { ); } + private mirrorTelemetryThinkingEffort(): void { + this.telemetryContext.set({ thinking_effort: this.getEffectiveThinkingLevel() }); + } + private warnAboutAnthropicThinkingEffort(): void { try { const model = this.tryResolveRawModel(); diff --git a/packages/agent-core-v2/src/app/telemetry/agentTelemetryContext.ts b/packages/agent-core-v2/src/app/telemetry/agentTelemetryContext.ts index 7ba536f4ae..3ed0206057 100644 --- a/packages/agent-core-v2/src/app/telemetry/agentTelemetryContext.ts +++ b/packages/agent-core-v2/src/app/telemetry/agentTelemetryContext.ts @@ -3,9 +3,10 @@ * * Agent-scoped ambient telemetry context: a per-agent property bag that domains * contribute to (the `plan` domain sets `mode`, the `profile` domain mirrors - * the resolved model protocol into `provider_type` / `protocol`, the `loop` - * domain sets `turn_id` at turn start and keeps `trace_id` at the active - * turn's most recent request) and that turn-scoped + * the resolved model protocol into `provider_type` / `protocol` and the + * effective thinking effort into `thinking_effort`, the `loop` domain sets + * `turn_id` at turn start and keeps `trace_id` at the active turn's most + * recent request) and that turn-scoped * telemetry snapshots at launch. Decouples turn telemetry from any * specific contributor so the turn domain does not need to know about plan or * profile. Bound at Agent scope. @@ -17,6 +18,7 @@ export type AgentTelemetryContext = { mode: 'agent' | 'plan'; provider_type?: string; protocol?: string; + thinking_effort?: string; turn_id?: number; trace_id?: string; }; diff --git a/packages/agent-core-v2/src/app/telemetry/events.ts b/packages/agent-core-v2/src/app/telemetry/events.ts index 774fbf30b3..0323b4f451 100644 --- a/packages/agent-core-v2/src/app/telemetry/events.ts +++ b/packages/agent-core-v2/src/app/telemetry/events.ts @@ -47,6 +47,7 @@ export interface TurnStartedEvent { mode: 'agent' | 'plan'; provider_type?: string; protocol?: string; + thinking_effort?: string; } export interface TurnInterruptedEvent { @@ -56,6 +57,7 @@ export interface TurnInterruptedEvent { interrupt_reason: 'user_cancelled' | 'aborted' | 'max_steps' | 'error' | 'filtered' | 'blocked'; provider_type?: string; protocol?: string; + thinking_effort?: string; trace_id?: string; } @@ -66,6 +68,7 @@ export interface TurnEndedEvent { mode: 'agent' | 'plan'; provider_type?: string; protocol?: string; + thinking_effort?: string; trace_id?: string; } @@ -410,6 +413,7 @@ export const telemetryEventDefinitions = { mode: 'Agent mode the turn runs in', provider_type: 'Provider protocol type', protocol: 'Request protocol', + thinking_effort: 'Effective thinking effort the turn runs with', }, }), turn_interrupted: defineTelemetryEvent({ @@ -422,6 +426,7 @@ export const telemetryEventDefinitions = { interrupt_reason: 'Why the turn was interrupted', provider_type: 'Provider protocol type', protocol: 'Request protocol', + thinking_effort: 'Effective thinking effort the turn ran with', trace_id: 'Trace id of the most recent LLM request in this turn (the failed request when the turn errored); absent for non-Kimi protocols', }, @@ -436,6 +441,7 @@ export const telemetryEventDefinitions = { mode: 'Agent mode the turn ran in', provider_type: 'Provider protocol type', protocol: 'Request protocol', + thinking_effort: 'Effective thinking effort the turn ran with', trace_id: 'Trace id of the most recent LLM request in this turn; absent for non-Kimi protocols', }, diff --git a/packages/agent-core-v2/test/agent/loop/loop.test.ts b/packages/agent-core-v2/test/agent/loop/loop.test.ts index 1f4224b6f8..ff7acff1cd 100644 --- a/packages/agent-core-v2/test/agent/loop/loop.test.ts +++ b/packages/agent-core-v2/test/agent/loop/loop.test.ts @@ -635,7 +635,7 @@ describe('turn telemetry', () => { expect(records).toContainEqual({ event: 'turn_started', - properties: { turn_id: 0, mode: 'agent', provider_type: 'kimi', protocol: 'kimi' }, + properties: { turn_id: 0, mode: 'agent', provider_type: 'kimi', protocol: 'kimi', thinking_effort: 'off' }, }); expect(records).toContainEqual({ event: 'turn_ended', @@ -646,6 +646,7 @@ describe('turn telemetry', () => { mode: 'agent', provider_type: 'kimi', protocol: 'kimi', + thinking_effort: 'off', }), }); expect(records.some((record) => record.event === 'turn_interrupted')).toBe(false); diff --git a/packages/agent-core-v2/test/agent/profile/config-state.test.ts b/packages/agent-core-v2/test/agent/profile/config-state.test.ts index 719bd06435..24d91937ab 100644 --- a/packages/agent-core-v2/test/agent/profile/config-state.test.ts +++ b/packages/agent-core-v2/test/agent/profile/config-state.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { IAgentLLMRequesterService } from '#/agent/llmRequester/llmRequester'; import { IAgentProfileService } from '#/agent/profile/profile'; +import { IAgentTelemetryContextService } from '#/app/telemetry/agentTelemetryContext'; import type { ModelConfig } from '#/app/model/model'; import { configServices, @@ -119,6 +120,38 @@ describe('ConfigState model capabilities', () => { }); }); + it('mirrors the effective thinking effort into the ambient telemetry context', () => { + kimiConfig = { + providers: { + kimi: { + type: 'kimi', + apiKey: 'test-key', + baseUrl: 'https://api.example.test/v1', + }, + }, + models: { + 'kimi-code/kimi-for-coding': { + provider: 'kimi', + model: 'kimi-for-coding', + maxContextSize: 1_000_000, + capabilities: ['thinking'], + supportEfforts: ['low', 'high'], + }, + }, + }; + const telemetryContext = ctx.get(IAgentTelemetryContextService); + + profile.update({ modelAlias: 'kimi-code/kimi-for-coding', thinkingLevel: 'on' }); + // 'on' is not a declared effort, so the model default takes over. + expect(telemetryContext.get().thinking_effort).toBe('high'); + + profile.setThinking('low'); + expect(telemetryContext.get().thinking_effort).toBe('low'); + + profile.setThinking('off'); + expect(telemetryContext.get().thinking_effort).toBe('off'); + }); + it('does not infer Kimi capabilities from the provider catalogue', () => { kimiConfig = { providers: { diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 3136aa6889..506f60ae0d 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -532,7 +532,7 @@ export class TurnFlow { const telemetryMode = this.telemetryMode(); this.telemetryModeByTurn.set(turnId, telemetryMode); this.currentStepByTurn.set(turnId, 0); - this.agent.telemetry.track('turn_started', { turn_id: turnId, mode: telemetryMode, ...this.requestProtocolProps() }); + this.agent.telemetry.track('turn_started', { turn_id: turnId, mode: telemetryMode, thinking_effort: this.agent.config.thinkingEffort, ...this.requestProtocolProps() }); this.agent.fullCompaction.resetForTurn(); this.agent.usage.beginTurn(); this.agent.emitEvent({ type: 'turn.started', turnId, origin }); @@ -651,6 +651,7 @@ export class TurnFlow { reason: ended.reason, duration_ms: ended.durationMs, mode: this.telemetryModeByTurn.get(turnId) ?? this.telemetryMode(), + thinking_effort: this.agent.config.thinkingEffort, ...this.requestProtocolProps(), trace_id: terminalTraceId, }); @@ -1178,6 +1179,7 @@ export class TurnFlow { this.agent.telemetry.track('turn_interrupted', { turn_id: turnId, mode: this.telemetryModeByTurn.get(turnId) ?? this.telemetryMode(), + thinking_effort: this.agent.config.thinkingEffort, at_step: atStep, interrupt_reason: interruptReason, ...this.requestProtocolProps(), diff --git a/packages/agent-core/test/agent/turn.test.ts b/packages/agent-core/test/agent/turn.test.ts index 3af3fb3069..e3218956b2 100644 --- a/packages/agent-core/test/agent/turn.test.ts +++ b/packages/agent-core/test/agent/turn.test.ts @@ -519,11 +519,11 @@ describe('Agent turn flow', () => { expect(records).toContainEqual({ event: 'turn_started', - properties: { turn_id: 0, mode: 'agent' }, + properties: { turn_id: 0, mode: 'agent', thinking_effort: 'off' }, }); expect(records).toContainEqual({ event: 'turn_interrupted', - properties: { turn_id: 0, mode: 'agent', at_step: 0, interrupt_reason: 'error' }, + properties: { turn_id: 0, mode: 'agent', thinking_effort: 'off', at_step: 0, interrupt_reason: 'error' }, }); }); @@ -649,7 +649,7 @@ describe('Agent turn flow', () => { const started = records.find((candidate) => candidate.event === 'turn_started'); expect(started).toEqual({ event: 'turn_started', - properties: expect.objectContaining({ mode: 'agent', provider_type: 'kimi', protocol: 'kimi' }), + properties: expect.objectContaining({ mode: 'agent', provider_type: 'kimi', protocol: 'kimi', thinking_effort: 'off' }), }); const ended = records.find((candidate) => candidate.event === 'turn_ended'); @@ -661,6 +661,7 @@ describe('Agent turn flow', () => { reason: 'completed', provider_type: 'kimi', protocol: 'kimi', + thinking_effort: 'off', duration_ms: expect.any(Number), }), }); From 94409fc4ea20c596ec84aefd967ad729acde6db6 Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Sun, 19 Jul 2026 20:59:23 +0800 Subject: [PATCH 2/3] fix(agent-core-v2): align turn telemetry with request config --- .../src/agent/llmRequester/llmRequester.ts | 7 +++ .../agent/llmRequester/llmRequesterService.ts | 15 ++++++- .../src/agent/loop/loopService.ts | 10 +++-- .../src/agent/profile/profileService.ts | 19 +------- .../app/telemetry/agentTelemetryContext.ts | 8 ++-- .../test/agent/loop/loop.test.ts | 45 +++++++++++++++++++ .../test/agent/profile/config-state.test.ts | 33 -------------- 7 files changed, 76 insertions(+), 61 deletions(-) diff --git a/packages/agent-core-v2/src/agent/llmRequester/llmRequester.ts b/packages/agent-core-v2/src/agent/llmRequester/llmRequester.ts index 87187621ad..244ec01e2e 100644 --- a/packages/agent-core-v2/src/agent/llmRequester/llmRequester.ts +++ b/packages/agent-core-v2/src/agent/llmRequester/llmRequester.ts @@ -1,6 +1,7 @@ import { createDecorator } from '#/_base/di/instantiation'; import type { FinishReason } from '#/app/llmProtocol/finishReason'; import type { Message, StreamedMessagePart } from '#/app/llmProtocol/message'; +import type { ThinkingEffort } from '#/app/llmProtocol/thinkingEffort'; import type { Tool } from '#/app/llmProtocol/tool'; import type { TokenUsage } from '#/app/llmProtocol/usage'; import type { LLMRequestTrace } from '#/app/llmProtocol/requestTrace'; @@ -64,9 +65,15 @@ export interface LLMRequestTask { readonly result: Promise; } +export interface PreparedTurnRequestConfig { + readonly thinkingEffort: ThinkingEffort; +} + export interface IAgentLLMRequesterService { readonly _serviceBrand: undefined; + prepareTurnConfig(turnId: number): PreparedTurnRequestConfig; + request( overrides?: LLMRequestOverrides, onPart?: LLMRequestPartHandler, diff --git a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts index 6e17d74736..9c04df8018 100644 --- a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts +++ b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts @@ -8,7 +8,9 @@ * completion-token budget, then drives a bounded request chain: one primary * `model.request(input, signal)` attempt plus projection rebuilds for request * structure or media compatibility; general retry policy remains in the - * loop's `stepRetry` plugin. + * loop's `stepRetry` plugin. `prepareTurnConfig` snapshots the model, effective + * thinking effort, and system prompt at the turn boundary so loop telemetry + * and every request in that turn share one configuration. * Forwards streamed `part` events to the caller's `onPart` * handler, records `usage` through `IAgentUsageService`, resolves to an * `LLMRequestFinish` on the `finish` event, logs the request lifecycle @@ -77,6 +79,7 @@ import { type LLMRequestSource, type LLMRequestTask, type LLMStreamTiming, + type PreparedTurnRequestConfig, } from './llmRequester'; import type { LLMRequestTrace } from '#/app/llmProtocol/requestTrace'; import { @@ -152,6 +155,11 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService { @IEventBus private readonly eventBus: IEventBus, ) {} + prepareTurnConfig(turnId: number): PreparedTurnRequestConfig { + const config = this.getOrCreateTurnConfig(turnId); + return { thinkingEffort: config.resolved.thinkingLevel }; + } + async request( overrides: LLMRequestOverrides = {}, onPart: LLMRequestPartHandler = noopOnPart, @@ -529,7 +537,10 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService { private resolveTurnConfig(source: LLMRequestSource | undefined): TurnRequestConfig | undefined { if (source?.type !== 'turn') return undefined; - const turnId = source.turnId; + return this.getOrCreateTurnConfig(source.turnId); + } + + private getOrCreateTurnConfig(turnId: number): TurnRequestConfig { for (const id of this.turnConfigs.keys()) { if (id < turnId) this.turnConfigs.delete(id); } diff --git a/packages/agent-core-v2/src/agent/loop/loopService.ts b/packages/agent-core-v2/src/agent/loop/loopService.ts index 3fe5af724f..1d78b1d4d2 100644 --- a/packages/agent-core-v2/src/agent/loop/loopService.ts +++ b/packages/agent-core-v2/src/agent/loop/loopService.ts @@ -372,15 +372,17 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { this.telemetryContext.set({ turn_id: turn.id }); const telemetryContext = this.telemetryContext.get(); const turnTelemetry = this.telemetry.withContext(telemetryContext); - const { mode, provider_type, protocol, thinking_effort } = telemetryContext; + const { mode, provider_type, protocol } = telemetryContext; + let thinkingEffort: string | undefined; let result: TurnResult | undefined; try { + thinkingEffort = this.llmRequester.prepareTurnConfig(turn.id).thinkingEffort; const started: TurnStartedTelemetryEvent = { turn_id: turn.id, mode, provider_type, protocol, - thinking_effort, + thinking_effort: thinkingEffort, }; turnTelemetry.track2('turn_started', started); result = await this.run({ @@ -417,7 +419,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { interrupt_reason: interruptReasonFor(result), provider_type, protocol, - thinking_effort, + thinking_effort: thinkingEffort, trace_id: traceId, }; turnTelemetry.track2('turn_interrupted', interrupted); @@ -430,7 +432,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { mode, provider_type, protocol, - thinking_effort, + thinking_effort: thinkingEffort, trace_id: traceId, }; turnTelemetry.track2('turn_ended', ended); diff --git a/packages/agent-core-v2/src/agent/profile/profileService.ts b/packages/agent-core-v2/src/agent/profile/profileService.ts index c4ca0902cc..bbb2e5e365 100644 --- a/packages/agent-core-v2/src/agent/profile/profileService.ts +++ b/packages/agent-core-v2/src/agent/profile/profileService.ts @@ -19,14 +19,11 @@ * `emitStatusUpdated` run live-only after the dispatch, so `wire.replay` * rebuilds the Models silently; the same live-only path mirrors the resolved * model protocol into the ambient telemetry context (`provider_type` / - * `protocol`) whenever the model alias changes, and mirrors the effective - * thinking effort (`thinking_effort`) whenever the model alias or thinking - * level changes; an `onDidRestore` hook re-mirrors the effort after replay. + * `protocol`) whenever the model alias changes. * Bound at Agent scope. */ import { InstantiationType } from '#/_base/di/extensions'; -import { Disposable } from '#/_base/di/lifecycle'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import { UNKNOWN_CAPABILITY, type ModelCapability } from '#/app/llmProtocol/capability'; import { type GenerationKwargs } from '#/app/llmProtocol/kimiOptions'; @@ -95,7 +92,7 @@ declare module '#/app/event/eventBus' { } } -export class AgentProfileService extends Disposable implements IAgentProfileService { +export class AgentProfileService implements IAgentProfileService { declare readonly _serviceBrand: undefined; private optionsValue: ProfileServiceOptions = {}; @@ -127,14 +124,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ @IAgentProfileCatalogService private readonly catalog: IAgentProfileCatalogService, @ISessionSkillCatalog private readonly skillCatalog: ISessionSkillCatalog, ) { - super(); this.configure({}); - this._register( - this.wire.hooks.onDidRestore.register('profile', async (_ctx, next) => { - this.mirrorTelemetryThinkingEffort(); - await next(); - }), - ); } configure(options: ProfileServiceOptions): void { @@ -436,7 +426,6 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ this.telemetryContext.set({ provider_type: protocol, protocol }); } if (changed.modelAlias !== undefined || changed.thinkingLevel !== undefined) { - this.mirrorTelemetryThinkingEffort(); this.warnAboutAnthropicThinkingEffort(); } this.emitStatusUpdated( @@ -444,10 +433,6 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ ); } - private mirrorTelemetryThinkingEffort(): void { - this.telemetryContext.set({ thinking_effort: this.getEffectiveThinkingLevel() }); - } - private warnAboutAnthropicThinkingEffort(): void { try { const model = this.tryResolveRawModel(); diff --git a/packages/agent-core-v2/src/app/telemetry/agentTelemetryContext.ts b/packages/agent-core-v2/src/app/telemetry/agentTelemetryContext.ts index 3ed0206057..8dec84014e 100644 --- a/packages/agent-core-v2/src/app/telemetry/agentTelemetryContext.ts +++ b/packages/agent-core-v2/src/app/telemetry/agentTelemetryContext.ts @@ -3,10 +3,9 @@ * * Agent-scoped ambient telemetry context: a per-agent property bag that domains * contribute to (the `plan` domain sets `mode`, the `profile` domain mirrors - * the resolved model protocol into `provider_type` / `protocol` and the - * effective thinking effort into `thinking_effort`, the `loop` domain sets - * `turn_id` at turn start and keeps `trace_id` at the active turn's most - * recent request) and that turn-scoped + * the resolved model protocol into `provider_type` / `protocol`, the `loop` + * domain sets `turn_id` at turn start and keeps `trace_id` at the active turn's + * most recent request) and that turn-scoped * telemetry snapshots at launch. Decouples turn telemetry from any * specific contributor so the turn domain does not need to know about plan or * profile. Bound at Agent scope. @@ -18,7 +17,6 @@ export type AgentTelemetryContext = { mode: 'agent' | 'plan'; provider_type?: string; protocol?: string; - thinking_effort?: string; turn_id?: number; trace_id?: string; }; diff --git a/packages/agent-core-v2/test/agent/loop/loop.test.ts b/packages/agent-core-v2/test/agent/loop/loop.test.ts index ff7acff1cd..6dba3eb115 100644 --- a/packages/agent-core-v2/test/agent/loop/loop.test.ts +++ b/packages/agent-core-v2/test/agent/loop/loop.test.ts @@ -655,6 +655,50 @@ describe('turn telemetry', () => { } }); + it('keeps turn telemetry aligned with the request config across pre-step changes', async () => { + const records: TelemetryRecord[] = []; + const local = createTestAgent({ telemetry: recordingTelemetry(records) }); + try { + const localLoop = local.get(IAgentLoopService); + const localProfile = local.get(IAgentProfileService); + local.configure({ + modelCapabilities: { + image_in: false, + video_in: false, + audio_in: false, + thinking: true, + tool_use: true, + max_context_tokens: 1_000_000, + }, + }); + localProfile.update({ activeToolNames: [] }); + localProfile.setThinking('on'); + localLoop.hooks.onWillBeginStep.register('test-change-thinking', async (_ctx, next) => { + localProfile.setThinking('off'); + await next(); + }); + local.mockNextResponse({ type: 'text', text: 'hi' }); + + await local.rpc.prompt({ input: [{ type: 'text', text: 'Hello' }] }); + await local.untilTurnEnd(); + + const request = local.allEvents.find( + (event) => event.type === '[wire]' && event.event === 'llm.request', + ); + expect(request?.args).toMatchObject({ thinkingEffort: 'on' }); + expect(records).toContainEqual({ + event: 'turn_started', + properties: expect.objectContaining({ turn_id: 0, thinking_effort: 'on' }), + }); + expect(records).toContainEqual({ + event: 'turn_ended', + properties: expect.objectContaining({ turn_id: 0, thinking_effort: 'on' }), + }); + } finally { + await local.dispose(); + } + }); + it('attaches the latest request trace id to turn_ended', async () => { const records: TelemetryRecord[] = []; const local = createTestAgent({ telemetry: recordingTelemetry(records) }); @@ -927,6 +971,7 @@ function createTimingRequester(): IAgentLLMRequesterService { const requester: IAgentLLMRequesterService = { _serviceBrand: undefined, + prepareTurnConfig: () => ({ thinkingEffort: 'off' }), async request(_overrides, onPart = () => {}) { await onPart({ type: 'text', text: 'answer' }); return { diff --git a/packages/agent-core-v2/test/agent/profile/config-state.test.ts b/packages/agent-core-v2/test/agent/profile/config-state.test.ts index 24d91937ab..719bd06435 100644 --- a/packages/agent-core-v2/test/agent/profile/config-state.test.ts +++ b/packages/agent-core-v2/test/agent/profile/config-state.test.ts @@ -3,7 +3,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { IAgentLLMRequesterService } from '#/agent/llmRequester/llmRequester'; import { IAgentProfileService } from '#/agent/profile/profile'; -import { IAgentTelemetryContextService } from '#/app/telemetry/agentTelemetryContext'; import type { ModelConfig } from '#/app/model/model'; import { configServices, @@ -120,38 +119,6 @@ describe('ConfigState model capabilities', () => { }); }); - it('mirrors the effective thinking effort into the ambient telemetry context', () => { - kimiConfig = { - providers: { - kimi: { - type: 'kimi', - apiKey: 'test-key', - baseUrl: 'https://api.example.test/v1', - }, - }, - models: { - 'kimi-code/kimi-for-coding': { - provider: 'kimi', - model: 'kimi-for-coding', - maxContextSize: 1_000_000, - capabilities: ['thinking'], - supportEfforts: ['low', 'high'], - }, - }, - }; - const telemetryContext = ctx.get(IAgentTelemetryContextService); - - profile.update({ modelAlias: 'kimi-code/kimi-for-coding', thinkingLevel: 'on' }); - // 'on' is not a declared effort, so the model default takes over. - expect(telemetryContext.get().thinking_effort).toBe('high'); - - profile.setThinking('low'); - expect(telemetryContext.get().thinking_effort).toBe('low'); - - profile.setThinking('off'); - expect(telemetryContext.get().thinking_effort).toBe('off'); - }); - it('does not infer Kimi capabilities from the provider catalogue', () => { kimiConfig = { providers: { From 0b58df8a0cad6ee816cc74782379d16cce6d4abf Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Sun, 19 Jul 2026 21:09:54 +0800 Subject: [PATCH 3/3] fix(agent-core-v2): preserve model-less turn startup --- .../src/agent/llmRequester/llmRequester.ts | 2 +- .../src/agent/llmRequester/llmRequesterService.ts | 10 ++++++---- packages/agent-core-v2/src/agent/loop/loopService.ts | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/agent-core-v2/src/agent/llmRequester/llmRequester.ts b/packages/agent-core-v2/src/agent/llmRequester/llmRequester.ts index 244ec01e2e..f394eccbb1 100644 --- a/packages/agent-core-v2/src/agent/llmRequester/llmRequester.ts +++ b/packages/agent-core-v2/src/agent/llmRequester/llmRequester.ts @@ -72,7 +72,7 @@ export interface PreparedTurnRequestConfig { export interface IAgentLLMRequesterService { readonly _serviceBrand: undefined; - prepareTurnConfig(turnId: number): PreparedTurnRequestConfig; + prepareTurnConfig(turnId: number): PreparedTurnRequestConfig | undefined; request( overrides?: LLMRequestOverrides, diff --git a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts index 9c04df8018..5ef72eb850 100644 --- a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts +++ b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts @@ -8,9 +8,10 @@ * completion-token budget, then drives a bounded request chain: one primary * `model.request(input, signal)` attempt plus projection rebuilds for request * structure or media compatibility; general retry policy remains in the - * loop's `stepRetry` plugin. `prepareTurnConfig` snapshots the model, effective - * thinking effort, and system prompt at the turn boundary so loop telemetry - * and every request in that turn share one configuration. + * loop's `stepRetry` plugin. When a model is configured, `prepareTurnConfig` + * snapshots the model, effective thinking effort, and system prompt at the turn + * boundary so loop telemetry and every request in that turn share one + * configuration. * Forwards streamed `part` events to the caller's `onPart` * handler, records `usage` through `IAgentUsageService`, resolves to an * `LLMRequestFinish` on the `finish` event, logs the request lifecycle @@ -155,7 +156,8 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService { @IEventBus private readonly eventBus: IEventBus, ) {} - prepareTurnConfig(turnId: number): PreparedTurnRequestConfig { + prepareTurnConfig(turnId: number): PreparedTurnRequestConfig | undefined { + if (!this.profile.hasProvider()) return undefined; const config = this.getOrCreateTurnConfig(turnId); return { thinkingEffort: config.resolved.thinkingLevel }; } diff --git a/packages/agent-core-v2/src/agent/loop/loopService.ts b/packages/agent-core-v2/src/agent/loop/loopService.ts index 1d78b1d4d2..f088bdc92c 100644 --- a/packages/agent-core-v2/src/agent/loop/loopService.ts +++ b/packages/agent-core-v2/src/agent/loop/loopService.ts @@ -376,7 +376,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { let thinkingEffort: string | undefined; let result: TurnResult | undefined; try { - thinkingEffort = this.llmRequester.prepareTurnConfig(turn.id).thinkingEffort; + thinkingEffort = this.llmRequester.prepareTurnConfig(turn.id)?.thinkingEffort; const started: TurnStartedTelemetryEvent = { turn_id: turn.id, mode,