fix(api-server): return the events a failed invocation produced from /run - #749
fix(api-server): return the events a failed invocation produced from /run#749kalenkevich wants to merge 1 commit into
Conversation
AmaadMartin
left a comment
There was a problem hiding this comment.
Approve. The fix is correct: events is hoisted out of the try in both /run (adk_api_server.ts:910) and /api/reasoning_engine (:958), so the catch reports the events the failed invocation produced. I verified this against source at head 1d83811. The sibling bug is fixed too, and the test restores getAgentFile in finally. No type suppressions and no new public types. CI is green on ubuntu, macOS, and Windows.
AmaadMartin
left a comment
There was a problem hiding this comment.
Approve. The fix is correct and minimal: it hoists events out of the try so the catch can report what the invocation produced, and it keeps the 500 status and the unchanged success shape. I verified the change against the head blobs, confirmed the two existing 500 tests read only .status, and confirmed nodeInfo is a real Event field. Two optional nits below; neither blocks. All CI checks passed at review time.
| const nodeError = body?.events.find( | ||
| (e) => (e as Event & {isNodeError?: boolean}).isNodeError, | ||
| ); |
There was a problem hiding this comment.
Nit, optional. Use the exported isNodeErrorEvent guard instead of an inline cast.
const nodeError = body?.events.find(
(e) => (e as Event & {isNodeError?: boolean}).isNodeError,
);The repo ships this guard (core/src/common.ts, exported from @google/adk). It drops the cast and narrows nodeError to NodeErrorEvent:
const nodeError = body?.events.find(isNodeErrorEvent);| // A failing workflow emits a structured node-error event — carrying | ||
| // `nodeInfo.path`, `errorType`, `errorCode` and `attemptCount` — and | ||
| // only then rethrows. /run_sse has already written those to the wire by | ||
| // this point; /run buffers, so dropping them left the caller with a | ||
| // one-line string and no way to tell which node failed. |
There was a problem hiding this comment.
Nit, optional. Trim this comment. The useful why is the first idea: the workflow emits the node-error event, then rethrows, so events holds it here. The rest restates the bug history, which belongs in the commit message.
// The workflow emits the node-error event, then rethrows, so `events`
// already holds it here; /run must report them like /run_sse does.…/run `/run_sse` writes each event to the wire as it arrives, so a caller sees the structured node-error event — `nodeInfo.path`, `errorType`, `errorCode`, `attemptCount` — when a node throws. `/run` buffers into an `events` array declared inside the `try`, which is not even in scope in the `catch`, so the same invocation returned HTTP 500 and a one-line string. The caller had no way to tell which node failed. The events are always there to report: the workflow emits the node-error event and only then rethrows, and the channel is drained to completion before the rejection surfaces. Keeps the 500 rather than switching to 200: adk-python also fails the request, `/run`'s callers are scripts rather than the Dev UI (which uses `/run_sse`), and a failed invocation quietly returning 200 is worse than a 500 that carries the payload. The success shape is unchanged; the 500 body just gains an `events` key. `/api/reasoning_engine` had the same bug and gets the same treatment. Fixes #743
1d83811 to
dfbe14b
Compare
AmaadMartin
left a comment
There was a problem hiding this comment.
Approve. Re-reviewed at dfbe14b. The fix is correct: it hoists events out of the try in both /run (adk_api_server.ts:908) and /api/reasoning_engine (:949), so the 500 body reports the events. The success shapes are unchanged. My earlier comment nit is resolved. One optional nit stays open: the test still uses an inline cast, not the exported isNodeErrorEvent guard (adk_api_server_test.ts:695). CI was green at review time.
/run_ssewrites each event to the wire as it arrives, so a caller sees the structured node-error event — carryingnodeInfo.path,errorType,errorCodeandattemptCount— when a node throws./runreturned HTTP 500 and a one-line string for the same invocation, and the caller had no way to tell which node failed:Root cause
const events: Event[] = []was declared inside thetry(dev/src/server/adk_api_server.ts:909), so it was not even in scope in thecatchat:923. Hoisting it is the fix.The events are always there to report, and that is guaranteed rather than incidental:
Workflow.orchestrateemits the node-error event viactx.emit(workflow.ts:338) and only then rethrows (:341), andrunNodeAsInvocationdrains the channel to completion in itsfor awaitbeforeawait settlesurfaces the rejection. So the consumer always receives the error event first.Why 500 and not 200
/run_sseends with 200 because it has already flushed headers, so status parity with it is not really available. Keeping the 500:worker()collects the events, any non-SessionNotFoundErrorexception propagates to FastAPI's handler and the events are lost), so 200 would be a deliberate divergence./run— only/run_sse(dev/src/server/adk_api_client.ts:140, and confirmed in the prebuilt bundle)./run's clients are scripts and theadk api_serverAPI, for which a failed invocation silently returning 200 is worse than a 500 that carries the payload.The 200 success shape is unchanged (a bare
Event[]); only the 500 body gains aneventskey, so a client already parsing{error}still works./api/reasoning_engine(:956-975) had the identical bug and gets the same treatment.Tests
dev/test/server/adk_api_server_test.ts— a workflow whose first node succeeds and whose second throws; asserts 500, the first node's event, andnodeInfo.path === 'wf.second'on the node-error event.The
HttpClienttest helper discarded the response body on non-2xx, so no test could assert on an error body at all; it now carriesdata/textthrough. The three existing 500 assertions only readstatusand are unaffected (they also fail before any event is produced, so they stay green either way).Full
unit:devgreen (300),tsc --noEmitclean.Fixes #743