Skip to content

Message Flow

This page traces the lifecycle of a message from an external channel through the Komand system and back, including tool execution, real-time updates, and error handling.

1. User sends message on Telegram
2. Telegram webhook hits Gateway API
3. Channel adapter normalizes to InboundMessage
4. Gateway resolves SessionGrain key:
"Telegram:{botId}:{userId}"
5. SessionGrain.HandleMessageAsync()
├── Lazy-initializes if new session (sets channel, sender, timestamps)
├── Validates channel matches existing session
└── Routes to bound AgentGrain
6. AgentGrain.ProcessMessageAsync()
├── Appends user turn to session history
├── Trims oldest turns if history exceeds MaxTurnsPerSession
├── Generates response (see note below)
├── Appends assistant turn to history
└── Returns OutboundMessage
7. Gateway sends response back via channel adapter
8. User receives reply on Telegram

Channel adapters are thin ASP.NET webhook endpoints that normalise platform-specific payloads into a common message format and route them to the appropriate SessionGrain.

ChannelTransportStatus
WebChatREST + SignalRImplemented
SlackWebhookPlanned
Microsoft TeamsWebhookPlanned
DiscordWebhookPlanned
TelegramWebhookPlanned
WhatsAppWebhookPlanned
SignalWebhookPlanned
SMSWebhookPlanned

All channels normalise to the same InboundMessage/OutboundMessage format, so grains are completely channel-agnostic. Adding a new channel means writing a thin adapter — the grain layer requires no changes.

All channels normalize messages to a common InboundMessage format:

{
"messageId": "msg-abc-123",
"channel": "Telegram",
"channelAccountId": "bot-456",
"senderId": "user-789",
"senderDisplayName": "Alice",
"text": "Book a meeting with Bob tomorrow at 2pm",
"timestamp": "2026-02-23T10:30:00Z",
"attachments": [],
"metadata": {
"telegramChatId": "12345"
}
}

Responses use a corresponding OutboundMessage format:

{
"sessionId": "Telegram:bot-456:user-789",
"channel": "Telegram",
"recipientId": "user-789",
"text": "I've booked a meeting with Bob for tomorrow at 2:00 PM.",
"timestamp": "2026-02-23T10:30:02Z",
"attachments": []
}

Inside the AgentGrain, messages are stored as conversation turns:

{
"role": "user",
"content": "Book a meeting with Bob tomorrow at 2pm",
"timestamp": "2026-02-23T10:30:00Z",
"toolCallIds": null
}
{
"role": "assistant",
"content": "I've booked a meeting with Bob for tomorrow at 2:00 PM.",
"timestamp": "2026-02-23T10:30:02Z",
"toolCallIds": ["exec-abc-123"]
}

When the LLM decides to use a skill:

AgentGrain receives LLM response with tool_call
Validates tool against SkillRegistryGrain
├── Checks skill exists
└── Checks agent's granted permissions satisfy skill's requirements
Creates ToolGrain with unique executionId
ToolGrain.ExecuteAsync(request)
├── Status: Pending → Running
├── Executes skill with enforced timeout (capped at MaxToolExecutionTimeoutMinutes)
├── Status: Running → Completed | Failed | TimedOut | Cancelled
└── Returns ToolExecutionResult
AgentGrain feeds result back to LLM
LLM generates final response incorporating tool output
{
"executionId": "exec-abc-123",
"toolName": "calendar-booking",
"agentId": "default",
"sessionId": "session-xyz",
"parameters": {
"attendee": "Bob",
"date": "2026-02-24",
"time": "14:00"
},
"timeout": "00:05:00",
"requestedAt": "2026-02-23T10:30:01Z"
}
{
"executionId": "exec-abc-123",
"status": "Completed",
"output": "{\"eventId\":\"cal-789\",\"confirmed\":true,\"time\":\"2026-02-24T14:00:00Z\"}",
"error": null,
"completedAt": "2026-02-23T10:30:02Z",
"duration": "00:00:01.234"
}

Sessions are automatically created on first contact. By default, all sessions bind to the "default" agent. The session key is derived from the channel, account, and sender:

Key: "{Channel}:{ChannelAccountId}:{SenderId}"
Example: "Telegram:bot-456:user-789"

This key structure means:

  • The same user on Telegram and Slack gets separate sessions
  • The same Telegram user talking to different bots gets separate sessions
  • The same Telegram user talking to the same bot always gets the same session

You can rebind a session to a different agent:

Terminal window
curl -X POST http://localhost:5000/api/sessions/{sessionId}/bind \
-H "Content-Type: application/json" \
-d '{ "agentId": "sales-bot" }'

After rebinding, all future messages in that session are routed to the new agent.

All grain calls from the Gateway have a 30-second timeout. If a grain doesn’t respond within this window, the API returns 504 Gateway Timeout:

{
"success": false,
"error": "Request timed out after 30 seconds"
}

Tool executions have a separate, configurable timeout managed by the ToolGrain. The timeout is specified per-request and capped at MaxToolExecutionTimeoutMinutes (default: 30 minutes).

If a tool exceeds its timeout:

  1. The ToolGrain transitions to TimedOut status
  2. The AgentGrain receives an error result
  3. The LLM is informed the tool timed out and generates an appropriate response

The WebChat channel uses SignalR for bi-directional real-time communication. The web dashboard connects to the SignalR hub for live message streaming.

MethodDirectionPurpose
SendMessageClient → ServerSend a message to the agent
ReceiveMessageServer → ClientReceive the agent’s response
AgentTypingServer → ClientIndicate the agent is generating a response

The connection uses exponential backoff for reconnection and gracefully handles disconnects.

The React frontend uses the @microsoft/signalr package to manage the connection:

  • Auth store provides the Bearer token for authenticated connections
  • Chat store manages message state and real-time updates via Zustand
  • Correlation ID is sent as X-Request-Id on every API call for end-to-end tracing

Every significant action in the message flow generates an audit log entry. These are persisted to a dedicated komand_audit_log table in PostgreSQL.

For a typical message exchange, the audit trail captures:

  1. MessageReceived — inbound message arrives at the Gateway
  2. SessionCreated — if this is the first message (new session)
  3. ToolExecutionStarted — if the agent invokes a skill
  4. ToolExecutionCompleted or ToolExecutionFailed — skill result
  5. MessageSent — outbound response dispatched

Each entry includes the actor ID, agent ID, session ID, and timestamp, enabling full reconstruction of any conversation.

ErrorResponseRecovery
Unknown agent404 Not FoundCheck agent ID exists
Session channel mismatch400 Bad RequestUse correct session key
Skill permission denied403 ForbiddenGrant required permissions
Grain timeout504 Gateway TimeoutRetry or check silo health
Tool execution failedError in LLM contextLLM generates error-aware response
Tool execution timed outTimeout in LLM contextLLM informs user of timeout
Validation failure400 Bad RequestFix input per error message