Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2be2ae1
docs: record architecture review and adoption criteria
chamsechan Oct 3, 2026
548c03a
docs(plans): record architecture review decisions and progress table
chamsechan Oct 3, 2026
ee58f1a
fix(validator): infer per-request item shapes instead of comparing ca…
chamsechan Oct 3, 2026
9391fe0
refactor(adapter): serialize rule match responses in output converters
chamsechan Oct 3, 2026
f9e1516
feat(operator): map failures below the facade to stage-based public c…
chamsechan Oct 4, 2026
ec1119a
docs(adapter): define whole-batch failures and per-biz single-item re…
chamsechan Oct 4, 2026
0f7b351
refactor(adapter): remove unused integration result structs
chamsechan Oct 4, 2026
0df9221
refactor(adapter): share one port binding implementation per direction
chamsechan Oct 4, 2026
6f88abd
refactor(adapter): stop presenting input limits as resolved deploymen…
chamsechan Oct 4, 2026
bfd41c1
refactor(core): drop write-only runtime options and single-model regi…
chamsechan Oct 4, 2026
a1fba64
refactor(nodes): move class-style port helpers out of NodeBase
chamsechan Oct 4, 2026
3a7c07c
refactor(nodes): run Map nodes on the Batch AuthorNode runtime
chamsechan Oct 4, 2026
59f146f
docs(plans): record why port declaration/state separation is not adopted
chamsechan Oct 4, 2026
6b62002
refactor(nodes): share ownership of model capability calls
chamsechan Oct 4, 2026
731c2f5
refactor(cli): generate remediation in alg_pipeline_tool instead of Core
chamsechan Oct 4, 2026
5cae3ed
docs(plans): record why biz audit stays with graph validation for now
chamsechan Oct 4, 2026
2e6448f
docs(plans): record why Control payloads are still parsed per layer
chamsechan Oct 4, 2026
da70557
refactor(contracts): reuse JSON Schema keyword predicates for Control…
chamsechan Oct 4, 2026
7ab1fe0
refactor(adapter): share one binding selection across deployment stages
chamsechan Oct 4, 2026
406f9e2
refactor(adapter): derive converter external_type from slot types
chamsechan Oct 4, 2026
7c410e3
docs(plans): record why host type registration stays centralized
chamsechan Oct 4, 2026
f066eab
refactor(engine): declare each Model's identity once
chamsechan Oct 4, 2026
315a04d
refactor(engine): declare each Backend provider's type once
chamsechan Oct 4, 2026
a042e1d
refactor(engine): read Model and Backend config defaults from declara…
chamsechan Oct 4, 2026
1f76aae
docs(plans): record why Backend switches stay inside the sources
chamsechan Oct 4, 2026
05e9f8e
refactor(registry): share fail-closed conflict state across registries
chamsechan Oct 4, 2026
e5c721f
docs(plans): record why core contracts stay in place
chamsechan Oct 4, 2026
10e1028
docs: summarize extension API changes and close the review plan status
chamsechan Oct 4, 2026
4430642
fix(architecture): preserve move-only callbacks and correct flow diag…
chamsechan Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .agents/skills/edgeflow-backend-developer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ description: 新增或修改 LLM-EdgeFlow Backend 的厂商 SDK、硬件运行
2. 明确资源创建/失败回收/销毁顺序、输入输出内存生命周期、执行协议、真实并发能力和
BatchPolicy。`BackendLoadSpec::requested_protocol` 必须匹配,尽早拒绝不支持的协议。
Session 实际协议仍需与声明一致。
3. 注册 `BackendDefinition` 与 `REGISTER_BACKEND_WITH_DEFINITION`,声明全部字段、默认值、
3. Provider 继承 `BackendIdentity<Backend>` 并只在 `kBackendType` 声明类型名;从
`MakeBackendDefinition<Backend>()` 开始注册 `BackendDefinition` 与
`REGISTER_BACKEND_WITH_DEFINITION`,声明全部字段、默认值、
范围和协议;额外纯配置约束使用 `validate_config`,Load 复用同一解析规则。
prompt 格式、图像解码、向量池化等模型语义交给 Model。

Expand Down
4 changes: 3 additions & 1 deletion .agents/skills/edgeflow-model-developer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ description: 新增或修改 LLM-EdgeFlow Model 的预处理、输出解释和
不进入 Model。需要新执行运行时,转 [Backend skill](../edgeflow-backend-developer/SKILL.md);
必须扩展中性协议时按跨层契约变更处理,不能直接引用具体 Backend。
- 通过 `REGISTER_MODEL_WITH_DEFINITION` 注册完整 `ModelDefinition`,声明能力、执行协议、
并发约束及 `config_fields`。不要在 Web、skill 或 Node 再维护模型列表。
并发约束及 `config_fields`。身份只写一次:继承 `ModelIdentity<Model, 能力接口>` 并声明
`kModelType`、`kConcurrency`,Definition 从 `MakeModelDefinition<Model>()` 开始。
不要在 Web、skill 或 Node 再维护模型列表。
- 额外配置语义写入纯函数 `validate_config`:输入已完成字段校验和默认值补齐,函数不做
文件/资源 I/O;Validator 和 Factory 会在 Backend 创建/加载前执行。直接 Create 复用
相同语义检查,资源存在性、Tensor metadata 和会话相关检查留在创建阶段。
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ models/*_vocab.txt
# 4. 测试报告与运行日志 (Test Reports, GTest Outputs & Logs)
# ------------------------------------------------------------------------------
results/
output/
# Demo run outputs; anchored so src/adapter/output/ converters stay tracked.
/output/
*.log
*.tlog
report.xml
Expand Down
4 changes: 3 additions & 1 deletion dev_support/node_authoring/benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ python3 dev_support/node_authoring/benchmark/run.py \
- 旧 starter 所需的两个已退役辅助头文件固定从 Git `87a28b7` 提取到工作目录的
`baseline_include/nodes`,仅作为探针私有比较基线;当前 SDK 不恢复旧作者接口。
`summary.json` 的 `baseline_helper_revision` 记录该版本。
- 旧 Map 为等价显式 `NodeBase` 透传实现的重建,不冒充历史生产 Node。
- 旧基类依赖的类写法端口辅助函数已移出 `NodeBase`,由
[legacy_node_base.h](../legacy_node_base.h) 的 `LegacyNodeBase` 提供;脚本提取旧头文件时改为继承它。
- 旧 Map 为等价显式 `LegacyNodeBase` 透传实现的重建,不冒充历史生产 Node。
- 每批 32 条、每条 1024 字节,固定 Echo mock;预热 100 次,每个进程测 10000 次,独立重复 5 次。
- 测量 `Process` 的线程 CPU 时间及 C++ `new/new[]` 请求次数、字节数;不包含输入发布和输出断言,
也不代表全部 malloc、存活堆或峰值 RSS。每次校验文本及 req/sub,记录实际模型调用次数。
Expand Down
12 changes: 8 additions & 4 deletions dev_support/node_authoring/benchmark/probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "baseline_starter.cpp"
#include "contracts/config_schema_validation.h"
#include "current_starter.cpp"
#include "dev_support/node_authoring/legacy_node_base.h"
#include "nodes/authoring.h"

namespace {
Expand Down Expand Up @@ -54,9 +55,9 @@ class EchoModel final : public ILlmModel {
return 0;
}
};
class ExplicitMap final : public NodeBase {
class ExplicitMap final : public LegacyNodeBase {
public:
ExplicitMap() : NodeBase("ExplicitMap") {}
ExplicitMap() : LegacyNodeBase("ExplicitMap") {}

protected:
bool InitNode(const NodeInitContext& init, const nlohmann::json&,
Expand Down Expand Up @@ -137,8 +138,11 @@ int main(int argc, char** argv) {
const int batch_size = 32, length = 1024;
SessionContext session;
auto mock = std::make_shared<EchoModel>();
if (!session.GetModelManager().RegisterModel("echo", mock, "probe-v1"))
return 2;
ModelRegistration registration;
registration.model_id = "echo";
registration.revision = "probe-v1";
registration.model = mock;
if (!session.GetModelManager().RegisterBatch({registration})) return 2;
ValidatedNodePlan plan;
std::unique_ptr<INode> node;
if (mode == "old_map")
Expand Down
6 changes: 6 additions & 0 deletions dev_support/node_authoring/benchmark/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ def run(command, **kwargs):
header = run(["git", "-C", str(root), "show",
BASELINE_HELPER_REVISION + ":include/nodes/" + name],
capture_output=True).stdout
# The retired base relied on class-style port helpers that NodeBase no
# longer carries; LegacyNodeBase keeps them for this comparison only.
header = header.replace('#include "nodes/node_base.h"',
'#include "dev_support/node_authoring/legacy_node_base.h"')
header = header.replace("public NodeBase", "public LegacyNodeBase")
header = header.replace(": NodeBase(", ": LegacyNodeBase(")
(baseline_headers / name).write_text(header)

old = run(["git", "-C", str(root), "show",
Expand Down
103 changes: 103 additions & 0 deletions dev_support/node_authoring/legacy_node_base.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#pragma once

#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>

#include "nodes/node_base.h"

namespace llm_edgeflow {

// Retired class-style port helpers. Tests that drive the NodeBase runtime
// directly and the authoring benchmark's historical baseline use them;
// production Nodes use Specs, whose AuthorNode owns port binding.
class LegacyNodeBase : public NodeBase {
public:
using NodeBase::NodeBase;

protected:
template <typename T>
void BindPort(const NodeInitContext& init_ctx, BoundInput<T>& in_port) const {
const auto result = detail::ResolvePortBinding(
*init_ctx.plan, PortDirection::kInput, in_port);
if (result.status != detail::PortBindingStatus::kUnbound) {
if (result.status == detail::PortBindingStatus::kTypeMismatch) {
throw std::invalid_argument(
"Input port TypeId mismatch for " + in_port.LogicalName() +
" (expected: " + in_port.TypeId() +
", bound: " + result.binding->type_id + ")");
}
} else {
in_port.Unbind();
}
}

template <typename T>
void BindPort(const NodeInitContext& init_ctx,
BoundOutput<T>& out_port) const {
const auto result = detail::ResolvePortBinding(
*init_ctx.plan, PortDirection::kOutput, out_port);
if (result.status == detail::PortBindingStatus::kUnbound) {
throw std::invalid_argument("Output port is unbound in plan: " +
out_port.LogicalName());
}
if (result.status == detail::PortBindingStatus::kTypeMismatch) {
throw std::invalid_argument("Output port TypeId mismatch for " +
out_port.LogicalName() +
" (expected: " + out_port.TypeId() +
", bound: " + result.binding->type_id + ")");
}
}

template <typename T>
BoundInput<T> BindInput(const NodeInitContext& init_ctx,
std::string logical_name) const {
BoundInput<T> port(std::move(logical_name));
BindPort(init_ctx, port);
return port;
}

// Preserve BindPort's validation and left-to-right error ordering.
template <typename... Ports>
void BindPorts(const NodeInitContext& init_ctx, Ports&... ports) const {
(BindPort(init_ctx, ports), ...);
}

template <typename T>
BoundOutput<T> BindOutput(const NodeInitContext& init_ctx,
std::string logical_name) const {
BoundOutput<T> port(std::move(logical_name));
BindPort(init_ctx, port);
return port;
}

template <typename T>
const T* Require(AlgContext& ctx, const BlackboardKey<T>& key, int error_code,
std::string_view semantic = {}) const {
const bool key_exists = ctx.Has(key);
const T* val = ctx.Read(key);
if (!val) {
std::string msg = Name() +
(key_exists ? ": type mismatch for input key '"
: ": missing required input key '") +
key.name + "' (expected type: " + key.type_id + ")";
if (!semantic.empty()) {
msg += " for " + std::string(semantic);
}
ctx.SetError(error_code, std::move(msg));
return nullptr;
}
return val;
}

template <typename T>
void Publish(AlgContext& ctx, const BlackboardKey<T>& key, T value) const {
if (!ctx.Publish(key, std::move(value))) {
throw std::logic_error("Duplicate output publication for key '" +
std::string(key.name) + "'");
}
}
};

} // namespace llm_edgeflow
40 changes: 40 additions & 0 deletions doc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@

## Unreleased

架构审查回归修复:Map 的回调通过移动交给运行时,支持捕获 `unique_ptr` 等不可复制状态;
流契约错误由 Validator 提供生产者、消费者、有效端口契约与推导出的数量形状,CLI 据此解释
节点输入、业务出口及 IO 边界错误,保留拆分来源并正确区分逐项配对与出口数量要求。
`ConfigValueOrDefault` 始终核对字段声明,拒绝数组和标量配置,`null` 仍按省略配置读取声明默认值。

源码扩展接口整理(随架构审查落地,不涉及 Operator ABI):
- Model 继承 `ModelIdentity<Model, 能力接口>`、Backend Provider 继承 `BackendIdentity<Backend>`,
身份只声明一次,Definition 从 `MakeModelDefinition` / `MakeBackendDefinition` 开始;
配置读取使用 `ConfigValueOrDefault`,默认值只写在 `config_fields`。
- 转换器 `external_type` 留空时由外部槽类型推导;`InputPortBindings` / `OutputPortBindings`
共用 `PortBindings<方向>`;`ResolvedInputLimits` 更名为 `InputLimits` 且不再出现在部署配置中。
- 删除 `include/adapter/biz_results.h`、`ModelManager::RegisterModel`(改用 `RegisterBatch`)和
`RuntimeOptions` 中只写不读的 `biz_type`、`depth_num`、`biz_name`;`NodeBase` 的类写法端口辅助
函数移到 `dev_support` 的 `LegacyNodeBase`。
- Map Node 改在 Batch 运行时上执行,作者写法与 Definition 不变;Batch 端口绑定诊断补充期望与实际
类型。注册表冲突状态在消息无法保存时仍保持失败封闭。

修复建议移出 Core:remediation 的原因、事实、中文摘要与经 Validator 复核的 JSON Patch 修复改由
`alg_pipeline_tool` 生成,不再编入 SDK,也不在 Create 校验失败时执行。`PipelineValidator::Explain`
删除;Validator 只返回中性诊断,未注册类型的相近名称仍在 `suggestions` 中给出。CLI 与 Studio 的
`validate` / `--explain` 继续提供修复建议;流契约诊断与建议的 `facts` 包含有效声明,
数量错误还附上 Validator 已推导的数量形状。

公开错误码:Operator 门面按失败阶段映射返回码,内部的 Pipeline、Node、Model 错误码不再直接返回给宿主。
Create 阶段的配置、部署与模型/Backend 加载失败返回 `-2`(此前部分为 `-3`);Process 中节点或模型
执行失败返回 `-100`;Control 请求不合法返回 `-2`(此前为 `-1`),节点拒绝或未能应用更新返回 `-100`。
`GetOperatorLastError()` 以 `<阶段> failed with internal code <内部码>: ...` 保留原始码与节点诊断,
见[宿主调用与生命周期](dev_guide/operator_output_allocation.md#宿主调用与生命周期)。

规则匹配结果:`TextRuleMatchNode` 只输出中性结果,`RuleMatchItem` 以 `matches` 列出全部命中、
以 `slots` 保存带 JSON 类型的捕获与常量,删除 `match_result_json`、`details`、`captures` 和
`constants`。关键词响应的 `match_result_json` 与音频响应的 `intent_slot_json` 改由输出转换器序列化,
字段、类型、默认命中与字节内容保持不变。

端口数量关系:输入端口的 cardinality 只描述节点如何消费,接受任意数量的上游数据;
`TextChunkNode` 等拆分输出可以直接接逐项节点(如 `TextRuleMatchNode`)。Validator 改为沿 DAG
推导每个数据名在请求内的条目数,仅在 `1:1` 业务/IO 边界收到多项,或同一节点的逐项输入无法配对时
报告 `PORT_CARDINALITY_MISMATCH`。`TextEmbeddingNode` 的端口声明由 `N:M` 改为实际的 `1:1`。
Pipeline JSON 与 Definition 字段不变,见[数量关系声明](dev_guide/custom_node_concepts.md#数量关系声明与-validator-检查)。

文档链接检查:门禁新增 `DocLinksTest`,检查仓库内 Markdown 的相对链接、HTML `href`/`src`
和标题锚点;没有扫描到文件或跨文件锚点时直接失败,不会空跑通过。

Expand Down
28 changes: 27 additions & 1 deletion doc/dev_guide/business_onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ JSON 请求是不同的输入约定。已有 Nodes 能完成算法,也不代
| 环节 | 样例文件 | 需要补齐时落实的内容 |
| --- | --- | --- |
| 本地模拟平台结构 | [Operator 数据结构](../../include/platform_mock/operator_data_types.h)、[平台交互类型](../../include/platform_mock/operator_types.h) | 已有载体不足时才新增结构,明确字段、长度和所有权;本目录只保存模拟约定,真实公司定义在授权内网接入 |
| 内部数据边界 | [业务 key](../../include/adapter/biz_blackboard_keys.h)、[业务 Result](../../include/adapter/biz_results.h) | ingress/egress typed key 与接入适配层持有的结果值;已有类型可复用 |
| 内部数据边界 | [业务 key](../../include/adapter/biz_blackboard_keys.h)、[中性结果类型](../../include/core/common_contracts.h) | ingress/egress typed key 与 Pipeline 产出的中性结果;已有类型可复用,外部响应由输出转换器组装 |
| 输入转换器 | [text_input.cpp](../../src/adapter/input/text_input.cpp) | 外部输入校验、中性数据封装及 `REGISTER_INPUT_CONVERTER` |
| 输出转换器 | [keyword_result_output.cpp](../../src/adapter/output/keyword_result_output.cpp) | 内部结果关联、写入已分配的输出结构及 `REGISTER_OUTPUT_CONVERTER` |
| 业务契约与绑定 | [keyword_match_bindings.cpp](../../src/adapter/biz/keyword_match_bindings.cpp) | 声明 `BizDefinition`、转换器组合和非同名端口映射;默认批次上限为 64,用 `REGISTER_IO_BINDING` 注册 |
Expand Down Expand Up @@ -169,6 +169,32 @@ Operator 的宿主输入校验会拒绝 `CompanyString` 中的原始嵌入 NUL
| 多个外部槽 | [图像问题输入](../../src/adapter/input/image_query_input.cpp) / [票据输出](../../src/adapter/output/invoice_result_output.cpp) | frame 的请求 ID、票据与 OCR boxes |
| 候选展开与排名 | [重排输入](../../src/adapter/input/rerank_input.cpp) / [重排输出](../../src/adapter/output/rerank_result_output.cpp) | sub_id、排名和原始索引恢复 |

### 整批失败与单条结果

致命错误使整批不发布新输出:输入转换失败、任一 Node 返回非零、输出转换器拒绝结果或容量不足时,
本批已租用的输出全部归还,宿主按阶段收到公开返回码,见
[宿主调用与生命周期](operator_output_allocation.md#宿主调用与生命周期)。框架不提供部分成功通道。

单条结果是否"无效但仍可返回"由各业务的输出契约决定。Node 可以按自身契约产出逐项状态,例如
`StructuredJsonParseNode` 的 `is_valid`、`parse_status`、`diagnostic`,以及
`emit_diagnostic` / `configured_fallback` 策略下不触发节点失败的坏项;输出转换器决定接受这些项、
用 `status_code` 表达,还是拒绝整批。`status_code` 是宿主结构中的业务状态,不恒为 0。
新契约在转换器中写明这三项含义,并在契约测试中覆盖。现有业务的约定:

| 业务(输出转换器) | `status_code` | 单条结果不满足契约时 |
| --- | --- | --- |
| `keyword_match`(`keyword.result.operator.v1`) | 复制规则结果的 `status_code`;`TextRuleMatchNode` 写 0 | 未命中是正常结果(`is_hit=0`),没有无效项 |
| `audio_asr_intent`(`audio_result.plain.operator.v1`) | 复制意图结果的 `status_code` | 缺少转写或意图结果时整批失败 |
| `doc_qa`(`doc_answer.plain.operator.v1`) | 复制意图结果的 `status_code` | 缺少回答、意图或片段数时整批失败 |
| `entity_extract`(`document.structured.operator.v1`) | 0 | 解析失败或使用 fallback 的文档使整批失败 |
| `ocr_invoice_qa`(`invoice_result.plain.operator.v1`) | 0 | 同上 |
| `dialogue_audit`(`audit_result.plain.operator.v1`) | 0 | 判定解析失败或使用 fallback、首要策略排名不为 1、`risk_level` 不在 `SAFE`/`LOW_RISK`/`MEDIUM_RISK`/`HIGH_RISK`,或 `risk_score` 不在 [0, 1] 时整批失败 |
| `cross_rerank`(`rerank_result.plain.operator.v1`) | 0 | 排名不连续、超过 8 项或候选编号越界时整批失败;无候选时 `count=0` |
| `translate`(`translate.json.operator.v1`) | 0 | 译文无法序列化为 JSON(如非法 UTF-8)时整批失败 |

需要让某个业务在单条无效时仍返回其他请求的结果,应先设计该业务的外部表达
(例如非零 `status_code` 与诊断字段)和测试,再修改对应转换器;不要在框架中跳过失败项。

## 4. 需要新的宿主类型时

多数业务复用已注册的宿主类型和 ValueType,载荷协议变化只需修改转换器。确需新的平台结构或
Expand Down
Loading
Loading