diff --git a/packages/agent-core-v2/src/agent/profile/profileService.ts b/packages/agent-core-v2/src/agent/profile/profileService.ts index 95ac1facdb0..8ca3eddfb74 100644 --- a/packages/agent-core-v2/src/agent/profile/profileService.ts +++ b/packages/agent-core-v2/src/agent/profile/profileService.ts @@ -172,6 +172,12 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ this.states.contributeState(profileEmittedToolPatternWarningsKey); this.states.contributeState(profileEmittedPluginBudgetWarningsKey); this.configure({}); + this._register( + this.dispatcher.hooks.onDidRestore.register('profile', async (_ctx, next) => { + this.syncTelemetryModelContext(this.modelAlias); + await next(); + }), + ); this._register( this.config.onDidSectionChange(({ domain }) => { if (domain === TOOLS_SECTION) { @@ -542,11 +548,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ private afterConfigDispatch(changed: Omit): void { if (changed.modelAlias !== undefined) { - const model = this.tryResolveRawModel(); - this.telemetry.setContext({ - provider_type: model?.providerType ?? model?.protocol, - protocol: model?.protocol, - }); + this.syncTelemetryModelContext(changed.modelAlias); } if (changed.modelAlias !== undefined || changed.thinkingLevel !== undefined) { this.warnAboutAnthropicThinkingEffort(); @@ -556,6 +558,18 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ ); } + private syncTelemetryModelContext(modelAlias: string | undefined): void { + if (modelAlias === undefined) { + return; + } + const model = this.tryResolveRawModel(); + this.telemetry.setContext({ + model: modelAlias, + provider_type: model?.providerType ?? model?.protocol, + protocol: model?.protocol, + }); + } + private warnAboutAnthropicThinkingEffort(): void { try { const model = this.tryResolveRawModel(); diff --git a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts index b247edb7252..51fd7fc1471 100644 --- a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts +++ b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts @@ -1082,6 +1082,7 @@ describe('FullCompaction', () => { from: 'compacting', trace_id: 'trace-compact-retry', mode: 'agent', + model: 'kimi-code', protocol: 'openai', provider_type: 'kimi', }, 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 ff60cb44cb7..afd2147123f 100644 --- a/packages/agent-core-v2/test/agent/loop/loop.test.ts +++ b/packages/agent-core-v2/test/agent/loop/loop.test.ts @@ -1061,6 +1061,7 @@ describe('turn telemetry', () => { turn_id: 0, agent_id: 'main', mode: 'agent', + model: 'mock-model', provider_type: 'kimi', protocol: 'openai', thinking_effort: 'off', diff --git a/packages/agent-core-v2/test/agent/permissionMode/setModeAndBroadcast.test.ts b/packages/agent-core-v2/test/agent/permissionMode/setModeAndBroadcast.test.ts index 9247a0abcd2..cbe59eb605c 100644 --- a/packages/agent-core-v2/test/agent/permissionMode/setModeAndBroadcast.test.ts +++ b/packages/agent-core-v2/test/agent/permissionMode/setModeAndBroadcast.test.ts @@ -26,7 +26,7 @@ describe('setModeAndBroadcast', () => { expect(ctx.get(IAgentPermissionModeService).mode).toBe('auto'); expect(records).toContainEqual({ event: 'afk_toggle', - properties: { agent_id: 'main', enabled: true, mode: 'agent', protocol: 'openai', provider_type: 'kimi' }, + properties: { agent_id: 'main', enabled: true, mode: 'agent', model: 'mock-model', protocol: 'openai', provider_type: 'kimi' }, }); }); @@ -40,11 +40,11 @@ describe('setModeAndBroadcast', () => { expect(ctx.get(IAgentPermissionModeService).mode).toBe('manual'); expect(records).toContainEqual({ event: 'yolo_toggle', - properties: { agent_id: 'main', enabled: true, mode: 'agent', protocol: 'openai', provider_type: 'kimi' }, + properties: { agent_id: 'main', enabled: true, mode: 'agent', model: 'mock-model', protocol: 'openai', provider_type: 'kimi' }, }); expect(records).toContainEqual({ event: 'yolo_toggle', - properties: { agent_id: 'main', enabled: false, mode: 'agent', protocol: 'openai', provider_type: 'kimi' }, + properties: { agent_id: 'main', enabled: false, mode: 'agent', model: 'mock-model', protocol: 'openai', provider_type: 'kimi' }, }); }); }); 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 41e3de24e3f..20d4c61d4ee 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,13 +3,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { IAgentLLMRequesterService } from '#/agent/llmRequester/llmRequester'; import { IAgentProfileService } from '#/agent/profile/profile'; +import { ITelemetryService } from '#/app/telemetry/telemetry'; import type { ModelRecord } from '#/kosong/model/model'; import { configServices, createTestAgent, + InMemoryWireRecordPersistence, llmGenerateServices, modelProviderOptionServices, telemetryServices, + wireRecordPersistenceServices, type TestAgentContext, } from '../../harness'; import { recordingTelemetry, type TelemetryRecord } from '../../app/telemetry/stubs'; @@ -162,12 +165,90 @@ describe('ConfigState model capabilities', () => { effort: 'low', from: 'off', mode: 'agent', + model: 'kimi-code/kimi-for-coding', protocol: 'openai', provider_type: 'kimi', }, }); }); + it('writes the bound model 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, + }, + }, + }; + + profile.update({ modelAlias: 'kimi-code/kimi-for-coding' }); + + expect(ctx.get(ITelemetryService).getContext()).toMatchObject({ + model: 'kimi-code/kimi-for-coding', + provider_type: 'kimi', + protocol: 'openai', + }); + }); + + it('keeps the alias as ambient model when the bound model does not resolve', () => { + profile.update({ modelAlias: 'ghost/model' }); + + expect(ctx.get(ITelemetryService).getContext()).toMatchObject({ + model: 'ghost/model', + }); + }); + + it('restores the ambient model after a cold resume', async () => { + 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, + }, + }, + }; + const resumedRecords: TelemetryRecord[] = []; + const resumed = createTestAgent( + { autoConfigure: false }, + configServices(() => kimiConfig), + llmGenerateServices((...args) => generate(...args)), + telemetryServices(recordingTelemetry(resumedRecords)), + wireRecordPersistenceServices( + new InMemoryWireRecordPersistence([ + { type: 'config.update', agentId: 'main', modelAlias: 'kimi-code/kimi-for-coding' }, + ]), + ), + ); + try { + await resumed.restorePersisted(); + + expect(resumed.get(ITelemetryService).getContext()).toMatchObject({ + model: 'kimi-code/kimi-for-coding', + provider_type: 'kimi', + protocol: 'openai', + }); + } finally { + await resumed.dispose(); + } + }); + it('does not infer Kimi capabilities from the provider catalogue', () => { kimiConfig = { providers: { diff --git a/packages/agent-core-v2/test/agent/task/rpc-events.test.ts b/packages/agent-core-v2/test/agent/task/rpc-events.test.ts index 666aa8a55ea..e277c84b74a 100644 --- a/packages/agent-core-v2/test/agent/task/rpc-events.test.ts +++ b/packages/agent-core-v2/test/agent/task/rpc-events.test.ts @@ -327,6 +327,7 @@ describe('AgentTaskService — event emission', () => { task_id: taskId, kind: 'bash', mode: 'agent', + model: 'mock-model', protocol: 'openai', provider_type: 'kimi', }, @@ -356,6 +357,7 @@ describe('AgentTaskService — event emission', () => { task_id: taskId, kind: 'agent', mode: 'agent', + model: 'mock-model', protocol: 'openai', provider_type: 'kimi', }, diff --git a/packages/agent-core-v2/test/agent/undo/undo.test.ts b/packages/agent-core-v2/test/agent/undo/undo.test.ts index cfd16ee2d0f..d88b161d424 100644 --- a/packages/agent-core-v2/test/agent/undo/undo.test.ts +++ b/packages/agent-core-v2/test/agent/undo/undo.test.ts @@ -508,6 +508,7 @@ describe('AgentConversationUndoService', () => { agent_id: 'main', count: 1, mode: 'agent', + model: 'mock-model', protocol: 'openai', provider_type: 'kimi', }, @@ -582,6 +583,7 @@ describe('AgentConversationUndoService', () => { agent_id: 'main', count: 1, mode: 'agent', + model: 'mock-model', protocol: 'openai', provider_type: 'kimi', }, diff --git a/packages/agent-core-v2/test/features/goal/goal.test.ts b/packages/agent-core-v2/test/features/goal/goal.test.ts index a7849ddee53..d5c0a138a4a 100644 --- a/packages/agent-core-v2/test/features/goal/goal.test.ts +++ b/packages/agent-core-v2/test/features/goal/goal.test.ts @@ -622,6 +622,7 @@ describe('AgentGoalService', () => { actor: 'user', replace: true, mode: 'agent', + model: 'mock-model', protocol: 'openai', provider_type: 'kimi', }); diff --git a/packages/agent-core-v2/test/features/plan/tools/plan-tools-telemetry.test.ts b/packages/agent-core-v2/test/features/plan/tools/plan-tools-telemetry.test.ts index 61cfc855f7c..15ba029888f 100644 --- a/packages/agent-core-v2/test/features/plan/tools/plan-tools-telemetry.test.ts +++ b/packages/agent-core-v2/test/features/plan/tools/plan-tools-telemetry.test.ts @@ -256,6 +256,7 @@ describe('AgentPlanService EnterPlanMode telemetry', () => { properties: { agent_id: 'main', mode: 'plan', + model: 'mock-model', outcome: 'auto_approved', protocol: 'openai', provider_type: 'kimi',