mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
Headless enhancement: add stream-json as input-format/output-format to support programmatically use (#926)
This commit is contained in:
@@ -62,7 +62,7 @@ import { WriteFileTool } from '../tools/write-file.js';
|
||||
|
||||
// Other modules
|
||||
import { ideContextStore } from '../ide/ideContext.js';
|
||||
import { OutputFormat } from '../output/types.js';
|
||||
import { InputFormat, OutputFormat } from '../output/types.js';
|
||||
import { PromptRegistry } from '../prompts/prompt-registry.js';
|
||||
import { SubagentManager } from '../subagents/subagent-manager.js';
|
||||
import {
|
||||
@@ -216,6 +216,7 @@ export interface ConfigParameters {
|
||||
sandbox?: SandboxConfig;
|
||||
targetDir: string;
|
||||
debugMode: boolean;
|
||||
includePartialMessages?: boolean;
|
||||
question?: string;
|
||||
fullContext?: boolean;
|
||||
coreTools?: string[];
|
||||
@@ -290,6 +291,27 @@ export interface ConfigParameters {
|
||||
useSmartEdit?: boolean;
|
||||
output?: OutputSettings;
|
||||
skipStartupContext?: boolean;
|
||||
inputFormat?: InputFormat;
|
||||
outputFormat?: OutputFormat;
|
||||
}
|
||||
|
||||
function normalizeConfigOutputFormat(
|
||||
format: OutputFormat | undefined,
|
||||
): OutputFormat | undefined {
|
||||
if (!format) {
|
||||
return undefined;
|
||||
}
|
||||
switch (format) {
|
||||
case 'stream-json':
|
||||
return OutputFormat.STREAM_JSON;
|
||||
case 'json':
|
||||
case OutputFormat.JSON:
|
||||
return OutputFormat.JSON;
|
||||
case 'text':
|
||||
case OutputFormat.TEXT:
|
||||
default:
|
||||
return OutputFormat.TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
export class Config {
|
||||
@@ -306,6 +328,9 @@ export class Config {
|
||||
private readonly targetDir: string;
|
||||
private workspaceContext: WorkspaceContext;
|
||||
private readonly debugMode: boolean;
|
||||
private readonly inputFormat: InputFormat;
|
||||
private readonly outputFormat: OutputFormat;
|
||||
private readonly includePartialMessages: boolean;
|
||||
private readonly question: string | undefined;
|
||||
private readonly fullContext: boolean;
|
||||
private readonly coreTools: string[] | undefined;
|
||||
@@ -388,7 +413,6 @@ export class Config {
|
||||
private readonly enableToolOutputTruncation: boolean;
|
||||
private readonly eventEmitter?: EventEmitter;
|
||||
private readonly useSmartEdit: boolean;
|
||||
private readonly outputSettings: OutputSettings;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
this.sessionId = params.sessionId;
|
||||
@@ -401,6 +425,12 @@ export class Config {
|
||||
params.includeDirectories ?? [],
|
||||
);
|
||||
this.debugMode = params.debugMode;
|
||||
this.inputFormat = params.inputFormat ?? InputFormat.TEXT;
|
||||
const normalizedOutputFormat = normalizeConfigOutputFormat(
|
||||
params.outputFormat ?? params.output?.format,
|
||||
);
|
||||
this.outputFormat = normalizedOutputFormat ?? OutputFormat.TEXT;
|
||||
this.includePartialMessages = params.includePartialMessages ?? false;
|
||||
this.question = params.question;
|
||||
this.fullContext = params.fullContext ?? false;
|
||||
this.coreTools = params.coreTools;
|
||||
@@ -495,12 +525,9 @@ export class Config {
|
||||
this.extensionManagement = params.extensionManagement ?? true;
|
||||
this.storage = new Storage(this.targetDir);
|
||||
this.vlmSwitchMode = params.vlmSwitchMode;
|
||||
this.inputFormat = params.inputFormat ?? InputFormat.TEXT;
|
||||
this.fileExclusions = new FileExclusions(this);
|
||||
this.eventEmitter = params.eventEmitter;
|
||||
this.outputSettings = {
|
||||
format: params.output?.format ?? OutputFormat.TEXT,
|
||||
};
|
||||
|
||||
if (params.contextFileName) {
|
||||
setGeminiMdFilename(params.contextFileName);
|
||||
}
|
||||
@@ -786,6 +813,14 @@ export class Config {
|
||||
return this.showMemoryUsage;
|
||||
}
|
||||
|
||||
getInputFormat(): 'text' | 'stream-json' {
|
||||
return this.inputFormat;
|
||||
}
|
||||
|
||||
getIncludePartialMessages(): boolean {
|
||||
return this.includePartialMessages;
|
||||
}
|
||||
|
||||
getAccessibility(): AccessibilitySettings {
|
||||
return this.accessibility;
|
||||
}
|
||||
@@ -1082,9 +1117,7 @@ export class Config {
|
||||
}
|
||||
|
||||
getOutputFormat(): OutputFormat {
|
||||
return this.outputSettings?.format
|
||||
? this.outputSettings.format
|
||||
: OutputFormat.TEXT;
|
||||
return this.outputFormat;
|
||||
}
|
||||
|
||||
async getGitService(): Promise<GitService> {
|
||||
|
||||
@@ -371,6 +371,8 @@ describe('CoreToolScheduler', () => {
|
||||
getUseSmartEdit: () => false,
|
||||
getUseModelRouter: () => false,
|
||||
getGeminiClient: () => null, // No client needed for these tests
|
||||
getExcludeTools: () => undefined,
|
||||
isInteractive: () => true,
|
||||
} as unknown as Config;
|
||||
const mockToolRegistry = {
|
||||
getAllToolNames: () => ['list_files', 'read_file', 'write_file'],
|
||||
@@ -400,6 +402,241 @@ describe('CoreToolScheduler', () => {
|
||||
' Did you mean one of: "list_files", "read_file", "write_file"?',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use Levenshtein suggestions for excluded tools (getToolSuggestion only handles non-excluded)', () => {
|
||||
// Create mocked tool registry
|
||||
const mockToolRegistry = {
|
||||
getAllToolNames: () => ['list_files', 'read_file'],
|
||||
} as unknown as ToolRegistry;
|
||||
|
||||
// Create mocked config with excluded tools
|
||||
const mockConfig = {
|
||||
getToolRegistry: () => mockToolRegistry,
|
||||
getUseSmartEdit: () => false,
|
||||
getUseModelRouter: () => false,
|
||||
getGeminiClient: () => null,
|
||||
getExcludeTools: () => ['write_file', 'edit', 'run_shell_command'],
|
||||
isInteractive: () => false, // Value doesn't matter, but included for completeness
|
||||
} as unknown as Config;
|
||||
|
||||
// Create scheduler
|
||||
const scheduler = new CoreToolScheduler({
|
||||
config: mockConfig,
|
||||
getPreferredEditor: () => 'vscode',
|
||||
onEditorClose: vi.fn(),
|
||||
});
|
||||
|
||||
// getToolSuggestion no longer handles excluded tools - it only handles truly missing tools
|
||||
// So excluded tools will use Levenshtein distance to find similar registered tools
|
||||
// @ts-expect-error accessing private method
|
||||
const excludedTool = scheduler.getToolSuggestion('write_file');
|
||||
expect(excludedTool).toContain('Did you mean');
|
||||
});
|
||||
|
||||
it('should use Levenshtein suggestions for non-excluded tools', () => {
|
||||
// Create mocked tool registry
|
||||
const mockToolRegistry = {
|
||||
getAllToolNames: () => ['list_files', 'read_file'],
|
||||
} as unknown as ToolRegistry;
|
||||
|
||||
// Create mocked config with excluded tools
|
||||
const mockConfig = {
|
||||
getToolRegistry: () => mockToolRegistry,
|
||||
getUseSmartEdit: () => false,
|
||||
getUseModelRouter: () => false,
|
||||
getGeminiClient: () => null,
|
||||
getExcludeTools: () => ['write_file', 'edit'],
|
||||
isInteractive: () => false, // Value doesn't matter
|
||||
} as unknown as Config;
|
||||
|
||||
// Create scheduler
|
||||
const scheduler = new CoreToolScheduler({
|
||||
config: mockConfig,
|
||||
getPreferredEditor: () => 'vscode',
|
||||
onEditorClose: vi.fn(),
|
||||
});
|
||||
|
||||
// Test that non-excluded tool (hallucinated) still uses Levenshtein suggestions
|
||||
// @ts-expect-error accessing private method
|
||||
const hallucinatedTool = scheduler.getToolSuggestion('list_fils');
|
||||
expect(hallucinatedTool).toContain('Did you mean');
|
||||
expect(hallucinatedTool).not.toContain(
|
||||
'not available in the current environment',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('excluded tools handling', () => {
|
||||
it('should return permission error for excluded tools instead of "not found" message', async () => {
|
||||
const onAllToolCallsComplete = vi.fn();
|
||||
const onToolCallsUpdate = vi.fn();
|
||||
|
||||
const mockToolRegistry = {
|
||||
getTool: () => undefined, // Tool not in registry
|
||||
getAllToolNames: () => ['list_files', 'read_file'],
|
||||
getFunctionDeclarations: () => [],
|
||||
tools: new Map(),
|
||||
discovery: {},
|
||||
registerTool: () => {},
|
||||
getToolByName: () => undefined,
|
||||
getToolByDisplayName: () => undefined,
|
||||
getTools: () => [],
|
||||
discoverTools: async () => {},
|
||||
getAllTools: () => [],
|
||||
getToolsByServer: () => [],
|
||||
} as unknown as ToolRegistry;
|
||||
|
||||
const mockConfig = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getDebugMode: () => false,
|
||||
getApprovalMode: () => ApprovalMode.DEFAULT,
|
||||
getAllowedTools: () => [],
|
||||
getExcludeTools: () => ['write_file', 'edit', 'run_shell_command'],
|
||||
getContentGeneratorConfig: () => ({
|
||||
model: 'test-model',
|
||||
authType: 'oauth-personal',
|
||||
}),
|
||||
getShellExecutionConfig: () => ({
|
||||
terminalWidth: 90,
|
||||
terminalHeight: 30,
|
||||
}),
|
||||
storage: {
|
||||
getProjectTempDir: () => '/tmp',
|
||||
},
|
||||
getTruncateToolOutputThreshold: () =>
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
getToolRegistry: () => mockToolRegistry,
|
||||
getUseSmartEdit: () => false,
|
||||
getUseModelRouter: () => false,
|
||||
getGeminiClient: () => null,
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
config: mockConfig,
|
||||
onAllToolCallsComplete,
|
||||
onToolCallsUpdate,
|
||||
getPreferredEditor: () => 'vscode',
|
||||
onEditorClose: vi.fn(),
|
||||
});
|
||||
|
||||
const abortController = new AbortController();
|
||||
const request = {
|
||||
callId: '1',
|
||||
name: 'write_file', // Excluded tool
|
||||
args: {},
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-id-excluded',
|
||||
};
|
||||
|
||||
await scheduler.schedule([request], abortController.signal);
|
||||
|
||||
// Wait for completion
|
||||
await vi.waitFor(() => {
|
||||
expect(onAllToolCallsComplete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const completedCalls = onAllToolCallsComplete.mock
|
||||
.calls[0][0] as ToolCall[];
|
||||
expect(completedCalls).toHaveLength(1);
|
||||
const completedCall = completedCalls[0];
|
||||
expect(completedCall.status).toBe('error');
|
||||
|
||||
if (completedCall.status === 'error') {
|
||||
const errorMessage = completedCall.response.error?.message;
|
||||
expect(errorMessage).toBe(
|
||||
'Qwen Code requires permission to use write_file, but that permission was declined.',
|
||||
);
|
||||
// Should NOT contain "not found in registry"
|
||||
expect(errorMessage).not.toContain('not found in registry');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return "not found" message for truly missing tools (not excluded)', async () => {
|
||||
const onAllToolCallsComplete = vi.fn();
|
||||
const onToolCallsUpdate = vi.fn();
|
||||
|
||||
const mockToolRegistry = {
|
||||
getTool: () => undefined, // Tool not in registry
|
||||
getAllToolNames: () => ['list_files', 'read_file'],
|
||||
getFunctionDeclarations: () => [],
|
||||
tools: new Map(),
|
||||
discovery: {},
|
||||
registerTool: () => {},
|
||||
getToolByName: () => undefined,
|
||||
getToolByDisplayName: () => undefined,
|
||||
getTools: () => [],
|
||||
discoverTools: async () => {},
|
||||
getAllTools: () => [],
|
||||
getToolsByServer: () => [],
|
||||
} as unknown as ToolRegistry;
|
||||
|
||||
const mockConfig = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getDebugMode: () => false,
|
||||
getApprovalMode: () => ApprovalMode.DEFAULT,
|
||||
getAllowedTools: () => [],
|
||||
getExcludeTools: () => ['write_file', 'edit'], // Different excluded tools
|
||||
getContentGeneratorConfig: () => ({
|
||||
model: 'test-model',
|
||||
authType: 'oauth-personal',
|
||||
}),
|
||||
getShellExecutionConfig: () => ({
|
||||
terminalWidth: 90,
|
||||
terminalHeight: 30,
|
||||
}),
|
||||
storage: {
|
||||
getProjectTempDir: () => '/tmp',
|
||||
},
|
||||
getTruncateToolOutputThreshold: () =>
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
getToolRegistry: () => mockToolRegistry,
|
||||
getUseSmartEdit: () => false,
|
||||
getUseModelRouter: () => false,
|
||||
getGeminiClient: () => null,
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
config: mockConfig,
|
||||
onAllToolCallsComplete,
|
||||
onToolCallsUpdate,
|
||||
getPreferredEditor: () => 'vscode',
|
||||
onEditorClose: vi.fn(),
|
||||
});
|
||||
|
||||
const abortController = new AbortController();
|
||||
const request = {
|
||||
callId: '1',
|
||||
name: 'nonexistent_tool', // Not excluded, just doesn't exist
|
||||
args: {},
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-id-missing',
|
||||
};
|
||||
|
||||
await scheduler.schedule([request], abortController.signal);
|
||||
|
||||
// Wait for completion
|
||||
await vi.waitFor(() => {
|
||||
expect(onAllToolCallsComplete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const completedCalls = onAllToolCallsComplete.mock
|
||||
.calls[0][0] as ToolCall[];
|
||||
expect(completedCalls).toHaveLength(1);
|
||||
const completedCall = completedCalls[0];
|
||||
expect(completedCall.status).toBe('error');
|
||||
|
||||
if (completedCall.status === 'error') {
|
||||
const errorMessage = completedCall.response.error?.message;
|
||||
// Should contain "not found in registry"
|
||||
expect(errorMessage).toContain('not found in registry');
|
||||
// Should NOT contain permission message
|
||||
expect(errorMessage).not.toContain('requires permission');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -449,6 +686,9 @@ describe('CoreToolScheduler with payload', () => {
|
||||
getUseSmartEdit: () => false,
|
||||
getUseModelRouter: () => false,
|
||||
getGeminiClient: () => null, // No client needed for these tests
|
||||
isInteractive: () => true, // Required to prevent auto-denial of tool calls
|
||||
getIdeMode: () => false,
|
||||
getExperimentalZedIntegration: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
@@ -769,6 +1009,9 @@ describe('CoreToolScheduler edit cancellation', () => {
|
||||
getUseSmartEdit: () => false,
|
||||
getUseModelRouter: () => false,
|
||||
getGeminiClient: () => null, // No client needed for these tests
|
||||
isInteractive: () => true, // Required to prevent auto-denial of tool calls
|
||||
getIdeMode: () => false,
|
||||
getExperimentalZedIntegration: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
@@ -1421,6 +1664,9 @@ describe('CoreToolScheduler request queueing', () => {
|
||||
getUseSmartEdit: () => false,
|
||||
getUseModelRouter: () => false,
|
||||
getGeminiClient: () => null, // No client needed for these tests
|
||||
isInteractive: () => true, // Required to prevent auto-denial of tool calls
|
||||
getIdeMode: () => false,
|
||||
getExperimentalZedIntegration: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const testTool = new TestApprovalTool(mockConfig);
|
||||
@@ -1450,7 +1696,10 @@ describe('CoreToolScheduler request queueing', () => {
|
||||
const onAllToolCallsComplete = vi.fn();
|
||||
const onToolCallsUpdate = vi.fn();
|
||||
const pendingConfirmations: Array<
|
||||
(outcome: ToolConfirmationOutcome) => void
|
||||
(
|
||||
outcome: ToolConfirmationOutcome,
|
||||
payload?: ToolConfirmationPayload,
|
||||
) => Promise<void>
|
||||
> = [];
|
||||
|
||||
const scheduler = new CoreToolScheduler({
|
||||
@@ -1521,7 +1770,7 @@ describe('CoreToolScheduler request queueing', () => {
|
||||
|
||||
// Approve the first tool with ProceedAlways
|
||||
const firstConfirmation = pendingConfirmations[0];
|
||||
firstConfirmation(ToolConfirmationOutcome.ProceedAlways);
|
||||
await firstConfirmation(ToolConfirmationOutcome.ProceedAlways);
|
||||
|
||||
// Wait for all tools to be completed
|
||||
await vi.waitFor(() => {
|
||||
|
||||
@@ -587,12 +587,16 @@ export class CoreToolScheduler {
|
||||
|
||||
/**
|
||||
* Generates a suggestion string for a tool name that was not found in the registry.
|
||||
* It finds the closest matches based on Levenshtein distance.
|
||||
* Uses Levenshtein distance to suggest similar tool names for hallucinated or misspelled tools.
|
||||
* Note: Excluded tools are handled separately before calling this method, so this only
|
||||
* handles the case where a tool is truly not found (hallucinated or typo).
|
||||
* @param unknownToolName The tool name that was not found.
|
||||
* @param topN The number of suggestions to return. Defaults to 3.
|
||||
* @returns A suggestion string like " Did you mean 'tool'?" or " Did you mean one of: 'tool1', 'tool2'?", or an empty string if no suggestions are found.
|
||||
* @returns A suggestion string like " Did you mean 'tool'?" or " Did you mean one of: 'tool1', 'tool2'?",
|
||||
* or an empty string if no suggestions are found.
|
||||
*/
|
||||
private getToolSuggestion(unknownToolName: string, topN = 3): string {
|
||||
// Use Levenshtein distance to find similar tool names from the registry.
|
||||
const allToolNames = this.toolRegistry.getAllToolNames();
|
||||
|
||||
const matches = allToolNames.map((toolName) => ({
|
||||
@@ -670,8 +674,35 @@ export class CoreToolScheduler {
|
||||
|
||||
const newToolCalls: ToolCall[] = requestsToProcess.map(
|
||||
(reqInfo): ToolCall => {
|
||||
// Check if the tool is excluded due to permissions/environment restrictions
|
||||
// This check should happen before registry lookup to provide a clear permission error
|
||||
const excludeTools = this.config.getExcludeTools?.() ?? undefined;
|
||||
if (excludeTools && excludeTools.length > 0) {
|
||||
const normalizedToolName = reqInfo.name.toLowerCase().trim();
|
||||
const excludedMatch = excludeTools.find(
|
||||
(excludedTool) =>
|
||||
excludedTool.toLowerCase().trim() === normalizedToolName,
|
||||
);
|
||||
|
||||
if (excludedMatch) {
|
||||
// The tool exists but is excluded - return permission error directly
|
||||
const permissionErrorMessage = `Qwen Code requires permission to use ${excludedMatch}, but that permission was declined.`;
|
||||
return {
|
||||
status: 'error',
|
||||
request: reqInfo,
|
||||
response: createErrorResponse(
|
||||
reqInfo,
|
||||
new Error(permissionErrorMessage),
|
||||
ToolErrorType.EXECUTION_DENIED,
|
||||
),
|
||||
durationMs: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const toolInstance = this.toolRegistry.getTool(reqInfo.name);
|
||||
if (!toolInstance) {
|
||||
// Tool is not in registry and not excluded - likely hallucinated or typo
|
||||
const suggestion = this.getToolSuggestion(reqInfo.name);
|
||||
const errorMessage = `Tool "${reqInfo.name}" not found in registry. Tools must use the exact names that are registered.${suggestion}`;
|
||||
return {
|
||||
@@ -777,6 +808,32 @@ export class CoreToolScheduler {
|
||||
);
|
||||
this.setStatusInternal(reqInfo.callId, 'scheduled');
|
||||
} else {
|
||||
/**
|
||||
* In non-interactive mode where no user will respond to approval prompts,
|
||||
* and not running as IDE companion or Zed integration, automatically deny approval.
|
||||
* This is intended to create an explicit denial of the tool call,
|
||||
* rather than silently waiting for approval and hanging forever.
|
||||
*/
|
||||
const shouldAutoDeny =
|
||||
!this.config.isInteractive() &&
|
||||
!this.config.getIdeMode() &&
|
||||
!this.config.getExperimentalZedIntegration();
|
||||
|
||||
if (shouldAutoDeny) {
|
||||
// Treat as execution denied error, similar to excluded tools
|
||||
const errorMessage = `Qwen Code requires permission to use "${reqInfo.name}", but that permission was declined.`;
|
||||
this.setStatusInternal(
|
||||
reqInfo.callId,
|
||||
'error',
|
||||
createErrorResponse(
|
||||
reqInfo,
|
||||
new Error(errorMessage),
|
||||
ToolErrorType.EXECUTION_DENIED,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow IDE to resolve confirmation
|
||||
if (
|
||||
confirmationDetails.type === 'edit' &&
|
||||
|
||||
@@ -9,7 +9,18 @@ import type {
|
||||
ToolCallResponseInfo,
|
||||
Config,
|
||||
} from '../index.js';
|
||||
import { CoreToolScheduler } from './coreToolScheduler.js';
|
||||
import {
|
||||
CoreToolScheduler,
|
||||
type AllToolCallsCompleteHandler,
|
||||
type OutputUpdateHandler,
|
||||
type ToolCallsUpdateHandler,
|
||||
} from './coreToolScheduler.js';
|
||||
|
||||
export interface ExecuteToolCallOptions {
|
||||
outputUpdateHandler?: OutputUpdateHandler;
|
||||
onAllToolCallsComplete?: AllToolCallsCompleteHandler;
|
||||
onToolCallsUpdate?: ToolCallsUpdateHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a single tool call non-interactively by leveraging the CoreToolScheduler.
|
||||
@@ -18,15 +29,21 @@ export async function executeToolCall(
|
||||
config: Config,
|
||||
toolCallRequest: ToolCallRequestInfo,
|
||||
abortSignal: AbortSignal,
|
||||
options: ExecuteToolCallOptions = {},
|
||||
): Promise<ToolCallResponseInfo> {
|
||||
return new Promise<ToolCallResponseInfo>((resolve, reject) => {
|
||||
new CoreToolScheduler({
|
||||
config,
|
||||
getPreferredEditor: () => undefined,
|
||||
onEditorClose: () => {},
|
||||
outputUpdateHandler: options.outputUpdateHandler,
|
||||
onAllToolCallsComplete: async (completedToolCalls) => {
|
||||
if (options.onAllToolCallsComplete) {
|
||||
await options.onAllToolCallsComplete(completedToolCalls);
|
||||
}
|
||||
resolve(completedToolCalls[0].response);
|
||||
},
|
||||
onToolCallsUpdate: options.onToolCallsUpdate,
|
||||
getPreferredEditor: () => undefined,
|
||||
onEditorClose: () => {},
|
||||
})
|
||||
.schedule(toolCallRequest, abortSignal)
|
||||
.catch(reject);
|
||||
|
||||
@@ -6,9 +6,15 @@
|
||||
|
||||
import type { SessionMetrics } from '../telemetry/uiTelemetry.js';
|
||||
|
||||
export enum InputFormat {
|
||||
TEXT = 'text',
|
||||
STREAM_JSON = 'stream-json',
|
||||
}
|
||||
|
||||
export enum OutputFormat {
|
||||
TEXT = 'text',
|
||||
JSON = 'json',
|
||||
STREAM_JSON = 'stream-json',
|
||||
}
|
||||
|
||||
export interface JsonError {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
ToolCallConfirmationDetails,
|
||||
ToolConfirmationOutcome,
|
||||
} from '../tools/tools.js';
|
||||
import type { Part } from '@google/genai';
|
||||
|
||||
export type SubAgentEvent =
|
||||
| 'start'
|
||||
@@ -72,6 +73,7 @@ export interface SubAgentToolResultEvent {
|
||||
name: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
responseParts?: Part[];
|
||||
resultDisplay?: string;
|
||||
durationMs?: number;
|
||||
timestamp: number;
|
||||
|
||||
@@ -619,6 +619,13 @@ export class SubAgentScope {
|
||||
name: toolName,
|
||||
success,
|
||||
error: errorMessage,
|
||||
responseParts: call.response.responseParts,
|
||||
/**
|
||||
* Tools like todoWrite will add some extra contents to the result,
|
||||
* making it unable to deserialize the `responseParts` to a JSON object.
|
||||
* While `resultDisplay` is normally a string, if not we stringify it,
|
||||
* so that we can deserialize it to a JSON object when needed.
|
||||
*/
|
||||
resultDisplay: call.response.resultDisplay
|
||||
? typeof call.response.resultDisplay === 'string'
|
||||
? call.response.resultDisplay
|
||||
|
||||
@@ -332,7 +332,7 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
||||
...this.currentToolCalls![toolCallIndex],
|
||||
status: event.success ? 'success' : 'failed',
|
||||
error: event.error,
|
||||
resultDisplay: event.resultDisplay,
|
||||
responseParts: event.responseParts,
|
||||
};
|
||||
|
||||
this.updateDisplay(
|
||||
|
||||
@@ -14,6 +14,8 @@ export enum ToolErrorType {
|
||||
UNHANDLED_EXCEPTION = 'unhandled_exception',
|
||||
TOOL_NOT_REGISTERED = 'tool_not_registered',
|
||||
EXECUTION_FAILED = 'execution_failed',
|
||||
// Try to execute a tool that is excluded due to the approval mode
|
||||
EXECUTION_DENIED = 'execution_denied',
|
||||
|
||||
// File System Errors
|
||||
FILE_NOT_FOUND = 'file_not_found',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { FunctionDeclaration, PartListUnion } from '@google/genai';
|
||||
import type { FunctionDeclaration, Part, PartListUnion } from '@google/genai';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
import type { DiffUpdateResult } from '../ide/ide-client.js';
|
||||
import type { ShellExecutionConfig } from '../services/shellExecutionService.js';
|
||||
@@ -461,6 +461,7 @@ export interface TaskResultDisplay {
|
||||
args?: Record<string, unknown>;
|
||||
result?: string;
|
||||
resultDisplay?: string;
|
||||
responseParts?: Part[];
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user