@aalis/plugin-agent 0.15.2 → 0.16.0
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/helpers.d.ts +16 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +31 -0
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts +13 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +688 -418
- package/dist/index.js.map +1 -1
- package/dist/prompt-assembly.d.ts +3 -3
- package/dist/prompt-assembly.d.ts.map +1 -1
- package/dist/prompt-assembly.js +27 -12
- package/dist/prompt-assembly.js.map +1 -1
- package/package.json +19 -19
package/dist/index.js
CHANGED
|
@@ -10,14 +10,14 @@ import { memory as memoryService } from '@aalis/api-memory';
|
|
|
10
10
|
import { messageArchive as messageArchiveService } from '@aalis/api-message-archive';
|
|
11
11
|
import { persona as personaService } from '@aalis/api-persona';
|
|
12
12
|
import { getPlatformSelfIdentity, platform as platformService } from '@aalis/api-platform';
|
|
13
|
-
import { sessionManager as sessionManagerService } from '@aalis/api-session-manager';
|
|
14
|
-
import {
|
|
13
|
+
import { rootSessionOf, SESSION_LAYERS, sessionManager as sessionManagerService, } from '@aalis/api-session-manager';
|
|
14
|
+
import { storage as storageService } from '@aalis/api-storage';
|
|
15
15
|
import { tools as toolsService, } from '@aalis/api-tools';
|
|
16
16
|
import { config as configCap, definePlugin, events, lifecycle, logger as loggerCap, optional, pluginsService, provide, services, } from '@aalis/core';
|
|
17
17
|
import { defineConfig, parseConfig } from '@aalis/schema-config';
|
|
18
18
|
import { CONTROL_KINDS, getMessageName, getSenderLabel, WellKnownKinds, WellKnownMetadataKeys, } from '@aalis/schema-message';
|
|
19
19
|
import { normalizeAssistantContent, stripLeakedSpecialTokens, truncateChars } from '@aalis/util-text-normalize';
|
|
20
|
-
import { buildFocusGuidance, describeLLMFailure, estimateMsgTokens, estimateTextTokens, estimateTokens, formatTimeLabel, formatToolArgsForLog, INPUT_CONVENTIONS, isSameMessage, toPersonaOptions, } from './helpers.js';
|
|
20
|
+
import { buildFocusGuidance, describeLLMFailure, estimateHistoryTokens, estimateMsgTokens, estimateTextTokens, estimateTokens, formatTimeLabel, formatToolArgsForLog, INPUT_CONVENTIONS, isSameMessage, parseToolArguments, toPersonaOptions, } from './helpers.js';
|
|
21
21
|
import { assemblePromptContributions, VOLATILE_INJECTOR } from './prompt-assembly.js';
|
|
22
22
|
// ===== 能力声明 =====
|
|
23
23
|
//
|
|
@@ -32,7 +32,6 @@ const uses = {
|
|
|
32
32
|
contributions,
|
|
33
33
|
lifecycle,
|
|
34
34
|
provide,
|
|
35
|
-
services,
|
|
36
35
|
commands: optional(commandsService),
|
|
37
36
|
tools: optional(toolsService),
|
|
38
37
|
llm: optional(llmService),
|
|
@@ -45,6 +44,8 @@ const uses = {
|
|
|
45
44
|
storage: optional(storageService),
|
|
46
45
|
gateway: optional(gatewayService),
|
|
47
46
|
plugins: optional(pluginsService),
|
|
47
|
+
/** 会话没设模型时比对本插件该生效的 llm 偏好与实际用的模型 */
|
|
48
|
+
services,
|
|
48
49
|
};
|
|
49
50
|
/**
|
|
50
51
|
* 默认 Agent 实现 —— 对话编排器
|
|
@@ -61,16 +62,19 @@ const uses = {
|
|
|
61
62
|
*/
|
|
62
63
|
/** 统一读取 LLM 单次最大输出 token;缺省回退 4096(与各 adapter 默认一致)。 */
|
|
63
64
|
const DEFAULT_MAX_OUTPUT_TOKENS = 4096;
|
|
65
|
+
/** 会话设置「最大工具迭代」没设时一轮里工具调用循环的最大次数 */
|
|
66
|
+
const MAX_TOOL_ITERATIONS = 30;
|
|
67
|
+
// 宿主通知的公开链接须在最终正文保留;同时识别普通文本和 Markdown 链接的边界。
|
|
68
|
+
const NOTICE_URL = /https?:\/\/[^\s<>()[\]"'`*“”‘’,。!?;、]+/gu;
|
|
64
69
|
function getModelMaxOutput(llm) {
|
|
65
70
|
return llm.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
66
71
|
}
|
|
67
72
|
class DefaultAgent {
|
|
73
|
+
instanceId;
|
|
68
74
|
caps;
|
|
69
75
|
logger;
|
|
70
76
|
systemPrompt;
|
|
71
77
|
memoryTokenBudget;
|
|
72
|
-
historyLimit;
|
|
73
|
-
maxToolIterations;
|
|
74
78
|
/** 单个 agent:prompt 贡献 build 的等待上限(毫秒;0=不设限)——见 prompt-assembly 的护栏说明 */
|
|
75
79
|
promptBuildTimeoutMs;
|
|
76
80
|
/** 单条工具结果占上下文窗口的最大比例 (0~1),超出则截断 */
|
|
@@ -78,12 +82,17 @@ class DefaultAgent {
|
|
|
78
82
|
/** 内存裁剪触发比例 (0~1):估算输入 token 占 contextLength 的比例上限,超过则触发本次调用的内存裁剪 */
|
|
79
83
|
trimThresholdRatio;
|
|
80
84
|
/**
|
|
81
|
-
*
|
|
85
|
+
* 在飞回合表:lane → 这一轮的 AbortController 与 turnId
|
|
82
86
|
*
|
|
83
|
-
*
|
|
87
|
+
* lane = `${sessionId}::${source}` — 同一 session 不同来源(user / scheduler)
|
|
84
88
|
* 独立管理,互不打断;同来源新消息会中止旧的生成。
|
|
85
89
|
*/
|
|
86
90
|
activeControllers = new Map();
|
|
91
|
+
/**
|
|
92
|
+
* 延续某位发言者身份的宿主通知回合(lane → 会话与 hostNotice.callerUserId)。那位发言者在同一会话发来的真人消息
|
|
93
|
+
* 中止这些回合;别人的消息与不延续任何人身份的通知(白纸等)不受影响,宿主通知也从不打断别的回合。
|
|
94
|
+
*/
|
|
95
|
+
noticeCallers = new Map();
|
|
87
96
|
/**
|
|
88
97
|
* 在飞回合 Promise。关停时 abort 之后等它们以 AbortError(或正常完成)收尾,
|
|
89
98
|
* 而不是等 LLM 跑完——收尾里的钩子 / 出站事件还要用 memory。
|
|
@@ -100,15 +109,18 @@ class DefaultAgent {
|
|
|
100
109
|
* 与 token:usage 事件共用同一份 breakdown 数据,零额外计算。
|
|
101
110
|
*/
|
|
102
111
|
tokenLogState = new Map();
|
|
112
|
+
/** 已告警过的(llm 服务的偏好,实际用的模型)组合 */
|
|
113
|
+
preferenceFallbacks = new Set();
|
|
103
114
|
/** 已注册的预处理器(name → { dispose }) */
|
|
104
115
|
preprocessors = new Map();
|
|
105
|
-
constructor(caps, cfg
|
|
116
|
+
constructor(caps, cfg,
|
|
117
|
+
/** 本插件实例 id:读它的本插件偏好 */
|
|
118
|
+
instanceId) {
|
|
119
|
+
this.instanceId = instanceId;
|
|
106
120
|
this.caps = caps;
|
|
107
|
-
this.logger = caps.logger.child('agent');
|
|
121
|
+
this.logger = caps.logger.use.child('agent');
|
|
108
122
|
this.systemPrompt = cfg.systemPrompt;
|
|
109
123
|
this.memoryTokenBudget = cfg.memoryTokenBudget;
|
|
110
|
-
this.historyLimit = cfg.historyLimit;
|
|
111
|
-
this.maxToolIterations = cfg.maxToolIterations;
|
|
112
124
|
this.promptBuildTimeoutMs = cfg.promptBuildTimeoutMs;
|
|
113
125
|
this.toolResultMaxRatio = cfg.toolResultMaxRatio;
|
|
114
126
|
this.trimThresholdRatio = cfg.trimThresholdRatio;
|
|
@@ -117,38 +129,43 @@ class DefaultAgent {
|
|
|
117
129
|
/**
|
|
118
130
|
* 根据优先级链解析当前会话该用哪个 LLMModel entry。
|
|
119
131
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* 4. ServicePreference(由 agent 启动时锁定的全局默认 entry,未传 ref 时生效)
|
|
132
|
+
* 会话设置的 llm 交给 session-manager.resolveConfig 解析(按层表叠加,全局默认层即 llm 服务的偏好);
|
|
133
|
+
* 全局默认之上哪一层都没设时不给值,由 resolveLLMModel 落到本插件此刻选中的那一个(本插件偏好 > 全局偏好 > 提供者优先级 >
|
|
134
|
+
* 登记先后)。这时该生效的偏好(设了本插件偏好就是它,否则是全局偏好)指向的模型不在(模型删了、提供者在重载或激活失败),
|
|
135
|
+
* 服务落到别的模型上:照常用它,每个(偏好,实际)组合告警一次。
|
|
125
136
|
*
|
|
126
|
-
* 返回
|
|
137
|
+
* 返回 llm 的提供者条目(id 与这一家的业务句柄 use),条目固定那一个模型。
|
|
127
138
|
*/
|
|
128
139
|
async resolveLLM(platform, sessionId) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const sm = this.caps.sessionManager.current;
|
|
132
|
-
if (sm && sessionId) {
|
|
133
|
-
const resolved = sm.resolveConfig(sessionId, platform);
|
|
134
|
-
if (resolved.llm?.provider && resolved.llm?.model)
|
|
135
|
-
ref = resolved.llm;
|
|
136
|
-
}
|
|
140
|
+
const sm = this.caps.sessionManager.present ? this.caps.sessionManager.use : undefined;
|
|
141
|
+
const ref = sm && sessionId ? sm.resolveConfig(sessionId, platform).llm : undefined;
|
|
137
142
|
// 解析为具体 LLMModel entry(要求至少 chat 能力;ref 为空时走 ServicePreference / 优先级)
|
|
138
|
-
|
|
143
|
+
const entry = resolveLLMModel(this.caps.llm, ref, ['chat']);
|
|
144
|
+
if (ref || !entry)
|
|
145
|
+
return entry;
|
|
146
|
+
const own = this.caps.services.use.preferred(llmService, { for: this.instanceId });
|
|
147
|
+
const preferred = own ?? this.caps.services.use.preferred(llmService);
|
|
148
|
+
if (preferred !== undefined && entry.id !== preferred) {
|
|
149
|
+
const combo = `${preferred}\n${entry.id}`;
|
|
150
|
+
if (!this.preferenceFallbacks.has(combo)) {
|
|
151
|
+
this.preferenceFallbacks.add(combo);
|
|
152
|
+
this.logger.warn(`llm 服务的${own === undefined ? '' : '本插件'}偏好 ${preferred} 没有生效(指向的模型不在、提供者在重载或激活失败),实际用的是 ${entry.id}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return entry;
|
|
139
156
|
}
|
|
140
157
|
/** 解析失败时说清**为什么**(取数在此,措辞在 helpers 的 describeLLMFailure,纯函数便于单测)。 */
|
|
141
158
|
explainLLMFailure(platform, sessionId) {
|
|
142
|
-
const available = listLLMModels(this.caps.llm, { caps: ['chat'] }).map(e => e.
|
|
143
|
-
const sm = this.caps.sessionManager.
|
|
159
|
+
const available = listLLMModels(this.caps.llm, { caps: ['chat'] }).map(e => e.id);
|
|
160
|
+
const sm = this.caps.sessionManager.present ? this.caps.sessionManager.use : undefined;
|
|
144
161
|
const wanted = sm && sessionId ? sm.resolveConfig(sessionId, platform).llm : undefined;
|
|
145
162
|
return describeLLMFailure(available, wanted, this.erroredLLMPlugins());
|
|
146
163
|
}
|
|
147
164
|
/** 激活失败且声明提供 llm 的插件实例名——「一个模型都没有」时,病因通常就在它们身上(多为缺 apiKey)。 */
|
|
148
165
|
erroredLLMPlugins() {
|
|
149
|
-
|
|
150
|
-
if (!pm)
|
|
166
|
+
if (!this.caps.plugins.present)
|
|
151
167
|
return [];
|
|
168
|
+
const pm = this.caps.plugins.use;
|
|
152
169
|
// 只取实例名:这串会拼进**发到聊天**(含群聊)的文案里,error 原文可能带内网地址与端口等部署细节,
|
|
153
170
|
// 原因留在日志、WebUI 与 /doctor
|
|
154
171
|
return pm
|
|
@@ -160,16 +177,20 @@ class DefaultAgent {
|
|
|
160
177
|
return `${sessionId}::${source ?? 'user'}`;
|
|
161
178
|
}
|
|
162
179
|
/**
|
|
163
|
-
*
|
|
180
|
+
* 中止指定会话的在飞回合:不带 turnId 时停所有 lane,带 turnId 时只停那一轮。返回是否停下了回合。
|
|
164
181
|
*/
|
|
165
|
-
abort(sessionId) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
182
|
+
abort(sessionId, turnId) {
|
|
183
|
+
let aborted = false;
|
|
184
|
+
for (const [key, turn] of this.activeControllers) {
|
|
185
|
+
if (!key.startsWith(`${sessionId}::`) || (turnId !== undefined && turn.turnId !== turnId))
|
|
186
|
+
continue;
|
|
187
|
+
turn.controller.abort();
|
|
188
|
+
this.activeControllers.delete(key);
|
|
189
|
+
aborted = true;
|
|
171
190
|
}
|
|
172
|
-
|
|
191
|
+
if (aborted)
|
|
192
|
+
this.logger.info(`生成已中止: session=${sessionId}${turnId ? ` turn=${turnId}` : ''}`);
|
|
193
|
+
return aborted;
|
|
173
194
|
}
|
|
174
195
|
/**
|
|
175
196
|
* 中止全部在飞回合(拆卸路径用)。
|
|
@@ -181,8 +202,8 @@ class DefaultAgent {
|
|
|
181
202
|
*/
|
|
182
203
|
abortAll() {
|
|
183
204
|
let n = 0;
|
|
184
|
-
for (const [key,
|
|
185
|
-
controller.abort();
|
|
205
|
+
for (const [key, turn] of this.activeControllers) {
|
|
206
|
+
turn.controller.abort();
|
|
186
207
|
this.activeControllers.delete(key);
|
|
187
208
|
n++;
|
|
188
209
|
}
|
|
@@ -213,7 +234,7 @@ class DefaultAgent {
|
|
|
213
234
|
registerPreprocessor(name, handler) {
|
|
214
235
|
// 同名替换
|
|
215
236
|
this.preprocessors.get(name)?.dispose();
|
|
216
|
-
const dispose = this.caps.hooks.middleware('agent:input:before', async (data, next) => {
|
|
237
|
+
const dispose = this.caps.hooks.use.middleware('agent:input:before', async (data, next) => {
|
|
217
238
|
await handler(data.message, next);
|
|
218
239
|
});
|
|
219
240
|
// 退订按条目引用比对:同名重注册后,旧退订闭包只摘自己的中间件,不动新登记的账目
|
|
@@ -243,9 +264,9 @@ class DefaultAgent {
|
|
|
243
264
|
* (平台属于独立子系统,由 api-platform 的 helper 负责)。
|
|
244
265
|
*/
|
|
245
266
|
getPluginGroups() {
|
|
246
|
-
|
|
247
|
-
if (!pm)
|
|
267
|
+
if (!this.caps.plugins.present)
|
|
248
268
|
return [];
|
|
269
|
+
const pm = this.caps.plugins.use;
|
|
249
270
|
// 子系统归属:Agent 域的服务(不含 platform——平台是独立子系统)
|
|
250
271
|
const targetServices = new Set(['llm', 'memory', 'persona', 'message-archive']);
|
|
251
272
|
const grouped = [];
|
|
@@ -261,8 +282,12 @@ class DefaultAgent {
|
|
|
261
282
|
*
|
|
262
283
|
* 同时构建本轮调用的 segments 时间线(按 chunk 到达顺序记录 text / reasoning_text,
|
|
263
284
|
* 相邻同类合并)——用于上层将多轮调用 + 工具执行交错拼接成统一时间线。
|
|
285
|
+
*
|
|
286
|
+
* 工具调用的生成进度按调用的序号各配一个 callId 随进度推出,界面在参数生成期间就画出它的工具卡。generating 按序号
|
|
287
|
+
* 记下 callId 与工具名(provider 按序号组装 toolCalls,序号即调用在 toolCalls 里的位置);没报过进度的调用没有。
|
|
264
288
|
*/
|
|
265
|
-
async consumeStream(llm, request, sessionId, platform) {
|
|
289
|
+
async consumeStream(llm, request, turnId, sessionId, platform, emitProgress = true) {
|
|
290
|
+
const generating = new Map();
|
|
266
291
|
let content = '';
|
|
267
292
|
let reasoningContent = '';
|
|
268
293
|
let toolCalls;
|
|
@@ -277,7 +302,7 @@ class DefaultAgent {
|
|
|
277
302
|
segments.push({ type: kind, content: delta });
|
|
278
303
|
}
|
|
279
304
|
};
|
|
280
|
-
for await (const chunk of llm.chatStream(request)) {
|
|
305
|
+
for await (const chunk of llm.chatStream({ ...request, sessionId, purpose: this.purposeOf(sessionId) })) {
|
|
281
306
|
// 检查中止信号
|
|
282
307
|
if (request.signal?.aborted) {
|
|
283
308
|
throw new DOMException('Generation aborted', 'AbortError');
|
|
@@ -285,26 +310,34 @@ class DefaultAgent {
|
|
|
285
310
|
if (chunk.contentDelta) {
|
|
286
311
|
content += chunk.contentDelta;
|
|
287
312
|
appendDelta('text', chunk.contentDelta);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
313
|
+
if (emitProgress)
|
|
314
|
+
await this.caps.events.use.emit('outbound:stream', {
|
|
315
|
+
sessionId,
|
|
316
|
+
turnId,
|
|
317
|
+
platform,
|
|
318
|
+
contentDelta: chunk.contentDelta,
|
|
319
|
+
});
|
|
293
320
|
}
|
|
294
321
|
if (chunk.reasoningDelta) {
|
|
295
322
|
reasoningContent += chunk.reasoningDelta;
|
|
296
323
|
appendDelta('reasoning_text', chunk.reasoningDelta);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
324
|
+
if (emitProgress)
|
|
325
|
+
await this.caps.events.use.emit('outbound:stream', {
|
|
326
|
+
sessionId,
|
|
327
|
+
turnId,
|
|
328
|
+
platform,
|
|
329
|
+
reasoningDelta: chunk.reasoningDelta,
|
|
330
|
+
});
|
|
302
331
|
}
|
|
303
|
-
if (chunk.toolCallProgress) {
|
|
304
|
-
|
|
332
|
+
if (emitProgress && chunk.toolCallProgress) {
|
|
333
|
+
const { index, name } = chunk.toolCallProgress;
|
|
334
|
+
const callId = generating.get(index)?.callId ?? crypto.randomUUID();
|
|
335
|
+
generating.set(index, { callId, name });
|
|
336
|
+
await this.caps.events.use.emit('outbound:stream', {
|
|
305
337
|
sessionId,
|
|
338
|
+
turnId,
|
|
306
339
|
platform,
|
|
307
|
-
toolCallProgress: chunk.toolCallProgress,
|
|
340
|
+
toolCallProgress: { ...chunk.toolCallProgress, callId },
|
|
308
341
|
});
|
|
309
342
|
}
|
|
310
343
|
if (chunk.done) {
|
|
@@ -319,7 +352,7 @@ class DefaultAgent {
|
|
|
319
352
|
// 兜底剥离 LLM 端漏出的特殊 token 残渣(DSML 等),同时修复 GFM 表格
|
|
320
353
|
const { sanitized, hadLeak } = stripLeakedSpecialTokens(content);
|
|
321
354
|
if (hadLeak) {
|
|
322
|
-
this.caps.logger.warn(`agent: 检测到 LLM 内容残留 DSML 标记,已剥离(session=${sessionId} platform=${platform} 原长=${content.length} 净化后=${sanitized.length})`);
|
|
355
|
+
this.caps.logger.use.warn(`agent: 检测到 LLM 内容残留 DSML 标记,已剥离(session=${sessionId} platform=${platform} 原长=${content.length} 净化后=${sanitized.length})`);
|
|
323
356
|
}
|
|
324
357
|
finalContent = normalizeAssistantContent(content);
|
|
325
358
|
}
|
|
@@ -329,8 +362,16 @@ class DefaultAgent {
|
|
|
329
362
|
toolCalls,
|
|
330
363
|
usage,
|
|
331
364
|
segments,
|
|
365
|
+
generating,
|
|
332
366
|
};
|
|
333
367
|
}
|
|
368
|
+
/** 记账的用途:子会话里的回合是子任务,其余是对话 */
|
|
369
|
+
purposeOf(sessionId) {
|
|
370
|
+
return (this.caps.sessionManager.present ? this.caps.sessionManager.use.getSession(sessionId)?.kind : undefined) ===
|
|
371
|
+
'task'
|
|
372
|
+
? '子任务'
|
|
373
|
+
: '对话';
|
|
374
|
+
}
|
|
334
375
|
/**
|
|
335
376
|
* Debug 模式下格式化输出 LLM 响应详情
|
|
336
377
|
*/
|
|
@@ -439,18 +480,34 @@ class DefaultAgent {
|
|
|
439
480
|
// 仅中止同一 lane(同 session + 同 source)的旧生成;不同来源互不打断
|
|
440
481
|
const prev = this.activeControllers.get(lane);
|
|
441
482
|
if (prev)
|
|
442
|
-
prev.abort();
|
|
483
|
+
prev.controller.abort();
|
|
484
|
+
// 例外:真人消息打断延续其本人身份的宿主通知回合,本人说话就停得下她自己开的那一轮。
|
|
485
|
+
if (incoming.source === undefined && incoming.userId !== undefined) {
|
|
486
|
+
for (const notice of this.noticeCallers.values()) {
|
|
487
|
+
if (notice.sessionId === incoming.sessionId && notice.callerUserId === incoming.userId) {
|
|
488
|
+
notice.controller.abort();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
443
492
|
const controller = new AbortController();
|
|
444
|
-
|
|
493
|
+
// 回合 id:这一轮的入站钩子、流式片段、工具调用、用量与回合结束都带它,停止键也能按它只停这一轮
|
|
494
|
+
const turnId = crypto.randomUUID();
|
|
495
|
+
this.activeControllers.set(lane, { controller, turnId });
|
|
496
|
+
const callerUserId = incoming.hostNotice?.callerUserId;
|
|
497
|
+
if (callerUserId !== undefined) {
|
|
498
|
+
this.noticeCallers.set(lane, { sessionId: incoming.sessionId, callerUserId, controller });
|
|
499
|
+
}
|
|
445
500
|
const turn = (async () => {
|
|
446
501
|
try {
|
|
447
|
-
await this._handleMessageInner(incoming, controller.signal, lane);
|
|
502
|
+
await this._handleMessageInner(incoming, controller.signal, lane, turnId);
|
|
448
503
|
}
|
|
449
504
|
finally {
|
|
450
505
|
// 仅清理自己创建的 controller(避免清掉后续新请求的)
|
|
451
|
-
if (this.activeControllers.get(lane) === controller) {
|
|
506
|
+
if (this.activeControllers.get(lane)?.controller === controller) {
|
|
452
507
|
this.activeControllers.delete(lane);
|
|
453
508
|
}
|
|
509
|
+
if (this.noticeCallers.get(lane)?.controller === controller)
|
|
510
|
+
this.noticeCallers.delete(lane);
|
|
454
511
|
}
|
|
455
512
|
})();
|
|
456
513
|
this.inflightTurns.add(turn);
|
|
@@ -461,33 +518,47 @@ class DefaultAgent {
|
|
|
461
518
|
this.inflightTurns.delete(turn);
|
|
462
519
|
}
|
|
463
520
|
}
|
|
464
|
-
async _handleMessageInner(incoming, signal, lane) {
|
|
521
|
+
async _handleMessageInner(incoming, signal, lane, turnId) {
|
|
465
522
|
// Hook: agent:input:before — 插件可以修改或拦截消息
|
|
466
523
|
// 中间件不调用 next() 即可中断整个流程(包括 LLM 调用)
|
|
467
524
|
const msgHookData = {
|
|
468
525
|
message: incoming,
|
|
526
|
+
turnId,
|
|
469
527
|
metadata: {},
|
|
470
528
|
};
|
|
471
529
|
let handled = false;
|
|
472
|
-
await this.caps.hooks.run('agent:input:before', msgHookData, async () => {
|
|
530
|
+
await this.caps.hooks.use.run('agent:input:before', msgHookData, async () => {
|
|
473
531
|
handled = true;
|
|
474
532
|
// ===== defaultAction: 全部消息处理逻辑在此 =====
|
|
475
533
|
// 中间件不调用 next() → 此处永远不执行 → 消息被拦截
|
|
476
534
|
incoming = msgHookData.message;
|
|
535
|
+
const reportOnly = incoming.hostNotice?.reportOnly === true;
|
|
536
|
+
const noticeLinks = reportOnly ? (incoming.content.match(NOTICE_URL) ?? []) : [];
|
|
477
537
|
const archivedIncoming = await this.archiveIncomingMessageInOrder(lane, incoming);
|
|
478
538
|
const resolved = await this.resolveLLM(incoming.platform, incoming.sessionId);
|
|
479
539
|
if (!resolved) {
|
|
480
540
|
const why = this.explainLLMFailure(incoming.platform, incoming.sessionId);
|
|
481
541
|
this.logger.warn(`LLM 解析失败,无法处理消息:${why}`);
|
|
482
|
-
|
|
483
|
-
|
|
542
|
+
if (!reportOnly) {
|
|
543
|
+
await this.dispatchOutbound({
|
|
544
|
+
content: `[系统] ${why}`,
|
|
545
|
+
sessionId: incoming.sessionId,
|
|
546
|
+
platform: incoming.platform,
|
|
547
|
+
source: 'system',
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
await this.caps.hooks.use.run('agent:turn:after', {
|
|
551
|
+
message: incoming,
|
|
552
|
+
reply: '',
|
|
553
|
+
outcome: signal.aborted ? 'aborted' : 'error',
|
|
554
|
+
...(signal.aborted ? {} : { error: why }),
|
|
484
555
|
sessionId: incoming.sessionId,
|
|
485
|
-
|
|
486
|
-
|
|
556
|
+
turnId,
|
|
557
|
+
metadata: msgHookData.metadata,
|
|
487
558
|
});
|
|
488
559
|
return;
|
|
489
560
|
}
|
|
490
|
-
const llm = resolved.
|
|
561
|
+
const llm = resolved.use;
|
|
491
562
|
// 从 LLM model entry 读取参数。contextLength / maxOutputTokens 均为 per-model 属性,
|
|
492
563
|
// service-granularity 后不再需要 router.getContextLengthFor() 反查,也不再会出现
|
|
493
564
|
// 默认 provider 全局窗口与会话实际 model 不一致的偏差(Bug F 结构性修复)。
|
|
@@ -499,35 +570,42 @@ class DefaultAgent {
|
|
|
499
570
|
const turnPersistedTimestamps = [];
|
|
500
571
|
try {
|
|
501
572
|
// 统一解析 session 配置(一次解析,多处复用)
|
|
502
|
-
const sessionMgr = this.caps.sessionManager.
|
|
573
|
+
const sessionMgr = this.caps.sessionManager.present ? this.caps.sessionManager.use : undefined;
|
|
503
574
|
const resolved = sessionMgr && incoming.sessionId
|
|
504
575
|
? sessionMgr.resolveConfig(incoming.sessionId, incoming.platform)
|
|
505
576
|
: undefined;
|
|
506
|
-
//
|
|
507
|
-
const
|
|
508
|
-
const maxToolIterations = typeof sessionMaxIter === 'number' && Number.isInteger(sessionMaxIter) && sessionMaxIter >= 1
|
|
509
|
-
? sessionMaxIter
|
|
510
|
-
: this.maxToolIterations;
|
|
577
|
+
// 会话设置的 maxToolIterations 按登记的字段解析过(正整数),没设置时用默认值
|
|
578
|
+
const maxToolIterations = resolved?.maxToolIterations ?? MAX_TOOL_ITERATIONS;
|
|
511
579
|
// 构建 persona 会话选项(从 resolved config 中提取,传给 persona 服务)
|
|
512
580
|
const personaOpts = toPersonaOptions(resolved);
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
const
|
|
581
|
+
// 会话的深度思考档位:当前模型有这一档才带上;没设置、模型没有这一档或不能调时不带,由模型服务按它的默认档
|
|
582
|
+
const level = resolved?.think;
|
|
583
|
+
const thinking = llm.thinking?.levels.some(({ id }) => id === level) ? { think: level } : {};
|
|
584
|
+
const built = await this.buildMessages(incoming, resolved?.historyLimit, personaOpts, archivedIncoming);
|
|
585
|
+
const messages = built.messages;
|
|
586
|
+
/** 这次调用的消息列表里来自会话历史的条数:读进的,加本回合追加的工具调用与结果 */
|
|
587
|
+
let historyMessages = built.historyMessages;
|
|
517
588
|
// 通过 resolved config 获取工具分组
|
|
518
589
|
let enabledGroups;
|
|
519
590
|
if (resolved?.enabledToolGroups && resolved.enabledToolGroups.length > 0) {
|
|
520
591
|
enabledGroups = resolved.enabledToolGroups;
|
|
521
592
|
}
|
|
522
593
|
this.logger.debug(`工具分组: platform=${incoming.platform}, enabledGroups=${enabledGroups ? JSON.stringify(enabledGroups) : '(无)'}`);
|
|
523
|
-
const tools =
|
|
594
|
+
const tools = reportOnly
|
|
595
|
+
? []
|
|
596
|
+
: this.caps.tools.present
|
|
597
|
+
? this.caps.tools.use.getDefinitions(enabledGroups ? { groups: enabledGroups } : undefined)
|
|
598
|
+
: [];
|
|
524
599
|
const toolCtx = {
|
|
525
600
|
sessionId: incoming.sessionId,
|
|
526
|
-
// platform/userId
|
|
527
|
-
//
|
|
528
|
-
// scheduler/
|
|
529
|
-
// 不可再用 actor 覆盖 platform
|
|
530
|
-
|
|
601
|
+
// platform/userId 保持**会话/物理**语义(定时任务归属、平台档继承、记忆平台域都靠它们);
|
|
602
|
+
// 授权身份单独走 actor——authority 守卫按 actor 查等级,
|
|
603
|
+
// scheduler/workflow/subtask 等触发的 AI 因此以创建者等级执行而非匿名。
|
|
604
|
+
// 不可再用 actor 覆盖 platform:actor 来自另一平台时(如跨平台的定时任务、workflow)
|
|
605
|
+
// 会把上述下游全路由到发起者平台。
|
|
606
|
+
// 延续某次工具调用的宿主通知不带发言者,userId 取那次调用的(hostNotice.callerUserId),工具眼里是起它的人在调。
|
|
607
|
+
// 只用在这里,归档与提示词钩子仍按消息本身的 userId(没有)。
|
|
608
|
+
userId: incoming.userId ?? incoming.hostNotice?.callerUserId,
|
|
531
609
|
platform: incoming.platform,
|
|
532
610
|
actor: incoming.actor,
|
|
533
611
|
// 恒为数组:平台档没配分组时给 [](列举面本就等价于「只给无分组工具」),
|
|
@@ -535,8 +613,17 @@ class DefaultAgent {
|
|
|
535
613
|
enabledGroups: enabledGroups ?? [],
|
|
536
614
|
// 本循环会把 ToolExecutionResult.images 挂到 tool 消息交给主模型(出口由 prepareLLMMessages 编码)
|
|
537
615
|
acceptsImages: true,
|
|
538
|
-
//
|
|
616
|
+
// 回合中止信号:等 owner 审批期间被 latest-wins / 手动 abort 掐掉的工具不再执行
|
|
539
617
|
signal,
|
|
618
|
+
// 本回合的入站来源:只有这里填写,工具据此正向区分真人消息(source 缺省)与内部注入;
|
|
619
|
+
// 延续某次调用的宿主通知另带那次调用的发言者,执行守卫据此认出 owner 链的延续;
|
|
620
|
+
// 入站消息的平台消息 id 供转到后台的调用结束时,通知的回复引用它
|
|
621
|
+
inbound: {
|
|
622
|
+
source: incoming.source,
|
|
623
|
+
callerUserId: incoming.hostNotice?.callerUserId,
|
|
624
|
+
messageId: incoming.messageId,
|
|
625
|
+
},
|
|
626
|
+
turnId,
|
|
540
627
|
};
|
|
541
628
|
// 保存原始完整工具列表,后续迭代均以此为基础(避免被 hooks 修改后丢失)
|
|
542
629
|
const originalTools = [...tools];
|
|
@@ -548,27 +635,70 @@ class DefaultAgent {
|
|
|
548
635
|
userId: incoming.userId,
|
|
549
636
|
platform: incoming.platform,
|
|
550
637
|
triggerType: incoming.triggerType,
|
|
638
|
+
source: incoming.source,
|
|
551
639
|
};
|
|
552
640
|
// 组装先于链:贡献块(档案/技能/记忆/即时提示)先物化,拦截者审完整成品
|
|
553
641
|
await assemblePromptContributions(this.caps, llmBeforeData, {
|
|
554
642
|
buildTimeoutMs: this.promptBuildTimeoutMs,
|
|
555
643
|
signal,
|
|
556
644
|
});
|
|
557
|
-
await this.caps.hooks.run('agent:llm:before', llmBeforeData, undefined, { warnOnStall: true });
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
645
|
+
await this.caps.hooks.use.run('agent:llm:before', llmBeforeData, undefined, { warnOnStall: true });
|
|
646
|
+
if (reportOnly) {
|
|
647
|
+
llmBeforeData.tools = [];
|
|
648
|
+
// 这是结果汇报,不续跑历史工具链。保留人设与对话,去掉会诱发继续查任务的工具协议。
|
|
649
|
+
llmBeforeData.messages = llmBeforeData.messages.filter(message => message.role !== 'tool' &&
|
|
650
|
+
!message.toolCalls?.length &&
|
|
651
|
+
message.metadata?.injector !== WellKnownKinds.HostNotice);
|
|
652
|
+
// 放在钩子之后、消息末尾,确保裁剪仍保留本次结果及转述要求。
|
|
653
|
+
llmBeforeData.messages.push({
|
|
654
|
+
role: 'system',
|
|
655
|
+
content: `[宿主通知]\n${incoming.content}${incoming.hostNotice?.untrusted ? `\n\n${incoming.hostNotice.untrusted}` : ''}\n\n` +
|
|
656
|
+
'本轮只转述以上已确认的结果。沿用当前人设和回复格式,向当前会话自然地说明结果,保留通知中的链接;' +
|
|
657
|
+
'不要调用工具、重新查询状态或继续制作,不把编号、内部流程当成回复正文,也不要编造未提供的作品细节。请给出非空回复。',
|
|
658
|
+
metadata: { injector: WellKnownKinds.HostNotice },
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
/** 本回合到目前为止经过了硬裁剪:首次调用的裁剪结果写回底稿,之后各次调用都在它上面追加 */
|
|
662
|
+
let turnTrimmed = false;
|
|
663
|
+
/** 首次调用的硬裁剪从底稿里去掉的历史与工具结果:之后各次调用按底稿估算没裁剪的量时补回 */
|
|
664
|
+
let trimmedFromBase = 0;
|
|
665
|
+
/** 硬裁剪这次要发出的列表,删了消息时记一行日志;untrimmedHistory 按没经过硬裁剪的列表估算 */
|
|
666
|
+
const trimForCall = (list) => {
|
|
667
|
+
const sent = this.trimMessages(list, tokenBudget);
|
|
668
|
+
if (sent.messages.length < list.length) {
|
|
669
|
+
this.logger.info(`上下文硬裁剪: session=${incoming.sessionId}, ${list.length} → ${sent.messages.length} 条消息`);
|
|
670
|
+
}
|
|
671
|
+
turnTrimmed ||= sent.trimmed;
|
|
672
|
+
return { messages: sent.messages, untrimmedHistory: estimateHistoryTokens(list) + trimmedFromBase };
|
|
673
|
+
};
|
|
674
|
+
// 裁剪消息以确保不超过上下文窗口;裁剪结果写回,作为之后各次调用的底稿
|
|
675
|
+
const firstSent = trimForCall(llmBeforeData.messages);
|
|
676
|
+
trimmedFromBase = firstSent.untrimmedHistory - estimateHistoryTokens(firstSent.messages);
|
|
677
|
+
llmBeforeData.messages = firstSent.messages;
|
|
678
|
+
// 每次模型调用之后发一次用量:拆分按这次发出的消息与工具定义估算,used 取模型服务返回的输入 token
|
|
679
|
+
const reportUsage = (sent, sentTools, response) => this.emitTokenUsage({
|
|
680
|
+
sessionId: incoming.sessionId,
|
|
681
|
+
platform: incoming.platform,
|
|
682
|
+
turnId,
|
|
683
|
+
contextWindow: contextLength,
|
|
684
|
+
maxTokens,
|
|
685
|
+
tokenBudget,
|
|
686
|
+
messages: sent.messages,
|
|
687
|
+
tools: sentTools,
|
|
688
|
+
trimmed: turnTrimmed,
|
|
689
|
+
untrimmedHistory: sent.untrimmedHistory,
|
|
690
|
+
historyMessages,
|
|
691
|
+
promptTokens: response.usage?.promptTokens,
|
|
692
|
+
});
|
|
562
693
|
this.logger.debug(`LLM 请求: ${llmBeforeData.messages.length} 条消息, ` +
|
|
563
694
|
`${llmBeforeData.tools.length} 个工具, ` +
|
|
564
695
|
`maxTokens=${maxTokens}`);
|
|
565
696
|
const t0 = Date.now();
|
|
566
|
-
//
|
|
567
|
-
const turnStartTs = t0;
|
|
697
|
+
// 本轮累加的用量,用于最终 assistant 消息的 modelInfo 元数据。
|
|
568
698
|
// 注:缓存命中量(usage.cachedPromptTokens)不进这里——本对象只喂
|
|
569
699
|
// assistant 消息的 modelInfo,而其字段是逐个挑的(见下方 turnModelInfo),
|
|
570
700
|
// 加进来也不会流到下游。命中率的观测出口是 LLM 响应日志那一行。
|
|
571
|
-
const turnUsageAcc = { promptTokens: 0, completionTokens: 0
|
|
701
|
+
const turnUsageAcc = { promptTokens: 0, completionTokens: 0 };
|
|
572
702
|
const accUsage = (u) => {
|
|
573
703
|
if (!u)
|
|
574
704
|
return;
|
|
@@ -580,16 +710,18 @@ class DefaultAgent {
|
|
|
580
710
|
// completionTokens 语义 = "本回合真正新生成的输出"(含中间 reasoning / tool-call 生成
|
|
581
711
|
// 与最终回复文本),每次调用都是不重复的新输出,故逐次累加。
|
|
582
712
|
turnUsageAcc.completionTokens += u.completionTokens ?? 0;
|
|
583
|
-
turnUsageAcc.totalTokens = turnUsageAcc.promptTokens + turnUsageAcc.completionTokens;
|
|
584
713
|
};
|
|
585
714
|
const firstResult = await this.consumeStream(llm, {
|
|
586
715
|
messages: llmBeforeData.messages,
|
|
587
716
|
tools: llmBeforeData.tools.length > 0 ? llmBeforeData.tools : undefined,
|
|
588
717
|
maxTokens,
|
|
589
718
|
signal,
|
|
590
|
-
...
|
|
591
|
-
}, incoming.sessionId, incoming.platform);
|
|
719
|
+
...thinking,
|
|
720
|
+
}, turnId, incoming.sessionId, incoming.platform, !reportOnly);
|
|
721
|
+
await reportUsage(firstSent, llmBeforeData.tools, firstResult);
|
|
592
722
|
accUsage(firstResult.usage);
|
|
723
|
+
/** 这一次模型回复里各工具调用在生成进度里配好的 callId 与工具名,按调用的位置 */
|
|
724
|
+
let generating = firstResult.generating;
|
|
593
725
|
// 维护本轮(一次完整对话回合)的统一时间线 segments:
|
|
594
726
|
// 多次 LLM 调用 + 工具执行的输出按到达顺序拼接,保留模型原本的"思考/回答/工具/思考/回答"交错。
|
|
595
727
|
const turnSegments = [...firstResult.segments];
|
|
@@ -597,57 +729,71 @@ class DefaultAgent {
|
|
|
597
729
|
this.debugLogResponse(response, Date.now() - t0);
|
|
598
730
|
// Hook: agent:llm:after — 插件可以处理 LLM 返回结果
|
|
599
731
|
const llmAfterData = { response, messages: llmBeforeData.messages };
|
|
600
|
-
await this.caps.hooks.run('agent:llm:after', llmAfterData);
|
|
732
|
+
await this.caps.hooks.use.run('agent:llm:after', llmAfterData);
|
|
601
733
|
response = llmAfterData.response;
|
|
602
|
-
// 收集所有思考内容
|
|
603
|
-
const allReasoning = [];
|
|
604
|
-
if (response.reasoningContent) {
|
|
605
|
-
allReasoning.push(response.reasoningContent);
|
|
606
|
-
}
|
|
607
734
|
// 收集工具调用摘要
|
|
608
735
|
const toolCallSummaries = [];
|
|
609
|
-
const assistantMetadata = this.buildAssistantMetadata(incoming);
|
|
736
|
+
const assistantMetadata = this.buildAssistantMetadata(incoming, turnId);
|
|
610
737
|
// 工具调用循环
|
|
611
738
|
let iterations = 0;
|
|
612
|
-
while (response.toolCalls && response.toolCalls.length > 0 && iterations < maxToolIterations) {
|
|
739
|
+
while (!reportOnly && response.toolCalls && response.toolCalls.length > 0 && iterations < maxToolIterations) {
|
|
613
740
|
if (signal.aborted)
|
|
614
741
|
throw new DOMException('Generation aborted', 'AbortError');
|
|
615
742
|
iterations++;
|
|
616
743
|
this.logger.debug(`工具调用迭代 ${iterations}: ${response.toolCalls.map(tc => tc.function.name).join(', ')}`);
|
|
744
|
+
// 先解析各次调用的参数:解析不了的,写进本回合消息与存档的那次调用的 arguments 换成 {}(见 parseToolArguments)
|
|
745
|
+
const parsedCalls = response.toolCalls.map(parseToolArguments);
|
|
746
|
+
const toolCalls = parsedCalls.map(parsed => parsed.toolCall);
|
|
617
747
|
// 将 assistant 消息 (含 toolCalls) 加入历史
|
|
618
748
|
llmBeforeData.messages.push({
|
|
619
749
|
role: 'assistant',
|
|
620
750
|
content: response.content,
|
|
621
|
-
toolCalls
|
|
751
|
+
toolCalls,
|
|
622
752
|
reasoningContent: response.reasoningContent,
|
|
623
753
|
metadata: assistantMetadata,
|
|
624
754
|
});
|
|
625
755
|
const assistantToolMessage = {
|
|
626
756
|
role: 'assistant',
|
|
627
757
|
content: response.content,
|
|
628
|
-
toolCalls
|
|
758
|
+
toolCalls,
|
|
629
759
|
reasoningContent: response.reasoningContent,
|
|
630
760
|
metadata: assistantMetadata,
|
|
631
761
|
};
|
|
632
762
|
const toolMessages = [];
|
|
633
763
|
// 并行执行所有工具调用(互不依赖的工具无需串行等待)
|
|
634
764
|
const toolResultMaxChars = Math.floor(contextLength * this.toolResultMaxRatio * 3.5);
|
|
635
|
-
const parallelResults = await Promise.all(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
765
|
+
const parallelResults = await Promise.all(parsedCalls.map(async (parsed, position) => {
|
|
766
|
+
const { toolCall } = parsed;
|
|
767
|
+
// 这次调用的 id:开始与结束事件、工具上下文都带它。同一位置生成参数时报过同一个工具的进度,用那时配的
|
|
768
|
+
// (会话里已有它的工具卡);没报过的、或钩子改过调用列表而对不上的现在生成,免得接管画着别的工具的卡。
|
|
769
|
+
// 自己生成而不用模型给的 toolCall.id——模型给的 id 不保证唯一(有的 provider 用毫秒时间戳加序号拼,并发的
|
|
770
|
+
// 两轮会撞上),而 callId 要在全局配对开始与结束。模型的 id 只留在发回模型的 tool 消息里配对调用与结果。
|
|
771
|
+
const generated = generating.get(position);
|
|
772
|
+
const callId = generated?.name === toolCall.function.name ? generated.callId : crypto.randomUUID();
|
|
773
|
+
// 会话里的工具卡跟着这一对事件走:没执行就失败的调用(参数不合法、执行前的钩子出错)同样发一对,
|
|
774
|
+
// 结束带回给模型的错误,否则那张卡一直停在生成中
|
|
775
|
+
const announce = (phase, toolName, toolArgs, result) => this.caps.events.use.emit('tool:execute', {
|
|
776
|
+
sessionId: incoming.sessionId,
|
|
777
|
+
turnId,
|
|
778
|
+
callId,
|
|
779
|
+
platform: incoming.platform,
|
|
780
|
+
toolName,
|
|
781
|
+
args: toolArgs,
|
|
782
|
+
phase,
|
|
783
|
+
result,
|
|
784
|
+
});
|
|
785
|
+
if ('error' in parsed) {
|
|
786
|
+
// 参数不是合法 JSON 对象:跳过本次执行,把错误当工具结果回给模型重新生成 arguments。
|
|
787
|
+
// 不能按空参数执行:「无必填参数」的工具会真的空参跑一遍(副作用已发生,模型却不知情)。
|
|
788
|
+
this.logger.warn(`工具参数解析失败: ${toolCall.function.name}: ${parsed.error}`);
|
|
647
789
|
const failedAt = Date.now();
|
|
790
|
+
const result = JSON.stringify({ error: parsed.error });
|
|
791
|
+
await announce('start', toolCall.function.name, {});
|
|
792
|
+
await announce('end', toolCall.function.name, {}, result);
|
|
648
793
|
return {
|
|
649
794
|
toolCall,
|
|
650
|
-
|
|
795
|
+
callId,
|
|
796
|
+
result,
|
|
651
797
|
resultImages: undefined,
|
|
652
798
|
toolName: toolCall.function.name,
|
|
653
799
|
toolArgs: {},
|
|
@@ -655,35 +801,37 @@ class DefaultAgent {
|
|
|
655
801
|
endTime: failedAt,
|
|
656
802
|
};
|
|
657
803
|
}
|
|
804
|
+
const callCtx = { ...toolCtx, callId };
|
|
658
805
|
// Hook: agent:tool:before — 插件可以拦截或修改工具调用
|
|
659
|
-
const toolBeforeData = { name: toolCall.function.name, args, toolCallContext:
|
|
806
|
+
const toolBeforeData = { name: toolCall.function.name, args: parsed.args, toolCallContext: callCtx };
|
|
660
807
|
// 单个工具的链路异常(钩子 / 守卫 / 结果处理抛错)只算该工具失败,转成它自己的错误结果:
|
|
661
808
|
// 整批照常落定、整组落库,兄弟工具已发生的副作用不会因连坐而从历史里消失。
|
|
662
809
|
// executeDone 区分失败发生在执行前还是执行后——后者须明说「已执行」,免得模型重做有副作用的操作。
|
|
663
810
|
let executeDone = false;
|
|
664
811
|
let toolT0;
|
|
665
812
|
try {
|
|
666
|
-
await this.caps.hooks.run('agent:tool:before', toolBeforeData);
|
|
813
|
+
await this.caps.hooks.use.run('agent:tool:before', toolBeforeData);
|
|
667
814
|
// 通知平台:工具开始执行
|
|
668
|
-
await
|
|
669
|
-
|
|
670
|
-
platform: incoming.platform,
|
|
671
|
-
toolName: toolBeforeData.name,
|
|
672
|
-
args: toolBeforeData.args,
|
|
673
|
-
phase: 'start',
|
|
674
|
-
});
|
|
675
|
-
// 紧随 start 赋值:catch 据此判断 start 已发出、要补发配对的 end
|
|
815
|
+
await announce('start', toolBeforeData.name, toolBeforeData.args);
|
|
816
|
+
// 紧随 start 赋值:catch 据此判断 start 已发出
|
|
676
817
|
toolT0 = Date.now();
|
|
677
818
|
this.logger.debug(`工具执行: ${toolBeforeData.name} 参数=${formatToolArgsForLog(toolBeforeData.args)}`);
|
|
678
|
-
const executed = await (this.caps.tools.
|
|
819
|
+
const executed = await (this.caps.tools.present
|
|
820
|
+
? this.caps.tools.use.execute(toolBeforeData.name, toolBeforeData.args, callCtx)
|
|
821
|
+
: Promise.resolve({ content: JSON.stringify({ error: 'tools 服务不可用' }) }));
|
|
679
822
|
executeDone = true;
|
|
680
823
|
let result = executed.content;
|
|
681
824
|
// 工具交给主模型看的图:只随本回合的 tool 消息走(出口由 prepareLLMMessages 编码),
|
|
682
825
|
// 不进钩子/事件/时间线(那些面都是文本),也不落库(见 saveToolCallGroup)。
|
|
683
826
|
const resultImages = executed.images;
|
|
684
|
-
// Hook: agent:tool:after —
|
|
685
|
-
const toolAfterData = {
|
|
686
|
-
|
|
827
|
+
// Hook: agent:tool:after — 插件可以处理工具执行结果,写进 metadata 的随结果落库
|
|
828
|
+
const toolAfterData = {
|
|
829
|
+
name: toolBeforeData.name,
|
|
830
|
+
result,
|
|
831
|
+
toolCallContext: callCtx,
|
|
832
|
+
metadata: {},
|
|
833
|
+
};
|
|
834
|
+
await this.caps.hooks.use.run('agent:tool:after', toolAfterData);
|
|
687
835
|
result = toolAfterData.result;
|
|
688
836
|
// 工具结果截断:按上下文窗口比例限制单条工具结果长度
|
|
689
837
|
if (result.length > toolResultMaxChars) {
|
|
@@ -693,18 +841,13 @@ class DefaultAgent {
|
|
|
693
841
|
const toolEndTime = Date.now();
|
|
694
842
|
this.logger.debug(`工具完成: ${toolBeforeData.name} (${toolEndTime - toolT0}ms) 结果=${result}`);
|
|
695
843
|
// 通知平台:工具执行完成
|
|
696
|
-
await
|
|
697
|
-
sessionId: incoming.sessionId,
|
|
698
|
-
platform: incoming.platform,
|
|
699
|
-
toolName: toolBeforeData.name,
|
|
700
|
-
args: toolBeforeData.args,
|
|
701
|
-
phase: 'end',
|
|
702
|
-
result,
|
|
703
|
-
});
|
|
844
|
+
await announce('end', toolBeforeData.name, toolBeforeData.args, result);
|
|
704
845
|
return {
|
|
705
846
|
toolCall,
|
|
847
|
+
callId,
|
|
706
848
|
result,
|
|
707
849
|
resultImages,
|
|
850
|
+
resultMetadata: toolAfterData.metadata,
|
|
708
851
|
toolName: toolBeforeData.name,
|
|
709
852
|
toolArgs: toolBeforeData.args,
|
|
710
853
|
startTime: toolT0,
|
|
@@ -719,19 +862,13 @@ class DefaultAgent {
|
|
|
719
862
|
const result = JSON.stringify({
|
|
720
863
|
error: executeDone ? `工具已执行,但结果处理失败:${reason}` : `工具调用失败:${reason}`,
|
|
721
864
|
});
|
|
722
|
-
//
|
|
723
|
-
if (toolT0
|
|
724
|
-
await
|
|
725
|
-
|
|
726
|
-
platform: incoming.platform,
|
|
727
|
-
toolName: toolBeforeData.name,
|
|
728
|
-
args: toolBeforeData.args,
|
|
729
|
-
phase: 'end',
|
|
730
|
-
result,
|
|
731
|
-
});
|
|
732
|
-
}
|
|
865
|
+
// 执行前就失败的补发开始,再发配对的结束(emit 恒 resolve)
|
|
866
|
+
if (toolT0 === undefined)
|
|
867
|
+
await announce('start', toolBeforeData.name, toolBeforeData.args);
|
|
868
|
+
await announce('end', toolBeforeData.name, toolBeforeData.args, result);
|
|
733
869
|
return {
|
|
734
870
|
toolCall,
|
|
871
|
+
callId,
|
|
735
872
|
result,
|
|
736
873
|
resultImages: undefined,
|
|
737
874
|
toolName: toolBeforeData.name,
|
|
@@ -751,14 +888,20 @@ class DefaultAgent {
|
|
|
751
888
|
}
|
|
752
889
|
}
|
|
753
890
|
// 按原始 toolCalls 顺序将结果推入消息列表 + 时间线
|
|
754
|
-
for (const { toolCall, result, resultImages, toolName, toolArgs, startTime, endTime } of parallelResults) {
|
|
891
|
+
for (const { toolCall, callId, result, resultImages, resultMetadata, toolName, toolArgs, startTime, endTime, } of parallelResults) {
|
|
755
892
|
const resultPreview = result.length > 200 ? `${result.slice(0, 200)}...` : result;
|
|
756
893
|
toolCallSummaries.push(`[${toolCall.function.name}] ${resultPreview}`);
|
|
894
|
+
// callId 随工具结果与时间线落库:没回复就收尾的一轮只落工具调用组,历史里的工具卡靠它对回这次调用
|
|
757
895
|
const toolMessage = {
|
|
758
896
|
role: 'tool',
|
|
759
897
|
content: result,
|
|
760
898
|
toolCallId: toolCall.id,
|
|
761
899
|
...(resultImages && resultImages.length > 0 ? { images: resultImages } : {}),
|
|
900
|
+
metadata: {
|
|
901
|
+
...resultMetadata,
|
|
902
|
+
[WellKnownMetadataKeys.TurnId]: turnId,
|
|
903
|
+
[WellKnownMetadataKeys.CallId]: callId,
|
|
904
|
+
},
|
|
762
905
|
};
|
|
763
906
|
llmBeforeData.messages.push(toolMessage);
|
|
764
907
|
toolMessages.push(toolMessage);
|
|
@@ -770,8 +913,10 @@ class DefaultAgent {
|
|
|
770
913
|
result,
|
|
771
914
|
startTime,
|
|
772
915
|
endTime,
|
|
916
|
+
callId,
|
|
773
917
|
});
|
|
774
918
|
}
|
|
919
|
+
historyMessages += 1 + toolMessages.length;
|
|
775
920
|
await this.saveToolCallGroup(incoming.sessionId, assistantToolMessage, toolMessages, turnPersistedTimestamps);
|
|
776
921
|
// 继续请求 LLM (再次经过 hooks),使用原始完整工具列表而非被上一轮 hooks 修改过的列表
|
|
777
922
|
const nextLlmData = {
|
|
@@ -781,35 +926,34 @@ class DefaultAgent {
|
|
|
781
926
|
userId: incoming.userId,
|
|
782
927
|
platform: incoming.platform,
|
|
783
928
|
triggerType: incoming.triggerType,
|
|
929
|
+
source: incoming.source,
|
|
784
930
|
};
|
|
785
931
|
// 已物化的贡献按全局键跳过;本轮工具调用新注册的贡献(如新激活技能)增量落位
|
|
786
932
|
await assemblePromptContributions(this.caps, nextLlmData, {
|
|
787
933
|
buildTimeoutMs: this.promptBuildTimeoutMs,
|
|
788
934
|
signal,
|
|
789
935
|
});
|
|
790
|
-
await this.caps.hooks.run('agent:llm:before', nextLlmData, undefined, { warnOnStall: true });
|
|
936
|
+
await this.caps.hooks.use.run('agent:llm:before', nextLlmData, undefined, { warnOnStall: true });
|
|
791
937
|
// 裁剪消息以确保不超过上下文窗口
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
this.emitTokenUsage(incoming.sessionId, incoming.platform, nextLlmData.messages, nextLlmData.tools, contextLength, maxTokens, tokenBudget);
|
|
938
|
+
const nextSent = trimForCall(nextLlmData.messages);
|
|
939
|
+
nextLlmData.messages = nextSent.messages;
|
|
795
940
|
const tN = Date.now();
|
|
796
941
|
const nextResult = await this.consumeStream(llm, {
|
|
797
942
|
messages: nextLlmData.messages,
|
|
798
943
|
tools: nextLlmData.tools.length > 0 ? nextLlmData.tools : undefined,
|
|
799
944
|
maxTokens,
|
|
800
945
|
signal,
|
|
801
|
-
...
|
|
802
|
-
}, incoming.sessionId, incoming.platform);
|
|
946
|
+
...thinking,
|
|
947
|
+
}, turnId, incoming.sessionId, incoming.platform);
|
|
948
|
+
await reportUsage(nextSent, nextLlmData.tools, nextResult);
|
|
803
949
|
turnSegments.push(...nextResult.segments);
|
|
804
950
|
response = nextResult;
|
|
951
|
+
generating = nextResult.generating;
|
|
805
952
|
accUsage(response.usage);
|
|
806
953
|
this.debugLogResponse(response, Date.now() - tN, iterations);
|
|
807
954
|
const nextLlmAfterData = { response, messages: nextLlmData.messages };
|
|
808
|
-
await this.caps.hooks.run('agent:llm:after', nextLlmAfterData);
|
|
955
|
+
await this.caps.hooks.use.run('agent:llm:after', nextLlmAfterData);
|
|
809
956
|
response = nextLlmAfterData.response;
|
|
810
|
-
if (response.reasoningContent) {
|
|
811
|
-
allReasoning.push(response.reasoningContent);
|
|
812
|
-
}
|
|
813
957
|
}
|
|
814
958
|
// 检测是否因工具调用次数达到上限而退出循环
|
|
815
959
|
const toolLimitReached = iterations >= maxToolIterations && response.toolCalls != null && response.toolCalls.length > 0;
|
|
@@ -829,7 +973,24 @@ class DefaultAgent {
|
|
|
829
973
|
triggerType: incoming.triggerType,
|
|
830
974
|
attempt: 0,
|
|
831
975
|
};
|
|
832
|
-
|
|
976
|
+
const validateReply = async () => {
|
|
977
|
+
const attemptedTools = reportOnly && !!response.toolCalls?.length;
|
|
978
|
+
if (attemptedTools)
|
|
979
|
+
responseData.content = '';
|
|
980
|
+
await this.caps.hooks.use.run('agent:reply:before', responseData);
|
|
981
|
+
// 误调用工具或静默先给模型一次纠正机会,复用格式重试预算,不能无限尝试或执行工具。
|
|
982
|
+
const replyLinks = reportOnly ? (responseData.content.match(NOTICE_URL) ?? []) : [];
|
|
983
|
+
const missingLink = noticeLinks.some(link => !replyLinks.some(candidate => candidate === link || candidate.replace(/[.!;:]+$/u, '') === link));
|
|
984
|
+
if (reportOnly && (attemptedTools || !responseData.content.trim() || missingLink)) {
|
|
985
|
+
responseData.content = '';
|
|
986
|
+
responseData.maxRetries = Math.max(1, responseData.maxRetries ?? 0);
|
|
987
|
+
responseData.retryRequested = true;
|
|
988
|
+
responseData.retryFeedback =
|
|
989
|
+
`${responseData.retryFeedback ?? ''}\n本轮只需转述已给出的宿主结果,不要调用工具或查询状态。` +
|
|
990
|
+
'请沿用当前人设和规定的回复格式,给出包含结果与已有链接的非空回复。';
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
await validateReply();
|
|
833
994
|
// 重试循环:当 hook(如 persona 的 outputFormat 解析)报告 retryRequested 时,
|
|
834
995
|
// 把失败的 assistant 输出 + 系统反馈追加到消息列表,重新请求 LLM;最多按 maxRetries 次。
|
|
835
996
|
// maxRetries 由 hook 端写入(plugin-persona 从 outputFormat.retries 读取,默认 1)。
|
|
@@ -845,20 +1006,19 @@ class DefaultAgent {
|
|
|
845
1006
|
role: 'system',
|
|
846
1007
|
content: responseData.retryFeedback ?? '上一次回复未能通过格式校验,请严格按照系统提示中规定的格式重新输出。',
|
|
847
1008
|
});
|
|
848
|
-
const
|
|
1009
|
+
const retrySent = trimForCall(llmBeforeData.messages);
|
|
849
1010
|
const retryResult = await this.consumeStream(llm, {
|
|
850
|
-
messages:
|
|
1011
|
+
messages: retrySent.messages,
|
|
851
1012
|
tools: undefined,
|
|
852
1013
|
maxTokens,
|
|
853
1014
|
signal,
|
|
854
|
-
...
|
|
855
|
-
}, incoming.sessionId, incoming.platform);
|
|
1015
|
+
...thinking,
|
|
1016
|
+
}, turnId, incoming.sessionId, incoming.platform, !reportOnly);
|
|
1017
|
+
await reportUsage(retrySent, [], retryResult);
|
|
856
1018
|
turnSegments.push(...retryResult.segments);
|
|
857
1019
|
response = retryResult;
|
|
858
1020
|
accUsage(response.usage);
|
|
859
1021
|
rawLlmContent = response.content ?? '';
|
|
860
|
-
if (response.reasoningContent)
|
|
861
|
-
allReasoning.push(response.reasoningContent);
|
|
862
1022
|
// 用新输出再次跑 hook;hook 端根据 attempt 决定继续重试或走兜底(静默丢弃)
|
|
863
1023
|
responseData.content = rawLlmContent;
|
|
864
1024
|
responseData.archiveContent = undefined;
|
|
@@ -866,7 +1026,7 @@ class DefaultAgent {
|
|
|
866
1026
|
responseData.retryRequested = false;
|
|
867
1027
|
responseData.retryFeedback = undefined;
|
|
868
1028
|
responseData.attempt = attempt;
|
|
869
|
-
await
|
|
1029
|
+
await validateReply();
|
|
870
1030
|
}
|
|
871
1031
|
// 双保险:循环结束后若 hook 仍标记 retryRequested(理论上 persona 已在用尽时自动走兜底),
|
|
872
1032
|
// 强制把 content 置空,避免原始未校验内容被外发。
|
|
@@ -891,8 +1051,9 @@ class DefaultAgent {
|
|
|
891
1051
|
if (signal.aborted)
|
|
892
1052
|
throw new DOMException('Generation aborted', 'AbortError');
|
|
893
1053
|
// 发出流结束标记
|
|
894
|
-
await this.caps.events.emit('outbound:stream', {
|
|
1054
|
+
await this.caps.events.use.emit('outbound:stream', {
|
|
895
1055
|
sessionId: incoming.sessionId,
|
|
1056
|
+
turnId,
|
|
896
1057
|
platform: incoming.platform,
|
|
897
1058
|
done: true,
|
|
898
1059
|
toolLimitReached,
|
|
@@ -902,20 +1063,18 @@ class DefaultAgent {
|
|
|
902
1063
|
this.logger.debug(`空回复,跳过发送 (session=${incoming.sessionId})`);
|
|
903
1064
|
}
|
|
904
1065
|
else {
|
|
905
|
-
const
|
|
1066
|
+
const visibleSegments = !reportOnly && turnSegments.length > 0 ? turnSegments : undefined;
|
|
906
1067
|
// 在 assistant 最终持久化时记录 modelInfo:前端从消息历史可还原 "这条回复
|
|
907
|
-
// 由哪个 model 生成、用了多少 token
|
|
1068
|
+
// 由哪个 model 生成、用了多少 token",方便用户验证模型切换是否生效。
|
|
908
1069
|
const turnModelInfo = {
|
|
909
1070
|
provider: llm.providerId,
|
|
910
1071
|
model: llm.id,
|
|
911
1072
|
promptTokens: turnUsageAcc.promptTokens || undefined,
|
|
912
1073
|
completionTokens: turnUsageAcc.completionTokens || undefined,
|
|
913
|
-
totalTokens: turnUsageAcc.totalTokens || undefined,
|
|
914
|
-
elapsedMs: Date.now() - turnStartTs,
|
|
915
1074
|
};
|
|
916
1075
|
const finalAssistantMetadata = {
|
|
917
1076
|
...(assistantMetadata ?? {}),
|
|
918
|
-
|
|
1077
|
+
[WellKnownMetadataKeys.ModelInfo]: turnModelInfo,
|
|
919
1078
|
};
|
|
920
1079
|
// 落库的是信封(如 outputFormat 的整串 JSON)时,另带解码后的可见正文,供检索、摘要等按语义读历史的一方用
|
|
921
1080
|
if (visibleContent !== archiveContent) {
|
|
@@ -926,29 +1085,33 @@ class DefaultAgent {
|
|
|
926
1085
|
await this.saveToMemory(incoming.sessionId, {
|
|
927
1086
|
role: 'assistant',
|
|
928
1087
|
content: archiveContent,
|
|
929
|
-
reasoningContent: response.reasoningContent,
|
|
1088
|
+
reasoningContent: reportOnly ? undefined : response.reasoningContent,
|
|
930
1089
|
timestamp: Date.now(),
|
|
931
1090
|
metadata: finalAssistantMetadata,
|
|
932
|
-
segments:
|
|
1091
|
+
segments: visibleSegments,
|
|
933
1092
|
});
|
|
934
|
-
//
|
|
1093
|
+
// 外发给平台:只带正文与这条回复的模型和 token,思考与时间线只在上面落库的消息上
|
|
935
1094
|
await this.dispatchOutbound({
|
|
936
1095
|
content: replyContent,
|
|
937
1096
|
sessionId: incoming.sessionId,
|
|
938
1097
|
platform: incoming.platform,
|
|
939
|
-
|
|
1098
|
+
...(incoming.hostNotice
|
|
1099
|
+
? { hostNotice: { kind: incoming.hostNotice.kind, id: incoming.hostNotice.id } }
|
|
1100
|
+
: {}),
|
|
1101
|
+
...(incoming.hostNotice?.replyTo ? { replyTo: incoming.hostNotice.replyTo } : {}),
|
|
940
1102
|
source: 'agent',
|
|
941
|
-
|
|
1103
|
+
turnId,
|
|
942
1104
|
modelInfo: turnModelInfo,
|
|
943
1105
|
});
|
|
944
1106
|
}
|
|
945
1107
|
// Hook: agent:turn:after — 插件可以在完整消息周期结束后做后处理
|
|
946
1108
|
const turnOutcome = replyContent.trim().length === 0 ? 'silent' : 'replied';
|
|
947
|
-
await this.caps.hooks.run('agent:turn:after', {
|
|
1109
|
+
await this.caps.hooks.use.run('agent:turn:after', {
|
|
948
1110
|
message: incoming,
|
|
949
1111
|
reply: replyContent,
|
|
950
1112
|
outcome: turnOutcome,
|
|
951
1113
|
sessionId: incoming.sessionId,
|
|
1114
|
+
turnId,
|
|
952
1115
|
metadata: msgHookData.metadata,
|
|
953
1116
|
});
|
|
954
1117
|
}
|
|
@@ -970,84 +1133,94 @@ class DefaultAgent {
|
|
|
970
1133
|
(turnPersistedTimestamps.length > 0
|
|
971
1134
|
? `(保留本轮已完成的 ${turnPersistedTimestamps.length} 条工具调用记录,便于下一轮 agent 感知)`
|
|
972
1135
|
: ''));
|
|
973
|
-
await this.caps.events.emit('outbound:stream', {
|
|
1136
|
+
await this.caps.events.use.emit('outbound:stream', {
|
|
974
1137
|
sessionId: incoming.sessionId,
|
|
1138
|
+
turnId,
|
|
975
1139
|
platform: incoming.platform,
|
|
976
1140
|
done: true,
|
|
977
1141
|
});
|
|
978
1142
|
// 中止同样是回合终态:发 agent:turn:after(outcome=aborted) 让生命周期订阅方收尾——
|
|
979
|
-
//
|
|
980
|
-
// checkpoint 关闭当前回合(否则中止后回合不关闭、长期泄漏)。
|
|
1143
|
+
// 编排的 agent 节点在等这一轮的结果,不发它就要等到超时才失败。
|
|
981
1144
|
// 文档与 agent-api 早已声明 outcome 含 aborted,此处兑现契约。
|
|
982
|
-
await this.caps.hooks.run('agent:turn:after', {
|
|
1145
|
+
await this.caps.hooks.use.run('agent:turn:after', {
|
|
983
1146
|
message: incoming,
|
|
984
1147
|
reply: '',
|
|
985
1148
|
outcome: 'aborted',
|
|
986
1149
|
sessionId: incoming.sessionId,
|
|
1150
|
+
turnId,
|
|
987
1151
|
metadata: msgHookData.metadata,
|
|
988
1152
|
});
|
|
989
1153
|
return;
|
|
990
1154
|
}
|
|
991
1155
|
const message = err instanceof Error ? err.message : String(err);
|
|
992
1156
|
this.logger.error(`处理消息失败: ${message}`);
|
|
993
|
-
// 异常同样要结束流:不发 done,CLI
|
|
1157
|
+
// 异常同样要结束流:不发 done,CLI 的流式块不收尾、会话里流式输出的条目不结束,
|
|
994
1158
|
// 下一轮 delta 还会抹掉这条 [错误] 与用户的新输入。顺序与正常收尾一致——先 done 再发消息。
|
|
995
|
-
await this.caps.events.emit('outbound:stream', {
|
|
1159
|
+
await this.caps.events.use.emit('outbound:stream', {
|
|
996
1160
|
sessionId: incoming.sessionId,
|
|
1161
|
+
turnId,
|
|
997
1162
|
platform: incoming.platform,
|
|
998
1163
|
done: true,
|
|
999
1164
|
});
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
//
|
|
1009
|
-
await this.caps.hooks.run('agent:turn:after', {
|
|
1165
|
+
if (!reportOnly) {
|
|
1166
|
+
await this.dispatchOutbound({
|
|
1167
|
+
content: `[错误] ${message}`,
|
|
1168
|
+
sessionId: incoming.sessionId,
|
|
1169
|
+
platform: incoming.platform,
|
|
1170
|
+
source: 'system',
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
// 异常也是回合终态:同样发 turn:after(outcome=error),等这一轮结果的订阅方才收得了尾。
|
|
1174
|
+
await this.caps.hooks.use.run('agent:turn:after', {
|
|
1010
1175
|
message: incoming,
|
|
1011
1176
|
reply: '',
|
|
1012
1177
|
outcome: 'error',
|
|
1178
|
+
error: message,
|
|
1013
1179
|
sessionId: incoming.sessionId,
|
|
1180
|
+
turnId,
|
|
1014
1181
|
metadata: msgHookData.metadata,
|
|
1015
1182
|
});
|
|
1016
1183
|
}
|
|
1017
1184
|
});
|
|
1018
1185
|
// 消息被拦截(如流控缓冲),通知前端结束 loading
|
|
1019
1186
|
if (!handled) {
|
|
1020
|
-
await this.caps.events.emit('outbound:stream', {
|
|
1187
|
+
await this.caps.events.use.emit('outbound:stream', {
|
|
1021
1188
|
sessionId: incoming.sessionId,
|
|
1189
|
+
turnId,
|
|
1022
1190
|
platform: incoming.platform,
|
|
1023
1191
|
done: true,
|
|
1024
1192
|
});
|
|
1025
1193
|
}
|
|
1026
1194
|
}
|
|
1027
1195
|
/**
|
|
1028
|
-
* 构建发送给 LLM
|
|
1196
|
+
* 构建发送给 LLM 的消息列表。历史按会话的条数线读:historyLimit 为 0 或没设置时读全部活跃历史。
|
|
1197
|
+
* historyMessages 是从记忆读进的条数(含已归档的这一轮入站消息),与之后的清洗、裁剪无关
|
|
1029
1198
|
*/
|
|
1030
|
-
async buildMessages(incoming, personaOpts, archivedIncoming) {
|
|
1199
|
+
async buildMessages(incoming, historyLimit, personaOpts, archivedIncoming) {
|
|
1031
1200
|
const messages = [];
|
|
1201
|
+
let historyMessages = 0;
|
|
1032
1202
|
// 1. 系统提示
|
|
1033
1203
|
const systemPrompt = this.buildSystemPrompt(personaOpts);
|
|
1034
1204
|
messages.push({ role: 'system', content: systemPrompt, metadata: { injector: 'persona' } });
|
|
1035
1205
|
// 2. 历史消息
|
|
1036
|
-
const memory = this.caps.memory.
|
|
1206
|
+
const memory = this.caps.memory.present ? this.caps.memory.use : undefined;
|
|
1037
1207
|
if (memory) {
|
|
1038
1208
|
try {
|
|
1039
|
-
const
|
|
1209
|
+
const stored = await memory.getHistory(incoming.sessionId, historyLimit || Infinity);
|
|
1210
|
+
historyMessages = stored.length;
|
|
1211
|
+
const history = this.sanitizeToolCallHistory(stored, incoming.sessionId);
|
|
1040
1212
|
const now = Date.now();
|
|
1041
1213
|
for (const m of history) {
|
|
1042
1214
|
if (archivedIncoming && this.isSameMessage(m, archivedIncoming))
|
|
1043
1215
|
continue;
|
|
1044
1216
|
if (CONTROL_KINDS.includes(m.kind ?? ''))
|
|
1045
1217
|
continue;
|
|
1046
|
-
// 为用户消息注入时间标注,帮助 LLM
|
|
1218
|
+
// 为用户消息注入时间标注,帮助 LLM 理解时间先后。标注加在副本上:记忆服务可能直接交出存着的消息对象
|
|
1047
1219
|
if (m.role === 'user' && m.timestamp && m.content) {
|
|
1048
1220
|
const timeLabel = formatTimeLabel(m.timestamp, now);
|
|
1049
1221
|
if (timeLabel && !m.content.startsWith(`(${timeLabel})`)) {
|
|
1050
|
-
m
|
|
1222
|
+
messages.push({ ...m, content: `(${timeLabel}) ${m.content}` });
|
|
1223
|
+
continue;
|
|
1051
1224
|
}
|
|
1052
1225
|
}
|
|
1053
1226
|
messages.push(m);
|
|
@@ -1067,34 +1240,33 @@ class DefaultAgent {
|
|
|
1067
1240
|
const archivedBody = archivedIncoming?.content;
|
|
1068
1241
|
const fallbackBody = senderLabel ? `[${senderLabel}]: ${incoming.content}` : incoming.content;
|
|
1069
1242
|
const currentContent = `(${nowLabel}) ${archivedBody ?? fallbackBody}`;
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1074
|
-
|
|
1243
|
+
// 宿主通知分支:宿主撰写的事件通知不是任何人的发言,与 proactive 一样以 system 呈现、
|
|
1244
|
+
// 不推当前 user 消息与易变块。不可信段只在当轮接在宿主正文之后(归档只留正文),
|
|
1245
|
+
// 之后的回合从历史里只看得到宿主正文。
|
|
1246
|
+
if (incoming.hostNotice) {
|
|
1247
|
+
const { untrusted } = incoming.hostNotice;
|
|
1248
|
+
messages.push({
|
|
1249
|
+
role: 'system',
|
|
1250
|
+
content: `[宿主通知]\n${incoming.content}${untrusted ? `\n\n${untrusted}` : ''}`,
|
|
1251
|
+
metadata: { injector: WellKnownKinds.HostNotice },
|
|
1252
|
+
});
|
|
1253
|
+
return { messages, historyMessages };
|
|
1254
|
+
}
|
|
1255
|
+
// 3a. proactive 分支:系统代发给本会话的任务指令(如 workflow 的 agent 节点)
|
|
1075
1256
|
//
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1078
|
-
//
|
|
1257
|
+
// 这条消息不是用户请求,必须作为 system 指令呈现给 LLM,否则 agent 会把它当成
|
|
1258
|
+
// 真实用户在指挥(典型 BUG:agent 看到 user 角色的任务指令,回复时使用「您」
|
|
1259
|
+
// 「您好」等措辞,把派发方当成了用户)。
|
|
1079
1260
|
if (incoming.triggerType === 'proactive') {
|
|
1080
|
-
const sourceMatch = incoming.source?.match(/^proactive:from:(.+)$/);
|
|
1081
|
-
const sourceSessionId = sourceMatch?.[1];
|
|
1082
|
-
const sourceLine = sourceSessionId ? `源会话 ID: ${sourceSessionId}\n` : '';
|
|
1083
|
-
// 不在 hint 里写死 limit,让 LLM 按 plugin-tool-session 的 defaultLimit / 自身判断决定
|
|
1084
|
-
const hintLine = sourceSessionId
|
|
1085
|
-
? `如需了解源会话上下文(例如「按之前讨论的方案」之类的引用),调用 \`session_get_history(sessionId="${sourceSessionId}")\` 自行查阅(可按需附加 limit)。\n`
|
|
1086
|
-
: '';
|
|
1087
1261
|
messages.push({
|
|
1088
1262
|
role: 'system',
|
|
1089
1263
|
content: `[跨会话委派 — 非用户消息]\n` +
|
|
1090
|
-
sourceLine +
|
|
1091
1264
|
`任务: ${incoming.content}\n\n` +
|
|
1092
1265
|
`说明: 这是你(作为同一 agent 在另一会话的实例)派发给本会话的任务指令,` +
|
|
1093
|
-
`不是用户请求。处理时不要使用「您」「请问」等面向用户的措辞,按指令直接执行并简明回报结果。\n
|
|
1094
|
-
|
|
1095
|
-
metadata: { injector: WellKnownKinds.CrossSessionDelegation, sourceSessionId },
|
|
1266
|
+
`不是用户请求。处理时不要使用「您」「请问」等面向用户的措辞,按指令直接执行并简明回报结果。\n`,
|
|
1267
|
+
metadata: { injector: WellKnownKinds.CrossSessionDelegation },
|
|
1096
1268
|
});
|
|
1097
|
-
return messages;
|
|
1269
|
+
return { messages, historyMessages };
|
|
1098
1270
|
}
|
|
1099
1271
|
// 3b. 普通用户消息分支
|
|
1100
1272
|
// 检测预处理附件内容——引导 LLM 综合分析而非逐项转述
|
|
@@ -1117,7 +1289,7 @@ class DefaultAgent {
|
|
|
1117
1289
|
// 多模态:把 attachments 中的 image 项传递给 LLM(视觉模型多模态字段)
|
|
1118
1290
|
const imageAtts = incoming.attachments?.filter(a => a.kind === 'image') ?? [];
|
|
1119
1291
|
if (imageAtts.length > 0) {
|
|
1120
|
-
if (this.caps.media.
|
|
1292
|
+
if (this.caps.media.present) {
|
|
1121
1293
|
// media 在场:原样透传。出口形态由 media 的 agent:llm:before 中间件规范化
|
|
1122
1294
|
//(describe 剥离 / passthrough 物化),此处不做任何转换——它的描述缓存键与
|
|
1123
1295
|
// 动图提示都按 attachment.data 原串命中,改形态会打散。
|
|
@@ -1147,7 +1319,7 @@ class DefaultAgent {
|
|
|
1147
1319
|
// 秒级时间戳一变,人设 + 记忆 + 历史近 7 万 token 的前缀全部作废——实测每个
|
|
1148
1320
|
// 新回合命中率恒为 0,只有同回合内的工具迭代(prompt 不重建)才命中。
|
|
1149
1321
|
// 放到这里语义也更顺:它们本就是「此刻的事实」,紧挨当前消息说更自然。
|
|
1150
|
-
const persona = this.caps.persona.
|
|
1322
|
+
const persona = this.caps.persona.present ? this.caps.persona.use : undefined;
|
|
1151
1323
|
const volatileCtx = persona?.getVolatilePrompt?.(personaOpts);
|
|
1152
1324
|
if (volatileCtx) {
|
|
1153
1325
|
messages.push({ role: 'system', content: volatileCtx, metadata: { injector: VOLATILE_INJECTOR } });
|
|
@@ -1158,7 +1330,7 @@ class DefaultAgent {
|
|
|
1158
1330
|
if (focusGuidance)
|
|
1159
1331
|
messages.push(focusGuidance);
|
|
1160
1332
|
messages.push(userMessage);
|
|
1161
|
-
return messages;
|
|
1333
|
+
return { messages, historyMessages };
|
|
1162
1334
|
}
|
|
1163
1335
|
/**
|
|
1164
1336
|
* 裁剪预算 = 上下文长度 × trimThresholdRatio − 最大输出 token − 512 安全余量(下限 1024)。
|
|
@@ -1172,7 +1344,7 @@ class DefaultAgent {
|
|
|
1172
1344
|
* 构建系统提示词
|
|
1173
1345
|
*/
|
|
1174
1346
|
buildSystemPrompt(personaOpts) {
|
|
1175
|
-
const persona = this.caps.persona.
|
|
1347
|
+
const persona = this.caps.persona.present ? this.caps.persona.use : undefined;
|
|
1176
1348
|
const base = persona
|
|
1177
1349
|
? this.systemPrompt
|
|
1178
1350
|
? `${persona.getSystemPrompt(personaOpts)}\n\n${this.systemPrompt}`
|
|
@@ -1187,11 +1359,10 @@ class DefaultAgent {
|
|
|
1187
1359
|
return estimateTokens(messages);
|
|
1188
1360
|
}
|
|
1189
1361
|
/**
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
* 包含各维度的 token 使用分解,供前端展示和自动压缩判断。
|
|
1362
|
+
* 发 token:usage:各维度的拆分按消息与工具定义估算,used 取模型服务返回的输入 token,没有时取估算。
|
|
1363
|
+
* 等全部监听者处理完才返回:摘要插件在回合结束时按这一轮最后一条用量判定,不能让它晚于回合结束
|
|
1193
1364
|
*/
|
|
1194
|
-
emitTokenUsage(sessionId, platform, messages, tools,
|
|
1365
|
+
async emitTokenUsage({ sessionId, platform, turnId, contextWindow, maxTokens, tokenBudget, messages, tools, trimmed, untrimmedHistory, historyMessages, promptTokens, }) {
|
|
1195
1366
|
// 按来源分类统计各消息的 token 占用
|
|
1196
1367
|
let historyTokens = 0;
|
|
1197
1368
|
let toolResultTokens = 0;
|
|
@@ -1250,11 +1421,11 @@ class DefaultAgent {
|
|
|
1250
1421
|
personaTokens += t;
|
|
1251
1422
|
}
|
|
1252
1423
|
}
|
|
1253
|
-
else {
|
|
1424
|
+
else if (rawSource) {
|
|
1254
1425
|
// 其余注入者(memory-history/user-profile/user-relation/自定义插件等):
|
|
1255
|
-
// 计入 systemOther
|
|
1426
|
+
// 计入 systemOther 汇总之余按全局键点名,预算争抢时能看出是谁在吃;界面按键里的实例前缀合并到插件
|
|
1256
1427
|
systemOtherTokens += t;
|
|
1257
|
-
injectorTokens[
|
|
1428
|
+
injectorTokens[rawSource] = (injectorTokens[rawSource] ?? 0) + t;
|
|
1258
1429
|
}
|
|
1259
1430
|
}
|
|
1260
1431
|
else if (msg.role === 'tool') {
|
|
@@ -1273,17 +1444,23 @@ class DefaultAgent {
|
|
|
1273
1444
|
platformTokens +
|
|
1274
1445
|
subtaskTokens +
|
|
1275
1446
|
systemOtherTokens;
|
|
1276
|
-
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1279
|
-
|
|
1447
|
+
// 服务商回的数没人核过:不是正的有限数(缺省、0、NaN)时按估算,一次调用的输入不会是 0 token
|
|
1448
|
+
const used = promptTokens !== undefined && promptTokens > 0 && Number.isFinite(promptTokens)
|
|
1449
|
+
? promptTokens
|
|
1450
|
+
: systemTokens + historyTokens + toolResultTokens + toolDefsTokens;
|
|
1451
|
+
const usageRatio = contextWindow > 0 ? used / contextWindow : 0;
|
|
1452
|
+
await this.caps.events.use.emit('token:usage', {
|
|
1280
1453
|
sessionId,
|
|
1454
|
+
turnId,
|
|
1281
1455
|
platform,
|
|
1282
|
-
contextWindow
|
|
1456
|
+
contextWindow,
|
|
1283
1457
|
maxTokens,
|
|
1284
1458
|
tokenBudget,
|
|
1285
|
-
used
|
|
1459
|
+
used,
|
|
1286
1460
|
usageRatio,
|
|
1461
|
+
trimmed,
|
|
1462
|
+
untrimmedHistory,
|
|
1463
|
+
historyMessages,
|
|
1287
1464
|
breakdown: {
|
|
1288
1465
|
system: systemTokens,
|
|
1289
1466
|
persona: personaTokens,
|
|
@@ -1299,8 +1476,19 @@ class DefaultAgent {
|
|
|
1299
1476
|
toolDefs: toolDefsTokens,
|
|
1300
1477
|
reservedForReply: maxTokens,
|
|
1301
1478
|
},
|
|
1302
|
-
|
|
1303
|
-
|
|
1479
|
+
parts: [
|
|
1480
|
+
{ label: '历史消息', tokens: historyTokens },
|
|
1481
|
+
{ label: '工具结果', tokens: toolResultTokens },
|
|
1482
|
+
{ label: '工具定义', tokens: toolDefsTokens },
|
|
1483
|
+
{ label: '人设', tokens: personaTokens },
|
|
1484
|
+
{ label: '对话摘要', tokens: memorySummaryTokens },
|
|
1485
|
+
{ label: '向量记忆', tokens: memoryVectorTokens },
|
|
1486
|
+
{ label: '技能', tokens: skillsTokens },
|
|
1487
|
+
{ label: '平台提示', tokens: platformTokens },
|
|
1488
|
+
{ label: '子任务', tokens: subtaskTokens },
|
|
1489
|
+
{ label: '其余系统提示', tokens: systemOtherTokens, injectors: injectorTokens },
|
|
1490
|
+
],
|
|
1491
|
+
});
|
|
1304
1492
|
// 节流日志:与 WebUI 看到的同一份数据,让 CLI / 文件日志使用者也能看见预算消耗。
|
|
1305
1493
|
// 跨过 0.5 / 0.7 / 0.85 阈值必输出,否则每 10 轮一次。
|
|
1306
1494
|
const bucket = usageRatio >= 0.85 ? 3 : usageRatio >= 0.7 ? 2 : usageRatio >= 0.5 ? 1 : 0;
|
|
@@ -1309,7 +1497,7 @@ class DefaultAgent {
|
|
|
1309
1497
|
const crossedBucket = bucket !== st.lastRatioBucket;
|
|
1310
1498
|
if (crossedBucket || st.count % 10 === 1) {
|
|
1311
1499
|
const tag = bucket >= 3 ? 'CRITICAL' : bucket >= 2 ? 'WARN' : bucket >= 1 ? 'INFO' : 'OK';
|
|
1312
|
-
this.logger.info(`[token-usage:${tag}] ${sessionId} ${
|
|
1500
|
+
this.logger.info(`[token-usage:${tag}] ${sessionId} ${used}/${contextWindow} (${(usageRatio * 100).toFixed(1)}%) ` +
|
|
1313
1501
|
`sys=${systemTokens}(persona=${personaTokens} mem=${memorySummaryTokens + memoryVectorTokens} ` +
|
|
1314
1502
|
`skills=${skillsTokens} subtask=${subtaskTokens} other=${systemOtherTokens}) ` +
|
|
1315
1503
|
`hist=${historyTokens} tools=${toolResultTokens}+${toolDefsTokens}def reserve=${maxTokens}`);
|
|
@@ -1330,9 +1518,8 @@ class DefaultAgent {
|
|
|
1330
1518
|
const rel = t.match(/^data\/((?:images|videos|audios|files)\/.+)$/);
|
|
1331
1519
|
if (!rel)
|
|
1332
1520
|
return t;
|
|
1333
|
-
//
|
|
1334
|
-
|
|
1335
|
-
const storage = createStorageGateway(this.caps.storage);
|
|
1521
|
+
// storage 按 URI 的根名路由;缺 data 根(含 storage 整体缺席)时抛错,落到下面的 catch
|
|
1522
|
+
const storage = this.caps.storage.use;
|
|
1336
1523
|
try {
|
|
1337
1524
|
const raw = (await storage.readFile(`data:/${rel[1]}`));
|
|
1338
1525
|
// mime 优先取附件自带的 mimeType;缺省按 basename 扩展名推断(适配器落盘只产
|
|
@@ -1349,7 +1536,8 @@ class DefaultAgent {
|
|
|
1349
1536
|
}
|
|
1350
1537
|
}
|
|
1351
1538
|
/**
|
|
1352
|
-
* 裁剪消息列表,使总 token
|
|
1539
|
+
* 裁剪消息列表,使总 token 数不超过预算;trimmed 报告入口估算超过了预算、进入了硬裁剪(不一定真裁掉了内容)。
|
|
1540
|
+
* 不记日志:token:request 的快照同样走这里,删了消息的日志由真实回合的调用点记
|
|
1353
1541
|
*
|
|
1354
1542
|
* 渐进式压缩策略(保证 agent 在长工具链中无限运行):
|
|
1355
1543
|
* 1. 首条 system(主提示词)和末条消息永不删除
|
|
@@ -1363,7 +1551,7 @@ class DefaultAgent {
|
|
|
1363
1551
|
const result = messages.map(m => ({ ...m }));
|
|
1364
1552
|
let estimated = this.estimateTokens(result);
|
|
1365
1553
|
if (estimated <= budget)
|
|
1366
|
-
return result;
|
|
1554
|
+
return { messages: result, trimmed: false };
|
|
1367
1555
|
/** 重新扫描 system 消息索引(首条和末条之间) */
|
|
1368
1556
|
const findSystemIndices = () => {
|
|
1369
1557
|
const indices = [];
|
|
@@ -1391,7 +1579,7 @@ class DefaultAgent {
|
|
|
1391
1579
|
}
|
|
1392
1580
|
}
|
|
1393
1581
|
if (estimated <= budget)
|
|
1394
|
-
return result;
|
|
1582
|
+
return { messages: result, trimmed: true };
|
|
1395
1583
|
// === Phase 2: 截断过长的 tool 输出 ===
|
|
1396
1584
|
for (let i = 1; i < result.length - 1; i++) {
|
|
1397
1585
|
if (estimated <= budget)
|
|
@@ -1403,7 +1591,7 @@ class DefaultAgent {
|
|
|
1403
1591
|
}
|
|
1404
1592
|
}
|
|
1405
1593
|
if (estimated <= budget)
|
|
1406
|
-
return result;
|
|
1594
|
+
return { messages: result, trimmed: true };
|
|
1407
1595
|
// === Phase 2.5: 缩减 assistant 消息的 reasoningContent ===
|
|
1408
1596
|
// 深度思考模型的推理内容可能非常长(数万 token),优先缩减旧迭代的推理,
|
|
1409
1597
|
// 仍超预算时缩减最新一条(保留头尾摘要以保持上下文连贯性)
|
|
@@ -1429,7 +1617,7 @@ class DefaultAgent {
|
|
|
1429
1617
|
}
|
|
1430
1618
|
}
|
|
1431
1619
|
if (estimated <= budget)
|
|
1432
|
-
return result;
|
|
1620
|
+
return { messages: result, trimmed: true };
|
|
1433
1621
|
// === Phase 3: 将旧的 assistant+tool 组压缩为摘要 ===
|
|
1434
1622
|
// 识别所有工具调用组 (assistant(toolCalls) + 紧跟的 tool 消息)
|
|
1435
1623
|
{
|
|
@@ -1476,12 +1664,8 @@ class DefaultAgent {
|
|
|
1476
1664
|
}
|
|
1477
1665
|
}
|
|
1478
1666
|
}
|
|
1479
|
-
if (estimated <= budget)
|
|
1480
|
-
|
|
1481
|
-
this.logger.info(`上下文压缩: ${messages.length} → ${result.length} 条消息 (约 ${estimated} tokens)`);
|
|
1482
|
-
}
|
|
1483
|
-
return result;
|
|
1484
|
-
}
|
|
1667
|
+
if (estimated <= budget)
|
|
1668
|
+
return { messages: result, trimmed: true };
|
|
1485
1669
|
// === Phase 4: 删除最旧的非 system 消息(保护最后一组工具调用 + 最新用户消息) ===
|
|
1486
1670
|
{
|
|
1487
1671
|
// 识别最后一组工具调用的索引范围,确保不被删除
|
|
@@ -1540,10 +1724,8 @@ class DefaultAgent {
|
|
|
1540
1724
|
adjustAfterSplice(i);
|
|
1541
1725
|
}
|
|
1542
1726
|
}
|
|
1543
|
-
if (estimated <= budget)
|
|
1544
|
-
|
|
1545
|
-
return result;
|
|
1546
|
-
}
|
|
1727
|
+
if (estimated <= budget)
|
|
1728
|
+
return { messages: result, trimmed: true };
|
|
1547
1729
|
// === Phase 5: 极端情况 — 删除 hook 注入的 system 消息 ===
|
|
1548
1730
|
// 从最靠前的块开始删。turn-context 迁移后头部只剩技能正文/会话摘要、
|
|
1549
1731
|
// 档案/记忆类材料都在尾部——正序扫描意味着极端裁剪时**先牺牲头部稳定块**、
|
|
@@ -1577,15 +1759,11 @@ class DefaultAgent {
|
|
|
1577
1759
|
}
|
|
1578
1760
|
}
|
|
1579
1761
|
result.splice(insertIdx, 0, hint);
|
|
1580
|
-
estimated += estimateMsgTokens(hint);
|
|
1581
1762
|
}
|
|
1582
|
-
|
|
1583
|
-
this.logger.info(`上下文截断: ${messages.length} → ${result.length} 条消息 (约 ${estimated} tokens)`);
|
|
1584
|
-
}
|
|
1585
|
-
return result;
|
|
1763
|
+
return { messages: result, trimmed: true };
|
|
1586
1764
|
}
|
|
1587
1765
|
getArchiveForWrite() {
|
|
1588
|
-
const archive = this.caps.messageArchive.
|
|
1766
|
+
const archive = this.caps.messageArchive.present ? this.caps.messageArchive.use : undefined;
|
|
1589
1767
|
if (!archive && !this.archiveUnavailableWarned) {
|
|
1590
1768
|
this.archiveUnavailableWarned = true;
|
|
1591
1769
|
this.logger.warn('message-archive 服务不可用,缺席期间的新消息不会写入记忆,对话继续。请加载 @aalis/plugin-message-archive 及其 memory 依赖。');
|
|
@@ -1604,22 +1782,24 @@ class DefaultAgent {
|
|
|
1604
1782
|
}
|
|
1605
1783
|
}
|
|
1606
1784
|
}
|
|
1607
|
-
|
|
1785
|
+
/** 这一轮落库的 assistant 消息的元数据;带 turnId,会话的客户端据此把存档归回它所在的回合 */
|
|
1786
|
+
buildAssistantMetadata(incoming, turnId) {
|
|
1608
1787
|
const identity = getPlatformSelfIdentity(this.caps.platform, incoming.platform, incoming.sessionId);
|
|
1609
1788
|
const metadata = {
|
|
1610
|
-
|
|
1789
|
+
[WellKnownMetadataKeys.Platform]: incoming.platform,
|
|
1611
1790
|
senderType: 'assistant',
|
|
1791
|
+
[WellKnownMetadataKeys.TurnId]: turnId,
|
|
1612
1792
|
};
|
|
1613
1793
|
if (identity?.selfId)
|
|
1614
|
-
metadata.
|
|
1794
|
+
metadata[WellKnownMetadataKeys.UserId] = identity.selfId;
|
|
1615
1795
|
if (identity?.nickname)
|
|
1616
|
-
metadata.
|
|
1796
|
+
metadata[WellKnownMetadataKeys.Nickname] = identity.nickname;
|
|
1617
1797
|
if (incoming.groupId)
|
|
1618
|
-
metadata.
|
|
1798
|
+
metadata[WellKnownMetadataKeys.GroupId] = incoming.groupId;
|
|
1619
1799
|
if (incoming.groupName)
|
|
1620
|
-
metadata.
|
|
1800
|
+
metadata[WellKnownMetadataKeys.GroupName] = incoming.groupName;
|
|
1621
1801
|
if (incoming.sessionType)
|
|
1622
|
-
metadata.
|
|
1802
|
+
metadata[WellKnownMetadataKeys.SessionType] = incoming.sessionType;
|
|
1623
1803
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
1624
1804
|
}
|
|
1625
1805
|
async saveToolCallGroup(sessionId, assistantMessage, toolMessages, persistedTimestamps) {
|
|
@@ -1719,22 +1899,17 @@ class DefaultAgent {
|
|
|
1719
1899
|
* 生产部署务必确保 plugin-gateway 已加载,否则中间件链会被跳过。
|
|
1720
1900
|
*/
|
|
1721
1901
|
async dispatchOutbound(message) {
|
|
1722
|
-
const gateway = this.caps.gateway.
|
|
1902
|
+
const gateway = this.caps.gateway.present ? this.caps.gateway.use : undefined;
|
|
1723
1903
|
if (gateway) {
|
|
1724
1904
|
await gateway.dispatchOutbound(message);
|
|
1725
1905
|
return;
|
|
1726
1906
|
}
|
|
1727
1907
|
this.logger.warn('Gateway 服务不可用,回退至 outbound:message 事件(中间件链被跳过)');
|
|
1728
|
-
await this.caps.events.emit('outbound:message', message);
|
|
1908
|
+
await this.caps.events.use.emit('outbound:message', message);
|
|
1729
1909
|
}
|
|
1730
1910
|
}
|
|
1731
1911
|
// ----- 插件导出 -----
|
|
1732
1912
|
const configSchema = defineConfig({
|
|
1733
|
-
defaultLLM: {
|
|
1734
|
-
type: 'llm-ref',
|
|
1735
|
-
label: '默认对话模型',
|
|
1736
|
-
description: '全局默认 LLM。apply() 时经 services.prefer 锁定 llm 服务的偏好提供者。会话 / 平台 profile 未覆盖时生效。',
|
|
1737
|
-
},
|
|
1738
1913
|
systemPrompt: {
|
|
1739
1914
|
type: 'textarea',
|
|
1740
1915
|
label: '行为准则提示词',
|
|
@@ -1745,20 +1920,9 @@ const configSchema = defineConfig({
|
|
|
1745
1920
|
type: 'number',
|
|
1746
1921
|
label: '长期记忆预留 Token',
|
|
1747
1922
|
default: 4096,
|
|
1923
|
+
secret: false,
|
|
1748
1924
|
description: '为注入的 system 消息预留的 token 额度:超出时先按比例缩减,极端情况下(最后阶段)仍会删除',
|
|
1749
1925
|
},
|
|
1750
|
-
historyLimit: {
|
|
1751
|
-
type: 'number',
|
|
1752
|
-
label: '历史消息条数',
|
|
1753
|
-
default: 50,
|
|
1754
|
-
description: '从记忆中加载的最近对话历史条数',
|
|
1755
|
-
},
|
|
1756
|
-
maxToolIterations: {
|
|
1757
|
-
type: 'number',
|
|
1758
|
-
label: '最大工具迭代',
|
|
1759
|
-
default: 30,
|
|
1760
|
-
description: '工具调用循环的最大迭代次数',
|
|
1761
|
-
},
|
|
1762
1926
|
promptBuildTimeoutMs: {
|
|
1763
1927
|
type: 'number',
|
|
1764
1928
|
label: '提示词贡献构建超时 (ms)',
|
|
@@ -1775,66 +1939,151 @@ const configSchema = defineConfig({
|
|
|
1775
1939
|
type: 'number',
|
|
1776
1940
|
label: '裁剪触发比例',
|
|
1777
1941
|
default: 1.0,
|
|
1778
|
-
description: '裁剪预算 = 上下文长度 × 该比例 − 最大输出 token − 512 安全余量(下限 1024)。本次调用估算输入 token 超过该预算才会对消息列表做内存裁剪(不影响 DB)。默认 1.0
|
|
1942
|
+
description: '裁剪预算 = 上下文长度 × 该比例 − 最大输出 token − 512 安全余量(下限 1024)。本次调用估算输入 token 超过该预算才会对消息列表做内存裁剪(不影响 DB)。默认 1.0 表示用满扣除输出预留后的可用窗口;调低可提前裁剪。压缩触发在会话设置里调:自动压缩线(摘要插件登记)与历史条数上限(本插件登记)。',
|
|
1779
1943
|
},
|
|
1780
1944
|
});
|
|
1945
|
+
/**
|
|
1946
|
+
* 本插件登记的会话设置项:模型(输入框旁的快捷切换)、深度思考、工具迭代上限与历史条数上限,各种会话都适用。模型的全局
|
|
1947
|
+
* 默认层就是 llm 服务的偏好(会话管理按字段类型承担),界面写服务实际用的那一个
|
|
1948
|
+
*/
|
|
1949
|
+
const SESSION_SETTINGS = [
|
|
1950
|
+
{
|
|
1951
|
+
key: 'llm',
|
|
1952
|
+
field: {
|
|
1953
|
+
type: 'llm-ref',
|
|
1954
|
+
label: '模型',
|
|
1955
|
+
description: '没设置时用默认模型:给本插件设了 llm 的服务偏好时用它,否则是全局默认层的「模型」,那里也没设时用第一个可用的对话模型',
|
|
1956
|
+
},
|
|
1957
|
+
appliesTo: ['owner', 'room', 'task'],
|
|
1958
|
+
prominence: 'composer',
|
|
1959
|
+
// 会话选的是一个具体的模型:服务与模型都要给
|
|
1960
|
+
validate: value => {
|
|
1961
|
+
const ref = value;
|
|
1962
|
+
return typeof ref?.provider === 'string' &&
|
|
1963
|
+
ref.provider !== '' &&
|
|
1964
|
+
typeof ref.model === 'string' &&
|
|
1965
|
+
ref.model !== ''
|
|
1966
|
+
? undefined
|
|
1967
|
+
: '要是 { provider, model },两项都是非空字符串';
|
|
1968
|
+
},
|
|
1969
|
+
},
|
|
1970
|
+
{
|
|
1971
|
+
key: 'think',
|
|
1972
|
+
field: {
|
|
1973
|
+
type: 'select',
|
|
1974
|
+
label: '深度思考',
|
|
1975
|
+
dynamicOptions: 'llm-thinking',
|
|
1976
|
+
description: '可选的档位随所用的模型;没设置或当前模型没有这一档时按模型的默认档',
|
|
1977
|
+
},
|
|
1978
|
+
// 可选项是「模型」这一项生效的那个模型声明的档位
|
|
1979
|
+
dependsOn: 'llm',
|
|
1980
|
+
appliesTo: ['owner', 'room', 'task'],
|
|
1981
|
+
prominence: 'common',
|
|
1982
|
+
order: 20,
|
|
1983
|
+
validate: value => (typeof value === 'string' && value !== '' ? undefined : '要是档位 id(非空字符串)'),
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
key: 'maxToolIterations',
|
|
1987
|
+
field: {
|
|
1988
|
+
type: 'number',
|
|
1989
|
+
label: '最大工具迭代',
|
|
1990
|
+
integer: true,
|
|
1991
|
+
min: 1,
|
|
1992
|
+
default: MAX_TOOL_ITERATIONS,
|
|
1993
|
+
description: '一轮里工具调用循环的最大次数',
|
|
1994
|
+
},
|
|
1995
|
+
appliesTo: ['owner', 'room', 'task'],
|
|
1996
|
+
prominence: 'advanced',
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
key: 'historyLimit',
|
|
2000
|
+
field: {
|
|
2001
|
+
type: 'number',
|
|
2002
|
+
label: '历史条数上限',
|
|
2003
|
+
integer: true,
|
|
2004
|
+
min: 0,
|
|
2005
|
+
default: 0,
|
|
2006
|
+
description: '一轮最多读进这么多条会话历史;装了摘要插件时,会话的消息到这个条数就压缩。0 表示不按条数:读全部活跃历史,只按自动压缩线压缩',
|
|
2007
|
+
},
|
|
2008
|
+
appliesTo: ['owner', 'room', 'task'],
|
|
2009
|
+
prominence: 'advanced',
|
|
2010
|
+
// 子会话按自己的继承链实时解析,不冻结父会话当时的值
|
|
2011
|
+
liveOnly: true,
|
|
2012
|
+
},
|
|
2013
|
+
];
|
|
1781
2014
|
function run(caps) {
|
|
1782
|
-
const cfg = parseConfig(configSchema, caps.config, caps.logger);
|
|
1783
|
-
const
|
|
1784
|
-
|
|
2015
|
+
const cfg = parseConfig(configSchema, caps.config.use, caps.logger.use);
|
|
2016
|
+
for (const setting of SESSION_SETTINGS)
|
|
2017
|
+
caps.sessionManager.use.registerSetting(setting);
|
|
2018
|
+
const agentImpl = new DefaultAgent(caps, cfg, caps.lifecycle.use.id);
|
|
2019
|
+
caps.provide.use(agentService, agentImpl);
|
|
1785
2020
|
// 收尾段中止在飞回合并等 AbortError 落定。
|
|
1786
2021
|
// 第一方栈由 core 关停编排(optional 环成员先全部 drain)与 agent:turn:after 收口;
|
|
1787
2022
|
// session-manager 的 onDrain 兜的是未装 agent 或钩子未挂上的路径。
|
|
1788
2023
|
// 超时沿用 core 对 onDrain 的 disposeTimeoutMs,不另加配置键。
|
|
1789
|
-
caps.lifecycle.onDrain(() => agentImpl.abortInflightAndSettle(), '中止在飞回合');
|
|
2024
|
+
caps.lifecycle.use.onDrain(() => agentImpl.abortInflightAndSettle(), '中止在飞回合');
|
|
1790
2025
|
// 清理段再 abort 一次:drain 超时或未走到收尾时的兜底,表空则空操作。
|
|
1791
|
-
caps.lifecycle.onDispose(() => agentImpl.abortAll(), '中止在飞回合(兜底)');
|
|
2026
|
+
caps.lifecycle.use.onDispose(() => agentImpl.abortAll(), '中止在飞回合(兜底)');
|
|
1792
2027
|
// 会话删除:丢弃该会话的 token 日志节流状态(与 todo-list / file-reader 走同一条会话清理路径)
|
|
1793
|
-
caps.events.on('session:deleted', sessionId => agentImpl.dropTokenLogState(sessionId));
|
|
2028
|
+
caps.events.use.on('session:deleted', sessionId => agentImpl.dropTokenLogState(sessionId));
|
|
1794
2029
|
const agent = agentImpl;
|
|
1795
|
-
//
|
|
1796
|
-
//
|
|
1797
|
-
//
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2030
|
+
// ===== 会话级配置指令 =====
|
|
2031
|
+
// /session 查(模型+人设+名+来源) · /session.set 改 · /session.reset 复位 · /model 列可用模型
|
|
2032
|
+
// 与"会话"相关的模型改写统一收敛到 /session.*;/model 只保留全局的"列可用模型"发现。
|
|
2033
|
+
/** 会话设置存在会话管理里:它没在运行时三条会话指令都回这一句 */
|
|
2034
|
+
const NO_SESSION_MANAGER = '会话管理没有在运行,会话设置要靠它:在 WebUI 插件区启用「会话管理」,' +
|
|
2035
|
+
'或把它从配置文件的 disabledPlugins 里删掉;启用了还起不来就用 /doctor 看原因';
|
|
2036
|
+
/**
|
|
2037
|
+
* 继承值的来源:层表里这一层的称呼,平台档、平台类型档与场所另写这个会话取的是哪一份(「平台档 onebot」
|
|
2038
|
+
* 「平台类型档 onebot · group」「场所『<显示名>』」);哪一层都没设时为「默认」
|
|
2039
|
+
*/
|
|
2040
|
+
function sourceName(smSvc, sessionId, inheritance, layer) {
|
|
2041
|
+
if (layer === undefined)
|
|
2042
|
+
return '默认';
|
|
2043
|
+
const label = SESSION_LAYERS.find(({ name }) => name === layer).label;
|
|
2044
|
+
if (layer === 'platform')
|
|
2045
|
+
return `${label} ${inheritance.platform}`;
|
|
2046
|
+
if (layer === 'type')
|
|
2047
|
+
return `${label} ${inheritance.platform} · ${inheritance.type}`;
|
|
2048
|
+
if (layer !== 'place')
|
|
2049
|
+
return label;
|
|
2050
|
+
const { key } = smSvc.placeOf(sessionId);
|
|
2051
|
+
return `${label}『${smSvc.getPlace(key)?.name ?? key}』`;
|
|
1801
2052
|
}
|
|
1802
|
-
function resolutionLines(sessionId, platform, label, pick) {
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
const profile = pick(smSvc.getPlatformProfiles()?.[platform || 'webui']);
|
|
1811
|
-
const resolved = pick(smSvc.resolveConfig(sessionId, platform));
|
|
1812
|
-
let source = '默认';
|
|
1813
|
-
if (own)
|
|
1814
|
-
source = '会话覆盖';
|
|
1815
|
-
else if (parentDefaults)
|
|
1816
|
-
source = '父会话 sessionDefaults';
|
|
1817
|
-
else if (profile)
|
|
1818
|
-
source = `平台 profile (${platform})`;
|
|
1819
|
-
const lines = [`${label}: ${resolved ?? '(默认)'} [来源: ${source}]`];
|
|
1820
|
-
const chain = [];
|
|
1821
|
-
if (own)
|
|
1822
|
-
chain.push(`会话: ${own}`);
|
|
1823
|
-
if (parentDefaults)
|
|
1824
|
-
chain.push(`父 sessionDefaults: ${parentDefaults}`);
|
|
1825
|
-
if (profile)
|
|
1826
|
-
chain.push(`平台 profile: ${profile}`);
|
|
1827
|
-
if (chain.length > 0) {
|
|
1828
|
-
lines.push(' 解析链(高优先级在前):');
|
|
1829
|
-
for (const c of chain)
|
|
1830
|
-
lines.push(` - ${c}`);
|
|
1831
|
-
}
|
|
1832
|
-
return lines;
|
|
2053
|
+
function resolutionLines(smSvc, sessionId, platform, label, key, pick) {
|
|
2054
|
+
const inheritance = smSvc.resolveInheritance(sessionId, platform);
|
|
2055
|
+
const inherited = pick(inheritance.values);
|
|
2056
|
+
const from = sourceName(smSvc, sessionId, inheritance, inheritance.sources[key]);
|
|
2057
|
+
const own = pick(smSvc.getOwnConfig(sessionId));
|
|
2058
|
+
if (own == null)
|
|
2059
|
+
return [`${label}: ${inherited ?? '(默认)'} [来源: ${from}]`];
|
|
2060
|
+
return [`${label}: ${own} [来源: 会话覆盖]`, ` 被覆盖的继承值: ${inherited ?? '(默认)'} [来源: ${from}]`];
|
|
1833
2061
|
}
|
|
1834
2062
|
const fmtModel = (c) => (c?.llm ? `${c.llm.provider}/${c.llm.model}` : undefined);
|
|
1835
2063
|
const fmtPersona = (c) => c?.persona;
|
|
1836
|
-
|
|
1837
|
-
|
|
2064
|
+
const fmtThink = (c) => c?.think;
|
|
2065
|
+
/**
|
|
2066
|
+
* /session 模型那一行之后:这一轮实际会用的模型与显示的生效值不同时写出来(哪一层都没设、偏好也没有时,或偏好指向的
|
|
2067
|
+
* 模型不在时,服务落到别的模型上)
|
|
2068
|
+
*/
|
|
2069
|
+
async function modelNote(smSvc, sessionId, platform) {
|
|
2070
|
+
const actual = (await agent.resolveLLM(platform, sessionId))?.id;
|
|
2071
|
+
const shown = fmtModel(smSvc.getOwnConfig(sessionId)) ?? fmtModel(smSvc.resolveInheritance(sessionId, platform).values);
|
|
2072
|
+
return actual && actual !== shown ? ` 这一轮会用: ${actual}` : '';
|
|
2073
|
+
}
|
|
2074
|
+
/** 模型的档位写成一句:id(名字),由浅到深 */
|
|
2075
|
+
const levelList = (thinking) => thinking.levels.map(({ id, label }) => `${id}(${label})`).join('、');
|
|
2076
|
+
/** /session 深度思考那一行之后:这一轮会用的模型的档位与默认档,生效的档位它没有时写明按默认 */
|
|
2077
|
+
async function thinkingNote(sessionId, platform) {
|
|
2078
|
+
const thinking = (await agent.resolveLLM(platform, sessionId))?.use.thinking;
|
|
2079
|
+
if (!thinking)
|
|
2080
|
+
return ' 当前模型不能调';
|
|
2081
|
+
const level = caps.sessionManager.present
|
|
2082
|
+
? caps.sessionManager.use.resolveConfig(sessionId, platform).think
|
|
2083
|
+
: undefined;
|
|
2084
|
+
const missing = level !== undefined && !thinking.levels.some(({ id }) => id === level);
|
|
2085
|
+
return ` 当前模型的档位:${levelList(thinking)},默认 ${thinking.default}${missing ? ';当前模型没有这一档,按默认' : ''}`;
|
|
2086
|
+
}
|
|
1838
2087
|
// 分页渲染 helper:限制每页 PAGE_SIZE 条 + 关键词过滤 + 翻页提示(/model 与 /persona 共用)。
|
|
1839
2088
|
const PAGE_SIZE = 20;
|
|
1840
2089
|
function renderPaged(all, keyword, page, label, cmd) {
|
|
@@ -1852,54 +2101,56 @@ function run(caps) {
|
|
|
1852
2101
|
}
|
|
1853
2102
|
return lines.join('\n');
|
|
1854
2103
|
}
|
|
1855
|
-
caps.commands
|
|
1856
|
-
.command('model [keyword:string]', '
|
|
2104
|
+
caps.commands.use
|
|
2105
|
+
.command('model [keyword:string]', '列出/搜索可用对话模型。分页显示,-p 翻页;如 /model grok')
|
|
1857
2106
|
.option('page', '-p <n:number>', { description: '页码(默认 1)' })
|
|
1858
2107
|
.action(async (argv, keyword) => {
|
|
1859
2108
|
const seen = new Set();
|
|
1860
2109
|
const ids = [];
|
|
1861
2110
|
for (const e of listLLMModels(caps.llm, { caps: ['chat'] })) {
|
|
1862
|
-
if (seen.has(e.
|
|
2111
|
+
if (seen.has(e.id))
|
|
1863
2112
|
continue;
|
|
1864
|
-
seen.add(e.
|
|
1865
|
-
ids.push(e.
|
|
2113
|
+
seen.add(e.id);
|
|
2114
|
+
ids.push(e.id);
|
|
1866
2115
|
}
|
|
1867
2116
|
const page = typeof argv.options.page === 'number' ? argv.options.page : 1;
|
|
1868
2117
|
return renderPaged(ids, String(keyword ?? ''), page, '可用模型', '/model');
|
|
1869
2118
|
});
|
|
1870
|
-
caps.commands
|
|
2119
|
+
caps.commands.use
|
|
1871
2120
|
.command('persona [keyword:string]', '列出/搜索可用人设(分页,-p 翻页)')
|
|
1872
2121
|
.option('page', '-p <n:number>', { description: '页码(默认 1)' })
|
|
1873
2122
|
.action(async (argv, keyword) => {
|
|
1874
|
-
const persona = caps.persona.
|
|
2123
|
+
const persona = caps.persona.present ? caps.persona.use : undefined;
|
|
1875
2124
|
const names = persona?.listModels ? await persona.listModels() : [];
|
|
1876
2125
|
const page = typeof argv.options.page === 'number' ? argv.options.page : 1;
|
|
1877
2126
|
return renderPaged(names, String(keyword ?? ''), page, '可用人设', '/persona');
|
|
1878
2127
|
});
|
|
1879
|
-
// ---- 会话级「模型 + 人设 +
|
|
1880
|
-
caps.commands
|
|
1881
|
-
.command('session', '
|
|
2128
|
+
// ---- 会话级「模型 + 人设 + 深度思考 + 名称」配置(onebot 等平台对话直接改当前对话生效) ----
|
|
2129
|
+
caps.commands.use
|
|
2130
|
+
.command('session', '查看当前对话生效的设定及来源。设定指模型、人设、深度思考与名称', { risk: 'sensitive' })
|
|
1882
2131
|
.action(async (argv) => {
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2132
|
+
if (!caps.sessionManager.present)
|
|
2133
|
+
return NO_SESSION_MANAGER;
|
|
2134
|
+
const smSvc = caps.sessionManager.use;
|
|
1886
2135
|
const sid = argv.session.sessionId;
|
|
1887
2136
|
const session = smSvc.getSession(sid);
|
|
1888
2137
|
return [
|
|
1889
2138
|
`会话: ${session?.name ?? sid}`,
|
|
1890
|
-
...resolutionLines(sid, argv.session.platform, '模型', fmtModel),
|
|
1891
|
-
|
|
1892
|
-
...resolutionLines(sid, argv.session.platform, '
|
|
2139
|
+
...resolutionLines(smSvc, sid, argv.session.platform, '模型', 'llm', fmtModel),
|
|
2140
|
+
await modelNote(smSvc, sid, argv.session.platform),
|
|
2141
|
+
...resolutionLines(smSvc, sid, argv.session.platform, '人设', 'persona', fmtPersona),
|
|
2142
|
+
...resolutionLines(smSvc, sid, argv.session.platform, '深度思考', 'think', fmtThink),
|
|
2143
|
+
await thinkingNote(sid, argv.session.platform),
|
|
1893
2144
|
session ? '' : '(本对话尚无独立配置记录,全部继承默认)',
|
|
1894
2145
|
'────',
|
|
1895
|
-
'改配置: /session.set -m <模型> -p <人设> -t
|
|
1896
|
-
'复位: /session.reset (-m 仅模型 / -p 仅人设 / -t
|
|
2146
|
+
'改配置: /session.set -m <模型> -p <人设> -t <档位> [-n <名>] (/model 看可用模型/人设名错时也会列出)',
|
|
2147
|
+
'复位: /session.reset (-m 仅模型 / -p 仅人设 / -t 仅深度思考)',
|
|
1897
2148
|
]
|
|
1898
2149
|
.filter(Boolean)
|
|
1899
2150
|
.join('\n');
|
|
1900
2151
|
});
|
|
1901
|
-
caps.commands
|
|
1902
|
-
.command('session.set', '
|
|
2152
|
+
caps.commands.use
|
|
2153
|
+
.command('session.set', '设定当前对话的模型、人设、深度思考或显示名。会话级覆盖,持久化,重启不丢', {
|
|
1903
2154
|
risk: 'sensitive',
|
|
1904
2155
|
examples: [
|
|
1905
2156
|
'/session.set -m @aalis/plugin-llm-openai:main/gpt-4o -p catgirl',
|
|
@@ -1910,42 +2161,50 @@ function run(caps) {
|
|
|
1910
2161
|
})
|
|
1911
2162
|
.option('model', '-m <ref:string>', { description: '模型 provider/model(LLM entry 的 contextId;/model 可列出)' })
|
|
1912
2163
|
.option('persona', '-p <name:string>', { description: '人设卡名(不含后缀)' })
|
|
1913
|
-
.option('think', '-t <
|
|
1914
|
-
description: '
|
|
2164
|
+
.option('think', '-t <level:string>', {
|
|
2165
|
+
description: '深度思考的档位 id,按当前模型的档位(/session 列出;会话级覆盖,/session.reset 后回落默认)',
|
|
1915
2166
|
})
|
|
1916
2167
|
.option('name', '-n <label:string>', { description: '会话显示名(可选)' })
|
|
1917
2168
|
.action(async (argv) => {
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2169
|
+
if (!caps.sessionManager.present)
|
|
2170
|
+
return NO_SESSION_MANAGER;
|
|
2171
|
+
const smSvc = caps.sessionManager.use;
|
|
1921
2172
|
const modelRef = typeof argv.options.model === 'string' ? argv.options.model.trim() : '';
|
|
1922
2173
|
const personaName = typeof argv.options.persona === 'string' ? argv.options.persona.trim() : '';
|
|
1923
|
-
const
|
|
2174
|
+
const level = typeof argv.options.think === 'string' ? argv.options.think.trim() : '';
|
|
1924
2175
|
const displayName = typeof argv.options.name === 'string' ? argv.options.name.trim() : '';
|
|
1925
|
-
if (!modelRef && !personaName && !
|
|
1926
|
-
return '
|
|
1927
|
-
}
|
|
1928
|
-
if (thinkRaw && thinkRaw !== 'on' && thinkRaw !== 'off') {
|
|
1929
|
-
return `thinking 取值只能是 on 或 off(收到 "${thinkRaw}")。例:/session.set -t off`;
|
|
2176
|
+
if (!modelRef && !personaName && !level && !displayName) {
|
|
2177
|
+
return '至少给一项。敲 /session.set -h 看用法';
|
|
1930
2178
|
}
|
|
1931
2179
|
const config = {};
|
|
1932
|
-
if (thinkRaw)
|
|
1933
|
-
config.think = thinkRaw === 'on';
|
|
1934
2180
|
// 模型:校验存在再落,避免错名静默回退默认
|
|
2181
|
+
let chosen;
|
|
1935
2182
|
if (modelRef) {
|
|
1936
2183
|
const lastSlash = modelRef.lastIndexOf('/');
|
|
1937
2184
|
if (lastSlash <= 0 || lastSlash === modelRef.length - 1) {
|
|
1938
2185
|
return '模型格式错误。请用 `provider/model`,例如 `@aalis/plugin-llm-openai:main/gpt-4o`';
|
|
1939
2186
|
}
|
|
1940
2187
|
const chatModels = listLLMModels(caps.llm, { caps: ['chat'] });
|
|
1941
|
-
|
|
2188
|
+
chosen = chatModels.find(e => e.id === modelRef);
|
|
2189
|
+
if (!chosen) {
|
|
1942
2190
|
return `未知模型 "${modelRef}"。用 /model <关键词> 搜索(如 /model grok),共 ${chatModels.length} 个可用。`;
|
|
1943
2191
|
}
|
|
1944
2192
|
config.llm = { provider: modelRef.slice(0, lastSlash), model: modelRef.slice(lastSlash + 1) };
|
|
1945
2193
|
}
|
|
1946
|
-
//
|
|
2194
|
+
// 深度思考:按这一轮会用的模型(同时改了模型时按新模型)的档位校验
|
|
2195
|
+
if (level) {
|
|
2196
|
+
const thinking = (chosen ?? (await agent.resolveLLM(argv.session.platform, argv.session.sessionId)))?.use
|
|
2197
|
+
.thinking;
|
|
2198
|
+
if (!thinking)
|
|
2199
|
+
return '当前模型不能调深度思考';
|
|
2200
|
+
if (!thinking.levels.some(({ id }) => id === level)) {
|
|
2201
|
+
return `当前模型没有档位「${level}」,可选:${levelList(thinking)}`;
|
|
2202
|
+
}
|
|
2203
|
+
config.think = level;
|
|
2204
|
+
}
|
|
2205
|
+
// 人设:人设插件不在时这一项没有登记,session-manager 拒绝写入
|
|
1947
2206
|
if (personaName) {
|
|
1948
|
-
const persona = caps.persona.
|
|
2207
|
+
const persona = caps.persona.present ? caps.persona.use : undefined;
|
|
1949
2208
|
if (persona?.listModels) {
|
|
1950
2209
|
const names = await persona.listModels();
|
|
1951
2210
|
if (!names.includes(personaName)) {
|
|
@@ -1954,35 +2213,31 @@ function run(caps) {
|
|
|
1954
2213
|
}
|
|
1955
2214
|
config.persona = personaName;
|
|
1956
2215
|
}
|
|
1957
|
-
await smSvc.ensureSession(argv.session.sessionId, {
|
|
1958
|
-
config,
|
|
1959
|
-
createdBy: 'user',
|
|
1960
|
-
...(displayName ? { name: displayName } : {}),
|
|
1961
|
-
});
|
|
2216
|
+
await smSvc.ensureSession(argv.session.sessionId, { config, ...(displayName ? { name: displayName } : {}) });
|
|
1962
2217
|
const lines = ['本对话已切换 →'];
|
|
1963
2218
|
if (config.llm)
|
|
1964
2219
|
lines.push(`· 模型: ${config.llm.provider}/${config.llm.model}`);
|
|
1965
2220
|
if (config.persona)
|
|
1966
2221
|
lines.push(`· 人设: ${config.persona}`);
|
|
1967
|
-
if (config.think
|
|
1968
|
-
lines.push(`·
|
|
2222
|
+
if (config.think)
|
|
2223
|
+
lines.push(`· 深度思考: ${config.think}`);
|
|
1969
2224
|
if (displayName)
|
|
1970
2225
|
lines.push(`· 名称: ${displayName}`);
|
|
1971
2226
|
lines.push('后续消息持续生效(已持久化,重启不丢)。/session.reset 可复位。');
|
|
1972
2227
|
return lines.join('\n');
|
|
1973
2228
|
});
|
|
1974
|
-
caps.commands
|
|
1975
|
-
.command('session.reset', '
|
|
2229
|
+
caps.commands.use
|
|
2230
|
+
.command('session.reset', '复位当前对话的会话级覆盖。默认清模型、人设与深度思考,-m/-p/-t 单独清对应项', {
|
|
1976
2231
|
risk: 'sensitive',
|
|
1977
2232
|
examples: ['/session.reset', '/session.reset -m', '/session.reset -p', '/session.reset -t'],
|
|
1978
2233
|
})
|
|
1979
2234
|
.option('model', '-m', { description: '仅复位模型覆盖' })
|
|
1980
2235
|
.option('persona', '-p', { description: '仅复位人设覆盖' })
|
|
1981
|
-
.option('think', '-t', { description: '
|
|
2236
|
+
.option('think', '-t', { description: '仅复位深度思考覆盖' })
|
|
1982
2237
|
.action(async (argv) => {
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2238
|
+
if (!caps.sessionManager.present)
|
|
2239
|
+
return NO_SESSION_MANAGER;
|
|
2240
|
+
const smSvc = caps.sessionManager.use;
|
|
1986
2241
|
const mFlag = argv.options.model === true;
|
|
1987
2242
|
const pFlag = argv.options.persona === true;
|
|
1988
2243
|
const tFlag = argv.options.think === true;
|
|
@@ -1996,53 +2251,55 @@ function run(caps) {
|
|
|
1996
2251
|
const willClear = (clearModel && session?.config?.llm) ||
|
|
1997
2252
|
(clearPersona && session?.config?.persona) ||
|
|
1998
2253
|
(clearThink && session?.config?.think != null);
|
|
1999
|
-
if (
|
|
2000
|
-
//
|
|
2001
|
-
const patch = {
|
|
2254
|
+
if (willClear) {
|
|
2255
|
+
// null 删掉这一项,回到继承链
|
|
2256
|
+
const patch = {};
|
|
2002
2257
|
if (clearModel)
|
|
2003
|
-
patch.llm =
|
|
2258
|
+
patch.llm = null;
|
|
2004
2259
|
if (clearPersona)
|
|
2005
|
-
patch.persona =
|
|
2260
|
+
patch.persona = null;
|
|
2006
2261
|
if (clearThink)
|
|
2007
|
-
patch.think =
|
|
2008
|
-
await smSvc.
|
|
2262
|
+
patch.think = null;
|
|
2263
|
+
await smSvc.updateSession(sid, { config: patch });
|
|
2009
2264
|
}
|
|
2010
2265
|
const eff = smSvc.resolveConfig(sid, argv.session.platform);
|
|
2011
|
-
|
|
2012
|
-
return `已复位 → 模型: ${eff.llm ? `${eff.llm.provider}/${eff.llm.model}` : '(默认)'},人设: ${eff.persona ?? '(默认)'},thinking: ${thinkStr}`;
|
|
2266
|
+
return `已复位 → 模型: ${eff.llm ? `${eff.llm.provider}/${eff.llm.model}` : '(默认)'},人设: ${eff.persona ?? '(默认)'},深度思考: ${eff.think ?? '(默认)'}`;
|
|
2013
2267
|
});
|
|
2014
2268
|
// 监听 token:request 事件 — 客户端刷新/重连时主动请求 token 用量
|
|
2015
|
-
caps.events.on('token:request', async (data) => {
|
|
2269
|
+
caps.events.use.on('token:request', async (data) => {
|
|
2016
2270
|
if (!data?.sessionId)
|
|
2017
2271
|
return;
|
|
2018
|
-
//
|
|
2019
|
-
|
|
2272
|
+
// platform 是调用方的入口平台(owner 面会话的快照按它选档);会话服务对房间不填、其余填连接的平台。
|
|
2273
|
+
// 缺省时 IM 房间按会话记下的出生平台(子会话取根会话的)、其余按 'webui':它还是提示词贡献方的「当前平台」
|
|
2274
|
+
//(按平台取跨会话料),不能缺省成空
|
|
2275
|
+
const sm = caps.sessionManager.present ? caps.sessionManager.use : undefined;
|
|
2276
|
+
const origin = sm && rootSessionOf(sm.getSession(data.sessionId), id => sm.getSession(id))?.originPlatform;
|
|
2277
|
+
const platform = data.platform ?? origin ?? 'webui';
|
|
2020
2278
|
try {
|
|
2021
2279
|
const resolved = await agent.resolveLLM(platform, data.sessionId);
|
|
2022
2280
|
if (!resolved)
|
|
2023
2281
|
return;
|
|
2024
|
-
const llm = resolved.
|
|
2282
|
+
const llm = resolved.use;
|
|
2025
2283
|
const contextLength = llm.contextLength;
|
|
2026
2284
|
const maxTokens = getModelMaxOutput(llm);
|
|
2027
2285
|
const tokenBudget = agent.computeTokenBudget(contextLength, maxTokens);
|
|
2028
2286
|
// 与真实回合同一份会话解析:人设覆盖、结构化输出开关、额外提示都影响系统提示长度
|
|
2029
|
-
const sm = caps.sessionManager.current;
|
|
2030
2287
|
const sessionResolved = sm ? sm.resolveConfig(data.sessionId, platform) : undefined;
|
|
2031
2288
|
// 获取历史消息并构建基础消息列表
|
|
2032
|
-
const memory = caps.memory.
|
|
2289
|
+
const memory = caps.memory.present ? caps.memory.use : undefined;
|
|
2033
2290
|
const messages = [];
|
|
2034
2291
|
// 系统提示
|
|
2035
2292
|
const systemPrompt = agent.buildSystemPrompt(toPersonaOptions(sessionResolved));
|
|
2036
2293
|
messages.push({ role: 'system', content: systemPrompt, metadata: { injector: 'persona' } });
|
|
2037
|
-
//
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
messages.push(...history.filter(m => !CONTROL_KINDS.includes(m.kind ?? '')));
|
|
2041
|
-
}
|
|
2294
|
+
// 历史消息:与真实回合同样按会话的条数线读
|
|
2295
|
+
const history = memory ? await memory.getHistory(data.sessionId, sessionResolved?.historyLimit || Infinity) : [];
|
|
2296
|
+
messages.push(...history.filter(m => !CONTROL_KINDS.includes(m.kind ?? '')));
|
|
2042
2297
|
// 与真实回合同序:先组装 agent:prompt 贡献(摘要/向量记忆/档案等物化为 system 块),
|
|
2043
2298
|
// 再跑 agent:llm:before(工具搜索层过滤等拦截职责),使快照贴近实际送入 LLM 的形态
|
|
2044
2299
|
const enabledGroups = sessionResolved?.enabledToolGroups?.length ? sessionResolved.enabledToolGroups : undefined;
|
|
2045
|
-
const tools = caps.tools.
|
|
2300
|
+
const tools = caps.tools.present
|
|
2301
|
+
? caps.tools.use.getDefinitions(enabledGroups ? { groups: enabledGroups } : undefined)
|
|
2302
|
+
: [];
|
|
2046
2303
|
const llmBeforeData = {
|
|
2047
2304
|
messages,
|
|
2048
2305
|
tools,
|
|
@@ -2052,11 +2309,24 @@ function run(caps) {
|
|
|
2052
2309
|
dryRun: true, // 纯统计路径:昂贵注入者(向量检索/档案加载)据此跳过副作用
|
|
2053
2310
|
};
|
|
2054
2311
|
await assemblePromptContributions(caps, llmBeforeData, { buildTimeoutMs: agent.promptBuildTimeoutMs });
|
|
2055
|
-
await caps.hooks.run('agent:llm:before', llmBeforeData, undefined, { warnOnStall: true });
|
|
2056
|
-
|
|
2312
|
+
await caps.hooks.use.run('agent:llm:before', llmBeforeData, undefined, { warnOnStall: true });
|
|
2313
|
+
// 与真实回合同样过硬裁剪,拆分按裁剪之后的消息算
|
|
2314
|
+
const sent = agent.trimMessages(llmBeforeData.messages, tokenBudget);
|
|
2315
|
+
await agent.emitTokenUsage({
|
|
2316
|
+
sessionId: data.sessionId,
|
|
2317
|
+
platform,
|
|
2318
|
+
contextWindow: contextLength,
|
|
2319
|
+
maxTokens,
|
|
2320
|
+
tokenBudget,
|
|
2321
|
+
messages: sent.messages,
|
|
2322
|
+
tools: llmBeforeData.tools,
|
|
2323
|
+
trimmed: sent.trimmed,
|
|
2324
|
+
untrimmedHistory: estimateHistoryTokens(llmBeforeData.messages),
|
|
2325
|
+
historyMessages: history.length,
|
|
2326
|
+
});
|
|
2057
2327
|
}
|
|
2058
2328
|
catch (err) {
|
|
2059
|
-
caps.logger.debug('token:request 处理失败:', err);
|
|
2329
|
+
caps.logger.use.debug('token:request 处理失败:', err);
|
|
2060
2330
|
}
|
|
2061
2331
|
});
|
|
2062
2332
|
}
|