refactor: decompose LlmSessionActor for composability (#411, #414)#417
Merged
Conversation
…sionTuning, and slimmed SessionConfig (#414) Split the 16-property SessionConfig into three focused types: - ModelCapabilities: runtime-derived model properties (ModelId, ContextWindowTokens, InputModalities, OutputModalities, CompactionModelId) registered as a separate DI singleton resolved from the capability detection pipeline. - SessionTuning: internal tuning constants (compaction settings, tool retention, snapshot interval, title generation) nested under SessionConfig.Tuning. Bindable from config for testing but not part of the documented operator surface. - SessionConfig: slimmed to 6 user-facing operational settings with TimeSpan timeouts (TurnLlmTimeout, ToolExecutionTimeout, SidecarLlmTimeout) replacing int-seconds properties, eliminating 15+ Math.Max(1, ...) guards. Also includes: - SessionConfig.BindFromConfiguration() static factory for backward-compatible JSON binding (int-seconds keys converted to TimeSpan with min 1s enforcement) - ModelCapabilityResolution returns ModelCapabilities directly instead of a tuple - JSON schema Session section enforces additionalProperties: false - LlmSessionActor accepts ModelCapabilities as separate constructor parameter - SQLiteMemoryRecallCoordinator takes SessionTuning for feature flags - DaemonRuntimeStatusService and ChatViewModel use ModelCapabilities - OpenSpec change artifacts for the full LlmSessionActor refactoring plan All 1,400 tests pass. Closes #414
…to 7 via composite records (#411) Group individual dependencies into four composable records: - SessionServices: core runtime (ClientProvider, PromptProvider, ContextLayers, TimeProvider, Paths) - SessionToolServices: tool execution (ToolExecutor, AuditLogger, ToolRegistry, AccessPolicy, TrustDeriver, SkillRegistry) — nullable for tool-less sessions - SessionMemoryServices: memory infrastructure (MemoryExtractor, RecallCoordinator, CheckpointSink, MemoryStore) - SessionObservability: metrics and lifecycle (SessionMetrics, LifecycleObserver) Composite records registered as DI singletons in Program.cs and resolved by Akka.DependencyInjection's resolver.Props<LlmSessionActor>(entityId). All 1,400 tests pass.
… Passivating state (#411) Replace implicit Become() calls with explicit TransitionTo(SessionPhase) that validates legal transitions and throws InvalidOperationException for illegal ones. - SessionPhase enum: Recovering, Ready, Processing, Compacting, Passivating - TransitionTo() validates transition legality, logs phase changes, and notifies the observer actor via SessionPhaseChanged messages - Passivating behavior: buffers messages, requests final memory distillation from observer via DistillMemories, waits up to 5s grace period, then saves snapshot and stops. Falls back to immediate stop if no observer is configured. - ReceiveTimeout in Ready now transitions to Passivating instead of inline snapshot+stop, enabling the memory observer to distill final memories before session death. Legal transitions: Recovering → Ready Ready → Processing, Compacting, Passivating Processing → Ready, Compacting Compacting → Ready, Processing Passivating → (terminal) All 1,400 tests pass.
…m LlmSessionActor (#411) Extract 9 focused modules from LlmSessionActor, reducing it from 3,208 to 2,273 lines (~930 lines relocated). Handler modules (src/Netclaw.Actors/Sessions/Handlers/): - TurnStateTracker: per-turn counters, duplicate detection, budget tracking - ProcessingWatchdog: operation ID tracking, timer management, expiry validation - DeliveryRetryHandler: retry counting, eligibility, nudge construction - DiscoveredToolCache: MCP tool retention, lease countdown, eviction Pipeline utilities (src/Netclaw.Actors/Sessions/Pipelines/): - SessionTitleGenerator: title generation decision + sidecar LLM call - SessionCompactionPipeline: tiered compaction, observation generation, token estimation, CompactionParameters record - SessionLlmInvoker: streaming LLM invocation with delta forwarding - SessionToolExecutionPipeline: parallel tool execution, result clamping, sub-agent finding review, ToolCallResult record - SessionRecallManager: turn recall cache, progressive exclusion, recall injection and formatting Each module is independently testable without an ActorSystem. The actor delegates to modules via field references, preserving identical behavior. All 1,400 tests pass.
4 tasks
…ange Sync delta specs to main specs: - New: session-config-decomposition, session-state-machine - Modified: netclaw-model-capabilities, netclaw-session Archive completed change to openspec/changes/archive/2026-03-25-refactor-llm-session-actor/
Aaronontheweb
commented
Mar 25, 2026
Aaronontheweb
left a comment
Collaborator
Author
There was a problem hiding this comment.
Reviewing piece by piece
| _operationId++; | ||
| _operationName = operationName; | ||
|
|
||
| timers.StartSingleTimer( |
Collaborator
Author
There was a problem hiding this comment.
akkadotnet/akka.net#7630 - worth noting if that if we ever allow parallel tool calls, we'll need to implement this in order to get those to work properly
| /// empty-response retries, and force-no-tools state. Reset at the start of | ||
| /// each user turn. | ||
| /// </summary> | ||
| internal sealed class TurnStateTracker |
Collaborator
Author
There was a problem hiding this comment.
We need to move more of the decision-making about "stuck turns" et al into this class.
…icate, and empty response decisions TurnStateTracker was a property bag. Now it owns the actual decision-making: - RecordToolCompletion(count, max) → ToolBudgetStatus (Ok/NudgeNeeded/Exhausted) with pre-built nudge text and remaining-call count - CheckForDuplicates() → DuplicateToolNudge? with tool name extraction and nudge text, or null if no duplicates meet threshold - EvaluateEmptyResponse() → EmptyResponseAction (Retry with nudge / Fail with error message and cause) covering pre-tool empty, post-tool first empty, and post-tool second empty scenarios - ResetEmptyResponseGuards() for when model starts tool work (not stuck) - ResetToolCounters() for mid-turn buffer drain Result types use discriminated union records (ToolBudgetStatus, EmptyResponseAction) so the actor uses pattern matching instead of flag checks. Each decision is now independently unit-testable without an ActorSystem. All 1,400 tests pass.
Aaronontheweb
commented
Mar 25, 2026
Aaronontheweb
left a comment
Collaborator
Author
There was a problem hiding this comment.
Rest of my comments
| try | ||
| { | ||
| using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(300)); | ||
| return coordinator.RecallAsync(request, cts.Token) |
Collaborator
Author
There was a problem hiding this comment.
Any way we can make this method just return async Task ?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refactors
LlmSessionActorandSessionConfigfor composability, maintainability, and eventual Akka.Agents framework extraction.Phase 0 (this commit): SessionConfig Decomposition (#414)
SessionConfigintoModelCapabilities(runtime-derived),SessionTuning(internal constants), and slimmedSessionConfig(6 user-facing settings)intseconds toTimeSpan, eliminating 15+Math.Max(1, ...)guardsadditionalProperties: falsefor the Session sectionFixes added during review follow-up
Selfas senderReceiveTimeoutticks from resetting the parent session idle timer when there is nothing new to distillSession.*tuning keys while preferring nestedSession:Tuning:*Remaining phases (WIP):
SessionPhaseenum +TransitionTo()+Passivatingstate)OpenSpec artifacts:
openspec/changes/refactor-llm-session-actor/Test plan
dotnet build- 0 errorsdotnet test- 1,400 tests pass, 0 failuresdotnet slopwatch analyze- no new violationsCloses #414
Closes #426
Refs #411, #326