fix: Agent and web fixes (#40)

* fix: Agent and web fixes

* cr

* cr

* cr

* cr

* cr
This commit is contained in:
Brace Sproul 2025-05-28 15:18:58 -07:00 • committed by GitHub
parent 9b825de25d
commit d079c63424
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 106 additions and 20 deletions

View file

@ -5,6 +5,8 @@ import { getRepoAbsolutePath } from "../utils/git/index.js";
import { formatPlanPrompt } from "../utils/plan-prompt.js";
import { pauseSandbox } from "../utils/sandbox.js";
import { createLogger, LogLevel } from "../utils/logger.js";
import { getCurrentTask } from "../utils/current-task.js";
import { getMessageContentString } from "../utils/message/content.js";
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
@ -81,6 +83,7 @@ You MUST adhere to the following criteria when executing the task:
- Always use \`rg\` instead of \`grep/ls -R\` because it is much faster and respects gitignore.
- Always use glob patterns when searching with \`rg\` for specific file types. For example, to search for all TSX files, use \`rg -i star -g **/*.tsx project-directory/\`. This is because \`rg\` does not have built in file types for every language.
- Only make changes to the existing Git repo ({REPO_DIRECTORY}). Any changes outside this repo will not be detected, so do not attempt to create new files or directories outside of this repo.
- You do NOT have access to the \`set_task_status\` or \`diagnose_error\` tools. NEVER attempt to call them.
Below, is a collection of useful context about the codebase. It is updated after each completed task, and is provided to you to help you make decisions, and avoid duplicate work:
{CODEBASE_CONTEXT}
@ -133,9 +136,16 @@ export async function generateAction(
}
logger.info("Generated action", {
name: response.tool_calls?.[0].name,
args: response.tool_calls?.[0].args,
currentTask: getCurrentTask(state.plan).plan,
...(response.tool_calls?.[0] && {
name: response.tool_calls?.[0].name,
args: response.tool_calls?.[0].args,
}),
...(getMessageContentString(response.content) && {
content: getMessageContentString(response.content),
}),
});
return {
messages: [response],
...(newSandboxSessionId && { sandboxSessionId: newSandboxSessionId }),

View file

@ -17,6 +17,7 @@ import {
zodSchemaToString,
} from "../utils/zod-to-string.js";
import { Command } from "@langchain/langgraph";
import { truncateOutput } from "../utils/truncate-outputs.js";
const logger = createLogger(LogLevel.INFO, "TakeAction");
@ -105,7 +106,7 @@ export async function takeAction(
const toolMessage = new ToolMessage({
tool_call_id: toolCall.id ?? "",
content: result,
content: truncateOutput(result),
name: toolCall.name,
status: toolCallStatus,
});

View file

@ -1,8 +1,12 @@
import { loadModel, Task } from "../../../utils/load-model.js";
import { shellTool, applyPatchTool } from "../../../tools/index.js";
import { shellTool } from "../../../tools/index.js";
import { PlannerGraphState, PlannerGraphUpdate } from "../types.js";
import { GraphConfig } from "../../../types.js";
import { isHumanMessage } from "@langchain/core/messages";
import { createLogger, LogLevel } from "../../../utils/logger.js";
import { getMessageContentString } from "../../../utils/message/content.js";
const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode");
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.
@ -23,7 +27,7 @@ export async function generateAction(
config: GraphConfig,
): Promise<PlannerGraphUpdate> {
const model = await loadModel(config, Task.ACTION_GENERATOR);
const tools = [shellTool, applyPatchTool];
const tools = [shellTool];
const modelWithTools = model.bindTools(tools, { tool_choice: "auto" });
const firstUserMessage = state.messages.find(isHumanMessage);
@ -39,6 +43,16 @@ export async function generateAction(
...state.plannerMessages,
]);
logger.info("Generated planning message", {
...(response.tool_calls?.[0] && {
name: response.tool_calls?.[0].name,
args: response.tool_calls?.[0].args,
}),
...(getMessageContentString(response.content) && {
content: getMessageContentString(response.content),
}),
});
return {
plannerMessages: [response],
};

View file

@ -1,10 +1,11 @@
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
import { applyPatchTool, shellTool } from "../../../tools/index.js";
import { shellTool } from "../../../tools/index.js";
import { GraphConfig } from "../../../types.js";
import { PlannerGraphState, PlannerGraphUpdate } from "../types.js";
import { createLogger, LogLevel } from "../../../utils/logger.js";
import { zodSchemaToString } from "../../../utils/zod-to-string.js";
import { formatBadArgsError } from "../../../utils/zod-to-string.js";
import { truncateOutput } from "../../../utils/truncate-outputs.js";
const logger = createLogger(LogLevel.INFO, "TakeAction");
@ -20,7 +21,6 @@ export async function takeAction(
}
const toolsMap = {
[applyPatchTool.name]: applyPatchTool,
[shellTool.name]: shellTool,
};
@ -39,9 +39,12 @@ export async function takeAction(
let result = "";
let toolCallStatus: "success" | "error" = "success";
try {
const toolResult: { result: string; status: "success" | "error" } =
const toolResult =
// @ts-expect-error tool.invoke types are weird here...
await tool.invoke(toolCall.args);
(await tool.invoke(toolCall.args)) as {
result: string;
status: "success" | "error";
};
result = toolResult.result;
toolCallStatus = toolResult.status;
} catch (e) {
@ -68,7 +71,7 @@ export async function takeAction(
const toolMessage = new ToolMessage({
tool_call_id: toolCall.id ?? "",
content: result,
content: truncateOutput(result),
name: toolCall.name,
status: toolCallStatus,
});

View file

@ -0,0 +1,12 @@
import { PlanItem } from "../types.js";
export function getCurrentTask(plan: PlanItem[]) {
return (
plan.filter((p) => !p.completed).sort((a, b) => a.index - b.index)?.[0] || {
plan: "No current task found.",
index: -1,
completed: true,
summary: "",
}
);
}

View file

@ -0,0 +1,36 @@
export function truncateOutput(
output: string,
options?: {
/**
* @default 10000
*/
numStartCharacters?: number;
/**
* @default 10000
*/
numEndCharacters?: number;
},
) {
const { numStartCharacters = 10000, numEndCharacters = 10000 } =
options ?? {};
if (numStartCharacters < 0 || numEndCharacters < 0) {
throw new Error("numStartCharacters and numEndCharacters must be >= 0");
}
if (!numStartCharacters && !numEndCharacters) {
throw new Error(
"At least one of numStartCharacters or numEndCharacters must be > 0",
);
}
if (output.length <= numStartCharacters + numEndCharacters) {
return output;
}
return (
output.slice(0, numStartCharacters) +
`\n... Output too long. Truncated the middle ${output.length - numStartCharacters - numEndCharacters} characters of the output ...\n` +
output.slice(-numEndCharacters)
);
}

View file

@ -254,6 +254,16 @@ export function Thread() {
stream.submit(undefined, {
checkpoint: parentCheckpoint,
streamMode: ["values"],
config: {
// TODO: Make configurable (just that target_repository object) & recursion limit in the UI.
configurable: {
target_repository: {
owner: "langchain-ai",
repo: "open-swe",
},
},
recursion_limit: 400,
},
});
};

View file

@ -141,19 +141,19 @@ export function AssistantMessage({
}
return (
<div className="group mr-auto flex items-start gap-2">
<div className="flex flex-col gap-2">
<div className="group mr-auto flex w-full max-w-3xl items-start gap-2">
<div className="flex w-full flex-col gap-2">
{isToolResult ? (
<>
<span>
<ToolResult message={message} />
<Interrupt
interruptValue={threadInterrupt?.value}
isLastMessage={isLastMessage}
hasNoAIOrToolMessages={hasNoAIOrToolMessages}
/>
</>
</span>
) : (
<>
<span>
{contentString.length > 0 && (
<div className="py-1">
<MarkdownText>{contentString}</MarkdownText>
@ -161,7 +161,7 @@ export function AssistantMessage({
)}
{!hideToolCalls && (
<>
<span>
{(hasToolCalls && toolCallsHaveContents && (
<ToolCalls toolCalls={message.tool_calls} />
)) ||
@ -171,7 +171,7 @@ export function AssistantMessage({
(hasToolCalls && (
<ToolCalls toolCalls={message.tool_calls} />
))}
</>
</span>
)}
{message && (
@ -204,7 +204,7 @@ export function AssistantMessage({
handleRegenerate={() => handleRegenerate(parentCheckpoint)}
/>
</div>
</>
</span>
)}
</div>
</div>

View file

@ -119,7 +119,7 @@ export function HumanMessage({
)}
{/* Render text if present, otherwise fallback to file/image name */}
{contentString ? (
<p className="bg-muted ml-auto w-fit rounded-3xl px-4 py-2 text-right whitespace-pre-wrap">
<p className="bg-muted ml-auto w-fit rounded-3xl px-4 py-2 whitespace-pre-wrap">
{contentString}
</p>
) : null}

View file

@ -15,7 +15,7 @@ export function ToolCalls({
if (!toolCalls || toolCalls.length === 0) return null;
return (
<div className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2">
<div className="grid w-full max-w-3xl grid-rows-[1fr_auto] gap-2">
{toolCalls.map((tc, idx) => {
const args = tc.args as Record<string, any>;
const hasArgs = Object.keys(args).length > 0;