BID · Console
Baseline · Intelligence · Decision
src/decision/tools.ts 5,103 bytes · typescript
/**
 * Decision — Anthropic tool surface for the Rule Library.
 *
 * Every Pillar 3 agent exposes the same two tools to its LLM:
 *
 *   find_rules({type, domain, agent, triggers})
 *     → search the library for active rules that match. Used at the
 *       start of any interpretation / visualization / delivery step
 *       (Std 5 — declared methods only).
 *
 *   get_rule({rule_id})
 *     → fetch the full content of one entry. Used after the LLM
 *       (or the deterministic short-circuit in index.ts) picks a
 *       candidate so it can apply the conditions / action / confidence
 *       framework.
 *
 * Mirrors src/intelligence/tools.ts: the library is loaded lazily on
 * first call and cached for the process lifetime. Failures are
 * returned as structured DecisionToolResult objects so the agent's
 * tool-use loop never sees a raw exception (Std 12).
 */

import {
  findRules,
  getRule,
  type Rule,
  type RuleAgent,
  type RuleType,
} from './library.js';

export interface DecisionToolDescriptor {
  readonly name: string;
  readonly description: string;
  readonly input_schema: {
    readonly type: 'object';
    readonly properties: Record<string, { type: string; description: string }>;
    readonly required: readonly string[];
  };
}

export const RULE_TOOLS: readonly DecisionToolDescriptor[] = [
  {
    name: 'find_rules',
    description:
      'Search the SME Rule Library for active entries that match the current decision-layer ' +
      'operation. Filter by `type` (interpretation | visualization | delivery), `domain` ' +
      '(banking | wealth | insurance | all | …), `agent` (output_ingestion | visualization | ' +
      'delivery), and free-text `triggers`. Returns lightweight ' +
      '{rule_id, name, type, domain, triggers, sourceFile} entries; call get_rule for the ' +
      'full conditions / action / confidence_framework / disclosure_policy.',
    input_schema: {
      type: 'object',
      properties: {
        type: {
          type: 'string',
          description: 'Filter by rule type.',
        },
        domain: {
          type: 'string',
          description: 'Filter by domain (banking, wealth, insurance, all, …). Entries with domain="all" always match.',
        },
        agent: {
          type: 'string',
          description: 'Filter by intended Decision agent (output_ingestion, visualization, delivery).',
        },
        triggers: {
          type: 'string',
          description: 'Optional comma-separated free-text trigger terms (e.g. "peer_comparison, decision_maker"). Each term must appear in the entry\'s triggers list (case-insensitive substring).',
        },
      },
      required: [],
    },
  },
  {
    name: 'get_rule',
    description:
      'Fetch the full content of one rule by its rule_id. Returns the conditions, action, ' +
      'confidence_framework, disclosure_policy, and rationale.',
    input_schema: {
      type: 'object',
      properties: {
        rule_id: { type: 'string', description: 'Unique snake_case identifier.' },
      },
      required: ['rule_id'],
    },
  },
];

export interface DecisionToolResult {
  readonly ok: boolean;
  readonly result?: unknown;
  readonly error?: { readonly category: string; readonly message: string };
}

export async function executeRuleTool(
  name: string,
  rawInput: unknown,
): Promise<DecisionToolResult> {
  const input = (rawInput && typeof rawInput === 'object') ? (rawInput as Record<string, unknown>) : {};
  try {
    switch (name) {
      case 'find_rules': {
        const triggers = typeof input.triggers === 'string'
          ? input.triggers.split(',').map(s => s.trim()).filter(s => s.length > 0)
          : undefined;
        const results = await findRules({
          type: typeof input.type === 'string' ? (input.type as RuleType) : undefined,
          domain: typeof input.domain === 'string' ? input.domain : undefined,
          agent: typeof input.agent === 'string' ? (input.agent as RuleAgent) : undefined,
          triggers,
        });
        return { ok: true, result: results.map(summarize) };
      }
      case 'get_rule': {
        const id = typeof input.rule_id === 'string' ? input.rule_id : '';
        if (!id) {
          return { ok: false, error: { category: 'invalid-request', message: 'rule_id is required.' } };
        }
        const r = await getRule(id);
        if (!r) {
          return { ok: false, error: { category: 'not-found', message: `no rule "${id}" in library.` } };
        }
        return { ok: true, result: r };
      }
      default:
        return { ok: false, error: { category: 'unknown-tool', message: `unknown rule tool "${name}"` } };
    }
  } catch (err) {
    return {
      ok: false,
      error: { category: 'internal', message: err instanceof Error ? err.message : String(err) },
    };
  }
}

function summarize(r: Rule) {
  return {
    rule_id: r.rule_id,
    name: r.name,
    type: r.type,
    domain: r.domain,
    agent: r.applies_to.agent,
    triggers: r.applies_to.triggers,
    status: r.status,
    sourceFile: r.sourceFile,
  };
}