Open a turn for each message
ThesendLink, 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.linksholds 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 passsendLinktobuy.turnto send each one the moment the tool hands it over. A link yoursendLinkthrows on stays out ofturn.links.turn.resultsholds the whole answer of each call, and anonResultpassed tobuy.turnhears each one as it arrives.turn.reactionandturn.ackhold the acknowledgement the turn sent, as “Acknowledging the shopper” describes.- An
onProgresspassed tobuy.turnhears only that turn’s stages. turn.agentcardMessagesholds each messageagentcard.listenoragentcardWebhookposted into this shopper’s chat that the model has not read yet.
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. PassreplyAsWritten: 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
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. PassonItem 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
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: trueto space them like someone texting: each one then waits untiltypingDelayof 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.onTypinghearstruewhen a pause starts andfalsewhen its message goes out. Pass a function for your own pause in milliseconds. - With
onItem, the turn sends no link to asendLinkand no instant reply to anonWorking, so a tool built with those for other turns sends nothing twice. PassingonItembesidesendLinkoronWorkingto the same turn throws. turn.itemsholds 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, orstore_loginpage 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 untilturn.sayis 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.sayand the stream’sendresolve 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.
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
- Send “two packs of AA batteries from Amazon” as a test shopper, and send “get me tacos” before the first answer arrives.
- Check that each message’s cards land with its own reply, and that the battery cards never appear beside the taco stores.
- Check that
turn.linksandturn.resultsfor each turn hold only that message’s links and carts. - Turn on
pace: true. Check that the messages arrive a short pause apart and the cards arrive at once. - Turn on
replyAsWritten: true, send an ask that gets a product list, and check that your model runs only once. - 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.