WayFlow `user_envelope` contract (Phase 348 / V5.1-09)
This page closes the v5.0 Phase 332 deferral: how WayFlow’s agent runtime
should forward HITL resume payloads that carry artifact attachments so
cinatra can de-envelope them on the receiving side and the LLM sees only
the plain text field.
The cinatra side of the contract has been complete since v5.0 Phase
329c (the parseUserEnvelope parser at
src/app/api/llm-bridge/user-envelope.ts). Phase 348 ratifies it as the
v5.1 contract and pins the back-compat invariant with regression tests
under that label.
The WayFlow side is a Python runtime change handled by the operator during the v5.1 cutover — out-of-codebase here.
Why this contract exists
Section titled “Why this contract exists”A2A messages are text-only by design. The chat / WayFlow round-trip that
resumes a paused agent run carries the user’s structured-form submission
as a JSON-stringified payload inside the parts[].text slot. v5.0 added
artifact attachments to that submission shape:
// userResponse (resume payload, JSON-stringified into A2A parts[].text):{ "text": "Approved with edits to draft #4.", "attachments": [ { "artifactId": "art_…", "representationRevisionId": "rep_…", "digest": "sha256:…", "mime": "application/pdf", "originKind": "upload" } ]}A2A is text-only, so this envelope must ride inside the text payload —
the model itself should still see only "Approved with edits to draft #4.",
not the whole envelope. The bridge route at /api/llm-bridge is where the
de-enveloping happens.
The opt-in flag
Section titled “The opt-in flag”The receiving bridge opts into envelope parsing via body.user_envelope:
body.user_envelope | bridge behavior |
|---|---|
true | parse body.user as {text, attachments?}; LLM sees only text |
false or absent | byte-identical pass-through of body.user — the legacy invariant |
The opt-in is the safety latch: any old caller that hasn’t been updated (and any malicious-looking JSON-shaped string the user might type) remains untouched.
Python operator step (post-merge)
Section titled “Python operator step (post-merge)”The WayFlow agent runtime (agent_loader.py, out-of-codebase here) must
forward body.user_envelope=true on the bridge call when the upstream
resume payload itself parses as the envelope shape. This is the one-line
opt-in:
# agent_loader.py (WayFlow side; pseudocode)resume_text = sendTask_part.text # the JSON-stringified envelopebridge_body = { "user": resume_text, "user_envelope": True, # ← Phase 348 opt-in # …other bridge fields}Until WayFlow is updated, cinatra MUST behave byte-identically to the
v5.0 legacy path: body.user_envelope absent → body.user flows
through verbatim, attachments must come from the top-level
body.attachments slot (the only existing v5.0 path).
Cinatra contract (already shipped)
Section titled “Cinatra contract (already shipped)”| Surface | What it guarantees |
|---|---|
src/app/api/llm-bridge/user-envelope.ts | parseUserEnvelope(rawUser, enabled, topLevelAttachments?) — opt-in JSON parse + merged cap 20 |
src/app/api/llm-bridge/route.ts | calls parseUserEnvelope with enabled = body.user_envelope === true |
packages/agents/src/review-task-actions.ts (lines ~270–295) | the comment block documenting the contract end-to-end |
Back-compat regression (Phase 348)
Section titled “Back-compat regression (Phase 348)”Regression-pinned at
src/app/api/llm-bridge/__tests__/user-envelope.test.ts (legacy-invariant
cases ride under the explicit Phase 348 label). The invariant is:
An A2A resume payload arriving with
user_envelopeabsent (= every WayFlow runtime that has NOT taken the Phase 348 Python step) MUST reach the model byte-identical to the v5.0 path. Any divergence is a regression, NOT a Phase 348 win.
Failure modes (and the parser’s fail-closed posture)
Section titled “Failure modes (and the parser’s fail-closed posture)”parseUserEnvelope(_, true, _) is strict-schema. When the caller has set
body.user_envelope=true and body.user does not parse as the envelope
shape, the parser throws (UserEnvelopeParseError) — never silently
falls back to plain text. The bridge route translates that throw to a
400 so the WayFlow side can fix the payload rather than confusing the
model with a half-parsed envelope.
Test references
Section titled “Test references”- Back-compat invariant (every
enabled=falsecase):src/app/api/llm-bridge/__tests__/user-envelope.test.ts - End-to-end attachment wiring through the bridge:
src/app/api/llm-bridge/__tests__/attachments-wiring.test.ts