Skip to content

feat(sdk): advertise Vercel messages protocol headers#4769

Closed
mmabrouk wants to merge 1 commit into
feat/agent-load-session-store-portfrom
feat/vercel-messages-protocol-version
Closed

feat(sdk): advertise Vercel messages protocol headers#4769
mmabrouk wants to merge 1 commit into
feat/agent-load-session-store-portfrom
feat/vercel-messages-protocol-version

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Jun 19, 2026

Copy link
Copy Markdown
Member

This PR is part of a stack. Review bottom-up.

Each PR's diff is only its own delta. Merge from the bottom. This PR's base is #4768 (merge that first).

Context

The agent /messages endpoint speaks the Vercel UIMessage wire format, but the response never said so. A client had to assume the format and version out of band. This PR makes the server declare both on every response, so a client can read them and branch on what the server actually speaks.

Base branch: feat/agent-load-session-store-port. This is the Protocol Shell Hardening slice (#2) from docs/design/agent-workflows/pr-stack.md, which makes /messages reviewable as an HTTP contract.

What this changes

The endpoint now stamps two response headers:

  • x-ag-messages-format: vercel
  • x-ag-messages-version: v1

A new helper, set_vercel_message_protocol_headers, applies both with setdefault, so it never clobbers a header a path already set. It wraps every response the adapter returns:

  • /messages success JSON (batch response)
  • /messages success SSE (Vercel stream)
  • /messages 400 for an invalid session_id
  • /messages upstream error JSON (status code at or above 400)
  • /messages 406 not-acceptable, for both the stream and batch type mismatches
  • /messages exception path (handle_failure)
  • /load-session JSON

Before, a 400 looked like:

HTTP/1.1 400 Bad Request
content-type: application/json

After:

HTTP/1.1 400 Bad Request
content-type: application/json
x-ag-messages-format: vercel
x-ag-messages-version: v1

The header constants, the headers map, and the helper are also exported from vercel/__init__.py so callers and tests can reference them by name instead of hardcoding strings.

Key architectural decision to review

The decision to scrutinize is versioning the wire protocol through response headers rather than the response body. The server names the format (vercel) and a version (v1) on the envelope, so a client can detect both before it parses anything. The body stays a pure payload.

The tradeoff is that header stamping is opt-in per return path. Nothing in FastAPI forces a /messages response through the helper. Each return must wrap its response by hand. set_vercel_message_protocol_headers lives in routing.py and the call sites are spread across make_messages_endpoint and make_load_session_endpoint. A middleware would stamp every response unconditionally, but it would also touch responses from other routes and would need to know which routes are agent message routes. The per-return approach keeps the blast radius to this adapter at the cost of a contributor remembering to wrap each new exit.

How to review this PR

Read sdks/python/agenta/sdk/agents/adapters/vercel/routing.py first.

  1. Check the constants and the helper at lines 28 to 40. Confirm setdefault is intentional, so a path that needs a different format or version can override it.
  2. Walk every return in make_messages_endpoint (lines 84 to 146) and confirm each one passes through the helper. The 400, the upstream-error JSON, both not-acceptable branches, the stream, the success JSON, and the except all wrap.
  3. Confirm make_load_session_endpoint (lines 167 to 169) wraps its only return.
  4. Check the exports in vercel/__init__.py.

The likely regression is a new /messages return path that forgets the helper, so that one response ships without the headers and a client misreads the format. The not-acceptable and exception paths are the easiest to overlook, since they read like error plumbing rather than protocol surface.

Tests / notes

sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py adds a _assert_vercel_message_protocol check and asserts the headers on the success JSON, the echoed-session response, the SSE stream, the pre-stream 500, the invalid-session 400, and both /load-session paths (route and direct endpoint). The direct /load-session test asserts the literal header values, which pins the constants. No assertion yet covers the two not-acceptable (406) branches.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 20255a2c-bf6f-426e-9a8a-ee0180101d33

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vercel-messages-protocol-version

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot Bot added python Pull requests that update Python code SDK size:M This PR changes 30-99 lines, ignoring generated files. labels Jun 19, 2026
@mmabrouk

Copy link
Copy Markdown
Member Author

Reviewer guide: interesting code

  • sdks/python/agenta/sdk/agents/adapters/vercel/routing.py:28 — the protocol constants and the headers map that name the format (vercel) and version (v1).
  • sdks/python/agenta/sdk/agents/adapters/vercel/routing.py:36 — the set_vercel_message_protocol_headers helper; note setdefault, so it never overrides a header a path already set.
  • sdks/python/agenta/sdk/agents/adapters/vercel/routing.py:86 — every /messages return path now routes through the helper, including the 400, both 406 branches, and the except; this is where a forgotten wrap would regress.
  • sdks/python/agenta/sdk/agents/adapters/vercel/routing.py:167 — the /load-session return stamps the same headers, so both endpoints advertise one identity.
  • sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py:72_assert_vercel_message_protocol and its call sites cover JSON, SSE, the pre-stream 500, the 400, and both load-session paths.

def set_vercel_message_protocol_headers(response: Response) -> Response:
"""Stamp the default agent ``/messages`` protocol identity on an HTTP response."""
for key, value in VERCEL_MESSAGE_PROTOCOL_HEADERS.items():
response.headers.setdefault(key, value)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

setdefault is the load-bearing choice here: it lets a future path stamp a different format/version (or a non-Vercel adapter reuse this helper) without this overwriting it. Worth a one-line comment so nobody 'fixes' it to a plain assignment.


except Exception as exception:
return await handle_failure(exception)
return set_vercel_message_protocol_headers(await handle_failure(exception))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The exception path is the easiest exit to miss: handle_failure already builds the response, so wrapping it here is the only way these headers reach an error reply. A new early-return added above this except would silently ship without the protocol identity.

response = await endpoint(None, LoadSessionRequest(session_id="sess_abc"))

assert response.status_code == 200
assert response.headers["x-ag-messages-format"] == VERCEL_MESSAGE_PROTOCOL

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good that the direct load-session test asserts the literal header strings rather than the constants. That pins the wire values, so a rename of the constant can't silently change the contract a client depends on.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
sdks/python/agenta/sdk/agents/adapters/vercel/routing.py (1)

134-134: ⚡ Quick win

Use the exported protocol constant instead of a string literal.

Line 134 hardcodes "vercel" while the protocol identity is already centralized in VERCEL_MESSAGE_PROTOCOL. Reusing the constant avoids silent drift between stream behavior and stamped headers if the protocol token ever changes.

Suggested patch
-                return set_vercel_message_protocol_headers(
-                    make_stream_response(response, "vercel")
-                )
+                return set_vercel_message_protocol_headers(
+                    make_stream_response(response, VERCEL_MESSAGE_PROTOCOL)
+                )
sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py (1)

244-256: ⚡ Quick win

Add explicit 406-path header tests.

The suite now covers 200/400/500 and /load-session, but the two not-acceptable branches (stream requested + batch returned, and JSON requested + stream returned) still aren’t asserted for protocol headers. Adding both tests would close the main regression gap for this change.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a6c349da-9433-4508-b735-b097fe85d658

📥 Commits

Reviewing files that changed from the base of the PR and between dca2abd and f48d7a2.

📒 Files selected for processing (3)
  • sdks/python/agenta/sdk/agents/adapters/vercel/__init__.py
  • sdks/python/agenta/sdk/agents/adapters/vercel/routing.py
  • sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-06-19T16:30:17.099Z

@mmabrouk

Copy link
Copy Markdown
Member Author

Superseded. Replacing the path-based stack with PRs sliced by functional area showing final code only, so reviewers don't comment on intermediate scaffolding that a later PR rewrites. See the new set.

@mmabrouk mmabrouk closed this Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Pull requests that update Python code SDK size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant