@hmharness/agent 0.19.6 → 0.19.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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/rlm-tool.d.ts +14 -0
- package/dist/rlm-tool.js +89 -0
- package/dist/runner.js +6 -2
- package/dist/tools.js +2 -1
- package/dist/topology-live.d.ts +37 -0
- package/dist/topology-live.js +111 -0
- package/package.json +8 -8
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { mechanicalGate, parseVerdict, runPipeline, type PipelineOptions, type P
|
|
|
5
5
|
export { buildSystemPrompt } from './prompt.js';
|
|
6
6
|
export { strings, type Locale, type Strings } from './i18n.js';
|
|
7
7
|
export { makeSpawnTool, MAX_SPAWN_DEPTH, type SpawnBase } from './spawn.js';
|
|
8
|
+
export { withTopologyGovernance, mapRole, liveTopologySnapshot, teamLog } from './topology-live.js';
|
|
8
9
|
export { buildRegistry, contextPack, makeApproval, liveYolo, nativeRegistry, runAgentTask, spawnBase, toServerConfig, type AgentTaskOptions, type RunnerEvents, } from './runner.js';
|
|
9
10
|
export * from './skills-conformance.js';
|
|
10
11
|
export * from './acp.js';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { mechanicalGate, parseVerdict, runPipeline, } from "./pipeline.js";
|
|
|
5
5
|
export { buildSystemPrompt } from "./prompt.js";
|
|
6
6
|
export { strings } from "./i18n.js";
|
|
7
7
|
export { makeSpawnTool, MAX_SPAWN_DEPTH } from "./spawn.js";
|
|
8
|
+
export { withTopologyGovernance, mapRole, liveTopologySnapshot, teamLog } from "./topology-live.js";
|
|
8
9
|
export { buildRegistry, contextPack, makeApproval, liveYolo, nativeRegistry, runAgentTask, spawnBase, toServerConfig, } from "./runner.js";
|
|
9
10
|
export * from "./skills-conformance.js";
|
|
10
11
|
export * from "./acp.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/agent - RLM workspace tool (blueprint M5 / RLM-001..010 live)
|
|
3
|
+
*
|
|
4
|
+
* Gives the LIVE model a persistent, programmable cognitive workspace for
|
|
5
|
+
* the duration of a task: set/get variables, eval sandboxed code against
|
|
6
|
+
* them, checkpoint/restore, fork. The runtime enforces the budget and
|
|
7
|
+
* freezes its governance state — the model composes context, it can never
|
|
8
|
+
* rewrite the meter (no hidden unsafe self-modification, RLM-008).
|
|
9
|
+
*
|
|
10
|
+
* One workspace per session (spawned subagents share the parent's by home
|
|
11
|
+
* key — deliberately: delegation sees the same whiteboard).
|
|
12
|
+
*/
|
|
13
|
+
import type { Tool } from '@hmharness/kernel';
|
|
14
|
+
export declare const rlmWorkspaceTool: Tool;
|
package/dist/rlm-tool.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { RLMRuntime } from '@hmharness/cognitive';
|
|
2
|
+
const workspaces = new Map();
|
|
3
|
+
function wsFor(home) {
|
|
4
|
+
let ws = workspaces.get(home);
|
|
5
|
+
if (!ws) {
|
|
6
|
+
ws = new RLMRuntime({ maxEvals: 200, maxWallMs: 30 * 60_000, maxSubagents: 0 });
|
|
7
|
+
workspaces.set(home, ws);
|
|
8
|
+
}
|
|
9
|
+
return ws;
|
|
10
|
+
}
|
|
11
|
+
export const rlmWorkspaceTool = {
|
|
12
|
+
name: 'rlm_workspace',
|
|
13
|
+
description: 'Persistent structured workspace for THIS task: variables survive between calls (unlike your context window). ' +
|
|
14
|
+
'Actions: set(name,value) / get(name) / list() / eval(code — JS, `ctx.vars` holds the variables, return a value) / ' +
|
|
15
|
+
'checkpoint(label?) / restore(checkpointId) / reset(). Use it to accumulate findings, counts, plans, intermediate ' +
|
|
16
|
+
'results across many tool calls, then eval a composition over them. eval is sandboxed and budget-metered.',
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
action: { type: 'string', description: 'set | get | list | eval | checkpoint | restore | reset' },
|
|
21
|
+
name: { type: 'string', description: 'variable name (set/get)' },
|
|
22
|
+
value: { type: 'string', description: 'value to set (stored as string; eval can JSON.parse)' },
|
|
23
|
+
code: { type: 'string', description: 'eval body: `return ...` over ctx.vars' },
|
|
24
|
+
checkpointId: { type: 'string', description: 'restore target (from checkpoint result)' },
|
|
25
|
+
label: { type: 'string', description: 'checkpoint label' },
|
|
26
|
+
},
|
|
27
|
+
required: ['action'],
|
|
28
|
+
},
|
|
29
|
+
async execute(args, ctx) {
|
|
30
|
+
const action = String(args.action ?? '');
|
|
31
|
+
const ws = wsFor(ctx.home);
|
|
32
|
+
try {
|
|
33
|
+
switch (action) {
|
|
34
|
+
case 'set': {
|
|
35
|
+
const name = String(args.name ?? '');
|
|
36
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
37
|
+
return { output: `invalid variable name '${name}'`, isError: true };
|
|
38
|
+
ws.set(name, String(args.value ?? ''));
|
|
39
|
+
return { output: `set ${name} (${String(args.value ?? '').length} chars)` };
|
|
40
|
+
}
|
|
41
|
+
case 'get': {
|
|
42
|
+
const v = ws.get(String(args.name ?? ''));
|
|
43
|
+
if (v === undefined)
|
|
44
|
+
return { output: `undefined`, isError: true };
|
|
45
|
+
return { output: typeof v === 'string' ? v : JSON.stringify(v) };
|
|
46
|
+
}
|
|
47
|
+
case 'list':
|
|
48
|
+
return { output: ws.names().length ? ws.names().join(', ') : '(empty)' };
|
|
49
|
+
case 'eval': {
|
|
50
|
+
const code = String(args.code ?? '');
|
|
51
|
+
if (!code.trim())
|
|
52
|
+
return { output: 'code required', isError: true };
|
|
53
|
+
const r = await ws.eval(code);
|
|
54
|
+
if (!r.ok)
|
|
55
|
+
return { output: `eval failed [${r.error?.code}]: ${r.error?.message}`, isError: true };
|
|
56
|
+
const text = r.value === undefined ? 'undefined' : typeof r.value === 'string' ? r.value : JSON.stringify(r.value);
|
|
57
|
+
return { output: text.length > 8_000 ? text.slice(0, 8_000) + '…[truncated]' : text };
|
|
58
|
+
}
|
|
59
|
+
case 'checkpoint': {
|
|
60
|
+
const cp = ws.checkpoint(args.label ? String(args.label) : undefined);
|
|
61
|
+
checkpoints.set(`${ctx.home}::${cp.id}`, cp);
|
|
62
|
+
return { output: `checkpoint ${cp.id}${cp.label ? ` (${cp.label})` : ''} — restore with checkpointId=${cp.id}` };
|
|
63
|
+
}
|
|
64
|
+
case 'restore': {
|
|
65
|
+
const id = String(args.checkpointId ?? '');
|
|
66
|
+
if (!id)
|
|
67
|
+
return { output: 'checkpointId required', isError: true };
|
|
68
|
+
// restore needs the checkpoint object; keep an id->cp map on the tool side
|
|
69
|
+
const cp = checkpoints.get(`${ctx.home}::${id}`);
|
|
70
|
+
if (!cp)
|
|
71
|
+
return { output: `unknown checkpoint ${id} (checkpoints live in-process)`, isError: true };
|
|
72
|
+
ws.restore(cp);
|
|
73
|
+
return { output: `restored ${id}` };
|
|
74
|
+
}
|
|
75
|
+
case 'reset': {
|
|
76
|
+
workspaces.delete(ctx.home);
|
|
77
|
+
checkpoints.delete(ctx.home);
|
|
78
|
+
return { output: 'workspace cleared' };
|
|
79
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
return { output: `unknown action '${action}' (set|get|list|eval|checkpoint|restore|reset)`, isError: true };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return { output: 'rlm_workspace failed: ' + String(err).slice(0, 200), isError: true };
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
const checkpoints = new Map();
|
package/dist/runner.js
CHANGED
|
@@ -21,6 +21,7 @@ import { baseTools } from "./tools.js";
|
|
|
21
21
|
import { buildSystemPrompt } from "./prompt.js";
|
|
22
22
|
import { strings } from "./i18n.js";
|
|
23
23
|
import { makeSpawnTool, MAX_SPAWN_DEPTH } from "./spawn.js";
|
|
24
|
+
import { withTopologyGovernance } from "./topology-live.js";
|
|
24
25
|
/** Flatten the config.json shape into the runtime discriminated union. */
|
|
25
26
|
export function toServerConfig(c) {
|
|
26
27
|
if (c.type === 'http')
|
|
@@ -36,14 +37,17 @@ export function nativeRegistry(depth) {
|
|
|
36
37
|
const reg = new Registry();
|
|
37
38
|
reg.registerAll(baseTools).registerAll(harmonyTools).registerAll(opsTools);
|
|
38
39
|
if (depth < MAX_SPAWN_DEPTH) {
|
|
39
|
-
|
|
40
|
+
// blueprint M10: every spawn_agent is governed by the live topology
|
|
41
|
+
// (role contract + shared budget + immutable multi-agent.jsonl audit);
|
|
42
|
+
// governance failure falls back to the bare spawn (never blocks work)
|
|
43
|
+
reg.register(withTopologyGovernance(makeSpawnTool({
|
|
40
44
|
depth,
|
|
41
45
|
getBase: () => spawnBase.current ?? {
|
|
42
46
|
provider: { baseUrl: '', apiKey: '', model: '' },
|
|
43
47
|
ctx: { cwd: process.cwd(), home: homeDir() },
|
|
44
48
|
},
|
|
45
49
|
buildChildRegistry: nativeRegistry,
|
|
46
|
-
}));
|
|
50
|
+
})));
|
|
47
51
|
}
|
|
48
52
|
return reg;
|
|
49
53
|
}
|
package/dist/tools.js
CHANGED
|
@@ -10,6 +10,7 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
10
10
|
import { promisify } from 'node:util';
|
|
11
11
|
import { chatVision, homeDir, isBareProbe, isVisionRefusal, loadConfig, visionProviderChain } from '@hmharness/kernel';
|
|
12
12
|
import { cognitiveQueryTool } from "./cognitive-tools.js";
|
|
13
|
+
import { rlmWorkspaceTool } from "./rlm-tool.js";
|
|
13
14
|
const execCb = promisify(exec);
|
|
14
15
|
const DENY_PATTERNS = [
|
|
15
16
|
// recursive deletes aimed at ROOT/HOME/SYSTEM targets only - relative
|
|
@@ -663,5 +664,5 @@ export const seeImageTool = {
|
|
|
663
664
|
export const baseTools = [
|
|
664
665
|
readFileTool, editFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool, seeImageTool,
|
|
665
666
|
webSearchTool, webFetchTool, browserOpenTool, desktopScreenshotTool, desktopClickTool, desktopTypeTool, sshRunTool,
|
|
666
|
-
cognitiveQueryTool,
|
|
667
|
+
cognitiveQueryTool, rlmWorkspaceTool,
|
|
667
668
|
];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/agent - live multi-agent governance (blueprint §11 / MA-001..010)
|
|
3
|
+
*
|
|
4
|
+
* Wraps the REAL spawn_agent tool with blueprint-2.0 governance: every spawn
|
|
5
|
+
* becomes a topology node with a role contract (timeout/budget/heartbeat),
|
|
6
|
+
* spends from the shared blackboard budget, and appends an immutable event
|
|
7
|
+
* to cognitive/multi-agent.jsonl — process-agnostic, so the CLI and web
|
|
8
|
+
* panel can replay any run's team structure long after it finished.
|
|
9
|
+
*
|
|
10
|
+
* v1 heartbeat = start/finish stamps (subagents are short-lived); the
|
|
11
|
+
* watchdog threshold (3x the 30s role interval) only bites on stuck nodes.
|
|
12
|
+
*/
|
|
13
|
+
import type { Tool } from '@hmharness/kernel';
|
|
14
|
+
import { type AgentRoleName } from '@hmharness/cognitive';
|
|
15
|
+
/** map the tool-facing role vocabulary onto blueprint roles */
|
|
16
|
+
export declare function mapRole(raw: string): AgentRoleName;
|
|
17
|
+
export declare function liveTopologySnapshot(): {
|
|
18
|
+
goal: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
nodes: Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
role: string;
|
|
23
|
+
status: string;
|
|
24
|
+
startedAt?: string;
|
|
25
|
+
endedAt?: string;
|
|
26
|
+
failure?: {
|
|
27
|
+
code: string;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
} | null;
|
|
32
|
+
export declare function teamLog(limit?: number): Promise<Array<Record<string, unknown>>>;
|
|
33
|
+
/** Wrap a spawn-like tool with topology governance. Non-fatal by contract:
|
|
34
|
+
* governance failures never break the underlying spawn. */
|
|
35
|
+
export declare function withTopologyGovernance(tool: Tool, opts?: {
|
|
36
|
+
task?: () => string;
|
|
37
|
+
}): Tool;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homeDir } from '@hmharness/kernel';
|
|
4
|
+
import { defineTopology, AgentTopology } from '@hmharness/cognitive';
|
|
5
|
+
/** map the tool-facing role vocabulary onto blueprint roles */
|
|
6
|
+
export function mapRole(raw) {
|
|
7
|
+
const r = raw.toLowerCase();
|
|
8
|
+
if (r.includes('plan'))
|
|
9
|
+
return 'planner';
|
|
10
|
+
if (r.includes('test') || r.includes('verif') || r.includes('judge'))
|
|
11
|
+
return 'verifier';
|
|
12
|
+
if (r.includes('review') || r.includes('critic'))
|
|
13
|
+
return 'critic';
|
|
14
|
+
if (r.includes('research') || r.includes('explore'))
|
|
15
|
+
return 'researcher';
|
|
16
|
+
if (r.includes('repair'))
|
|
17
|
+
return 'implementer';
|
|
18
|
+
return 'implementer';
|
|
19
|
+
}
|
|
20
|
+
/** one live topology per process, seeded from the first governed spawn */
|
|
21
|
+
const live = {};
|
|
22
|
+
export function liveTopologySnapshot() {
|
|
23
|
+
const t = live.current;
|
|
24
|
+
if (!t)
|
|
25
|
+
return null;
|
|
26
|
+
return {
|
|
27
|
+
goal: t.goal,
|
|
28
|
+
createdAt: t.createdAt,
|
|
29
|
+
nodes: t.topology.view().map((n) => ({ id: n.id, role: n.contract.role, status: n.status, startedAt: n.startedAt, endedAt: n.endedAt, failure: n.failure })),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function auditTeam(event) {
|
|
33
|
+
try {
|
|
34
|
+
const home = homeDir();
|
|
35
|
+
const dir = join(home, 'cognitive');
|
|
36
|
+
await mkdir(dir, { recursive: true });
|
|
37
|
+
await appendFile(join(dir, 'multi-agent.jsonl'), JSON.stringify({ at: new Date().toISOString(), ...event }) + '\n', 'utf8');
|
|
38
|
+
}
|
|
39
|
+
catch { /* governance audit is best-effort, never blocks the spawn */ }
|
|
40
|
+
}
|
|
41
|
+
export async function teamLog(limit = 30) {
|
|
42
|
+
try {
|
|
43
|
+
const text = await readFile(join(homeDir(), 'cognitive', 'multi-agent.jsonl'), 'utf8');
|
|
44
|
+
return text.split('\n').filter((l) => l.trim()).slice(-limit).map((l) => JSON.parse(l));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Wrap a spawn-like tool with topology governance. Non-fatal by contract:
|
|
51
|
+
* governance failures never break the underlying spawn. */
|
|
52
|
+
export function withTopologyGovernance(tool, opts = {}) {
|
|
53
|
+
const original = tool.execute.bind(tool);
|
|
54
|
+
return {
|
|
55
|
+
...tool,
|
|
56
|
+
async execute(args, ctx) {
|
|
57
|
+
const task = String(args.task ?? '').slice(0, 120);
|
|
58
|
+
const role = String(args.role ?? '');
|
|
59
|
+
const maxTurns = Math.min(Math.max(Number(args.max_turns ?? 8), 1), 12);
|
|
60
|
+
const blueprintRole = mapRole(role || 'implementer');
|
|
61
|
+
try {
|
|
62
|
+
if (!live.current) {
|
|
63
|
+
const spec = defineTopology([blueprintRole, 'supervisor']);
|
|
64
|
+
live.current = {
|
|
65
|
+
topology: new AgentTopology(spec, {
|
|
66
|
+
goalGraph: { nodes: [], edges: [] },
|
|
67
|
+
worldDigest: { beliefs: [], uncertainty: {} },
|
|
68
|
+
evidence: [],
|
|
69
|
+
budget: { totalCostUnits: 400, spent: 0 },
|
|
70
|
+
approvals: [],
|
|
71
|
+
artifacts: [],
|
|
72
|
+
}),
|
|
73
|
+
createdAt: new Date().toISOString(),
|
|
74
|
+
goal: opts.task?.() ?? task,
|
|
75
|
+
};
|
|
76
|
+
await auditTeam({ event: 'team.created', goal: live.current.goal.slice(0, 200) });
|
|
77
|
+
}
|
|
78
|
+
const t = live.current;
|
|
79
|
+
// a fresh node of this role per spawn (the topology grows with use)
|
|
80
|
+
const node = t.topology.addNode(blueprintRole);
|
|
81
|
+
// shared budget: each turn ~1 cost unit; over-budget spawns are denied
|
|
82
|
+
const spend = t.topology.trySpend(maxTurns);
|
|
83
|
+
if (!spend.ok) {
|
|
84
|
+
return { output: `team budget exhausted (${spend.spent}/${spend.total} cost units) - do the work directly instead`, isError: true };
|
|
85
|
+
}
|
|
86
|
+
t.topology.start(node.id);
|
|
87
|
+
await auditTeam({ event: 'spawn.started', nodeId: node.id, role: blueprintRole, roleLabel: role, task, budgetUnits: maxTurns });
|
|
88
|
+
const started = Date.now();
|
|
89
|
+
try {
|
|
90
|
+
const result = await original(args, ctx);
|
|
91
|
+
const failed = Boolean(result.isError);
|
|
92
|
+
if (failed)
|
|
93
|
+
t.topology.fail(node.id, { code: 'E_SUBAGENT', message: String(result.output).slice(0, 200) });
|
|
94
|
+
else
|
|
95
|
+
t.topology.finish(node.id, { text: String(result.output).slice(0, 400) });
|
|
96
|
+
await auditTeam({ event: failed ? 'spawn.failed' : 'spawn.done', nodeId: node.id, role: blueprintRole, turnsBudget: maxTurns, durationMs: Date.now() - started });
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
t.topology.fail(node.id, { code: 'E_SUBAGENT_THREW', message: String(err).slice(0, 200) });
|
|
101
|
+
await auditTeam({ event: 'spawn.failed', nodeId: node.id, role: blueprintRole, durationMs: Date.now() - started, error: String(err).slice(0, 200) });
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// governance itself must never break the spawn — run it bare
|
|
107
|
+
return original(args, ctx);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/agent",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.8",
|
|
4
4
|
"description": "hmharness agent execution layer: base tools, system prompt, sub-agent spawn, and the shared task runner that frontends (cli, web) drive.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
"build": "tsc -p tsconfig.build.json"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@hmharness/cognitive": "0.19.
|
|
19
|
-
"@hmharness/domain-harmony": "0.19.
|
|
20
|
-
"@hmharness/domain-ops": "0.19.
|
|
21
|
-
"@hmharness/evolution": "0.19.
|
|
22
|
-
"@hmharness/kernel": "0.19.
|
|
23
|
-
"@hmharness/observability": "0.19.
|
|
24
|
-
"@hmharness/sandbox": "0.19.
|
|
18
|
+
"@hmharness/cognitive": "0.19.8",
|
|
19
|
+
"@hmharness/domain-harmony": "0.19.8",
|
|
20
|
+
"@hmharness/domain-ops": "0.19.8",
|
|
21
|
+
"@hmharness/evolution": "0.19.8",
|
|
22
|
+
"@hmharness/kernel": "0.19.8",
|
|
23
|
+
"@hmharness/observability": "0.19.8",
|
|
24
|
+
"@hmharness/sandbox": "0.19.8"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist"
|