@ggui-ai/protocol-reference-server 0.1.0-rc.1 → 0.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ kit grounds the claim empirically rather than by assertion.
12
12
  ## Scope
13
13
 
14
14
  - WebSocket transport matching the ggui live-channel wire.
15
- - In-memory session store (no persistence).
15
+ - In-memory render store (no persistence).
16
16
  - `schemaVersion` handshake with `UPGRADE_REQUIRED` on mismatch.
17
17
  - A 4-handler wired-action registry:
18
18
  - `echo` — returns `{received: args}`.
@@ -1,34 +1,41 @@
1
- import type { Session } from './session.js';
1
+ import type { Render } from './render.js';
2
2
  import type { ToolRegistry } from './tool-registry.js';
3
3
  /**
4
4
  * One inbound action frame shape. Matches the fixtures' authored
5
5
  * `inputEnvelope` for `wired-action-*` cases — the runner sends the
6
- * envelope verbatim.
6
+ * envelope verbatim. The wire spelling of the render-identity field
7
+ * is normalized to `renderId` by {@link isActionFrame} (it accepts
8
+ * either the canonical `renderId` or the kit's still-legacy
9
+ * `sessionId`).
7
10
  */
8
11
  interface IncomingActionFrame {
9
12
  readonly type: 'action';
10
13
  readonly channel?: number;
11
- readonly sessionId: string;
14
+ readonly renderId: string;
12
15
  readonly action: {
13
16
  readonly name: string;
14
17
  readonly data?: unknown;
15
18
  };
16
19
  }
17
20
  /**
18
- * Type-guard the action frame shape before dispatching. Anything
19
- * malformed is dropped with a `no-op` warning — the matcher for
21
+ * Parse + validate an inbound action frame. Returns the normalized
22
+ * shape on success, `undefined` on any malformed input (matcher for
20
23
  * `no-op` fixtures expects silence, so loud rejection would break
21
- * them.
24
+ * them).
25
+ *
26
+ * Accepts both `renderId` (canonical) and `sessionId` (the kit's
27
+ * legacy spelling that has not yet been migrated); the returned
28
+ * shape always surfaces the value as `renderId`.
22
29
  */
23
- export declare function isActionFrame(frame: unknown): frame is IncomingActionFrame;
30
+ export declare function parseActionFrame(frame: unknown): IncomingActionFrame | undefined;
24
31
  export interface DispatchContext {
25
- readonly session: Session;
32
+ readonly render: Render;
26
33
  readonly tools: ToolRegistry;
27
34
  }
28
35
  /**
29
36
  * Dispatch one action frame. Runs asynchronously; contract-error
30
- * emissions are broadcast to every subscriber on the session via
31
- * `session.subscribers`.
37
+ * emissions are broadcast to every subscriber on the render via
38
+ * `render.subscribers`.
32
39
  *
33
40
  * Returns when the dispatch's observable outcome has been emitted
34
41
  * (either a happy-path observability frame or a contract-error).
@@ -1 +1 @@
1
- {"version":3,"file":"action-router.d.ts","sourceRoot":"","sources":["../src/action-router.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAMvD;;;;GAIG;AACH,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;KACzB,CAAC;CACH;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,mBAAmB,CAS1E;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAsIf"}
1
+ {"version":3,"file":"action-router.d.ts","sourceRoot":"","sources":["../src/action-router.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAMvD;;;;;;;GAOG;AACH,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;KACzB,CAAC;CACH;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,GAAG,SAAS,CAwBhF;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAsIf"}
@@ -22,35 +22,60 @@
22
22
  * `{toolName, actionName}` so an iframe-host kit can match the same
23
23
  * tool invocation signal from either side. Pure WS kit currently
24
24
  * treats this as SKIP.
25
+ *
26
+ * Wire-field note: the inbound action frame names the render
27
+ * identity `sessionId` on the wire (consumer field name from
28
+ * `@ggui-ai/protocol-conformance`, which has not yet renamed to
29
+ * `renderId`). The type-guard accepts both spellings and surfaces
30
+ * the value as `renderId` for the rest of the dispatcher.
25
31
  */
26
32
  import { makeContractErrorPayload } from '@ggui-ai/protocol';
27
33
  /** Timeout the `timeout` handler exceeds; the kit's matcher
28
34
  * recognizes TOOL_TIMEOUT within this window. */
29
35
  const TIMEOUT_MS = 500;
30
36
  /**
31
- * Type-guard the action frame shape before dispatching. Anything
32
- * malformed is dropped with a `no-op` warning — the matcher for
37
+ * Parse + validate an inbound action frame. Returns the normalized
38
+ * shape on success, `undefined` on any malformed input (matcher for
33
39
  * `no-op` fixtures expects silence, so loud rejection would break
34
- * them.
40
+ * them).
41
+ *
42
+ * Accepts both `renderId` (canonical) and `sessionId` (the kit's
43
+ * legacy spelling that has not yet been migrated); the returned
44
+ * shape always surfaces the value as `renderId`.
35
45
  */
36
- export function isActionFrame(frame) {
46
+ export function parseActionFrame(frame) {
37
47
  if (frame === null || typeof frame !== 'object')
38
- return false;
48
+ return undefined;
39
49
  const f = frame;
40
50
  if (f['type'] !== 'action')
41
- return false;
42
- if (typeof f['sessionId'] !== 'string')
43
- return false;
51
+ return undefined;
52
+ const renderId = typeof f['renderId'] === 'string'
53
+ ? f['renderId']
54
+ : typeof f['sessionId'] === 'string'
55
+ ? f['sessionId']
56
+ : undefined;
57
+ if (renderId === undefined)
58
+ return undefined;
44
59
  const action = f['action'];
45
60
  if (action === null || typeof action !== 'object')
46
- return false;
61
+ return undefined;
47
62
  const a = action;
48
- return typeof a['name'] === 'string';
63
+ const name = a['name'];
64
+ if (typeof name !== 'string')
65
+ return undefined;
66
+ const data = 'data' in a ? a['data'] : undefined;
67
+ const channelValue = f['channel'];
68
+ return {
69
+ type: 'action',
70
+ ...(typeof channelValue === 'number' ? { channel: channelValue } : {}),
71
+ renderId,
72
+ action: { name, ...(data !== undefined ? { data } : {}) },
73
+ };
49
74
  }
50
75
  /**
51
76
  * Dispatch one action frame. Runs asynchronously; contract-error
52
- * emissions are broadcast to every subscriber on the session via
53
- * `session.subscribers`.
77
+ * emissions are broadcast to every subscriber on the render via
78
+ * `render.subscribers`.
54
79
  *
55
80
  * Returns when the dispatch's observable outcome has been emitted
56
81
  * (either a happy-path observability frame or a contract-error).
@@ -58,10 +83,10 @@ export function isActionFrame(frame) {
58
83
  * await ensures the kit's observation window captures the emission.
59
84
  */
60
85
  export async function dispatchAction(frame, context) {
61
- const { session, tools } = context;
86
+ const { render, tools } = context;
62
87
  const actionName = frame.action.name;
63
88
  // Action → tool resolution, in priority order:
64
- // 1. Explicit `register-actionspec` directive on this session.
89
+ // 1. Explicit `register-actionspec` directive on this render.
65
90
  // 2. Tool whose name equals the action name (1:1 convention).
66
91
  //
67
92
  // A real ggui server uses a blueprint's declared `actionSpec` to
@@ -71,13 +96,13 @@ export async function dispatchAction(frame, context) {
71
96
  // with `toolName = actionName` — the error payload names the
72
97
  // missing tool by the identifier the dispatcher was asked to
73
98
  // resolve, which is the action name itself.
74
- const actionSpec = session.actionSpecs.get(actionName);
99
+ const actionSpec = render.actionSpecs.get(actionName);
75
100
  let toolName = actionSpec?.tool;
76
101
  if (toolName === undefined && tools.has(actionName)) {
77
102
  toolName = actionName;
78
103
  }
79
104
  if (toolName === undefined) {
80
- emitContractError(session, {
105
+ emitContractError(render, {
81
106
  code: 'TOOL_NOT_FOUND',
82
107
  toolName: actionName,
83
108
  actionName,
@@ -87,7 +112,7 @@ export async function dispatchAction(frame, context) {
87
112
  }
88
113
  const tool = tools.get(toolName);
89
114
  if (tool === undefined) {
90
- emitContractError(session, {
115
+ emitContractError(render, {
91
116
  code: 'TOOL_NOT_FOUND',
92
117
  toolName,
93
118
  actionName,
@@ -98,7 +123,7 @@ export async function dispatchAction(frame, context) {
98
123
  // Malformed handler is defined to return the wrong shape — emit
99
124
  // SCHEMA_VIOLATION instead of passing the bad return through.
100
125
  if (tool.kind === 'malformed') {
101
- emitContractError(session, {
126
+ emitContractError(render, {
102
127
  code: 'SCHEMA_VIOLATION',
103
128
  toolName: tool.name,
104
129
  actionName,
@@ -131,7 +156,7 @@ export async function dispatchAction(frame, context) {
131
156
  }
132
157
  catch (err) {
133
158
  // Promise.race never rejects since we catch inside; defensive.
134
- emitContractError(session, {
159
+ emitContractError(render, {
135
160
  code: 'TOOL_THREW',
136
161
  toolName: tool.name,
137
162
  actionName,
@@ -141,7 +166,7 @@ export async function dispatchAction(frame, context) {
141
166
  return;
142
167
  }
143
168
  if (handlerOutcome.kind === 'timeout') {
144
- emitContractError(session, {
169
+ emitContractError(render, {
145
170
  code: 'TOOL_TIMEOUT',
146
171
  toolName: tool.name,
147
172
  actionName,
@@ -150,7 +175,7 @@ export async function dispatchAction(frame, context) {
150
175
  return;
151
176
  }
152
177
  if (handlerOutcome.kind === 'rejected') {
153
- emitContractError(session, {
178
+ emitContractError(render, {
154
179
  code: 'TOOL_THREW',
155
180
  toolName: tool.name,
156
181
  actionName,
@@ -164,7 +189,7 @@ export async function dispatchAction(frame, context) {
164
189
  // `unmatchable-on-ws` for `observability-event`, which the runner
165
190
  // maps to SKIP (not FAIL). That's the intended behavior per the
166
191
  // kit's design note at the top of `match-behavior.ts`.
167
- broadcast(session, {
192
+ broadcast(render, {
168
193
  type: 'stream',
169
194
  payload: {
170
195
  channel: '_ggui:wired-tool-invoked',
@@ -173,7 +198,7 @@ export async function dispatchAction(frame, context) {
173
198
  });
174
199
  // Refresh-stream fan-out (SPEC §2.3 StreamSpec refresh triggers).
175
200
  // After a successful wired-action dispatch, every streamSpec
176
- // declared on the session fires its refresh tool and emits a
201
+ // declared on the render fires its refresh tool and emits a
177
202
  // stream-update on the bound channel. This is the wire-level
178
203
  // proof of the refresh-after-action contract the kit's
179
204
  // `stream-refresh-success` fixture asserts.
@@ -183,7 +208,7 @@ export async function dispatchAction(frame, context) {
183
208
  // distinguish action-path failures from refresh-path failures
184
209
  // (and so `stream-schema-violation` has a path forward when its
185
210
  // fixture flips off the known-failures list).
186
- await dispatchRefreshStreams(session, tools);
211
+ await dispatchRefreshStreams(render, tools);
187
212
  }
188
213
  /**
189
214
  * Run every streamSpec's refresh tool and broadcast the result on
@@ -194,11 +219,11 @@ export async function dispatchAction(frame, context) {
194
219
  * runtime emits in declaration order and the reference server
195
220
  * preserves that contract.
196
221
  */
197
- async function dispatchRefreshStreams(session, tools) {
198
- for (const spec of session.streamSpecs.values()) {
222
+ async function dispatchRefreshStreams(render, tools) {
223
+ for (const spec of render.streamSpecs.values()) {
199
224
  const refreshTool = tools.get(spec.tool);
200
225
  if (refreshTool === undefined) {
201
- emitContractError(session, {
226
+ emitContractError(render, {
202
227
  code: 'TOOL_NOT_FOUND',
203
228
  toolName: spec.tool,
204
229
  actionName: spec.channel,
@@ -208,7 +233,7 @@ async function dispatchRefreshStreams(session, tools) {
208
233
  continue;
209
234
  }
210
235
  if (refreshTool.kind === 'malformed' || refreshTool.kind === 'malformed-stream') {
211
- emitContractError(session, {
236
+ emitContractError(render, {
212
237
  code: 'SCHEMA_VIOLATION',
213
238
  toolName: refreshTool.name,
214
239
  actionName: spec.channel,
@@ -223,7 +248,7 @@ async function dispatchRefreshStreams(session, tools) {
223
248
  }
224
249
  catch (err) {
225
250
  const error = err instanceof Error ? err : new Error(String(err));
226
- emitContractError(session, {
251
+ emitContractError(render, {
227
252
  code: 'TOOL_THREW',
228
253
  toolName: refreshTool.name,
229
254
  actionName: spec.channel,
@@ -233,7 +258,7 @@ async function dispatchRefreshStreams(session, tools) {
233
258
  });
234
259
  continue;
235
260
  }
236
- broadcast(session, {
261
+ broadcast(render, {
237
262
  type: 'stream',
238
263
  payload: {
239
264
  channel: spec.channel,
@@ -242,7 +267,7 @@ async function dispatchRefreshStreams(session, tools) {
242
267
  });
243
268
  }
244
269
  }
245
- function emitContractError(session, input) {
270
+ function emitContractError(render, input) {
246
271
  // Canonical SPEC §4.4 `ContractErrorPayload` via the central
247
272
  // builder. Nested `error: {code, message, causedBy}` alongside
248
273
  // flat `toolName` / `actionName` / `sourceAction` / `timestamp`.
@@ -262,7 +287,7 @@ function emitContractError(session, input) {
262
287
  },
263
288
  timestamp: new Date().toISOString(),
264
289
  });
265
- broadcast(session, {
290
+ broadcast(render, {
266
291
  type: 'stream',
267
292
  payload: {
268
293
  channel: '_ggui:contract-error',
@@ -270,8 +295,8 @@ function emitContractError(session, input) {
270
295
  },
271
296
  });
272
297
  }
273
- function broadcast(session, frame) {
274
- for (const subscriber of session.subscribers) {
298
+ function broadcast(render, frame) {
299
+ for (const subscriber of render.subscribers) {
275
300
  try {
276
301
  subscriber.send(frame);
277
302
  }
@@ -6,13 +6,12 @@
6
6
  * Directives split into "implement" and "throw":
7
7
  *
8
8
  * Implement:
9
- * - create-session → `sessions.create()`
9
+ * - create-render → `renders.create()`
10
10
  * - register-tool → `tools.register(name, handler)`
11
- * - register-actionspec → `sessions.registerActionSpec()`
12
- * - register-streamspec → `sessions.registerStreamSpec()`
13
- * - server-version-override → `sessions.setVersionOverride()`
14
- * - emit-envelope → `sessions.injectFrame()`
15
- * - close-session → `sessions.close()`
11
+ * - register-actionspec → `renders.registerActionSpec()`
12
+ * - register-streamspec → `renders.registerStreamSpec()`
13
+ * - server-version-override → `renders.setVersionOverride()`
14
+ * - emit-envelope → `renders.injectFrame()`
16
15
  * - unregister-tool → `tools.unregister()`
17
16
  *
18
17
  * Throw (kit records SKIP, not FAIL):
@@ -24,6 +23,10 @@
24
23
  * skip expectations — browser-level fault injection that requires a
25
24
  * richer host harness. Throwing surfaces "directive not implemented"
26
25
  * with the error message as the skip reason.
26
+ *
27
+ * Note: render-termination directive (`close-render`) is intentionally
28
+ * absent — render lifecycle is implicit (created → active → TTL-expired);
29
+ * there is no agent-facing close tool, and no kit directive to invoke.
27
30
  */
28
31
  import type { ConformanceHost } from '@ggui-ai/protocol-conformance';
29
32
  import type { ReferenceServer } from './server.js';
@@ -36,8 +39,8 @@ export interface CreateReferenceConformanceHostInput {
36
39
  * drive the kit against the server.
37
40
  *
38
41
  * The server MUST be `start()`-ed before the first dispatch — the
39
- * kit calls `create-session` via `dispatchSetup` before any subscribe,
40
- * so the session store must be reachable. The caller owns the
42
+ * kit calls `create-render` via `dispatchSetup` before any subscribe,
43
+ * so the render store must be reachable. The caller owns the
41
44
  * server lifecycle (`start()` + `stop()`).
42
45
  */
43
46
  export declare function createReferenceConformanceHost({ serverInstance, }: CreateReferenceConformanceHostInput): ConformanceHost;
@@ -1 +1 @@
1
- {"version":3,"file":"conformance-host.d.ts","sourceRoot":"","sources":["../src/conformance-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAEV,eAAe,EAMhB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC;CAC1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,EAC7C,cAAc,GACf,EAAE,mCAAmC,GAAG,eAAe,CAwNvD"}
1
+ {"version":3,"file":"conformance-host.d.ts","sourceRoot":"","sources":["../src/conformance-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,EACV,eAAe,EAMhB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC;CAC1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,EAC7C,cAAc,GACf,EAAE,mCAAmC,GAAG,eAAe,CAmNvD"}
@@ -4,8 +4,8 @@
4
4
  * drive the kit against the server.
5
5
  *
6
6
  * The server MUST be `start()`-ed before the first dispatch — the
7
- * kit calls `create-session` via `dispatchSetup` before any subscribe,
8
- * so the session store must be reachable. The caller owns the
7
+ * kit calls `create-render` via `dispatchSetup` before any subscribe,
8
+ * so the render store must be reachable. The caller owns the
9
9
  * server lifecycle (`start()` + `stop()`).
10
10
  */
11
11
  export function createReferenceConformanceHost({ serverInstance, }) {
@@ -14,9 +14,9 @@ export function createReferenceConformanceHost({ serverInstance, }) {
14
14
  // Discriminant-narrow via explicit casts — the extensibly-closed
15
15
  // `HostUnknownSetupStep` arm (`kind: string & {}`) widens the
16
16
  // discriminant and blocks literal-narrowing on the union.
17
- if (step.kind === 'create-session') {
17
+ if (step.kind === 'create-render') {
18
18
  const s = step;
19
- serverInstance.sessions.create(s.sessionId, s.appId ?? 'conformance');
19
+ serverInstance.renders.create(s.renderId, s.appId ?? 'conformance');
20
20
  return;
21
21
  }
22
22
  if (step.kind === 'register-tool') {
@@ -35,16 +35,16 @@ export function createReferenceConformanceHost({ serverInstance, }) {
35
35
  }
36
36
  if (step.kind === 'register-actionspec') {
37
37
  const s = step;
38
- // register-actionspec doesn't carry a sessionId in the
38
+ // register-actionspec doesn't carry a renderId in the
39
39
  // directive shape — it's scoped to the most-recently-created
40
- // session, matching the fixture-authoring convention that
41
- // create-session → register-tool → register-actionspec all
42
- // land in order on the same session.
43
- const lastSession = serverInstance.sessions.lastCreatedSessionId();
44
- if (lastSession === undefined) {
45
- throw new Error('reference-server: register-actionspec invoked before create-session — no session scope to bind to');
40
+ // render, matching the fixture-authoring convention that
41
+ // create-render → register-tool → register-actionspec all
42
+ // land in order on the same render.
43
+ const lastRenderId = serverInstance.renders.lastCreatedRenderId();
44
+ if (lastRenderId === undefined) {
45
+ throw new Error('reference-server: register-actionspec invoked before create-render — no render scope to bind to');
46
46
  }
47
- serverInstance.sessions.registerActionSpec(lastSession, {
47
+ serverInstance.renders.registerActionSpec(lastRenderId, {
48
48
  name: s.name,
49
49
  tool: s.tool,
50
50
  });
@@ -58,7 +58,7 @@ export function createReferenceConformanceHost({ serverInstance, }) {
58
58
  // Slice I refresh-stream support); the runtime shape is
59
59
  // narrowed locally, matching the same convention as the
60
60
  // pre-existing `register-tool` branch above. Same most-
61
- // recently-created session scoping as register-actionspec.
61
+ // recently-created render scoping as register-actionspec.
62
62
  const raw = step;
63
63
  if (typeof raw.channel !== 'string' || raw.channel.length === 0) {
64
64
  throw new Error(`register-streamspec directive missing channel: ${JSON.stringify(step)}`);
@@ -66,11 +66,11 @@ export function createReferenceConformanceHost({ serverInstance, }) {
66
66
  if (typeof raw.tool !== 'string' || raw.tool.length === 0) {
67
67
  throw new Error(`register-streamspec directive missing tool: ${JSON.stringify(step)}`);
68
68
  }
69
- const lastSession = serverInstance.sessions.lastCreatedSessionId();
70
- if (lastSession === undefined) {
71
- throw new Error('reference-server: register-streamspec invoked before create-session — no session scope to bind to');
69
+ const lastRenderId = serverInstance.renders.lastCreatedRenderId();
70
+ if (lastRenderId === undefined) {
71
+ throw new Error('reference-server: register-streamspec invoked before create-render — no render scope to bind to');
72
72
  }
73
- serverInstance.sessions.registerStreamSpec(lastSession, {
73
+ serverInstance.renders.registerStreamSpec(lastRenderId, {
74
74
  channel: raw.channel,
75
75
  tool: raw.tool,
76
76
  });
@@ -78,11 +78,11 @@ export function createReferenceConformanceHost({ serverInstance, }) {
78
78
  }
79
79
  if (step.kind === 'emit-envelope') {
80
80
  // The directive carries `channel` + `payload` but no
81
- // sessionId — it's scoped to the most-recently-created
82
- // session, matching the same fixture-authoring convention as
81
+ // renderId — it's scoped to the most-recently-created
82
+ // render, matching the same fixture-authoring convention as
83
83
  // register-actionspec / register-streamspec / server-version-
84
84
  // override (the kit's `narrowSetupStep` is a flat `type → kind`
85
- // rename pass-through, so any sessionId on the directive JSON
85
+ // rename pass-through, so any renderId on the directive JSON
86
86
  // would survive, but the canonical EmitEnvelopeSetup shape
87
87
  // doesn't declare one).
88
88
  //
@@ -98,11 +98,11 @@ export function createReferenceConformanceHost({ serverInstance, }) {
98
98
  if (typeof s.channel !== 'string' || s.channel.length === 0) {
99
99
  throw new Error(`emit-envelope directive missing channel: ${JSON.stringify(step)}`);
100
100
  }
101
- const lastSession = serverInstance.sessions.lastCreatedSessionId();
102
- if (lastSession === undefined) {
103
- throw new Error('reference-server: emit-envelope invoked before create-session — no session scope to bind to');
101
+ const lastRenderId = serverInstance.renders.lastCreatedRenderId();
102
+ if (lastRenderId === undefined) {
103
+ throw new Error('reference-server: emit-envelope invoked before create-render — no render scope to bind to');
104
104
  }
105
- const fanned = serverInstance.sessions.injectFrame(lastSession, {
105
+ const fanned = serverInstance.renders.injectFrame(lastRenderId, {
106
106
  type: 'stream',
107
107
  payload: {
108
108
  channel: s.channel,
@@ -112,12 +112,12 @@ export function createReferenceConformanceHost({ serverInstance, }) {
112
112
  if (!fanned) {
113
113
  // No subscribers attached — the directive's emission is
114
114
  // unobservable. Surface for fixture-authoring debuggability
115
- // (the canonical sequence is create-session → subscribe →
115
+ // (the canonical sequence is create-render → subscribe →
116
116
  // emit-envelope; fixtures that swap order silently lose the
117
117
  // injection). Not a throw — the directive itself succeeded;
118
118
  // the unobservability is a fixture concern.
119
119
  // eslint-disable-next-line no-console
120
- console.warn(`[@ggui-ai/protocol-reference-server] emit-envelope on session '${lastSession}' channel '${s.channel}' had no subscribers — frame dropped`);
120
+ console.warn(`[@ggui-ai/protocol-reference-server] emit-envelope on render '${lastRenderId}' channel '${s.channel}' had no subscribers — frame dropped`);
121
121
  }
122
122
  return;
123
123
  }
@@ -141,21 +141,21 @@ export function createReferenceConformanceHost({ serverInstance, }) {
141
141
  if (typeof advertise !== 'string' || advertise.length === 0) {
142
142
  throw new Error(`server-version-override directive missing advertiseVersion/version: ${JSON.stringify(step)}`);
143
143
  }
144
- // Same most-recently-created session scope as register-
144
+ // Same most-recently-created render scope as register-
145
145
  // actionspec / register-streamspec — the fixture authoring
146
- // convention is `create-session` immediately precedes this
146
+ // convention is `create-render` immediately precedes this
147
147
  // directive, and the kit's narrowSetupStep doesn't surface a
148
- // sessionId on the directive object even when the fixture
148
+ // renderId on the directive object even when the fixture
149
149
  // JSON includes one (only `type → kind` is renamed; the rest
150
- // is a flat passthrough, so a `sessionId` field WOULD survive
150
+ // is a flat passthrough, so a `renderId` field WOULD survive
151
151
  // — but the canonical ServerVersionOverrideSetup type doesn't
152
152
  // declare one, so fixtures may omit it. Falling back to
153
- // `lastCreatedSessionId()` keeps the host robust to either.
154
- const lastSession = serverInstance.sessions.lastCreatedSessionId();
155
- if (lastSession === undefined) {
156
- throw new Error('reference-server: server-version-override invoked before create-session — no session scope to bind to');
153
+ // `lastCreatedRenderId()` keeps the host robust to either.
154
+ const lastRenderId = serverInstance.renders.lastCreatedRenderId();
155
+ if (lastRenderId === undefined) {
156
+ throw new Error('reference-server: server-version-override invoked before create-render — no render scope to bind to');
157
157
  }
158
- serverInstance.sessions.setVersionOverride(lastSession, advertise);
158
+ serverInstance.renders.setVersionOverride(lastRenderId, advertise);
159
159
  return;
160
160
  }
161
161
  // Unknown kind — extensibly-closed. Throw so the kit records
@@ -164,11 +164,6 @@ export function createReferenceConformanceHost({ serverInstance, }) {
164
164
  throw new Error(`reference server does not implement setup kind '${String(unknownKind)}'`);
165
165
  },
166
166
  async dispatchTeardown(step) {
167
- if (step.kind === 'close-session') {
168
- const s = step;
169
- serverInstance.sessions.close(s.sessionId);
170
- return;
171
- }
172
167
  if (step.kind === 'unregister-tool') {
173
168
  // Same toolName/name tolerance as register-tool.
174
169
  const raw = step;
@@ -1,14 +1,21 @@
1
1
  /**
2
- * In-memory session store for the reference server.
2
+ * In-memory render store for the reference server.
3
3
  *
4
- * Sessions are ephemeral and process-local — this is the whole
4
+ * Renders are ephemeral and process-local — this is the whole
5
5
  * point of the reference server. Persistence is explicitly out of
6
6
  * scope. Restart drops state; that's documented behavior, not a TODO.
7
7
  *
8
- * Each session carries an actionSpec map that the `register-actionspec`
8
+ * Each render carries an actionSpec map that the `register-actionspec`
9
9
  * ConformanceHost directive populates. The action router
10
10
  * (`./action-router.ts`) consults this map at dispatch time to
11
11
  * resolve action-name → tool-name → handler.
12
+ *
13
+ * Wire-shape note: the conformance kit drives the reference server
14
+ * over the SPEC §12.2 wire using the field name `sessionId` (its
15
+ * fixture catalog has not yet been migrated to the canonical
16
+ * `renderId`). The reference server READS that wire field and binds
17
+ * it to a `renderId` internally — the value is a render identity
18
+ * regardless of which spelling the consumer sends.
12
19
  */
13
20
  /**
14
21
  * Actionspec entry maps an action name (the value wired into DOM
@@ -31,7 +38,7 @@ export interface ActionSpecEntry {
31
38
  *
32
39
  * Reference-server scope: refresh-tool invocation is unconditional
33
40
  * — every successful wired-action dispatch fans out through every
34
- * registered streamSpec for the session. Real ggui servers may
41
+ * registered streamSpec for the render. Real ggui servers may
35
42
  * filter by which actions touch which channels; the reference
36
43
  * server's narrower contract is "any successful action triggers all
37
44
  * declared refreshes", which is sufficient for the kit's
@@ -42,14 +49,14 @@ export interface StreamSpecEntry {
42
49
  readonly channel: string;
43
50
  readonly tool: string;
44
51
  }
45
- export interface Session {
46
- readonly sessionId: string;
52
+ export interface Render {
53
+ readonly renderId: string;
47
54
  readonly appId: string;
48
55
  readonly actionSpecs: Map<string, ActionSpecEntry>;
49
56
  readonly streamSpecs: Map<string, StreamSpecEntry>;
50
57
  readonly subscribers: Set<Subscriber>;
51
58
  /**
52
- * Per-session protocol-version override. When set, the WS subscribe
59
+ * Per-render protocol-version override. When set, the WS subscribe
53
60
  * + UPGRADE_REQUIRED paths advertise this value in place of the
54
61
  * server-instance-level `versionOverride`.
55
62
  *
@@ -58,9 +65,9 @@ export interface Session {
58
65
  * `server-version-override` setup directive in the conformance host
59
66
  * adapter — production code paths leave this `undefined`.
60
67
  *
61
- * Why per-session, not per-instance: parallel kit fixtures share
68
+ * Why per-render, not per-instance: parallel kit fixtures share
62
69
  * one `ReferenceServer`. Mutating the instance-level override would
63
- * leak across sessions; the per-session field scopes the mismatch
70
+ * leak across renders; the per-render field scopes the mismatch
64
71
  * to the one fixture that asked for it.
65
72
  */
66
73
  versionOverride?: string;
@@ -74,59 +81,59 @@ export interface Subscriber {
74
81
  send(frame: unknown): void;
75
82
  }
76
83
  /**
77
- * In-memory session store. Wraps a `Map<sessionId, Session>` with
84
+ * In-memory render store. Wraps a `Map<renderId, Render>` with
78
85
  * the operations the ConformanceHost adapter + WS subscribe handler
79
86
  * need. No locking — JS single-threaded; all calls originate from
80
87
  * the event loop.
81
88
  */
82
- export declare class SessionStore {
83
- private readonly sessions;
89
+ export declare class RenderStore {
90
+ private readonly renders;
84
91
  private lastCreated;
85
- create(sessionId: string, appId: string): Session;
92
+ create(renderId: string, appId: string): Render;
86
93
  /**
87
- * The sessionId most recently passed to `create()`. Used by the
94
+ * The renderId most recently passed to `create()`. Used by the
88
95
  * ConformanceHost's `register-actionspec` dispatcher — that
89
- * directive doesn't carry a sessionId in its JSON shape, so the
96
+ * directive doesn't carry a renderId in its JSON shape, so the
90
97
  * adapter needs the "most recently created" scope to bind the
91
98
  * actionspec to. This matches the fixture-authoring convention that
92
- * create-session always precedes register-actionspec.
99
+ * create-render always precedes register-actionspec.
93
100
  */
94
- lastCreatedSessionId(): string | undefined;
95
- get(sessionId: string): Session | undefined;
96
- close(sessionId: string): boolean;
97
- addSubscriber(sessionId: string, subscriber: Subscriber): Session;
98
- removeSubscriber(sessionId: string, subscriber: Subscriber): void;
99
- registerActionSpec(sessionId: string, entry: ActionSpecEntry): void;
101
+ lastCreatedRenderId(): string | undefined;
102
+ get(renderId: string): Render | undefined;
103
+ close(renderId: string): boolean;
104
+ addSubscriber(renderId: string, subscriber: Subscriber): Render;
105
+ removeSubscriber(renderId: string, subscriber: Subscriber): void;
106
+ registerActionSpec(renderId: string, entry: ActionSpecEntry): void;
100
107
  /**
101
108
  * Register a stream channel ↔ refresh-tool binding on the named
102
- * session. Same "create-if-missing" semantics as
109
+ * render. Same "create-if-missing" semantics as
103
110
  * {@link registerActionSpec} so the ConformanceHost adapter can
104
111
  * dispatch this directive before subscribe lands. Keyed by
105
112
  * `entry.channel` — registering the same channel twice replaces
106
113
  * the prior binding, matching the action-spec map's behavior.
107
114
  */
108
- registerStreamSpec(sessionId: string, entry: StreamSpecEntry): void;
115
+ registerStreamSpec(renderId: string, entry: StreamSpecEntry): void;
109
116
  /**
110
- * Set the per-session protocol-version override. Used by the
117
+ * Set the per-render protocol-version override. Used by the
111
118
  * `server-version-override` ConformanceHost directive — populates
112
- * {@link Session.versionOverride} so the WS subscribe handler
119
+ * {@link Render.versionOverride} so the WS subscribe handler
113
120
  * advertises this value (and emits UPGRADE_REQUIRED keyed off it)
114
- * for THIS session only, leaving parallel sessions on the instance-
121
+ * for THIS render only, leaving parallel renders on the instance-
115
122
  * level default.
116
123
  *
117
124
  * Same "create-if-missing" semantics as the other register* setters
118
125
  * so directive ordering relative to subscribe doesn't matter.
119
126
  */
120
- setVersionOverride(sessionId: string, version: string): void;
127
+ setVersionOverride(renderId: string, version: string): void;
121
128
  /**
122
- * Fan out a frame to every subscriber on the named session. Used by
129
+ * Fan out a frame to every subscriber on the named render. Used by
123
130
  * the `emit-envelope` ConformanceHost directive — kit fixtures use
124
131
  * it to inject WS-observable side-effects (envelopes the server
125
132
  * would not normally emit on its own) so the kit can assert
126
133
  * downstream consequences (sequencing, fan-out, observability).
127
134
  *
128
- * Returns `true` if the session existed and at least one subscriber
129
- * received the frame; `false` if the session is unknown OR has no
135
+ * Returns `true` if the render existed and at least one subscriber
136
+ * received the frame; `false` if the render is unknown OR has no
130
137
  * subscribers attached. Caller may use the boolean to log a warning
131
138
  * when a fixture's directive-injection lands before any subscribe
132
139
  * — the directive then has no observable effect, which is usually
@@ -137,6 +144,6 @@ export declare class SessionStore {
137
144
  * `broadcast()` — one bad subscriber must not block fan-out to the
138
145
  * rest.
139
146
  */
140
- injectFrame(sessionId: string, frame: unknown): boolean;
147
+ injectFrame(renderId: string, frame: unknown): boolean;
141
148
  }
142
- //# sourceMappingURL=session.d.ts.map
149
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnD,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnD,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IACtC;;;;;;;;;;;;;;OAcG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,WAAW,CAAqB;IAExC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAkB/C;;;;;;;OAOG;IACH,mBAAmB,IAAI,MAAM,GAAG,SAAS;IAIzC,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIzC,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIhC,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM;IAM/D,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;IAMhE,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,IAAI;IAKlE;;;;;;;OAOG;IACH,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,IAAI;IAKlE;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAK3D;;;;;;;;;;;;;;;;;;OAkBG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO;CAcvD"}
@@ -1,109 +1,116 @@
1
1
  /**
2
- * In-memory session store for the reference server.
2
+ * In-memory render store for the reference server.
3
3
  *
4
- * Sessions are ephemeral and process-local — this is the whole
4
+ * Renders are ephemeral and process-local — this is the whole
5
5
  * point of the reference server. Persistence is explicitly out of
6
6
  * scope. Restart drops state; that's documented behavior, not a TODO.
7
7
  *
8
- * Each session carries an actionSpec map that the `register-actionspec`
8
+ * Each render carries an actionSpec map that the `register-actionspec`
9
9
  * ConformanceHost directive populates. The action router
10
10
  * (`./action-router.ts`) consults this map at dispatch time to
11
11
  * resolve action-name → tool-name → handler.
12
+ *
13
+ * Wire-shape note: the conformance kit drives the reference server
14
+ * over the SPEC §12.2 wire using the field name `sessionId` (its
15
+ * fixture catalog has not yet been migrated to the canonical
16
+ * `renderId`). The reference server READS that wire field and binds
17
+ * it to a `renderId` internally — the value is a render identity
18
+ * regardless of which spelling the consumer sends.
12
19
  */
13
20
  /**
14
- * In-memory session store. Wraps a `Map<sessionId, Session>` with
21
+ * In-memory render store. Wraps a `Map<renderId, Render>` with
15
22
  * the operations the ConformanceHost adapter + WS subscribe handler
16
23
  * need. No locking — JS single-threaded; all calls originate from
17
24
  * the event loop.
18
25
  */
19
- export class SessionStore {
20
- sessions = new Map();
26
+ export class RenderStore {
27
+ renders = new Map();
21
28
  lastCreated;
22
- create(sessionId, appId) {
23
- const existing = this.sessions.get(sessionId);
29
+ create(renderId, appId) {
30
+ const existing = this.renders.get(renderId);
24
31
  if (existing !== undefined) {
25
- this.lastCreated = sessionId;
32
+ this.lastCreated = renderId;
26
33
  return existing;
27
34
  }
28
- const session = {
29
- sessionId,
35
+ const render = {
36
+ renderId,
30
37
  appId,
31
38
  actionSpecs: new Map(),
32
39
  streamSpecs: new Map(),
33
40
  subscribers: new Set(),
34
41
  };
35
- this.sessions.set(sessionId, session);
36
- this.lastCreated = sessionId;
37
- return session;
42
+ this.renders.set(renderId, render);
43
+ this.lastCreated = renderId;
44
+ return render;
38
45
  }
39
46
  /**
40
- * The sessionId most recently passed to `create()`. Used by the
47
+ * The renderId most recently passed to `create()`. Used by the
41
48
  * ConformanceHost's `register-actionspec` dispatcher — that
42
- * directive doesn't carry a sessionId in its JSON shape, so the
49
+ * directive doesn't carry a renderId in its JSON shape, so the
43
50
  * adapter needs the "most recently created" scope to bind the
44
51
  * actionspec to. This matches the fixture-authoring convention that
45
- * create-session always precedes register-actionspec.
52
+ * create-render always precedes register-actionspec.
46
53
  */
47
- lastCreatedSessionId() {
54
+ lastCreatedRenderId() {
48
55
  return this.lastCreated;
49
56
  }
50
- get(sessionId) {
51
- return this.sessions.get(sessionId);
57
+ get(renderId) {
58
+ return this.renders.get(renderId);
52
59
  }
53
- close(sessionId) {
54
- return this.sessions.delete(sessionId);
60
+ close(renderId) {
61
+ return this.renders.delete(renderId);
55
62
  }
56
- addSubscriber(sessionId, subscriber) {
57
- const session = this.create(sessionId, 'conformance');
58
- session.subscribers.add(subscriber);
59
- return session;
63
+ addSubscriber(renderId, subscriber) {
64
+ const render = this.create(renderId, 'conformance');
65
+ render.subscribers.add(subscriber);
66
+ return render;
60
67
  }
61
- removeSubscriber(sessionId, subscriber) {
62
- const session = this.sessions.get(sessionId);
63
- if (session === undefined)
68
+ removeSubscriber(renderId, subscriber) {
69
+ const render = this.renders.get(renderId);
70
+ if (render === undefined)
64
71
  return;
65
- session.subscribers.delete(subscriber);
72
+ render.subscribers.delete(subscriber);
66
73
  }
67
- registerActionSpec(sessionId, entry) {
68
- const session = this.create(sessionId, 'conformance');
69
- session.actionSpecs.set(entry.name, entry);
74
+ registerActionSpec(renderId, entry) {
75
+ const render = this.create(renderId, 'conformance');
76
+ render.actionSpecs.set(entry.name, entry);
70
77
  }
71
78
  /**
72
79
  * Register a stream channel ↔ refresh-tool binding on the named
73
- * session. Same "create-if-missing" semantics as
80
+ * render. Same "create-if-missing" semantics as
74
81
  * {@link registerActionSpec} so the ConformanceHost adapter can
75
82
  * dispatch this directive before subscribe lands. Keyed by
76
83
  * `entry.channel` — registering the same channel twice replaces
77
84
  * the prior binding, matching the action-spec map's behavior.
78
85
  */
79
- registerStreamSpec(sessionId, entry) {
80
- const session = this.create(sessionId, 'conformance');
81
- session.streamSpecs.set(entry.channel, entry);
86
+ registerStreamSpec(renderId, entry) {
87
+ const render = this.create(renderId, 'conformance');
88
+ render.streamSpecs.set(entry.channel, entry);
82
89
  }
83
90
  /**
84
- * Set the per-session protocol-version override. Used by the
91
+ * Set the per-render protocol-version override. Used by the
85
92
  * `server-version-override` ConformanceHost directive — populates
86
- * {@link Session.versionOverride} so the WS subscribe handler
93
+ * {@link Render.versionOverride} so the WS subscribe handler
87
94
  * advertises this value (and emits UPGRADE_REQUIRED keyed off it)
88
- * for THIS session only, leaving parallel sessions on the instance-
95
+ * for THIS render only, leaving parallel renders on the instance-
89
96
  * level default.
90
97
  *
91
98
  * Same "create-if-missing" semantics as the other register* setters
92
99
  * so directive ordering relative to subscribe doesn't matter.
93
100
  */
94
- setVersionOverride(sessionId, version) {
95
- const session = this.create(sessionId, 'conformance');
96
- session.versionOverride = version;
101
+ setVersionOverride(renderId, version) {
102
+ const render = this.create(renderId, 'conformance');
103
+ render.versionOverride = version;
97
104
  }
98
105
  /**
99
- * Fan out a frame to every subscriber on the named session. Used by
106
+ * Fan out a frame to every subscriber on the named render. Used by
100
107
  * the `emit-envelope` ConformanceHost directive — kit fixtures use
101
108
  * it to inject WS-observable side-effects (envelopes the server
102
109
  * would not normally emit on its own) so the kit can assert
103
110
  * downstream consequences (sequencing, fan-out, observability).
104
111
  *
105
- * Returns `true` if the session existed and at least one subscriber
106
- * received the frame; `false` if the session is unknown OR has no
112
+ * Returns `true` if the render existed and at least one subscriber
113
+ * received the frame; `false` if the render is unknown OR has no
107
114
  * subscribers attached. Caller may use the boolean to log a warning
108
115
  * when a fixture's directive-injection lands before any subscribe
109
116
  * — the directive then has no observable effect, which is usually
@@ -114,13 +121,13 @@ export class SessionStore {
114
121
  * `broadcast()` — one bad subscriber must not block fan-out to the
115
122
  * rest.
116
123
  */
117
- injectFrame(sessionId, frame) {
118
- const session = this.sessions.get(sessionId);
119
- if (session === undefined)
124
+ injectFrame(renderId, frame) {
125
+ const render = this.renders.get(renderId);
126
+ if (render === undefined)
120
127
  return false;
121
- if (session.subscribers.size === 0)
128
+ if (render.subscribers.size === 0)
122
129
  return false;
123
- for (const subscriber of session.subscribers) {
130
+ for (const subscriber of render.subscribers) {
124
131
  try {
125
132
  subscriber.send(frame);
126
133
  }
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SessionStore } from './session.js';
1
+ import { RenderStore } from './render.js';
2
2
  import { ToolRegistry } from './tool-registry.js';
3
3
  export interface ReferenceServerOptions {
4
4
  /** Port to bind. `0` = ephemeral — use {@link ReferenceServer.port}
@@ -31,7 +31,7 @@ export interface ReferenceServerOptions {
31
31
  readonly versionOverride?: string;
32
32
  }
33
33
  export declare class ReferenceServer {
34
- readonly sessions: SessionStore;
34
+ readonly renders: RenderStore;
35
35
  readonly tools: ToolRegistry;
36
36
  private readonly options;
37
37
  private http;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,YAAY,EAAmB,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,WAAW,sBAAsB;IACrC;iDAC6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,qBAAa,eAAe;IAC1B,QAAQ,CAAC,QAAQ,eAAsB;IACvC,QAAQ,CAAC,KAAK,eAAsB;IAEpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,IAAI,CAA2B;IACvC,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,SAAS,CAAuB;gBAE5B,OAAO,EAAE,sBAAsB;IAS3C;;;;;;OAMG;IACH,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED,sDAAsD;IACtD,IAAI,IAAI,IAAI,MAAM,CAKjB;IAED,4DAA4D;IAC5D,IAAI,OAAO,IAAI,MAAM,CAEpB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA+BtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B,OAAO,CAAC,gBAAgB;YAoCV,aAAa;IAuC3B,OAAO,CAAC,eAAe;CAmExB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,WAAW,EAAmB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,WAAW,sBAAsB;IACrC;iDAC6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,qBAAa,eAAe;IAC1B,QAAQ,CAAC,OAAO,cAAqB;IACrC,QAAQ,CAAC,KAAK,eAAsB;IAEpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,IAAI,CAA2B;IACvC,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,SAAS,CAAuB;gBAE5B,OAAO,EAAE,sBAAsB;IAS3C;;;;;;OAMG;IACH,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED,sDAAsD;IACtD,IAAI,IAAI,IAAI,MAAM,CAKjB;IAED,4DAA4D;IAC5D,IAAI,OAAO,IAAI,MAAM,CAEpB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA+BtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B,OAAO,CAAC,gBAAgB;YAoCV,aAAa;IAyC3B,OAAO,CAAC,eAAe;CA4ExB"}
package/dist/server.js CHANGED
@@ -8,15 +8,22 @@
8
8
  * separation claim (Protocol #6) is empirically grounded — if this
9
9
  * server passes `@ggui-ai/protocol-conformance`, the protocol has
10
10
  * no implicit `@ggui-ai/mcp-server` coupling.
11
+ *
12
+ * Wire-field note: the consumer (`@ggui-ai/protocol-conformance`)
13
+ * still names the render identity field `sessionId` on the wire (its
14
+ * fixtures have not yet been renamed). The reference server honors
15
+ * the consumer contract by reading that field name verbatim, then
16
+ * binds the value to a `renderId` internally — see {@link Render}
17
+ * for the canonical identity name used throughout this package.
11
18
  */
12
19
  import { createServer } from 'node:http';
13
20
  import { PROTOCOL_SCHEMA_VERSION } from '@ggui-ai/protocol';
14
21
  import { WebSocketServer } from 'ws';
15
- import { dispatchAction, isActionFrame } from './action-router.js';
16
- import { SessionStore } from './session.js';
22
+ import { dispatchAction, parseActionFrame } from './action-router.js';
23
+ import { RenderStore } from './render.js';
17
24
  import { ToolRegistry } from './tool-registry.js';
18
25
  export class ReferenceServer {
19
- sessions = new SessionStore();
26
+ renders = new RenderStore();
20
27
  tools = new ToolRegistry();
21
28
  options;
22
29
  http = null;
@@ -96,8 +103,8 @@ export class ReferenceServer {
96
103
  // ===========================================================================
97
104
  handleConnection(socket) {
98
105
  // Subscribe state is per-connection — one WS may subscribe to
99
- // one session at a time. Re-subscribe overwrites.
100
- let subscribedSessionId = null;
106
+ // one render at a time. Re-subscribe overwrites.
107
+ let subscribedRenderId = null;
101
108
  const subscriber = {
102
109
  send: (frame) => {
103
110
  try {
@@ -112,19 +119,19 @@ export class ReferenceServer {
112
119
  void this.handleMessage(raw.toString('utf8'), {
113
120
  socket,
114
121
  subscriber,
115
- onSubscribed: (sessionId) => {
116
- // If previously subscribed to a different session, unsub
122
+ onSubscribed: (renderId) => {
123
+ // If previously subscribed to a different render, unsub
117
124
  // from it first.
118
- if (subscribedSessionId !== null && subscribedSessionId !== sessionId) {
119
- this.sessions.removeSubscriber(subscribedSessionId, subscriber);
125
+ if (subscribedRenderId !== null && subscribedRenderId !== renderId) {
126
+ this.renders.removeSubscriber(subscribedRenderId, subscriber);
120
127
  }
121
- subscribedSessionId = sessionId;
128
+ subscribedRenderId = renderId;
122
129
  },
123
130
  });
124
131
  });
125
132
  socket.on('close', () => {
126
- if (subscribedSessionId !== null) {
127
- this.sessions.removeSubscriber(subscribedSessionId, subscriber);
133
+ if (subscribedRenderId !== null) {
134
+ this.renders.removeSubscriber(subscribedRenderId, subscriber);
128
135
  }
129
136
  });
130
137
  }
@@ -150,11 +157,14 @@ export class ReferenceServer {
150
157
  });
151
158
  return;
152
159
  }
153
- if (f['type'] === 'action' && isActionFrame(frame)) {
154
- const session = this.sessions.get(frame.sessionId);
155
- if (session === undefined)
156
- return; // drop actions for unknown sessions
157
- await dispatchAction(frame, { session, tools: this.tools });
160
+ if (f['type'] === 'action') {
161
+ const parsed = parseActionFrame(frame);
162
+ if (parsed === undefined)
163
+ return; // malformed — silently drop
164
+ const render = this.renders.get(parsed.renderId);
165
+ if (render === undefined)
166
+ return; // drop actions for unknown renders
167
+ await dispatchAction(parsed, { render, tools: this.tools });
158
168
  return;
159
169
  }
160
170
  // Unrecognized type — silently drop (extensibly-closed; third
@@ -165,8 +175,16 @@ export class ReferenceServer {
165
175
  if (payload === null || typeof payload !== 'object')
166
176
  return;
167
177
  const p = payload;
168
- const sessionId = p['sessionId'];
169
- if (typeof sessionId !== 'string')
178
+ // Wire-field acceptance: the conformance kit currently sends
179
+ // `sessionId`; the canonical SPEC field is `renderId`. Read both
180
+ // so the reference server is forward-compatible with the kit's
181
+ // eventual rename without breaking today's fixtures.
182
+ const renderId = typeof p['renderId'] === 'string'
183
+ ? p['renderId']
184
+ : typeof p['sessionId'] === 'string'
185
+ ? p['sessionId']
186
+ : undefined;
187
+ if (renderId === undefined)
170
188
  return;
171
189
  const appId = typeof p['appId'] === 'string' ? p['appId'] : 'conformance';
172
190
  const requestId = typeof frame['requestId'] === 'string' ? frame['requestId'] : undefined;
@@ -182,13 +200,13 @@ export class ReferenceServer {
182
200
  // - `strictVersionPolicy: false` (advisory opt-out): emit +
183
201
  // keep the connection open.
184
202
  //
185
- // Per-session override precedence: if the `server-version-override`
186
- // directive set a `versionOverride` on this session BEFORE the
203
+ // Per-render override precedence: if the `server-version-override`
204
+ // directive set a `versionOverride` on this render BEFORE the
187
205
  // subscribe landed, advertise that value instead of the instance-
188
206
  // level default. Lets parallel kit fixtures share one server while
189
- // mismatching version on exactly one session.
190
- const existingSession = this.sessions.get(sessionId);
191
- const advertised = existingSession?.versionOverride ?? this.options.versionOverride;
207
+ // mismatching version on exactly one render.
208
+ const existingRender = this.renders.get(renderId);
209
+ const advertised = existingRender?.versionOverride ?? this.options.versionOverride;
192
210
  if (supportedVersions !== undefined && !supportedVersions.includes(advertised)) {
193
211
  ctx.subscriber.send({
194
212
  type: 'error',
@@ -210,11 +228,11 @@ export class ReferenceServer {
210
228
  return;
211
229
  }
212
230
  // Add the subscriber + emit ack.
213
- this.sessions.addSubscriber(sessionId, ctx.subscriber);
231
+ this.renders.addSubscriber(renderId, ctx.subscriber);
214
232
  // Preserve appId on first subscribe — create() is no-op if the
215
- // session already exists from an earlier directive.
216
- this.sessions.create(sessionId, appId);
217
- ctx.onSubscribed(sessionId);
233
+ // render already exists from an earlier directive.
234
+ this.renders.create(renderId, appId);
235
+ ctx.onSubscribed(renderId);
218
236
  ctx.subscriber.send({
219
237
  type: 'ack',
220
238
  payload: { serverVersion: advertised },
@@ -45,9 +45,9 @@ export interface RegisteredTool {
45
45
  */
46
46
  export declare function buildHandler(kind: ToolHandlerKind): ToolHandler;
47
47
  /**
48
- * In-memory tool registry. Scoped to a session via the action router
48
+ * In-memory tool registry. Scoped to a render via the action router
49
49
  * (the plan's register-tool directive wires a handler under the
50
- * session's tool namespace). No-persistence by design.
50
+ * render's tool namespace). No-persistence by design.
51
51
  */
52
52
  export declare class ToolRegistry {
53
53
  private readonly tools;
@@ -71,9 +71,9 @@ export function buildHandler(kind) {
71
71
  }
72
72
  }
73
73
  /**
74
- * In-memory tool registry. Scoped to a session via the action router
74
+ * In-memory tool registry. Scoped to a render via the action router
75
75
  * (the plan's register-tool directive wires a handler under the
76
- * session's tool namespace). No-persistence by design.
76
+ * render's tool namespace). No-persistence by design.
77
77
  */
78
78
  export class ToolRegistry {
79
79
  tools = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/protocol-reference-server",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.2.0-alpha.1",
4
4
  "description": "Minimal reference implementation of the ggui protocol. Implements exactly enough of the live-channel WebSocket wire to pass the @ggui-ai/protocol-conformance kit. Not a production server — it exists to prove the protocol is vendor-neutral: an independent, from-scratch implementation passing the kit grounds that claim empirically.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -29,14 +29,14 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "ws": "^8.20.1",
32
- "@ggui-ai/protocol": "0.1.0-rc.1"
32
+ "@ggui-ai/protocol": "0.2.0-alpha.1"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^22.0.0",
36
36
  "@types/ws": "^8.5.10",
37
37
  "typescript": "^5.0.0",
38
38
  "vitest": "^3.0.0",
39
- "@ggui-ai/protocol-conformance": "0.1.0-rc.1"
39
+ "@ggui-ai/protocol-conformance": "0.2.0-alpha.1"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",
@@ -1 +0,0 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnD,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnD,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IACtC;;;;;;;;;;;;;;OAcG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8B;IACvD,OAAO,CAAC,WAAW,CAAqB;IAExC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAkBjD;;;;;;;OAOG;IACH,oBAAoB,IAAI,MAAM,GAAG,SAAS;IAI1C,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAI3C,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIjC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO;IAMjE,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;IAMjE,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,IAAI;IAKnE;;;;;;;OAOG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,IAAI;IAKnE;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAK5D;;;;;;;;;;;;;;;;;;OAkBG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO;CAcxD"}