Part of #1483
Summary
Define the strict version-1 attach-agent-session protocol, property-based protocol/client proxy DTOs, RemoteAgentSessionClient, and RemoteAgentChat with authoritative ordered state, complete AgentDefinition for every authorized viewer, reconnect/replay, queue/tool/modal/subagent commands, and sanitized wire errors.
Scope
Implement only commit 3 of the approved design. Preserve the contracts below exactly; local and remote behavior must remain compatible. This detail owns protocol/client/proxy signatures and DTOs only; it does not add server runtime, attach authorization, host dispatch, viewer lifecycle, or MXC integration.
Implementation commit mapping
- Approved design SHA:
0c6578641befad31d5a38003baf6c0f0fe64748a.
- This issue maps exactly to Commit 3 - Add strict protocol and proxy state.
- Boundary: use an in-memory
IMessageChannel; no server runtime or MXC dependency.
- The client/proxy surface must stay compatible with the common chat/state/queue contracts and support later host/runtime integration without changing the public request/options or protocol DTO shapes below.
Files
Files: Phantom.Workspaces.Llm.Core/Remote/AgentSessionProtocol.cs, property-based protocol DTOs and codec, Phantom.Workspaces.Llm.Core/Remote/RemoteAgentSessionClient.cs, Phantom.Workspaces.Llm.Core/Remote/RemoteAgentChat.cs, Phantom.Workspaces.Llm.Core/Remote/RemoteAgentSessionException.cs, and RemoteAgentChatAttachOptions.
Tests: required-init metadata, optional defaults, named-initializer construction, exact discriminator/serialization round trips, codec, all client public methods, atomic usage/information, proxy queue parity, full authorized AgentDefinition, no metadata on denial, background command/event/snapshot, revisions/conflicts/deduplication, state/events/commands, cancellation, and disposal. The protocol contains enqueue/edit/remove/move/configure/create/delete queue commands and no steering command.
Preserved background and design decisions
attach-agent-session remains a dedicated bidirectional session protocol, separate from the chat-client request/response protocol. ChatClientOverTransport stays the lower-level remote IChatClient mechanism and is not the remote-session proxy.
- Multiple independent values use property-based
*Request / *Options records with required init members and documented optional defaults. Constructors remain only for compact value-object invariants or a small cohesive set of service dependencies.
- Protocol DTO/property records preserve fixed serializers and discriminators.
AgentSessionServerFrame.Type is codec-assigned from the concrete event discriminator and is not caller-selectable.
- Authorized viewers receive the same complete
AgentDefinition in AgentInformation; unauthorized peers receive no session metadata, no definition bytes, and no existence signal.
- Queue mutation is owner-authoritative and revisioned. There is no steering member on
IAgentChat and no steer protocol verb. InterruptAsync(Guid commandId, CancellationToken ct = default) remains direct; do not add a one-property InterruptAgentSessionRequest.
JsonElement remains limited to already-versioned domain payloads; strict top-level request/frame members still reject unknown properties.
- This detail preserves authoritative ordered snapshot-plus-delta state, reconnect by last applied cursor, and sanitized wire failures only.
Detailed design
Usage and AgentInformation - Shared protocol snapshot state
The protocol snapshot and replacement state use the exact common property-based forms below. These names, fields, types, and semantics are authoritative for this detail because the proxy and wire contracts consume them directly.
public readonly record struct Usage
{
public Usage() { }
public long? TotalInputTokenCount { get; init; } = null;
public long? TotalOutputTokenCount { get; init; } = null;
public long? TotalCacheReadTokenCount { get; init; } = null;
public long? TotalCacheWriteTokenCount { get; init; } = null;
public long? TotalReasoningTokenCount { get; init; } = null;
public double? TotalSessionCostUsd { get; init; } = null;
}
public readonly record struct AgentInformation
{
public AgentInformation() { }
public required string AgentSessionId { get; init; }
public required string AgentId { get; init; }
public required string Name { get; init; }
public required string DisplayName { get; init; }
public required string Description { get; init; }
public required bool AcceptsUserInput { get; init; }
public string? CurrentModelId { get; init; } = null;
public required AgentDefinition AgentDefinition { get; init; }
}
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. 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.
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.
RemoteAgentChat - New
- Namespace/project/file:
Phantom.Workspaces.Llm.Remote;
Phantom.Workspaces.Llm.Core/Remote/RemoteAgentChat.cs.
- Visibility/kind:
public sealed class : IAgentChat.
- Responsibility/lifetime/threading: owns a proxy-only
AgentChatHistoryCollection, immutable queue/running item projections, tools, subagent proxies, slash-command facade, Usage, and AgentInformation. It owns one RemoteAgentSessionClient, applies frames on its supplied foreground scheduler, and never owns the remote runtime.
public static Task<RemoteAgentChat> AttachAsync(
RemoteAgentChatAttachOptions options, CancellationToken ct = default);
public Task DetachAsync(CancellationToken ct = default);
public Task TerminateAsync(CancellationToken ct = default);
// IAgentChat members have the exact signatures above.
public sealed record RemoteAgentChatAttachOptions
{
public required RemoteAgentSessionClient Client { get; init; }
public required AgentSessionOpenRequest OpenRequest { get; init; }
public required TaskScheduler ForegroundScheduler { get; init; }
}
var chat = await RemoteAgentChat.AttachAsync(
new RemoteAgentChatAttachOptions
{
Client = client,
OpenRequest = openRequest,
ForegroundScheduler = foregroundScheduler,
},
ct);
AttachAsync validates arguments, waits for the first authoritative snapshot, then publishes the object; cancellation before snapshot disposes the channel and publishes nothing. DetachAsync sends best-effort explicit detach once and disposes proxy state; owner-side release stops the runtime when this is the final viewer and background continuation is disabled. TerminateAsync requires the current epoch, crosses transport, and completes only after a terminal event or safe command error. Commands after detach/disposal throw ObjectDisposedException; stale epoch maps to RemoteAgentSessionException("runtime-changed"). Local-only slash commands are not advertised by the proxy. Event production follows apply-state-then-notify ordering.
RemoteAgentSessionClient - New
- Namespace/project/file:
Phantom.Workspaces.Llm.Remote;
Phantom.Workspaces.Llm.Core/Remote/RemoteAgentSessionClient.cs.
- Visibility/kind:
public sealed class : IAsyncDisposable.
- Responsibility/lifetime/threading: owns one
ITransport-opened IMessageChannel, one receive pump, pending command completions, and the last accepted cursor. It does not mutate UI collections.
public event EventHandler<AgentSessionServerFrame>? FrameReceived;
public ReplayCursor? LastAppliedCursor { get; }
public RemoteAgentSessionClient(ITransport transport);
public static Task<AgentSessionRemoteStatus> GetStatusAsync(
AgentSessionStatusRequest request, CancellationToken ct = default);
public Task ConnectAsync(AgentSessionOpenRequest request, CancellationToken ct = default);
public Task ReconnectAsync(CancellationToken ct = default);
public Task<AgentInputQueueCommandResult> CreateQueueAsync(
CreateAgentInputQueueRequest request, CancellationToken ct = default);
public Task<AgentInputQueueCommandResult> DeleteQueueAsync(
DeleteAgentInputQueueRequest request, CancellationToken ct = default);
public Task<AgentInputQueueCommandResult> EnqueueAsync(
EnqueueAgentInputRequest request, CancellationToken ct = default);
public Task<AgentInputQueueCommandResult> EditAsync(
EditAgentInputQueueItemRequest request, CancellationToken ct = default);
public Task<AgentInputQueueCommandResult> RemoveAsync(
RemoveAgentInputQueueItemRequest request, CancellationToken ct = default);
public Task<AgentInputQueueCommandResult> MoveAsync(
MoveAgentInputQueueItemRequest request, CancellationToken ct = default);
public Task<AgentInputQueueCommandResult> ConfigureAsync(
ConfigureAgentInputQueueRequest request, CancellationToken ct = default);
public Task InterruptAsync(Guid commandId, CancellationToken ct = default);
public Task TerminateAsync(
TerminateAgentSessionRequest request, CancellationToken ct = default);
public Task<RemoteSubagentDescriptor> OpenSubagentAsync(
OpenAgentSubagentRequest request, CancellationToken ct = default);
public Task RespondToModalAsync(
RespondToAgentModalRequest request, CancellationToken ct = default);
public Task SetToolEnabledAsync(
SetAgentToolEnabledRequest request, CancellationToken ct = default);
public Task SetContinueInBackgroundAsync(
SetAgentSessionRetentionRequest request, CancellationToken ct = default);
public Task DetachAsync(CancellationToken ct = default);
public ValueTask DisposeAsync();
public sealed record AgentSessionStatusRequest
{
public required ITransport Transport { get; init; }
public required AgentSessionOpenRequest OpenRequest { get; init; }
}
public sealed record TerminateAgentSessionRequest
{
public required string Reason { get; init; }
public required Guid CommandId { get; init; }
}
public sealed record OpenAgentSubagentRequest
{
public required string AgentId { get; init; }
public required Guid CommandId { get; init; }
}
public sealed record RespondToAgentModalRequest
{
public required string ModalId { get; init; }
public required JsonElement Response { get; init; }
public required Guid CommandId { get; init; }
}
public sealed record SetAgentToolEnabledRequest
{
public required string ToolId { get; init; }
public required bool Enabled { get; init; }
public required Guid CommandId { get; init; }
}
public sealed record SetAgentSessionRetentionRequest
{
public required bool ContinueInBackground { get; init; }
public required Guid CommandId { get; init; }
}
var status = await RemoteAgentSessionClient.GetStatusAsync(
new AgentSessionStatusRequest
{
Transport = transport,
OpenRequest = new AgentSessionOpenRequest
{
ProtocolVersion = 1,
AgentSessionId = agentSessionId,
ExpectedOwningProfileEntityId = owningProfileEntityId,
ExpectedOwnershipGeneration = ownershipGeneration,
OpenIntent = AgentSessionOpenIntent.Status,
AttachmentToken = attachmentToken,
Capabilities = capabilities,
},
},
ct);
await client.MoveAsync(
new MoveAgentInputQueueItemRequest
{
SourceQueueId = sourceQueueId,
ItemId = itemId,
TargetQueueId = targetQueueId,
CommandId = commandId,
ExpectedRevision = revision,
},
ct);
await client.TerminateAsync(
new TerminateAgentSessionRequest
{
Reason = "user-requested",
CommandId = commandId,
},
ct);
GetStatusAsync requires OpenIntent = Status, borrows the transport, authorizes before lookup, returns Running or NotRunning to an authorized peer, maps denial/not-found/unsafe failure to Unavailable, closes its one-shot channel, and returns no snapshot or session metadata. ConnectAsync is the initial-open operation and may succeed once; it stores the validated request and serializes it into ITransport.ConnectToMessageChannelAsync, starts one reader, and completes after snapshot/replay validation. A second call throws InvalidOperationException. After unexpected channel loss, ReconnectAsync may be called while the five-second grace remains; it single-flights reconnect, reuses the original peer-bound attachment token, forces OpenIntent = Attach regardless of the initial request's intent, supplies LastAppliedCursor, replaces the channel and pump, and completes after replay/snapshot validation. It can therefore reclaim only the reserved existing attachment and can never start a replacement runtime after grace expiry. Calls while connected, after explicit detach/terminal/disposal, or after grace expiry throw InvalidOperationException. Cancellation stops only that attempt and leaves another attempt possible before the deadline.
RemoteAgentChat automatically calls ReconnectAsync after unexpected loss with delays of 250 ms, 500 ms, then one second until the grace deadline; an accepted terminal/not-found result ends retry. Each typed command method validates its payload and nonempty caller-supplied command id, requires a connected nonterminal epoch, serializes the corresponding strict DTO, and waits for its correlated acknowledgement/error. Cancellation cancels only the caller's wait after a successful write; command ids make retry safe. Queue methods are the transport implementation behind the proxy IAgentInputQueues and have the same validation/result semantics as that interface. An applied queue task completes only after the proxy has applied the authoritative result/delta revision; no caller observes completion against a stale projection. OpenSubagentAsync returns only the authorized child session/open descriptor. SetToolEnabledAsync completes only after the owner-authoritative tools-changed event has been applied; rejection does not alter the proxy tool snapshot. SetContinueInBackgroundAsync completes only after the owner has persisted the preference and the matching ordered retention event has been applied. FrameReceived is emitted synchronously in validated epoch and sequence order; gaps, regressions, unknown discriminators, or mismatched correlations close the channel with RemoteAgentProtocolException. DetachAsync is idempotent and best effort. DisposeAsync cancels the pump and channel without sending terminate-session; releasing the attachment can still cause default last-viewer stop. The constructor rejects a null transport; the client borrows the process-scoped transport and owns only its opened channel.
Protocol records and strict codec - New
- Namespace/project/file:
Phantom.Workspaces.Llm.Remote;
Phantom.Workspaces.Llm.Core/Remote/AgentSessionProtocol.cs.
- Visibility/kind:
RuntimeEpoch, ReplayCursor, AgentSessionOpenRequest, AgentSessionServerFrame, AgentSessionOpenIntent, and RemoteSubagentDescriptor are public immutable records because the client/core boundary consumes them. Commands, events, and AgentSessionProtocolCodec are internal.
public readonly record struct RuntimeEpoch
{
public required Guid Value { get; init; }
}
public readonly record struct ReplayCursor
{
public required RuntimeEpoch Epoch { get; init; }
public required long Sequence { get; init; }
}
public enum AgentSessionOpenIntent { Status, Start, Attach, StartOrAttach, Resume }
public enum AgentSessionRemoteStatus { Running, NotRunning, Unavailable }
public sealed record AgentSessionOpenRequest
{
public required int ProtocolVersion { get; init; }
public required string AgentSessionId { get; init; }
public required string ExpectedOwningProfileEntityId { get; init; }
public required long ExpectedOwnershipGeneration { get; init; }
public required AgentSessionOpenIntent OpenIntent { get; init; }
public required string AttachmentToken { get; init; }
public ReplayCursor? ReplayCursor { get; init; } = null;
public required IReadOnlyList<string> Capabilities { get; init; }
}
internal abstract record AgentSessionCommand
{
public abstract string Type { get; }
public required Guid CommandId { get; init; }
public required Guid CorrelationId { get; init; }
public required RuntimeEpoch RuntimeEpoch { get; init; }
}
public sealed record RemoteSubagentDescriptor
{
public required string AgentSessionId { get; init; }
public required string AgentId { get; init; }
public required string OwningProfileEntityId { get; init; }
public required long OwnershipGeneration { get; init; }
public required RuntimeEpoch RuntimeEpoch { get; init; }
}
public sealed record AgentSessionServerFrame
{
public required int ProtocolVersion { get; init; }
public string Type { get; internal init; } = null!;
public required Guid CorrelationId { get; init; }
public required RuntimeEpoch RuntimeEpoch { get; init; }
public required long Sequence { get; init; }
public required JsonElement Payload { get; init; }
}
internal abstract record AgentSessionServerEvent
{
public abstract string Type { get; }
}
internal sealed record SessionStatusEvent : AgentSessionServerEvent
{
public override string Type => "session-status";
public required AgentSessionRemoteStatus Status { get; init; }
}
internal sealed record AgentSessionTakeoverRequest
{
public required string AgentSessionId { get; init; }
public required string ExpectedOwningProfileEntityId { get; init; }
public required long ExpectedOwnershipGeneration { get; init; }
public required string NewOwningProfileEntityId { get; init; }
public required Guid CorrelationId { get; init; }
}
var openRequest = new AgentSessionOpenRequest
{
ProtocolVersion = 1,
AgentSessionId = agentSessionId,
ExpectedOwningProfileEntityId = owningProfileEntityId,
ExpectedOwnershipGeneration = ownershipGeneration,
OpenIntent = AgentSessionOpenIntent.Attach,
AttachmentToken = attachmentToken,
Capabilities = capabilities,
};
AgentSessionServerFrame.Type is a required wire member but deliberately not a public initializer: the internal codec derives it from the concrete AgentSessionServerEvent.Type on serialization and sets it only after recognizing a supported discriminator on deserialization. It is therefore fixed like each concrete command/event discriminator rather than caller-selectable. Encoding rejects any frame whose internally assigned type is null, blank, or not the recognized event discriminator.
The strict version-1 command records are:
internal sealed record CreateQueueCommand : AgentSessionCommand
{
public override string Type => "create-queue";
public required long ExpectedRevision { get; init; }
public required AgentInputQueueConfiguration Configuration { get; init; }
}
internal sealed record DeleteQueueCommand : AgentSessionCommand
{
public override string Type => "delete-queue";
public required long ExpectedRevision { get; init; }
public required string QueueId { get; init; }
}
internal sealed record EnqueueInputCommand : AgentSessionCommand
{
public override string Type => "enqueue-input";
public required long ExpectedRevision { get; init; }
public required string TargetQueueId { get; init; }
public required JsonElement Messages { get; init; }
}
internal sealed record EditQueueItemCommand : AgentSessionCommand
{
public override string Type => "edit-queue-item";
public required long ExpectedRevision { get; init; }
public required string QueueId { get; init; }
public required string ItemId { get; init; }
public required JsonElement Messages { get; init; }
}
internal sealed record RemoveQueueItemCommand : AgentSessionCommand
{
public override string Type => "remove-queue-item";
public required long ExpectedRevision { get; init; }
public required string QueueId { get; init; }
public required string ItemId { get; init; }
}
internal sealed record MoveQueueItemCommand : AgentSessionCommand
{
public override string Type => "move-queue-item";
public required long ExpectedRevision { get; init; }
public required string SourceQueueId { get; init; }
public required string ItemId { get; init; }
public required string TargetQueueId { get; init; }
public string? BeforeItemId { get; init; } = null;
}
internal sealed record ConfigureQueueCommand : AgentSessionCommand
{
public override string Type => "configure-queue";
public required long ExpectedRevision { get; init; }
public required string QueueId { get; init; }
public required AgentInputQueueConfiguration Configuration { get; init; }
}
internal sealed record InterruptCommand : AgentSessionCommand
{
public override string Type => "interrupt";
}
internal sealed record TerminateSessionCommand : AgentSessionCommand
{
public override string Type => "terminate-session";
public required string Reason { get; init; }
}
internal sealed record OpenSubagentCommand : AgentSessionCommand
{
public override string Type => "open-subagent";
public required string AgentId { get; init; }
}
internal sealed record ModalResponseCommand : AgentSessionCommand
{
public override string Type => "modal-response";
public required string ModalId { get; init; }
public required JsonElement Response { get; init; }
}
internal sealed record SetToolEnabledCommand : AgentSessionCommand
{
public override string Type => "set-tool-enabled";
public required string ToolId { get; init; }
public required bool Enabled { get; init; }
}
internal sealed record SetContinueInBackgroundCommand : AgentSessionCommand
{
public override string Type => "set-continue-in-background";
public required bool ContinueInBackground { get; init; }
}
internal sealed record DetachCommand : AgentSessionCommand
{
public override string Type => "detach";
}
The strict version-1 event records all inherit AgentSessionServerEvent; the frame supplies version/correlation/epoch/sequence exactly once:
internal sealed record SessionSnapshotEvent : AgentSessionServerEvent
{
public override string Type => "session-snapshot";
public required AgentSessionSnapshot Snapshot { get; init; }
}
internal sealed record HistoryAppendedEvent : AgentSessionServerEvent
{
public override string Type => "history-appended";
public required JsonElement Item { get; init; }
}
internal sealed record UsageChangedEvent : AgentSessionServerEvent
{
public override string Type => "usage-changed";
public required Usage Usage { get; init; }
}
internal sealed record AgentInformationChangedEvent : AgentSessionServerEvent
{
public override string Type => "agent-information-changed";
public required AgentInformation Information { get; init; }
}
internal sealed record QueueChangedEvent : AgentSessionServerEvent
{
public override string Type => "queue-changed";
public required long Revision { get; init; }
public required IReadOnlyList<AgentInputQueueSnapshot> Queues { get; init; }
public required IReadOnlyList<string> RemovedQueueIds { get; init; }
}
internal sealed record StreamingStartedEvent : AgentSessionServerEvent
{
public override string Type => "streaming-started";
public required string RunId { get; init; }
public required JsonElement Item { get; init; }
}
internal sealed record StreamingUpdatedEvent : AgentSessionServerEvent
{
public override string Type => "streaming-updated";
public required string RunId { get; init; }
public required JsonElement Update { get; init; }
}
internal sealed record StreamingCompletedEvent : AgentSessionServerEvent
{
public override string Type => "streaming-completed";
public required string RunId { get; init; }
public required JsonElement Item { get; init; }
}
internal sealed record BusyChangedEvent : AgentSessionServerEvent
{
public override string Type => "busy-changed";
public required bool IsBusy { get; init; }
}
internal sealed record ToolsSnapshotEvent : AgentSessionServerEvent
{
public override string Type => "tools-snapshot";
public required IReadOnlyList<JsonElement> Tools { get; init; }
}
internal sealed record ToolsChangedEvent : AgentSessionServerEvent
{
public override string Type => "tools-changed";
public required IReadOnlyList<JsonElement> Tools { get; init; }
}
internal sealed record SubagentsSnapshotEvent : AgentSessionServerEvent
{
public override string Type => "subagents-snapshot";
public required IReadOnlyList<JsonElement> Subagents { get; init; }
}
internal sealed record SubagentsChangedEvent : AgentSessionServerEvent
{
public override string Type => "subagents-changed";
public required IReadOnlyList<JsonElement> Subagents { get; init; }
}
internal sealed record ModalRaisedEvent : AgentSessionServerEvent
{
public override string Type => "modal-raised";
public required AgentChatModal Modal { get; init; }
}
internal sealed record ModalUpdatedEvent : AgentSessionServerEvent
{
public override string Type => "modal-updated";
public required AgentChatModal Modal { get; init; }
}
internal sealed record ModalDismissedEvent : AgentSessionServerEvent
{
public override string Type => "modal-dismissed";
public required string ModalId { get; init; }
}
internal sealed record SessionRetentionChangedEvent : AgentSessionServerEvent
{
public override string Type => "session-retention-changed";
public required bool ContinueInBackground { get; init; }
public required int ViewerCount { get; init; }
}
internal sealed record CommandCompletedEvent : AgentSessionServerEvent
{
public override string Type => "command-completed";
public required Guid CommandId { get; init; }
public JsonElement? Result { get; init; } = null;
}
internal sealed record OperationErrorEvent : AgentSessionServerEvent
{
public override string Type => "operation-error";
public required RemoteAgentOperationError Error { get; init; }
}
internal sealed record SessionTerminalEvent : AgentSessionServerEvent
{
public override string Type => "session-terminal";
public required string Reason { get; init; }
public required JsonElement CompletionState { get; init; }
}
internal sealed record AgentSessionSnapshot
{
public required AgentInformation Information { get; init; }
public required Usage Usage { get; init; }
public required AgentInputQueuesSnapshot InputQueues { get; init; }
public required bool IsBusy { get; init; }
public required IReadOnlyList<JsonElement> History { get; init; }
public required IReadOnlyList<JsonElement> RunningItems { get; init; }
public required IReadOnlyList<JsonElement> Tools { get; init; }
public required IReadOnlyList<JsonElement> Subagents { get; init; }
public required IReadOnlyList<AgentChatModal> Modals { get; init; }
public required bool ContinueInBackground { get; init; }
public required int ViewerCount { get; init; }
public JsonElement? CompletionState { get; init; } = null;
}
JsonElement is used only for already-versioned domain payloads whose polymorphism is owned by PhantomAgentSchema or Microsoft.Extensions.AI.AIJsonUtilities.DefaultOptions; each element is cloned before the read buffer advances. It is never used to bypass strict top-level member checking. The protocol codec derives AgentSessionServerFrame.Payload from the concrete event's named properties when encoding and selects the concrete event type from the recognized raw type discriminator before decoding that payload; generic serializer polymorphism is not used.
RemoteAgentSessionException is a new public sealed exception with string Code { get; }, string Operation { get; }, bool IsRetryable { get; }, and Guid CorrelationId { get; }. Its internal factory accepts only RemoteAgentOperationError. It contains the safe message only; local exceptions are retained solely in host logs.
Version 1 uses kebab-case JSON and rejects unknown members. Queue message arrays use Microsoft.Extensions.AI.AIJsonUtilities.DefaultOptions; agent definitions use AgentDefinition.ToJson() and PhantomAgentSchema.AgentDefinitionFromJson(string) after the authorization gate described above. Required fields are non-null and ids are nonempty. Initializer/codec validation rejects an empty epoch, negative cursor sequence, unsupported protocol version, negative generation, and duplicate/unknown capabilities. Sequence is positive and increases for every server frame in an epoch. The snapshot sequence is its high-water mark; replay starts at cursor+1. CommandId is the stable idempotency key and is reused across retries; CorrelationId identifies one wire attempt. Acknowledgement/error frames echo that attempt's correlation id, while unsolicited events use a fresh correlation id.
Open descriptor: type:"attach-agent-session", protocol-version, agent-session-id, expected-owning-profile-entity-id, expected-ownership-generation, open-intent (status, start, attach, start-or-attach, resume), a cryptographically random 128-bit attachment-token, optional replay-cursor:{runtime-epoch,sequence}, and capabilities. The token is scoped to the authenticated peer and runtime epoch and is retained only for the five-second unexpected-loss grace. agent-definition is not a negotiable capability. Commands are:
| Discriminator |
Required payload |
Semantics |
create-queue |
expected aggregate revision, name, configuration |
Create one custom queue with an owner-issued stable id. |
delete-queue |
expected aggregate revision, queue id |
Delete a custom queue; default/immediate queues are rejected. |
enqueue-input |
expected aggregate revision, target queue id, messages |
Enqueue once and assign an owner-issued stable item id. |
edit-queue-item |
expected aggregate revision, queue id, item id, messages |
Replace item messages while preserving item id. |
remove-queue-item |
expected aggregate revision, queue id, item id |
Remove an unconsumed item by id. |
move-queue-item |
expected aggregate revision, source/item/target ids, optional before-item id |
Atomically reorder or move an item. |
configure-queue |
expected aggregate revision, queue id, configuration |
Rename or change priority/coalescing/immediacy, including hold/release. |
interrupt |
none |
Cancel active turn; repeated calls succeed without terminating. |
terminate-session |
reason |
Fence, terminate, and emit terminal once. |
open-subagent |
agent-id |
Reauthorize child membership and return child attach descriptor; never local re-parent. |
modal-response |
modal-id, response |
Accept only the current unresolved modal; duplicate command id returns prior result. |
set-tool-enabled |
tool-id, boolean enabled |
Reauthorize and update owner tool state; complete only after the ordered tools event is applied. |
set-continue-in-background |
boolean continue-in-background |
Persist the per-session preference, publish authoritative retention state, and stop immediately if set false at zero viewers. |
detach |
none |
Remove only this attachment; no later frame is required. |
Server frames are:
| Discriminator |
Required payload |
session-status |
running, not-running, or unavailable; terminal one-shot response with no session metadata |
session-snapshot |
AgentInformation with full definition, Usage, full queue snapshot, history, running/streaming state, busy, tools, subagents, modals, continue-in-background, viewer count, terminal state |
history-appended |
serialized history item |
usage-changed |
complete replacement Usage |
agent-information-changed |
complete replacement AgentInformation |
queue-changed |
aggregate revision, complete affected queue snapshots, removed queue ids |
streaming-started / streaming-updated / streaming-completed |
run id and serialized item/update |
busy-changed |
boolean busy |
tools-snapshot / tools-changed |
tool ids, display state, enabled/status |
subagents-snapshot / subagents-changed |
child identity, display, completion, parent relationship |
modal-raised / modal-updated / modal-dismissed |
modal id, owner agent id, title, body, strict typed content/response state |
session-retention-changed |
authoritative continue-in-background and nonnegative logical viewer count |
command-completed |
command id and optional result (including child descriptor) |
operation-error |
RemoteAgentOperationError |
session-terminal |
safe reason and final completion state |
RemoteAgentOperationError is an internal strict property record:
internal sealed record RemoteAgentOperationError
{
public required string Code { get; init; }
public required string Operation { get; init; }
public required bool IsRetryable { get; init; }
public required string Message { get; init; }
public required Guid CorrelationId { get; init; }
}
Allowed codes are invalid-request, unauthorized, not-found, owner-mismatch, generation-mismatch, runtime-changed, unsupported, conflict, cancelled, containment-required, launch-failed, takeover-blocked, and internal-error. Queue rejection uses stable operation-specific error codes in the command result; conflict also carries the authoritative queue revision/snapshot. It never contains policy JSON, paths, environment, argv, stderr, native handles, or credentials. The command deduplication cache stores the last 2,048 command results for 15 minutes per runtime. Reusing an id with a different payload is conflict; exact reuse returns the original result without mutation.
AgentSessionProtocolCodec has only internal static JsonElement SerializeOpen(AgentSessionOpenRequest), AgentSessionOpenRequest DeserializeOpen(JsonElement), JsonElement SerializeCommand(AgentSessionCommand), AgentSessionCommand DeserializeCommand(JsonElement), JsonElement SerializeFrame(AgentSessionServerFrame), and AgentSessionServerFrame DeserializeFrame(JsonElement). Serialization is deterministic; each deserialize clones retained JsonElement values and rejects unknown top-level members before any authorization or mutation.
Tests
RemoteAgentChatTests (Phantom.Workspaces.Llm.Core.Tests)
AgentChatModal_InvalidIdentityTitleOrBody_RejectsInitialization.
MultipleChoiceModalContent_Options_AreClonedOnInitialization.
FreeformModalContent_ValidSettings_RoundTrips.
MultipleChoiceModalContent_DuplicateOrEmptyOptions_RejectsInitialization.
ApprovalModalContent_BlankLabels_RejectsInitialization.
AttachAsync_ValidSnapshot_PublishesInitializedProxy.
RemoteAgentChatAttachOptions_RequiredInitProperties_AreMarkedRequired.
AttachAsync_CancelledBeforeSnapshot_DisposesClientAndPublishesNothing.
AttachAsync_InvalidSnapshot_ThrowsProtocolException.
Reconnect_UnexpectedLoss_RetriesWithinGraceAndKeepsProxyEpoch.
ProxyGetters_AfterOrderedFrames_ReturnMirroredState.
ProxyEvents_OrderedFrame_AreRaisedAfterStateMutation.
UsageChanged_OrderedFrame_AtomicallyReplacesUsage.
InformationChanged_OrderedFrame_AtomicallyReplacesInformation.
InputQueues_OrderedDelta_MatchesLocalReadModel.
InputQueues_RejectedCommand_DoesNotMutateProjection.
InputQueues_ConflictResult_RefreshesFromAuthoritativeSnapshot.
InputQueues_CommandPending_DoesNotMutateProjection.
SetToolEnabledAsync_RemoteTool_SerializesCommandAndAppliesAcknowledgedEvent.
RespondToModalAsync_CurrentModal_SerializesResponseCommand.
EnqueueSystemNote_RemoteProxy_AddsLocalDisplayOnlyNote.
EnqueueHelpNote_RemoteProxy_AddsLocalDisplayOnlyNote.
EnqueueTransientDiagnostic_RemoteProxy_AddsLocalNonPersistedDiagnostic.
Interrupt_ConnectedProxy_SerializesInterrupt.
DetachAsync_RepeatedCall_SendsAtMostOneDetach.
DetachAsync_LastViewer_DefaultPolicy_TerminatesRuntime.
DetachAsync_LastViewer_BackgroundEnabled_PreservesRuntime.
TerminateAsync_CurrentEpoch_WaitsForTerminalFrame.
TerminateAsync_StaleEpoch_ThrowsRuntimeChanged.
DisposeAsync_ConnectedProxy_ReleasesViewerWithoutTerminateCommand.
ProxyCommand_AfterDispose_ThrowsObjectDisposedException.
GetService_TransportOrPolicyType_ReturnsNull.
RemoteAgentSessionClientTests (Phantom.Workspaces.Llm.Core.Tests)
Constructor_NullTransport_ThrowsArgumentNullException.
Constructor_ProcessScopedTransport_DoesNotDisposeBorrowedTransport.
ClientRequestTypes_RequiredInitProperties_AreMarkedRequired.
ClientRequestTypes_NamedInitializers_PreserveStatusAndCommandPayloads.
GetStatusAsync_AuthorizedRunningOrStopped_ReturnsAuthoritativeStatusOnly.
GetStatusAsync_UnauthorizedOrMissing_ReturnsUnavailableWithoutMetadata.
ConnectAsync_FirstCall_OpensAttachAgentSessionChannel.
ConnectAsync_SecondCall_ThrowsInvalidOperationException.
ConnectAsync_CancelledBeforeOpen_LeavesClientDisconnected.
ConnectAsync_SnapshotThenDelta_UpdatesCursorAndRaisesFramesInOrder.
ConnectAsync_SequenceGap_ClosesWithProtocolException.
ConnectAsync_UnknownDiscriminator_ClosesWithProtocolException.
ReconnectAsync_UnexpectedLoss_ForcesAttachWithTokenAndLastAppliedCursor.
ReconnectAsync_ConnectedDetachedTerminalOrExpired_ThrowsInvalidOperationException.
ReconnectAsync_CancelledAttempt_AllowsRetryBeforeDeadline.
CreateQueueAsync_Connected_SerializesCommandAndAwaitsResult.
DeleteQueueAsync_Connected_SerializesCommandAndAwaitsResult.
EnqueueAsync_Connected_SerializesMessagesTargetAndRevision.
EditAsync_Connected_SerializesStableItemIdAndMessages.
RemoveAsync_Connected_SerializesStableItemId.
MoveAsync_Connected_SerializesSourceTargetAndPlacementIds.
ConfigureAsync_Connected_SerializesConfigurationAndRevision.
InterruptAsync_Connected_SerializesInterruptAndAwaitsCorrelation.
TerminateAsync_Connected_SerializesReasonAndAwaitsTerminal.
OpenSubagentAsync_Authorized_ReturnsChildDescriptor.
RespondToModalAsync_Connected_SerializesModalIdAndResponse.
SetToolEnabledAsync_Connected_AwaitsAuthoritativeToolsEvent.
SetContinueInBackgroundAsync_Connected_AwaitsPersistedAuthoritativeEvent.
SetContinueInBackgroundAsync_Rejected_LeavesProjectionUnchanged.
CommandMethod_EmptyCommandId_ThrowsArgumentException.
CommandMethod_CancelledAfterWrite_DoesNotRetractCommand.
CommandMethod_NotConnected_ThrowsInvalidOperationException.
FrameReceived_ValidFrame_CursorAdvancesBeforeSubscriberRuns.
DetachAsync_RepeatedCall_IsIdempotent.
DisposeAsync_ActivePump_ClosesChannelWithoutTerminateCommandAndReleasesViewer.
LastAppliedCursor_NoFrames_IsNull.
RemoteAgentSessionException_WireError_ExposesOnlySafeFields.
AgentSessionProtocolCodecTests (Phantom.Workspaces.Llm.Core.Tests, internal contract)
RuntimeEpoch_EmptyValue_RejectsInitialization.
ReplayCursor_NegativeSequence_RejectsInitialization.
AgentSessionOpenRequest_InvalidVersionOrGeneration_RejectsInitialization.
TransportPeerIdentity_BlankAuthenticatedIdentity_RejectsInitialization.
ProtocolDtos_RequiredInitProperties_AreMarkedRequired.
ProtocolDtos_OptionalProperties_UseDocumentedDefaults.
ProtocolDtos_NamedInitializers_PreserveFixedDiscriminators.
AgentSessionServerFrame_Type_IsCodecAssignedFromEventDiscriminator.
RemoteSubagentDescriptor_ValidValues_RoundTrips.
Serialize_AllOpenIntents_UsesVersionOneDiscriminators.
RoundTrip_AgentSessionTakeoverRequest_PreservesProfilesGenerationAndCorrelation.
RoundTrip_AllCommandDiscriminators_PreservesIdsEpochAndPayload.
RoundTrip_AllServerEventDiscriminators_PreservesSequenceAndCorrelation.
RoundTrip_SessionSnapshot_PreservesUsageInformationAndFullQueues.
RoundTrip_SessionSnapshot_PreservesBackgroundPreferenceViewerCountAndFullDefinition.
RoundTrip_SetContinueInBackgroundCommand_PreservesCommandAndCorrelationIds.
RoundTrip_SessionRetentionChanged_PreservesPreferenceAndViewerCount.
RoundTrip_QueueChanged_PreservesStableIdsRevisionsAndOrdering.
RoundTrip_AgentInformation_ClonesDefinitionJsonElements.
Deserialize_UnknownMember_RejectsFrame.
Deserialize_CompiledPolicyMember_RejectsFrame.
Deserialize_EmptyRequiredId_RejectsFrame.
ServerFrames_ConcurrentPublish_AreStrictlyOrdered.
Dependencies
Part of #1483
Summary
Define the strict version-1
attach-agent-sessionprotocol, property-based protocol/client proxy DTOs,RemoteAgentSessionClient, andRemoteAgentChatwith authoritative ordered state, completeAgentDefinitionfor every authorized viewer, reconnect/replay, queue/tool/modal/subagent commands, and sanitized wire errors.Scope
Implement only commit 3 of the approved design. Preserve the contracts below exactly; local and remote behavior must remain compatible. This detail owns protocol/client/proxy signatures and DTOs only; it does not add server runtime, attach authorization, host dispatch, viewer lifecycle, or MXC integration.
Implementation commit mapping
0c6578641befad31d5a38003baf6c0f0fe64748a.IMessageChannel; no server runtime or MXC dependency.Files
Files:
Phantom.Workspaces.Llm.Core/Remote/AgentSessionProtocol.cs, property-based protocol DTOs and codec,Phantom.Workspaces.Llm.Core/Remote/RemoteAgentSessionClient.cs,Phantom.Workspaces.Llm.Core/Remote/RemoteAgentChat.cs,Phantom.Workspaces.Llm.Core/Remote/RemoteAgentSessionException.cs, andRemoteAgentChatAttachOptions.Tests: required-init metadata, optional defaults, named-initializer construction, exact discriminator/serialization round trips, codec, all client public methods, atomic usage/information, proxy queue parity, full authorized
AgentDefinition, no metadata on denial, background command/event/snapshot, revisions/conflicts/deduplication, state/events/commands, cancellation, and disposal. The protocol contains enqueue/edit/remove/move/configure/create/delete queue commands and no steering command.Preserved background and design decisions
attach-agent-sessionremains a dedicated bidirectional session protocol, separate from thechat-clientrequest/response protocol.ChatClientOverTransportstays the lower-level remoteIChatClientmechanism and is not the remote-session proxy.*Request/*Optionsrecords withrequired initmembers and documented optional defaults. Constructors remain only for compact value-object invariants or a small cohesive set of service dependencies.AgentSessionServerFrame.Typeis codec-assigned from the concrete event discriminator and is not caller-selectable.AgentDefinitioninAgentInformation; unauthorized peers receive no session metadata, no definition bytes, and no existence signal.IAgentChatand nosteerprotocol verb.InterruptAsync(Guid commandId, CancellationToken ct = default)remains direct; do not add a one-propertyInterruptAgentSessionRequest.JsonElementremains limited to already-versioned domain payloads; strict top-level request/frame members still reject unknown properties.Detailed design
UsageandAgentInformation- Shared protocol snapshot stateThe protocol snapshot and replacement state use the exact common property-based forms below. These names, fields, types, and semantics are authoritative for this detail because the proxy and wire contracts consume them directly.
Usagepermits null for a metric the provider did not report; counts must otherwise be nonnegative and cost remains a nonnegativedoublemeasured in USD.AgentInformationrequires non-null, nonblank values for its first five strings and a non-null completeAgentDefinition;CurrentModelIdis null or nonblank. 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 atomic assignment, once per applied session sequence; observers never see fields from different versions.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 withPhantomAgentSchema.AgentDefinitionFromJson(string). Every authorized attached GUI receives the same complete definition inAgentInformation; 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 redactedAgentInformation. No field-level redaction is applied after authorization.RemoteAgentChat- NewPhantom.Workspaces.Llm.Remote;Phantom.Workspaces.Llm.Core/Remote/RemoteAgentChat.cs.public sealed class : IAgentChat.AgentChatHistoryCollection, immutable queue/running item projections, tools, subagent proxies, slash-command facade,Usage, andAgentInformation. It owns oneRemoteAgentSessionClient, applies frames on its supplied foreground scheduler, and never owns the remote runtime.AttachAsyncvalidates arguments, waits for the first authoritative snapshot, then publishes the object; cancellation before snapshot disposes the channel and publishes nothing.DetachAsyncsends best-effort explicit detach once and disposes proxy state; owner-side release stops the runtime when this is the final viewer and background continuation is disabled.TerminateAsyncrequires the current epoch, crosses transport, and completes only after a terminal event or safe command error. Commands after detach/disposal throwObjectDisposedException; stale epoch maps toRemoteAgentSessionException("runtime-changed"). Local-only slash commands are not advertised by the proxy. Event production follows apply-state-then-notify ordering.RemoteAgentSessionClient- NewPhantom.Workspaces.Llm.Remote;Phantom.Workspaces.Llm.Core/Remote/RemoteAgentSessionClient.cs.public sealed class : IAsyncDisposable.ITransport-openedIMessageChannel, one receive pump, pending command completions, and the last accepted cursor. It does not mutate UI collections.GetStatusAsyncrequiresOpenIntent = Status, borrows the transport, authorizes before lookup, returnsRunningorNotRunningto an authorized peer, maps denial/not-found/unsafe failure toUnavailable, closes its one-shot channel, and returns no snapshot or session metadata.ConnectAsyncis the initial-open operation and may succeed once; it stores the validated request and serializes it intoITransport.ConnectToMessageChannelAsync, starts one reader, and completes after snapshot/replay validation. A second call throwsInvalidOperationException. After unexpected channel loss,ReconnectAsyncmay be called while the five-second grace remains; it single-flights reconnect, reuses the original peer-bound attachment token, forcesOpenIntent = Attachregardless of the initial request's intent, suppliesLastAppliedCursor, replaces the channel and pump, and completes after replay/snapshot validation. It can therefore reclaim only the reserved existing attachment and can never start a replacement runtime after grace expiry. Calls while connected, after explicit detach/terminal/disposal, or after grace expiry throwInvalidOperationException. Cancellation stops only that attempt and leaves another attempt possible before the deadline.RemoteAgentChatautomatically callsReconnectAsyncafter unexpected loss with delays of 250 ms, 500 ms, then one second until the grace deadline; an accepted terminal/not-found result ends retry. Each typed command method validates its payload and nonempty caller-supplied command id, requires a connected nonterminal epoch, serializes the corresponding strict DTO, and waits for its correlated acknowledgement/error. Cancellation cancels only the caller's wait after a successful write; command ids make retry safe. Queue methods are the transport implementation behind the proxyIAgentInputQueuesand have the same validation/result semantics as that interface. An applied queue task completes only after the proxy has applied the authoritative result/delta revision; no caller observes completion against a stale projection.OpenSubagentAsyncreturns only the authorized child session/open descriptor.SetToolEnabledAsynccompletes only after the owner-authoritativetools-changedevent has been applied; rejection does not alter the proxy tool snapshot.SetContinueInBackgroundAsynccompletes only after the owner has persisted the preference and the matching ordered retention event has been applied.FrameReceivedis emitted synchronously in validated epoch and sequence order; gaps, regressions, unknown discriminators, or mismatched correlations close the channel withRemoteAgentProtocolException.DetachAsyncis idempotent and best effort.DisposeAsynccancels the pump and channel without sendingterminate-session; releasing the attachment can still cause default last-viewer stop. The constructor rejects a null transport; the client borrows the process-scoped transport and owns only its opened channel.Protocol records and strict codec - New
Phantom.Workspaces.Llm.Remote;Phantom.Workspaces.Llm.Core/Remote/AgentSessionProtocol.cs.RuntimeEpoch,ReplayCursor,AgentSessionOpenRequest,AgentSessionServerFrame,AgentSessionOpenIntent, andRemoteSubagentDescriptorare public immutable records because the client/core boundary consumes them. Commands, events, andAgentSessionProtocolCodecare internal.AgentSessionServerFrame.Typeis a required wire member but deliberately not a public initializer: the internal codec derives it from the concreteAgentSessionServerEvent.Typeon serialization and sets it only after recognizing a supported discriminator on deserialization. It is therefore fixed like each concrete command/event discriminator rather than caller-selectable. Encoding rejects any frame whose internally assigned type is null, blank, or not the recognized event discriminator.The strict version-1 command records are:
The strict version-1 event records all inherit
AgentSessionServerEvent; the frame supplies version/correlation/epoch/sequence exactly once:JsonElementis used only for already-versioned domain payloads whose polymorphism is owned byPhantomAgentSchemaorMicrosoft.Extensions.AI.AIJsonUtilities.DefaultOptions; each element is cloned before the read buffer advances. It is never used to bypass strict top-level member checking. The protocol codec derivesAgentSessionServerFrame.Payloadfrom the concrete event's named properties when encoding and selects the concrete event type from the recognized rawtypediscriminator before decoding that payload; generic serializer polymorphism is not used.RemoteAgentSessionExceptionis a new public sealed exception withstring Code { get; },string Operation { get; },bool IsRetryable { get; }, andGuid CorrelationId { get; }. Its internal factory accepts onlyRemoteAgentOperationError. It contains the safe message only; local exceptions are retained solely in host logs.Version 1 uses kebab-case JSON and rejects unknown members. Queue message arrays use
Microsoft.Extensions.AI.AIJsonUtilities.DefaultOptions; agent definitions useAgentDefinition.ToJson()andPhantomAgentSchema.AgentDefinitionFromJson(string)after the authorization gate described above. Required fields are non-null and ids are nonempty. Initializer/codec validation rejects an empty epoch, negative cursor sequence, unsupported protocol version, negative generation, and duplicate/unknown capabilities.Sequenceis positive and increases for every server frame in an epoch. The snapshot sequence is its high-water mark; replay starts at cursor+1.CommandIdis the stable idempotency key and is reused across retries;CorrelationIdidentifies one wire attempt. Acknowledgement/error frames echo that attempt's correlation id, while unsolicited events use a fresh correlation id.Open descriptor:
type:"attach-agent-session",protocol-version,agent-session-id,expected-owning-profile-entity-id,expected-ownership-generation,open-intent(status,start,attach,start-or-attach,resume), a cryptographically random 128-bitattachment-token, optionalreplay-cursor:{runtime-epoch,sequence}, andcapabilities. The token is scoped to the authenticated peer and runtime epoch and is retained only for the five-second unexpected-loss grace.agent-definitionis not a negotiable capability. Commands are:create-queuedelete-queueenqueue-inputedit-queue-itemremove-queue-itemmove-queue-itemconfigure-queueinterruptterminate-sessionreasonopen-subagentagent-idmodal-responsemodal-id,responseset-tool-enabledtool-id, booleanenabledset-continue-in-backgroundcontinue-in-backgrounddetachServer frames are:
session-statusrunning,not-running, orunavailable; terminal one-shot response with no session metadatasession-snapshotAgentInformationwith full definition,Usage, full queue snapshot, history, running/streaming state, busy, tools, subagents, modals,continue-in-background, viewer count, terminal statehistory-appendedusage-changedUsageagent-information-changedAgentInformationqueue-changedstreaming-started/streaming-updated/streaming-completedbusy-changedtools-snapshot/tools-changedsubagents-snapshot/subagents-changedmodal-raised/modal-updated/modal-dismissedsession-retention-changedcontinue-in-backgroundand nonnegative logical viewer countcommand-completedoperation-errorRemoteAgentOperationErrorsession-terminalRemoteAgentOperationErroris an internal strict property record:Allowed codes are
invalid-request,unauthorized,not-found,owner-mismatch,generation-mismatch,runtime-changed,unsupported,conflict,cancelled,containment-required,launch-failed,takeover-blocked, andinternal-error. Queue rejection uses stable operation-specific error codes in the command result;conflictalso carries the authoritative queue revision/snapshot. It never contains policy JSON, paths, environment, argv, stderr, native handles, or credentials. The command deduplication cache stores the last 2,048 command results for 15 minutes per runtime. Reusing an id with a different payload isconflict; exact reuse returns the original result without mutation.AgentSessionProtocolCodechas only internal staticJsonElement SerializeOpen(AgentSessionOpenRequest),AgentSessionOpenRequest DeserializeOpen(JsonElement),JsonElement SerializeCommand(AgentSessionCommand),AgentSessionCommand DeserializeCommand(JsonElement),JsonElement SerializeFrame(AgentSessionServerFrame), andAgentSessionServerFrame DeserializeFrame(JsonElement). Serialization is deterministic; each deserialize clones retainedJsonElementvalues and rejects unknown top-level members before any authorization or mutation.Tests
RemoteAgentChatTests(Phantom.Workspaces.Llm.Core.Tests)AgentChatModal_InvalidIdentityTitleOrBody_RejectsInitialization.MultipleChoiceModalContent_Options_AreClonedOnInitialization.FreeformModalContent_ValidSettings_RoundTrips.MultipleChoiceModalContent_DuplicateOrEmptyOptions_RejectsInitialization.ApprovalModalContent_BlankLabels_RejectsInitialization.AttachAsync_ValidSnapshot_PublishesInitializedProxy.RemoteAgentChatAttachOptions_RequiredInitProperties_AreMarkedRequired.AttachAsync_CancelledBeforeSnapshot_DisposesClientAndPublishesNothing.AttachAsync_InvalidSnapshot_ThrowsProtocolException.Reconnect_UnexpectedLoss_RetriesWithinGraceAndKeepsProxyEpoch.ProxyGetters_AfterOrderedFrames_ReturnMirroredState.ProxyEvents_OrderedFrame_AreRaisedAfterStateMutation.UsageChanged_OrderedFrame_AtomicallyReplacesUsage.InformationChanged_OrderedFrame_AtomicallyReplacesInformation.InputQueues_OrderedDelta_MatchesLocalReadModel.InputQueues_RejectedCommand_DoesNotMutateProjection.InputQueues_ConflictResult_RefreshesFromAuthoritativeSnapshot.InputQueues_CommandPending_DoesNotMutateProjection.SetToolEnabledAsync_RemoteTool_SerializesCommandAndAppliesAcknowledgedEvent.RespondToModalAsync_CurrentModal_SerializesResponseCommand.EnqueueSystemNote_RemoteProxy_AddsLocalDisplayOnlyNote.EnqueueHelpNote_RemoteProxy_AddsLocalDisplayOnlyNote.EnqueueTransientDiagnostic_RemoteProxy_AddsLocalNonPersistedDiagnostic.Interrupt_ConnectedProxy_SerializesInterrupt.DetachAsync_RepeatedCall_SendsAtMostOneDetach.DetachAsync_LastViewer_DefaultPolicy_TerminatesRuntime.DetachAsync_LastViewer_BackgroundEnabled_PreservesRuntime.TerminateAsync_CurrentEpoch_WaitsForTerminalFrame.TerminateAsync_StaleEpoch_ThrowsRuntimeChanged.DisposeAsync_ConnectedProxy_ReleasesViewerWithoutTerminateCommand.ProxyCommand_AfterDispose_ThrowsObjectDisposedException.GetService_TransportOrPolicyType_ReturnsNull.RemoteAgentSessionClientTests(Phantom.Workspaces.Llm.Core.Tests)Constructor_NullTransport_ThrowsArgumentNullException.Constructor_ProcessScopedTransport_DoesNotDisposeBorrowedTransport.ClientRequestTypes_RequiredInitProperties_AreMarkedRequired.ClientRequestTypes_NamedInitializers_PreserveStatusAndCommandPayloads.GetStatusAsync_AuthorizedRunningOrStopped_ReturnsAuthoritativeStatusOnly.GetStatusAsync_UnauthorizedOrMissing_ReturnsUnavailableWithoutMetadata.ConnectAsync_FirstCall_OpensAttachAgentSessionChannel.ConnectAsync_SecondCall_ThrowsInvalidOperationException.ConnectAsync_CancelledBeforeOpen_LeavesClientDisconnected.ConnectAsync_SnapshotThenDelta_UpdatesCursorAndRaisesFramesInOrder.ConnectAsync_SequenceGap_ClosesWithProtocolException.ConnectAsync_UnknownDiscriminator_ClosesWithProtocolException.ReconnectAsync_UnexpectedLoss_ForcesAttachWithTokenAndLastAppliedCursor.ReconnectAsync_ConnectedDetachedTerminalOrExpired_ThrowsInvalidOperationException.ReconnectAsync_CancelledAttempt_AllowsRetryBeforeDeadline.CreateQueueAsync_Connected_SerializesCommandAndAwaitsResult.DeleteQueueAsync_Connected_SerializesCommandAndAwaitsResult.EnqueueAsync_Connected_SerializesMessagesTargetAndRevision.EditAsync_Connected_SerializesStableItemIdAndMessages.RemoveAsync_Connected_SerializesStableItemId.MoveAsync_Connected_SerializesSourceTargetAndPlacementIds.ConfigureAsync_Connected_SerializesConfigurationAndRevision.InterruptAsync_Connected_SerializesInterruptAndAwaitsCorrelation.TerminateAsync_Connected_SerializesReasonAndAwaitsTerminal.OpenSubagentAsync_Authorized_ReturnsChildDescriptor.RespondToModalAsync_Connected_SerializesModalIdAndResponse.SetToolEnabledAsync_Connected_AwaitsAuthoritativeToolsEvent.SetContinueInBackgroundAsync_Connected_AwaitsPersistedAuthoritativeEvent.SetContinueInBackgroundAsync_Rejected_LeavesProjectionUnchanged.CommandMethod_EmptyCommandId_ThrowsArgumentException.CommandMethod_CancelledAfterWrite_DoesNotRetractCommand.CommandMethod_NotConnected_ThrowsInvalidOperationException.FrameReceived_ValidFrame_CursorAdvancesBeforeSubscriberRuns.DetachAsync_RepeatedCall_IsIdempotent.DisposeAsync_ActivePump_ClosesChannelWithoutTerminateCommandAndReleasesViewer.LastAppliedCursor_NoFrames_IsNull.RemoteAgentSessionException_WireError_ExposesOnlySafeFields.AgentSessionProtocolCodecTests(Phantom.Workspaces.Llm.Core.Tests, internal contract)RuntimeEpoch_EmptyValue_RejectsInitialization.ReplayCursor_NegativeSequence_RejectsInitialization.AgentSessionOpenRequest_InvalidVersionOrGeneration_RejectsInitialization.TransportPeerIdentity_BlankAuthenticatedIdentity_RejectsInitialization.ProtocolDtos_RequiredInitProperties_AreMarkedRequired.ProtocolDtos_OptionalProperties_UseDocumentedDefaults.ProtocolDtos_NamedInitializers_PreserveFixedDiscriminators.AgentSessionServerFrame_Type_IsCodecAssignedFromEventDiscriminator.RemoteSubagentDescriptor_ValidValues_RoundTrips.Serialize_AllOpenIntents_UsesVersionOneDiscriminators.RoundTrip_AgentSessionTakeoverRequest_PreservesProfilesGenerationAndCorrelation.RoundTrip_AllCommandDiscriminators_PreservesIdsEpochAndPayload.RoundTrip_AllServerEventDiscriminators_PreservesSequenceAndCorrelation.RoundTrip_SessionSnapshot_PreservesUsageInformationAndFullQueues.RoundTrip_SessionSnapshot_PreservesBackgroundPreferenceViewerCountAndFullDefinition.RoundTrip_SetContinueInBackgroundCommand_PreservesCommandAndCorrelationIds.RoundTrip_SessionRetentionChanged_PreservesPreferenceAndViewerCount.RoundTrip_QueueChanged_PreservesStableIdsRevisionsAndOrdering.RoundTrip_AgentInformation_ClonesDefinitionJsonElements.Deserialize_UnknownMember_RejectsFrame.Deserialize_CompiledPolicyMember_RejectsFrame.Deserialize_EmptyRequiredId_RejectsFrame.ServerFrames_ConcurrentPublish_AreStrictlyOrdered.Dependencies