import { type AskQuestionRequest, type OnQuestionOutcome, type QuestionAnswer, type AskQuestion } from "@sema-agent/core"; /** A live question frame delivered to whoever tails this run's stream. `type` IS the SSE event name (mirrors the * named-event convention; the payload also carries `type` so a proxy that strips event names still works). The shell * renders `question` as a dialog and dismisses on `question_complete`. */ export interface QuestionFrame { type: "question" | "question_complete"; questionId: string; /** "question" only: the model's structured questions, secret-redacted. UNTRUSTED-for-display (shell renders; never * re-feeds a model). Absent on an over-cap payload (the ask then reports unavailable — no frame is emitted at all). */ questions?: AskQuestion[]; /** "question_complete" only: whether the human answered (`answered`) or the ask was released unanswered * (`unanswered` — ttl/abort → core was told nobody was reachable). Cosmetic dialog-dismiss. */ outcome?: "answered" | "unanswered"; /** Server-signed emit timestamp (additive, [ref]§五→[ref]). Server-minted side-frame ⇒ no core-stream * `eventId` to carry (absence is honest); this is the frame's own time coordinate. */ serverNowMs?: number; /** "question" only ([ref]③, additive, the `tool_approval` window-trio precedent from [ref]): the * coordinator's OWN answer window (`QUESTION_TTL_MS`, default 5min) — at expiry the ask releases * UNAVAILABLE ("unanswered") and core adjudicates park-vs-continue. Minted from the SAME `ttlMs` the * `setTimeout` below actually arms (single source; a shell countdown rendered from these can never * disagree with the release). Relative + absolute + `serverNowMs` above = clock-skew-proof trio. */ expiresInMs?: number; /** "question" only: `serverNowMs + expiresInMs` — absolute deadline on the server's clock. */ expiresAtMs?: number; } /** The per-run context `onQuestion` recovers via ALS. `emit` delivers a frame to the run's live stream; `abortSignal` * (the run's own cancel signal) releases a question parked awaiting a human when the run aborts. */ export interface QuestionRunContext { taskId: string; owner: string | null; emit: (frame: QuestionFrame) => void | Promise; abortSignal?: AbortSignal; /** OPTIONAL reachability predicate for this leg's delivery face, consulted by {@link QuestionCoordinator.hasLiveContext} * (the park-vs-live adjudication probe), NOT by `question` itself. CONSTRAINT: a leg whose face can go away WHILE the * leg keeps running MUST supply it — `/v1/tasks/stream` under `x-detach-on-disconnect: true` keeps executing inside * this ALS scope after the client left, and `emit` there writes to a dead socket and is silently dropped (question * frames have no durable ledger twin, unlike SendUserFile's ledgerSink). Without the predicate the adjudication would * read "live" and hand the leg to a face nobody can reach. ABSENT ⇒ always reachable, which is exactly right for the * background leg (emit appends to the durable events tail, readable whenever a client attaches). */ deliverable?: () => boolean; } /** The per-run question window the deployment owns (core caps nothing). Env: `QUESTION_MAX_CONCURRENT_PER_RUN` / * `QUESTION_MAX_TOTAL_PER_RUN` / `QUESTION_TTL_MS` (see `ServiceConfig.questionThrottle`). */ export interface QuestionThrottle { /** Max concurrent in-flight questions per run leg (parallel tool calls can each ask). Breach ⇒ unavailable. */ maxConcurrentPerRun: number; /** Max total questions one run leg surfaces to the human. Breach ⇒ unavailable (flood defense). */ maxTotalPerRun: number; /** An unanswered question auto-releases as unavailable after this (ms) — the human walked away; don't hold core. */ ttlMs: number; } export declare const DEFAULT_QUESTION_THROTTLE: QuestionThrottle; /** Validate the respond body = core's {@link QuestionAnswer}. `selected`/`note` are OPAQUE passthrough (core fences * them: `selected ⊆ options`, `note` untrusted-fenced) — we validate only the outer SHAPE (answers[] of {header, * selected:string[], note?}), never the option semantics. */ export declare function parseQuestionResponse(body: unknown): { ok: true; value: QuestionAnswer; } | { ok: false; error: string; }; /** * Coordinates AskUserQuestion HITL for the singleton runner. Process-local + same-replica (the pending map is in memory, * like ElicitationCoordinator / steerableRuns): a respond that lands on another replica finds nothing → 404. Present * (passed into `RunnerDeps.onQuestion` + the respond route) ONLY when `ASK_QUESTION_ENABLED` — absent ⇒ core mounts the * AskUserQuestion tool with no seam at all (`seam_absent` continuation; the model just can't get a live answer). */ export declare class QuestionCoordinator { private readonly als; private readonly pending; private readonly counters; private readonly throttle; constructor(throttle?: QuestionThrottle); /** Run `fn` with the per-run question context ambient. On exit, release any still-pending question for this run (a * live-only question cannot outlive its leg → unavailable) and drop the run's counters (no leak). */ runWithContext(ctx: QuestionRunContext, fn: () => Promise): Promise; /** `RunnerDeps.onQuestion`. core calls this when the agent's AskUserQuestion tool fires; the resolved outcome is what * core acts on — a {@link QuestionAnswer} ONLY when a human really answered, otherwise `{kind:"unavailable"}` (core * then parks on a durable leg / continues with the `declined_unavailable` card on a non-durable one; either way the * run never hangs). Arrow property so it can be passed as `onQuestion: coordinator.question` with `this` bound. */ question: (req: AskQuestionRequest, signal?: AbortSignal) => Promise; /** `POST /v1/questions/:id/respond` — resolve a parked question with the shell's answer. Owner-gated with a 404 (no * existence oracle): a non-owner AND an unknown id (answered / expired / wrong replica) both get 404. Returns the * HTTP {status, body}; the HTTP layer owns auth (gatedPrincipal + REQUIRE_PRINCIPAL) before calling. */ respond(id: string, principal: string | undefined, body: unknown): { status: number; body: unknown; }; /** Test/observability hook: number of currently-parked questions. */ pendingCount(): number; /** [ref] ([ref] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条**投递得到人的** * 活流腿上(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade、durable resume 驱动=false)。 * resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒ * ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。 * 🔴 判据是「投递面此刻可达」而不只是「ALS 在场」(复审 A1):detach 腿断连后 run 仍在本作用域里跑, * 只判 ALS 会把一条谁也收不到的问题判成 allow——挂满 ttl 后以 unavailable 结算,非 durable 形 core * 就此合成续跑,活人再没机会答;而 park 才是它该走的腿。 * 谓词缺席 ⇒ 恒可达(后台腿的 durable events tail 语义)。谓词本身抛错按不可达处理:判决面 * fail-closed 到 park(park 可由运维补答,续跑掉的问题不可回收)。 */ hasLiveContext(): boolean; private countersFor; } //# sourceMappingURL=question.d.ts.map