Skip to content

Commit f5da460

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 18b8ecd commit f5da460

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
@@ -554,13 +554,14 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
554554
)
555555
except Exception as e:
556556
logger.error(f"Error generating simple answer: {e}")
557+
message, error_code, _ = (
558+
normalize_error_to_openai_format(e)
559+
)
557560
yield sse_json(
558-
"wait_confirm",
561+
"error",
559562
{
560-
"content": "I encountered an error"
561-
" while processing "
562-
"your question.",
563-
"question": question,
563+
"message": message,
564+
"error_code": error_code,
564565
},
565566
)
566567

@@ -1260,13 +1261,14 @@ async def run_decomposition():
12601261
"Error generating simple "
12611262
f"answer in multi-turn: {e}"
12621263
)
1264+
message, error_code, _ = (
1265+
normalize_error_to_openai_format(e)
1266+
)
12631267
yield sse_json(
1264-
"wait_confirm",
1268+
"error",
12651269
{
1266-
"content": "I encountered an error "
1267-
"while processing your "
1268-
"question.",
1269-
"question": new_task_content,
1270+
"message": message,
1271+
"error_code": error_code,
12701272
},
12711273
)
12721274

@@ -1599,6 +1601,21 @@ def on_stream_text(chunk):
15991601
},
16001602
)
16011603

1604+
elif item.action == Action.error:
1605+
logger.error(
1606+
"[LIFECYCLE] ERROR action received "
1607+
f"for project {options.project_id}, "
1608+
f"task {options.task_id}: "
1609+
f"{item.data.get('message', 'Unknown error')}"
1610+
)
1611+
yield sse_json(
1612+
"error",
1613+
{
1614+
"message": item.data.get("message", "Unknown error"),
1615+
"error_code": item.data.get("error_code"),
1616+
},
1617+
)
1618+
16021619
elif item.action == Action.end:
16031620
logger.info("=" * 80)
16041621
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
@@ -621,6 +621,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
621621
extra_params: {},
622622
};
623623
let preferredProviderId: number | null = null;
624+
let preferredProvider: any = null;
624625
if (modelType === 'custom' || modelType === 'local') {
625626
const res = await proxyFetchGet('/api/providers', {
626627
prefer: true,
@@ -636,6 +637,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
636637
}
637638

638639
preferredProviderId = provider.id ?? null;
640+
preferredProvider = provider;
639641
apiModel = {
640642
api_key: provider.api_key,
641643
model_type: provider.model_type,
@@ -2056,9 +2058,23 @@ const chatStore = (initial?: Partial<ChatStore>) =>
20562058
if (errorCode === 'invalid_api_key' && preferredProviderId) {
20572059
proxyFetchPatch(
20582060
`/api/provider/${preferredProviderId}/invalidate`
2059-
).catch((err: unknown) =>
2060-
console.error('Failed to invalidate provider:', err)
2061-
);
2061+
).catch(() => {
2062+
// Fallback: PATCH endpoint may not be deployed yet,
2063+
// use PUT with full provider data to set is_vaild=1
2064+
if (preferredProvider) {
2065+
proxyFetchPut(`/api/provider/${preferredProviderId}`, {
2066+
provider_name: preferredProvider.provider_name,
2067+
model_type: preferredProvider.model_type,
2068+
api_key: preferredProvider.api_key,
2069+
endpoint_url: preferredProvider.endpoint_url || '',
2070+
encrypted_config: preferredProvider.encrypted_config,
2071+
is_vaild: 1,
2072+
prefer: preferredProvider.prefer ?? false,
2073+
}).catch((err: unknown) =>
2074+
console.error('Failed to invalidate provider:', err)
2075+
);
2076+
}
2077+
});
20622078
}
20632079

20642080
// Mark all incomplete tasks as failed

0 commit comments

Comments
 (0)