@hmharness/agent 0.23.34 → 0.23.36

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/gate.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @hmharness/agent - gate (2026-10-15, v0.24 P2A research deploy)
3
+ *
4
+ * Production opt-in action gate. The 66-run live experiment arc proved the
5
+ * MECHANISM (WM -> kernel toolGate -> refusal -> model re-plans) and the
6
+ * SELECTIVITY (fires only on the corpus's genuinely divergent pair,
7
+ * read_file->edit_file 0.56 vs 0.83 baseline), but no synthetic task suite
8
+ * reproduces the real-session failures that produced the corpus signal.
9
+ * The next instrument is production A/B telemetry — this module makes that
10
+ * deployable: config `gate: { enabled: true, tau: 0.7 }` wires a
11
+ * sequence-conditioned gate into every run, default remains OFF.
12
+ *
13
+ * Policy (identical to scripts/p2a-intervention.mts arm C, extracted here
14
+ * so production and experiment share one implementation):
15
+ * - pair (last-executed type -> this type) with support >= MIN_SUPPORT
16
+ * decides, in BOTH directions (risky pair blocks, benign pair passes);
17
+ * - else the per-type rate with support >= MIN_SUPPORT decides;
18
+ * - else NO EVIDENCE = allow (the WM contract: knownUnknown is an explore
19
+ * signal, never 0% success).
20
+ */
21
+ import type { LoopToolGate } from '@hmharness/kernel';
22
+ export declare const GATE_MIN_SUPPORT = 10;
23
+ export interface Count {
24
+ s: number;
25
+ f: number;
26
+ }
27
+ export type StatTable = Map<string, Count>;
28
+ /** Pure decision policy over precomputed tables — the single implementation
29
+ * shared by the lab script and production (tested directly). */
30
+ export declare function conditionedPolicy(typeStat: StatTable, pairStat: StatTable, tau: number): {
31
+ check(name: string): {
32
+ allowed: true;
33
+ } | {
34
+ allowed: false;
35
+ reason: string;
36
+ };
37
+ stats: () => {
38
+ consults: number;
39
+ refusals: number;
40
+ types: number;
41
+ pairs: number;
42
+ tau: number;
43
+ };
44
+ };
45
+ export interface GateStats {
46
+ consults: number;
47
+ refusals: number;
48
+ types: number;
49
+ pairs: number;
50
+ tau: number;
51
+ }
52
+ /** Build the gate from this home's terminal corpus (same train-split
53
+ * protocol as the experiments: seed 1, 70/30). An empty corpus yields an
54
+ * all-allow gate — no evidence never blocks. */
55
+ export declare function buildConditionedGate(home: string, tau: number): Promise<LoopToolGate & {
56
+ stats(): GateStats;
57
+ }>;
package/dist/gate.js ADDED
@@ -0,0 +1,91 @@
1
+ import { loadTrajectories } from '@hmharness/cognitive';
2
+ export const GATE_MIN_SUPPORT = 10;
3
+ /** Pure decision policy over precomputed tables — the single implementation
4
+ * shared by the lab script and production (tested directly). */
5
+ export function conditionedPolicy(typeStat, pairStat, tau) {
6
+ let lastType = null; // last ALLOWED action (executions change the world, refusals do not)
7
+ let consults = 0;
8
+ let refusals = 0;
9
+ return {
10
+ check(name) {
11
+ consults++;
12
+ const pair = lastType !== null ? pairStat.get(`${lastType}->${name}`) : undefined;
13
+ const pairN = pair ? pair.s + pair.f : 0;
14
+ const type = typeStat.get(name);
15
+ const typeN = type ? type.s + type.f : 0;
16
+ let v;
17
+ if (pairN >= GATE_MIN_SUPPORT) {
18
+ const pr = pair.s / pairN;
19
+ v = pr < tau
20
+ ? { allowed: false, reason: `the learned world model predicts ${lastType}->${name} fails often in similar contexts (pair success ${pr.toFixed(2)} < ${tau}, ${pairN} observations); choose a different approach or verify prerequisites first` }
21
+ : { allowed: true };
22
+ }
23
+ else if (typeN >= GATE_MIN_SUPPORT) {
24
+ const tr = type.s / typeN;
25
+ v = tr < tau
26
+ ? { allowed: false, reason: `the learned world model predicts this action type (${name}) tends to fail (success ${tr.toFixed(2)} < ${tau}, ${typeN} observations); choose a different approach or verify prerequisites first` }
27
+ : { allowed: true };
28
+ }
29
+ else {
30
+ v = { allowed: true };
31
+ }
32
+ if (v.allowed)
33
+ lastType = name;
34
+ if (!v.allowed)
35
+ refusals++;
36
+ return v;
37
+ },
38
+ stats: () => ({ consults, refusals, types: typeStat.size, pairs: pairStat.size, tau }),
39
+ };
40
+ }
41
+ /** Build the gate from this home's terminal corpus (same train-split
42
+ * protocol as the experiments: seed 1, 70/30). An empty corpus yields an
43
+ * all-allow gate — no evidence never blocks. */
44
+ export async function buildConditionedGate(home, tau) {
45
+ const typeStat = new Map();
46
+ const pairStat = new Map();
47
+ try {
48
+ const all = await loadTrajectories(home, 1000);
49
+ const usable = all.filter((t) => t.steps.length > 0 && t.environment.id === 'terminal');
50
+ // deterministic split (seed 1) — analysis-only, no decision leakage
51
+ const rng = mulberry32(hashSeed('p2a-intervention-1'));
52
+ const shuffled = [...usable].sort(() => rng() - 0.5);
53
+ const train = shuffled.slice(0, Math.floor(shuffled.length * 0.7));
54
+ const bump = (m, k, ok) => {
55
+ const e = m.get(k) ?? { s: 0, f: 0 };
56
+ if (ok)
57
+ e.s++;
58
+ else
59
+ e.f++;
60
+ m.set(k, e);
61
+ };
62
+ for (const t of train) {
63
+ let prev = null;
64
+ for (const s of t.steps) {
65
+ const type = String(s.action.type ?? '?');
66
+ if (s.outcome === 'success' || s.outcome === 'failure') {
67
+ bump(typeStat, type, s.outcome === 'success');
68
+ if (prev !== null)
69
+ bump(pairStat, `${prev}->${type}`, s.outcome === 'success');
70
+ }
71
+ prev = type;
72
+ }
73
+ }
74
+ }
75
+ catch {
76
+ // unreadable corpus = no evidence = all-allow (the gate must never take
77
+ // the agent down; it is an advisor, not a dependency)
78
+ }
79
+ const policy = conditionedPolicy(typeStat, pairStat, tau);
80
+ return {
81
+ check: async (name) => policy.check(name),
82
+ stats: policy.stats,
83
+ };
84
+ }
85
+ function mulberry32(a) {
86
+ return () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
87
+ }
88
+ function hashSeed(s) { let h = 2166136261; for (let i = 0; i < s.length; i++) {
89
+ h ^= s.charCodeAt(i);
90
+ h = Math.imul(h, 16777619);
91
+ } return h >>> 0; }
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 { pageReadSink } from './code-wm-recorder.js';
8
9
  export { withTopologyGovernance, mapRole, liveTopologySnapshot, teamLog } from './topology-live.js';
9
10
  export { buildRegistry, contextPack, makeApproval, liveYolo, nativeRegistry, runAgentTask, spawnBase, toServerConfig, type AgentTaskOptions, type RunnerEvents, } from './runner.js';
10
11
  export * from './skills-conformance.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 { pageReadSink } from "./code-wm-recorder.js";
8
9
  export { withTopologyGovernance, mapRole, liveTopologySnapshot, teamLog } from "./topology-live.js";
9
10
  export { buildRegistry, contextPack, makeApproval, liveYolo, nativeRegistry, runAgentTask, spawnBase, toServerConfig, } from "./runner.js";
10
11
  export * from "./skills-conformance.js";
package/dist/runner.js CHANGED
@@ -25,6 +25,7 @@ import { baseTools } from "./tools.js";
25
25
  import { buildSystemPrompt } from "./prompt.js";
26
26
  import { strings } from "./i18n.js";
27
27
  import { makeSpawnTool, MAX_SPAWN_DEPTH } from "./spawn.js";
28
+ import { buildConditionedGate } from "./gate.js";
28
29
  import { withTopologyGovernance } from "./topology-live.js";
29
30
  /** Flatten the config.json shape into the runtime discriminated union. */
30
31
  export function toServerConfig(c) {
@@ -433,6 +434,13 @@ export async function runAgentTask(opts) {
433
434
  ]);
434
435
  return r.message.content ?? '';
435
436
  };
437
+ // v0.24 P2A research: opt-in conditioned action gate (config gate.enabled,
438
+ // DEFAULT OFF — the 66-run live arc: mechanism + selectivity proven, task
439
+ // gain unproven, ~2x token cost on non-failing runs; production A/B only)
440
+ const gateTau = cfg.gate?.tau ?? 0.7;
441
+ const actionGate = cfg.gate?.enabled === true ? await buildConditionedGate(ctx.home, gateTau) : undefined;
442
+ if (actionGate)
443
+ traj.emit('gate.enabled', 'system', { tau: gateTau });
436
444
  const result = await runLoop({
437
445
  provider: chatProvider,
438
446
  registry: opts.registry,
@@ -449,6 +457,7 @@ export async function runAgentTask(opts) {
449
457
  // poll-shaped steering: drained at every turn boundary (codex semantics)
450
458
  injections: injectPoll,
451
459
  approval: spawnBase.current.approval,
460
+ toolGate: actionGate,
452
461
  events: {
453
462
  onDelta: (kind, chunk) => events.onDelta?.(kind, chunk),
454
463
  onInjected: (message) => {
@@ -481,6 +490,11 @@ export async function runAgentTask(opts) {
481
490
  traj.emit(granted ? 'tool.approved' : 'tool.denied', 'system', { name });
482
491
  events.onApproval?.(name, args, granted);
483
492
  },
493
+ onGate: (name, _args, allowed, reason) => {
494
+ // v0.24 P2A telemetry: every gate consult lands in the trajectory —
495
+ // production A/B reads these events, not lab reconstructions
496
+ traj.emit('gate.consulted', 'system', { name, allowed, reason: reason ? brief(reason, 200) : undefined });
497
+ },
484
498
  onAssistant: async (m) => {
485
499
  // stamp the event when the message lands, not after the session
486
500
  // write drains - awaiting first made model.responded land AFTER
@@ -502,6 +516,8 @@ export async function runAgentTask(opts) {
502
516
  // of silently vanishing into a channel nothing drains
503
517
  if (injectChannel)
504
518
  injectChannel.active = false;
519
+ if (actionGate)
520
+ traj.emit('gate.summary', 'system', actionGate.stats());
505
521
  traj.finish({ success: result.reason === 'final', reason: result.reason }, { turns: result.turns, toolUses: result.toolUses, promptTokens: result.usage.promptTokens, completionTokens: result.usage.completionTokens });
506
522
  // cognitive trajectory + episodic index land AFTER the observability
507
523
  // recorder settles, still before onFinal so callers can rely on both
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/agent",
3
- "version": "0.23.34",
3
+ "version": "0.23.36",
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,16 +15,16 @@
15
15
  "build": "tsc -p tsconfig.build.json"
16
16
  },
17
17
  "dependencies": {
18
- "@hmharness/cognitive": "0.23.34",
19
- "@hmharness/domain-harmony": "0.23.34",
20
- "@hmharness/domain-ops": "0.23.34",
21
- "@hmharness/evolution": "0.23.34",
22
- "@hmharness/kernel": "0.23.34",
23
- "@hmharness/observability": "0.23.34",
24
- "@hmharness/sandbox": "0.23.34",
25
- "@hmharness/lsp": "0.23.34",
26
- "@hmharness/browser": "0.23.34",
27
- "@hmharness/extension": "0.23.34"
18
+ "@hmharness/cognitive": "0.23.36",
19
+ "@hmharness/domain-harmony": "0.23.36",
20
+ "@hmharness/domain-ops": "0.23.36",
21
+ "@hmharness/evolution": "0.23.36",
22
+ "@hmharness/kernel": "0.23.36",
23
+ "@hmharness/observability": "0.23.36",
24
+ "@hmharness/sandbox": "0.23.36",
25
+ "@hmharness/lsp": "0.23.36",
26
+ "@hmharness/browser": "0.23.36",
27
+ "@hmharness/extension": "0.23.36"
28
28
  },
29
29
  "files": [
30
30
  "dist"