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.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/conformance/README.md +59 -0
- package/conformance/fixtures/composite-trigger.valid.json +70 -0
- package/conformance/fixtures/event-lineage.valid.json +26 -0
- package/conformance/reference-host.mjs +145 -0
- package/package.json +15 -3
- package/scripts/embedded-host-kit.d.mts +67 -1
- package/scripts/embedded-host-kit.mjs +218 -11
- package/scripts/host-conformance.d.mts +75 -0
- package/scripts/host-conformance.mjs +238 -0
- package/scripts/host-integration.d.mts +5 -0
- package/scripts/host-integration.mjs +23 -4
- package/scripts/lib/composite-event-consumer.mjs +28 -0
- package/scripts/lib/composite-wake-coordinator.mjs +64 -0
- package/scripts/lib/local-event-intelligence-runtime.mjs +39 -0
- package/scripts/lib/mcp-events-client.mjs +36 -0
- package/scripts/lib/observability.d.mts +77 -0
- package/scripts/lib/observability.mjs +134 -0
- package/scripts/lib/temporal-deadline-scheduler.mjs +10 -0
- package/scripts/lib/trigger-control-plane.mjs +26 -0
- package/scripts/lib/wake-retry-scheduler.mjs +10 -0
- package/server.json +2 -2
|
@@ -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
|
-
*
|
|
308
|
-
*
|
|
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 (
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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 {
|