Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,103 @@ describe('handleDialogTurnFailed', () => {
});
});

async function startStreamingMachine(): Promise<void> {
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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export const __test_only__ = {
handleDialogTurnStarted,
handleDialogTurnFailed,
handleSubagentSessionLinked,
handleModelRoundStart,
};

function shouldMarkUnreadCompletion(sessionId: string): boolean {
Expand Down Expand Up @@ -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,
Expand All @@ -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 } }
: {}),
Expand All @@ -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,
);
}

Expand Down
8 changes: 4 additions & 4 deletions src/web-ui/src/infrastructure/api/service-api/AgentAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down