@vpxa/aikit 0.1.278 → 0.1.280
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/package.json +1 -1
- package/packages/cli/dist/index.js +3 -3
- package/packages/cli/dist/{init-jjI8OFjT.js → init-D9pqZcbt.js} +1 -1
- package/packages/cli/dist/{templates-CDa0UuoE.js → templates-BvYW6rrq.js} +15 -14
- package/packages/server/dist/bin.js +1 -1
- package/packages/server/dist/index.js +1 -1
- package/packages/server/dist/{server-Biloog6X.js → server-BFwmsrI5.js} +135 -135
- package/packages/server/dist/{server-D8nmZP6y.js → server-Bi0xdSS7.js} +135 -135
- package/packages/server/package.json +1 -0
- package/packages/tool-routing/dist/index.d.mts +164 -0
- package/packages/tool-routing/dist/index.mjs +403 -0
- package/packages/tool-routing/package.json +26 -0
- package/packages/tools/dist/index.d.ts +2 -0
- package/packages/tools/dist/index.js +72 -72
- package/packages/tools/package.json +1 -0
- package/scaffold/dist/adapters/claude-code.mjs +1 -1
- package/scaffold/dist/adapters/codex.mjs +1 -1
- package/scaffold/dist/adapters/copilot.mjs +17 -17
- package/scaffold/dist/adapters/flows.mjs +1 -1
- package/scaffold/dist/adapters/gemini.mjs +1 -1
- package/scaffold/dist/adapters/hooks.mjs +1 -1
- package/scaffold/dist/adapters/intellij.mjs +1 -1
- package/scaffold/dist/adapters/opencode.mjs +1 -1
- package/scaffold/dist/adapters/skills.mjs +1 -1
- package/scaffold/dist/adapters/zed.mjs +1 -1
- package/scaffold/dist/definitions/agents.mjs +1 -1
- package/scaffold/dist/definitions/bodies.mjs +10 -1
- package/scaffold/dist/definitions/policies.mjs +2 -2
- package/scaffold/dist/definitions/protocols.mjs +88 -27
- package/scaffold/dist/definitions/skills/c4-architecture.mjs +1 -1
- package/scaffold/dist/definitions/skills/index.mjs +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Core types for the tool routing system.
|
|
4
|
+
*
|
|
5
|
+
* A "route" maps a forbidden tool usage to an AI Kit alternative.
|
|
6
|
+
* Routes are the single source of truth consumed by:
|
|
7
|
+
* - Pre-dispatch compliance checks (check_tool_compliance tool)
|
|
8
|
+
* - Post-hoc replay scoring (compliance-scorer.ts)
|
|
9
|
+
* - Prompt-analysis compliance reporting
|
|
10
|
+
* - Agent instruction generation
|
|
11
|
+
*/
|
|
12
|
+
/** Severity level for a routing violation */
|
|
13
|
+
type RouteSeverity = 'error' | 'warning' | 'info';
|
|
14
|
+
/** Action the caller should take when a route matches */
|
|
15
|
+
type RouteAction = 'redirect' | 'warn' | 'allow';
|
|
16
|
+
/**
|
|
17
|
+
* A single tool routing rule.
|
|
18
|
+
*
|
|
19
|
+
* Rules match on tool name and optionally on command patterns or heuristics.
|
|
20
|
+
* When a tool call matches, the compliance system reports the alternative.
|
|
21
|
+
*/
|
|
22
|
+
interface ToolRoute {
|
|
23
|
+
/** Unique rule identifier (kebab-case, e.g. "no-terminal-test") */
|
|
24
|
+
readonly id: string;
|
|
25
|
+
/** The tool name that triggers this route (e.g. "grep_search", "run_in_terminal") */
|
|
26
|
+
readonly toolName: string;
|
|
27
|
+
/** Human-readable description of what this route enforces */
|
|
28
|
+
readonly description: string;
|
|
29
|
+
/** Suggested alternative tool(s) to use instead */
|
|
30
|
+
readonly alternative: string;
|
|
31
|
+
/** Why this routing exists — for user-facing messages */
|
|
32
|
+
readonly reason: string;
|
|
33
|
+
/** Severity when this route is violated */
|
|
34
|
+
readonly severity: RouteSeverity;
|
|
35
|
+
/** Action to take when this route matches */
|
|
36
|
+
readonly action: RouteAction;
|
|
37
|
+
/**
|
|
38
|
+
* Optional regex patterns to match against the command/input.
|
|
39
|
+
* Only evaluated when toolName matches AND command context is provided.
|
|
40
|
+
* Example: ['vitest', 'jest', 'pnpm test'] for run_in_terminal
|
|
41
|
+
*/
|
|
42
|
+
readonly commandPatterns?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Optional heuristic description for human-readable context.
|
|
45
|
+
* Not evaluated programmatically — used for reporting and documentation.
|
|
46
|
+
*/
|
|
47
|
+
readonly heuristic?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Result of a single route check */
|
|
50
|
+
interface RouteCheckResult {
|
|
51
|
+
/** Whether the tool usage is compliant */
|
|
52
|
+
readonly compliant: boolean;
|
|
53
|
+
/** The route that matched (undefined if no route matched) */
|
|
54
|
+
readonly route?: ToolRoute;
|
|
55
|
+
/** Suggested alternative tool */
|
|
56
|
+
readonly alternative?: string;
|
|
57
|
+
/** Human-readable message explaining the result */
|
|
58
|
+
readonly message?: string;
|
|
59
|
+
}
|
|
60
|
+
/** Context provided alongside a tool call for richer checking */
|
|
61
|
+
interface ToolCallContext {
|
|
62
|
+
/** The raw command string (for run_in_terminal checks) */
|
|
63
|
+
readonly command?: string;
|
|
64
|
+
/** Line range start (for read_file checks) */
|
|
65
|
+
readonly startLine?: number;
|
|
66
|
+
/** Line range end (for read_file checks) */
|
|
67
|
+
readonly endLine?: number;
|
|
68
|
+
/** Whether the caller is a subagent */
|
|
69
|
+
readonly isSubagent?: boolean;
|
|
70
|
+
/** Additional arbitrary context */
|
|
71
|
+
readonly [key: string]: unknown;
|
|
72
|
+
}
|
|
73
|
+
/** Registry of routes with add/remove/check operations */
|
|
74
|
+
interface RouteRegistry {
|
|
75
|
+
/** All registered routes */
|
|
76
|
+
readonly routes: readonly ToolRoute[];
|
|
77
|
+
/** Check a tool call against all registered routes */
|
|
78
|
+
check(toolName: string, context?: ToolCallContext): RouteCheckResult;
|
|
79
|
+
/** Check a tool call against all registered routes, returning ALL matching violations */
|
|
80
|
+
checkAll(toolName: string, context?: ToolCallContext): RouteCheckResult[];
|
|
81
|
+
/** Register a new route */
|
|
82
|
+
add(route: ToolRoute): void;
|
|
83
|
+
/** Remove a route by ID */
|
|
84
|
+
remove(id: string): boolean;
|
|
85
|
+
}
|
|
86
|
+
/** Options for initializing a RouteRegistry */
|
|
87
|
+
interface RouteRegistryOptions {
|
|
88
|
+
/** Initial routes to register (defaults to DEFAULT_ROUTES) */
|
|
89
|
+
routes?: readonly ToolRoute[];
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/checker.d.ts
|
|
93
|
+
/**
|
|
94
|
+
* Check a single route against a tool call.
|
|
95
|
+
* Returns a violation result if the route matches, null if compliant.
|
|
96
|
+
*/
|
|
97
|
+
declare function checkSingleRoute(route: ToolRoute, toolName: string, context?: ToolCallContext): RouteCheckResult | null;
|
|
98
|
+
/**
|
|
99
|
+
* Create a RouteRegistry with the given routes (defaults to DEFAULT_ROUTES).
|
|
100
|
+
*
|
|
101
|
+
* The registry provides:
|
|
102
|
+
* - `check(toolName, context?)` — single-result check (first matching route)
|
|
103
|
+
* - `checkAll(toolName, context?)` — ALL matching routes
|
|
104
|
+
* - `add(route)` — register a new route at runtime
|
|
105
|
+
* - `remove(id)` — deregister a route by ID
|
|
106
|
+
*/
|
|
107
|
+
declare function createRouteRegistry(options?: RouteRegistryOptions): RouteRegistry;
|
|
108
|
+
/**
|
|
109
|
+
* Convenience function: check a single tool call against DEFAULT_ROUTES.
|
|
110
|
+
*
|
|
111
|
+
* @param toolName — the tool being called
|
|
112
|
+
* @param context — optional context (command, line ranges, etc.)
|
|
113
|
+
* @returns RouteCheckResult with compliant status and violation details
|
|
114
|
+
*/
|
|
115
|
+
declare function checkToolRoute(toolName: string, context?: ToolCallContext): RouteCheckResult;
|
|
116
|
+
/**
|
|
117
|
+
* Convenience: get all routes that are violations (for summary/display).
|
|
118
|
+
*/
|
|
119
|
+
declare function getViolationSummary(results: RouteCheckResult[]): {
|
|
120
|
+
total: number;
|
|
121
|
+
errors: number;
|
|
122
|
+
warnings: number;
|
|
123
|
+
redirects: number;
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/prompt.d.ts
|
|
127
|
+
/**
|
|
128
|
+
* Generate the FORBIDDEN tool routing markdown table for agent instructions.
|
|
129
|
+
*
|
|
130
|
+
* This replaces the hardcoded tables in CLAUDE.md and AGENTS.md.
|
|
131
|
+
* Usage:
|
|
132
|
+
* console.log(generateRoutingTable()); // full routing table
|
|
133
|
+
*/
|
|
134
|
+
declare function generateRoutingTable(): string;
|
|
135
|
+
/**
|
|
136
|
+
* Generate compact routing rules text for the Orchestrator pre-dispatch gate.
|
|
137
|
+
* Returns a minimal set of instructions an agent should follow.
|
|
138
|
+
*/
|
|
139
|
+
declare function generateRoutingRules(): string;
|
|
140
|
+
/**
|
|
141
|
+
* Generate forbidden tool names list for programmatic use.
|
|
142
|
+
*/
|
|
143
|
+
declare function getForbiddenToolNames(severity?: ToolRoute['severity']): string[];
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/routes.d.ts
|
|
146
|
+
/**
|
|
147
|
+
* Default tool routing rules.
|
|
148
|
+
*
|
|
149
|
+
* This is the SINGLE source of truth for tool routing policies.
|
|
150
|
+
* Must mirror scaffold/definitions/policies.mjs — update both in sync.
|
|
151
|
+
* All downstream consumers (compliance-scorer, check_tool_compliance tool,
|
|
152
|
+
* prompt-analysis, agent instructions) derive from this list.
|
|
153
|
+
*/
|
|
154
|
+
declare const DEFAULT_ROUTES: readonly ToolRoute[];
|
|
155
|
+
/**
|
|
156
|
+
* Get routes filtered by severity.
|
|
157
|
+
*/
|
|
158
|
+
declare function getRoutesBySeverity(routes: readonly ToolRoute[], severity: ToolRoute['severity']): ToolRoute[];
|
|
159
|
+
/**
|
|
160
|
+
* Get routes for a specific tool name.
|
|
161
|
+
*/
|
|
162
|
+
declare function getRoutesForTool(routes: readonly ToolRoute[], toolName: string): ToolRoute[];
|
|
163
|
+
//#endregion
|
|
164
|
+
export { DEFAULT_ROUTES, type RouteAction, type RouteCheckResult, type RouteRegistry, type RouteRegistryOptions, type RouteSeverity, type ToolCallContext, type ToolRoute, checkSingleRoute, checkToolRoute, createRouteRegistry, generateRoutingRules, generateRoutingTable, getForbiddenToolNames, getRoutesBySeverity, getRoutesForTool, getViolationSummary };
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
//#region src/routes.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default tool routing rules.
|
|
4
|
+
*
|
|
5
|
+
* This is the SINGLE source of truth for tool routing policies.
|
|
6
|
+
* Must mirror scaffold/definitions/policies.mjs — update both in sync.
|
|
7
|
+
* All downstream consumers (compliance-scorer, check_tool_compliance tool,
|
|
8
|
+
* prompt-analysis, agent instructions) derive from this list.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_ROUTES = [
|
|
11
|
+
{
|
|
12
|
+
id: "no-grep-search",
|
|
13
|
+
toolName: "grep_search",
|
|
14
|
+
description: "grep_search / semantic_search → use search",
|
|
15
|
+
alternative: "search({ query })",
|
|
16
|
+
reason: "Hybrid search across all indexed + curated content — richer results than grep",
|
|
17
|
+
severity: "error",
|
|
18
|
+
action: "redirect"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "no-semantic-search",
|
|
22
|
+
toolName: "semantic_search",
|
|
23
|
+
description: "semantic_search → use search",
|
|
24
|
+
alternative: "search({ query })",
|
|
25
|
+
reason: "Hybrid search across all indexed + curated content — vector + FTS fusion",
|
|
26
|
+
severity: "error",
|
|
27
|
+
action: "redirect"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "no-grep-symbol",
|
|
31
|
+
toolName: "grep_search",
|
|
32
|
+
description: "grep_search for a symbol → use symbol",
|
|
33
|
+
alternative: "symbol({ name })",
|
|
34
|
+
reason: "Definition + references with scope and call context",
|
|
35
|
+
severity: "error",
|
|
36
|
+
action: "redirect",
|
|
37
|
+
heuristic: "grep_search for a specific symbol name"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "no-read-file-understanding",
|
|
41
|
+
toolName: "read_file",
|
|
42
|
+
description: "read_file to understand a file → use file_summary",
|
|
43
|
+
alternative: "file_summary({ path })",
|
|
44
|
+
reason: "Structure, exports, imports — 10x fewer tokens than raw file reads",
|
|
45
|
+
severity: "error",
|
|
46
|
+
action: "warn",
|
|
47
|
+
heuristic: "read_file with range > 50 lines and no subsequent edit"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "no-read-file-find-code",
|
|
51
|
+
toolName: "read_file",
|
|
52
|
+
description: "read_file to find specific code → use compact",
|
|
53
|
+
alternative: "compact({ path, query })",
|
|
54
|
+
reason: "Fresh compression: compact({ path, query }); query is required unless ref is supplied",
|
|
55
|
+
severity: "error",
|
|
56
|
+
action: "warn",
|
|
57
|
+
heuristic: "read_file to locate specific code without editing"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "no-read-file-multiple",
|
|
61
|
+
toolName: "read_file",
|
|
62
|
+
description: "Multiple read_file calls → use digest",
|
|
63
|
+
alternative: "digest({ sources, query: \"<task description>\" })",
|
|
64
|
+
reason: "Compresses multiple files into token-budgeted summary",
|
|
65
|
+
severity: "error",
|
|
66
|
+
action: "warn",
|
|
67
|
+
heuristic: "consecutive read_file calls on different files without edits"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "no-read-file-large",
|
|
71
|
+
toolName: "read_file",
|
|
72
|
+
description: "read_file (>50 lines to understand) → use file_summary → compact → digest",
|
|
73
|
+
alternative: "file_summary → compact → digest",
|
|
74
|
+
reason: "Use compressed context, not raw reads — 10x fewer tokens",
|
|
75
|
+
severity: "warning",
|
|
76
|
+
action: "warn",
|
|
77
|
+
heuristic: "read_file with endLine - startLine > 50 and no subsequent edit on same file"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "no-ctxc-misuse",
|
|
81
|
+
toolName: "compact",
|
|
82
|
+
description: "Cached ctxc ref to refocus prior output → use compact with ref",
|
|
83
|
+
alternative: "compact({ ref }) or compact({ ref, query? })",
|
|
84
|
+
reason: "ctxc_... values are reversible refs, not ids or file paths. Prefer enrich: true on follow-up retrieval",
|
|
85
|
+
severity: "error",
|
|
86
|
+
action: "warn",
|
|
87
|
+
heuristic: "using ctxc_ value as a file path instead of as a ref"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "no-terminal-test",
|
|
91
|
+
toolName: "run_in_terminal",
|
|
92
|
+
description: "run_in_terminal for tests → use test_run",
|
|
93
|
+
alternative: "test_run({})",
|
|
94
|
+
reason: "Run tests with structured output — no shell needed",
|
|
95
|
+
severity: "error",
|
|
96
|
+
action: "redirect",
|
|
97
|
+
commandPatterns: [
|
|
98
|
+
"vitest",
|
|
99
|
+
"jest",
|
|
100
|
+
"mocha",
|
|
101
|
+
"pnpm test",
|
|
102
|
+
"npm test"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "no-terminal-typecheck",
|
|
107
|
+
toolName: "run_in_terminal",
|
|
108
|
+
description: "run_in_terminal for tsc/lint → use check",
|
|
109
|
+
alternative: "check({})",
|
|
110
|
+
reason: "Typecheck + lint combined, summary output — no shell needed",
|
|
111
|
+
severity: "error",
|
|
112
|
+
action: "redirect",
|
|
113
|
+
commandPatterns: [
|
|
114
|
+
"\\btsc\\b",
|
|
115
|
+
"pnpm check",
|
|
116
|
+
"pnpm typecheck",
|
|
117
|
+
"pnpm lint",
|
|
118
|
+
"biome"
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "no-terminal-grep",
|
|
123
|
+
toolName: "run_in_terminal",
|
|
124
|
+
description: "run_in_terminal for find/grep → use find or search",
|
|
125
|
+
alternative: "find({ pattern }) or search({ query })",
|
|
126
|
+
reason: "No shell needed, richer results with AI Kit search",
|
|
127
|
+
severity: "error",
|
|
128
|
+
action: "redirect",
|
|
129
|
+
commandPatterns: [
|
|
130
|
+
"\\bgrep\\b",
|
|
131
|
+
"\\bfind\\b",
|
|
132
|
+
"\\brg\\b",
|
|
133
|
+
"Select-String"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "no-terminal-code-edits",
|
|
138
|
+
toolName: "run_in_terminal",
|
|
139
|
+
description: "run_in_terminal for code edits → use replace_string_in_file",
|
|
140
|
+
alternative: "replace_string_in_file",
|
|
141
|
+
reason: "Avoid shell-edit loops; use native edit tool instead",
|
|
142
|
+
severity: "error",
|
|
143
|
+
action: "redirect",
|
|
144
|
+
commandPatterns: [
|
|
145
|
+
"sed",
|
|
146
|
+
"awk",
|
|
147
|
+
"Set-Content",
|
|
148
|
+
"Out-File",
|
|
149
|
+
">>",
|
|
150
|
+
"\\| tee"
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "no-edit-without-reading",
|
|
155
|
+
toolName: "replace_string_in_file",
|
|
156
|
+
description: "Editing without reading → use file_summary then targeted read_file",
|
|
157
|
+
alternative: "file_summary({ path }) → read_file({ path, offset, limit })",
|
|
158
|
+
reason: "Safer edits: understand structure before modifying",
|
|
159
|
+
severity: "error",
|
|
160
|
+
action: "warn",
|
|
161
|
+
heuristic: "replace_string_in_file on a file not previously read"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "no-get-changed-files",
|
|
165
|
+
toolName: "get_changed_files",
|
|
166
|
+
description: "get_changed_files → use run_in_terminal with git diff",
|
|
167
|
+
alternative: "run_in_terminal with `git diff <specific-file>`",
|
|
168
|
+
reason: "Diff only target file instead of all uncommitted diffs (100K+ tokens)",
|
|
169
|
+
severity: "error",
|
|
170
|
+
action: "redirect"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "no-fetch-webpage",
|
|
174
|
+
toolName: "fetch_webpage",
|
|
175
|
+
description: "fetch_webpage → use web_fetch",
|
|
176
|
+
alternative: "web_fetch({ url })",
|
|
177
|
+
reason: "Readability extract + token budget — richer output than raw fetch",
|
|
178
|
+
severity: "error",
|
|
179
|
+
action: "redirect"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "no-subagent-present",
|
|
183
|
+
toolName: "present",
|
|
184
|
+
description: "present (from subagent) → return structured text",
|
|
185
|
+
alternative: "Return findings as structured text",
|
|
186
|
+
reason: "Subagent present calls are invisible to the user (only Orchestrator should present)",
|
|
187
|
+
severity: "error",
|
|
188
|
+
action: "warn",
|
|
189
|
+
heuristic: "present called in subagent context"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "no-apply-patch",
|
|
193
|
+
toolName: "apply_patch",
|
|
194
|
+
description: "apply_patch → use native edit tool",
|
|
195
|
+
alternative: "edit file tool",
|
|
196
|
+
reason: "AI Kit does not manage apply_patch; use the host environment edit tool",
|
|
197
|
+
severity: "warning",
|
|
198
|
+
action: "warn"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "no-memory-native",
|
|
202
|
+
toolName: "memory",
|
|
203
|
+
description: "memory tool → use knowledge tool",
|
|
204
|
+
alternative: "knowledge (remember / search / list)",
|
|
205
|
+
reason: "AI Kit knowledge tool provides persistent cross-session memory with categories and tags",
|
|
206
|
+
severity: "warning",
|
|
207
|
+
action: "warn"
|
|
208
|
+
}
|
|
209
|
+
];
|
|
210
|
+
/**
|
|
211
|
+
* Get routes filtered by severity.
|
|
212
|
+
*/
|
|
213
|
+
function getRoutesBySeverity(routes, severity) {
|
|
214
|
+
return routes.filter((route) => route.severity === severity);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get routes for a specific tool name.
|
|
218
|
+
*/
|
|
219
|
+
function getRoutesForTool(routes, toolName) {
|
|
220
|
+
return routes.filter((route) => route.toolName === toolName);
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/checker.ts
|
|
224
|
+
/**
|
|
225
|
+
* Check if a command string matches any of the given patterns.
|
|
226
|
+
* Each pattern is a RegExp source string — tested case-insensitively.
|
|
227
|
+
*/
|
|
228
|
+
function matchesCommandPattern(command, patterns) {
|
|
229
|
+
for (const pattern of patterns) try {
|
|
230
|
+
if (new RegExp(pattern, "i").test(command)) return true;
|
|
231
|
+
} catch {}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Check a single route against a tool call.
|
|
236
|
+
* Returns a violation result if the route matches, null if compliant.
|
|
237
|
+
*/
|
|
238
|
+
function checkSingleRoute(route, toolName, context) {
|
|
239
|
+
if (route.toolName !== toolName) return null;
|
|
240
|
+
if (route.commandPatterns && route.commandPatterns.length > 0) {
|
|
241
|
+
if (!context?.command) return null;
|
|
242
|
+
if (!matchesCommandPattern(context.command, route.commandPatterns)) return null;
|
|
243
|
+
}
|
|
244
|
+
if (route.toolName === "read_file" && route.heuristic?.includes("read_file")) {
|
|
245
|
+
const startLine = context?.startLine;
|
|
246
|
+
const endLine = context?.endLine;
|
|
247
|
+
if (startLine === void 0 || endLine === void 0) return null;
|
|
248
|
+
if (endLine - startLine + 1 <= 50) return null;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
compliant: false,
|
|
252
|
+
route,
|
|
253
|
+
alternative: route.alternative,
|
|
254
|
+
message: buildViolationMessage(route, context)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Build a human-readable violation message.
|
|
259
|
+
*/
|
|
260
|
+
function buildViolationMessage(route, _context) {
|
|
261
|
+
return [
|
|
262
|
+
`${route.description}.`,
|
|
263
|
+
`Use \`${route.alternative}\` instead.`,
|
|
264
|
+
route.reason
|
|
265
|
+
].join(" ");
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Create a RouteRegistry with the given routes (defaults to DEFAULT_ROUTES).
|
|
269
|
+
*
|
|
270
|
+
* The registry provides:
|
|
271
|
+
* - `check(toolName, context?)` — single-result check (first matching route)
|
|
272
|
+
* - `checkAll(toolName, context?)` — ALL matching routes
|
|
273
|
+
* - `add(route)` — register a new route at runtime
|
|
274
|
+
* - `remove(id)` — deregister a route by ID
|
|
275
|
+
*/
|
|
276
|
+
function createRouteRegistry(options) {
|
|
277
|
+
const routes = [...options?.routes ?? DEFAULT_ROUTES];
|
|
278
|
+
return {
|
|
279
|
+
get routes() {
|
|
280
|
+
return [...routes];
|
|
281
|
+
},
|
|
282
|
+
check(toolName, context) {
|
|
283
|
+
for (const route of routes) {
|
|
284
|
+
const result = checkSingleRoute(route, toolName, context);
|
|
285
|
+
if (result !== null) return result;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
compliant: true,
|
|
289
|
+
message: `No routing violations for tool: ${toolName}`
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
checkAll(toolName, context) {
|
|
293
|
+
const results = [];
|
|
294
|
+
for (const route of routes) {
|
|
295
|
+
const result = checkSingleRoute(route, toolName, context);
|
|
296
|
+
if (result !== null) results.push(result);
|
|
297
|
+
}
|
|
298
|
+
return results;
|
|
299
|
+
},
|
|
300
|
+
add(route) {
|
|
301
|
+
const existingIndex = routes.findIndex((r) => r.id === route.id);
|
|
302
|
+
if (existingIndex >= 0) routes[existingIndex] = route;
|
|
303
|
+
else routes.push(route);
|
|
304
|
+
},
|
|
305
|
+
remove(id) {
|
|
306
|
+
const index = routes.findIndex((r) => r.id === id);
|
|
307
|
+
if (index >= 0) {
|
|
308
|
+
routes.splice(index, 1);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const defaultRegistry = /*#__PURE__*/ createRouteRegistry();
|
|
316
|
+
/**
|
|
317
|
+
* Convenience function: check a single tool call against DEFAULT_ROUTES.
|
|
318
|
+
*
|
|
319
|
+
* @param toolName — the tool being called
|
|
320
|
+
* @param context — optional context (command, line ranges, etc.)
|
|
321
|
+
* @returns RouteCheckResult with compliant status and violation details
|
|
322
|
+
*/
|
|
323
|
+
function checkToolRoute(toolName, context) {
|
|
324
|
+
if (toolName.startsWith("mcp_aikit_") || toolName.startsWith("aikit_")) return {
|
|
325
|
+
compliant: true,
|
|
326
|
+
message: `${toolName} is an AI Kit tool — always compliant`
|
|
327
|
+
};
|
|
328
|
+
return defaultRegistry.check(toolName, context);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Convenience: get all routes that are violations (for summary/display).
|
|
332
|
+
*/
|
|
333
|
+
function getViolationSummary(results) {
|
|
334
|
+
const violations = results.filter((r) => !r.compliant && r.route);
|
|
335
|
+
const errors = violations.filter((r) => r.route?.severity === "error");
|
|
336
|
+
const warnings = violations.filter((r) => r.route?.severity === "warning");
|
|
337
|
+
return {
|
|
338
|
+
total: violations.length,
|
|
339
|
+
errors: errors.length,
|
|
340
|
+
warnings: warnings.length,
|
|
341
|
+
redirects: errors.filter((r) => r.route?.action === "redirect").length
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/prompt.ts
|
|
346
|
+
/**
|
|
347
|
+
* Generate the FORBIDDEN tool routing markdown table for agent instructions.
|
|
348
|
+
*
|
|
349
|
+
* This replaces the hardcoded tables in CLAUDE.md and AGENTS.md.
|
|
350
|
+
* Usage:
|
|
351
|
+
* console.log(generateRoutingTable()); // full routing table
|
|
352
|
+
*/
|
|
353
|
+
function generateRoutingTable() {
|
|
354
|
+
const errorRoutes = getRoutesBySeverity(DEFAULT_ROUTES, "error");
|
|
355
|
+
const warningRoutes = getRoutesBySeverity(DEFAULT_ROUTES, "warning");
|
|
356
|
+
const header = "| NEVER use this | USE THIS AI Kit TOOL INSTEAD | Why |";
|
|
357
|
+
const separator = "|---|---|---|";
|
|
358
|
+
const errorRows = errorRoutes.map((r) => formatTableRow(r));
|
|
359
|
+
const warningRows = warningRoutes.map((r) => formatTableRow(r));
|
|
360
|
+
const parts = [
|
|
361
|
+
"## Tool Routing Rules",
|
|
362
|
+
"",
|
|
363
|
+
"These rules are enforced by AI Kit compliance checking. Violations are flagged in reports.",
|
|
364
|
+
"",
|
|
365
|
+
"### ⛔ Errors — DO NOT use these tools",
|
|
366
|
+
header,
|
|
367
|
+
separator,
|
|
368
|
+
...errorRows
|
|
369
|
+
];
|
|
370
|
+
if (warningRows.length > 0) parts.push("", "### ⚠️ Warnings — Prefer alternatives", header, separator, ...warningRows);
|
|
371
|
+
return parts.join("\n");
|
|
372
|
+
}
|
|
373
|
+
function formatTableRow(route) {
|
|
374
|
+
return `| ${route.commandPatterns ? `\`${route.toolName}\` (for ${route.heuristic ?? route.description})` : `\`${route.toolName}\``} | ${`\`${route.alternative}\``} | ${route.reason} |`;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Generate compact routing rules text for the Orchestrator pre-dispatch gate.
|
|
378
|
+
* Returns a minimal set of instructions an agent should follow.
|
|
379
|
+
*/
|
|
380
|
+
function generateRoutingRules() {
|
|
381
|
+
return [
|
|
382
|
+
"### Tool Compliance Rules",
|
|
383
|
+
"Before calling any tool, check these rules:",
|
|
384
|
+
...getRoutesBySeverity(DEFAULT_ROUTES, "error").map((r) => {
|
|
385
|
+
if (r.commandPatterns && r.commandPatterns.length > 0) {
|
|
386
|
+
const patterns = r.commandPatterns.join(", ");
|
|
387
|
+
return `- NEVER \`${r.toolName}\` for (${patterns}) — use \`${r.alternative}\` instead`;
|
|
388
|
+
}
|
|
389
|
+
return `- NEVER \`${r.toolName}\` — use \`${r.alternative}\` instead`;
|
|
390
|
+
}),
|
|
391
|
+
"",
|
|
392
|
+
"If a tool call violates these rules, use the suggested alternative."
|
|
393
|
+
].join("\n");
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Generate forbidden tool names list for programmatic use.
|
|
397
|
+
*/
|
|
398
|
+
function getForbiddenToolNames(severity) {
|
|
399
|
+
const routes = severity ? getRoutesBySeverity(DEFAULT_ROUTES, severity) : [...DEFAULT_ROUTES];
|
|
400
|
+
return [...new Set(routes.map((r) => r.toolName))].sort();
|
|
401
|
+
}
|
|
402
|
+
//#endregion
|
|
403
|
+
export { DEFAULT_ROUTES, checkSingleRoute, checkToolRoute, createRouteRegistry, generateRoutingRules, generateRoutingTable, getForbiddenToolNames, getRoutesBySeverity, getRoutesForTool, getViolationSummary };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aikit/tool-routing",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tool routing rules and pre-dispatch compliance checker — single source of truth for tool routing policies",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.mts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsdown",
|
|
17
|
+
"clean": "rimraf dist",
|
|
18
|
+
"test": "vitest run"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"rimraf": "^6.x",
|
|
22
|
+
"tsdown": "^0.x",
|
|
23
|
+
"typescript": "^6.x",
|
|
24
|
+
"vitest": "^4.x"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -503,6 +503,8 @@ interface ReplayEntry {
|
|
|
503
503
|
traceId: string;
|
|
504
504
|
/** Original output size in characters before truncation */
|
|
505
505
|
outputChars: number;
|
|
506
|
+
/** Original input size in characters before truncation */
|
|
507
|
+
inputChars: number;
|
|
506
508
|
}
|
|
507
509
|
interface ReplayOptions {
|
|
508
510
|
/** Max entries to return (default: 20) */
|