Skip to main content
buy.turn(shopperText, options) binds your buy tool to one shopper message. Pass the returned turn to your model in place of the tool. Every call the model makes through the turn hands its links, answers and instant reply to that turn’s own callbacks and lists, so a shopper who sends a second message before the first is answered never gets one message’s cards beside the other’s reply.
chat.ts
reactToShopperMessage stands for your own code. The function returns the model’s answer, the links the turn’s calls handed over, and the carts of the last call. The Answering each shopper message in its own turn guide walks through this code.

Open a turn

Call it the moment the message arrives, before your model runs. With an onReaction, a message that starts a search gets its 👀 at once. With only an onWorking, the instant reply that buy.instantAck would return goes out at once. A callback left out of options falls back to the one the tool was built with, except with onItem. The turn is the tool, with the same name, schema and methods. Pass it to every framework the way you pass buy: tools: [turn], tools: { buy: turn }, tools: [turn.openai] with turn.handle, or toOpenRouterTool(turn, { tool, z }).

Options

With onItem, the turn sends no link to a sendLink and no instant reply to an onWorking, the tool’s included. A turn that gets onItem beside sendLink, onWorking or onMessage throws. A step link, which is a vault, approval or store_login page the shopper opens to go on, goes out right after the message that asks for it. The turn holds the link until your model’s next message stream ends or turn.say is called. The turn also sends a held link when the turn’s next call starts, or after 20 seconds, so a model that writes nothing still sends it. Product and store cards go out the moment the tool hands them over.

Members of the turn

Each read of links, results and items returns a new array. ack, reaction, links and results stay out of the tool’s JSON, so a tool runner sends the API the same fields as for buy.

messages

turn.messages(history) returns a new array and never changes history. The array holds history, then Agentcard’s text as the assistant’s words with every link taken out, then { role: 'user', content: message }. When the last message in history is the assistant’s plain text, a string or text parts, Agentcard’s words join that message. After anything else, such as a tool result or an OpenAI Responses item, they come as an assistant message of their own. The Anthropic SDK, the Vercel AI SDK, OpenAI’s chat completions and the Responses API’s input all take what it returns. A message stays kept until a history you pass to turn.messages holds it. When a turn fails and you keep the history you had, the next turn hands the message to the model again.

replyAsWritten

Only a reply Agentcard sent goes out as written. 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. replyAsWritten(result) applies the same rule to one answer.

BuyToolTurnItem

A text item is the instant reply, or a message your model wrote through turn.messageStream() or turn.say(). A link item is a link the tool handed over, with the fields sendLink would get. With onItem, the 👀 still goes to onReaction and never joins the items.

Next step

Opens the turn your agent takes on its own once the shopper finishes a step outside the chat. Open it for a message whose advances is true, run your model on it as on buy.turn, and post the model’s answer in place of message.text. The shopper then reads one message that does the next thing, without saying “ok” first.
steps.ts
buyFor, runModel and postToChat stand for your own code. The listener moves on while the model runs, and a turn that fails falls back to the message’s own text. NEXT_STEP_KINDS lists the kinds that advance: card_added, store_connected and approval_declined. nextStep returns null, and you post message.text, when:
  • the message’s advances is false, such as order_placed;
  • the message is for another shopper, or its link closed;
  • the tool already took a turn for the message;
  • a call for the shopper’s own message is still running, because that call’s answer tells the model where the purchase stands.
The turn moves the purchase only up to the shopper’s own decision:
  • No call of the turn sends an ask to Agentcard. Each call reads where the purchase stands and answers with the next step: the card link, or the cart and the ask for the shopper’s yes.
  • A confirm goes in only for the yes the shopper already gave in the chat, when that confirm stopped for the card or the sign-in they finished. The confirm goes in once, for that cart’s hash, and Agentcard refuses it if the cart changed. A later shopper message that is not a plain go-ahead, such as “wait, make it two”, takes the yes back.
  • Any other confirm the model sends is refused by the tool, and nothing reaches Agentcard.
  • The approval stays the shopper’s: the confirm sends the approval link, and the order goes in only once they approve it.
turn.messages(history) for a next-step turn ends with the event, marked as written by the system, and the instruction to take the next step in one short message. nextStepPrompt(message) writes those words. The turn gets no 👀 and no instant reply. A shopper who was not connected yet when they asked gets their own first ask run, in their words, once their card is saved. Once nextStep opens a turn, the client never hands the message’s text to a later turn as words the shopper read. After step.release(), the text reaches later turns once agentcard.delivered records it, and a redelivery of the event opens a turn again.

Message helpers

toMessages

Cuts text at its blank lines into the messages a shopper reads one at a time. Each message is trimmed, and empty ones are left out. A list whose items blank lines separate stays one message, and so does a fenced code block.
split.ts

messageStream

Cuts streamed text into messages as it arrives. Each paragraph goes to onMessage once the blank line that ends it arrives, however the stream splits the text. A paragraph inside a list waits for the first characters of the next line, which say whether the list goes on. Start a new stream for each model message. MessageStream has two methods. push(delta) adds the next piece of text. end() hands over the last message and resolves once every onMessage call settled. After a send fails, no later message goes out, and end rejects with that error. A push after end throws.

typingDelay

Returns how long someone texting takes to type message, in milliseconds: 300, plus 12 for each character, at most 1500. With pace: true, the stream and the turn wait this long between two messages. Time the model spent writing counts toward the pause.