diff --git a/package-lock.json b/package-lock.json index 5895a8ac51..8c97ad8b50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/core/package.json b/packages/core/package.json index 598aceae3c..f3db05ae93 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 302b89d7f0..18cc3088f3 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -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, diff --git a/packages/core/src/core/gcliAdkModel.ts b/packages/core/src/core/gcliAdkModel.ts new file mode 100644 index 0000000000..64aea8b219 --- /dev/null +++ b/packages/core/src/core/gcliAdkModel.ts @@ -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 { + // 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 { + throw new Error('Bidi connections not supported in Dumb Model Swap.'); + } +} diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 6a728884a5..4685cde5af 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -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, originalRequest: GenerateContentParameters, + prompt_id: string, ): AsyncGenerator { 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 {