/** Contracts for per-app AI tools — the app-facing side of the Ask Corva agent runtime. */ /** * JSON Schema for a tool's arguments. Constraints belong here as well as in the * handler: the model sees the schema when it builds the call, so an * `exclusiveMinimum` or an `anyOf` prevents an invalid call instead of explaining * one afterwards. The handler still validates — the schema is a contract with the * model, not a guarantee about what arrives. */ type AppToolParametersSchema = { type: 'object'; properties: Record; required: string[]; /** For "at least one of these": `[{ required: ['a'] }, { required: ['b'] }]`. */ anyOf?: Array<{ required: string[]; }>; additionalProperties?: boolean; }; type AppToolSchema = { parameters: AppToolParametersSchema; description?: string; }; /** * What the host shows when a call must be confirmed. Field for field the shape the * platform's core-tool gate already renders, so a host adapter passes it through * rather than translating it. */ type AppToolConfirmationRequest = { title: string; message: string; confirmLabel: string; }; /** Host callback that asks the user and resolves to their answer. */ type AppToolConfirmFn = (request: AppToolConfirmationRequest) => Promise; /** * `REQUIRED` gates every valid call; `CONDITIONAL` gates only the calls its * predicate marks risky. * * Prefer `CONDITIONAL` for a tool the agent calls often: a gate that fires on * routine work is dismissed reflexively, which costs the confirmation its meaning * on the call that mattered. */ type AppToolConfirmationPolicy = 'REQUIRED' | 'CONDITIONAL'; /** * The host-owned half of the gate. `shouldRequest` carries the risk boundary for a * `CONDITIONAL` policy and is not consulted under `REQUIRED`. */ type AppToolConfirmation = { shouldRequest: (params: Record) => boolean; getRequest: (params: Record) => AppToolConfirmationRequest; }; type AppToolDefinition = { name: string; /** Whether the frontend tool reads loaded state or changes app-local UI state. */ toolClass: AppToolClass; tags?: string[]; description: string; shortDescription?: string; parameters: AppToolParametersSchema; /** Live schema at describe-time; overrides `description`/`parameters` when present. */ getSchema?: () => AppToolSchema; /** Declared together with `confirmation`; neither half means anything alone. */ confirmationPolicy?: AppToolConfirmationPolicy; confirmation?: AppToolConfirmation; }; type AppToolResultFields = Record & { error?: string; message?: string; code?: string; nextAction?: NextAction; changed?: Record; resultingState?: unknown; }; type AppToolReadyResult = AppToolResultFields & { success: true; outcome: 'READY'; isDataReady?: true; }; type AppToolPartialResult = AppToolResultFields & { success: true; outcome: 'PARTIAL'; isDataReady: false; }; type AppToolDataReadyResult, Row = unknown, Filters = Record> = AppToolReadyResult & { isDataReady: true; scope: DataScope; coverage: DataCoverage; units: Record; summary: Summary; sampleRows?: SampleRows; goals?: GoalSummary[]; provenance: DataProvenance; }; type AppToolDataPartialResult, Row = unknown, Filters = Record> = AppToolPartialResult & { scope: DataScope; coverage: DataCoverage; units: Record; summary: Summary; sampleRows?: SampleRows; goals?: GoalSummary[]; provenance: DataProvenance; }; type AppToolNotReadyResult = AppToolResultFields & { success: true; outcome: 'NOT_READY'; isDataReady: false; message: string; code: string; nextAction: NextAction; }; /** * A call the user declined, or one the host could not put to them at all. * * Success-shaped on purpose: a failure envelope reaches the user as a raw * transport error, and nothing failed here — the app was asked and answered no. */ type AppToolCancelledResult = AppToolResultFields & { success: true; outcome: 'CANCELLED'; executed: false; toolName: string; message: string; code: string; nextAction: NextAction; }; type AppToolFailureResult = AppToolResultFields & { success: false; outcome: AppToolFailureOutcome; toolName: string; error: string; message: string; code: string; nextAction: NextAction; }; /** Temporary compatibility member for apps that still call `appToolError`. */ type AppToolLegacyErrorResult = AppToolResultFields & { success: false; outcome?: never; error: string; }; /** Wire envelope with impossible success/outcome combinations excluded. */ type AppToolResult = AppToolReadyResult | AppToolPartialResult | AppToolNotReadyResult | AppToolCancelledResult | AppToolFailureResult | AppToolLegacyErrorResult; type AppToolResultExtra = Record; type AppToolSuccessPayload = { /** Only what actually moved — see `diffSettings`. */ changed?: Record; /** Say so when nothing changed, or when the applied state differs from the request. */ message?: string; resultingState?: unknown; extra?: AppToolResultExtra; }; type AppToolErrorPayload = { extra?: AppToolResultExtra; }; /** * Which layer a tool belongs to. Fixed per tool, and the conformance suite reads * it: a snapshot read that starts a fetch, or a command that poses as a read, are * the two mistakes this label exists to make visible. * * `SNAPSHOT_READ` describes already-loaded state, changes nothing, awaits no I/O. * `UI_COMMAND` states its side effect and returns the state actually applied. * Whole-dataset queries are not a third frontend class: they belong to Corvian's * backend tool surface and therefore cannot be represented as an `AppTool`. */ type AppToolClass = 'SNAPSHOT_READ' | 'UI_COMMAND'; /** A completed read: the figures are whole, bounded, or not there yet. */ type AppToolDataOutcome = 'READY' | 'PARTIAL' | 'NOT_READY'; /** * A completed call the app declined. The request was understood — an execution * that never happened (a throw, a vanished iframe, a timeout) is the transport's * `tool_error`, not one of these. * * `INTERNAL` is the exception the handler caught and chose to report; an uncaught * one is transport. */ type AppToolFailureOutcome = 'INVALID_INPUT' | 'REJECTED' | 'CONFLICT' | 'DEPENDENCY' | 'INTERNAL'; type AppToolOutcome = AppToolDataOutcome | AppToolFailureOutcome | 'CANCELLED'; /** * The next legal operation, not a policy someone must enforce. * * Nothing in the backend schedules a retry or blocks a repeat, so a field named * `retry` would state a guarantee that does not exist. This names what the agent * may do next instead, and `delayMs` is reachable only under `WAIT` — where it is * the app's own estimate rather than an invitation to invent a backoff. */ type NextAction = { type: 'NONE'; } /** `fields` names arguments that are wrong; `requiredAnyOf` names a set where at least one must be present. */ | { type: 'FIX_ARGUMENTS'; fields?: string[]; requiredAnyOf?: string[]; } | { type: 'ASK_USER'; missing?: string[]; } | { type: 'REFRESH_STATE'; } | { type: 'WAIT'; delayMs: number; } | { type: 'CHANGE_SCOPE'; } | { type: 'CONFIGURE_APP'; }; type TimeRange = { start: string; end: string; }; /** * What produced the figures. An agent asked to average "everything on this * dashboard" cannot know what that covers, and without it stated it substitutes * its own guess. * * `selectedIds` is empty when the scope is not per-entity (a single chart's view * mode, say); the app-specific bits live in `filters`. */ type DataScope> = { entityType: string; selectedIds: Array; timeRange?: TimeRange; filters?: Filters; }; /** * How much of what was asked for the figures actually cover. `isComplete` is what * separates "the average of everything shown" from "the average of page one", and * it is derived rather than declared so an app cannot claim more than it covered. */ type DataCoverage = { requestedEntities: number; analysedEntities: number; requestedRows?: number; analysedRows: number; isComplete: boolean; /** Named gaps — a well with no data, a page that failed, an excluded section. */ omissions?: string[]; }; /** Caller-supplied coverage facts; completeness is always derived by the kit. */ type DataCoverageInput = Omit & { /** Accepted during migration but ignored by `appToolData`. */ isComplete?: boolean; }; /** Where the numbers came from: the screen, or a backend sweep. */ type DataProvenance = { source: 'LOADED_APP_STATE' | 'BACKEND_QUERY'; computedAt: string; }; /** * Example rows, fenced off from the aggregates on purpose: a truncated sample one * key away from figures computed over everything gets read as the dataset. */ type SampleRows = { items: Row[]; returned: number; totalAnalysed: number; truncated: boolean; }; type FailureParamPrimitive = string | number | boolean; /** * A structured fact about the refusal. Lists stay lists and a constraint stays an * object: comma-joining an option set, or writing `'minutes > 0'`, forces the * agent to parse prose back into data before it can fix its arguments. */ type FailureParamValue = FailureParamPrimitive | FailureParamPrimitive[] | Record | null; type FailureBase = { /** Namespaced and append-only, e.g. `report_data.partial_load`. Never renamed by copy edits. */ code: string; /** * Machine-readable facts about the refusal: primitives and primitive lists, no * payload dumps, no authored text, no customer content. A list stays a list — * comma-joining an option set forces the agent to parse prose back into data. */ params?: Record; extra?: AppToolResultExtra; }; type AppToolFailurePayload = FailureBase & { /** Exact registered tool name that produced the refusal. */ toolName: string; outcome: AppToolFailureOutcome; nextAction: NextAction; }; type NumericSummary = { /** How many values were numeric — never inflated by blank or text cells. */ count: number; average: number | null; min: number | null; max: number | null; sum: number; }; /** * A per-entity average re-aggregated by the event count that produced it. * * `missingWeightCount` is load-bearing: a value whose weight is absent cannot * enter a weighted figure, and silently dropping it produces a confident number * computed over fewer entities than the caller believes. */ type WeightedSummary = NumericSummary & { weightedAverage: number | null; weightTotal: number; weightedCount: number; missingWeightCount: number; }; /** * The exact cross-entity figure when the app exposes both a total and a count: * sum(total) / sum(count). Preferred over weighting per-entity averages — no * precision lost to pre-rounded means, and a row missing either part is counted * rather than quietly excluded. */ type RatioSummary = { ratio: number | null; numeratorTotal: number; denominatorTotal: number; contributingRowCount: number; missingPartCount: number; }; /** * A field summary that states which basis its average stands on: `rows` counts * every row equally, `weights` re-aggregates per-entity averages by the weights * the app supplied. An agent quoting a figure needs to know which it holds. */ /** Every row counted equally — correct for a raw measurement, wrong for a mean of means. */ type RowBasisSummary = NumericSummary & { averageBasis: 'rows'; }; type WeightedBasisSummary = NumericSummary & WeightedSummary & { averageBasis: 'weights'; weightedAverage: number; weightField: string; /** False when some values had no weight, so the figure covers fewer entities. */ isWeightComplete: boolean; }; type RatioBasisSummary = NumericSummary & RatioSummary & { averageBasis: 'ratio'; ratio: number; numeratorField: string; denominatorField: string; isRatioComplete: boolean; }; /** * Discriminated on `averageBasis` so an invalid state cannot be built: a weighted * summary always carries its figure, its weight field and its coverage, and a * consumer narrowing on the basis needs no casts. */ type FieldSummary = RowBasisSummary | WeightedBasisSummary | RatioBasisSummary; /** Field summaries for one group of records, plus how many records it held. */ type GroupSummary = { key: string; rowCount: number; metrics: Record; }; /** * A target the app displays next to a figure, with the direction that makes it * good. Without the direction an agent cannot say whether 4.2 beats a goal of 5 — * for connection times it does, for ROP it does not. */ type GoalSummary = { metric: string; target: number | null; actual: number | null; direction: 'higher_is_better' | 'lower_is_better'; delta: number | null; isMet: boolean | null; /** Where the goal comes from, e.g. `personal`, `company`, `well`. */ source?: string; }; /** * A digest of what the app currently holds. Five blocks are mandatory — * `scope`, `coverage`, `units`, `summary`, `provenance` — because each one closes * a way for a correct-looking number to answer the wrong question: which entities, * how much of them, in what unit, computed how, and from where. * * `summary` is the app's own shape; the convention is `{ metrics, groups? }` built * with `summarizeFields` / `summarizeGroups`. Example rows go in `sampleRows`, * never beside the aggregates. */ type AppToolDataPayload, Row = unknown, Filters = Record> = { scope: DataScope; coverage: DataCoverageInput; /** Unit per dimension, e.g. `{ durations: 'hours', depth: 'ft' }`. */ units: Record; summary: Summary; sampleRows?: SampleRows; goals?: GoalSummary[]; /** Defaults to the loaded app state, stamped now. */ provenance?: { source?: DataProvenance['source']; computedAt?: string; }; /** Set when a PARTIAL digest should name why it is bounded. */ code?: string; message?: string; extra?: AppToolResultExtra; }; type AppToolDataNotReadyPayload> = FailureBase & { nextAction: NextAction; scope?: DataScope; }; type AppToolHandler = (params: Record) => Promise; /** The unit an app registers: wire schema + handler. */ type AppTool = AppToolDefinition & { handler: AppToolHandler; }; /** Builds one tool from app-provided dependencies. */ type AppToolFactory = (deps: Deps) => AppTool; /** Host callback an app registers its tools through. */ type AppToolRegisterFn = (tools: AppTool[]) => void; /** Host callback an app persists a setting change through. */ type AppToolSettingChangeFn = (key: string, value: unknown) => void; declare global { interface Window { __askCorvaEvalMode?: boolean; __evalGetAppState?: () => unknown; __evalSetAppState?: (overrides: Record) => Promise; } } export type { AppToolSettingChangeFn as A, AppToolHandler as B, AppToolOutcome as C, DataCoverage as D, AppToolParametersSchema as E, FieldSummary as F, GoalSummary as G, AppToolPartialResult as H, AppToolResultExtra as I, AppToolSchema as J, DataCoverageInput as K, DataProvenance as L, DataScope as M, NextAction as N, RatioBasisSummary as O, RowBasisSummary as P, WeightedBasisSummary as Q, RatioSummary as R, SampleRows as S, TimeRange as T, WeightedSummary as W, AppToolConfirmFn as a, AppToolRegisterFn as b, AppTool as c, AppToolFailureResult as d, AppToolSuccessPayload as e, AppToolResult as f, AppToolCancelledResult as g, AppToolDataPayload as h, AppToolDataReadyResult as i, AppToolDataPartialResult as j, AppToolDataNotReadyPayload as k, AppToolNotReadyResult as l, AppToolFailurePayload as m, AppToolErrorPayload as n, AppToolLegacyErrorResult as o, AppToolReadyResult as p, AppToolFactory as q, GroupSummary as r, NumericSummary as s, AppToolClass as t, AppToolConfirmation as u, AppToolConfirmationPolicy as v, AppToolConfirmationRequest as w, AppToolDataOutcome as x, AppToolDefinition as y, AppToolFailureOutcome as z };