knolo-core 3.1.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/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,6 +58,14 @@ 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
@@ -126,7 +138,7 @@ const hits = query(kb, 'throttle events', { topK: 3 });
126
138
 
127
139
  ---
128
140
 
129
- ## 🔀 Hybrid retrieval with embeddings (recommended direction)
141
+ ## 🔀 Hybrid retrieval with embeddings (optional)
130
142
 
131
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).
132
144
 
@@ -221,7 +233,6 @@ Agent namespace binding is **strict**: when `resolveAgent()` composes retrieval
221
233
  ```ts
222
234
  type AgentPromptTemplate = string[] | { format: 'markdown'; template: string };
223
235
 
224
-
225
236
  type AgentRegistry = {
226
237
  version: 1;
227
238
  agents: AgentDefinitionV1[];
@@ -336,6 +347,211 @@ type Hit = {
336
347
  - `buildSystemPrompt(agent, patch?) => string`
337
348
  - `isToolAllowed(agent, toolId) => boolean` (defaults to allow-all when no `toolPolicy`)
338
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.
339
555
 
340
556
  ### Build a pack with agents and resolve at runtime
341
557
 
@@ -470,7 +686,7 @@ Yes. Runtime text encoder/decoder compatibility is included.
470
686
 
471
687
  ---
472
688
 
473
- ## 🗺️ Direction / roadmap
689
+ ## 🗺️ Roadmap
474
690
 
475
691
  - stronger hybrid retrieval evaluation tooling
476
692
  - richer pack introspection and diagnostics
package/dist/index.d.ts CHANGED
@@ -9,3 +9,13 @@ export type { QueryOptions, Hit } from './query.js';
9
9
  export type { ContextPatch } from './patch.js';
10
10
  export type { BuildInputDoc, BuildPackOptions } from './builder.js';
11
11
  export type { AgentPromptTemplate, AgentToolPolicy, AgentRetrievalDefaults, AgentDefinitionV1, AgentRegistry, ResolveAgentInput, ResolvedAgent, } from './agent.js';
12
+ export { parseToolCallV1FromText } from './tool_parse.js';
13
+ export { nowIso, createTrace } from './trace.js';
14
+ export { assertToolCallAllowed } from './tool_gate.js';
15
+ export { getAgentRoutingProfileV1, getPackRoutingProfilesV1, } from './routing_profile.js';
16
+ export { isRouteDecisionV1, validateRouteDecisionV1, selectAgentIdFromRouteDecisionV1, } from './router.js';
17
+ export { isToolCallV1, isToolResultV1 } from './tools.js';
18
+ export type { ToolId, ToolCallV1, ToolResultErrorV1, ToolResultV1, ToolSpecV1, } from './tools.js';
19
+ export type { TraceEventV1 } from './trace.js';
20
+ export type { AgentRoutingProfileV1 } from './routing_profile.js';
21
+ export type { RouteCandidateV1, RouteDecisionV1 } from './router.js';
package/dist/index.js CHANGED
@@ -5,3 +5,9 @@ export { makeContextPatch } from './patch.js';
5
5
  export { buildPack } from './builder.js';
6
6
  export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16, } from './semantic.js';
7
7
  export { listAgents, getAgent, resolveAgent, buildSystemPrompt, isToolAllowed, assertToolAllowed, validateAgentRegistry, validateAgentDefinition, } from './agent.js';
8
+ export { parseToolCallV1FromText } from './tool_parse.js';
9
+ export { nowIso, createTrace } from './trace.js';
10
+ export { assertToolCallAllowed } from './tool_gate.js';
11
+ export { getAgentRoutingProfileV1, getPackRoutingProfilesV1, } from './routing_profile.js';
12
+ export { isRouteDecisionV1, validateRouteDecisionV1, selectAgentIdFromRouteDecisionV1, } from './router.js';
13
+ export { isToolCallV1, isToolResultV1 } from './tools.js';
@@ -0,0 +1,28 @@
1
+ import type { AgentDefinitionV1 } from './agent.js';
2
+ export interface RouteCandidateV1 {
3
+ agentId: string;
4
+ score: number;
5
+ why?: string;
6
+ }
7
+ export interface RouteDecisionV1 {
8
+ type: 'route_decision';
9
+ intent?: string;
10
+ entities?: Record<string, unknown>;
11
+ candidates: RouteCandidateV1[];
12
+ selected: string;
13
+ needsTools?: string[];
14
+ risk?: 'low' | 'med' | 'high';
15
+ }
16
+ export declare function isRouteDecisionV1(x: unknown): x is RouteDecisionV1;
17
+ export declare function validateRouteDecisionV1(decision: RouteDecisionV1, agentRegistry: Record<string, AgentDefinitionV1>): {
18
+ ok: true;
19
+ } | {
20
+ ok: false;
21
+ error: string;
22
+ };
23
+ export declare function selectAgentIdFromRouteDecisionV1(decision: RouteDecisionV1, agentRegistry: Record<string, AgentDefinitionV1>, opts?: {
24
+ fallbackAgentId?: string;
25
+ }): {
26
+ agentId: string;
27
+ reason: 'selected' | 'top_candidate' | 'fallback';
28
+ };
package/dist/router.js ADDED
@@ -0,0 +1,74 @@
1
+ export function isRouteDecisionV1(x) {
2
+ if (!x || typeof x !== 'object')
3
+ return false;
4
+ const v = x;
5
+ if (v.type !== 'route_decision')
6
+ return false;
7
+ if (typeof v.selected !== 'string' || !v.selected.trim())
8
+ return false;
9
+ if (!Array.isArray(v.candidates) || v.candidates.length < 1)
10
+ return false;
11
+ if (v.needsTools !== undefined &&
12
+ (!Array.isArray(v.needsTools) ||
13
+ v.needsTools.some((toolId) => typeof toolId !== 'string'))) {
14
+ return false;
15
+ }
16
+ for (const candidate of v.candidates) {
17
+ if (!candidate || typeof candidate !== 'object')
18
+ return false;
19
+ const c = candidate;
20
+ if (typeof c.agentId !== 'string' || !c.agentId.trim())
21
+ return false;
22
+ if (typeof c.score !== 'number' || !Number.isFinite(c.score))
23
+ return false;
24
+ if (c.score < 0 || c.score > 1)
25
+ return false;
26
+ if (c.why !== undefined && typeof c.why !== 'string')
27
+ return false;
28
+ }
29
+ return true;
30
+ }
31
+ export function validateRouteDecisionV1(decision, agentRegistry) {
32
+ if (!agentRegistry[decision.selected]) {
33
+ return {
34
+ ok: false,
35
+ error: `selected agent is not registered: ${decision.selected}`,
36
+ };
37
+ }
38
+ const seen = new Set();
39
+ for (const candidate of decision.candidates) {
40
+ if (seen.has(candidate.agentId)) {
41
+ return {
42
+ ok: false,
43
+ error: `duplicate candidate agentId: ${candidate.agentId}`,
44
+ };
45
+ }
46
+ seen.add(candidate.agentId);
47
+ if (!agentRegistry[candidate.agentId]) {
48
+ return {
49
+ ok: false,
50
+ error: `candidate agent is not registered: ${candidate.agentId}`,
51
+ };
52
+ }
53
+ }
54
+ return { ok: true };
55
+ }
56
+ export function selectAgentIdFromRouteDecisionV1(decision, agentRegistry, opts = {}) {
57
+ if (agentRegistry[decision.selected]) {
58
+ return { agentId: decision.selected, reason: 'selected' };
59
+ }
60
+ const sortedCandidates = [...decision.candidates].sort((a, b) => b.score - a.score || a.agentId.localeCompare(b.agentId));
61
+ for (const candidate of sortedCandidates) {
62
+ if (agentRegistry[candidate.agentId]) {
63
+ return { agentId: candidate.agentId, reason: 'top_candidate' };
64
+ }
65
+ }
66
+ if (opts.fallbackAgentId && agentRegistry[opts.fallbackAgentId]) {
67
+ return { agentId: opts.fallbackAgentId, reason: 'fallback' };
68
+ }
69
+ const defaultAgentId = Object.keys(agentRegistry).sort()[0];
70
+ if (defaultAgentId) {
71
+ return { agentId: defaultAgentId, reason: 'fallback' };
72
+ }
73
+ return { agentId: '', reason: 'fallback' };
74
+ }
@@ -0,0 +1,19 @@
1
+ import type { AgentDefinitionV1 } from './agent.js';
2
+ import type { Pack } from './pack.js';
3
+ export interface AgentRoutingProfileV1 {
4
+ agentId: string;
5
+ namespace?: string;
6
+ heading?: string;
7
+ description?: string;
8
+ tags: string[];
9
+ examples: string[];
10
+ capabilities: string[];
11
+ toolPolicy?: unknown;
12
+ toolPolicySummary?: {
13
+ mode: 'allow_all' | 'deny_all' | 'mixed' | 'unknown';
14
+ allowed?: string[];
15
+ denied?: string[];
16
+ };
17
+ }
18
+ export declare function getAgentRoutingProfileV1(agent: AgentDefinitionV1): AgentRoutingProfileV1;
19
+ export declare function getPackRoutingProfilesV1(pack: Pack): AgentRoutingProfileV1[];
@@ -0,0 +1,102 @@
1
+ const MAX_DISCOVERABILITY_ITEMS = 20;
2
+ export function getAgentRoutingProfileV1(agent) {
3
+ const metadata = agent.metadata ?? {};
4
+ const heading = getStringMetadata(metadata, 'heading');
5
+ const namespace = getPrimaryNamespace(agent);
6
+ return {
7
+ agentId: agent.id,
8
+ namespace,
9
+ heading,
10
+ description: agent.description,
11
+ tags: parseDiscoverabilityList(metadata.tags),
12
+ examples: parseDiscoverabilityList(metadata.examples),
13
+ capabilities: parseDiscoverabilityList(metadata.capabilities),
14
+ toolPolicy: agent.toolPolicy,
15
+ toolPolicySummary: summarizeToolPolicy(agent.toolPolicy),
16
+ };
17
+ }
18
+ export function getPackRoutingProfilesV1(pack) {
19
+ const agents = pack.meta.agents?.agents ?? [];
20
+ return agents.map((agent) => getAgentRoutingProfileV1(agent));
21
+ }
22
+ function getPrimaryNamespace(agent) {
23
+ const first = agent.retrievalDefaults.namespace[0];
24
+ if (typeof first === 'string' && first.trim()) {
25
+ return first;
26
+ }
27
+ return undefined;
28
+ }
29
+ function getStringMetadata(metadata, key) {
30
+ const value = metadata[key];
31
+ if (typeof value !== 'string')
32
+ return undefined;
33
+ const normalized = value.trim();
34
+ return normalized ? normalized : undefined;
35
+ }
36
+ function parseDiscoverabilityList(value) {
37
+ if (typeof value !== 'string')
38
+ return [];
39
+ const raw = value.trim();
40
+ if (!raw)
41
+ return [];
42
+ let parsed;
43
+ if (raw.startsWith('[')) {
44
+ parsed = parseJsonArrayString(raw);
45
+ }
46
+ else if (raw.includes('\n')) {
47
+ parsed = raw.split('\n');
48
+ }
49
+ else {
50
+ parsed = raw.split(',');
51
+ }
52
+ const deduped = [];
53
+ const seen = new Set();
54
+ for (const item of parsed) {
55
+ const normalized = item.trim();
56
+ if (!normalized || seen.has(normalized))
57
+ continue;
58
+ seen.add(normalized);
59
+ deduped.push(normalized);
60
+ if (deduped.length >= MAX_DISCOVERABILITY_ITEMS)
61
+ break;
62
+ }
63
+ return deduped;
64
+ }
65
+ function parseJsonArrayString(raw) {
66
+ try {
67
+ const parsed = JSON.parse(raw);
68
+ if (!Array.isArray(parsed))
69
+ return [];
70
+ return parsed.filter((item) => typeof item === 'string');
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ }
76
+ function summarizeToolPolicy(policy) {
77
+ if (!policy) {
78
+ return {
79
+ mode: 'allow_all',
80
+ };
81
+ }
82
+ if (!Array.isArray(policy.tools)) {
83
+ return {
84
+ mode: 'unknown',
85
+ };
86
+ }
87
+ if (policy.mode === 'allow') {
88
+ return {
89
+ mode: 'mixed',
90
+ allowed: policy.tools,
91
+ };
92
+ }
93
+ if (policy.mode === 'deny') {
94
+ return {
95
+ mode: 'mixed',
96
+ denied: policy.tools,
97
+ };
98
+ }
99
+ return {
100
+ mode: 'unknown',
101
+ };
102
+ }
@@ -0,0 +1,3 @@
1
+ import { type AgentDefinitionV1 } from './agent.js';
2
+ import { type ToolCallV1 } from './tools.js';
3
+ export declare function assertToolCallAllowed(agent: AgentDefinitionV1, call: ToolCallV1): void;
@@ -0,0 +1,8 @@
1
+ import { assertToolAllowed } from './agent.js';
2
+ import { isToolCallV1 } from './tools.js';
3
+ export function assertToolCallAllowed(agent, call) {
4
+ if (!isToolCallV1(call)) {
5
+ throw new Error('tool call must be a valid ToolCallV1 object.');
6
+ }
7
+ assertToolAllowed(agent, call.tool);
8
+ }
@@ -0,0 +1,2 @@
1
+ import { type ToolCallV1 } from './tools.js';
2
+ export declare function parseToolCallV1FromText(text: string): ToolCallV1 | null;
@@ -0,0 +1,102 @@
1
+ import { isToolCallV1 } from './tools.js';
2
+ function tryParseJson(input) {
3
+ try {
4
+ return JSON.parse(input);
5
+ }
6
+ catch {
7
+ return null;
8
+ }
9
+ }
10
+ function findBalancedJsonObject(text) {
11
+ let depth = 0;
12
+ let start = -1;
13
+ let inString = false;
14
+ let escaped = false;
15
+ for (let i = 0; i < text.length; i++) {
16
+ const char = text[i];
17
+ if (inString) {
18
+ if (escaped) {
19
+ escaped = false;
20
+ continue;
21
+ }
22
+ if (char === '\\') {
23
+ escaped = true;
24
+ continue;
25
+ }
26
+ if (char === '"') {
27
+ inString = false;
28
+ }
29
+ continue;
30
+ }
31
+ if (char === '"') {
32
+ inString = true;
33
+ continue;
34
+ }
35
+ if (char === '{') {
36
+ if (depth === 0)
37
+ start = i;
38
+ depth += 1;
39
+ continue;
40
+ }
41
+ if (char === '}') {
42
+ if (depth === 0)
43
+ continue;
44
+ depth -= 1;
45
+ if (depth === 0 && start >= 0) {
46
+ return text.slice(start, i + 1);
47
+ }
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ function parseToolCallCandidate(candidate) {
53
+ const value = tryParseJson(candidate);
54
+ return isToolCallV1(value) ? value : null;
55
+ }
56
+ function parseFromFencedBlock(text) {
57
+ const fencedRegex = /```(?:json)?\s*([\s\S]*?)```/gi;
58
+ let match;
59
+ while ((match = fencedRegex.exec(text)) !== null) {
60
+ const parsed = parseToolCallCandidate(match[1].trim());
61
+ if (parsed)
62
+ return parsed;
63
+ }
64
+ return null;
65
+ }
66
+ function parseFromMarkerLine(text) {
67
+ const lines = text.split(/\r?\n/);
68
+ for (const line of lines) {
69
+ const markerIndex = line.indexOf('TOOL_CALL:');
70
+ if (markerIndex === -1)
71
+ continue;
72
+ const tail = line.slice(markerIndex + 'TOOL_CALL:'.length).trim();
73
+ if (!tail)
74
+ continue;
75
+ const objectText = findBalancedJsonObject(tail) ?? tail;
76
+ const parsed = parseToolCallCandidate(objectText);
77
+ if (parsed)
78
+ return parsed;
79
+ }
80
+ return null;
81
+ }
82
+ function parseFirstJsonObject(text) {
83
+ const objectText = findBalancedJsonObject(text);
84
+ if (!objectText)
85
+ return null;
86
+ return tryParseJson(objectText);
87
+ }
88
+ export function parseToolCallV1FromText(text) {
89
+ if (typeof text !== 'string' || !text.trim())
90
+ return null;
91
+ const whole = parseToolCallCandidate(text.trim());
92
+ if (whole)
93
+ return whole;
94
+ const fenced = parseFromFencedBlock(text);
95
+ if (fenced)
96
+ return fenced;
97
+ const marker = parseFromMarkerLine(text);
98
+ if (marker)
99
+ return marker;
100
+ const firstObject = parseFirstJsonObject(text);
101
+ return isToolCallV1(firstObject) ? firstObject : null;
102
+ }
@@ -0,0 +1,27 @@
1
+ export type ToolId = string;
2
+ export interface ToolCallV1 {
3
+ type: 'tool_call';
4
+ callId: string;
5
+ tool: ToolId;
6
+ args: Record<string, unknown>;
7
+ }
8
+ export interface ToolResultErrorV1 {
9
+ message: string;
10
+ code?: string;
11
+ details?: unknown;
12
+ }
13
+ export interface ToolResultV1 {
14
+ type: 'tool_result';
15
+ callId: string;
16
+ tool: ToolId;
17
+ ok: boolean;
18
+ output?: unknown;
19
+ error?: ToolResultErrorV1;
20
+ }
21
+ export interface ToolSpecV1 {
22
+ id: ToolId;
23
+ description?: string;
24
+ jsonSchema?: unknown;
25
+ }
26
+ export declare function isToolCallV1(x: unknown): x is ToolCallV1;
27
+ export declare function isToolResultV1(x: unknown): x is ToolResultV1;
package/dist/tools.js ADDED
@@ -0,0 +1,34 @@
1
+ function isPlainObject(value) {
2
+ if (!value || typeof value !== 'object' || Array.isArray(value))
3
+ return false;
4
+ const proto = Object.getPrototypeOf(value);
5
+ return proto === Object.prototype || proto === null;
6
+ }
7
+ export function isToolCallV1(x) {
8
+ if (!isPlainObject(x))
9
+ return false;
10
+ return (x.type === 'tool_call' &&
11
+ typeof x.callId === 'string' &&
12
+ x.callId.trim().length > 0 &&
13
+ typeof x.tool === 'string' &&
14
+ x.tool.trim().length > 0 &&
15
+ isPlainObject(x.args));
16
+ }
17
+ export function isToolResultV1(x) {
18
+ if (!isPlainObject(x))
19
+ return false;
20
+ if (x.type !== 'tool_result' ||
21
+ typeof x.callId !== 'string' ||
22
+ x.callId.trim().length === 0 ||
23
+ typeof x.tool !== 'string' ||
24
+ x.tool.trim().length === 0 ||
25
+ typeof x.ok !== 'boolean') {
26
+ return false;
27
+ }
28
+ if (x.ok) {
29
+ return x.error === undefined;
30
+ }
31
+ return (isPlainObject(x.error) &&
32
+ typeof x.error.message === 'string' &&
33
+ x.error.message.trim().length > 0);
34
+ }
@@ -0,0 +1,45 @@
1
+ import type { ToolCallV1, ToolResultV1 } from './tools.js';
2
+ import type { RouteDecisionV1 } from './router.js';
3
+ export type TraceEventV1 = {
4
+ type: 'route.requested';
5
+ ts: string;
6
+ text: string;
7
+ agentCount: number;
8
+ } | {
9
+ type: 'route.decided';
10
+ ts: string;
11
+ decision: RouteDecisionV1;
12
+ selectedAgentId: string;
13
+ } | {
14
+ type: 'agent.selected';
15
+ ts: string;
16
+ agentId: string;
17
+ namespace?: string;
18
+ } | {
19
+ type: 'prompt.resolved';
20
+ ts: string;
21
+ agentId: string;
22
+ promptHash?: string;
23
+ patchKeys?: string[];
24
+ } | {
25
+ type: 'tool.requested';
26
+ ts: string;
27
+ agentId: string;
28
+ call: ToolCallV1;
29
+ } | {
30
+ type: 'tool.executed';
31
+ ts: string;
32
+ agentId: string;
33
+ result: ToolResultV1;
34
+ durationMs?: number;
35
+ } | {
36
+ type: 'run.completed';
37
+ ts: string;
38
+ agentId: string;
39
+ status: 'ok' | 'error';
40
+ };
41
+ export declare function nowIso(): string;
42
+ export declare function createTrace(): {
43
+ events: TraceEventV1[];
44
+ push(e: TraceEventV1): void;
45
+ };
package/dist/trace.js ADDED
@@ -0,0 +1,12 @@
1
+ export function nowIso() {
2
+ return new Date().toISOString();
3
+ }
4
+ export function createTrace() {
5
+ const events = [];
6
+ return {
7
+ events,
8
+ push(e) {
9
+ events.push(e);
10
+ },
11
+ };
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knolo-core",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "type": "module",
5
5
  "description": "Local-first knowledge packs for small LLMs.",
6
6
  "keywords": [