mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-02 13:40:46 +00:00
Merge branch 'main' into fix/pr2371-btw-complete
This commit is contained in:
commit
905f2c3f36
108 changed files with 5064 additions and 965 deletions
|
|
@ -64,6 +64,7 @@ import type { CliArgs } from '../config/config.js';
|
|||
import { loadCliConfig } from '../config/config.js';
|
||||
import { Session } from './session/Session.js';
|
||||
import { formatAcpModelId } from '../utils/acpModelUtils.js';
|
||||
import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js';
|
||||
|
||||
const debugLogger = createDebugLogger('ACP_AGENT');
|
||||
|
||||
|
|
@ -87,7 +88,33 @@ export async function runAcpAgent(
|
|||
stream,
|
||||
);
|
||||
|
||||
// Handle SIGTERM/SIGINT for graceful shutdown.
|
||||
// Without this, signal handlers registered elsewhere in the CLI
|
||||
// (e.g., stdin raw mode restoration) override the default exit behavior,
|
||||
// causing the ACP process to ignore termination signals.
|
||||
let shuttingDown = false;
|
||||
const shutdownHandler = () => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
debugLogger.debug('[ACP] Shutdown signal received, closing streams');
|
||||
try {
|
||||
process.stdin.destroy();
|
||||
} catch {
|
||||
// stdin may already be closed
|
||||
}
|
||||
try {
|
||||
process.stdout.destroy();
|
||||
} catch {
|
||||
// stdout may already be closed
|
||||
}
|
||||
};
|
||||
process.on('SIGTERM', shutdownHandler);
|
||||
process.on('SIGINT', shutdownHandler);
|
||||
|
||||
await connection.closed;
|
||||
|
||||
process.off('SIGTERM', shutdownHandler);
|
||||
process.off('SIGINT', shutdownHandler);
|
||||
}
|
||||
|
||||
function toStdioServer(server: McpServer): McpServerStdio | undefined {
|
||||
|
|
@ -188,8 +215,14 @@ class QwenAgent implements Agent {
|
|||
}
|
||||
|
||||
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
|
||||
const sessionService = new SessionService(params.cwd);
|
||||
const exists = await sessionService.sessionExists(params.sessionId);
|
||||
const exists = await runWithAcpRuntimeOutputDir(
|
||||
this.settings,
|
||||
params.cwd,
|
||||
async () => {
|
||||
const sessionService = new SessionService(params.cwd);
|
||||
return sessionService.sessionExists(params.sessionId);
|
||||
},
|
||||
);
|
||||
if (!exists) {
|
||||
throw RequestError.invalidParams(
|
||||
undefined,
|
||||
|
|
@ -230,10 +263,12 @@ class QwenAgent implements Agent {
|
|||
params: ListSessionsRequest,
|
||||
): Promise<ListSessionsResponse> {
|
||||
const cwd = params.cwd || process.cwd();
|
||||
const sessionService = new SessionService(cwd);
|
||||
const numericCursor = params.cursor ? Number(params.cursor) : undefined;
|
||||
const result = await sessionService.listSessions({
|
||||
cursor: Number.isNaN(numericCursor) ? undefined : numericCursor,
|
||||
const result = await runWithAcpRuntimeOutputDir(this.settings, cwd, () => {
|
||||
const sessionService = new SessionService(cwd);
|
||||
return sessionService.listSessions({
|
||||
cursor: Number.isNaN(numericCursor) ? undefined : numericCursor,
|
||||
});
|
||||
});
|
||||
|
||||
const sessions: SessionInfo[] = result.items.map((item) => ({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { Storage } from '@qwen-code/qwen-code-core';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js';
|
||||
|
||||
describe('runWithAcpRuntimeOutputDir', () => {
|
||||
beforeEach(() => {
|
||||
Storage.setRuntimeBaseDir(null);
|
||||
delete process.env['QWEN_RUNTIME_DIR'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Storage.setRuntimeBaseDir(null);
|
||||
delete process.env['QWEN_RUNTIME_DIR'];
|
||||
});
|
||||
|
||||
it('uses the merged runtimeOutputDir relative to cwd within the async context', async () => {
|
||||
const cwd = path.resolve('workspace', 'project-a');
|
||||
const settings = {
|
||||
merged: {
|
||||
advanced: {
|
||||
runtimeOutputDir: '.qwen-runtime',
|
||||
},
|
||||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
await runWithAcpRuntimeOutputDir(settings, cwd, async () => {
|
||||
expect(Storage.getRuntimeBaseDir()).toBe(path.join(cwd, '.qwen-runtime'));
|
||||
});
|
||||
|
||||
expect(Storage.getRuntimeBaseDir()).toBe(Storage.getGlobalQwenDir());
|
||||
});
|
||||
});
|
||||
14
packages/cli/src/acp-integration/runtimeOutputDirContext.ts
Normal file
14
packages/cli/src/acp-integration/runtimeOutputDirContext.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Storage } from '@qwen-code/qwen-code-core';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
|
||||
export function runWithAcpRuntimeOutputDir<T>(
|
||||
settings: LoadedSettings,
|
||||
cwd: string,
|
||||
fn: () => T,
|
||||
): T {
|
||||
return Storage.runWithRuntimeBaseDir(
|
||||
settings.merged.advanced?.runtimeOutputDir,
|
||||
cwd,
|
||||
fn,
|
||||
);
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { ChatRecord, TaskResultDisplay } from '@qwen-code/qwen-code-core';
|
||||
import type { ChatRecord, AgentResultDisplay } from '@qwen-code/qwen-code-core';
|
||||
import type {
|
||||
Content,
|
||||
GenerateContentResponseUsageMetadata,
|
||||
|
|
@ -165,16 +165,16 @@ export class HistoryReplayer {
|
|||
(resultDisplay as { type?: unknown }).type === 'task_execution'
|
||||
) {
|
||||
await this.emitTaskUsageFromResultDisplay(
|
||||
resultDisplay as TaskResultDisplay,
|
||||
resultDisplay as AgentResultDisplay,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits token usage from a TaskResultDisplay execution summary, if present.
|
||||
* Emits token usage from a AgentResultDisplay execution summary, if present.
|
||||
*/
|
||||
private async emitTaskUsageFromResultDisplay(
|
||||
resultDisplay: TaskResultDisplay,
|
||||
resultDisplay: AgentResultDisplay,
|
||||
): Promise<void> {
|
||||
const summary = resultDisplay.executionSummary;
|
||||
if (!summary) {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ describe('Session', () => {
|
|||
switchModel: switchModelSpy,
|
||||
getModel: vi.fn().mockImplementation(() => currentModel),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getWorkingDir: vi.fn().mockReturnValue(process.cwd()),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue(undefined),
|
||||
|
|
@ -241,5 +242,38 @@ describe('Session', () => {
|
|||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('runs prompt inside runtime output dir context', async () => {
|
||||
const runtimeDir = path.resolve('runtime', 'from-settings');
|
||||
core.Storage.setRuntimeBaseDir(runtimeDir);
|
||||
session = new Session(
|
||||
'test-session-id',
|
||||
mockChat,
|
||||
mockConfig,
|
||||
mockClient,
|
||||
mockSettings,
|
||||
);
|
||||
const runWithRuntimeBaseDirSpy = vi.spyOn(
|
||||
core.Storage,
|
||||
'runWithRuntimeBaseDir',
|
||||
);
|
||||
|
||||
mockChat.sendMessageStream = vi
|
||||
.fn()
|
||||
.mockResolvedValue((async function* () {})());
|
||||
|
||||
const promptRequest: PromptRequest = {
|
||||
sessionId: 'test-session-id',
|
||||
prompt: [{ type: 'text', text: 'hello' }],
|
||||
};
|
||||
|
||||
await session.prompt(promptRequest);
|
||||
|
||||
expect(runWithRuntimeBaseDirSpy).toHaveBeenCalledWith(
|
||||
runtimeDir,
|
||||
process.cwd(),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,11 +29,12 @@ import {
|
|||
logToolCall,
|
||||
logUserPrompt,
|
||||
getErrorStatus,
|
||||
TaskTool,
|
||||
AgentTool,
|
||||
UserPromptEvent,
|
||||
TodoWriteTool,
|
||||
ExitPlanModeTool,
|
||||
readManyFiles,
|
||||
Storage,
|
||||
ToolNames,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
||||
|
|
@ -100,6 +101,7 @@ export class Session implements SessionContext {
|
|||
*/
|
||||
private pendingPromptCompletion: Promise<void> | null = null;
|
||||
private turn: number = 0;
|
||||
private readonly runtimeBaseDir: string;
|
||||
|
||||
// Modular components
|
||||
private readonly historyReplayer: HistoryReplayer;
|
||||
|
|
@ -118,6 +120,7 @@ export class Session implements SessionContext {
|
|||
private readonly settings: LoadedSettings,
|
||||
) {
|
||||
this.sessionId = id;
|
||||
this.runtimeBaseDir = Storage.getRuntimeBaseDir();
|
||||
|
||||
// Initialize modular components with this session as context
|
||||
this.toolCallEmitter = new ToolCallEmitter(this);
|
||||
|
|
@ -189,150 +192,170 @@ export class Session implements SessionContext {
|
|||
params: PromptRequest,
|
||||
pendingSend: AbortController,
|
||||
): Promise<PromptResponse> {
|
||||
// Increment turn counter for each user prompt
|
||||
this.turn += 1;
|
||||
return Storage.runWithRuntimeBaseDir(
|
||||
this.runtimeBaseDir,
|
||||
this.config.getWorkingDir(),
|
||||
async () => {
|
||||
// Increment turn counter for each user prompt
|
||||
this.turn += 1;
|
||||
|
||||
const chat = this.chat;
|
||||
const promptId = this.config.getSessionId() + '########' + this.turn;
|
||||
const chat = this.chat;
|
||||
const promptId = this.config.getSessionId() + '########' + this.turn;
|
||||
|
||||
// Extract text from all text blocks to construct the full prompt text for logging
|
||||
const promptText = params.prompt
|
||||
.filter((block) => block.type === 'text')
|
||||
.map((block) => (block.type === 'text' ? block.text : ''))
|
||||
.join(' ');
|
||||
// Extract text from all text blocks to construct the full prompt text for logging
|
||||
const promptText = params.prompt
|
||||
.filter((block) => block.type === 'text')
|
||||
.map((block) => (block.type === 'text' ? block.text : ''))
|
||||
.join(' ');
|
||||
|
||||
// Log user prompt
|
||||
logUserPrompt(
|
||||
this.config,
|
||||
new UserPromptEvent(
|
||||
promptText.length,
|
||||
promptId,
|
||||
this.config.getContentGeneratorConfig()?.authType,
|
||||
promptText,
|
||||
),
|
||||
);
|
||||
|
||||
// record user message for session management
|
||||
this.config.getChatRecordingService()?.recordUserMessage(promptText);
|
||||
|
||||
// Check if the input contains a slash command
|
||||
// Extract text from the first text block if present
|
||||
const firstTextBlock = params.prompt.find((block) => block.type === 'text');
|
||||
const inputText = firstTextBlock?.text || '';
|
||||
|
||||
let parts: Part[] | null;
|
||||
|
||||
if (isSlashCommand(inputText)) {
|
||||
// Handle slash command - uses default allowed commands (init, summary, compress)
|
||||
const slashCommandResult = await handleSlashCommand(
|
||||
inputText,
|
||||
pendingSend,
|
||||
this.config,
|
||||
this.settings,
|
||||
);
|
||||
|
||||
parts = await this.#processSlashCommandResult(
|
||||
slashCommandResult,
|
||||
params.prompt,
|
||||
);
|
||||
|
||||
// If parts is null, the command was fully handled (e.g., /summary completed)
|
||||
// Return early without sending to the model
|
||||
if (parts === null) {
|
||||
return { stopReason: 'end_turn' };
|
||||
}
|
||||
} else {
|
||||
// Normal processing for non-slash commands
|
||||
parts = await this.#resolvePrompt(params.prompt, pendingSend.signal);
|
||||
}
|
||||
|
||||
let nextMessage: Content | null = { role: 'user', parts };
|
||||
|
||||
while (nextMessage !== null) {
|
||||
if (pendingSend.signal.aborted) {
|
||||
chat.addHistory(nextMessage);
|
||||
return { stopReason: 'cancelled' };
|
||||
}
|
||||
|
||||
const functionCalls: FunctionCall[] = [];
|
||||
let usageMetadata: GenerateContentResponseUsageMetadata | null = null;
|
||||
const streamStartTime = Date.now();
|
||||
|
||||
try {
|
||||
const responseStream = await chat.sendMessageStream(
|
||||
this.config.getModel(),
|
||||
{
|
||||
message: nextMessage?.parts ?? [],
|
||||
config: {
|
||||
abortSignal: pendingSend.signal,
|
||||
},
|
||||
},
|
||||
promptId,
|
||||
// Log user prompt
|
||||
logUserPrompt(
|
||||
this.config,
|
||||
new UserPromptEvent(
|
||||
promptText.length,
|
||||
promptId,
|
||||
this.config.getContentGeneratorConfig()?.authType,
|
||||
promptText,
|
||||
),
|
||||
);
|
||||
nextMessage = null;
|
||||
|
||||
for await (const resp of responseStream) {
|
||||
// record user message for session management
|
||||
this.config.getChatRecordingService()?.recordUserMessage(promptText);
|
||||
|
||||
// Check if the input contains a slash command
|
||||
// Extract text from the first text block if present
|
||||
const firstTextBlock = params.prompt.find(
|
||||
(block) => block.type === 'text',
|
||||
);
|
||||
const inputText = firstTextBlock?.text || '';
|
||||
|
||||
let parts: Part[] | null;
|
||||
|
||||
if (isSlashCommand(inputText)) {
|
||||
// Handle slash command - uses default allowed commands (init, summary, compress)
|
||||
const slashCommandResult = await handleSlashCommand(
|
||||
inputText,
|
||||
pendingSend,
|
||||
this.config,
|
||||
this.settings,
|
||||
);
|
||||
|
||||
parts = await this.#processSlashCommandResult(
|
||||
slashCommandResult,
|
||||
params.prompt,
|
||||
);
|
||||
|
||||
// If parts is null, the command was fully handled (e.g., /summary completed)
|
||||
// Return early without sending to the model
|
||||
if (parts === null) {
|
||||
return { stopReason: 'end_turn' };
|
||||
}
|
||||
} else {
|
||||
// Normal processing for non-slash commands
|
||||
parts = await this.#resolvePrompt(params.prompt, pendingSend.signal);
|
||||
}
|
||||
|
||||
let nextMessage: Content | null = { role: 'user', parts };
|
||||
|
||||
while (nextMessage !== null) {
|
||||
if (pendingSend.signal.aborted) {
|
||||
chat.addHistory(nextMessage);
|
||||
return { stopReason: 'cancelled' };
|
||||
}
|
||||
|
||||
if (
|
||||
resp.type === StreamEventType.CHUNK &&
|
||||
resp.value.candidates &&
|
||||
resp.value.candidates.length > 0
|
||||
) {
|
||||
const candidate = resp.value.candidates[0];
|
||||
for (const part of candidate.content?.parts ?? []) {
|
||||
if (!part.text) {
|
||||
continue;
|
||||
const functionCalls: FunctionCall[] = [];
|
||||
let usageMetadata: GenerateContentResponseUsageMetadata | null = null;
|
||||
const streamStartTime = Date.now();
|
||||
|
||||
try {
|
||||
const responseStream = await chat.sendMessageStream(
|
||||
this.config.getModel(),
|
||||
{
|
||||
message: nextMessage?.parts ?? [],
|
||||
config: {
|
||||
abortSignal: pendingSend.signal,
|
||||
},
|
||||
},
|
||||
promptId,
|
||||
);
|
||||
nextMessage = null;
|
||||
|
||||
for await (const resp of responseStream) {
|
||||
if (pendingSend.signal.aborted) {
|
||||
return { stopReason: 'cancelled' };
|
||||
}
|
||||
|
||||
this.messageEmitter.emitMessage(
|
||||
part.text,
|
||||
'assistant',
|
||||
part.thought,
|
||||
if (
|
||||
resp.type === StreamEventType.CHUNK &&
|
||||
resp.value.candidates &&
|
||||
resp.value.candidates.length > 0
|
||||
) {
|
||||
const candidate = resp.value.candidates[0];
|
||||
for (const part of candidate.content?.parts ?? []) {
|
||||
if (!part.text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.messageEmitter.emitMessage(
|
||||
part.text,
|
||||
'assistant',
|
||||
part.thought,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
resp.type === StreamEventType.CHUNK &&
|
||||
resp.value.usageMetadata
|
||||
) {
|
||||
usageMetadata = resp.value.usageMetadata;
|
||||
}
|
||||
|
||||
if (
|
||||
resp.type === StreamEventType.CHUNK &&
|
||||
resp.value.functionCalls
|
||||
) {
|
||||
functionCalls.push(...resp.value.functionCalls);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (getErrorStatus(error) === 429) {
|
||||
throw new RequestError(
|
||||
429,
|
||||
'Rate limit exceeded. Try again later.',
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (resp.type === StreamEventType.CHUNK && resp.value.usageMetadata) {
|
||||
usageMetadata = resp.value.usageMetadata;
|
||||
if (usageMetadata) {
|
||||
const durationMs = Date.now() - streamStartTime;
|
||||
await this.messageEmitter.emitUsageMetadata(
|
||||
usageMetadata,
|
||||
'',
|
||||
durationMs,
|
||||
);
|
||||
}
|
||||
|
||||
if (resp.type === StreamEventType.CHUNK && resp.value.functionCalls) {
|
||||
functionCalls.push(...resp.value.functionCalls);
|
||||
if (functionCalls.length > 0) {
|
||||
const toolResponseParts: Part[] = [];
|
||||
|
||||
for (const fc of functionCalls) {
|
||||
const response = await this.runTool(
|
||||
pendingSend.signal,
|
||||
promptId,
|
||||
fc,
|
||||
);
|
||||
toolResponseParts.push(...response);
|
||||
}
|
||||
|
||||
nextMessage = { role: 'user', parts: toolResponseParts };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (getErrorStatus(error) === 429) {
|
||||
throw new RequestError(429, 'Rate limit exceeded. Try again later.');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (usageMetadata) {
|
||||
const durationMs = Date.now() - streamStartTime;
|
||||
await this.messageEmitter.emitUsageMetadata(
|
||||
usageMetadata,
|
||||
'',
|
||||
durationMs,
|
||||
);
|
||||
}
|
||||
|
||||
if (functionCalls.length > 0) {
|
||||
const toolResponseParts: Part[] = [];
|
||||
|
||||
for (const fc of functionCalls) {
|
||||
const response = await this.runTool(pendingSend.signal, promptId, fc);
|
||||
toolResponseParts.push(...response);
|
||||
}
|
||||
|
||||
nextMessage = { role: 'user', parts: toolResponseParts };
|
||||
}
|
||||
}
|
||||
|
||||
return { stopReason: 'end_turn' };
|
||||
return { stopReason: 'end_turn' };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async sendUpdate(update: SessionUpdate): Promise<void> {
|
||||
|
|
@ -518,7 +541,7 @@ export class Session implements SessionContext {
|
|||
|
||||
// Detect TodoWriteTool early - route to plan updates instead of tool_call events
|
||||
const isTodoWriteTool = tool.name === TodoWriteTool.Name;
|
||||
const isTaskTool = tool.name === TaskTool.Name;
|
||||
const isAgentTool = tool.name === AgentTool.Name;
|
||||
const isExitPlanModeTool = tool.name === ExitPlanModeTool.Name;
|
||||
|
||||
// Track cleanup functions for sub-agent event listeners
|
||||
|
|
@ -527,15 +550,15 @@ export class Session implements SessionContext {
|
|||
try {
|
||||
const invocation = tool.build(args);
|
||||
|
||||
if (isTaskTool && 'eventEmitter' in invocation) {
|
||||
// Access eventEmitter from TaskTool invocation
|
||||
if (isAgentTool && 'eventEmitter' in invocation) {
|
||||
// Access eventEmitter from AgentTool invocation
|
||||
const taskEventEmitter = (
|
||||
invocation as {
|
||||
eventEmitter: AgentEventEmitter;
|
||||
}
|
||||
).eventEmitter;
|
||||
|
||||
// Extract subagent metadata from TaskTool call
|
||||
// Extract subagent metadata from AgentTool call
|
||||
const parentToolCallId = callId;
|
||||
const subagentType = (args['subagent_type'] as string) ?? '';
|
||||
|
||||
|
|
@ -867,13 +890,12 @@ export class Session implements SessionContext {
|
|||
}
|
||||
|
||||
case 'no_command':
|
||||
// No command was found or executed, use original prompt
|
||||
return originalPrompt.map((block) => {
|
||||
if (block.type === 'text') {
|
||||
return { text: block.text };
|
||||
}
|
||||
throw new Error(`Unsupported block type: ${block.type}`);
|
||||
});
|
||||
// No command was found or executed, resolve the original prompt
|
||||
// through the standard path that handles all block types
|
||||
return this.#resolvePrompt(
|
||||
originalPrompt,
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
default: {
|
||||
// Exhaustiveness check
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ const basicPermissionOptions: readonly PermissionOptionConfig[] = [
|
|||
] as const;
|
||||
|
||||
/**
|
||||
* Tracks and emits events for sub-agent tool calls within TaskTool execution.
|
||||
* Tracks and emits events for sub-agent tool calls within AgentTool execution.
|
||||
*
|
||||
* Uses the unified ToolCallEmitter for consistency with normal flow
|
||||
* and history replay. Also handles permission requests for tools that
|
||||
|
|
@ -106,7 +106,7 @@ export class SubAgentTracker {
|
|||
/**
|
||||
* Sets up event listeners for a sub-agent's tool events.
|
||||
*
|
||||
* @param eventEmitter - The AgentEventEmitter from TaskTool
|
||||
* @param eventEmitter - The AgentEventEmitter from AgentTool
|
||||
* @param abortSignal - Signal to abort tracking if parent is cancelled
|
||||
* @returns Array of cleanup functions to remove listeners
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -225,7 +225,13 @@ export class ToolCallEmitter extends BaseEmitter {
|
|||
// Pass tool name to handle special cases like exit_plan_mode -> switch_mode
|
||||
kind = this.mapToolKind(tool.kind, toolName);
|
||||
} catch {
|
||||
// Use defaults on build failure
|
||||
// Fallback: use the description arg directly if available
|
||||
if (typeof args['description'] === 'string') {
|
||||
title = `${title}: ${args['description']}`;
|
||||
}
|
||||
if (tool.kind) {
|
||||
kind = this.mapToolKind(tool.kind, toolName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ export interface SessionContext extends SessionUpdateSender {
|
|||
* Subagent metadata for tracking parent tool call context.
|
||||
*/
|
||||
export interface SubagentMeta {
|
||||
/** ID of the parent TaskTool call that created this subagent */
|
||||
/** ID of the parent AgentTool call that created this subagent */
|
||||
parentToolCallId?: string;
|
||||
/** Type of subagent (from TaskParams.subagent_type) */
|
||||
/** Type of subagent (from AgentParams.subagent_type) */
|
||||
subagentType?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
DEFAULT_QWEN_MODEL,
|
||||
OutputFormat,
|
||||
NativeLspService,
|
||||
Storage,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
||||
import type { Settings } from './settings.js';
|
||||
|
|
@ -2439,3 +2440,79 @@ describe('Telemetry configuration via environment variables', () => {
|
|||
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig runtimeOutputDir', () => {
|
||||
const originalArgv = process.argv;
|
||||
const originalRuntimeEnv = process.env['QWEN_RUNTIME_DIR'];
|
||||
|
||||
beforeEach(() => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
Storage.setRuntimeBaseDir(null);
|
||||
delete process.env['QWEN_RUNTIME_DIR'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv;
|
||||
Storage.setRuntimeBaseDir(null);
|
||||
if (originalRuntimeEnv !== undefined) {
|
||||
process.env['QWEN_RUNTIME_DIR'] = originalRuntimeEnv;
|
||||
} else {
|
||||
delete process.env['QWEN_RUNTIME_DIR'];
|
||||
}
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should set runtime base dir from settings with absolute path', async () => {
|
||||
const runtimeDir = path.resolve('custom', 'runtime');
|
||||
const argv = await parseArguments();
|
||||
const settings: Settings = {
|
||||
advanced: { runtimeOutputDir: runtimeDir },
|
||||
};
|
||||
await loadCliConfig(settings, argv);
|
||||
expect(Storage.getRuntimeBaseDir()).toBe(runtimeDir);
|
||||
});
|
||||
|
||||
it('should resolve relative runtimeOutputDir against cwd', async () => {
|
||||
const argv = await parseArguments();
|
||||
const settings: Settings = {
|
||||
advanced: { runtimeOutputDir: '.qwen' },
|
||||
};
|
||||
const cwd = path.resolve('workspace', 'my-project');
|
||||
await loadCliConfig(settings, argv, cwd);
|
||||
expect(Storage.getRuntimeBaseDir()).toBe(path.join(cwd, '.qwen'));
|
||||
});
|
||||
|
||||
it('should not set runtime base dir when runtimeOutputDir is absent', async () => {
|
||||
const argv = await parseArguments();
|
||||
const settings: Settings = {};
|
||||
await loadCliConfig(settings, argv);
|
||||
expect(Storage.getRuntimeBaseDir()).toBe(Storage.getGlobalQwenDir());
|
||||
});
|
||||
|
||||
it('should let QWEN_RUNTIME_DIR env var take priority over settings', async () => {
|
||||
const envDir = path.resolve('from-env');
|
||||
const settingsDir = path.resolve('from-settings');
|
||||
process.env['QWEN_RUNTIME_DIR'] = envDir;
|
||||
const argv = await parseArguments();
|
||||
const settings: Settings = {
|
||||
advanced: { runtimeOutputDir: settingsDir },
|
||||
};
|
||||
await loadCliConfig(settings, argv);
|
||||
// getRuntimeBaseDir checks env var first at call time
|
||||
expect(Storage.getRuntimeBaseDir()).toBe(envDir);
|
||||
});
|
||||
|
||||
it('should reset runtime base dir on subsequent load when runtimeOutputDir is absent', async () => {
|
||||
const argv = await parseArguments();
|
||||
const firstRuntimeDir = path.resolve('first', 'runtime');
|
||||
await loadCliConfig(
|
||||
{ advanced: { runtimeOutputDir: firstRuntimeDir } },
|
||||
argv,
|
||||
);
|
||||
expect(Storage.getRuntimeBaseDir()).toBe(firstRuntimeDir);
|
||||
|
||||
await loadCliConfig({}, argv);
|
||||
expect(Storage.getRuntimeBaseDir()).toBe(Storage.getGlobalQwenDir());
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -708,6 +708,11 @@ export async function loadCliConfig(
|
|||
): Promise<Config> {
|
||||
const debugMode = isDebugMode(argv);
|
||||
|
||||
// Set runtime output directory from settings (env var QWEN_RUNTIME_DIR
|
||||
// is auto-detected inside getRuntimeBaseDir() at each call site).
|
||||
// Pass cwd so that relative paths like ".qwen" resolve per-project.
|
||||
Storage.setRuntimeBaseDir(settings.advanced?.runtimeOutputDir, cwd);
|
||||
|
||||
const ideMode = settings.ide?.enabled ?? false;
|
||||
|
||||
const folderTrust = settings.security?.folderTrust?.enabled ?? false;
|
||||
|
|
|
|||
|
|
@ -1263,6 +1263,17 @@ const SETTINGS_SCHEMA = {
|
|||
description: 'Configuration for the bug report command.',
|
||||
showInDialog: false,
|
||||
},
|
||||
runtimeOutputDir: {
|
||||
type: 'string',
|
||||
label: 'Runtime Output Directory',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description:
|
||||
'Custom directory for runtime output (temp files, debug logs, session data, todos, etc.). ' +
|
||||
'Config files remain at ~/.qwen. Env var QWEN_RUNTIME_DIR takes priority.',
|
||||
showInDialog: false,
|
||||
},
|
||||
tavilyApiKey: {
|
||||
type: 'string',
|
||||
label: 'Tavily API Key (Deprecated)',
|
||||
|
|
|
|||
|
|
@ -326,8 +326,15 @@ export async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle --resume without a session ID by showing the session picker
|
||||
// Handle --resume without a session ID by showing the session picker.
|
||||
// Set the runtime output dir early so the picker can find sessions stored
|
||||
// under a custom runtimeOutputDir (setRuntimeBaseDir is idempotent and will
|
||||
// be called again inside loadCliConfig).
|
||||
if (argv.resume === '') {
|
||||
Storage.setRuntimeBaseDir(
|
||||
settings.merged.advanced?.runtimeOutputDir,
|
||||
process.cwd(),
|
||||
);
|
||||
const selectedSessionId = await showResumeSessionPicker();
|
||||
if (!selectedSessionId) {
|
||||
// User cancelled or no sessions available
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
type Config,
|
||||
type ServerGeminiStreamEvent,
|
||||
type ToolCallRequestInfo,
|
||||
type TaskResultDisplay,
|
||||
type AgentResultDisplay,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import type { Part, GenerateContentResponseUsageMetadata } from '@google/genai';
|
||||
import type {
|
||||
|
|
@ -144,7 +144,7 @@ class TestJsonOutputAdapter extends BaseJsonOutputAdapter {
|
|||
|
||||
exposeCreateSubagentToolUseBlock(
|
||||
state: MessageState,
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
parentToolUseId: string,
|
||||
) {
|
||||
return this.createSubagentToolUseBlock(state, toolCall, parentToolUseId);
|
||||
|
|
@ -1314,7 +1314,7 @@ describe('BaseJsonOutputAdapter', () => {
|
|||
it('should process subagent tool call', () => {
|
||||
const parentToolUseId = 'parent-tool-1';
|
||||
adapter.startSubagentAssistantMessage(parentToolUseId);
|
||||
const toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number] = {
|
||||
const toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number] = {
|
||||
callId: 'tool-1',
|
||||
name: 'test_tool',
|
||||
args: { param: 'value' },
|
||||
|
|
@ -1346,7 +1346,7 @@ describe('BaseJsonOutputAdapter', () => {
|
|||
const state = adapter.exposeGetMessageState(parentToolUseId);
|
||||
adapter.exposeAppendText(state, 'Text', parentToolUseId);
|
||||
|
||||
const toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number] = {
|
||||
const toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number] = {
|
||||
callId: 'tool-1',
|
||||
name: 'test_tool',
|
||||
args: {},
|
||||
|
|
@ -1367,7 +1367,7 @@ describe('BaseJsonOutputAdapter', () => {
|
|||
it('should create tool_use block for subagent', () => {
|
||||
const state = adapter.exposeCreateMessageState();
|
||||
adapter.startAssistantMessage();
|
||||
const toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number] = {
|
||||
const toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number] = {
|
||||
callId: 'tool-1',
|
||||
name: 'test_tool',
|
||||
args: { param: 'value' },
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
ToolCallResponseInfo,
|
||||
SessionMetrics,
|
||||
ServerGeminiStreamEvent,
|
||||
TaskResultDisplay,
|
||||
AgentResultDisplay,
|
||||
McpToolProgressData,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
|
|
@ -110,7 +110,7 @@ export interface JsonOutputAdapterInterface extends MessageEmitter {
|
|||
|
||||
startSubagentAssistantMessage?(parentToolUseId: string): void;
|
||||
processSubagentToolCall?(
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
parentToolUseId: string,
|
||||
): void;
|
||||
finalizeSubagentAssistantMessage?(
|
||||
|
|
@ -693,7 +693,7 @@ export abstract class BaseJsonOutputAdapter {
|
|||
* @param parentToolUseId - Parent tool use ID
|
||||
*/
|
||||
processSubagentToolCall(
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
parentToolUseId: string,
|
||||
): void {
|
||||
const state = this.getMessageState(parentToolUseId);
|
||||
|
|
@ -744,7 +744,7 @@ export abstract class BaseJsonOutputAdapter {
|
|||
protected processSubagentToolUseBlock(
|
||||
state: MessageState,
|
||||
index: number,
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
parentToolUseId: string,
|
||||
): void {
|
||||
// Emit tool_use block creation event (with empty input)
|
||||
|
|
@ -937,7 +937,7 @@ export abstract class BaseJsonOutputAdapter {
|
|||
*/
|
||||
protected createSubagentToolUseBlock(
|
||||
state: MessageState,
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
_parentToolUseId: string,
|
||||
): { block: ToolUseBlock; index: number } {
|
||||
const index = state.blocks.length;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import {
|
|||
extractPartsFromUserMessage,
|
||||
buildSystemMessage,
|
||||
createToolProgressHandler,
|
||||
createTaskToolProgressHandler,
|
||||
createAgentToolProgressHandler,
|
||||
computeUsageFromMetrics,
|
||||
} from './utils/nonInteractiveHelpers.js';
|
||||
|
||||
|
|
@ -320,12 +320,12 @@ export async function runNonInteractive(
|
|||
: undefined;
|
||||
|
||||
// Build outputUpdateHandler for this tool call.
|
||||
// Task tool has its own complex handler (subagent messages).
|
||||
// Agent tool has its own complex handler (subagent messages).
|
||||
// All other tools with canUpdateOutput=true (e.g., MCP tools)
|
||||
// get a generic handler that emits progress via the adapter.
|
||||
const isTaskTool = finalRequestInfo.name === 'task';
|
||||
const { handler: outputUpdateHandler } = isTaskTool
|
||||
? createTaskToolProgressHandler(
|
||||
const isAgentTool = finalRequestInfo.name === 'agent';
|
||||
const { handler: outputUpdateHandler } = isAgentTool
|
||||
? createAgentToolProgressHandler(
|
||||
config,
|
||||
finalRequestInfo.callId,
|
||||
adapter,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Code
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { Storage, type Config } from '@qwen-code/qwen-code-core';
|
||||
import { StaticInsightGenerator } from './StaticInsightGenerator.js';
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
default: {
|
||||
mkdir: vi.fn(),
|
||||
access: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
symlink: vi.fn(),
|
||||
copyFile: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('StaticInsightGenerator', () => {
|
||||
const mockedFs = vi.mocked(fs);
|
||||
const mockConfig = {} as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-03-05T12:34:56.000Z'));
|
||||
Storage.setRuntimeBaseDir(path.resolve('runtime-output'));
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockedFs.mkdir.mockResolvedValue(undefined);
|
||||
mockedFs.access.mockRejectedValue(new Error('not found'));
|
||||
mockedFs.writeFile.mockResolvedValue(undefined);
|
||||
mockedFs.unlink.mockRejectedValue(new Error('not found'));
|
||||
mockedFs.symlink.mockResolvedValue(undefined);
|
||||
mockedFs.copyFile.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Storage.setRuntimeBaseDir(null);
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('writes insights under runtime output directory', async () => {
|
||||
const generator = new StaticInsightGenerator(mockConfig);
|
||||
const generateInsights = vi.fn().mockResolvedValue({});
|
||||
const renderInsightHTML = vi.fn().mockResolvedValue('<html>ok</html>');
|
||||
|
||||
(
|
||||
generator as unknown as {
|
||||
dataProcessor: { generateInsights: typeof generateInsights };
|
||||
}
|
||||
).dataProcessor = { generateInsights };
|
||||
(
|
||||
generator as unknown as {
|
||||
templateRenderer: { renderInsightHTML: typeof renderInsightHTML };
|
||||
}
|
||||
).templateRenderer = { renderInsightHTML };
|
||||
|
||||
const projectsDir = path.resolve(
|
||||
'workspace',
|
||||
'project-a',
|
||||
'.qwen',
|
||||
'projects',
|
||||
);
|
||||
const outputDir = path.join(Storage.getRuntimeBaseDir(), 'insights');
|
||||
const facetsDir = path.join(outputDir, 'facets');
|
||||
const expectedOutputPath = path.join(outputDir, 'insight-2026-03-05.html');
|
||||
|
||||
const outputPath = await generator.generateStaticInsight(projectsDir);
|
||||
|
||||
expect(mockedFs.mkdir).toHaveBeenCalledWith(outputDir, { recursive: true });
|
||||
expect(mockedFs.mkdir).toHaveBeenCalledWith(facetsDir, { recursive: true });
|
||||
expect(generateInsights).toHaveBeenCalledWith(
|
||||
projectsDir,
|
||||
facetsDir,
|
||||
undefined,
|
||||
);
|
||||
expect(renderInsightHTML).toHaveBeenCalledWith({});
|
||||
expect(mockedFs.writeFile).toHaveBeenCalledWith(
|
||||
expectedOutputPath,
|
||||
'<html>ok</html>',
|
||||
'utf-8',
|
||||
);
|
||||
expect(outputPath).toBe(expectedOutputPath);
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { DataProcessor } from './DataProcessor.js';
|
||||
import { TemplateRenderer } from './TemplateRenderer.js';
|
||||
import type {
|
||||
|
|
@ -14,7 +13,7 @@ import type {
|
|||
InsightProgressCallback,
|
||||
} from '../types/StaticInsightTypes.js';
|
||||
|
||||
import { updateSymlink, type Config } from '@qwen-code/qwen-code-core';
|
||||
import { updateSymlink, Storage, type Config } from '@qwen-code/qwen-code-core';
|
||||
|
||||
export class StaticInsightGenerator {
|
||||
private dataProcessor: DataProcessor;
|
||||
|
|
@ -27,7 +26,7 @@ export class StaticInsightGenerator {
|
|||
|
||||
// Ensure the output directory exists
|
||||
private async ensureOutputDirectory(): Promise<string> {
|
||||
const outputDir = path.join(os.homedir(), '.qwen', 'insights');
|
||||
const outputDir = path.join(Storage.getRuntimeBaseDir(), 'insights');
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
return outputDir;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
getShellConfiguration,
|
||||
ShellExecutionService,
|
||||
flatMapTextParts,
|
||||
checkArgumentSafety,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
||||
import type { CommandContext } from '../../ui/commands/types.js';
|
||||
|
|
@ -99,6 +100,16 @@ export class ShellProcessor implements IPromptProcessor {
|
|||
const { shell } = getShellConfiguration();
|
||||
const userArgsEscaped = escapeShellArg(userArgsRaw, shell);
|
||||
|
||||
// Check safety of the value that will be used for $ARGUMENTS (after removing outer quotes)
|
||||
let userArgsForArgumentsPlaceholder = userArgsRaw.replace(
|
||||
/^'([\s\S]*?)'$/,
|
||||
'$1',
|
||||
);
|
||||
const argumentSafety = checkArgumentSafety(userArgsForArgumentsPlaceholder);
|
||||
if (!argumentSafety.isSafe) {
|
||||
userArgsForArgumentsPlaceholder = userArgsEscaped;
|
||||
}
|
||||
|
||||
const resolvedInjections: ResolvedShellInjection[] = injections.map(
|
||||
(injection) => {
|
||||
const command = injection.content;
|
||||
|
|
@ -109,7 +120,7 @@ export class ShellProcessor implements IPromptProcessor {
|
|||
|
||||
const resolvedCommand = command
|
||||
.replaceAll(SHORTHAND_ARGS_PLACEHOLDER, userArgsEscaped) // Replace {{args}}
|
||||
.replaceAll('$ARGUMENTS', userArgsEscaped); // Replace $ARGUMENTS
|
||||
.replaceAll('$ARGUMENTS', userArgsForArgumentsPlaceholder);
|
||||
return { ...injection, resolvedCommand };
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ describe('AppContainer State Management', () => {
|
|||
// Mock config's getTargetDir to return consistent workspace directory
|
||||
vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
|
||||
// Mock GeminiClient to prevent unhandled errors from TaskTool.refreshSubagents
|
||||
// Mock GeminiClient to prevent unhandled errors from AgentTool.refreshSubagents
|
||||
const mockGeminiClient: Partial<GeminiClient> = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
setTools: vi.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -278,7 +278,7 @@ describe('AppContainer State Management', () => {
|
|||
mockGeminiClient as GeminiClient,
|
||||
);
|
||||
|
||||
// Mock SubagentManager to prevent errors during TaskTool initialization
|
||||
// Mock SubagentManager to prevent errors during AgentTool initialization
|
||||
const mockSubagentManager: Partial<SubagentManager> = {
|
||||
listSubagents: vi.fn().mockResolvedValue([]),
|
||||
addChangeListener: vi.fn(),
|
||||
|
|
|
|||
66
packages/cli/src/ui/commands/insightCommand.test.ts
Normal file
66
packages/cli/src/ui/commands/insightCommand.test.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Code
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import path from 'path';
|
||||
import open from 'open';
|
||||
import { Storage } from '@qwen-code/qwen-code-core';
|
||||
import { insightCommand } from './insightCommand.js';
|
||||
import type { CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
|
||||
const mockGenerateStaticInsight = vi.fn();
|
||||
|
||||
vi.mock('../../services/insight/generators/StaticInsightGenerator.js', () => ({
|
||||
StaticInsightGenerator: vi.fn(() => ({
|
||||
generateStaticInsight: mockGenerateStaticInsight,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('open', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('insightCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
|
||||
beforeEach(() => {
|
||||
Storage.setRuntimeBaseDir(path.resolve('runtime-output'));
|
||||
mockGenerateStaticInsight.mockResolvedValue(
|
||||
path.resolve('runtime-output', 'insights', 'insight-2026-03-05.html'),
|
||||
);
|
||||
vi.mocked(open).mockResolvedValue(undefined as never);
|
||||
|
||||
mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {} as CommandContext['services']['config'],
|
||||
},
|
||||
ui: {
|
||||
addItem: vi.fn(),
|
||||
setPendingItem: vi.fn(),
|
||||
setDebugMessage: vi.fn(),
|
||||
},
|
||||
} as unknown as CommandContext);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Storage.setRuntimeBaseDir(null);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('uses runtime base dir to locate projects directory', async () => {
|
||||
if (!insightCommand.action) {
|
||||
throw new Error('insight command must have action');
|
||||
}
|
||||
|
||||
await insightCommand.action(mockContext, '');
|
||||
|
||||
expect(mockGenerateStaticInsight).toHaveBeenCalledWith(
|
||||
path.join(Storage.getRuntimeBaseDir(), 'projects'),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -10,9 +10,8 @@ import { MessageType } from '../types.js';
|
|||
import type { HistoryItemInsightProgress } from '../types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { join } from 'path';
|
||||
import os from 'os';
|
||||
import { StaticInsightGenerator } from '../../services/insight/generators/StaticInsightGenerator.js';
|
||||
import { createDebugLogger } from '@qwen-code/qwen-code-core';
|
||||
import { createDebugLogger, Storage } from '@qwen-code/qwen-code-core';
|
||||
import open from 'open';
|
||||
|
||||
const logger = createDebugLogger('DataProcessor');
|
||||
|
|
@ -29,7 +28,7 @@ export const insightCommand: SlashCommand = {
|
|||
try {
|
||||
context.ui.setDebugMessage(t('Generating insights...'));
|
||||
|
||||
const projectsDir = join(os.homedir(), '.qwen', 'projects');
|
||||
const projectsDir = join(Storage.getRuntimeBaseDir(), 'projects');
|
||||
if (!context.services.config) {
|
||||
throw new Error('Config service is not available');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
|||
import { TodoDisplay } from '../TodoDisplay.js';
|
||||
import type {
|
||||
TodoResultDisplay,
|
||||
TaskResultDisplay,
|
||||
AgentResultDisplay,
|
||||
PlanResultDisplay,
|
||||
AnsiOutput,
|
||||
Config,
|
||||
|
|
@ -50,7 +50,7 @@ type DisplayRendererResult =
|
|||
| { type: 'plan'; data: PlanResultDisplay }
|
||||
| { type: 'string'; data: string }
|
||||
| { type: 'diff'; data: { fileDiff: string; fileName: string } }
|
||||
| { type: 'task'; data: TaskResultDisplay }
|
||||
| { type: 'task'; data: AgentResultDisplay }
|
||||
| { type: 'ansi'; data: AnsiOutput };
|
||||
|
||||
/**
|
||||
|
|
@ -98,7 +98,7 @@ const useResultDisplayRenderer = (
|
|||
) {
|
||||
return {
|
||||
type: 'task',
|
||||
data: resultDisplay as TaskResultDisplay,
|
||||
data: resultDisplay as AgentResultDisplay,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ const PlanResultRenderer: React.FC<{
|
|||
* Component to render subagent execution results
|
||||
*/
|
||||
const SubagentExecutionRenderer: React.FC<{
|
||||
data: TaskResultDisplay;
|
||||
data: AgentResultDisplay;
|
||||
availableHeight?: number;
|
||||
childWidth: number;
|
||||
config: Config;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type {
|
||||
TaskResultDisplay,
|
||||
AgentResultDisplay,
|
||||
AgentStatsSummary,
|
||||
Config,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
|
@ -20,7 +20,7 @@ import { ToolConfirmationMessage } from '../../messages/ToolConfirmationMessage.
|
|||
export type DisplayMode = 'compact' | 'default' | 'verbose';
|
||||
|
||||
export interface AgentExecutionDisplayProps {
|
||||
data: TaskResultDisplay;
|
||||
data: AgentResultDisplay;
|
||||
availableHeight?: number;
|
||||
childWidth: number;
|
||||
config: Config;
|
||||
|
|
@ -28,7 +28,7 @@ export interface AgentExecutionDisplayProps {
|
|||
|
||||
const getStatusColor = (
|
||||
status:
|
||||
| TaskResultDisplay['status']
|
||||
| AgentResultDisplay['status']
|
||||
| 'executing'
|
||||
| 'success'
|
||||
| 'awaiting_approval',
|
||||
|
|
@ -50,7 +50,7 @@ const getStatusColor = (
|
|||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: TaskResultDisplay['status']) => {
|
||||
const getStatusText = (status: AgentResultDisplay['status']) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'Running';
|
||||
|
|
@ -301,7 +301,7 @@ const TaskPromptSection: React.FC<{
|
|||
* Status dot component with similar height as text
|
||||
*/
|
||||
const StatusDot: React.FC<{
|
||||
status: TaskResultDisplay['status'];
|
||||
status: AgentResultDisplay['status'];
|
||||
}> = ({ status }) => (
|
||||
<Box marginLeft={1} marginRight={1}>
|
||||
<Text color={getStatusColor(status)}>●</Text>
|
||||
|
|
@ -312,7 +312,7 @@ const StatusDot: React.FC<{
|
|||
* Status indicator component
|
||||
*/
|
||||
const StatusIndicator: React.FC<{
|
||||
status: TaskResultDisplay['status'];
|
||||
status: AgentResultDisplay['status'];
|
||||
}> = ({ status }) => {
|
||||
const color = getStatusColor(status);
|
||||
const text = getStatusText(status);
|
||||
|
|
@ -323,7 +323,7 @@ const StatusIndicator: React.FC<{
|
|||
* Tool calls list - format consistent with ToolInfo in ToolMessage.tsx
|
||||
*/
|
||||
const ToolCallsList: React.FC<{
|
||||
toolCalls: TaskResultDisplay['toolCalls'];
|
||||
toolCalls: AgentResultDisplay['toolCalls'];
|
||||
displayMode: DisplayMode;
|
||||
}> = ({ toolCalls, displayMode }) => {
|
||||
const calls = toolCalls || [];
|
||||
|
|
@ -435,7 +435,7 @@ const ToolCallItem: React.FC<{
|
|||
* Execution summary details component
|
||||
*/
|
||||
const ExecutionSummaryDetails: React.FC<{
|
||||
data: TaskResultDisplay;
|
||||
data: AgentResultDisplay;
|
||||
displayMode: DisplayMode;
|
||||
}> = ({ data, displayMode: _displayMode }) => {
|
||||
const stats = data.executionSummary;
|
||||
|
|
@ -505,7 +505,7 @@ const ToolUsageStats: React.FC<{
|
|||
* Results section for completed executions - matches the clean layout from the image
|
||||
*/
|
||||
const ResultsSection: React.FC<{
|
||||
data: TaskResultDisplay;
|
||||
data: AgentResultDisplay;
|
||||
displayMode: DisplayMode;
|
||||
}> = ({ data, displayMode }) => (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
|
|
|
|||
|
|
@ -181,10 +181,7 @@ function calculateFileStats(records: ChatRecord[]): FileOperationStats {
|
|||
let filePath: string;
|
||||
if (typeof display.fileName === 'string') {
|
||||
// Prefer args.file_path for full path, fallback to fileName (which may be basename)
|
||||
filePath =
|
||||
(args?.['file_path'] as string) ||
|
||||
(args?.['absolute_path'] as string) ||
|
||||
display.fileName;
|
||||
filePath = (args?.['file_path'] as string) || display.fileName;
|
||||
} else {
|
||||
// Fallback if fileName is not a string
|
||||
filePath = 'unknown';
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ function formatToolDescription(
|
|||
const invocation = tool.build(args);
|
||||
return invocation.getDescription();
|
||||
} catch {
|
||||
// Fallback: use the description arg directly if available
|
||||
if (typeof args['description'] === 'string') {
|
||||
return args['description'];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|||
import type {
|
||||
Config,
|
||||
SessionMetrics,
|
||||
TaskResultDisplay,
|
||||
AgentResultDisplay,
|
||||
ToolCallResponseInfo,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
|
|
@ -30,7 +30,7 @@ import {
|
|||
computeUsageFromMetrics,
|
||||
buildSystemMessage,
|
||||
createToolProgressHandler,
|
||||
createTaskToolProgressHandler,
|
||||
createAgentToolProgressHandler,
|
||||
functionResponsePartsToString,
|
||||
toolResultContent,
|
||||
} from './nonInteractiveHelpers.js';
|
||||
|
|
@ -731,7 +731,7 @@ describe('createToolProgressHandler', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('createTaskToolProgressHandler', () => {
|
||||
describe('createAgentToolProgressHandler', () => {
|
||||
let mockAdapter: JsonOutputAdapterInterface;
|
||||
let mockConfig: Config;
|
||||
|
||||
|
|
@ -751,13 +751,13 @@ describe('createTaskToolProgressHandler', () => {
|
|||
});
|
||||
|
||||
it('should create handler that processes task tool calls', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
);
|
||||
|
||||
const taskDisplay: TaskResultDisplay = {
|
||||
const taskDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -786,13 +786,13 @@ describe('createTaskToolProgressHandler', () => {
|
|||
});
|
||||
|
||||
it('should emit tool_result when tool call completes', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
);
|
||||
|
||||
const taskDisplay: TaskResultDisplay = {
|
||||
const taskDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -825,13 +825,13 @@ describe('createTaskToolProgressHandler', () => {
|
|||
});
|
||||
|
||||
it('should not duplicate tool_use emissions', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
);
|
||||
|
||||
const taskDisplay: TaskResultDisplay = {
|
||||
const taskDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -855,13 +855,13 @@ describe('createTaskToolProgressHandler', () => {
|
|||
});
|
||||
|
||||
it('should not duplicate tool_result emissions', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
);
|
||||
|
||||
const taskDisplay: TaskResultDisplay = {
|
||||
const taskDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -886,14 +886,14 @@ describe('createTaskToolProgressHandler', () => {
|
|||
});
|
||||
|
||||
it('should handle status transitions from executing to completed', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
);
|
||||
|
||||
// First: executing state
|
||||
const executingDisplay: TaskResultDisplay = {
|
||||
const executingDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -910,7 +910,7 @@ describe('createTaskToolProgressHandler', () => {
|
|||
};
|
||||
|
||||
// Second: completed state
|
||||
const completedDisplay: TaskResultDisplay = {
|
||||
const completedDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -935,13 +935,13 @@ describe('createTaskToolProgressHandler', () => {
|
|||
});
|
||||
|
||||
it('should emit error result for failed task status', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
);
|
||||
|
||||
const runningDisplay: TaskResultDisplay = {
|
||||
const runningDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -950,7 +950,7 @@ describe('createTaskToolProgressHandler', () => {
|
|||
toolCalls: [],
|
||||
};
|
||||
|
||||
const failedDisplay: TaskResultDisplay = {
|
||||
const failedDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -971,13 +971,13 @@ describe('createTaskToolProgressHandler', () => {
|
|||
});
|
||||
|
||||
it('should emit error result for cancelled task status', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
);
|
||||
|
||||
const runningDisplay: TaskResultDisplay = {
|
||||
const runningDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -986,7 +986,7 @@ describe('createTaskToolProgressHandler', () => {
|
|||
toolCalls: [],
|
||||
};
|
||||
|
||||
const cancelledDisplay: TaskResultDisplay = {
|
||||
const cancelledDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -1006,7 +1006,7 @@ describe('createTaskToolProgressHandler', () => {
|
|||
});
|
||||
|
||||
it('should not process non-task-execution displays', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
|
|
@ -1017,20 +1017,20 @@ describe('createTaskToolProgressHandler', () => {
|
|||
content: 'some content',
|
||||
};
|
||||
|
||||
handler('call-id', nonTaskDisplay as unknown as TaskResultDisplay);
|
||||
handler('call-id', nonTaskDisplay as unknown as AgentResultDisplay);
|
||||
|
||||
expect(mockAdapter.processSubagentToolCall).not.toHaveBeenCalled();
|
||||
expect(mockAdapter.emitToolResult).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle tool calls with failed status', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
);
|
||||
|
||||
const taskDisplay: TaskResultDisplay = {
|
||||
const taskDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -1061,13 +1061,13 @@ describe('createTaskToolProgressHandler', () => {
|
|||
});
|
||||
|
||||
it('should handle tool calls without result content', () => {
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
mockAdapter,
|
||||
);
|
||||
|
||||
const taskDisplay: TaskResultDisplay = {
|
||||
const taskDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
@ -1096,13 +1096,13 @@ describe('createTaskToolProgressHandler', () => {
|
|||
emitToolResult: vi.fn(),
|
||||
} as unknown as JsonOutputAdapterInterface;
|
||||
|
||||
const { handler } = createTaskToolProgressHandler(
|
||||
const { handler } = createAgentToolProgressHandler(
|
||||
mockConfig,
|
||||
'parent-tool-id',
|
||||
limitedAdapter,
|
||||
);
|
||||
|
||||
const taskDisplay: TaskResultDisplay = {
|
||||
const taskDisplay: AgentResultDisplay = {
|
||||
type: 'task_execution',
|
||||
subagentName: 'test-agent',
|
||||
taskDescription: 'Test task',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import type {
|
||||
Config,
|
||||
ToolResultDisplay,
|
||||
TaskResultDisplay,
|
||||
AgentResultDisplay,
|
||||
OutputUpdateHandler,
|
||||
ToolCallRequestInfo,
|
||||
ToolCallResponseInfo,
|
||||
|
|
@ -335,25 +335,25 @@ export function createToolProgressHandler(
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates an output update handler specifically for Task tool subagent execution.
|
||||
* This handler monitors TaskResultDisplay updates and converts them to protocol messages
|
||||
* Creates an output update handler specifically for Agent tool subagent execution.
|
||||
* This handler monitors AgentResultDisplay updates and converts them to protocol messages
|
||||
* using the unified adapter's subagent APIs. All emitted messages will have parent_tool_use_id set to
|
||||
* the task tool's callId.
|
||||
* the agent tool's callId.
|
||||
*
|
||||
* @param config - Config instance for getting output format
|
||||
* @param taskToolCallId - The task tool's callId to use as parent_tool_use_id for all subagent messages
|
||||
* @param agentToolCallId - The agent tool's callId to use as parent_tool_use_id for all subagent messages
|
||||
* @param adapter - The unified adapter instance (JsonOutputAdapter or StreamJsonOutputAdapter)
|
||||
* @returns An object containing the output update handler
|
||||
*/
|
||||
export function createTaskToolProgressHandler(
|
||||
export function createAgentToolProgressHandler(
|
||||
config: Config,
|
||||
taskToolCallId: string,
|
||||
agentToolCallId: string,
|
||||
adapter: JsonOutputAdapterInterface,
|
||||
): {
|
||||
handler: OutputUpdateHandler;
|
||||
} {
|
||||
// Track previous TaskResultDisplay states per tool call to detect changes
|
||||
const previousTaskStates = new Map<string, TaskResultDisplay>();
|
||||
// Track previous AgentResultDisplay states per tool call to detect changes
|
||||
const previousTaskStates = new Map<string, AgentResultDisplay>();
|
||||
// Track which tool call IDs have already emitted tool_use to prevent duplicates
|
||||
const emittedToolUseIds = new Set<string>();
|
||||
// Track which tool call IDs have already emitted tool_result to prevent duplicates
|
||||
|
|
@ -366,7 +366,7 @@ export function createTaskToolProgressHandler(
|
|||
* @returns ToolCallRequestInfo object
|
||||
*/
|
||||
const buildRequest = (
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
): ToolCallRequestInfo => ({
|
||||
callId: toolCall.callId,
|
||||
name: toolCall.name,
|
||||
|
|
@ -383,7 +383,7 @@ export function createTaskToolProgressHandler(
|
|||
* @returns ToolCallResponseInfo object
|
||||
*/
|
||||
const buildResponse = (
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
): ToolCallResponseInfo => ({
|
||||
callId: toolCall.callId,
|
||||
error:
|
||||
|
|
@ -403,7 +403,7 @@ export function createTaskToolProgressHandler(
|
|||
* @returns True if the tool call has result content to emit
|
||||
*/
|
||||
const hasResultContent = (
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
): boolean => {
|
||||
// Check resultDisplay string
|
||||
if (
|
||||
|
|
@ -429,14 +429,14 @@ export function createTaskToolProgressHandler(
|
|||
* @param fallbackStatus - Optional fallback status if toolCall.status should be overridden
|
||||
*/
|
||||
const emitToolUseIfNeeded = (
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
fallbackStatus?: 'executing' | 'awaiting_approval',
|
||||
): void => {
|
||||
if (emittedToolUseIds.has(toolCall.callId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolCallToEmit: NonNullable<TaskResultDisplay['toolCalls']>[number] =
|
||||
const toolCallToEmit: NonNullable<AgentResultDisplay['toolCalls']>[number] =
|
||||
fallbackStatus
|
||||
? {
|
||||
...toolCall,
|
||||
|
|
@ -449,7 +449,7 @@ export function createTaskToolProgressHandler(
|
|||
toolCallToEmit.status === 'awaiting_approval'
|
||||
) {
|
||||
if (adapter.processSubagentToolCall) {
|
||||
adapter.processSubagentToolCall(toolCallToEmit, taskToolCallId);
|
||||
adapter.processSubagentToolCall(toolCallToEmit, agentToolCallId);
|
||||
emittedToolUseIds.add(toolCall.callId);
|
||||
}
|
||||
}
|
||||
|
|
@ -461,7 +461,7 @@ export function createTaskToolProgressHandler(
|
|||
* @param toolCall - The tool call information
|
||||
*/
|
||||
const emitToolResultIfNeeded = (
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
): void => {
|
||||
if (emittedToolResultIds.has(toolCall.callId)) {
|
||||
return;
|
||||
|
|
@ -482,7 +482,7 @@ export function createTaskToolProgressHandler(
|
|||
'emitToolResult' in adapter &&
|
||||
typeof adapter.emitToolResult === 'function'
|
||||
) {
|
||||
adapter.emitToolResult(request, response, taskToolCallId);
|
||||
adapter.emitToolResult(request, response, agentToolCallId);
|
||||
} else {
|
||||
adapter.emitToolResult(request, response);
|
||||
}
|
||||
|
|
@ -495,8 +495,8 @@ export function createTaskToolProgressHandler(
|
|||
* @param previousCall - The previous state of the tool call (if any)
|
||||
*/
|
||||
const processToolCall = (
|
||||
toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
previousCall?: NonNullable<TaskResultDisplay['toolCalls']>[number],
|
||||
toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
previousCall?: NonNullable<AgentResultDisplay['toolCalls']>[number],
|
||||
): void => {
|
||||
const isCompleted =
|
||||
toolCall.status === 'success' || toolCall.status === 'failed';
|
||||
|
|
@ -531,14 +531,14 @@ export function createTaskToolProgressHandler(
|
|||
callId: string,
|
||||
outputChunk: ToolResultDisplay,
|
||||
) => {
|
||||
// Only process TaskResultDisplay (Task tool updates)
|
||||
// Only process AgentResultDisplay (Task tool updates)
|
||||
if (
|
||||
typeof outputChunk === 'object' &&
|
||||
outputChunk !== null &&
|
||||
'type' in outputChunk &&
|
||||
outputChunk.type === 'task_execution'
|
||||
) {
|
||||
const taskDisplay = outputChunk as TaskResultDisplay;
|
||||
const taskDisplay = outputChunk as AgentResultDisplay;
|
||||
const previous = previousTaskStates.get(callId);
|
||||
|
||||
// Only process if adapter supports subagent APIs
|
||||
|
|
@ -585,7 +585,7 @@ export function createTaskToolProgressHandler(
|
|||
? 'Task was cancelled'
|
||||
: 'Task execution failed');
|
||||
// Use subagent adapter's emitSubagentErrorResult method
|
||||
adapter.emitSubagentErrorResult(errorMessage, 0, taskToolCallId);
|
||||
adapter.emitSubagentErrorResult(errorMessage, 0, agentToolCallId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -601,7 +601,7 @@ export function createTaskToolProgressHandler(
|
|||
// Emit the user message with the correct parent_tool_use_id
|
||||
adapter.emitUserMessage(
|
||||
[{ text: taskDisplay.taskPrompt }],
|
||||
taskToolCallId,
|
||||
agentToolCallId,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -610,7 +610,7 @@ export function createTaskToolProgressHandler(
|
|||
}
|
||||
};
|
||||
|
||||
// No longer need to attach adapter to handler - task.ts uses TaskResultDisplay.message instead
|
||||
// No longer need to attach adapter to handler - task.ts uses AgentResultDisplay.message instead
|
||||
|
||||
return {
|
||||
handler: outputUpdateHandler,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue