@openrouter/agent 0.8.0 → 0.9.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 (87) hide show
  1. package/README.md +483 -5
  2. package/esm/index.d.ts +19 -4
  3. package/esm/index.d.ts.map +1 -1
  4. package/esm/index.js +9 -2
  5. package/esm/index.js.map +1 -1
  6. package/esm/inner-loop/call-model.d.ts.map +1 -1
  7. package/esm/inner-loop/call-model.js +40 -41
  8. package/esm/inner-loop/call-model.js.map +1 -1
  9. package/esm/inner-loop/resume-tool-results.d.ts +128 -0
  10. package/esm/inner-loop/resume-tool-results.d.ts.map +1 -0
  11. package/esm/inner-loop/resume-tool-results.js +244 -0
  12. package/esm/inner-loop/resume-tool-results.js.map +1 -0
  13. package/esm/lib/agent-tool.d.ts +92 -0
  14. package/esm/lib/agent-tool.d.ts.map +1 -0
  15. package/esm/lib/agent-tool.js +211 -0
  16. package/esm/lib/agent-tool.js.map +1 -0
  17. package/esm/lib/async-params.d.ts +82 -0
  18. package/esm/lib/async-params.d.ts.map +1 -1
  19. package/esm/lib/async-params.js +5 -0
  20. package/esm/lib/async-params.js.map +1 -1
  21. package/esm/lib/async-tool-registry.d.ts +134 -0
  22. package/esm/lib/async-tool-registry.d.ts.map +1 -0
  23. package/esm/lib/async-tool-registry.js +352 -0
  24. package/esm/lib/async-tool-registry.js.map +1 -0
  25. package/esm/lib/async-tools.d.ts +23 -0
  26. package/esm/lib/async-tools.d.ts.map +1 -0
  27. package/esm/lib/async-tools.js +19 -0
  28. package/esm/lib/async-tools.js.map +1 -0
  29. package/esm/lib/conversation-state.d.ts +1 -0
  30. package/esm/lib/conversation-state.d.ts.map +1 -1
  31. package/esm/lib/conversation-state.js +8 -0
  32. package/esm/lib/conversation-state.js.map +1 -1
  33. package/esm/lib/doom-loop.d.ts +533 -0
  34. package/esm/lib/doom-loop.d.ts.map +1 -0
  35. package/esm/lib/doom-loop.js +1221 -0
  36. package/esm/lib/doom-loop.js.map +1 -0
  37. package/esm/lib/hooks-schemas.d.ts +33 -0
  38. package/esm/lib/hooks-schemas.d.ts.map +1 -1
  39. package/esm/lib/hooks-schemas.js +54 -0
  40. package/esm/lib/hooks-schemas.js.map +1 -1
  41. package/esm/lib/hooks-types.d.ts +6 -2
  42. package/esm/lib/hooks-types.d.ts.map +1 -1
  43. package/esm/lib/hooks-types.js.map +1 -1
  44. package/esm/lib/model-result.d.ts +646 -10
  45. package/esm/lib/model-result.d.ts.map +1 -1
  46. package/esm/lib/model-result.js +2749 -153
  47. package/esm/lib/model-result.js.map +1 -1
  48. package/esm/lib/reusable-stream.d.ts +9 -0
  49. package/esm/lib/reusable-stream.d.ts.map +1 -1
  50. package/esm/lib/reusable-stream.js +17 -0
  51. package/esm/lib/reusable-stream.js.map +1 -1
  52. package/esm/lib/stream-transformers.d.ts +20 -0
  53. package/esm/lib/stream-transformers.d.ts.map +1 -1
  54. package/esm/lib/stream-transformers.js +45 -0
  55. package/esm/lib/stream-transformers.js.map +1 -1
  56. package/esm/lib/tool-check.d.ts +76 -0
  57. package/esm/lib/tool-check.d.ts.map +1 -0
  58. package/esm/lib/tool-check.js +241 -0
  59. package/esm/lib/tool-check.js.map +1 -0
  60. package/esm/lib/tool-concurrency.d.ts +42 -0
  61. package/esm/lib/tool-concurrency.d.ts.map +1 -0
  62. package/esm/lib/tool-concurrency.js +80 -0
  63. package/esm/lib/tool-concurrency.js.map +1 -0
  64. package/esm/lib/tool-context.d.ts +45 -1
  65. package/esm/lib/tool-context.d.ts.map +1 -1
  66. package/esm/lib/tool-context.js +60 -1
  67. package/esm/lib/tool-context.js.map +1 -1
  68. package/esm/lib/tool-executor.d.ts +52 -9
  69. package/esm/lib/tool-executor.d.ts.map +1 -1
  70. package/esm/lib/tool-executor.js +253 -21
  71. package/esm/lib/tool-executor.js.map +1 -1
  72. package/esm/lib/tool-orchestrator.d.ts.map +1 -1
  73. package/esm/lib/tool-orchestrator.js +87 -50
  74. package/esm/lib/tool-orchestrator.js.map +1 -1
  75. package/esm/lib/tool-task.d.ts +157 -0
  76. package/esm/lib/tool-task.d.ts.map +1 -0
  77. package/esm/lib/tool-task.js +206 -0
  78. package/esm/lib/tool-task.js.map +1 -0
  79. package/esm/lib/tool-types.d.ts +473 -14
  80. package/esm/lib/tool-types.d.ts.map +1 -1
  81. package/esm/lib/tool-types.js +59 -10
  82. package/esm/lib/tool-types.js.map +1 -1
  83. package/esm/lib/tool.d.ts +184 -2
  84. package/esm/lib/tool.d.ts.map +1 -1
  85. package/esm/lib/tool.js +193 -3
  86. package/esm/lib/tool.js.map +1 -1
  87. package/package.json +30 -2
@@ -0,0 +1,533 @@
1
+ /**
2
+ * Doom-loop detection for the tool-execution loop.
3
+ *
4
+ * A "doom loop" is a run that stops making progress while continuing to spend:
5
+ * the model re-issues the same tool call with identical arguments (including
6
+ * repeated *empty* calls and repeated unparseable calls), or emits the same
7
+ * text tokens over and over. This module provides the deterministic detection
8
+ * primitives; `ModelResult` wires them into the loop behind the `doomLoop`
9
+ * option on `callModel`.
10
+ *
11
+ * Design principles:
12
+ *
13
+ * - **Deterministic.** Every verdict is a pure function of the recorded
14
+ * sequence of tool calls / assistant texts. The same scripted transcript
15
+ * produces the same verdicts at the same indices — no wall clocks, no
16
+ * randomness — so the feature is testable without a live model and
17
+ * portable across SDK ports via shared test vectors.
18
+ * - **Cross-port fingerprints.** Key material is canonicalized per RFC 8785
19
+ * (JCS) and hashed with SHA-256 over the UTF-8 bytes. Ports (Python/Go)
20
+ * MUST use a JCS implementation (e.g. pip `jcs`,
21
+ * `cyberphone/json-canonicalization`), not their stdlib JSON serializer —
22
+ * the shared vector file `tests/vectors/doom-loop-fingerprints.json` is
23
+ * the conformance contract.
24
+ * - **Tool-declared identity.** A tool opts into precise loop identity via
25
+ * `loopKey` on its definition: a function computing key material
26
+ * (e.g. `({ command, cwd }) => ({ command, cwd })`), a field-name array
27
+ * (`['command', 'cwd']` — data, not code, so it can ride MCP tool caches
28
+ * and the `_meta['openrouter/loopKey']` wire field), or `false` to exempt
29
+ * the tool. Tools declare *what identifies a call*; this module owns
30
+ * canonicalization and hashing so fingerprints are uniform across tools
31
+ * and ports.
32
+ * - **Round-scoped streaks.** A streak measures the model *re-issuing a call
33
+ * after seeing its result*, which requires a model round trip. N identical
34
+ * calls fanned out in ONE round count once; the duplicates share that
35
+ * round's decision. A round's identity for one tool is the *set* of
36
+ * fingerprints it was called with, so a fan-out of distinct arguments
37
+ * (`read(a), read(b), read(c)`) reissued verbatim accumulates, and every
38
+ * call in the round reports the round's streak. Ports must declare a
39
+ * round's complete set before scoring any of its calls (see
40
+ * `declareRound`): scoring a set as it accumulates makes a superset round
41
+ * transiently match the previous one, which both fires on real progress
42
+ * and makes the outcome depend on emission order.
43
+ * - **Graduated response.** Detection feeds a configurable action ladder
44
+ * (observe → steer → block → stop) rather than killing the run outright.
45
+ * - **Never the cause of failure.** Detection may only affect a run through
46
+ * its defined actions. Any internal error (unhashable key material,
47
+ * throwing `loopKey`, corrupt persisted state) degrades to a fallback
48
+ * identity or skips detection for that call — it never rejects the call
49
+ * or crashes the run.
50
+ */
51
+ /**
52
+ * Escalation actions, weakest to strongest:
53
+ *
54
+ * - `observe` — emit the `DoomLoopDetected` hook only.
55
+ * - `steer` — inject a corrective user message before the next model turn
56
+ * (same mechanism as the Stop hook's `appendPrompt`); the call still runs.
57
+ * - `escalate` — recover by throwing more intelligence at the NEXT turn:
58
+ * temporarily swap to a stronger model and/or force an `openrouter:advisor`
59
+ * consult, per the `escalation` config. One-turn overrides; the run
60
+ * returns to its configured model afterward. Bounded by
61
+ * `escalation.maxEscalations`; exhausted or unconfigured escalations fall
62
+ * through to the weaker rungs. The call still runs.
63
+ * - `block` — refuse the tool call and synthesize an error
64
+ * `function_call_output` (same shape as a PreToolUse block); the error text
65
+ * is itself steering, delivered where the model looks.
66
+ * - `stop` — halt the run before the next model request
67
+ * (`SessionEnd.reason: 'doom_loop'`).
68
+ */
69
+ export type DoomLoopAction = 'observe' | 'steer' | 'escalate' | 'block' | 'stop';
70
+ /** Which detector produced a verdict. */
71
+ export type DoomLoopDetectorKind = 'tool-fingerprint' | 'server-tool-fingerprint' | 'text-repetition' | 'text-streak';
72
+ /**
73
+ * A doom-loop detection event.
74
+ *
75
+ * `streak` is the repetition count that crossed a ladder threshold: for
76
+ * fingerprint detectors the number of consecutive rounds in which that tool
77
+ * was called with the same fingerprint *set* (so a repeated fan-out counts,
78
+ * and every call in the round reports the round's count); for
79
+ * `text-repetition` the number of consecutive repeats of a token block within
80
+ * one response; for `text-streak` the number of consecutive steps with
81
+ * identical assistant text.
82
+ */
83
+ export interface DoomLoopVerdict {
84
+ detector: DoomLoopDetectorKind;
85
+ action: DoomLoopAction;
86
+ streak: number;
87
+ /** Deterministic fingerprint of the repeated unit (call identity or text). */
88
+ fingerprint: string;
89
+ /** Present for fingerprint verdicts. */
90
+ toolName?: string;
91
+ /** Human/model-readable explanation; used verbatim for block outputs and steer messages. */
92
+ message: string;
93
+ }
94
+ /**
95
+ * Result of recording one tool call with the monitor.
96
+ *
97
+ * `duplicateInRound` is true when the same `(toolName, fingerprint)` was
98
+ * already recorded in this round — the streak did NOT increment and the
99
+ * caller should reuse the decision it applied for the first occurrence
100
+ * rather than re-emitting hooks.
101
+ */
102
+ export interface DoomLoopCallRecord {
103
+ fingerprint: string;
104
+ streak: number;
105
+ duplicateInRound: boolean;
106
+ verdict?: DoomLoopVerdict;
107
+ }
108
+ /**
109
+ * Streak thresholds for each action. A threshold of `false` disables that
110
+ * rung. When several rungs are crossed the strongest wins
111
+ * (stop > block > escalate > steer > observe). Thresholds compare with
112
+ * `>=`, so a verdict fires on *every* record at or past a rung — escalating
113
+ * as the streak grows.
114
+ */
115
+ export interface DoomLoopLadder {
116
+ observe?: number | false;
117
+ steer?: number | false;
118
+ escalate?: number | false;
119
+ block?: number | false;
120
+ stop?: number | false;
121
+ }
122
+ /**
123
+ * Recovery configuration for the `escalate` ladder action: what "throw more
124
+ * intelligence at the stuck turn" means for this run. At least one of
125
+ * `model`/`advisor` must be set for the rung to do anything (enabling the
126
+ * rung without a config warns and the rung is skipped).
127
+ */
128
+ export interface DoomLoopEscalationConfig {
129
+ /**
130
+ * Model slug to run the NEXT turn on (e.g. a frontier model), replacing
131
+ * the request's configured model for one request only. The following
132
+ * turn reverts automatically.
133
+ */
134
+ model?: string;
135
+ /**
136
+ * Force an `openrouter:advisor` consult on the next turn: the advisor
137
+ * server tool is added to the request with these parameters and
138
+ * `toolChoice` pinned to it, so the stuck model must ask for guidance
139
+ * before doing anything else. The advisor's transcript context defaults
140
+ * to on (`forwardTranscript: true`) so it sees the loop it is diagnosing.
141
+ *
142
+ * `true` uses defaults (advisor model chosen by the platform, transcript
143
+ * forwarded, instructions describing the detected loop); an object is
144
+ * passed through as the advisor tool's `parameters` (missing
145
+ * `instructions`/`forwardTranscript` filled with those defaults).
146
+ */
147
+ advisor?: boolean | Record<string, unknown>;
148
+ /**
149
+ * Escalations are real spend on a run already suspected of wasting it:
150
+ * cap how many times this run may escalate. When exhausted, `escalate`
151
+ * verdicts fall through to the weaker rungs (steer/observe). Default 2.
152
+ */
153
+ maxEscalations?: number;
154
+ }
155
+ /** Options for within-response text-repetition detection. */
156
+ export interface DoomLoopTextOptions {
157
+ /** Master switch for both text detectors. Default true. */
158
+ enabled?: boolean;
159
+ /** Longest repeating token block to search for. Default 16. */
160
+ maxPeriodTokens?: number;
161
+ /** Minimum consecutive repeats of a block to count as repetition. Default 4. */
162
+ minRepeats?: number;
163
+ /** Minimum total tokens covered by the repetition. Default 12. */
164
+ minCoveredTokens?: number;
165
+ /** Only the trailing N tokens of a response are scanned. Default 400. */
166
+ maxWindowTokens?: number;
167
+ }
168
+ /** Configuration for doom-loop detection. `doomLoop: true` uses all defaults. */
169
+ export interface DoomLoopConfig {
170
+ ladder?: DoomLoopLadder;
171
+ /** Text-loop detection. `false` disables; an object tunes it. Default on. */
172
+ text?: boolean | DoomLoopTextOptions;
173
+ /** Recovery behavior for the `escalate` rung. Off unless configured. */
174
+ escalation?: DoomLoopEscalationConfig;
175
+ }
176
+ /** The `doomLoop` option on `callModel`: `true` for defaults, or a config object. */
177
+ export type DoomLoopOption = boolean | DoomLoopConfig;
178
+ /** Consecutive-repetition counter for one identity (a tool, or step text). */
179
+ export interface DoomLoopStreak {
180
+ fingerprint: string;
181
+ streak: number;
182
+ /**
183
+ * The full fingerprint set of the tool's last round, present only when that
184
+ * round had more than one distinct call. `fingerprint`+`streak` alone cannot
185
+ * say WHICH set earned a count, so without this a fan-out streak either
186
+ * attached to one arbitrary member (refusing a lesser resumed call on first
187
+ * appearance) or had to be discarded at every save — losing block-level
188
+ * evidence across approval pauses and re-baselining on every per-turn
189
+ * resume. Absent for single-call rounds and in pre-existing blobs, where
190
+ * `fingerprint` fully describes the round. Never present on text streaks.
191
+ */
192
+ roundFingerprints?: readonly string[];
193
+ /**
194
+ * Per-call streaks for the tool's last round: fingerprint → number of
195
+ * consecutive rounds that exact call has been issued in. This is the
196
+ * evidence for the PER-CALL detector, which catches a call repeating inside
197
+ * rounds whose other members keep changing (`[a,b]`, `[a,c]`, `[a,d]` — the
198
+ * round set differs every time, but `a` is a 3-peat). Persisted so a repeat
199
+ * spanning a save/resume boundary (approval pause, per-turn resume) keeps
200
+ * counting. Bounded by the width of one round; absent in pre-existing
201
+ * blobs, where per-call evidence simply restarts. Never present on text
202
+ * streaks.
203
+ */
204
+ callStreaks?: Record<string, number>;
205
+ }
206
+ /**
207
+ * Plain-JSON detector state, persisted inside `ConversationState.doomLoop`
208
+ * so loop memory survives serialize → resume: a resumed doom loop is still
209
+ * a doom loop (provided the resuming call passes `doomLoop` again).
210
+ */
211
+ export interface DoomLoopSerializedState {
212
+ /** Per-tool streaks keyed by tool name. Interleaved calls to other tools do not reset a tool's streak. */
213
+ tools: Record<string, DoomLoopStreak>;
214
+ /** Cross-step assistant-text streak. */
215
+ text?: DoomLoopStreak;
216
+ /**
217
+ * The verdict that condemned this run, when a `stop` action armed. Kept
218
+ * across decision-only resumes (`approveToolCalls`/`rejectToolCalls`) so a
219
+ * condemned run stays halted; cleared by a fresh conversational turn
220
+ * (operator intervention = new information). Streaks are kept either way.
221
+ */
222
+ stopVerdict?: DoomLoopVerdict;
223
+ /**
224
+ * Steer guidance queued but not yet injected when the run paused. Flushed
225
+ * into the conversation on resume.
226
+ */
227
+ pendingSteer?: string[];
228
+ /**
229
+ * How many escalation recoveries this conversation has consumed, counted
230
+ * against `escalation.maxEscalations`. Persisted so a resumed run cannot
231
+ * reset its escalation budget by resuming.
232
+ */
233
+ escalationsUsed?: number;
234
+ }
235
+ /**
236
+ * The result of resolving a tool's `loopKey` declaration against one call's
237
+ * validated arguments. See {@link resolveLoopKeyMaterial}.
238
+ */
239
+ export type LoopKeyResolution = {
240
+ kind: 'exempt';
241
+ } | {
242
+ kind: 'key';
243
+ keyMaterial: unknown;
244
+ } | {
245
+ kind: 'fallback';
246
+ keyMaterial: unknown;
247
+ warning: string;
248
+ };
249
+ /**
250
+ * Default ladder: observe at 2 consecutive identical rounds, block at 3,
251
+ * stop at 6. Steer is off by default (blocking already delivers feedback in
252
+ * the tool output, where the model looks first).
253
+ */
254
+ export declare const DEFAULT_DOOM_LOOP_LADDER: Readonly<Required<DoomLoopLadder>>;
255
+ /** Default escalation budget when the rung is enabled without a cap. */
256
+ export declare const DEFAULT_MAX_ESCALATIONS = 2;
257
+ /** Escalation config with the budget resolved. */
258
+ export interface ResolvedEscalationConfig {
259
+ model?: string;
260
+ advisor?: boolean | Record<string, unknown>;
261
+ maxEscalations: number;
262
+ }
263
+ /** Fully-resolved config used by the monitor. */
264
+ export interface ResolvedDoomLoopConfig {
265
+ ladder: Required<DoomLoopLadder>;
266
+ text: Required<DoomLoopTextOptions>;
267
+ escalation: ResolvedEscalationConfig | null;
268
+ }
269
+ /**
270
+ * Normalize the `doomLoop` option. `undefined` / `false` → null (detection
271
+ * off — the SDK's default posture: explicit control over implicit magic).
272
+ *
273
+ * Overloaded so the direct-construction pattern compiles without a null
274
+ * check: `true` or a config object always yields a resolved config, so
275
+ * `new DoomLoopMonitor(resolveDoomLoopOption(true))` is well-typed. Only the
276
+ * engine's pass-through of a caller's raw option can produce `null`.
277
+ */
278
+ export declare function resolveDoomLoopOption(option: DoomLoopConfig | true): ResolvedDoomLoopConfig;
279
+ export declare function resolveDoomLoopOption(option: DoomLoopOption | undefined): ResolvedDoomLoopConfig | null;
280
+ /**
281
+ * Resolve a tool's `loopKey` declaration (function, `false`, or absent)
282
+ * against one call's validated arguments.
283
+ *
284
+ * - absent (`undefined` declaration) → the full arguments object.
285
+ * - `false` → the tool is statically exempt.
286
+ * - function → called with the arguments, like every other tool hook.
287
+ * `null` exempts THIS call; `undefined` falls back to the full arguments
288
+ * (with a warning — a bare `return;` is almost always a bug, and treating
289
+ * it as key material would collide every call of the tool onto one
290
+ * fingerprint); a throw falls back to the full arguments (detection must
291
+ * never take down a run).
292
+ */
293
+ export declare function resolveLoopKeyMaterial(loopKey: unknown, args: Record<string, unknown>): LoopKeyResolution;
294
+ /**
295
+ * Maximum nesting depth accepted by {@link canonicalizeKeyMaterial}. Real
296
+ * tool arguments come from parsed JSON and stay shallow; anything deeper is
297
+ * hostile or buggy key material and must fail fast (with a catchable error)
298
+ * instead of overflowing the stack.
299
+ */
300
+ export declare const MAX_CANONICALIZE_DEPTH = 64;
301
+ /**
302
+ * RFC 8785 (JCS) canonical JSON: object keys sorted by UTF-16 code units
303
+ * (recursively), arrays in order, strings and finite numbers serialized per
304
+ * ECMAScript `JSON.stringify` (which IS the JCS serialization — e.g. `-0`
305
+ * canonicalizes to `0`, `1e21` to `1e+21`, lone surrogates to `\udXXX`
306
+ * escapes). Insensitive to key insertion order, so `{a:1,b:2}` and
307
+ * `{b:2,a:1}` canonicalize identically.
308
+ *
309
+ * This is the cross-port contract: Python and Go ports must produce
310
+ * byte-identical canonical strings for the same key material, which means
311
+ * they MUST use an RFC 8785 implementation (pip `jcs`,
312
+ * `cyberphone/json-canonicalization`), not their stdlib JSON serializer
313
+ * (Python's `json.dumps(-0.0)` is `-0.0`, which is NOT JCS).
314
+ *
315
+ * JSON-data-model semantics for JS-only values:
316
+ * - `undefined` / function / symbol object entries are dropped (as
317
+ * `JSON.stringify` does); in arrays they serialize as `null`.
318
+ *
319
+ * @throws Error on values RFC 8785 cannot represent — non-finite numbers,
320
+ * bigint — plus circular references and nesting deeper than
321
+ * {@link MAX_CANONICALIZE_DEPTH}. Real tool arguments (parsed JSON) can
322
+ * never trigger these; only computed `loopKey` material can, and the engine
323
+ * catches the error and falls back to the full-arguments identity.
324
+ */
325
+ export declare function canonicalizeKeyMaterial(value: unknown): string;
326
+ /**
327
+ * Fingerprint arbitrary key material: `sha256(utf8(jcs(keyMaterial)))`,
328
+ * lowercase hex (64 chars).
329
+ */
330
+ export declare function fingerprintKeyMaterial(keyMaterial: unknown): Promise<string>;
331
+ /**
332
+ * Fingerprint a tool call: `sha256(utf8(toolName + '\n' + jcs(keyMaterial)))`,
333
+ * lowercase hex. The tool name participates so `search({q})` and
334
+ * `fetch({q})` with equal arguments never share a fingerprint. This exact
335
+ * construction is the cross-port contract — see
336
+ * `tests/vectors/doom-loop-fingerprints.json`.
337
+ */
338
+ export declare function fingerprintToolCall(toolName: string, keyMaterial: unknown): Promise<string>;
339
+ /** A detected repeating token block at the tail of a response. */
340
+ export interface TextRepetitionResult {
341
+ /** Consecutive repeats of the block (feeds the ladder as the streak). */
342
+ repeats: number;
343
+ /** Block length in whitespace-delimited tokens. */
344
+ periodTokens: number;
345
+ /** repeats × periodTokens. */
346
+ coveredTokens: number;
347
+ /** The repeating block itself. */
348
+ sample: string;
349
+ }
350
+ /**
351
+ * Detect a period-p token block repeating at the *tail* of `text` — the
352
+ * canonical shape of an in-response token doom loop ("I am stuck. I am
353
+ * stuck. I am stuck. …"). Pure and deterministic: whitespace tokenization,
354
+ * suffix comparison, no heuristics beyond the thresholds.
355
+ *
356
+ * Only the trailing region of the text is examined: a fixed character
357
+ * budget (64 chars per window token) is sliced off the tail before
358
+ * tokenization, so multi-megabyte responses do not pay a full-text token
359
+ * split. The budget is part of the deterministic contract (same text ⇒
360
+ * same slice ⇒ same result).
361
+ *
362
+ * Ties on covered tokens prefer the smallest period (the most repeats), so
363
+ * "no no no no no no" reports p=1 × 6, not p=2 × 3 — the repeat count is
364
+ * what feeds the ladder.
365
+ *
366
+ * Returns null when no block meets `minRepeats` and `minCoveredTokens`
367
+ * within `maxPeriodTokens` / `maxWindowTokens`.
368
+ *
369
+ * KNOWN LIMIT: token blocks must repeat *exactly*. Paraphrased loops ("I am
370
+ * stuck" / "I appear to be stuck" / …) do not trip this detector.
371
+ */
372
+ export declare function detectTextRepetition(text: string, options?: Partial<DoomLoopTextOptions>): TextRepetitionResult | null;
373
+ /**
374
+ * Map a streak onto the strongest crossed ladder rung.
375
+ *
376
+ * `allowBlock: false` is used for text and server-tool verdicts: the tokens
377
+ * are already emitted / the tool already ran server-side, so there is
378
+ * nothing to block — a block-level streak falls through to escalate/steer
379
+ * (when enabled) or observe, while stop still stops.
380
+ *
381
+ * `allowEscalate: false` disables the escalate rung for this resolution —
382
+ * used when no escalation mechanism is configured or the run's escalation
383
+ * budget is exhausted; the streak falls through to the weaker rungs.
384
+ * Escalation applies to the NEXT model request, so it is available to every
385
+ * detector kind (tool, server-tool, and text verdicts alike).
386
+ */
387
+ export declare function resolveLadderAction(ladder: Required<DoomLoopLadder>, streak: number, options: {
388
+ allowBlock: boolean;
389
+ allowEscalate?: boolean;
390
+ }): DoomLoopAction | undefined;
391
+ /**
392
+ * Pure state machine over the recorded transcript: feed it tool calls and
393
+ * assistant texts, get verdicts back. Holds no engine concerns (hook
394
+ * emission, blocking, steering, stopping live in `ModelResult`), so its
395
+ * behavior is fully exercisable in unit tests and portable as a spec.
396
+ *
397
+ * State is bounded, plain JSON (one streak entry per distinct tool name +
398
+ * one text streak) and round-trips through `ConversationState`.
399
+ */
400
+ export declare class DoomLoopMonitor {
401
+ private readonly config;
402
+ private tools;
403
+ private text;
404
+ private escalationsUsed;
405
+ private declaredRound;
406
+ constructor(config: ResolvedDoomLoopConfig, initialState?: unknown);
407
+ /**
408
+ * Declare the complete set of calls a round will make, before any of them
409
+ * is recorded. Each entry is one call's `(toolName, keyMaterial)`; the
410
+ * monitor groups them per tool into that tool's round set.
411
+ *
412
+ * A round's identity is the *set* of fingerprints a tool was called with,
413
+ * so that set has to be known up front. Accumulating it call by call meant
414
+ * a round that is a strict superset of the previous one transiently equaled
415
+ * it while filling — `[a,b]`, `[a,b]`, `[a,b,c]` scored a verdict on the `b`
416
+ * of the third round and refused a call that represented real progress. It
417
+ * was also emission-order dependent: `[c,a,b]` never formed the matching
418
+ * prefix and scored nothing. Declaring the whole set removes both.
419
+ *
420
+ * Idempotent per round, and safe to skip: PER-CALL detection needs no
421
+ * declaration — every repeated `(tool, arguments)` identity accumulates its
422
+ * own consecutive-round count regardless, so an undeclared repeating
423
+ * fan-out still flags each repeated member (server-tool records take that
424
+ * path). What the declaration adds is round-set evidence: the fan-out
425
+ * scored as one unit, with a shared verdict and one steer message, instead
426
+ * of member-by-member. Unhashable key material is skipped here with a
427
+ * warning; the caller's own fallback chain handles it at record time.
428
+ * Never serialized.
429
+ */
430
+ declareRound(round: number, calls: readonly {
431
+ toolName: string;
432
+ keyMaterial: unknown;
433
+ }[]): Promise<void>;
434
+ /**
435
+ * Fingerprint with a per-object memo. The engine hands the SAME key-material
436
+ * object to `declareRound` and then to `recordToolCall` (the resolved
437
+ * `loopKey` output is cached per call id), so without this every checked
438
+ * call is canonicalized and SHA-256'd twice per round. WeakMap keeps the
439
+ * memo from retaining arguments beyond their natural lifetime; primitive
440
+ * key material (raw-string malformed args) skips the memo — it cannot key a
441
+ * WeakMap, and such calls are rare and small. Callers that mutate key
442
+ * material between declare and record get the declare-time identity, which
443
+ * is the identity the round was declared with — the consistent choice.
444
+ */
445
+ private readonly fingerprintMemo;
446
+ private fingerprintOnce;
447
+ /**
448
+ * True when the escalate rung can still fire: a mechanism is configured
449
+ * and the budget is not exhausted.
450
+ */
451
+ canEscalate(): boolean;
452
+ /**
453
+ * Consume one escalation from the budget. The ENGINE calls this when it
454
+ * actually applies the recovery (model swap / advisor forcing) — not at
455
+ * verdict time, so a verdict the engine ends up not honoring (e.g. a hook
456
+ * override) does not burn budget.
457
+ */
458
+ consumeEscalation(): void;
459
+ /**
460
+ * Snapshot the serializable detector state (deep copy). Round markers are
461
+ * deliberately dropped: rounds are meaningful only within one run.
462
+ * `stopVerdict`/`pendingSteer` are owned by the engine, which merges them
463
+ * into the persisted blob alongside this snapshot.
464
+ */
465
+ getState(): DoomLoopSerializedState;
466
+ /**
467
+ * Restore persisted state (from `ConversationState.doomLoop`). Invalid
468
+ * blobs are ignored with a warning — a corrupt detector state must never
469
+ * take down a resumed run. Engine-owned fields (`stopVerdict`,
470
+ * `pendingSteer`) are ignored here; the engine restores them itself.
471
+ */
472
+ restore(state: unknown): void;
473
+ /**
474
+ * Record one tool call and return the streak record plus any verdict.
475
+ *
476
+ * Streak semantics:
477
+ * - Per tool: interleaved calls to *other* tools do not reset a tool's
478
+ * streak (so `search X, read file, search X, read file` still trips).
479
+ * - Per round: the same `(tool, fingerprint)` recorded again in the SAME
480
+ * `round` is a duplicate — the streak does not increment
481
+ * (`duplicateInRound: true`) and the caller should reuse the decision it
482
+ * applied for the first occurrence. A streak measures the model
483
+ * re-issuing a call *after seeing its result*, which requires a round
484
+ * trip; N parallel identical calls in one turn are one piece of
485
+ * evidence, not N.
486
+ * - A round's identity is the *set* of fingerprints the tool was called
487
+ * with, not its last call, so a fan-out of distinct arguments reissued
488
+ * verbatim accumulates. A round whose set differs from the previous
489
+ * round's — in either direction, including a superset — resets to 1.
490
+ * Every call in a round reports that round's streak, so the ladder
491
+ * applies to the round as a unit. See {@link declareRound}: the set must
492
+ * be declared before the round's first call for this to hold for
493
+ * multi-call rounds.
494
+ *
495
+ * Blocked calls are recorded like any other — a model re-issuing a
496
+ * blocked call in a later round is stronger loop evidence, not progress.
497
+ *
498
+ * @throws Whatever {@link canonicalizeKeyMaterial} throws for unhashable
499
+ * key material. Callers (the engine) must catch and fall back — see
500
+ * `resolveLoopKeyMaterial` and the engine's fallback chain.
501
+ */
502
+ recordToolCall(toolName: string, keyMaterial: unknown, round: number, options?: {
503
+ /** False for post-execution records (server tools): block is meaningless. */
504
+ allowBlock?: boolean;
505
+ /** Verdict detector label; default 'tool-fingerprint'. */
506
+ detector?: Extract<DoomLoopDetectorKind, 'tool-fingerprint' | 'server-tool-fingerprint'>;
507
+ }): Promise<DoomLoopCallRecord>;
508
+ /**
509
+ * Clear the cross-step text streak. Called by the engine when a late
510
+ * async-tool result is injected into the conversation: the run made
511
+ * observable forward progress, so a model that said "still waiting…"
512
+ * between deliveries must not accumulate a repetition streak toward a
513
+ * false-positive stop. Tool streaks are deliberately kept — re-issuing an
514
+ * identical tool call after a delivery is still loop evidence.
515
+ */
516
+ resetTextStreak(): void;
517
+ /**
518
+ * Record one step's assistant text and return the strongest verdict from
519
+ * the two text detectors:
520
+ *
521
+ * - `text-repetition`: a token block repeating within THIS response; the
522
+ * repeat count feeds the ladder directly, so a single response spinning
523
+ * "I am stuck." dozens of times can stop the run immediately.
524
+ * - `text-streak`: byte-identical (whitespace-normalized) text across
525
+ * consecutive steps. KNOWN LIMIT: paraphrased repetition does not trip.
526
+ *
527
+ * Empty/whitespace-only text (typical for tool-only turns) is a no-op:
528
+ * it neither counts nor resets the cross-step streak, mirroring how
529
+ * interleaved other-tool calls don't reset a tool streak.
530
+ */
531
+ recordAssistantText(text: string): Promise<DoomLoopVerdict | undefined>;
532
+ }
533
+ //# sourceMappingURL=doom-loop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doom-loop.d.ts","sourceRoot":"","sources":["../../src/lib/doom-loop.ts"],"names":[],"mappings":"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"}