2.8 KiB
HITL button action envelope
Authoritative wire shape for buttons rendered by @copilotkit/channels-teams and
the click Teams delivers back. A consumer that decodes clicks out-of-band (e.g.
the Intelligence managed-Teams ingress, which deep-imports this package's renderer
but runs its own inbound decode) must match this exactly.
Contract test: src/button-action-envelope.contract.test.ts.
Emitter: renderButton in src/render/adaptive-card.ts.
Decoder: parseCardAction in src/interaction.ts.
Outbound — what the renderer emits
A <Button> (with an onClick handler, i.e. not a link button) renders as a
top-level Adaptive Card Action.Submit — deliberately Action.Submit, not
Action.Execute (no verb). The opaque action id and optional value ride in
the action's data:
{
"type": "Action.Submit",
"title": "Approve",
"data": {
"ckActionId": "ck:approve", // opaque id; present only when the Button had an onClick handler
"value": { "decision": "yes" }, // present only when the Button had a `value` prop
},
"style": "positive", // optional: "positive" (primary) | "destructive" (danger)
}
- A link
<Button>(has aurlprop) renders asAction.OpenUrlinstead and carries nodata— it is not an interactive submit and never round-trips. datais omitted entirely if the button has neither anonClickid nor avalue.
Inbound — what Teams delivers on click
Clicking an Action.Submit arrives as a Message activity (activity.type === "message"), NOT an invoke / adaptiveCard/action / Action.Execute activity.
The action's data becomes activity.value, and the message text is empty:
{
"type": "message",
"text": "", // empty — the payload is in `value`, not text
"value": {
// === the emitted action `data` (merged with any card inputs)
"ckActionId": "ck:approve",
"value": { "decision": "yes" },
},
"conversation": { "id": "<stable conversation id>" },
}
Decode rules
- Is it a card action?
typeof activity.value.ckActionId === "string". If not, it's an ordinary chat message. - Fields:
id = activity.value.ckActionId,value = activity.value.value. Carry only these two — no resume-data smuggling; durability rides on the consumer's action store keyed byid. - Card inputs: if the card also had
<Input>/<Select>fields, Teams merges their values intoactivity.valuealongsideckActionId/value. Read the named input keys directly fromactivity.valueif needed. - Conversation key: derive it from
activity.conversation.id(seeconversationKeyOf). Ingress and interaction decode MUST use the same key or the waiter is stranded.