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. `structured` is the same answer as data for * hosts that show a tool's structured content in place of its text, so it repeats the text. */ export interface ToolResult { text: string; isError?: boolean; structured?: Record; } /** 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, call?: ToolCall): Promise | ToolResult; } /** What a surface knows about the request a tool runs for. */ export interface ToolCall { /** Aborts when the caller can no longer receive the result. A read that finds it aborted clears * nothing, so the messages stay for the next read. */ signal?: AbortSignal; } /** * 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; /** 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; /** * 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. */ export declare function refuseAnyArgs(name: string, args: unknown): string | undefined; /** * 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[]; /** Receive keys no response could ever carry, whatever the window held at the time. Keyed like an * ack, so two id-less items (#662) are never one entry. */ 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; /** * Receive keys 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 receive key. */ 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 channel a send with none goes to, from the channels the agent reads. A mesh that enforces * ACLs refuses a post outside `allowPublish`, so there the default is the first concrete channel * the agent reads AND may post to: a read-only channel such as an announcements feed is never it. * An open mesh enforces no post ACL, so there it is the first concrete channel read, as the * endpoint resolves it. A wildcard subscription like `team.>` is not a destination. Undefined when * no channel qualifies; a send without a channel is then refused. */ export declare function defaultSendChannel(config: Pick, reading: readonly string[]): string | undefined; /** * Why a send with no channel has nowhere to go, and the exact next call. Built from the channels * the agent reads now and the channels its ACL lets it post to, so the refusal names a channel the * agent can use rather than pointing at another tool. Two shapes: on no channel at all, or on * channels it may post to none of. `postable` is empty where the ACL grants no concrete post * channel the agent could name (an operator widens that; nothing here can). */ export declare function sendNoDefaultRemedy(config: Pick, reading: readonly string[]): string; /** The orientation card as the model reads it, over the SAME gated tool list the connector exposes * (cotalToolSpecs already filters spawn/persona by capability), so the card can't claim a tool the * agent can't call. The `cotal_orientation` tool, a connect result and a session-start hook all * hand over this one rendering. */ export declare function orientationCard(agent: MeshAgent, config: AgentConfig, source?: string, env?: NodeJS.ProcessEnv, offered?: ReadonlySet): Promise; /** The `cotal_send` success line: where the post went and the next move. It names the channel and * any mention and makes no claim about who woke or how many received it, since nothing reports that * to the sender. A name that was new before the post says so, with close matches first because a * typo is the likely cause, and tells the sender to point others at it. Whether a later joiner is * shown the post is the channel's effective replay. */ export declare function sendResult(channel: string, mentions: readonly string[], receipt: { known: boolean; hints: readonly string[]; }, replay: boolean): string; /** The `cotal_spawn` line for a seat the manager reports started: who it is, where it is and the next * move. It names only the `join` channels the caller passed. The reply carries no channel list, and * a persona the caller does not own may list channels the caller cannot read, so without `join` it * says the seat has only what its persona lists. A rename leads, so the caller addresses the seat it * got. A seat waiting at an operator screen has not joined the mesh yet, and the line says so. A once * seat is stopped by its manager, so it is not told to despawn it. */ export declare function spawnResult(s: { requested: string; started: string; renamed: boolean; mode?: string; model?: string; join?: readonly string[]; waitingFor?: string; once: boolean; }): string; /** How to reach a channel family, since every publish names one concrete channel (SPEC §3). The * advertised `channel` text of `cotal_send`, here and in the gateway. */ export declare const PATTERN_SEND_HELP = "A pattern such as team.> is refused. To reach the agents following team.>, post once to one channel it matches, such as team.announce: those connected then get it, and one that is away gets a stored copy if that channel is durable. To reach several channels, post to each by name (cotal_channels lists the existing ones)."; /** The full Cotal tool set for a given config. Renderers iterate this; `source` names the * hosting connector and is stamped onto outgoing feedback. An explicit env restricts feedback's * Git lookup to its global config; omitted preserves process inheritance and repository lookup. * `offered` limits the set to those names for a host that serves only part of the surface, and the * orientation card then lists only those. `emptyInboxNote` lets a delivery-owning connector * explain a prior confirmed handoff when an ordinary inbox read has no unread items. It is * advisory only and cannot change selection or acknowledgements. */ export declare function cotalToolSpecs(config: AgentConfig, source?: string, env?: NodeJS.ProcessEnv, offered?: ReadonlySet, emptyInboxNote?: () => string | undefined): CotalToolSpec[]; //# sourceMappingURL=tool-specs.d.ts.map