@aipack-ai/multi-agent 1.1.7 → 1.1.8
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/index.js +1315 -1022
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -78,7 +78,50 @@ function createEmptyUsage() {
|
|
|
78
78
|
return { input: 0, output: 0, total: 0 };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// ../agent/core/tokens.ts
|
|
82
|
+
function estimateTextTokens(text) {
|
|
83
|
+
if (!text) return 0;
|
|
84
|
+
return Math.ceil(text.length / 4);
|
|
85
|
+
}
|
|
86
|
+
function estimateMessageTokens(message) {
|
|
87
|
+
const content = message.content;
|
|
88
|
+
if (typeof content === "string") return estimateTextTokens(content);
|
|
89
|
+
try {
|
|
90
|
+
return Math.ceil(JSON.stringify(content ?? []).length / 4);
|
|
91
|
+
} catch {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ../agent/core/errors.ts
|
|
97
|
+
var AgentErrorCategory = {
|
|
98
|
+
RETRYABLE: "retryable",
|
|
99
|
+
TIMEOUT: "timeout",
|
|
100
|
+
AUTH: "auth",
|
|
101
|
+
CONTEXT_OVERFLOW: "context-overflow",
|
|
102
|
+
RATE_LIMIT: "rate-limit",
|
|
103
|
+
INVALID_REQUEST: "invalid-request",
|
|
104
|
+
UNKNOWN: "unknown"
|
|
105
|
+
};
|
|
106
|
+
|
|
81
107
|
// ../agent/core/tapable.ts
|
|
108
|
+
var tapFailurePolicy = "log";
|
|
109
|
+
var tapErrorHandler;
|
|
110
|
+
function setTapErrorHandler(handler) {
|
|
111
|
+
tapErrorHandler = handler;
|
|
112
|
+
}
|
|
113
|
+
function handleTapError(hook, tap, err) {
|
|
114
|
+
if (tapErrorHandler) {
|
|
115
|
+
try {
|
|
116
|
+
tapErrorHandler({ hook, tap: tap.name, error: err });
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (tapFailurePolicy === "throw") throw err;
|
|
121
|
+
if (tapFailurePolicy === "log") {
|
|
122
|
+
console.warn(`[aipack] extension tap "${tap.name}" on hook "${hook}" \u5931\u8D25:`, err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
82
125
|
var AsyncSeriesHook = class {
|
|
83
126
|
constructor(name) {
|
|
84
127
|
this.name = name;
|
|
@@ -94,6 +137,7 @@ var AsyncSeriesHook = class {
|
|
|
94
137
|
try {
|
|
95
138
|
await tap.fn(...args);
|
|
96
139
|
} catch (err) {
|
|
140
|
+
handleTapError(this.name, tap, err);
|
|
97
141
|
}
|
|
98
142
|
}
|
|
99
143
|
}
|
|
@@ -120,6 +164,7 @@ var AsyncSeriesWaterfallHook = class {
|
|
|
120
164
|
try {
|
|
121
165
|
current = await tap.fn(current, ...rest);
|
|
122
166
|
} catch (err) {
|
|
167
|
+
handleTapError(this.name, tap, err);
|
|
123
168
|
}
|
|
124
169
|
}
|
|
125
170
|
return current;
|
|
@@ -606,15 +651,6 @@ function resourcesToMessages(resources) {
|
|
|
606
651
|
import { Type } from "@sinclair/typebox";
|
|
607
652
|
|
|
608
653
|
// ../agent/ai/errors.ts
|
|
609
|
-
var AgentErrorCategory = {
|
|
610
|
-
RETRYABLE: "retryable",
|
|
611
|
-
TIMEOUT: "timeout",
|
|
612
|
-
AUTH: "auth",
|
|
613
|
-
CONTEXT_OVERFLOW: "context-overflow",
|
|
614
|
-
RATE_LIMIT: "rate-limit",
|
|
615
|
-
INVALID_REQUEST: "invalid-request",
|
|
616
|
-
UNKNOWN: "unknown"
|
|
617
|
-
};
|
|
618
654
|
var RETRYABLE_CATEGORIES = /* @__PURE__ */ new Set([
|
|
619
655
|
AgentErrorCategory.RETRYABLE,
|
|
620
656
|
AgentErrorCategory.RATE_LIMIT
|
|
@@ -802,62 +838,7 @@ function isContextOverflow(message, contextWindow) {
|
|
|
802
838
|
return false;
|
|
803
839
|
}
|
|
804
840
|
|
|
805
|
-
// ../agent/
|
|
806
|
-
function ensureToolPairing(messages) {
|
|
807
|
-
const toolCallIds = /* @__PURE__ */ new Set();
|
|
808
|
-
const toolResultIds = /* @__PURE__ */ new Set();
|
|
809
|
-
for (const msg of messages) {
|
|
810
|
-
if (msg.role === "assistant") {
|
|
811
|
-
const content = msg.content;
|
|
812
|
-
if (Array.isArray(content)) {
|
|
813
|
-
for (const tc of extractToolCalls(content)) {
|
|
814
|
-
toolCallIds.add(tc.id);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
if (msg.role === "toolResult") {
|
|
819
|
-
toolResultIds.add(msg.toolCallId);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
const orphanedCalls = /* @__PURE__ */ new Set();
|
|
823
|
-
const orphanedResults = /* @__PURE__ */ new Set();
|
|
824
|
-
for (const id of toolCallIds) {
|
|
825
|
-
if (!toolResultIds.has(id)) orphanedCalls.add(id);
|
|
826
|
-
}
|
|
827
|
-
for (const id of toolResultIds) {
|
|
828
|
-
if (!toolCallIds.has(id)) orphanedResults.add(id);
|
|
829
|
-
}
|
|
830
|
-
if (orphanedCalls.size === 0 && orphanedResults.size === 0) {
|
|
831
|
-
return messages;
|
|
832
|
-
}
|
|
833
|
-
const result = [];
|
|
834
|
-
for (const msg of messages) {
|
|
835
|
-
if (msg.role === "assistant") {
|
|
836
|
-
const content = msg.content;
|
|
837
|
-
if (Array.isArray(content)) {
|
|
838
|
-
const filteredContent = content.filter((block) => {
|
|
839
|
-
if (block.type === "toolCall") {
|
|
840
|
-
return !orphanedCalls.has(block.id);
|
|
841
|
-
}
|
|
842
|
-
return true;
|
|
843
|
-
});
|
|
844
|
-
if (filteredContent.length === 0 && content.length > 0) continue;
|
|
845
|
-
result.push({ ...msg, content: filteredContent });
|
|
846
|
-
} else {
|
|
847
|
-
result.push(msg);
|
|
848
|
-
}
|
|
849
|
-
} else if (msg.role === "toolResult") {
|
|
850
|
-
if (!orphanedResults.has(msg.toolCallId)) {
|
|
851
|
-
result.push(msg);
|
|
852
|
-
}
|
|
853
|
-
} else {
|
|
854
|
-
result.push(msg);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
return result;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// ../agent/runtime/index.ts
|
|
841
|
+
// ../agent/runtime/shared.ts
|
|
861
842
|
import { randomUUID } from "crypto";
|
|
862
843
|
function newTraceId() {
|
|
863
844
|
return `${Date.now().toString(36)}-${randomUUID()}`;
|
|
@@ -929,19 +910,25 @@ function buildImageContent(media) {
|
|
|
929
910
|
}
|
|
930
911
|
return { type: "image", mimeType: "image/url", data: media };
|
|
931
912
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
913
|
+
function buildUserMessage(request) {
|
|
914
|
+
if (request.media && request.media.length > 0) {
|
|
915
|
+
const blocks = [
|
|
916
|
+
createTextContent(request.message),
|
|
917
|
+
...request.media.filter(Boolean).map(buildImageContent)
|
|
918
|
+
];
|
|
919
|
+
return {
|
|
920
|
+
role: "user",
|
|
921
|
+
content: blocks,
|
|
922
|
+
timestamp: Date.now()
|
|
923
|
+
};
|
|
940
924
|
}
|
|
925
|
+
return {
|
|
926
|
+
role: "user",
|
|
927
|
+
content: request.message,
|
|
928
|
+
timestamp: Date.now()
|
|
929
|
+
};
|
|
941
930
|
}
|
|
942
|
-
|
|
943
|
-
return Math.ceil(text.length / 4);
|
|
944
|
-
}
|
|
931
|
+
var OVERFLOW_RECOVERY_LIMIT = 2;
|
|
945
932
|
var COMPACTION_SUMMARY_BUDGET_RATIO = 0.6;
|
|
946
933
|
var COMPACTION_LINE_CLAMP = 4e3;
|
|
947
934
|
var DEFAULT_COMPACTION_PROMPT = [
|
|
@@ -992,150 +979,146 @@ function messageToSummaryLine(msg) {
|
|
|
992
979
|
}
|
|
993
980
|
}
|
|
994
981
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
_extensionContext;
|
|
1015
|
-
_maxTurns;
|
|
1016
|
-
_toolTimeoutMs;
|
|
1017
|
-
_parallelToolCalls;
|
|
1018
|
-
_contextBudgetRatio;
|
|
1019
|
-
/** 内置摘要压缩配置(未配置 = 保持旧行为,仅硬截断兜底) */
|
|
1020
|
-
_compaction;
|
|
1021
|
-
_telemetry;
|
|
1022
|
-
/** 框架级工具权限策略(未配置 → 放行,向后兼容) */
|
|
1023
|
-
_permissionPolicy;
|
|
1024
|
-
/** 审批管理器(pending 决策挂起等待外部批准;未配置 → pending 视为 deny) */
|
|
1025
|
-
_approvals;
|
|
1026
|
-
/** 审批等待超时(毫秒) */
|
|
1027
|
-
_approvalTimeoutMs;
|
|
1028
|
-
/** traceId 生成器(测试可注入确定性 id) */
|
|
1029
|
-
_traceIdGenerator;
|
|
1030
|
-
constructor(options) {
|
|
1031
|
-
this._config = options.config ?? {};
|
|
1032
|
-
this._extensions = new ExtensionManager();
|
|
1033
|
-
this._hooks = this._extensions.getHooks();
|
|
1034
|
-
this._transformers = [...options.transformers ?? []];
|
|
1035
|
-
this._model = options.model ?? {
|
|
1036
|
-
id: "unknown",
|
|
1037
|
-
name: "unknown",
|
|
1038
|
-
provider: "unknown",
|
|
1039
|
-
contextWindow: 128e3,
|
|
1040
|
-
maxTokens: 8192,
|
|
1041
|
-
reasoning: false
|
|
1042
|
-
};
|
|
1043
|
-
this._streamFn = options.streamFn ?? (async function* () {
|
|
1044
|
-
throw new Error("streamFn \u672A\u8BBE\u7F6E\uFF0C\u8BF7\u901A\u8FC7 setStreamFn() \u6216 RuntimeOptions.streamFn \u63D0\u4F9B");
|
|
1045
|
-
});
|
|
1046
|
-
this._systemPrompt = options.systemPrompt ?? "";
|
|
1047
|
-
this._thinkingLevel = options.thinkingLevel ?? "off";
|
|
1048
|
-
this._sessionStorage = options.sessionStorage;
|
|
1049
|
-
this._maxTurns = options.maxTurns ?? 50;
|
|
1050
|
-
this._toolTimeoutMs = options.toolTimeoutMs ?? 12e4;
|
|
1051
|
-
this._parallelToolCalls = options.parallelToolCalls ?? true;
|
|
1052
|
-
this._contextBudgetRatio = options.contextBudgetRatio ?? 0.8;
|
|
1053
|
-
this._compaction = options.compaction;
|
|
1054
|
-
this._telemetry = options.telemetry;
|
|
1055
|
-
this._permissionPolicy = options.permissionPolicy;
|
|
1056
|
-
this._approvals = options.approvals;
|
|
1057
|
-
this._approvalTimeoutMs = options.approvalTimeoutMs ?? 3e5;
|
|
1058
|
-
this._traceIdGenerator = options.traceIdGenerator;
|
|
1059
|
-
if (options.tools) {
|
|
1060
|
-
for (const tool of options.tools) {
|
|
1061
|
-
this._globalTools.set(tool.name, tool);
|
|
982
|
+
function buildResult(compilation) {
|
|
983
|
+
const messages = compilation.messages;
|
|
984
|
+
let content = "";
|
|
985
|
+
let stopReason = "completed";
|
|
986
|
+
let error;
|
|
987
|
+
const toolsUsed = [];
|
|
988
|
+
const usage = {};
|
|
989
|
+
for (const msg of messages) {
|
|
990
|
+
if (msg.role === "assistant") {
|
|
991
|
+
const assistant = msg;
|
|
992
|
+
content = extractText(assistant.content);
|
|
993
|
+
stopReason = assistant.stopReason ?? "completed";
|
|
994
|
+
error = assistant.errorMessage;
|
|
995
|
+
if (assistant.usage) {
|
|
996
|
+
usage.input = (usage.input ?? 0) + assistant.usage.input;
|
|
997
|
+
usage.output = (usage.output ?? 0) + assistant.usage.output;
|
|
998
|
+
usage.total = (usage.total ?? 0) + assistant.usage.total;
|
|
999
|
+
if (assistant.usage.cacheRead) usage.cacheRead = (usage.cacheRead ?? 0) + assistant.usage.cacheRead;
|
|
1000
|
+
if (assistant.usage.cacheWrite) usage.cacheWrite = (usage.cacheWrite ?? 0) + assistant.usage.cacheWrite;
|
|
1062
1001
|
}
|
|
1063
1002
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
static create(options = {}) {
|
|
1070
|
-
const runtime = new _AgentRuntime(options);
|
|
1071
|
-
if (options.extensions) {
|
|
1072
|
-
runtime._extensions.registerAll(options.extensions);
|
|
1003
|
+
if (msg.role === "toolResult") {
|
|
1004
|
+
const toolMsg = msg;
|
|
1005
|
+
if (!toolsUsed.includes(toolMsg.toolName)) {
|
|
1006
|
+
toolsUsed.push(toolMsg.toolName);
|
|
1007
|
+
}
|
|
1073
1008
|
}
|
|
1074
|
-
const ctx = {
|
|
1075
|
-
config: runtime._config,
|
|
1076
|
-
workspace: options.workspace ?? process.cwd(),
|
|
1077
|
-
sessionKey: runtime._sessionKey,
|
|
1078
|
-
shared: /* @__PURE__ */ new Map(),
|
|
1079
|
-
runtime
|
|
1080
|
-
};
|
|
1081
|
-
runtime._extensionContext = ctx;
|
|
1082
|
-
runtime._extensions.applyAll(ctx);
|
|
1083
|
-
return runtime;
|
|
1084
1009
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1010
|
+
const resources = messagesToResources(messages);
|
|
1011
|
+
const builder = new ResultBuilder().content(content).toolsUsed(toolsUsed).usage(usage).stopReason(stopReason).error(error).resources(resources).metadata("traceId", compilation.traceId);
|
|
1012
|
+
if (compilation.terminateReason) {
|
|
1013
|
+
builder.stopReason("terminated").metadata("terminateReason", compilation.terminateReason);
|
|
1088
1014
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1015
|
+
if (compilation.maxTurnsExhausted) {
|
|
1016
|
+
builder.stopReason("max_turns").metadata("maxTurns", true);
|
|
1091
1017
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1018
|
+
return builder.build();
|
|
1019
|
+
}
|
|
1020
|
+
function streamEventToChunk(event) {
|
|
1021
|
+
switch (event.type) {
|
|
1022
|
+
case "text_delta":
|
|
1023
|
+
return { type: "text", content: event.delta };
|
|
1024
|
+
case "thinking_delta":
|
|
1025
|
+
return { type: "thinking", content: event.delta };
|
|
1026
|
+
case "error":
|
|
1027
|
+
return {
|
|
1028
|
+
type: "error",
|
|
1029
|
+
content: event.message.errorMessage || "\u6A21\u578B\u8C03\u7528\u51FA\u9519"
|
|
1030
|
+
};
|
|
1031
|
+
default:
|
|
1032
|
+
return null;
|
|
1094
1033
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ../agent/runtime/session-store.ts
|
|
1037
|
+
var SessionStore = class {
|
|
1038
|
+
/** 多会话状态表:key = sessionKey。模型/工具/扩展/转换器等资源跨会话共享 */
|
|
1039
|
+
_sessions;
|
|
1040
|
+
/** 内存会话状态表 LRU 上限 */
|
|
1041
|
+
_maxSessions;
|
|
1042
|
+
/** 默认会话 key(常量 'default';请求未指定 sessionKey 时路由到此会话) */
|
|
1043
|
+
defaultKey;
|
|
1044
|
+
constructor(defaultKey, maxSessions = DEFAULT_MAX_SESSIONS) {
|
|
1045
|
+
this.defaultKey = defaultKey;
|
|
1046
|
+
this._maxSessions = maxSessions;
|
|
1047
|
+
this._sessions = /* @__PURE__ */ new Map();
|
|
1048
|
+
this._sessions.set(defaultKey, createSessionState());
|
|
1102
1049
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1050
|
+
/**
|
|
1051
|
+
* 获取(懒创建)会话状态。同一 Runtime 下不同 sessionKey 的消息历史、
|
|
1052
|
+
* 串行队列、abort 控制相互独立;共享模型/工具/扩展/转换器。
|
|
1053
|
+
* 超过 maxSessions 时淘汰最久未用的非活动会话(仅清内存态,不删存储)。
|
|
1054
|
+
*/
|
|
1055
|
+
ensure(key) {
|
|
1056
|
+
let session = this._sessions.get(key);
|
|
1057
|
+
if (session) {
|
|
1058
|
+
this._sessions.delete(key);
|
|
1059
|
+
this._sessions.set(key, session);
|
|
1060
|
+
return session;
|
|
1106
1061
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
return
|
|
1111
|
-
}
|
|
1112
|
-
setModel(model) {
|
|
1113
|
-
this._model = model;
|
|
1114
|
-
return this;
|
|
1062
|
+
session = createSessionState();
|
|
1063
|
+
this._sessions.set(key, session);
|
|
1064
|
+
this.evictIdleSessions();
|
|
1065
|
+
return session;
|
|
1115
1066
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
return this;
|
|
1067
|
+
/** 只读访问(不创建、不刷新 LRU;会话不存在返回 undefined) */
|
|
1068
|
+
peek(key) {
|
|
1069
|
+
return this._sessions.get(key);
|
|
1119
1070
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1071
|
+
/** LRU 淘汰:仅淘汰非活动(未运行、未排队)的最久未用会话 */
|
|
1072
|
+
evictIdleSessions() {
|
|
1073
|
+
if (this._sessions.size <= this._maxSessions) return;
|
|
1074
|
+
for (const [key, session] of this._sessions) {
|
|
1075
|
+
if (this._sessions.size <= this._maxSessions) break;
|
|
1076
|
+
if (session.isStreaming || session.lockHeld) continue;
|
|
1077
|
+
if (session.idleResolvers.length > 0) {
|
|
1078
|
+
const resolvers = session.idleResolvers.splice(0);
|
|
1079
|
+
for (const resolve of resolvers) resolve();
|
|
1080
|
+
}
|
|
1081
|
+
this._sessions.delete(key);
|
|
1082
|
+
}
|
|
1123
1083
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
return this;
|
|
1084
|
+
/** 当前活跃的会话 key 列表(含默认会话) */
|
|
1085
|
+
keys() {
|
|
1086
|
+
return Array.from(this._sessions.keys());
|
|
1127
1087
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
return this;
|
|
1088
|
+
/** 某会话是否存在(内存中) */
|
|
1089
|
+
has(key) {
|
|
1090
|
+
return this._sessions.has(key);
|
|
1131
1091
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1092
|
+
/**
|
|
1093
|
+
* 获取同一会话的执行锁:返回 release 函数,调用后释放。
|
|
1094
|
+
* 同一 sessionKey 的 run/stream 会串行执行,避免消息数组交错、
|
|
1095
|
+
* abortController 互相覆盖、hydrate 竞态。
|
|
1096
|
+
*/
|
|
1097
|
+
async acquire(session) {
|
|
1098
|
+
let release;
|
|
1099
|
+
const prev = session.queue;
|
|
1100
|
+
session.queue = new Promise((resolve) => {
|
|
1101
|
+
release = () => resolve();
|
|
1102
|
+
});
|
|
1103
|
+
await prev;
|
|
1104
|
+
session.lockHeld = true;
|
|
1105
|
+
return () => {
|
|
1106
|
+
session.lockHeld = false;
|
|
1107
|
+
release();
|
|
1108
|
+
};
|
|
1135
1109
|
}
|
|
1136
|
-
/**
|
|
1137
|
-
|
|
1138
|
-
|
|
1110
|
+
/** 标记会话空闲并唤醒所有 waitForIdle 等待者 */
|
|
1111
|
+
markIdle(session) {
|
|
1112
|
+
session.isStreaming = false;
|
|
1113
|
+
session.abortController = null;
|
|
1114
|
+
if (session.idleResolvers.length > 0) {
|
|
1115
|
+
const resolvers = session.idleResolvers.splice(0);
|
|
1116
|
+
for (const resolve of resolvers) resolve();
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/** 指定会话的消息列表深拷贝(避免外部直接修改会话内部状态;不存在返回空数组) */
|
|
1120
|
+
getMessagesCopy(key) {
|
|
1121
|
+
const session = this._sessions.get(key);
|
|
1139
1122
|
if (!session) return [];
|
|
1140
1123
|
const messages = session.messages;
|
|
1141
1124
|
try {
|
|
@@ -1144,372 +1127,253 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1144
1127
|
return JSON.parse(JSON.stringify(messages));
|
|
1145
1128
|
}
|
|
1146
1129
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1130
|
+
/**
|
|
1131
|
+
* 指定会话消息的只读视图(高频轮询用):仅浅拷贝数组(O(n) 引用,
|
|
1132
|
+
* 不做 structuredClone 深拷贝),消息对象与内部共享——调用方不得修改,
|
|
1133
|
+
* 需要可变副本时用 getMessagesCopy。
|
|
1134
|
+
*/
|
|
1135
|
+
peekMessages(key) {
|
|
1136
|
+
return this._sessions.get(key)?.messages.slice() ?? [];
|
|
1137
|
+
}
|
|
1138
|
+
/** 等待指定会话空闲(基于 promise,无轮询;会话不存在或非运行中直接返回) */
|
|
1139
|
+
async waitForIdle(key, timeoutMs) {
|
|
1140
|
+
const session = this._sessions.get(key);
|
|
1141
|
+
if (!session || !session.isStreaming) return;
|
|
1142
|
+
if (timeoutMs === void 0) {
|
|
1143
|
+
await new Promise((resolve) => {
|
|
1144
|
+
session.idleResolvers.push(resolve);
|
|
1145
|
+
});
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
await new Promise((resolve, reject) => {
|
|
1149
|
+
const resolver = () => {
|
|
1150
|
+
clearTimeout(timer);
|
|
1151
|
+
resolve();
|
|
1152
|
+
};
|
|
1153
|
+
const timer = setTimeout(() => {
|
|
1154
|
+
const i = session.idleResolvers.indexOf(resolver);
|
|
1155
|
+
if (i >= 0) session.idleResolvers.splice(i, 1);
|
|
1156
|
+
reject(new Error(
|
|
1157
|
+
`[Runtime] waitForIdle \u8D85\u65F6\uFF08${timeoutMs}ms\uFF09: ${key}`
|
|
1158
|
+
));
|
|
1159
|
+
}, timeoutMs);
|
|
1160
|
+
session.idleResolvers.push(resolver);
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
/** 清除指定会话消息(仅内存;下次 run 会从存储恢复) */
|
|
1164
|
+
clearMessages(key) {
|
|
1165
|
+
const session = this._sessions.get(key);
|
|
1166
|
+
if (!session) return;
|
|
1167
|
+
session.messages = [];
|
|
1168
|
+
session.hydrated = false;
|
|
1151
1169
|
}
|
|
1152
1170
|
/**
|
|
1153
|
-
*
|
|
1154
|
-
* 串行队列、abort 控制相互独立;共享模型/工具/扩展/转换器。
|
|
1155
|
-
* 超过 _maxSessions 时淘汰最久未用的非活动会话(仅清内存态,不删存储)。
|
|
1171
|
+
* 删除指定会话(仅内存态):等待在途任务完成后移除,返回被移除的会话。
|
|
1156
1172
|
*/
|
|
1157
|
-
|
|
1158
|
-
|
|
1173
|
+
async remove(key) {
|
|
1174
|
+
const session = this._sessions.get(key);
|
|
1159
1175
|
if (session) {
|
|
1176
|
+
await session.queue;
|
|
1160
1177
|
this._sessions.delete(key);
|
|
1161
|
-
this._sessions.set(key, session);
|
|
1162
|
-
return session;
|
|
1163
1178
|
}
|
|
1164
|
-
session = createSessionState();
|
|
1165
|
-
this._sessions.set(key, session);
|
|
1166
|
-
this.evictIdleSessions();
|
|
1167
1179
|
return session;
|
|
1168
1180
|
}
|
|
1169
|
-
/**
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
for (const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
this._sessions.delete(key);
|
|
1181
|
+
/** 关闭用:等待所有会话的在途任务完成后清空状态表 */
|
|
1182
|
+
async drainAndClear() {
|
|
1183
|
+
await Promise.allSettled(Array.from(this._sessions.values(), (s) => s.queue));
|
|
1184
|
+
for (const session of this._sessions.values()) {
|
|
1185
|
+
session.messages = [];
|
|
1186
|
+
session.hydrated = false;
|
|
1176
1187
|
}
|
|
1188
|
+
this._sessions.clear();
|
|
1177
1189
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
// ../agent/runtime/session-persistence.ts
|
|
1193
|
+
var SessionPersistence = class {
|
|
1194
|
+
constructor(_storage, _defaultModel, _store) {
|
|
1195
|
+
this._storage = _storage;
|
|
1196
|
+
this._defaultModel = _defaultModel;
|
|
1197
|
+
this._store = _store;
|
|
1198
|
+
}
|
|
1199
|
+
_storage;
|
|
1200
|
+
_defaultModel;
|
|
1201
|
+
_store;
|
|
1202
|
+
get storage() {
|
|
1203
|
+
return this._storage;
|
|
1181
1204
|
}
|
|
1182
|
-
/**
|
|
1183
|
-
|
|
1184
|
-
|
|
1205
|
+
/** 从存储懒加载会话(串行化后无竞态,每个会话仅恢复一次) */
|
|
1206
|
+
async hydrate(sessionKey, session) {
|
|
1207
|
+
if (!this._storage) return;
|
|
1208
|
+
if (session.hydrated) return;
|
|
1209
|
+
session.hydrated = true;
|
|
1210
|
+
const stored = await this._storage.load(sessionKey);
|
|
1211
|
+
if (!stored) return;
|
|
1212
|
+
session.messages = stored.messages;
|
|
1213
|
+
session.createdAt = stored.createdAt;
|
|
1185
1214
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
if (!
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
const traceId = this.newTraceId();
|
|
1208
|
-
const queuedAt = Date.now();
|
|
1209
|
-
await this.emitTelemetry("onRunStart", {
|
|
1210
|
-
traceId,
|
|
1211
|
-
sessionKey,
|
|
1212
|
-
request: finalRequest,
|
|
1213
|
-
queuedAt
|
|
1214
|
-
});
|
|
1215
|
-
const session = this.getSession(sessionKey);
|
|
1216
|
-
const release = await this.acquire(session);
|
|
1217
|
-
const queuedMs = Date.now() - queuedAt;
|
|
1215
|
+
/** 整体保存指定会话(ephemeral 跳过;失败不影响运行结果) */
|
|
1216
|
+
async persist(sessionKey, session) {
|
|
1217
|
+
if (!this._storage) return;
|
|
1218
|
+
const stored = {
|
|
1219
|
+
key: sessionKey,
|
|
1220
|
+
version: SESSION_VERSION,
|
|
1221
|
+
messages: session.messages,
|
|
1222
|
+
model: this.deriveModel(session.messages),
|
|
1223
|
+
usage: this.sumUsage(session.messages),
|
|
1224
|
+
createdAt: session.createdAt,
|
|
1225
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1226
|
+
};
|
|
1227
|
+
await this._storage.save(sessionKey, stored);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* 实时持久化指定会话:每轮 assistant 回复/工具结果完成后调用,
|
|
1231
|
+
* 让运行中的会话随时可被持久化数据观测到。ephemeral 跳过;
|
|
1232
|
+
* 存储失败仅告警,不影响对话循环继续。
|
|
1233
|
+
*/
|
|
1234
|
+
async persistSafe(request, sessionKey) {
|
|
1235
|
+
if (request.ephemeral || !this._storage) return;
|
|
1218
1236
|
try {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
);
|
|
1224
|
-
} finally {
|
|
1225
|
-
release();
|
|
1237
|
+
const session = this._store.peek(sessionKey);
|
|
1238
|
+
if (!session) return;
|
|
1239
|
+
await this.persist(sessionKey, session);
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
console.warn("[Runtime] \u4F1A\u8BDD\u6301\u4E45\u5316\u5931\u8D25:", err?.message);
|
|
1226
1242
|
}
|
|
1227
1243
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
queuedMs: 0,
|
|
1239
|
-
turnCount: 0,
|
|
1240
|
-
result: new ResultBuilder().error(message).build(),
|
|
1241
|
-
success: false,
|
|
1242
|
-
errorClass: "validation",
|
|
1243
|
-
tokens: { input: 0, output: 0 }
|
|
1244
|
-
});
|
|
1245
|
-
yield { type: "error", content: message };
|
|
1246
|
-
yield { type: "done" };
|
|
1247
|
-
return;
|
|
1244
|
+
/** 从消息中推导最后使用的模型 */
|
|
1245
|
+
deriveModel(messages) {
|
|
1246
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1247
|
+
const msg = messages[i];
|
|
1248
|
+
if (msg.role === "assistant" && msg.model) {
|
|
1249
|
+
return {
|
|
1250
|
+
provider: msg.provider ?? this._defaultModel().provider,
|
|
1251
|
+
modelId: msg.model
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1248
1254
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
() => this._stream(finalRequest, sessionKey, session, traceId, queuedMs)
|
|
1267
|
-
);
|
|
1268
|
-
} finally {
|
|
1269
|
-
release();
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
/** 汇总所有 assistant 消息的 token 用量(含 cache) */
|
|
1258
|
+
sumUsage(messages) {
|
|
1259
|
+
const usage = createEmptyUsage();
|
|
1260
|
+
for (const msg of messages) {
|
|
1261
|
+
if (msg.role === "assistant") {
|
|
1262
|
+
const u = msg.usage;
|
|
1263
|
+
if (u) {
|
|
1264
|
+
usage.input += u.input;
|
|
1265
|
+
usage.output += u.output;
|
|
1266
|
+
usage.total += u.total;
|
|
1267
|
+
usage.cacheRead = (usage.cacheRead ?? 0) + (u.cacheRead ?? 0);
|
|
1268
|
+
usage.cacheWrite = (usage.cacheWrite ?? 0) + (u.cacheWrite ?? 0);
|
|
1269
|
+
usage.reasoning = (usage.reasoning ?? 0) + (u.reasoning ?? 0);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1270
1272
|
}
|
|
1273
|
+
return usage;
|
|
1271
1274
|
}
|
|
1272
1275
|
/**
|
|
1273
1276
|
* 非 ephemeral 请求在"读(load)-改(run)-写(save)"全程持有存储级锁,
|
|
1274
1277
|
* 防止多进程并发写同一会话导致 last-write-wins 丢消息。
|
|
1275
1278
|
* ephemeral / 无锁支持 / 无存储时直接执行。
|
|
1276
1279
|
*/
|
|
1277
|
-
async
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
return storage.withLock(sessionKey, fn);
|
|
1280
|
+
async withStorageLock(request, sessionKey, fn) {
|
|
1281
|
+
if (request.ephemeral || !this._storage?.withLock) return fn();
|
|
1282
|
+
return this._storage.withLock(sessionKey, fn);
|
|
1281
1283
|
}
|
|
1282
1284
|
/** 流式版本:无法用回调包住生成器,改用手动锁(acquire/release) */
|
|
1283
1285
|
async *streamWithStorageLock(request, sessionKey, gen) {
|
|
1284
|
-
|
|
1285
|
-
if (request.ephemeral || !storage?.acquireLock) {
|
|
1286
|
+
if (request.ephemeral || !this._storage?.acquireLock) {
|
|
1286
1287
|
yield* gen();
|
|
1287
1288
|
return;
|
|
1288
1289
|
}
|
|
1289
|
-
const lock = await
|
|
1290
|
+
const lock = await this._storage.acquireLock(sessionKey);
|
|
1290
1291
|
try {
|
|
1291
1292
|
yield* gen();
|
|
1292
1293
|
} finally {
|
|
1293
1294
|
await lock.release();
|
|
1294
1295
|
}
|
|
1295
1296
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
// ../agent/runtime/telemetry.ts
|
|
1300
|
+
var RuntimeTelemetry = class {
|
|
1301
|
+
constructor(_telemetry, _traceIdGenerator) {
|
|
1302
|
+
this._telemetry = _telemetry;
|
|
1303
|
+
this._traceIdGenerator = _traceIdGenerator;
|
|
1304
|
+
}
|
|
1305
|
+
_telemetry;
|
|
1306
|
+
_traceIdGenerator;
|
|
1307
|
+
/** traceId 生成:优先用注入的生成器(测试可确定性) */
|
|
1308
|
+
newTraceId() {
|
|
1309
|
+
return this._traceIdGenerator ? this._traceIdGenerator() : newTraceId();
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* 触发遥测回调。全可选、失败不阻断主流程。
|
|
1313
|
+
*/
|
|
1314
|
+
async emit(event, info) {
|
|
1315
|
+
const fn = this._telemetry?.[event];
|
|
1316
|
+
if (!fn) return;
|
|
1306
1317
|
try {
|
|
1307
|
-
await
|
|
1308
|
-
const result = this.buildResult(compilation);
|
|
1309
|
-
await this._hooks.beforeEmit.promise(result);
|
|
1310
|
-
await this._hooks.afterEmit.promise(result);
|
|
1311
|
-
await this._hooks.done.promise(result, finalRequest);
|
|
1312
|
-
compilation.completed = true;
|
|
1313
|
-
await this.emitRunEnd(finalRequest, sessionKey, compilation, result, queuedMs, activeStartedAt);
|
|
1314
|
-
return result;
|
|
1318
|
+
await Promise.resolve(fn(info));
|
|
1315
1319
|
} catch (err) {
|
|
1316
|
-
|
|
1317
|
-
if (error?.name !== "AbortError") {
|
|
1318
|
-
console.error("[Runtime] \u8FD0\u884C\u5931\u8D25:", error?.stack ?? error);
|
|
1319
|
-
}
|
|
1320
|
-
await this._hooks.failed.promise(error, finalRequest);
|
|
1321
|
-
const result = new ResultBuilder().error(error.message).build();
|
|
1322
|
-
await this.emitRunEnd(finalRequest, sessionKey, compilation, result, queuedMs, activeStartedAt);
|
|
1323
|
-
return result;
|
|
1324
|
-
} finally {
|
|
1325
|
-
await this.persistSessionSafe(finalRequest, sessionKey);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
async *_stream(request, sessionKey, session, traceId, queuedMs) {
|
|
1329
|
-
const activeStartedAt = Date.now();
|
|
1330
|
-
await this._hooks.beforeInitialize.promise(request);
|
|
1331
|
-
await this._hooks.afterInitialize.promise(request);
|
|
1332
|
-
const finalRequest = await this._hooks.beforeRun.promise(request);
|
|
1333
|
-
if (!finalRequest.ephemeral) {
|
|
1334
|
-
await this.hydrateSession(sessionKey, session);
|
|
1335
|
-
}
|
|
1336
|
-
const compilation = this.createCompilation(finalRequest, sessionKey, session, traceId);
|
|
1337
|
-
compilation.messages.push(this.buildUserMessage(finalRequest));
|
|
1338
|
-
try {
|
|
1339
|
-
for await (const chunk of this.runLoopStream(compilation, finalRequest, session)) {
|
|
1340
|
-
yield chunk;
|
|
1341
|
-
}
|
|
1342
|
-
const result = this.buildResult(compilation);
|
|
1343
|
-
await this._hooks.beforeEmit.promise(result);
|
|
1344
|
-
await this._hooks.afterEmit.promise(result);
|
|
1345
|
-
await this._hooks.done.promise(result, finalRequest);
|
|
1346
|
-
yield { type: "done", result };
|
|
1347
|
-
await this.emitRunEnd(finalRequest, sessionKey, compilation, result, queuedMs, activeStartedAt);
|
|
1348
|
-
} catch (err) {
|
|
1349
|
-
const error = err;
|
|
1350
|
-
if (error?.name !== "AbortError") {
|
|
1351
|
-
console.error("[Runtime] \u6D41\u5F0F\u8FD0\u884C\u5931\u8D25:", error?.stack ?? error);
|
|
1352
|
-
}
|
|
1353
|
-
await this._hooks.failed.promise(error, finalRequest);
|
|
1354
|
-
const result = new ResultBuilder().error(error.message).build();
|
|
1355
|
-
yield { type: "error", content: error.message };
|
|
1356
|
-
yield { type: "done", result };
|
|
1357
|
-
await this.emitRunEnd(finalRequest, sessionKey, compilation, result, queuedMs, activeStartedAt);
|
|
1358
|
-
} finally {
|
|
1359
|
-
await this.persistSessionSafe(finalRequest, sessionKey);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
createCompilation(request, sessionKey, session, traceId) {
|
|
1363
|
-
return {
|
|
1364
|
-
request,
|
|
1365
|
-
graph: createTaskGraph(),
|
|
1366
|
-
resources: [],
|
|
1367
|
-
messages: (session ?? this.getSession(sessionKey ?? this._sessionKey)).messages,
|
|
1368
|
-
completed: false,
|
|
1369
|
-
traceId: traceId ?? this.newTraceId()
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
async close() {
|
|
1373
|
-
await Promise.allSettled(Array.from(this._sessions.values(), (s) => s.queue));
|
|
1374
|
-
for (const session of this._sessions.values()) {
|
|
1375
|
-
session.messages = [];
|
|
1376
|
-
session.hydrated = false;
|
|
1320
|
+
console.warn(`[aipack] telemetry "${String(event)}" \u4E0A\u62A5\u5931\u8D25:`, err);
|
|
1377
1321
|
}
|
|
1378
|
-
this._sessions.clear();
|
|
1379
|
-
this._extensions.clear();
|
|
1380
|
-
this._transformers = [];
|
|
1381
1322
|
}
|
|
1382
|
-
|
|
1383
|
-
async
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
const toolCalls = extractToolCalls(assistantMessage.content);
|
|
1406
|
-
if (toolCalls.length === 0) {
|
|
1407
|
-
break;
|
|
1408
|
-
}
|
|
1409
|
-
const outcome = await this.executeToolCalls(
|
|
1410
|
-
compilation,
|
|
1411
|
-
toolCalls,
|
|
1412
|
-
session.abortController.signal
|
|
1413
|
-
);
|
|
1414
|
-
await this.persistSessionSafe(request, sessionKey);
|
|
1415
|
-
if (outcome.terminate) {
|
|
1416
|
-
compilation.terminateReason = outcome.terminateReason ?? "terminated";
|
|
1417
|
-
break;
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
if (maxTurns < 0) compilation.maxTurnsExhausted = true;
|
|
1421
|
-
compilation.turnCount = turnCount;
|
|
1422
|
-
} finally {
|
|
1423
|
-
this.markIdle(session);
|
|
1424
|
-
}
|
|
1323
|
+
/** 组装并上报 run 级完成事件(run/stream 内部统一调用) */
|
|
1324
|
+
async emitRunEnd(request, sessionKey, compilation, result, queuedMs, activeStartedAt, defaultModelId) {
|
|
1325
|
+
const activeMs = Date.now() - activeStartedAt;
|
|
1326
|
+
await this.emit("onRunEnd", {
|
|
1327
|
+
traceId: compilation.traceId,
|
|
1328
|
+
sessionKey,
|
|
1329
|
+
// 请求未显式指定 model 时补实际模型(模型排行按 run 级 requests 统计,缺省会落入 'unknown')
|
|
1330
|
+
request: request.model ? request : { ...request, model: defaultModelId },
|
|
1331
|
+
durationMs: activeMs + queuedMs,
|
|
1332
|
+
activeMs,
|
|
1333
|
+
queuedMs,
|
|
1334
|
+
turnCount: compilation.turnCount ?? 0,
|
|
1335
|
+
result,
|
|
1336
|
+
success: result.success,
|
|
1337
|
+
errorClass: this.runErrorClass(compilation),
|
|
1338
|
+
tokens: {
|
|
1339
|
+
input: result.usage.input ?? 0,
|
|
1340
|
+
output: result.usage.output ?? 0,
|
|
1341
|
+
cacheRead: result.usage.cacheRead,
|
|
1342
|
+
cacheWrite: result.usage.cacheWrite
|
|
1343
|
+
},
|
|
1344
|
+
ttftMs: compilation.ttftMs
|
|
1345
|
+
});
|
|
1425
1346
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
await this.transformMessages(compilation, sessionKey);
|
|
1437
|
-
await this.maybeCompactByThreshold(
|
|
1438
|
-
compilation,
|
|
1439
|
-
sessionKey,
|
|
1440
|
-
session.abortController.signal
|
|
1441
|
-
);
|
|
1442
|
-
const turn = this.modelTurnWithRecovery(
|
|
1443
|
-
compilation,
|
|
1444
|
-
session.abortController.signal,
|
|
1445
|
-
sessionKey,
|
|
1446
|
-
true
|
|
1447
|
-
// stream 模式:同时统计首 token 延迟
|
|
1448
|
-
);
|
|
1449
|
-
let turnResult = await turn.next();
|
|
1450
|
-
while (!turnResult.done) {
|
|
1451
|
-
yield turnResult.value;
|
|
1452
|
-
turnResult = await turn.next();
|
|
1453
|
-
}
|
|
1454
|
-
const assistantMessage = turnResult.value;
|
|
1455
|
-
if (!assistantMessage) break;
|
|
1456
|
-
compilation.messages.push(assistantMessage);
|
|
1457
|
-
await this.persistSessionSafe(request, sessionKey);
|
|
1458
|
-
const toolCalls = extractToolCalls(assistantMessage.content);
|
|
1459
|
-
if (toolCalls.length === 0) {
|
|
1460
|
-
break;
|
|
1461
|
-
}
|
|
1462
|
-
for (const toolCall of toolCalls) {
|
|
1463
|
-
yield {
|
|
1464
|
-
type: "tool_start",
|
|
1465
|
-
toolName: toolCall.name,
|
|
1466
|
-
toolCallId: toolCall.id
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
const outcome = await this.executeToolCalls(
|
|
1470
|
-
compilation,
|
|
1471
|
-
toolCalls,
|
|
1472
|
-
session.abortController.signal
|
|
1473
|
-
);
|
|
1474
|
-
await this.persistSessionSafe(request, sessionKey);
|
|
1475
|
-
for (let i = 0; i < toolCalls.length; i++) {
|
|
1476
|
-
yield {
|
|
1477
|
-
type: "tool_end",
|
|
1478
|
-
toolName: toolCalls[i].name,
|
|
1479
|
-
toolCallId: toolCalls[i].id,
|
|
1480
|
-
isError: this.isErrorResult(outcome.results[i])
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
if (outcome.terminate) {
|
|
1484
|
-
compilation.terminateReason = outcome.terminateReason ?? "terminated";
|
|
1485
|
-
break;
|
|
1486
|
-
}
|
|
1347
|
+
/** run 级错误分类:terminate → 'terminated';否则只看最后一条 assistant 消息(与 buildResult 的 result.success 同口径) */
|
|
1348
|
+
runErrorClass(compilation) {
|
|
1349
|
+
if (compilation.terminateReason) return "terminated";
|
|
1350
|
+
const messages = compilation.messages;
|
|
1351
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1352
|
+
const m = messages[i];
|
|
1353
|
+
if (m.role === "assistant") {
|
|
1354
|
+
if (!m.errorMessage) return void 0;
|
|
1355
|
+
const cls = errorClassFromMessage(m.errorMessage);
|
|
1356
|
+
return cls ?? "unknown";
|
|
1487
1357
|
}
|
|
1488
|
-
if (maxTurns < 0) compilation.maxTurnsExhausted = true;
|
|
1489
|
-
compilation.turnCount = turnCount;
|
|
1490
|
-
} finally {
|
|
1491
|
-
this.markIdle(session);
|
|
1492
1358
|
}
|
|
1359
|
+
return void 0;
|
|
1493
1360
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
this.buildToolResultMessage(toolCalls[i], outcome.results[i])
|
|
1501
|
-
);
|
|
1502
|
-
}
|
|
1503
|
-
return outcome;
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
// ../agent/runtime/tool-executor.ts
|
|
1364
|
+
var ToolExecutor = class {
|
|
1365
|
+
constructor(_d) {
|
|
1366
|
+
this._d = _d;
|
|
1504
1367
|
}
|
|
1368
|
+
_d;
|
|
1505
1369
|
/**
|
|
1506
1370
|
* 执行一组工具调用:parallelToolCalls 为 true 时并行,否则串行。
|
|
1507
1371
|
* 返回 ToolExecutionOutcome:任一工具请求 terminate 即终止整个 run;
|
|
1508
1372
|
* 串行模式下 terminate 后剩余工具生成 skipped 结果以保持配对完整。
|
|
1509
1373
|
*/
|
|
1510
|
-
async
|
|
1511
|
-
const execute = (tc) => this.
|
|
1512
|
-
if (this.
|
|
1374
|
+
async run(toolCalls, signal, request, traceId) {
|
|
1375
|
+
const execute = (tc) => this.execute(tc, signal, request, traceId);
|
|
1376
|
+
if (this._d.parallelToolCalls && toolCalls.length > 1) {
|
|
1513
1377
|
const outcomes = await Promise.all(toolCalls.map(execute));
|
|
1514
1378
|
const terminated = outcomes.find((o) => o.terminate);
|
|
1515
1379
|
return {
|
|
@@ -1535,8 +1399,8 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1535
1399
|
}
|
|
1536
1400
|
return { results, terminate, terminateReason };
|
|
1537
1401
|
}
|
|
1538
|
-
async
|
|
1539
|
-
const tool = this.
|
|
1402
|
+
async execute(toolCall, signal, request, traceId) {
|
|
1403
|
+
const tool = this._d.getTools().get(toolCall.name);
|
|
1540
1404
|
if (!tool) {
|
|
1541
1405
|
return {
|
|
1542
1406
|
result: {
|
|
@@ -1550,27 +1414,43 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1550
1414
|
if (tool.prepareArguments) {
|
|
1551
1415
|
args = tool.prepareArguments(toolCall.arguments);
|
|
1552
1416
|
}
|
|
1553
|
-
if (this.
|
|
1417
|
+
if (!this._d.permissionPolicy) {
|
|
1418
|
+
if (!this._d.permissionFailOpen) {
|
|
1419
|
+
const reason = `no permissionPolicy configured (fail-closed by default). Configure RuntimeOptions.permissionPolicy (e.g. createAllowAllPolicy()) or explicitly set permissionFailOpen: true to allow unrestricted tool execution`;
|
|
1420
|
+
await this._d.telemetry.emit("onPermissionDenied", {
|
|
1421
|
+
traceId,
|
|
1422
|
+
sessionKey: request.sessionKey ?? this._d.defaultSessionKey,
|
|
1423
|
+
toolName: toolCall.name,
|
|
1424
|
+
permissions: tool.permissions ?? [],
|
|
1425
|
+
args,
|
|
1426
|
+
reason: `permission denied for tool "${toolCall.name}": ${reason}`
|
|
1427
|
+
});
|
|
1428
|
+
return {
|
|
1429
|
+
result: this.makeBlockedResult(`permission denied for tool "${toolCall.name}": ${reason}`),
|
|
1430
|
+
terminate: false
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
} else {
|
|
1554
1434
|
const permissionReq = {
|
|
1555
1435
|
toolName: toolCall.name,
|
|
1556
1436
|
permissions: tool.permissions ?? [],
|
|
1557
1437
|
args,
|
|
1558
|
-
sessionKey: request.sessionKey ?? this.
|
|
1438
|
+
sessionKey: request.sessionKey ?? this._d.defaultSessionKey,
|
|
1559
1439
|
request,
|
|
1560
|
-
shared: this.
|
|
1440
|
+
shared: this._d.getShared() ?? /* @__PURE__ */ new Map()
|
|
1561
1441
|
};
|
|
1562
|
-
const decision = await this.
|
|
1442
|
+
const decision = await this._d.permissionPolicy.check(permissionReq);
|
|
1563
1443
|
let allowed = decision === "allow";
|
|
1564
1444
|
if (decision === "confirm") {
|
|
1565
|
-
allowed = this.
|
|
1445
|
+
allowed = this._d.permissionPolicy.confirm ? await this._d.permissionPolicy.confirm(permissionReq) : false;
|
|
1566
1446
|
}
|
|
1567
1447
|
if (decision === "pending") {
|
|
1568
|
-
if (this.
|
|
1569
|
-
const approval = this.
|
|
1570
|
-
timeoutMs: this.
|
|
1448
|
+
if (this._d.approvals) {
|
|
1449
|
+
const approval = this._d.approvals.create(permissionReq, {
|
|
1450
|
+
timeoutMs: this._d.approvalTimeoutMs,
|
|
1571
1451
|
signal
|
|
1572
1452
|
});
|
|
1573
|
-
await this.
|
|
1453
|
+
await this._d.telemetry.emit("onApprovalPending", {
|
|
1574
1454
|
traceId,
|
|
1575
1455
|
sessionKey: permissionReq.sessionKey,
|
|
1576
1456
|
approvalId: approval.id,
|
|
@@ -1579,9 +1459,9 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1579
1459
|
args,
|
|
1580
1460
|
expiresAt: approval.expiresAt
|
|
1581
1461
|
});
|
|
1582
|
-
const outcome = await this.
|
|
1462
|
+
const outcome = await this._d.approvals.wait(approval);
|
|
1583
1463
|
allowed = outcome.status === "approved";
|
|
1584
|
-
await this.
|
|
1464
|
+
await this._d.telemetry.emit("onApprovalResolved", {
|
|
1585
1465
|
traceId,
|
|
1586
1466
|
sessionKey: permissionReq.sessionKey,
|
|
1587
1467
|
approvalId: approval.id,
|
|
@@ -1595,7 +1475,7 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1595
1475
|
}
|
|
1596
1476
|
if (!allowed) {
|
|
1597
1477
|
const reason = `permission denied by policy for tool "${toolCall.name}"`;
|
|
1598
|
-
await this.
|
|
1478
|
+
await this._d.telemetry.emit("onPermissionDenied", {
|
|
1599
1479
|
traceId,
|
|
1600
1480
|
sessionKey: permissionReq.sessionKey,
|
|
1601
1481
|
toolName: toolCall.name,
|
|
@@ -1609,10 +1489,10 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1609
1489
|
};
|
|
1610
1490
|
}
|
|
1611
1491
|
}
|
|
1612
|
-
const { signal: timedSignal, clear } = withTimeoutSignal(signal, this.
|
|
1492
|
+
const { signal: timedSignal, clear } = withTimeoutSignal(signal, this._d.toolTimeoutMs);
|
|
1613
1493
|
try {
|
|
1614
1494
|
const beforeCtx = this.buildToolCallContext(toolCall, tool, args, request, timedSignal);
|
|
1615
|
-
const before = await this.
|
|
1495
|
+
const before = await this._d.hooks.beforeToolCall.promise(
|
|
1616
1496
|
{ block: false, terminate: false, args },
|
|
1617
1497
|
beforeCtx
|
|
1618
1498
|
);
|
|
@@ -1644,10 +1524,10 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1644
1524
|
};
|
|
1645
1525
|
}
|
|
1646
1526
|
const status = toolResultStatus(result);
|
|
1647
|
-
await this.
|
|
1527
|
+
await this._d.telemetry.emit("onToolCall", {
|
|
1648
1528
|
traceId,
|
|
1649
1529
|
spanId: newSpanId(),
|
|
1650
|
-
sessionKey: request.sessionKey ?? this.
|
|
1530
|
+
sessionKey: request.sessionKey ?? this._d.defaultSessionKey,
|
|
1651
1531
|
toolName: toolCall.name,
|
|
1652
1532
|
args,
|
|
1653
1533
|
durationMs: Date.now() - toolStartedAt,
|
|
@@ -1657,7 +1537,7 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1657
1537
|
errorClass: status === "error" ? errorClassFromMessage(String(result.details?.error ?? "")) ?? "tool_error" : void 0
|
|
1658
1538
|
});
|
|
1659
1539
|
const afterCtx = this.buildToolCallContext(toolCall, tool, args, request, timedSignal);
|
|
1660
|
-
const after = await this.
|
|
1540
|
+
const after = await this._d.hooks.afterToolCall.promise(
|
|
1661
1541
|
{ result, terminate: false },
|
|
1662
1542
|
afterCtx
|
|
1663
1543
|
);
|
|
@@ -1693,13 +1573,14 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1693
1573
|
toolCall,
|
|
1694
1574
|
tool,
|
|
1695
1575
|
args,
|
|
1696
|
-
sessionKey: request.sessionKey ?? this.
|
|
1576
|
+
sessionKey: request.sessionKey ?? this._d.defaultSessionKey,
|
|
1697
1577
|
request,
|
|
1698
|
-
shared: this.
|
|
1578
|
+
shared: this._d.getShared() ?? /* @__PURE__ */ new Map(),
|
|
1699
1579
|
signal
|
|
1700
1580
|
};
|
|
1701
1581
|
}
|
|
1702
|
-
|
|
1582
|
+
/** 工具结果 → toolResult 消息(写入会话历史) */
|
|
1583
|
+
buildResultMessage(toolCall, result) {
|
|
1703
1584
|
return {
|
|
1704
1585
|
role: "toolResult",
|
|
1705
1586
|
content: result.content,
|
|
@@ -1709,294 +1590,69 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
1709
1590
|
timestamp: Date.now()
|
|
1710
1591
|
};
|
|
1711
1592
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
// ../agent/transformer/index.ts
|
|
1596
|
+
function ensureToolPairing(messages) {
|
|
1597
|
+
const toolCallIds = /* @__PURE__ */ new Set();
|
|
1598
|
+
const toolResultIds = /* @__PURE__ */ new Set();
|
|
1599
|
+
for (const msg of messages) {
|
|
1600
|
+
if (msg.role === "assistant") {
|
|
1601
|
+
const content = msg.content;
|
|
1602
|
+
if (Array.isArray(content)) {
|
|
1603
|
+
for (const tc of extractToolCalls(content)) {
|
|
1604
|
+
toolCallIds.add(tc.id);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1723
1607
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
content: request.message,
|
|
1727
|
-
timestamp: Date.now()
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
// ─── 内部方法 ───────────────────────────────────────────────────
|
|
1731
|
-
/**
|
|
1732
|
-
* 获取同一会话的执行锁:返回 release 函数,调用后释放。
|
|
1733
|
-
* 同一 sessionKey 的 run/stream 会串行执行,避免消息数组交错、
|
|
1734
|
-
* abortController 互相覆盖、hydrate 竞态。
|
|
1735
|
-
*/
|
|
1736
|
-
async acquire(session) {
|
|
1737
|
-
let release;
|
|
1738
|
-
const prev = session.queue;
|
|
1739
|
-
session.queue = new Promise((resolve) => {
|
|
1740
|
-
release = () => resolve();
|
|
1741
|
-
});
|
|
1742
|
-
await prev;
|
|
1743
|
-
session.lockHeld = true;
|
|
1744
|
-
return () => {
|
|
1745
|
-
session.lockHeld = false;
|
|
1746
|
-
release();
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
/** 标记会话空闲并唤醒所有 waitForIdle 等待者 */
|
|
1750
|
-
markIdle(session) {
|
|
1751
|
-
session.isStreaming = false;
|
|
1752
|
-
session.abortController = null;
|
|
1753
|
-
if (session.idleResolvers.length > 0) {
|
|
1754
|
-
const resolvers = session.idleResolvers.splice(0);
|
|
1755
|
-
for (const resolve of resolvers) resolve();
|
|
1608
|
+
if (msg.role === "toolResult") {
|
|
1609
|
+
toolResultIds.add(msg.toolCallId);
|
|
1756
1610
|
}
|
|
1757
1611
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
if (!
|
|
1762
|
-
if (session.hydrated) return;
|
|
1763
|
-
session.hydrated = true;
|
|
1764
|
-
const stored = await this._sessionStorage.load(sessionKey);
|
|
1765
|
-
if (!stored) return;
|
|
1766
|
-
session.messages = stored.messages;
|
|
1767
|
-
session.createdAt = stored.createdAt;
|
|
1612
|
+
const orphanedCalls = /* @__PURE__ */ new Set();
|
|
1613
|
+
const orphanedResults = /* @__PURE__ */ new Set();
|
|
1614
|
+
for (const id of toolCallIds) {
|
|
1615
|
+
if (!toolResultIds.has(id)) orphanedCalls.add(id);
|
|
1768
1616
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
if (!this._sessionStorage) return;
|
|
1772
|
-
const stored = {
|
|
1773
|
-
key: sessionKey,
|
|
1774
|
-
version: SESSION_VERSION,
|
|
1775
|
-
messages: session.messages,
|
|
1776
|
-
model: this.deriveModel(session.messages),
|
|
1777
|
-
usage: this.sumUsage(session.messages),
|
|
1778
|
-
createdAt: session.createdAt,
|
|
1779
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1780
|
-
};
|
|
1781
|
-
await this._sessionStorage.save(sessionKey, stored);
|
|
1617
|
+
for (const id of toolResultIds) {
|
|
1618
|
+
if (!toolCallIds.has(id)) orphanedResults.add(id);
|
|
1782
1619
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
* 让运行中的会话随时可被持久化数据观测到。ephemeral 跳过;
|
|
1786
|
-
* 存储失败仅告警,不影响对话循环继续。
|
|
1787
|
-
*/
|
|
1788
|
-
async persistSessionSafe(request, sessionKey) {
|
|
1789
|
-
if (request.ephemeral || !this._sessionStorage) return;
|
|
1790
|
-
try {
|
|
1791
|
-
const session = this._sessions.get(sessionKey);
|
|
1792
|
-
if (!session) return;
|
|
1793
|
-
await this.persistSession(sessionKey, session);
|
|
1794
|
-
} catch (err) {
|
|
1795
|
-
console.warn("[Runtime] \u4F1A\u8BDD\u6301\u4E45\u5316\u5931\u8D25:", err?.message);
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
/** 从消息中推导最后使用的模型 */
|
|
1799
|
-
deriveModel(messages) {
|
|
1800
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1801
|
-
const msg = messages[i];
|
|
1802
|
-
if (msg.role === "assistant" && msg.model) {
|
|
1803
|
-
return {
|
|
1804
|
-
provider: msg.provider ?? this._model.provider,
|
|
1805
|
-
modelId: msg.model
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
return null;
|
|
1810
|
-
}
|
|
1811
|
-
/** 汇总所有 assistant 消息的 token 用量(含 cache) */
|
|
1812
|
-
sumUsage(messages) {
|
|
1813
|
-
const usage = createEmptyUsage();
|
|
1814
|
-
for (const msg of messages) {
|
|
1815
|
-
if (msg.role === "assistant") {
|
|
1816
|
-
const u = msg.usage;
|
|
1817
|
-
if (u) {
|
|
1818
|
-
usage.input += u.input;
|
|
1819
|
-
usage.output += u.output;
|
|
1820
|
-
usage.total += u.total;
|
|
1821
|
-
usage.cacheRead = (usage.cacheRead ?? 0) + (u.cacheRead ?? 0);
|
|
1822
|
-
usage.cacheWrite = (usage.cacheWrite ?? 0) + (u.cacheWrite ?? 0);
|
|
1823
|
-
usage.reasoning = (usage.reasoning ?? 0) + (u.reasoning ?? 0);
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
return usage;
|
|
1828
|
-
}
|
|
1829
|
-
/** 链式执行上下文转换器(按数组顺序,上一个输出作为下一个输入) */
|
|
1830
|
-
async transformMessages(compilation, sessionKey) {
|
|
1831
|
-
if (this._transformers.length === 0) {
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
let resources = messagesToResources(compilation.messages);
|
|
1835
|
-
const context = {
|
|
1836
|
-
graph: compilation.graph,
|
|
1837
|
-
runtime: {
|
|
1838
|
-
sessionKey,
|
|
1839
|
-
turn: compilation.messages.length,
|
|
1840
|
-
contextWindow: this._model.contextWindow,
|
|
1841
|
-
maxTokens: this._model.maxTokens,
|
|
1842
|
-
contextBudgetRatio: this._contextBudgetRatio
|
|
1843
|
-
}
|
|
1844
|
-
};
|
|
1845
|
-
for (const transformer of this._transformers) {
|
|
1846
|
-
try {
|
|
1847
|
-
resources = await transformer.transform(resources, context);
|
|
1848
|
-
} catch (err) {
|
|
1849
|
-
console.warn(
|
|
1850
|
-
`[Runtime] \u8F6C\u6362\u5668 "${transformer.name}" \u5931\u8D25\uFF0C\u5DF2\u8DF3\u8FC7:`,
|
|
1851
|
-
err?.message ?? err
|
|
1852
|
-
);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
const messages = resourcesToMessages(resources);
|
|
1856
|
-
compilation.messages.splice(0, compilation.messages.length, ...messages);
|
|
1857
|
-
}
|
|
1858
|
-
buildContext(messages) {
|
|
1859
|
-
const tools = Array.from(this._globalTools.values());
|
|
1860
|
-
return {
|
|
1861
|
-
systemPrompt: this._systemPrompt,
|
|
1862
|
-
// compactionSummary / stateSnapshot 为内部扩展 role,provider 适配层
|
|
1863
|
-
// 仅支持 user/assistant/toolResult,发出前统一转为带标注的 user 消息
|
|
1864
|
-
messages: messages.filter((m) => m.role !== "system").map((m) => {
|
|
1865
|
-
const role = m.role;
|
|
1866
|
-
if (role === "compactionSummary") return this.compactionSummaryToUser(m);
|
|
1867
|
-
if (role === "stateSnapshot") return this.stateSnapshotToUser(m);
|
|
1868
|
-
return m;
|
|
1869
|
-
}),
|
|
1870
|
-
tools: tools.length > 0 ? tools : void 0
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
|
-
/** compactionSummary 消息 → user 消息(所有 provider 均兼容 user role) */
|
|
1874
|
-
compactionSummaryToUser(msg) {
|
|
1875
|
-
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
1876
|
-
return {
|
|
1877
|
-
role: "user",
|
|
1878
|
-
content: `${COMPACTION_USER_PREFIX}
|
|
1879
|
-
${text}`,
|
|
1880
|
-
timestamp: msg.timestamp
|
|
1881
|
-
};
|
|
1882
|
-
}
|
|
1883
|
-
/** stateSnapshot 消息 → user 消息(所有 provider 均兼容 user role) */
|
|
1884
|
-
stateSnapshotToUser(msg) {
|
|
1885
|
-
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
1886
|
-
return {
|
|
1887
|
-
role: "user",
|
|
1888
|
-
content: `${STATE_SNAPSHOT_USER_PREFIX}
|
|
1889
|
-
${text}`,
|
|
1890
|
-
timestamp: msg.timestamp
|
|
1891
|
-
};
|
|
1892
|
-
}
|
|
1893
|
-
async streamModel(compilation, signal, sessionKey) {
|
|
1894
|
-
const turn = this.modelTurnWithRecovery(compilation, signal, sessionKey, false);
|
|
1895
|
-
let r = await turn.next();
|
|
1896
|
-
while (!r.done) r = await turn.next();
|
|
1897
|
-
return r.value ?? this.emptyAssistantMessage();
|
|
1898
|
-
}
|
|
1899
|
-
/** 空的 assistant 消息(流异常中断无 done/error 事件时的兜底,与旧 streamModel 行为一致) */
|
|
1900
|
-
emptyAssistantMessage() {
|
|
1901
|
-
return {
|
|
1902
|
-
role: "assistant",
|
|
1903
|
-
content: [],
|
|
1904
|
-
stopReason: "stop",
|
|
1905
|
-
usage: createEmptyUsage(),
|
|
1906
|
-
model: this._model.id,
|
|
1907
|
-
provider: this._model.provider,
|
|
1908
|
-
timestamp: Date.now()
|
|
1909
|
-
};
|
|
1620
|
+
if (orphanedCalls.size === 0 && orphanedResults.size === 0) {
|
|
1621
|
+
return messages;
|
|
1910
1622
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
* - 显式错误 / 零产出截断溢出 / thinking 耗尽溢出 / 输出打满溢出:丢弃失败
|
|
1920
|
-
* 消息后重试;流式路径吞掉可恢复的 error chunk(消费者看不到瞬态错误),
|
|
1921
|
-
* 不可恢复时补发。
|
|
1922
|
-
* - 静默溢出(stop + 有完整产出):保留回复,仅压缩旧上下文供后续轮次。
|
|
1923
|
-
* - 恢复耗尽或单请求超窗(无可丢弃):返回最后一次错误消息,维持旧行为。
|
|
1924
|
-
*
|
|
1925
|
-
* 流式路径 yield 模型事件 chunk;非流式路径由 streamModel 消费(chunk 丢弃)。
|
|
1926
|
-
* 返回最终 assistant 消息;无 done/error 事件时返回 null。
|
|
1927
|
-
*/
|
|
1928
|
-
async *modelTurnWithRecovery(compilation, signal, sessionKey, stream) {
|
|
1929
|
-
const contextWindow = this._model.contextWindow;
|
|
1930
|
-
const maxTokens = this._model.maxTokens;
|
|
1931
|
-
let recoveries = 0;
|
|
1932
|
-
const toProbe = (m) => ({
|
|
1933
|
-
stopReason: m.stopReason,
|
|
1934
|
-
errorMessage: m.errorMessage,
|
|
1935
|
-
usage: m.usage,
|
|
1936
|
-
content: m.content,
|
|
1937
|
-
maxTokens
|
|
1938
|
-
});
|
|
1939
|
-
while (true) {
|
|
1940
|
-
let assistant = null;
|
|
1941
|
-
let suppressed = null;
|
|
1942
|
-
for await (const event of this.streamModelEvents(compilation, signal, stream)) {
|
|
1943
|
-
if (event.type === "error") {
|
|
1944
|
-
const msg = event.message;
|
|
1945
|
-
if (recoveries < OVERFLOW_RECOVERY_LIMIT && isContextOverflow(toProbe(msg), contextWindow)) {
|
|
1946
|
-
suppressed = msg;
|
|
1947
|
-
continue;
|
|
1623
|
+
const result = [];
|
|
1624
|
+
for (const msg of messages) {
|
|
1625
|
+
if (msg.role === "assistant") {
|
|
1626
|
+
const content = msg.content;
|
|
1627
|
+
if (Array.isArray(content)) {
|
|
1628
|
+
const filteredContent = content.filter((block) => {
|
|
1629
|
+
if (block.type === "toolCall") {
|
|
1630
|
+
return !orphanedCalls.has(block.id);
|
|
1948
1631
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
if (event.type === "done") assistant = event.message;
|
|
1956
|
-
}
|
|
1632
|
+
return true;
|
|
1633
|
+
});
|
|
1634
|
+
if (filteredContent.length === 0 && content.length > 0) continue;
|
|
1635
|
+
result.push({ ...msg, content: filteredContent });
|
|
1636
|
+
} else {
|
|
1637
|
+
result.push(msg);
|
|
1957
1638
|
}
|
|
1958
|
-
|
|
1959
|
-
if (!
|
|
1960
|
-
|
|
1961
|
-
const output = final.usage?.output ?? 0;
|
|
1962
|
-
const blocks = Array.isArray(final.content) ? final.content : null;
|
|
1963
|
-
const hasMeaningfulText = blocks?.some(
|
|
1964
|
-
(b) => b.type === "text" && "text" in b && typeof b.text === "string" && b.text.trim().length > 0
|
|
1965
|
-
);
|
|
1966
|
-
const hasToolCall = blocks?.some((b) => b.type === "toolCall");
|
|
1967
|
-
const thinkingOnly = blocks ? !hasMeaningfulText && !hasToolCall && blocks.some((b) => b.type === "thinking") : false;
|
|
1968
|
-
const outputFull = maxTokens > 0 && output >= Math.floor(maxTokens * 0.95);
|
|
1969
|
-
const failed = final.stopReason === "error" || output === 0 || thinkingOnly || outputFull;
|
|
1970
|
-
if (failed && recoveries < OVERFLOW_RECOVERY_LIMIT) {
|
|
1971
|
-
recoveries += 1;
|
|
1972
|
-
if (await this.recoverFromOverflow(compilation, recoveries, sessionKey, signal)) {
|
|
1973
|
-
const reason = thinkingOnly ? "thinking \u8017\u5C3D" : outputFull ? "output \u6253\u6EE1" : final.stopReason;
|
|
1974
|
-
console.warn(
|
|
1975
|
-
`[Runtime] \u4E0A\u4E0B\u6587\u6EA2\u51FA\uFF08${reason}\uFF09\uFF0C\u5DF2\u538B\u7F29\u5386\u53F2\u5E76\u540C\u56DE\u5408\u91CD\u8BD5\uFF08${recoveries}/${OVERFLOW_RECOVERY_LIMIT}\uFF09`
|
|
1976
|
-
);
|
|
1977
|
-
await this.emitTelemetry("onRetry", {
|
|
1978
|
-
traceId: compilation.traceId,
|
|
1979
|
-
provider: this._model.provider,
|
|
1980
|
-
modelId: this._model.id,
|
|
1981
|
-
attempt: recoveries,
|
|
1982
|
-
errorClass: "context-overflow",
|
|
1983
|
-
delayMs: 0,
|
|
1984
|
-
willRetry: true
|
|
1985
|
-
});
|
|
1986
|
-
continue;
|
|
1987
|
-
}
|
|
1988
|
-
if (suppressed) {
|
|
1989
|
-
yield { type: "error", content: final.errorMessage || "\u6A21\u578B\u8C03\u7528\u51FA\u9519" };
|
|
1990
|
-
}
|
|
1991
|
-
return final;
|
|
1992
|
-
}
|
|
1993
|
-
if (failed) return final;
|
|
1994
|
-
await this.recoverFromOverflow(compilation, 1, sessionKey, signal);
|
|
1995
|
-
console.warn("[Runtime] \u68C0\u6D4B\u5230\u9759\u9ED8\u4E0A\u4E0B\u6587\u6EA2\u51FA\uFF08usage \u8D85\u7A97\uFF09\uFF0C\u5DF2\u538B\u7F29\u5386\u53F2\u6D88\u606F");
|
|
1639
|
+
} else if (msg.role === "toolResult") {
|
|
1640
|
+
if (!orphanedResults.has(msg.toolCallId)) {
|
|
1641
|
+
result.push(msg);
|
|
1996
1642
|
}
|
|
1997
|
-
|
|
1643
|
+
} else {
|
|
1644
|
+
result.push(msg);
|
|
1998
1645
|
}
|
|
1999
1646
|
}
|
|
1647
|
+
return result;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// ../agent/runtime/compactor.ts
|
|
1651
|
+
var ContextCompactor = class {
|
|
1652
|
+
constructor(_d) {
|
|
1653
|
+
this._d = _d;
|
|
1654
|
+
}
|
|
1655
|
+
_d;
|
|
2000
1656
|
/**
|
|
2001
1657
|
* 计算溢出恢复的截断点:按 token 预算从最旧消息开始丢弃。
|
|
2002
1658
|
*
|
|
@@ -2007,10 +1663,10 @@ ${text}`,
|
|
|
2007
1663
|
* 最小集,单条请求即超窗,无法恢复。
|
|
2008
1664
|
*/
|
|
2009
1665
|
computeOverflowSplit(messages, recovery) {
|
|
2010
|
-
const contextWindow = this.
|
|
1666
|
+
const contextWindow = this._d.getModel().contextWindow;
|
|
2011
1667
|
if (!contextWindow || contextWindow <= 0) return 0;
|
|
2012
1668
|
const target = Math.max(
|
|
2013
|
-
Math.floor(contextWindow * this.
|
|
1669
|
+
Math.floor(contextWindow * this._d.contextBudgetRatio * 0.5 ** recovery),
|
|
2014
1670
|
1
|
|
2015
1671
|
);
|
|
2016
1672
|
let total = 0;
|
|
@@ -2038,7 +1694,7 @@ ${text}`,
|
|
|
2038
1694
|
const messages = compilation.messages;
|
|
2039
1695
|
const split = this.computeOverflowSplit(messages, recovery);
|
|
2040
1696
|
if (split <= 0) return false;
|
|
2041
|
-
const useCompaction = !!this.
|
|
1697
|
+
const useCompaction = !!this._d.compaction && this._d.compaction.enabled !== false && this._d.compaction.onOverflow !== false;
|
|
2042
1698
|
if (useCompaction) {
|
|
2043
1699
|
await this.compactOrTruncate(
|
|
2044
1700
|
messages,
|
|
@@ -2061,30 +1717,17 @@ ${text}`,
|
|
|
2061
1717
|
* 其余部分摘要替换;摘要失败降级硬截断。
|
|
2062
1718
|
*/
|
|
2063
1719
|
async maybeCompactByThreshold(compilation, sessionKey, signal) {
|
|
2064
|
-
if (!this.
|
|
2065
|
-
const contextWindow = this.
|
|
1720
|
+
if (!this._d.compaction || this._d.compaction.enabled === false) return;
|
|
1721
|
+
const contextWindow = this._d.getModel().contextWindow;
|
|
2066
1722
|
if (!contextWindow || contextWindow <= 0) return;
|
|
2067
1723
|
const messages = compilation.messages;
|
|
2068
1724
|
const triggerTokens = Math.floor(
|
|
2069
|
-
contextWindow * (this.
|
|
1725
|
+
contextWindow * (this._d.compaction?.triggerRatio ?? this._d.contextBudgetRatio)
|
|
2070
1726
|
);
|
|
2071
1727
|
let total = 0;
|
|
2072
1728
|
for (const m of messages) total += estimateMessageTokens(m);
|
|
2073
1729
|
if (total <= triggerTokens) return;
|
|
2074
|
-
const
|
|
2075
|
-
contextWindow * (this._compaction?.targetRatio ?? 0.5)
|
|
2076
|
-
);
|
|
2077
|
-
const keepTokens = Math.max(Math.floor(targetTokens / 2), 1);
|
|
2078
|
-
let kept = 0;
|
|
2079
|
-
let split = messages.length;
|
|
2080
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2081
|
-
if (kept >= keepTokens) {
|
|
2082
|
-
split = i + 1;
|
|
2083
|
-
break;
|
|
2084
|
-
}
|
|
2085
|
-
kept += estimateMessageTokens(messages[i]);
|
|
2086
|
-
split = i;
|
|
2087
|
-
}
|
|
1730
|
+
const split = this.computeKeepSplit(messages, contextWindow);
|
|
2088
1731
|
if (split <= 0 || split >= messages.length) return;
|
|
2089
1732
|
const mode = await this.compactOrTruncate(
|
|
2090
1733
|
messages,
|
|
@@ -2099,20 +1742,12 @@ ${text}`,
|
|
|
2099
1742
|
);
|
|
2100
1743
|
}
|
|
2101
1744
|
/**
|
|
2102
|
-
*
|
|
2103
|
-
*
|
|
2104
|
-
* 的保留段计算与 compactOrTruncate 执行路径),压缩后持久化。
|
|
1745
|
+
* 保留段计算:从尾部累计保留最新消息,保留量为压缩目标(contextWindow ×
|
|
1746
|
+
* targetRatio)的一半。返回被压缩段的结束下标;messages[0..split) 为被压缩段。
|
|
2105
1747
|
*/
|
|
2106
|
-
|
|
2107
|
-
if (!this._compaction || this._compaction.enabled === false) return null;
|
|
2108
|
-
const key = sessionKey ?? this._sessionKey;
|
|
2109
|
-
const session = this.getSession(key);
|
|
2110
|
-
const messages = session.messages;
|
|
2111
|
-
if (messages.length === 0) return null;
|
|
2112
|
-
const contextWindow = this._model.contextWindow;
|
|
2113
|
-
if (!contextWindow || contextWindow <= 0) return null;
|
|
1748
|
+
computeKeepSplit(messages, contextWindow) {
|
|
2114
1749
|
const targetTokens = Math.floor(
|
|
2115
|
-
contextWindow * (this.
|
|
1750
|
+
contextWindow * (this._d.compaction?.targetRatio ?? 0.5)
|
|
2116
1751
|
);
|
|
2117
1752
|
const keepTokens = Math.max(Math.floor(targetTokens / 2), 1);
|
|
2118
1753
|
let kept = 0;
|
|
@@ -2125,21 +1760,7 @@ ${text}`,
|
|
|
2125
1760
|
kept += estimateMessageTokens(messages[i]);
|
|
2126
1761
|
split = i;
|
|
2127
1762
|
}
|
|
2128
|
-
|
|
2129
|
-
const mode = await this.compactOrTruncate(
|
|
2130
|
-
messages,
|
|
2131
|
-
split,
|
|
2132
|
-
key,
|
|
2133
|
-
`manual-${Date.now().toString(36)}`,
|
|
2134
|
-
new AbortController().signal,
|
|
2135
|
-
"threshold"
|
|
2136
|
-
);
|
|
2137
|
-
try {
|
|
2138
|
-
await this.persistSession(key, session);
|
|
2139
|
-
} catch (err) {
|
|
2140
|
-
console.warn("[Runtime] \u624B\u52A8\u538B\u7F29\u540E\u6301\u4E45\u5316\u5931\u8D25:", err?.message);
|
|
2141
|
-
}
|
|
2142
|
-
return mode;
|
|
1763
|
+
return split;
|
|
2143
1764
|
}
|
|
2144
1765
|
/**
|
|
2145
1766
|
* 执行压缩:messages[0..split) 为被压缩段,原地替换。
|
|
@@ -2154,10 +1775,10 @@ ${text}`,
|
|
|
2154
1775
|
const tokensBefore = messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
|
|
2155
1776
|
let mode = "truncate";
|
|
2156
1777
|
let summaryText = "";
|
|
2157
|
-
const summaryEnabled = !!this.
|
|
1778
|
+
const summaryEnabled = !!this._d.compaction && this._d.compaction.enabled !== false;
|
|
2158
1779
|
if (summaryEnabled && compacted.length > 0) {
|
|
2159
1780
|
const summaryBudget = Math.floor(
|
|
2160
|
-
this.
|
|
1781
|
+
this._d.getModel().contextWindow * COMPACTION_SUMMARY_BUDGET_RATIO
|
|
2161
1782
|
);
|
|
2162
1783
|
const inputText = compacted.map((m) => messageToSummaryLine(m)).filter((line) => line.length > 0).join("\n");
|
|
2163
1784
|
if (inputText && estimateTextTokens(inputText) <= summaryBudget) {
|
|
@@ -2184,7 +1805,7 @@ ${text}`,
|
|
|
2184
1805
|
messages.splice(0, messages.length, ...kept);
|
|
2185
1806
|
tokensAfter = kept.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
|
|
2186
1807
|
}
|
|
2187
|
-
await this.
|
|
1808
|
+
await this._d.telemetry.emit("onCompaction", {
|
|
2188
1809
|
traceId,
|
|
2189
1810
|
sessionKey,
|
|
2190
1811
|
mode,
|
|
@@ -2201,7 +1822,7 @@ ${text}`,
|
|
|
2201
1822
|
* 由调用方降级硬截断——摘要失败不影响主流程。
|
|
2202
1823
|
*/
|
|
2203
1824
|
async summarizeMessages(inputText, sessionKey, traceId, signal) {
|
|
2204
|
-
const prompt = this.
|
|
1825
|
+
const prompt = this._d.compaction?.prompt ?? DEFAULT_COMPACTION_PROMPT;
|
|
2205
1826
|
const context = {
|
|
2206
1827
|
systemPrompt: prompt,
|
|
2207
1828
|
messages: [{ role: "user", content: inputText, timestamp: Date.now() }]
|
|
@@ -2211,7 +1832,7 @@ ${text}`,
|
|
|
2211
1832
|
let usage;
|
|
2212
1833
|
let failed = false;
|
|
2213
1834
|
try {
|
|
2214
|
-
for await (const event of this.
|
|
1835
|
+
for await (const event of this._d.getStreamFn()(this._d.getModel(), context, { signal })) {
|
|
2215
1836
|
if (event.type === "text_delta") {
|
|
2216
1837
|
text += event.delta;
|
|
2217
1838
|
} else if (event.type === "error") {
|
|
@@ -2229,11 +1850,11 @@ ${text}`,
|
|
|
2229
1850
|
return null;
|
|
2230
1851
|
}
|
|
2231
1852
|
if (failed || !text.trim()) return null;
|
|
2232
|
-
await this.
|
|
1853
|
+
await this._d.telemetry.emit("onModelCall", {
|
|
2233
1854
|
traceId,
|
|
2234
1855
|
spanId: newSpanId(),
|
|
2235
1856
|
sessionKey,
|
|
2236
|
-
modelId: this.
|
|
1857
|
+
modelId: this._d.getModel().id,
|
|
2237
1858
|
attempts: 1,
|
|
2238
1859
|
inputTokens: usage?.input ?? estimateTextTokens(inputText),
|
|
2239
1860
|
outputTokens: usage?.output ?? estimateTextTokens(text),
|
|
@@ -2250,30 +1871,148 @@ ${text}`,
|
|
|
2250
1871
|
timestamp: Date.now()
|
|
2251
1872
|
};
|
|
2252
1873
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1876
|
+
// ../agent/runtime/model-turn.ts
|
|
1877
|
+
var ModelTurnRunner = class {
|
|
1878
|
+
constructor(_d) {
|
|
1879
|
+
this._d = _d;
|
|
1880
|
+
}
|
|
1881
|
+
_d;
|
|
1882
|
+
async streamModel(compilation, signal, sessionKey) {
|
|
1883
|
+
const turn = this.modelTurnWithRecovery(compilation, signal, sessionKey, false);
|
|
1884
|
+
let r = await turn.next();
|
|
1885
|
+
while (!r.done) r = await turn.next();
|
|
1886
|
+
return r.value ?? this.emptyAssistantMessage();
|
|
1887
|
+
}
|
|
1888
|
+
/** 空的 assistant 消息(流异常中断无 done/error 事件时的兜底) */
|
|
1889
|
+
emptyAssistantMessage() {
|
|
1890
|
+
const model = this._d.getModel();
|
|
1891
|
+
return {
|
|
1892
|
+
role: "assistant",
|
|
1893
|
+
content: [],
|
|
1894
|
+
stopReason: "stop",
|
|
1895
|
+
usage: createEmptyUsage(),
|
|
1896
|
+
model: model.id,
|
|
1897
|
+
provider: model.provider,
|
|
1898
|
+
timestamp: Date.now()
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* 单回合模型调用 + 上下文溢出自动恢复闭环。
|
|
1903
|
+
*
|
|
1904
|
+
* 检测(isContextOverflow,统一传入 model.contextWindow / content / maxTokens,
|
|
1905
|
+
* 覆盖显式错误 / 静默溢出 / 输入截断溢出 / 输出 thinking 耗尽 / 输出打满 五模式)
|
|
1906
|
+
* → 丢弃失败的 assistant 消息 → 截断会话历史 → 同回合重试(不消耗回合数,
|
|
1907
|
+
* 上限 OVERFLOW_RECOVERY_LIMIT):
|
|
1908
|
+
*
|
|
1909
|
+
* - 显式错误 / 零产出截断溢出 / thinking 耗尽溢出 / 输出打满溢出:丢弃失败
|
|
1910
|
+
* 消息后重试;流式路径吞掉可恢复的 error chunk(消费者看不到瞬态错误),
|
|
1911
|
+
* 不可恢复时补发。
|
|
1912
|
+
* - 静默溢出(stop + 有完整产出):保留回复,仅压缩旧上下文供后续轮次。
|
|
1913
|
+
* - 恢复耗尽或单请求超窗(无可丢弃):返回最后一次错误消息,维持旧行为。
|
|
1914
|
+
*
|
|
1915
|
+
* 流式路径 yield 模型事件 chunk;非流式路径由 streamModel 消费(chunk 丢弃)。
|
|
1916
|
+
* 返回最终 assistant 消息;无 done/error 事件时返回 null。
|
|
1917
|
+
*/
|
|
1918
|
+
async *modelTurnWithRecovery(compilation, signal, sessionKey, stream) {
|
|
1919
|
+
const model = this._d.getModel();
|
|
1920
|
+
const contextWindow = model.contextWindow;
|
|
1921
|
+
const maxTokens = model.maxTokens;
|
|
1922
|
+
let recoveries = 0;
|
|
1923
|
+
const toProbe = (m) => ({
|
|
1924
|
+
stopReason: m.stopReason,
|
|
1925
|
+
errorMessage: m.errorMessage,
|
|
1926
|
+
usage: m.usage,
|
|
1927
|
+
content: m.content,
|
|
1928
|
+
maxTokens
|
|
1929
|
+
});
|
|
1930
|
+
while (true) {
|
|
1931
|
+
let assistant = null;
|
|
1932
|
+
let suppressed = null;
|
|
1933
|
+
for await (const event of this.streamModelEvents(compilation, signal, stream)) {
|
|
1934
|
+
if (event.type === "error") {
|
|
1935
|
+
const msg = event.message;
|
|
1936
|
+
if (recoveries < OVERFLOW_RECOVERY_LIMIT && isContextOverflow(toProbe(msg), contextWindow)) {
|
|
1937
|
+
suppressed = msg;
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
const chunk = streamEventToChunk(event);
|
|
1941
|
+
if (chunk) yield chunk;
|
|
1942
|
+
assistant = msg;
|
|
1943
|
+
} else {
|
|
1944
|
+
const chunk = streamEventToChunk(event);
|
|
1945
|
+
if (chunk) yield chunk;
|
|
1946
|
+
if (event.type === "done") assistant = event.message;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
const final = assistant ?? suppressed;
|
|
1950
|
+
if (!final) return null;
|
|
1951
|
+
if (isContextOverflow(toProbe(final), contextWindow)) {
|
|
1952
|
+
const output = final.usage?.output ?? 0;
|
|
1953
|
+
const blocks = Array.isArray(final.content) ? final.content : null;
|
|
1954
|
+
const hasMeaningfulText = blocks?.some(
|
|
1955
|
+
(b) => b.type === "text" && "text" in b && typeof b.text === "string" && b.text.trim().length > 0
|
|
1956
|
+
);
|
|
1957
|
+
const hasToolCall = blocks?.some((b) => b.type === "toolCall");
|
|
1958
|
+
const thinkingOnly = blocks ? !hasMeaningfulText && !hasToolCall && blocks.some((b) => b.type === "thinking") : false;
|
|
1959
|
+
const outputFull = maxTokens > 0 && output >= Math.floor(maxTokens * 0.95);
|
|
1960
|
+
const failed = final.stopReason === "error" || output === 0 || thinkingOnly || outputFull;
|
|
1961
|
+
if (failed && recoveries < OVERFLOW_RECOVERY_LIMIT) {
|
|
1962
|
+
recoveries += 1;
|
|
1963
|
+
if (await this._d.compactor.recoverFromOverflow(compilation, recoveries, sessionKey, signal)) {
|
|
1964
|
+
const reason = thinkingOnly ? "thinking \u8017\u5C3D" : outputFull ? "output \u6253\u6EE1" : final.stopReason;
|
|
1965
|
+
console.warn(
|
|
1966
|
+
`[Runtime] \u4E0A\u4E0B\u6587\u6EA2\u51FA\uFF08${reason}\uFF09\uFF0C\u5DF2\u538B\u7F29\u5386\u53F2\u5E76\u540C\u56DE\u5408\u91CD\u8BD5\uFF08${recoveries}/${OVERFLOW_RECOVERY_LIMIT}\uFF09`
|
|
1967
|
+
);
|
|
1968
|
+
await this._d.telemetry.emit("onRetry", {
|
|
1969
|
+
traceId: compilation.traceId,
|
|
1970
|
+
provider: model.provider,
|
|
1971
|
+
modelId: model.id,
|
|
1972
|
+
attempt: recoveries,
|
|
1973
|
+
errorClass: "context-overflow",
|
|
1974
|
+
delayMs: 0,
|
|
1975
|
+
willRetry: true
|
|
1976
|
+
});
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
if (suppressed) {
|
|
1980
|
+
yield { type: "error", content: final.errorMessage || "\u6A21\u578B\u8C03\u7528\u51FA\u9519" };
|
|
1981
|
+
}
|
|
1982
|
+
return final;
|
|
1983
|
+
}
|
|
1984
|
+
if (failed) return final;
|
|
1985
|
+
await this._d.compactor.recoverFromOverflow(compilation, 1, sessionKey, signal);
|
|
1986
|
+
console.warn("[Runtime] \u68C0\u6D4B\u5230\u9759\u9ED8\u4E0A\u4E0B\u6587\u6EA2\u51FA\uFF08usage \u8D85\u7A97\uFF09\uFF0C\u5DF2\u538B\u7F29\u5386\u53F2\u6D88\u606F");
|
|
1987
|
+
}
|
|
1988
|
+
return final;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* 统一模型调用埋点生成器:run()(streamModel)与 stream()(runLoopStream)两路径共用。
|
|
1993
|
+
* 职责:模型调用计时、spanId、attempts 累计、onRetry 转发、onModelCall 上报
|
|
1994
|
+
* (含 tokens / cost / errorClass / ttft)。
|
|
1995
|
+
*/
|
|
1996
|
+
async *streamModelEvents(compilation, signal, stream) {
|
|
1997
|
+
const model = this._d.getModel();
|
|
1998
|
+
const sessionKey = this._d.resolveSessionKey(compilation.request);
|
|
1999
|
+
const modelStartedAt = Date.now();
|
|
2000
|
+
const spanId = newSpanId();
|
|
2262
2001
|
let attempts = 1;
|
|
2263
2002
|
let ttftAt;
|
|
2264
2003
|
let lastAssistant;
|
|
2265
2004
|
const options = { signal };
|
|
2266
|
-
if (this.
|
|
2267
|
-
options.reasoning = this.
|
|
2005
|
+
if (this._d.getThinkingLevel() !== "off" && model.reasoning) {
|
|
2006
|
+
options.reasoning = this._d.getThinkingLevel();
|
|
2268
2007
|
}
|
|
2269
2008
|
options.onRetryAttempt = (info) => {
|
|
2270
2009
|
attempts += 1;
|
|
2271
|
-
void this.
|
|
2010
|
+
void this._d.telemetry.emit("onRetry", {
|
|
2272
2011
|
traceId: compilation.traceId,
|
|
2273
2012
|
spanId,
|
|
2274
2013
|
// P2:重试明细关联到本模型调用 span
|
|
2275
|
-
provider:
|
|
2276
|
-
modelId:
|
|
2014
|
+
provider: model.provider,
|
|
2015
|
+
modelId: model.id,
|
|
2277
2016
|
attempt: info.attempt,
|
|
2278
2017
|
errorClass: classifyError(info.error),
|
|
2279
2018
|
status: statusOfRetryError(info.error),
|
|
@@ -2282,10 +2021,10 @@ ${text}`,
|
|
|
2282
2021
|
});
|
|
2283
2022
|
};
|
|
2284
2023
|
try {
|
|
2285
|
-
const context = await this.
|
|
2024
|
+
const context = await this._d.hooks.beforeModelCall.promise(
|
|
2286
2025
|
this.buildContext(compilation.messages)
|
|
2287
2026
|
);
|
|
2288
|
-
for await (const event of this.
|
|
2027
|
+
for await (const event of this._d.getStreamFn()(model, context, options)) {
|
|
2289
2028
|
if (stream && event.type === "text_delta" && ttftAt === void 0) {
|
|
2290
2029
|
ttftAt = Date.now();
|
|
2291
2030
|
}
|
|
@@ -2297,11 +2036,11 @@ ${text}`,
|
|
|
2297
2036
|
} finally {
|
|
2298
2037
|
const assistant = lastAssistant;
|
|
2299
2038
|
const errorClass = assistant?.errorMessage ? errorClassFromMessage(assistant.errorMessage) ?? "unknown" : void 0;
|
|
2300
|
-
await this.
|
|
2039
|
+
await this._d.telemetry.emit("onModelCall", {
|
|
2301
2040
|
traceId: compilation.traceId,
|
|
2302
2041
|
spanId,
|
|
2303
2042
|
sessionKey,
|
|
2304
|
-
modelId:
|
|
2043
|
+
modelId: model.id,
|
|
2305
2044
|
attempts,
|
|
2306
2045
|
inputTokens: assistant?.usage?.input ?? 0,
|
|
2307
2046
|
outputTokens: assistant?.usage?.output ?? 0,
|
|
@@ -2316,164 +2055,718 @@ ${text}`,
|
|
|
2316
2055
|
}
|
|
2317
2056
|
}
|
|
2318
2057
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2058
|
+
buildContext(messages) {
|
|
2059
|
+
const tools = Array.from(this._d.getTools().values());
|
|
2060
|
+
return {
|
|
2061
|
+
systemPrompt: this._d.getSystemPrompt(),
|
|
2062
|
+
// compactionSummary / stateSnapshot 为内部扩展 role,provider 适配层
|
|
2063
|
+
// 仅支持 user/assistant/toolResult,发出前统一转为带标注的 user 消息
|
|
2064
|
+
messages: messages.filter((m) => m.role !== "system").map((m) => {
|
|
2065
|
+
const role = m.role;
|
|
2066
|
+
if (role === "compactionSummary") return this.compactionSummaryToUser(m);
|
|
2067
|
+
if (role === "stateSnapshot") return this.stateSnapshotToUser(m);
|
|
2068
|
+
return m;
|
|
2069
|
+
}),
|
|
2070
|
+
tools: tools.length > 0 ? tools : void 0
|
|
2071
|
+
};
|
|
2330
2072
|
}
|
|
2331
|
-
/**
|
|
2332
|
-
|
|
2333
|
-
|
|
2073
|
+
/** compactionSummary 消息 → user 消息(所有 provider 均兼容 user role) */
|
|
2074
|
+
compactionSummaryToUser(msg) {
|
|
2075
|
+
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
2076
|
+
return {
|
|
2077
|
+
role: "user",
|
|
2078
|
+
content: `${COMPACTION_USER_PREFIX}
|
|
2079
|
+
${text}`,
|
|
2080
|
+
timestamp: msg.timestamp
|
|
2081
|
+
};
|
|
2334
2082
|
}
|
|
2335
|
-
/**
|
|
2336
|
-
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
activeMs,
|
|
2345
|
-
queuedMs,
|
|
2346
|
-
turnCount: compilation.turnCount ?? 0,
|
|
2347
|
-
result,
|
|
2348
|
-
success: result.success,
|
|
2349
|
-
errorClass: this.runErrorClass(compilation),
|
|
2350
|
-
tokens: {
|
|
2351
|
-
input: result.usage.input ?? 0,
|
|
2352
|
-
output: result.usage.output ?? 0,
|
|
2353
|
-
cacheRead: result.usage.cacheRead,
|
|
2354
|
-
cacheWrite: result.usage.cacheWrite
|
|
2355
|
-
},
|
|
2356
|
-
ttftMs: compilation.ttftMs
|
|
2357
|
-
});
|
|
2083
|
+
/** stateSnapshot 消息 → user 消息(所有 provider 均兼容 user role) */
|
|
2084
|
+
stateSnapshotToUser(msg) {
|
|
2085
|
+
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
2086
|
+
return {
|
|
2087
|
+
role: "user",
|
|
2088
|
+
content: `${STATE_SNAPSHOT_USER_PREFIX}
|
|
2089
|
+
${text}`,
|
|
2090
|
+
timestamp: msg.timestamp
|
|
2091
|
+
};
|
|
2358
2092
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2093
|
+
};
|
|
2094
|
+
|
|
2095
|
+
// ../agent/runtime/loops.ts
|
|
2096
|
+
var RunLoops = class {
|
|
2097
|
+
constructor(_d) {
|
|
2098
|
+
this._d = _d;
|
|
2099
|
+
}
|
|
2100
|
+
_d;
|
|
2101
|
+
async run(request, sessionKey, session, traceId, queuedMs) {
|
|
2102
|
+
const activeStartedAt = Date.now();
|
|
2103
|
+
this._d.onSessionActivated(sessionKey);
|
|
2104
|
+
await this._d.hooks.beforeInitialize.promise(request);
|
|
2105
|
+
await this._d.hooks.afterInitialize.promise(request);
|
|
2106
|
+
const finalRequest = await this._d.hooks.beforeRun.promise(request);
|
|
2107
|
+
if (!finalRequest.ephemeral) {
|
|
2108
|
+
await this._d.persistence.hydrate(sessionKey, session);
|
|
2109
|
+
}
|
|
2110
|
+
const compilation = this._d.createCompilation(finalRequest, sessionKey, session, traceId);
|
|
2111
|
+
compilation.messages.push(this._d.buildUserMessage(finalRequest));
|
|
2112
|
+
try {
|
|
2113
|
+
await this.runLoop(compilation, finalRequest, session);
|
|
2114
|
+
const result = this._d.buildResult(compilation);
|
|
2115
|
+
await this._d.hooks.beforeEmit.promise(result);
|
|
2116
|
+
await this._d.hooks.afterEmit.promise(result);
|
|
2117
|
+
await this._d.hooks.done.promise(result, finalRequest);
|
|
2118
|
+
compilation.completed = true;
|
|
2119
|
+
await this.emitRunEnd(finalRequest, sessionKey, compilation, result, queuedMs, activeStartedAt);
|
|
2120
|
+
return result;
|
|
2121
|
+
} catch (err) {
|
|
2122
|
+
const error = err;
|
|
2123
|
+
const message = error?.message ?? String(err);
|
|
2124
|
+
if (error?.name !== "AbortError") {
|
|
2125
|
+
console.error("[Runtime] \u8FD0\u884C\u5931\u8D25:", error?.stack ?? message);
|
|
2369
2126
|
}
|
|
2127
|
+
await this._d.hooks.failed.promise(error, finalRequest);
|
|
2128
|
+
const result = new ResultBuilder().error(message).build();
|
|
2129
|
+
await this.emitRunEnd(finalRequest, sessionKey, compilation, result, queuedMs, activeStartedAt);
|
|
2130
|
+
return result;
|
|
2131
|
+
} finally {
|
|
2132
|
+
await this._d.persistence.persistSafe(finalRequest, sessionKey);
|
|
2370
2133
|
}
|
|
2371
|
-
return void 0;
|
|
2372
2134
|
}
|
|
2373
|
-
|
|
2374
|
-
const
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
usage.input = (usage.input ?? 0) + assistant.usage.input;
|
|
2388
|
-
usage.output = (usage.output ?? 0) + assistant.usage.output;
|
|
2389
|
-
usage.total = (usage.total ?? 0) + assistant.usage.total;
|
|
2390
|
-
if (assistant.usage.cacheRead) usage.cacheRead = (usage.cacheRead ?? 0) + assistant.usage.cacheRead;
|
|
2391
|
-
if (assistant.usage.cacheWrite) usage.cacheWrite = (usage.cacheWrite ?? 0) + assistant.usage.cacheWrite;
|
|
2392
|
-
}
|
|
2135
|
+
async *stream(request, sessionKey, session, traceId, queuedMs) {
|
|
2136
|
+
const activeStartedAt = Date.now();
|
|
2137
|
+
this._d.onSessionActivated(sessionKey);
|
|
2138
|
+
await this._d.hooks.beforeInitialize.promise(request);
|
|
2139
|
+
await this._d.hooks.afterInitialize.promise(request);
|
|
2140
|
+
const finalRequest = await this._d.hooks.beforeRun.promise(request);
|
|
2141
|
+
if (!finalRequest.ephemeral) {
|
|
2142
|
+
await this._d.persistence.hydrate(sessionKey, session);
|
|
2143
|
+
}
|
|
2144
|
+
const compilation = this._d.createCompilation(finalRequest, sessionKey, session, traceId);
|
|
2145
|
+
compilation.messages.push(this._d.buildUserMessage(finalRequest));
|
|
2146
|
+
try {
|
|
2147
|
+
for await (const chunk of this.runLoopStream(compilation, finalRequest, session)) {
|
|
2148
|
+
yield chunk;
|
|
2393
2149
|
}
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2150
|
+
const result = this._d.buildResult(compilation);
|
|
2151
|
+
await this._d.hooks.beforeEmit.promise(result);
|
|
2152
|
+
await this._d.hooks.afterEmit.promise(result);
|
|
2153
|
+
await this._d.hooks.done.promise(result, finalRequest);
|
|
2154
|
+
yield { type: "done", result };
|
|
2155
|
+
await this.emitRunEnd(finalRequest, sessionKey, compilation, result, queuedMs, activeStartedAt);
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
const error = err;
|
|
2158
|
+
const message = error?.message ?? String(err);
|
|
2159
|
+
if (error?.name !== "AbortError") {
|
|
2160
|
+
console.error("[Runtime] \u6D41\u5F0F\u8FD0\u884C\u5931\u8D25:", error?.stack ?? message);
|
|
2399
2161
|
}
|
|
2162
|
+
await this._d.hooks.failed.promise(error, finalRequest);
|
|
2163
|
+
const result = new ResultBuilder().error(message).build();
|
|
2164
|
+
yield { type: "error", content: message };
|
|
2165
|
+
yield { type: "done", result };
|
|
2166
|
+
await this.emitRunEnd(finalRequest, sessionKey, compilation, result, queuedMs, activeStartedAt);
|
|
2167
|
+
} finally {
|
|
2168
|
+
await this._d.persistence.persistSafe(finalRequest, sessionKey);
|
|
2400
2169
|
}
|
|
2401
|
-
const resources = messagesToResources(messages);
|
|
2402
|
-
const builder = new ResultBuilder().content(content).toolsUsed(toolsUsed).usage(usage).stopReason(stopReason).error(error).resources(resources).metadata("traceId", compilation.traceId);
|
|
2403
|
-
if (compilation.terminateReason) {
|
|
2404
|
-
builder.stopReason("terminated").metadata("terminateReason", compilation.terminateReason);
|
|
2405
|
-
}
|
|
2406
|
-
if (compilation.maxTurnsExhausted) {
|
|
2407
|
-
builder.stopReason("max_turns").metadata("maxTurns", true);
|
|
2408
|
-
}
|
|
2409
|
-
return builder.build();
|
|
2410
2170
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2171
|
+
// ─── 对话循环(同步) ───────────────────────────────────────────
|
|
2172
|
+
async runLoop(compilation, request, session) {
|
|
2173
|
+
session.isStreaming = true;
|
|
2174
|
+
session.abortController = new AbortController();
|
|
2175
|
+
const sessionKey = this._d.resolveSessionKey(request);
|
|
2176
|
+
try {
|
|
2177
|
+
let maxTurns = this._d.getMaxTurns();
|
|
2178
|
+
let turnCount = 0;
|
|
2179
|
+
while (maxTurns-- > 0) {
|
|
2180
|
+
turnCount += 1;
|
|
2181
|
+
await this._d.transformMessages(compilation, sessionKey);
|
|
2182
|
+
await this._d.compactor.maybeCompactByThreshold(
|
|
2183
|
+
compilation,
|
|
2184
|
+
sessionKey,
|
|
2185
|
+
session.abortController.signal
|
|
2186
|
+
);
|
|
2187
|
+
const assistantMessage = await this._d.modelTurn.streamModel(
|
|
2188
|
+
compilation,
|
|
2189
|
+
session.abortController.signal,
|
|
2190
|
+
sessionKey
|
|
2191
|
+
);
|
|
2192
|
+
compilation.messages.push(assistantMessage);
|
|
2193
|
+
await this._d.persistence.persistSafe(request, sessionKey);
|
|
2194
|
+
const toolCalls = extractToolCalls(assistantMessage.content);
|
|
2195
|
+
if (toolCalls.length === 0) {
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
const outcome = await this.executeToolCalls(
|
|
2199
|
+
compilation,
|
|
2200
|
+
toolCalls,
|
|
2201
|
+
session.abortController.signal
|
|
2202
|
+
);
|
|
2203
|
+
await this._d.persistence.persistSafe(request, sessionKey);
|
|
2204
|
+
if (outcome.terminate) {
|
|
2205
|
+
compilation.terminateReason = outcome.terminateReason ?? "terminated";
|
|
2206
|
+
break;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
if (maxTurns < 0) compilation.maxTurnsExhausted = true;
|
|
2210
|
+
compilation.turnCount = turnCount;
|
|
2211
|
+
} finally {
|
|
2212
|
+
this._d.store.markIdle(session);
|
|
2424
2213
|
}
|
|
2425
2214
|
}
|
|
2215
|
+
// ─── 对话循环(流式) ───────────────────────────────────────────
|
|
2216
|
+
async *runLoopStream(compilation, request, session) {
|
|
2217
|
+
session.isStreaming = true;
|
|
2218
|
+
session.abortController = new AbortController();
|
|
2219
|
+
const sessionKey = this._d.resolveSessionKey(request);
|
|
2220
|
+
try {
|
|
2221
|
+
let maxTurns = this._d.getMaxTurns();
|
|
2222
|
+
let turnCount = 0;
|
|
2223
|
+
while (maxTurns-- > 0) {
|
|
2224
|
+
turnCount += 1;
|
|
2225
|
+
await this._d.transformMessages(compilation, sessionKey);
|
|
2226
|
+
await this._d.compactor.maybeCompactByThreshold(
|
|
2227
|
+
compilation,
|
|
2228
|
+
sessionKey,
|
|
2229
|
+
session.abortController.signal
|
|
2230
|
+
);
|
|
2231
|
+
const turn = this._d.modelTurn.modelTurnWithRecovery(
|
|
2232
|
+
compilation,
|
|
2233
|
+
session.abortController.signal,
|
|
2234
|
+
sessionKey,
|
|
2235
|
+
true
|
|
2236
|
+
// stream 模式:同时统计首 token 延迟
|
|
2237
|
+
);
|
|
2238
|
+
let turnResult = await turn.next();
|
|
2239
|
+
while (!turnResult.done) {
|
|
2240
|
+
yield turnResult.value;
|
|
2241
|
+
turnResult = await turn.next();
|
|
2242
|
+
}
|
|
2243
|
+
const assistantMessage = turnResult.value;
|
|
2244
|
+
if (!assistantMessage) break;
|
|
2245
|
+
compilation.messages.push(assistantMessage);
|
|
2246
|
+
await this._d.persistence.persistSafe(request, sessionKey);
|
|
2247
|
+
const toolCalls = extractToolCalls(assistantMessage.content);
|
|
2248
|
+
if (toolCalls.length === 0) {
|
|
2249
|
+
break;
|
|
2250
|
+
}
|
|
2251
|
+
for (const toolCall of toolCalls) {
|
|
2252
|
+
yield {
|
|
2253
|
+
type: "tool_start",
|
|
2254
|
+
toolName: toolCall.name,
|
|
2255
|
+
toolCallId: toolCall.id
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
const outcome = await this.executeToolCalls(
|
|
2259
|
+
compilation,
|
|
2260
|
+
toolCalls,
|
|
2261
|
+
session.abortController.signal
|
|
2262
|
+
);
|
|
2263
|
+
await this._d.persistence.persistSafe(request, sessionKey);
|
|
2264
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
2265
|
+
yield {
|
|
2266
|
+
type: "tool_end",
|
|
2267
|
+
toolName: toolCalls[i].name,
|
|
2268
|
+
toolCallId: toolCalls[i].id,
|
|
2269
|
+
isError: this._d.tools.isErrorResult(outcome.results[i])
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
if (outcome.terminate) {
|
|
2273
|
+
compilation.terminateReason = outcome.terminateReason ?? "terminated";
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
if (maxTurns < 0) compilation.maxTurnsExhausted = true;
|
|
2278
|
+
compilation.turnCount = turnCount;
|
|
2279
|
+
} finally {
|
|
2280
|
+
this._d.store.markIdle(session);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
/** 同步循环:执行工具调用并将结果消息按原顺序追加 */
|
|
2284
|
+
async executeToolCalls(compilation, toolCalls, signal) {
|
|
2285
|
+
const outcome = await this._d.tools.run(
|
|
2286
|
+
toolCalls,
|
|
2287
|
+
signal,
|
|
2288
|
+
compilation.request,
|
|
2289
|
+
compilation.traceId
|
|
2290
|
+
);
|
|
2291
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
2292
|
+
compilation.messages.push(
|
|
2293
|
+
this._d.tools.buildResultMessage(toolCalls[i], outcome.results[i])
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
return outcome;
|
|
2297
|
+
}
|
|
2298
|
+
/** run 级完成事件上报(补充默认模型 id) */
|
|
2299
|
+
async emitRunEnd(request, sessionKey, compilation, result, queuedMs, activeStartedAt) {
|
|
2300
|
+
await this._d.telemetry.emitRunEnd(
|
|
2301
|
+
request,
|
|
2302
|
+
sessionKey,
|
|
2303
|
+
compilation,
|
|
2304
|
+
result,
|
|
2305
|
+
queuedMs,
|
|
2306
|
+
activeStartedAt,
|
|
2307
|
+
this._d.getModel().id
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
|
|
2312
|
+
// ../agent/runtime/index.ts
|
|
2313
|
+
var AgentRuntime = class _AgentRuntime {
|
|
2314
|
+
_config;
|
|
2315
|
+
_extensions;
|
|
2316
|
+
_hooks;
|
|
2317
|
+
/** 上下文转换器列表,按数组顺序链式执行(上一个输出作为下一个输入) */
|
|
2318
|
+
_transformers;
|
|
2319
|
+
_model;
|
|
2320
|
+
_streamFn;
|
|
2321
|
+
_systemPrompt;
|
|
2322
|
+
_thinkingLevel;
|
|
2323
|
+
_globalTools = /* @__PURE__ */ new Map();
|
|
2324
|
+
/** 默认会话 key(常量 'default';请求未指定 sessionKey 时路由到此会话) */
|
|
2325
|
+
_sessionKey;
|
|
2326
|
+
_sessionStorage;
|
|
2327
|
+
/** Extension 应用时的上下文(shared Map 供 ToolCallContext 引用) */
|
|
2328
|
+
_extensionContext;
|
|
2329
|
+
_maxTurns;
|
|
2330
|
+
_toolTimeoutMs;
|
|
2331
|
+
_parallelToolCalls;
|
|
2332
|
+
_contextBudgetRatio;
|
|
2333
|
+
/** 内置摘要压缩配置(未配置 = 保持旧行为,仅硬截断兜底) */
|
|
2334
|
+
_compaction;
|
|
2335
|
+
_telemetry;
|
|
2336
|
+
/** 框架级工具权限策略(未配置 → 默认 fail-closed,见 permissionFailOpen) */
|
|
2337
|
+
_permissionPolicy;
|
|
2338
|
+
/** 未配置策略时是否显式放行工具执行(默认 false = 拒绝,安全姿态) */
|
|
2339
|
+
_permissionFailOpen;
|
|
2340
|
+
/** 审批管理器(pending 决策挂起等待外部批准;未配置 → pending 视为 deny) */
|
|
2341
|
+
_approvals;
|
|
2342
|
+
/** 审批等待超时(毫秒) */
|
|
2343
|
+
_approvalTimeoutMs;
|
|
2344
|
+
/** traceId 生成器(测试可注入确定性 id) */
|
|
2345
|
+
_traceIdGenerator;
|
|
2346
|
+
// ─── 协作模块(构造时组装,职责见文件头注释) ───────────────────
|
|
2347
|
+
_store;
|
|
2348
|
+
_persistence;
|
|
2349
|
+
_emitter;
|
|
2350
|
+
_tools;
|
|
2351
|
+
_compactor;
|
|
2352
|
+
_modelTurn;
|
|
2353
|
+
_loops;
|
|
2354
|
+
constructor(options) {
|
|
2355
|
+
this._config = options.config ?? {};
|
|
2356
|
+
this._extensions = new ExtensionManager();
|
|
2357
|
+
this._hooks = this._extensions.getHooks();
|
|
2358
|
+
this._transformers = [...options.transformers ?? []];
|
|
2359
|
+
this._model = options.model ?? {
|
|
2360
|
+
id: "unknown",
|
|
2361
|
+
name: "unknown",
|
|
2362
|
+
provider: "unknown",
|
|
2363
|
+
contextWindow: 128e3,
|
|
2364
|
+
maxTokens: 8192,
|
|
2365
|
+
reasoning: false
|
|
2366
|
+
};
|
|
2367
|
+
this._streamFn = options.streamFn ?? (async function* () {
|
|
2368
|
+
throw new Error("streamFn \u672A\u8BBE\u7F6E\uFF0C\u8BF7\u901A\u8FC7 setStreamFn() \u6216 RuntimeOptions.streamFn \u63D0\u4F9B");
|
|
2369
|
+
});
|
|
2370
|
+
this._systemPrompt = options.systemPrompt ?? "";
|
|
2371
|
+
this._thinkingLevel = options.thinkingLevel ?? "off";
|
|
2372
|
+
this._sessionStorage = options.sessionStorage;
|
|
2373
|
+
this._maxTurns = options.maxTurns ?? 50;
|
|
2374
|
+
this._toolTimeoutMs = options.toolTimeoutMs ?? 12e4;
|
|
2375
|
+
this._parallelToolCalls = options.parallelToolCalls ?? true;
|
|
2376
|
+
this._contextBudgetRatio = options.contextBudgetRatio ?? 0.8;
|
|
2377
|
+
this._compaction = options.compaction;
|
|
2378
|
+
this._telemetry = options.telemetry;
|
|
2379
|
+
this._permissionPolicy = options.permissionPolicy;
|
|
2380
|
+
this._permissionFailOpen = options.permissionFailOpen ?? false;
|
|
2381
|
+
this._approvals = options.approvals;
|
|
2382
|
+
this._approvalTimeoutMs = options.approvalTimeoutMs ?? 3e5;
|
|
2383
|
+
this._traceIdGenerator = options.traceIdGenerator;
|
|
2384
|
+
if (options.tools) {
|
|
2385
|
+
for (const tool of options.tools) {
|
|
2386
|
+
this._globalTools.set(tool.name, tool);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
this._sessionKey = "default";
|
|
2390
|
+
this._emitter = new RuntimeTelemetry(options.telemetry, options.traceIdGenerator);
|
|
2391
|
+
setTapErrorHandler((info) => {
|
|
2392
|
+
void this._emitter.emit("onHookError", {
|
|
2393
|
+
sessionKey: this._sessionKey,
|
|
2394
|
+
hook: info.hook,
|
|
2395
|
+
tap: info.tap,
|
|
2396
|
+
error: info.error
|
|
2397
|
+
});
|
|
2398
|
+
});
|
|
2399
|
+
this._store = new SessionStore(this._sessionKey, options.maxSessions);
|
|
2400
|
+
this._persistence = new SessionPersistence(
|
|
2401
|
+
this._sessionStorage,
|
|
2402
|
+
() => this._model,
|
|
2403
|
+
this._store
|
|
2404
|
+
);
|
|
2405
|
+
this._tools = new ToolExecutor({
|
|
2406
|
+
hooks: this._hooks,
|
|
2407
|
+
telemetry: this._emitter,
|
|
2408
|
+
permissionPolicy: this._permissionPolicy,
|
|
2409
|
+
permissionFailOpen: this._permissionFailOpen,
|
|
2410
|
+
approvals: this._approvals,
|
|
2411
|
+
approvalTimeoutMs: this._approvalTimeoutMs,
|
|
2412
|
+
toolTimeoutMs: this._toolTimeoutMs,
|
|
2413
|
+
parallelToolCalls: this._parallelToolCalls,
|
|
2414
|
+
defaultSessionKey: this._sessionKey,
|
|
2415
|
+
getTools: () => this._globalTools,
|
|
2416
|
+
getShared: () => this._extensionContext?.shared
|
|
2417
|
+
});
|
|
2418
|
+
this._compactor = new ContextCompactor({
|
|
2419
|
+
telemetry: this._emitter,
|
|
2420
|
+
getModel: () => this._model,
|
|
2421
|
+
getStreamFn: () => this._streamFn,
|
|
2422
|
+
contextBudgetRatio: this._contextBudgetRatio,
|
|
2423
|
+
compaction: this._compaction
|
|
2424
|
+
});
|
|
2425
|
+
this._modelTurn = new ModelTurnRunner({
|
|
2426
|
+
telemetry: this._emitter,
|
|
2427
|
+
compactor: this._compactor,
|
|
2428
|
+
hooks: this._hooks,
|
|
2429
|
+
getModel: () => this._model,
|
|
2430
|
+
getStreamFn: () => this._streamFn,
|
|
2431
|
+
getSystemPrompt: () => this._systemPrompt,
|
|
2432
|
+
getThinkingLevel: () => this._thinkingLevel,
|
|
2433
|
+
getTools: () => this._globalTools,
|
|
2434
|
+
resolveSessionKey: (request) => this.resolveSessionKey(request)
|
|
2435
|
+
});
|
|
2436
|
+
this._loops = new RunLoops({
|
|
2437
|
+
hooks: this._hooks,
|
|
2438
|
+
telemetry: this._emitter,
|
|
2439
|
+
store: this._store,
|
|
2440
|
+
persistence: this._persistence,
|
|
2441
|
+
tools: this._tools,
|
|
2442
|
+
compactor: this._compactor,
|
|
2443
|
+
modelTurn: this._modelTurn,
|
|
2444
|
+
getModel: () => this._model,
|
|
2445
|
+
getMaxTurns: () => this._maxTurns,
|
|
2446
|
+
resolveSessionKey: (request) => this.resolveSessionKey(request),
|
|
2447
|
+
onSessionActivated: (key) => this.syncExtensionSessionKey(key),
|
|
2448
|
+
createCompilation: (request, sessionKey, session, traceId) => this.createCompilation(request, sessionKey, session, traceId),
|
|
2449
|
+
buildUserMessage: (request) => buildUserMessage(request),
|
|
2450
|
+
buildResult: (compilation) => buildResult(compilation),
|
|
2451
|
+
transformMessages: (compilation, sessionKey) => this.transformMessages(compilation, sessionKey)
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
// ─── 静态工厂 ───────────────────────────────────────────────────
|
|
2455
|
+
static create(options = {}) {
|
|
2456
|
+
const runtime = new _AgentRuntime(options);
|
|
2457
|
+
if (options.extensions) {
|
|
2458
|
+
runtime._extensions.registerAll(options.extensions);
|
|
2459
|
+
}
|
|
2460
|
+
const ctx = {
|
|
2461
|
+
config: runtime._config,
|
|
2462
|
+
workspace: options.workspace ?? process.cwd(),
|
|
2463
|
+
sessionKey: runtime._sessionKey,
|
|
2464
|
+
shared: /* @__PURE__ */ new Map(),
|
|
2465
|
+
runtime
|
|
2466
|
+
};
|
|
2467
|
+
runtime._extensionContext = ctx;
|
|
2468
|
+
runtime._extensions.applyAll(ctx);
|
|
2469
|
+
return runtime;
|
|
2470
|
+
}
|
|
2471
|
+
// ─── Runtime 接口实现 ───────────────────────────────────────────
|
|
2472
|
+
get config() {
|
|
2473
|
+
return this._config;
|
|
2474
|
+
}
|
|
2475
|
+
get extensions() {
|
|
2476
|
+
return this._extensions;
|
|
2477
|
+
}
|
|
2478
|
+
get hooks() {
|
|
2479
|
+
return this._hooks;
|
|
2480
|
+
}
|
|
2481
|
+
// ─── 工具/模型/流管理 ───────────────────────────────────────────
|
|
2482
|
+
registerTool(tool) {
|
|
2483
|
+
if (this._globalTools.has(tool.name)) {
|
|
2484
|
+
console.warn(`[Runtime] \u5DE5\u5177 "${tool.name}" \u5DF2\u5B58\u5728\uFF0C\u5C06\u88AB\u8986\u76D6`);
|
|
2485
|
+
}
|
|
2486
|
+
this._globalTools.set(tool.name, tool);
|
|
2487
|
+
return this;
|
|
2488
|
+
}
|
|
2489
|
+
registerTools(tools) {
|
|
2490
|
+
for (const tool of tools) {
|
|
2491
|
+
this.registerTool(tool);
|
|
2492
|
+
}
|
|
2493
|
+
return this;
|
|
2494
|
+
}
|
|
2495
|
+
unregisterTool(name) {
|
|
2496
|
+
return this._globalTools.delete(name);
|
|
2497
|
+
}
|
|
2498
|
+
setModel(model) {
|
|
2499
|
+
this._model = model;
|
|
2500
|
+
return this;
|
|
2501
|
+
}
|
|
2502
|
+
setSystemPrompt(prompt) {
|
|
2503
|
+
this._systemPrompt = prompt;
|
|
2504
|
+
return this;
|
|
2505
|
+
}
|
|
2506
|
+
setThinkingLevel(level) {
|
|
2507
|
+
this._thinkingLevel = level;
|
|
2508
|
+
return this;
|
|
2509
|
+
}
|
|
2510
|
+
setStreamFn(fn) {
|
|
2511
|
+
this._streamFn = fn;
|
|
2512
|
+
return this;
|
|
2513
|
+
}
|
|
2514
|
+
registerExtension(extension) {
|
|
2515
|
+
this._extensions.register(extension);
|
|
2516
|
+
return this;
|
|
2517
|
+
}
|
|
2518
|
+
useTransformer(transformer) {
|
|
2519
|
+
this._transformers.push(transformer);
|
|
2520
|
+
return this;
|
|
2521
|
+
}
|
|
2522
|
+
/**
|
|
2523
|
+
* 获取指定会话的消息列表(默认会话;会话不存在返回空数组)。
|
|
2524
|
+
* 同步方法:会话被 LRU 淘汰且配置了存储时,后台异步恢复(下次调用可读到),
|
|
2525
|
+
* 需确定性读取(如断言/导出)请用 loadMessages()。
|
|
2526
|
+
*/
|
|
2527
|
+
getMessages(sessionKey) {
|
|
2528
|
+
const key = sessionKey ?? this._sessionKey;
|
|
2529
|
+
if (this._store.has(key)) {
|
|
2530
|
+
return this._store.getMessagesCopy(key);
|
|
2531
|
+
}
|
|
2532
|
+
if (this._sessionStorage) {
|
|
2533
|
+
const session = this._store.ensure(key);
|
|
2534
|
+
void this._persistence.hydrate(key, session).catch(() => {
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
return [];
|
|
2538
|
+
}
|
|
2539
|
+
/** 异步读取指定会话的消息列表:内存未命中(LRU 淘汰)时等待存储恢复后返回 */
|
|
2540
|
+
async loadMessages(sessionKey) {
|
|
2541
|
+
const key = sessionKey ?? this._sessionKey;
|
|
2542
|
+
const existing = this._store.peek(key);
|
|
2543
|
+
if (existing && existing.hydrated) {
|
|
2544
|
+
return this._store.getMessagesCopy(key);
|
|
2545
|
+
}
|
|
2546
|
+
const session = existing ?? this._store.ensure(key);
|
|
2547
|
+
await this._persistence.hydrate(key, session);
|
|
2548
|
+
return this._store.getMessagesCopy(key);
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* 指定会话消息的只读视图(高频轮询用):浅拷贝数组、共享消息对象,
|
|
2552
|
+
* 避免每次 structuredClone 深拷贝整个会话历史;调用方不得修改内容。
|
|
2553
|
+
*/
|
|
2554
|
+
peekMessages(sessionKey) {
|
|
2555
|
+
return this._store.peekMessages(sessionKey ?? this._sessionKey);
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* 同步共享扩展上下文的会话键为本次 run/stream 的真实会话。
|
|
2559
|
+
* 注意:ExtensionContext 为跨会话共享单例,多会话并发时该值为最近激活的
|
|
2560
|
+
* 会话;需精确会话请用 ToolCallContext.sessionKey / Request.sessionKey。
|
|
2561
|
+
*/
|
|
2562
|
+
syncExtensionSessionKey(sessionKey) {
|
|
2563
|
+
if (this._extensionContext && this._extensionContext.sessionKey !== sessionKey) {
|
|
2564
|
+
this._extensionContext.sessionKey = sessionKey;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
// ─── 核心运行逻辑 ───────────────────────────────────────────────
|
|
2568
|
+
/** 解析请求路由的会话 key:request.sessionKey ?? 默认会话 key */
|
|
2569
|
+
resolveSessionKey(request) {
|
|
2570
|
+
return request.sessionKey ?? this._sessionKey;
|
|
2571
|
+
}
|
|
2572
|
+
/** 当前活跃的会话 key 列表(含默认会话) */
|
|
2573
|
+
getSessionKeys() {
|
|
2574
|
+
return this._store.keys();
|
|
2575
|
+
}
|
|
2576
|
+
/** 某会话是否存在(内存中) */
|
|
2577
|
+
hasSession(sessionKey) {
|
|
2578
|
+
return this._store.has(sessionKey);
|
|
2579
|
+
}
|
|
2580
|
+
async run(request) {
|
|
2581
|
+
const validation = validateRequest(request);
|
|
2582
|
+
if (!validation.valid) {
|
|
2583
|
+
const invalidResult = new ResultBuilder().error(`\u8BF7\u6C42\u6821\u9A8C\u5931\u8D25: ${validation.errors.join("; ")}`).build();
|
|
2584
|
+
await this._emitter.emit("onRunEnd", {
|
|
2585
|
+
traceId: this._emitter.newTraceId(),
|
|
2586
|
+
sessionKey: this.resolveSessionKey(request),
|
|
2587
|
+
request,
|
|
2588
|
+
durationMs: 0,
|
|
2589
|
+
activeMs: 0,
|
|
2590
|
+
queuedMs: 0,
|
|
2591
|
+
turnCount: 0,
|
|
2592
|
+
result: invalidResult,
|
|
2593
|
+
success: false,
|
|
2594
|
+
errorClass: "validation",
|
|
2595
|
+
tokens: { input: 0, output: 0 }
|
|
2596
|
+
});
|
|
2597
|
+
return invalidResult;
|
|
2598
|
+
}
|
|
2599
|
+
const finalRequest = normalizeRequest(request);
|
|
2600
|
+
const sessionKey = this.resolveSessionKey(finalRequest);
|
|
2601
|
+
const traceId = this._emitter.newTraceId();
|
|
2602
|
+
const queuedAt = Date.now();
|
|
2603
|
+
await this._emitter.emit("onRunStart", {
|
|
2604
|
+
traceId,
|
|
2605
|
+
sessionKey,
|
|
2606
|
+
request: finalRequest,
|
|
2607
|
+
queuedAt
|
|
2608
|
+
});
|
|
2609
|
+
const session = this._store.ensure(sessionKey);
|
|
2610
|
+
const release = await this._store.acquire(session);
|
|
2611
|
+
const queuedMs = Date.now() - queuedAt;
|
|
2612
|
+
try {
|
|
2613
|
+
return await this._persistence.withStorageLock(
|
|
2614
|
+
finalRequest,
|
|
2615
|
+
sessionKey,
|
|
2616
|
+
() => this._loops.run(finalRequest, sessionKey, session, traceId, queuedMs)
|
|
2617
|
+
);
|
|
2618
|
+
} finally {
|
|
2619
|
+
release();
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
async *stream(request) {
|
|
2623
|
+
const validation = validateRequest(request);
|
|
2624
|
+
if (!validation.valid) {
|
|
2625
|
+
const message = `\u8BF7\u6C42\u6821\u9A8C\u5931\u8D25: ${validation.errors.join("; ")}`;
|
|
2626
|
+
await this._emitter.emit("onRunEnd", {
|
|
2627
|
+
traceId: this._emitter.newTraceId(),
|
|
2628
|
+
sessionKey: this.resolveSessionKey(request),
|
|
2629
|
+
request,
|
|
2630
|
+
durationMs: 0,
|
|
2631
|
+
activeMs: 0,
|
|
2632
|
+
queuedMs: 0,
|
|
2633
|
+
turnCount: 0,
|
|
2634
|
+
result: new ResultBuilder().error(message).build(),
|
|
2635
|
+
success: false,
|
|
2636
|
+
errorClass: "validation",
|
|
2637
|
+
tokens: { input: 0, output: 0 }
|
|
2638
|
+
});
|
|
2639
|
+
yield { type: "error", content: message };
|
|
2640
|
+
yield { type: "done" };
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
const finalRequest = normalizeRequest(request);
|
|
2644
|
+
const sessionKey = this.resolveSessionKey(finalRequest);
|
|
2645
|
+
const traceId = this._emitter.newTraceId();
|
|
2646
|
+
const queuedAt = Date.now();
|
|
2647
|
+
await this._emitter.emit("onRunStart", {
|
|
2648
|
+
traceId,
|
|
2649
|
+
sessionKey,
|
|
2650
|
+
request: finalRequest,
|
|
2651
|
+
queuedAt
|
|
2652
|
+
});
|
|
2653
|
+
const session = this._store.ensure(sessionKey);
|
|
2654
|
+
const release = await this._store.acquire(session);
|
|
2655
|
+
const queuedMs = Date.now() - queuedAt;
|
|
2656
|
+
try {
|
|
2657
|
+
yield* this._persistence.streamWithStorageLock(
|
|
2658
|
+
finalRequest,
|
|
2659
|
+
sessionKey,
|
|
2660
|
+
() => this._loops.stream(finalRequest, sessionKey, session, traceId, queuedMs)
|
|
2661
|
+
);
|
|
2662
|
+
} finally {
|
|
2663
|
+
release();
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
createCompilation(request, sessionKey, session, traceId) {
|
|
2667
|
+
return {
|
|
2668
|
+
request,
|
|
2669
|
+
graph: createTaskGraph(),
|
|
2670
|
+
resources: [],
|
|
2671
|
+
messages: (session ?? this._store.ensure(sessionKey ?? this._sessionKey)).messages,
|
|
2672
|
+
completed: false,
|
|
2673
|
+
traceId: traceId ?? this._emitter.newTraceId()
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
async close() {
|
|
2677
|
+
await this._store.drainAndClear();
|
|
2678
|
+
this._extensions.clear();
|
|
2679
|
+
this._transformers = [];
|
|
2680
|
+
}
|
|
2681
|
+
// ─── 上下文转换(链式执行,原地替换保持 session 引用) ──────────
|
|
2682
|
+
/** 链式执行上下文转换器(按数组顺序,上一个输出作为下一个输入) */
|
|
2683
|
+
async transformMessages(compilation, sessionKey) {
|
|
2684
|
+
if (this._transformers.length === 0) {
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
let resources = messagesToResources(compilation.messages);
|
|
2688
|
+
const originalResources = resources;
|
|
2689
|
+
const context = {
|
|
2690
|
+
graph: compilation.graph,
|
|
2691
|
+
runtime: {
|
|
2692
|
+
sessionKey,
|
|
2693
|
+
turn: compilation.messages.length,
|
|
2694
|
+
contextWindow: this._model.contextWindow,
|
|
2695
|
+
maxTokens: this._model.maxTokens,
|
|
2696
|
+
contextBudgetRatio: this._contextBudgetRatio
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2699
|
+
for (const transformer of this._transformers) {
|
|
2700
|
+
try {
|
|
2701
|
+
const next = await transformer.transform(resources, context);
|
|
2702
|
+
if (next !== resources) resources = next;
|
|
2703
|
+
} catch (err) {
|
|
2704
|
+
console.warn(
|
|
2705
|
+
`[Runtime] \u8F6C\u6362\u5668 "${transformer.name}" \u5931\u8D25\uFF0C\u5DF2\u8DF3\u8FC7:`,
|
|
2706
|
+
err?.message ?? err
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
if (resources === originalResources) {
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
const messages = resourcesToMessages(resources);
|
|
2714
|
+
compilation.messages.splice(0, compilation.messages.length, ...messages);
|
|
2715
|
+
}
|
|
2716
|
+
// ─── 手动压缩(交互命令 /compact 等) ───────────────────────────
|
|
2717
|
+
/**
|
|
2718
|
+
* 手动压缩指定会话:跳过阈值判断,直接将历史压缩至 targetRatio
|
|
2719
|
+
* 保留量(复用 compactor 的保留段计算与 compactOrTruncate 执行路径),
|
|
2720
|
+
* 压缩后持久化。返回压缩模式;compaction 未启用或无可压缩段时返回 null。
|
|
2721
|
+
*/
|
|
2722
|
+
async compact(sessionKey) {
|
|
2723
|
+
if (!this._compaction || this._compaction.enabled === false) return null;
|
|
2724
|
+
const key = sessionKey ?? this._sessionKey;
|
|
2725
|
+
const session = this._store.ensure(key);
|
|
2726
|
+
const messages = session.messages;
|
|
2727
|
+
if (messages.length === 0) return null;
|
|
2728
|
+
const contextWindow = this._model.contextWindow;
|
|
2729
|
+
if (!contextWindow || contextWindow <= 0) return null;
|
|
2730
|
+
const split = this._compactor.computeKeepSplit(messages, contextWindow);
|
|
2731
|
+
if (split <= 0 || split >= messages.length) return null;
|
|
2732
|
+
const mode = await this._compactor.compactOrTruncate(
|
|
2733
|
+
messages,
|
|
2734
|
+
split,
|
|
2735
|
+
key,
|
|
2736
|
+
`manual-${Date.now().toString(36)}`,
|
|
2737
|
+
new AbortController().signal,
|
|
2738
|
+
"threshold"
|
|
2739
|
+
);
|
|
2740
|
+
try {
|
|
2741
|
+
await this._persistence.persist(key, session);
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
console.warn("[Runtime] \u624B\u52A8\u538B\u7F29\u540E\u6301\u4E45\u5316\u5931\u8D25:", err?.message);
|
|
2744
|
+
}
|
|
2745
|
+
return mode;
|
|
2746
|
+
}
|
|
2426
2747
|
// ─── 便捷方法 ───────────────────────────────────────────────────
|
|
2427
2748
|
/** 终止指定会话的运行(默认会话;会话不存在为 no-op) */
|
|
2428
2749
|
abort(sessionKey) {
|
|
2429
|
-
this.
|
|
2750
|
+
this._store.peek(sessionKey ?? this._sessionKey)?.abortController?.abort();
|
|
2430
2751
|
}
|
|
2431
2752
|
/** 检查指定会话是否正在运行(默认会话;会话不存在返回 false) */
|
|
2432
2753
|
isBusy(sessionKey) {
|
|
2433
|
-
return this.
|
|
2754
|
+
return this._store.peek(sessionKey ?? this._sessionKey)?.isStreaming ?? false;
|
|
2434
2755
|
}
|
|
2435
2756
|
/** 等待指定会话空闲(默认会话;基于 promise,无轮询) */
|
|
2436
2757
|
async waitForIdle(sessionKey, timeoutMs) {
|
|
2437
|
-
|
|
2438
|
-
if (!session || !session.isStreaming) return;
|
|
2439
|
-
if (timeoutMs === void 0) {
|
|
2440
|
-
await new Promise((resolve) => {
|
|
2441
|
-
session.idleResolvers.push(resolve);
|
|
2442
|
-
});
|
|
2443
|
-
return;
|
|
2444
|
-
}
|
|
2445
|
-
await new Promise((resolve, reject) => {
|
|
2446
|
-
const resolver = () => {
|
|
2447
|
-
clearTimeout(timer);
|
|
2448
|
-
resolve();
|
|
2449
|
-
};
|
|
2450
|
-
const timer = setTimeout(() => {
|
|
2451
|
-
const i = session.idleResolvers.indexOf(resolver);
|
|
2452
|
-
if (i >= 0) session.idleResolvers.splice(i, 1);
|
|
2453
|
-
reject(new Error(
|
|
2454
|
-
`[Runtime] waitForIdle \u8D85\u65F6\uFF08${timeoutMs}ms\uFF09: ${sessionKey ?? "default"}`
|
|
2455
|
-
));
|
|
2456
|
-
}, timeoutMs);
|
|
2457
|
-
session.idleResolvers.push(resolver);
|
|
2458
|
-
});
|
|
2758
|
+
await this._store.waitForIdle(sessionKey ?? this._sessionKey, timeoutMs);
|
|
2459
2759
|
}
|
|
2460
2760
|
/** 清除指定会话消息(仅内存;下次 run 会从存储恢复) */
|
|
2461
2761
|
clearSession(sessionKey) {
|
|
2462
|
-
|
|
2463
|
-
if (!session) return;
|
|
2464
|
-
session.messages = [];
|
|
2465
|
-
session.hydrated = false;
|
|
2762
|
+
this._store.clearMessages(sessionKey ?? this._sessionKey);
|
|
2466
2763
|
}
|
|
2467
2764
|
/** 删除指定会话(内存 + 存储),返回是否删除成功 */
|
|
2468
2765
|
async deleteSession(sessionKey) {
|
|
2469
2766
|
const key = sessionKey ?? this._sessionKey;
|
|
2470
|
-
|
|
2471
|
-
if (session) {
|
|
2472
|
-
await session.queue;
|
|
2473
|
-
this._sessions.delete(key);
|
|
2474
|
-
}
|
|
2475
|
-
if (!this._sessionStorage) return true;
|
|
2767
|
+
await this._store.remove(key);
|
|
2476
2768
|
const storage = this._sessionStorage;
|
|
2769
|
+
if (!storage) return true;
|
|
2477
2770
|
if (!storage.withLock) return storage.delete(key);
|
|
2478
2771
|
return storage.withLock(key, () => storage.delete(key));
|
|
2479
2772
|
}
|