Repository navigation
Add IRunningAgentChatFactory interface and AgentChatFactory implementation to Llm.Core #670
Description
Summary
Introduce IRunningAgentChatFactory in Phantom.Workspaces.Llm.Core and its reference-counted implementation AgentChatFactory. Adds IRunningAgentChatFactory? RunningAgentChatFactory to AgentServices. On first GetAsync for a session ID the factory loads AgentChat from IAgentPersistenceStore, acquires an anchor lease, and adds a RunningAgentChat to RunningSessions. On last lease release it removes the entry and disposes AgentChat. CreateAsync persists the new session before adding to the table.
Part of #659.
Prerequisites
- #667 —
AgentSessionIdtyped wrapper - #668 —
RunningAgentChatLeaseinLlm.Core - #669 —
RunningAgentChatentry type
Design
Interface
// Phantom.Workspaces.Llm.Core/IRunningAgentChatFactory.cs
public interface IRunningAgentChatFactory
{
/// <summary>
/// The live set of sessions currently held by at least one lease.
/// Mutations are dispatched on the foreground scheduler; UI subscribers need not marshal.
/// </summary>
ObservableCollection<RunningAgentChat> RunningSessions { get; }
/// <summary>
/// Acquires a ref-counted lease on the AgentChat for <paramref name="sessionId"/>,
/// loading it from persistence if not already running.
/// </summary>
Task<RunningAgentChatLease> GetAsync(AgentSessionId sessionId, CancellationToken ct = default);
/// <summary>
/// Creates a new AgentChat from <paramref name="definition"/> + <paramref name="sessionId"/>,
/// persists it, adds to RunningSessions, and returns a lease.
/// </summary>
Task<RunningAgentChatLease> CreateAsync(
AgentDefinition definition,
AgentSessionId sessionId,
AgentServices? services = null,
CancellationToken ct = default);
}Implementation
// Phantom.Workspaces.Llm.Core/AgentChatFactory.cs
internal sealed class AgentChatFactory : IRunningAgentChatFactory, IAsyncDisposable
{
private readonly IAgentPersistenceStore _store;
private readonly AgentServices _services;
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly Dictionary<AgentSessionId, Entry> _entries = new();
private readonly ObservableCollection<RunningAgentChat> _runningSessions = new();
public ObservableCollection<RunningAgentChat> RunningSessions => _runningSessions;
public Task<RunningAgentChatLease> GetAsync(AgentSessionId sessionId, CancellationToken ct = default) { … }
public Task<RunningAgentChatLease> CreateAsync(AgentDefinition definition, AgentSessionId sessionId, AgentServices? services = null, CancellationToken ct = default) { … }
}AgentServices change
// Phantom.Workspaces.Llm.Interfaces/AgentServices.cs
public IRunningAgentChatFactory? RunningAgentChatFactory { get; init; }AgentChat receives IRunningAgentChatFactory via AgentServices at construction and passes it to SubAgent instances (see #658).
Threading
All mutations to RunningSessions (Add and Remove) must be posted to the foreground TaskScheduler (the UI thread scheduler). The factory must accept a TaskScheduler foregroundScheduler constructor parameter. This makes the threading contract testable without requiring a real UI thread.
At production call sites, pass TaskScheduler.FromCurrentSynchronizationContext() captured on the UI thread before the factory is constructed.
In tests, pass TaskScheduler.Default (or a custom capturing scheduler) to run mutations synchronously on the test thread.
Files affected
| File | Change |
|---|---|
New: Phantom.Workspaces.Llm.Core/IRunningAgentChatFactory.cs |
New interface with GetAsync + CreateAsync |
New: Phantom.Workspaces.Llm.Core/AgentChatFactory.cs |
Reference-counted implementation |
Phantom.Workspaces.Llm.Interfaces/AgentServices.cs |
Add IRunningAgentChatFactory? RunningAgentChatFactory |
Phantom.Workspaces.Llm.Core/AgentChat.cs |
Accept IRunningAgentChatFactory via AgentServices |
Expected tests
| Test name | What it verifies |
|---|---|
GetAsync_NewSession_LoadsFromPersistenceAndReturnsLease |
When the session ID is not in memory, GetAsync loads AgentChat from IAgentPersistenceStore and returns a valid lease |
GetAsync_NewSession_AppearsInRunningSessions |
After the first GetAsync, the session appears in RunningSessions |
GetAsync_SessionAlreadyRunning_ReturnsSameAgentChatNewLease |
A second GetAsync for the same ID returns a new lease wrapping the same AgentChat instance |
GetAsync_AfterLastLeaseDisposed_RemovesFromRunningSessions |
When the last lease is disposed, the session is removed from RunningSessions |
GetAsync_AfterLastLeaseDisposed_DisposesAgentChat |
When the last lease is disposed, the AgentChat is disposed |
GetAsync_MultipleLeases_RemovedOnlyAfterAllDisposed |
With two outstanding leases, the session remains in RunningSessions until both are disposed |
GetAsync_ConcurrentFirstAcquire_OnlyCreatesOneAgentChat |
Concurrent GetAsync calls for the same session ID create exactly one AgentChat (gate serialises) |
CreateAsync_PersistsSessionBeforeReturning |
CreateAsync calls IAgentPersistenceStore to persist the definition before returning the lease |
CreateAsync_NewSession_AppearsInRunningSessions |
After CreateAsync, the new session appears in RunningSessions |
CreateAsync_DuplicateSessionId_ThrowsInvalidOperationException |
Calling CreateAsync with an already-running session ID throws (duplicate creation is a caller error) |
RunningSessions_Add_HappensOnForegroundScheduler |
AgentChatFactory is constructed with a capturing test scheduler; after GetAsync, the Add mutation to RunningSessions is observed on that scheduler |
RunningSessions_Remove_HappensOnForegroundScheduler |
After the last lease is disposed, the Remove mutation to RunningSessions is observed on the foreground scheduler |
DisposeAsync_ReleasesAllSessions |
Disposing the factory disposes all outstanding AgentChat instances and clears RunningSessions |
Activity
JoshuaRowePhantom commentedon Jul 3, 2026
Design
Approach: Expand the IRunningAgentChatFactory stub from #669 with RunningSessions and CreateAsync, then implement AgentChatFactory as a reference-counted session table.
IRunningAgentChatFactory
AgentChatFactory
AgentServices
FakeRunningAgentChatFactory (in test)
Files
JoshuaRowePhantom commentedon Jul 3, 2026
Tests
All in Phantom.Workspaces.Llm.Core.Tests/AgentChatFactoryTests.cs:
JoshuaRowePhantom commentedon Jul 6, 2026
Verification failed
Checked: Searched \Phantom.Workspaces.Llm.Core\ for \IRunningAgentChatFactory.cs\ and \AgentChatFactory.cs; searched \Phantom.Workspaces.Llm.Interfaces/AgentServices.cs\ for the \RunningAgentChatFactory\ property; searched \Phantom.Workspaces.Llm.Core.Tests\ for \AgentChatFactoryTests.cs. Also searched the entire codebase for the identifiers \IRunningAgentChatFactory, \AgentChatFactory, and \RunningAgentChatFactory.
Found: \AgentServices.cs\ exists but does not contain \IRunningAgentChatFactory? RunningAgentChatFactory. A pre-existing \RunningAgentChatTable/\IRunningAgentChatTable\ lives in \Phantom.Workspaces\Services\\ (different project, different design — string keys, event-based notification, no \ObservableCollection). No git commits reference this issue number.
Missing:
Fix #669: add RunningAgentChat entry type to Llm.Core
Fix #670: add IRunningAgentChatFactory interface and AgentChatFactory…