ai-runtime-engine 2.7.0 → 2.9.0

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.
@@ -13,13 +13,17 @@ import { actionCapabilityRows, renderActionCapabilities, renderCapabilityGaps }
13
13
  import { renderMcpList, renderMcpDetail } from '../commands/mcp.js';
14
14
  import { candidatesFrom, deriveCapabilitiesOffline } from '../../runtime/planning/deriveCapabilities.js';
15
15
  import { displaySafe } from '../render.js';
16
+ import { AGENT_TERMINAL } from '../../agents/task.js';
17
+ import { TERMINAL as EXEC_TERMINAL } from '../../executions/execution.js';
16
18
  /** Top-level slash commands, for REPL tab-completion (Phase 21b). Kept in sync with the `handle` dispatch. */
17
19
  export const SLASH_COMMANDS = [
18
20
  'help', 'status', 'info', 'doctor', 'cleanup', 'mode', 'compare', 'models', 'config', 'providers', 'tools', 'capabilities', 'mcp',
19
- 'skills', 'memory', 'conversations', 'executions', 'resume', 'resume-execution', 'pause', 'cancel', 'approve',
21
+ 'skills', 'memory', 'conversations', 'executions', 'agents', 'resume', 'resume-execution', 'pause', 'cancel', 'approve',
20
22
  'deny', 'learning', 'feedback', 'permissions', 'budget', 'stream', 'dry-run', 'clear', 'exit', 'quit',
21
23
  ];
22
- const HELP = [
24
+ /** Exported so a test can prove every reachable command is documented — the three touch points below
25
+ * are synced by hand, and `/agents` shipped tab-completable but absent from this list. */
26
+ export const HELP = [
23
27
  'Commands:',
24
28
  ' /help show this help',
25
29
  ' /status workspace, mode, provider count',
@@ -49,6 +53,8 @@ const HELP = [
49
53
  ' /conversations list recent conversations',
50
54
  ' /resume <id> resume a conversation',
51
55
  ' /executions list persisted executions',
56
+ ' /agents list agent tasks (state, progress, spend, findings)',
57
+ ' /agents stop <id> stop one agent task',
52
58
  ' /resume-execution <id> resume an execution',
53
59
  ' /approve <id> approve an execution waiting for approval, then continue',
54
60
  ' /deny <id> deny an execution waiting for approval (cancels it)',
@@ -59,7 +65,7 @@ const HELP = [
59
65
  ' /stream toggle token-by-token streaming of answers',
60
66
  ' /budget show the call/cost budget (AI_MAX_CALLS / AI_MAX_COST_USD)',
61
67
  ' /clear clear the screen',
62
- ' /exit leave the session',
68
+ ' /exit, /quit leave the session',
63
69
  '',
64
70
  'Anything else is sent to the runtime as a request.',
65
71
  ];
@@ -209,6 +215,8 @@ export class ReplSession {
209
215
  return this.resume(args[0]);
210
216
  case 'executions':
211
217
  return this.executionsList();
218
+ case 'agents':
219
+ return args[0] === 'stop' ? this.agentStop(args[1]) : this.agentsList();
212
220
  case 'resume-execution':
213
221
  return this.resumeExecution(args[0]);
214
222
  case 'approve':
@@ -327,6 +335,32 @@ export class ReplSession {
327
335
  return { lines: ['no conversations yet.'] };
328
336
  return { lines: ['Recent conversations:', ...list.map((c, i) => ` ${i + 1}. ${c.id} ${c.title} (${c.turns} turns)`)] };
329
337
  }
338
+ /** Agent tasks across this project's executions, newest first. */
339
+ agentsList() {
340
+ if (!this.runtime.executionStore.enabled)
341
+ return { lines: ['agent tasks are not persisted (stateless mode).'] };
342
+ const tasks = this.runtime.agentTasks().slice(0, 12);
343
+ if (!tasks.length)
344
+ return { lines: ['no agent tasks yet.', '(agents run when `runtime.agents.enabled` is set and a plan delegates to one)'] };
345
+ // A waiting task's question goes on its own line: it is the one thing the user must read to act.
346
+ const lines = tasks.flatMap((t) => {
347
+ const steps = `${t.innerSteps.succeeded}/${t.innerSteps.total}`;
348
+ const spend = `${t.callsUsed}/${t.callsReserved} call(s), ${t.toolCallsUsed} tool call(s)`;
349
+ const why = t.interruption ? ` (${t.interruption.kind})` : '';
350
+ // `stepId` is planner-authored and `question` is model-authored: both go through displaySafe,
351
+ // like every other untrusted string this file renders.
352
+ const row = ` ${t.agentTaskId} [${t.state}] ${t.agentId} @ ${displaySafe(t.stepId, 40)} ${steps} inner step(s), ${spend}, ${t.findings} finding(s)${why}`;
353
+ return t.question ? [row, ` ? ${displaySafe(t.question, 200)}`] : [row];
354
+ });
355
+ return { lines: ['Agent tasks:', ...lines, '', 'stop one with /agents stop <agent-task-id>'] };
356
+ }
357
+ /** Stop one agent task. Every outcome is reported — a stop that looks like nothing happened is a bug. */
358
+ agentStop(id) {
359
+ if (!id)
360
+ return { lines: ['usage: /agents stop <agent-task-id>'] };
361
+ const r = this.runtime.stopAgentTask(id);
362
+ return { lines: [`${r.ok ? 'stopped' : 'not stopped'} ${id}: ${r.reason}${r.state ? ` (state: ${r.state})` : ''}`] };
363
+ }
330
364
  executionsList() {
331
365
  if (!this.runtime.executionStore.enabled)
332
366
  return { lines: ['executions are disabled (stateless mode).'] };
@@ -362,13 +396,31 @@ export class ReplSession {
362
396
  status() {
363
397
  const ws = this.runtime.workspaceInfo();
364
398
  const providers = this.runtime.ai.providers();
365
- return {
366
- lines: [
367
- `workspace: ${ws?.name ?? '(none)'}${ws?.git.branch ? ` @ ${ws.git.branch}` : ''}`,
368
- `mode: ${this.mode}`,
369
- `providers: ${providers.length} configured (${providers.filter((p) => p.enabled).length} enabled)`,
370
- ],
371
- };
399
+ const lines = [
400
+ `workspace: ${ws?.name ?? '(none)'}${ws?.git.branch ? ` @ ${ws.git.branch}` : ''}`,
401
+ `mode: ${this.mode}`,
402
+ `providers: ${providers.length} configured (${providers.filter((p) => p.enabled).length} enabled)`,
403
+ ];
404
+ // Phase 3.6: agent work is the one thing that can be UNFINISHED and invisible — a task left waiting
405
+ // or interrupted holds its execution up, so /status names it rather than leaving the user to think
406
+ // to run /agents. Absent entirely when nothing has ever delegated, so the flag-off output is
407
+ // unchanged.
408
+ // Only executions that are themselves still going: a non-terminal task on a finished execution is
409
+ // stale bookkeeping, and reporting it as "unfinished work" forever would train the user to ignore
410
+ // this line — which is the one line that has to be trusted when something IS waiting.
411
+ const live = this.runtime.executionStore.enabled ? this.runtime.executions().filter((e) => !EXEC_TERMINAL.has(e.status)) : [];
412
+ const tasks = live.flatMap((e) => this.runtime.agentTasks(e.id));
413
+ if (tasks.length) {
414
+ const unfinished = tasks.filter((t) => !AGENT_TERMINAL.has(t.state));
415
+ const waiting = unfinished.filter((t) => t.state === 'waiting_for_clarification' || t.state === 'waiting_for_input');
416
+ lines.push(`agents: ${tasks.length} task(s), ${unfinished.length} unfinished${waiting.length ? `, ${waiting.length} waiting for an answer` : ''}`);
417
+ for (const t of unfinished.slice(0, 3)) {
418
+ lines.push(` ${t.agentTaskId} [${t.state}] ${displaySafe(t.agentId, 24)} @ ${displaySafe(t.stepId, 16)} ${t.innerSteps.succeeded}/${t.innerSteps.total} step(s)`);
419
+ }
420
+ if (unfinished.length > 3)
421
+ lines.push(` … ${unfinished.length - 3} more (/agents)`);
422
+ }
423
+ return { lines };
372
424
  }
373
425
  setOrShowMode(next) {
374
426
  if (!next)