mu-harness 0.16.7 → 0.16.9
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/scheduler/tool.js +1 -2
- package/esm/harness/npm/src/session/agent-session.js +1 -0
- package/esm/harness/npm/src/session/manager.js +3 -0
- package/esm/harness/npm/src/session/persist.js +3 -0
- package/esm/harness/npm/src/session/types.d.ts +2 -1
- package/esm/harness/npm/src/skills/run.js +1 -2
- package/esm/harness/npm/src/skills/tool.js +4 -4
- package/esm/harness/npm/src/skills/writer.js +1 -2
- package/esm/harness/npm/src/subAgents/tool.js +34 -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/scheduler/tool.js +1 -2
- package/script/harness/npm/src/session/agent-session.js +1 -0
- package/script/harness/npm/src/session/manager.js +3 -0
- package/script/harness/npm/src/session/persist.js +3 -0
- package/script/harness/npm/src/session/types.d.ts +2 -1
- package/script/harness/npm/src/skills/run.js +1 -2
- package/script/harness/npm/src/skills/tool.js +4 -4
- package/script/harness/npm/src/skills/writer.js +1 -2
- package/script/harness/npm/src/subAgents/tool.js +34 -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';
|
|
@@ -10,8 +10,7 @@ const toSchedule = (raw) => {
|
|
|
10
10
|
};
|
|
11
11
|
export const createScheduleTaskTool = (deps) => ({
|
|
12
12
|
name: 'schedule_task',
|
|
13
|
-
description: 'Persist a
|
|
14
|
-
prompt: 'Use `schedule_task` to register work that should run later or on a schedule: a `cron` expression, an `everyMs` heartbeat, or a one-shot `at` timestamp. It invokes a skill on a prompt, like `run_skill`.',
|
|
13
|
+
description: 'Persist work to run later or on a schedule: a `cron` expression, an `everyMs` heartbeat, or a one-shot `at` timestamp. It invokes a skill on a prompt (like `run_skill`) via a sub-agent.',
|
|
15
14
|
parameters: {
|
|
16
15
|
type: 'object',
|
|
17
16
|
properties: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContentPart, LoopEvent, Message } from 'mu-core';
|
|
1
|
+
import type { ContentPart, LoopEvent, Message, Tool } from 'mu-core';
|
|
2
2
|
export type AgentSessionEvent = {
|
|
3
3
|
type: 'turn_start';
|
|
4
4
|
input: Message;
|
|
@@ -11,6 +11,7 @@ export type AgentSessionEvent = {
|
|
|
11
11
|
export interface AgentSession {
|
|
12
12
|
readonly id: string;
|
|
13
13
|
readonly messages: readonly Message[];
|
|
14
|
+
readonly tools: readonly Tool[];
|
|
14
15
|
send(input: string | ContentPart[]): Promise<void>;
|
|
15
16
|
abort(): void;
|
|
16
17
|
subscribe(listener: (event: AgentSessionEvent) => void): () => void;
|
|
@@ -18,8 +18,7 @@ export const runSkill = async (deps, args) => {
|
|
|
18
18
|
};
|
|
19
19
|
export const createRunSkillTool = (deps) => ({
|
|
20
20
|
name: 'run_skill',
|
|
21
|
-
description: 'Invoke a sub-agent equipped with a named skill to carry out a
|
|
22
|
-
prompt: 'To carry out a self-contained task under a skill, call `run_skill` with the skill name, the task, and the agent persona to run it as.',
|
|
21
|
+
description: 'Invoke a sub-agent equipped with a named skill to carry out a self-contained task — pass the skill name, the task, and the agent persona to run it as. Returns its final answer.',
|
|
23
22
|
parameters: {
|
|
24
23
|
type: 'object',
|
|
25
24
|
properties: {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export const createSkillTool = (registry) => ({
|
|
2
2
|
name: 'skill',
|
|
3
|
-
description
|
|
4
|
-
|
|
3
|
+
get description() {
|
|
4
|
+
const base = "Load a named skill's instructions into the conversation, then follow them.";
|
|
5
5
|
const list = registry.list();
|
|
6
6
|
if (!list.length)
|
|
7
|
-
return
|
|
7
|
+
return base;
|
|
8
8
|
const catalog = list.map((skill) => `- ${skill.name}${skill.description ? `: ${skill.description}` : ''}`).join('\n');
|
|
9
|
-
return
|
|
9
|
+
return `${base} When a request matches one of these skills, call \`skill\` with its name BEFORE acting:\n${catalog}`;
|
|
10
10
|
},
|
|
11
11
|
parameters: {
|
|
12
12
|
type: 'object',
|
|
@@ -4,8 +4,7 @@ import { parseSkill } from './parser.js';
|
|
|
4
4
|
const slug = (name) => name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
5
5
|
export const createSkillWriterTool = (deps) => ({
|
|
6
6
|
name: 'create_skill',
|
|
7
|
-
description: 'Create a reusable skill
|
|
8
|
-
prompt: 'When you discover a reusable workflow worth keeping, capture it with `create_skill` (name, description, instructions). Use `scope: "local"` for a skill specific to this project, or `scope: "config"` to make it available across all projects. It can then be loaded via `skill`.',
|
|
7
|
+
description: 'Create a reusable skill (name, description, instructions) the agent can load on demand later — capture a reusable workflow worth keeping. `scope: "local"` saves it to this project, `scope: "config"` makes it available across all projects; load it later via `skill`.',
|
|
9
8
|
parameters: {
|
|
10
9
|
type: 'object',
|
|
11
10
|
properties: {
|
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import { runSubAgent } from './runner.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
const BASE_PROMPT = 'Delegate an isolated, fully-specifiable task to a named sub-agent instead of doing it inline; it returns only its final answer. Pick the matching sub-agent, brief it from scratch, then verify its answer before relying on it.';
|
|
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: roster ? `${BASE_PROMPT}\n\nAvailable sub-agents:\n${roster}` : BASE_PROMPT,
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
agent: { type: 'string', description: 'Sub-agent name.' },
|
|
15
|
+
task: { type: 'string', description: 'The task to delegate.' },
|
|
16
|
+
},
|
|
17
|
+
required: ['agent', 'task'],
|
|
18
|
+
additionalProperties: false,
|
|
11
19
|
},
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
},
|
|
30
|
-
});
|
|
20
|
+
run: async (input, ctx) => {
|
|
21
|
+
const { agent, task } = (input ?? {});
|
|
22
|
+
if (!agent || !task)
|
|
23
|
+
return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
|
|
24
|
+
const def = deps.registry.get(agent);
|
|
25
|
+
if (!def)
|
|
26
|
+
return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
|
|
27
|
+
const result = await runSubAgent(def, task, {
|
|
28
|
+
spawn: deps.spawn,
|
|
29
|
+
runs: deps.runs,
|
|
30
|
+
parentId: deps.parentId,
|
|
31
|
+
signal: ctx.signal,
|
|
32
|
+
});
|
|
33
|
+
return [{ type: 'text', text: result.text }];
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
};
|