Every request to your agents gets a unique session ID (sess_...). Sessions link logs, traces, and state, making them essential for debugging.
Why Sessions?
Traditional observability tracks individual HTTP requests. That works for stateless web servers. Agents go beyond single requests—a conversation might span dozens of LLM calls, tool executions, and orchestration steps across multiple interactions.
Without sessions, debugging becomes guesswork. You see individual logs but can't connect them. Was that error from the same conversation? What happened before it failed? Which user was affected?
Agentuity tracks all of this automatically. Every run ties to a session. Every conversation thread is preserved. You get full context into what happened, when, and why—without writing tracking code.
In practice:
- Unified tracing: All logs, spans, and state from a single request are linked by session ID
- Conversation context: Sessions group into threads, so you can trace multi-turn conversations
- Automatic correlation: No manual tracking code, every call in a session is connected
- Session inspection: Review what happened in a session to reproduce issues
Sessions vs Threads
| Scope | Lifetime | ID Prefix | Use For |
|---|---|---|---|
| Session | Single request | sess_ | Debugging, request-scoped state |
| Thread | 1 hour (conversation) | thrd_ | Chat history, user preferences |
A thread contains multiple sessions. When a user has a multi-turn conversation, each message creates a new session within the same thread.
Threads "wrap" sessions. Think of a thread as a conversation and a session as one message in that conversation.
Accessing Session ID
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('SessionExample', {
handler: async (ctx, input) => {
// In agents
ctx.logger.info('Processing request', { sessionId: ctx.sessionId });
// Thread ID for conversation tracking
ctx.logger.info('Thread context', { threadId: ctx.thread.id });
return { sessionId: ctx.sessionId };
},
});In routes:
import { createRouter } from '@agentuity/runtime';
import myAgent from '@agent/my-agent';
const router = createRouter();
router.post('/', myAgent.validator(), async (c) => {
const input = c.req.valid('json');
// In routes
c.var.logger.info('Route called', { sessionId: c.var.sessionId });
const result = await myAgent.run(input);
return c.json({ ...result, sessionId: c.var.sessionId });
});
export default router;Viewing Session Logs
Use the CLI to view logs for a specific session:
agentuity cloud session logs sess_abc123xyzSee CLI Reference for additional session commands.
Including Session ID in Responses
For easier debugging, include the session ID in error responses:
const agent = createAgent('ErrorHandler', {
handler: async (ctx, input) => {
try {
const result = await processRequest(input);
return { success: true, data: result };
} catch (error) {
ctx.logger.error('Request failed', {
sessionId: ctx.sessionId,
error: error.message,
});
return {
success: false,
error: 'Processing failed',
sessionId: ctx.sessionId, // Helpful for debugging
};
}
},
});Linking External Logs
If you use external logging services, include the session ID so you can connect logs across systems:
const agent = createAgent('WebhookHandler', {
handler: async (ctx, input) => {
// Create a logger with session context for external services
const requestLogger = ctx.logger.child({
sessionId: ctx.sessionId,
threadId: ctx.thread.id,
service: 'webhook-handler',
});
requestLogger.info('Processing webhook', { eventType: input.event });
// External service call with session context
await externalApi.process({
...input,
metadata: { agentuitySessionId: ctx.sessionId },
});
return { success: true };
},
});Session State
Use session state for request-scoped data that doesn't persist:
const agent = createAgent('TimingExample', {
handler: async (ctx, input) => {
// Track timing within this request
ctx.session.state.set('startTime', Date.now());
const result = await processRequest(input);
const duration = Date.now() - (ctx.session.state.get('startTime') as number);
ctx.logger.info('Request completed', { durationMs: duration });
return result;
},
});Session state is cleared after the response. For persistent data, use thread state or KV storage.
Thread and Session Metadata
Store unencrypted metadata for filtering and querying. Unlike state (which is encrypted), metadata is stored as-is with database indexes for efficient lookups.
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('UserContext', {
handler: async (ctx, input) => {
// Thread metadata persists across sessions within a thread
ctx.thread.metadata.userId = input.userId;
ctx.thread.metadata.department = 'sales';
ctx.thread.metadata.plan = 'enterprise';
// Session metadata is request-scoped
ctx.session.metadata.requestType = 'chat';
ctx.session.metadata.clientVersion = input.clientVersion;
ctx.session.metadata.source = 'web';
ctx.logger.info('Request context', {
threadId: ctx.thread.id,
userId: ctx.thread.metadata.userId,
requestType: ctx.session.metadata.requestType,
});
return { success: true };
},
});
export default agent;Metadata vs State
| Aspect | Metadata | State |
|---|---|---|
| Storage | Unencrypted, indexed | Encrypted |
| Use case | Filtering, querying, analytics | Sensitive data, conversation history |
| Access | ctx.thread.metadata / ctx.session.metadata | ctx.thread.state / ctx.session.state |
| Best for | User IDs, request types, feature flags | Messages, preferences, tokens |
Use metadata for values you might filter or query on later, like user IDs, request types, or client versions. Use state for data that should remain private, like conversation history or user preferences.
Best Practices
- Include session ID in logs and error responses: Makes it easy to trace issues back to specific requests
- Use structured logging: Add context to logs for easier filtering
- Create child loggers: Add session context to component-specific loggers
Next Steps
- Logging: Structured logging patterns
- Tracing: OpenTelemetry spans for performance
- State Management: Session, thread, and request state