@jsonstudio/llms 0.6.1739 → 0.6.1749

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.
@@ -172,7 +172,11 @@ export class VirtualRouterEngine {
172
172
  stopMessageMaxRepeats: sessionState.stopMessageMaxRepeats,
173
173
  stopMessageUsed: sessionState.stopMessageUsed,
174
174
  stopMessageUpdatedAt: sessionState.stopMessageUpdatedAt,
175
- stopMessageLastUsedAt: sessionState.stopMessageLastUsedAt
175
+ stopMessageLastUsedAt: sessionState.stopMessageLastUsedAt,
176
+ stopMessageStage: sessionState.stopMessageStage,
177
+ stopMessageObservationHash: sessionState.stopMessageObservationHash,
178
+ stopMessageObservationStableCount: sessionState.stopMessageObservationStableCount,
179
+ stopMessageBdWorkState: sessionState.stopMessageBdWorkState
176
180
  };
177
181
  }
178
182
  }
@@ -241,6 +245,10 @@ export class VirtualRouterEngine {
241
245
  nextSessionState.stopMessageUpdatedAt = clearedAt;
242
246
  nextSessionState.stopMessageLastUsedAt = clearedAt;
243
247
  nextSessionState.stopMessageSource = undefined;
248
+ nextSessionState.stopMessageStage = undefined;
249
+ nextSessionState.stopMessageObservationHash = undefined;
250
+ nextSessionState.stopMessageObservationStableCount = undefined;
251
+ nextSessionState.stopMessageBdWorkState = undefined;
244
252
  shouldPersistSessionState = true;
245
253
  }
246
254
  else if (hasStopMessageSet) {
@@ -267,6 +275,10 @@ export class VirtualRouterEngine {
267
275
  typeof routingState.stopMessageUpdatedAt === 'number'
268
276
  ? routingState.stopMessageUpdatedAt
269
277
  : Date.now();
278
+ nextSessionState.stopMessageStage = undefined;
279
+ nextSessionState.stopMessageObservationHash = undefined;
280
+ nextSessionState.stopMessageObservationStableCount = 0;
281
+ nextSessionState.stopMessageBdWorkState = undefined;
270
282
  nextSessionState.stopMessageLastUsedAt = undefined;
271
283
  shouldPersistSessionState = true;
272
284
  }
@@ -286,6 +298,10 @@ export class VirtualRouterEngine {
286
298
  routingState.stopMessageUsed = nextSessionState.stopMessageUsed;
287
299
  routingState.stopMessageUpdatedAt = nextSessionState.stopMessageUpdatedAt;
288
300
  routingState.stopMessageLastUsedAt = nextSessionState.stopMessageLastUsedAt;
301
+ routingState.stopMessageStage = nextSessionState.stopMessageStage;
302
+ routingState.stopMessageObservationHash = nextSessionState.stopMessageObservationHash;
303
+ routingState.stopMessageObservationStableCount = nextSessionState.stopMessageObservationStableCount;
304
+ routingState.stopMessageBdWorkState = nextSessionState.stopMessageBdWorkState;
289
305
  }
290
306
  }
291
307
  }
@@ -299,6 +315,10 @@ export class VirtualRouterEngine {
299
315
  routingState.stopMessageUsed = sessionState.stopMessageUsed;
300
316
  routingState.stopMessageUpdatedAt = sessionState.stopMessageUpdatedAt;
301
317
  routingState.stopMessageLastUsedAt = sessionState.stopMessageLastUsedAt;
318
+ routingState.stopMessageStage = sessionState.stopMessageStage;
319
+ routingState.stopMessageObservationHash = sessionState.stopMessageObservationHash;
320
+ routingState.stopMessageObservationStableCount = sessionState.stopMessageObservationStableCount;
321
+ routingState.stopMessageBdWorkState = sessionState.stopMessageBdWorkState;
302
322
  }
303
323
  }
304
324
  // Guardrail: if a session is restricted to providers that do not exist in any routing pools,
@@ -623,6 +643,19 @@ export class VirtualRouterEngine {
623
643
  ...(typeof effectiveState.stopMessageLastUsedAt === 'number' &&
624
644
  Number.isFinite(effectiveState.stopMessageLastUsedAt)
625
645
  ? { stopMessageLastUsedAt: effectiveState.stopMessageLastUsedAt }
646
+ : {}),
647
+ ...(typeof effectiveState.stopMessageStage === 'string' && effectiveState.stopMessageStage.trim()
648
+ ? { stopMessageStage: effectiveState.stopMessageStage.trim() }
649
+ : {}),
650
+ ...(typeof effectiveState.stopMessageObservationHash === 'string' && effectiveState.stopMessageObservationHash.trim()
651
+ ? { stopMessageObservationHash: effectiveState.stopMessageObservationHash.trim() }
652
+ : {}),
653
+ ...(typeof effectiveState.stopMessageObservationStableCount === 'number' &&
654
+ Number.isFinite(effectiveState.stopMessageObservationStableCount)
655
+ ? { stopMessageObservationStableCount: Math.max(0, Math.floor(effectiveState.stopMessageObservationStableCount)) }
656
+ : {}),
657
+ ...(typeof effectiveState.stopMessageBdWorkState === 'string' && effectiveState.stopMessageBdWorkState.trim()
658
+ ? { stopMessageBdWorkState: effectiveState.stopMessageBdWorkState.trim() }
626
659
  : {})
627
660
  };
628
661
  }
@@ -46,6 +46,10 @@ export interface RoutingInstructionState {
46
46
  stopMessageUsed?: number;
47
47
  stopMessageUpdatedAt?: number;
48
48
  stopMessageLastUsedAt?: number;
49
+ stopMessageStage?: string;
50
+ stopMessageObservationHash?: string;
51
+ stopMessageObservationStableCount?: number;
52
+ stopMessageBdWorkState?: string;
49
53
  }
50
54
  export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
51
55
  /**
@@ -362,11 +362,16 @@ export function applyRoutingInstructions(instructions, currentState) {
362
362
  disabledProviders: new Set(currentState.disabledProviders),
363
363
  disabledKeys: new Map(Array.from(currentState.disabledKeys.entries()).map(([k, v]) => [k, new Set(v)])),
364
364
  disabledModels: new Map(Array.from(currentState.disabledModels.entries()).map(([k, v]) => [k, new Set(v)])),
365
+ stopMessageSource: currentState.stopMessageSource,
365
366
  stopMessageText: currentState.stopMessageText,
366
367
  stopMessageMaxRepeats: currentState.stopMessageMaxRepeats,
367
368
  stopMessageUsed: currentState.stopMessageUsed,
368
369
  stopMessageUpdatedAt: currentState.stopMessageUpdatedAt,
369
- stopMessageLastUsedAt: currentState.stopMessageLastUsedAt
370
+ stopMessageLastUsedAt: currentState.stopMessageLastUsedAt,
371
+ stopMessageStage: currentState.stopMessageStage,
372
+ stopMessageObservationHash: currentState.stopMessageObservationHash,
373
+ stopMessageObservationStableCount: currentState.stopMessageObservationStableCount,
374
+ stopMessageBdWorkState: currentState.stopMessageBdWorkState
370
375
  };
371
376
  let allowReset = false;
372
377
  let disableReset = false;
@@ -516,6 +521,10 @@ export function applyRoutingInstructions(instructions, currentState) {
516
521
  newState.stopMessageUsed = 0;
517
522
  newState.stopMessageUpdatedAt = Date.now();
518
523
  newState.stopMessageLastUsedAt = undefined;
524
+ newState.stopMessageStage = undefined;
525
+ newState.stopMessageObservationHash = undefined;
526
+ newState.stopMessageObservationStableCount = 0;
527
+ newState.stopMessageBdWorkState = undefined;
519
528
  }
520
529
  }
521
530
  break;
@@ -527,6 +536,10 @@ export function applyRoutingInstructions(instructions, currentState) {
527
536
  newState.stopMessageSource = undefined;
528
537
  newState.stopMessageUpdatedAt = undefined;
529
538
  newState.stopMessageLastUsedAt = undefined;
539
+ newState.stopMessageStage = undefined;
540
+ newState.stopMessageObservationHash = undefined;
541
+ newState.stopMessageObservationStableCount = undefined;
542
+ newState.stopMessageBdWorkState = undefined;
530
543
  break;
531
544
  }
532
545
  }
@@ -586,6 +599,18 @@ export function serializeRoutingInstructionState(state) {
586
599
  : {}),
587
600
  ...(typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)
588
601
  ? { stopMessageLastUsedAt: state.stopMessageLastUsedAt }
602
+ : {}),
603
+ ...(typeof state.stopMessageStage === 'string' && state.stopMessageStage.trim()
604
+ ? { stopMessageStage: state.stopMessageStage.trim() }
605
+ : {}),
606
+ ...(typeof state.stopMessageObservationHash === 'string' && state.stopMessageObservationHash.trim()
607
+ ? { stopMessageObservationHash: state.stopMessageObservationHash.trim() }
608
+ : {}),
609
+ ...(typeof state.stopMessageObservationStableCount === 'number' && Number.isFinite(state.stopMessageObservationStableCount)
610
+ ? { stopMessageObservationStableCount: Math.max(0, Math.floor(state.stopMessageObservationStableCount)) }
611
+ : {}),
612
+ ...(typeof state.stopMessageBdWorkState === 'string' && state.stopMessageBdWorkState.trim()
613
+ ? { stopMessageBdWorkState: state.stopMessageBdWorkState.trim() }
589
614
  : {})
590
615
  };
591
616
  }
@@ -599,8 +624,12 @@ export function deserializeRoutingInstructionState(data) {
599
624
  disabledKeys: new Map(),
600
625
  disabledModels: new Map(),
601
626
  stopMessageText: undefined,
627
+ stopMessageSource: undefined,
602
628
  stopMessageMaxRepeats: undefined,
603
- stopMessageUsed: undefined
629
+ stopMessageUsed: undefined,
630
+ stopMessageStage: undefined,
631
+ stopMessageObservationHash: undefined,
632
+ stopMessageBdWorkState: undefined
604
633
  };
605
634
  if (data.forcedTarget && typeof data.forcedTarget === 'object') {
606
635
  state.forcedTarget = data.forcedTarget;
@@ -649,5 +678,17 @@ export function deserializeRoutingInstructionState(data) {
649
678
  if (typeof data.stopMessageLastUsedAt === 'number' && Number.isFinite(data.stopMessageLastUsedAt)) {
650
679
  state.stopMessageLastUsedAt = data.stopMessageLastUsedAt;
651
680
  }
681
+ if (typeof data.stopMessageStage === 'string' && data.stopMessageStage.trim()) {
682
+ state.stopMessageStage = data.stopMessageStage.trim();
683
+ }
684
+ if (typeof data.stopMessageObservationHash === 'string' && data.stopMessageObservationHash.trim()) {
685
+ state.stopMessageObservationHash = data.stopMessageObservationHash.trim();
686
+ }
687
+ if (typeof data.stopMessageObservationStableCount === 'number' && Number.isFinite(data.stopMessageObservationStableCount)) {
688
+ state.stopMessageObservationStableCount = Math.max(0, Math.floor(data.stopMessageObservationStableCount));
689
+ }
690
+ if (typeof data.stopMessageBdWorkState === 'string' && data.stopMessageBdWorkState.trim()) {
691
+ state.stopMessageBdWorkState = data.stopMessageBdWorkState.trim();
692
+ }
652
693
  return state;
653
694
  }
@@ -1,5 +1,5 @@
1
1
  import type { RoutingInstructionState } from './routing-instructions.js';
2
- type StopMessageSubset = Pick<RoutingInstructionState, 'stopMessageSource' | 'stopMessageText' | 'stopMessageMaxRepeats' | 'stopMessageUsed' | 'stopMessageUpdatedAt' | 'stopMessageLastUsedAt'>;
2
+ type StopMessageSubset = Pick<RoutingInstructionState, 'stopMessageSource' | 'stopMessageText' | 'stopMessageMaxRepeats' | 'stopMessageUsed' | 'stopMessageStage' | 'stopMessageObservationHash' | 'stopMessageObservationStableCount' | 'stopMessageBdWorkState' | 'stopMessageUpdatedAt' | 'stopMessageLastUsedAt'>;
3
3
  /**
4
4
  * Decide whether we should overwrite in-memory stopMessage fields with persisted ones.
5
5
  *
@@ -41,6 +41,10 @@ export function mergeStopMessageFromPersisted(existing, persisted) {
41
41
  stopMessageText: persisted.stopMessageText,
42
42
  stopMessageMaxRepeats: persisted.stopMessageMaxRepeats,
43
43
  stopMessageUsed: persisted.stopMessageUsed,
44
+ stopMessageStage: persisted.stopMessageStage,
45
+ stopMessageObservationHash: persisted.stopMessageObservationHash,
46
+ stopMessageObservationStableCount: persisted.stopMessageObservationStableCount,
47
+ stopMessageBdWorkState: persisted.stopMessageBdWorkState,
44
48
  stopMessageUpdatedAt: persisted.stopMessageUpdatedAt,
45
49
  stopMessageLastUsedAt: persisted.stopMessageLastUsedAt
46
50
  };
@@ -460,6 +460,10 @@ export interface StopMessageStateSnapshot {
460
460
  stopMessageUsed?: number;
461
461
  stopMessageUpdatedAt?: number;
462
462
  stopMessageLastUsedAt?: number;
463
+ stopMessageStage?: string;
464
+ stopMessageObservationHash?: string;
465
+ stopMessageObservationStableCount?: number;
466
+ stopMessageBdWorkState?: string;
463
467
  }
464
468
  export interface RoutingStatusSnapshot {
465
469
  routes: Record<string, {
@@ -2,6 +2,7 @@ import { registerServerToolHandler } from '../registry.js';
2
2
  import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } 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';
6
7
  const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
7
8
  const STOPMESSAGE_IMPLICIT_GEMINI = (process.env.ROUTECODEX_STOPMESSAGE_IMPLICIT_GEMINI || '').trim() === '1';
@@ -30,16 +31,8 @@ const handler = async (ctx) => {
30
31
  const followupFlagRaw = rt?.serverToolFollowup;
31
32
  if (followupFlagRaw === true ||
32
33
  (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) {
40
- debugLog('skip_followup_loop');
41
- return null;
42
- }
34
+ debugLog('skip_followup_loop');
35
+ return null;
43
36
  }
44
37
  if (hasCompactionFlag(rt)) {
45
38
  debugLog('skip_compaction_flag');
@@ -68,11 +61,7 @@ const handler = async (ctx) => {
68
61
  typeof state.stopMessageSource === 'string' &&
69
62
  state.stopMessageSource.trim().toLowerCase() === 'auto' &&
70
63
  !STOPMESSAGE_IMPLICIT_GEMINI) {
71
- state.stopMessageText = undefined;
72
- state.stopMessageMaxRepeats = undefined;
73
- state.stopMessageUsed = undefined;
74
- state.stopMessageUpdatedAt = undefined;
75
- state.stopMessageLastUsedAt = undefined;
64
+ clearStopMessageState(state, Date.now());
76
65
  saveRoutingInstructionStateAsync(stickyKey, state);
77
66
  debugLog('skip_auto_state_disabled', { stickyKey });
78
67
  return null;
@@ -114,14 +103,7 @@ const handler = async (ctx) => {
114
103
  maxRepeats
115
104
  });
116
105
  // 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;
106
+ clearStopMessageState(state, Date.now());
125
107
  saveRoutingInstructionStateAsync(stickyKey, state);
126
108
  return null;
127
109
  }
@@ -148,6 +130,31 @@ const handler = async (ctx) => {
148
130
  debugLog('skip_failed_build_followup', { stickyKey });
149
131
  return null;
150
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
152
+ });
153
+ return null;
154
+ }
155
+ const followupText = typeof stageDecision.followupText === 'string' && stageDecision.followupText.trim()
156
+ ? stageDecision.followupText.trim()
157
+ : text;
151
158
  // Extract assistant message for potential followup injection (no-op today; keeps compat).
152
159
  void extractAssistantMessageForFollowup(ctx.base);
153
160
  // Increment stopMessage usage counter when we decide to trigger followup.
@@ -158,12 +165,7 @@ const handler = async (ctx) => {
158
165
  // We still return the followup plan for this trigger, but clear the config
159
166
  // so the next response won't trigger again.
160
167
  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;
168
+ clearStopMessageState(state, Date.now());
167
169
  }
168
170
  saveRoutingInstructionStateAsync(stickyKey, state);
169
171
  const followupProviderKey = resolveStopMessageFollowupProviderKey({ record, runtimeMetadata: rt });
@@ -179,7 +181,7 @@ const handler = async (ctx) => {
179
181
  }
180
182
  followupOps.push({ op: 'append_assistant_message', required: false });
181
183
  followupOps.push({ op: 'ensure_standard_tools' });
182
- followupOps.push({ op: 'append_user_text', text });
184
+ followupOps.push({ op: 'append_user_text', text: followupText });
183
185
  return {
184
186
  flowId: FLOW_ID,
185
187
  finalize: async () => ({
@@ -437,13 +439,29 @@ function resolveStopMessageSnapshot(raw) {
437
439
  const source = typeof record.stopMessageSource === 'string' && record.stopMessageSource.trim()
438
440
  ? record.stopMessageSource.trim()
439
441
  : undefined;
442
+ const stage = typeof record.stopMessageStage === 'string' && record.stopMessageStage.trim()
443
+ ? record.stopMessageStage.trim()
444
+ : undefined;
445
+ const observationHash = typeof record.stopMessageObservationHash === 'string' && record.stopMessageObservationHash.trim()
446
+ ? record.stopMessageObservationHash.trim()
447
+ : undefined;
448
+ const observationStableCount = typeof record.stopMessageObservationStableCount === 'number' && Number.isFinite(record.stopMessageObservationStableCount)
449
+ ? Math.max(0, Math.floor(record.stopMessageObservationStableCount))
450
+ : undefined;
451
+ const bdWorkState = typeof record.stopMessageBdWorkState === 'string' && record.stopMessageBdWorkState.trim()
452
+ ? record.stopMessageBdWorkState.trim()
453
+ : undefined;
440
454
  return {
441
455
  text,
442
456
  maxRepeats,
443
457
  used,
444
458
  ...(source ? { source } : {}),
445
459
  ...(updatedAt ? { updatedAt } : {}),
446
- ...(lastUsedAt ? { lastUsedAt } : {})
460
+ ...(lastUsedAt ? { lastUsedAt } : {}),
461
+ ...(stage ? { stage } : {}),
462
+ ...(observationHash ? { observationHash } : {}),
463
+ ...(typeof observationStableCount === 'number' ? { observationStableCount } : {}),
464
+ ...(bdWorkState ? { bdWorkState } : {})
447
465
  };
448
466
  }
449
467
  function hasCompactionFlag(rt) {
@@ -564,9 +582,25 @@ function createStopMessageState(snapshot) {
564
582
  stopMessageMaxRepeats: snapshot.maxRepeats,
565
583
  stopMessageUsed: snapshot.used,
566
584
  stopMessageUpdatedAt: snapshot.updatedAt,
567
- stopMessageLastUsedAt: snapshot.lastUsedAt
585
+ stopMessageLastUsedAt: snapshot.lastUsedAt,
586
+ stopMessageStage: snapshot.stage,
587
+ stopMessageObservationHash: snapshot.observationHash,
588
+ stopMessageObservationStableCount: snapshot.observationStableCount,
589
+ stopMessageBdWorkState: snapshot.bdWorkState
568
590
  };
569
591
  }
592
+ function clearStopMessageState(state, now) {
593
+ state.stopMessageText = undefined;
594
+ state.stopMessageMaxRepeats = undefined;
595
+ state.stopMessageUsed = undefined;
596
+ state.stopMessageSource = undefined;
597
+ state.stopMessageStage = undefined;
598
+ state.stopMessageObservationHash = undefined;
599
+ state.stopMessageObservationStableCount = undefined;
600
+ state.stopMessageBdWorkState = undefined;
601
+ state.stopMessageUpdatedAt = now;
602
+ state.stopMessageLastUsedAt = now;
603
+ }
570
604
  function resolveEntryEndpoint(record) {
571
605
  const raw = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
572
606
  ? record.entryEndpoint.trim()
@@ -0,0 +1,22 @@
1
+ export type StopMessageStageName = 'status_probe' | 'active_continue' | 'loop_self_check';
2
+ export type StopMessageBdWorkState = 'active' | 'idle' | 'unknown';
3
+ export interface StopMessageStageStateSnapshot {
4
+ stopMessageStage?: string;
5
+ stopMessageObservationHash?: string;
6
+ stopMessageObservationStableCount?: number;
7
+ stopMessageBdWorkState?: string;
8
+ }
9
+ export interface StopMessageStageDecision {
10
+ action: 'followup' | 'stop';
11
+ stage?: StopMessageStageName;
12
+ followupText?: string;
13
+ observationHash: string;
14
+ observationStableCount: number;
15
+ bdWorkState: StopMessageBdWorkState;
16
+ stopReason?: 'loop_stable' | 'bd_idle';
17
+ }
18
+ export declare function resolveStopMessageStageDecision(args: {
19
+ baseText: string;
20
+ state: StopMessageStageStateSnapshot;
21
+ capturedMessages: unknown[];
22
+ }): StopMessageStageDecision;
@@ -0,0 +1,272 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { resolveStopMessageText } from '../../router/virtual-router/stop-message-file-resolver.js';
3
+ const DEFAULT_STATUS_TEMPLATE = [
4
+ '先做状态确认再继续执行:',
5
+ '1) 运行 `bd --no-db ready`,确认是否存在 open/in_progress 任务;',
6
+ '2) 若有当前任务,继续执行下一步并更新状态;',
7
+ '3) 若没有可推进任务,直接说明并停止。',
8
+ '',
9
+ '{{BASE_STOP_MESSAGE}}'
10
+ ].join('\n');
11
+ const DEFAULT_ACTIVE_TEMPLATE = [
12
+ '检测到当前存在进行中任务(in_progress),必须继续执行,不允许只汇报状态:',
13
+ '1) 继续当前 in_progress 任务并执行至少一个可验证动作(改代码/跑测试/更新证据);',
14
+ '2) 未完成上述动作前,不要输出“已完成/已汇报”类结论;',
15
+ '3) 完成动作后只汇报结果与下一步。',
16
+ '',
17
+ '{{BASE_STOP_MESSAGE}}'
18
+ ].join('\n');
19
+ const DEFAULT_LOOP_TEMPLATE = [
20
+ '检测到可能出现重复循环,请先自检并尝试跳出:',
21
+ '1) 对比最近两轮是否重复同一命令/同一结论;',
22
+ '2) 若重复,改用新的有效动作(更具体命令、换验证路径或直接给结论);',
23
+ '3) 执行后再观察状态变化;若仍无变化则停止并报告阻塞点。',
24
+ '',
25
+ '{{BASE_STOP_MESSAGE}}'
26
+ ].join('\n');
27
+ const STAGE_TEMPLATE_REFS = {
28
+ status_probe: '<file://stopMessage/stage-status-check.md>',
29
+ active_continue: '<file://stopMessage/stage-active-continue.md>',
30
+ loop_self_check: '<file://stopMessage/stage-loop-self-check.md>'
31
+ };
32
+ export function resolveStopMessageStageDecision(args) {
33
+ const templates = resolveStageTemplates();
34
+ const observationHash = buildObservationHash(args.capturedMessages);
35
+ if (!templates.enabled) {
36
+ return {
37
+ action: 'followup',
38
+ followupText: args.baseText,
39
+ observationHash,
40
+ observationStableCount: 0,
41
+ bdWorkState: 'unknown'
42
+ };
43
+ }
44
+ const previousHash = normalizeText(args.state.stopMessageObservationHash);
45
+ const previousStableCount = toSafeInt(args.state.stopMessageObservationStableCount, 0);
46
+ const stableCount = previousHash && previousHash === observationHash ? previousStableCount + 1 : 0;
47
+ const bdWorkState = detectBdWorkState(args.capturedMessages);
48
+ if (bdWorkState === 'idle') {
49
+ return {
50
+ action: 'stop',
51
+ observationHash,
52
+ observationStableCount: stableCount,
53
+ bdWorkState,
54
+ stopReason: 'bd_idle'
55
+ };
56
+ }
57
+ if (stableCount >= 2) {
58
+ return {
59
+ action: 'stop',
60
+ observationHash,
61
+ observationStableCount: stableCount,
62
+ bdWorkState,
63
+ stopReason: 'loop_stable'
64
+ };
65
+ }
66
+ const stage = bdWorkState === 'active'
67
+ ? 'active_continue'
68
+ : stableCount <= 0
69
+ ? 'status_probe'
70
+ : 'loop_self_check';
71
+ const stageTemplate = stage === 'status_probe'
72
+ ? templates.statusProbeTemplate
73
+ : stage === 'active_continue'
74
+ ? templates.activeContinueTemplate
75
+ : templates.loopSelfCheckTemplate;
76
+ const followupText = buildStageMessage(stageTemplate, args.baseText);
77
+ return {
78
+ action: 'followup',
79
+ stage,
80
+ followupText,
81
+ observationHash,
82
+ observationStableCount: stableCount,
83
+ bdWorkState
84
+ };
85
+ }
86
+ function buildStageMessage(template, baseText) {
87
+ const safeBaseText = baseText.trim();
88
+ if (!safeBaseText) {
89
+ return template.trim();
90
+ }
91
+ if (template.includes('{{BASE_STOP_MESSAGE}}')) {
92
+ return template.replaceAll('{{BASE_STOP_MESSAGE}}', safeBaseText).trim();
93
+ }
94
+ return `${template.trim()}\n\n原始约束:\n${safeBaseText}`.trim();
95
+ }
96
+ function resolveStageTemplates() {
97
+ const mode = resolveStageMode();
98
+ if (mode === 'off') {
99
+ return {
100
+ enabled: false,
101
+ statusProbeTemplate: '',
102
+ activeContinueTemplate: '',
103
+ loopSelfCheckTemplate: ''
104
+ };
105
+ }
106
+ const statusProbeTemplate = loadStageTemplate('status_probe');
107
+ const activeContinueTemplate = loadStageTemplate('active_continue');
108
+ const loopSelfCheckTemplate = loadStageTemplate('loop_self_check');
109
+ const hasUserTemplate = Boolean(statusProbeTemplate || activeContinueTemplate || loopSelfCheckTemplate);
110
+ const enabled = mode === 'on' || hasUserTemplate;
111
+ if (!enabled) {
112
+ return {
113
+ enabled: false,
114
+ statusProbeTemplate: '',
115
+ activeContinueTemplate: '',
116
+ loopSelfCheckTemplate: ''
117
+ };
118
+ }
119
+ return {
120
+ enabled: true,
121
+ statusProbeTemplate: statusProbeTemplate || DEFAULT_STATUS_TEMPLATE,
122
+ activeContinueTemplate: activeContinueTemplate || DEFAULT_ACTIVE_TEMPLATE,
123
+ loopSelfCheckTemplate: loopSelfCheckTemplate || DEFAULT_LOOP_TEMPLATE
124
+ };
125
+ }
126
+ function resolveStageMode() {
127
+ const raw = normalizeText(process.env.ROUTECODEX_STOPMESSAGE_STAGE_MODE).toLowerCase();
128
+ if (raw === '1' || raw === 'true' || raw === 'on') {
129
+ return 'on';
130
+ }
131
+ if (raw === '0' || raw === 'false' || raw === 'off') {
132
+ return 'off';
133
+ }
134
+ if (raw === 'auto') {
135
+ return 'auto';
136
+ }
137
+ return 'on';
138
+ }
139
+ function loadStageTemplate(stage) {
140
+ const envRef = stage === 'status_probe'
141
+ ? normalizeText(process.env.ROUTECODEX_STOPMESSAGE_STAGE_STATUS_REF)
142
+ : stage === 'active_continue'
143
+ ? normalizeText(process.env.ROUTECODEX_STOPMESSAGE_STAGE_ACTIVE_REF)
144
+ : normalizeText(process.env.ROUTECODEX_STOPMESSAGE_STAGE_LOOP_REF);
145
+ const ref = envRef || STAGE_TEMPLATE_REFS[stage];
146
+ if (!ref) {
147
+ return null;
148
+ }
149
+ try {
150
+ const resolved = resolveStopMessageText(ref);
151
+ return normalizeText(resolved) || null;
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function detectBdWorkState(messages) {
158
+ const normalizedTail = messages
159
+ .slice(-30)
160
+ .map((message) => normalizeMessageForHeuristics(message))
161
+ .filter(Boolean)
162
+ .join('\n')
163
+ .toLowerCase();
164
+ if (!normalizedTail.includes('bd')) {
165
+ return 'unknown';
166
+ }
167
+ const idlePatterns = [
168
+ /\bno\s+(ready|open|in_progress)\b/i,
169
+ /\b0\s+(ready|open|in_progress)\b/i,
170
+ /\bnothing\s+(to\s+do|ready|in\s+progress)\b/i,
171
+ /\ball\s+(tasks?|issues?)\s+(done|closed)\b/i,
172
+ /没有可(做|推进)任务/i,
173
+ /无\s*(ready|open|in_progress)\s*任务/i
174
+ ];
175
+ if (idlePatterns.some((pattern) => pattern.test(normalizedTail))) {
176
+ return 'idle';
177
+ }
178
+ const activePatterns = [
179
+ /\bin_progress\b/i,
180
+ /\bready\b/i,
181
+ /\bblocked\b/i,
182
+ /\bopen\b/i,
183
+ /\bepic status\b/i,
184
+ /\bshow\s+[a-z0-9._-]+\b/i
185
+ ];
186
+ if (activePatterns.some((pattern) => pattern.test(normalizedTail))) {
187
+ return 'active';
188
+ }
189
+ return 'unknown';
190
+ }
191
+ function buildObservationHash(messages) {
192
+ const tail = messages
193
+ .slice(-20)
194
+ .map((message) => normalizeMessageForHash(message))
195
+ .filter(Boolean)
196
+ .join('\n');
197
+ if (!tail) {
198
+ return 'empty';
199
+ }
200
+ return createHash('sha1').update(tail).digest('hex').slice(0, 16);
201
+ }
202
+ function normalizeMessageForHash(message) {
203
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
204
+ return '';
205
+ }
206
+ const record = message;
207
+ const role = normalizeText(record.role).toLowerCase() || 'unknown';
208
+ const text = extractMessageText(record.content || record.output || record.input || '');
209
+ if (!text) {
210
+ return role;
211
+ }
212
+ return `${role}:${text.slice(0, 400)}`;
213
+ }
214
+ function normalizeMessageForHeuristics(message) {
215
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
216
+ return '';
217
+ }
218
+ const record = message;
219
+ const role = normalizeText(record.role).toLowerCase();
220
+ const text = extractMessageText(record.content || record.output || record.input || '');
221
+ return `${role}:${text}`.trim();
222
+ }
223
+ function extractMessageText(value) {
224
+ if (typeof value === 'string') {
225
+ return normalizeWhitespace(value);
226
+ }
227
+ if (Array.isArray(value)) {
228
+ const chunks = [];
229
+ for (const entry of value) {
230
+ const text = extractMessageText(entry);
231
+ if (text) {
232
+ chunks.push(text);
233
+ }
234
+ }
235
+ return normalizeWhitespace(chunks.join('\n'));
236
+ }
237
+ if (!value || typeof value !== 'object') {
238
+ return '';
239
+ }
240
+ const record = value;
241
+ const candidates = [
242
+ record.text,
243
+ record.content,
244
+ record.output_text,
245
+ record.input_text,
246
+ record.arguments,
247
+ record.name,
248
+ record.result,
249
+ record.stdout,
250
+ record.stderr
251
+ ];
252
+ const chunks = [];
253
+ for (const candidate of candidates) {
254
+ const text = extractMessageText(candidate);
255
+ if (text) {
256
+ chunks.push(text);
257
+ }
258
+ }
259
+ return normalizeWhitespace(chunks.join('\n'));
260
+ }
261
+ function normalizeWhitespace(text) {
262
+ return text.replace(/\s+/g, ' ').trim();
263
+ }
264
+ function normalizeText(value) {
265
+ return typeof value === 'string' ? value.trim() : '';
266
+ }
267
+ function toSafeInt(value, fallback) {
268
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
269
+ return fallback;
270
+ }
271
+ return Math.max(0, Math.floor(value));
272
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.1739",
3
+ "version": "0.6.1749",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",