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
@@ -1,4 +1,4 @@
1
- """Routine operations for the Hermes worker.
1
+ """Scheduled task operations for the Hermes worker.
2
2
 
3
3
  Wraps Hermes's `cron.jobs` / `cron.scheduler` modules with input validation,
4
4
  shape normalization, and a background ticker thread.
@@ -18,8 +18,8 @@ from hermes_worker_utils import (
18
18
  )
19
19
 
20
20
 
21
- _ROUTINES_TICKER_STARTED = False
22
- _ROUTINES_TICKER_LOCK = threading.Lock()
21
+ _SCHEDULED_TASKS_TICKER_STARTED = False
22
+ _SCHEDULED_TASKS_TICKER_LOCK = threading.Lock()
23
23
 
24
24
 
25
25
  def _ensure_imports() -> None:
@@ -31,7 +31,7 @@ def _ensure_imports() -> None:
31
31
  hermes_worker._ensure_imports()
32
32
 
33
33
 
34
- def _normalize_routine(job: dict[str, Any] | None) -> dict[str, Any] | None:
34
+ def _normalize_scheduled_task(job: dict[str, Any] | None) -> dict[str, Any] | None:
35
35
  if not isinstance(job, dict):
36
36
  return None
37
37
 
@@ -136,27 +136,27 @@ def _build_update_dict(request: dict[str, Any]) -> dict[str, Any]:
136
136
  updates["context_from"] = request.get("contextFrom") or None
137
137
 
138
138
  if not updates:
139
- raise WorkerError("No routine updates were provided.", code="bad_request")
139
+ raise WorkerError("No scheduled task updates were provided.", code="bad_request")
140
140
  return updates
141
141
 
142
142
 
143
- def list_routines(include_disabled: bool = False) -> dict[str, Any]:
143
+ def list_scheduled_tasks(include_disabled: bool = False) -> dict[str, Any]:
144
144
  _ensure_imports()
145
145
  from cron.jobs import list_jobs
146
146
 
147
- jobs = [_normalize_routine(job) for job in list_jobs(include_disabled=include_disabled)]
148
- return {"jobs": [job for job in jobs if job is not None]}
147
+ jobs = [_normalize_scheduled_task(job) for job in list_jobs(include_disabled=include_disabled)]
148
+ return {"scheduledTasks": [job for job in jobs if job is not None]}
149
149
 
150
150
 
151
- def get_routine(job_id: Any) -> dict[str, Any]:
151
+ def get_scheduled_task(job_id: Any) -> dict[str, Any]:
152
152
  _ensure_imports()
153
153
  from cron.jobs import get_job
154
154
 
155
- job = _normalize_routine(get_job(_validate_path_segment(job_id, "Routine ID")))
156
- return {"job": job}
155
+ job = _normalize_scheduled_task(get_job(_validate_path_segment(job_id, "Scheduled task ID")))
156
+ return {"scheduledTask": job}
157
157
 
158
158
 
159
- def create_routine(request: dict[str, Any]) -> dict[str, Any]:
159
+ def create_scheduled_task(request: dict[str, Any]) -> dict[str, Any]:
160
160
  _ensure_imports()
161
161
  from cron.jobs import create_job
162
162
 
@@ -176,45 +176,45 @@ def create_routine(request: dict[str, Any]) -> dict[str, Any]:
176
176
  )
177
177
  except ValueError as exc:
178
178
  raise WorkerError(str(exc), code="bad_request") from exc
179
- return {"job": _normalize_routine(job)}
179
+ return {"scheduledTask": _normalize_scheduled_task(job)}
180
180
 
181
181
 
182
- def update_routine(request: dict[str, Any]) -> dict[str, Any]:
182
+ def update_scheduled_task(request: dict[str, Any]) -> dict[str, Any]:
183
183
  _ensure_imports()
184
184
  from cron.jobs import update_job
185
185
 
186
- job_id = _validate_path_segment(request.get("jobId"), "Routine ID")
186
+ job_id = _validate_path_segment(request.get("scheduledTaskId"), "Scheduled task ID")
187
187
  try:
188
- job = _normalize_routine(update_job(job_id, _build_update_dict(request)))
188
+ job = _normalize_scheduled_task(update_job(job_id, _build_update_dict(request)))
189
189
  except ValueError as exc:
190
190
  raise WorkerError(str(exc), code="bad_request") from exc
191
- return {"job": job}
191
+ return {"scheduledTask": job}
192
192
 
193
193
 
194
- def pause_routine(job_id: Any, reason: Any = None) -> dict[str, Any]:
194
+ def pause_scheduled_task(job_id: Any, reason: Any = None) -> dict[str, Any]:
195
195
  _ensure_imports()
196
196
  from cron.jobs import pause_job
197
197
 
198
- routine_id = _validate_path_segment(job_id, "Routine ID")
199
- return {"job": _normalize_routine(pause_job(routine_id, reason=string_or_none(reason)))}
198
+ scheduled_task_id = _validate_path_segment(job_id, "Scheduled task ID")
199
+ return {"scheduledTask": _normalize_scheduled_task(pause_job(scheduled_task_id, reason=string_or_none(reason)))}
200
200
 
201
201
 
202
- def resume_routine(job_id: Any) -> dict[str, Any]:
202
+ def resume_scheduled_task(job_id: Any) -> dict[str, Any]:
203
203
  _ensure_imports()
204
204
  from cron.jobs import resume_job
205
205
 
206
- return {"job": _normalize_routine(resume_job(_validate_path_segment(job_id, "Routine ID")))}
206
+ return {"scheduledTask": _normalize_scheduled_task(resume_job(_validate_path_segment(job_id, "Scheduled task ID")))}
207
207
 
208
208
 
209
- def trigger_routine(job_id: Any) -> dict[str, Any]:
209
+ def trigger_scheduled_task(job_id: Any) -> dict[str, Any]:
210
210
  _ensure_imports()
211
211
  from cron.jobs import trigger_job
212
212
 
213
- routine_id = _validate_path_segment(job_id, "Routine ID")
214
- job = _normalize_routine(trigger_job(routine_id))
213
+ scheduled_task_id = _validate_path_segment(job_id, "Scheduled task ID")
214
+ job = _normalize_scheduled_task(trigger_job(scheduled_task_id))
215
215
  if job is not None:
216
216
  _kick_immediate_tick()
217
- return {"job": job}
217
+ return {"scheduledTask": job}
218
218
 
219
219
 
220
220
  def _kick_immediate_tick() -> None:
@@ -227,43 +227,43 @@ def _kick_immediate_tick() -> None:
227
227
  """
228
228
  def _run() -> None:
229
229
  try:
230
- tick_routines()
230
+ tick_scheduled_tasks()
231
231
  except Exception as exc: # noqa: BLE001 — log and swallow, this is best-effort
232
- print(f"[hermes-worker] immediate routine tick failed: {exc}", file=sys.stderr, flush=True)
232
+ print(f"[hermes-worker] immediate scheduled task tick failed: {exc}", file=sys.stderr, flush=True)
233
233
 
234
- threading.Thread(target=_run, name="hermes-routines-trigger", daemon=True).start()
234
+ threading.Thread(target=_run, name="hermes-scheduled-tasks-trigger", daemon=True).start()
235
235
 
236
236
 
237
- def remove_routine(job_id: Any) -> dict[str, Any]:
237
+ def remove_scheduled_task(job_id: Any) -> dict[str, Any]:
238
238
  _ensure_imports()
239
239
  from cron.jobs import remove_job
240
240
 
241
- return {"ok": bool(remove_job(_validate_path_segment(job_id, "Routine ID")))}
241
+ return {"ok": bool(remove_job(_validate_path_segment(job_id, "Scheduled task ID")))}
242
242
 
243
243
 
244
- def tick_routines() -> int:
244
+ def tick_scheduled_tasks() -> int:
245
245
  _ensure_imports()
246
246
  from cron.scheduler import tick
247
247
 
248
248
  return int(tick(verbose=False) or 0)
249
249
 
250
250
 
251
- def _routines_ticker_loop() -> None:
251
+ def _scheduled_tasks_ticker_loop() -> None:
252
252
  while True:
253
253
  try:
254
- executed = tick_routines()
254
+ executed = tick_scheduled_tasks()
255
255
  if executed:
256
- print(f"[hermes-worker] routine tick executed {executed} job(s)", file=sys.stderr, flush=True)
256
+ print(f"[hermes-worker] scheduled task tick executed {executed} job(s)", file=sys.stderr, flush=True)
257
257
  except Exception as exc:
258
- print(f"[hermes-worker] routine tick failed: {exc}", file=sys.stderr, flush=True)
258
+ print(f"[hermes-worker] scheduled task tick failed: {exc}", file=sys.stderr, flush=True)
259
259
  time.sleep(60)
260
260
 
261
261
 
262
- def start_routine_ticker() -> None:
263
- global _ROUTINES_TICKER_STARTED
264
- with _ROUTINES_TICKER_LOCK:
265
- if _ROUTINES_TICKER_STARTED:
262
+ def start_scheduled_task_ticker() -> None:
263
+ global _SCHEDULED_TASKS_TICKER_STARTED
264
+ with _SCHEDULED_TASKS_TICKER_LOCK:
265
+ if _SCHEDULED_TASKS_TICKER_STARTED:
266
266
  return
267
- thread = threading.Thread(target=_routines_ticker_loop, name="hermes-routines-ticker", daemon=True)
267
+ thread = threading.Thread(target=_scheduled_tasks_ticker_loop, name="hermes-scheduled-tasks-ticker", daemon=True)
268
268
  thread.start()
269
- _ROUTINES_TICKER_STARTED = True
269
+ _SCHEDULED_TASKS_TICKER_STARTED = True
@@ -8,7 +8,6 @@ import dataclasses
8
8
  import inspect
9
9
  import json
10
10
  import os
11
- import re
12
11
  import sys
13
12
  import threading
14
13
  import time
@@ -32,23 +31,25 @@ from hermes_sessions import (
32
31
  project_session_messages,
33
32
  project_session_metadata,
34
33
  )
35
- from hermes_routines import (
36
- create_routine,
37
- get_routine,
38
- list_routines,
39
- pause_routine,
40
- remove_routine,
41
- resume_routine,
42
- start_routine_ticker,
43
- tick_routines,
44
- trigger_routine,
45
- update_routine,
34
+ from hermes_scheduled_tasks import (
35
+ create_scheduled_task,
36
+ get_scheduled_task,
37
+ list_scheduled_tasks,
38
+ pause_scheduled_task,
39
+ remove_scheduled_task,
40
+ resume_scheduled_task,
41
+ start_scheduled_task_ticker,
42
+ tick_scheduled_tasks,
43
+ trigger_scheduled_task,
44
+ update_scheduled_task,
46
45
  )
47
46
 
48
47
  PROTOCOL_OUT = sys.stdout
49
48
  PROTOCOL_LOCK = threading.Lock()
49
+ # Keep in sync with MINIONS_GOAL_MAX_TURNS in shared/types.ts.
50
+ MINIONS_GOAL_MAX_TURNS = 20
50
51
 
51
- # Cap on concurrent AIAgent.run_conversation calls (chat + judge).
52
+ # Cap on concurrent AIAgent.run_conversation calls.
52
53
  AGENT_RUN_LIMIT = int(os.environ.get("HERMES_AGENT_RUN_LIMIT", "10"))
53
54
  AGENT_SEMAPHORE = threading.BoundedSemaphore(AGENT_RUN_LIMIT)
54
55
  ACTIVE_TASKS: dict[str, str] = {}
@@ -1022,6 +1023,94 @@ def _warm_agent() -> None:
1022
1023
  _load_config()
1023
1024
 
1024
1025
 
1026
+ def _goal_manager(session_id: str) -> Any:
1027
+ _ensure_imports()
1028
+ if not session_id:
1029
+ raise WorkerError("Session ID is required.", code="bad_request")
1030
+ from hermes_cli.goals import GoalManager
1031
+
1032
+ return GoalManager(session_id=session_id, default_max_turns=MINIONS_GOAL_MAX_TURNS)
1033
+
1034
+
1035
+ def _project_goal_state(state: Any) -> dict[str, Any] | None:
1036
+ if not state:
1037
+ return None
1038
+ return {
1039
+ "goal": str(getattr(state, "goal", "") or ""),
1040
+ "status": str(getattr(state, "status", "") or "active"),
1041
+ "turnsUsed": int(getattr(state, "turns_used", 0) or 0),
1042
+ "maxTurns": int(getattr(state, "max_turns", 0) or 0),
1043
+ "lastReason": string_or_none(getattr(state, "last_reason", None)),
1044
+ "pausedReason": string_or_none(getattr(state, "paused_reason", None)),
1045
+ }
1046
+
1047
+
1048
+ def _project_goal_decision(decision: dict[str, Any], state: Any) -> dict[str, Any]:
1049
+ return {
1050
+ "status": string_or_none(decision.get("status")),
1051
+ "shouldContinue": bool(decision.get("should_continue")),
1052
+ "continuationPrompt": string_or_none(decision.get("continuation_prompt")),
1053
+ "verdict": string_or_none(decision.get("verdict")) or "inactive",
1054
+ "reason": string_or_none(decision.get("reason")) or "",
1055
+ "message": string_or_none(decision.get("message")) or "",
1056
+ "state": _project_goal_state(state),
1057
+ }
1058
+
1059
+
1060
+ def _goal_mgr_from_request(request: dict[str, Any]) -> Any:
1061
+ return _goal_manager(string_or_none(request.get("sessionId")) or "")
1062
+
1063
+
1064
+ def _goal_status(request: dict[str, Any]) -> dict[str, Any]:
1065
+ mgr = _goal_mgr_from_request(request)
1066
+ return {"goal": _project_goal_state(mgr.state)}
1067
+
1068
+
1069
+ def _goal_set(request: dict[str, Any]) -> dict[str, Any]:
1070
+ goal = string_or_none(request.get("goal")) or ""
1071
+ if not goal.strip():
1072
+ raise WorkerError("Goal text is required.", code="bad_request")
1073
+
1074
+ mgr = _goal_mgr_from_request(request)
1075
+ raw_max_turns = request.get("maxTurns")
1076
+ if raw_max_turns is None:
1077
+ state = mgr.set(goal)
1078
+ else:
1079
+ try:
1080
+ max_turns = int(raw_max_turns)
1081
+ except (TypeError, ValueError) as exc:
1082
+ raise WorkerError("maxTurns must be a positive integer.", code="bad_request") from exc
1083
+ if max_turns <= 0:
1084
+ raise WorkerError("maxTurns must be a positive integer.", code="bad_request")
1085
+ state = mgr.set(goal, max_turns=max_turns)
1086
+ return {"goal": _project_goal_state(state)}
1087
+
1088
+
1089
+ def _goal_pause(request: dict[str, Any]) -> dict[str, Any]:
1090
+ reason = string_or_none(request.get("reason")) or "user-paused"
1091
+ mgr = _goal_mgr_from_request(request)
1092
+ return {"goal": _project_goal_state(mgr.pause(reason))}
1093
+
1094
+
1095
+ def _goal_resume(request: dict[str, Any]) -> dict[str, Any]:
1096
+ mgr = _goal_mgr_from_request(request)
1097
+ return {"goal": _project_goal_state(mgr.resume())}
1098
+
1099
+
1100
+ def _goal_clear(request: dict[str, Any]) -> dict[str, Any]:
1101
+ mgr = _goal_mgr_from_request(request)
1102
+ had_goal = bool(mgr.has_goal())
1103
+ mgr.clear()
1104
+ return {"cleared": had_goal}
1105
+
1106
+
1107
+ def _goal_evaluate(request: dict[str, Any]) -> dict[str, Any]:
1108
+ response_text = string_or_none(request.get("responseText")) or ""
1109
+ mgr = _goal_mgr_from_request(request)
1110
+ decision = mgr.evaluate_after_turn(response_text)
1111
+ return _project_goal_decision(decision, mgr.state)
1112
+
1113
+
1025
1114
  def _run_chat(request_id: str, request: dict[str, Any]) -> None:
1026
1115
  settings = request.get("settings") if isinstance(request.get("settings"), dict) else {}
1027
1116
  requested_model = string_or_none(settings.get("model"))
@@ -1205,7 +1294,18 @@ def _submit_background_agent_request(
1205
1294
  *,
1206
1295
  name_prefix: str,
1207
1296
  handler: Callable[[dict[str, Any]], dict[str, Any]],
1297
+ task_key: str | None = None,
1208
1298
  ) -> None:
1299
+ if task_key and not _try_mark_task_active(task_key, request_id):
1300
+ _send_error(
1301
+ request_id,
1302
+ WorkerError(
1303
+ "This task is already running. Wait for the current operation to finish, then retry.",
1304
+ code="task_busy",
1305
+ ),
1306
+ )
1307
+ return
1308
+
1209
1309
  def runner() -> None:
1210
1310
  acquired = False
1211
1311
  try:
@@ -1217,6 +1317,8 @@ def _submit_background_agent_request(
1217
1317
  finally:
1218
1318
  if acquired:
1219
1319
  AGENT_SEMAPHORE.release()
1320
+ if task_key:
1321
+ _clear_task_active(task_key, request_id)
1220
1322
 
1221
1323
  threading.Thread(
1222
1324
  target=runner,
@@ -1291,50 +1393,6 @@ def _run_compress(request: dict[str, Any]) -> dict[str, Any]:
1291
1393
  }
1292
1394
 
1293
1395
 
1294
- def _judge_completion(request: dict[str, Any]) -> dict[str, Any]:
1295
- """Evaluate whether the agent's response indicates task completion."""
1296
- task_title = string_or_none(request.get("taskTitle")) or ""
1297
- task_description = string_or_none(request.get("taskDescription")) or ""
1298
- response_text = string_or_none(request.get("responseText")) or ""
1299
-
1300
- if not response_text:
1301
- return {"done": False, "reason": "empty response"}
1302
-
1303
- response_text = truncate_with_ellipsis(response_text, 4000)
1304
-
1305
- task_context = task_title
1306
- if task_description:
1307
- task_context += f"\n{task_description}"
1308
-
1309
- judge_system = (
1310
- "You are a task completion judge. "
1311
- "Respond ONLY with a JSON object, nothing else. "
1312
- "Do not use any tools."
1313
- )
1314
-
1315
- judge_prompt = (
1316
- f"Task:\n{task_context}\n\n"
1317
- f"Agent's most recent response:\n{response_text}\n\n"
1318
- 'Has the agent completed the task? Respond with: {"done": true/false, "reason": "one sentence"}\n'
1319
- "- done=true ONLY when the response clearly delivers the final result or confirms completion\n"
1320
- "- done=false for: questions, partial progress, clarification requests, errors, or ongoing work"
1321
- )
1322
-
1323
- text = _run_one_shot_agent("judge", judge_system, judge_prompt)
1324
- json_match = re.search(r"\{[^{}]*\}", text)
1325
- if json_match:
1326
- try:
1327
- parsed = json.loads(json_match.group())
1328
- return {
1329
- "done": bool(parsed.get("done", False)),
1330
- "reason": str(parsed.get("reason", "")),
1331
- }
1332
- except (json.JSONDecodeError, ValueError):
1333
- pass
1334
-
1335
- return {"done": False, "reason": "unparseable response"}
1336
-
1337
-
1338
1396
  def _clean_generated_title(raw: str) -> str:
1339
1397
  """Sanitize the LLM's title output: strip quotes, trailing punctuation, cap length."""
1340
1398
  stripped = raw.strip()
@@ -1417,34 +1475,50 @@ def _handle_request(request: dict[str, Any]) -> None:
1417
1475
  _result(request_id, _set_defaults(request))
1418
1476
  elif request_type == "models.list":
1419
1477
  _result(request_id, _list_models())
1420
- elif request_type == "routines.jobs.list":
1421
- _result(request_id, list_routines(bool(request.get("includeDisabled"))))
1422
- elif request_type == "routines.jobs.get":
1423
- _result(request_id, get_routine(request.get("jobId")))
1424
- elif request_type == "routines.jobs.create":
1425
- _result(request_id, create_routine(request))
1426
- elif request_type == "routines.jobs.update":
1427
- _result(request_id, update_routine(request))
1428
- elif request_type == "routines.jobs.pause":
1429
- _result(request_id, pause_routine(request.get("jobId"), request.get("reason")))
1430
- elif request_type == "routines.jobs.resume":
1431
- _result(request_id, resume_routine(request.get("jobId")))
1432
- elif request_type == "routines.jobs.run":
1433
- _result(request_id, trigger_routine(request.get("jobId")))
1434
- elif request_type == "routines.jobs.remove":
1435
- _result(request_id, remove_routine(request.get("jobId")))
1436
- elif request_type == "routines.tick":
1437
- _result(request_id, {"executed": tick_routines()})
1478
+ elif request_type == "scheduledTasks.list":
1479
+ _result(request_id, list_scheduled_tasks(bool(request.get("includeDisabled"))))
1480
+ elif request_type == "scheduledTasks.get":
1481
+ _result(request_id, get_scheduled_task(request.get("scheduledTaskId")))
1482
+ elif request_type == "scheduledTasks.create":
1483
+ _result(request_id, create_scheduled_task(request))
1484
+ elif request_type == "scheduledTasks.update":
1485
+ _result(request_id, update_scheduled_task(request))
1486
+ elif request_type == "scheduledTasks.pause":
1487
+ _result(request_id, pause_scheduled_task(request.get("scheduledTaskId"), request.get("reason")))
1488
+ elif request_type == "scheduledTasks.resume":
1489
+ _result(request_id, resume_scheduled_task(request.get("scheduledTaskId")))
1490
+ elif request_type == "scheduledTasks.run":
1491
+ _result(request_id, trigger_scheduled_task(request.get("scheduledTaskId")))
1492
+ elif request_type == "scheduledTasks.remove":
1493
+ _result(request_id, remove_scheduled_task(request.get("scheduledTaskId")))
1494
+ elif request_type == "scheduledTasks.tick":
1495
+ _result(request_id, {"executed": tick_scheduled_tasks()})
1438
1496
  elif request_type == "session.messages.get":
1439
1497
  _result(request_id, project_session_messages(request.get("sessionId"), request.get("taskId")))
1440
1498
  elif request_type == "session.get":
1441
1499
  _result(request_id, project_session_metadata(request.get("sessionId")))
1500
+ elif request_type == "goal.status":
1501
+ _result(request_id, _goal_status(request))
1502
+ elif request_type == "goal.set":
1503
+ _result(request_id, _goal_set(request))
1504
+ elif request_type == "goal.pause":
1505
+ _result(request_id, _goal_pause(request))
1506
+ elif request_type == "goal.resume":
1507
+ _result(request_id, _goal_resume(request))
1508
+ elif request_type == "goal.clear":
1509
+ _result(request_id, _goal_clear(request))
1510
+ elif request_type == "goal.evaluate":
1511
+ _submit_background_agent_request(request_id, request, name_prefix="goal", handler=_goal_evaluate)
1442
1512
  elif request_type == "chat":
1443
1513
  _submit_chat_request(request_id, request)
1444
1514
  elif request_type == "session.compress":
1445
- _submit_background_agent_request(request_id, request, name_prefix="compress", handler=_run_compress)
1446
- elif request_type == "judge.completion":
1447
- _submit_background_agent_request(request_id, request, name_prefix="judge", handler=_judge_completion)
1515
+ _submit_background_agent_request(
1516
+ request_id,
1517
+ request,
1518
+ name_prefix="compress",
1519
+ handler=_run_compress,
1520
+ task_key=_task_key_for(request),
1521
+ )
1448
1522
  elif request_type == "title.generate":
1449
1523
  _submit_background_agent_request(request_id, request, name_prefix="title", handler=_generate_title)
1450
1524
  else:
@@ -1503,7 +1577,7 @@ def main() -> int:
1503
1577
  return _self_test()
1504
1578
 
1505
1579
  sys.stdout = sys.stderr
1506
- start_routine_ticker()
1580
+ start_scheduled_task_ticker()
1507
1581
  try:
1508
1582
  _run_loop()
1509
1583
  except KeyboardInterrupt:
@@ -1,7 +1,7 @@
1
1
  """Shared utilities for the Hermes worker and its submodules.
2
2
 
3
3
  Kept intentionally small: just the error type and pure helpers that are used
4
- across `hermes_worker.py`, `hermes_sessions.py`, and `hermes_routines.py`.
4
+ across `hermes_worker.py`, `hermes_sessions.py`, and `hermes_scheduled_tasks.py`.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -6,9 +6,13 @@ export interface AppVersion {
6
6
  name: string;
7
7
  version: string;
8
8
  }
9
+ export declare const CHAT_RUN_MODES: readonly ["task", "goal"];
10
+ export type ChatRunMode = (typeof CHAT_RUN_MODES)[number];
11
+ export declare const MINIONS_GOAL_MAX_TURNS = 20;
9
12
  export interface AgentRunSettings {
10
13
  model?: string | null;
11
14
  reasoningEffort?: ReasoningEffort | null;
15
+ mode?: ChatRunMode;
12
16
  }
13
17
  export interface Task {
14
18
  id: string;
@@ -38,13 +42,16 @@ export interface ToolProgressEvent {
38
42
  duration?: number;
39
43
  label?: string;
40
44
  }
41
- export type LiveChatRunStatus = 'streaming' | 'done' | 'error';
45
+ export type TaskRunKind = 'chat' | 'goal' | 'compact';
46
+ export type LiveChatRunStatus = 'streaming' | 'compacting' | 'done' | 'error';
42
47
  export interface TaskRunState {
43
48
  taskId: string;
44
49
  runId: string;
50
+ kind: TaskRunKind;
45
51
  status: LiveChatRunStatus;
46
52
  startedAt: number;
47
53
  updatedAt: number;
54
+ goal?: GoalStateSnapshot | null;
48
55
  }
49
56
  export type BoardEvent = {
50
57
  type: 'task_created';
@@ -68,11 +75,13 @@ export type LiveChatMessage = TaskMessage & {
68
75
  export interface LiveChatRun {
69
76
  taskId: string;
70
77
  runId: string;
78
+ kind: TaskRunKind;
71
79
  sessionId: string;
72
80
  status: LiveChatRunStatus;
73
81
  startedAt: number;
74
82
  updatedAt: number;
75
83
  messages: LiveChatMessage[];
84
+ goal?: GoalStateSnapshot | null;
76
85
  context?: ContextUsage | null;
77
86
  error?: string;
78
87
  }
@@ -87,6 +96,23 @@ export interface CompactResult {
87
96
  compressedMessageCount: number;
88
97
  context?: ContextUsage | null;
89
98
  }
99
+ export interface GoalStateSnapshot {
100
+ goal: string;
101
+ status: 'active' | 'paused' | 'done' | 'cleared';
102
+ turnsUsed: number;
103
+ maxTurns: number;
104
+ lastReason?: string | null;
105
+ pausedReason?: string | null;
106
+ }
107
+ export interface GoalDecision {
108
+ status: GoalStateSnapshot['status'] | null;
109
+ shouldContinue: boolean;
110
+ continuationPrompt?: string | null;
111
+ verdict: 'done' | 'continue' | 'skipped' | 'inactive';
112
+ reason: string;
113
+ message: string;
114
+ state?: GoalStateSnapshot | null;
115
+ }
90
116
  export interface SessionMetadata {
91
117
  id: string;
92
118
  input_tokens: number;
@@ -134,14 +160,14 @@ export interface TaskAgentSettings {
134
160
  reasoningEffort: ReasoningEffort | null;
135
161
  };
136
162
  }
137
- export interface RoutineOrigin {
163
+ export interface ScheduledTaskOrigin {
138
164
  platform?: string | null;
139
165
  chat_id?: string | null;
140
166
  chat_name?: string | null;
141
167
  thread_id?: string | null;
142
168
  [key: string]: unknown;
143
169
  }
144
- export interface Routine {
170
+ export interface ScheduledTask {
145
171
  id: string;
146
172
  name: string;
147
173
  prompt: string | null;
@@ -151,38 +177,38 @@ export interface Routine {
151
177
  state: string | null;
152
178
  nextRunAt: string | null;
153
179
  lastRunAt: string | null;
154
- lastStatus: RoutineStatus | null;
180
+ lastStatus: ScheduledTaskStatus | null;
155
181
  lastError: string | null;
156
182
  lastDeliveryError: string | null;
157
183
  model: string | null;
158
184
  provider: string | null;
159
185
  baseUrl: string | null;
160
186
  deliver: string | null;
161
- origin: RoutineOrigin | null;
162
- repeat: RoutineRepeat | null;
187
+ origin: ScheduledTaskOrigin | null;
188
+ repeat: ScheduledTaskRepeat | null;
163
189
  contextFrom: string[];
164
190
  skills: string[];
165
191
  workdir: string | null;
166
192
  createdAt: string | null;
167
193
  }
168
- export type RoutineStatus = 'ok' | 'error' | 'unknown';
169
- export interface RoutineRepeat {
194
+ export type ScheduledTaskStatus = 'ok' | 'error' | 'unknown';
195
+ export interface ScheduledTaskRepeat {
170
196
  times: number | null;
171
197
  completed: number;
172
198
  }
173
- export interface RoutineRun {
199
+ export interface ScheduledTaskRun {
174
200
  id: string;
175
- jobId: string;
201
+ scheduledTaskId: string;
176
202
  ranAt: string | null;
177
203
  path: string;
178
- status: RoutineStatus;
204
+ status: ScheduledTaskStatus;
179
205
  preview: string;
180
206
  }
181
- export interface RoutineRunContent {
207
+ export interface ScheduledTaskRunContent {
182
208
  body: string;
183
- status: RoutineStatus;
209
+ status: ScheduledTaskStatus;
184
210
  }
185
- export interface RoutineInput {
211
+ export interface ScheduledTaskInput {
186
212
  name?: string;
187
213
  prompt: string;
188
214
  schedule: string;
@@ -1,2 +1,4 @@
1
1
  export const TASK_STATUSES = ['in_progress', 'in_review', 'done'];
2
2
  export const REASONING_EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'];
3
+ export const CHAT_RUN_MODES = ['task', 'goal'];
4
+ export const MINIONS_GOAL_MAX_TURNS = 20;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minionsai",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Mission Control for Hermes Agent — a Kanban board for autonomous agent tasks",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -50,6 +50,7 @@
50
50
  "react-dom": "^19.0.0",
51
51
  "react-router": "^7.14.2",
52
52
  "react-router-dom": "^7.14.2",
53
+ "sonner": "^2.0.7",
53
54
  "streamdown": "^2.4.0",
54
55
  "uuid": "^11.0.5",
55
56
  "zustand": "^5.0.12"