mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: create draft framework for cli & sdk
This commit is contained in:
@@ -65,6 +65,7 @@ import { ideContextStore } from '../ide/ideContext.js';
|
||||
import { InputFormat, OutputFormat } from '../output/types.js';
|
||||
import { PromptRegistry } from '../prompts/prompt-registry.js';
|
||||
import { SubagentManager } from '../subagents/subagent-manager.js';
|
||||
import type { SubagentConfig } from '../subagents/types.js';
|
||||
import {
|
||||
DEFAULT_OTLP_ENDPOINT,
|
||||
DEFAULT_TELEMETRY_TARGET,
|
||||
@@ -333,9 +334,11 @@ export interface ConfigParameters {
|
||||
eventEmitter?: EventEmitter;
|
||||
useSmartEdit?: boolean;
|
||||
output?: OutputSettings;
|
||||
skipStartupContext?: boolean;
|
||||
inputFormat?: InputFormat;
|
||||
outputFormat?: OutputFormat;
|
||||
skipStartupContext?: boolean;
|
||||
sdkMode?: boolean;
|
||||
sessionSubagents?: SubagentConfig[];
|
||||
}
|
||||
|
||||
function normalizeConfigOutputFormat(
|
||||
@@ -383,8 +386,10 @@ export class Config {
|
||||
private readonly toolDiscoveryCommand: string | undefined;
|
||||
private readonly toolCallCommand: string | undefined;
|
||||
private readonly mcpServerCommand: string | undefined;
|
||||
private readonly mcpServers: Record<string, MCPServerConfig> | undefined;
|
||||
private mcpServers: Record<string, MCPServerConfig> | undefined;
|
||||
private sessionSubagents: SubagentConfig[];
|
||||
private userMemory: string;
|
||||
private sdkMode: boolean;
|
||||
private geminiMdFileCount: number;
|
||||
private approvalMode: ApprovalMode;
|
||||
private readonly showMemoryUsage: boolean;
|
||||
@@ -487,6 +492,8 @@ export class Config {
|
||||
this.toolCallCommand = params.toolCallCommand;
|
||||
this.mcpServerCommand = params.mcpServerCommand;
|
||||
this.mcpServers = params.mcpServers;
|
||||
this.sessionSubagents = params.sessionSubagents ?? [];
|
||||
this.sdkMode = params.sdkMode ?? false;
|
||||
this.userMemory = params.userMemory ?? '';
|
||||
this.geminiMdFileCount = params.geminiMdFileCount ?? 0;
|
||||
this.approvalMode = params.approvalMode ?? ApprovalMode.DEFAULT;
|
||||
@@ -842,6 +849,46 @@ export class Config {
|
||||
return this.mcpServers;
|
||||
}
|
||||
|
||||
setMcpServers(servers: Record<string, MCPServerConfig>): void {
|
||||
if (this.initialized) {
|
||||
throw new Error('Cannot modify mcpServers after initialization');
|
||||
}
|
||||
this.mcpServers = servers;
|
||||
}
|
||||
|
||||
addMcpServers(servers: Record<string, MCPServerConfig>): void {
|
||||
if (this.initialized) {
|
||||
throw new Error('Cannot modify mcpServers after initialization');
|
||||
}
|
||||
this.mcpServers = { ...this.mcpServers, ...servers };
|
||||
}
|
||||
|
||||
getSessionSubagents(): SubagentConfig[] {
|
||||
return this.sessionSubagents;
|
||||
}
|
||||
|
||||
setSessionSubagents(subagents: SubagentConfig[]): void {
|
||||
if (this.initialized) {
|
||||
throw new Error('Cannot modify sessionSubagents after initialization');
|
||||
}
|
||||
this.sessionSubagents = subagents;
|
||||
}
|
||||
|
||||
addSessionSubagents(subagents: SubagentConfig[]): void {
|
||||
if (this.initialized) {
|
||||
throw new Error('Cannot modify sessionSubagents after initialization');
|
||||
}
|
||||
this.sessionSubagents = [...this.sessionSubagents, ...subagents];
|
||||
}
|
||||
|
||||
getSdkMode(): boolean {
|
||||
return this.sdkMode;
|
||||
}
|
||||
|
||||
setSdkMode(value: boolean): void {
|
||||
this.sdkMode = value;
|
||||
}
|
||||
|
||||
getUserMemory(): string {
|
||||
return this.userMemory;
|
||||
}
|
||||
|
||||
@@ -916,7 +916,10 @@ export class CoreToolScheduler {
|
||||
|
||||
async handleConfirmationResponse(
|
||||
callId: string,
|
||||
originalOnConfirm: (outcome: ToolConfirmationOutcome) => Promise<void>,
|
||||
originalOnConfirm: (
|
||||
outcome: ToolConfirmationOutcome,
|
||||
payload?: ToolConfirmationPayload,
|
||||
) => Promise<void>,
|
||||
outcome: ToolConfirmationOutcome,
|
||||
signal: AbortSignal,
|
||||
payload?: ToolConfirmationPayload,
|
||||
@@ -925,9 +928,7 @@ export class CoreToolScheduler {
|
||||
(c) => c.request.callId === callId && c.status === 'awaiting_approval',
|
||||
);
|
||||
|
||||
if (toolCall && toolCall.status === 'awaiting_approval') {
|
||||
await originalOnConfirm(outcome);
|
||||
}
|
||||
await originalOnConfirm(outcome, payload);
|
||||
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||
await this.autoApproveCompatiblePendingTools(signal, callId);
|
||||
@@ -936,11 +937,10 @@ export class CoreToolScheduler {
|
||||
this.setToolCallOutcome(callId, outcome);
|
||||
|
||||
if (outcome === ToolConfirmationOutcome.Cancel || signal.aborted) {
|
||||
this.setStatusInternal(
|
||||
callId,
|
||||
'cancelled',
|
||||
'User did not allow tool call',
|
||||
);
|
||||
// Use custom cancel message from payload if provided, otherwise use default
|
||||
const cancelMessage =
|
||||
payload?.cancelMessage || 'User did not allow tool call';
|
||||
this.setStatusInternal(callId, 'cancelled', cancelMessage);
|
||||
} else if (outcome === ToolConfirmationOutcome.ModifyWithEditor) {
|
||||
const waitingToolCall = toolCall as WaitingToolCall;
|
||||
if (isModifiableDeclarativeTool(waitingToolCall.tool)) {
|
||||
@@ -998,7 +998,8 @@ export class CoreToolScheduler {
|
||||
): Promise<void> {
|
||||
if (
|
||||
toolCall.confirmationDetails.type !== 'edit' ||
|
||||
!isModifiableDeclarativeTool(toolCall.tool)
|
||||
!isModifiableDeclarativeTool(toolCall.tool) ||
|
||||
!payload.newContent
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { EventEmitter } from 'events';
|
||||
import type {
|
||||
ToolCallConfirmationDetails,
|
||||
ToolConfirmationOutcome,
|
||||
ToolResultDisplay,
|
||||
} from '../tools/tools.js';
|
||||
import type { Part } from '@google/genai';
|
||||
|
||||
@@ -74,7 +75,7 @@ export interface SubAgentToolResultEvent {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
responseParts?: Part[];
|
||||
resultDisplay?: string;
|
||||
resultDisplay?: ToolResultDisplay;
|
||||
durationMs?: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,15 @@ export class SubagentManager {
|
||||
): Promise<void> {
|
||||
this.validator.validateOrThrow(config);
|
||||
|
||||
// Prevent creating session-level agents
|
||||
if (options.level === 'session') {
|
||||
throw new SubagentError(
|
||||
`Cannot create session-level subagent "${config.name}". Session agents are read-only and provided at runtime.`,
|
||||
SubagentErrorCode.INVALID_CONFIG,
|
||||
config.name,
|
||||
);
|
||||
}
|
||||
|
||||
// Determine file path
|
||||
const filePath =
|
||||
options.customPath || this.getSubagentPath(config.name, options.level);
|
||||
@@ -142,6 +151,11 @@ export class SubagentManager {
|
||||
return BuiltinAgentRegistry.getBuiltinAgent(name);
|
||||
}
|
||||
|
||||
if (level === 'session') {
|
||||
const sessionSubagents = this.subagentsCache?.get('session') || [];
|
||||
return sessionSubagents.find((agent) => agent.name === name) || null;
|
||||
}
|
||||
|
||||
return this.findSubagentByNameAtLevel(name, level);
|
||||
}
|
||||
|
||||
@@ -191,6 +205,15 @@ export class SubagentManager {
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent updating session-level agents
|
||||
if (existing.level === 'session') {
|
||||
throw new SubagentError(
|
||||
`Cannot update session-level subagent "${name}"`,
|
||||
SubagentErrorCode.INVALID_CONFIG,
|
||||
name,
|
||||
);
|
||||
}
|
||||
|
||||
// Merge updates with existing configuration
|
||||
const updatedConfig = this.mergeConfigurations(existing, updates);
|
||||
|
||||
@@ -236,8 +259,8 @@ export class SubagentManager {
|
||||
let deleted = false;
|
||||
|
||||
for (const currentLevel of levelsToCheck) {
|
||||
// Skip builtin level for deletion
|
||||
if (currentLevel === 'builtin') {
|
||||
// Skip builtin and session levels for deletion
|
||||
if (currentLevel === 'builtin' || currentLevel === 'session') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -277,6 +300,38 @@ export class SubagentManager {
|
||||
const subagents: SubagentConfig[] = [];
|
||||
const seenNames = new Set<string>();
|
||||
|
||||
// In SDK mode, only load session-level subagents
|
||||
if (this.config.getSdkMode()) {
|
||||
const sessionSubagents = this.config.getSessionSubagents();
|
||||
if (sessionSubagents && sessionSubagents.length > 0) {
|
||||
this.loadSessionSubagents(sessionSubagents);
|
||||
}
|
||||
|
||||
const levelsToCheck: SubagentLevel[] = options.level
|
||||
? [options.level]
|
||||
: ['session'];
|
||||
|
||||
for (const level of levelsToCheck) {
|
||||
const levelSubagents = this.subagentsCache?.get(level) || [];
|
||||
|
||||
for (const subagent of levelSubagents) {
|
||||
// Apply tool filter if specified
|
||||
if (
|
||||
options.hasTool &&
|
||||
(!subagent.tools || !subagent.tools.includes(options.hasTool))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
subagents.push(subagent);
|
||||
seenNames.add(subagent.name);
|
||||
}
|
||||
}
|
||||
|
||||
return subagents;
|
||||
}
|
||||
|
||||
// Normal mode: load from project, user, and builtin levels
|
||||
const levelsToCheck: SubagentLevel[] = options.level
|
||||
? [options.level]
|
||||
: ['project', 'user', 'builtin'];
|
||||
@@ -322,8 +377,8 @@ export class SubagentManager {
|
||||
comparison = a.name.localeCompare(b.name);
|
||||
break;
|
||||
case 'level': {
|
||||
// Project comes before user, user comes before builtin
|
||||
const levelOrder = { project: 0, user: 1, builtin: 2 };
|
||||
// Project comes before user, user comes before builtin, session comes last
|
||||
const levelOrder = { project: 0, user: 1, builtin: 2, session: 3 };
|
||||
comparison = levelOrder[a.level] - levelOrder[b.level];
|
||||
break;
|
||||
}
|
||||
@@ -339,6 +394,27 @@ export class SubagentManager {
|
||||
return subagents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads session-level subagents into the cache.
|
||||
* Session subagents are provided directly via config and are read-only.
|
||||
*
|
||||
* @param subagents - Array of session subagent configurations
|
||||
*/
|
||||
loadSessionSubagents(subagents: SubagentConfig[]): void {
|
||||
if (!this.subagentsCache) {
|
||||
this.subagentsCache = new Map();
|
||||
}
|
||||
|
||||
const sessionSubagents = subagents.map((config) => ({
|
||||
...config,
|
||||
level: 'session' as SubagentLevel,
|
||||
filePath: `<session:${config.name}>`,
|
||||
}));
|
||||
|
||||
this.subagentsCache.set('session', sessionSubagents);
|
||||
this.notifyChangeListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the subagents cache by loading all subagents from disk.
|
||||
* This method is called automatically when cache is null or when force=true.
|
||||
@@ -693,6 +769,10 @@ export class SubagentManager {
|
||||
return `<builtin:${name}>`;
|
||||
}
|
||||
|
||||
if (level === 'session') {
|
||||
return `<session:${name}>`;
|
||||
}
|
||||
|
||||
const baseDir =
|
||||
level === 'project'
|
||||
? path.join(
|
||||
|
||||
@@ -11,8 +11,9 @@ import type { Content, FunctionDeclaration } from '@google/genai';
|
||||
* - 'project': Stored in `.qwen/agents/` within the project directory
|
||||
* - 'user': Stored in `~/.qwen/agents/` in the user's home directory
|
||||
* - 'builtin': Built-in agents embedded in the codebase, always available
|
||||
* - 'session': Session-level agents provided at runtime, read-only
|
||||
*/
|
||||
export type SubagentLevel = 'project' | 'user' | 'builtin';
|
||||
export type SubagentLevel = 'project' | 'user' | 'builtin' | 'session';
|
||||
|
||||
/**
|
||||
* Core configuration for a subagent as stored in Markdown files.
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ToolInvocation,
|
||||
ToolMcpConfirmationDetails,
|
||||
ToolResult,
|
||||
ToolConfirmationPayload,
|
||||
} from './tools.js';
|
||||
import {
|
||||
BaseDeclarativeTool,
|
||||
@@ -98,7 +99,10 @@ class DiscoveredMCPToolInvocation extends BaseToolInvocation<
|
||||
serverName: this.serverName,
|
||||
toolName: this.serverToolName, // Display original tool name in confirmation
|
||||
toolDisplayName: this.displayName, // Display global registry name exposed to model and user
|
||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||
onConfirm: async (
|
||||
outcome: ToolConfirmationOutcome,
|
||||
_payload?: ToolConfirmationPayload,
|
||||
) => {
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlwaysServer) {
|
||||
DiscoveredMCPToolInvocation.allowlist.add(serverAllowListKey);
|
||||
} else if (outcome === ToolConfirmationOutcome.ProceedAlwaysTool) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
ToolResultDisplay,
|
||||
ToolCallConfirmationDetails,
|
||||
ToolExecuteConfirmationDetails,
|
||||
ToolConfirmationPayload,
|
||||
} from './tools.js';
|
||||
import {
|
||||
BaseDeclarativeTool,
|
||||
@@ -102,7 +103,10 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
||||
title: 'Confirm Shell Command',
|
||||
command: this.params.command,
|
||||
rootCommand: commandsToConfirm.join(', '),
|
||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||
onConfirm: async (
|
||||
outcome: ToolConfirmationOutcome,
|
||||
_payload?: ToolConfirmationPayload,
|
||||
) => {
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||
commandsToConfirm.forEach((command) => this.allowlist.add(command));
|
||||
}
|
||||
|
||||
@@ -531,13 +531,18 @@ export interface ToolEditConfirmationDetails {
|
||||
export interface ToolConfirmationPayload {
|
||||
// used to override `modifiedProposedContent` for modifiable tools in the
|
||||
// inline modify flow
|
||||
newContent: string;
|
||||
newContent?: string;
|
||||
// used to provide custom cancellation message when outcome is Cancel
|
||||
cancelMessage?: string;
|
||||
}
|
||||
|
||||
export interface ToolExecuteConfirmationDetails {
|
||||
type: 'exec';
|
||||
title: string;
|
||||
onConfirm: (outcome: ToolConfirmationOutcome) => Promise<void>;
|
||||
onConfirm: (
|
||||
outcome: ToolConfirmationOutcome,
|
||||
payload?: ToolConfirmationPayload,
|
||||
) => Promise<void>;
|
||||
command: string;
|
||||
rootCommand: string;
|
||||
}
|
||||
@@ -548,7 +553,10 @@ export interface ToolMcpConfirmationDetails {
|
||||
serverName: string;
|
||||
toolName: string;
|
||||
toolDisplayName: string;
|
||||
onConfirm: (outcome: ToolConfirmationOutcome) => Promise<void>;
|
||||
onConfirm: (
|
||||
outcome: ToolConfirmationOutcome,
|
||||
payload?: ToolConfirmationPayload,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ToolInfoConfirmationDetails {
|
||||
@@ -573,6 +581,11 @@ export interface ToolPlanConfirmationDetails {
|
||||
onConfirm: (outcome: ToolConfirmationOutcome) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* 1. support explicit denied outcome
|
||||
* 2. support proceed with modified input
|
||||
*/
|
||||
export enum ToolConfirmationOutcome {
|
||||
ProceedOnce = 'proceed_once',
|
||||
ProceedAlways = 'proceed_always',
|
||||
|
||||
Reference in New Issue
Block a user