Affaan Mustafa 726972d735 feat(layer4): agent-space distance metric + TCAS-style collision avoidance (v0)
The moat layer: spatial deconfliction for multiple agents (and humans) on one
codebase, modeled on aircraft TCAS — measure how close two agents are in
code-space, then transmit-intent (Traffic Advisory) and steer-away (Resolution
Advisory) before they collide at the git layer.

scripts/lib/agent-proximity/:
- distance.js — the math: per-channel collision probabilities combined via
  noisy-OR R = 1 - Π(1 - ω·r). Channels: edit overlap (file + line-range
  Jaccard), dependency coupling (γ^(d-1) over the import graph, direction-
  agnostic — catches 'edit there breaks here' even when tree-distant), and tree
  proximity (LCA-based, soft prior). TCAS advise(): clear / advisory(transmit) /
  resolution(steer), with deterministic right-of-way priority so the maneuver is
  coordinated. closureRate() for approach-speed escalation.
- graph.js — lightweight require/import dependency-graph builder (fs or in-memory).
- index.js — scanAirspace(): pairwise advisories + 3D vector embedding (space-
  filling path embedding pulled toward dependency neighbours) so a 'where are
  the agents' visualization can render the file-cloud and watch agents crawl /
  steer.

docs/design/agent-proximity.md — full mathematical formulation + protocol + viz
+ roadmap (v1 call-graph/symbol channels + live session-diff wiring; v2 cross-
machine airspace over Tailscale, the zero-conflict-swarm demo).

17 tests; full suite 2869/2869; lint green.
2026-06-20 15:40:40 -04:00

171 lines
5.6 KiB
JavaScript

'use strict';
/**
* Agent-proximity orchestration: scan all agents in a codebase, compute the
* pairwise TCAS advisories that drive the steer/transmit triggers, and embed
* each agent in 3D space for the "where are the agents" visualization.
*
* This is the call the control pane / hook layer makes each tick:
* const scan = scanAirspace(agents, graph)
* for (const a of scan.advisories) fireTrigger(a) // transmit / steer
* renderViz(scan.positions, scan.advisories) // 3D crawl view
*/
const crypto = require('crypto');
const { advise, collisionRisk, DEFAULTS } = require('./distance');
const { buildDependencyGraph, buildDependencyGraphFromSources } = require('./graph');
const { normalizePath, segments } = require('./distance')._internal;
/**
* Deterministic hash of a string to a unit-ish vector in R^dims (components in
* roughly [-1, 1]). Used to place tree prefixes in space.
*/
function hashVec(str, dims) {
const digest = crypto.createHash('sha256').update(String(str)).digest();
const v = new Array(dims).fill(0);
for (let d = 0; d < dims; d += 1) {
// Two bytes per dim → [-1, 1).
const hi = digest[(d * 2) % digest.length];
const lo = digest[(d * 2 + 1) % digest.length];
v[d] = ((hi << 8) | lo) / 32768 - 1;
}
return v;
}
/**
* Coordinate of a file: a space-filling embedding of its path. Files that share
* a long directory prefix share most of their coordinate (deeper segments
* perturb less), so tree-close files are space-close — exactly what eq. (6)
* wants the visualization to show.
*/
function fileCoordinate(filePath, dims = 3) {
const segs = segments(filePath);
const v = new Array(dims).fill(0);
let prefix = '';
for (let i = 0; i < segs.length; i += 1) {
prefix += '/' + segs[i];
const h = hashVec(prefix, dims);
const scale = 1 / Math.pow(2, i);
for (let d = 0; d < dims; d += 1) v[d] += h[d] * scale;
}
return v;
}
/**
* Pull a file's coordinate toward the coordinates of its dependency neighbours
* (one averaging step), so coupled files that are far in the tree are drawn
* closer in space — the dependency channel made visible.
*/
function smoothByDependency(coords, graph, alpha = 0.35) {
const adj = (graph && graph.adjacency) || {};
const out = {};
for (const file of Object.keys(coords)) {
const base = coords[file];
const neighbours = (adj[file] || []).map(normalizePath).filter(n => coords[n]);
if (neighbours.length === 0) {
out[file] = base.slice();
continue;
}
const dims = base.length;
const avg = new Array(dims).fill(0);
for (const n of neighbours) for (let d = 0; d < dims; d += 1) avg[d] += coords[n][d];
for (let d = 0; d < dims; d += 1) avg[d] /= neighbours.length;
out[file] = base.map((x, d) => (1 - alpha) * x + alpha * avg[d]);
}
return out;
}
function weightedCentroid(files, fileCoords, dims) {
const v = new Array(dims).fill(0);
let wsum = 0;
for (const f of files) {
const c = fileCoords[normalizePath(f.path)];
if (!c) continue;
const w = f.weight ?? 1;
for (let d = 0; d < dims; d += 1) v[d] += c[d] * w;
wsum += w;
}
if (wsum > 0) for (let d = 0; d < dims; d += 1) v[d] /= wsum;
return v;
}
/**
* Embed agents in R^dims for visualization. Returns one position per agent plus
* the file coordinates used, so a renderer can draw both the agents and the
* file-cloud they sit in.
*/
function embedAgents(agents, graph = {}, options = {}) {
const dims = options.dims || 3;
const fileCoords = {};
for (const agent of agents) {
for (const f of agent.files || []) {
const p = normalizePath(f.path);
if (!fileCoords[p]) fileCoords[p] = fileCoordinate(p, dims);
}
}
const smoothed = smoothByDependency(fileCoords, graph, options.dependencyPull ?? 0.35);
const positions = agents.map(agent => ({
agentId: agent.agentId,
position: weightedCentroid(agent.files || [], smoothed, dims),
fileCount: (agent.files || []).length
}));
return { dims, positions, fileCoordinates: smoothed };
}
/**
* Scan the whole airspace: pairwise advisories + 3D positions in one pass.
*
* @param {Array<{agentId,files,startedAt?,intent?}>} agents
* @param {object} graph dependency graph (adjacency)
* @param {object} [options]
* @returns {{ advisories, positions, links, generatedAt }}
*/
function scanAirspace(agents, graph = {}, options = {}) {
const list = Array.isArray(agents) ? agents.filter(a => a && a.agentId !== null && a.agentId !== undefined) : [];
const advisories = [];
const links = [];
for (let i = 0; i < list.length; i += 1) {
for (let j = i + 1; j < list.length; j += 1) {
const a = list[i];
const b = list[j];
const verdict = advise(a, b, graph, options);
links.push({
a: a.agentId,
b: b.agentId,
risk: verdict.risk,
distance: verdict.distance,
level: verdict.level
});
if (verdict.level !== 'clear') {
advisories.push({ a: a.agentId, b: b.agentId, ...verdict });
}
}
}
advisories.sort((x, y) => y.risk - x.risk);
links.sort((x, y) => y.risk - x.risk);
const embedding = embedAgents(list, graph, options);
return {
advisories,
positions: embedding.positions,
fileCoordinates: embedding.fileCoordinates,
links,
counts: {
agents: list.length,
advisories: advisories.length,
resolutions: advisories.filter(a => a.level === 'resolution').length
}
};
}
module.exports = {
DEFAULTS,
scanAirspace,
embedAgents,
fileCoordinate,
collisionRisk,
advise,
buildDependencyGraph,
buildDependencyGraphFromSources
};