Fix live activity streaming and replay summary

This commit is contained in:
wuyang6
2026-05-13 20:58:35 +08:00
parent 0eb57f3f42
commit d7c62a4929
4 changed files with 59 additions and 4 deletions
+17 -1
View File
@@ -1901,7 +1901,14 @@ def create_app(state: AgentState) -> FastAPI:
break break
yield json.dumps(item, ensure_ascii=False) + '\n' yield json.dumps(item, ensure_ascii=False) + '\n'
return StreamingResponse(generate(), media_type='application/x-ndjson') return StreamingResponse(
generate(),
media_type='application/x-ndjson',
headers={
'Cache-Control': 'no-cache, no-transform',
'X-Accel-Buffering': 'no',
},
)
@app.post('/api/clear') @app.post('/api/clear')
async def clear_state(account_id: str | None = None) -> dict[str, Any]: async def clear_state(account_id: str | None = None) -> dict[str, Any]:
@@ -2117,12 +2124,17 @@ def _interrupted_session_message(status: str) -> str:
def _runtime_event_stage(event: dict[str, object]) -> str: def _runtime_event_stage(event: dict[str, object]) -> str:
event_type = event.get('type') event_type = event.get('type')
if event_type == 'content_delta':
return ''
if event_type == 'tool_start': if event_type == 'tool_start':
stage_note = str(event.get('assistant_content') or '').strip() stage_note = str(event.get('assistant_content') or '').strip()
if stage_note.startswith(('进度:', '进度:')): if stage_note.startswith(('进度:', '进度:')):
return stage_note return stage_note
tool_name = str(event.get('tool_name') or '').strip() tool_name = str(event.get('tool_name') or '').strip()
return f'调用工具 {tool_name}' if tool_name else '正在调用工具' return f'调用工具 {tool_name}' if tool_name else '正在调用工具'
if event_type == 'tool_delta':
tool_name = str(event.get('tool_name') or '').strip()
return f'{tool_name} 输出中' if tool_name else '工具输出中'
if event_type == 'tool_result': if event_type == 'tool_result':
tool_name = str(event.get('tool_name') or '').strip() tool_name = str(event.get('tool_name') or '').strip()
return f'工具完成 {tool_name}' if tool_name else '工具调用完成' return f'工具完成 {tool_name}' if tool_name else '工具调用完成'
@@ -2149,7 +2161,9 @@ def _runtime_event_stage(event: dict[str, object]) -> str:
_RUN_EVENT_TYPES = { _RUN_EVENT_TYPES = {
'run_queued', 'run_queued',
'run_started', 'run_started',
'content_delta',
'tool_start', 'tool_start',
'tool_delta',
'tool_result', 'tool_result',
'final_text_start', 'final_text_start',
'final_text_end', 'final_text_end',
@@ -2175,8 +2189,10 @@ def _normalize_run_event(event: dict[str, object]) -> dict[str, Any] | None:
'tool_name', 'tool_name',
'tool_call_id', 'tool_call_id',
'arguments', 'arguments',
'delta',
'assistant_content', 'assistant_content',
'message_id', 'message_id',
'stream',
'ok', 'ok',
'metadata', 'metadata',
'reason', 'reason',
+13 -2
View File
@@ -56,6 +56,7 @@ type StreamState = {
phase: "waiting" | "queued" | "running" | "done"; phase: "waiting" | "queued" | "running" | "done";
runId?: string; runId?: string;
announcedToolCallKeys: Set<string>; announcedToolCallKeys: Set<string>;
streamedToolCallIds: Set<string>;
}; };
type ChatRequestBody = { type ChatRequestBody = {
@@ -112,6 +113,7 @@ export async function POST(req: Request) {
textStreamed: false, textStreamed: false,
phase: "waiting", phase: "waiting",
announcedToolCallKeys: new Set<string>(), announcedToolCallKeys: new Set<string>(),
streamedToolCallIds: new Set<string>(),
}; };
writer.write({ type: "start" }); writer.write({ type: "start" });
writer.write({ type: "start-step" }); writer.write({ type: "start-step" });
@@ -142,7 +144,7 @@ export async function POST(req: Request) {
delta: `\n思考并执行完成,用时 ${formatDuration(elapsedMs)}`, delta: `\n思考并执行完成,用时 ${formatDuration(elapsedMs)}`,
}); });
writer.write({ type: "reasoning-end", id: "reasoning-1" }); writer.write({ type: "reasoning-end", id: "reasoning-1" });
writeToolTrace(writer, payload.transcript); writeToolTrace(writer, payload.transcript, streamState);
if (!streamState.textStreamed) { if (!streamState.textStreamed) {
writer.write({ type: "text-start", id: "text-1" }); writer.write({ type: "text-start", id: "text-1" });
writer.write({ type: "text-delta", id: "text-1", delta: text }); writer.write({ type: "text-delta", id: "text-1", delta: text });
@@ -183,7 +185,13 @@ export async function POST(req: Request) {
}, },
}); });
return createUIMessageStreamResponse({ stream }); return createUIMessageStreamResponse({
stream,
headers: {
"Cache-Control": "no-cache, no-transform",
"X-Accel-Buffering": "no",
},
});
} }
function formatDuration(ms: number) { function formatDuration(ms: number) {
@@ -199,6 +207,7 @@ function formatDuration(ms: number) {
function writeToolTrace( function writeToolTrace(
writer: UIMessageStreamWriter<UIMessage>, writer: UIMessageStreamWriter<UIMessage>,
transcript?: ClawTranscriptEntry[], transcript?: ClawTranscriptEntry[],
streamState?: StreamState,
) { ) {
if (!transcript?.length) return; if (!transcript?.length) return;
@@ -216,6 +225,7 @@ function writeToolTrace(
const toolCallId = call.id; const toolCallId = call.id;
const toolName = call.function?.name ?? call.name; const toolName = call.function?.name ?? call.name;
if (!toolCallId || !toolName) continue; if (!toolCallId || !toolName) continue;
if (streamState?.streamedToolCallIds.has(toolCallId)) continue;
writer.write({ writer.write({
type: "tool-input-available", type: "tool-input-available",
@@ -539,6 +549,7 @@ function writeRuntimeEvent(
} }
} }
if (event.type === "tool_start" && event.tool_call_id && event.tool_name) { if (event.type === "tool_start" && event.tool_call_id && event.tool_name) {
streamState?.streamedToolCallIds.add(event.tool_call_id);
writer.write({ writer.write({
type: "tool-input-available", type: "tool-input-available",
toolCallId: event.tool_call_id, toolCallId: event.tool_call_id,
@@ -979,6 +979,10 @@ function summarizeLiveRunEvents(
if (event.type === "run_started") { if (event.type === "run_started") {
lines.push("后端已开始执行本轮任务。"); lines.push("后端已开始执行本轮任务。");
} }
if (event.type === "content_delta" && event.delta) {
const note = event.delta.trim();
if (note && !isNoisyLiveDelta(note)) lines.push(note);
}
if (event.type === "tool_start") { if (event.type === "tool_start") {
const stageNote = const stageNote =
typeof event.assistant_content === "string" typeof event.assistant_content === "string"
@@ -994,6 +998,9 @@ function summarizeLiveRunEvents(
event.tool_name ? `工具完成 ${event.tool_name}` : "工具调用完成", event.tool_name ? `工具完成 ${event.tool_name}` : "工具调用完成",
); );
} }
if (event.type === "tool_delta") {
lines.push(event.tool_name ? `${event.tool_name} 输出中` : "工具输出中");
}
if (event.type === "final_text_start") { if (event.type === "final_text_start") {
lines.push("正在整理回复"); lines.push("正在整理回复");
} }
@@ -1011,6 +1018,10 @@ function summarizeLiveRunEvents(
return lines.slice(-40); return lines.slice(-40);
} }
function isNoisyLiveDelta(value: string) {
return value === "。" || value === "" || value === "," || value.length <= 1;
}
function collectActivityItems(messages: readonly MessageState[]) { function collectActivityItems(messages: readonly MessageState[]) {
const items: ActivityItem[] = []; const items: ActivityItem[] = [];
@@ -86,6 +86,7 @@ type ClawRunEvent = {
tool_call_id?: string; tool_call_id?: string;
arguments?: unknown; arguments?: unknown;
assistant_content?: string; assistant_content?: string;
delta?: string;
ok?: boolean; ok?: boolean;
metadata?: { metadata?: {
output_preview?: unknown; output_preview?: unknown;
@@ -430,7 +431,12 @@ export function toReplayRepository(
? shouldShowAssistantText ? shouldShowAssistantText
? [...toolParts, { type: "text", text: content }] ? [...toolParts, { type: "text", text: content }]
: toolParts : toolParts
: [{ type: "text", text: content }] : message.role === "assistant" && elapsedMs !== undefined
? [
{ type: "reasoning", text: "思考并执行完成。", elapsedMs },
{ type: "text", text: content },
]
: [{ type: "text", text: content }]
) as UIMessage["parts"]; ) as UIMessage["parts"];
const replayParts = withReplayElapsedMs(parts, elapsedMs); const replayParts = withReplayElapsedMs(parts, elapsedMs);
const uiMessage: UIMessage = { const uiMessage: UIMessage = {
@@ -599,6 +605,10 @@ function summarizeRunEvents(runStatus: ClawActiveRunStatus) {
if (event.type === "run_started") { if (event.type === "run_started") {
lines.push("后端已开始执行本轮任务。"); lines.push("后端已开始执行本轮任务。");
} }
if (event.type === "content_delta" && event.delta) {
const note = event.delta.trim();
if (note && !isNoisyLiveDelta(note)) lines.push(note);
}
if (event.type === "tool_start") { if (event.type === "tool_start") {
const stageNote = const stageNote =
typeof event.assistant_content === "string" typeof event.assistant_content === "string"
@@ -614,6 +624,9 @@ function summarizeRunEvents(runStatus: ClawActiveRunStatus) {
event.tool_name ? `工具完成 ${event.tool_name}` : "工具调用完成", event.tool_name ? `工具完成 ${event.tool_name}` : "工具调用完成",
); );
} }
if (event.type === "tool_delta") {
lines.push(event.tool_name ? `${event.tool_name} 输出中` : "工具输出中");
}
if (event.type === "final_text_start") { if (event.type === "final_text_start") {
lines.push("正在整理回复"); lines.push("正在整理回复");
} }
@@ -627,6 +640,10 @@ function summarizeRunEvents(runStatus: ClawActiveRunStatus) {
return lines.slice(-40); return lines.slice(-40);
} }
function isNoisyLiveDelta(value: string) {
return value === "。" || value === "" || value === "," || value.length <= 1;
}
function buildRunToolParts(events: readonly ClawRunEvent[]) { function buildRunToolParts(events: readonly ClawRunEvent[]) {
const resultById = new Map<string, ClawRunEvent>(); const resultById = new Map<string, ClawRunEvent>();
for (const event of events) { for (const event of events) {