stitchkit 0.55.0 → 0.56.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.
Files changed (78) hide show
  1. package/dist/browser/socket-io.d.ts +14 -6
  2. package/dist/browser/socket-io.d.ts.map +1 -1
  3. package/dist/cli.js +4 -4
  4. package/dist/contract/errors.d.ts +7 -0
  5. package/dist/contract/errors.d.ts.map +1 -1
  6. package/dist/contract/index.js +1 -1
  7. package/dist/files/boundary.d.ts +8 -5
  8. package/dist/files/boundary.d.ts.map +1 -1
  9. package/dist/files.js +1 -1
  10. package/dist/{index-rgd18hx3.js → index-28cqssm3.js} +7 -0
  11. package/dist/index-557by2db.js +34 -0
  12. package/dist/index-5r13htq1.js +283 -0
  13. package/dist/index-6y759j86.js +1031 -0
  14. package/dist/{index-npye5kt7.js → index-75njxz7p.js} +69 -7
  15. package/dist/{index-1k16zv57.js → index-bfcpjw20.js} +125 -31
  16. package/dist/{index-0gv7k0ra.js → index-escqg10p.js} +73 -666
  17. package/dist/{index-v4bbq3p4.js → index-j3dem06f.js} +1 -1
  18. package/dist/{index-qx84bqzk.js → index-jcc611vh.js} +1 -1
  19. package/dist/{index-pzc32v75.js → index-sm2tjx06.js} +41 -4
  20. package/dist/{index-js7yexng.js → index-xy8fmh6w.js} +83 -4
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +27 -6
  24. package/dist/node.js +5 -4
  25. package/dist/observability/audit.d.ts +4 -0
  26. package/dist/observability/audit.d.ts.map +1 -1
  27. package/dist/observability/event.d.ts +5 -0
  28. package/dist/observability/event.d.ts.map +1 -1
  29. package/dist/observability/index.js +17 -11
  30. package/dist/remote.js +1 -1
  31. package/dist/server/context-contribution.d.ts +8 -0
  32. package/dist/server/context-contribution.d.ts.map +1 -1
  33. package/dist/server/create.d.ts.map +1 -1
  34. package/dist/server/error-hook.d.ts +1 -1
  35. package/dist/server/error-hook.d.ts.map +1 -1
  36. package/dist/server/index.d.ts +1 -1
  37. package/dist/server/index.d.ts.map +1 -1
  38. package/dist/server/index.js +10 -6
  39. package/dist/server/logger.d.ts.map +1 -1
  40. package/dist/server/middleware/auth.d.ts +29 -2
  41. package/dist/server/middleware/auth.d.ts.map +1 -1
  42. package/dist/server/request-body.d.ts.map +1 -1
  43. package/dist/testing/surface-conformance.d.ts +143 -10
  44. package/dist/testing/surface-conformance.d.ts.map +1 -1
  45. package/dist/testing/surface-manifest.d.ts +132 -33
  46. package/dist/testing/surface-manifest.d.ts.map +1 -1
  47. package/dist/testing.d.ts +2 -2
  48. package/dist/testing.d.ts.map +1 -1
  49. package/dist/testing.js +470 -122
  50. package/dist/tools/async-operation.d.ts +210 -18
  51. package/dist/tools/async-operation.d.ts.map +1 -1
  52. package/dist/tools/define-download-tool.d.ts.map +1 -1
  53. package/dist/tools/define-upload-tool.d.ts.map +1 -1
  54. package/dist/tools/internal/surface-projector.d.ts +101 -0
  55. package/dist/tools/internal/surface-projector.d.ts.map +1 -0
  56. package/dist/tools/managed-file-error.d.ts +6 -0
  57. package/dist/tools/managed-file-error.d.ts.map +1 -0
  58. package/dist/tools/mcp-prepare.d.ts +3 -21
  59. package/dist/tools/mcp-prepare.d.ts.map +1 -1
  60. package/dist/tools/mcp-round-policy.d.ts +9 -0
  61. package/dist/tools/mcp-round-policy.d.ts.map +1 -0
  62. package/dist/tools/mcp-round.d.ts +0 -4
  63. package/dist/tools/mcp-round.d.ts.map +1 -1
  64. package/dist/tools/mount-download.d.ts.map +1 -1
  65. package/dist/tools/mount-upload.d.ts.map +1 -1
  66. package/dist/tools/mount.d.ts +5 -9
  67. package/dist/tools/mount.d.ts.map +1 -1
  68. package/dist/tools/runtime-tool.d.ts.map +1 -1
  69. package/dist/tools/surface.d.ts +7 -0
  70. package/dist/tools/surface.d.ts.map +1 -1
  71. package/dist/tools/view-file.d.ts.map +1 -1
  72. package/dist/tools.d.ts +1 -1
  73. package/dist/tools.d.ts.map +1 -1
  74. package/dist/tools.js +348 -383
  75. package/llms-full.txt +425 -66
  76. package/package.json +1 -1
  77. package/dist/index-h60df7rj.js +0 -110
  78. package/dist/index-k4dftwf7.js +0 -66
package/dist/testing.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import {
2
- assertToolName,
3
- assertUniqueToolName,
4
- toToolName
5
- } from "./index-k4dftwf7.js";
2
+ RealtimeRequestDisconnectedError,
3
+ RealtimeRequestInvalidAcknowledgementError,
4
+ RealtimeRequestTimeoutError
5
+ } from "./index-557by2db.js";
6
+ import {
7
+ mcpProjectionCandidate,
8
+ prepareProjectedMcpTools,
9
+ projectToolSurface
10
+ } from "./index-6y759j86.js";
6
11
  import {
7
12
  toJsonSchema
8
13
  } from "./index-cby4ar3v.js";
@@ -24,6 +29,7 @@ import { createHash } from "node:crypto";
24
29
  import { z } from "zod";
25
30
  var HttpMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
26
31
  var ToolTransportSchema = z.enum(["MCP", "AGENT", "CLI"]);
32
+ var SurfaceTransportSchema = z.enum(["HTTP", "MCP", "AGENT", "CLI", "REALTIME"]);
27
33
  var SurfaceSchemaDigestsSchema = z.object({
28
34
  params: z.string().nullable(),
29
35
  input: z.string().nullable(),
@@ -38,8 +44,19 @@ var SurfaceManifestOperationSchema = z.object({
38
44
  scope: z.string().nullable(),
39
45
  description: z.string(),
40
46
  schemas: SurfaceSchemaDigestsSchema,
41
- http: z.array(z.object({ method: HttpMethodSchema, path: z.string() })),
42
- tools: z.partialRecord(ToolTransportSchema, z.string())
47
+ http: z.array(z.object({ method: HttpMethodSchema, path: z.string() }))
48
+ });
49
+ var SurfaceManifestToolSchema = z.object({
50
+ kind: z.enum(["contract", "runtime"]),
51
+ service: z.string(),
52
+ action: z.string(),
53
+ name: z.string(),
54
+ input: z.string()
55
+ });
56
+ var SurfaceManifestToolSurfaceSchema = z.object({
57
+ transport: ToolTransportSchema,
58
+ surface: z.string().nullable(),
59
+ tools: z.array(SurfaceManifestToolSchema)
43
60
  });
44
61
  var SurfaceManifestCliCommandSchema = z.object({
45
62
  name: z.string(),
@@ -49,12 +66,26 @@ var SurfaceManifestCliCommandSchema = z.object({
49
66
  });
50
67
  var SurfaceManifestExtensionSchema = z.object({
51
68
  name: z.string(),
52
- transport: z.enum(["HTTP", "MCP", "AGENT", "CLI"])
69
+ transport: SurfaceTransportSchema
70
+ });
71
+ var SurfaceRealtimeSchemaPairSchema = z.object({
72
+ input: z.string(),
73
+ output: z.string()
74
+ });
75
+ var SurfaceManifestRealtimeEventSchema = z.object({
76
+ contract: z.string(),
77
+ direction: z.enum(["serverToClient", "clientToServer"]),
78
+ event: z.string(),
79
+ args: SurfaceRealtimeSchemaPairSchema,
80
+ acknowledgement: SurfaceRealtimeSchemaPairSchema.nullable()
53
81
  });
54
82
  var SurfaceManifestSchema = z.object({
55
- manifestVersion: z.literal(1),
83
+ manifestVersion: z.literal(2),
56
84
  digestVersion: z.literal(1),
57
85
  operations: z.array(SurfaceManifestOperationSchema),
86
+ toolSurfaces: z.array(SurfaceManifestToolSurfaceSchema),
87
+ realtimeContracts: z.array(z.string()),
88
+ realtime: z.array(SurfaceManifestRealtimeEventSchema),
58
89
  cliOnly: z.array(SurfaceManifestCliCommandSchema),
59
90
  extensions: z.array(SurfaceManifestExtensionSchema)
60
91
  });
@@ -100,6 +131,9 @@ function schemaDigest(schema, io) {
100
131
  return null;
101
132
  return digestValue(canonicalSchemaValue(toJsonSchema(schema, io, "any")));
102
133
  }
134
+ function presentationDigest(schema) {
135
+ return digestValue(canonicalSchemaValue(schema));
136
+ }
103
137
  function multipartDigest(value) {
104
138
  return value === undefined ? null : digestValue(value);
105
139
  }
@@ -110,114 +144,202 @@ function joinPath(...parts) {
110
144
  function operationKey(kind, service, action) {
111
145
  return `${kind}\x00${service}\x00${action}`;
112
146
  }
113
- function uniqueToolServices(config) {
114
- const services = new Set;
115
- for (const service of config.services ?? [])
116
- services.add(service);
117
- for (const group of config.groups ?? []) {
118
- for (const service of group.services)
119
- services.add(service);
120
- }
121
- return [...services];
147
+ function operationFrom(kind, source) {
148
+ return {
149
+ kind,
150
+ service: source.serviceName,
151
+ action: source.key,
152
+ method: source.method,
153
+ scope: source.scope ?? null,
154
+ description: source.desc,
155
+ schemas: {
156
+ params: schemaDigest(source.paramsSchema, "input"),
157
+ input: schemaDigest(source.inputSchema, "input"),
158
+ output: schemaDigest(source.outputSchema, "output"),
159
+ multipart: multipartDigest(source.multipart)
160
+ },
161
+ http: []
162
+ };
163
+ }
164
+ function operationFingerprint(source) {
165
+ const mcp = source.mcp ? {
166
+ inputRequired: source.mcp.inputRequired.map((request) => ({
167
+ key: request.key,
168
+ message: request.message,
169
+ schema: schemaDigest(request.schema, "input")
170
+ }))
171
+ } : null;
172
+ return serializeSurfaceValue({
173
+ method: source.method,
174
+ serviceName: source.serviceName,
175
+ key: source.key,
176
+ desc: source.desc,
177
+ scope: source.scope ?? null,
178
+ path: source.path ?? null,
179
+ expose: source.expose ? [...source.expose].sort(compareCodepoints) : null,
180
+ toolName: source.toolName ?? null,
181
+ annotations: source.annotations ?? null,
182
+ ui: source.ui ?? null,
183
+ mcp,
184
+ rawBody: source.rawBody ?? false,
185
+ rawResponse: source.rawResponse ?? false,
186
+ responseMeta: source.responseMeta ?? null,
187
+ maxJsonBodyBytes: source.maxJsonBodyBytes ?? null,
188
+ idempotent: source.idempotent ?? false,
189
+ contentType: source.contentType ?? null,
190
+ meta: source.meta ?? null,
191
+ schemas: {
192
+ params: schemaDigest(source.paramsSchema, "input"),
193
+ input: schemaDigest(source.inputSchema, "input"),
194
+ output: schemaDigest(source.outputSchema, "output"),
195
+ multipart: multipartDigest(source.multipart)
196
+ }
197
+ });
198
+ }
199
+ function sortTools(tools) {
200
+ return tools.sort((left, right) => compareCodepoints(left.name, right.name) || compareCodepoints(left.service, right.service) || compareCodepoints(left.action, right.action) || compareCodepoints(left.kind, right.kind));
122
201
  }
123
202
  function buildSurfaceManifest(config) {
124
203
  const operations = new Map;
125
- const addContract = (service, prefix) => {
204
+ const operationFingerprints = new Map;
205
+ const operationDefinitions = new Map;
206
+ const addOperation = (kind, source) => {
207
+ const candidate = operationFrom(kind, source);
208
+ const key = operationKey(kind, candidate.service, candidate.action);
209
+ const definition = source.definitionToken ?? source;
210
+ const existing = operations.get(key);
211
+ if (!existing) {
212
+ operations.set(key, candidate);
213
+ operationFingerprints.set(key, operationFingerprint(source));
214
+ operationDefinitions.set(key, definition);
215
+ return candidate;
216
+ }
217
+ if (operationDefinitions.get(key) !== definition || operationFingerprints.get(key) !== operationFingerprint(source)) {
218
+ throw new Error(`Conflicting ${kind} operation identity ${candidate.service}.${candidate.action}`);
219
+ }
220
+ return existing;
221
+ };
222
+ const addHttp = (service, prefix) => {
126
223
  for (const method of Object.values(service.methods)) {
127
- const key = operationKey("contract", service.name, method.key);
128
- const existing = operations.get(key);
129
- const http = !method.expose || method.expose.includes("HTTP") ? [{ method: method.method, path: joinPath(prefix, service.prefix, method.path) }] : [];
130
- if (existing) {
131
- existing.http.push(...http);
224
+ const operation = addOperation("contract", method);
225
+ if (method.expose && !method.expose.includes("HTTP"))
132
226
  continue;
133
- }
134
- operations.set(key, {
135
- kind: "contract",
136
- service: service.name,
137
- action: method.key,
227
+ const route = {
138
228
  method: method.method,
139
- scope: method.scope ?? service.scope ?? null,
140
- description: method.desc,
141
- schemas: {
142
- params: schemaDigest(method.paramsSchema, "input"),
143
- input: schemaDigest(method.inputSchema, "input"),
144
- output: schemaDigest(method.outputSchema, "output"),
145
- multipart: multipartDigest(method.multipart)
146
- },
147
- http,
148
- tools: {}
149
- });
229
+ path: joinPath(prefix, service.prefix, method.path)
230
+ };
231
+ if (!operation.http.some((entry) => entry.method === route.method && entry.path === route.path)) {
232
+ operation.http.push(route);
233
+ }
150
234
  }
151
235
  };
152
236
  for (const service of config.services ?? []) {
153
- addContract(service, config.scopePrefixes?.[service.scope] ?? "");
237
+ addHttp(service, config.scopePrefixes?.[service.scope] ?? "");
154
238
  }
155
239
  for (const group of config.groups ?? []) {
156
240
  for (const service of group.services)
157
- addContract(service, group.pathPrefix ?? "");
241
+ addHttp(service, group.pathPrefix ?? "");
158
242
  }
159
- for (const definition of config.runtimeTools ?? []) {
160
- const identity = definition.identity;
161
- const key = operationKey("runtime", identity.serviceName, identity.action);
162
- if (operations.has(key)) {
163
- throw new Error(`Duplicate runtime operation identity ${identity.serviceName}.${identity.action}`);
243
+ const toolSurfaces = [];
244
+ const addToolProjection = (transport, surface, projection, presentation = {}) => {
245
+ const projected = projectToolSurface(projection, transport, transport === "MCP" ? config.mcpPreparation ?? {} : presentation);
246
+ const collected = transport === "MCP" ? prepareProjectedMcpTools(projected.map(mcpProjectionCandidate), config.mcpPreparation ?? {}).map((prepared) => ({
247
+ ...prepared.tool,
248
+ presentationSchema: prepared.inputSchema
249
+ })) : projected;
250
+ const tools = [];
251
+ for (const entry of collected) {
252
+ const source = entry.kind === "contract" ? entry.source : {
253
+ definitionToken: entry.source,
254
+ method: entry.source.identity.method,
255
+ serviceName: entry.source.identity.serviceName,
256
+ key: entry.source.identity.action,
257
+ desc: entry.source.description,
258
+ ...entry.source.identity.scope !== undefined && {
259
+ scope: entry.source.identity.scope
260
+ },
261
+ inputSchema: entry.source.input,
262
+ ...entry.source.output !== undefined && {
263
+ outputSchema: entry.source.output
264
+ },
265
+ toolName: entry.source.name,
266
+ expose: entry.source.transports,
267
+ ...entry.source.annotations !== undefined && {
268
+ annotations: entry.source.annotations
269
+ },
270
+ ...entry.source.ui !== undefined && { ui: entry.source.ui },
271
+ ...entry.source.identity.meta !== undefined && {
272
+ meta: entry.source.identity.meta
273
+ },
274
+ ...entry.source.mcp !== undefined && { mcp: entry.source.mcp }
275
+ };
276
+ const operation = addOperation(entry.kind, source);
277
+ tools.push({
278
+ kind: entry.kind,
279
+ service: operation.service,
280
+ action: operation.action,
281
+ name: entry.name,
282
+ input: presentationDigest(entry.presentationSchema)
283
+ });
164
284
  }
165
- operations.set(key, {
166
- kind: "runtime",
167
- service: identity.serviceName,
168
- action: identity.action,
169
- method: identity.method,
170
- scope: identity.scope ?? null,
171
- description: definition.description,
172
- schemas: {
173
- params: null,
174
- input: schemaDigest(definition.input, "input"),
175
- output: schemaDigest(definition.output, "output"),
176
- multipart: null
177
- },
178
- http: [],
179
- tools: {}
180
- });
181
- }
182
- const usedNames = {
183
- MCP: new Set,
184
- AGENT: new Set,
185
- CLI: new Set
285
+ toolSurfaces.push({ transport, surface, tools: sortTools(tools) });
186
286
  };
187
- const addTool = (operation, name, transport) => {
188
- if (transport !== "CLI")
189
- assertToolName(name, operation.service, operation.action);
190
- const surface = transport === "CLI" ? "CLI command" : transport === "MCP" ? "MCP tool name" : "agent tool name";
191
- assertUniqueToolName(name, usedNames[transport].has(name), surface);
192
- usedNames[transport].add(name);
193
- operation.tools[transport] = name;
287
+ const shared = {
288
+ services: config.services,
289
+ runtimeTools: config.runtimeTools
194
290
  };
195
- for (const service of uniqueToolServices(config)) {
196
- for (const method of Object.values(service.methods)) {
197
- if (method.multipart || method.rawBody || method.rawResponse || method.responseMeta)
198
- continue;
199
- const operation = operations.get(operationKey("contract", service.name, method.key));
200
- if (!operation)
201
- continue;
202
- const name = method.toolName ?? toToolName(service.name, method.key);
203
- if (!method.expose || method.expose.includes("MCP"))
204
- addTool(operation, name, "MCP");
205
- if (!method.expose || method.expose.includes("AGENT"))
206
- addTool(operation, name, "AGENT");
207
- if (method.expose?.includes("CLI"))
208
- addTool(operation, name, "CLI");
291
+ const explicitMcp = config.toolSurfaces?.MCP;
292
+ const namedMcp = config.mcpSurfaces;
293
+ if (explicitMcp && namedMcp) {
294
+ throw new Error("Surface manifest MCP topology must use either one direct surface or named surfaces");
295
+ }
296
+ if (explicitMcp)
297
+ addToolProjection("MCP", null, explicitMcp);
298
+ if (namedMcp) {
299
+ for (const name of Object.keys(namedMcp).sort(compareCodepoints)) {
300
+ const projection = namedMcp[name];
301
+ if (projection)
302
+ addToolProjection("MCP", name, projection);
209
303
  }
210
304
  }
211
- for (const definition of config.runtimeTools ?? []) {
212
- const operation = operations.get(operationKey("runtime", definition.identity.serviceName, definition.identity.action));
213
- if (!operation)
305
+ if (!explicitMcp && !namedMcp)
306
+ addToolProjection("MCP", null, shared);
307
+ const agent = config.toolSurfaces?.AGENT;
308
+ addToolProjection("AGENT", null, agent ?? shared, agent ?? {});
309
+ addToolProjection("CLI", null, config.toolSurfaces?.CLI ?? shared);
310
+ const realtime = [];
311
+ const realtimeContracts = Object.keys(config.realtime ?? {}).sort(compareCodepoints);
312
+ for (const contractName of realtimeContracts) {
313
+ const contract = config.realtime?.[contractName];
314
+ if (!contract)
214
315
  continue;
215
- const transports = definition.transports ?? [
216
- "MCP",
217
- "AGENT"
218
- ];
219
- for (const transport of transports)
220
- addTool(operation, definition.name, transport);
316
+ for (const direction of ["serverToClient", "clientToServer"]) {
317
+ const registry = contract[direction];
318
+ for (const event of Object.keys(registry).sort(compareCodepoints)) {
319
+ const definition = registry[event];
320
+ if (!definition)
321
+ continue;
322
+ const argsInput = schemaDigest(definition.args, "input");
323
+ const argsOutput = schemaDigest(definition.args, "output");
324
+ if (!argsInput || !argsOutput)
325
+ throw new Error("Realtime args schema digest missing");
326
+ let acknowledgement = null;
327
+ if (definition.ack) {
328
+ const input = schemaDigest(definition.ack, "input");
329
+ const output = schemaDigest(definition.ack, "output");
330
+ if (!input || !output)
331
+ throw new Error("Realtime ack schema digest missing");
332
+ acknowledgement = { input, output };
333
+ }
334
+ realtime.push({
335
+ contract: contractName,
336
+ direction,
337
+ event,
338
+ args: { input: argsInput, output: argsOutput },
339
+ acknowledgement
340
+ });
341
+ }
342
+ }
221
343
  }
222
344
  const cliOnly = (config.cliCommands ?? []).map((command) => ({
223
345
  name: command.name,
@@ -225,10 +347,16 @@ function buildSurfaceManifest(config) {
225
347
  input: schemaDigest(command.input, "input") ?? digestValue({}),
226
348
  output: schemaDigest(command.output, "output")
227
349
  })).sort((left, right) => compareCodepoints(left.name, right.name));
350
+ for (const operation of operations.values()) {
351
+ operation.http.sort((left, right) => compareCodepoints(left.path, right.path) || compareCodepoints(left.method, right.method));
352
+ }
228
353
  return SurfaceManifestSchema.parse({
229
- manifestVersion: 1,
354
+ manifestVersion: 2,
230
355
  digestVersion: 1,
231
356
  operations: [...operations.values()].sort((left, right) => compareCodepoints(left.service, right.service) || compareCodepoints(left.action, right.action) || compareCodepoints(left.kind, right.kind)),
357
+ toolSurfaces: toolSurfaces.sort((left, right) => compareCodepoints(left.transport, right.transport) || compareCodepoints(left.surface ?? "", right.surface ?? "")),
358
+ realtimeContracts,
359
+ realtime,
232
360
  cliOnly,
233
361
  extensions: [...config.extensions ?? []].sort((left, right) => compareCodepoints(left.transport, right.transport) || compareCodepoints(left.name, right.name))
234
362
  });
@@ -244,13 +372,182 @@ actual: ${actual}`);
244
372
  }
245
373
 
246
374
  // src/testing/surface-conformance.ts
247
- var ProbeOutcomeSchema = z2.enum(["success", "validation_error", "domain_error", "aborted"]);
248
- var TransportObservationSchema = z2.object({
249
- outcome: ProbeOutcomeSchema,
375
+ var RealtimeRejectionObservationSchema = z2.object({
376
+ direction: z2.enum([
377
+ "client-inbound",
378
+ "client-outbound",
379
+ "server-inbound",
380
+ "server-outbound"
381
+ ]),
382
+ phase: z2.enum(["arguments", "acknowledgement"]),
383
+ reason: z2.enum([
384
+ "unknown-event",
385
+ "invalid-arguments",
386
+ "invalid-acknowledgement-value",
387
+ "missing-acknowledgement"
388
+ ]),
389
+ fault: z2.enum(["peer", "local"])
390
+ });
391
+ var ObservationFields = {
250
392
  code: z2.string().optional(),
251
393
  data: z2.unknown().optional(),
252
- diagnostic: z2.string().optional()
394
+ diagnostic: z2.string().optional(),
395
+ handlerCalls: z2.number().int().nonnegative().optional()
396
+ };
397
+ var RealtimeDisconnectObservationSchema = z2.object({
398
+ phase: z2.enum(["before-invoke", "in-flight"])
253
399
  });
400
+ var TransportObservationSchema = z2.discriminatedUnion("outcome", [
401
+ z2.object({ outcome: z2.literal("success"), ...ObservationFields }),
402
+ z2.object({ outcome: z2.literal("validation_error"), ...ObservationFields }),
403
+ z2.object({ outcome: z2.literal("domain_error"), ...ObservationFields }),
404
+ z2.object({ outcome: z2.literal("aborted"), ...ObservationFields }),
405
+ z2.object({
406
+ outcome: z2.literal("realtime_rejected"),
407
+ ...ObservationFields,
408
+ rejection: RealtimeRejectionObservationSchema
409
+ }),
410
+ z2.object({
411
+ outcome: z2.literal("disconnected"),
412
+ ...ObservationFields,
413
+ disconnect: RealtimeDisconnectObservationSchema
414
+ }),
415
+ z2.object({ outcome: z2.literal("timeout"), ...ObservationFields })
416
+ ]);
417
+ function scenarioOutcome(scenario) {
418
+ if (scenario === "event" || scenario === "acknowledgement")
419
+ return "success";
420
+ if (scenario === "invalid_arguments" || scenario === "invalid_acknowledgement") {
421
+ return "realtime_rejected";
422
+ }
423
+ if (scenario === "disconnected" || scenario === "in_flight_disconnect") {
424
+ return "disconnected";
425
+ }
426
+ return "timeout";
427
+ }
428
+ function assertScenarioExpectation(scenario, expected) {
429
+ const outcome = scenarioOutcome(scenario);
430
+ if (expected.outcome !== outcome) {
431
+ throw new TypeError(`Realtime probe scenario "${scenario}" requires outcome "${outcome}"`);
432
+ }
433
+ if (expected.outcome === "realtime_rejected" && (scenario === "invalid_arguments" && expected.rejection.phase !== "arguments" || scenario === "invalid_acknowledgement" && expected.rejection.phase !== "acknowledgement")) {
434
+ throw new TypeError(`Realtime probe scenario "${scenario}" has incompatible rejection phase`);
435
+ }
436
+ if (scenario === "invalid_arguments" && expected.handlerCalls !== 0) {
437
+ throw new TypeError('Realtime probe scenario "invalid_arguments" requires handlerCalls: 0');
438
+ }
439
+ if (expected.outcome === "disconnected") {
440
+ const phase = scenario === "disconnected" ? "before-invoke" : "in-flight";
441
+ if (expected.disconnect.phase !== phase) {
442
+ throw new TypeError(`Realtime probe scenario "${scenario}" requires disconnect phase "${phase}"`);
443
+ }
444
+ }
445
+ }
446
+ function defineRealtimeProbe(config) {
447
+ const expected = TransportObservationSchema.parse(config.expected);
448
+ assertScenarioExpectation(config.scenario, expected);
449
+ return {
450
+ name: config.name,
451
+ fixture: { scenario: config.scenario, value: config.fixture },
452
+ transports: ["REALTIME"],
453
+ expected: { REALTIME: expected },
454
+ setup: config.setup,
455
+ teardown: config.teardown
456
+ };
457
+ }
458
+ function rejectionObservation(rejected) {
459
+ return RealtimeRejectionObservationSchema.parse(rejected);
460
+ }
461
+ function createRealtimeProbeDriver({
462
+ bind,
463
+ handlerCalls
464
+ }) {
465
+ return {
466
+ async invoke(fixture, signal) {
467
+ let rejected;
468
+ const currentRejection = () => rejected;
469
+ const adapter = bind((event) => {
470
+ rejected = event;
471
+ }, fixture);
472
+ const connectedBefore = adapter.connected();
473
+ const callsBefore = handlerCalls?.();
474
+ const withCalls = (observation) => {
475
+ if (callsBefore === undefined || !handlerCalls)
476
+ return observation;
477
+ return { ...observation, handlerCalls: handlerCalls() - callsBefore };
478
+ };
479
+ try {
480
+ signal.throwIfAborted();
481
+ const data = await adapter.invoke(fixture, signal);
482
+ const observedRejection = currentRejection();
483
+ if (observedRejection) {
484
+ return withCalls({
485
+ outcome: "realtime_rejected",
486
+ code: observedRejection.error.code,
487
+ rejection: rejectionObservation(observedRejection)
488
+ });
489
+ }
490
+ return withCalls({
491
+ outcome: "success",
492
+ ...data !== undefined && { data }
493
+ });
494
+ } catch (error) {
495
+ const observedRejection = currentRejection();
496
+ if (observedRejection) {
497
+ return withCalls({
498
+ outcome: "realtime_rejected",
499
+ code: error instanceof RealtimeRequestInvalidAcknowledgementError ? error.code : observedRejection.error.code,
500
+ rejection: rejectionObservation(observedRejection)
501
+ });
502
+ }
503
+ if (error instanceof RealtimeRequestInvalidAcknowledgementError) {
504
+ return withCalls({
505
+ outcome: "realtime_rejected",
506
+ code: error.code,
507
+ rejection: {
508
+ direction: "client-inbound",
509
+ phase: "acknowledgement",
510
+ reason: "invalid-acknowledgement-value",
511
+ fault: "peer"
512
+ }
513
+ });
514
+ }
515
+ if (error instanceof RealtimeRequestDisconnectedError) {
516
+ return withCalls({
517
+ outcome: "disconnected",
518
+ code: error.code,
519
+ disconnect: {
520
+ phase: connectedBefore ? "in-flight" : "before-invoke"
521
+ }
522
+ });
523
+ }
524
+ if (error instanceof RealtimeRequestTimeoutError) {
525
+ return withCalls({ outcome: "timeout", code: error.code });
526
+ }
527
+ if (signal.aborted && error === signal.reason) {
528
+ return withCalls({ outcome: "aborted" });
529
+ }
530
+ const parsedRejection = RealtimeRejectionObservationSchema.safeParse(typeof error === "object" && error !== null && "details" in error ? error.details : undefined);
531
+ if (parsedRejection.success) {
532
+ const code2 = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "REALTIME_CONTRACT_VIOLATION";
533
+ return withCalls({
534
+ outcome: "realtime_rejected",
535
+ code: code2,
536
+ rejection: parsedRejection.data
537
+ });
538
+ }
539
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : undefined;
540
+ return withCalls({
541
+ outcome: "domain_error",
542
+ ...code !== undefined && { code },
543
+ diagnostic: error instanceof Error ? error.message : String(error)
544
+ });
545
+ } finally {
546
+ await adapter.dispose?.();
547
+ }
548
+ }
549
+ };
550
+ }
254
551
  function sorted(values) {
255
552
  return [...values].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
256
553
  }
@@ -266,11 +563,12 @@ actual: ${actual.join(", ") || "(empty)"}`);
266
563
  function openApiOperations(document) {
267
564
  return Object.entries(document.paths).flatMap(([path, item]) => Object.keys(item).filter((method) => ["get", "post", "put", "patch", "delete", "head"].includes(method)).map((method) => `${method.toUpperCase()}:${path}`));
268
565
  }
269
- function expectedToolNames(operations, transport) {
270
- return operations.flatMap((operation) => {
271
- const name = operation.tools[transport];
272
- return name ? [name] : [];
273
- });
566
+ function expectedToolNames(manifest, transport, surface) {
567
+ const projection = manifest.toolSurfaces.find((candidate) => candidate.transport === transport && candidate.surface === surface);
568
+ if (!projection) {
569
+ throw new Error(`${transport} surface "${surface ?? "(default)"}" is not declared in the manifest`);
570
+ }
571
+ return projection.tools.map((tool) => tool.name);
274
572
  }
275
573
  function assertSurfaceDiscovery(manifest, observed) {
276
574
  if (observed.openApi) {
@@ -278,13 +576,31 @@ function assertSurfaceDiscovery(manifest, observed) {
278
576
  }
279
577
  for (const transport of ["MCP", "AGENT", "CLI"]) {
280
578
  const names = observed[transport];
281
- if (names)
282
- assertSameSet(`${transport} discovery`, expectedToolNames(manifest.operations, transport), names);
579
+ if (names) {
580
+ assertSameSet(`${transport} discovery`, expectedToolNames(manifest, transport, null), names);
581
+ }
582
+ }
583
+ for (const projection of observed.toolSurfaces ?? []) {
584
+ assertSameSet(`${projection.transport} surface "${projection.surface ?? "(default)"}" discovery`, expectedToolNames(manifest, projection.transport, projection.surface ?? null), projection.names);
585
+ }
586
+ for (const [contract, topology] of Object.entries(observed.realtime ?? {})) {
587
+ if (!manifest.realtimeContracts.includes(contract)) {
588
+ throw new Error(`REALTIME contract "${contract}" is not declared in the manifest`);
589
+ }
590
+ for (const direction of ["serverToClient", "clientToServer"]) {
591
+ assertSameSet(`REALTIME ${contract}.${direction} discovery`, manifest.realtime.filter((entry) => entry.contract === contract && entry.direction === direction).map((entry) => entry.event), topology[direction]);
592
+ }
283
593
  }
284
594
  if (observed.cliOnly) {
285
595
  assertSameSet("CLI-only discovery", manifest.cliOnly.map((command) => command.name), observed.cliOnly);
286
596
  }
287
- for (const transport of ["HTTP", "MCP", "AGENT", "CLI"]) {
597
+ for (const transport of [
598
+ "HTTP",
599
+ "MCP",
600
+ "AGENT",
601
+ "CLI",
602
+ "REALTIME"
603
+ ]) {
288
604
  const names = observed.extensions?.[transport];
289
605
  if (!names)
290
606
  continue;
@@ -303,8 +619,13 @@ function boundedObservation(observation, maxDiagnosticBytes) {
303
619
  diagnostic: new TextDecoder().decode(bytes.slice(0, maxDiagnosticBytes))
304
620
  };
305
621
  }
306
- async function invokeBounded(driver, fixture, signal) {
307
- signal.throwIfAborted();
622
+ async function runBounded(run, signal) {
623
+ if (signal.aborted) {
624
+ Promise.resolve().then(run).catch(() => {
625
+ return;
626
+ });
627
+ throw signal.reason;
628
+ }
308
629
  let removeAbortListener = () => {
309
630
  return;
310
631
  };
@@ -314,7 +635,7 @@ async function invokeBounded(driver, fixture, signal) {
314
635
  removeAbortListener = () => signal.removeEventListener("abort", onAbort);
315
636
  });
316
637
  try {
317
- return await Promise.race([driver.invoke(fixture, signal), aborted]);
638
+ return await Promise.race([Promise.resolve().then(run), aborted]);
318
639
  } finally {
319
640
  removeAbortListener();
320
641
  }
@@ -326,22 +647,28 @@ async function runSurfaceProbes({
326
647
  timeoutMs = 1e4,
327
648
  maxDiagnosticBytes = 4096
328
649
  }) {
329
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
330
- throw new TypeError("Surface probe timeoutMs must be a finite positive number");
650
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
651
+ throw new TypeError("Surface probe timeoutMs must be a positive integer");
331
652
  }
332
653
  if (!Number.isInteger(maxDiagnosticBytes) || maxDiagnosticBytes <= 0) {
333
654
  throw new TypeError("Surface probe maxDiagnosticBytes must be a positive integer");
334
655
  }
335
656
  for (const probe of probes) {
336
- await probe.setup?.();
657
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
658
+ const probeSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
659
+ let failed = false;
660
+ let failure;
337
661
  try {
662
+ probeSignal.throwIfAborted();
663
+ if (probe.setup)
664
+ await runBounded(() => probe.setup?.(probeSignal), probeSignal);
338
665
  for (const transport of probe.transports) {
666
+ probeSignal.throwIfAborted();
339
667
  const driver = drivers[transport];
340
- if (!driver)
668
+ if (!driver) {
341
669
  throw new Error(`Surface probe "${probe.name}" has no ${transport} driver`);
342
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
343
- const invokeSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
344
- const actual = boundedObservation(await invokeBounded(driver, probe.fixture, invokeSignal), maxDiagnosticBytes);
670
+ }
671
+ const actual = boundedObservation(await runBounded(() => driver.invoke(probe.fixture, probeSignal), probeSignal), maxDiagnosticBytes);
345
672
  const expected = probe.expected[transport];
346
673
  if (!expected) {
347
674
  throw new Error(`Surface probe "${probe.name}" has no ${transport} expectation`);
@@ -352,9 +679,22 @@ expected: ${serializeSurfaceValue(expected)}
352
679
  actual: ${serializeSurfaceValue(actual)}`);
353
680
  }
354
681
  }
355
- } finally {
356
- await probe.teardown?.();
682
+ } catch (error) {
683
+ failed = true;
684
+ failure = error;
685
+ }
686
+ if (probe.teardown) {
687
+ try {
688
+ await runBounded(() => probe.teardown?.(probeSignal), probeSignal);
689
+ } catch (error) {
690
+ if (!failed) {
691
+ failed = true;
692
+ failure = error;
693
+ }
694
+ }
357
695
  }
696
+ if (failed)
697
+ throw failure;
358
698
  }
359
699
  }
360
700
 
@@ -384,9 +724,15 @@ function createHandlerTestClients(config) {
384
724
  return createClients(config.contracts, createTestClientConfig(config), config.contractConfig);
385
725
  }
386
726
  export {
727
+ RealtimeDisconnectObservationSchema,
728
+ RealtimeRejectionObservationSchema,
387
729
  SurfaceManifestExtensionSchema,
388
730
  SurfaceManifestOperationSchema,
731
+ SurfaceManifestRealtimeEventSchema,
389
732
  SurfaceManifestSchema,
733
+ SurfaceManifestToolSchema,
734
+ SurfaceManifestToolSurfaceSchema,
735
+ SurfaceRealtimeSchemaPairSchema,
390
736
  SurfaceSchemaDigestsSchema,
391
737
  TransportObservationSchema,
392
738
  assertSurfaceDiscovery,
@@ -394,6 +740,8 @@ export {
394
740
  buildSurfaceManifest,
395
741
  createHandlerTestClient,
396
742
  createHandlerTestClients,
743
+ createRealtimeProbeDriver,
744
+ defineRealtimeProbe,
397
745
  runSurfaceProbes,
398
746
  serializeSurfaceValue
399
747
  };