minionsai 0.1.11 → 0.1.13
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 +3 -3
- package/dist/server/client/dist/assets/{highlighted-body-TPN3WLV5-Bsa7IWN6.js → highlighted-body-TPN3WLV5-K6mK4CnF.js} +1 -1
- package/dist/server/client/dist/assets/index-BP1qiwyD.css +1 -0
- package/dist/server/client/dist/assets/index-o62nNllV.js +751 -0
- package/dist/server/client/dist/index.html +2 -2
- package/dist/server/client/dist/sounds/done.mp3 +0 -0
- package/dist/server/server/adapters/hermes-worker.d.ts +18 -14
- package/dist/server/server/adapters/hermes-worker.js +78 -39
- package/dist/server/server/adapters/types.d.ts +18 -14
- package/dist/server/server/adapters/worker-protocol.d.ts +51 -28
- package/dist/server/server/app.js +2 -2
- package/dist/server/server/live-chat.d.ts +17 -2
- package/dist/server/server/live-chat.js +97 -26
- package/dist/server/server/prompts/task-agent.d.ts +1 -1
- package/dist/server/server/prompts/task-agent.js +2 -2
- package/dist/server/server/routes/chat.js +169 -51
- package/dist/server/server/routes/{routines.d.ts → scheduled-tasks.d.ts} +1 -1
- package/dist/server/server/routes/{routines.js → scheduled-tasks.js} +49 -49
- package/dist/server/server/scheduled-tasks/runs.d.ts +3 -0
- package/dist/server/server/{routines → scheduled-tasks}/runs.js +7 -7
- package/dist/server/server/workers/{hermes_routines.py → hermes_scheduled_tasks.py} +42 -42
- package/dist/server/server/workers/hermes_worker.py +153 -79
- package/dist/server/server/workers/hermes_worker_utils.py +1 -1
- package/dist/server/shared/types.d.ts +40 -14
- package/dist/server/shared/types.js +2 -0
- package/package.json +2 -1
- package/dist/server/client/dist/assets/index-BdyoTQw9.css +0 -1
- package/dist/server/client/dist/assets/index-C0fNYxd2.js +0 -727
- package/dist/server/server/routines/runs.d.ts +0 -3
|
@@ -2,16 +2,28 @@ import { Router } from 'express';
|
|
|
2
2
|
import { contextFromTask, getTask, updateTask, touchTask, recordAgentResponse } from '../db/queries.js';
|
|
3
3
|
import { adapter } from '../app.js';
|
|
4
4
|
import { broadcast, initSSE } from '../events.js';
|
|
5
|
-
import { applyEvent, broadcast as broadcastLive, finishRun, getRun, getRunContext, getRunStatus, sendSnapshot, startRun, subscribe, } from '../live-chat.js';
|
|
5
|
+
import { appendSystemMessage, appendUserMessage, applyEvent, broadcast as broadcastLive, finishRun, getRun, getRunContext, getRunStatus, sendSnapshot, startAssistantMessage, startCompactionRun, startGoalRun, startRun, subscribe, updateRunGoal, updateRunContext, updateRunStatus, } from '../live-chat.js';
|
|
6
6
|
import { taskRunSettings, parseRunSettingsBody } from '../agent-settings.js';
|
|
7
7
|
import { TASK_AGENT_SYSTEM_PROMPT } from '../prompts/task-agent.js';
|
|
8
|
-
import { toErrorMessage } from '../errors.js';
|
|
8
|
+
import { isRecord, toErrorMessage } from '../errors.js';
|
|
9
|
+
import { CHAT_RUN_MODES, MINIONS_GOAL_MAX_TURNS } from '../../shared/types.js';
|
|
9
10
|
export const chatRouter = Router();
|
|
10
11
|
function hasNoSession(task) {
|
|
11
12
|
if (task.last_agent_response_at !== null)
|
|
12
13
|
return false;
|
|
13
14
|
return getRunStatus(task.id)?.status !== 'streaming';
|
|
14
15
|
}
|
|
16
|
+
function isTaskRunActive(status) {
|
|
17
|
+
return status?.status === 'streaming' || status?.status === 'compacting';
|
|
18
|
+
}
|
|
19
|
+
function completeTaskRun(taskId, runId, status, ttlMs, options) {
|
|
20
|
+
const updated = updateRunStatus(taskId, status, options);
|
|
21
|
+
if (updated) {
|
|
22
|
+
broadcast({ type: 'task_run_updated', run: updated });
|
|
23
|
+
broadcastRunSnapshot(taskId);
|
|
24
|
+
}
|
|
25
|
+
finishRun(taskId, ttlMs, runId);
|
|
26
|
+
}
|
|
15
27
|
chatRouter.get('/:id/messages', async (req, res) => {
|
|
16
28
|
const task = getTask(req.params.id);
|
|
17
29
|
if (!task)
|
|
@@ -44,31 +56,46 @@ chatRouter.get('/:id/session', async (req, res) => {
|
|
|
44
56
|
});
|
|
45
57
|
const DONE_SNAPSHOT_TTL_MS = 30_000;
|
|
46
58
|
const ERROR_SNAPSHOT_TTL_MS = 24 * 60 * 60_000;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
function parseChatRunMode(body) {
|
|
60
|
+
const record = isRecord(body) ? body : {};
|
|
61
|
+
const settings = isRecord(record.settings) ? record.settings : {};
|
|
62
|
+
const mode = settings.mode ?? record.mode ?? 'task';
|
|
63
|
+
if (CHAT_RUN_MODES.includes(mode))
|
|
64
|
+
return mode;
|
|
65
|
+
throw new Error(`mode must be one of: ${CHAT_RUN_MODES.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
function broadcastRunSnapshot(taskId) {
|
|
68
|
+
const liveRun = getRun(taskId);
|
|
69
|
+
if (liveRun)
|
|
70
|
+
broadcastLive(taskId, { type: 'snapshot', run: liveRun });
|
|
71
|
+
}
|
|
72
|
+
function recordCompletedAgentRun(taskId, context) {
|
|
73
|
+
const updated = recordAgentResponse(taskId, Date.now(), context);
|
|
74
|
+
if (updated && updated.status === 'in_progress') {
|
|
75
|
+
return updateTask(taskId, { status: 'in_review' });
|
|
63
76
|
}
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
return updated;
|
|
78
|
+
}
|
|
79
|
+
function settleRun(taskId, runId, context) {
|
|
80
|
+
const status = getRunStatus(taskId);
|
|
81
|
+
if (status)
|
|
82
|
+
broadcast({ type: 'task_run_updated', run: status });
|
|
83
|
+
if (status?.status === 'done') {
|
|
84
|
+
const updated = recordCompletedAgentRun(taskId, context);
|
|
85
|
+
if (updated)
|
|
86
|
+
broadcast({ type: 'task_updated', task: updated });
|
|
66
87
|
}
|
|
88
|
+
else {
|
|
89
|
+
touchTask(taskId);
|
|
90
|
+
}
|
|
91
|
+
const ttl = status?.status === 'error' ? ERROR_SNAPSHOT_TTL_MS : DONE_SNAPSHOT_TTL_MS;
|
|
92
|
+
finishRun(taskId, ttl, runId);
|
|
67
93
|
}
|
|
68
|
-
async function
|
|
94
|
+
async function streamChatTurn(runTask, sessionId, content, options) {
|
|
69
95
|
let sawDone = false;
|
|
70
96
|
let doneContext;
|
|
71
97
|
let responseText = '';
|
|
98
|
+
let hadError = false;
|
|
72
99
|
try {
|
|
73
100
|
const stream = adapter.chatStream(sessionId, content, {
|
|
74
101
|
systemMessage: TASK_AGENT_SYSTEM_PROMPT,
|
|
@@ -76,44 +103,111 @@ async function consumeChatRun(runTask, sessionId, content, runId) {
|
|
|
76
103
|
task: { id: runTask.id, title: runTask.title },
|
|
77
104
|
});
|
|
78
105
|
for await (const event of stream) {
|
|
79
|
-
if (event.type === 'text_delta' &&
|
|
80
|
-
responseText += event.content
|
|
106
|
+
if (options.captureResponseText && event.type === 'text_delta' && event.content) {
|
|
107
|
+
responseText += event.content;
|
|
108
|
+
}
|
|
81
109
|
if (event.type === 'done') {
|
|
82
110
|
sawDone = true;
|
|
83
111
|
doneContext = event.context;
|
|
112
|
+
if (!options.completeOnDone) {
|
|
113
|
+
updateRunContext(runTask.id, event.context, event.sessionId);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (event.type === 'error') {
|
|
118
|
+
hadError = true;
|
|
84
119
|
}
|
|
85
120
|
applyEvent(runTask.id, event);
|
|
86
121
|
broadcastLive(runTask.id, event);
|
|
87
122
|
}
|
|
88
123
|
}
|
|
89
124
|
catch (error) {
|
|
125
|
+
hadError = true;
|
|
90
126
|
const event = { type: 'error', error: toErrorMessage(error, 'Hermes chat stream failed') };
|
|
91
127
|
applyEvent(runTask.id, event);
|
|
92
128
|
broadcastLive(runTask.id, event);
|
|
93
129
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
const event = { type: 'done', sessionId };
|
|
130
|
+
const finalRun = getRunStatus(runTask.id);
|
|
131
|
+
if (!sawDone && !hadError && finalRun?.status === 'streaming') {
|
|
132
|
+
if (options.completeOnDone) {
|
|
133
|
+
const event = { type: 'done', sessionId, context: doneContext };
|
|
98
134
|
sawDone = true;
|
|
99
135
|
applyEvent(runTask.id, event);
|
|
100
136
|
broadcastLive(runTask.id, event);
|
|
101
137
|
}
|
|
102
|
-
const finishedRun = getRunStatus(runTask.id);
|
|
103
|
-
if (finishedRun)
|
|
104
|
-
broadcast({ type: 'task_run_updated', run: finishedRun });
|
|
105
|
-
if (sawDone && finishedRun?.status === 'done') {
|
|
106
|
-
const responseAt = Date.now();
|
|
107
|
-
const updated = recordAgentResponse(runTask.id, responseAt, doneContext ?? null);
|
|
108
|
-
if (updated)
|
|
109
|
-
broadcast({ type: 'task_updated', task: updated });
|
|
110
|
-
void judgeTaskCompletion(runTask, responseText, responseAt);
|
|
111
|
-
}
|
|
112
138
|
else {
|
|
113
|
-
|
|
139
|
+
hadError = true;
|
|
140
|
+
const event = { type: 'error', error: 'Hermes chat stream ended before completion' };
|
|
141
|
+
applyEvent(runTask.id, event);
|
|
142
|
+
broadcastLive(runTask.id, event);
|
|
114
143
|
}
|
|
115
|
-
|
|
116
|
-
|
|
144
|
+
}
|
|
145
|
+
return { responseText, sawDone, context: doneContext, hadError };
|
|
146
|
+
}
|
|
147
|
+
async function consumeChatRun(runTask, sessionId, content, runId) {
|
|
148
|
+
const result = await streamChatTurn(runTask, sessionId, content, { completeOnDone: true });
|
|
149
|
+
try {
|
|
150
|
+
settleRun(runTask.id, runId, result.context ?? null);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
finishRun(runTask.id, ERROR_SNAPSHOT_TTL_MS, runId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function consumeGoalRun(runTask, sessionId, initialContent, runId) {
|
|
157
|
+
let finalContext;
|
|
158
|
+
let hadError = false;
|
|
159
|
+
let turnContent = initialContent;
|
|
160
|
+
let turnCount = 0;
|
|
161
|
+
try {
|
|
162
|
+
while (turnContent) {
|
|
163
|
+
if (++turnCount > MINIONS_GOAL_MAX_TURNS) {
|
|
164
|
+
appendSystemMessage(runTask.id, 'Goal turn limit reached');
|
|
165
|
+
broadcastRunSnapshot(runTask.id);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
appendUserMessage(runTask.id, turnContent);
|
|
169
|
+
startAssistantMessage(runTask.id);
|
|
170
|
+
const turn = await streamChatTurn(runTask, sessionId, turnContent, {
|
|
171
|
+
completeOnDone: false,
|
|
172
|
+
captureResponseText: true,
|
|
173
|
+
});
|
|
174
|
+
if (turn.context !== undefined)
|
|
175
|
+
finalContext = turn.context;
|
|
176
|
+
const currentRun = getRunStatus(runTask.id);
|
|
177
|
+
if (turn.hadError || currentRun?.status === 'error') {
|
|
178
|
+
hadError = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
const decision = await adapter.evaluateGoal(sessionId, turn.responseText);
|
|
182
|
+
let shouldBroadcastSnapshot = false;
|
|
183
|
+
if (decision.state) {
|
|
184
|
+
const goalRun = updateRunGoal(runTask.id, decision.state);
|
|
185
|
+
if (goalRun)
|
|
186
|
+
broadcast({ type: 'task_run_updated', run: goalRun });
|
|
187
|
+
shouldBroadcastSnapshot = true;
|
|
188
|
+
}
|
|
189
|
+
if (decision.message) {
|
|
190
|
+
appendSystemMessage(runTask.id, decision.message);
|
|
191
|
+
shouldBroadcastSnapshot = true;
|
|
192
|
+
}
|
|
193
|
+
if (shouldBroadcastSnapshot)
|
|
194
|
+
broadcastRunSnapshot(runTask.id);
|
|
195
|
+
if (!decision.shouldContinue)
|
|
196
|
+
break;
|
|
197
|
+
turnContent = decision.continuationPrompt?.trim() ? decision.continuationPrompt : null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
hadError = true;
|
|
202
|
+
const event = { type: 'error', error: toErrorMessage(error, 'Hermes goal loop failed') };
|
|
203
|
+
applyEvent(runTask.id, event);
|
|
204
|
+
broadcastLive(runTask.id, event);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
if (!hadError && getRunStatus(runTask.id)?.status === 'streaming') {
|
|
208
|
+
updateRunStatus(runTask.id, 'done', { context: finalContext ?? null });
|
|
209
|
+
}
|
|
210
|
+
settleRun(runTask.id, runId, finalContext ?? null);
|
|
117
211
|
}
|
|
118
212
|
}
|
|
119
213
|
chatRouter.post('/:id/messages', async (req, res) => {
|
|
@@ -125,14 +219,16 @@ chatRouter.post('/:id/messages', async (req, res) => {
|
|
|
125
219
|
return res.status(400).json({ error: 'content is required' });
|
|
126
220
|
}
|
|
127
221
|
let runSettings;
|
|
222
|
+
let mode;
|
|
128
223
|
try {
|
|
129
224
|
runSettings = parseRunSettingsBody(req.body);
|
|
225
|
+
mode = parseChatRunMode(req.body);
|
|
130
226
|
}
|
|
131
227
|
catch (error) {
|
|
132
228
|
return res.status(400).json({ error: toErrorMessage(error, 'Invalid run settings') });
|
|
133
229
|
}
|
|
134
230
|
const activeRun = getRunStatus(task.id);
|
|
135
|
-
if (activeRun
|
|
231
|
+
if (isTaskRunActive(activeRun)) {
|
|
136
232
|
return res.status(409).json({ error: 'This task already has a message in progress' });
|
|
137
233
|
}
|
|
138
234
|
let runTask = task;
|
|
@@ -157,24 +253,43 @@ chatRouter.post('/:id/messages', async (req, res) => {
|
|
|
157
253
|
broadcast({ type: 'task_updated', task: updated });
|
|
158
254
|
}
|
|
159
255
|
const sessionId = runTask.id;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
256
|
+
if (mode === 'goal') {
|
|
257
|
+
let goalState;
|
|
258
|
+
try {
|
|
259
|
+
goalState = await adapter.setGoal(sessionId, content);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return res.status(503).json({ error: toErrorMessage(error, 'Could not set Hermes goal') });
|
|
263
|
+
}
|
|
264
|
+
const { snapshot, state } = startGoalRun(runTask.id, sessionId, goalState);
|
|
265
|
+
broadcast({ type: 'task_run_updated', run: state });
|
|
266
|
+
broadcastLive(runTask.id, { type: 'snapshot', run: snapshot });
|
|
267
|
+
void consumeGoalRun(runTask, sessionId, content, snapshot.runId);
|
|
268
|
+
return res.status(202).json({ runId: snapshot.runId });
|
|
269
|
+
}
|
|
270
|
+
const { snapshot, state } = startRun(runTask.id, sessionId, content);
|
|
271
|
+
broadcast({ type: 'task_run_updated', run: state });
|
|
272
|
+
broadcastLive(runTask.id, { type: 'snapshot', run: snapshot });
|
|
273
|
+
void consumeChatRun(runTask, sessionId, content, snapshot.runId);
|
|
274
|
+
res.status(202).json({ runId: snapshot.runId });
|
|
167
275
|
});
|
|
168
276
|
chatRouter.post('/:id/compact', async (req, res) => {
|
|
169
277
|
const task = getTask(req.params.id);
|
|
170
278
|
if (!task)
|
|
171
279
|
return res.status(404).json({ error: 'Task not found' });
|
|
172
280
|
const activeRun = getRunStatus(task.id);
|
|
173
|
-
if (activeRun
|
|
174
|
-
return res.status(409).json({
|
|
281
|
+
if (isTaskRunActive(activeRun)) {
|
|
282
|
+
return res.status(409).json({
|
|
283
|
+
error: activeRun?.status === 'compacting'
|
|
284
|
+
? 'This task is already compacting'
|
|
285
|
+
: 'Cannot compact while a message is streaming',
|
|
286
|
+
});
|
|
175
287
|
}
|
|
176
288
|
const focusTopic = typeof req.body?.focusTopic === 'string' ? req.body.focusTopic.trim() || null : null;
|
|
177
289
|
const currentTokens = task.last_context_used_tokens ?? undefined;
|
|
290
|
+
const { snapshot, state } = startCompactionRun(task.id, task.id);
|
|
291
|
+
broadcast({ type: 'task_run_updated', run: state });
|
|
292
|
+
broadcastLive(task.id, { type: 'snapshot', run: snapshot });
|
|
178
293
|
try {
|
|
179
294
|
const result = await adapter.compressSession(task.id, {
|
|
180
295
|
focusTopic,
|
|
@@ -187,10 +302,13 @@ chatRouter.post('/:id/compact', async (req, res) => {
|
|
|
187
302
|
if (updated)
|
|
188
303
|
broadcast({ type: 'task_updated', task: updated });
|
|
189
304
|
}
|
|
305
|
+
completeTaskRun(task.id, snapshot.runId, 'done', DONE_SNAPSHOT_TTL_MS, { context: result.context });
|
|
190
306
|
res.json(result);
|
|
191
307
|
}
|
|
192
308
|
catch (error) {
|
|
193
|
-
|
|
309
|
+
const message = toErrorMessage(error, 'Compaction failed');
|
|
310
|
+
completeTaskRun(task.id, snapshot.runId, 'error', ERROR_SNAPSHOT_TTL_MS, { error: message });
|
|
311
|
+
res.status(503).json({ error: message });
|
|
194
312
|
}
|
|
195
313
|
});
|
|
196
314
|
chatRouter.get('/:id/live', (req, res) => {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import type { HermesWorkerAdapter } from '../adapters/hermes-worker.js';
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function createScheduledTasksRouter(adapter: HermesWorkerAdapter): Router;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { errorCode, isRecord, toErrorMessage } from '../errors.js';
|
|
3
|
-
import {
|
|
4
|
-
const
|
|
3
|
+
import { listScheduledTaskRuns, getScheduledTaskRunContent } from '../scheduled-tasks/runs.js';
|
|
4
|
+
const SCHEDULED_TASK_INPUT_FIELDS = [
|
|
5
5
|
'name',
|
|
6
6
|
'prompt',
|
|
7
7
|
'schedule',
|
|
@@ -17,11 +17,11 @@ const ROUTINE_INPUT_FIELDS = [
|
|
|
17
17
|
function hasText(value) {
|
|
18
18
|
return typeof value === 'string' && value.trim().length > 0;
|
|
19
19
|
}
|
|
20
|
-
function
|
|
20
|
+
function scheduledTaskInputFromBody(body) {
|
|
21
21
|
if (!isRecord(body))
|
|
22
22
|
return {};
|
|
23
23
|
const input = {};
|
|
24
|
-
for (const field of
|
|
24
|
+
for (const field of SCHEDULED_TASK_INPUT_FIELDS) {
|
|
25
25
|
if (Object.prototype.hasOwnProperty.call(body, field)) {
|
|
26
26
|
Object.assign(input, { [field]: body[field] });
|
|
27
27
|
}
|
|
@@ -37,48 +37,48 @@ function workerStatus(error) {
|
|
|
37
37
|
return 503;
|
|
38
38
|
}
|
|
39
39
|
function workerErrorFallback(error) {
|
|
40
|
-
return workerStatus(error) === 400 ? 'Invalid
|
|
40
|
+
return workerStatus(error) === 400 ? 'Invalid scheduled task' : 'Hermes scheduled tasks worker unavailable';
|
|
41
41
|
}
|
|
42
|
-
export function
|
|
42
|
+
export function createScheduledTasksRouter(adapter) {
|
|
43
43
|
const router = Router();
|
|
44
|
-
router.get('/
|
|
44
|
+
router.get('/', async (req, res) => {
|
|
45
45
|
try {
|
|
46
46
|
const includeDisabled = req.query.includeDisabled === 'true';
|
|
47
|
-
const
|
|
48
|
-
res.json({
|
|
47
|
+
const scheduledTasks = await adapter.listScheduledTasks(includeDisabled);
|
|
48
|
+
res.json({ scheduledTasks });
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
|
51
|
-
res.status(503).json({ error: toErrorMessage(error, 'Hermes
|
|
51
|
+
res.status(503).json({ error: toErrorMessage(error, 'Hermes scheduled tasks worker unavailable') });
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
|
-
router.get('
|
|
54
|
+
router.get('/:id', async (req, res) => {
|
|
55
55
|
try {
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
58
|
-
return res.status(404).json({ error: '
|
|
59
|
-
res.json({
|
|
56
|
+
const scheduledTask = await adapter.getScheduledTask(req.params.id);
|
|
57
|
+
if (!scheduledTask)
|
|
58
|
+
return res.status(404).json({ error: 'Scheduled task not found' });
|
|
59
|
+
res.json({ scheduledTask });
|
|
60
60
|
}
|
|
61
61
|
catch (error) {
|
|
62
|
-
res.status(503).json({ error: toErrorMessage(error, 'Hermes
|
|
62
|
+
res.status(503).json({ error: toErrorMessage(error, 'Hermes scheduled tasks worker unavailable') });
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
|
-
router.post('/
|
|
66
|
-
const input =
|
|
65
|
+
router.post('/', async (req, res) => {
|
|
66
|
+
const input = scheduledTaskInputFromBody(req.body);
|
|
67
67
|
if (!hasText(input.prompt))
|
|
68
68
|
return res.status(400).json({ error: 'prompt is required' });
|
|
69
69
|
if (!hasText(input.schedule))
|
|
70
70
|
return res.status(400).json({ error: 'schedule is required' });
|
|
71
71
|
try {
|
|
72
|
-
const
|
|
73
|
-
res.json({
|
|
72
|
+
const scheduledTask = await adapter.createScheduledTask(input);
|
|
73
|
+
res.json({ scheduledTask });
|
|
74
74
|
}
|
|
75
75
|
catch (error) {
|
|
76
76
|
const status = workerStatus(error);
|
|
77
77
|
res.status(status).json({ error: toErrorMessage(error, workerErrorFallback(error)) });
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
|
-
router.patch('
|
|
81
|
-
const updates =
|
|
80
|
+
router.patch('/:id', async (req, res) => {
|
|
81
|
+
const updates = scheduledTaskInputFromBody(req.body);
|
|
82
82
|
if ('prompt' in updates && !hasText(updates.prompt)) {
|
|
83
83
|
return res.status(400).json({ error: 'prompt cannot be empty' });
|
|
84
84
|
}
|
|
@@ -86,69 +86,69 @@ export function createRoutinesRouter(adapter) {
|
|
|
86
86
|
return res.status(400).json({ error: 'schedule cannot be empty' });
|
|
87
87
|
}
|
|
88
88
|
try {
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
91
|
-
return res.status(404).json({ error: '
|
|
92
|
-
res.json({
|
|
89
|
+
const scheduledTask = await adapter.updateScheduledTask(req.params.id, updates);
|
|
90
|
+
if (!scheduledTask)
|
|
91
|
+
return res.status(404).json({ error: 'Scheduled task not found' });
|
|
92
|
+
res.json({ scheduledTask });
|
|
93
93
|
}
|
|
94
94
|
catch (error) {
|
|
95
95
|
const status = workerStatus(error);
|
|
96
96
|
res.status(status).json({ error: toErrorMessage(error, workerErrorFallback(error)) });
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
|
-
router.get('
|
|
99
|
+
router.get('/:id/runs', async (req, res) => {
|
|
100
100
|
try {
|
|
101
101
|
const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
|
|
102
102
|
const limit = rawLimit ? Number.parseInt(String(rawLimit), 10) : 20;
|
|
103
|
-
const runs = await
|
|
103
|
+
const runs = await listScheduledTaskRuns(req.params.id, Number.isFinite(limit) ? limit : 20);
|
|
104
104
|
res.json({ runs });
|
|
105
105
|
}
|
|
106
106
|
catch (error) {
|
|
107
|
-
res.status(500).json({ error: toErrorMessage(error, 'Failed to list
|
|
107
|
+
res.status(500).json({ error: toErrorMessage(error, 'Failed to list scheduled task runs') });
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
|
-
router.get('
|
|
110
|
+
router.get('/:id/runs/:runId/content', async (req, res) => {
|
|
111
111
|
try {
|
|
112
|
-
const content = await
|
|
112
|
+
const content = await getScheduledTaskRunContent(req.params.id, req.params.runId);
|
|
113
113
|
if (!content)
|
|
114
|
-
return res.status(404).json({ error: '
|
|
114
|
+
return res.status(404).json({ error: 'Scheduled task run output not found' });
|
|
115
115
|
res.json({ content });
|
|
116
116
|
}
|
|
117
117
|
catch (error) {
|
|
118
|
-
res.status(500).json({ error: toErrorMessage(error, 'Failed to read
|
|
118
|
+
res.status(500).json({ error: toErrorMessage(error, 'Failed to read scheduled task run') });
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
|
-
async function
|
|
121
|
+
async function scheduledTaskActionHandler(res, id, action) {
|
|
122
122
|
try {
|
|
123
|
-
const
|
|
124
|
-
if (!
|
|
125
|
-
return res.status(404).json({ error: '
|
|
126
|
-
res.json({
|
|
123
|
+
const scheduledTask = await action(id);
|
|
124
|
+
if (!scheduledTask)
|
|
125
|
+
return res.status(404).json({ error: 'Scheduled task not found' });
|
|
126
|
+
res.json({ scheduledTask });
|
|
127
127
|
}
|
|
128
128
|
catch (error) {
|
|
129
|
-
res.status(503).json({ error: toErrorMessage(error, 'Hermes
|
|
129
|
+
res.status(503).json({ error: toErrorMessage(error, 'Hermes scheduled tasks worker unavailable') });
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
router.post('
|
|
132
|
+
router.post('/:id/pause', (req, res) => {
|
|
133
133
|
const rawReason = req.body?.reason;
|
|
134
134
|
const reason = typeof rawReason === 'string' && rawReason.trim() ? rawReason.trim() : undefined;
|
|
135
|
-
|
|
135
|
+
scheduledTaskActionHandler(res, req.params.id, (id) => adapter.pauseScheduledTask(id, reason));
|
|
136
136
|
});
|
|
137
|
-
router.post('
|
|
138
|
-
|
|
137
|
+
router.post('/:id/resume', (req, res) => {
|
|
138
|
+
scheduledTaskActionHandler(res, req.params.id, (id) => adapter.resumeScheduledTask(id));
|
|
139
139
|
});
|
|
140
|
-
router.post('
|
|
141
|
-
|
|
140
|
+
router.post('/:id/run', (req, res) => {
|
|
141
|
+
scheduledTaskActionHandler(res, req.params.id, (id) => adapter.runScheduledTask(id));
|
|
142
142
|
});
|
|
143
|
-
router.delete('
|
|
143
|
+
router.delete('/:id', async (req, res) => {
|
|
144
144
|
try {
|
|
145
|
-
const removed = await adapter.
|
|
145
|
+
const removed = await adapter.removeScheduledTask(req.params.id);
|
|
146
146
|
if (!removed)
|
|
147
|
-
return res.status(404).json({ error: '
|
|
147
|
+
return res.status(404).json({ error: 'Scheduled task not found' });
|
|
148
148
|
res.json({ ok: true });
|
|
149
149
|
}
|
|
150
150
|
catch (error) {
|
|
151
|
-
res.status(503).json({ error: toErrorMessage(error, 'Hermes
|
|
151
|
+
res.status(503).json({ error: toErrorMessage(error, 'Hermes scheduled tasks worker unavailable') });
|
|
152
152
|
}
|
|
153
153
|
});
|
|
154
154
|
return router;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ScheduledTaskRun, ScheduledTaskRunContent } from '../../shared/types.js';
|
|
2
|
+
export declare function listScheduledTaskRuns(scheduledTaskId: string, limit?: number): Promise<ScheduledTaskRun[]>;
|
|
3
|
+
export declare function getScheduledTaskRunContent(scheduledTaskId: string, runId: string): Promise<ScheduledTaskRunContent | null>;
|
|
@@ -53,10 +53,10 @@ async function readHead(path, maxBytes = 8192) {
|
|
|
53
53
|
await fh.close();
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
export async function
|
|
57
|
-
if (!isValidSegment(
|
|
56
|
+
export async function listScheduledTaskRuns(scheduledTaskId, limit = 20) {
|
|
57
|
+
if (!isValidSegment(scheduledTaskId))
|
|
58
58
|
return [];
|
|
59
|
-
const dir = join(resolveOutputDir(),
|
|
59
|
+
const dir = join(resolveOutputDir(), scheduledTaskId);
|
|
60
60
|
const safeLimit = Math.max(1, Math.min(limit, 100));
|
|
61
61
|
let names;
|
|
62
62
|
try {
|
|
@@ -75,13 +75,13 @@ export async function listRoutineRuns(jobId, limit = 20) {
|
|
|
75
75
|
head = await readHead(path);
|
|
76
76
|
}
|
|
77
77
|
catch { }
|
|
78
|
-
return { id: stem,
|
|
78
|
+
return { id: stem, scheduledTaskId, ranAt: parseTimestamp(stem), path, status: detectStatus(head), preview: buildPreview(head) };
|
|
79
79
|
}));
|
|
80
80
|
}
|
|
81
|
-
export async function
|
|
82
|
-
if (!isValidSegment(
|
|
81
|
+
export async function getScheduledTaskRunContent(scheduledTaskId, runId) {
|
|
82
|
+
if (!isValidSegment(scheduledTaskId) || !isValidSegment(runId))
|
|
83
83
|
return null;
|
|
84
|
-
const path = join(resolveOutputDir(),
|
|
84
|
+
const path = join(resolveOutputDir(), scheduledTaskId, `${runId}.md`);
|
|
85
85
|
let content;
|
|
86
86
|
try {
|
|
87
87
|
content = await readFile(path, 'utf8');
|