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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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
|
|
139
|
+
raise WorkerError("No scheduled task updates were provided.", code="bad_request")
|
|
140
140
|
return updates
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
def
|
|
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 = [
|
|
148
|
-
return {"
|
|
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
|
|
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 =
|
|
156
|
-
return {"
|
|
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
|
|
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 {"
|
|
179
|
+
return {"scheduledTask": _normalize_scheduled_task(job)}
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def
|
|
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("
|
|
186
|
+
job_id = _validate_path_segment(request.get("scheduledTaskId"), "Scheduled task ID")
|
|
187
187
|
try:
|
|
188
|
-
job =
|
|
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 {"
|
|
191
|
+
return {"scheduledTask": job}
|
|
192
192
|
|
|
193
193
|
|
|
194
|
-
def
|
|
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
|
-
|
|
199
|
-
return {"
|
|
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
|
|
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 {"
|
|
206
|
+
return {"scheduledTask": _normalize_scheduled_task(resume_job(_validate_path_segment(job_id, "Scheduled task ID")))}
|
|
207
207
|
|
|
208
208
|
|
|
209
|
-
def
|
|
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
|
-
|
|
214
|
-
job =
|
|
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 {"
|
|
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
|
-
|
|
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
|
|
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-
|
|
234
|
+
threading.Thread(target=_run, name="hermes-scheduled-tasks-trigger", daemon=True).start()
|
|
235
235
|
|
|
236
236
|
|
|
237
|
-
def
|
|
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, "
|
|
241
|
+
return {"ok": bool(remove_job(_validate_path_segment(job_id, "Scheduled task ID")))}
|
|
242
242
|
|
|
243
243
|
|
|
244
|
-
def
|
|
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
|
|
251
|
+
def _scheduled_tasks_ticker_loop() -> None:
|
|
252
252
|
while True:
|
|
253
253
|
try:
|
|
254
|
-
executed =
|
|
254
|
+
executed = tick_scheduled_tasks()
|
|
255
255
|
if executed:
|
|
256
|
-
print(f"[hermes-worker]
|
|
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]
|
|
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
|
|
263
|
-
global
|
|
264
|
-
with
|
|
265
|
-
if
|
|
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=
|
|
267
|
+
thread = threading.Thread(target=_scheduled_tasks_ticker_loop, name="hermes-scheduled-tasks-ticker", daemon=True)
|
|
268
268
|
thread.start()
|
|
269
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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 == "
|
|
1421
|
-
_result(request_id,
|
|
1422
|
-
elif request_type == "
|
|
1423
|
-
_result(request_id,
|
|
1424
|
-
elif request_type == "
|
|
1425
|
-
_result(request_id,
|
|
1426
|
-
elif request_type == "
|
|
1427
|
-
_result(request_id,
|
|
1428
|
-
elif request_type == "
|
|
1429
|
-
_result(request_id,
|
|
1430
|
-
elif request_type == "
|
|
1431
|
-
_result(request_id,
|
|
1432
|
-
elif request_type == "
|
|
1433
|
-
_result(request_id,
|
|
1434
|
-
elif request_type == "
|
|
1435
|
-
_result(request_id,
|
|
1436
|
-
elif request_type == "
|
|
1437
|
-
_result(request_id, {"executed":
|
|
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(
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
162
|
-
repeat:
|
|
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
|
|
169
|
-
export interface
|
|
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
|
|
199
|
+
export interface ScheduledTaskRun {
|
|
174
200
|
id: string;
|
|
175
|
-
|
|
201
|
+
scheduledTaskId: string;
|
|
176
202
|
ranAt: string | null;
|
|
177
203
|
path: string;
|
|
178
|
-
status:
|
|
204
|
+
status: ScheduledTaskStatus;
|
|
179
205
|
preview: string;
|
|
180
206
|
}
|
|
181
|
-
export interface
|
|
207
|
+
export interface ScheduledTaskRunContent {
|
|
182
208
|
body: string;
|
|
183
|
-
status:
|
|
209
|
+
status: ScheduledTaskStatus;
|
|
184
210
|
}
|
|
185
|
-
export interface
|
|
211
|
+
export interface ScheduledTaskInput {
|
|
186
212
|
name?: string;
|
|
187
213
|
prompt: string;
|
|
188
214
|
schedule: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minionsai",
|
|
3
|
-
"version": "0.1.
|
|
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"
|