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/dist/builder.js CHANGED
@@ -8,6 +8,7 @@ import { buildIndex } from './indexer.js';
8
8
  import { tokenize } from './tokenize.js';
9
9
  import { getTextEncoder } from './utils/utf8.js';
10
10
  import { encodeScaleF16, quantizeEmbeddingInt8L2Norm } from './semantic.js';
11
+ import { validateAgentRegistry } from './agent.js';
11
12
  export async function buildPack(docs, opts = {}) {
12
13
  const normalizedDocs = validateDocs(docs);
13
14
  // Prepare blocks (strip MD) and carry heading/docId for optional boosts.
@@ -21,6 +22,7 @@ export async function buildPack(docs, opts = {}) {
21
22
  const blockTokenLens = blocks.map((b) => tokenize(b.text).length);
22
23
  const totalTokens = blockTokenLens.reduce((sum, len) => sum + len, 0);
23
24
  const avgBlockLen = blocks.length ? totalTokens / blocks.length : 1;
25
+ const agents = normalizeAgents(opts.agents);
24
26
  const meta = {
25
27
  version: 3,
26
28
  stats: {
@@ -29,6 +31,7 @@ export async function buildPack(docs, opts = {}) {
29
31
  terms: lexicon.length,
30
32
  avgBlockLen,
31
33
  },
34
+ ...(agents ? { agents } : {}),
32
35
  };
33
36
  // Persist blocks as objects to optionally carry heading/docId/token length.
34
37
  const blocksPayload = blocks.map((b, i) => ({
@@ -47,12 +50,18 @@ export async function buildPack(docs, opts = {}) {
47
50
  const semanticSection = semanticEnabled && opts.semantic
48
51
  ? buildSemanticSection(blocks.length, opts.semantic)
49
52
  : undefined;
50
- const semBytes = semanticSection ? enc.encode(JSON.stringify(semanticSection.semJson)) : undefined;
53
+ const semBytes = semanticSection
54
+ ? enc.encode(JSON.stringify(semanticSection.semJson))
55
+ : undefined;
51
56
  const semBlob = semanticSection?.semBlob;
52
- const totalLength = 4 + metaBytes.length +
53
- 4 + lexBytes.length +
54
- 4 + postings.length * 4 +
55
- 4 + blocksBytes.length +
57
+ const totalLength = 4 +
58
+ metaBytes.length +
59
+ 4 +
60
+ lexBytes.length +
61
+ 4 +
62
+ postings.length * 4 +
63
+ 4 +
64
+ blocksBytes.length +
56
65
  (semanticEnabled && semBytes && semBlob
57
66
  ? 4 + semBytes.length + 4 + semBlob.length
58
67
  : 0);
@@ -92,6 +101,15 @@ export async function buildPack(docs, opts = {}) {
92
101
  }
93
102
  return out;
94
103
  }
104
+ function normalizeAgents(input) {
105
+ if (!input)
106
+ return undefined;
107
+ const registry = Array.isArray(input)
108
+ ? { version: 1, agents: input }
109
+ : input;
110
+ validateAgentRegistry(registry);
111
+ return registry;
112
+ }
95
113
  function buildSemanticSection(blockCount, semantic) {
96
114
  const { embeddings } = semantic;
97
115
  if (!Array.isArray(embeddings) || embeddings.length !== blockCount) {
@@ -133,7 +151,11 @@ function buildSemanticSection(blockCount, semantic) {
133
151
  perVectorScale: true,
134
152
  blocks: {
135
153
  vectors: { byteOffset: vecByteOffset, length: vecs.length },
136
- scales: { byteOffset: scalesByteOffset, length: scales.length, encoding: 'float16' },
154
+ scales: {
155
+ byteOffset: scalesByteOffset,
156
+ length: scales.length,
157
+ encoding: 'float16',
158
+ },
137
159
  },
138
160
  };
139
161
  return { semJson, semBlob };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,21 @@
1
1
  export { mountPack, hasSemantic } from './pack.js';
2
- export { query, lexConfidence, validateSemanticQueryOptions } from './query.js';
2
+ export { query, lexConfidence, validateQueryOptions, validateSemanticQueryOptions, } from './query.js';
3
3
  export { makeContextPatch } from './patch.js';
4
4
  export { buildPack } from './builder.js';
5
- export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16 } from './semantic.js';
5
+ export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16, } from './semantic.js';
6
+ export { listAgents, getAgent, resolveAgent, buildSystemPrompt, isToolAllowed, assertToolAllowed, validateAgentRegistry, validateAgentDefinition, } from './agent.js';
6
7
  export type { MountOptions, PackMeta, Pack } from './pack.js';
7
8
  export type { QueryOptions, Hit } from './query.js';
8
9
  export type { ContextPatch } from './patch.js';
9
10
  export type { BuildInputDoc, BuildPackOptions } from './builder.js';
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
@@ -1,6 +1,13 @@
1
1
  // src/index.ts
2
2
  export { mountPack, hasSemantic } from './pack.js';
3
- export { query, lexConfidence, validateSemanticQueryOptions } from './query.js';
3
+ export { query, lexConfidence, validateQueryOptions, validateSemanticQueryOptions, } from './query.js';
4
4
  export { makeContextPatch } from './patch.js';
5
5
  export { buildPack } from './builder.js';
6
- export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16 } from './semantic.js';
6
+ export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16, } from './semantic.js';
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';
package/dist/pack.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { AgentRegistry } from './agent.js';
1
2
  export type MountOptions = {
2
3
  src: string | ArrayBufferLike | Uint8Array;
3
4
  };
@@ -9,6 +10,7 @@ export type PackMeta = {
9
10
  terms: number;
10
11
  avgBlockLen?: number;
11
12
  };
13
+ agents?: AgentRegistry;
12
14
  };
13
15
  export type Pack = {
14
16
  meta: PackMeta;
package/dist/pack.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * Includes RN/Expo-safe TextDecoder via ponyfill.
8
8
  */
9
9
  import { getTextDecoder } from './utils/utf8.js';
10
+ import { validateAgentRegistry } from './agent.js';
10
11
  export function hasSemantic(pack) {
11
12
  return Boolean(pack.semantic && pack.semantic.dims > 0 && pack.semantic.vecs.length > 0);
12
13
  }
@@ -21,6 +22,9 @@ export async function mountPack(opts) {
21
22
  const metaJson = dec.decode(new Uint8Array(buf, offset, metaLen));
22
23
  offset += metaLen;
23
24
  const meta = JSON.parse(metaJson);
25
+ if (meta.agents) {
26
+ validateAgentRegistry(meta.agents);
27
+ }
24
28
  // lexicon
25
29
  const lexLen = dv.getUint32(offset, true);
26
30
  offset += 4;
@@ -89,7 +93,17 @@ export async function mountPack(opts) {
89
93
  const semBlob = new Uint8Array(buf, offset, semBlobLen);
90
94
  semantic = parseSemanticSection(sem, semBlob);
91
95
  }
92
- return { meta, lexicon, postings, blocks, headings, docIds, namespaces, blockTokenLens, semantic };
96
+ return {
97
+ meta,
98
+ lexicon,
99
+ postings,
100
+ blocks,
101
+ headings,
102
+ docIds,
103
+ namespaces,
104
+ blockTokenLens,
105
+ semantic,
106
+ };
93
107
  }
94
108
  function parseSemanticSection(sem, blob) {
95
109
  const vectors = sem?.blocks?.vectors;
@@ -133,12 +147,17 @@ async function resolveToBuffer(src) {
133
147
  return src;
134
148
  }
135
149
  function isNodeRuntime() {
136
- return typeof process !== 'undefined' && !!process.versions?.node;
150
+ const p = globalThis
151
+ .process;
152
+ return !!p?.versions?.node;
137
153
  }
138
154
  function isLikelyLocalPath(value) {
139
155
  if (value.startsWith('file://'))
140
156
  return true;
141
- if (value.startsWith('./') || value.startsWith('../') || value.startsWith('/') || value.startsWith('~'))
157
+ if (value.startsWith('./') ||
158
+ value.startsWith('../') ||
159
+ value.startsWith('/') ||
160
+ value.startsWith('~'))
142
161
  return true;
143
162
  if (/^[A-Za-z]:[\\/]/.test(value))
144
163
  return true; // Windows absolute path
package/dist/query.d.ts CHANGED
@@ -26,6 +26,7 @@ export type QueryOptions = {
26
26
  force?: boolean;
27
27
  };
28
28
  };
29
+ export declare function validateQueryOptions(opts?: QueryOptions): void;
29
30
  export declare function validateSemanticQueryOptions(options?: QueryOptions["semantic"]): void;
30
31
  export type Hit = {
31
32
  blockId: number;
package/dist/query.js CHANGED
@@ -14,6 +14,40 @@ import { minCoverSpan, proximityMultiplier } from "./quality/proximity.js";
14
14
  import { diversifyAndDedupe } from "./quality/diversify.js";
15
15
  import { knsSignature, knsDistance } from "./quality/signature.js";
16
16
  import { decodeScaleF16, quantizeEmbeddingInt8L2Norm } from "./semantic.js";
17
+ export function validateQueryOptions(opts) {
18
+ if (!opts)
19
+ return;
20
+ if (opts.topK !== undefined && (!Number.isInteger(opts.topK) || opts.topK < 1)) {
21
+ throw new Error("query(...): topK must be a positive integer.");
22
+ }
23
+ if (opts.minScore !== undefined && (!Number.isFinite(opts.minScore) || opts.minScore < 0)) {
24
+ throw new Error("query(...): minScore must be a finite number >= 0.");
25
+ }
26
+ if (opts.requirePhrases !== undefined && (!Array.isArray(opts.requirePhrases) || opts.requirePhrases.some((p) => typeof p !== "string"))) {
27
+ throw new Error("query(...): requirePhrases must be an array of strings when provided.");
28
+ }
29
+ validateStringOrStringArrayOption("namespace", opts.namespace);
30
+ validateStringOrStringArrayOption("source", opts.source);
31
+ if (opts.queryExpansion) {
32
+ const qe = opts.queryExpansion;
33
+ if (qe.enabled !== undefined && typeof qe.enabled !== "boolean") {
34
+ throw new Error("query(...): queryExpansion.enabled must be a boolean when provided.");
35
+ }
36
+ if (qe.docs !== undefined && (!Number.isInteger(qe.docs) || qe.docs < 1)) {
37
+ throw new Error("query(...): queryExpansion.docs must be a positive integer.");
38
+ }
39
+ if (qe.terms !== undefined && (!Number.isInteger(qe.terms) || qe.terms < 1)) {
40
+ throw new Error("query(...): queryExpansion.terms must be a positive integer.");
41
+ }
42
+ if (qe.weight !== undefined && (!Number.isFinite(qe.weight) || qe.weight < 0)) {
43
+ throw new Error("query(...): queryExpansion.weight must be a finite number >= 0.");
44
+ }
45
+ if (qe.minTermLength !== undefined && (!Number.isInteger(qe.minTermLength) || qe.minTermLength < 1)) {
46
+ throw new Error("query(...): queryExpansion.minTermLength must be a positive integer.");
47
+ }
48
+ }
49
+ validateSemanticQueryOptions(opts.semantic);
50
+ }
17
51
  export function validateSemanticQueryOptions(options) {
18
52
  if (!options)
19
53
  return;
@@ -48,7 +82,7 @@ export function validateSemanticQueryOptions(options) {
48
82
  }
49
83
  }
50
84
  export function query(pack, q, opts = {}) {
51
- validateSemanticQueryOptions(opts.semantic);
85
+ validateQueryOptions(opts);
52
86
  const topK = opts.topK ?? 10;
53
87
  const minScore = Number.isFinite(opts.minScore) ? Math.max(0, opts.minScore) : 0;
54
88
  const expansionOpts = {
@@ -419,3 +453,11 @@ function normalizeSourceFilter(input) {
419
453
  const values = Array.isArray(input) ? input : [input];
420
454
  return new Set(values.map((v) => normalize(v)).filter(Boolean));
421
455
  }
456
+ function validateStringOrStringArrayOption(name, value) {
457
+ if (value === undefined)
458
+ return;
459
+ const valid = typeof value === "string" || (Array.isArray(value) && value.every((entry) => typeof entry === "string"));
460
+ if (!valid) {
461
+ throw new Error(`query(...): ${name} must be a string or an array of strings when provided.`);
462
+ }
463
+ }
@@ -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
+ }