mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-02 01:33:15 +00:00
295 lines
9.5 KiB
TypeScript
295 lines
9.5 KiB
TypeScript
|
|
import {
|
||
|
|
coerceMessageLikeToMessage,
|
||
|
|
ToolMessage,
|
||
|
|
isAIMessage,
|
||
|
|
isHumanMessage,
|
||
|
|
isToolMessage,
|
||
|
|
} from "@langchain/core/messages";
|
||
|
|
import { getMessageContentString } from "@open-swe/shared/messages";
|
||
|
|
import { createWriteTechnicalNotesToolFields } from "@open-swe/shared/open-swe/tools";
|
||
|
|
|
||
|
|
interface LogChunk {
|
||
|
|
event: string;
|
||
|
|
data: any;
|
||
|
|
ops?: Array<{ value: string }>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format a tool result based on its type and content
|
||
|
|
*/
|
||
|
|
function formatToolResult(message: ToolMessage): string {
|
||
|
|
const content = getMessageContentString(message.content);
|
||
|
|
|
||
|
|
if (!content) return "";
|
||
|
|
|
||
|
|
// For successful tool executions, format nicely
|
||
|
|
const toolName = message.name || "tool";
|
||
|
|
switch (toolName.toLowerCase()) {
|
||
|
|
case "shell":
|
||
|
|
case "grep_search":
|
||
|
|
case "search":
|
||
|
|
return content;
|
||
|
|
case "apply_patch":
|
||
|
|
return content.includes("Error")
|
||
|
|
? `Error: ${content}`
|
||
|
|
: "Patch applied successfully";
|
||
|
|
case "install_dependencies":
|
||
|
|
return content.includes("Error")
|
||
|
|
? `Error: ${content}`
|
||
|
|
: "Dependencies installed successfully";
|
||
|
|
default:
|
||
|
|
if (content.length > 200) {
|
||
|
|
return content.slice(0, 200) + "...";
|
||
|
|
}
|
||
|
|
return content;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatDisplayLog(chunk: LogChunk | string): string[] {
|
||
|
|
if (typeof chunk === "string") {
|
||
|
|
if (chunk.startsWith("Human feedback:")) {
|
||
|
|
return [
|
||
|
|
`[HUMAN FEEDBACK RECEIVED] ${chunk.replace("Human feedback:", "").trim()}`,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
if (chunk.startsWith("Interrupt:")) {
|
||
|
|
const message = chunk.replace("Interrupt:", "").trim();
|
||
|
|
return [
|
||
|
|
"═══════════════════════════════════════",
|
||
|
|
`📤 INTERRUPT: "${message}"`,
|
||
|
|
"═══════════════════════════════════════",
|
||
|
|
];
|
||
|
|
}
|
||
|
|
// Filter out raw file content and object references
|
||
|
|
if (
|
||
|
|
chunk === "[object Object]" ||
|
||
|
|
chunk.includes("total 4") ||
|
||
|
|
chunk.includes("drwxr-xr-x") ||
|
||
|
|
chunk.includes("Exit code 1") ||
|
||
|
|
chunk.startsWith("#") ||
|
||
|
|
chunk.startsWith("-") ||
|
||
|
|
chunk.startsWith("./")
|
||
|
|
) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
// Single line system messages
|
||
|
|
const cleanChunk = chunk.replace(/\s+/g, " ").trim();
|
||
|
|
const maxLength = 150;
|
||
|
|
const truncated =
|
||
|
|
cleanChunk.length > maxLength
|
||
|
|
? cleanChunk.slice(0, maxLength) + "... [trunc]"
|
||
|
|
: cleanChunk;
|
||
|
|
return [`[SYSTEM] ${truncated}`];
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = chunk.data;
|
||
|
|
const logs: string[] = [];
|
||
|
|
|
||
|
|
// Handle session events
|
||
|
|
if (data.plannerSession) {
|
||
|
|
logs.push("[PLANNER SESSION STARTED]");
|
||
|
|
}
|
||
|
|
if (data.programmerSession) {
|
||
|
|
logs.push("[PROGRAMMER SESSION STARTED]");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle messages
|
||
|
|
const nestedDataObj = Object.values(data)[0] as unknown as Record<
|
||
|
|
string,
|
||
|
|
any
|
||
|
|
>;
|
||
|
|
if (
|
||
|
|
nestedDataObj &&
|
||
|
|
typeof nestedDataObj === "object" &&
|
||
|
|
"messages" in nestedDataObj
|
||
|
|
) {
|
||
|
|
const messages = Array.isArray(nestedDataObj.messages)
|
||
|
|
? nestedDataObj.messages
|
||
|
|
: [nestedDataObj.messages];
|
||
|
|
for (const msg of messages) {
|
||
|
|
try {
|
||
|
|
const message = coerceMessageLikeToMessage(msg);
|
||
|
|
|
||
|
|
// Handle tool messages
|
||
|
|
if (isToolMessage(message)) {
|
||
|
|
const toolName = message.name || "tool";
|
||
|
|
const result = formatToolResult(message);
|
||
|
|
if (result) {
|
||
|
|
// Concatenate long tool results to a single line (truncate if too long)
|
||
|
|
const maxLength = 500;
|
||
|
|
let formattedResult = result.replace(/\s+/g, " ");
|
||
|
|
if (formattedResult.length > maxLength) {
|
||
|
|
formattedResult =
|
||
|
|
formattedResult.slice(0, maxLength) + "... [trunc]";
|
||
|
|
}
|
||
|
|
logs.push(`[TOOL RESULT] ${toolName}: ${formattedResult}`);
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle AI messages
|
||
|
|
if (isAIMessage(message)) {
|
||
|
|
// Handle reasoning if present
|
||
|
|
if (message.additional_kwargs?.reasoning) {
|
||
|
|
const reasoning = String(message.additional_kwargs.reasoning)
|
||
|
|
.replace(/\s+/g, " ")
|
||
|
|
.trim();
|
||
|
|
const maxLength = 150;
|
||
|
|
const truncated =
|
||
|
|
reasoning.length > maxLength
|
||
|
|
? reasoning.slice(0, maxLength) + "... [trunc]"
|
||
|
|
: reasoning;
|
||
|
|
logs.push(`[REASONING] ${truncated}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle tool calls
|
||
|
|
if (message.tool_calls && message.tool_calls.length > 0) {
|
||
|
|
const technicalNotesToolName =
|
||
|
|
createWriteTechnicalNotesToolFields().name;
|
||
|
|
|
||
|
|
message.tool_calls.forEach((tool) => {
|
||
|
|
let argsString = "";
|
||
|
|
if (typeof tool.args === "string") {
|
||
|
|
argsString = tool.args;
|
||
|
|
} else if (tool.args !== undefined) {
|
||
|
|
try {
|
||
|
|
argsString = JSON.stringify(tool.args, null, 2);
|
||
|
|
} catch {
|
||
|
|
argsString = String(tool.args);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Truncate the string if too long
|
||
|
|
const maxLength = 150;
|
||
|
|
const truncatedArgs =
|
||
|
|
argsString.length > maxLength
|
||
|
|
? argsString.slice(0, maxLength) + "... [trunc]"
|
||
|
|
: argsString;
|
||
|
|
const toolName = tool.name || "unknown";
|
||
|
|
logs.push(`[TOOL CALL] ${toolName}: ${truncatedArgs}`);
|
||
|
|
|
||
|
|
// Handle technical notes from tool call
|
||
|
|
if (
|
||
|
|
tool.name === technicalNotesToolName &&
|
||
|
|
tool.args &&
|
||
|
|
typeof tool.args === "object" &&
|
||
|
|
"notes" in tool.args
|
||
|
|
) {
|
||
|
|
const notes = (tool.args as any).notes;
|
||
|
|
if (Array.isArray(notes)) {
|
||
|
|
logs.push(
|
||
|
|
"[TECHNICAL NOTES]",
|
||
|
|
...notes.map((note: string) => ` • ${note}`),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle regular AI messages
|
||
|
|
const text = getMessageContentString(message.content);
|
||
|
|
if (text) {
|
||
|
|
// Always single line, remove newlines and truncate
|
||
|
|
const cleanText = text.replace(/\s+/g, " ").trim();
|
||
|
|
const maxLength = 200;
|
||
|
|
const truncated =
|
||
|
|
cleanText.length > maxLength
|
||
|
|
? cleanText.slice(0, maxLength) + "... [trunc]"
|
||
|
|
: cleanText;
|
||
|
|
logs.push(`[AI] ${truncated}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle human messages
|
||
|
|
if (isHumanMessage(message)) {
|
||
|
|
const text = getMessageContentString(message.content);
|
||
|
|
if (text) {
|
||
|
|
// Single line human messages
|
||
|
|
const cleanText = text.replace(/\s+/g, " ").trim();
|
||
|
|
const maxLength = 150;
|
||
|
|
const truncated =
|
||
|
|
cleanText.length > maxLength
|
||
|
|
? cleanText.slice(0, maxLength) + "... [trunc]"
|
||
|
|
: cleanText;
|
||
|
|
logs.push(`[HUMAN] ${truncated}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error("Error formatting log:", error.message);
|
||
|
|
// Fallback to original message if conversion fails
|
||
|
|
if (msg.type === "tool") {
|
||
|
|
const toolName = msg.name || "tool";
|
||
|
|
const content = getMessageContentString(msg.content);
|
||
|
|
if (content) {
|
||
|
|
logs.push(`[TOOL RESULT] ${toolName}: ${content}`);
|
||
|
|
}
|
||
|
|
} else if (msg.type === "ai") {
|
||
|
|
const text = getMessageContentString(msg.content);
|
||
|
|
if (text) {
|
||
|
|
const cleanText = text.replace(/\s+/g, " ").trim();
|
||
|
|
const maxLength = 200;
|
||
|
|
const truncated =
|
||
|
|
cleanText.length > maxLength
|
||
|
|
? cleanText.slice(0, maxLength) + "... [trunc]"
|
||
|
|
: cleanText;
|
||
|
|
logs.push(`[AI] ${truncated}`);
|
||
|
|
}
|
||
|
|
} else if (msg.type === "human") {
|
||
|
|
const text = getMessageContentString(msg.content);
|
||
|
|
if (text) {
|
||
|
|
const cleanText = text.replace(/\s+/g, " ").trim();
|
||
|
|
const maxLength = 150;
|
||
|
|
const truncated =
|
||
|
|
cleanText.length > maxLength
|
||
|
|
? cleanText.slice(0, maxLength) + "... [trunc]"
|
||
|
|
: cleanText;
|
||
|
|
logs.push(`[HUMAN] ${truncated}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Handle feedback messages
|
||
|
|
if (data.command?.resume?.[0]?.type) {
|
||
|
|
const type = data.command.resume[0].type;
|
||
|
|
logs.push(`[HUMAN FEEDBACK RECEIVED] ${type}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle interrupts and plans
|
||
|
|
if (data.__interrupt__) {
|
||
|
|
const interrupt = data.__interrupt__[0]?.value;
|
||
|
|
if (interrupt?.action_request?.args?.plan) {
|
||
|
|
const plan = interrupt.action_request.args.plan;
|
||
|
|
const steps = plan
|
||
|
|
.split(":::")
|
||
|
|
.map((s: string) => s.trim())
|
||
|
|
.filter(Boolean);
|
||
|
|
|
||
|
|
// Add clear visual separation and format nicely
|
||
|
|
logs.push(
|
||
|
|
" ", // Blank line for separation
|
||
|
|
|
||
|
|
"🎯 PROPOSED PLAN",
|
||
|
|
...steps.map((step: string, idx: number) => ` ${idx + 1}. ${step}`),
|
||
|
|
|
||
|
|
" ", // Blank line after
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
logs.push(
|
||
|
|
" ", // Blank line for separation
|
||
|
|
"⏳ INTERRUPT: Waiting for feedback...",
|
||
|
|
" ", // Blank line after
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return logs;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Formats a log chunk for debug purposes, showing all raw data.
|
||
|
|
* This should only be used during development.
|
||
|
|
*/
|
||
|
|
export function formatDebugLog(chunk: LogChunk | string): string {
|
||
|
|
if (typeof chunk === "string") return chunk;
|
||
|
|
return JSON.stringify(chunk, null, 2);
|
||
|
|
}
|