mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-08 03:30:34 +00:00
* feat(subagents): show runtime metadata on task cards * fix(subagents): stop task-card render loop and dedupe model fetches Address code review on the runtime-metadata cards: - P1 render loop: the terminal ToolMessage is re-parsed on every MessageList render and always carries modelName/usage, so the presence-based setTasks condition fired a fresh state object each render -> "Maximum update depth exceeded". computeNextSubtask now returns a value-compared `changed` flag and a pure subtaskNotification() routes terminal transitions through the deferred after-render path while skipping no-op re-parses. - Per-card useModels refetch: add staleTime: Infinity to the ["models"] query so every subtask card shares one /api/models fetch instead of refetching on each mount. * make format * refactor(subagents): dedupe token-usage validators + tidy event narrowing Address PR review follow-ups: - DRY: extract one shared token-usage validator per side. Backend status_contract.normalize_token_usage() now backs both the terminal ToolMessage metadata and the subagent.step/.end run events (step_events.py), and frontend messages/usage.normalizeTokenUsage() backs both the live task_running event (lifecycle.ts) and the terminal ToolMessage metadata (subtask-result.ts). Prevents the input/output/ total_tokens validation from drifting across the four former copies. - Nit: onCustomEvent narrows event.type once instead of re-checking the object shape per branch; the redundant task_started early-return (already validated by taskEventToSubtaskUpdate) is dropped.
102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
|
|
import { computeNextSubtask, subtaskNotification } from "./subtask-update";
|
|
import type { Subtask } from "./types";
|
|
|
|
export interface SubtaskContextValue {
|
|
tasks: Record<string, Subtask>;
|
|
// Always mirrors the latest `tasks` (updated during render). `updateSubtask`
|
|
// reads/writes through this instead of a closure snapshot so async callers
|
|
// (e.g. a late-resolving backfill) merge into current state, not stale state.
|
|
tasksRef: React.RefObject<Record<string, Subtask>>;
|
|
setTasks: (tasks: Record<string, Subtask>) => void;
|
|
}
|
|
|
|
export const SubtaskContext = createContext<SubtaskContextValue>({
|
|
tasks: {},
|
|
tasksRef: { current: {} },
|
|
setTasks: () => {
|
|
/* noop */
|
|
},
|
|
});
|
|
|
|
export function SubtasksProvider({ children }: { children: React.ReactNode }) {
|
|
const [tasks, setTasks] = useState<Record<string, Subtask>>({});
|
|
const tasksRef = useRef(tasks);
|
|
// Keep the ref pointing at the freshest state on every render so reads in
|
|
// async callbacks (backfill `.then`) never see a stale map.
|
|
tasksRef.current = tasks;
|
|
return (
|
|
<SubtaskContext.Provider value={{ tasks, tasksRef, setTasks }}>
|
|
{children}
|
|
</SubtaskContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useSubtaskContext() {
|
|
const context = useContext(SubtaskContext);
|
|
if (context === undefined) {
|
|
throw new Error(
|
|
"useSubtaskContext must be used within a SubtaskContext.Provider",
|
|
);
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export function useSubtask(id: string) {
|
|
const { tasks } = useSubtaskContext();
|
|
return tasks[id];
|
|
}
|
|
|
|
export function useUpdateSubtask() {
|
|
const { tasksRef, setTasks } = useSubtaskContext();
|
|
const shouldNotifyAfterRenderRef = useRef(false);
|
|
// No deps: must run after every render to check the ref set during render.
|
|
useEffect(() => {
|
|
if (!shouldNotifyAfterRenderRef.current) {
|
|
return;
|
|
}
|
|
shouldNotifyAfterRenderRef.current = false;
|
|
setTasks({ ...tasksRef.current });
|
|
});
|
|
|
|
const updateSubtask = useCallback(
|
|
(task: Partial<Subtask> & { id: string }) => {
|
|
// Read the *latest* state via the ref, never a `tasks` snapshot captured in
|
|
// this callback's closure. Without this, an in-flight
|
|
// fetchSubtaskSteps().then(updateSubtask) resolving late would write a stale
|
|
// map, clobbering SSE steps/status and sibling subtasks added meanwhile (#3779).
|
|
const current = tasksRef.current;
|
|
const { next, becameTerminal, changed } = computeNextSubtask(
|
|
current[task.id],
|
|
task,
|
|
);
|
|
|
|
current[task.id] = next;
|
|
|
|
// Gate on an actual state change, not mere field presence. The terminal
|
|
// ToolMessage is re-parsed on every MessageList render and always carries
|
|
// modelName/usage, so a presence check would setTasks({...}) with a fresh
|
|
// reference each render — an infinite loop. `subtaskNotification` routes a
|
|
// terminal transition through the deferred (after-render) path and skips
|
|
// no-op re-parses entirely.
|
|
const notify = subtaskNotification(task, { becameTerminal, changed });
|
|
if (notify === "eager") {
|
|
setTasks({ ...current });
|
|
} else if (notify === "deferred") {
|
|
shouldNotifyAfterRenderRef.current = true;
|
|
}
|
|
},
|
|
[tasksRef, setTasks],
|
|
);
|
|
|
|
return updateSubtask;
|
|
}
|