feat: support subtasks

This commit is contained in:
Henry Li
2026-02-07 16:14:48 +08:00
parent 617559a900
commit a016332a37
19 changed files with 433 additions and 109 deletions

View File

@@ -209,6 +209,16 @@ export const enUS: Translations = {
skillInstallTooltip: "Install skill and make it available to DeerFlow",
},
// Subtasks
subtasks: {
subtask: "Subtask",
executing: (count: number) =>
`Executing ${count} subtask${count === 1 ? "" : "s"} in parallel`,
running: "Running subtask",
completed: "Subtask completed",
failed: "Subtask failed",
},
// Settings
settings: {
title: "Settings",

View File

@@ -155,6 +155,15 @@ export interface Translations {
skillInstallTooltip: string;
};
// Subtasks
subtasks: {
subtask: string;
executing: (count: number) => string;
running: string;
completed: string;
failed: string;
};
// Settings
settings: {
title: string;

View File

@@ -78,7 +78,8 @@ export const zhCN: Translations = {
proMode: "专业",
proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
ultraMode: "超级",
ultraModeDescription: "专业模式加子代理,适用于复杂的多步骤任务,功能最强大",
ultraModeDescription:
"专业模式加子代理,适用于复杂的多步骤任务,功能最强大",
searchModels: "搜索模型...",
surpriseMe: "小惊喜",
surpriseMePrompt: "给我一个小惊喜吧",
@@ -203,6 +204,14 @@ export const zhCN: Translations = {
skillInstallTooltip: "安装技能并使其可在 DeerFlow 中使用",
},
subtasks: {
subtask: "子任务",
executing: (count: number) => `并行执行 ${count} 个子任务`,
running: "子任务运行中",
completed: "子任务已完成",
failed: "子任务失败",
},
// Settings
settings: {
title: "设置",

View File

@@ -1,4 +1,4 @@
import type { Message } from "@langchain/langgraph-sdk";
import type { AIMessage, Message } from "@langchain/langgraph-sdk";
interface GenericMessageGroup<T = string> {
type: T;
@@ -16,12 +16,15 @@ interface AssistantPresentFilesGroup extends GenericMessageGroup<"assistant:pres
interface AssistantClarificationGroup extends GenericMessageGroup<"assistant:clarification"> {}
interface AssistantSubagentGroup extends GenericMessageGroup<"assistant:subagent"> {}
type MessageGroup =
| HumanMessageGroup
| AssistantProcessingGroup
| AssistantMessageGroup
| AssistantPresentFilesGroup
| AssistantClarificationGroup;
| AssistantClarificationGroup
| AssistantSubagentGroup;
export function groupMessages<T>(
messages: Message[],
@@ -78,6 +81,12 @@ export function groupMessages<T>(
type: "assistant:present-files",
messages: [message],
});
} else if (hasSubagent(message)) {
groups.push({
id: message.id,
type: "assistant:subagent",
messages: [message],
});
} else {
if (lastGroup?.type !== "assistant:processing") {
groups.push({
@@ -232,6 +241,15 @@ export function extractPresentFilesFromMessage(message: Message) {
return files;
}
export function hasSubagent(message: AIMessage) {
for (const toolCall of message.tool_calls ?? []) {
if (toolCall.name === "task") {
return true;
}
}
return false;
}
export function findToolCallResult(toolCallId: string, messages: Message[]) {
for (const message of messages) {
if (message.type === "tool" && message.tool_call_id === toolCallId) {

View File

@@ -1,13 +0,0 @@
import { createContext, useContext } from "react";
import type { SubagentState } from "../threads/types";
export const SubagentContext = createContext<Map<string, SubagentState>>(new Map());
export function useSubagentContext() {
const context = useContext(SubagentContext);
if (context === undefined) {
throw new Error("useSubagentContext must be used within a SubagentContext.Provider");
}
return context;
}

View File

@@ -1,69 +0,0 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { SubagentProgressEvent, SubagentState } from "../threads/types";
export function useSubagentStates() {
const [subagents, setSubagents] = useState<Map<string, SubagentState>>(new Map());
const subagentsRef = useRef<Map<string, SubagentState>>(new Map());
// 保持 ref 与 state 同步
useEffect(() => {
subagentsRef.current = subagents;
}, [subagents]);
const handleSubagentProgress = useCallback((event: SubagentProgressEvent) => {
console.log('[SubagentProgress] Received event:', event);
const { task_id, trace_id, subagent_type, event_type, result, error } = event;
setSubagents(prev => {
const newSubagents = new Map(prev);
const existingState = newSubagents.get(task_id) || {
task_id,
trace_id,
subagent_type,
status: "running" as const,
};
let newState = { ...existingState };
switch (event_type) {
case "started":
newState = {
...newState,
status: "running",
};
break;
case "completed":
newState = {
...newState,
status: "completed",
result,
};
break;
case "failed":
newState = {
...newState,
status: "failed",
error,
};
break;
}
newSubagents.set(task_id, newState);
return newSubagents;
});
}, []);
const clearSubagents = useCallback(() => {
setSubagents(new Map());
}, []);
return {
subagents,
handleSubagentProgress,
clearSubagents,
};
}

View File

@@ -1,2 +0,0 @@
export { useSubagentStates } from "./hooks";
export { SubagentContext, useSubagentContext } from "./context";

View File

@@ -0,0 +1,46 @@
import { createContext, useCallback, useContext, useState } from "react";
import type { Subtask } from "./types";
export interface SubtaskContextValue {
tasks: Map<string, Subtask>;
}
export const SubtaskContext = createContext<SubtaskContextValue>({
tasks: new Map(),
});
export function SubtasksProvider({ children }: { children: React.ReactNode }) {
const [tasks] = useState<Map<string, Subtask>>(new Map());
return (
<SubtaskContext.Provider value={{ tasks }}>
{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.get(id);
}
export function useUpdateSubtask() {
const { tasks } = useSubtaskContext();
const updateSubtask = useCallback(
(task: Partial<Subtask> & { id: string }) => {
tasks.set(task.id, { ...tasks.get(task.id), ...task } as Subtask);
},
[tasks],
);
return updateSubtask;
}

View File

@@ -0,0 +1 @@
export * from "./types";

View File

@@ -0,0 +1,9 @@
export interface Subtask {
id: string;
status: "in_progress" | "completed" | "failed";
subagent_type: string;
description: string;
prompt: string;
result?: string;
error?: string;
}

View File

@@ -31,6 +31,9 @@ export function useThreadStream({
threadId: isNewThread ? undefined : threadId,
reconnectOnMount: true,
fetchStateHistory: true,
onCustomEvent(event) {
console.info(event);
},
onFinish(state) {
onFinish?.(state.values);
// void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });