import { type WebFetchConfig, type BackgroundAgentStore, type CheckpointStore, type PromptProvider, type Runner, type SkillSpec, type SubagentSpawnContext, type TaskListStore, type ToolSpec, type WebSearchConfig } from "@sema-agent/core"; import type { Metrics } from "../observability/metrics.js"; import type { Logger } from "../observability/logger.js"; import { type RepoReadClient } from "./repo-tools.js"; import { type LoadedSkill } from "./skills.js"; import { type ScenarioHands } from "./hands-lane.js"; /** * Scenario routing — a deployment serves many scenarios from one image. Each request's `scenario` * selects a pre-wired capability bundle: tools (incl. subagent roster) + a whole-harness prompt + * skills. Heavy bits (HTTP clients, skill files) are startup singletons; per task we only bind * coordinates and build cheap closures, so there is no per-request "assembly" cost. */ export interface ScenarioRequest { scenario?: string; repo?: string; [k: string]: unknown; } export interface ScenarioBundle { tools: ToolSpec[]; skills: SkillSpec[]; /** * [ref]:本场景对 core 手带工具面(Bash/Edit/Write/Read/Grep/…)的表态 —— **必填闭集**。 * `full` = 任务跑在挂了 `executionEnvFactory` 的 Runner 上(core 把 band 并集进 roster); * `none` = 跑在不挂 factory 的 Runner 上(band 根本不 mount,工具 schema 对模型不可见)。 * 必填是设计的一半:新场景不表态 = 编译红,而不是静默继承 full(那正是 [ref] 的病灶—— * `scan` 声明 clone-free 四件、实际拿到全量可写手)。 */ hands: ScenarioHands; promptProvider?: PromptProvider; /** [ref] 场景解释层定死的终验开关(core TaskSpec.finalVerification):bench 实测产品默认请求与 * bench 态差距三件之一。[ref] 起无场景定死(autonomous 退役)——纯 caller 显式旋钮;spec 组装处 OR 折入(场景只会 * 多加一轮收官验证,绝不收窄 caller 意图),壳/caller 不感知。 */ finalVerification?: true; } /** * [ref]:场景工厂的**服务端事实**上下文 —— 与 `req`(= 调用方请求体,不可信)分开的第三个入参。 * 这里的每一位都由鉴权信道解析,调用方无法自报;新增位一律走本接口,绝不往 `req` 上加。 */ export interface ScenarioContext { /** * 本请求的会话 id。属主 = `boot/resolve-spec.ts` 的 `auth.sessionId`(authorizer 解析 + 归属校验 * 过的那一只),**绝不是 `body.sessionId`** —— 后者是调用方自报串,拿它当会话级设施的分区键 * 等于让一个租户读另一个租户的清单(同 spec 组装处那条「sessionId comes from `auth`, NEVER the * body」的逐字纪律)。 * * 缺席 = 这条调用形本来就没有会话(`main.ts` 的 parkedReviveTool 只取裸 Agent 工具、 * `builtinScenarioDetails` 的工具名探针)。此时会话级设施退化为 per-call 实例 —— 不为它发明 * 一个假 session。 */ sessionId?: string; } export type Scenario = (req: ScenarioRequest, principal?: string, ctx?: ScenarioContext) => ScenarioBundle; export interface ScenarioDeps { runner: Runner; /** In-memory runner for ephemeral sub-tasks (council lenses/arbiter) — keeps them out of TiDB. */ subRunner: Runner; /** * [ref]:`subRunner` 的无手孪生 —— 同 deps,唯独不挂 `executionEnvFactory`。hands=none 的场景把它交给 * 自己造的子任务工具(council 的 lens/arbiter、discuss 的成员/synthesizer):那些子任务只该读仓库/讨论, * 拿全量可写手是 [ref] 病灶里最意外的一支(零工具人格照拿 Bash)。取用一律走 {@link pickHandsRunner}, * 不在场景体内手挑 —— 判别位是唯一开关。 */ handslessSubRunner: Runner; model: string; skills: LoadedSkill[]; repoClient?: RepoReadClient; /** Multi-tenant deployment (REQUIRE_PRINCIPAL). Gates the full-body `WebFetch` tool out of the * default roster — core says a multi-tenant deployment MUST allowlist hosts (an injected/untrusted tenant model * could exfil via an arbitrary public URL; the SSRF floor only blocks internal). Single-user TOC keeps WebFetch. */ requirePrincipal?: boolean; /** Deployment-injected WebSearch backend (Brave/Tavily/SearXNG via `WEB_SEARCH_PROVIDER`). When set, * `assembleCodeTools` mounts the WebSearch tool; absent ⇒ not assembled (core ships no search backend). */ webSearch?: WebSearchConfig; /** [ref]/[ref] WebFetch 摘要器(core `makeWebFetchSummarizer(brain, summarize 角色模型)` 的产物, * main.ts 构造一次注入)。在场 ⇒ 装配实参携 `webFetch.summarize`——带 prompt 的 WebFetch 走 cheap * 模型摘要而非整页灌上下文([ref] L1 的 ~660× 放大就是这缝);缺席 ⇒ `webFetch` 键不铸(修前 * 字节形,core 落「summarization unavailable」诚实兜底)。 */ webFetchSummarize?: NonNullable>; metrics?: Metrics; logger?: Logger; /** * When true, the `default` scenario carries the Sema product identity (see {@link semaIdentityProvider}). * Set by the local/TOC deployment (CONFIG_PROVIDER=local / run-local) so the local engine answers "who * are you" as Sema instead of the model's hallucinated identity. Business scenarios (discuss/code-review) * keep their own prompts; cloud (CONFIG_PROVIDER≠local) leaves the default scenario neutral unless turned * on, so integrators that inject their own persona are unaffected. */ brandIdentity?: boolean; /** * core 1.287 `SubagentToolOptions.extraTools` seam wire-through — per-SPAWN dynamic tool injection into the * delegated (Agent-tool) child roster. Deployment-injected per-task tools (today: SendUserFile — its instance is * execute-time late-bound via ctx.taskId → TaskEnvRegistry, so ONE spec serves every spawn) cannot ride the * ASSEMBLY-time static `subagent.tools` pool; this factory is evaluated by core at each spawn and its product is * merged into the pool, then goes through core's `resolveToolSubset` whitelist like any other tool (agent-def * allowTools/denyTools still govern — no extra gate needed on our side). Absent ⇒ no dynamic injection (children * see exactly the static full-body roster, the pre-1.287 behavior). */ subagentExtraTools?: (ctx: SubagentSpawnContext) => ToolSpec[]; /** * core 1.364 durable background agents([ref] S1,[ref] 提货单②写半场):后台子代 `a*` 行的 * durable 执行记录 store。**必须与 `RunnerDeps.backgroundAgentStore` 同实例**(读写双点配对,engine * 无法核对——半接=静默死特性,[ref]①)。绝不在这里另起实例。absent ⇒ pre-151 行为(settle 后 * eager release,读面纯 live)。 */ backgroundAgentStore?: BackgroundAgentStore; /** * core 1.382([ref] 二拍B 件1+件2,parked 状态机)[ref] 提货单①②:「同车必接」的一对——core * `parkEligible` 门要求 checkpointStore 与 ensureChildSessionDurable **全在场**才成立,任一缺席 = 净是 * pre-153 行为(子代 SAFETY ask 照旧 deny+continue,零回归)。 * - `checkpointStore` 必须与 `RunnerDeps.checkpointStore`(主 runner 的 per-task spec 面)**同实例** * ——子代 suspend 在那里铸 token,异实例 = park 全静默失效(core JSDoc 钉过症状,组装区注释同步)。 * - `ensureChildSessionDurable` = park 提交前的 capability 挂点(core 签名 `(sessionId) => Promise`) * ——resolve = 该子代 session 已从任意进程可读,reject/缺席 = veto(子代按 pre-153 settle failed, * checkpoint 被 expire)。**本车保守实现**:只在子代 session 已经真正路由到宿主 durable 会话店 * **且真有内容**(`getLeafId() !== null`——排除 F-7 同族的空壳 claim-create 行,那种行 acquire * 命中但零内容,是`register()`对任意自报 sessionId 的副作用,不是真正 durable 的会话)时 resolve * (=fork 续聊的常见形),其余一律 reject——不做「主动迁移私店会话进 durable 店」这一步 * (ForkRoutingSessionStore 的路由表是私有状态,无迁移 API;主动迁移是更大的活,candidate 留给 * 后续按需再做)。 */ checkpointStore?: CheckpointStore; ensureChildSessionDurable?: (sessionId: string) => Promise; /** * [ref]([ref] F2 症状 → [ref] 定谳):**会话级**任务清单店解析器,属主 = * `boot/task-list-lane.ts` 的 {@link import("../boot/task-list-lane.js").createTaskListLane} * (boot 期构造一次,SQL 后端在场接对应 twin、否则进程级 per-session 内存店)。 * * 🔴 这是 F2 的修点:core 的 `assembleCodeTools` 不收 `taskListStore` 时落私有 per-run 内存店, * 而本仓的场景工厂是**每请求**调用的 ⇒ 同一会话每个 turn 一只新店(TaskList 中途清空、 * TaskCreate 从 #1 重铸号)。缺席(嵌入式装配自己造 deps、未接本车道)⇒ 逐字回到修前行为, * additive 不回归。 */ taskListStoreFor?: (sessionId: string) => TaskListStore; } /** * Sema product identity, prepended to the core default base for the `default` scenario when * {@link ScenarioDeps.brandIdentity} is on. Honest about the substrate (the engine runs whichever model * the operator configured — not Claude/Anthropic), so the agent never inherits the model's training-time * "I am Claude" hallucination. * * Live-caught incident (a deployment served by one model family described itself as running on a DIFFERENT * vendor's model, because that vendor's name was baked into this text): * NOTHING substrate-specific may be hardcoded here — onboard is multi-provider (any OpenAI-/Anthropic- * compatible endpoint, self-hosted vLLM included), so a baked maker name makes every deployment that is not * that maker's misstate its own substrate. Same for * the gateway protocol (the brain stack serves Anthropic-compatible routes too — routing brain + * createAnthropicBrain — so "OpenAI-compatible gateway" would be the same class of lie). Neutral wording * (zero core-seam need; interpolating the model id would need a `StablePromptContext.model` * seam and reads poorly for ugly preset ids). The anti-"I am Claude" design is kept — only the baked * substrate facts are removed. */ export { SEMA_IDENTITY } from "./prompts/identity.js"; /** * Prompt provider for the branded `default` scenario: SEMA_IDENTITY on top of the ROLE base — the * task's own `body.systemPrompt`, or core's {@link DEFAULT_SYSTEM_PROMPT} when none. Core 1.243 * (提示词主权批) appends the constitution structurally AFTER this role layer (harness * safety blocks + modes + memory guidance + the prefix-cache-safe `` tail), so identity * cannot displace the safety blocks. The pre-1.243 form (identity + `defaultPromptProvider.stableSystem`, * i.e. an ALREADY-assembled prompt) would now trip the migration guard (anchor-detect → pass-through + * `onError(phase:"prompt-constitution")`) — this role-only form is the guard-free contract. */ export declare function semaIdentityProvider(): PromptProvider; /** * `code` 场景的提示词提供者([ref] 造,[ref] clay 裁定改名 autonomous→code):role 基底 = core * {@link CODE_SYSTEM_PROMPT}(core 1.298 [ref] 改名批正名,旧名 FULL_BODY_SYSTEM_PROMPT=恒等 * 别名过渡窗;CODE_AGENT persona + AUTONOMY_SELF_AUDIT + ANTI_VERBOSITY + * TOOL_PARAM_JSON;[ref] 谱系定论 = CC 生产编码提示词的蒸馏去牌版,底本随 CC 版本对表 cc-anchors, * 非自研)——与 bench 的请求组装完全同源,引用 core 常量随版本走,绝不复制字符串(tb bench 实测: * 产品默认请求与同引擎 bench 态的差距根因就在这三件 persona/自检/终验)。品牌层与 * {@link semaIdentityProvider} 同构:brandIdentity 开时 SEMA_IDENTITY 叠在 role 基底之上;任务自带 * systemPrompt 按同一优先序赢过基底(role 合成规则与 default 场景一致)。宪法由 core 1.243 在 role * 层之后结构化追加——此处只产 role-only 形态(guard-free 契约,同 semaIdentityProvider 注释)。 * 无 systemPrompt 且无品牌时原样返回 core 常量(测试以引用比对锁 bench 同源,不做字符串快照)。 */ export declare function codePromptProvider(brand: boolean): PromptProvider; export declare function buildScenarios(deps: ScenarioDeps): Record; /** Resolve the scenario for a request (falls back to `default`). */ export declare function selectScenario(scenarios: Record, name: string | undefined): Scenario; /** web 渲染契约(定稿形状;knobs 现在没有=整字段省略)。 */ export interface ScenarioDetail { name: string; /** selectScenario 实际用哪份定义(center 同名 shadow 时="center")——与 centerScenarios overlay 同判定源。 */ source: "builtin" | "center"; /** 是否存在同名内建定义(被 shadow 时仍 true,web 画锁徽章+shadow 标注)。 */ builtin: boolean; summary: string; toolset: string; tools: string[]; /** 内建=概览(不外泄提示词资产全文);center 条目=prompt 原文(本就 ≤4KB 明文在配置域)。 */ promptSummary: string; /** **已声明**(内建恒 true;center 条目 `enabled !== false`)。⚠️ 不是「跑得动」——那是 {@link available}。 */ enabled: boolean; /** * [C132] **本部署现在真跑得动吗**:后端依赖(如 repo 型场景的 `GIT_API_BASEURL`)是否就位。 * 与请求面的 501 拒绝臂**单一属主**({@link scenarioAvailability});false ⟺ 良性请求真吃 501。 */ available: boolean; /** 机读原因键;仅 `available:false` 时在场(缺席=没有理由)。消费方按键分支,禁匹配英文文案。 */ unavailableReason?: ScenarioUnavailableReason; } /** 内建五场景详情。default/code/discuss 工厂对良性请求无副作用可真调(拿真实工具名单);code-review/scan 是 * fail-loud 语义(缺 GIT_API 配置/principal 即 throw)→ 探针失败落静态表兜底(表↔工厂一致性由测试锁: * 测试喂 fake deps 真调工厂对账工具名)。enabled 对内建恒 true(约定②)。 */ export declare function builtinScenarioDetails(scenarios: Record, deps: Pick): Record; /** center 条目详情(有效性判定与 centerScenarios 完全同款:无效条目既不进 overlay 也不进详情—— * 保证约定①「详情显示的来源=运行实际用的定义」永不错位)。 */ export declare function centerScenarioDetails(specs: CenterScenarioSpec[] | undefined, builtinNames: string[], deps: Pick): Record; export interface CenterScenarioSpec { name: string; toolset: string; prompt?: string; enabled?: boolean; } /** 场景不可用的**机读原因**(闭集)。新增成员必须在 {@link scenarioUnavailableMessage} 的穷举 switch * 里表态——漏表态是编译错误,不是运行期 miss 臂。消费方按这个键分支,禁去正则匹配英文文案。 */ export type ScenarioUnavailableReason = "git_client_unconfigured"; /** 判别式:可用臂**不带**原因键(缺席=没有理由),不可用臂必带。 */ export type ScenarioAvailability = { readonly available: true; } | { readonly available: false; readonly reason: ScenarioUnavailableReason; }; /** * 🔴 场景可用性的**唯一属主**。列举面(`ScenarioDetail.available` / `unavailableReason`)与请求面 * ({@link requireRepoClient} 的 501 拒绝臂,三处调用点)都只从这里取值——两处各写一份就是本仓反复 * 吃过的「同一语义两个属主」病:判据一漂,列举面开始说谎而没人先红。一致性由 capabilities.test 的 * **对表格**逐名钉住(`available:false` ⟺ 良性请求真吃 501),而不是靠这段注释。 * * 判据键 = **toolset**:内建详情与 center 条目都带这个字段,故两条产线天然共用同一份判据。 * `requiresRepo` 的 toolset 需要部署配好 git 后端(`GIT_API_BASEURL` ⇒ `deps.repoClient`)。 * 词表外的 toolset(内建的 `full-body`/`discuss`)不依赖后端 ⇒ 恒可用。 */ export declare function scenarioAvailability(deps: Pick, toolset: string): ScenarioAvailability; /** 拒绝文案的唯一属主:闭集穷举 switch(新增原因词不在这里表态即编译红)。 */ export declare function scenarioUnavailableMessage(reason: ScenarioUnavailableReason, scenarioLabel: string): string; export declare const SCENARIO_NAME_RE: RegExp; /** * Build the center-declared scenario overlay. Invalid specs are SKIPPED with a warning (never fail * boot — same posture as skills/teams); valid ones become scenarios keyed by name, ready to be merged * OVER the built-ins (center wins). Returns the overlay plus which built-in names it shadows. */ export declare function centerScenarios(deps: ScenarioDeps, specs: CenterScenarioSpec[] | undefined, builtinNames: string[], logger?: Logger): { overlay: Record; shadows: string[]; }; /** * Personalization seam: merge the caller's own skills AFTER the scenario's, with * SCENARIO-WINS on a name clash — the security baseline (scenario/center skills) can never be shadowed * by user content. Duplicate names within the user list keep the first. Clashes are logged for audit * (silent drops would make "my skill didn't fire" undebuggable). Input is already shape/size-validated * by the HTTP layer (validateUserSkills); this stays pure merge policy. */ export declare function mergeUserSkills(scenarioSkills: SkillSpec[] | undefined, userSkills: SkillSpec[] | undefined, logger?: Logger): SkillSpec[] | undefined; /** center caps view 的解析结果两键(形状锚定)。`scenario` null = 三层皆未配。 */ export interface ScenarioRuling { scenario: string | null; /** 纯场景名集合(principal/组名不上 wire);空 = 该 principal 无任何指派 = 无治理。 */ allowlist: string[]; } /** * resolveSpec 的执法入口(指派为默认、body.scenario 可换但限 allowlist)。返回本请求的 * 生效场景名(未做 unknown→default 回退——那是 resolveSpec 既有职责,保持单点)。 * * - body.scenario 显式给出:allowlist 非空且集外 ⇒ typed 拒绝(400 `scenario_not_allowed`,附 allowlist * ——机器码归 wire、人话归壳/web);集内 ⇒ 用户主动切换赢。allowlist 为空(无 ruling / 该 principal * 三层皆无指派 / caps 面故障)⇒ 无治理约束,现状放行(与「null 落兜底链」同一语义:分配面未覆盖 = * 不收紧,恒不比没有治理时更宽;caps 故障不把用户锁死在 default——场景切换不是权限放大面,fail-open * 到现状,与 workflows 面的 fail-closed 是刻意的 posture 差异,见 runtime-caps-resolver)。 * - body.scenario 未给:ruling 胜者 ⇒ 指派为默认;无胜者 ⇒ defaultScenario 现状链。 */ export declare function gateScenarioRequest(ruling: ScenarioRuling | undefined, bodyScenario: unknown, defaultScenario: string, known: readonly string[], logger?: Logger): string; //# sourceMappingURL=scenarios.d.ts.map