@@ -20,17 +20,13 @@ import {
2020} from '@codebuff/internal/openai-compatible/index'
2121import {
2222 streamText,
23- APICallError,
2423 generateText,
2524 generateObject,
2625 NoSuchToolError,
27- InvalidToolInputError ,
26+ APICallError ,
2827} from 'ai'
2928
3029import { WEBSITE_URL } from '../constants'
31- import { NetworkError, PaymentRequiredError, ErrorCodes } from '../errors'
32-
33- import type { ErrorCode } from '../errors'
3430import type { LanguageModelV2 } from '@ai-sdk/provider'
3531import type { OpenRouterProviderRoutingOptions } from '@codebuff/common/types/agent-template'
3632import type {
@@ -236,15 +232,21 @@ export async function* promptAiSdkStream(
236232 if (NoSuchToolError.isInstance(error) && 'spawn_agents' in tools) {
237233 // Also check for underscore variant (e.g., "file_picker" -> "file-picker")
238234 const toolNameWithHyphens = toolName.replace(/_/g, '-')
239-
235+
240236 const matchingAgentId = spawnableAgents.find((agentId) => {
241237 const withoutVersion = agentId.split('@')[0]
242238 const parts = withoutVersion.split('/')
243239 const agentName = parts[parts.length - 1]
244- return agentName === toolName || agentName === toolNameWithHyphens || agentId === toolName
240+ return (
241+ agentName === toolName ||
242+ agentName === toolNameWithHyphens ||
243+ agentId === toolName
244+ )
245245 })
246246 const isSpawnableAgent = matchingAgentId !== undefined
247- const isLocalAgent = toolName in localAgentTemplates || toolNameWithHyphens in localAgentTemplates
247+ const isLocalAgent =
248+ toolName in localAgentTemplates ||
249+ toolNameWithHyphens in localAgentTemplates
248250
249251 if (isSpawnableAgent || isLocalAgent) {
250252 // Transform agent tool call to spawn_agents
@@ -286,9 +288,12 @@ export async function* promptAiSdkStream(
286288 )
287289
288290 // Use the matching agent ID or corrected name with hyphens
289- const correctedAgentType = matchingAgentId
290- ?? (toolNameWithHyphens in localAgentTemplates ? toolNameWithHyphens : toolName)
291-
291+ const correctedAgentType =
292+ matchingAgentId ??
293+ (toolNameWithHyphens in localAgentTemplates
294+ ? toolNameWithHyphens
295+ : toolName)
296+
292297 const spawnAgentsInput = {
293298 agents: [
294299 {
@@ -345,15 +350,9 @@ export async function* promptAiSdkStream(
345350 }
346351 }
347352 if (chunkValue.type === 'error') {
348- logger.error(
349- {
350- chunk: { ...chunkValue, error: undefined },
351- error: getErrorObject(chunkValue.error),
352- model: params.model,
353- },
354- 'Error from AI SDK',
355- )
356-
353+ // Error chunks from fullStream are non-network errors (tool failures, model issues, etc.)
354+ // Network errors are thrown, not yielded as chunks.
355+ // Pass all error chunks back to the agent so it can see what went wrong and retry.
357356 const errorBody = APICallError.isInstance(chunkValue.error)
358357 ? chunkValue.error.responseBody
359358 : undefined
@@ -365,66 +364,20 @@ export async function* promptAiSdkStream(
365364 : JSON.stringify(chunkValue.error)
366365 const errorMessage = `Error from AI SDK (model ${params.model}): ${buildArray([mainErrorMessage, errorBody]).join('\n')}`
367366
368- // Determine error code from the error
369- let errorCode: ErrorCode = ErrorCodes.UNKNOWN_ERROR
370- let statusCode: number | undefined
371-
372- if (APICallError.isInstance(chunkValue.error)) {
373- statusCode = chunkValue.error.statusCode
374- if (statusCode) {
375- if (statusCode === 402) {
376- // Payment required - extract message from JSON response body
377- let paymentErrorMessage = mainErrorMessage
378- if (errorBody) {
379- try {
380- const parsed = JSON.parse(errorBody)
381- paymentErrorMessage = parsed.message || errorBody
382- } catch {
383- paymentErrorMessage = errorBody
384- }
385- }
386- throw new PaymentRequiredError(paymentErrorMessage)
387- } else if (statusCode === 503) {
388- errorCode = ErrorCodes.SERVICE_UNAVAILABLE
389- } else if (statusCode >= 500) {
390- errorCode = ErrorCodes.SERVER_ERROR
391- } else if (statusCode === 408 || statusCode === 429) {
392- errorCode = ErrorCodes.TIMEOUT
393- }
394- }
395- } else if (chunkValue.error instanceof Error) {
396- // Check error message for error type indicators (case-insensitive)
397- const msg = chunkValue.error.message.toLowerCase()
398- if (msg.includes('service unavailable') || msg.includes('503')) {
399- errorCode = ErrorCodes.SERVICE_UNAVAILABLE
400- } else if (
401- msg.includes('econnrefused') ||
402- msg.includes('connection refused')
403- ) {
404- errorCode = ErrorCodes.CONNECTION_REFUSED
405- } else if (msg.includes('enotfound') || msg.includes('dns')) {
406- errorCode = ErrorCodes.DNS_FAILURE
407- } else if (msg.includes('timeout')) {
408- errorCode = ErrorCodes.TIMEOUT
409- } else if (
410- msg.includes('server error') ||
411- msg.includes('500') ||
412- msg.includes('502') ||
413- msg.includes('504')
414- ) {
415- errorCode = ErrorCodes.SERVER_ERROR
416- } else if (msg.includes('network') || msg.includes('fetch failed')) {
417- errorCode = ErrorCodes.NETWORK_ERROR
418- }
419- }
420-
421- // Throw NetworkError so retry logic can handle it
422- throw new NetworkError(
423- errorMessage,
424- errorCode,
425- statusCode,
426- chunkValue.error,
367+ logger.warn(
368+ {
369+ chunk: { ...chunkValue, error: undefined },
370+ error: getErrorObject(chunkValue.error),
371+ model: params.model,
372+ },
373+ 'Error chunk from AI SDK stream - yielding to agent for retry',
427374 )
375+
376+ yield {
377+ type: 'error',
378+ message: errorMessage,
379+ }
380+ continue
428381 }
429382 if (chunkValue.type === 'reasoning-delta') {
430383 for (const provider of ['openrouter', 'codebuff'] as const) {
0 commit comments