> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentcard.com/llms.txt
> Use this file to discover all available pages before exploring further.

# buy.turn

> Opens the turn of one shopper message, so its links, answers and acknowledgements never mix with another message's.

`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.

```ts chat.ts theme={null}
import Anthropic from '@anthropic-ai/sdk';
import { AgentcardClient, buyTool } from '@agent-cards/commerce-sdk';

const agentcard = new AgentcardClient({
  clientId: process.env.AGENTCARD_CLIENT_ID ?? '',
  clientSecret: process.env.AGENTCARD_CLIENT_SECRET ?? '',
});
const buy = buyTool({ agentcard, externalUserId: 'user-42' }); // one per shopper, kept
let history: Anthropic.Beta.BetaMessageParam[] = [];

export async function onShopperMessage(message: string) {
  const turn = buy.turn(message, { onReaction: (emoji) => reactToShopperMessage('user-42', emoji) });
  const runner = new Anthropic().beta.messages.toolRunner({
    model: 'claude-opus-5-5',
    max_tokens: 16000,
    tools: [turn],
    messages: turn.messages(history),
  });
  const answer = await runner;
  history = runner.params.messages;
  return { answer, links: turn.links, carts: turn.results.at(-1)?.carts ?? [] };
}
```

`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](https://docs.agentcard.com/commerce-sdk/guides/answering-each-message.md) guide walks through this code.

## Open a turn

```ts theme={null}
buy.turn(shopperText: string, options?: BuyToolTurnOptions): BuyToolTurn
```

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

| Option | Type | Description |
| - | - | - |
| `onItem` | `(item: BuyToolTurnItem) => void` or a promise, optional | Hears everything the turn hands the shopper, one item at a time, in the order it happened. Each item waits for the one before it to settle. |
| `pace` | `boolean` or `(message: string) => number`, optional | With `onItem` or `onMessage`: how the turn spaces its messages. Default `false`. `true` waits `typingDelay` between messages. A function returns the pause in milliseconds. |
| `onTyping` | `(typing: boolean) => void`, optional | With `onItem` or `onMessage`: hears `true` when a pause starts and `false` the moment its message goes out. |
| `onMessage` | `(message: string) => void` or a promise, optional | Without `onItem`: hears each message your model writes through `turn.messageStream()` or `turn.say()`, paced by `pace`. |
| `sendLink` | `(link: BuyToolLink) => void` or a promise, optional | Delivers a link of this turn to the shopper. Each call waits for it. |
| `onReaction` | `(emoji: string) => void` or a promise, optional | Reacts `👀` to this turn's message when the turn opens on a new request, or once a slow call passes `ackDelayMs`. |
| `onWorking` | `(message: string) => void` or a promise, optional | Sends this turn's instant text reply under the same rules. |
| `onResult` | `(result: BuyToolResult) => void` or a promise, optional | Hears the whole answer of each call this turn makes, after its links went out and before the model reads it. |
| `onProgress` | `(progress: BuyProgress) => void`, optional | Hears each stage Agentcard reaches on a call this turn makes. |
| `replyAsWritten` | `boolean`, optional | `true` sets `turn.replyAsWritten` when an answer only asks the shopper something. `false` leaves it `null`. Default: the tool's `replyAsWritten`. |

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

| Member | Type | Description |
| - | - | - |
| `event` | `ShopperMessage` or `null` | For a turn that `buy.nextStep` opened: the message whose step the shopper finished. `null` for a turn of a shopper message. |
| `release()` | `void` | For a `nextStep` turn your agent could not finish: call it before you post `message.text`. Does nothing on another turn. |
| `ack` | `string` or `null` | The instant reply this turn sent to `onWorking` when it opened, or `null`. |
| `reaction` | `string` or `null` | The reaction this turn put on the shopper's message, `👀`, or `null` while it put none or when `onReaction` threw. |
| `agentcardMessages` | `ShopperMessage[]` | The messages Agentcard posted into this shopper's chat that the model has not read yet, oldest first. |
| `messages(history)` | `Array` | What to send your model: `history`, then what Agentcard told the shopper that `history` does not hold yet, then the shopper's message. |
| `links` | `BuyToolLink[]` | Every link this turn's calls handed over, in order. A link `sendLink` threw on is left out. |
| `results` | `BuyToolResult[]` | The whole answer of each call this turn made, in the order the calls returned. |
| `replyAsWritten` | `string` or `null` | With the option on: Agentcard's reply when the turn's last answer only asks the shopper something, with every link taken out. |
| `items` | `BuyToolTurnItem[]` | Every message and link the turn handed over, in the order they went out. |
| `messageStream()` | `MessageStream` | A message stream whose messages become this turn's `text` items. |
| `say(text)` | `Promise<void>` | Cuts a text into messages and hands each one over as a `text` item. |

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

```ts theme={null}
type BuyToolTurnItem = { kind: 'text'; text: string } | { kind: 'link'; link: BuyToolLink };
```

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

```ts theme={null}
buy.nextStep(message: ShopperMessage, options?: BuyToolTurnOptions): BuyToolTurn | null
```

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.

```ts steps.ts theme={null}
agentcard.listen({
  onMessage: (shopperId, message) => {
    const step = buyFor(shopperId).nextStep(message);
    if (!step) return postToChat(shopperId, message.text);
    void runModel(shopperId, step).catch(() => {
      step.release();
      agentcard.delivered(shopperId, message);
      postToChat(shopperId, message.text);
    });
  },
});
```

`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`

```ts theme={null}
toMessages(text: string): string[]
```

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.

```ts split.ts theme={null}
import { toMessages } from '@agent-cards/commerce-sdk';

toMessages('Found two.\n\nWant the first one?'); // ['Found two.', 'Want the first one?']
```

### `messageStream`

```ts theme={null}
messageStream(options: MessageStreamOptions): 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.

| Option | Type | Description |
| - | - | - |
| `onMessage` | `(message: string) => void` or a promise, required | Hears each message once, in order. The stream awaits a returned promise before it sends the next message. |
| `pace` | `MessagePace`, optional | `false` (default) sends each message the moment it is written. `true` waits `typingDelay` since the message before. A function returns the pause in milliseconds. |
| `onTyping` | `(typing: boolean) => void`, optional | Hears `true` when a pause starts and `false` the moment its message goes out. |

`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`

```ts theme={null}
typingDelay(message: string): number
```

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.