mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-03 02:13:28 +00:00
feat: Implement Request Human Help Tool for Agent-Human Collaboration (#505)
* Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * fix open-swe syntax errors * fix render text input for human_request_help * improve UI/UX post response (close input render human msg) * remove comments * move request_human_help to its own component file * format * update github comment and abstract getopensweurl util * add username tag in request help comment, rm comments * cr * refactor * cr --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Dylan Boudro <121908331+starmorph@users.noreply.github.com> Co-authored-by: starmorph <dylan@starmorph.com> Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
parent
bfa020ef07
commit
2f561796a5
13 changed files with 594 additions and 123 deletions
|
|
@ -39,98 +39,13 @@ import {
|
||||||
} from "@open-swe/shared/open-swe/custom-node-events";
|
} from "@open-swe/shared/open-swe/custom-node-events";
|
||||||
import { getDefaultHeaders } from "../../../utils/default-headers.js";
|
import { getDefaultHeaders } from "../../../utils/default-headers.js";
|
||||||
import { getCustomConfigurableFields } from "../../../utils/config.js";
|
import { getCustomConfigurableFields } from "../../../utils/config.js";
|
||||||
import { getGitHubTokensFromConfig } from "../../../utils/github-tokens.js";
|
|
||||||
import {
|
import {
|
||||||
createIssueComment,
|
postGitHubIssueComment,
|
||||||
getIssueComments,
|
cleanTaskItems,
|
||||||
updateIssueComment,
|
} from "../../../utils/github/plan.js";
|
||||||
} from "../../../utils/github/api.js";
|
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
|
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
|
||||||
|
|
||||||
const PLAN_MESSAGE_OPEN_TAG = "<open-swe-plan-message>";
|
|
||||||
const PLAN_MESSAGE_CLOSE_TAG = "</open-swe-plan-message>";
|
|
||||||
|
|
||||||
function formatBodyWithPlanMessage(body: string, message: string): string {
|
|
||||||
if (
|
|
||||||
body.includes(PLAN_MESSAGE_OPEN_TAG) &&
|
|
||||||
body.includes(PLAN_MESSAGE_CLOSE_TAG)
|
|
||||||
) {
|
|
||||||
const bodyBeforeTag = body.split(PLAN_MESSAGE_OPEN_TAG)[0];
|
|
||||||
const bodyAfterTag = body.split(PLAN_MESSAGE_CLOSE_TAG)[1];
|
|
||||||
const newInnerContents = `\n${PLAN_MESSAGE_OPEN_TAG}\n\n${message}\n\n${PLAN_MESSAGE_CLOSE_TAG}\n`;
|
|
||||||
return `${bodyBeforeTag}${newInnerContents}${bodyAfterTag}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${body}\n${PLAN_MESSAGE_OPEN_TAG}\n\n${message}\n\n${PLAN_MESSAGE_CLOSE_TAG}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanTaskItems(taskItem: string): string {
|
|
||||||
return "```\n" + taskItem.replace("```", "\\```") + "\n```";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Posts a comment to a GitHub issue using the installation token
|
|
||||||
*/
|
|
||||||
async function postGitHubIssueComment(input: {
|
|
||||||
githubIssueId: number;
|
|
||||||
targetRepository: { owner: string; repo: string };
|
|
||||||
commentBody: string;
|
|
||||||
config: GraphConfig;
|
|
||||||
}): Promise<void> {
|
|
||||||
const { githubIssueId, targetRepository, commentBody, config } = input;
|
|
||||||
const githubAppName = process.env.GITHUB_APP_NAME;
|
|
||||||
if (!githubAppName) {
|
|
||||||
throw new Error("GITHUB_APP_NAME not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
|
||||||
const existingComments = await getIssueComments({
|
|
||||||
owner: targetRepository.owner,
|
|
||||||
repo: targetRepository.repo,
|
|
||||||
issueNumber: githubIssueId,
|
|
||||||
githubInstallationToken,
|
|
||||||
filterBotComments: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingOpenSWEComment = existingComments?.findLast((c) =>
|
|
||||||
c.user?.login?.toLowerCase()?.startsWith(githubAppName.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!existingOpenSWEComment) {
|
|
||||||
await createIssueComment({
|
|
||||||
owner: targetRepository.owner,
|
|
||||||
repo: targetRepository.repo,
|
|
||||||
issueNumber: githubIssueId,
|
|
||||||
body: commentBody,
|
|
||||||
githubToken: githubInstallationToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`Posted comment to GitHub issue #${githubIssueId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the comment
|
|
||||||
const newCommentBody = formatBodyWithPlanMessage(
|
|
||||||
existingOpenSWEComment.body ?? "",
|
|
||||||
commentBody,
|
|
||||||
);
|
|
||||||
await updateIssueComment({
|
|
||||||
owner: targetRepository.owner,
|
|
||||||
repo: targetRepository.repo,
|
|
||||||
commentId: existingOpenSWEComment.id,
|
|
||||||
body: newCommentBody,
|
|
||||||
githubInstallationToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`Updated comment to GitHub issue #${githubIssueId}`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to post GitHub comment:", error);
|
|
||||||
// Don't throw - we don't want to fail the entire process if comment posting fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAcceptedPlanMessage(input: {
|
function createAcceptedPlanMessage(input: {
|
||||||
planTitle: string;
|
planTitle: string;
|
||||||
planItems: PlanItem[];
|
planItems: PlanItem[];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
|
import { AIMessage, isAIMessage, ToolMessage } from "@langchain/core/messages";
|
||||||
import {
|
import {
|
||||||
GraphConfig,
|
GraphConfig,
|
||||||
GraphState,
|
GraphState,
|
||||||
|
|
@ -7,10 +7,20 @@ import {
|
||||||
} from "@open-swe/shared/open-swe/types";
|
} from "@open-swe/shared/open-swe/types";
|
||||||
import { HumanInterrupt, HumanResponse } from "@langchain/langgraph/prebuilt";
|
import { HumanInterrupt, HumanResponse } from "@langchain/langgraph/prebuilt";
|
||||||
import { END, interrupt, Command } from "@langchain/langgraph";
|
import { END, interrupt, Command } from "@langchain/langgraph";
|
||||||
|
import {
|
||||||
|
DO_NOT_RENDER_ID_PREFIX,
|
||||||
|
GITHUB_USER_LOGIN_HEADER,
|
||||||
|
} from "@open-swe/shared/constants";
|
||||||
import {
|
import {
|
||||||
getSandboxWithErrorHandling,
|
getSandboxWithErrorHandling,
|
||||||
stopSandbox,
|
stopSandbox,
|
||||||
} from "../../../utils/sandbox.js";
|
} from "../../../utils/sandbox.js";
|
||||||
|
import { getOpenSweAppUrl } from "../../../utils/url-helpers.js";
|
||||||
|
import {
|
||||||
|
CustomNodeEvent,
|
||||||
|
REQUEST_HELP_NODE_ID,
|
||||||
|
} from "@open-swe/shared/open-swe/custom-node-events";
|
||||||
|
import { postGitHubIssueComment } from "../../../utils/github/plan.js";
|
||||||
|
|
||||||
const constructDescription = (helpRequest: string): string => {
|
const constructDescription = (helpRequest: string): string => {
|
||||||
return `The agent has requested help. Here is the help request:
|
return `The agent has requested help. Here is the help request:
|
||||||
|
|
@ -20,6 +30,16 @@ ${helpRequest}
|
||||||
\`\`\``;
|
\`\`\``;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createEventsMessage = (events: CustomNodeEvent[]) =>
|
||||||
|
new AIMessage({
|
||||||
|
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
|
||||||
|
content: "Request help response",
|
||||||
|
additional_kwargs: {
|
||||||
|
hidden: true,
|
||||||
|
customNodeEvents: events,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export async function requestHelp(
|
export async function requestHelp(
|
||||||
state: GraphState,
|
state: GraphState,
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
|
|
@ -35,6 +55,42 @@ export async function requestHelp(
|
||||||
|
|
||||||
const toolCall = lastMessage.tool_calls[0];
|
const toolCall = lastMessage.tool_calls[0];
|
||||||
|
|
||||||
|
const threadId = config.configurable?.thread_id;
|
||||||
|
if (!threadId) {
|
||||||
|
throw new Error("Thread ID not found in config");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userLogin = config.configurable?.[GITHUB_USER_LOGIN_HEADER];
|
||||||
|
const userTag = userLogin ? `@${userLogin} ` : "";
|
||||||
|
|
||||||
|
const runUrl = getOpenSweAppUrl(threadId);
|
||||||
|
const commentBody = runUrl
|
||||||
|
? `### 🤖 Open SWE Needs Help
|
||||||
|
|
||||||
|
${userTag}I've encountered a situation where I need human assistance to continue.
|
||||||
|
|
||||||
|
**Help Request:**
|
||||||
|
${toolCall.args.help_request}
|
||||||
|
|
||||||
|
You can view and respond to this request in the [Open SWE interface](${runUrl}).
|
||||||
|
|
||||||
|
Please provide guidance so I can continue working on this issue.`
|
||||||
|
: `### 🤖 Open SWE Needs Help
|
||||||
|
|
||||||
|
${userTag}I've encountered a situation where I need human assistance to continue.
|
||||||
|
|
||||||
|
**Help Request:**
|
||||||
|
${toolCall.args.help_request}
|
||||||
|
|
||||||
|
Please check the Open SWE interface to respond to this request.`;
|
||||||
|
|
||||||
|
await postGitHubIssueComment({
|
||||||
|
githubIssueId: state.githubIssueId,
|
||||||
|
targetRepository: state.targetRepository,
|
||||||
|
commentBody,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
const interruptInput: HumanInterrupt = {
|
const interruptInput: HumanInterrupt = {
|
||||||
action_request: {
|
action_request: {
|
||||||
action: "Help Requested",
|
action: "Help Requested",
|
||||||
|
|
@ -78,8 +134,28 @@ export async function requestHelp(
|
||||||
status: "success",
|
status: "success",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customEvent = [
|
||||||
|
{
|
||||||
|
nodeId: REQUEST_HELP_NODE_ID,
|
||||||
|
actionId: uuidv4(),
|
||||||
|
action: "Help request response",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
data: {
|
||||||
|
status: "success" as const,
|
||||||
|
response: interruptRes.args,
|
||||||
|
runId: config.configurable?.run_id ?? "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
config?.writer?.(customEvent);
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanResponseCustomEventMsg = createEventsMessage(customEvent);
|
||||||
const commandUpdate: GraphUpdate = {
|
const commandUpdate: GraphUpdate = {
|
||||||
messages: [toolMessage],
|
messages: [toolMessage, humanResponseCustomEventMsg],
|
||||||
internalMessages: [toolMessage],
|
internalMessages: [toolMessage],
|
||||||
sandboxSessionId: sandbox.id,
|
sandboxSessionId: sandbox.id,
|
||||||
...(codebaseTree && { codebaseTree }),
|
...(codebaseTree && { codebaseTree }),
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types";
|
import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types";
|
||||||
import { RequestSource } from "../../constants.js";
|
import { RequestSource } from "../../constants.js";
|
||||||
import { isAllowedUser } from "@open-swe/shared/github/allowed-users";
|
import { isAllowedUser } from "@open-swe/shared/github/allowed-users";
|
||||||
|
import { getOpenSweAppUrl } from "../../utils/url-helpers.js";
|
||||||
import { StreamMode } from "@langchain/langgraph-sdk";
|
import { StreamMode } from "@langchain/langgraph-sdk";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GitHubIssueWebhook");
|
const logger = createLogger(LogLevel.INFO, "GitHubIssueWebhook");
|
||||||
|
|
@ -37,19 +38,6 @@ const webhooks = new Webhooks({
|
||||||
secret: GITHUB_WEBHOOK_SECRET,
|
secret: GITHUB_WEBHOOK_SECRET,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOpenSweAppUrl = (threadId: string) => {
|
|
||||||
if (!process.env.OPEN_SWE_APP_URL) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const baseUrl = new URL(process.env.OPEN_SWE_APP_URL);
|
|
||||||
baseUrl.pathname = `/chat/${threadId}`;
|
|
||||||
return baseUrl.toString();
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPayload = (body: string): Record<string, any> | null => {
|
const getPayload = (body: string): Record<string, any> | null => {
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(body);
|
const payload = JSON.parse(body);
|
||||||
|
|
|
||||||
93
apps/open-swe/src/utils/github/plan.ts
Normal file
93
apps/open-swe/src/utils/github/plan.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { getGitHubTokensFromConfig } from "../github-tokens.js";
|
||||||
|
import {
|
||||||
|
createIssueComment,
|
||||||
|
getIssueComments,
|
||||||
|
updateIssueComment,
|
||||||
|
} from "./api.js";
|
||||||
|
import { createLogger, LogLevel } from "../logger.js";
|
||||||
|
|
||||||
|
const logger = createLogger(LogLevel.INFO, "GitHubPlan");
|
||||||
|
|
||||||
|
const PLAN_MESSAGE_OPEN_TAG = "<open-swe-plan-message>";
|
||||||
|
const PLAN_MESSAGE_CLOSE_TAG = "</open-swe-plan-message>";
|
||||||
|
|
||||||
|
function formatBodyWithPlanMessage(body: string, message: string): string {
|
||||||
|
if (
|
||||||
|
body.includes(PLAN_MESSAGE_OPEN_TAG) &&
|
||||||
|
body.includes(PLAN_MESSAGE_CLOSE_TAG)
|
||||||
|
) {
|
||||||
|
const bodyBeforeTag = body.split(PLAN_MESSAGE_OPEN_TAG)[0];
|
||||||
|
const bodyAfterTag = body.split(PLAN_MESSAGE_CLOSE_TAG)[1];
|
||||||
|
const newInnerContents = `\n${PLAN_MESSAGE_OPEN_TAG}\n\n${message}\n\n${PLAN_MESSAGE_CLOSE_TAG}\n`;
|
||||||
|
return `${bodyBeforeTag}${newInnerContents}${bodyAfterTag}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${body}\n${PLAN_MESSAGE_OPEN_TAG}\n\n${message}\n\n${PLAN_MESSAGE_CLOSE_TAG}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanTaskItems(taskItem: string): string {
|
||||||
|
return "```\n" + taskItem.replace("```", "\\```") + "\n```";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts a comment to a GitHub issue using the installation token
|
||||||
|
*/
|
||||||
|
export async function postGitHubIssueComment(input: {
|
||||||
|
githubIssueId: number;
|
||||||
|
targetRepository: { owner: string; repo: string };
|
||||||
|
commentBody: string;
|
||||||
|
config: GraphConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { githubIssueId, targetRepository, commentBody, config } = input;
|
||||||
|
const githubAppName = process.env.GITHUB_APP_NAME;
|
||||||
|
if (!githubAppName) {
|
||||||
|
throw new Error("GITHUB_APP_NAME not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
||||||
|
const existingComments = await getIssueComments({
|
||||||
|
owner: targetRepository.owner,
|
||||||
|
repo: targetRepository.repo,
|
||||||
|
issueNumber: githubIssueId,
|
||||||
|
githubInstallationToken,
|
||||||
|
filterBotComments: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingOpenSWEComment = existingComments?.findLast((c) =>
|
||||||
|
c.user?.login?.toLowerCase()?.startsWith(githubAppName.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingOpenSWEComment) {
|
||||||
|
await createIssueComment({
|
||||||
|
owner: targetRepository.owner,
|
||||||
|
repo: targetRepository.repo,
|
||||||
|
issueNumber: githubIssueId,
|
||||||
|
body: commentBody,
|
||||||
|
githubToken: githubInstallationToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Posted comment to GitHub issue #${githubIssueId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the comment
|
||||||
|
const newCommentBody = formatBodyWithPlanMessage(
|
||||||
|
existingOpenSWEComment.body ?? "",
|
||||||
|
commentBody,
|
||||||
|
);
|
||||||
|
await updateIssueComment({
|
||||||
|
owner: targetRepository.owner,
|
||||||
|
repo: targetRepository.repo,
|
||||||
|
commentId: existingOpenSWEComment.id,
|
||||||
|
body: newCommentBody,
|
||||||
|
githubInstallationToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Updated comment to GitHub issue #${githubIssueId}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to post GitHub comment:", error);
|
||||||
|
// Don't throw - we don't want to fail the entire process if comment posting fails
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/open-swe/src/utils/url-helpers.ts
Normal file
12
apps/open-swe/src/utils/url-helpers.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const getOpenSweAppUrl = (threadId: string): string => {
|
||||||
|
if (!process.env.OPEN_SWE_APP_URL) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const baseUrl = new URL(process.env.OPEN_SWE_APP_URL);
|
||||||
|
baseUrl.pathname = `/chat/${threadId}`;
|
||||||
|
return baseUrl.toString();
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -264,7 +264,6 @@ function ActionItem(props: ActionItemProps) {
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine if we should show the content toggle button
|
|
||||||
const shouldShowToggle = () => {
|
const shouldShowToggle = () => {
|
||||||
if (props.status !== "done") return false;
|
if (props.status !== "done") return false;
|
||||||
|
|
||||||
|
|
@ -542,8 +541,8 @@ function ActionItem(props: ActionItemProps) {
|
||||||
|
|
||||||
// Render the content based on action type
|
// Render the content based on action type
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (props.status !== "done" || !("actionType" in props)) return null;
|
if (!("actionType" in props)) return null;
|
||||||
|
if (props.status !== "done") return null;
|
||||||
if (!expanded) return null;
|
if (!expanded) return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
292
apps/web/src/components/gen-ui/request-human-help.tsx
Normal file
292
apps/web/src/components/gen-ui/request-human-help.tsx
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
HelpCircle,
|
||||||
|
Loader2,
|
||||||
|
CheckCircle,
|
||||||
|
ChevronDown,
|
||||||
|
Send,
|
||||||
|
Clock,
|
||||||
|
Check,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { BasicMarkdownText } from "../thread/markdown-text";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Badge } from "../ui/badge";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
import { CustomNodeEvent } from "@open-swe/shared/open-swe/custom-node-events";
|
||||||
|
|
||||||
|
type RequestHumanHelpProps = {
|
||||||
|
status: "loading" | "generating" | "done";
|
||||||
|
helpRequest?: string;
|
||||||
|
reasoningText?: string;
|
||||||
|
onSubmitResponse?: (response: string) => void;
|
||||||
|
requestHelpEvents?: CustomNodeEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RequestHumanHelp({
|
||||||
|
status,
|
||||||
|
helpRequest,
|
||||||
|
reasoningText,
|
||||||
|
onSubmitResponse,
|
||||||
|
requestHelpEvents,
|
||||||
|
}: RequestHumanHelpProps) {
|
||||||
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
const [userResponse, setUserResponse] = useState("");
|
||||||
|
const [submittedResponse, setSubmittedResponse] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||||
|
|
||||||
|
// Extract response from requestHelpEvents if available
|
||||||
|
const eventResponse = requestHelpEvents?.find(
|
||||||
|
(event) =>
|
||||||
|
event.data && typeof event.data === "object" && "response" in event.data,
|
||||||
|
)?.data?.response as string | undefined;
|
||||||
|
|
||||||
|
const hasEventResponse = !!eventResponse;
|
||||||
|
const finalResponse = eventResponse || submittedResponse;
|
||||||
|
const isResponded = hasEventResponse || hasSubmitted;
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const getStatusBadge = () => {
|
||||||
|
if (isResponded) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="border-emerald-200 bg-emerald-100 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
|
||||||
|
>
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
{hasEventResponse ? "Responded" : "Submitted"}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case "loading":
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="border-orange-200 bg-orange-100 text-orange-700 dark:border-orange-800 dark:bg-orange-950 dark:text-orange-300"
|
||||||
|
>
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
Preparing
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
case "generating":
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="border-orange-200 bg-orange-100 text-orange-700 dark:border-orange-800 dark:bg-orange-950 dark:text-orange-300"
|
||||||
|
>
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
Requesting
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
case "done":
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="border-blue-200 bg-blue-100 text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300"
|
||||||
|
>
|
||||||
|
<HelpCircle className="h-3 w-3" />
|
||||||
|
Awaiting Response
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = () => {
|
||||||
|
if (isResponded) {
|
||||||
|
return "Human Help Response";
|
||||||
|
}
|
||||||
|
return "Human Help Requested";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubtitleText = () => {
|
||||||
|
if (hasEventResponse) {
|
||||||
|
return "Response received";
|
||||||
|
}
|
||||||
|
if (hasSubmitted) {
|
||||||
|
return "Response submitted successfully";
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case "loading":
|
||||||
|
return "Preparing help request...";
|
||||||
|
case "generating":
|
||||||
|
return "Requesting human assistance...";
|
||||||
|
case "done":
|
||||||
|
return "Awaiting your response";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldShowToggle = () => {
|
||||||
|
return (
|
||||||
|
!!helpRequest &&
|
||||||
|
(status === "generating" || status === "done" || isResponded)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
if (userResponse.trim() && onSubmitResponse) {
|
||||||
|
const response = userResponse.trim();
|
||||||
|
setSubmittedResponse(response);
|
||||||
|
setHasSubmitted(true);
|
||||||
|
onSubmitResponse(response);
|
||||||
|
setUserResponse("");
|
||||||
|
}
|
||||||
|
}, [userResponse, onSubmitResponse]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (!expanded) return null;
|
||||||
|
|
||||||
|
const shouldShowContent =
|
||||||
|
status === "done" || status === "generating" || isResponded;
|
||||||
|
|
||||||
|
if (!shouldShowContent) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t p-4">
|
||||||
|
{helpRequest && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="bg-muted/30 rounded-lg p-3">
|
||||||
|
<h4 className="text-muted-foreground mb-2 text-xs font-medium">
|
||||||
|
Help Request
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
id="help-request-description"
|
||||||
|
className="text-foreground text-sm"
|
||||||
|
>
|
||||||
|
<BasicMarkdownText className="text-sm">
|
||||||
|
{helpRequest}
|
||||||
|
</BasicMarkdownText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isResponded && finalResponse ? (
|
||||||
|
<div className="bg-muted/30 rounded-lg p-3">
|
||||||
|
<h4 className="text-muted-foreground mb-2 text-xs font-medium">
|
||||||
|
{hasEventResponse ? "Response" : "Your Response"}
|
||||||
|
</h4>
|
||||||
|
<div className="rounded-lg border border-emerald-200 bg-emerald-100/50 p-3 dark:border-emerald-800 dark:bg-emerald-900/30">
|
||||||
|
<div className="text-sm whitespace-pre-wrap text-emerald-700 dark:text-emerald-300">
|
||||||
|
{finalResponse}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(status === "generating" || status === "done") &&
|
||||||
|
onSubmitResponse &&
|
||||||
|
!hasEventResponse && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-muted/30 rounded-lg p-3">
|
||||||
|
<h4 className="text-muted-foreground mb-2 text-xs font-medium">
|
||||||
|
Your Response
|
||||||
|
</h4>
|
||||||
|
<Textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={userResponse}
|
||||||
|
onChange={(e) => setUserResponse(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Type your response here... (Ctrl+Enter to submit)"
|
||||||
|
className="min-h-[80px] text-sm"
|
||||||
|
aria-label="Human help response"
|
||||||
|
aria-describedby="help-request-description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!userResponse.trim()}
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Send className="mr-2 h-3 w-3" />
|
||||||
|
Submit Response
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group via-background to-background dark:via-background dark:to-background rounded-xl border bg-gradient-to-br from-orange-50/50 transition-shadow dark:from-orange-950/20",
|
||||||
|
!expanded ? "shadow-sm hover:shadow-md" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex items-center bg-gradient-to-r from-orange-50 to-orange-50/50 p-4 backdrop-blur-sm dark:from-orange-950/30 dark:to-orange-950/10",
|
||||||
|
!expanded ? "rounded-xl" : "rounded-t-xl rounded-b-none",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-orange-500 shadow-md dark:bg-orange-600">
|
||||||
|
<HelpCircle className="h-4 w-4 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-3 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-foreground text-sm font-semibold">
|
||||||
|
{getStatusText()}
|
||||||
|
</h3>
|
||||||
|
{getStatusBadge()}
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
{getSubtitleText()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{shouldShowToggle() && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 transition-transform duration-200",
|
||||||
|
!expanded && "-rotate-90",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{renderContent()}
|
||||||
|
|
||||||
|
{/* Reasoning Footer */}
|
||||||
|
{reasoningText && status === "done" && expanded && (
|
||||||
|
<div className="border-t bg-gradient-to-r from-blue-50 to-blue-50/50 p-3 backdrop-blur-sm dark:from-blue-950/30 dark:to-blue-950/10">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 dark:bg-blue-600">
|
||||||
|
<HelpCircle className="h-3 w-3 text-white" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
{reasoningText}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import { CheckCircle, Loader2, FileText, MinusCircle } from "lucide-react";
|
||||||
CheckCircle,
|
|
||||||
Loader2,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronUp,
|
|
||||||
FileText,
|
|
||||||
MinusCircle,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BasicMarkdownText } from "../thread/markdown-text";
|
import { BasicMarkdownText } from "../thread/markdown-text";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
AIMessage,
|
AIMessage,
|
||||||
Checkpoint,
|
Checkpoint,
|
||||||
Message,
|
Message,
|
||||||
|
StreamMode,
|
||||||
ToolMessage,
|
ToolMessage,
|
||||||
} from "@langchain/langgraph-sdk";
|
} from "@langchain/langgraph-sdk";
|
||||||
import { getContentString } from "../utils";
|
import { getContentString } from "../utils";
|
||||||
|
|
@ -28,6 +29,7 @@ import {
|
||||||
import { DiagnoseErrorAction } from "@/components/v2/diagnose-error-action";
|
import { DiagnoseErrorAction } from "@/components/v2/diagnose-error-action";
|
||||||
import { WriteTechnicalNotes } from "@/components/gen-ui/write-technical-notes";
|
import { WriteTechnicalNotes } from "@/components/gen-ui/write-technical-notes";
|
||||||
import { CodeReviewStarted } from "@/components/gen-ui/code-review-started";
|
import { CodeReviewStarted } from "@/components/gen-ui/code-review-started";
|
||||||
|
import { RequestHumanHelp } from "@/components/gen-ui/request-human-help";
|
||||||
import { ToolCall } from "@langchain/core/messages/tool";
|
import { ToolCall } from "@langchain/core/messages/tool";
|
||||||
import {
|
import {
|
||||||
createApplyPatchToolFields,
|
createApplyPatchToolFields,
|
||||||
|
|
@ -44,6 +46,7 @@ import {
|
||||||
createSearchDocumentForToolFields,
|
createSearchDocumentForToolFields,
|
||||||
createWriteTechnicalNotesToolFields,
|
createWriteTechnicalNotesToolFields,
|
||||||
createConversationHistorySummaryToolFields,
|
createConversationHistorySummaryToolFields,
|
||||||
|
createRequestHumanHelpToolFields,
|
||||||
createReviewStartedToolFields,
|
createReviewStartedToolFields,
|
||||||
createScratchpadFields,
|
createScratchpadFields,
|
||||||
createTextEditorToolFields,
|
createTextEditorToolFields,
|
||||||
|
|
@ -54,6 +57,9 @@ import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
|
||||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||||
import { ConversationHistorySummary } from "@/components/gen-ui/conversation-summary";
|
import { ConversationHistorySummary } from "@/components/gen-ui/conversation-summary";
|
||||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||||
|
import { HumanResponse } from "@langchain/langgraph/prebuilt";
|
||||||
|
import { OPEN_SWE_STREAM_MODE } from "@open-swe/shared/constants";
|
||||||
|
import { CustomNodeEvent } from "@open-swe/shared/open-swe/custom-node-events";
|
||||||
|
|
||||||
// Used only for Zod type inference.
|
// Used only for Zod type inference.
|
||||||
const dummyRepo = { owner: "dummy", repo: "dummy" };
|
const dummyRepo = { owner: "dummy", repo: "dummy" };
|
||||||
|
|
@ -109,6 +115,8 @@ type ConversationHistorySummaryToolArgs = z.infer<
|
||||||
typeof conversationHistorySummaryTool.schema
|
typeof conversationHistorySummaryTool.schema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
const requestHumanHelpTool = createRequestHumanHelpToolFields();
|
||||||
|
type RequestHumanHelpToolArgs = z.infer<typeof requestHumanHelpTool.schema>;
|
||||||
const textEditorTool = createTextEditorToolFields({
|
const textEditorTool = createTextEditorToolFields({
|
||||||
owner: "dummy",
|
owner: "dummy",
|
||||||
repo: "dummy",
|
repo: "dummy",
|
||||||
|
|
@ -128,6 +136,7 @@ function isMcpTool(toolName: string): boolean {
|
||||||
getURLContentTool.name,
|
getURLContentTool.name,
|
||||||
openPrTool.name,
|
openPrTool.name,
|
||||||
diagnoseErrorTool.name,
|
diagnoseErrorTool.name,
|
||||||
|
requestHumanHelpTool.name,
|
||||||
textEditorTool.name,
|
textEditorTool.name,
|
||||||
viewTool.name,
|
viewTool.name,
|
||||||
];
|
];
|
||||||
|
|
@ -338,20 +347,44 @@ export function mapToolMessageToActionStepProps(
|
||||||
|
|
||||||
export function AssistantMessage({
|
export function AssistantMessage({
|
||||||
message,
|
message,
|
||||||
isLoading,
|
threadId,
|
||||||
handleRegenerate,
|
assistantId,
|
||||||
forceRenderInterrupt = false,
|
forceRenderInterrupt = false,
|
||||||
thread,
|
thread,
|
||||||
threadMessages,
|
threadMessages,
|
||||||
|
modifyRunId,
|
||||||
|
requestHelpEvents,
|
||||||
}: {
|
}: {
|
||||||
message: Message | undefined;
|
message: Message | undefined;
|
||||||
isLoading: boolean;
|
threadId: string;
|
||||||
handleRegenerate: (parentCheckpoint: Checkpoint | null | undefined) => void;
|
assistantId: string;
|
||||||
forceRenderInterrupt?: boolean;
|
forceRenderInterrupt?: boolean;
|
||||||
thread: ReturnType<typeof useStream<Record<string, unknown>>>;
|
thread: ReturnType<typeof useStream<Record<string, unknown>>>;
|
||||||
threadMessages: Message[];
|
threadMessages: Message[];
|
||||||
|
modifyRunId?: (runId: string) => Promise<void>;
|
||||||
|
requestHelpEvents?: CustomNodeEvent[];
|
||||||
}) {
|
}) {
|
||||||
const content = message?.content ?? [];
|
const content = message?.content ?? [];
|
||||||
|
|
||||||
|
const handleHumanHelpResponse = async (response: string) => {
|
||||||
|
const humanResponse: HumanResponse[] = [
|
||||||
|
{
|
||||||
|
type: "response",
|
||||||
|
args: response,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const newRun = await thread.client.runs.create(threadId, assistantId, {
|
||||||
|
command: { resume: humanResponse },
|
||||||
|
config: {
|
||||||
|
recursion_limit: 400,
|
||||||
|
},
|
||||||
|
streamResumable: true,
|
||||||
|
streamMode: OPEN_SWE_STREAM_MODE as StreamMode[],
|
||||||
|
});
|
||||||
|
await modifyRunId?.(newRun.run_id);
|
||||||
|
};
|
||||||
|
|
||||||
const contentString = getContentString(content);
|
const contentString = getContentString(content);
|
||||||
const [hideToolCalls] = useQueryState(
|
const [hideToolCalls] = useQueryState(
|
||||||
"hideToolCalls",
|
"hideToolCalls",
|
||||||
|
|
@ -361,9 +394,7 @@ export function AssistantMessage({
|
||||||
const messages = threadMessages;
|
const messages = threadMessages;
|
||||||
const idx = message ? messages.findIndex((m) => m.id === message.id) : -1;
|
const idx = message ? messages.findIndex((m) => m.id === message.id) : -1;
|
||||||
|
|
||||||
const meta = message ? thread.getMessagesMetadata(message) : undefined;
|
|
||||||
const threadInterrupt = thread.interrupt;
|
const threadInterrupt = thread.interrupt;
|
||||||
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
|
|
||||||
const anthropicStreamedToolCalls = Array.isArray(content)
|
const anthropicStreamedToolCalls = Array.isArray(content)
|
||||||
? parseAnthropicStreamedToolCalls(content)
|
? parseAnthropicStreamedToolCalls(content)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
@ -444,6 +475,10 @@ export function AssistantMessage({
|
||||||
? aiToolCalls.find((tc) => tc.name === reviewStartedTool.name)
|
? aiToolCalls.find((tc) => tc.name === reviewStartedTool.name)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const requestHumanHelpToolCall = message
|
||||||
|
? aiToolCalls.find((tc) => tc.name === requestHumanHelpTool.name)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Check if this is a conversation history summary message
|
// Check if this is a conversation history summary message
|
||||||
if (conversationHistorySummaryToolCall && aiToolCalls.length === 1) {
|
if (conversationHistorySummaryToolCall && aiToolCalls.length === 1) {
|
||||||
const correspondingToolResult = toolResults.find(
|
const correspondingToolResult = toolResults.find(
|
||||||
|
|
@ -476,6 +511,27 @@ export function AssistantMessage({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requestHumanHelpToolCall) {
|
||||||
|
const correspondingToolResult = toolResults.find(
|
||||||
|
(tr) => tr && tr.tool_call_id === requestHumanHelpToolCall.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const args = requestHumanHelpToolCall.args as RequestHumanHelpToolArgs;
|
||||||
|
const reasoningText = getContentString(content);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<RequestHumanHelp
|
||||||
|
status={correspondingToolResult ? "done" : "generating"}
|
||||||
|
helpRequest={args.help_request}
|
||||||
|
reasoningText={reasoningText}
|
||||||
|
onSubmitResponse={handleHumanHelpResponse}
|
||||||
|
requestHelpEvents={requestHelpEvents}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// We can be sure that if either task status tool call is present, it will be the
|
// We can be sure that if either task status tool call is present, it will be the
|
||||||
// only tool call/result we need to render for this message.
|
// only tool call/result we need to render for this message.
|
||||||
if (markTaskCompletedToolCall || markTaskNotCompletedToolCall) {
|
if (markTaskCompletedToolCall || markTaskNotCompletedToolCall) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
INITIALIZE_NODE_ID,
|
INITIALIZE_NODE_ID,
|
||||||
ACCEPTED_PLAN_NODE_ID,
|
ACCEPTED_PLAN_NODE_ID,
|
||||||
mapCustomEventsToSteps,
|
mapCustomEventsToSteps,
|
||||||
|
REQUEST_HELP_NODE_ID,
|
||||||
} from "@open-swe/shared/open-swe/custom-node-events";
|
} from "@open-swe/shared/open-swe/custom-node-events";
|
||||||
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
||||||
import { Message } from "@langchain/langgraph-sdk";
|
import { Message } from "@langchain/langgraph-sdk";
|
||||||
|
|
@ -66,10 +67,12 @@ interface ActionsRendererProps<
|
||||||
StateType extends PlannerGraphState | GraphState,
|
StateType extends PlannerGraphState | GraphState,
|
||||||
> {
|
> {
|
||||||
runId?: string;
|
runId?: string;
|
||||||
|
threadId: string;
|
||||||
customNodeEvents: CustomNodeEvent[];
|
customNodeEvents: CustomNodeEvent[];
|
||||||
setCustomNodeEvents: Dispatch<SetStateAction<CustomNodeEvent[]>>;
|
setCustomNodeEvents: Dispatch<SetStateAction<CustomNodeEvent[]>>;
|
||||||
stream: ReturnType<typeof useStream<StateType>>;
|
stream: ReturnType<typeof useStream<StateType>>;
|
||||||
taskPlan?: TaskPlan;
|
taskPlan?: TaskPlan;
|
||||||
|
modifyRunId?: (runId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomNodeEventsFromMessages = (
|
const getCustomNodeEventsFromMessages = (
|
||||||
|
|
@ -102,6 +105,8 @@ export function ActionsRenderer<
|
||||||
customNodeEvents,
|
customNodeEvents,
|
||||||
setCustomNodeEvents,
|
setCustomNodeEvents,
|
||||||
stream,
|
stream,
|
||||||
|
threadId,
|
||||||
|
modifyRunId,
|
||||||
}: ActionsRendererProps<StateType>) {
|
}: ActionsRendererProps<StateType>) {
|
||||||
const [streamLoading, setStreamLoading] = useState(stream.isLoading);
|
const [streamLoading, setStreamLoading] = useState(stream.isLoading);
|
||||||
const [errorState, setErrorState] = useState<ErrorState | null>(null);
|
const [errorState, setErrorState] = useState<ErrorState | null>(null);
|
||||||
|
|
@ -122,6 +127,11 @@ export function ActionsRenderer<
|
||||||
[customNodeEvents, runId],
|
[customNodeEvents, runId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const requestHelpEvents = useMemo(
|
||||||
|
() => customNodeEvents.filter((e) => e.nodeId === REQUEST_HELP_NODE_ID),
|
||||||
|
[customNodeEvents],
|
||||||
|
);
|
||||||
|
|
||||||
const steps = mapCustomEventsToSteps(initializeEvents);
|
const steps = mapCustomEventsToSteps(initializeEvents);
|
||||||
const allSuccess =
|
const allSuccess =
|
||||||
steps.length > 0 && steps.every((s) => s.status === "success");
|
steps.length > 0 && steps.every((s) => s.status === "success");
|
||||||
|
|
@ -223,8 +233,10 @@ export function ActionsRenderer<
|
||||||
thread={stream as UseStream<Record<string, unknown>>}
|
thread={stream as UseStream<Record<string, unknown>>}
|
||||||
threadMessages={stream.messages}
|
threadMessages={stream.messages}
|
||||||
message={m}
|
message={m}
|
||||||
isLoading={false}
|
modifyRunId={modifyRunId}
|
||||||
handleRegenerate={() => {}}
|
threadId={threadId}
|
||||||
|
assistantId={stream.assistantId}
|
||||||
|
requestHelpEvents={requestHelpEvents}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{acceptedPlanEvents.length > 0 &&
|
{acceptedPlanEvents.length > 0 &&
|
||||||
|
|
|
||||||
|
|
@ -410,6 +410,7 @@ export function ThreadView({
|
||||||
setCustomPlannerNodeEvents
|
setCustomPlannerNodeEvents
|
||||||
}
|
}
|
||||||
stream={plannerStream}
|
stream={plannerStream}
|
||||||
|
threadId={plannerSession.threadId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -455,6 +456,40 @@ export function ThreadView({
|
||||||
setCustomProgrammerNodeEvents
|
setCustomProgrammerNodeEvents
|
||||||
}
|
}
|
||||||
stream={programmerStream}
|
stream={programmerStream}
|
||||||
|
threadId={programmerSession.threadId}
|
||||||
|
modifyRunId={async (runId) => {
|
||||||
|
setProgrammerSession((prev) => {
|
||||||
|
if (!prev) {
|
||||||
|
return {
|
||||||
|
threadId: programmerSession.threadId,
|
||||||
|
runId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
runId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (plannerSession?.threadId) {
|
||||||
|
try {
|
||||||
|
// Attempt to update the planner session with the new run ID of the programmer.
|
||||||
|
await programmerStream.client.threads.updateState(
|
||||||
|
plannerSession?.threadId,
|
||||||
|
{
|
||||||
|
values: {
|
||||||
|
programmerSession: {
|
||||||
|
threadId:
|
||||||
|
programmerSession.threadId,
|
||||||
|
runId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,3 @@
|
||||||
"app": "./apps/open-swe/src/routes/app.ts:app"
|
"app": "./apps/open-swe/src/routes/app.ts:app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,6 +28,7 @@ export function isCustomNodeEvent(event: unknown): event is CustomNodeEvent {
|
||||||
}
|
}
|
||||||
export const INITIALIZE_NODE_ID = "initialize";
|
export const INITIALIZE_NODE_ID = "initialize";
|
||||||
export const ACCEPTED_PLAN_NODE_ID = "accepted-plan";
|
export const ACCEPTED_PLAN_NODE_ID = "accepted-plan";
|
||||||
|
export const REQUEST_HELP_NODE_ID = "request-help";
|
||||||
|
|
||||||
export const INIT_STEPS = [
|
export const INIT_STEPS = [
|
||||||
"Resuming sandbox",
|
"Resuming sandbox",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue