Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
105 lines
4.7 KiB
JavaScript
105 lines
4.7 KiB
JavaScript
/**
|
|
* DCD Scope Router (ADR-157) — the "narrow scope BEFORE retrieval" step.
|
|
*
|
|
* DCD (Domain → Collection → Document) is a retrieval *policy*, not a storage
|
|
* model — our liquid substrate (ADR-156) already provides the hierarchy
|
|
* physically (Space → Widget/Table/`room` → Row). This module is the explicit
|
|
* router that picks the Collection-level scope for a memory query, so retrieval
|
|
* is constrained before the semantic search instead of filtered after.
|
|
*
|
|
* For Hindsight `memory_recall`, the Collection step IS the `room` filter — the
|
|
* one that already buys +34% precision when applied before search. So the
|
|
* router's whole job here is: query → `room` (or "widen": no room).
|
|
*
|
|
* IMPLEMENTATION NOTE (Ponytail / ADR-157): this is a deterministic lexical
|
|
* router, mirroring `CommandClassifier`. The ADR explicitly allows "a cheap
|
|
* model (Haiku) OR a rule-of-thumb threshold". The rule-of-thumb is chosen for
|
|
* the prototype because it validates the *contract* — `{ room, confidence }` +
|
|
* widen-on-low-confidence — with zero new dependency, zero secret, zero
|
|
* latency, and full offline testability. An LLM structured-output backing is a
|
|
* drop-in replacement behind this same signature; nothing downstream changes.
|
|
*/
|
|
|
|
// Canonical Hindsight room vocabulary (mirrors the `room` enum documented in
|
|
// agent-tools/tool-definitions/memory.js). The router can only ever emit one of
|
|
// these or null (= widen to domain).
|
|
export const ROOMS = [
|
|
'auth', 'pipeline', 'schema', 'tax', 'hr', 'legal', 'compliance',
|
|
'infrastructure', 'ui', 'api', 'deployment', 'monitoring', 'agent', 'general',
|
|
];
|
|
|
|
// Keyword hints per room. A hit is a whole-word match of a hint in the query.
|
|
// `general` is intentionally absent — it is the "no specific collection" room,
|
|
// reached only as the widen target, never as a positive route.
|
|
const ROOM_HINTS = {
|
|
auth: ['auth', 'login', 'token', 'jwt', 'password', 'oauth', 'session', 'permission', 'permissions'],
|
|
pipeline: ['pipeline', 'deal', 'deals', 'stage', 'funnel', 'lead', 'leads'],
|
|
schema: ['schema', 'column', 'columns', 'migration', 'field', 'fields'],
|
|
tax: ['tax', 'taxes', 'vat', 'invoice', 'invoices', 'accounting'],
|
|
hr: ['hr', 'employee', 'employees', 'hire', 'hiring', 'salary', 'payroll', 'vacation'],
|
|
legal: ['legal', 'contract', 'contracts', 'nda', 'gdpr', 'license', 'licensing'],
|
|
compliance: ['compliance', 'audit', 'audits', 'policy', 'regulation', 'regulations'],
|
|
infrastructure: ['infra', 'infrastructure', 'server', 'servers', 'nginx', 'pm2', 'docker', 'vpn', 'host'],
|
|
ui: ['ui', 'button', 'css', 'component', 'components', 'frontend', 'react', 'layout', 'widget'],
|
|
api: ['api', 'endpoint', 'endpoints', 'route', 'routes', 'rest', 'request', 'response'],
|
|
deployment: ['deployment', 'deploy', 'release', 'rollout', 'ci', 'cd', 'build', 'builds'],
|
|
monitoring: ['monitor', 'monitoring', 'log', 'logs', 'metric', 'metrics', 'alert', 'alerts', 'telemetry', 'observability'],
|
|
agent: ['agent', 'agents', 'prompt', 'prompts', 'llm', 'memory', 'hindsight', 'tool', 'tools'],
|
|
};
|
|
|
|
// Confidence below this floor means "do not trust the route" → widen to domain
|
|
// (no room). A single weak keyword hit (0.5) widens; ≥2 distinct hits (0.9)
|
|
// applies. Exported so the caller's threshold stays in lockstep with the router.
|
|
export const ROUTE_CONFIDENCE_FLOOR = 0.6;
|
|
|
|
const WORD = /[a-z0-9]+/g;
|
|
|
|
function tokenize(text) {
|
|
return new Set(String(text || '').toLowerCase().match(WORD) || []);
|
|
}
|
|
|
|
/**
|
|
* Route a memory query to a Hindsight `room` (the Collection step of DCD).
|
|
*
|
|
* @param {string} query - the recall query
|
|
* @returns {{ room: string|null, confidence: number, reason: string }}
|
|
* `room === null` means "widen to domain" — search unscoped. `confidence` is
|
|
* in [0,1]; the caller applies the route only when it clears
|
|
* `ROUTE_CONFIDENCE_FLOOR`.
|
|
*/
|
|
export function routeMemoryScope(query) {
|
|
const tokens = tokenize(query);
|
|
if (tokens.size === 0) {
|
|
return { room: null, confidence: 0, reason: 'empty query → widen to domain' };
|
|
}
|
|
|
|
let best = null;
|
|
let bestHits = 0;
|
|
for (const [room, hints] of Object.entries(ROOM_HINTS)) {
|
|
let hits = 0;
|
|
for (const h of hints) if (tokens.has(h)) hits += 1;
|
|
if (hits > bestHits) {
|
|
bestHits = hits;
|
|
best = room;
|
|
}
|
|
}
|
|
|
|
if (bestHits === 0) {
|
|
return { room: null, confidence: 0, reason: 'no room keywords matched → widen to domain' };
|
|
}
|
|
|
|
const confidence = bestHits >= 2 ? 0.9 : 0.5;
|
|
if (confidence < ROUTE_CONFIDENCE_FLOOR) {
|
|
return {
|
|
room: null,
|
|
confidence,
|
|
reason: `weak match for "${best}" (1 keyword) → widen to domain`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
room: best,
|
|
confidence,
|
|
reason: `routed to "${best}" (${bestHits} keyword hits)`,
|
|
};
|
|
}
|