@theokit/sdk 2.13.0 → 2.14.0
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/CHANGELOG.md +12 -0
- package/dist/a2a/index.cjs +120 -1
- package/dist/a2a/index.cjs.map +1 -1
- package/dist/a2a/index.js +120 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/cron.cjs +110 -1
- package/dist/cron.cjs.map +1 -1
- package/dist/cron.js +110 -1
- package/dist/cron.js.map +1 -1
- package/dist/define-tool.d.ts +8 -0
- package/dist/eval.cjs +113 -4
- package/dist/eval.cjs.map +1 -1
- package/dist/eval.js +113 -4
- package/dist/eval.js.map +1 -1
- package/dist/index.cjs +115 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +115 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/llm/hermes-tool-extract.d.ts +1 -0
- package/dist/sanitize/coerce.d.cts +1 -0
- package/dist/sanitize/coerce.d.ts +1 -0
- package/dist/sanitize/index.cjs +119 -0
- package/dist/sanitize/index.cjs.map +1 -0
- package/dist/sanitize/index.d.cts +9 -0
- package/dist/sanitize/index.d.ts +9 -0
- package/dist/sanitize/index.js +116 -0
- package/dist/sanitize/index.js.map +1 -0
- package/dist/sanitize/sanitize-tool-input.d.cts +11 -0
- package/dist/sanitize/sanitize-tool-input.d.ts +11 -0
- package/dist/sanitize/types.d.cts +39 -0
- package/dist/sanitize/types.d.ts +39 -0
- package/package.json +13 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var module$1 = require('module');
|
|
4
|
+
|
|
5
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
6
|
+
// src/sanitize/coerce.ts
|
|
7
|
+
var cachedJsonrepair;
|
|
8
|
+
function loadJsonrepair() {
|
|
9
|
+
if (cachedJsonrepair === void 0) {
|
|
10
|
+
const req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
11
|
+
cachedJsonrepair = req("jsonrepair").jsonrepair;
|
|
12
|
+
}
|
|
13
|
+
return cachedJsonrepair;
|
|
14
|
+
}
|
|
15
|
+
function isPlainObject(v) {
|
|
16
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
17
|
+
}
|
|
18
|
+
function toFiniteNumber(raw) {
|
|
19
|
+
if (raw === "") return void 0;
|
|
20
|
+
const n = Number(raw);
|
|
21
|
+
return Number.isFinite(n) && String(n) === raw ? n : void 0;
|
|
22
|
+
}
|
|
23
|
+
function tryJson(raw, repair) {
|
|
24
|
+
const t = raw.trimStart();
|
|
25
|
+
if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(repair ? loadJsonrepair()(t) : t);
|
|
28
|
+
} catch {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function heuristicCoerce(raw, repairJson) {
|
|
33
|
+
if (raw === "true") return true;
|
|
34
|
+
if (raw === "false") return false;
|
|
35
|
+
if (raw === "null") return null;
|
|
36
|
+
const n = toFiniteNumber(raw);
|
|
37
|
+
if (n !== void 0) return n;
|
|
38
|
+
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
39
|
+
return json === void 0 ? raw : json;
|
|
40
|
+
}
|
|
41
|
+
function coerceCandidates(raw, repairJson) {
|
|
42
|
+
const out = [];
|
|
43
|
+
if (raw === "true") out.push(true);
|
|
44
|
+
else if (raw === "false") out.push(false);
|
|
45
|
+
else if (raw === "null") out.push(null);
|
|
46
|
+
const n = toFiniteNumber(raw);
|
|
47
|
+
if (n !== void 0) out.push(n);
|
|
48
|
+
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
49
|
+
if (json !== void 0) out.push(json);
|
|
50
|
+
out.push(raw);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function objectShape(schema) {
|
|
54
|
+
const shape = schema?.shape;
|
|
55
|
+
return shape !== null && typeof shape === "object" ? shape : void 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/sanitize/sanitize-tool-input.ts
|
|
59
|
+
function applyTrim(key, value, ctx) {
|
|
60
|
+
const trimmed = value.trim();
|
|
61
|
+
if (trimmed !== value) ctx.notes.push(`trimmed "${key}"`);
|
|
62
|
+
return trimmed;
|
|
63
|
+
}
|
|
64
|
+
function applyCoerce(key, raw, ctx) {
|
|
65
|
+
const field = ctx.shape?.[key];
|
|
66
|
+
let coerced = raw;
|
|
67
|
+
if (field) {
|
|
68
|
+
for (const candidate of coerceCandidates(raw, ctx.repairJson)) {
|
|
69
|
+
if (field.safeParse(candidate).success) {
|
|
70
|
+
coerced = candidate;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
coerced = heuristicCoerce(raw, ctx.repairJson);
|
|
76
|
+
}
|
|
77
|
+
if (coerced !== raw) ctx.notes.push(`coerced "${key}"`);
|
|
78
|
+
return coerced;
|
|
79
|
+
}
|
|
80
|
+
function applyRepair(key, value, ctx) {
|
|
81
|
+
const repaired = tryJson(value, true);
|
|
82
|
+
if (repaired === void 0) return value;
|
|
83
|
+
ctx.notes.push(`repaired json "${key}"`);
|
|
84
|
+
return repaired;
|
|
85
|
+
}
|
|
86
|
+
function sanitizeString(key, value, ctx) {
|
|
87
|
+
let out = ctx.trim ? applyTrim(key, value, ctx) : value;
|
|
88
|
+
if (ctx.coerce && typeof out === "string") out = applyCoerce(key, out, ctx);
|
|
89
|
+
if (ctx.repairJson && !ctx.coerce && typeof out === "string") out = applyRepair(key, out, ctx);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function walk(input, ctx, depth) {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const [key, value] of Object.entries(input)) {
|
|
95
|
+
if (typeof value === "string") out[key] = sanitizeString(key, value, ctx);
|
|
96
|
+
else if (ctx.deep && depth < ctx.maxDepth && isPlainObject(value))
|
|
97
|
+
out[key] = walk(value, ctx, depth + 1);
|
|
98
|
+
else out[key] = value;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function sanitizeToolInput(input, options) {
|
|
103
|
+
if (!isPlainObject(input)) return { value: input, changed: false, notes: [] };
|
|
104
|
+
const ctx = {
|
|
105
|
+
trim: options?.trim ?? true,
|
|
106
|
+
coerce: options?.coerce ?? false,
|
|
107
|
+
repairJson: options?.repairJson ?? false,
|
|
108
|
+
deep: options?.deep ?? false,
|
|
109
|
+
maxDepth: options?.maxDepth ?? 8,
|
|
110
|
+
shape: objectShape(options?.schema),
|
|
111
|
+
notes: []
|
|
112
|
+
};
|
|
113
|
+
const value = walk(input, ctx, 0);
|
|
114
|
+
return { value, changed: ctx.notes.length > 0, notes: ctx.notes };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
exports.sanitizeToolInput = sanitizeToolInput;
|
|
118
|
+
//# sourceMappingURL=index.cjs.map
|
|
119
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sanitize/coerce.ts","../../src/sanitize/sanitize-tool-input.ts"],"names":["createRequire"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createRequire } from \"node:module\";\n\nimport type { ZodType } from \"zod\";\n\n// `jsonrepair` is loaded lazily — consumers who never set `repairJson: true` never pay its\n// module-parse cost. Unlike `zod` (an optional peer dep), `jsonrepair` is a direct dependency and\n// is always installed; the lazy load is a startup-cost optimization, not an optional-install story.\n// Cached after the first repair.\nlet cachedJsonrepair: ((text: string) => string) | undefined;\nfunction loadJsonrepair(): (text: string) => string {\n if (cachedJsonrepair === undefined) {\n const req = createRequire(import.meta.url);\n cachedJsonrepair = (req(\"jsonrepair\") as { jsonrepair: (t: string) => string }).jsonrepair;\n }\n return cachedJsonrepair;\n}\n\n/** @internal */\nexport function isPlainObject(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === \"object\" && !Array.isArray(v);\n}\n\n/** Number coercion with a round-trip + finite guard: rejects big-ints, leading-zeros, `NaN`,\n * `Infinity`, and non-canonical forms (`\"1e3\"`), preventing silent ID/precision corruption (EC-2). */\nfunction toFiniteNumber(raw: string): number | undefined {\n if (raw === \"\") return undefined;\n const n = Number(raw);\n return Number.isFinite(n) && String(n) === raw ? n : undefined;\n}\n\n/** Parse a value ONLY when it looks like a JSON object/array (EC-3 guard; leading whitespace\n * ignored so `trim:false` inputs are still gated correctly). `repair` routes it through\n * `jsonrepair` first. Returns `undefined` when not JSON-looking or on parse failure.\n * @internal */\nexport function tryJson(raw: string, repair: boolean): unknown {\n const t = raw.trimStart();\n if (!(t.startsWith(\"{\") || t.startsWith(\"[\"))) return undefined;\n try {\n return JSON.parse(repair ? loadJsonrepair()(t) : t);\n } catch {\n return undefined;\n }\n}\n\n/** Heuristic scalar/JSON coercion (agentfw `coerceParameter` shape) used when there is no\n * per-field schema. Returns the coerced value, or the raw string when nothing applies.\n * @internal */\nexport function heuristicCoerce(raw: string, repairJson: boolean): unknown {\n if (raw === \"true\") return true;\n if (raw === \"false\") return false;\n if (raw === \"null\") return null;\n const n = toFiniteNumber(raw);\n if (n !== undefined) return n;\n const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : undefined);\n return json === undefined ? raw : json;\n}\n\n/** Candidates a raw string could coerce to, most-specific first. Used for schema-aware coercion:\n * the field schema selects the first candidate it accepts (so a string field keeps `\"5\"`).\n * @internal */\nexport function coerceCandidates(raw: string, repairJson: boolean): unknown[] {\n const out: unknown[] = [];\n if (raw === \"true\") out.push(true);\n else if (raw === \"false\") out.push(false);\n else if (raw === \"null\") out.push(null);\n const n = toFiniteNumber(raw);\n if (n !== undefined) out.push(n);\n const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : undefined);\n if (json !== undefined) out.push(json);\n out.push(raw); // raw string is always the last-resort candidate\n return out;\n}\n\n/** True for a Zod schema that exposes a per-key `.shape` (a `z.object`, incl. Zod-v4\n * `.refine()`/`.default()` which keep `.shape`); non-object schemas (union/record) return\n * `undefined` so callers fall back to heuristic coercion (EC-4).\n * @internal */\nexport function objectShape(schema: ZodType | undefined): Record<string, ZodType> | undefined {\n const shape = (schema as unknown as { shape?: unknown } | undefined)?.shape;\n return shape !== null && typeof shape === \"object\"\n ? (shape as Record<string, ZodType>)\n : undefined;\n}\n","import type { ZodType } from \"zod\";\n\nimport {\n coerceCandidates,\n heuristicCoerce,\n isPlainObject,\n objectShape,\n tryJson,\n} from \"./coerce.js\";\nimport type { SanitizeOptions, SanitizeResult } from \"./types.js\";\n\ninterface Ctx {\n trim: boolean;\n coerce: boolean;\n repairJson: boolean;\n deep: boolean;\n maxDepth: number;\n shape: Record<string, ZodType> | undefined;\n notes: string[];\n}\n\n/** Trim rung — returns the trimmed string, noting the change. */\nfunction applyTrim(key: string, value: string, ctx: Ctx): string {\n const trimmed = value.trim();\n if (trimmed !== value) ctx.notes.push(`trimmed \"${key}\"`);\n return trimmed;\n}\n\n/** Coerce rung — schema-aware when a field schema exists (pick the first accepted candidate),\n * else heuristic. Returns the coerced value (or the raw string), noting the change. */\nfunction applyCoerce(key: string, raw: string, ctx: Ctx): unknown {\n const field = ctx.shape?.[key];\n let coerced: unknown = raw;\n if (field) {\n for (const candidate of coerceCandidates(raw, ctx.repairJson)) {\n if (field.safeParse(candidate).success) {\n coerced = candidate;\n break;\n }\n }\n } else {\n coerced = heuristicCoerce(raw, ctx.repairJson);\n }\n if (coerced !== raw) ctx.notes.push(`coerced \"${key}\"`);\n return coerced;\n}\n\n/** Repair rung — repair-then-parse a JSON-looking string, noting the change. */\nfunction applyRepair(key: string, value: string, ctx: Ctx): unknown {\n const repaired = tryJson(value, true);\n if (repaired === undefined) return value;\n ctx.notes.push(`repaired json \"${key}\"`);\n return repaired;\n}\n\n/** Sanitize one string value through the enabled rungs (trim → coerce → repair). */\nfunction sanitizeString(key: string, value: string, ctx: Ctx): unknown {\n let out: unknown = ctx.trim ? applyTrim(key, value, ctx) : value;\n if (ctx.coerce && typeof out === \"string\") out = applyCoerce(key, out, ctx);\n // Standalone repair runs ONLY when coerce is off. When coerce is on it already embedded the\n // repair candidate (via coerceCandidates / heuristicCoerce), and a schema-confirmed raw string\n // (e.g. a JSON string a `z.string()` field accepted) must not be clobbered back into an object.\n if (ctx.repairJson && !ctx.coerce && typeof out === \"string\") out = applyRepair(key, out, ctx);\n return out;\n}\n\nfunction walk(input: Record<string, unknown>, ctx: Ctx, depth: number): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(input)) {\n if (typeof value === \"string\") out[key] = sanitizeString(key, value, ctx);\n else if (ctx.deep && depth < ctx.maxDepth && isPlainObject(value))\n out[key] = walk(value, ctx, depth + 1);\n else out[key] = value;\n }\n return out;\n}\n\n/**\n * Sanitize the raw arguments a model emitted for a tool call — trim (default), optionally coerce\n * string values toward their expected type, optionally repair malformed JSON. Pure, synchronous,\n * and TOTAL: it never throws (non-object input is returned unchanged) and never changes a value's\n * meaning — only its hygiene/representation. Reused internally by the leaked-dialect recovery so\n * the public primitive and the internal path never diverge.\n *\n * @public\n */\nexport function sanitizeToolInput(\n input: Record<string, unknown>,\n options?: SanitizeOptions,\n): SanitizeResult {\n // EC-1 — total contract: a non-object (null / array / primitive) is returned as-is, never thrown on.\n if (!isPlainObject(input)) return { value: input, changed: false, notes: [] };\n const ctx: Ctx = {\n trim: options?.trim ?? true,\n coerce: options?.coerce ?? false,\n repairJson: options?.repairJson ?? false,\n deep: options?.deep ?? false,\n maxDepth: options?.maxDepth ?? 8,\n shape: objectShape(options?.schema),\n notes: [],\n };\n const value = walk(input, ctx, 0);\n return { value, changed: ctx.notes.length > 0, notes: ctx.notes };\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@theokit/sdk/sanitize` — a professional, isolated tool-input sanitization primitive that
|
|
3
|
+
* custom tools consume to clean model-emitted arguments (trim / coerce / json-repair). Pure,
|
|
4
|
+
* synchronous, dependency-light. See `sanitizeToolInput`.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
export { sanitizeToolInput } from "./sanitize-tool-input.js";
|
|
9
|
+
export type { SanitizeOptions, SanitizeResult } from "./types.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@theokit/sdk/sanitize` — a professional, isolated tool-input sanitization primitive that
|
|
3
|
+
* custom tools consume to clean model-emitted arguments (trim / coerce / json-repair). Pure,
|
|
4
|
+
* synchronous, dependency-light. See `sanitizeToolInput`.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
export { sanitizeToolInput } from "./sanitize-tool-input.js";
|
|
9
|
+
export type { SanitizeOptions, SanitizeResult } from "./types.js";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
|
|
3
|
+
// src/sanitize/coerce.ts
|
|
4
|
+
var cachedJsonrepair;
|
|
5
|
+
function loadJsonrepair() {
|
|
6
|
+
if (cachedJsonrepair === void 0) {
|
|
7
|
+
const req = createRequire(import.meta.url);
|
|
8
|
+
cachedJsonrepair = req("jsonrepair").jsonrepair;
|
|
9
|
+
}
|
|
10
|
+
return cachedJsonrepair;
|
|
11
|
+
}
|
|
12
|
+
function isPlainObject(v) {
|
|
13
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
14
|
+
}
|
|
15
|
+
function toFiniteNumber(raw) {
|
|
16
|
+
if (raw === "") return void 0;
|
|
17
|
+
const n = Number(raw);
|
|
18
|
+
return Number.isFinite(n) && String(n) === raw ? n : void 0;
|
|
19
|
+
}
|
|
20
|
+
function tryJson(raw, repair) {
|
|
21
|
+
const t = raw.trimStart();
|
|
22
|
+
if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(repair ? loadJsonrepair()(t) : t);
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function heuristicCoerce(raw, repairJson) {
|
|
30
|
+
if (raw === "true") return true;
|
|
31
|
+
if (raw === "false") return false;
|
|
32
|
+
if (raw === "null") return null;
|
|
33
|
+
const n = toFiniteNumber(raw);
|
|
34
|
+
if (n !== void 0) return n;
|
|
35
|
+
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
36
|
+
return json === void 0 ? raw : json;
|
|
37
|
+
}
|
|
38
|
+
function coerceCandidates(raw, repairJson) {
|
|
39
|
+
const out = [];
|
|
40
|
+
if (raw === "true") out.push(true);
|
|
41
|
+
else if (raw === "false") out.push(false);
|
|
42
|
+
else if (raw === "null") out.push(null);
|
|
43
|
+
const n = toFiniteNumber(raw);
|
|
44
|
+
if (n !== void 0) out.push(n);
|
|
45
|
+
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
46
|
+
if (json !== void 0) out.push(json);
|
|
47
|
+
out.push(raw);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function objectShape(schema) {
|
|
51
|
+
const shape = schema?.shape;
|
|
52
|
+
return shape !== null && typeof shape === "object" ? shape : void 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/sanitize/sanitize-tool-input.ts
|
|
56
|
+
function applyTrim(key, value, ctx) {
|
|
57
|
+
const trimmed = value.trim();
|
|
58
|
+
if (trimmed !== value) ctx.notes.push(`trimmed "${key}"`);
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
function applyCoerce(key, raw, ctx) {
|
|
62
|
+
const field = ctx.shape?.[key];
|
|
63
|
+
let coerced = raw;
|
|
64
|
+
if (field) {
|
|
65
|
+
for (const candidate of coerceCandidates(raw, ctx.repairJson)) {
|
|
66
|
+
if (field.safeParse(candidate).success) {
|
|
67
|
+
coerced = candidate;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
coerced = heuristicCoerce(raw, ctx.repairJson);
|
|
73
|
+
}
|
|
74
|
+
if (coerced !== raw) ctx.notes.push(`coerced "${key}"`);
|
|
75
|
+
return coerced;
|
|
76
|
+
}
|
|
77
|
+
function applyRepair(key, value, ctx) {
|
|
78
|
+
const repaired = tryJson(value, true);
|
|
79
|
+
if (repaired === void 0) return value;
|
|
80
|
+
ctx.notes.push(`repaired json "${key}"`);
|
|
81
|
+
return repaired;
|
|
82
|
+
}
|
|
83
|
+
function sanitizeString(key, value, ctx) {
|
|
84
|
+
let out = ctx.trim ? applyTrim(key, value, ctx) : value;
|
|
85
|
+
if (ctx.coerce && typeof out === "string") out = applyCoerce(key, out, ctx);
|
|
86
|
+
if (ctx.repairJson && !ctx.coerce && typeof out === "string") out = applyRepair(key, out, ctx);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function walk(input, ctx, depth) {
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [key, value] of Object.entries(input)) {
|
|
92
|
+
if (typeof value === "string") out[key] = sanitizeString(key, value, ctx);
|
|
93
|
+
else if (ctx.deep && depth < ctx.maxDepth && isPlainObject(value))
|
|
94
|
+
out[key] = walk(value, ctx, depth + 1);
|
|
95
|
+
else out[key] = value;
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function sanitizeToolInput(input, options) {
|
|
100
|
+
if (!isPlainObject(input)) return { value: input, changed: false, notes: [] };
|
|
101
|
+
const ctx = {
|
|
102
|
+
trim: options?.trim ?? true,
|
|
103
|
+
coerce: options?.coerce ?? false,
|
|
104
|
+
repairJson: options?.repairJson ?? false,
|
|
105
|
+
deep: options?.deep ?? false,
|
|
106
|
+
maxDepth: options?.maxDepth ?? 8,
|
|
107
|
+
shape: objectShape(options?.schema),
|
|
108
|
+
notes: []
|
|
109
|
+
};
|
|
110
|
+
const value = walk(input, ctx, 0);
|
|
111
|
+
return { value, changed: ctx.notes.length > 0, notes: ctx.notes };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { sanitizeToolInput };
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sanitize/coerce.ts","../../src/sanitize/sanitize-tool-input.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { createRequire } from \"node:module\";\n\nimport type { ZodType } from \"zod\";\n\n// `jsonrepair` is loaded lazily — consumers who never set `repairJson: true` never pay its\n// module-parse cost. Unlike `zod` (an optional peer dep), `jsonrepair` is a direct dependency and\n// is always installed; the lazy load is a startup-cost optimization, not an optional-install story.\n// Cached after the first repair.\nlet cachedJsonrepair: ((text: string) => string) | undefined;\nfunction loadJsonrepair(): (text: string) => string {\n if (cachedJsonrepair === undefined) {\n const req = createRequire(import.meta.url);\n cachedJsonrepair = (req(\"jsonrepair\") as { jsonrepair: (t: string) => string }).jsonrepair;\n }\n return cachedJsonrepair;\n}\n\n/** @internal */\nexport function isPlainObject(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === \"object\" && !Array.isArray(v);\n}\n\n/** Number coercion with a round-trip + finite guard: rejects big-ints, leading-zeros, `NaN`,\n * `Infinity`, and non-canonical forms (`\"1e3\"`), preventing silent ID/precision corruption (EC-2). */\nfunction toFiniteNumber(raw: string): number | undefined {\n if (raw === \"\") return undefined;\n const n = Number(raw);\n return Number.isFinite(n) && String(n) === raw ? n : undefined;\n}\n\n/** Parse a value ONLY when it looks like a JSON object/array (EC-3 guard; leading whitespace\n * ignored so `trim:false` inputs are still gated correctly). `repair` routes it through\n * `jsonrepair` first. Returns `undefined` when not JSON-looking or on parse failure.\n * @internal */\nexport function tryJson(raw: string, repair: boolean): unknown {\n const t = raw.trimStart();\n if (!(t.startsWith(\"{\") || t.startsWith(\"[\"))) return undefined;\n try {\n return JSON.parse(repair ? loadJsonrepair()(t) : t);\n } catch {\n return undefined;\n }\n}\n\n/** Heuristic scalar/JSON coercion (agentfw `coerceParameter` shape) used when there is no\n * per-field schema. Returns the coerced value, or the raw string when nothing applies.\n * @internal */\nexport function heuristicCoerce(raw: string, repairJson: boolean): unknown {\n if (raw === \"true\") return true;\n if (raw === \"false\") return false;\n if (raw === \"null\") return null;\n const n = toFiniteNumber(raw);\n if (n !== undefined) return n;\n const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : undefined);\n return json === undefined ? raw : json;\n}\n\n/** Candidates a raw string could coerce to, most-specific first. Used for schema-aware coercion:\n * the field schema selects the first candidate it accepts (so a string field keeps `\"5\"`).\n * @internal */\nexport function coerceCandidates(raw: string, repairJson: boolean): unknown[] {\n const out: unknown[] = [];\n if (raw === \"true\") out.push(true);\n else if (raw === \"false\") out.push(false);\n else if (raw === \"null\") out.push(null);\n const n = toFiniteNumber(raw);\n if (n !== undefined) out.push(n);\n const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : undefined);\n if (json !== undefined) out.push(json);\n out.push(raw); // raw string is always the last-resort candidate\n return out;\n}\n\n/** True for a Zod schema that exposes a per-key `.shape` (a `z.object`, incl. Zod-v4\n * `.refine()`/`.default()` which keep `.shape`); non-object schemas (union/record) return\n * `undefined` so callers fall back to heuristic coercion (EC-4).\n * @internal */\nexport function objectShape(schema: ZodType | undefined): Record<string, ZodType> | undefined {\n const shape = (schema as unknown as { shape?: unknown } | undefined)?.shape;\n return shape !== null && typeof shape === \"object\"\n ? (shape as Record<string, ZodType>)\n : undefined;\n}\n","import type { ZodType } from \"zod\";\n\nimport {\n coerceCandidates,\n heuristicCoerce,\n isPlainObject,\n objectShape,\n tryJson,\n} from \"./coerce.js\";\nimport type { SanitizeOptions, SanitizeResult } from \"./types.js\";\n\ninterface Ctx {\n trim: boolean;\n coerce: boolean;\n repairJson: boolean;\n deep: boolean;\n maxDepth: number;\n shape: Record<string, ZodType> | undefined;\n notes: string[];\n}\n\n/** Trim rung — returns the trimmed string, noting the change. */\nfunction applyTrim(key: string, value: string, ctx: Ctx): string {\n const trimmed = value.trim();\n if (trimmed !== value) ctx.notes.push(`trimmed \"${key}\"`);\n return trimmed;\n}\n\n/** Coerce rung — schema-aware when a field schema exists (pick the first accepted candidate),\n * else heuristic. Returns the coerced value (or the raw string), noting the change. */\nfunction applyCoerce(key: string, raw: string, ctx: Ctx): unknown {\n const field = ctx.shape?.[key];\n let coerced: unknown = raw;\n if (field) {\n for (const candidate of coerceCandidates(raw, ctx.repairJson)) {\n if (field.safeParse(candidate).success) {\n coerced = candidate;\n break;\n }\n }\n } else {\n coerced = heuristicCoerce(raw, ctx.repairJson);\n }\n if (coerced !== raw) ctx.notes.push(`coerced \"${key}\"`);\n return coerced;\n}\n\n/** Repair rung — repair-then-parse a JSON-looking string, noting the change. */\nfunction applyRepair(key: string, value: string, ctx: Ctx): unknown {\n const repaired = tryJson(value, true);\n if (repaired === undefined) return value;\n ctx.notes.push(`repaired json \"${key}\"`);\n return repaired;\n}\n\n/** Sanitize one string value through the enabled rungs (trim → coerce → repair). */\nfunction sanitizeString(key: string, value: string, ctx: Ctx): unknown {\n let out: unknown = ctx.trim ? applyTrim(key, value, ctx) : value;\n if (ctx.coerce && typeof out === \"string\") out = applyCoerce(key, out, ctx);\n // Standalone repair runs ONLY when coerce is off. When coerce is on it already embedded the\n // repair candidate (via coerceCandidates / heuristicCoerce), and a schema-confirmed raw string\n // (e.g. a JSON string a `z.string()` field accepted) must not be clobbered back into an object.\n if (ctx.repairJson && !ctx.coerce && typeof out === \"string\") out = applyRepair(key, out, ctx);\n return out;\n}\n\nfunction walk(input: Record<string, unknown>, ctx: Ctx, depth: number): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(input)) {\n if (typeof value === \"string\") out[key] = sanitizeString(key, value, ctx);\n else if (ctx.deep && depth < ctx.maxDepth && isPlainObject(value))\n out[key] = walk(value, ctx, depth + 1);\n else out[key] = value;\n }\n return out;\n}\n\n/**\n * Sanitize the raw arguments a model emitted for a tool call — trim (default), optionally coerce\n * string values toward their expected type, optionally repair malformed JSON. Pure, synchronous,\n * and TOTAL: it never throws (non-object input is returned unchanged) and never changes a value's\n * meaning — only its hygiene/representation. Reused internally by the leaked-dialect recovery so\n * the public primitive and the internal path never diverge.\n *\n * @public\n */\nexport function sanitizeToolInput(\n input: Record<string, unknown>,\n options?: SanitizeOptions,\n): SanitizeResult {\n // EC-1 — total contract: a non-object (null / array / primitive) is returned as-is, never thrown on.\n if (!isPlainObject(input)) return { value: input, changed: false, notes: [] };\n const ctx: Ctx = {\n trim: options?.trim ?? true,\n coerce: options?.coerce ?? false,\n repairJson: options?.repairJson ?? false,\n deep: options?.deep ?? false,\n maxDepth: options?.maxDepth ?? 8,\n shape: objectShape(options?.schema),\n notes: [],\n };\n const value = walk(input, ctx, 0);\n return { value, changed: ctx.notes.length > 0, notes: ctx.notes };\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SanitizeOptions, SanitizeResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sanitize the raw arguments a model emitted for a tool call — trim (default), optionally coerce
|
|
4
|
+
* string values toward their expected type, optionally repair malformed JSON. Pure, synchronous,
|
|
5
|
+
* and TOTAL: it never throws (non-object input is returned unchanged) and never changes a value's
|
|
6
|
+
* meaning — only its hygiene/representation. Reused internally by the leaked-dialect recovery so
|
|
7
|
+
* the public primitive and the internal path never diverge.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export declare function sanitizeToolInput(input: Record<string, unknown>, options?: SanitizeOptions): SanitizeResult;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SanitizeOptions, SanitizeResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sanitize the raw arguments a model emitted for a tool call — trim (default), optionally coerce
|
|
4
|
+
* string values toward their expected type, optionally repair malformed JSON. Pure, synchronous,
|
|
5
|
+
* and TOTAL: it never throws (non-object input is returned unchanged) and never changes a value's
|
|
6
|
+
* meaning — only its hygiene/representation. Reused internally by the leaked-dialect recovery so
|
|
7
|
+
* the public primitive and the internal path never diverge.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export declare function sanitizeToolInput(input: Record<string, unknown>, options?: SanitizeOptions): SanitizeResult;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ZodType } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Options for {@link sanitizeToolInput}. Trim is the only default-on rung — coercion and JSON
|
|
4
|
+
* repair change a value's representation, so they are opt-in (the SDK's "values are strings; Zod
|
|
5
|
+
* coerces" boundary; see `define-tool.ts` doc-comment).
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export interface SanitizeOptions {
|
|
10
|
+
/** Trim leading/trailing whitespace from string values. Default `true`. */
|
|
11
|
+
trim?: boolean;
|
|
12
|
+
/** Coerce string values to typed values (`"5"`→`5`, `"true"`→`true`, `"null"`→`null`, JSON). Default `false`. */
|
|
13
|
+
coerce?: boolean;
|
|
14
|
+
/** Repair-then-parse malformed JSON-looking string values (via `jsonrepair`). Default `false`. */
|
|
15
|
+
repairJson?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Optional Zod schema. When it is a `z.object(...)`, coercion is schema-aware: each TOP-LEVEL
|
|
18
|
+
* field is coerced only toward a candidate its field-schema accepts (so a `z.string()` field
|
|
19
|
+
* keeps `"5"` as a string). Non-object schemas (union/record) fall back to heuristic coercion.
|
|
20
|
+
* Note: with `deep: true`, nested fields always use heuristic coercion (the schema is not
|
|
21
|
+
* descended into).
|
|
22
|
+
*/
|
|
23
|
+
schema?: ZodType;
|
|
24
|
+
/** Recurse into nested objects/arrays. Default `false` (shallow). */
|
|
25
|
+
deep?: boolean;
|
|
26
|
+
/** Max recursion depth when `deep` is set. Default `8`. */
|
|
27
|
+
maxDepth?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Result of {@link sanitizeToolInput}. `value` is the sanitized copy; `changed` is true when any
|
|
31
|
+
* value was altered; `notes` records a human-readable line per change (for logging/debugging).
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export interface SanitizeResult<T = Record<string, unknown>> {
|
|
36
|
+
value: T;
|
|
37
|
+
changed: boolean;
|
|
38
|
+
notes: string[];
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ZodType } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Options for {@link sanitizeToolInput}. Trim is the only default-on rung — coercion and JSON
|
|
4
|
+
* repair change a value's representation, so they are opt-in (the SDK's "values are strings; Zod
|
|
5
|
+
* coerces" boundary; see `define-tool.ts` doc-comment).
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export interface SanitizeOptions {
|
|
10
|
+
/** Trim leading/trailing whitespace from string values. Default `true`. */
|
|
11
|
+
trim?: boolean;
|
|
12
|
+
/** Coerce string values to typed values (`"5"`→`5`, `"true"`→`true`, `"null"`→`null`, JSON). Default `false`. */
|
|
13
|
+
coerce?: boolean;
|
|
14
|
+
/** Repair-then-parse malformed JSON-looking string values (via `jsonrepair`). Default `false`. */
|
|
15
|
+
repairJson?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Optional Zod schema. When it is a `z.object(...)`, coercion is schema-aware: each TOP-LEVEL
|
|
18
|
+
* field is coerced only toward a candidate its field-schema accepts (so a `z.string()` field
|
|
19
|
+
* keeps `"5"` as a string). Non-object schemas (union/record) fall back to heuristic coercion.
|
|
20
|
+
* Note: with `deep: true`, nested fields always use heuristic coercion (the schema is not
|
|
21
|
+
* descended into).
|
|
22
|
+
*/
|
|
23
|
+
schema?: ZodType;
|
|
24
|
+
/** Recurse into nested objects/arrays. Default `false` (shallow). */
|
|
25
|
+
deep?: boolean;
|
|
26
|
+
/** Max recursion depth when `deep` is set. Default `8`. */
|
|
27
|
+
maxDepth?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Result of {@link sanitizeToolInput}. `value` is the sanitized copy; `changed` is true when any
|
|
31
|
+
* value was altered; `notes` records a human-readable line per change (for logging/debugging).
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export interface SanitizeResult<T = Record<string, unknown>> {
|
|
36
|
+
value: T;
|
|
37
|
+
changed: boolean;
|
|
38
|
+
notes: string[];
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theokit/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"description": "TypeScript SDK for the Theo agent harness — same surface, local or cloud.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/usetheo/theokit-sdk#readme",
|
|
@@ -208,6 +208,16 @@
|
|
|
208
208
|
"default": "./dist/subscription/index.cjs"
|
|
209
209
|
}
|
|
210
210
|
},
|
|
211
|
+
"./sanitize": {
|
|
212
|
+
"import": {
|
|
213
|
+
"types": "./dist/sanitize/index.d.ts",
|
|
214
|
+
"default": "./dist/sanitize/index.js"
|
|
215
|
+
},
|
|
216
|
+
"require": {
|
|
217
|
+
"types": "./dist/sanitize/index.d.cts",
|
|
218
|
+
"default": "./dist/sanitize/index.cjs"
|
|
219
|
+
}
|
|
220
|
+
},
|
|
211
221
|
"./internal/persistence": {
|
|
212
222
|
"import": {
|
|
213
223
|
"types": "./dist/internal/persistence/index.d.ts",
|
|
@@ -358,7 +368,8 @@
|
|
|
358
368
|
"Theo"
|
|
359
369
|
],
|
|
360
370
|
"dependencies": {
|
|
361
|
-
"croner": "^9.0.0"
|
|
371
|
+
"croner": "^9.0.0",
|
|
372
|
+
"jsonrepair": "^3.13.2"
|
|
362
373
|
},
|
|
363
374
|
"devDependencies": {
|
|
364
375
|
"@opentelemetry/api": "^1.9.1",
|