wu-framework 1.1.15 → 1.1.17
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 +52 -20
- package/dist/wu-framework.cjs.js +1 -1
- package/dist/wu-framework.cjs.js.map +1 -1
- package/dist/wu-framework.dev.js +15511 -15146
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +1 -1
- package/dist/wu-framework.esm.js.map +1 -1
- package/dist/wu-framework.umd.js +1 -1
- package/dist/wu-framework.umd.js.map +1 -1
- package/package.json +166 -161
- package/src/adapters/angular/ai.js +30 -30
- package/src/adapters/angular/index.d.ts +154 -154
- package/src/adapters/angular/index.js +932 -932
- package/src/adapters/angular.d.ts +3 -3
- package/src/adapters/angular.js +3 -3
- package/src/adapters/index.js +168 -168
- package/src/adapters/lit/ai.js +20 -20
- package/src/adapters/lit/index.d.ts +120 -120
- package/src/adapters/lit/index.js +721 -721
- package/src/adapters/lit.d.ts +3 -3
- package/src/adapters/lit.js +3 -3
- package/src/adapters/preact/ai.js +33 -33
- package/src/adapters/preact/index.d.ts +108 -108
- package/src/adapters/preact/index.js +661 -661
- package/src/adapters/preact.d.ts +3 -3
- package/src/adapters/preact.js +3 -3
- package/src/adapters/react/index.js +48 -54
- package/src/adapters/react.d.ts +3 -3
- package/src/adapters/react.js +3 -3
- package/src/adapters/shared.js +64 -64
- package/src/adapters/solid/ai.js +32 -32
- package/src/adapters/solid/index.d.ts +101 -101
- package/src/adapters/solid/index.js +586 -586
- package/src/adapters/solid.d.ts +3 -3
- package/src/adapters/solid.js +3 -3
- package/src/adapters/svelte/ai.js +31 -31
- package/src/adapters/svelte/index.d.ts +166 -166
- package/src/adapters/svelte/index.js +798 -798
- package/src/adapters/svelte.d.ts +3 -3
- package/src/adapters/svelte.js +3 -3
- package/src/adapters/vanilla/ai.js +30 -30
- package/src/adapters/vanilla/index.d.ts +179 -179
- package/src/adapters/vanilla/index.js +785 -785
- package/src/adapters/vanilla.d.ts +3 -3
- package/src/adapters/vanilla.js +3 -3
- package/src/adapters/vue/ai.js +52 -52
- package/src/adapters/vue/index.d.ts +299 -299
- package/src/adapters/vue/index.js +610 -610
- package/src/adapters/vue.d.ts +3 -3
- package/src/adapters/vue.js +3 -3
- package/src/ai/wu-ai-actions.js +261 -261
- package/src/ai/wu-ai-agent.js +546 -546
- package/src/ai/wu-ai-browser-primitives.js +354 -354
- package/src/ai/wu-ai-browser.js +380 -380
- package/src/ai/wu-ai-context.js +332 -332
- package/src/ai/wu-ai-conversation.js +613 -613
- package/src/ai/wu-ai-orchestrate.js +1021 -1021
- package/src/ai/wu-ai-permissions.js +381 -381
- package/src/ai/wu-ai-provider.js +700 -700
- package/src/ai/wu-ai-schema.js +225 -225
- package/src/ai/wu-ai-triggers.js +396 -396
- package/src/ai/wu-ai.js +804 -804
- package/src/core/wu-app.js +236 -236
- package/src/core/wu-cache.js +498 -477
- package/src/core/wu-core.js +1412 -1398
- package/src/core/wu-error-boundary.js +396 -382
- package/src/core/wu-event-bus.js +390 -348
- package/src/core/wu-hooks.js +350 -350
- package/src/core/wu-html-parser.js +199 -190
- package/src/core/wu-iframe-sandbox.js +328 -328
- package/src/core/wu-loader.js +385 -273
- package/src/core/wu-logger.js +142 -134
- package/src/core/wu-manifest.js +532 -509
- package/src/core/wu-mcp-bridge.js +432 -432
- package/src/core/wu-overrides.js +510 -510
- package/src/core/wu-performance.js +228 -228
- package/src/core/wu-plugin.js +401 -348
- package/src/core/wu-prefetch.js +414 -414
- package/src/core/wu-proxy-sandbox.js +477 -476
- package/src/core/wu-sandbox.js +779 -779
- package/src/core/wu-script-executor.js +161 -113
- package/src/core/wu-snapshot-sandbox.js +227 -227
- package/src/core/wu-store.js +13 -3
- package/src/core/wu-strategies.js +256 -256
- package/src/core/wu-style-bridge.js +477 -477
- package/src/index.d.ts +317 -0
- package/src/index.js +234 -224
- package/src/utils/dependency-resolver.js +327 -327
package/src/ai/wu-ai-schema.js
CHANGED
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WU-AI-SCHEMA: Tool schema generation for function calling
|
|
3
|
-
*
|
|
4
|
-
* Converts wu.ai.action() definitions into the canonical tool format
|
|
5
|
-
* that providers consume. Also handles input sanitization for prompts.
|
|
6
|
-
*
|
|
7
|
-
* Canonical tool format:
|
|
8
|
-
* { name: string, description: string, parameters: JSONSchema }
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// ─── Sanitization ────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
const SENSITIVE_KEYS = ['password', 'token', 'apiKey', 'secret', 'credential', 'authorization', 'cookie', 'session'];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Sanitize data before injecting into prompts.
|
|
17
|
-
* Prevents prompt injection and redacts sensitive fields.
|
|
18
|
-
*
|
|
19
|
-
* @param {*} data - Any value to sanitize
|
|
20
|
-
* @param {number} [maxChars=2000] - Max chars per value
|
|
21
|
-
* @returns {string} Safe string representation
|
|
22
|
-
*/
|
|
23
|
-
export function sanitizeForPrompt(data, maxChars = 2000) {
|
|
24
|
-
if (data === null || data === undefined) return 'null';
|
|
25
|
-
if (typeof data === 'function') return '[Function]';
|
|
26
|
-
if (typeof data === 'symbol') return '[Symbol]';
|
|
27
|
-
|
|
28
|
-
if (typeof data === 'string') {
|
|
29
|
-
const truncated = data.length > maxChars ? data.slice(0, maxChars) + '...[truncated]' : data;
|
|
30
|
-
return `<user_data>${truncated}</user_data>`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (typeof data === 'number' || typeof data === 'boolean') {
|
|
34
|
-
return String(data);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (typeof data === 'object') {
|
|
38
|
-
const redacted = redactSensitive(data);
|
|
39
|
-
const json = JSON.stringify(redacted);
|
|
40
|
-
if (json.length > maxChars) {
|
|
41
|
-
return `<user_data>${json.slice(0, maxChars)}...[truncated]</user_data>`;
|
|
42
|
-
}
|
|
43
|
-
return `<user_data>${json}</user_data>`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return String(data).slice(0, maxChars);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Deep-clone an object, replacing sensitive keys with [REDACTED].
|
|
51
|
-
*
|
|
52
|
-
* @param {*} obj
|
|
53
|
-
* @param {number} [depth=0]
|
|
54
|
-
* @returns {*}
|
|
55
|
-
*/
|
|
56
|
-
export function redactSensitive(obj, depth = 0) {
|
|
57
|
-
if (depth > 10) return '[MAX_DEPTH]';
|
|
58
|
-
if (obj === null || obj === undefined) return obj;
|
|
59
|
-
if (typeof obj !== 'object') return obj;
|
|
60
|
-
|
|
61
|
-
if (Array.isArray(obj)) {
|
|
62
|
-
return obj.map(item => redactSensitive(item, depth + 1));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const result = {};
|
|
66
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
67
|
-
const lowerKey = key.toLowerCase();
|
|
68
|
-
if (SENSITIVE_KEYS.some(sk => lowerKey.includes(sk.toLowerCase()))) {
|
|
69
|
-
result[key] = '[REDACTED]';
|
|
70
|
-
} else {
|
|
71
|
-
result[key] = redactSensitive(value, depth + 1);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ─── Template Interpolation ──────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Interpolate {{var}} placeholders in a template string.
|
|
81
|
-
* Supports dot-notation: {{data.user.name}}
|
|
82
|
-
*
|
|
83
|
-
* @param {string} template
|
|
84
|
-
* @param {object} vars - Variable map { data: ..., context: ... }
|
|
85
|
-
* @returns {string}
|
|
86
|
-
*/
|
|
87
|
-
export function interpolate(template, vars) {
|
|
88
|
-
return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_match, path) => {
|
|
89
|
-
const value = path.split('.').reduce((obj, key) => obj?.[key], vars);
|
|
90
|
-
if (value === undefined || value === null) return '';
|
|
91
|
-
if (typeof value === 'object') return sanitizeForPrompt(value);
|
|
92
|
-
return String(value);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ─── Tool Schema Builder ─────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Build canonical tool definitions from registered actions.
|
|
100
|
-
*
|
|
101
|
-
* @param {Map<string, object>} actions - Map of action name → config
|
|
102
|
-
* @returns {Array<{ name: string, description: string, parameters: object }>}
|
|
103
|
-
*/
|
|
104
|
-
export function buildToolSchemas(actions) {
|
|
105
|
-
const tools = [];
|
|
106
|
-
|
|
107
|
-
for (const [name, config] of actions) {
|
|
108
|
-
tools.push({
|
|
109
|
-
name,
|
|
110
|
-
description: config.description || `Execute action: ${name}`,
|
|
111
|
-
parameters: normalizeParameters(config.parameters),
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return tools;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Normalize user-provided parameter definitions into JSON Schema.
|
|
120
|
-
*
|
|
121
|
-
* Accepts two formats:
|
|
122
|
-
* 1. Full JSON Schema: { type: 'object', properties: {...}, required: [...] }
|
|
123
|
-
* 2. Shorthand: { message: { type: 'string', required: true }, count: { type: 'number' } }
|
|
124
|
-
*
|
|
125
|
-
* @param {object} params
|
|
126
|
-
* @returns {object} Valid JSON Schema
|
|
127
|
-
*/
|
|
128
|
-
export function normalizeParameters(params) {
|
|
129
|
-
if (!params || typeof params !== 'object') {
|
|
130
|
-
return { type: 'object', properties: {}, required: [] };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Already a JSON Schema
|
|
134
|
-
if (params.type === 'object' && params.properties) {
|
|
135
|
-
return params;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Shorthand format — convert
|
|
139
|
-
const properties = {};
|
|
140
|
-
const required = [];
|
|
141
|
-
|
|
142
|
-
for (const [key, def] of Object.entries(params)) {
|
|
143
|
-
if (typeof def === 'string') {
|
|
144
|
-
// Simplest: { message: 'string' }
|
|
145
|
-
properties[key] = { type: def };
|
|
146
|
-
} else if (typeof def === 'object') {
|
|
147
|
-
const { required: isRequired, ...rest } = def;
|
|
148
|
-
properties[key] = rest.type ? rest : { type: 'string', ...rest };
|
|
149
|
-
if (isRequired) required.push(key);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return { type: 'object', properties, required };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Validate params against a JSON Schema (lightweight, no external deps).
|
|
158
|
-
* Only checks type, required, and enum — not full JSON Schema validation.
|
|
159
|
-
*
|
|
160
|
-
* @param {object} params - Actual params from LLM
|
|
161
|
-
* @param {object} schema - JSON Schema from normalizeParameters()
|
|
162
|
-
* @returns {{ valid: boolean, errors: string[] }}
|
|
163
|
-
*/
|
|
164
|
-
export function validateParams(params, schema) {
|
|
165
|
-
const errors = [];
|
|
166
|
-
if (!schema || !schema.properties) return { valid: true, errors };
|
|
167
|
-
|
|
168
|
-
// Check required
|
|
169
|
-
for (const key of (schema.required || [])) {
|
|
170
|
-
if (params[key] === undefined || params[key] === null) {
|
|
171
|
-
errors.push(`'${key}' is required`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Check types and enums
|
|
176
|
-
for (const [key, def] of Object.entries(schema.properties)) {
|
|
177
|
-
const value = params[key];
|
|
178
|
-
if (value === undefined || value === null) continue;
|
|
179
|
-
|
|
180
|
-
if (def.type && def.type !== 'any') {
|
|
181
|
-
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
182
|
-
if (def.type === 'integer') {
|
|
183
|
-
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
184
|
-
errors.push(`'${key}' must be integer, got ${actualType}`);
|
|
185
|
-
}
|
|
186
|
-
} else if (def.type !== actualType) {
|
|
187
|
-
errors.push(`'${key}' must be ${def.type}, got ${actualType}`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (def.enum && !def.enum.includes(value)) {
|
|
192
|
-
errors.push(`'${key}' must be one of [${def.enum.join(', ')}], got '${value}'`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return { valid: errors.length === 0, errors };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ─── Context Budget ──────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Estimate token count from character count.
|
|
203
|
-
* Rough heuristic: 1 token ≈ 4 chars for English, ≈ 2 chars for CJK.
|
|
204
|
-
*
|
|
205
|
-
* @param {string} text
|
|
206
|
-
* @param {number} [charRatio=4]
|
|
207
|
-
* @returns {number}
|
|
208
|
-
*/
|
|
209
|
-
export function estimateTokens(text, charRatio = 4) {
|
|
210
|
-
return Math.ceil(text.length / charRatio);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Truncate text to fit within a token budget.
|
|
215
|
-
*
|
|
216
|
-
* @param {string} text
|
|
217
|
-
* @param {number} maxTokens
|
|
218
|
-
* @param {number} [charRatio=4]
|
|
219
|
-
* @returns {string}
|
|
220
|
-
*/
|
|
221
|
-
export function truncateToTokenBudget(text, maxTokens, charRatio = 4) {
|
|
222
|
-
const maxChars = maxTokens * charRatio;
|
|
223
|
-
if (text.length <= maxChars) return text;
|
|
224
|
-
return text.slice(0, maxChars) + '\n...[truncated to fit token budget]';
|
|
225
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* WU-AI-SCHEMA: Tool schema generation for function calling
|
|
3
|
+
*
|
|
4
|
+
* Converts wu.ai.action() definitions into the canonical tool format
|
|
5
|
+
* that providers consume. Also handles input sanitization for prompts.
|
|
6
|
+
*
|
|
7
|
+
* Canonical tool format:
|
|
8
|
+
* { name: string, description: string, parameters: JSONSchema }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ─── Sanitization ────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const SENSITIVE_KEYS = ['password', 'token', 'apiKey', 'secret', 'credential', 'authorization', 'cookie', 'session'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sanitize data before injecting into prompts.
|
|
17
|
+
* Prevents prompt injection and redacts sensitive fields.
|
|
18
|
+
*
|
|
19
|
+
* @param {*} data - Any value to sanitize
|
|
20
|
+
* @param {number} [maxChars=2000] - Max chars per value
|
|
21
|
+
* @returns {string} Safe string representation
|
|
22
|
+
*/
|
|
23
|
+
export function sanitizeForPrompt(data, maxChars = 2000) {
|
|
24
|
+
if (data === null || data === undefined) return 'null';
|
|
25
|
+
if (typeof data === 'function') return '[Function]';
|
|
26
|
+
if (typeof data === 'symbol') return '[Symbol]';
|
|
27
|
+
|
|
28
|
+
if (typeof data === 'string') {
|
|
29
|
+
const truncated = data.length > maxChars ? data.slice(0, maxChars) + '...[truncated]' : data;
|
|
30
|
+
return `<user_data>${truncated}</user_data>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof data === 'number' || typeof data === 'boolean') {
|
|
34
|
+
return String(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof data === 'object') {
|
|
38
|
+
const redacted = redactSensitive(data);
|
|
39
|
+
const json = JSON.stringify(redacted);
|
|
40
|
+
if (json.length > maxChars) {
|
|
41
|
+
return `<user_data>${json.slice(0, maxChars)}...[truncated]</user_data>`;
|
|
42
|
+
}
|
|
43
|
+
return `<user_data>${json}</user_data>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return String(data).slice(0, maxChars);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Deep-clone an object, replacing sensitive keys with [REDACTED].
|
|
51
|
+
*
|
|
52
|
+
* @param {*} obj
|
|
53
|
+
* @param {number} [depth=0]
|
|
54
|
+
* @returns {*}
|
|
55
|
+
*/
|
|
56
|
+
export function redactSensitive(obj, depth = 0) {
|
|
57
|
+
if (depth > 10) return '[MAX_DEPTH]';
|
|
58
|
+
if (obj === null || obj === undefined) return obj;
|
|
59
|
+
if (typeof obj !== 'object') return obj;
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(obj)) {
|
|
62
|
+
return obj.map(item => redactSensitive(item, depth + 1));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = {};
|
|
66
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
67
|
+
const lowerKey = key.toLowerCase();
|
|
68
|
+
if (SENSITIVE_KEYS.some(sk => lowerKey.includes(sk.toLowerCase()))) {
|
|
69
|
+
result[key] = '[REDACTED]';
|
|
70
|
+
} else {
|
|
71
|
+
result[key] = redactSensitive(value, depth + 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Template Interpolation ──────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Interpolate {{var}} placeholders in a template string.
|
|
81
|
+
* Supports dot-notation: {{data.user.name}}
|
|
82
|
+
*
|
|
83
|
+
* @param {string} template
|
|
84
|
+
* @param {object} vars - Variable map { data: ..., context: ... }
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
export function interpolate(template, vars) {
|
|
88
|
+
return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_match, path) => {
|
|
89
|
+
const value = path.split('.').reduce((obj, key) => obj?.[key], vars);
|
|
90
|
+
if (value === undefined || value === null) return '';
|
|
91
|
+
if (typeof value === 'object') return sanitizeForPrompt(value);
|
|
92
|
+
return String(value);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Tool Schema Builder ─────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build canonical tool definitions from registered actions.
|
|
100
|
+
*
|
|
101
|
+
* @param {Map<string, object>} actions - Map of action name → config
|
|
102
|
+
* @returns {Array<{ name: string, description: string, parameters: object }>}
|
|
103
|
+
*/
|
|
104
|
+
export function buildToolSchemas(actions) {
|
|
105
|
+
const tools = [];
|
|
106
|
+
|
|
107
|
+
for (const [name, config] of actions) {
|
|
108
|
+
tools.push({
|
|
109
|
+
name,
|
|
110
|
+
description: config.description || `Execute action: ${name}`,
|
|
111
|
+
parameters: normalizeParameters(config.parameters),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return tools;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Normalize user-provided parameter definitions into JSON Schema.
|
|
120
|
+
*
|
|
121
|
+
* Accepts two formats:
|
|
122
|
+
* 1. Full JSON Schema: { type: 'object', properties: {...}, required: [...] }
|
|
123
|
+
* 2. Shorthand: { message: { type: 'string', required: true }, count: { type: 'number' } }
|
|
124
|
+
*
|
|
125
|
+
* @param {object} params
|
|
126
|
+
* @returns {object} Valid JSON Schema
|
|
127
|
+
*/
|
|
128
|
+
export function normalizeParameters(params) {
|
|
129
|
+
if (!params || typeof params !== 'object') {
|
|
130
|
+
return { type: 'object', properties: {}, required: [] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Already a JSON Schema
|
|
134
|
+
if (params.type === 'object' && params.properties) {
|
|
135
|
+
return params;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Shorthand format — convert
|
|
139
|
+
const properties = {};
|
|
140
|
+
const required = [];
|
|
141
|
+
|
|
142
|
+
for (const [key, def] of Object.entries(params)) {
|
|
143
|
+
if (typeof def === 'string') {
|
|
144
|
+
// Simplest: { message: 'string' }
|
|
145
|
+
properties[key] = { type: def };
|
|
146
|
+
} else if (typeof def === 'object') {
|
|
147
|
+
const { required: isRequired, ...rest } = def;
|
|
148
|
+
properties[key] = rest.type ? rest : { type: 'string', ...rest };
|
|
149
|
+
if (isRequired) required.push(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { type: 'object', properties, required };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Validate params against a JSON Schema (lightweight, no external deps).
|
|
158
|
+
* Only checks type, required, and enum — not full JSON Schema validation.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} params - Actual params from LLM
|
|
161
|
+
* @param {object} schema - JSON Schema from normalizeParameters()
|
|
162
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
163
|
+
*/
|
|
164
|
+
export function validateParams(params, schema) {
|
|
165
|
+
const errors = [];
|
|
166
|
+
if (!schema || !schema.properties) return { valid: true, errors };
|
|
167
|
+
|
|
168
|
+
// Check required
|
|
169
|
+
for (const key of (schema.required || [])) {
|
|
170
|
+
if (params[key] === undefined || params[key] === null) {
|
|
171
|
+
errors.push(`'${key}' is required`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check types and enums
|
|
176
|
+
for (const [key, def] of Object.entries(schema.properties)) {
|
|
177
|
+
const value = params[key];
|
|
178
|
+
if (value === undefined || value === null) continue;
|
|
179
|
+
|
|
180
|
+
if (def.type && def.type !== 'any') {
|
|
181
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
182
|
+
if (def.type === 'integer') {
|
|
183
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
184
|
+
errors.push(`'${key}' must be integer, got ${actualType}`);
|
|
185
|
+
}
|
|
186
|
+
} else if (def.type !== actualType) {
|
|
187
|
+
errors.push(`'${key}' must be ${def.type}, got ${actualType}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (def.enum && !def.enum.includes(value)) {
|
|
192
|
+
errors.push(`'${key}' must be one of [${def.enum.join(', ')}], got '${value}'`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { valid: errors.length === 0, errors };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Context Budget ──────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Estimate token count from character count.
|
|
203
|
+
* Rough heuristic: 1 token ≈ 4 chars for English, ≈ 2 chars for CJK.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} text
|
|
206
|
+
* @param {number} [charRatio=4]
|
|
207
|
+
* @returns {number}
|
|
208
|
+
*/
|
|
209
|
+
export function estimateTokens(text, charRatio = 4) {
|
|
210
|
+
return Math.ceil(text.length / charRatio);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Truncate text to fit within a token budget.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} text
|
|
217
|
+
* @param {number} maxTokens
|
|
218
|
+
* @param {number} [charRatio=4]
|
|
219
|
+
* @returns {string}
|
|
220
|
+
*/
|
|
221
|
+
export function truncateToTokenBudget(text, maxTokens, charRatio = 4) {
|
|
222
|
+
const maxChars = maxTokens * charRatio;
|
|
223
|
+
if (text.length <= maxChars) return text;
|
|
224
|
+
return text.slice(0, maxChars) + '\n...[truncated to fit token budget]';
|
|
225
|
+
}
|