@hmharness/agent 0.23.34 → 0.23.35
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 +57 -0
- package/dist/gate.js +91 -0
- package/dist/runner.js +16 -0
- package/package.json +11 -11
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/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.
|
|
3
|
+
"version": "0.23.35",
|
|
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.
|
|
19
|
-
"@hmharness/domain-harmony": "0.23.
|
|
20
|
-
"@hmharness/domain-ops": "0.23.
|
|
21
|
-
"@hmharness/evolution": "0.23.
|
|
22
|
-
"@hmharness/kernel": "0.23.
|
|
23
|
-
"@hmharness/observability": "0.23.
|
|
24
|
-
"@hmharness/sandbox": "0.23.
|
|
25
|
-
"@hmharness/lsp": "0.23.
|
|
26
|
-
"@hmharness/browser": "0.23.
|
|
27
|
-
"@hmharness/extension": "0.23.
|
|
18
|
+
"@hmharness/cognitive": "0.23.35",
|
|
19
|
+
"@hmharness/domain-harmony": "0.23.35",
|
|
20
|
+
"@hmharness/domain-ops": "0.23.35",
|
|
21
|
+
"@hmharness/evolution": "0.23.35",
|
|
22
|
+
"@hmharness/kernel": "0.23.35",
|
|
23
|
+
"@hmharness/observability": "0.23.35",
|
|
24
|
+
"@hmharness/sandbox": "0.23.35",
|
|
25
|
+
"@hmharness/lsp": "0.23.35",
|
|
26
|
+
"@hmharness/browser": "0.23.35",
|
|
27
|
+
"@hmharness/extension": "0.23.35"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist"
|