mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-06 01:52:12 +00:00
feat: Make task completion action agentic (#481)
* feat: Make task completion action agentic * cr * cr * cr * cr * cr * cr * cr * cr * cr * cr * cr * cr * cr * cr
This commit is contained in:
parent
45e1a67171
commit
5aaa814f66
20 changed files with 400 additions and 325 deletions
|
|
@ -8,19 +8,20 @@ import {
|
||||||
import {
|
import {
|
||||||
generateAction,
|
generateAction,
|
||||||
takeAction,
|
takeAction,
|
||||||
progressPlanStep,
|
|
||||||
generateConclusion,
|
generateConclusion,
|
||||||
openPullRequest,
|
openPullRequest,
|
||||||
diagnoseError,
|
diagnoseError,
|
||||||
requestHelp,
|
requestHelp,
|
||||||
updatePlan,
|
updatePlan,
|
||||||
summarizeHistory,
|
summarizeHistory,
|
||||||
|
handleCompletedTask,
|
||||||
} from "./nodes/index.js";
|
} from "./nodes/index.js";
|
||||||
import { BaseMessage, isAIMessage } from "@langchain/core/messages";
|
import { BaseMessage, isAIMessage } from "@langchain/core/messages";
|
||||||
import { initializeSandbox } from "../shared/initialize-sandbox.js";
|
import { initializeSandbox } from "../shared/initialize-sandbox.js";
|
||||||
import { graph as reviewerGraph } from "../reviewer/index.js";
|
import { graph as reviewerGraph } from "../reviewer/index.js";
|
||||||
import { getRemainingPlanItems } from "../../utils/current-task.js";
|
import { getRemainingPlanItems } from "../../utils/current-task.js";
|
||||||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||||
|
import { createMarkTaskCompletedToolFields } from "@open-swe/shared/open-swe/tools";
|
||||||
|
|
||||||
function lastMessagesMissingToolCalls(
|
function lastMessagesMissingToolCalls(
|
||||||
messages: BaseMessage[],
|
messages: BaseMessage[],
|
||||||
|
|
@ -40,7 +41,7 @@ function lastMessagesMissingToolCalls(
|
||||||
* Otherwise, it ends the process.
|
* Otherwise, it ends the process.
|
||||||
*
|
*
|
||||||
* @param {GraphState} state - The current graph state.
|
* @param {GraphState} state - The current graph state.
|
||||||
* @returns {"route-to-review-or-conclusion" | "take-action" | "request-help" | "generate-action" | Send} The next node to execute, or END if the process should stop.
|
* @returns {"route-to-review-or-conclusion" | "take-action" | "request-help" | "generate-action" | "handle-completed-task" | Send} The next node to execute, or END if the process should stop.
|
||||||
*/
|
*/
|
||||||
function routeGeneratedAction(
|
function routeGeneratedAction(
|
||||||
state: GraphState,
|
state: GraphState,
|
||||||
|
|
@ -49,6 +50,7 @@ function routeGeneratedAction(
|
||||||
| "take-action"
|
| "take-action"
|
||||||
| "request-help"
|
| "request-help"
|
||||||
| "generate-action"
|
| "generate-action"
|
||||||
|
| "handle-completed-task"
|
||||||
| Send {
|
| Send {
|
||||||
const { internalMessages } = state;
|
const { internalMessages } = state;
|
||||||
const lastMessage = internalMessages[internalMessages.length - 1];
|
const lastMessage = internalMessages[internalMessages.length - 1];
|
||||||
|
|
@ -71,6 +73,12 @@ function routeGeneratedAction(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const taskMarkedCompleted =
|
||||||
|
toolCall.name === createMarkTaskCompletedToolFields().name;
|
||||||
|
if (taskMarkedCompleted) {
|
||||||
|
return "handle-completed-task";
|
||||||
|
}
|
||||||
|
|
||||||
return "take-action";
|
return "take-action";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,10 +130,10 @@ const workflow = new StateGraph(GraphAnnotation, GraphConfiguration)
|
||||||
.addNode("initialize", initializeSandbox)
|
.addNode("initialize", initializeSandbox)
|
||||||
.addNode("generate-action", generateAction)
|
.addNode("generate-action", generateAction)
|
||||||
.addNode("take-action", takeAction, {
|
.addNode("take-action", takeAction, {
|
||||||
ends: ["progress-plan-step", "diagnose-error"],
|
ends: ["generate-action", "diagnose-error"],
|
||||||
})
|
})
|
||||||
.addNode("update-plan", updatePlan)
|
.addNode("update-plan", updatePlan)
|
||||||
.addNode("progress-plan-step", progressPlanStep, {
|
.addNode("handle-completed-task", handleCompletedTask, {
|
||||||
ends: [
|
ends: [
|
||||||
"summarize-history",
|
"summarize-history",
|
||||||
"generate-action",
|
"generate-action",
|
||||||
|
|
@ -151,6 +159,7 @@ const workflow = new StateGraph(GraphAnnotation, GraphConfiguration)
|
||||||
"route-to-review-or-conclusion",
|
"route-to-review-or-conclusion",
|
||||||
"update-plan",
|
"update-plan",
|
||||||
"generate-action",
|
"generate-action",
|
||||||
|
"handle-completed-task",
|
||||||
])
|
])
|
||||||
.addEdge("update-plan", "generate-action")
|
.addEdge("update-plan", "generate-action")
|
||||||
.addEdge("diagnose-error", "generate-action")
|
.addEdge("diagnose-error", "generate-action")
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import {
|
import {
|
||||||
GraphState,
|
GraphState,
|
||||||
GraphConfig,
|
GraphConfig,
|
||||||
|
|
@ -45,16 +46,16 @@ import {
|
||||||
convertMessagesToCacheControlledMessages,
|
convertMessagesToCacheControlledMessages,
|
||||||
trackCachePerformance,
|
trackCachePerformance,
|
||||||
} from "../../../../utils/caching.js";
|
} from "../../../../utils/caching.js";
|
||||||
|
import { createMarkTaskCompletedToolFields } from "@open-swe/shared/open-swe/tools";
|
||||||
|
import { HumanMessage } from "@langchain/core/messages";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
|
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
|
||||||
|
|
||||||
const formatDynamicContextPrompt = (state: GraphState) => {
|
const formatDynamicContextPrompt = (state: GraphState) => {
|
||||||
return DYNAMIC_SYSTEM_PROMPT.replaceAll(
|
const planString = getActivePlanItems(state.taskPlan)
|
||||||
"{PLAN_PROMPT_WITH_SUMMARIES}",
|
.map((i) => `<plan-item index="${i.index}">\n${i.plan}\n</plan-item>`)
|
||||||
formatPlanPrompt(getActivePlanItems(state.taskPlan), {
|
.join("\n");
|
||||||
includeSummaries: true,
|
return DYNAMIC_SYSTEM_PROMPT.replaceAll("{PLAN_PROMPT}", planString)
|
||||||
}),
|
|
||||||
)
|
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
"{PLAN_GENERATION_NOTES}",
|
"{PLAN_GENERATION_NOTES}",
|
||||||
state.contextGatheringNotes || "No context gathering notes available.",
|
state.contextGatheringNotes || "No context gathering notes available.",
|
||||||
|
|
@ -113,6 +114,27 @@ const formatCacheablePrompt = (state: GraphState): CacheablePromptSegment[] => {
|
||||||
return segments.filter((segment) => segment.text.trim() !== "");
|
return segments.filter((segment) => segment.text.trim() !== "");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const planSpecificPrompt = `<detailed_plan_information>
|
||||||
|
Here is the task execution plan for the request you're working on.
|
||||||
|
Ensure you carefully read through all of the instructions, messages, and context provided above.
|
||||||
|
Once you have a clear understanding of the current state of the task, analyze the plan provided below, and take an action based on it.
|
||||||
|
You're provided with the full list of tasks, including the completed, current and remaining tasks.
|
||||||
|
|
||||||
|
You are in the process of executing the current task:
|
||||||
|
|
||||||
|
{PLAN_PROMPT}
|
||||||
|
</detailed_plan_information>`;
|
||||||
|
|
||||||
|
const formatSpecificPlanPrompt = (state: GraphState): HumanMessage => {
|
||||||
|
return new HumanMessage({
|
||||||
|
id: uuidv4(),
|
||||||
|
content: planSpecificPrompt.replace(
|
||||||
|
"{PLAN_PROMPT}",
|
||||||
|
formatPlanPrompt(getActivePlanItems(state.taskPlan)),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export async function generateAction(
|
export async function generateAction(
|
||||||
state: GraphState,
|
state: GraphState,
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
|
|
@ -123,6 +145,7 @@ export async function generateAction(
|
||||||
Task.PROGRAMMER,
|
Task.PROGRAMMER,
|
||||||
);
|
);
|
||||||
const mcpTools = await getMcpTools(config);
|
const mcpTools = await getMcpTools(config);
|
||||||
|
const markTaskCompletedTool = createMarkTaskCompletedToolFields();
|
||||||
|
|
||||||
const tools = [
|
const tools = [
|
||||||
createSearchTool(state),
|
createSearchTool(state),
|
||||||
|
|
@ -132,6 +155,7 @@ export async function generateAction(
|
||||||
createUpdatePlanToolFields(),
|
createUpdatePlanToolFields(),
|
||||||
createGetURLContentTool(),
|
createGetURLContentTool(),
|
||||||
createInstallDependenciesTool(state),
|
createInstallDependenciesTool(state),
|
||||||
|
markTaskCompletedTool,
|
||||||
...mcpTools,
|
...mcpTools,
|
||||||
];
|
];
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -176,6 +200,7 @@ export async function generateAction(
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
...inputMessagesWithCache,
|
...inputMessagesWithCache,
|
||||||
|
formatSpecificPlanPrompt(state),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const hasToolCalls = !!response.tool_calls?.length;
|
const hasToolCalls = !!response.tool_calls?.length;
|
||||||
|
|
@ -186,6 +211,22 @@ export async function generateAction(
|
||||||
newSandboxSessionId = await stopSandbox(state.sandboxSessionId);
|
newSandboxSessionId = await stopSandbox(state.sandboxSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
response.tool_calls?.length &&
|
||||||
|
response.tool_calls?.length > 1 &&
|
||||||
|
response.tool_calls.some((t) => t.name === markTaskCompletedTool.name)
|
||||||
|
) {
|
||||||
|
logger.error(
|
||||||
|
"Multiple tool calls found, including mark_task_completed. Removing the mark_task_completed call.",
|
||||||
|
{
|
||||||
|
toolCalls: JSON.stringify(response.tool_calls, null, 2),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
response.tool_calls = response.tool_calls.filter(
|
||||||
|
(t) => t.name !== markTaskCompletedTool.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Generated action", {
|
logger.info("Generated action", {
|
||||||
currentTask: getCurrentPlanItem(getActivePlanItems(state.taskPlan)).plan,
|
currentTask: getCurrentPlanItem(getActivePlanItems(state.taskPlan)).plan,
|
||||||
...(getMessageContentString(response.content) && {
|
...(getMessageContentString(response.content) && {
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,15 @@ You are a terminal-based agentic coding assistant built by LangChain. You wrap L
|
||||||
<description>Use this tool to add or remove tasks from the plan, or to update the plan in any other way</description>
|
<description>Use this tool to add or remove tasks from the plan, or to update the plan in any other way</description>
|
||||||
</special_tools>
|
</special_tools>
|
||||||
|
|
||||||
|
<mark_task_completed_guidelines>
|
||||||
|
- When you believe you've completed a task, you may call the \`mark_task_completed\` tool to mark the task as complete.
|
||||||
|
- The \`mark_task_completed\` tool should NEVER be called in parallel with any other tool calls. Ensure it's the only tool you're calling in this message, if you do determine the task is completed.
|
||||||
|
- Carefully read over the actions you've taken, and the current task (listed below) to ensure the task is complete. You want to avoid prematurely marking a task as complete.
|
||||||
|
- If the current task involves fixing an issue, such as a failing test, a broken build, etc., you must validate the issue is ACTUALLY fixed before marking it as complete.
|
||||||
|
- To verify a fix, ensure you run the test, build, or other command first to validate the fix.
|
||||||
|
- If you do not believe the task is complete, you do not need to call the \`mark_task_completed\` tool. You can continue working on the task, until you determine it is complete.
|
||||||
|
</mark_task_completed_guidelines>
|
||||||
|
|
||||||
</instructions>
|
</instructions>
|
||||||
|
|
||||||
<custom_rules>
|
<custom_rules>
|
||||||
|
|
@ -113,8 +122,10 @@ export const CODE_REVIEW_PROMPT = `<code_review>
|
||||||
export const DYNAMIC_SYSTEM_PROMPT = `<context>
|
export const DYNAMIC_SYSTEM_PROMPT = `<context>
|
||||||
|
|
||||||
<plan_information>
|
<plan_information>
|
||||||
- Current plan with summaries
|
- Task execution plan
|
||||||
{PLAN_PROMPT_WITH_SUMMARIES}
|
<execution_plan>
|
||||||
|
{PLAN_PROMPT}
|
||||||
|
</execution_plan>
|
||||||
|
|
||||||
- Plan generation notes
|
- Plan generation notes
|
||||||
These are notes you took while gathering context for the plan:
|
These are notes you took while gathering context for the plan:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
||||||
|
import {
|
||||||
|
GraphConfig,
|
||||||
|
GraphState,
|
||||||
|
GraphUpdate,
|
||||||
|
} from "@open-swe/shared/open-swe/types";
|
||||||
|
import { Command } from "@langchain/langgraph";
|
||||||
|
import {
|
||||||
|
completePlanItem,
|
||||||
|
getActivePlanItems,
|
||||||
|
getActiveTask,
|
||||||
|
} from "@open-swe/shared/open-swe/tasks";
|
||||||
|
import {
|
||||||
|
getCurrentPlanItem,
|
||||||
|
getRemainingPlanItems,
|
||||||
|
} from "../../../utils/current-task.js";
|
||||||
|
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
|
||||||
|
import { addTaskPlanToIssue } from "../../../utils/github/issue-task.js";
|
||||||
|
import { createMarkTaskCompletedToolFields } from "@open-swe/shared/open-swe/tools";
|
||||||
|
import {
|
||||||
|
calculateConversationHistoryTokenCount,
|
||||||
|
getMessagesSinceLastSummary,
|
||||||
|
MAX_INTERNAL_TOKENS,
|
||||||
|
} from "../../../utils/tokens.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const logger = createLogger(LogLevel.INFO, "HandleCompletedTask");
|
||||||
|
|
||||||
|
export async function handleCompletedTask(
|
||||||
|
state: GraphState,
|
||||||
|
config: GraphConfig,
|
||||||
|
): Promise<Command> {
|
||||||
|
const markCompletedTool = createMarkTaskCompletedToolFields();
|
||||||
|
const markCompletedMessage =
|
||||||
|
state.internalMessages[state.internalMessages.length - 1];
|
||||||
|
if (
|
||||||
|
!isAIMessage(markCompletedMessage) ||
|
||||||
|
!markCompletedMessage.tool_calls?.length ||
|
||||||
|
!markCompletedMessage.tool_calls.some(
|
||||||
|
(tc) => tc.name === markCompletedTool.name,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("Failed to find a tool call when checking task status.");
|
||||||
|
}
|
||||||
|
const toolCall = markCompletedMessage.tool_calls?.[0];
|
||||||
|
if (!toolCall) {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to generate a tool call when checking task status.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activePlanItems = getActivePlanItems(state.taskPlan);
|
||||||
|
const currentTask = getCurrentPlanItem(activePlanItems);
|
||||||
|
const toolMessage = new ToolMessage({
|
||||||
|
id: uuidv4(),
|
||||||
|
tool_call_id: toolCall.id ?? "",
|
||||||
|
content: `Saved task status as completed for task ${currentTask?.plan || "unknown"}`,
|
||||||
|
name: toolCall.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newMessages = [toolMessage];
|
||||||
|
|
||||||
|
const newMessageList = [...state.internalMessages, ...newMessages];
|
||||||
|
const wouldBeConversationHistoryToSummarize =
|
||||||
|
await getMessagesSinceLastSummary(newMessageList, {
|
||||||
|
excludeHiddenMessages: true,
|
||||||
|
excludeCountFromEnd: 20,
|
||||||
|
});
|
||||||
|
const totalInternalTokenCount = calculateConversationHistoryTokenCount(
|
||||||
|
wouldBeConversationHistoryToSummarize,
|
||||||
|
{
|
||||||
|
// Retain the last 20 messages from state
|
||||||
|
excludeHiddenMessages: true,
|
||||||
|
excludeCountFromEnd: 20,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const summary = (toolCall.args as z.infer<typeof markCompletedTool.schema>)
|
||||||
|
.completed_task_summary;
|
||||||
|
|
||||||
|
// LLM marked as completed, so we need to update the plan to reflect that.
|
||||||
|
const updatedPlanTasks = completePlanItem(
|
||||||
|
state.taskPlan,
|
||||||
|
getActiveTask(state.taskPlan).id,
|
||||||
|
currentTask.index,
|
||||||
|
summary,
|
||||||
|
);
|
||||||
|
// Update the github issue to reflect this task as completed.
|
||||||
|
await addTaskPlanToIssue(
|
||||||
|
{
|
||||||
|
githubIssueId: state.githubIssueId,
|
||||||
|
targetRepository: state.targetRepository,
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
updatedPlanTasks,
|
||||||
|
);
|
||||||
|
|
||||||
|
const commandUpdate: GraphUpdate = {
|
||||||
|
messages: newMessages,
|
||||||
|
internalMessages: newMessages,
|
||||||
|
// Even though there are no remaining tasks, still mark as completed so the UI reflects that the task is completed.
|
||||||
|
taskPlan: updatedPlanTasks,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This should in theory never happen, but ensure we route properly if it does.
|
||||||
|
const remainingTask = getRemainingPlanItems(activePlanItems)?.[0];
|
||||||
|
if (!remainingTask) {
|
||||||
|
logger.info(
|
||||||
|
"Found no remaining tasks in the plan during the check plan step. Continuing to the conclusion generation step.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Command({
|
||||||
|
goto: "route-to-review-or-conclusion",
|
||||||
|
update: commandUpdate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalInternalTokenCount >= MAX_INTERNAL_TOKENS) {
|
||||||
|
logger.info(
|
||||||
|
"Internal messages list is at or above the max token limit. Routing to summarize history step.",
|
||||||
|
{
|
||||||
|
totalInternalTokenCount,
|
||||||
|
maxInternalTokenCount: MAX_INTERNAL_TOKENS,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Command({
|
||||||
|
goto: "summarize-history",
|
||||||
|
update: commandUpdate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Command({
|
||||||
|
goto: "generate-action",
|
||||||
|
update: commandUpdate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * from "./generate-message/index.js";
|
export * from "./generate-message/index.js";
|
||||||
export * from "./take-action.js";
|
export * from "./take-action.js";
|
||||||
export * from "./progress-plan-step.js";
|
export * from "./handle-completed-task.js";
|
||||||
export * from "./generate-conclusion.js";
|
export * from "./generate-conclusion.js";
|
||||||
export * from "./open-pr.js";
|
export * from "./open-pr.js";
|
||||||
export * from "./diagnose-error.js";
|
export * from "./diagnose-error.js";
|
||||||
|
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
|
||||||
import {
|
|
||||||
GraphConfig,
|
|
||||||
GraphState,
|
|
||||||
GraphUpdate,
|
|
||||||
PlanItem,
|
|
||||||
} from "@open-swe/shared/open-swe/types";
|
|
||||||
import {
|
|
||||||
loadModel,
|
|
||||||
supportsParallelToolCallsParam,
|
|
||||||
Task,
|
|
||||||
} from "../../../utils/load-model.js";
|
|
||||||
import { formatPlanPrompt } from "../../../utils/plan-prompt.js";
|
|
||||||
import { Command } from "@langchain/langgraph";
|
|
||||||
import { getMessageString } from "../../../utils/message/content.js";
|
|
||||||
import { formatUserRequestPrompt } from "../../../utils/user-request.js";
|
|
||||||
import {
|
|
||||||
completePlanItem,
|
|
||||||
getActivePlanItems,
|
|
||||||
getActiveTask,
|
|
||||||
} from "@open-swe/shared/open-swe/tasks";
|
|
||||||
import {
|
|
||||||
getCurrentPlanItem,
|
|
||||||
getRemainingPlanItems,
|
|
||||||
} from "../../../utils/current-task.js";
|
|
||||||
import { ToolMessage } from "@langchain/core/messages";
|
|
||||||
import { addTaskPlanToIssue } from "../../../utils/github/issue-task.js";
|
|
||||||
import {
|
|
||||||
createMarkTaskNotCompletedToolFields,
|
|
||||||
createMarkTaskCompletedToolFields,
|
|
||||||
} from "@open-swe/shared/open-swe/tools";
|
|
||||||
import {
|
|
||||||
calculateConversationHistoryTokenCount,
|
|
||||||
getMessagesSinceLastSummary,
|
|
||||||
MAX_INTERNAL_TOKENS,
|
|
||||||
} from "../../../utils/tokens.js";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { trackCachePerformance } from "../../../utils/caching.js";
|
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "ProgressPlanStep");
|
|
||||||
|
|
||||||
const systemPrompt = `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.
|
|
||||||
|
|
||||||
In your workflow, you generate a plan, then act on said plan. It may take many actions to complete a single step, or a single action to complete the step.
|
|
||||||
|
|
||||||
Here is the plan, along with the summaries of each completed task:
|
|
||||||
{PLAN_PROMPT}
|
|
||||||
|
|
||||||
Analyze the tasks you've completed, the tasks which are remaining, and the current task you just took an action on.
|
|
||||||
In addition to this, you're also provided the full conversation history between you and the user. All of the messages in this conversation are from the previous steps/actions you've taken, and any user input.
|
|
||||||
If the task you're working on is to fix a failing command (e.g. a test, build, lint, etc.), and you've made changes to fix the issue, you must re-run the command to ensure the fix was successful before you can mark the task as complete.
|
|
||||||
For example: If you have a failing test, and you've applied an update to the file to fix the test, you MUST re-run the test before you can mark the task as complete.
|
|
||||||
|
|
||||||
Take all of this information, and determine if the current task is complete, or if you still have work left to do.
|
|
||||||
Once you've determined the status of the current task, call either:
|
|
||||||
- \`mark_task_completed\` if the task is complete.
|
|
||||||
- \`mark_task_not_completed\` if the task is not complete.
|
|
||||||
`;
|
|
||||||
|
|
||||||
const formatPrompt = (taskPlan: PlanItem[]): string => {
|
|
||||||
return systemPrompt.replace(
|
|
||||||
"{PLAN_PROMPT}",
|
|
||||||
formatPlanPrompt(taskPlan, { includeSummaries: true }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function progressPlanStep(
|
|
||||||
state: GraphState,
|
|
||||||
config: GraphConfig,
|
|
||||||
): Promise<Command> {
|
|
||||||
const markNotCompletedTool = createMarkTaskNotCompletedToolFields();
|
|
||||||
const markCompletedTool = createMarkTaskCompletedToolFields();
|
|
||||||
const model = await loadModel(config, Task.SUMMARIZER);
|
|
||||||
const modelSupportsParallelToolCallsParam = supportsParallelToolCallsParam(
|
|
||||||
config,
|
|
||||||
Task.SUMMARIZER,
|
|
||||||
);
|
|
||||||
const modelWithTools = model.bindTools(
|
|
||||||
[markNotCompletedTool, markCompletedTool],
|
|
||||||
{
|
|
||||||
tool_choice: "any",
|
|
||||||
...(modelSupportsParallelToolCallsParam
|
|
||||||
? {
|
|
||||||
parallel_tool_calls: false,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const conversationHistoryStr = `Here is the full conversation history including the user's request(s):
|
|
||||||
|
|
||||||
${state.internalMessages.map(getMessageString).join("\n")}
|
|
||||||
|
|
||||||
${formatUserRequestPrompt(state.internalMessages)}
|
|
||||||
|
|
||||||
Take all of this information, and determine whether or not you have completed this task in the plan.
|
|
||||||
Once you've determined the status of the current task, call either the \`mark_task_completed\` or \`mark_task_not_completed\` tool.`;
|
|
||||||
|
|
||||||
const activePlanItems = getActivePlanItems(state.taskPlan);
|
|
||||||
|
|
||||||
const response = await modelWithTools.invoke([
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: formatPrompt(activePlanItems),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: conversationHistoryStr,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const toolCall = response.tool_calls?.[0];
|
|
||||||
|
|
||||||
if (!toolCall) {
|
|
||||||
throw new Error(
|
|
||||||
"Failed to generate a tool call when checking task status.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCompleted = toolCall.name === markCompletedTool.name;
|
|
||||||
const currentTask = getCurrentPlanItem(activePlanItems);
|
|
||||||
const toolMessage = new ToolMessage({
|
|
||||||
id: uuidv4(),
|
|
||||||
tool_call_id: toolCall.id ?? "",
|
|
||||||
content: `Saved task status as ${isCompleted ? "completed" : "not completed"} for task ${currentTask?.plan || "unknown"}`,
|
|
||||||
name: toolCall.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newMessages = [response, toolMessage];
|
|
||||||
|
|
||||||
const newMessageList = [...state.internalMessages, ...newMessages];
|
|
||||||
const wouldBeConversationHistoryToSummarize =
|
|
||||||
await getMessagesSinceLastSummary(newMessageList, {
|
|
||||||
excludeHiddenMessages: true,
|
|
||||||
excludeCountFromEnd: 20,
|
|
||||||
});
|
|
||||||
const totalInternalTokenCount = calculateConversationHistoryTokenCount(
|
|
||||||
wouldBeConversationHistoryToSummarize,
|
|
||||||
{
|
|
||||||
// Retain the last 20 messages from state
|
|
||||||
excludeHiddenMessages: true,
|
|
||||||
excludeCountFromEnd: 20,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isCompleted) {
|
|
||||||
logger.info("Current task has not been completed.", {
|
|
||||||
reasoning: toolCall.args.reasoning,
|
|
||||||
});
|
|
||||||
const commandUpdate: GraphUpdate = {
|
|
||||||
messages: newMessages,
|
|
||||||
internalMessages: newMessages,
|
|
||||||
tokenData: trackCachePerformance(response),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if we have any messages to summarize, and if we're at or above the max token limit.
|
|
||||||
if (totalInternalTokenCount >= MAX_INTERNAL_TOKENS) {
|
|
||||||
logger.info(
|
|
||||||
"Internal messages list is at or above the max token limit. Routing to summarize history step.",
|
|
||||||
{
|
|
||||||
totalInternalTokenCount,
|
|
||||||
maxInternalTokenCount: MAX_INTERNAL_TOKENS,
|
|
||||||
wouldBeConversationHistoryToSummarizeLength:
|
|
||||||
wouldBeConversationHistoryToSummarize.length,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return new Command({
|
|
||||||
goto: "summarize-history",
|
|
||||||
update: commandUpdate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Command({
|
|
||||||
goto: "generate-action",
|
|
||||||
update: commandUpdate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const summary = (toolCall.args as z.infer<typeof markCompletedTool.schema>)
|
|
||||||
.completed_task_summary;
|
|
||||||
|
|
||||||
// LLM marked as completed, so we need to update the plan to reflect that.
|
|
||||||
const updatedPlanTasks = completePlanItem(
|
|
||||||
state.taskPlan,
|
|
||||||
getActiveTask(state.taskPlan).id,
|
|
||||||
currentTask.index,
|
|
||||||
summary,
|
|
||||||
);
|
|
||||||
// Update the github issue to reflect this task as completed.
|
|
||||||
await addTaskPlanToIssue(
|
|
||||||
{
|
|
||||||
githubIssueId: state.githubIssueId,
|
|
||||||
targetRepository: state.targetRepository,
|
|
||||||
},
|
|
||||||
config,
|
|
||||||
updatedPlanTasks,
|
|
||||||
);
|
|
||||||
|
|
||||||
// This should in theory never happen, but ensure we route properly if it does.
|
|
||||||
const remainingTask = getRemainingPlanItems(activePlanItems)?.[0];
|
|
||||||
if (!remainingTask) {
|
|
||||||
logger.info(
|
|
||||||
"Found no remaining tasks in the plan during the check plan step. Continuing to the conclusion generation step.",
|
|
||||||
);
|
|
||||||
const commandUpdate: GraphUpdate = {
|
|
||||||
messages: newMessages,
|
|
||||||
internalMessages: newMessages,
|
|
||||||
// Even though there are no remaining tasks, still mark as completed so the UI reflects that the task is completed.
|
|
||||||
taskPlan: updatedPlanTasks,
|
|
||||||
tokenData: trackCachePerformance(response),
|
|
||||||
};
|
|
||||||
return new Command({
|
|
||||||
goto: "route-to-review-or-conclusion",
|
|
||||||
update: commandUpdate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Task marked as completed. Routing to task summarization step.", {
|
|
||||||
remainingTask: {
|
|
||||||
...remainingTask,
|
|
||||||
completed: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const commandUpdate: GraphUpdate = {
|
|
||||||
messages: newMessages,
|
|
||||||
internalMessages: newMessages,
|
|
||||||
taskPlan: updatedPlanTasks,
|
|
||||||
tokenData: trackCachePerformance(response),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (totalInternalTokenCount >= MAX_INTERNAL_TOKENS) {
|
|
||||||
logger.info(
|
|
||||||
"Internal messages list is at or above the max token limit. Routing to summarize history step.",
|
|
||||||
{
|
|
||||||
totalInternalTokenCount,
|
|
||||||
maxInternalTokenCount: MAX_INTERNAL_TOKENS,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return new Command({
|
|
||||||
goto: "summarize-history",
|
|
||||||
update: commandUpdate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Command({
|
|
||||||
goto: "generate-action",
|
|
||||||
update: commandUpdate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -211,7 +211,7 @@ export async function takeAction(
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
return new Command({
|
return new Command({
|
||||||
goto: shouldRouteDiagnoseNode ? "diagnose-error" : "progress-plan-step",
|
goto: shouldRouteDiagnoseNode ? "diagnose-error" : "generate-action",
|
||||||
update: commandUpdate,
|
update: commandUpdate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { PlanItem } from "@open-swe/shared/open-swe/types";
|
import { PlanItem } from "@open-swe/shared/open-swe/types";
|
||||||
|
|
||||||
export const PLAN_PROMPT = `## Completed Tasks
|
export const PLAN_PROMPT = `<completed_tasks>
|
||||||
{COMPLETED_TASKS}
|
{COMPLETED_TASKS}
|
||||||
|
</completed_tasks>
|
||||||
|
|
||||||
## Remaining Tasks
|
<remaining_tasks>
|
||||||
(This list does not include the current task)
|
(This list does not include the current task)
|
||||||
{REMAINING_TASKS}
|
{REMAINING_TASKS}
|
||||||
|
</remaining_tasks>
|
||||||
|
|
||||||
## Current Task
|
{CURRENT_TASK}
|
||||||
{CURRENT_TASK}`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a plan for use in a prompt.
|
* Formats a plan for use in a prompt.
|
||||||
|
|
@ -50,7 +52,7 @@ export function formatPlanPrompt(
|
||||||
: completedTasks
|
: completedTasks
|
||||||
.map(
|
.map(
|
||||||
(task) =>
|
(task) =>
|
||||||
`<completed-task index="${task.index}">${task.plan}</completed-task>`,
|
`<completed_task index="${task.index}">\n${task.plan}\n</completed_task>`,
|
||||||
)
|
)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
: "No completed tasks.",
|
: "No completed tasks.",
|
||||||
|
|
@ -61,14 +63,14 @@ export function formatPlanPrompt(
|
||||||
? remainingTasks
|
? remainingTasks
|
||||||
.map(
|
.map(
|
||||||
(task) =>
|
(task) =>
|
||||||
`<remaining-task index="${task.index}">${task.plan}</remaining-task>`,
|
`<remaining_task index="${task.index}">\n${task.plan}\n</remaining_task>`,
|
||||||
)
|
)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
: "No remaining tasks.",
|
: "No remaining tasks.",
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"{CURRENT_TASK}",
|
"{CURRENT_TASK}",
|
||||||
`<current-task index="${currentTask?.index}">${currentTask?.plan || "No current task found."}</current-task>`,
|
`<current_task index="${currentTask?.index}">\n${currentTask?.plan || "No current task found."}\n</current_task>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +78,7 @@ export function formatPlanPromptWithSummaries(taskPlan: PlanItem[]): string {
|
||||||
return taskPlan
|
return taskPlan
|
||||||
.map(
|
.map(
|
||||||
(p) =>
|
(p) =>
|
||||||
`<${p.completed ? "completed-" : ""}task index="${p.index}">\n${p.plan}\n <task-summary>\n${p.summary || "No task summary found"}\n </task-summary>\n</${p.completed ? "completed-" : ""}task>`,
|
`<${p.completed ? "completed_" : ""}task index="${p.index}">\n${p.plan}\n <task_summary>\n${p.summary || "No task summary found"}\n </task_summary>\n</${p.completed ? "completed_" : ""}task>`,
|
||||||
)
|
)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -622,9 +622,9 @@ export function ActionStep(props: ActionStepProps) {
|
||||||
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
||||||
</button>
|
</button>
|
||||||
{showReasoning && (
|
{showReasoning && (
|
||||||
<p className="mt-1 text-xs font-normal text-blue-700 dark:text-blue-300">
|
<BasicMarkdownText className="mt-1 text-xs font-normal text-blue-700 dark:text-blue-300">
|
||||||
{reasoningText || "No reasoning provided."}
|
{reasoningText || "No reasoning provided."}
|
||||||
</p>
|
</BasicMarkdownText>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,9 +120,9 @@ export function PullRequestOpened({
|
||||||
<h3 className="mb-1 text-xs font-normal text-gray-500 dark:text-gray-400">
|
<h3 className="mb-1 text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||||
Description
|
Description
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-xs font-normal whitespace-pre-wrap text-gray-800 dark:text-gray-200">
|
<pre className="text-xs font-normal whitespace-pre-wrap text-gray-800 dark:text-gray-200">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,9 @@ export function PushChanges({
|
||||||
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
||||||
</button>
|
</button>
|
||||||
{showReasoning && (
|
{showReasoning && (
|
||||||
<p className="mt-1 text-xs font-normal text-blue-800">
|
<BasicMarkdownText className="mt-1 text-xs font-normal text-blue-800">
|
||||||
{reasoningText}
|
{reasoningText}
|
||||||
</p>
|
</BasicMarkdownText>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,9 @@ export function ReplanningStep({
|
||||||
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
||||||
</button>
|
</button>
|
||||||
{showReasoning && (
|
{showReasoning && (
|
||||||
<p className="mt-1 text-xs font-normal text-blue-800">
|
<BasicMarkdownText className="mt-1 text-xs font-normal text-blue-800">
|
||||||
{reasoningText}
|
{reasoningText}
|
||||||
</p>
|
</BasicMarkdownText>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,9 @@ export function MarkTaskCompleted({
|
||||||
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
||||||
</button>
|
</button>
|
||||||
{showReasoning && (
|
{showReasoning && (
|
||||||
<p className="mt-1 text-xs font-normal text-blue-800 dark:text-blue-400">
|
<BasicMarkdownText className="mt-1 text-xs font-normal text-blue-800 dark:text-blue-400">
|
||||||
{reasoningText}
|
{reasoningText}
|
||||||
</p>
|
</BasicMarkdownText>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -194,9 +194,9 @@ export function MarkTaskIncomplete({
|
||||||
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
||||||
</button>
|
</button>
|
||||||
{showReasoning && (
|
{showReasoning && (
|
||||||
<p className="mt-1 text-xs font-normal text-blue-800">
|
<BasicMarkdownText className="mt-1 text-xs font-normal text-blue-800">
|
||||||
{reasoningText}
|
{reasoningText}
|
||||||
</p>
|
</BasicMarkdownText>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ProgressBar } from "./progress-bar";
|
import { ProgressBar } from "./progress-bar";
|
||||||
import { PlanItem, TaskPlan } from "@open-swe/shared/open-swe/types";
|
import { PlanItem, TaskPlan } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { BasicMarkdownText } from "../thread/markdown-text";
|
||||||
|
|
||||||
interface TasksSidebarProps {
|
interface TasksSidebarProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -327,7 +328,7 @@ export function TasksSidebar({
|
||||||
<>
|
<>
|
||||||
<div className="mb-1 flex items-start justify-between gap-2">
|
<div className="mb-1 flex items-start justify-between gap-2">
|
||||||
<p className="text-sm leading-relaxed text-gray-900 dark:text-gray-100">
|
<p className="text-sm leading-relaxed text-gray-900 dark:text-gray-100">
|
||||||
{item.plan}
|
<BasicMarkdownText>{item.plan}</BasicMarkdownText>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -384,7 +385,9 @@ export function TasksSidebar({
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="mt-2">
|
<CollapsibleContent className="mt-2">
|
||||||
<div className="rounded border border-green-200 bg-green-50 p-2 text-xs text-green-800 dark:border-green-700 dark:bg-green-900/30 dark:text-green-400">
|
<div className="rounded border border-green-200 bg-green-50 p-2 text-xs text-green-800 dark:border-green-700 dark:bg-green-900/30 dark:text-green-400">
|
||||||
{item.summary}
|
<BasicMarkdownText>
|
||||||
|
{item.summary}
|
||||||
|
</BasicMarkdownText>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
|
||||||
|
|
@ -289,3 +289,109 @@ const BasicMarkdownTextImpl: FC<{ children: string; className?: string }> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BasicMarkdownText = memo(BasicMarkdownTextImpl);
|
export const BasicMarkdownText = memo(BasicMarkdownTextImpl);
|
||||||
|
|
||||||
|
const InlineMarkdownTextImpl: FC<{ children: string; className?: string }> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const inlineMarkdownComponents: any = {
|
||||||
|
// Only include inline elements
|
||||||
|
strong: ({ className, ...props }: { className?: string }) => (
|
||||||
|
<strong
|
||||||
|
className={cn("font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
em: ({ className, ...props }: { className?: string }) => (
|
||||||
|
<em
|
||||||
|
className={cn("italic", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
code: ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
// Only render inline code, not code blocks
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
if (match) {
|
||||||
|
// If it's a code block, render as plain text to keep it inline
|
||||||
|
return (
|
||||||
|
<span className={cn("font-mono text-sm", className)}>
|
||||||
|
{String(children)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className={cn(
|
||||||
|
"bg-muted rounded px-1 py-0.5 font-mono text-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
a: ({ className, ...props }: { className?: string }) => (
|
||||||
|
<a
|
||||||
|
className={cn(
|
||||||
|
"text-primary hover:text-primary/80 font-medium underline underline-offset-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
del: ({ className, ...props }: { className?: string }) => (
|
||||||
|
<del
|
||||||
|
className={cn("line-through", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
// Remove all block-level elements by not including them
|
||||||
|
// This will cause them to render as plain text
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cn("inline", className)}>
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={inlineMarkdownComponents}
|
||||||
|
// Disable block-level parsing by treating everything as inline
|
||||||
|
disallowedElements={[
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"p",
|
||||||
|
"div",
|
||||||
|
"blockquote",
|
||||||
|
"ul",
|
||||||
|
"ol",
|
||||||
|
"li",
|
||||||
|
"pre",
|
||||||
|
"table",
|
||||||
|
"thead",
|
||||||
|
"tbody",
|
||||||
|
"tr",
|
||||||
|
"th",
|
||||||
|
"td",
|
||||||
|
"hr",
|
||||||
|
"br",
|
||||||
|
]}
|
||||||
|
unwrapDisallowed
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InlineMarkdownText = memo(InlineMarkdownTextImpl);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ function TabsTrigger({
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-border text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { BasicMarkdownText } from "../thread/markdown-text";
|
||||||
|
|
||||||
type DiagnoseErrorActionProps = {
|
type DiagnoseErrorActionProps = {
|
||||||
status: "loading" | "generating" | "done";
|
status: "loading" | "generating" | "done";
|
||||||
|
|
@ -57,9 +58,9 @@ export function DiagnoseErrorAction({
|
||||||
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
||||||
</button>
|
</button>
|
||||||
{showReasoning && (
|
{showReasoning && (
|
||||||
<p className="mt-1 text-xs font-normal text-blue-800 dark:text-blue-300">
|
<BasicMarkdownText className="mt-1 text-xs font-normal text-blue-800 dark:text-blue-300">
|
||||||
{reasoningText}
|
{reasoningText}
|
||||||
</p>
|
</BasicMarkdownText>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { ThreadUIStatus } from "@/lib/schemas/thread-status";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { TaskPlan } from "@open-swe/shared/open-swe/types";
|
import { TaskPlan } from "@open-swe/shared/open-swe/types";
|
||||||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||||
|
import { InlineMarkdownText } from "../thread/markdown-text";
|
||||||
|
|
||||||
interface ThreadCardProps {
|
interface ThreadCardProps {
|
||||||
thread: ThreadMetadata;
|
thread: ThreadMetadata;
|
||||||
|
|
@ -133,10 +134,10 @@ export function ThreadCard({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<CardTitle className="text-foreground truncate text-sm font-medium">
|
<CardTitle className="text-foreground truncate text-sm">
|
||||||
{thread.title}
|
<InlineMarkdownText>{thread.title}</InlineMarkdownText>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="mt-1 flex items-center gap-1">
|
<div className="mt-1 flex items-center gap-1">
|
||||||
<GitBranch className="text-muted-foreground h-2 w-2" />
|
<GitBranch className="text-muted-foreground h-2 w-2" />
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ export function ThreadView({
|
||||||
programmerSession.runId !== joinedProgrammerRunId.current
|
programmerSession.runId !== joinedProgrammerRunId.current
|
||||||
) {
|
) {
|
||||||
joinedProgrammerRunId.current = programmerSession.runId;
|
joinedProgrammerRunId.current = programmerSession.runId;
|
||||||
plannerStream.joinStream(programmerSession.runId).catch(console.error);
|
programmerStream.joinStream(programmerSession.runId).catch(console.error);
|
||||||
} else if (!programmerSession?.runId) {
|
} else if (!programmerSession?.runId) {
|
||||||
joinedProgrammerRunId.current = undefined;
|
joinedProgrammerRunId.current = undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -311,19 +311,9 @@ export function ThreadView({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex flex-shrink-0 items-center gap-3">
|
<div className="flex flex-shrink-0 items-center gap-3">
|
||||||
<TabsList className="bg-muted/70 h-13 px-2 dark:bg-gray-800">
|
<TabsList className="bg-muted/70 dark:bg-gray-800">
|
||||||
<TabsTrigger
|
<TabsTrigger value="planner">Planner</TabsTrigger>
|
||||||
className="h-8"
|
<TabsTrigger value="programmer">Programmer</TabsTrigger>
|
||||||
value="planner"
|
|
||||||
>
|
|
||||||
Planner
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
className="h-8"
|
|
||||||
value="programmer"
|
|
||||||
>
|
|
||||||
Programmer
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{programmerTaskPlan && (
|
{programmerTaskPlan && (
|
||||||
|
|
@ -333,7 +323,7 @@ export function ThreadView({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="ml-auto flex items-center justify-center gap-2">
|
||||||
{selectedTab === "planner" && plannerStream.isLoading && (
|
{selectedTab === "planner" && plannerStream.isLoading && (
|
||||||
<CancelStreamButton
|
<CancelStreamButton
|
||||||
stream={plannerStream}
|
stream={plannerStream}
|
||||||
|
|
@ -365,7 +355,7 @@ export function ThreadView({
|
||||||
|
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="planner"
|
value="planner"
|
||||||
className="mb-10"
|
className="mb-2"
|
||||||
>
|
>
|
||||||
<Card className="border-border bg-card relative h-full p-0 dark:bg-gray-950">
|
<Card className="border-border bg-card relative h-full p-0 dark:bg-gray-950">
|
||||||
<CardContent className="h-full p-0">
|
<CardContent className="h-full p-0">
|
||||||
|
|
@ -410,7 +400,7 @@ export function ThreadView({
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="programmer"
|
value="programmer"
|
||||||
className="mb-10"
|
className="mb-2"
|
||||||
>
|
>
|
||||||
<Card className="border-border bg-card relative h-full p-0 dark:bg-gray-950">
|
<Card className="border-border bg-card relative h-full p-0 dark:bg-gray-950">
|
||||||
<CardContent className="h-full p-0">
|
<CardContent className="h-full p-0">
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,15 @@ export function TokenUsage({ tokenData }: TokenUsageProps) {
|
||||||
if (!tokenData || tokenData.length === 0) return null;
|
if (!tokenData || tokenData.length === 0) return null;
|
||||||
|
|
||||||
const mergedTokenData = mergeTokenData(tokenData);
|
const mergedTokenData = mergeTokenData(tokenData);
|
||||||
|
const totalCachedInputTokens =
|
||||||
|
mergedTokenData.cacheCreationInputTokens +
|
||||||
|
mergedTokenData.cacheReadInputTokens;
|
||||||
|
const totalUncachedInputTokens = mergedTokenData.inputTokens;
|
||||||
|
const cachePercentage = (
|
||||||
|
(totalCachedInputTokens /
|
||||||
|
(totalCachedInputTokens + totalUncachedInputTokens)) *
|
||||||
|
100
|
||||||
|
).toFixed(2);
|
||||||
const metrics = calculateCostSavings(mergedTokenData);
|
const metrics = calculateCostSavings(mergedTokenData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -66,7 +75,7 @@ export function TokenUsage({ tokenData }: TokenUsageProps) {
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Zap className="h-3 w-3 text-blue-500" />
|
<Zap className="h-3 w-3 text-blue-500 dark:text-blue-400" />
|
||||||
<span className="text-muted-foreground text-xs font-medium">
|
<span className="text-muted-foreground text-xs font-medium">
|
||||||
Input
|
Input
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -77,7 +86,7 @@ export function TokenUsage({ tokenData }: TokenUsageProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<TrendingUp className="h-3 w-3 text-green-500" />
|
<TrendingUp className="h-3 w-3 text-green-500 dark:text-green-400" />
|
||||||
<span className="text-muted-foreground text-xs font-medium">
|
<span className="text-muted-foreground text-xs font-medium">
|
||||||
Output
|
Output
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -103,7 +112,7 @@ export function TokenUsage({ tokenData }: TokenUsageProps) {
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Coins className="h-3 w-3 text-amber-500" />
|
<Coins className="h-3 w-3 text-amber-500 dark:text-amber-400" />
|
||||||
<span className="text-muted-foreground text-xs font-medium">
|
<span className="text-muted-foreground text-xs font-medium">
|
||||||
Cost
|
Cost
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -114,17 +123,30 @@ export function TokenUsage({ tokenData }: TokenUsageProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{metrics.totalSavings > 0 && (
|
{metrics.totalSavings > 0 && (
|
||||||
<div className="flex items-center justify-between">
|
<>
|
||||||
<span className="text-xs font-medium text-green-600">
|
<div className="flex items-center justify-between">
|
||||||
Cache Savings
|
<span className="text-xs font-medium text-blue-600 dark:text-blue-400">
|
||||||
</span>
|
Cache Percentage
|
||||||
<Badge
|
</span>
|
||||||
variant="outline"
|
<Badge
|
||||||
className="border-green-200 text-green-600"
|
variant="outline"
|
||||||
>
|
className="border-blue-200 text-blue-600 dark:border-blue-800 dark:text-blue-400"
|
||||||
-${metrics.totalSavings.toFixed(2)}
|
>
|
||||||
</Badge>
|
{cachePercentage}%
|
||||||
</div>
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-medium text-green-600 dark:text-green-400">
|
||||||
|
Cache Savings
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-green-200 text-green-600 dark:border-green-800 dark:text-green-400"
|
||||||
|
>
|
||||||
|
-${metrics.totalSavings.toFixed(2)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue