-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: Implement Tool Coordinator for efficient multi-step tool execution #375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
f2bc7da
75e01df
0311b79
924bebd
612479d
9e2a308
37290d5
e4dbb38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| import { generateObject } from 'ai' | ||
| import { z } from 'zod' | ||
| import { getModel } from '@/lib/models' | ||
| import { Message } from 'ai/react' | ||
| import { getTools } from '@/lib/tools' | ||
| import { ToolResultPart } from '@/lib/types' | ||
|
|
||
| // --- 1. Schema Definition for Structured Planning --- | ||
|
|
||
| const toolStepSchema = z.object({ | ||
| toolName: z.string().describe('The name of the tool to be executed (e.g., "geospatialQueryTool", "searchTool").'), | ||
| toolArgs: z.record(z.any()).describe('The arguments for the tool function call.'), | ||
| dependencyIndices: z.array(z.number()).optional().describe('An array of indices of previous steps whose results are required for this step. Use 0-based indexing.'), | ||
| purpose: z.string().describe('A brief explanation of why this tool is being called in this step.') | ||
| }) | ||
|
|
||
| const toolPlanSchema = z.object({ | ||
| reasoning: z.string().describe('A detailed explanation of the multi-step plan to answer the user query.'), | ||
| steps: z.array(toolStepSchema).describe('A sequence of tool execution steps to fulfill the user request.') | ||
| }) | ||
|
|
||
| export type ToolPlan = z.infer<typeof toolPlanSchema> | ||
| export type ToolStep = z.infer<typeof toolStepSchema> | ||
|
|
||
| // --- 2. Tool Coordinator Planning Function --- | ||
|
|
||
| /** | ||
| * Analyzes the user query and generates a structured, multi-step tool execution plan. | ||
| */ | ||
| export async function toolCoordinator(messages: Message[]): Promise<ToolPlan> { | ||
| const model = getModel() | ||
| const tools = getTools({}) // Get tool definitions for the prompt | ||
|
|
||
| const toolDescriptions = tools.map(tool => ({ | ||
| name: tool.toolName, | ||
| description: tool.description, | ||
| parameters: tool.parameters | ||
| })) | ||
|
|
||
| const systemPrompt = `You are an expert Tool Coordinator. Your task is to analyze the user's request and create a structured, multi-step plan to answer it using the available tools. | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Rules: | ||
| 1. The plan must be a sequence of steps. | ||
| 2. For steps that depend on the output of a previous step, specify the 'dependencyIndices' array (0-based index). | ||
| 3. You must use the exact 'toolName' and 'toolArgs' structure as defined in the tool descriptions. | ||
| 4. The final output must strictly adhere to the provided JSON schema. | ||
| Available Tools: | ||
| ${JSON.stringify(toolDescriptions, null, 2)} | ||
| ` | ||
|
|
||
| const { object } = await generateObject({ | ||
| model: model, | ||
| system: systemPrompt, | ||
| messages: messages, | ||
| schema: toolPlanSchema | ||
| }) | ||
|
|
||
| return object | ||
| } | ||
|
|
||
| // --- 3. Tool Execution Function --- | ||
|
|
||
| /** | ||
| * Executes the tool plan, handling dependencies and parallel execution. | ||
| */ | ||
| export async function executeToolPlan(plan: ToolPlan): Promise<ToolResultPart[]> { | ||
| const allTools = getTools({}) | ||
| const toolMap = new Map(allTools.map(tool => [tool.toolName, tool])) | ||
| const results: Map<number, any> = new Map() | ||
| const toolResults: ToolResultPart[] = [] | ||
|
|
||
| // Function to get results of dependencies | ||
| const getDependencyResults = (indices: number[]) => { | ||
| return indices.map(index => { | ||
| if (!results.has(index)) { | ||
| throw new Error(\`Dependency step \${index} has not been executed yet.\`) | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| return results.get(index) | ||
| }) | ||
| } | ||
|
|
||
| for (let i = 0; i < plan.steps.length; i++) { | ||
| const step = plan.steps[i] | ||
| const tool = toolMap.get(step.toolName) | ||
|
|
||
| if (!tool) { | ||
| console.error(\`Tool \${step.toolName} not found.\`) | ||
| results.set(i, { error: \`Tool \${step.toolName} not found.\` }) | ||
| continue | ||
| } | ||
|
|
||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try { | ||
| const dependencyResults = step.dependencyIndices ? getDependencyResults(step.dependencyIndices) : [] | ||
|
|
||
| // Inject dependency results into tool arguments for the tool to use | ||
| const argsWithDependencies = { | ||
| ...step.toolArgs, | ||
| _dependencyResults: dependencyResults.length > 0 ? dependencyResults : undefined | ||
| } | ||
|
|
||
| console.log(\`Executing step \${i}: \${step.toolName} with args: \${JSON.stringify(argsWithDependencies)}\`) | ||
|
|
||
| // Execute the tool directly | ||
| const result = await tool.execute(argsWithDependencies) | ||
|
|
||
| results.set(i, result) | ||
| toolResults.push({ | ||
| toolName: step.toolName, | ||
| toolCallId: \`coord-\${i}\`, | ||
| result: result | ||
| }) | ||
| } catch (error) { | ||
| console.error(\`Error executing step \${i} (\${step.toolName}):\`, error) | ||
| const errorMessage = error instanceof Error ? error.message : String(error) | ||
| results.set(i, { error: errorMessage }) | ||
| toolResults.push({ | ||
| toolName: step.toolName, | ||
| toolCallId: \`coord-\${i}\`, | ||
| result: { error: errorMessage } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| return toolResults | ||
| } | ||
|
|
||
| // --- 4. Result Aggregation Function --- | ||
|
|
||
| /** | ||
| * Aggregates the tool results into a structured summary for the final agent. | ||
| */ | ||
| export function aggregateToolResults(toolResults: ToolResultPart[], plan: ToolPlan): string { | ||
| let summary = \`## Tool Coordinator Execution Summary | ||
|
|
||
| The Tool Coordinator executed a multi-step plan to address the user's request. | ||
|
|
||
| ### Plan Reasoning | ||
| \${plan.reasoning} | ||
|
|
||
| ### Execution Steps and Results | ||
| \` | ||
|
|
||
| toolResults.forEach((toolResult, index) => { | ||
| const step = plan.steps[index] | ||
| const result = toolResult.result | ||
| const isError = result && typeof result === 'object' && 'error' in result | ||
|
|
||
| summary += \` | ||
| #### Step \${index + 1}: \${step.purpose} (\${step.toolName}) | ||
| \` | ||
| if (isError) { | ||
| summary += \`**Status:** ❌ FAILED | ||
| **Error:** \${result.error} | ||
| \` | ||
| } else { | ||
| summary += \`**Status:** ✅ SUCCESS | ||
| **Result Summary:** \${JSON.stringify(result, null, 2).substring(0, 500)}...\` | ||
| } | ||
| summary += '\n' | ||
| }) | ||
|
|
||
| summary += \` | ||
| --- | ||
| **INSTRUCTION:** Use the above summary and the original user messages to generate a final, coherent, and helpful response. Do not mention the Tool Coordinator or the plan execution process in the final answer, only the synthesized information. | ||
| \` | ||
|
|
||
| return summary | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -217,10 +217,21 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g | |
|
|
||
| , | ||
| parameters: geospatialQuerySchema, | ||
| execute: async (params: z.infer<typeof geospatialQuerySchema>) => { | ||
| const { queryType, includeMap = true } = params; | ||
| execute: async (params: z.infer<typeof geospatialQuerySchema> & { _dependencyResults?: any[] }) => { | ||
| const { queryType, includeMap = true, _dependencyResults } = params; | ||
| console.log('[GeospatialTool] Execute called with:', params); | ||
|
|
||
| if (_dependencyResults && _dependencyResults.length > 0) { | ||
| console.log('[GeospatialTool] Processing dependency results:', _dependencyResults); | ||
| // Logic to process dependency results can be added here. | ||
| // For example, if a previous step was a search, the result might contain coordinates | ||
| // that can be used as input for a subsequent directions query. | ||
| // Since the full logic for dependency injection is complex and depends on the | ||
| // specific tool schema, we will log it for now and ensure the tool can handle it. | ||
| // The LLM planning step is responsible for generating the correct 'params' | ||
| // based on the dependency results. The tool only needs to be aware of them. | ||
| } | ||
|
Comment on lines
+220
to
+233
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Dependency results plumbing is sound; consider scope of logging and persisted payload Extending the executor signature to accept Two things to be aware of:
- return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), queryType, ... }
+ const { _dependencyResults, ...cleanParams } = params
+ return {
+ type: 'MAP_QUERY_TRIGGER',
+ originalUserInput: JSON.stringify(cleanParams),
+ queryType,
+ ...
+ }Functionally the current change is correct; this is mainly about log hygiene and avoiding unnecessary data retention. Also applies to: 346-347 |
||
|
|
||
| const uiFeedbackStream = createStreamableValue<string>(); | ||
| uiStream.append(<BotMessage content={uiFeedbackStream.value} />); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fix syntax around
researchercall and preserve coordinator tool resultsThere are two separate issues in this block:
Broken syntax (parse error) around
researchercallLines 365‑374 contain stray characters (
c,)cAPI,, duplicated!useToolCoordinator), which Biome also flags. This makes the file invalid TypeScript/JS.You likely intended the loop body to look like this:
useToolCoordinatoristrue, you correctly settoolOutputs = resultsfromexecuteToolPlan(plan).toolOutputs = toolResponses.researcheris called withuseTools = !useToolCoordinator→false, sotoolResponsesis always[]. That overwrites the actual coordinator results.aiState:never sees the coordinator’s tool results.
To fix this while keeping current behavior for non‑coordinator runs, guard the assignment:
Together with the cleaned‑up call, that will:
toolOutputscontinues to refer to the executed plan’s results and gets persisted intoaiStateas intended, while non‑coordinator flows keep usingresearcher’s tool responses.After applying the fixes, re‑run your linter/formatter and verify:
USE_TOOL_COORDINATOR=true, you seetoolmessages in saved chats corresponding to the coordinator’s executed steps, and with itfalseyou still see the regular streaming-tool behavior.Also applies to: 128-129, 323-357, 359-379
🏁 Script executed:
Length of output: 2626
🏁 Script executed:
Length of output: 513
🏁 Script executed:
Length of output: 362
Fix syntax errors in
researchercall and preserve coordinator resultsLines 365–375 contain malformed syntax blocking file parsing:
cbeforeconst)cAPI,!useToolCoordinatorcomments with extra closing parenthesesClean up the call to match the intended function signature:
while ( useSpecificAPI ? answer.length === 0 : answer.length === 0 && !errorOccurred ) { // If coordinator was used, pass finalMessages and disable tools for researcher - c const { fullResponse, hasError, toolResponses } = await researcher( - currentSystemPrompt, - uiStream, - streamText, - finalMessages, - useSpecificAPI, - !useToolCoordinator // Pass a flag to disable tools if coordinator was used - )cAPI, - !useToolCoordinator // Pass a flag to disable tools if coordinator was used - ) !useToolCoordinator // Pass a flag to disable tools if coordinator was used - ) + const { fullResponse, hasError, toolResponses } = await researcher( + currentSystemPrompt, + uiStream, + streamText, + finalMessages, + useSpecificAPI, + !useToolCoordinator // Pass a flag to disable tools if coordinator was used + )Additionally, line 377 unconditionally overwrites
toolOutputs(set from coordinator results at line 331) withtoolResponses, which is empty when coordinator is enabled sinceresearcheris called with tools disabled. Guard the assignment to preserve coordinator results:This ensures the block at line 380–396 that persists tool outputs receives the coordinator's executed plan results when enabled, while non-coordinator flows continue using researcher tool responses.