@@ -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