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,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentRunner —— Agent 执行器(基于 dispatchReplyFromConfig 的实现)
|
|
3
|
+
*
|
|
4
|
+
* 历史背景:
|
|
5
|
+
* 早期实现直接调用 `runtime.subagent.run + waitForRun + events.onAgentEvent`,
|
|
6
|
+
* 该路径仅在「Gateway request 范围」内可用;当 channel plugin 在后台 socket
|
|
7
|
+
* 循环中(非 inbound 请求 scope)调用时,SDK 会抛
|
|
8
|
+
* "Plugin runtime subagent methods are only available during a gateway request"。
|
|
9
|
+
*
|
|
10
|
+
* 现行实现:
|
|
11
|
+
* 与私聊 inbound.ts 一致,统一走
|
|
12
|
+
* `runtime.channel.reply.dispatchReplyFromConfig` + `withReplyDispatcher`
|
|
13
|
+
* 该路径由 SDK 自行管理 agent run 生命周期,无需手动操作 subagent。
|
|
14
|
+
*
|
|
15
|
+
* 职责:
|
|
16
|
+
* 1. 拼装 sessionKey:`agent:<agentId>:<channel>:group:<groupId>`
|
|
17
|
+
* 2. 通过 `finalizeInboundContext` 构造群聊 InboundContext(ChatType: 'group')
|
|
18
|
+
* 3. 自定义 lightweight ReplyDispatcher:
|
|
19
|
+
* - sendBlockReply / sendFinalReply 用于聚合 finalText(也作为兜底来源)
|
|
20
|
+
* - sendToolResult 用于产 tool kind 的 StreamChunk
|
|
21
|
+
* 4. 自定义 replyOptions:
|
|
22
|
+
* - onPartialReply / onAssistantMessageStart / onToolStart 转 StreamChunk
|
|
23
|
+
* - onAgentRunStart 捕获真实 runId
|
|
24
|
+
* - abortSignal 透传给 SDK,原生级联中止
|
|
25
|
+
* 5. 用 `withReplyDispatcher(dispatcher, run)` 包裹 dispatchReplyFromConfig,
|
|
26
|
+
* 自动处理 markComplete / 异常兜底
|
|
27
|
+
* 6. 错误兜底:SDK 抛错 / 超时 / abort 都转换成结构化 status 返回,不再外抛
|
|
28
|
+
*
|
|
29
|
+
* 设计要点:
|
|
30
|
+
* - 不写群账本:调用方拿到 finalText 后由路径层决定是否写
|
|
31
|
+
* - 不感知出站协议:完全交给注入的 StreamSink
|
|
32
|
+
* - 不依赖 runtime.subagent / runtime.events,规避 gateway-request scope 限制
|
|
33
|
+
*/
|
|
34
|
+
import { CHANNEL_KEY } from '../../../config.js';
|
|
35
|
+
import { getLightclawRuntime } from '../../../runtime.js';
|
|
36
|
+
import { getModuleLogger } from '../../../utils/logger.js';
|
|
37
|
+
/**
|
|
38
|
+
* Agent 执行器
|
|
39
|
+
*/
|
|
40
|
+
export class AgentRunner {
|
|
41
|
+
accountId;
|
|
42
|
+
log;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.accountId = options.accountId;
|
|
45
|
+
this.log = getModuleLogger('group.agent-runner');
|
|
46
|
+
}
|
|
47
|
+
/** 获取 PluginRuntime 实例(延迟获取,确保运行时已初始化) */
|
|
48
|
+
get runtime() {
|
|
49
|
+
return getLightclawRuntime();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 拼装群聊场景下的 sessionKey
|
|
53
|
+
*
|
|
54
|
+
* 与 `src/inbound.ts` 中私聊形态保持一致的命名风格:
|
|
55
|
+
* 私聊:agent:<agentId>:<channel>:direct:<userId>
|
|
56
|
+
* 群聊:agent:<agentId>:<channel>:group:<groupId>
|
|
57
|
+
*
|
|
58
|
+
* @param agentId - Agent ID
|
|
59
|
+
* @param groupId - 群 ID
|
|
60
|
+
*/
|
|
61
|
+
static buildGroupSessionKey(agentId, groupId) {
|
|
62
|
+
return `agent:${agentId}:${CHANNEL_KEY}:group:${groupId}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 执行一次 Agent run
|
|
66
|
+
*
|
|
67
|
+
* @param input - 执行入参
|
|
68
|
+
* @returns run 终态聚合结果
|
|
69
|
+
*/
|
|
70
|
+
async run(input) {
|
|
71
|
+
const sessionKey = AgentRunner.buildGroupSessionKey(input.agentId, input.groupId);
|
|
72
|
+
const tag = `[${CHANNEL_KEY}: AgentRunner ${input.role} ${input.agentId}@${input.groupId}]`;
|
|
73
|
+
const preRunId = input.preassignedRunId ?? '';
|
|
74
|
+
// ── 1. 已 abort 直接短路 ──
|
|
75
|
+
if (input.abortSignal.aborted) {
|
|
76
|
+
this.log.info(`${tag} abort before start`);
|
|
77
|
+
const empty = {
|
|
78
|
+
runId: preRunId,
|
|
79
|
+
agentId: input.agentId,
|
|
80
|
+
status: 'aborted',
|
|
81
|
+
finalText: '',
|
|
82
|
+
};
|
|
83
|
+
// 群聊场景:通过 sinkContext 发出 abort 终态帧
|
|
84
|
+
if (input.sinkContext) {
|
|
85
|
+
input.sinkContext.dispatcher.markComplete({ aborted: true });
|
|
86
|
+
}
|
|
87
|
+
return empty;
|
|
88
|
+
}
|
|
89
|
+
// ── 2. 装配 dispatcher 与 replyOptions ──
|
|
90
|
+
// 优先使用外部注入的 sinkContext(由路径层通过 createStreamReplyConfig 构造)
|
|
91
|
+
// 若未注入则内部构造(兼容旧调用方式,但群聊场景下应始终注入)
|
|
92
|
+
const sinkContext = input.sinkContext ?? this.createInternalSinkContext(input, tag);
|
|
93
|
+
const { dispatcher, replyOptions } = sinkContext;
|
|
94
|
+
// ── 3. 构造 InboundContext(群聊形态) ──
|
|
95
|
+
const fromAddress = `user:${input.userId}`;
|
|
96
|
+
// 构造媒体字段:SDK 的媒体理解管线依赖 MediaPaths/MediaUrls/MediaTypes 来处理文件
|
|
97
|
+
// (与私聊 inbound.ts 保持一致,确保 vision 模型能"看到"图片等媒体文件)
|
|
98
|
+
const mediaFields = {};
|
|
99
|
+
if (input.attachments && input.attachments.length > 0) {
|
|
100
|
+
const mediaUrls = input.attachments.map((a) => a.url);
|
|
101
|
+
const mediaTypes = input.attachments.map((a) => a.mimeType);
|
|
102
|
+
// 本地路径:SDK vision 模型优先通过 MediaPaths 读取本地文件内容
|
|
103
|
+
const mediaPaths = input.attachments.map((a) => a.localPath).filter((p) => !!p);
|
|
104
|
+
if (mediaPaths.length > 0) {
|
|
105
|
+
mediaFields.MediaPaths = mediaPaths;
|
|
106
|
+
mediaFields.MediaPath = mediaPaths[0];
|
|
107
|
+
}
|
|
108
|
+
mediaFields.MediaUrls = mediaUrls;
|
|
109
|
+
mediaFields.MediaUrl = mediaUrls[0];
|
|
110
|
+
mediaFields.MediaTypes = mediaTypes;
|
|
111
|
+
mediaFields.MediaType = mediaTypes[0];
|
|
112
|
+
}
|
|
113
|
+
const ctxPayload = this.runtime.channel.reply.finalizeInboundContext({
|
|
114
|
+
Body: input.message,
|
|
115
|
+
BodyForAgent: input.message,
|
|
116
|
+
RawBody: input.message,
|
|
117
|
+
CommandBody: input.message,
|
|
118
|
+
From: fromAddress,
|
|
119
|
+
To: `${CHANNEL_KEY}:${this.accountId}`,
|
|
120
|
+
Channel: CHANNEL_KEY,
|
|
121
|
+
ChatType: 'group',
|
|
122
|
+
SessionKey: sessionKey,
|
|
123
|
+
AccountId: this.accountId,
|
|
124
|
+
SenderId: input.userId,
|
|
125
|
+
SenderName: input.userId,
|
|
126
|
+
Provider: CHANNEL_KEY,
|
|
127
|
+
Surface: CHANNEL_KEY,
|
|
128
|
+
Timestamp: Date.now(),
|
|
129
|
+
OriginatingChannel: CHANNEL_KEY,
|
|
130
|
+
OriginatingTo: fromAddress,
|
|
131
|
+
AgentId: input.agentId,
|
|
132
|
+
// 群聊上下文额外标识:便于工具层按 groupId 还原会话
|
|
133
|
+
GroupId: input.groupId,
|
|
134
|
+
ConversationKind: 'group',
|
|
135
|
+
// 文件附件元数据:传递给 AI 上下文
|
|
136
|
+
Attachments: input.attachments && input.attachments.length > 0 ? input.attachments : undefined,
|
|
137
|
+
// 媒体字段:SDK 媒体理解管线所需(MediaUrls/MediaTypes),使 AI 能真正"看到"文件内容
|
|
138
|
+
...mediaFields,
|
|
139
|
+
// 群协作指令 + 群成员能力卡作为 GroupSystemPrompt 注入,
|
|
140
|
+
// SDK 会将其作为 extraSystemPromptParts 的一部分追加到 agent 的 system prompt 中,
|
|
141
|
+
// 确保 LLM 将派活指令视为系统级指令而非用户消息。
|
|
142
|
+
GroupSystemPrompt: input.systemPrompt || undefined,
|
|
143
|
+
});
|
|
144
|
+
// ── 4. 加载最新配置并执行 dispatch ──
|
|
145
|
+
const cfg = this.runtime.config.loadConfig();
|
|
146
|
+
let timedOut = false;
|
|
147
|
+
let timer;
|
|
148
|
+
// 构造 configOverride:
|
|
149
|
+
// - tools.deny: 禁用指定的内置工具(如 sessions_spawn)
|
|
150
|
+
// - tools.agentToAgent: 启用跨 Agent 通信权限(群聊派活场景必需)
|
|
151
|
+
// - responseFormat: 强制 JSON 输出(Planning 阶段使用)
|
|
152
|
+
const hasDeny = input.denyTools && input.denyTools.length > 0;
|
|
153
|
+
const hasA2A = input.enableAgentToAgent;
|
|
154
|
+
const hasJson = input.jsonMode;
|
|
155
|
+
const configOverride = hasDeny || hasA2A || hasJson
|
|
156
|
+
? {
|
|
157
|
+
tools: {
|
|
158
|
+
...(hasDeny ? { deny: input.denyTools } : {}),
|
|
159
|
+
...(hasA2A ? { agentToAgent: { enabled: true } } : {}),
|
|
160
|
+
},
|
|
161
|
+
...(hasJson ? { responseFormat: { type: 'json_object' } } : {}),
|
|
162
|
+
}
|
|
163
|
+
: undefined;
|
|
164
|
+
try {
|
|
165
|
+
await this.runtime.channel.reply.withReplyDispatcher({
|
|
166
|
+
dispatcher,
|
|
167
|
+
run: () => {
|
|
168
|
+
const dispatchPromise = this.runtime.channel.reply.dispatchReplyFromConfig({
|
|
169
|
+
ctx: ctxPayload,
|
|
170
|
+
cfg,
|
|
171
|
+
dispatcher,
|
|
172
|
+
replyOptions,
|
|
173
|
+
configOverride,
|
|
174
|
+
});
|
|
175
|
+
if (input.timeoutMs && input.timeoutMs > 0) {
|
|
176
|
+
return Promise.race([
|
|
177
|
+
dispatchPromise,
|
|
178
|
+
new Promise((_, reject) => {
|
|
179
|
+
timer = setTimeout(() => {
|
|
180
|
+
timedOut = true;
|
|
181
|
+
reject(new Error(`dispatchReplyFromConfig timeout after ${input.timeoutMs}ms`));
|
|
182
|
+
}, input.timeoutMs);
|
|
183
|
+
}),
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
return dispatchPromise;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
192
|
+
this.log.error(`${tag} dispatch failed: ${e.message}`);
|
|
193
|
+
// dispatch 异常时通过 markComplete 通知 sink 终态
|
|
194
|
+
dispatcher.markComplete({ aborted: timedOut || input.abortSignal.aborted });
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
if (timer)
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
}
|
|
200
|
+
// ── 5. 归一化 status ──
|
|
201
|
+
// 从 sinkContext 获取最终文本(通过 hasEmittedContent 判断是否有输出)
|
|
202
|
+
const realRunId = sinkContext.getRunId() || preRunId;
|
|
203
|
+
const aborted = input.abortSignal.aborted;
|
|
204
|
+
const normalizedStatus = aborted ? 'aborted' : timedOut ? 'timeout' : 'ok';
|
|
205
|
+
// 获取最终文本:从 sinkContext 获取聚合后的完整文本
|
|
206
|
+
// markComplete 已经在 withReplyDispatcher 内部被 SDK 调用过了(正常路径)
|
|
207
|
+
const finalText = sinkContext.getFinalText();
|
|
208
|
+
const result = {
|
|
209
|
+
runId: realRunId,
|
|
210
|
+
agentId: input.agentId,
|
|
211
|
+
status: normalizedStatus,
|
|
212
|
+
finalText,
|
|
213
|
+
error: timedOut ? new Error(`timeout after ${input.timeoutMs}ms`) : undefined,
|
|
214
|
+
};
|
|
215
|
+
this.log.info(`${tag} finished runId=${realRunId} status=${normalizedStatus} bytes=${finalText.length}`);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 内部构造 SinkContext(兼容旧调用方式,不推荐)
|
|
220
|
+
*
|
|
221
|
+
* 当调用方未注入 sinkContext 时使用此方法构造一个最小化的 context。
|
|
222
|
+
* 群聊场景下应始终由路径层注入外部构造的 sinkContext。
|
|
223
|
+
*/
|
|
224
|
+
createInternalSinkContext(input, tag) {
|
|
225
|
+
const log = this.log;
|
|
226
|
+
// 内部状态
|
|
227
|
+
let streamedText = '';
|
|
228
|
+
let finalText = '';
|
|
229
|
+
let lastFinalReplyText = '';
|
|
230
|
+
let abortDetected = false;
|
|
231
|
+
let completed = false;
|
|
232
|
+
const dispatcher = {
|
|
233
|
+
sendToolResult: (_payload) => {
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
sendBlockReply: (payload) => {
|
|
237
|
+
if (payload.text && payload.text.length > streamedText.length) {
|
|
238
|
+
streamedText = payload.text;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
},
|
|
242
|
+
sendFinalReply: (payload) => {
|
|
243
|
+
const text = payload.text ?? '';
|
|
244
|
+
if (/^⚙️ Agent was aborted\./.test(text.trim())) {
|
|
245
|
+
abortDetected = true;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
if (/Previous run is still shutting down/i.test(text)) {
|
|
249
|
+
log.warn(`${tag} SDK session lock conflict detected, suppressing: ${text.slice(0, 100)}`);
|
|
250
|
+
abortDetected = true;
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
if (text.length > 0) {
|
|
254
|
+
lastFinalReplyText = text;
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
},
|
|
258
|
+
waitForIdle: async () => {
|
|
259
|
+
/* no-op */
|
|
260
|
+
},
|
|
261
|
+
getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }),
|
|
262
|
+
getFailedCounts: () => ({ tool: 0, block: 0, final: 0 }),
|
|
263
|
+
markComplete: () => {
|
|
264
|
+
completed = true;
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
const replyOptions = {
|
|
268
|
+
runId: input.preassignedRunId,
|
|
269
|
+
abortSignal: input.abortSignal,
|
|
270
|
+
onPartialReply: (payload) => {
|
|
271
|
+
if (!payload.text)
|
|
272
|
+
return;
|
|
273
|
+
const delta = payload.text.startsWith(streamedText) ? payload.text.slice(streamedText.length) : payload.text;
|
|
274
|
+
streamedText = payload.text.startsWith(streamedText) ? payload.text : streamedText + delta;
|
|
275
|
+
},
|
|
276
|
+
onAssistantMessageStart: () => {
|
|
277
|
+
if (streamedText) {
|
|
278
|
+
finalText += (finalText ? '\n' : '') + streamedText;
|
|
279
|
+
streamedText = '';
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
suppressTyping: true,
|
|
283
|
+
disableBlockStreaming: true,
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
dispatcher,
|
|
287
|
+
replyOptions,
|
|
288
|
+
hasEmittedContent: () => streamedText.length > 0 || finalText.length > 0,
|
|
289
|
+
emitStart: () => {
|
|
290
|
+
/* no-op for internal sink */
|
|
291
|
+
},
|
|
292
|
+
getRunId: () => input.preassignedRunId,
|
|
293
|
+
getFinalText: () => {
|
|
294
|
+
const aggregated = finalText && streamedText ? `${finalText}\n${streamedText}` : finalText || streamedText;
|
|
295
|
+
return aggregated || lastFinalReplyText;
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptBuilder —— 三段式 system prompt 构造器
|
|
3
|
+
*
|
|
4
|
+
* 主 Agent 在群聊场景下扮演两种形态、子 Agent 一种形态,对应三个构造方法:
|
|
5
|
+
*
|
|
6
|
+
* 1. buildLeader 自答阶段:本体 SOUL(由 SDK 注入)+ 群协作扩展指令 + 群成员能力卡
|
|
7
|
+
*
|
|
8
|
+
* 2. buildSummarizer 汇总阶段:本体 SOUL(由 SDK 注入)+ 子任务结果列表
|
|
9
|
+
*
|
|
10
|
+
* 3. buildChild 子 Agent:本体 SOUL(由 SDK 注入)+ 任务上下文
|
|
11
|
+
*
|
|
12
|
+
* 设计要点:
|
|
13
|
+
* 1. 本体 SOUL 由 OpenClaw SDK 自动加载(`~/.openclaw/agents/<agentId>/SOUL.md`),
|
|
14
|
+
* 这里只产出 * 这里只产出"额外注入"的部分,通过 SubagentRunParams.extraSystemPrompt 传给 SDK;
|
|
15
|
+
* 2. 群成员能力卡来自群账本 meta.json 中的 GroupMember 数据;
|
|
16
|
+
* 3. 模板与"主Agent设计.md §2.2 / §2.3"中文案保持一致;
|
|
17
|
+
* 4. 纯函数:不做 IO,便于单测。
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* PromptBuilder 主体(无状态,方法纯函数)
|
|
21
|
+
*/
|
|
22
|
+
export class PromptBuilder {
|
|
23
|
+
/**
|
|
24
|
+
* 构造主 Agent 自答阶段的额外 system prompt(提供群成员信息上下文)
|
|
25
|
+
*/
|
|
26
|
+
buildLeader(input) {
|
|
27
|
+
const members = this.buildMemberCards(input.memberCards, input.leaderAgentId);
|
|
28
|
+
return `---
|
|
29
|
+
**场景设定**
|
|
30
|
+
在团队协作类群聊环境中,作为群内沟通协调的核心角色,负责引导和管理群内成员的交流流程、整合多方意见并推动问题解决,适用于团队项目讨论、工作事务沟通等多成员参与的实时互动场景。
|
|
31
|
+
|
|
32
|
+
**AI角色**
|
|
33
|
+
群聊协作管理专家(领航员),具备沟通引导、信息整合、问题分析与解决方案输出的专业能力,以协助团队成员高效完成协作任务。
|
|
34
|
+
|
|
35
|
+
**核心任务**
|
|
36
|
+
1. 接收并准确理解群内成员提出的问题、需求或请求;
|
|
37
|
+
2. 调用group_chat_history工具查询群内历史对话内容及其他成员的回复信息;
|
|
38
|
+
3. 基于历史对话和当前需求,提供针对性协作方案、沟通技巧或决策建议;
|
|
39
|
+
4. 以流式输出形式持续回应,保持对话连贯性与指导性。
|
|
40
|
+
|
|
41
|
+
**输出要求**
|
|
42
|
+
- 格式:自然语言文本
|
|
43
|
+
- 风格:专业、友好、指导性强,符合商务/团队协作沟通语境
|
|
44
|
+
- 长度:无严格字数限制,根据问题复杂度灵活调整,确保信息完整且不过于冗长
|
|
45
|
+
- 关键要素:包含问题回应、历史参考信息(如适用)、协作方案建议
|
|
46
|
+
|
|
47
|
+
**约束条件**
|
|
48
|
+
⚠️ 排除事项:不提供与群聊协作无关的内容;不泄露群成员隐私信息;不发表具有争议性或违法导向的观点;仅基于群内合理场景提供指导。敏感词过滤:不涉及政治敏感、人身攻击、负面情绪宣泄等内容。数据来源限制:仅依托群内历史对话记录和合理逻辑推导提供信息,不虚构群外未发生的事件或信息。
|
|
49
|
+
|
|
50
|
+
**群成员信息**
|
|
51
|
+
${members}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 构造 Planning 阶段的 system prompt(新方案:强制 JSON 输出决策)
|
|
55
|
+
*
|
|
56
|
+
* 该 prompt 要求 LLM 输出结构化 JSON,二选一:
|
|
57
|
+
* - { action: "self_answer" } → 自己直接回答
|
|
58
|
+
* - { action: "dispatch", ... } → 拆分派活
|
|
59
|
+
*/
|
|
60
|
+
buildPlanner(input) {
|
|
61
|
+
const members = this.buildMemberCards(input.memberCards, input.leaderAgentId);
|
|
62
|
+
return `---
|
|
63
|
+
## 场景设定
|
|
64
|
+
在一个多Agent协作的群聊环境中,你作为特定群组的领航员(对应 ${input.leaderAgentId} 角色),需要接收用户发送的消息并判断是否由自身直接回应或协调群内其他Agent协作完成任务,属于团队协作流程中的任务分配与决策环节。
|
|
65
|
+
|
|
66
|
+
## AI角色
|
|
67
|
+
团队协作流程中的群组领航型智能助手(任务决策与分配角色),具备分析任务复杂度、判断协作需求的专业能力。
|
|
68
|
+
|
|
69
|
+
## 核心任务
|
|
70
|
+
以群组领航员的身份,分析用户发送的消息内容,依据任务复杂度与群成员能力匹配度,从“自己直接回答”和“拆分派活”两种方案中选择其一,若选择拆分派活,需明确拆分逻辑、过渡话表述与子任务分配,确保符合协作约束规则。
|
|
71
|
+
|
|
72
|
+
## 输出要求
|
|
73
|
+
- 格式:JSON
|
|
74
|
+
- 风格:专业、简洁、指令性
|
|
75
|
+
- 长度:单条JSON对象,不超过200字符(含必要字段)
|
|
76
|
+
- 关键要素:包含action、thinking、transition、tasks等必要字段(若选拆分派活),或仅包含action字段(若选自己回答)。
|
|
77
|
+
- 你必须且只能输出一个 JSON 对象,不要输出任何其他文字、markdown 标记或代码块包裹。
|
|
78
|
+
|
|
79
|
+
## 输出格式
|
|
80
|
+
### 决策一:自己回答
|
|
81
|
+
|
|
82
|
+
{"action":"self_answer"}
|
|
83
|
+
|
|
84
|
+
### 决策二:拆分派活
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
"action": "dispatch",
|
|
88
|
+
"thinking": "简要说明你的拆分思路(1-2句话),需说明任务间是否有先后依赖",
|
|
89
|
+
"transition": "给用户看的过渡话,需要用 @agentName 提及对应的 Agent,例如:好的,我让 @设计师 负责logo,@程序员 负责页面开发。
|
|
90
|
+
"tasks": [
|
|
91
|
+
{
|
|
92
|
+
"id": "t1",
|
|
93
|
+
"to": "agentId_A",
|
|
94
|
+
"task": "第一个任务描述(无依赖,最先执行)",
|
|
95
|
+
"dependsOn": []
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "t2",
|
|
99
|
+
"to": "agentId_B",
|
|
100
|
+
"task": "第二个任务描述(需要t1的产出物才能完成)",
|
|
101
|
+
"dependsOn": ["t1"]
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
## 拆活约束
|
|
107
|
+
|
|
108
|
+
- \`to\` 必须是下方「群成员」列表中的 agentId,不能是你自己(${input.leaderAgentId})
|
|
109
|
+
- 最多拆 5 个子任务,能合并的合并
|
|
110
|
+
- 不可循环依赖
|
|
111
|
+
- \`task\` 描述要完整自洽,子 Agent 不会看到用户原始消息的全部上下文
|
|
112
|
+
- \`transition\` 过渡话中必须用 @name 格式提及每个被派活的 Agent(使用群成员的 name 而非 agentId),让用户清楚谁在做什么
|
|
113
|
+
|
|
114
|
+
## dependsOn 依赖判断规则
|
|
115
|
+
|
|
116
|
+
判断任务之间是否存在依赖关系,核心原则:**如果任务 B 需要使用任务 A 的产出物(如文件、设计稿、数据、代码等)才能正确完成,则 B 必须依赖 A**。
|
|
117
|
+
|
|
118
|
+
### 必须设置 dependsOn 的场景:
|
|
119
|
+
- 设计/创作类任务产出素材 → 开发/集成类任务使用该素材(如:设计logo → 把logo写到页面)
|
|
120
|
+
- 数据生成任务 → 数据消费任务(如:爬取数据 → 分析数据)
|
|
121
|
+
- 基础架构搭建 → 基于该架构的功能开发(如:搭建项目框架 → 开发具体功能模块)
|
|
122
|
+
|
|
123
|
+
### 可以并行(dependsOn 留空 [])的场景:
|
|
124
|
+
- 各任务之间相互独立,不需要对方的产出物(如:写文案 + 设计图标,两者互不依赖)
|
|
125
|
+
- 任务虽然属于同一项目,但各自可以独立完成(如:开发登录页 + 开发注册页)
|
|
126
|
+
|
|
127
|
+
### 示例:
|
|
128
|
+
用户说"设计一个logo并写到网页中":
|
|
129
|
+
- t1: 设计师设计logo → dependsOn: []
|
|
130
|
+
- t2: 程序员把logo写到网页 → dependsOn: ["t1"](因为程序员需要设计师产出的logo文件)
|
|
131
|
+
|
|
132
|
+
## 决策要点
|
|
133
|
+
|
|
134
|
+
- 若群内无成员具备匹配该任务的能力,则执行self_answer;
|
|
135
|
+
- 若任务仅需单一领域知识且自身能够胜任,则执行self_answer;
|
|
136
|
+
- 若任务涉及多个领域或明确需要特定专业Agent的能力支持,则执行dispatch;
|
|
137
|
+
- 若明确要求"大家"、"所有人"、"每个人"各自完成某事(如自我介绍、各自发表意见),则执行dispatch,让每个成员独立回答;
|
|
138
|
+
- 若任务可拆分成更小子任务,则尝试将任务拆分为合理子任务后再分配;
|
|
139
|
+
|
|
140
|
+
## 群成员信息
|
|
141
|
+
${members}
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
[用户消息]
|
|
145
|
+
|
|
146
|
+
${input.userMessage.trim()}`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 构造主 Agent 汇总阶段的额外 system prompt
|
|
150
|
+
*/
|
|
151
|
+
buildSummarizer(input) {
|
|
152
|
+
return `---
|
|
153
|
+
## 场景设定
|
|
154
|
+
在多群体聊协作项目完成后,各群成员已独立完成子任务,现处于汇总答复阶段,需要将各成员任务成果整合为统一最终答复提供给用户的场景。
|
|
155
|
+
|
|
156
|
+
## AI角色
|
|
157
|
+
团队协作汇总型专业助手,具备信息整合、内容提炼与自然语言沟通能力的协作支持角色,专注于完成从分散成果到完整答复的专业转化工作。
|
|
158
|
+
|
|
159
|
+
## 核心任务
|
|
160
|
+
接收各群成员提交的所有子任务成果,执行信息整合与提炼操作,以自然语言生成完整最终答复;确保涵盖所有关键信息、核心结论与要点,同时处理可能存在的失败子任务情况(诚实告知并提供建议);严格遵守汇总答复要求,仅输出整合后的最终答复,不涉及任务分配或决策类表述。
|
|
161
|
+
|
|
162
|
+
## 输出要求
|
|
163
|
+
- 格式:Markdown
|
|
164
|
+
- 风格:正式自然,逻辑连贯
|
|
165
|
+
- 长度:约50-200字
|
|
166
|
+
- 关键要素:各任务成果总结、核心结论、关键要点、后续建议(若有失败任务)、自然语言表达
|
|
167
|
+
|
|
168
|
+
## 约束条件
|
|
169
|
+
- 严格遵守禁止项:不输出JSON格式;
|
|
170
|
+
- 不逐字复述群员输出,需做信息整合;确保紧扣用户原始诉求,不凭空发挥;
|
|
171
|
+
- 若存在失败的子任务,需诚实告知用户并提出可行后续建议。
|
|
172
|
+
|
|
173
|
+
## 用户原始诉求
|
|
174
|
+
|
|
175
|
+
${input.userMessage.trim()}
|
|
176
|
+
|
|
177
|
+
## 群成员任务执行结果
|
|
178
|
+
${this.formatSubtaskResults(input.subtaskResults)}`;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 构造创建群后主Agent欢迎消息的额外 system prompt
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* 创建群成功后,主Agent需要根据群目标和成员能力,生成一条欢迎消息,
|
|
185
|
+
* 包含:欢迎词 + 群目标总结 + 开始方向建议。
|
|
186
|
+
*/
|
|
187
|
+
buildWelcome(input) {
|
|
188
|
+
const members = this.buildMemberCards(input.memberCards, input.leaderAgentId);
|
|
189
|
+
return `---
|
|
190
|
+
## 场景设定
|
|
191
|
+
你是一个多 Agent 协作群的领航员(主 Agent),群刚刚被创建成功。你需要发送一条欢迎消息,向创建者介绍这个群的目标和如何开始。
|
|
192
|
+
|
|
193
|
+
## AI角色
|
|
194
|
+
群聊协作领航员,负责引导团队开展工作、明确目标方向、提供建议。
|
|
195
|
+
|
|
196
|
+
## 核心任务
|
|
197
|
+
根据群的名称和目标描述,生成一条热情、专业的欢迎消息,内容包括:
|
|
198
|
+
1. 欢迎来到「群名称」
|
|
199
|
+
2. 简要总结群的终极目标是什么
|
|
200
|
+
3. 询问创建者当前的想法或方向(如风格、技术栈、优先级等)
|
|
201
|
+
4. 提供 2-3 个建议的开始方向,让创建者可以快速上手
|
|
202
|
+
5. 提及群规则(如可以随时 @ 成员)
|
|
203
|
+
|
|
204
|
+
## 输出要求
|
|
205
|
+
- 格式:Markdown
|
|
206
|
+
- 风格:热情、友好、专业,像一个经验丰富的团队协作者
|
|
207
|
+
- 长度:100-200字
|
|
208
|
+
- 语气:自然随和,不要过于正式
|
|
209
|
+
|
|
210
|
+
## 约束条件
|
|
211
|
+
- 不要输出 JSON 格式
|
|
212
|
+
- 不要派活或分配任务
|
|
213
|
+
- 不要编造群成员的能力,仅基于下方提供的成员信息
|
|
214
|
+
- **提及成员时必须使用名称(如 @程序员、@设计师),绝不要使用 agentId(如 @agent-xxx)**
|
|
215
|
+
|
|
216
|
+
## 群信息
|
|
217
|
+
- 群名称:${input.groupName}
|
|
218
|
+
- 群目标:${input.groupDesc}
|
|
219
|
+
|
|
220
|
+
${members}`;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 构造子 Agent 任务阶段的额外 system prompt
|
|
224
|
+
*/
|
|
225
|
+
buildChild(input) {
|
|
226
|
+
const userContext = input.userMessage?.trim()
|
|
227
|
+
? `\n## 用户原始诉求(仅供参考)\n\n${input.userMessage.trim()}\n`
|
|
228
|
+
: '';
|
|
229
|
+
return `---
|
|
230
|
+
[群聊协作 - 子任务模式]
|
|
231
|
+
|
|
232
|
+
你正在群聊中作为群成员协作。领航员(${input.leaderAgentId})把以下子任务派给了你:
|
|
233
|
+
|
|
234
|
+
## 你的任务
|
|
235
|
+
|
|
236
|
+
${input.taskDescription.trim()}
|
|
237
|
+
|
|
238
|
+
## 必须遵守
|
|
239
|
+
|
|
240
|
+
- ✅ 专注完成上述任务,输出可被领航员直接整合的结果。
|
|
241
|
+
- ✅ 输出要简洁、结构化,避免大段寒暄。
|
|
242
|
+
- ❌ **你不能再次派活给其他 Agent**(最多 1 层嵌套)。
|
|
243
|
+
- ❌ 不要替领航员给用户做最终答复,那是领航员的职责。
|
|
244
|
+
${userContext}
|
|
245
|
+
## 可用工具提示
|
|
246
|
+
|
|
247
|
+
- 如果你需要了解群聊中之前的对话内容或其他成员的回复,可以调用 \`group_chat_history\` 工具查询历史记录。`;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 群成员能力卡片块
|
|
251
|
+
*/
|
|
252
|
+
buildMemberCards(cards, leaderAgentId) {
|
|
253
|
+
const others = cards.filter((c) => c.agentId !== leaderAgentId);
|
|
254
|
+
if (others.length === 0) {
|
|
255
|
+
return `---
|
|
256
|
+
[群成员(不含你自己)]
|
|
257
|
+
|
|
258
|
+
(当前群里没有其他可派活成员,请直接独立完成任务。)`;
|
|
259
|
+
}
|
|
260
|
+
const memberList = others
|
|
261
|
+
.map((c) => {
|
|
262
|
+
const summary = c.summary.replace(/\n+/g, ' ').trim();
|
|
263
|
+
return `- **${c.name}** (agentId: ${c.agentId})\n ${summary}`;
|
|
264
|
+
})
|
|
265
|
+
.join('\n\n');
|
|
266
|
+
return `---
|
|
267
|
+
[群成员(不含你自己)]
|
|
268
|
+
|
|
269
|
+
${memberList}
|
|
270
|
+
|
|
271
|
+
> 注意:提及成员时请使用 @名称(如 @${others[0].name}),不要使用 @agentId`;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 子任务结果列表序列化
|
|
275
|
+
*/
|
|
276
|
+
formatSubtaskResults(results) {
|
|
277
|
+
if (results.length === 0) {
|
|
278
|
+
return '(无子任务结果)';
|
|
279
|
+
}
|
|
280
|
+
const blocks = results.map((r, idx) => {
|
|
281
|
+
const ok = r.status === 'done';
|
|
282
|
+
const head = `### 子任务 ${idx + 1} - ${r.taskId} (${r.agentId}) - ${ok ? '✅ 成功' : `❌ ${r.status}`}`;
|
|
283
|
+
const body = ok ? (r.output ?? '(无输出)').trim() : `失败原因:${(r.errorMessage ?? '未知错误').trim()}`;
|
|
284
|
+
return [head, '', body].join('\n');
|
|
285
|
+
});
|
|
286
|
+
return blocks.join('\n\n');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file SoulResolver —— 群成员能力解析器
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 从群账本 meta.json 中的 GroupMember 数据直接构建 AgentCapabilityCard
|
|
6
|
+
* - 不涉及任何文件 IO,能力信息完全来源于群元数据
|
|
7
|
+
* - 输出 AgentCapabilityCard,供 PromptBuilder 拼接能力卡片块
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 群成员能力解析器
|
|
11
|
+
*
|
|
12
|
+
* 将 GroupMember(群账本元数据)映射为 AgentCapabilityCard(能力卡片),
|
|
13
|
+
* 供 PromptBuilder 在构建 Leader 编排 prompt 时使用。
|
|
14
|
+
*/
|
|
15
|
+
export class SoulResolver {
|
|
16
|
+
/**
|
|
17
|
+
* 解析单个群成员为能力卡片
|
|
18
|
+
*
|
|
19
|
+
* @param member - 群成员信息(来自 meta.json 的 GroupMember)
|
|
20
|
+
* @returns 对应的 AgentCapabilityCard
|
|
21
|
+
*/
|
|
22
|
+
resolve(member) {
|
|
23
|
+
return {
|
|
24
|
+
agentId: member.agentId,
|
|
25
|
+
name: member.agentName || member.agentId,
|
|
26
|
+
summary: member.agentDesc || '',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 批量解析群成员列表为能力卡片数组(保证顺序与输入一致)
|
|
31
|
+
*
|
|
32
|
+
* @param members - GroupMember 列表(来自 meta.json)
|
|
33
|
+
* @returns AgentCapabilityCard 数组
|
|
34
|
+
*/
|
|
35
|
+
resolveMany(members) {
|
|
36
|
+
return members.map((m) => this.resolve(m));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Orchestrator 模块出口
|
|
3
|
+
* @description 统一导出调度器相关的类型、基础设施层、执行层、路径层和门面
|
|
4
|
+
*/
|
|
5
|
+
// ── 公共工具函数(从统一工具层转发) ──
|
|
6
|
+
export { generatePreRunId, normalizeRunStatus, errorToString, } from '../utils/index.js';
|
|
7
|
+
// ── 基础设施层(lifecycle) ──
|
|
8
|
+
export { ConversationStateMachine, RunRegistry, LedgerWriter, } from './lifecycle/index.js';
|
|
9
|
+
// ── 执行层(execution) ──
|
|
10
|
+
export { AgentRunner, SoulResolver, PromptBuilder, } from './execution/index.js';
|
|
11
|
+
// ── 路径层(routes) ──
|
|
12
|
+
export { LeaderOrchestrationRoute, } from './routes/index.js';
|
|
13
|
+
// ── 调度器门面 ──
|
|
14
|
+
export { Orchestrator } from './orchestrator.js';
|