feat(agent): handoff routes, capability declaration, post-prompt normalization - #92
Merged
Conversation
…alization
Adds the second half of what a browser client needs around ChatGateway, plus
two pieces of agent infrastructure.
ai_chat/handoff.py — HandoffRouter. The SignalWire address widget already
hardcodes {gateway-url}/handoff, /escalate and /say against the same URL that
serves a ChatGateway, and sends handoff_nonce and chat_handle as user
variables. Without these routes the SDK ships a gateway its own widget
considers incomplete, with the missing half specified nowhere.
It owns the wire contract only — the routes, the nonce, the ordering
guarantee, the spend guards — and nothing about what a conversation is; where
a transcript is written and what a resumed greeting says are injected as
callbacks.
The nonce is the load-bearing part: ai_message takes a call_id, so a
page-supplied one would let anyone who guessed an id inject speech into a
stranger's live call. The browser proves which call it is on instead — the
application puts a random handoff_nonce in one dial's user variables and the
browser presents it later. An unknown nonce is answered exactly like an
expired one, so it cannot be used to probe whether a call is live. Redemption
is single-use; typing is repeatable up to max_messages_per_call.
core/capabilities.py, core/post_prompt.py — capability declaration and
post-prompt normalization, with tests for each.
Verified against main's baseline rather than asserted:
- 6060 tests pass, up 118. The 6 mcp_gateway failures are pre-existing and
byte-identical to main's.
- mypy: 19 findings, the same 19 as main, set difference empty.
- LINT and FMT clean.
DRIFT is the one gate this cannot satisfy from here: it adds public API, so
the surface oracle in porting-sdk needs regenerating, which a maintainer lands
alongside. Note porting-sdk#138 excludes ai_chat.handoff from the oracle as
Python-first (ChatGateway/HandoffRouter are route factories over AIChatClient
and add no server capability), so once that merges the regen covers
core.capabilities and core.post_prompt only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
HandoffRouter(the routes the SignalWire address widget already calls), capability declaration, and post-prompt normalization.HandoffRouter
The address widget hardcodes
{gateway-url}/handoff,/escalateand/sayagainst the same URL that serves a
ChatGateway, and sendshandoff_nonceandchat_handleas user variables. Without these routes the SDK ships a gatewayits own widget considers incomplete, with the missing half specified nowhere.
It owns the wire contract only — routes, nonce, ordering guarantee, spend
guards — and nothing about what a conversation is; transcript capture and
resumed-greeting text are injected as callbacks.
The nonce is the load-bearing part.
ai_messagetakes acall_id, so apage-supplied one would let anyone who guessed an id inject speech into a
stranger's live call. The browser proves which call it is on instead: the
application puts a random
handoff_noncein one dial's user variables and thebrowser presents it later. An unknown nonce is answered exactly like an expired
one, so it cannot be used to probe whether a call is live. Redemption is
single-use; typing is repeatable up to
max_messages_per_call.Plus
core/capabilities.pyandcore/post_prompt.py, each with tests.Verified against main's baseline, measured not asserted
mcp_gatewayfailures are pre-existing andidentical to main's.
Note
DRIFT reports new public surface here (
core.capabilities,core.post_prompt). Since #91 that is advisory — python is the reference andthe oracle is derived from it — so the snapshot gets regenerated downstream in
porting-sdk on its own schedule.
ai_chat.handoffis excluded from that oracleby porting-sdk#138 as Python-first, so it carries no cross-port obligation.
🤖 Generated with Claude Code
https://claude.ai/code/session_015dYktt85Ltj3oK9gG5VBww