Browse Source

fix(34): resolve tool calling errors and stale workflow state

- Add execute functions to all three chat tools (pass-through pattern)
  so AI SDK produces proper tool-call/tool-result pairs in conversation
  history, fixing AI_MissingToolResultsError on subsequent messages
- Fix stale closure in ChatPanel customFetch by using a ref for
  workflowState, ensuring current canvas state is sent with each request
- Add EDITABLE NODE PROPERTIES section to system prompt so LLM uses
  correct data property names (e.g. "prompt" not "text" for prompt nodes)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
4e11a05b3f
  1. 23
      src/components/ChatPanel.tsx
  2. 20
      src/lib/chat/tools.ts

23
src/components/ChatPanel.tsx

@ -22,7 +22,12 @@ export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow
const messagesEndRef = useRef<HTMLDivElement>(null);
const [input, setInput] = useState("");
// Create a custom fetch function that includes workflowState
// Use a ref so the fetch closure always reads the latest workflowState
// without needing to re-create the transport
const workflowStateRef = useRef(workflowState);
workflowStateRef.current = workflowState;
// Stable fetch function that reads workflowState from ref
const customFetch = useCallback(async (input: RequestInfo | URL, init?: RequestInit) => {
if (!init?.body) {
return fetch(input, init);
@ -31,17 +36,19 @@ export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow
const body = JSON.parse(init.body as string);
const bodyWithWorkflow = {
...body,
workflowState,
workflowState: workflowStateRef.current,
};
return fetch(input, {
...init,
body: JSON.stringify(bodyWithWorkflow),
});
}, [workflowState]);
}, []);
const [transport] = useState(() => new DefaultChatTransport({ api: "/api/chat", fetch: customFetch }));
const { messages, sendMessage, setMessages, status } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat", fetch: customFetch }),
transport,
});
const isLoading = status === "streaming" || status === "submitted";
@ -57,22 +64,20 @@ export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow
if (!("state" in part) || !("toolCallId" in part) || !("input" in part)) return;
if (part.state !== "output-available") return;
const callId = part.toolCallId;
const callId = (part as Record<string, unknown>).toolCallId as string;
if (processedToolCalls.current.has(callId)) return;
processedToolCalls.current.add(callId);
const toolName = part.type.replace("tool-", "");
if (toolName === "createWorkflow" && onBuildWorkflow) {
// Extract description from tool args (input is the args)
const description = (part.input as { description?: string }).description;
const description = ((part as Record<string, unknown>).input as { description?: string }).description;
if (description) {
onBuildWorkflow(description);
}
}
if (toolName === "editWorkflow" && onApplyEdits) {
// Extract operations from tool args (input is the args)
const operations = (part.input as { operations?: EditOperation[] }).operations;
const operations = ((part as Record<string, unknown>).input as { operations?: EditOperation[] }).operations;
if (operations) {
onApplyEdits(operations);
}

20
src/lib/chat/tools.ts

@ -108,7 +108,18 @@ ${formatContextForPrompt(workflowContext)}`;
- Use **editWorkflow** when the user wants to ADD, REMOVE, CHANGE, or MODIFY nodes/connections in the CURRENT workflow.
- Always explain what you're about to do BEFORE calling a tool.
- When editing, reference nodes by their ID from the current workflow state.
- After editing, summarize what changed.`;
- After editing, summarize what changed.
## EDITABLE NODE PROPERTIES
When using editWorkflow with updateNode, you MUST use these exact property names in the data object:
- **prompt** node: \`{ "prompt": "the text" }\`
- **nanoBanana** (Generate Image) node: \`{ "resolution": "1K"|"2K"|"4K", "aspectRatio": "1:1"|"2:3"|"3:2"|"3:4"|"4:3"|"4:5"|"5:4"|"9:16"|"16:9"|"21:9", "useGoogleSearch": true|false }\`
- **llmGenerate** node: \`{ "temperature": 0-2, "maxTokens": 256-16384 }\`
- **Any node** title: \`{ "customTitle": "New Name" }\`
Do NOT use "text", "content", or other guessed property names. Use ONLY the exact names listed above.`;
return baseDomainExpertise + contextSection + toolUsageRules;
}
@ -130,8 +141,7 @@ export function createChatTools(nodeIds: string[]) {
.string()
.describe("The helpful answer to the user question"),
}),
// No execute function - this is a "generate" tool pattern
// The LLM provides the answer directly in the tool call result
execute: async ({ answer }) => ({ answer }),
}),
createWorkflow: tool({
@ -142,7 +152,7 @@ export function createChatTools(nodeIds: string[]) {
.string()
.describe("Description of what the workflow should do"),
}),
// No execute function - the client handles calling quickstart API
execute: async ({ description }) => ({ description }),
}),
editWorkflow: tool({
@ -199,7 +209,7 @@ export function createChatTools(nodeIds: string[]) {
"Brief explanation of what changes are being made and why"
),
}),
// No execute function - results are returned to client for application
execute: async ({ operations, explanation }) => ({ operations, explanation }),
}),
};
}

Loading…
Cancel
Save