@hmharness/agent 0.19.5 → 0.19.7

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 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";
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
- reg.register(makeSpawnTool({
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
  }
@@ -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.5",
3
+ "version": "0.19.7",
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.5",
19
- "@hmharness/domain-harmony": "0.19.5",
20
- "@hmharness/domain-ops": "0.19.5",
21
- "@hmharness/evolution": "0.19.5",
22
- "@hmharness/kernel": "0.19.5",
23
- "@hmharness/observability": "0.19.5",
24
- "@hmharness/sandbox": "0.19.5"
18
+ "@hmharness/cognitive": "0.19.7",
19
+ "@hmharness/domain-harmony": "0.19.7",
20
+ "@hmharness/domain-ops": "0.19.7",
21
+ "@hmharness/evolution": "0.19.7",
22
+ "@hmharness/kernel": "0.19.7",
23
+ "@hmharness/observability": "0.19.7",
24
+ "@hmharness/sandbox": "0.19.7"
25
25
  },
26
26
  "files": [
27
27
  "dist"