diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 9e8b0604d6..0673f6a31a 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -2276,13 +2276,24 @@ class AgentClient extends BaseClient { * so prepended skill cards cannot enter a phase and a filtered-away leading * reasoning part advances the bound to the first retained child. * + * Both arrays may be sparse: the aggregator writes parts at provider-source + * indexes, which can skip slots. Holes must not enter the identity map — a + * hole reads as `undefined`, and one `undefined` key would falsely match + * every hole in `previousParts` as a retained part. + * * @param {Array} previousParts */ rebaseActivityPhaseBounds(previousParts) { if (!Array.isArray(previousParts) || !Array.isArray(this.contentParts)) { return; } - const retainedIndexes = new Map(this.contentParts.map((part, index) => [part, index])); + const retainedIndexes = new Map(); + for (let index = 0; index < this.contentParts.length; index += 1) { + const part = this.contentParts[index]; + if (part != null) { + retainedIndexes.set(part, index); + } + } for (let markerIndex = 0; markerIndex < this.contentParts.length; markerIndex += 1) { const marker = this.contentParts[markerIndex]; if ( diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index 872d05369a..5bfa1f6093 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -186,6 +186,30 @@ describe('AgentClient - applyHideSequentialOutputsFilter', () => { expect(ctx.contentParts).toEqual([skillCard, activityTool, phase, final]); expect(phase.activity_start_index).toBe(1); }); + + it('rebases phase bounds over sparse content without treating holes as retained parts', () => { + const reasoning = { type: ContentTypes.THINK, think: 'planning' }; + const toolCall = toolCallPart('tc-sparse'); + const phase = { + type: ContentTypes.ACTIVITY_LABEL, + activity_label: 'Searched for tools', + activity_label_type: 'phase', + activity_start_index: 1, + }; + const final = textPart('answer'); + const contentParts = []; + contentParts[0] = reasoning; + contentParts[2] = toolCall; + contentParts[3] = phase; + contentParts[4] = final; + const previousParts = [...contentParts]; + const ctx = { options: { agent: {} }, contentParts }; + + expect(() => + AgentClient.prototype.rebaseActivityPhaseBounds.call(ctx, previousParts), + ).not.toThrow(); + expect(phase.activity_start_index).toBe(2); + }); }); describe('AgentClient - startup telemetry', () => {