@sema-agent/core
Version:
Stateless, task-oriented AI agent core
570 lines (569 loc) • 41.7 kB
TypeScript
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>;