@goodandready/dsh-cron 0.2.5 → 0.2.6
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/README.md +18 -6
- package/docs/README.ru.md +21 -5
- package/docs/README.zh.md +18 -6
- package/docs/design/DESIGN.md +6 -4
- package/docs/plans/0.2.6-economy-block.md +61 -0
- package/lib/api.js +399 -0
- package/lib/client.js +154 -4
- package/lib/failure-inspector.js +107 -0
- package/lib/http-utils.js +24 -0
- package/lib/index.js +121 -388
- package/lib/llm-ask.js +141 -0
- package/lib/recipes.js +238 -0
- package/lib/runner.js +210 -171
- package/lib/scheduler.js +226 -80
- package/lib/silent-rule.js +100 -0
- package/lib/store.js +11 -0
- package/lib/task-patch.js +81 -0
- package/lib/task-transfer.js +4 -0
- package/lib/templates.js +4 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -36,6 +36,7 @@ window.__ModuleLoader__.load({
|
|
|
36
36
|
const ICON_EDIT = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
|
|
37
37
|
const ICON_SEND = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
|
|
38
38
|
const ICON_COPY = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
|
39
|
+
const ICON_TUNE = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>';
|
|
39
40
|
const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>';
|
|
40
41
|
const ICON_UPLOAD = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>';
|
|
41
42
|
const ICON_TELEGRAM = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8-1.7 8.01c-.13.58-.47.72-.95.45l-2.6-1.92-1.25 1.21c-.14.14-.26.26-.53.26l.19-2.64 4.81-4.35c.21-.19-.05-.29-.32-.11L8.34 13.5 5.78 12.7c-.56-.17-.57-.56.12-.83l10-3.86c.46-.17.87.11.74.79z"/></svg>';
|
|
@@ -60,6 +61,8 @@ window.__ModuleLoader__.load({
|
|
|
60
61
|
'actions.runNowShort': 'Run',
|
|
61
62
|
'actions.edit': 'Edit / Details',
|
|
62
63
|
'actions.duplicate': 'Duplicate task',
|
|
64
|
+
'actions.tuneWithDsh': 'Tune with DSH',
|
|
65
|
+
'tune.startFailed': 'Could not start the tuning dialogue: {error}',
|
|
63
66
|
'actions.delete': 'Delete task',
|
|
64
67
|
'duplicate.done': 'Copy created and paused: review its schedule before resuming it.',
|
|
65
68
|
'duplicate.oneShotHint': 'The copy is one-shot and paused — its original time has likely passed, so set a new time before resuming.',
|
|
@@ -107,6 +110,7 @@ window.__ModuleLoader__.load({
|
|
|
107
110
|
'list.oneShot': 'One-shot',
|
|
108
111
|
'list.next': ' · next: {time}',
|
|
109
112
|
'recs.title': 'Recommended tasks',
|
|
113
|
+
'recs.hint': 'Ready-to-use recipes. Clicking one opens the form with its fields filled in — nothing is created until you save.',
|
|
110
114
|
'modal.editTitle': 'Edit task',
|
|
111
115
|
'modal.newTitle': 'New scheduled task',
|
|
112
116
|
'modal.statusActive': '● Active',
|
|
@@ -170,6 +174,18 @@ window.__ModuleLoader__.load({
|
|
|
170
174
|
'form.providerDefault': 'Default (DSH)',
|
|
171
175
|
'form.modelLabel': 'LLM model',
|
|
172
176
|
'form.modelDefault': 'Default',
|
|
177
|
+
'form.silentRuleLabel': 'Silent rule (optional)',
|
|
178
|
+
'form.silentRulePlaceholder': 'Stay silent when no filesystem is above 80%',
|
|
179
|
+
'form.silentRuleHint': 'For tasks with output: a cheap model judges the run against this rule and the report is skipped when it says to stay silent. If the model is unavailable or unsure, the report is delivered.',
|
|
180
|
+
'history.silentSkip': 'stayed silent:',
|
|
181
|
+
'history.diagnosis': 'Diagnosis',
|
|
182
|
+
'history.applySuggestion': 'Open the edit form with this suggestion',
|
|
183
|
+
'form.inspectOnFailureLabel': 'Diagnose failures with a model',
|
|
184
|
+
'form.inspectOnFailureHint': 'After a failed run a model reads the prompt, the error and the output, and the diagnosis with a proposed prompt change is stored with the run. Nothing is applied automatically.',
|
|
185
|
+
'settings.inspectorModelLabel': 'Model for failure diagnosis',
|
|
186
|
+
'form.fallbackModelLabel': 'Fallback model (optional)',
|
|
187
|
+
'form.fallbackModelPlaceholder': 'e.g. deepseek-reasoner',
|
|
188
|
+
'form.fallbackModelHint': 'If the run fails on the model above, it is retried once on this model. Both attempts are counted in the cost.',
|
|
173
189
|
'form.promptLabelScript': 'Shell command or script',
|
|
174
190
|
'form.promptLabelLlm': 'Instruction / Prompt for the agent',
|
|
175
191
|
'form.promptPlaceholderScript': 'curl -fsSL https://... || exit 1',
|
|
@@ -195,7 +211,7 @@ window.__ModuleLoader__.load({
|
|
|
195
211
|
'form.channelsHint': 'Pick the channels for this task. An explicit selection overrides the Telegram switch above; leave everything unchecked to fall back to it.',
|
|
196
212
|
'form.templateLabel': 'Message template (optional)',
|
|
197
213
|
'form.templatePlaceholder': '⏰ {title} — {status} in {duration}',
|
|
198
|
-
'form.templateHint': 'Variables: {title} {id} {status} {output} {error} {duration} {schedule} {time} {tokens} {cost}. Empty = built-in text.',
|
|
214
|
+
'form.templateHint': 'Variables: {title} {id} {status} {output} {error} {duration} {schedule} {time} {tokens} {cost} {model}. Empty = built-in text.',
|
|
199
215
|
'form.channelTelegram': 'Telegram',
|
|
200
216
|
'form.channelKanban': 'dsh-kanban card',
|
|
201
217
|
'form.channelDiscord': 'Discord',
|
|
@@ -229,6 +245,9 @@ window.__ModuleLoader__.load({
|
|
|
229
245
|
'settings.templateLabel': 'Default message template',
|
|
230
246
|
'settings.templateHint': 'Applies to every channel without its own template. Empty = built-in text.',
|
|
231
247
|
'settings.deliveryTimeoutMsLabel': 'Delivery timeout per channel (ms)',
|
|
248
|
+
'settings.automationSection': 'Automation models',
|
|
249
|
+
'settings.automationHint': 'Models used for the small helper calls (rule evaluation, failure analysis). Leave empty to use the task model.',
|
|
250
|
+
'settings.silentRuleModelLabel': 'Model for silent rules',
|
|
232
251
|
'settings.secretsOnCard': 'Credentials referenced from the settings panel are shown masked and stored by name.',
|
|
233
252
|
'form.nextRunLabel': 'Next run: ',
|
|
234
253
|
'form.notScheduled': 'not scheduled',
|
|
@@ -365,6 +384,8 @@ window.__ModuleLoader__.load({
|
|
|
365
384
|
{ key: 'giteaBaseUrl', labelKey: 'settings.giteaBaseUrlLabel', group: 'channels' },
|
|
366
385
|
{ key: 'giteaRepo', labelKey: 'settings.giteaRepoLabel', group: 'channels' },
|
|
367
386
|
{ key: 'deliveryTimeoutMs', labelKey: 'settings.deliveryTimeoutMsLabel', group: 'channels', numeric: true, defaultValue: 15000, min: 1000 },
|
|
387
|
+
{ key: 'silentRuleModel', labelKey: 'settings.silentRuleModelLabel', group: 'automation' },
|
|
388
|
+
{ key: 'inspectorModel', labelKey: 'settings.inspectorModelLabel', group: 'automation' },
|
|
368
389
|
];
|
|
369
390
|
const DELIVERY_SETTING_KEYS = DELIVERY_SETTING_FIELDS.map((f) => f.key);
|
|
370
391
|
// The global template plus the per-channel override map (server model).
|
|
@@ -436,6 +457,12 @@ window.__ModuleLoader__.load({
|
|
|
436
457
|
...DELIVERY_SETTING_FIELDS.filter((f) => f.group === 'channels').map(field)
|
|
437
458
|
),
|
|
438
459
|
]),
|
|
460
|
+
section('automation', 'settings.automationSection', [
|
|
461
|
+
React.createElement('div', { className: 'dsh-cron-hint', key: 'hint', style: { marginBottom: '10px' } }, T('settings.automationHint')),
|
|
462
|
+
React.createElement('div', { className: 'dsh-cron-form-row', key: 'row' },
|
|
463
|
+
...DELIVERY_SETTING_FIELDS.filter((f) => f.group === 'automation').map(field)
|
|
464
|
+
),
|
|
465
|
+
]),
|
|
439
466
|
section('templates', 'settings.templatesSection', [
|
|
440
467
|
React.createElement('div', { className: 'dsh-cron-form-group', key: 'global', style: { marginBottom: '10px' } },
|
|
441
468
|
React.createElement('label', null, T('settings.templateLabel')),
|
|
@@ -815,6 +842,9 @@ window.__ModuleLoader__.load({
|
|
|
815
842
|
const [formPrompt, setFormPrompt] = React.useState('');
|
|
816
843
|
const [formProvider, setFormProvider] = React.useState('');
|
|
817
844
|
const [formModel, setFormModel] = React.useState('');
|
|
845
|
+
const [formFallbackModel, setFormFallbackModel] = React.useState('');
|
|
846
|
+
const [formSilentRule, setFormSilentRule] = React.useState('');
|
|
847
|
+
const [formInspectOnFailure, setFormInspectOnFailure] = React.useState(false);
|
|
818
848
|
const [formDelivery, setFormDelivery] = React.useState('isolated');
|
|
819
849
|
const [selectedTaskMeta, setSelectedTaskMeta] = React.useState(null);
|
|
820
850
|
const [modalTab, setModalTab] = React.useState('params');
|
|
@@ -1138,6 +1168,40 @@ window.__ModuleLoader__.load({
|
|
|
1138
1168
|
}
|
|
1139
1169
|
};
|
|
1140
1170
|
|
|
1171
|
+
/** #49: tune an existing task in a dialogue instead of editing fields. */
|
|
1172
|
+
const handleTuneWithDsh = async (task) => {
|
|
1173
|
+
const lines = [
|
|
1174
|
+
`Tune the existing scheduled task "${task.title}" (id: ${task.id}).`,
|
|
1175
|
+
'Its current configuration is:',
|
|
1176
|
+
`- type: ${task.type || 'llm'}`,
|
|
1177
|
+
`- schedule: ${task.scheduleText || task.schedule} (${task.schedule})`,
|
|
1178
|
+
`- status: ${task.status}`,
|
|
1179
|
+
task.model ? `- model: ${task.model}${task.fallbackModel ? `, fallback: ${task.fallbackModel}` : ''}` : null,
|
|
1180
|
+
Array.isArray(task.channels) && task.channels.length ? `- channels: ${task.channels.join(', ')}` : null,
|
|
1181
|
+
`- prompt: ${String(task.prompt || '').slice(0, 400)}`,
|
|
1182
|
+
'',
|
|
1183
|
+
'Ask what should change, then apply it with the cron_update_task tool (read the current state again with cron_get_task if you need the full configuration).',
|
|
1184
|
+
'If the change makes the task execute code (script/node/python/ssh/docker), confirm it with me explicitly before applying it.',
|
|
1185
|
+
].filter(Boolean);
|
|
1186
|
+
try {
|
|
1187
|
+
const res = await fetch('/dsh-cron/chat/start', {
|
|
1188
|
+
method: 'POST',
|
|
1189
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1190
|
+
body: JSON.stringify({ prompt: lines.join(String.fromCharCode(10)) })
|
|
1191
|
+
});
|
|
1192
|
+
const data = await res.json();
|
|
1193
|
+
if (!data.ok || !data.sessionId) {
|
|
1194
|
+
alert(T_KEY(t, 'tune.startFailed', { error: (data && data.error) || '' }));
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (onClose) onClose();
|
|
1198
|
+
else toggle.set(false);
|
|
1199
|
+
await openSession(ctx, data.sessionId);
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
alert(T_KEY(t, 'tune.startFailed', { error: err.message }));
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1141
1205
|
const handleDelete = async (id) => {
|
|
1142
1206
|
if (!confirm(T_KEY(t, 'confirm.delete'))) return;
|
|
1143
1207
|
try {
|
|
@@ -1272,10 +1336,21 @@ window.__ModuleLoader__.load({
|
|
|
1272
1336
|
setFormPythonPath('');
|
|
1273
1337
|
setFormSkillName('');
|
|
1274
1338
|
setFormWorkflowName('');
|
|
1339
|
+
// Reset before the preset branch, so a recipe keeps what it sets.
|
|
1340
|
+
setFormFallbackModel('');
|
|
1341
|
+
setFormSilentRule('');
|
|
1342
|
+
setFormInspectOnFailure(false);
|
|
1275
1343
|
if (preset) {
|
|
1276
1344
|
setFormTitle(preset.title || '');
|
|
1277
1345
|
setFormSchedule(preset.schedule || '');
|
|
1278
1346
|
setFormPrompt(preset.prompt || '');
|
|
1347
|
+
// #48: a recipe preset also carries its runtime and delivery shape.
|
|
1348
|
+
if (preset.type) setFormType(preset.type);
|
|
1349
|
+
if (Array.isArray(preset.channels) && preset.channels.length) setFormChannels(preset.channels.filter((c) => DELIVERY_CHANNELS.includes(c)));
|
|
1350
|
+
if (preset.silentRule !== undefined) setFormSilentRule(preset.silentRule || '');
|
|
1351
|
+
if (preset.fallbackModel !== undefined) setFormFallbackModel(preset.fallbackModel || '');
|
|
1352
|
+
if (preset.inspectOnFailure !== undefined) setFormInspectOnFailure(Boolean(preset.inspectOnFailure));
|
|
1353
|
+
setFormOnlyOnFailure(Boolean(preset.onlyOnFailure));
|
|
1279
1354
|
setFormProvider(defaultModelInfo?.provider || '');
|
|
1280
1355
|
setFormModel(defaultModelInfo?.model || '');
|
|
1281
1356
|
if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
|
|
@@ -1305,7 +1380,7 @@ window.__ModuleLoader__.load({
|
|
|
1305
1380
|
}
|
|
1306
1381
|
};
|
|
1307
1382
|
|
|
1308
|
-
const openEditModal = (task) => {
|
|
1383
|
+
const openEditModal = (task, overrides = {}) => {
|
|
1309
1384
|
setDropdownOpen(false);
|
|
1310
1385
|
setFormId(task.id);
|
|
1311
1386
|
setSelectedTaskMeta(task);
|
|
@@ -1313,9 +1388,12 @@ window.__ModuleLoader__.load({
|
|
|
1313
1388
|
setFormTitle(task.title || '');
|
|
1314
1389
|
setFormSchedule(task.schedule || '');
|
|
1315
1390
|
setFormType(task.type || 'llm');
|
|
1316
|
-
setFormPrompt(task.prompt || '');
|
|
1391
|
+
setFormPrompt(overrides.prompt !== undefined ? overrides.prompt : (task.prompt || ''));
|
|
1317
1392
|
setFormProvider(task.provider || '');
|
|
1318
1393
|
setFormModel(task.model || '');
|
|
1394
|
+
setFormFallbackModel(task.fallbackModel || '');
|
|
1395
|
+
setFormSilentRule(task.silentRule || '');
|
|
1396
|
+
setFormInspectOnFailure(Boolean(task.inspectOnFailure));
|
|
1319
1397
|
setFormDelivery(task.delivery || 'isolated');
|
|
1320
1398
|
setFormNotifyTelegram(Boolean(task.notifyTelegram));
|
|
1321
1399
|
setFormChannels(Array.isArray(task.channels) ? task.channels.filter((c) => DELIVERY_CHANNELS.includes(c)) : []);
|
|
@@ -1372,6 +1450,9 @@ window.__ModuleLoader__.load({
|
|
|
1372
1450
|
delivery: formDelivery || 'isolated',
|
|
1373
1451
|
provider: isAgentType ? (formProvider || undefined) : undefined,
|
|
1374
1452
|
model: isAgentType ? (formModel || undefined) : undefined,
|
|
1453
|
+
fallbackModel: isAgentType ? (formFallbackModel.trim() || undefined) : undefined,
|
|
1454
|
+
silentRule: isAgentType ? undefined : formSilentRule.trim(),
|
|
1455
|
+
inspectOnFailure: isAgentType ? Boolean(formInspectOnFailure) : false,
|
|
1375
1456
|
notifyTelegram: formNotifyTelegram,
|
|
1376
1457
|
channels: formChannels,
|
|
1377
1458
|
template: formTemplate.trim(),
|
|
@@ -1715,6 +1796,13 @@ window.__ModuleLoader__.load({
|
|
|
1715
1796
|
'aria-label': T_KEY(t, 'actions.edit'),
|
|
1716
1797
|
onClick: () => openEditModal(task)
|
|
1717
1798
|
}, React.createElement('span', svgProps(ICON_EDIT))),
|
|
1799
|
+
React.createElement('button', {
|
|
1800
|
+
type: 'button',
|
|
1801
|
+
className: 'dsh-cron-icon-btn',
|
|
1802
|
+
title: T_KEY(t, 'actions.tuneWithDsh'),
|
|
1803
|
+
'aria-label': T_KEY(t, 'actions.tuneWithDsh'),
|
|
1804
|
+
onClick: () => handleTuneWithDsh(task)
|
|
1805
|
+
}, React.createElement('span', svgProps(ICON_TUNE))),
|
|
1718
1806
|
React.createElement('button', {
|
|
1719
1807
|
type: 'button',
|
|
1720
1808
|
className: 'dsh-cron-icon-btn',
|
|
@@ -1734,6 +1822,7 @@ window.__ModuleLoader__.load({
|
|
|
1734
1822
|
)
|
|
1735
1823
|
),
|
|
1736
1824
|
React.createElement('div', { className: 'dsh-cron-recs-title' }, T_KEY(t, 'recs.title')),
|
|
1825
|
+
React.createElement('div', { className: 'dsh-cron-hint', style: { marginBottom: '8px' } }, T_KEY(t, 'recs.hint')),
|
|
1737
1826
|
React.createElement('div', { className: 'dsh-cron-recs-list' },
|
|
1738
1827
|
recs.map((rec) =>
|
|
1739
1828
|
React.createElement('div', {
|
|
@@ -1820,6 +1909,35 @@ window.__ModuleLoader__.load({
|
|
|
1820
1909
|
React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary, #888)', fontSize: '11.5px' } }, (run.durationMs || 0) + ' ms')
|
|
1821
1910
|
)
|
|
1822
1911
|
),
|
|
1912
|
+
// #43: a diagnosed failure shows what went wrong and
|
|
1913
|
+
// offers to prefill the prompt with the suggestion.
|
|
1914
|
+
run.diagnosis && React.createElement('div', {
|
|
1915
|
+
className: 'dsh-cron-status-banner',
|
|
1916
|
+
'data-type': 'info',
|
|
1917
|
+
style: { marginTop: '8px', display: 'flex', flexDirection: 'column', gap: '6px' }
|
|
1918
|
+
},
|
|
1919
|
+
React.createElement('div', null,
|
|
1920
|
+
React.createElement('strong', null, T_KEY(t, 'history.diagnosis')),
|
|
1921
|
+
run.confidence ? ` (${run.confidence})` : ''
|
|
1922
|
+
),
|
|
1923
|
+
React.createElement('div', null, run.diagnosis),
|
|
1924
|
+
run.suggestion && React.createElement('div', null,
|
|
1925
|
+
React.createElement('button', {
|
|
1926
|
+
type: 'button',
|
|
1927
|
+
className: 'dsh-cron-btn-secondary',
|
|
1928
|
+
style: { padding: '2px 8px', fontSize: '12px' },
|
|
1929
|
+
onClick: () => openEditModal(selectedTaskMeta, { prompt: run.suggestion })
|
|
1930
|
+
}, T_KEY(t, 'history.applySuggestion'))
|
|
1931
|
+
)
|
|
1932
|
+
),
|
|
1933
|
+
// #44: a run suppressed by its silent rule says why.
|
|
1934
|
+
run.silentSkip && React.createElement('div', {
|
|
1935
|
+
className: 'dsh-cron-hint',
|
|
1936
|
+
style: { display: 'flex', gap: '6px', alignItems: 'center' }
|
|
1937
|
+
},
|
|
1938
|
+
React.createElement('span', { className: 'dsh-cron-badge-ref' }, T_KEY(t, 'history.silentSkip')),
|
|
1939
|
+
React.createElement('span', null, run.silentReason || '')
|
|
1940
|
+
),
|
|
1823
1941
|
run.output && React.createElement('div', { className: 'dsh-cron-history-output' }, run.output),
|
|
1824
1942
|
run.error && React.createElement('div', { className: 'dsh-cron-history-output', style: { color: 'var(--dsh-cron-danger)', borderColor: 'var(--dsh-cron-danger-bg)' } }, run.error),
|
|
1825
1943
|
run.sessionId && React.createElement('button', {
|
|
@@ -1982,7 +2100,28 @@ window.__ModuleLoader__.load({
|
|
|
1982
2100
|
React.createElement('option', { value: '' }, T_KEY(t, 'form.modelDefault')),
|
|
1983
2101
|
models.map((m) => React.createElement('option', { key: m.id, value: m.id }, m.name || m.id))
|
|
1984
2102
|
)
|
|
1985
|
-
)
|
|
2103
|
+
),
|
|
2104
|
+
// #45: one retry on a stronger model when the cheap one fails.
|
|
2105
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2106
|
+
React.createElement('label', null, T_KEY(t, 'form.fallbackModelLabel')),
|
|
2107
|
+
React.createElement('input', {
|
|
2108
|
+
type: 'text',
|
|
2109
|
+
value: formFallbackModel,
|
|
2110
|
+
placeholder: T_KEY(t, 'form.fallbackModelPlaceholder'),
|
|
2111
|
+
onChange: (e) => setFormFallbackModel(e.target.value)
|
|
2112
|
+
}),
|
|
2113
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.fallbackModelHint'))
|
|
2114
|
+
),
|
|
2115
|
+
// #43: diagnose failures with a model.
|
|
2116
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginTop: '4px' } },
|
|
2117
|
+
React.createElement('input', {
|
|
2118
|
+
type: 'checkbox',
|
|
2119
|
+
checked: formInspectOnFailure,
|
|
2120
|
+
onChange: (e) => setFormInspectOnFailure(e.target.checked)
|
|
2121
|
+
}),
|
|
2122
|
+
React.createElement('span', null, T_KEY(t, 'form.inspectOnFailureLabel'))
|
|
2123
|
+
),
|
|
2124
|
+
formInspectOnFailure && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.inspectOnFailureHint'))
|
|
1986
2125
|
),
|
|
1987
2126
|
formType === 'http' && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1988
2127
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
@@ -2204,6 +2343,17 @@ window.__ModuleLoader__.load({
|
|
|
2204
2343
|
onChange: (e) => setFormTemplate(e.target.value)
|
|
2205
2344
|
}),
|
|
2206
2345
|
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.templateHint'))
|
|
2346
|
+
),
|
|
2347
|
+
// #44: output-producing runtimes may stay silent by rule.
|
|
2348
|
+
!AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-group', style: { marginTop: '12px', marginBottom: 0 } },
|
|
2349
|
+
React.createElement('label', null, T_KEY(t, 'form.silentRuleLabel')),
|
|
2350
|
+
React.createElement('input', {
|
|
2351
|
+
type: 'text',
|
|
2352
|
+
value: formSilentRule,
|
|
2353
|
+
placeholder: T_KEY(t, 'form.silentRulePlaceholder'),
|
|
2354
|
+
onChange: (e) => setFormSilentRule(e.target.value)
|
|
2355
|
+
}),
|
|
2356
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.silentRuleHint'))
|
|
2207
2357
|
)
|
|
2208
2358
|
),
|
|
2209
2359
|
selectedTaskMeta && React.createElement('div', {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure inspector (#43): after a run fails, ask the model what went wrong and
|
|
3
|
+
* what to change in the task.
|
|
4
|
+
*
|
|
5
|
+
* The value is speed: instead of reading a stack trace or a wall of logs, the
|
|
6
|
+
* history entry carries a short diagnosis and a proposed prompt change the user
|
|
7
|
+
* can apply with one click. Nothing is applied automatically.
|
|
8
|
+
*
|
|
9
|
+
* Fail-open like the silent rule: no flag, no model, a failed call or an
|
|
10
|
+
* unreadable answer leave the run exactly as it was.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { askModel, parseJsonAnswer, resolveAskTarget } from './llm-ask.js';
|
|
14
|
+
|
|
15
|
+
/** Agent-mediated tasks have a prompt worth revising. */
|
|
16
|
+
export const INSPECTABLE_TASK_TYPES = ['llm', 'skill', 'workflow'];
|
|
17
|
+
|
|
18
|
+
export function supportsInspection(task) {
|
|
19
|
+
const type = String((task && task.type) || 'llm');
|
|
20
|
+
return INSPECTABLE_TASK_TYPES.includes(type);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SYSTEM_PROMPT = [
|
|
24
|
+
'You diagnose a failed scheduled agent task.',
|
|
25
|
+
'Answer with a single JSON object and nothing else:',
|
|
26
|
+
'{"diagnosis": "what went wrong, in one or two sentences", "suggestion": "a concrete improved prompt or configuration change", "confidence": "low"|"medium"|"high"}',
|
|
27
|
+
'Be specific and short. If the cause is unclear from the material, say so and set confidence to low.',
|
|
28
|
+
].join(' ');
|
|
29
|
+
|
|
30
|
+
export function buildInspectPrompt(task, runInfo) {
|
|
31
|
+
const error = String((runInfo && runInfo.error) || '').slice(0, 2000);
|
|
32
|
+
const output = String((runInfo && runInfo.output) || '').slice(0, 2000);
|
|
33
|
+
return [
|
|
34
|
+
`Task: ${task.title || 'scheduled task'} (type: ${task.type || 'llm'})`,
|
|
35
|
+
`Schedule: ${task.scheduleText || task.schedule || ''}`,
|
|
36
|
+
task.model ? `Model: ${task.model}` : '',
|
|
37
|
+
'',
|
|
38
|
+
'Current prompt:',
|
|
39
|
+
'```',
|
|
40
|
+
String(task.prompt || '').slice(0, 2000),
|
|
41
|
+
'```',
|
|
42
|
+
'',
|
|
43
|
+
`Result: ${(runInfo && runInfo.status) || 'error'}`,
|
|
44
|
+
'Error:',
|
|
45
|
+
'```',
|
|
46
|
+
error || '(no error text)',
|
|
47
|
+
'```',
|
|
48
|
+
output ? 'Last output:' : '',
|
|
49
|
+
output ? '```' : '',
|
|
50
|
+
output,
|
|
51
|
+
output ? '```' : '',
|
|
52
|
+
].filter((line) => line !== '').join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Read a diagnosis out of a model answer; anything unusable is rejected. */
|
|
56
|
+
export function readDiagnosis(text) {
|
|
57
|
+
const parsed = parseJsonAnswer(text);
|
|
58
|
+
if (!parsed || !parsed.diagnosis) return { ok: false, error: 'the model did not answer with a diagnosis' };
|
|
59
|
+
const confidence = ['low', 'medium', 'high'].includes(String(parsed.confidence || '').toLowerCase())
|
|
60
|
+
? String(parsed.confidence).toLowerCase()
|
|
61
|
+
: 'low';
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
diagnosis: String(parsed.diagnosis).slice(0, 1000),
|
|
65
|
+
suggestion: String(parsed.suggestion || '').slice(0, 2000),
|
|
66
|
+
confidence,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Inspect a failed run. Returns `{ inspected: true, diagnosis, suggestion,
|
|
72
|
+
* confidence }` or `{ inspected: false, error }` — never throws.
|
|
73
|
+
*/
|
|
74
|
+
export async function inspectFailure({ task, runInfo, ask, preferredModel, timeoutMs }) {
|
|
75
|
+
// Order matters: an inspection only ever concerns failed runs, so a healthy
|
|
76
|
+
// run must not produce a "no model available" warning.
|
|
77
|
+
const status = (runInfo && runInfo.status) || '';
|
|
78
|
+
if (status !== 'error' && status !== 'timeout') return { inspected: false };
|
|
79
|
+
if (!task || !task.inspectOnFailure) return { inspected: false };
|
|
80
|
+
if (!supportsInspection(task)) return { inspected: false, error: 'this task type has no prompt to revise' };
|
|
81
|
+
if (typeof ask !== 'function') return { inspected: false, error: 'no model is available for the inspection' };
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await ask({
|
|
85
|
+
task,
|
|
86
|
+
prompt: buildInspectPrompt(task, runInfo),
|
|
87
|
+
system: SYSTEM_PROMPT,
|
|
88
|
+
preferredModel,
|
|
89
|
+
maxTokens: 400,
|
|
90
|
+
timeoutMs,
|
|
91
|
+
});
|
|
92
|
+
if (!result || !result.ok) return { inspected: false, error: (result && result.error) || 'the model call failed' };
|
|
93
|
+
const diagnosis = readDiagnosis(result.text);
|
|
94
|
+
if (!diagnosis.ok) return { inspected: false, error: diagnosis.error };
|
|
95
|
+
return { inspected: true, diagnosis: diagnosis.diagnosis, suggestion: diagnosis.suggestion, confidence: diagnosis.confidence };
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { inspected: false, error: (err && err.message) || String(err) };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Ask helper bound to a context, used by the scheduler. */
|
|
102
|
+
export function makeInspectAsk(ctx) {
|
|
103
|
+
return async ({ prompt, system, preferredModel, maxTokens, timeoutMs, task }) => {
|
|
104
|
+
const target = resolveAskTarget(ctx, task || {}, preferredModel);
|
|
105
|
+
return askModel(ctx, { ...target, prompt, system, maxTokens, timeoutMs });
|
|
106
|
+
};
|
|
107
|
+
}
|
package/lib/http-utils.js
CHANGED
|
@@ -85,3 +85,27 @@ export function pickPatchableFields(body, allowed) {
|
|
|
85
85
|
}
|
|
86
86
|
return patch;
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
export function sendJson(res, statusCode, data) {
|
|
90
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
91
|
+
res.end(JSON.stringify(data));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function rejectCrossOrigin(req, res) {
|
|
95
|
+
if (isCrossOrigin(req)) {
|
|
96
|
+
sendJson(res, 403, { ok: false, error: 'Cross-origin request rejected' });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function readBody(req, res) {
|
|
103
|
+
try {
|
|
104
|
+
return { body: await parseJsonBody(req) };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
sendJson(res, err.statusCode || 400, { ok: false, error: err.message });
|
|
107
|
+
return { error: true };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Settings keys synchronized between the store and the settings service. */
|