AgentNava · Run

Conversations

One conversation holds its own history, its own files and the version it pinned.

Starting one

const conversation = await agent.start({ endUserId: 'u_ananya' });
conversation.id;   // 'conv_5d3a'  store this

Store conversation.id against whoever you are talking to. Every later call is a method on it, and asking again is just the next turn. There is no resume call, because once you hold the id there is nothing to resume to.

endUserId is required: every conversation belongs to someone. Use your own id for the person, or a name for a job such as 'nightly-report'. Ids starting with member:, visitor: or job: (or the older webhook:, builder:, librarian:, in any case) are refused, because those name people and work the platform tracks itself.

const conversation = ws.agent(row.agentId).conversation(row.conversationId);   // no network call
await conversation.ask(message);

agent.start(...) always starts a new conversation. Which id you stored decides what you get: an agent id gives you a new conversation, a conversation id continues an existing one.

Asking

One method. Await it for the reply, or iterate it for the events as they happen.

const reply = await conversation.ask('Order 4471 was late. Refund?');
for await (const e of conversation.ask('...')) {
  if (e.type === 'text')     append(e.text);
  if (e.type === 'progress') showSteps(e.steps);
  if (e.type === 'file')     showArtifact(e.path);
  if (e.type === 'done')     save(e.reply);
}
Ask once

The message is sent when you call ask, not when you await or iterate, so calling it twice asks twice. Awaiting and iterating the same turn throws.

Events

EventWhenTimes
startthe turn is accepted, with its turnIdexactly once, first
progressthe agent's step list changedzero or more
toola tool call begins, and endspaired
texta piece of the replyzero or more
filethe agent wrote a file you can readonce per file
widgeta card, table or chart in the reply grew (only with widgets: true)zero or more
donethe reply is finalonce, or error
errorthe turn failed, and the stream endsonce, or done

text arrives in fragments. Append each one as it comes. A short reply can arrive as one fragment. done carries the whole reply, so a client that only wants the answer can ignore every other event and still be correct.

A progress event carries the whole step list, every time. Replace what you were showing. That is what makes reconnecting safe: one event and you are correct again.

The steps are the ones the AgentNava app shows while an agent works, in the same words: one per tool call, in the order they started. Each has an id, a label such as Searching the web: steel tariffs, a kind for an icon (read, web, shell and so on), and a status of active, done or error. A call that only draws on screen is not a step.

A tool event carries the tool's name, for your code, and the same label, for a person. A file event's path is what conversation.readFile() takes.

for await (const e of conversation.ask('Research steel tariffs and save a summary.')) {
  if (e.type === 'progress') {
    for (const step of e.steps) console.log(step.status, step.label);
  }
  if (e.type === 'file') console.log(await conversation.readFile(e.path));
}
Your connection can drop

It runs on our side. If your connection drops it keeps going and settles, read the answer with conversation.messages() when you reconnect.

Widgets

If your screen can draw cards, tables and charts, start the conversation with widgets: true, and the agent may answer with them, unless its Widgets setting is off. Continue one the same way, with agent.conversation(id, { widgets: true }).

const conversation = await agent.start({ endUserId: row.userId, widgets: true });

for await (const e of conversation.ask('How did October spend compare with budget?')) {
  if (e.type === 'widget') draw(e.widget);           // sent whole each time: replace by surfaceId
  if (e.type === 'done')   keep(e.reply, e.widgets);  // the final list
}

Without widgets: true nothing changes: no widget events, and the agent is never asked to draw.

Ready-made React views

On a React screen, the SDK draws widgets for you. ReplyView shows a reply's words with each widget in place, in a default look you can theme with CSS variables. React is optional: only this entry needs it.

import { ReplyView } from '@cerebro-labs/agentnava-sdk/react';
import '@cerebro-labs/agentnava-sdk/react/styles.css';

<ReplyView text={reply} widgets={widgets} onAsk={send} />

onAsk receives a pressed button's label or a tapped suggestion, to send as the next message. Pass renderText to draw the words with your own markdown renderer, currency and locale for numbers, and parts to swap any kind of part for your own component, such as parts={{ Stat: MyStat }}. WidgetView draws a single widget.

Drawing a widget yourself

Widgets use A2UI, Google's open format for interfaces an agent draws, with AgentNava's own set of parts. A widget is a flat list of parts. Each part has an id and a component, which says what kind of part it is. Containers name their children by id, and the part called root is the top. at is where the widget sits in the reply's text, as a character count, so it goes between the right sentences. closed is false while it is still arriving.

{
  surfaceId: 'w1',
  at: 31,
  closed: true,
  components: [
    { id: 'root',   component: 'Row',  children: ['earned', 'casual'] },
    { id: 'earned', component: 'Stat', value: '9 days', label: 'Earned leave', caption: '4 pending' },
    { id: 'casual', component: 'Stat', value: '7 days', label: 'Casual leave' },
  ],
}

To draw one, start at root, draw its part, and draw each child the same way, mapping each kind (Stat, Table, Chart and so on) to a component in your own look.

Draw it defensively

A widget is written by the model, so treat it like any other input. Skip a kind you do not know and a part that is missing a field. Draw each part once, even if two containers name it. Open only https:// links and pictures. A widget you cannot draw at all can simply be left out: the reply's words still make sense without it.

Sending an image or a file

await conversation.ask('What is wrong in this screenshot?', {
  attachments: [{ kind: 'image', mimeType: 'image/png', data: base64 }],
});

await conversation.ask('Which of these are overdue?', {
  attachments: [{ kind: 'file', name: 'invoices.csv',
                  mimeType: 'text/csv', data: base64 }],
});

An image goes to the model as an image. Anything else is written into the conversation's drive before the turn starts, so the agent can parse it, compute over it, and still have it next turn.

Reading it back

await conversation.messages();   // [{ role, content, at, widgets? }, ...]
await agent.conversations();     // this agent's newest 50, each with its startedAt and title
await agent.conversations({ of: 'customers', limit: 5 });   // only your end users', not the team's own

Every conversation has its own history and its own filesystem. Two conversations share nothing.