@voltro/ai 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -353,6 +353,10 @@ export declare interface AiServiceImpl {
353
353
  export declare const aiSpendUsd: (store: DataStore, opts?: {
354
354
  readonly tenantId?: string | null;
355
355
  readonly since?: Date;
356
+ /** Scope the sum to ONE prompt version. This is the "did revision 4 cost
357
+ * more than revision 3" query, and it is the reason the digest is on the
358
+ * ledger row rather than only on the step row. */
359
+ readonly promptDigest?: string;
356
360
  }) => Effect.Effect<number>;
357
361
 
358
362
  /** Token tally as returned by `generateText` / `generateObject` / `streamText`. */
@@ -376,6 +380,10 @@ export declare interface AiUsageRecordInput {
376
380
  * models the static table prices at zero. Extract it from the AI-SDK result's
377
381
  * `providerMetadata.gateway.cost` (see `gatewayCostUsd`). */
378
382
  readonly actualCostUsd?: number;
383
+ /** Which prompt VERSION produced this call — `definePrompt(...).render(...)`
384
+ * returns one, and `aiStep` passes it through automatically. Stamped onto the
385
+ * ledger row so spend can be attributed to a prompt revision. */
386
+ readonly prompt?: PromptStamp;
379
387
  /** Override the timestamp (tests; default `new Date()`). */
380
388
  readonly calledAt?: Date;
381
389
  }
@@ -406,6 +414,9 @@ export declare interface AnyTool {
406
414
  readonly name: string;
407
415
  readonly description: string;
408
416
  readonly input: Schema.Schema.Any;
417
+ /** See `ToolDef.inputJsonSchema` — a foreign parameter schema shown to the
418
+ * model in place of a rendered `input`. */
419
+ readonly inputJsonSchema?: Record<string, unknown>;
409
420
  readonly output?: Schema.Schema.Any;
410
421
  readonly execute?: (input: never) => Effect.Effect<unknown, never, unknown>;
411
422
  readonly handler?: ToolHandler<never, unknown>;
@@ -442,6 +453,57 @@ export declare const appendStreamingMessage: (store: DataStore, input: {
442
453
  */
443
454
  export declare const applyMiddleware: (model: LanguageModelV4, middleware?: AiMiddleware | ReadonlyArray<AiMiddleware>) => LanguageModelV4;
444
455
 
456
+ /**
457
+ * The EXECUTION admission for one descriptor, as data.
458
+ *
459
+ * Same shape and same ORDER as `mcpToolDecision` in `mcpTools.ts`, on purpose:
460
+ * an app tool and an external MCP tool are admitted by two functions that agree
461
+ * about what a decision is, so one confirm-UI, one inventory type and one
462
+ * refusal message cover both. The asymmetry between them is only in step 1 —
463
+ * an app descriptor carries an `exposeAsTool` act, so an absent allowlist is
464
+ * permissive here and refused there.
465
+ *
466
+ * It exists because a SECOND caller appeared. `appTools` used to inline the
467
+ * three filters, which was fine while it was the only executor; the moment a
468
+ * transport (the inspect agent-tool surface, reached over MCP) needed the same
469
+ * verdict, an inlined chain would have become a second policy path — and a
470
+ * second policy path is how a ceiling gets bypassed. So the verdict is a
471
+ * function, `appTools` calls it, and the transport calls the same one.
472
+ *
473
+ * Order, asserted in the tests:
474
+ * 1. `passesPolicy` (deny beats allow) on the descriptor NAME,
475
+ * 2. exposability — an `exposeAsTool` object WITH a description,
476
+ * 3. the writes gate — a mutation/action needs `includeWrites`,
477
+ * 4. the CONFIRM gate — `confirm` must be backed by `requiresApproval:`.
478
+ *
479
+ * ── Why step 4 exists (`confirm` was reported, not enforced) ─────────────────
480
+ *
481
+ * `confirm` shipped as metadata: `synthesizeToolSpecs` reported it, this
482
+ * function admitted the tool anyway, and the file said out loud that enforcement
483
+ * was "the agent loop / UI"'s job. That is the shape this repo keeps paying for
484
+ * — a control that reads as enforcement in every inventory and enforces nothing
485
+ * in any caller that forgets to look.
486
+ *
487
+ * There is now somewhere for it to MEAN something. A descriptor that declares
488
+ * `requiresApproval:` parks the call as a durable approval in the shared serve
489
+ * pipeline — the same gate a human's call goes through — so the second human is
490
+ * the app's own approval queue rather than a prompt in a harness we do not
491
+ * control. A confirm tool WITH that policy is therefore admitted: executing it
492
+ * cannot skip the human, because the handler behind it will not run without one.
493
+ *
494
+ * A confirm tool WITHOUT it is refused. The alternative is to mount it and hope
495
+ * the caller asks, and the caller is a model.
496
+ */
497
+ export declare type AppToolDecision = {
498
+ readonly admitted: true;
499
+ readonly spec: SynthesizedTool;
500
+ } | {
501
+ readonly admitted: false;
502
+ readonly reason: string;
503
+ };
504
+
505
+ export declare const appToolDecision: (descriptor: AppToolDescriptor, policy: AppToolPolicy) => AppToolDecision;
506
+
445
507
  /** The descriptor fields tool synthesis reads. */
446
508
  export declare interface AppToolDescriptor {
447
509
  readonly kind: 'query' | 'mutation' | 'action' | string;
@@ -449,6 +511,15 @@ export declare interface AppToolDescriptor {
449
511
  readonly input: Schema.Schema.Any;
450
512
  readonly output?: Schema.Schema.Any;
451
513
  readonly exposeAsTool?: ExposeAsToolLike;
514
+ /**
515
+ * Presence of the descriptor's `requiresApproval:` policy — read as a BOOLEAN
516
+ * here and nothing more, so `@voltro/ai` needs no `@voltro/protocol` shape for
517
+ * it (structural, like `exposeAsTool` above).
518
+ *
519
+ * It is what turns `confirm` from a report into an enforcement: see
520
+ * `appToolDecision`.
521
+ */
522
+ readonly requiresApproval?: unknown;
452
523
  }
453
524
 
454
525
  /** A descriptor paired with its ctx-bound executor (the real handler). */
@@ -475,9 +546,14 @@ export declare interface AppToolPolicy {
475
546
  * The EXECUTABLE toolset for the agent loop. Read tools are always included;
476
547
  * WRITE tools only when `policy.includeWrites` is true (writes are opt-in —
477
548
  * the safety default). Each tool's handler runs the descriptor's bound
478
- * `invoke` (the real handler under the caller's subject). Confirm is enforced
479
- * by the agent loop / UI using `synthesizeToolSpecs` — the pure executable
480
- * tool can't gate a human prompt.
549
+ * `invoke` (the real handler under the caller's subject).
550
+ *
551
+ * A `confirm` tool is admitted only when the descriptor is APPROVAL-BACKED
552
+ * (`requiresApproval:`), and then the human step is enforced by the real handler
553
+ * — the tool call returns the typed `ApprovalRequired` refusal, a human decides
554
+ * in the app, and the identical call succeeds once. This module still gates no
555
+ * human prompt of its own, and no longer needs to: it decides which tools can
556
+ * exist, and the pipeline decides whether the call runs.
481
557
  */
482
558
  export declare const appTools: (entries: ReadonlyArray<AppToolEntry>, policy?: AppToolPolicy) => ReadonlyArray<AnyTool>;
483
559
 
@@ -660,8 +736,9 @@ export declare interface DataCopilotDeps {
660
736
  *
661
737
  * The two tables (`streamEventsTable`, `streamStateTable`) register into the
662
738
  * app's migration set like the agent/usage tables — add them to the database
663
- * barrel (or they ride along with a `*.agent.tsx` app). Run `gcResumableStreams`
664
- * periodically to drop finished streams past a retention window.
739
+ * barrel (or they ride along with a `*.agent.tsx` app). Building this store
740
+ * BOUNDS them (see `registerStreamLogRetention`); `gcResumableStreams` remains
741
+ * for a tighter, done-aware collection an app can run on its own schedule.
665
742
  */
666
743
  export declare const dataStoreResumableStreamStore: (store: DataStore) => ResumableStreamStore;
667
744
 
@@ -697,6 +774,14 @@ export declare const DEFAULT_INFERENCE_LEASE_MS: number;
697
774
  * top of it. */
698
775
  export declare const DEFAULT_INFERENCE_POLL_MS = 250;
699
776
 
777
+ /** Default window on `_voltro_ai_inferences`: 30 days since COMPLETION.
778
+ *
779
+ * Env `VOLTRO_AI_INFERENCES_TTL_HOURS`. Shorter than the usage ledger's year
780
+ * because this table stores the PROMPT verbatim (the dispatcher has to, to
781
+ * perform the call) — so it is the one AI table where "keep it forever by
782
+ * default" is a data-protection decision as well as a size one. */
783
+ export declare const DEFAULT_INFERENCE_RETENTION_HOURS: number;
784
+
700
785
  declare const DEFAULT_INPUT: Schema.Struct<{
701
786
  prompt: typeof Schema.String;
702
787
  }>;
@@ -704,6 +789,21 @@ declare const DEFAULT_INPUT: Schema.Struct<{
704
789
  /** The default judge threshold when a judge is configured without one. */
705
790
  export declare const DEFAULT_JUDGE_THRESHOLD = 0.7;
706
791
 
792
+ /** Default window on `_voltro_prompts`: a year since last use. Long, because a
793
+ * registry row is tiny and its value is answering a question about a run you
794
+ * are only now investigating. Env `VOLTRO_AI_PROMPTS_TTL_HOURS`. */
795
+ export declare const DEFAULT_PROMPT_RETENTION_HOURS: number;
796
+
797
+ /** Default window on the resumable-stream log: 7 days since the row was
798
+ * written. Env `VOLTRO_STREAM_LOG_TTL_HOURS`.
799
+ *
800
+ * It is measured in DAYS against a thing whose useful life is MINUTES on
801
+ * purpose. A resumable stream exists so a browser that lost its socket can
802
+ * reconnect and replay; nothing reads a week-old log. The width is there so the
803
+ * bound can never be the reason a reconnect came back short, which makes it a
804
+ * bound on the DISK rather than a decision about the feature. */
805
+ export declare const DEFAULT_STREAM_LOG_RETENTION_HOURS: number;
806
+
707
807
  /** Default input schema type when `input` is omitted (`{ prompt: string }`). */
708
808
  export declare type DefaultAgentInputSchema = typeof DEFAULT_INPUT;
709
809
 
@@ -754,6 +854,17 @@ export declare const defineAgent: <S extends Schema.Schema.Any = DefaultAgentInp
754
854
  */
755
855
  export declare const defineAgentExecutor: <S extends Schema.Schema.Any = DefaultAgentInputSchema>(descriptor: AgentDescriptor<S>, config: AgentExecutorConfig<AgentInput<S>>) => AgentExecutor<S>;
756
856
 
857
+ export declare interface DefinedPrompt extends PromptDefinition {
858
+ /** Content digest of `template` + `system`. The version. */
859
+ readonly digest: string;
860
+ /** Placeholder names found in the template, in first-appearance order. */
861
+ readonly variables: ReadonlyArray<string>;
862
+ /** Substitute the variables. Fails when one is missing — a `{{ticket}}` left
863
+ * unsubstituted is a prompt that quietly asks the model about the literal
864
+ * string `{{ticket}}`, which is worse than an error. */
865
+ readonly render: (vars?: Record<string, string | number | boolean>) => Effect.Effect<RenderedPrompt, PromptRenderError>;
866
+ }
867
+
757
868
  /**
758
869
  * Declare an eval suite. Validated at module load — an empty case list, a
759
870
  * duplicate case id, or a nonsensical threshold throws loudly so a broken eval
@@ -773,16 +884,46 @@ export declare interface DefineEvalInput {
773
884
  readonly tools?: ReadonlyArray<string>;
774
885
  }
775
886
 
887
+ /**
888
+ * Declare a versioned prompt.
889
+ *
890
+ * ```ts
891
+ * export const triage = definePrompt({
892
+ * id: 'support.triage',
893
+ * system: 'You triage support tickets. Answer only with the category.',
894
+ * template: 'Ticket:\n{{body}}\n\nCategories: {{categories}}',
895
+ * label: 'v3-shorter-system',
896
+ * })
897
+ * ```
898
+ *
899
+ * The digest is computed here, from the file — so it exists before any database
900
+ * does, and a test, a CLI, or an eval can identify a prompt version without a
901
+ * connection. Registration (`recordPromptVersion`) only adds the human-facing
902
+ * revision number and the retrievable copy.
903
+ */
904
+ export declare const definePrompt: (definition: PromptDefinition) => DefinedPrompt;
905
+
776
906
  export declare const defineTool: <A, I, O>(def: {
777
907
  readonly name: string;
778
908
  readonly description: string;
779
909
  readonly input: Schema.Schema<A, I>;
910
+ readonly inputJsonSchema?: Record<string, unknown>;
780
911
  readonly output?: Schema.Schema.Any;
781
912
  readonly execute?: (input: A) => Effect.Effect<O, never, unknown>;
782
913
  readonly handler?: ToolHandler<A, O>;
783
914
  readonly timeoutMs?: number;
784
915
  }) => AnyTool;
785
916
 
917
+ /**
918
+ * Describe an external tool to the model.
919
+ *
920
+ * The provenance prefix is short on purpose — it is paid per tool, per turn —
921
+ * but it is not decoration. A model that has been told which text is
922
+ * third-party has something to weigh an instruction in that text against; a
923
+ * model handed the same sentence with no attribution has nothing.
924
+ */
925
+ export declare const describeExternalTool: (namespace: string, tool: McpToolInfo) => string;
926
+
786
927
  /**
787
928
  * One dispatch tick. Never throws.
788
929
  *
@@ -961,6 +1102,15 @@ export declare type ExposeAsToolLike = boolean | {
961
1102
 
962
1103
  export { extractReasoningMiddleware }
963
1104
 
1105
+ /**
1106
+ * Flatten an MCP `content` array into the string the model sees, bounded.
1107
+ *
1108
+ * Non-text content is DESCRIBED, not inlined: a server returning a 6 MB base64
1109
+ * image would otherwise spend a context window (and a lot of money) on
1110
+ * something the text channel cannot use anyway.
1111
+ */
1112
+ export declare const flattenMcpContent: (result: Record<string, unknown>, maxBytes: number) => string;
1113
+
964
1114
  /**
965
1115
  * Pull the ACTUAL per-call cost (USD) out of an AI-SDK result's provider
966
1116
  * metadata, when the Vercel AI Gateway reported it. The gateway attaches
@@ -1322,6 +1472,36 @@ export declare const hashToVector: (text: string, dim: number) => number[];
1322
1472
  */
1323
1473
  export declare const historyToModelMessages: (history: ReadonlyArray<AgentMessage>) => ModelMessage[];
1324
1474
 
1475
+ /**
1476
+ * MCP over Streamable HTTP.
1477
+ *
1478
+ * SSRF note, stated rather than silently assumed: `url` is app configuration.
1479
+ * Nothing here validates that it points outside your own network, because a
1480
+ * legitimate deployment mounts an MCP server on a private address. If the URL
1481
+ * can ever come from user or model input in your app, gate it there — this
1482
+ * layer treats it as trusted and everything the URL RETURNS as untrusted.
1483
+ */
1484
+ export declare const httpMcpTransport: (options: HttpMcpTransportOptions) => McpTransport;
1485
+
1486
+ export declare interface HttpMcpTransportOptions {
1487
+ /** The server's Streamable-HTTP endpoint. App configuration — NEVER derived
1488
+ * from anything a model produced. */
1489
+ readonly url: string;
1490
+ /** Namespace, used only for error attribution. */
1491
+ readonly server: string;
1492
+ /**
1493
+ * Extra request headers — this is where an `Authorization` goes.
1494
+ *
1495
+ * Read it from the environment at the call site (`process.env.GITHUB_MCP_TOKEN`).
1496
+ * The framework ships no credential value for any server, and there is
1497
+ * deliberately no "default token" field here to make that accidental.
1498
+ */
1499
+ readonly headers?: Record<string, string>;
1500
+ readonly bounds?: Partial<McpBounds>;
1501
+ /** Injectable for tests. Defaults to the global `fetch`. */
1502
+ readonly fetchImpl?: typeof fetch;
1503
+ }
1504
+
1325
1505
  export declare interface InferenceDispatchDeps {
1326
1506
  readonly store: DataStore;
1327
1507
  /**
@@ -1398,12 +1578,31 @@ export declare const isAgentExecutor: (x: unknown) => x is AnyAgentExecutor;
1398
1578
 
1399
1579
  export declare const isEvalDefinition: (value: unknown) => value is EvalDefinition;
1400
1580
 
1581
+ /**
1582
+ * Is this external tool a WRITE, for the purpose of the `includeWrites` gate
1583
+ * and the confirm default?
1584
+ *
1585
+ * The default answer is YES for everything. That is not pessimism for its own
1586
+ * sake: the classification exists to decide whether an agent may take an action
1587
+ * with side effects, and the only party who can answer that for an external
1588
+ * server is the app that chose to mount it. So the app answers, with `readOnly`.
1589
+ * `trustToolHints` delegates the answer back to the server, explicitly, in one
1590
+ * place a reviewer can find.
1591
+ */
1592
+ export declare const isExternalWrite: (tool: McpToolInfo, namespace: string, policy: McpToolPolicy) => boolean;
1593
+
1594
+ /** Is this a rendered prompt or a bare string? The discriminator `aiStep` uses
1595
+ * so `prompt:` can accept either without a wrapper type at every call site. */
1596
+ export declare const isRenderedPrompt: (value: unknown) => value is RenderedPrompt;
1597
+
1401
1598
  /** Whether a stream with this id is currently registered (in flight). */
1402
1599
  export declare const isStreamActive: (streamId: string) => boolean;
1403
1600
 
1404
1601
  /** Is `x` a `defineTool(...)` result? */
1405
1602
  export declare const isTool: (x: unknown) => x is AnyTool;
1406
1603
 
1604
+ export declare const isUsableMcpToolName: (name: unknown, maxChars: number) => name is string;
1605
+
1407
1606
  export declare interface JudgeVerdict {
1408
1607
  readonly pass: boolean;
1409
1608
  /** 0..1. Below `judge.threshold` fails the case even when `pass` is true. */
@@ -1437,6 +1636,267 @@ export declare const loggingMiddleware: (opts?: {
1437
1636
  readonly log?: (event: AiCallLog) => void;
1438
1637
  }) => AiMiddleware;
1439
1638
 
1639
+ /**
1640
+ * Build ONE external tool. Exported so the gate can be tested at the sharpest
1641
+ * point: hand this a tool the policy denies and it still refuses at call time,
1642
+ * without touching the transport.
1643
+ */
1644
+ export declare const makeMcpTool: (server: McpServer, tool: McpToolInfo, policy: McpToolPolicy, bounds: McpBounds) => AnyTool;
1645
+
1646
+ export declare const MCP_BOUNDS_DEFAULTS: McpBounds;
1647
+
1648
+ /** Identity this client reports in `initialize`. No version literal here — the
1649
+ * caller passes its own, so a published build reports the lockstep release. */
1650
+ export declare const MCP_CLIENT_INFO: {
1651
+ readonly name: string;
1652
+ readonly version: string;
1653
+ };
1654
+
1655
+ /** The MCP revision this client requests. Mirror of `@voltro/mcp`'s. */
1656
+ export declare const MCP_CLIENT_PROTOCOL_VERSION = "2025-06-18";
1657
+
1658
+ /**
1659
+ * The ceilings applied to everything an external server sends.
1660
+ *
1661
+ * Resolution order is explicit option → environment variable → default, so an
1662
+ * operator can tighten a deployment without a code change and an app can pin a
1663
+ * value the operator cannot loosen.
1664
+ */
1665
+ export declare interface McpBounds {
1666
+ /** How many of a server's tools may be mounted, AFTER the policy gate.
1667
+ * A server advertising 4 000 tools would otherwise buy the whole context
1668
+ * window. Env `VOLTRO_MCP_MAX_TOOLS`. */
1669
+ readonly maxTools: number;
1670
+ /** Ceiling on one tool's description — the prompt-injection surface.
1671
+ * Env `VOLTRO_MCP_MAX_DESCRIPTION_CHARS`. */
1672
+ readonly maxDescriptionChars: number;
1673
+ /** Ceiling on a tool's advertised input JSON Schema, serialized. A schema
1674
+ * over this is DROPPED rather than truncated — a half-schema would make the
1675
+ * model call the tool wrongly. Env `VOLTRO_MCP_MAX_SCHEMA_BYTES`. */
1676
+ readonly maxSchemaBytes: number;
1677
+ /** How deep + how wide the input schema may be before it is pruned. Guards
1678
+ * the recursive sanitizer itself against a schema built to blow the stack. */
1679
+ readonly maxSchemaDepth: number;
1680
+ readonly maxSchemaNodes: number;
1681
+ /** Ceiling on one tool-call RESULT as handed back to the model. Truncated
1682
+ * with a visible marker. Env `VOLTRO_MCP_MAX_RESULT_BYTES`. */
1683
+ readonly maxResultBytes: number;
1684
+ /** Hard ceiling on any single HTTP/stdio response body, applied while
1685
+ * reading — an unbounded stream is cancelled, not buffered.
1686
+ * Env `VOLTRO_MCP_MAX_RESPONSE_BYTES`. */
1687
+ readonly maxResponseBytes: number;
1688
+ /** Per-JSON-RPC-request deadline. Env `VOLTRO_MCP_TIMEOUT_MS`. */
1689
+ readonly requestTimeoutMs: number;
1690
+ /** Ceiling on a tool NAME. Longer names are dropped, not truncated —
1691
+ * a truncated name would not be the name we allowed. */
1692
+ readonly maxToolNameChars: number;
1693
+ }
1694
+
1695
+ /**
1696
+ * `tools/call`. Returns the flattened, bounded text the model receives.
1697
+ *
1698
+ * `isError: true` is surfaced as text prefixed with `Error:` rather than as a
1699
+ * failed Effect: a tool that failed IS information the model should get and act
1700
+ * on, and that is exactly how `buildSdkTools` treats an app tool's rejection.
1701
+ */
1702
+ export declare const mcpCallTool: (transport: McpTransport, server: string, toolName: string, args: Record<string, unknown>, bounds: McpBounds) => Effect.Effect<string, McpError>;
1703
+
1704
+ /**
1705
+ * Typed MCP client failure.
1706
+ *
1707
+ * `transport` — the socket/process/HTTP layer failed or timed out.
1708
+ * `protocol` — we reached the server and it answered with something that is
1709
+ * not a usable MCP response (a JSON-RPC error, a malformed body).
1710
+ * `bounds` — the server answered, and the answer exceeded a stated bound.
1711
+ * Kept SEPARATE from `protocol` on purpose: "the server is broken"
1712
+ * and "the server sent 40 MB" are different operational events and
1713
+ * the second one is the one you want to alert on.
1714
+ * `policy` — the app's own allow/deny gate refused. Never the server's fault.
1715
+ */
1716
+ export declare class McpError extends McpError_base {
1717
+ }
1718
+
1719
+ declare const McpError_base: Schema.TaggedErrorClass<McpError, "McpError", {
1720
+ readonly _tag: Schema.tag<"McpError">;
1721
+ } & {
1722
+ /** The app-chosen namespace of the server, never a server-supplied string. */
1723
+ server: typeof Schema.String;
1724
+ reason: Schema.Literal<["transport", "protocol", "bounds", "policy"]>;
1725
+ message: typeof Schema.String;
1726
+ }>;
1727
+
1728
+ /** The MCP handshake: `initialize`, then the `notifications/initialized` the
1729
+ * spec requires before any other request. */
1730
+ export declare const mcpInitialize: (transport: McpTransport, server: string, clientInfo?: {
1731
+ readonly name: string;
1732
+ readonly version: string;
1733
+ }) => Effect.Effect<{
1734
+ readonly protocolVersion: string;
1735
+ readonly serverName: string;
1736
+ }, McpError>;
1737
+
1738
+ export declare interface McpJsonRpcRequest {
1739
+ readonly jsonrpc: '2.0';
1740
+ readonly id?: string | number | null;
1741
+ readonly method: string;
1742
+ readonly params?: Record<string, unknown>;
1743
+ }
1744
+
1745
+ export declare interface McpJsonRpcResponse {
1746
+ readonly jsonrpc: '2.0';
1747
+ readonly id: string | number | null;
1748
+ readonly result?: unknown;
1749
+ readonly error?: {
1750
+ readonly code: number;
1751
+ readonly message: string;
1752
+ };
1753
+ }
1754
+
1755
+ /**
1756
+ * `tools/list`, bounded.
1757
+ *
1758
+ * Everything a server can inflate is capped HERE rather than at the call site:
1759
+ * a tool whose name is unusable or whose schema is oversized is DROPPED (with
1760
+ * its name reported), a description is sanitized + truncated, and the list is
1761
+ * cut to `maxTools` after sorting so the cut is deterministic across calls
1762
+ * rather than "whichever ones the server put first this time".
1763
+ */
1764
+ export declare const mcpListTools: (transport: McpTransport, server: string, bounds: McpBounds) => Effect.Effect<{
1765
+ readonly tools: ReadonlyArray<McpToolInfo>;
1766
+ readonly dropped: ReadonlyArray<{
1767
+ readonly name: string;
1768
+ readonly reason: string;
1769
+ }>;
1770
+ }, McpError>;
1771
+
1772
+ /** An external server, as the app names and reaches it. */
1773
+ export declare interface McpServer {
1774
+ /**
1775
+ * The app's own name for this server. Prefixes every tool tag
1776
+ * (`github.create_issue`), so it is what an allow/deny rule matches on.
1777
+ * App-chosen — never the server's self-reported name, which is untrusted.
1778
+ */
1779
+ readonly namespace: string;
1780
+ readonly transport: McpTransport;
1781
+ }
1782
+
1783
+ export declare type McpToolDecision = {
1784
+ readonly admitted: true;
1785
+ readonly spec: SynthesizedTool;
1786
+ } | {
1787
+ readonly admitted: false;
1788
+ readonly reason: string;
1789
+ };
1790
+
1791
+ /**
1792
+ * THE gate. One pure function, called at mount AND re-called inside every tool
1793
+ * body before a request leaves the process, so a toolset that was tampered with
1794
+ * after construction still cannot reach the server.
1795
+ *
1796
+ * Order matters and is asserted in the tests: default-deny, then the shared
1797
+ * `passesPolicy` (deny beats allow), then the writes gate.
1798
+ */
1799
+ export declare const mcpToolDecision: (tool: McpToolInfo, namespace: string, policy: McpToolPolicy) => McpToolDecision;
1800
+
1801
+ /** One tool as an external server advertises it, already bounded + sanitized. */
1802
+ export declare interface McpToolInfo {
1803
+ readonly name: string;
1804
+ readonly description: string;
1805
+ readonly inputSchema: Record<string, unknown>;
1806
+ /**
1807
+ * The server's own hints about the tool. ADVISORY ONLY — see
1808
+ * `mcpTools.ts`; the server is the untrusted party, so its claim that a tool
1809
+ * is read-only cannot be what decides whether the tool is allowed to run.
1810
+ */
1811
+ readonly annotations?: {
1812
+ readonly readOnlyHint?: boolean;
1813
+ readonly destructiveHint?: boolean;
1814
+ readonly idempotentHint?: boolean;
1815
+ };
1816
+ }
1817
+
1818
+ /**
1819
+ * The mount policy. Extends `AppToolPolicy` rather than paralleling it: an app
1820
+ * that already expresses its agent policy in one shape expresses this one in
1821
+ * the same shape, and the two lists can literally be the same object.
1822
+ */
1823
+ export declare interface McpToolPolicy extends AppToolPolicy {
1824
+ /**
1825
+ * Tools the APP declares to be reads (tags, `*` globs, or a RegExp), matched
1826
+ * against the namespaced tag. This is the app's judgement, and it is the only
1827
+ * one that counts by default — see {@link isExternalWrite}.
1828
+ */
1829
+ readonly readOnly?: ReadonlyArray<string> | RegExp;
1830
+ /**
1831
+ * Accept the server's `annotations.readOnlyHint` as evidence a tool is a read.
1832
+ *
1833
+ * Default FALSE, and the default is the security decision. For an app tool the
1834
+ * read/write split comes from the descriptor KIND, which somebody in this
1835
+ * repository wrote. For an external tool it would come from the untrusted
1836
+ * party, who can flip it between two `tools/list` calls — so trusting it would
1837
+ * let a server talk its way past `includeWrites: false`.
1838
+ */
1839
+ readonly trustToolHints?: boolean;
1840
+ /** Ceilings on everything the server sends. See {@link McpBounds}. */
1841
+ readonly bounds?: Partial<McpBounds>;
1842
+ }
1843
+
1844
+ /** The `Record<string, AnyTool>` an agent loop wants, without the inventory. */
1845
+ export declare const mcpTools: (server: McpServer, policy: McpToolPolicy) => Effect.Effect<Record<string, AnyTool>, McpError>;
1846
+
1847
+ /** A mounted server's toolset. */
1848
+ export declare interface McpToolset {
1849
+ readonly namespace: string;
1850
+ /** Keyed by the namespaced tag — the shape the agent loop consumes. */
1851
+ readonly tools: Record<string, AnyTool>;
1852
+ /** The same inventory type `synthesizeToolSpecs` returns, for a confirm-UI. */
1853
+ readonly specs: ReadonlyArray<SynthesizedTool>;
1854
+ /** Everything the server advertised that did NOT mount, and why. Surfaced
1855
+ * rather than logged: a tool silently missing from an agent's set is a
1856
+ * support ticket that starts "the agent just says it can't do that". */
1857
+ readonly dropped: ReadonlyArray<{
1858
+ readonly name: string;
1859
+ readonly reason: string;
1860
+ }>;
1861
+ /** The bounds actually in force (after env + defaults resolved). */
1862
+ readonly bounds: McpBounds;
1863
+ readonly close: () => Effect.Effect<void>;
1864
+ }
1865
+
1866
+ /**
1867
+ * Connect to an external MCP server, list its tools, and mount the ones this
1868
+ * app's policy admits.
1869
+ *
1870
+ * ```ts
1871
+ * const github = yield* mcpToolset(
1872
+ * { namespace: 'github', transport: httpMcpTransport({
1873
+ * server: 'github', url: process.env.GITHUB_MCP_URL!,
1874
+ * headers: { authorization: `Bearer ${process.env.GITHUB_MCP_TOKEN!}` },
1875
+ * }) },
1876
+ * { allow: ['github.get_*', 'github.search_*'], readOnly: ['github.get_*', 'github.search_*'] },
1877
+ * )
1878
+ * yield* generateObjectWithTools({ prompt, tools: github.tools, schema: Result })
1879
+ * ```
1880
+ */
1881
+ export declare const mcpToolset: (server: McpServer, policy: McpToolPolicy) => Effect.Effect<McpToolset, McpError>;
1882
+
1883
+ /** `<namespace>.<toolName>` — the tag an allow/deny rule matches. */
1884
+ export declare const mcpToolTag: (namespace: string, toolName: string) => string;
1885
+
1886
+ /**
1887
+ * One JSON-RPC round trip against an external server. The seam every test uses
1888
+ * — a test supplies an in-memory transport and never opens a socket, which is
1889
+ * what makes the untrusted-input bounds assertable on their own.
1890
+ */
1891
+ export declare interface McpTransport {
1892
+ /** Send a request; resolve with its response. */
1893
+ readonly send: (request: McpJsonRpcRequest) => Effect.Effect<McpJsonRpcResponse, McpError>;
1894
+ /** Send a notification (no id, no response expected). */
1895
+ readonly notify: (request: McpJsonRpcRequest) => Effect.Effect<void, McpError>;
1896
+ /** Release the socket / child process. Idempotent. */
1897
+ readonly close: () => Effect.Effect<void>;
1898
+ }
1899
+
1440
1900
  /** Token tally, mirroring `GenerateTextResult['usage']`. Only `generateImage`
1441
1901
  * reports one (the speech + video model specs carry no usage), which is why
1442
1902
  * it's optional on every media result. */
@@ -1619,6 +2079,16 @@ export declare type OpenAIModel = 'gpt-5.5' | 'gpt-4o' | 'gpt-4o-mini' | (string
1619
2079
  * cannot be journaled, and its JSON rendering can. */
1620
2080
  export declare const outputJsonSchema: (schema: Schema.Schema.Any, fn: string) => Effect.Effect<Record<string, unknown>, AiError>;
1621
2081
 
2082
+ /**
2083
+ * Pull the first JSON-RPC message out of a Streamable-HTTP response body.
2084
+ * A server may answer a POST with `application/json` (one object) or with
2085
+ * `text/event-stream` (SSE frames, the response interleaved with whatever else
2086
+ * the server wants to push). Both are spec-legal; we take the first frame whose
2087
+ * `id` matches and ignore the rest, because a client that keeps reading after
2088
+ * its answer is a client an unbounded server can hold open forever.
2089
+ */
2090
+ export declare const parseHttpMcpBody: (body: string, contentType: string, id: string | number | null) => McpJsonRpcResponse | null;
2091
+
1622
2092
  /** Does this tag pass the allow/deny policy? deny wins; allow (when set) gates. */
1623
2093
  export declare const passesPolicy: (tag: string, policy: AppToolPolicy) => boolean;
1624
2094
 
@@ -1638,6 +2108,119 @@ export declare const patchStreamingMessage: (store: DataStore, id: string, parts
1638
2108
  */
1639
2109
  export declare const priceForModel: (model: string) => ModelPrice | undefined;
1640
2110
 
2111
+ export declare interface PromptDefinition {
2112
+ /**
2113
+ * Stable identity across revisions. Dotted, like a descriptor tag
2114
+ * (`support.triage`) — the same vocabulary the rest of the framework uses for
2115
+ * "the name of a thing an app owns".
2116
+ */
2117
+ readonly id: string;
2118
+ /**
2119
+ * The template, with `{{name}}` placeholders. A STRING, not a function, and
2120
+ * that is the design: a function has no stable content to hash, so a
2121
+ * function-built prompt could be versioned only by hashing its output — which
2122
+ * would make every distinct customer message a new "version". Compose in the
2123
+ * variables instead.
2124
+ */
2125
+ readonly template: string;
2126
+ /** Optional system message. Part of the digest — changing it IS a new
2127
+ * version, because it changes the model's behaviour as surely as the
2128
+ * template does. */
2129
+ readonly system?: string;
2130
+ /** Optional human label for this revision (`'shorter-system'`). Metadata:
2131
+ * deliberately NOT part of the digest, so relabelling does not fork a
2132
+ * version. */
2133
+ readonly label?: string;
2134
+ }
2135
+
2136
+ /** The digest form: a prefix, not the whole hash. This is a correlation handle,
2137
+ * and a full sha256 in a dashboard cell is a wall of hex nobody reads.
2138
+ *
2139
+ * Lives here rather than in `inferStep.ts` (where it started) because it is now
2140
+ * the identity of a versioned artefact, and `inferStep` imports
2141
+ * `@voltro/workflow` — a prompt registry has no business needing the workflow
2142
+ * engine to compute a hash. `inferStep` re-exports it. */
2143
+ export declare const promptDigest: (prompt: string) => string;
2144
+
2145
+ export declare class PromptRenderError extends PromptRenderError_base {
2146
+ }
2147
+
2148
+ declare const PromptRenderError_base: Schema.TaggedErrorClass<PromptRenderError, "PromptRenderError", {
2149
+ readonly _tag: Schema.tag<"PromptRenderError">;
2150
+ } & {
2151
+ promptId: typeof Schema.String;
2152
+ missing: Schema.Array$<typeof Schema.String>;
2153
+ message: typeof Schema.String;
2154
+ }>;
2155
+
2156
+ export declare const PROMPTS_TABLE = "_voltro_prompts";
2157
+
2158
+ /**
2159
+ * One row per (prompt id · content digest) — i.e. one row per VERSION.
2160
+ *
2161
+ * `promptId` + `digest` are `.maxLength(191)` because they carry a composite
2162
+ * UNIQUE: an unbounded text column under a UNIQUE is backed on MariaDB by a
2163
+ * hash long-unique index whose hidden column wedges binlog CDC (see
2164
+ * `@voltro/database`'s maintainer note). 191 is the utf8mb4 index-prefix limit.
2165
+ *
2166
+ * `lastUsedAt` is what the retention sweep prunes on, and the table is
2167
+ * self-healing under it: `recordPromptVersion` re-inserts on the next use, so a
2168
+ * version that ages out is a version nothing has run in a year.
2169
+ */
2170
+ export declare const promptsTable: Table<typeof PROMPTS_TABLE, Record<string, ColumnDefinition<unknown>>, true>;
2171
+
2172
+ /** A prompt's identity, as it is stamped onto a step row and a usage row. */
2173
+ export declare interface PromptStamp {
2174
+ /** The author-chosen stable id, e.g. `support.triage`. */
2175
+ readonly promptId: string;
2176
+ /** Content digest of the template (+ system). CHANGES when the template does
2177
+ * — that is the version. */
2178
+ readonly digest: string;
2179
+ /** The monotonic revision `recordPromptVersion` assigned, once it has been
2180
+ * registered. Absent before first registration; the digest is the identity
2181
+ * either way. */
2182
+ readonly revision?: number;
2183
+ /** Optional human label the author gave this revision. */
2184
+ readonly label?: string;
2185
+ }
2186
+
2187
+ /** Look one version up by its digest — the reverse of the stamp on a step row
2188
+ * or a usage row, and therefore the answer to "which prompt produced this
2189
+ * run". */
2190
+ export declare const promptVersionByDigest: (store: DataStore, digest: string) => Effect.Effect<PromptVersionRow | null>;
2191
+
2192
+ /**
2193
+ * The version digest of a (template, system) pair.
2194
+ *
2195
+ * LENGTH-PREFIXED, not separator-joined. A separator is only injective when it
2196
+ * cannot occur in the parts, and every printable candidate can occur in prose:
2197
+ * join on a space and template `'a b'` + no system collides with template `'a'`
2198
+ * + system `'b'`, so two genuinely different prompts read as ONE version and the
2199
+ * second one's provenance silently points at the first. A length prefix has no
2200
+ * such case and needs no exotic character.
2201
+ *
2202
+ * (The first version of this used a raw NUL as the separator, which is the
2203
+ * repo-wide rule this file now also demonstrates the reason for: the file
2204
+ * became BINARY to grep, and every text search over it silently returned
2205
+ * nothing — indistinguishable from a clean file.)
2206
+ */
2207
+ export declare const promptVersionDigest: (template: string, system: string | undefined) => string;
2208
+
2209
+ /** A registered version, as read back. */
2210
+ export declare interface PromptVersionRow {
2211
+ readonly promptId: string;
2212
+ readonly digest: string;
2213
+ readonly revision: number;
2214
+ readonly label: string | null;
2215
+ readonly template: string | null;
2216
+ readonly system: string | null;
2217
+ readonly firstSeenAt: Date | null;
2218
+ readonly lastUsedAt: Date | null;
2219
+ }
2220
+
2221
+ /** Every registered version of one prompt, newest revision first. */
2222
+ export declare const promptVersionsFor: (store: DataStore, promptId: string) => Effect.Effect<ReadonlyArray<PromptVersionRow>>;
2223
+
1641
2224
  /**
1642
2225
  * A resolved provider choice — a DISCRIMINATED UNION on `name`. Each provider
1643
2226
  * only carries the fields that apply to it: the mock-only `mockText` / `script`
@@ -1749,6 +2332,10 @@ export declare interface QueuedInference {
1749
2332
  readonly model: string;
1750
2333
  readonly prompt: string;
1751
2334
  readonly system: string | null;
2335
+ /** Prompt provenance carried from the parked run — see the table. */
2336
+ readonly promptId: string | null;
2337
+ readonly promptDigest: string | null;
2338
+ readonly promptRevision: number | null;
1752
2339
  readonly maxTokens: number | null;
1753
2340
  readonly providerOptions: unknown;
1754
2341
  readonly jsonSchema: Record<string, unknown> | null;
@@ -1775,6 +2362,25 @@ export declare interface ReadRecorder<S extends QueryableStore> {
1775
2362
  */
1776
2363
  export declare const recordAiUsage: (store: DataStore, record: AiUsageRecordInput) => Effect.Effect<CostBreakdown>;
1777
2364
 
2365
+ /**
2366
+ * Register a prompt version, idempotently, and return it.
2367
+ *
2368
+ * First sight of a `(promptId, digest)` pair inserts it with the next revision
2369
+ * number for that `promptId`; every later sight only touches `lastUsedAt`. The
2370
+ * insert is an `insertIgnore` over the composite UNIQUE, so two replicas
2371
+ * racing the same first sight produce one row.
2372
+ *
2373
+ * The revision NUMBER is read-then-write and therefore not strictly serialised:
2374
+ * two different versions of one prompt first seen in the same millisecond on
2375
+ * two replicas can both land on revision 4. That is accepted deliberately — the
2376
+ * digest is the identity and it is exact; the revision is a label for humans,
2377
+ * and paying for a serialisable counter to make a label prettier is the wrong
2378
+ * trade. Order by `firstSeenAt` when you need the true sequence.
2379
+ */
2380
+ export declare const recordPromptVersion: (store: DataStore, prompt: PromptDefinition & {
2381
+ readonly digest?: string;
2382
+ }) => Effect.Effect<PromptVersionRow>;
2383
+
1778
2384
  /**
1779
2385
  * Wrap a store so the rows it serves become a dependency set — automatic
1780
2386
  * provenance capture with no runtime edit. Every `query()` records
@@ -1802,6 +2408,16 @@ export declare const redisResumableStreamStore: (redis: ResumableRedis, opts?: {
1802
2408
  readonly ttlSeconds?: number;
1803
2409
  }) => ResumableStreamStore;
1804
2410
 
2411
+ /**
2412
+ * Re-list and re-gate a mounted server.
2413
+ *
2414
+ * This is the ONLY way the tool set changes. Nothing reacts to a
2415
+ * `notifications/tools/list_changed`, and that is deliberate: a set that can be
2416
+ * rewritten by the party it protects against is not a set, and the cost of the
2417
+ * alternative is a stale tool until the app decides otherwise.
2418
+ */
2419
+ export declare const refreshMcpToolset: (server: McpServer, policy: McpToolPolicy) => Effect.Effect<McpToolset, McpError>;
2420
+
1805
2421
  /**
1806
2422
  * Register (or reuse) the `AbortController` for `streamId`. `streamText` calls
1807
2423
  * this when given a `streamId`; it threads the returned controller's `signal`
@@ -1811,6 +2427,18 @@ export declare const redisResumableStreamStore: (redis: ResumableRedis, opts?: {
1811
2427
  */
1812
2428
  export declare const registerStream: (streamId: string) => AbortController;
1813
2429
 
2430
+ /** What `.render(vars)` produces — a stamp plus the text to send. */
2431
+ export declare interface RenderedPrompt extends PromptStamp {
2432
+ readonly prompt: string;
2433
+ readonly system?: string;
2434
+ /** The artefact this was rendered from. Carried along so a consumer that
2435
+ * receives only the rendered value (`aiStep`) can REGISTER the version
2436
+ * without the app having to remember a separate boot-time call. It is the
2437
+ * template — code, not data — so carrying it costs nothing a repository
2438
+ * checkout does not already cost. */
2439
+ readonly definition: PromptDefinition;
2440
+ }
2441
+
1814
2442
  /**
1815
2443
  * Gate an AI call against a per-tenant USD budget with an ATOMIC reservation —
1816
2444
  * the cap precedent is billing's `requireEntitlement` / storage's
@@ -1888,6 +2516,9 @@ export declare const resolveEmbeddingModel: (config: EmbeddingProviderConfig) =>
1888
2516
 
1889
2517
  export declare const resolveImageModel: (config: ProviderConfig) => ImageModelV4;
1890
2518
 
2519
+ /** Explicit option → env → default, per field. */
2520
+ export declare const resolveMcpBounds: (overrides: Partial<McpBounds> | undefined, env?: Record<string, string | undefined>) => McpBounds;
2521
+
1891
2522
  export declare const resolveModel: (config: ProviderConfig, middleware?: AiMiddleware | ReadonlyArray<AiMiddleware>) => LanguageModelV4;
1892
2523
 
1893
2524
  export declare const resolveSpeechModel: (config: ProviderConfig) => SpeechModelV4;
@@ -2076,6 +2707,30 @@ export declare interface RunEvalDeps {
2076
2707
  readonly branchId?: string;
2077
2708
  }
2078
2709
 
2710
+ /**
2711
+ * Make one server-supplied string safe to place in a model's context: strip
2712
+ * invisibles, normalise runaway whitespace, and cap the length with a marker
2713
+ * the model can see.
2714
+ *
2715
+ * The cap is the honest part. It does NOT stop a description from CONTAINING
2716
+ * an instruction — nothing does, short of not showing the model the description
2717
+ * at all, which would make the tool unusable. It stops the description from
2718
+ * being a whole second system prompt, and it stops the invisible encodings that
2719
+ * make an injection unreviewable by a human reading the same string.
2720
+ */
2721
+ export declare const sanitizeExternalText: (raw: unknown, maxChars: number) => string;
2722
+
2723
+ /**
2724
+ * Recursively bound + clean a server's input JSON Schema.
2725
+ *
2726
+ * Depth and node budget are enforced as we walk (a subtree past either is
2727
+ * dropped, not truncated mid-object), and every `description` / `title` string
2728
+ * anywhere in the tree is sanitized — those are model-visible too, and a
2729
+ * per-property description is the least-inspected place to hide an instruction.
2730
+ * Returns `null` when the schema is unusable, in which case the tool is dropped.
2731
+ */
2732
+ export declare const sanitizeInputSchema: (schema: unknown, bounds: McpBounds) => Record<string, unknown> | null;
2733
+
2079
2734
  /**
2080
2735
  * Combine assertion failures + the judge verdict into one pass/fail. Pure. A case
2081
2736
  * passes iff no assertion failed AND (no judge, or the judge's score clears the
@@ -2252,6 +2907,31 @@ export { simulateStreamingMiddleware }
2252
2907
  */
2253
2908
  export declare const startResumableProducer: (streamId: string, store: ResumableStreamStore, source: () => Stream.Stream<AgentEvent>) => Effect.Effect<void>;
2254
2909
 
2910
+ /**
2911
+ * MCP over stdio — the transport most published servers ship (`npx
2912
+ * some-mcp-server`).
2913
+ *
2914
+ * The child does NOT inherit `process.env`. That is a deliberate difference
2915
+ * from `child_process.spawn`'s default: an MCP server is a program you are
2916
+ * handing your model's tool surface to, and the default should not also hand
2917
+ * it every credential in the process. Spread what it needs explicitly.
2918
+ */
2919
+ export declare const stdioMcpTransport: (options: StdioMcpTransportOptions) => McpTransport;
2920
+
2921
+ export declare interface StdioMcpTransportOptions {
2922
+ /** Executable to spawn. App configuration, never model output. */
2923
+ readonly command: string;
2924
+ readonly args?: ReadonlyArray<string>;
2925
+ /** Namespace, for error attribution. */
2926
+ readonly server: string;
2927
+ /** Environment for the child. Pass credentials here, from YOUR env — the
2928
+ * child does NOT inherit the parent environment unless you spread it in,
2929
+ * so a server cannot read a secret you did not hand it. */
2930
+ readonly env?: Record<string, string>;
2931
+ readonly cwd?: string;
2932
+ readonly bounds?: Partial<McpBounds>;
2933
+ }
2934
+
2255
2935
  export declare const STREAM_EVENTS_TABLE = "_voltro_stream_events";
2256
2936
 
2257
2937
  export declare const STREAM_STATE_TABLE = "_voltro_stream_state";
@@ -2368,6 +3048,16 @@ export declare interface SynthesizedTool {
2368
3048
  readonly write: boolean;
2369
3049
  /** Require human confirmation before this call executes. */
2370
3050
  readonly confirm: boolean;
3051
+ /**
3052
+ * Whether that confirmation is ENFORCED by the app — i.e. the descriptor also
3053
+ * declares `requiresApproval:`, so the real handler parks the call as a
3054
+ * durable approval instead of running it.
3055
+ *
3056
+ * The inventory reports both flags because they answer different questions: a
3057
+ * confirm-UI asks `confirm`, and "why is this tool not executable" is answered
3058
+ * only by the pair.
3059
+ */
3060
+ readonly approvalBacked: boolean;
2371
3061
  readonly maxPerRun?: number;
2372
3062
  }
2373
3063
 
@@ -2375,6 +3065,10 @@ export declare interface SynthesizedTool {
2375
3065
  * The discovery/inventory view: every descriptor that is exposable AND passes
2376
3066
  * the policy, as `SynthesizedTool` metadata (no executable body). Use for the
2377
3067
  * capability surface / a confirm-UI. Sorted by name.
3068
+ *
3069
+ * Deliberately does NOT apply the `includeWrites` gate — that is an EXECUTION
3070
+ * decision (`appToolDecision`), and an inventory that hid write tools would
3071
+ * make a confirm-UI unable to explain why one is missing.
2378
3072
  */
2379
3073
  export declare const synthesizeToolSpecs: (entries: ReadonlyArray<AppToolEntry>, policy?: AppToolPolicy) => ReadonlyArray<SynthesizedTool>;
2380
3074
 
@@ -2409,6 +3103,18 @@ export declare interface ToolDef<A, I, O> {
2409
3103
  readonly name: string;
2410
3104
  readonly description: string;
2411
3105
  readonly input: Schema.Schema<A, I>;
3106
+ /**
3107
+ * A ready-made JSON Schema for the tool's parameters, used INSTEAD of
3108
+ * rendering `input`.
3109
+ *
3110
+ * For a tool whose shape is defined elsewhere — an external MCP server's
3111
+ * `inputSchema` — there is no faithful Effect Schema to render from, and a
3112
+ * lossy re-expression would reject arguments the far side would have
3113
+ * accepted. So the far side's schema is what the model sees, while `input`
3114
+ * stays the decode gate on the way in. Leave unset for a normal tool; a
3115
+ * rendered `input` is strictly better when you own the shape.
3116
+ */
3117
+ readonly inputJsonSchema?: Record<string, unknown>;
2412
3118
  /** Optional result schema. When set, `buildSdkTools` decodes the body's
2413
3119
  * output through it before handing the value back to the model. */
2414
3120
  readonly output?: Schema.Schema.Any;