@jsonstudio/llms 0.6.2125 → 0.6.2172

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 (37) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-response.js +27 -3
  2. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
  3. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +9 -3
  4. package/dist/conversion/hub/process/chat-process.js +15 -18
  5. package/dist/conversion/responses/responses-openai-bridge.js +13 -12
  6. package/dist/conversion/shared/bridge-message-utils.js +92 -39
  7. package/dist/router/virtual-router/classifier.js +29 -5
  8. package/dist/router/virtual-router/engine/routing-pools/index.js +111 -5
  9. package/dist/router/virtual-router/engine-selection/multimodal-capability.d.ts +3 -0
  10. package/dist/router/virtual-router/engine-selection/multimodal-capability.js +26 -0
  11. package/dist/router/virtual-router/engine-selection/route-utils.js +6 -2
  12. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +1 -0
  13. package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -0
  14. package/dist/router/virtual-router/engine.d.ts +2 -0
  15. package/dist/router/virtual-router/engine.js +57 -14
  16. package/dist/router/virtual-router/features.js +12 -4
  17. package/dist/router/virtual-router/message-utils.d.ts +8 -0
  18. package/dist/router/virtual-router/message-utils.js +170 -45
  19. package/dist/router/virtual-router/token-counter.js +51 -10
  20. package/dist/router/virtual-router/types.d.ts +3 -0
  21. package/dist/servertool/clock/session-scope.d.ts +3 -0
  22. package/dist/servertool/clock/session-scope.js +52 -0
  23. package/dist/servertool/engine.js +68 -8
  24. package/dist/servertool/handlers/clock-auto.js +2 -8
  25. package/dist/servertool/handlers/clock.js +3 -9
  26. package/dist/servertool/handlers/stop-message-auto/blocked-report.d.ts +16 -0
  27. package/dist/servertool/handlers/stop-message-auto/blocked-report.js +349 -0
  28. package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -0
  29. package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +503 -0
  30. package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +38 -0
  31. package/dist/servertool/handlers/stop-message-auto/routing-state.js +149 -0
  32. package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +67 -0
  33. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +387 -0
  34. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -7
  35. package/dist/servertool/handlers/stop-message-auto.js +69 -971
  36. package/dist/servertool/handlers/web-search.js +117 -0
  37. package/package.json +1 -1
@@ -1,17 +1,30 @@
1
- import * as childProcess from 'node:child_process';
2
- import * as fs from 'node:fs';
3
- import * as path from 'node:path';
4
1
  import { registerServerToolHandler } from '../registry.js';
5
- import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync, saveRoutingInstructionStateSync } from '../../router/virtual-router/sticky-session-store.js';
6
- import { DEFAULT_STOP_MESSAGE_MAX_REPEATS, ensureStopMessageModeMaxRepeats } from '../../router/virtual-router/routing-stop-message-state-codec.js';
7
2
  import { isCompactionRequest } from './compaction-detect.js';
8
3
  import { extractCapturedChatSeed } from './followup-request-builder.js';
9
- import { resolveStopMessageStageDecision } from './stop-message-stage-policy.js';
10
4
  import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
11
5
  import { isStopEligibleForServerTool } from '../stop-gateway-context.js';
12
6
  import { attachStopMessageCompareContext } from '../stop-message-compare-context.js';
7
+ import { extractStopMessageAutoResponseSnapshot, renderStopMessageAutoFollowupViaIflow } from './stop-message-auto/iflow-followup.js';
8
+ import { getCapturedRequest, hasCompactionFlag, readServerToolFollowupFlowId, resolveClientConnectionState, resolveEntryEndpoint, resolveImplicitGeminiStopMessageSnapshot, resolveStopMessageFollowupProviderKey, resolveStopMessageFollowupToolContentMaxChars } from './stop-message-auto/runtime-utils.js';
9
+ export { extractBlockedReportFromMessagesForTests } from './stop-message-auto/blocked-report.js';
13
10
  const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
14
11
  const STOPMESSAGE_IMPLICIT_GEMINI = (process.env.ROUTECODEX_STOPMESSAGE_IMPLICIT_GEMINI || '').trim() === '1';
12
+ const STOPMESSAGE_DEFAULT_ENABLED = (() => {
13
+ const raw = process.env.ROUTECODEX_STOPMESSAGE_DEFAULT_ENABLED;
14
+ if (typeof raw === 'string' && raw.trim().length > 0) {
15
+ return raw.trim() !== '0';
16
+ }
17
+ return process.env.NODE_ENV !== 'test';
18
+ })();
19
+ const STOPMESSAGE_DEFAULT_TEXT = (() => {
20
+ const raw = process.env.ROUTECODEX_STOPMESSAGE_DEFAULT_TEXT;
21
+ return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : '继续执行';
22
+ })();
23
+ const STOPMESSAGE_DEFAULT_MAX_REPEATS = (() => {
24
+ const raw = process.env.ROUTECODEX_STOPMESSAGE_DEFAULT_MAX_REPEATS;
25
+ const parsed = typeof raw === 'string' ? Number(raw.trim()) : Number.NaN;
26
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1;
27
+ })();
15
28
  function debugLog(message, extra) {
16
29
  if (!STOPMESSAGE_DEBUG) {
17
30
  return;
@@ -35,9 +48,6 @@ const STOP_MESSAGE_EXECUTION_DIRECTIVE = [
35
48
  '- 若遇阻塞,请按 JSON 输出结构化阻塞信息(type=blocked, summary, blocker, impact, next_action, evidence)。',
36
49
  '- 如果即将返回 finish_reason=stop 或协议等价 end_turn,先调用 continue_execution 工具,不要直接结束会话。'
37
50
  ].join('\n');
38
- const STOP_MESSAGE_BD_CREATE_TIMEOUT_MS = 2_000;
39
- const STOP_MESSAGE_BLOCKED_TEXT_SCAN_LIMIT = 12;
40
- const STOP_MESSAGE_BLOCKED_CANDIDATE_MAX_LENGTH = 12_000;
41
51
  const handler = async (ctx) => {
42
52
  const record = ctx.adapterContext;
43
53
  const rt = readRuntimeMetadata(ctx.adapterContext);
@@ -101,185 +111,87 @@ const handler = async (ctx) => {
101
111
  return markSkip('skip_compaction_flag');
102
112
  }
103
113
  const connectionState = resolveClientConnectionState(record.clientConnectionState);
104
- const stickyKey = resolveStickyKey(record, rt);
105
- let state = stickyKey ? loadRoutingInstructionStateSync(stickyKey) : undefined;
106
- if (state &&
107
- typeof state.stopMessageSource === 'string' &&
108
- state.stopMessageSource.trim().toLowerCase() === 'auto' &&
109
- !STOPMESSAGE_IMPLICIT_GEMINI) {
110
- clearStopMessageState(state, Date.now());
111
- if (stickyKey) {
112
- persistStopMessageState(stickyKey, state);
113
- }
114
- debugLog('skip_auto_state_disabled', { stickyKey });
115
- return markSkip('skip_auto_state_disabled', { armed: false, mode: 'off' });
116
- }
117
- if (!state || !hasArmedStopMessageState(state)) {
118
- const fallback = resolveStopMessageSnapshot(rt?.stopMessageState);
119
- if (fallback) {
120
- state = createStopMessageState(fallback);
121
- }
122
- else {
123
- const implicit = STOPMESSAGE_IMPLICIT_GEMINI ? resolveImplicitGeminiStopMessageSnapshot(ctx, record) : null;
124
- if (!implicit) {
125
- debugLog('skip_no_state', { stickyKey });
126
- return markSkip('skip_no_state', { armed: false, mode: 'off' });
127
- }
128
- state = createStopMessageState(implicit);
129
- }
130
- if (stickyKey) {
131
- persistStopMessageState(stickyKey, state);
132
- }
133
- }
134
- if (state && ensureStopMessageModeMaxRepeats(state) && stickyKey) {
135
- persistStopMessageState(stickyKey, state);
136
- }
137
- updateCompare({ armed: hasArmedStopMessageState(state) });
138
- const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
139
- const stageMode = normalizeStopMessageModeValue(state.stopMessageStageMode);
140
- const maxRepeats = resolveStopMessageMaxRepeats(state.stopMessageMaxRepeats, stageMode);
141
- const mode = stageMode === 'on' || stageMode === 'auto' || stageMode === 'off' ? stageMode : 'off';
142
- const allowModeOnlyState = !text && maxRepeats > 0 && (stageMode === 'on' || stageMode === 'auto');
114
+ const implicit = STOPMESSAGE_IMPLICIT_GEMINI
115
+ ? resolveImplicitGeminiStopMessageSnapshot(ctx, record)
116
+ : null;
117
+ if (!STOPMESSAGE_DEFAULT_ENABLED && !implicit) {
118
+ debugLog('skip_default_disabled');
119
+ return markSkip('skip_default_disabled', { armed: false, mode: 'off' });
120
+ }
121
+ const text = ((typeof implicit?.text === 'string' && implicit.text.trim().length > 0
122
+ ? implicit.text
123
+ : STOPMESSAGE_DEFAULT_TEXT) || '继续执行').trim();
124
+ const maxRepeats = typeof implicit?.maxRepeats === 'number' && Number.isFinite(implicit.maxRepeats) && implicit.maxRepeats > 0
125
+ ? Math.floor(implicit.maxRepeats)
126
+ : STOPMESSAGE_DEFAULT_MAX_REPEATS;
143
127
  updateCompare({
144
- mode,
145
- allowModeOnly: allowModeOnlyState,
128
+ armed: true,
129
+ mode: 'on',
130
+ allowModeOnly: text.length === 0,
146
131
  textLength: text.length,
147
- maxRepeats
132
+ maxRepeats: Math.max(1, maxRepeats),
133
+ used: 0
148
134
  });
149
- if ((!text && !allowModeOnlyState) || maxRepeats <= 0) {
150
- debugLog('skip_invalid_text_or_maxRepeats', {
151
- stickyKey,
152
- textLength: text.length,
153
- maxRepeats,
154
- stageMode
155
- });
156
- return markSkip('skip_invalid_text_or_maxRepeats');
157
- }
158
- const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
159
- ? Math.max(0, Math.floor(state.stopMessageUsed))
160
- : 0;
161
- updateCompare({ used });
162
- if (used >= maxRepeats) {
163
- debugLog('skip_reached_max_repeats', {
164
- stickyKey,
165
- used,
166
- maxRepeats
167
- });
168
- clearStopMessageState(state, Date.now());
169
- if (stickyKey) {
170
- persistStopMessageState(stickyKey, state);
171
- }
172
- return markSkip('skip_reached_max_repeats');
173
- }
174
135
  const stopEligible = isStopEligibleForServerTool(ctx.base, ctx.adapterContext);
175
136
  updateCompare({ stopEligible });
176
137
  if (!stopEligible) {
177
- debugLog('skip_not_stop_finish_reason', {
178
- stickyKey
179
- });
138
+ debugLog('skip_not_stop_finish_reason');
180
139
  return markSkip('skip_not_stop_finish_reason');
181
140
  }
182
141
  const captured = getCapturedRequest(ctx.adapterContext);
183
142
  updateCompare({ hasCapturedRequest: Boolean(captured) });
184
143
  if (!captured) {
185
- debugLog('skip_no_captured_request', {
186
- stickyKey
187
- });
144
+ debugLog('skip_no_captured_request');
188
145
  return markSkip('skip_no_captured_request');
189
146
  }
190
147
  const compactionRequest = isCompactionRequest(captured);
191
148
  updateCompare({ compactionRequest });
192
149
  if (compactionRequest) {
193
- debugLog('skip_compaction_request', { stickyKey });
150
+ debugLog('skip_compaction_request');
194
151
  return markSkip('skip_compaction_request');
195
152
  }
196
153
  const entryEndpoint = resolveEntryEndpoint(record);
197
154
  const seed = extractCapturedChatSeed(captured);
198
155
  updateCompare({ hasSeed: Boolean(seed) });
199
156
  if (!seed) {
200
- debugLog('skip_failed_build_followup', { stickyKey });
157
+ debugLog('skip_failed_build_followup');
201
158
  return markSkip('skip_failed_build_followup');
202
159
  }
203
- const capturedMessages = Array.isArray(seed.messages)
204
- ? (seed.messages || [])
205
- : [];
206
- const blockedReport = extractBlockedReportFromMessages(capturedMessages);
207
- if (blockedReport) {
208
- const bdWorkingDirectory = resolveBdWorkingDirectoryForRecord(record, rt);
209
- const issueId = createBdIssueFromBlockedReport(blockedReport, {
210
- requestId: typeof record.requestId === 'string'
211
- ? record.requestId
212
- : undefined,
213
- sessionId: typeof record.sessionId === 'string' ? record.sessionId : undefined
214
- }, bdWorkingDirectory);
215
- if (issueId) {
216
- clearStopMessageState(state, Date.now());
217
- if (stickyKey) {
218
- persistStopMessageState(stickyKey, state);
219
- }
220
- debugLog('blocked_report_issue_created', {
221
- stickyKey,
222
- issueId
223
- });
224
- return markSkip('blocked_issue_created');
225
- }
226
- debugLog('blocked_report_issue_create_failed', { stickyKey });
227
- }
228
- const bdWorkingDirectory = resolveBdWorkingDirectoryForRecord(record, rt);
229
- const stageDecision = resolveStopMessageStageDecision({
230
- baseText: text,
231
- state,
232
- capturedMessages,
233
- bdWorkingDirectory
234
- });
235
- state.stopMessageObservationHash = stageDecision.observationHash;
236
- state.stopMessageObservationStableCount = stageDecision.observationStableCount;
237
- state.stopMessageBdWorkState = stageDecision.bdWorkState;
238
- state.stopMessageStage = stageDecision.stage;
239
- state.stopMessageAssignedIssueId = stageDecision.assignedIssueId;
240
- state.stopMessageAssignedIssueSource = stageDecision.assignedIssueSource;
241
- state.stopMessageNoTaskSummaryUsed = stageDecision.noTaskSummaryUsed;
242
- updateCompare({
243
- stage: stageDecision.stage,
244
- bdWorkState: stageDecision.bdWorkState,
245
- observationHash: stageDecision.observationHash,
246
- observationStableCount: stageDecision.observationStableCount,
247
- toolSignatureHash: stageDecision.toolSignatureHash
160
+ const used = 0;
161
+ let followupText = text || '继续执行';
162
+ const autoResponseSnapshot = extractStopMessageAutoResponseSnapshot(ctx.base, ctx.adapterContext);
163
+ const iflowFollowupText = renderStopMessageAutoFollowupViaIflow({
164
+ baseStopMessageText: text,
165
+ candidateFollowupText: followupText,
166
+ responseSnapshot: autoResponseSnapshot,
167
+ requestId: typeof record.requestId === 'string'
168
+ ? record.requestId.trim()
169
+ : undefined,
170
+ sessionId: typeof record.sessionId === 'string'
171
+ ? record.sessionId.trim()
172
+ : undefined,
173
+ providerKey: resolveStopMessageFollowupProviderKey({ record, runtimeMetadata: rt }),
174
+ model: typeof seed.model === 'string' ? seed.model : undefined,
175
+ usedRepeats: used,
176
+ maxRepeats
248
177
  });
249
- if (stageDecision.action === 'stop') {
250
- clearStopMessageState(state, Date.now());
251
- if (stickyKey) {
252
- persistStopMessageState(stickyKey, state);
178
+ if (iflowFollowupText) {
179
+ followupText = iflowFollowupText.trim();
180
+ if (text && !followupText.includes(text)) {
181
+ followupText = `${text}\n${followupText}`.trim();
253
182
  }
254
- debugLog('stop_by_stage_policy', {
255
- stickyKey,
256
- reason: stageDecision.stopReason,
257
- observationStableCount: stageDecision.observationStableCount,
258
- bdWorkState: stageDecision.bdWorkState
183
+ debugLog('iflow_automessage_followup_applied', {
184
+ textLength: followupText.length
259
185
  });
260
- return markSkip('stage_policy_stop');
261
186
  }
262
- const followupText = typeof stageDecision.followupText === 'string' && stageDecision.followupText.trim()
263
- ? stageDecision.followupText.trim()
264
- : text;
187
+ else {
188
+ followupText = '继续执行';
189
+ debugLog('iflow_automessage_followup_fallback_to_continue_execution');
190
+ }
265
191
  if (!followupText) {
266
- debugLog('skip_empty_followup_text_after_stage', {
267
- stickyKey,
268
- stage: stageDecision.stage,
269
- bdWorkState: stageDecision.bdWorkState
270
- });
192
+ debugLog('skip_empty_followup_text_after_stage');
271
193
  return markSkip('skip_empty_followup_text_after_stage');
272
194
  }
273
- const nextUsed = used + 1;
274
- state.stopMessageUsed = nextUsed;
275
- state.stopMessageLastUsedAt = Date.now();
276
- updateCompare({ used: nextUsed });
277
- if (nextUsed >= maxRepeats) {
278
- clearStopMessageState(state, Date.now());
279
- }
280
- if (stickyKey) {
281
- persistStopMessageState(stickyKey, state);
282
- }
283
195
  const followupProviderKey = resolveStopMessageFollowupProviderKey({ record, runtimeMetadata: rt });
284
196
  const followupToolContentMaxChars = resolveStopMessageFollowupToolContentMaxChars({
285
197
  providerKey: followupProviderKey,
@@ -321,817 +233,3 @@ const handler = async (ctx) => {
321
233
  }
322
234
  };
323
235
  registerServerToolHandler('stop_message_auto', handler, { trigger: 'auto', hook: { phase: 'default', priority: 40 } });
324
- function resolveStickyKey(record, runtimeMetadata) {
325
- const sessionId = readSessionScopeValue(record, runtimeMetadata, 'sessionId') ||
326
- readSessionScopeValue(record, runtimeMetadata, 'session_id');
327
- const conversationId = readSessionScopeValue(record, runtimeMetadata, 'conversationId') ||
328
- readSessionScopeValue(record, runtimeMetadata, 'conversation_id');
329
- if (sessionId) {
330
- return `session:${sessionId}`;
331
- }
332
- if (conversationId) {
333
- return `conversation:${conversationId}`;
334
- }
335
- return undefined;
336
- }
337
- function persistStopMessageState(stickyKey, state) {
338
- if (!stickyKey) {
339
- return;
340
- }
341
- saveRoutingInstructionStateSync(stickyKey, state);
342
- saveRoutingInstructionStateAsync(stickyKey, state);
343
- }
344
- function readSessionScopeValue(record, runtimeMetadata, key) {
345
- const direct = toNonEmptyText(record[key]);
346
- if (direct) {
347
- return direct;
348
- }
349
- const metadata = asRecord(record.metadata);
350
- const fromMetadata = metadata ? toNonEmptyText(metadata[key]) : '';
351
- if (fromMetadata) {
352
- return fromMetadata;
353
- }
354
- const fromMetadataContext = metadata ? toNonEmptyText(asRecord(metadata.context)?.[key]) : '';
355
- if (fromMetadataContext) {
356
- return fromMetadataContext;
357
- }
358
- const originalRequest = asRecord(record.originalRequest);
359
- const fromOriginalMetadata = originalRequest
360
- ? toNonEmptyText(asRecord(originalRequest.metadata)?.[key])
361
- : '';
362
- if (fromOriginalMetadata) {
363
- return fromOriginalMetadata;
364
- }
365
- const runtime = asRecord(runtimeMetadata);
366
- const fromRuntime = runtime ? toNonEmptyText(runtime[key]) : '';
367
- if (fromRuntime) {
368
- return fromRuntime;
369
- }
370
- return '';
371
- }
372
- function resolveBdWorkingDirectoryForRecord(record, runtimeMetadata) {
373
- const fromWorkdir = readSessionScopeValue(record, runtimeMetadata, 'workdir');
374
- if (fromWorkdir) {
375
- return fromWorkdir;
376
- }
377
- const fromCwd = readSessionScopeValue(record, runtimeMetadata, 'cwd');
378
- if (fromCwd) {
379
- return fromCwd;
380
- }
381
- const fromWorkingDirectory = readSessionScopeValue(record, runtimeMetadata, 'workingDirectory');
382
- if (fromWorkingDirectory) {
383
- return fromWorkingDirectory;
384
- }
385
- return undefined;
386
- }
387
- function asRecord(value) {
388
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
389
- return null;
390
- }
391
- return value;
392
- }
393
- function toNonEmptyText(value) {
394
- return typeof value === 'string' && value.trim().length ? value.trim() : '';
395
- }
396
- function readProviderKeyFromMetadata(value) {
397
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
398
- return '';
399
- }
400
- const metadata = value;
401
- const direct = toNonEmptyText(metadata.providerKey) ||
402
- toNonEmptyText(metadata.providerId) ||
403
- toNonEmptyText(metadata.targetProviderKey);
404
- if (direct) {
405
- return direct;
406
- }
407
- const target = metadata.target;
408
- if (target && typeof target === 'object' && !Array.isArray(target)) {
409
- const targetRecord = target;
410
- return toNonEmptyText(targetRecord.providerKey) || toNonEmptyText(targetRecord.providerId);
411
- }
412
- return '';
413
- }
414
- function readServerToolFollowupFlowId(runtimeMetadata) {
415
- const runtime = asRecord(runtimeMetadata);
416
- const loopState = runtime ? asRecord(runtime.serverToolLoopState) : null;
417
- const flowId = loopState ? toNonEmptyText(loopState.flowId) : '';
418
- return flowId;
419
- }
420
- function hasArmedStopMessageState(state) {
421
- const mode = normalizeStopMessageModeValue(state.stopMessageStageMode);
422
- const maxRepeats = resolveStopMessageMaxRepeats(state.stopMessageMaxRepeats, mode);
423
- if (maxRepeats <= 0) {
424
- return false;
425
- }
426
- const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
427
- if (text) {
428
- return true;
429
- }
430
- return mode === 'on' || mode === 'auto';
431
- }
432
- function normalizeStopMessageModeValue(value) {
433
- if (typeof value !== 'string') {
434
- return undefined;
435
- }
436
- const normalized = value.trim().toLowerCase();
437
- if (normalized === 'on' || normalized === 'off' || normalized === 'auto') {
438
- return normalized;
439
- }
440
- return undefined;
441
- }
442
- function resolveStopMessageFollowupProviderKey(args) {
443
- const direct = toNonEmptyText(args.record.providerKey) ||
444
- toNonEmptyText(args.record.providerId) ||
445
- readProviderKeyFromMetadata(args.record.metadata) ||
446
- readProviderKeyFromMetadata(args.runtimeMetadata);
447
- return direct;
448
- }
449
- function resolveStopMessageFollowupToolContentMaxChars(params) {
450
- const raw = String(process.env.ROUTECODEX_STOPMESSAGE_FOLLOWUP_TOOL_CONTENT_MAX_CHARS || '').trim();
451
- if (raw) {
452
- const parsed = Number(raw);
453
- if (Number.isFinite(parsed) && parsed > 0) {
454
- return Math.max(64, Math.floor(parsed));
455
- }
456
- return undefined;
457
- }
458
- const providerKey = typeof params.providerKey === 'string' ? params.providerKey.trim().toLowerCase() : '';
459
- if (providerKey.startsWith('iflow.')) {
460
- return 1200;
461
- }
462
- const model = typeof params.model === 'string' ? params.model.trim().toLowerCase() : '';
463
- if (model === 'kimi-k2.5' || model.startsWith('kimi-k2.5-')) {
464
- return 1200;
465
- }
466
- return undefined;
467
- }
468
- function getCapturedRequest(adapterContext) {
469
- if (!adapterContext || typeof adapterContext !== 'object') {
470
- return null;
471
- }
472
- const contextRecord = adapterContext;
473
- const direct = contextRecord.capturedChatRequest;
474
- if (direct && typeof direct === 'object' && !Array.isArray(direct)) {
475
- return direct;
476
- }
477
- const runtime = readRuntimeMetadata(contextRecord);
478
- const runtimeCaptured = runtime && typeof runtime === 'object' && !Array.isArray(runtime)
479
- ? runtime.capturedChatRequest
480
- : undefined;
481
- if (runtimeCaptured && typeof runtimeCaptured === 'object' && !Array.isArray(runtimeCaptured)) {
482
- return runtimeCaptured;
483
- }
484
- const originalRequest = contextRecord.originalRequest;
485
- if (originalRequest && typeof originalRequest === 'object' && !Array.isArray(originalRequest)) {
486
- return originalRequest;
487
- }
488
- return null;
489
- }
490
- function extractResponsesOutputText(base) {
491
- const raw = base.output_text;
492
- if (typeof raw === 'string') {
493
- return raw.trim();
494
- }
495
- if (Array.isArray(raw)) {
496
- const texts = raw
497
- .map((entry) => (typeof entry === 'string' ? entry : ''))
498
- .filter((entry) => entry.trim().length > 0);
499
- if (texts.length > 0) {
500
- return texts.join('\n').trim();
501
- }
502
- }
503
- const output = Array.isArray(base.output) ? (base.output) : [];
504
- const chunks = [];
505
- for (const item of output) {
506
- if (!item || typeof item !== 'object' || Array.isArray(item))
507
- continue;
508
- if (typeof item.type !== 'string')
509
- continue;
510
- const type = String(item.type).trim().toLowerCase();
511
- if (type !== 'message')
512
- continue;
513
- const content = Array.isArray(item.content) ? (item.content) : [];
514
- for (const part of content) {
515
- if (!part || typeof part !== 'object' || Array.isArray(part))
516
- continue;
517
- const pType = typeof part.type === 'string'
518
- ? String(part.type).trim().toLowerCase()
519
- : '';
520
- if (pType === 'output_text') {
521
- const text = typeof part.text === 'string' ? String(part.text) : '';
522
- if (text.trim().length)
523
- chunks.push(text.trim());
524
- }
525
- }
526
- }
527
- return chunks.join('\n').trim();
528
- }
529
- function hasToolLikeOutput(value) {
530
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
531
- return false;
532
- }
533
- const typeRaw = value.type;
534
- const type = typeof typeRaw === 'string' ? typeRaw.trim().toLowerCase() : '';
535
- if (!type) {
536
- return false;
537
- }
538
- return (type === 'tool_call' ||
539
- type === 'tool_use' ||
540
- type === 'function_call' ||
541
- type.includes('tool'));
542
- }
543
- function resolveClientConnectionState(value) {
544
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
545
- return null;
546
- }
547
- return value;
548
- }
549
- function resolveStopMessageSnapshot(raw) {
550
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
551
- return null;
552
- }
553
- const record = raw;
554
- const text = typeof record.stopMessageText === 'string' ? record.stopMessageText.trim() : '';
555
- const stageMode = normalizeStopMessageStageMode(record.stopMessageStageMode);
556
- const maxRepeats = resolveStopMessageMaxRepeats(record.stopMessageMaxRepeats, stageMode);
557
- const allowModeOnlyState = !text && maxRepeats > 0 && (stageMode === 'on' || stageMode === 'auto');
558
- if ((!text && !allowModeOnlyState) || maxRepeats <= 0) {
559
- return null;
560
- }
561
- const used = typeof record.stopMessageUsed === 'number' && Number.isFinite(record.stopMessageUsed)
562
- ? Math.max(0, Math.floor(record.stopMessageUsed))
563
- : 0;
564
- const updatedAt = typeof record.stopMessageUpdatedAt === 'number' && Number.isFinite(record.stopMessageUpdatedAt)
565
- ? record.stopMessageUpdatedAt
566
- : undefined;
567
- const lastUsedAt = typeof record.stopMessageLastUsedAt === 'number' && Number.isFinite(record.stopMessageLastUsedAt)
568
- ? record.stopMessageLastUsedAt
569
- : undefined;
570
- const source = typeof record.stopMessageSource === 'string' && record.stopMessageSource.trim()
571
- ? record.stopMessageSource.trim()
572
- : undefined;
573
- const stage = typeof record.stopMessageStage === 'string' && record.stopMessageStage.trim()
574
- ? record.stopMessageStage.trim()
575
- : undefined;
576
- const observationHash = typeof record.stopMessageObservationHash === 'string' && record.stopMessageObservationHash.trim()
577
- ? record.stopMessageObservationHash.trim()
578
- : undefined;
579
- const observationStableCount = typeof record.stopMessageObservationStableCount === 'number' && Number.isFinite(record.stopMessageObservationStableCount)
580
- ? Math.max(0, Math.floor(record.stopMessageObservationStableCount))
581
- : undefined;
582
- const bdWorkState = typeof record.stopMessageBdWorkState === 'string' && record.stopMessageBdWorkState.trim()
583
- ? record.stopMessageBdWorkState.trim()
584
- : undefined;
585
- const assignedIssueId = typeof record.stopMessageAssignedIssueId === 'string' && record.stopMessageAssignedIssueId.trim()
586
- ? record.stopMessageAssignedIssueId.trim()
587
- : undefined;
588
- const sourceRaw = typeof record.stopMessageAssignedIssueSource === 'string'
589
- ? record.stopMessageAssignedIssueSource.trim().toLowerCase()
590
- : '';
591
- const assignedIssueSource = sourceRaw === 'in_progress' || sourceRaw === 'ready' || sourceRaw === 'open'
592
- ? sourceRaw
593
- : undefined;
594
- const noTaskSummaryUsed = typeof record.stopMessageNoTaskSummaryUsed === 'boolean'
595
- ? record.stopMessageNoTaskSummaryUsed
596
- : undefined;
597
- return {
598
- text,
599
- maxRepeats,
600
- used,
601
- ...(source ? { source } : {}),
602
- ...(updatedAt ? { updatedAt } : {}),
603
- ...(lastUsedAt ? { lastUsedAt } : {}),
604
- ...(stage ? { stage } : {}),
605
- ...(stageMode ? { stageMode } : {}),
606
- ...(observationHash ? { observationHash } : {}),
607
- ...(typeof observationStableCount === 'number' ? { observationStableCount } : {}),
608
- ...(bdWorkState ? { bdWorkState } : {}),
609
- ...(assignedIssueId ? { assignedIssueId } : {}),
610
- ...(assignedIssueSource ? { assignedIssueSource } : {}),
611
- ...(typeof noTaskSummaryUsed === 'boolean' ? { noTaskSummaryUsed } : {})
612
- };
613
- }
614
- function hasCompactionFlag(rt) {
615
- const flag = rt && typeof rt === 'object' && !Array.isArray(rt) ? rt.compactionRequest : undefined;
616
- if (flag === true) {
617
- return true;
618
- }
619
- if (typeof flag === 'string' && flag.trim().toLowerCase() === 'true') {
620
- return true;
621
- }
622
- return false;
623
- }
624
- function resolveImplicitGeminiStopMessageSnapshot(ctx, record) {
625
- try {
626
- const protoFromCtx = ctx.providerProtocol;
627
- const protoFromRecord = typeof record.providerProtocol === 'string' && record.providerProtocol.trim()
628
- ? String(record.providerProtocol).trim()
629
- : undefined;
630
- const providerProtocol = (protoFromCtx || protoFromRecord || '').toString().toLowerCase();
631
- if (providerProtocol !== 'gemini-chat') {
632
- return null;
633
- }
634
- const entryFromRecord = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
635
- ? String(record.entryEndpoint).trim()
636
- : undefined;
637
- const metaEntry = record.metadata &&
638
- typeof record.metadata === 'object' &&
639
- record.metadata.entryEndpoint;
640
- const entryFromMeta = typeof metaEntry === 'string' && metaEntry.trim() ? metaEntry.trim() : undefined;
641
- const entryEndpoint = (entryFromRecord || entryFromMeta || '').toLowerCase();
642
- if (!entryEndpoint.includes('/v1/responses')) {
643
- return null;
644
- }
645
- // 仅在本轮响应被视为“自然结束”(stop/length,且无 tool_calls)时触发,避免干扰正常对话。
646
- if (!isStopEligibleForServerTool(ctx.base, ctx.adapterContext)) {
647
- return null;
648
- }
649
- // 仅在“空回复”时触发隐式 stopMessage:
650
- // - 这个场景由 empty_reply_continue 专门处理;
651
- // - stop_message_auto 里的隐式逻辑只作为兼容兜底(且默认关闭),避免对正常 stop 响应追加“继续执行”。
652
- if (!isEmptyAssistantReply(ctx.base)) {
653
- return null;
654
- }
655
- return {
656
- text: '继续执行',
657
- maxRepeats: 1,
658
- used: 0,
659
- source: 'auto'
660
- };
661
- }
662
- catch {
663
- return null;
664
- }
665
- }
666
- function isEmptyAssistantReply(base) {
667
- if (!base || typeof base !== 'object' || Array.isArray(base)) {
668
- return false;
669
- }
670
- const payload = base;
671
- const choicesRaw = payload.choices;
672
- if (Array.isArray(choicesRaw) && choicesRaw.length) {
673
- const first = choicesRaw[0];
674
- if (!first || typeof first !== 'object' || Array.isArray(first)) {
675
- return false;
676
- }
677
- const finishReasonRaw = first.finish_reason;
678
- const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
679
- ? finishReasonRaw.trim().toLowerCase()
680
- : '';
681
- // 仅接受 stop:length 截断通常并非“空回复”,而是需要续写(由 empty_reply_continue 负责)。
682
- if (finishReason !== 'stop') {
683
- return false;
684
- }
685
- const message = first.message &&
686
- typeof first.message === 'object' &&
687
- !Array.isArray(first.message)
688
- ? first.message
689
- : null;
690
- if (!message) {
691
- return false;
692
- }
693
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
694
- if (toolCalls.length > 0) {
695
- return false;
696
- }
697
- const contentRaw = message.content;
698
- const text = typeof contentRaw === 'string' ? contentRaw.trim() : '';
699
- return text.length === 0;
700
- }
701
- // OpenAI Responses shape: treat empty output_text + no tool-like output as empty reply.
702
- const statusRaw = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
703
- if (statusRaw && statusRaw !== 'completed') {
704
- return false;
705
- }
706
- if (payload.required_action && typeof payload.required_action === 'object') {
707
- return false;
708
- }
709
- const outputText = extractResponsesOutputText(payload);
710
- if (outputText.length > 0) {
711
- return false;
712
- }
713
- const outputRaw = Array.isArray(payload.output) ? payload.output : [];
714
- if (outputRaw.some((item) => hasToolLikeOutput(item))) {
715
- return false;
716
- }
717
- return true;
718
- }
719
- function extractBlockedReportFromMessages(messages) {
720
- if (!Array.isArray(messages) || messages.length === 0) {
721
- return null;
722
- }
723
- const start = Math.max(0, messages.length - STOP_MESSAGE_BLOCKED_TEXT_SCAN_LIMIT);
724
- for (let idx = messages.length - 1; idx >= start; idx -= 1) {
725
- const text = extractCapturedMessageText(messages[idx]);
726
- if (!text) {
727
- continue;
728
- }
729
- const blockedReport = extractBlockedReportFromText(text);
730
- if (blockedReport) {
731
- return blockedReport;
732
- }
733
- }
734
- return null;
735
- }
736
- export function extractBlockedReportFromMessagesForTests(messages) {
737
- return extractBlockedReportFromMessages(messages);
738
- }
739
- function extractBlockedReportFromText(text) {
740
- const trimmed = typeof text === 'string' ? text.trim() : '';
741
- if (!trimmed) {
742
- return null;
743
- }
744
- const candidates = [];
745
- const pushCandidate = (candidate) => {
746
- const normalized = candidate.trim();
747
- if (!normalized || normalized.length > STOP_MESSAGE_BLOCKED_CANDIDATE_MAX_LENGTH) {
748
- return;
749
- }
750
- if (candidates.includes(normalized)) {
751
- return;
752
- }
753
- candidates.push(normalized);
754
- };
755
- pushCandidate(trimmed);
756
- for (const codeBlock of extractJsonCodeBlocks(trimmed)) {
757
- pushCandidate(codeBlock);
758
- }
759
- for (const objectText of extractBalancedJsonObjectStrings(trimmed)) {
760
- if (objectText.includes('"type"') && objectText.toLowerCase().includes('"blocked"')) {
761
- pushCandidate(objectText);
762
- }
763
- }
764
- for (const candidate of candidates) {
765
- let parsed;
766
- try {
767
- parsed = JSON.parse(candidate);
768
- }
769
- catch {
770
- continue;
771
- }
772
- const report = normalizeBlockedReport(parsed);
773
- if (report) {
774
- return report;
775
- }
776
- }
777
- return null;
778
- }
779
- function normalizeBlockedReport(value) {
780
- if (Array.isArray(value)) {
781
- for (const entry of value) {
782
- const report = normalizeBlockedReport(entry);
783
- if (report) {
784
- return report;
785
- }
786
- }
787
- return null;
788
- }
789
- if (!value || typeof value !== 'object') {
790
- return null;
791
- }
792
- const record = value;
793
- const type = toNonEmptyText(record.type).toLowerCase();
794
- if (type !== 'blocked') {
795
- return null;
796
- }
797
- const summary = toNonEmptyText(record.summary) ||
798
- toNonEmptyText(record.title) ||
799
- toNonEmptyText(record.problem);
800
- const blocker = toNonEmptyText(record.blocker) ||
801
- toNonEmptyText(record.reason) ||
802
- toNonEmptyText(record.blocked_by);
803
- if (!summary || !blocker) {
804
- return null;
805
- }
806
- const impact = toNonEmptyText(record.impact) || toNonEmptyText(record.effect);
807
- const nextAction = toNonEmptyText(record.next_action) ||
808
- toNonEmptyText(record.nextAction) ||
809
- toNonEmptyText(record.next_step);
810
- const evidence = normalizeBlockedEvidence(record.evidence);
811
- return {
812
- summary: summary.slice(0, 1_000),
813
- blocker: blocker.slice(0, 1_000),
814
- ...(impact ? { impact: impact.slice(0, 1_000) } : {}),
815
- ...(nextAction ? { nextAction: nextAction.slice(0, 1_000) } : {}),
816
- evidence
817
- };
818
- }
819
- function normalizeBlockedEvidence(raw) {
820
- if (Array.isArray(raw)) {
821
- const normalized = raw
822
- .map((entry) => toNonEmptyText(entry))
823
- .filter((entry) => entry.length > 0)
824
- .map((entry) => entry.slice(0, 800));
825
- return normalized.slice(0, 8);
826
- }
827
- const single = toNonEmptyText(raw);
828
- return single ? [single.slice(0, 800)] : [];
829
- }
830
- function extractCapturedMessageText(message) {
831
- if (!message) {
832
- return '';
833
- }
834
- if (typeof message === 'string') {
835
- return message.trim();
836
- }
837
- if (typeof message !== 'object' || Array.isArray(message)) {
838
- return '';
839
- }
840
- const record = message;
841
- const contentText = extractTextFromMessageContent(record.content);
842
- if (contentText) {
843
- return contentText;
844
- }
845
- const inputText = extractTextFromMessageContent(record.input);
846
- if (inputText) {
847
- return inputText;
848
- }
849
- const outputText = extractTextFromMessageContent(record.output);
850
- if (outputText) {
851
- return outputText;
852
- }
853
- const argumentsText = toNonEmptyText(record.arguments);
854
- return argumentsText;
855
- }
856
- function extractTextFromMessageContent(content) {
857
- if (typeof content === 'string') {
858
- return content.trim();
859
- }
860
- if (!Array.isArray(content)) {
861
- return '';
862
- }
863
- const chunks = [];
864
- for (const item of content) {
865
- if (typeof item === 'string') {
866
- if (item.trim())
867
- chunks.push(item.trim());
868
- continue;
869
- }
870
- if (!item || typeof item !== 'object' || Array.isArray(item)) {
871
- continue;
872
- }
873
- const record = item;
874
- const type = toNonEmptyText(record.type).toLowerCase();
875
- if (type === 'text' || type === 'output_text' || type === 'input_text' || !type) {
876
- const text = toNonEmptyText(record.text);
877
- if (text) {
878
- chunks.push(text);
879
- }
880
- continue;
881
- }
882
- const fallbackText = toNonEmptyText(record.content) || toNonEmptyText(record.value);
883
- if (fallbackText) {
884
- chunks.push(fallbackText);
885
- }
886
- }
887
- return chunks.join('\n').trim();
888
- }
889
- function extractJsonCodeBlocks(text) {
890
- const candidates = [];
891
- const regex = /```(?:json)?\s*([\s\S]*?)```/gi;
892
- let match;
893
- while ((match = regex.exec(text)) !== null) {
894
- const body = (match[1] || '').trim();
895
- if (!body) {
896
- continue;
897
- }
898
- candidates.push(body);
899
- }
900
- return candidates;
901
- }
902
- function extractBalancedJsonObjectStrings(text) {
903
- const results = [];
904
- let start = -1;
905
- let depth = 0;
906
- let inString = false;
907
- let escaped = false;
908
- for (let idx = 0; idx < text.length; idx += 1) {
909
- const ch = text[idx];
910
- if (inString) {
911
- if (escaped) {
912
- escaped = false;
913
- }
914
- else if (ch === '\\') {
915
- escaped = true;
916
- }
917
- else if (ch === '"') {
918
- inString = false;
919
- }
920
- continue;
921
- }
922
- if (ch === '"') {
923
- inString = true;
924
- continue;
925
- }
926
- if (ch === '{') {
927
- if (depth === 0) {
928
- start = idx;
929
- }
930
- depth += 1;
931
- continue;
932
- }
933
- if (ch === '}') {
934
- if (depth <= 0) {
935
- continue;
936
- }
937
- depth -= 1;
938
- if (depth === 0 && start >= 0) {
939
- results.push(text.slice(start, idx + 1));
940
- start = -1;
941
- }
942
- }
943
- }
944
- return results;
945
- }
946
- function createBdIssueFromBlockedReport(blockedReport, context, cwdOverride) {
947
- const cwd = resolveBdWorkingDirectoryForStopMessage(cwdOverride);
948
- const title = buildBlockedIssueTitle(blockedReport);
949
- const description = buildBlockedIssueDescription(blockedReport, context);
950
- const acceptance = buildBlockedIssueAcceptance(blockedReport);
951
- try {
952
- const result = childProcess.spawnSync('bd', [
953
- '--no-db',
954
- 'create',
955
- '--json',
956
- '-t',
957
- 'bug',
958
- '-p',
959
- '0',
960
- '--title',
961
- title,
962
- '--description',
963
- description,
964
- '--acceptance',
965
- acceptance
966
- ], {
967
- cwd,
968
- encoding: 'utf8',
969
- timeout: STOP_MESSAGE_BD_CREATE_TIMEOUT_MS,
970
- maxBuffer: 1024 * 1024
971
- });
972
- if (result.error || result.status !== 0) {
973
- return null;
974
- }
975
- return parseCreatedIssueId(result.stdout);
976
- }
977
- catch {
978
- return null;
979
- }
980
- }
981
- function resolveBdWorkingDirectoryForStopMessage(cwdOverride) {
982
- const fromOverride = toNonEmptyText(cwdOverride);
983
- if (fromOverride) {
984
- return path.resolve(fromOverride);
985
- }
986
- const fromEnv = toNonEmptyText(process.env.ROUTECODEX_STOPMESSAGE_BD_WORKDIR);
987
- if (fromEnv) {
988
- return path.resolve(fromEnv);
989
- }
990
- const current = process.cwd();
991
- return findBdProjectRootForStopMessage(current) || current;
992
- }
993
- function findBdProjectRootForStopMessage(startDirectory) {
994
- let current = path.resolve(startDirectory);
995
- while (true) {
996
- const beadsDir = path.join(current, '.beads');
997
- const issuesFile = path.join(beadsDir, 'issues.jsonl');
998
- if (fs.existsSync(issuesFile) || fs.existsSync(beadsDir)) {
999
- return current;
1000
- }
1001
- const parent = path.dirname(current);
1002
- if (parent === current) {
1003
- return null;
1004
- }
1005
- current = parent;
1006
- }
1007
- }
1008
- function buildBlockedIssueTitle(report) {
1009
- const summary = report.summary.replace(/\s+/g, ' ').trim();
1010
- const title = summary.length > 96 ? `${summary.slice(0, 96)}...` : summary;
1011
- return `stopMessage blocked: ${title}`;
1012
- }
1013
- function buildBlockedIssueDescription(report, context) {
1014
- const lines = [
1015
- '自动建单来源:stop_message_auto 结构化 blocked 报告',
1016
- '',
1017
- `Summary: ${report.summary}`,
1018
- `Blocker: ${report.blocker}`,
1019
- `Impact: ${report.impact || 'n/a'}`,
1020
- `Next Action: ${report.nextAction || 'n/a'}`,
1021
- '',
1022
- 'Evidence:'
1023
- ];
1024
- if (report.evidence.length > 0) {
1025
- for (const evidence of report.evidence) {
1026
- lines.push(`- ${evidence}`);
1027
- }
1028
- }
1029
- else {
1030
- lines.push('- n/a');
1031
- }
1032
- lines.push('', 'Context:');
1033
- lines.push(`- requestId: ${context?.requestId || 'n/a'}`);
1034
- lines.push(`- sessionId: ${context?.sessionId || 'n/a'}`);
1035
- lines.push(`- createdAt: ${new Date().toISOString()}`);
1036
- return lines.join('\n').trim();
1037
- }
1038
- function buildBlockedIssueAcceptance(report) {
1039
- const summary = report.summary.replace(/\s+/g, ' ').trim();
1040
- return [
1041
- `1) 明确阻塞原因并给出可执行解法(${summary.slice(0, 80)})`,
1042
- '2) 完成至少一条可验证动作(实现/测试/配置)并记录证据'
1043
- ].join(';');
1044
- }
1045
- function parseCreatedIssueId(stdout) {
1046
- const output = typeof stdout === 'string' ? stdout.trim() : '';
1047
- if (!output) {
1048
- return null;
1049
- }
1050
- try {
1051
- const parsed = JSON.parse(output);
1052
- const id = toNonEmptyText(parsed.id);
1053
- if (id) {
1054
- return id;
1055
- }
1056
- }
1057
- catch {
1058
- // ignore parse failures and continue with regex fallback.
1059
- }
1060
- const match = output.match(/\b(?:routecodex|bd)-[A-Za-z0-9._-]+\b/);
1061
- return match ? match[0] : null;
1062
- }
1063
- function createStopMessageState(snapshot) {
1064
- return {
1065
- forcedTarget: undefined,
1066
- stickyTarget: undefined,
1067
- allowedProviders: new Set(),
1068
- disabledProviders: new Set(),
1069
- disabledKeys: new Map(),
1070
- disabledModels: new Map(),
1071
- stopMessageSource: snapshot.source && snapshot.source.trim() ? snapshot.source.trim() : 'explicit',
1072
- stopMessageText: snapshot.text,
1073
- stopMessageMaxRepeats: snapshot.maxRepeats,
1074
- stopMessageUsed: snapshot.used,
1075
- stopMessageUpdatedAt: snapshot.updatedAt,
1076
- stopMessageLastUsedAt: snapshot.lastUsedAt,
1077
- stopMessageStage: snapshot.stage,
1078
- stopMessageStageMode: snapshot.stageMode,
1079
- stopMessageObservationHash: snapshot.observationHash,
1080
- stopMessageObservationStableCount: snapshot.observationStableCount,
1081
- stopMessageBdWorkState: snapshot.bdWorkState,
1082
- stopMessageAssignedIssueId: snapshot.assignedIssueId,
1083
- stopMessageAssignedIssueSource: snapshot.assignedIssueSource,
1084
- stopMessageNoTaskSummaryUsed: snapshot.noTaskSummaryUsed
1085
- };
1086
- }
1087
- function normalizeStopMessageStageMode(value) {
1088
- if (typeof value !== 'string') {
1089
- return undefined;
1090
- }
1091
- const normalized = value.trim().toLowerCase();
1092
- if (normalized === 'on' || normalized === 'off' || normalized === 'auto') {
1093
- return normalized;
1094
- }
1095
- return undefined;
1096
- }
1097
- function resolveStopMessageMaxRepeats(value, stageMode) {
1098
- const parsed = typeof value === 'number' && Number.isFinite(value)
1099
- ? Math.floor(value)
1100
- : 0;
1101
- if (parsed > 0) {
1102
- return parsed;
1103
- }
1104
- if (stageMode === 'on' || stageMode === 'auto') {
1105
- return DEFAULT_STOP_MESSAGE_MAX_REPEATS;
1106
- }
1107
- return 0;
1108
- }
1109
- function clearStopMessageState(state, now) {
1110
- state.stopMessageText = undefined;
1111
- state.stopMessageMaxRepeats = undefined;
1112
- state.stopMessageUsed = undefined;
1113
- state.stopMessageSource = undefined;
1114
- state.stopMessageStage = undefined;
1115
- state.stopMessageStageMode = undefined;
1116
- state.stopMessageObservationHash = undefined;
1117
- state.stopMessageObservationStableCount = undefined;
1118
- state.stopMessageBdWorkState = undefined;
1119
- state.stopMessageAssignedIssueId = undefined;
1120
- state.stopMessageAssignedIssueSource = undefined;
1121
- state.stopMessageNoTaskSummaryUsed = undefined;
1122
- state.stopMessageUpdatedAt = now;
1123
- state.stopMessageLastUsedAt = now;
1124
- }
1125
- function resolveEntryEndpoint(record) {
1126
- const raw = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
1127
- ? record.entryEndpoint.trim()
1128
- : undefined;
1129
- if (raw) {
1130
- return raw;
1131
- }
1132
- const metaEntry = record.metadata && typeof record.metadata === 'object' && record.metadata.entryEndpoint;
1133
- if (typeof metaEntry === 'string' && metaEntry.trim()) {
1134
- return metaEntry.trim();
1135
- }
1136
- return '/v1/chat/completions';
1137
- }