lightclawbot 1.2.6-beta.0 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/gateway.js +50 -6
- package/dist/src/group/constants/index.js +20 -0
- package/dist/src/group/inbound/index.js +254 -0
- package/dist/src/group/index.js +15 -0
- package/dist/src/group/orchestrator/execution/agent-runner.js +299 -0
- package/dist/src/group/orchestrator/execution/index.js +7 -0
- package/dist/src/group/orchestrator/execution/prompt-builder.js +288 -0
- package/dist/src/group/orchestrator/execution/soul-resolver.js +38 -0
- package/dist/src/group/orchestrator/execution/types.js +7 -0
- package/dist/src/group/orchestrator/index.js +14 -0
- package/dist/src/group/orchestrator/lifecycle/conversation-state.js +162 -0
- package/dist/src/group/orchestrator/lifecycle/index.js +7 -0
- package/dist/src/group/orchestrator/lifecycle/ledger-writer.js +96 -0
- package/dist/src/group/orchestrator/lifecycle/run-registry.js +174 -0
- package/dist/src/group/orchestrator/orchestrator.js +265 -0
- package/dist/src/group/orchestrator/planning/index.js +13 -0
- package/dist/src/group/orchestrator/planning/plan-validator.js +233 -0
- package/dist/src/group/orchestrator/planning/planning-parser.js +207 -0
- package/dist/src/group/orchestrator/planning/subtask-executor.js +345 -0
- package/dist/src/group/orchestrator/planning/summarizer-runner.js +224 -0
- package/dist/src/group/orchestrator/routes/index.js +9 -0
- package/dist/src/group/orchestrator/routes/leader-dispatch.js +229 -0
- package/dist/src/group/orchestrator/routes/leader-orchestration-route.js +179 -0
- package/dist/src/group/orchestrator/routes/leader-planning.js +92 -0
- package/dist/src/group/orchestrator/routes/leader-self-answer.js +223 -0
- package/dist/src/group/orchestrator/routes/mention-concurrent-route.js +226 -0
- package/dist/src/group/orchestrator/routes/route-helpers.js +186 -0
- package/dist/src/group/orchestrator/routes/types.js +8 -0
- package/dist/src/group/services/group-cleanup-service.js +183 -0
- package/dist/src/group/services/group-creation-service.js +122 -0
- package/dist/src/group/services/group-deletion-service.js +111 -0
- package/dist/src/group/services/group-history-service.js +73 -0
- package/dist/src/group/services/group-member-service.js +169 -0
- package/dist/src/group/services/group-query-service.js +133 -0
- package/dist/src/group/services/group-update-service.js +144 -0
- package/dist/src/group/services/index.js +20 -0
- package/dist/src/group/storage/concurrency-manager.js +119 -0
- package/dist/src/group/storage/group-storage-core.js +227 -0
- package/dist/src/group/storage/index.js +12 -0
- package/dist/src/group/storage/message-reader.js +213 -0
- package/dist/src/group/storage/message-writer.js +229 -0
- package/dist/src/group/storage/slice-manager.js +165 -0
- package/dist/src/group/types/common.js +5 -0
- package/dist/src/group/types/index.js +5 -0
- package/dist/src/group/types/message.js +5 -0
- package/dist/src/group/types/orchestrator.js +5 -0
- package/dist/src/group/types/storage.js +5 -0
- package/dist/src/group/utils/id-generator.js +15 -0
- package/dist/src/group/utils/index.js +12 -0
- package/dist/src/group/utils/mime.js +36 -0
- package/dist/src/group/utils/normalize.js +32 -0
- package/dist/src/group/utils/run-helpers.js +36 -0
- package/dist/src/outbound.js +12 -19
- package/dist/src/shared.js +4 -3
- package/dist/src/socket/events/agents-request.js +147 -0
- package/dist/src/socket/events/chat-request.js +67 -0
- package/dist/src/socket/events/file-download.js +121 -0
- package/dist/src/socket/events/group-abort.js +59 -0
- package/dist/src/socket/events/group-history.js +59 -0
- package/dist/src/socket/events/group-member.js +83 -0
- package/dist/src/socket/events/group-request.js +91 -0
- package/dist/src/socket/events/history-request.js +95 -0
- package/dist/src/socket/events/index.js +39 -0
- package/dist/src/socket/events/message-private.js +82 -0
- package/dist/src/socket/handlers.js +53 -517
- package/dist/src/socket/native-socket.js +21 -20
- package/dist/src/socket/registry.js +6 -3
- package/dist/src/socket/reliable-emitter.js +16 -13
- package/dist/src/socket/service/chat-common.js +36 -0
- package/dist/src/socket/service/chat-create.js +75 -0
- package/dist/src/socket/service/chat-delete.js +94 -0
- package/dist/src/socket/service/chat-list.js +82 -0
- package/dist/src/socket/service/chat-update.js +83 -0
- package/dist/src/socket/service/group-abort.js +104 -0
- package/dist/src/socket/service/group-history.js +140 -0
- package/dist/src/socket/service/group-member.js +209 -0
- package/dist/src/socket/service/group.js +233 -0
- package/dist/src/socket/service/history.js +102 -0
- package/dist/src/socket/service/index.js +14 -0
- package/dist/src/socket/types/index.js +7 -0
- package/dist/src/socket/types/request.js +8 -0
- package/dist/src/socket/types/service.js +8 -0
- package/dist/src/socket/utils/agent-soul.js +95 -0
- package/dist/src/socket/utils/index.js +8 -0
- package/dist/src/socket/utils/message.js +83 -0
- package/dist/src/socket/utils/validate.js +42 -0
- package/dist/src/streaming/index.js +1 -0
- package/dist/src/streaming/stream-reply-sink.js +270 -14
- package/dist/src/streaming/types.js +20 -1
- package/dist/src/{download-tool.js → tools/download-tool.js} +41 -35
- package/dist/src/tools/group-history-tool.js +172 -0
- package/dist/src/{upload-tool.js → tools/upload-tool.js} +2 -2
- package/dist/src/tools.js +4 -3
- package/dist/src/utils/index.js +1 -0
- package/dist/src/utils/logger.js +38 -0
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/dist/src/socket/chat.js +0 -257
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview PlanningParser —— Planning 阶段 JSON 决策解析器
|
|
3
|
+
*
|
|
4
|
+
* 本模块负责解析主 Agent(Leader)在 Planning 阶段输出的 JSON 决策文本,
|
|
5
|
+
* 将其转换为结构化的 TypeScript 对象,供后续流程使用。
|
|
6
|
+
*
|
|
7
|
+
* 核心职责:
|
|
8
|
+
* 1. 解析 LLM 输出的 JSON 文本(支持纯 JSON 和 markdown 代码块两种格式);
|
|
9
|
+
* 2. 区分 "self_answer"(主 Agent 自己回答)和 "dispatch"(拆分派活)两种决策;
|
|
10
|
+
* 3. dispatch 决策时提取 transition(过渡话)和 tasks(子任务列表);
|
|
11
|
+
* 4. 容错处理:JSON 解析失败时返回结构化的错误信息。
|
|
12
|
+
*
|
|
13
|
+
* 设计要点:
|
|
14
|
+
* - 纯函数实现,不做任何 IO 操作,便于单元测试;
|
|
15
|
+
* - 与 PlanValidator 配合使用:本模块只做 JSON 解析 + 结构提取,
|
|
16
|
+
* 字段合法性校验(如 id 唯一性、成员归属、循环依赖等)由 PlanValidator 负责。
|
|
17
|
+
*
|
|
18
|
+
* @module planning-parser
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* 解析 LLM 输出的 Planning JSON 决策
|
|
22
|
+
*
|
|
23
|
+
* 这是本模块的主入口函数,负责将 LLM 的文本输出转换为结构化决策对象。
|
|
24
|
+
*
|
|
25
|
+
* 容错策略(按优先级依次尝试):
|
|
26
|
+
* 1. 直接对整段文本执行 JSON.parse;
|
|
27
|
+
* 2. 若失败,尝试从文本中提取 ```json ... ``` markdown 代码块后再解析;
|
|
28
|
+
* 3. 若仍失败,返回 ok=false 并附带错误描述。
|
|
29
|
+
*
|
|
30
|
+
* @param text - LLM 输出的 finalText(可能是纯 JSON 或包含 markdown 代码块)
|
|
31
|
+
* @returns 解析结果,包含决策对象或错误信息
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const result = parsePlanningDecision('{"action":"self_answer"}');
|
|
36
|
+
* if (result.ok) {
|
|
37
|
+
* console.log(result.decision); // { action: 'self_answer' }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function parsePlanningDecision(text) {
|
|
42
|
+
const trimmed = text.trim();
|
|
43
|
+
// 空内容直接返回失败
|
|
44
|
+
if (!trimmed) {
|
|
45
|
+
return { ok: false, error: '响应内容为空', rawText: text };
|
|
46
|
+
}
|
|
47
|
+
// 第一步:尝试解析 JSON(含 markdown 代码块容错)
|
|
48
|
+
const parseResult = tryParseJson(trimmed);
|
|
49
|
+
if (!parseResult.ok) {
|
|
50
|
+
return { ok: false, error: parseResult.error, rawText: text };
|
|
51
|
+
}
|
|
52
|
+
const parsed = parseResult.data;
|
|
53
|
+
// 第二步:校验顶层结构必须是对象
|
|
54
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
55
|
+
return { ok: false, error: '解析结果不是一个对象', rawText: text };
|
|
56
|
+
}
|
|
57
|
+
const obj = parsed;
|
|
58
|
+
const action = obj.action;
|
|
59
|
+
// 第三步:根据 action 字段分发处理
|
|
60
|
+
if (action === 'self_answer') {
|
|
61
|
+
return { ok: true, decision: { action: 'self_answer' }, rawText: text };
|
|
62
|
+
}
|
|
63
|
+
if (action === 'dispatch') {
|
|
64
|
+
return buildDispatchResult(obj, text);
|
|
65
|
+
}
|
|
66
|
+
// action 字段不是已知值
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: `未知的 action 类型: "${String(action)}"`,
|
|
70
|
+
rawText: text,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 从 DispatchDecision 中提取 Plan 对象
|
|
75
|
+
*
|
|
76
|
+
* 将解析器产出的 DispatchDecision 转换为 PlanValidator 所需的 Plan 结构。
|
|
77
|
+
* 主要处理:将空的 dependsOn 数组转为 undefined,避免下游产生无意义的空依赖。
|
|
78
|
+
*
|
|
79
|
+
* @param decision - 已解析的 dispatch 决策
|
|
80
|
+
* @returns 标准化的 Plan 对象,可直接传入 PlanValidator 进行校验
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const plan = extractPlanFromDecision(dispatchDecision);
|
|
85
|
+
* const validationResult = validatePlan(plan, context);
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function extractPlanFromDecision(decision) {
|
|
89
|
+
return {
|
|
90
|
+
tasks: decision.tasks.map((t) => ({
|
|
91
|
+
id: t.id,
|
|
92
|
+
to: t.to,
|
|
93
|
+
task: t.task,
|
|
94
|
+
// 空数组视为无依赖,设为 undefined 以简化下游判断
|
|
95
|
+
dependsOn: t.dependsOn?.length ? t.dependsOn : undefined,
|
|
96
|
+
})),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 尝试解析 JSON 文本
|
|
101
|
+
*
|
|
102
|
+
* 支持两种格式:
|
|
103
|
+
* 1. 纯 JSON 文本(直接 parse)
|
|
104
|
+
* 2. 包含 markdown 代码块的文本(提取 ```json ... ``` 内容后 parse)
|
|
105
|
+
*
|
|
106
|
+
* @param text - 待解析的文本(已 trim)
|
|
107
|
+
* @returns 解析结果,成功时包含 data,失败时包含 error 描述
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
function tryParseJson(text) {
|
|
111
|
+
// 策略一:直接解析整段文本
|
|
112
|
+
try {
|
|
113
|
+
return { ok: true, data: JSON.parse(text) };
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// 直接解析失败,继续尝试代码块提取
|
|
117
|
+
}
|
|
118
|
+
// 策略二:尝试匹配 markdown 代码块(支持 ```json 和 ``` 两种写法)
|
|
119
|
+
const codeBlockMatch = /```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/.exec(text);
|
|
120
|
+
if (codeBlockMatch?.[1]) {
|
|
121
|
+
try {
|
|
122
|
+
return { ok: true, data: JSON.parse(codeBlockMatch[1].trim()) };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// 代码块内容也无法解析
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: `JSON 解析失败(已尝试代码块提取): ${text.slice(0, 200)}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 两种策略均失败,返回错误(附带前 200 字符用于调试)
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: `JSON 解析失败: ${text.slice(0, 200)}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 构建 dispatch 类型的解析结果
|
|
140
|
+
*
|
|
141
|
+
* 从已解析的 JSON 对象中提取 dispatch 决策所需的字段:
|
|
142
|
+
* - thinking: 可选的拆分思路
|
|
143
|
+
* - transition: 过渡话(缺失时默认为空字符串)
|
|
144
|
+
* - tasks: 子任务数组(逐项标准化)
|
|
145
|
+
*
|
|
146
|
+
* @param obj - 已解析的 JSON 对象(action 已确认为 'dispatch')
|
|
147
|
+
* @param rawText - 原始文本(透传到结果中用于日志)
|
|
148
|
+
* @returns 解析结果
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
function buildDispatchResult(obj, rawText) {
|
|
152
|
+
// 提取可选的拆分思路(仅 string 类型有效)
|
|
153
|
+
const thinking = typeof obj.thinking === 'string' ? obj.thinking : undefined;
|
|
154
|
+
// 提取过渡话(缺失或非 string 时默认为空)
|
|
155
|
+
const transition = typeof obj.transition === 'string' ? obj.transition : '';
|
|
156
|
+
// 提取任务数组(非数组时降级为空数组)
|
|
157
|
+
const tasks = Array.isArray(obj.tasks) ? obj.tasks : [];
|
|
158
|
+
// tasks 为空时视为无效的 dispatch 决策
|
|
159
|
+
if (tasks.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error: 'dispatch 决策的 tasks 数组为空',
|
|
163
|
+
rawText,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// 逐项标准化任务结构
|
|
167
|
+
const extractedTasks = tasks.map(normalizeTaskItem);
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
decision: {
|
|
171
|
+
action: 'dispatch',
|
|
172
|
+
thinking,
|
|
173
|
+
transition,
|
|
174
|
+
tasks: extractedTasks,
|
|
175
|
+
},
|
|
176
|
+
rawText,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 标准化单个 task 项
|
|
181
|
+
*
|
|
182
|
+
* 仅做结构提取和类型安全转换,不做业务合法性校验。
|
|
183
|
+
* 对于缺失或类型不匹配的字段,使用空字符串作为默认值,
|
|
184
|
+
* 后续由 PlanValidator 统一报告校验错误。
|
|
185
|
+
*
|
|
186
|
+
* @param t - 原始 task 数据(类型未知)
|
|
187
|
+
* @returns 标准化后的 task 对象
|
|
188
|
+
* @internal
|
|
189
|
+
*/
|
|
190
|
+
function normalizeTaskItem(t) {
|
|
191
|
+
// 非对象类型直接返回空壳(由 PlanValidator 报错)
|
|
192
|
+
if (!t || typeof t !== 'object') {
|
|
193
|
+
return { id: '', to: '', task: '' };
|
|
194
|
+
}
|
|
195
|
+
const item = t;
|
|
196
|
+
// 过滤 dependsOn 中的非 string 元素,确保类型安全
|
|
197
|
+
const dependsOn = Array.isArray(item.dependsOn)
|
|
198
|
+
? item.dependsOn.filter((d) => typeof d === 'string')
|
|
199
|
+
: undefined;
|
|
200
|
+
return {
|
|
201
|
+
id: typeof item.id === 'string' ? item.id : '',
|
|
202
|
+
to: typeof item.to === 'string' ? item.to : '',
|
|
203
|
+
task: typeof item.task === 'string' ? item.task : '',
|
|
204
|
+
// 仅在有有效依赖时才设置 dependsOn 字段
|
|
205
|
+
...(dependsOn?.length ? { dependsOn } : {}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubtaskExecutor —— 子任务 DAG 执行器
|
|
3
|
+
*
|
|
4
|
+
* 职责(场景 C 拆活路径的"运行儿子"那一段):
|
|
5
|
+
* 1. 将主 Agent 产出的 Plan,按 dependsOn 做拓扑分层;
|
|
6
|
+
* 2. 层间串行、层内并发地驱动每个子 Agent 的 run(同层无依赖可安全并发);
|
|
7
|
+
* 3. 把上游产出的文本作为「## 前置任务结果」注入到下游 child 的 message 前缀;
|
|
8
|
+
* 4. 收集 SubtaskResult[],供后续 SummarizerRunner 汇总;
|
|
9
|
+
* 5. 失败容忍:单个 child 失败不阻断同层其他任务,下游依赖标记为 upstream_failed;
|
|
10
|
+
* 6. 级联 abort:任一 abort 立即跳过尚未启动的 child,已启动的由自身 abortSignal 终止。
|
|
11
|
+
*
|
|
12
|
+
* 设计要点:
|
|
13
|
+
* - 不写状态机:状态 PLANNING / RUNNING_CHILDREN 由调用方(路由)转移;
|
|
14
|
+
* - 不直接发出站事件:每个 child 的 sink 由本类内部构造,路径层不感知;
|
|
15
|
+
* - 成功的 child 写 assistant 账本(parentRunId=leaderRunId);
|
|
16
|
+
* 失败 / 中止写 system 账本(run_failed / user_aborted);
|
|
17
|
+
* - MVP 阶段简单实现:依赖 PlanValidator 已校验过环路 / 自引用 / 成员合法性。
|
|
18
|
+
*/
|
|
19
|
+
import { createStreamReplyConfig } from '../../../streaming/stream-reply-sink.js';
|
|
20
|
+
import { errorToString, normalizeRunStatus } from '../../utils/run-helpers.js';
|
|
21
|
+
import { getModuleLogger } from '../../../utils/logger.js';
|
|
22
|
+
import { generateMsgId } from '../../../dedup.js';
|
|
23
|
+
/**
|
|
24
|
+
* SubtaskExecutor 主体(无状态,可作为单例长期持有)
|
|
25
|
+
*/
|
|
26
|
+
export class SubtaskExecutor {
|
|
27
|
+
deps;
|
|
28
|
+
log = getModuleLogger('group.subtask-executor');
|
|
29
|
+
constructor(deps) {
|
|
30
|
+
this.deps = deps;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 执行整张 Plan,按拓扑分层:层间串行、层内并发跑子任务
|
|
34
|
+
*
|
|
35
|
+
* @returns 与 plan.tasks 一一对应的 SubtaskResult 列表(顺序与 plan.tasks 一致)
|
|
36
|
+
*/
|
|
37
|
+
async run(input) {
|
|
38
|
+
const { plan, ctx } = input;
|
|
39
|
+
const tag = `[SubtaskExecutor conv=${ctx.conversationId}]`;
|
|
40
|
+
const log = this.log;
|
|
41
|
+
// ── 1. 拓扑分层(Kahn) ──
|
|
42
|
+
const layers = this.topoLayers(plan.tasks);
|
|
43
|
+
log.info(`${tag} plan accepted tasks=${plan.tasks.length} layers=${layers.length}`);
|
|
44
|
+
// ── 2. 层间串行、层内并发执行 ──
|
|
45
|
+
// 同一层的任务无依赖关系,可安全并发;层间有依赖,必须串行等待上一层完成
|
|
46
|
+
const resultsById = new Map();
|
|
47
|
+
for (let i = 0; i < layers.length; i++) {
|
|
48
|
+
// 已被外部 abort:剩余层全部跳过
|
|
49
|
+
if (ctx.abortSignal.aborted) {
|
|
50
|
+
for (const remaining of layers.slice(i).flat()) {
|
|
51
|
+
resultsById.set(remaining.id, this.makeAbortedResult(remaining));
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
const layer = layers[i];
|
|
56
|
+
// 并发执行同层任务(无依赖关系,前端按 agentId 分区展示)
|
|
57
|
+
const layerResults = await Promise.all(layer.map((task) => {
|
|
58
|
+
if (ctx.abortSignal.aborted) {
|
|
59
|
+
return Promise.resolve(this.makeAbortedResult(task));
|
|
60
|
+
}
|
|
61
|
+
return this.runSingleTask(task, resultsById, input);
|
|
62
|
+
}));
|
|
63
|
+
for (const r of layerResults) {
|
|
64
|
+
resultsById.set(r.taskId, r);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// ── 3. 按 plan.tasks 原顺序输出结果 ──
|
|
68
|
+
return plan.tasks.map((t) => resultsById.get(t.id) ?? this.makeUpstreamFailedResult(t));
|
|
69
|
+
}
|
|
70
|
+
// ────────────────────────────────────────────────
|
|
71
|
+
// 内部:单任务执行
|
|
72
|
+
// ────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* 跑一个 child run;上游失败时直接产出 upstream_failed 结果不启动 SDK
|
|
75
|
+
*/
|
|
76
|
+
async runSingleTask(task, upstreamResults, input) {
|
|
77
|
+
const { ctx, leaderAgentId, leaderRunId, originalMsgId } = input;
|
|
78
|
+
const { runner, runRegistry, ledger, emitter, promptBuilder } = this.deps;
|
|
79
|
+
const log = this.log;
|
|
80
|
+
const tag = `[SubtaskExecutor conv=${ctx.conversationId} task=${task.id}]`;
|
|
81
|
+
// 1) 上游失败 → 直接短路
|
|
82
|
+
const blocking = this.collectBlockingUpstream(task, upstreamResults);
|
|
83
|
+
if (blocking.length > 0) {
|
|
84
|
+
const reason = `上游任务未完成: ${blocking.map((b) => `${b.taskId}(${b.status})`).join(', ')}`;
|
|
85
|
+
log.warn(`${tag} skipped due to upstream: ${reason}`);
|
|
86
|
+
try {
|
|
87
|
+
await ledger.appendSystem({
|
|
88
|
+
groupId: ctx.groupId,
|
|
89
|
+
event: 'run_failed',
|
|
90
|
+
agentId: task.to,
|
|
91
|
+
runId: `child:${task.id}:skipped`,
|
|
92
|
+
content: reason,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
log.warn(`${tag} appendSystem(skipped) failed: ${err}`);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
taskId: task.id,
|
|
100
|
+
agentId: task.to,
|
|
101
|
+
runId: `child:${task.id}:skipped`,
|
|
102
|
+
status: 'failed',
|
|
103
|
+
errorMessage: reason,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// 2) 注册 abort(使用稳定 runId 便于前端聚合)
|
|
107
|
+
const childRunId = `child:${ctx.conversationId}:${task.id}`;
|
|
108
|
+
const abortSignal = runRegistry.registerRun(ctx.conversationId, childRunId, task.to);
|
|
109
|
+
// 3) 构造 sinkContext
|
|
110
|
+
const replyMsgId = generateMsgId();
|
|
111
|
+
const signalCtx = {
|
|
112
|
+
emitter,
|
|
113
|
+
targetId: ctx.userId,
|
|
114
|
+
replyMsgId,
|
|
115
|
+
originalMsgId,
|
|
116
|
+
agentId: task.to,
|
|
117
|
+
};
|
|
118
|
+
const sinkContext = createStreamReplyConfig({
|
|
119
|
+
emitter,
|
|
120
|
+
targetId: ctx.userId,
|
|
121
|
+
replyMsgId,
|
|
122
|
+
originalMsgId,
|
|
123
|
+
effectiveApiKey: ctx.effectiveApiKey ?? '',
|
|
124
|
+
agentId: task.to,
|
|
125
|
+
groupId: ctx.groupId,
|
|
126
|
+
runId: childRunId,
|
|
127
|
+
parentRunId: leaderRunId,
|
|
128
|
+
conversationId: ctx.conversationId,
|
|
129
|
+
isSubtask: true,
|
|
130
|
+
}, { suppressTyping: true, runId: childRunId }, signalCtx);
|
|
131
|
+
sinkContext.emitStart();
|
|
132
|
+
// 4) 拼 system prompt + message(注入上游结果)
|
|
133
|
+
const systemPrompt = promptBuilder.buildChild({
|
|
134
|
+
childAgentId: task.to,
|
|
135
|
+
leaderAgentId,
|
|
136
|
+
taskDescription: task.task,
|
|
137
|
+
userMessage: ctx.userMessage,
|
|
138
|
+
});
|
|
139
|
+
const childMessage = this.buildChildMessage(task, upstreamResults);
|
|
140
|
+
// 5) 拉起 child run
|
|
141
|
+
let runOutput;
|
|
142
|
+
try {
|
|
143
|
+
runOutput = await runner.run({
|
|
144
|
+
groupId: ctx.groupId,
|
|
145
|
+
agentId: task.to,
|
|
146
|
+
userId: ctx.userId,
|
|
147
|
+
role: 'child',
|
|
148
|
+
parentRunId: leaderRunId,
|
|
149
|
+
message: childMessage,
|
|
150
|
+
systemPrompt,
|
|
151
|
+
abortSignal,
|
|
152
|
+
sinkContext,
|
|
153
|
+
preassignedRunId: childRunId,
|
|
154
|
+
// 子任务执行也需要跨 Agent 通信权限
|
|
155
|
+
enableAgentToAgent: true,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
160
|
+
log.error(`${tag} runner exception: ${e.message}`);
|
|
161
|
+
runRegistry.unregisterRun(ctx.conversationId, childRunId);
|
|
162
|
+
try {
|
|
163
|
+
await ledger.appendSystem({
|
|
164
|
+
groupId: ctx.groupId,
|
|
165
|
+
event: 'run_failed',
|
|
166
|
+
agentId: task.to,
|
|
167
|
+
runId: childRunId,
|
|
168
|
+
content: e.message,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (logErr) {
|
|
172
|
+
log.warn(`${tag} appendSystem(exception) failed: ${logErr}`);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
taskId: task.id,
|
|
176
|
+
agentId: task.to,
|
|
177
|
+
runId: childRunId,
|
|
178
|
+
status: 'failed',
|
|
179
|
+
errorMessage: errorToString(e),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// 6) 落账本
|
|
183
|
+
const status = normalizeRunStatus(runOutput.status);
|
|
184
|
+
const result = {
|
|
185
|
+
taskId: task.id,
|
|
186
|
+
agentId: task.to,
|
|
187
|
+
runId: runOutput.runId || childRunId,
|
|
188
|
+
status,
|
|
189
|
+
};
|
|
190
|
+
if (runOutput.status === 'ok') {
|
|
191
|
+
result.output = runOutput.finalText;
|
|
192
|
+
try {
|
|
193
|
+
await ledger.appendAssistant({
|
|
194
|
+
groupId: ctx.groupId,
|
|
195
|
+
agentId: task.to,
|
|
196
|
+
content: runOutput.finalText,
|
|
197
|
+
runId: result.runId,
|
|
198
|
+
parentRunId: leaderRunId,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
log.warn(`${tag} appendAssistant failed: ${err}`);
|
|
203
|
+
}
|
|
204
|
+
log.info(`${tag} done bytes=${runOutput.finalText.length}`);
|
|
205
|
+
}
|
|
206
|
+
else if (runOutput.status === 'aborted') {
|
|
207
|
+
// 被中止:如果已有部分输出内容,将其作为 assistant 条目持久化,
|
|
208
|
+
// 刷新页面后用户仍能看到中断前的内容。
|
|
209
|
+
// 不再写 user_aborted 系统事件——由上层路由(leader-dispatch)统一写一条即可。
|
|
210
|
+
result.errorMessage = 'aborted';
|
|
211
|
+
if (runOutput.finalText) {
|
|
212
|
+
result.output = runOutput.finalText;
|
|
213
|
+
try {
|
|
214
|
+
await ledger.appendAssistant({
|
|
215
|
+
groupId: ctx.groupId,
|
|
216
|
+
agentId: task.to,
|
|
217
|
+
content: runOutput.finalText,
|
|
218
|
+
runId: result.runId,
|
|
219
|
+
parentRunId: leaderRunId,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
log.warn(`${tag} appendAssistant(aborted partial) failed: ${err}`);
|
|
224
|
+
}
|
|
225
|
+
log.info(`${tag} aborted but has partial content, persisted bytes=${runOutput.finalText.length}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
result.errorMessage = errorToString(runOutput.error);
|
|
230
|
+
try {
|
|
231
|
+
await ledger.appendSystem({
|
|
232
|
+
groupId: ctx.groupId,
|
|
233
|
+
event: 'run_failed',
|
|
234
|
+
agentId: task.to,
|
|
235
|
+
runId: result.runId,
|
|
236
|
+
content: result.errorMessage,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
log.warn(`${tag} appendSystem failed: ${err}`);
|
|
241
|
+
}
|
|
242
|
+
log.warn(`${tag} ${runOutput.status} reason=${result.errorMessage}`);
|
|
243
|
+
}
|
|
244
|
+
runRegistry.unregisterRun(ctx.conversationId, childRunId);
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
// ────────────────────────────────────────────────
|
|
248
|
+
// 内部:辅助方法
|
|
249
|
+
// ────────────────────────────────────────────────
|
|
250
|
+
/**
|
|
251
|
+
* 拓扑分层:返回 PlanItem[][],第 i 层在第 i-1 层全部完成后再执行
|
|
252
|
+
*/
|
|
253
|
+
topoLayers(tasks) {
|
|
254
|
+
const indeg = new Map();
|
|
255
|
+
const byId = new Map();
|
|
256
|
+
for (const t of tasks) {
|
|
257
|
+
indeg.set(t.id, (t.dependsOn ?? []).length);
|
|
258
|
+
byId.set(t.id, t);
|
|
259
|
+
}
|
|
260
|
+
const layers = [];
|
|
261
|
+
let remaining = tasks.length;
|
|
262
|
+
while (remaining > 0) {
|
|
263
|
+
const layer = [];
|
|
264
|
+
for (const [id, deg] of indeg) {
|
|
265
|
+
if (deg === 0)
|
|
266
|
+
layer.push(byId.get(id));
|
|
267
|
+
}
|
|
268
|
+
if (layer.length === 0) {
|
|
269
|
+
// PlanValidator 应该已经挡掉环路,这里仅作兜底;把剩余任务塞最后一层抛失败
|
|
270
|
+
const stuck = Array.from(indeg.keys()).map((id) => byId.get(id));
|
|
271
|
+
layers.push(stuck);
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
// 从入度表中移除本层
|
|
275
|
+
for (const t of layer)
|
|
276
|
+
indeg.delete(t.id);
|
|
277
|
+
// 后续任务的入度 -1
|
|
278
|
+
for (const id of indeg.keys()) {
|
|
279
|
+
const t = byId.get(id);
|
|
280
|
+
const deps = (t.dependsOn ?? []).filter((d) => indeg.has(d));
|
|
281
|
+
indeg.set(id, deps.length);
|
|
282
|
+
}
|
|
283
|
+
layers.push(layer);
|
|
284
|
+
remaining -= layer.length;
|
|
285
|
+
}
|
|
286
|
+
return layers;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 收集"阻塞下游执行"的上游:状态非 done 的依赖
|
|
290
|
+
*/
|
|
291
|
+
collectBlockingUpstream(task, upstream) {
|
|
292
|
+
const blocking = [];
|
|
293
|
+
for (const dep of task.dependsOn ?? []) {
|
|
294
|
+
const r = upstream.get(dep);
|
|
295
|
+
if (!r || r.status !== 'done')
|
|
296
|
+
blocking.push(r ?? this.makeMissingResult(dep, task.to));
|
|
297
|
+
}
|
|
298
|
+
return blocking;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* 把上游 done 结果拼接进 child 的 message,让子 Agent 能拿到上下文
|
|
302
|
+
*/
|
|
303
|
+
buildChildMessage(task, upstream) {
|
|
304
|
+
const deps = (task.dependsOn ?? [])
|
|
305
|
+
.map((d) => upstream.get(d))
|
|
306
|
+
.filter((r) => !!r && r.status === 'done' && !!r.output);
|
|
307
|
+
if (deps.length === 0)
|
|
308
|
+
return task.task;
|
|
309
|
+
const blocks = deps.map((d) => `- [${d.taskId} by ${d.agentId}]\n${(d.output ?? '').trim()}`);
|
|
310
|
+
return [
|
|
311
|
+
'## 前置任务结果',
|
|
312
|
+
blocks.join('\n\n'),
|
|
313
|
+
'',
|
|
314
|
+
'## 你的任务',
|
|
315
|
+
task.task,
|
|
316
|
+
].join('\n');
|
|
317
|
+
}
|
|
318
|
+
makeAbortedResult(task) {
|
|
319
|
+
return {
|
|
320
|
+
taskId: task.id,
|
|
321
|
+
agentId: task.to,
|
|
322
|
+
runId: `child:${task.id}:aborted`,
|
|
323
|
+
status: 'aborted',
|
|
324
|
+
errorMessage: '已停止',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
makeUpstreamFailedResult(task) {
|
|
328
|
+
return {
|
|
329
|
+
taskId: task.id,
|
|
330
|
+
agentId: task.to,
|
|
331
|
+
runId: `child:${task.id}:upstream_failed`,
|
|
332
|
+
status: 'failed',
|
|
333
|
+
errorMessage: 'upstream_failed',
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
makeMissingResult(taskId, agentId) {
|
|
337
|
+
return {
|
|
338
|
+
taskId,
|
|
339
|
+
agentId,
|
|
340
|
+
runId: `child:${taskId}:missing`,
|
|
341
|
+
status: 'failed',
|
|
342
|
+
errorMessage: 'missing upstream result',
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|