Skip to content

fix(execute): propagate agent 4xx in async lane instead of blanket 502 - #945

Merged
santoshkumarradha merged 1 commit into
Agent-Field:mainfrom
7vignesh:fix/862-async-502-for-client-errors
Aug 23, 2026
Merged

santoshkumarradha merged 1 commit into
Agent-Field:mainfrom
7vignesh:fix/862-async-502-for-client-errors

Conversation

@7vignesh

Copy link
Copy Markdown
Contributor

Summary

Fixes #862. The async-completion branch in handleSync hardcoded HTTP 502 for all failed executions, making client-input rejections (e.g. 422 from input validation) indistinguishable from upstream outages. A reasoner validating its own input would return invalid_input: ... but the caller saw 502 Bad Gateway — sending operators to check pods instead of the request.

Root cause: The callError.statusCode is lost once the execution is persisted; the async lane (which reads the execution from DB after the agent calls back) had no way to recover the original HTTP status.

Fix (two-pronged):

Control plane

  • Add error_status_code field to executionStatusUpdateRequest so the SDK can forward the HTTP status in its failure callback
  • When error_status_code is 4xx, encode it in StatusReason as agent_client_error:<code> for persistence
  • Add httpStatusForFailedExecution() helper that resolves the HTTP status from:
    1. StatusReason — encoded client errors and known server-side categories (agent_timeout→504, target_not_found→404, etc.)
    2. ErrorMessage — parses "agent error (NNN):" pattern as fallback for existing executions
    3. Default: 502
  • Replace hardcoded 502 in the async-completion branch with the helper

Python SDK

  • In _execute_async_with_callback failure path, propagate error_status_code from exception.status_code / exception.code when the value is a valid HTTP status (400–599)

Backward-compatible: SDKs that don't send error_status_code get existing behavior (502 default). SDKs that do send it get correct 4xx propagation immediately.

Type of change

  • Bug fix

Test plan

  • cd control-plane && go test ./internal/handlers/ -run TestHttpStatusForFailedExecution -v (12 cases: encoded 4xx, category mapping, error message parsing, defaults)
  • cd control-plane && go test ./internal/handlers/ -run TestUpdateExecutionStatusHandler_ErrorStatusCode -v
  • cd control-plane && go test ./internal/handlers/ -run TestExecuteHandler_AgentError -v (existing 500→502 preserved)
  • cd control-plane && go test ./internal/handlers/ -timeout 120s (full suite)
  • cd control-plane && go vet ./internal/handlers/...
  • cd control-plane && golangci-lint run ./internal/handlers/ (no new warnings)
  • cd sdk/python && python -m pytest tests/test_usage_transport.py (async callback path)
  • python -c "import agentfield.agent" (syntax OK)

Test coverage

  • I ran tests for the surface(s) I changed locally.
  • New code paths are covered by tests in this PR (no bare additions).
  • If I removed code, I updated coverage-baseline.json in this PR only if the removal caused a legitimate regression and I called it out in the summary above.
  • The coverage gate check is green in CI before requesting review.

Checklist

Related issues / PRs

Fixes #862

Note: TestCallAgent_ErrorResponse is a pre-existing flaky test (timing-sensitive elapsed > 0 assertion) — confirmed by running it with -count=5 on both main and this branch.

@7vignesh
7vignesh requested review from a team and AbirAbbas as code owners August 22, 2026 21:18
Agent-Field#862)

The async-completion branch in handleSync hardcoded HTTP 502 for all
failed executions, making client-input rejections (e.g. 422)
indistinguishable from upstream outages. The sync lane correctly
propagated the agent's 4xx via writeExecutionError, but the async
lane — which reads the execution from DB after the agent calls back —
had no way to recover the original HTTP status.

Root cause: the callError.statusCode is lost once the execution is
persisted; the async lane only sees the stored record.

Fix (two-pronged):

Control plane:
- Add error_status_code field to executionStatusUpdateRequest so the
  SDK can forward the HTTP status in its failure callback.
- When error_status_code is 4xx, encode it in StatusReason as
  "agent_client_error:<code>" for persistence.
- Add httpStatusForFailedExecution() helper that resolves the HTTP
  status from StatusReason (encoded client errors, known categories)
  and ErrorMessage ("agent error (NNN):" pattern fallback).
- Replace hardcoded 502 in the async-completion branch with the helper.

Python SDK:
- In _execute_async_with_callback failure path, propagate
  error_status_code from exception.status_code / exception.code when
  the value is a valid HTTP status (400-599).

Backward-compatible: SDKs that don't send error_status_code continue
to get the existing behavior (502 default). SDKs that do send it get
correct 4xx propagation immediately.
@7vignesh
7vignesh force-pushed the fix/862-async-502-for-client-errors branch from 5e04137 to 4b44d05 Compare August 22, 2026 21:25
@github-actions

Copy link
Copy Markdown
Contributor

Performance

SDK Memory Δ Latency Δ Tests Status
Python 9.0 KB - 0.33 µs -6% ✓ ✓

✓ No regressions detected

@github-actions

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 87.20% 87.40% ↓ -0.20 pp 🟡
sdk-go 92.90% 92.00% ↑ +0.90 pp 🟢
sdk-python 94.20% 93.73% ↑ +0.47 pp 🟢
sdk-typescript 91.39% 90.42% ↑ +0.97 pp 🟢
web-ui 84.76% 84.79% ↓ -0.03 pp 🟡
aggregate 85.66% 85.75% ↓ -0.09 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 40 92.00% ✅
sdk-go 0 — ➖ no changes
sdk-python 0 — ➖ no changes
sdk-typescript 0 — ➖ no changes
web-ui 0 — ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

@santoshkumarradha santoshkumarradha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated the control-plane and Python surfaces locally. The status propagation fix and regression coverage look good to me.

@santoshkumarradha
santoshkumarradha added this pull request to the merge queue Aug 23, 2026
Merged via the queue into Agent-Field:main with commit ad79ab5 Aug 23, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

execute: async lane returns 502 for a client-input rejection; sync lane returns the agent's 4xx

2 participants