mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix DevUI streaming memory growth regression (#6038)
* Fix DevUI streaming memory growth regression Bounds retained streaming/debug state in DevUI and strengthens browser regression coverage for long streamed responses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address DevUI memory review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix DevUI bundle trailing whitespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
e1e6e3d35e
commit
3242d8a4c4
@@ -40,11 +40,22 @@ import type {
|
||||
ExtendedResponseStreamEvent,
|
||||
} from "@/types";
|
||||
import { useDevUIStore } from "@/stores";
|
||||
import { loadStreamingState } from "@/services/streaming-state";
|
||||
import { loadStreamingState, type StreamingState } from "@/services/streaming-state";
|
||||
|
||||
type DebugEventHandler = (event: ExtendedResponseStreamEvent | "clear") => void;
|
||||
|
||||
const ASSISTANT_TEXT_RENDER_INTERVAL_MS = 50;
|
||||
const STREAMING_PREVIEW_PREFIX = "[Earlier streaming content omitted after refresh]\n\n";
|
||||
|
||||
function getRestoredStreamingText(state: StreamingState): string {
|
||||
if (!state.accumulatedText) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return state.accumulatedTextIsPreview
|
||||
? `${STREAMING_PREVIEW_PREFIX}${state.accumulatedText}`
|
||||
: state.accumulatedText;
|
||||
}
|
||||
|
||||
interface AgentViewProps {
|
||||
selectedAgent: AgentInfo;
|
||||
@@ -683,13 +694,14 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
const state = loadStreamingState(mostRecent.id);
|
||||
|
||||
if (state && !state.completed) {
|
||||
accumulatedTextRef.current = state.accumulatedText || "";
|
||||
const restoredText = getRestoredStreamingText(state);
|
||||
accumulatedTextRef.current = restoredText;
|
||||
// Add assistant message with resumed text
|
||||
const assistantMsg: import("@/types/openai").ConversationMessage = {
|
||||
id: state.lastMessageId || `assistant-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: state.accumulatedText ? [{ type: "text", text: state.accumulatedText }] : [],
|
||||
content: restoredText ? [{ type: "text", text: restoredText }] : [],
|
||||
status: "in_progress",
|
||||
};
|
||||
setChatItems([...allItems as import("@/types/openai").ConversationItem[], assistantMsg]);
|
||||
@@ -988,13 +1000,14 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
// Check for incomplete stream and restore accumulated text
|
||||
const state = loadStreamingState(conversationId);
|
||||
if (state?.accumulatedText) {
|
||||
accumulatedTextRef.current = state.accumulatedText;
|
||||
const restoredText = getRestoredStreamingText(state);
|
||||
accumulatedTextRef.current = restoredText;
|
||||
// Add assistant message with resumed text - streaming will continue automatically
|
||||
const assistantMsg: import("@/types/openai").ConversationMessage = {
|
||||
id: `assistant-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: state.accumulatedText }],
|
||||
content: [{ type: "output_text", text: restoredText }],
|
||||
status: "in_progress",
|
||||
};
|
||||
setChatItems([...items, assistantMsg]);
|
||||
|
||||
@@ -49,6 +49,28 @@ interface WorkflowViewProps {
|
||||
onDebugEvent: DebugEventHandler;
|
||||
}
|
||||
|
||||
function getWorkflowEventTimestamp(event: ExtendedResponseStreamEvent): number | undefined {
|
||||
if ("created_at" in event && typeof event.created_at === "number" && event.created_at) {
|
||||
return event.created_at;
|
||||
}
|
||||
|
||||
const response = "response" in event ? event.response : undefined;
|
||||
if (response && typeof response === "object" && "created_at" in response) {
|
||||
const createdAt = response.created_at;
|
||||
if (typeof createdAt === "number") {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
const data = "data" in event ? event.data : undefined;
|
||||
if (data && typeof data === "object" && "timestamp" in data && typeof data.timestamp === "string") {
|
||||
const milliseconds = new Date(data.timestamp).getTime();
|
||||
return Number.isFinite(milliseconds) ? milliseconds / 1000 : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: CheckpointSelector is not currently used but may be needed for checkpoint resumption feature
|
||||
// Smart Run Workflow Button Component moved to separate file
|
||||
|
||||
@@ -581,20 +603,7 @@ export function WorkflowView({
|
||||
// 2. response.created_at (response.created / lifecycle events)
|
||||
// 3. data.timestamp (response.workflow_event.completed ISO string)
|
||||
// Fall back to a synthesized timestamp only when none is present.
|
||||
const anyEvent = openAIEvent as Record<string, unknown>;
|
||||
const eventTimestamp: number | undefined =
|
||||
typeof anyEvent["created_at"] === "number" && anyEvent["created_at"]
|
||||
? (anyEvent["created_at"] as number)
|
||||
: typeof (anyEvent["response"] as Record<string, unknown> | undefined)?.["created_at"] === "number"
|
||||
? ((anyEvent["response"] as Record<string, number>)["created_at"] as number)
|
||||
: (() => {
|
||||
const ts = (anyEvent["data"] as Record<string, unknown> | undefined)?.["timestamp"];
|
||||
if (typeof ts !== "string") return undefined;
|
||||
const ms = new Date(ts).getTime();
|
||||
// Guard against NaN: Python isoformat() emits microseconds without Z,
|
||||
// which some JS engines cannot parse. Number.isFinite rejects NaN.
|
||||
return Number.isFinite(ms) ? ms / 1000 : undefined;
|
||||
})();
|
||||
const eventTimestamp = getWorkflowEventTimestamp(openAIEvent);
|
||||
const baseTimestamp = Math.floor(Date.now() / 1000);
|
||||
const lastTimestamp =
|
||||
prev.length > 0
|
||||
@@ -1018,20 +1027,7 @@ export function WorkflowView({
|
||||
// 2. response.created_at (response.created / lifecycle events)
|
||||
// 3. data.timestamp (response.workflow_event.completed ISO string)
|
||||
// Fall back to a synthesized timestamp only when none is present.
|
||||
const anyEvent = openAIEvent as Record<string, unknown>;
|
||||
const eventTimestamp: number | undefined =
|
||||
typeof anyEvent["created_at"] === "number" && anyEvent["created_at"]
|
||||
? (anyEvent["created_at"] as number)
|
||||
: typeof (anyEvent["response"] as Record<string, unknown> | undefined)?.["created_at"] === "number"
|
||||
? ((anyEvent["response"] as Record<string, number>)["created_at"] as number)
|
||||
: (() => {
|
||||
const ts = (anyEvent["data"] as Record<string, unknown> | undefined)?.["timestamp"];
|
||||
if (typeof ts !== "string") return undefined;
|
||||
const ms = new Date(ts).getTime();
|
||||
// Guard against NaN: Python isoformat() emits microseconds without Z,
|
||||
// which some JS engines cannot parse. Number.isFinite rejects NaN.
|
||||
return Number.isFinite(ms) ? ms / 1000 : undefined;
|
||||
})();
|
||||
const eventTimestamp = getWorkflowEventTimestamp(openAIEvent);
|
||||
const baseTimestamp = Math.floor(Date.now() / 1000);
|
||||
const lastTimestamp =
|
||||
prev.length > 0
|
||||
|
||||
Reference in New Issue
Block a user