@kontourai/flow-agents 0.1.2 → 0.3.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.
Files changed (117) hide show
  1. package/.github/dependabot.yml +23 -0
  2. package/.github/workflows/release-please.yml +31 -0
  3. package/.github/workflows/runtime-compat.yml +118 -0
  4. package/CHANGELOG.md +46 -0
  5. package/CONTRIBUTING.md +4 -0
  6. package/README.md +80 -18
  7. package/build/src/cli/flow-kit.js +9 -4
  8. package/build/src/cli/init.js +215 -5
  9. package/build/src/cli/runtime-adapter.js +9 -5
  10. package/build/src/cli/telemetry-doctor.js +4 -1
  11. package/build/src/cli/utterance-check.js +65 -1
  12. package/build/src/runtime-adapters.js +34 -0
  13. package/build/src/tools/build-universal-bundles.js +285 -0
  14. package/build/src/tools/filter-installed-packs.js +3 -0
  15. package/build/src/tools/validate-source-tree.js +5 -1
  16. package/console.telemetry.json +115 -20
  17. package/context/scripts/telemetry/lib/config.sh +5 -1
  18. package/context/settings/flow-agents-settings.json +7 -0
  19. package/docs/_layouts/default.html +2 -0
  20. package/docs/context-map.md +1 -0
  21. package/docs/index.md +53 -4
  22. package/docs/integrations/conformance.md +246 -0
  23. package/docs/integrations/framework-adapter.md +275 -0
  24. package/docs/integrations/harness-install.md +213 -0
  25. package/docs/integrations/index.md +58 -0
  26. package/docs/integrations/knowledge-kit-live.md +211 -0
  27. package/docs/kit-authoring-guide.md +169 -0
  28. package/docs/north-star.md +2 -2
  29. package/docs/spec/runtime-hook-surface.md +525 -0
  30. package/docs/survey-utterance-check.md +211 -94
  31. package/docs/vision.md +45 -0
  32. package/evals/acceptance/run.sh +13 -2
  33. package/evals/acceptance/test_knowledge_kit_live.sh +221 -0
  34. package/evals/acceptance/test_opencode_harness.sh +121 -0
  35. package/evals/acceptance/test_pi_harness.sh +113 -0
  36. package/evals/integration/test_bundle_install.sh +226 -1
  37. package/evals/integration/test_bundle_lifecycle.sh +641 -0
  38. package/evals/integration/test_runtime_adapter_activation.sh +113 -1
  39. package/evals/integration/test_utterance_check.sh +291 -44
  40. package/evals/run.sh +2 -0
  41. package/evals/static/test_universal_bundles.sh +137 -2
  42. package/integrations/strands/README.md +256 -0
  43. package/integrations/strands/example.py +74 -0
  44. package/integrations/strands/examples/knowledge_kit_live.py +461 -0
  45. package/integrations/strands/flow_agents_strands/__init__.py +27 -0
  46. package/integrations/strands/flow_agents_strands/hooks.py +194 -0
  47. package/integrations/strands/flow_agents_strands/policy.py +348 -0
  48. package/integrations/strands/flow_agents_strands/steering.py +225 -0
  49. package/integrations/strands/flow_agents_strands/telemetry.py +238 -0
  50. package/integrations/strands/pyproject.toml +38 -0
  51. package/integrations/strands/tests/__init__.py +0 -0
  52. package/integrations/strands/tests/test_hooks.py +392 -0
  53. package/integrations/strands/tests/test_policy.py +315 -0
  54. package/integrations/strands/tests/test_telemetry.py +184 -0
  55. package/integrations/strands-ts/README.md +224 -0
  56. package/integrations/strands-ts/bin/conformance-shim.mjs +257 -0
  57. package/integrations/strands-ts/package.json +53 -0
  58. package/integrations/strands-ts/src/hooks.ts +312 -0
  59. package/integrations/strands-ts/src/index.ts +22 -0
  60. package/integrations/strands-ts/src/policy.ts +345 -0
  61. package/integrations/strands-ts/src/telemetry.ts +251 -0
  62. package/integrations/strands-ts/test/test-policy.ts +322 -0
  63. package/integrations/strands-ts/test/test-steering.ts +159 -0
  64. package/integrations/strands-ts/test/test-telemetry.ts +226 -0
  65. package/integrations/strands-ts/tsconfig.json +20 -0
  66. package/kits/catalog.json +6 -0
  67. package/kits/knowledge/adapters/default-store/index.js +821 -0
  68. package/kits/knowledge/adapters/flow-runner/index.js +1179 -0
  69. package/kits/knowledge/adapters/flow-runner/telemetry.js +174 -0
  70. package/kits/knowledge/docs/README.md +135 -0
  71. package/kits/knowledge/docs/store-contract.md +526 -0
  72. package/kits/knowledge/evals/consolidation/suite.test.js +1234 -0
  73. package/kits/knowledge/evals/contract-suite/suite.test.js +670 -0
  74. package/kits/knowledge/evals/ingest-compile/suite.test.js +574 -0
  75. package/kits/knowledge/evals/synthesis/suite.test.js +909 -0
  76. package/kits/knowledge/flows/compile.flow.json +60 -0
  77. package/kits/knowledge/flows/consolidate.flow.json +77 -0
  78. package/kits/knowledge/flows/ingest.flow.json +60 -0
  79. package/kits/knowledge/flows/store-contract.flow.json +48 -0
  80. package/kits/knowledge/flows/synthesize.flow.json +77 -0
  81. package/kits/knowledge/kit.json +78 -0
  82. package/package.json +7 -2
  83. package/packaging/conformance/README.md +142 -0
  84. package/packaging/conformance/fixtures/config-protection--allow-no-path.json +18 -0
  85. package/packaging/conformance/fixtures/config-protection--allow-safe-file.json +20 -0
  86. package/packaging/conformance/fixtures/config-protection--block-biome.json +20 -0
  87. package/packaging/conformance/fixtures/config-protection--block-eslintrc.json +20 -0
  88. package/packaging/conformance/fixtures/quality-gate--allow-no-path.json +17 -0
  89. package/packaging/conformance/fixtures/quality-gate--allow-nonexistent-file.json +19 -0
  90. package/packaging/conformance/fixtures/stop-goal-fit--allow-clean-cwd.json +17 -0
  91. package/packaging/conformance/fixtures/stop-goal-fit--block-strict-mode.json +23 -0
  92. package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +21 -0
  93. package/packaging/conformance/fixtures/workflow-steering--allow-no-state.json +16 -0
  94. package/packaging/conformance/fixtures/workflow-steering--inject-active-state.json +29 -0
  95. package/packaging/conformance/fixtures/workflow-steering--inject-subagent-steering.json +25 -0
  96. package/packaging/conformance/package.json +4 -0
  97. package/packaging/conformance/run-conformance.js +322 -0
  98. package/packaging/manifest.json +59 -0
  99. package/schemas/flow-agents-settings.schema.json +48 -0
  100. package/scripts/README.md +4 -0
  101. package/scripts/dogfood.js +16 -0
  102. package/scripts/hooks/opencode-hook-adapter.js +123 -0
  103. package/scripts/hooks/opencode-telemetry-hook.js +101 -0
  104. package/scripts/hooks/pi-hook-adapter.js +123 -0
  105. package/scripts/hooks/pi-telemetry-hook.js +105 -0
  106. package/scripts/hooks/run-hook.js +8 -0
  107. package/scripts/hooks/utterance-check.js +124 -22
  108. package/scripts/telemetry/lib/config.sh +5 -1
  109. package/src/cli/flow-kit.ts +10 -4
  110. package/src/cli/init.ts +219 -6
  111. package/src/cli/runtime-adapter.ts +10 -5
  112. package/src/cli/telemetry-doctor.ts +4 -1
  113. package/src/cli/utterance-check.ts +71 -1
  114. package/src/runtime-adapters.ts +35 -0
  115. package/src/tools/build-universal-bundles.ts +283 -0
  116. package/src/tools/filter-installed-packs.ts +3 -0
  117. package/src/tools/validate-source-tree.ts +5 -1
@@ -0,0 +1,574 @@
1
+ /**
2
+ * Knowledge Kit — Ingest + Compile Eval Suite
3
+ *
4
+ * Covers:
5
+ * AC1: kit validates (command evidence from validate:source)
6
+ * AC2: ingest+compile against default store yields a compiled note whose
7
+ * provenance refs resolve (all ref.id values retrievable from store)
8
+ * AC3: canonical telemetry emitted at each gate (schema v0.3.0 events
9
+ * appear in .telemetry/full.jsonl)
10
+ *
11
+ * Also covers:
12
+ * - ingest produces classified raw (category + type="raw")
13
+ * - compile rejects when provenance missing
14
+ * - compile succeeds and provenance refs resolve
15
+ * - telemetry events emitted at gates
16
+ *
17
+ * Run:
18
+ * node --test kits/knowledge/evals/ingest-compile/suite.test.js
19
+ */
20
+
21
+ import { test, describe, before, after, beforeEach } from "node:test";
22
+ import assert from "node:assert/strict";
23
+ import * as fs from "node:fs";
24
+ import * as path from "node:path";
25
+ import * as os from "node:os";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+ const KIT_ROOT = path.resolve(__dirname, "../..");
30
+
31
+ // Adapter and runner imports
32
+ const adapterPath = path.join(KIT_ROOT, "adapters/default-store/index.js");
33
+ const runnerPath = path.join(KIT_ROOT, "adapters/flow-runner/index.js");
34
+ const telemetryPath = path.join(KIT_ROOT, "adapters/flow-runner/telemetry.js");
35
+
36
+ const { DefaultKnowledgeStore } = await import(adapterPath);
37
+ const { KnowledgeFlowRunner } = await import(runnerPath);
38
+ const { KnowledgeTelemetry } = await import(telemetryPath);
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Helpers
42
+ // ---------------------------------------------------------------------------
43
+
44
+ function makeTempDir() {
45
+ return fs.mkdtempSync(path.join(os.tmpdir(), "knowledge-ingest-compile-"));
46
+ }
47
+
48
+ function makeStore(dir) {
49
+ return new DefaultKnowledgeStore({ storeRoot: dir });
50
+ }
51
+
52
+ function makeRunner(store, storeDir) {
53
+ // Telemetry sink lives in storeDir/.telemetry/full.jsonl for test isolation
54
+ return new KnowledgeFlowRunner({
55
+ store,
56
+ workspace: storeDir,
57
+ agent: "test-runner",
58
+ sessionId: "test-session-001",
59
+ });
60
+ }
61
+
62
+ function readTelemetryEvents(dir) {
63
+ const sinkPath = path.join(dir, ".telemetry", "full.jsonl");
64
+ if (!fs.existsSync(sinkPath)) return [];
65
+ return fs.readFileSync(sinkPath, "utf8")
66
+ .trim()
67
+ .split("\n")
68
+ .filter(Boolean)
69
+ .map((line) => JSON.parse(line));
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // AC2 + ingest: produces classified raw record
74
+ // ---------------------------------------------------------------------------
75
+
76
+ describe("ingest flow — classified raw capture (AC2 pre-condition)", () => {
77
+ let dir, store, runner;
78
+
79
+ before(() => {
80
+ dir = makeTempDir();
81
+ store = makeStore(dir);
82
+ runner = makeRunner(store, dir);
83
+ });
84
+ after(() => fs.rmSync(dir, { recursive: true, force: true }));
85
+
86
+ test("capture produces a raw record with non-empty category", async () => {
87
+ const result = await runner.capture(
88
+ "How to design a REST API with versioning",
89
+ { agent: "test-runner" }
90
+ );
91
+
92
+ assert.ok(result.id, "capture returns an id");
93
+ assert.ok(result.record, "capture returns the record");
94
+ assert.equal(result.record.type, "raw", "record type is raw");
95
+ assert.ok(result.record.category, "record has a non-empty category");
96
+ assert.match(
97
+ result.record.category,
98
+ /^[a-z0-9_-]+(\.[a-z0-9_-]+)*$/,
99
+ "category is a valid dot-separated string"
100
+ );
101
+ });
102
+
103
+ test("capture infers category from text when not provided", async () => {
104
+ const result = await runner.capture("Fix: NullPointerException in auth middleware");
105
+ assert.equal(result.record.type, "raw");
106
+ assert.ok(result.record.category, "category inferred");
107
+ });
108
+
109
+ test("capture accepts explicit category via meta", async () => {
110
+ const result = await runner.capture(
111
+ "Some raw text about anything",
112
+ { category: "research.notes", title: "Manual categorization test" }
113
+ );
114
+ assert.equal(result.record.category, "research.notes");
115
+ assert.equal(result.record.title, "Manual categorization test");
116
+ });
117
+
118
+ test("capture stores record retrievable by id", async () => {
119
+ const result = await runner.capture("Retrievability test content");
120
+ const fetched = await store.get(result.id);
121
+ assert.ok(fetched, "record is retrievable from store by id");
122
+ assert.equal(fetched.id, result.id);
123
+ assert.equal(fetched.type, "raw");
124
+ });
125
+
126
+ test("capture rejects empty rawText", async () => {
127
+ await assert.rejects(
128
+ () => runner.capture(""),
129
+ (err) => {
130
+ assert.equal(err.code, "MISSING_EVIDENCE", `Expected MISSING_EVIDENCE, got: ${err.code}`);
131
+ return true;
132
+ }
133
+ );
134
+ });
135
+
136
+ test("capture rejects whitespace-only rawText", async () => {
137
+ await assert.rejects(
138
+ () => runner.capture(" \n "),
139
+ (err) => {
140
+ assert.equal(err.code, "MISSING_EVIDENCE");
141
+ return true;
142
+ }
143
+ );
144
+ });
145
+ });
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // compile: rejects when provenance missing
149
+ // ---------------------------------------------------------------------------
150
+
151
+ describe("compile flow — provenance gate rejection", () => {
152
+ let dir, store, runner;
153
+
154
+ before(() => {
155
+ dir = makeTempDir();
156
+ store = makeStore(dir);
157
+ runner = makeRunner(store, dir);
158
+ });
159
+ after(() => fs.rmSync(dir, { recursive: true, force: true }));
160
+
161
+ test("compile rejects empty rawIds array", async () => {
162
+ await assert.rejects(
163
+ () => runner.compile([]),
164
+ (err) => {
165
+ assert.equal(err.code, "MISSING_EVIDENCE");
166
+ assert.match(err.message, /rawIds must be a non-empty array/);
167
+ return true;
168
+ }
169
+ );
170
+ });
171
+
172
+ test("compile rejects non-array rawIds", async () => {
173
+ await assert.rejects(
174
+ () => runner.compile(null),
175
+ (err) => {
176
+ assert.equal(err.code, "MISSING_EVIDENCE");
177
+ return true;
178
+ }
179
+ );
180
+ });
181
+
182
+ test("compile rejects rawId that does not exist in store", async () => {
183
+ await assert.rejects(
184
+ () => runner.compile(["nonexistent-id-xyz"]),
185
+ (err) => {
186
+ assert.equal(err.code, "MISSING_EVIDENCE");
187
+ assert.match(err.message, /not found/);
188
+ return true;
189
+ }
190
+ );
191
+ });
192
+
193
+ test("compile rejects record that is not type=raw", async () => {
194
+ // Create a concept record and try to compile it
195
+ const conceptId = await store.create({
196
+ type: "concept",
197
+ title: "Some concept",
198
+ body: "A concept body",
199
+ category: "engineering",
200
+ provenance: { agent: "test-runner" },
201
+ });
202
+
203
+ await assert.rejects(
204
+ () => runner.compile([conceptId]),
205
+ (err) => {
206
+ assert.equal(err.code, "MISSING_EVIDENCE");
207
+ assert.match(err.message, /expected "raw"/);
208
+ return true;
209
+ }
210
+ );
211
+ });
212
+ });
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // AC2: compile succeeds and provenance refs resolve
216
+ // ---------------------------------------------------------------------------
217
+
218
+ describe("compile flow — provenance refs resolve (AC2)", () => {
219
+ let dir, store, runner;
220
+ let rawId1, rawId2;
221
+
222
+ before(async () => {
223
+ dir = makeTempDir();
224
+ store = makeStore(dir);
225
+ runner = makeRunner(store, dir);
226
+
227
+ // Pre-create two raw records to compile
228
+ rawId1 = await store.create({
229
+ type: "raw",
230
+ title: "Research Note A",
231
+ body: "First raw capture about API design patterns.",
232
+ category: "research.notes",
233
+ provenance: { agent: "test-runner" },
234
+ });
235
+
236
+ rawId2 = await store.create({
237
+ type: "raw",
238
+ title: "Research Note B",
239
+ body: "Second raw capture about REST versioning strategies.",
240
+ category: "research.notes",
241
+ provenance: { agent: "test-runner" },
242
+ });
243
+ });
244
+ after(() => fs.rmSync(dir, { recursive: true, force: true }));
245
+
246
+ test("compile returns a compiled record id and record", async () => {
247
+ const result = await runner.compile([rawId1, rawId2], {
248
+ title: "API Design Synthesis",
249
+ category: "research.notes",
250
+ });
251
+
252
+ assert.ok(result.id, "compile returns an id");
253
+ assert.ok(result.record, "compile returns the record object");
254
+ assert.equal(result.record.type, "compiled");
255
+ });
256
+
257
+ test("compiled record has provenance.source_ids listing all consumed raws", async () => {
258
+ const result = await runner.compile([rawId1, rawId2], {
259
+ title: "Provenance Check Compilation",
260
+ });
261
+
262
+ const record = result.record;
263
+ assert.ok(record.provenance, "record has provenance");
264
+ assert.ok(Array.isArray(record.provenance.source_ids), "provenance.source_ids is an array");
265
+ assert.ok(
266
+ record.provenance.source_ids.includes(rawId1),
267
+ "provenance.source_ids includes rawId1"
268
+ );
269
+ assert.ok(
270
+ record.provenance.source_ids.includes(rawId2),
271
+ "provenance.source_ids includes rawId2"
272
+ );
273
+ assert.equal(
274
+ record.provenance.source_ids.length,
275
+ 2,
276
+ "provenance.source_ids covers all consumed raws"
277
+ );
278
+ });
279
+
280
+ test("compiled record has source links to every consumed raw", async () => {
281
+ const result = await runner.compile([rawId1, rawId2], {
282
+ title: "Source Links Check",
283
+ });
284
+
285
+ const links = result.record.links || [];
286
+ const sourceLinks = links.filter((l) => l.kind === "source");
287
+ const linkedIds = sourceLinks.map((l) => l.target_id);
288
+
289
+ assert.ok(linkedIds.includes(rawId1), "source link to rawId1 present");
290
+ assert.ok(linkedIds.includes(rawId2), "source link to rawId2 present");
291
+ });
292
+
293
+ test("all provenance refs resolve via store.get()", async () => {
294
+ const result = await runner.compile([rawId1, rawId2], {
295
+ title: "Ref Resolution Check",
296
+ });
297
+
298
+ const sourceIds = result.record.provenance.source_ids;
299
+ for (const srcId of sourceIds) {
300
+ const ref = await store.get(srcId);
301
+ assert.ok(ref, `provenance ref ${srcId} resolves to a record`);
302
+ assert.equal(ref.type, "raw", `provenance ref ${srcId} is type=raw`);
303
+ }
304
+ });
305
+
306
+ test("graph index reflects source links for compiled record", async () => {
307
+ const result = await runner.compile([rawId1, rawId2], {
308
+ title: "Graph Index Check",
309
+ });
310
+
311
+ const { forward } = await store.getLinks(result.id);
312
+ const sourceLinks = forward.filter((l) => l.kind === "source");
313
+
314
+ assert.ok(
315
+ sourceLinks.some((l) => l.target_id === rawId1),
316
+ "graph index has forward source link to rawId1"
317
+ );
318
+ assert.ok(
319
+ sourceLinks.some((l) => l.target_id === rawId2),
320
+ "graph index has forward source link to rawId2"
321
+ );
322
+ });
323
+
324
+ test("ingest → compile end-to-end: captured raws compile with provenance", async () => {
325
+ const r1 = await runner.capture("End-to-end test: capture one about databases");
326
+ const r2 = await runner.capture("End-to-end test: capture two about caching");
327
+
328
+ const compiled = await runner.compile([r1.id, r2.id], {
329
+ title: "End-to-End Compiled Note",
330
+ });
331
+
332
+ assert.equal(compiled.record.type, "compiled");
333
+ assert.ok(compiled.record.provenance.source_ids.includes(r1.id));
334
+ assert.ok(compiled.record.provenance.source_ids.includes(r2.id));
335
+
336
+ // All refs resolve
337
+ for (const srcId of compiled.record.provenance.source_ids) {
338
+ const ref = await store.get(srcId);
339
+ assert.ok(ref, `ref ${srcId} resolves`);
340
+ }
341
+ });
342
+
343
+ test("single-raw compile works and provenance covers the one raw", async () => {
344
+ const result = await runner.compile([rawId1], {
345
+ title: "Single Source Compile",
346
+ });
347
+
348
+ assert.equal(result.record.type, "compiled");
349
+ assert.deepEqual(result.record.provenance.source_ids, [rawId1]);
350
+ const sourceLinks = (result.record.links || []).filter((l) => l.kind === "source");
351
+ assert.ok(sourceLinks.some((l) => l.target_id === rawId1));
352
+ });
353
+ });
354
+
355
+ // ---------------------------------------------------------------------------
356
+ // AC3: canonical telemetry events emitted at gates
357
+ // ---------------------------------------------------------------------------
358
+
359
+ describe("telemetry — canonical events at gates (AC3)", () => {
360
+ let dir, store, runner;
361
+
362
+ before(() => {
363
+ dir = makeTempDir();
364
+ store = makeStore(dir);
365
+ runner = makeRunner(store, dir);
366
+ });
367
+ after(() => fs.rmSync(dir, { recursive: true, force: true }));
368
+
369
+ test("ingest emits telemetry events at classify-gate and route-gate", async () => {
370
+ await runner.capture("Telemetry test: capture for ingest gates");
371
+
372
+ const events = readTelemetryEvents(dir);
373
+ assert.ok(events.length > 0, "telemetry events were emitted");
374
+
375
+ // Check that events have correct schema_version
376
+ for (const ev of events) {
377
+ assert.equal(ev.schema_version, "0.3.0", "event has schema_version 0.3.0");
378
+ assert.ok(ev.timestamp, "event has timestamp");
379
+ assert.ok(ev.session_id, "event has session_id");
380
+ assert.ok(ev.event_id, "event has event_id");
381
+ assert.ok(ev.event_type, "event has event_type");
382
+ assert.ok(ev.agent, "event has agent block");
383
+ assert.ok(ev.hook, "event has hook block");
384
+ }
385
+ });
386
+
387
+ test("ingest events include classify-gate entry and exit", async () => {
388
+ // Use a fresh dir for this test to get isolated events
389
+ const testDir = makeTempDir();
390
+ const testStore = makeStore(testDir);
391
+ const testRunner = makeRunner(testStore, testDir);
392
+
393
+ try {
394
+ await testRunner.capture("Classify gate telemetry test");
395
+
396
+ const events = readTelemetryEvents(testDir);
397
+ const gateEvents = events.filter(
398
+ (ev) => ev.tool?.name?.includes("classify-gate")
399
+ );
400
+
401
+ assert.ok(gateEvents.length >= 2, "at least 2 classify-gate events (entry + exit)");
402
+
403
+ const entryEvent = gateEvents.find((ev) => ev.event_type === "tool.invoke");
404
+ const exitEvent = gateEvents.find((ev) => ev.event_type === "tool.result");
405
+
406
+ assert.ok(entryEvent, "classify-gate entry event (tool.invoke) emitted");
407
+ assert.ok(exitEvent, "classify-gate exit event (tool.result) emitted");
408
+ } finally {
409
+ fs.rmSync(testDir, { recursive: true, force: true });
410
+ }
411
+ });
412
+
413
+ test("compile events include compile-gate entry and exit", async () => {
414
+ const testDir = makeTempDir();
415
+ const testStore = makeStore(testDir);
416
+ const testRunner = makeRunner(testStore, testDir);
417
+
418
+ try {
419
+ const rawId = await testStore.create({
420
+ type: "raw",
421
+ title: "Compile gate tel test raw",
422
+ body: "raw content for telemetry test",
423
+ category: "test",
424
+ provenance: { agent: "test-runner" },
425
+ });
426
+
427
+ await testRunner.compile([rawId], { title: "Compile Gate Telemetry Test" });
428
+
429
+ const events = readTelemetryEvents(testDir);
430
+ const compileGateEvents = events.filter(
431
+ (ev) => ev.tool?.name?.includes("compile-gate")
432
+ );
433
+
434
+ assert.ok(compileGateEvents.length >= 2, "at least 2 compile-gate events (entry + exit)");
435
+
436
+ const entryEvent = compileGateEvents.find((ev) => ev.event_type === "tool.invoke");
437
+ const exitEvent = compileGateEvents.find((ev) => ev.event_type === "tool.result");
438
+
439
+ assert.ok(entryEvent, "compile-gate entry event (tool.invoke) emitted");
440
+ assert.ok(exitEvent, "compile-gate exit event (tool.result) emitted");
441
+ } finally {
442
+ fs.rmSync(testDir, { recursive: true, force: true });
443
+ }
444
+ });
445
+
446
+ test("compile events include link-gate entry and exit", async () => {
447
+ const testDir = makeTempDir();
448
+ const testStore = makeStore(testDir);
449
+ const testRunner = makeRunner(testStore, testDir);
450
+
451
+ try {
452
+ const rawId = await testStore.create({
453
+ type: "raw",
454
+ title: "Link gate tel test raw",
455
+ body: "raw content",
456
+ category: "test",
457
+ provenance: { agent: "test-runner" },
458
+ });
459
+
460
+ await testRunner.compile([rawId], { title: "Link Gate Telemetry Test" });
461
+
462
+ const events = readTelemetryEvents(testDir);
463
+ const linkGateEvents = events.filter(
464
+ (ev) => ev.tool?.name?.includes("link-gate")
465
+ );
466
+
467
+ assert.ok(linkGateEvents.length >= 2, "at least 2 link-gate events (entry + exit)");
468
+ } finally {
469
+ fs.rmSync(testDir, { recursive: true, force: true });
470
+ }
471
+ });
472
+
473
+ test("telemetry events have correct agent block shape", async () => {
474
+ const testDir = makeTempDir();
475
+ const testStore = makeStore(testDir);
476
+ const testRunner = new KnowledgeFlowRunner({
477
+ store: testStore,
478
+ workspace: testDir,
479
+ agent: "specific-test-agent",
480
+ sessionId: "specific-session-123",
481
+ });
482
+
483
+ try {
484
+ await testRunner.capture("Agent block shape test");
485
+ const events = readTelemetryEvents(testDir);
486
+
487
+ assert.ok(events.length > 0, "events were emitted");
488
+ for (const ev of events) {
489
+ assert.equal(ev.agent.name, "specific-test-agent", "agent.name matches constructor arg");
490
+ assert.equal(ev.agent.runtime, "knowledge-kit", "agent.runtime is knowledge-kit");
491
+ assert.equal(ev.session_id, "specific-session-123", "session_id matches constructor arg");
492
+ }
493
+ } finally {
494
+ fs.rmSync(testDir, { recursive: true, force: true });
495
+ }
496
+ });
497
+
498
+ test("telemetry sink file is JSONL (one JSON object per line)", async () => {
499
+ const testDir = makeTempDir();
500
+ const testStore = makeStore(testDir);
501
+ const testRunner = makeRunner(testStore, testDir);
502
+
503
+ try {
504
+ await testRunner.capture("JSONL format test");
505
+
506
+ const sinkPath = path.join(testDir, ".telemetry", "full.jsonl");
507
+ assert.ok(fs.existsSync(sinkPath), "telemetry sink file exists");
508
+
509
+ const lines = fs.readFileSync(sinkPath, "utf8")
510
+ .trim()
511
+ .split("\n")
512
+ .filter(Boolean);
513
+
514
+ assert.ok(lines.length > 0, "at least one line in JSONL file");
515
+ for (const line of lines) {
516
+ const parsed = JSON.parse(line); // throws if invalid JSON
517
+ assert.ok(typeof parsed === "object" && parsed !== null, "each line is a JSON object");
518
+ }
519
+ } finally {
520
+ fs.rmSync(testDir, { recursive: true, force: true });
521
+ }
522
+ });
523
+ });
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // Telemetry helper unit tests
527
+ // ---------------------------------------------------------------------------
528
+
529
+ describe("KnowledgeTelemetry — helper unit tests", () => {
530
+ let dir;
531
+
532
+ before(() => { dir = makeTempDir(); });
533
+ after(() => fs.rmSync(dir, { recursive: true, force: true }));
534
+
535
+ test("emit writes a line to the JSONL sink", () => {
536
+ const tel = new KnowledgeTelemetry({ workspace: dir, agentName: "tel-test" });
537
+ const ev = tel.emit("preToolUse", { tool: { name: "test-tool", input: null } });
538
+
539
+ assert.ok(ev, "emit returns the event");
540
+ assert.equal(ev.event_type, "tool.invoke");
541
+ assert.equal(ev.schema_version, "0.3.0");
542
+
543
+ const sinkPath = path.join(dir, ".telemetry", "full.jsonl");
544
+ assert.ok(fs.existsSync(sinkPath), "JSONL file was created");
545
+ });
546
+
547
+ test("emitGate produces a tool.invoke event", () => {
548
+ const tel = new KnowledgeTelemetry({ workspace: dir, agentName: "tel-test" });
549
+ const ev = tel.emitGate("test.flow", "my-gate", { key: "value" });
550
+
551
+ assert.equal(ev.event_type, "tool.invoke");
552
+ assert.equal(ev.tool.name, "test.flow.my-gate");
553
+ assert.equal(ev.tool.normalized_name, "flow.gate");
554
+ assert.deepEqual(ev.tool.input, { key: "value" });
555
+ });
556
+
557
+ test("emitGateResult produces a tool.result event", () => {
558
+ const tel = new KnowledgeTelemetry({ workspace: dir, agentName: "tel-test" });
559
+ const ev = tel.emitGateResult("test.flow", "my-gate", { status: "pass" });
560
+
561
+ assert.equal(ev.event_type, "tool.result");
562
+ assert.deepEqual(ev.tool.output, { status: "pass" });
563
+ });
564
+
565
+ test("emit fails open on bad sink path (no throw)", () => {
566
+ const tel = new KnowledgeTelemetry({
567
+ workspace: "/nonexistent/path/that/cannot/be/created/xyz123",
568
+ agentName: "fail-open-test",
569
+ });
570
+ // Should not throw even if directory creation fails
571
+ const ev = tel.emit("preToolUse", {});
572
+ assert.ok(ev, "emit returns event even on sink write failure");
573
+ });
574
+ });