@knolo/core 3.1.2

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.
@@ -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,7 @@
1
+ export type QuantizedVector = {
2
+ q: Int8Array;
3
+ scale: number;
4
+ };
5
+ export declare function quantizeEmbeddingInt8L2Norm(embedding: Float32Array): QuantizedVector;
6
+ export declare function encodeScaleF16(scale: number): number;
7
+ export declare function decodeScaleF16(encoded: number): number;
@@ -0,0 +1,98 @@
1
+ export function quantizeEmbeddingInt8L2Norm(embedding) {
2
+ const dims = embedding.length;
3
+ const normalized = new Float32Array(dims);
4
+ let normSq = 0;
5
+ for (let i = 0; i < dims; i++)
6
+ normSq += embedding[i] * embedding[i];
7
+ const norm = Math.sqrt(normSq);
8
+ if (norm === 0) {
9
+ return { q: new Int8Array(dims), scale: 0 };
10
+ }
11
+ let maxAbs = 0;
12
+ for (let i = 0; i < dims; i++) {
13
+ const value = embedding[i] / norm;
14
+ normalized[i] = value;
15
+ const abs = Math.abs(value);
16
+ if (abs > maxAbs)
17
+ maxAbs = abs;
18
+ }
19
+ const scale = maxAbs / 127;
20
+ if (scale === 0) {
21
+ return { q: new Int8Array(dims), scale: 0 };
22
+ }
23
+ const q = new Int8Array(dims);
24
+ for (let i = 0; i < dims; i++) {
25
+ const quantized = Math.round(normalized[i] / scale);
26
+ q[i] = clampInt8(quantized);
27
+ }
28
+ return { q, scale };
29
+ }
30
+ export function encodeScaleF16(scale) {
31
+ return float32ToFloat16(scale);
32
+ }
33
+ export function decodeScaleF16(encoded) {
34
+ return float16ToFloat32(encoded);
35
+ }
36
+ function clampInt8(value) {
37
+ if (value > 127)
38
+ return 127;
39
+ if (value < -127)
40
+ return -127;
41
+ return value;
42
+ }
43
+ function float32ToFloat16(value) {
44
+ if (Number.isNaN(value))
45
+ return 0x7e00;
46
+ if (value === Infinity)
47
+ return 0x7c00;
48
+ if (value === -Infinity)
49
+ return 0xfc00;
50
+ const f32 = new Float32Array(1);
51
+ const u32 = new Uint32Array(f32.buffer);
52
+ f32[0] = value;
53
+ const bits = u32[0];
54
+ const sign = (bits >>> 16) & 0x8000;
55
+ let exp = (bits >>> 23) & 0xff;
56
+ let mantissa = bits & 0x7fffff;
57
+ if (exp === 0xff) {
58
+ return sign | (mantissa ? 0x7e00 : 0x7c00);
59
+ }
60
+ const halfExp = exp - 127 + 15;
61
+ if (halfExp >= 0x1f) {
62
+ return sign | 0x7c00;
63
+ }
64
+ if (halfExp <= 0) {
65
+ if (halfExp < -10)
66
+ return sign;
67
+ mantissa = (mantissa | 0x800000) >>> (1 - halfExp);
68
+ if (mantissa & 0x1000)
69
+ mantissa += 0x2000;
70
+ return sign | (mantissa >>> 13);
71
+ }
72
+ if (mantissa & 0x1000) {
73
+ mantissa += 0x2000;
74
+ if (mantissa & 0x800000) {
75
+ mantissa = 0;
76
+ exp += 1;
77
+ if (exp > 142)
78
+ return sign | 0x7c00;
79
+ }
80
+ }
81
+ return sign | (halfExp << 10) | (mantissa >>> 13);
82
+ }
83
+ function float16ToFloat32(bits) {
84
+ const sign = (bits & 0x8000) ? -1 : 1;
85
+ const exp = (bits >>> 10) & 0x1f;
86
+ const frac = bits & 0x03ff;
87
+ if (exp === 0) {
88
+ if (frac === 0)
89
+ return sign * 0;
90
+ return sign * Math.pow(2, -14) * (frac / 1024);
91
+ }
92
+ if (exp === 0x1f) {
93
+ if (frac === 0)
94
+ return sign * Infinity;
95
+ return NaN;
96
+ }
97
+ return sign * Math.pow(2, exp - 15) * (1 + frac / 1024);
98
+ }
@@ -0,0 +1,24 @@
1
+ export type Token = {
2
+ term: string;
3
+ pos: number;
4
+ };
5
+ /** Normalize a string by:
6
+ * - Applying NFKD Unicode normalization
7
+ * - Stripping combining diacritics
8
+ * - Converting to lowercase
9
+ * - Replacing non‑alphanumeric characters (except hyphen and space) with space
10
+ *
11
+ * This normalization ensures that accents and case do not affect matching.
12
+ */
13
+ export declare function normalize(s: string): string;
14
+ /** Split a piece of text into tokens with positional information. Each token
15
+ * contains the normalized term and its position in the sequence. Positions
16
+ * increment only on actual tokens, ignoring multiple whitespace separators.
17
+ */
18
+ export declare function tokenize(text: string): Token[];
19
+ /** Parse quoted phrases in a query string. A phrase is a sequence of words
20
+ * enclosed in double quotes. Returns an array of term arrays representing
21
+ * each phrase. Single words outside quotes are ignored here and handled
22
+ * separately by tokenize().
23
+ */
24
+ export declare function parsePhrases(q: string): string[][];
@@ -0,0 +1,53 @@
1
+ /*
2
+ * tokenize.ts
3
+ *
4
+ * Provides functions for normalizing strings and splitting them into tokens
5
+ * suitable for indexing and querying. This module deliberately avoids any
6
+ * language‑specific stemming or lemmatization to keep the core simple and
7
+ * deterministic across platforms. Basic Unicode normalization and diacritic
8
+ * stripping are applied to ensure consistent matches.
9
+ */
10
+ /** Normalize a string by:
11
+ * - Applying NFKD Unicode normalization
12
+ * - Stripping combining diacritics
13
+ * - Converting to lowercase
14
+ * - Replacing non‑alphanumeric characters (except hyphen and space) with space
15
+ *
16
+ * This normalization ensures that accents and case do not affect matching.
17
+ */
18
+ export function normalize(s) {
19
+ return s
20
+ .normalize('NFKD')
21
+ .replace(/\p{M}+/gu, '')
22
+ .toLowerCase()
23
+ .replace(/[^\p{L}\p{N}\s-]/gu, ' ');
24
+ }
25
+ /** Split a piece of text into tokens with positional information. Each token
26
+ * contains the normalized term and its position in the sequence. Positions
27
+ * increment only on actual tokens, ignoring multiple whitespace separators.
28
+ */
29
+ export function tokenize(text) {
30
+ const norm = normalize(text);
31
+ const out = [];
32
+ let pos = 0;
33
+ for (const w of norm.split(/\s+/).filter(Boolean)) {
34
+ out.push({ term: w, pos: pos++ });
35
+ }
36
+ return out;
37
+ }
38
+ /** Parse quoted phrases in a query string. A phrase is a sequence of words
39
+ * enclosed in double quotes. Returns an array of term arrays representing
40
+ * each phrase. Single words outside quotes are ignored here and handled
41
+ * separately by tokenize().
42
+ */
43
+ export function parsePhrases(q) {
44
+ const parts = [];
45
+ const regex = /["“”]([^"“”]+)["“”]/g;
46
+ let match;
47
+ while ((match = regex.exec(q)) !== null) {
48
+ const phrase = match[1].trim().split(/\s+/);
49
+ if (phrase.length > 0)
50
+ parts.push(phrase);
51
+ }
52
+ return parts;
53
+ }
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ export type TextDecoderLike = {
2
+ decode: (u8: Uint8Array) => string;
3
+ };
4
+ export type TextEncoderLike = {
5
+ encode: (s: string) => Uint8Array;
6
+ };
7
+ export declare function getTextDecoder(): TextDecoderLike;
8
+ export declare function getTextEncoder(): TextEncoderLike;
@@ -0,0 +1,72 @@
1
+ // src/utils/utf8.ts
2
+ // Small, dependency-free UTF-8 encoder/decoder that works in RN/Hermes.
3
+ export function getTextDecoder() {
4
+ try {
5
+ // eslint-disable-next-line no-new
6
+ const td = new TextDecoder();
7
+ return td;
8
+ }
9
+ catch {
10
+ return {
11
+ decode: (u8) => {
12
+ let out = '';
13
+ for (let i = 0; i < u8.length;) {
14
+ const a = u8[i++];
15
+ if (a < 0x80) {
16
+ out += String.fromCharCode(a);
17
+ }
18
+ else if ((a & 0xe0) === 0xc0) {
19
+ const b = u8[i++] & 0x3f;
20
+ const cp = ((a & 0x1f) << 6) | b;
21
+ out += String.fromCharCode(cp);
22
+ }
23
+ else if ((a & 0xf0) === 0xe0) {
24
+ const b = u8[i++] & 0x3f;
25
+ const c = u8[i++] & 0x3f;
26
+ const cp = ((a & 0x0f) << 12) | (b << 6) | c;
27
+ out += String.fromCharCode(cp);
28
+ }
29
+ else {
30
+ const b = u8[i++] & 0x3f;
31
+ const c = u8[i++] & 0x3f;
32
+ const d = u8[i++] & 0x3f;
33
+ let cp = ((a & 0x07) << 18) | (b << 12) | (c << 6) | d;
34
+ cp -= 0x10000;
35
+ out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
36
+ }
37
+ }
38
+ return out;
39
+ },
40
+ };
41
+ }
42
+ }
43
+ export function getTextEncoder() {
44
+ try {
45
+ // eslint-disable-next-line no-new
46
+ const te = new TextEncoder();
47
+ return te;
48
+ }
49
+ catch {
50
+ return {
51
+ encode: (s) => {
52
+ const out = [];
53
+ for (let i = 0; i < s.length; i++) {
54
+ let cp = s.charCodeAt(i);
55
+ if (cp >= 0xd800 && cp <= 0xdbff && i + 1 < s.length) {
56
+ const next = s.charCodeAt(++i);
57
+ cp = 0x10000 + ((cp - 0xd800) << 10) + (next - 0xdc00);
58
+ }
59
+ if (cp < 0x80)
60
+ out.push(cp);
61
+ else if (cp < 0x800)
62
+ out.push(0xc0 | (cp >> 6), 0x80 | (cp & 0x3f));
63
+ else if (cp < 0x10000)
64
+ out.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
65
+ else
66
+ out.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
67
+ }
68
+ return new Uint8Array(out);
69
+ },
70
+ };
71
+ }
72
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@knolo/core",
3
+ "version": "3.1.2",
4
+ "type": "module",
5
+ "description": "Local-first knowledge packs for small LLMs.",
6
+ "keywords": [
7
+ "llm",
8
+ "knowledge-base",
9
+ "rag",
10
+ "local",
11
+ "expo"
12
+ ],
13
+ "author": "Sam Paniagua",
14
+ "license": "Apache-2.0",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "import": "./dist/index.js",
23
+ "types": "./dist/index.d.ts"
24
+ }
25
+ },
26
+ "sideEffects": false,
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "prepublishOnly": "npm run build",
30
+ "smoke": "node scripts/smoke.mjs",
31
+ "test": "npm run build && node scripts/test.mjs",
32
+ "format": "prettier --write src/agent.ts src/pack.ts src/builder.ts src/index.ts scripts/test.mjs ../../README.md",
33
+ "format:check": "prettier --check src/agent.ts src/pack.ts src/builder.ts src/index.ts scripts/test.mjs ../../README.md"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20.11.0",
37
+ "typescript": "^5.5.0"
38
+ }
39
+ }