minionsai 0.1.13 → 0.1.14

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.
@@ -8,8 +8,8 @@
8
8
  <script>
9
9
  (function(){var t=localStorage.getItem('theme');var d=t==='dark'||(!t||t==='system')&&window.matchMedia('(prefers-color-scheme: dark)').matches;if(d)document.documentElement.classList.add('dark')})();
10
10
  </script>
11
- <script type="module" crossorigin src="/assets/index-o62nNllV.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-BP1qiwyD.css">
11
+ <script type="module" crossorigin src="/assets/index-DdbCvYan.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-CAVFyzK8.css">
13
13
  </head>
14
14
  <body class="bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100">
15
15
  <div id="root"></div>
@@ -9,6 +9,7 @@ export declare class HermesWorkerAdapter implements AgentAdapter {
9
9
  sessionId: string;
10
10
  }>;
11
11
  chatStream(sessionId: string, message: string, options?: AgentRunOptions): AsyncIterable<StreamEvent>;
12
+ interruptChat(sessionId: string, reason?: string): Promise<boolean>;
12
13
  healthCheck(): Promise<boolean>;
13
14
  getMessages(sessionId: string, taskId: string): Promise<TaskMessage[]>;
14
15
  getSessionMetadata(sessionId: string): Promise<SessionMetadata | null>;
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { randomUUID } from 'node:crypto';
7
7
  import { expandHomePrefix, resolveHermesHome, resolveMinionsWorkspaceDir } from '../paths.js';
8
8
  const WORKER_READY_TIMEOUT_MS = 10_000;
9
+ const WORKER_INTERRUPT_TIMEOUT_MS = 10_000;
9
10
  function resolveAgentDirFromHermesCli() {
10
11
  try {
11
12
  const hermesBin = execFileSync('which', ['hermes'], { encoding: 'utf8' }).trim();
@@ -191,13 +192,13 @@ class HermesWorkerClient {
191
192
  child.kill(signal);
192
193
  });
193
194
  }
194
- async request(input) {
195
+ async request(input, timeoutMs) {
195
196
  await this.start();
196
197
  const id = randomUUID();
197
198
  const request = typeof input === 'string'
198
199
  ? { id, type: input }
199
200
  : { ...input, id };
200
- return await this.sendRequest(request);
201
+ return await this.sendRequest(request, timeoutMs);
201
202
  }
202
203
  async *stream(request) {
203
204
  await this.start();
@@ -243,7 +244,7 @@ class HermesWorkerClient {
243
244
  if (timeoutMs) {
244
245
  timeout = setTimeout(() => {
245
246
  this.pending.delete(request.id);
246
- reject(new Error(`Hermes worker did not become ready within ${timeoutMs}ms`));
247
+ reject(new Error(`Hermes worker did not respond within ${timeoutMs}ms`));
247
248
  }, timeoutMs);
248
249
  }
249
250
  try {
@@ -398,13 +399,22 @@ export class HermesWorkerAdapter {
398
399
  yield { type: 'error', error: formatWorkerError(event.error), code: workerErrorCode(event.error) };
399
400
  break;
400
401
  case 'done':
401
- yield { type: 'done', sessionId: event.sessionId ?? sessionId, context: event.context };
402
+ yield { type: 'done', sessionId: event.sessionId ?? sessionId, context: event.context, interrupted: event.interrupted };
402
403
  break;
403
404
  case 'result':
404
405
  break;
405
406
  }
406
407
  }
407
408
  }
409
+ async interruptChat(sessionId, reason) {
410
+ const result = await this.client.request({
411
+ type: 'chat.interrupt',
412
+ sessionId,
413
+ taskId: sessionId,
414
+ reason,
415
+ }, WORKER_INTERRUPT_TIMEOUT_MS);
416
+ return result.interrupted;
417
+ }
408
418
  async healthCheck() {
409
419
  try {
410
420
  await this.client.start();
@@ -19,6 +19,7 @@ export interface StreamEvent {
19
19
  duration?: number;
20
20
  label?: string;
21
21
  context?: ContextUsage | null;
22
+ interrupted?: boolean;
22
23
  }
23
24
  export interface AgentAdapter {
24
25
  chat(sessionId: string, message: string, options?: AgentRunOptions): Promise<{
@@ -26,6 +27,7 @@ export interface AgentAdapter {
26
27
  sessionId: string;
27
28
  }>;
28
29
  chatStream(sessionId: string, message: string, options?: AgentRunOptions): AsyncIterable<StreamEvent>;
30
+ interruptChat(sessionId: string, reason?: string): Promise<boolean>;
29
31
  healthCheck(): Promise<boolean>;
30
32
  getMessages(sessionId: string, taskId: string): Promise<TaskMessage[]>;
31
33
  getSessionMetadata(sessionId: string): Promise<SessionMetadata | null>;
@@ -87,6 +87,12 @@ export type WorkerRequest = {
87
87
  type: 'goal.evaluate';
88
88
  sessionId: string;
89
89
  responseText: string;
90
+ } | {
91
+ id: string;
92
+ type: 'chat.interrupt';
93
+ taskId?: string;
94
+ sessionId?: string;
95
+ reason?: string;
90
96
  } | {
91
97
  id: string;
92
98
  type: 'chat';
@@ -132,6 +138,8 @@ export type WorkerResult = {
132
138
  goal: GoalStateSnapshot | null;
133
139
  } | {
134
140
  cleared: boolean;
141
+ } | {
142
+ interrupted: boolean;
135
143
  } | GoalDecision | {
136
144
  title: string;
137
145
  } | {
@@ -165,6 +173,7 @@ export type WorkerEvent = {
165
173
  type: 'done';
166
174
  sessionId?: string;
167
175
  context?: ContextUsage | null;
176
+ interrupted?: boolean;
168
177
  } | {
169
178
  id: string;
170
179
  type: 'error';
@@ -4,13 +4,17 @@ import { once } from 'node:events';
4
4
  import { createServer } from 'node:http';
5
5
  import app, { adapter } from './app.js';
6
6
  import { mountFrontend } from './frontend.js';
7
- import { ensureBundledSkillsLinked } from './skills/catalog.js';
7
+ import { ensureHermesExternalSkillsDir } from './routes/skills.js';
8
8
  const PORT = parseInt(process.env.PORT || '6969', 10);
9
9
  const httpServer = createServer(app);
10
10
  let closeFrontend = () => { };
11
11
  let shuttingDown = false;
12
12
  async function main() {
13
- ensureBundledSkillsLinked();
13
+ // Re-register the skills dir with Hermes every boot, so installed skills stay
14
+ // visible even if config.yaml was regenerated or skills were restored on disk.
15
+ await ensureHermesExternalSkillsDir().catch((error) => {
16
+ console.error('Failed to register skills directory with Hermes — installed skills may not load:', error instanceof Error ? error.message : error);
17
+ });
14
18
  closeFrontend = await mountFrontend(app, httpServer);
15
19
  try {
16
20
  await adapter.start();
@@ -22,7 +22,7 @@ export declare function getRun(taskId: string): LiveChatRun | undefined;
22
22
  export declare function getRunContext(taskId: string): LiveChatRun['context'] | undefined;
23
23
  export declare function getRunStatus(taskId: string): TaskRunState | undefined;
24
24
  export declare function getRunStatuses(): TaskRunState[];
25
- export declare function updateRunStatus(taskId: string, status: Extract<LiveChatRunStatus, 'done' | 'error'>, options?: {
25
+ export declare function updateRunStatus(taskId: string, status: Extract<LiveChatRunStatus, 'done' | 'error' | 'stopped'>, options?: {
26
26
  context?: LiveChatRun['context'];
27
27
  error?: string;
28
28
  }): TaskRunState | undefined;
@@ -201,7 +201,7 @@ export function applyEvent(taskId, event) {
201
201
  }
202
202
  else if (event.type === 'done') {
203
203
  if (run.status !== 'error')
204
- run.status = 'done';
204
+ run.status = event.interrupted ? 'stopped' : 'done';
205
205
  if (event.sessionId)
206
206
  run.sessionId = event.sessionId;
207
207
  if (event.context !== undefined) {
@@ -4,5 +4,6 @@ export declare function resolveMinionsHome(): string;
4
4
  export declare function resolveMinionsDataDir(): string;
5
5
  export declare function resolveMinionsLogsDir(): string;
6
6
  export declare function resolveMinionsWorkspaceDir(): string;
7
+ export declare function resolveMinionsSkillsDir(): string;
7
8
  export declare function resolveMinionsDbPath(): string;
8
9
  export declare function ensureMinionsStateDirs(): void;
@@ -28,6 +28,9 @@ export function resolveMinionsLogsDir() {
28
28
  export function resolveMinionsWorkspaceDir() {
29
29
  return join(resolveMinionsHome(), 'workspace');
30
30
  }
31
+ export function resolveMinionsSkillsDir() {
32
+ return join(resolveMinionsHome(), 'skills');
33
+ }
31
34
  export function resolveMinionsDbPath() {
32
35
  const configured = process.env.DB_PATH?.trim();
33
36
  if (configured)
@@ -39,5 +42,6 @@ export function ensureMinionsStateDirs() {
39
42
  mkdirSync(resolveMinionsDataDir(), { recursive: true });
40
43
  mkdirSync(resolveMinionsLogsDir(), { recursive: true });
41
44
  mkdirSync(resolveMinionsWorkspaceDir(), { recursive: true });
45
+ mkdirSync(resolveMinionsSkillsDir(), { recursive: true });
42
46
  mkdirSync(dirname(dbPath), { recursive: true });
43
47
  }
@@ -1 +1 @@
1
- export declare const TASK_AGENT_SYSTEM_PROMPT = "<task_agent>\n <role>\n You are an autonomous task agent. A user has given you a task to accomplish.\n </role>\n\n <responsibilities>\n <responsibility name=\"understand\">\n Read the task carefully. Identify anything unclear, ambiguous, or underspecified.\n </responsibility>\n <responsibility name=\"clarify\">\n Before doing work, make sure you fully understand what the user wants. Ask focused clarifying questions about scope, constraints, expected outcomes, edge cases, or anything else you are uncertain about. Do not assume when the uncertainty matters. The user is available to answer. Keep asking until you are confident you understand the task correctly.\n </responsibility>\n <responsibility name=\"execute\">\n Once you and the user are aligned, choose the best execution strategy. Do the work yourself in this session if it is straightforward. Create a child session if you need a dedicated sub-agent for complex sub-work. Set up a cron job when the work is recurring, periodic, scheduled, or better handled as durable batches over time. You have full autonomy to use the tools and approach that best accomplish the task.\n </responsibility>\n </responsibilities>\n\n <guidelines>\n <guideline>Understand first, act second. Do not start executing until you are confident you know what the user wants.</guideline>\n <guideline>When clarifying, ask focused questions rather than a long wall of questions. A natural back-and-forth conversation is ideal.</guideline>\n <guideline>When the user asks for a cron job, schedule, scheduled task, recurring task, monitor, daily/weekly task, or similar repeated work, default to a Hermes cron job using the available cronjob tooling. Do not use Linux cron, systemd timers, or host OS schedulers unless the user explicitly asks for them.</guideline>\n <guideline>For lead generation, prospecting, and large list processing, prefer a small sample or validation run first. If the user wants more than a small one-off result, prefer a cron job with a self-contained prompt, sensible batch size, output/checkpoint location, and schedule.</guideline>\n <guideline>Keep the user informed of meaningful progress in your responses.</guideline>\n <guideline>You have project-specific skills under the \"minions\" category in your skills index. Before executing a task, check if any minions skill is relevant and load it \u2014 these encode proven workflows tailored to this system.</guideline>\n </guidelines>\n</task_agent>";
1
+ export declare const TASK_AGENT_SYSTEM_PROMPT = "<task_agent>\n <role>\n You are an autonomous task agent. A user has given you a task to accomplish.\n </role>\n\n <responsibilities>\n <responsibility name=\"understand\">\n Read the task carefully. Identify anything unclear, ambiguous, or underspecified.\n </responsibility>\n <responsibility name=\"clarify\">\n Before doing work, make sure you fully understand what the user wants. Ask focused clarifying questions about scope, constraints, expected outcomes, edge cases, or anything else you are uncertain about. Do not assume when the uncertainty matters. The user is available to answer. Keep asking until you are confident you understand the task correctly.\n </responsibility>\n <responsibility name=\"execute\">\n Once you and the user are aligned, choose the best execution strategy. Do the work yourself in this session if it is straightforward. Create a child session if you need a dedicated sub-agent for complex sub-work. Set up a cron job when the work is recurring, periodic, scheduled, or better handled as durable batches over time. You have full autonomy to use the tools and approach that best accomplish the task.\n </responsibility>\n </responsibilities>\n\n <guidelines>\n <guideline>Understand first, act second. Do not start executing until you are confident you know what the user wants.</guideline>\n <guideline>When clarifying, ask focused questions rather than a long wall of questions. A natural back-and-forth conversation is ideal.</guideline>\n <guideline>When the user asks for a cron job, schedule, scheduled task, recurring task, monitor, daily/weekly task, or similar repeated work, default to a Hermes cron job using the available cronjob tooling. Do not use Linux cron, systemd timers, or host OS schedulers unless the user explicitly asks for them.</guideline>\n <guideline>For lead generation, prospecting, data collection, and other large list-processing work, start with a small sample or validation run. Choose useful columns, write results to a local CSV file when tabular output is valuable, and continue from the same file instead of starting over.</guideline>\n <guideline>If list-processing work is larger than a small one-off result, prefer a Hermes cron job with a self-contained prompt, sensible batch size, CSV/checkpoint path, and schedule so the work can continue durably over time.</guideline>\n <guideline>Keep the user informed of meaningful progress in your responses.</guideline>\n </guidelines>\n</task_agent>";
@@ -19,8 +19,8 @@ export const TASK_AGENT_SYSTEM_PROMPT = `<task_agent>
19
19
  <guideline>Understand first, act second. Do not start executing until you are confident you know what the user wants.</guideline>
20
20
  <guideline>When clarifying, ask focused questions rather than a long wall of questions. A natural back-and-forth conversation is ideal.</guideline>
21
21
  <guideline>When the user asks for a cron job, schedule, scheduled task, recurring task, monitor, daily/weekly task, or similar repeated work, default to a Hermes cron job using the available cronjob tooling. Do not use Linux cron, systemd timers, or host OS schedulers unless the user explicitly asks for them.</guideline>
22
- <guideline>For lead generation, prospecting, and large list processing, prefer a small sample or validation run first. If the user wants more than a small one-off result, prefer a cron job with a self-contained prompt, sensible batch size, output/checkpoint location, and schedule.</guideline>
22
+ <guideline>For lead generation, prospecting, data collection, and other large list-processing work, start with a small sample or validation run. Choose useful columns, write results to a local CSV file when tabular output is valuable, and continue from the same file instead of starting over.</guideline>
23
+ <guideline>If list-processing work is larger than a small one-off result, prefer a Hermes cron job with a self-contained prompt, sensible batch size, CSV/checkpoint path, and schedule so the work can continue durably over time.</guideline>
23
24
  <guideline>Keep the user informed of meaningful progress in your responses.</guideline>
24
- <guideline>You have project-specific skills under the "minions" category in your skills index. Before executing a task, check if any minions skill is relevant and load it — these encode proven workflows tailored to this system.</guideline>
25
25
  </guidelines>
26
26
  </task_agent>`;
@@ -16,6 +16,9 @@ function hasNoSession(task) {
16
16
  function isTaskRunActive(status) {
17
17
  return status?.status === 'streaming' || status?.status === 'compacting';
18
18
  }
19
+ function isInterruptibleRun(status) {
20
+ return status?.status === 'streaming' && (status.kind === 'chat' || status.kind === 'goal');
21
+ }
19
22
  function completeTaskRun(taskId, runId, status, ttlMs, options) {
20
23
  const updated = updateRunStatus(taskId, status, options);
21
24
  if (updated) {
@@ -96,6 +99,7 @@ async function streamChatTurn(runTask, sessionId, content, options) {
96
99
  let doneContext;
97
100
  let responseText = '';
98
101
  let hadError = false;
102
+ let interrupted = false;
99
103
  try {
100
104
  const stream = adapter.chatStream(sessionId, content, {
101
105
  systemMessage: TASK_AGENT_SYSTEM_PROMPT,
@@ -109,6 +113,8 @@ async function streamChatTurn(runTask, sessionId, content, options) {
109
113
  if (event.type === 'done') {
110
114
  sawDone = true;
111
115
  doneContext = event.context;
116
+ if (event.interrupted)
117
+ interrupted = true;
112
118
  if (!options.completeOnDone) {
113
119
  updateRunContext(runTask.id, event.context, event.sessionId);
114
120
  continue;
@@ -142,7 +148,7 @@ async function streamChatTurn(runTask, sessionId, content, options) {
142
148
  broadcastLive(runTask.id, event);
143
149
  }
144
150
  }
145
- return { responseText, sawDone, context: doneContext, hadError };
151
+ return { responseText, sawDone, context: doneContext, hadError, interrupted };
146
152
  }
147
153
  async function consumeChatRun(runTask, sessionId, content, runId) {
148
154
  const result = await streamChatTurn(runTask, sessionId, content, { completeOnDone: true });
@@ -156,13 +162,13 @@ async function consumeChatRun(runTask, sessionId, content, runId) {
156
162
  async function consumeGoalRun(runTask, sessionId, initialContent, runId) {
157
163
  let finalContext;
158
164
  let hadError = false;
165
+ let wasInterrupted = false;
159
166
  let turnContent = initialContent;
160
167
  let turnCount = 0;
161
168
  try {
162
169
  while (turnContent) {
163
170
  if (++turnCount > MINIONS_GOAL_MAX_TURNS) {
164
171
  appendSystemMessage(runTask.id, 'Goal turn limit reached');
165
- broadcastRunSnapshot(runTask.id);
166
172
  break;
167
173
  }
168
174
  appendUserMessage(runTask.id, turnContent);
@@ -178,6 +184,10 @@ async function consumeGoalRun(runTask, sessionId, initialContent, runId) {
178
184
  hadError = true;
179
185
  break;
180
186
  }
187
+ if (turn.interrupted) {
188
+ wasInterrupted = true;
189
+ break;
190
+ }
181
191
  const decision = await adapter.evaluateGoal(sessionId, turn.responseText);
182
192
  let shouldBroadcastSnapshot = false;
183
193
  if (decision.state) {
@@ -205,8 +215,13 @@ async function consumeGoalRun(runTask, sessionId, initialContent, runId) {
205
215
  }
206
216
  finally {
207
217
  if (!hadError && getRunStatus(runTask.id)?.status === 'streaming') {
208
- updateRunStatus(runTask.id, 'done', { context: finalContext ?? null });
218
+ updateRunStatus(runTask.id, wasInterrupted ? 'stopped' : 'done', { context: finalContext ?? null });
209
219
  }
220
+ // Goal-turn `done` events are swallowed (completeOnDone=false), so the live
221
+ // channel never sees the terminal status — push a final snapshot for it. The
222
+ // error path already delivered a terminal `error` event, so skip it there.
223
+ if (!hadError)
224
+ broadcastRunSnapshot(runTask.id);
210
225
  settleRun(runTask.id, runId, finalContext ?? null);
211
226
  }
212
227
  }
@@ -273,6 +288,27 @@ chatRouter.post('/:id/messages', async (req, res) => {
273
288
  void consumeChatRun(runTask, sessionId, content, snapshot.runId);
274
289
  res.status(202).json({ runId: snapshot.runId });
275
290
  });
291
+ chatRouter.post('/:id/interrupt', async (req, res) => {
292
+ const task = getTask(req.params.id);
293
+ if (!task)
294
+ return res.status(404).json({ error: 'Task not found' });
295
+ if (!isInterruptibleRun(getRunStatus(task.id))) {
296
+ return res.status(409).json({ error: 'This task has no active message to stop' });
297
+ }
298
+ const reason = typeof req.body?.reason === 'string' && req.body.reason.trim()
299
+ ? req.body.reason.trim()
300
+ : undefined;
301
+ try {
302
+ const interrupted = await adapter.interruptChat(task.id, reason);
303
+ if (!interrupted) {
304
+ return res.status(409).json({ error: 'Hermes had no active agent to stop for this task' });
305
+ }
306
+ res.json({ interrupted: true });
307
+ }
308
+ catch (error) {
309
+ res.status(503).json({ error: toErrorMessage(error, 'Could not stop Hermes run') });
310
+ }
311
+ });
276
312
  chatRouter.post('/:id/compact', async (req, res) => {
277
313
  const task = getTask(req.params.id);
278
314
  if (!task)
@@ -1 +1,2 @@
1
1
  export declare const skillsRouter: import("express-serve-static-core").Router;
2
+ export declare function ensureHermesExternalSkillsDir(): Promise<void>;