@theokit/sdk 2.13.1 → 2.15.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/a2a/index.cjs +242 -3
  3. package/dist/a2a/index.cjs.map +1 -1
  4. package/dist/a2a/index.js +242 -3
  5. package/dist/a2a/index.js.map +1 -1
  6. package/dist/{cron-CpxLdAXc.d.cts → cron-BxLSz1UH.d.cts} +1 -1
  7. package/dist/{cron-CL_9nfhQ.d.ts → cron-DcaoP7aW.d.ts} +1 -1
  8. package/dist/cron.cjs +225 -3
  9. package/dist/cron.cjs.map +1 -1
  10. package/dist/cron.d.cts +2 -2
  11. package/dist/cron.d.ts +2 -2
  12. package/dist/cron.js +225 -3
  13. package/dist/cron.js.map +1 -1
  14. package/dist/define-tool.d.ts +8 -0
  15. package/dist/{errors-9yw4UQwX.d.cts → errors-Bart0ptP.d.cts} +1 -1
  16. package/dist/{errors-DFiY-NHK.d.ts → errors-DJuuubJK.d.ts} +1 -1
  17. package/dist/errors.d.cts +2 -2
  18. package/dist/eval.cjs +228 -6
  19. package/dist/eval.cjs.map +1 -1
  20. package/dist/eval.js +228 -6
  21. package/dist/eval.js.map +1 -1
  22. package/dist/index.cjs +230 -4
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +41 -6
  25. package/dist/index.d.ts +41 -6
  26. package/dist/index.js +230 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/internal/agent-loop/doom-loop-tracker.d.ts +22 -0
  29. package/dist/internal/agent-loop/loop-types.d.ts +6 -0
  30. package/dist/internal/llm/hermes-tool-extract.d.ts +1 -0
  31. package/dist/{run-TMdc7gmo.d.cts → run-DXy_MVwz.d.cts} +29 -1
  32. package/dist/{run-TMdc7gmo.d.ts → run-DXy_MVwz.d.ts} +29 -1
  33. package/dist/sanitize/coerce.d.cts +1 -0
  34. package/dist/sanitize/coerce.d.ts +1 -0
  35. package/dist/sanitize/index.cjs +119 -0
  36. package/dist/sanitize/index.cjs.map +1 -0
  37. package/dist/sanitize/index.d.cts +9 -0
  38. package/dist/sanitize/index.d.ts +9 -0
  39. package/dist/sanitize/index.js +116 -0
  40. package/dist/sanitize/index.js.map +1 -0
  41. package/dist/sanitize/sanitize-tool-input.d.cts +11 -0
  42. package/dist/sanitize/sanitize-tool-input.d.ts +11 -0
  43. package/dist/sanitize/types.d.cts +39 -0
  44. package/dist/sanitize/types.d.ts +39 -0
  45. package/dist/types/run.d.ts +28 -0
  46. package/package.json +13 -2
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Doom-loop guard — detects consecutive IDENTICAL tool calls (same name + same canonical input)
3
+ * and escalates `ok` → `soft` (one-time nudge) → `hard` (stop). Inspection is TOTAL — `signatureOf`
4
+ * and `inspect` never throw on any tool-call input. The constructor validates thresholds and throws
5
+ * a typed `ConfigurationError` on invalid config (fail-fast at the boundary — a 0/negative/non-integer
6
+ * threshold is a caller bug, not a loop input). Ported in shape from cline `LoopDetectionTracker`.
7
+ * Complements the empty-round `no_progress` terminal (a DIFFERENT failure mode: model stuck repeating
8
+ * vs model gone silent).
9
+ * @internal
10
+ */
11
+ /** Verdict from {@link DoomLoopTracker.inspect}. `soft` carries a guidance message, `hard` a stop message. */
12
+ export interface DoomLoopVerdict {
13
+ kind: "ok" | "soft" | "hard";
14
+ message?: string;
15
+ }
16
+ /** Consecutive-identical thresholds. `soft` fires once (at exactly the count); `hard` stops. */
17
+ export interface DoomLoopConfig {
18
+ softThreshold: number;
19
+ hardThreshold: number;
20
+ }
21
+ /** Public config on a send: `false` disables the guard; an object tunes the thresholds; absent = on. */
22
+ export type DoomLoopOption = false | Partial<DoomLoopConfig>;
@@ -32,4 +32,10 @@ export interface AgentLoopOutput {
32
32
  * `RunResult.stoppedAtIterationLimit`.
33
33
  */
34
34
  stoppedAtIterationLimit?: boolean;
35
+ /**
36
+ * Doom-loop guard: true when the loop stopped because the model repeated IDENTICAL tool calls to
37
+ * the hard threshold. Copied verbatim onto `RunResult.stoppedByDoomLoop`; the continuation driver
38
+ * classifies it as a `no_progress` terminal (a controlled stop, not a truncation to re-send).
39
+ */
40
+ stoppedByDoomLoop?: boolean;
35
41
  }
@@ -1,3 +1,4 @@
1
+ import type { LlmToolCallPart } from "./types.js";
1
2
  export interface HermesExtractResult {
2
3
  /** Recovered tool calls (empty when none matched). */
3
4
  toolCalls: LlmToolCallPart[];
@@ -687,6 +687,28 @@ interface RunResult {
687
687
  * @public
688
688
  */
689
689
  stoppedAtIterationLimit?: boolean;
690
+ /**
691
+ * `true` when the run stopped because the **doom-loop guard** detected the model repeating
692
+ * IDENTICAL tool calls (same name + same input) to the hard threshold — making no progress (e.g.
693
+ * a tool that keeps failing and is retried unchanged). `undefined`/absent otherwise. Through the
694
+ * continuation driver this surfaces as `terminal: "no_progress"` (a controlled stop, NOT a
695
+ * truncation to re-send). Tune or disable via {@link SendOptions.doomLoop}.
696
+ *
697
+ * @public
698
+ */
699
+ stoppedByDoomLoop?: boolean;
700
+ }
701
+ /**
702
+ * Doom-loop guard thresholds (see {@link SendOptions.doomLoop}). Both are counts of CONSECUTIVE
703
+ * identical tool calls: `softThreshold` injects a one-time guidance nudge; `hardThreshold` stops.
704
+ *
705
+ * @public
706
+ */
707
+ interface DoomLoopThresholds {
708
+ /** Consecutive-identical count at which a one-time guidance nudge is injected. Default 3. */
709
+ softThreshold?: number;
710
+ /** Consecutive-identical count at which the run stops (`no_progress`). Default 5. */
711
+ hardThreshold?: number;
690
712
  }
691
713
  /**
692
714
  * Options for {@link SDKAgent.runToCompletion} (M1 Phase 3 — continuation driver).
@@ -803,6 +825,12 @@ interface SDKUserMessage {
803
825
  */
804
826
  interface SendOptions {
805
827
  model?: ModelSelection;
828
+ /**
829
+ * Doom-loop guard config. The loop stops (with `terminal: "no_progress"`, `RunResult.stoppedByDoomLoop`)
830
+ * when the model repeats IDENTICAL tool calls to the hard threshold. On by default with generous
831
+ * thresholds (soft 3 / hard 5). Set `false` to disable, or an object to tune the thresholds.
832
+ */
833
+ doomLoop?: false | DoomLoopThresholds;
806
834
  /**
807
835
  * Per-call system prompt override. Wins over `AgentOptions.systemPrompt`.
808
836
  * String only — for dynamic resolvers, configure on `AgentOptions`. An
@@ -909,4 +937,4 @@ interface Run {
909
937
  onDidChangeStatus(listener: (status: RunStatus) => void): () => void;
910
938
  }
911
939
 
912
- export type { ThinkingMessage as $, AgentConversationTurn as A, SDKTaskMessage as B, CustomTool as C, SDKThinkingMessage as D, SDKToolUseMessage as E, SDKUserMessage as F, SDKUserMessageEvent as G, SendOptions as H, InteractionUpdate as I, ShellCommand as J, ShellConversationTurn as K, ShellOutput as L, ModelSelection as M, ShellOutputDeltaUpdate as N, StepCompletedUpdate as O, PartialToolCallUpdate as P, StepStartedUpdate as Q, RunResult as R, SDKMessage as S, StreamToCompletionResult as T, SummaryCompletedUpdate as U, SummaryStartedUpdate as V, SummaryUpdate as W, TextBlock as X, TextDeltaUpdate as Y, ThinkingCompletedUpdate as Z, ThinkingDeltaUpdate as _, McpServerConfig as a, TokenDeltaUpdate as a0, TokenUsage as a1, ToolCall as a2, ToolCallCompletedUpdate as a3, ToolCallStartedUpdate as a4, ToolResult as a5, ToolUseBlock as a6, TurnEndedUpdate as a7, UserMessage as a8, UserMessageAppendedUpdate as a9, Run as b, AssistantMessage as c, ConversationStep as d, ConversationTurn as e, CostBreakdown as f, CostSource as g, CostStatus as h, McpAuthConfig as i, McpHttpServerConfig as j, McpOAuthConfig as k, McpStdioServerConfig as l, ModelParameterValue as m, RunErrorDetail as n, RunGitInfo as o, RunOperation as p, RunStatus as q, RunToCompletionOptions as r, RunToCompletionResult as s, SDKAssistantMessage as t, SDKImage as u, SDKImageDimension as v, SDKObjectDelta as w, SDKRequestMessage as x, SDKStatusMessage as y, SDKSystemMessage as z };
940
+ export type { ThinkingDeltaUpdate as $, AgentConversationTurn as A, SDKTaskMessage as B, CustomTool as C, DoomLoopThresholds as D, SDKThinkingMessage as E, SDKToolUseMessage as F, SDKUserMessage as G, SDKUserMessageEvent as H, InteractionUpdate as I, SendOptions as J, ShellCommand as K, ShellConversationTurn as L, ModelSelection as M, ShellOutput as N, ShellOutputDeltaUpdate as O, PartialToolCallUpdate as P, StepCompletedUpdate as Q, RunResult as R, SDKMessage as S, StepStartedUpdate as T, StreamToCompletionResult as U, SummaryCompletedUpdate as V, SummaryStartedUpdate as W, SummaryUpdate as X, TextBlock as Y, TextDeltaUpdate as Z, ThinkingCompletedUpdate as _, McpServerConfig as a, ThinkingMessage as a0, TokenDeltaUpdate as a1, TokenUsage as a2, ToolCall as a3, ToolCallCompletedUpdate as a4, ToolCallStartedUpdate as a5, ToolResult as a6, ToolUseBlock as a7, TurnEndedUpdate as a8, UserMessage as a9, UserMessageAppendedUpdate as aa, Run as b, AssistantMessage as c, ConversationStep as d, ConversationTurn as e, CostBreakdown as f, CostSource as g, CostStatus as h, McpAuthConfig as i, McpHttpServerConfig as j, McpOAuthConfig as k, McpStdioServerConfig as l, ModelParameterValue as m, RunErrorDetail as n, RunGitInfo as o, RunOperation as p, RunStatus as q, RunToCompletionOptions as r, RunToCompletionResult as s, SDKAssistantMessage as t, SDKImage as u, SDKImageDimension as v, SDKObjectDelta as w, SDKRequestMessage as x, SDKStatusMessage as y, SDKSystemMessage as z };
@@ -687,6 +687,28 @@ interface RunResult {
687
687
  * @public
688
688
  */
689
689
  stoppedAtIterationLimit?: boolean;
690
+ /**
691
+ * `true` when the run stopped because the **doom-loop guard** detected the model repeating
692
+ * IDENTICAL tool calls (same name + same input) to the hard threshold — making no progress (e.g.
693
+ * a tool that keeps failing and is retried unchanged). `undefined`/absent otherwise. Through the
694
+ * continuation driver this surfaces as `terminal: "no_progress"` (a controlled stop, NOT a
695
+ * truncation to re-send). Tune or disable via {@link SendOptions.doomLoop}.
696
+ *
697
+ * @public
698
+ */
699
+ stoppedByDoomLoop?: boolean;
700
+ }
701
+ /**
702
+ * Doom-loop guard thresholds (see {@link SendOptions.doomLoop}). Both are counts of CONSECUTIVE
703
+ * identical tool calls: `softThreshold` injects a one-time guidance nudge; `hardThreshold` stops.
704
+ *
705
+ * @public
706
+ */
707
+ interface DoomLoopThresholds {
708
+ /** Consecutive-identical count at which a one-time guidance nudge is injected. Default 3. */
709
+ softThreshold?: number;
710
+ /** Consecutive-identical count at which the run stops (`no_progress`). Default 5. */
711
+ hardThreshold?: number;
690
712
  }
691
713
  /**
692
714
  * Options for {@link SDKAgent.runToCompletion} (M1 Phase 3 — continuation driver).
@@ -803,6 +825,12 @@ interface SDKUserMessage {
803
825
  */
804
826
  interface SendOptions {
805
827
  model?: ModelSelection;
828
+ /**
829
+ * Doom-loop guard config. The loop stops (with `terminal: "no_progress"`, `RunResult.stoppedByDoomLoop`)
830
+ * when the model repeats IDENTICAL tool calls to the hard threshold. On by default with generous
831
+ * thresholds (soft 3 / hard 5). Set `false` to disable, or an object to tune the thresholds.
832
+ */
833
+ doomLoop?: false | DoomLoopThresholds;
806
834
  /**
807
835
  * Per-call system prompt override. Wins over `AgentOptions.systemPrompt`.
808
836
  * String only — for dynamic resolvers, configure on `AgentOptions`. An
@@ -909,4 +937,4 @@ interface Run {
909
937
  onDidChangeStatus(listener: (status: RunStatus) => void): () => void;
910
938
  }
911
939
 
912
- export type { ThinkingMessage as $, AgentConversationTurn as A, SDKTaskMessage as B, CustomTool as C, SDKThinkingMessage as D, SDKToolUseMessage as E, SDKUserMessage as F, SDKUserMessageEvent as G, SendOptions as H, InteractionUpdate as I, ShellCommand as J, ShellConversationTurn as K, ShellOutput as L, ModelSelection as M, ShellOutputDeltaUpdate as N, StepCompletedUpdate as O, PartialToolCallUpdate as P, StepStartedUpdate as Q, RunResult as R, SDKMessage as S, StreamToCompletionResult as T, SummaryCompletedUpdate as U, SummaryStartedUpdate as V, SummaryUpdate as W, TextBlock as X, TextDeltaUpdate as Y, ThinkingCompletedUpdate as Z, ThinkingDeltaUpdate as _, McpServerConfig as a, TokenDeltaUpdate as a0, TokenUsage as a1, ToolCall as a2, ToolCallCompletedUpdate as a3, ToolCallStartedUpdate as a4, ToolResult as a5, ToolUseBlock as a6, TurnEndedUpdate as a7, UserMessage as a8, UserMessageAppendedUpdate as a9, Run as b, AssistantMessage as c, ConversationStep as d, ConversationTurn as e, CostBreakdown as f, CostSource as g, CostStatus as h, McpAuthConfig as i, McpHttpServerConfig as j, McpOAuthConfig as k, McpStdioServerConfig as l, ModelParameterValue as m, RunErrorDetail as n, RunGitInfo as o, RunOperation as p, RunStatus as q, RunToCompletionOptions as r, RunToCompletionResult as s, SDKAssistantMessage as t, SDKImage as u, SDKImageDimension as v, SDKObjectDelta as w, SDKRequestMessage as x, SDKStatusMessage as y, SDKSystemMessage as z };
940
+ export type { ThinkingDeltaUpdate as $, AgentConversationTurn as A, SDKTaskMessage as B, CustomTool as C, DoomLoopThresholds as D, SDKThinkingMessage as E, SDKToolUseMessage as F, SDKUserMessage as G, SDKUserMessageEvent as H, InteractionUpdate as I, SendOptions as J, ShellCommand as K, ShellConversationTurn as L, ModelSelection as M, ShellOutput as N, ShellOutputDeltaUpdate as O, PartialToolCallUpdate as P, StepCompletedUpdate as Q, RunResult as R, SDKMessage as S, StepStartedUpdate as T, StreamToCompletionResult as U, SummaryCompletedUpdate as V, SummaryStartedUpdate as W, SummaryUpdate as X, TextBlock as Y, TextDeltaUpdate as Z, ThinkingCompletedUpdate as _, McpServerConfig as a, ThinkingMessage as a0, TokenDeltaUpdate as a1, TokenUsage as a2, ToolCall as a3, ToolCallCompletedUpdate as a4, ToolCallStartedUpdate as a5, ToolResult as a6, ToolUseBlock as a7, TurnEndedUpdate as a8, UserMessage as a9, UserMessageAppendedUpdate as aa, Run as b, AssistantMessage as c, ConversationStep as d, ConversationTurn as e, CostBreakdown as f, CostSource as g, CostStatus as h, McpAuthConfig as i, McpHttpServerConfig as j, McpOAuthConfig as k, McpStdioServerConfig as l, ModelParameterValue as m, RunErrorDetail as n, RunGitInfo as o, RunOperation as p, RunStatus as q, RunToCompletionOptions as r, RunToCompletionResult as s, SDKAssistantMessage as t, SDKImage as u, SDKImageDimension as v, SDKObjectDelta as w, SDKRequestMessage as x, SDKStatusMessage as y, SDKSystemMessage as z };
@@ -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
+ }
@@ -81,6 +81,28 @@ export interface RunResult {
81
81
  * @public
82
82
  */
83
83
  stoppedAtIterationLimit?: boolean;
84
+ /**
85
+ * `true` when the run stopped because the **doom-loop guard** detected the model repeating
86
+ * IDENTICAL tool calls (same name + same input) to the hard threshold — making no progress (e.g.
87
+ * a tool that keeps failing and is retried unchanged). `undefined`/absent otherwise. Through the
88
+ * continuation driver this surfaces as `terminal: "no_progress"` (a controlled stop, NOT a
89
+ * truncation to re-send). Tune or disable via {@link SendOptions.doomLoop}.
90
+ *
91
+ * @public
92
+ */
93
+ stoppedByDoomLoop?: boolean;
94
+ }
95
+ /**
96
+ * Doom-loop guard thresholds (see {@link SendOptions.doomLoop}). Both are counts of CONSECUTIVE
97
+ * identical tool calls: `softThreshold` injects a one-time guidance nudge; `hardThreshold` stops.
98
+ *
99
+ * @public
100
+ */
101
+ export interface DoomLoopThresholds {
102
+ /** Consecutive-identical count at which a one-time guidance nudge is injected. Default 3. */
103
+ softThreshold?: number;
104
+ /** Consecutive-identical count at which the run stops (`no_progress`). Default 5. */
105
+ hardThreshold?: number;
84
106
  }
85
107
  /**
86
108
  * Options for {@link SDKAgent.runToCompletion} (M1 Phase 3 — continuation driver).
@@ -197,6 +219,12 @@ export interface SDKUserMessage {
197
219
  */
198
220
  export interface SendOptions {
199
221
  model?: ModelSelection;
222
+ /**
223
+ * Doom-loop guard config. The loop stops (with `terminal: "no_progress"`, `RunResult.stoppedByDoomLoop`)
224
+ * when the model repeats IDENTICAL tool calls to the hard threshold. On by default with generous
225
+ * thresholds (soft 3 / hard 5). Set `false` to disable, or an object to tune the thresholds.
226
+ */
227
+ doomLoop?: false | DoomLoopThresholds;
200
228
  /**
201
229
  * Per-call system prompt override. Wins over `AgentOptions.systemPrompt`.
202
230
  * String only — for dynamic resolvers, configure on `AgentOptions`. An