Send a message to the agent

post/v2/agent/chat

Runs one agent turn over the conversation you send. The agent may read what the store stocks, search the catalogue privately, put chosen products on screen, compose an outfit into one or more looks, ask the shopper a structured question, offer follow-up chips, and — when a cart snapshot is present — read or change the cart. All of that happens server-side inside a single request; tool results go back into the model before it writes, capped at ten model calls per turn.

Streaming

The response streams by default. It is text/event-stream and each thing the agent does arrives as its own event — see the streaming guide and the event catalogue below. Pass stream: false to receive one JSON body once the turn has finished.

Conversation state

You own the conversation state. The turn ends with a replay-ready history array (on the done event, or in the JSON body). Send it back verbatim as messages on the next request, with the shopper's new message appended, and the conversation continues. There is no server-side chat id and nothing is stored between requests.

Dataset scope

datasetIds narrows the catalogue the agent works from; omit it to use every active dataset. An invalid or archived dataset scope does not fail the request — the agent's tools report the problem to the model (you will see it on tool_output), and the model answers without catalogue data.

Prices in the shopper's country

Send country (on a Shopify storefront, Shopify.country) and every product in search_results, looks and the tool outputs, the prices on cart actions, the sizes in stock and the sale information are the shopper's market's. Without it the store's default market's prices come back, flagged product.marketExact: false. An unrecognised country is treated as absent, never as an error. See Introduction.

Metering

One request consumes one unit of api.v2.agent.chat regardless of how many tool calls the turn takes. The rate-limit headers are set on the stream response as well as on JSON responses. This resource is separate from v1 chat, so v2 usage never draws down your POST /v1/chat allowance and vice versa.

Where errors arrive

Authentication, quota and validation failures are decided before any stream exists, so they always come back as an ordinary JSON error with the right HTTP status — check Content-Type before you attach an SSE reader. Once the stream has started the status is already 200; a failure mid-run is delivered as an error event and the stream closes without a done event.

Public keyapi.v2.agent.chatDemo key OKStreams SSE

Request body

application/jsonrequired
array<UserMessage | AssistantMessage | FunctionCall | FunctionCallResult>required

The whole conversation so far: the previous turn's history, if any, followed by the shopper's new message. On the first turn this is a single UserMessage.

minItems: 1
datasetIdsarray<string>

Restrict the agent to these datasets. Omit or send [] to use every active dataset in the organization, narrowed to one store when store is sent. When present, these always win over store. Must be an array when present. An invalid scope does not fail the request; the agent's tools report the problem to the model instead.

default: []
CartSnapshot | null

A snapshot of the shopper's cart. Sending one — even an empty cart with items: [] — enables the cart tools for this turn; omitting it or sending null leaves the agent with no cart capability at all. The server does not validate the snapshot: any non-null value enables the tools, and a malformed one makes the cart tools report errors to the model (visible on tool_output) rather than failing the request. See the cart guide.

default: null

Option 1: CartSnapshot

itemCountintegerrequired

Total units across all lines.

e.g. 2
totalnumberrequired

Cart total in major currency units.

e.g. 168
currencystringrequired

ISO 4217 currency code for total and each line's price.

e.g. "USD"
array<CartLine>required

The cart lines. May be empty.

Option 2: null

string | boolean | null

How much the model reasons before it acts, for this turn. Left out (null, false or absent) it is low: the least that keeps a turn from ending on a promise with nothing on screen. none is the fast option — nothing then sits between the request and the first word, about 700 ms sooner — at the cost of an occasional dead turn. true also means low; a string picks a level. Accepted levels depend on the model and are checked before the run: an unsupported one is a 422 naming the accepted ones. With thinking on, the reasoning is summarised and streamed as thinking, thinking_delta and thinking_break events so the wait is visible; at the default none of those are sent.

default: nulle.g. "low"

Option 1: string

Option 2: boolean

Option 3: null

objectnullable

What the shopper is looking at. Send product when the chat is open on a product page and the agent treats "this" and "it" as that product, answering questions about its price, options and availability without a lookup. It is context only: it enables no tools. The server reads the fields below by name, clamps them, and ignores everything else; a product without a title is ignored. The Stylor widget fills this from the storefront's /products/{handle}.js.

default: null
object
streamboolean

Omitted, true or any other truthy value streams Server-Sent Events. false — or any falsy value such as null or 0 — returns one JSON body when the turn is complete.

default: true
sessionIdstringnullable

The analytics session this visit belongs to — the session.sessionId returned by the v1 widget preflight — so events the server records line up with events your storefront reports for the same visit. Never used for authorization. It also binds cart intent tokens: without it, add and swap cart actions carry intent: null and their outcomes cannot be reported.

default: nulle.g. "3b0d4a1e-6f52-4b8a-9d3c-7e2f1a0b9c8d"
Session | null

Visit metadata for analytics. Ignored unless it is useful to you.

default: null

Option 1: Session

surfacestring

Where the shopper is talking to the agent. Defaults to widget when omitted.

e.g. "widget"
agentVersionstring

Which agent version your client is built against. Use v2.

e.g. "v2"
storageModestring

How your client persists the visit id — for example memory, session or local. Defaults to memory.

e.g. "local"
tzstringnullable

The shopper's IANA time zone.

e.g. "America/Toronto"
localestringnullable

The shopper's BCP 47 locale.

e.g. "en-CA"
devicestringnullable

A coarse device class, typically mobile or desktop.

e.g. "desktop"

Option 2: null

object | null

Which store the shopper is on, for an organization that runs several stores under one key. origin is the storefront page's origin (window.location.origin) and shop is Shopify's permanent shop domain (Shopify.shop, e.g. kit-and-ace.myshopify.com), which still identifies the store when it serves from a different domain. When datasetIds is empty, the agent searches only that store's active datasets and speaks as that store. If neither matches a dataset, every active dataset is used; when those belong to several stores, the agent says it represents several stores, names the store each product comes from, and points policy questions to that store's own site. Ignored when datasetIds is sent. It can only narrow within your own organization, never widen it. The Stylor chat embed sends it.

default: null

Option 1: object

originstringnullable
e.g. "https://kitandace.com"
shopstringnullable
e.g. "kit-and-ace.myshopify.com"

Option 2: null

countrystringnullable

The shopper's country, ISO 3166-1 alpha-2 in any case (US, ca). On a Shopify storefront, read it from Shopify.country. Every product the agent finds, shows or puts in a look is priced in that shopper's market: price, currency, each variant's price, compareAtPrice and inStock, the sizes it offers, the prices on cart actions, and onSale / discount. The lookup_products tool's minPrice and maxPrice are in that currency, and its onSale argument finds products reduced for this shopper. Products their market does not sell are not shown. Omitted, or for a country the store does not sell into, products carry the store's default market's prices, flagged product.marketExact: false. A value that is not a two-letter code is treated as unknown, not refused.

default: nulle.g. "US"

Response

200With stream omitted or true: a Server-Sent Events stream. With stream: false: one JSON body describing the whole turn.

Response headers

X-RateLimit-Resource

The quota resource this endpoint is metered against: api.v2.agent.chat for the chat endpoint, api.v2.agent.looks for the looks endpoint and api.v2.agent.match for the shop-the-look endpoint. These are separate from the v1 resources, so Agent API usage never draws down a v1 allowance.

X-RateLimit-Quota-Limit

Requests allowed in the current billing period, or unlimited.

X-RateLimit-Quota-Remaining

Requests left in the current billing period, or unlimited.

X-RateLimit-Quota-Reset

Unix epoch seconds at which the billing period resets.

X-RateLimit-RPM-Limit

Requests allowed per minute for this resource.

X-RateLimit-RPM-Remaining

Requests left in the current one-minute window.

X-RateLimit-RPM-Reset

Unix epoch seconds at which the one-minute window resets.

text/event-stream
string

event: <name>\ndata: <json>\n\n frames, one per event, in the order listed under Stream events. The stream closes after done (success) or error (failure). Cache-Control: no-cache and Connection: keep-alive are set.

application/json
outputstringrequired

The assistant's final text. May be empty when a row or a card was the whole answer.

e.g. "All three are linen, so they will breathe on a warm afternoon."
array<UserMessage | AssistantMessage | FunctionCall | FunctionCallResult>required

Replay-ready conversation. Send it back as messages next turn.

array<SearchResults>required

Every product row the agent displayed, in order.

labelstringrequired

Heading for the row.

e.g. "Wedding shirts"
kindstringrequired

outfit — worn together; list — alternatives to choose between.

One ofoutfitlist
e.g. "list"
querystringrequired

Repeats label.

e.g. "Wedding shirts"
countintegerrequired

Number of entries in results.

e.g. 3
array<Product>required
array<Look>required

Every look the agent composed. Render each with the looks endpoint.

labelstringrequired

Name of the look; pass it to the looks endpoint.

e.g. "Garden wedding"
array<Product>required

The garments worn together, as full Product records. Pass their itemIds to the looks endpoint.

minItems: 2maxItems: 6
customInstructionsstringnullablerequired

The agent's direction for the renderer — a garment the shopper owns, how pieces sit, the setting. Pass it through to the looks endpoint unchanged.

e.g. "Shirt untucked with the top button open; outdoor daylight setting."
array<QuestionCard>required

Question cards the agent asked (at most one per turn in practice).

questionIdstring (uuid)required
e.g. "5f0e8c3a-2b1d-4e7f-9a6c-3d2e1f0a9b8c"
questionstringrequired
e.g. "Who am I styling for?"
array<QuestionOption>required
minItems: 2maxItems: 10
allowFreeTextbooleanrequired

Whether the shopper may type their own answer instead of picking an option.

e.g. false
array<object>

Present only when the card asks more than one question. Every question, in order, including the first — which question, options and allowFreeText above repeat. Answer all of them and reply once, a line per step: <topic>: <label>.

minItems: 2maxItems: 6
suggestionsarray<string>required

Follow-up chips from suggest_replies.

Stream events

13

Each frame is event: <name> followed by data: <json> and a blank line. Events are listed in the order they normally arrive.

thinking

The model started (status: started) or finished (status: done) a stretch of reasoning. Sent only on a turn that reasons — every turn by default, none of them when the request sets thinking to none. Show a "thinking" state between the two — a summary may or may not follow, so this pair is the reliable signal. The stretch ends when text or a tool call begins.

data
{
  "status": "started"
}
thinking_delta

A chunk of the model's reasoning summary, in order, between a thinking start and done. Written for a reader, not the raw chain of thought, and not part of the reply. Only on a turn that reasons.

data
{
  "delta": "They want a dinner look; I'll check dresses and heels first."
}
thinking_break

The reasoning summary began a new paragraph. The payload is empty. Only on a turn that reasons.

data
{}
text_delta

A chunk of assistant text, in order. Append the chunks to render the reply as it is written. Text can arrive in more than one run inside a turn — typically a short opener before the tools run, and a closing line after — so treat a tool_called event as the end of the current paragraph.

data
{
  "delta": "Let me pull together a few options."
}
tool_called

The model invoked a tool. arguments is the raw JSON string the model produced (parse it if you need the values), or null. Useful for showing progress — for instance "Searching…" while lookup_products runs. Tool names: get_articles, get_colors, get_details, lookup_products, find_product, show_products, create_looks, ask_question, suggest_replies; list_store_pages and read_store_page when the organization's catalogue is synced from a Shopify store; and — only when a cart was sent — get_cart, add_to_cart, set_cart_quantity, change_cart_size, remove_from_cart. Anything a tool draws on screen (search_results, looks, question, suggestions, cart_action) is emitted between that tool's tool_called and tool_output. When the model calls several tools in one step, every tool_called for that step arrives first, then the tools' display events in the order the tools produce them, then every tool_output — so match display events by their type, not by position.

data
{
  "name": "lookup_products",
  "arguments": "{\"query\":\"linen wedding shirt\",\"slot\":null,\"family\":[\"shirt\"],\"age_group\":null,\"gender\":\"mens\",\"colors\":null,\"pattern\":null,\"fit\":null,\"materials\":[\"linen\"],\"features\":null,\"minPrice\":null,\"maxPrice\":300,\"onSale\":null,\"exclude_terms\":null,\"limit\":8}"
}
search_results

A row of products to display, from show_products. kind is outfit when the items are meant to be worn together (at most one per slot) or list when they are alternatives to choose between — render them differently. query repeats label. results holds full Product records in the order the agent chose, which is part of the recommendation. The agent may emit several rows in one turn.

data
{
  "label": "Wedding shirts",
  "kind": "list",
  "query": "Wedding shirts",
  "count": 3,
  "results": [
    {
      "itemId": "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
      "datasetId": "9c21f0aa-3e4b-4d2a-b6d1-0f7e8a9b1c2d",
      "status": "active",
      "metadata": {
        "gender": "mens",
        "color_family": "navy",
        "slot": "top",
        "family": "shirt"
      },
      "product": {
        "name": "Coastal Linen Shirt",
        "url": "https://shop.example.com/products/coastal-linen-shirt",
        "price": 89,
        "currency": "USD",
        "variants": [
          {
            "variantId": "gid://shopify/ProductVariant/41611609636909",
            "sku": "CLS-NVY-M",
            "label": "M / Navy",
            "options": [
              {
                "name": "Size",
                "value": "M"
              },
              {
                "name": "Color",
                "value": "Navy"
              }
            ],
            "inStock": true,
            "quantity": 4,
            "price": 89,
            "compareAtPrice": null
          }
        ],
        "assets": {
          "images": [
            {
              "uploaded": {
                "key": "org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg"
              }
            }
          ]
        }
      }
    }
  ]
}
suggestions

Two to four follow-up chips, written in the shopper's voice. Emitted by suggest_replies, and also alongside search_results and looks when the agent attached chips to them. When tapped, send the chip text as the next user message. A turn can emit this more than once; keep the latest. Never shown together with a question card.

data
{
  "suggestions": [
    "Show me trousers to match",
    "Something more formal",
    "Cheaper options"
  ]
}
question

A multiple-choice card from ask_question. The agent has stopped to wait for an answer, so the turn ends shortly after this event. Render the options; when the shopper picks one, send the option's label (or their typed text if allowFreeText) as the next user message. questionId is unique per card, for your own bookkeeping. A card can carry several questions — typically a size for each piece of an outfit being added to the cart. It then has a steps array holding every question, the first included, and question, options and allowFreeText repeat the first so a client that ignores steps still shows something answerable. Collect an answer to every step and send them as ONE user message, one line per step in order: <topic>: <label>, e.g. Shirt size: M / White. Do not send them as separate messages; the agent acts on the full set.

data
{
  "questionId": "5f0e8c3a-2b1d-4e7f-9a6c-3d2e1f0a9b8c",
  "question": "Who am I styling for?",
  "options": [
    {
      "label": "Menswear",
      "value": "mens"
    },
    {
      "label": "Womenswear",
      "value": "womens"
    }
  ],
  "allowFreeText": false
}
looks

One or more complete outfits the agent wants rendered. Each look carries its label, its full Product records in items (two to six per look), and customInstructions — the agent's direction for anything the product photos cannot show, or null. Nothing has been rendered yet: call POST /v2/agent/looks once per look, passing the items' itemIds, the label and customInstructions, and show each image as it returns. The items may include pieces that were never shown as cards.

data
{
  "looks": [
    {
      "label": "Garden wedding",
      "items": [
        {
          "itemId": "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
          "product": {
            "name": "Coastal Linen Shirt",
            "price": 89,
            "currency": "USD"
          }
        },
        {
          "itemId": "1b7e33c0-9a2d-4f8e-b5c6-7d8e9f0a1b2c",
          "product": {
            "name": "Everyday Chino",
            "price": 79,
            "currency": "USD"
          }
        },
        {
          "itemId": "c4d5e6f7-0a1b-4c2d-8e9f-0a1b2c3d4e5f",
          "product": {
            "name": "Suede Loafer",
            "price": 129,
            "currency": "USD"
          }
        }
      ],
      "customInstructions": "Shirt untucked with the top button open; outdoor daylight setting."
    }
  ]
}
cart_action

The agent read or wants to change the shopper's cart. Only possible when the request carried a cart snapshot. action is one of read, add, quantity, swap or remove, and the rest of the payload depends on it — see the CartAction schema. A read is already satisfied (it was answered from your snapshot) and can be shown as a receipt. Every other action is a request: the server has verified the item, variant and stock, but your client must perform the change on the storefront and show the outcome. add and swap carry an intent token (or null when no sessionId was sent) for reporting the result — see the cart guide.

data
{
  "action": "add",
  "itemId": "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
  "name": "Coastal Linen Shirt",
  "url": "https://shop.example.com/products/coastal-linen-shirt",
  "sku": "CLS-NVY-M",
  "size": "M / Navy",
  "quantity": 1,
  "price": 89,
  "currency": "USD",
  "image": "org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg",
  "intent": "eyJ2IjoxLCJqdGkiOiI3ZjFlIn0.q8vT2rN9…"
}
tool_output

A tool call finished. output is the value the model saw — usually an object with a note written for the model, sometimes an error key when the tool could not do its job (for example an invalid dataset scope). Product searches return compact summaries here, never full products; the full records travel on search_results. output is a plain string rather than an object when the tool could not run at all — for example when the model produced arguments that failed validation, in which case it reads An error occurred while running the tool. Please try again. Error: …. The agent sees that text and usually retries; it does not end the stream.

data
{
  "name": "lookup_products",
  "output": {
    "count": 8,
    "items": [
      {
        "id": "8f2c1a9e-4b7d",
        "description": "Coastal Linen Shirt — navy, linen, relaxed fit, $89"
      }
    ],
    "note": "The shopper saw NOTHING — this was for your eyes only."
  }
}
done

The turn finished. output is the assistant's final text (may be an empty string when a card or a row was the whole answer) and history is the complete, replay-ready conversation — every user message, assistant message, tool call and tool result. Store `history` and send it back as `messages` on the next turn. The stream closes after this event.

data
{
  "output": "All three are linen, so they will breathe on a warm afternoon.",
  "history": [
    {
      "role": "user",
      "content": "I need something for a summer wedding, menswear, under $300."
    },
    {
      "type": "message",
      "role": "assistant",
      "status": "completed",
      "content": [
        {
          "type": "output_text",
          "text": "Let me pull together a few options."
        }
      ]
    },
    {
      "type": "function_call",
      "callId": "call_8kP2wN7r",
      "name": "show_products",
      "arguments": "{\"label\":\"Wedding shirts\",\"kind\":\"list\",\"itemIds\":[\"8f2c1a9e-4b7d\",\"1b7e33c0-9a2d\",\"c4d5e6f7-0a1b\"],\"suggestions\":[\"Show me trousers to match\",\"Something more formal\"]}",
      "status": "completed"
    },
    {
      "type": "function_call_result",
      "callId": "call_8kP2wN7r",
      "name": "show_products",
      "status": "completed",
      "output": {
        "type": "text",
        "text": "{\"shown\":3,\"note\":\"3 cards are now on the shopper's screen under \\\"Wedding shirts\\\".\"}"
      }
    },
    {
      "type": "message",
      "role": "assistant",
      "status": "completed",
      "content": [
        {
          "type": "output_text",
          "text": "All three are linen, so they will breathe on a warm afternoon."
        }
      ]
    }
  ]
}
error

The run failed after the stream had started — for example the model exceeded the ten-call cap or the model provider returned an error. The HTTP status is already 200; this event is the only signal. The message is always Agent run failed.; the cause is logged on our side. No done follows and no history is returned, so replay the previous turn's history plus the shopper's message to retry. The stream closes after this event.

data
{
  "error": "Agent run failed."
}

Errors

21

Every error is JSON with a single error string unless noted. Match on the status code; the message is for humans.

400
Request body must be valid JSON.

The body could not be parsed as JSON.

400
Request body must be a JSON object.

The body is empty, or is valid JSON that is not an object (for example an array).

422
messages must be an array.

messages is missing or not a JSON array. Returned as JSON even when streaming was requested.

422
messages cannot be empty.

messages is an empty array. Returned as JSON even when streaming was requested.

curl -X POST "https://stylor.ai/api/v2/agent/chat" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -N \
  -d '{
  "messages": [
    {
      "role": "user",
      "content": "I need something for a summer wedding, menswear, under $300."
    }
  ],
  "datasetIds": [
    "9c21f0aa-3e4b-4d2a-b6d1-0f7e8a9b1c2d"
  ],
  "sessionId": "3b0d4a1e-6f52-4b8a-9d3c-7e2f1a0b9c8d",
  "session": {
    "surface": "widget",
    "agentVersion": "v2",
    "storageMode": "local",
    "tz": "America/Toronto",
    "locale": "en-CA",
    "device": "desktop"
  }
}'
event: thinking
data: {"status":"started"}

event: thinking_delta
data: {"delta":"They want a dinner look; I'll check dresses and heels first."}

event: thinking_break
data: {}

event: text_delta
data: {"delta":"Let me pull together a few options."}

event: tool_called
data: {"name":"lookup_products","arguments":"{\"query\":\"linen wedding shirt\",\"slot\":null,\"family\":[\"shirt\"],\"age_group\":null,\"gender\":\"mens\",\"colors\":null,\"pattern\":null,\"fit\":null,\"materials\":[\"linen\"],\"features\":null,\"minPrice\":null,\"maxPrice\":300,\"onSale\":null,\"exclude_terms\":null,\"limit\":8}"}

event: search_results
data: {"label":"Wedding shirts","kind":"list","query":"Wedding shirts","count":3,"results":[{"itemId":"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90","datasetId":"9c21f0aa-3e4b-4d2a-b6d1-0f7e8a9b1c2d","status":"active","metadata":{"gender":"mens","color_family":"navy","slot":"top","family":"shirt"},"product":{"name":"Coastal Linen Shirt","url":"https://shop.example.com/products/coastal-linen-shirt","price":89,"currency":"USD","variants":[{"variantId":"gid://shopify/ProductVariant/41611609636909","sku":"CLS-NVY-M","label":"M / Navy","options":[{"name":"Size","value":"M"},{"name":"Color","value":"Navy"}],"inStock":true,"quantity":4,"price":89,"compareAtPrice":null}],"assets":{"images":[{"uploaded":{"key":"org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg"}}]}}}]}

event: suggestions
data: {"suggestions":["Show me trousers to match","Something more formal","Cheaper options"]}

event: question
data: {"questionId":"5f0e8c3a-2b1d-4e7f-9a6c-3d2e1f0a9b8c","question":"Who am I styling for?","options":[{"label":"Menswear","value":"mens"},{"label":"Womenswear","value":"womens"}],"allowFreeText":false}

event: looks
data: {"looks":[{"label":"Garden wedding","items":[{"itemId":"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90","product":{"name":"Coastal Linen Shirt","price":89,"currency":"USD"}},{"itemId":"1b7e33c0-9a2d-4f8e-b5c6-7d8e9f0a1b2c","product":{"name":"Everyday Chino","price":79,"currency":"USD"}},{"itemId":"c4d5e6f7-0a1b-4c2d-8e9f-0a1b2c3d4e5f","product":{"name":"Suede Loafer","price":129,"currency":"USD"}}],"customInstructions":"Shirt untucked with the top button open; outdoor daylight setting."}]}

event: cart_action
data: {"action":"add","itemId":"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90","name":"Coastal Linen Shirt","url":"https://shop.example.com/products/coastal-linen-shirt","sku":"CLS-NVY-M","size":"M / Navy","quantity":1,"price":89,"currency":"USD","image":"org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg","intent":"eyJ2IjoxLCJqdGkiOiI3ZjFlIn0.q8vT2rN9…"}

event: tool_output
data: {"name":"lookup_products","output":{"count":8,"items":[{"id":"8f2c1a9e-4b7d","description":"Coastal Linen Shirt — navy, linen, relaxed fit, $89"}],"note":"The shopper saw NOTHING — this was for your eyes only."}}

event: done
data: {"output":"All three are linen, so they will breathe on a warm afternoon.","history":[{"role":"user","content":"I need something for a summer wedding, menswear, under $300."},{"type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Let me pull together a few options."}]},{"type":"function_call","callId":"call_8kP2wN7r","name":"show_products","arguments":"{\"label\":\"Wedding shirts\",\"kind\":\"list\",\"itemIds\":[\"8f2c1a9e-4b7d\",\"1b7e33c0-9a2d\",\"c4d5e6f7-0a1b\"],\"suggestions\":[\"Show me trousers to match\",\"Something more formal\"]}","status":"completed"},{"type":"function_call_result","callId":"call_8kP2wN7r","name":"show_products","status":"completed","output":{"type":"text","text":"{\"shown\":3,\"note\":\"3 cards are now on the shopper's screen under \\\"Wedding shirts\\\".\"}"}},{"type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"All three are linen, so they will breathe on a warm afternoon."}]}]}

event: error
data: {"error":"Agent run failed."}