mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor(server): inline client capability parsing
With capability negotiation in place, the consumer-side tolerance pattern is unnecessary. Drop the dead readDeclaredClientCapabilities helper, inline the parsing in ClientCapabilities.fromHello, delete a tautological assertion in the relay reconnect test, and document the compatibility rules in architecture.md.
This commit is contained in:
@@ -137,6 +137,21 @@ Terminal I/O and agent streaming share the same connection via `BinaryMuxFrame`:
|
||||
- Channel 1: terminal data
|
||||
- 1-byte channel ID + 1-byte flags + variable payload
|
||||
|
||||
### Compatibility rules
|
||||
|
||||
- WebSocket schemas are append-only. Add fields, do not remove fields, and never make optional fields required.
|
||||
- New wire enum values must be gated at serialization with `session.supports(CLIENT_CAPS.someCapability)`.
|
||||
- `Session` stores client capabilities from the `hello` handshake and rehydrates them on reconnect, so the wire boundary can ask one question: `session.supports(...)`.
|
||||
|
||||
Example: adding a new enum value
|
||||
|
||||
```ts
|
||||
// 1. Add CLIENT_CAPS.newThing = "new_thing"
|
||||
// 2. Let new clients advertise it in WS hello
|
||||
// 3. Keep the shared producer schema strict
|
||||
// 4. Gate the new emitted value: session.supports(CLIENT_CAPS.newThing) ? "new_value" : "old_value"
|
||||
```
|
||||
|
||||
## Agent lifecycle
|
||||
|
||||
```
|
||||
|
||||
@@ -8,11 +8,7 @@ import { basename, resolve, sep } from "path";
|
||||
import { homedir } from "node:os";
|
||||
import { z } from "zod";
|
||||
import type { ToolSet } from "ai";
|
||||
import {
|
||||
CLIENT_CAPS,
|
||||
readDeclaredClientCapabilities,
|
||||
type ClientCapability,
|
||||
} from "../shared/client-capabilities.js";
|
||||
import { CLIENT_CAPS, type ClientCapability } from "../shared/client-capabilities.js";
|
||||
import {
|
||||
isLegacyEditorTargetId,
|
||||
serializeAgentStreamEvent,
|
||||
@@ -731,20 +727,18 @@ function convertPCMToWavBuffer(
|
||||
return wavBuffer;
|
||||
}
|
||||
|
||||
class ClientCapabilities {
|
||||
private readonly supported: ReadonlySet<ClientCapability>;
|
||||
|
||||
constructor(capabilities: Iterable<ClientCapability>) {
|
||||
this.supported = new Set(capabilities);
|
||||
}
|
||||
|
||||
static fromHello(capabilities: Record<string, unknown> | null | undefined): ClientCapabilities {
|
||||
return new ClientCapabilities(readDeclaredClientCapabilities(capabilities));
|
||||
}
|
||||
|
||||
supports(capability: ClientCapability): boolean {
|
||||
return this.supported.has(capability);
|
||||
function parseClientCapabilities(
|
||||
capabilities: Record<string, unknown> | null | undefined,
|
||||
): ReadonlySet<ClientCapability> {
|
||||
if (!capabilities) {
|
||||
return new Set();
|
||||
}
|
||||
const known = new Set<ClientCapability>(Object.values(CLIENT_CAPS));
|
||||
return new Set(
|
||||
Object.entries(capabilities).flatMap(([key, value]) =>
|
||||
value === true && known.has(key as ClientCapability) ? [key as ClientCapability] : [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -755,7 +749,7 @@ class ClientCapabilities {
|
||||
export class Session {
|
||||
private readonly clientId: string;
|
||||
private appVersion: string | null;
|
||||
private clientCapabilities: ClientCapabilities;
|
||||
private clientCapabilities: ReadonlySet<ClientCapability>;
|
||||
private readonly sessionId: string;
|
||||
private readonly onMessage: (msg: SessionOutboundMessage) => void;
|
||||
private readonly onBinaryMessage: ((frame: Uint8Array) => void) | null;
|
||||
@@ -912,7 +906,7 @@ export class Session {
|
||||
} = options;
|
||||
this.clientId = clientId;
|
||||
this.appVersion = appVersion ?? null;
|
||||
this.clientCapabilities = ClientCapabilities.fromHello(clientCapabilities);
|
||||
this.clientCapabilities = parseClientCapabilities(clientCapabilities);
|
||||
this.sessionId = uuidv4();
|
||||
this.onMessage = onMessage;
|
||||
this.onBinaryMessage = onBinaryMessage ?? null;
|
||||
@@ -985,11 +979,11 @@ export class Session {
|
||||
}
|
||||
|
||||
updateClientCapabilities(capabilities: Record<string, unknown> | null): void {
|
||||
this.clientCapabilities = ClientCapabilities.fromHello(capabilities);
|
||||
this.clientCapabilities = parseClientCapabilities(capabilities);
|
||||
}
|
||||
|
||||
supports(capability: ClientCapability): boolean {
|
||||
return this.clientCapabilities.supports(capability);
|
||||
return this.clientCapabilities.has(capability);
|
||||
}
|
||||
|
||||
async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise<void> {
|
||||
|
||||
@@ -429,7 +429,6 @@ describe("relay external socket reconnect behavior", () => {
|
||||
expect(session.args.clientCapabilities).toEqual({
|
||||
[CLIENT_CAPS.reasoningMergeEnum]: true,
|
||||
});
|
||||
expect(session.supports(CLIENT_CAPS.reasoningMergeEnum)).toBe(true);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
@@ -3,21 +3,3 @@ export const CLIENT_CAPS = {
|
||||
} as const;
|
||||
|
||||
export type ClientCapability = (typeof CLIENT_CAPS)[keyof typeof CLIENT_CAPS];
|
||||
|
||||
const CLIENT_CAPABILITY_SET = new Set<string>(Object.values(CLIENT_CAPS));
|
||||
|
||||
export function isClientCapability(value: string): value is ClientCapability {
|
||||
return CLIENT_CAPABILITY_SET.has(value);
|
||||
}
|
||||
|
||||
export function readDeclaredClientCapabilities(
|
||||
capabilities: Record<string, unknown> | null | undefined,
|
||||
): ClientCapability[] {
|
||||
if (!capabilities) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(capabilities).flatMap(([key, value]) =>
|
||||
value === true && isClientCapability(key) ? [key] : [],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user