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.
Files changed (29) hide show
  1. package/README.md +3 -3
  2. package/dist/server/client/dist/assets/{highlighted-body-TPN3WLV5-Bsa7IWN6.js → highlighted-body-TPN3WLV5-K6mK4CnF.js} +1 -1
  3. package/dist/server/client/dist/assets/index-BP1qiwyD.css +1 -0
  4. package/dist/server/client/dist/assets/index-o62nNllV.js +751 -0
  5. package/dist/server/client/dist/index.html +2 -2
  6. package/dist/server/client/dist/sounds/done.mp3 +0 -0
  7. package/dist/server/server/adapters/hermes-worker.d.ts +18 -14
  8. package/dist/server/server/adapters/hermes-worker.js +78 -39
  9. package/dist/server/server/adapters/types.d.ts +18 -14
  10. package/dist/server/server/adapters/worker-protocol.d.ts +51 -28
  11. package/dist/server/server/app.js +2 -2
  12. package/dist/server/server/live-chat.d.ts +17 -2
  13. package/dist/server/server/live-chat.js +97 -26
  14. package/dist/server/server/prompts/task-agent.d.ts +1 -1
  15. package/dist/server/server/prompts/task-agent.js +2 -2
  16. package/dist/server/server/routes/chat.js +169 -51
  17. package/dist/server/server/routes/{routines.d.ts → scheduled-tasks.d.ts} +1 -1
  18. package/dist/server/server/routes/{routines.js → scheduled-tasks.js} +49 -49
  19. package/dist/server/server/scheduled-tasks/runs.d.ts +3 -0
  20. package/dist/server/server/{routines → scheduled-tasks}/runs.js +7 -7
  21. package/dist/server/server/workers/{hermes_routines.py → hermes_scheduled_tasks.py} +42 -42
  22. package/dist/server/server/workers/hermes_worker.py +153 -79
  23. package/dist/server/server/workers/hermes_worker_utils.py +1 -1
  24. package/dist/server/shared/types.d.ts +40 -14
  25. package/dist/server/shared/types.js +2 -0
  26. package/package.json +2 -1
  27. package/dist/server/client/dist/assets/index-BdyoTQw9.css +0 -1
  28. package/dist/server/client/dist/assets/index-C0fNYxd2.js +0 -727
  29. 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
- async function judgeTaskCompletion(task, responseText, responseAt) {
48
- if (!responseText.trim() || task.status !== 'in_progress')
49
- return;
50
- try {
51
- const result = await adapter.judgeCompletion(task.title, task.description, responseText);
52
- if (result.done) {
53
- const current = getTask(task.id);
54
- if (!current ||
55
- current.status !== 'in_progress' ||
56
- current.last_agent_response_at !== responseAt) {
57
- return;
58
- }
59
- const updated = updateTask(task.id, { status: 'in_review' });
60
- if (updated)
61
- broadcast({ type: 'task_updated', task: updated });
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
- catch {
65
- // Judge failure is non-critical — leave task as-is
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 consumeChatRun(runTask, sessionId, content, runId) {
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' && responseText.length < 4200)
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
- finally {
95
- const currentRun = getRunStatus(runTask.id);
96
- if (!sawDone && currentRun?.status === 'streaming') {
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
- touchTask(runTask.id);
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
- const ttl = finishedRun?.status === 'error' ? ERROR_SNAPSHOT_TTL_MS : DONE_SNAPSHOT_TTL_MS;
116
- finishRun(runTask.id, ttl, runId);
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?.status === 'streaming') {
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
- const run = startRun(runTask.id, sessionId, content);
161
- const startedRun = getRunStatus(runTask.id);
162
- if (startedRun)
163
- broadcast({ type: 'task_run_updated', run: startedRun });
164
- broadcastLive(runTask.id, { type: 'snapshot', run });
165
- void consumeChatRun(runTask, sessionId, content, run.runId);
166
- res.status(202).json({ runId: run.runId });
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?.status === 'streaming') {
174
- return res.status(409).json({ error: 'Cannot compact while a message is streaming' });
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
- res.status(503).json({ error: toErrorMessage(error, 'Compaction failed') });
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 createRoutinesRouter(adapter: HermesWorkerAdapter): Router;
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 { listRoutineRuns, getRoutineRunContent } from '../routines/runs.js';
4
- const ROUTINE_INPUT_FIELDS = [
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 routineInputFromBody(body) {
20
+ function scheduledTaskInputFromBody(body) {
21
21
  if (!isRecord(body))
22
22
  return {};
23
23
  const input = {};
24
- for (const field of ROUTINE_INPUT_FIELDS) {
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 routine' : 'Hermes routines worker unavailable';
40
+ return workerStatus(error) === 400 ? 'Invalid scheduled task' : 'Hermes scheduled tasks worker unavailable';
41
41
  }
42
- export function createRoutinesRouter(adapter) {
42
+ export function createScheduledTasksRouter(adapter) {
43
43
  const router = Router();
44
- router.get('/jobs', async (req, res) => {
44
+ router.get('/', async (req, res) => {
45
45
  try {
46
46
  const includeDisabled = req.query.includeDisabled === 'true';
47
- const jobs = await adapter.listRoutines(includeDisabled);
48
- res.json({ jobs });
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 routines worker unavailable') });
51
+ res.status(503).json({ error: toErrorMessage(error, 'Hermes scheduled tasks worker unavailable') });
52
52
  }
53
53
  });
54
- router.get('/jobs/:jobId', async (req, res) => {
54
+ router.get('/:id', async (req, res) => {
55
55
  try {
56
- const job = await adapter.getRoutine(req.params.jobId);
57
- if (!job)
58
- return res.status(404).json({ error: 'Routine not found' });
59
- res.json({ job });
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 routines worker unavailable') });
62
+ res.status(503).json({ error: toErrorMessage(error, 'Hermes scheduled tasks worker unavailable') });
63
63
  }
64
64
  });
65
- router.post('/jobs', async (req, res) => {
66
- const input = routineInputFromBody(req.body);
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 job = await adapter.createRoutine(input);
73
- res.json({ job });
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('/jobs/:jobId', async (req, res) => {
81
- const updates = routineInputFromBody(req.body);
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 job = await adapter.updateRoutine(req.params.jobId, updates);
90
- if (!job)
91
- return res.status(404).json({ error: 'Routine not found' });
92
- res.json({ job });
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('/jobs/:jobId/runs', async (req, res) => {
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 listRoutineRuns(req.params.jobId, Number.isFinite(limit) ? limit : 20);
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 routine runs') });
107
+ res.status(500).json({ error: toErrorMessage(error, 'Failed to list scheduled task runs') });
108
108
  }
109
109
  });
110
- router.get('/jobs/:jobId/runs/:runId/content', async (req, res) => {
110
+ router.get('/:id/runs/:runId/content', async (req, res) => {
111
111
  try {
112
- const content = await getRoutineRunContent(req.params.jobId, req.params.runId);
112
+ const content = await getScheduledTaskRunContent(req.params.id, req.params.runId);
113
113
  if (!content)
114
- return res.status(404).json({ error: 'Routine run output not found' });
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 routine run') });
118
+ res.status(500).json({ error: toErrorMessage(error, 'Failed to read scheduled task run') });
119
119
  }
120
120
  });
121
- async function jobActionHandler(res, jobId, action) {
121
+ async function scheduledTaskActionHandler(res, id, action) {
122
122
  try {
123
- const job = await action(jobId);
124
- if (!job)
125
- return res.status(404).json({ error: 'Routine not found' });
126
- res.json({ job });
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 routines worker unavailable') });
129
+ res.status(503).json({ error: toErrorMessage(error, 'Hermes scheduled tasks worker unavailable') });
130
130
  }
131
131
  }
132
- router.post('/jobs/:jobId/pause', (req, res) => {
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
- jobActionHandler(res, req.params.jobId, (jobId) => adapter.pauseRoutine(jobId, reason));
135
+ scheduledTaskActionHandler(res, req.params.id, (id) => adapter.pauseScheduledTask(id, reason));
136
136
  });
137
- router.post('/jobs/:jobId/resume', (req, res) => {
138
- jobActionHandler(res, req.params.jobId, (jobId) => adapter.resumeRoutine(jobId));
137
+ router.post('/:id/resume', (req, res) => {
138
+ scheduledTaskActionHandler(res, req.params.id, (id) => adapter.resumeScheduledTask(id));
139
139
  });
140
- router.post('/jobs/:jobId/run', (req, res) => {
141
- jobActionHandler(res, req.params.jobId, (jobId) => adapter.runRoutine(jobId));
140
+ router.post('/:id/run', (req, res) => {
141
+ scheduledTaskActionHandler(res, req.params.id, (id) => adapter.runScheduledTask(id));
142
142
  });
143
- router.delete('/jobs/:jobId', async (req, res) => {
143
+ router.delete('/:id', async (req, res) => {
144
144
  try {
145
- const removed = await adapter.removeRoutine(req.params.jobId);
145
+ const removed = await adapter.removeScheduledTask(req.params.id);
146
146
  if (!removed)
147
- return res.status(404).json({ error: 'Routine not found' });
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 routines worker unavailable') });
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 listRoutineRuns(jobId, limit = 20) {
57
- if (!isValidSegment(jobId))
56
+ export async function listScheduledTaskRuns(scheduledTaskId, limit = 20) {
57
+ if (!isValidSegment(scheduledTaskId))
58
58
  return [];
59
- const dir = join(resolveOutputDir(), jobId);
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, jobId, ranAt: parseTimestamp(stem), path, status: detectStatus(head), preview: buildPreview(head) };
78
+ return { id: stem, scheduledTaskId, ranAt: parseTimestamp(stem), path, status: detectStatus(head), preview: buildPreview(head) };
79
79
  }));
80
80
  }
81
- export async function getRoutineRunContent(jobId, runId) {
82
- if (!isValidSegment(jobId) || !isValidSegment(runId))
81
+ export async function getScheduledTaskRunContent(scheduledTaskId, runId) {
82
+ if (!isValidSegment(scheduledTaskId) || !isValidSegment(runId))
83
83
  return null;
84
- const path = join(resolveOutputDir(), jobId, `${runId}.md`);
84
+ const path = join(resolveOutputDir(), scheduledTaskId, `${runId}.md`);
85
85
  let content;
86
86
  try {
87
87
  content = await readFile(path, 'utf8');