mcp-event-intelligence 0.8.1 → 0.9.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.
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { z } from 'zod';
2
3
  import {
3
4
  RuntimeTargetSchema,
@@ -242,6 +243,24 @@ export function createResultReference(reference) {
242
243
  };
243
244
  }
244
245
 
246
+ async function projectPortableFailure(projectError, input, canonical) {
247
+ if (typeof projectError !== 'function') return canonical;
248
+ try {
249
+ const projected = await projectError({
250
+ ...input,
251
+ error: canonical.error,
252
+ result: canonical,
253
+ });
254
+ if (projected == null) return canonical;
255
+ if (typeof projected !== 'object' || projected.ok !== false) {
256
+ throw new Error('projectError() must return a PortableToolResult with ok:false');
257
+ }
258
+ return projected;
259
+ } catch {
260
+ return canonical;
261
+ }
262
+ }
263
+
245
264
  async function projectPortableResult(projectResult, input) {
246
265
  if (typeof projectResult !== 'function') {
247
266
  return { ok: true, data: input.value };
@@ -301,11 +320,33 @@ function wakeIdFromPacket(packet) {
301
320
  return typeof value === 'string' && value.trim() ? value : null;
302
321
  }
303
322
 
323
+ export function createDeterministicReceiptId(
324
+ namespace,
325
+ wakeId,
326
+ { length = 32 } = {},
327
+ ) {
328
+ const normalizedNamespace = String(namespace ?? '').trim();
329
+ const normalizedWakeId = String(wakeId ?? '').trim();
330
+ if (!normalizedNamespace) {
331
+ throw new Error('receipt namespace is required');
332
+ }
333
+ if (!normalizedWakeId) {
334
+ throw new Error('wake id is required');
335
+ }
336
+ if (!Number.isInteger(length) || length < 8 || length > 64) {
337
+ throw new Error('receipt id length must be an integer between 8 and 64');
338
+ }
339
+ return createHash('sha256')
340
+ .update(JSON.stringify([normalizedNamespace, normalizedWakeId]))
341
+ .digest('hex')
342
+ .slice(0, length);
343
+ }
344
+
304
345
  /**
305
346
  * Runtime-neutral registry over host-owned event connections.
306
347
  *
307
- * The host keeps credentials, transport and lifecycle ownership. EI only asks
308
- * for the currently available connections and listens for topology changes.
348
+ * Static iterables are accepted directly by createEmbeddedRuntimeIntegration();
349
+ * use this explicit factory when the host has dynamic discovery/subscription.
309
350
  */
310
351
  export function createEventSourceRegistry({
311
352
  list,
@@ -328,6 +369,83 @@ export function createEventSourceRegistry({
328
369
  * Neutral catalog for host tool exposure. It does not decide policy or adapt
329
370
  * descriptors to any vendor SDK; the embedding runtime owns both.
330
371
  */
372
+ export function summarizeEventSourceStatus(
373
+ statuses = [],
374
+ refreshOutcomes = [],
375
+ ) {
376
+ const rows = Array.isArray(statuses) ? statuses : [];
377
+ const refresh = Array.isArray(refreshOutcomes) ? refreshOutcomes : [];
378
+ const connectionIds = new Set();
379
+ const errorIds = new Set();
380
+ let anonymousErrors = 0;
381
+ let eventsCapable = 0;
382
+ let eventDefinitions = 0;
383
+
384
+ for (const outcome of refresh) {
385
+ if (!outcome || typeof outcome !== 'object') continue;
386
+ const id = String(outcome.connectionId ?? '').trim();
387
+ if (outcome.status !== 'detached' && id) connectionIds.add(id);
388
+ if (outcome.status === 'error' || outcome.error != null) {
389
+ if (id) errorIds.add(id);
390
+ else anonymousErrors += 1;
391
+ }
392
+ }
393
+
394
+ for (const row of rows) {
395
+ if (!row || typeof row !== 'object') continue;
396
+ const id = String(row.connectionId ?? row.id ?? '').trim();
397
+ if (id) connectionIds.add(id);
398
+ if (row.error != null) {
399
+ if (id) errorIds.add(id);
400
+ else anonymousErrors += 1;
401
+ continue;
402
+ }
403
+ if (Array.isArray(row.events) && row.events.length > 0) {
404
+ eventsCapable += 1;
405
+ eventDefinitions += row.events.length;
406
+ }
407
+ }
408
+
409
+ return Object.freeze({
410
+ connections: connectionIds.size || rows.length,
411
+ eventsCapable,
412
+ eventDefinitions,
413
+ errors: errorIds.size + anonymousErrors,
414
+ statuses: rows,
415
+ refreshOutcomes: refresh,
416
+ });
417
+ }
418
+
419
+ export function bindEmbeddedRuntimeIntegration(
420
+ integration,
421
+ { adapt, register, onClose } = {},
422
+ ) {
423
+ if (!integration || typeof integration !== 'object') {
424
+ throw new Error('embedded integration is required');
425
+ }
426
+ if (typeof adapt !== 'function') {
427
+ throw new Error('bind adapt() is required');
428
+ }
429
+ if (typeof register !== 'function') {
430
+ throw new Error('bind register() is required');
431
+ }
432
+ if (onClose !== undefined && typeof onClose !== 'function') {
433
+ throw new Error('bind onClose must be a function');
434
+ }
435
+
436
+ const adapted = integration.tools.map((portableTool) => {
437
+ const hostTool = adapt(portableTool);
438
+ register(hostTool, portableTool);
439
+ return hostTool;
440
+ });
441
+
442
+ if (onClose) {
443
+ onClose(() => integration.close());
444
+ }
445
+
446
+ return Object.freeze(adapted);
447
+ }
448
+
331
449
  export function createPortableToolCatalog(tools = []) {
332
450
  const all = Object.freeze([...tools]);
333
451
  const byName = new Map(all.map((tool) => [tool.name, tool]));
@@ -367,6 +485,7 @@ export function createActivationDispatcher({
367
485
  resolveTarget,
368
486
  hasReceipt,
369
487
  receiptId,
488
+ receiptNamespace,
370
489
  deliver,
371
490
  } = {}) {
372
491
  if (resolveTarget !== undefined && typeof resolveTarget !== 'function') {
@@ -378,6 +497,15 @@ export function createActivationDispatcher({
378
497
  if (receiptId !== undefined && typeof receiptId !== 'function') {
379
498
  throw new Error('receiptId must be a function');
380
499
  }
500
+ if (
501
+ receiptNamespace !== undefined &&
502
+ (typeof receiptNamespace !== 'string' || !receiptNamespace.trim())
503
+ ) {
504
+ throw new Error('receiptNamespace must be a non-empty string');
505
+ }
506
+ if (receiptId !== undefined && receiptNamespace !== undefined) {
507
+ throw new Error('Provide receiptId or receiptNamespace, not both');
508
+ }
381
509
  if (typeof deliver !== 'function') {
382
510
  throw new Error('Activation dispatcher requires deliver()');
383
511
  }
@@ -394,9 +522,16 @@ export function createActivationDispatcher({
394
522
  }
395
523
 
396
524
  const candidateReceiptId = String(
397
- await (receiptId
398
- ? receiptId({ packet, activation })
399
- : activation.wake.wakeId),
525
+ await (
526
+ receiptId
527
+ ? receiptId({ packet, activation })
528
+ : receiptNamespace
529
+ ? createDeterministicReceiptId(
530
+ receiptNamespace,
531
+ activation.wake.wakeId,
532
+ )
533
+ : activation.wake.wakeId
534
+ ),
400
535
  ).trim();
401
536
  if (!candidateReceiptId) {
402
537
  const error = new Error('Activation receipt id must be non-empty');
@@ -518,6 +653,7 @@ export function createEventIntelligenceAgentTools({
518
653
  resolveContext,
519
654
  control,
520
655
  projectResult,
656
+ projectError,
521
657
  authorize,
522
658
  names = {},
523
659
  } = {}) {
@@ -541,6 +677,9 @@ export function createEventIntelligenceAgentTools({
541
677
  if (projectResult !== undefined && typeof projectResult !== 'function') {
542
678
  throw new Error('Agent tools projectResult must be a function');
543
679
  }
680
+ if (projectError !== undefined && typeof projectError !== 'function') {
681
+ throw new Error('Agent tools projectError must be a function');
682
+ }
544
683
 
545
684
  const toolNames = {
546
685
  ...DEFAULT_TOOL_NAMES,
@@ -582,7 +721,13 @@ export function createEventIntelligenceAgentTools({
582
721
  value,
583
722
  });
584
723
  } catch (error) {
585
- return fail(error, 'EVENT_SOURCES_LIST_FAILED');
724
+ const canonical = fail(error, 'EVENT_SOURCES_LIST_FAILED');
725
+ return projectPortableFailure(projectError, {
726
+ capability: sourcesCapability,
727
+ runtimeContext,
728
+ context: undefined,
729
+ phase: 'event.sources.list',
730
+ }, canonical);
586
731
  }
587
732
  },
588
733
  },
@@ -596,7 +741,7 @@ export function createEventIntelligenceAgentTools({
596
741
  const parsed = TriggerCreateInput.safeParse(args ?? {});
597
742
  if (!parsed.success) {
598
743
  const issue = parsed.error.issues[0];
599
- return {
744
+ const canonical = {
600
745
  ok: false,
601
746
  error: {
602
747
  code: 'EVENT_TRIGGER_INPUT_INVALID',
@@ -604,6 +749,12 @@ export function createEventIntelligenceAgentTools({
604
749
  `${issue?.path?.join('.') || 'input'} — ${issue?.message || 'invalid'}`,
605
750
  },
606
751
  };
752
+ return projectPortableFailure(projectError, {
753
+ capability: createCapability,
754
+ runtimeContext,
755
+ context: undefined,
756
+ phase: 'trigger.input',
757
+ }, canonical);
607
758
  }
608
759
 
609
760
  let resolved;
@@ -624,7 +775,13 @@ export function createEventIntelligenceAgentTools({
624
775
  buildPlanInput(parsed.data, target),
625
776
  );
626
777
  } catch (error) {
627
- return fail(error, 'EVENT_TRIGGER_PLAN_FAILED');
778
+ const canonical = fail(error, 'EVENT_TRIGGER_PLAN_FAILED');
779
+ return projectPortableFailure(projectError, {
780
+ capability: createCapability,
781
+ runtimeContext,
782
+ context: resolved,
783
+ phase: 'trigger.plan',
784
+ }, canonical);
628
785
  }
629
786
 
630
787
  // Host control is intentionally outside EI's error-normalization
@@ -656,7 +813,13 @@ export function createEventIntelligenceAgentTools({
656
813
  }),
657
814
  );
658
815
  } catch (error) {
659
- return fail(error, 'EVENT_TRIGGER_CREATE_FAILED');
816
+ const canonical = fail(error, 'EVENT_TRIGGER_CREATE_FAILED');
817
+ return projectPortableFailure(projectError, {
818
+ capability: createCapability,
819
+ runtimeContext,
820
+ context: resolved,
821
+ phase: 'trigger.control',
822
+ }, canonical);
660
823
  }
661
824
  }
662
825
  const decision = normalizeHostControlDecision(rawDecision);
@@ -700,7 +863,13 @@ export function createEventIntelligenceAgentTools({
700
863
  warnings: plan.warnings ?? [],
701
864
  };
702
865
  } catch (error) {
703
- return fail(error, 'EVENT_TRIGGER_CREATE_FAILED');
866
+ const canonical = fail(error, 'EVENT_TRIGGER_CREATE_FAILED');
867
+ return projectPortableFailure(projectError, {
868
+ capability: createCapability,
869
+ runtimeContext,
870
+ context: resolved,
871
+ phase: 'trigger.create',
872
+ }, canonical);
704
873
  }
705
874
 
706
875
  // Trigger creation is already durable at this point. Projection is a
@@ -734,6 +903,7 @@ export async function createEmbeddedEventIntelligence({
734
903
  mcp,
735
904
  mcpClients = [],
736
905
  semanticEvaluator,
906
+ observability,
737
907
  activation,
738
908
  wake,
739
909
  wakeHandlers = {},
@@ -763,6 +933,7 @@ export async function createEmbeddedEventIntelligence({
763
933
  mcpRegistry: registry,
764
934
  mcpClients,
765
935
  semanticEvaluator,
936
+ observability,
766
937
  ...(wakeHandler ? { wake: wakeHandler } : {}),
767
938
  wakeHandlers,
768
939
  });
@@ -814,6 +985,7 @@ export async function createEmbeddedRuntimeIntegration({
814
985
  eventSources,
815
986
  mcpClients = [],
816
987
  semanticEvaluator,
988
+ observability,
817
989
  activation,
818
990
  wake,
819
991
  wakeHandlers = {},
@@ -837,9 +1009,24 @@ export async function createEmbeddedRuntimeIntegration({
837
1009
  ) {
838
1010
  throw new Error('tooling.projectResult must be a function');
839
1011
  }
1012
+ if (
1013
+ tooling?.projectError !== undefined &&
1014
+ typeof tooling.projectError !== 'function'
1015
+ ) {
1016
+ throw new Error('tooling.projectError must be a function');
1017
+ }
840
1018
 
841
1019
  const sourceRegistry = eventSources
842
- ? createEventSourceRegistry(eventSources)
1020
+ ? (
1021
+ typeof eventSources?.list === 'function'
1022
+ ? createEventSourceRegistry(eventSources)
1023
+ : (() => {
1024
+ const staticSources = Object.freeze([...eventSources]);
1025
+ return createEventSourceRegistry({
1026
+ list: () => staticSources,
1027
+ });
1028
+ })()
1029
+ )
843
1030
  : null;
844
1031
 
845
1032
  const embedded = await createEmbeddedEventIntelligence({
@@ -858,6 +1045,7 @@ export async function createEmbeddedRuntimeIntegration({
858
1045
  : {}),
859
1046
  mcpClients,
860
1047
  semanticEvaluator,
1048
+ observability,
861
1049
  activation,
862
1050
  wake,
863
1051
  wakeHandlers,
@@ -872,6 +1060,9 @@ export async function createEmbeddedRuntimeIntegration({
872
1060
  ...(tooling.projectResult
873
1061
  ? { projectResult: tooling.projectResult }
874
1062
  : {}),
1063
+ ...(tooling.projectError
1064
+ ? { projectError: tooling.projectError }
1065
+ : {}),
875
1066
  names: tooling.names ?? {},
876
1067
  })
877
1068
  : [];
@@ -883,6 +1074,22 @@ export async function createEmbeddedRuntimeIntegration({
883
1074
  toolCatalog,
884
1075
  capabilities: Object.freeze(toolCatalog.capabilities()),
885
1076
  eventSources: sourceRegistry,
1077
+ async diagnostics() {
1078
+ const refreshOutcomes = sourceRegistry ? await embedded.refresh() : [];
1079
+ return summarizeEventSourceStatus(
1080
+ await embedded.status(),
1081
+ refreshOutcomes,
1082
+ );
1083
+ },
1084
+ bind(options) {
1085
+ return bindEmbeddedRuntimeIntegration(
1086
+ {
1087
+ ...embedded,
1088
+ tools: toolCatalog.all,
1089
+ },
1090
+ options,
1091
+ );
1092
+ },
886
1093
  });
887
1094
  }
888
1095
 
@@ -0,0 +1,75 @@
1
+ import type {
2
+ EventIntelligenceObservabilityEvent,
3
+ } from './lib/observability.d.mts';
4
+
5
+ export interface HostConformanceTriggerInput {
6
+ triggerId: string;
7
+ threshold: number;
8
+ oneShot: boolean;
9
+ maxFirings?: number;
10
+ }
11
+
12
+ export interface HostConformanceEventInput {
13
+ eventId: string;
14
+ value: number;
15
+ secretProbe?: string;
16
+ }
17
+
18
+ export interface HostConformanceDelivery {
19
+ triggerId: string;
20
+ wakeId?: string;
21
+ runtimeReceiptId?: string;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export interface HostConformanceHarness {
26
+ createTrigger(input: HostConformanceTriggerInput): Promise<void>;
27
+ emitEvent(input: HostConformanceEventInput): Promise<void>;
28
+ deliveries(): Promise<readonly HostConformanceDelivery[]> | readonly HostConformanceDelivery[];
29
+ restart(): Promise<void>;
30
+ inspectTrigger(triggerId: string): Promise<{
31
+ status?: string;
32
+ fireCount?: number;
33
+ [key: string]: unknown;
34
+ } | null>;
35
+ close(): Promise<void>;
36
+ }
37
+
38
+ export interface HostConformanceAdapter {
39
+ name?: string;
40
+ createHarness(input: {
41
+ observability(event: EventIntelligenceObservabilityEvent): void;
42
+ }): Promise<HostConformanceHarness> | HostConformanceHarness;
43
+ }
44
+
45
+ export interface HostConformanceResult {
46
+ id: string;
47
+ status: 'pass' | 'fail';
48
+ durationMs: number;
49
+ detail?: unknown;
50
+ error?: string;
51
+ }
52
+
53
+ export interface HostConformanceReport {
54
+ schema: 'event-intelligence.host-conformance.v1' | string;
55
+ adapter: string;
56
+ passed: boolean;
57
+ summary: {
58
+ total: number;
59
+ passed: number;
60
+ failed: number;
61
+ };
62
+ results: readonly HostConformanceResult[];
63
+ observability: {
64
+ captured: number;
65
+ eventNames: readonly string[];
66
+ };
67
+ }
68
+
69
+ export const HOST_CONFORMANCE_REPORT_SCHEMA:
70
+ 'event-intelligence.host-conformance.v1';
71
+
72
+ export function runHostConformance(
73
+ adapter: HostConformanceAdapter,
74
+ options?: { throwOnFailure?: boolean },
75
+ ): Promise<HostConformanceReport>;
@@ -0,0 +1,238 @@
1
+ const REPORT_SCHEMA = 'event-intelligence.host-conformance.v1';
2
+
3
+ function assertFunction(value, name) {
4
+ if (typeof value !== 'function') {
5
+ throw new Error(`Host conformance adapter requires ${name}()`);
6
+ }
7
+ }
8
+
9
+ function validateHarness(harness) {
10
+ if (!harness || typeof harness !== 'object') {
11
+ throw new Error('createHarness() must return a harness object');
12
+ }
13
+ for (const method of [
14
+ 'createTrigger',
15
+ 'emitEvent',
16
+ 'deliveries',
17
+ 'restart',
18
+ 'inspectTrigger',
19
+ 'close',
20
+ ]) {
21
+ assertFunction(harness[method], method);
22
+ }
23
+ return harness;
24
+ }
25
+
26
+ function deliveryCount(rows, triggerId) {
27
+ return rows.filter((row) => row?.triggerId === triggerId).length;
28
+ }
29
+
30
+ async function runCase(id, execute) {
31
+ const startedAt = Date.now();
32
+ try {
33
+ const detail = await execute();
34
+ return Object.freeze({
35
+ id,
36
+ status: 'pass',
37
+ durationMs: Date.now() - startedAt,
38
+ ...(detail ? { detail } : {}),
39
+ });
40
+ } catch (error) {
41
+ return Object.freeze({
42
+ id,
43
+ status: 'fail',
44
+ durationMs: Date.now() - startedAt,
45
+ error: error instanceof Error ? error.message : String(error),
46
+ });
47
+ }
48
+ }
49
+
50
+ function requireCondition(condition, message) {
51
+ if (!condition) throw new Error(message);
52
+ }
53
+
54
+ /**
55
+ * Run the runtime-neutral EI host contract against an isolated host harness.
56
+ *
57
+ * The adapter owns all runtime-specific translation. The kit only uses a
58
+ * canonical value-change source and inspects externally visible deliveries,
59
+ * persisted trigger state and structured observability events.
60
+ */
61
+ export async function runHostConformance(
62
+ adapter,
63
+ { throwOnFailure = false } = {},
64
+ ) {
65
+ if (!adapter || typeof adapter !== 'object') {
66
+ throw new Error('Host conformance adapter is required');
67
+ }
68
+ assertFunction(adapter.createHarness, 'createHarness');
69
+
70
+ const observed = [];
71
+ const harness = validateHarness(await adapter.createHarness({
72
+ observability(event) {
73
+ observed.push(event);
74
+ },
75
+ }));
76
+
77
+ const results = [];
78
+ try {
79
+ results.push(await runCase('core.once-threshold', async () => {
80
+ await harness.createTrigger({
81
+ triggerId: 'conformance-once',
82
+ threshold: 1000,
83
+ oneShot: true,
84
+ });
85
+
86
+ await harness.emitEvent({
87
+ eventId: 'once-below',
88
+ value: 999,
89
+ secretProbe: 'ei-conformance-secret-probe',
90
+ });
91
+ requireCondition(
92
+ deliveryCount(await harness.deliveries(), 'conformance-once') === 0,
93
+ 'below-threshold event produced a delivery',
94
+ );
95
+
96
+ await harness.emitEvent({
97
+ eventId: 'once-match',
98
+ value: 1001,
99
+ secretProbe: 'ei-conformance-secret-probe',
100
+ });
101
+ requireCondition(
102
+ deliveryCount(await harness.deliveries(), 'conformance-once') === 1,
103
+ 'matching event did not produce exactly one delivery',
104
+ );
105
+
106
+ }));
107
+
108
+ results.push(await runCase('core.replay-dedup', async () => {
109
+ await harness.createTrigger({
110
+ triggerId: 'conformance-replay',
111
+ threshold: 1000,
112
+ oneShot: false,
113
+ maxFirings: 2,
114
+ });
115
+
116
+ await harness.emitEvent({
117
+ eventId: 'replay-same-id',
118
+ value: 1100,
119
+ secretProbe: 'ei-conformance-secret-probe',
120
+ });
121
+ await harness.emitEvent({
122
+ eventId: 'replay-same-id',
123
+ value: 1100,
124
+ secretProbe: 'ei-conformance-secret-probe',
125
+ });
126
+
127
+ requireCondition(
128
+ deliveryCount(await harness.deliveries(), 'conformance-replay') === 1,
129
+ 'replayed event produced a duplicate delivery on a persistent trigger',
130
+ );
131
+ const state = await harness.inspectTrigger('conformance-replay');
132
+ requireCondition(
133
+ Number(state?.fireCount) === 1,
134
+ `replayed event advanced fireCount to ${state?.fireCount}`,
135
+ );
136
+ }));
137
+
138
+ results.push(await runCase('core.restart-durability', async () => {
139
+ await harness.createTrigger({
140
+ triggerId: 'conformance-restart',
141
+ threshold: 1000,
142
+ oneShot: false,
143
+ maxFirings: 2,
144
+ });
145
+ await harness.restart();
146
+
147
+ await harness.emitEvent({
148
+ eventId: 'restart-1',
149
+ value: 1200,
150
+ secretProbe: 'ei-conformance-secret-probe',
151
+ });
152
+ requireCondition(
153
+ deliveryCount(await harness.deliveries(), 'conformance-restart') === 1,
154
+ 'trigger did not survive runtime restart',
155
+ );
156
+ }));
157
+
158
+ results.push(await runCase('core.max-firings', async () => {
159
+ await harness.emitEvent({
160
+ eventId: 'restart-2',
161
+ value: 1300,
162
+ secretProbe: 'ei-conformance-secret-probe',
163
+ });
164
+ await harness.emitEvent({
165
+ eventId: 'restart-3',
166
+ value: 1400,
167
+ secretProbe: 'ei-conformance-secret-probe',
168
+ });
169
+
170
+ requireCondition(
171
+ deliveryCount(await harness.deliveries(), 'conformance-restart') === 2,
172
+ 'maxFirings=2 did not cap external deliveries at two',
173
+ );
174
+
175
+ const state = await harness.inspectTrigger('conformance-restart');
176
+ requireCondition(Boolean(state), 'trigger state unavailable after firing');
177
+ requireCondition(
178
+ Number(state.fireCount) === 2,
179
+ `expected fireCount=2, got ${state.fireCount}`,
180
+ );
181
+ }));
182
+
183
+ results.push(await runCase('observability.required-events', async () => {
184
+ const names = new Set(observed.map((event) => event?.event));
185
+ for (const name of [
186
+ 'ei.trigger.created',
187
+ 'ei.match.matched',
188
+ 'ei.wake.delivered',
189
+ ]) {
190
+ requireCondition(
191
+ names.has(name),
192
+ `missing required observability event ${name}`,
193
+ );
194
+ }
195
+
196
+ const serialized = JSON.stringify(observed);
197
+ requireCondition(
198
+ !serialized.includes('ei-conformance-secret-probe'),
199
+ 'observability leaked event payload content',
200
+ );
201
+ }));
202
+ } finally {
203
+ await harness.close();
204
+ }
205
+
206
+ const failed = results.filter((result) => result.status === 'fail');
207
+ const report = Object.freeze({
208
+ schema: REPORT_SCHEMA,
209
+ adapter: String(adapter.name ?? 'unnamed-host'),
210
+ passed: failed.length === 0,
211
+ summary: Object.freeze({
212
+ total: results.length,
213
+ passed: results.length - failed.length,
214
+ failed: failed.length,
215
+ }),
216
+ results: Object.freeze(results),
217
+ observability: Object.freeze({
218
+ captured: observed.length,
219
+ eventNames: Object.freeze([...new Set(
220
+ observed.map((event) => event?.event).filter(Boolean),
221
+ )].sort()),
222
+ }),
223
+ });
224
+
225
+ if (throwOnFailure && !report.passed) {
226
+ const error = new Error(
227
+ `Event Intelligence host conformance failed: ${failed
228
+ .map((item) => item.id)
229
+ .join(', ')}`,
230
+ );
231
+ error.report = report;
232
+ throw error;
233
+ }
234
+
235
+ return report;
236
+ }
237
+
238
+ export const HOST_CONFORMANCE_REPORT_SCHEMA = REPORT_SCHEMA;
@@ -2,6 +2,9 @@ import type {
2
2
  ActivationEnvelope,
3
3
  TriggerPlanInput,
4
4
  } from '../dist/src/intelligenceProtocol/index.js';
5
+ import type {
6
+ EventIntelligenceObservabilitySink,
7
+ } from './lib/observability.d.mts';
5
8
 
6
9
  export interface HostMcpClientLike {
7
10
  request(
@@ -130,6 +133,8 @@ export interface EventIntelligenceHostOptions {
130
133
  /** Optional runtime-specific dispatchers. */
131
134
  wakeHandlers?: Map<string, HostWakeHandler> | Record<string, HostWakeHandler>;
132
135
  semanticEvaluator?: unknown;
136
+ /** Runtime-neutral structured operational events. Sink failures never affect EI semantics. */
137
+ observability?: EventIntelligenceObservabilitySink;
133
138
  }
134
139
 
135
140
  export interface EventIntelligenceScopedHost {