From ef0aa08511200b6a68c0e99c97b57546e571cabe Mon Sep 17 00:00:00 2001 From: guantw Date: Fri, 31 Jul 2026 21:25:58 +0800 Subject: [PATCH] fix(web-ui): tolerate missing model identity in model-round events The ACP client emitter builds agentic://model-round-started payloads by hand and does not carry modelConfigId/effectiveModelName, which the native projection always provides. Since 4d7358556 the UI treated these fields as required and called .trim() unconditionally, so external ACP agent rounds threw on start, no ModelRound was created, and subsequent text-chunk/tool events were dropped - leaving the transcript empty while the agent kept working. Make both fields optional on the event contracts and omit them from the round when absent instead of fabricating placeholder values. The subagent parent task model update now reads from the created round and only runs when a model name exists. --- .../EventHandlerModule.test.ts | 97 +++++++++++++++++++ .../flow-chat-manager/EventHandlerModule.ts | 15 ++- .../api/service-api/AgentAPI.ts | 8 +- 3 files changed, 108 insertions(+), 12 deletions(-) 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 88ccee49a3..2ed491ccd8 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 024edc331e..a4d2608c9f 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 9b6b2559a8..2479933c00 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 {