@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.
- package/dist/router/virtual-router/engine.js +34 -1
- package/dist/router/virtual-router/routing-instructions.d.ts +4 -0
- package/dist/router/virtual-router/routing-instructions.js +43 -2
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
- package/dist/router/virtual-router/stop-message-state-sync.js +4 -0
- package/dist/router/virtual-router/types.d.ts +4 -0
- package/dist/servertool/handlers/stop-message-auto.js +66 -32
- package/dist/servertool/handlers/stop-message-stage-policy.d.ts +22 -0
- package/dist/servertool/handlers/stop-message-stage-policy.js +272 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
34
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|