mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 13:53:15 +00:00
refactor: Drop ThreadWithTasks (#142)
* refactor: Drop ThreadWithTasks * cr
This commit is contained in:
parent
0db1a02d13
commit
76d412275a
16 changed files with 387 additions and 137 deletions
|
|
@ -19,7 +19,7 @@ import {
|
|||
getCompletedPlanItems,
|
||||
getCurrentPlanItem,
|
||||
} from "../utils/current-task.js";
|
||||
import { getActivePlanItems } from "../utils/task-plan.js";
|
||||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "DiagnoseError");
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
completeTask,
|
||||
getActivePlanItems,
|
||||
getActiveTask,
|
||||
} from "../utils/task-plan.js";
|
||||
} from "@open-swe/shared/open-swe/tasks";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "GenerateConclusionNode");
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { stopSandbox } from "../utils/sandbox.js";
|
|||
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||
import { getCurrentPlanItem } from "../utils/current-task.js";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { getActivePlanItems } from "../utils/task-plan.js";
|
||||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||
import { SANDBOX_ROOT_DIR } from "@open-swe/shared/constants";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
HumanResponse,
|
||||
} from "@langchain/langgraph/prebuilt";
|
||||
import { startSandbox } from "../utils/sandbox.js";
|
||||
import { createNewTask } from "../utils/task-plan.js";
|
||||
import { createNewTask } from "@open-swe/shared/open-swe/tasks";
|
||||
import { getUserRequest } from "../utils/user-request.js";
|
||||
import {
|
||||
PLAN_INTERRUPT_ACTION_TITLE,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { getUserRequest } from "../utils/user-request.js";
|
|||
import { ToolMessage } from "@langchain/core/messages";
|
||||
import { daytonaClient, deleteSandbox } from "../utils/sandbox.js";
|
||||
import { getGitHubTokensFromConfig } from "../utils/github-tokens.js";
|
||||
import { getActivePlanItems } from "../utils/task-plan.js";
|
||||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "Open PR");
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
completePlanItem,
|
||||
getActivePlanItems,
|
||||
getActiveTask,
|
||||
} from "../utils/task-plan.js";
|
||||
} from "@open-swe/shared/open-swe/tasks";
|
||||
import {
|
||||
getCurrentPlanItem,
|
||||
getRemainingPlanItems,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
completePlanItem,
|
||||
getActivePlanItems,
|
||||
getActiveTask,
|
||||
} from "../utils/task-plan.js";
|
||||
} from "@open-swe/shared/open-swe/tasks";
|
||||
import { getCompletedPlanItems } from "../utils/current-task.js";
|
||||
|
||||
const taskSummarySysPrompt = `You are operating as a terminal-based agentic coding assistant built by LangChain. It wraps LLM models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import {
|
|||
} from "@open-swe/shared/open-swe/types";
|
||||
import { loadModel, Task } from "../utils/load-model.js";
|
||||
import { z } from "zod";
|
||||
import { getActiveTask, updateTaskPlanItems } from "../utils/task-plan.js";
|
||||
import {
|
||||
getActiveTask,
|
||||
updateTaskPlanItems,
|
||||
} from "@open-swe/shared/open-swe/tasks";
|
||||
import {
|
||||
BaseMessage,
|
||||
isAIMessage,
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import {
|
|||
PanelRightOpen,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
|
||||
import { useThreads } from "@/providers/Thread";
|
||||
import { useQueryState, parseAsString } from "nuqs";
|
||||
import { useState, useCallback } from "react";
|
||||
import { ThreadItem } from "./thread-item";
|
||||
import { Thread } from "@langchain/langgraph-sdk";
|
||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||
|
||||
const THREADS_PER_PAGE = 10;
|
||||
|
||||
|
|
@ -18,13 +20,12 @@ interface TaskListSidebarProps {
|
|||
}
|
||||
|
||||
export default function TaskListSidebar({ onCollapse }: TaskListSidebarProps) {
|
||||
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
|
||||
const [threadId, setThreadId] = useQueryState("threadId", parseAsString);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const { threads, threadsLoading, handleThreadClick } = useThreads();
|
||||
|
||||
const onThreadClick = useCallback(
|
||||
(thread: ThreadWithTasks) => {
|
||||
(thread: Thread<GraphState>) => {
|
||||
handleThreadClick(thread, threadId, setThreadId);
|
||||
},
|
||||
[handleThreadClick, threadId, setThreadId],
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
"use client";
|
||||
import { Archive, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
|
||||
import { useThreads } from "@/providers/Thread";
|
||||
import { useQueryState, parseAsString } from "nuqs";
|
||||
import { useState, useCallback } from "react";
|
||||
import { ThreadItem } from "./thread-item";
|
||||
import { Thread } from "@langchain/langgraph-sdk";
|
||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||
|
||||
const THREADS_PER_PAGE = 5;
|
||||
|
||||
|
|
@ -17,7 +19,7 @@ export default function TaskList() {
|
|||
const isDashboardMode = !taskId;
|
||||
|
||||
const onThreadClick = useCallback(
|
||||
(thread: ThreadWithTasks) => {
|
||||
(thread: Thread<GraphState>) => {
|
||||
handleThreadClick(thread, threadId, setThreadId);
|
||||
setTaskId(null);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
"use client";
|
||||
import { memo } from "react";
|
||||
import {
|
||||
formatDistanceToNow,
|
||||
differenceInHours,
|
||||
differenceInMinutes,
|
||||
format,
|
||||
} from "date-fns";
|
||||
import { differenceInHours, differenceInMinutes, format } from "date-fns";
|
||||
import { GitBranch, ArrowRight, ListTodo } from "lucide-react";
|
||||
import { ThreadWithTasks, useThreads } from "@/providers/Thread";
|
||||
import { useThreads } from "@/providers/Thread";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { StatusIndicator } from "@/components/status-indicator";
|
||||
import { GitHubSVG } from "./icons/github";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Thread } from "@langchain/langgraph-sdk";
|
||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||
import { getThreadTasks, getThreadTitle } from "@/lib/thread";
|
||||
|
||||
interface ThreadItemProps {
|
||||
thread: ThreadWithTasks;
|
||||
onClick: (thread: ThreadWithTasks) => void;
|
||||
thread: Thread<GraphState>;
|
||||
onClick: (thread: Thread<GraphState>) => void;
|
||||
variant?: "sidebar" | "dashboard";
|
||||
className?: string;
|
||||
}
|
||||
|
|
@ -55,6 +53,8 @@ export const ThreadItem = memo(function ThreadItem({
|
|||
|
||||
const displayDate = formatRelativeDate(thread.created_at);
|
||||
|
||||
const { totalTasks, completedTasks } = getThreadTasks(thread);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -74,7 +74,7 @@ export const ThreadItem = memo(function ThreadItem({
|
|||
<div className="flex w-full items-center gap-1.5">
|
||||
<StatusIndicator status={thread.status} />
|
||||
<h4 className="w-full truncate text-xs leading-tight font-medium text-gray-900">
|
||||
{thread.threadTitle}
|
||||
{getThreadTitle(thread)}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
|
|
@ -85,10 +85,14 @@ export const ThreadItem = memo(function ThreadItem({
|
|||
height="16"
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<span className="max-w-[90px] truncate">{thread.repository}</span>
|
||||
<span className="max-w-[90px] truncate">
|
||||
{thread.values.targetRepository.repo}
|
||||
</span>
|
||||
<span>/</span>
|
||||
<GitBranch className="size-2.5 flex-shrink-0" />
|
||||
<span className="max-w-[70px] truncate">{thread.branch}</span>
|
||||
<span className="max-w-[70px] truncate">
|
||||
{thread.values.targetRepository.branch}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span>•</span>
|
||||
|
|
@ -101,7 +105,7 @@ export const ThreadItem = memo(function ThreadItem({
|
|||
<div className="ml-1 flex items-center gap-1">
|
||||
<ListTodo className="size-4 flex-shrink-0" />
|
||||
<span>
|
||||
{thread.completedTasksCount}/{thread.totalTasksCount} tasks
|
||||
{completedTasks}/{totalTasks} tasks
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -111,7 +115,7 @@ export const ThreadItem = memo(function ThreadItem({
|
|||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<ListTodo className="size-4 flex-shrink-0" />
|
||||
<span>
|
||||
{thread.completedTasksCount}/{thread.totalTasksCount} tasks
|
||||
{completedTasks}/{totalTasks} tasks
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { ThreadPoller, PollConfig } from "@/lib/polling/thread-poller";
|
||||
import { ThreadWithTasks } from "@/providers/Thread";
|
||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||
import { Thread } from "@langchain/langgraph-sdk";
|
||||
|
||||
interface UseThreadPollingProps {
|
||||
threads: ThreadWithTasks[];
|
||||
getThread: (threadId: string) => Promise<ThreadWithTasks | null>;
|
||||
threads: Thread<GraphState>[];
|
||||
getThread: (threadId: string) => Promise<Thread<GraphState> | null>;
|
||||
onUpdate: (
|
||||
updatedThreads: ThreadWithTasks[],
|
||||
updatedThreads: Thread<GraphState>[],
|
||||
changedThreadIds: string[],
|
||||
) => void;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { ThreadWithTasks } from "@/providers/Thread";
|
||||
import { Thread } from "@langchain/langgraph-sdk";
|
||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||
import { getThreadTasks, getThreadTitle } from "../thread";
|
||||
|
||||
export interface PollConfig {
|
||||
interval: number;
|
||||
onUpdate: (
|
||||
updatedThreads: ThreadWithTasks[],
|
||||
updatedThreads: Thread<GraphState>[],
|
||||
changedThreadIds: string[],
|
||||
) => void;
|
||||
}
|
||||
|
|
@ -12,13 +14,13 @@ export class ThreadPoller {
|
|||
private config: PollConfig;
|
||||
private isPolling: boolean = false;
|
||||
private intervalId: NodeJS.Timeout | null = null;
|
||||
private threads: ThreadWithTasks[];
|
||||
private getThreadFn: (threadId: string) => Promise<ThreadWithTasks | null>;
|
||||
private threads: Thread<GraphState>[];
|
||||
private getThreadFn: (threadId: string) => Promise<Thread<GraphState> | null>;
|
||||
|
||||
constructor(
|
||||
config: PollConfig,
|
||||
threads: ThreadWithTasks[],
|
||||
getThreadFn: (threadId: string) => Promise<ThreadWithTasks | null>,
|
||||
threads: Thread<GraphState>[],
|
||||
getThreadFn: (threadId: string) => Promise<Thread<GraphState> | null>,
|
||||
) {
|
||||
this.config = config;
|
||||
this.threads = threads;
|
||||
|
|
@ -49,7 +51,7 @@ export class ThreadPoller {
|
|||
const currentThreads = this.threads;
|
||||
|
||||
const threadsToPool = currentThreads.slice(0, 10);
|
||||
const updatedThreads: ThreadWithTasks[] = [];
|
||||
const updatedThreads: Thread<GraphState>[] = [];
|
||||
const changedThreadIds: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
|
|
@ -79,19 +81,24 @@ export class ThreadPoller {
|
|||
}
|
||||
|
||||
private hasThreadChanged(
|
||||
current: ThreadWithTasks,
|
||||
updated: ThreadWithTasks,
|
||||
current: Thread<GraphState>,
|
||||
updated: Thread<GraphState>,
|
||||
): boolean {
|
||||
const currentTaskCounts = getThreadTasks(current);
|
||||
const updatedTaskCounts = getThreadTasks(updated);
|
||||
const currentTargetRepo = current.values?.targetRepository;
|
||||
const updatedTargetRepo = updated.values?.targetRepository;
|
||||
return (
|
||||
current.completedTasksCount !== updated.completedTasksCount ||
|
||||
current.totalTasksCount !== updated.totalTasksCount ||
|
||||
currentTaskCounts.completedTasks !== updatedTaskCounts.completedTasks ||
|
||||
currentTaskCounts.totalTasks !== updatedTaskCounts.totalTasks ||
|
||||
current.status !== updated.status ||
|
||||
current.threadTitle !== updated.threadTitle ||
|
||||
current.repository !== updated.repository ||
|
||||
current.branch !== updated.branch ||
|
||||
JSON.stringify(current.tasks) !== JSON.stringify(updated.tasks) ||
|
||||
JSON.stringify(current.proposedPlan) !==
|
||||
JSON.stringify(updated.proposedPlan)
|
||||
getThreadTitle(current) !== getThreadTitle(updated) ||
|
||||
currentTargetRepo.repo !== updatedTargetRepo.repo ||
|
||||
currentTargetRepo.branch !== updatedTargetRepo.branch ||
|
||||
JSON.stringify(current.values?.plan) !==
|
||||
JSON.stringify(updated.values?.plan) ||
|
||||
JSON.stringify(current.values?.proposedPlan) !==
|
||||
JSON.stringify(updated.values?.proposedPlan)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
apps/web/src/lib/thread.ts
Normal file
32
apps/web/src/lib/thread.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Thread } from "@langchain/langgraph-sdk";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||
|
||||
export function getThreadTitle(thread: Thread<GraphState>): string {
|
||||
const messages = thread?.values.messages;
|
||||
if (!messages?.length || !messages[0]?.content) {
|
||||
return `Thread ${thread.thread_id.substring(0, 8)}`;
|
||||
}
|
||||
const threadTitle = getMessageContentString(messages[0].content);
|
||||
return threadTitle;
|
||||
}
|
||||
|
||||
export function getThreadTasks(thread: Thread<GraphState>): {
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
} {
|
||||
if (!thread.values.plan) {
|
||||
return {
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
};
|
||||
}
|
||||
const activePlanItems = getActivePlanItems(thread.values.plan);
|
||||
const totalTasks = activePlanItems.length;
|
||||
const completedTasks = activePlanItems.filter((p) => p.completed).length;
|
||||
return {
|
||||
totalTasks,
|
||||
completedTasks,
|
||||
};
|
||||
}
|
||||
|
|
@ -12,31 +12,20 @@ import {
|
|||
useTransition,
|
||||
} from "react";
|
||||
import { createClient } from "./client";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { TaskPlan, GraphState } from "@open-swe/shared/open-swe/types";
|
||||
import { useThreadPolling } from "@/hooks/useThreadPolling";
|
||||
|
||||
export interface ThreadWithTasks extends Thread {
|
||||
threadTitle: string;
|
||||
repository: string;
|
||||
branch: string;
|
||||
completedTasksCount: number;
|
||||
totalTasksCount: number;
|
||||
tasks: TaskPlan | undefined;
|
||||
proposedPlan: string[];
|
||||
}
|
||||
|
||||
interface ThreadContextType {
|
||||
threads: ThreadWithTasks[];
|
||||
setThreads: Dispatch<SetStateAction<ThreadWithTasks[]>>;
|
||||
threads: Thread<GraphState>[];
|
||||
setThreads: Dispatch<SetStateAction<Thread<GraphState>[]>>;
|
||||
threadsLoading: boolean;
|
||||
setThreadsLoading: Dispatch<SetStateAction<boolean>>;
|
||||
refreshThreads: () => Promise<void>;
|
||||
getThread: (threadId: string) => Promise<ThreadWithTasks | null>;
|
||||
getThread: (threadId: string) => Promise<Thread<GraphState> | null>;
|
||||
isPending: boolean;
|
||||
recentlyUpdatedThreads: Set<string>;
|
||||
handleThreadClick: (
|
||||
thread: ThreadWithTasks,
|
||||
thread: Thread<GraphState>,
|
||||
currentThreadId: string | null,
|
||||
setThreadId: (id: string) => void,
|
||||
) => void;
|
||||
|
|
@ -115,7 +104,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
|||
const assistantId: string | undefined =
|
||||
process.env.NEXT_PUBLIC_ASSISTANT_ID ?? "";
|
||||
|
||||
const [threads, setThreads] = useState<ThreadWithTasks[]>([]);
|
||||
const [threads, setThreads] = useState<Thread<GraphState>[]>([]);
|
||||
const [threadsLoading, setThreadsLoading] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [recentlyUpdatedThreads, setRecentlyUpdatedThreads] = useState<
|
||||
|
|
@ -123,23 +112,13 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
|||
>(new Set());
|
||||
|
||||
const getThread = useCallback(
|
||||
async (threadId: string): Promise<ThreadWithTasks | null> => {
|
||||
async (threadId: string): Promise<Thread<GraphState> | null> => {
|
||||
if (!apiUrl || !assistantId) return null;
|
||||
const client = createClient(apiUrl);
|
||||
|
||||
try {
|
||||
const thread = await client.threads.get(threadId);
|
||||
|
||||
// Get the comprehensive state data which contains the plan
|
||||
let stateData: { values: GraphState } | null = null;
|
||||
|
||||
try {
|
||||
stateData = await client.threads.getState(threadId);
|
||||
} catch (stateError) {
|
||||
console.error("Failed to get state data:", stateError);
|
||||
}
|
||||
|
||||
return enhanceThreadWithTasks(thread, stateData);
|
||||
const thread = await client.threads.get<GraphState>(threadId);
|
||||
return thread;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch thread:", threadId, error);
|
||||
return null;
|
||||
|
|
@ -148,42 +127,6 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
|||
[apiUrl, assistantId],
|
||||
);
|
||||
|
||||
const enhanceThreadWithTasks = (
|
||||
thread: Thread,
|
||||
stateData?: { values: GraphState } | null,
|
||||
): ThreadWithTasks => {
|
||||
const stateValues = stateData?.values;
|
||||
const threadValues = thread.values as GraphState;
|
||||
|
||||
const plan: TaskPlan | undefined = stateValues?.plan || threadValues?.plan;
|
||||
const proposedPlan: string[] =
|
||||
stateValues?.proposedPlan || threadValues?.proposedPlan || [];
|
||||
|
||||
const targetRepository =
|
||||
stateValues?.targetRepository || threadValues?.targetRepository;
|
||||
const messages = stateValues?.messages || threadValues?.messages;
|
||||
const firstMessageContent = messages?.[0]?.content;
|
||||
const threadTitle = firstMessageContent
|
||||
? getMessageContentString(firstMessageContent)
|
||||
: `Thread ${thread.thread_id.substring(0, 8)}`;
|
||||
|
||||
const { totalTasksCount, completedTasksCount } = getTaskCounts(
|
||||
plan,
|
||||
proposedPlan,
|
||||
);
|
||||
|
||||
return {
|
||||
...thread,
|
||||
threadTitle,
|
||||
repository: targetRepository?.repo || "Unknown Repository",
|
||||
branch: targetRepository?.branch || "main",
|
||||
completedTasksCount,
|
||||
totalTasksCount,
|
||||
tasks: plan,
|
||||
proposedPlan,
|
||||
};
|
||||
};
|
||||
|
||||
const refreshThreads = useCallback(async (): Promise<void> => {
|
||||
if (!apiUrl || !assistantId) return;
|
||||
|
||||
|
|
@ -196,42 +139,25 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
|||
metadata: getThreadSearchMetadata(assistantId),
|
||||
};
|
||||
|
||||
let threadsResponse = await client.threads.search(searchParams);
|
||||
let threadsResponse =
|
||||
await client.threads.search<GraphState>(searchParams);
|
||||
|
||||
if (threadsResponse.length === 0) {
|
||||
const altMetadata = assistantId.includes("-")
|
||||
? { assistant_id: assistantId }
|
||||
: { graph_id: assistantId };
|
||||
threadsResponse = await client.threads.search({
|
||||
threadsResponse = await client.threads.search<GraphState>({
|
||||
limit: 100,
|
||||
metadata: altMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
const enhancedThreads: ThreadWithTasks[] = [];
|
||||
for (const thread of threadsResponse) {
|
||||
try {
|
||||
const fullThread = await client.threads.get(thread.thread_id);
|
||||
|
||||
let stateData: { values: GraphState } | null = null;
|
||||
try {
|
||||
stateData = await client.threads.getState(thread.thread_id);
|
||||
} catch (stateError) {
|
||||
console.error("Failed to get state data:", stateError);
|
||||
}
|
||||
|
||||
enhancedThreads.push(enhanceThreadWithTasks(fullThread, stateData));
|
||||
} catch (error) {
|
||||
console.error(`Failed to enhance thread ${thread.thread_id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
enhancedThreads.sort(
|
||||
threadsResponse.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||
);
|
||||
|
||||
setThreads(enhancedThreads);
|
||||
setThreads(threadsResponse);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch threads:", error);
|
||||
} finally {
|
||||
|
|
@ -244,7 +170,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
|||
}, [refreshThreads]);
|
||||
|
||||
const handlePollingUpdate = useCallback(
|
||||
(updatedThreads: ThreadWithTasks[], changedThreadIds: string[]) => {
|
||||
(updatedThreads: Thread<GraphState>[], changedThreadIds: string[]) => {
|
||||
setThreads((currentThreads) => {
|
||||
const updatedMap = new Map(updatedThreads.map((t) => [t.thread_id, t]));
|
||||
return currentThreads.map(
|
||||
|
|
@ -271,7 +197,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
const handleThreadClick = useCallback(
|
||||
(
|
||||
thread: ThreadWithTasks,
|
||||
thread: Thread<GraphState>,
|
||||
currentThreadId: string | null,
|
||||
setThreadId: (id: string) => void,
|
||||
) => {
|
||||
|
|
|
|||
274
packages/shared/src/open-swe/tasks.ts
Normal file
274
packages/shared/src/open-swe/tasks.ts
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import { PlanItem, Task, TaskPlan, PlanRevision } from "./types.js";
|
||||
|
||||
/**
|
||||
* Creates a new task with the provided plan items.
|
||||
* Can either add to an existing TaskPlan or create a brand new one.
|
||||
*
|
||||
* @param request The original user request text that initiated this task
|
||||
* @param planItems The plan items to include in the new task
|
||||
* @param existingTaskPlan Optional existing TaskPlan to add the new task to
|
||||
* @param parentTaskId Optional ID of a parent task if this task is derived from another
|
||||
* @returns The updated TaskPlan with the new task added
|
||||
*/
|
||||
export function createNewTask(
|
||||
request: string,
|
||||
planItems: PlanItem[],
|
||||
existingTaskPlan?: TaskPlan,
|
||||
parentTaskId?: string,
|
||||
): TaskPlan {
|
||||
// Create the initial plan revision
|
||||
const initialRevision: PlanRevision = {
|
||||
revisionIndex: 0,
|
||||
plans: planItems,
|
||||
createdAt: Date.now(),
|
||||
createdBy: "agent",
|
||||
};
|
||||
|
||||
// Create the new task
|
||||
const newTask: Task = {
|
||||
id: uuidv4(),
|
||||
taskIndex: existingTaskPlan ? existingTaskPlan.tasks.length : 0,
|
||||
request,
|
||||
createdAt: Date.now(),
|
||||
completed: false,
|
||||
planRevisions: [initialRevision],
|
||||
activeRevisionIndex: 0,
|
||||
parentTaskId,
|
||||
};
|
||||
|
||||
// If there's an existing task plan, add the new task to it
|
||||
if (existingTaskPlan) {
|
||||
return {
|
||||
tasks: [...existingTaskPlan.tasks, newTask],
|
||||
activeTaskIndex: existingTaskPlan.tasks.length, // Set the new task as active
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise create a new task plan with just this task
|
||||
return {
|
||||
tasks: [newTask],
|
||||
activeTaskIndex: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the plan items for an existing task by creating a new revision.
|
||||
*
|
||||
* @param taskPlan The current task plan
|
||||
* @param taskId The ID of the task to update
|
||||
* @param planItems The new plan items
|
||||
* @param createdBy Who created this revision ('agent' or 'user')
|
||||
* @returns The updated TaskPlan with the new revision
|
||||
* @throws Error if the task ID doesn't exist
|
||||
*/
|
||||
export function updateTaskPlanItems(
|
||||
taskPlan: TaskPlan,
|
||||
taskId: string,
|
||||
planItems: PlanItem[],
|
||||
createdBy: "agent" | "user" = "agent",
|
||||
): TaskPlan {
|
||||
// Find the task to update
|
||||
const taskIndex = taskPlan.tasks.findIndex((task) => task.id === taskId);
|
||||
|
||||
if (taskIndex === -1) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
const task = taskPlan.tasks[taskIndex];
|
||||
|
||||
// Create a new revision with the updated plan items
|
||||
const newRevision: PlanRevision = {
|
||||
revisionIndex: task.planRevisions.length,
|
||||
plans: planItems,
|
||||
createdAt: Date.now(),
|
||||
createdBy,
|
||||
};
|
||||
|
||||
// Create an updated task with the new revision
|
||||
const updatedTask: Task = {
|
||||
...task,
|
||||
planRevisions: [...task.planRevisions, newRevision],
|
||||
activeRevisionIndex: task.planRevisions.length, // Set the new revision as active
|
||||
};
|
||||
|
||||
// Create a new array of tasks with the updated task
|
||||
const updatedTasks = [...taskPlan.tasks];
|
||||
updatedTasks[taskIndex] = updatedTask;
|
||||
|
||||
// Return the updated task plan
|
||||
return {
|
||||
...taskPlan,
|
||||
tasks: updatedTasks,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the active task from a TaskPlan
|
||||
*
|
||||
* @param taskPlan The task plan
|
||||
* @returns The currently active task
|
||||
* @throws Error if there are no tasks
|
||||
*/
|
||||
export function getActiveTask(taskPlan: TaskPlan): Task {
|
||||
if (taskPlan.tasks.length === 0) {
|
||||
throw new Error("No tasks available");
|
||||
}
|
||||
|
||||
return taskPlan.tasks[taskPlan.activeTaskIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the active plan items for the active task
|
||||
*
|
||||
* @param taskPlan The task plan
|
||||
* @returns The currently active plan items
|
||||
* @throws Error if there are no tasks or no plan revisions
|
||||
*/
|
||||
export function getActivePlanItems(taskPlan: TaskPlan): PlanItem[] {
|
||||
const activeTask = getActiveTask(taskPlan);
|
||||
|
||||
if (activeTask.planRevisions.length === 0) {
|
||||
throw new Error("No plan revisions available for the active task");
|
||||
}
|
||||
|
||||
return activeTask.planRevisions[activeTask.activeRevisionIndex].plans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a specific plan item as completed and adds a summary.
|
||||
* This operation modifies the current active revision directly and does NOT create a new revision.
|
||||
*
|
||||
* @param taskPlan The current task plan
|
||||
* @param taskId The ID of the task containing the plan item
|
||||
* @param planItemIndex The `index` property of the plan item to mark as completed
|
||||
* @param summary Optional summary of the completed plan item. If undefined, existing summary is preserved.
|
||||
* @returns The updated TaskPlan
|
||||
* @throws Error if the task or plan item is not found, or if no active revision exists.
|
||||
*/
|
||||
export function completePlanItem(
|
||||
taskPlan: TaskPlan,
|
||||
taskId: string,
|
||||
planItemIndex: number,
|
||||
summary?: string,
|
||||
): TaskPlan {
|
||||
const taskIndexInPlan = taskPlan.tasks.findIndex(
|
||||
(task) => task.id === taskId,
|
||||
);
|
||||
|
||||
if (taskIndexInPlan === -1) {
|
||||
throw new Error(`Task with ID ${taskId} not found in task plan`);
|
||||
}
|
||||
|
||||
const originalTask = taskPlan.tasks[taskIndexInPlan];
|
||||
|
||||
const activeRevisionIndex = originalTask.activeRevisionIndex;
|
||||
|
||||
// Ensure there's a planRevisions array and the activeRevisionIndex is valid
|
||||
if (
|
||||
!originalTask.planRevisions ||
|
||||
activeRevisionIndex < 0 ||
|
||||
activeRevisionIndex >= originalTask.planRevisions.length
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid active revision index (${activeRevisionIndex}) for task ${taskId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const originalActiveRevision =
|
||||
originalTask.planRevisions[activeRevisionIndex];
|
||||
|
||||
// This check should be covered by the index check, but reinforces intent
|
||||
if (!originalActiveRevision) {
|
||||
// This case implies an issue with activeRevisionIndex or planRevisions structure
|
||||
throw new Error(
|
||||
`Active revision (index ${activeRevisionIndex}) not found for task ${taskId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const planItemToUpdateActualIndexInPlansArray =
|
||||
originalActiveRevision.plans.findIndex(
|
||||
(item) => item.index === planItemIndex,
|
||||
);
|
||||
|
||||
if (planItemToUpdateActualIndexInPlansArray === -1) {
|
||||
throw new Error(
|
||||
`Plan item with .index ${planItemIndex} not found in active revision (index ${activeRevisionIndex}) of task ${taskId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create a new 'plans' array with the specific item updated
|
||||
const updatedPlansForRevision = originalActiveRevision.plans.map((item) => {
|
||||
if (item.index === planItemIndex) {
|
||||
const newSummary = summary !== undefined ? summary : item.summary;
|
||||
return { ...item, completed: true, summary: newSummary };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
// Create a new 'PlanRevision' object for the active revision, with the updated 'plans'
|
||||
const updatedActiveRevision: PlanRevision = {
|
||||
...originalActiveRevision,
|
||||
plans: updatedPlansForRevision,
|
||||
};
|
||||
|
||||
// Create a new 'planRevisions' array, replacing the active revision with the updated one
|
||||
const updatedPlanRevisions = [...originalTask.planRevisions];
|
||||
updatedPlanRevisions[activeRevisionIndex] = updatedActiveRevision;
|
||||
|
||||
// Create a new 'Task' object with the updated 'planRevisions'
|
||||
const updatedTask: Task = {
|
||||
...originalTask,
|
||||
planRevisions: updatedPlanRevisions,
|
||||
};
|
||||
|
||||
// Create a new 'tasks' array for the TaskPlan, replacing the updated task
|
||||
const updatedTasksArray = [...taskPlan.tasks];
|
||||
updatedTasksArray[taskIndexInPlan] = updatedTask;
|
||||
|
||||
// Return the new TaskPlan object
|
||||
return {
|
||||
...taskPlan,
|
||||
tasks: updatedTasksArray,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a task as completed
|
||||
*
|
||||
* @param taskPlan The current task plan
|
||||
* @param taskId The ID of the task to mark as completed
|
||||
* @param summary Optional summary of the completed task
|
||||
* @returns The updated TaskPlan
|
||||
*/
|
||||
export function completeTask(
|
||||
taskPlan: TaskPlan,
|
||||
taskId: string,
|
||||
summary?: string,
|
||||
): TaskPlan {
|
||||
const taskIndex = taskPlan.tasks.findIndex((task) => task.id === taskId);
|
||||
|
||||
if (taskIndex === -1) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
const task = taskPlan.tasks[taskIndex];
|
||||
|
||||
// Create an updated task marked as completed
|
||||
const updatedTask: Task = {
|
||||
...task,
|
||||
completed: true,
|
||||
completedAt: Date.now(),
|
||||
summary,
|
||||
};
|
||||
|
||||
// Create a new array of tasks with the updated task
|
||||
const updatedTasks = [...taskPlan.tasks];
|
||||
updatedTasks[taskIndex] = updatedTask;
|
||||
|
||||
// Return the updated task plan
|
||||
return {
|
||||
...taskPlan,
|
||||
tasks: updatedTasks,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue