@hmharness/cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +22 -0
- package/package.json +8 -7
- package/dist/prompt.d.ts +0 -7
- package/dist/prompt.js +0 -14
- package/dist/runner.d.ts +0 -68
- package/dist/runner.js +0 -179
- package/dist/spawn.d.ts +0 -29
- package/dist/spawn.js +0 -64
- package/dist/tools.d.ts +0 -7
- package/dist/tools.js +0 -137
package/dist/main.js
CHANGED
|
@@ -680,6 +680,28 @@ flags:
|
|
|
680
680
|
}
|
|
681
681
|
return;
|
|
682
682
|
}
|
|
683
|
+
if (cmd === 'eval') {
|
|
684
|
+
// V2 M2: the Evaluator surface - hard evidence outranks LLM judgment.
|
|
685
|
+
await initHome();
|
|
686
|
+
const { allEvaluators } = await import('@hmharness/evaluation');
|
|
687
|
+
const { listCases } = await import('@hmharness/evolution');
|
|
688
|
+
const cases = await listCases(homeDir());
|
|
689
|
+
stdout.write('evaluators:\n' + allEvaluators.map((e) => ' ' + e.id.padEnd(16) + DIM('rank ' + e.evidenceKind) + ' ' + e.description.slice(0, 70)).join('\n') + '\n');
|
|
690
|
+
stdout.write('bench cases: ' + cases.length + ' (train ' + cases.filter((c) => !c.holdout).length + ' / holdout ' + cases.filter((c) => c.holdout).length + ')\n');
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (cmd === 'capability') {
|
|
694
|
+
// V2 M4: capability manifests - declared risk/permissions per tool.
|
|
695
|
+
await initHome();
|
|
696
|
+
const { capabilityReport, authorize, buildRegistry } = await import('@hmharness/agent');
|
|
697
|
+
const { reg } = await buildRegistry({ announce: false });
|
|
698
|
+
const mode = rest.includes('--lockdown') ? 'lockdown' : 'standard';
|
|
699
|
+
for (const m of capabilityReport(reg)) {
|
|
700
|
+
const d = authorize(m, mode);
|
|
701
|
+
stdout.write(' ' + (d.allow ? GREEN('allow') : RED('DENY ')) + ' ' + m.risk.padEnd(8) + m.id.padEnd(22) + DIM(m.permissions.join(',')) + '\n');
|
|
702
|
+
}
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
683
705
|
if (cmd === 'tui') {
|
|
684
706
|
await initHome();
|
|
685
707
|
// tui(yes, noWeb): inside the TTY check the TUI auto-links the web UI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "hmharness command line: one-shot tasks, an interactive REPL, a fullscreen TUI, the web frontend, and direct tool invocation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -43,12 +43,13 @@
|
|
|
43
43
|
"build": "tsc -p tsconfig.build.json"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@hmharness/kernel": "0.
|
|
46
|
+
"@hmharness/kernel": "0.8.0",
|
|
47
47
|
"@hmharness/observability": "0.7.0",
|
|
48
|
-
"@hmharness/evolution": "0.
|
|
49
|
-
"@hmharness/domain-harmony": "0.
|
|
50
|
-
"@hmharness/domain-ops": "0.
|
|
51
|
-
"@hmharness/agent": "0.
|
|
52
|
-
"@hmharness/
|
|
48
|
+
"@hmharness/evolution": "0.8.0",
|
|
49
|
+
"@hmharness/domain-harmony": "0.8.0",
|
|
50
|
+
"@hmharness/domain-ops": "0.8.0",
|
|
51
|
+
"@hmharness/agent": "0.8.0",
|
|
52
|
+
"@hmharness/evaluation": "0.8.0",
|
|
53
|
+
"@hmharness/web": "0.8.0"
|
|
53
54
|
}
|
|
54
55
|
}
|
package/dist/prompt.d.ts
DELETED
package/dist/prompt.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export function buildSystemPrompt(opts) {
|
|
2
|
-
const parts = [];
|
|
3
|
-
parts.push(`You are hmh, a coding agent powered by ${opts.model}, running on hmharness - a self-evolving agent framework designed for the full HarmonyOS development lifecycle. Working directory: ${opts.cwd}.`, '', 'Reply in the language the user writes in (Chinese in, Chinese out).', '', 'HarmonyOS development is your home domain: DevEco Studio toolchain, hvigor builds, ohpm packages, hdc devices, ArkTS/ArkUI, OpenHarmony and Cangjie. When a task touches it, prefer the harmony_* tools and precise toolchain knowledge.', '', 'Working style: read before writing; prefer small focused commands; verify results; state tradeoffs briefly. For risky operations (deleting, overwriting, publishing) say what will happen first.');
|
|
4
|
-
if (opts.memory.trim()) {
|
|
5
|
-
parts.push('', '## Long-term memory', opts.memory.trim());
|
|
6
|
-
}
|
|
7
|
-
if (opts.skills.trim()) {
|
|
8
|
-
parts.push('', '## Skill library', 'Read a skill file with read_file before applying it the first time.', opts.skills.trim());
|
|
9
|
-
}
|
|
10
|
-
if (opts.insights.trim()) {
|
|
11
|
-
parts.push('', '## Recent session outcomes (what worked / what failed)', opts.insights.trim());
|
|
12
|
-
}
|
|
13
|
-
return parts.join('\n');
|
|
14
|
-
}
|
package/dist/runner.d.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @hmh/cli - runner
|
|
3
|
-
* The shared agent-task execution layer. CLI maps its events to terminal
|
|
4
|
-
* output; the web frontend maps them to SSE - one behavior, two frontends.
|
|
5
|
-
* Also owns the native registry factory (spawn_agent recursion) and the
|
|
6
|
-
* approval gate construction.
|
|
7
|
-
*/
|
|
8
|
-
import { Registry, type ChatMessage, type DeltaKind, type HmhConfig, type LoopApproval, type LoopResult, type McpClient, type McpServerConfig, type McpServerImport, type ToolContext } from '@hmh/kernel';
|
|
9
|
-
import * as readline from 'node:readline/promises';
|
|
10
|
-
import { type SpawnBase } from './spawn.ts';
|
|
11
|
-
/** Flatten the config.json shape into the runtime discriminated union. */
|
|
12
|
-
export declare function toServerConfig(c: McpServerImport): McpServerConfig;
|
|
13
|
-
/**
|
|
14
|
-
* Current spawn base, set per task so a long-lived registry (REPL, web
|
|
15
|
-
* server) always routes sub-agents to the CURRENT session and gate.
|
|
16
|
-
*/
|
|
17
|
-
export declare const spawnBase: {
|
|
18
|
-
current?: SpawnBase;
|
|
19
|
-
};
|
|
20
|
-
export declare function nativeRegistry(depth: number): Registry;
|
|
21
|
-
export declare function buildRegistry(opts?: {
|
|
22
|
-
mcp?: boolean;
|
|
23
|
-
announce?: boolean;
|
|
24
|
-
}): Promise<{
|
|
25
|
-
reg: Registry;
|
|
26
|
-
clients: McpClient[];
|
|
27
|
-
}>;
|
|
28
|
-
/** Retrieval-based context pack: task-relevant memories, not the whole file. */
|
|
29
|
-
export declare function contextPack(task: string): Promise<{
|
|
30
|
-
memory: string;
|
|
31
|
-
skills: string;
|
|
32
|
-
insights: string;
|
|
33
|
-
}>;
|
|
34
|
-
/**
|
|
35
|
-
* Terminal approval gate: auto mode passes everything; a TTY gets a y/N
|
|
36
|
-
* prompt (reusing a caller-provided readline); a pipe gets a safe deny.
|
|
37
|
-
* The kernel loop denies by default when no gate is wired at all.
|
|
38
|
-
*/
|
|
39
|
-
export declare function makeApproval(cfg: HmhConfig, yes: boolean, sharedRl?: readline.Interface): LoopApproval;
|
|
40
|
-
export interface RunnerEvents {
|
|
41
|
-
onLine?(line: string): void;
|
|
42
|
-
onDelta?(kind: DeltaKind, chunk: string): void;
|
|
43
|
-
onToolCall?(name: string, args: Record<string, unknown>): void;
|
|
44
|
-
onToolResult?(name: string, output: string, isError: boolean): void;
|
|
45
|
-
onApproval?(name: string, args: Record<string, unknown>, granted: boolean): void;
|
|
46
|
-
onFinal?(r: {
|
|
47
|
-
text: string;
|
|
48
|
-
turns: number;
|
|
49
|
-
toolUses: number;
|
|
50
|
-
sessionId: string;
|
|
51
|
-
}): void;
|
|
52
|
-
}
|
|
53
|
-
export interface AgentTaskOptions {
|
|
54
|
-
task: string;
|
|
55
|
-
registry: Registry;
|
|
56
|
-
cfg?: HmhConfig;
|
|
57
|
-
ctx?: ToolContext;
|
|
58
|
-
yes?: boolean;
|
|
59
|
-
/** Overrides the terminal gate (web supplies a remote one). */
|
|
60
|
-
approvalAsk?: LoopApproval['ask'];
|
|
61
|
-
resumeMessages?: ChatMessage[];
|
|
62
|
-
events?: RunnerEvents;
|
|
63
|
-
}
|
|
64
|
-
/** Run one full agent task end-to-end; audit + insight recording included. */
|
|
65
|
-
export declare function runAgentTask(opts: AgentTaskOptions): Promise<LoopResult & {
|
|
66
|
-
sessionId: string;
|
|
67
|
-
toolsUsed: string[];
|
|
68
|
-
}>;
|
package/dist/runner.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @hmh/cli - runner
|
|
3
|
-
* The shared agent-task execution layer. CLI maps its events to terminal
|
|
4
|
-
* output; the web frontend maps them to SSE - one behavior, two frontends.
|
|
5
|
-
* Also owns the native registry factory (spawn_agent recursion) and the
|
|
6
|
-
* approval gate construction.
|
|
7
|
-
*/
|
|
8
|
-
import { homeDir, loadConfig, mcpServerTools, Registry, runLoop, Session, } from '@hmh/kernel';
|
|
9
|
-
import { listSkills, recentInsights, recordInsight, retrieveMemory, skillsToPrompt } from '@hmh/evolution';
|
|
10
|
-
import { harmonyTools } from '@hmh/domain-harmony';
|
|
11
|
-
import * as readline from 'node:readline/promises';
|
|
12
|
-
import { stdin } from 'node:process';
|
|
13
|
-
import { baseTools } from "./tools.js";
|
|
14
|
-
import { buildSystemPrompt } from "./prompt.js";
|
|
15
|
-
import { makeSpawnTool, MAX_SPAWN_DEPTH } from "./spawn.js";
|
|
16
|
-
/** Flatten the config.json shape into the runtime discriminated union. */
|
|
17
|
-
export function toServerConfig(c) {
|
|
18
|
-
if (c.type === 'http')
|
|
19
|
-
return { type: 'http', url: c.url ?? '', headers: c.headers, trusted: c.trusted };
|
|
20
|
-
return { type: 'stdio', command: c.command ?? '', args: c.args, env: c.env, trusted: c.trusted };
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Current spawn base, set per task so a long-lived registry (REPL, web
|
|
24
|
-
* server) always routes sub-agents to the CURRENT session and gate.
|
|
25
|
-
*/
|
|
26
|
-
export const spawnBase = {};
|
|
27
|
-
export function nativeRegistry(depth) {
|
|
28
|
-
const reg = new Registry();
|
|
29
|
-
reg.registerAll(baseTools).registerAll(harmonyTools);
|
|
30
|
-
if (depth < MAX_SPAWN_DEPTH) {
|
|
31
|
-
reg.register(makeSpawnTool({
|
|
32
|
-
depth,
|
|
33
|
-
getBase: () => spawnBase.current ?? {
|
|
34
|
-
provider: { baseUrl: '', apiKey: '', model: '' },
|
|
35
|
-
ctx: { cwd: process.cwd(), home: homeDir() },
|
|
36
|
-
},
|
|
37
|
-
buildChildRegistry: nativeRegistry,
|
|
38
|
-
}));
|
|
39
|
-
}
|
|
40
|
-
return reg;
|
|
41
|
-
}
|
|
42
|
-
export async function buildRegistry(opts = {}) {
|
|
43
|
-
const reg = nativeRegistry(0);
|
|
44
|
-
const clients = [];
|
|
45
|
-
if (opts.mcp !== false) {
|
|
46
|
-
const cfg = await loadConfig();
|
|
47
|
-
const servers = Object.entries(cfg.mcpServers ?? {});
|
|
48
|
-
if (servers.length > 0) {
|
|
49
|
-
await Promise.all(servers.map(async ([name, raw]) => {
|
|
50
|
-
try {
|
|
51
|
-
const { client, tools } = await mcpServerTools(name, toServerConfig(raw));
|
|
52
|
-
for (const t of tools) {
|
|
53
|
-
try {
|
|
54
|
-
reg.register(t);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
/* name collision after sanitization - first server wins */
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
clients.push(client);
|
|
61
|
-
if (opts.announce !== false)
|
|
62
|
-
console.log(` [mcp] ${name}: ${tools.length} tools attached`);
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
if (opts.announce !== false)
|
|
66
|
-
console.log(` [mcp] ${name}: unavailable (${String(err).slice(0, 140)})`);
|
|
67
|
-
}
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return { reg, clients };
|
|
72
|
-
}
|
|
73
|
-
/** Retrieval-based context pack: task-relevant memories, not the whole file. */
|
|
74
|
-
export async function contextPack(task) {
|
|
75
|
-
const home = homeDir();
|
|
76
|
-
const [memory, skills, insights] = await Promise.all([
|
|
77
|
-
retrieveMemory(home, task),
|
|
78
|
-
listSkills(home),
|
|
79
|
-
recentInsights(home),
|
|
80
|
-
]);
|
|
81
|
-
return { memory, skills: skillsToPrompt(skills), insights };
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Terminal approval gate: auto mode passes everything; a TTY gets a y/N
|
|
85
|
-
* prompt (reusing a caller-provided readline); a pipe gets a safe deny.
|
|
86
|
-
* The kernel loop denies by default when no gate is wired at all.
|
|
87
|
-
*/
|
|
88
|
-
export function makeApproval(cfg, yes, sharedRl) {
|
|
89
|
-
return {
|
|
90
|
-
async ask(toolName, args) {
|
|
91
|
-
if (yes || cfg.approval === 'auto')
|
|
92
|
-
return true;
|
|
93
|
-
const brief = JSON.stringify(args).slice(0, 120);
|
|
94
|
-
if (!stdin.isTTY) {
|
|
95
|
-
process.stdout.write(`\x1b[33m [approval] ${toolName} ${brief} — denied (no TTY; use --yes to allow)\x1b[0m\n`);
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
const rl = sharedRl ?? readline.createInterface({ input: stdin, output: process.stdout });
|
|
99
|
-
let answer;
|
|
100
|
-
try {
|
|
101
|
-
answer = (await rl.question(`\x1b[33m [approval] ${toolName} ${brief} — run it? [y/N] \x1b[0m`)).trim().toLowerCase();
|
|
102
|
-
}
|
|
103
|
-
finally {
|
|
104
|
-
if (!sharedRl)
|
|
105
|
-
rl.close();
|
|
106
|
-
}
|
|
107
|
-
return answer === 'y' || answer === 'yes';
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
/** Run one full agent task end-to-end; audit + insight recording included. */
|
|
112
|
-
export async function runAgentTask(opts) {
|
|
113
|
-
const cfg = opts.cfg ?? (await loadConfig());
|
|
114
|
-
const ctx = opts.ctx ?? { cwd: process.cwd(), home: homeDir() };
|
|
115
|
-
const events = opts.events ?? {};
|
|
116
|
-
const pack = await contextPack(opts.task);
|
|
117
|
-
const system = buildSystemPrompt({
|
|
118
|
-
cwd: ctx.cwd,
|
|
119
|
-
memory: pack.memory,
|
|
120
|
-
skills: pack.skills,
|
|
121
|
-
insights: pack.insights,
|
|
122
|
-
model: cfg.provider.model,
|
|
123
|
-
});
|
|
124
|
-
const session = new Session(ctx.home, ctx.cwd, cfg.provider.model);
|
|
125
|
-
await session.user(opts.task);
|
|
126
|
-
const approval = opts.approvalAsk ? { ask: opts.approvalAsk } : makeApproval(cfg, opts.yes === true);
|
|
127
|
-
spawnBase.current = {
|
|
128
|
-
provider: cfg.provider,
|
|
129
|
-
ctx,
|
|
130
|
-
approval,
|
|
131
|
-
session,
|
|
132
|
-
onLine: (l) => events.onLine?.(l),
|
|
133
|
-
};
|
|
134
|
-
const messages = [
|
|
135
|
-
{ role: 'system', content: system },
|
|
136
|
-
...(opts.resumeMessages ?? []),
|
|
137
|
-
{ role: 'user', content: opts.task },
|
|
138
|
-
];
|
|
139
|
-
const toolsUsed = [];
|
|
140
|
-
const result = await runLoop({
|
|
141
|
-
provider: cfg.provider,
|
|
142
|
-
registry: opts.registry,
|
|
143
|
-
messages,
|
|
144
|
-
ctx,
|
|
145
|
-
maxTurns: cfg.maxTurns,
|
|
146
|
-
maxContextChars: cfg.maxContextChars,
|
|
147
|
-
approval: spawnBase.current.approval,
|
|
148
|
-
events: {
|
|
149
|
-
onDelta: (kind, chunk) => events.onDelta?.(kind, chunk),
|
|
150
|
-
onToolCall: (name, args) => {
|
|
151
|
-
toolsUsed.push(name);
|
|
152
|
-
events.onToolCall?.(name, args);
|
|
153
|
-
},
|
|
154
|
-
onToolResult: (name, output, isError) => {
|
|
155
|
-
void session.tool(name, output, isError);
|
|
156
|
-
events.onToolResult?.(name, output, isError);
|
|
157
|
-
},
|
|
158
|
-
onApproval: (name, args, granted) => {
|
|
159
|
-
void session.approval(name, granted);
|
|
160
|
-
events.onApproval?.(name, args, granted);
|
|
161
|
-
},
|
|
162
|
-
onAssistant: async (m) => {
|
|
163
|
-
await session.assistant(m.content ?? null, m.tool_calls);
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
await session.final(result.text, result.turns, result.toolUses);
|
|
168
|
-
await recordInsight(ctx.home, {
|
|
169
|
-
time: new Date().toISOString(),
|
|
170
|
-
session: session.id,
|
|
171
|
-
task: opts.task.slice(0, 120),
|
|
172
|
-
outcome: result.turns >= cfg.maxTurns ? 'turn-budget' : 'ok',
|
|
173
|
-
turns: result.turns,
|
|
174
|
-
toolUses: result.toolUses,
|
|
175
|
-
toolsUsed: [...new Set(toolsUsed)],
|
|
176
|
-
});
|
|
177
|
-
events.onFinal?.({ text: result.text, turns: result.turns, toolUses: result.toolUses, sessionId: session.id });
|
|
178
|
-
return { ...result, sessionId: session.id, toolsUsed: [...new Set(toolsUsed)] };
|
|
179
|
-
}
|
package/dist/spawn.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @hmh/cli - spawn
|
|
3
|
-
* The sub-agent tool: run a nested agent loop with a FRESH context on a
|
|
4
|
-
* self-contained subtask and return its final answer. Children share the
|
|
5
|
-
* tool registry (minus MCP - children stay fast and deterministic) and the
|
|
6
|
-
* approval gate, but never the parent's conversation - context isolation
|
|
7
|
-
* is the point. Depth-capped so a confused model can't fork-bomb itself.
|
|
8
|
-
*/
|
|
9
|
-
import { type Session, type LoopApproval, type Registry, type Tool } from '@hmh/kernel';
|
|
10
|
-
export declare const MAX_SPAWN_DEPTH = 2;
|
|
11
|
-
export interface SpawnBase {
|
|
12
|
-
provider: import('@hmh/kernel').ProviderConfig;
|
|
13
|
-
ctx: import('@hmh/kernel').ToolContext;
|
|
14
|
-
approval?: LoopApproval;
|
|
15
|
-
session?: Session;
|
|
16
|
-
/** Bubbled tool traffic for display: `[sub1] list_dir {...}`. */
|
|
17
|
-
onLine?(line: string): void;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Resolved lazily at each spawn so a long-lived REPL registry always sees the
|
|
21
|
-
* CURRENT task's session/approval, not the one from when it was built.
|
|
22
|
-
*/
|
|
23
|
-
export interface SpawnDeps {
|
|
24
|
-
depth: number;
|
|
25
|
-
getBase(): SpawnBase;
|
|
26
|
-
/** Build the registry for a child at the given depth (no spawn at the cap). */
|
|
27
|
-
buildChildRegistry(depth: number): Registry;
|
|
28
|
-
}
|
|
29
|
-
export declare function makeSpawnTool(deps: SpawnDeps): Tool;
|
package/dist/spawn.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @hmh/cli - spawn
|
|
3
|
-
* The sub-agent tool: run a nested agent loop with a FRESH context on a
|
|
4
|
-
* self-contained subtask and return its final answer. Children share the
|
|
5
|
-
* tool registry (minus MCP - children stay fast and deterministic) and the
|
|
6
|
-
* approval gate, but never the parent's conversation - context isolation
|
|
7
|
-
* is the point. Depth-capped so a confused model can't fork-bomb itself.
|
|
8
|
-
*/
|
|
9
|
-
import { runLoop } from '@hmh/kernel';
|
|
10
|
-
export const MAX_SPAWN_DEPTH = 2;
|
|
11
|
-
export function makeSpawnTool(deps) {
|
|
12
|
-
return {
|
|
13
|
-
name: 'spawn_agent',
|
|
14
|
-
description: 'Run a sub-agent with a fresh context on a self-contained subtask (e.g. "explore the project layout and report module names", "find which file defines X"). Returns the sub-agent\'s final answer. The sub-agent has the same tools but NO conversation history and no MCP tools - include every detail it needs in the task. Use it to keep this conversation small: delegate exploration and focused lookups.',
|
|
15
|
-
parameters: {
|
|
16
|
-
type: 'object',
|
|
17
|
-
properties: {
|
|
18
|
-
task: { type: 'string', description: 'complete, self-contained instructions for the sub-agent' },
|
|
19
|
-
max_turns: { type: 'number', description: 'turn budget for the sub-agent (default 8, max 12)' },
|
|
20
|
-
},
|
|
21
|
-
required: ['task'],
|
|
22
|
-
},
|
|
23
|
-
async execute(args) {
|
|
24
|
-
const base = deps.getBase();
|
|
25
|
-
if (deps.depth >= MAX_SPAWN_DEPTH) {
|
|
26
|
-
return { output: `spawn depth cap (${MAX_SPAWN_DEPTH}) reached. Do the work directly instead.`, isError: true };
|
|
27
|
-
}
|
|
28
|
-
const task = String(args.task ?? '').trim();
|
|
29
|
-
if (!task)
|
|
30
|
-
return { output: 'spawn_agent requires a non-empty task.', isError: true };
|
|
31
|
-
const childDepth = deps.depth + 1;
|
|
32
|
-
const tag = `sub${childDepth}`;
|
|
33
|
-
const maxTurns = Math.min(Math.max(Number(args.max_turns ?? 8), 1), 12);
|
|
34
|
-
const registry = deps.buildChildRegistry(childDepth);
|
|
35
|
-
const system = [
|
|
36
|
-
`You are a hmh sub-agent (depth ${childDepth}). You have no conversation history beyond this task.`,
|
|
37
|
-
'Do exactly what the task asks, use tools as needed, verify before answering, and reply with a concise result (the caller only sees your final answer).',
|
|
38
|
-
].join('\n');
|
|
39
|
-
base.onLine?.(`[${tag}] start: ${task.slice(0, 80)}`);
|
|
40
|
-
const result = await runLoop({
|
|
41
|
-
provider: base.provider,
|
|
42
|
-
registry,
|
|
43
|
-
messages: [
|
|
44
|
-
{ role: 'system', content: system },
|
|
45
|
-
{ role: 'user', content: task },
|
|
46
|
-
],
|
|
47
|
-
ctx: base.ctx,
|
|
48
|
-
maxTurns,
|
|
49
|
-
approval: base.approval,
|
|
50
|
-
events: {
|
|
51
|
-
onToolCall: (name, a) => base.onLine?.(`[${tag}] ${name} ${JSON.stringify(a).slice(0, 80)}`),
|
|
52
|
-
onToolResult: (name, output, isError) => {
|
|
53
|
-
void base.session?.tool(`${tag}>${name}`, output, isError);
|
|
54
|
-
if (isError)
|
|
55
|
-
base.onLine?.(`[${tag}] ${name} ERROR: ${output.slice(0, 100)}`);
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
const text = result.text || '(sub-agent returned empty output)';
|
|
60
|
-
base.onLine?.(`[${tag}] done (${result.turns} turns, ${result.toolUses} tool uses)`);
|
|
61
|
-
return { output: text.length > 20_000 ? text.slice(0, 20_000) + '\n...[truncated]' : text };
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
}
|
package/dist/tools.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Tool } from '@hmh/kernel';
|
|
2
|
-
export declare const readFileTool: Tool;
|
|
3
|
-
export declare const writeFileTool: Tool;
|
|
4
|
-
export declare const listDirTool: Tool;
|
|
5
|
-
export declare const runCommandTool: Tool;
|
|
6
|
-
export declare const rememberTool: Tool;
|
|
7
|
-
export declare const baseTools: Tool[];
|
package/dist/tools.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @hmh/cli - base tools
|
|
3
|
-
* The general coding-agent toolset: read, write, list, and a guarded shell
|
|
4
|
-
* runner. Deny-first guard on obviously destructive one-liners; the real
|
|
5
|
-
* sandbox/approval stack is a Phase 1 kernel feature.
|
|
6
|
-
*/
|
|
7
|
-
import { exec } from 'node:child_process';
|
|
8
|
-
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
|
9
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
10
|
-
import { promisify } from 'node:util';
|
|
11
|
-
const execCb = promisify(exec);
|
|
12
|
-
const DENY_PATTERNS = [
|
|
13
|
-
{ re: /rm\s+-rf?\s+[/~C:\\]|format\s+[a-z]:|del\s+\/[sq]/i, why: 'recursive delete of a root/home path' },
|
|
14
|
-
{ re: /shutdown|restart\s+computer|taskkill\s+\/f\s+\/im\s+explorer/i, why: 'system power/shell action' },
|
|
15
|
-
{ re: /reg\s+(delete|add).*(Run|CurrentVersion)/i, why: 'autostart registry mutation' },
|
|
16
|
-
];
|
|
17
|
-
function safePath(p, cwd) {
|
|
18
|
-
return isAbsolute(p) ? p : resolve(cwd, p);
|
|
19
|
-
}
|
|
20
|
-
export const readFileTool = {
|
|
21
|
-
name: 'read_file',
|
|
22
|
-
description: 'Read a text file. Returns the full content (truncated at 60k chars).',
|
|
23
|
-
parameters: {
|
|
24
|
-
type: 'object',
|
|
25
|
-
properties: { path: { type: 'string', description: 'file path (absolute or relative to cwd)' } },
|
|
26
|
-
required: ['path'],
|
|
27
|
-
},
|
|
28
|
-
async execute(args, ctx) {
|
|
29
|
-
try {
|
|
30
|
-
const text = await readFile(safePath(String(args.path), ctx.cwd), 'utf8');
|
|
31
|
-
return { output: text.length > 60_000 ? text.slice(0, 60_000) + '\n...[truncated]' : text };
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
return { output: String(err), isError: true };
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
export const writeFileTool = {
|
|
39
|
-
name: 'write_file',
|
|
40
|
-
description: 'Write a text file (creates or overwrites). Use for code, config, docs.',
|
|
41
|
-
parameters: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
properties: {
|
|
44
|
-
path: { type: 'string', description: 'file path' },
|
|
45
|
-
content: { type: 'string', description: 'full file content' },
|
|
46
|
-
},
|
|
47
|
-
required: ['path', 'content'],
|
|
48
|
-
},
|
|
49
|
-
needsApproval: () => true,
|
|
50
|
-
async execute(args, ctx) {
|
|
51
|
-
try {
|
|
52
|
-
const p = safePath(String(args.path), ctx.cwd);
|
|
53
|
-
await writeFile(p, String(args.content ?? ''), 'utf8');
|
|
54
|
-
return { output: `wrote ${String(args.content ?? '').length} chars to ${p}` };
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
return { output: String(err), isError: true };
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
export const listDirTool = {
|
|
62
|
-
name: 'list_dir',
|
|
63
|
-
description: 'List a directory: names with d/- prefix and size.',
|
|
64
|
-
parameters: {
|
|
65
|
-
type: 'object',
|
|
66
|
-
properties: { path: { type: 'string', description: 'directory path (default cwd)' } },
|
|
67
|
-
required: [],
|
|
68
|
-
},
|
|
69
|
-
async execute(args, ctx) {
|
|
70
|
-
try {
|
|
71
|
-
const dir = args.path ? safePath(String(args.path), ctx.cwd) : ctx.cwd;
|
|
72
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
73
|
-
const lines = entries.slice(0, 300).map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`);
|
|
74
|
-
return { output: `${dir}\n${lines.join('\n') || '(empty)'}` };
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
return { output: String(err), isError: true };
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
export const runCommandTool = {
|
|
82
|
-
name: 'run_command',
|
|
83
|
-
description: 'Run a shell command (one line, cmd on Windows / sh elsewhere) with a timeout. Prefer focused commands; read output carefully before deciding next steps.',
|
|
84
|
-
parameters: {
|
|
85
|
-
type: 'object',
|
|
86
|
-
properties: {
|
|
87
|
-
command: { type: 'string', description: 'the command line to run' },
|
|
88
|
-
timeout_ms: { type: 'number', description: 'timeout in ms (default 60000, max 300000)' },
|
|
89
|
-
},
|
|
90
|
-
required: ['command'],
|
|
91
|
-
},
|
|
92
|
-
needsApproval: () => true,
|
|
93
|
-
async execute(args, ctx) {
|
|
94
|
-
const command = String(args.command ?? '');
|
|
95
|
-
for (const d of DENY_PATTERNS) {
|
|
96
|
-
if (d.re.test(command)) {
|
|
97
|
-
return { output: `Refused: ${d.why}. Ask the user to run it manually if truly intended.`, isError: true };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const timeout = Math.min(Number(args.timeout_ms ?? 60_000), 300_000);
|
|
101
|
-
try {
|
|
102
|
-
const { stdout, stderr } = await execCb(command, {
|
|
103
|
-
cwd: ctx.cwd,
|
|
104
|
-
timeout,
|
|
105
|
-
windowsHide: true,
|
|
106
|
-
maxBuffer: 8 * 1024 * 1024,
|
|
107
|
-
});
|
|
108
|
-
const out = (stdout || '') + (stderr ? `\n[stderr]\n${stderr}` : '');
|
|
109
|
-
return { output: (out.trim() || '(no output)').slice(0, 60_000) };
|
|
110
|
-
}
|
|
111
|
-
catch (err) {
|
|
112
|
-
const e = err;
|
|
113
|
-
const parts = [e.stdout, e.stderr, e.killed ? '(timed out)' : null, e.message].filter(Boolean).join('\n');
|
|
114
|
-
return { output: parts.slice(0, 60_000), isError: true };
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
export const rememberTool = {
|
|
119
|
-
name: 'remember',
|
|
120
|
-
description: 'Persist a note to long-term memory (survives restarts, loaded into future sessions). Use for user preferences, project facts, and hard-won lessons - not transient details.',
|
|
121
|
-
parameters: {
|
|
122
|
-
type: 'object',
|
|
123
|
-
properties: { note: { type: 'string', description: 'the fact/lesson to remember, one line preferred' } },
|
|
124
|
-
required: ['note'],
|
|
125
|
-
},
|
|
126
|
-
async execute(args) {
|
|
127
|
-
try {
|
|
128
|
-
const { appendMemory } = await import('@hmh/evolution');
|
|
129
|
-
await appendMemory(process.env.HMH_HOME ?? join(process.env.USERPROFILE ?? '.', '.hmharness'), String(args.note ?? ''));
|
|
130
|
-
return { output: 'remembered.' };
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
return { output: String(err), isError: true };
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
export const baseTools = [readFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool];
|