@jsonstudio/llms 0.6.1739 → 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 +359 -39
  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 +15 -1
  40. package/dist/router/virtual-router/routing-instructions.js +110 -151
  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 +5 -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 +19 -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 +386 -257
  85. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +43 -0
  86. package/dist/servertool/handlers/stop-message-stage-policy.js +684 -0
  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,8 +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
+ import { resolveStopMessageStageDecision } from './stop-message-stage-policy.js';
5
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';
6
9
  const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
7
10
  const STOPMESSAGE_IMPLICIT_GEMINI = (process.env.ROUTECODEX_STOPMESSAGE_IMPLICIT_GEMINI || '').trim() === '1';
8
11
  function debugLog(message, extra) {
@@ -20,190 +23,277 @@ function debugLog(message, extra) {
20
23
  }
21
24
  }
22
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');
23
32
  const handler = async (ctx) => {
24
33
  const record = ctx.adapterContext;
25
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
+ };
26
75
  debugLog('handler_start', {
27
76
  requestId: record.requestId,
28
77
  providerProtocol: record.providerProtocol
29
78
  });
30
- const followupFlagRaw = rt?.serverToolFollowup;
31
- if (followupFlagRaw === true ||
32
- (typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true')) {
33
- // Allow chained followups only within the stop_message_flow loop itself.
34
- // Other servertool followups must not re-trigger stopMessage (prevents cross-flow loops).
35
- const loopState = rt?.serverToolLoopState;
36
- const flowId = loopState && typeof loopState === 'object' && !Array.isArray(loopState)
37
- ? String(loopState.flowId || '').trim()
38
- : '';
39
- if (flowId !== FLOW_ID) {
79
+ try {
80
+ const followupFlagRaw = rt?.serverToolFollowup;
81
+ if (followupFlagRaw === true ||
82
+ (typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true')) {
40
83
  debugLog('skip_followup_loop');
41
- return null;
84
+ return markSkip('skip_followup_loop');
42
85
  }
43
- }
44
- if (hasCompactionFlag(rt)) {
45
- debugLog('skip_compaction_flag');
46
- return null;
47
- }
48
- const connectionState = resolveClientConnectionState(record.clientConnectionState);
49
- if (connectionState?.disconnected === true) {
50
- debugLog('skip_client_disconnected');
51
- return null;
52
- }
53
- const clientDisconnectedRaw = record.clientDisconnected;
54
- if (clientDisconnectedRaw === true ||
55
- (typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
56
- debugLog('skip_client_disconnected_flag');
57
- return null;
58
- }
59
- const stickyKey = resolveStickyKey(record);
60
- if (!stickyKey) {
61
- debugLog('skip_no_sticky_key');
62
- return null;
63
- }
64
- let state = loadRoutingInstructionStateSync(stickyKey);
65
- // If stopMessage was created implicitly (auto) but implicit mode is disabled, do not run it.
66
- // This avoids surprising followups like "继续执行" when the user never enabled stopMessage.
67
- if (state &&
68
- typeof state.stopMessageSource === 'string' &&
69
- state.stopMessageSource.trim().toLowerCase() === 'auto' &&
70
- !STOPMESSAGE_IMPLICIT_GEMINI) {
71
- state.stopMessageText = undefined;
72
- state.stopMessageMaxRepeats = undefined;
73
- state.stopMessageUsed = undefined;
74
- state.stopMessageUpdatedAt = undefined;
75
- state.stopMessageLastUsedAt = undefined;
76
- saveRoutingInstructionStateAsync(stickyKey, state);
77
- debugLog('skip_auto_state_disabled', { stickyKey });
78
- return null;
79
- }
80
- if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
81
- const fallback = resolveStopMessageSnapshot(rt?.stopMessageState);
82
- if (fallback) {
83
- state = createStopMessageState(fallback);
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');
84
100
  }
85
- else {
86
- const implicit = STOPMESSAGE_IMPLICIT_GEMINI ? resolveImplicitGeminiStopMessageSnapshot(ctx, record) : null;
87
- if (!implicit) {
88
- debugLog('skip_no_state', { stickyKey });
89
- 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);
90
110
  }
91
- state = createStopMessageState(implicit);
111
+ debugLog('skip_auto_state_disabled', { stickyKey });
112
+ return markSkip('skip_auto_state_disabled', { armed: false, mode: 'off' });
92
113
  }
93
- saveRoutingInstructionStateAsync(stickyKey, state);
94
- }
95
- const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
96
- const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
97
- ? Math.max(1, Math.floor(state.stopMessageMaxRepeats))
98
- : 0;
99
- if (!text || maxRepeats <= 0) {
100
- debugLog('skip_invalid_text_or_maxRepeats', {
101
- 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,
102
142
  textLength: text.length,
103
143
  maxRepeats
104
144
  });
105
- return null;
106
- }
107
- const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
108
- ? Math.max(0, Math.floor(state.stopMessageUsed))
109
- : 0;
110
- if (used >= maxRepeats) {
111
- debugLog('skip_reached_max_repeats', {
112
- stickyKey,
113
- used,
114
- 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
+ : []
115
205
  });
116
- // Auto-clear after reaching max repeats to avoid leaving an "exhausted" stopMessage stuck in sticky state.
117
- const now = Date.now();
118
- state.stopMessageText = undefined;
119
- state.stopMessageMaxRepeats = undefined;
120
- state.stopMessageUsed = undefined;
121
- state.stopMessageSource = undefined;
122
- // Keep monotonic timestamps as a tombstone to prevent accidental re-application from replayed history.
123
- state.stopMessageUpdatedAt = now;
124
- state.stopMessageLastUsedAt = now;
125
- saveRoutingInstructionStateAsync(stickyKey, state);
126
- return null;
127
- }
128
- if (!isStopFinishReason(ctx.base)) {
129
- debugLog('skip_not_stop_finish_reason', {
130
- 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
131
216
  });
132
- return null;
133
- }
134
- const captured = getCapturedRequest(ctx.adapterContext);
135
- if (!captured) {
136
- debugLog('skip_no_captured_request', {
137
- 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
138
255
  });
139
- return null;
140
- }
141
- if (isCompactionRequest(captured)) {
142
- debugLog('skip_compaction_request', { stickyKey });
143
- return null;
144
- }
145
- const entryEndpoint = resolveEntryEndpoint(record);
146
- const seed = extractCapturedChatSeed(captured);
147
- if (!seed) {
148
- debugLog('skip_failed_build_followup', { stickyKey });
149
- return null;
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'
268
+ });
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
+ };
150
286
  }
151
- // Extract assistant message for potential followup injection (no-op today; keeps compat).
152
- void extractAssistantMessageForFollowup(ctx.base);
153
- // Increment stopMessage usage counter when we decide to trigger followup.
154
- const nextUsed = used + 1;
155
- state.stopMessageUsed = nextUsed;
156
- state.stopMessageLastUsedAt = Date.now();
157
- // If this will be the last allowed trigger, mark it for cleanup.
158
- // We still return the followup plan for this trigger, but clear the config
159
- // so the next response won't trigger again.
160
- if (nextUsed >= maxRepeats) {
161
- const now = Date.now();
162
- state.stopMessageText = undefined;
163
- state.stopMessageMaxRepeats = undefined;
164
- state.stopMessageUsed = undefined;
165
- state.stopMessageSource = undefined;
166
- state.stopMessageUpdatedAt = now;
287
+ finally {
288
+ attachStopMessageCompareContext(ctx.adapterContext, compare);
167
289
  }
168
- saveRoutingInstructionStateAsync(stickyKey, state);
169
- const followupProviderKey = resolveStopMessageFollowupProviderKey({ record, runtimeMetadata: rt });
170
- const followupToolContentMaxChars = resolveStopMessageFollowupToolContentMaxChars({
171
- providerKey: followupProviderKey,
172
- model: seed.model
173
- });
174
- const followupOps = [];
175
- if (typeof followupToolContentMaxChars === 'number' &&
176
- Number.isFinite(followupToolContentMaxChars) &&
177
- followupToolContentMaxChars > 0) {
178
- followupOps.push({ op: 'compact_tool_content', maxChars: Math.floor(followupToolContentMaxChars) });
179
- }
180
- followupOps.push({ op: 'append_assistant_message', required: false });
181
- followupOps.push({ op: 'ensure_standard_tools' });
182
- followupOps.push({ op: 'append_user_text', text });
183
- return {
184
- flowId: FLOW_ID,
185
- finalize: async () => ({
186
- chatResponse: ctx.base,
187
- execution: {
188
- flowId: FLOW_ID,
189
- followup: {
190
- requestIdSuffix: ':stop_followup',
191
- entryEndpoint,
192
- injection: {
193
- ops: followupOps
194
- },
195
- metadata: (connectionState ? { clientConnectionState: connectionState } : {})
196
- }
197
- }
198
- })
199
- };
200
290
  };
201
- registerServerToolHandler('stop_message_auto', handler, { trigger: 'auto' });
202
- function resolveStickyKey(record) {
203
- const sessionId = typeof record.sessionId === 'string' && record.sessionId.trim() ? record.sessionId.trim() : '';
204
- const conversationId = typeof record.conversationId === 'string' && record.conversationId.trim()
205
- ? record.conversationId.trim()
206
- : '';
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');
207
297
  if (sessionId) {
208
298
  return `session:${sessionId}`;
209
299
  }
@@ -212,6 +302,47 @@ function resolveStickyKey(record) {
212
302
  }
213
303
  return undefined;
214
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
+ }
215
346
  function toNonEmptyText(value) {
216
347
  return typeof value === 'string' && value.trim().length ? value.trim() : '';
217
348
  }
@@ -233,6 +364,30 @@ function readProviderKeyFromMetadata(value) {
233
364
  }
234
365
  return '';
235
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
+ }
236
391
  function resolveStopMessageFollowupProviderKey(args) {
237
392
  const direct = toNonEmptyText(args.record.providerKey) ||
238
393
  toNonEmptyText(args.record.providerId) ||
@@ -259,100 +414,27 @@ function resolveStopMessageFollowupToolContentMaxChars(params) {
259
414
  }
260
415
  return undefined;
261
416
  }
262
- function isStopFinishReason(base) {
263
- if (!base || typeof base !== 'object' || Array.isArray(base)) {
264
- return false;
265
- }
266
- const payload = base;
267
- const choicesRaw = payload.choices;
268
- if (Array.isArray(choicesRaw) && choicesRaw.length) {
269
- const first = choicesRaw[0];
270
- if (!first || typeof first !== 'object' || Array.isArray(first)) {
271
- return false;
272
- }
273
- const finishReasonRaw = first.finish_reason;
274
- const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
275
- ? finishReasonRaw.trim().toLowerCase()
276
- : '';
277
- // 将模型视为“自然结束”的场景:
278
- // - OpenAI 兼容:finish_reason === 'stop'
279
- // - 截断场景:finish_reason === 'length'(例如 Gemini/Claude 流式输出被 max token 截断)
280
- // 统一视作可触发 stopMessage 的终止点;仅排除显式的 tool_calls。
281
- if (!finishReason || finishReason === 'tool_calls') {
282
- return false;
283
- }
284
- if (finishReason !== 'stop' && finishReason !== 'length') {
285
- return false;
286
- }
287
- const message = first.message &&
288
- typeof first.message === 'object' &&
289
- !Array.isArray(first.message)
290
- ? first.message
291
- : null;
292
- if (!message) {
293
- return false;
294
- }
295
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
296
- if (toolCalls.length > 0) {
297
- // 如果当前响应仍在发起工具调用,则由工具执行驱动后续轮次,不触发 stopMessage。
298
- return false;
299
- }
300
- return true;
301
- }
302
- // OpenAI Responses shape: status completed + no required_action + no tool-like output entries
303
- const statusRaw = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
304
- if (statusRaw && statusRaw !== 'completed') {
305
- return false;
306
- }
307
- if (payload.required_action && typeof payload.required_action === 'object') {
308
- return false;
309
- }
310
- const outputRaw = Array.isArray(payload.output) ? payload.output : [];
311
- if (!outputRaw.length) {
312
- return false;
313
- }
314
- if (outputRaw.some((item) => hasToolLikeOutput(item))) {
315
- return false;
316
- }
317
- return true;
318
- }
319
- function extractAssistantMessageForFollowup(chatResponse) {
320
- if (!chatResponse || typeof chatResponse !== 'object' || Array.isArray(chatResponse)) {
321
- return null;
322
- }
323
- const choices = Array.isArray(chatResponse.choices)
324
- ? chatResponse.choices
325
- : [];
326
- if (!choices.length) {
327
- return null;
328
- }
329
- const first = choices[0];
330
- if (!first || typeof first !== 'object' || Array.isArray(first)) {
331
- return null;
332
- }
333
- const message = first.message;
334
- if (!message || typeof message !== 'object' || Array.isArray(message)) {
335
- return null;
336
- }
337
- const role = typeof message.role === 'string' ? String(message.role) : '';
338
- if (role && role.toLowerCase() !== 'assistant') {
339
- return null;
340
- }
341
- const content = message.content;
342
- if (typeof content !== 'string' || !content.trim()) {
343
- return null;
344
- }
345
- return { role: 'assistant', content: content.trim() };
346
- }
347
417
  function getCapturedRequest(adapterContext) {
348
418
  if (!adapterContext || typeof adapterContext !== 'object') {
349
419
  return null;
350
420
  }
351
- const captured = adapterContext.capturedChatRequest;
352
- if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
353
- 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;
354
432
  }
355
- return captured;
433
+ const originalRequest = contextRecord.originalRequest;
434
+ if (originalRequest && typeof originalRequest === 'object' && !Array.isArray(originalRequest)) {
435
+ return originalRequest;
436
+ }
437
+ return null;
356
438
  }
357
439
  function extractResponsesOutputText(base) {
358
440
  const raw = base.output_text;
@@ -422,7 +504,9 @@ function resolveStopMessageSnapshot(raw) {
422
504
  const maxRepeats = typeof record.stopMessageMaxRepeats === 'number' && Number.isFinite(record.stopMessageMaxRepeats)
423
505
  ? Math.max(1, Math.floor(record.stopMessageMaxRepeats))
424
506
  : 0;
425
- 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) {
426
510
  return null;
427
511
  }
428
512
  const used = typeof record.stopMessageUsed === 'number' && Number.isFinite(record.stopMessageUsed)
@@ -437,13 +521,30 @@ function resolveStopMessageSnapshot(raw) {
437
521
  const source = typeof record.stopMessageSource === 'string' && record.stopMessageSource.trim()
438
522
  ? record.stopMessageSource.trim()
439
523
  : undefined;
524
+ const stage = typeof record.stopMessageStage === 'string' && record.stopMessageStage.trim()
525
+ ? record.stopMessageStage.trim()
526
+ : undefined;
527
+ const observationHash = typeof record.stopMessageObservationHash === 'string' && record.stopMessageObservationHash.trim()
528
+ ? record.stopMessageObservationHash.trim()
529
+ : undefined;
530
+ const observationStableCount = typeof record.stopMessageObservationStableCount === 'number' && Number.isFinite(record.stopMessageObservationStableCount)
531
+ ? Math.max(0, Math.floor(record.stopMessageObservationStableCount))
532
+ : undefined;
533
+ const bdWorkState = typeof record.stopMessageBdWorkState === 'string' && record.stopMessageBdWorkState.trim()
534
+ ? record.stopMessageBdWorkState.trim()
535
+ : undefined;
440
536
  return {
441
537
  text,
442
538
  maxRepeats,
443
539
  used,
444
540
  ...(source ? { source } : {}),
445
541
  ...(updatedAt ? { updatedAt } : {}),
446
- ...(lastUsedAt ? { lastUsedAt } : {})
542
+ ...(lastUsedAt ? { lastUsedAt } : {}),
543
+ ...(stage ? { stage } : {}),
544
+ ...(stageMode ? { stageMode } : {}),
545
+ ...(observationHash ? { observationHash } : {}),
546
+ ...(typeof observationStableCount === 'number' ? { observationStableCount } : {}),
547
+ ...(bdWorkState ? { bdWorkState } : {})
447
548
  };
448
549
  }
449
550
  function hasCompactionFlag(rt) {
@@ -478,7 +579,7 @@ function resolveImplicitGeminiStopMessageSnapshot(ctx, record) {
478
579
  return null;
479
580
  }
480
581
  // 仅在本轮响应被视为“自然结束”(stop/length,且无 tool_calls)时触发,避免干扰正常对话。
481
- if (!isStopFinishReason(ctx.base)) {
582
+ if (!isStopEligibleForServerTool(ctx.base, ctx.adapterContext)) {
482
583
  return null;
483
584
  }
484
585
  // 仅在“空回复”时触发隐式 stopMessage:
@@ -564,9 +665,37 @@ function createStopMessageState(snapshot) {
564
665
  stopMessageMaxRepeats: snapshot.maxRepeats,
565
666
  stopMessageUsed: snapshot.used,
566
667
  stopMessageUpdatedAt: snapshot.updatedAt,
567
- stopMessageLastUsedAt: snapshot.lastUsedAt
668
+ stopMessageLastUsedAt: snapshot.lastUsedAt,
669
+ stopMessageStage: snapshot.stage,
670
+ stopMessageStageMode: snapshot.stageMode,
671
+ stopMessageObservationHash: snapshot.observationHash,
672
+ stopMessageObservationStableCount: snapshot.observationStableCount,
673
+ stopMessageBdWorkState: snapshot.bdWorkState
568
674
  };
569
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
+ }
686
+ function clearStopMessageState(state, now) {
687
+ state.stopMessageText = undefined;
688
+ state.stopMessageMaxRepeats = undefined;
689
+ state.stopMessageUsed = undefined;
690
+ state.stopMessageSource = undefined;
691
+ state.stopMessageStage = undefined;
692
+ state.stopMessageStageMode = undefined;
693
+ state.stopMessageObservationHash = undefined;
694
+ state.stopMessageObservationStableCount = undefined;
695
+ state.stopMessageBdWorkState = undefined;
696
+ state.stopMessageUpdatedAt = now;
697
+ state.stopMessageLastUsedAt = now;
698
+ }
570
699
  function resolveEntryEndpoint(record) {
571
700
  const raw = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
572
701
  ? record.entryEndpoint.trim()