vicTop-cw/fist-mbt/src/executor does not have a README file

    Executor

    pub trait Executor {
    fn name(self : Self) -> String
    fn run(self : Self, task :
    Task
    , ctx : ExecContext) -> ExecResult
    }

    执行器 trait

    CliFailure

    pub enum CliFailure {
    NotExecutable
    Timeout
    NonZeroExit
    SignalKilled
    DispatchFailed
    Unknown
    } derive(Eq,
    Debug
    )

    CLI 通道(aider / atomcode / 受限自定义执行器这类本地 spawn)的失败归因档。 与 ofm 通道的 OfmFailure 同族:归不出来就 Unknown,绝不猜。

    立项理由(2026-10-23 实测):spawn 原语 run_check_external 对「进程没起来 / 跑满超时 / 被信号杀」一律以 ok=false 落地、永不 reject,而调用面此前把这三类一起说成 「可执行文件不可用」。跑满一单 atomcode 任务后回执是 ok=false / exit_code=-1 / error_tail="timeout after 900000ms" + 「不可用」提示—— 而进程活着、产物与 diff 都在盘上。「没等到」与「没装」的处置完全相反 (前者先验产物再决定要不要重派,后者去修环境),把前者报成后者会让人重派一单已经干完的活。

    CliFailure::equal

    fn CliFailure::equal(CliFailure, CliFailure) -> Bool

    CliFailure::label

    fn CliFailure::label(self : CliFailure) -> String

    回执里 failure_kind 的稳定标签面(调用方可据此分支,不靠自然语言提示)。

    CliFailure::not_equal

    fn CliFailure::not_equal(x : CliFailure, y : CliFailure) -> Bool

    ExecContext

    pub struct ExecContext {
    project_dir : String
    task_id : String
    description : String
    assignee : String
    }

    执行上下文:执行器运行所需的任务环境

    ExecContext::new

    fn ExecContext::new(project_dir : String, task_id : String, description : String, assignee : String) -> ExecContext

    ExecResult

    pub struct ExecResult {
    ok : Bool
    deliverable : String
    executor : String
    model : String
    tokens_in : Int
    tokens_out : Int
    cost : Double
    duration_ms : Int
    error_msg : String
    }

    执行结果

    ExecResult::fail

    fn ExecResult::fail(error_msg : String, executor : String) -> ExecResult

    ExecResult::ok

    fn ExecResult::ok(deliverable : String, executor : String) -> ExecResult

    ExecResult::to_json

    fn ExecResult::to_json(self : ExecResult) -> Json

    ExecResult::with_tokens

    fn ExecResult::with_tokens(self : ExecResult, tokens_in : Int, tokens_out : Int, cost : Double) -> ExecResult

    ExecutorRegistry

    pub struct ExecutorRegistry {
    executors : Map[String, String]
    abilities : Map[String, Array[String]]
    }

    注册表结构体:名称 -> 名称(既有),加 名称 -> 能力标签集合。

    ExecutorRegistry::abilities_of

    fn ExecutorRegistry::abilities_of(self : ExecutorRegistry, name : String) -> Array[String]

    查询执行器的能力标签(未注册/无标签返回空数组)。

    ExecutorRegistry::add_ability

    fn ExecutorRegistry::add_ability(self : ExecutorRegistry, name : String, a : Array[String]) -> ExecutorRegistry

    为已注册执行器登记(或覆盖)能力标签集合。

    ExecutorRegistry::auction_pick

    fn ExecutorRegistry::auction_pick(self : ExecutorRegistry, need : String, load : Map[String, Int], trust : Map[String, Double], bid : Map[String, Double]) -> Json

    Agora 置信度校准拍卖(R87,Agora arXiv 2607.09600 蒸馏):把执行者分派从"排序推荐" 升级为"按出价竞拍"。每个已注册执行者对任务所需能力 need 出价(显式 bid,缺省按 能力覆盖率自动出价),经校准系数 1 - |出价 - 历史验收通过率| 折扣——防"胜者诅咒": 过度自信(出价高而兑现差)的执行者被惩罚;再乘负载折扣 1/(1+负载) 得拍卖分。 能力覆盖 > 0 方可竞拍(能力不匹配不进入拍卖)。纯计算:need/load/trust/bid 均由 调用方注入,本层不读存储。排序:拍卖分 desc → 名称 asc。

    ExecutorRegistry::get

    fn ExecutorRegistry::get(self : ExecutorRegistry, name : String) -> String?

    获取执行器(返回名称,实际构造由调用方完成)

    ExecutorRegistry::has

    fn ExecutorRegistry::has(self : ExecutorRegistry, name : String) -> Bool

    检查执行器是否已注册

    ExecutorRegistry::list

    fn ExecutorRegistry::list(self : ExecutorRegistry) -> Array[String]

    列出所有已注册执行器

    ExecutorRegistry::new

    全局注册表(使用 Once 懒初始化)。默认注册 McpDelegateExecutor 与 manual。

    ExecutorRegistry::register

    fn ExecutorRegistry::register(self : ExecutorRegistry, name : String) -> ExecutorRegistry

    注册执行器(保留既有能力标签,无则置空)。

    ExecutorRegistry::route_pick

    fn ExecutorRegistry::route_pick(self : ExecutorRegistry, need : String, load : Map[String, Int]) -> Json

    纯计算能力路由:按 { 覆盖率 desc, 负载 asc, 名称 asc } 排序注册的执行器, 返回候选列表(每个带 name/coverage/load/abilities)。load 由调用方从 store 注入 (assignee 名下的活跃任务数),本层不读存储。

    ExecutorRegistry::route_pick_with_trust

    fn ExecutorRegistry::route_pick_with_trust(self : ExecutorRegistry, need : String, load : Map[String, Int], trust : Map[String, Double]) -> Json

    历史信任轴路由(R80,SwarmHarness 蒸馏):在 route_pick 排序里加 trust 轴—— 排序键 coverage desc → trust desc → load asc → name asc。trust 由调用方注入 (执行者历史验收通过率,无历史取 0.5 中性)。原 route_pick 签名保留零回归。

    McpDelegateExecutor

    pub struct McpDelegateExecutor {
    name : String
    }

    MCP 委托执行器(单例结构体)

    McpDelegateExecutor::new

    默认构造:名为 "McpDelegateExecutor"

    McpDelegateExecutor::with_name

    fn McpDelegateExecutor::with_name(n : String) -> McpDelegateExecutor

    命名构造(供自定义场景)

    ability_coverage

    fn ability_coverage(ab : Array[String], need : Array[String]) -> Double

    该执行器对 need 的能力覆盖率 0..1:命中所需标签数 / 所需标签总数。 need 为空(无可比性)返回 load-only 语义,score 记为 0(由调用方按负载兜底)。

    aider_argv

    fn aider_argv(prompt~ : String, model~ : String, edit_format? : String, map_tokens? : Int, encoding? : String) -> Result[Array[String], String]

    aider argv(不含可执行文件名)。与 FIST Python 版 --message/--model/--edit-format whole/ --map-tokens 0/--encoding gbk/--yes-always/--no-auto-commits 一致。 返回 Err 表示参数不合法(空提示词/含 NUL),调用方必须如实上报而不是"凑一个能跑的"。

    atomcode_argv

    fn atomcode_argv(prompt~ : String, model~ : String, workdir? : String, auto_yes? : Bool) -> Result[Array[String], String]

    atomcode argv(不含可执行文件名):-p [-C <project_dir>] [-y] [--model ] workdir~ 为空时不下发 -C(由 spawn 的 cwd 决定),避免把空串当路径传下去。

    BUG-238(2026-10-16):model~ 为空时省略 --model,atomcode 走自己 config.toml 的 default_model(如 longcat/LongCat-2.0)。为什么允许空: FIST model_route 的模型池名(ling-3.1-flash-free 等)与 atomcode provider 命名空间(longcat/deepseek/Kimi)两套名字不通,把 FIST 池名直接塞 --model 会让 atomcode 恒吃 HTTP 400 Unsupported model。server 层在 未配置模型映射时应传空串走执行器默认档;显式传非空模型名仍原样下发 (命名空间正确性由调用方自担)。aider 不同:空模型 = 绕过路由决策, 维持拒绝不变(见 aider_argv)。

    build_command

    fn build_command(executor~ : String, prompt~ : String, model~ : String, workdir? : String) -> Result[(String, Array[String]), String]

    构造完整调用(binary + args);未知执行器 → Err(不猜测、不降级)

    classify_cli_failure

    fn classify_cli_failure(error : String) -> CliFailure

    按 spawn 原语回执的 error 文本归因。文本形状是原语自己拼的、固定四类: · timeout after <ms>ms → Timeout(看门狗杀的,进程起来过) · exit code <n> → NonZeroExit;n == -1 是 node 把 code=null (信号终止/崩溃)折出来的哨兵 → SignalKilled · spawn <bin> ENOENT 一类(node err.message)→ NotExecutable · run_check_external 自己的两条 Err(调度失败 / 结果解析失败)→ DispatchFailed ——这一类坏的是我们这一侧,不是外部执行器 顺序即判据:先认「尺子自己坏了」,再认超时,再认起不来,最后才看退出码。

    cli_failure_hint

    fn cli_failure_hint(name : String, kind : CliFailure) -> String

    按归因档给出可处置的提示。只有 NotExecutable 才配说「可执行文件不可用」 (那正是 executor_missing_hint 的原有职责,原样复用不改字)。

    cli_retry_policy

    fn cli_retry_policy(kind : CliFailure) -> (Bool, String)

    换模型重试的准入判据(P1.1 fallback 链)。返回 (值得重试?, 为什么)。

    为什么要有这一格:executor_run_impl 的重试环过去只看「没成功且还有候选」, 于是三类与模型无关的失败也会被拿去换模型——NOT_EXECUTABLE 是 PATH 问题, 换哪个模型都是同一条 ENOENT,重试 N 遍就把同一次失败重跑 N 遍、还白记 N 次冷却账; DISPATCH_FAILED 坏的是本侧(进程没调度起来/结果 JSON 没解析出来),模型根本没跑过, 给它记「不可靠」是把尺子的问题算到执行器头上;TIMEOUT 是时长预算,换模型会带着 一份全新的 timeout_ms 把整单重跑,而第一跑的产物很可能已经在盘上 (2026-10-23 实测:atomcode 跑满 900s 后产物与 diff 齐全)。 这三类该给的是可处置处方(修 PATH / 立案本侧 / 提预算或拆小),不是第 N 次同样的调用。

    delegate_execute

    fn delegate_execute(task :
    Task
    , ctx : ExecContext) -> ExecResult

    便捷函数:直接委托(供 engine 调用)

    executor_binary

    fn executor_binary(name : String) -> String

    把执行器名映射到可执行文件名(与 PATH 中的命令名一致)

    executor_missing_hint

    fn executor_missing_hint(name : String) -> String

    执行器被识别但 CLI 不在 PATH 时的可诊断提示(不得静默成功)

    external_executors

    fn external_executors() -> Array[String]

    已支持的外部执行器(注册表;executor_run 工具按此校验)。 aider / atomcode 走本地 CLI spawn;ofm 走 HTTP 通道(executor_run_impl 分流, 不经 build_command)——CLI spawn 保留,HTTP 是并存第二通道。

    is_external_executor

    fn is_external_executor(name : String) -> Bool

    is_valid_executor

    fn is_valid_executor(name : String) -> Bool

    便捷函数:判断是否为合法执行器名称

    split_need_tags

    fn split_need_tags(need : String) -> Array[String]

    把 need 打散成能力标签数组:按 ,、,、空格、、 分隔,去掉空串。