Fix live activity streaming and replay summary
This commit is contained in:
+17
-1
@@ -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',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user