@vybestack/llxprt-code-agents 0.10.0-nightly.260613.1adad3b34
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/dist/.last_build +0 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/src/agents/executor-prompt-builder.d.ts +19 -0
- package/dist/src/agents/executor-prompt-builder.js +44 -0
- package/dist/src/agents/executor-prompt-builder.js.map +1 -0
- package/dist/src/agents/executor-termination.d.ts +22 -0
- package/dist/src/agents/executor-termination.js +44 -0
- package/dist/src/agents/executor-termination.js.map +1 -0
- package/dist/src/agents/executor-validation.d.ts +17 -0
- package/dist/src/agents/executor-validation.js +35 -0
- package/dist/src/agents/executor-validation.js.map +1 -0
- package/dist/src/agents/executor.d.ts +136 -0
- package/dist/src/agents/executor.js +874 -0
- package/dist/src/agents/executor.js.map +1 -0
- package/dist/src/agents/invocation.d.ts +45 -0
- package/dist/src/agents/invocation.js +118 -0
- package/dist/src/agents/invocation.js.map +1 -0
- package/dist/src/agents/recovery.d.ts +75 -0
- package/dist/src/agents/recovery.js +107 -0
- package/dist/src/agents/recovery.js.map +1 -0
- package/dist/src/agents/types.d.ts +149 -0
- package/dist/src/agents/types.js +18 -0
- package/dist/src/agents/types.js.map +1 -0
- package/dist/src/agents/utils.d.ts +15 -0
- package/dist/src/agents/utils.js +28 -0
- package/dist/src/agents/utils.js.map +1 -0
- package/dist/src/compression/CompressionHandler.d.ts +178 -0
- package/dist/src/compression/CompressionHandler.js +630 -0
- package/dist/src/compression/CompressionHandler.js.map +1 -0
- package/dist/src/compression/HighDensityStrategy.d.ts +113 -0
- package/dist/src/compression/HighDensityStrategy.js +769 -0
- package/dist/src/compression/HighDensityStrategy.js.map +1 -0
- package/dist/src/compression/MiddleOutStrategy.d.ts +27 -0
- package/dist/src/compression/MiddleOutStrategy.js +347 -0
- package/dist/src/compression/MiddleOutStrategy.js.map +1 -0
- package/dist/src/compression/OneShotStrategy.d.ts +22 -0
- package/dist/src/compression/OneShotStrategy.js +235 -0
- package/dist/src/compression/OneShotStrategy.js.map +1 -0
- package/dist/src/compression/TopDownTruncationStrategy.d.ts +13 -0
- package/dist/src/compression/TopDownTruncationStrategy.js +74 -0
- package/dist/src/compression/TopDownTruncationStrategy.js.map +1 -0
- package/dist/src/compression/compressionBudgeting.d.ts +34 -0
- package/dist/src/compression/compressionBudgeting.js +139 -0
- package/dist/src/compression/compressionBudgeting.js.map +1 -0
- package/dist/src/compression/compressionStrategyFactory.d.ts +23 -0
- package/dist/src/compression/compressionStrategyFactory.js +40 -0
- package/dist/src/compression/compressionStrategyFactory.js.map +1 -0
- package/dist/src/compression/index.d.ts +16 -0
- package/dist/src/compression/index.js +17 -0
- package/dist/src/compression/index.js.map +1 -0
- package/dist/src/compression/reasoningUtils.d.ts +18 -0
- package/dist/src/compression/reasoningUtils.js +22 -0
- package/dist/src/compression/reasoningUtils.js.map +1 -0
- package/dist/src/compression/utils.d.ts +114 -0
- package/dist/src/compression/utils.js +316 -0
- package/dist/src/compression/utils.js.map +1 -0
- package/dist/src/core/AgentHookManager.d.ts +41 -0
- package/dist/src/core/AgentHookManager.js +65 -0
- package/dist/src/core/AgentHookManager.js.map +1 -0
- package/dist/src/core/ChatSessionFactory.d.ts +50 -0
- package/dist/src/core/ChatSessionFactory.js +216 -0
- package/dist/src/core/ChatSessionFactory.js.map +1 -0
- package/dist/src/core/ConversationManager.d.ts +113 -0
- package/dist/src/core/ConversationManager.js +311 -0
- package/dist/src/core/ConversationManager.js.map +1 -0
- package/dist/src/core/DirectMessageProcessor.d.ts +79 -0
- package/dist/src/core/DirectMessageProcessor.js +478 -0
- package/dist/src/core/DirectMessageProcessor.js.map +1 -0
- package/dist/src/core/IdeContextTracker.d.ts +56 -0
- package/dist/src/core/IdeContextTracker.js +207 -0
- package/dist/src/core/IdeContextTracker.js.map +1 -0
- package/dist/src/core/MessageConverter.d.ts +78 -0
- package/dist/src/core/MessageConverter.js +492 -0
- package/dist/src/core/MessageConverter.js.map +1 -0
- package/dist/src/core/MessageStreamOrchestrator.d.ts +85 -0
- package/dist/src/core/MessageStreamOrchestrator.js +448 -0
- package/dist/src/core/MessageStreamOrchestrator.js.map +1 -0
- package/dist/src/core/MessageStreamTerminalHandler.d.ts +16 -0
- package/dist/src/core/MessageStreamTerminalHandler.js +170 -0
- package/dist/src/core/MessageStreamTerminalHandler.js.map +1 -0
- package/dist/src/core/StreamProcessor.d.ts +105 -0
- package/dist/src/core/StreamProcessor.js +660 -0
- package/dist/src/core/StreamProcessor.js.map +1 -0
- package/dist/src/core/TodoContinuationService.d.ts +74 -0
- package/dist/src/core/TodoContinuationService.js +312 -0
- package/dist/src/core/TodoContinuationService.js.map +1 -0
- package/dist/src/core/TurnProcessor.d.ts +91 -0
- package/dist/src/core/TurnProcessor.js +540 -0
- package/dist/src/core/TurnProcessor.js.map +1 -0
- package/dist/src/core/baseLlmClient.d.ts +115 -0
- package/dist/src/core/baseLlmClient.js +231 -0
- package/dist/src/core/baseLlmClient.js.map +1 -0
- package/dist/src/core/bucketFailoverIntegration.d.ts +62 -0
- package/dist/src/core/bucketFailoverIntegration.js +186 -0
- package/dist/src/core/bucketFailoverIntegration.js.map +1 -0
- package/dist/src/core/chatSession.d.ts +121 -0
- package/dist/src/core/chatSession.js +720 -0
- package/dist/src/core/chatSession.js.map +1 -0
- package/dist/src/core/client.d.ts +132 -0
- package/dist/src/core/client.js +524 -0
- package/dist/src/core/client.js.map +1 -0
- package/dist/src/core/clientHelpers.d.ts +15 -0
- package/dist/src/core/clientHelpers.js +122 -0
- package/dist/src/core/clientHelpers.js.map +1 -0
- package/dist/src/core/clientLlmUtilities.d.ts +30 -0
- package/dist/src/core/clientLlmUtilities.js +125 -0
- package/dist/src/core/clientLlmUtilities.js.map +1 -0
- package/dist/src/core/clientToolGovernance.d.ts +37 -0
- package/dist/src/core/clientToolGovernance.js +104 -0
- package/dist/src/core/clientToolGovernance.js.map +1 -0
- package/dist/src/core/compression-config.d.ts +12 -0
- package/dist/src/core/compression-config.js +21 -0
- package/dist/src/core/compression-config.js.map +1 -0
- package/dist/src/core/coreToolScheduler.d.ts +105 -0
- package/dist/src/core/coreToolScheduler.js +453 -0
- package/dist/src/core/coreToolScheduler.js.map +1 -0
- package/dist/src/core/hookToolRestrictions.d.ts +18 -0
- package/dist/src/core/hookToolRestrictions.js +187 -0
- package/dist/src/core/hookToolRestrictions.js.map +1 -0
- package/dist/src/core/nonInteractiveToolExecutor.d.ts +17 -0
- package/dist/src/core/nonInteractiveToolExecutor.js +102 -0
- package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -0
- package/dist/src/core/subagent.d.ts +105 -0
- package/dist/src/core/subagent.js +650 -0
- package/dist/src/core/subagent.js.map +1 -0
- package/dist/src/core/subagentExecution.d.ts +104 -0
- package/dist/src/core/subagentExecution.js +275 -0
- package/dist/src/core/subagentExecution.js.map +1 -0
- package/dist/src/core/subagentOrchestrator.d.ts +84 -0
- package/dist/src/core/subagentOrchestrator.js +503 -0
- package/dist/src/core/subagentOrchestrator.js.map +1 -0
- package/dist/src/core/subagentRuntimeSetup.d.ts +75 -0
- package/dist/src/core/subagentRuntimeSetup.js +387 -0
- package/dist/src/core/subagentRuntimeSetup.js.map +1 -0
- package/dist/src/core/subagentScheduler.d.ts +17 -0
- package/dist/src/core/subagentScheduler.js +7 -0
- package/dist/src/core/subagentScheduler.js.map +1 -0
- package/dist/src/core/subagentToolProcessing.d.ts +51 -0
- package/dist/src/core/subagentToolProcessing.js +359 -0
- package/dist/src/core/subagentToolProcessing.js.map +1 -0
- package/dist/src/core/toolGovernance.d.ts +18 -0
- package/dist/src/core/toolGovernance.js +50 -0
- package/dist/src/core/toolGovernance.js.map +1 -0
- package/dist/src/core/turn.d.ts +47 -0
- package/dist/src/core/turn.js +455 -0
- package/dist/src/core/turn.js.map +1 -0
- package/dist/src/core/turnLogging.d.ts +35 -0
- package/dist/src/core/turnLogging.js +80 -0
- package/dist/src/core/turnLogging.js.map +1 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.js +44 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/scheduler/confirmation-coordinator.d.ts +139 -0
- package/dist/src/scheduler/confirmation-coordinator.js +443 -0
- package/dist/src/scheduler/confirmation-coordinator.js.map +1 -0
- package/dist/src/scheduler/result-aggregator.d.ts +127 -0
- package/dist/src/scheduler/result-aggregator.js +302 -0
- package/dist/src/scheduler/result-aggregator.js.map +1 -0
- package/dist/src/scheduler/status-transitions.d.ts +39 -0
- package/dist/src/scheduler/status-transitions.js +184 -0
- package/dist/src/scheduler/status-transitions.js.map +1 -0
- package/dist/src/scheduler/tool-dispatcher.d.ts +40 -0
- package/dist/src/scheduler/tool-dispatcher.js +120 -0
- package/dist/src/scheduler/tool-dispatcher.js.map +1 -0
- package/dist/src/scheduler/tool-executor.d.ts +44 -0
- package/dist/src/scheduler/tool-executor.js +117 -0
- package/dist/src/scheduler/tool-executor.js.map +1 -0
- package/dist/src/scheduler/utils.d.ts +11 -0
- package/dist/src/scheduler/utils.js +19 -0
- package/dist/src/scheduler/utils.js.map +1 -0
- package/dist/src/tools/task.d.ts +128 -0
- package/dist/src/tools/task.js +932 -0
- package/dist/src/tools/task.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { FinishReason, } from '@google/genai';
|
|
7
|
+
import { isRetryableError, retryWithBackoff, } from '@vybestack/llxprt-code-core/utils/retry.js';
|
|
8
|
+
import { prependAsyncGenerator } from '@vybestack/llxprt-code-core/utils/asyncIterator.js';
|
|
9
|
+
// @plan:PLAN-20260608-ISSUE1586.P15 — auth types from auth package
|
|
10
|
+
import { flushRuntimeAuthScope } from '@vybestack/llxprt-code-auth';
|
|
11
|
+
import { isFunctionResponse } from '@vybestack/llxprt-code-core/utils/messageInspectors.js';
|
|
12
|
+
import { DebugLogger } from '@vybestack/llxprt-code-core/debug/index.js';
|
|
13
|
+
import { ContentConverters } from '@vybestack/llxprt-code-core/services/history/ContentConverters.js';
|
|
14
|
+
import { isValidResponse, isValidNonThoughtTextPart, convertIContentToResponse, } from './MessageConverter.js';
|
|
15
|
+
import { logApiRequest, logApiResponse, logApiError } from './turnLogging.js';
|
|
16
|
+
import { InvalidStreamError, EmptyStreamError, isSchemaDepthError, isThoughtPart, } from '@vybestack/llxprt-code-core/core/chatSessionTypes.js';
|
|
17
|
+
import { analyzeResponseOutcome } from '@vybestack/llxprt-code-core/utils/generateContentResponseUtilities.js';
|
|
18
|
+
import { hasCycleInSchema } from '@vybestack/llxprt-code-tools';
|
|
19
|
+
import { isStructuredError } from '@vybestack/llxprt-code-core/utils/quotaErrorDetection.js';
|
|
20
|
+
import { AgentExecutionStoppedError, AgentExecutionBlockedError, } from './chatSession.js';
|
|
21
|
+
import { attachHookRestrictedAllowedTools, filterHookRestrictedContent, filterHookRestrictedFunctionCalls, filterHookRestrictedParts, getHookRestrictedAllowedTools, getHookRestrictedFunctionCallsFromParts, mergeHookRestrictedFunctionCalls, } from './hookToolRestrictions.js';
|
|
22
|
+
import { canonicalizeToolName } from './toolGovernance.js';
|
|
23
|
+
function isMissingFinishReason(finishReason) {
|
|
24
|
+
return finishReason == null || finishReason === '';
|
|
25
|
+
}
|
|
26
|
+
export class StreamProcessor {
|
|
27
|
+
runtimeContext;
|
|
28
|
+
conversationManager;
|
|
29
|
+
compressionHandler;
|
|
30
|
+
providerResolver;
|
|
31
|
+
providerRuntimeBuilder;
|
|
32
|
+
historyService;
|
|
33
|
+
generationConfig;
|
|
34
|
+
logger = new DebugLogger('llxprt:gemini:stream-processor');
|
|
35
|
+
constructor(runtimeContext, conversationManager, compressionHandler, providerResolver, providerRuntimeBuilder, historyService, generationConfig) {
|
|
36
|
+
this.runtimeContext = runtimeContext;
|
|
37
|
+
this.conversationManager = conversationManager;
|
|
38
|
+
this.compressionHandler = compressionHandler;
|
|
39
|
+
this.providerResolver = providerResolver;
|
|
40
|
+
this.providerRuntimeBuilder = providerRuntimeBuilder;
|
|
41
|
+
this.historyService = historyService;
|
|
42
|
+
this.generationConfig = generationConfig;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Makes an API call with retry and returns a stream processor.
|
|
46
|
+
* This is the outer method that resolves the provider, enforces context window,
|
|
47
|
+
* makes the API call with retry, and returns processStreamResponse.
|
|
48
|
+
*/
|
|
49
|
+
async makeApiCallAndProcessStream(params, promptId, pendingTokens, userContent) {
|
|
50
|
+
const provider = this.providerResolver('stream');
|
|
51
|
+
const providerBaseUrl = this.runtimeContext.state.baseUrl;
|
|
52
|
+
this.logger.debug(() => '[StreamProcessor] Active provider snapshot before stream request', {
|
|
53
|
+
providerName: provider.name,
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Preserve defensive runtime boundary guard despite current static types.
|
|
55
|
+
providerDefaultModel: provider.getDefaultModel?.(),
|
|
56
|
+
configModel: this.runtimeContext.state.model,
|
|
57
|
+
baseUrl: providerBaseUrl,
|
|
58
|
+
});
|
|
59
|
+
// Enforce context window limits before proceeding
|
|
60
|
+
await this.compressionHandler.enforceContextWindow(pendingTokens, promptId, provider);
|
|
61
|
+
// Check if provider supports IContent interface
|
|
62
|
+
if (typeof provider.generateChatCompletion !== 'function') {
|
|
63
|
+
throw new Error(`Provider ${provider.name} does not support IContent interface`);
|
|
64
|
+
}
|
|
65
|
+
const streamResponse = await this._executeStreamApiCall(params, promptId, userContent, provider);
|
|
66
|
+
return this._createCancellableStream(streamResponse, userContent);
|
|
67
|
+
}
|
|
68
|
+
_createCancellableStream(streamResponse, userContent) {
|
|
69
|
+
let processedStream;
|
|
70
|
+
const ensureProcessedStream = () => {
|
|
71
|
+
processedStream ??= this.processStreamResponse(streamResponse, userContent);
|
|
72
|
+
return processedStream;
|
|
73
|
+
};
|
|
74
|
+
const cancellableStream = {
|
|
75
|
+
async next(value) {
|
|
76
|
+
return ensureProcessedStream().next(value);
|
|
77
|
+
},
|
|
78
|
+
async return(value) {
|
|
79
|
+
if (processedStream) {
|
|
80
|
+
return typeof processedStream.return === 'function'
|
|
81
|
+
? processedStream.return(value)
|
|
82
|
+
: { done: true, value: undefined };
|
|
83
|
+
}
|
|
84
|
+
if (typeof streamResponse.return === 'function') {
|
|
85
|
+
await streamResponse.return(value);
|
|
86
|
+
}
|
|
87
|
+
return { done: true, value: undefined };
|
|
88
|
+
},
|
|
89
|
+
async throw(error) {
|
|
90
|
+
if (processedStream) {
|
|
91
|
+
if (typeof processedStream.throw === 'function') {
|
|
92
|
+
return processedStream.throw(error);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
if (typeof streamResponse.throw === 'function') {
|
|
97
|
+
return streamResponse.throw(error);
|
|
98
|
+
}
|
|
99
|
+
if (typeof streamResponse.return === 'function') {
|
|
100
|
+
await streamResponse.return(undefined);
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
},
|
|
104
|
+
[Symbol.asyncIterator]() {
|
|
105
|
+
return this;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
return cancellableStream;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Execute the stream API call with retry and bucket failover.
|
|
112
|
+
* Split from makeApiCallAndProcessStream to keep methods under 80 lines.
|
|
113
|
+
*/
|
|
114
|
+
async _executeStreamApiCall(params, promptId, userContent, provider) {
|
|
115
|
+
const apiCall = () => this._buildAndSendStreamRequest(params, promptId, userContent, provider);
|
|
116
|
+
const retryFetchErrors = params.config?.retryFetchErrors;
|
|
117
|
+
return retryWithBackoff(apiCall, {
|
|
118
|
+
onPersistent429: () => this._handleBucketFailover(),
|
|
119
|
+
signal: params.config?.abortSignal,
|
|
120
|
+
retryFetchErrors,
|
|
121
|
+
shouldRetryOnError: (error, currentRetryFetchErrors) => error instanceof EmptyStreamError ||
|
|
122
|
+
isRetryableError(error, currentRetryFetchErrors),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async _buildAndSendStreamRequest(params, promptId, userContent, provider) {
|
|
126
|
+
const requestContents = this._buildRequestContents(userContent);
|
|
127
|
+
const configForHooks = this.runtimeContext.providerRuntime.config;
|
|
128
|
+
const requestTools = this._selectRequestTools(params);
|
|
129
|
+
const toolSelection = await this._applyToolSelectionHook(configForHooks, requestTools);
|
|
130
|
+
const tools = toolSelection.tools;
|
|
131
|
+
const { requestPayload, baseRuntimeContext, runtimeContext } = this._prepareRequestPayload(requestContents, tools, params);
|
|
132
|
+
const finalContents = await this._fireBeforeModelHook(configForHooks, requestPayload.contents, tools, toolSelection.allowedFunctionNames);
|
|
133
|
+
requestPayload.contents = finalContents;
|
|
134
|
+
logApiRequest(this.runtimeContext, this.runtimeContext.state, ContentConverters.toGeminiContents(requestPayload.contents), this.runtimeContext.state.model, promptId);
|
|
135
|
+
return this._sendProviderRequest(provider, requestPayload, runtimeContext, baseRuntimeContext, params, promptId, toolSelection.allowedFunctionNames);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Fire BeforeModel hook and return possibly-modified requestContents.
|
|
139
|
+
* Throws if the hook requests execution stop or blocking.
|
|
140
|
+
*/
|
|
141
|
+
async _fireBeforeModelHook(configForHooks, requestContents, tools, hookRestrictedAllowedTools) {
|
|
142
|
+
if (configForHooks === undefined ||
|
|
143
|
+
typeof configForHooks.getEnableHooks !== 'function' ||
|
|
144
|
+
configForHooks.getEnableHooks() !== true) {
|
|
145
|
+
return requestContents;
|
|
146
|
+
}
|
|
147
|
+
const hookSystem = typeof configForHooks.getHookSystem === 'function'
|
|
148
|
+
? configForHooks.getHookSystem()
|
|
149
|
+
: undefined;
|
|
150
|
+
if (hookSystem === undefined)
|
|
151
|
+
return requestContents;
|
|
152
|
+
await hookSystem.initialize();
|
|
153
|
+
const beforeModelResult = await hookSystem.fireBeforeModelEvent({
|
|
154
|
+
contents: requestContents,
|
|
155
|
+
tools,
|
|
156
|
+
});
|
|
157
|
+
if (beforeModelResult?.shouldStopExecution() === true) {
|
|
158
|
+
const reason = beforeModelResult.getEffectiveReason();
|
|
159
|
+
throw new AgentExecutionStoppedError(reason !== undefined && reason !== null && reason !== ''
|
|
160
|
+
? reason
|
|
161
|
+
: 'Execution stopped by BeforeModel hook', beforeModelResult.systemMessage);
|
|
162
|
+
}
|
|
163
|
+
if (beforeModelResult?.isBlockingDecision() === true) {
|
|
164
|
+
let syntheticResponse = beforeModelResult.getSyntheticResponse();
|
|
165
|
+
// eslint-disable-next-line sonarjs/nested-control-flow -- Existing structure is intentionally preserved; refactoring this boundary is outside the lint slice.
|
|
166
|
+
if (syntheticResponse) {
|
|
167
|
+
const candidate = syntheticResponse.candidates?.[0];
|
|
168
|
+
const candidateFinishReason = candidate?.finishReason;
|
|
169
|
+
if (candidate && isMissingFinishReason(candidateFinishReason)) {
|
|
170
|
+
syntheticResponse = this._patchMissingFinishReason(syntheticResponse, candidate);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const reason = beforeModelResult.getEffectiveReason();
|
|
174
|
+
throw new AgentExecutionBlockedError(reason !== undefined && reason !== null && reason !== ''
|
|
175
|
+
? reason
|
|
176
|
+
: 'Request blocked by BeforeModel hook', syntheticResponse === undefined
|
|
177
|
+
? undefined
|
|
178
|
+
: attachHookRestrictedAllowedTools(syntheticResponse, hookRestrictedAllowedTools));
|
|
179
|
+
}
|
|
180
|
+
return this._applyRequestModifications(beforeModelResult, requestContents);
|
|
181
|
+
}
|
|
182
|
+
_patchMissingFinishReason(syntheticResponse, candidate) {
|
|
183
|
+
return {
|
|
184
|
+
...syntheticResponse,
|
|
185
|
+
candidates: [{ ...candidate, finishReason: FinishReason.STOP }],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
_applyRequestModifications(beforeModelResult, requestContents) {
|
|
189
|
+
if (!beforeModelResult)
|
|
190
|
+
return requestContents;
|
|
191
|
+
const modifiedRequest = beforeModelResult.applyLLMRequestModifications({
|
|
192
|
+
model: this.runtimeContext.state.model || '',
|
|
193
|
+
contents: ContentConverters.toGeminiContents(requestContents),
|
|
194
|
+
});
|
|
195
|
+
const modifiedContents = modifiedRequest.contents;
|
|
196
|
+
if (modifiedContents !== undefined && modifiedContents !== null) {
|
|
197
|
+
return ContentConverters.toIContents(modifiedContents);
|
|
198
|
+
}
|
|
199
|
+
return requestContents;
|
|
200
|
+
}
|
|
201
|
+
_prepareRequestPayload(requestContents, tools, params) {
|
|
202
|
+
this.logger.debug(() => '[StreamProcessor] Calling provider.generateChatCompletion', {
|
|
203
|
+
providerName: this.providerResolver('stream').name,
|
|
204
|
+
model: this.runtimeContext.state.model,
|
|
205
|
+
historyLength: requestContents.length,
|
|
206
|
+
toolCount: tools?.length ?? 0,
|
|
207
|
+
baseUrl: this.runtimeContext.state.baseUrl,
|
|
208
|
+
});
|
|
209
|
+
const baseRuntimeContext = this.providerRuntimeBuilder('StreamProcessor.generateRequest', { historyLength: requestContents.length });
|
|
210
|
+
const requestPayload = { contents: requestContents, tools };
|
|
211
|
+
const runtimeContext = this._buildRuntimeContext(baseRuntimeContext, params, tools);
|
|
212
|
+
return { requestPayload, baseRuntimeContext, runtimeContext };
|
|
213
|
+
}
|
|
214
|
+
_buildRuntimeContext(baseRuntimeContext, params, tools) {
|
|
215
|
+
if (!params.config)
|
|
216
|
+
return baseRuntimeContext;
|
|
217
|
+
return {
|
|
218
|
+
...baseRuntimeContext,
|
|
219
|
+
config: { ...baseRuntimeContext.config, ...params.config, tools },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async _sendProviderRequest(provider, requestPayload, runtimeContext, baseRuntimeContext, params, promptId, hookRestrictedAllowedTools) {
|
|
223
|
+
const startTime = Date.now();
|
|
224
|
+
try {
|
|
225
|
+
const streamResponse = provider.generateChatCompletion({
|
|
226
|
+
contents: requestPayload.contents,
|
|
227
|
+
tools: requestPayload.tools,
|
|
228
|
+
config: runtimeContext.config,
|
|
229
|
+
runtime: runtimeContext,
|
|
230
|
+
settings: runtimeContext.settingsService,
|
|
231
|
+
metadata: {
|
|
232
|
+
...runtimeContext.metadata,
|
|
233
|
+
abortSignal: params.config?.abortSignal,
|
|
234
|
+
},
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Preserve defensive runtime boundary guard despite current static types.
|
|
236
|
+
userMemory: baseRuntimeContext.config?.getUserMemory?.(),
|
|
237
|
+
});
|
|
238
|
+
return await this._consumeFirstChunkAndReturn(streamResponse, requestPayload, promptId, startTime, hookRestrictedAllowedTools);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const durationMs = Date.now() - startTime;
|
|
242
|
+
logApiError(this.runtimeContext, this.runtimeContext.state, this.runtimeContext.state.model, promptId, durationMs, error);
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Eagerly consume first chunk within retry boundary (#1750).
|
|
248
|
+
*/
|
|
249
|
+
async _consumeFirstChunkAndReturn(streamResponse, requestPayload, promptId, startTime, hookRestrictedAllowedTools) {
|
|
250
|
+
const convertedStream = this._convertIContentStream(streamResponse, requestPayload, { promptId, startTime }, hookRestrictedAllowedTools);
|
|
251
|
+
const firstChunk = await convertedStream.next();
|
|
252
|
+
if (firstChunk.done === true) {
|
|
253
|
+
throw new EmptyStreamError('Model stream ended immediately with no content.');
|
|
254
|
+
}
|
|
255
|
+
return prependAsyncGenerator(firstChunk.value, convertedStream);
|
|
256
|
+
}
|
|
257
|
+
_selectRequestTools(params) {
|
|
258
|
+
return params.config?.tools ?? this.generationConfig.tools;
|
|
259
|
+
}
|
|
260
|
+
async _applyToolSelectionHook(configForHooks, tools) {
|
|
261
|
+
if (configForHooks === undefined) {
|
|
262
|
+
return { tools, allowedFunctionNames: undefined };
|
|
263
|
+
}
|
|
264
|
+
const getToolSelectionHooksEnabled = configForHooks.getEnableHooks;
|
|
265
|
+
if (typeof getToolSelectionHooksEnabled !== 'function' ||
|
|
266
|
+
getToolSelectionHooksEnabled.call(configForHooks) !== true) {
|
|
267
|
+
return { tools, allowedFunctionNames: undefined };
|
|
268
|
+
}
|
|
269
|
+
const getToolSelectionHookSystem = configForHooks.getHookSystem;
|
|
270
|
+
const hookSystem = typeof getToolSelectionHookSystem === 'function'
|
|
271
|
+
? getToolSelectionHookSystem.call(configForHooks)
|
|
272
|
+
: undefined;
|
|
273
|
+
if (hookSystem === undefined) {
|
|
274
|
+
return { tools, allowedFunctionNames: undefined };
|
|
275
|
+
}
|
|
276
|
+
await hookSystem.initialize();
|
|
277
|
+
const toolsFromConfig = Array.isArray(tools)
|
|
278
|
+
? tools
|
|
279
|
+
: [];
|
|
280
|
+
const toolSelectionResult = await hookSystem.fireBeforeToolSelectionEvent(toolsFromConfig);
|
|
281
|
+
const modifiedConfig = toolSelectionResult?.applyToolConfigModifications({
|
|
282
|
+
tools: toolsFromConfig,
|
|
283
|
+
});
|
|
284
|
+
const toolConfig = modifiedConfig?.toolConfig;
|
|
285
|
+
if (
|
|
286
|
+
// eslint-disable-next-line sonarjs/expression-complexity -- Existing structure is intentionally preserved; refactoring this boundary is outside the lint slice.
|
|
287
|
+
toolConfig !== undefined &&
|
|
288
|
+
toolConfig !== null &&
|
|
289
|
+
typeof toolConfig === 'object' &&
|
|
290
|
+
'allowedFunctionNames' in toolConfig &&
|
|
291
|
+
Array.isArray(toolConfig.allowedFunctionNames)) {
|
|
292
|
+
const allowedFunctions = toolConfig.allowedFunctionNames;
|
|
293
|
+
const allowedNames = new Set(allowedFunctions.map(canonicalizeToolName));
|
|
294
|
+
const filteredTools = toolsFromConfig
|
|
295
|
+
.map((toolGroup) => ({
|
|
296
|
+
...toolGroup,
|
|
297
|
+
functionDeclarations: Array.isArray(toolGroup.functionDeclarations)
|
|
298
|
+
? toolGroup.functionDeclarations.filter((fn) => typeof fn.name === 'string' &&
|
|
299
|
+
allowedNames.has(canonicalizeToolName(fn.name)))
|
|
300
|
+
: [],
|
|
301
|
+
}))
|
|
302
|
+
.filter((g) => g.functionDeclarations.length > 0);
|
|
303
|
+
return { tools: filteredTools, allowedFunctionNames: allowedFunctions };
|
|
304
|
+
}
|
|
305
|
+
return { tools: toolsFromConfig, allowedFunctionNames: undefined };
|
|
306
|
+
}
|
|
307
|
+
_buildRequestContents(userContent) {
|
|
308
|
+
const matcher = this.conversationManager.makePositionMatcher();
|
|
309
|
+
if (Array.isArray(userContent)) {
|
|
310
|
+
const userIContents = userContent.map((content) => {
|
|
311
|
+
const turnKey = this.historyService.generateTurnKey();
|
|
312
|
+
const idGen = this.historyService.getIdGeneratorCallback(turnKey);
|
|
313
|
+
return ContentConverters.toIContent(content, idGen, matcher, turnKey);
|
|
314
|
+
});
|
|
315
|
+
return this.historyService.getCuratedForProvider(userIContents);
|
|
316
|
+
}
|
|
317
|
+
const turnKey = this.historyService.generateTurnKey();
|
|
318
|
+
const idGen = this.historyService.getIdGeneratorCallback(turnKey);
|
|
319
|
+
const userIContent = ContentConverters.toIContent(userContent, idGen, matcher, turnKey);
|
|
320
|
+
return this.historyService.getCuratedForProvider([userIContent]);
|
|
321
|
+
}
|
|
322
|
+
async _handleBucketFailover() {
|
|
323
|
+
const failoverHandler = this.runtimeContext.providerRuntime.config?.getBucketFailoverHandler();
|
|
324
|
+
if (!failoverHandler)
|
|
325
|
+
return null;
|
|
326
|
+
this.logger.debug(() => 'Attempting bucket failover on persistent 429');
|
|
327
|
+
const success = await failoverHandler.tryFailover();
|
|
328
|
+
if (success) {
|
|
329
|
+
const runtimeId = this.runtimeContext.providerRuntime.runtimeId ??
|
|
330
|
+
this.runtimeContext.state.runtimeId;
|
|
331
|
+
if (typeof runtimeId === 'string' && runtimeId.trim() !== '') {
|
|
332
|
+
flushRuntimeAuthScope(runtimeId);
|
|
333
|
+
}
|
|
334
|
+
this.logger.debug(() => `Bucket failover successful, new bucket: ${failoverHandler.getCurrentBucket()}`);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
this.logger.debug(() => 'Bucket failover failed - no more buckets available');
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Convert IContent stream to GenerateContentResponse stream.
|
|
342
|
+
* Tracks token usage metadata from IContent format.
|
|
343
|
+
* Triggers AfterModel hook per streamed chunk.
|
|
344
|
+
*/
|
|
345
|
+
async *_convertIContentStream(streamResponse, llmRequest, telemetryContext, hookRestrictedAllowedTools) {
|
|
346
|
+
let lastConvertedChunk;
|
|
347
|
+
for await (const iContent of streamResponse) {
|
|
348
|
+
this._trackPromptTokens(iContent);
|
|
349
|
+
const convertedChunk = attachHookRestrictedAllowedTools(convertIContentToResponse(iContent), hookRestrictedAllowedTools);
|
|
350
|
+
lastConvertedChunk = convertedChunk;
|
|
351
|
+
const hookResult = await this._processAfterModelHook(iContent, llmRequest, convertedChunk, hookRestrictedAllowedTools);
|
|
352
|
+
if (hookResult.type === 'modified') {
|
|
353
|
+
const restrictedResponse = attachHookRestrictedAllowedTools(hookResult.response, hookRestrictedAllowedTools);
|
|
354
|
+
lastConvertedChunk = restrictedResponse;
|
|
355
|
+
yield restrictedResponse;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
yield convertedChunk;
|
|
359
|
+
}
|
|
360
|
+
this._logTelemetry(telemetryContext, lastConvertedChunk);
|
|
361
|
+
}
|
|
362
|
+
_trackPromptTokens(iContent) {
|
|
363
|
+
const promptTokens = iContent.metadata?.usage?.promptTokens;
|
|
364
|
+
if (promptTokens === undefined)
|
|
365
|
+
return;
|
|
366
|
+
const cacheReads = iContent.metadata?.usage?.cache_read_input_tokens ?? 0;
|
|
367
|
+
const cacheWrites = iContent.metadata?.usage?.cache_creation_input_tokens ?? 0;
|
|
368
|
+
const combinedPromptTokens = promptTokens + cacheReads + cacheWrites;
|
|
369
|
+
this.logger.debug(() => `[StreamProcessor] Tracking promptTokens from IContent: ${combinedPromptTokens}`);
|
|
370
|
+
this.compressionHandler.lastPromptTokenCount = combinedPromptTokens;
|
|
371
|
+
}
|
|
372
|
+
async _processAfterModelHook(iContent, llmRequest, convertedChunk, hookRestrictedAllowedTools) {
|
|
373
|
+
const hookConfig = this.runtimeContext.providerRuntime.config;
|
|
374
|
+
if (hookConfig === undefined ||
|
|
375
|
+
typeof hookConfig.getEnableHooks !== 'function' ||
|
|
376
|
+
hookConfig.getEnableHooks() !== true) {
|
|
377
|
+
return { type: 'passthrough' };
|
|
378
|
+
}
|
|
379
|
+
const hookSystem = typeof hookConfig.getHookSystem === 'function'
|
|
380
|
+
? hookConfig.getHookSystem()
|
|
381
|
+
: undefined;
|
|
382
|
+
if (hookSystem === undefined)
|
|
383
|
+
return { type: 'passthrough' };
|
|
384
|
+
// eslint-disable-next-line sonarjs/nested-control-flow -- Existing structure is intentionally preserved; refactoring this boundary is outside the lint slice.
|
|
385
|
+
if (!hookSystem.isInitialized()) {
|
|
386
|
+
await hookSystem.initialize();
|
|
387
|
+
}
|
|
388
|
+
const filteredContent = filterHookRestrictedContent(convertIContentToResponse(iContent).candidates?.[0]?.content ?? {
|
|
389
|
+
role: 'model',
|
|
390
|
+
parts: [],
|
|
391
|
+
}, hookRestrictedAllowedTools);
|
|
392
|
+
const afterModelResult = await hookSystem.fireAfterModelEvent(llmRequest ?? {}, ContentConverters.toIContent(filteredContent));
|
|
393
|
+
// eslint-disable-next-line sonarjs/nested-control-flow -- Existing structure is intentionally preserved; refactoring this boundary is outside the lint slice.
|
|
394
|
+
if (afterModelResult?.shouldStopExecution() === true) {
|
|
395
|
+
const effectiveReason = afterModelResult.getEffectiveReason();
|
|
396
|
+
throw new AgentExecutionStoppedError(effectiveReason ?? 'Execution stopped by AfterModel hook', afterModelResult.systemMessage);
|
|
397
|
+
}
|
|
398
|
+
// eslint-disable-next-line sonarjs/nested-control-flow -- Existing structure is intentionally preserved; refactoring this boundary is outside the lint slice.
|
|
399
|
+
if (afterModelResult?.isBlockingDecision() === true) {
|
|
400
|
+
const modifiedResponse = afterModelResult.getModifiedResponse();
|
|
401
|
+
const syntheticResponse = attachHookRestrictedAllowedTools(modifiedResponse ?? convertedChunk, hookRestrictedAllowedTools);
|
|
402
|
+
const effectiveReason = afterModelResult.getEffectiveReason();
|
|
403
|
+
throw new AgentExecutionBlockedError(effectiveReason ?? 'Execution blocked by AfterModel hook', syntheticResponse, afterModelResult.systemMessage);
|
|
404
|
+
}
|
|
405
|
+
const modifiedResponse = afterModelResult?.getModifiedResponse();
|
|
406
|
+
// eslint-disable-next-line sonarjs/nested-control-flow -- Existing structure is intentionally preserved; refactoring this boundary is outside the lint slice.
|
|
407
|
+
if (modifiedResponse) {
|
|
408
|
+
return { type: 'modified', response: modifiedResponse };
|
|
409
|
+
}
|
|
410
|
+
return { type: 'passthrough' };
|
|
411
|
+
}
|
|
412
|
+
_logTelemetry(telemetryContext, lastConvertedChunk) {
|
|
413
|
+
if (telemetryContext && lastConvertedChunk) {
|
|
414
|
+
const durationMs = Date.now() - telemetryContext.startTime;
|
|
415
|
+
logApiResponse(this.runtimeContext, this.runtimeContext.state, this.runtimeContext.state.model, telemetryContext.promptId, durationMs, lastConvertedChunk.usageMetadata, JSON.stringify(lastConvertedChunk));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Process streaming response chunks into a complete conversation turn.
|
|
420
|
+
* Yields each chunk immediately as it arrives from the provider stream,
|
|
421
|
+
* while simultaneously tracking metadata for validation and history.
|
|
422
|
+
*
|
|
423
|
+
* CRITICAL: This method must yield chunks inline during the for-await loop.
|
|
424
|
+
* Collecting all chunks first (as was done in a prior refactoring) blocks
|
|
425
|
+
* the entire pipeline — no output reaches the user, no abort signal checks
|
|
426
|
+
* run, and stalled provider streams hang indefinitely. See #1846.
|
|
427
|
+
*/
|
|
428
|
+
async *processStreamResponse(streamResponse, userInput) {
|
|
429
|
+
const acc = this._createStreamAccumulator();
|
|
430
|
+
const includeThoughts = this.runtimeContext.ephemerals.reasoning.includeInContext();
|
|
431
|
+
for await (const chunk of streamResponse) {
|
|
432
|
+
const filteredChunk = attachHookRestrictedAllowedTools(chunk, getHookRestrictedAllowedTools(chunk));
|
|
433
|
+
this._accumulateChunkMetadata(filteredChunk, acc, includeThoughts);
|
|
434
|
+
yield filteredChunk;
|
|
435
|
+
}
|
|
436
|
+
await this._finalizeStreamProcessing(acc, userInput);
|
|
437
|
+
}
|
|
438
|
+
_createStreamAccumulator() {
|
|
439
|
+
return {
|
|
440
|
+
modelResponseParts: [],
|
|
441
|
+
outcome: {
|
|
442
|
+
hasVisibleText: false,
|
|
443
|
+
hasThinking: false,
|
|
444
|
+
hasToolCalls: false,
|
|
445
|
+
isActionable: false,
|
|
446
|
+
},
|
|
447
|
+
finishReason: undefined,
|
|
448
|
+
allChunks: [],
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
_accumulateChunkMetadata(chunk, acc, includeThoughts) {
|
|
452
|
+
const candidateWithReason = chunk.candidates?.find((c) => c.finishReason !== undefined);
|
|
453
|
+
if (candidateWithReason !== undefined)
|
|
454
|
+
acc.finishReason = candidateWithReason.finishReason;
|
|
455
|
+
const allowedTools = getHookRestrictedAllowedTools(chunk);
|
|
456
|
+
const parts = chunk.candidates?.[0]?.content?.parts ?? [];
|
|
457
|
+
const effectiveParts = isValidResponse(chunk)
|
|
458
|
+
? filterHookRestrictedParts(parts, allowedTools)
|
|
459
|
+
: [];
|
|
460
|
+
const allowedPartCalls = getHookRestrictedFunctionCallsFromParts(effectiveParts, allowedTools);
|
|
461
|
+
const allowedMergedCalls = mergeHookRestrictedFunctionCalls(allowedPartCalls, filterHookRestrictedFunctionCalls(chunk.functionCalls ?? [], allowedTools));
|
|
462
|
+
const allowedTopLevelCallParts = allowedMergedCalls
|
|
463
|
+
.slice(allowedPartCalls.length)
|
|
464
|
+
.map((functionCall) => ({ functionCall }));
|
|
465
|
+
const outcomeParts = [...effectiveParts, ...allowedTopLevelCallParts];
|
|
466
|
+
if (outcomeParts.length > 0) {
|
|
467
|
+
const chunkOutcome = analyzeResponseOutcome(outcomeParts);
|
|
468
|
+
acc.outcome = {
|
|
469
|
+
hasVisibleText: acc.outcome.hasVisibleText || chunkOutcome.hasVisibleText,
|
|
470
|
+
hasThinking: acc.outcome.hasThinking || chunkOutcome.hasThinking,
|
|
471
|
+
hasToolCalls: acc.outcome.hasToolCalls || chunkOutcome.hasToolCalls,
|
|
472
|
+
isActionable: acc.outcome.isActionable || chunkOutcome.isActionable,
|
|
473
|
+
};
|
|
474
|
+
acc.modelResponseParts.push(...(includeThoughts
|
|
475
|
+
? outcomeParts
|
|
476
|
+
: outcomeParts.filter((p) => !isThoughtPart(p))));
|
|
477
|
+
}
|
|
478
|
+
const chunkText = typeof chunk.text === 'string' ? chunk.text : '';
|
|
479
|
+
this.logger.debug(() => `[stream:terminal] observed converted chunk`, {
|
|
480
|
+
chunkFinishReason: candidateWithReason?.finishReason,
|
|
481
|
+
partCount: effectiveParts.length,
|
|
482
|
+
toolCallCount: allowedMergedCalls.length,
|
|
483
|
+
textLength: chunkText.length,
|
|
484
|
+
hasUsageMetadata: Boolean(chunk.usageMetadata),
|
|
485
|
+
});
|
|
486
|
+
if (chunk.usageMetadata?.promptTokenCount !== undefined) {
|
|
487
|
+
const chunkUsage = chunk.usageMetadata;
|
|
488
|
+
this.compressionHandler.lastPromptTokenCount =
|
|
489
|
+
chunk.usageMetadata.promptTokenCount +
|
|
490
|
+
(chunkUsage.cache_read_input_tokens ?? 0) +
|
|
491
|
+
(chunkUsage.cache_creation_input_tokens ?? 0);
|
|
492
|
+
}
|
|
493
|
+
acc.allChunks.push(chunk);
|
|
494
|
+
}
|
|
495
|
+
async _finalizeStreamProcessing(acc, userInput) {
|
|
496
|
+
const consolidatedParts = this._consolidateTextParts(acc.modelResponseParts);
|
|
497
|
+
const responseText = this._extractResponseText(consolidatedParts);
|
|
498
|
+
if (isMissingFinishReason(acc.finishReason)) {
|
|
499
|
+
this.logger.debug(() => `[stream:terminal] stream ended without finishReason (hasToolCall=${String(acc.outcome.hasToolCalls)}, hasTextResponse=${String(acc.outcome.hasVisibleText)}, hasThinkingResponse=${String(acc.outcome.hasThinking)}, responseTextLength=${responseText.length})`);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
this.logger.debug(() => `[stream:terminal] finalized stream with finishReason`, {
|
|
503
|
+
finishReason: acc.finishReason,
|
|
504
|
+
hasToolCall: acc.outcome.hasToolCalls,
|
|
505
|
+
hasTextResponse: acc.outcome.hasVisibleText,
|
|
506
|
+
hasThinkingResponse: acc.outcome.hasThinking,
|
|
507
|
+
responseTextLength: responseText.length,
|
|
508
|
+
chunkCount: acc.allChunks.length,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
this._validateStreamCompletion(userInput, acc.outcome, acc.finishReason, responseText);
|
|
512
|
+
await this._recordHistoryWithUsage(userInput, consolidatedParts, acc.allChunks);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Consolidate adjacent text parts.
|
|
516
|
+
*/
|
|
517
|
+
_consolidateTextParts(modelResponseParts) {
|
|
518
|
+
const consolidatedParts = [];
|
|
519
|
+
for (const part of modelResponseParts) {
|
|
520
|
+
const lastPart = consolidatedParts[consolidatedParts.length - 1];
|
|
521
|
+
if (
|
|
522
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Preserve defensive runtime boundary guard despite current static types.
|
|
523
|
+
lastPart?.text &&
|
|
524
|
+
isValidNonThoughtTextPart(lastPart) &&
|
|
525
|
+
isValidNonThoughtTextPart(part)) {
|
|
526
|
+
lastPart.text += part.text;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
consolidatedParts.push(part);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return consolidatedParts;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Extract response text from consolidated parts.
|
|
536
|
+
*/
|
|
537
|
+
_extractResponseText(consolidatedParts) {
|
|
538
|
+
return consolidatedParts
|
|
539
|
+
.filter((part) => isValidNonThoughtTextPart(part))
|
|
540
|
+
.map((part) => part.text)
|
|
541
|
+
.join('')
|
|
542
|
+
.trim();
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Validate stream completion and throw appropriate errors.
|
|
546
|
+
*/
|
|
547
|
+
_validateStreamCompletion(userInput, outcome, finishReason, responseText) {
|
|
548
|
+
const isToolContinuationInput = Array.isArray(userInput)
|
|
549
|
+
? userInput.some(isFunctionResponse)
|
|
550
|
+
: isFunctionResponse(userInput);
|
|
551
|
+
const validationContext = {
|
|
552
|
+
hasToolCall: outcome.hasToolCalls,
|
|
553
|
+
hasTextResponse: outcome.hasVisibleText,
|
|
554
|
+
hasThinkingResponse: outcome.hasThinking,
|
|
555
|
+
finishReason,
|
|
556
|
+
responseTextLength: responseText.length,
|
|
557
|
+
isToolContinuationInput,
|
|
558
|
+
};
|
|
559
|
+
this.logger.debug(() => `[stream:terminal] validating converted stream completion`, validationContext);
|
|
560
|
+
const hasMissingFinishAndNoText = isMissingFinishReason(finishReason) && !outcome.hasVisibleText;
|
|
561
|
+
const isEmptyResponse = responseText === '';
|
|
562
|
+
const noRelevantContent = !outcome.hasToolCalls && !isToolContinuationInput && !outcome.hasThinking;
|
|
563
|
+
const isInvalidResponse = noRelevantContent && (hasMissingFinishAndNoText || isEmptyResponse);
|
|
564
|
+
if (isInvalidResponse) {
|
|
565
|
+
this._throwMissingResponseError(finishReason, outcome.hasVisibleText, validationContext);
|
|
566
|
+
}
|
|
567
|
+
if (finishReason === FinishReason.MALFORMED_FUNCTION_CALL) {
|
|
568
|
+
this.logger.warn(() => `[stream:terminal] validation failed: malformed function call finishReason`, validationContext);
|
|
569
|
+
throw new InvalidStreamError('Model stream ended with malformed function call.', 'MALFORMED_FUNCTION_CALL');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
_throwMissingResponseError(finishReason, hasTextResponse, validationContext) {
|
|
573
|
+
if (isMissingFinishReason(finishReason) && !hasTextResponse) {
|
|
574
|
+
this.logger.warn(() => `[stream:terminal] validation failed: missing finishReason and text`, validationContext);
|
|
575
|
+
throw new InvalidStreamError('Model stream ended without a finish reason and no text response.', 'NO_FINISH_REASON_NO_TEXT');
|
|
576
|
+
}
|
|
577
|
+
this.logger.warn(() => `[stream:terminal] validation failed: empty response text`, validationContext);
|
|
578
|
+
throw new InvalidStreamError('Model stream ended with empty response text.', 'NO_RESPONSE_TEXT');
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Record history with usage metadata and sync token counts.
|
|
582
|
+
*/
|
|
583
|
+
async _recordHistoryWithUsage(userInput, consolidatedParts, allChunks) {
|
|
584
|
+
// Use recordHistory to correctly save the conversation turn.
|
|
585
|
+
const modelOutput = [
|
|
586
|
+
{ role: 'model', parts: consolidatedParts },
|
|
587
|
+
];
|
|
588
|
+
// Capture usage metadata from the stream
|
|
589
|
+
let streamingUsageMetadata = null;
|
|
590
|
+
let actualPromptTokens = null;
|
|
591
|
+
// Find the last chunk that has usage metadata (similar to getLastChunkWithMetadata logic)
|
|
592
|
+
const lastChunkWithMetadata = allChunks
|
|
593
|
+
.slice()
|
|
594
|
+
.reverse()
|
|
595
|
+
.find((chunk) => chunk.usageMetadata);
|
|
596
|
+
if (lastChunkWithMetadata?.usageMetadata) {
|
|
597
|
+
streamingUsageMetadata = {
|
|
598
|
+
promptTokens: lastChunkWithMetadata.usageMetadata.promptTokenCount ?? 0,
|
|
599
|
+
completionTokens: lastChunkWithMetadata.usageMetadata.candidatesTokenCount ?? 0,
|
|
600
|
+
totalTokens: lastChunkWithMetadata.usageMetadata.totalTokenCount ?? 0,
|
|
601
|
+
};
|
|
602
|
+
const usageMetadata = lastChunkWithMetadata.usageMetadata;
|
|
603
|
+
const cacheReads = usageMetadata.cache_read_input_tokens ?? 0;
|
|
604
|
+
const cacheWrites = usageMetadata.cache_creation_input_tokens ?? 0;
|
|
605
|
+
actualPromptTokens =
|
|
606
|
+
streamingUsageMetadata.promptTokens + cacheReads + cacheWrites;
|
|
607
|
+
}
|
|
608
|
+
// Record history first (adds estimated tokens)
|
|
609
|
+
this.conversationManager.recordHistory(userInput, modelOutput, undefined, streamingUsageMetadata);
|
|
610
|
+
// Ensure token estimation updates are complete before syncing to actual API prompt tokens.
|
|
611
|
+
await this.historyService.waitForTokenUpdates();
|
|
612
|
+
// Sync token counts AFTER recording history to replace estimated tokens with actual API prompt tokens
|
|
613
|
+
// Use explicit check for undefined to allow 0 values
|
|
614
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, sonarjs/different-types-comparison -- preserve defensive runtime boundary guard despite current static types.
|
|
615
|
+
if (actualPromptTokens !== null && actualPromptTokens !== undefined) {
|
|
616
|
+
if (actualPromptTokens > 0) {
|
|
617
|
+
this.logger.debug(() => `[StreamProcessor] Syncing prompt token count to HistoryService: ${actualPromptTokens}`);
|
|
618
|
+
this.historyService.syncTotalTokens(actualPromptTokens);
|
|
619
|
+
await this.historyService.waitForTokenUpdates();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else if (this.compressionHandler.lastPromptTokenCount !== null) {
|
|
623
|
+
if (this.compressionHandler.lastPromptTokenCount > 0) {
|
|
624
|
+
this.logger.debug(() => `[StreamProcessor] Syncing prompt token count to HistoryService: ${this.compressionHandler.lastPromptTokenCount}`);
|
|
625
|
+
this.historyService.syncTotalTokens(this.compressionHandler.lastPromptTokenCount);
|
|
626
|
+
await this.historyService.waitForTokenUpdates();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
this.logger.debug(() => `[StreamProcessor] No token count to sync (lastPromptTokenCount: ${this.compressionHandler.lastPromptTokenCount})`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Enrich schema depth errors with diagnostic information.
|
|
635
|
+
* Adapted from maybeIncludeSchemaDepthContext in chatSession.ts.
|
|
636
|
+
*/
|
|
637
|
+
_enrichSchemaDepthError(error) {
|
|
638
|
+
// Check for potentially problematic cyclic tools with cyclic schemas
|
|
639
|
+
// and include a recommendation to remove potentially problematic tools.
|
|
640
|
+
if (isStructuredError(error) && isSchemaDepthError(error.message)) {
|
|
641
|
+
const toolNames = this.runtimeContext.tools.listToolNames();
|
|
642
|
+
const cyclicSchemaTools = [];
|
|
643
|
+
// Check each tool's metadata for cyclic schemas
|
|
644
|
+
for (const toolName of toolNames) {
|
|
645
|
+
const metadata = this.runtimeContext.tools.getToolMetadata(toolName);
|
|
646
|
+
if (metadata?.parameterSchema &&
|
|
647
|
+
hasCycleInSchema(metadata.parameterSchema)) {
|
|
648
|
+
cyclicSchemaTools.push(toolName);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (cyclicSchemaTools.length > 0) {
|
|
652
|
+
const extraDetails = `\n\nThis error was probably caused by cyclic schema references in one of the following tools, try disabling them:\n\n - ` +
|
|
653
|
+
cyclicSchemaTools.join(`\n - `) +
|
|
654
|
+
`\n`;
|
|
655
|
+
error.message += extraDetails;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
//# sourceMappingURL=StreamProcessor.js.map
|