import { z } from "zod"; import { type MeshAgent, type InboxItem } from "./agent.js"; import { type AgentConfig } from "./config.js"; /** What a Cotal tool returns: text to show the model, flagged on failure. MCP wraps it in * `content`; the OpenCode plugin returns the string. */ export interface ToolResult { text: string; isError?: boolean; } /** A tool's input contract: a **CLOSED** Zod object. Closed is the whole point — an unknown * top-level key is REFUSED, never stripped. * * A plain `z.object` DROPS unknown keys, so a caller-supplied `owner`/`actor`/`caller` argument * vanished silently and the tool ran as if it had never been sent. That is not a refusal; it is a * refusal-shaped absence, and it is indistinguishable from the argument having been rejected. The * identity a tool acts under comes from the connector's own credential and never from a tool * argument, and an attempt to supply one must be visibly turned away rather than quietly ignored. */ export type CotalToolInput = z.ZodObject; /** One Cotal tool, independent of any host's tool API. */ export interface CotalToolSpec { name: string; title: string; description: string; /** The CLOSED input object — see {@link CotalToolInput}. **Always present**, empty for a * no-argument tool: a tool that takes nothing still takes nothing *closed*, or `{owner, actor}` * on `cotal_roster` is swallowed by the very hole this type exists to shut. Adapters render from * THIS; none of them re-derives an open object, and none of them can, because * {@link cotalToolSpecs} is the only source and it closes every shape on the way out. */ schema: CotalToolInput; run(agent: MeshAgent, config: AgentConfig, args: any): Promise | ToolResult; } /** * Validate raw tool args against a spec's closed input, for the adapters whose host does NOT * validate for them: the args arrive exactly as the model wrote them, so this is the boundary. * * The MCP hosts and pi refuse an unmodelled key themselves — their `execute` is never reached. * The Hermes sidecar and OpenCode both hand the raw object straight through, so without this an * `owner`/`actor` the model believes it sent would reach {@link CotalToolSpec.run} unmentioned, or * be dropped by the first `z.object` to touch it. Refuse it by name instead: a wrong call the * caller can see and repair beats a right-looking call that quietly did something else. */ export declare function parseToolArgs(spec: CotalToolSpec, args: unknown): Record; /** * Refuse ANY caller-supplied argument to a tool an adapter publishes with none — returning the * refusal text, or `undefined` when the call is clean. * * `cotal_inbox` is the case: two adapters override it to pull quiet ambient only and supply the * `scope` themselves, so the caller's object is replaced wholesale. Replacing it is correct; * *ignoring* it is not — an `owner`/`actor` the model believes it sent would vanish on that one * tool while every sibling refuses it. The wording matches {@link parseToolArgs} so a caller cannot * tell which mechanism turned it away, and this stays dependency-free for hosts that bundle. */ /** The closed EMPTY input, for an adapter that republishes a tool with no arguments of its own. * A host given this refuses extras itself; a host given no `inputSchema` at all forwards them. */ export declare const NO_TOOL_ARGS: CotalToolInput; export declare function refuseAnyArgs(name: string, args: unknown): string | undefined; /** The neutralization and the per-item rendering live in `framing.ts`, one convention shared with * the auto-injected block, and are used here rather than restated. See that file for the rule. */ /** * HOW MUCH OF THE INBOX ONE RESPONSE MAY CARRY, in characters. * * A read is destructive, and the payload is largest exactly where recovery happens: reconnecting * brings a channel-history replay with it. Measured on a real reconnect: 200 messages, 3,490 lines, * 451 KB, an order of magnitude past what a host will hand to a model, so the call both CONSUMED * its contents and failed to deliver them. Whatever the host's own cap is, a response above this * bound is a response the caller may never see, so it is never a response we may clear. * * The budget is deliberately far below the smallest plausible host cap: overshooting costs a lost * message, undershooting costs one more call, and the response says so in its own text. */ export declare const INBOX_WINDOW_CHARS = 48000; /** What one response carries, what it leaves buffered, and the exact text that says so. */ export interface InboxResponse { /** The reply, already assembled and already inside the budget. Nothing may be appended to it. */ text: string; /** What that text actually carries. Only these may be cleared. */ shown: InboxItem[]; /** Everything it does not carry. */ held: InboxItem[]; /** Ids no response could ever carry, whatever the window held at the time. */ stuck: ReadonlySet; } /** * Build one inbox response, and make it impossible for the response to outgrow its own budget. * * THE HISTORY THIS SHAPE COMES FROM, because it explains why it assembles rather than estimates. * Three separate escapes were found here, each the same class one level further out: the items were * budgeted but an oversized one was shown alone anyway; the items were budgeted but the head line * and the held-note were not; the head and note were budgeted but the focus branch's recall warning * was appended afterwards. Every one of them was a writer to the response body that the arithmetic * did not know about. So the arithmetic is gone: this function ASSEMBLES the whole reply, measures * what it actually built, and drops trailing items until the real string fits. A future writer is * inside the bound by construction, because the bound is checked on the finished text. * * The order it drops in is the second rule: **mail before replay.** Direct messages and anycast * requests are first-party traffic with a sender waiting; replayed channel history is a backfill the * channel still holds. What gets dropped first is what someone else can still re-serve. * * And the third: **what does not fit is not cut off the end of the text.** It stays in the buffer, * unacked, named in {@link heldNote}. Only `shown` may be cleared, which is #603 itself. */ export declare function renderInbox(opts: { items: readonly InboxItem[]; /** The line above the messages, given whatever ends up being shown. */ head: (shown: readonly InboxItem[]) => string; peek?: boolean; /** A rider the response must carry, such as the focus branch's recall warning. */ warning?: string; budget?: number; /** * Ids of a lane that must be delivered IN ORDER, with no gaps: focus recall, which a caller walks * with a single mark rather than an acknowledgement per item. Stepping over one of these to fit a * later one would either strand it, if the mark then passes it, or re-serve everything after it, * if the mark stops short. The buffered lane has no such constraint, because each of its items is * acked by id. */ strictIds?: ReadonlySet; }): InboxResponse; /** Routing context for a `` tag. Keys must be [A-Za-z0-9_] (others are dropped). */ export declare function channelMeta(i: InboxItem): Record; /** The full Cotal tool set for a given config. Renderers iterate this; `source` names the * hosting connector and is stamped onto outgoing feedback. */ export declare function cotalToolSpecs(config: AgentConfig, source?: string): CotalToolSpec[]; //# sourceMappingURL=tool-specs.d.ts.map