From 3b513ab57b2e26feeefc46959deb8f096a349b47 Mon Sep 17 00:00:00 2001 From: Vincent Grobler Date: Thu, 9 Apr 2026 13:12:14 +0100 Subject: [PATCH] fix(orchestrator): capture final output for webhook dispatch Root cause: when the brain calls final_answer, the output was saved to the DB via finalizeRun but never captured into the finalOutput variable used by the webhook dispatch. The previous fix passed runOutput to the dispatcher, but runOutput was fetched from DB and could be null due to timing. Also the original code returned 'Final answer submitted.' instead of the actual output from the final_answer tool result. Changes: - Track finalOutput at function scope, set it in all three finalization paths: no-tool-call, final_answer tool, and loop exhaustion - Capture toolResult.result into finalOutput when isDone is true - Return actual output from final_answer case (not placeholder string) - Add error checking to finalizeRun's DB update - Add debug logging for output lengths at key points - Fall back to DB fetch only if inline tracking missed it --- task-runner/src/orchestratorExecutor.ts | 33 ++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/task-runner/src/orchestratorExecutor.ts b/task-runner/src/orchestratorExecutor.ts index e0966d3..d6f5d4b 100644 --- a/task-runner/src/orchestratorExecutor.ts +++ b/task-runner/src/orchestratorExecutor.ts @@ -138,6 +138,7 @@ export async function processOrchestratorRun(run: TeamRun): Promise { let totalTokens = 0; let totalCost = 0; let teamData: { name: string; config: OrchestratorConfig; mode: string; output_route_ids: string[] | null } | null = null; + let finalOutput: string | null = null; // Track the finalized output for webhook dispatch try { // 1. Fetch team to get config @@ -221,7 +222,8 @@ export async function processOrchestratorRun(run: TeamRun): Promise { if (!toolCall) { // Brain gave a text response without a tool call — treat as final answer - await finalizeRun(run.id, brainResult.result, totalTokens, totalCost); + finalOutput = brainResult.result; + await finalizeRun(run.id, finalOutput, totalTokens, totalCost); isDone = true; break; } @@ -261,6 +263,10 @@ export async function processOrchestratorRun(run: TeamRun): Promise { if (toolResult.isDone) { isDone = true; + // Capture the output from final_answer for webhook dispatch + if (!finalOutput && toolResult.result) { + finalOutput = toolResult.result; + } } // Update run progress @@ -288,6 +294,7 @@ export async function processOrchestratorRun(run: TeamRun): Promise { totalTokens, totalCost, ); + finalOutput = lastOutputs || 'Orchestrator reached maximum loop count without producing a final answer.'; } // Write usage record @@ -306,15 +313,22 @@ export async function processOrchestratorRun(run: TeamRun): Promise { console.log(`[Orchestrator] Completed run ${run.id} (${loopCount} loops, ${totalTokens} tokens)`); // Store output as team memory (fire-and-forget) - const completedRun = await supabase.from('team_runs').select('output').eq('id', run.id).single(); - const runOutput = (completedRun.data as { output: string | null } | null)?.output; + // Use finalOutput we tracked inline, or fall back to re-fetching from DB + let runOutput = finalOutput; + if (!runOutput) { + const completedRun = await supabase.from('team_runs').select('output').eq('id', run.id).single(); + runOutput = (completedRun.data as { output: string | null } | null)?.output ?? null; + } + + console.log(`[Orchestrator] Run ${run.id} output length: ${runOutput?.length ?? 0} chars`); + if (runOutput) { void storeTeamMemory(run.team_id, run.id, run.workspace_id, runOutput); } // Fire team_run.completed webhook (fire-and-forget) void dispatchTeamRunWebhooks( - { id: run.id, team_id: run.team_id, workspace_id: run.workspace_id, status: 'completed', input_task: run.input_task, output: runOutput ?? undefined }, + { id: run.id, team_id: run.team_id, workspace_id: run.workspace_id, status: 'completed', input_task: run.input_task, output: runOutput }, teamData.name, 'team_run.completed', teamData.output_route_ids, @@ -529,8 +543,9 @@ async function executeToolCall( case 'final_answer': { const output = args.output as string; + console.log(`[Orchestrator] final_answer received, output length: ${output?.length ?? 0} chars`); await finalizeRun(run.id, output, 0, 0); // tokens/cost already tracked - return { result: 'Final answer submitted.', tokensUsed: 0, costUsed: 0, isDone: true }; + return { result: output, tokensUsed: 0, costUsed: 0, isDone: true }; } default: @@ -675,7 +690,7 @@ async function finalizeRun( const current = currentResponse.data as { tokens_total: number; cost_estimate_usd: number } | null; - await supabase + const { error } = await supabase .from('team_runs') .update({ status: 'completed', @@ -685,4 +700,10 @@ async function finalizeRun( completed_at: new Date().toISOString(), }) .eq('id', runId); + + if (error) { + console.error(`[Orchestrator] finalizeRun failed for ${runId}:`, error.message); + } else { + console.log(`[Orchestrator] finalizeRun saved output for ${runId} (${output.length} chars)`); + } }