Skip to content

fix(responses): project collaboration through a plaintext shim - #273

Draft
Menci wants to merge 49 commits into
mainfrom
responses-multi-agent
Draft

fix(responses): project collaboration through a plaintext shim#273
Menci wants to merge 49 commits into
mainfrom
responses-multi-agent

Conversation

@Menci

@Menci Menci commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Codex exposes a plaintext collaboration dispatch path to non-OpenAI providers, but Copilot reserves the exact collaboration namespace and requires its encrypted schema. Forwarding that client contract unchanged therefore makes Copilot produce opaque arguments that another upstream cannot consume.

This PR makes collaboration a reversible Responses source-protocol membrane:

  • The client continues to see the ordinary Codex contract: namespace collaboration, encrypted message schemas, and encrypted_function_args: [] on plaintext message actions.
  • The selected upstream sees a collision-free ordinary namespace (collaboration_2, then _3, …) whose message schemas omit encrypted: true.
  • Function calls, histories, deferred tool inventories, snapshots, and namespace-qualified tool choices are projected in both directions.
  • History-only collaboration calls engage the same membrane, including Codex replay that omits the empty plaintext marker for providers not named exactly OpenAI.
  • Native Responses and representable Messages targets are supported. Chat Completions and Messages shapes that cannot preserve the namespace contract are removed during candidate evaluation.

The collaboration shim wraps the complete server-tool loop, so every internal turn stays in one plaintext alias domain and the client stream is restored exactly once. Context mutation remains one-way, matching the interceptor contract.

Collaboration arguments and agent_message content are no longer affinity carriers. The gateway does not inspect or patch inter-agent ciphertext and does not require a subagent turn to land on the same upstream as its parent. Existing structured encrypted tool-output and reasoning carriers remain scoped to their native Responses contracts.

The Copilot item-id membrane likewise treats plaintext agent messages as public items and no longer embeds provider-private affinity into their content.

Evidence

  • Live Copilot and GHE probes reject removing encrypted: true from the reserved collaboration schema, while ordinary collaboration_2 / collaboration_3 namespaces accept the same plaintext arguments and replay.
  • Codex dispatches a collaboration message locally when the returned call uses namespace collaboration and an explicit empty encrypted_function_args list.
  • Codex removes that empty marker from replay for providers whose configured name is not exactly OpenAI; the ingress projection accepts this plaintext replay form while rejecting explicit null or non-empty encrypted markers.

The previous hosted multi_agent implementation on this PR was useful design archaeology for action vocabulary and collision handling, but it was not the protocol authority. Its scheduler, checkpoint format, and response.inject server implementation are not part of the resulting design.

Verification

  • pnpm run typecheck
  • pnpm run lint
  • 480 focused collaboration, server-tool, affinity, translation, HTTP, serve, and Copilot item-id tests
  • Agent Setup installer verifier: 106 passed, 4 skipped
  • Full workspace: 5405 passed, 5 unrelated web tests fail because the test runtime exposes a malformed window.localStorage (getItem is not a function)

@Menci
Menci marked this pull request as draft July 28, 2026 11:45
@Menci

Menci commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

Holding this in draft: nothing has exercised it yet.

The audit that shaped this PR established that outside the openai org nobody has implemented the server side — every third-party project is either a client or a gateway that forwards to a real OpenAI upstream, and no server anywhere accepts response.inject. So there is no reference implementation to compare behaviour against, only the guide and the two reference SDK clients' source and tests.

Everything here is therefore derived from the published contract rather than from observed behaviour, and it has never been driven end to end by a real client. That is the gap to close before this ships: point agents-python or agents-js's experimental hosted multi-agent model at Floway and run a real workflow through it. GPT-5.6 is reachable through both Copilot and Azure, so the model is not the blocker — the upstreams simply do not serve the hosted capability, which is the premise for hosting it here.

The contract-derived parts are the ones worth the most scrutiny in review: the per-action argument schemas (only spawn_agent's are documented; the other five are transcribed from Codex CLI collaboration traffic, a different feature reusing the same vocabulary), and the eager serial scheduler, which deliberately diverges from the upstream's asynchronous one because there is no coordinator to run subagents in.

Menci added 28 commits August 5, 2026 18:51
Describe Codex encrypted function arguments and structured encrypted tool outputs in the Responses protocol. Reject encrypted output explicitly at translated target boundaries so opaque state cannot be flattened or dropped.
Authenticate collaboration arguments across their move into agent_message input and make encrypted inter-agent and tool-output state require its producing target. Centralize opaque Responses locations and keep streamed argument deltas, done events, item snapshots, and terminal resources coherent.
Accept the complete Responses tool-output union at the Messages translator boundary so encrypted content reaches its explicit unsupported-input guard.
Remove a refusal branch that the Responses function-output protocol cannot produce after the tool-output union gained its complete encrypted-content shape.
Keep opaque-location reconstruction type-safe across open agent content and structured tool outputs, adapt Copilot image compression to the complete output union, and narrow the round-trip fixture to a legal input status.
Widen the image-compression test accessor alongside the production Responses tool-output union.
Treat output items with encrypted collaboration arguments or structured encrypted output as carrier-capable first items, avoiding an unrelated synthetic prefix. Keep the plaintext collaboration fixture focused on its unchanged call.
Model mandatory upstream continuity separately from model-bound continuation state. Inter-agent and encrypted tool-output ciphertext can follow child model overrides within the producing upstream while rejecting every other upstream.
Document the complete opaque-location inventory, cross-shape Codex collaboration carrier, upstream-only requirements, coherent streaming rewrites, and explicit translation rejection for encrypted tool output. Align the architecture guide and candidate resolution contract.
Treat every absent, null, or non-empty encrypted_function_args marker as encrypted for collaboration calls; only the explicit empty list is plaintext. Cover the observed absent form and the declared non-empty form.
Make the opaque-location module the single owner of natural and synthetic Responses carrier domains so ingress and egress cannot drift.
Preserve global event order while buffering encrypted collaboration arguments, reject malformed encrypted calls, accept pre-deploy agent-message carrier domains, require a native Responses candidate on the producing upstream, and mark Messages-translated collaboration calls explicitly plaintext.
Align the affinity and translation contracts with review repairs: encrypted tool state stays on a native Responses model at its producing upstream, and ingress accepts the prior agent-message carrier domain during deployment.
Attach pinned Codex references to the vendor-defined action subset shared by affinity and Messages translation.
Resolve buffered collaboration arguments from done items or terminal snapshots, preserve globally interleaved event order, and suppress unresolved ciphertext deltas on failed, errored, or truncated streams.
Resolve terminal collaboration calls by output index and required call_id, and reject malformed authoritative arguments before any buffered ciphertext delta reaches the client.
Order local affinity imports before parent modules and remove the superseded Messages translator type import.
Replace inter-agent ciphertext affinity with a request-scoped ordinary namespace membrane. Native Responses upstreams receive plaintext collaboration schemas and history; Codex receives the reserved collaboration identity plus encrypted_function_args:[] so child messages use input_text. Remove encrypted agent-message handling and argument buffering from affinity and Copilot item IDs.
Model namespace tools through the hosted-tool shape and keep nullable request tool arrays from leaking into response resources.
Remove inter-agent ciphertext affinity suites and cover the plaintext collaboration namespace membrane across request schemas, history, streamed arguments, done items, terminal resources, echoed tools, translated targets, and duplicate namespaces.
Assert that gateway affinity and Copilot item-id handling leave legacy agent-message content opaque while plaintext collaboration owns all newly generated agent traffic.
Verify deferred tool inventories, schema decryption, response restoration, exact plaintext stream arguments, history replay through fresh aliases, HTTP integration, and the remaining structured encrypted tool-output affinity.
Make the HTTP integration's raw namespace schema assertion explicit at the open tool-union boundary.
Describe the plaintext namespace membrane, exact Codex plaintext marker, native and translated target behavior, history persistence, and the removal of agent-message affinity and item-id carriers.
Use a deterministic collision-safe namespace, restore shared invocation state between server-tool turns, cover deferred tool inventories and null snapshots, reject explicit encrypted markers, strip Messages schemas, and exclude Chat Completions targets that cannot represent namespace tools.
Assert the collaboration schema after narrowing the translated Messages tool union.
Select candidates after Stateful hydration, exclude translated targets for unrepresentable deferred or allowed-tool forms, honor namespace-qualified Messages choices, reject explicit encrypted history, preserve snapshot nulls, and keep alias projection scoped to each inner server-tool turn.
Menci added 15 commits August 5, 2026 23:35
Import the Responses choice contract used by the plaintext namespace fixture.
Evaluate collaboration after Stateful hydration, route deferred inventories only to native Responses, exclude unrepresentable Messages choices, strip Messages schemas, validate explicit encrypted history, and document translated legacy behavior.
Read the open namespace extension through the JSON-record boundary after merging the latest protocol contracts.
Resolve the reserved namespace as collaboration_2, collaboration_3, and so on, matching ordinary client collision behavior without exposing a gateway-specific name on either wire.
Rewrite exact namespace-qualified function names in native tool choices and allowed-tool entries in both directions, including collision-suffixed aliases.
Rewrite collaboration-qualified names only for function choices while preserving unrelated dotted custom and namespace entries.
Centralize native and Messages namespace lowering, response restoration, target eligibility, tool-choice mapping, and payload lifecycle in a collaboration shim. Remove collaboration-specific behavior from the generic Messages translator and keep Chat candidates out when namespace semantics are not representable.
Keep plaintext markers and schema semantics exclusively in the collaboration shim. Generic Messages translation now flattens and restores namespaces without knowing collaboration actions.
Echo qualified and unrelated allowed-tool entries through the native snapshot fixture, and keep the Messages projection case on its representable automatic choice.
Activate the plaintext collaboration membrane for history-only function calls so target selection and marker validation cannot bypass it. Document Codex's provider-dependent replay normalization and cover the replay-only shape.
Project collaboration once around every upstream turn and preserve the interceptor chain's one-way context mutation invariant. Keep the plaintext alias stable as the server-tool loop accumulates input, then restore only the client-facing event stream.
@Menci
Menci force-pushed the responses-multi-agent branch from 74e22e7 to 3f546c5 Compare August 5, 2026 17:04
@Menci Menci changed the title feat(gateway): host the Responses beta multi_agent capability fix(responses): project collaboration through a plaintext shim Aug 5, 2026
Menci added 5 commits August 6, 2026 16:34
# Conflicts:
#	AGENTS.md
#	docs/AFFINITY.md
#	docs/RESOLUTION.md
#	docs/TRANSLATION.md
Keep the plaintext collaboration shim independent from Copilot's existing agent-message item-ID carrier behavior. Restore the provider implementation and its tests exactly to main.
Make the collaboration shim a target-independent source projection. Move namespace lowering into shared translation code, add Chat Completions round-trip support, and keep native-only payload selection based on generic Responses features.
Keep candidate selection limited to endpoint availability. Let each translator reject unsupported Responses input and tool-choice shapes at its owning boundary, while preserving namespace-qualified function choices.
@Menci

Menci commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

I am Claude Code (Opus 5), running for the maintainer. Flagging a defect found while
verifying #456 — the reachability argument there pointed at this branch, and the pointer was
correct even though the conclusion drawn from it was not.

opaque-locations.ts at e1d0911 is a net narrowing of the affinity carrier set. It
gains the function_call_output / custom_tool_call_output output.N.encrypted_content
slots, and it loses agent_message's content.N.encrypted_content on every side at once:

  • responsesOpaqueLocations has no agent_message branch, and an agent_message has no
    top-level encrypted_content, so it yields zero locations for those items;
  • the egress synthetic-carrier branch that appended
    content.${item.content.length}.encrypted_content is gone;
  • canCarryAffinity drops 'agent_message' from its list;
  • the ingress scan loop that walked item.content is gone.

ResponsesAgentMessageContent still carries the encrypted_content variant (now spelled
ResponsesEncryptedContent), so this is not a slot that stopped existing — it is a slot we
stopped looking at.

Two concrete consequences, both on carriers that are already sitting in client conversation
history from builds before this branch:

  1. A synthetic carrier reaches a third-party upstream. On main, ensureItemCarrier
    appends an encrypted_content content part to an agent_message whose payload is 100%
    Floway ciphertext with zero upstream-origin bytes, and ingress removes it before dispatch
    (projectOptionalAffinityBlob returns remove because decoded.value === undefined).
    After this branch, ingress never decodes it, so the removal never happens and we transmit
    our own ciphertext to the provider.
  2. An ordinary carrier reaches the upstream with our trailer attached. Same mechanism,
    for an agent_message content part that wraps real upstream bytes: unscanned, so
    unwrapped, so forwarded framed.

Both are exactly the impact #456 describes, arriving through our own change rather than
through the operational events that issue hypothesized. The affinity pin carried by those
items is lost along the way.

The narrowing is the bug, and it is the second time this file's slot inventory has been
restated by hand. Extracting responsesOpaqueLocations was the right move; the fix is to
finish it so the inventory cannot diverge again:

  • restore the agent_message content.N.encrypted_content locations, including the
    synthetic-carrier append path, so egress and ingress both derive from the single inventory
    with no per-side item-type lists;
  • reduce canCarryAffinity's residual literal list to something the inventory itself
    determines, so adding or removing a carrier slot is a one-place edit;
  • pin every item type × slot with a wrap-then-unwrap round trip, so a future narrowing fails
    the build rather than silently stranding carriers in client history.

I am picking this up now.

Extracting the opaque slot inventory dropped the agent message's
content.N.encrypted_content carrier from every side at once: the ingress
scan, the egress wrap, the synthetic-carrier append, and canCarryAffinity.
The protocol still carries that content variant, so the slot did not stop
existing — we stopped looking at it.

Carriers already held in client conversation history therefore replayed
unscanned, which meant unwrapped: a synthetic carrier of pure gateway
ciphertext, and ordinary carriers still wearing our trailer, both bound
for a third-party upstream, with the routing pin they encode lost on the
way. Reported against this branch in issue #456.

The inventory now enumerates each slot once, as a path, and reading,
rewriting, growing and removing a carrier all derive from that single
enumeration; egress no longer keeps its own item-type list, and a tool
output stays a slot we may rewrite but never grow. The round-trip test
walks every item type and slot, and asserts egress actually changed the
value — without that the previous agent message round trip passed while
nothing wrapped it at all, since an unwrapped value also survives a round
trip unchanged.
@Menci

Menci commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Fixed in 9ff9714, pushed to this branch.

The inventory now enumerates each slot once as a path, and reading, rewriting, growing and removing a carrier all derive from that single enumeration — canCarryAffinity and ensureItemCarrier no longer keep their own item-type lists, and a tool output stays a slot we may rewrite but never grow.

Worth recording: the existing agent-message natural and originless nested carriers round-trip test passed against the narrowing. It asserted only that the value survives egress and ingress, which is trivially true when nothing wraps it. The new table-driven test asserts egress changed the value and that no wrapped value ever reaches a mismatched upstream, walking every item type and slot. Reverting the fix fails it on agent_message for exactly that reason.

Verified: lint clean, typecheck clean, 5485 tests pass.

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.

1 participant