Skip to content

conversation

The Teams conversation turn: run a Mastra agent against an inbound Bot Framework activity and answer with Adaptive Card attachments.

This is the piece that makes POST /api/teams/activity behave like a Teams bot rather than a chat API that happens to return JSON. A turn is:

  1. read the user’s text off the inbound message activity;
  2. ANSWER it - a normal tool-using agent turn, with no mention of cards, so the agent queries Genie / calls its tools exactly as it would on a streaming chat endpoint;
  3. FORMAT that answer into a card.CardSpec in a second pass, via Mastra’s structuredOutput (prompt-injected, see JSON_PROMPT_INJECTION);
  4. compile the spec with the same deterministic builder the create_teams_card tool uses, and attach it to an outbound activity.

The two passes are the important part. Asking for the answer AND the card shape in ONE request makes the model treat formatting as the task: it emits a card straight away and never calls its tools, so a question that should have queried a data source came back as “I don’t have a real system connected - here is a template card with placeholders”. Answering first, then formatting a REAL answer, makes this endpoint’s content identical to the streaming endpoint’s; only the presentation differs.

Formatting is also why the turn does not simply rely on the agent calling create_teams_card: on this endpoint a card IS the response format, so it should be a property of the turn rather than a tool the model may forget. Agents keep the tool for the other direction - answering in prose on a normal chat endpoint and choosing to attach a card. When the agent DOES call it during the answering pass, that spec wins and the formatting pass is skipped.

The conversation id doubles as the agent’s memory thread id, so a client that keeps posting the same conversation.id gets a continuous conversation - the same mapping a real channel relies on.