import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore, type ExecutionEnv, type RunnerDeps } from "@sema-agent/core"; import type { CheckpointStoreFull } from "../plugins/store-backend.js"; import { type LeaderDeps, type LeaderResult } from "./leader.js"; import { type DeploymentPostureSeats } from "../boot/runner-deps.js"; import type { LeaderRequestBody } from "./endpoint.js"; export interface LeaderWireConfig { /** Static-env compat mode (E2B only): the leader provisions/owns each env. Required when `envFactory` unset. */ e2bApiKey?: string; /** [ref] F2(a) + k8s lane: the per-task execution-env factory the service main already built for * REMOTE_EXEC (e2b or k8s/Kata) — workers and the integration sandbox run on envs from THIS factory. * Requires `s3` (diff-out is the worker's self-upload; there is no leader-held env to pull from). */ envFactory?: ExecutionEnvFactory; /** Durable suspend/resume for SUB-WORKERS ([ref] C4): when set, each worker task carries the durable * question/ask policy + checkpoint store, so a gated ask/AskUserQuestion SUSPENDS the worker (checkpoint + * paused env) instead of failing — the trigger replan-lite C4 arbitrates on. Mirrors main.ts's per-task * durable wiring; ctx is seeded at provision so the surfaced token is resumable on the single-agent path. */ durable?: { checkpointStore: CheckpointStoreFull; requireApproval: string[]; deny?: string[]; ttlMs?: number; autoBudget?: number; neverAuto?: string[]; }; /** Durable tool-result offload store (TiDB). REQUIRED for `durable` to actually suspend: core's suspend * pre-commit refuses with the default InMemoryToolResultStore (offloaded results would deref to null after * a cross-replica resume) and silently falls back to the synchronous onAsk gate — which the leader does not * wire, so the gated call is denied and the worker FAILS instead of suspending (found live: drill c3). */ toolResultStore?: ToolResultStore; /** Durable session store (TiDB). Same durable-resume requirement as `toolResultStore`: a suspended sub-worker's * session history must be readable by the MAIN runner on /decide resume — with the per-wire in-memory default * the resume dies with SessionError "Entry not found" (found live: drill c3 deny path). */ sessionStore?: SessionStore; /** * [ref] 件B/C/D([ref],codex R2-F1)—— 部署治理三座席,**原样递给本车道的每一只 Runner**。 * 主车道的 `boot/governance-seams.ts` 是唯一属主(判据、拒启、解析全在那里),本字段只是把它的产物 * 搬过来:一个进程里「这台部署禁哪些能力、锁了哪些键」只能有一个答案。缺席 = 与接线前逐字相同。 */ governance?: Pick; /** * [ref](黑板 [ref])—— **部署姿态座席族**(readFace 四席 `readFace`/`readDenyPatterns`/`readDenyBuiltinTiers`/ * `readDenyBuiltinExclude` + `memoryDelegationEvidence`/`memoryProvenance`/`memoryCapturePolicy`/ * `delegationEntryCaps`),**原样递给本车道的每一只 Runner**。唯一属主 = `boot/runner-deps.ts` 的 * `buildDeploymentPostureSeats(config)`(主 runner / subRunner / run-local 的共享基座展开的就是它), * `boot/leader.ts` 把**同一函数、同一 config** 的产物搬过来:一个进程里「这台部署的 READ 面 / 记忆姿态 / * 委派 caps」只能有一个答案。缺席 = 展开空对象 = 五处字面量键缺席(与接线前逐字相同)。 * 修前(≤7.53)这五只 Runner 一席都没有:运维写的 READ_DENY_PATTERNS / MEMORY_DELEGATION_EVIDENCE 等对 * leader 编排的 planner / worker / repair / conflict 四类任务**静默不生效**(core 内建 deny 表仍在)。 * * 🔴 **是 provider 不是快照**(S-174 追加,codex 对抗复审 r1 [high] 验真后修):本车道的 Runner 是 * **逐调用现构**的,而 S-174 起 `config.readFace` 在进程内**会变**(center 下发的 read face 转成了 * `Runner.swapDeps` 热换席)。boot 期取一次值再五处复用,等于让 leader 车道永远停在起服那一刻的 READ * 姿态:读面(`/v1/capabilities`、`/v1/diagnostics/wiring`)报新档、`restart.reasons` 也不再要求重启, * 而**新建的 leader Runner 仍在旧档上跑** —— 组织把 `open` 收紧成 `roots` 之后这条腿还在 open,方向是 * fail-OPEN。改成函数席之后每只 Runner 构造时现读,与主车道经 swap 门换代**同代**;已经准备好的腿 * 保留它准备时的那一份(core 的自然快照语义,两条腿同律)。 */ deploymentPosture?: () => DeploymentPostureSeats; /** * [ref]([ref])—— **计费/遥测追踪席**,原样递给本车道的每一只 Runner。唯一属主 = * `boot/budget-tracing.ts` 的 `createTracer(...)`(主 runner / subRunner / hook runner 经共享基座吃的 * 就是它),`boot/leader.ts` 搬的是**同一只**。 * * 🔴 为什么这一席是承重的:`src/budget.ts` 的 `createTracer` 是 `brain.call` **唯一**的记账点 —— * `costQuota.add` / `fleetUsage.record` / `fleetLease.recordSpend` / `model_cost_micro_usd_total` 四个写口 * 全在那里(`createBrain` 自己不记账)。而 `POST /v1/leader` 的准入处**读**的正是 costQuota / fleetLease * 这两个桶。缺席 ⇒ 门查的桶与花钱的手不是同一只:`MAX_PRINCIPAL_COST_USD` 与 fleet 租约对这条腿结构性 * 失效,而一条 leader run 扇出 ≤6 个带手 worker + planner + repair/conflict 环,全是实钱。 * 归因的另一半在 `leader/endpoint.ts` 的 `drive()`(ALS `withPrincipal`)—— 记账臂按 `currentPrincipal()` * 分租户,只补席不包裹 = 归因恒 undefined = 假绿。缺席 ⇒ 展开空对象(与接线前逐字相同)。 */ tracer?: RunnerDeps["tracer"]; /** * [ref]([ref],[ref]-T1)—— **部署级 token 治理窗**两键,**成对**递给本车道的每一只 Runner。 * 属主 = `config.usageWindows`(声明)+ `boot/stores.ts` 造的账本店(两者同真同假,与 * `boot/runner-deps.ts` 的共享基座同规);leader 不自己读 config。 * * 成对的理由是 core 的消费形:`prepare-task.js` 的 `buildUsageGovernance(usageWindows, deps, …)` 要求 * 声明与 `deps.usageWindowStore` **同时**在场才铸 `usageGovernance`,缺一即 undefined(既不 check 也不 * commit)⇒ 半席只是死键。缺席 ⇒ 展开空对象。 * 提交面那一半(`POST /v1/leader` 的 `usageWindowDenied` 门)在 `src/http/routes/leader.ts`。 */ usageWindows?: RunnerDeps["usageWindows"]; usageWindowStore?: RunnerDeps["usageWindowStore"]; /** * [ref]([ref])—— **center 发布的 prompt catalog** 席,原样递给本车道的每一只 Runner。唯一属主 = * `boot/config-center.ts` 的 `configCenter.promptSource`(主 runner / subRunner 同一只;run-local 有自己 * 的等价腿)。缺席 ⇒ leader 的 planner/worker/repair/conflict 四类**顶层**任务跑引擎内置提示词,而同一 * 部署的 `/v1/tasks`、`/v1/runs` 跑运维发布的那一份 —— 且 core 顶层新任务走的是**静默**臂(pinned / * parentCenterArtifactDigest 两支才 `prompt.snapshot_unavailable` fail-loud),运维面零信号。 * 缺席 ⇒ 展开空对象(与接线前逐字相同)。 */ promptSource?: RunnerDeps["promptSource"]; /** * [ref] 的**披露半件**([ref],codex 对抗复审 r2-[high],亲读装树 core 后修)—— 部署级错误汇。 * * 🔴 为什么它与治理窗席**同生共死**:core 对「治理窗最终扣账失败」的唯一出口就是这一席 —— * `dist/core/runner/runtask.js` 收尾段 `catch (flushErr) { this.deps.onError?.(flushErr, { phase: "config", … }) }` * (以及 10s 慢盘披露的同一只回调),抛错之后任务**按原结果收尾**。挂了窗席却不挂本席 = 一次窗账 * 永久丢失时这条腿上零日志零计数,后续提交继续读偏低的窗被放行 —— CLAUDE.md [ref]「治理/门轴禁静默 * fail-open」正是说这个。它顺带也把 leader 车道其余 phase(degraded / rewind / memory / mcp / a2a / * prompt-cache …)的既有观测面补齐,与 `onNotice` 席当年([ref]②)同一个理由。 * * 属主唯一 = `boot/runner-deps.ts` `createRunnerDeps` 铸的那一只;`boot/leader.ts` 直引 * `runnerDeps.onError`(**同一实例**,[ref]§三族A 的最强同源形,与 main.ts subRunner 的先例逐字同姿势), * leader 绝不另写一份臂表(两份必漂)。缺席 ⇒ 展开空对象(与接线前逐字相同)。 */ onError?: RunnerDeps["onError"]; /** Worker model brain + catalog (the same the service runs tasks on). */ brain: Brain; models: Record; roles: ModelRoles; pricing: Record; /** Workspace root inside each E2B VM. Default /home/user. */ workspace?: string; /** Git identity for the Coordinator's local clone/commit. */ git?: { name: string; email: string; }; /** [ref] A6 knob: false = router never fans out (deterministic single route, zero router cost). */ fanoutEnabled?: boolean; /** The model (a catalog name) the ROUTER/planner call runs on. The route is a trivial single-vs-fanout * classification; running it on a heavy reasoning model (deepseek-v4-pro + reasoning) returns an EMPTY * completion ~2/3 of the time — the budget is spent on reasoning with no answer text → "not valid JSON" → * collapse-to-single, silently defeating fan-out (search 2026-06-14, reproduced 3×). A cheap non-reasoning * model (deepseek-v4-flash) routes correctly 51/51 in probes. Unset → the route runs on the default model. */ routerModel?: string; /** Max bounded integration-repair rounds on a clean-merge-but-failing-build (see merge.ts IntegrationRepair). * 0 / unset = OFF (a failing integration test is terminal, the pre-2026-06-14 behavior). Env override: * LEADER_REPAIR_ROUNDS. Repair runs the strong (default) model with hands in the live merged sandbox. */ repairRounds?: number; /** [ref] §8 F2(a): when set, workers run on FACTORY envs (runner-owned lifecycle — the durable-suspend- * complete path; a suspended worker's paused env is exempt from the leader reap by construction) and diff-out * is the worker's mandatory self-upload to this MinIO/S3 store (presigned per worker; creds adapter-held). * Unset = the original leader-owned static-env mode (pullDiff compat). */ s3?: { endpoint: string; bucket: string; accessKey: string; secretKey: string; region?: string; keyPrefix?: string; }; logger?: { warn?: (m: string, x?: Record) => void; info?: (m: string, x?: Record) => void; }; } /** The env surface wire needs (both the E2B and k8s adapters implement core's RemoteExecutionEnv). */ type WireEnv = Pick; /** * [ref] —— leader 自铸沙盒的**回收闸**(集成沙盒 / repair-loop grader 各一只)。 * * core 契约:`ExecutionEnv` 基面**没有** `destroy`;只有实现了 `RemoteExecutionEnv` 的适配器才有 * (`core/remote-env.d.ts`),而「这只 env 到底有没有」是**运行期结构事实**,core 自己也只用结构探测 * `hasDestroy()`(:487,runtask/prepare-task 全部 destroy 站点都先过它)。修前 leader 三处把它写成编译期 * 断言(`as … & { destroy(): Promise }`),于是 TS 对「手里其实是个 **Promise**」完全无感—— * `env.destroy()` 抛 `env.destroy is not a function`,还把**原始错误盖掉**([ref] 现场)。 * * 🔴 缺席臂 = **fail-closed,当场响亮拒**([ref]:执行车道禁静默兜底;codex r2-[medium] 采纳)。理由: * · core 之所以容忍「工厂产物没有 destroy」,是因为工厂**可以**交回一只**共享静态** env(`withWorktreeIsolation` * 的非隔离臂就是这一形)——那种 env 本来就不该被谁拆掉。但 leader 这两只是**专用、临时、只属于它自己** * 的沙盒:唯一的拆机口就是本闭包(`mergeBranches` / `runRepairLoopForLeader` 在 `finally` 里调它)。 * 收不动 = 一只计费中的沙盒必然留到后端自己的 timeout,零上行收益 ⇒ 没有「合法兜底」可言。 * · 静默 no-op 反而比**修前**更松:修前同步工厂路径上,一只没有 destroy 的 env 会当场抛 TypeError。 * 本闸只是把那个抛**提前到 provision 时刻**,并换成一句点名沙盒与成因的话——不是新增严格度。 * · 当前三条已接线的 leader 车道交回的 env 都实现了 destroy(e2b `RemoteContainerExecutionEnv`、 * k8s、host `RemoteHostExecutionEnv`——`hostExecutionEnvFactory` 恒 `new RemoteHostExecutionEnv`), * 所以这条臂是**防御性**的:它真响的那天,意思是有人接了一条新工厂车道而 leader 收不动它的沙盒。 */ export declare function createEnvReaper(env: ExecutionEnv, what: string): () => Promise; /** * Resume-safe worker staging (the [ref] F2(a) factory-env path). Runs adapter-side before the agent's * first workspace-touching call: seed the base repo at the worker branch + plant the upload script (so the * presigned URL never passes through the model). * * 🔴 BL-1 fix ([ref] Slice 6): on a RESOURCE-SUSPEND RESUME core re-invokes the `executionEnvFactory`, * producing a FRESH `withStaging` wrapper (`staged=undefined`) whose `stage()` runs AGAIN — AFTER core's * `resumeVM()` has already untar'd the snapshot onto the fresh pod (`resumeVM` is not a STAGED_METHOD, so it * is NOT intercepted by the staging proxy; the agent's first `exec` is what triggers `stage()`, lazily, and * by then the snapshot is restored). The old `stage()` ran the seed UNCONDITIONALLY: * `${seedCmd}; cd ${repo}; git checkout -q -b ${branch}` * and `seedCmd` begins with `rm -rf /workspace/repo` — so it WIPED the just-restored snapshot and re-seeded at * `baseSha`, then created the worker branch at base. The forensic proof (leaderRunId 019ecda7): the suspend * snapshot object DID contain the worker's commit (`git log` in the S3 tarball = the real progress), yet every * resumed slice's `git log` showed only the base commit — the worker rebuilt destroyed work for ~47 min. THIS * (a staging re-seed clobbering a correctly-restored snapshot) is the dominant cause of failed make-real runs. * * The fix = a RESTORED-SNAPSHOT SENTINEL that no-ops staging on resume. The robust signal is the upload-script * MARKER FILE (`uploadScriptPath`): staging itself plants it, so it is present in EVERY restored snapshot * (verified: it survives in both a base-state in-progress snapshot AND a post-commit one) and ABSENT on a * genuinely fresh pod — and it is INDEPENDENT of git/commit state. That last point matters: a worker can * suspend with work in progress but no commit yet (HEAD still == baseSha), so a "HEAD advanced past baseSha" * probe alone would WRONGLY re-seed+clobber that valid snapshot. The marker captures "staging already ran here" * regardless of whether a commit landed. We additionally accept the worker branch already existing as a * secondary restored-snapshot signal (belt-and-suspenders for the same git-state-independent reason). * * A FRESH pod has neither marker nor branch → full staging runs. A probe transport failure falls through to * full staging too (fails honestly via `sh()`, never silently skips). `branch`/paths are `validateSubtasks`- * clean / deployment config and are JSON-quoted under `bash -lc` regardless. Exported for the regression test. */ export declare function stageWorkerEnv(env: WireEnv, opts: { seedCmd: string; repo: string; branch: string; uploadScriptPath: string; uploadScript: string; }): Promise; /** * Per-leader-run resource bounds derived from env (2026-06-14). Exported for unit tests. The worker sandbox * lifetime (BUG2), the worker's diff-upload presigned-URL TTL (BUG1), and the worker spec's triple bound * (maxTurns/timeoutSec/maxCostUsd, #15) ALL track one `leaderTimeoutMs` — a hardcoded 10min (sandbox) or 1h * (URL) silently killed long autonomous builds or expired the diff upload mid-run. core's turn cap is now a * high net (10000), so the leader gives each worker an explicit triple bound; a sub-task's own limits override. */ export declare function leaderResourceConfig(env?: NodeJS.ProcessEnv): { leaderTimeoutMs: number; workerMaxTurns: number; workerBudgetUsd: number; workerLimits: Partial; presignTtlSec: number; /** [ref] Slice 6: opt-in (LEADER_RESOURCE_SUSPEND=true) resource-slice suspend for workers. When set, a * worker SUSPENDS at the per-slice window (`sliceMaxCostUsd`) instead of failing at the budget, and the * leader auto-resumes (resumeWithVerification) until the TOTAL (`totalBudgetUsd` = workerBudgetUsd) is spent * — a budget overrun becomes a resumable pause, not a worker failure. ⚠️ 语义更新(core 5.11.0 B3, * 2026-08-06):`maxSuspends` 现在只数「连续无进展」park(批准+执行后链基归零),不再是总 park 数上限 * ——下面按预算切片总数放大的 sizing 公式因此从「必要」变「保守冗余」(合法的资源续跑链永不触 cap, * cap 只剩防空转环职责);公式保留是无害的(上限只会更松不误杀),别再按「总数预算」理解它。 * 原注:`maxSuspends` is sized to the slice * count (+margin) so the suspend-loop cap doesn't fire before the budget is legitimately spent. */ resourceSuspend?: { totalBudgetUsd: number; sliceMaxCostUsd: number; maxSuspends: number; }; }; /** Repair/replan-loop knobs derived from env + optional `LeaderWireConfig` fallbacks (`repairRounds` lands in * cfg when the caller wires it programmatically instead of via env). Split out of * `createLeaderRunner`'s closure so it's a plain, unit-testable data constructor. */ export interface LeaderLoopConfig { repairRounds: number; repairBudgetUsd: number; repairLoopOn: boolean; measureGatesOn: boolean; repairLoopAttempts: number; oracleFlakyK: number; /** replan-lite ([ref] §6) fan-out spend cap from LEADER_BUDGET_USD. Absent = replan-lite's own default * (unset env stays absent — existing contract untouched; see leaderLoopConfig doc below). */ replanBudgetUsd?: number; } export declare function leaderLoopConfig(cfg: { repairRounds?: number; }, env?: NodeJS.ProcessEnv): LeaderLoopConfig; export declare const LEADER_PUSH_NETWORK_TIMEOUT_MS = 120000; export declare const LEADER_PUSH_LOCAL_TIMEOUT_MS = 30000; /** Coordinator(control plane,持有推送凭据)的 git push 闭包——从 `createLeaderRunner` 抽出为独立工厂, * 仅依赖 durableRemote/targetRef/git 身份(不依赖 wire 内其余状态),因此可以脱离完整的 * plan→provision→fan-out→merge 管线单测(那条管线需要真 brain + E2B/k8s env)。行为与抽出前逐字相同, * 唯一新增是六条 execFileSync 各自的 `timeout`。 */ export declare function createCoordinatorPush(durableRemote: string, targetRef: string | undefined, ident: { name: string; email: string; }): LeaderDeps["push"]; export declare function createLeaderRunner(cfg: LeaderWireConfig): (body: LeaderRequestBody) => Promise; export {}; //# sourceMappingURL=wire.d.ts.map