@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.
- package/README.md +728 -19
- package/esm/index.d.ts +24 -6
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +20 -5
- package/esm/index.js.map +1 -1
- package/esm/inner-loop/call-model.d.ts +8 -5
- package/esm/inner-loop/call-model.d.ts.map +1 -1
- package/esm/inner-loop/call-model.js +49 -40
- package/esm/inner-loop/call-model.js.map +1 -1
- package/esm/inner-loop/resume-tool-results.d.ts +128 -0
- package/esm/inner-loop/resume-tool-results.d.ts.map +1 -0
- package/esm/inner-loop/resume-tool-results.js +244 -0
- package/esm/inner-loop/resume-tool-results.js.map +1 -0
- package/esm/lib/agent-tool.d.ts +92 -0
- package/esm/lib/agent-tool.d.ts.map +1 -0
- package/esm/lib/agent-tool.js +211 -0
- package/esm/lib/agent-tool.js.map +1 -0
- package/esm/lib/async-params.d.ts +107 -6
- package/esm/lib/async-params.d.ts.map +1 -1
- package/esm/lib/async-params.js +8 -1
- package/esm/lib/async-params.js.map +1 -1
- package/esm/lib/async-tool-registry.d.ts +134 -0
- package/esm/lib/async-tool-registry.d.ts.map +1 -0
- package/esm/lib/async-tool-registry.js +352 -0
- package/esm/lib/async-tool-registry.js.map +1 -0
- package/esm/lib/async-tools.d.ts +23 -0
- package/esm/lib/async-tools.d.ts.map +1 -0
- package/esm/lib/async-tools.js +19 -0
- package/esm/lib/async-tools.js.map +1 -0
- package/esm/lib/conversation-state.d.ts +45 -1
- package/esm/lib/conversation-state.d.ts.map +1 -1
- package/esm/lib/conversation-state.js +119 -0
- package/esm/lib/conversation-state.js.map +1 -1
- package/esm/lib/doom-loop.d.ts +533 -0
- package/esm/lib/doom-loop.d.ts.map +1 -0
- package/esm/lib/doom-loop.js +1221 -0
- package/esm/lib/doom-loop.js.map +1 -0
- package/esm/lib/hooks-emit.d.ts +50 -0
- package/esm/lib/hooks-emit.d.ts.map +1 -0
- package/esm/lib/hooks-emit.js +307 -0
- package/esm/lib/hooks-emit.js.map +1 -0
- package/esm/lib/hooks-manager.d.ts +141 -0
- package/esm/lib/hooks-manager.d.ts.map +1 -0
- package/esm/lib/hooks-manager.js +282 -0
- package/esm/lib/hooks-manager.js.map +1 -0
- package/esm/lib/hooks-matchers.d.ts +12 -0
- package/esm/lib/hooks-matchers.d.ts.map +1 -0
- package/esm/lib/hooks-matchers.js +26 -0
- package/esm/lib/hooks-matchers.js.map +1 -0
- package/esm/lib/hooks-resolve.d.ts +17 -0
- package/esm/lib/hooks-resolve.d.ts.map +1 -0
- package/esm/lib/hooks-resolve.js +42 -0
- package/esm/lib/hooks-resolve.js.map +1 -0
- package/esm/lib/hooks-schemas.d.ts +221 -0
- package/esm/lib/hooks-schemas.d.ts.map +1 -0
- package/esm/lib/hooks-schemas.js +238 -0
- package/esm/lib/hooks-schemas.js.map +1 -0
- package/esm/lib/hooks-types.d.ts +178 -0
- package/esm/lib/hooks-types.d.ts.map +1 -0
- package/esm/lib/hooks-types.js +45 -0
- package/esm/lib/hooks-types.js.map +1 -0
- package/esm/lib/model-result.d.ts +848 -12
- package/esm/lib/model-result.d.ts.map +1 -1
- package/esm/lib/model-result.js +4055 -410
- package/esm/lib/model-result.js.map +1 -1
- package/esm/lib/reusable-stream.d.ts +15 -0
- package/esm/lib/reusable-stream.d.ts.map +1 -1
- package/esm/lib/reusable-stream.js +25 -0
- package/esm/lib/reusable-stream.js.map +1 -1
- package/esm/lib/stream-transformers.d.ts +20 -0
- package/esm/lib/stream-transformers.d.ts.map +1 -1
- package/esm/lib/stream-transformers.js +45 -0
- package/esm/lib/stream-transformers.js.map +1 -1
- package/esm/lib/tool-check.d.ts +76 -0
- package/esm/lib/tool-check.d.ts.map +1 -0
- package/esm/lib/tool-check.js +241 -0
- package/esm/lib/tool-check.js.map +1 -0
- package/esm/lib/tool-concurrency.d.ts +42 -0
- package/esm/lib/tool-concurrency.d.ts.map +1 -0
- package/esm/lib/tool-concurrency.js +80 -0
- package/esm/lib/tool-concurrency.js.map +1 -0
- package/esm/lib/tool-context.d.ts +45 -1
- package/esm/lib/tool-context.d.ts.map +1 -1
- package/esm/lib/tool-context.js +60 -1
- package/esm/lib/tool-context.js.map +1 -1
- package/esm/lib/tool-executor.d.ts +52 -9
- package/esm/lib/tool-executor.d.ts.map +1 -1
- package/esm/lib/tool-executor.js +263 -21
- package/esm/lib/tool-executor.js.map +1 -1
- package/esm/lib/tool-orchestrator.d.ts.map +1 -1
- package/esm/lib/tool-orchestrator.js +87 -47
- package/esm/lib/tool-orchestrator.js.map +1 -1
- package/esm/lib/tool-task.d.ts +157 -0
- package/esm/lib/tool-task.d.ts.map +1 -0
- package/esm/lib/tool-task.js +206 -0
- package/esm/lib/tool-task.js.map +1 -0
- package/esm/lib/tool-types.d.ts +609 -58
- package/esm/lib/tool-types.d.ts.map +1 -1
- package/esm/lib/tool-types.js +72 -10
- package/esm/lib/tool-types.js.map +1 -1
- package/esm/lib/tool.d.ts +217 -25
- package/esm/lib/tool.d.ts.map +1 -1
- package/esm/lib/tool.js +210 -1
- package/esm/lib/tool.js.map +1 -1
- 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
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
|
|
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
|
-
*
|
|
456
|
-
*
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|