2025-07-29 17:37:13 -04:00
|
|
|
import {
|
|
|
|
|
coerceMessageLikeToMessage,
|
|
|
|
|
ToolMessage,
|
|
|
|
|
isAIMessage,
|
|
|
|
|
isHumanMessage,
|
|
|
|
|
isToolMessage,
|
|
|
|
|
} from "@langchain/core/messages";
|
2025-08-05 18:52:45 -04:00
|
|
|
|
2025-07-29 17:37:13 -04:00
|
|
|
import { getMessageContentString } from "@open-swe/shared/messages";
|
|
|
|
|
import { createWriteTechnicalNotesToolFields } from "@open-swe/shared/open-swe/tools";
|
|
|
|
|
|
2025-08-05 18:52:45 -04:00
|
|
|
export type ToolCall = {
|
|
|
|
|
name: string;
|
|
|
|
|
args: Record<string, any>;
|
|
|
|
|
id?: string;
|
|
|
|
|
type?: "tool_call";
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-29 17:37:13 -04:00
|
|
|
interface LogChunk {
|
|
|
|
|
event: string;
|
|
|
|
|
data: any;
|
|
|
|
|
ops?: Array<{ value: string }>;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-05 18:52:45 -04:00
|
|
|
/**
|
|
|
|
|
* Format a tool call arguments into a clean, readable string
|
|
|
|
|
*/
|
|
|
|
|
function formatToolCallArgs(tool: ToolCall): string {
|
|
|
|
|
const toolName = tool.name || "unknown tool";
|
|
|
|
|
|
|
|
|
|
if (!tool.args) return toolName;
|
|
|
|
|
|
|
|
|
|
switch (toolName.toLowerCase()) {
|
|
|
|
|
case "shell": {
|
|
|
|
|
if (Array.isArray(tool.args.command)) {
|
|
|
|
|
return `${toolName}: ${tool.args.command.join(" ")}`;
|
|
|
|
|
}
|
|
|
|
|
return `${toolName}: ${tool.args.command || ""}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "grep": {
|
|
|
|
|
const query = tool.args.query || "";
|
|
|
|
|
return `${toolName}: "${query}"`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "view": {
|
|
|
|
|
return `${toolName}: ${tool.args.path || ""}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "str_replace_based_edit_tool": {
|
|
|
|
|
const command = tool.args.command || "";
|
|
|
|
|
|
|
|
|
|
switch (command) {
|
|
|
|
|
case "insert": {
|
|
|
|
|
const insertLine = tool.args.insert_line;
|
|
|
|
|
const newStr = tool.args.new_str || "";
|
|
|
|
|
return `${toolName}: insert_line=${insertLine}, new_str="${newStr}"`;
|
|
|
|
|
}
|
|
|
|
|
case "str_replace": {
|
|
|
|
|
const oldStr = tool.args.old_str || "";
|
|
|
|
|
const newStr = tool.args.new_str || "";
|
|
|
|
|
return `${toolName}: old_str="${oldStr}", new_str="${newStr}"`;
|
|
|
|
|
}
|
|
|
|
|
case "create": {
|
|
|
|
|
const fileText = tool.args.file_text || "";
|
|
|
|
|
return `${toolName}: file_text="${fileText}"`;
|
|
|
|
|
}
|
|
|
|
|
case "view": {
|
|
|
|
|
const viewRange = tool.args.view_range;
|
|
|
|
|
if (viewRange) {
|
|
|
|
|
return `${toolName}: view_range=[${viewRange[0]}, ${viewRange[1]}]`;
|
|
|
|
|
}
|
|
|
|
|
return `${toolName}: view`;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return `${toolName}: ${command}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "search_documents_for": {
|
|
|
|
|
const query = tool.args.query || "";
|
|
|
|
|
const url = tool.args.url || "";
|
|
|
|
|
return `${toolName}: "${query}" in ${url}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "get_url_content": {
|
|
|
|
|
return `${toolName}: ${tool.args.url || ""}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "session_plan": {
|
|
|
|
|
const title = tool.args.title || "";
|
|
|
|
|
const planSteps = tool.args.plan || [];
|
|
|
|
|
if (title) {
|
|
|
|
|
return `${toolName}: "${title}" (${planSteps.length} steps)`;
|
|
|
|
|
}
|
|
|
|
|
return `${toolName}: ${planSteps.length} plan steps`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "apply_patch": {
|
|
|
|
|
const filePath = tool.args.file_path || "";
|
|
|
|
|
const diff = tool.args.diff || "";
|
|
|
|
|
const diffLines = diff.split("\n").length;
|
|
|
|
|
return `${toolName}: applied ${diffLines} line diff to ${filePath}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "install_dependencies": {
|
|
|
|
|
const command = tool.args.command || [];
|
|
|
|
|
if (Array.isArray(command)) {
|
|
|
|
|
return `${toolName}: ${command.join(" ")}`;
|
|
|
|
|
}
|
|
|
|
|
return `${toolName}: ${command}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "scratchpad": {
|
|
|
|
|
const scratchpad = tool.args.scratchpad || [];
|
|
|
|
|
if (Array.isArray(scratchpad)) {
|
|
|
|
|
return `${toolName}: ${scratchpad.length} notes`;
|
|
|
|
|
}
|
|
|
|
|
return `${toolName}: ${scratchpad}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "command_safety_evaluator": {
|
|
|
|
|
const command = tool.args.command || "";
|
|
|
|
|
return `${toolName}: evaluating "${command}"`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "respond_and_route": {
|
|
|
|
|
const response = tool.args.response || "";
|
|
|
|
|
const route = tool.args.route || "";
|
|
|
|
|
if (response && route) {
|
|
|
|
|
return `${toolName}: "${response}" → ${route}`;
|
|
|
|
|
} else if (response) {
|
|
|
|
|
return `${toolName}: "${response}"`;
|
|
|
|
|
} else if (route) {
|
|
|
|
|
return `${toolName}: → ${route}`;
|
|
|
|
|
}
|
|
|
|
|
return `${toolName}: routing decision`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "request_human_help": {
|
|
|
|
|
const helpRequest = tool.args.help_request || "";
|
|
|
|
|
return `${toolName}: "${helpRequest}"`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "update_plan": {
|
|
|
|
|
const reasoning = tool.args.update_plan_reasoning || "";
|
|
|
|
|
return `${toolName}: ${reasoning.slice(0, 50)}...`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "mark_task_completed": {
|
|
|
|
|
const summary = tool.args.completed_task_summary || "";
|
|
|
|
|
return `${toolName}: ${summary.slice(0, 50)}...`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "mark_task_not_completed": {
|
|
|
|
|
const reasoning = tool.args.reasoning || "";
|
|
|
|
|
return `${toolName}: ${reasoning.slice(0, 50)}...`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "diagnose_error": {
|
|
|
|
|
const diagnosis = tool.args.diagnosis || "";
|
|
|
|
|
return `${toolName}: ${diagnosis.slice(0, 50)}...`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "write_technical_notes": {
|
|
|
|
|
const notes = tool.args.notes || "";
|
|
|
|
|
return `${toolName}: ${notes.slice(0, 50)}...`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "summarize_conversation_history": {
|
|
|
|
|
const reasoning = tool.args.reasoning || "";
|
|
|
|
|
return `${toolName}: ${reasoning.slice(0, 50)}...`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "code_review_mark_task_completed": {
|
|
|
|
|
const review = tool.args.review || "";
|
|
|
|
|
return `${toolName}: ${review.slice(0, 50)}...`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "code_review_mark_task_not_complete": {
|
|
|
|
|
const review = tool.args.review || "";
|
|
|
|
|
const actions = tool.args.additional_actions || [];
|
|
|
|
|
return `${toolName}: ${review.slice(0, 30)}... (${actions.length} actions)`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "review_started": {
|
|
|
|
|
const started = tool.args.review_started || false;
|
|
|
|
|
return `${toolName}: ${started ? "started" : "not started"}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 17:37:13 -04:00
|
|
|
/**
|
|
|
|
|
* Format a tool result based on its type and content
|
|
|
|
|
*/
|
|
|
|
|
function formatToolResult(message: ToolMessage): string {
|
|
|
|
|
const content = getMessageContentString(message.content);
|
|
|
|
|
|
|
|
|
|
if (!content) return "";
|
|
|
|
|
|
2025-08-05 18:52:45 -04:00
|
|
|
const isError = message.status === "error";
|
2025-07-29 17:37:13 -04:00
|
|
|
const toolName = message.name || "tool";
|
2025-08-05 18:52:45 -04:00
|
|
|
|
|
|
|
|
// If it's an error, return error message immediately
|
|
|
|
|
if (isError) return `Error: ${content}`;
|
|
|
|
|
|
2025-07-29 17:37:13 -04:00
|
|
|
switch (toolName.toLowerCase()) {
|
|
|
|
|
case "shell":
|
|
|
|
|
return content;
|
2025-08-05 18:52:45 -04:00
|
|
|
|
|
|
|
|
case "grep": {
|
|
|
|
|
if (content.includes("Exit code 1. No results found.")) {
|
|
|
|
|
return "No results found";
|
|
|
|
|
}
|
|
|
|
|
const lines = content.split("\n").filter((line) => line.trim());
|
|
|
|
|
return `${lines.length} matches found`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "view": {
|
|
|
|
|
const contentLength = content.length;
|
|
|
|
|
return contentLength > 1000
|
|
|
|
|
? `${contentLength} characters (truncated)`
|
|
|
|
|
: `${contentLength} characters`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "str_replace_based_edit_tool":
|
|
|
|
|
return "File edited successfully";
|
|
|
|
|
|
|
|
|
|
case "get_url_content":
|
|
|
|
|
return `${content.length} characters of content`;
|
|
|
|
|
|
2025-07-29 17:37:13 -04:00
|
|
|
case "apply_patch":
|
2025-08-05 18:52:45 -04:00
|
|
|
return "Patch applied successfully";
|
|
|
|
|
|
2025-07-29 17:37:13 -04:00
|
|
|
case "install_dependencies":
|
2025-08-05 18:52:45 -04:00
|
|
|
return "Dependencies installed successfully";
|
|
|
|
|
|
|
|
|
|
case "command_safety_evaluator":
|
|
|
|
|
try {
|
|
|
|
|
const evaluation = JSON.parse(content);
|
|
|
|
|
return `Safety: ${evaluation.is_safe ? "SAFE" : "UNSAFE"} (${evaluation.risk_level} risk)`;
|
|
|
|
|
} catch {
|
|
|
|
|
return content;
|
2025-07-29 17:37:13 -04:00
|
|
|
}
|
2025-08-05 18:52:45 -04:00
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return content.length > 200 ? content.slice(0, 200) + "..." : content;
|
2025-07-29 17:37:13 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) => {
|
2025-08-05 18:52:45 -04:00
|
|
|
const formattedArgs = formatToolCallArgs(tool);
|
|
|
|
|
logs.push(`[TOOL CALL] ${formattedArgs}`);
|
2025-07-29 17:37:13 -04:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|