/** * ONE inbound text → ONE harness turn → the texts it writes back, each sent as * it finishes rather than all at once (`streamTexts`). * * It does NOT go through the away runner: an away run hardcodes * `presence: "away"` (agents/src/away.ts), which is exactly wrong here — there * IS a person on the other end, holding their phone, and the whole point of the * approval bridge below is that they can answer. So the ctx is built locally: * `venue: "chat"`, `presence: "present"`, the subject from the link, and the * delivery's `eventId` as the conversation the guard scopes its cards by. */ import { type AutomationsEngine } from "./automations/index.js"; import { type Membership, type Principal } from "./core/index.js"; import type { VendoGuard } from "./guard/index.js"; import type { ChannelAskRepository, ChannelLink, ChannelLinkRepository } from "./channel-links.js"; import type { ChannelsService, InboundTextEvent } from "./channels.js"; import { type HarnessTurns } from "./harness-turn.js"; /** Texting humans reply on a human clock — they put the phone down, they drive, * they come back. The web's 90s wait is a closed-tab bound and would time out * every real approval here. * * WHAT THIS REQUIRES OF A HOST: the parked call is resumed by the instance that * parked it. The guard's decision callbacks are in-process (`guard.ts` * `#approvalCallbacks`) and the waiter is an in-process promise * (`turn-tools.ts`), so a "YES" delivered to a DIFFERENT instance decides the * approval record without waking the turn that is holding the call — the answer * is understood and recorded, and the effect still does not land. So * approve-by-text needs a deployment that keeps one long-lived process for the * ten minutes: a container host (Railway, Render, Fly), not a function that is * billed by the second and killed well inside the window. Making it survive a * restart or a second replica is resumable turns — a durable job that re-enters * the tool call once the record is decided — which is an architecture, not a * patch, and is deliberately NOT in this change. */ export declare const CHANNEL_APPROVAL_WAIT_MS = 600000; /** How a text READS, stated once. Shared with the Text me tool's descriptor * (text-me.ts): a text the agent sends from a web turn or an away firing is * still a text, and two copies of this sentence would drift. */ export declare const PLAIN_TEXT_RULE: string; export interface ChannelTurnDeps { /** `warm` is optional for the same reason the web's warm door is * (`wire/threads.ts`): an engine assembled through `createAgent` has none, * and an unwarmed turn is slower, never broken. */ harness: Pick & Partial>; guard: VendoGuard; channel: ChannelsService; links: ChannelLinkRepository; /** Which cards actually went out over this channel — see * `ChannelAskRepository`, and why it is in the store and not in memory. */ asks: ChannelAskRepository; /** Read-only, and only to NAME an automation whose grant set is being asked * about: the asks themselves are read off the guard's pending feed. */ automations: Pick; /** Build contract §9.1 — the host's orgs for the LINKED subject. The seam is * keyed on the principal rather than the request precisely so a session-less * path can ask it, and a texted turn must: without it a member who texts is in * none of their org's pools, so their messages and builds neither count against * the org's allowance nor accrue to it. */ memberships?: (principal: Principal) => Promise; } /** The common cron shapes an agent actually mints, in words — anything else * stays raw. Used beside the raw expression, never instead of it: the ask is * the consent boundary, so the verbatim value always shows. */ export declare function cronProse(cron: string): string | undefined; /** * The reply the model did NOT split, cut into bubble-sized texts. * * Measured across Yousef's own texted turns on 0.32.0: the divider teaching * (TEXT_STYLE) engaged on ONE turn in four. Three times out of four a six-account * listing landed as a wall of text, which is the product a person actually got. * The teaching stays and stays first — a split the model chooses knows what it is * saying and this does not — so this runs only when the model split nothing at * all. * * It never cuts inside a sentence. The boundaries are a blank line, then a line * end, then a sentence end; a reply with none of them (one long unbroken clause) * comes back whole, because every cut available in it would land mid-thought and * a bubble that stops mid-thought reads worse than the wall it replaced. * * Exported for its own test, the same reason `cronProse` is. */ export declare function bubbles(text: string): string[]; /** * A bare YES/NO answering a card THIS conversation raised is not a turn at all: * it is the answer to that card, decided on the SAME approval record the * waiting turn is blocked on — so that turn resumes and delivers its own reply. * * It is decided BEFORE the per-conversation queue (compose-channels.ts), and * that ordering is load-bearing rather than tidy: the turn this answer releases * is the one holding the queue, so queueing the answer behind it would deadlock * the pair for the full ten-minute approval wait and approve-by-text would * simply stop working. * * Answers whether the text was consumed as an answer. A YES that matches no * card this conversation raised is NOT one — it falls through and runs as an * ordinary turn. */ export declare function answerPendingCard(deps: Pick, input: { event: InboundTextEvent; link: ChannelLink; }): Promise; /** * Run one inbound text as the linked user. */ export declare function runChannelTurn(deps: ChannelTurnDeps, input: { event: InboundTextEvent; link: ChannelLink; }): Promise;