Packages
lemon_router
0.1.0
Run lifecycle and session orchestration for Lemon agents: single-flight execution per conversation, queueing and steering, coalescing, policy, watchdog and delivery routing.
Current section
Files
Jump to
Current section
Files
lemon_router
README.md
README.md
# LemonRouter
`lemon_router` owns request normalization, conversation identity, queue semantics, and semantic output tracking.
It sits between channel transports and `lemon_gateway`.
## Current Flow
```text
Channel transport or gateway-native ingress
-> LemonRouter.Router.handle_inbound/1
-> LemonRouter.RunOrchestrator
-> LemonRouter.SessionCoordinator
-> LemonRouter.RunProcess
-> configured LemonCore.EngineRuntime.submit_execution/1
-> gateway runtime scheduler / thread worker / run
-> LemonCore.Bus run events
-> LemonRouter semantic coalescers / output tracking
-> LemonCore.DeliveryIntent
-> LemonChannels.Dispatcher
-> channel-specific renderer / outbox
```
## Ownership
- Router owns:
- `RunRequest` normalization
- policy, model, and engine resolution
- resume resolution and conversation-key selection
- queue semantics: `collect`, `followup`, `steer`, `steer_backlog`, `interrupt`
- pending-compaction prompt rewriting
- semantic stream and tool-status coalescing
- Router does not own:
- Telegram or Discord rendering details
- `OutboundPayload` construction
- Telegram message-id presentation state
- gateway slot scheduling or engine lifecycle
## Key Modules
| Module | Responsibility |
| --- | --- |
| `LemonRouter.Router` | Main inbound entrypoint, session-key resolution, pending-compaction application, control-plane abort/keepalive hooks |
| LemonRouter.RunOrchestrator (internal) | Builds router-owned submissions from `LemonCore.RunRequest` and hands them to `SessionCoordinator` |
| `LemonRouter.SessionCoordinator` | Single owner of per-conversation queue semantics and active-run handoff |
| Router internal session read model | Internal read model over coordinator-owned active session state |
| `LemonRouter.ConversationKey` | Canonical conversation-key selection from structured resume or session key |
| `LemonRouter.ResumeResolver` | Structured resume resolution before runtime submission |
| `LemonRouter.RunProcess` | Active-run lifecycle wrapper around one execution |
| `LemonRouter.MediaJobRecorder` | Records generated final-answer files into redacted media job metadata before channel delivery |
| LemonRouter.ChannelsDelivery (internal) | Narrow bridge from router-adjacent automation delivery requests into `LemonChannels`; must not construct `OutboundPayload` or own channel rendering |
| `LemonRouter.StreamCoalescer` | Semantic answer coalescing that emits `DeliveryIntent` snapshots/finalization |
| `LemonRouter.ToolStatusCoalescer` | Semantic tool-status coalescing that emits `DeliveryIntent` snapshots/finalization |
| `LemonRouter.PendingCompactionStore` | Router-owned typed wrapper for pending-compaction markers |
| `LemonRouter.AgentEndpointStore` | Router-owned typed wrapper for persistent endpoint aliases |
| `LemonRouter.AgentInbox` | BEAM-local send API with selectors, fanout, and queue-mode selection |
| `LemonRouter.AgentDirectory` | Active/durable session discovery |
| `LemonRouter.AgentEndpoints` | Persistent route aliases |
`SurfaceManager.finalize_answer/3` dispatches `:stream_finalize` first for runs that already
streamed answer deltas, then finalizes `StreamCoalescer` state so late flushes cannot overwrite
the final. For non-streamed completions it still finalizes through `StreamCoalescer`, with a direct
`:final_text` fallback if the coalescer finalize call exits or times out. Completion-time artifact
metadata enrichment is best-effort and must not block the final answer path.
`RunProcess.ArtifactTracker` also supports Hermes-style final-answer media
directives: a line containing `MEDIA:<project-relative-path>` is converted into
an explicit `auto_send_files` entry after existing-file, cwd, and symlink escape
checks pass. The directive line is removed from the final text before channel
rendering, and Telegram/Discord delivery still goes through the normal
attachment policy.
## Important Contracts
- Inbound callers should provide structured resume data through `LemonCore.RunRequest.resume` when they already know it.
- Engine ID validation and normalization should use `LemonCore.EngineCatalog`; default cwd resolution should use `LemonCore.Cwd`.
- Router emits `LemonCore.DeliveryIntent`, not `LemonChannels.OutboundPayload`.
- Tool-status failure summaries preserve safe structured fields from
`action.detail.result_meta`, including `error_type`, tool name, timeout,
command exit code, exception class/name, status, reason, message, and
validation errors.
- Cron/channel-origin summary delivery may cross router through the internal LemonRouter.ChannelsDelivery bridge, but the bridge must stay narrow. Router must not construct `LemonChannels.OutboundPayload`; `LemonChannels` remains responsible for enqueue semantics, retries, chunking, and adapter delivery.
- Runtime input is `LemonCore.ExecutionCommand`; gateway-private adapters may translate it to `LemonGateway.ExecutionRequest`.
- Telegram-specific state is owned by `lemon_channels` wrappers:
- `LemonChannels.Telegram.StateStore`
- `LemonChannels.Telegram.ResumeIndexStore`
- External apps must query busy/active session state through `LemonRouter.Router` or `LemonCore.RouterBridge`, not router-internal read-model or registry details.
## Session And Queue Semantics
`SessionCoordinator` serializes by conversation key:
- `{:resume, engine, token}` when a structured resume token is available
- `{:session, session_key}` otherwise
Queue-mode behavior lives here:
- `:collect` appends
- `:followup` debounces/merges recent followups, except async task/delegated followups which stay separate
- `:steer` attempts in-run steer and falls back to followup
- `:steer_backlog` attempts in-run steer and falls back to collect
- active async task/delegated auto-followups are promoted to `:steer` so completions try to reach the live parent run before falling back to a queued followup
- `:interrupt` cancels the active run and inserts the new request at the front
## Output Semantics
Router coalescers only track semantic state:
- accumulated text
- sequence numbers
- semantic tool/action state
- run/session metadata needed for `DeliveryIntent`
Channels decides:
- send vs edit
- truncation
- reply markup
- media batching
- Telegram resume indexing by platform message id
## Testing
Run the app suite from the umbrella root:
```bash
mix test apps/lemon_router
```
Useful focused suites during refactors:
```bash
mix test apps/lemon_router/test/lemon_router/router_test.exs
mix test apps/lemon_router/test/lemon_router/run_orchestrator_test.exs
mix test apps/lemon_router/test/lemon_router/session_coordinator_test.exs
mix test apps/lemon_router/test/lemon_router/run_process_test.exs
mix test apps/lemon_router/test/lemon_router/stream_coalescer_test.exs
mix test apps/lemon_router/test/lemon_router/tool_status_coalescer_test.exs
```
Run architecture checks after boundary changes:
```bash
mix lemon.quality
```