mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-25 02:37:53 -07:00
fix(core): strongly-typed ADK Model pass-through and session tracking
- Removed 'as any' castings and replaced with strongly-typed 'as unknown as T' bounds with safe ESLint exceptions. - Renamed synthetic 'synth-' invocation IDs to descriptive session and history domain states. - Linked local @google/adk workspace dependency.
This commit is contained in:
Generated
+53
@@ -86,6 +86,54 @@
|
||||
"node-pty": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"../adk-js/core": {
|
||||
"name": "@google/adk",
|
||||
"version": "1.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.10",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
|
||||
"@google-cloud/storage": "^7.17.1",
|
||||
"@google-cloud/vertexai": "^1.12.0",
|
||||
"@google/genai": "^1.37.0",
|
||||
"@mikro-orm/core": "^6.6.10",
|
||||
"@mikro-orm/reflection": "^6.6.6",
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.205.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.205.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.205.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.205.0",
|
||||
"@opentelemetry/resource-detector-gcp": "^0.40.0",
|
||||
"@opentelemetry/resources": "^2.1.0",
|
||||
"@opentelemetry/sdk-logs": "^0.205.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.1.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
||||
"@opentelemetry/sdk-trace-node": "^2.1.0",
|
||||
"express": "^4.22.1",
|
||||
"google-auth-library": "^10.3.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonpath-plus": "^10.4.0",
|
||||
"lodash-es": "^4.18.1",
|
||||
"winston": "^3.19.0",
|
||||
"zod": "^4.2.1",
|
||||
"zod-to-json-schema": "^3.25.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mikro-orm/sqlite": "^6.6.6",
|
||||
"@types/express": "^4.17.25",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"openapi-types": "^12.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mikro-orm/mariadb": "^6.6.6",
|
||||
"@mikro-orm/mssql": "^6.6.6",
|
||||
"@mikro-orm/mysql": "^6.6.6",
|
||||
"@mikro-orm/postgresql": "^6.6.6",
|
||||
"@mikro-orm/sqlite": "^6.6.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.16.1.tgz",
|
||||
@@ -1398,6 +1446,10 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/adk": {
|
||||
"resolved": "../adk-js/core",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@google/gemini-cli": {
|
||||
"resolved": "packages/cli",
|
||||
"link": true
|
||||
@@ -18408,6 +18460,7 @@
|
||||
"@google-cloud/logging": "^11.2.1",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
|
||||
"@google/adk": "file:../../../adk-js/core",
|
||||
"@google/genai": "1.30.0",
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@google/adk": "file:../../../adk-js/core",
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"@google-cloud/logging": "^11.2.1",
|
||||
|
||||
@@ -30,6 +30,8 @@ import { getCoreSystemPrompt } from './prompts.js';
|
||||
import { checkNextSpeaker } from '../utils/nextSpeakerChecker.js';
|
||||
import { reportError } from '../utils/errorReporting.js';
|
||||
import { GeminiChat } from './geminiChat.js';
|
||||
import { GcliAdkModel } from './gcliAdkModel.js';
|
||||
import type { LlmRequest } from '@google/adk';
|
||||
import {
|
||||
retryWithBackoff,
|
||||
type RetryAvailabilityContext,
|
||||
@@ -1101,15 +1103,35 @@ export class GeminiClient {
|
||||
systemInstruction,
|
||||
};
|
||||
|
||||
return this.getContentGeneratorOrFail().generateContent(
|
||||
{
|
||||
model: currentAttemptModel,
|
||||
config: requestConfig,
|
||||
contents,
|
||||
},
|
||||
const adkModel = new GcliAdkModel(
|
||||
this.getContentGeneratorOrFail(),
|
||||
this.lastPromptId,
|
||||
role,
|
||||
currentAttemptModel,
|
||||
);
|
||||
|
||||
const adkRequest: LlmRequest = {
|
||||
contents,
|
||||
config: requestConfig,
|
||||
liveConnectConfig: {},
|
||||
toolsDict: {},
|
||||
};
|
||||
|
||||
const adkStream = adkModel.generateContentAsync(
|
||||
adkRequest,
|
||||
false,
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
const processResponse = async () => {
|
||||
const nextVal = await adkStream.next();
|
||||
if (nextVal.done || !nextVal.value) {
|
||||
throw new Error('No response from ADK model.');
|
||||
}
|
||||
return nextVal.value.rawResponse;
|
||||
};
|
||||
|
||||
return processResponse();
|
||||
};
|
||||
const onPersistent429Callback = async (
|
||||
authType?: string,
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
BaseLlm,
|
||||
type LlmRequest,
|
||||
type LlmResponse,
|
||||
type BaseLlmConnection,
|
||||
} from '@google/adk';
|
||||
import type {
|
||||
GenerateContentResponse,
|
||||
Content,
|
||||
GenerateContentConfig,
|
||||
} from '@google/genai';
|
||||
import type { ContentGenerator } from './contentGenerator.js';
|
||||
import type { LlmRole } from '../telemetry/llmRole.js';
|
||||
|
||||
export interface GcliLlmResponse extends LlmResponse {
|
||||
rawResponse: GenerateContentResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an LlmResponse from a GenerateContentResponse.
|
||||
* Locally defined to avoid dependency export resolution issues.
|
||||
*/
|
||||
function localCreateLlmResponse(
|
||||
response: GenerateContentResponse,
|
||||
): LlmResponse {
|
||||
const usageMetadata = response.usageMetadata;
|
||||
|
||||
if (response.candidates && response.candidates.length > 0) {
|
||||
const candidate = response.candidates[0];
|
||||
if (candidate.content?.parts && candidate.content.parts.length > 0) {
|
||||
return {
|
||||
content: candidate.content,
|
||||
groundingMetadata: candidate.groundingMetadata,
|
||||
citationMetadata: candidate.citationMetadata,
|
||||
usageMetadata,
|
||||
finishReason: candidate.finishReason,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
errorCode: candidate.finishReason,
|
||||
errorMessage: candidate.finishMessage,
|
||||
usageMetadata,
|
||||
citationMetadata: candidate.citationMetadata,
|
||||
finishReason: candidate.finishReason,
|
||||
};
|
||||
}
|
||||
|
||||
if (response.promptFeedback) {
|
||||
return {
|
||||
errorCode: response.promptFeedback.blockReason,
|
||||
errorMessage: response.promptFeedback.blockReasonMessage,
|
||||
usageMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
errorCode: 'UNKNOWN_ERROR',
|
||||
errorMessage: 'Unknown error.',
|
||||
usageMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
export class GcliAdkModel extends BaseLlm {
|
||||
constructor(
|
||||
private readonly contentGenerator: ContentGenerator,
|
||||
private readonly promptId: string,
|
||||
private readonly role: LlmRole,
|
||||
modelName: string,
|
||||
) {
|
||||
super({ model: modelName });
|
||||
}
|
||||
|
||||
async *generateContentAsync(
|
||||
llmRequest: LlmRequest,
|
||||
stream = true,
|
||||
abortSignal?: AbortSignal,
|
||||
): AsyncGenerator<GcliLlmResponse, void> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const contents = llmRequest.contents as unknown as Content[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const config = {
|
||||
...llmRequest.config,
|
||||
abortSignal,
|
||||
} as unknown as GenerateContentConfig;
|
||||
|
||||
if (stream) {
|
||||
const responseStream = await this.contentGenerator.generateContentStream(
|
||||
{
|
||||
model: this.model,
|
||||
contents,
|
||||
config,
|
||||
},
|
||||
this.promptId,
|
||||
this.role,
|
||||
);
|
||||
|
||||
for await (const chunk of responseStream) {
|
||||
const adkResponse = localCreateLlmResponse(chunk);
|
||||
yield {
|
||||
...adkResponse,
|
||||
rawResponse: chunk,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const response = await this.contentGenerator.generateContent(
|
||||
{
|
||||
model: this.model,
|
||||
contents,
|
||||
config,
|
||||
},
|
||||
this.promptId,
|
||||
this.role,
|
||||
);
|
||||
const adkResponse = localCreateLlmResponse(response);
|
||||
yield {
|
||||
...adkResponse,
|
||||
rawResponse: response,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async connect(_llmRequest: LlmRequest): Promise<BaseLlmConnection> {
|
||||
throw new Error('Bidi connections not supported in Dumb Model Swap.');
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,13 @@ import {
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import {
|
||||
createSession,
|
||||
createEvent,
|
||||
type Session,
|
||||
type LlmRequest,
|
||||
} from '@google/adk';
|
||||
import { GcliAdkModel } from './gcliAdkModel.js';
|
||||
|
||||
export enum StreamEventType {
|
||||
/** A regular content chunk from the API. */
|
||||
@@ -267,6 +274,7 @@ export class GeminiChat {
|
||||
private lastPromptTokenCount: number;
|
||||
private callCounter = 0;
|
||||
agentHistory: AgentChatHistory;
|
||||
private readonly adkSession: Session;
|
||||
|
||||
constructor(
|
||||
readonly context: AgentLoopContext,
|
||||
@@ -282,6 +290,18 @@ export class GeminiChat {
|
||||
this.lastPromptTokenCount = estimateTokenCountSync(
|
||||
this.agentHistory.flatMap((c) => c.parts || []),
|
||||
);
|
||||
this.adkSession = createSession({
|
||||
id: context.config.getSessionId() || 'default-adk-session',
|
||||
appName: 'Gemini CLI',
|
||||
userId: 'gemini-user',
|
||||
events: history.map((content) =>
|
||||
createEvent({
|
||||
invocationId: context.config.getSessionId() || 'session-init',
|
||||
author: content.role,
|
||||
content,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
get loopContext(): AgentLoopContext {
|
||||
@@ -375,9 +395,16 @@ export class GeminiChat {
|
||||
if (binaryInjections) {
|
||||
// Turn 1: The original tool response (now cleaned)
|
||||
this.agentHistory.push(userContent);
|
||||
this.adkSession.events.push(
|
||||
createEvent({
|
||||
invocationId: prompt_id,
|
||||
author: userContent.role,
|
||||
content: userContent,
|
||||
}),
|
||||
);
|
||||
|
||||
// Turn 2: Synthetic Model Acknowledgment
|
||||
this.agentHistory.push({
|
||||
const ackContent: Content = {
|
||||
role: 'model',
|
||||
parts: [
|
||||
{
|
||||
@@ -386,7 +413,15 @@ export class GeminiChat {
|
||||
thoughtSignature: SYNTHETIC_THOUGHT_SIGNATURE,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
this.agentHistory.push(ackContent);
|
||||
this.adkSession.events.push(
|
||||
createEvent({
|
||||
invocationId: prompt_id,
|
||||
author: 'model',
|
||||
content: ackContent,
|
||||
}),
|
||||
);
|
||||
|
||||
// Turn 3: The actual binary data (becomes the current request message)
|
||||
userContent = {
|
||||
@@ -396,6 +431,13 @@ export class GeminiChat {
|
||||
}
|
||||
|
||||
this.agentHistory.push(userContent);
|
||||
this.adkSession.events.push(
|
||||
createEvent({
|
||||
invocationId: prompt_id,
|
||||
author: userContent.role,
|
||||
content: userContent,
|
||||
}),
|
||||
);
|
||||
const requestContents = this.getHistory(true);
|
||||
|
||||
const streamWithRetries = async function* (
|
||||
@@ -740,15 +782,33 @@ export class GeminiChat {
|
||||
|
||||
const finalContents = stripToolCallIdPrefixes(contentsToUse);
|
||||
|
||||
return this.context.config.getContentGenerator().generateContentStream(
|
||||
{
|
||||
model: modelToUse,
|
||||
contents: finalContents,
|
||||
config,
|
||||
},
|
||||
const adkModel = new GcliAdkModel(
|
||||
this.context.config.getContentGenerator(),
|
||||
prompt_id,
|
||||
role,
|
||||
modelToUse,
|
||||
);
|
||||
|
||||
const adkRequest: LlmRequest = {
|
||||
contents: finalContents,
|
||||
config,
|
||||
liveConnectConfig: {},
|
||||
toolsDict: {},
|
||||
};
|
||||
|
||||
const adkStream = adkModel.generateContentAsync(
|
||||
adkRequest,
|
||||
true,
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
const responseGenerator = async function* () {
|
||||
for await (const adkResponse of adkStream) {
|
||||
yield adkResponse.rawResponse;
|
||||
}
|
||||
};
|
||||
|
||||
return responseGenerator();
|
||||
};
|
||||
|
||||
const onPersistent429Callback = async (
|
||||
@@ -803,6 +863,7 @@ export class GeminiChat {
|
||||
lastModelToUse,
|
||||
streamResponse,
|
||||
originalRequest,
|
||||
prompt_id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -844,6 +905,7 @@ export class GeminiChat {
|
||||
*/
|
||||
clearHistory(): void {
|
||||
this.agentHistory.clear();
|
||||
this.adkSession.events = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -851,6 +913,13 @@ export class GeminiChat {
|
||||
*/
|
||||
addHistory(content: Content): void {
|
||||
this.agentHistory.push(content);
|
||||
this.adkSession.events.push(
|
||||
createEvent({
|
||||
invocationId: this.context.config.getSessionId() || 'history-add',
|
||||
author: content.role,
|
||||
content,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setHistory(
|
||||
@@ -862,6 +931,13 @@ export class GeminiChat {
|
||||
this.agentHistory.flatMap((c) => c.parts || []),
|
||||
);
|
||||
this.chatRecordingService.updateMessagesFromHistory(history);
|
||||
this.adkSession.events = history.map((content) =>
|
||||
createEvent({
|
||||
invocationId: this.context.config.getSessionId() || 'history-sync',
|
||||
author: content.role,
|
||||
content,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
stripThoughtsFromHistory(): void {
|
||||
@@ -968,6 +1044,7 @@ export class GeminiChat {
|
||||
model: string,
|
||||
streamResponse: AsyncGenerator<GenerateContentResponse>,
|
||||
originalRequest: GenerateContentParameters,
|
||||
prompt_id: string,
|
||||
): AsyncGenerator<GenerateContentResponse> {
|
||||
const modelResponseParts: Part[] = [];
|
||||
|
||||
@@ -1208,7 +1285,18 @@ export class GeminiChat {
|
||||
}
|
||||
}
|
||||
|
||||
this.agentHistory.push({ role: 'model', parts: consolidatedParts });
|
||||
const consolidatedContent: Content = {
|
||||
role: 'model',
|
||||
parts: consolidatedParts,
|
||||
};
|
||||
this.agentHistory.push(consolidatedContent);
|
||||
this.adkSession.events.push(
|
||||
createEvent({
|
||||
invocationId: prompt_id,
|
||||
author: 'model',
|
||||
content: consolidatedContent,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getLastPromptTokenCount(): number {
|
||||
|
||||
Reference in New Issue
Block a user