Skip to content

Hold a conversation

Every turn belongs to a conversation. A run without a conversationId opens a new one, and its result carries the id. Pass the id with the next message, and the agent's model sees what was said before.

The scripts on this page read the API's URL and token from KINDGI_API_URL and KINDGI_API_TOKEN. With kindgi dev, they're the URL and token it prints (also in the pack's .kindgirc.json).

scripts/chat.ts
import { createClient } from '@kindgi/sdk/client';
import type { ThreadId } from '@kindgi/sdk/types';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
type Turn = { conversationId: ThreadId; turnNumber: number; response: { content: string } };
const first = await kindgi.runs.start({
agent: 'acme.order-desk',
input: { userMessage: 'Hi, I am Ada. My order is A-1002.' },
});
const { conversationId } = first.output as Turn;
const second = await kindgi.runs.start({
agent: 'acme.order-desk',
input: { userMessage: 'Has it shipped yet?', conversationId },
});
const turn = second.output as Turn;
console.log(turn.turnNumber, turn.response.content);
const history = await kindgi.conversations.messages(conversationId);
for (const m of history.data) console.log(m.sequence, m.role, JSON.stringify(m.content));
Terminal window
node scripts/chat.ts
2 No, your order A-1002 hasn't shipped yet—it's still in processing status. You'll receive an update once it's on its way.
0 user "Hi, I am Ada. My order is A-1002."
1 agent {"text":"","toolCalls":[{"id":"toolu_01LLRAWoi55mYXt2ztfW1Ap8","name":"acme.lookup-order","arguments":{"orderId":"A-1002"}}]}
2 tool {"eta":null,"status":"processing","orderId":"A-1002"}
3 agent "Hi Ada! Your order A-1002 is currently being processed. We don't have an estimated delivery date yet, but we'll update you once it ships."
4 user "Has it shipped yet?"
5 agent "No, your order A-1002 hasn't shipped yet—it's still in processing status. You'll receive an update once it's on its way."

The second message names no order, and the agent answers about A-1002: its model saw the first turn, including the tool call and its result. The history is every message in order: the user's, the agent's (a tool call or an answer) and each tool result. From the command line, pass the same field: --input='{"userMessage":"Has it shipped yet?","conversationId":"<id>"}'.

A long conversation makes every turn's prompt longer. historyLimit caps how many earlier messages a turn loads:

// in agents/order-desk/index.ts
conversationPolicy: { historyLimit: 20 },

The turn loads the last 20 messages before the new one; older ones stay in the conversation but aren't sent. Without historyLimit, every turn loads the whole history. To see what a turn sent, read its journal (kindgi runs journal <run-id>): the build-initial-messages step's output has the messages.

A conversation can also be opened before its first turn, with a title and the person it's with. Closing one ends it: a turn on a closed conversation is refused.

scripts/close.ts
import { KindgiApiError, createClient } from '@kindgi/sdk/client';
import type { AgentId } from '@kindgi/sdk/types';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
const conversation = await kindgi.conversations.open({
agentId: 'acme.order-desk' as AgentId,
agentVersion: '0.1.0',
title: 'Order A-1002',
participantId: 'customer-4411',
});
console.log(conversation.id, conversation.status, conversation.turnCount);
await kindgi.runs.start({
agent: 'acme.order-desk',
input: { userMessage: 'Where is order A-1002?', conversationId: conversation.id },
});
const closed = await kindgi.conversations.close(conversation.id);
console.log(closed.status, closed.turnCount);
try {
await kindgi.runs.start({
agent: 'acme.order-desk',
input: { userMessage: 'One more thing…', conversationId: conversation.id },
});
} catch (err) {
if (!(err instanceof KindgiApiError) || err.error.code !== 'conflict') throw err;
console.log(err.error.reason, err.message);
}
const list = await kindgi.conversations.list({ agent: 'acme.order-desk' as AgentId, status: 'closed' });
console.log(list.data.map((c) => c.id));
88356874-400d-4bcd-9708-63ab51f88dd3 open 0
closed 1
conversation-closed Conversation "88356874-400d-4bcd-9708-63ab51f88dd3" is closed
[
'88356874-400d-4bcd-9708-63ab51f88dd3',
…
]
  • open pins the agent and its version (below); title defaults to Untitled conversation. A conversation that a turn opens takes the agent's name as its title.
  • participantId is kept on the conversation. A first turn can set it too, with participantId in its input.
  • close is safe to repeat: closing a closed conversation returns it as it is.
  • list filters by agent and by status (open or closed), newest first; get fetches one conversation. From the CLI: kindgi conversations list --status=closed.
  • A comparison's replays have conversations too, and lists leave them out unless you ask: replays: 'include' or 'only' (--replays=include or --replays=only in the CLI). A replay's conversation has replayOf and evalRunId in its metadata.
  • A conversation is in a project: a run opens it in the run's project, and open takes a projectId (one of your tenant's projects; without one, the Default project, as for a run). list takes a scope for one project's conversations, or every project's in an org: { kind: 'project', projectId } (scope_kind="project", scope_id=…) or { kind: 'org', orgId }. A conversation from before Kindgi 0.1.3 has no project and is listed only without a scope.

From the CLI:

Terminal window
kindgi conversations open acme.order-desk 0.1.0 --title="Order A-1002" --participant=customer-4411
kindgi runs start --agent=acme.order-desk --agent-version=0.1.0 --input='{"userMessage":"Where is order A-1002?","conversationId":"<conversation-id>"}'
kindgi conversations messages <conversation-id> # oldest first
kindgi conversations get <conversation-id>
kindgi conversations close <conversation-id>

Over HTTP, these are POST /v1/conversations, GET /v1/conversations, GET /v1/conversations/{id}, POST /v1/conversations/{id}/close and GET /v1/conversations/{id}/messages.

Terminal window
kindgi conversations unregister <conversation-id>

It prints the conversation, now with unregisteredAt:

{
"id": "a74b27e5-9166-4b16-badd-258424f5ae20",
"tenantId": "9f1543c9-7b93-46ff-a5ff-205093a148bb",
"agentId": "acme.desk",
"agentVersion": "1.0.0",
"title": "acme.desk",
"participantId": "end-7",
"projectId": "aa0cf701-a8f7-40e5-b5d9-bec1a1cb1723",
"scope": {
"tenantId": "9f1543c9-7b93-46ff-a5ff-205093a148bb"
},
"status": "open",
"openedAt": "2026-10-09T13:30:24.826Z",
"unregisteredAt": "2026-10-09T13:30:39.043Z",
"turnCount": 1,
"lastMessageAt": "2026-10-09T13:30:32.233Z"
}

From then on, no read, list or recall returns it, and it takes no more turns. With authorization on, unregistering takes write on the conversation's project (an editor or above); anyone else is refused with 403 permission-denied, and the conversation stays. An id that doesn't exist, or a conversation already unregistered, answers 404. Its messages stay in the database until a retention policy on conversation purges them (Keep and purge deleted data). In your app: conversations.unregister; over HTTP, POST /v1/conversations/{id}/unregister.

To clear a person's words now, from every conversation and run that held them, erase their data.

A conversation is pinned to the agent version it was opened with, and its turns run that version. With kindgi dev, only the current version of an agent is registered, so after you change the agent's version, the next turn of an open conversation is refused:

Error [not-found]: The conversation was opened with agent "acme.order-desk@0.1.0", which is no longer registered.

A turn that names another version (--agent-version=0.2.0 in the CLI, agentVersion in runs.start, agent_version in Python) is refused too:

Error [agent-version-mismatch]: Conversation opened with acme.order-desk@0.1.0; invoked with acme.order-desk@0.2.0

On a runtime you deploy to, where both versions are registered, the conversation's turns go on with its version. With kindgi dev, start a new conversation. Bump the version when a change would break conversations under way, not on every edit.

A conversation can make a person approve each turn once it runs long, or before a tool runs: see Ask before a long conversation continues and Ask before a tool runs.