feat(sdk): attachment block ingress, model capabilities, and the delivery chain - #5617
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe SDK now supports attachment content blocks, model input-modality metadata, structured agent failures, attachment-delivery stream events, and Vercel adapter projections. Python and runner wire-contract tests cover image attachment requests and multimodal result handling. ChangesMultimodal agent delivery
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant VercelAdapter
participant AgentStream
participant ConnectionResolver
participant ModelCatalog
Client->>VercelAdapter: submit file part with attachment metadata
VercelAdapter->>AgentStream: convert file to attachment block
ConnectionResolver->>ModelCatalog: resolve model input modalities
ModelCatalog-->>ConnectionResolver: return text and image modalities
AgentStream-->>VercelAdapter: emit attachment delivery or failure event
VercelAdapter-->>Client: emit structured data and standard stream frame
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
| resolved_code = AgentRunFailed.failure_code | ||
| resolved_text = _as_text(error_text) | ||
| yield { | ||
| "type": "data-agent-error", |
There was a problem hiding this comment.
The error code deliberately rides this separate data part, never the error frame itself: the pinned AI SDK validates the error chunk as a zod strictObject and an extra key aborts the whole stream with a parse failure (empirically verified during review). If you ever need more fields on failures, they go here, not on the frame.
| return _CLAUDE_CATALOG | ||
|
|
||
|
|
||
| def model_input_modalities( |
There was a problem hiding this comment.
This lookup is the one place the wire learns what a model can see, and its honesty rule is the load-bearing part: a miss returns None so the field is omitted and the runner reads unknown (workspace-only with a notice), never a guess. The dated-Claude fallback reads the same sourced fact from the Pi catalog under anthropic/; that is a second read of recorded data, not an inference.
|
@coderabbitai review |
✅ Action performedReview finished.
|
Railway Preview Environment
Updated at 2026-08-01T18:14:58.673Z |
bb7728c to
f4a014e
Compare
e760f6c to
42d83fe
Compare
f4a014e to
200c360
Compare
42d83fe to
df5321b
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: fd53e2b7-f116-46b7-b150-076a9a3c13c5
📒 Files selected for processing (26)
docs/design/agent-workflows/projects/agent-multi-modality/protocols/stage-1.mdsdks/python/agenta/sdk/agents/__init__.pysdks/python/agenta/sdk/agents/adapters/vercel/messages.pysdks/python/agenta/sdk/agents/adapters/vercel/stream.pysdks/python/agenta/sdk/agents/connections/models.pysdks/python/agenta/sdk/agents/connections/resolver.pysdks/python/agenta/sdk/agents/dtos.pysdks/python/agenta/sdk/agents/errors.pysdks/python/agenta/sdk/agents/model_catalog.pysdks/python/agenta/sdk/agents/platform/connections.pysdks/python/agenta/sdk/agents/utils/wire.pysdks/python/agenta/sdk/agents/wire_models.pysdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_messages_roundtrip.pysdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_stream_conformance.pysdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_stream_multimodality.pysdks/python/oss/tests/pytest/unit/agents/connections/test_model_catalog.pysdks/python/oss/tests/pytest/unit/agents/connections/test_models.pysdks/python/oss/tests/pytest/unit/agents/connections/test_resolver.pysdks/python/oss/tests/pytest/unit/agents/golden/run_request.attachment.jsonsdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.pysdks/python/oss/tests/pytest/unit/agents/test_dtos_content_blocks.pysdks/python/oss/tests/pytest/unit/agents/test_result_multimodality.pysdks/python/oss/tests/pytest/unit/agents/test_ui_messages.pysdks/python/oss/tests/pytest/unit/agents/test_wire_contract.pysdks/python/oss/tests/pytest/unit/agents/test_wire_models.pyservices/runner/tests/unit/wire-contract.test.ts
| resolved_text = _as_text(error_text) | ||
| yield { | ||
| "type": "data-agent-error", | ||
| "data": {"code": resolved_code, "errorText": resolved_text}, | ||
| } | ||
| yield {"type": "error", "errorText": resolved_text} |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Sanitize runner error text in the shared emission helper.
_error_parts converts error_text with _as_text only. The runner-error paths can therefore send stack frames, filesystem paths, or redaction candidates to both UI-facing error parts. Apply sanitize_runner_error here before emitting either part.
Proposed fix
- resolved_text = _as_text(error_text)
+ resolved_text = sanitize_runner_error(error_text)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| resolved_text = _as_text(error_text) | |
| yield { | |
| "type": "data-agent-error", | |
| "data": {"code": resolved_code, "errorText": resolved_text}, | |
| } | |
| yield {"type": "error", "errorText": resolved_text} | |
| resolved_text = sanitize_runner_error(error_text) | |
| yield { | |
| "type": "data-agent-error", | |
| "data": {"code": resolved_code, "errorText": resolved_text}, | |
| } | |
| yield {"type": "error", "errorText": resolved_text} |
200c360 to
4708ca5
Compare
df5321b to
d9f9f93
Compare
…very chain
The Vercel adapter maps a file part whose providerMetadata.agenta carries a
canonical-UUID attachmentId (and optionally size, since FileUIPart has no
top-level size field) into the neutral attachment block; the mapping is
ingress-only, because the neutral block carries no URL to rebuild a part from.
The resolved connection puts the model's input modalities on the wire as
modelCapabilities, sourced from the catalog at the resolver boundary; a lookup
miss omits the field so the runner reads unknown, and bare dated Claude ids
fall back to the same sourced fact in the Pi catalog. Run failures raise a
typed AgentRunFailed with a stable failure_code and sanitized message; the
Vercel stream keeps the error frame at exactly {type, errorText} (the pinned
AI SDK validates it strictly) and carries the code in a preceding
data-agent-error part; attachment_delivery events project as
data-attachment-delivery in both stream twins. A new shared golden pins the
attachment request shape in both contract suites; the two existing goldens are
byte-identical.
Claude-Session: https://claude.ai/code/session_01A1XQVjHPYJgVBHWSNUphtx
…arized base errors.py and the wire-contract suite build on the sandbox-slug rename and the Pi-builtins changes in the same regions, which is why those two lanes now sit below this one in the stack. Claude-Session: https://claude.ai/code/session_01A1XQVjHPYJgVBHWSNUphtx
4708ca5 to
356aa03
Compare
d9f9f93 to
caf7ab2
Compare
The runner understands attachment references and model capabilities since WP2, but nothing produces them: the SDK's Vercel adapter has no way to carry a server-issued attachment id, the wire has no model-modality fact, and a failed run reaches the browser as a bare string. This PR is work package 3 of 4 of the Stage 1 plan, the producer side, stacked on #5597.
Before: a
filepart is always inlined base64;modelCapabilitiesdoes not exist;result_from_wireraises a bareRuntimeErrordiscarding everything but the message; delivery outcomes never reach the stream.After: a
filepart whoseproviderMetadata.agenta.attachmentIdis a canonical UUID becomes a neutralattachmentblock (ingress-only: the neutral block carries no URL, so the reverse mapping cannot exist and the docstring says so);sizerides the same metadata envelope, because the pinned AI SDK'sFileUIParthas no top-level size field and validation would strip it; the resolved connection emitsmodelCapabilities.inputModalitiessourced from the model catalog at the resolver boundary, where a lookup miss omits the field entirely so the runner reads unknown, and bare dated Claude ids fall back to the same sourced fact in the Pi catalog; run failures raise a typedAgentRunFailedwith a stablefailure_codeand sanitized message;attachment_deliveryevents project onto the Vercel stream asdata-attachment-deliveryparts in both twins. A new shared golden pins the attachment request shape in both contract suites; the two existing goldens are byte-identical.The change was adversarially reviewed before this PR opened. The review's headline finding was empirically verified against the pinned AI SDK: the first implementation put the stable error code on the Vercel error frame, and the SDK validates that frame with a zod strictObject that rejects unknown keys, so every error-carrying stream would have aborted with an opaque parse failure instead of showing the sanitized message. The code now travels in a
data-agent-errorpart emitted before the standard two-key error frame. The review also settled the plan's open key-space question (the Pi path resolves for the common case; the dated-Claude miss is closed by the cross-catalog fallback) and exposed one semantics defect at the WP2 seam, fixed on the WP2 branch below: the catalogs only ever enumerate text and image, so the runner's gate treating absence as "unsupported" asserted a false negative for documents; absence now reads as unknown.Implicit decisions and their tradeoffs
agent_run_failed,runner_error,no_output, or the exception's ownfailure_code): the deployed runner result carries onlyerror?: string, so there was nothing to mirror. The attribute isfailure_code, notcode, because the errors module already usescodefor an integer HTTP status.attachment_deliveryparses through the generic event path rather than a dedicated branch: the generic path preserves the full payload, and a special case would only duplicate it. Regression coverage pins it.modalitiesfield feeds the runtime delivery gate through the connection resolver, though the catalog itself still gates nothing.Forced routes to double-check
QA
704 Python agent tests and 1,377 runner tests pass; ruff clean; both existing goldens byte-identical (sha256-verified before and after). End-to-end live QA (real references flowing browser to model) runs with WP4 on the dev stack; this package alone is dormant until the front end produces references, which is the reader-first rollout working as designed.
https://claude.ai/code/session_01A1XQVjHPYJgVBHWSNUphtx