mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
Session-Level Conversation History Management (#1113)
This commit is contained in:
137
packages/cli/src/acp-integration/session/HistoryReplayer.ts
Normal file
137
packages/cli/src/acp-integration/session/HistoryReplayer.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { ChatRecord } from '@qwen-code/qwen-code-core';
|
||||
import type { Content } from '@google/genai';
|
||||
import type { SessionContext } from './types.js';
|
||||
import { MessageEmitter } from './emitters/MessageEmitter.js';
|
||||
import { ToolCallEmitter } from './emitters/ToolCallEmitter.js';
|
||||
|
||||
/**
|
||||
* Handles replaying session history on session load.
|
||||
*
|
||||
* Uses the unified emitters to ensure consistency with normal flow.
|
||||
* This ensures that replayed history looks identical to how it would
|
||||
* have appeared during the original session.
|
||||
*/
|
||||
export class HistoryReplayer {
|
||||
private readonly messageEmitter: MessageEmitter;
|
||||
private readonly toolCallEmitter: ToolCallEmitter;
|
||||
|
||||
constructor(ctx: SessionContext) {
|
||||
this.messageEmitter = new MessageEmitter(ctx);
|
||||
this.toolCallEmitter = new ToolCallEmitter(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays all chat records from a loaded session.
|
||||
*
|
||||
* @param records - Array of chat records to replay
|
||||
*/
|
||||
async replay(records: ChatRecord[]): Promise<void> {
|
||||
for (const record of records) {
|
||||
await this.replayRecord(record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays a single chat record.
|
||||
*/
|
||||
private async replayRecord(record: ChatRecord): Promise<void> {
|
||||
switch (record.type) {
|
||||
case 'user':
|
||||
if (record.message) {
|
||||
await this.replayContent(record.message, 'user');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'assistant':
|
||||
if (record.message) {
|
||||
await this.replayContent(record.message, 'assistant');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_result':
|
||||
await this.replayToolResult(record);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Skip system records (compression, telemetry, slash commands)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays content from a message (user or assistant).
|
||||
* Handles text parts, thought parts, and function calls.
|
||||
*/
|
||||
private async replayContent(
|
||||
content: Content,
|
||||
role: 'user' | 'assistant',
|
||||
): Promise<void> {
|
||||
for (const part of content.parts ?? []) {
|
||||
// Text content
|
||||
if ('text' in part && part.text) {
|
||||
const isThought = (part as { thought?: boolean }).thought ?? false;
|
||||
await this.messageEmitter.emitMessage(part.text, role, isThought);
|
||||
}
|
||||
|
||||
// Function call (tool start)
|
||||
if ('functionCall' in part && part.functionCall) {
|
||||
const functionName = part.functionCall.name ?? '';
|
||||
const callId = part.functionCall.id ?? `${functionName}-${Date.now()}`;
|
||||
|
||||
await this.toolCallEmitter.emitStart({
|
||||
toolName: functionName,
|
||||
callId,
|
||||
args: part.functionCall.args as Record<string, unknown>,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays a tool result record.
|
||||
*/
|
||||
private async replayToolResult(record: ChatRecord): Promise<void> {
|
||||
// message is required - skip if not present
|
||||
if (!record.message?.parts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = record.toolCallResult;
|
||||
const callId = result?.callId ?? record.uuid;
|
||||
|
||||
// Extract tool name from the function response in message if available
|
||||
const toolName = this.extractToolNameFromRecord(record);
|
||||
|
||||
await this.toolCallEmitter.emitResult({
|
||||
toolName,
|
||||
callId,
|
||||
success: !result?.error,
|
||||
message: record.message.parts,
|
||||
resultDisplay: result?.resultDisplay,
|
||||
// For TodoWriteTool fallback, try to extract args from the record
|
||||
// Note: args aren't stored in tool_result records by default
|
||||
args: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts tool name from a chat record's function response.
|
||||
*/
|
||||
private extractToolNameFromRecord(record: ChatRecord): string {
|
||||
// Try to get from functionResponse in message
|
||||
if (record.message?.parts) {
|
||||
for (const part of record.message.parts) {
|
||||
if ('functionResponse' in part && part.functionResponse?.name) {
|
||||
return part.functionResponse.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user