@ggui-ai/protocol-reference-server 0.2.0-alpha.3 → 0.3.0-rc.0

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,14 +12,24 @@ 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 render store (no persistence).
15
+ - In-memory render store with a consume-buffer event ledger (no persistence).
16
16
  - `schemaVersion` handshake with `UPGRADE_REQUIRED` on mismatch.
17
- - A 4-handler wired-action registry:
18
- - `echo` — returns `{received: args}`.
19
- - `throw` — rejects → `TOOL_THREW` emitted on `_ggui:contract-error`.
20
- - `timeout` — never resolves → `TOOL_TIMEOUT` after 500ms.
21
- - `malformed` — shape-mismatched return → `SCHEMA_VIOLATION`.
22
- - `TOOL_NOT_FOUND` when an unregistered action is invoked.
17
+ - Subscribe tenancy: the subscribe's `appId` must match the GguiSession's
18
+ bound `appId` or the subscribe is rejected with an `error` frame, code
19
+ `APP_MISMATCH` (an unknown session id still provisions on subscribe,
20
+ binding the subscribe's own `appId`).
21
+ - `host_context_observed` persistence: the client-observed
22
+ `HostContextProjection` is validated against the protocol shape and
23
+ persisted onto `GguiSession.hostContext` (idempotent overwrite, no
24
+ response frame); the conformance host reads it back via
25
+ `readSessionField('hostContext')`.
26
+ - The single action-routing model:
27
+ - a declared action appends to the GguiSession's consume buffer and the
28
+ ack carries `payload.sequence` (validate → append → ack);
29
+ - a `data:submit` action absent from the declared actionSpec is rejected
30
+ with an `error` frame, code `CONTRACT_VIOLATION` — nothing is appended.
31
+ - The agent-side drain (`ggui_consume`) is an MCP surface this WS-only
32
+ server does not implement — a declared kit grading gap.
23
33
 
24
34
  ## Non-scope
25
35
 
@@ -40,6 +50,8 @@ Prints `READY ws://127.0.0.1:3100/ws` when bound. Ctrl-C to stop.
40
50
 
41
51
  `src/conformance.test.ts` boots this server and runs `@ggui-ai/protocol-conformance`
42
52
  against it through a `ConformanceHost` adapter. Every drivable conformance fixture must
43
- pass (bootstrap-success, wired-action-success, wired-action-tool-threw,
44
- stream-schema-violation); directives outside this server's scope (renderer-url-override,
45
- ui-initialize-response-override, and similar) skip cleanly per the kit's design.
53
+ pass (bootstrap-success, action-ack-sequence, undeclared-action-rejected,
54
+ action-payload-schema-violation, version-match, version-mismatch, app-mismatch,
55
+ absent-appid-defaults, host-context-observed-persists); directives outside this server's scope
56
+ (renderer-url-override, ui-initialize-response-override, and similar) skip cleanly per
57
+ the kit's design.
@@ -1,47 +1,44 @@
1
- import type { Render } from './render.js';
2
- import type { ToolRegistry } from './tool-registry.js';
1
+ import { type GguiSession, type Subscriber } from './render.js';
3
2
  /**
4
- * One inbound action frame shape. Matches the fixtures' authored
5
- * `inputEnvelope` for `wired-action-*` cases — the runner sends the
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`).
3
+ * One inbound action message — the canonical live-channel shape:
4
+ * `{type: 'action', payload: ActionEnvelope, requestId?}` where the
5
+ * envelope carries `{sessionId, type, payload?}` and, for
6
+ * `type: 'data:submit'`, `payload` is the ActionEventValue
7
+ * (`{action, data?, tool?}`).
10
8
  */
11
- interface IncomingActionFrame {
9
+ interface IncomingActionMessage {
12
10
  readonly type: 'action';
13
- readonly channel?: number;
14
- readonly renderId: string;
15
- readonly action: {
16
- readonly name: string;
17
- readonly data?: unknown;
11
+ readonly requestId?: string;
12
+ readonly payload: {
13
+ readonly sessionId: string;
14
+ /** Event type, e.g. `'data:submit'`. */
15
+ readonly type: string;
16
+ /** ActionEventValue for `data:submit`; free-form otherwise. */
17
+ readonly payload?: unknown;
18
18
  };
19
19
  }
20
20
  /**
21
- * Parse + validate an inbound action frame. Returns the normalized
21
+ * Parse + validate an inbound action message. Returns the normalized
22
22
  * shape on success, `undefined` on any malformed input (matcher for
23
23
  * `no-op` fixtures expects silence, so loud rejection would break
24
24
  * them).
25
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`.
26
+ * Reads the canonical SPEC session-identity field `sessionId` from
27
+ * the envelope body.
29
28
  */
30
- export declare function parseActionFrame(frame: unknown): IncomingActionFrame | undefined;
31
- export interface DispatchContext {
32
- readonly render: Render;
33
- readonly tools: ToolRegistry;
29
+ export declare function parseActionFrame(frame: unknown): IncomingActionMessage | undefined;
30
+ export interface HandleActionContext {
31
+ readonly render: GguiSession;
32
+ /** Reply handle for the SENDING socket — acks and contract
33
+ * rejections go to the dispatcher, not the broadcast set. */
34
+ readonly reply: Subscriber;
34
35
  }
35
36
  /**
36
- * Dispatch one action frame. Runs asynchronously; contract-error
37
- * emissions are broadcast to every subscriber on the render via
38
- * `render.subscribers`.
39
- *
40
- * Returns when the dispatch's observable outcome has been emitted
41
- * (either a happy-path observability frame or a contract-error).
42
- * The awaiting call site is the WS message handler — letting it
43
- * await ensures the kit's observation window captures the emission.
37
+ * Handle one parsed action message: enforce the declared-action
38
+ * contract, append, ack. Synchronous — the ledger is in-memory, so
39
+ * the ack ordering is deterministic relative to the inbound message
40
+ * stream.
44
41
  */
45
- export declare function dispatchAction(frame: IncomingActionFrame, context: DispatchContext): Promise<void>;
42
+ export declare function handleAction(message: IncomingActionMessage, context: HandleActionContext): void;
46
43
  export {};
47
44
  //# sourceMappingURL=action-router.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"action-router.d.ts","sourceRoot":"","sources":["../src/action-router.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAe,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAE7E;;;;;;GAMG;AACH,UAAU,qBAAqB;IAC7B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,wCAAwC;QACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,+DAA+D;QAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;KAC5B,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,GAAG,SAAS,CAmBlF;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B;kEAC8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,mBAAmB,GAC3B,IAAI,CAuCN"}
@@ -1,308 +1,107 @@
1
1
  /**
2
- * Wired-action dispatcher — consumes an inbound `action` frame and
3
- * emits the matching `_ggui:contract-error` stream frame (plus happy-
4
- * path effects) per SPEC §4.4 + Contract #3.
2
+ * Inbound `action` frame handling — parse the canonical live-channel
3
+ * action message, enforce the declared actionSpec contract, append the
4
+ * envelope to the GguiSession's consume-buffer ledger, and ack with
5
+ * the assigned sequence.
5
6
  *
6
- * This file owns every ContractErrorCode emission path the
7
- * conformance kit's matcher asserts on:
7
+ * Ordering contract (mirrors the first-party server): validate →
8
+ * append → ack. The ack's `payload.sequence` is the wire-observable
9
+ * proof the action event persisted to the consume buffer — the kit's
10
+ * `action-ack-sequence` fixture grades exactly this. The retrieval
11
+ * half (the agent draining the buffer via `ggui_consume`) is an MCP
12
+ * tool call outside this WS-only server's scope.
8
13
  *
9
- * - TOOL_NOT_FOUND — dispatched action's tool is not registered
10
- * - TOOL_THREW — handler throws
11
- * - TOOL_TIMEOUT — handler exceeds `TIMEOUT_MS` (500ms)
12
- * - SCHEMA_VIOLATION — handler returns shape not matching declared
13
- * channel schema (currently: `malformed` kind
14
- * always trips this path)
15
- *
16
- * Happy-path: echo handler's success return fires an observability
17
- * signal (kit's `wired-action-success` fixture asserts the router
18
- * dispatch observably happened). Since observability events are
19
- * unmatchable-on-ws per the conformance kit's matcher, the
20
- * happy-path emission here is best-effort: we emit a non-reserved
21
- * stream frame on `_ggui:wired-tool-invoked` carrying
22
- * `{toolName, actionName}` so an iframe-host kit can match the same
23
- * tool invocation signal from either side. Pure WS kit currently
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.
14
+ * Contract enforcement: for `type: 'data:submit'` envelopes against a
15
+ * session WITH a declared actionSpec, the ActionEventValue's `action`
16
+ * MUST name a declared entry, and — when that entry declares a
17
+ * `schema` — its `data` MUST conform to it (SPEC §4.6 receipt
18
+ * validation). Both halves are graded by the protocol's own
19
+ * `validateActionData`, the same validator the first-party server's
20
+ * `assertActionContract` enforcement runs, so the two
21
+ * implementations can never drift on what counts as a violation.
22
+ * Violations reply an `error` frame with code `CONTRACT_VIOLATION`
23
+ * (echoing the message's `requestId`, carrying the structured
24
+ * violation list under `payload.details`) and the envelope is NOT
25
+ * appended — rejected actions never reach the buffer. Sessions
26
+ * without a declared actionSpec accept every action: no contract,
27
+ * nothing to enforce.
31
28
  */
32
- import { makeContractErrorPayload } from '@ggui-ai/protocol';
33
- /** Timeout the `timeout` handler exceeds; the kit's matcher
34
- * recognizes TOOL_TIMEOUT within this window. */
35
- const TIMEOUT_MS = 500;
29
+ import { ContractViolationError, validateActionData } from '@ggui-ai/protocol';
30
+ import { isRecord } from '@ggui-ai/protocol';
31
+ import { appendEvent } from './render.js';
36
32
  /**
37
- * Parse + validate an inbound action frame. Returns the normalized
33
+ * Parse + validate an inbound action message. Returns the normalized
38
34
  * shape on success, `undefined` on any malformed input (matcher for
39
35
  * `no-op` fixtures expects silence, so loud rejection would break
40
36
  * them).
41
37
  *
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`.
38
+ * Reads the canonical SPEC session-identity field `sessionId` from
39
+ * the envelope body.
45
40
  */
46
41
  export function parseActionFrame(frame) {
47
- if (frame === null || typeof frame !== 'object')
42
+ if (!isRecord(frame))
48
43
  return undefined;
49
- const f = frame;
50
- if (f['type'] !== 'action')
44
+ if (frame['type'] !== 'action')
51
45
  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)
46
+ const envelope = frame['payload'];
47
+ if (!isRecord(envelope))
58
48
  return undefined;
59
- const action = f['action'];
60
- if (action === null || typeof action !== 'object')
49
+ const sessionId = typeof envelope['sessionId'] === 'string' ? envelope['sessionId'] : undefined;
50
+ if (sessionId === undefined)
61
51
  return undefined;
62
- const a = action;
63
- const name = a['name'];
64
- if (typeof name !== 'string')
52
+ const eventType = typeof envelope['type'] === 'string' ? envelope['type'] : undefined;
53
+ if (eventType === undefined)
65
54
  return undefined;
66
- const data = 'data' in a ? a['data'] : undefined;
67
- const channelValue = f['channel'];
55
+ const requestId = typeof frame['requestId'] === 'string' ? frame['requestId'] : undefined;
68
56
  return {
69
57
  type: 'action',
70
- ...(typeof channelValue === 'number' ? { channel: channelValue } : {}),
71
- renderId,
72
- action: { name, ...(data !== undefined ? { data } : {}) },
73
- };
74
- }
75
- /**
76
- * Dispatch one action frame. Runs asynchronously; contract-error
77
- * emissions are broadcast to every subscriber on the render via
78
- * `render.subscribers`.
79
- *
80
- * Returns when the dispatch's observable outcome has been emitted
81
- * (either a happy-path observability frame or a contract-error).
82
- * The awaiting call site is the WS message handler — letting it
83
- * await ensures the kit's observation window captures the emission.
84
- */
85
- export async function dispatchAction(frame, context) {
86
- const { render, tools } = context;
87
- const actionName = frame.action.name;
88
- // Action → tool resolution, in priority order:
89
- // 1. Explicit `register-actionspec` directive on this render.
90
- // 2. Tool whose name equals the action name (1:1 convention).
91
- //
92
- // A real ggui server uses a blueprint's declared `actionSpec` to
93
- // bind action names to tool names; the reference server honors the
94
- // same binding via the fixture's explicit `register-actionspec`
95
- // setup directive. If neither path resolves, emit TOOL_NOT_FOUND
96
- // with `toolName = actionName` — the error payload names the
97
- // missing tool by the identifier the dispatcher was asked to
98
- // resolve, which is the action name itself.
99
- const actionSpec = render.actionSpecs.get(actionName);
100
- let toolName = actionSpec?.tool;
101
- if (toolName === undefined && tools.has(actionName)) {
102
- toolName = actionName;
103
- }
104
- if (toolName === undefined) {
105
- emitContractError(render, {
106
- code: 'TOOL_NOT_FOUND',
107
- toolName: actionName,
108
- actionName,
109
- message: `no actionSpec bound action '${actionName}' and no tool of that name is registered`,
110
- });
111
- return;
112
- }
113
- const tool = tools.get(toolName);
114
- if (tool === undefined) {
115
- emitContractError(render, {
116
- code: 'TOOL_NOT_FOUND',
117
- toolName,
118
- actionName,
119
- message: `tool '${toolName}' is not registered`,
120
- });
121
- return;
122
- }
123
- // Malformed handler is defined to return the wrong shape — emit
124
- // SCHEMA_VIOLATION instead of passing the bad return through.
125
- if (tool.kind === 'malformed') {
126
- emitContractError(render, {
127
- code: 'SCHEMA_VIOLATION',
128
- toolName: tool.name,
129
- actionName,
130
- message: `tool '${tool.name}' returned a shape that does not match the declared channel schema`,
131
- });
132
- return;
133
- }
134
- // Run the handler with a timeout bound. `timeout` kind wins the
135
- // timeout race deliberately; other handlers should resolve or
136
- // throw well before 500ms.
137
- let handlerOutcome;
138
- try {
139
- handlerOutcome = await Promise.race([
140
- (async () => {
141
- try {
142
- const value = await tool.handler(frame.action.data);
143
- return { kind: 'resolved', value };
144
- }
145
- catch (err) {
146
- return {
147
- kind: 'rejected',
148
- error: err instanceof Error ? err : new Error(String(err)),
149
- };
150
- }
151
- })(),
152
- new Promise((done) => {
153
- setTimeout(() => done({ kind: 'timeout' }), TIMEOUT_MS);
154
- }),
155
- ]);
156
- }
157
- catch (err) {
158
- // Promise.race never rejects since we catch inside; defensive.
159
- emitContractError(render, {
160
- code: 'TOOL_THREW',
161
- toolName: tool.name,
162
- actionName,
163
- message: err.message ?? String(err),
164
- causedBy: err.stack?.split('\n').slice(0, 3).join('\n'),
165
- });
166
- return;
167
- }
168
- if (handlerOutcome.kind === 'timeout') {
169
- emitContractError(render, {
170
- code: 'TOOL_TIMEOUT',
171
- toolName: tool.name,
172
- actionName,
173
- message: `tool '${tool.name}' exceeded ${TIMEOUT_MS}ms`,
174
- });
175
- return;
176
- }
177
- if (handlerOutcome.kind === 'rejected') {
178
- emitContractError(render, {
179
- code: 'TOOL_THREW',
180
- toolName: tool.name,
181
- actionName,
182
- message: handlerOutcome.error.message,
183
- causedBy: handlerOutcome.error.stack?.split('\n').slice(0, 3).join('\n'),
184
- });
185
- return;
186
- }
187
- // Happy path — emit the observability signal so iframe-host kits
188
- // can observe it. WS-only kit's matcher will return
189
- // `unmatchable-on-ws` for `observability-event`, which the runner
190
- // maps to SKIP (not FAIL). That's the intended behavior per the
191
- // kit's design note at the top of `match-behavior.ts`.
192
- broadcast(render, {
193
- type: 'stream',
58
+ ...(requestId !== undefined ? { requestId } : {}),
194
59
  payload: {
195
- channel: '_ggui:wired-tool-invoked',
196
- value: { toolName: tool.name, actionName },
60
+ sessionId,
61
+ type: eventType,
62
+ ...('payload' in envelope ? { payload: envelope['payload'] } : {}),
197
63
  },
198
- });
199
- // Refresh-stream fan-out (SPEC §2.3 StreamSpec refresh triggers).
200
- // After a successful wired-action dispatch, every streamSpec
201
- // declared on the render fires its refresh tool and emits a
202
- // stream-update on the bound channel. This is the wire-level
203
- // proof of the refresh-after-action contract the kit's
204
- // `stream-refresh-success` fixture asserts.
205
- //
206
- // Refresh-tool failures are emitted as contract-errors with
207
- // `sourceAction: 'refresh-stream'` so the kit's matcher can
208
- // distinguish action-path failures from refresh-path failures
209
- // (and so `stream-schema-violation` has a path forward when its
210
- // fixture flips off the known-failures list).
211
- await dispatchRefreshStreams(render, tools);
64
+ };
212
65
  }
213
66
  /**
214
- * Run every streamSpec's refresh tool and broadcast the result on
215
- * its bound channel. Errors map to canonical `_ggui:contract-error`
216
- * envelopes with `sourceAction: 'refresh-stream'`. Sequential
217
- * dispatch keeps subscriber-frame ordering deterministic — the
218
- * kit's matcher doesn't depend on order today, but a real ggui
219
- * runtime emits in declaration order and the reference server
220
- * preserves that contract.
67
+ * Handle one parsed action message: enforce the declared-action
68
+ * contract, append, ack. Synchronous — the ledger is in-memory, so
69
+ * the ack ordering is deterministic relative to the inbound message
70
+ * stream.
221
71
  */
222
- async function dispatchRefreshStreams(render, tools) {
223
- for (const spec of render.streamSpecs.values()) {
224
- const refreshTool = tools.get(spec.tool);
225
- if (refreshTool === undefined) {
226
- emitContractError(render, {
227
- code: 'TOOL_NOT_FOUND',
228
- toolName: spec.tool,
229
- actionName: spec.channel,
230
- message: `streamSpec for channel '${spec.channel}' references tool '${spec.tool}', which is not registered`,
231
- sourceActionType: 'refresh-stream',
232
- });
233
- continue;
234
- }
235
- if (refreshTool.kind === 'malformed' || refreshTool.kind === 'malformed-stream') {
236
- emitContractError(render, {
237
- code: 'SCHEMA_VIOLATION',
238
- toolName: refreshTool.name,
239
- actionName: spec.channel,
240
- message: `refresh tool '${refreshTool.name}' returned a shape that does not match channel '${spec.channel}'`,
241
- sourceActionType: 'refresh-stream',
72
+ export function handleAction(message, context) {
73
+ const { render, reply } = context;
74
+ const requestIdProps = message.requestId !== undefined ? { requestId: message.requestId } : {};
75
+ if (message.payload.type === 'data:submit' && render.actionSpec !== undefined) {
76
+ const result = validateActionData(message.payload.payload, render.actionSpec);
77
+ if (!result.valid) {
78
+ // Same error construction as the first-party server's
79
+ // `assertActionContract` path: `ContractViolationError` formats
80
+ // the violation list into `message` and `toErrorData()` is the
81
+ // structured `details` payload its `sendError` attaches.
82
+ const violation = new ContractViolationError({
83
+ tool: 'ggui_event',
84
+ violations: result.violations,
242
85
  });
243
- continue;
244
- }
245
- let value;
246
- try {
247
- value = await refreshTool.handler(undefined);
248
- }
249
- catch (err) {
250
- const error = err instanceof Error ? err : new Error(String(err));
251
- emitContractError(render, {
252
- code: 'TOOL_THREW',
253
- toolName: refreshTool.name,
254
- actionName: spec.channel,
255
- message: error.message,
256
- causedBy: error.stack?.split('\n').slice(0, 3).join('\n'),
257
- sourceActionType: 'refresh-stream',
86
+ reply.send({
87
+ type: 'error',
88
+ payload: {
89
+ code: 'CONTRACT_VIOLATION',
90
+ message: violation.message,
91
+ details: violation.toErrorData(),
92
+ },
93
+ ...requestIdProps,
258
94
  });
259
- continue;
95
+ return;
260
96
  }
261
- broadcast(render, {
262
- type: 'stream',
263
- payload: {
264
- channel: spec.channel,
265
- value,
266
- },
267
- });
268
97
  }
269
- }
270
- function emitContractError(render, input) {
271
- // Canonical SPEC §4.4 `ContractErrorPayload` via the central
272
- // builder. Nested `error: {code, message, causedBy}` alongside
273
- // flat `toolName` / `actionName` / `sourceAction` / `timestamp`.
274
- // The conformance kit's matcher reads this canonical shape
275
- // directly — no dual-shape workaround needed.
276
- const value = makeContractErrorPayload({
277
- toolName: input.toolName,
278
- actionName: input.actionName,
279
- sourceAction: {
280
- type: input.sourceActionType ?? 'wired-action',
281
- dispatchedAt: new Date().toISOString(),
282
- },
283
- error: {
284
- code: input.code,
285
- message: input.message,
286
- ...(input.causedBy !== undefined ? { causedBy: input.causedBy } : {}),
287
- },
288
- timestamp: new Date().toISOString(),
98
+ const sequence = appendEvent(render, {
99
+ type: 'user.submitted',
100
+ data: message.payload,
289
101
  });
290
- broadcast(render, {
291
- type: 'stream',
292
- payload: {
293
- channel: '_ggui:contract-error',
294
- value,
295
- },
102
+ reply.send({
103
+ type: 'ack',
104
+ payload: { sequence, timestamp: Date.now() },
105
+ ...requestIdProps,
296
106
  });
297
107
  }
298
- function broadcast(render, frame) {
299
- for (const subscriber of render.subscribers) {
300
- try {
301
- subscriber.send(frame);
302
- }
303
- catch {
304
- // Subscriber lifecycle issues (closed socket, etc.) are the
305
- // subscriber's problem — the router keeps broadcasting.
306
- }
307
- }
308
- }
@@ -1,33 +1,3 @@
1
- /**
2
- * `ConformanceHost` adapter — wires the `@ggui-ai/protocol-conformance`
3
- * setup/teardown directive dispatcher onto this package's
4
- * `ReferenceServer` instance.
5
- *
6
- * Directives split into "implement" and "throw":
7
- *
8
- * Implement:
9
- * - create-render → `renders.create()`
10
- * - register-tool → `tools.register(name, handler)`
11
- * - register-actionspec → `renders.registerActionSpec()`
12
- * - register-streamspec → `renders.registerStreamSpec()`
13
- * - server-version-override → `renders.setVersionOverride()`
14
- * - emit-envelope → `renders.injectFrame()`
15
- * - unregister-tool → `tools.unregister()`
16
- *
17
- * Throw (kit records SKIP, not FAIL):
18
- * - seed-channel — unimplemented
19
- * - renderer-url-override — unimplemented (browser-level)
20
- * - ui-initialize-response-override — unimplemented
21
- *
22
- * The "throw" set matches the conformance kit's `unmatchable-on-ws`
23
- * skip expectations — browser-level fault injection that requires a
24
- * richer host harness. Throwing surfaces "directive not implemented"
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.
30
- */
31
1
  import type { ConformanceHost } from '@ggui-ai/protocol-conformance';
32
2
  import type { ReferenceServer } from './server.js';
33
3
  export interface CreateReferenceConformanceHostInput {
@@ -39,8 +9,8 @@ export interface CreateReferenceConformanceHostInput {
39
9
  * drive the kit against the server.
40
10
  *
41
11
  * The server MUST be `start()`-ed before the first dispatch — the
42
- * kit calls `create-render` via `dispatchSetup` before any subscribe,
43
- * so the render store must be reachable. The caller owns the
12
+ * kit calls `create-session` via `dispatchSetup` before any subscribe,
13
+ * so the GguiSession store must be reachable. The caller owns the
44
14
  * server lifecycle (`start()` + `stop()`).
45
15
  */
46
16
  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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
1
+ {"version":3,"file":"conformance-host.d.ts","sourceRoot":"","sources":["../src/conformance-host.ts"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,+BAA+B,CAAC;AAGvC,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,CA0IvD"}