diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.test.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.test.ts index 88ccee49a..2ed491ccd 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.test.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.test.ts @@ -532,6 +532,103 @@ describe('handleDialogTurnFailed', () => { }); }); +async function startStreamingMachine(): Promise { + await stateMachineManager.transition('session-1', SessionExecutionEvent.START, { + taskId: 'session-1', + dialogTurnId: 'turn-1', + }); +} + +describe('handleModelRoundStart', () => { + beforeEach(() => { + vi.restoreAllMocks(); + resetFlowChatStore(); + stateMachineManager.clear(); + }); + + afterEach(() => { + resetFlowChatStore(); + stateMachineManager.clear(); + }); + + it('creates a model round even when model identity fields are absent (external ACP agents)', async () => { + createSessionWithTurn({ + id: 'turn-1', + sessionId: 'session-1', + userMessage: { + id: 'user-1', + content: 'Initial request', + timestamp: 900, + }, + modelRounds: [], + status: 'processing', + startTime: 900, + }); + await startStreamingMachine(); + const context = createFlowChatContext(); + + expect(() => + __test_only__.handleModelRoundStart(context, { + sessionId: 'session-1', + turnId: 'turn-1', + roundId: 'round-1', + roundIndex: 0, + } as any), + ).not.toThrow(); + + const turn = FlowChatStore.getInstance() + .getState() + .sessions.get('session-1') + ?.dialogTurns.find(item => item.id === 'turn-1'); + + expect(turn?.modelRounds).toHaveLength(1); + expect(turn?.modelRounds[0]).toMatchObject({ + id: 'round-1', + index: 0, + isStreaming: true, + }); + expect(turn?.modelRounds[0]?.modelConfigId).toBeUndefined(); + expect(turn?.modelRounds[0]?.effectiveModelName).toBeUndefined(); + }); + + it('trims and stores model identity fields when present', async () => { + createSessionWithTurn({ + id: 'turn-1', + sessionId: 'session-1', + userMessage: { + id: 'user-1', + content: 'Initial request', + timestamp: 900, + }, + modelRounds: [], + status: 'processing', + startTime: 900, + }); + await startStreamingMachine(); + const context = createFlowChatContext(); + + __test_only__.handleModelRoundStart(context, { + sessionId: 'session-1', + turnId: 'turn-1', + roundId: 'round-1', + roundIndex: 0, + modelConfigId: ' config-1 ', + effectiveModelName: ' gpt-4o ', + } as any); + + const turn = FlowChatStore.getInstance() + .getState() + .sessions.get('session-1') + ?.dialogTurns.find(item => item.id === 'turn-1'); + + expect(turn?.modelRounds).toHaveLength(1); + expect(turn?.modelRounds[0]).toMatchObject({ + modelConfigId: 'config-1', + effectiveModelName: 'gpt-4o', + }); + }); +}); + function resetFlowChatStore(): void { FlowChatStore.getInstance().setState(() => ({ sessions: new Map(), diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts index 024edc331..a4d2608c9 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts @@ -160,6 +160,7 @@ export const __test_only__ = { handleDialogTurnStarted, handleDialogTurnFailed, handleSubagentSessionLinked, + handleModelRoundStart, }; function shouldMarkUnreadCompletion(sessionId: string): boolean { @@ -1906,9 +1907,6 @@ function handleModelRoundStart(context: FlowChatContext, event: ModelRoundStarte event.renderHints?.disableExploreGrouping === true || event.metadata?.disableExploreGrouping === true || event.disableExploreGrouping === true; - const modelConfigId = event.modelConfigId.trim(); - const effectiveModelName = event.effectiveModelName.trim(); - const modelRound: ModelRound = { id: roundId, index: roundIndex || 0, @@ -1918,8 +1916,9 @@ function handleModelRoundStart(context: FlowChatContext, event: ModelRoundStarte isComplete: false, status: 'streaming', startTime: Date.now(), - modelConfigId, - effectiveModelName, + // Model identity is optional: external ACP agents carry none. + ...(event.modelConfigId ? { modelConfigId: event.modelConfigId.trim() } : {}), + ...(event.effectiveModelName ? { effectiveModelName: event.effectiveModelName.trim() } : {}), ...(disableExploreGrouping ? { renderHints: { disableExploreGrouping: true } } : {}), @@ -1931,12 +1930,12 @@ function handleModelRoundStart(context: FlowChatContext, event: ModelRoundStarte const linkedParentInfo = findSubagentParentInfoByRound(sessionId, turnId) || getLinkedSubagentParentInfo(sessionId); - if (linkedParentInfo && effectiveModelName) { + if (linkedParentInfo && modelRound.effectiveModelName) { updateSubagentParentTaskModel( context, linkedParentInfo, - modelConfigId, - effectiveModelName, + modelRound.modelConfigId, + modelRound.effectiveModelName, ); } diff --git a/src/web-ui/src/infrastructure/api/service-api/AgentAPI.ts b/src/web-ui/src/infrastructure/api/service-api/AgentAPI.ts index 9b6b2559a..2479933c0 100644 --- a/src/web-ui/src/infrastructure/api/service-api/AgentAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/AgentAPI.ts @@ -478,9 +478,9 @@ export interface ModelRoundCompletedEvent extends AgenticEvent { durationMs?: number; providerId?: string; /** Resolved AI model configuration ID. */ - modelConfigId: string; + modelConfigId?: string; /** Provider model name sent on the request. */ - effectiveModelName: string; + effectiveModelName?: string; firstChunkMs?: number; firstVisibleOutputMs?: number; streamDurationMs?: number; @@ -501,9 +501,9 @@ export interface ModelRoundStartedEvent extends AgenticEvent { roundGroupId?: string; roundIndex: number; /** Resolved AI model configuration ID. */ - modelConfigId: string; + modelConfigId?: string; /** Provider model name sent on the request. */ - effectiveModelName: string; + effectiveModelName?: string; } export interface AcpContextUsageUpdatedEvent extends AgenticEvent {