@goodandready/dsh-cron 0.3.3 → 0.3.4
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/lib/analytics-digest.js +236 -0
- package/lib/api.js +41 -0
- package/lib/client.js +4718 -4515
- package/lib/prompt-evolver.js +148 -0
- package/lib/routes.js +79 -0
- package/lib/scheduler-execution.js +10 -0
- package/lib/task-create.js +3 -0
- package/lib/task-patch.js +9 -0
- package/lib/task-transfer.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Auto-Evolver & Versioning Engine (#331)
|
|
3
|
+
*
|
|
4
|
+
* Tracks prompt revisions, maintains versioned prompt history, computes
|
|
5
|
+
* empirical Success Rates across versions, and enables 1-click rollback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MAX_PROMPT_HISTORY = 20;
|
|
9
|
+
|
|
10
|
+
export function sanitizePromptHistory(history) {
|
|
11
|
+
if (!Array.isArray(history)) return [];
|
|
12
|
+
return history
|
|
13
|
+
.filter((h) => h && typeof h === 'object' && typeof h.prompt === 'string')
|
|
14
|
+
.slice(-MAX_PROMPT_HISTORY)
|
|
15
|
+
.map((h, idx) => ({
|
|
16
|
+
version: typeof h.version === 'number' && h.version > 0 ? h.version : (idx + 1),
|
|
17
|
+
prompt: String(h.prompt || ''),
|
|
18
|
+
note: typeof h.note === 'string' ? h.note.slice(0, 100) : 'Revision',
|
|
19
|
+
runs: typeof h.runs === 'number' && h.runs >= 0 ? h.runs : 0,
|
|
20
|
+
successes: typeof h.successes === 'number' && h.successes >= 0 ? h.successes : 0,
|
|
21
|
+
successRate: typeof h.successRate === 'number' && h.successRate >= 0 && h.successRate <= 1
|
|
22
|
+
? Number(h.successRate.toFixed(3))
|
|
23
|
+
: (typeof h.runs === 'number' && h.runs > 0 ? Number(((h.successes || 0) / h.runs).toFixed(3)) : 1.0),
|
|
24
|
+
createdAt: typeof h.createdAt === 'number' ? h.createdAt : Date.now(),
|
|
25
|
+
lastRunAt: typeof h.lastRunAt === 'number' ? h.lastRunAt : null,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function trackPromptEdit(currentTask, newPrompt, note = 'Manual edit') {
|
|
30
|
+
if (!currentTask || typeof newPrompt !== 'string') {
|
|
31
|
+
return {
|
|
32
|
+
prompt: currentTask ? currentTask.prompt : newPrompt,
|
|
33
|
+
promptVersion: currentTask ? (currentTask.promptVersion || 1) : 1,
|
|
34
|
+
promptHistory: currentTask ? sanitizePromptHistory(currentTask.promptHistory) : [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const trimmedNew = newPrompt.trim();
|
|
39
|
+
const trimmedOld = (currentTask.prompt || '').trim();
|
|
40
|
+
|
|
41
|
+
let history = sanitizePromptHistory(currentTask.promptHistory);
|
|
42
|
+
|
|
43
|
+
// If prompt has not changed, keep current version and baseline history if needed
|
|
44
|
+
if (trimmedNew === trimmedOld) {
|
|
45
|
+
if (history.length === 0 && trimmedOld) {
|
|
46
|
+
history.push({
|
|
47
|
+
version: currentTask.promptVersion || 1,
|
|
48
|
+
prompt: currentTask.prompt,
|
|
49
|
+
note: 'Initial baseline',
|
|
50
|
+
runs: typeof currentTask.runs === 'number' ? currentTask.runs : 0,
|
|
51
|
+
successes: typeof currentTask.successes === 'number' ? currentTask.successes : 0,
|
|
52
|
+
successRate: currentTask.runs > 0
|
|
53
|
+
? Number(((currentTask.successes || 0) / currentTask.runs).toFixed(3))
|
|
54
|
+
: 1.0,
|
|
55
|
+
createdAt: currentTask.createdAt || Date.now(),
|
|
56
|
+
lastRunAt: currentTask.lastRun || null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
prompt: currentTask.prompt,
|
|
61
|
+
promptVersion: currentTask.promptVersion || 1,
|
|
62
|
+
promptHistory: history,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If no history exists yet, record baseline version 1 with existing task metrics before bumping
|
|
67
|
+
if (history.length === 0 && trimmedOld) {
|
|
68
|
+
history.push({
|
|
69
|
+
version: 1,
|
|
70
|
+
prompt: currentTask.prompt,
|
|
71
|
+
note: 'Initial baseline',
|
|
72
|
+
runs: typeof currentTask.runs === 'number' ? currentTask.runs : 0,
|
|
73
|
+
successes: typeof currentTask.successes === 'number' ? currentTask.successes : 0,
|
|
74
|
+
successRate: currentTask.runs > 0
|
|
75
|
+
? Number(((currentTask.successes || 0) / currentTask.runs).toFixed(3))
|
|
76
|
+
: 1.0,
|
|
77
|
+
createdAt: currentTask.createdAt || Date.now(),
|
|
78
|
+
lastRunAt: currentTask.lastRun || null,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const maxVersion = history.reduce((max, h) => Math.max(max, h.version || 1), currentTask.promptVersion || 1);
|
|
83
|
+
const nextVersion = maxVersion + 1;
|
|
84
|
+
|
|
85
|
+
history.push({
|
|
86
|
+
version: nextVersion,
|
|
87
|
+
prompt: newPrompt,
|
|
88
|
+
note: String(note || 'Manual edit').slice(0, 100),
|
|
89
|
+
runs: 0,
|
|
90
|
+
successes: 0,
|
|
91
|
+
successRate: 1.0,
|
|
92
|
+
createdAt: Date.now(),
|
|
93
|
+
lastRunAt: null,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
prompt: newPrompt,
|
|
98
|
+
promptVersion: nextVersion,
|
|
99
|
+
promptHistory: sanitizePromptHistory(history),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function recordPromptRunOutcome(task, isSuccess) {
|
|
104
|
+
if (!task || typeof task.prompt !== 'string') return task?.promptHistory || [];
|
|
105
|
+
|
|
106
|
+
let history = sanitizePromptHistory(task.promptHistory);
|
|
107
|
+
const activeVersion = task.promptVersion || 1;
|
|
108
|
+
|
|
109
|
+
let entry = history.find((h) => h.version === activeVersion);
|
|
110
|
+
if (!entry) {
|
|
111
|
+
entry = {
|
|
112
|
+
version: activeVersion,
|
|
113
|
+
prompt: task.prompt,
|
|
114
|
+
note: 'Active prompt',
|
|
115
|
+
runs: 0,
|
|
116
|
+
successes: 0,
|
|
117
|
+
successRate: 1.0,
|
|
118
|
+
createdAt: Date.now(),
|
|
119
|
+
lastRunAt: null,
|
|
120
|
+
};
|
|
121
|
+
history.push(entry);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
entry.runs = (entry.runs || 0) + 1;
|
|
125
|
+
if (isSuccess) {
|
|
126
|
+
entry.successes = (entry.successes || 0) + 1;
|
|
127
|
+
}
|
|
128
|
+
entry.successRate = Number((entry.successes / entry.runs).toFixed(3));
|
|
129
|
+
entry.lastRunAt = Date.now();
|
|
130
|
+
|
|
131
|
+
task.promptHistory = sanitizePromptHistory(history);
|
|
132
|
+
return task.promptHistory;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function rollbackPromptVersion(task, targetVersion) {
|
|
136
|
+
if (!task) return null;
|
|
137
|
+
const history = sanitizePromptHistory(task.promptHistory);
|
|
138
|
+
const target = history.find((h) => h.version === targetVersion);
|
|
139
|
+
if (!target) {
|
|
140
|
+
throw new Error(`Target prompt version ${targetVersion} not found in history`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const edit = trackPromptEdit(task, target.prompt, `Rollback to v${targetVersion}`);
|
|
144
|
+
task.prompt = edit.prompt;
|
|
145
|
+
task.promptVersion = edit.promptVersion;
|
|
146
|
+
task.promptHistory = edit.promptHistory;
|
|
147
|
+
return task;
|
|
148
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildAnalyticsDigest, formatDigestMarkdown, sendAnalyticsDigest } from './analytics-digest.js';
|
|
1
2
|
import { listVerifiedRecipes } from './recipes-catalog.js';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { createCronApiHandler } from './api.js';
|
|
@@ -100,6 +101,84 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
100
101
|
}
|
|
101
102
|
}), 'dsh-cron: /chat/start');
|
|
102
103
|
|
|
104
|
+
// Analytics digest routes (#330)
|
|
105
|
+
ctx.effect(() => ctx.webServer.register({
|
|
106
|
+
kind: 'prefix',
|
|
107
|
+
path: '/dsh-cron/analytics',
|
|
108
|
+
handler: async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
111
|
+
const path = url.pathname;
|
|
112
|
+
|
|
113
|
+
if (req.method === 'GET') {
|
|
114
|
+
const days = parseInt(url.searchParams.get('days') || '7', 10);
|
|
115
|
+
const tasks = store.list ? store.list() : Object.values(store.tasks || {});
|
|
116
|
+
let history = [];
|
|
117
|
+
if (store.history) {
|
|
118
|
+
if (typeof store.history.values === 'function') {
|
|
119
|
+
for (const runs of store.history.values()) {
|
|
120
|
+
if (Array.isArray(runs)) history.push(...runs);
|
|
121
|
+
}
|
|
122
|
+
} else if (typeof store.history === 'object') {
|
|
123
|
+
for (const runs of Object.values(store.history)) {
|
|
124
|
+
if (Array.isArray(runs)) history.push(...runs);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const costLedger = store.costLedger || [];
|
|
129
|
+
const digest = buildAnalyticsDigest({ tasks, history, costLedger, periodDays: days });
|
|
130
|
+
const markdown = formatDigestMarkdown(digest);
|
|
131
|
+
sendJson(res, 200, { ok: true, digest, markdown });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (req.method === 'POST') {
|
|
136
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
137
|
+
const { body } = await readBody(req, res);
|
|
138
|
+
const days = parseInt(body?.days || url.searchParams.get('days') || '7', 10);
|
|
139
|
+
const tasks = store.list ? store.list() : Object.values(store.tasks || {});
|
|
140
|
+
let history = [];
|
|
141
|
+
if (store.history) {
|
|
142
|
+
if (typeof store.history.values === 'function') {
|
|
143
|
+
for (const runs of store.history.values()) {
|
|
144
|
+
if (Array.isArray(runs)) history.push(...runs);
|
|
145
|
+
}
|
|
146
|
+
} else if (typeof store.history === 'object') {
|
|
147
|
+
for (const runs of Object.values(store.history)) {
|
|
148
|
+
if (Array.isArray(runs)) history.push(...runs);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const costLedger = store.costLedger || [];
|
|
153
|
+
const digest = buildAnalyticsDigest({ tasks, history, costLedger, periodDays: days });
|
|
154
|
+
const settings = store.getSettings ? store.getSettings() : {};
|
|
155
|
+
const deliveryConfig = {
|
|
156
|
+
telegram: body?.telegram !== false && Boolean(settings.botToken && settings.chatId),
|
|
157
|
+
webhookUrl: body?.webhookUrl || settings.webhookUrl,
|
|
158
|
+
};
|
|
159
|
+
const channels = {
|
|
160
|
+
sendTelegram: async (text) => sendTelegramMessage({ botToken: settings.botToken, chatId: settings.chatId, text }),
|
|
161
|
+
sendWebhook: async (targetUrl, payload) => {
|
|
162
|
+
const r = await fetch(targetUrl, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify(payload),
|
|
166
|
+
});
|
|
167
|
+
return { ok: r.ok, status: r.status };
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
const delivery = await sendAnalyticsDigest({ digest, channels, deliveryConfig });
|
|
171
|
+
sendJson(res, 200, { ok: true, digest, delivery });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
176
|
+
} catch (err) {
|
|
177
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}), 'dsh-cron: /analytics');
|
|
181
|
+
|
|
103
182
|
// GET /dsh-cron/recipes (#48): the built-in recipe catalog.
|
|
104
183
|
ctx.effect(() => ctx.webServer.register({
|
|
105
184
|
kind: 'exact',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordPromptRunOutcome } from './prompt-evolver.js';
|
|
1
2
|
import { evaluateAdaptiveRun, calculateNextAdaptiveState } from './adaptive.js';
|
|
2
3
|
import { approvalManager } from './approval.js';
|
|
3
4
|
import { globalBudgetManager } from './global-budgets.js';
|
|
@@ -513,6 +514,15 @@ export async function deliverNotifications(task, runInfo, { store, resolveSecret
|
|
|
513
514
|
export function finishRun(scheduler, taskSnapshot, taskId, status, queuedCount, meta = {}) {
|
|
514
515
|
scheduler.countRun(status);
|
|
515
516
|
|
|
517
|
+
// Track prompt version success rate outcome (#331)
|
|
518
|
+
if (taskSnapshot && taskSnapshot.prompt && (taskSnapshot.promptVersion || taskSnapshot.promptHistory)) {
|
|
519
|
+
const isSuccess = status === 'success';
|
|
520
|
+
recordPromptRunOutcome(taskSnapshot, isSuccess);
|
|
521
|
+
if (scheduler.store && typeof scheduler.store.set === 'function') {
|
|
522
|
+
scheduler.store.set(taskSnapshot);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
516
526
|
// Adaptive polling backoff (#329)
|
|
517
527
|
if (taskSnapshot && taskSnapshot.adaptive && taskSnapshot.adaptive.enabled && !taskSnapshot.oneShot && taskSnapshot.status === 'active' && !scheduler.isStopped) {
|
|
518
528
|
const history = scheduler.store.getHistory ? scheduler.store.getHistory(taskId, 2) : [];
|
package/lib/task-create.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizePromptHistory } from './prompt-evolver.js';
|
|
1
2
|
import { sanitizeAdaptiveConfig } from './adaptive.js';
|
|
2
3
|
import { sanitizeValidators } from './validators.js';
|
|
3
4
|
import { normalizeTaskType, EXTERNAL_RUNTIMES } from './runtimes.js';
|
|
@@ -33,6 +34,8 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
33
34
|
schedule: parsed.cronPattern || args.schedule,
|
|
34
35
|
scheduleText: parsed.humanText,
|
|
35
36
|
prompt: args.prompt,
|
|
37
|
+
promptVersion: args.prompt ? 1 : undefined,
|
|
38
|
+
promptHistory: args.prompt ? [{ version: 1, prompt: args.prompt, note: 'Initial creation', runs: 0, successes: 0, successRate: 1.0, createdAt: Date.now(), lastRunAt: null }] : undefined,
|
|
36
39
|
type: taskType,
|
|
37
40
|
delivery: args.delivery || 'isolated',
|
|
38
41
|
status: 'active',
|
package/lib/task-patch.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trackPromptEdit, sanitizePromptHistory } from './prompt-evolver.js';
|
|
1
2
|
import { sanitizeAdaptiveConfig } from './adaptive.js';
|
|
2
3
|
import { sanitizeValidators } from './validators.js';
|
|
3
4
|
import { sanitizeFileWatch } from './task-create.js';
|
|
@@ -108,6 +109,14 @@ export function buildTaskPatch(current, body) {
|
|
|
108
109
|
if (patch.validators !== undefined) {
|
|
109
110
|
patch.validators = Array.isArray(patch.validators) ? sanitizeValidators(patch.validators) : [];
|
|
110
111
|
}
|
|
112
|
+
if (patch.prompt !== undefined && current && typeof current.prompt === 'string') {
|
|
113
|
+
const edit = trackPromptEdit(current, patch.prompt, body && body.promptNote ? body.promptNote : 'Manual edit');
|
|
114
|
+
patch.prompt = edit.prompt;
|
|
115
|
+
patch.promptVersion = edit.promptVersion;
|
|
116
|
+
patch.promptHistory = edit.promptHistory;
|
|
117
|
+
} else if (patch.promptHistory !== undefined) {
|
|
118
|
+
patch.promptHistory = sanitizePromptHistory(patch.promptHistory);
|
|
119
|
+
}
|
|
111
120
|
if (patch.costLimitUsd !== undefined) {
|
|
112
121
|
patch.costLimitUsd = patch.costLimitUsd === null ? null : (Number(patch.costLimitUsd) > 0 ? Number(patch.costLimitUsd) : null);
|
|
113
122
|
}
|
package/lib/task-transfer.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|