@luckydraw/cumulus 1.0.27 → 1.0.29
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/CHANGELOG.md +20 -0
- package/dist/gateway/config.d.ts +10 -0
- package/dist/gateway/config.d.ts.map +1 -1
- package/dist/gateway/config.js +10 -4
- package/dist/gateway/config.js.map +1 -1
- package/dist/gateway/daemon.d.ts +1 -0
- package/dist/gateway/daemon.d.ts.map +1 -1
- package/dist/gateway/daemon.js +4 -0
- package/dist/gateway/daemon.js.map +1 -1
- package/dist/gateway/senders.d.ts +1 -1
- package/dist/gateway/senders.d.ts.map +1 -1
- package/dist/gateway/senders.js +1 -1
- package/dist/gateway/senders.js.map +1 -1
- package/dist/gateway/server.d.ts +5 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +94 -1
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/stall-detection.d.ts +96 -0
- package/dist/gateway/stall-detection.d.ts.map +1 -0
- package/dist/gateway/stall-detection.js +264 -0
- package/dist/gateway/stall-detection.js.map +1 -0
- package/dist/gateway/static/widget.js +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stall detection (task 164; model-agnostic classifier per task 165).
|
|
3
|
+
*
|
|
4
|
+
* A thread that ends a turn on a promised-but-untaken next action ("Let me
|
|
5
|
+
* look at how X renders.") with nothing after it is not empty (task 150/156
|
|
6
|
+
* doesn't catch it) and may never have armed a `schedule_trigger` (task 156
|
|
7
|
+
* only helps if the model remembers to). This module supplies the two
|
|
8
|
+
* detection signals `server.ts` uses on the busy→idle edge:
|
|
9
|
+
*
|
|
10
|
+
* 1. `hasUncheckedTodo` — free, structural: the thread's own `<todo>` block
|
|
11
|
+
* still has an open item.
|
|
12
|
+
* 2. `classifyStall` — a cheap-model fallback for the harder case: no todo
|
|
13
|
+
* block at all, prose just trails off. Deliberately conservative — the
|
|
14
|
+
* caller must default to "no nudge" on anything it isn't sure about,
|
|
15
|
+
* since prodding a thread that finished and is waiting on the user is
|
|
16
|
+
* the worse failure (Karl, 2026-08-30).
|
|
17
|
+
*
|
|
18
|
+
* The classifier runs on WHATEVER model the operator configured
|
|
19
|
+
* (`stallClassifierModel`), not just a Claude sub-model (task 165). It routes
|
|
20
|
+
* through `resolveModelProvider` — the same single source of truth every other
|
|
21
|
+
* model in the gateway uses — and branches: a Claude CLI sub-model runs as a
|
|
22
|
+
* one-off `claude --print --model <id>` call; any other catalog model runs as a
|
|
23
|
+
* one-shot HTTP `chat()` through the provider the resolver picks. The prompt
|
|
24
|
+
* and the fail-closed contract are identical on both branches.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hasUncheckedTodo(text: string): boolean;
|
|
27
|
+
/** Last `n` sentences of a message — the classifier only needs the tail, not the whole thing. */
|
|
28
|
+
export declare function lastSentences(text: string, n?: number): string;
|
|
29
|
+
/** The single dangling sentence to quote back in the nudge message. */
|
|
30
|
+
export declare function extractDanglingSentence(text: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Everything the classifier needs to decide WHICH provider runs the configured
|
|
33
|
+
* model and HOW to reach it. Mirrors the fields `server.ts` already threads into
|
|
34
|
+
* every turn's `sendMessage`, so the classifier resolves its model exactly the way
|
|
35
|
+
* a turn does — no second notion of "how do I run a non-Claude model."
|
|
36
|
+
*/
|
|
37
|
+
export interface StallClassifierContext {
|
|
38
|
+
/** Direct-provider catalog (HF / OpenAI / custom entries). */
|
|
39
|
+
models?: Array<{
|
|
40
|
+
id: string;
|
|
41
|
+
provider?: string;
|
|
42
|
+
contextWindow?: number;
|
|
43
|
+
}>;
|
|
44
|
+
/** Claude CLI sub-model catalog (Opus / Sonnet / Haiku variants). */
|
|
45
|
+
claudeModels?: Array<{
|
|
46
|
+
id: string;
|
|
47
|
+
}>;
|
|
48
|
+
hfApiKey?: string;
|
|
49
|
+
openaiApiKey?: string;
|
|
50
|
+
customProviders?: Array<{
|
|
51
|
+
id: string;
|
|
52
|
+
baseUrl: string;
|
|
53
|
+
apiKey?: string;
|
|
54
|
+
}>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the configured stall-classifier model id against BOTH catalogs.
|
|
58
|
+
*
|
|
59
|
+
* An id naming a `claudeModels[]` entry runs as a Claude CLI sub-model; an id
|
|
60
|
+
* naming a `models[]` entry runs on that entry's direct provider (task 165). An
|
|
61
|
+
* id that names nothing in either (absent, blank, or stale after a catalog edit)
|
|
62
|
+
* falls back to `"haiku"` rather than failing closed, since the classifier is a
|
|
63
|
+
* best-effort fallback signal, not a correctness gate.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveStallClassifierModel(stallClassifierModel: string | undefined, claudeModels?: Array<{
|
|
66
|
+
id: string;
|
|
67
|
+
}>, models?: Array<{
|
|
68
|
+
id: string;
|
|
69
|
+
}>): string;
|
|
70
|
+
export interface ClassifyStallOptions {
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
/**
|
|
73
|
+
* The configured model id. When no `context` is supplied this is used directly
|
|
74
|
+
* as a Claude CLI sub-model (the task-164 behavior, preserved for callers and
|
|
75
|
+
* tests that don't care about provider routing).
|
|
76
|
+
*/
|
|
77
|
+
model?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Full provider-resolution context. When present, the configured `model` is
|
|
80
|
+
* routed through `resolveModelProvider` and run on whatever provider that
|
|
81
|
+
* names — a Claude CLI sub-model or a direct-provider HTTP model (task 165).
|
|
82
|
+
*/
|
|
83
|
+
context?: StallClassifierContext;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Cheap-model classification for the "trails off with no `<todo>` at all" case.
|
|
87
|
+
*
|
|
88
|
+
* Routes the configured model through `resolveModelProvider` and runs it on
|
|
89
|
+
* whatever provider that names: a Claude CLI sub-model as a one-off
|
|
90
|
+
* `claude --print --model <id>` call, any other catalog model as a one-shot
|
|
91
|
+
* HTTP `chat()` (task 165). Fails closed on every branch.
|
|
92
|
+
*/
|
|
93
|
+
export declare function classifyStall(text: string, opts?: ClassifyStallOptions): Promise<boolean>;
|
|
94
|
+
/** Is the last message from the thread's own `<todo>`/classifier signals a stall? */
|
|
95
|
+
export declare function isStalled(text: string, opts?: ClassifyStallOptions): Promise<boolean>;
|
|
96
|
+
//# sourceMappingURL=stall-detection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stall-detection.d.ts","sourceRoot":"","sources":["../../src/gateway/stall-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAYH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUtD;AAkCD,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,SAAI,GAAG,MAAM,CAEzD;AAED,uEAAuE;AACvE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG5D;AAeD;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,oBAAoB,EAAE,MAAM,GAAG,SAAS,EACxC,YAAY,CAAC,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,EACpC,MAAM,CAAC,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7B,MAAM,CAMR;AAID,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,sBAAsB,CAAC;CAClC;AA2HD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAqClB;AAED,qFAAqF;AACrF,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,OAAO,CAAC,OAAO,CAAC,CAG/F"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stall detection (task 164; model-agnostic classifier per task 165).
|
|
3
|
+
*
|
|
4
|
+
* A thread that ends a turn on a promised-but-untaken next action ("Let me
|
|
5
|
+
* look at how X renders.") with nothing after it is not empty (task 150/156
|
|
6
|
+
* doesn't catch it) and may never have armed a `schedule_trigger` (task 156
|
|
7
|
+
* only helps if the model remembers to). This module supplies the two
|
|
8
|
+
* detection signals `server.ts` uses on the busy→idle edge:
|
|
9
|
+
*
|
|
10
|
+
* 1. `hasUncheckedTodo` — free, structural: the thread's own `<todo>` block
|
|
11
|
+
* still has an open item.
|
|
12
|
+
* 2. `classifyStall` — a cheap-model fallback for the harder case: no todo
|
|
13
|
+
* block at all, prose just trails off. Deliberately conservative — the
|
|
14
|
+
* caller must default to "no nudge" on anything it isn't sure about,
|
|
15
|
+
* since prodding a thread that finished and is waiting on the user is
|
|
16
|
+
* the worse failure (Karl, 2026-08-30).
|
|
17
|
+
*
|
|
18
|
+
* The classifier runs on WHATEVER model the operator configured
|
|
19
|
+
* (`stallClassifierModel`), not just a Claude sub-model (task 165). It routes
|
|
20
|
+
* through `resolveModelProvider` — the same single source of truth every other
|
|
21
|
+
* model in the gateway uses — and branches: a Claude CLI sub-model runs as a
|
|
22
|
+
* one-off `claude --print --model <id>` call; any other catalog model runs as a
|
|
23
|
+
* one-shot HTTP `chat()` through the provider the resolver picks. The prompt
|
|
24
|
+
* and the fail-closed contract are identical on both branches.
|
|
25
|
+
*/
|
|
26
|
+
import { spawn } from 'child_process';
|
|
27
|
+
import { resolveModelProvider } from '../lib/gateway.js';
|
|
28
|
+
import { HuggingFaceProvider } from '../lib/huggingface-provider.js';
|
|
29
|
+
import { OpenAIResponsesProvider } from '../lib/openai-responses-provider.js';
|
|
30
|
+
/** Last (authoritative, full-replacement) `<todo>` block still has an open `[ ]` item. */
|
|
31
|
+
const TODO_BLOCK_RE = /<todo>([\s\S]*?)<\/todo>/g;
|
|
32
|
+
const UNCHECKED_ITEM_RE = /\[\s\]/;
|
|
33
|
+
export function hasUncheckedTodo(text) {
|
|
34
|
+
if (!text)
|
|
35
|
+
return false;
|
|
36
|
+
let lastBlock;
|
|
37
|
+
let match;
|
|
38
|
+
TODO_BLOCK_RE.lastIndex = 0;
|
|
39
|
+
while ((match = TODO_BLOCK_RE.exec(text))) {
|
|
40
|
+
lastBlock = match[1];
|
|
41
|
+
}
|
|
42
|
+
if (!lastBlock)
|
|
43
|
+
return false;
|
|
44
|
+
return UNCHECKED_ITEM_RE.test(lastBlock);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Rough sentence split — good enough to isolate the tail of a message.
|
|
48
|
+
*
|
|
49
|
+
* `!` and `?` always end a sentence; `.` only does when it's the end of the
|
|
50
|
+
* text or followed by whitespace and an uppercase/quote/paren — otherwise a
|
|
51
|
+
* mid-word period (`inspect.ts`, `e.g.`) would fracture the tail into
|
|
52
|
+
* fragments and hand the classifier "ts renders claim rows." instead of
|
|
53
|
+
* the whole dangling sentence.
|
|
54
|
+
*/
|
|
55
|
+
function splitSentences(text) {
|
|
56
|
+
const trimmed = text.trim();
|
|
57
|
+
if (!trimmed)
|
|
58
|
+
return [];
|
|
59
|
+
const sentences = [];
|
|
60
|
+
let start = 0;
|
|
61
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
62
|
+
const ch = trimmed[i];
|
|
63
|
+
if (ch === '!' || ch === '?') {
|
|
64
|
+
sentences.push(trimmed.slice(start, i + 1).trim());
|
|
65
|
+
start = i + 1;
|
|
66
|
+
}
|
|
67
|
+
else if (ch === '.') {
|
|
68
|
+
const rest = trimmed.slice(i + 1);
|
|
69
|
+
const boundary = rest.match(/^\s+(\S)/);
|
|
70
|
+
if (!rest || (boundary && /[A-Z"'(]/.test(boundary[1]))) {
|
|
71
|
+
sentences.push(trimmed.slice(start, i + 1).trim());
|
|
72
|
+
start = i + 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (start < trimmed.length)
|
|
77
|
+
sentences.push(trimmed.slice(start).trim());
|
|
78
|
+
return sentences.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
/** Last `n` sentences of a message — the classifier only needs the tail, not the whole thing. */
|
|
81
|
+
export function lastSentences(text, n = 6) {
|
|
82
|
+
return splitSentences(text).slice(-n).join(' ').trim();
|
|
83
|
+
}
|
|
84
|
+
/** The single dangling sentence to quote back in the nudge message. */
|
|
85
|
+
export function extractDanglingSentence(text) {
|
|
86
|
+
const sentences = splitSentences(text);
|
|
87
|
+
return sentences.length > 0 ? sentences[sentences.length - 1] : text.trim();
|
|
88
|
+
}
|
|
89
|
+
const STALL_CLASSIFIER_PROMPT = (snippet) => `You are checking whether an AI assistant's message ends mid-task — having promised an ` +
|
|
90
|
+
`immediate next action it never took — versus ending at a normal stopping point.\n\n` +
|
|
91
|
+
`Message (last portion):\n"""\n${snippet}\n"""\n\n` +
|
|
92
|
+
`Question: Does this message end with an explicit, concrete statement of an immediate next ` +
|
|
93
|
+
`action (e.g. "Let me check X", "I'll look at Y now", "Next I'll do Z") that the message does ` +
|
|
94
|
+
`NOT go on to actually take? Answer YES only if this is unambiguous and obvious. If the ` +
|
|
95
|
+
`message reads as a completed thought, a finished answer, a question back to the user, an ` +
|
|
96
|
+
`offer awaiting the user's decision, or anything you are not certain about, answer NO.\n\n` +
|
|
97
|
+
`Respond with exactly one word: YES or NO.`;
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the configured stall-classifier model id against BOTH catalogs.
|
|
100
|
+
*
|
|
101
|
+
* An id naming a `claudeModels[]` entry runs as a Claude CLI sub-model; an id
|
|
102
|
+
* naming a `models[]` entry runs on that entry's direct provider (task 165). An
|
|
103
|
+
* id that names nothing in either (absent, blank, or stale after a catalog edit)
|
|
104
|
+
* falls back to `"haiku"` rather than failing closed, since the classifier is a
|
|
105
|
+
* best-effort fallback signal, not a correctness gate.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveStallClassifierModel(stallClassifierModel, claudeModels, models) {
|
|
108
|
+
const id = stallClassifierModel?.trim();
|
|
109
|
+
if (!id)
|
|
110
|
+
return 'haiku';
|
|
111
|
+
if (claudeModels?.some(entry => entry.id === id))
|
|
112
|
+
return id;
|
|
113
|
+
if (models?.some(entry => entry.id === id))
|
|
114
|
+
return id;
|
|
115
|
+
return 'haiku';
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The Claude CLI branch: a one-off `claude --print --model <model>` call, not a
|
|
119
|
+
* full agentic turn. Fails closed: any error, non-zero exit, timeout, or
|
|
120
|
+
* non-"YES" response resolves to `false` (no nudge).
|
|
121
|
+
*
|
|
122
|
+
* The prompt goes in as PLAIN TEXT on stdin — not `--input-format stream-json`.
|
|
123
|
+
* That flag requires a matching `--output-format stream-json --verbose`, and without
|
|
124
|
+
* it the CLI errors before reaching the model (`Error: --input-format=stream-json
|
|
125
|
+
* requires output-format=stream-json.`), so the classifier would fail closed on every
|
|
126
|
+
* call and the fallback signal would be dead. Plain text on stdin is the form that
|
|
127
|
+
* actually works and sidesteps ARG_MAX for long prompts.
|
|
128
|
+
*/
|
|
129
|
+
async function classifyViaClaudeCli(text, model, timeoutMs) {
|
|
130
|
+
const snippet = lastSentences(text, 6);
|
|
131
|
+
if (!snippet)
|
|
132
|
+
return false;
|
|
133
|
+
return new Promise(resolve => {
|
|
134
|
+
const args = ['--print', '--model', model];
|
|
135
|
+
const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('CLAUDE') && key !== 'CLAUDECODE'));
|
|
136
|
+
let claude;
|
|
137
|
+
try {
|
|
138
|
+
claude = spawn('claude', args, {
|
|
139
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
140
|
+
env: cleanEnv,
|
|
141
|
+
shell: process.platform === 'win32',
|
|
142
|
+
windowsHide: true,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
resolve(false);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Plain text on stdin (see doc comment) — no JSON envelope, no --input-format.
|
|
150
|
+
claude.stdin?.write(STALL_CLASSIFIER_PROMPT(snippet));
|
|
151
|
+
claude.stdin?.end();
|
|
152
|
+
let stdout = '';
|
|
153
|
+
let settled = false;
|
|
154
|
+
const finish = (result) => {
|
|
155
|
+
if (settled)
|
|
156
|
+
return;
|
|
157
|
+
settled = true;
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
resolve(result);
|
|
160
|
+
};
|
|
161
|
+
const timer = setTimeout(() => {
|
|
162
|
+
claude.kill();
|
|
163
|
+
finish(false);
|
|
164
|
+
}, timeoutMs);
|
|
165
|
+
timer.unref?.();
|
|
166
|
+
claude.stdout?.on('data', (data) => {
|
|
167
|
+
stdout += data.toString();
|
|
168
|
+
});
|
|
169
|
+
claude.on('close', code => {
|
|
170
|
+
if (code !== 0) {
|
|
171
|
+
finish(false);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
finish(/^YES\b/i.test(stdout.trim()));
|
|
175
|
+
});
|
|
176
|
+
claude.on('error', () => finish(false));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The direct-provider branch (task 165): a one-shot HTTP `chat()` through the
|
|
181
|
+
* provider `resolveModelProvider` picked for the configured model. No tools, no
|
|
182
|
+
* system prompt beyond the classifier prompt, small `maxTokens` (the answer is
|
|
183
|
+
* one word). Fails closed exactly like the CLI branch: any throw, timeout, or
|
|
184
|
+
* non-"YES" response resolves to `false`.
|
|
185
|
+
*/
|
|
186
|
+
async function classifyViaHttp(text, modelId, resolved, timeoutMs) {
|
|
187
|
+
const snippet = lastSentences(text, 6);
|
|
188
|
+
if (!snippet)
|
|
189
|
+
return false;
|
|
190
|
+
const provider = resolved.wireProtocol === 'openai-responses'
|
|
191
|
+
? new OpenAIResponsesProvider(resolved.apiKey, resolved.baseUrl)
|
|
192
|
+
: new HuggingFaceProvider(resolved.apiKey, modelId, resolved.baseUrl);
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
195
|
+
timer.unref?.();
|
|
196
|
+
try {
|
|
197
|
+
const response = await provider.chat({
|
|
198
|
+
model: modelId,
|
|
199
|
+
system: [],
|
|
200
|
+
messages: [{ role: 'user', content: STALL_CLASSIFIER_PROMPT(snippet) }],
|
|
201
|
+
tools: [],
|
|
202
|
+
maxTokens: 16,
|
|
203
|
+
signal: controller.signal,
|
|
204
|
+
});
|
|
205
|
+
const answer = response.content.map(b => (b.type === 'text' ? b.text : '')).join('');
|
|
206
|
+
return /^YES\b/i.test(answer.trim());
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Any provider error, abort, or malformed response fails closed to "no nudge".
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Cheap-model classification for the "trails off with no `<todo>` at all" case.
|
|
218
|
+
*
|
|
219
|
+
* Routes the configured model through `resolveModelProvider` and runs it on
|
|
220
|
+
* whatever provider that names: a Claude CLI sub-model as a one-off
|
|
221
|
+
* `claude --print --model <id>` call, any other catalog model as a one-shot
|
|
222
|
+
* HTTP `chat()` (task 165). Fails closed on every branch.
|
|
223
|
+
*/
|
|
224
|
+
export async function classifyStall(text, opts = {}) {
|
|
225
|
+
const timeoutMs = opts.timeoutMs ?? 10000;
|
|
226
|
+
const ctx = opts.context;
|
|
227
|
+
// No context: preserve the task-164 behavior — the configured id (or "haiku")
|
|
228
|
+
// runs directly as a Claude CLI sub-model. This is what unit tests and any
|
|
229
|
+
// caller without a catalog rely on.
|
|
230
|
+
if (!ctx) {
|
|
231
|
+
const model = opts.model?.trim() || 'haiku';
|
|
232
|
+
return classifyViaClaudeCli(text, model, timeoutMs);
|
|
233
|
+
}
|
|
234
|
+
const modelId = resolveStallClassifierModel(opts.model, ctx.claudeModels, ctx.models);
|
|
235
|
+
// A Claude CLI sub-model runs on the CLI — detected by membership in claudeModels[],
|
|
236
|
+
// plus the "haiku" fallback (a valid CLI alias). These are NEVER passed to
|
|
237
|
+
// resolveModelProvider: that resolver's contract is "claude" or a direct-provider id,
|
|
238
|
+
// and feeding it a Claude sub-model id would fall through to the HF branch and
|
|
239
|
+
// misroute a Claude model over HTTP when an HF key is present.
|
|
240
|
+
const isClaudeSub = modelId === 'haiku' || ctx.claudeModels?.some(entry => entry.id === modelId);
|
|
241
|
+
if (isClaudeSub) {
|
|
242
|
+
return classifyViaClaudeCli(text, modelId, timeoutMs);
|
|
243
|
+
}
|
|
244
|
+
// A direct-provider model: route through the same single source of truth every turn
|
|
245
|
+
// uses (task 118/147). No second notion of "how do I run a non-Claude model."
|
|
246
|
+
const resolved = resolveModelProvider(modelId, ctx.models, {
|
|
247
|
+
hfApiKey: ctx.hfApiKey,
|
|
248
|
+
openaiApiKey: ctx.openaiApiKey,
|
|
249
|
+
customProviders: ctx.customProviders,
|
|
250
|
+
});
|
|
251
|
+
if (!resolved.isAgentic) {
|
|
252
|
+
// Gate closed (no credential for the resolved provider) — degrade to the default
|
|
253
|
+
// Claude sub-model rather than spawning a doomed HTTP loop.
|
|
254
|
+
return classifyViaClaudeCli(text, 'haiku', timeoutMs);
|
|
255
|
+
}
|
|
256
|
+
return classifyViaHttp(text, modelId, resolved, timeoutMs);
|
|
257
|
+
}
|
|
258
|
+
/** Is the last message from the thread's own `<todo>`/classifier signals a stall? */
|
|
259
|
+
export async function isStalled(text, opts = {}) {
|
|
260
|
+
if (hasUncheckedTodo(text))
|
|
261
|
+
return true;
|
|
262
|
+
return classifyStall(text, opts);
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=stall-detection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stall-detection.js","sourceRoot":"","sources":["../../src/gateway/stall-detection.ts"],"names":[],"mappings":"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"}
|
|
@@ -139,6 +139,11 @@
|
|
|
139
139
|
// and re-adding it does not quietly clear the voice setting. The gateway's
|
|
140
140
|
// resolveVoiceModel treats an unmatched id as "no override" anyway.
|
|
141
141
|
voiceModel: typeof config.voiceModel === 'string' ? config.voiceModel : '',
|
|
142
|
+
// Task 164. Same verbatim rule as voiceModel: an id naming nothing in claudeModels[]
|
|
143
|
+
// is preserved on round-trip; the gateway's resolveStallClassifierModel falls back to
|
|
144
|
+
// "haiku" for an unmatched id anyway.
|
|
145
|
+
stallClassifierModel:
|
|
146
|
+
typeof config.stallClassifierModel === 'string' ? config.stallClassifierModel : '',
|
|
142
147
|
};
|
|
143
148
|
}
|
|
144
149
|
|
|
@@ -200,6 +205,9 @@
|
|
|
200
205
|
// Task 152: '' means "same as thread" and must reach the server as an explicit
|
|
201
206
|
// null (clear), not as absent — absent would leave the stored value untouched.
|
|
202
207
|
voiceModel: state.voiceModel ? state.voiceModel : null,
|
|
208
|
+
// Task 164: '' means "haiku" and must reach the server as an explicit null (clear),
|
|
209
|
+
// not as absent — absent would leave a previously-set classifier model untouched.
|
|
210
|
+
stallClassifierModel: state.stallClassifierModel ? state.stallClassifierModel : null,
|
|
203
211
|
// Registry travels without credentials — addCredentialPatch fills those
|
|
204
212
|
// in, so an untouched key round-trips as absent and the server preserves it.
|
|
205
213
|
customProviders: (Array.isArray(state.customProviders) ? state.customProviders : []).map(
|
|
@@ -6461,6 +6469,24 @@
|
|
|
6461
6469
|
voiceCard.appendChild(voiceSelect);
|
|
6462
6470
|
body.appendChild(voiceCard);
|
|
6463
6471
|
|
|
6472
|
+
// Task 164: the stall-detection classifier is a one-off `claude --print --model <id>`
|
|
6473
|
+
// call, so it can only run on a Claude CLI sub-model — this dropdown lists ONLY the
|
|
6474
|
+
// Anthropic sub-models (not the full catalog), with "Haiku (default)" clearing the
|
|
6475
|
+
// setting. Same card shape as the voice model above.
|
|
6476
|
+
var stallCard = document.createElement('div');
|
|
6477
|
+
stallCard.className = 'cumulus-settings-default-card';
|
|
6478
|
+
var stallCardLabel = document.createElement('label');
|
|
6479
|
+
stallCardLabel.textContent = 'Stall-detection classifier uses';
|
|
6480
|
+
var stallSelect = document.createElement('select');
|
|
6481
|
+
stallSelect.className = 'cumulus-settings-select';
|
|
6482
|
+
stallSelect.setAttribute('data-testid', 'webchat-gw-stall-classifier-select');
|
|
6483
|
+
stallSelect.addEventListener('change', function () {
|
|
6484
|
+
gwState.stallClassifierModel = stallSelect.value;
|
|
6485
|
+
});
|
|
6486
|
+
stallCard.appendChild(stallCardLabel);
|
|
6487
|
+
stallCard.appendChild(stallSelect);
|
|
6488
|
+
body.appendChild(stallCard);
|
|
6489
|
+
|
|
6464
6490
|
var modelsLabel = document.createElement('div');
|
|
6465
6491
|
modelsLabel.className = 'cumulus-settings-section-label';
|
|
6466
6492
|
modelsLabel.textContent = 'Model catalog';
|
|
@@ -7084,6 +7110,22 @@
|
|
|
7084
7110
|
if (voiceSelect.value !== (gwState.voiceModel || '')) voiceSelect.value = '';
|
|
7085
7111
|
}
|
|
7086
7112
|
|
|
7113
|
+
// Task 165: the classifier runs on ANY catalog model (a Claude CLI sub-model or a
|
|
7114
|
+
// direct-provider model), so this lists the FULL catalog grouped by provider — same
|
|
7115
|
+
// shape as the voice selector — plus a leading "Haiku (default)" clear option. A
|
|
7116
|
+
// stored id naming nothing in either catalog falls back to "Haiku (default)" in the
|
|
7117
|
+
// UI — which is what the gateway does with it too (resolveStallClassifierModel → "haiku").
|
|
7118
|
+
function renderStallSelect() {
|
|
7119
|
+
stallSelect.innerHTML = '';
|
|
7120
|
+
var haiku = document.createElement('option');
|
|
7121
|
+
haiku.value = '';
|
|
7122
|
+
haiku.textContent = 'Haiku (default)';
|
|
7123
|
+
stallSelect.appendChild(haiku);
|
|
7124
|
+
fillModelOptions(stallSelect);
|
|
7125
|
+
stallSelect.value = gwState.stallClassifierModel || '';
|
|
7126
|
+
if (stallSelect.value !== (gwState.stallClassifierModel || '')) stallSelect.value = '';
|
|
7127
|
+
}
|
|
7128
|
+
|
|
7087
7129
|
// Rebuild the provider list from state before anything reads it, so a
|
|
7088
7130
|
// provider added or removed this session reaches the cards, the default
|
|
7089
7131
|
// dropdown and the add-model select in the same pass (task 147).
|
|
@@ -7107,6 +7149,7 @@
|
|
|
7107
7149
|
renderAddProviderOptions();
|
|
7108
7150
|
renderDefaultSelect();
|
|
7109
7151
|
renderVoiceSelect();
|
|
7152
|
+
renderStallSelect();
|
|
7110
7153
|
renderGwModels();
|
|
7111
7154
|
}
|
|
7112
7155
|
|