Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { FeatureCollection } from 'geojson'
import { Spinner } from '@/components/ui/spinner'
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.

// Removed import of useGeospatialToolMcp as it no longer exists and was incorrectly used here.
// The geospatialTool (if used by agents like researcher) now manages its own MCP client.
import { writer } from '@/lib/agents/writer'
Expand Down Expand Up @@ -125,6 +125,7 @@ async function submit(formData?: FormData, skip?: boolean) {

const groupeId = nanoid()
const useSpecificAPI = process.env.USE_SPECIFIC_API_FOR_WRITER === 'true'
const useToolCoordinator = process.env.USE_TOOL_COORDINATOR === 'true'
const maxMessages = useSpecificAPI ? 5 : 10
messages.splice(0, Math.max(messages.length - maxMessages, 0))

Expand Down Expand Up @@ -319,17 +320,58 @@ async function submit(formData?: FormData, skip?: boolean) {
const streamText = createStreamableValue<string>()
uiStream.update(<Spinner />)

let finalMessages = messages

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
]

// 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
) {
const { fullResponse, hasError, toolResponses } = await researcher(
currentSystemPrompt,
uiStream,
streamText,
messages,
useSpecificAPI
// 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
)
answer = fullResponse
toolOutputs = toolResponses
Expand Down
1 change: 1 addition & 0 deletions lib/agents/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './inquire'
export * from './query-suggestor'
export * from './researcher'
export * from './resolution-search'
export * from './tool-coordinator'
5 changes: 3 additions & 2 deletions lib/agents/researcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ export async function researcher(
uiStream: ReturnType<typeof createStreamableUI>,
streamText: ReturnType<typeof createStreamableValue<string>>,
messages: CoreMessage[],
useSpecificModel?: boolean
useSpecificModel?: boolean,
useTools: boolean = true
) {
let fullResponse = ''
let hasError = false
Expand All @@ -101,7 +102,7 @@ export async function researcher(
maxTokens: 4096,
system: systemPromptToUse,
messages,
tools: getTools({ uiStream, fullResponse }),
tools: useTools ? getTools({ uiStream, fullResponse }) : undefined,
})

uiStream.update(null) // remove spinner
Expand Down
169 changes: 169 additions & 0 deletions lib/agents/tool-coordinator.tsx
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.
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.\`)
}
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
}

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
}
15 changes: 13 additions & 2 deletions lib/agents/tools/geospatial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
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


const uiFeedbackStream = createStreamableValue<string>();
uiStream.append(<BotMessage content={uiFeedbackStream.value} />);

Expand Down
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.