mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-05 17:42:12 +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,
|
getCompletedPlanItems,
|
||||||
getCurrentPlanItem,
|
getCurrentPlanItem,
|
||||||
} from "../utils/current-task.js";
|
} 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");
|
const logger = createLogger(LogLevel.INFO, "DiagnoseError");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
completeTask,
|
completeTask,
|
||||||
getActivePlanItems,
|
getActivePlanItems,
|
||||||
getActiveTask,
|
getActiveTask,
|
||||||
} from "../utils/task-plan.js";
|
} from "@open-swe/shared/open-swe/tasks";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GenerateConclusionNode");
|
const logger = createLogger(LogLevel.INFO, "GenerateConclusionNode");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { stopSandbox } from "../utils/sandbox.js";
|
||||||
import { createLogger, LogLevel } from "../utils/logger.js";
|
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||||
import { getCurrentPlanItem } from "../utils/current-task.js";
|
import { getCurrentPlanItem } from "../utils/current-task.js";
|
||||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
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";
|
import { SANDBOX_ROOT_DIR } from "@open-swe/shared/constants";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
|
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
HumanResponse,
|
HumanResponse,
|
||||||
} from "@langchain/langgraph/prebuilt";
|
} from "@langchain/langgraph/prebuilt";
|
||||||
import { startSandbox } from "../utils/sandbox.js";
|
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 { getUserRequest } from "../utils/user-request.js";
|
||||||
import {
|
import {
|
||||||
PLAN_INTERRUPT_ACTION_TITLE,
|
PLAN_INTERRUPT_ACTION_TITLE,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { getUserRequest } from "../utils/user-request.js";
|
||||||
import { ToolMessage } from "@langchain/core/messages";
|
import { ToolMessage } from "@langchain/core/messages";
|
||||||
import { daytonaClient, deleteSandbox } from "../utils/sandbox.js";
|
import { daytonaClient, deleteSandbox } from "../utils/sandbox.js";
|
||||||
import { getGitHubTokensFromConfig } from "../utils/github-tokens.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");
|
const logger = createLogger(LogLevel.INFO, "Open PR");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
completePlanItem,
|
completePlanItem,
|
||||||
getActivePlanItems,
|
getActivePlanItems,
|
||||||
getActiveTask,
|
getActiveTask,
|
||||||
} from "../utils/task-plan.js";
|
} from "@open-swe/shared/open-swe/tasks";
|
||||||
import {
|
import {
|
||||||
getCurrentPlanItem,
|
getCurrentPlanItem,
|
||||||
getRemainingPlanItems,
|
getRemainingPlanItems,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
completePlanItem,
|
completePlanItem,
|
||||||
getActivePlanItems,
|
getActivePlanItems,
|
||||||
getActiveTask,
|
getActiveTask,
|
||||||
} from "../utils/task-plan.js";
|
} from "@open-swe/shared/open-swe/tasks";
|
||||||
import { getCompletedPlanItems } from "../utils/current-task.js";
|
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.
|
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";
|
} from "@open-swe/shared/open-swe/types";
|
||||||
import { loadModel, Task } from "../utils/load-model.js";
|
import { loadModel, Task } from "../utils/load-model.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getActiveTask, updateTaskPlanItems } from "../utils/task-plan.js";
|
import {
|
||||||
|
getActiveTask,
|
||||||
|
updateTaskPlanItems,
|
||||||
|
} from "@open-swe/shared/open-swe/tasks";
|
||||||
import {
|
import {
|
||||||
BaseMessage,
|
BaseMessage,
|
||||||
isAIMessage,
|
isAIMessage,
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ import {
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
|
import { useThreads } from "@/providers/Thread";
|
||||||
import { useQueryState, parseAsString } from "nuqs";
|
import { useQueryState, parseAsString } from "nuqs";
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { ThreadItem } from "./thread-item";
|
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;
|
const THREADS_PER_PAGE = 10;
|
||||||
|
|
||||||
|
|
@ -18,13 +20,12 @@ interface TaskListSidebarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TaskListSidebar({ onCollapse }: TaskListSidebarProps) {
|
export default function TaskListSidebar({ onCollapse }: TaskListSidebarProps) {
|
||||||
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
|
|
||||||
const [threadId, setThreadId] = useQueryState("threadId", parseAsString);
|
const [threadId, setThreadId] = useQueryState("threadId", parseAsString);
|
||||||
const [currentPage, setCurrentPage] = useState(0);
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
const { threads, threadsLoading, handleThreadClick } = useThreads();
|
const { threads, threadsLoading, handleThreadClick } = useThreads();
|
||||||
|
|
||||||
const onThreadClick = useCallback(
|
const onThreadClick = useCallback(
|
||||||
(thread: ThreadWithTasks) => {
|
(thread: Thread<GraphState>) => {
|
||||||
handleThreadClick(thread, threadId, setThreadId);
|
handleThreadClick(thread, threadId, setThreadId);
|
||||||
},
|
},
|
||||||
[handleThreadClick, threadId, setThreadId],
|
[handleThreadClick, threadId, setThreadId],
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { Archive, ChevronLeft, ChevronRight } from "lucide-react";
|
import { Archive, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
|
import { useThreads } from "@/providers/Thread";
|
||||||
import { useQueryState, parseAsString } from "nuqs";
|
import { useQueryState, parseAsString } from "nuqs";
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { ThreadItem } from "./thread-item";
|
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;
|
const THREADS_PER_PAGE = 5;
|
||||||
|
|
||||||
|
|
@ -17,7 +19,7 @@ export default function TaskList() {
|
||||||
const isDashboardMode = !taskId;
|
const isDashboardMode = !taskId;
|
||||||
|
|
||||||
const onThreadClick = useCallback(
|
const onThreadClick = useCallback(
|
||||||
(thread: ThreadWithTasks) => {
|
(thread: Thread<GraphState>) => {
|
||||||
handleThreadClick(thread, threadId, setThreadId);
|
handleThreadClick(thread, threadId, setThreadId);
|
||||||
setTaskId(null);
|
setTaskId(null);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import {
|
import { differenceInHours, differenceInMinutes, format } from "date-fns";
|
||||||
formatDistanceToNow,
|
|
||||||
differenceInHours,
|
|
||||||
differenceInMinutes,
|
|
||||||
format,
|
|
||||||
} from "date-fns";
|
|
||||||
import { GitBranch, ArrowRight, ListTodo } from "lucide-react";
|
import { GitBranch, ArrowRight, ListTodo } from "lucide-react";
|
||||||
import { ThreadWithTasks, useThreads } from "@/providers/Thread";
|
import { useThreads } from "@/providers/Thread";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { StatusIndicator } from "@/components/status-indicator";
|
import { StatusIndicator } from "@/components/status-indicator";
|
||||||
import { GitHubSVG } from "./icons/github";
|
import { GitHubSVG } from "./icons/github";
|
||||||
import { useQueryState } from "nuqs";
|
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 {
|
interface ThreadItemProps {
|
||||||
thread: ThreadWithTasks;
|
thread: Thread<GraphState>;
|
||||||
onClick: (thread: ThreadWithTasks) => void;
|
onClick: (thread: Thread<GraphState>) => void;
|
||||||
variant?: "sidebar" | "dashboard";
|
variant?: "sidebar" | "dashboard";
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +53,8 @@ export const ThreadItem = memo(function ThreadItem({
|
||||||
|
|
||||||
const displayDate = formatRelativeDate(thread.created_at);
|
const displayDate = formatRelativeDate(thread.created_at);
|
||||||
|
|
||||||
|
const { totalTasks, completedTasks } = getThreadTasks(thread);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -74,7 +74,7 @@ export const ThreadItem = memo(function ThreadItem({
|
||||||
<div className="flex w-full items-center gap-1.5">
|
<div className="flex w-full items-center gap-1.5">
|
||||||
<StatusIndicator status={thread.status} />
|
<StatusIndicator status={thread.status} />
|
||||||
<h4 className="w-full truncate text-xs leading-tight font-medium text-gray-900">
|
<h4 className="w-full truncate text-xs leading-tight font-medium text-gray-900">
|
||||||
{thread.threadTitle}
|
{getThreadTitle(thread)}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -85,10 +85,14 @@ export const ThreadItem = memo(function ThreadItem({
|
||||||
height="16"
|
height="16"
|
||||||
className="flex-shrink-0"
|
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>
|
<span>/</span>
|
||||||
<GitBranch className="size-2.5 flex-shrink-0" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
|
|
@ -101,7 +105,7 @@ export const ThreadItem = memo(function ThreadItem({
|
||||||
<div className="ml-1 flex items-center gap-1">
|
<div className="ml-1 flex items-center gap-1">
|
||||||
<ListTodo className="size-4 flex-shrink-0" />
|
<ListTodo className="size-4 flex-shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{thread.completedTasksCount}/{thread.totalTasksCount} tasks
|
{completedTasks}/{totalTasks} tasks
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -111,7 +115,7 @@ export const ThreadItem = memo(function ThreadItem({
|
||||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||||
<ListTodo className="size-4 flex-shrink-0" />
|
<ListTodo className="size-4 flex-shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{thread.completedTasksCount}/{thread.totalTasksCount} tasks
|
{completedTasks}/{totalTasks} tasks
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { ThreadPoller, PollConfig } from "@/lib/polling/thread-poller";
|
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 {
|
interface UseThreadPollingProps {
|
||||||
threads: ThreadWithTasks[];
|
threads: Thread<GraphState>[];
|
||||||
getThread: (threadId: string) => Promise<ThreadWithTasks | null>;
|
getThread: (threadId: string) => Promise<Thread<GraphState> | null>;
|
||||||
onUpdate: (
|
onUpdate: (
|
||||||
updatedThreads: ThreadWithTasks[],
|
updatedThreads: Thread<GraphState>[],
|
||||||
changedThreadIds: string[],
|
changedThreadIds: string[],
|
||||||
) => void;
|
) => 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 {
|
export interface PollConfig {
|
||||||
interval: number;
|
interval: number;
|
||||||
onUpdate: (
|
onUpdate: (
|
||||||
updatedThreads: ThreadWithTasks[],
|
updatedThreads: Thread<GraphState>[],
|
||||||
changedThreadIds: string[],
|
changedThreadIds: string[],
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -12,13 +14,13 @@ export class ThreadPoller {
|
||||||
private config: PollConfig;
|
private config: PollConfig;
|
||||||
private isPolling: boolean = false;
|
private isPolling: boolean = false;
|
||||||
private intervalId: NodeJS.Timeout | null = null;
|
private intervalId: NodeJS.Timeout | null = null;
|
||||||
private threads: ThreadWithTasks[];
|
private threads: Thread<GraphState>[];
|
||||||
private getThreadFn: (threadId: string) => Promise<ThreadWithTasks | null>;
|
private getThreadFn: (threadId: string) => Promise<Thread<GraphState> | null>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: PollConfig,
|
config: PollConfig,
|
||||||
threads: ThreadWithTasks[],
|
threads: Thread<GraphState>[],
|
||||||
getThreadFn: (threadId: string) => Promise<ThreadWithTasks | null>,
|
getThreadFn: (threadId: string) => Promise<Thread<GraphState> | null>,
|
||||||
) {
|
) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.threads = threads;
|
this.threads = threads;
|
||||||
|
|
@ -49,7 +51,7 @@ export class ThreadPoller {
|
||||||
const currentThreads = this.threads;
|
const currentThreads = this.threads;
|
||||||
|
|
||||||
const threadsToPool = currentThreads.slice(0, 10);
|
const threadsToPool = currentThreads.slice(0, 10);
|
||||||
const updatedThreads: ThreadWithTasks[] = [];
|
const updatedThreads: Thread<GraphState>[] = [];
|
||||||
const changedThreadIds: string[] = [];
|
const changedThreadIds: string[] = [];
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
|
@ -79,19 +81,24 @@ export class ThreadPoller {
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasThreadChanged(
|
private hasThreadChanged(
|
||||||
current: ThreadWithTasks,
|
current: Thread<GraphState>,
|
||||||
updated: ThreadWithTasks,
|
updated: Thread<GraphState>,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
const currentTaskCounts = getThreadTasks(current);
|
||||||
|
const updatedTaskCounts = getThreadTasks(updated);
|
||||||
|
const currentTargetRepo = current.values?.targetRepository;
|
||||||
|
const updatedTargetRepo = updated.values?.targetRepository;
|
||||||
return (
|
return (
|
||||||
current.completedTasksCount !== updated.completedTasksCount ||
|
currentTaskCounts.completedTasks !== updatedTaskCounts.completedTasks ||
|
||||||
current.totalTasksCount !== updated.totalTasksCount ||
|
currentTaskCounts.totalTasks !== updatedTaskCounts.totalTasks ||
|
||||||
current.status !== updated.status ||
|
current.status !== updated.status ||
|
||||||
current.threadTitle !== updated.threadTitle ||
|
getThreadTitle(current) !== getThreadTitle(updated) ||
|
||||||
current.repository !== updated.repository ||
|
currentTargetRepo.repo !== updatedTargetRepo.repo ||
|
||||||
current.branch !== updated.branch ||
|
currentTargetRepo.branch !== updatedTargetRepo.branch ||
|
||||||
JSON.stringify(current.tasks) !== JSON.stringify(updated.tasks) ||
|
JSON.stringify(current.values?.plan) !==
|
||||||
JSON.stringify(current.proposedPlan) !==
|
JSON.stringify(updated.values?.plan) ||
|
||||||
JSON.stringify(updated.proposedPlan)
|
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,
|
useTransition,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { createClient } from "./client";
|
import { createClient } from "./client";
|
||||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
|
||||||
import { TaskPlan, GraphState } from "@open-swe/shared/open-swe/types";
|
import { TaskPlan, GraphState } from "@open-swe/shared/open-swe/types";
|
||||||
import { useThreadPolling } from "@/hooks/useThreadPolling";
|
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 {
|
interface ThreadContextType {
|
||||||
threads: ThreadWithTasks[];
|
threads: Thread<GraphState>[];
|
||||||
setThreads: Dispatch<SetStateAction<ThreadWithTasks[]>>;
|
setThreads: Dispatch<SetStateAction<Thread<GraphState>[]>>;
|
||||||
threadsLoading: boolean;
|
threadsLoading: boolean;
|
||||||
setThreadsLoading: Dispatch<SetStateAction<boolean>>;
|
setThreadsLoading: Dispatch<SetStateAction<boolean>>;
|
||||||
refreshThreads: () => Promise<void>;
|
refreshThreads: () => Promise<void>;
|
||||||
getThread: (threadId: string) => Promise<ThreadWithTasks | null>;
|
getThread: (threadId: string) => Promise<Thread<GraphState> | null>;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
recentlyUpdatedThreads: Set<string>;
|
recentlyUpdatedThreads: Set<string>;
|
||||||
handleThreadClick: (
|
handleThreadClick: (
|
||||||
thread: ThreadWithTasks,
|
thread: Thread<GraphState>,
|
||||||
currentThreadId: string | null,
|
currentThreadId: string | null,
|
||||||
setThreadId: (id: string) => void,
|
setThreadId: (id: string) => void,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
@ -115,7 +104,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
||||||
const assistantId: string | undefined =
|
const assistantId: string | undefined =
|
||||||
process.env.NEXT_PUBLIC_ASSISTANT_ID ?? "";
|
process.env.NEXT_PUBLIC_ASSISTANT_ID ?? "";
|
||||||
|
|
||||||
const [threads, setThreads] = useState<ThreadWithTasks[]>([]);
|
const [threads, setThreads] = useState<Thread<GraphState>[]>([]);
|
||||||
const [threadsLoading, setThreadsLoading] = useState(false);
|
const [threadsLoading, setThreadsLoading] = useState(false);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [recentlyUpdatedThreads, setRecentlyUpdatedThreads] = useState<
|
const [recentlyUpdatedThreads, setRecentlyUpdatedThreads] = useState<
|
||||||
|
|
@ -123,23 +112,13 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
||||||
>(new Set());
|
>(new Set());
|
||||||
|
|
||||||
const getThread = useCallback(
|
const getThread = useCallback(
|
||||||
async (threadId: string): Promise<ThreadWithTasks | null> => {
|
async (threadId: string): Promise<Thread<GraphState> | null> => {
|
||||||
if (!apiUrl || !assistantId) return null;
|
if (!apiUrl || !assistantId) return null;
|
||||||
const client = createClient(apiUrl);
|
const client = createClient(apiUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const thread = await client.threads.get(threadId);
|
const thread = await client.threads.get<GraphState>(threadId);
|
||||||
|
return thread;
|
||||||
// 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);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch thread:", threadId, error);
|
console.error("Failed to fetch thread:", threadId, error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -148,42 +127,6 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
||||||
[apiUrl, assistantId],
|
[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> => {
|
const refreshThreads = useCallback(async (): Promise<void> => {
|
||||||
if (!apiUrl || !assistantId) return;
|
if (!apiUrl || !assistantId) return;
|
||||||
|
|
||||||
|
|
@ -196,42 +139,25 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
||||||
metadata: getThreadSearchMetadata(assistantId),
|
metadata: getThreadSearchMetadata(assistantId),
|
||||||
};
|
};
|
||||||
|
|
||||||
let threadsResponse = await client.threads.search(searchParams);
|
let threadsResponse =
|
||||||
|
await client.threads.search<GraphState>(searchParams);
|
||||||
|
|
||||||
if (threadsResponse.length === 0) {
|
if (threadsResponse.length === 0) {
|
||||||
const altMetadata = assistantId.includes("-")
|
const altMetadata = assistantId.includes("-")
|
||||||
? { assistant_id: assistantId }
|
? { assistant_id: assistantId }
|
||||||
: { graph_id: assistantId };
|
: { graph_id: assistantId };
|
||||||
threadsResponse = await client.threads.search({
|
threadsResponse = await client.threads.search<GraphState>({
|
||||||
limit: 100,
|
limit: 100,
|
||||||
metadata: altMetadata,
|
metadata: altMetadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const enhancedThreads: ThreadWithTasks[] = [];
|
threadsResponse.sort(
|
||||||
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(
|
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||||
);
|
);
|
||||||
|
|
||||||
setThreads(enhancedThreads);
|
setThreads(threadsResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch threads:", error);
|
console.error("Failed to fetch threads:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -244,7 +170,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
||||||
}, [refreshThreads]);
|
}, [refreshThreads]);
|
||||||
|
|
||||||
const handlePollingUpdate = useCallback(
|
const handlePollingUpdate = useCallback(
|
||||||
(updatedThreads: ThreadWithTasks[], changedThreadIds: string[]) => {
|
(updatedThreads: Thread<GraphState>[], changedThreadIds: string[]) => {
|
||||||
setThreads((currentThreads) => {
|
setThreads((currentThreads) => {
|
||||||
const updatedMap = new Map(updatedThreads.map((t) => [t.thread_id, t]));
|
const updatedMap = new Map(updatedThreads.map((t) => [t.thread_id, t]));
|
||||||
return currentThreads.map(
|
return currentThreads.map(
|
||||||
|
|
@ -271,7 +197,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
const handleThreadClick = useCallback(
|
const handleThreadClick = useCallback(
|
||||||
(
|
(
|
||||||
thread: ThreadWithTasks,
|
thread: Thread<GraphState>,
|
||||||
currentThreadId: string | null,
|
currentThreadId: string | null,
|
||||||
setThreadId: (id: string) => void,
|
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