mu-harness 0.16.6 → 0.16.8

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 (53) hide show
  1. package/esm/harness/npm/src/agents/index.d.ts +2 -2
  2. package/esm/harness/npm/src/agents/index.js +1 -1
  3. package/esm/harness/npm/src/agents/parser.js +18 -6
  4. package/esm/harness/npm/src/agents/registry.d.ts +3 -1
  5. package/esm/harness/npm/src/agents/registry.js +21 -0
  6. package/esm/harness/npm/src/agents/types.d.ts +3 -1
  7. package/esm/harness/npm/src/harness/create.js +18 -4
  8. package/esm/harness/npm/src/harness/types.d.ts +10 -1
  9. package/esm/harness/npm/src/permissions/approval-manager.d.ts +31 -0
  10. package/esm/harness/npm/src/permissions/approval-manager.js +55 -0
  11. package/esm/harness/npm/src/permissions/index.d.ts +1 -0
  12. package/esm/harness/npm/src/permissions/index.js +1 -0
  13. package/esm/harness/npm/src/plugin/import-ts.js +1 -1
  14. package/esm/harness/npm/src/scheduler/command.js +1 -1
  15. package/esm/harness/npm/src/scheduler/engine/index.d.ts +2 -1
  16. package/esm/harness/npm/src/scheduler/engine/index.js +1 -0
  17. package/esm/harness/npm/src/scheduler/engine/memory-store.d.ts +2 -0
  18. package/esm/harness/npm/src/scheduler/engine/memory-store.js +23 -0
  19. package/esm/harness/npm/src/scheduler/engine/scheduler.d.ts +2 -1
  20. package/esm/harness/npm/src/scheduler/engine/scheduler.js +9 -1
  21. package/esm/harness/npm/src/scheduler/engine/types.d.ts +19 -1
  22. package/esm/harness/npm/src/scheduler/index.d.ts +2 -2
  23. package/esm/harness/npm/src/scheduler/index.js +1 -1
  24. package/esm/harness/npm/src/session/agent-session.js +4 -5
  25. package/esm/harness/npm/src/subAgents/tool.js +35 -28
  26. package/esm/tui/src/layout/ansi.js +12 -5
  27. package/package.json +3 -3
  28. package/script/harness/npm/src/agents/index.d.ts +2 -2
  29. package/script/harness/npm/src/agents/index.js +3 -1
  30. package/script/harness/npm/src/agents/parser.js +18 -6
  31. package/script/harness/npm/src/agents/registry.d.ts +3 -1
  32. package/script/harness/npm/src/agents/registry.js +24 -1
  33. package/script/harness/npm/src/agents/types.d.ts +3 -1
  34. package/script/harness/npm/src/harness/create.js +17 -3
  35. package/script/harness/npm/src/harness/types.d.ts +10 -1
  36. package/script/harness/npm/src/permissions/approval-manager.d.ts +31 -0
  37. package/script/harness/npm/src/permissions/approval-manager.js +59 -0
  38. package/script/harness/npm/src/permissions/index.d.ts +1 -0
  39. package/script/harness/npm/src/permissions/index.js +3 -1
  40. package/script/harness/npm/src/plugin/import-ts.js +3 -3
  41. package/script/harness/npm/src/scheduler/command.js +1 -1
  42. package/script/harness/npm/src/scheduler/engine/index.d.ts +2 -1
  43. package/script/harness/npm/src/scheduler/engine/index.js +3 -1
  44. package/script/harness/npm/src/scheduler/engine/memory-store.d.ts +2 -0
  45. package/script/harness/npm/src/scheduler/engine/memory-store.js +27 -0
  46. package/script/harness/npm/src/scheduler/engine/scheduler.d.ts +2 -1
  47. package/script/harness/npm/src/scheduler/engine/scheduler.js +9 -1
  48. package/script/harness/npm/src/scheduler/engine/types.d.ts +19 -1
  49. package/script/harness/npm/src/scheduler/index.d.ts +2 -2
  50. package/script/harness/npm/src/scheduler/index.js +2 -1
  51. package/script/harness/npm/src/session/agent-session.js +4 -5
  52. package/script/harness/npm/src/subAgents/tool.js +35 -28
  53. package/script/tui/src/layout/ansi.js +12 -5
@@ -1,4 +1,4 @@
1
- export type { Agent } from './types.js';
2
- export { type AgentRegistry, createAgentRegistry } from './registry.js';
1
+ export type { Agent, ToolDecision, ToolGrants } from './types.js';
2
+ export { type AgentRegistry, createAgentRegistry, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
@@ -1,3 +1,3 @@
1
- export { createAgentRegistry } from './registry.js';
1
+ export { createAgentRegistry, toolDecision, toolNames } from './registry.js';
2
2
  export { parseAgent } from './parser.js';
3
3
  export { loadAgents } from './loader.js';
@@ -1,10 +1,22 @@
1
1
  import { parseFrontmatter, str } from '../common/index.js';
2
- const parseToolList = (raw) => {
3
- if (Array.isArray(raw)) {
4
- return raw.filter((entry) => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
2
+ const DECISIONS = new Set(['allow', 'ask', 'deny']);
3
+ const parseStringList = (raw) => (Array.isArray(raw) ? raw : raw.split(','))
4
+ .filter((entry) => typeof entry === 'string')
5
+ .map((entry) => entry.trim())
6
+ .filter(Boolean);
7
+ const parseTools = (raw) => {
8
+ if (Array.isArray(raw) || typeof raw === 'string') {
9
+ const list = parseStringList(raw);
10
+ return list.length > 0 ? list : undefined;
11
+ }
12
+ if (raw && typeof raw === 'object') {
13
+ const out = {};
14
+ for (const [tool, decision] of Object.entries(raw)) {
15
+ if (typeof decision === 'string' && DECISIONS.has(decision))
16
+ out[tool] = decision;
17
+ }
18
+ return Object.keys(out).length > 0 ? out : undefined;
5
19
  }
6
- if (typeof raw === 'string')
7
- return raw.split(',').map((part) => part.trim()).filter(Boolean);
8
20
  return undefined;
9
21
  };
10
22
  export const parseAgent = (source, fallbackName) => {
@@ -13,7 +25,7 @@ export const parseAgent = (source, fallbackName) => {
13
25
  name: str(fields.name) ?? fallbackName,
14
26
  description: str(fields.description) ?? '',
15
27
  prompt: body,
16
- tools: parseToolList(fields.tools),
28
+ tools: parseTools(fields.tools),
17
29
  model: str(fields.model),
18
30
  color: str(fields.color),
19
31
  extends: str(fields.extends),
@@ -1,6 +1,8 @@
1
- import type { Agent } from './types.js';
1
+ import type { Agent, ToolDecision } from './types.js';
2
2
  export interface AgentRegistry {
3
3
  list(): Agent[];
4
4
  get(name: string): Agent | undefined;
5
5
  }
6
+ export declare const toolDecision: (agent: Agent, tool: string) => ToolDecision;
7
+ export declare const toolNames: (agent: Agent) => string[] | undefined;
6
8
  export declare const createAgentRegistry: (agents?: Agent[]) => AgentRegistry;
@@ -1,3 +1,24 @@
1
+ const asMap = (tools) => {
2
+ if (!tools)
3
+ return undefined;
4
+ if (Array.isArray(tools))
5
+ return Object.fromEntries(tools.map((tool) => [tool, 'allow']));
6
+ return tools;
7
+ };
8
+ export const toolDecision = (agent, tool) => {
9
+ const map = asMap(agent.tools);
10
+ if (!map)
11
+ return 'allow';
12
+ return map[tool] ?? map['*'] ?? 'deny';
13
+ };
14
+ export const toolNames = (agent) => {
15
+ const map = asMap(agent.tools);
16
+ if (!map)
17
+ return undefined;
18
+ if (map['*'] && map['*'] !== 'deny')
19
+ return ['*'];
20
+ return Object.entries(map).filter(([, decision]) => decision !== 'deny').map(([tool]) => tool);
21
+ };
1
22
  const merge = (base, child) => ({
2
23
  name: child.name,
3
24
  description: child.description || base.description,
@@ -1,8 +1,10 @@
1
+ export type ToolDecision = 'allow' | 'ask' | 'deny';
2
+ export type ToolGrants = string[] | Record<string, ToolDecision>;
1
3
  export interface Agent {
2
4
  name: string;
3
5
  description: string;
4
6
  prompt: string;
5
- tools?: string[];
7
+ tools?: ToolGrants;
6
8
  model?: string;
7
9
  color?: string;
8
10
  extends?: string;
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import process from 'node:process';
3
- import { createAgentRegistry, loadAgents } from '../agents/index.js';
3
+ import { createAgentRegistry, loadAgents, toolDecision, toolNames } from '../agents/index.js';
4
4
  import { createAgentsCommand, createCommandRegistry, createHelpCommand, createSessionsCommand, createSkillsCommand, } from '../commands/index.js';
5
5
  import { createHarnessConfig } from '../config/index.js';
6
6
  import { mergeHooks } from '../hooks/index.js';
@@ -18,7 +18,7 @@ const TITLE_AGENT = {
18
18
  tools: [],
19
19
  };
20
20
  export const createHarness = async (options) => {
21
- const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], title, titleModel, scheduler: enableScheduler = false, ...sessionDefaults } = options;
21
+ const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
22
22
  const cwd = options.cwd ?? process.cwd();
23
23
  const config = createHarnessConfig({ hostName, xdg });
24
24
  const models = createModelRegistry({ providers, default: model });
@@ -42,17 +42,28 @@ export const createHarness = async (options) => {
42
42
  const runs = createSubAgentRegistry();
43
43
  const store = createSessionStore({ dir: join(config.dataDir, 'sessions') });
44
44
  const sessionTools = (extra = []) => [...(sessionDefaults.tools ?? []), ...extra, ...skillTools, ...schedulerTools];
45
+ const approvalHook = (getAgent) => approvals
46
+ ? approvals.manager.hooksFor({
47
+ decide: (call) => {
48
+ const agent = getAgent();
49
+ if (!agent)
50
+ return 'allow';
51
+ return approvals.decide ? approvals.decide(agent, call) : toolDecision(agent, call.name);
52
+ },
53
+ agent: () => getAgent()?.name,
54
+ })
55
+ : undefined;
45
56
  const persona = (agent, opts) => createAgentSession({
46
57
  tools: opts.tools,
47
58
  plugins: sessionDefaults.plugins,
48
59
  system: agent.prompt,
49
- hooks: opts.hooks ?? allowList(agent.tools),
60
+ hooks: opts.hooks ?? allowList(toolNames(agent)),
50
61
  ...models.resolve(opts.model ?? agent.model),
51
62
  id: newId(),
52
63
  });
53
64
  const spawn = (agent) => persistTo(store, persona(agent, {
54
65
  tools: sessionTools(),
55
- hooks: mergeHooks([sessionDefaults.hooks, allowList(agent.tools)]),
66
+ hooks: mergeHooks([sessionDefaults.hooks, allowList(toolNames(agent)), approvalHook(() => agent)]),
56
67
  }));
57
68
  const scheduler = enableScheduler && tasks
58
69
  ? createScheduler({
@@ -61,6 +72,8 @@ export const createHarness = async (options) => {
61
72
  try {
62
73
  if (!task.agent)
63
74
  throw new Error('scheduled task is missing an agent');
75
+ if (!task.skill)
76
+ throw new Error('scheduled task is missing a skill');
64
77
  const output = await runSkill({ skills, agents, spawn, runs, parentId: task.id }, {
65
78
  skill: task.skill,
66
79
  task: task.prompt,
@@ -98,6 +111,7 @@ export const createHarness = async (options) => {
98
111
  },
99
112
  revive: ({ id, model: ref, messages }) => createAgentSession({
100
113
  ...sessionDefaults,
114
+ hooks: mergeHooks([sessionDefaults.hooks, approvalHook(() => approvals?.activeAgent())]),
101
115
  tools: sessionTools([createSubAgentTool({ registry: agents, spawn, runs, parentId: id })]),
102
116
  ...models.resolve(ref),
103
117
  id,
@@ -1,7 +1,8 @@
1
1
  import type { Provider } from 'mu-core';
2
- import type { Agent, AgentRegistry } from '../agents/index.js';
2
+ import type { Agent, AgentRegistry, ToolDecision } from '../agents/index.js';
3
3
  import type { CommandRegistry } from '../commands/index.js';
4
4
  import type { HarnessConfig, HarnessConfigOptions } from '../config/index.js';
5
+ import type { ApprovalManager } from '../permissions/index.js';
5
6
  import type { PluginStore } from '../plugin/index.js';
6
7
  import type { Scheduler, TaskStore } from '../scheduler/index.js';
7
8
  import type { AgentSessionConfig, SessionManager } from '../session/index.js';
@@ -17,6 +18,14 @@ export type HarnessOptions = HarnessConfigOptions & Omit<AgentSessionConfig, 'pr
17
18
  titleModel?: string;
18
19
  cwd?: string;
19
20
  scheduler?: boolean;
21
+ approvals?: {
22
+ manager: ApprovalManager;
23
+ activeAgent: () => Agent | undefined;
24
+ decide?: (agent: Agent, call: {
25
+ name: string;
26
+ input: unknown;
27
+ }) => ToolDecision;
28
+ };
20
29
  };
21
30
  export interface Harness {
22
31
  readonly config: HarnessConfig;
@@ -0,0 +1,31 @@
1
+ import type { AgentSessionHooks } from '../hooks/index.js';
2
+ export type ApprovalAction = 'approve' | 'approve_always' | 'deny';
3
+ export type ApprovalDecision = 'allow' | 'ask' | 'deny';
4
+ export interface PendingApproval {
5
+ id: string;
6
+ name: string;
7
+ input: unknown;
8
+ agent?: string;
9
+ }
10
+ export interface ApprovalManager {
11
+ hooks: AgentSessionHooks;
12
+ hooksFor(opts: {
13
+ decide(call: {
14
+ name: string;
15
+ input: unknown;
16
+ }): ApprovalDecision;
17
+ agent?(): string | undefined;
18
+ }): AgentSessionHooks;
19
+ pending(): PendingApproval[];
20
+ resolve(id: string, action: ApprovalAction): boolean;
21
+ subscribe(listener: (req: PendingApproval) => void): () => void;
22
+ }
23
+ export interface ApprovalManagerOptions {
24
+ needsApproval?: (call: {
25
+ name: string;
26
+ input: unknown;
27
+ }) => boolean;
28
+ askTools?: string[];
29
+ newId?: () => string;
30
+ }
31
+ export declare const createApprovalManager: (options?: ApprovalManagerOptions) => ApprovalManager;
@@ -0,0 +1,55 @@
1
+ import { requireApproval } from './approval.js';
2
+ const denied = (name) => [{ type: 'text', text: `Denied: ${name}` }];
3
+ export const createApprovalManager = (options = {}) => {
4
+ const askTools = options.askTools ? new Set(options.askTools) : undefined;
5
+ const alwaysAllow = new Set();
6
+ const newId = options.newId ?? (() => crypto.randomUUID());
7
+ const keyOf = (agent, tool) => `${agent ?? ''}:${tool}`;
8
+ const waiters = new Map();
9
+ const listeners = new Set();
10
+ const request = (id, name, input, agent) => new Promise((resolve) => {
11
+ const req = { id, name, input, agent };
12
+ waiters.set(id, { resolve, req, key: keyOf(agent, name) });
13
+ for (const listener of listeners)
14
+ listener(req);
15
+ });
16
+ const defaultNeeds = options.needsApproval ?? (({ name }) => (askTools ? askTools.has(name) : true));
17
+ const hooks = requireApproval({
18
+ needsApproval: (call) => defaultNeeds(call) && !alwaysAllow.has(keyOf(undefined, call.name)),
19
+ newId,
20
+ prompt: (call) => request(call.id, call.name, call.input, undefined),
21
+ });
22
+ const hooksFor = ({ decide, agent }) => ({
23
+ beforeToolCall: async (call) => {
24
+ const decision = decide(call);
25
+ if (decision === 'allow')
26
+ return;
27
+ const agentName = agent?.();
28
+ if (decision === 'deny')
29
+ return denied(call.name);
30
+ if (alwaysAllow.has(keyOf(agentName, call.name)))
31
+ return;
32
+ const allow = await request(newId(), call.name, call.input, agentName);
33
+ return allow ? undefined : denied(call.name);
34
+ },
35
+ });
36
+ return {
37
+ hooks,
38
+ hooksFor,
39
+ pending: () => [...waiters.values()].map((w) => w.req),
40
+ resolve: (id, action) => {
41
+ const waiter = waiters.get(id);
42
+ if (!waiter)
43
+ return false;
44
+ waiters.delete(id);
45
+ if (action === 'approve_always')
46
+ alwaysAllow.add(waiter.key);
47
+ waiter.resolve(action !== 'deny');
48
+ return true;
49
+ },
50
+ subscribe: (listener) => {
51
+ listeners.add(listener);
52
+ return () => listeners.delete(listener);
53
+ },
54
+ };
55
+ };
@@ -1,3 +1,4 @@
1
1
  export { allowList, filterTools } from './allow-list.js';
2
2
  export { type ApprovalCall, requireApproval, type RequireApprovalOptions } from './approval.js';
3
+ export { type ApprovalAction, type ApprovalDecision, type ApprovalManager, type ApprovalManagerOptions, createApprovalManager, type PendingApproval, } from './approval-manager.js';
3
4
  export { matchesAnyGlob } from './glob.js';
@@ -1,3 +1,4 @@
1
1
  export { allowList, filterTools } from './allow-list.js';
2
2
  export { requireApproval } from './approval.js';
3
+ export { createApprovalManager, } from './approval-manager.js';
3
4
  export { matchesAnyGlob } from './glob.js';
@@ -1,4 +1,3 @@
1
- import { transformSync } from '@swc/wasm-typescript';
2
1
  import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
3
2
  import { tmpdir } from 'node:os';
4
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
@@ -62,6 +61,7 @@ const commonBase = (files) => {
62
61
  const outName = (path) => path.replace(TS_EXT, '.mjs');
63
62
  const rewriteSpecifiers = (code) => code.replace(SPECIFIER, (whole, quote, spec) => whole.replace(`${quote}${spec}${quote}`, `${quote}${outName(spec)}${quote}`));
64
63
  const transpileTree = async (entry) => {
64
+ const { transformSync } = await import('@swc/wasm-typescript');
65
65
  const sources = await collect(entry);
66
66
  if (sources.size === 1) {
67
67
  const { code } = transformSync(sources.get(entry), { mode: 'transform', module: true });
@@ -12,7 +12,7 @@ export const createTasksCommand = (tasks) => ({
12
12
  ? `every ${t.schedule.ms}ms`
13
13
  : 'once';
14
14
  const state = t.enabled ? when : `${when} (disabled)`;
15
- return `- ${t.id} — ${t.skill} [${state}]`;
15
+ return `- ${t.id} — ${t.skill ?? t.agent ?? 'task'} [${state}]`;
16
16
  };
17
17
  return { ok: true, output: list.map(describe).join('\n') };
18
18
  },
@@ -1,3 +1,4 @@
1
- export type { Schedule, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './types.js';
1
+ export type { Schedule, SchedulerEvent, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './types.js';
2
2
  export { createTaskStore } from './store.js';
3
+ export { createMemoryTaskStore } from './memory-store.js';
3
4
  export { createScheduler, type Scheduler } from './scheduler.js';
@@ -1,2 +1,3 @@
1
1
  export { createTaskStore } from './store.js';
2
+ export { createMemoryTaskStore } from './memory-store.js';
2
3
  export { createScheduler } from './scheduler.js';
@@ -0,0 +1,2 @@
1
+ import type { Task, TaskStore } from './types.js';
2
+ export declare const createMemoryTaskStore: (initial?: Task[]) => TaskStore;
@@ -0,0 +1,23 @@
1
+ export const createMemoryTaskStore = (initial = []) => {
2
+ const tasks = new Map(initial.map((task) => [task.id, task]));
3
+ return {
4
+ list: async () => [...tasks.values()],
5
+ get: async (id) => tasks.get(id),
6
+ create: async (input) => {
7
+ const task = { ...input, id: crypto.randomUUID(), enabled: input.enabled ?? true, createdAt: Date.now() };
8
+ tasks.set(task.id, task);
9
+ return task;
10
+ },
11
+ update: async (id, patch) => {
12
+ const task = tasks.get(id);
13
+ if (!task)
14
+ return undefined;
15
+ const next = { ...task, ...patch, id: task.id };
16
+ tasks.set(id, next);
17
+ return next;
18
+ },
19
+ remove: async (id) => {
20
+ tasks.delete(id);
21
+ },
22
+ };
23
+ };
@@ -1,4 +1,4 @@
1
- import type { Task, TaskRunner, TaskStore } from './types.js';
1
+ import type { SchedulerEvent, Task, TaskRunner, TaskStore } from './types.js';
2
2
  export interface Scheduler {
3
3
  start(): Promise<void>;
4
4
  stop(): void;
@@ -8,5 +8,6 @@ export interface Scheduler {
8
8
  export declare const createScheduler: (deps: {
9
9
  store: TaskStore;
10
10
  run: TaskRunner;
11
+ onEvent?: (event: SchedulerEvent) => void;
11
12
  onError?: (error: unknown, task: Task) => void;
12
13
  }) => Scheduler;
@@ -8,14 +8,22 @@ export const createScheduler = (deps) => {
8
8
  const task = await store.get(id);
9
9
  if (!task || !task.enabled)
10
10
  return;
11
+ const at = Date.now();
12
+ deps.onEvent?.({ type: 'task_started', task, at });
11
13
  try {
12
14
  const result = await run(task);
13
15
  await store.update(id, { lastRun: Date.now(), lastResult: result });
16
+ const durationMs = Date.now() - at;
17
+ if (result.ok)
18
+ deps.onEvent?.({ type: 'task_completed', task, at: Date.now(), durationMs, result });
19
+ else
20
+ deps.onEvent?.({ type: 'task_failed', task, at: Date.now(), durationMs, error: result.error ?? 'task failed' });
14
21
  }
15
22
  catch (error) {
16
23
  deps.onError?.(error, task);
17
24
  const message = error instanceof Error ? error.message : String(error);
18
25
  await store.update(id, { lastRun: Date.now(), lastResult: { ok: false, error: message } });
26
+ deps.onEvent?.({ type: 'task_failed', task, at: Date.now(), durationMs: Date.now() - at, error: message });
19
27
  }
20
28
  if (task.schedule.kind === 'once')
21
29
  await store.update(id, { enabled: false });
@@ -33,7 +41,7 @@ export const createScheduler = (deps) => {
33
41
  return;
34
42
  const s = task.schedule;
35
43
  if (s.kind === 'cron') {
36
- crons.set(task.id, new Cron(s.expr, () => void fire(task.id)));
44
+ crons.set(task.id, new Cron(s.expr, s.timezone ? { timezone: s.timezone } : {}, () => void fire(task.id)));
37
45
  }
38
46
  else if (s.kind === 'interval') {
39
47
  timers.set(task.id, setInterval(() => void fire(task.id), s.ms));
@@ -1,6 +1,7 @@
1
1
  export type Schedule = {
2
2
  kind: 'cron';
3
3
  expr: string;
4
+ timezone?: string;
4
5
  } | {
5
6
  kind: 'interval';
6
7
  ms: number;
@@ -15,7 +16,7 @@ export interface TaskResult {
15
16
  }
16
17
  export interface Task {
17
18
  id: string;
18
- skill: string;
19
+ skill?: string;
19
20
  prompt: string;
20
21
  agent?: string;
21
22
  schedule: Schedule;
@@ -24,6 +25,23 @@ export interface Task {
24
25
  lastRun?: number;
25
26
  lastResult?: TaskResult;
26
27
  }
28
+ export type SchedulerEvent = {
29
+ type: 'task_started';
30
+ task: Task;
31
+ at: number;
32
+ } | {
33
+ type: 'task_completed';
34
+ task: Task;
35
+ at: number;
36
+ durationMs: number;
37
+ result: TaskResult;
38
+ } | {
39
+ type: 'task_failed';
40
+ task: Task;
41
+ at: number;
42
+ durationMs: number;
43
+ error: string;
44
+ };
27
45
  export type TaskInput = Omit<Task, 'id' | 'enabled' | 'createdAt' | 'lastRun' | 'lastResult'> & {
28
46
  enabled?: boolean;
29
47
  };
@@ -1,4 +1,4 @@
1
- export type { Schedule, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './engine/index.js';
2
- export { createScheduler, createTaskStore, type Scheduler } from './engine/index.js';
1
+ export type { Schedule, SchedulerEvent, Task, TaskInput, TaskResult, TaskRunner, TaskStore } from './engine/index.js';
2
+ export { createMemoryTaskStore, createScheduler, createTaskStore, type Scheduler } from './engine/index.js';
3
3
  export { createScheduleTaskTool } from './tool.js';
4
4
  export { createTasksCommand } from './command.js';
@@ -1,3 +1,3 @@
1
- export { createScheduler, createTaskStore } from './engine/index.js';
1
+ export { createMemoryTaskStore, createScheduler, createTaskStore } from './engine/index.js';
2
2
  export { createScheduleTaskTool } from './tool.js';
3
3
  export { createTasksCommand } from './command.js';
@@ -28,6 +28,7 @@ export const createAgentSession = (config) => {
28
28
  running = true;
29
29
  const ac = new AbortController();
30
30
  controller = ac;
31
+ let terminal = { type: 'turn_end' };
31
32
  try {
32
33
  if (!started) {
33
34
  started = true;
@@ -58,18 +59,16 @@ export const createAgentSession = (config) => {
58
59
  messages.push(event.message);
59
60
  emitter.emit(event);
60
61
  }
61
- emitter.emit({ type: 'turn_end' });
62
62
  }
63
63
  catch (error) {
64
- if (ac.signal.aborted)
65
- emitter.emit({ type: 'turn_end' });
66
- else
67
- emitter.emit({ type: 'error', error });
64
+ if (!ac.signal.aborted)
65
+ terminal = { type: 'error', error };
68
66
  }
69
67
  finally {
70
68
  running = false;
71
69
  controller = undefined;
72
70
  }
71
+ emitter.emit(terminal);
73
72
  };
74
73
  return {
75
74
  id,
@@ -1,30 +1,37 @@
1
1
  import { runSubAgent } from './runner.js';
2
- export const createSubAgentTool = (deps) => ({
3
- name: 'subagent',
4
- description: 'Delegate an isolated task to a named sub-agent. Returns its final answer.',
5
- prompt: 'Delegate self-contained research or sub-tasks to a named sub-agent with `subagent`; treat its answer as research input.',
6
- parameters: {
7
- type: 'object',
8
- properties: {
9
- agent: { type: 'string', description: 'Sub-agent name.' },
10
- task: { type: 'string', description: 'The task to delegate.' },
2
+ const BASE_PROMPT = 'Delegate self-contained research or sub-tasks to a named sub-agent with `subagent`; treat its answer as research input.';
3
+ export const createSubAgentTool = (deps) => {
4
+ const roster = deps.registry.list()
5
+ .filter((agent) => agent.name !== 'title')
6
+ .map((agent) => `- ${agent.name}: ${agent.description}`)
7
+ .join('\n');
8
+ return {
9
+ name: 'subagent',
10
+ description: 'Delegate an isolated task to a named sub-agent. Returns its final answer.',
11
+ prompt: roster ? `${BASE_PROMPT}\n\nAvailable sub-agents:\n${roster}` : BASE_PROMPT,
12
+ parameters: {
13
+ type: 'object',
14
+ properties: {
15
+ agent: { type: 'string', description: 'Sub-agent name.' },
16
+ task: { type: 'string', description: 'The task to delegate.' },
17
+ },
18
+ required: ['agent', 'task'],
19
+ additionalProperties: false,
11
20
  },
12
- required: ['agent', 'task'],
13
- additionalProperties: false,
14
- },
15
- run: async (input, ctx) => {
16
- const { agent, task } = (input ?? {});
17
- if (!agent || !task)
18
- return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
19
- const def = deps.registry.get(agent);
20
- if (!def)
21
- return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
22
- const result = await runSubAgent(def, task, {
23
- spawn: deps.spawn,
24
- runs: deps.runs,
25
- parentId: deps.parentId,
26
- signal: ctx.signal,
27
- });
28
- return [{ type: 'text', text: result.text }];
29
- },
30
- });
21
+ run: async (input, ctx) => {
22
+ const { agent, task } = (input ?? {});
23
+ if (!agent || !task)
24
+ return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
25
+ const def = deps.registry.get(agent);
26
+ if (!def)
27
+ return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
28
+ const result = await runSubAgent(def, task, {
29
+ spawn: deps.spawn,
30
+ runs: deps.runs,
31
+ parentId: deps.parentId,
32
+ signal: ctx.signal,
33
+ });
34
+ return [{ type: 'text', text: result.text }];
35
+ },
36
+ };
37
+ };
@@ -288,12 +288,19 @@ function sgrDelta(prev, next) {
288
288
  return `\x1b[${params.join(';')}m`;
289
289
  }
290
290
  if (prev.bold !== next.bold || prev.dim !== next.dim) {
291
- if (!next.bold && !next.dim && (prev.bold || prev.dim))
291
+ if ((prev.bold && !next.bold) || (prev.dim && !next.dim)) {
292
292
  params.push('22');
293
- if (next.bold && !prev.bold)
294
- params.push('1');
295
- if (next.dim && !prev.dim)
296
- params.push('2');
293
+ if (next.bold)
294
+ params.push('1');
295
+ if (next.dim)
296
+ params.push('2');
297
+ }
298
+ else {
299
+ if (next.bold && !prev.bold)
300
+ params.push('1');
301
+ if (next.dim && !prev.dim)
302
+ params.push('2');
303
+ }
297
304
  }
298
305
  if (prev.italic !== next.italic)
299
306
  params.push(next.italic ? '3' : '23');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-harness",
3
- "version": "0.16.6",
3
+ "version": "0.16.8",
4
4
  "description": "Agent harness: createHarness wires mu-core into a host — XDG paths, model registry, plugins, disk-loaded agents & skills, sub-agents, sessions (JSONL + SQLite catalog), slash commands, permission/approval hooks, an optional scheduler, and a composable TUI chat app",
5
5
  "license": "MIT",
6
6
  "main": "./script/index.js",
@@ -16,8 +16,8 @@
16
16
  "dependencies": {
17
17
  "@swc/wasm-typescript": "^1.15.0",
18
18
  "croner": "^9.0.0",
19
- "mu-core": "^0.16.6",
20
- "mu-tui": "^0.16.6"
19
+ "mu-core": "^0.16.8",
20
+ "mu-tui": "^0.16.8"
21
21
  },
22
22
  "_generatedBy": "dnt@dev"
23
23
  }
@@ -1,4 +1,4 @@
1
- export type { Agent } from './types.js';
2
- export { type AgentRegistry, createAgentRegistry } from './registry.js';
1
+ export type { Agent, ToolDecision, ToolGrants } from './types.js';
2
+ export { type AgentRegistry, createAgentRegistry, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadAgents = exports.parseAgent = exports.createAgentRegistry = void 0;
3
+ exports.loadAgents = exports.parseAgent = exports.toolNames = exports.toolDecision = exports.createAgentRegistry = void 0;
4
4
  var registry_js_1 = require("./registry.js");
5
5
  Object.defineProperty(exports, "createAgentRegistry", { enumerable: true, get: function () { return registry_js_1.createAgentRegistry; } });
6
+ Object.defineProperty(exports, "toolDecision", { enumerable: true, get: function () { return registry_js_1.toolDecision; } });
7
+ Object.defineProperty(exports, "toolNames", { enumerable: true, get: function () { return registry_js_1.toolNames; } });
6
8
  var parser_js_1 = require("./parser.js");
7
9
  Object.defineProperty(exports, "parseAgent", { enumerable: true, get: function () { return parser_js_1.parseAgent; } });
8
10
  var loader_js_1 = require("./loader.js");