@openrouter/agent 0.3.2 → 0.5.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/esm/index.d.ts +7 -5
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +4 -2
- package/esm/index.js.map +1 -1
- package/esm/lib/conversation-state.d.ts +5 -0
- package/esm/lib/conversation-state.d.ts.map +1 -1
- package/esm/lib/conversation-state.js +17 -5
- package/esm/lib/conversation-state.js.map +1 -1
- package/esm/lib/model-result.d.ts +91 -7
- package/esm/lib/model-result.d.ts.map +1 -1
- package/esm/lib/model-result.js +483 -92
- package/esm/lib/model-result.js.map +1 -1
- package/esm/lib/next-turn-params.d.ts.map +1 -1
- package/esm/lib/next-turn-params.js +3 -1
- package/esm/lib/next-turn-params.js.map +1 -1
- package/esm/lib/stream-transformers.d.ts +84 -5
- package/esm/lib/stream-transformers.d.ts.map +1 -1
- package/esm/lib/stream-transformers.js +8 -15
- package/esm/lib/stream-transformers.js.map +1 -1
- package/esm/lib/stream-type-guards.d.ts +29 -0
- package/esm/lib/stream-type-guards.d.ts.map +1 -1
- package/esm/lib/stream-type-guards.js +37 -0
- package/esm/lib/stream-type-guards.js.map +1 -1
- package/esm/lib/tool-executor.d.ts +48 -9
- package/esm/lib/tool-executor.d.ts.map +1 -1
- package/esm/lib/tool-executor.js +257 -14
- package/esm/lib/tool-executor.js.map +1 -1
- package/esm/lib/tool-orchestrator.js +6 -6
- package/esm/lib/tool-orchestrator.js.map +1 -1
- package/esm/lib/tool-types.d.ts +173 -8
- package/esm/lib/tool-types.d.ts.map +1 -1
- package/esm/lib/tool-types.js +53 -2
- package/esm/lib/tool-types.js.map +1 -1
- package/esm/lib/tool.d.ts +58 -1
- package/esm/lib/tool.d.ts.map +1 -1
- package/esm/lib/tool.js +65 -0
- package/esm/lib/tool.js.map +1 -1
- package/esm/openrouter.d.ts +10 -4
- package/esm/openrouter.d.ts.map +1 -1
- package/esm/openrouter.js +41 -2
- package/esm/openrouter.js.map +1 -1
- package/package.json +9 -15
package/esm/lib/model-result.js
CHANGED
|
@@ -5,32 +5,48 @@ import { applyNextTurnParamsToRequest, executeNextTurnParamsFunctions, } from '.
|
|
|
5
5
|
import { ReusableReadableStream } from './reusable-stream.js';
|
|
6
6
|
import { isStopConditionMet, stepCountIs } from './stop-conditions.js';
|
|
7
7
|
import { buildItemsStream, buildResponsesMessageStream, buildToolCallStream, consumeStreamForCompletion, extractReasoningDeltas, extractResponsesMessageFromResponse, extractTextDeltas, extractTextFromResponse, extractToolCallsFromResponse, extractToolDeltas, itemsStreamHandlers, streamTerminationEvents, } from './stream-transformers.js';
|
|
8
|
-
import { hasTypeProperty, isFunctionCallItem, isOutputTextDeltaEvent, isReasoningDeltaEvent, isResponseCompletedEvent, isResponseFailedEvent, isResponseIncompleteEvent, } from './stream-type-guards.js';
|
|
8
|
+
import { hasTypeProperty, isFunctionCallItem, isFunctionCallOutputItem, isOutputTextDeltaEvent, isReasoningDeltaEvent, isResponseCompletedEvent, isResponseFailedEvent, isResponseIncompleteEvent, isServerToolResultItem, } from './stream-type-guards.js';
|
|
9
9
|
import { resolveContext, ToolContextStore } from './tool-context.js';
|
|
10
10
|
import { ToolEventBroadcaster } from './tool-event-broadcaster.js';
|
|
11
|
-
import { executeTool } from './tool-executor.js';
|
|
12
|
-
import {
|
|
11
|
+
import { applyOnResponseReceivedHooks, executeTool } from './tool-executor.js';
|
|
12
|
+
import { isAutoResolvableTool, isClientTool, isServerTool, isToolCallOutputEvent, } from './tool-types.js';
|
|
13
13
|
/**
|
|
14
14
|
* Default maximum number of tool execution steps if no stopWhen is specified.
|
|
15
15
|
* This prevents infinite loops in tool execution.
|
|
16
16
|
*/
|
|
17
17
|
const DEFAULT_MAX_STEPS = 5;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
|
|
19
|
+
* Typeguard for plain-object records (non-null, non-array).
|
|
20
|
+
*/
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Human-readable label for a value that failed the `isRecord` check. Used
|
|
26
|
+
* exclusively to make `toModelOutput` misuse errors specific.
|
|
27
|
+
*/
|
|
28
|
+
function describeNonRecord(value) {
|
|
29
|
+
if (value === null) {
|
|
30
|
+
return 'null';
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return 'array';
|
|
34
|
+
}
|
|
35
|
+
return typeof value;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Type guard for stream event responses
|
|
39
|
+
* Checks constructor name and readable stream behavior
|
|
21
40
|
*/
|
|
22
41
|
function isEventStream(value) {
|
|
23
42
|
if (value === null || typeof value !== 'object') {
|
|
24
43
|
return false;
|
|
25
44
|
}
|
|
26
|
-
|
|
27
|
-
const constructorName = Object.getPrototypeOf(value)?.constructor?.name;
|
|
28
|
-
if (constructorName === 'EventStream') {
|
|
45
|
+
if (typeof ReadableStream !== 'undefined' && value instanceof ReadableStream) {
|
|
29
46
|
return true;
|
|
30
47
|
}
|
|
31
|
-
// Fallback: check for toReadableStream method (may be on prototype)
|
|
32
48
|
const maybeStream = value;
|
|
33
|
-
return typeof maybeStream.
|
|
49
|
+
return typeof maybeStream.getReader === 'function';
|
|
34
50
|
}
|
|
35
51
|
/**
|
|
36
52
|
* A wrapper around a streaming response that provides multiple consumption patterns.
|
|
@@ -236,13 +252,10 @@ export class ModelResult {
|
|
|
236
252
|
}
|
|
237
253
|
/**
|
|
238
254
|
* Type guard to check if a value is a non-streaming response
|
|
239
|
-
* Only requires 'output' field and absence of
|
|
255
|
+
* Only requires 'output' field and absence of readable stream behavior
|
|
240
256
|
*/
|
|
241
257
|
isNonStreamingResponse(value) {
|
|
242
|
-
return (value !== null &&
|
|
243
|
-
typeof value === 'object' &&
|
|
244
|
-
'output' in value &&
|
|
245
|
-
!('toReadableStream' in value));
|
|
258
|
+
return (value !== null && typeof value === 'object' && 'output' in value && !isEventStream(value));
|
|
246
259
|
}
|
|
247
260
|
// =========================================================================
|
|
248
261
|
// Extracted Helper Methods for executeToolsIfNeeded
|
|
@@ -350,17 +363,23 @@ export class ModelResult {
|
|
|
350
363
|
: [
|
|
351
364
|
stopWhen,
|
|
352
365
|
];
|
|
366
|
+
const isFunctionCallOutput = (tr) => tr.type === 'function_call_output';
|
|
367
|
+
const isServerToolResult = (tr) => tr.type !== 'function_call_output';
|
|
353
368
|
return isStopConditionMet({
|
|
354
369
|
stopConditions,
|
|
355
370
|
steps: this.allToolExecutionRounds.map((round) => ({
|
|
356
371
|
stepType: 'continue',
|
|
357
372
|
text: extractTextFromResponse(round.response),
|
|
358
373
|
toolCalls: round.toolCalls,
|
|
359
|
-
|
|
374
|
+
// `toolResults` is client-tool-centric; server-tool output items are
|
|
375
|
+
// surfaced on `serverToolResults` so stop conditions can react to
|
|
376
|
+
// either class of result.
|
|
377
|
+
toolResults: round.toolResults.filter(isFunctionCallOutput).map((tr) => ({
|
|
360
378
|
toolCallId: tr.callId,
|
|
361
379
|
toolName: round.toolCalls.find((tc) => tc.id === tr.callId)?.name ?? '',
|
|
362
380
|
result: typeof tr.output === 'string' ? JSON.parse(tr.output) : tr.output,
|
|
363
381
|
})),
|
|
382
|
+
serverToolResults: round.toolResults.filter(isServerToolResult),
|
|
364
383
|
response: round.response,
|
|
365
384
|
usage: round.response.usage,
|
|
366
385
|
finishReason: undefined,
|
|
@@ -368,21 +387,32 @@ export class ModelResult {
|
|
|
368
387
|
});
|
|
369
388
|
}
|
|
370
389
|
/**
|
|
371
|
-
* Check if any tool calls
|
|
390
|
+
* Check if any tool calls can be auto-resolved in the current turn.
|
|
372
391
|
* Used to determine if automatic tool execution should be attempted.
|
|
373
392
|
*
|
|
393
|
+
* A tool call is auto-resolvable if its tool has either an `execute` function
|
|
394
|
+
* (regular or generator) or an `onToolCalled` hook (HITL). HITL tools are
|
|
395
|
+
* included here because their hook fires before the model's follow-up request,
|
|
396
|
+
* even when the hook ultimately decides to pause by returning `null`.
|
|
397
|
+
*
|
|
374
398
|
* @param toolCalls - The tool calls to check
|
|
375
|
-
* @returns True if at least one tool call
|
|
399
|
+
* @returns True if at least one tool call is auto-resolvable
|
|
376
400
|
*/
|
|
377
401
|
hasExecutableToolCalls(toolCalls) {
|
|
378
402
|
return toolCalls.some((toolCall) => {
|
|
379
|
-
const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
|
|
380
|
-
return tool &&
|
|
403
|
+
const tool = this.options.tools?.find((t) => isClientTool(t) && t.function.name === toolCall.name);
|
|
404
|
+
return tool && isAutoResolvableTool(tool);
|
|
381
405
|
});
|
|
382
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* A manual tool call is one whose tool has neither an `execute` function nor
|
|
409
|
+
* an `onToolCalled` hook — i.e. the caller is expected to produce the output
|
|
410
|
+
* externally. HITL tools are auto-resolvable even when they pause, so they
|
|
411
|
+
* are not classified as manual here.
|
|
412
|
+
*/
|
|
383
413
|
isManualToolCall(item) {
|
|
384
|
-
const tool = this.options.tools?.find((t) => t.function.name === item.name);
|
|
385
|
-
return !!tool && !
|
|
414
|
+
const tool = this.options.tools?.find((t) => isClientTool(t) && t.function.name === item.name);
|
|
415
|
+
return !!tool && !isAutoResolvableTool(tool);
|
|
386
416
|
}
|
|
387
417
|
/**
|
|
388
418
|
* Execute tools that can auto-execute (don't require approval) in parallel.
|
|
@@ -393,11 +423,15 @@ export class ModelResult {
|
|
|
393
423
|
*/
|
|
394
424
|
async executeAutoApproveTools(toolCalls, turnContext) {
|
|
395
425
|
const toolCallPromises = toolCalls.map(async (tc) => {
|
|
396
|
-
const tool = this.options.tools?.find((t) => t.function.name === tc.name);
|
|
397
|
-
if (!tool || !
|
|
426
|
+
const tool = this.options.tools?.find((t) => isClientTool(t) && t.function.name === tc.name);
|
|
427
|
+
if (!tool || !isAutoResolvableTool(tool)) {
|
|
398
428
|
return null;
|
|
399
429
|
}
|
|
400
430
|
const result = await executeTool(tool, tc, turnContext, undefined, this.contextStore ?? undefined, this.options.sharedContextSchema);
|
|
431
|
+
if (result === null) {
|
|
432
|
+
// HITL tool paused — no unsent result for this call in this round
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
401
435
|
if (result.error) {
|
|
402
436
|
return createRejectedResult(tc.id, String(tc.name), result.error.message);
|
|
403
437
|
}
|
|
@@ -464,18 +498,81 @@ export class ModelResult {
|
|
|
464
498
|
this.finalResponse = currentResponse;
|
|
465
499
|
return true; // Pause for approval
|
|
466
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Persist state when one or more HITL tools paused during a round.
|
|
503
|
+
*
|
|
504
|
+
* Mirrors `handleApprovalCheck` so paused HITL calls are surfaced through
|
|
505
|
+
* `pendingToolCalls` (visible via `getPendingToolCalls()` / `getState()`).
|
|
506
|
+
* Sets the status to `awaiting_hitl` so the caller can discriminate HITL
|
|
507
|
+
* pauses from approval pauses.
|
|
508
|
+
*
|
|
509
|
+
* Already-executed results from the same round are persisted on the turn's
|
|
510
|
+
* message history via `saveToolResultsToState` (called by the outer loop
|
|
511
|
+
* before this helper) — no need to duplicate them in `unsentToolResults`.
|
|
512
|
+
*
|
|
513
|
+
* @param currentResponse - The response that produced the paused tool calls
|
|
514
|
+
* @param pausedCalls - HITL tool calls whose `onToolCalled` returned `null`
|
|
515
|
+
*/
|
|
516
|
+
async persistHitlPause(currentResponse, pausedCalls) {
|
|
517
|
+
this.finalResponse = currentResponse;
|
|
518
|
+
if (!this.stateAccessor) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const stateUpdates = {
|
|
522
|
+
pendingToolCalls: pausedCalls,
|
|
523
|
+
status: 'awaiting_hitl',
|
|
524
|
+
};
|
|
525
|
+
await this.saveStateSafely(stateUpdates);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Compute the `output` payload sent to the model for a successfully
|
|
529
|
+
* settled tool execution. Routes through `toModelOutput` when the tool
|
|
530
|
+
* defines one (which may itself throw to surface an error), falls back to
|
|
531
|
+
* `JSON.stringify(result)` otherwise, and emits an error envelope when the
|
|
532
|
+
* executor itself reported an error.
|
|
533
|
+
*/
|
|
534
|
+
async computeToolOutputForModel(value) {
|
|
535
|
+
if (value.result.error) {
|
|
536
|
+
return JSON.stringify({
|
|
537
|
+
error: value.result.error.message,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
if (!isAutoResolvableTool(value.tool) || !value.tool.function.toModelOutput) {
|
|
541
|
+
return JSON.stringify(value.result.result);
|
|
542
|
+
}
|
|
543
|
+
// Arguments have already been validated upstream by the tool's Zod
|
|
544
|
+
// inputSchema (which must be a ZodObject), so the runtime shape is
|
|
545
|
+
// always a record here. A non-record value here signals a real upstream
|
|
546
|
+
// bug we want surfaced, not a case to paper over with `{}`.
|
|
547
|
+
const rawArgs = value.toolCall.arguments;
|
|
548
|
+
if (!isRecord(rawArgs)) {
|
|
549
|
+
throw new Error(`toolCall.arguments for "${value.toolCall.name}" must be an object after Zod validation, got ${describeNonRecord(rawArgs)}`);
|
|
550
|
+
}
|
|
551
|
+
const modelOutputResult = await value.tool.function.toModelOutput({
|
|
552
|
+
output: value.result.result,
|
|
553
|
+
input: rawArgs,
|
|
554
|
+
});
|
|
555
|
+
if (modelOutputResult.type === 'content') {
|
|
556
|
+
return modelOutputResult.value;
|
|
557
|
+
}
|
|
558
|
+
return JSON.stringify(value.result.result);
|
|
559
|
+
}
|
|
467
560
|
/**
|
|
468
561
|
* Execute all tools in a single round in parallel.
|
|
469
562
|
* Emits tool.result events after tool execution completes.
|
|
470
563
|
*
|
|
471
564
|
* @param toolCalls - The tool calls to execute
|
|
472
565
|
* @param turnContext - The current turn context
|
|
473
|
-
* @returns
|
|
566
|
+
* @returns Object with the function call outputs formatted for the API and
|
|
567
|
+
* the list of HITL tool calls that paused (returned `null` from
|
|
568
|
+
* `onToolCalled`). Callers should break out of the execution loop when
|
|
569
|
+
* `pausedCalls` is non-empty rather than sending an incomplete set of
|
|
570
|
+
* outputs back to the model.
|
|
474
571
|
*/
|
|
475
572
|
async executeToolRound(toolCalls, turnContext) {
|
|
476
573
|
const toolCallPromises = toolCalls.map(async (toolCall) => {
|
|
477
|
-
const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
|
|
478
|
-
if (!tool || !
|
|
574
|
+
const tool = this.options.tools?.find((t) => isClientTool(t) && t.function.name === toolCall.name);
|
|
575
|
+
if (!tool || !isAutoResolvableTool(tool)) {
|
|
479
576
|
return null;
|
|
480
577
|
}
|
|
481
578
|
// Check if arguments failed to parse (remained as string instead of object)
|
|
@@ -511,6 +608,13 @@ export class ModelResult {
|
|
|
511
608
|
}
|
|
512
609
|
: undefined;
|
|
513
610
|
const result = await executeTool(tool, toolCall, turnContext, onPreliminaryResult, this.contextStore ?? undefined, this.options.sharedContextSchema);
|
|
611
|
+
if (result === null) {
|
|
612
|
+
// HITL tool paused — surface as manual (no output this round)
|
|
613
|
+
return {
|
|
614
|
+
type: 'paused',
|
|
615
|
+
toolCall,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
514
618
|
return {
|
|
515
619
|
type: 'execution',
|
|
516
620
|
toolCall,
|
|
@@ -521,6 +625,7 @@ export class ModelResult {
|
|
|
521
625
|
});
|
|
522
626
|
const settledResults = await Promise.allSettled(toolCallPromises);
|
|
523
627
|
const toolResults = [];
|
|
628
|
+
const pausedCalls = [];
|
|
524
629
|
for (let i = 0; i < settledResults.length; i++) {
|
|
525
630
|
const settled = settledResults[i];
|
|
526
631
|
const originalToolCall = toolCalls[i];
|
|
@@ -561,32 +666,21 @@ export class ModelResult {
|
|
|
561
666
|
});
|
|
562
667
|
continue;
|
|
563
668
|
}
|
|
669
|
+
if (value.type === 'paused') {
|
|
670
|
+
// HITL tool returned null — record the pause so the caller can break
|
|
671
|
+
// out of the outer loop before attempting a follow-up request with an
|
|
672
|
+
// incomplete set of outputs. The call will be surfaced via state
|
|
673
|
+
// (pendingToolCalls + status='awaiting_hitl') for manual resume.
|
|
674
|
+
pausedCalls.push(value.toolCall);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
564
677
|
const toolResult = (value.result.error
|
|
565
678
|
? {
|
|
566
679
|
error: value.result.error.message,
|
|
567
680
|
}
|
|
568
681
|
: value.result.result);
|
|
569
682
|
this.broadcastToolResult(value.toolCall.id, toolResult, value.preliminaryResultsForCall.length > 0 ? value.preliminaryResultsForCall : undefined);
|
|
570
|
-
|
|
571
|
-
if (value.result.error) {
|
|
572
|
-
outputForModel = JSON.stringify({
|
|
573
|
-
error: value.result.error.message,
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
else if (value.tool.function.toModelOutput) {
|
|
577
|
-
// toModelOutput exists - call it (may throw, which surfaces the error)
|
|
578
|
-
const modelOutputResult = await value.tool.function.toModelOutput({
|
|
579
|
-
output: value.result.result,
|
|
580
|
-
input: value.toolCall.arguments,
|
|
581
|
-
});
|
|
582
|
-
outputForModel =
|
|
583
|
-
modelOutputResult.type === 'content'
|
|
584
|
-
? modelOutputResult.value
|
|
585
|
-
: JSON.stringify(value.result.result);
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
outputForModel = JSON.stringify(value.result.result);
|
|
589
|
-
}
|
|
683
|
+
const outputForModel = await this.computeToolOutputForModel(value);
|
|
590
684
|
const executedOutput = {
|
|
591
685
|
type: 'function_call_output',
|
|
592
686
|
id: `output_${value.toolCall.id}`,
|
|
@@ -600,7 +694,10 @@ export class ModelResult {
|
|
|
600
694
|
timestamp: Date.now(),
|
|
601
695
|
});
|
|
602
696
|
}
|
|
603
|
-
return
|
|
697
|
+
return {
|
|
698
|
+
toolResults,
|
|
699
|
+
pausedCalls,
|
|
700
|
+
};
|
|
604
701
|
}
|
|
605
702
|
/**
|
|
606
703
|
* Resolve async functions for the current turn.
|
|
@@ -725,6 +822,72 @@ export class ModelResult {
|
|
|
725
822
|
const { stopWhen: _, state: _s, requireApproval: _r, approveToolCalls: _a, rejectToolCalls: _rj, context: _c, ...rest } = this.options.request;
|
|
726
823
|
return rest;
|
|
727
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* Apply `onResponseReceived` hooks to the freshly-supplied input items
|
|
827
|
+
* only, without re-hooking historical items that live in
|
|
828
|
+
* `currentState.messages`. Historical `function_call` items are passed to
|
|
829
|
+
* `applyOnResponseReceivedHooks` purely as callId → toolName
|
|
830
|
+
* name-resolution context and are dropped from the returned array.
|
|
831
|
+
*
|
|
832
|
+
* This keeps hooks idempotent across `callModel` invocations on the same
|
|
833
|
+
* conversation: the first call hooks the caller-supplied output, and
|
|
834
|
+
* subsequent calls (which rehydrate state) do not re-fire it.
|
|
835
|
+
*
|
|
836
|
+
* @param freshItems - Items newly supplied this turn (not yet hooked).
|
|
837
|
+
* May contain any mix of InputsUnion array members — only
|
|
838
|
+
* `function_call_output` items are affected by hooks; everything else
|
|
839
|
+
* is returned unchanged.
|
|
840
|
+
* @param historicalItems - Existing messages from loaded state. Only
|
|
841
|
+
* `function_call` entries are consulted for name resolution; no other
|
|
842
|
+
* items are inspected and none are mutated.
|
|
843
|
+
* @param turnContext - Turn context for hook invocation
|
|
844
|
+
* @returns The fresh items in original order, with `output` rewritten on
|
|
845
|
+
* any `function_call_output` whose matching HITL tool defines
|
|
846
|
+
* `onResponseReceived`.
|
|
847
|
+
*/
|
|
848
|
+
async applyHooksToFreshItems(freshItems, historicalItems, turnContext) {
|
|
849
|
+
if (freshItems.length === 0) {
|
|
850
|
+
return freshItems;
|
|
851
|
+
}
|
|
852
|
+
// Collect function_call items from history so the hook executor can
|
|
853
|
+
// resolve callId -> toolName without us having to mirror that logic.
|
|
854
|
+
const historyArray = Array.isArray(historicalItems)
|
|
855
|
+
? historicalItems
|
|
856
|
+
: [
|
|
857
|
+
historicalItems,
|
|
858
|
+
];
|
|
859
|
+
const functionCallItems = [];
|
|
860
|
+
for (const item of historyArray) {
|
|
861
|
+
if (isFunctionCallItem(item)) {
|
|
862
|
+
functionCallItems.push(item);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// Build a synthetic input that puts the historical function_calls
|
|
866
|
+
// BEFORE the fresh items. `applyOnResponseReceivedHooks` only rewrites
|
|
867
|
+
// function_call_output items, so the function_call items are seen only
|
|
868
|
+
// as name-resolution context.
|
|
869
|
+
const syntheticInput = [
|
|
870
|
+
...functionCallItems,
|
|
871
|
+
...freshItems,
|
|
872
|
+
];
|
|
873
|
+
const hookedInput = await applyOnResponseReceivedHooks(syntheticInput, this.options.tools, turnContext, this.contextStore ?? undefined, this.options.sharedContextSchema);
|
|
874
|
+
if (hookedInput === syntheticInput) {
|
|
875
|
+
// No rewrites; return the originals unchanged.
|
|
876
|
+
return freshItems;
|
|
877
|
+
}
|
|
878
|
+
// Drop the leading function_call items we prepended; what remains is
|
|
879
|
+
// the fresh items in their original order (some with rewritten outputs).
|
|
880
|
+
const hookedArray = Array.isArray(hookedInput)
|
|
881
|
+
? hookedInput
|
|
882
|
+
: [
|
|
883
|
+
hookedInput,
|
|
884
|
+
];
|
|
885
|
+
if (hookedArray.length !== syntheticInput.length) {
|
|
886
|
+
// Shouldn't happen (hooks only rewrite in-place), but be conservative.
|
|
887
|
+
return freshItems;
|
|
888
|
+
}
|
|
889
|
+
return hookedArray.slice(functionCallItems.length);
|
|
890
|
+
}
|
|
728
891
|
/**
|
|
729
892
|
* Safely persist state with error handling.
|
|
730
893
|
* Wraps state save operations to ensure failures are properly reported.
|
|
@@ -779,8 +942,14 @@ export class ModelResult {
|
|
|
779
942
|
const loadedState = await this.stateAccessor.load();
|
|
780
943
|
if (loadedState) {
|
|
781
944
|
this.currentState = loadedState;
|
|
782
|
-
// Check if we're resuming from awaiting_approval
|
|
783
|
-
|
|
945
|
+
// Check if we're resuming from awaiting_approval or awaiting_hitl
|
|
946
|
+
// with decisions. `awaiting_hitl` reuses `processApprovalDecisions`
|
|
947
|
+
// because the resume mechanism is identical — the caller supplies
|
|
948
|
+
// `approveToolCalls`/`rejectToolCalls` for paused call IDs, and we
|
|
949
|
+
// re-invoke `executeTool` on approved calls (which re-runs
|
|
950
|
+
// `onToolCalled` for HITL tools).
|
|
951
|
+
const isResumableStatus = loadedState.status === 'awaiting_approval' || loadedState.status === 'awaiting_hitl';
|
|
952
|
+
if (isResumableStatus &&
|
|
784
953
|
(this.approvedToolCalls.length > 0 || this.rejectedToolCalls.length > 0)) {
|
|
785
954
|
// Initialize context store before resuming so tools have access
|
|
786
955
|
if (this.options.context !== undefined) {
|
|
@@ -826,29 +995,55 @@ export class ModelResult {
|
|
|
826
995
|
}
|
|
827
996
|
// Resolve any async functions first
|
|
828
997
|
let baseRequest = await this.resolveRequestForContext(initialContext);
|
|
829
|
-
//
|
|
830
|
-
|
|
998
|
+
// Split input into "historical" (already in state.messages) and "fresh"
|
|
999
|
+
// (newly supplied this turn). `onResponseReceived` must fire only for
|
|
1000
|
+
// fresh items — re-hooking historical outputs on every callModel call
|
|
1001
|
+
// would double-invoke non-idempotent hooks.
|
|
1002
|
+
const hasLoadedHistory = !!this.currentState?.messages &&
|
|
831
1003
|
Array.isArray(this.currentState.messages) &&
|
|
832
|
-
this.currentState.messages.length > 0
|
|
833
|
-
|
|
1004
|
+
this.currentState.messages.length > 0;
|
|
1005
|
+
if (hasLoadedHistory && this.currentState) {
|
|
1006
|
+
// `currentState.messages` is InputsUnion — keep it as that union so
|
|
1007
|
+
// appendToMessages (which expects InputsUnion) accepts it directly.
|
|
1008
|
+
const historicalMessages = this.currentState.messages;
|
|
1009
|
+
// Normalize the caller-supplied input for this turn into an array of
|
|
1010
|
+
// fresh items. Undefined stays undefined (no new items). The widening
|
|
1011
|
+
// to BaseInputsUnion[] matches the signature of appendToMessages and
|
|
1012
|
+
// mirrors the pre-existing pattern elsewhere in this file; the two
|
|
1013
|
+
// union shapes (InputsUnion1 vs BaseInputsUnion1) describe the same
|
|
1014
|
+
// SDK input items with different nominal types, and BaseInputsUnion
|
|
1015
|
+
// already includes `any` in its element type, so the runtime shape
|
|
1016
|
+
// is preserved either way.
|
|
834
1017
|
const newInput = baseRequest.input;
|
|
835
|
-
|
|
836
|
-
|
|
1018
|
+
let freshItems;
|
|
1019
|
+
if (newInput !== undefined) {
|
|
1020
|
+
freshItems = Array.isArray(newInput)
|
|
837
1021
|
? newInput
|
|
838
1022
|
: [
|
|
839
1023
|
newInput,
|
|
840
1024
|
];
|
|
841
|
-
baseRequest = {
|
|
842
|
-
...baseRequest,
|
|
843
|
-
input: appendToMessages(this.currentState.messages, inputArray),
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
else {
|
|
847
|
-
baseRequest = {
|
|
848
|
-
...baseRequest,
|
|
849
|
-
input: this.currentState.messages,
|
|
850
|
-
};
|
|
851
1025
|
}
|
|
1026
|
+
// Hook fresh items only (historical function_calls serve as
|
|
1027
|
+
// name-resolution context). Leave historical items untouched.
|
|
1028
|
+
const hookedFresh = freshItems
|
|
1029
|
+
? await this.applyHooksToFreshItems(freshItems, historicalMessages, initialContext)
|
|
1030
|
+
: undefined;
|
|
1031
|
+
baseRequest = {
|
|
1032
|
+
...baseRequest,
|
|
1033
|
+
input: hookedFresh
|
|
1034
|
+
? appendToMessages(historicalMessages, hookedFresh)
|
|
1035
|
+
: historicalMessages,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
else if (baseRequest.input !== undefined) {
|
|
1039
|
+
// No loaded history — everything in input is fresh. Hook the whole
|
|
1040
|
+
// thing (non-array inputs pass through applyOnResponseReceivedHooks
|
|
1041
|
+
// unchanged).
|
|
1042
|
+
const hookedInput = await applyOnResponseReceivedHooks(baseRequest.input, this.options.tools, initialContext, this.contextStore ?? undefined, this.options.sharedContextSchema);
|
|
1043
|
+
baseRequest = {
|
|
1044
|
+
...baseRequest,
|
|
1045
|
+
input: hookedInput,
|
|
1046
|
+
};
|
|
852
1047
|
}
|
|
853
1048
|
// Store resolved request with stream mode
|
|
854
1049
|
this.resolvedRequest = {
|
|
@@ -895,19 +1090,28 @@ export class ModelResult {
|
|
|
895
1090
|
numberOfTurns: this.allToolExecutionRounds.length + 1,
|
|
896
1091
|
// context is handled via contextStore, not on TurnContext
|
|
897
1092
|
};
|
|
1093
|
+
// Track approved HITL calls that paused (onToolCalled returned null) —
|
|
1094
|
+
// these stay in pendingToolCalls so the caller can resume them later.
|
|
1095
|
+
const hitlPausedIds = new Set();
|
|
898
1096
|
// Process approvals - execute the approved tools
|
|
899
1097
|
for (const callId of this.approvedToolCalls) {
|
|
900
1098
|
const toolCall = pendingCalls.find((tc) => tc.id === callId);
|
|
901
1099
|
if (!toolCall) {
|
|
902
1100
|
continue;
|
|
903
1101
|
}
|
|
904
|
-
const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
|
|
905
|
-
if (!tool || !
|
|
1102
|
+
const tool = this.options.tools?.find((t) => isClientTool(t) && t.function.name === toolCall.name);
|
|
1103
|
+
if (!tool || !isAutoResolvableTool(tool)) {
|
|
906
1104
|
// Can't execute, create error result
|
|
907
1105
|
unsentResults.push(createRejectedResult(callId, String(toolCall.name), 'Tool not found or not executable'));
|
|
908
1106
|
continue;
|
|
909
1107
|
}
|
|
910
1108
|
const result = await executeTool(tool, toolCall, turnContext, undefined, this.contextStore ?? undefined, this.options.sharedContextSchema);
|
|
1109
|
+
if (result === null) {
|
|
1110
|
+
// HITL tool paused on approval — keep the call visible to the caller
|
|
1111
|
+
// via pendingToolCalls (status becomes 'awaiting_hitl' below).
|
|
1112
|
+
hitlPausedIds.add(callId);
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
911
1115
|
if (result.error) {
|
|
912
1116
|
unsentResults.push(createRejectedResult(callId, String(toolCall.name), result.error.message));
|
|
913
1117
|
}
|
|
@@ -923,15 +1127,32 @@ export class ModelResult {
|
|
|
923
1127
|
}
|
|
924
1128
|
unsentResults.push(createRejectedResult(callId, String(toolCall.name), 'Rejected by user'));
|
|
925
1129
|
}
|
|
926
|
-
// Remove processed calls from pending
|
|
1130
|
+
// Remove processed calls from pending. Approved HITL calls that paused are
|
|
1131
|
+
// NOT considered processed — they stay on pendingToolCalls so getPendingToolCalls()
|
|
1132
|
+
// still surfaces them to the caller on resume.
|
|
927
1133
|
const processedIds = new Set([
|
|
928
1134
|
...this.approvedToolCalls,
|
|
929
1135
|
...this.rejectedToolCalls,
|
|
930
|
-
]);
|
|
1136
|
+
].filter((id) => !hitlPausedIds.has(id)));
|
|
931
1137
|
const remainingPending = pendingCalls.filter((tc) => !processedIds.has(tc.id));
|
|
1138
|
+
// Determine status:
|
|
1139
|
+
// - Any still-unprocessed approval-required call keeps us in 'awaiting_approval'
|
|
1140
|
+
// - Otherwise, any HITL paused call moves us to 'awaiting_hitl'
|
|
1141
|
+
// - Otherwise, we continue with 'in_progress'
|
|
1142
|
+
const remainingUnresolvedApprovals = remainingPending.filter((tc) => !hitlPausedIds.has(tc.id));
|
|
1143
|
+
let nextStatus;
|
|
1144
|
+
if (remainingUnresolvedApprovals.length > 0) {
|
|
1145
|
+
nextStatus = 'awaiting_approval';
|
|
1146
|
+
}
|
|
1147
|
+
else if (hitlPausedIds.size > 0) {
|
|
1148
|
+
nextStatus = 'awaiting_hitl';
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
nextStatus = 'in_progress';
|
|
1152
|
+
}
|
|
932
1153
|
// Update state - conditionally include optional properties only if they have values
|
|
933
1154
|
const stateUpdates = {
|
|
934
|
-
status:
|
|
1155
|
+
status: nextStatus,
|
|
935
1156
|
};
|
|
936
1157
|
if (remainingPending.length > 0) {
|
|
937
1158
|
stateUpdates.pendingToolCalls = remainingPending;
|
|
@@ -952,8 +1173,8 @@ export class ModelResult {
|
|
|
952
1173
|
this.clearOptionalStateProperties(propsToClear);
|
|
953
1174
|
await this.saveStateSafely();
|
|
954
1175
|
}
|
|
955
|
-
// If we
|
|
956
|
-
if (
|
|
1176
|
+
// If we are paused (for approval or for HITL), stop here
|
|
1177
|
+
if (nextStatus !== 'in_progress') {
|
|
957
1178
|
return;
|
|
958
1179
|
}
|
|
959
1180
|
// Otherwise, continue with tool execution using unsent results
|
|
@@ -972,7 +1193,16 @@ export class ModelResult {
|
|
|
972
1193
|
}
|
|
973
1194
|
// Convert to API format
|
|
974
1195
|
const toolOutputs = unsentResultsToAPIFormat(unsentResults);
|
|
975
|
-
// Build
|
|
1196
|
+
// Build turn context for hook resolution
|
|
1197
|
+
// numberOfTurns represents the current turn number (1-indexed after initial)
|
|
1198
|
+
const turnContext = {
|
|
1199
|
+
numberOfTurns: this.allToolExecutionRounds.length + 1,
|
|
1200
|
+
};
|
|
1201
|
+
// Append SDK-generated tool outputs directly — `onResponseReceived` is
|
|
1202
|
+
// reserved for caller-supplied outputs (the resume-with-function-call-
|
|
1203
|
+
// output path, hooked during init). SDK-produced outputs from auto-
|
|
1204
|
+
// executed tools already went through the tool's own execute/generator
|
|
1205
|
+
// pipeline and must not be mutated by the resume hook.
|
|
976
1206
|
const currentMessages = this.currentState.messages;
|
|
977
1207
|
const newInput = appendToMessages(currentMessages, toolOutputs);
|
|
978
1208
|
// Clear unsent results from state
|
|
@@ -984,12 +1214,11 @@ export class ModelResult {
|
|
|
984
1214
|
]);
|
|
985
1215
|
await this.saveStateSafely();
|
|
986
1216
|
// Build request with the updated input
|
|
987
|
-
// numberOfTurns represents the current turn number (1-indexed after initial)
|
|
988
|
-
const turnContext = {
|
|
989
|
-
numberOfTurns: this.allToolExecutionRounds.length + 1,
|
|
990
|
-
};
|
|
991
1217
|
const baseRequest = await this.resolveRequestForContext(turnContext);
|
|
992
|
-
//
|
|
1218
|
+
// No hooking here: SDK-generated outputs are appended as-is and any
|
|
1219
|
+
// caller-supplied items in `newInput` (carried over from init) were
|
|
1220
|
+
// already hooked during `initStream` — re-hooking would double-fire
|
|
1221
|
+
// non-idempotent hooks.
|
|
993
1222
|
const request = {
|
|
994
1223
|
...baseRequest,
|
|
995
1224
|
input: newInput,
|
|
@@ -1025,8 +1254,12 @@ export class ModelResult {
|
|
|
1025
1254
|
// biome-ignore lint: IIFE used for lazy initialization pattern
|
|
1026
1255
|
this.toolExecutionPromise = (async () => {
|
|
1027
1256
|
await this.initStream();
|
|
1028
|
-
// If resuming from approval and still pending, don't continue
|
|
1029
|
-
|
|
1257
|
+
// If resuming from approval or HITL pause and still pending, don't continue.
|
|
1258
|
+
// `processApprovalDecisions` runs in initStream for resumes; if it left us
|
|
1259
|
+
// paused (any remaining pending calls), the outer loop should not execute.
|
|
1260
|
+
if (this.isResumingFromApproval &&
|
|
1261
|
+
(this.currentState?.status === 'awaiting_approval' ||
|
|
1262
|
+
this.currentState?.status === 'awaiting_hitl')) {
|
|
1030
1263
|
return;
|
|
1031
1264
|
}
|
|
1032
1265
|
// Get initial response
|
|
@@ -1082,16 +1315,46 @@ export class ModelResult {
|
|
|
1082
1315
|
// Resolve async functions for this turn
|
|
1083
1316
|
await this.resolveAsyncFunctionsForTurn(turnContext);
|
|
1084
1317
|
// Execute tools
|
|
1085
|
-
const toolResults = await this.executeToolRound(currentToolCalls, turnContext);
|
|
1318
|
+
const { toolResults, pausedCalls } = await this.executeToolRound(currentToolCalls, turnContext);
|
|
1319
|
+
// Server-tool output items are already-executed results in the
|
|
1320
|
+
// response; collect them so toolResults presents a unified list.
|
|
1321
|
+
const serverToolItems = [];
|
|
1322
|
+
for (const item of currentResponse.output) {
|
|
1323
|
+
if (!hasTypeProperty(item)) {
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (item.type === 'message' ||
|
|
1327
|
+
item.type === 'reasoning' ||
|
|
1328
|
+
item.type === 'function_call') {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
// Everything else is a server-tool output item (web_search_call,
|
|
1332
|
+
// image_generation_call, file_search_call, or generic
|
|
1333
|
+
// OutputServerToolItem covering openrouter:datetime and any new
|
|
1334
|
+
// SDK server tool types).
|
|
1335
|
+
if (isServerToolResultItem(item)) {
|
|
1336
|
+
serverToolItems.push(item);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1086
1339
|
// Track execution round
|
|
1087
1340
|
this.allToolExecutionRounds.push({
|
|
1088
1341
|
round: currentRound,
|
|
1089
1342
|
toolCalls: currentToolCalls,
|
|
1090
1343
|
response: currentResponse,
|
|
1091
|
-
toolResults
|
|
1344
|
+
toolResults: [
|
|
1345
|
+
...toolResults,
|
|
1346
|
+
...serverToolItems,
|
|
1347
|
+
],
|
|
1092
1348
|
});
|
|
1093
1349
|
// Save tool results to state
|
|
1094
1350
|
await this.saveToolResultsToState(toolResults);
|
|
1351
|
+
// If any HITL tools paused this round, stop here without making a
|
|
1352
|
+
// follow-up request — sending an incomplete set of outputs would be
|
|
1353
|
+
// incorrect. Persist the paused calls so the caller can resume later.
|
|
1354
|
+
if (pausedCalls.length > 0) {
|
|
1355
|
+
await this.persistHitlPause(currentResponse, pausedCalls);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1095
1358
|
// Apply nextTurnParams
|
|
1096
1359
|
await this.applyNextTurnParams(currentToolCalls);
|
|
1097
1360
|
currentResponse = await this.makeFollowupRequest(currentResponse, toolResults, turnNumber);
|
|
@@ -1208,6 +1471,30 @@ export class ModelResult {
|
|
|
1208
1471
|
* - function_call_output: Results from executed tools
|
|
1209
1472
|
*/
|
|
1210
1473
|
getItemsStream() {
|
|
1474
|
+
// Build the allowed-item-type scope from the tools actually passed to
|
|
1475
|
+
// callModel, mirroring the compile-time rules that produce
|
|
1476
|
+
// StreamableOutputItem<TTools>. A runtime predicate then drops items
|
|
1477
|
+
// whose type isn't reachable in the narrowed union. The predicate's
|
|
1478
|
+
// claim (`item is StreamableOutputItem<TTools>`) is sound because:
|
|
1479
|
+
// - `allowed` is constructed from the same tools that produced TTools
|
|
1480
|
+
// - `OutputServerToolItem.type` is `string` (open), so any non-client
|
|
1481
|
+
// item type is structurally assignable to it, covering generic /
|
|
1482
|
+
// unmapped server-tool outputs.
|
|
1483
|
+
const scope = this.computeItemStreamScope();
|
|
1484
|
+
const isInScope = (item) => {
|
|
1485
|
+
if (scope.acceptAll) {
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
if (scope.allowed.has(item.type)) {
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1491
|
+
if (scope.acceptGenericServerItem &&
|
|
1492
|
+
item.type !== 'function_call' &&
|
|
1493
|
+
item.type !== 'function_call_output') {
|
|
1494
|
+
return true;
|
|
1495
|
+
}
|
|
1496
|
+
return false;
|
|
1497
|
+
};
|
|
1211
1498
|
return async function* () {
|
|
1212
1499
|
await this.initStream();
|
|
1213
1500
|
if (!this.reusableStream && !this.finalResponse) {
|
|
@@ -1216,7 +1503,11 @@ export class ModelResult {
|
|
|
1216
1503
|
// No tools — stream single turn directly (no broadcaster needed)
|
|
1217
1504
|
if (!this.options.tools?.length) {
|
|
1218
1505
|
if (this.reusableStream) {
|
|
1219
|
-
|
|
1506
|
+
for await (const item of buildItemsStream(this.reusableStream)) {
|
|
1507
|
+
if (isInScope(item)) {
|
|
1508
|
+
yield item;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1220
1511
|
}
|
|
1221
1512
|
return;
|
|
1222
1513
|
}
|
|
@@ -1228,7 +1519,9 @@ export class ModelResult {
|
|
|
1228
1519
|
for await (const event of consumer) {
|
|
1229
1520
|
// Tool call outputs → yield directly as function_call_output items
|
|
1230
1521
|
if (isToolCallOutputEvent(event)) {
|
|
1231
|
-
|
|
1522
|
+
if (isInScope(event.output)) {
|
|
1523
|
+
yield event.output;
|
|
1524
|
+
}
|
|
1232
1525
|
continue;
|
|
1233
1526
|
}
|
|
1234
1527
|
// Stream termination → reset items map for next turn
|
|
@@ -1243,7 +1536,7 @@ export class ModelResult {
|
|
|
1243
1536
|
const handler = itemsStreamHandlers[event.type];
|
|
1244
1537
|
if (handler) {
|
|
1245
1538
|
const result = handler(event, itemsInProgress);
|
|
1246
|
-
if (result) {
|
|
1539
|
+
if (result && isInScope(result)) {
|
|
1247
1540
|
yield result;
|
|
1248
1541
|
}
|
|
1249
1542
|
}
|
|
@@ -1252,6 +1545,98 @@ export class ModelResult {
|
|
|
1252
1545
|
await executionPromise;
|
|
1253
1546
|
}.call(this);
|
|
1254
1547
|
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Compute the runtime allow-list of item types that `getItemsStream()`
|
|
1550
|
+
* may yield, derived from the tools actually passed to callModel. The
|
|
1551
|
+
* three return modes correspond to the compile-time narrowing:
|
|
1552
|
+
*
|
|
1553
|
+
* - `acceptAll: true` — no tools or fully-unconstrained TTools; the
|
|
1554
|
+
* yielded union is the widest `StreamableOutputItem`.
|
|
1555
|
+
* - Specific `allowed` set — client tools contribute
|
|
1556
|
+
* `function_call` / `function_call_output`; mapped server tools
|
|
1557
|
+
* contribute their SDK output item type literal
|
|
1558
|
+
* (`web_search_call`, `file_search_call`, `image_generation_call`).
|
|
1559
|
+
* - `acceptGenericServerItem: true` — at least one server tool has a
|
|
1560
|
+
* type the agent SDK does not have a dedicated output mapping for
|
|
1561
|
+
* (e.g. `openrouter:datetime`, `mcp`, new SDK additions). Any
|
|
1562
|
+
* non-client item type is accepted because these items pass through
|
|
1563
|
+
* as `OutputServerToolItem`, whose `type` field is an open `string`.
|
|
1564
|
+
*/
|
|
1565
|
+
computeItemStreamScope() {
|
|
1566
|
+
const tools = this.options.tools ?? [];
|
|
1567
|
+
if (tools.length === 0) {
|
|
1568
|
+
// No tools passed: runtime only emits message/reasoning, but the
|
|
1569
|
+
// widest StreamableOutputItem<readonly Tool[]> includes every item
|
|
1570
|
+
// type. Accept all so the default unconstrained case matches its
|
|
1571
|
+
// compile-time union.
|
|
1572
|
+
return {
|
|
1573
|
+
acceptAll: true,
|
|
1574
|
+
allowed: new Set(),
|
|
1575
|
+
acceptGenericServerItem: false,
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
const allowed = new Set([
|
|
1579
|
+
'message',
|
|
1580
|
+
'reasoning',
|
|
1581
|
+
]);
|
|
1582
|
+
let acceptGenericServerItem = false;
|
|
1583
|
+
for (const tool of tools) {
|
|
1584
|
+
if (isClientTool(tool)) {
|
|
1585
|
+
allowed.add('function_call');
|
|
1586
|
+
allowed.add('function_call_output');
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (!isServerTool(tool)) {
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const requestType = tool.config.type;
|
|
1593
|
+
switch (requestType) {
|
|
1594
|
+
case 'web_search':
|
|
1595
|
+
case 'web_search_2025_08_26':
|
|
1596
|
+
case 'web_search_preview':
|
|
1597
|
+
case 'web_search_preview_2025_03_11':
|
|
1598
|
+
allowed.add('web_search_call');
|
|
1599
|
+
break;
|
|
1600
|
+
case 'openrouter:web_search':
|
|
1601
|
+
// Defensive: OpenRouter's web_search variant may emit either the
|
|
1602
|
+
// standard OutputWebSearchCallItem (type='web_search_call') OR be
|
|
1603
|
+
// wrapped in OutputServerToolItem with type='openrouter:web_search'.
|
|
1604
|
+
// Accept both literals so the runtime filter doesn't silently drop
|
|
1605
|
+
// valid items. Do NOT set acceptGenericServerItem — we know the
|
|
1606
|
+
// tool type and want the filter narrow.
|
|
1607
|
+
allowed.add('web_search_call');
|
|
1608
|
+
allowed.add('openrouter:web_search');
|
|
1609
|
+
break;
|
|
1610
|
+
case 'file_search':
|
|
1611
|
+
allowed.add('file_search_call');
|
|
1612
|
+
break;
|
|
1613
|
+
case 'image_generation':
|
|
1614
|
+
allowed.add('image_generation_call');
|
|
1615
|
+
break;
|
|
1616
|
+
case 'openrouter:datetime':
|
|
1617
|
+
// Known server tool whose SDK output item uses the same literal
|
|
1618
|
+
// as the request type. Mirrors `KnownServerToolOutputs` in
|
|
1619
|
+
// stream-transformers.ts so the runtime filter stays as narrow
|
|
1620
|
+
// as the compile-time union (no acceptGenericServerItem widening).
|
|
1621
|
+
allowed.add('openrouter:datetime');
|
|
1622
|
+
break;
|
|
1623
|
+
default:
|
|
1624
|
+
// Unknown / generic server tool — at runtime its output items
|
|
1625
|
+
// pass through as the request-type literal or as the SDK's
|
|
1626
|
+
// OutputServerToolItem wrapper. Accept the literal plus the
|
|
1627
|
+
// generic fallback. See `StreamableOutputItem` narrowing in
|
|
1628
|
+
// stream-transformers.ts for the matching type-level rules.
|
|
1629
|
+
allowed.add(requestType);
|
|
1630
|
+
acceptGenericServerItem = true;
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return {
|
|
1635
|
+
acceptAll: false,
|
|
1636
|
+
allowed,
|
|
1637
|
+
acceptGenericServerItem,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1255
1640
|
/**
|
|
1256
1641
|
* @deprecated Use `getItemsStream()` instead. This method only streams messages,
|
|
1257
1642
|
* while `getItemsStream()` streams all output item types (messages, function_calls,
|
|
@@ -1286,9 +1671,12 @@ export class ModelResult {
|
|
|
1286
1671
|
yield item;
|
|
1287
1672
|
}
|
|
1288
1673
|
}
|
|
1289
|
-
// Then yield the function_call_output results
|
|
1674
|
+
// Then yield the function_call_output results (client tools only;
|
|
1675
|
+
// server-tool output items are surfaced through getItemsStream).
|
|
1290
1676
|
for (const toolResult of round.toolResults) {
|
|
1291
|
-
|
|
1677
|
+
if (isFunctionCallOutputItem(toolResult)) {
|
|
1678
|
+
yield toolResult;
|
|
1679
|
+
}
|
|
1292
1680
|
}
|
|
1293
1681
|
}
|
|
1294
1682
|
// Yield manual tool function_call items from finalResponse, skipping duplicates
|
|
@@ -1489,13 +1877,16 @@ export class ModelResult {
|
|
|
1489
1877
|
// Multi-Turn Conversation State Methods
|
|
1490
1878
|
// =========================================================================
|
|
1491
1879
|
/**
|
|
1492
|
-
* Check if the conversation requires human
|
|
1493
|
-
* Returns true
|
|
1880
|
+
* Check if the conversation requires human input to continue.
|
|
1881
|
+
* Returns true when the conversation is paused waiting for caller-supplied
|
|
1882
|
+
* decisions — either approval/rejection (`awaiting_approval`) or HITL tool
|
|
1883
|
+
* resume (`awaiting_hitl`). Also returns true whenever `pendingToolCalls`
|
|
1884
|
+
* is populated regardless of status.
|
|
1494
1885
|
*/
|
|
1495
1886
|
async requiresApproval() {
|
|
1496
1887
|
await this.initStream();
|
|
1497
|
-
|
|
1498
|
-
if (
|
|
1888
|
+
const status = this.currentState?.status;
|
|
1889
|
+
if (status === 'awaiting_approval' || status === 'awaiting_hitl') {
|
|
1499
1890
|
return true;
|
|
1500
1891
|
}
|
|
1501
1892
|
// Also check if pendingToolCalls is populated
|