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.
Inbound Flow
Section titled “Inbound Flow”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 TelegramChannel Adapters
Section titled “Channel Adapters”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.
Supported Channels
Section titled “Supported Channels”| Channel | Transport | Status |
|---|---|---|
| WebChat | REST + SignalR | Implemented |
| Slack | Webhook | Planned |
| Microsoft Teams | Webhook | Planned |
| Discord | Webhook | Planned |
| Telegram | Webhook | Planned |
| Webhook | Planned | |
| Signal | Webhook | Planned |
| SMS | Webhook | Planned |
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.
Message Format
Section titled “Message Format”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": []}Conversation Turns
Section titled “Conversation Turns”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"]}Tool Execution Flow
Section titled “Tool Execution Flow”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 outputTool Execution Request
Section titled “Tool Execution Request”{ "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"}Tool Execution Result
Section titled “Tool Execution Result”{ "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"}Session Binding
Section titled “Session Binding”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
Rebinding
Section titled “Rebinding”You can rebind a session to a different agent:
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.
Timeout Handling
Section titled “Timeout Handling”Gateway Timeout
Section titled “Gateway Timeout”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 Execution Timeout
Section titled “Tool Execution Timeout”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:
- The
ToolGraintransitions toTimedOutstatus - The
AgentGrainreceives an error result - The LLM is informed the tool timed out and generates an appropriate response
Real-Time Updates
Section titled “Real-Time Updates”The WebChat channel uses SignalR for bi-directional real-time communication. The web dashboard connects to the SignalR hub for live message streaming.
Hub Methods
Section titled “Hub Methods”| Method | Direction | Purpose |
|---|---|---|
SendMessage | Client → Server | Send a message to the agent |
ReceiveMessage | Server → Client | Receive the agent’s response |
AgentTyping | Server → Client | Indicate the agent is generating a response |
The connection uses exponential backoff for reconnection and gracefully handles disconnects.
Frontend Integration
Section titled “Frontend Integration”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-Idon every API call for end-to-end tracing
Audit Trail
Section titled “Audit Trail”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:
MessageReceived— inbound message arrives at the GatewaySessionCreated— if this is the first message (new session)ToolExecutionStarted— if the agent invokes a skillToolExecutionCompletedorToolExecutionFailed— skill resultMessageSent— outbound response dispatched
Each entry includes the actor ID, agent ID, session ID, and timestamp, enabling full reconstruction of any conversation.
Error Handling
Section titled “Error Handling”| Error | Response | Recovery |
|---|---|---|
| Unknown agent | 404 Not Found | Check agent ID exists |
| Session channel mismatch | 400 Bad Request | Use correct session key |
| Skill permission denied | 403 Forbidden | Grant required permissions |
| Grain timeout | 504 Gateway Timeout | Retry or check silo health |
| Tool execution failed | Error in LLM context | LLM generates error-aware response |
| Tool execution timed out | Timeout in LLM context | LLM informs user of timeout |
| Validation failure | 400 Bad Request | Fix input per error message |