knolo-core 0.3.0 → 3.1.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.
package/DOCS.md CHANGED
@@ -71,8 +71,16 @@ npx knolo docs.json knowledge.knolo
71
71
 
72
72
  # semantic-enabled build (embeddings JSON + model id)
73
73
  npx knolo docs.json knowledge.knolo --embeddings embeddings.json --model-id text-embedding-3-small
74
+
75
+ # embed agents from a local directory (.json/.yml/.yaml)
76
+ npx knolo docs.json knowledge.knolo --agents ./examples/agents
74
77
  ```
75
78
 
79
+
80
+ ### Agents and namespace binding
81
+
82
+ When agent definitions are embedded into `meta.agents`, `resolveAgent(pack, { agentId, query, patch })` enforces **strict namespace binding**: `retrievalDefaults.namespace` always wins over caller `query.namespace`. This keeps retrieval deterministic and on-policy for each agent.
83
+
76
84
  ---
77
85
 
78
86
  ## Concepts
@@ -176,9 +184,15 @@ const hits: Hit[] = query(pack, '“react native bridge” throttling', {
176
184
  ### Semantic helper ergonomics
177
185
 
178
186
  ```ts
179
- import { hasSemantic, validateSemanticQueryOptions } from "knolo-core";
187
+ import { hasSemantic, validateQueryOptions, validateSemanticQueryOptions } from "knolo-core";
180
188
 
181
189
  if (hasSemantic(pack)) {
190
+ validateQueryOptions({
191
+ topK: 10,
192
+ namespace: "mobile",
193
+ queryExpansion: { enabled: true, docs: 3, terms: 4 },
194
+ });
195
+
182
196
  validateSemanticQueryOptions({
183
197
  enabled: true,
184
198
  topN: 40,
@@ -188,7 +202,7 @@ if (hasSemantic(pack)) {
188
202
  }
189
203
  ```
190
204
 
191
- `validateSemanticQueryOptions(...)` throws useful errors for invalid option types/ranges (`topN`, `minLexConfidence`, blend weights, missing `Float32Array` embedding type).
205
+ `validateQueryOptions(...)` and `validateSemanticQueryOptions(...)` throw useful errors for invalid option types/ranges (for example `topK`, `queryExpansion.docs`, `topN`, `minLexConfidence`, blend weights, and missing `Float32Array` embedding types).
192
206
 
193
207
  **What the ranker does**
194
208
 
package/README.md CHANGED
@@ -16,7 +16,7 @@ Build a portable `.knolo` pack and run deterministic lexical retrieval with opti
16
16
 
17
17
  ---
18
18
 
19
- ## ✨ What’s in v0.3.0
19
+ ## ✨ What’s in v0.3.1
20
20
 
21
21
  - **Deterministic lexical quality upgrades**
22
22
  - required phrase enforcement (quoted + `requirePhrases`)
@@ -31,6 +31,10 @@ Build a portable `.knolo` pack and run deterministic lexical retrieval with opti
31
31
  - **Stability & diversity**
32
32
  - near-duplicate suppression + MMR diversity
33
33
  - KNS tie-break signal for stable close-score ordering
34
+ - **Agent/runtime utilities**
35
+ - embedded agent registries with strict namespace binding
36
+ - tool call parsing + policy gating helpers
37
+ - provider-agnostic routing profile + route decision validators
34
38
  - **Portable packs**
35
39
  - single `.knolo` artifact
36
40
  - semantic payload embedded directly in pack when enabled
@@ -54,26 +58,34 @@ npm run build
54
58
 
55
59
  ---
56
60
 
61
+ ## 🧪 Playground
62
+
63
+ Try KnoLo Core directly in your browser with the hosted playground:
64
+
65
+ - https://playgrounds.knolo.dev
66
+
67
+ ---
68
+
57
69
  ## 🚀 Quickstart
58
70
 
59
71
  ### 1) Build + mount + query
60
72
 
61
73
  ```ts
62
- import { buildPack, mountPack, query, makeContextPatch } from "knolo-core";
74
+ import { buildPack, mountPack, query, makeContextPatch } from 'knolo-core';
63
75
 
64
76
  const docs = [
65
77
  {
66
- id: "bridge-guide",
67
- namespace: "mobile",
68
- heading: "React Native Bridge",
69
- text: "The bridge sends messages between JS and native modules. Throttling limits event frequency."
78
+ id: 'bridge-guide',
79
+ namespace: 'mobile',
80
+ heading: 'React Native Bridge',
81
+ text: 'The bridge sends messages between JS and native modules. Throttling limits event frequency.',
70
82
  },
71
83
  {
72
- id: "perf-notes",
73
- namespace: "mobile",
74
- heading: "Debounce vs Throttle",
75
- text: "Debounce waits for silence; throttle enforces a maximum trigger rate."
76
- }
84
+ id: 'perf-notes',
85
+ namespace: 'mobile',
86
+ heading: 'Debounce vs Throttle',
87
+ text: 'Debounce waits for silence; throttle enforces a maximum trigger rate.',
88
+ },
77
89
  ];
78
90
 
79
91
  const bytes = await buildPack(docs);
@@ -81,11 +93,11 @@ const kb = await mountPack({ src: bytes });
81
93
 
82
94
  const hits = query(kb, '"react native" throttle', {
83
95
  topK: 5,
84
- requirePhrases: ["maximum trigger rate"],
85
- namespace: "mobile"
96
+ requirePhrases: ['maximum trigger rate'],
97
+ namespace: 'mobile',
86
98
  });
87
99
 
88
- const patch = makeContextPatch(hits, { budget: "small" });
100
+ const patch = makeContextPatch(hits, { budget: 'small' });
89
101
  console.log(hits, patch);
90
102
  ```
91
103
 
@@ -95,34 +107,45 @@ console.log(hits, patch);
95
107
 
96
108
  ```json
97
109
  [
98
- { "id": "guide", "heading": "Guide", "text": "Install deps.\n\n## Throttle\nLimit event frequency." },
99
- { "id": "faq", "heading": "FAQ", "text": "What is throttling? It reduces event frequency." }
110
+ {
111
+ "id": "guide",
112
+ "heading": "Guide",
113
+ "text": "Install deps.\n\n## Throttle\nLimit event frequency."
114
+ },
115
+ {
116
+ "id": "faq",
117
+ "heading": "FAQ",
118
+ "text": "What is throttling? It reduces event frequency."
119
+ }
100
120
  ]
101
121
  ```
102
122
 
103
123
  ```bash
104
124
  npx knolo docs.json knowledge.knolo
125
+
126
+ # embed agents from a local directory (.json/.yml/.yaml)
127
+ npx knolo docs.json knowledge.knolo --agents ./examples/agents
105
128
  ```
106
129
 
107
130
  Then query in app:
108
131
 
109
132
  ```ts
110
- import { mountPack, query } from "knolo-core";
133
+ import { mountPack, query } from 'knolo-core';
111
134
 
112
- const kb = await mountPack({ src: "./knowledge.knolo" });
113
- const hits = query(kb, "throttle events", { topK: 3 });
135
+ const kb = await mountPack({ src: './knowledge.knolo' });
136
+ const hits = query(kb, 'throttle events', { topK: 3 });
114
137
  ```
115
138
 
116
139
  ---
117
140
 
118
- ## 🔀 Hybrid retrieval with embeddings (recommended direction)
141
+ ## 🔀 Hybrid retrieval with embeddings (optional)
119
142
 
120
143
  KnoLo’s core retrieval remains lexical-first and deterministic. Semantic signals are added as an **optional rerank stage** when lexical confidence is low (or forced).
121
144
 
122
145
  ### Build a semantic-enabled pack
123
146
 
124
147
  ```ts
125
- import { buildPack } from "knolo-core";
148
+ import { buildPack } from 'knolo-core';
126
149
 
127
150
  // embeddings must align 1:1 with docs/block order
128
151
  const embeddings: Float32Array[] = await embedDocumentsInOrder(docs);
@@ -130,32 +153,32 @@ const embeddings: Float32Array[] = await embedDocumentsInOrder(docs);
130
153
  const bytes = await buildPack(docs, {
131
154
  semantic: {
132
155
  enabled: true,
133
- modelId: "text-embedding-3-small",
156
+ modelId: 'text-embedding-3-small',
134
157
  embeddings,
135
- quantization: { type: "int8_l2norm", perVectorScale: true }
136
- }
158
+ quantization: { type: 'int8_l2norm', perVectorScale: true },
159
+ },
137
160
  });
138
161
  ```
139
162
 
140
163
  ### Query with semantic rerank
141
164
 
142
165
  ```ts
143
- import { mountPack, query, hasSemantic } from "knolo-core";
166
+ import { mountPack, query, hasSemantic } from 'knolo-core';
144
167
 
145
168
  const kb = await mountPack({ src: bytes });
146
- const queryEmbedding = await embedQuery("react native bridge throttling");
169
+ const queryEmbedding = await embedQuery('react native bridge throttling');
147
170
 
148
- const hits = query(kb, "react native bridge throttling", {
171
+ const hits = query(kb, 'react native bridge throttling', {
149
172
  topK: 8,
150
173
  semantic: {
151
174
  enabled: hasSemantic(kb),
152
- mode: "rerank",
175
+ mode: 'rerank',
153
176
  topN: 50,
154
177
  minLexConfidence: 0.35,
155
178
  blend: { enabled: true, wLex: 0.75, wSem: 0.25 },
156
179
  queryEmbedding,
157
- force: false
158
- }
180
+ force: false,
181
+ },
159
182
  });
160
183
  ```
161
184
 
@@ -165,8 +188,8 @@ const hits = query(kb, "react native bridge throttling", {
165
188
  import {
166
189
  quantizeEmbeddingInt8L2Norm,
167
190
  encodeScaleF16,
168
- decodeScaleF16
169
- } from "knolo-core";
191
+ decodeScaleF16,
192
+ } from 'knolo-core';
170
193
 
171
194
  const { q, scale } = quantizeEmbeddingInt8L2Norm(queryEmbedding);
172
195
  const packed = encodeScaleF16(scale);
@@ -188,25 +211,74 @@ type BuildInputDoc = {
188
211
  };
189
212
 
190
213
  type BuildPackOptions = {
214
+ agents?: AgentRegistry | AgentDefinitionV1[];
191
215
  semantic?: {
192
216
  enabled: boolean;
193
217
  modelId: string;
194
218
  embeddings: Float32Array[];
195
219
  quantization?: {
196
- type: "int8_l2norm";
220
+ type: 'int8_l2norm';
197
221
  perVectorScale?: true;
198
222
  };
199
223
  };
200
224
  };
201
225
  ```
202
226
 
227
+ ### Agents in pack metadata
228
+
229
+ Agents are optional and embedded in `meta.agents` so a single `.knolo` artifact can ship retrieval behavior + prompt defaults on-prem. Agent registries are validated once at `mountPack()` time, so invalid embedded registries fail fast during mount.
230
+
231
+ Agent namespace binding is **strict**: when `resolveAgent()` composes retrieval options, `retrievalDefaults.namespace` is always enforced and caller-provided `query.namespace` is ignored.
232
+
233
+ ```ts
234
+ type AgentPromptTemplate = string[] | { format: 'markdown'; template: string };
235
+
236
+ type AgentRegistry = {
237
+ version: 1;
238
+ agents: AgentDefinitionV1[];
239
+ };
240
+
241
+ type PackMeta = {
242
+ version: number;
243
+ stats: { docs: number; blocks: number; terms: number; avgBlockLen?: number };
244
+ agents?: AgentRegistry;
245
+ };
246
+
247
+ type AgentDefinitionV1 = {
248
+ id: string;
249
+ version: 1;
250
+ name?: string;
251
+ description?: string;
252
+ systemPrompt: AgentPromptTemplate;
253
+ retrievalDefaults: {
254
+ namespace: string[]; // required
255
+ topK?: number;
256
+ queryExpansion?: QueryOptions['queryExpansion'];
257
+ semantic?: Omit<
258
+ NonNullable<QueryOptions['semantic']>,
259
+ 'queryEmbedding' | 'enabled' | 'force'
260
+ > & { enabled?: boolean };
261
+ minScore?: number;
262
+ requirePhrases?: string[];
263
+ source?: string[];
264
+ };
265
+ toolPolicy?: { mode: 'allow' | 'deny'; tools: string[] };
266
+ metadata?: Record<string, string | number | boolean | null>;
267
+ };
268
+ ```
269
+
203
270
  ### `mountPack({ src }) => Promise<Pack>`
204
271
 
205
272
  ```ts
206
273
  type Pack = {
207
274
  meta: {
208
275
  version: number;
209
- stats: { docs: number; blocks: number; terms: number; avgBlockLen?: number };
276
+ stats: {
277
+ docs: number;
278
+ blocks: number;
279
+ terms: number;
280
+ avgBlockLen?: number;
281
+ };
210
282
  };
211
283
  lexicon: Map<string, number>;
212
284
  postings: Uint32Array;
@@ -219,7 +291,7 @@ type Pack = {
219
291
  version: 1;
220
292
  modelId: string;
221
293
  dims: number;
222
- encoding: "int8_l2norm";
294
+ encoding: 'int8_l2norm';
223
295
  perVectorScale: boolean;
224
296
  vecs: Int8Array;
225
297
  scales?: Uint16Array;
@@ -245,7 +317,7 @@ type QueryOptions = {
245
317
  };
246
318
  semantic?: {
247
319
  enabled?: boolean;
248
- mode?: "rerank";
320
+ mode?: 'rerank';
249
321
  topN?: number;
250
322
  minLexConfidence?: number;
251
323
  blend?: {
@@ -267,6 +339,264 @@ type Hit = {
267
339
  };
268
340
  ```
269
341
 
342
+ ### Agent runtime helpers
343
+
344
+ - `listAgents(pack) => string[]`
345
+ - `getAgent(pack, agentId) => AgentDefinitionV1 | undefined`
346
+ - `resolveAgent(pack, { agentId, query?, patch? }) => { agent, systemPrompt, retrievalOptions }`
347
+ - `buildSystemPrompt(agent, patch?) => string`
348
+ - `isToolAllowed(agent, toolId) => boolean` (defaults to allow-all when no `toolPolicy`)
349
+ - `assertToolAllowed(agent, toolId) => void` (throws deterministic error when blocked)
350
+ - `parseToolCallV1FromText(text) => ToolCallV1 | null` (safe parser for model outputs)
351
+ - `assertToolCallAllowed(agent, call) => void` (policy gate for parsed calls)
352
+ - `isToolCallV1(value) / isToolResultV1(value)` (runtime-safe type guards)
353
+ - `getAgentRoutingProfileV1(agent) => AgentRoutingProfileV1`
354
+ - `getPackRoutingProfilesV1(pack) => AgentRoutingProfileV1[]`
355
+ - `isRouteDecisionV1(value) => boolean` (strict contract guard for router output)
356
+ - `validateRouteDecisionV1(decision, registryById) => { ok: true } | { ok: false; error: string }`
357
+ - `selectAgentIdFromRouteDecisionV1(decision, registryById, { fallbackAgentId? }) => { agentId, reason }`
358
+
359
+ ### Routing discoverability conventions
360
+
361
+ To make an agent easier to route, use these optional `metadata` keys on `AgentDefinitionV1`:
362
+
363
+ - `tags`: comma-separated (`"shopping,checkout"`) or JSON array string (`"[\"shopping\",\"checkout\"]"`)
364
+ - `examples`: comma-separated, newline-separated, or JSON array string
365
+ - `capabilities`: comma-separated, newline-separated, or JSON array string
366
+ - `heading`: short UI heading shown in routing cards
367
+
368
+ `knolo-core` parses these into a compact routing profile with trimming + dedupe + caps and never throws on bad metadata formats.
369
+
370
+ ```ts
371
+ type AgentRoutingProfileV1 = {
372
+ agentId: string;
373
+ namespace?: string;
374
+ heading?: string;
375
+ description?: string;
376
+ tags: string[];
377
+ examples: string[];
378
+ capabilities: string[];
379
+ toolPolicy?: unknown;
380
+ toolPolicySummary?: {
381
+ mode: 'allow_all' | 'deny_all' | 'mixed' | 'unknown';
382
+ allowed?: string[];
383
+ denied?: string[];
384
+ };
385
+ };
386
+ ```
387
+
388
+ Example profile payload:
389
+
390
+ ```json
391
+ {
392
+ "agentId": "shopping.agent",
393
+ "namespace": "shopping",
394
+ "heading": "Shopping Assistant",
395
+ "description": "Handles product lookup, checkout help, and order tracking.",
396
+ "tags": ["shopping", "checkout", "order-status"],
397
+ "examples": ["track my order", "find running shoes under $120"],
398
+ "capabilities": ["catalog_search", "order_lookup"],
399
+ "toolPolicySummary": {
400
+ "mode": "mixed",
401
+ "allowed": ["search_docs", "order_lookup"]
402
+ }
403
+ }
404
+ ```
405
+
406
+ ### Route decision contract
407
+
408
+ `knolo-core` does not call Ollama (or any model provider). A runtime can call any router model, then validate the output with this contract:
409
+
410
+ ```ts
411
+ type RouteCandidateV1 = {
412
+ agentId: string;
413
+ score: number; // 0..1
414
+ why?: string;
415
+ };
416
+
417
+ type RouteDecisionV1 = {
418
+ type: 'route_decision';
419
+ intent?: string;
420
+ entities?: Record<string, unknown>;
421
+ candidates: RouteCandidateV1[];
422
+ selected: string;
423
+ needsTools?: string[];
424
+ risk?: 'low' | 'med' | 'high';
425
+ };
426
+ ```
427
+
428
+ JSON example:
429
+
430
+ ```json
431
+ {
432
+ "type": "route_decision",
433
+ "intent": "order_tracking",
434
+ "entities": { "orderId": "A-1023" },
435
+ "candidates": [
436
+ { "agentId": "shopping.agent", "score": 0.91, "why": "Order-related intent" },
437
+ { "agentId": "returns.agent", "score": 0.37 }
438
+ ],
439
+ "selected": "shopping.agent",
440
+ "needsTools": ["order_lookup"],
441
+ "risk": "low"
442
+ }
443
+ ```
444
+
445
+ Validation and selection notes:
446
+
447
+ - `isRouteDecisionV1(...)` is strict and rejects malformed payloads.
448
+ - `validateRouteDecisionV1(...)` requires `selected` and every candidate `agentId` to exist in the mounted registry and rejects duplicate candidate ids.
449
+ - `selectAgentIdFromRouteDecisionV1(...)` is deterministic and never throws:
450
+ 1. use `selected` if registered,
451
+ 2. else highest-score registered candidate,
452
+ 3. else caller `fallbackAgentId` if valid,
453
+ 4. else lexicographically first registered agent id.
454
+
455
+ ### Router runtime flow (provider-agnostic)
456
+
457
+ 1. Receive user input text.
458
+ 2. Build routing profiles from mounted pack agents via `getPackRoutingProfilesV1(pack)`.
459
+ 3. Send input + profiles to your router model (Ollama or any provider) outside `knolo-core`.
460
+ 4. Parse model output JSON and gate with `isRouteDecisionV1`.
461
+ 5. Validate against mounted registry with `validateRouteDecisionV1`.
462
+ 6. Pick final agent using `selectAgentIdFromRouteDecisionV1`.
463
+ 7. Call `resolveAgent(pack, { agentId, ... })` and run your existing loop.
464
+
465
+ ### Tool call + result contracts
466
+
467
+ ```ts
468
+ type ToolCallV1 = {
469
+ type: 'tool_call';
470
+ callId: string;
471
+ tool: string;
472
+ args: Record<string, unknown>;
473
+ };
474
+
475
+ type ToolResultV1 = {
476
+ type: 'tool_result';
477
+ callId: string;
478
+ tool: string;
479
+ ok: boolean;
480
+ output?: unknown; // when ok=true
481
+ error?: { message: string; code?: string; details?: unknown }; // when ok=false
482
+ };
483
+ ```
484
+
485
+ JSON examples:
486
+
487
+ ```json
488
+ {
489
+ "type": "tool_call",
490
+ "callId": "call-42",
491
+ "tool": "search_docs",
492
+ "args": { "query": "bridge throttle" }
493
+ }
494
+ ```
495
+
496
+ ```json
497
+ {
498
+ "type": "tool_result",
499
+ "callId": "call-42",
500
+ "tool": "search_docs",
501
+ "ok": true,
502
+ "output": { "hits": [{ "id": "mobile-doc" }] }
503
+ }
504
+ ```
505
+
506
+ ### Runtime loop shape (model-agnostic)
507
+
508
+ 1. Run model with current conversation state.
509
+ 2. Parse text output with `parseToolCallV1FromText(...)`.
510
+ 3. If parsed: gate with `assertToolCallAllowed(resolved.agent, call)`.
511
+ 4. Runtime executes the tool and creates `ToolResultV1`.
512
+ 5. Feed the tool result back into the conversation and continue until completion.
513
+
514
+ ### Trace events for timeline UIs
515
+
516
+ ```ts
517
+ type TraceEventV1 =
518
+ | {
519
+ type: 'route.requested';
520
+ ts: string;
521
+ text: string;
522
+ agentCount: number;
523
+ }
524
+ | {
525
+ type: 'route.decided';
526
+ ts: string;
527
+ decision: RouteDecisionV1;
528
+ selectedAgentId: string;
529
+ }
530
+ | { type: 'agent.selected'; ts: string; agentId: string; namespace?: string }
531
+ | {
532
+ type: 'prompt.resolved';
533
+ ts: string;
534
+ agentId: string;
535
+ promptHash?: string;
536
+ patchKeys?: string[];
537
+ }
538
+ | { type: 'tool.requested'; ts: string; agentId: string; call: ToolCallV1 }
539
+ | {
540
+ type: 'tool.executed';
541
+ ts: string;
542
+ agentId: string;
543
+ result: ToolResultV1;
544
+ durationMs?: number;
545
+ }
546
+ | {
547
+ type: 'run.completed';
548
+ ts: string;
549
+ agentId: string;
550
+ status: 'ok' | 'error';
551
+ };
552
+ ```
553
+
554
+ Helpers: `nowIso()` for timestamps and `createTrace()` for lightweight trace collection.
555
+
556
+ ### Build a pack with agents and resolve at runtime
557
+
558
+ ```ts
559
+ import {
560
+ buildPack,
561
+ mountPack,
562
+ resolveAgent,
563
+ query,
564
+ isToolAllowed,
565
+ assertToolAllowed,
566
+ } from 'knolo-core';
567
+
568
+ const bytes = await buildPack(docs, {
569
+ agents: [
570
+ {
571
+ id: 'mobile.agent',
572
+ version: 1,
573
+ systemPrompt: {
574
+ format: 'markdown',
575
+ template: 'You are {{team}} support.',
576
+ },
577
+ retrievalDefaults: { namespace: ['mobile'], topK: 5 },
578
+ toolPolicy: { mode: 'allow', tools: ['search_docs'] },
579
+ },
580
+ ],
581
+ });
582
+
583
+ const pack = await mountPack({ src: bytes });
584
+ const resolved = resolveAgent(pack, {
585
+ agentId: 'mobile.agent',
586
+ patch: { team: 'mobile' },
587
+ query: { namespace: ['backend'], topK: 8 },
588
+ });
589
+
590
+ console.log(resolved.retrievalOptions.namespace); // ['mobile'] (strict binding)
591
+
592
+ if (isToolAllowed(resolved.agent, 'search_docs')) {
593
+ // invoke search_docs
594
+ }
595
+ assertToolAllowed(resolved.agent, 'search_docs');
596
+
597
+ const hits = query(pack, 'bridge throttle', resolved.retrievalOptions);
598
+ ```
599
+
270
600
  ### `makeContextPatch(hits, { budget }) => ContextPatch`
271
601
 
272
602
  Budgets: `"mini" | "small" | "full"`
@@ -278,10 +608,10 @@ Budgets: `"mini" | "small" | "full"`
278
608
  ### Namespace + source filtering
279
609
 
280
610
  ```ts
281
- const hits = query(kb, "retry backoff", {
282
- namespace: ["sdk", "api"],
283
- source: ["errors-guide", "http-reference"],
284
- topK: 6
611
+ const hits = query(kb, 'retry backoff', {
612
+ namespace: ['sdk', 'api'],
613
+ source: ['errors-guide', 'http-reference'],
614
+ topK: 6,
285
615
  });
286
616
  ```
287
617
 
@@ -290,29 +620,29 @@ const hits = query(kb, "retry backoff", {
290
620
  If your query has no free tokens but includes required phrases, KnoLo still forms candidates from phrase tokens and enforces phrase presence.
291
621
 
292
622
  ```ts
293
- const hits = query(kb, '"event loop"', { requirePhrases: ["single thread"] });
623
+ const hits = query(kb, '"event loop"', { requirePhrases: ['single thread'] });
294
624
  ```
295
625
 
296
626
  ### Precision mode with minimum score
297
627
 
298
628
  ```ts
299
- const strictHits = query(kb, "jwt refresh token rotation", {
629
+ const strictHits = query(kb, 'jwt refresh token rotation', {
300
630
  topK: 5,
301
- minScore: 2.5
631
+ minScore: 2.5,
302
632
  });
303
633
  ```
304
634
 
305
635
  ### Validate semantic query options early
306
636
 
307
637
  ```ts
308
- import { validateSemanticQueryOptions } from "knolo-core";
638
+ import { validateSemanticQueryOptions } from 'knolo-core';
309
639
 
310
640
  validateSemanticQueryOptions({
311
641
  enabled: true,
312
642
  topN: 40,
313
643
  minLexConfidence: 0.3,
314
644
  blend: { enabled: true, wLex: 0.8, wSem: 0.2 },
315
- queryEmbedding
645
+ queryEmbedding,
316
646
  });
317
647
  ```
318
648
 
@@ -356,7 +686,7 @@ Yes. Runtime text encoder/decoder compatibility is included.
356
686
 
357
687
  ---
358
688
 
359
- ## 🗺️ Direction / roadmap
689
+ ## 🗺️ Roadmap
360
690
 
361
691
  - stronger hybrid retrieval evaluation tooling
362
692
  - richer pack introspection and diagnostics