UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

570 lines (569 loc) • 41.7 kB
import { type BackgroundAgentRecord, type BackgroundAgentStore } from "./background-agent-store.js"; import { type StopSource, type TaskAccess, type TaskRetrievalStatus, type UnifiedTaskOutput, type UnifiedTaskResult, type BackgroundAgentTaskHandle, type DurableAgentCore, type ParkedClaimTicket, type RegisterBackgroundAgentInput } from "./task-registry-shared.js"; import { type ToolResultStore } from "./tool-result-store.js"; import { type AgentState } from "./agent-state.js"; import type { CheckpointSeat } from "./checkpoint-seat.js"; /** * Subagent transcript persistence (delegation entry caps) — the ACTIVE handles of one delegation * tree in THIS process's registry: `background_agent` handles whose status is running/pending * (parked does not burn a concurrency slot — a suspension is not concurrency; terminal handles are * not active) under the `(scope, rootSessionId)` key (full depth: a grandchild's row carries the * tree's fixed-point root, so the whole tree shares one pool; a depth-1 row whose rootSessionId * predates the floor falls back to its parentSessionId — same fallback the access predicate's root * arm rides). SYNCHRONOUS by contract: the caps enforcement runs check→register in one synchronous * segment, which is what makes the reservation race-free in-process. */ export declare function activeDelegationHandlesLane(core: DurableAgentCore, scope: string, rootSessionId: string): string[]; export declare function ensureDurableHeartbeatLane(core: DurableAgentCore): void; /** design/151 S1a — enqueue one durable-row write (see {@link DurableAgentLane} for the lane * contract). `patch` is captured at CALL time (the settle-site values), applied in chain order. * * hardening — failure taxonomy, decided by EVIDENCE not by exception class: * - thrown store errors get a bounded in-lane retry (a transient outage must not permanently * strand a terminal write — the settle is often the row's LAST write); * - `already_exists` on a put and a lost CAS both trigger a read-back writerId check first: an * AMBIGUOUS earlier commit (backend committed, response failed) or a stale-arm flip * (reap wrote rev up under a live run) is OUR row wearing a newer rev — ADOPT the rev and * re-apply (the real terminal truth must beat a reaper's guess); only a row carrying a * FOREIGN writerId poisons the lane (never clobber another writer). */ export declare function durableAgentWriteLane(handle: BackgroundAgentTaskHandle, patch: Partial<BackgroundAgentRecord>, clear?: readonly (keyof BackgroundAgentRecord)[]): void; /** design/151 S1b — is this row's durable lane ARMED (store wired at registration, not contaminated)? * Observability/test face. ⚠️ NOT the lifecycle-flip key: arming says a row was ATTEMPTED, not * that it EXISTS — release anchors gate on {@link durableAgentRowProbe}. */ export declare function durableAgentArmedLane(core: DurableAgentCore, id: string): boolean; /** design/151 S1b — the AWAITABLE lifecycle-flip key. Returns undefined when no * lane was armed; otherwise a probe CLOSURE over the HANDLE (outlives its eviction from the map — * retain-ledger TTL arms fire hours after terminal GC) that flushes the queued writes and answers * whether a durable row actually EXISTS right now (initial put confirmed, not contaminated). A store * that never accepted the put answers false — the caller then RELEASES the session (pre-151 path) * instead of stranding an unreachable transcript. * * The closure reads `handle.durable` at CALL time rather than capturing the lane: a revive's claim * hands the row to a fresh lane and retires the one that was current when the probe was minted, and * a probe bound to the retired object would answer "no row" for a row that plainly exists — and the * caller acts on that by releasing the child's transcript session, the exact strand this probe was * built to prevent. Reading through the handle also survives the map eviction (the closure keeps the * handle object alive), so the outlives-terminal-GC property the retain arms rely on is unchanged. */ export declare function durableAgentRowProbeLane(core: DurableAgentCore, id: string): (() => Promise<boolean>) | undefined; /** design/151 §7.3 (F-8) — open the in-process claim window for a tier-3 revival of `id`. * SYNCHRONOUS admission check + mark in one call: refuses (false) while the handle is live * running/pending in THIS process (the live delivery ladder owns it), while a reap sweep is * adjudicating it, or while another claim holds the window. The caller MUST call * {@link endDurableClaim} in a finally — the window spans its whole claim-CAS→spawn/rollback * sequence, and a leaked mark would permanently fence the handle from revival, reap AND the * terminal-handle GC (which skips ids inside this window), i.e. it would also leak the handle. */ export declare function beginDurableClaimLane(core: DurableAgentCore, id: string): boolean; /** Close the {@link beginDurableClaim} window (idempotent). */ export declare function endDurableClaimLane(core: DurableAgentCore, id: string): void; /** design/151 §3.1 MED-7 — the BLESSED retention path: the joint reap that drops a * doomed row under its rev guard and THEN releases its transcript session (raw `store.reap` * strands transcripts — its contract says so). Policy semantics mirror the store sweep (double * bound + stale-running flip; running rows are never deleted). * * ORDER: conditional DELETE first, release only after the delete WINS. The * reverse (release→delete) destroyed a live transcript whenever a concurrent writer revived the * row between the release and the losing CAS (stale-flip heal / retained-session revive: row * back to running, session already gone). Failure economics of this order: a crash (or release * throw) after the winning delete LEAKS the session to the deployment's session TTL sweep — * recoverable — instead of stranding a live transcript — not. The release must therefore treat * a missing/already-released id as a no-op (idempotent — a release that throws on * not_found merely forfeits this bookkeeping, the TTL sweep still owns the orphan). An * IN-PROCESS handle running again gates the row out entirely (a revive flips memory before its * durable write flushes). `sessions` is structural (unpin/release) so any SessionStore fits * without an import cycle. * ⚠️ Review note: passing `sessions` DECLARES that its `release` truly ends the * transcript's addressability (durable deletion or an equivalent lifecycle handoff). A face * whose release only clears in-memory state must NOT be passed — omit `sessions` instead * (fail-closed: rows are kept and counted in `skippedNoSessions`, and the deployment's own * session GC owns the joint lifecycle). A half-true release deletes the row while the * transcript lives on unanchored — the exact strand this orchestration exists to prevent. */ export declare function reapDurableAgentsLane(core: DurableAgentCore, scope: string, deps: { store: BackgroundAgentStore; /** `listPlaced` (subagent transcript persistence, optional): arms the PARTITION leg below — * placed transcript sessions whose owning row is GONE (row never landed / deleted-but-release- * failed) are the orphan shape only a session-side enumeration can find. Absent = that leg is * unavailable (documented; the row-joined path still runs). */ sessions?: { unpin?(sessionId: string): unknown; release(sessionId: string): Promise<void> | void; listPlaced?(kind: "subagent", opts?: { olderThanMs?: number; scope?: string; }): Promise<Array<{ sessionId: string; placedAt: number; } & ({ scope: string; handle: string; } | { tupleIncomplete: true; })>>; }; /** design/151 §7.7 (F-13) — the row's mailbox dies with the row: a WINNING delete also drops * the (scope, handle) mailbox (advisory — a mailbox fault never blocks the reap; an orphaned * box is bounded by the mailbox's own `reap` policy). Structural face (drop only), so the * deployment passes its `RunnerDeps.mailboxStore` directly. */ mailbox?: { drop(scope: string, handle: string): Promise<void>; }; }, policy: { now?: number; maxAgeMs?: number; keep?: number; staleRunningMaxAgeMs?: number; }): Promise<{ rowsReaped: number; sessionsReleased: number; skippedNoSessions: number; orphanPlacedReleased: number; }>; /** after a probe-false RELEASE the row must stop claiming a transcript: * the heartbeat's F-1 arm keeps re-driving a flush-failed lane, so a later successful flush * would otherwise persist a row whose sessionId points at the session the caller just released. * Clearing the anchor makes the eventual row honest: state/result survive, transcript does not. */ export declare function releaseDurableTranscriptAnchorLane(core: DurableAgentCore, id: string): void; /** design/151 S1a — bind the child's freshly-minted store session id onto the row (the transcript / * resume anchor; absent at register time because the child session is created after registration). */ export declare function bindBackgroundAgentSessionLane(core: DurableAgentCore, id: string, sessionId: string): void; /** * #22 — record the org-memory admission verdict THIS leg adjudicated onto the durable row, so the * next CROSS-PROCESS revival (which rebuilds the spec from the reviving caller's mount and cannot * see this process's memory) starts from it instead of re-adjudicating unconstrained. Called once * per leg at the injector-ready barrier (prepare has succeeded by then, so the verdict exists); a * revival cycle overwrites the previous cycle's record with its own, which the admission door * guarantees is a subset. Same durable write lane as every other post-registration fact — a * store-less registration is a no-op, and a contaminated lane stays contaminated. * * A DROPPED write is disclosed. This record is the only carrier a narrowing has across a process * boundary, so what stands on the row when the write does not land is the PREVIOUS leg's wider * verdict — which the next revival then seeds from. Two drop shapes, one message each: a contaminated * lane refuses at its front door, and an exhausted flush leaves the value unwritten with the lane * still armed. The channel is the same durable-agents warning channel the lane uses for the contamination * itself (that one says writes stopped; this one says WHICH fact stopped with them). The write * remains best-effort: no leg faults because its bookkeeping failed. * * The FORK background lane deliberately does not call this: a fork row refuses the tier-3 claim * (subagent.ts, revival preflight), so it has no reader. */ export declare function recordBackgroundAgentOrgAdmissionLane(core: DurableAgentCore, id: string, verdict: import("./memory-admission.js").OwnOrgAdmissionVerdict): void; /** design/115 P3: register a background sub-agent run. The CALLER owns driving the child promise and * calling {@link settleBackgroundAgent} at the end; the registry provides the unified task_id, the * owner/scope guard, poll/stop dispatch, and terminal GC — exactly like the other two kinds. */ export declare function registerBackgroundAgentLane(core: DurableAgentCore, input: RegisterBackgroundAgentInput): string; /** * design/153 §7.1 — the SINGLE running→parked transition point (r4 F-02: every parked-related * transition goes through registry discipline, never a bare store-status write). Called by the bg * watcher's `suspended` branch AFTER the durable checkpoint exists and the deployment attested the * child session durable (§7.3 capability). First-writer-wins like settle: a TaskStop that already * flipped killed refuses the park (returns undefined — the CALLER must then EXPIRE the checkpoint, * the §7.1 no-orphans compensation). Releases the delivery channel like the other non-running * transitions (a parked child has no live lane; a SendMessage parks per session as with terminal). */ export declare function parkBackgroundAgentLane(core: DurableAgentCore, id: string, park: { checkpointToken: string; seq?: number; resolveStop?: () => Promise<boolean>; }): "parked" | undefined; /** * design/153 §7.1 (件1 split-brain fence) — the registry-aware reconciliation entry an * IN-PROCESS deployment calls on its retention cadence. Two halves, one call: * 1. LIVE half: this instance's own parked handles are probed against their checkpoint and, on * expired/missing/DECIDED-ELSEWHERE, settled failed THROUGH the handle (poll/stop/durable row * stay coherent — quiescence poked, arbitration closure dropped, durable clear rides the settle * write). * 2. STORE half: delegates to the store-level {@link reconcileParkedAgents} with * `excludeWriterId` = this instance's writerId, so foreign (dead-process) rows are cleaned * without ever touching a row this process is live-managing. * * The live half used to skip EVERY non-expired/missing state, `resolved` included, and * that was the whole zombie: a parked row whose checkpoint was decided by the task-level * `resumeCheckpoint` leg (a serving layer's documented fallback when its parked-row discriminator * misses) is decided but still says `parked`, its side effects already executed by a path that never * touched this registry. Nothing else could reach it either — a re-claim answers * `checkpoint_not_pending`, and the store half excludes exactly the rows this process live-manages — * so it stayed `parked` for the life of the process. It now converges here. The one `resolved` case * that must still be LET THROUGH is the registry's own claim window (reservation → consume flip): that * skip is load-bearing, and it is now expressed as what it actually is — a claim in flight — instead * of as a blanket state filter that also swallowed the case with no claimant at all. */ export declare function reconcileParkedAgentsLane(core: DurableAgentCore, stores: { agentStore: import("./background-agent-store.js").BackgroundAgentStore; checkpointStore: Pick<import("./checkpoint-store.js").CheckpointStore, "get">; }, scope: string, now: number, opts?: { staleClaimMaxAgeMs?: number; }): Promise<{ failed: number; rolledBack: number; }>; /** * design/153 §7.2c (件3b) — the RESERVATION half of the parked-resume claim. Pure reservation * (r6 H-1): the row STAYS `parked` — only `parkClaimId` is written (guarded CAS binding * rev+status+token+claim-absence, single-claimer by construction), the live handle keeps its * parked state AND its TaskStop arbitration closure (an expire during the reservation window * still wins — by design; the resume's token-consume CAS is the only thing that can beat it). * parked→running happens ONLY at the 件3c consume flip, never here. * * AUTHORIZATION LAYERING (r4 F-06, doc-pinned): `access` is ROW VISIBILITY only * ({@link canAccessAgentRecord}); the operator's approval authority (/decide) is the SERVER's * front gate — this verb must only be reached by an already-authorized decision path. * Binding (r6): the token comes FROM THE ROW (never caller-supplied), the checkpoint must be * `pending`, and `cp.sessionId === row.sessionId` (same child transcript — blocks swapping in a * different child's checkpoint). */ export declare function claimParkedAgentLane(stores: { agentStore: import("./background-agent-store.js").BackgroundAgentStore; checkpointStore: Pick<import("./checkpoint-store.js").CheckpointStore, "get">; }, handle: string, access: TaskAccess): Promise<{ ok: true; ticket: ParkedClaimTicket; } | { ok: false; reason: "not_found" | "not_parked" | "binding_broken" | "checkpoint_not_pending" | "claim_lost" | "store_unreachable"; }>; /** * design/153 §7.2c (件3b) — roll back / settle a reservation whose resume could not proceed. * Disposition follows the CHECKPOINT truth re-read at call time (r6: never blind): * - `pending` → the reservation is cleared, the row returns to plainly-redeemable `parked` * ("rolled_back"); no ownership change (a reservation never transferred it, so no epoch churn). * - `expired`/`missing` → honest terminal `failed` ("failed") — same shape reconciliation mints. * - `resolved` → the approval was CONSUMED (the resume died post-consume): NON-COMPENSABLE * (r6 H-4) — terminal `failed`/outcome-unknown ("failed"); NEVER back to parked. * - store unreachable → "retry" (nothing written; the reconciliation cadence is the backstop). * A LOCAL live parked handle follows the failed dispositions (poll/stop stay coherent). */ export declare function rollbackParkedClaimLane(core: DurableAgentCore, stores: { agentStore: import("./background-agent-store.js").BackgroundAgentStore; checkpointStore: Pick<import("./checkpoint-store.js").CheckpointStore, "get">; }, ticket: ParkedClaimTicket): Promise<"rolled_back" | "failed" | "lost" | "retry">; /** * design/153 §7.2d (件3c 3c H-1) — the CONSUME FLIP, SERIALIZED THROUGH THE DURABLE LANE: * the guarded parked→running store CAS runs INSIDE the lane chain (ordered after every seeded * write — the session-bind CAS included), so no in-flight lane write can race it, CAS-fail on the * flip's rev bump, read the new epoch as foreign, and contaminate the lane (which would permanently * drop finalize/terminal/re-park writes). On a won CAS the lane record/rev swap happens in the * SAME chain step and the live handle adopts (running, arbitration dropped — the token is * consumed; `parkClaimId` stays visible until finalize). Returns false when the row moved * (claim/status/token guard lost) — the caller aborts the resume. */ export declare function consumeParkedFlipLane(core: DurableAgentCore, id: string, stores: { agentStore: import("./background-agent-store.js").BackgroundAgentStore; }, ticket: ParkedClaimTicket): Promise<boolean>; /** * design/153 §7.2d (件3c) — attach finalize: the resume's delivery lane is live, the claim is * complete — clear `parkClaimId` (handle + durable row) so reconciliation stops seeing an * in-flight claim. Idempotent; a terminal settle that lands first clears the same keys itself. */ export declare function finalizeParkedResumeLane(core: DurableAgentCore, id: string): void; /** Terminal update for a background agent (the spawn-side then/catch calls this exactly once). * Reap sentinel note: {@link BG_AGENT_REAP_STOP_ERROR} is never STORED as `error` — status "killed" * + stoppedBy already carry that fact, and an `error:` line beside a salvaged partial result reads * as "the child's work failed" (review A1 follow-up). Real errors (e.g. "session released", a * child crash) still land. * design/129-B: returns the WINNING terminal status — the earlier writer's when this settle is refused * (first-writer-wins), `undefined` when the handle is gone (evicted). Callers report THIS in their * notification/terminal event so the push never contradicts the registry row (a completed-notify over a * killed row — the TaskStop-then-late-resolve race — was an observable incoherence). */ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: string, outcome: { status: "completed" | "failed" | "killed"; result?: string; resultFull?: string; error?: string; errorCode?: string; retryable?: boolean; errorKind?: string; retryAfterMs?: number; stoppedBy?: StopSource; seq?: number; cycle?: number; }): "completed" | "failed" | "killed" | undefined; /** Abort every background agent belonging to `access` (parent-task teardown — a finished parent must * not leave orphan child runs burning tokens; mirrors clearBackgroundForOwner for bash). */ export declare function abortBackgroundAgentsForOwnerLane(core: DurableAgentCore, access: TaskAccess, opts?: { skipSessionScoped?: boolean; sessionScopedOnly?: boolean; }): number; /** CC206-B name resolution over the caller-visible background_agent rows (CC `cgo`/`gvy`, 206:575142-575178 * and 206:575199-575210 — sema's single-lane subset: no teammate/name-registry branches). * * [c209-D] PUBLIC + explicit-name layer 0: the single resolver both TaskStop (:1839 leg) and * SendMessage consume, so the two verbs can never drift. Ladder: * 0a. verbatim `handle.name` match — LATEST-WINS (CC SendMessage.md: "a newer agent took the name * (latest wins)"; older rows stay task_id-addressable), any status (names keep working after * completion — the resume face is the CC "resumes it from its transcript" counterpart). * **Latest-wins is RESOLUTION, not permission to redirect a conversation**: CC pairs the same * resolver with a per-conversation pin guard (`send_message_pin_guard`), and SendMessage REFUSES * a send whose name has changed hands since this caller last used it. That guard lives at the * SendMessage face (`createSendMessageTool`), exactly as it does upstream — this resolver stays * a pure resolver, and TaskStop keeps its own (preferRunning) posture untouched; * 0b. normalized `handle.name` match — same latest-wins (the spawn regex bans whitespace, so * normalization here is effectively NFKC+lowercase: "Builder"/"builder" share the name pool); * 1+. the pre-[c209] description layers, UNCHANGED (exact → normalized, running-preferred + * ambiguous posture — TaskStop's anchored behavior does not regress). * Matching key of the legacy layers = normalized description (the spawn label that IS the agent's * name on this face). Running rows are preferred over terminal ones (CC `gvy`), so a re-used label * addresses the live agent, while a lone terminal match still resolves. */ /** NOTE (server[1523] 对等复审 4b, recorded): the name pool scans LIVE handles only — durable-only * rows (restart survivors) are deliberately NOT name-addressable through this rung (canonical-only * 裁决); tier-3 revival's name rung goes through the roster instead (design/151 §7.4). */ export declare function resolveBackgroundAgentByNameLane(core: DurableAgentCore, name: string, access: TaskAccess, opts?: { preferRunning?: boolean; }): { status: "found"; handle: BackgroundAgentTaskHandle; } | { status: "ambiguous"; message: string; } | { status: "not_found"; suggestion?: string; }; /** S2b': flip the park-redeemability bit AFTER retention is actually secured * (tryRetainChild success) — registration happens before the retain attempt, and session-scope * alone does NOT retain the child session, so declaring at register time acknowledged parks that * a capacity/pin failure (or plain session-scope) could never drain. No-op on unknown ids. */ export declare function markRetainedContinuationLane(core: DurableAgentCore, id: string): void; /** * The stop-cycle generation of the RUN BEING SPAWNED under this row (the same counter the * register/revive lanes keep on the handle: fresh spawn = 1, durable-seeded revive = the claimed * row's seq, in-memory wake = the bump). The spawn lanes read it ONCE, right after registering, to * thread into the child's `RunInternals.cycleSeq` — one authoritative source instead of each lane * re-deriving the seed-vs-fresh arithmetic. `undefined` for an unknown id or a non-agent row. * * PARKED-BORN arm (falsification F1): a parked handle's counter still names the cycle that PARKED — * the run this spawn lane is about to drive begins at the consume flip, which installs the bumped * value (`flipped.seq = seq + 1`, the §7.2d consume write). Answering the pre-flip number made one * resumed run speak two generations (spawn/ticks at n, terminal at n+1), so the parked arm answers * the flip's target instead. A resume that loses the consume arbitration never runs, so the answer * is never attached to a live cycle it doesn't describe. */ export declare function backgroundAgentCycleSeqLane(core: DurableAgentCore, id: string): number | undefined; /** S2b RB-27② — REVIVE a settled background-agent row for a retained-session RESUME cycle: the * row flips back to "running" with a fresh "attaching" channel and a bumped revive-cycle stamp * (returned; the resume leg threads it through attach and settle so a stale cycle's late calls * can never clobber the new cycle's channel). Only terminal rows revive; running rows refuse * (the SendMessage still_running path owns that story). * * The DURABLE transition is a {@link claimTerminalRowForRevive} claim, awaited BEFORE the in-memory * flip — the two orders are not equivalent: flipping memory first (the pre-arbitration shape, a plain lane * write with no row guard) let a foreign claim win the row while this process kept serving * `running`, and every later write of this cycle then died in the contaminated lane without a trace. * Three lane shapes, three treatments: * - NO lane (store-less registration): the pre-arbitration in-memory-only transition, unchanged — there is * no row, hence no arbitration domain and no competitor; * - lane whose initial put never CREATED a row: same — nothing exists for a foreign claim to have * won, and the write lane keeps retrying the put. This is decided by the claim's own read INSIDE * the chain (row absent AND the put still unconfirmed after the chain drained), never by the * entry-time flag: a put merely in flight, or one that committed under a failed response, would * otherwise exempt a row that does exist — the split-brain window this claim exists to close; * - Contaminated lane (stale-flip ownership transfer / row reaped mid-write / heartbeat re-drive * ceiling): the claim runs DIRECTLY through the store — the dead lane's chain is no longer a * serialization point, the rev guard is. Re-arming that lane object in place is what is NOT * done: an in-flight write from it would then see its own writerId on the re-claimed row and * adopt it. It stays contaminated (its record keeps the pre-claim writerId/epoch, so its read-back * answers "foreign" and it stops), and a row that cannot be claimed — gone, or live under * another writer — REFUSES the revive rather than resuming with a durably unreachable row. * A WON claim always hands the row to a FRESH lane for the claimed generation (both paths), for the * reason spelled out at the handoff: a write appended while the claim was in flight speaks for the * cycle that just ended and must not be applied under the new ownership. * While the claim is in flight the handle holds the in-process claim window * ({@link beginDurableClaimLane}'s mark), so a tier-3 claim, a reap sweep and a second revive of * the same row all observe it and take their existing refusals. */ export declare function reviveBackgroundAgentLane(core: DurableAgentCore, id: string, access: TaskAccess, abort?: AbortController): Promise<{ ok: true; cycle: number; } | { ok: false; reason: "not_found" | "still_running" | "recycling"; }>; /** S2b RB-27② — settle a REVIVED cycle (cycle-stamped: a stale cycle's late settle is a no-op so * it can never flip a newer revived cycle back to terminal / clear its channel). */ export declare function settleRevivedAgentLane(core: DurableAgentCore, id: string, cycle: number, outcome: { status: "completed" | "failed" | "killed"; result?: string; resultFull?: string; error?: string; errorCode?: string; retryable?: boolean; errorKind?: string; retryAfterMs?: number; }): "completed" | "failed" | "killed" | undefined; /** * design/164 — record LIVE ACTIVITY on a running background-agent row (one tool-lifecycle beat). * * Why this exists: retiring `SESSION_BG_DEFAULT_TIMEOUT_SEC` removed the wall-clock ceiling that used to * bound a session-scoped background child, and the design/129 F3 anti-zombie duty had to land somewhere * that does not punish a legitimately long task. Collecting by STALL needs a stall signal, and the * in-memory row had none: `updatedAt` moved only at registration / park / settle, so a child grinding * through a two-hour job looked exactly like one whose lane died. This is that signal — the same * tool-lifecycle beat the fleet view already samples, recorded on the row. * * Cheap and idempotent: a timestamp write on a running row, no-op for anything else. */ export declare function noteBackgroundAgentActivityLane(core: DurableAgentCore, id: string, now?: number): void; /** * design/164 — collect SESSION-SCOPED background agents that have gone SILENT, the replacement for the * retired 30-minute wall-clock ceiling on that lane. * * By STALL, not by age, deliberately: the old ceiling killed a child at thirty minutes whether it was * wedged or working, and "this task has run a long time" is not evidence of anything. `staleMs` is the * caller's policy — there is no default here either, matching every other retention policy in this file * (`maxAgeMs` / `keep` / `staleRunningMaxAgeMs` are all caller-supplied). Size it well above the child's * expected quiet stretches; the durable twin's guidance (≥10× the heartbeat) is the reference point. * * A collected row is settled `killed` with attribution `"system"` and the SAME wording the durable * stale-running sweep writes, so an operator reading either face sees one explanation, and its run is * aborted. Returns how many rows were collected. */ export declare function reapStaleSessionBackgroundAgentsLane(core: DurableAgentCore, staleMs: number, now?: number, onTerminal?: (note: () => void) => void): number; /** S2b — retention is REVOCABLE: every retain-ledger eviction path (TTL / LRU / abandon / * parent-teardown disposeAll) runs the entry's release closure, and the spawner wraps that closure * to call this — so a park can never be acknowledged against a continuation that was already * released (the write-once bit was a lie window during parent teardown). */ export declare function unmarkRetainedContinuationLane(core: DurableAgentCore, id: string): void; /** design/147 S2a — park a RUNNING child's live notification injector on its handle (called by the * spawner once the child's lane binds). No-op for unknown / non-agent / already-terminal rows — * the lane can outrace a fast child, and a stale attach must never resurrect a settled handle. */ export declare function attachAgentNotifyLane(core: DurableAgentCore, id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void; /** design/147 S2a — deliver a message TO a RUNNING background agent at its next turn boundary * (CC's in-memory pendingMessages pedestal). Access-checked like every registry verb; returns a * typed refusal instead of throwing so the SendMessage face can phrase honestly: * `not_running` (row isn't running any more — race with settle; caller re-reads status) or * `no_channel` (running but its lane never attached — e.g. a non-core spawner). */ export declare function deliverToRunningAgentLane(core: DurableAgentCore, id: string, access: TaskAccess, notification: import("./task-notification.js").TaskNotificationPayload, opts?: { priority?: import("./task-notification.js").SystemInjectionPriority; }): Promise<{ ok: true; disposition: "queued" | "parked" | "pending"; } | { ok: false; reason: "not_found" | "not_running" | "no_channel" | "queue_full"; }>; /** CC `ugo`/`rxo` (206:575227-575243 = 220:467937-467951): `id (label)` rows for a not-found footer — * running bg agents the CALLER can address (canAccess scopes; CC's self/observer exclusions have no * counterpart rows here). This variant is the SENDMESSAGE footer's source and lists EVERY running * agent, named or not: SendMessage addresses by name AND by id, so dropping named rows here (the way * {@link runningAgentFooterLane} does for the task faces, per CC `rxo`'s `!n.has(i.id)`) would hide the * very rows a retry needs. The task faces' split lives in `runningAgentFooterLane` (RB-332). */ export declare function runningBackgroundAgentLabelsLane(core: DurableAgentCore, access: TaskAccess): string[]; /** * The not-found FOOTER's two rows, split the way CC 220 assembles them: * - `named` = CC `uG_` (220:467941-467953): the agentNameRegistry's KEYS whose task is a running local * agent, listed as BARE NAMES — the string a retry should pass verbatim. sema's counterpart * of "has a registry entry" is the handle carrying an explicit `name` (spawn-time label), * the same field TaskStop's name leg and SendMessage address by. * - `background` = CC `rxo` (220:467937-467951): the remaining running background agents as `id (label)` * rows. CC filters `!n.has(i.id)` — a NAMED agent is deliberately NOT repeated here, because * its addressable form is the name, not the id. The pre-RB-332 sema code merged both legs * into one list and justified it with "named has no counterpart", a claim [c209-D] retired * when the explicit `name` field landed. * CC's `YEd` "Running teammates:" leg has no sema counterpart (no teammate registry on this face) and is * honestly absent rather than faked. */ export declare function runningAgentFooterLane(core: DurableAgentCore, access: TaskAccess): { named: string[]; background: string[]; }; /** The not-found TAIL both task faces share (CC `rxo`, appended by `uMs` for TaskStop * and by `xwd` 220:472095-472097 for TaskOutput). Empty string when the caller has nothing running. */ export declare function notFoundRunningAgentsTail(footer: { named: string[]; background: string[]; }): string; /** design/151 S1a (RB-425) — the SINGLE construction of a background-agent row's poll `details`. The * live in-process poll ({@link pollBackgroundAgentLane}) and the cross-restart durable fallback * ({@link serveDurableAgentRowLane}) must serve the IDENTICAL field set: that symmetry IS the S1a * contract ("a consumer cannot tell a fallback read from a live one except by the row being older than * the process"), and two hand-maintained object literals had already drifted — `error` was live-only, * so a structured consumer lost the failure FACT at exactly the moment it could no longer reach the * live handle, left to scrape the prose body's `error:` line. * * The fact gates live here too (failed ⇒ error/errorCode/retryable/retryAfterMs; `stoppedBy` rides * whenever the ROW carries it, on any status — a reaper-settled `failed` row keeps its attribution, * see the widening note in the builder) rather than at the call sites: which facts a given row may * carry is part of the same contract, and a gate copied per face is the same drift with extra steps. `error` is model/provider-influenceable text, so * the fencing + bounding (RB-386②'s posture) happens once, here, for both faces. * The parked projection is this builder with fewer facts, not a third literal. */ export interface AgentPollDetailsInput { taskId: string; status: UnifiedTaskOutput["status"]; retrievalStatus: TaskRetrievalStatus; seq?: number; stoppedBy?: StopSource; error?: string; errorCode?: string; errorRetryable?: boolean; errorRetryAfterMs?: number; resultIsPartial?: boolean; completionId?: string; /** The agent's current state ({@link import("./agent-state.js").readAgentState}) — rides as * `agentState` when given; the poll arms always give it, a serving layer may omit it. */ agentState?: AgentState; } /** PUBLIC (see the doc above): a serving layer that renders its own protocol projection of a row * builds the structured facts HERE, so the status gates and the untrusted-text fencing apply to it * too — a third literal is exactly the drift this builder exists to prevent. Input only; it reads no * store and authorizes nothing, so the caller must already have passed the row through * {@link import("./background-agent-store.js").canAccessAgentRecord}. */ export declare function buildAgentPollDetails(input: AgentPollDetailsInput): UnifiedTaskOutput; /** design/151 S1a — serve a TERMINAL durable row in the exact pollBackgroundAgent terminal shape * (same body grammar, same details fields — {@link buildAgentPollDetails} is the shared source of * the latter), so a consumer cannot tell a fallback read from a live one except by the row being * older than the process. * * PUBLIC: a serving layer answering for a row whose writing process is gone renders it through THIS, * not through a hand-written projection that would drift from the live poll's. It takes a row the * caller has ALREADY read and authorized — it reads no store and applies no predicate, so a * deployment passes every row through * {@link import("./background-agent-store.js").canAccessAgentRecord} before it gets here. */ export declare function serveDurableAgentRowLane(row: BackgroundAgentRecord, /** The row's current state ({@link import("./agent-state.js").readAgentState} over * {@link import("./agent-state.js").agentStateSourceOfRecord}(row)): rendered as the `state:` line and * carried as `details.agentState`. Absent ⇒ neither appears (a serving layer that does not read it). */ agentState?: AgentState): UnifiedTaskResult; /** * design/158 S1 (RB-205-B 半) — spill the FULL text behind a CLIPPED background_agent result to the * offload store, and append a ref-disclosure so the middle `clipTaskOutput` drops is not gone for good. * `RegisterBackgroundAgentInput` deliberately carries no `outputFile` (ref-not-path is the design/158 * §2.4 divergence — TOC/TOB compatible, unlike a local path), so this is the only pageback this lane * gets; it reuses the SAME vocabulary as the offload face (design/108: {@link OFFLOAD_TOOL_NAME}, * {@link buildToolResultRef} — RB-273's single mint point) instead of inventing a parallel one. * * Lazy: only writes on the FIRST poll whose clip actually cut something (`clipped !== full`) — a short * result never touches the store, matching design/158 §2.1 ("spill is a fidelity layer, not a new * source of truth"). Idempotent PER REVIVE CYCLE: the ref folds in `handle.reviveCycle` (a revive clears * `handle.spillRef` — see {@link reviveBackgroundAgentLane} — but `handle.id` itself does NOT change * across a revive, so without the cycle in the ref, cycle 2's spill would mint the SAME ref string as * cycle 1's and the store's write-once contract would silently keep cycle 1's STALE text under a ref * now disclosed as cycle 2's current result). Caching the minted ref on the handle (rather than just * re-deriving+re-`put`ting every poll and trusting the store's own write-once no-op) keeps the disclosed * ref byte-stable across polls and avoids a redundant round-trip to a durable backend on every poll. * * No store configured (a deployment that never wired a `toolResultStore`) ⇒ returns `clipped` UNCHANGED * — the legal degrade design/158 §2.2 calls for, byte-identical to pre-S1 behavior. * * Backlog #169 — a FAILING `put` (throwing or rejecting; the store contract admits both dialects) is * NOT fatal to the poll: the clipped text is still served, with {@link AGENT_SPILL_FAILED_NOTE} in * place of the ref disclosure (advertising a ref nobody can read back would be a false promise), and * `handle.spillFailed` latches so later polls of the same cycle repeat the honest note instead of * retrying the write — see the catch below for why latch-not-retry. Same "store failure is never * fatal" posture as the monitor/offload/budget/compaction write sites. A put that settles AFTER a * revive (stale cycle) is generation-guarded: it answers its own caller honestly but never writes * spill state onto the new cycle (see `mintCycle` below). */ export declare function spillClippedAgentResult(handle: BackgroundAgentTaskHandle, full: string, clipped: string, store: ToolResultStore | undefined, sessionId: string | undefined): Promise<string>; export declare function pollBackgroundAgentLane(handle: BackgroundAgentTaskHandle, deadline?: number, signal?: AbortSignal, /** Mirrors {@link import("./types.js").TaskSpec.oneShot} (see * {@link import("./task-registry.js").TaskToolOptions.oneShot} for the full contract): this run has * no later turn for an async background notification to land in, so the still-running body must * not teach "you will be notified when it completes". */ oneShot?: boolean, /** design/158 S1 — the offload store a clipped result's full text spills to (see * {@link spillClippedAgentResult}). Absent ⇒ legal degrade, byte-identical to pre-S1 output. */ store?: ToolResultStore, /** design/158 S1 — the caller's session id, folded into the minted ref for global uniqueness (same * role `sessionId` plays at every other {@link buildToolResultRef} call site). */ sessionId?: string, /** The run's checkpoint seat — the store a parked row's approval card is read from * ({@link import("./agent-state.js").readAgentState}). Absent ⇒ a parked row reads `approval unreadable`. */ checkpointSeat?: CheckpointSeat): Promise<UnifiedTaskResult>; export declare function stopBackgroundAgentLane(core: DurableAgentCore, handle: BackgroundAgentTaskHandle): Promise<UnifiedTaskResult>;