@ai-sdk/google 3.0.71 → 3.0.72
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/CHANGELOG.md +6 -0
- package/dist/index.js +294 -82
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +290 -68
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/interactions/cancel-google-interaction.ts +60 -0
- package/src/interactions/google-interactions-language-model.ts +68 -42
- package/src/interactions/poll-google-interactions.ts +47 -28
- package/src/interactions/stream-google-interactions.ts +239 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/google",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.72",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@ai-sdk/provider": "
|
|
40
|
-
"@ai-sdk/provider
|
|
39
|
+
"@ai-sdk/provider-utils": "4.0.27",
|
|
40
|
+
"@ai-sdk/provider": "3.0.10"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "20.17.24",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
combineHeaders,
|
|
3
|
+
getRuntimeEnvironmentUserAgent,
|
|
4
|
+
withUserAgentSuffix,
|
|
5
|
+
type FetchFunction,
|
|
6
|
+
} from '@ai-sdk/provider-utils';
|
|
7
|
+
|
|
8
|
+
const getOriginalFetch = () => globalThis.fetch;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Best-effort `POST /interactions/{id}/cancel` to stop a background interaction
|
|
12
|
+
* on Google's side after the caller has aborted locally. Errors and non-2xx
|
|
13
|
+
* responses are swallowed so a cancel failure cannot mask the original abort.
|
|
14
|
+
*
|
|
15
|
+
* Skips the request entirely if `interactionId` is missing/empty -- e.g. when
|
|
16
|
+
* the interaction was created with `store: false` and the API did not return an
|
|
17
|
+
* id.
|
|
18
|
+
*/
|
|
19
|
+
export async function cancelGoogleInteraction({
|
|
20
|
+
baseURL,
|
|
21
|
+
interactionId,
|
|
22
|
+
headers,
|
|
23
|
+
fetch = getOriginalFetch(),
|
|
24
|
+
}: {
|
|
25
|
+
baseURL: string;
|
|
26
|
+
interactionId: string | null | undefined;
|
|
27
|
+
headers: Record<string, string | undefined>;
|
|
28
|
+
fetch?: FetchFunction;
|
|
29
|
+
}): Promise<void> {
|
|
30
|
+
if (interactionId == null || interactionId.length === 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const url = `${baseURL}/interactions/${encodeURIComponent(interactionId)}/cancel`;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: withUserAgentSuffix(
|
|
40
|
+
combineHeaders({ 'Content-Type': 'application/json' }, headers),
|
|
41
|
+
getRuntimeEnvironmentUserAgent(),
|
|
42
|
+
),
|
|
43
|
+
body: '{}',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* Drain the body so undici/Node can return the connection to the pool.
|
|
48
|
+
* Errors (e.g. non-2xx, network failure) are intentionally ignored: this
|
|
49
|
+
* is a best-effort cleanup and must not throw past the caller, which is
|
|
50
|
+
* already handling an aborted/failed run.
|
|
51
|
+
*/
|
|
52
|
+
try {
|
|
53
|
+
await response.text();
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore -- cancel is best-effort
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
pollGoogleInteractionUntilTerminal,
|
|
45
45
|
} from './poll-google-interactions';
|
|
46
46
|
import { prepareGoogleInteractionsTools } from './prepare-google-interactions-tools';
|
|
47
|
+
import { streamGoogleInteractionEvents } from './stream-google-interactions';
|
|
47
48
|
import { synthesizeGoogleInteractionsAgentStream } from './synthesize-google-interactions-agent-stream';
|
|
48
49
|
|
|
49
50
|
export type GoogleInteractionsConfig = {
|
|
@@ -278,12 +279,14 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV3 {
|
|
|
278
279
|
* Agent calls require `background: true` on the wire — otherwise the API
|
|
279
280
|
* rejects them with `background=true is required for agent interactions.`
|
|
280
281
|
* The server returns a non-terminal status (`in_progress`/`requires_action`)
|
|
281
|
-
* and the final outputs
|
|
282
|
-
*
|
|
283
|
-
* surface stays identical to
|
|
282
|
+
* and the final outputs are streamed via `GET /interactions/{id}?stream=true`
|
|
283
|
+
* (or polled via `GET /interactions/{id}`). This is handled internally in
|
|
284
|
+
* `doGenerate` / `doStream` so the user-facing surface stays identical to
|
|
285
|
+
* model-id calls.
|
|
284
286
|
*
|
|
285
287
|
* Model-id calls retain their original synchronous behavior — no
|
|
286
|
-
* `background` field is sent.
|
|
288
|
+
* `background` field is sent. (No documented model accepts `background:
|
|
289
|
+
* true` today; revisit when one does.)
|
|
287
290
|
*/
|
|
288
291
|
const args: GoogleInteractionsRequestBody = pruneUndefined({
|
|
289
292
|
...(isAgent ? { agent: this.agent } : { model: this.modelId }),
|
|
@@ -460,14 +463,13 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV3 {
|
|
|
460
463
|
|
|
461
464
|
/*
|
|
462
465
|
* Agent calls require `background: true`, which is incompatible with
|
|
463
|
-
* `stream: true` on POST.
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
* emitted in the same order as a true SSE response.
|
|
466
|
+
* `stream: true` on POST. Drive these via POST background -> GET stream
|
|
467
|
+
* (with terminal-status short-circuit). The user-facing stream surface
|
|
468
|
+
* stays identical -- text-start / text-delta / text-end / finish parts
|
|
469
|
+
* are emitted in the same order as a true SSE response.
|
|
468
470
|
*/
|
|
469
471
|
if (isAgent) {
|
|
470
|
-
return this.
|
|
472
|
+
return this.doStreamBackground({
|
|
471
473
|
args,
|
|
472
474
|
warnings,
|
|
473
475
|
url,
|
|
@@ -515,26 +517,24 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV3 {
|
|
|
515
517
|
}
|
|
516
518
|
|
|
517
519
|
/*
|
|
518
|
-
* Drive the streaming surface for agent calls.
|
|
520
|
+
* Drive the streaming surface for agent calls. Agents require
|
|
519
521
|
* `background: true`, which is incompatible with `stream: true` on POST.
|
|
520
522
|
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
523
|
+
* Approach:
|
|
524
|
+
* 1. POST `/interactions` with `background: true`. The response includes
|
|
525
|
+
* the interaction id and an initial (usually non-terminal) status.
|
|
526
|
+
* 2. If the POST status is already terminal (rare), synthesize a stream
|
|
527
|
+
* from the polled outputs and we're done.
|
|
528
|
+
* 3. Otherwise open `GET /interactions/{id}?stream=true` and pipe the
|
|
529
|
+
* SSE events through `buildGoogleInteractionsStreamTransform` so the
|
|
530
|
+
* consumer receives text deltas / thinking summaries / tool events as
|
|
531
|
+
* they happen instead of all at once at the end.
|
|
529
532
|
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
* user-facing surface stays identical -- text-start / text-delta /
|
|
534
|
-
* text-end / finish parts arrive in the same order as a true SSE
|
|
535
|
-
* response, just buffered until the agent completes.
|
|
533
|
+
* The SSE connection can drop while the agent idles between events
|
|
534
|
+
* (`UND_ERR_BODY_TIMEOUT`); `streamGoogleInteractionEvents` handles the
|
|
535
|
+
* reconnect-with-`last_event_id` loop transparently.
|
|
536
536
|
*/
|
|
537
|
-
private async
|
|
537
|
+
private async doStreamBackground({
|
|
538
538
|
args,
|
|
539
539
|
warnings,
|
|
540
540
|
url,
|
|
@@ -561,38 +561,64 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV3 {
|
|
|
561
561
|
fetch: this.config.fetch,
|
|
562
562
|
});
|
|
563
563
|
|
|
564
|
-
|
|
564
|
+
const { responseHeaders: postHeaders, value: postResponse } = postResult;
|
|
565
565
|
const interactionId = postResponse.id;
|
|
566
566
|
|
|
567
567
|
if (interactionId == null || interactionId.length === 0) {
|
|
568
568
|
throw new Error(
|
|
569
|
-
'google.interactions:
|
|
569
|
+
'google.interactions: background POST response did not include an interaction id; cannot stream the result.',
|
|
570
570
|
);
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
573
|
+
const headerServiceTier = postHeaders?.['x-gemini-service-tier'];
|
|
574
|
+
|
|
575
|
+
/*
|
|
576
|
+
* If the POST already returned a terminal status (e.g. cached, immediate
|
|
577
|
+
* failure, or `incomplete`), there is nothing to stream from the GET --
|
|
578
|
+
* synthesize directly from the response so the caller still gets a
|
|
579
|
+
* complete stream.
|
|
580
|
+
*/
|
|
581
|
+
if (isTerminalStatus(postResponse.status)) {
|
|
582
|
+
const synthesized = synthesizeGoogleInteractionsAgentStream({
|
|
583
|
+
response: postResponse,
|
|
584
|
+
warnings,
|
|
585
|
+
generateId: this.config.generateId ?? defaultGenerateId,
|
|
586
|
+
includeRawChunks: options.includeRawChunks,
|
|
587
|
+
headerServiceTier,
|
|
581
588
|
});
|
|
582
|
-
|
|
583
|
-
|
|
589
|
+
return {
|
|
590
|
+
stream: synthesized,
|
|
591
|
+
request: { body: args },
|
|
592
|
+
response: { headers: postHeaders },
|
|
593
|
+
};
|
|
584
594
|
}
|
|
585
595
|
|
|
586
|
-
|
|
587
|
-
|
|
596
|
+
/*
|
|
597
|
+
* `pollingTimeoutMs` is unused on the live-SSE path -- there's no poll
|
|
598
|
+
* loop to time out -- but we surface it as the per-attempt timeout for
|
|
599
|
+
* the AbortSignal-driven cancel that the caller already controls. Future
|
|
600
|
+
* iterations may use it as a backstop if the SSE+resume loop spins
|
|
601
|
+
* indefinitely.
|
|
602
|
+
*/
|
|
603
|
+
void pollingTimeoutMs;
|
|
604
|
+
|
|
605
|
+
const events = streamGoogleInteractionEvents({
|
|
606
|
+
baseURL: this.config.baseURL,
|
|
607
|
+
interactionId,
|
|
608
|
+
headers: mergedHeaders,
|
|
609
|
+
fetch: this.config.fetch,
|
|
610
|
+
abortSignal: options.abortSignal,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const transform = buildGoogleInteractionsStreamTransform({
|
|
588
614
|
warnings,
|
|
589
615
|
generateId: this.config.generateId ?? defaultGenerateId,
|
|
590
616
|
includeRawChunks: options.includeRawChunks,
|
|
591
|
-
|
|
617
|
+
serviceTier: headerServiceTier,
|
|
592
618
|
});
|
|
593
619
|
|
|
594
620
|
return {
|
|
595
|
-
stream,
|
|
621
|
+
stream: events.pipeThrough(transform),
|
|
596
622
|
request: { body: args },
|
|
597
623
|
response: { headers: postHeaders },
|
|
598
624
|
};
|
|
@@ -2,9 +2,11 @@ import {
|
|
|
2
2
|
createJsonResponseHandler,
|
|
3
3
|
delay,
|
|
4
4
|
getFromApi,
|
|
5
|
+
isAbortError,
|
|
5
6
|
type FetchFunction,
|
|
6
7
|
} from '@ai-sdk/provider-utils';
|
|
7
8
|
import { googleFailedResponseHandler } from '../google-error';
|
|
9
|
+
import { cancelGoogleInteraction } from './cancel-google-interaction';
|
|
8
10
|
import {
|
|
9
11
|
googleInteractionsResponseSchema,
|
|
10
12
|
type GoogleInteractionsResponse,
|
|
@@ -73,38 +75,55 @@ export async function pollGoogleInteractionUntilTerminal({
|
|
|
73
75
|
let nextDelayMs = initialDelayMs;
|
|
74
76
|
const url = `${baseURL}/interactions/${encodeURIComponent(interactionId)}`;
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
/*
|
|
79
|
+
* When the caller aborts, fire a best-effort `POST /interactions/{id}/cancel`
|
|
80
|
+
* so the run stops billing on Google's side. Wrap every exit path that's
|
|
81
|
+
* triggered by an abort -- the explicit `abortSignal.aborted` check, the
|
|
82
|
+
* AbortError thrown by `delay()`, and any AbortError thrown by `getFromApi`.
|
|
83
|
+
*/
|
|
84
|
+
const cancelOnServer = () =>
|
|
85
|
+
cancelGoogleInteraction({ baseURL, interactionId, headers, fetch });
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
try {
|
|
88
|
+
while (true) {
|
|
89
|
+
if (abortSignal?.aborted) {
|
|
90
|
+
await cancelOnServer();
|
|
91
|
+
throw new DOMException('Polling was aborted', 'AbortError');
|
|
92
|
+
}
|
|
86
93
|
|
|
87
|
-
|
|
94
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`google.interactions: timed out polling interaction ${interactionId} after ${timeoutMs}ms.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
88
99
|
|
|
89
|
-
|
|
90
|
-
value: response,
|
|
91
|
-
rawValue: rawResponse,
|
|
92
|
-
responseHeaders,
|
|
93
|
-
} = await getFromApi({
|
|
94
|
-
url,
|
|
95
|
-
headers,
|
|
96
|
-
failedResponseHandler: googleFailedResponseHandler,
|
|
97
|
-
successfulResponseHandler: createJsonResponseHandler(
|
|
98
|
-
googleInteractionsResponseSchema,
|
|
99
|
-
),
|
|
100
|
-
abortSignal,
|
|
101
|
-
fetch,
|
|
102
|
-
});
|
|
100
|
+
await delay(nextDelayMs, { abortSignal });
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
const {
|
|
103
|
+
value: response,
|
|
104
|
+
rawValue: rawResponse,
|
|
105
|
+
responseHeaders,
|
|
106
|
+
} = await getFromApi({
|
|
107
|
+
url,
|
|
108
|
+
headers,
|
|
109
|
+
failedResponseHandler: googleFailedResponseHandler,
|
|
110
|
+
successfulResponseHandler: createJsonResponseHandler(
|
|
111
|
+
googleInteractionsResponseSchema,
|
|
112
|
+
),
|
|
113
|
+
abortSignal,
|
|
114
|
+
fetch,
|
|
115
|
+
});
|
|
107
116
|
|
|
108
|
-
|
|
117
|
+
if (isTerminalStatus(response.status)) {
|
|
118
|
+
return { response, rawResponse, responseHeaders };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
nextDelayMs = Math.min(nextDelayMs * 2, maxDelayMs);
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (isAbortError(error)) {
|
|
125
|
+
await cancelOnServer();
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
109
128
|
}
|
|
110
129
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEventSourceResponseHandler,
|
|
3
|
+
delay,
|
|
4
|
+
getFromApi,
|
|
5
|
+
isAbortError,
|
|
6
|
+
type FetchFunction,
|
|
7
|
+
type ParseResult,
|
|
8
|
+
} from '@ai-sdk/provider-utils';
|
|
9
|
+
import { googleFailedResponseHandler } from '../google-error';
|
|
10
|
+
import { cancelGoogleInteraction } from './cancel-google-interaction';
|
|
11
|
+
import {
|
|
12
|
+
googleInteractionsEventSchema,
|
|
13
|
+
type GoogleInteractionsEvent,
|
|
14
|
+
} from './google-interactions-api';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
17
|
+
const DEFAULT_RETRY_DELAY_MS = 500;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Connects to `GET {baseURL}/interactions/{id}?stream=true` and surfaces the
|
|
21
|
+
* server-sent events as a `ReadableStream<ParseResult<GoogleInteractionsEvent>>`
|
|
22
|
+
* so the existing `buildGoogleInteractionsStreamTransform` can consume them
|
|
23
|
+
* unchanged.
|
|
24
|
+
*
|
|
25
|
+
* The connection can drop mid-run: deep-research agents idle for long
|
|
26
|
+
* stretches between SSE events and undici's default body timeout terminates
|
|
27
|
+
* the request with `UND_ERR_BODY_TIMEOUT`. We track the last seen `event_id`
|
|
28
|
+
* and reconnect with `?last_event_id=<id>` on any unexpected end. After
|
|
29
|
+
* `maxRetries` consecutive failures the stream errors out so the caller can
|
|
30
|
+
* decide whether to fall back to polling.
|
|
31
|
+
*
|
|
32
|
+
* The stream completes cleanly when an `interaction.complete` event with a
|
|
33
|
+
* terminal status arrives, or when an `error` event arrives.
|
|
34
|
+
*/
|
|
35
|
+
export function streamGoogleInteractionEvents({
|
|
36
|
+
baseURL,
|
|
37
|
+
interactionId,
|
|
38
|
+
headers,
|
|
39
|
+
fetch,
|
|
40
|
+
abortSignal,
|
|
41
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
42
|
+
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
|
|
43
|
+
}: {
|
|
44
|
+
baseURL: string;
|
|
45
|
+
interactionId: string;
|
|
46
|
+
headers: Record<string, string | undefined>;
|
|
47
|
+
fetch?: FetchFunction;
|
|
48
|
+
abortSignal?: AbortSignal;
|
|
49
|
+
maxRetries?: number;
|
|
50
|
+
retryDelayMs?: number;
|
|
51
|
+
}): ReadableStream<ParseResult<GoogleInteractionsEvent>> {
|
|
52
|
+
if (interactionId.length === 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'google.interactions: cannot stream a background interaction without an id.',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const eventSourceHeaders = {
|
|
59
|
+
...headers,
|
|
60
|
+
accept: 'text/event-stream',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let lastEventId: string | undefined;
|
|
64
|
+
let complete = false;
|
|
65
|
+
let attempt = 0;
|
|
66
|
+
let receivedAnyEventThisAttempt = false;
|
|
67
|
+
let currentReader:
|
|
68
|
+
| ReadableStreamDefaultReader<ParseResult<GoogleInteractionsEvent>>
|
|
69
|
+
| undefined;
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* Forwards `cancel()` from the consumer (and the upstream `abortSignal`) to
|
|
73
|
+
* any in-flight `getFromApi` or `delay` so the loop unblocks immediately
|
|
74
|
+
* instead of waiting for the next iteration to notice a flag.
|
|
75
|
+
*/
|
|
76
|
+
const internalAbort = new AbortController();
|
|
77
|
+
const upstreamAbortHandler = () => internalAbort.abort();
|
|
78
|
+
if (abortSignal != null) {
|
|
79
|
+
if (abortSignal.aborted) {
|
|
80
|
+
internalAbort.abort();
|
|
81
|
+
} else {
|
|
82
|
+
abortSignal.addEventListener('abort', upstreamAbortHandler, {
|
|
83
|
+
once: true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const effectiveSignal = internalAbort.signal;
|
|
88
|
+
|
|
89
|
+
function buildUrl(): string {
|
|
90
|
+
const base = `${baseURL}/interactions/${encodeURIComponent(interactionId)}`;
|
|
91
|
+
const params = new URLSearchParams({ stream: 'true' });
|
|
92
|
+
if (lastEventId != null) {
|
|
93
|
+
params.set('last_event_id', lastEventId);
|
|
94
|
+
}
|
|
95
|
+
return `${base}?${params.toString()}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function openReader() {
|
|
99
|
+
const { value: stream } = await getFromApi({
|
|
100
|
+
url: buildUrl(),
|
|
101
|
+
headers: eventSourceHeaders,
|
|
102
|
+
failedResponseHandler: googleFailedResponseHandler,
|
|
103
|
+
successfulResponseHandler: createEventSourceResponseHandler(
|
|
104
|
+
googleInteractionsEventSchema,
|
|
105
|
+
),
|
|
106
|
+
abortSignal: effectiveSignal,
|
|
107
|
+
fetch,
|
|
108
|
+
});
|
|
109
|
+
return stream.getReader();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new ReadableStream<ParseResult<GoogleInteractionsEvent>>({
|
|
113
|
+
async start(controller) {
|
|
114
|
+
try {
|
|
115
|
+
while (!complete && !effectiveSignal.aborted) {
|
|
116
|
+
if (currentReader == null) {
|
|
117
|
+
try {
|
|
118
|
+
currentReader = await openReader();
|
|
119
|
+
receivedAnyEventThisAttempt = false;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (isAbortError(error) || effectiveSignal.aborted) {
|
|
122
|
+
controller.error(error);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
attempt++;
|
|
126
|
+
if (attempt >= maxRetries) {
|
|
127
|
+
controller.error(error);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await delay(retryDelayMs * attempt, {
|
|
131
|
+
abortSignal: effectiveSignal,
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const { done, value } = await currentReader.read();
|
|
139
|
+
if (done) {
|
|
140
|
+
/*
|
|
141
|
+
* Underlying stream ended. If we already saw the terminal event
|
|
142
|
+
* we exit cleanly; otherwise this is an unexpected disconnect
|
|
143
|
+
* and we'll reconnect with `last_event_id`.
|
|
144
|
+
*
|
|
145
|
+
* If the connection closed without producing any events at all
|
|
146
|
+
* this attempt, count it as a failed attempt -- otherwise an
|
|
147
|
+
* empty/misbehaving server response would loop forever.
|
|
148
|
+
*/
|
|
149
|
+
currentReader = undefined;
|
|
150
|
+
if (complete) break;
|
|
151
|
+
if (!receivedAnyEventThisAttempt) {
|
|
152
|
+
attempt++;
|
|
153
|
+
if (attempt >= maxRetries) {
|
|
154
|
+
controller.error(
|
|
155
|
+
new Error(
|
|
156
|
+
'google.interactions: SSE stream closed without producing any events.',
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await delay(retryDelayMs * attempt, {
|
|
162
|
+
abortSignal: effectiveSignal,
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
attempt = 0;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
receivedAnyEventThisAttempt = true;
|
|
171
|
+
|
|
172
|
+
if (value.success) {
|
|
173
|
+
const ev = value.value as {
|
|
174
|
+
event_id?: string;
|
|
175
|
+
event_type?: string;
|
|
176
|
+
};
|
|
177
|
+
if (typeof ev.event_id === 'string' && ev.event_id.length > 0) {
|
|
178
|
+
lastEventId = ev.event_id;
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
ev.event_type === 'interaction.complete' ||
|
|
182
|
+
ev.event_type === 'error'
|
|
183
|
+
) {
|
|
184
|
+
complete = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
controller.enqueue(value);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (isAbortError(error) || effectiveSignal.aborted) {
|
|
191
|
+
controller.error(error);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
currentReader = undefined;
|
|
195
|
+
attempt++;
|
|
196
|
+
if (attempt >= maxRetries) {
|
|
197
|
+
controller.error(error);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await delay(retryDelayMs * attempt, {
|
|
201
|
+
abortSignal: effectiveSignal,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
controller.close();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
controller.error(error);
|
|
208
|
+
} finally {
|
|
209
|
+
if (abortSignal != null) {
|
|
210
|
+
abortSignal.removeEventListener('abort', upstreamAbortHandler);
|
|
211
|
+
}
|
|
212
|
+
currentReader?.cancel().catch(() => {});
|
|
213
|
+
currentReader = undefined;
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
* If we're exiting because the caller aborted (or the consumer
|
|
217
|
+
* cancelled the stream) before the agent finished, fire
|
|
218
|
+
* `POST /interactions/{id}/cancel` so the run stops billing on
|
|
219
|
+
* Google's side. Skipped when `complete` is set -- the agent already
|
|
220
|
+
* reported terminal status via `interaction.complete` / `error`.
|
|
221
|
+
*/
|
|
222
|
+
if (effectiveSignal.aborted && !complete) {
|
|
223
|
+
await cancelGoogleInteraction({
|
|
224
|
+
baseURL,
|
|
225
|
+
interactionId,
|
|
226
|
+
headers,
|
|
227
|
+
fetch,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
cancel() {
|
|
234
|
+
internalAbort.abort();
|
|
235
|
+
currentReader?.cancel().catch(() => {});
|
|
236
|
+
currentReader = undefined;
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|