import type { CapabilityTier, CallContext, Provider, SignedDecisionLogEntry, SpendDecision, SpendPolicy, SpendScope } from '../types'; import { inferProvider } from '../cost-table'; import { sanitizeMetadata } from '../governance'; import { AgentGuardBlockedError, SpendGuard, type SpendGuardConfig } from '../spend-guard'; export interface FrameworkAdapterOptions { policy: SpendPolicy; scope: SpendScope; capabilityClaim?: CapabilityTier; config?: Omit; licenseKey?: string; defaultModel?: string; defaultOutputTokens?: number; framework?: string; } export interface FrameworkPreflight { guard: SpendGuard; call: CallContext; decision: SpendDecision; signed: SignedDecisionLogEntry | null; } export interface FrameworkCallInput { framework: string; model?: string; provider?: Provider; params?: unknown; requestShape?: Record; metadata?: Record; workflowId?: string; subagentId?: string; toolName?: string; outputTokens?: number; } const DATA_PLANE_KEYS = /^(prompt|completion|content|messages|input|output|text|raw)$/i; export function createFrameworkGuard(opts: FrameworkAdapterOptions): SpendGuard { return new SpendGuard({ policy: opts.policy, ...(opts.config ?? {}), licenseKey: opts.licenseKey ?? opts.config?.licenseKey, }); } export function safeRequestShape(value: unknown): Record { const redacted = redactDataPlane(value); if (redacted && typeof redacted === 'object' && !Array.isArray(redacted)) { return sanitizeMetadata(redacted as Record); } return { valueType: typeof value }; } export function redactDataPlane(value: unknown): unknown { if (value == null) return value; if (Array.isArray(value)) return value.map((child) => redactDataPlane(child)); if (typeof value !== 'object') return value; const out: Record = {}; for (const [key, child] of Object.entries(value as Record)) { if (DATA_PLANE_KEYS.test(key)) { out[key + '_present'] = child != null; out[key + '_type'] = Array.isArray(child) ? 'array' : typeof child; if (Array.isArray(child)) out[key + '_items'] = child.length; continue; } out[key] = redactDataPlane(child); } return out; } export function estimateInputTokens(guard: SpendGuard, value: unknown): number { const serialized = typeof value === 'string' ? value : stableJson(value ?? {}); return Math.max(1, guard.estimateTokens(serialized)); } export function outputTokensFromParams(params: unknown, fallback = 512): number { const record = objectRecord(params); const value = record?.max_tokens ?? record?.maxTokens ?? record?.maxOutputTokens ?? record?.max_completion_tokens ?? record?.maxCompletionTokens; return Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : fallback; } export function modelFromUnknown(model: unknown, fallback = 'unknown'): string { if (typeof model === 'string' && model.trim()) return model.trim(); const record = objectRecord(model); for (const key of ['modelId', 'model_id', 'id', 'name']) { const value = record?.[key]; if (typeof value === 'string' && value.trim()) return value.trim(); } return fallback; } export function providerFromModel(model: string, provider?: Provider): Provider { return provider ?? inferProvider(model); } export function buildFrameworkCall( guard: SpendGuard, opts: FrameworkAdapterOptions, input: FrameworkCallInput, ): CallContext { const model = modelFromUnknown(input.model, opts.defaultModel ?? 'unknown'); const metadata = sanitizeMetadata(redactDataPlane(input.metadata ?? {}) as Record); const requestShape = sanitizeMetadata({ framework: input.framework, ...(input.requestShape ?? safeRequestShape(input.params)), }); return { provider: providerFromModel(model, input.provider), model, inputTokens: estimateInputTokens(guard, input.params), outputTokens: input.outputTokens ?? outputTokensFromParams(input.params, opts.defaultOutputTokens ?? 512), scope: opts.scope, capabilityClaim: opts.capabilityClaim, workflowId: input.workflowId ?? stringValue(metadata.workflowId) ?? stringValue(metadata.workflow_id), subagentId: input.subagentId ?? stringValue(metadata.subagentId) ?? stringValue(metadata.subagent_id), label: stringValue(metadata.label) ?? input.framework, toolName: input.toolName, requestShape, }; } export async function preflightFrameworkCall( guard: SpendGuard, opts: FrameworkAdapterOptions, input: FrameworkCallInput, ): Promise { const call = buildFrameworkCall(guard, opts, input); const { decision, signed } = await guard.decide(call); if (decision.action === 'block') { throw new AgentGuardBlockedError(decision, opts.scope, opts.config?.locale); } return { guard, call, decision, signed }; } export async function settleFrameworkCall(preflight: FrameworkPreflight, result: unknown, partial = false): Promise { const usage = usageFromResult(result); if (!usage) return; await preflight.guard.settleStreamUsage( preflight.decision.decisionId, usage.inputTokens, usage.outputTokens, { partial }, ); } export async function recordFrameworkReceipt( guard: SpendGuard, framework: string, receipt: Record, ): Promise<{ decision: SpendDecision; signed: SignedDecisionLogEntry | null }> { return guard.recordOutcomeReceipt({ type: 'framework_adapter', framework, totalCostCents: safeInteger(receipt.totalCostCents), ...sanitizeMetadata(redactDataPlane(receipt) as Record), }); } export function usageFromResult(result: unknown): { inputTokens: number; outputTokens: number } | null { const candidates = [ objectRecord(result)?.usage, objectRecord(objectRecord(result)?.response)?.usage, objectRecord(objectRecord(result)?.rawResponse)?.usage, ]; for (const candidate of candidates) { const usage = objectRecord(candidate); if (!usage) continue; const input = firstInteger(usage.inputTokens, usage.promptTokens, usage.prompt_tokens, usage.input_tokens); const output = firstInteger(usage.outputTokens, usage.completionTokens, usage.completion_tokens, usage.output_tokens); if (input !== null && output !== null) return { inputTokens: input, outputTokens: output }; } return null; } export function objectRecord(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; } export function stringValue(value: unknown): string | undefined { return typeof value === 'string' && value.trim() ? value.trim() : undefined; } function firstInteger(...values: unknown[]): number | null { for (const value of values) { if (Number.isSafeInteger(value) && (value as number) >= 0) return value as number; } return null; } function safeInteger(value: unknown): number { return Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0; } function stableJson(value: unknown): string { try { return JSON.stringify(value); } catch { return String(value); } }