mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
🐛 fix(agent): handle Codex message snapshots
This commit is contained in:
@@ -121,6 +121,12 @@ export interface StreamChunkData {
|
|||||||
| 'content_part'
|
| 'content_part'
|
||||||
| 'reasoning_part';
|
| 'reasoning_part';
|
||||||
content?: string;
|
content?: string;
|
||||||
|
/**
|
||||||
|
* Defaults to `delta`.
|
||||||
|
* `snapshot` means `content` is the full current text for this stream step,
|
||||||
|
* not an append-only token delta.
|
||||||
|
*/
|
||||||
|
contentMode?: 'delta' | 'snapshot';
|
||||||
/** Multimodal content parts (text + images) */
|
/** Multimodal content parts (text + images) */
|
||||||
contentParts?: Array<{ text: string; type: 'text' } | { image: string; type: 'image' }>;
|
contentParts?: Array<{ text: string; type: 'text' } | { image: string; type: 'image' }>;
|
||||||
/** Grounding/search data */
|
/** Grounding/search data */
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ export type StreamChunkType =
|
|||||||
export interface StreamChunkData {
|
export interface StreamChunkData {
|
||||||
chunkType: StreamChunkType;
|
chunkType: StreamChunkType;
|
||||||
content?: string;
|
content?: string;
|
||||||
|
/**
|
||||||
|
* Defaults to `delta`.
|
||||||
|
* `snapshot` means `content` is the full current text for this stream step,
|
||||||
|
* not an append-only token delta.
|
||||||
|
*/
|
||||||
|
contentMode?: 'delta' | 'snapshot';
|
||||||
contentParts?: Array<{ text: string; type: 'text' } | { image: string; type: 'image' }>;
|
contentParts?: Array<{ text: string; type: 'text' } | { image: string; type: 'image' }>;
|
||||||
grounding?: any;
|
grounding?: any;
|
||||||
imageList?: any[];
|
imageList?: any[];
|
||||||
|
|||||||
@@ -43,7 +43,110 @@ describe('CodexAdapter', () => {
|
|||||||
type: 'stream_start',
|
type: 'stream_start',
|
||||||
});
|
});
|
||||||
expect(text[0]).toMatchObject({
|
expect(text[0]).toMatchObject({
|
||||||
data: { chunkType: 'text', content: 'hello from codex' },
|
data: { chunkType: 'text', content: 'hello from codex', contentMode: 'snapshot' },
|
||||||
|
type: 'stream_chunk',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats Codex agent message updates as replaceable text snapshots', () => {
|
||||||
|
const adapter = new CodexAdapter();
|
||||||
|
|
||||||
|
adapter.adapt({ type: 'turn.started' });
|
||||||
|
const draft = adapter.adapt({
|
||||||
|
item: {
|
||||||
|
id: 'item_0',
|
||||||
|
text: 'I will inspect the whole repository.',
|
||||||
|
type: 'agent_message',
|
||||||
|
},
|
||||||
|
type: 'item.updated',
|
||||||
|
});
|
||||||
|
const shortened = adapter.adapt({
|
||||||
|
item: {
|
||||||
|
id: 'item_0',
|
||||||
|
text: 'I will inspect the repo.',
|
||||||
|
type: 'agent_message',
|
||||||
|
},
|
||||||
|
type: 'item.updated',
|
||||||
|
});
|
||||||
|
const completed = adapter.adapt({
|
||||||
|
item: {
|
||||||
|
id: 'item_0',
|
||||||
|
text: 'I will inspect the repo first.',
|
||||||
|
type: 'agent_message',
|
||||||
|
},
|
||||||
|
type: 'item.completed',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(draft[0]).toMatchObject({
|
||||||
|
data: {
|
||||||
|
chunkType: 'text',
|
||||||
|
content: 'I will inspect the whole repository.',
|
||||||
|
contentMode: 'snapshot',
|
||||||
|
},
|
||||||
|
type: 'stream_chunk',
|
||||||
|
});
|
||||||
|
expect(shortened[0]).toMatchObject({
|
||||||
|
data: {
|
||||||
|
chunkType: 'text',
|
||||||
|
content: 'I will inspect the repo.',
|
||||||
|
contentMode: 'snapshot',
|
||||||
|
},
|
||||||
|
type: 'stream_chunk',
|
||||||
|
});
|
||||||
|
expect(completed[0]).toMatchObject({
|
||||||
|
data: {
|
||||||
|
chunkType: 'text',
|
||||||
|
content: 'I will inspect the repo first.',
|
||||||
|
contentMode: 'snapshot',
|
||||||
|
},
|
||||||
|
type: 'stream_chunk',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps prior agent message text when a later item updates in the same step', () => {
|
||||||
|
const adapter = new CodexAdapter();
|
||||||
|
|
||||||
|
adapter.adapt({ type: 'turn.started' });
|
||||||
|
adapter.adapt({
|
||||||
|
item: {
|
||||||
|
id: 'item_0',
|
||||||
|
text: 'First status update.',
|
||||||
|
type: 'agent_message',
|
||||||
|
},
|
||||||
|
type: 'item.completed',
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondDraft = adapter.adapt({
|
||||||
|
item: {
|
||||||
|
id: 'item_1',
|
||||||
|
text: 'Second draft.',
|
||||||
|
type: 'agent_message',
|
||||||
|
},
|
||||||
|
type: 'item.updated',
|
||||||
|
});
|
||||||
|
const secondRevision = adapter.adapt({
|
||||||
|
item: {
|
||||||
|
id: 'item_1',
|
||||||
|
text: 'Second revised status update.',
|
||||||
|
type: 'agent_message',
|
||||||
|
},
|
||||||
|
type: 'item.updated',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(secondDraft[0]).toMatchObject({
|
||||||
|
data: {
|
||||||
|
chunkType: 'text',
|
||||||
|
content: 'First status update.\n\nSecond draft.',
|
||||||
|
contentMode: 'snapshot',
|
||||||
|
},
|
||||||
|
type: 'stream_chunk',
|
||||||
|
});
|
||||||
|
expect(secondRevision[0]).toMatchObject({
|
||||||
|
data: {
|
||||||
|
chunkType: 'text',
|
||||||
|
content: 'First status update.\n\nSecond revised status update.',
|
||||||
|
contentMode: 'snapshot',
|
||||||
|
},
|
||||||
type: 'stream_chunk',
|
type: 'stream_chunk',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -231,7 +334,11 @@ describe('CodexAdapter', () => {
|
|||||||
|
|
||||||
expect(secondMessage).toHaveLength(1);
|
expect(secondMessage).toHaveLength(1);
|
||||||
expect(secondMessage[0]).toMatchObject({
|
expect(secondMessage[0]).toMatchObject({
|
||||||
data: { chunkType: 'text', content: '\n\nSecond status update.' },
|
data: {
|
||||||
|
chunkType: 'text',
|
||||||
|
content: 'First status update.\n\nSecond status update.',
|
||||||
|
contentMode: 'snapshot',
|
||||||
|
},
|
||||||
stepIndex: 0,
|
stepIndex: 0,
|
||||||
type: 'stream_chunk',
|
type: 'stream_chunk',
|
||||||
});
|
});
|
||||||
@@ -289,7 +396,11 @@ describe('CodexAdapter', () => {
|
|||||||
|
|
||||||
expect(nextMessage).toHaveLength(1);
|
expect(nextMessage).toHaveLength(1);
|
||||||
expect(nextMessage[0]).toMatchObject({
|
expect(nextMessage[0]).toMatchObject({
|
||||||
data: { chunkType: 'text', content: '\n\nThe broad search is done; continuing.' },
|
data: {
|
||||||
|
chunkType: 'text',
|
||||||
|
content: 'Continuing with narrower checks.\n\nThe broad search is done; continuing.',
|
||||||
|
contentMode: 'snapshot',
|
||||||
|
},
|
||||||
stepIndex: 1,
|
stepIndex: 1,
|
||||||
type: 'stream_chunk',
|
type: 'stream_chunk',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -522,9 +522,17 @@ const getCodexTerminalErrorStderr = (raw: any): string | undefined => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAgentMessageText = (item: unknown): string | undefined => {
|
||||||
|
if (!isRecord(item)) return;
|
||||||
|
const text = item.text;
|
||||||
|
return typeof text === 'string' ? text : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export class CodexAdapter implements AgentEventAdapter {
|
export class CodexAdapter implements AgentEventAdapter {
|
||||||
private currentAgentMessageItemId?: string;
|
private currentAgentMessageItemId?: string;
|
||||||
|
private currentAgentMessageText = '';
|
||||||
private currentModel?: string;
|
private currentModel?: string;
|
||||||
|
private currentStepText = '';
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
|
||||||
private hasTextInCurrentStep = false;
|
private hasTextInCurrentStep = false;
|
||||||
@@ -563,6 +571,9 @@ export class CodexAdapter implements AgentEventAdapter {
|
|||||||
case 'item.started': {
|
case 'item.started': {
|
||||||
return this.handleItemStarted(raw.item);
|
return this.handleItemStarted(raw.item);
|
||||||
}
|
}
|
||||||
|
case 'item.updated': {
|
||||||
|
return this.handleItemUpdated(raw.item);
|
||||||
|
}
|
||||||
case 'item.completed': {
|
case 'item.completed': {
|
||||||
return this.handleItemCompleted(raw.item);
|
return this.handleItemCompleted(raw.item);
|
||||||
}
|
}
|
||||||
@@ -638,6 +649,8 @@ export class CodexAdapter implements AgentEventAdapter {
|
|||||||
|
|
||||||
private handleTurnStarted(): HeterogeneousAgentEvent[] {
|
private handleTurnStarted(): HeterogeneousAgentEvent[] {
|
||||||
this.currentAgentMessageItemId = undefined;
|
this.currentAgentMessageItemId = undefined;
|
||||||
|
this.currentAgentMessageText = '';
|
||||||
|
this.currentStepText = '';
|
||||||
this.hasTextInCurrentStep = false;
|
this.hasTextInCurrentStep = false;
|
||||||
this.hasToolActivitySinceAgentMessage = false;
|
this.hasToolActivitySinceAgentMessage = false;
|
||||||
this.resetStepToolCalls();
|
this.resetStepToolCalls();
|
||||||
@@ -666,42 +679,16 @@ export class CodexAdapter implements AgentEventAdapter {
|
|||||||
return this.emitToolChunk(tool);
|
return this.emitToolChunk(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleItemUpdated(item: any): HeterogeneousAgentEvent[] {
|
||||||
|
if (item?.type === 'agent_message') return this.handleAgentMessageItem(item);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
private handleItemCompleted(item: any): HeterogeneousAgentEvent[] {
|
private handleItemCompleted(item: any): HeterogeneousAgentEvent[] {
|
||||||
if (!item?.type) return [];
|
if (!item?.type) return [];
|
||||||
|
|
||||||
if (item.type === 'agent_message') {
|
if (item.type === 'agent_message') {
|
||||||
if (!item.text) return [];
|
return this.handleAgentMessageItem(item);
|
||||||
|
|
||||||
const events: HeterogeneousAgentEvent[] = [];
|
|
||||||
const shouldStartNewStep =
|
|
||||||
this.hasToolActivitySinceAgentMessage &&
|
|
||||||
!!item.id &&
|
|
||||||
item.id !== this.currentAgentMessageItemId;
|
|
||||||
|
|
||||||
if (shouldStartNewStep) {
|
|
||||||
this.stepIndex += 1;
|
|
||||||
this.resetStepToolCalls();
|
|
||||||
this.hasTextInCurrentStep = false;
|
|
||||||
events.push(this.makeEvent('stream_end', {}));
|
|
||||||
events.push(this.makeEvent('stream_start', this.getStreamStartData({ newStep: true })));
|
|
||||||
}
|
|
||||||
|
|
||||||
const content =
|
|
||||||
this.hasTextInCurrentStep && item.id !== this.currentAgentMessageItemId
|
|
||||||
? `\n\n${item.text}`
|
|
||||||
: item.text;
|
|
||||||
|
|
||||||
this.currentAgentMessageItemId = item.id;
|
|
||||||
this.hasTextInCurrentStep = true;
|
|
||||||
this.hasToolActivitySinceAgentMessage = false;
|
|
||||||
events.push(
|
|
||||||
this.makeEvent('stream_chunk', {
|
|
||||||
chunkType: 'text',
|
|
||||||
content,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return events;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!item.id) return [];
|
if (!item.id) return [];
|
||||||
@@ -731,6 +718,50 @@ export class CodexAdapter implements AgentEventAdapter {
|
|||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleAgentMessageItem(item: any): HeterogeneousAgentEvent[] {
|
||||||
|
const text = getAgentMessageText(item);
|
||||||
|
if (text === undefined) return [];
|
||||||
|
|
||||||
|
const events: HeterogeneousAgentEvent[] = [];
|
||||||
|
const isNewAgentMessageItem = !!item.id && item.id !== this.currentAgentMessageItemId;
|
||||||
|
const shouldStartNewStep = this.hasToolActivitySinceAgentMessage && isNewAgentMessageItem;
|
||||||
|
|
||||||
|
if (shouldStartNewStep) {
|
||||||
|
this.stepIndex += 1;
|
||||||
|
this.resetStepToolCalls();
|
||||||
|
this.currentAgentMessageText = '';
|
||||||
|
this.currentStepText = '';
|
||||||
|
this.hasTextInCurrentStep = false;
|
||||||
|
events.push(this.makeEvent('stream_end', {}));
|
||||||
|
events.push(this.makeEvent('stream_start', this.getStreamStartData({ newStep: true })));
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = this.hasTextInCurrentStep && isNewAgentMessageItem ? '\n\n' : '';
|
||||||
|
if (isNewAgentMessageItem) {
|
||||||
|
this.currentStepText = `${this.currentStepText}${separator}${text}`;
|
||||||
|
} else {
|
||||||
|
const prefixLength = Math.max(
|
||||||
|
0,
|
||||||
|
this.currentStepText.length - this.currentAgentMessageText.length,
|
||||||
|
);
|
||||||
|
this.currentStepText = `${this.currentStepText.slice(0, prefixLength)}${text}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentAgentMessageItemId = item.id;
|
||||||
|
this.currentAgentMessageText = text;
|
||||||
|
this.hasTextInCurrentStep = true;
|
||||||
|
this.hasToolActivitySinceAgentMessage = false;
|
||||||
|
events.push(
|
||||||
|
this.makeEvent('stream_chunk', {
|
||||||
|
chunkType: 'text',
|
||||||
|
content: this.currentStepText,
|
||||||
|
contentMode: 'snapshot',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
private drainPendingToolEndEvents(): HeterogeneousAgentEvent[] {
|
private drainPendingToolEndEvents(): HeterogeneousAgentEvent[] {
|
||||||
const events = [...this.pendingToolCalls].map((toolCallId) =>
|
const events = [...this.pendingToolCalls].map((toolCallId) =>
|
||||||
this.makeEvent('tool_end', {
|
this.makeEvent('tool_end', {
|
||||||
|
|||||||
@@ -166,6 +166,12 @@ export interface SubagentEventContext {
|
|||||||
export interface StreamChunkData {
|
export interface StreamChunkData {
|
||||||
chunkType: StreamChunkType;
|
chunkType: StreamChunkType;
|
||||||
content?: string;
|
content?: string;
|
||||||
|
/**
|
||||||
|
* Defaults to `delta`.
|
||||||
|
* `snapshot` means `content` is the full current text for this stream step,
|
||||||
|
* not an append-only token delta.
|
||||||
|
*/
|
||||||
|
contentMode?: 'delta' | 'snapshot';
|
||||||
reasoning?: string;
|
reasoning?: string;
|
||||||
/**
|
/**
|
||||||
* Subagent context for the entire chunk — peer to `toolsCalling`,
|
* Subagent context for the entire chunk — peer to `toolsCalling`,
|
||||||
|
|||||||
@@ -601,20 +601,35 @@ export class ResponsesService extends BaseService {
|
|||||||
if (event.type === 'stream_chunk') {
|
if (event.type === 'stream_chunk') {
|
||||||
const chunk = event.data as StreamChunkData;
|
const chunk = event.data as StreamChunkData;
|
||||||
|
|
||||||
if (chunk.chunkType === 'text' && chunk.content) {
|
if (
|
||||||
|
chunk.chunkType === 'text' &&
|
||||||
|
typeof chunk.content === 'string' &&
|
||||||
|
(chunk.content || chunk.contentMode === 'snapshot')
|
||||||
|
) {
|
||||||
// Start text message output item if not already started
|
// Start text message output item if not already started
|
||||||
yield* startTextMessage(seq);
|
yield* startTextMessage(seq);
|
||||||
|
|
||||||
accumulatedText += chunk.content;
|
const nextText =
|
||||||
yield {
|
chunk.contentMode === 'snapshot' ? chunk.content : accumulatedText + chunk.content;
|
||||||
content_index: 0,
|
const delta =
|
||||||
delta: chunk.content,
|
chunk.contentMode === 'snapshot'
|
||||||
item_id: currentTextItemId,
|
? nextText.startsWith(accumulatedText)
|
||||||
logprobs: [],
|
? nextText.slice(accumulatedText.length)
|
||||||
output_index: currentOutputIndex,
|
: ''
|
||||||
sequence_number: seq.n++,
|
: chunk.content;
|
||||||
type: 'response.output_text.delta' as const,
|
accumulatedText = nextText;
|
||||||
};
|
|
||||||
|
if (delta) {
|
||||||
|
yield {
|
||||||
|
content_index: 0,
|
||||||
|
delta,
|
||||||
|
item_id: currentTextItemId,
|
||||||
|
logprobs: [],
|
||||||
|
output_index: currentOutputIndex,
|
||||||
|
sequence_number: seq.n++,
|
||||||
|
type: 'response.output_text.delta' as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
} else if (chunk.chunkType === 'tools_calling' && chunk.toolsCalling) {
|
} else if (chunk.chunkType === 'tools_calling' && chunk.toolsCalling) {
|
||||||
// Close any open text message before emitting tool calls
|
// Close any open text message before emitting tool calls
|
||||||
yield* finishTextMessage(seq, accumulatedText);
|
yield* finishTextMessage(seq, accumulatedText);
|
||||||
|
|||||||
@@ -101,8 +101,13 @@ export const createMockStoreInjector = (get: () => ChatStore, params: MockStoreI
|
|||||||
const data = event.data as StreamChunkData | undefined;
|
const data = event.data as StreamChunkData | undefined;
|
||||||
if (!data) break;
|
if (!data) break;
|
||||||
|
|
||||||
if (data.chunkType === 'text' && data.content) {
|
if (
|
||||||
accumulatedContent += data.content;
|
data.chunkType === 'text' &&
|
||||||
|
typeof data.content === 'string' &&
|
||||||
|
(data.content || data.contentMode === 'snapshot')
|
||||||
|
) {
|
||||||
|
accumulatedContent =
|
||||||
|
data.contentMode === 'snapshot' ? data.content : accumulatedContent + data.content;
|
||||||
get().internal_dispatchMessage(
|
get().internal_dispatchMessage(
|
||||||
{
|
{
|
||||||
id: assistantMessageId,
|
id: assistantMessageId,
|
||||||
|
|||||||
@@ -193,6 +193,31 @@ describe('createGatewayEventHandler', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should replace text content for snapshot chunks', async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
const handler = createHandler(store);
|
||||||
|
|
||||||
|
handler(makeEvent('stream_chunk', { chunkType: 'text', content: 'Draft with tail' }));
|
||||||
|
handler(
|
||||||
|
makeEvent('stream_chunk', {
|
||||||
|
chunkType: 'text',
|
||||||
|
content: 'Draft',
|
||||||
|
contentMode: 'snapshot',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
handler(makeEvent('stream_chunk', { chunkType: 'text', content: ' final' }));
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(store.internal_dispatchMessage).toHaveBeenLastCalledWith(
|
||||||
|
{
|
||||||
|
id: 'msg-initial',
|
||||||
|
type: 'updateMessage',
|
||||||
|
value: { content: 'Draft final' },
|
||||||
|
},
|
||||||
|
{ operationId: 'op-1' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should accumulate reasoning content', async () => {
|
it('should accumulate reasoning content', async () => {
|
||||||
const store = createMockStore();
|
const store = createMockStore();
|
||||||
const handler = createHandler(store);
|
const handler = createHandler(store);
|
||||||
|
|||||||
@@ -356,6 +356,15 @@ const codexAgentMessage = (id: string, text: string) => ({
|
|||||||
type: 'item.completed',
|
type: 'item.completed',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const codexAgentMessageUpdated = (id: string, text: string) => ({
|
||||||
|
item: {
|
||||||
|
id,
|
||||||
|
text,
|
||||||
|
type: 'agent_message',
|
||||||
|
},
|
||||||
|
type: 'item.updated',
|
||||||
|
});
|
||||||
|
|
||||||
const codexCommandStarted = (id: string, command: string) => ({
|
const codexCommandStarted = (id: string, command: string) => ({
|
||||||
item: {
|
item: {
|
||||||
aggregated_output: '',
|
aggregated_output: '',
|
||||||
@@ -1322,6 +1331,38 @@ describe('heterogeneousAgentExecutor DB persistence', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should persist the latest Codex agent_message snapshot instead of appending snapshots', async () => {
|
||||||
|
const contentUpdates: Array<{ assistantId: string; content: string }> = [];
|
||||||
|
mockUpdateMessage.mockImplementation(async (id: string, val: any) => {
|
||||||
|
if (typeof val.content === 'string') {
|
||||||
|
contentUpdates.push({ assistantId: id, content: val.content });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await runWithEvents(
|
||||||
|
[
|
||||||
|
codexThreadStarted(),
|
||||||
|
codexTurnStarted(),
|
||||||
|
codexAgentMessageUpdated('item_0', 'Draft with stale tail'),
|
||||||
|
codexAgentMessageUpdated('item_0', 'Draft'),
|
||||||
|
codexAgentMessage('item_0', 'Draft final'),
|
||||||
|
codexTurnCompleted({ input_tokens: 10, output_tokens: 3 }),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
heterogeneousProvider: { command: 'codex', type: 'codex' as const },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
contentUpdates.findLast((update) => update.assistantId === 'ast-initial')?.content,
|
||||||
|
).toBe('Draft final');
|
||||||
|
expect(contentUpdates.map((update) => update.content)).not.toContain(
|
||||||
|
'Draft with stale tailDraftDraft final',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should switch to a new assistant before persisting the next turn tool', async () => {
|
it('should switch to a new assistant before persisting the next turn tool', async () => {
|
||||||
const idCounter = { assistant: 0, tool: 0 };
|
const idCounter = { assistant: 0, tool: 0 };
|
||||||
mockCreateMessage.mockImplementation(async (params: any) => {
|
mockCreateMessage.mockImplementation(async (params: any) => {
|
||||||
|
|||||||
@@ -350,11 +350,16 @@ export const createGatewayEventHandler = (
|
|||||||
const data = event.data as StreamChunkData | undefined;
|
const data = event.data as StreamChunkData | undefined;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
if (data.chunkType === 'text' && data.content) {
|
if (
|
||||||
|
data.chunkType === 'text' &&
|
||||||
|
typeof data.content === 'string' &&
|
||||||
|
(data.content || data.contentMode === 'snapshot')
|
||||||
|
) {
|
||||||
// Text after reasoning marks the end of the thinking pass — see
|
// Text after reasoning marks the end of the thinking pass — see
|
||||||
// `StreamingHandler.handleText` for the same transition.
|
// `StreamingHandler.handleText` for the same transition.
|
||||||
endReasoningIfNeeded();
|
endReasoningIfNeeded();
|
||||||
accumulatedContent += data.content;
|
accumulatedContent =
|
||||||
|
data.contentMode === 'snapshot' ? data.content : accumulatedContent + data.content;
|
||||||
hasStreamedContent = true;
|
hasStreamedContent = true;
|
||||||
get().internal_dispatchMessage(
|
get().internal_dispatchMessage(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1369,8 +1369,13 @@ export const executeHeterogeneousAgent = async (
|
|||||||
const mainAsstId = currentAssistantMessageId;
|
const mainAsstId = currentAssistantMessageId;
|
||||||
persistQueue = persistQueue.then(() => reduceAndApplySubagent(event, mainAsstId));
|
persistQueue = persistQueue.then(() => reduceAndApplySubagent(event, mainAsstId));
|
||||||
} else {
|
} else {
|
||||||
if (chunk?.chunkType === 'text' && chunk.content) {
|
if (
|
||||||
accumulatedContent += chunk.content;
|
chunk?.chunkType === 'text' &&
|
||||||
|
typeof chunk.content === 'string' &&
|
||||||
|
(chunk.content || chunk.contentMode === 'snapshot')
|
||||||
|
) {
|
||||||
|
accumulatedContent =
|
||||||
|
chunk.contentMode === 'snapshot' ? chunk.content : accumulatedContent + chunk.content;
|
||||||
}
|
}
|
||||||
if (chunk?.chunkType === 'reasoning' && chunk.reasoning) {
|
if (chunk?.chunkType === 'reasoning' && chunk.reasoning) {
|
||||||
accumulatedReasoning += chunk.reasoning;
|
accumulatedReasoning += chunk.reasoning;
|
||||||
|
|||||||
Reference in New Issue
Block a user