@openrouter/agent 0.7.2 → 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 (105) hide show
  1. package/README.md +728 -19
  2. package/esm/index.d.ts +24 -6
  3. package/esm/index.d.ts.map +1 -1
  4. package/esm/index.js +20 -5
  5. package/esm/index.js.map +1 -1
  6. package/esm/inner-loop/call-model.d.ts +8 -5
  7. package/esm/inner-loop/call-model.d.ts.map +1 -1
  8. package/esm/inner-loop/call-model.js +49 -40
  9. package/esm/inner-loop/call-model.js.map +1 -1
  10. package/esm/inner-loop/resume-tool-results.d.ts +128 -0
  11. package/esm/inner-loop/resume-tool-results.d.ts.map +1 -0
  12. package/esm/inner-loop/resume-tool-results.js +244 -0
  13. package/esm/inner-loop/resume-tool-results.js.map +1 -0
  14. package/esm/lib/agent-tool.d.ts +92 -0
  15. package/esm/lib/agent-tool.d.ts.map +1 -0
  16. package/esm/lib/agent-tool.js +211 -0
  17. package/esm/lib/agent-tool.js.map +1 -0
  18. package/esm/lib/async-params.d.ts +107 -6
  19. package/esm/lib/async-params.d.ts.map +1 -1
  20. package/esm/lib/async-params.js +8 -1
  21. package/esm/lib/async-params.js.map +1 -1
  22. package/esm/lib/async-tool-registry.d.ts +134 -0
  23. package/esm/lib/async-tool-registry.d.ts.map +1 -0
  24. package/esm/lib/async-tool-registry.js +352 -0
  25. package/esm/lib/async-tool-registry.js.map +1 -0
  26. package/esm/lib/async-tools.d.ts +23 -0
  27. package/esm/lib/async-tools.d.ts.map +1 -0
  28. package/esm/lib/async-tools.js +19 -0
  29. package/esm/lib/async-tools.js.map +1 -0
  30. package/esm/lib/conversation-state.d.ts +45 -1
  31. package/esm/lib/conversation-state.d.ts.map +1 -1
  32. package/esm/lib/conversation-state.js +119 -0
  33. package/esm/lib/conversation-state.js.map +1 -1
  34. package/esm/lib/doom-loop.d.ts +533 -0
  35. package/esm/lib/doom-loop.d.ts.map +1 -0
  36. package/esm/lib/doom-loop.js +1221 -0
  37. package/esm/lib/doom-loop.js.map +1 -0
  38. package/esm/lib/hooks-emit.d.ts +50 -0
  39. package/esm/lib/hooks-emit.d.ts.map +1 -0
  40. package/esm/lib/hooks-emit.js +307 -0
  41. package/esm/lib/hooks-emit.js.map +1 -0
  42. package/esm/lib/hooks-manager.d.ts +141 -0
  43. package/esm/lib/hooks-manager.d.ts.map +1 -0
  44. package/esm/lib/hooks-manager.js +282 -0
  45. package/esm/lib/hooks-manager.js.map +1 -0
  46. package/esm/lib/hooks-matchers.d.ts +12 -0
  47. package/esm/lib/hooks-matchers.d.ts.map +1 -0
  48. package/esm/lib/hooks-matchers.js +26 -0
  49. package/esm/lib/hooks-matchers.js.map +1 -0
  50. package/esm/lib/hooks-resolve.d.ts +17 -0
  51. package/esm/lib/hooks-resolve.d.ts.map +1 -0
  52. package/esm/lib/hooks-resolve.js +42 -0
  53. package/esm/lib/hooks-resolve.js.map +1 -0
  54. package/esm/lib/hooks-schemas.d.ts +221 -0
  55. package/esm/lib/hooks-schemas.d.ts.map +1 -0
  56. package/esm/lib/hooks-schemas.js +238 -0
  57. package/esm/lib/hooks-schemas.js.map +1 -0
  58. package/esm/lib/hooks-types.d.ts +178 -0
  59. package/esm/lib/hooks-types.d.ts.map +1 -0
  60. package/esm/lib/hooks-types.js +45 -0
  61. package/esm/lib/hooks-types.js.map +1 -0
  62. package/esm/lib/model-result.d.ts +848 -12
  63. package/esm/lib/model-result.d.ts.map +1 -1
  64. package/esm/lib/model-result.js +4055 -410
  65. package/esm/lib/model-result.js.map +1 -1
  66. package/esm/lib/reusable-stream.d.ts +15 -0
  67. package/esm/lib/reusable-stream.d.ts.map +1 -1
  68. package/esm/lib/reusable-stream.js +25 -0
  69. package/esm/lib/reusable-stream.js.map +1 -1
  70. package/esm/lib/stream-transformers.d.ts +20 -0
  71. package/esm/lib/stream-transformers.d.ts.map +1 -1
  72. package/esm/lib/stream-transformers.js +45 -0
  73. package/esm/lib/stream-transformers.js.map +1 -1
  74. package/esm/lib/tool-check.d.ts +76 -0
  75. package/esm/lib/tool-check.d.ts.map +1 -0
  76. package/esm/lib/tool-check.js +241 -0
  77. package/esm/lib/tool-check.js.map +1 -0
  78. package/esm/lib/tool-concurrency.d.ts +42 -0
  79. package/esm/lib/tool-concurrency.d.ts.map +1 -0
  80. package/esm/lib/tool-concurrency.js +80 -0
  81. package/esm/lib/tool-concurrency.js.map +1 -0
  82. package/esm/lib/tool-context.d.ts +45 -1
  83. package/esm/lib/tool-context.d.ts.map +1 -1
  84. package/esm/lib/tool-context.js +60 -1
  85. package/esm/lib/tool-context.js.map +1 -1
  86. package/esm/lib/tool-executor.d.ts +52 -9
  87. package/esm/lib/tool-executor.d.ts.map +1 -1
  88. package/esm/lib/tool-executor.js +263 -21
  89. package/esm/lib/tool-executor.js.map +1 -1
  90. package/esm/lib/tool-orchestrator.d.ts.map +1 -1
  91. package/esm/lib/tool-orchestrator.js +87 -47
  92. package/esm/lib/tool-orchestrator.js.map +1 -1
  93. package/esm/lib/tool-task.d.ts +157 -0
  94. package/esm/lib/tool-task.d.ts.map +1 -0
  95. package/esm/lib/tool-task.js +206 -0
  96. package/esm/lib/tool-task.js.map +1 -0
  97. package/esm/lib/tool-types.d.ts +609 -58
  98. package/esm/lib/tool-types.d.ts.map +1 -1
  99. package/esm/lib/tool-types.js +72 -10
  100. package/esm/lib/tool-types.js.map +1 -1
  101. package/esm/lib/tool.d.ts +217 -25
  102. package/esm/lib/tool.d.ts.map +1 -1
  103. package/esm/lib/tool.js +210 -1
  104. package/esm/lib/tool.js.map +1 -1
  105. package/package.json +35 -3
@@ -3,9 +3,23 @@ import type { RequestOptions } from '@openrouter/sdk/lib/sdks';
3
3
  import type * as models from '@openrouter/sdk/models';
4
4
  import type { $ZodObject, $ZodShape } from 'zod/v4/core';
5
5
  import type { CallModelInput } from './async-params.js';
6
+ import type { DoomLoopOption, DoomLoopVerdict } from './doom-loop.js';
7
+ import type { HooksManager } from './hooks-manager.js';
8
+ import type { SessionUsageTotals } from './hooks-types.js';
6
9
  import type { StreamableOutputItem } from './stream-transformers.js';
7
10
  import type { ContextInput } from './tool-context.js';
8
- import type { ConversationState, InferToolEventsUnion, InferToolOutputsUnion, ParsedToolCall, ResponseStreamEvent, StateAccessor, StopWhen, Tool, ToolContextMapWithShared, ToolStreamEvent, TurnContext } from './tool-types.js';
11
+ import type { ConversationState, InferToolEventsUnion, InferToolOutputsUnion, ParsedToolCall, PendingAsyncTool, ResponseStreamEvent, StateAccessor, StopWhen, Tool, ToolContextMapWithShared, ToolStreamEvent, TurnContext } from './tool-types.js';
12
+ /**
13
+ * Default directive appended as a final user message on the forced final
14
+ * turn (`allowFinalResponse` defaulting to on, or explicitly `true`).
15
+ * Forbidding tools via `toolChoice: 'none'` alone is not enough: models
16
+ * that emit tool-call syntax as text (e.g. GLM) will attempt another call
17
+ * and leak it into `content` as unparsed `<tool_call>…` text unless they
18
+ * are told this is the final turn.
19
+ * Pass a non-empty string to `allowFinalResponse` to override the wording,
20
+ * or `''` to append no message at all (legacy behavior).
21
+ */
22
+ export declare const DEFAULT_FINAL_RESPONSE_DIRECTIVE = "You have reached the tool-use limit, and tools are no longer available. Do not attempt to call any more tools. Using the information you already have, write your final answer now.";
9
23
  export interface GetResponseOptions<TTools extends readonly Tool[], TShared extends Record<string, unknown> = Record<string, never>> {
10
24
  request: CallModelInput<TTools, TShared>;
11
25
  client: OpenRouterCore;
@@ -35,6 +49,72 @@ export interface GetResponseOptions<TTools extends readonly Tool[], TShared exte
35
49
  * a final user message.
36
50
  */
37
51
  allowFinalResponse?: boolean | string;
52
+ /**
53
+ * When true, always throw if the final response has an empty `output` array
54
+ * (legacy behavior). Default false: after at least one completed tool
55
+ * execution round, an empty final turn is retried once and then accepted
56
+ * so tool-terminal runs are not reported as failures.
57
+ */
58
+ strictFinalResponse?: boolean;
59
+ /** Hook system for lifecycle events */
60
+ hooks?: HooksManager;
61
+ /**
62
+ * Doom-loop detection: catch runs that repeat the same tool call with
63
+ * identical arguments (including repeated empty or unparseable calls) or
64
+ * emit the same text over and over, and respond with a graduated ladder
65
+ * (observe → steer → block → stop). `true` enables recommended defaults;
66
+ * an object tunes thresholds. Off by default.
67
+ */
68
+ doomLoop?: DoomLoopOption;
69
+ /**
70
+ * Cancel the whole run: stops the loop at the next boundary and aborts
71
+ * the in-flight API request/stream. See `CallModelInput.signal`.
72
+ */
73
+ signal?: AbortSignal;
74
+ /**
75
+ * Default per-tool execution deadline in milliseconds. A tool-level
76
+ * `timeoutMs` overrides it. See `BaseToolFunction.timeoutMs` for the
77
+ * semantics (bounds the round's wait, not the tool body).
78
+ */
79
+ toolTimeoutMs?: number;
80
+ /**
81
+ * Concurrency limits for tool execution. A bare number is shorthand for
82
+ * `{ round: n }`. `round` bounds simultaneous executions within a tool
83
+ * round (unbounded by default — matching existing behavior); `background`
84
+ * bounds detached background-tool work (default 16).
85
+ */
86
+ toolConcurrency?: number | {
87
+ round?: number;
88
+ background?: number;
89
+ };
90
+ /**
91
+ * Behavior for async tools (`tool.background` / `tool.deferred`).
92
+ */
93
+ asyncTools?: {
94
+ /**
95
+ * What to do when the run would end with background tasks in flight:
96
+ * - `'drain'` (default): wait (bounded by `drainTimeoutMs`) and give the
97
+ * model up to `maxDrainTurns` extra turns so the final answer
98
+ * incorporates late results;
99
+ * - `'detach'`: return immediately; tasks keep running and their results
100
+ * are dropped (persisted as `orphaned` when a StateAccessor exists);
101
+ * - `'cancel'`: abort in-flight tasks and finish.
102
+ */
103
+ onRunEnd?: 'drain' | 'detach' | 'cancel';
104
+ /** Bound on each end-of-run drain wait. Default 30_000. */
105
+ drainTimeoutMs?: number;
106
+ /** Max extra model turns granted to deliver drained results. Default 2. */
107
+ maxDrainTurns?: number;
108
+ /**
109
+ * Model-side check-ins: whether calling a long-running tool with a
110
+ * `taskId` answers status/logs/transcript instead of starting new work.
111
+ * Default true. When false, placeholders revert to "do not call this
112
+ * tool again" and the check schema is not advertised.
113
+ */
114
+ checkins?: boolean;
115
+ /** Max characters for the `transcript` check view. Default 20_000. */
116
+ maxTranscriptChars?: number;
117
+ };
38
118
  }
39
119
  /**
40
120
  * A wrapper around a streaming response that provides multiple consumption patterns.
@@ -66,7 +146,16 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
66
146
  private toolEventBroadcaster;
67
147
  private allToolExecutionRounds;
68
148
  private resolvedRequest;
149
+ private activeConsumedForcedToolChoiceKey;
150
+ private configuredForcedToolChoiceKey;
151
+ private configuredToolChoice;
152
+ private preparedEffectiveToolChoice;
153
+ private preparedToolChoiceCommit;
154
+ private dispatchedToolChoiceCommit;
155
+ private dispatchedCallerToolChoice;
69
156
  private pendingFreshItems;
157
+ private resumingFromClientTools;
158
+ private resumingFromAsyncTools;
70
159
  private stateAccessor;
71
160
  private currentState;
72
161
  private requireApprovalFn;
@@ -77,7 +166,42 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
77
166
  private initialStreamPipeStarted;
78
167
  private initialPipePromise;
79
168
  private contextStore;
169
+ private readonly hooksManager;
170
+ private sessionStartEmitted;
171
+ private sessionEndEmitted;
172
+ private readonly hookDeniedCalls;
173
+ private pendingModelCall;
174
+ private readonly sessionUsage;
175
+ private readonly doomLoopMonitor;
176
+ private doomLoopStop;
177
+ private pendingDoomLoopSteer;
178
+ private doomLoopRound;
179
+ private readonly doomLoopRoundDecisions;
180
+ private readonly doomLoopRoundKeyMaterial;
181
+ private doomLoopChain;
182
+ private pendingDoomLoopEscalation;
183
+ private readonly doomLoopEscalation;
184
+ private asyncToolRegistry;
185
+ private roundGate;
186
+ private backgroundPool;
187
+ private readonly perToolGates;
188
+ private readonly inflightToolControllers;
80
189
  constructor(options: GetResponseOptions<TTools, TShared>);
190
+ /** Lazily create the async tool registry. */
191
+ private ensureAsyncToolRegistry;
192
+ /** Per-tool concurrency gate, created on first use. */
193
+ private perToolGate;
194
+ /**
195
+ * Compose the per-call abort signal for one tool execution: the run
196
+ * signal plus a per-call controller the engine can abort independently
197
+ * (timeout race, cancelTask, cancel()). The tool deadline deliberately
198
+ * does NOT ride the signal here — the race in `executeSingleToolCall`
199
+ * owns exactly one timer (which aborts the controller), so the round's
200
+ * timeout resolution and the body's abort observation cannot disagree.
201
+ */
202
+ private composeToolSignal;
203
+ /** Abort every in-flight tool execution and background task. */
204
+ private abortAllToolWork;
81
205
  /**
82
206
  * Get or create the unified turn broadcaster (lazy initialization).
83
207
  * Broadcasts all API stream events, tool events, and turn delimiters across turns.
@@ -94,6 +218,12 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
94
218
  * Emits turn.start / turn.end delimiters around the stream events.
95
219
  */
96
220
  private pipeAndConsumeStream;
221
+ /**
222
+ * Resolve a tool's result `source` from its call name by looking it up in the
223
+ * configured tools. Used where the concrete tool reference isn't in scope
224
+ * (e.g. a rejected execution). Defaults to `'client'` when not found.
225
+ */
226
+ private toolSourceByName;
97
227
  /**
98
228
  * Push a tool result event to both the legacy tool event broadcaster
99
229
  * and the unified turn broadcaster.
@@ -114,6 +244,12 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
114
244
  * Only requires 'output' field and absence of readable stream behavior
115
245
  */
116
246
  private isNonStreamingResponse;
247
+ /**
248
+ * Materialize a betaResponsesSend result value into a completed response:
249
+ * streaming values are consumed to completion (through the turn
250
+ * broadcaster when one is attached), non-streaming values pass through.
251
+ */
252
+ private materializeResponse;
117
253
  /**
118
254
  * Get initial response from stream or cached final response.
119
255
  * Consumes the stream to completion if needed to extract the response.
@@ -149,6 +285,15 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
149
285
  * @returns True if interrupted and caller should exit, false to continue
150
286
  */
151
287
  private checkForInterruption;
288
+ /**
289
+ * Inject a user-role message into the conversation state and into the
290
+ * accumulated request input, so the next turn picks it up. Used by the
291
+ * Stop hook's `appendPrompt` to nudge the model without forcing a resume.
292
+ *
293
+ * This advances observable state (messages/input change) so the next
294
+ * iteration of the execution loop is not a no-op.
295
+ */
296
+ private injectAppendPromptMessage;
152
297
  /**
153
298
  * Check if stop conditions are met.
154
299
  * Returns true if execution should stop.
@@ -180,6 +325,309 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
180
325
  * are not classified as manual here.
181
326
  */
182
327
  private isManualToolCall;
328
+ /**
329
+ * Shared helper: execute a single tool and emit the full Pre/Post lifecycle
330
+ * hooks around it.
331
+ *
332
+ * Every code path that ultimately calls `executeTool()` for a user-visible
333
+ * tool call funnels through here so that PreToolUse/PostToolUse/
334
+ * PostToolUseFailure fire consistently — regardless of whether the tool was
335
+ * auto-executed, required approval, or was approved later.
336
+ *
337
+ * Return shape:
338
+ * - `parse_error`: `toolCall.arguments` was a raw JSON string the model
339
+ * failed to produce valid JSON for. The caller should use the prebuilt
340
+ * FunctionCallOutputItem and not execute the tool. No hooks fire.
341
+ * - `hook_blocked`: PreToolUse returned `block` (boolean true or a reason
342
+ * string). The caller should synthesize a denied result without invoking
343
+ * the tool. The FunctionCallOutputItem is prebuilt for convenience.
344
+ * - `execution`: The tool ran. `result` is the ToolExecutionResult.
345
+ * `effectiveToolCall` reflects any `mutatedInput` piped by PreToolUse.
346
+ */
347
+ private runToolWithHooks;
348
+ /**
349
+ * Advance the doom-loop round counter and declare the round's calls. Called
350
+ * at every execution-batch boundary (main tool round, auto-approve batch
351
+ * while pausing, approved-on-resume batch). Identical calls WITHIN one round
352
+ * are duplicates — one piece of loop evidence, one shared decision.
353
+ *
354
+ * `batch` is every call the round will make. A round's identity for one tool
355
+ * is the *set* of fingerprints it was called with, and that set has to be
356
+ * complete before any of the round's calls is scored: accumulating it call
357
+ * by call made a round that is a superset of the previous one transiently
358
+ * match it, blocking calls that represented real progress, with the outcome
359
+ * depending on emission order. Declaration is best-effort — a call whose key
360
+ * material is exempt or unhashable is simply left out, and the per-call
361
+ * fallback chain in `enqueueDoomLoopEvaluation` still governs identity at
362
+ * record time.
363
+ *
364
+ * Only calls that the round will actually CHECK are declared. A `loopKey`
365
+ * function is user code that may count or log, so it must not run for a call
366
+ * the detector never evaluates: manual tool calls (no `execute`, no
367
+ * `onToolCalled`) are handed to the caller and never recorded, and every
368
+ * execution path skips them with the same `isAutoResolvableTool` predicate
369
+ * used here. Their absence from the declared set is also correct on its own
370
+ * terms — they are not evidence, so they are not part of the round.
371
+ *
372
+ * INVARIANT — every declared member must eventually be recorded. A call
373
+ * that is declared but never reaches the doom-loop checkpoint is a phantom
374
+ * member of the round's identity: the tool's streak silently resets the
375
+ * moment the phantom stops being emitted (pinned by the "declared-but-
376
+ * never-recorded member" test in doom-loop-fanout.test.ts). The filters
377
+ * below therefore MIRROR every path that skips recording — unknown/manual
378
+ * tools (`isAutoResolvableTool`, also gating executeAutoApproveTools and
379
+ * the approval-resume loop), `hookDeniedCalls` (consumed before the
380
+ * checkpoint in executeSingleToolCall), and `loopKey`-exempt calls (early
381
+ * return in checkDoomLoopBeforeExecution; the same resolution is cached
382
+ * here so both sides agree). If you add a new short-circuit between
383
+ * declaration and the checkpoint — a pre-execution gate, a batch filter —
384
+ * it MUST be reflected here, or fan-out detection degrades silently for
385
+ * that tool rather than failing loudly.
386
+ *
387
+ * KNOWN RESIDUAL (documented, not mirrored): a per-request timeout or
388
+ * abort can cancel a call AFTER declaration but BEFORE its checkpoint,
389
+ * leaving a declared phantom for that round. Bounded and fail-safe: the
390
+ * round-set streak for that tool resets when the phantom stops recurring,
391
+ * while per-call streaks are unaffected — detection degrades to per-call
392
+ * for the affected tool rather than being lost. Un-declaring mid-round
393
+ * would mutate the round's identity while it is being scored, which is the
394
+ * incremental-set order-dependence this design exists to prevent.
395
+ */
396
+ private beginDoomLoopRound;
397
+ /**
398
+ * Evaluate one call against the doom-loop monitor, serialized on
399
+ * `doomLoopChain` so parallel executions record in the deterministic
400
+ * order their evaluations were ENQUEUED (the `.map()` over a round's
401
+ * calls runs synchronously to its first await, i.e. model-emission
402
+ * order) — not in hash-completion order.
403
+ *
404
+ * Applies the fallback invariant: detection must never take down or
405
+ * alter a run except via its defined actions. Unhashable key material
406
+ * (bigint, non-finite numbers, circular/too-deep structures from a
407
+ * computed loopKey) degrades to `fallbackKeyMaterial`; if THAT is also
408
+ * unhashable (parse-error path passes null), detection is skipped for
409
+ * the call and it proceeds normally.
410
+ *
411
+ * Returns the final decision for this call (`action: 'proceed'` when no
412
+ * rung fired or the call is exempt/skipped; `message` carries the
413
+ * verdict's explanation for block outputs). Side effects (steer queue,
414
+ * stop arm) are applied here so every caller shares them.
415
+ */
416
+ private enqueueDoomLoopEvaluation;
417
+ /**
418
+ * Doom-loop checkpoint for one tool call, run before PreToolUse/execution.
419
+ *
420
+ * Resolves the tool's `loopKey` declaration (function |
421
+ * false | absent — see {@link resolveLoopKeyMaterial}) and evaluates the
422
+ * call against the monitor. Ladder actions:
423
+ *
424
+ * - `observe` — hook already emitted, nothing else.
425
+ * - `steer` — queue a corrective user message for the next model turn.
426
+ * - `block` — return `blocked` so the caller synthesizes an error output
427
+ * without executing. The model sees the explanation in the tool result.
428
+ * - `stop` — arm `doomLoopStop`; ALSO blocks this call (executing a
429
+ * call the detector just condemned, then stopping, would be
430
+ * incoherent). The loop halts before the next model request.
431
+ */
432
+ private checkDoomLoopBeforeExecution;
433
+ /**
434
+ * Emit the DoomLoopDetected hook for a verdict and resolve the final
435
+ * action. Handlers may override per event (`overrideAction`), letting
436
+ * userland escalate or de-escalate without reconfiguring the ladder;
437
+ * the LAST override in the chain wins (matching PermissionRequest's
438
+ * last-wins convention). Non-blockable verdicts (text, server tools —
439
+ * the tokens are already emitted / the tool already ran) downgrade a
440
+ * 'block' override to 'observe'. An 'escalate' override is honored only
441
+ * when an escalation mechanism is configured with remaining budget;
442
+ * otherwise it downgrades to 'observe' (never silently to a stronger
443
+ * action).
444
+ */
445
+ private applyDoomLoopVerdict;
446
+ /**
447
+ * Apply the side effects of a resolved doom-loop action shared by every
448
+ * checkpoint: queue steer guidance, arm an escalation for the next
449
+ * request, or arm the stop verdict. 'block' has no shared side effect —
450
+ * the calling checkpoint synthesizes the blocked output itself.
451
+ */
452
+ private applyDoomLoopSideEffects;
453
+ /**
454
+ * Consume a pending escalation into ONE-TURN request overrides:
455
+ *
456
+ * - `model` — replace the request's model for this dispatch only (the
457
+ * base `resolvedRequest` is never mutated, so the following turn
458
+ * reverts automatically).
459
+ * - `advisor` — append an `openrouter:advisor` server tool (transcript
460
+ * forwarded, instructions describing the detected loop) and pin
461
+ * `toolChoice` to it via `allowed_tools`/`required`, so the stuck model
462
+ * must consult the advisor before doing anything else this turn.
463
+ *
464
+ * Burns one unit of the escalation budget (persisted via the monitor) at
465
+ * APPLICATION time — verdicts the engine never got to apply (run stopped,
466
+ * paused, or overridden) do not spend.
467
+ */
468
+ private takeDoomLoopEscalationOverrides;
469
+ /**
470
+ * Step-level doom-loop checkpoint on a fresh model response:
471
+ *
472
+ * 1. Text detectors — within-response token repetition + cross-step
473
+ * identical-text streak.
474
+ * 2. Server-tool fingerprints — server tools (web_search_call etc.) never
475
+ * pass through runToolWithHooks; their repetition is detected here,
476
+ * post-execution, from the echoed call fields on the output item.
477
+ * Post-execution ⇒ block is meaningless (allowBlock: false); the
478
+ * actions are observe/steer/stop.
479
+ *
480
+ * Runs where the loop already inspects each fresh response, so scripted
481
+ * transcripts exercise it deterministically.
482
+ */
483
+ private checkDoomLoopForResponse;
484
+ /**
485
+ * Queue steer guidance, deduplicating identical messages (one verdict can
486
+ * repeat across calls in a round; the model needs the guidance once).
487
+ */
488
+ private queueDoomLoopSteer;
489
+ /**
490
+ * Inject any queued doom-loop steer messages as a user message before the
491
+ * next model turn. Reuses the Stop-hook appendPrompt injection path, so
492
+ * state/messages advance observably. Called in-loop before each follow-up
493
+ * request AND before every pause-persist, so guidance queued right before
494
+ * a pause lands in the persisted conversation instead of being dropped.
495
+ */
496
+ private flushDoomLoopSteer;
497
+ /**
498
+ * Halt the run for an armed doom-loop stop verdict, keeping the persisted
499
+ * history well-formed: every executable `function_call` in the current
500
+ * response that has no output yet gets a synthesized halt-error output
501
+ * (persisted via the normal tool-result path), so a stateful resume never
502
+ * sends a dangling `function_call` (providers reject those with a 400).
503
+ *
504
+ * Used by every stop site: the pre-round break, the post-round break, the
505
+ * no-tools early return, the allow-final-response gate, and the approval-
506
+ * resume gate.
507
+ *
508
+ * @param resolvedCallIds - call ids that already have outputs this round.
509
+ */
510
+ private sealDoomLoopStop;
511
+ /**
512
+ * Build the per-emit context for lifecycle hook emits. Threads this run's
513
+ * session identity into `context.sessionId` on every emit, so a
514
+ * `HooksManager` instance shared across concurrent runs never leaks one
515
+ * run's id into another's handlers (the manager-level `setSessionId`
516
+ * default is a single mutable field and would be clobbered by the last
517
+ * run to start).
518
+ */
519
+ private hookEmitContext;
520
+ /**
521
+ * Emit PostToolUse / PostToolUseFailure for an async task at settlement —
522
+ * the point its final output actually exists. Round-synchronous tools
523
+ * emit these in runToolWithHooks; async invocations skip that emission
524
+ * (no output yet) and land here instead. `durationMs` is the task's
525
+ * start-to-settle wall clock (includes background-pool queue wait, which
526
+ * counts against the task's own timeout budget). Observation-only — a
527
+ * throwing hook must not break delivery.
528
+ */
529
+ private emitAsyncSettledHooks;
530
+ /**
531
+ * Emit SessionEnd exactly once, and only when a matching SessionStart
532
+ * actually succeeded. Safe to call from multiple teardown paths.
533
+ */
534
+ private emitSessionEndOnce;
535
+ /**
536
+ * Materialize the running session aggregate as an immutable
537
+ * `SessionUsageTotals`. `cost` is present only when at least one response
538
+ * reported it, matching the hook payload's optionality (a summed cost of
539
+ * `0` from cost-less responses would read as "free" rather than "unknown").
540
+ *
541
+ * Single source of truth for both `SessionEnd.totalUsage` and the public
542
+ * `getUsage()` accessor, so the two can never drift.
543
+ */
544
+ private snapshotSessionUsage;
545
+ /**
546
+ * Emit PostModelCall for a completed model response and fold its usage
547
+ * into the session aggregate. One emit per materialized response.
548
+ *
549
+ * Accumulation runs unconditionally — before the hooks short-circuit —
550
+ * because `getUsage()` surfaces these totals to callers who configured no
551
+ * hooks at all. Only the hook emit is gated on a `HooksManager`.
552
+ */
553
+ private emitPostModelCall;
554
+ /**
555
+ * Emit the parked initial/resume PostModelCall once its response has been
556
+ * materialized. No-ops when nothing is parked (e.g. the non-streaming
557
+ * branch already emitted). Safe to call from multiple materialization
558
+ * sites; the first wins.
559
+ */
560
+ private emitPendingModelCallOnce;
561
+ /**
562
+ * Emit the Stop hook when a stopWhen condition halts the loop, and decide
563
+ * whether the loop should resume.
564
+ *
565
+ * - `appendPrompt` values from all handlers are concatenated (newline
566
+ * separated) and injected as a user message. Honored independently of
567
+ * forceResume so a handler can nudge the next turn without resuming.
568
+ * - `forceResume` is honored if ANY handler returns it, capped at
569
+ * MAX_FORCE_RESUME_OVERRIDES consecutive overrides without tool
570
+ * progress (the caller resets its counter when a tool round or a fresh
571
+ * response lands).
572
+ *
573
+ * Returns 'resume' when the loop should continue, 'stop' otherwise.
574
+ */
575
+ private runStopHook;
576
+ /**
577
+ * Session teardown for the no-tools stream paths, which bypass
578
+ * executeToolsIfNeeded (the normal SessionEnd site). Materializes the
579
+ * parked model-call telemetry, emits SessionEnd once, and drains pending
580
+ * hook work. Never throws: teardown must not mask the stream's own outcome.
581
+ */
582
+ private finishHooksSessionForStream;
583
+ /**
584
+ * initStream wrapper for the streaming getters. initStream can throw
585
+ * after SessionStart was emitted (e.g. the initial API call fails), and
586
+ * the "Stream not initialized" guard can throw right after it — on both
587
+ * paths the hook session must still be torn down (SessionEnd + drain),
588
+ * otherwise Start/End handlers that treat the pair as a contract see a
589
+ * dangling Start. Teardown is emit-once, so the tools path (which runs
590
+ * its own teardown in executeToolsIfNeeded) is unaffected.
591
+ */
592
+ private initStreamGuarded;
593
+ /**
594
+ * Emit the PermissionRequest hook before the SDK blocks for user approval.
595
+ *
596
+ * Returns the hook's collective decision:
597
+ * - `allow`: the tool should proceed as if auto-approved (skip approval gate)
598
+ * - `deny`: the tool should NOT run; caller should produce a denied result
599
+ * - `ask_user`: fall through to the existing approval flow (the default)
600
+ *
601
+ * Last-wins when multiple handlers return conflicting decisions.
602
+ */
603
+ private emitPermissionRequest;
604
+ /**
605
+ * Run the UserPromptSubmit hook, supporting both string and structured
606
+ * inputs. If a handler returns a mutated prompt, the returned object
607
+ * applies the mutation back to the original input shape (string in =
608
+ * string out, message array in = message array out with the latest user
609
+ * text replaced).
610
+ *
611
+ * Throws if any handler rejects the prompt.
612
+ *
613
+ * Returns `undefined` when the hook does nothing, or when no usable prompt
614
+ * could be extracted from a structured input (handler is skipped and a
615
+ * one-time `console.warn` in dev builds explains why).
616
+ */
617
+ private maybeRunUserPromptSubmit;
618
+ /**
619
+ * Settle an async invocation on the approval/resume path, where results
620
+ * are accumulated as `UnsentToolResult`s rather than round outputs.
621
+ *
622
+ * - background: degrade to synchronous execution — the approval flow is
623
+ * already a discrete resume step, so there is no round to unblock; the
624
+ * work is awaited and its (validated) value becomes the unsent result.
625
+ * - defer: the external task HAS been started by `start()`; record the
626
+ * pending placeholder as the unsent result and track the task on
627
+ * `pendingAsyncTools` so `resumeToolResults` can deliver the real
628
+ * result later as an envelope.
629
+ */
630
+ private settleAsyncInvocationAsUnsent;
183
631
  /**
184
632
  * Execute tools that can auto-execute (don't require approval) in parallel.
185
633
  *
@@ -215,6 +663,56 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
215
663
  * @param pausedCalls - HITL tool calls whose `onToolCalled` returned `null`
216
664
  */
217
665
  private persistHitlPause;
666
+ /**
667
+ * Persist state when the loop stops due to unresolved manual (client-executed)
668
+ * tool calls — tools with neither `execute` nor `onToolCalled`.
669
+ *
670
+ * Mirrors `persistHitlPause` so callers can read the unresolved calls via
671
+ * `getPendingToolCalls()` / `getState()` after the loop ends. Uses the
672
+ * distinct status `awaiting_client_tools` so consumers can discriminate
673
+ * manual pauses from HITL pauses (`awaiting_hitl`).
674
+ *
675
+ * Resume behavior: `awaiting_client_tools` is intentionally NOT treated as a
676
+ * resumable status for `processApprovalDecisions` — manual tools are not
677
+ * approved/rejected via call IDs; the caller executes them externally and
678
+ * typically supplies `function_call_output` items as new input on the next
679
+ * `callModel`. The paused status and calls remain durable until that request
680
+ * produces a response; they are then cleared atomically with the response.
681
+ *
682
+ * Without a `StateAccessor` nothing is persisted (mirroring
683
+ * `persistHitlPause`): `getPendingToolCalls()` returns `[]` and the caller
684
+ * must read the unresolved `function_call` items off `getResponse().output`.
685
+ * Manual tools require a StateAccessor to be recoverable across processes.
686
+ *
687
+ * @param currentResponse - The response that produced the unresolved calls
688
+ * @param unresolvedCalls - Manual (or otherwise non-auto-resolvable) tool calls
689
+ */
690
+ private persistClientToolsPause;
691
+ /**
692
+ * Persist state when one or more deferred tools (`tool.deferred`) started
693
+ * durable external tasks this round. The placeholder outputs are already
694
+ * persisted (the round is fully paired), so — unlike HITL/manual pauses —
695
+ * nothing dangles; the pause exists purely because the model's next turn
696
+ * should wait for the real results.
697
+ *
698
+ * Mirrors `persistHitlPause` in shape. Status `awaiting_async_tools` is
699
+ * distinct from `awaiting_client_tools`: the tool DID execute; only its
700
+ * result is pending. Resumption happens via the deferred tool's
701
+ * `.resolve()` / `.fail()` / `.cancel()` or `resumeToolResults()`.
702
+ */
703
+ private persistAsyncToolPause;
704
+ /**
705
+ * Harvest background tasks that settled since the last flush and inject
706
+ * their outcomes into the conversation as `tool_task_result` envelope
707
+ * messages (user role). Runs at the same safe point as
708
+ * `flushDoomLoopSteer()` — after a fully-paired round, before the next
709
+ * request — because injecting between a dangling `function_call` and its
710
+ * future output would be invalid history.
711
+ *
712
+ * A delivery is forward progress: the text-repetition streak is reset so a
713
+ * model saying "still waiting…" between deliveries is not condemned.
714
+ */
715
+ private flushAsyncToolDeliveries;
218
716
  /**
219
717
  * Compute the `output` payload sent to the model for a successfully
220
718
  * settled tool execution. Routes through `toModelOutput` when the tool
@@ -235,7 +733,126 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
235
733
  * `pausedCalls` is non-empty rather than sending an incomplete set of
236
734
  * outputs back to the model.
237
735
  */
736
+ /**
737
+ * Execute one tool call for a round: resolve the tool, honor any pending
738
+ * PermissionRequest denial, wire preliminary-result broadcasting, and run
739
+ * the tool through the full Pre/Post lifecycle hooks. Returns a tagged
740
+ * outcome consumed by `executeToolRound`'s aggregation loop.
741
+ */
742
+ private executeSingleToolCall;
743
+ /**
744
+ * The `tool_timeout` execution outcome for a call whose deadline fired:
745
+ * the model receives an error output immediately (the abandoned body
746
+ * keeps running detached).
747
+ */
748
+ private buildToolTimeoutOutcome;
749
+ /**
750
+ * PermissionRequest-hook denial for a call, as a `hook_blocked` outcome —
751
+ * or null when the call was not denied. Consumes the entry so a later
752
+ * round with a reused id is not affected.
753
+ */
754
+ private consumeHookDenial;
755
+ /**
756
+ * Race a tool execution against its deadline. Returns `'timeout'` when the
757
+ * deadline fires first — the caller synthesizes the `tool_timeout` output —
758
+ * and aborts the body's controller so cooperative tools stop working. The
759
+ * timer is cleared on settle either way.
760
+ */
761
+ private raceToolDeadline;
238
762
  private executeToolRound;
763
+ /**
764
+ * Handle one async invocation from a round.
765
+ *
766
+ * Background: start the work under the background pool, race it against
767
+ * the grace window — settle in time and the call yields a plain
768
+ * synchronous output (no async machinery visible); otherwise emit the
769
+ * pending placeholder and let the registry track the task.
770
+ *
771
+ * Deferred: emit the placeholder and report the task for the pause path.
772
+ */
773
+ private handleAsyncInvocation;
774
+ /**
775
+ * Reject a `ctx.defer()` taskId already claimed by ANOTHER pending task.
776
+ * Task ids are the resolution key (resumeToolResults matches on them), so
777
+ * a duplicate would make one of the two calls unresolvable — the
778
+ * conversation would wait forever. Caller-supplied ids
779
+ * (`ctx.defer(ticket.id)`) carry no uniqueness guarantee; reject the
780
+ * collision at start time, when the tool can still see it.
781
+ * @returns the error output for the colliding call, or null when unique.
782
+ */
783
+ private rejectDuplicateTaskId;
784
+ /**
785
+ * A mutable binding between a tool call's run context and its (created
786
+ * later) ToolTask. `ctx.log()` entries before the task exists are
787
+ * buffered; `ctx.onMessage()` registrations are forwarded on bind.
788
+ */
789
+ private createRunBinding;
790
+ /**
791
+ * Run a background tool's work under the tool's own concurrency gate AND
792
+ * the background pool. The round path does not touch the per-tool gate
793
+ * for background lifecycles (the thunk had not run yet), so the gate is
794
+ * acquired here for the body's lifetime — `maxConcurrency` bounds
795
+ * executions of the BODY, wherever they run.
796
+ *
797
+ * Acquisition order: per-tool gate FIRST, then the pool. The narrow gate
798
+ * is where a saturated tool's excess calls queue — queuing them on the
799
+ * shared pool instead would let 16 waiters on one maxConcurrency:2 tool
800
+ * hold every global slot while only two bodies run, starving every other
801
+ * background tool. Order is deadlock-safe: only background bodies take
802
+ * the per-tool gate (the round path skips it for them), and every body
803
+ * acquires in this same fixed order.
804
+ *
805
+ * Queue wait counts against the task's own timeout (tracked by the
806
+ * registry); an abort observed before the slots free up short-circuits
807
+ * without running the body.
808
+ */
809
+ private runBackgroundWork;
810
+ /** Broadcast a `tool.async_started` event on the turn broadcaster. */
811
+ private broadcastAsyncStarted;
812
+ /** Broadcast a `tool.async_settled` event on the turn broadcaster. */
813
+ private broadcastAsyncSettled;
814
+ /**
815
+ * Build the pending placeholder `function_call_output` for an async tool
816
+ * call. Pairs the `function_call` immediately (providers 400 on unpaired
817
+ * calls in follow-up history); the real result arrives later as a
818
+ * `tool_task_result` envelope. The note steers the model: with check-ins
819
+ * enabled it points at the universal `task` tool; with check-ins
820
+ * disabled it forbids re-calling entirely.
821
+ */
822
+ private buildPendingPlaceholder;
823
+ /**
824
+ * True when the built-in task tool is active for this run. Mirrors
825
+ * `needsTaskTool` exactly: when a user tool claims the reserved name the
826
+ * built-in is not registered, so calls named "task" must NOT be
827
+ * intercepted — they belong to the user's tool.
828
+ */
829
+ private taskToolActive;
830
+ /**
831
+ * Answer a call to the universal `task` tool: resolve the taskId to its
832
+ * live registry task (or persisted pendingAsyncTools entry, post-restart)
833
+ * and its OWNING tool, then dispatch by action:
834
+ *
835
+ * - `check` (default): the owning tool's `check.execute` when declared
836
+ * (custom `params` validated against `check.schema`), else the SDK
837
+ * default status / logs / transcript views.
838
+ * - `steer`: deliver `message` to the run body's inbox.
839
+ * - `result`: the final result if settled, else the status view.
840
+ * - `cancel`: cancel the task.
841
+ */
842
+ private answerTaskToolCall;
843
+ /** `task` tool, action=cancel. */
844
+ private taskToolCancel;
845
+ /** `task` tool, action=steer. Returns [result, error?] for `answer`. */
846
+ private taskToolSteer;
847
+ /**
848
+ * `task` tool, action=check (and the status-view fallback for `result`
849
+ * on an unsettled task): the owning tool's `check.execute` when
850
+ * declared, else the SDK default views — live-task-backed when the task
851
+ * is in this process, persisted-state-backed post-restart.
852
+ */
853
+ private taskToolCheck;
854
+ /** Build the narrow ToolTaskHandle façade for check.execute handlers. */
855
+ private buildTaskHandle;
239
856
  /**
240
857
  * Resolve async functions for the current turn.
241
858
  * Updates the resolved request with turn-specific parameter values.
@@ -256,11 +873,54 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
256
873
  */
257
874
  private makeFollowupRequest;
258
875
  /**
259
- * Make a final no-tools request to coerce a text response after the loop
260
- * was halted by `stopWhen` mid-tool-call. Reuses the resolved request so
261
- * `instructions`, `model`, and other API fields ride along unchanged.
262
- * `tools`, `toolChoice`, and `parallelToolCalls` are stripped — the whole
263
- * point is to force a text turn. The caller is expected to have already
876
+ * Append a response's output items to the accumulated request input,
877
+ * exactly as `makeFollowupRequest` does before adding tool results. Used
878
+ * by the end-of-run drain, where envelope messages must land AFTER the
879
+ * final assistant output in the request.
880
+ */
881
+ private accumulateResponseIntoInput;
882
+ /**
883
+ * End-of-run handling for async background tasks still in flight (or
884
+ * settled but unharvested) when the loop is about to finalize.
885
+ *
886
+ * - `'drain'` (default): wait for tasks (bounded by `drainTimeoutMs`),
887
+ * inject their results, and give the model up to `maxDrainTurns` extra
888
+ * `toolChoice: 'none'` turns so the final answer incorporates them.
889
+ * - `'detach'`: return immediately; still-working tasks are marked
890
+ * `orphaned` on persisted state and their eventual results are dropped.
891
+ * - `'cancel'`: abort in-flight tasks and finish.
892
+ *
893
+ * @returns the (possibly replaced) final response.
894
+ */
895
+ private handleRunEndAsyncTasks;
896
+ /**
897
+ * Broadcast every unharvested settlement as dropped (no in-run delivery),
898
+ * mirror the terminal status onto persisted `pendingAsyncTools`, and
899
+ * append a terminal `tool_task_result` envelope to persisted history.
900
+ *
901
+ * The status mirror prevents a stuck resume: an entry persisted as
902
+ * `working` for a task that only ever settles in-memory (abortAll at
903
+ * drain exhaustion / run cancel) would pin a later cross-process resume
904
+ * to `awaiting_async_tools` forever. The envelope keeps the pending
905
+ * placeholder's "the result will be delivered" promise honest for
906
+ * PERSISTED conversations: the run is over (no more model dispatch), but
907
+ * the terminal outcome rides along on the next `callModel({ state })`
908
+ * instead of vanishing.
909
+ */
910
+ private dropSettledTasks;
911
+ /**
912
+ * `onRunEnd: 'detach'`: mark still-working tasks orphaned on persisted
913
+ * state (their eventual results are dropped) and drop unharvested
914
+ * settlements.
915
+ */
916
+ private detachWorkingTasks;
917
+ /**
918
+ * Make a final no-tool-calls request to coerce a text response after the
919
+ * loop was halted by `stopWhen` mid-tool-call. Reuses the resolved request
920
+ * so `instructions`, `model`, and other API fields ride along unchanged.
921
+ * `tools` stays in the request with `toolChoice: 'none'` — forbidding
922
+ * calls while keeping the prompt-cache prefix intact (stripping the tools
923
+ * block would bust the cache). The caller is expected to have already
264
924
  * executed the pending tool calls and to pass their outputs in
265
925
  * `toolOutputs` so every function_call in the input has a matching output.
266
926
  */
@@ -268,10 +928,51 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
268
928
  /**
269
929
  * Validate the final response has required fields.
270
930
  *
931
+ * Message prefixes (`Invalid final response: missing required fields`,
932
+ * `Invalid final response: empty or invalid output`) are load-bearing —
933
+ * callers and tests match on them. Detail is appended after an em dash;
934
+ * never reword the prefixes.
935
+ *
936
+ * The detail exists because the bare "empty or invalid output" message was
937
+ * routinely misread as "the model returned a tool call and validation
938
+ * rejected it" (issue #45). It does not: this is a pure array-length check,
939
+ * so a `function_call`-only output passes. Naming the actual defect —
940
+ * empty array vs. wrong type — stops that misdiagnosis at the error string.
941
+ *
271
942
  * @param response - The response to validate
943
+ * @param allowEmptyOutput - When true, tolerate an empty (but present) output array
272
944
  * @throws Error if response is missing required fields or has invalid output
273
945
  */
274
946
  private validateFinalResponse;
947
+ /**
948
+ * Re-send the current resolved request (same accumulated input) once.
949
+ * Used when a follow-up after tool execution returned an empty `output`.
950
+ *
951
+ * `toolChoice` is forced to `'none'` when tools are present (mirroring
952
+ * `makeFinalResponseRequest`) so the retry coerces a text turn: on the
953
+ * natural-loop-completion path the resolved request still carries tools,
954
+ * and a retry that emitted a fresh `function_call` would pass
955
+ * `validateFinalResponse` but never be executed — silently dropping a
956
+ * proposed tool call. Tools stay in the request to keep the prompt-cache
957
+ * prefix intact.
958
+ */
959
+ private retryCurrentRequest;
960
+ /**
961
+ * Build the RequestOptions for one API dispatch, composing the run-level
962
+ * cancellation signal (`options.signal`) with the caller's RequestOptions.
963
+ *
964
+ * The SDK only auto-wires `timeoutMs` into an abort signal when NO signal
965
+ * is present on the options — a supplied signal silently disables the
966
+ * timeout. Composing here keeps both: each request is bounded by
967
+ * whichever of {run signal, caller signal, per-request timeout} fires
968
+ * first. MUST be called once per dispatch — `AbortSignal.timeout()`
969
+ * starts counting at creation, so a cached composite would burn its
970
+ * budget across turns instead of per request.
971
+ *
972
+ * Also the per-request fail-fast checkpoint: an already-aborted run
973
+ * signal throws its abort reason here, before any network I/O.
974
+ */
975
+ private dispatchRequestOptions;
275
976
  /**
276
977
  * Resolve async functions in the request for a given turn context.
277
978
  * Extracts non-function fields and resolves any async parameter functions.
@@ -280,6 +981,33 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
280
981
  * @returns The resolved request without async functions
281
982
  */
282
983
  private resolveRequestForContext;
984
+ /**
985
+ * Record the freshly-resolved caller choice before deriving its wire value.
986
+ * An unforced turn clears the consumed key, re-arming the same forced value
987
+ * if it appears again later. A different forced key re-arms immediately.
988
+ */
989
+ private applyResolvedForcedToolChoicePolicy;
990
+ /**
991
+ * Derive the caller-configured tool choice for the active logical run.
992
+ * Engine-owned one-turn overrides (`none` for finalization, or a pinned
993
+ * doom-loop advisor) are applied after this helper and therefore still win.
994
+ */
995
+ private applyForcedToolChoicePolicy;
996
+ /**
997
+ * Bind the prepared caller-policy transition to the actual wire request.
998
+ * Engine-owned overrides (for example a forced advisor consult) deliberately
999
+ * produce a different semantic key and therefore cannot commit caller state.
1000
+ */
1001
+ private beginToolChoiceDispatch;
1002
+ /**
1003
+ * Commit only after the prepared caller choice reached the wire and a
1004
+ * response was materialized. A pause before dispatch leaves persisted state
1005
+ * untouched; a forced choice is consumed only when that response called a
1006
+ * tool.
1007
+ */
1008
+ private commitToolChoiceDispatch;
1009
+ /** Clear all runtime and persisted policy state for a terminal run exit. */
1010
+ private resetForcedToolChoicePolicy;
283
1011
  /**
284
1012
  * Apply `onResponseReceived` hooks to the freshly-supplied input items
285
1013
  * only, without re-hooking historical items that live in
@@ -350,6 +1078,10 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
350
1078
  * Get the complete response object including usage information.
351
1079
  * This will consume the stream until completion and execute any tools.
352
1080
  * Returns the full OpenResponsesResult with usage data (inputTokens, outputTokens, cachedTokens, etc.)
1081
+ *
1082
+ * Note: in a multi-round tool loop this is the **final** round's response
1083
+ * only — its `usage` block covers that one generation, not the run. For
1084
+ * aggregate totals across every model call, use `getUsage()`.
353
1085
  */
354
1086
  getResponse(): Promise<models.OpenResponsesResult>;
355
1087
  /**
@@ -377,6 +1109,17 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
377
1109
  * - file_search_call: File search operations
378
1110
  * - image_generation_call: Image generation operations
379
1111
  * - function_call_output: Results from executed tools
1112
+ *
1113
+ * This stream carries **output items only** — no usage or response-level
1114
+ * metadata. The `response.completed` events (which hold each round's usage
1115
+ * block) are not surfaced here, and `getResponse()` afterwards returns only
1116
+ * the *final* round's response, so a multi-round tool loop's `tool_calls`
1117
+ * generations are not accounted for by either. For usage, reach for:
1118
+ * - `getUsage()` — aggregate token/cost totals across every round
1119
+ * - the `PostModelCall` hook — one emit per model call, with
1120
+ * `turnType`/`turnNumber` and that call's own `usage`
1121
+ * - `getFullResponsesStream()` — raw events including each round's
1122
+ * `response.completed`, and therefore its per-round usage block
380
1123
  */
381
1124
  getItemsStream(): AsyncIterableIterator<StreamableOutputItem<TTools>>;
382
1125
  /**
@@ -448,15 +1191,66 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
448
1191
  */
449
1192
  getContextUpdates(): AsyncGenerator<ToolContextMapWithShared<TTools, TShared>>;
450
1193
  /**
451
- * Cancel the underlying stream and all consumers
1194
+ * Cancel the underlying stream and all consumers, and abort in-flight
1195
+ * tool executions and background tasks (their `ctx.signal` fires).
1196
+ *
1197
+ * Note: prior releases only cancelled the stream; aborting tool work is
1198
+ * new with async tool support.
452
1199
  */
453
1200
  cancel(): Promise<void>;
454
1201
  /**
455
- * Check if the conversation requires human input to continue.
456
- * Returns true when the conversation is paused waiting for caller-supplied
457
- * decisions — either approval/rejection (`awaiting_approval`) or HITL tool
458
- * resume (`awaiting_hitl`). Also returns true whenever `pendingToolCalls`
459
- * is populated regardless of status.
1202
+ * Snapshot of every async tool task tracked by this run (background and
1203
+ * deferred), including settled ones.
1204
+ */
1205
+ getAsyncTasks(): PendingAsyncTool[];
1206
+ /**
1207
+ * Cancel one in-process async task by its task id. Background tasks abort
1208
+ * their `ctx.signal`; the model receives a `status: 'cancelled'` envelope
1209
+ * at the next delivery point so it is not left waiting forever.
1210
+ *
1211
+ * For deferred tasks in a paused (possibly other-process) conversation,
1212
+ * use the deferred tool's `.cancel()` method instead.
1213
+ *
1214
+ * @returns true when a working task was found and cancelled.
1215
+ */
1216
+ cancelTask(taskId: string, reason?: string): boolean;
1217
+ /**
1218
+ * Send a steering message to a running in-process task. Delivered to the
1219
+ * run body's `ctx.onMessage` handler (queued until one registers). Agent
1220
+ * tools (`tool.agent()`) auto-forward messages into the child
1221
+ * conversation as user messages at the child's next turn boundary.
1222
+ *
1223
+ * Throws for deferred tasks — their work runs in an external system;
1224
+ * steer it there.
1225
+ *
1226
+ * @returns true when a working task received (or queued) the message.
1227
+ */
1228
+ sendToTask(taskId: string, message: unknown): boolean;
1229
+ /**
1230
+ * Queue a user-role message for injection into this run's conversation at
1231
+ * the next safe turn boundary (the same mechanism doom-loop steer
1232
+ * guidance uses — never between a dangling `function_call` and its
1233
+ * output). The primary consumer is `tool.agent()`, which forwards
1234
+ * steering messages into child conversations through this.
1235
+ *
1236
+ * Delivery requires a next dispatch: boundaries are post-tool-round
1237
+ * follow-ups, pause-persists, and end-of-run drain turns. A message
1238
+ * queued while the run is composing its FINAL text response (no further
1239
+ * dispatch of any kind) has no boundary left and is not delivered —
1240
+ * steer while the run still has work in flight.
1241
+ */
1242
+ queueUserMessage(text: string): void;
1243
+ /**
1244
+ * Check if the conversation requires human/client input to continue.
1245
+ * Returns true when the conversation is paused waiting on the CALLER —
1246
+ * approval/rejection (`awaiting_approval`), HITL tool resume
1247
+ * (`awaiting_hitl`), client-executed manual tools
1248
+ * (`awaiting_client_tools`), or an external async task resolution
1249
+ * (`awaiting_async_tools`, cleared via the deferred tool's `.resolve()` /
1250
+ * `.fail()` / `.cancel()` or `resumeToolResults()` — NOT via
1251
+ * `approveToolCalls`). Also returns true whenever `pendingToolCalls` is
1252
+ * populated regardless of status. To branch on the pause KIND, read
1253
+ * `(await getState()).status` instead.
460
1254
  */
461
1255
  requiresApproval(): Promise<boolean>;
462
1256
  /**
@@ -471,5 +1265,47 @@ export declare class ModelResult<TTools extends readonly Tool[], TShared extends
471
1265
  * To resume a conversation, use the StateAccessor pattern.
472
1266
  */
473
1267
  getState(): Promise<ConversationState<TTools>>;
1268
+ /**
1269
+ * The doom-loop verdict that stopped this run, or null when the run was
1270
+ * not stopped by doom-loop detection (including when detection is off).
1271
+ * Resolves after the tool loop settles. For per-event observation use the
1272
+ * `DoomLoopDetected` hook.
1273
+ */
1274
+ getDoomLoopVerdict(): Promise<DoomLoopVerdict | null>;
1275
+ /**
1276
+ * Aggregate token/cost usage across **every** model call this run made —
1277
+ * the initial request, each tool-round follow-up, the empty-final retry,
1278
+ * the `allowFinalResponse` final turn, and approval-resume requests.
1279
+ *
1280
+ * This is the pull-based counterpart to the `SessionEnd.totalUsage` hook
1281
+ * payload (same `SessionUsageTotals` shape, same numbers) and exists
1282
+ * because `getResponse()` returns only the **final** round's response: in a
1283
+ * multi-round tool loop the `tool_calls` generations' tokens are otherwise
1284
+ * unreachable, and `getItemsStream()` carries output items only — no
1285
+ * `response.completed` usage block.
1286
+ *
1287
+ * Gates on run completion the same way `getResponse()` does, so totals are
1288
+ * final whether you await it directly, after `getResponse()`, or after
1289
+ * consuming any of the streaming getters — except on an approval-resumed
1290
+ * run, where reading usage never advances the tool loop: the resume
1291
+ * request's own generation is awaited and counted, but no further rounds
1292
+ * are driven, so totals can be mid-run. Await `getResponse()`/`getText()`
1293
+ * first for final totals there. On a run that paused (approval / HITL) or
1294
+ * failed, it returns the totals accrued so far — `modelCalls: 0` with
1295
+ * zeroed tokens when no model call ever completed.
1296
+ *
1297
+ * Unlike `getResponse()`, this never rejects: a failed run still consumed
1298
+ * tokens, and cost accounting typically runs in a `finally`/`catch` where a
1299
+ * second throw would mask the run's original error. Await the run itself
1300
+ * (e.g. `getResponse()`) if you need to observe the failure.
1301
+ *
1302
+ * `cost` is present only when the server reported cost accounting for at
1303
+ * least one call; a `0` would be indistinguishable from "free".
1304
+ *
1305
+ * For per-call granularity use the `PostModelCall` hook (one emit per model
1306
+ * call, with `turnType`/`turnNumber`) or `getFullResponsesStream()`, whose
1307
+ * `response.completed` events carry each round's own usage block.
1308
+ */
1309
+ getUsage(): Promise<SessionUsageTotals>;
474
1310
  }
475
1311
  //# sourceMappingURL=model-result.d.ts.map