mu-harness 0.16.7 → 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.
- package/esm/harness/npm/src/agents/index.d.ts +2 -2
- package/esm/harness/npm/src/agents/index.js +1 -1
- package/esm/harness/npm/src/agents/parser.js +18 -6
- package/esm/harness/npm/src/agents/registry.d.ts +3 -1
- package/esm/harness/npm/src/agents/registry.js +21 -0
- package/esm/harness/npm/src/agents/types.d.ts +3 -1
- package/esm/harness/npm/src/harness/create.js +18 -4
- package/esm/harness/npm/src/harness/types.d.ts +10 -1
- package/esm/harness/npm/src/permissions/approval-manager.d.ts +31 -0
- package/esm/harness/npm/src/permissions/approval-manager.js +55 -0
- package/esm/harness/npm/src/permissions/index.d.ts +1 -0
- package/esm/harness/npm/src/permissions/index.js +1 -0
- package/esm/harness/npm/src/plugin/import-ts.js +1 -1
- package/esm/harness/npm/src/scheduler/command.js +1 -1
- package/esm/harness/npm/src/scheduler/engine/index.d.ts +2 -1
- package/esm/harness/npm/src/scheduler/engine/index.js +1 -0
- package/esm/harness/npm/src/scheduler/engine/memory-store.d.ts +2 -0
- package/esm/harness/npm/src/scheduler/engine/memory-store.js +23 -0
- package/esm/harness/npm/src/scheduler/engine/scheduler.d.ts +2 -1
- package/esm/harness/npm/src/scheduler/engine/scheduler.js +9 -1
- package/esm/harness/npm/src/scheduler/engine/types.d.ts +19 -1
- package/esm/harness/npm/src/scheduler/index.d.ts +2 -2
- package/esm/harness/npm/src/scheduler/index.js +1 -1
- package/esm/harness/npm/src/subAgents/tool.js +35 -28
- package/esm/tui/src/layout/ansi.js +12 -5
- package/package.json +3 -3
- package/script/harness/npm/src/agents/index.d.ts +2 -2
- package/script/harness/npm/src/agents/index.js +3 -1
- package/script/harness/npm/src/agents/parser.js +18 -6
- package/script/harness/npm/src/agents/registry.d.ts +3 -1
- package/script/harness/npm/src/agents/registry.js +24 -1
- package/script/harness/npm/src/agents/types.d.ts +3 -1
- package/script/harness/npm/src/harness/create.js +17 -3
- package/script/harness/npm/src/harness/types.d.ts +10 -1
- package/script/harness/npm/src/permissions/approval-manager.d.ts +31 -0
- package/script/harness/npm/src/permissions/approval-manager.js +59 -0
- package/script/harness/npm/src/permissions/index.d.ts +1 -0
- package/script/harness/npm/src/permissions/index.js +3 -1
- package/script/harness/npm/src/plugin/import-ts.js +3 -3
- package/script/harness/npm/src/scheduler/command.js +1 -1
- package/script/harness/npm/src/scheduler/engine/index.d.ts +2 -1
- package/script/harness/npm/src/scheduler/engine/index.js +3 -1
- package/script/harness/npm/src/scheduler/engine/memory-store.d.ts +2 -0
- package/script/harness/npm/src/scheduler/engine/memory-store.js +27 -0
- package/script/harness/npm/src/scheduler/engine/scheduler.d.ts +2 -1
- package/script/harness/npm/src/scheduler/engine/scheduler.js +9 -1
- package/script/harness/npm/src/scheduler/engine/types.d.ts +19 -1
- package/script/harness/npm/src/scheduler/index.d.ts +2 -2
- package/script/harness/npm/src/scheduler/index.js +2 -1
- package/script/harness/npm/src/subAgents/tool.js +35 -28
- 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,10 +1,22 @@
|
|
|
1
1
|
import { parseFrontmatter, str } from '../common/index.js';
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
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:
|
|
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?:
|
|
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
|
|
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
|
|
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,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';
|
|
@@ -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
|
|
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,30 +1,37 @@
|
|
|
1
1
|
import { runSubAgent } from './runner.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 (
|
|
291
|
+
if ((prev.bold && !next.bold) || (prev.dim && !next.dim)) {
|
|
292
292
|
params.push('22');
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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.
|
|
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.
|
|
20
|
-
"mu-tui": "^0.16.
|
|
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");
|
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseAgent = void 0;
|
|
4
4
|
const index_js_1 = require("../common/index.js");
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const DECISIONS = new Set(['allow', 'ask', 'deny']);
|
|
6
|
+
const parseStringList = (raw) => (Array.isArray(raw) ? raw : raw.split(','))
|
|
7
|
+
.filter((entry) => typeof entry === 'string')
|
|
8
|
+
.map((entry) => entry.trim())
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
const parseTools = (raw) => {
|
|
11
|
+
if (Array.isArray(raw) || typeof raw === 'string') {
|
|
12
|
+
const list = parseStringList(raw);
|
|
13
|
+
return list.length > 0 ? list : undefined;
|
|
14
|
+
}
|
|
15
|
+
if (raw && typeof raw === 'object') {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [tool, decision] of Object.entries(raw)) {
|
|
18
|
+
if (typeof decision === 'string' && DECISIONS.has(decision))
|
|
19
|
+
out[tool] = decision;
|
|
20
|
+
}
|
|
21
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
8
22
|
}
|
|
9
|
-
if (typeof raw === 'string')
|
|
10
|
-
return raw.split(',').map((part) => part.trim()).filter(Boolean);
|
|
11
23
|
return undefined;
|
|
12
24
|
};
|
|
13
25
|
const parseAgent = (source, fallbackName) => {
|
|
@@ -16,7 +28,7 @@ const parseAgent = (source, fallbackName) => {
|
|
|
16
28
|
name: (0, index_js_1.str)(fields.name) ?? fallbackName,
|
|
17
29
|
description: (0, index_js_1.str)(fields.description) ?? '',
|
|
18
30
|
prompt: body,
|
|
19
|
-
tools:
|
|
31
|
+
tools: parseTools(fields.tools),
|
|
20
32
|
model: (0, index_js_1.str)(fields.model),
|
|
21
33
|
color: (0, index_js_1.str)(fields.color),
|
|
22
34
|
extends: (0, index_js_1.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,6 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createAgentRegistry = void 0;
|
|
3
|
+
exports.createAgentRegistry = exports.toolNames = exports.toolDecision = void 0;
|
|
4
|
+
const asMap = (tools) => {
|
|
5
|
+
if (!tools)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (Array.isArray(tools))
|
|
8
|
+
return Object.fromEntries(tools.map((tool) => [tool, 'allow']));
|
|
9
|
+
return tools;
|
|
10
|
+
};
|
|
11
|
+
const toolDecision = (agent, tool) => {
|
|
12
|
+
const map = asMap(agent.tools);
|
|
13
|
+
if (!map)
|
|
14
|
+
return 'allow';
|
|
15
|
+
return map[tool] ?? map['*'] ?? 'deny';
|
|
16
|
+
};
|
|
17
|
+
exports.toolDecision = toolDecision;
|
|
18
|
+
const toolNames = (agent) => {
|
|
19
|
+
const map = asMap(agent.tools);
|
|
20
|
+
if (!map)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (map['*'] && map['*'] !== 'deny')
|
|
23
|
+
return ['*'];
|
|
24
|
+
return Object.entries(map).filter(([, decision]) => decision !== 'deny').map(([tool]) => tool);
|
|
25
|
+
};
|
|
26
|
+
exports.toolNames = toolNames;
|
|
4
27
|
const merge = (base, child) => ({
|
|
5
28
|
name: child.name,
|
|
6
29
|
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?:
|
|
7
|
+
tools?: ToolGrants;
|
|
6
8
|
model?: string;
|
|
7
9
|
color?: string;
|
|
8
10
|
extends?: string;
|
|
@@ -24,7 +24,7 @@ const TITLE_AGENT = {
|
|
|
24
24
|
tools: [],
|
|
25
25
|
};
|
|
26
26
|
const createHarness = async (options) => {
|
|
27
|
-
const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], title, titleModel, scheduler: enableScheduler = false, ...sessionDefaults } = options;
|
|
27
|
+
const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
|
|
28
28
|
const cwd = options.cwd ?? node_process_1.default.cwd();
|
|
29
29
|
const config = (0, index_js_3.createHarnessConfig)({ hostName, xdg });
|
|
30
30
|
const models = (0, models_js_1.createModelRegistry)({ providers, default: model });
|
|
@@ -48,17 +48,28 @@ const createHarness = async (options) => {
|
|
|
48
48
|
const runs = (0, index_js_10.createSubAgentRegistry)();
|
|
49
49
|
const store = (0, index_js_8.createSessionStore)({ dir: (0, node_path_1.join)(config.dataDir, 'sessions') });
|
|
50
50
|
const sessionTools = (extra = []) => [...(sessionDefaults.tools ?? []), ...extra, ...skillTools, ...schedulerTools];
|
|
51
|
+
const approvalHook = (getAgent) => approvals
|
|
52
|
+
? approvals.manager.hooksFor({
|
|
53
|
+
decide: (call) => {
|
|
54
|
+
const agent = getAgent();
|
|
55
|
+
if (!agent)
|
|
56
|
+
return 'allow';
|
|
57
|
+
return approvals.decide ? approvals.decide(agent, call) : (0, index_js_1.toolDecision)(agent, call.name);
|
|
58
|
+
},
|
|
59
|
+
agent: () => getAgent()?.name,
|
|
60
|
+
})
|
|
61
|
+
: undefined;
|
|
51
62
|
const persona = (agent, opts) => (0, index_js_8.createAgentSession)({
|
|
52
63
|
tools: opts.tools,
|
|
53
64
|
plugins: sessionDefaults.plugins,
|
|
54
65
|
system: agent.prompt,
|
|
55
|
-
hooks: opts.hooks ?? (0, index_js_5.allowList)(
|
|
66
|
+
hooks: opts.hooks ?? (0, index_js_5.allowList)((0, index_js_1.toolNames)(agent)),
|
|
56
67
|
...models.resolve(opts.model ?? agent.model),
|
|
57
68
|
id: newId(),
|
|
58
69
|
});
|
|
59
70
|
const spawn = (agent) => (0, index_js_8.persistTo)(store, persona(agent, {
|
|
60
71
|
tools: sessionTools(),
|
|
61
|
-
hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, (0, index_js_5.allowList)(
|
|
72
|
+
hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, (0, index_js_5.allowList)((0, index_js_1.toolNames)(agent)), approvalHook(() => agent)]),
|
|
62
73
|
}));
|
|
63
74
|
const scheduler = enableScheduler && tasks
|
|
64
75
|
? (0, index_js_7.createScheduler)({
|
|
@@ -67,6 +78,8 @@ const createHarness = async (options) => {
|
|
|
67
78
|
try {
|
|
68
79
|
if (!task.agent)
|
|
69
80
|
throw new Error('scheduled task is missing an agent');
|
|
81
|
+
if (!task.skill)
|
|
82
|
+
throw new Error('scheduled task is missing a skill');
|
|
70
83
|
const output = await (0, index_js_9.runSkill)({ skills, agents, spawn, runs, parentId: task.id }, {
|
|
71
84
|
skill: task.skill,
|
|
72
85
|
task: task.prompt,
|
|
@@ -104,6 +117,7 @@ const createHarness = async (options) => {
|
|
|
104
117
|
},
|
|
105
118
|
revive: ({ id, model: ref, messages }) => (0, index_js_8.createAgentSession)({
|
|
106
119
|
...sessionDefaults,
|
|
120
|
+
hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, approvalHook(() => approvals?.activeAgent())]),
|
|
107
121
|
tools: sessionTools([(0, index_js_10.createSubAgentTool)({ registry: agents, spawn, runs, parentId: id })]),
|
|
108
122
|
...models.resolve(ref),
|
|
109
123
|
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,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createApprovalManager = void 0;
|
|
4
|
+
const approval_js_1 = require("./approval.js");
|
|
5
|
+
const denied = (name) => [{ type: 'text', text: `Denied: ${name}` }];
|
|
6
|
+
const createApprovalManager = (options = {}) => {
|
|
7
|
+
const askTools = options.askTools ? new Set(options.askTools) : undefined;
|
|
8
|
+
const alwaysAllow = new Set();
|
|
9
|
+
const newId = options.newId ?? (() => crypto.randomUUID());
|
|
10
|
+
const keyOf = (agent, tool) => `${agent ?? ''}:${tool}`;
|
|
11
|
+
const waiters = new Map();
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
const request = (id, name, input, agent) => new Promise((resolve) => {
|
|
14
|
+
const req = { id, name, input, agent };
|
|
15
|
+
waiters.set(id, { resolve, req, key: keyOf(agent, name) });
|
|
16
|
+
for (const listener of listeners)
|
|
17
|
+
listener(req);
|
|
18
|
+
});
|
|
19
|
+
const defaultNeeds = options.needsApproval ?? (({ name }) => (askTools ? askTools.has(name) : true));
|
|
20
|
+
const hooks = (0, approval_js_1.requireApproval)({
|
|
21
|
+
needsApproval: (call) => defaultNeeds(call) && !alwaysAllow.has(keyOf(undefined, call.name)),
|
|
22
|
+
newId,
|
|
23
|
+
prompt: (call) => request(call.id, call.name, call.input, undefined),
|
|
24
|
+
});
|
|
25
|
+
const hooksFor = ({ decide, agent }) => ({
|
|
26
|
+
beforeToolCall: async (call) => {
|
|
27
|
+
const decision = decide(call);
|
|
28
|
+
if (decision === 'allow')
|
|
29
|
+
return;
|
|
30
|
+
const agentName = agent?.();
|
|
31
|
+
if (decision === 'deny')
|
|
32
|
+
return denied(call.name);
|
|
33
|
+
if (alwaysAllow.has(keyOf(agentName, call.name)))
|
|
34
|
+
return;
|
|
35
|
+
const allow = await request(newId(), call.name, call.input, agentName);
|
|
36
|
+
return allow ? undefined : denied(call.name);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
hooks,
|
|
41
|
+
hooksFor,
|
|
42
|
+
pending: () => [...waiters.values()].map((w) => w.req),
|
|
43
|
+
resolve: (id, action) => {
|
|
44
|
+
const waiter = waiters.get(id);
|
|
45
|
+
if (!waiter)
|
|
46
|
+
return false;
|
|
47
|
+
waiters.delete(id);
|
|
48
|
+
if (action === 'approve_always')
|
|
49
|
+
alwaysAllow.add(waiter.key);
|
|
50
|
+
waiter.resolve(action !== 'deny');
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
subscribe: (listener) => {
|
|
54
|
+
listeners.add(listener);
|
|
55
|
+
return () => listeners.delete(listener);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
exports.createApprovalManager = createApprovalManager;
|
|
@@ -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,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.matchesAnyGlob = exports.requireApproval = exports.filterTools = exports.allowList = void 0;
|
|
3
|
+
exports.matchesAnyGlob = exports.createApprovalManager = exports.requireApproval = exports.filterTools = exports.allowList = void 0;
|
|
4
4
|
var allow_list_js_1 = require("./allow-list.js");
|
|
5
5
|
Object.defineProperty(exports, "allowList", { enumerable: true, get: function () { return allow_list_js_1.allowList; } });
|
|
6
6
|
Object.defineProperty(exports, "filterTools", { enumerable: true, get: function () { return allow_list_js_1.filterTools; } });
|
|
7
7
|
var approval_js_1 = require("./approval.js");
|
|
8
8
|
Object.defineProperty(exports, "requireApproval", { enumerable: true, get: function () { return approval_js_1.requireApproval; } });
|
|
9
|
+
var approval_manager_js_1 = require("./approval-manager.js");
|
|
10
|
+
Object.defineProperty(exports, "createApprovalManager", { enumerable: true, get: function () { return approval_manager_js_1.createApprovalManager; } });
|
|
9
11
|
var glob_js_1 = require("./glob.js");
|
|
10
12
|
Object.defineProperty(exports, "matchesAnyGlob", { enumerable: true, get: function () { return glob_js_1.matchesAnyGlob; } });
|
|
@@ -34,7 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.importModule = void 0;
|
|
37
|
-
const wasm_typescript_1 = require("@swc/wasm-typescript");
|
|
38
37
|
const promises_1 = require("node:fs/promises");
|
|
39
38
|
const node_os_1 = require("node:os");
|
|
40
39
|
const node_path_1 = require("node:path");
|
|
@@ -98,15 +97,16 @@ const commonBase = (files) => {
|
|
|
98
97
|
const outName = (path) => path.replace(TS_EXT, '.mjs');
|
|
99
98
|
const rewriteSpecifiers = (code) => code.replace(SPECIFIER, (whole, quote, spec) => whole.replace(`${quote}${spec}${quote}`, `${quote}${outName(spec)}${quote}`));
|
|
100
99
|
const transpileTree = async (entry) => {
|
|
100
|
+
const { transformSync } = await Promise.resolve().then(() => __importStar(require('@swc/wasm-typescript')));
|
|
101
101
|
const sources = await collect(entry);
|
|
102
102
|
if (sources.size === 1) {
|
|
103
|
-
const { code } =
|
|
103
|
+
const { code } = transformSync(sources.get(entry), { mode: 'transform', module: true });
|
|
104
104
|
return `data:text/javascript;base64,${btoa(unescape(encodeURIComponent(code)))}`;
|
|
105
105
|
}
|
|
106
106
|
const base = commonBase([...sources.keys()]);
|
|
107
107
|
const outDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'mu-plugin-'));
|
|
108
108
|
for (const [file, source] of sources) {
|
|
109
|
-
const { code } =
|
|
109
|
+
const { code } = transformSync(source, { mode: 'transform', module: true });
|
|
110
110
|
const out = (0, node_path_1.join)(outDir, outName((0, node_path_1.relative)(base, file)));
|
|
111
111
|
await (0, promises_1.mkdir)((0, node_path_1.dirname)(out), { recursive: true });
|
|
112
112
|
await (0, promises_1.writeFile)(out, rewriteSpecifiers(code), 'utf-8');
|
|
@@ -15,7 +15,7 @@ const createTasksCommand = (tasks) => ({
|
|
|
15
15
|
? `every ${t.schedule.ms}ms`
|
|
16
16
|
: 'once';
|
|
17
17
|
const state = t.enabled ? when : `${when} (disabled)`;
|
|
18
|
-
return `- ${t.id} — ${t.skill} [${state}]`;
|
|
18
|
+
return `- ${t.id} — ${t.skill ?? t.agent ?? 'task'} [${state}]`;
|
|
19
19
|
};
|
|
20
20
|
return { ok: true, output: list.map(describe).join('\n') };
|
|
21
21
|
},
|
|
@@ -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,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createScheduler = exports.createTaskStore = void 0;
|
|
3
|
+
exports.createScheduler = exports.createMemoryTaskStore = exports.createTaskStore = void 0;
|
|
4
4
|
var store_js_1 = require("./store.js");
|
|
5
5
|
Object.defineProperty(exports, "createTaskStore", { enumerable: true, get: function () { return store_js_1.createTaskStore; } });
|
|
6
|
+
var memory_store_js_1 = require("./memory-store.js");
|
|
7
|
+
Object.defineProperty(exports, "createMemoryTaskStore", { enumerable: true, get: function () { return memory_store_js_1.createMemoryTaskStore; } });
|
|
6
8
|
var scheduler_js_1 = require("./scheduler.js");
|
|
7
9
|
Object.defineProperty(exports, "createScheduler", { enumerable: true, get: function () { return scheduler_js_1.createScheduler; } });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMemoryTaskStore = void 0;
|
|
4
|
+
const createMemoryTaskStore = (initial = []) => {
|
|
5
|
+
const tasks = new Map(initial.map((task) => [task.id, task]));
|
|
6
|
+
return {
|
|
7
|
+
list: async () => [...tasks.values()],
|
|
8
|
+
get: async (id) => tasks.get(id),
|
|
9
|
+
create: async (input) => {
|
|
10
|
+
const task = { ...input, id: crypto.randomUUID(), enabled: input.enabled ?? true, createdAt: Date.now() };
|
|
11
|
+
tasks.set(task.id, task);
|
|
12
|
+
return task;
|
|
13
|
+
},
|
|
14
|
+
update: async (id, patch) => {
|
|
15
|
+
const task = tasks.get(id);
|
|
16
|
+
if (!task)
|
|
17
|
+
return undefined;
|
|
18
|
+
const next = { ...task, ...patch, id: task.id };
|
|
19
|
+
tasks.set(id, next);
|
|
20
|
+
return next;
|
|
21
|
+
},
|
|
22
|
+
remove: async (id) => {
|
|
23
|
+
tasks.delete(id);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
exports.createMemoryTaskStore = createMemoryTaskStore;
|
|
@@ -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;
|
|
@@ -11,14 +11,22 @@ const createScheduler = (deps) => {
|
|
|
11
11
|
const task = await store.get(id);
|
|
12
12
|
if (!task || !task.enabled)
|
|
13
13
|
return;
|
|
14
|
+
const at = Date.now();
|
|
15
|
+
deps.onEvent?.({ type: 'task_started', task, at });
|
|
14
16
|
try {
|
|
15
17
|
const result = await run(task);
|
|
16
18
|
await store.update(id, { lastRun: Date.now(), lastResult: result });
|
|
19
|
+
const durationMs = Date.now() - at;
|
|
20
|
+
if (result.ok)
|
|
21
|
+
deps.onEvent?.({ type: 'task_completed', task, at: Date.now(), durationMs, result });
|
|
22
|
+
else
|
|
23
|
+
deps.onEvent?.({ type: 'task_failed', task, at: Date.now(), durationMs, error: result.error ?? 'task failed' });
|
|
17
24
|
}
|
|
18
25
|
catch (error) {
|
|
19
26
|
deps.onError?.(error, task);
|
|
20
27
|
const message = error instanceof Error ? error.message : String(error);
|
|
21
28
|
await store.update(id, { lastRun: Date.now(), lastResult: { ok: false, error: message } });
|
|
29
|
+
deps.onEvent?.({ type: 'task_failed', task, at: Date.now(), durationMs: Date.now() - at, error: message });
|
|
22
30
|
}
|
|
23
31
|
if (task.schedule.kind === 'once')
|
|
24
32
|
await store.update(id, { enabled: false });
|
|
@@ -36,7 +44,7 @@ const createScheduler = (deps) => {
|
|
|
36
44
|
return;
|
|
37
45
|
const s = task.schedule;
|
|
38
46
|
if (s.kind === 'cron') {
|
|
39
|
-
crons.set(task.id, new croner_1.Cron(s.expr, () => void fire(task.id)));
|
|
47
|
+
crons.set(task.id, new croner_1.Cron(s.expr, s.timezone ? { timezone: s.timezone } : {}, () => void fire(task.id)));
|
|
40
48
|
}
|
|
41
49
|
else if (s.kind === 'interval') {
|
|
42
50
|
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
|
|
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,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createTasksCommand = exports.createScheduleTaskTool = exports.createTaskStore = exports.createScheduler = void 0;
|
|
3
|
+
exports.createTasksCommand = exports.createScheduleTaskTool = exports.createTaskStore = exports.createScheduler = exports.createMemoryTaskStore = void 0;
|
|
4
4
|
var index_js_1 = require("./engine/index.js");
|
|
5
|
+
Object.defineProperty(exports, "createMemoryTaskStore", { enumerable: true, get: function () { return index_js_1.createMemoryTaskStore; } });
|
|
5
6
|
Object.defineProperty(exports, "createScheduler", { enumerable: true, get: function () { return index_js_1.createScheduler; } });
|
|
6
7
|
Object.defineProperty(exports, "createTaskStore", { enumerable: true, get: function () { return index_js_1.createTaskStore; } });
|
|
7
8
|
var tool_js_1 = require("./tool.js");
|
|
@@ -2,33 +2,40 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createSubAgentTool = void 0;
|
|
4
4
|
const runner_js_1 = require("./runner.js");
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
const BASE_PROMPT = 'Delegate self-contained research or sub-tasks to a named sub-agent with `subagent`; treat its answer as research input.';
|
|
6
|
+
const createSubAgentTool = (deps) => {
|
|
7
|
+
const roster = deps.registry.list()
|
|
8
|
+
.filter((agent) => agent.name !== 'title')
|
|
9
|
+
.map((agent) => `- ${agent.name}: ${agent.description}`)
|
|
10
|
+
.join('\n');
|
|
11
|
+
return {
|
|
12
|
+
name: 'subagent',
|
|
13
|
+
description: 'Delegate an isolated task to a named sub-agent. Returns its final answer.',
|
|
14
|
+
prompt: roster ? `${BASE_PROMPT}\n\nAvailable sub-agents:\n${roster}` : BASE_PROMPT,
|
|
15
|
+
parameters: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
agent: { type: 'string', description: 'Sub-agent name.' },
|
|
19
|
+
task: { type: 'string', description: 'The task to delegate.' },
|
|
20
|
+
},
|
|
21
|
+
required: ['agent', 'task'],
|
|
22
|
+
additionalProperties: false,
|
|
14
23
|
},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
33
|
-
});
|
|
24
|
+
run: async (input, ctx) => {
|
|
25
|
+
const { agent, task } = (input ?? {});
|
|
26
|
+
if (!agent || !task)
|
|
27
|
+
return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
|
|
28
|
+
const def = deps.registry.get(agent);
|
|
29
|
+
if (!def)
|
|
30
|
+
return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
|
|
31
|
+
const result = await (0, runner_js_1.runSubAgent)(def, task, {
|
|
32
|
+
spawn: deps.spawn,
|
|
33
|
+
runs: deps.runs,
|
|
34
|
+
parentId: deps.parentId,
|
|
35
|
+
signal: ctx.signal,
|
|
36
|
+
});
|
|
37
|
+
return [{ type: 'text', text: result.text }];
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
};
|
|
34
41
|
exports.createSubAgentTool = createSubAgentTool;
|
|
@@ -292,12 +292,19 @@ function sgrDelta(prev, next) {
|
|
|
292
292
|
return `\x1b[${params.join(';')}m`;
|
|
293
293
|
}
|
|
294
294
|
if (prev.bold !== next.bold || prev.dim !== next.dim) {
|
|
295
|
-
if (
|
|
295
|
+
if ((prev.bold && !next.bold) || (prev.dim && !next.dim)) {
|
|
296
296
|
params.push('22');
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
if (next.bold)
|
|
298
|
+
params.push('1');
|
|
299
|
+
if (next.dim)
|
|
300
|
+
params.push('2');
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
if (next.bold && !prev.bold)
|
|
304
|
+
params.push('1');
|
|
305
|
+
if (next.dim && !prev.dim)
|
|
306
|
+
params.push('2');
|
|
307
|
+
}
|
|
301
308
|
}
|
|
302
309
|
if (prev.italic !== next.italic)
|
|
303
310
|
params.push(next.italic ? '3' : '23');
|