import type { HttpContext } from '@adonisjs/core/http';
import { AgentService } from '../agent-service.js';
import type { AttachmentLimits } from '../attachment-limits.js';
import type { UiCapabilities } from '../genui/capabilities.js';
import type { Catalog } from '../genui/catalog.js';
import { type GenuiChannelBase, type GenuiChannels } from '../genui/channels.js';
import type { ActionProposal } from '../spi/action-proposal-store.js';
import type { AttachmentRef } from '../spi/attachment-staging.js';
import type { Actor, PageContext } from '../types.js';
import { type ChannelRetryOptions, type ChannelStepServer, type ChannelWorkflowEngine } from './executor.js';
import { type MarkdownTable } from './markdown.js';
import { type ChannelStore } from './store.js';
import { type ChannelComponent, type ChannelMediaRefusal, type ChannelTexts, type ChannelTextsOverrides } from './texts.js';
import type { ChannelAdapter, ChannelMediaFile, ChannelRequest, InboundMedia, InboundMessage, OutboundMedia, OutboundMessage } from './types.js';
/**
 * What a channel turn needs from the agent — `AgentService` is one. The optional methods turn
 * features on: proposals (buttons, outcomes), questions (`answer`), media (`stageAttachment`), and
 * the Lucid default store (`lucidDatabase`).
 */
export interface ChannelTurnService extends Pick<AgentService, 'subscribe' | 'skip' | 'cancel' | 'actionProposalReply'>, Partial<Pick<AgentService, 'decideActionProposal' | 'listActionProposals' | 'actionProposalVocabulary' | 'actionApprovalMode' | 'answer' | 'attachmentLimits' | 'stageAttachment' | 'lucidDatabase' | 'drive'>> {
    /**
     * Start (or queue) a turn. The channel passes `{ textDecisions: false }`: it decides text
     * decisions itself, only for the cards it delivered to the conversation.
     */
    send(...args: Parameters<AgentService['send']>): ReturnType<AgentService['send']>;
}
/**
 * Something the channel sends on the app's behalf (a hook's answer): text in the model's markdown
 * (converted and split for the channel), text sent exactly as given (`raw` — a message the person
 * forwards as is), or a file with a caption.
 */
export type ChannelReply = string | {
    text: string;
    raw?: boolean;
} | {
    media: OutboundMedia;
    /** In the model's markdown, converted. */
    caption?: string;
    /** What a channel without files sends instead. Default: the caption. */
    fallbackText?: string;
};
/** What {@link ChannelHandleOptions.beforeTurn} decides. */
export type ChannelGate = 'continue' | 'stop' | undefined | {
    reply: ChannelReply;
} | {
    replies: ChannelReply[];
};
/** What a hook is told about the message being handled. */
export interface ChannelHookContext {
    /** The adapter's name. */
    channel: string;
    conversation: string;
    message: InboundMessage;
    /**
     * Every message the turn is made of, in the order they arrived — set only when `coalesce` grouped
     * several into one turn (`message` is then one of them: the one the hook is about, or the first).
     */
    coalesced?: readonly InboundMessage[];
}
/** One message about to go out — what {@link ChannelHandleOptions.canDeliver} checks. */
export interface ChannelDelivery {
    channel: string;
    conversation: string;
    /** What goes out, exactly as the adapter will get it. */
    outbound: OutboundMessage;
    /**
     * What it is: the turn's answer (`reply`), a proposal card, a relayed `outcome`, a `question`, a
     * hook's answer before the turn (`gate`), or the channel's own text (`notice`).
     */
    kind: 'reply' | 'card' | 'outcome' | 'question' | 'gate' | 'notice';
    /** Who the conversation was answered as, when known. */
    actor: Actor | null;
    /** The account reference the outbound belongs to: the actor's id, or a relayed proposal's `actorRef`. */
    actorRef: string | null;
    /** The message being answered, when there is one. */
    message: InboundMessage | null;
    /** The proposal whose outcome is relayed (`kind: 'outcome'`). */
    proposal: ActionProposal | null;
}
/** What a webhook request came to — {@link ChannelHandleOptions.onWebhook}. */
export interface ChannelWebhookEvent {
    channel: string;
    /**
     * `accepted` → at least one new message taken; `duplicate` → only messages already taken;
     * `ignored` → a verified body with no message to answer; `unauthorized` → `verify` refused it;
     * `challenge` → a subscription check answered; `method_not_allowed`; `failed` → the messages
     * could not be taken (store or engine down): answered `500`, the provider retries.
     */
    status: 'accepted' | 'duplicate' | 'ignored' | 'unauthorized' | 'challenge' | 'method_not_allowed' | 'failed';
    /** Why it was ignored (the adapter's reason — never message content), or what failed. */
    reason?: string;
    /** The provider's event name, when the body has one. */
    event?: string;
    /** New messages taken. */
    accepted: number;
    /** Messages already taken before (provider retries). */
    duplicates: number;
    /** The new messages, for counting — handled on their own, do not answer them here. */
    messages: readonly InboundMessage[];
}
/** A turn has started — {@link ChannelHandleOptions.onTurnStarted}. */
export interface ChannelTurnStarted {
    runId: string;
    threadId: string;
    actor: Actor;
    message: InboundMessage;
    /** The message waits in the thread's queue (its run starts later under `runId`). */
    queued: boolean;
    /**
     * Every message the turn is made of, in order — set only when `coalesce` grouped several (then
     * `message` is the first of them).
     */
    coalesced?: readonly InboundMessage[];
    /** From the webhook taking the (first) message to its turn starting, in ms. */
    queueWaitMs?: number;
}
/**
 * How a message's (or a resumed run's) handling ended in this process —
 * {@link ChannelTurnEnded.outcome}:
 * - `replied`: the turn's stream ended and what it said was delivered (possibly nothing — a turn
 *   that said nothing, or whose messages `canDeliver` dropped);
 * - `failed`: the turn failed (`texts.failed` was sent), or the job itself failed after its retries
 *   (`error` is set; `onError` is told too);
 * - `timeout`: the turn ran past `timeoutMs` and was cancelled (what it said so far was delivered);
 * - `blocked`: the turn waits for a decision a text channel cannot make (blocking approvals);
 * - `parked`: the turn asked a question and waits for the answer — a later message (or the question's
 *   timeout) resumes it, and that one ends with its own call;
 * - `stopped`: no turn — the sender is unknown (`unknownSender`) or `beforeTurn` stopped it;
 * - `handled`: no turn to read — an answer, a button press, a text decision, or nothing left to answer;
 * - `interrupted`: the durable engine took the job away from this process mid-way (it goes on
 *   elsewhere, or later); this process will not deliver anything more for it.
 */
export type ChannelTurnOutcome = 'replied' | 'failed' | 'timeout' | 'blocked' | 'parked' | 'stopped' | 'handled' | 'interrupted';
/** This process is done with a message (or a resumed run) — {@link ChannelHandleOptions.onTurnEnded}. */
export interface ChannelTurnEnded {
    /** The adapter's name. */
    channel: string;
    conversation: string;
    /** Who the conversation was answered as — `null` for an unknown sender, or before it was known. */
    actor: Actor | null;
    /** The message handled — `null` when a run is resumed after its question (answered or timed out). */
    message: InboundMessage | null;
    /** The run read, when a turn was read (or resumed). */
    runId: string | null;
    threadId: string | null;
    outcome: ChannelTurnOutcome;
    /** What failed, for `outcome: 'failed'` from an error (after its retries). */
    error?: unknown;
    /**
     * Every message the turn was made of, in order — set only when `coalesce` grouped several (then
     * `message` is the first of them). A message grouped into another's turn is told about here, on
     * that turn: its own job ends without a call of its own.
     */
    coalesced?: readonly InboundMessage[];
    /** The files the turn was sent with (after refusals and `transformInbound`), when it started one. */
    attachments?: number;
    /** From the webhook taking the (first) message to its turn starting, in ms — when a turn started. */
    queueWaitMs?: number;
}
/** What {@link ChannelHandleOptions.prepareMedia} makes of a downloaded file. */
export type ChannelPreparedMedia = undefined
/** Attach this file instead (converted, resized…). */
 | {
    file: ChannelMediaFile;
}
/** Read the file as this text (a voice note's transcript): added to the message, not attached. */
 | {
    text: string;
}
/** Refuse it with `texts.mediaRefused(reason)`. */
 | {
    refuse: ChannelMediaRefusal;
};
/** The message as it goes to the agent — what {@link ChannelHandleOptions.transformInbound} edits. */
export interface ChannelInbound {
    /** The text (with prepared media's text appended). */
    text: string;
    attachments: AttachmentRef[];
    /**
     * The messages a coalesced turn is made of, each with its own text (its caption, with its prepared
     * media's text) and its own files — set only when `coalesce` grouped several. `text` is their
     * texts joined in order and `attachments` their files in order; rebuild them from `parts` to,
     * say, put each caption next to its file.
     */
    parts?: ChannelInboundPart[];
}
/** One message of a coalesced turn — see {@link ChannelInbound.parts}. */
export interface ChannelInboundPart {
    message: InboundMessage;
    text: string;
    attachments: AttachmentRef[];
}
/**
 * Group a burst of messages into one turn — see {@link ChannelHandleOptions.coalesce}.
 */
export interface ChannelCoalesceOptions {
    /**
     * How long the conversation must stay quiet after its last message before the burst becomes a
     * turn. Every message waits at least this long before it is answered.
     */
    quietMs: number;
    /**
     * The longest a burst is held, from its first message being taken, however long the person keeps
     * writing. Default: 30 s (or `quietMs`, when longer).
     */
    maxWaitMs?: number;
    /** The most messages one turn groups; the rest go to the next turn. Default 20. */
    maxMessages?: number;
    /**
     * Which messages may be grouped — e.g. only files. One it returns `false` for is handled alone
     * (and ends a burst). Default: every message but a control one (a button press, a text decision,
     * an answer to a question), which is never grouped.
     */
    when?(message: InboundMessage): boolean;
}
export interface ChannelHandleOptions {
    /**
     * The account the sender is — from `message.from` (a phone number, a Telegram id). `null` → the
     * message is not answered by the agent ({@link unknownSender} / `texts.unknownSender`). This IS
     * the authentication of every message, so map only senders the channel itself vouches for.
     */
    actor(message: InboundMessage): Actor | null | Promise<Actor | null>;
    /**
     * A sender `actor()` mapped to nobody: what to answer (an onboarding step, a "link your number"
     * link) — `null`/`undefined` → nothing. Default: `texts.unknownSender`.
     */
    unknownSender?(message: InboundMessage, context: ChannelHookContext): ChannelReply | ChannelReply[] | null | undefined | Promise<ChannelReply | ChannelReply[] | null | undefined>;
    /**
     * Before anything else for a known sender — before its answers, button presses, text decisions
     * and turns: `'continue'` (or nothing) goes on; `'stop'` ends here; `{ reply }` / `{ replies }`
     * answers and ends here. For flows the app owns: terms to accept, an account to finish, a quota.
     */
    beforeTurn?(message: InboundMessage, actor: Actor, context: ChannelHookContext): ChannelGate | Promise<ChannelGate>;
    /**
     * The thread this conversation continues; `null`/`undefined` → a new one, reported to
     * {@link onThreadCreated} so you can store it.
     */
    thread(actor: Actor, message: InboundMessage): string | null | undefined | Promise<string | null | undefined>;
    /** A new thread was created for the conversation — remember it for {@link thread}. */
    onThreadCreated?(threadId: string, actor: Actor, message: InboundMessage): void | Promise<void>;
    /** A turn started (or was queued) for a message — record where it came from, start a meter. */
    onTurnStarted?(turn: ChannelTurnStarted): void | Promise<void>;
    /**
     * This process is done handling a message, or a run resumed after its question — called ALWAYS,
     * once per job this process ran, whatever it came to ({@link ChannelTurnOutcome}): a reply, a
     * failure, a timeout, a turn that sent nothing, a sender stopped before any turn (after
     * `adapter.acknowledge`), a durable job taken away mid-way. For what a process started for the
     * message and must stop — a "typing…" presence, a meter. Per attempt: when a durable job runs
     * again (a crash, a lease taken over by another process) each process that ran it is told. Not
     * called for a question's timeout itself (the resumed run it starts is). Errors it throws are
     * logged and ignored.
     */
    onTurnEnded?(turn: ChannelTurnEnded): void | Promise<void>;
    /**
     * Checked before EVERY message the channel sends — replies, cards, questions, relayed outcomes.
     * `false` → that message is dropped (e.g. the number was unlinked from the account mid-turn).
     */
    canDeliver?(delivery: ChannelDelivery): boolean | Promise<boolean>;
    /**
     * The turn's page context. Default `{ kind: adapter.name }`. The handler adds `channel: { name,
     * conversation }` to it — what a proposal's outcome is routed back by.
     */
    pageContext?: PageContext | ((message: InboundMessage) => PageContext);
    /** The agent that answers. Default: the thread's, else the configured default. */
    agentName?: string;
    /** The agent service. Default: `AgentService` from the container. */
    service?: ChannelTurnService;
    /**
     * Where the channel's short-lived state lives: message ids already taken, what was delivered,
     * questions waiting for an answer, cards sent, outcomes relayed. Default: `lucidChannelStore()`
     * over the agent store's database when the agent store is Lucid, else this process's memory.
     */
    store?: ChannelStore;
    /**
     * Process messages as `@adonis-agora/durable` runs: persisted before the `200`, one at a time per
     * conversation, retried, and resumed after a crash (a reply never lost, never sent twice). Default:
     * on when the agent runs durably (`durable: true` in `config/agent.ts`), on that engine. `false` →
     * in this process (a restart loses what is in flight). An engine → that one.
     */
    durable?: boolean | ChannelWorkflowEngine;
    /**
     * Where a durable job's phases run as DISPATCHED steps (`agora.channel.prepare`, `.act`, `.read`,
     * …), so any process that registered the channel runs them and any process continues the job: a
     * durable `Transport` (anything with `handle(name, fn)`). Default: the transport of the app's
     * durable engine (from the container) when the channel runs on the agent's engine, or the engine's
     * own `handleStep`. `false`, or none found → each phase runs in the process that runs the
     * workflow (`ctx.localStep`).
     */
    durableSteps?: ChannelStepServer | false;
    /**
     * Group a burst of messages of one conversation into ONE turn — "oi" / "tudo bem?" / "queria saber
     * X", or ten files sent in a row: a message waits until the conversation has been quiet for
     * `quietMs` (at most `maxWaitMs`, at most `maxMessages` messages), then the burst is one turn —
     * texts joined in order, every file attached (each message's files prepared and refused as
     * alone). Control messages are never grouped: a button press, a text decision ("yes") or an
     * answer to a question is handled on its own, in order, and ends a burst. Messages that arrive
     * while a grouped turn runs go to the next turn. With a durable engine the wait is the engine's
     * durable sleep (any process resumes it); inline, a timer. Off by default.
     */
    coalesce?: ChannelCoalesceOptions;
    /** How each phase of a message is retried when it fails. */
    retry?: ChannelRetryOptions;
    /** How long a message id (and what was delivered for it) is remembered. Default 24 h. */
    dedupeTtlMs?: number;
    /** Stop waiting for a turn after this long (and cancel it). Default 5 minutes. */
    timeoutMs?: number;
    /**
     * After a proposal is approved, wait this long for it to execute and relay its outcome. `0` → do
     * not wait (`channels.onSettled` relays it once it runs). Default 60 s.
     */
    outcomeTimeoutMs?: number;
    /** How long a question waits for its answer before the agent proceeds without it. Default 30 min. */
    questionTimeoutMs?: number;
    /**
     * What the channel says on its own. Omitted parts come from {@link channelTextsFor}: Brazilian
     * Portuguese when the agent's `actionProposalText` is `ptBrActionProposalText`, else English. A
     * function picks them per message (the actor's locale) — `actor` is `null` before it is known and
     * for a relayed outcome (then `proposal` is set).
     */
    texts?: ChannelTextsOverrides | ((context: {
        actor: Actor | null;
        message: InboundMessage | null;
        proposal?: ActionProposal;
    }) => ChannelTextsOverrides | Promise<ChannelTextsOverrides>);
    /**
     * Accept "always in this conversation" in a text decision. Default `true`; `false` → it is refused
     * with `texts.rememberRefused` (every action needs its own confirmation here).
     */
    allowRemember?: boolean;
    /**
     * What the turn may draw. Default `{ components: [] }`: every component arrives as its
     * `fallbackText`. Allow some with {@link renderComponent} to send them as files. With
     * `genui({ channels })` configured for this channel ({@link ChannelAdapter.kind}, else `default`)
     * the default is the channel's own: the components it can draw, delivered natively.
     */
    uiCapabilities?: UiCapabilities;
    /**
     * The genui setup this channel draws with — default: the app's (`AgentGenui` from the container).
     * With `channels` configured for this channel, components arrive natively (their `channels`
     * conversion: text, reply buttons, lists, images; their text summary otherwise), and a pressed
     * button is the user's next turn, as a UI action. `false` → never (every component as its text).
     * With a `service` handed in, the container is not read: pass `genui` too.
     */
    genui?: ChannelGenui | false;
    /**
     * A component the turn drew: what to send for it — a file (a chart as an image), text, several —
     * or `null`/`undefined` for its `fallbackText`. Rendered components are sent when the turn ends (or
     * asks a question), before its text; a later one with the same id replaces it; a failed turn sends
     * none.
     */
    renderComponent?(component: ChannelComponent, context: {
        actor: Actor;
        conversation: string;
        runId: string;
        rendered: number;
    }): ChannelReply | ChannelReply[] | null | undefined | Promise<ChannelReply | ChannelReply[] | null | undefined>;
    /**
     * A table in the turn's reply text (GitHub markdown): what to send in its place — a file (the
     * table as an image), text, several — or `null`/`undefined` to keep it in the text, where the
     * channel's markdown turns it into a list. The text around it goes as usual: before the table's
     * replies, and after them.
     */
    renderTable?(table: MarkdownTable, context: {
        actor: Actor;
        conversation: string;
        runId: string;
    }): ChannelReply | ChannelReply[] | null | undefined | Promise<ChannelReply | ChannelReply[] | null | undefined>;
    /**
     * The attachment limits for one file — e.g. let audio through to {@link prepareMedia}, which
     * transcribes it. Default: the agent's.
     */
    mediaLimits?(limits: AttachmentLimits | null, media: InboundMedia): AttachmentLimits | null;
    /**
     * A downloaded file, before it is attached: attach another (`{ file }`), read it as text (`{ text }`
     * — a voice note's transcript), refuse it, or (nothing) attach it as is.
     */
    prepareMedia?(file: ChannelMediaFile, media: InboundMedia, context: ChannelHookContext & {
        actor: Actor;
    }): ChannelPreparedMedia | Promise<ChannelPreparedMedia>;
    /**
     * The message as it goes to the agent, last: add a note about the attachments, a default question
     * for an image without a caption… Text decisions are read from the result.
     */
    transformInbound?(inbound: ChannelInbound, context: ChannelHookContext & {
        actor: Actor;
        threadId: string | null;
    }): ChannelInbound | Promise<ChannelInbound>;
    /**
     * What an executed proposal tells the person, in order — the outcome, then follow-ups (a message
     * to forward, sent `raw` and alone). Relayed once. `null`/`undefined` → the default (`text`: the
     * proposal's own outcome text, else `texts.actionSucceeded` / `texts.actionFailed`); `[]` → nothing.
     */
    formatOutcome?(proposal: ActionProposal, context: {
        text: string;
        texts: ChannelTexts;
    }): ChannelReply[] | null | undefined | Promise<ChannelReply[] | null | undefined>;
    /** What every webhook request came to — for counters and logs, without parsing the body again. */
    onWebhook?(event: ChannelWebhookEvent): void | Promise<void>;
    /** A message whose handling failed (after its retries). Default: `console.error`. */
    onError?(error: unknown, message: InboundMessage): void;
}
/** What a channel draws generative UI with — `AgentGenui` is one. */
export interface ChannelGenui {
    catalog: Catalog;
    channels?: GenuiChannels;
    base?: GenuiChannelBase;
}
/** The route handler `channels.handle()` returns. */
export interface ChannelRouteHandler {
    (ctx: HttpContext): Promise<void>;
    /**
     * Resolves once every message this handler took has been handled — for tests and shutdown. With
     * a durable engine: once their runs ended.
     */
    drain(): Promise<void>;
    /** Handle a request built by hand (another framework, a test). */
    handleRequest(request: ChannelRequest): Promise<{
        status: number;
        body: unknown;
        contentType?: string;
    }>;
}
/** The button ids for a proposal: what {@link ChannelRouteHandler} maps back to its decision. */
export declare function proposalButtonIds(proposalId: string): {
    approve: string;
    reject: string;
};
/** Where a turn's output goes back to — `pageContext.channel`, recorded with every proposal. */
export interface ChannelAddress {
    name: string;
    conversation: string;
    /** The adapter's {@link ChannelAdapter.kind}, when it has one — what `turnChannel` reads first. */
    kind?: string;
    /**
     * How many reply buttons and list rows the adapter shows (its capabilities): what a tool result's
     * `delivered` describes choices by, the way the handler will send them.
     */
    choices?: {
        buttons: number;
        lists: number;
    };
}
/**
 * The webhook route for one channel. Each request is verified (`401` when it is not the provider's),
 * parsed, deduplicated by the provider's message id, taken (persisted, with a durable engine) and
 * acknowledged with `200`; the message is handled after the response — one at a time per
 * conversation, each phase retried — so a slow model never makes the provider retry. Then:
 *
 * - an unknown sender gets `unknownSender`; a known one goes through `beforeTurn`;
 * - an answer to a question the turn asked resumes it;
 * - a press on a proposal's button decides that proposal (`via` = the adapter's name);
 * - a text decision ("yes", "no #ID", a button label) decides a card delivered to THIS conversation
 *   — never a proposal made elsewhere (`texts.noPendingConfirmation`);
 * - media are downloaded (`prepareMedia`) and attached, or refused with a text;
 * - the message (`transformInbound`) starts a turn with the channel's capabilities;
 * - the reply is converted to the channel's markdown, split at its length limit, and sent — each
 *   message once, even when the work is retried or resumed after a crash;
 * - a proposal the turn left pending is sent with Confirm/Cancel buttons (or a text instruction);
 * - an approved proposal's outcome is relayed once it executed (see `outcomeTimeoutMs`, and
 *   `channels.onSettled` for one that runs later).
 *
 * Wants `actionApprovalMode: 'independent'`: a blocking approval holds the turn open until someone
 * decides it in the app, so the channel sends what it has and `texts.blockingApproval`.
 *
 * Registers the adapter under its `name` for `channels.onSettled` and the durable jobs — names must
 * be unique, and a process that resumes durable channel jobs must create the same handlers at boot.
 */
export declare function handleChannel(adapter: ChannelAdapter, options: ChannelHandleOptions): ChannelRouteHandler;
/**
 * `actionProposalWorker.onSettled` for text channels: relays an executed proposal's outcome to the
 * conversation it was proposed in, through the adapter registered under the recorded channel name
 * (with its `formatOutcome`, `texts` and `canDeliver`). Proposals from other surfaces are ignored.
 * Safe next to the handler's own wait: an outcome is relayed once.
 *
 * ```ts
 * // config/agent.ts
 * actionProposalWorker: { onSettled: channels.onSettled }
 * ```
 */
export declare function relayChannelOutcome(proposal: ActionProposal): Promise<void>;
export declare const channels: {
    /** See {@link handleChannel}. */
    handle: typeof handleChannel;
    /** See {@link relayChannelOutcome}. */
    onSettled: typeof relayChannelOutcome;
    /** Relay one proposal's outcome now; `true` when it was sent. */
    deliverOutcome: (proposal: ActionProposal, service?: ChannelTurnService) => Promise<boolean>;
    /** The adapter registered under `name` (by `channels.handle`), if any. */
    adapter: (name: string) => ChannelAdapter | undefined;
    /**
     * Deliver a run started outside the channel (a turn continued on the server, e.g. after a sign-in
     * completed elsewhere) to the conversation `address` names (`pageContext.channel`). `false` when
     * no channel of that name is registered in this process.
     */
    follow: (address: unknown, run: {
        runId: string;
        threadId: string;
        actor: Actor;
    }) => Promise<boolean>;
};
//# sourceMappingURL=handler.d.ts.map