Skip to content

Conversation

@ngoiyaeric
Copy link
Collaborator

@ngoiyaeric ngoiyaeric commented Nov 21, 2025

User description

This pull request implements the Tool Coordinator worker, which introduces the Orchestrator-Worker pattern for tool use, as outlined in the Agent workflow documentation.

Key Improvements for Mapbox/Geospatial Tool Efficiency:

  1. Structured Planning: Tool calls are now planned upfront using a structured JSON schema, replacing the less reliable dynamic, streaming-based tool selection. This ensures a deterministic and efficient sequence of tool calls.
  2. Multi-Step Coordination: The new architecture enables complex, sequential workflows, such as:
    • Geocode a location (Step 1)
    • Use the resulting coordinates to find directions or nearby points of interest (Step 2)
  3. Tool Refinement: The geospatialTool is updated to accept _dependencyResults, allowing it to consume outputs from previous steps in the coordinated plan.
  4. Feature Flag: The new logic is integrated into app/actions.tsx and controlled by the USE_TOOL_COORDINATOR environment variable for safe, gradual rollout.

This change significantly improves the reliability and efficiency of multi-tool queries, especially those involving the Mapbox/Geospatial tool.


PR Type

Enhancement


Description

  • Implements Tool Coordinator worker for structured multi-step tool execution planning

  • Enables sequential tool workflows with dependency tracking between steps

  • Updates geospatialTool to accept and process dependency results from previous steps

  • Integrates coordinator into app actions with USE_TOOL_COORDINATOR feature flag

  • Adds fallback mechanism to researcher when coordinator execution fails


Diagram Walkthrough

flowchart LR
  A["User Query"] --> B["Tool Coordinator"]
  B --> C["Generate Structured Plan"]
  C --> D["Execute Tool Plan"]
  D --> E["Aggregate Results"]
  E --> F["Researcher Synthesis"]
  F --> G["Final Response"]
Loading

File Walkthrough

Relevant files
Enhancement
tool-coordinator.tsx
New Tool Coordinator module for multi-step planning           

lib/agents/tool-coordinator.tsx

  • Defines toolStepSchema and toolPlanSchema for structured tool
    execution planning
  • Implements toolCoordinator() function to analyze queries and generate
    multi-step plans using LLM
  • Implements executeToolPlan() function to sequentially execute planned
    steps with dependency resolution
  • Implements aggregateToolResults() function to summarize execution
    results for final synthesis
+169/-0 
actions.tsx
Integrate Tool Coordinator into action submission flow     

app/actions.tsx

  • Imports Tool Coordinator functions (toolCoordinator, executeToolPlan,
    aggregateToolResults)
  • Adds USE_TOOL_COORDINATOR environment variable check for feature flag
    control
  • Conditionally executes tool coordinator before researcher when flag is
    enabled
  • Passes coordinator results summary to researcher as tool message
  • Adds fallback to standard researcher flow if coordinator fails
  • Passes useTools flag to researcher to disable tools when coordinator
    is active
+49/-7   
geospatial.tsx
Add dependency results support to geospatial tool               

lib/agents/tools/geospatial.tsx

  • Updates execute() function signature to accept optional
    _dependencyResults parameter
  • Adds logic to process and log dependency results from previous tool
    steps
  • Enables tool to consume outputs from coordinated multi-step workflows
+13/-2   
researcher.tsx
Add conditional tool usage to researcher agent                     

lib/agents/researcher.tsx

  • Adds useTools parameter (defaults to true) to control tool
    availability
  • Conditionally passes tools to streamText() based on useTools flag
  • Allows researcher to operate without tools when Tool Coordinator is
    active
+3/-2     
index.tsx
Export Tool Coordinator functions                                               

lib/agents/index.tsx

  • Exports Tool Coordinator module functions for use throughout the
    application
+1/-0     

Summary by CodeRabbit

  • New Features

    • Coordinated tool workflow: plans and runs multi-step tool actions, then incorporates a summarized result into final responses
    • User-facing progress updates shown during planning and execution
  • Improvements

    • Ability to disable tool usage for downstream processing when coordination is active
    • Dependency-aware tool execution with richer per-step results and graceful fallback to the original streaming flow on failure

✏️ Tip: You can customize this high-level summary in your review settings.

Feat: Improve prompting for exploration efficiency
@vercel
Copy link

vercel bot commented Nov 21, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
qcx Error Error Nov 25, 2025 11:45am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

Walkthrough

Adds a tool coordination flow: a new coordinator that plans, executes, and aggregates multi-step tool calls; app/actions invokes the coordinator (optionally), injects a coordinator summary into final messages, and falls back to the original streaming researcher flow on coordinator errors. Researcher and geospatial tool signatures updated to support the flow.

Changes

Cohort / File(s) Summary
Tool Coordinator module
lib/agents/tool-coordinator.tsx, lib/agents/index.tsx
New coordinator: toolCoordinator (planner), executeToolPlan (executor), aggregateToolResults (aggregator); zod-validated ToolPlan/ToolStep types; index re-exports the module.
Integration & control flow
app/actions.tsx
Adds USE_TOOL_COORDINATOR branch: request plan, execute plan, aggregate results into a tool_coordinator_summary message appended to finalMessages; emits progress updates; on failure logs error, notifies user, and falls back to original streaming researcher path.
Researcher API change
lib/agents/researcher.tsx
researcher signature gains useTools: boolean = true; when false, tools are not fetched/passed (tool invocation disabled).
Tool signature update
lib/agents/tools/geospatial.tsx
execute signature extended to accept optional _dependencyResults?: any[]; logs and (placeholder) handles dependency results before continuing execution.

Sequence Diagram(s)

sequenceDiagram
    participant UI as app/actions
    participant Coord as toolCoordinator
    participant Exec as executeToolPlan
    participant Tool as Tool (various)
    participant Agg as aggregateToolResults
    participant Rsrch as researcher

    UI->>Coord: request ToolPlan (messages)
    Coord->>Coord: build & zod-validate ToolPlan
    Coord-->>UI: ToolPlan

    UI->>Exec: execute ToolPlan
    loop for each step
        Exec->>Exec: resolve dependencyIndices -> _dependencyResults
        Exec->>Tool: call toolName with toolArgs (+ _dependencyResults)
        Tool-->>Exec: ToolResultPart (success / error)
    end
    Exec-->>UI: ToolResultPart[]

    UI->>Agg: aggregateToolResults(plan, results)
    Agg-->>UI: markdown summary

    alt coordinator succeeded
        UI->>Rsrch: call researcher(finalMessages, useTools=false)
    else coordinator failed
        UI->>UI: log & notify user
        UI->>Rsrch: fallback researcher(originalMessages, useTools=true)
    end

    Rsrch-->>UI: final synthesis response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • lib/agents/tool-coordinator.tsx: zod schemas, plan parsing, dependency resolution, error propagation, and fallbacks.
    • app/actions.tsx: correct message injection, progress UI messages, and fallback handling.
    • lib/agents/researcher.tsx and lib/agents/tools/*: consistent handling of useTools and _dependencyResults types across tools.

Possibly related PRs

Suggested reviewers

  • CharlieHelps

Poem

🐇 I hopped through plans and threaded each call,

Steps linked like carrots, short and tall,
I gathered results and rolled them in rhyme,
A markdown bouquet to save you time,
Hooray — tools danced together, one neat sprawl. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a Tool Coordinator module for orchestrating multi-step tool execution with planning and aggregation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/tool-coordinator-mapbox

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@charliecreates
Copy link

  1. The introduction of the Tool Coordinator worker implementing the Orchestrator-Worker pattern is a strong architectural upgrade—clear separation of planning and execution makes the flow transparent and maintainable.

  2. The upfront structured JSON planning approach (toolPlan) is a major reliability improvement. This should help chase down elusive bugs from dynamic, stepwise tool invocation. Consider including more in-line documentation or a plan schema example in the code or PR for easier onboarding and review.

  3. Updating the geospatialTool to support _dependencyResults is a thoughtful touch, enabling more advanced multi-step flows. Double-check that circular dependencies and race conditions are guarded, especially if this is opened up to others tools in the future.

  4. The use of the USE_TOOL_COORDINATOR feature flag is excellent for gradual rollout. Test coverage for both old and new flows is important—are there new/updated tests ensuring parity and preventing regressions? If not, add tests for both single-tool and multi-step flows, including Mapbox edge cases.

  5. The changes are well-scoped to targeted files; great focus. Just confirm documentation is up-to-date, especially the Agent workflow docs and any in-repo usage guides.

  6. Consider adding logging (even if hidden behind the feature flag or only for dev) for coordinated tool execution plans and run results to aid future debugging.

Overall, this is a very solid improvement that addresses systemic workflow issues. The only remaining asks: add some extra doc/examples, and double-check testing for both paths and new error cases.

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Nov 21, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🔴
Robustness/DoS risk

Description: The newly added loop contains malformed/duplicate arguments and stray characters ('c',
')cAPI,', and repeated flags) which indicate a likely logic/flow bug that could break
control flow and error handling, potentially causing unexpected behavior or denial of
service at runtime.
actions.tsx [365-375]

Referred Code
      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
  )
Sensitive information exposure

Description: Unstructured tool result and error details (including potential sensitive data from tools)
are logged to console via JSON.stringify and console.error, risking leakage of user
inputs, API responses, or internal errors into logs in production.
tool-coordinator.tsx [102-115]

Referred Code
  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)
Sensitive information exposure

Description: Dependency results from prior steps are logged verbosely to console without redaction,
which may include coordinates, addresses, or other sensitive user/location data, leading
to sensitive information exposure in logs.
geospatial.tsx [221-233]

Referred Code
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.
}
Sensitive information exposure

Description: Fallback path logs 'Tool Coordinator failed' with the caught error object to console,
potentially exposing internal error details or sensitive data from tool execution in
production logs.
actions.tsx [349-356]

Referred Code
} catch (e) {
  console.error('Tool Coordinator failed:', e)
  uiStream.append(
    <BotMessage content="Tool Coordinator failed. Falling back to streaming researcher." />
  )
  // Fallback: continue with the original messages and let the researcher handle it
  finalMessages = messages
}
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Syntax Error Risk: The new researcher invocation includes corrupted syntax and duplicated parameters which
will cause runtime or build errors rather than graceful handling.

Referred Code
      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
  )

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing Audit Logs: New critical actions like tool planning and execution lack structured audit logging with
user ID and outcomes, relying only on console logs.

Referred Code
  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[] = []



 ... (clipped 54 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Garbled Code Lines: Introduced malformed lines and duplicated arguments around the researcher call obscure
intent and reduce readability.

Referred Code
      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
  )

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Verbose Console Errors: Console.error outputs include tool names and raw error objects which may surface internal
details if surfaced to users or logs without sanitization.

Referred Code
if (!tool) {
  console.error(\`Tool \${step.toolName} not found.\`)
  results.set(i, { error: \`Tool \${step.toolName} not found.\` })
  continue
}

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)


 ... (clipped 14 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Logs Raw Params: The geospatial tool logs full params and dependency results which may inadvertently
include sensitive user inputs or coordinates.

Referred Code
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.
  }

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated Inputs: Tool arguments and dependency results are passed through and executed without explicit
validation or sanitization beyond schema adherence in planning.

Referred Code
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)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ ngoiyaeric
❌ Manus AI


Manus AI seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Nov 21, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Separate dependency updates from feature code

Revert the extensive dependency updates from pnpm-lock.yaml and move them to a
separate PR. This isolates the new feature implementation from unrelated and
high-risk package upgrades, improving reviewability and stability.

Examples:

pnpm-lock.yaml [9-269]
  .:
    dependencies:
      '@ai-sdk/amazon-bedrock':
        specifier: ^1.1.6
        version: 1.1.6(zod@3.24.1)
      '@ai-sdk/anthropic':
        specifier: ^1.2.12
        version: 1.2.12(zod@3.24.1)
      '@ai-sdk/google':
        specifier: ^1.2.22

 ... (clipped 251 lines)

Solution Walkthrough:

Before:

# Pull Request Changes

## File: app/actions.tsx
+ import { toolCoordinator, executeToolPlan, ... } from '@/lib/agents'
+ const useToolCoordinator = process.env.USE_TOOL_COORDINATOR === 'true'
+ if (useToolCoordinator) {
+   const plan = await toolCoordinator(messages)
+   const results = await executeToolPlan(plan)
+   // ...
+ }

## File: pnpm-lock.yaml
- react: version 18.3.1
+ react: version 19.2.0
- next: version 14.2.23
+ next: version 15.5.6
# ... hundreds of other dependency updates

After:

# Pull Request Changes (This PR)

## File: app/actions.tsx
+ import { toolCoordinator, executeToolPlan, ... } from '@/lib/agents'
+ const useToolCoordinator = process.env.USE_TOOL_COORDINATOR === 'true'
+ if (useToolCoordinator) {
+   const plan = await toolCoordinator(messages)
+   const results = await executeToolPlan(plan)
+   // ...
+ }
# pnpm-lock.yaml only contains changes for newly added dependencies.

# --- In a separate PR ---
# Pull Request: "chore: Update dependencies"

## File: pnpm-lock.yaml
- react: version 18.3.1
+ react: version 19.2.0
- next: version 14.2.23
+ next: version 15.5.6
# ... hundreds of other dependency updates
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that mixing a major feature with massive dependency upgrades in pnpm-lock.yaml is poor practice, as it complicates review and increases risk.

High
Possible issue
Fix broken fallback logic

Introduce a coordinatorSucceeded flag to correctly manage the researcher's tool
usage, ensuring tools are enabled as a fallback if the Tool Coordinator fails.

app/actions.tsx [323-377]

 let finalMessages = messages
+let coordinatorSucceeded = false
 
 if (useToolCoordinator) {
   uiStream.update(<Spinner text="Planning tool execution..." />)
   try {
     const plan = await toolCoordinator(messages)
     uiStream.update(<Spinner text="Executing tool plan..." />)
     const results = await executeToolPlan(plan)
     toolOutputs = results
     const summary = aggregateToolResults(results, plan)
     
     // Add the summary to the messages for the final synthesis agent
     finalMessages = [
       ...messages,
       {
         id: nanoid(),
         role: 'tool',
         content: summary,
         type: 'tool_coordinator_summary'
       } as any // Cast to any to satisfy CoreMessage type for custom type
     ]
+    coordinatorSucceeded = true
     
     // Stream a message to the user about the tool execution completion
     uiStream.append(
       <BotMessage content="Tool execution complete. Synthesizing final answer..." />
     )
   } catch (e) {
     console.error('Tool Coordinator failed:', e)
     uiStream.append(
       <BotMessage content="Tool Coordinator failed. Falling back to streaming researcher." />
     )
     // Fallback: continue with the original messages and let the researcher handle it
     finalMessages = messages
   }
 }
 
 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,
+    !coordinatorSucceeded // Pass a flag to disable tools if coordinator succeeded
   )
   answer = fullResponse
   toolOutputs = toolResponses

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical flaw in the new fallback logic where the researcher's tools would be incorrectly disabled after a Tool Coordinator failure, and provides a correct solution.

High
Fix markdown rendering in results
Suggestion Impact:The commit modified the result aggregation to format results within a fenced code block and adjusted markdown structure, addressing the rendering concern. Although it did not explicitly escape backticks, it changed the output to a ```json code block with proper opening/closing fences and improved headings, which aligns with the suggestion’s intent to fix markdown rendering.

code diff:

 export function aggregateToolResults(toolResults: ToolResultPart[], plan: ToolPlan): string {
-  let summary = \`## Tool Coordinator Execution Summary
+  let summary = `# Tool Coordinator Execution Summary
 
 The Tool Coordinator executed a multi-step plan to address the user's request.
 
 ### Plan Reasoning
-\${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
+    const hasError = result && typeof result === 'object' && 'error' in result
 
-    summary += \`
-#### Step \${index + 1}: \${step.purpose} (\${step.toolName})
-\`
-    if (isError) {
-      summary += \`**Status:** ❌ FAILED
-**Error:** \${result.error}
-\`
+    summary += `
+#### Step ${index + 1}: ${step.purpose} (\`${step.toolName}\`)
+`
+
+    if (hasError) {
+      summary += `**Status:** ❌ FAILED
+**Error:** ${result.error}
+`
     } else {
-      summary += \`**Status:** ✅ SUCCESS
-**Result Summary:** \${JSON.stringify(result, null, 2).substring(0, 500)}...\`
+      const resultStr M= JSON.stringify(result, null, 2)
+      const truncated = resultStr.length > 500 ? resultStr.substring(0, 500) + '...' : resultStr
+      summary += `**Status:** ✅ SUCCESS
+**Result Summary:**
+\`\`\`json
+${truncated}
+\`\`\`
+`
     }
-    summary += '\n'
   })
 
-  summary += \`
+  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.
-\`
+**INSTRUCTION:** Using the information above and the original user messages, generate a final, coherent, and helpful response to the user. 
+Do not mention the Tool Coordinator, internal planning, or execution details — only present the synthesized answer naturally.
+`
 

Fix a potential markdown rendering issue in the result summary by escaping
backticks in the result string and ensuring the code block is properly closed.

lib/agents/tool-coordinator.tsx [147-159]

-const isError = result && typeof result === 'object' && 'error' in result
+const isError = result && typeof result === 'object' && 'error' in result;
+const resultString = JSON.stringify(result, null, 2)
+  .replace(/`/g, '\\`') // Escape backticks
+  .substring(0, 500);
 
 summary += `
 #### Step ${index + 1}: ${step.purpose} (${step.toolName})
-`
+`;
 if (isError) {
-  summary += `**Status:** ❌ FAILED
+  summary += `
+**Status:** ❌ FAILED
 **Error:** ${result.error}
-`
+`;
 } else {
-  summary += `**Status:** ✅ SUCCESS
-**Result Summary:** ${JSON.stringify(result, null, 2).substring(0, 500)}...\`
+  summary += `
+**Status:** ✅ SUCCESS
+**Result Summary:** \`${resultString}...\`
+`;
 }

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential markdown rendering issue by escaping backticks and ensures the code block is always correctly formatted, which prevents broken output in the summary.

Low
Add validation for tool dependencies

Add validation to the getDependencyResults function to ensure a tool step does
not depend on a future or current step, improving error handling for malformed
plans.

lib/agents/tool-coordinator.tsx [73-84]

 // Function to get results of dependencies
-const getDependencyResults = (indices: number[]) => {
-  return indices.map(index => {
+const getDependencyResults = (indices: number[], currentStepIndex: number) => {
+  for (const index of indices) {
+    if (index >= currentStepIndex) {
+      throw new Error(`Invalid dependency: Step ${currentStepIndex} cannot depend on a future or current step ${index}.`);
+    }
     if (!results.has(index)) {
-      throw new Error(`Dependency step ${index} has not been executed yet.`)
+      throw new Error(`Dependency error: Result for step ${index} is not available.`);
     }
-    return results.get(index)
-  })
+  }
+  return indices.map(index => results.get(index));
 }
 
 for (let i = 0; i < plan.steps.length; i++) {
   const step = plan.steps[i]
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that the LLM could produce an invalid plan, and while the existing code handles this, the proposed change adds explicit validation for dependency indices, resulting in clearer error messages and improved debuggability.

Low
General
Improve system prompt for reliability

Improve the system prompt to explicitly instruct the LLM that dependency indices
must refer to previous steps, increasing the reliability of generated plans.

lib/agents/tool-coordinator.tsx [40-50]

 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.
 
 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).
+1. The plan must be a sequence of steps, executed in the order they are defined.
+2. For a step that depends on the output of a previous step, specify its index in the 'dependencyIndices' array. Dependency indices must be less than the current step's index (0-based).
 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)}
 `
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion improves the system prompt with clearer instructions for the LLM, which is likely to increase the reliability of the generated tool plans and reduce execution errors.

Medium
  • Update

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13b890e and f2bc7da.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • app/actions.tsx (3 hunks)
  • lib/agents/index.tsx (1 hunks)
  • lib/agents/researcher.tsx (2 hunks)
  • lib/agents/tool-coordinator.tsx (1 hunks)
  • lib/agents/tools/geospatial.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
lib/agents/index.tsx (1)
lib/agents/tools/index.tsx (2)
  • ToolProps (7-11)
  • ToolProps (14-39)
lib/agents/researcher.tsx (1)
lib/agents/tools/index.tsx (2)
  • getTools (14-39)
  • ToolProps (7-11)
lib/agents/tools/geospatial.tsx (1)
lib/schema/geospatial.tsx (1)
  • geospatialQuerySchema (13-121)
app/actions.tsx (2)
lib/db/schema.ts (1)
  • messages (26-37)
lib/agents/researcher.tsx (1)
  • researcher (76-154)
🪛 Biome (2.1.2)
lib/agents/tool-coordinator.tsx

[error] 77-77: unexpected token \

(parse)

app/actions.tsx

[error] 365-365: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 372-372: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)

🔇 Additional comments (2)
lib/agents/index.tsx (1)

6-6: Expose Tool Coordinator via agents index is appropriate

Re‑exporting ./tool-coordinator from the agents index cleanly surfaces the new orchestration APIs to callers and matches how app/actions.tsx imports them. No issues here.

lib/agents/researcher.tsx (1)

81-82: New useTools toggle on researcher looks correct; ensure callers pass intent explicitly

Adding useTools: boolean = true and wiring it into tools: useTools ? getTools(...) : undefined gives a clean, backwards‑compatible way to disable tool calls. Existing callers remain unaffected, and app/actions.tsx can now explicitly turn tools off when the coordinator has already run. Just ensure all new call sites intentionally decide whether to pass false (as in the coordinator path) vs relying on the default.

If you want to double‑check usage, run a quick search for researcher( to confirm all non‑coordinator paths still rely on the default and don’t accidentally disable tools.

Also applies to: 105-105

import { Section } from '@/components/section'
import { FollowupPanel } from '@/components/followup-panel'
import { inquire, researcher, taskManager, querySuggestor, resolutionSearch } from '@/lib/agents'
import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, toolCoordinator, executeToolPlan, aggregateToolResults } from '@/lib/agents'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix syntax around researcher call and preserve coordinator tool results

There are two separate issues in this block:

  1. Broken syntax (parse error) around researcher call
    Lines 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:

-    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
-      )
+    while (
+      useSpecificAPI
+        ? answer.length === 0
+        : answer.length === 0 && !errorOccurred
+    ) {
+      // If coordinator was used, pass finalMessages and disable tools for researcher
+      const { fullResponse, hasError, toolResponses } = await researcher(
+        currentSystemPrompt,
+        uiStream,
+        streamText,
+        finalMessages,
+        useSpecificAPI,
+        !useToolCoordinator // Pass a flag to disable tools if coordinator was used
+      )
  1. Coordinator results are lost before being persisted
    • When useToolCoordinator is true, you correctly set toolOutputs = results from executeToolPlan(plan).
    • But inside the loop you unconditionally do toolOutputs = toolResponses.
    • In the coordinator path, researcher is called with useTools = !useToolCoordinatorfalse, so toolResponses is always []. That overwrites the actual coordinator results.
    • As a result, the block that persists tool outputs to aiState:
if (toolOutputs.length > 0) {
  toolOutputs.map(output => {
    aiState.update({ ... })
  })
}

never sees the coordinator’s tool results.

To fix this while keeping current behavior for non‑coordinator runs, guard the assignment:

-      answer = fullResponse
-      toolOutputs = toolResponses
+      answer = fullResponse
+      if (!useToolCoordinator) {
+        toolOutputs = toolResponses
+      }

Together with the cleaned‑up call, that will:

  • Restore valid syntax.
  • Ensure that, when the coordinator is enabled, toolOutputs continues to refer to the executed plan’s results and gets persisted into aiState as intended, while non‑coordinator flows keep using researcher’s tool responses.

After applying the fixes, re‑run your linter/formatter and verify:

  • The file parses cleanly.
  • With USE_TOOL_COORDINATOR=true, you see tool messages in saved chats corresponding to the coordinator’s executed steps, and with it false you still see the regular streaming-tool behavior.

Also applies to: 128-129, 323-357, 359-379


🏁 Script executed:

#!/bin/bash
# Inspect the actual code around the syntax errors at lines 320-380
cat -n app/actions.tsx | sed -n '320,380p'

Length of output: 2626


🏁 Script executed:

#!/bin/bash
# Check lines around USE_TOOL_COORDINATOR flag (128-129)
cat -n app/actions.tsx | sed -n '125,135p'

Length of output: 513


🏁 Script executed:

#!/bin/bash
# Check the toolOutputs assignment pattern in the file
rg -n 'toolOutputs\s*=' app/actions.tsx -C2

Length of output: 362


Fix syntax errors in researcher call and preserve coordinator results

Lines 365–375 contain malformed syntax blocking file parsing:

  • Line 365: stray c before const
  • Line 372: malformed )cAPI,
  • Lines 373–375: duplicated and misplaced !useToolCoordinator comments with extra closing parentheses

Clean 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) with toolResponses, which is empty when coordinator is enabled since researcher is called with tools disabled. Guard the assignment to preserve coordinator results:

       answer = fullResponse
+      if (!useToolCoordinator) {
+        toolOutputs = toolResponses
+      }

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.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +220 to +233
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.
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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 { _dependencyResults?: any[] } and simply ignoring it when absent is a good way to make the tool coordinator compatible without impacting existing callers.

Two things to be aware of:

  • The new log console.log('[GeospatialTool] Processing dependency results:', _dependencyResults); will print the full dependency payloads. Depending on size/sensitivity, you may want to redact or summarize these rather than dumping raw objects.
  • Returning originalUserInput: JSON.stringify(params) now means _dependencyResults (including upstream tool outputs) will be embedded in the stored payload whenever the coordinator is involved. If that object can be large or contain redundant/sensitive data, consider omitting _dependencyResults from the serialized copy:
-    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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
app/actions.tsx (1)

374-374: Guard toolOutputs assignment to preserve coordinator results.

When useToolCoordinator is true, line 331 sets toolOutputs = results from the executed plan. However, line 374 unconditionally overwrites this with toolResponses. Since researcher is called with useTools = !useToolCoordinator (i.e., false when coordinator is active), toolResponses will be an empty array, causing the coordinator's results to be lost before persistence at lines 377–393.

Apply this diff to preserve coordinator results:

       answer = fullResponse
+      if (!useToolCoordinator) {
         toolOutputs = toolResponses
+      }
       errorOccurred = hasError

This ensures that when the coordinator is enabled, toolOutputs retains the executed plan's results for persistence, while non-coordinator flows continue using the researcher's tool responses. Based on learnings (past review).

lib/agents/tool-coordinator.tsx (2)

5-5: Fix import path and correct getTools usage.

Three related issues require fixing:

  1. Wrong import path: Line 5 imports from @/lib/tools, which doesn't exist. The correct path is @/lib/agents/tools (confirmed by the relevant code snippets from lib/agents/tools/index.tsx).

  2. Missing required parameters: Lines 32 and 68 call getTools({}) with an empty object, but getTools requires { uiStream, fullResponse } parameters (see lib/agents/tools/index.tsx lines 6-10 for the ToolProps interface).

  3. Wrong return type assumption: Lines 34 and 69 call .map() directly on the result of getTools, but getTools returns an object map of tools (e.g., { search: ..., retrieve: ..., geospatialQueryTool: ... }), not an array. You must use Object.values() to convert it to an array first.

Apply these fixes:

Update the import at line 5:

-import { getTools } from '@/lib/tools'
+import { getTools } from '@/lib/agents/tools'
+import { createStreamableUI } from 'ai/rsc'

Fix lines 32-38 in toolCoordinator:

   const model = getModel()
-  const tools = getTools({})
+  const toolsObj = getTools({ 
+    uiStream: createStreamableUI(), 
+    fullResponse: '' 
+  })

-  const toolDescriptions = tools.map(tool => ({
+  const toolDescriptions = Object.values(toolsObj).map(tool => ({
     name: tool.toolName,
     description: tool.description,
     parameters: tool.parameters
   }))

Fix lines 68-69 in executeToolPlan:

-  const allTools = getTools({})
-  const toolMap = new Map(allTools.map(tool => [tool.toolName, tool]))
+  const toolsObj = getTools({ 
+    uiStream: createStreamableUI(), 
+    fullResponse: '' 
+  })
+  const toolMap = new Map(Object.values(toolsObj).map(tool => [tool.toolName, tool]))

Based on learnings (past review).

Also applies to: 32-38, 68-69


87-91: Push a ToolResultPart placeholder for missing tools to maintain aggregation alignment.

When a tool is not found, the code logs an error and stores it in results but doesn't push a corresponding ToolResultPart to toolResults. This breaks the 1:1 alignment between plan.steps and toolResults that aggregateToolResults assumes at line 144-145, where it directly indexes plan.steps[index] based on toolResults iteration.

If any step's tool is missing, toolResults.length < plan.steps.length, causing incorrect step-to-result mapping and missing steps in the summary.

Apply this diff to maintain alignment:

     if (!tool) {
       console.error(`Tool ${step.toolName} not found.`)
       const errorResult = { error: `Tool ${step.toolName} not found.` }
       results.set(i, errorResult)
+      toolResults.push({
+        toolName: step.toolName,
+        toolCallId: `coord-${i}`,
+        result: errorResult
+      })
       continue
     }

This ensures every step produces a corresponding ToolResultPart, allowing aggregateToolResults to correctly report all steps including failures. Based on learnings (past review).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2bc7da and 0311b79.

📒 Files selected for processing (2)
  • app/actions.tsx (3 hunks)
  • lib/agents/tool-coordinator.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/actions.tsx (3)
lib/db/schema.ts (1)
  • messages (26-37)
lib/agents/tool-coordinator.tsx (1)
  • toolCoordinator (30-60)
lib/agents/researcher.tsx (1)
  • researcher (76-154)
lib/agents/tool-coordinator.tsx (7)
lib/utils/index.ts (1)
  • getModel (19-62)
lib/agents/tools/index.tsx (2)
  • getTools (14-39)
  • ToolProps (7-11)
lib/agents/tools/retrieve.tsx (1)
  • ToolProps (8-67)
lib/agents/tools/search.tsx (1)
  • ToolProps (8-54)
lib/agents/researcher.tsx (1)
  • researcher (76-153)
lib/agents/tools/video-search.tsx (1)
  • ToolProps (8-50)
lib/agents/tools/geospatial.tsx (1)
  • mcpClient (247-260)
🪛 Biome (2.1.2)
lib/agents/tool-coordinator.tsx

[error] 77-77: unexpected token \

(parse)

🔇 Additional comments (1)
app/actions.tsx (1)

365-372: Researcher function signature is correct and matches the call.

Verification confirms the researcher function at lib/agents/researcher.tsx:76-83 accepts the six-parameter signature as called. The function correctly defines:

  • Parameter 6: useTools: boolean = true
  • Return object includes fullResponse, hasError, and toolResponses

The call at app/actions.tsx:365-372 passes all six arguments correctly, including the boolean flag !useToolCoordinator for the useTools parameter. No syntax or signature mismatch issues exist.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
lib/agents/tool-coordinator.tsx (1)

24-27: Markdown special characters in error messages can break formatting.

As noted in a previous review, line 26 embeds result.error directly into the markdown output without escaping special characters. If the error message contains characters like #, *, [, or backticks, they could break the markdown rendering.

The previous review suggested escaping or wrapping dynamic content in code blocks. Consider applying that fix:

     if (hasError) {
-      summary += `**Status:** Failed
+      summary += `**Status:** ❌ Failed
 **Error:** ${result.error}
 `

Or wrap the error in a code block for safer rendering:

     if (hasError) {
-      summary += `**Status:** Failed
-**Error:** ${result.error}
+      summary += `**Status:** ❌ Failed
+**Error:**
+\`\`\`
+${result.error}
+\`\`\`
 `
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0311b79 and 612479d.

📒 Files selected for processing (1)
  • lib/agents/tool-coordinator.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lib/agents/tool-coordinator.tsx (2)
app/actions.tsx (2)
  • output (339-353)
  • msg (360-368)
lib/agents/researcher.tsx (1)
  • researcher (76-153)

Comment on lines 15 to 22
toolResults.forEach((toolResult, index) => {
const step = plan.steps[index]
const result = toolResult.result
const hasError = result && typeof result === 'object' && 'error' in result

summary += `
#### Step ${index + 1}: ${step.purpose} (\`${step.toolName}\`)
`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add bounds checking for plan.steps[index] access.

The code assumes a 1:1 correspondence between toolResults and plan.steps, accessing plan.steps[index] at line 16 without verifying the index is valid. If toolResults.length > plan.steps.length (which can occur if the alignment issue noted in past reviews is not fixed in executeToolPlan), then step will be undefined and line 21 will throw a TypeError when accessing step.purpose and step.toolName.

Add a guard to handle misaligned arrays:

   toolResults.forEach((toolResult, index) => {
     const step = plan.steps[index]
+    if (!step) {
+      console.error(`No plan step found for tool result at index ${index}`)
+      return
+    }
     const result = toolResult.result

Better yet, validate the lengths upfront:

+  if (toolResults.length !== plan.steps.length) {
+    console.warn(`Mismatch: ${toolResults.length} tool results but ${plan.steps.length} plan steps`)
+  }
+
   toolResults.forEach((toolResult, index) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
toolResults.forEach((toolResult, index) => {
const step = plan.steps[index]
const result = toolResult.result
const hasError = result && typeof result === 'object' && 'error' in result
summary += `
#### Step ${index + 1}: ${step.purpose} (\`${step.toolName}\`)
`
if (toolResults.length !== plan.steps.length) {
console.warn(`Mismatch: ${toolResults.length} tool results but ${plan.steps.length} plan steps`)
}
toolResults.forEach((toolResult, index) => {
const step = plan.steps[index]
if (!step) {
console.error(`No plan step found for tool result at index ${index}`)
return
}
const result = toolResult.result
const hasError = result && typeof result === 'object' && 'error' in result
summary += `
#### Step ${index + 1}: ${step.purpose} (\`${step.toolName}\`)
`
🤖 Prompt for AI Agents
In lib/agents/tool-coordinator.tsx around lines 15 to 22, the code indexes
plan.steps[index] without checking bounds which can produce a TypeError if
toolResults is longer than plan.steps; add a guard so you only read step when
plan.steps[index] is defined (e.g. compute const validLen =
Math.min(toolResults.length, plan.steps.length) or skip iterations where
plan.steps[index] is undefined), and handle the misaligned case by either
skipping extra toolResults or appending a safe fallback summary entry and
logging a warning about the mismatch; also consider validating array lengths
once before the loop and early-return or trim toolResults to validLen.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/agents/tool-coordinator.tsx (1)

170-197: Aggregation assumes 1:1 alignment between toolResults and plan.steps; consider a defensive guard

aggregateToolResults indexes plan.steps[i] for each toolResults[i] without checking bounds. With the current implementation of executeToolPlan, you always push exactly one ToolResultPart per step, so the invariant holds internally.

However, if in the future someone:

  • Appends extra ToolResultParts, or
  • Calls aggregateToolResults with a toolResults array not produced by executeToolPlan,

then plan.steps[i] could be undefined and step.purpose will throw at runtime.

Given the label on this PR and that this function formats user‑visible output, I’d add a small guard:

toolResults.forEach((tr, i) => {
  const step = plan.steps[i]
  if (!step) {
    console.warn(`[ToolCoordinator] No plan step for tool result at index ${i}`)
    return
  }
  // existing logic...
})

Not strictly required today, but it hardens the API against misuse. Since a similar concern was raised in a past review, this is effectively reinforcing that suggestion.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 612479d and 37290d5.

📒 Files selected for processing (1)
  • lib/agents/tool-coordinator.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lib/agents/tool-coordinator.tsx (2)
lib/utils/index.ts (1)
  • getModel (19-62)
lib/agents/tools/index.tsx (1)
  • getTools (14-39)
🔇 Additional comments (3)
lib/agents/tool-coordinator.tsx (3)

70-103: Planner flow looks correct; just ensure model and tools metadata stay aligned

The planner correctly:

  • Fetches tools via getTools({ uiStream: createStreamableUI(), fullResponse: '' }).
  • Projects them to { name, description, parameters } and embeds that JSON into the system prompt.
  • Uses generateObject with toolPlanSchema to get a typed ToolPlan.

Two things to keep in mind:

  1. If getModel() throws (e.g., because the dynamic require fallback fails), the error will bubble from here; confirm callers wrap toolCoordinator in a try/catch and fall back to the legacy path (per PR description).
  2. The planning call instantiates real tool instances just to read metadata. If tool construction ever becomes expensive or side‑effectful, you may want a lightweight, metadata‑only registry instead of calling getTools here.

Overall, the logic is sound and matches the intended orchestrator pattern.


109-164: Execution logic and dependency handling are solid; logging and error shaping look reasonable

This executor is in good shape:

  • Tools are resolved once into toolMap keyed by toolName.
  • resolveDeps enforces that dependencies refer only to already‑executed steps (results.has(i)), which prevents using undefined results.
  • A missing tool or dependency just yields { error: ... } for that step; you still record it in both results and toolResults, keeping alignment with plan.steps.
  • _dependencyResults is passed only when there are dependencies, avoiding noisy args for tools that don’t support it.

Minor considerations:

  • Dependency indices that point to future steps or to the current step are treated as “missing” and surface as errors. That’s acceptable, but if you want stricter guarantees you could validate dependencyIndices during planning (e.g., all < current index).
  • console.log/console.error contain step index and tool name only, which is good from a privacy standpoint; ensure no PII‑bearing args get logged elsewhere.

Functionally, this executor looks correct for sequential orchestrated tool calls.


11-45: Dynamic require fallbacks for getModel/createStreamableUI may not behave as intended under bundlers

This block assumes that missing modules like @/lib/models or @/lib/streamable will be handled at runtime via try/catch, but most bundlers (Next/Webpack) still need to statically resolve these require() paths. If those modules don’t exist, you may get a build‑time “module not found” error despite the try/catch. Also, there is already a canonical getModel in lib/utils/index.ts (see snippet), so this introduces a second, slightly different resolution path.

I recommend either:

  • Re‑using the existing getModel export (or a single shared @/lib/models module that it re‑exports from), and
  • Ensuring the createStreamableUI path matches how other agents obtain it, or centralizing this in a shared utility.

At minimum, please verify that @/lib/models and @/lib/streamable (or @/lib/ui/streamable) actually exist and resolve correctly in this repo’s build pipeline.

Comment on lines +51 to +64
const toolStepSchema = z.object({
toolName: z.string(),
toolArgs: z.record(z.any()),
dependencyIndices: z.array(z.number()).optional(),
purpose: z.string()
})

const toolPlanSchema = z.object({
reasoning: z.string(),
steps: z.array(toolStepSchema)
})

export type ToolPlan = z.infer<typeof toolPlanSchema>
export type ToolStep = z.infer<typeof toolStepSchema>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Schema is functional but could be tightened for safety and guidance

The toolStepSchema/toolPlanSchema are straightforward and work, but they’re very permissive:

  • toolArgs: z.record(z.any()) allows arbitrary shapes, including non‑JSON‑safe values.
  • There’s no bound on steps length, so a misbehaving model could produce a very large plan.

Consider:

  • Switching toolArgs to z.record(z.unknown()) (or a narrower shape if you standardize tool args) to avoid implicitly “any” values.
  • Adding .max(n) to steps (e.g. 6–10) to guard against pathological plans and control tool‑call blast radius.

These changes aren’t required for correctness but would improve robustness.

🤖 Prompt for AI Agents
In lib/agents/tool-coordinator.tsx around lines 51–64, the zod schemas are too
permissive: change toolArgs from z.record(z.any()) to z.record(z.unknown()) (or
a narrower shape if you have a standardized arg schema) to avoid implicit
any/unsafe values, and add a max bound to steps (e.g., .max(10)) on the
toolPlanSchema's steps array to limit plan size; keep dependencyIndices optional
as-is. Ensure types exported (ToolPlan/ToolStep) remain in sync after these
schema changes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/actions.tsx (1)

373-374: Coordinator tool results are lost before persistence

Line 374 unconditionally overwrites toolOutputs with toolResponses. When useToolCoordinator is true:

  • Line 331 sets toolOutputs = results (coordinator's executed plan results)
  • Line 371 calls researcher with useTools = false, so toolResponses is []
  • Line 374 overwrites coordinator results with []
  • The persistence block at lines 377-393 never sees the coordinator's tool results

This was flagged in the previous review.

Guard the assignment to preserve coordinator results:

       answer = fullResponse
+      if (!useToolCoordinator) {
+        toolOutputs = toolResponses
+      }
-      toolOutputs = toolResponses
       errorOccurred = hasError

This ensures coordinator results persist to aiState, while non-coordinator flows continue using researcher tool responses.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37290d5 and e4dbb38.

📒 Files selected for processing (1)
  • app/actions.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/actions.tsx (4)
lib/db/schema.ts (1)
  • messages (26-37)
lib/agents/tool-coordinator.tsx (3)
  • toolCoordinator (70-103)
  • executeToolPlan (114-164)
  • aggregateToolResults (170-197)
components/message.tsx (1)
  • BotMessage (11-31)
lib/agents/researcher.tsx (1)
  • researcher (76-154)
🔇 Additional comments (2)
app/actions.tsx (2)

15-15: LGTM!

The coordinator function imports are correctly added and align with the module exports.


365-372: LGTM!

The researcher call syntax is now clean and correctly passes finalMessages and disables tools via !useToolCoordinator when the coordinator is active.

Comment on lines +325 to +357
if (useToolCoordinator) {
uiStream.update(<div><Spinner /> Planning tool execution...</div>)
try {
const plan = await toolCoordinator(messages)
uiStream.update(<div><Spinner /> Executing tool plan...</div>)
const results = await executeToolPlan(plan)
toolOutputs = results
const summary = aggregateToolResults(results, plan)

// Add the summary to the messages for the final synthesis agent
finalMessages = [
...messages,
{
id: nanoid(),
role: 'tool',
content: summary,
type: 'tool_coordinator_summary'
} as any // Cast to any to satisfy CoreMessage type for custom type
]

// Stream a message to the user about the tool execution completion
uiStream.append(
<BotMessage content="Tool execution complete. Synthesizing final answer..." />
)
} catch (e) {
console.error('Tool Coordinator failed:', e)
uiStream.append(
<BotMessage content="Tool Coordinator failed. Falling back to streaming researcher." />
)
// Fallback: continue with the original messages and let the researcher handle it
finalMessages = messages
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix missing parameter, type mismatches, and BotMessage usage

Three issues in the coordinator flow:

  1. Missing required context parameter (line 330)
    executeToolPlan expects (plan, context) but is called with only plan. This will cause a runtime error.

  2. Type mismatch for tool message content (lines 335-343)
    Role 'tool' in CoreMessage expects content to be ToolResultPart[], not a string. The cast to any at line 342 hides this type error and may cause issues when researcher processes finalMessages.

  3. BotMessage expects StreamableValue (lines 347, 352)
    BotMessage component requires content: StreamableValue<string>, but plain strings are passed.

Apply these fixes:

     if (useToolCoordinator) {
       uiStream.update(<div><Spinner /> Planning tool execution...</div>)
       try {
         const plan = await toolCoordinator(messages)
         uiStream.update(<div><Spinner /> Executing tool plan...</div>)
-        const results = await executeToolPlan(plan)
+        const results = await executeToolPlan(plan, { uiStream, fullResponse: '' })
         toolOutputs = results
         const summary = aggregateToolResults(results, plan)
         
-        // Add the summary to the messages for the final synthesis agent
+        // Add coordinator summary as an assistant message for synthesis
         finalMessages = [
           ...messages,
           {
             id: nanoid(),
-            role: 'tool',
+            role: 'assistant',
             content: summary,
             type: 'tool_coordinator_summary'
-          } as any // Cast to any to satisfy CoreMessage type for custom type
+          }
         ]
         
-        // Stream a message to the user about the tool execution completion
+        const completionMsg = createStreamableValue<string>()
+        completionMsg.done("Tool execution complete. Synthesizing final answer...")
         uiStream.append(
-          <BotMessage content="Tool execution complete. Synthesizing final answer..." />
+          <BotMessage content={completionMsg.value} />
         )
       } catch (e) {
         console.error('Tool Coordinator failed:', e)
+        const errorMsg = createStreamableValue<string>()
+        errorMsg.done("Tool Coordinator failed. Falling back to streaming researcher.")
         uiStream.append(
-          <BotMessage content="Tool Coordinator failed. Falling back to streaming researcher." />
+          <BotMessage content={errorMsg.value} />
         )
-        // Fallback: continue with the original messages and let the researcher handle it
         finalMessages = messages
       }
     }

Changing the role from 'tool' to 'assistant' ensures type safety and allows the summary to flow naturally to the researcher as context, while the actual tool results are already captured in toolOutputs for persistence.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/actions.tsx around lines 325 to 357, fix three issues: call
executeToolPlan with the missing context parameter (pass the current context
object used elsewhere when invoking tools), change the synthesized tool-summary
message to use role 'assistant' (not 'tool') and remove the any cast so the
content is a plain string (or appropriate assistant content type) to satisfy
CoreMessage typing, and ensure BotMessage receives a StreamableValue<string> by
wrapping the plain string messages with the project’s streamable value helper
(or creating a StreamableValue) before passing to uiStream.append/update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants