@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.
Files changed (107) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.d.ts +3 -0
  2. package/dist/conversion/compat/actions/deepseek-web-request.js +350 -0
  3. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +3 -0
  4. package/dist/conversion/compat/actions/deepseek-web-response.js +886 -0
  5. package/dist/conversion/compat/actions/gemini-cli-request.js +3 -1
  6. package/dist/conversion/compat/profiles/chat-deepseek-web.json +18 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +166 -2
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +169 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +6 -0
  10. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +12 -0
  11. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -0
  12. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +4 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.js +365 -144
  14. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +9 -0
  15. package/dist/conversion/hub/policy/policy-engine.d.ts +2 -0
  16. package/dist/conversion/hub/policy/policy-engine.js +8 -0
  17. package/dist/conversion/hub/process/chat-process.js +466 -16
  18. package/dist/conversion/hub/response/provider-response.js +0 -35
  19. package/dist/conversion/responses/responses-openai-bridge.d.ts +2 -0
  20. package/dist/conversion/responses/responses-openai-bridge.js +166 -8
  21. package/dist/conversion/shared/anthropic-message-utils.js +10 -1
  22. package/dist/conversion/shared/protocol-field-allowlists.d.ts +2 -2
  23. package/dist/conversion/shared/protocol-field-allowlists.js +4 -0
  24. package/dist/conversion/shared/tool-governor.js +102 -0
  25. package/dist/guidance/index.js +17 -0
  26. package/dist/router/virtual-router/bootstrap.js +46 -1
  27. package/dist/router/virtual-router/classifier.js +59 -4
  28. package/dist/router/virtual-router/engine/health/index.js +6 -6
  29. package/dist/router/virtual-router/engine/routing-state/store.js +16 -3
  30. package/dist/router/virtual-router/engine-logging.js +62 -24
  31. package/dist/router/virtual-router/engine-selection/route-utils.js +20 -20
  32. package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -2
  33. package/dist/router/virtual-router/engine.d.ts +3 -1
  34. package/dist/router/virtual-router/engine.js +325 -38
  35. package/dist/router/virtual-router/features.js +2 -1
  36. package/dist/router/virtual-router/pre-command-file-resolver.d.ts +2 -0
  37. package/dist/router/virtual-router/pre-command-file-resolver.js +90 -0
  38. package/dist/router/virtual-router/provider-registry.js +3 -1
  39. package/dist/router/virtual-router/routing-instructions.d.ts +11 -1
  40. package/dist/router/virtual-router/routing-instructions.js +101 -183
  41. package/dist/router/virtual-router/routing-pre-command-actions.d.ts +3 -0
  42. package/dist/router/virtual-router/routing-pre-command-actions.js +26 -0
  43. package/dist/router/virtual-router/routing-pre-command-parser.d.ts +2 -0
  44. package/dist/router/virtual-router/routing-pre-command-parser.js +85 -0
  45. package/dist/router/virtual-router/routing-pre-command-state-codec.d.ts +3 -0
  46. package/dist/router/virtual-router/routing-pre-command-state-codec.js +24 -0
  47. package/dist/router/virtual-router/routing-stop-message-actions.d.ts +2 -0
  48. package/dist/router/virtual-router/routing-stop-message-actions.js +96 -0
  49. package/dist/router/virtual-router/routing-stop-message-parser.d.ts +3 -0
  50. package/dist/router/virtual-router/routing-stop-message-parser.js +142 -0
  51. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +4 -0
  52. package/dist/router/virtual-router/routing-stop-message-state-codec.js +85 -0
  53. package/dist/router/virtual-router/sticky-session-store.js +206 -57
  54. package/dist/router/virtual-router/stop-message-stage-template-files.d.ts +12 -0
  55. package/dist/router/virtual-router/stop-message-stage-template-files.js +67 -0
  56. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  57. package/dist/router/virtual-router/stop-message-state-sync.js +1 -0
  58. package/dist/router/virtual-router/token-file-scanner.d.ts +9 -0
  59. package/dist/router/virtual-router/token-file-scanner.js +64 -3
  60. package/dist/router/virtual-router/tool-signals.d.ts +5 -0
  61. package/dist/router/virtual-router/tool-signals.js +42 -3
  62. package/dist/router/virtual-router/types.d.ts +15 -1
  63. package/dist/router/virtual-router/types.js +1 -0
  64. package/dist/servertool/clock/config.d.ts +1 -1
  65. package/dist/servertool/clock/config.js +27 -4
  66. package/dist/servertool/clock/state.js +41 -2
  67. package/dist/servertool/clock/task-store.d.ts +2 -2
  68. package/dist/servertool/clock/task-store.js +1 -1
  69. package/dist/servertool/clock/tasks.d.ts +3 -1
  70. package/dist/servertool/clock/tasks.js +209 -18
  71. package/dist/servertool/clock/types.d.ts +17 -0
  72. package/dist/servertool/continue-execution/log.d.ts +3 -0
  73. package/dist/servertool/continue-execution/log.js +13 -0
  74. package/dist/servertool/engine.js +414 -68
  75. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +6 -6
  76. package/dist/servertool/handlers/clock-auto.js +54 -71
  77. package/dist/servertool/handlers/clock.js +121 -6
  78. package/dist/servertool/handlers/continue-execution.d.ts +1 -0
  79. package/dist/servertool/handlers/continue-execution.js +91 -0
  80. package/dist/servertool/handlers/followup-request-builder.js +13 -0
  81. package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -1
  82. package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
  83. package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
  84. package/dist/servertool/handlers/stop-message-auto.js +352 -257
  85. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +22 -1
  86. package/dist/servertool/handlers/stop-message-stage-policy.js +472 -60
  87. package/dist/servertool/handlers/vision.js +1 -1
  88. package/dist/servertool/log/progress-file.d.ts +14 -0
  89. package/dist/servertool/log/progress-file.js +88 -0
  90. package/dist/servertool/pre-command-hooks.d.ts +17 -0
  91. package/dist/servertool/pre-command-hooks.js +491 -0
  92. package/dist/servertool/registry.d.ts +23 -6
  93. package/dist/servertool/registry.js +66 -1
  94. package/dist/servertool/server-side-tools.d.ts +1 -0
  95. package/dist/servertool/server-side-tools.js +216 -14
  96. package/dist/servertool/stop-gateway-context.d.ts +14 -0
  97. package/dist/servertool/stop-gateway-context.js +167 -0
  98. package/dist/servertool/stop-message-compare-context.d.ts +24 -0
  99. package/dist/servertool/stop-message-compare-context.js +133 -0
  100. package/dist/servertool/types.d.ts +12 -0
  101. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -0
  102. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +36 -1
  103. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +3 -0
  104. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +3 -0
  105. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +118 -1
  106. package/dist/tools/apply-patch/args-normalizer/default-actions.js +1 -1
  107. 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
- const followupFlagRaw = rt?.serverToolFollowup;
32
- if (followupFlagRaw === true ||
33
- (typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true')) {
34
- debugLog('skip_followup_loop');
35
- return null;
36
- }
37
- if (hasCompactionFlag(rt)) {
38
- debugLog('skip_compaction_flag');
39
- return null;
40
- }
41
- const connectionState = resolveClientConnectionState(record.clientConnectionState);
42
- if (connectionState?.disconnected === true) {
43
- debugLog('skip_client_disconnected');
44
- return null;
45
- }
46
- const clientDisconnectedRaw = record.clientDisconnected;
47
- if (clientDisconnectedRaw === true ||
48
- (typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
49
- debugLog('skip_client_disconnected_flag');
50
- return null;
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
- else {
75
- const implicit = STOPMESSAGE_IMPLICIT_GEMINI ? resolveImplicitGeminiStopMessageSnapshot(ctx, record) : null;
76
- if (!implicit) {
77
- debugLog('skip_no_state', { stickyKey });
78
- return null;
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
- state = createStopMessageState(implicit);
111
+ debugLog('skip_auto_state_disabled', { stickyKey });
112
+ return markSkip('skip_auto_state_disabled', { armed: false, mode: 'off' });
81
113
  }
82
- saveRoutingInstructionStateAsync(stickyKey, state);
83
- }
84
- const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
85
- const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
86
- ? Math.max(1, Math.floor(state.stopMessageMaxRepeats))
87
- : 0;
88
- if (!text || maxRepeats <= 0) {
89
- debugLog('skip_invalid_text_or_maxRepeats', {
90
- stickyKey,
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
- return null;
95
- }
96
- const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
97
- ? Math.max(0, Math.floor(state.stopMessageUsed))
98
- : 0;
99
- if (used >= maxRepeats) {
100
- debugLog('skip_reached_max_repeats', {
101
- stickyKey,
102
- used,
103
- maxRepeats
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
- // Auto-clear after reaching max repeats to avoid leaving an "exhausted" stopMessage stuck in sticky state.
106
- clearStopMessageState(state, Date.now());
107
- saveRoutingInstructionStateAsync(stickyKey, state);
108
- return null;
109
- }
110
- if (!isStopFinishReason(ctx.base)) {
111
- debugLog('skip_not_stop_finish_reason', {
112
- stickyKey
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
- return null;
115
- }
116
- const captured = getCapturedRequest(ctx.adapterContext);
117
- if (!captured) {
118
- debugLog('skip_no_captured_request', {
119
- stickyKey
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
- return null;
122
- }
123
- if (isCompactionRequest(captured)) {
124
- debugLog('skip_compaction_request', { stickyKey });
125
- return null;
126
- }
127
- const entryEndpoint = resolveEntryEndpoint(record);
128
- const seed = extractCapturedChatSeed(captured);
129
- if (!seed) {
130
- debugLog('skip_failed_build_followup', { stickyKey });
131
- return null;
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 null;
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
- const followupText = typeof stageDecision.followupText === 'string' && stageDecision.followupText.trim()
156
- ? stageDecision.followupText.trim()
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 = typeof record.sessionId === 'string' && record.sessionId.trim() ? record.sessionId.trim() : '';
206
- const conversationId = typeof record.conversationId === 'string' && record.conversationId.trim()
207
- ? record.conversationId.trim()
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 captured = adapterContext.capturedChatRequest;
354
- if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
355
- return null;
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 captured;
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
- if (!text || maxRepeats <= 0) {
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 (!isStopFinishReason(ctx.base)) {
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;