Replay a bad answer
By the end of this tutorial you will have a bad answer replaced on its own history — the same conversation, branched at the customer's question and answered again by a fixed agent, with the original left intact beside it.
Retyping the conversation from memory tests a paraphrase. Forking the session tests the exact context the agent saw.
Eight steps:
- Start a session for the agent.
- Send the first message.
- Ask the question that goes wrong.
- Read the bad turn back.
- Find where to branch.
- Write the fix as a new agent.
- Fork at the question.
- Answer again on the fork, then prove the branch.
Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.
Prerequisites
- A
nat_sk_…API key exported asNATURALI_TOKEN, and a client set up as in Create a provider. - The agent from Your first agent generation,
with
PROJECT,PROVIDERandAGENTexported. Its instructions carry no refund policy, which is what makes its answer below go wrong.
export NATURALI_TOKEN=nat_sk_...
export PROJECT=proj_V1StGXR8Z5jdHi6B
export PROVIDER=aip_V1StGXR8Z5jdHi6B
export AGENT=agent_V1StGXR8Z5jdHi6B
Every reply below is one generation: three in all.
1. Start a session
A session holds a conversation
with the agent.
auto_generate: true
makes each customer message answer itself, so every
message in steps 2 and 3 is one call.
- CLI
- SDK
- curl
naturali create-session \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--name ticket-4471 \
--auto-generate true
const { data: session } = await naturali.sessions.createSession({
path: { project_id: process.env.PROJECT! },
body: {
agent_id: process.env.AGENT!,
name: 'ticket-4471',
auto_generate: true,
},
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/sessions" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"agent_id\": \"$AGENT\",
\"name\": \"ticket-4471\",
\"auto_generate\": true
}"
{
"id": "sess_mbfVgSKRIKHn7wOZ",
"agent_id": "agent_wqZgqIhg278sf9eR",
"conversation_id": "conv_sBuVIHG9g1WSQF3s",
"forked_from_session_id": null,
"forked_from_position": null,
"status": "open",
"name": "ticket-4471",
"auto_generate": true
}
export SESSION=sess_mbfVgSKRIKHn7wOZ
export CONVERSATION=conv_sBuVIHG9g1WSQF3s
2. Send the first message
POST /v1/projects/{project_id}/sessions/{session_id}/messages
saves the customer's message and, with auto_generate on, returns the agent's
reply.
- CLI
- SDK
- curl
naturali add-session-message \
--project-id "$PROJECT" \
--session-id "$SESSION" \
--message 'Hi, my order 4471 arrived with a cracked screen.'
const { data: first } = await naturali.sessions.addSessionMessage({
path: { project_id: process.env.PROJECT!, session_id: process.env.SESSION! },
body: { message: 'Hi, my order 4471 arrived with a cracked screen.' },
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/sessions/$SESSION/messages" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "message": "Hi, my order 4471 arrived with a cracked screen." }'
{
"status": "completed",
"message": {
"role": "assistant",
"content": "Sorry to hear that, as it's a shipping issue, but since the package arrived damaged, we can definitely process a refund or credit for that order."
},
"generation_id": "gen_GcJ9Uzq1K9lF7r1P",
"trace_id": "trace_XDhQmTFjq9dxjoLy"
}
3. Ask the question that goes wrong
- CLI
- SDK
- curl
naturali add-session-message \
--project-id "$PROJECT" \
--session-id "$SESSION" \
--message 'So what do I do now? Do I get a refund or not?'
const { data: bad } = await naturali.sessions.addSessionMessage({
path: { project_id: process.env.PROJECT!, session_id: process.env.SESSION! },
body: { message: 'So what do I do now? Do I get a refund or not?' },
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/sessions/$SESSION/messages" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "message": "So what do I do now? Do I get a refund or not?" }'
{
"status": "completed",
"message": {
"role": "assistant",
"content": "I can process a refund for a cracked screen issue, but have you tried shipping the device back so the warehouse can verify the damage first?"
},
"generation_id": "gen_WnItK9ZWP1zvPcko",
"trace_id": "trace_7lM5b1PKjCYAyZWT"
}
That is the bad answer: the customer asked yes or no, and the agent hedged and invented a step — ship it back first — that no policy asks for. Your model may word it differently; any answer that dodges the question will do.
export GENERATION=gen_WnItK9ZWP1zvPcko
4. Read the bad turn back
GET /v1/projects/{project_id}/generations/{generation_id}/transcript
shows exactly what the model was given and what it returned — see
Reading a run back, step by step.
- CLI
- SDK
- curl
naturali get-generation-transcript \
--project-id "$PROJECT" \
--generation-id "$GENERATION"
const { data: transcript } = await naturali.generations.getGenerationTranscript(
{
path: {
project_id: process.env.PROJECT!,
generation_id: process.env.GENERATION!,
},
}
);
curl "https://api.naturali.ai/v1/projects/$PROJECT/generations/$GENERATION/transcript" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"generation_id": "gen_WnItK9ZWP1zvPcko",
"agent_id": "agent_wqZgqIhg278sf9eR",
"agent_version": 1,
"status": "completed",
"step_count": 1,
"input": [
{
"role": "system",
"content": "Answer in one sentence. If you are unsure, say so.\n\nYou are refund-explainer. Reply as this participant only — do not speak for any other actor."
},
{
"role": "user",
"content": "[participant]: Hi, my order 4471 arrived with a cracked screen."
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "Sorry to hear that, as it's a shipping issue, but since the package arrived damaged, we can definitely process a refund or credit for that order."
}
]
},
{
"role": "user",
"content": "[participant]: So what do I do now? Do I get a refund or not?"
}
],
"output": {
"content": "I can process a refund for a cracked screen issue, but have you tried shipping the device back so the warehouse can verify the damage first?",
"finish_reason": "stop"
}
}
The input names the cause: the instructions carry no refund policy, so the
model made one up. The fix is the instructions, not the conversation.
5. Find where to branch
A fork branches after a message
position.
GET /v1/projects/{project_id}/conversations/{conversation_id}/messages
lists them.
- CLI
- SDK
- curl
naturali list-conversation-messages \
--project-id "$PROJECT" \
--conversation-id "$CONVERSATION"
const { data: messages } =
await naturali.conversations.listConversationMessages({
path: {
project_id: process.env.PROJECT!,
conversation_id: process.env.CONVERSATION!,
},
});
curl "https://api.naturali.ai/v1/projects/$PROJECT/conversations/$CONVERSATION/messages" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"data": [
{
"position": 0,
"role": "user",
"document_id": "doc_Mvb2wcDMJ0mCpaY9",
"content": "Hi, my order 4471 arrived with a cracked screen."
},
{
"position": 1,
"role": "assistant",
"document_id": "doc_ddGeJI4bF68l70tt",
"content": "Sorry to hear that, as it's a shipping issue, but since the package arrived damaged, we can definitely process a refund or credit for that order."
},
{
"position": 2,
"role": "user",
"document_id": "doc_P6hzgzI9kxCHjTbE",
"content": "So what do I do now? Do I get a refund or not?"
},
{
"position": 3,
"role": "assistant",
"document_id": "doc_4scGDjrKPeiqnJup",
"content": "I can process a refund for a cracked screen issue, but have you tried shipping the device back so the warehouse can verify the damage first?"
}
],
"total": 4
}
Position 2 is the customer's question. Branching there keeps everything up to it and drops the answer you are replacing.
6. Write the fix as a new agent
Give the policy to a second agent, so the original keeps serving — and stays available to compare against — while you try the fix.
- CLI
- SDK
- curl
naturali create-agent \
--project-id "$PROJECT" \
--ai-provider-id "$PROVIDER" \
--name refund-explainer-v2 \
--instructions "Answer in two short sentences. Policy: an item that arrives damaged gets a full refund or a free replacement, the customer's choice, and nothing needs to be sent back. Ask for a photo of the damage."
const { data: fixed } = await naturali.agents.createAgent({
path: { project_id: process.env.PROJECT! },
body: {
ai_provider_id: process.env.PROVIDER!,
name: 'refund-explainer-v2',
instructions:
"Answer in two short sentences. Policy: an item that arrives damaged gets a full refund or a free replacement, the customer's choice, and nothing needs to be sent back. Ask for a photo of the damage.",
},
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/agents" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"ai_provider_id\": \"$PROVIDER\",
\"name\": \"refund-explainer-v2\",
\"instructions\": \"Answer in two short sentences. Policy: an item that arrives damaged gets a full refund or a free replacement, the customer's choice, and nothing needs to be sent back. Ask for a photo of the damage.\"
}"
{
"id": "agent_ixltZtxnUCYlkZBV",
"name": "refund-explainer-v2",
"instructions": "Answer in two short sentences. Policy: an item that arrives damaged gets a full refund or a free replacement, the customer's choice, and nothing needs to be sent back. Ask for a photo of the damage.",
"version": 1
}
export FIXED_AGENT=agent_ixltZtxnUCYlkZBV
7. Fork at the question
POST /v1/projects/{project_id}/sessions/{session_id}/fork
branches the session after fork_at_position, and agent_id sets who answers
on the branch.
- CLI
- SDK
- curl
naturali fork-session \
--project-id "$PROJECT" \
--session-id "$SESSION" \
--fork-at-position 2 \
--agent-id "$FIXED_AGENT" \
--name ticket-4471-retry
const { data: fork } = await naturali.sessions.forkSession({
path: { project_id: process.env.PROJECT!, session_id: process.env.SESSION! },
body: {
fork_at_position: 2,
agent_id: process.env.FIXED_AGENT!,
name: 'ticket-4471-retry',
},
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/sessions/$SESSION/fork" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"fork_at_position\": 2,
\"agent_id\": \"$FIXED_AGENT\",
\"name\": \"ticket-4471-retry\"
}"
{
"id": "sess_ME7EHfzUrcgW74HT",
"agent_id": "agent_ixltZtxnUCYlkZBV",
"conversation_id": "conv_SU0DID9CkMKdAByZ",
"forked_from_session_id": "sess_mbfVgSKRIKHn7wOZ",
"forked_from_position": 2,
"status": "open",
"name": "ticket-4471-retry",
"auto_generate": false
}
export FORK=sess_ME7EHfzUrcgW74HT
The fork's conversation holds positions 0 to 2 and points at the same
documents as the parent — doc_Mvb2wcDMJ0mCpaY9, doc_ddGeJI4bF68l70tt,
doc_P6hzgzI9kxCHjTbE — so the history cannot drift from what happened. It is
created inert (auto_generate: false): nothing runs until you ask.
8. Answer again on the fork
POST /v1/projects/{project_id}/sessions/{session_id}/generate
answers the customer's question again, from the copied history:
- CLI
- SDK
- curl
naturali generate-session-response \
--project-id "$PROJECT" \
--session-id "$FORK" \
--wait true
const { data: retry } = await naturali.sessions.generateSessionResponse({
path: { project_id: process.env.PROJECT!, session_id: process.env.FORK! },
query: { wait: true },
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/sessions/$FORK/generate?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"status": "completed",
"message": {
"role": "assistant",
"content": "No, we don't require the item to be returned for a refund or replacement. Please attach a photo of the damage to your original email so we can take care of it right away."
},
"generation_id": "gen_HJEoOLi1M5JkMPis",
"trace_id": "trace_sOA19UFrqWc2I0Tv"
}
That is the proof: the same three messages, answered by the fixed agent, give
the policy instead of an invented step. The original session still holds all
four of its messages, and
GET /v1/projects/{project_id}/sessions/{session_id}/forks
on it lists the branch:
{
"data": [
{
"id": "sess_ME7EHfzUrcgW74HT",
"agent_id": "agent_ixltZtxnUCYlkZBV",
"forked_from_session_id": "sess_mbfVgSKRIKHn7wOZ",
"forked_from_position": 2,
"name": "ticket-4471-retry"
}
],
"total": 1
}
Tool results recorded on the copied messages are replayed as model input, so exploring a "what if" cannot send an email or charge a card a second time. The fork sees the tool data as it was, not as it is now.
What's next
- Score an agent change — promote the bad turn into a dataset item so the answer cannot regress quietly.
- Roll out an agent version — ship the fix to real traffic as a new version beside the old one.
- Debug a failed run — when the turn failed on a tool rather than on the instructions.
- Sessions — forks of forks, actors and
tool_contexton a branch.