Ctrl K
ComunidadGuías…

src/core/contracts/tool-annotations.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { ToolOutcome, ToolOutcomeSideEffect, ToolRateLimit, ToolRateLimitWindow, ToolRisk } from './tool-definition';

const RATE_LIMIT_WINDOWS = new Set<ToolRateLimitWindow>(['1s', '1m', '1h', '1d']);
const OUTCOME_SIDE_EFFECTS = new Set<ToolOutcomeSideEffect>(['none', 'read', 'write', 'notify', 'external']);

export const AGENT_CAPABILITY_KEY = 'x-agent-capability';

export interface ToolAnnotations {
  present: boolean;
  enabled: boolean;
  scope: string;
  riskLevel: unknown;
  confirmRequired: boolean;
  confirmWhen: unknown;
  rateLimit: ToolRateLimit | undefined;
  rateLimitPerMin: number;
  requiresSubject: boolean;
  sensitive: boolean;
  readonly: boolean | null;
  idempotent: boolean | null;
  timeoutPresent: boolean;
  timeoutRaw: unknown;
  timeoutMs: number;
  whenToUse: string;
  returns: string;
  examples: unknown[];
  confirmPrompt: string;
  context: string[];
  outcome: ToolOutcome | undefined;
  extensions: Record<string, unknown>;
}

export function toolAnnotationsOf(op: Record<string, unknown>): ToolAnnotations {
  const cap = record(op[AGENT_CAPABILITY_KEY]);
  const execution = record(cap?.['execution']);
  const subject = record(cap?.['subject']);
  const risk = record(cap?.['risk']);
  const approval = record(cap?.['approval']);
  const audit = record(cap?.['audit']);
  const guidance = record(cap?.['guidance']);
  const outcome = record(cap?.['outcome']);
  const rateLimit = parseRateLimit(execution?.['rate_limit']);

  return {
    present: !!cap,
    enabled: cap?.['enabled'] === true,
    scope: String(cap?.['scope'] ?? '').trim(),
    riskLevel: risk?.['level'],
    confirmRequired: approval?.['required'] === true,
    confirmWhen: approval?.['when'],
    rateLimit,
    rateLimitPerMin: rateLimit ? rateLimitToPerMin(rateLimit) : 0,
    requiresSubject: subject?.['required'] === true,
    sensitive: audit?.['sensitive'] === true,
    readonly: typeof execution?.['readonly'] === 'boolean' ? execution['readonly'] : null,
    idempotent: typeof execution?.['idempotent'] === 'boolean' ? execution['idempotent'] : null,
    timeoutPresent: !!execution && Object.hasOwn(execution, 'timeout_ms'),
    timeoutRaw: execution?.['timeout_ms'],
    timeoutMs: parseTimeout(execution?.['timeout_ms']),
    whenToUse: String(guidance?.['when_to_use'] ?? '').trim(),
    returns: String(guidance?.['returns'] ?? '').trim(),
    examples: Array.isArray(guidance?.['examples']) ? guidance!['examples'] as unknown[] : [],
    confirmPrompt: String(approval?.['prompt'] ?? '').slice(0, 200),
    context: Array.isArray(guidance?.['context']) ? guidance!['context'].map(String).filter(Boolean).slice(0, 50) : [],
    outcome: parseOutcome(outcome),
    extensions: extensionBagOf(op),
  };
}

export function deriveRisk(explicit: unknown, _method: string, readonly: boolean): ToolRisk {
  const v = String(explicit ?? '').toLowerCase();
  if (v === 'low' || v === 'medium' || v === 'high') return v;
  return readonly ? 'low' : 'medium';
}

function parseRateLimit(v: unknown): ToolRateLimit | undefined {
  const r = record(v);
  if (!r) return undefined;
  const count = Number(r['count']);
  const window = String(r['window'] ?? '') as ToolRateLimitWindow;
  if (!Number.isInteger(count) || count <= 0 || !RATE_LIMIT_WINDOWS.has(window)) return undefined;
  return { count, window };
}

function rateLimitToPerMin(v: ToolRateLimit): number {
  if (v.window === '1s') return v.count * 60;
  if (v.window === '1m') return v.count;
  if (v.window === '1h') return Math.max(1, Math.round(v.count / 60));
  return Math.max(1, Math.round(v.count / 1440));
}

function parseTimeout(v: unknown): number {
  if (v === undefined || v === null || v === '') return 0;
  return typeof v === 'number' ? v : Number.NaN;
}

function parseOutcome(v: Record<string, unknown> | null): ToolOutcome | undefined {
  if (!v) return undefined;
  const result = String(v['result'] ?? '').trim();
  const sideEffect = String(v['side_effect'] ?? '') as ToolOutcomeSideEffect;
  if (!result || !OUTCOME_SIDE_EFFECTS.has(sideEffect)) return undefined;
  return { result: result.slice(0, 300), sideEffect };
}

function extensionBagOf(op: Record<string, unknown>): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(op)) {
    if (key === AGENT_CAPABILITY_KEY) continue;
    if (!isExtensionKey(key)) continue;
    out[key] = value;
  }
  return out;
}

function isExtensionKey(key: string): boolean {
  return key.startsWith('x-bailing-') || key.startsWith('x-business-');
}

function record(v: unknown): Record<string, unknown> | null {
  return !!v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, unknown> : null;
}