@sema-agent/core
Version:
Stateless, task-oriented AI agent core
133 lines (132 loc) • 8.73 kB
TypeScript
import type { BrainRetryErrClass, BrainStatus } from "../core/types.js";
import type { ReasoningWireFacts } from "./reasoning.js";
/** Run `fn` with a per-task brain-status sink in scope. ALS propagates it through the async brain calls
* inside `fn` (the harness's prompt → brain.stream → connect/retry loop), so {@link emitBrainStatus}
* reaches THIS task's sink and nothing else. */
export declare function runWithStatusSink<T>(emit: (s: BrainStatus) => void, fn: () => Promise<T>): Promise<T>;
/**
* Report a brain-layer liveness status to the active per-task sink, if any. A no-op outside a
* {@link runWithStatusSink} scope (e.g. compaction's `complete`, or a brain used standalone).
*
* HRD-BRN-6: swallow-guarded, like its sibling {@link emitBrainTelemetry} — an ADVISORY frame must never
* change a brain call's outcome. It could: the emit forwards straight into deployment-supplied code, and
* the circuit breaker's fast-fail emits the `circuit_open` frame from a void-ed async IIFE BEFORE the
* error terminal is pushed, so a throwing sink took the whole call down (`result()` pending forever plus
* an unhandled rejection) instead of costing one status frame. `stream-engine.ts` already wrapped its own
* call site for this exact reason and states the contract ("an advisory frame must never break a settled
* call"); the guard belongs HERE so every call site inherits it rather than each remembering.
*/
/**
* #530 — bind the ACTIVE sink now, for an emit that will happen from a timer callback later. The
* async-local store propagates through Node's timers, but not through every timer implementation a
* process may run under (fake clocks in tests replace `setTimeout` outright and run callbacks from
* their own stack, where `getStore()` is empty). A frame scheduled inside the sink's scope must reach
* that sink whichever clock fires it, so the scheduler captures the store here and the callback emits
* through the bound function. Same swallow guard and the same retryAtMs stamp as {@link emitBrainStatus};
* a no-op function when no sink is active.
*/
export declare function bindBrainStatusEmitter(): (status: BrainStatus) => void;
export declare function emitBrainStatus(status: BrainStatus): void;
/**
* Silent-fallback telemetry (C1/C4/C5/C6) — the brain→runner TELEMETRY channel, a
* SECOND ALS sink parallel to the status one. Statuses are user-facing liveness frames (wire
* `status` events); telemetry is operator-facing fallback accounting the Runner bridges to trace
* events (`brain.failover` / `breaker.transition` / `brain.retry` / `vision.placeholder` / `brain.echo_dropped`). Kept
* separate so widening telemetry never bloats the shell-visible status vocabulary. Same contract:
* fire-and-forget, no-op outside a {@link runWithBrainTelemetry} scope, MUST NOT throw.
*/
export type BrainTelemetry = {
kind: "failover";
servedIndex: number;
total: number;
errorCode?: string;
} | {
kind: "breaker";
key: string;
phase: "open" | "half-open" | "closed";
failures: number;
}
/** A retried provider call. `errClass` is the same closed bucket the user-facing status frame carries
* (so an operator reading traces and a user reading a progress line agree on why); `nextDelayMs` is
* the backoff actually about to be slept — 0 for the immediate re-sends that take no backoff. Both
* optional so a caller that has no verdict to report is not forced to invent one. */
| {
kind: "retry";
attempt: number;
phase: "connect" | "midstream";
errClass?: BrainRetryErrClass;
nextDelayMs?: number;
} | {
kind: "vision_placeholder";
count: number;
}
/** PDF read: a `document` content block was substituted with a text placeholder because the serving
* API/model cannot take native document input (openai brain always; anthropic brain when the model
* declares no vision). Silent quality loss → frame (same C4 discipline as vision_placeholder). */
| {
kind: "document_placeholder";
count: number;
}
/** A provider echo field ({@link import("./errors.js").PROVIDER_ECHO_SHAPE}) refused by its shape: the value was dropped
* and the field is absent. One event per provider response per field. */
| {
kind: "echo_dropped";
field: "requestId" | "responseModel" | "apiErrorCode";
};
/** Run `fn` with a per-task brain-telemetry sink in scope (Runner-side; compose with runWithStatusSink). */
export declare function runWithBrainTelemetry<T>(emit: (t: BrainTelemetry) => void, fn: () => Promise<T>): Promise<T>;
/** Report a brain-layer fallback/telemetry event to the active per-task sink, if any. */
export declare function emitBrainTelemetry(t: BrainTelemetry): void;
/**
* Run `fn` with a per-task REASONING-WIRE-FACTS sink in scope — the THIRD brain→runner ALS channel,
* same decoupling contract as its two siblings (no brain-interface field, no per-call option, no-op
* outside the scope, an emit must never change a call's outcome).
*
* What it carries and WHY it is a channel rather than a runner-side computation: the reporting
* resolver's cap-wins arm ({@link import("./reasoning.js").budgetCapSkipsThinking}) needs facts that
* belong to ONE outgoing request — the output cap that actually reached the wire and whether it was a
* HARD bound. The runner's per-leg eager mint cannot know them without re-deriving the adapter's own
* cap precedence chain (engine override > caller `options.maxTokens` > `model.maxTokens` >
* brain-construction default) — a SECOND source of truth that a deployment's own brain, a
* construction-time default or a per-attempt engine override would silently desync. So the MINTER
* reports the facts it judged — the law `SSERequest.outputCapTokens` already states for the overflow
* recovery ("what the adapter actually put on the wire") — and the resolver stays one predicate.
*
* REVOKED ON SETTLEMENT, unlike its two siblings, because this sink WRITES to a seat later read into
* a returned value instead of pushing an advisory frame. `AsyncLocalStorage.run` does not revoke the
* store from async resources created inside `fn`: a brain call the engine ABANDONED (the guardrail's
* whole purpose) keeps the scope alive in its own continuation and can call `observe` long after the
* task settled — mutating a resolution whose `TaskResult` was already assembled and handed back. The
* scope therefore carries a liveness flag cleared when `fn`'s promise settles, which is also the
* "freeze before result assembly" boundary: assembly runs after the brain-driving call resolves, so
* a report that could still land is exactly one that arrives before the freeze.
*
* Passing a NO-OP `observe` is the documented way to SHIELD a nested internal brain call (compaction
* summary, side query): it installs a fresh innermost scope for the duration, so the inner call's
* reports are swallowed instead of restating the outer leg's posture — the same shape the status sink
* uses for the same reason ("background/internal brain calls are not surfaced").
*/
export declare function runWithReasoningWireFacts<T>(observe: (f: ReasoningWireFacts) => void, fn: () => Promise<T>): Promise<T>;
/**
* Report the reasoning wire facts of the request the engine has SELECTED to send, to the active
* per-task sink, if any.
*
* Call it from the adapter's `onCommitted` hook, never from `buildRequest`: the context-overflow
* recovery builds candidate requests speculatively and may decline them, and a declined candidate's
* facts would otherwise be reported against a request that was never sent (the exact hazard
* `onCommitted` exists for).
*
* Precisely what that buys, since the hook's own summary ("becomes the one that is sent") is a shade
* stronger than its position: the engine calls it immediately after choosing a request and BEFORE the
* attempt loop's abort check and `doFetch`, so a call aborted before its first connect can still have
* reported. The distinction that matters here is nonetheless the one the hook does guarantee — a
* DECLINED candidate never reports, so the facts always belong to the request the adapter's decision
* was made about. The residual (a selected-but-never-connected request on an aborting leg) describes
* the posture that leg was about to run under, and is not worth moving a hook three adapters share
* their own diagnostics attribution on.
*
* Fire-and-forget and swallow-guarded like its siblings — an observability fact must never turn a
* settled brain call into a failure — and inert once its scope has settled (see
* {@link runWithReasoningWireFacts}).
*/
export declare function reportReasoningWireFacts(facts: ReasoningWireFacts): void;