Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
25 changes: 21 additions & 4 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,16 @@ async function submit(formData?: FormData, skip?: boolean) {
const streamText = createStreamableValue<string>()
uiStream.update(<Spinner />)

/* removal 1:
while (
useSpecificAPI
? answer.length === 0
: answer.length === 0 && !errorOccurred
) {
useSpecificAPI
? answer.length === 0
: answer.length === 0 && !errorOccurred
) {
*/
// Split and stage producers
const abortController = new AbortController()
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

Remove or wire AbortController.

Declared but unused. Either pass its signal into a budget/timeout helper or remove it.

-    const abortController = new AbortController()
+    // If cancellation will be supported, pass AbortSignal into withBudget/agents; otherwise omit.
📝 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
const abortController = new AbortController()
// If cancellation will be supported, pass AbortSignal into withBudget/agents; otherwise omit.
🤖 Prompt for AI Agents
In app/actions.tsx around line 325, an AbortController is declared but never
used; either remove the unused AbortController declaration or wire it into the
async operation that needs cancellation by passing abortController.signal to the
fetch/budget/timeout helper (or storing it to call abort() where appropriate).
If you keep it, thread the signal into the request/timeouter function and ensure
you handle AbortError in the catch; otherwise delete the AbortController
declaration to eliminate the unused variable.

const stage1 = (async () => {
const { fullResponse, hasError, toolResponses } = await researcher(
currentSystemPrompt,
uiStream,
Expand All @@ -329,7 +334,9 @@ async function submit(formData?: FormData, skip?: boolean) {
answer = fullResponse
toolOutputs = toolResponses
errorOccurred = hasError
})()

/* removal 1:
if (toolOutputs.length > 0) {
toolOutputs.map(output => {
aiState.update({
Expand All @@ -348,6 +355,16 @@ async function submit(formData?: FormData, skip?: boolean) {
})
}
}
*/

// Stage 2: cheap producer - related suggestions
const stage2 = (async () => {
try {
await querySuggestor(uiStream, messages)
} catch {}
})()
Comment on lines +362 to +365
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

Don’t swallow stage2 errors silently.

Log at least a warning so failures aren’t invisible.

-      try {
-        await querySuggestor(uiStream, messages)
-      } catch {}
+      try {
+        await querySuggestor(uiStream, messages)
+      } catch (err) {
+        console.warn('querySuggestor failed:', err)
+      }
📝 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
try {
await querySuggestor(uiStream, messages)
} catch {}
})()
try {
await querySuggestor(uiStream, messages)
} catch (err) {
console.warn('querySuggestor failed:', err)
}
})()
🤖 Prompt for AI Agents
In app/actions.tsx around lines 362 to 365, the catch block after await
querySuggestor(uiStream, messages) is silently swallowing errors; change it to
catch the error and log at least a warning with the error details (e.g.,
processLogger.warn or console.warn with a contextual message and the error) so
stage2 failures are visible for debugging; preserve existing control flow after
logging.


Comment on lines +361 to +366
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Don’t run querySuggestor twice; capture stage2 result and reuse to avoid duplicate “Related” UI and extra API calls.

Stage2 already runs querySuggestor, but it’s called again later. Capture the stage2 result and only call again if stage2 failed.

-    const stage2 = (async () => {
-      try {
-        await querySuggestor(uiStream, messages)
-      } catch {}
-    })()
+    let relatedFromStage2: RelatedQueries | null = null
+    const stage2 = (async () => {
+      try {
+        relatedFromStage2 = await querySuggestor(uiStream, messages)
+      } catch (err) {
+        console.warn('querySuggestor failed (stage2):', err)
+      }
+    })()

     await Promise.allSettled([stage1, stage2])

-    if (!errorOccurred) {
-      const relatedQueries = await querySuggestor(uiStream, messages)
+    if (!errorOccurred) {
+      // If stage2 already produced related queries (and UI), reuse them; otherwise fetch once.
+      const relatedQueries = relatedFromStage2 ?? await querySuggestor(uiStream, messages)
       uiStream.append(
         <Section title="Follow-up">
           <FollowupPanel />
         </Section>
       )
@@
           {
             id: groupeId,
             role: 'assistant',
             content: JSON.stringify(relatedQueries),
             type: 'related'
           },

Also applies to: 393-427

🤖 Prompt for AI Agents
In app/actions.tsx around lines 361-366 (and similarly 393-427), the current
code immediately invokes an async IIFE that calls querySuggestor and then later
calls querySuggestor again, causing duplicate "Related" UI and extra API calls;
change the pattern to capture the stage2 promise (e.g., assign the invoked
promise to a variable) and reuse its result where querySuggestor is called
later: await the captured stage2 promise when you need its outcome, and only
call querySuggestor again as a fallback if that awaited promise rejected or
returned an explicit failure. Ensure you preserve existing try/catch semantics
around the initial invocation and the later consumption so duplicate requests
are avoided.

await Promise.allSettled([stage1, stage2])

if (useSpecificAPI && answer.length === 0) {
const modifiedMessages = aiState
Expand Down
14 changes: 11 additions & 3 deletions lib/agents/researcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ Analysis & Planning
})
})

// Remove the spinner
uiStream.update(null)
// removal 1: // Remove the spinner
// removal 1: // uiStream.update(null)
// Append the answer section immediately to avoid gating on first token
uiStream.append(answerSection)

Comment on lines +83 to 87
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

Clear the spinner before appending the answer section to prevent a stuck spinner.

The spinner is set in app/actions.tsx via uiStream.update(). Call update(null) here to remove it once streaming begins.

-  // Append the answer section immediately to avoid gating on first token
-  uiStream.append(answerSection)
+  // Replace spinner with the answer section once streaming starts
+  uiStream.update(null)
+  uiStream.append(answerSection)
📝 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
// removal 1: // Remove the spinner
// removal 1: // uiStream.update(null)
// Append the answer section immediately to avoid gating on first token
uiStream.append(answerSection)
// removal 1: // Remove the spinner
// removal 1: // uiStream.update(null)
// Replace spinner with the answer section once streaming starts
uiStream.update(null)
uiStream.append(answerSection)
🤖 Prompt for AI Agents
In lib/agents/researcher.tsx around lines 83 to 87, the spinner set earlier via
uiStream.update(<Spinner />) is not being cleared before appending the answer
section which can leave the spinner visible; call uiStream.update(null)
immediately before uiStream.append(answerSection) to remove the spinner, then
append the answerSection so streaming begins without a stuck spinner.

// Process the response
const toolCalls: ToolCallPart[] = []
Expand All @@ -90,12 +92,13 @@ Analysis & Planning
switch (delta.type) {
case 'text-delta':
if (delta.textDelta) {
/* removal 1:
// If the first text delta is available, add a UI section
if (fullResponse.length === 0 && delta.textDelta.length > 0) {
// Update the UI
uiStream.update(answerSection)
}

*/
fullResponse += delta.textDelta
streamText.update(fullResponse)
}
Expand All @@ -104,10 +107,13 @@ Analysis & Planning
toolCalls.push(delta)
break
case 'tool-result':
/* removal 1:
// Append the answer section if the specific model is not used
if (!useSpecificModel && toolResponses.length === 0 && delta.result) {
uiStream.append(answerSection)
}
*/
// Keep answer section already appended; just collect tool outputs
if (!delta.result) {
hasError = true
}
Expand All @@ -129,5 +135,7 @@ Analysis & Planning
messages.push({ role: 'tool', content: toolResponses })
}

// Mark the answer stream as done
streamText.done()
return { result, fullResponse, hasError, toolResponses }
}
12 changes: 10 additions & 2 deletions lib/agents/tools/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ export const searchTool = ({ uiStream, fullResponse }: ToolProps) => ({
query: string
max_results: number
search_depth: 'basic' | 'advanced'
}) => {

/* removal 1:
}) => {
*/

}, injectedStream?: ReturnType<typeof createStreamableValue<string>>) => {
let hasError = false
// Append the search section
const streamResults = createStreamableValue<string>()
/* removal 1:
const streamResults = createStreamableValue<string>()
*/
const streamResults = injectedStream ?? createStreamableValue<string>()
uiStream.append(<SearchSection result={streamResults.value} />)

// Tavily API requires a minimum of 5 characters in the query
Expand Down
63 changes: 63 additions & 0 deletions lib/utils/concurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export async function runWithConcurrencyLimit<T>(limit: number, tasks: Array<() => Promise<T>>): Promise<PromiseSettledResult<T>[]> {
if (limit <= 0) limit = 1;
const results: PromiseSettledResult<T>[] = [];
let index = 0;

async function worker() {
while (index < tasks.length) {
const current = index++;
const task = tasks[current];
try {
const value = await task();
results[current] = { status: 'fulfilled', value } as PromiseFulfilledResult<T>;
} catch (reason) {
results[current] = { status: 'rejected', reason } as PromiseRejectedResult;
}
}
}

const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => worker());
await Promise.all(workers);
return results;
}

export async function withBudget<T>(options: { maxMs: number; signal?: AbortSignal }, task: () => Promise<T>): Promise<T> {
const { maxMs, signal } = options;
let timeoutId: ReturnType<typeof setTimeout> | null = null;

const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error('budget_exceeded')), maxMs);
});

if (signal) {
if (signal.aborted) throw new Error('aborted');
signal.addEventListener('abort', () => {
if (timeoutId) clearTimeout(timeoutId);
}, { once: true });
}

try {
return await Promise.race([task(), timeoutPromise]);
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
Comment on lines +32 to +44
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

withBudget can hang on AbortSignal; reject on abort and clear timeout.

Currently abort clears the timeout but doesn’t reject, potentially hanging forever. Include an abortPromise in the race.

   if (signal) {
-    if (signal.aborted) throw new Error('aborted');
-    signal.addEventListener('abort', () => {
-      if (timeoutId) clearTimeout(timeoutId);
-    }, { once: true });
+    if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
+    // Ensure we reject promptly on abort and clear timeout
+    // Note: listeners are one-shot
+    var abortPromise = new Promise<never>((_, reject) => {
+      signal.addEventListener(
+        'abort',
+        () => {
+          if (timeoutId) clearTimeout(timeoutId);
+          reject(new DOMException('Aborted', 'AbortError'));
+        },
+        { once: true }
+      )
+    })
   }
 
   try {
-    return await Promise.race([task(), timeoutPromise]);
+    // Include abortPromise in the race if provided
+    return await Promise.race(
+      typeof abortPromise !== 'undefined'
+        ? [task(), timeoutPromise, abortPromise]
+        : [task(), timeoutPromise]
+    );
   } finally {
     if (timeoutId) clearTimeout(timeoutId);
   }
📝 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
if (signal) {
if (signal.aborted) throw new Error('aborted');
signal.addEventListener('abort', () => {
if (timeoutId) clearTimeout(timeoutId);
}, { once: true });
}
try {
return await Promise.race([task(), timeoutPromise]);
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
if (signal) {
if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
// Ensure we reject promptly on abort and clear timeout
// Note: listeners are one-shot
var abortPromise = new Promise<never>((_, reject) => {
signal.addEventListener(
'abort',
() => {
if (timeoutId) clearTimeout(timeoutId);
reject(new DOMException('Aborted', 'AbortError'));
},
{ once: true }
);
});
}
try {
// Include abortPromise in the race if provided
return await Promise.race(
typeof abortPromise !== 'undefined'
? [task(), timeoutPromise, abortPromise]
: [task(), timeoutPromise]
);
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
🤖 Prompt for AI Agents
In lib/utils/concurrency.ts around lines 32 to 44, the current abort handler
only clears the timeout and doesn't reject, which can cause withBudget to hang;
add an abortPromise that rejects when signal.aborted (or on 'abort' event) and
include it in the Promise.race alongside task() and timeoutPromise, ensure the
abort listener also clears the timeout and rejects with an appropriate
AbortError, and finally keep the existing finally cleanup to clear the timeout
and remove the abort listener so no handlers leak.


export async function tokenGate(start: () => number, threshold: number, fn: () => Promise<void>): Promise<void> {
if (start() >= threshold) {
await fn();
return;
}
// Poll lightly until threshold reached
await new Promise<void>((resolve) => {
const id = setInterval(async () => {
if (start() >= threshold) {
clearInterval(id);
resolve();
}
}, 50);
});
await fn();
}
Comment on lines +46 to +61
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

Optional: tokenGate can wait indefinitely; consider max wait or external signal.

To avoid unbounded waits, support a max wait or AbortSignal.

-export async function tokenGate(start: () => number, threshold: number, fn: () => Promise<void>): Promise<void> {
+export async function tokenGate(start: () => number, threshold: number, fn: () => Promise<void>, options?: { maxMs?: number; signal?: AbortSignal }): Promise<void> {
   if (start() >= threshold) {
     await fn();
     return;
   }
   // Poll lightly until threshold reached
-  await new Promise<void>((resolve) => {
+  await new Promise<void>((resolve, reject) => {
     const id = setInterval(async () => {
       if (start() >= threshold) {
         clearInterval(id);
         resolve();
       }
-    }, 50);
+    }, 50);
+    if (options?.signal) {
+      options.signal.addEventListener('abort', () => { clearInterval(id); reject(new DOMException('Aborted', 'AbortError')) }, { once: true })
+    }
+    if (options?.maxMs) {
+      setTimeout(() => { clearInterval(id); reject(new Error('token_gate_timeout')) }, options.maxMs)
+    }
   });
   await fn();
 }
📝 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
export async function tokenGate(start: () => number, threshold: number, fn: () => Promise<void>): Promise<void> {
if (start() >= threshold) {
await fn();
return;
}
// Poll lightly until threshold reached
await new Promise<void>((resolve) => {
const id = setInterval(async () => {
if (start() >= threshold) {
clearInterval(id);
resolve();
}
}, 50);
});
await fn();
}
export async function tokenGate(
start: () => number,
threshold: number,
fn: () => Promise<void>,
options?: { maxMs?: number; signal?: AbortSignal }
): Promise<void> {
if (start() >= threshold) {
await fn()
return
}
// Poll lightly until threshold reached
await new Promise<void>((resolve, reject) => {
const id = setInterval(async () => {
if (start() >= threshold) {
clearInterval(id)
resolve()
}
}, 50)
if (options?.signal) {
options.signal.addEventListener(
'abort',
() => {
clearInterval(id)
reject(new DOMException('Aborted', 'AbortError'))
},
{ once: true }
)
}
if (options?.maxMs) {
setTimeout(() => {
clearInterval(id)
reject(new Error('token_gate_timeout'))
}, options.maxMs)
}
})
await fn()
}
🤖 Prompt for AI Agents
In lib/utils/concurrency.ts around lines 46 to 61, tokenGate currently can block
forever waiting for the threshold; change its signature to accept an optional
maxWaitMs (number) and/or an AbortSignal, and update the wait logic to race the
interval check against a timeout and an abort signal: start the interval checker
as now, also start a timeout that clears the interval and rejects/returns if
maxWaitMs elapses (or treat as a resolved no-op depending on desired behavior),
and attach an abort listener that clears the interval and rejects immediately
when signalled; ensure all timers/listeners are cleaned up before calling fn or
returning so fn is only invoked when threshold reached and callers receive a
clear error/abort when waiting stops.