Skip to content

Commit b0f8259

Browse files
eureka0928claude
andcommitted
fix: propagate model errors from all backend code paths to SSE stream
- Add Action.error and ActionErrorData to queue system so workforce background tasks can push errors to the SSE stream - Fix "simple answer" error handlers to emit SSE error events instead of silently swallowing exceptions - Add PUT fallback when PATCH /invalidate endpoint is not deployed - Store preferredProvider reference for invalidation fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0f6eb68 commit b0f8259

4 files changed

Lines changed: 87 additions & 13 deletions

File tree

backend/app/service/chat_service.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -592,13 +592,14 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
592592
)
593593
except Exception as e:
594594
logger.error(f"Error generating simple answer: {e}")
595+
message, error_code, _ = (
596+
normalize_error_to_openai_format(e)
597+
)
595598
yield sse_json(
596-
"wait_confirm",
599+
"error",
597600
{
598-
"content": "I encountered an error"
599-
" while processing "
600-
"your question.",
601-
"question": question,
601+
"message": message,
602+
"error_code": error_code,
602603
},
603604
)
604605

@@ -1298,13 +1299,14 @@ async def run_decomposition():
12981299
"Error generating simple "
12991300
f"answer in multi-turn: {e}"
13001301
)
1302+
message, error_code, _ = (
1303+
normalize_error_to_openai_format(e)
1304+
)
13011305
yield sse_json(
1302-
"wait_confirm",
1306+
"error",
13031307
{
1304-
"content": "I encountered an error "
1305-
"while processing your "
1306-
"question.",
1307-
"question": new_task_content,
1308+
"message": message,
1309+
"error_code": error_code,
13081310
},
13091311
)
13101312

@@ -1637,6 +1639,21 @@ def on_stream_text(chunk):
16371639
},
16381640
)
16391641

1642+
elif item.action == Action.error:
1643+
logger.error(
1644+
"[LIFECYCLE] ERROR action received "
1645+
f"for project {options.project_id}, "
1646+
f"task {options.task_id}: "
1647+
f"{item.data.get('message', 'Unknown error')}"
1648+
)
1649+
yield sse_json(
1650+
"error",
1651+
{
1652+
"message": item.data.get("message", "Unknown error"),
1653+
"error_code": item.data.get("error_code"),
1654+
},
1655+
)
1656+
16401657
elif item.action == Action.end:
16411658
logger.info("=" * 80)
16421659
logger.info(

backend/app/service/task.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class Action(str, Enum):
6565
resume = "resume" # user -> backend user take control
6666
new_agent = "new_agent" # user -> backend
6767
budget_not_enough = "budget_not_enough" # backend -> user
68+
error = "error" # backend -> user (model/runtime error)
6869
add_task = "add_task" # user -> backend
6970
remove_task = "remove_task" # user -> backend
7071
skip_task = "skip_task" # user -> backend
@@ -255,6 +256,11 @@ class ActionNewAgent(BaseModel):
255256
custom_model_config: "AgentModelConfig | None" = None
256257

257258

259+
class ActionErrorData(BaseModel):
260+
action: Literal[Action.error] = Action.error
261+
data: dict
262+
263+
258264
class ActionBudgetNotEnough(BaseModel):
259265
action: Literal[Action.budget_not_enough] = Action.budget_not_enough
260266

@@ -303,6 +309,7 @@ class ActionSkipTaskData(BaseModel):
303309
| ActionTakeControl
304310
| ActionNewAgent
305311
| ActionBudgetNotEnough
312+
| ActionErrorData
306313
| ActionAddTaskData
307314
| ActionRemoveTaskData
308315
| ActionSkipTaskData

backend/app/utils/workforce.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@
4242

4343
from app.agent.listen_chat_agent import ListenChatAgent
4444
from app.component import code
45+
from app.component.error_format import normalize_error_to_openai_format
4546
from app.exception.exception import UserException
4647
from app.service.task import (
4748
Action,
4849
ActionAssignTaskData,
4950
ActionEndData,
51+
ActionErrorData,
5052
ActionTaskStateData,
5153
ActionTimeoutData,
5254
get_camel_task,
@@ -260,6 +262,38 @@ async def eigent_start(self, subtasks: list[Task]):
260262
exc_info=True,
261263
)
262264
self._state = WorkforceState.STOPPED
265+
# Push error event to SSE queue so frontend receives notification
266+
try:
267+
task_lock = get_task_lock(self.api_task_id)
268+
logger.info(
269+
f"[WF-LIFECYCLE] Pushing error to SSE queue, "
270+
f"task_lock={'found' if task_lock else 'None'}"
271+
)
272+
if task_lock is not None:
273+
message, error_code, _ = normalize_error_to_openai_format(
274+
e
275+
)
276+
logger.info(
277+
f"[WF-LIFECYCLE] Error normalized: "
278+
f"error_code={error_code}, message={message[:100]}"
279+
)
280+
await task_lock.put_queue(
281+
ActionErrorData(
282+
data={
283+
"message": message,
284+
"error_code": error_code,
285+
},
286+
)
287+
)
288+
logger.info(
289+
"[WF-LIFECYCLE] Error event pushed to SSE queue"
290+
)
291+
except Exception as queue_err:
292+
logger.error(
293+
"[WF-LIFECYCLE] Failed to push error to SSE queue: "
294+
f"{queue_err}",
295+
exc_info=True,
296+
)
263297
raise
264298
finally:
265299
if self._state != WorkforceState.STOPPED:

src/store/chatStore.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
528528
extra_params: {},
529529
};
530530
let preferredProviderId: number | null = null;
531+
let preferredProvider: any = null;
531532
if (modelType === 'custom' || modelType === 'local') {
532533
const res = await proxyFetchGet('/api/providers', {
533534
prefer: true,
@@ -543,6 +544,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
543544
}
544545

545546
preferredProviderId = provider.id ?? null;
547+
preferredProvider = provider;
546548
apiModel = {
547549
api_key: provider.api_key,
548550
model_type: provider.model_type,
@@ -1903,9 +1905,23 @@ const chatStore = (initial?: Partial<ChatStore>) =>
19031905
if (errorCode === 'invalid_api_key' && preferredProviderId) {
19041906
proxyFetchPatch(
19051907
`/api/provider/${preferredProviderId}/invalidate`
1906-
).catch((err: unknown) =>
1907-
console.error('Failed to invalidate provider:', err)
1908-
);
1908+
).catch(() => {
1909+
// Fallback: PATCH endpoint may not be deployed yet,
1910+
// use PUT with full provider data to set is_vaild=1
1911+
if (preferredProvider) {
1912+
proxyFetchPut(`/api/provider/${preferredProviderId}`, {
1913+
provider_name: preferredProvider.provider_name,
1914+
model_type: preferredProvider.model_type,
1915+
api_key: preferredProvider.api_key,
1916+
endpoint_url: preferredProvider.endpoint_url || '',
1917+
encrypted_config: preferredProvider.encrypted_config,
1918+
is_vaild: 1,
1919+
prefer: preferredProvider.prefer ?? false,
1920+
}).catch((err: unknown) =>
1921+
console.error('Failed to invalidate provider:', err)
1922+
);
1923+
}
1924+
});
19091925
}
19101926

19111927
// Mark all incomplete tasks as failed

0 commit comments

Comments
 (0)