Skip to content

Commit 6b7f4c9

Browse files
author
echoVic
committed
refactor(子会话): 重构子会话实现为独立会话模型
feat(存储): 引入新的JSONL事件schema支持会话、消息、部件分离 feat(UI): 添加子会话引用卡片展示与导航 feat(工具): 改造Task工具为子会话创建器 chore: 清理旧subagent相关代码与JSONL格式
1 parent ae75567 commit 6b7f4c9

55 files changed

Lines changed: 927 additions & 2071 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/subagent-subsession-plan.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# 子会话语义化对齐落地方案(不向下兼容)
2+
3+
## 目标
4+
- 将 subagent 的执行结果从“工具输出”升级为“独立子会话”
5+
- 主会话仅保存对子会话的引用与摘要,完整过程在子会话中保留
6+
- CLI 与 Web 的 UI 体验一致,可浏览子会话的完整消息流
7+
8+
## 不做的事
9+
- 不考虑向下兼容既有 JSONL schema 或旧会话加载逻辑
10+
- 不保留旧 subagent JSONL 文件与旧会话列表过滤逻辑
11+
12+
## 现状基线
13+
- 会话持久化基于 JSONL,写入入口在 PersistentStore
14+
- subagent 当前由 Task 工具执行,结果回填主会话
15+
16+
## 新的领域模型
17+
### Session
18+
字段建议
19+
- id
20+
- rootId
21+
- parentId
22+
- relationType: "subagent" | null
23+
- title
24+
- status: "running" | "completed" | "failed"
25+
- agentType
26+
- model
27+
- permission
28+
- createdAt
29+
- updatedAt
30+
31+
### Message
32+
- id
33+
- sessionId
34+
- role: "system" | "user" | "assistant"
35+
- parentId
36+
- createdAt
37+
38+
### Part
39+
类型建议
40+
- text
41+
- tool_call
42+
- tool_result
43+
- diff
44+
- patch
45+
- summary
46+
- subtask_ref
47+
48+
### SubtaskRef
49+
挂载在主会话的 message.parts 中
50+
- childSessionId
51+
- agentType
52+
- model
53+
- status
54+
- summary
55+
- startedAt
56+
- finishedAt
57+
58+
## 新的 JSONL schema
59+
建议按事件序列化,一行一条
60+
- session_created
61+
- session_updated
62+
- message_created
63+
- part_created
64+
- part_updated
65+
66+
示例
67+
- {"type":"session_created","data":{...}}
68+
- {"type":"message_created","data":{...}}
69+
- {"type":"part_created","data":{"partType":"subtask_ref",...}}
70+
71+
## 存储与索引
72+
- session 元信息与消息、part 仍落在 JSONL
73+
- 增加子会话索引文件 children/{parentId}.json
74+
- 子会话不再使用 agent_*.jsonl 命名,统一 sessionId.jsonl
75+
76+
## API 设计
77+
### Session API
78+
- createSession({parentId, relationType, agentType, model, permission})
79+
- listSessions({parentId})
80+
- getSession(sessionId)
81+
- updateSession(sessionId, patch)
82+
83+
### Message API
84+
- listMessages(sessionId)
85+
- appendMessage(sessionId, message)
86+
- appendPart(messageId, part)
87+
88+
### Subtask API
89+
- createSubtask({parentSessionId, agentType, prompt, model, permission})
90+
- getSubtask(childSessionId)
91+
- summarizeSubtask(childSessionId)
92+
93+
### Event API
94+
- subscribeSession(sessionId)
95+
- subscribeChildren(parentSessionId)
96+
97+
## 运行时执行流
98+
1. 主会话触发 subagent
99+
2. 生成子会话 sessionId 与 parentId
100+
3. 子会话进入独立 Agent loop
101+
4. 子会话完成时生成 summary part
102+
5. 主会话写入 subtask_ref part
103+
104+
## 权限与治理
105+
- 子会话权限默认独立配置
106+
- 主会话只控制是否允许创建子会话
107+
- 子会话工具权限与模型选择由 agentType 或显式配置决定
108+
109+
## CLI UI 设计
110+
### 主会话展示
111+
- Subtask 卡片
112+
- 标题:@agentType
113+
- 状态:running/completed/failed
114+
- 摘要:summary
115+
- 快捷键:打开子会话
116+
117+
### 子会话展示
118+
- 与主会话一致的消息流展示
119+
- 支持返回父会话
120+
121+
## Web UI 设计
122+
### 主会话展示
123+
- SubtaskCard 组件
124+
- 展示状态与摘要
125+
- 点击打开子会话面板
126+
127+
### 子会话面板
128+
- 独立消息流
129+
- 实时更新状态与新消息
130+
131+
## 关键改造点
132+
### 工具层
133+
- Task 工具不再直接返回完整文本结果
134+
- Task 工具返回 subtask_ref 并触发子会话创建
135+
136+
### 存储层
137+
- SessionService 统一加载新 JSONL 事件
138+
- 删除旧 agent_*.jsonl 的过滤逻辑
139+
140+
### Agent 运行时
141+
- Subagent 执行器从“工具执行”转为“子会话执行”
142+
- 子会话完成后生成 summary part
143+
144+
## 任务拆解
145+
### 数据与存储
146+
- 设计新的 JSONL 事件 schema 与类型定义
147+
- 实现 Session/Message/Part 的新读写层
148+
- 实现 children 索引与查询
149+
- 统一 sessionId 命名与文件路径规则
150+
151+
### 运行时与执行
152+
- Task 工具改造为子会话创建器
153+
- Subagent 执行器改为独立会话运行
154+
- 子会话完成后写入 summary part
155+
- 主会话追加 subtask_ref part
156+
157+
### 权限与配置
158+
- 子会话权限规则定义与解析
159+
- 子会话模型选择规则
160+
- 主会话创建子会话的权限控制
161+
162+
### CLI UI
163+
- SubtaskCard 组件
164+
- 子会话视图与导航
165+
- 主会话与子会话的状态同步
166+
167+
### Web UI
168+
- SubtaskCard 组件
169+
- 子会话面板与路由
170+
- Session 与 Subsession 的 store 拆分
171+
172+
### 观测与测试
173+
- JSONL 事件写入完整性测试
174+
- 子会话创建与回放测试
175+
- CLI/Web UI 的状态一致性验证
176+
177+
## 风险与约束
178+
- 旧会话不可读取
179+
- 子会话与主会话的权限隔离需严格执行
180+
- JSONL 事件增长可能带来性能压力
181+
182+
## 交付里程碑
183+
1. 新 JSONL schema 与 Session/Message/Part 基础读写
184+
2. Task → 子会话执行链路打通
185+
3. CLI 与 Web 子会话 UI 完成
186+
4. 权限与模型策略完善

knip.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,18 @@
66
"project": ["src/**/*.{ts,tsx}"],
77
"ignore": ["**/*.test.ts", "**/*.spec.ts", "tests/**/*"]
88
},
9-
"packages/web": {
9+
"packages/cli/web": {
1010
"entry": ["src/main.tsx"],
1111
"project": ["src/**/*.{ts,tsx}"],
1212
"ignore": ["**/*.test.ts", "**/*.spec.ts"]
1313
},
1414
"packages/vscode": {
15-
"entry": ["src/extension.ts"],
16-
"project": ["src/**/*.ts"],
17-
"ignore": ["**/*.test.ts", "**/*.spec.ts"]
18-
},
19-
"packages/shared": {
20-
"entry": ["src/index.ts"],
2115
"project": ["src/**/*.ts"],
2216
"ignore": ["**/*.test.ts", "**/*.spec.ts"]
2317
}
2418
},
25-
"ignoreDependencies": ["@azure/identity", "pino-pretty", "vscode"],
19+
"ignoreBinaries": ["vite"],
20+
"ignoreDependencies": [],
2621
"ignoreExportsUsedInFile": true,
2722
"rules": {
2823
"enumMembers": "off"

packages/cli/package.json

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -62,36 +62,15 @@
6262
"@ai-sdk/azure": "^3.0.5",
6363
"@ai-sdk/deepseek": "^2.0.4",
6464
"@ai-sdk/google": "^3.0.4",
65-
"@ai-sdk/openai": "^3.0.5",
6665
"@ai-sdk/openai-compatible": "^2.0.4",
67-
"@anthropic-ai/sdk": "^0.71.2",
68-
"@azure/identity": "^4.13.0",
69-
"@fontsource/jetbrains-mono": "^5.2.8",
70-
"@google/genai": "^1.34.0",
7166
"@inkjs/ui": "^2.0.0",
7267
"@modelcontextprotocol/sdk": "^1.17.4",
73-
"@radix-ui/react-dialog": "^1.1.15",
74-
"@radix-ui/react-dropdown-menu": "^2.1.16",
75-
"@radix-ui/react-label": "^2.1.8",
76-
"@radix-ui/react-popover": "^1.1.15",
77-
"@radix-ui/react-scroll-area": "^1.2.10",
78-
"@radix-ui/react-select": "^2.2.6",
79-
"@radix-ui/react-separator": "^1.1.8",
80-
"@radix-ui/react-slot": "^1.0.2",
81-
"@radix-ui/react-tabs": "^1.1.13",
82-
"@radix-ui/react-tooltip": "^1.2.8",
83-
"@xterm/addon-fit": "^0.11.0",
84-
"@xterm/addon-web-links": "^0.12.0",
85-
"@xterm/xterm": "^6.0.0",
8668
"ahooks": "^3.9.6",
8769
"ai": "^6.0.39",
8870
"ansi-escapes": "^7.2.0",
8971
"async-mutex": "^0.5.0",
9072
"axios": "^1.12.2",
9173
"chalk": "^5.4.1",
92-
"class-variance-authority": "^0.7.0",
93-
"clsx": "^2.1.0",
94-
"cmdk": "^1.1.1",
9574
"diff": "^8.0.2",
9675
"fast-glob": "^3.3.3",
9776
"fuse.js": "^7.1.0",
@@ -107,21 +86,14 @@
10786
"lodash-es": "^4.17.21",
10887
"lowlight": "^3.3.0",
10988
"lru-cache": "^11.2.4",
110-
"lucide-react": "^0.300.0",
11189
"nanoid": "^5.1.6",
11290
"open": "^10.1.2",
11391
"openai": "^6.2.0",
11492
"picomatch": "^4.0.3",
11593
"react": "^19.1.1",
11694
"react-dom": "^19.1.1",
117-
"react-markdown": "^10.1.0",
118-
"react-syntax-highlighter": "^16.1.0",
119-
"remark-gfm": "^4.0.1",
12095
"semver": "^7.7.3",
12196
"string-width": "^8.1.0",
122-
"swr": "^2.2.4",
123-
"tailwind-merge": "^2.2.0",
124-
"tailwindcss-animate": "^1.0.7",
12597
"undici": "^7.16.0",
12698
"write-file-atomic": "^7.0.0",
12799
"ws": "^8.18.0",
@@ -133,27 +105,20 @@
133105
},
134106
"devDependencies": {
135107
"@biomejs/biome": "^2.2.4",
136-
"@monaco-editor/react": "^4.7.0",
137108
"@types/bun": "^1.3.4",
138109
"@types/json-schema": "^7.0.15",
139110
"@types/lodash-es": "^4.17.12",
140111
"@types/node": "^22.15.24",
141112
"@types/picomatch": "^4.0.2",
142113
"@types/react": "^19.1.1",
143114
"@types/react-dom": "^19.1.1",
144-
"@types/react-syntax-highlighter": "^15.5.13",
145115
"@types/semver": "^7.7.1",
146116
"@types/write-file-atomic": "^4.0.3",
147117
"@types/ws": "^8.5.12",
148118
"@types/yargs": "^17.0.33",
149-
"@vitejs/plugin-react": "^4.2.1",
150119
"@vitest/coverage-v8": "^3.0.0",
151-
"autoprefixer": "^10.4.16",
152120
"jsdom": "^26.0.0",
153-
"postcss": "^8.4.32",
154-
"tailwindcss": "^3.4.0",
155121
"typescript": "^5.9.2",
156-
"vite": "^5.0.8",
157122
"vitest": "^3.0.0"
158123
}
159124
}

packages/cli/src/agent/Agent.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1245,14 +1245,44 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
12451245
try {
12461246
const contextMgr = this.executionEngine?.getContextManager();
12471247
if (contextMgr && context.sessionId) {
1248+
const metadata =
1249+
result.metadata && typeof result.metadata === 'object'
1250+
? (result.metadata as Record<string, unknown>)
1251+
: undefined;
1252+
const isSubagentStatus = (
1253+
value: unknown
1254+
): value is 'running' | 'completed' | 'failed' | 'cancelled' =>
1255+
value === 'running' ||
1256+
value === 'completed' ||
1257+
value === 'failed' ||
1258+
value === 'cancelled';
1259+
const subagentStatus = isSubagentStatus(metadata?.subagentStatus)
1260+
? metadata.subagentStatus
1261+
: 'completed';
1262+
const subagentRef =
1263+
metadata && typeof metadata.subagentSessionId === 'string'
1264+
? {
1265+
subagentSessionId: metadata.subagentSessionId,
1266+
subagentType:
1267+
typeof metadata.subagentType === 'string'
1268+
? metadata.subagentType
1269+
: toolCall.function.name,
1270+
subagentStatus,
1271+
subagentSummary:
1272+
typeof metadata.subagentSummary === 'string'
1273+
? metadata.subagentSummary
1274+
: undefined,
1275+
}
1276+
: undefined;
12481277
lastMessageUuid = await contextMgr.saveToolResult(
12491278
context.sessionId,
12501279
toolCall.id,
12511280
toolCall.function.name,
12521281
result.success ? toJsonValue(result.llmContent) : null,
12531282
toolUseUuid,
12541283
result.success ? undefined : result.error?.message,
1255-
context.subagentInfo
1284+
context.subagentInfo,
1285+
subagentRef
12561286
);
12571287
}
12581288
} catch (err) {

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* - 支持等待完成、恢复、终止
88
*/
99

10-
import { randomUUID } from 'crypto';
10+
import { nanoid } from 'nanoid';
1111
import type { PermissionMode } from '../../config/types.js';
1212
import { createLogger, LogCategory } from '../../logging/Logger.js';
1313
import type { Message } from '../../services/ChatServiceInterface.js';
@@ -129,7 +129,7 @@ export class BackgroundAgentManager {
129129
} = options;
130130

131131
// 生成或使用已有的 agent ID
132-
const id = agentId || `agent_${randomUUID()}`;
132+
const id = agentId || nanoid();
133133

134134
// 创建 AbortController 用于取消
135135
const abortController = new AbortController();
@@ -213,7 +213,7 @@ export class BackgroundAgentManager {
213213
subagentInfo: {
214214
parentSessionId: parentSessionId || '',
215215
subagentType: config.name,
216-
isSidechain: true,
216+
isSidechain: false,
217217
},
218218
};
219219

@@ -437,5 +437,3 @@ export class BackgroundAgentManager {
437437
return this.sessionStore.cleanupExpiredSessions(maxAgeMs);
438438
}
439439
}
440-
441-

0 commit comments

Comments
 (0)