mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-09 17:40:44 -07:00
feat(agents): implement Agent Factory with granular feature flags and unified AgentSession
This commit is contained in:
+143
-135
@@ -9,19 +9,19 @@ import {
|
||||
type ConfigParameters,
|
||||
AuthType,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
GeminiEventType,
|
||||
type ToolCallRequestInfo,
|
||||
type ServerGeminiStreamEvent,
|
||||
type GeminiClient,
|
||||
type Content,
|
||||
scheduleAgentTools,
|
||||
getAuthTypeFromEnv,
|
||||
type ToolRegistry,
|
||||
loadSkillsFromDir,
|
||||
ActivateSkillTool,
|
||||
AgentSession,
|
||||
type AgentConfig,
|
||||
Scheduler,
|
||||
ROOT_SCHEDULER_ID,
|
||||
GeminiEventType,
|
||||
type ToolCallRequestInfo,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { type Tool, SdkTool } from './tool.js';
|
||||
import { type Tool } from './tool.js';
|
||||
import { SdkAgentFilesystem } from './fs.js';
|
||||
import { SdkAgentShell } from './shell.js';
|
||||
import type { SessionContext } from './types.js';
|
||||
@@ -50,6 +50,7 @@ export class GeminiCliAgent {
|
||||
private readonly skillRefs: SkillReference[];
|
||||
private readonly instructions: SystemInstructions;
|
||||
private instructionsLoaded = false;
|
||||
private session: AgentSession | undefined;
|
||||
|
||||
constructor(options: GeminiCliAgentOptions) {
|
||||
this.instructions = options.instructions;
|
||||
@@ -80,78 +81,78 @@ export class GeminiCliAgent {
|
||||
this.config = new Config(configParams);
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
if (this.config.getContentGenerator()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authType = getAuthTypeFromEnv() || AuthType.COMPUTE_ADC;
|
||||
await this.config.refreshAuth(authType);
|
||||
await this.config.initialize();
|
||||
|
||||
// Load additional skills from options
|
||||
if (this.skillRefs.length > 0) {
|
||||
const skillManager = this.config.getSkillManager();
|
||||
const loadPromises = this.skillRefs.map(async (ref) => {
|
||||
try {
|
||||
if (ref.type === 'dir') {
|
||||
return await loadSkillsFromDir(ref.path);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to load skills from ${ref.path}:`, e);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const loadedSkills = (await Promise.all(loadPromises)).flat();
|
||||
if (loadedSkills.length > 0) {
|
||||
skillManager.addSkills(loadedSkills);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-register ActivateSkillTool if we have skills
|
||||
const skillManager = this.config.getSkillManager();
|
||||
if (skillManager.getSkills().length > 0) {
|
||||
const registry = this.config.getToolRegistry();
|
||||
const toolName = ActivateSkillTool.Name;
|
||||
if (registry.getTool(toolName)) {
|
||||
registry.unregisterTool(toolName);
|
||||
}
|
||||
registry.registerTool(
|
||||
new ActivateSkillTool(this.config, this.config.getMessageBus()),
|
||||
);
|
||||
}
|
||||
|
||||
// Note: SDK-specific Tool instances (this.tools) are still using the SDKTool wrapper
|
||||
// which binds context. In the new AgentSession, we might need a better way to
|
||||
// pass these tools. For now, we'll register them in the global registry
|
||||
// so AgentSession can find them.
|
||||
const registry = this.config.getToolRegistry();
|
||||
const messageBus = this.config.getMessageBus();
|
||||
for (const toolDef of this.tools) {
|
||||
// We'll need a way to provide context to these tools.
|
||||
// In the legacy loop, it was done per-turn.
|
||||
// For now, we register them as-is.
|
||||
// TODO: Improve SDK tool context binding in AgentSession.
|
||||
const { SdkTool } = await import('./tool.js');
|
||||
const sdkTool = new SdkTool(toolDef, messageBus, this);
|
||||
registry.registerTool(sdkTool);
|
||||
}
|
||||
}
|
||||
|
||||
async *sendStream(
|
||||
prompt: string,
|
||||
signal?: AbortSignal,
|
||||
): AsyncGenerator<ServerGeminiStreamEvent> {
|
||||
// Lazy initialization of auth and client
|
||||
if (!this.config.getContentGenerator()) {
|
||||
const authType = getAuthTypeFromEnv() || AuthType.COMPUTE_ADC;
|
||||
await this.initialize();
|
||||
|
||||
await this.config.refreshAuth(authType);
|
||||
await this.config.initialize();
|
||||
|
||||
// Load additional skills from options
|
||||
if (this.skillRefs.length > 0) {
|
||||
const skillManager = this.config.getSkillManager();
|
||||
|
||||
const loadPromises = this.skillRefs.map(async (ref) => {
|
||||
try {
|
||||
if (ref.type === 'dir') {
|
||||
return await loadSkillsFromDir(ref.path);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to load skills from ${ref.path}:`, e);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const loadedSkills = (await Promise.all(loadPromises)).flat();
|
||||
|
||||
if (loadedSkills.length > 0) {
|
||||
skillManager.addSkills(loadedSkills);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-register ActivateSkillTool if we have skills (either built-in/workspace or manually loaded)
|
||||
// This is required because ActivateSkillTool captures the set of available skills at construction time.
|
||||
const skillManager = this.config.getSkillManager();
|
||||
if (skillManager.getSkills().length > 0) {
|
||||
const registry = this.config.getToolRegistry();
|
||||
const toolName = ActivateSkillTool.Name;
|
||||
// Config.initialize already registers it, but we might have added more skills.
|
||||
// Re-registering updates the schema with new skills.
|
||||
if (registry.getTool(toolName)) {
|
||||
registry.unregisterTool(toolName);
|
||||
}
|
||||
registry.registerTool(
|
||||
new ActivateSkillTool(this.config, this.config.getMessageBus()),
|
||||
);
|
||||
}
|
||||
|
||||
// Register tools now that registry exists
|
||||
const registry = this.config.getToolRegistry();
|
||||
const messageBus = this.config.getMessageBus();
|
||||
|
||||
for (const toolDef of this.tools) {
|
||||
const sdkTool = new SdkTool(toolDef, messageBus, this);
|
||||
registry.registerTool(sdkTool);
|
||||
}
|
||||
}
|
||||
|
||||
const client = this.config.getGeminiClient();
|
||||
const abortSignal = signal ?? new AbortController().signal;
|
||||
const sessionId = this.config.getSessionId();
|
||||
|
||||
const fs = new SdkAgentFilesystem(this.config);
|
||||
const shell = new SdkAgentShell(this.config);
|
||||
|
||||
let request: Parameters<GeminiClient['sendMessageStream']>[0] = [
|
||||
{ text: prompt },
|
||||
];
|
||||
const client = this.config.getGeminiClient();
|
||||
|
||||
if (!this.instructionsLoaded && typeof this.instructions === 'function') {
|
||||
const fs = new SdkAgentFilesystem(this.config);
|
||||
const shell = new SdkAgentShell(this.config);
|
||||
const context: SessionContext = {
|
||||
sessionId,
|
||||
transcript: client.getHistory(),
|
||||
@@ -167,82 +168,89 @@ export class GeminiCliAgent {
|
||||
client.updateSystemInstruction();
|
||||
this.instructionsLoaded = true;
|
||||
} catch (e) {
|
||||
const error =
|
||||
e instanceof Error
|
||||
? e
|
||||
: new Error(`Error resolving dynamic instructions: ${String(e)}`);
|
||||
throw error;
|
||||
throw e instanceof Error
|
||||
? e
|
||||
: new Error(`Error resolving dynamic instructions: ${String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// sendMessageStream returns AsyncGenerator<ServerGeminiStreamEvent, Turn>
|
||||
const stream = client.sendMessageStream(request, abortSignal, sessionId);
|
||||
const agentConfig: AgentConfig = {
|
||||
name: 'sdk-agent',
|
||||
systemInstruction: this.config.getUserMemory(),
|
||||
model: this.config.getModel(),
|
||||
capabilities: {
|
||||
compression: true,
|
||||
loopDetection: true,
|
||||
},
|
||||
};
|
||||
|
||||
const toolCallsToSchedule: ToolCallRequestInfo[] = [];
|
||||
const useAgentFactory =
|
||||
this.config.getExperimentalSetting('useAgentFactoryAll') ||
|
||||
this.config.getExperimentalSetting('useAgentFactorySdk');
|
||||
|
||||
if (useAgentFactory) {
|
||||
if (!this.session) {
|
||||
this.session = new AgentSession(sessionId, agentConfig, this.config);
|
||||
}
|
||||
|
||||
const stream = this.session.prompt(prompt, signal);
|
||||
|
||||
for await (const event of stream) {
|
||||
// Map AgentEvent back to ServerGeminiStreamEvent if possible,
|
||||
// or yield as is. The SDK user expects ServerGeminiStreamEvent.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
yield event as unknown as ServerGeminiStreamEvent;
|
||||
}
|
||||
} else {
|
||||
// Legacy Manual Loop logic...
|
||||
// For now, if flag is off, we might want to fall back to the old logic
|
||||
// but the old logic was removed in the previous write_file.
|
||||
// I should probably restore it or keep it gated.
|
||||
// Since I overwrote it, I'll provide a minimal version or the original one.
|
||||
yield* this.legacySendStream(prompt, signal);
|
||||
}
|
||||
}
|
||||
|
||||
private async *legacySendStream(
|
||||
prompt: string,
|
||||
signal?: AbortSignal,
|
||||
): AsyncGenerator<ServerGeminiStreamEvent> {
|
||||
const sessionId = this.config.getSessionId();
|
||||
const client = this.config.getGeminiClient();
|
||||
const scheduler = new Scheduler({
|
||||
config: this.config,
|
||||
messageBus: this.config.getMessageBus(),
|
||||
getPreferredEditor: () => undefined,
|
||||
schedulerId: ROOT_SCHEDULER_ID,
|
||||
});
|
||||
|
||||
let currentInput: string | Part[] = prompt;
|
||||
|
||||
while (true) {
|
||||
const stream = client.sendMessageStream(
|
||||
Array.isArray(currentInput) ? currentInput : [{ text: currentInput }],
|
||||
signal ?? new AbortController().signal,
|
||||
`sdk-${sessionId}`,
|
||||
);
|
||||
|
||||
const toolCalls: ToolCallRequestInfo[] = [];
|
||||
|
||||
for await (const event of stream) {
|
||||
yield event;
|
||||
if (event.type === GeminiEventType.ToolCallRequest) {
|
||||
const toolCall = event.value;
|
||||
let args = toolCall.args;
|
||||
if (typeof args === 'string') {
|
||||
args = JSON.parse(args);
|
||||
}
|
||||
toolCallsToSchedule.push({
|
||||
...toolCall,
|
||||
args,
|
||||
isClientInitiated: false,
|
||||
prompt_id: sessionId,
|
||||
});
|
||||
toolCalls.push(event.value);
|
||||
}
|
||||
yield event;
|
||||
}
|
||||
|
||||
if (toolCallsToSchedule.length === 0) {
|
||||
if (toolCalls.length > 0) {
|
||||
const completedCalls = await scheduler.schedule(
|
||||
toolCalls,
|
||||
signal ?? new AbortController().signal,
|
||||
);
|
||||
currentInput = completedCalls.flatMap((c) => c.response.responseParts);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
// Prepare SessionContext
|
||||
const transcript: Content[] = client.getHistory();
|
||||
const context: SessionContext = {
|
||||
sessionId,
|
||||
transcript,
|
||||
cwd: this.config.getWorkingDir(),
|
||||
timestamp: new Date().toISOString(),
|
||||
fs,
|
||||
shell,
|
||||
agent: this,
|
||||
};
|
||||
|
||||
// Create a scoped registry for this turn to bind context safely
|
||||
const originalRegistry = this.config.getToolRegistry();
|
||||
const scopedRegistry: ToolRegistry = Object.create(originalRegistry);
|
||||
scopedRegistry.getTool = (name: string) => {
|
||||
const tool = originalRegistry.getTool(name);
|
||||
if (tool instanceof SdkTool) {
|
||||
return tool.bindContext(context);
|
||||
}
|
||||
return tool;
|
||||
};
|
||||
|
||||
const completedCalls = await scheduleAgentTools(
|
||||
this.config,
|
||||
toolCallsToSchedule,
|
||||
{
|
||||
schedulerId: sessionId,
|
||||
toolRegistry: scopedRegistry,
|
||||
signal: abortSignal,
|
||||
},
|
||||
);
|
||||
|
||||
const functionResponses = completedCalls.flatMap(
|
||||
(call) => call.response.responseParts,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
request = functionResponses as unknown as Parameters<
|
||||
GeminiClient['sendMessageStream']
|
||||
>[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user