@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 +1 -1
- package/dist/action-router.d.ts +17 -10
- package/dist/action-router.d.ts.map +1 -1
- package/dist/action-router.js +59 -34
- package/dist/conformance-host.d.ts +11 -8
- package/dist/conformance-host.d.ts.map +1 -1
- package/dist/conformance-host.js +35 -40
- package/dist/{session.d.ts → render.d.ts} +40 -33
- package/dist/render.d.ts.map +1 -0
- package/dist/{session.js → render.js} +58 -51
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +46 -28
- package/dist/tool-registry.d.ts +2 -2
- package/dist/tool-registry.js +2 -2
- package/package.json +3 -3
- package/dist/session.d.ts.map +0 -1
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
|
|
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}`.
|
package/dist/action-router.d.ts
CHANGED
|
@@ -1,34 +1,41 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
|
30
|
+
export declare function parseActionFrame(frame: unknown): IncomingActionFrame | undefined;
|
|
24
31
|
export interface DispatchContext {
|
|
25
|
-
readonly
|
|
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
|
|
31
|
-
* `
|
|
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":"
|
|
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"}
|
package/dist/action-router.js
CHANGED
|
@@ -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
|
-
*
|
|
32
|
-
*
|
|
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
|
|
46
|
+
export function parseActionFrame(frame) {
|
|
37
47
|
if (frame === null || typeof frame !== 'object')
|
|
38
|
-
return
|
|
48
|
+
return undefined;
|
|
39
49
|
const f = frame;
|
|
40
50
|
if (f['type'] !== 'action')
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
61
|
+
return undefined;
|
|
47
62
|
const a = action;
|
|
48
|
-
|
|
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
|
|
53
|
-
* `
|
|
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 {
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
198
|
-
for (const spec of
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
274
|
-
for (const subscriber of
|
|
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-
|
|
9
|
+
* - create-render → `renders.create()`
|
|
10
10
|
* - register-tool → `tools.register(name, handler)`
|
|
11
|
-
* - register-actionspec → `
|
|
12
|
-
* - register-streamspec → `
|
|
13
|
-
* - server-version-override → `
|
|
14
|
-
* - emit-envelope → `
|
|
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-
|
|
40
|
-
* so 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
|
|
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"}
|
package/dist/conformance-host.js
CHANGED
|
@@ -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-
|
|
8
|
-
* so 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-
|
|
17
|
+
if (step.kind === 'create-render') {
|
|
18
18
|
const s = step;
|
|
19
|
-
serverInstance.
|
|
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
|
|
38
|
+
// register-actionspec doesn't carry a renderId in the
|
|
39
39
|
// directive shape — it's scoped to the most-recently-created
|
|
40
|
-
//
|
|
41
|
-
// create-
|
|
42
|
-
// land in order on the same
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
throw new Error('reference-server: register-actionspec invoked before create-
|
|
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.
|
|
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
|
|
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
|
|
70
|
-
if (
|
|
71
|
-
throw new Error('reference-server: register-streamspec invoked before create-
|
|
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.
|
|
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
|
-
//
|
|
82
|
-
//
|
|
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
|
|
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
|
|
102
|
-
if (
|
|
103
|
-
throw new Error('reference-server: emit-envelope invoked before create-
|
|
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.
|
|
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-
|
|
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
|
|
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
|
|
144
|
+
// Same most-recently-created render scope as register-
|
|
145
145
|
// actionspec / register-streamspec — the fixture authoring
|
|
146
|
-
// convention is `create-
|
|
146
|
+
// convention is `create-render` immediately precedes this
|
|
147
147
|
// directive, and the kit's narrowSetupStep doesn't surface a
|
|
148
|
-
//
|
|
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 `
|
|
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
|
-
// `
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
throw new Error('reference-server: server-version-override invoked before create-
|
|
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.
|
|
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
|
|
2
|
+
* In-memory render store for the reference server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
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
|
|
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
|
|
46
|
-
readonly
|
|
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-
|
|
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-
|
|
68
|
+
* Why per-render, not per-instance: parallel kit fixtures share
|
|
62
69
|
* one `ReferenceServer`. Mutating the instance-level override would
|
|
63
|
-
* leak across
|
|
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
|
|
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
|
|
83
|
-
private readonly
|
|
89
|
+
export declare class RenderStore {
|
|
90
|
+
private readonly renders;
|
|
84
91
|
private lastCreated;
|
|
85
|
-
create(
|
|
92
|
+
create(renderId: string, appId: string): Render;
|
|
86
93
|
/**
|
|
87
|
-
* 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
|
|
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-
|
|
99
|
+
* create-render always precedes register-actionspec.
|
|
93
100
|
*/
|
|
94
|
-
|
|
95
|
-
get(
|
|
96
|
-
close(
|
|
97
|
-
addSubscriber(
|
|
98
|
-
removeSubscriber(
|
|
99
|
-
registerActionSpec(
|
|
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
|
-
*
|
|
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(
|
|
115
|
+
registerStreamSpec(renderId: string, entry: StreamSpecEntry): void;
|
|
109
116
|
/**
|
|
110
|
-
* Set the per-
|
|
117
|
+
* Set the per-render protocol-version override. Used by the
|
|
111
118
|
* `server-version-override` ConformanceHost directive — populates
|
|
112
|
-
* {@link
|
|
119
|
+
* {@link Render.versionOverride} so the WS subscribe handler
|
|
113
120
|
* advertises this value (and emits UPGRADE_REQUIRED keyed off it)
|
|
114
|
-
* for THIS
|
|
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(
|
|
127
|
+
setVersionOverride(renderId: string, version: string): void;
|
|
121
128
|
/**
|
|
122
|
-
* Fan out a frame to every subscriber on the named
|
|
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
|
|
129
|
-
* received the frame; `false` if the
|
|
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(
|
|
147
|
+
injectFrame(renderId: string, frame: unknown): boolean;
|
|
141
148
|
}
|
|
142
|
-
//# sourceMappingURL=
|
|
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
|
|
2
|
+
* In-memory render store for the reference server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
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
|
|
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
|
|
20
|
-
|
|
26
|
+
export class RenderStore {
|
|
27
|
+
renders = new Map();
|
|
21
28
|
lastCreated;
|
|
22
|
-
create(
|
|
23
|
-
const existing = this.
|
|
29
|
+
create(renderId, appId) {
|
|
30
|
+
const existing = this.renders.get(renderId);
|
|
24
31
|
if (existing !== undefined) {
|
|
25
|
-
this.lastCreated =
|
|
32
|
+
this.lastCreated = renderId;
|
|
26
33
|
return existing;
|
|
27
34
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
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.
|
|
36
|
-
this.lastCreated =
|
|
37
|
-
return
|
|
42
|
+
this.renders.set(renderId, render);
|
|
43
|
+
this.lastCreated = renderId;
|
|
44
|
+
return render;
|
|
38
45
|
}
|
|
39
46
|
/**
|
|
40
|
-
* 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
|
|
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-
|
|
52
|
+
* create-render always precedes register-actionspec.
|
|
46
53
|
*/
|
|
47
|
-
|
|
54
|
+
lastCreatedRenderId() {
|
|
48
55
|
return this.lastCreated;
|
|
49
56
|
}
|
|
50
|
-
get(
|
|
51
|
-
return this.
|
|
57
|
+
get(renderId) {
|
|
58
|
+
return this.renders.get(renderId);
|
|
52
59
|
}
|
|
53
|
-
close(
|
|
54
|
-
return this.
|
|
60
|
+
close(renderId) {
|
|
61
|
+
return this.renders.delete(renderId);
|
|
55
62
|
}
|
|
56
|
-
addSubscriber(
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
return
|
|
63
|
+
addSubscriber(renderId, subscriber) {
|
|
64
|
+
const render = this.create(renderId, 'conformance');
|
|
65
|
+
render.subscribers.add(subscriber);
|
|
66
|
+
return render;
|
|
60
67
|
}
|
|
61
|
-
removeSubscriber(
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
68
|
+
removeSubscriber(renderId, subscriber) {
|
|
69
|
+
const render = this.renders.get(renderId);
|
|
70
|
+
if (render === undefined)
|
|
64
71
|
return;
|
|
65
|
-
|
|
72
|
+
render.subscribers.delete(subscriber);
|
|
66
73
|
}
|
|
67
|
-
registerActionSpec(
|
|
68
|
-
const
|
|
69
|
-
|
|
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
|
-
*
|
|
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(
|
|
80
|
-
const
|
|
81
|
-
|
|
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-
|
|
91
|
+
* Set the per-render protocol-version override. Used by the
|
|
85
92
|
* `server-version-override` ConformanceHost directive — populates
|
|
86
|
-
* {@link
|
|
93
|
+
* {@link Render.versionOverride} so the WS subscribe handler
|
|
87
94
|
* advertises this value (and emits UPGRADE_REQUIRED keyed off it)
|
|
88
|
-
* for THIS
|
|
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(
|
|
95
|
-
const
|
|
96
|
-
|
|
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
|
|
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
|
|
106
|
-
* received the frame; `false` if the
|
|
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(
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
124
|
+
injectFrame(renderId, frame) {
|
|
125
|
+
const render = this.renders.get(renderId);
|
|
126
|
+
if (render === undefined)
|
|
120
127
|
return false;
|
|
121
|
-
if (
|
|
128
|
+
if (render.subscribers.size === 0)
|
|
122
129
|
return false;
|
|
123
|
-
for (const subscriber of
|
|
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 {
|
|
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
|
|
34
|
+
readonly renders: RenderStore;
|
|
35
35
|
readonly tools: ToolRegistry;
|
|
36
36
|
private readonly options;
|
|
37
37
|
private http;
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
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,
|
|
16
|
-
import {
|
|
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
|
-
|
|
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
|
|
100
|
-
let
|
|
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: (
|
|
116
|
-
// If previously subscribed to a different
|
|
122
|
+
onSubscribed: (renderId) => {
|
|
123
|
+
// If previously subscribed to a different render, unsub
|
|
117
124
|
// from it first.
|
|
118
|
-
if (
|
|
119
|
-
this.
|
|
125
|
+
if (subscribedRenderId !== null && subscribedRenderId !== renderId) {
|
|
126
|
+
this.renders.removeSubscriber(subscribedRenderId, subscriber);
|
|
120
127
|
}
|
|
121
|
-
|
|
128
|
+
subscribedRenderId = renderId;
|
|
122
129
|
},
|
|
123
130
|
});
|
|
124
131
|
});
|
|
125
132
|
socket.on('close', () => {
|
|
126
|
-
if (
|
|
127
|
-
this.
|
|
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'
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
return; //
|
|
157
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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-
|
|
186
|
-
// directive set a `versionOverride` on this
|
|
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
|
|
190
|
-
const
|
|
191
|
-
const advertised =
|
|
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.
|
|
231
|
+
this.renders.addSubscriber(renderId, ctx.subscriber);
|
|
214
232
|
// Preserve appId on first subscribe — create() is no-op if the
|
|
215
|
-
//
|
|
216
|
-
this.
|
|
217
|
-
ctx.onSubscribed(
|
|
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 },
|
package/dist/tool-registry.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
50
|
+
* render's tool namespace). No-persistence by design.
|
|
51
51
|
*/
|
|
52
52
|
export declare class ToolRegistry {
|
|
53
53
|
private readonly tools;
|
package/dist/tool-registry.js
CHANGED
|
@@ -71,9 +71,9 @@ export function buildHandler(kind) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
* In-memory tool registry. Scoped to a
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
39
|
+
"@ggui-ai/protocol-conformance": "0.2.0-alpha.1"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|
package/dist/session.d.ts.map
DELETED
|
@@ -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"}
|