minimal-agent 0.18.9 → 0.18.11
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/package.json +1 -1
- package/src/advisor/advisorCoordinator.js +30 -6
- package/src/advisor/advisorCoordinator.ts +23 -6
- package/src/advisor/deliveryReview.js +23 -0
- package/src/advisor/deliveryReview.ts +22 -0
- package/src/advisor/presentation.js +3 -1
- package/src/advisor/presentation.ts +4 -1
- package/src/loop/README.md +44 -0
- package/src/loop/deliveryIntent.js +22 -0
- package/src/loop/deliveryIntent.ts +39 -0
- package/src/loop/messages.js +146 -0
- package/src/loop/messages.ts +166 -0
- package/src/loop/options.js +1 -0
- package/src/loop/options.ts +155 -0
- package/src/loop/progressStream.js +40 -0
- package/src/loop/progressStream.ts +44 -0
- package/src/loop/publication.js +94 -0
- package/src/loop/publication.ts +124 -0
- package/src/loop/requestPreparation.js +180 -0
- package/src/loop/requestPreparation.ts +226 -0
- package/src/loop/toolBatch.js +99 -0
- package/src/loop/toolBatch.ts +123 -0
- package/src/loop/toolExecutor.js +304 -0
- package/src/loop/toolExecutor.ts +363 -0
- package/src/loop/toolPresentation.js +135 -0
- package/src/loop/toolPresentation.ts +134 -0
- package/src/loop/toolSemantics.js +45 -0
- package/src/loop/toolSemantics.ts +51 -0
- package/src/loop.js +368 -1111
- package/src/loop.ts +480 -1378
- package/src/tools/advisor/advisor.js +5 -2
- package/src/tools/advisor/advisor.ts +6 -2
- package/src/tools/applypatch/applyPatch.js +1 -1
- package/src/tools/applypatch/applyPatch.ts +1 -1
- package/src/tools/bash/semantics.js +33 -1
- package/src/tools/bash/semantics.ts +27 -1
- package/src/tools/edit/edit.js +11 -15
- package/src/tools/edit/edit.ts +10 -15
- package/src/tools/edit/multi-edit.js +14 -4
- package/src/tools/edit/multi-edit.ts +10 -4
- package/src/tools/read/read.js +91 -47
- package/src/tools/read/read.ts +77 -46
- package/src/tools/shared/diffSummary.js +25 -3
- package/src/tools/shared/diffSummary.ts +23 -3
- package/src/tools/shared/editTransaction.js +45 -0
- package/src/tools/shared/editTransaction.ts +39 -0
- package/src/tools/shared/fileActivity.js +10 -2
- package/src/tools/shared/fileActivity.ts +11 -2
- package/src/tools/shared/fileState.js +2 -0
- package/src/tools/shared/fileState.ts +4 -0
- package/src/tools/shared/readBudget.js +12 -0
- package/src/tools/shared/readBudget.ts +12 -0
- package/src/tools/toolPromptGuides.js +2 -2
- package/src/tools/toolPromptGuides.ts +2 -2
- package/src/tools/types.js +2 -6
- package/src/tools/types.ts +2 -6
- package/src/types.ts +9 -2
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { buildContextBudgetPlan, fitToolsForEmergency } from '../context/budgetPlan.js';
|
|
2
|
+
import { autoCompactIfNeeded, shouldAutoCompact } from '../context/compact.js';
|
|
3
|
+
import { snipCompactIfNeeded } from '../context/snipCompact.js';
|
|
4
|
+
import { countMessagesTokens, countTextTokens, estimateToolsTokens } from '../context/tokenCounter.js';
|
|
5
|
+
import { createEffectiveToolSet } from '../tools/effectiveToolSet.js';
|
|
6
|
+
/** Request-local degradation; never owns the accumulated turn resource ledger. */
|
|
7
|
+
export async function* prepareModelRequest(input) {
|
|
8
|
+
const { history, provider, activeTools, effectiveToolSet, runtimeContext, sessionState, signal, lastPromptTokens, cacheEditsMode, options, chatFn, replaceHistory, buildRequestTransientContext, } = input;
|
|
9
|
+
// 2. 自动压缩(判据 = max(上一轮真实 usage, 历史估算 + 工具 schema),
|
|
10
|
+
// 阈值 provider 无关、按 contextWindow × compactRatio 比例触发)
|
|
11
|
+
let currentEffectiveToolSet = effectiveToolSet;
|
|
12
|
+
let requestTransientContext = buildRequestTransientContext(currentEffectiveToolSet);
|
|
13
|
+
const preCompactBudget = buildContextBudgetPlan({
|
|
14
|
+
provider,
|
|
15
|
+
history,
|
|
16
|
+
tools: activeTools,
|
|
17
|
+
transientContext: requestTransientContext,
|
|
18
|
+
});
|
|
19
|
+
if (preCompactBudget.overBudget || lastPromptTokens !== undefined) {
|
|
20
|
+
try {
|
|
21
|
+
const compactOptions = {
|
|
22
|
+
actualPromptTokens: lastPromptTokens,
|
|
23
|
+
toolsTokens: estimateToolsTokens(activeTools),
|
|
24
|
+
transientTokens: countTextTokens(requestTransientContext),
|
|
25
|
+
// cacheEdits 判据校正:本地 history 未瘦身,扣掉服务端已删部分的估算,
|
|
26
|
+
// 防止虚高估算劫持 Math.max 导致压缩过早触发(压缩会重建整个前缀缓存)。
|
|
27
|
+
...(cacheEditsMode && sessionState?.microCompact
|
|
28
|
+
? {
|
|
29
|
+
serverDeletedTokens: sessionState.microCompact.cacheEdits.activeDeletedTokens,
|
|
30
|
+
}
|
|
31
|
+
: {}),
|
|
32
|
+
signal,
|
|
33
|
+
promptCache: options.promptCache,
|
|
34
|
+
chat: chatFn,
|
|
35
|
+
};
|
|
36
|
+
if (shouldAutoCompact(history, provider, compactOptions)) {
|
|
37
|
+
yield { type: 'compact_start' };
|
|
38
|
+
yield { type: 'stage_change', stage: 'compacting' };
|
|
39
|
+
}
|
|
40
|
+
const compact = await autoCompactIfNeeded(history, provider, compactOptions);
|
|
41
|
+
if (compact.compacted) {
|
|
42
|
+
replaceHistory(compact.messages);
|
|
43
|
+
yield {
|
|
44
|
+
type: 'compact_done',
|
|
45
|
+
before: compact.before,
|
|
46
|
+
after: compact.after,
|
|
47
|
+
trigger: 'threshold',
|
|
48
|
+
mode: compact.mode,
|
|
49
|
+
degraded: compact.degraded,
|
|
50
|
+
...(compact.fallbackReason ? { fallbackReason: compact.fallbackReason } : {}),
|
|
51
|
+
...(compact.messagesRemoved !== undefined ? { messagesRemoved: compact.messagesRemoved } : {}),
|
|
52
|
+
...(compact.tokensFreed !== undefined ? { tokensFreed: compact.tokensFreed } : {}),
|
|
53
|
+
};
|
|
54
|
+
yield { type: 'stage_change', stage: 'thinking' };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
if (signal?.aborted || e.name === 'AbortError') {
|
|
59
|
+
return { kind: 'interrupted' };
|
|
60
|
+
}
|
|
61
|
+
// 压缩失败不要让整个对话挂掉:先 snip 砍最老消息释放空间再继续。
|
|
62
|
+
let recovered = false;
|
|
63
|
+
try {
|
|
64
|
+
const before = countMessagesTokens(history);
|
|
65
|
+
const snipped = snipCompactIfNeeded(history, {
|
|
66
|
+
force: true,
|
|
67
|
+
snipPercent: 0.3,
|
|
68
|
+
});
|
|
69
|
+
if (snipped.messagesRemoved > 0) {
|
|
70
|
+
replaceHistory(snipped.messages);
|
|
71
|
+
recovered = true;
|
|
72
|
+
yield {
|
|
73
|
+
type: 'compact_done',
|
|
74
|
+
before,
|
|
75
|
+
after: countMessagesTokens(history),
|
|
76
|
+
trigger: 'threshold',
|
|
77
|
+
mode: 'snip-fallback',
|
|
78
|
+
degraded: true,
|
|
79
|
+
fallbackReason: 'llm-error',
|
|
80
|
+
messagesRemoved: snipped.messagesRemoved,
|
|
81
|
+
tokensFreed: snipped.tokensFreed,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* snip 也失败 → 真的只能硬上 */
|
|
87
|
+
}
|
|
88
|
+
if (!recovered) {
|
|
89
|
+
// 事件是 provider 原始报错唯一该去的地方:只到 stderr / UI,不进 history、
|
|
90
|
+
// 不落盘、不会在后续轮次重新发给模型(compact.ts 里那条降级消息因此保持无错误体)。
|
|
91
|
+
yield {
|
|
92
|
+
type: 'error',
|
|
93
|
+
error: `自动压缩失败,规则式截断也未能释放空间;已继续尝试回答:${e.message}`,
|
|
94
|
+
code: 'compact_failed',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Re-open transient capacity after compaction. If the compacted tail is still
|
|
100
|
+
// too large, trim old history before sacrificing any model-visible ability.
|
|
101
|
+
requestTransientContext = buildRequestTransientContext(currentEffectiveToolSet);
|
|
102
|
+
let postCompactBudget = buildContextBudgetPlan({
|
|
103
|
+
provider,
|
|
104
|
+
history,
|
|
105
|
+
tools: activeTools,
|
|
106
|
+
transientContext: requestTransientContext,
|
|
107
|
+
});
|
|
108
|
+
if (postCompactBudget.overBudget) {
|
|
109
|
+
const before = countMessagesTokens(history);
|
|
110
|
+
const snipped = snipCompactIfNeeded(history, {
|
|
111
|
+
force: true,
|
|
112
|
+
snipPercent: 0.3,
|
|
113
|
+
});
|
|
114
|
+
if (snipped.messagesRemoved > 0) {
|
|
115
|
+
replaceHistory(snipped.messages);
|
|
116
|
+
yield {
|
|
117
|
+
type: 'compact_done',
|
|
118
|
+
before,
|
|
119
|
+
after: countMessagesTokens(history),
|
|
120
|
+
trigger: 'post-budget',
|
|
121
|
+
mode: 'snip-fallback',
|
|
122
|
+
degraded: true,
|
|
123
|
+
fallbackReason: 'still-over-budget',
|
|
124
|
+
messagesRemoved: snipped.messagesRemoved,
|
|
125
|
+
tokensFreed: snipped.tokensFreed,
|
|
126
|
+
};
|
|
127
|
+
requestTransientContext = buildRequestTransientContext(currentEffectiveToolSet);
|
|
128
|
+
postCompactBudget = buildContextBudgetPlan({
|
|
129
|
+
provider,
|
|
130
|
+
history,
|
|
131
|
+
tools: activeTools,
|
|
132
|
+
transientContext: requestTransientContext,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const emergencyFit = fitToolsForEmergency({
|
|
137
|
+
provider,
|
|
138
|
+
history,
|
|
139
|
+
tools: activeTools,
|
|
140
|
+
transientContext: requestTransientContext,
|
|
141
|
+
requiredNames: effectiveToolSet.requiredNames,
|
|
142
|
+
});
|
|
143
|
+
currentEffectiveToolSet =
|
|
144
|
+
emergencyFit.removedNames.length === 0
|
|
145
|
+
? effectiveToolSet
|
|
146
|
+
: createEffectiveToolSet({
|
|
147
|
+
availableTools: emergencyFit.tools,
|
|
148
|
+
runtime: runtimeContext,
|
|
149
|
+
requiredNames: [...effectiveToolSet.requiredNames],
|
|
150
|
+
});
|
|
151
|
+
// Tool availability may change during emergency fitting. Rebuild dynamic
|
|
152
|
+
// guidance against the actual request boundary, then account for its size.
|
|
153
|
+
requestTransientContext = buildRequestTransientContext(currentEffectiveToolSet);
|
|
154
|
+
const finalRequestBudget = buildContextBudgetPlan({
|
|
155
|
+
provider,
|
|
156
|
+
history,
|
|
157
|
+
tools: currentEffectiveToolSet.tools,
|
|
158
|
+
transientContext: requestTransientContext,
|
|
159
|
+
});
|
|
160
|
+
if (emergencyFit.removedNames.length > 0) {
|
|
161
|
+
yield {
|
|
162
|
+
type: 'warning',
|
|
163
|
+
message: `当前模型窗口压力过高,本次请求临时省略工具 Schema:${emergencyFit.removedNames.join(', ')};下一轮会重新评估并恢复。`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (finalRequestBudget.overBudget) {
|
|
167
|
+
yield {
|
|
168
|
+
type: 'error',
|
|
169
|
+
error: `当前请求即使经过上下文降级仍需要约 ${finalRequestBudget.totalInputTokens} tokens,超过模型输入上限 ${finalRequestBudget.inputLimit}。已停止发送,避免 Provider 接收必然失败的超限请求。`,
|
|
170
|
+
code: 'context_overflow',
|
|
171
|
+
};
|
|
172
|
+
return { kind: 'overflow', requestTransientContext };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
kind: 'ready',
|
|
176
|
+
requestTransientContext,
|
|
177
|
+
currentEffectiveToolSet,
|
|
178
|
+
remainingTokens: finalRequestBudget.inputLimit - finalRequestBudget.totalInputTokens,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { buildContextBudgetPlan, fitToolsForEmergency } from '../context/budgetPlan.js';
|
|
2
|
+
import { autoCompactIfNeeded, shouldAutoCompact } from '../context/compact.js';
|
|
3
|
+
import { snipCompactIfNeeded } from '../context/snipCompact.js';
|
|
4
|
+
import { countMessagesTokens, countTextTokens, estimateToolsTokens } from '../context/tokenCounter.js';
|
|
5
|
+
import type { AgentRuntimeContext } from '../runtime/agentRuntimeContext.js';
|
|
6
|
+
import type { SessionState } from '../session/sessionState.js';
|
|
7
|
+
import { createEffectiveToolSet, type EffectiveToolSet } from '../tools/effectiveToolSet.js';
|
|
8
|
+
import type { LoopEvent, Message, Provider, Tool } from '../types.js';
|
|
9
|
+
import type { RunQueryOptions } from './options.js';
|
|
10
|
+
|
|
11
|
+
/** Request-local degradation; never owns the accumulated turn resource ledger. */
|
|
12
|
+
export async function* prepareModelRequest(input: {
|
|
13
|
+
history: Message[];
|
|
14
|
+
provider: Provider;
|
|
15
|
+
activeTools: Tool[];
|
|
16
|
+
effectiveToolSet: EffectiveToolSet;
|
|
17
|
+
runtimeContext: AgentRuntimeContext;
|
|
18
|
+
sessionState?: SessionState;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
lastPromptTokens?: number;
|
|
21
|
+
cacheEditsMode: boolean;
|
|
22
|
+
options: Pick<RunQueryOptions, 'promptCache'>;
|
|
23
|
+
chatFn: NonNullable<RunQueryOptions['chat']>;
|
|
24
|
+
replaceHistory: (messages: Message[]) => void;
|
|
25
|
+
buildRequestTransientContext: (tools: EffectiveToolSet) => string;
|
|
26
|
+
}): AsyncGenerator<
|
|
27
|
+
LoopEvent,
|
|
28
|
+
| {
|
|
29
|
+
kind: 'ready';
|
|
30
|
+
requestTransientContext: string;
|
|
31
|
+
currentEffectiveToolSet: EffectiveToolSet;
|
|
32
|
+
remainingTokens: number;
|
|
33
|
+
}
|
|
34
|
+
| { kind: 'interrupted' }
|
|
35
|
+
| { kind: 'overflow'; requestTransientContext: string }
|
|
36
|
+
> {
|
|
37
|
+
const {
|
|
38
|
+
history,
|
|
39
|
+
provider,
|
|
40
|
+
activeTools,
|
|
41
|
+
effectiveToolSet,
|
|
42
|
+
runtimeContext,
|
|
43
|
+
sessionState,
|
|
44
|
+
signal,
|
|
45
|
+
lastPromptTokens,
|
|
46
|
+
cacheEditsMode,
|
|
47
|
+
options,
|
|
48
|
+
chatFn,
|
|
49
|
+
replaceHistory,
|
|
50
|
+
buildRequestTransientContext,
|
|
51
|
+
} = input;
|
|
52
|
+
// 2. 自动压缩(判据 = max(上一轮真实 usage, 历史估算 + 工具 schema),
|
|
53
|
+
// 阈值 provider 无关、按 contextWindow × compactRatio 比例触发)
|
|
54
|
+
let currentEffectiveToolSet = effectiveToolSet;
|
|
55
|
+
let requestTransientContext = buildRequestTransientContext(currentEffectiveToolSet);
|
|
56
|
+
const preCompactBudget = buildContextBudgetPlan({
|
|
57
|
+
provider,
|
|
58
|
+
history,
|
|
59
|
+
tools: activeTools,
|
|
60
|
+
transientContext: requestTransientContext,
|
|
61
|
+
});
|
|
62
|
+
if (preCompactBudget.overBudget || lastPromptTokens !== undefined) {
|
|
63
|
+
try {
|
|
64
|
+
const compactOptions = {
|
|
65
|
+
actualPromptTokens: lastPromptTokens,
|
|
66
|
+
toolsTokens: estimateToolsTokens(activeTools),
|
|
67
|
+
transientTokens: countTextTokens(requestTransientContext),
|
|
68
|
+
// cacheEdits 判据校正:本地 history 未瘦身,扣掉服务端已删部分的估算,
|
|
69
|
+
// 防止虚高估算劫持 Math.max 导致压缩过早触发(压缩会重建整个前缀缓存)。
|
|
70
|
+
...(cacheEditsMode && sessionState?.microCompact
|
|
71
|
+
? {
|
|
72
|
+
serverDeletedTokens: sessionState.microCompact.cacheEdits.activeDeletedTokens,
|
|
73
|
+
}
|
|
74
|
+
: {}),
|
|
75
|
+
signal,
|
|
76
|
+
promptCache: options.promptCache,
|
|
77
|
+
chat: chatFn,
|
|
78
|
+
};
|
|
79
|
+
if (shouldAutoCompact(history, provider, compactOptions)) {
|
|
80
|
+
yield { type: 'compact_start' };
|
|
81
|
+
yield { type: 'stage_change', stage: 'compacting' };
|
|
82
|
+
}
|
|
83
|
+
const compact = await autoCompactIfNeeded(history, provider, compactOptions);
|
|
84
|
+
if (compact.compacted) {
|
|
85
|
+
replaceHistory(compact.messages);
|
|
86
|
+
yield {
|
|
87
|
+
type: 'compact_done',
|
|
88
|
+
before: compact.before,
|
|
89
|
+
after: compact.after,
|
|
90
|
+
trigger: 'threshold',
|
|
91
|
+
mode: compact.mode,
|
|
92
|
+
degraded: compact.degraded,
|
|
93
|
+
...(compact.fallbackReason ? { fallbackReason: compact.fallbackReason } : {}),
|
|
94
|
+
...(compact.messagesRemoved !== undefined ? { messagesRemoved: compact.messagesRemoved } : {}),
|
|
95
|
+
...(compact.tokensFreed !== undefined ? { tokensFreed: compact.tokensFreed } : {}),
|
|
96
|
+
};
|
|
97
|
+
yield { type: 'stage_change', stage: 'thinking' };
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if (signal?.aborted || (e as Error).name === 'AbortError') {
|
|
101
|
+
return { kind: 'interrupted' as const };
|
|
102
|
+
}
|
|
103
|
+
// 压缩失败不要让整个对话挂掉:先 snip 砍最老消息释放空间再继续。
|
|
104
|
+
let recovered = false;
|
|
105
|
+
try {
|
|
106
|
+
const before = countMessagesTokens(history);
|
|
107
|
+
const snipped = snipCompactIfNeeded(history, {
|
|
108
|
+
force: true,
|
|
109
|
+
snipPercent: 0.3,
|
|
110
|
+
});
|
|
111
|
+
if (snipped.messagesRemoved > 0) {
|
|
112
|
+
replaceHistory(snipped.messages);
|
|
113
|
+
recovered = true;
|
|
114
|
+
yield {
|
|
115
|
+
type: 'compact_done',
|
|
116
|
+
before,
|
|
117
|
+
after: countMessagesTokens(history),
|
|
118
|
+
trigger: 'threshold',
|
|
119
|
+
mode: 'snip-fallback',
|
|
120
|
+
degraded: true,
|
|
121
|
+
fallbackReason: 'llm-error',
|
|
122
|
+
messagesRemoved: snipped.messagesRemoved,
|
|
123
|
+
tokensFreed: snipped.tokensFreed,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
/* snip 也失败 → 真的只能硬上 */
|
|
128
|
+
}
|
|
129
|
+
if (!recovered) {
|
|
130
|
+
// 事件是 provider 原始报错唯一该去的地方:只到 stderr / UI,不进 history、
|
|
131
|
+
// 不落盘、不会在后续轮次重新发给模型(compact.ts 里那条降级消息因此保持无错误体)。
|
|
132
|
+
yield {
|
|
133
|
+
type: 'error',
|
|
134
|
+
error: `自动压缩失败,规则式截断也未能释放空间;已继续尝试回答:${(e as Error).message}`,
|
|
135
|
+
code: 'compact_failed',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Re-open transient capacity after compaction. If the compacted tail is still
|
|
142
|
+
// too large, trim old history before sacrificing any model-visible ability.
|
|
143
|
+
requestTransientContext = buildRequestTransientContext(currentEffectiveToolSet);
|
|
144
|
+
let postCompactBudget = buildContextBudgetPlan({
|
|
145
|
+
provider,
|
|
146
|
+
history,
|
|
147
|
+
tools: activeTools,
|
|
148
|
+
transientContext: requestTransientContext,
|
|
149
|
+
});
|
|
150
|
+
if (postCompactBudget.overBudget) {
|
|
151
|
+
const before = countMessagesTokens(history);
|
|
152
|
+
const snipped = snipCompactIfNeeded(history, {
|
|
153
|
+
force: true,
|
|
154
|
+
snipPercent: 0.3,
|
|
155
|
+
});
|
|
156
|
+
if (snipped.messagesRemoved > 0) {
|
|
157
|
+
replaceHistory(snipped.messages);
|
|
158
|
+
yield {
|
|
159
|
+
type: 'compact_done',
|
|
160
|
+
before,
|
|
161
|
+
after: countMessagesTokens(history),
|
|
162
|
+
trigger: 'post-budget',
|
|
163
|
+
mode: 'snip-fallback',
|
|
164
|
+
degraded: true,
|
|
165
|
+
fallbackReason: 'still-over-budget',
|
|
166
|
+
messagesRemoved: snipped.messagesRemoved,
|
|
167
|
+
tokensFreed: snipped.tokensFreed,
|
|
168
|
+
};
|
|
169
|
+
requestTransientContext = buildRequestTransientContext(currentEffectiveToolSet);
|
|
170
|
+
postCompactBudget = buildContextBudgetPlan({
|
|
171
|
+
provider,
|
|
172
|
+
history,
|
|
173
|
+
tools: activeTools,
|
|
174
|
+
transientContext: requestTransientContext,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const emergencyFit = fitToolsForEmergency({
|
|
180
|
+
provider,
|
|
181
|
+
history,
|
|
182
|
+
tools: activeTools,
|
|
183
|
+
transientContext: requestTransientContext,
|
|
184
|
+
requiredNames: effectiveToolSet.requiredNames,
|
|
185
|
+
});
|
|
186
|
+
currentEffectiveToolSet =
|
|
187
|
+
emergencyFit.removedNames.length === 0
|
|
188
|
+
? effectiveToolSet
|
|
189
|
+
: createEffectiveToolSet({
|
|
190
|
+
availableTools: emergencyFit.tools,
|
|
191
|
+
runtime: runtimeContext,
|
|
192
|
+
requiredNames: [...effectiveToolSet.requiredNames],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Tool availability may change during emergency fitting. Rebuild dynamic
|
|
196
|
+
// guidance against the actual request boundary, then account for its size.
|
|
197
|
+
requestTransientContext = buildRequestTransientContext(currentEffectiveToolSet);
|
|
198
|
+
const finalRequestBudget = buildContextBudgetPlan({
|
|
199
|
+
provider,
|
|
200
|
+
history,
|
|
201
|
+
tools: currentEffectiveToolSet.tools,
|
|
202
|
+
transientContext: requestTransientContext,
|
|
203
|
+
});
|
|
204
|
+
if (emergencyFit.removedNames.length > 0) {
|
|
205
|
+
yield {
|
|
206
|
+
type: 'warning',
|
|
207
|
+
message: `当前模型窗口压力过高,本次请求临时省略工具 Schema:${emergencyFit.removedNames.join(', ')};下一轮会重新评估并恢复。`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (finalRequestBudget.overBudget) {
|
|
212
|
+
yield {
|
|
213
|
+
type: 'error',
|
|
214
|
+
error: `当前请求即使经过上下文降级仍需要约 ${finalRequestBudget.totalInputTokens} tokens,超过模型输入上限 ${finalRequestBudget.inputLimit}。已停止发送,避免 Provider 接收必然失败的超限请求。`,
|
|
215
|
+
code: 'context_overflow',
|
|
216
|
+
};
|
|
217
|
+
return { kind: 'overflow' as const, requestTransientContext };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
kind: 'ready',
|
|
222
|
+
requestTransientContext,
|
|
223
|
+
currentEffectiveToolSet,
|
|
224
|
+
remainingTokens: finalRequestBudget.inputLimit - finalRequestBudget.totalInputTokens,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { progressStream } from './progressStream.js';
|
|
2
|
+
import { getToolByName } from '../tools/index.js';
|
|
3
|
+
import { executionSegments } from '../tools/executionSegments.js';
|
|
4
|
+
import { orderedPool } from '../runtime/orderedPool.js';
|
|
5
|
+
import { conciseToolPath } from '../runtime/toolDisplay.js';
|
|
6
|
+
import { previewArgs, friendlyToolDescription } from './toolPresentation.js';
|
|
7
|
+
/** Live events follow completion order; history commits follow request order. */
|
|
8
|
+
export async function* executeToolBatch(input) {
|
|
9
|
+
const { toolCallsByIndex, currentEffectiveToolSet, options, runOne, commitOutcome } = input;
|
|
10
|
+
const startEvent = (tc) => ({
|
|
11
|
+
type: 'tool_start',
|
|
12
|
+
toolCallId: tc.id,
|
|
13
|
+
toolName: tc.function.name,
|
|
14
|
+
argsPreview: previewArgs(tc.function.arguments),
|
|
15
|
+
argsFriendly: friendlyToolDescription(tc.function.name, tc.function.arguments),
|
|
16
|
+
argsConcise: friendlyToolDescription(tc.function.name, tc.function.arguments, conciseToolPath),
|
|
17
|
+
});
|
|
18
|
+
const endEvent = (o) => ({
|
|
19
|
+
type: 'tool_end',
|
|
20
|
+
toolCallId: o.tc.id,
|
|
21
|
+
toolName: o.tc.function.name,
|
|
22
|
+
ok: o.result.ok,
|
|
23
|
+
content: o.result.ok ? o.result.content : o.result.error === 'aborted' ? '(已中断)' : `Error: ${o.result.error}`,
|
|
24
|
+
metadata: o.result.metadata,
|
|
25
|
+
});
|
|
26
|
+
const segments = executionSegments(toolCallsByIndex, (tc) => {
|
|
27
|
+
const tool = currentEffectiveToolSet.tools.find((tool) => tool.name === tc.function.name);
|
|
28
|
+
return (![
|
|
29
|
+
'AskUserQuestion',
|
|
30
|
+
'Advisor',
|
|
31
|
+
'AdvisorDecision',
|
|
32
|
+
'OptimizeConversation',
|
|
33
|
+
'AgentReport',
|
|
34
|
+
'AgentResolve',
|
|
35
|
+
].includes(tc.function.name) &&
|
|
36
|
+
(tool?.isConcurrencySafe ?? getToolByName(tc.function.name)?.isConcurrencySafe) === true);
|
|
37
|
+
});
|
|
38
|
+
const settled = [];
|
|
39
|
+
let questionEncountered = false;
|
|
40
|
+
let advisorEncountered = false;
|
|
41
|
+
for (const segment of segments) {
|
|
42
|
+
if (!questionEncountered && !advisorEncountered && segment.length > 1) {
|
|
43
|
+
// Only concurrency-safe segments run together; hosts may bound throughput.
|
|
44
|
+
const outcomes = yield* progressStream((enqueue) => orderedPool(segment, options.toolConcurrency ?? segment.length, async (tc) => {
|
|
45
|
+
enqueue(startEvent(tc));
|
|
46
|
+
const o = await runOne(tc, (message, detail) => enqueue({
|
|
47
|
+
type: 'tool_progress',
|
|
48
|
+
toolCallId: tc.id,
|
|
49
|
+
toolName: tc.function.name,
|
|
50
|
+
message,
|
|
51
|
+
detail,
|
|
52
|
+
}));
|
|
53
|
+
enqueue(endEvent(o));
|
|
54
|
+
return o;
|
|
55
|
+
}));
|
|
56
|
+
settled.push(...outcomes);
|
|
57
|
+
for (const outcome of outcomes)
|
|
58
|
+
yield* commitOutcome(outcome);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// —— 串行:逐个执行(含写类工具,或只有一个工具)。保持实时进度,完成后才提交历史。
|
|
62
|
+
for (const tc of segment) {
|
|
63
|
+
if (questionEncountered || advisorEncountered) {
|
|
64
|
+
const outcome = {
|
|
65
|
+
tc,
|
|
66
|
+
result: {
|
|
67
|
+
ok: false,
|
|
68
|
+
disposition: 'deferred',
|
|
69
|
+
error: questionEncountered
|
|
70
|
+
? '此调用在用户回答前已生成,尚未执行。请先阅读提问结果并重新规划;仅在仍必要时重新调用。'
|
|
71
|
+
: '此调用在顾问返回前已生成,尚未执行。请先阅读顾问结果,自主决定采纳、部分采纳或拒绝,再重新生成必要的工具调用。咨询失败则自行判断继续,不必重复咨询。',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
yield startEvent(tc);
|
|
75
|
+
yield endEvent(outcome);
|
|
76
|
+
settled.push(outcome);
|
|
77
|
+
yield* commitOutcome(outcome);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
questionEncountered = tc.function.name === 'AskUserQuestion';
|
|
81
|
+
yield startEvent(tc);
|
|
82
|
+
const o = yield* progressStream((emit) => runOne(tc, (message, detail) => emit({
|
|
83
|
+
type: 'tool_progress',
|
|
84
|
+
toolCallId: tc.id,
|
|
85
|
+
toolName: tc.function.name,
|
|
86
|
+
message,
|
|
87
|
+
detail,
|
|
88
|
+
})));
|
|
89
|
+
// Every later call was planned without this second opinion, including
|
|
90
|
+
// decision records and further consultations. Return control to the LLM.
|
|
91
|
+
advisorEncountered = tc.function.name === 'Advisor';
|
|
92
|
+
yield endEvent(o);
|
|
93
|
+
settled.push(o);
|
|
94
|
+
yield* commitOutcome(o);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return settled;
|
|
99
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { progressStream } from './progressStream.js';
|
|
2
|
+
import type { LoopEvent, ToolCall } from '../types.js';
|
|
3
|
+
import type { RunQueryOptions } from './options.js';
|
|
4
|
+
import type { EffectiveToolSet } from '../tools/effectiveToolSet.js';
|
|
5
|
+
import { getToolByName } from '../tools/index.js';
|
|
6
|
+
import { executionSegments } from '../tools/executionSegments.js';
|
|
7
|
+
import { orderedPool } from '../runtime/orderedPool.js';
|
|
8
|
+
import { conciseToolPath } from '../runtime/toolDisplay.js';
|
|
9
|
+
import { previewArgs, friendlyToolDescription } from './toolPresentation.js';
|
|
10
|
+
import type { createToolExecutor, WorkerOutcome } from './toolExecutor.js';
|
|
11
|
+
|
|
12
|
+
/** Live events follow completion order; history commits follow request order. */
|
|
13
|
+
export async function* executeToolBatch(input: {
|
|
14
|
+
toolCallsByIndex: ToolCall[];
|
|
15
|
+
currentEffectiveToolSet: EffectiveToolSet;
|
|
16
|
+
options: Pick<RunQueryOptions, 'toolConcurrency'>;
|
|
17
|
+
runOne: ReturnType<typeof createToolExecutor>['runOne'];
|
|
18
|
+
commitOutcome: (outcome: WorkerOutcome) => AsyncGenerator<LoopEvent>;
|
|
19
|
+
}): AsyncGenerator<LoopEvent, WorkerOutcome[]> {
|
|
20
|
+
const { toolCallsByIndex, currentEffectiveToolSet, options, runOne, commitOutcome } = input;
|
|
21
|
+
const startEvent = (tc: ToolCall): Extract<LoopEvent, { type: 'tool_start' }> => ({
|
|
22
|
+
type: 'tool_start',
|
|
23
|
+
toolCallId: tc.id,
|
|
24
|
+
toolName: tc.function.name,
|
|
25
|
+
argsPreview: previewArgs(tc.function.arguments),
|
|
26
|
+
argsFriendly: friendlyToolDescription(tc.function.name, tc.function.arguments),
|
|
27
|
+
argsConcise: friendlyToolDescription(tc.function.name, tc.function.arguments, conciseToolPath),
|
|
28
|
+
});
|
|
29
|
+
const endEvent = (o: WorkerOutcome): Extract<LoopEvent, { type: 'tool_end' }> => ({
|
|
30
|
+
type: 'tool_end',
|
|
31
|
+
toolCallId: o.tc.id,
|
|
32
|
+
toolName: o.tc.function.name,
|
|
33
|
+
ok: o.result.ok,
|
|
34
|
+
content: o.result.ok ? o.result.content : o.result.error === 'aborted' ? '(已中断)' : `Error: ${o.result.error}`,
|
|
35
|
+
metadata: o.result.metadata,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const segments = executionSegments(toolCallsByIndex, (tc) => {
|
|
39
|
+
const tool = currentEffectiveToolSet.tools.find((tool) => tool.name === tc.function.name);
|
|
40
|
+
return (
|
|
41
|
+
![
|
|
42
|
+
'AskUserQuestion',
|
|
43
|
+
'Advisor',
|
|
44
|
+
'AdvisorDecision',
|
|
45
|
+
'OptimizeConversation',
|
|
46
|
+
'AgentReport',
|
|
47
|
+
'AgentResolve',
|
|
48
|
+
].includes(tc.function.name) &&
|
|
49
|
+
(tool?.isConcurrencySafe ?? getToolByName(tc.function.name)?.isConcurrencySafe) === true
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const settled: WorkerOutcome[] = [];
|
|
54
|
+
let questionEncountered = false;
|
|
55
|
+
let advisorEncountered = false;
|
|
56
|
+
|
|
57
|
+
for (const segment of segments) {
|
|
58
|
+
if (!questionEncountered && !advisorEncountered && segment.length > 1) {
|
|
59
|
+
// Only concurrency-safe segments run together; hosts may bound throughput.
|
|
60
|
+
const outcomes = yield* progressStream<LoopEvent, WorkerOutcome[]>((enqueue) =>
|
|
61
|
+
orderedPool(segment, options.toolConcurrency ?? segment.length, async (tc) => {
|
|
62
|
+
enqueue(startEvent(tc));
|
|
63
|
+
const o = await runOne(tc, (message, detail) =>
|
|
64
|
+
enqueue({
|
|
65
|
+
type: 'tool_progress',
|
|
66
|
+
toolCallId: tc.id,
|
|
67
|
+
toolName: tc.function.name,
|
|
68
|
+
message,
|
|
69
|
+
detail,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
enqueue(endEvent(o));
|
|
73
|
+
return o;
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
settled.push(...outcomes);
|
|
78
|
+
for (const outcome of outcomes) yield* commitOutcome(outcome);
|
|
79
|
+
} else {
|
|
80
|
+
// —— 串行:逐个执行(含写类工具,或只有一个工具)。保持实时进度,完成后才提交历史。
|
|
81
|
+
for (const tc of segment) {
|
|
82
|
+
if (questionEncountered || advisorEncountered) {
|
|
83
|
+
const outcome = {
|
|
84
|
+
tc,
|
|
85
|
+
result: {
|
|
86
|
+
ok: false as const,
|
|
87
|
+
disposition: 'deferred' as const,
|
|
88
|
+
error: questionEncountered
|
|
89
|
+
? '此调用在用户回答前已生成,尚未执行。请先阅读提问结果并重新规划;仅在仍必要时重新调用。'
|
|
90
|
+
: '此调用在顾问返回前已生成,尚未执行。请先阅读顾问结果,自主决定采纳、部分采纳或拒绝,再重新生成必要的工具调用。咨询失败则自行判断继续,不必重复咨询。',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
yield startEvent(tc);
|
|
94
|
+
yield endEvent(outcome);
|
|
95
|
+
settled.push(outcome);
|
|
96
|
+
yield* commitOutcome(outcome);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
questionEncountered = tc.function.name === 'AskUserQuestion';
|
|
100
|
+
yield startEvent(tc);
|
|
101
|
+
const o = yield* progressStream<LoopEvent, WorkerOutcome>((emit) =>
|
|
102
|
+
runOne(tc, (message, detail) =>
|
|
103
|
+
emit({
|
|
104
|
+
type: 'tool_progress',
|
|
105
|
+
toolCallId: tc.id,
|
|
106
|
+
toolName: tc.function.name,
|
|
107
|
+
message,
|
|
108
|
+
detail,
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
// Every later call was planned without this second opinion, including
|
|
113
|
+
// decision records and further consultations. Return control to the LLM.
|
|
114
|
+
advisorEncountered = tc.function.name === 'Advisor';
|
|
115
|
+
yield endEvent(o);
|
|
116
|
+
settled.push(o);
|
|
117
|
+
yield* commitOutcome(o);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return settled;
|
|
123
|
+
}
|