You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Introduce the common local/remote chat and immutable queue contracts, migrate local ownership to stable ids and revisions, and keep queue-based Copilot steering internal with no public/protocol steering API.
Scope
Implement only commit 2 of the approved design. Preserve the contracts below exactly; local and remote behavior must remain compatible. This child owns the common state, queue, and UI-facing request/options surface only; no transport behavior is added here.
Files
Files:IAgentChat, property-based Usage and AgentInformation, IAgentInputQueues, the seven queue request types and property-based snapshot/configuration/result types, owner queue adapter, stable ids/revisions in the existing queue domain, AgentChat, RunningAgentChat, RunningAgentChatLease, RunningAgentChatWithEntityInfo, AgentViewModel, AgentViewModelOptions, and InputQueueViewModel. Tests: required-init metadata, optional defaults, named initializers, serialization, AgentChatInterfaceTests, AgentInputQueuesTests, internal Copilot/non-Copilot queue consumption tests, options/common-surface tests, and unchanged local regression suite. Migrate the UI away from concrete queue collections and index identity. No transport behavior yet.
Background and preserved design decisions
API shape convention
Every method and constructor in this design is audited for call-site clarity. A method with four or
more independent arguments (including similarly typed identifiers), or any method whose positional
arguments are easy to transpose, takes one property-based *Request or *Options value plus an
optional CancellationToken. Small cohesive operations remain direct, for example ConnectAsync(AgentSessionOpenRequest request, CancellationToken ct = default).
Request/options and data records use object initializers. Semantically required members are required init; optional members have explicit defaults. Constructors are reserved for enforcing a
scalar value invariant or receiving a small cohesive set of services.
Protocol DTOs are property-based records with required init payload members. Their fixed Type
discriminator is initialized by the concrete DTO and is not caller-selectable. This changes only
the C# construction shape: version-1 kebab-case JSON names, required/optional wire members,
discriminators, strict unknown-member rejection, and semantics remain unchanged.
Considered/background decisions carried into this child
Existing queue classes expose owner objects and use indexes for some edits. Resolution: retain AgentInputQueue, AgentChatQueue, AgentInputQueueManager, and AgentChatQueueManager as local implementation types, but expose immutable queue/item snapshots
through IAgentInputQueues. Assign stable ids at creation/enqueue, use indexes only as placement
hints, and route every mutation through owner-authoritative revisioned commands.
Steering is an execution decision, not a separate kind of user input. Resolution: there is no steering member on IAgentChat and no steer protocol verb. A GUI
enqueues through the same queue API whether a run is idle or active. The owning AgentChat
decides from queue immediacy, mode, and current-run state whether to consume that item as
Copilot steering or as a future turn.
Responsibility/lifetime/threading: the UI-facing chat surface shared by the local engine and
remote proxy. The implementation owns its observable collections and mutates them only on the
foreground scheduler captured at construction. It deliberately excludes process handles,
transports, trust profiles, and MXC objects.
The two state values are New public readonly record structs in Phantom.Workspaces.Llm.Core/IAgentChat.cs, with exactly these public names, fields, types, and
semantics:
Usage permits null for a metric the provider did not report; counts must otherwise be nonnegative
and cost remains a nonnegative double measured in USD. AgentInformation requires non-null,
nonblank values for its first five strings and a non-null complete AgentDefinition; CurrentModelId is null or nonblank. Required members provide compile-time construction checks;
local publishers and the protocol codec validate value invariants before accepting,
serializing, or publishing them. Local implementations build a complete replacement value before
publishing it. Proxy implementations deserialize and validate a complete replacement value before
one foreground assignment. UsageChanged and InformationChanged are raised only after that atomic
assignment, once per applied session sequence; observers never see fields from different versions.
Record equality is the intended value equality.
Both records use the existing JSON options and kebab-case protocol naming. After authorization, the
owner serializes the full definition with AgentDefinition.ToJson() and the client deserializes it
with PhantomAgentSchema.AgentDefinitionFromJson(string). Every authorized attached GUI receives
the same complete definition in AgentInformation; this is mandatory protocol state, not an
optional capability. Authorization completes before runtime lookup, snapshot construction, or
definition serialization. An unauthorized peer receives only the indistinguishable sanitized
denial and no session metadata, definition bytes, existence signal, or redacted AgentInformation.
No field-level redaction is applied after authorization.
AgentSessionIdChanged and ModelChanged are not on the common interface: those values are members
of the atomically replaced AgentInformation, and InformationChanged is their sole common event.
During implementation migration, existing scalar getters and events may temporarily forward from Information/Usage on concrete AgentChat for source compatibility. They are not the desired
public design, must not be added to RemoteAgentChat, and are removed after local callers migrate.
Visibility/kind: the interfaces, snapshots, result, and status below are public. Concrete local
and proxy implementations and protocol DTOs are internal.
Decision: yes, introduce one common queue aggregate. It is the only queue surface consumed by AgentViewModel/InputQueueViewModel, and has local and remote implementations with identical
semantics.
AgentInputQueue, AgentChatQueue, AgentInputQueueManager, AgentChatQueueManager, and AgentInputItem remain the owner-side domain/implementation types; they are not serialized or
returned by the common interface. The implementation adds stable nonblank queue ids when default,
immediate, or custom queues are created and stable nonblank item ids when items are enqueued. Record
updates preserve item ids. Indexes may be calculated for display or placement, but commands identify
items only by id. BeforeItemId == null means append. MoveAsync supports reorder within one queue
and movement between queues atomically.
Snapshots are immutable point-in-time values. Their arrays and messages are deep copied through Microsoft.Extensions.AI.AIJsonUtilities.DefaultOptions; no ObservableCollection, mutable
manager, AgentInputQueue, AgentChatQueue, AgentInputItem, ResetSession, or other live owner
reference crosses the interface or wire. IAgentInputQueue instances are stable read models for the
life of a queue and atomically replace Snapshot; Queues is a read-only projection. Affected
queues raise Changed, then the aggregate raises Changed, after all snapshots have been replaced
on the captured foreground scheduler.
The aggregate revision increases once for every applied queue transaction; each affected queue's
revision also increases once. Every command compares expectedRevision with the aggregate revision,
which gives create/delete and cross-queue movement the same deterministic conflict rule. A
cross-queue move returns both affected queue snapshots in the delta. Blank ids/names, empty message
lists, invalid enum values, negative revisions/priorities, unknown queues/items, edits of consumed
items, deletion of default/immediate or nonempty queues, and configuration that changes fixed queue
roles are rejected before mutation. Immediate, Queue, and Held retain the existing AgentInputQueueImmediacy semantics. Hold/release is ConfigureAsync with Held or the desired
released immediacy; target selection is the explicit targetQueueId.
Commands are acknowledged and server-authoritative; there are no optimistic queue mutations.
Malformed caller arguments throw locally. Owner validation failures return Rejected with a stable
safe error code and current revision. A stale expected revision returns Conflict, performs no
mutation, and includes the complete current aggregate snapshot; the client atomically replaces its
projection, then may retry as a new command after user intent is reconciled. Exact reuse of a commandId and canonical payload returns Duplicate with the original result and no delta; reuse
with a different payload returns Conflict. Cancellation before write performs no mutation;
cancellation after write cancels only the wait and retrying the same id is safe.
The aggregate and per-queue read models live exactly as long as their owning IAgentChat; callers
borrow them and do not dispose them. After chat disposal, mutation methods throw ObjectDisposedException, no further Changed events are raised, and the last immutable snapshot
remains readable. The local adapter unsubscribes from both existing queue managers during chat
disposal; the proxy adapter unsubscribes from its session client before that client detaches.
The owner captures queue mutations, including consumption by a run, on its serialized runtime
scheduler and broadcasts one queue-changed delta on the existing session stream. Every delta has
the runtime epoch, global session sequence, resulting aggregate revision, affected full queue
snapshots, and removed queue ids. A fresh/reconnect session snapshot contains the full aggregate
snapshot. Replay applies deltas only in epoch/global-sequence order. Gaps, wrong epochs, or
noncontiguous queue revisions trigger normal reconnect/snapshot refresh. Multiple GUIs therefore
converge without sharing collections or trusting a client mutation.
AgentChatModal and its content hierarchy are New public immutable records in the same
namespace/file:
Initializer validation rejects blank ids/owner/title/body/type and invalid or duplicate
options/labels, and
clone option elements. The strict discriminator permits new modal content records in later protocol
versions without changing the modal envelope. All getters return the latest
foreground-applied state and never cross transport. GetToolSnapshot
returns an immutable point-in-time copy. Note-enqueue methods reject null/blank text; user input uses InputQueues.EnqueueAsync. SetToolEnabledAsync and RespondToModalAsync validate before mutation, honor pre-write
cancellation, and serialize a command only for RemoteAgentChat. A modal response is accepted once
for an unresolved modal owned by this chat. Tool ids must exist. Interrupt is idempotent and affects only the active turn. Events are raised after the corresponding
state mutation, in wire-sequence order for a proxy. DisposeAsync is idempotent; local disposal ends
the local chat, while proxy disposal detaches that viewer and can therefore trigger owner-side
last-viewer stop. Existing engine-only public methods remain on AgentChat and are not added to
this UI contract.
Visibility/kind: existing public sealed class, additionally implements IAgentChat.
Behavior: existing foreground-scheduler guarantees remain. It exposes atomic Information and Usage values and an owner-backed IAgentInputQueues adapter over the existing queue managers.
That adapter is the sole public mutation path used by migrated UI code. Existing concrete scalar
and index-based queue members are implementation-migration forwarding members only. The new
cancellation token on the interface tool-toggle member is honored by the existing async mutation
path. No transport branch is added to this class and it remains sealed.
The current source already makes the correct steering decision at the owner: CopilotSdkChatClient
subscribes to AgentInputQueueManager.QueueStateChanged only while a turn is live and ForwardPendingImmediateMessages drains immediate items into CopilotSession.SendAsync with Mode = "immediate"; ToolResultSteeringMiddleware injects immediate items at tool-result
boundaries for ordinary clients. This remains internal implementation behavior. The public IChatSteeringTarget transport capability is narrowed to an internal split-client adapter seam;
no replacement steering method is added to IAgentChat, RemoteAgentChat, or RemoteAgentSessionClient. Copilot-specific members ForwardPendingImmediateMessages and SteeringMessageForwarded remain internal. During teardown,
the existing suspension gate leaves an item queued rather than losing it. For a non-Copilot client,
the same queued input is consumed at the next supported tool boundary or future turn; enqueue still
succeeds and never reports steering as unsupported.
AcquireAgentChatRequest, IRunningAgentChatTable, and running leases - Existing, Modified
Namespace/project/files:Phantom.Workspaces.Services; AcquireAgentChatRequest.cs, IRunningAgentChatTable.cs, RunningAgentChatTable.cs, RunningAgentChatWithEntityInfo.cs; and Phantom.Workspaces.Llm/RunningAgentChat*.cs.
Visibility/kind: existing public DTO/interface/sealed implementations.
publicenumAgentChatAcquisitionMode{Local,AttachRemote,StartOrAttachRemote}// Added init-only fields on AcquireAgentChatRequest:publicAgentChatAcquisitionModeAcquisitionMode{get;init;}=AgentChatAcquisitionMode.Local;publicITransport?OwningProfileTransport{get;init;}=null;publicReplayCursor?ReplayCursor{get;init;}=null;publicinterfaceIRunningAgentChatTable{ObservableCollection<RunningAgentChatWithEntityInfo>RunningSessions{get;}Task<RunningAgentChatLease>AcquireAsync(AcquireAgentChatRequestrequest,CancellationTokenct=default);Task<bool>TerminateAsync(AgentSessionIdsessionId,CancellationTokenct=default);TaskSetContinueInBackgroundAsync(AgentSessionIdsessionId,boolcontinueInBackground,CancellationTokenct=default);}
The three new request setters are ordinary init-only storage: they perform no I/O and clone no
transport. AcquireAsync validates their legal combinations (Local forbids OwningProfileTransport; remote modes require it and persisted owner/generation). The request does
not own or dispose the transport.
RunningAgentChatLease.AgentChat returns the acquired registered instance. The metadata getters on RunningAgentChatWithEntityInfo are owner/client-authoritative snapshots and raise the existing
property-change path before RunningAgentBrainViewModel.Refresh updates a row. Lease disposal is
idempotent and decrements local/proxy viewer ownership once; it does not send an explicit terminate,
but final release can invoke the default graceful-stop transition.
Common-surface UI consumers - Existing, Modified
AgentViewModel: Phantom.Workspaces.Agent.Gui.ViewModels; Phantom.Workspaces.Agent.Gui/ViewModels/AgentViewModel.cs; existing public sealed class.
Its constructor becomes AgentViewModel(AgentViewModelOptions options). AgentViewModelOptions
has required-init IAgentChat AgentChat, string DisplayName, string Description, ObservableLoggerFactory LoggerFactory, and TaskScheduler ForegroundScheduler, plus AgentViewModel? ParentAgentViewModel { get; init; } = null. Existing foreground validation
remains. It uses the
common collections/events, awaits async remote tool toggles, and owns a ReadOnlyObservableCollection<AgentSessionModalViewModel> Modals. Modal response calls Task RespondToModalAsync(string modalId, JsonElement response, CancellationToken ct = default),
which delegates to IAgentChat, preserves the modal until a dismiss event, and maps cancellation
or safe remote errors without optimistic removal.
Input is gated only while this editor has an unresolved modal; descendant modals affect only root
notification aggregation. Its existing public AgentChat getter changes to public IAgentChat AgentChat { get; }. Disposal unsubscribes before disposing its chat lease/proxy.
InputQueueViewModel: Phantom.Workspaces.Agent.Gui.ViewModels; existing public sealed class. Its constructor and
fields consume IAgentChat.InputQueues, IAgentInputQueue, and immutable snapshots rather than
concrete AgentChat, AgentChatQueue, or AgentInputQueueManager. Submit, create, edit, remove,
move, hold/release, and target selection await the aggregate commands. Controls stay unchanged
until the acknowledged owner delta/result is applied; conflict replaces the projection from the
returned snapshot and prompts/retries only after reconciling user intent. Changed is marshalled
to the existing foreground scheduler, and disposal unsubscribes without disposing the chat-owned
aggregate.
SlashCommandContext is Existing, Modified in Phantom.Workspaces.Llm.SlashCommands, Phantom.Workspaces.Llm.Core/SlashCommands/SlashCommandContext.cs: its public required IAgentChat AgentChat { get; init; } replaces the concrete type. The init setter
rejects null. Handlers needing engine-only APIs are not registered by RemoteAgentChat.SlashCommands;
common handlers use only IAgentChat. This prevents a hidden concrete cast in the open path.
Files:IAgentChat, property-based Usage and AgentInformation, IAgentInputQueues,
the seven queue request types and property-based snapshot/configuration/result types, owner queue
adapter, stable ids/revisions in the existing queue domain, AgentChat, RunningAgentChat, RunningAgentChatLease, RunningAgentChatWithEntityInfo, AgentViewModel, and AgentViewModelOptions, and InputQueueViewModel. Tests: required-init metadata, optional defaults, named initializers, serialization, AgentChatInterfaceTests, AgentInputQueuesTests, internal Copilot/non-Copilot queue consumption
tests, options/common-surface tests, and unchanged local regression suite. Migrate
the UI away from concrete queue collections and index identity. No transport behavior yet. Dependencies: none.
Part of #1483
Summary
Introduce the common local/remote chat and immutable queue contracts, migrate local ownership to stable ids and revisions, and keep queue-based Copilot steering internal with no public/protocol steering API.
Scope
Implement only commit 2 of the approved design. Preserve the contracts below exactly; local and remote behavior must remain compatible. This child owns the common state, queue, and UI-facing request/options surface only; no transport behavior is added here.
Files
Files:
IAgentChat, property-basedUsageandAgentInformation,IAgentInputQueues, the seven queue request types and property-based snapshot/configuration/result types, owner queue adapter, stable ids/revisions in the existing queue domain,AgentChat,RunningAgentChat,RunningAgentChatLease,RunningAgentChatWithEntityInfo,AgentViewModel,AgentViewModelOptions, andInputQueueViewModel.Tests: required-init metadata, optional defaults, named initializers, serialization,
AgentChatInterfaceTests,AgentInputQueuesTests, internal Copilot/non-Copilot queue consumption tests, options/common-surface tests, and unchanged local regression suite. Migrate the UI away from concrete queue collections and index identity. No transport behavior yet.Background and preserved design decisions
API shape convention
more independent arguments (including similarly typed identifiers), or any method whose positional
arguments are easy to transpose, takes one property-based
*Requestor*Optionsvalue plus anoptional
CancellationToken. Small cohesive operations remain direct, for exampleConnectAsync(AgentSessionOpenRequest request, CancellationToken ct = default).required init; optional members have explicit defaults. Constructors are reserved for enforcing ascalar value invariant or receiving a small cohesive set of services.
required initpayload members. Their fixedTypediscriminator is initialized by the concrete DTO and is not caller-selectable. This changes only
the C# construction shape: version-1 kebab-case JSON names, required/optional wire members,
discriminators, strict unknown-member rejection, and semantics remain unchanged.
from [mxc] - Streaming process executor backed by MXC #1474-[mxc] - Execute MCP tools through MXC-constrained process executor #1477 retain the signatures owned and tested by those designs. Neither case introduces a
new positional API in this design.
Considered/background decisions carried into this child
Existing queue classes expose owner objects and use indexes for some edits.
Resolution: retain
AgentInputQueue,AgentChatQueue,AgentInputQueueManager, andAgentChatQueueManageras local implementation types, but expose immutable queue/item snapshotsthrough
IAgentInputQueues. Assign stable ids at creation/enqueue, use indexes only as placementhints, and route every mutation through owner-authoritative revisioned commands.
Steering is an execution decision, not a separate kind of user input.
Resolution: there is no steering member on
IAgentChatand nosteerprotocol verb. A GUIenqueues through the same queue API whether a run is idle or active. The owning
AgentChatdecides from queue immediacy, mode, and current-run state whether to consume that item as
Copilot steering or as a future turn.
Detailed design
IAgentChat- NewPhantom.Workspaces.Llm;Phantom.Workspaces.Llm.Core/IAgentChat.cs.public interface : IAsyncDisposable, IServiceProvider.remote proxy. The implementation owns its observable collections and mutates them only on the
foreground scheduler captured at construction. It deliberately excludes process handles,
transports, trust profiles, and MXC objects.
The two state values are New public readonly record structs in
Phantom.Workspaces.Llm.Core/IAgentChat.cs, with exactly these public names, fields, types, andsemantics:
Usagepermits null for a metric the provider did not report; counts must otherwise be nonnegativeand cost remains a nonnegative
doublemeasured in USD.AgentInformationrequires non-null,nonblank values for its first five strings and a non-null complete
AgentDefinition;CurrentModelIdis null or nonblank. Required members provide compile-time construction checks;local publishers and the protocol codec validate value invariants before accepting,
serializing, or publishing them. Local implementations build a complete replacement value before
publishing it. Proxy implementations deserialize and validate a complete replacement value before
one foreground assignment.
UsageChangedandInformationChangedare raised only after that atomicassignment, once per applied session sequence; observers never see fields from different versions.
Record equality is the intended value equality.
Both records use the existing JSON options and kebab-case protocol naming. After authorization, the
owner serializes the full definition with
AgentDefinition.ToJson()and the client deserializes itwith
PhantomAgentSchema.AgentDefinitionFromJson(string). Every authorized attached GUI receivesthe same complete definition in
AgentInformation; this is mandatory protocol state, not anoptional capability. Authorization completes before runtime lookup, snapshot construction, or
definition serialization. An unauthorized peer receives only the indistinguishable sanitized
denial and no session metadata, definition bytes, existence signal, or redacted
AgentInformation.No field-level redaction is applied after authorization.
AgentSessionIdChangedandModelChangedare not on the common interface: those values are membersof the atomically replaced
AgentInformation, andInformationChangedis their sole common event.During implementation migration, existing scalar getters and events may temporarily forward from
Information/Usageon concreteAgentChatfor source compatibility. They are not the desiredpublic design, must not be added to
RemoteAgentChat, and are removed after local callers migrate.Common input queues - New
Phantom.Workspaces.Llm;Phantom.Workspaces.Llm.Core/IAgentInputQueues.cs.and proxy implementations and protocol DTOs are internal.
AgentViewModel/InputQueueViewModel, and has local and remote implementations with identicalsemantics.
AgentInputQueue,AgentChatQueue,AgentInputQueueManager,AgentChatQueueManager, andAgentInputItemremain the owner-side domain/implementation types; they are not serialized orreturned by the common interface. The implementation adds stable nonblank queue ids when default,
immediate, or custom queues are created and stable nonblank item ids when items are enqueued. Record
updates preserve item ids. Indexes may be calculated for display or placement, but commands identify
items only by id.
BeforeItemId == nullmeans append.MoveAsyncsupports reorder within one queueand movement between queues atomically.
Snapshots are immutable point-in-time values. Their arrays and messages are deep copied through
Microsoft.Extensions.AI.AIJsonUtilities.DefaultOptions; noObservableCollection, mutablemanager,
AgentInputQueue,AgentChatQueue,AgentInputItem,ResetSession, or other live ownerreference crosses the interface or wire.
IAgentInputQueueinstances are stable read models for thelife of a queue and atomically replace
Snapshot;Queuesis a read-only projection. Affectedqueues raise
Changed, then the aggregate raisesChanged, after all snapshots have been replacedon the captured foreground scheduler.
The aggregate revision increases once for every applied queue transaction; each affected queue's
revision also increases once. Every command compares
expectedRevisionwith the aggregate revision,which gives create/delete and cross-queue movement the same deterministic conflict rule. A
cross-queue move returns both affected queue snapshots in the delta. Blank ids/names, empty message
lists, invalid enum values, negative revisions/priorities, unknown queues/items, edits of consumed
items, deletion of default/immediate or nonempty queues, and configuration that changes fixed queue
roles are rejected before mutation.
Immediate,Queue, andHeldretain the existingAgentInputQueueImmediacysemantics. Hold/release isConfigureAsyncwithHeldor the desiredreleased immediacy; target selection is the explicit
targetQueueId.Commands are acknowledged and server-authoritative; there are no optimistic queue mutations.
Malformed caller arguments throw locally. Owner validation failures return
Rejectedwith a stablesafe error code and current revision. A stale expected revision returns
Conflict, performs nomutation, and includes the complete current aggregate snapshot; the client atomically replaces its
projection, then may retry as a new command after user intent is reconciled. Exact reuse of a
commandIdand canonical payload returnsDuplicatewith the original result and no delta; reusewith a different payload returns
Conflict. Cancellation before write performs no mutation;cancellation after write cancels only the wait and retrying the same id is safe.
The aggregate and per-queue read models live exactly as long as their owning
IAgentChat; callersborrow them and do not dispose them. After chat disposal, mutation methods throw
ObjectDisposedException, no furtherChangedevents are raised, and the last immutable snapshotremains readable. The local adapter unsubscribes from both existing queue managers during chat
disposal; the proxy adapter unsubscribes from its session client before that client detaches.
The owner captures queue mutations, including consumption by a run, on its serialized runtime
scheduler and broadcasts one
queue-changeddelta on the existing session stream. Every delta hasthe runtime epoch, global session sequence, resulting aggregate revision, affected full queue
snapshots, and removed queue ids. A fresh/reconnect session snapshot contains the full aggregate
snapshot. Replay applies deltas only in epoch/global-sequence order. Gaps, wrong epochs, or
noncontiguous queue revisions trigger normal reconnect/snapshot refresh. Multiple GUIs therefore
converge without sharing collections or trusting a client mutation.
AgentChatModaland its content hierarchy are New public immutable records in the samenamespace/file:
Initializer validation rejects blank ids/owner/title/body/type and invalid or duplicate
options/labels, and
clone option elements. The strict discriminator permits new modal content records in later protocol
versions without changing the modal envelope. All getters return the latest
foreground-applied state and never cross transport.
GetToolSnapshotreturns an immutable point-in-time copy. Note-enqueue methods reject null/blank text; user input uses
InputQueues.EnqueueAsync.SetToolEnabledAsyncandRespondToModalAsyncvalidate before mutation, honor pre-writecancellation, and serialize a command only for
RemoteAgentChat. A modal response is accepted oncefor an unresolved modal owned by this chat. Tool ids must exist.
Interruptis idempotent and affects only the active turn. Events are raised after the correspondingstate mutation, in wire-sequence order for a proxy.
DisposeAsyncis idempotent; local disposal endsthe local chat, while proxy disposal detaches that viewer and can therefore trigger owner-side
last-viewer stop. Existing engine-only public methods remain on
AgentChatand are not added tothis UI contract.
AgentChat- Existing, ModifiedPhantom.Workspaces.Llm;Phantom.Workspaces.Llm.Core/AgentChat.cs.public sealed class, additionally implementsIAgentChat.InformationandUsagevalues and an owner-backedIAgentInputQueuesadapter over the existing queue managers.That adapter is the sole public mutation path used by migrated UI code. Existing concrete scalar
and index-based queue members are implementation-migration forwarding members only. The new
cancellation token on the interface tool-toggle member is honored by the existing async mutation
path. No transport branch is added to this class and it remains sealed.
The current source already makes the correct steering decision at the owner:
CopilotSdkChatClientsubscribes to
AgentInputQueueManager.QueueStateChangedonly while a turn is live andForwardPendingImmediateMessagesdrains immediate items intoCopilotSession.SendAsyncwithMode = "immediate";ToolResultSteeringMiddlewareinjects immediate items at tool-resultboundaries for ordinary clients. This remains internal implementation behavior. The public
IChatSteeringTargettransport capability is narrowed to an internal split-client adapter seam;no replacement steering method is added to
IAgentChat,RemoteAgentChat, orRemoteAgentSessionClient. Copilot-specific membersForwardPendingImmediateMessagesandSteeringMessageForwardedremain internal. During teardown,the existing suspension gate leaves an item queued rather than losing it. For a non-Copilot client,
the same queued input is consumed at the next supported tool boundary or future turn; enqueue still
succeeds and never reports steering as unsupported.
AcquireAgentChatRequest,IRunningAgentChatTable, and running leases - Existing, ModifiedPhantom.Workspaces.Services;AcquireAgentChatRequest.cs,IRunningAgentChatTable.cs,RunningAgentChatTable.cs,RunningAgentChatWithEntityInfo.cs; andPhantom.Workspaces.Llm/RunningAgentChat*.cs.The three new request setters are ordinary init-only storage: they perform no I/O and clone no
transport.
AcquireAsyncvalidates their legal combinations (LocalforbidsOwningProfileTransport; remote modes require it and persisted owner/generation). The request doesnot own or dispose the transport.
RunningAgentChatLease.AgentChatreturns the acquired registered instance. The metadata getters onRunningAgentChatWithEntityInfoare owner/client-authoritative snapshots and raise the existingproperty-change path before
RunningAgentBrainViewModel.Refreshupdates a row. Lease disposal isidempotent and decrements local/proxy viewer ownership once; it does not send an explicit terminate,
but final release can invoke the default graceful-stop transition.
Common-surface UI consumers - Existing, Modified
AgentViewModel:Phantom.Workspaces.Agent.Gui.ViewModels;Phantom.Workspaces.Agent.Gui/ViewModels/AgentViewModel.cs; existingpublic sealed class.Its constructor becomes
AgentViewModel(AgentViewModelOptions options).AgentViewModelOptionshas required-init
IAgentChat AgentChat,string DisplayName,string Description,ObservableLoggerFactory LoggerFactory, andTaskScheduler ForegroundScheduler, plusAgentViewModel? ParentAgentViewModel { get; init; } = null. Existing foreground validationremains. It uses the
common collections/events, awaits async remote tool toggles, and owns a
ReadOnlyObservableCollection<AgentSessionModalViewModel> Modals. Modal response callsTask RespondToModalAsync(string modalId, JsonElement response, CancellationToken ct = default),which delegates to
IAgentChat, preserves the modal until a dismiss event, and maps cancellationor safe remote errors without optimistic removal.
Input is gated only while this editor has an unresolved modal; descendant modals affect only root
notification aggregation. Its existing public
AgentChatgetter changes topublic IAgentChat AgentChat { get; }. Disposal unsubscribes before disposing its chat lease/proxy.InputQueueViewModel:Phantom.Workspaces.Agent.Gui.ViewModels; existingpublic sealed class. Its constructor andfields consume
IAgentChat.InputQueues,IAgentInputQueue, and immutable snapshots rather thanconcrete
AgentChat,AgentChatQueue, orAgentInputQueueManager. Submit, create, edit, remove,move, hold/release, and target selection await the aggregate commands. Controls stay unchanged
until the acknowledged owner delta/result is applied; conflict replaces the projection from the
returned snapshot and prompts/retries only after reconciling user intent.
Changedis marshalledto the existing foreground scheduler, and disposal unsubscribes without disposing the chat-owned
aggregate.
The added/changed public signatures are:
SlashCommandContextis Existing, Modified inPhantom.Workspaces.Llm.SlashCommands,Phantom.Workspaces.Llm.Core/SlashCommands/SlashCommandContext.cs: itspublic required IAgentChat AgentChat { get; init; }replaces the concrete type. The init setterrejects null. Handlers needing engine-only APIs are not registered by
RemoteAgentChat.SlashCommands;common handlers use only
IAgentChat. This prevents a hidden concrete cast in the open path.Tests
AgentChatInterfaceTests(Phantom.Workspaces.Llm.Core.Tests)Information_LocalChat_ReturnsAtomicAgentInformation.Usage_LocalChat_ReturnsAtomicUsage.InputQueues_LocalChat_ReturnsCommonQueueAggregate.GetToolSnapshot_MutationAfterRead_DoesNotChangeReturnedSnapshot.SetToolEnabledAsync_KnownTool_ChangesStateThenRaisesToolsChanged.SetToolEnabledAsync_UnknownTool_ThrowsArgumentException.SetToolEnabledAsync_Cancelled_DoesNotMutateOrRaiseEvent.RespondToModalAsync_CurrentModal_AcceptsExactlyOnce.RespondToModalAsync_UnknownModal_ThrowsArgumentException.EnqueueSystemNote_ValidText_AppendsSystemNote.EnqueueHelpNote_ValidText_AppendsHelpNote.EnqueueTransientDiagnostic_ValidText_AppendsNonPersistedDiagnostic.Interrupt_ActiveTurn_CancelsTurnWithoutDisposingChat.Interrupt_NoActiveTurn_IsIdempotent.Usage_EqualValues_CompareEqual.Usage_DefaultInitialization_AllOptionalMetricsAreNull.Usage_NamedInitializer_PreservesExactMetricTypes.Usage_RoundTrip_PreservesNullableCountsAndDoubleUsd.UsagePublisher_NegativeMetric_RejectsBeforePublication.UsageChanged_CompleteReplacement_StateVisibleBeforeSingleEvent.AgentInformation_EqualValues_CompareEqual.AgentInformation_RequiredInitProperties_AreMarkedRequired.AgentInformation_NamedInitializer_PreservesAllFields.AgentInformationPublisher_InvalidRequiredString_RejectsBeforePublication.AgentInformationPublisher_InvalidOptionalModel_RejectsBeforePublication.AgentInformation_AuthorizedPeer_RoundTripsCompleteDefinition.AgentInformationPublisher_NullDefinition_RejectsBeforePublication.SessionSnapshot_TwoAuthorizedViewers_ReceiveEquivalentFullDefinition.OpenAsync_UnauthorizedPeer_SerializesNoSessionMetadata.InformationChanged_SessionAndModelChange_StateVisibleBeforeSingleEvent.TurnCompleted_TurnPersists_EventRaisedAfterHistoryMutation.DisposeAsync_RepeatedCall_DisposesOnce.GetService_KnownService_ReturnsExistingService.AgentInputQueuesTests(Phantom.Workspaces.Llm.Core.Tests)Snapshot_LocalQueue_ReturnsDeepImmutableCopy.QueueSnapshotPublisher_InvalidRevisionOrDuplicateIds_RejectsBeforePublication.QueueSnapshotPublisher_InvalidIdentityRoleOrRevision_RejectsBeforePublication.QueueSnapshotPublisher_InvalidItemIdentityOrMessages_RejectsBeforePublication.QueueCommandValidator_InvalidNameImmediacyOrPriority_RejectsBeforeMutation.AgentInputQueueCommandResult_RoundTrip_PreservesStatusRevisionAndSnapshot.QueueRequestTypes_RequiredInitProperties_AreMarkedRequired.QueueRequestTypes_OptionalProperties_UseDocumentedDefaults.QueueRequestTypes_NamedInitializers_MapToExactCommandSerialization.Queues_LocalAndProxy_ExposeEquivalentReadModels.DefaultQueue_LocalAndProxy_ReturnSameStableQueueId.ImmediateQueue_LocalAndProxy_ReturnSameStableQueueId.CreateQueueAsync_ValidConfiguration_AssignsStableQueueIdAndRevision.DeleteQueueAsync_CustomQueue_RemovesQueueOnce.DeleteQueueAsync_DefaultOrImmediateQueue_ReturnsRejected.EnqueueAsync_DefaultImmediateHeldAndCustomTargets_AssignsStableItemIds.EnqueueAsync_CancelledBeforeMutation_DoesNotChangeRevision.EditAsync_ExistingItem_PreservesItemIdAndAdvancesRevision.RemoveAsync_ExistingItem_RemovesByIdNotIndex.MoveAsync_BeforeItem_ReordersByStableIds.MoveAsync_DifferentTarget_MovesAtomicallyAcrossQueues.ConfigureAsync_ImmediateQueueInvalidRoleChange_ReturnsRejected.ConfigureAsync_HoldAndRelease_ChangesImmediacy.Command_StaleExpectedRevision_ReturnsConflictAndAuthoritativeSnapshot.Command_DuplicateIdSamePayload_ReturnsOriginalResultWithoutSecondMutation.Command_DuplicateIdDifferentPayload_ReturnsConflict.Changed_AppliedCommand_ReplacesSnapshotBeforeEvent.QueueChanged_AppliedCommand_ReplacesQueueSnapshotBeforeEvent.QueueConsumption_ActiveRun_AdvancesRevisionAndRaisesChanged.EnqueueAsync_ActiveCopilotRun_ConsumesAsInternalSteering.EnqueueAsync_ActiveNonCopilotRun_RemainsQueuedUntilSupportedBoundaryOrFutureTurn.CurrentSessionContextTestsandAgentServicesTestsCurrentSessionContext_ValidOwnerGenerationEpoch_PreservesOwningHostIdentity.CurrentSessionContext_BlankOwner_RejectsInitialization.CurrentSessionContext_NegativeGeneration_RejectsInitialization.CurrentSessionContext_AttachmentPeer_DoesNotReplaceHostIdentity.AgentServices_AgentExecutionTrustContextSetter_WithExpressionPreservesOtherServices.AgentServices_RemoteRuntimeIntentSetter_WithExpressionPreservesOtherServices.AgentServices_GetService_NewObjectTypedSeams_DoesNotExposeConcreteTypes.AcquireAgentChatRequest_RemoteInitProperties_PreserveModeTransportAndCursor.AcquireAgentChatRequest_Defaults_SelectLocalModeWithoutTransportOrCursor.AcquireAgentChatRequest_InvalidModeCombination_AcquireRejectsRequest.AgentViewModel_AgentChatProperty_LocalAndRemote_ReturnsIAgentChat.RunningAgentChatLease_AgentChatProperty_LocalAndRemote_ReturnsIAgentChat.RunningAgentChatWithEntityInfo_RetentionMetadataChange_RaisesAuthoritativeUpdate.UI public API tests
AgentViewModelTests:Constructor_RemoteChat_UsesCommonSurfaceWithoutConcreteCast.AgentViewModelOptions_NamedInitializer_PreservesRequiredValuesAndParentDefault.Constructor_WrongForegroundContext_Throws.RespondToModalAsync_CurrentModal_SendsResponseAndKeepsInputGatedUntilDismissed.RespondToModalAsync_UnknownModal_ThrowsArgumentException.RespondToModalAsync_Cancelled_DoesNotDismissModal.ModalEvent_DescendantModal_UpdatesRootAggregateOnly.DisposeAsync_RemoteChat_UnsubscribesBeforeDetaching.InterruptCommand_RemoteChat_InvokesCommonInterrupt.ConfigureSlashCommands_RemoteChat_RegistersOnlyCommonHandlers.InputQueueViewModelTests:CommandPending_RemoteQueue_DoesNotMutateProjectionOptimistically.CommandConflict_StaleRevision_RefreshesFromAuthoritativeSnapshot.CommandApplied_AuthoritativeDeltaAppliedBeforeTaskCompletes.SlashCommandContextTests:AgentChatSetter_LocalOrRemote_PreservesCommonChat.AgentChatSetter_Null_RejectsInitialization.Implementation commit mapping
Commit 2 - Add the common chat surface
Files:
IAgentChat, property-basedUsageandAgentInformation,IAgentInputQueues,the seven queue request types and property-based snapshot/configuration/result types, owner queue
adapter, stable ids/revisions in the existing queue domain,
AgentChat,RunningAgentChat,RunningAgentChatLease,RunningAgentChatWithEntityInfo,AgentViewModel, andAgentViewModelOptions, andInputQueueViewModel.Tests: required-init metadata, optional defaults, named initializers, serialization,
AgentChatInterfaceTests,AgentInputQueuesTests, internal Copilot/non-Copilot queue consumptiontests, options/common-surface tests, and unchanged local regression suite. Migrate
the UI away from concrete queue collections and index identity. No transport behavior yet.
Dependencies: none.
Dependencies
None.