@aalis/plugin-agent 0.13.6 → 0.15.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 +14 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +26 -0
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts +25 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +316 -190
- package/dist/index.js.map +1 -1
- package/dist/prompt-assembly.d.ts +11 -2
- package/dist/prompt-assembly.d.ts.map +1 -1
- package/dist/prompt-assembly.js +9 -9
- package/dist/prompt-assembly.js.map +1 -1
- package/package.json +36 -18
package/dist/index.js
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { agent as agentService, } from '@aalis/api-agent';
|
|
3
|
+
import { commands as commandsService } from '@aalis/api-commands';
|
|
4
|
+
import { contributions } from '@aalis/api-contributions';
|
|
5
|
+
import { gateway as gatewayService } from '@aalis/api-gateway';
|
|
6
|
+
import { hooks } from '@aalis/api-hooks';
|
|
7
|
+
import { listLLMModels, llm as llmService, resolveLLMModel } from '@aalis/api-llm';
|
|
8
|
+
import { media as mediaService } from '@aalis/api-media';
|
|
9
|
+
import { memory as memoryService } from '@aalis/api-memory';
|
|
10
|
+
import { messageArchive as messageArchiveService } from '@aalis/api-message-archive';
|
|
11
|
+
import { persona as personaService } from '@aalis/api-persona';
|
|
12
|
+
import { getPlatformSelfIdentity, platform as platformService } from '@aalis/api-platform';
|
|
13
|
+
import { sessionManager as sessionManagerService } from '@aalis/api-session-manager';
|
|
14
|
+
import { createStorageGateway, storage as storageService } from '@aalis/api-storage';
|
|
15
|
+
import { tools as toolsService, } from '@aalis/api-tools';
|
|
16
|
+
import { config as configCap, definePlugin, events, lifecycle, logger as loggerCap, optional, pluginsService, provide, services, } from '@aalis/core';
|
|
17
|
+
import { CONTROL_KINDS, getMessageName, getSenderLabel, WellKnownKinds, WellKnownMetadataKeys, } from '@aalis/schema-message';
|
|
7
18
|
import { normalizeAssistantContent, stripLeakedSpecialTokens, truncateChars } from '@aalis/util-text-normalize';
|
|
8
|
-
import { buildFocusGuidance, describeLLMFailure, estimateMsgTokens, estimateTextTokens, estimateTokens, formatTimeLabel, INPUT_CONVENTIONS, isSameMessage, } from './helpers.js';
|
|
19
|
+
import { buildFocusGuidance, describeLLMFailure, estimateMsgTokens, estimateTextTokens, estimateTokens, formatTimeLabel, formatToolArgsForLog, INPUT_CONVENTIONS, isSameMessage, toPersonaOptions, } from './helpers.js';
|
|
9
20
|
import { assemblePromptContributions, VOLATILE_INJECTOR } from './prompt-assembly.js';
|
|
21
|
+
// ===== 能力声明 =====
|
|
22
|
+
//
|
|
23
|
+
// 对话编排要碰的东西全在这里:没写进 uses 的,插件碰不到。全部 optional——少一样能力
|
|
24
|
+
// 只是少一段能力(没有 memory 就没有历史、没有 tools 就把「工具不可用」回给模型、没有
|
|
25
|
+
// gateway 就直接走事件总线),不该让整个 agent 停活。
|
|
26
|
+
const uses = {
|
|
27
|
+
logger: loggerCap,
|
|
28
|
+
config: configCap,
|
|
29
|
+
events,
|
|
30
|
+
hooks,
|
|
31
|
+
contributions,
|
|
32
|
+
lifecycle,
|
|
33
|
+
provide,
|
|
34
|
+
services,
|
|
35
|
+
commands: optional(commandsService),
|
|
36
|
+
tools: optional(toolsService),
|
|
37
|
+
llm: optional(llmService),
|
|
38
|
+
memory: optional(memoryService),
|
|
39
|
+
persona: optional(personaService),
|
|
40
|
+
messageArchive: optional(messageArchiveService),
|
|
41
|
+
sessionManager: optional(sessionManagerService),
|
|
42
|
+
platform: optional(platformService),
|
|
43
|
+
media: optional(mediaService),
|
|
44
|
+
storage: optional(storageService),
|
|
45
|
+
gateway: optional(gatewayService),
|
|
46
|
+
plugins: optional(pluginsService),
|
|
47
|
+
};
|
|
10
48
|
/**
|
|
11
49
|
* 默认 Agent 实现 —— 对话编排器
|
|
12
50
|
*
|
|
@@ -26,7 +64,7 @@ function getModelMaxOutput(llm) {
|
|
|
26
64
|
return llm.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
27
65
|
}
|
|
28
66
|
class DefaultAgent {
|
|
29
|
-
|
|
67
|
+
caps;
|
|
30
68
|
logger;
|
|
31
69
|
systemPrompt;
|
|
32
70
|
memoryTokenBudget;
|
|
@@ -45,8 +83,15 @@ class DefaultAgent {
|
|
|
45
83
|
* 独立管理,互不打断;同来源新消息会中止旧的生成。
|
|
46
84
|
*/
|
|
47
85
|
activeControllers = new Map();
|
|
86
|
+
/**
|
|
87
|
+
* 在飞回合 Promise。关停时 abort 之后等它们以 AbortError(或正常完成)收尾,
|
|
88
|
+
* 而不是等 LLM 跑完——收尾里的钩子 / 出站事件还要用 memory。
|
|
89
|
+
*/
|
|
90
|
+
inflightTurns = new Set();
|
|
48
91
|
/** 同一 lane 的入站消息归档串行化,避免连续消息读取历史时漏掉前一条输入。 */
|
|
49
92
|
archiveQueues = new Map();
|
|
93
|
+
/** 每次激活只在实际写入缺少归档时告警一次,不因插件加载顺序误报。 */
|
|
94
|
+
archiveUnavailableWarned = false;
|
|
50
95
|
/**
|
|
51
96
|
* 节流日志状态:记录每个 session 上次 token:usage 日志的"轮次"与 ratio 桶。
|
|
52
97
|
* - 跨过 0.5/0.7/0.85 三个阈值必输出
|
|
@@ -54,11 +99,12 @@ class DefaultAgent {
|
|
|
54
99
|
* 与 token:usage 事件共用同一份 breakdown 数据,零额外计算。
|
|
55
100
|
*/
|
|
56
101
|
tokenLogState = new Map();
|
|
57
|
-
/** 已注册的预处理器(name → {
|
|
102
|
+
/** 已注册的预处理器(name → { dispose }) */
|
|
58
103
|
preprocessors = new Map();
|
|
59
|
-
constructor(
|
|
60
|
-
this.
|
|
61
|
-
this.logger =
|
|
104
|
+
constructor(caps) {
|
|
105
|
+
this.caps = caps;
|
|
106
|
+
this.logger = caps.logger.child('agent');
|
|
107
|
+
const config = caps.config;
|
|
62
108
|
this.systemPrompt = config.systemPrompt || '';
|
|
63
109
|
this.memoryTokenBudget = config.memoryTokenBudget ?? 4096;
|
|
64
110
|
this.historyLimit = config.historyLimit ?? 50;
|
|
@@ -82,25 +128,25 @@ class DefaultAgent {
|
|
|
82
128
|
async resolveLLM(platform, sessionId) {
|
|
83
129
|
let ref;
|
|
84
130
|
// session-manager 一步到位:会话 > 父 sessionDefaults > platform profile
|
|
85
|
-
const sm = this.
|
|
131
|
+
const sm = this.caps.sessionManager.current;
|
|
86
132
|
if (sm && sessionId) {
|
|
87
133
|
const resolved = sm.resolveConfig(sessionId, platform);
|
|
88
134
|
if (resolved.llm?.provider && resolved.llm?.model)
|
|
89
135
|
ref = resolved.llm;
|
|
90
136
|
}
|
|
91
137
|
// 解析为具体 LLMModel entry(要求至少 chat 能力;ref 为空时走 ServicePreference / 优先级)
|
|
92
|
-
return resolveLLMModel(this.
|
|
138
|
+
return resolveLLMModel(this.caps.llm, ref, ['chat']);
|
|
93
139
|
}
|
|
94
140
|
/** 解析失败时说清**为什么**(取数在此,措辞在 helpers 的 describeLLMFailure,纯函数便于单测)。 */
|
|
95
141
|
explainLLMFailure(platform, sessionId) {
|
|
96
|
-
const available = listLLMModels(this.
|
|
97
|
-
const sm = this.
|
|
142
|
+
const available = listLLMModels(this.caps.llm, { caps: ['chat'] }).map(e => e.contextId);
|
|
143
|
+
const sm = this.caps.sessionManager.current;
|
|
98
144
|
const wanted = sm && sessionId ? sm.resolveConfig(sessionId, platform).llm : undefined;
|
|
99
145
|
return describeLLMFailure(available, wanted, this.erroredLLMPlugins());
|
|
100
146
|
}
|
|
101
147
|
/** 激活失败且声明提供 llm 的插件——「一个模型都没有」时,病因通常就在它们身上(多为缺 apiKey)。 */
|
|
102
148
|
erroredLLMPlugins() {
|
|
103
|
-
const pm = this.
|
|
149
|
+
const pm = this.caps.plugins.current;
|
|
104
150
|
if (!pm)
|
|
105
151
|
return [];
|
|
106
152
|
// 这串会拼进**发到聊天**的文案里,原因文本来自插件自抛的 error,长度不可控:
|
|
@@ -129,10 +175,10 @@ class DefaultAgent {
|
|
|
129
175
|
/**
|
|
130
176
|
* 中止全部在飞回合(拆卸路径用)。
|
|
131
177
|
*
|
|
132
|
-
* activeControllers 是实例私有的,bounce
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
178
|
+
* activeControllers 是实例私有的,bounce 后新实例看不见旧实例的在飞回合。
|
|
179
|
+
* abort 走已有的 AbortError 收尾(outbound:stream done + turn:after outcome='aborted'),
|
|
180
|
+
* 与 WebUI「停止生成」同语义。关停在 onDrain 里先调本方法再等回合 Promise;
|
|
181
|
+
* onDispose 再调一次作兜底(已中止的 controller 不在表里,是空操作)。
|
|
136
182
|
*/
|
|
137
183
|
abortAll() {
|
|
138
184
|
let n = 0;
|
|
@@ -144,6 +190,20 @@ class DefaultAgent {
|
|
|
144
190
|
if (n > 0)
|
|
145
191
|
this.logger.info(`拆卸:已中止 ${n} 个在飞回合`);
|
|
146
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* 关停收尾:中止在飞回合并等待其 AbortError(或正常完成)收尾落定。
|
|
195
|
+
* 超时由 core 对 onDrain 回调的 disposeTimeoutMs 护栏负责,不另加配置键。
|
|
196
|
+
*/
|
|
197
|
+
async abortInflightAndSettle() {
|
|
198
|
+
this.abortAll();
|
|
199
|
+
if (this.inflightTurns.size === 0)
|
|
200
|
+
return;
|
|
201
|
+
await Promise.allSettled([...this.inflightTurns]);
|
|
202
|
+
}
|
|
203
|
+
/** 会话删除时丢弃其节流日志状态(订阅见 run() 的 session:deleted)。 */
|
|
204
|
+
dropTokenLogState(sessionId) {
|
|
205
|
+
this.tokenLogState.delete(sessionId);
|
|
206
|
+
}
|
|
147
207
|
/**
|
|
148
208
|
* 注册输入预处理器(如图片识别、文件读取、用户画像等)。
|
|
149
209
|
* 多个预处理器按注册顺序串行执行(Koa-style 洋葱模型)。
|
|
@@ -154,7 +214,7 @@ class DefaultAgent {
|
|
|
154
214
|
registerPreprocessor(name, handler) {
|
|
155
215
|
// 同名替换
|
|
156
216
|
this.preprocessors.get(name)?.dispose();
|
|
157
|
-
const dispose = this.
|
|
217
|
+
const dispose = this.caps.hooks.middleware('agent:input:before', async (data, next) => {
|
|
158
218
|
await handler(data.message, next);
|
|
159
219
|
});
|
|
160
220
|
// 退订按条目引用比对:同名重注册后,旧退订闭包只摘自己的中间件,不动新登记的账目
|
|
@@ -184,7 +244,7 @@ class DefaultAgent {
|
|
|
184
244
|
* (平台属于独立子系统,由 api-platform 的 helper 负责)。
|
|
185
245
|
*/
|
|
186
246
|
getPluginGroups() {
|
|
187
|
-
const pm = this.
|
|
247
|
+
const pm = this.caps.plugins.current;
|
|
188
248
|
if (!pm)
|
|
189
249
|
return [];
|
|
190
250
|
// 子系统归属:Agent 域的服务(不含 platform——平台是独立子系统)
|
|
@@ -203,7 +263,7 @@ class DefaultAgent {
|
|
|
203
263
|
* 同时构建本轮调用的 segments 时间线(按 chunk 到达顺序记录 text / reasoning_text,
|
|
204
264
|
* 相邻同类合并)——用于上层将多轮调用 + 工具执行交错拼接成统一时间线。
|
|
205
265
|
*/
|
|
206
|
-
async consumeStream(llm, request, sessionId, platform
|
|
266
|
+
async consumeStream(llm, request, sessionId, platform) {
|
|
207
267
|
let content = '';
|
|
208
268
|
let reasoningContent = '';
|
|
209
269
|
let toolCalls;
|
|
@@ -220,13 +280,13 @@ class DefaultAgent {
|
|
|
220
280
|
};
|
|
221
281
|
for await (const chunk of llm.chatStream(request)) {
|
|
222
282
|
// 检查中止信号
|
|
223
|
-
if (signal?.aborted) {
|
|
283
|
+
if (request.signal?.aborted) {
|
|
224
284
|
throw new DOMException('Generation aborted', 'AbortError');
|
|
225
285
|
}
|
|
226
286
|
if (chunk.contentDelta) {
|
|
227
287
|
content += chunk.contentDelta;
|
|
228
288
|
appendDelta('text', chunk.contentDelta);
|
|
229
|
-
await this.
|
|
289
|
+
await this.caps.events.emit('outbound:stream', {
|
|
230
290
|
sessionId,
|
|
231
291
|
platform,
|
|
232
292
|
contentDelta: chunk.contentDelta,
|
|
@@ -235,14 +295,14 @@ class DefaultAgent {
|
|
|
235
295
|
if (chunk.reasoningDelta) {
|
|
236
296
|
reasoningContent += chunk.reasoningDelta;
|
|
237
297
|
appendDelta('reasoning_text', chunk.reasoningDelta);
|
|
238
|
-
await this.
|
|
298
|
+
await this.caps.events.emit('outbound:stream', {
|
|
239
299
|
sessionId,
|
|
240
300
|
platform,
|
|
241
301
|
reasoningDelta: chunk.reasoningDelta,
|
|
242
302
|
});
|
|
243
303
|
}
|
|
244
304
|
if (chunk.toolCallProgress) {
|
|
245
|
-
await this.
|
|
305
|
+
await this.caps.events.emit('outbound:stream', {
|
|
246
306
|
sessionId,
|
|
247
307
|
platform,
|
|
248
308
|
toolCallProgress: chunk.toolCallProgress,
|
|
@@ -260,7 +320,7 @@ class DefaultAgent {
|
|
|
260
320
|
// 兜底剥离 LLM 端漏出的特殊 token 残渣(DSML 等),同时修复 GFM 表格
|
|
261
321
|
const { sanitized, hadLeak } = stripLeakedSpecialTokens(content);
|
|
262
322
|
if (hadLeak) {
|
|
263
|
-
this.
|
|
323
|
+
this.caps.logger.warn(`agent: 检测到 LLM 内容残留 DSML 标记,已剥离(session=${sessionId} platform=${platform} 原长=${content.length} 净化后=${sanitized.length})`);
|
|
264
324
|
}
|
|
265
325
|
finalContent = normalizeAssistantContent(content);
|
|
266
326
|
}
|
|
@@ -383,14 +443,23 @@ class DefaultAgent {
|
|
|
383
443
|
prev.abort();
|
|
384
444
|
const controller = new AbortController();
|
|
385
445
|
this.activeControllers.set(lane, controller);
|
|
446
|
+
const turn = (async () => {
|
|
447
|
+
try {
|
|
448
|
+
await this._handleMessageInner(incoming, controller.signal, lane);
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
// 仅清理自己创建的 controller(避免清掉后续新请求的)
|
|
452
|
+
if (this.activeControllers.get(lane) === controller) {
|
|
453
|
+
this.activeControllers.delete(lane);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
})();
|
|
457
|
+
this.inflightTurns.add(turn);
|
|
386
458
|
try {
|
|
387
|
-
await
|
|
459
|
+
await turn;
|
|
388
460
|
}
|
|
389
461
|
finally {
|
|
390
|
-
|
|
391
|
-
if (this.activeControllers.get(lane) === controller) {
|
|
392
|
-
this.activeControllers.delete(lane);
|
|
393
|
-
}
|
|
462
|
+
this.inflightTurns.delete(turn);
|
|
394
463
|
}
|
|
395
464
|
}
|
|
396
465
|
async _handleMessageInner(incoming, signal, lane) {
|
|
@@ -401,7 +470,7 @@ class DefaultAgent {
|
|
|
401
470
|
metadata: {},
|
|
402
471
|
};
|
|
403
472
|
let handled = false;
|
|
404
|
-
await this.
|
|
473
|
+
await this.caps.hooks.run('agent:input:before', msgHookData, async () => {
|
|
405
474
|
handled = true;
|
|
406
475
|
// ===== defaultAction: 全部消息处理逻辑在此 =====
|
|
407
476
|
// 中间件不调用 next() → 此处永远不执行 → 消息被拦截
|
|
@@ -425,16 +494,13 @@ class DefaultAgent {
|
|
|
425
494
|
// 默认 provider 全局窗口与会话实际 model 不一致的偏差(Bug F 结构性修复)。
|
|
426
495
|
const maxTokens = getModelMaxOutput(llm);
|
|
427
496
|
const contextLength = llm.contextLength;
|
|
428
|
-
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
// Bug B 防回溯:本回合中通过工具循环写入到 memory 的 (assistant+toolCalls + tool 结果) 消息时间戳。
|
|
432
|
-
// 用户中途点「停止生成」时,这些条目要从历史中删除——否则下一条消息发出后,agent 会再次
|
|
433
|
-
// 看到上一轮被中断的工具调用 + 半截 assistant 文本,导致「假回退」。
|
|
497
|
+
const tokenBudget = this.computeTokenBudget(contextLength, maxTokens);
|
|
498
|
+
// 本回合经工具循环写入 memory 的 (assistant+toolCalls + tool 结果) 消息时间戳。
|
|
499
|
+
// 只用于中止日志里的条数:中止时这些已完成的工具调用记录保留不回滚(见下方 catch 分支)。
|
|
434
500
|
const turnPersistedTimestamps = [];
|
|
435
501
|
try {
|
|
436
502
|
// 统一解析 session 配置(一次解析,多处复用)
|
|
437
|
-
const sessionMgr = this.
|
|
503
|
+
const sessionMgr = this.caps.sessionManager.current;
|
|
438
504
|
const resolved = sessionMgr && incoming.sessionId
|
|
439
505
|
? sessionMgr.resolveConfig(incoming.sessionId, incoming.platform)
|
|
440
506
|
: undefined;
|
|
@@ -444,13 +510,7 @@ class DefaultAgent {
|
|
|
444
510
|
? sessionMaxIter
|
|
445
511
|
: this.maxToolIterations;
|
|
446
512
|
// 构建 persona 会话选项(从 resolved config 中提取,传给 persona 服务)
|
|
447
|
-
const personaOpts = resolved
|
|
448
|
-
? {
|
|
449
|
-
persona: resolved.persona,
|
|
450
|
-
disableOutputFormat: resolved.disableOutputFormat,
|
|
451
|
-
clientSideJsonRendering: resolved.clientSideJsonRendering,
|
|
452
|
-
}
|
|
453
|
-
: undefined;
|
|
513
|
+
const personaOpts = toPersonaOptions(resolved);
|
|
454
514
|
// 会话级 thinking 覆盖(/session.set -t on|off):未设置则不带 think 字段,
|
|
455
515
|
// 由各 provider 按自己的全局配置决定(ollama thinking / deepseek thinkingMode)。
|
|
456
516
|
const thinkOverride = resolved?.think;
|
|
@@ -461,9 +521,7 @@ class DefaultAgent {
|
|
|
461
521
|
enabledGroups = resolved.enabledToolGroups;
|
|
462
522
|
}
|
|
463
523
|
this.logger.debug(`工具分组: platform=${incoming.platform}, enabledGroups=${enabledGroups ? JSON.stringify(enabledGroups) : '(无)'}`);
|
|
464
|
-
const tools = this.
|
|
465
|
-
.getService('tools')
|
|
466
|
-
?.getDefinitions(enabledGroups ? { groups: enabledGroups } : undefined) ?? [];
|
|
524
|
+
const tools = this.caps.tools.current?.getDefinitions(enabledGroups ? { groups: enabledGroups } : undefined) ?? [];
|
|
467
525
|
const toolCtx = {
|
|
468
526
|
sessionId: incoming.sessionId,
|
|
469
527
|
// platform/userId 保持**会话/物理**语义(定时任务归属、平台档继承、记忆平台域、
|
|
@@ -493,11 +551,11 @@ class DefaultAgent {
|
|
|
493
551
|
triggerType: incoming.triggerType,
|
|
494
552
|
};
|
|
495
553
|
// 组装先于链:贡献块(档案/技能/记忆/即时提示)先物化,拦截者审完整成品
|
|
496
|
-
await assemblePromptContributions(this.
|
|
554
|
+
await assemblePromptContributions(this.caps, llmBeforeData, {
|
|
497
555
|
buildTimeoutMs: this.promptBuildTimeoutMs,
|
|
498
556
|
signal,
|
|
499
557
|
});
|
|
500
|
-
await this.
|
|
558
|
+
await this.caps.hooks.run('agent:llm:before', llmBeforeData, undefined, { warnOnStall: true });
|
|
501
559
|
// 裁剪消息以确保不超过上下文窗口
|
|
502
560
|
llmBeforeData.messages = this.trimMessages(llmBeforeData.messages, tokenBudget);
|
|
503
561
|
// 推送 token 使用量统计
|
|
@@ -509,7 +567,7 @@ class DefaultAgent {
|
|
|
509
567
|
// 本轮初始时间与累加用量,用于最终 assistant 消息的 modelInfo 元数据。
|
|
510
568
|
const turnStartTs = t0;
|
|
511
569
|
// 注:缓存命中量(usage.cachedPromptTokens)不进这里——本对象只喂
|
|
512
|
-
// assistant 消息的 modelInfo,而其字段是逐个挑的(见下方
|
|
570
|
+
// assistant 消息的 modelInfo,而其字段是逐个挑的(见下方 turnModelInfo),
|
|
513
571
|
// 加进来也不会流到下游。命中率的观测出口是 LLM 响应日志那一行。
|
|
514
572
|
const turnUsageAcc = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
515
573
|
const accUsage = (u) => {
|
|
@@ -531,7 +589,7 @@ class DefaultAgent {
|
|
|
531
589
|
maxTokens,
|
|
532
590
|
signal,
|
|
533
591
|
...(thinkOverride !== undefined ? { think: thinkOverride } : {}),
|
|
534
|
-
}, incoming.sessionId, incoming.platform
|
|
592
|
+
}, incoming.sessionId, incoming.platform);
|
|
535
593
|
accUsage(firstResult.usage);
|
|
536
594
|
// 维护本轮(一次完整对话回合)的统一时间线 segments:
|
|
537
595
|
// 多次 LLM 调用 + 工具执行的输出按到达顺序拼接,保留模型原本的"思考/回答/工具/思考/回答"交错。
|
|
@@ -540,7 +598,7 @@ class DefaultAgent {
|
|
|
540
598
|
this.debugLogResponse(response, Date.now() - t0);
|
|
541
599
|
// Hook: agent:llm:after — 插件可以处理 LLM 返回结果
|
|
542
600
|
const llmAfterData = { response, messages: llmBeforeData.messages };
|
|
543
|
-
await this.
|
|
601
|
+
await this.caps.hooks.run('agent:llm:after', llmAfterData);
|
|
544
602
|
response = llmAfterData.response;
|
|
545
603
|
// 收集所有思考内容
|
|
546
604
|
const allReasoning = [];
|
|
@@ -600,54 +658,89 @@ class DefaultAgent {
|
|
|
600
658
|
}
|
|
601
659
|
// Hook: agent:tool:before — 插件可以拦截或修改工具调用
|
|
602
660
|
const toolBeforeData = { name: toolCall.function.name, args, toolCallContext: toolCtx };
|
|
603
|
-
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
661
|
+
// 单个工具的链路异常(钩子 / 守卫 / 结果处理抛错)只算该工具失败,转成它自己的错误结果:
|
|
662
|
+
// 整批照常落定、整组落库,兄弟工具已发生的副作用不会因连坐而从历史里消失。
|
|
663
|
+
// executeDone 区分失败发生在执行前还是执行后——后者须明说「已执行」,免得模型重做有副作用的操作。
|
|
664
|
+
let executeDone = false;
|
|
665
|
+
let toolT0;
|
|
666
|
+
try {
|
|
667
|
+
await this.caps.hooks.run('agent:tool:before', toolBeforeData);
|
|
668
|
+
// 通知平台:工具开始执行
|
|
669
|
+
await this.caps.events.emit('tool:execute', {
|
|
670
|
+
sessionId: incoming.sessionId,
|
|
671
|
+
platform: incoming.platform,
|
|
672
|
+
toolName: toolBeforeData.name,
|
|
673
|
+
args: toolBeforeData.args,
|
|
674
|
+
phase: 'start',
|
|
675
|
+
});
|
|
676
|
+
// 紧随 start 赋值:catch 据此判断 start 已发出、要补发配对的 end
|
|
677
|
+
toolT0 = Date.now();
|
|
678
|
+
this.logger.debug(`工具执行: ${toolBeforeData.name} 参数=${formatToolArgsForLog(toolBeforeData.args)}`);
|
|
679
|
+
const executed = await (this.caps.tools.current?.execute(toolBeforeData.name, toolBeforeData.args, toolCtx) ?? Promise.resolve({ content: JSON.stringify({ error: 'tools 服务不可用' }) }));
|
|
680
|
+
executeDone = true;
|
|
681
|
+
let result = executed.content;
|
|
682
|
+
// 工具交给主模型看的图:只随本回合的 tool 消息走(出口由 prepareLLMMessages 编码),
|
|
683
|
+
// 不进钩子/事件/时间线(那些面都是文本),也不落库(见 saveToolCallGroup)。
|
|
684
|
+
const resultImages = executed.images;
|
|
685
|
+
// Hook: agent:tool:after — 插件可以处理工具执行结果
|
|
686
|
+
const toolAfterData = { name: toolBeforeData.name, result, toolCallContext: toolCtx };
|
|
687
|
+
await this.caps.hooks.run('agent:tool:after', toolAfterData);
|
|
688
|
+
result = toolAfterData.result;
|
|
689
|
+
// 工具结果截断:按上下文窗口比例限制单条工具结果长度
|
|
690
|
+
if (result.length > toolResultMaxChars) {
|
|
691
|
+
this.logger.info(`工具结果过长 (${result.length} 字符),截断至 ${toolResultMaxChars} 字符: ${toolBeforeData.name}`);
|
|
692
|
+
result = truncateChars(result, toolResultMaxChars, `\n... [工具输出已截断,原始长度 ${result.length} 字符]`);
|
|
693
|
+
}
|
|
694
|
+
const toolEndTime = Date.now();
|
|
695
|
+
this.logger.debug(`工具完成: ${toolBeforeData.name} (${toolEndTime - toolT0}ms) 结果=${result}`);
|
|
696
|
+
// 通知平台:工具执行完成
|
|
697
|
+
await this.caps.events.emit('tool:execute', {
|
|
698
|
+
sessionId: incoming.sessionId,
|
|
699
|
+
platform: incoming.platform,
|
|
700
|
+
toolName: toolBeforeData.name,
|
|
701
|
+
args: toolBeforeData.args,
|
|
702
|
+
phase: 'end',
|
|
703
|
+
result,
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
toolCall,
|
|
707
|
+
result,
|
|
708
|
+
resultImages,
|
|
709
|
+
toolName: toolBeforeData.name,
|
|
710
|
+
toolArgs: toolBeforeData.args,
|
|
711
|
+
startTime: toolT0,
|
|
712
|
+
endTime: toolEndTime,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
// 执行后的失败不回退到原始结果(fail-closed):after 钩子可能是脱敏钩子,失败时原始输出不能漏给模型
|
|
717
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
718
|
+
this.logger.warn(`工具${executeDone ? '已执行,但结果处理' : '调用'}失败: ${toolBeforeData.name} (${reason})`);
|
|
719
|
+
const failedAt = Date.now();
|
|
720
|
+
const result = JSON.stringify({
|
|
721
|
+
error: executeDone ? `工具已执行,但结果处理失败:${reason}` : `工具调用失败:${reason}`,
|
|
722
|
+
});
|
|
723
|
+
// start 已发出就补发配对的 end(emit 恒 resolve):否则 WebUI 流式缓冲里这一段一直显示执行中
|
|
724
|
+
if (toolT0 !== undefined) {
|
|
725
|
+
await this.caps.events.emit('tool:execute', {
|
|
726
|
+
sessionId: incoming.sessionId,
|
|
727
|
+
platform: incoming.platform,
|
|
728
|
+
toolName: toolBeforeData.name,
|
|
729
|
+
args: toolBeforeData.args,
|
|
730
|
+
phase: 'end',
|
|
731
|
+
result,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
toolCall,
|
|
736
|
+
result,
|
|
737
|
+
resultImages: undefined,
|
|
738
|
+
toolName: toolBeforeData.name,
|
|
739
|
+
toolArgs: toolBeforeData.args,
|
|
740
|
+
startTime: toolT0 ?? failedAt,
|
|
741
|
+
endTime: failedAt,
|
|
742
|
+
};
|
|
630
743
|
}
|
|
631
|
-
const toolEndTime = Date.now();
|
|
632
|
-
this.logger.debug(`工具完成: ${toolBeforeData.name} (${toolEndTime - toolT0}ms) 结果=${result}`);
|
|
633
|
-
// 通知平台:工具执行完成
|
|
634
|
-
await this.ctx.emit('tool:execute', {
|
|
635
|
-
sessionId: incoming.sessionId,
|
|
636
|
-
platform: incoming.platform,
|
|
637
|
-
toolName: toolBeforeData.name,
|
|
638
|
-
args: toolBeforeData.args,
|
|
639
|
-
phase: 'end',
|
|
640
|
-
result,
|
|
641
|
-
});
|
|
642
|
-
return {
|
|
643
|
-
toolCall,
|
|
644
|
-
result,
|
|
645
|
-
resultImages,
|
|
646
|
-
toolName: toolBeforeData.name,
|
|
647
|
-
toolArgs: toolBeforeData.args,
|
|
648
|
-
startTime: toolT0,
|
|
649
|
-
endTime: toolEndTime,
|
|
650
|
-
};
|
|
651
744
|
}));
|
|
652
745
|
// 工具交出的图只服务紧接着的这一次请求:上一轮工具结果里的图在这里剥掉,否则多轮
|
|
653
746
|
// 工具迭代会让图片在上下文里累积(每张数百 token,且不受文本裁剪约束)。
|
|
@@ -691,11 +784,11 @@ class DefaultAgent {
|
|
|
691
784
|
triggerType: incoming.triggerType,
|
|
692
785
|
};
|
|
693
786
|
// 已物化的贡献按全局键跳过;本轮工具调用新注册的贡献(如新激活技能)增量落位
|
|
694
|
-
await assemblePromptContributions(this.
|
|
787
|
+
await assemblePromptContributions(this.caps, nextLlmData, {
|
|
695
788
|
buildTimeoutMs: this.promptBuildTimeoutMs,
|
|
696
789
|
signal,
|
|
697
790
|
});
|
|
698
|
-
await this.
|
|
791
|
+
await this.caps.hooks.run('agent:llm:before', nextLlmData, undefined, { warnOnStall: true });
|
|
699
792
|
// 裁剪消息以确保不超过上下文窗口
|
|
700
793
|
nextLlmData.messages = this.trimMessages(nextLlmData.messages, tokenBudget);
|
|
701
794
|
// 推送 token 使用量统计
|
|
@@ -707,13 +800,13 @@ class DefaultAgent {
|
|
|
707
800
|
maxTokens,
|
|
708
801
|
signal,
|
|
709
802
|
...(thinkOverride !== undefined ? { think: thinkOverride } : {}),
|
|
710
|
-
}, incoming.sessionId, incoming.platform
|
|
803
|
+
}, incoming.sessionId, incoming.platform);
|
|
711
804
|
turnSegments.push(...nextResult.segments);
|
|
712
805
|
response = nextResult;
|
|
713
806
|
accUsage(response.usage);
|
|
714
807
|
this.debugLogResponse(response, Date.now() - tN, iterations);
|
|
715
808
|
const nextLlmAfterData = { response, messages: nextLlmData.messages };
|
|
716
|
-
await this.
|
|
809
|
+
await this.caps.hooks.run('agent:llm:after', nextLlmAfterData);
|
|
717
810
|
response = nextLlmAfterData.response;
|
|
718
811
|
if (response.reasoningContent) {
|
|
719
812
|
allReasoning.push(response.reasoningContent);
|
|
@@ -727,6 +820,8 @@ class DefaultAgent {
|
|
|
727
820
|
// 保留原始 LLM 输出,用于存入 memory(避免纯文本历史污染 few-shot 示例)
|
|
728
821
|
let rawLlmContent = response.content ?? '';
|
|
729
822
|
let replyContent = rawLlmContent;
|
|
823
|
+
// Hook: agent:reply:before — 插件可以修改最终回复
|
|
824
|
+
// JSON 解析/修复统一由 persona 的 agent:reply:before 钩子处理
|
|
730
825
|
const responseData = {
|
|
731
826
|
content: replyContent,
|
|
732
827
|
sessionId: incoming.sessionId,
|
|
@@ -735,16 +830,18 @@ class DefaultAgent {
|
|
|
735
830
|
triggerType: incoming.triggerType,
|
|
736
831
|
attempt: 0,
|
|
737
832
|
};
|
|
738
|
-
await this.
|
|
833
|
+
await this.caps.hooks.run('agent:reply:before', responseData);
|
|
739
834
|
// 重试循环:当 hook(如 persona 的 outputFormat 解析)报告 retryRequested 时,
|
|
740
835
|
// 把失败的 assistant 输出 + 系统反馈追加到消息列表,重新请求 LLM;最多按 maxRetries 次。
|
|
741
836
|
// maxRetries 由 hook 端写入(plugin-persona 从 outputFormat.retries 读取,默认 1)。
|
|
837
|
+
// 上游吐空流同样会被判不合格:此时没有可回放的失败输出,只追加系统反馈再请求一次。
|
|
742
838
|
const maxRetries = Math.max(0, responseData.maxRetries ?? 0);
|
|
743
839
|
let attempt = 0;
|
|
744
|
-
while (responseData.retryRequested && attempt < maxRetries
|
|
840
|
+
while (responseData.retryRequested && attempt < maxRetries) {
|
|
745
841
|
attempt++;
|
|
746
842
|
this.logger.debug(`agent:reply:before 请求重试 (attempt=${attempt}/${maxRetries}, session=${incoming.sessionId}): ${responseData.retryFeedback ?? '(无反馈)'}`);
|
|
747
|
-
|
|
843
|
+
if (rawLlmContent.length > 0)
|
|
844
|
+
llmBeforeData.messages.push({ role: 'assistant', content: rawLlmContent });
|
|
748
845
|
llmBeforeData.messages.push({
|
|
749
846
|
role: 'system',
|
|
750
847
|
content: responseData.retryFeedback ?? '上一次回复未能通过格式校验,请严格按照系统提示中规定的格式重新输出。',
|
|
@@ -756,7 +853,7 @@ class DefaultAgent {
|
|
|
756
853
|
maxTokens,
|
|
757
854
|
signal,
|
|
758
855
|
...(thinkOverride !== undefined ? { think: thinkOverride } : {}),
|
|
759
|
-
}, incoming.sessionId, incoming.platform
|
|
856
|
+
}, incoming.sessionId, incoming.platform);
|
|
760
857
|
turnSegments.push(...retryResult.segments);
|
|
761
858
|
response = retryResult;
|
|
762
859
|
accUsage(response.usage);
|
|
@@ -766,10 +863,11 @@ class DefaultAgent {
|
|
|
766
863
|
// 用新输出再次跑 hook;hook 端根据 attempt 决定继续重试或走兜底(静默丢弃)
|
|
767
864
|
responseData.content = rawLlmContent;
|
|
768
865
|
responseData.archiveContent = undefined;
|
|
866
|
+
responseData.visibleContent = undefined;
|
|
769
867
|
responseData.retryRequested = false;
|
|
770
868
|
responseData.retryFeedback = undefined;
|
|
771
869
|
responseData.attempt = attempt;
|
|
772
|
-
await this.
|
|
870
|
+
await this.caps.hooks.run('agent:reply:before', responseData);
|
|
773
871
|
}
|
|
774
872
|
// 双保险:循环结束后若 hook 仍标记 retryRequested(理论上 persona 已在用尽时自动走兜底),
|
|
775
873
|
// 强制把 content 置空,避免原始未校验内容被外发。
|
|
@@ -780,6 +878,7 @@ class DefaultAgent {
|
|
|
780
878
|
}
|
|
781
879
|
replyContent = responseData.content;
|
|
782
880
|
const archiveContent = responseData.archiveContent ?? rawLlmContent;
|
|
881
|
+
const visibleContent = responseData.visibleContent ?? replyContent;
|
|
783
882
|
// 重复检测:如果回复与最近一条 assistant 消息完全相同,视为模型"卡壳",静默跳过。
|
|
784
883
|
// 比的是 archiveContent(与落库同源口径)——启用 outputFormat 时历史里存的是整串 JSON,
|
|
785
884
|
// 拿解码后的纯文本去比永远不成立。
|
|
@@ -788,8 +887,12 @@ class DefaultAgent {
|
|
|
788
887
|
this.logger.warn(`检测到重复回复,跳过发送 (session=${incoming.sessionId})`);
|
|
789
888
|
replyContent = '';
|
|
790
889
|
}
|
|
890
|
+
// 提交点:此后的流结束标记、落库与外发成组执行。流结束后到这里之间(llm:after / 工具循环退出 /
|
|
891
|
+
// reply:before / 重试)被 latest-wins、手动停止或拆卸掐掉的回合,走下方 AbortError 收尾,不落库、不外发。
|
|
892
|
+
if (signal.aborted)
|
|
893
|
+
throw new DOMException('Generation aborted', 'AbortError');
|
|
791
894
|
// 发出流结束标记
|
|
792
|
-
await this.
|
|
895
|
+
await this.caps.events.emit('outbound:stream', {
|
|
793
896
|
sessionId: incoming.sessionId,
|
|
794
897
|
platform: incoming.platform,
|
|
795
898
|
done: true,
|
|
@@ -815,6 +918,10 @@ class DefaultAgent {
|
|
|
815
918
|
...(assistantMetadata ?? {}),
|
|
816
919
|
modelInfo: turnModelInfo,
|
|
817
920
|
};
|
|
921
|
+
// 落库的是信封(如 outputFormat 的整串 JSON)时,另带解码后的可见正文,供检索、摘要等按语义读历史的一方用
|
|
922
|
+
if (visibleContent !== archiveContent) {
|
|
923
|
+
finalAssistantMetadata[WellKnownMetadataKeys.VisibleContent] = visibleContent;
|
|
924
|
+
}
|
|
818
925
|
// 保存最终 assistant 回复:优先存 persona 修复/规范化后的 JSON,保持格式完整,
|
|
819
926
|
// 避免坏 JSON 或解码后纯文本污染历史 few-shot 示例导致模型不再遵守 outputFormat
|
|
820
927
|
await this.saveToMemory(incoming.sessionId, {
|
|
@@ -838,7 +945,7 @@ class DefaultAgent {
|
|
|
838
945
|
}
|
|
839
946
|
// Hook: agent:turn:after — 插件可以在完整消息周期结束后做后处理
|
|
840
947
|
const turnOutcome = replyContent.trim().length === 0 ? 'silent' : 'replied';
|
|
841
|
-
await this.
|
|
948
|
+
await this.caps.hooks.run('agent:turn:after', {
|
|
842
949
|
message: incoming,
|
|
843
950
|
reply: replyContent,
|
|
844
951
|
outcome: turnOutcome,
|
|
@@ -851,9 +958,9 @@ class DefaultAgent {
|
|
|
851
958
|
//
|
|
852
959
|
// 历史教训:早期版本会用 turnPersistedTimestamps 把"本轮已持久化的中间消息"全部删掉,
|
|
853
960
|
// 注释里写的是防"半截 assistant 内容 / 假回退"。但实际审计 saveToolCallGroup(见下方)发现:
|
|
854
|
-
// 1. saveToolCallGroup
|
|
855
|
-
// tool result 一次性 push
|
|
856
|
-
//
|
|
961
|
+
// 1. saveToolCallGroup 只在并行工具全部落定后整组写入(assistant tool_call + 所有
|
|
962
|
+
// tool result 一次性 push;单个工具的异常已在闭包内转成它自己的错误结果),不存在
|
|
963
|
+
// 写了一半的组;turnPersistedTimestamps 里只可能是"已落定、副作用已发生"的工具调用对。
|
|
857
964
|
// 2. 删除这些 = 让 agent 忘记自己刚刚做过的有副作用的事(戳一戳/发送消息/调度任务/…),
|
|
858
965
|
// 下一轮 LLM 看不到自己的行为,会重复调用,外部观察就是"agent 一直以为戳不了"。
|
|
859
966
|
// 3. 真正的 orphan 风险(assistant tool_calls 缺 tool result)由 sanitizeToolCallHistory
|
|
@@ -864,7 +971,7 @@ class DefaultAgent {
|
|
|
864
971
|
(turnPersistedTimestamps.length > 0
|
|
865
972
|
? `(保留本轮已完成的 ${turnPersistedTimestamps.length} 条工具调用记录,便于下一轮 agent 感知)`
|
|
866
973
|
: ''));
|
|
867
|
-
await this.
|
|
974
|
+
await this.caps.events.emit('outbound:stream', {
|
|
868
975
|
sessionId: incoming.sessionId,
|
|
869
976
|
platform: incoming.platform,
|
|
870
977
|
done: true,
|
|
@@ -873,7 +980,7 @@ class DefaultAgent {
|
|
|
873
980
|
// session-manager 把会话状态从 active 收口为 completed(否则永远停在"进行中"),
|
|
874
981
|
// checkpoint 关闭当前回合(否则中止后回合不关闭、长期泄漏)。
|
|
875
982
|
// 文档与 agent-api 早已声明 outcome 含 aborted,此处兑现契约。
|
|
876
|
-
await this.
|
|
983
|
+
await this.caps.hooks.run('agent:turn:after', {
|
|
877
984
|
message: incoming,
|
|
878
985
|
reply: '',
|
|
879
986
|
outcome: 'aborted',
|
|
@@ -886,7 +993,7 @@ class DefaultAgent {
|
|
|
886
993
|
this.logger.error(`处理消息失败: ${message}`);
|
|
887
994
|
// 异常同样要结束流:不发 done,CLI 的流式块不收尾、WebUI 的工具进度与上限标记不复位,
|
|
888
995
|
// 下一轮 delta 还会抹掉这条 [错误] 与用户的新输入。顺序与正常收尾一致——先 done 再发消息。
|
|
889
|
-
await this.
|
|
996
|
+
await this.caps.events.emit('outbound:stream', {
|
|
890
997
|
sessionId: incoming.sessionId,
|
|
891
998
|
platform: incoming.platform,
|
|
892
999
|
done: true,
|
|
@@ -900,7 +1007,7 @@ class DefaultAgent {
|
|
|
900
1007
|
// 异常也是回合终态:同样发 turn:after(outcome=error) 让 checkpoint 关闭回合、
|
|
901
1008
|
// session-manager 收口状态。dispatchOutbound 已发系统错误消息,状态可被 outbound:message
|
|
902
1009
|
// 与本钩子双路径幂等收口。
|
|
903
|
-
await this.
|
|
1010
|
+
await this.caps.hooks.run('agent:turn:after', {
|
|
904
1011
|
message: incoming,
|
|
905
1012
|
reply: '',
|
|
906
1013
|
outcome: 'error',
|
|
@@ -911,7 +1018,7 @@ class DefaultAgent {
|
|
|
911
1018
|
});
|
|
912
1019
|
// 消息被拦截(如流控缓冲),通知前端结束 loading
|
|
913
1020
|
if (!handled) {
|
|
914
|
-
await this.
|
|
1021
|
+
await this.caps.events.emit('outbound:stream', {
|
|
915
1022
|
sessionId: incoming.sessionId,
|
|
916
1023
|
platform: incoming.platform,
|
|
917
1024
|
done: true,
|
|
@@ -927,7 +1034,7 @@ class DefaultAgent {
|
|
|
927
1034
|
const systemPrompt = this.buildSystemPrompt(personaOpts);
|
|
928
1035
|
messages.push({ role: 'system', content: systemPrompt, metadata: { injector: 'persona' } });
|
|
929
1036
|
// 2. 历史消息
|
|
930
|
-
const memory = this.
|
|
1037
|
+
const memory = this.caps.memory.current;
|
|
931
1038
|
if (memory) {
|
|
932
1039
|
try {
|
|
933
1040
|
const history = this.sanitizeToolCallHistory(await memory.getHistory(incoming.sessionId, this.historyLimit), incoming.sessionId);
|
|
@@ -1011,7 +1118,7 @@ class DefaultAgent {
|
|
|
1011
1118
|
// 多模态:把 attachments 中的 image 项传递给 LLM(视觉模型多模态字段)
|
|
1012
1119
|
const imageAtts = incoming.attachments?.filter(a => a.kind === 'image') ?? [];
|
|
1013
1120
|
if (imageAtts.length > 0) {
|
|
1014
|
-
if (this.
|
|
1121
|
+
if (this.caps.media.current !== undefined) {
|
|
1015
1122
|
// media 在场:原样透传。出口形态由 media 的 agent:llm:before 中间件规范化
|
|
1016
1123
|
//(describe 剥离 / passthrough 物化),此处不做任何转换——它的描述缓存键与
|
|
1017
1124
|
// 动图提示都按 attachment.data 原串命中,改形态会打散。
|
|
@@ -1041,7 +1148,7 @@ class DefaultAgent {
|
|
|
1041
1148
|
// 秒级时间戳一变,人设 + 记忆 + 历史近 7 万 token 的前缀全部作废——实测每个
|
|
1042
1149
|
// 新回合命中率恒为 0,只有同回合内的工具迭代(prompt 不重建)才命中。
|
|
1043
1150
|
// 放到这里语义也更顺:它们本就是「此刻的事实」,紧挨当前消息说更自然。
|
|
1044
|
-
const persona = this.
|
|
1151
|
+
const persona = this.caps.persona.current;
|
|
1045
1152
|
const volatileCtx = persona?.getVolatilePrompt?.(personaOpts);
|
|
1046
1153
|
if (volatileCtx) {
|
|
1047
1154
|
messages.push({ role: 'system', content: volatileCtx, metadata: { injector: VOLATILE_INJECTOR } });
|
|
@@ -1054,11 +1161,19 @@ class DefaultAgent {
|
|
|
1054
1161
|
messages.push(userMessage);
|
|
1055
1162
|
return messages;
|
|
1056
1163
|
}
|
|
1164
|
+
/**
|
|
1165
|
+
* 裁剪预算 = 上下文长度 × trimThresholdRatio − 最大输出 token − 512 安全余量(下限 1024)。
|
|
1166
|
+
* trimThresholdRatio < 1 可提前触发裁剪,默认 1.0 = 用满扣除输出预留后的可用窗口。
|
|
1167
|
+
* 真实回合与 token:request 快照共用,两处的预算口径因此一致。
|
|
1168
|
+
*/
|
|
1169
|
+
computeTokenBudget(contextLength, maxTokens) {
|
|
1170
|
+
return Math.max(1024, Math.floor(contextLength * this.trimThresholdRatio) - maxTokens - 512);
|
|
1171
|
+
}
|
|
1057
1172
|
/**
|
|
1058
1173
|
* 构建系统提示词
|
|
1059
1174
|
*/
|
|
1060
1175
|
buildSystemPrompt(personaOpts) {
|
|
1061
|
-
const persona = this.
|
|
1176
|
+
const persona = this.caps.persona.current;
|
|
1062
1177
|
const base = persona
|
|
1063
1178
|
? this.systemPrompt
|
|
1064
1179
|
? `${persona.getSystemPrompt(personaOpts)}\n\n${this.systemPrompt}`
|
|
@@ -1092,7 +1207,7 @@ class DefaultAgent {
|
|
|
1092
1207
|
for (const msg of messages) {
|
|
1093
1208
|
const t = estimateMsgTokens(msg);
|
|
1094
1209
|
if (msg.role === 'system') {
|
|
1095
|
-
// 贡献点物化块的 injector 是全局键 `${
|
|
1210
|
+
// 贡献点物化块的 injector 是全局键 `${贡献方实例 id}/${局部标签}`(实例 id 含 '/'),
|
|
1096
1211
|
// 归桶按局部标签(末段)匹配——桶规则与统计明细键保持迁移前的稳定值;
|
|
1097
1212
|
// 非贡献来源(persona / system-other 等)无 '/',末段即原值,不受影响。
|
|
1098
1213
|
const rawSource = msg.metadata?.injector;
|
|
@@ -1119,14 +1234,12 @@ class DefaultAgent {
|
|
|
1119
1234
|
subtaskTokens += t;
|
|
1120
1235
|
}
|
|
1121
1236
|
else if (source === 'persona' || !source) {
|
|
1122
|
-
// persona 消息可能被
|
|
1237
|
+
// persona 消息可能被 subtask 追加内容
|
|
1123
1238
|
if (contributions) {
|
|
1124
1239
|
let contributionTokens = 0;
|
|
1125
1240
|
for (const [key, charCount] of Object.entries(contributions)) {
|
|
1126
1241
|
const ct = estimateTextTokens('x'.repeat(charCount));
|
|
1127
|
-
if (key === '
|
|
1128
|
-
skillsTokens += ct;
|
|
1129
|
-
else if (key === 'subtask')
|
|
1242
|
+
if (key === 'subtask')
|
|
1130
1243
|
subtaskTokens += ct;
|
|
1131
1244
|
else
|
|
1132
1245
|
systemOtherTokens += ct;
|
|
@@ -1163,7 +1276,7 @@ class DefaultAgent {
|
|
|
1163
1276
|
systemOtherTokens;
|
|
1164
1277
|
const totalUsed = systemTokens + historyTokens + toolResultTokens + toolDefsTokens;
|
|
1165
1278
|
const usageRatio = contextLength > 0 ? totalUsed / contextLength : 0;
|
|
1166
|
-
this.
|
|
1279
|
+
this.caps.events
|
|
1167
1280
|
.emit('token:usage', {
|
|
1168
1281
|
sessionId,
|
|
1169
1282
|
platform,
|
|
@@ -1218,9 +1331,9 @@ class DefaultAgent {
|
|
|
1218
1331
|
const rel = t.match(/^data\/((?:images|videos|audios|files)\/.+)$/);
|
|
1219
1332
|
if (!rel)
|
|
1220
1333
|
return t;
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1334
|
+
// 经网关按 URI 路由:storage 按根逐条提供,胜者只是其中一个根(多根时通常是 workspace),
|
|
1335
|
+
// 读不了 data:/。缺 data 根(含 storage 整体缺席)时网关抛错,落到下面的 catch
|
|
1336
|
+
const storage = createStorageGateway(this.caps.storage);
|
|
1224
1337
|
try {
|
|
1225
1338
|
const raw = (await storage.readFile(`data:/${rel[1]}`));
|
|
1226
1339
|
// mime 优先取附件自带的 mimeType;缺省按 basename 扩展名推断(适配器落盘只产
|
|
@@ -1472,11 +1585,17 @@ class DefaultAgent {
|
|
|
1472
1585
|
}
|
|
1473
1586
|
return result;
|
|
1474
1587
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1588
|
+
getArchiveForWrite() {
|
|
1589
|
+
const archive = this.caps.messageArchive.current;
|
|
1590
|
+
if (!archive && !this.archiveUnavailableWarned) {
|
|
1591
|
+
this.archiveUnavailableWarned = true;
|
|
1592
|
+
this.logger.warn('message-archive 服务不可用,缺席期间的新消息不会写入记忆,对话继续。请加载 @aalis/plugin-message-archive 及其 memory 依赖。');
|
|
1593
|
+
}
|
|
1594
|
+
return archive;
|
|
1595
|
+
}
|
|
1596
|
+
/** 保存消息到记忆服务 */
|
|
1478
1597
|
async saveToMemory(sessionId, message) {
|
|
1479
|
-
const archive = this.
|
|
1598
|
+
const archive = this.getArchiveForWrite();
|
|
1480
1599
|
if (archive) {
|
|
1481
1600
|
try {
|
|
1482
1601
|
await archive.saveMessage(sessionId, message);
|
|
@@ -1487,7 +1606,7 @@ class DefaultAgent {
|
|
|
1487
1606
|
}
|
|
1488
1607
|
}
|
|
1489
1608
|
buildAssistantMetadata(incoming) {
|
|
1490
|
-
const identity = getPlatformSelfIdentity(this.
|
|
1609
|
+
const identity = getPlatformSelfIdentity(this.caps.platform, incoming.platform, incoming.sessionId);
|
|
1491
1610
|
const metadata = {
|
|
1492
1611
|
platform: incoming.platform,
|
|
1493
1612
|
senderType: 'assistant',
|
|
@@ -1576,7 +1695,7 @@ class DefaultAgent {
|
|
|
1576
1695
|
// 跳过非真实用户输入:闲聊主动触发是系统提示,不应作为 user 消息写入历史
|
|
1577
1696
|
if (incoming.source === 'idle-trigger')
|
|
1578
1697
|
return undefined;
|
|
1579
|
-
const archive = this.
|
|
1698
|
+
const archive = this.getArchiveForWrite();
|
|
1580
1699
|
if (!archive)
|
|
1581
1700
|
return undefined;
|
|
1582
1701
|
try {
|
|
@@ -1595,35 +1714,27 @@ class DefaultAgent {
|
|
|
1595
1714
|
* 此时该 fallback 永远不命中——出站消息总是经过 outbound:dispatch 钩子链
|
|
1596
1715
|
* (审计 / 脱敏 / 限速 / authority 等中间件)。
|
|
1597
1716
|
*
|
|
1598
|
-
*
|
|
1599
|
-
* 即使 gateway 未加载本插件仍会激活,因此保留该 fallback 以便:
|
|
1717
|
+
* gateway 只是本插件的 optional 依赖,它不在场时 agent 照常激活,因此保留该 fallback 以便:
|
|
1600
1718
|
* - 集成测试不必启动 gateway
|
|
1601
1719
|
* - 嵌入式 / 单 agent 部署场景
|
|
1602
1720
|
* 生产部署务必确保 plugin-gateway 已加载,否则中间件链会被跳过。
|
|
1603
1721
|
*/
|
|
1604
1722
|
async dispatchOutbound(message) {
|
|
1605
|
-
const gateway = this.
|
|
1723
|
+
const gateway = this.caps.gateway.current;
|
|
1606
1724
|
if (gateway) {
|
|
1607
1725
|
await gateway.dispatchOutbound(message);
|
|
1608
1726
|
return;
|
|
1609
1727
|
}
|
|
1610
|
-
this.logger.warn('Gateway 服务不可用,回退至
|
|
1611
|
-
await this.
|
|
1728
|
+
this.logger.warn('Gateway 服务不可用,回退至 outbound:message 事件(中间件链被跳过)');
|
|
1729
|
+
await this.caps.events.emit('outbound:message', message);
|
|
1612
1730
|
}
|
|
1613
1731
|
}
|
|
1614
1732
|
// ----- 插件导出 -----
|
|
1615
|
-
|
|
1616
|
-
export const displayName = '默认 Agent';
|
|
1617
|
-
export const subsystem = 'agent';
|
|
1618
|
-
export const provides = ['agent'];
|
|
1619
|
-
export const inject = {
|
|
1620
|
-
optional: ['llm', 'memory', 'persona', 'message-archive', 'platform', 'media', 'storage'],
|
|
1621
|
-
};
|
|
1622
|
-
export const configSchema = {
|
|
1733
|
+
const configSchema = {
|
|
1623
1734
|
defaultLLM: {
|
|
1624
1735
|
type: 'llm-ref',
|
|
1625
1736
|
label: '默认对话模型',
|
|
1626
|
-
description: '全局默认 LLM。apply()
|
|
1737
|
+
description: '全局默认 LLM。apply() 时经 services.prefer 锁定 llm 服务的偏好提供者。会话 / 平台 profile 未覆盖时生效。',
|
|
1627
1738
|
},
|
|
1628
1739
|
systemPrompt: {
|
|
1629
1740
|
type: 'textarea',
|
|
@@ -1635,7 +1746,7 @@ export const configSchema = {
|
|
|
1635
1746
|
type: 'number',
|
|
1636
1747
|
label: '长期记忆预留 Token',
|
|
1637
1748
|
default: 4096,
|
|
1638
|
-
description: '
|
|
1749
|
+
description: '为注入的 system 消息预留的 token 额度:超出时先按比例缩减,极端情况下(最后阶段)仍会删除',
|
|
1639
1750
|
},
|
|
1640
1751
|
historyLimit: {
|
|
1641
1752
|
type: 'number',
|
|
@@ -1668,21 +1779,28 @@ export const configSchema = {
|
|
|
1668
1779
|
description: '裁剪预算 = 上下文长度 × 该比例 − 最大输出 token − 512 安全余量(下限 1024)。本次调用估算输入 token 超过该预算才会对消息列表做内存裁剪(不影响 DB)。默认 1.0 表示用满扣除输出预留后的可用窗口;调低可提前裁剪。压缩触发请在“@aalis/plugin-memory-summary”中配置。',
|
|
1669
1780
|
},
|
|
1670
1781
|
};
|
|
1671
|
-
|
|
1672
|
-
const agentImpl = new DefaultAgent(
|
|
1673
|
-
|
|
1674
|
-
//
|
|
1675
|
-
|
|
1782
|
+
function run(caps) {
|
|
1783
|
+
const agentImpl = new DefaultAgent(caps);
|
|
1784
|
+
caps.provide(agentService, agentImpl);
|
|
1785
|
+
// 收尾段中止在飞回合并等 AbortError 落定。
|
|
1786
|
+
// 第一方栈由 core 关停编排(optional 环成员先全部 drain)与 agent:turn:after 收口;
|
|
1787
|
+
// session-manager 的 onDrain 兜的是未装 agent 或钩子未挂上的路径。
|
|
1788
|
+
// 超时沿用 core 对 onDrain 的 disposeTimeoutMs,不另加配置键。
|
|
1789
|
+
caps.lifecycle.onDrain(() => agentImpl.abortInflightAndSettle(), '中止在飞回合');
|
|
1790
|
+
// 清理段再 abort 一次:drain 超时或未走到收尾时的兜底,表空则空操作。
|
|
1791
|
+
caps.lifecycle.onDispose(() => agentImpl.abortAll(), '中止在飞回合(兜底)');
|
|
1792
|
+
// 会话删除:丢弃该会话的 token 日志节流状态(与 todo-list / file-reader 走同一条会话清理路径)
|
|
1793
|
+
caps.events.on('session:deleted', sessionId => agentImpl.dropTokenLogState(sessionId));
|
|
1676
1794
|
const agent = agentImpl;
|
|
1677
|
-
// 全局默认 LLM:通过 ServicePreference 锁定
|
|
1678
|
-
//
|
|
1795
|
+
// 全局默认 LLM:通过 ServicePreference 锁定 llm 服务的首选 entry。
|
|
1796
|
+
// 偏好的持久化在宿主配置文档(servicePreferences 字段,WebUI 切换时写入);这里只是开机时按 agent
|
|
1679
1797
|
// 自己的 cfg.defaultLLM 覆写一次,便于纯文件配置流(无 webui 干预)也能生效。
|
|
1680
|
-
const defaultLLM = config.defaultLLM;
|
|
1798
|
+
const defaultLLM = caps.config.defaultLLM;
|
|
1681
1799
|
if (defaultLLM?.provider && defaultLLM?.model) {
|
|
1682
|
-
|
|
1800
|
+
caps.services.prefer(llmService, `${defaultLLM.provider}/${defaultLLM.model}`);
|
|
1683
1801
|
}
|
|
1684
1802
|
function resolutionLines(sessionId, platform, label, pick) {
|
|
1685
|
-
const smSvc =
|
|
1803
|
+
const smSvc = caps.sessionManager.current;
|
|
1686
1804
|
if (!smSvc)
|
|
1687
1805
|
return [`${label}: (session-manager 不可用)`];
|
|
1688
1806
|
const session = smSvc.getSession(sessionId);
|
|
@@ -1734,13 +1852,13 @@ export function apply(ctx, config) {
|
|
|
1734
1852
|
}
|
|
1735
1853
|
return lines.join('\n');
|
|
1736
1854
|
}
|
|
1737
|
-
|
|
1855
|
+
caps.commands
|
|
1738
1856
|
.command('model [keyword:string]', '列出/搜索可用对话模型(分页,-p 翻页;如 /model grok)')
|
|
1739
1857
|
.option('page', '-p <n:number>', { description: '页码(默认 1)' })
|
|
1740
1858
|
.action(async (argv, keyword) => {
|
|
1741
1859
|
const seen = new Set();
|
|
1742
1860
|
const ids = [];
|
|
1743
|
-
for (const e of listLLMModels(
|
|
1861
|
+
for (const e of listLLMModels(caps.llm, { caps: ['chat'] })) {
|
|
1744
1862
|
if (seen.has(e.contextId))
|
|
1745
1863
|
continue;
|
|
1746
1864
|
seen.add(e.contextId);
|
|
@@ -1749,20 +1867,20 @@ export function apply(ctx, config) {
|
|
|
1749
1867
|
const page = typeof argv.options.page === 'number' ? argv.options.page : 1;
|
|
1750
1868
|
return renderPaged(ids, String(keyword ?? ''), page, '可用模型', '/model');
|
|
1751
1869
|
});
|
|
1752
|
-
|
|
1870
|
+
caps.commands
|
|
1753
1871
|
.command('persona [keyword:string]', '列出/搜索可用人设(分页,-p 翻页)')
|
|
1754
1872
|
.option('page', '-p <n:number>', { description: '页码(默认 1)' })
|
|
1755
1873
|
.action(async (argv, keyword) => {
|
|
1756
|
-
const persona =
|
|
1874
|
+
const persona = caps.persona.current;
|
|
1757
1875
|
const names = persona?.listModels ? await persona.listModels() : [];
|
|
1758
1876
|
const page = typeof argv.options.page === 'number' ? argv.options.page : 1;
|
|
1759
1877
|
return renderPaged(names, String(keyword ?? ''), page, '可用人设', '/persona');
|
|
1760
1878
|
});
|
|
1761
1879
|
// ---- 会话级「模型 + 人设 + thinking + 名称」配置(onebot 等平台对话直接改当前对话生效) ----
|
|
1762
|
-
|
|
1880
|
+
caps.commands
|
|
1763
1881
|
.command('session', '查看当前对话生效的模型 / 人设 / thinking / 名称及来源与解析链', { risk: 'sensitive' })
|
|
1764
1882
|
.action(async (argv) => {
|
|
1765
|
-
const smSvc =
|
|
1883
|
+
const smSvc = caps.sessionManager.current;
|
|
1766
1884
|
if (!smSvc)
|
|
1767
1885
|
return 'session-manager 服务不可用';
|
|
1768
1886
|
const sid = argv.session.sessionId;
|
|
@@ -1780,7 +1898,7 @@ export function apply(ctx, config) {
|
|
|
1780
1898
|
.filter(Boolean)
|
|
1781
1899
|
.join('\n');
|
|
1782
1900
|
});
|
|
1783
|
-
|
|
1901
|
+
caps.commands
|
|
1784
1902
|
.command('session.set', '设定当前对话的模型 / 人设 / thinking / 显示名(会话级覆盖,持久化,重启不丢)', {
|
|
1785
1903
|
risk: 'sensitive',
|
|
1786
1904
|
examples: [
|
|
@@ -1797,7 +1915,7 @@ export function apply(ctx, config) {
|
|
|
1797
1915
|
})
|
|
1798
1916
|
.option('name', '-n <label:string>', { description: '会话显示名(可选)' })
|
|
1799
1917
|
.action(async (argv) => {
|
|
1800
|
-
const smSvc =
|
|
1918
|
+
const smSvc = caps.sessionManager.current;
|
|
1801
1919
|
if (!smSvc)
|
|
1802
1920
|
return 'session-manager 服务不可用';
|
|
1803
1921
|
const modelRef = typeof argv.options.model === 'string' ? argv.options.model.trim() : '';
|
|
@@ -1819,7 +1937,7 @@ export function apply(ctx, config) {
|
|
|
1819
1937
|
if (lastSlash <= 0 || lastSlash === modelRef.length - 1) {
|
|
1820
1938
|
return '模型格式错误。请用 `provider/model`,例如 `@aalis/plugin-llm-openai:main/gpt-4o`';
|
|
1821
1939
|
}
|
|
1822
|
-
const chatModels = listLLMModels(
|
|
1940
|
+
const chatModels = listLLMModels(caps.llm, { caps: ['chat'] });
|
|
1823
1941
|
if (!chatModels.some(e => e.contextId === modelRef)) {
|
|
1824
1942
|
return `未知模型 "${modelRef}"。用 /model <关键词> 搜索(如 /model grok),共 ${chatModels.length} 个可用。`;
|
|
1825
1943
|
}
|
|
@@ -1827,7 +1945,7 @@ export function apply(ctx, config) {
|
|
|
1827
1945
|
}
|
|
1828
1946
|
// 人设:listModels 可选,缺失则不校验(persona 插件缺席时不误拒)
|
|
1829
1947
|
if (personaName) {
|
|
1830
|
-
const persona =
|
|
1948
|
+
const persona = caps.persona.current;
|
|
1831
1949
|
if (persona?.listModels) {
|
|
1832
1950
|
const names = await persona.listModels();
|
|
1833
1951
|
if (!names.includes(personaName)) {
|
|
@@ -1853,7 +1971,7 @@ export function apply(ctx, config) {
|
|
|
1853
1971
|
lines.push('后续消息持续生效(已持久化,重启不丢)。/session.reset 可复位。');
|
|
1854
1972
|
return lines.join('\n');
|
|
1855
1973
|
});
|
|
1856
|
-
|
|
1974
|
+
caps.commands
|
|
1857
1975
|
.command('session.reset', '复位当前对话的会话级覆盖(默认清模型+人设+thinking;-m/-p/-t 单独清对应项)', {
|
|
1858
1976
|
risk: 'sensitive',
|
|
1859
1977
|
examples: ['/session.reset', '/session.reset -m', '/session.reset -p', '/session.reset -t'],
|
|
@@ -1862,7 +1980,7 @@ export function apply(ctx, config) {
|
|
|
1862
1980
|
.option('persona', '-p', { description: '仅复位人设覆盖' })
|
|
1863
1981
|
.option('think', '-t', { description: '仅复位 thinking 覆盖' })
|
|
1864
1982
|
.action(async (argv) => {
|
|
1865
|
-
const smSvc =
|
|
1983
|
+
const smSvc = caps.sessionManager.current;
|
|
1866
1984
|
if (!smSvc)
|
|
1867
1985
|
return 'session-manager 服务不可用';
|
|
1868
1986
|
const mFlag = argv.options.model === true;
|
|
@@ -1894,8 +2012,7 @@ export function apply(ctx, config) {
|
|
|
1894
2012
|
return `已复位 → 模型: ${eff.llm ? `${eff.llm.provider}/${eff.llm.model}` : '(默认)'},人设: ${eff.persona ?? '(默认)'},thinking: ${thinkStr}`;
|
|
1895
2013
|
});
|
|
1896
2014
|
// 监听 token:request 事件 — 客户端刷新/重连时主动请求 token 用量
|
|
1897
|
-
|
|
1898
|
-
const data = args[0];
|
|
2015
|
+
caps.events.on('token:request', async (data) => {
|
|
1899
2016
|
if (!data?.sessionId)
|
|
1900
2017
|
return;
|
|
1901
2018
|
// 唯一发射方是 WebUI;平台缺省按 'webui' 兜底,否则模型/会话配置解析会绕过平台 profile 层
|
|
@@ -1907,12 +2024,15 @@ export function apply(ctx, config) {
|
|
|
1907
2024
|
const llm = resolved.instance;
|
|
1908
2025
|
const contextLength = llm.contextLength;
|
|
1909
2026
|
const maxTokens = getModelMaxOutput(llm);
|
|
1910
|
-
const tokenBudget =
|
|
2027
|
+
const tokenBudget = agent.computeTokenBudget(contextLength, maxTokens);
|
|
2028
|
+
// 与真实回合同一份会话解析:人设覆盖、结构化输出开关、额外提示都影响系统提示长度
|
|
2029
|
+
const sm = caps.sessionManager.current;
|
|
2030
|
+
const sessionResolved = sm ? sm.resolveConfig(data.sessionId, platform) : undefined;
|
|
1911
2031
|
// 获取历史消息并构建基础消息列表
|
|
1912
|
-
const memory =
|
|
2032
|
+
const memory = caps.memory.current;
|
|
1913
2033
|
const messages = [];
|
|
1914
2034
|
// 系统提示
|
|
1915
|
-
const systemPrompt = agent.buildSystemPrompt();
|
|
2035
|
+
const systemPrompt = agent.buildSystemPrompt(toPersonaOptions(sessionResolved));
|
|
1916
2036
|
messages.push({ role: 'system', content: systemPrompt, metadata: { injector: 'persona' } });
|
|
1917
2037
|
// 历史消息
|
|
1918
2038
|
if (memory) {
|
|
@@ -1921,11 +2041,8 @@ export function apply(ctx, config) {
|
|
|
1921
2041
|
}
|
|
1922
2042
|
// 与真实回合同序:先组装 agent:prompt 贡献(摘要/向量记忆/档案等物化为 system 块),
|
|
1923
2043
|
// 再跑 agent:llm:before(工具搜索层过滤等拦截职责),使快照贴近实际送入 LLM 的形态
|
|
1924
|
-
const sm = ctx.getService('session-manager');
|
|
1925
|
-
const sessionResolved = sm ? sm.resolveConfig(data.sessionId, platform) : undefined;
|
|
1926
2044
|
const enabledGroups = sessionResolved?.enabledToolGroups?.length ? sessionResolved.enabledToolGroups : undefined;
|
|
1927
|
-
const tools =
|
|
1928
|
-
[];
|
|
2045
|
+
const tools = caps.tools.current?.getDefinitions(enabledGroups ? { groups: enabledGroups } : undefined) ?? [];
|
|
1929
2046
|
const llmBeforeData = {
|
|
1930
2047
|
messages,
|
|
1931
2048
|
tools,
|
|
@@ -1934,13 +2051,22 @@ export function apply(ctx, config) {
|
|
|
1934
2051
|
platform,
|
|
1935
2052
|
dryRun: true, // 纯统计路径:昂贵注入者(向量检索/档案加载)据此跳过副作用
|
|
1936
2053
|
};
|
|
1937
|
-
await assemblePromptContributions(
|
|
1938
|
-
await
|
|
2054
|
+
await assemblePromptContributions(caps, llmBeforeData, { buildTimeoutMs: agent.promptBuildTimeoutMs });
|
|
2055
|
+
await caps.hooks.run('agent:llm:before', llmBeforeData, undefined, { warnOnStall: true });
|
|
1939
2056
|
agent.emitTokenUsage(data.sessionId, platform, llmBeforeData.messages, llmBeforeData.tools, contextLength, maxTokens, tokenBudget);
|
|
1940
2057
|
}
|
|
1941
2058
|
catch (err) {
|
|
1942
|
-
|
|
2059
|
+
caps.logger.debug('token:request 处理失败:', err);
|
|
1943
2060
|
}
|
|
1944
2061
|
});
|
|
1945
2062
|
}
|
|
2063
|
+
export default definePlugin({
|
|
2064
|
+
name: '@aalis/plugin-agent',
|
|
2065
|
+
displayName: '默认 Agent',
|
|
2066
|
+
subsystem: 'agent',
|
|
2067
|
+
configSchema,
|
|
2068
|
+
provides: [agentService],
|
|
2069
|
+
uses,
|
|
2070
|
+
apply: run,
|
|
2071
|
+
});
|
|
1946
2072
|
//# sourceMappingURL=index.js.map
|