mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-31 17:03:24 +00:00
🧭 fix: Re-Anchor Parent Activity Phase Bounds (#14741)
* test: cover parent activity phase finalization * test(e2e): stabilize parent phase coverage * fix(agents): reanchor parent activity phase bounds * fix(agents): preserve delayed tools in activity phases * test(agents): keep phase slice bounds typed * fix(agents): preserve sparse activity phase bounds * test(e2e): read structured phase replies
This commit is contained in:
parent
ba29a6c5d6
commit
236ee6c1ab
9 changed files with 645 additions and 20 deletions
|
|
@ -2287,6 +2287,23 @@ class AgentClient extends BaseClient {
|
|||
if (!Array.isArray(previousParts) || !Array.isArray(this.contentParts)) {
|
||||
return;
|
||||
}
|
||||
/** Preserve sparse coordinates when completion did not actually reshape
|
||||
* the content. A phase can reserve a leading hole for a tool part whose
|
||||
* SDK event lands after the phase closes; scanning retained identities
|
||||
* in an unchanged array would skip that hole and move the bound past the
|
||||
* delayed tool before it arrives. */
|
||||
if (previousParts.length === this.contentParts.length) {
|
||||
let unchanged = true;
|
||||
for (let index = 0; index < previousParts.length; index += 1) {
|
||||
if (previousParts[index] !== this.contentParts[index]) {
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (unchanged) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const retainedIndexes = new Map();
|
||||
for (let index = 0; index < this.contentParts.length; index += 1) {
|
||||
const part = this.contentParts[index];
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ describe('AgentClient - applyHideSequentialOutputsFilter', () => {
|
|||
expect(phase.activity_start_index).toBe(1);
|
||||
});
|
||||
|
||||
it('rebases phase bounds over sparse content without treating holes as retained parts', () => {
|
||||
it('rebases phase bounds over reshaped sparse content without retaining holes', () => {
|
||||
const reasoning = { type: ContentTypes.THINK, think: 'planning' };
|
||||
const toolCall = toolCallPart('tc-sparse');
|
||||
const phase = {
|
||||
|
|
@ -203,12 +203,56 @@ describe('AgentClient - applyHideSequentialOutputsFilter', () => {
|
|||
contentParts[3] = phase;
|
||||
contentParts[4] = final;
|
||||
const previousParts = [...contentParts];
|
||||
const ctx = { options: { agent: {} }, contentParts };
|
||||
const ctx = { options: { agent: {} }, contentParts: [toolCall, phase, final] };
|
||||
|
||||
expect(() =>
|
||||
AgentClient.prototype.rebaseActivityPhaseBounds.call(ctx, previousParts),
|
||||
).not.toThrow();
|
||||
expect(phase.activity_start_index).toBe(2);
|
||||
expect(phase.activity_start_index).toBe(0);
|
||||
});
|
||||
|
||||
it('preserves a sparse phase reservation when completion does not reshape content', () => {
|
||||
const firstTool = toolCallPart('tool-1');
|
||||
const secondTool = toolCallPart('tool-2');
|
||||
const firstLabel = {
|
||||
type: ContentTypes.ACTIVITY_LABEL,
|
||||
activity_label: 'Recorded the first result',
|
||||
tool_call_ids: ['tool-1'],
|
||||
};
|
||||
const secondLabel = {
|
||||
type: ContentTypes.ACTIVITY_LABEL,
|
||||
activity_label: 'Recorded the second result',
|
||||
tool_call_ids: ['tool-2'],
|
||||
};
|
||||
const phase = {
|
||||
type: ContentTypes.ACTIVITY_LABEL,
|
||||
activity_label: 'Verified both results',
|
||||
activity_label_type: 'phase',
|
||||
activity_start_index: 0,
|
||||
};
|
||||
const final = { type: ContentTypes.TEXT, text: 'Final answer', phase: 'final_answer' };
|
||||
const contentParts = [];
|
||||
contentParts[1] = { type: ContentTypes.TEXT, text: '', phase: 'final_answer' };
|
||||
contentParts[2] = firstLabel;
|
||||
contentParts[3] = secondTool;
|
||||
contentParts[4] = secondLabel;
|
||||
contentParts[5] = phase;
|
||||
contentParts[6] = final;
|
||||
const previousParts = [...contentParts];
|
||||
const ctx = { options: { agent: {} }, contentParts };
|
||||
|
||||
AgentClient.prototype.rebaseActivityPhaseBounds.call(ctx, previousParts);
|
||||
expect(phase.activity_start_index).toBe(0);
|
||||
|
||||
contentParts[0] = firstTool;
|
||||
const phaseChildren = contentParts.slice(
|
||||
phase.activity_start_index,
|
||||
contentParts.indexOf(phase),
|
||||
);
|
||||
expect(phaseChildren.map((part) => part?.tool_call?.id).filter(Boolean)).toEqual([
|
||||
'tool-1',
|
||||
'tool-2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue