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 thisStore 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);
}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
| Event | When | Times |
|---|---|---|
start | the turn is accepted, with its turnId | exactly once, first |
progress | the agent's step list changed | zero or more |
tool | a tool call begins, and ends | paired |
text | a piece of the reply | zero or more |
file | the agent wrote a file you can read | once per file |
widget | a card, table or chart in the reply grew (only with widgets: true) | zero or more |
done | the reply is final | once, or error |
error | the turn failed, and the stream ends | once, 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));
}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.
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 ownEvery conversation has its own history and its own filesystem. Two conversations share nothing.