/// engine_dispatch.mbt —— 引擎层 store-backed 自治派单 primitive(R56)
///
/// 自治闭环的最后一块地基:把"给某个执行者派单"下沉到 engine 层、直接读 store 持久化的
/// 执行者能力注册(executor_list),复用 executor 包的 `route_pick` 纯计算路由(单真源),
/// 不经 server 进程内 `exec_reg`。纯增量:新增 pub 函数,不改任何既有工具/看门狗行为(零回归)。
///
/// 这样 `watchdog_tick`(ops 层有 engine 引用)即可在显式 ns 无人值守场景直接调用
/// `engine.dispatch_next(...)` 实现"按能力/负载自动派单",而不再受 server 全局 exec_reg 耦合。
///|
/// 解析执行者能力注册(abilities_json 为 JSON 字符串数组)-> Array[String]。
fn parse_abilities(abs_json : String) -> Array[String] {
let out : Array[String] = []
let v = @json.parse(abs_json) catch { _ => return out }
match v {
Array(a) =>
for x in a {
match x {
String(s) => out.push(s)
_ => ()
}
}
_ => ()
}
out
}
///|
/// 计算每个执行者的活跃负载:名下"未完成且未归档"的任务数(由 store 分配状态推得,纯读)。
fn FistEngine::executor_loads(
self : FistEngine,
names : Array[String],
) -> Map[String, Int] {
let m : Map[String, Int] = Map([])
for n in names {
m.set(n, 0)
}
for t in self.list_all() {
match t.assignee {
Some(a) =>
if m.contains(a) {
let done = t.get_status().is_completed() ||
t.get_status().to_string() == "已归档"
if not(done) {
m.set(a, m.get(a).unwrap_or(0) + 1)
}
}
None => ()
}
}
m
}
///|
/// 读 Json 对象的字符串字段(内部辅助)。
fn _jstr(j : Json, key : String) -> String {
match j.value(key) {
Some(Json::String(s)) => s
_ => ""
}
}
///|
/// 从任务描述自动抽取所需能力标签(R58,复用 R33「免手传」思路、store-backed):
/// 扫描述里出现的「store 持久化执行者能力标签」,取最长命中者作 need(最具体)。
/// 没命中返回空串(调用方回退 agent / 空 want)。不依赖 server 进程内 exec_reg。
fn FistEngine::auto_want(self : FistEngine, desc : String) -> String {
let mut best = ""
for (_name, abl, _created) in self.executor_list() {
for a in parse_abilities(abl) {
if a != "" && desc.contains(a) && a.length() > best.length() {
best = a
}
}
}
best
}
///|
/// 引擎层自治派单(纯增量、零回归):取 `triage` 顶部可领取任务 → 用 store 持久化执行者能力
/// 注册经 executor 包 `route_pick` 按「能力覆盖 → 负载」路由最佳执行者 → 将任务直接认领给该执行者;
/// 无注册/路由失败时回退到 `agent`。want 为空时自动从顶部任务描述抽取所需能力(R58)。
/// 返回 { dispatched, task_id, to_executor, remaining, note?/error?, want? }。
pub fn FistEngine::dispatch_next(
self : FistEngine,
ns : String,
agent : String,
want : String,
now : String,
) -> Json {
let tri = self.triage(ns, agent, want)
let top = _jstr(tri, "task_id") // triage 顶层 task_id 字段(若顶层是 suggestion 则回退解析)
let top2 : String = if top == "" {
match tri.value("suggestion") {
Some(Object(sm)) => _jstr(Json::object(sm), "task_id")
_ => ""
}
} else {
top
}
if top2 == "" {
return Json::object({
"dispatched": Json::boolean(false),
"task_id": Json::string(""),
"to_executor": Json::string(""),
"note": Json::string(
"无可派发的待领取任务(依赖满足集合为空)",
),
})
}
// 确定所需能力:显式 want 优先;为空则从顶部任务描述自动抽取(R58 store-backed 免手传)
let want_eff : String = if want == "" {
match self.get_task(top2) {
Some(t) => self.auto_want(t.description)
None => ""
}
} else {
want
}
// 从 store 构建执行者注册表
let mut reg = @executor.ExecutorRegistry::new()
for (name, abl, _created) in self.executor_list() {
reg = reg.add_ability(name, parse_abilities(abl))
}
let names = reg.list()
let load = self.executor_loads(names)
let route = reg.route_pick(want_eff, load)
let best = match route.value("best") {
Some(Object(b)) => _jstr(Json::object(b), "name")
_ => ""
}
let to = if best != "" { best } else { agent }
let remaining = match tri.value("count") {
Some(Json::Number(n, ..)) => if n > 1.0 { (n - 1.0).to_int() } else { 0 }
_ => 0
}
match self.claim(task_id=top2, assignee=to, now~) {
Ok(_) =>
Json::object({
"dispatched": Json::boolean(true),
"task_id": Json::string(top2),
"to_executor": Json::string(to),
"remaining": Json::number(remaining.to_double()),
"want": Json::string(want_eff),
"note": Json::string(
"已按能力/负载路由自动派发给执行者(无注册则回退 agent)",
),
})
Err(e) =>
Json::object({
"dispatched": Json::boolean(false),
"task_id": Json::string(top2),
"to_executor": Json::string(to),
"want": Json::string(want_eff),
"error": Json::string(e),
"note": Json::string("派发认领失败,未改动任何状态"),
})
}
}