import { SourceRef, SourceManifestEntry, SourceDelivery, SourceRefOptions, SourceManifestSegment, SourceManifest, SourceSet, SourceManifestForm, ResolvedSourceSet } from '../index.js'; /** * The Source input's runtime types — what a list of picked Sources IS while a * person is still choosing, on top of the frozen v1 wire contract * (`@ai-matrx/agents/sources`), which is never re-declared here. * * UI-free: no React, no framework, no store library. Every client (the web * app, the browser extension, the desktop app, mobile) holds this same shape * in its own store through a `SourceSetStore` adapter. * * Contract of record: common-docs `projects/unified-source-input/DESIGN.md` * § "One core, many screens". */ /** Every tile a host can allow: the Add new doors, plus "existing" (Use existing + search). */ type SourceTileId = "upload" | "paste" | "web" | "youtube" | "audio" | "image" | "topic" | "existing"; /** * What a picked Source is while the person chooses: the tile it came through, * or — for something they already had — "files", "notes", "your_sources" * (a Source screen) or "records" (any other registry kind). */ type SourceKindId = SourceTileId | "your_sources" | "files" | "notes" | "records"; /** The input behind a Source that is still landing — enough to land it again. */ interface SourceIntakeInput { /** Pasted text (paste). */ text?: string | undefined; /** The name the person typed for pasted text. */ name?: string | undefined; /** A web page or YouTube link (web, youtube). */ url?: string | undefined; /** A recording that already finished uploading (audio) — transcribe from here. */ fileId?: string | undefined; } /** * One picked Source while the person is still choosing. JSON only — it is * persisted as a draft so a refresh never loses a pick. * * `ref` is null only while a NEW Source is still landing (reading a page, * transcribing, uploading); it becomes the pointer the moment the door * answers. Nothing is ever sent to the server as a blob. */ interface SourceDraft { kind: SourceKindId; /** What the person sees: a file name, a page title, the first line of a paste. */ label: string; ref: SourceRef | null; /** Where it came from, in plain words (a web address, "Pasted text"). */ origin?: string | undefined; /** The Source screen id when known (a landed Source) — every card opens. */ processedDocumentId?: string | undefined; /** * How a reused Source was captured (`processed_documents.source_kind`), so * the card and the review say "Transcript" or "Web page" — never "Document". */ sourceKind?: string | undefined; /** The stored file behind it, when it is one (the form chooser reads its family). */ fileId?: string | undefined; /** Every stand-in announces itself: a reader fallback, a door notice. */ notes?: string[] | undefined; /** The person asked to wait for the clean version before anything runs. */ waitForClean?: boolean | undefined; /** * Where the picked parts sit in the Source (label + pages), remembered when * they are picked. A Source still being cleaned lists its raw pages; when the * clean version lands its parts have NEW ids, and the pick is carried over by * page from here (verify-3: "0 characters · 4 of 21 parts"). Persisted with * the draft, so it survives a reload. */ pickedParts?: SourcePickedPart[] | undefined; /** * What the person handed over, kept ONLY while the Source is still landing * (or failed) so a reload or a failure never loses it: the pasted text, the * link, or an already-uploaded recording's file. Never file bytes. Cleared * the moment the Source settles. */ input?: SourceIntakeInput | undefined; } /** One picked part as the person saw it: its id, its label, and the pages it covers. */ interface SourcePickedPart { id: string; label: string; /** First and last page it covers, when the Source has pages. */ pages?: [number, number] | undefined; } type SourceCardStatus = "pending" | "resolving" | "ready" | "error"; /** One card: the draft, its lifecycle, and what the server measured. */ interface SourceCardModel { id: string; draft: SourceDraft; status: SourceCardStatus; /** A sentence with its remedy — never a code. */ error: string | null; /** The server's measurement (sizes, forms, parts, state). Null until read. */ manifest: SourceManifestEntry | null; } /** The entity the Sources are being used for — passed to the door as `attach_to`. */ interface SourceAttachTo { entityType: string; entityId: string; label?: string | undefined; } /** The whole state of one Source input: its cards (in the order picked) and the topic. */ interface SourceSetState { cards: readonly SourceCardModel[]; topic: string; } /** What a draft store keeps for a reload: the cards (without measurements) and the topic. */ interface PersistedSourceCard { id: string; draft: SourceDraft; status: SourceCardStatus; error: string | null; } interface PersistedSourceInput { sources: PersistedSourceCard[]; topic: string; } /** * How the AI gets a Source — THE one source of truth for delivery. * * The frozen contract (`SourceRef.delivery`, `@ai-matrx/agents/sources`) has * two values and a default: absent / "direct" = the text is handed to the AI * with the request; "context" = nothing is sent up front and the AI opens the * Source while it works. The server (aidream `source_resolution.py`) reads * exactly this field and nothing else — `promote` / `exclude` are NOT read on * the Source path. * * Every screen that shows or changes delivery (the Source card, "Review what * goes in", the planner) reads it through `sourceDelivery` and says it with * `DELIVERY_WORDS`, so a card can never say one thing while the review and the * request say another (V1-A: the card said "Nothing is copied in" — chat's * attachment editor's wording — while the review said "Include the text" and * the request carried the text). */ /** The delivery a pointer asks for — absent means the contract default, "direct". */ declare function sourceDelivery(ref: Pick | null | undefined): SourceDelivery; /** * The patch that sets a delivery. "direct" is the default, so it clears the * field (the wire stays minimal, exactly as the review has always written it). * Looking a Source up sends no text, so parts and a size limit no longer apply. */ declare function deliveryPatch(delivery: SourceDelivery): Pick; interface DeliveryWords { /** The choice's name on a control. */ label: string; /** Two or three words under the control: what happens (copy law R9). */ hint: string; /** The short phrase a summary line uses. */ summary: string; } declare const DELIVERY_WORDS: Record; /** The choices, in the order every control shows them. */ declare const DELIVERY_CHOICES: ReadonlyArray<{ value: SourceDelivery; } & DeliveryWords>; /** * What a HOST can use. Not every host can take every delivery: a generator * that needs the text up front (flashcards' segmented generator reads the * resolved text and nothing else) gets NOTHING from a Source set to "look it * up" — V2 verifier shots 06/26: the request went out with `delivery: * "context"` and the page said "None of the Sources had any text", which was * false. So a host declares the deliveries it can use (`SourceInputProps. * deliveries`); the card and the review offer only those, and a Source already * set to something the host cannot use is switched back — with a note on it. * Omitted = both (a host whose AI can open Sources while it works). */ declare function allowedDeliveries(deliveries: readonly SourceDelivery[] | undefined): readonly SourceDelivery[]; /** The choices one host offers, in the order every control shows them. */ declare function deliveryChoicesFor(deliveries: readonly SourceDelivery[] | undefined): ReadonlyArray<{ value: SourceDelivery; } & DeliveryWords>; /** The sentence a switched-back Source carries — every intervention announces itself. */ declare function deliverySwitchedNote(to: SourceDelivery): string; /** * Fit a pointer to what the host can use. Returns the patch to apply (null * when it already fits) and the delivery it lands on. */ declare function fitDelivery(ref: Pick | null | undefined, deliveries: readonly SourceDelivery[] | undefined): { patch: ReturnType; to: SourceDelivery; } | null; /** * Finding a part of a Source — THE one matcher, used by the Source card's * "Choose parts" and by "Review what goes in". * * A query is one of: * - a page number ("12") — the part that covers that page; * - a page range ("3-10", "3–10", "3 to 10") — every part inside it; * - words — every word must appear in the part's label or its preview (the * opening words the manifest carries, contract amendment A5), matched here; * or the part is one the server found holding every word in its full text * (`POST /sources/parts/search`, via `useSourcePartsSearch`). * * A part spanning several pages ("Pages 22–24") covers each of them: the * manifest's numeric `page` is its first page and the label names the span. * * V1-A: the search promised "words" but matched only labels, so * "photosynthesis" found nothing in a biology course. */ /** A manifest part, with the A5 preview (optional on the wire). */ type SourcePart = SourceManifestSegment & { preview?: string; }; /** The ids of parts the server found holding every word (`useSourcePartsSearch`). */ type PartMatchIds = ReadonlySet; /** The pages a part covers: its label's span ("Pages 22–24") or its one page. */ declare function partPages(part: SourcePart): [number, number] | null; /** True when a query is words (not a page or a range) — only then is the text searched. */ declare function isWordQuery(query: string): boolean; declare function matchesPart(part: SourcePart, query: string, serverMatches?: PartMatchIds): boolean; /** The parts a query shows, in the Source's own order. */ declare function findParts(parts: readonly T[], query: string, serverMatches?: PartMatchIds): T[]; /** * What a Source that was still landing becomes when the page reloads. * * Never lose input (table stakes): the pasted text, the link, or an * already-uploaded recording is kept in the draft (`SourceDraft.input`), so * the input picks it up again by itself — the landing door dedupes by content * hash, so landing it a second time reuses the same Source, never a copy. * A file that was mid-upload cannot come back (its bytes lived only in the * browser tab); the card says so honestly and keeps the file's name so the * person can choose it again in one click. */ /** Shown while a kept input is being landed again after a reload. */ declare const RELOADED_RESUMING = "The page reloaded while this was being added \u2014 picking it up again."; /** A kept input exists but has not been re-landed (it will be, or the person can press Try again). */ declare const RELOADED_WHILE_ADDING = "This was still being added when the page reloaded, so it did not finish. Add it again."; /** The kinds whose landing can be repeated from what the draft kept. */ declare function resumableInput(draft: SourceDraft): SourceIntakeInput | null; /** The sentence for a file whose upload the reload cut off. */ declare function uploadCutOffSentence(draft: SourceDraft): string; interface ReloadedCard { /** "resume" = land the kept input again now; "reoffer" = the person must re-choose a file. */ action: "resume" | "reoffer" | "lost"; sentence: string; } /** * The one decision: a card that was `pending`/`resolving` when the page * unloaded. Cards in any other state are restored unchanged (returns null). */ declare function reloadedCard(draft: SourceDraft, status: "pending" | "resolving" | "ready" | "error"): ReloadedCard | null; /** * The knob for the largest pasted text kept in the device draft (it would * crowd the draft store). Read by the intake; while unread nothing is kept and * the card says so. */ declare const MAX_KEPT_TEXT_KNOB: { readonly feature: "sources"; readonly key: "max_kept_draft_chars"; }; /** Said on a pasted card whose text is too large (or the limit unreadable) to keep for a reload. */ declare const NOT_KEPT_FOR_RELOAD = "Too large to restore if the page reloads before it is added."; /** * A landing stopped only because no organization is chosen yet. The card * waits; the moment one is set the intake lands every waiting card again from * what its draft kept (state, never an in-memory queue — it survives a reload). */ declare const WAITING_FOR_ORGANIZATION = "Waiting for an organization \u2014 choose one and this continues by itself."; /** A read Source whose keep waits for an organization; kept again once one is set. */ declare const KEEP_WAITING_FOR_ORGANIZATION = "Read. Waiting for an organization to keep it \u2014 choose one and it is kept by itself."; /** * A card's note while the organization picker is OPEN for its landing: the card * is waiting for the person, not reading. Removed the moment the picker settles. */ declare const ASKING_FOR_ORGANIZATION = "Choose an organization to continue."; /** * fileSource — how a stored file on a Source card becomes its Source, decided * from the SERVER's state alone (USI-3e). Pure: the one read * (`/files/{id}/rag-status`) and the file's own organization are INJECTED by * the host (`SourceFileState` below), never imported. * * The server owns reading a file: every upload's finalize already starts the * one processing run (the file adapters → a `processed_documents` row), and * `/files/{id}/rag-status` merges that run's lifecycle row with the Source it * made. So a card never starts its own run while one is live, a reload simply * reads the state again (re-attach, never a second job), and a file someone * already had ("use the one you already have", or picked from Files) is kept * the moment its existing Source is seen. */ /** The part of the server's `/files/{id}/rag-status` answer the decision reads. */ interface FileSourceStatus { state: string; processed_document_id?: string | null | undefined; error?: { message?: string | null | undefined; [detail: string]: unknown; } | null | undefined; } type FileSourceStep = /** The Source exists: keep it and file it against the thing being made. */ { do: "keep"; processedDocumentId: string; } /** A run is reading it now (server-side or this tab's own). Look again soon. */ | { do: "wait"; } /** Nothing is reading it and it has no Source yet: start the one run. */ | { do: "start"; } /** It cannot become a Source; say why, and stop looking. */ | { do: "unreadable"; reason: string; }; interface FileSourceContext { /** This card already started a run in this page's life. */ started: boolean; /** This tab's processing runner has a live run for the file. */ localRunning: boolean; } /** The one decision. `status` is the server's `/files/{id}/rag-status` answer. */ declare function nextFileStep(status: FileSourceStatus | null, ctx: FileSourceContext): FileSourceStep; /** How long to wait before looking again: quick at first, then spaced out. */ declare function fileSourcePollDelayMs(round: number): number; /** A stored file on a card whose Source is not known yet — the recovery reads its state. */ declare function isWaitingFileCard(card: SourceCardModel): boolean; /** * True while a waiting file card cannot be asked about yet: `/files/{id}/ * rag-status` answers only with an organization (the file's own, when the * host knows it, or the one the person picked). Without one every read was a * refused 400 — every ~3 s, piling up as page errors (V2-F #3). So the card * is HELD: it says it is waiting for an organization, offers the one picker, * and nothing is asked until one is known. * * `fileOrganizationId` is the host's lookup of a file's own organization * (omitted = the host does not know it). */ declare function fileCardHeldForOrganization(card: SourceCardModel, activeOrganizationId: string | null | undefined, fileOrganizationId?: (fileId: string) => string | null | undefined): boolean; /** * The token estimate the planner uses by default — the SAME measured ratios as * the web app's one estimator (matrx-frontend `lib/tokens/estimate.ts`) and the * server's `CHARS_PER_TOKEN_MEASURED` (aidream, 2026-09-28): 2.9 characters per * token for prose with markup, 2.4 for structured payloads. Measured, not * assumed — the textbook 4.0 under-counted a real run by 23%. * * A host that owns its own estimator passes it to `planSourceReview` * (`PlanInput.estimateTokens`) so a preview and a run never disagree. */ declare const SOURCE_CHARS_PER_TOKEN: { readonly prose: 2.9; readonly structured: 2.4; }; /** Estimated tokens for `chars` characters of prose. */ declare function estimateSourceTokens(chars: number): number; /** How close to the window counts as "Getting heavy" (the champion's 70%). */ declare const HEAVY_SHARE = 0.7; type SourcePlanStatus = /** Its text goes in. */ "included" /** The AI looks it up on demand — no text is sent up front. */ | "on_demand" /** Does not fit in what is left of the window — removed from the result. */ | "left_out" /** Cannot be used at all (no access, missing, failed). Kept so the host can show it. */ | "unusable"; interface SourcePlanEntry { index: number; ref: SourceRef; entry: SourceManifestEntry; status: SourcePlanStatus; /** Label of the form in use. */ formLabel: string; /** Characters of the chosen form before parts/cap. */ formChars: number; /** * THE one size of this Source — the same number its card and the header show * (`sourceEntryChars`: the chosen form or picked parts, capped). What a person * sees; `sentChars` is only the budget's exact wire length. */ chars: number; /** Characters the model receives for this Source (grounding headers included) — the budget's measure. */ sentChars: number; sentTokens: number; /** True when parts are known, so `sentChars` is exact; false = the text body only. */ exact: boolean; /** Number of parts that go in, when parts are known. */ partsSent: number | null; partsTotal: number | null; /** The per-Source cap cut something. */ capped: boolean; } type BudgetVerdict = "empty" | "fine" | "heavy" | "too_much"; interface SourcePlan { entries: SourcePlanEntry[]; /** THE one size of what goes in (sum of `chars` of the Sources whose text goes in) — shown to the person. */ chars: number; /** The exact wire length the budget is judged on (grounding headers included). */ sentChars: number; sentTokens: number; /** Window the verdict is judged against. */ windowTokens: number; /** True when the window is the labeled default, not the model's own. */ windowIsFallback: boolean; verdict: BudgetVerdict; /** Share of the window used, 0..∞. */ share: number; leftOut: SourcePlanEntry[]; /** Exactly what the review returns. */ sourceSet: SourceSet; } /** Length of one rendered grounding block: `### Chunk [ (page N)]\n`. */ declare function renderedPartChars(segment: { id: string; page?: number | undefined; }, chars: number): number; declare function chosenForm(entry: SourceManifestEntry, ref: SourceRef): SourceManifestForm | null; interface PlanInput { manifest: SourceManifest; /** The person's current choices, index-aligned with `manifest.sources`. */ refs: SourceRef[]; /** The original set — carries topic / grounding / retrieve_query. */ base: SourceSet; /** The model's window, or null when unknown. */ modelWindowTokens: number | null; /** The labeled default used when the model is unknown. */ fallbackWindowTokens: number; targetModelId?: string | undefined; /** The host's one token estimator (default: the measured `estimateSourceTokens`). */ estimateTokens?: ((chars: number) => number) | undefined; } declare function planSourceReview(input: PlanInput): SourcePlan; /** * THE one client for the Source doors on the server (aidream * `api/routers/source_sets.py`): * * - `POST /sources/manifest` — sizes, states, forms and parts (never bodies); * - `POST /sources/resolve` — the full grounded text a generator reads; * - `POST /sources/parts/search` — the ids of one Source's parts that hold * every word of a query (the manifest's own part ids; never a body). * * All three are READS decided by access, never by the selected organization: * the server names them in `BODY_CARRIED_READS` (aidream `api/read_by_access.py`), * so every call goes out marked `bodyCarriedRead` — with an organization it is * still named, with none it is sent naming none instead of being refused on * the client (V1-A: "Select an organization before sending this request" was a * dead end the server never asked for). * * The TRANSPORT is injected: auth, base URL and error shape belong to the * host. A web app binds its typed API client; any other client can use * `createFetchSourcesTransport` below. */ /** Options every Source door call carries. */ interface SourceCallOptions { organizationId?: string | undefined; signal?: AbortSignal | undefined; } /** What the transport receives: the call options plus the read marker. */ interface SourceTransportOptions extends SourceCallOptions { /** Always true for the Source doors — send without requiring an organization. */ bodyCarriedRead: true; } /** The server's answer to a part search. */ interface SourcePartsSearchResult { segment_ids?: string[] | null | undefined; truncated?: boolean | null | undefined; /** Set when the Source cannot be searched ("no_access", …). */ unavailable?: string | null | undefined; /** What happened and what to do, in the server's words. */ detail?: string | null | undefined; } /** The three door paths, typed by body and answer. */ interface SourceDoorCalls { "/sources/manifest": { body: { source_set: SourceSet; }; answer: SourceManifest; }; "/sources/resolve": { body: { source_set: SourceSet; }; answer: ResolvedSourceSet; }; "/sources/parts/search": { body: { source_ref: SourceRef; query: string; }; answer: SourcePartsSearchResult; }; } type SourceDoorPath = keyof SourceDoorCalls; /** The host's way to POST to the server (auth + base URL + error shape are its own). */ interface SourcesTransport { post

(path: P, body: SourceDoorCalls[P]["body"], options: SourceTransportOptions): Promise; } interface SourcesClient { manifest(sourceSet: SourceSet, options?: SourceCallOptions): Promise; resolve(sourceSet: SourceSet, options?: SourceCallOptions): Promise; /** The ids of `ref`'s parts whose label or text holds every word of `query`. */ searchParts(ref: SourceRef, query: string, options?: SourceCallOptions): Promise; } declare function createSourcesClient(transport: SourcesTransport): SourcesClient; /** Thrown by the fetch transport for a non-2xx answer — carries the server's words. */ declare class SourceDoorError extends Error { readonly status: number; readonly body: unknown; constructor(status: number, message: string, body: unknown); } interface FetchSourcesTransportOptions { /** The server's base URL ("https://server.app.matrxserver.com"). */ baseUrl: string; /** Headers for each call — the host's auth (a bearer token) goes here. */ headers?: (() => Record | Promise>) | undefined; /** The header that names the organization (default "X-Organization-Id"). */ organizationHeader?: string | undefined; fetch?: typeof fetch | undefined; } /** A plain `fetch` transport for clients without their own API layer. */ declare function createFetchSourcesTransport(options: FetchSourcesTransportOptions): SourcesTransport; /** * The source-list state machine — pure reducers and selectors over * `SourceSetState`. Every function returns a NEW state (or the same one when * nothing changed) and never touches a store, a network or a clock; the * controller (`controller.ts`) applies them through the host's store adapter. * * The rules here are the ones the web app's Source input has always kept: * - landing the same Source twice keeps ONE card and says so on it; * - a settled card drops the input it kept for a reload; * - a card cut off mid-landing by a reload is restored as an error the * input resolves (`interrupted.ts`), never a spinner forever; * - a new Source starts on the host's default form. */ declare const EMPTY_SOURCE_SET_STATE: SourceSetState; /** Said when a Source's parts were re-read (the clean version landed) and the pick was carried over by page. */ declare const PARTS_MATCHED_BY_PAGE = "The clean version is ready \u2014 your parts were matched to it by page."; /** Said when a pick could not be carried over to the Source's new parts: the whole Source goes in until parts are chosen again. */ declare const PARTS_NOT_MATCHED = "The clean version is ready, but your parts could not be matched to it \u2014 choose parts again. Until then the whole Source goes in."; /** Said on the one card kept when the same Source lands twice. */ declare const SAME_SOURCE_AGAIN = "You added this again \u2014 it is the same Source, so it is listed once."; declare function isSourceDraft(value: unknown): value is SourceDraft; declare function sameSourceRef(a: Pick, b: Pick): boolean; /** ":" — how a Source is keyed across screens. */ declare function sourceKey(ref: { resource_type: string; resource_id: string; }): string; /** The draft kind a registry token is picked as. */ declare function draftKindForToken(token: string): SourceDraft["kind"]; /** A new pointer starts on the host's default form (never over one already chosen). */ declare function withDefaultForm(ref: SourceRef, defaultForm: string | undefined): SourceRef; /** Add a Source that is already a pointer (a stored record, a landed Source). */ declare function addReadyCard(state: SourceSetState, id: string, draft: SourceDraft, options?: { defaultForm?: string | undefined; }): SourceSetState; /** Add a Source that is still landing; finish it with `settleCard` or `failCard`. */ declare function addPendingCard(state: SourceSetState, id: string, draft: Omit): SourceSetState; /** * A landing answered. The door dedupes by content: landing the same thing * twice returns the Source already picked — keep ONE card and say so on it. */ declare function settleCard(state: SourceSetState, id: string, patch: Partial & { ref: SourceRef; }, options?: { defaultForm?: string | undefined; }): SourceSetState; /** * A card that failed has ONE state — failed. Its last measurement goes, so it * can never also say "Still being read" (verify-3: a stored PDF showed both). */ declare function failCard(state: SourceSetState, id: string, sentence: string): SourceSetState; /** Back to landing (after a reload or a failure). */ declare function restartCard(state: SourceSetState, id: string): SourceSetState; /** Change what a card says or keeps (label, input, fileId, notes, processedDocumentId). */ declare function updateCardDraft(state: SourceSetState, id: string, patch: Partial>): SourceSetState; declare function removeCard(state: SourceSetState, id: string): SourceSetState; /** Change the pointer's choices (form, parts, cap, delivery). */ declare function updateCardRef(state: SourceSetState, id: string, options: SourceRefOptions): SourceSetState; /** * Remember where the picked parts sit (label + pages) from the card's current * measurement, so a pick survives the Source being re-split (raw pages → clean * parts). No parts picked = nothing remembered. */ declare function rememberPickedParts(card: SourceCardModel): SourceCardModel; /** * A fresh measurement listed NEW parts (the clean version landed, so the raw * pages the person picked no longer exist by id). Carry the pick over by page: * every new part that covers a page the old pick covered goes in. When the old * pick's pages are unknown, the pick is cleared and the card says so — never * "0 characters · 4 of 21 parts" with nothing ticked (verify-3, 11d/11e). */ declare function carryPickedParts(card: SourceCardModel, entry: SourceManifestEntry): SourceCardModel; declare function setCardWaitForClean(state: SourceSetState, id: string, wait: boolean): SourceSetState; declare function setSourceTopic(state: SourceSetState, topic: string): SourceSetState; /** * What the person removed in "Review what goes in": the Sources that went INTO * the review (`reviewed`) and did not come back in its answer — keyed by * `sourceKey`. A Source that landed while the review was open is never in * `reviewed`, so it is never removed by it (verify-4 V4-F #3). */ declare function removedInReview(reviewed: SourceSet, answer: SourceSet): Set; /** * Apply a set the review returned (forms, parts, caps, delivery) — and its * topic. Pass `reviewed` (the set that was sent INTO the review) and every * Source removed there leaves the list, bound by its own card id. */ declare function applyReviewedSourceSet(state: SourceSetState, set: SourceSet, reviewed?: SourceSet): SourceSetState; /** The server measured the ready Sources: each ready card takes its entry. */ declare function applyManifest(state: SourceSetState, manifest: SourceManifest): SourceSetState; /** * Only what the host can use (V2-F #1): a Source set to a delivery this surface * cannot use (picked elsewhere, restored from a draft, handed in by a link) is * switched back, and its card says so. Never a choice that cannot work. */ declare function fitCardDeliveries(state: SourceSetState, deliveries: readonly SourceDelivery[] | undefined): SourceSetState; /** Read whatever a draft store handed back; anything malformed is dropped. */ declare function readPersistedSourceInput(data: unknown): PersistedSourceInput; declare function toPersistedSourceInput(state: SourceSetState): PersistedSourceInput; /** * The cards a reload brings back. A card cut off mid-landing becomes an error * the input resolves (it re-lands the kept input, or asks for the file again) * — never a spinner forever. A landing held for an organization lost its hold * with the page: it is picked up again like one cut off mid-landing. */ declare function restoredCards(persisted: PersistedSourceInput): SourceCardModel[]; declare function readySourceRefs(state: SourceSetState): SourceRef[]; declare function hasSourceRef(state: SourceSetState, resourceType: string, resourceId: string): boolean; /** The frozen v1 payload, built from the ready Sources. */ declare function selectSourceSet(state: SourceSetState, options?: { targetModelId?: string | undefined; }): SourceSet; /** * THE one size rule (the contract's `totalChars`): the chosen form, or the * picked parts, capped — measured against each card's CURRENT pointer. */ declare function selectTotalChars(state: SourceSetState): number; /** * THE one size of one Source (verify-3: one Source read 109k, 115k, 108k and * 106k on four screens): the contract's rule — the chosen form, or the picked * parts, capped by `max_chars` — measured against the card's CURRENT pointer. * The card, the header total (`selectTotalChars`) and the review (`SourcePlanEntry.chars`) * all read this. Null when it is not measured or cannot go in. */ declare function sourceEntryChars(entry: SourceManifestEntry, ref: SourceRef): number | null; declare function sourceCardChars(card: Pick): number | null; /** * THE one state of one Source card, derived once: its own lifecycle wins * (adding, waiting for an organization, failed); only a card that landed takes * the server's measured state. Never two states at once. */ type SourceCardState = "adding" | "waiting_for_organization" | "failed" | "unmeasured" | SourceManifestEntry["state"]; declare function sourceCardState(card: Pick & { draft?: Pick; }): SourceCardState; /** The server's measured state to show on a card — only for a card that landed (null otherwise). */ declare function sourceCardMeasuredState(card: Pick): SourceManifestEntry["state"] | null; /** Every Source finished landing (nothing pending) and none failed. */ declare function selectSettled(state: SourceSetState): boolean; /** How many more Sources fit under the host's max (Infinity when there is none). */ declare function sourceRoomLeft(state: SourceSetState, max: number | undefined): number; /** * Every generator names a Source the way the person saw it on its card — the * file name, the name they gave pasted text — never the stored document's own * title (V2-F #5). The server's label stays only for a Source the input does * not hold. */ declare function withDisplayNames(resolved: ResolvedSourceSet, cards: readonly Pick[]): ResolvedSourceSet; /** * The Source-set controller — the imperative face of the state machine, with * every outside dependency injected through one adapter: * * - `store` where the cards live (a Redux slice, a Zustand store, the * in-memory store below — the host's choice); * - `persistence` the draft store a reload reads back (optional); * - `client` the Source doors (`createSourcesClient(transport)`); * - `config` the host's choices: default form, organization, the * deliveries it can use, the most Sources it takes. * * Every screen (the Studio grid, a compact strip, chat's "+", the review) * drives the SAME controller; `@ai-matrx/agents/sources/react` binds it to * React with `useSourceSet(adapter)`. */ /** Where the cards live. `getState` must return the SAME object until something changes. */ interface SourceSetStore { getState(): SourceSetState; setState(next: SourceSetState): void; subscribe(listener: () => void): () => void; /** Called once when a screen mounts (a store that registers entries lazily). */ init?: (() => void) | undefined; } /** The draft store a reload reads back. `load` returns the raw saved value (stable until it changes). */ interface SourceDraftStorage { load(): unknown; save(value: PersistedSourceInput): void; subscribe?: ((listener: () => void) => () => void) | undefined; } interface SourceSetConfig { /** The form each new Source starts on ("clean" | "raw"). Omitted = the server's choice. */ defaultForm?: string | undefined; /** The organization named on the door calls (they answer without one too). */ organizationId?: string | null | undefined; /** How the AI may get the Sources on this surface. Omitted = both. */ deliveries?: readonly SourceDelivery[] | undefined; /** Most Sources the person may pick. Omitted = no limit. */ max?: number | undefined; } interface SourceSetAdapter { store: SourceSetStore; persistence?: SourceDraftStorage | undefined; /** Needed for `manifest()` and `resolve()`. */ client?: SourcesClient | undefined; config?: SourceSetConfig | undefined; /** A new card's id (default: a random UUID). */ createId?: (() => string) | undefined; /** A failed call as one sentence with its remedy (default: the error's message). */ describeError?: ((error: unknown) => string) | undefined; } /** The measurement's own state (not persisted, not shared across controllers). */ interface SourceSetMeta { measuring: boolean; /** Why the measurement failed, with its remedy. */ manifestError: string | null; } /** The mutations every intake and screen drives. */ interface SourceSetActions { /** Add a Source that is already a pointer (a stored record, a landed Source). */ addReady(draft: SourceDraft): string; /** Add a Source that is still landing; finish it with `settle` or `fail`. */ addPending(draft: Omit): string; settle(id: string, patch: Partial & { ref: SourceRef; }): void; fail(id: string, sentence: string): void; /** Back to landing. False when the card is gone (removed) — nothing to land. */ restart(id: string): boolean; /** Change what a card that is still landing says or keeps (label, input, fileId, notes). */ updateDraft(id: string, patch: Partial>): void; /** Mark a landing card as waiting for the person to choose an organization (or clear it). */ setAsking(id: string, asking: boolean): void; remove(id: string): void; /** Change the pointer's choices (form, parts, cap, delivery). */ updateRef(id: string, options: SourceRefOptions): void; setWaitForClean(id: string, wait: boolean): void; setTopic(topic: string): void; /** True when this pointer is already picked. */ hasRef(resourceType: string, resourceId: string): boolean; /** How many Sources are picked RIGHT NOW (read from the store, never a stale render). */ liveCount(): number; /** POST /sources/manifest for the ready Sources; cards update with it. */ manifest(): Promise; } interface SourceSetController extends SourceSetActions { getState(): SourceSetState; subscribe(listener: () => void): () => void; getMeta(): SourceSetMeta; subscribeMeta(listener: () => void): () => void; /** Replace the host's choices (called on every render by a React binding). */ configure(config: SourceSetConfig): void; readonly config: SourceSetConfig; /** Bring back a saved draft once — only into an empty input. True when it restored. */ restore(saved: unknown): boolean; /** The frozen v1 payload, built from the ready Sources. */ toSourceSet(options?: { targetModelId?: string | undefined; }): SourceSet; /** * Apply a set the review returned (forms, parts, caps, delivery). Pass * `reviewed` — the set sent INTO the review — and every Source removed there * leaves the list too (only those; one that landed meanwhile stays). */ applySourceSet(set: SourceSet, options?: { reviewed?: SourceSet | undefined; }): void; /** POST /sources/resolve — the grounded text for a generator, named as the cards name them. */ resolve(options?: { targetModelId?: string | undefined; }): Promise; /** Switch back any Source set to a delivery the host cannot use (with a note). */ fitDeliveries(deliveries?: readonly SourceDelivery[]): void; /** How many more Sources fit under the host's max. */ roomLeft(): number; totalChars(): number; settled(): boolean; } /** An in-memory store — for clients without their own, and for tests. */ declare function createMemorySourceSetStore(initial?: SourceSetState): SourceSetStore; /** An in-memory draft store (a stand-in for device storage in tests and previews). */ declare function createMemorySourceDraftStorage(initial?: unknown): SourceDraftStorage & { current(): unknown; }; declare function createSourceSetController(adapter: SourceSetAdapter): SourceSetController; /** * Source intake — every way NEW material becomes a Source, through the doors * that already exist on the server (DESIGN.md amendment A3). Each call adds a * card at once (so the person sees it working), then settles it to a pointer * or fails it with a sentence and a remedy. Nothing is ever sent onward as a * blob. * * pasted text → `POST /sources/land`, kept. * a web page → the host's scraper lands it (`addScrapedPage`), or a link * typed before a reload is scraped again (`addWebPage`); * `POST /sources/{id}/keep` files it against the thing being made. * a file/image → the host's uploader hands over file ids (`addUploaded`); the * file is filed against `attachTo` in the direction the * association registry declares, and its Source is kept once * reading makes one (`fileLanded`, driven by the recovery). * existing → a registry item (`addExisting`). * YouTube/audio → the transcript comes from the host's readers and lands * through `POST /sources/land`. * * Never lose input: every landing keeps what the person handed over in the * draft (`SourceDraft.input` — text, link, uploaded recording; never bytes) * until it settles, so `resume` can land it again after a reload, a failure, * or once an organization is chosen (the recovery replays every card waiting * for one — from state, never an in-memory queue). Landing twice is safe: the * door dedupes by (organization, identity, content hash). * * Every outside dependency is INJECTED (`SourceIntakeDeps`): the doors, the * organization gate, the signed-in person, the failure wording. */ /** A door's notice: a sentence, and sometimes a remedy code ("save_it…"). */ interface SourceLandingNotice { message: string; remedy?: string | null | undefined; } /** The target a door files a Source against (`attach_to`). */ interface SourceAttachTarget { entity_type: string; entity_id: string; label: string | null; signal: boolean; } /** The part of a landing body the intake reads or sets — the rest is the host's door shape. */ interface SourceLandingBodyBase { name: string; canonical_identity?: string | null | undefined; provenance?: Record | null | undefined; } /** A landed (or kept) Source's answer. */ interface SourceLandingAnswer { processed_document_id: string; notices?: SourceLandingNotice[] | null | undefined; } /** What the host's web reader returns for a link. Null = the page could not be read. */ interface ScrapedSourcePage { processedDocumentId: string | null | undefined; sourceNotices: SourceLandingNotice[]; pageTitle?: string | null | undefined; } /** What the host's YouTube reader returns. `source` = the server landed a timed transcript Source. */ interface YouTubeTranscriptResult { text: string | null | undefined; note?: string | null | undefined; source?: { processed_document_id: string; title?: string | null | undefined; } | null | undefined; } /** One file the host's uploader already uploaded. */ interface UploadedSourceFile { fileId: string; name: string; } /** The association write (the host's one chokepoint). */ interface SourceAssociationEdge { sourceType: string; sourceId: string; targetType: string; targetId: string; orgId: string; } type SourceAssociationResult = { ok: true; } | { ok: false; error: { message: string; }; }; interface SourceIntakeDoors { /** `POST /sources/land` with the body as given (keep + attach_to set by the intake). */ land(body: Body & { keep: true; attach_to: SourceAttachTarget[]; }): Promise; /** `POST /sources/{id}/keep`, filed against the targets. */ keep(processedDocumentId: string, options: { attachTo: SourceAttachTarget[]; organizationId: string; }): Promise<{ notices?: SourceLandingNotice[] | null | undefined; }>; /** Build the landing body for text the person handed over. */ buildTextLanding(input: { text: string; name?: string | undefined; organizationId: string; userId: string; }): Promise; /** Read a web page (the scraper lands it). Null = it could not be read. */ scrapeUrl(url: string): Promise; /** Read a YouTube video's transcript. */ fetchYouTubeTranscript(url: string): Promise; /** Write out a recording that is already stored. */ transcribeFile(input: { fileId: string; organizationId: string; }): Promise<{ text?: string | null | undefined; }>; /** File one edge through the host's association chokepoint. */ associate(edge: SourceAssociationEdge): Promise; } interface SourceIntakeDeps { doors: SourceIntakeDoors; /** The organization to land in — asks the person when none is chosen (the host's gate). */ ensureOrganization(): Promise; /** Run a landing the person asked for so the gate can carry the click intent. Default: run it. */ holdIntent?: ((run: () => Promise) => Promise) | undefined; /** True when an organization is already chosen (asking would not open the picker). Omitted = unknown. */ hasOrganization?(): boolean; /** True when a landing stopped only because no organization is chosen yet. */ waitsForOrganization(error: unknown): boolean; /** A failed landing as one sentence with its remedy. */ failureSentence(error: unknown): string; /** The signed-in person (null while sign-in is still loading). */ userId(): string | null | undefined; /** The YouTube video id in a link, or null. */ youtubeId(url: string): string | null | undefined; /** The largest pasted text kept in the draft for a reload (null = unknown — nothing kept). */ maxKeptChars(): number | null; /** The thing being made — new Sources are filed against it and kept. */ attachTo?: SourceAttachTo | undefined; } interface SourceIntake { addPastedText(text: string, name?: string): Promise; addWebPage(url: string): Promise; /** * A page the host's web picker already read (and the scraper landed); null id * = land its text instead. `pendingId` = the card `beginWebPage` made when the * read started — it is settled instead of a new card being added. */ addScrapedPage(page: { url: string; title: string; text: string; processedDocumentId: string | null; }, pendingId?: string | null): Promise; /** * The host's web picker STARTED reading a link: a card with the link kept * appears at once, so a reload mid-read brings it back and lands it (verify-3 * 12a/12b: a reload while a page was read lost it, link and all). Finish it * with `addScrapedPage(page, id)`; if the person drops the preview, remove it; * if the picker goes away mid-read, `resume(card)` lands it through the web door. */ beginWebPage(url: string): string; /** Files the host's uploader already uploaded. */ addUploaded(files: readonly UploadedSourceFile[], kind: SourceKindId): Promise; /** * A recording made in place (the Voice Pad) that is already written out: it * lands as text, but it IS a recording — its card and review say "Recording", * never "Pasted text" — and it keeps its name ("Recording 3:41 PM"). */ addRecordedText(text: string, name?: string): Promise; /** A recording already uploaded — written out, then landed. */ addUploadedRecording(file: UploadedSourceFile): Promise; /** Something the person already has (Use existing / search). Returns false when already picked. */ addExisting(item: { token: string; id: string; title: string; }): boolean; addYouTube(url: string): Promise; /** Land a card's kept input again (after a reload or a failure). False when nothing was kept. */ resume(card: SourceCardModel): boolean; /** The person chose the file again (uploaded) for a card whose upload was cut off. */ retryFile(card: SourceCardModel, file: UploadedSourceFile): Promise; /** Reading an uploaded file made its Source: keep it and file it against `attachTo`. */ fileLanded(card: SourceCardModel, processedDocumentId: string): Promise; } declare function sourceAttachTargets(attachTo: SourceAttachTo | undefined): SourceAttachTarget[]; /** * Which way a file ↔ `targetType` edge is registered — read from the package's * generated association registry (`@ai-matrx/associations`) — or null when it * is not registered. */ declare function registeredFileEdge(targetType: string): "file_to_target" | "target_to_file" | null; declare function createSourceIntake(set: SourceSetActions, deps: SourceIntakeDeps): SourceIntake; /** * Recovery — the UI-free half of "never lose input" (USI-3b), as decisions and * one step function. A React binding (`@ai-matrx/agents/sources/react` * `useSourceRecovery`) schedules them; any other client can call them itself. * * 1. A Source a reload cut off while it was landing, whose input the draft * kept (`interrupted.ts`), is handed back to the door once — the door * dedupes by content hash, so a second landing reuses the same Source. * 2. A landing that waited for an organization is landed again the moment one * is set — from the card's kept input, or, for a read Source whose keep * waited, by keeping it again. State, never an in-memory queue. * 3. A stored file's Source (new upload, reused copy, or picked file) is read * from the SERVER's state (`fileSource.ts`): kept and filed once it exists, * the one run started only when nothing is reading it, re-attached after a * reload — never a duplicate run. * * The once-per-card guards live on `globalThis` (one registry however many * loader graphs import this module), so two inputs on one surface, or a * re-mount, never land the same card twice. */ interface RecoveryGuards { resumedCards: Set; keptFileSources: Set; startedFileRuns: Set; } declare function sourceRecoveryGuards(): RecoveryGuards; /** Cards a reload cut off whose kept input lands again now. */ declare function cardsToResume(cards: readonly SourceCardModel[]): SourceCardModel[]; /** Resume each cut-off card once (per card id, across every input). */ declare function resumeInterrupted(cards: readonly SourceCardModel[], intake: Pick): void; /** Cards that waited for an organization (a landing, or a read Source's keep). */ declare function cardsWaitingForOrganization(cards: readonly SourceCardModel[]): SourceCardModel[]; /** An organization is known now: land or keep every card that waited for one. */ declare function continueAfterOrganization(cards: readonly SourceCardModel[], intake: Pick): void; /** The host's processing runner, as far as the recovery needs it. */ interface SourceFileRunner { jobs: ReadonlyArray<{ cldFileId?: string | null | undefined; status: string; processedDocumentId?: string | null | undefined; }>; runForCldFile(fileId: string, label: string, reason: string): Promise; } interface FileRecoveryDeps { /** The organization the person picked (null = none yet). */ organizationId: string | null | undefined; /** The server's `/files/{id}/rag-status` read. */ readFileState(fileId: string, signal?: AbortSignal): Promise; /** The host's lookup of a file's own organization (omitted = unknown). */ fileOrganizationId?: ((fileId: string) => string | null | undefined) | undefined; } /** A file card the recovery may ask about now (waiting, and not held for an organization). */ declare function isAskableFileCard(card: SourceCardModel, deps: Pick): boolean; /** * One look at every askable file card: a Source → keep and file it; a live run * → look again; nothing reading it → start the one run (once); unreadable → * say so. Returns true while any card still waits (look again later). */ declare function lookAtFileCards(cards: readonly SourceCardModel[], ctx: { set: Pick; intake: Pick; runner: SourceFileRunner; signal: AbortSignal; } & FileRecoveryDeps): Promise; export { ASKING_FOR_ORGANIZATION, DELIVERY_CHOICES, DELIVERY_WORDS, EMPTY_SOURCE_SET_STATE, HEAVY_SHARE, KEEP_WAITING_FOR_ORGANIZATION, MAX_KEPT_TEXT_KNOB, NOT_KEPT_FOR_RELOAD, PARTS_MATCHED_BY_PAGE, PARTS_NOT_MATCHED, RELOADED_RESUMING, RELOADED_WHILE_ADDING, SAME_SOURCE_AGAIN, SOURCE_CHARS_PER_TOKEN, SourceDelivery, SourceDoorError, WAITING_FOR_ORGANIZATION, addPendingCard, addReadyCard, allowedDeliveries, applyManifest, applyReviewedSourceSet, cardsToResume, cardsWaitingForOrganization, carryPickedParts, chosenForm, continueAfterOrganization, createFetchSourcesTransport, createMemorySourceDraftStorage, createMemorySourceSetStore, createSourceIntake, createSourceSetController, createSourcesClient, deliveryChoicesFor, deliveryPatch, deliverySwitchedNote, draftKindForToken, estimateSourceTokens, failCard, fileCardHeldForOrganization, fileSourcePollDelayMs, findParts, fitCardDeliveries, fitDelivery, hasSourceRef, isAskableFileCard, isSourceDraft, isWaitingFileCard, isWordQuery, lookAtFileCards, matchesPart, nextFileStep, partPages, planSourceReview, readPersistedSourceInput, readySourceRefs, registeredFileEdge, reloadedCard, rememberPickedParts, removeCard, removedInReview, renderedPartChars, restartCard, restoredCards, resumableInput, resumeInterrupted, sameSourceRef, selectSettled, selectSourceSet, selectTotalChars, setCardWaitForClean, setSourceTopic, settleCard, sourceAttachTargets, sourceCardChars, sourceCardMeasuredState, sourceCardState, sourceDelivery, sourceEntryChars, sourceKey, sourceRecoveryGuards, sourceRoomLeft, toPersistedSourceInput, updateCardDraft, updateCardRef, uploadCutOffSentence, withDefaultForm, withDisplayNames }; export type { BudgetVerdict, DeliveryWords, FetchSourcesTransportOptions, FileRecoveryDeps, FileSourceContext, FileSourceStatus, FileSourceStep, PartMatchIds, PersistedSourceCard, PersistedSourceInput, PlanInput, ReloadedCard, ScrapedSourcePage, SourceAssociationEdge, SourceAssociationResult, SourceAttachTarget, SourceAttachTo, SourceCallOptions, SourceCardModel, SourceCardState, SourceCardStatus, SourceDoorCalls, SourceDoorPath, SourceDraft, SourceDraftStorage, SourceFileRunner, SourceIntake, SourceIntakeDeps, SourceIntakeDoors, SourceIntakeInput, SourceKindId, SourceLandingAnswer, SourceLandingBodyBase, SourceLandingNotice, SourcePart, SourcePartsSearchResult, SourcePickedPart, SourcePlan, SourcePlanEntry, SourcePlanStatus, SourceSetActions, SourceSetAdapter, SourceSetConfig, SourceSetController, SourceSetMeta, SourceSetState, SourceSetStore, SourceTileId, SourceTransportOptions, SourcesClient, SourcesTransport, UploadedSourceFile, YouTubeTranscriptResult };