@jsonstudio/llms 0.6.1749 → 0.6.1890
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/conversion/compat/actions/deepseek-web-request.d.ts +3 -0
- package/dist/conversion/compat/actions/deepseek-web-request.js +350 -0
- package/dist/conversion/compat/actions/deepseek-web-response.d.ts +3 -0
- package/dist/conversion/compat/actions/deepseek-web-response.js +886 -0
- package/dist/conversion/compat/actions/gemini-cli-request.js +3 -1
- package/dist/conversion/compat/profiles/chat-deepseek-web.json +18 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +166 -2
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +169 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +12 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +4 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +365 -144
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +9 -0
- package/dist/conversion/hub/policy/policy-engine.d.ts +2 -0
- package/dist/conversion/hub/policy/policy-engine.js +8 -0
- package/dist/conversion/hub/process/chat-process.js +466 -16
- package/dist/conversion/hub/response/provider-response.js +0 -35
- package/dist/conversion/responses/responses-openai-bridge.d.ts +2 -0
- package/dist/conversion/responses/responses-openai-bridge.js +166 -8
- package/dist/conversion/shared/anthropic-message-utils.js +10 -1
- package/dist/conversion/shared/protocol-field-allowlists.d.ts +2 -2
- package/dist/conversion/shared/protocol-field-allowlists.js +4 -0
- package/dist/conversion/shared/tool-governor.js +102 -0
- package/dist/guidance/index.js +17 -0
- package/dist/router/virtual-router/bootstrap.js +46 -1
- package/dist/router/virtual-router/classifier.js +59 -4
- package/dist/router/virtual-router/engine/health/index.js +6 -6
- package/dist/router/virtual-router/engine/routing-state/store.js +16 -3
- package/dist/router/virtual-router/engine-logging.js +62 -24
- package/dist/router/virtual-router/engine-selection/route-utils.js +20 -20
- package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -2
- package/dist/router/virtual-router/engine.d.ts +3 -1
- package/dist/router/virtual-router/engine.js +325 -38
- package/dist/router/virtual-router/features.js +2 -1
- package/dist/router/virtual-router/pre-command-file-resolver.d.ts +2 -0
- package/dist/router/virtual-router/pre-command-file-resolver.js +90 -0
- package/dist/router/virtual-router/provider-registry.js +3 -1
- package/dist/router/virtual-router/routing-instructions.d.ts +11 -1
- package/dist/router/virtual-router/routing-instructions.js +101 -183
- package/dist/router/virtual-router/routing-pre-command-actions.d.ts +3 -0
- package/dist/router/virtual-router/routing-pre-command-actions.js +26 -0
- package/dist/router/virtual-router/routing-pre-command-parser.d.ts +2 -0
- package/dist/router/virtual-router/routing-pre-command-parser.js +85 -0
- package/dist/router/virtual-router/routing-pre-command-state-codec.d.ts +3 -0
- package/dist/router/virtual-router/routing-pre-command-state-codec.js +24 -0
- package/dist/router/virtual-router/routing-stop-message-actions.d.ts +2 -0
- package/dist/router/virtual-router/routing-stop-message-actions.js +96 -0
- package/dist/router/virtual-router/routing-stop-message-parser.d.ts +3 -0
- package/dist/router/virtual-router/routing-stop-message-parser.js +142 -0
- package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +4 -0
- package/dist/router/virtual-router/routing-stop-message-state-codec.js +85 -0
- package/dist/router/virtual-router/sticky-session-store.js +206 -57
- package/dist/router/virtual-router/stop-message-stage-template-files.d.ts +12 -0
- package/dist/router/virtual-router/stop-message-stage-template-files.js +67 -0
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
- package/dist/router/virtual-router/stop-message-state-sync.js +1 -0
- package/dist/router/virtual-router/token-file-scanner.d.ts +9 -0
- package/dist/router/virtual-router/token-file-scanner.js +64 -3
- package/dist/router/virtual-router/tool-signals.d.ts +5 -0
- package/dist/router/virtual-router/tool-signals.js +42 -3
- package/dist/router/virtual-router/types.d.ts +15 -1
- package/dist/router/virtual-router/types.js +1 -0
- package/dist/servertool/clock/config.d.ts +1 -1
- package/dist/servertool/clock/config.js +27 -4
- package/dist/servertool/clock/state.js +41 -2
- package/dist/servertool/clock/task-store.d.ts +2 -2
- package/dist/servertool/clock/task-store.js +1 -1
- package/dist/servertool/clock/tasks.d.ts +3 -1
- package/dist/servertool/clock/tasks.js +209 -18
- package/dist/servertool/clock/types.d.ts +17 -0
- package/dist/servertool/continue-execution/log.d.ts +3 -0
- package/dist/servertool/continue-execution/log.js +13 -0
- package/dist/servertool/engine.js +414 -68
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +6 -6
- package/dist/servertool/handlers/clock-auto.js +54 -71
- package/dist/servertool/handlers/clock.js +121 -6
- package/dist/servertool/handlers/continue-execution.d.ts +1 -0
- package/dist/servertool/handlers/continue-execution.js +91 -0
- package/dist/servertool/handlers/followup-request-builder.js +13 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -1
- package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
- package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
- package/dist/servertool/handlers/stop-message-auto.js +352 -257
- package/dist/servertool/handlers/stop-message-stage-policy.d.ts +22 -1
- package/dist/servertool/handlers/stop-message-stage-policy.js +472 -60
- package/dist/servertool/handlers/vision.js +1 -1
- package/dist/servertool/log/progress-file.d.ts +14 -0
- package/dist/servertool/log/progress-file.js +88 -0
- package/dist/servertool/pre-command-hooks.d.ts +17 -0
- package/dist/servertool/pre-command-hooks.js +491 -0
- package/dist/servertool/registry.d.ts +23 -6
- package/dist/servertool/registry.js +66 -1
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +216 -14
- package/dist/servertool/stop-gateway-context.d.ts +14 -0
- package/dist/servertool/stop-gateway-context.js +167 -0
- package/dist/servertool/stop-message-compare-context.d.ts +24 -0
- package/dist/servertool/stop-message-compare-context.js +133 -0
- package/dist/servertool/types.d.ts +12 -0
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -0
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +36 -1
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +3 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +3 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +118 -1
- package/dist/tools/apply-patch/args-normalizer/default-actions.js +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { registerServerToolHandler } from '../registry.js';
|
|
2
|
-
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } from '../../router/virtual-router/sticky-session-store.js';
|
|
2
|
+
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync, saveRoutingInstructionStateSync } from '../../router/virtual-router/sticky-session-store.js';
|
|
3
3
|
import { isCompactionRequest } from './compaction-detect.js';
|
|
4
4
|
import { extractCapturedChatSeed } from './followup-request-builder.js';
|
|
5
5
|
import { resolveStopMessageStageDecision } from './stop-message-stage-policy.js';
|
|
6
6
|
import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
|
|
7
|
+
import { isStopEligibleForServerTool } from '../stop-gateway-context.js';
|
|
8
|
+
import { attachStopMessageCompareContext } from '../stop-message-compare-context.js';
|
|
7
9
|
const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
|
|
8
10
|
const STOPMESSAGE_IMPLICIT_GEMINI = (process.env.ROUTECODEX_STOPMESSAGE_IMPLICIT_GEMINI || '').trim() === '1';
|
|
9
11
|
function debugLog(message, extra) {
|
|
@@ -21,191 +23,277 @@ function debugLog(message, extra) {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
const FLOW_ID = 'stop_message_flow';
|
|
26
|
+
const STOP_MESSAGE_EXECUTION_DIRECTIVE = [
|
|
27
|
+
'执行策略(必须遵守):',
|
|
28
|
+
'- 禁止输出口播式状态汇报(如“继续执行中”“我会继续”)。',
|
|
29
|
+
'- 先执行一个真实动作(优先工具调用),再在必要时简短汇报结果。',
|
|
30
|
+
'- 如果即将返回 finish_reason=stop 或协议等价 end_turn,先调用 continue_execution 工具,不要直接结束会话。'
|
|
31
|
+
].join('\n');
|
|
24
32
|
const handler = async (ctx) => {
|
|
25
33
|
const record = ctx.adapterContext;
|
|
26
34
|
const rt = readRuntimeMetadata(ctx.adapterContext);
|
|
35
|
+
const compare = {
|
|
36
|
+
armed: false,
|
|
37
|
+
mode: 'off',
|
|
38
|
+
allowModeOnly: false,
|
|
39
|
+
textLength: 0,
|
|
40
|
+
maxRepeats: 0,
|
|
41
|
+
used: 0,
|
|
42
|
+
remaining: 0,
|
|
43
|
+
active: false,
|
|
44
|
+
stopEligible: false,
|
|
45
|
+
hasCapturedRequest: false,
|
|
46
|
+
compactionRequest: false,
|
|
47
|
+
hasSeed: false,
|
|
48
|
+
decision: 'skip',
|
|
49
|
+
reason: 'handler_start'
|
|
50
|
+
};
|
|
51
|
+
const syncCompareRound = () => {
|
|
52
|
+
const max = Number.isFinite(compare.maxRepeats) ? Math.max(0, Math.floor(compare.maxRepeats)) : 0;
|
|
53
|
+
const used = Number.isFinite(compare.used) ? Math.max(0, Math.floor(compare.used)) : 0;
|
|
54
|
+
compare.maxRepeats = max;
|
|
55
|
+
compare.used = used;
|
|
56
|
+
compare.remaining = max > 0 ? Math.max(0, max - used) : 0;
|
|
57
|
+
compare.active =
|
|
58
|
+
compare.armed &&
|
|
59
|
+
compare.mode !== 'off' &&
|
|
60
|
+
max > 0 &&
|
|
61
|
+
(compare.textLength > 0 || compare.allowModeOnly);
|
|
62
|
+
};
|
|
63
|
+
const updateCompare = (patch) => {
|
|
64
|
+
Object.assign(compare, patch);
|
|
65
|
+
syncCompareRound();
|
|
66
|
+
};
|
|
67
|
+
const markSkip = (reason, patch) => {
|
|
68
|
+
updateCompare({
|
|
69
|
+
decision: 'skip',
|
|
70
|
+
reason,
|
|
71
|
+
...(patch || {})
|
|
72
|
+
});
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
27
75
|
debugLog('handler_start', {
|
|
28
76
|
requestId: record.requestId,
|
|
29
77
|
providerProtocol: record.providerProtocol
|
|
30
78
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const stickyKey = resolveStickyKey(record);
|
|
53
|
-
if (!stickyKey) {
|
|
54
|
-
debugLog('skip_no_sticky_key');
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
let state = loadRoutingInstructionStateSync(stickyKey);
|
|
58
|
-
// If stopMessage was created implicitly (auto) but implicit mode is disabled, do not run it.
|
|
59
|
-
// This avoids surprising followups like "继续执行" when the user never enabled stopMessage.
|
|
60
|
-
if (state &&
|
|
61
|
-
typeof state.stopMessageSource === 'string' &&
|
|
62
|
-
state.stopMessageSource.trim().toLowerCase() === 'auto' &&
|
|
63
|
-
!STOPMESSAGE_IMPLICIT_GEMINI) {
|
|
64
|
-
clearStopMessageState(state, Date.now());
|
|
65
|
-
saveRoutingInstructionStateAsync(stickyKey, state);
|
|
66
|
-
debugLog('skip_auto_state_disabled', { stickyKey });
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
|
|
70
|
-
const fallback = resolveStopMessageSnapshot(rt?.stopMessageState);
|
|
71
|
-
if (fallback) {
|
|
72
|
-
state = createStopMessageState(fallback);
|
|
79
|
+
try {
|
|
80
|
+
const followupFlagRaw = rt?.serverToolFollowup;
|
|
81
|
+
if (followupFlagRaw === true ||
|
|
82
|
+
(typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true')) {
|
|
83
|
+
debugLog('skip_followup_loop');
|
|
84
|
+
return markSkip('skip_followup_loop');
|
|
85
|
+
}
|
|
86
|
+
if (hasCompactionFlag(rt)) {
|
|
87
|
+
debugLog('skip_compaction_flag');
|
|
88
|
+
return markSkip('skip_compaction_flag');
|
|
89
|
+
}
|
|
90
|
+
const connectionState = resolveClientConnectionState(record.clientConnectionState);
|
|
91
|
+
if (connectionState?.disconnected === true) {
|
|
92
|
+
debugLog('skip_client_disconnected');
|
|
93
|
+
return markSkip('skip_client_disconnected');
|
|
94
|
+
}
|
|
95
|
+
const clientDisconnectedRaw = record.clientDisconnected;
|
|
96
|
+
if (clientDisconnectedRaw === true ||
|
|
97
|
+
(typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
|
|
98
|
+
debugLog('skip_client_disconnected_flag');
|
|
99
|
+
return markSkip('skip_client_disconnected_flag');
|
|
73
100
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
const stickyKey = resolveStickyKey(record, rt);
|
|
102
|
+
let state = stickyKey ? loadRoutingInstructionStateSync(stickyKey) : undefined;
|
|
103
|
+
if (state &&
|
|
104
|
+
typeof state.stopMessageSource === 'string' &&
|
|
105
|
+
state.stopMessageSource.trim().toLowerCase() === 'auto' &&
|
|
106
|
+
!STOPMESSAGE_IMPLICIT_GEMINI) {
|
|
107
|
+
clearStopMessageState(state, Date.now());
|
|
108
|
+
if (stickyKey) {
|
|
109
|
+
persistStopMessageState(stickyKey, state);
|
|
79
110
|
}
|
|
80
|
-
|
|
111
|
+
debugLog('skip_auto_state_disabled', { stickyKey });
|
|
112
|
+
return markSkip('skip_auto_state_disabled', { armed: false, mode: 'off' });
|
|
81
113
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
if (!state || !hasArmedStopMessageState(state)) {
|
|
115
|
+
const fallback = resolveStopMessageSnapshot(rt?.stopMessageState);
|
|
116
|
+
if (fallback) {
|
|
117
|
+
state = createStopMessageState(fallback);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const implicit = STOPMESSAGE_IMPLICIT_GEMINI ? resolveImplicitGeminiStopMessageSnapshot(ctx, record) : null;
|
|
121
|
+
if (!implicit) {
|
|
122
|
+
debugLog('skip_no_state', { stickyKey });
|
|
123
|
+
return markSkip('skip_no_state', { armed: false, mode: 'off' });
|
|
124
|
+
}
|
|
125
|
+
state = createStopMessageState(implicit);
|
|
126
|
+
}
|
|
127
|
+
if (stickyKey) {
|
|
128
|
+
persistStopMessageState(stickyKey, state);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
updateCompare({ armed: hasArmedStopMessageState(state) });
|
|
132
|
+
const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
|
|
133
|
+
const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
|
|
134
|
+
? Math.max(1, Math.floor(state.stopMessageMaxRepeats))
|
|
135
|
+
: 0;
|
|
136
|
+
const stageMode = normalizeStopMessageModeValue(state.stopMessageStageMode);
|
|
137
|
+
const mode = stageMode === 'on' || stageMode === 'auto' || stageMode === 'off' ? stageMode : 'off';
|
|
138
|
+
const allowModeOnlyState = !text && maxRepeats > 0 && (stageMode === 'on' || stageMode === 'auto');
|
|
139
|
+
updateCompare({
|
|
140
|
+
mode,
|
|
141
|
+
allowModeOnly: allowModeOnlyState,
|
|
91
142
|
textLength: text.length,
|
|
92
143
|
maxRepeats
|
|
93
144
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
145
|
+
if ((!text && !allowModeOnlyState) || maxRepeats <= 0) {
|
|
146
|
+
debugLog('skip_invalid_text_or_maxRepeats', {
|
|
147
|
+
stickyKey,
|
|
148
|
+
textLength: text.length,
|
|
149
|
+
maxRepeats,
|
|
150
|
+
stageMode
|
|
151
|
+
});
|
|
152
|
+
return markSkip('skip_invalid_text_or_maxRepeats');
|
|
153
|
+
}
|
|
154
|
+
const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
|
|
155
|
+
? Math.max(0, Math.floor(state.stopMessageUsed))
|
|
156
|
+
: 0;
|
|
157
|
+
updateCompare({ used });
|
|
158
|
+
if (used >= maxRepeats) {
|
|
159
|
+
debugLog('skip_reached_max_repeats', {
|
|
160
|
+
stickyKey,
|
|
161
|
+
used,
|
|
162
|
+
maxRepeats
|
|
163
|
+
});
|
|
164
|
+
clearStopMessageState(state, Date.now());
|
|
165
|
+
if (stickyKey) {
|
|
166
|
+
persistStopMessageState(stickyKey, state);
|
|
167
|
+
}
|
|
168
|
+
return markSkip('skip_reached_max_repeats');
|
|
169
|
+
}
|
|
170
|
+
const stopEligible = isStopEligibleForServerTool(ctx.base, ctx.adapterContext);
|
|
171
|
+
updateCompare({ stopEligible });
|
|
172
|
+
if (!stopEligible) {
|
|
173
|
+
debugLog('skip_not_stop_finish_reason', {
|
|
174
|
+
stickyKey
|
|
175
|
+
});
|
|
176
|
+
return markSkip('skip_not_stop_finish_reason');
|
|
177
|
+
}
|
|
178
|
+
const captured = getCapturedRequest(ctx.adapterContext);
|
|
179
|
+
updateCompare({ hasCapturedRequest: Boolean(captured) });
|
|
180
|
+
if (!captured) {
|
|
181
|
+
debugLog('skip_no_captured_request', {
|
|
182
|
+
stickyKey
|
|
183
|
+
});
|
|
184
|
+
return markSkip('skip_no_captured_request');
|
|
185
|
+
}
|
|
186
|
+
const compactionRequest = isCompactionRequest(captured);
|
|
187
|
+
updateCompare({ compactionRequest });
|
|
188
|
+
if (compactionRequest) {
|
|
189
|
+
debugLog('skip_compaction_request', { stickyKey });
|
|
190
|
+
return markSkip('skip_compaction_request');
|
|
191
|
+
}
|
|
192
|
+
const entryEndpoint = resolveEntryEndpoint(record);
|
|
193
|
+
const seed = extractCapturedChatSeed(captured);
|
|
194
|
+
updateCompare({ hasSeed: Boolean(seed) });
|
|
195
|
+
if (!seed) {
|
|
196
|
+
debugLog('skip_failed_build_followup', { stickyKey });
|
|
197
|
+
return markSkip('skip_failed_build_followup');
|
|
198
|
+
}
|
|
199
|
+
const stageDecision = resolveStopMessageStageDecision({
|
|
200
|
+
baseText: text,
|
|
201
|
+
state,
|
|
202
|
+
capturedMessages: Array.isArray(seed.messages)
|
|
203
|
+
? (seed.messages || [])
|
|
204
|
+
: []
|
|
104
205
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
206
|
+
state.stopMessageObservationHash = stageDecision.observationHash;
|
|
207
|
+
state.stopMessageObservationStableCount = stageDecision.observationStableCount;
|
|
208
|
+
state.stopMessageBdWorkState = stageDecision.bdWorkState;
|
|
209
|
+
state.stopMessageStage = stageDecision.stage;
|
|
210
|
+
updateCompare({
|
|
211
|
+
stage: stageDecision.stage,
|
|
212
|
+
bdWorkState: stageDecision.bdWorkState,
|
|
213
|
+
observationHash: stageDecision.observationHash,
|
|
214
|
+
observationStableCount: stageDecision.observationStableCount,
|
|
215
|
+
toolSignatureHash: stageDecision.toolSignatureHash
|
|
113
216
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
217
|
+
if (stageDecision.action === 'stop') {
|
|
218
|
+
clearStopMessageState(state, Date.now());
|
|
219
|
+
if (stickyKey) {
|
|
220
|
+
persistStopMessageState(stickyKey, state);
|
|
221
|
+
}
|
|
222
|
+
debugLog('stop_by_stage_policy', {
|
|
223
|
+
stickyKey,
|
|
224
|
+
reason: stageDecision.stopReason,
|
|
225
|
+
observationStableCount: stageDecision.observationStableCount,
|
|
226
|
+
bdWorkState: stageDecision.bdWorkState
|
|
227
|
+
});
|
|
228
|
+
return markSkip('stage_policy_stop');
|
|
229
|
+
}
|
|
230
|
+
const followupText = typeof stageDecision.followupText === 'string' && stageDecision.followupText.trim()
|
|
231
|
+
? stageDecision.followupText.trim()
|
|
232
|
+
: text;
|
|
233
|
+
if (!followupText) {
|
|
234
|
+
debugLog('skip_empty_followup_text_after_stage', {
|
|
235
|
+
stickyKey,
|
|
236
|
+
stage: stageDecision.stage,
|
|
237
|
+
bdWorkState: stageDecision.bdWorkState
|
|
238
|
+
});
|
|
239
|
+
return markSkip('skip_empty_followup_text_after_stage');
|
|
240
|
+
}
|
|
241
|
+
const nextUsed = used + 1;
|
|
242
|
+
state.stopMessageUsed = nextUsed;
|
|
243
|
+
state.stopMessageLastUsedAt = Date.now();
|
|
244
|
+
updateCompare({ used: nextUsed });
|
|
245
|
+
if (nextUsed >= maxRepeats) {
|
|
246
|
+
clearStopMessageState(state, Date.now());
|
|
247
|
+
}
|
|
248
|
+
if (stickyKey) {
|
|
249
|
+
persistStopMessageState(stickyKey, state);
|
|
250
|
+
}
|
|
251
|
+
const followupProviderKey = resolveStopMessageFollowupProviderKey({ record, runtimeMetadata: rt });
|
|
252
|
+
const followupToolContentMaxChars = resolveStopMessageFollowupToolContentMaxChars({
|
|
253
|
+
providerKey: followupProviderKey,
|
|
254
|
+
model: seed.model
|
|
120
255
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const stageDecision = resolveStopMessageStageDecision({
|
|
134
|
-
baseText: text,
|
|
135
|
-
state,
|
|
136
|
-
capturedMessages: Array.isArray(seed.messages)
|
|
137
|
-
? (seed.messages || [])
|
|
138
|
-
: []
|
|
139
|
-
});
|
|
140
|
-
state.stopMessageObservationHash = stageDecision.observationHash;
|
|
141
|
-
state.stopMessageObservationStableCount = stageDecision.observationStableCount;
|
|
142
|
-
state.stopMessageBdWorkState = stageDecision.bdWorkState;
|
|
143
|
-
state.stopMessageStage = stageDecision.stage;
|
|
144
|
-
if (stageDecision.action === 'stop') {
|
|
145
|
-
clearStopMessageState(state, Date.now());
|
|
146
|
-
saveRoutingInstructionStateAsync(stickyKey, state);
|
|
147
|
-
debugLog('stop_by_stage_policy', {
|
|
148
|
-
stickyKey,
|
|
149
|
-
reason: stageDecision.stopReason,
|
|
150
|
-
observationStableCount: stageDecision.observationStableCount,
|
|
151
|
-
bdWorkState: stageDecision.bdWorkState
|
|
256
|
+
const followupOps = [];
|
|
257
|
+
if (typeof followupToolContentMaxChars === 'number' &&
|
|
258
|
+
Number.isFinite(followupToolContentMaxChars) &&
|
|
259
|
+
followupToolContentMaxChars > 0) {
|
|
260
|
+
followupOps.push({ op: 'compact_tool_content', maxChars: Math.floor(followupToolContentMaxChars) });
|
|
261
|
+
}
|
|
262
|
+
followupOps.push({ op: 'ensure_standard_tools' });
|
|
263
|
+
followupOps.push({ op: 'inject_system_text', text: STOP_MESSAGE_EXECUTION_DIRECTIVE });
|
|
264
|
+
followupOps.push({ op: 'append_user_text', text: followupText });
|
|
265
|
+
updateCompare({
|
|
266
|
+
decision: 'trigger',
|
|
267
|
+
reason: 'triggered'
|
|
152
268
|
});
|
|
153
|
-
return
|
|
269
|
+
return {
|
|
270
|
+
flowId: FLOW_ID,
|
|
271
|
+
finalize: async () => ({
|
|
272
|
+
chatResponse: ctx.base,
|
|
273
|
+
execution: {
|
|
274
|
+
flowId: FLOW_ID,
|
|
275
|
+
followup: {
|
|
276
|
+
requestIdSuffix: ':stop_followup',
|
|
277
|
+
entryEndpoint,
|
|
278
|
+
injection: {
|
|
279
|
+
ops: followupOps
|
|
280
|
+
},
|
|
281
|
+
metadata: (connectionState ? { clientConnectionState: connectionState } : {})
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
};
|
|
154
286
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
: text;
|
|
158
|
-
// Extract assistant message for potential followup injection (no-op today; keeps compat).
|
|
159
|
-
void extractAssistantMessageForFollowup(ctx.base);
|
|
160
|
-
// Increment stopMessage usage counter when we decide to trigger followup.
|
|
161
|
-
const nextUsed = used + 1;
|
|
162
|
-
state.stopMessageUsed = nextUsed;
|
|
163
|
-
state.stopMessageLastUsedAt = Date.now();
|
|
164
|
-
// If this will be the last allowed trigger, mark it for cleanup.
|
|
165
|
-
// We still return the followup plan for this trigger, but clear the config
|
|
166
|
-
// so the next response won't trigger again.
|
|
167
|
-
if (nextUsed >= maxRepeats) {
|
|
168
|
-
clearStopMessageState(state, Date.now());
|
|
287
|
+
finally {
|
|
288
|
+
attachStopMessageCompareContext(ctx.adapterContext, compare);
|
|
169
289
|
}
|
|
170
|
-
saveRoutingInstructionStateAsync(stickyKey, state);
|
|
171
|
-
const followupProviderKey = resolveStopMessageFollowupProviderKey({ record, runtimeMetadata: rt });
|
|
172
|
-
const followupToolContentMaxChars = resolveStopMessageFollowupToolContentMaxChars({
|
|
173
|
-
providerKey: followupProviderKey,
|
|
174
|
-
model: seed.model
|
|
175
|
-
});
|
|
176
|
-
const followupOps = [];
|
|
177
|
-
if (typeof followupToolContentMaxChars === 'number' &&
|
|
178
|
-
Number.isFinite(followupToolContentMaxChars) &&
|
|
179
|
-
followupToolContentMaxChars > 0) {
|
|
180
|
-
followupOps.push({ op: 'compact_tool_content', maxChars: Math.floor(followupToolContentMaxChars) });
|
|
181
|
-
}
|
|
182
|
-
followupOps.push({ op: 'append_assistant_message', required: false });
|
|
183
|
-
followupOps.push({ op: 'ensure_standard_tools' });
|
|
184
|
-
followupOps.push({ op: 'append_user_text', text: followupText });
|
|
185
|
-
return {
|
|
186
|
-
flowId: FLOW_ID,
|
|
187
|
-
finalize: async () => ({
|
|
188
|
-
chatResponse: ctx.base,
|
|
189
|
-
execution: {
|
|
190
|
-
flowId: FLOW_ID,
|
|
191
|
-
followup: {
|
|
192
|
-
requestIdSuffix: ':stop_followup',
|
|
193
|
-
entryEndpoint,
|
|
194
|
-
injection: {
|
|
195
|
-
ops: followupOps
|
|
196
|
-
},
|
|
197
|
-
metadata: (connectionState ? { clientConnectionState: connectionState } : {})
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
})
|
|
201
|
-
};
|
|
202
290
|
};
|
|
203
|
-
registerServerToolHandler('stop_message_auto', handler, { trigger: 'auto' });
|
|
204
|
-
function resolveStickyKey(record) {
|
|
205
|
-
const sessionId =
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
291
|
+
registerServerToolHandler('stop_message_auto', handler, { trigger: 'auto', hook: { phase: 'default', priority: 40 } });
|
|
292
|
+
function resolveStickyKey(record, runtimeMetadata) {
|
|
293
|
+
const sessionId = readSessionScopeValue(record, runtimeMetadata, 'sessionId') ||
|
|
294
|
+
readSessionScopeValue(record, runtimeMetadata, 'session_id');
|
|
295
|
+
const conversationId = readSessionScopeValue(record, runtimeMetadata, 'conversationId') ||
|
|
296
|
+
readSessionScopeValue(record, runtimeMetadata, 'conversation_id');
|
|
209
297
|
if (sessionId) {
|
|
210
298
|
return `session:${sessionId}`;
|
|
211
299
|
}
|
|
@@ -214,6 +302,47 @@ function resolveStickyKey(record) {
|
|
|
214
302
|
}
|
|
215
303
|
return undefined;
|
|
216
304
|
}
|
|
305
|
+
function persistStopMessageState(stickyKey, state) {
|
|
306
|
+
if (!stickyKey) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
saveRoutingInstructionStateSync(stickyKey, state);
|
|
310
|
+
saveRoutingInstructionStateAsync(stickyKey, state);
|
|
311
|
+
}
|
|
312
|
+
function readSessionScopeValue(record, runtimeMetadata, key) {
|
|
313
|
+
const direct = toNonEmptyText(record[key]);
|
|
314
|
+
if (direct) {
|
|
315
|
+
return direct;
|
|
316
|
+
}
|
|
317
|
+
const metadata = asRecord(record.metadata);
|
|
318
|
+
const fromMetadata = metadata ? toNonEmptyText(metadata[key]) : '';
|
|
319
|
+
if (fromMetadata) {
|
|
320
|
+
return fromMetadata;
|
|
321
|
+
}
|
|
322
|
+
const fromMetadataContext = metadata ? toNonEmptyText(asRecord(metadata.context)?.[key]) : '';
|
|
323
|
+
if (fromMetadataContext) {
|
|
324
|
+
return fromMetadataContext;
|
|
325
|
+
}
|
|
326
|
+
const originalRequest = asRecord(record.originalRequest);
|
|
327
|
+
const fromOriginalMetadata = originalRequest
|
|
328
|
+
? toNonEmptyText(asRecord(originalRequest.metadata)?.[key])
|
|
329
|
+
: '';
|
|
330
|
+
if (fromOriginalMetadata) {
|
|
331
|
+
return fromOriginalMetadata;
|
|
332
|
+
}
|
|
333
|
+
const runtime = asRecord(runtimeMetadata);
|
|
334
|
+
const fromRuntime = runtime ? toNonEmptyText(runtime[key]) : '';
|
|
335
|
+
if (fromRuntime) {
|
|
336
|
+
return fromRuntime;
|
|
337
|
+
}
|
|
338
|
+
return '';
|
|
339
|
+
}
|
|
340
|
+
function asRecord(value) {
|
|
341
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
217
346
|
function toNonEmptyText(value) {
|
|
218
347
|
return typeof value === 'string' && value.trim().length ? value.trim() : '';
|
|
219
348
|
}
|
|
@@ -235,6 +364,30 @@ function readProviderKeyFromMetadata(value) {
|
|
|
235
364
|
}
|
|
236
365
|
return '';
|
|
237
366
|
}
|
|
367
|
+
function hasArmedStopMessageState(state) {
|
|
368
|
+
const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
|
|
369
|
+
? Math.max(1, Math.floor(state.stopMessageMaxRepeats))
|
|
370
|
+
: 0;
|
|
371
|
+
if (maxRepeats <= 0) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
|
|
375
|
+
if (text) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
const mode = normalizeStopMessageModeValue(state.stopMessageStageMode);
|
|
379
|
+
return mode === 'on' || mode === 'auto';
|
|
380
|
+
}
|
|
381
|
+
function normalizeStopMessageModeValue(value) {
|
|
382
|
+
if (typeof value !== 'string') {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
const normalized = value.trim().toLowerCase();
|
|
386
|
+
if (normalized === 'on' || normalized === 'off' || normalized === 'auto') {
|
|
387
|
+
return normalized;
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
238
391
|
function resolveStopMessageFollowupProviderKey(args) {
|
|
239
392
|
const direct = toNonEmptyText(args.record.providerKey) ||
|
|
240
393
|
toNonEmptyText(args.record.providerId) ||
|
|
@@ -261,100 +414,27 @@ function resolveStopMessageFollowupToolContentMaxChars(params) {
|
|
|
261
414
|
}
|
|
262
415
|
return undefined;
|
|
263
416
|
}
|
|
264
|
-
function isStopFinishReason(base) {
|
|
265
|
-
if (!base || typeof base !== 'object' || Array.isArray(base)) {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
const payload = base;
|
|
269
|
-
const choicesRaw = payload.choices;
|
|
270
|
-
if (Array.isArray(choicesRaw) && choicesRaw.length) {
|
|
271
|
-
const first = choicesRaw[0];
|
|
272
|
-
if (!first || typeof first !== 'object' || Array.isArray(first)) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
const finishReasonRaw = first.finish_reason;
|
|
276
|
-
const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
|
|
277
|
-
? finishReasonRaw.trim().toLowerCase()
|
|
278
|
-
: '';
|
|
279
|
-
// 将模型视为“自然结束”的场景:
|
|
280
|
-
// - OpenAI 兼容:finish_reason === 'stop'
|
|
281
|
-
// - 截断场景:finish_reason === 'length'(例如 Gemini/Claude 流式输出被 max token 截断)
|
|
282
|
-
// 统一视作可触发 stopMessage 的终止点;仅排除显式的 tool_calls。
|
|
283
|
-
if (!finishReason || finishReason === 'tool_calls') {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
if (finishReason !== 'stop' && finishReason !== 'length') {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
const message = first.message &&
|
|
290
|
-
typeof first.message === 'object' &&
|
|
291
|
-
!Array.isArray(first.message)
|
|
292
|
-
? first.message
|
|
293
|
-
: null;
|
|
294
|
-
if (!message) {
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
298
|
-
if (toolCalls.length > 0) {
|
|
299
|
-
// 如果当前响应仍在发起工具调用,则由工具执行驱动后续轮次,不触发 stopMessage。
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
// OpenAI Responses shape: status completed + no required_action + no tool-like output entries
|
|
305
|
-
const statusRaw = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
|
|
306
|
-
if (statusRaw && statusRaw !== 'completed') {
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
if (payload.required_action && typeof payload.required_action === 'object') {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
const outputRaw = Array.isArray(payload.output) ? payload.output : [];
|
|
313
|
-
if (!outputRaw.length) {
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
if (outputRaw.some((item) => hasToolLikeOutput(item))) {
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
function extractAssistantMessageForFollowup(chatResponse) {
|
|
322
|
-
if (!chatResponse || typeof chatResponse !== 'object' || Array.isArray(chatResponse)) {
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
const choices = Array.isArray(chatResponse.choices)
|
|
326
|
-
? chatResponse.choices
|
|
327
|
-
: [];
|
|
328
|
-
if (!choices.length) {
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
const first = choices[0];
|
|
332
|
-
if (!first || typeof first !== 'object' || Array.isArray(first)) {
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
const message = first.message;
|
|
336
|
-
if (!message || typeof message !== 'object' || Array.isArray(message)) {
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
const role = typeof message.role === 'string' ? String(message.role) : '';
|
|
340
|
-
if (role && role.toLowerCase() !== 'assistant') {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
const content = message.content;
|
|
344
|
-
if (typeof content !== 'string' || !content.trim()) {
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
return { role: 'assistant', content: content.trim() };
|
|
348
|
-
}
|
|
349
417
|
function getCapturedRequest(adapterContext) {
|
|
350
418
|
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
351
419
|
return null;
|
|
352
420
|
}
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
421
|
+
const contextRecord = adapterContext;
|
|
422
|
+
const direct = contextRecord.capturedChatRequest;
|
|
423
|
+
if (direct && typeof direct === 'object' && !Array.isArray(direct)) {
|
|
424
|
+
return direct;
|
|
425
|
+
}
|
|
426
|
+
const runtime = readRuntimeMetadata(contextRecord);
|
|
427
|
+
const runtimeCaptured = runtime && typeof runtime === 'object' && !Array.isArray(runtime)
|
|
428
|
+
? runtime.capturedChatRequest
|
|
429
|
+
: undefined;
|
|
430
|
+
if (runtimeCaptured && typeof runtimeCaptured === 'object' && !Array.isArray(runtimeCaptured)) {
|
|
431
|
+
return runtimeCaptured;
|
|
432
|
+
}
|
|
433
|
+
const originalRequest = contextRecord.originalRequest;
|
|
434
|
+
if (originalRequest && typeof originalRequest === 'object' && !Array.isArray(originalRequest)) {
|
|
435
|
+
return originalRequest;
|
|
356
436
|
}
|
|
357
|
-
return
|
|
437
|
+
return null;
|
|
358
438
|
}
|
|
359
439
|
function extractResponsesOutputText(base) {
|
|
360
440
|
const raw = base.output_text;
|
|
@@ -424,7 +504,9 @@ function resolveStopMessageSnapshot(raw) {
|
|
|
424
504
|
const maxRepeats = typeof record.stopMessageMaxRepeats === 'number' && Number.isFinite(record.stopMessageMaxRepeats)
|
|
425
505
|
? Math.max(1, Math.floor(record.stopMessageMaxRepeats))
|
|
426
506
|
: 0;
|
|
427
|
-
|
|
507
|
+
const stageMode = normalizeStopMessageStageMode(record.stopMessageStageMode);
|
|
508
|
+
const allowModeOnlyState = !text && maxRepeats > 0 && (stageMode === 'on' || stageMode === 'auto');
|
|
509
|
+
if ((!text && !allowModeOnlyState) || maxRepeats <= 0) {
|
|
428
510
|
return null;
|
|
429
511
|
}
|
|
430
512
|
const used = typeof record.stopMessageUsed === 'number' && Number.isFinite(record.stopMessageUsed)
|
|
@@ -459,6 +541,7 @@ function resolveStopMessageSnapshot(raw) {
|
|
|
459
541
|
...(updatedAt ? { updatedAt } : {}),
|
|
460
542
|
...(lastUsedAt ? { lastUsedAt } : {}),
|
|
461
543
|
...(stage ? { stage } : {}),
|
|
544
|
+
...(stageMode ? { stageMode } : {}),
|
|
462
545
|
...(observationHash ? { observationHash } : {}),
|
|
463
546
|
...(typeof observationStableCount === 'number' ? { observationStableCount } : {}),
|
|
464
547
|
...(bdWorkState ? { bdWorkState } : {})
|
|
@@ -496,7 +579,7 @@ function resolveImplicitGeminiStopMessageSnapshot(ctx, record) {
|
|
|
496
579
|
return null;
|
|
497
580
|
}
|
|
498
581
|
// 仅在本轮响应被视为“自然结束”(stop/length,且无 tool_calls)时触发,避免干扰正常对话。
|
|
499
|
-
if (!
|
|
582
|
+
if (!isStopEligibleForServerTool(ctx.base, ctx.adapterContext)) {
|
|
500
583
|
return null;
|
|
501
584
|
}
|
|
502
585
|
// 仅在“空回复”时触发隐式 stopMessage:
|
|
@@ -584,17 +667,29 @@ function createStopMessageState(snapshot) {
|
|
|
584
667
|
stopMessageUpdatedAt: snapshot.updatedAt,
|
|
585
668
|
stopMessageLastUsedAt: snapshot.lastUsedAt,
|
|
586
669
|
stopMessageStage: snapshot.stage,
|
|
670
|
+
stopMessageStageMode: snapshot.stageMode,
|
|
587
671
|
stopMessageObservationHash: snapshot.observationHash,
|
|
588
672
|
stopMessageObservationStableCount: snapshot.observationStableCount,
|
|
589
673
|
stopMessageBdWorkState: snapshot.bdWorkState
|
|
590
674
|
};
|
|
591
675
|
}
|
|
676
|
+
function normalizeStopMessageStageMode(value) {
|
|
677
|
+
if (typeof value !== 'string') {
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
const normalized = value.trim().toLowerCase();
|
|
681
|
+
if (normalized === 'on' || normalized === 'off' || normalized === 'auto') {
|
|
682
|
+
return normalized;
|
|
683
|
+
}
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
592
686
|
function clearStopMessageState(state, now) {
|
|
593
687
|
state.stopMessageText = undefined;
|
|
594
688
|
state.stopMessageMaxRepeats = undefined;
|
|
595
689
|
state.stopMessageUsed = undefined;
|
|
596
690
|
state.stopMessageSource = undefined;
|
|
597
691
|
state.stopMessageStage = undefined;
|
|
692
|
+
state.stopMessageStageMode = undefined;
|
|
598
693
|
state.stopMessageObservationHash = undefined;
|
|
599
694
|
state.stopMessageObservationStableCount = undefined;
|
|
600
695
|
state.stopMessageBdWorkState = undefined;
|