feat: Add component for write technical notes tool call (#395)

* feat: Add component for write technical notes tool call

* cr
This commit is contained in:
Brace Sproul 2025-07-11 15:34:02 -07:00 • committed by GitHub
parent caf222235f
commit d769983483
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 145 additions and 12 deletions

View file

@ -12,6 +12,7 @@ import { formatCustomRulesPrompt } from "../../../utils/custom-rules.js";
import { getPlannerNotes } from "../utils/get-notes.js";
import { ToolMessage } from "@langchain/core/messages";
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
import { createWriteTechnicalNotesToolFields } from "@open-swe/shared/open-swe/tools";
const PLANNER_NOTES_PROMPT = `You've also taken technical notes throughout the context gathering process. Ensure you include/incorporate these notes, or the highest quality parts of these notes in your conclusion notes.
@ -93,17 +94,7 @@ const formatPrompt = (state: PlannerGraphState): string => {
);
};
const condenseContextToolSchema = z.object({
notes: z
.string()
.describe("The notes you've generated based on the conversation history."),
});
const condenseContextTool = {
name: "write_technical_notes",
description:
"Write technical notes based on the conversation history provided. Ensure these notes are concise, but still containing enough information to be useful to you when you go to execute the plan.",
schema: condenseContextToolSchema,
};
const condenseContextTool = createWriteTechnicalNotesToolFields();
export async function notetaker(
state: PlannerGraphState,
@ -144,7 +135,7 @@ ${state.messages.map(getMessageString).join("\n")}`;
return {
messages: [response, toolResponse],
contextGatheringNotes: (
toolCall.args as z.infer<typeof condenseContextToolSchema>
toolCall.args as z.infer<typeof condenseContextTool.schema>
).notes,
};
}

View file

@ -0,0 +1,94 @@
"use client";
import { useState } from "react";
import {
CheckCircle,
Loader2,
ChevronDown,
ChevronUp,
FileText,
} from "lucide-react";
import { cn } from "@/lib/utils";
type WriteTechnicalNotesProps = {
status: "generating" | "done";
notes?: string;
reasoningText?: string;
};
export function WriteTechnicalNotes({
status,
notes,
reasoningText,
}: WriteTechnicalNotesProps) {
const [expanded, setExpanded] = useState(false);
const getStatusIcon = () => {
switch (status) {
case "generating":
return (
<Loader2 className="text-muted-foreground size-3.5 animate-spin" />
);
case "done":
return <CheckCircle className="size-3.5 text-green-500" />;
}
};
const getStatusText = () => {
switch (status) {
case "generating":
return "Writing technical notes...";
case "done":
return "Technical notes written";
}
};
const shouldShowToggle = () => {
return status === "done" && notes;
};
return (
<div className="border-border overflow-hidden rounded-md border">
<div className="border-border flex items-center border-b bg-gray-50 p-2 dark:bg-gray-800">
<FileText className="text-muted-foreground mr-2 size-3.5" />
<span className="text-foreground/80 flex-1 text-xs font-normal">
Technical Notes
</span>
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs font-normal">
{getStatusText()}
</span>
{getStatusIcon()}
{shouldShowToggle() && (
<button
onClick={() => setExpanded(!expanded)}
className="text-muted-foreground hover:text-foreground"
>
{expanded ? (
<ChevronUp className="size-3.5" />
) : (
<ChevronDown className="size-3.5" />
)}
</button>
)}
</div>
</div>
{expanded && notes && status === "done" && (
<div className="bg-muted overflow-x-auto p-2 dark:bg-gray-900">
<pre className="text-foreground/90 text-xs font-normal whitespace-pre-wrap">
{notes}
</pre>
</div>
)}
{reasoningText && status === "done" && (
<div className="border-t border-blue-300 bg-blue-100/50 p-2 dark:border-blue-800 dark:bg-blue-900/50">
<p className="text-xs font-normal text-blue-700 dark:text-blue-300">
{reasoningText}
</p>
</div>
)}
</div>
);
}

View file

@ -22,6 +22,7 @@ import { ActionStep, ActionItemProps } from "@/components/gen-ui/action-step";
import { TaskSummary } from "@/components/gen-ui/task-summary";
import { PullRequestOpened } from "@/components/gen-ui/pull-request-opened";
import { DiagnoseErrorAction } from "@/components/v2/diagnose-error-action";
import { WriteTechnicalNotes } from "@/components/gen-ui/write-technical-notes";
import { ToolCall } from "@langchain/core/messages/tool";
import {
createApplyPatchToolFields,
@ -34,6 +35,7 @@ import {
createDiagnoseErrorToolFields,
createGetURLContentToolFields,
createFindInstancesOfToolFields,
createWriteTechnicalNotesToolFields,
} from "@open-swe/shared/open-swe/tools";
import { z } from "zod";
import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
@ -67,6 +69,11 @@ type GetURLContentToolArgs = z.infer<typeof getURLContentTool.schema>;
const findInstancesOfTool = createFindInstancesOfToolFields(dummyRepo);
type FindInstancesOfToolArgs = z.infer<typeof findInstancesOfTool.schema>;
const writeTechnicalNotesTool = createWriteTechnicalNotesToolFields();
type WriteTechnicalNotesToolArgs = z.infer<
typeof writeTechnicalNotesTool.schema
>;
function CustomComponent({
message,
thread,
@ -305,6 +312,10 @@ export function AssistantMessage({
? aiToolCalls.find((tc) => tc.name === diagnoseErrorTool.name)
: undefined;
const writeTechnicalNotesToolCall = message
? aiToolCalls.find((tc) => tc.name === writeTechnicalNotesTool.name)
: undefined;
// We can be sure that if the task status tool call is present, it will be the
// only tool call/result we need to render for this message.
if (taskStatusToolCall) {
@ -346,6 +357,26 @@ export function AssistantMessage({
);
}
if (writeTechnicalNotesToolCall) {
const correspondingToolResult = toolResults.find(
(tr) => tr && tr.tool_call_id === writeTechnicalNotesToolCall.id,
);
const args =
writeTechnicalNotesToolCall.args as WriteTechnicalNotesToolArgs;
const reasoningText = getContentString(content);
return (
<div className="flex flex-col gap-4">
<WriteTechnicalNotes
status={correspondingToolResult ? "done" : "generating"}
notes={args.notes}
reasoningText={reasoningText}
/>
</div>
);
}
if (openPrToolCall) {
let branch: string | undefined;
let targetBranch: string | undefined = "main";

View file

@ -342,3 +342,20 @@ export function createGetURLContentToolFields() {
schema: getURLContentSchema,
};
}
export function createWriteTechnicalNotesToolFields() {
const writeTechnicalNotesSchema = z.object({
notes: z
.string()
.describe(
"The notes you've generated based on the conversation history.",
),
});
return {
name: "write_technical_notes",
description:
"Write technical notes based on the conversation history provided. Ensure these notes are concise, but still containing enough information to be useful to you when you go to execute the plan.",
schema: writeTechnicalNotesSchema,
};
}