Debug Session, Generation, and Trace History
This tutorial teaches a practical debugging workflow for first-time SOAT users. You will build a traceable conversation and keep a deterministic mapping between:
- session_id
- generation_id
- trace_id
By the end, you will be able to:
- Start from a session and retrieve all messages.
- Track every generation ID for that session.
- Inspect each trace and trace tree.
- Reverse lookup from trace_id to generation_id and session_id using your debug ledger.
This workflow uses Sessions debugging links, Agent traces, Trace debugging joins, and Files examples.
Prerequisites
- A running SOAT instance. Follow Quick Start if needed.
- New to the platform? Read Key Concepts.
- For production hardening, read Advanced Configuration.
- Ollama available locally for this example.
- CLI
- SDK
- curl
export SOAT_BASE_URL=http://localhost:5047
import { SoatClient } from '@soat/sdk';
const soat = new SoatClient({ baseUrl: 'http://localhost:5047' });
export SOAT_URL=http://localhost:5047
Step 1 - Login as admin
Use the Users examples login flow to get a token.
- CLI
- SDK
- curl
ADMIN_TOKEN=$(soat login-user --username admin --password Admin1234! | jq -r '.token')
export SOAT_TOKEN=$ADMIN_TOKEN
const { data: login, error: loginError } = await soat.users.loginUser({
body: { username: 'admin', password: 'Admin1234!' },
});
if (loginError) throw new Error(JSON.stringify(loginError));
const adminSoat = new SoatClient({
baseUrl: 'http://localhost:5047',
token: login.token,
});
ADMIN_TOKEN=$(curl -s -X POST "$SOAT_URL/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin1234!"}' | jq -r '.token')
Step 2 - Create project, AI provider, agent, and session
Create the minimum resources from Projects examples, AI Providers examples, Agents examples, and Sessions examples.
This tutorial uses a local Ollama provider so it can run without external credentials. To connect xAI, OpenAI, Anthropic, or Amazon Bedrock instead, see Connect Third-Party LLMs.
- CLI
- SDK
- curl
PROJECT_ID=$(soat create-project --name "Debug Graph Demo" | jq -r '.id')
AI_PROVIDER_ID=$(soat create-ai-provider \
--project-id "$PROJECT_ID" \
--name "Local Ollama" \
--provider "ollama" \
--default-model "qwen2.5:0.5b" | jq -r '.id')
AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$AI_PROVIDER_ID" \
--name "Debug Assistant" \
--instructions "You are a concise debugging assistant." | jq -r '.id')
SESSION_ID=$(soat create-agent-session \
--agent-id "$AGENT_ID" \
--name "Debug Session" \
--auto-generate false | jq -r '.id')
echo "PROJECT_ID=$PROJECT_ID"
echo "AGENT_ID=$AGENT_ID"
echo "SESSION_ID=$SESSION_ID"
const { data: project } = await adminSoat.projects.createProject({
body: { name: 'Debug Graph Demo' },
});
const { data: provider } = await adminSoat.aiProviders.createAiProvider({
body: {
project_id: project.id,
name: 'Local Ollama',
provider: 'ollama',
default_model: 'qwen2.5:0.5b',
},
});
const { data: agent } = await adminSoat.agents.createAgent({
body: {
project_id: project.id,
ai_provider_id: provider.id,
name: 'Debug Assistant',
instructions: 'You are a concise debugging assistant.',
},
});
const { data: session } = await adminSoat.sessions.createAgentSession({
path: { agent_id: agent.id },
body: { name: 'Debug Session', auto_generate: false },
});
PROJECT_ID=$(curl -s -X POST "$SOAT_URL/api/v1/projects" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Debug Graph Demo"}' | jq -r '.id')
AI_PROVIDER_ID=$(curl -s -X POST "$SOAT_URL/api/v1/ai-providers" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"project_id\":\"$PROJECT_ID\",\"name\":\"Local Ollama\",\"provider\":\"ollama\",\"default_model\":\"qwen2.5:0.5b\"}" | jq -r '.id')
AGENT_ID=$(curl -s -X POST "$SOAT_URL/api/v1/agents" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"project_id\":\"$PROJECT_ID\",\"ai_provider_id\":\"$AI_PROVIDER_ID\",\"name\":\"Debug Assistant\",\"instructions\":\"You are a concise debugging assistant.\"}" | jq -r '.id')
SESSION_ID=$(curl -s -X POST "$SOAT_URL/api/v1/agents/$AGENT_ID/sessions" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Debug Session","auto_generate":false}' | jq -r '.id')
Step 3 - Run two generations and capture generation_id + trace_id
Use Sessions debugging links and Sessions async generation endpoints to produce assistant replies and capture the correlation IDs.
- CLI
- SDK
- curl
soat add-session-message \
--agent-id "$AGENT_ID" \
--session-id "$SESSION_ID" \
--message "Explain what a generation is in one sentence." > /dev/null
GEN_1=$(soat generate-session-response \
--agent-id "$AGENT_ID" \
--session-id "$SESSION_ID")
GEN_1_ID=$(printf '%s\n' "$GEN_1" | jq -r '.generation_id')
TRACE_1_ID=$(printf '%s\n' "$GEN_1" | jq -r '.trace_id')
soat add-session-message \
--agent-id "$AGENT_ID" \
--session-id "$SESSION_ID" \
--message "Now explain what a trace is in one sentence." > /dev/null
GEN_2=$(soat generate-session-response \
--agent-id "$AGENT_ID" \
--session-id "$SESSION_ID")
GEN_2_ID=$(printf '%s\n' "$GEN_2" | jq -r '.generation_id')
TRACE_2_ID=$(printf '%s\n' "$GEN_2" | jq -r '.trace_id')
printf '%s\n' "$GEN_1" | jq '{generation_id, trace_id, status}'
printf '%s\n' "$GEN_2" | jq '{generation_id, trace_id, status}'
await adminSoat.sessions.addSessionMessage({
path: { agent_id: agent.id, session_id: session.id },
body: { message: 'Explain what a generation is in one sentence.' },
});
const { data: gen1 } = await adminSoat.sessions.generateSessionResponse({
path: { agent_id: agent.id, session_id: session.id },
});
await adminSoat.sessions.addSessionMessage({
path: { agent_id: agent.id, session_id: session.id },
body: { message: 'Now explain what a trace is in one sentence.' },
});
const { data: gen2 } = await adminSoat.sessions.generateSessionResponse({
path: { agent_id: agent.id, session_id: session.id },
});
const debugLinks = [
{
sessionId: session.id,
generationId: gen1.generation_id,
traceId: gen1.trace_id,
},
{
sessionId: session.id,
generationId: gen2.generation_id,
traceId: gen2.trace_id,
},
];
curl -s -X POST "$SOAT_URL/api/v1/agents/$AGENT_ID/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Explain what a generation is in one sentence."}' > /dev/null
GEN_1=$(curl -s -X POST "$SOAT_URL/api/v1/agents/$AGENT_ID/sessions/$SESSION_ID/generate" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{}')
curl -s -X POST "$SOAT_URL/api/v1/agents/$AGENT_ID/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Now explain what a trace is in one sentence."}' > /dev/null
GEN_2=$(curl -s -X POST "$SOAT_URL/api/v1/agents/$AGENT_ID/sessions/$SESSION_ID/generate" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{}')
printf '%s\n' "$GEN_1" | jq '{generation_id, trace_id, status}'
printf '%s\n' "$GEN_2" | jq '{generation_id, trace_id, status}'
Step 4 - Retrieve the full session message timeline
Use Sessions key concepts and Sessions examples to inspect the canonical conversation history.
- CLI
- SDK
- curl
soat list-agent-session-messages \
--agent-id "$AGENT_ID" \
--session-id "$SESSION_ID" | jq '.data[] | {position, role, content}'
const { data: messagePage } = await adminSoat.sessions.listAgentSessionMessages(
{
path: { agent_id: agent.id, session_id: session.id },
query: { limit: 50, offset: 0 },
}
);
const timeline = messagePage.data.map((m) => ({
position: m.position,
role: m.role,
content: m.content,
}));
curl -s "$SOAT_URL/api/v1/agents/$AGENT_ID/sessions/$SESSION_ID/messages?limit=50&offset=0" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.data[] | {position, role, content}'
Step 5 - Inspect traces for each generation
Use Traces key concepts, Trace ancestry model, and Traces examples to inspect metadata and tree structure.
- CLI
- SDK
- curl
soat get-trace --trace-id "$TRACE_1_ID" | jq '{id, agent_id, file_id, parent_trace_id, root_trace_id, step_count}'
soat get-trace-tree --trace-id "$TRACE_1_ID" | jq '{id, children: [.children[].id]}'
const { data: trace1 } = await adminSoat.traces.getTrace({
path: { trace_id: gen1.trace_id },
});
const { data: traceTree1 } = await adminSoat.traces.getTraceTree({
path: { trace_id: gen1.trace_id },
});
curl -s "$SOAT_URL/api/v1/traces/$(printf '%s\n' "$GEN_1" | jq -r '.trace_id')" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '{id, agent_id, file_id, parent_trace_id, root_trace_id, step_count}'
curl -s "$SOAT_URL/api/v1/traces/$(printf '%s\n' "$GEN_1" | jq -r '.trace_id')/tree" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '{id, children: [.children[].id]}'
Step 6 - Download raw trace steps using file_id
Use Files key concepts and Files examples to inspect raw trace payloads.
- CLI
- SDK
- curl
TRACE_FILE_ID=$(soat get-trace --trace-id "$TRACE_1_ID" | jq -r '.file_id')
soat download-file-base64 --file-id "$TRACE_FILE_ID" \
| jq -r '.content' | base64 -d | jq '.[0]'
const traceFileId = trace1.file_id;
const { data: traceFile } = await adminSoat.files.downloadFileBase64({
path: { file_id: traceFileId },
});
const rawTraceSteps = JSON.parse(
Buffer.from(traceFile.content, 'base64').toString('utf8')
);
TRACE_FILE_ID=$(curl -s "$SOAT_URL/api/v1/traces/$TRACE_1_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq -r '.file_id')
curl -s "$SOAT_URL/api/v1/files/$TRACE_FILE_ID/download/base64" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
| jq -r '.content' | base64 -d | jq '.[0]'
Step 7 - Build a reusable debug ledger (reverse lookup)
Use Trace debugging joins to resolve trace_id -> generation_id[] directly, then keep a lightweight ledger only for generation_id -> session_id correlation.
- CLI
- SDK
- curl
curl -s "$SOAT_URL/api/v1/traces/$TRACE_1_ID/generations" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '{trace_id, generation_ids}'
cat > /tmp/debug-links.json <<EOF
[
{"session_id":"$SESSION_ID","generation_id":"$GEN_1_ID","trace_id":"$TRACE_1_ID"},
{"session_id":"$SESSION_ID","generation_id":"$GEN_2_ID","trace_id":"$TRACE_2_ID"}
]
EOF
# Reverse lookup example: trace_id -> generation_id + session_id
jq -r '.[] | select(.trace_id == "'"$TRACE_1_ID"'")' /tmp/debug-links.json
const { data: traceGenerations } = await adminSoat.traces.getTraceGenerations({
path: { trace_id: gen1.trace_id },
});
// traceGenerations.generation_ids => ['gen_...', 'gen_...']
const linksByTraceId = new Map(debugLinks.map((row) => [row.traceId, row]));
const reverse = linksByTraceId.get(gen1.trace_id);
// reverse => { sessionId, generationId, traceId }
curl -s "$SOAT_URL/api/v1/traces/$TRACE_1_ID/generations" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '{trace_id, generation_ids}'
cat > /tmp/debug-links.json <<EOF
[
{"session_id":"$SESSION_ID","generation_id":$(printf '%s\n' "$GEN_1" | jq -r '.generation_id | @json'),"trace_id":$(printf '%s\n' "$GEN_1" | jq -r '.trace_id | @json')},
{"session_id":"$SESSION_ID","generation_id":$(printf '%s\n' "$GEN_2" | jq -r '.generation_id | @json'),"trace_id":$(printf '%s\n' "$GEN_2" | jq -r '.trace_id | @json')}
]
EOF
jq '.' /tmp/debug-links.json
What you achieved
You now have a deterministic debugging workflow for:
- session -> all messages
- session -> all generation IDs
- generation -> trace ID
- trace -> generation IDs
- trace -> raw steps and trace tree
- trace -> session via your debug ledger
For direct module references, see Sessions debugging links, Agent traces, Trace debugging joins, and Files key concepts.