Skip to main content
A shopper can send a second message before the first is answered. If your app hands every link to “the message running now”, the first message’s cards land beside the second message’s reply. Open a turn for each message instead, and each message keeps its own cards, its own cart, and its own acknowledgement.

Open a turn for each message

The sendLink, onWorking, and onResult you pass to buyTool hear every call, whichever message the call answers. Pass them to buy.turn instead, and give the turn to your model in place of the tool.
chat.ts
history stands for the shopper’s earlier messages. turn.messages(history) is the history, then what Agentcard told the shopper that the history does not hold yet, then the message. The turn opens the moment the message arrives, so its 👀 and its links belong to that message, while the shopper’s model runs go one after another. Each run reads the history the run before it saved, so a second message never writes the first one’s whole exchange out of the history. A turn buy.nextStep opened goes in the same queue, as “Taking the next step” shows. Every call the model makes through the turn belongs to that message alone:
  • turn.links holds each link the turn’s calls handed over, in order: the Vault, approval, and store sign-in links, each product’s store page, and each store card’s page. Send them with the reply, or pass sendLink to buy.turn to send each one the moment the tool hands it over. A link your sendLink throws on stays out of turn.links.
  • turn.results holds the whole answer of each call, and an onResult passed to buy.turn hears each one as it arrives.
  • turn.reaction and turn.ack hold the acknowledgement the turn sent, as “Acknowledging the shopper” describes.
  • An onProgress passed to buy.turn hears only that turn’s stages.
  • turn.agentcardMessages holds each message agentcard.listen or agentcardWebhook posted into this shopper’s chat that the model has not read yet.
A callback you leave out of buy.turn falls back to the one the tool was built with. A tool whose sendLink texts the shopper keeps texting, and turn.links lists the same links. The turn is the tool, with the same name, schema, and methods, so pass it to every framework the way you pass buy: tools: [turn] for Anthropic, tools: { buy: turn } for the Vercel AI SDK, tools: [turn.openai] with turn.handle for OpenAI and OpenRouter chat, and toOpenRouterTool(turn, { tool, z }) for OpenRouter’s agent SDK. Build the tool once for each shopper and open a turn from it for each message. The turns share the shopper’s conversation, so a card the shopper already holds from an earlier turn is not sent again. A tool built for every message buys the same way, but it sends the same product links again each time Agentcard shows the same products. A call made on buy itself still works: it belongs to the turn the tool reads from its timing, and its links go to the tool’s sendLink.

Show Agentcard’s reply as written

Most answers only ask the shopper something: which product, which size, which store, or a yes to a cart’s total. Agentcard’s reply already says that, so a second model call that rewords it costs the shopper another round trip and changes nothing. Pass replyAsWritten: true, to buyTool for every turn or to buy.turn for one, and turn.replyAsWritten holds Agentcard’s reply, with every link taken out, whenever the turn’s last answer only asks. Show it and end the turn without calling your model again:
chat.ts
Any other answer, such as an approval sent, an order placed, or a refusal, leaves turn.replyAsWritten null, and your model words it. So does a line the tool writes itself for your model, such as the line saying the cart changed. Only a reply Agentcard sent goes out as written. Leave the option off to have your model write every answer.

Hand the app the turn in order

A turn reaches the shopper as messages and links that interleave: the instant reply, the cards a search found, then your model’s words about them. Pass onItem to buy.turn, and the turn hands your app all of them as one sequence, one item at a time, in the order they happened.
chat.ts
A text item is the instant reply, or a message your model wrote through turn.messageStream() or turn.say(text). A link item is each link the tool hands over, with the fields sendLink would get. Show each item below the one before it.
  • Each message goes out the moment it is ready. Pass pace: true to space them like someone texting: each one then waits until typingDelay of it has passed since the item before it went out, which is 300 ms plus 12 ms a character and at most 1.5 seconds. Time your model spent writing counts toward the pause, so a slow model gets no added wait, and links never wait. onTyping hears true when a pause starts and false when its message goes out. Pass a function for your own pause in milliseconds.
  • With onItem, the turn sends no link to a sendLink and no instant reply to an onWorking, so a tool built with those for other turns sends nothing twice. Passing onItem beside sendLink or onWorking to the same turn throws.
  • turn.items holds every item that went out, in order, so an app that answers the whole turn at once returns it as is.
  • A step link, the vault, approval, or store_login page the shopper opens to go on, comes after the message that asks for it. The turn holds it until your model’s next message stream ends, or until turn.say is called, then sends it right after that message. A held link also goes out when the turn’s next call starts, or after 20 seconds. turn.say and the stream’s end resolve once the held links went out too, and reject when one failed to send, so your app never answers without a link the shopper needs.
Without onItem, pass onMessage beside sendLink, and write the model’s text through turn.messageStream(). onMessage hears each message, paced, and sendLink hears each step link after the message that asks for it.

Send each paragraph as its own message

A chat app reads best when each paragraph arrives as its own message. toMessages(text) cuts text at its blank lines, trims each message, and leaves out the empty ones. A list whose items blank lines separate stays one message, and so does a fenced code block. messageStream does the same for a reply your model streams: push each piece of text, then call end for the last paragraph. Start a new stream for each model message, so one message never runs into the next.

Check it in the sandbox

  1. Send “two packs of AA batteries from Amazon” as a test shopper, and send “get me tacos” before the first answer arrives.
  2. Check that each message’s cards land with its own reply, and that the battery cards never appear beside the taco stores.
  3. Check that turn.links and turn.results for each turn hold only that message’s links and carts.
  4. Turn on pace: true. Check that the messages arrive a short pause apart and the cards arrive at once.
  5. Turn on replyAsWritten: true, send an ask that gets a product list, and check that your model runs only once.
  6. Once both answers arrive, send “what did i ask you for”. Check that the reply names the batteries and the tacos, so neither exchange left the history.