Skip to content

Commit c39a083

Browse files
author
echoVic
committed
feat(子代理): 添加子代理会话ID支持并优化相关功能
refactor(任务工具): 重构任务工具以支持子代理会话跟踪 fix(会话路由): 修复会话列表去重逻辑并添加子代理过滤 style(前端): 优化子任务引用组件的交互和显示 perf(持久化存储): 改进子代理任务状态跟踪性能 chore(配置): 更新web开发命令以支持环境变量配置
1 parent 6b7f4c9 commit c39a083

13 files changed

Lines changed: 361 additions & 108 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,4 @@ packages/vscode/*.vsix
8282

8383
# Web 构建缓存
8484
packages/web/.vite/
85+
packages/cli/web/.vite/

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"scripts": {
88
"dev": "pnpm -r --parallel dev",
99
"dev:cli": "pnpm --filter blade-code dev",
10-
"dev:web": "cd packages/cli/web && pnpm dev",
10+
"dev:web": "pnpm --filter blade-code dev:serve & VITE_API_TARGET=http://localhost:4097 pnpm -C packages/cli/web dev",
1111
"build": "pnpm -r build",
1212
"build:cli": "pnpm --filter blade-code build",
1313
"build:vscode": "pnpm --filter blade-vscode build",

packages/cli/src/agent/Agent.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* 负责:LLM 交互、工具执行、循环检测
1111
*/
1212

13+
import { nanoid } from 'nanoid';
1314
import * as os from 'os';
1415
import * as path from 'path';
1516
import {
@@ -1104,6 +1105,16 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
11041105
try {
11051106
// 解析工具参数
11061107
const params = JSON.parse(toolCall.function.arguments);
1108+
if (
1109+
toolCall.function.name === 'Task' &&
1110+
(typeof params.subagent_session_id !== 'string' ||
1111+
params.subagent_session_id.length === 0)
1112+
) {
1113+
params.subagent_session_id =
1114+
typeof params.resume === 'string' && params.resume.length > 0
1115+
? params.resume
1116+
: nanoid();
1117+
}
11071118

11081119
// 智能修复: 如果 todos 参数被错误地序列化为字符串,自动解析
11091120
if (params.todos && typeof params.todos === 'string') {
@@ -1729,7 +1740,7 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
17291740
}
17301741

17311742
// 规范化上下文为 ChatContext
1732-
// 🔧 修复:确保复制 systemPrompt 和 permissionMode,避免子代理行为回归
1743+
// 🔧 修复:确保复制 systemPrompt、permissionModesubagentInfo,避免子代理行为回归
17331744
const chatContext: ChatContext = {
17341745
messages: context.messages as Message[],
17351746
userId: (context.userId as string) || 'subagent',
@@ -1739,6 +1750,7 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
17391750
confirmationHandler: context.confirmationHandler,
17401751
permissionMode: context.permissionMode, // 继承权限模式
17411752
systemPrompt: context.systemPrompt, // 🆕 继承系统提示词(无状态设计关键)
1753+
subagentInfo: context.subagentInfo, // 🆕 继承 subagent 信息(用于 JSONL 写入)
17421754
};
17431755

17441756
// 调用重构后的 runLoop

packages/cli/src/agent/subagents/SubagentExecutor.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class SubagentExecutor {
2121
*/
2222
async execute(context: SubagentContext): Promise<SubagentResult> {
2323
const startTime = Date.now();
24-
const agentId = nanoid();
24+
const agentId = context.subagentSessionId ?? nanoid();
2525

2626
try {
2727
const systemPrompt = this.buildSystemPrompt(context);
@@ -34,6 +34,12 @@ export class SubagentExecutor {
3434
let toolCallCount = 0;
3535
let tokensUsed = 0;
3636

37+
const subagentInfo = {
38+
parentSessionId: context.parentSessionId || '',
39+
subagentType: this.config.name,
40+
isSidechain: false,
41+
};
42+
3743
const loopResult = await agent.runAgenticLoop(
3844
context.prompt,
3945
{
@@ -43,11 +49,7 @@ export class SubagentExecutor {
4349
workspaceRoot: process.cwd(),
4450
permissionMode: context.permissionMode,
4551
systemPrompt,
46-
subagentInfo: {
47-
parentSessionId: context.parentSessionId || '',
48-
subagentType: this.config.name,
49-
isSidechain: false,
50-
},
52+
subagentInfo,
5153
},
5254
{
5355
onToolStart: context.onToolStart

packages/cli/src/agent/subagents/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ export interface SubagentContext {
121121
/** 父 Agent 的权限模式(继承给子 Agent) */
122122
permissionMode?: PermissionMode;
123123

124+
/** 子代理会话 ID(用于与主会话关联) */
125+
subagentSessionId?: string;
126+
124127
/** 工具执行开始回调(用于 UI 进度显示) */
125128
onToolStart?: (toolName: string) => void;
126129
}

packages/cli/src/context/storage/PersistentStore.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@ import * as fs from 'node:fs/promises';
33
import * as path from 'node:path';
44
import type { JsonValue, MessageRole } from '../../store/types.js';
55
import type {
6-
ContextData,
7-
ConversationContext,
8-
MessageInfo,
9-
PartInfo,
10-
SessionContext,
11-
SessionEvent,
12-
SessionInfo,
6+
ContextData,
7+
ConversationContext,
8+
MessageInfo,
9+
PartInfo,
10+
SessionContext,
11+
SessionEvent,
12+
SessionInfo,
1313
} from '../types.js';
1414
import { JSONLStore } from './JSONLStore.js';
1515
import {
16-
detectGitBranch,
17-
getProjectStoragePath,
18-
getSessionFilePath,
19-
listProjectDirectories,
16+
detectGitBranch,
17+
getProjectStoragePath,
18+
getSessionFilePath,
19+
listProjectDirectories,
2020
} from './pathUtils.js';
2121

2222
/**
@@ -197,6 +197,36 @@ export class PersistentStore {
197197
createdAt: now,
198198
};
199199
entries.push(this.createEvent('part_created', sessionId, partInfo));
200+
if (toolName === 'Task' && toolInput && typeof toolInput === 'object') {
201+
const subtaskInput = toolInput as Record<string, unknown>;
202+
const childSessionId =
203+
typeof subtaskInput.subagent_session_id === 'string'
204+
? subtaskInput.subagent_session_id
205+
: undefined;
206+
const agentType =
207+
typeof subtaskInput.subagent_type === 'string'
208+
? subtaskInput.subagent_type
209+
: undefined;
210+
if (childSessionId && agentType) {
211+
const subtaskPart: PartInfo = {
212+
partId: nanoid(),
213+
messageId,
214+
partType: 'subtask_ref',
215+
payload: {
216+
childSessionId,
217+
agentType,
218+
status: 'running',
219+
summary:
220+
typeof subtaskInput.description === 'string'
221+
? subtaskInput.description
222+
: '',
223+
startedAt: now,
224+
},
225+
createdAt: now,
226+
};
227+
entries.push(this.createEvent('part_created', sessionId, subtaskPart));
228+
}
229+
}
200230
await store.appendBatch(entries);
201231
return toolCallId;
202232
} catch (error) {
@@ -255,6 +285,8 @@ export class PersistentStore {
255285
};
256286
entries.push(this.createEvent('part_created', sessionId, toolResultPart));
257287
if (subagentRef) {
288+
const finishedAt =
289+
subagentRef.subagentStatus === 'running' ? null : now;
258290
const subtaskPart: PartInfo = {
259291
partId: nanoid(),
260292
messageId,
@@ -265,7 +297,7 @@ export class PersistentStore {
265297
status: subagentRef.subagentStatus,
266298
summary: subagentRef.subagentSummary ?? '',
267299
startedAt: now,
268-
finishedAt: now,
300+
finishedAt,
269301
},
270302
createdAt: now,
271303
};

packages/cli/src/server/routes/session.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ interface SessionInfo {
5656
createdAt: Date;
5757
messages: Message[];
5858
currentRunId?: string;
59+
relationType?: 'subagent';
5960
}
6061

6162
const sessions = new Map<string, SessionInfo>();
@@ -95,7 +96,13 @@ export const SessionRoutes = () => {
9596
try {
9697
const persistedSessions = await SessionService.listSessions();
9798

98-
const activeSessionsList = Array.from(sessions.values()).map((s) => ({
99+
const subagentSessionIds = new Set(
100+
persistedSessions.filter((s) => s.relationType === 'subagent').map((s) => s.sessionId)
101+
);
102+
103+
const activeSessionsList = Array.from(sessions.values())
104+
.filter((s) => !subagentSessionIds.has(s.id) && s.relationType !== 'subagent')
105+
.map((s) => ({
99106
sessionId: s.id,
100107
projectPath: s.projectPath,
101108
title: s.title,
@@ -111,11 +118,22 @@ export const SessionRoutes = () => {
111118
isActive: true,
112119
}));
113120

121+
const activeSessionIds = new Set(activeSessionsList.map((s) => s.sessionId));
122+
const filteredPersisted = persistedSessions.filter(
123+
(s) => !sessions.has(s.sessionId) && !activeSessionIds.has(s.sessionId) && s.relationType !== 'subagent'
124+
);
125+
126+
const seenSessionIds = new Set(activeSessionIds);
127+
const deduplicatedPersisted = filteredPersisted.filter((s) => {
128+
if (seenSessionIds.has(s.sessionId)) return false;
129+
seenSessionIds.add(s.sessionId);
130+
return true;
131+
});
132+
114133
const allSessions = [
115134
...activeSessionsList,
116-
...persistedSessions.filter((s) => !sessions.has(s.sessionId)),
135+
...deduplicatedPersisted,
117136
];
118-
119137
return c.json(allSessions);
120138
} catch (error) {
121139
logger.error('[SessionRoutes] Failed to list sessions:', error);
@@ -472,6 +490,7 @@ async function executeRunAsync(
472490
onToolStart: async (toolCall, toolKind) => {
473491
if (toolCall.type !== 'function') return;
474492
emit('tool.start', {
493+
messageId: assistantMessageId,
475494
toolName: toolCall.function.name,
476495
toolCallId: toolCall.id,
477496
arguments: toolCall.function.arguments,
@@ -481,6 +500,7 @@ async function executeRunAsync(
481500
onToolResult: async (toolCall, result) => {
482501
if (toolCall.type !== 'function') return;
483502
emit('tool.result', {
503+
messageId: assistantMessageId,
484504
toolName: toolCall.function.name,
485505
toolCallId: toolCall.id,
486506
success: !result.error,

packages/cli/src/tools/builtin/task/task.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ export const taskTool = createTool({
157157
.describe(
158158
'Optional agent ID to resume from. If provided, the agent will continue from the previous execution transcript.'
159159
),
160+
subagent_session_id: z
161+
.string()
162+
.optional()
163+
.describe('Internal subagent session id for tracking'),
160164
}),
161165

162166
// 工具描述
@@ -202,8 +206,15 @@ export const taskTool = createTool({
202206
prompt,
203207
run_in_background = false,
204208
resume,
209+
subagent_session_id,
205210
} = params;
206211
const { updateOutput } = context;
212+
const subagentSessionId =
213+
typeof subagent_session_id === 'string' && subagent_session_id.length > 0
214+
? subagent_session_id
215+
: typeof resume === 'string' && resume.length > 0
216+
? resume
217+
: nanoid();
207218

208219
try {
209220
// 1. 获取 subagent 配置
@@ -229,7 +240,13 @@ export const taskTool = createTool({
229240

230241
// 3. 处理后台执行模式
231242
if (run_in_background) {
232-
return handleBackgroundExecution(subagentConfig, description, prompt, context);
243+
return handleBackgroundExecution(
244+
subagentConfig,
245+
description,
246+
prompt,
247+
context,
248+
subagentSessionId
249+
);
233250
}
234251

235252
// 4. 同步执行模式(原有逻辑)
@@ -249,6 +266,7 @@ export const taskTool = createTool({
249266
prompt,
250267
parentSessionId: context.sessionId,
251268
permissionMode: context.permissionMode, // 继承父 Agent 的权限模式
269+
subagentSessionId,
252270
onToolStart: (toolName) => {
253271
vanillaStore.getState().app.actions.updateSubagentTool(toolName);
254272
},
@@ -330,7 +348,7 @@ export const taskTool = createTool({
330348
description,
331349
duration,
332350
stats: result.stats,
333-
subagentSessionId: result.agentId,
351+
subagentSessionId,
334352
subagentType: subagent_type,
335353
subagentStatus: 'completed' as const,
336354
subagentSummary: result.message.slice(0, 500),
@@ -352,7 +370,7 @@ export const taskTool = createTool({
352370
message: result.error || 'Unknown error',
353371
},
354372
metadata: {
355-
subagentSessionId: result.agentId,
373+
subagentSessionId,
356374
subagentType: subagent_type,
357375
subagentStatus: 'failed' as const,
358376
},
@@ -398,7 +416,8 @@ function handleBackgroundExecution(
398416
},
399417
description: string,
400418
prompt: string,
401-
context: ExecutionContext
419+
context: ExecutionContext,
420+
subagentSessionId: string
402421
): ToolResult {
403422
const manager = BackgroundAgentManager.getInstance();
404423

@@ -409,6 +428,7 @@ function handleBackgroundExecution(
409428
prompt,
410429
parentSessionId: context.sessionId,
411430
permissionMode: context.permissionMode,
431+
agentId: subagentSessionId,
412432
});
413433

414434
return {

packages/cli/src/tools/builtin/task/taskOutput.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ async function handleAgentOutput(
241241
stats: session.stats,
242242
};
243243

244+
const subagentStatus =
245+
session.status === 'completed'
246+
? 'completed'
247+
: session.status === 'failed'
248+
? 'failed'
249+
: 'running';
250+
244251
const statusEmoji = getStatusEmoji(session.status);
245252
const displayContent =
246253
`${statusEmoji} TaskOutput(${taskId}) - Agent\n` +
@@ -256,7 +263,16 @@ async function handleAgentOutput(
256263
success: true,
257264
llmContent: payload,
258265
displayContent,
259-
metadata: payload,
266+
metadata: {
267+
...payload,
268+
subagentSessionId: session.id,
269+
subagentType: session.subagentType,
270+
subagentStatus,
271+
subagentSummary:
272+
typeof session.result?.message === 'string'
273+
? session.result.message.slice(0, 500)
274+
: undefined,
275+
},
260276
};
261277
}
262278

0 commit comments

Comments
 (0)