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 +220 -4
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/router.d.ts +28 -0
- package/dist/router.js +74 -0
- package/dist/routing_profile.d.ts +19 -0
- package/dist/routing_profile.js +102 -0
- package/dist/tool_gate.d.ts +3 -0
- package/dist/tool_gate.js +8 -0
- package/dist/tool_parse.d.ts +2 -0
- package/dist/tool_parse.js +102 -0
- package/dist/tools.d.ts +27 -0
- package/dist/tools.js +34 -0
- package/dist/trace.d.ts +45 -0
- package/dist/trace.js +12 -0
- package/package.json +1 -1
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.
|
|
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 (
|
|
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
|
-
## 🗺️
|
|
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';
|
package/dist/router.d.ts
ADDED
|
@@ -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,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,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
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/trace.d.ts
ADDED
|
@@ -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