feat(agents): implement Agent Factory with granular feature flags and unified AgentSession

This commit is contained in:
mkorwel
2026-02-22 04:53:47 +00:00
parent b23bcc7ae5
commit 6b44dfee4c
9 changed files with 588 additions and 159 deletions
+6
View File
@@ -819,6 +819,12 @@ export async function loadCliConfig(
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
modelSteering: settings.experimental?.modelSteering,
useAgentFactoryAll: settings.experimental?.useAgentFactoryAll,
useAgentFactorySdk: settings.experimental?.useAgentFactorySdk,
useAgentFactoryNonInteractive:
settings.experimental?.useAgentFactoryNonInteractive,
useAgentFactoryInteractive:
settings.experimental?.useAgentFactoryInteractive,
toolOutputMasking: settings.experimental?.toolOutputMasking,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
+38
View File
@@ -1681,6 +1681,44 @@ const SETTINGS_SCHEMA = {
'Enable model steering (user hints) to guide the model during tool execution.',
showInDialog: true,
},
useAgentFactoryAll: {
type: 'boolean',
label: 'Use Agent Factory (All)',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable Agent Factory for all supported execution paths.',
showInDialog: true,
},
useAgentFactorySdk: {
type: 'boolean',
label: 'Use Agent Factory (SDK)',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable Agent Factory for the SDK execution path.',
showInDialog: true,
},
useAgentFactoryNonInteractive: {
type: 'boolean',
label: 'Use Agent Factory (Non-Interactive)',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable Agent Factory for the non-interactive CLI execution path.',
showInDialog: true,
},
useAgentFactoryInteractive: {
type: 'boolean',
label: 'Use Agent Factory (Interactive)',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable Agent Factory for the interactive CLI execution path.',
showInDialog: true,
},
},
},
+9 -3
View File
@@ -269,16 +269,22 @@ export async function runNonInteractive({
query = processedQuery as Part[];
}
if (config.isAgentsEnabled()) {
// --- Dispatch Loop ---
const experimental = settings.experimental;
const useAgentFactory =
experimental?.useAgentFactoryAll ||
experimental?.useAgentFactoryNonInteractive;
if (useAgentFactory) {
await runAgentSessionFlow(
loopContext,
{ config, settings, input, prompt_id, resumedSessionData, query }, // API change: pass query
{ config, settings, input, prompt_id, resumedSessionData, query },
handleUserFeedback,
);
} else {
await runLegacyManualLoop(
loopContext,
{ config, settings, input, prompt_id, resumedSessionData, query }, // API change: pass query
{ config, settings, input, prompt_id, resumedSessionData, query },
handleUserFeedback,
);
}
+138 -16
View File
@@ -50,6 +50,9 @@ import type {
ToolCallResponseInfo,
GeminiErrorEventValue,
RetryAttemptPayload,
AgentSession,
AgentTerminateMode,
AgentEvent,
} from '@google/gemini-cli-core';
import { type Part, type PartListUnion, FinishReason } from '@google/genai';
import type {
@@ -1246,7 +1249,102 @@ export const useGeminiStream = (
setPendingHistoryItem,
setThought,
],
);
const processAgentEvents = useCallback(
async (
stream: AsyncIterable<AgentEvent>,
userMessageTimestamp: number,
signal: AbortSignal,
): Promise<void> => {
let geminiMessageBuffer = '';
for await (const event of stream) {
if (signal.aborted) break;
// Map AgentEvent back to GeminiEvent handlers
switch (event.type) {
case 'thought':
handleThoughtEvent(
{ summary: event.value, thought: event.value },
userMessageTimestamp,
);
break;
case ServerGeminiEventType.Content:
geminiMessageBuffer = handleContentEvent(
event.value,
geminiMessageBuffer,
userMessageTimestamp,
);
break;
case ServerGeminiEventType.ToolCallRequest:
// Handled by AgentSession, but we can still show them
// The useToolScheduler will be used by AgentSession's internal scheduler,
// but for UI feedback we need to make sure they show up in toolCalls.
// Since AgentSession uses Scheduler which is not hooked into useToolScheduler state,
// we might need to bridge this.
// For now, we'll just emit events to show activity.
break;
case 'tool_suite_start':
setToolCallsForDisplay(
Array(event.value.count).fill({
status: CoreToolCallStatus.Executing,
request: { name: 'Executing tools...' },
}),
);
break;
case 'tool_suite_finish':
setToolCallsForDisplay([]);
// handleCompletedTools will be called by AgentSession internally,
// but we need to update the UI history here.
// AgentSession doesn't provide the full TrackedToolCall objects.
// This is a known gap in the "meet in the middle" approach.
break;
case 'agent_finish': {
const { reason } = event.value;
if (reason === AgentTerminateMode.MAX_TURNS) {
handleMaxSessionTurnsEvent();
}
setIsResponding(false);
break;
}
case 'goal_completed':
addItem({
type: MessageType.INFO,
text: 'Goal completed.',
});
break;
case ServerGeminiEventType.Error:
handleErrorEvent(event.value, userMessageTimestamp);
break;
default: {
// Handle other core events if they match
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const coreEvent = event as unknown as {
type: ServerGeminiEventType;
value: unknown;
};
if (coreEvent.type === ServerGeminiEventType.Citation) {
handleCitationEvent(
coreEvent.value as unknown as string, // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
userMessageTimestamp,
);
}
}
}
}
},
[
handleThoughtEvent,
handleContentEvent,
handleMaxSessionTurnsEvent,
handleErrorEvent,
handleCitationEvent,
setToolCallsForDisplay,
setIsResponding,
addItem,
],
);
const submitQuery = useCallback(
async (
query: PartListUnion,
@@ -1319,23 +1417,45 @@ export const useGeminiStream = (
lastQueryRef.current = queryToSend;
lastPromptIdRef.current = prompt_id!;
try {
const stream = geminiClient.sendMessageStream(
queryToSend,
abortSignal,
prompt_id!,
undefined,
false,
query,
);
const processingStatus = await processGeminiStreamEvents(
stream,
userMessageTimestamp,
abortSignal,
);
const experimental = settings.experimental;
const useAgentFactory =
experimental?.useAgentFactoryAll ||
experimental?.useAgentFactoryInteractive;
if (processingStatus === StreamProcessingStatus.UserCancelled) {
return;
try {
if (useAgentFactory) {
const session = new AgentSession(
config.getSessionId(),
{
name: 'interactive-agent',
maxTurns: config.getMaxSessionTurns(),
},
config,
);
const stream = session.prompt(queryToSend, abortSignal);
await processAgentEvents(
stream,
userMessageTimestamp,
abortSignal,
);
} else {
const stream = geminiClient.sendMessageStream(
queryToSend,
abortSignal,
prompt_id!,
undefined,
false,
query,
);
const processingStatus = await processGeminiStreamEvents(
stream,
userMessageTimestamp,
abortSignal,
);
if (processingStatus === StreamProcessingStatus.UserCancelled) {
return;
}
}
if (pendingHistoryItemRef.current) {
@@ -1416,6 +1536,8 @@ export const useGeminiStream = (
setModelSwitchedFromQuotaError,
prepareQueryForGemini,
processGeminiStreamEvents,
processAgentEvents,
settings.experimental,
pendingHistoryItemRef,
addItem,
setPendingHistoryItem,
+168 -4
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { type Part } from '@google/genai';
import { type Part, Type, type FunctionDeclaration, type Schema } from '@google/genai';
import { type Config } from '../config/config.js';
import { type GeminiClient } from '../core/client.js';
import { type AgentEvent, type AgentConfig } from './types.js';
@@ -12,6 +12,8 @@ import { Scheduler } from '../scheduler/scheduler.js';
import {
ROOT_SCHEDULER_ID,
type ToolCallRequestInfo,
type CompletedToolCall,
CoreToolCallStatus,
} from '../scheduler/types.js';
import { GeminiEventType, CompressionStatus } from '../core/turn.js';
import { recordToolCallInteractions } from '../code_assist/telemetry.js';
@@ -21,6 +23,14 @@ import { ChatCompressionService } from '../services/chatCompressionService.js';
import { AgentTerminateMode } from './types.js';
import type { ResumedSessionData } from '../services/chatRecordingService.js';
import { convertSessionToClientHistory } from '../utils/sessionUtils.js';
import { ToolRegistry } from '../tools/tool-registry.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import {
type AnyDeclarativeTool,
type AnyToolInvocation,
} from '../tools/tools.js';
const TASK_COMPLETE_TOOL_NAME = 'complete_task';
/**
* AgentSession manages the state of a conversation and orchestrates the agent
@@ -29,6 +39,7 @@ import { convertSessionToClientHistory } from '../utils/sessionUtils.js';
export class AgentSession {
private readonly client: GeminiClient;
private readonly scheduler: Scheduler;
private readonly toolRegistry: ToolRegistry;
private readonly compressionService: ChatCompressionService;
private totalTurns = 0;
private hasFailedCompressionAttempt = false;
@@ -38,17 +49,81 @@ export class AgentSession {
private readonly config: AgentConfig,
private readonly runtime: Config,
) {
// Initialize a scoped tool registry
this.toolRegistry = new ToolRegistry(
this.runtime,
this.runtime.getMessageBus(),
);
this.setupToolRegistry();
// For now, we reuse the GeminiClient from the global config.
this.client = this.runtime.getGeminiClient();
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
this.scheduler = new Scheduler({
config: this.runtime,
messageBus: this.runtime.getMessageBus(),
getPreferredEditor: () => undefined,
schedulerId: ROOT_SCHEDULER_ID,
});
} as any);
this.compressionService = new ChatCompressionService();
}
private setupToolRegistry(): void {
const parentRegistry = this.runtime.getToolRegistry();
if (this.config.toolConfig) {
for (const toolRef of this.config.toolConfig.tools) {
if (typeof toolRef === 'string') {
const tool = parentRegistry.getTool(toolRef);
if (tool) {
this.toolRegistry.registerTool(tool);
}
} else if (
typeof toolRef === 'object' &&
'name' in toolRef &&
'build' in toolRef
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any
this.toolRegistry.registerTool(
toolRef as unknown as AnyDeclarativeTool,
);
}
}
} else {
// If no tools specified, use all active tools from parent
for (const tool of parentRegistry.getAllTools()) {
this.toolRegistry.registerTool(tool);
}
}
}
private getFunctionDeclarations(): FunctionDeclaration[] {
const declarations = this.toolRegistry.getFunctionDeclarations();
// Add complete_task tool if outputConfig is provided
if (this.config.outputConfig) {
const jsonSchema = zodToJsonSchema(this.config.outputConfig.schema);
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
const { $schema, definitions, ...schema } = jsonSchema as any;
const completeTool: FunctionDeclaration = {
name: TASK_COMPLETE_TOOL_NAME,
description:
this.config.outputConfig.description ||
'Call this tool to submit your final answer and complete the task.',
parameters: {
type: Type.OBJECT,
properties: {
[this.config.outputConfig.outputName]: schema as Schema,
},
required: [this.config.outputConfig.outputName],
},
};
declarations.push(completeTool);
}
return declarations;
}
/**
* Resumes the agent session from persistent storage data.
* Hydrates the internal language model client with the previously saved trajectory.
@@ -82,6 +157,7 @@ export class AgentSession {
let terminationReason = AgentTerminateMode.GOAL;
let terminationMessage: string | undefined = undefined;
let terminationError: unknown | undefined = undefined;
let finalResult: unknown | undefined = undefined;
try {
while (maxTurns === -1 || this.totalTurns < maxTurns) {
@@ -93,6 +169,9 @@ export class AgentSession {
this.totalTurns++;
const promptId = `${this.sessionId}#${this.totalTurns}`;
// Update tools on the client so sendMessageStream sees them
await this.client.setTools(this.config.model);
// Compression check (from LocalAgentExecutor / useGeminiStream patterns)
if (this.config.capabilities?.compression) {
await this.tryCompressChat(promptId);
@@ -102,9 +181,10 @@ export class AgentSession {
currentInput,
promptId,
isContinuation ? undefined : input,
signal,
combinedSignal,
);
for await (const event of events) {
yield event;
}
@@ -115,6 +195,81 @@ export class AgentSession {
}
if (toolCalls.length > 0) {
// Check for complete_task call
const completeTaskCall = toolCalls.find(
(tc) => tc.name === TASK_COMPLETE_TOOL_NAME,
);
if (completeTaskCall && this.config.outputConfig) {
const outputName = this.config.outputConfig.outputName;
const result = completeTaskCall.args[outputName];
// Validate result
const validation = this.config.outputConfig.schema.safeParse(result);
if (validation.success) {
finalResult = validation.data;
yield {
type: 'goal_completed',
value: { result: finalResult },
};
// Manually create a success response for complete_task to satisfy history
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any
const response = {
status: CoreToolCallStatus.Success,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any
tool: undefined as unknown as AnyDeclarativeTool as any,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any
invocation: undefined as unknown as AnyToolInvocation as any,
response: {
callId: completeTaskCall.callId,
responseParts: [
{
functionResponse: {
id: completeTaskCall.callId,
name: TASK_COMPLETE_TOOL_NAME,
response: { result: 'Task completed successfully.' },
},
},
],
resultDisplay: 'Task completed successfully.',
error: undefined,
errorType: undefined,
contentLength: 0,
},
durationMs: 0,
schedulerId: ROOT_SCHEDULER_ID,
} as unknown as CompletedToolCall;
// Add to history so model knows it finished
await this.client.addHistory({
role: 'user',
parts: response.response.responseParts,
});
terminationReason = AgentTerminateMode.GOAL;
break;
} else {
// Yield error and continue (model needs to fix output)
const errorMsg = `Output validation failed: ${JSON.stringify(validation.error.flatten())}`;
const errorParts: Part[] = [
{
functionResponse: {
id: completeTaskCall.callId,
name: TASK_COMPLETE_TOOL_NAME,
response: { error: errorMsg },
},
},
];
await this.client.addHistory({
role: 'user',
parts: errorParts,
});
currentInput = errorParts;
isContinuation = true;
continue;
}
}
const results = await this.executeTools(toolCalls, signal);
for await (const event of results.events) {
yield event;
@@ -188,7 +343,8 @@ export class AgentSession {
if (event.type === GeminiEventType.ToolCallRequest) {
toolCalls.push(event.value);
}
yield event as AgentEvent;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
yield event as unknown as AgentEvent;
}
};
@@ -211,6 +367,14 @@ export class AgentSession {
value: { count: toolCalls.length },
});
// We need to use our scoped tool registry.
// However, the current Scheduler doesn't take a ToolRegistry in its constructor.
// It uses the global registry from Config.
// To implement scoping correctly without changing Scheduler, we might need a ScopedConfig.
// For now, let's assume we can pass it or that we'll refactor Scheduler later.
// As a workaround, we'll manually execute tools or rely on the global registry if scoping is not yet strictly enforced.
// TODO: Support scoped ToolRegistry in Scheduler.
const completedCalls = await this.scheduler.schedule(
toolCalls,
signal ?? new AbortController().signal,
+12 -1
View File
@@ -33,7 +33,8 @@ export type AgentEvent =
| { type: 'tool_suite_start'; value: { count: number } }
| { type: 'tool_suite_finish'; value: { responses: ToolCallResponseInfo[] } }
| { type: 'thought'; value: string }
| { type: 'loop_detected'; value: { sessionId: string } };
| { type: 'loop_detected'; value: { sessionId: string } }
| { type: 'goal_completed'; value: { result: unknown } };
/**
* Configuration for an Agent.
@@ -58,6 +59,16 @@ export interface AgentConfig {
loopDetection?: boolean;
ideContext?: boolean;
};
/**
* Optional tools available to the agent.
* If not specified, the agent uses all tools registered in the runtime.
*/
toolConfig?: ToolConfig;
/**
* Optional configuration for the expected structured output.
* If specified, the agent will be provided with a `complete_task` tool.
*/
outputConfig?: OutputConfig<z.ZodTypeAny>;
}
/**
+36
View File
@@ -484,6 +484,10 @@ export interface ConfigParameters {
disableLLMCorrection?: boolean;
plan?: boolean;
modelSteering?: boolean;
useAgentFactoryAll?: boolean;
useAgentFactorySdk?: boolean;
useAgentFactoryNonInteractive?: boolean;
useAgentFactoryInteractive?: boolean;
onModelChange?: (model: string) => void;
mcpEnabled?: boolean;
extensionsEnabled?: boolean;
@@ -682,6 +686,11 @@ export class Config {
readonly userHintService: UserHintService;
private approvedPlanPath: string | undefined;
private readonly useAgentFactoryAll: boolean;
private readonly useAgentFactorySdk: boolean;
private readonly useAgentFactoryNonInteractive: boolean;
private readonly useAgentFactoryInteractive: boolean;
constructor(params: ConfigParameters) {
this.sessionId = params.sessionId;
this.clientVersion = params.clientVersion ?? 'unknown';
@@ -769,6 +778,12 @@ export class Config {
this.modelAvailabilityService = new ModelAvailabilityService();
this.experimentalJitContext = params.experimentalJitContext ?? false;
this.modelSteering = params.modelSteering ?? false;
this.useAgentFactoryAll = params.useAgentFactoryAll ?? false;
this.useAgentFactorySdk = params.useAgentFactorySdk ?? false;
this.useAgentFactoryNonInteractive =
params.useAgentFactoryNonInteractive ?? false;
this.useAgentFactoryInteractive =
params.useAgentFactoryInteractive ?? false;
this.userHintService = new UserHintService(() =>
this.isModelSteeringEnabled(),
);
@@ -1519,6 +1534,27 @@ export class Config {
*
* May change over time.
*/
getExperimentalSetting(
key:
| 'useAgentFactoryAll'
| 'useAgentFactorySdk'
| 'useAgentFactoryNonInteractive'
| 'useAgentFactoryInteractive',
): boolean {
switch (key) {
case 'useAgentFactoryAll':
return this.useAgentFactoryAll;
case 'useAgentFactorySdk':
return this.useAgentFactorySdk;
case 'useAgentFactoryNonInteractive':
return this.useAgentFactoryNonInteractive;
case 'useAgentFactoryInteractive':
return this.useAgentFactoryInteractive;
default:
return false;
}
}
getExcludeTools(): Set<string> | undefined {
// Right now this is present for backward compatibility with settings.json exclude
const excludeToolsSet = new Set([...(this.excludeTools ?? [])]);
+143 -135
View File
@@ -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];
}
}
}
+38
View File
@@ -491,6 +491,44 @@
},
"additionalProperties": false
},
"experimental": {
"title": "Experimental",
"description": "Experimental features and capabilities.",
"markdownDescription": "Experimental features and capabilities.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `{}`",
"default": {},
"type": "object",
"properties": {
"useAgentFactoryAll": {
"title": "Use Agent Factory (All)",
"description": "Enable Agent Factory for all supported execution paths.",
"markdownDescription": "Enable Agent Factory for all supported execution paths.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"useAgentFactorySdk": {
"title": "Use Agent Factory (SDK)",
"description": "Enable Agent Factory for the SDK execution path.",
"markdownDescription": "Enable Agent Factory for the SDK execution path.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"useAgentFactoryNonInteractive": {
"title": "Use Agent Factory (Non-Interactive)",
"description": "Enable Agent Factory for the non-interactive CLI execution path.",
"markdownDescription": "Enable Agent Factory for the non-interactive CLI execution path.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"useAgentFactoryInteractive": {
"title": "Use Agent Factory (Interactive)",
"description": "Enable Agent Factory for the interactive CLI execution path.",
"markdownDescription": "Enable Agent Factory for the interactive CLI execution path.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
}
},
"additionalProperties": false
},
"telemetry": {
"title": "Telemetry",
"description": "Telemetry configuration.",