@ggui-ai/protocol-reference-server 0.2.0-alpha.4 → 0.4.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 +22 -10
- package/dist/action-router.d.ts +28 -31
- package/dist/action-router.d.ts.map +1 -1
- package/dist/action-router.js +77 -278
- package/dist/conformance-host.d.ts +2 -32
- package/dist/conformance-host.d.ts.map +1 -1
- package/dist/conformance-host.js +190 -138
- package/dist/host-context.d.ts +70 -0
- package/dist/host-context.d.ts.map +1 -0
- package/dist/host-context.js +139 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/render.d.ts +145 -81
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +87 -68
- package/dist/server.d.ts +2 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +105 -51
- package/package.json +5 -5
- package/dist/tool-registry.d.ts +0 -63
- package/dist/tool-registry.d.ts.map +0 -1
- package/dist/tool-registry.js +0 -99
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
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
package/dist/action-router.d.ts
CHANGED
|
@@ -1,47 +1,44 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { ToolRegistry } from './tool-registry.js';
|
|
1
|
+
import { type GguiSession, type Subscriber } from './render.js';
|
|
3
2
|
/**
|
|
4
|
-
* One inbound action
|
|
5
|
-
* `
|
|
6
|
-
* envelope
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
9
|
+
interface IncomingActionMessage {
|
|
12
10
|
readonly type: 'action';
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
readonly
|
|
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
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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):
|
|
31
|
-
export interface
|
|
32
|
-
readonly render:
|
|
33
|
-
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
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":"
|
|
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"}
|
package/dist/action-router.js
CHANGED
|
@@ -1,308 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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 {
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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
|
|
42
|
+
if (!isRecord(frame))
|
|
48
43
|
return undefined;
|
|
49
|
-
|
|
50
|
-
if (f['type'] !== 'action')
|
|
44
|
+
if (frame['type'] !== 'action')
|
|
51
45
|
return undefined;
|
|
52
|
-
const
|
|
53
|
-
|
|
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
|
|
60
|
-
if (
|
|
49
|
+
const sessionId = typeof envelope['sessionId'] === 'string' ? envelope['sessionId'] : undefined;
|
|
50
|
+
if (sessionId === undefined)
|
|
61
51
|
return undefined;
|
|
62
|
-
const
|
|
63
|
-
|
|
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
|
|
67
|
-
const channelValue = f['channel'];
|
|
55
|
+
const requestId = typeof frame['requestId'] === 'string' ? frame['requestId'] : undefined;
|
|
68
56
|
return {
|
|
69
57
|
type: 'action',
|
|
70
|
-
...(
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
95
|
+
return;
|
|
260
96
|
}
|
|
261
|
-
broadcast(render, {
|
|
262
|
-
type: 'stream',
|
|
263
|
-
payload: {
|
|
264
|
-
channel: spec.channel,
|
|
265
|
-
value,
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
97
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
291
|
-
type: '
|
|
292
|
-
payload: {
|
|
293
|
-
|
|
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-
|
|
43
|
-
* so 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":"
|
|
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"}
|