Skip to content

Refactor: strip heavy-lifting from AgentSessionShortcutContext; centralize AgentServices/MCP/persistence composition #1403

Description

@JoshuaRowePhantom

Summary

AgentSessionShortcutContext (features/Phantom.Workspaces/ViewModels/AgentSessionShortcutContext.cs) is a GUI ViewModels-layer class, but it performs a large amount of composition and infrastructure heavy-lifting that belongs in lower layers:

  1. AgentServices composition — hand-assembles the toolset-factory chain, the MCP tool-resource factory (machine/user/defaults mcp-servers search prefixes), the account-upsert service, the current-session context, and forwards SecretProvider.
  2. Agent-persistence-store construction — a ~95-line switch over RepositorySource that builds Web / DevTunnel (reconnecting) / MongoDB / in-memory stores, including GitHub-auth-token resolution and chat-history-provider wiring.
  3. Agent-session entity authoring — builds the agent-session entity JSON document, names, display name, parameter-values, host-profile id, and name sanitization.

Because each launch path re-assembles AgentServices by hand, the assemblies drift and silently drop services. This is not hypothetical — it is the direct root cause of two filed bugs:

Both are symptoms of the same architectural problem: service composition is duplicated across App.axaml.cs and AgentSessionShortcutContext (and other launch sites) instead of living in one shared composition root. The goal of this bug is to strip AgentSessionShortcutContext down to a thin GUI adapter and move the heavy lifting (especially all MCP-server work) into the appropriate layers, so every launch path gets one complete, identical AgentServices bundle and the whole class of "forgot to thread service X" bugs is eliminated.

Root Cause

AgentSessionShortcutContext mixes four unrelated responsibilities in the GUI layer.

(a) AgentServices composition (37-86) — including the MCP tool-resource factory (101-129):

ToolResourceFactory = this.CreateToolResourceFactory(dataAccessLayer),   // 79
...
private IToolResourceFactory CreateToolResourceFactory(IDataAccessLayer dataAccessLayer)
{
    ...
    return new ComposingToolResourceFactory(
        new FixedToolResourceFactory(CreateFixedToolMapping()),
        new McpServerEntityToolResourceFactory(dataAccessLayer, [ machineProfilePrefix, ${USER}/mcp-servers, defaults/mcp-servers ]));  // 115-128
}

The returned AgentServices (74-85) sets SecretProvider but not McpOAuthOptions (#1402), and the app-level composition in App.axaml.cs:313-316 builds a different AgentServices that does have McpOAuthOptions. Two hand-written bundles, guaranteed to drift.

(b) Persistence-store construction (197-292) — a RepositorySource switch that belongs in a persistence-store factory:

private static async Task<IAgentPersistenceStore> CreateAgentPersistenceStoreAsync(RepositorySource repositorySource)
    => repositorySource switch
    {
        WebRepositorySource webSource => CreateWebAgentPersistenceStore(webSource),
        DevTunnelNameRepositorySource devTunnelSource => await CreateDevTunnelAgentPersistenceStoreAsync(devTunnelSource),
        MongoDbRepositorySource mongoSource => await CreateMongoDbAgentPersistenceStoreAsync(mongoSource),
        _ => AgentPersistenceStoreFactory.CreateInMemory(),
    };

AgentPersistenceStoreFactory already exists (used at 222/276/291) — this switch and its three helpers should live there (or in an IAgentPersistenceStoreFactory keyed on RepositorySource), not in a GUI ViewModel.

(c) Agent-session entity authoring (138-195, 294-369)CreateAgentSessionEntityData, CreateSessionObjectSimpleName, SanitizeNameComponent are data-layer concerns.

(d) The only genuinely GUI-specific inputs are MainWindowViewModel, ShortcutManager, and WorkspaceGuiContextProvider. Everything else is host/infrastructure composition.

Affected Files

File What it contributes
features/Phantom.Workspaces/ViewModels/AgentSessionShortcutContext.cs The bloated class: AgentServices composition (37-86), MCP tool-resource factory (101-129), persistence-store switch (197-292), entity authoring (138-195, 294-369).
features/Phantom.Workspaces/App.axaml.cs Builds a parallel AgentServices (313-316) with McpOAuthOptions; target caller of the shared composition root.
features/Phantom.Workspaces/Services/ApplicationServices.cs Holds process-wide services (SecretProvider); natural home for (or reference to) the shared AgentServices composition + McpOAuthOptions.
features/Phantom.Workspaces.Llm.Interfaces/AgentServices.cs The bundle being assembled; should be produced by one factory, not hand-written per site.
features/Phantom.Workspaces/Services/Mcp/McpOAuthComposition.cs MCP OAuth options bundle that must be part of the shared composition.
features/Phantom.Workspaces.Llm.Core/* (AgentPersistenceStoreFactory, ToolResourceFactory, McpServerEntityToolResourceFactory) Existing lower-layer factories the extracted logic should move into or be driven by.

Design / Fix

Reduce AgentSessionShortcutContext to a thin GUI adapter; move each responsibility to its proper layer. Prefer extracting into existing factories over inventing new abstractions.

  1. Single AgentServices composition root. Introduce one host-level composer (e.g. Services/AgentServicesComposition.cs, or a method on ApplicationServices) that produces the complete AgentServices bundle: toolset-factory chain, MCP tool-resource factory, SecretProvider, McpOAuthOptions, account-upsert service, current-session context. Both App.axaml.cs and AgentSessionShortcutContext (and any other session-launch site) call this one method, passing only the small GUI-specific context (MainWindowViewModel/ShortcutManager/WorkspaceGuiContextProvider). This structurally prevents Secret materialization is gated on the manifest; agent-session launches never resolve ${SECRET:...} (only sessions/definitions should materialize secrets) #1401/Interactive MCP OAuth not wired on session-launch path: AgentServices omits McpOAuthOptions #1402-class drift — there is exactly one place that can forget a service, and it forgets it for everyone (caught by one test) rather than silently for one path.

  2. Move MCP-server tool-resource composition out of the GUI. CreateToolResourceFactory / CreateFixedToolMapping (the ComposingToolResourceFactory + McpServerEntityToolResourceFactory + machine/user/defaults prefixes) belong in an Llm.Core factory (e.g. extend ToolResourceFactory), parameterized by the data-access layer and execution context. The GUI should not know MCP search-prefix precedence.

  3. Move persistence-store construction into AgentPersistenceStoreFactory. Relocate CreateAgentPersistenceStoreAsync and its Web/DevTunnel/MongoDB helpers into AgentPersistenceStoreFactory (or a new IAgentPersistenceStoreFactory keyed on RepositorySource). AgentSessionShortcutContext should call one factory method, not own the switch.

  4. Move agent-session entity authoring into the data layer. Extract CreateAgentSessionEntityData, CreateSessionObjectSimpleName, SanitizeNameComponent into a data-layer AgentSessionEntityFactory (or similar). The shortcut context orchestrates the UpdateAsync call; it does not build the document.

  5. Result. AgentSessionShortcutContext shrinks to: resolve GUI context → call the shared AgentServices composition → call the persistence-store factory → call the entity factory → issue the entity UpdateAsync. No MCP wiring, no persistence switch, no JSON authoring.

This bug is the umbrella refactor; #1401 and #1402 are its symptoms. Landing this eliminates the root cause (per-site hand-assembly of AgentServices). #1401/#1402 may be fixed first as tactical patches, or folded into this refactor — either way their fixes should converge on the single composition root.

Considered / Background

Expected Tests

Test Name Class What It Verifies
AgentServicesComposition_Compose_ProducesCompleteBundle AgentServicesCompositionTests The single composition root returns AgentServices with SecretProvider, McpOAuthOptions, toolset factory, MCP tool-resource factory, account-upsert service, and current-session context all set.
AgentSessionShortcutContext_CreateAgentServices_DelegatesToCompositionRoot AgentSessionShortcutContextTests The session-launch path returns the bundle from the shared composition root (same McpOAuthOptions/SecretProvider instances as the app-level path), not a hand-assembled one.
AgentSessionShortcutContext_DoesNotConstructPersistenceStoreOrToolResourceFactory AgentSessionShortcutContextTests The class no longer owns the RepositorySource switch or MCP tool-resource composition (delegates to the factories).
AgentPersistenceStoreFactory_CreateForRepositorySource_Web AgentPersistenceStoreFactoryTests The Web branch is created by the factory (moved out of the GUI).
AgentPersistenceStoreFactory_CreateForRepositorySource_MongoDbAndDevTunnelAndInMemory AgentPersistenceStoreFactoryTests DevTunnel/MongoDB/in-memory branches are created by the factory with equivalent behavior to the old inline switch.
ToolResourceFactory_CreateMcpServerResolution_UsesMachineUserDefaultsPrecedence ToolResourceFactoryTests The extracted MCP tool-resource factory preserves machine > ${USER}/mcp-servers > defaults/mcp-servers precedence (issue #1399).
AgentSessionEntityFactory_CreateEntityData_EscapesFreeTextAndSetsOptionalFields AgentSessionEntityFactoryTests The extracted entity factory builds the agent-session document (names, display name, parameter-values, host-profile) with safe JSON escaping (issue #1397).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingdiagnosedRoot cause identifiedneeds-slow-testsRequires full test suite including slow Git tests at checkinverified-locallyImplementation has been verified locally

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions