fix(builder): LinearBuilder 把 event 当终端节点——event 之后的节点永不串接,`linear().event().end()` 构建期报「end 不可达」并把注意力引向错误节点
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
Research direction
Start at _TERMINAL_TYPES in plaita/dsl/builder.py (around line 721) and read _auto_chain just below it; the fix is removing event from that set so it gets the default next-chaining. Put the two new unit tests under tests/unit/dsl/ as the issue asks, and confirm the reproduction in the issue prints OK. Done means the event node gets next pointing at the following node, the final event in a flow still has no next, and tests/integration/server/test_flow.json behaves as before.
Written by the indexing model from the issue text.
Description
优先级 P2 · 依赖:无
基线 HEAD = 65f48f2
Summary
LinearBuilder 的自动串接把 event 与 end 一起当成「终端节点」,于是 event 之后声明的节点永远不会被串上 next;而引擎侧 event 本来是可以通过 next 续跑的普通节点。结果 linear().start().event(...).end(...).build() 在构建期直接失败,且错误信息指向 end 节点(真正的断链点是 event 缺失的 next),把使用者引向错误的排查方向。
证据 1:终端类型集把 event 与 end 并列,注释断言「终端节点:没有 next」。
plaita/dsl/builder.py:718-721
# 这些节点类型的 next 是「真分支目标」而非「顺序下一个」,不参与自动串接
_BRANCH_SKIP_AUTO_NEXT = {"if", "switch", "case"}
# 终端节点:没有 next
_TERMINAL_TYPES = {"end", "event"}
证据 2:_auto_chain 对 _TERMINAL_TYPES 直接 continue,跳过 next 填充——这是断链的直接位置。
plaita/dsl/builder.py:1007-1020
def _auto_chain(self) -> None:
"""按声明顺序填充缺失的 ``next``。"""
nodes = self._builder._nodes
for i, n in enumerate(nodes):
ntype = n.get("type")
if ntype in _TERMINAL_TYPES:
continue
if ntype in _BRANCH_SKIP_AUTO_NEXT:
# if 的真分支(next)若未指定,默认走下一个声明节点
if ntype == "if" and n.get("next") is None and i + 1 < len(nodes):
n["next"] = nodes[i + 1].get("id")
continue
if n.get("next") is None and i + 1 < len(nodes):
n["next"] = nodes[i + 1].get("id")
证据 3:event() 是 LinearBuilder 的公开链式方法,本身不提供 next 参数(也从不写 next),完全依赖 _auto_chain。
plaita/dsl/builder.py:975-985
def event(
self,
event_type: str,
id: Optional[str] = None,
event_filter: Optional[Dict[str, Any]] = None,
**extra: Any,
) -> "LinearBuilder":
return self._append(event(
id=self._ensure_id(id), event_type=event_type,
event_filter=event_filter, **extra,
))
证据 4:引擎侧 event 并非终端。Node.next 是通用字段,EventNode 没有覆盖 next,也没有覆盖 branching。
plaita/node/basic.py:94
next: Optional[Any] = Field(None, description="下一个节点的 id(通常由画布连线维护,无需手填)")
plaita/node/basic.py:74
branching: ClassVar[bool] = False
对 EventNode 是否覆盖 next / branching 的反证:
$ grep -n "next\|branching\|is_suspending" plaita/node/event_node.py
31: is_suspending: ClassVar[bool] = True
(只有 is_suspending,next / branching 零覆盖。)
证据 5:因此 _get_target_node 对 event 会走 next 分支——event 只要有 next 就会续跑,_TERMINAL_TYPES 的假设与引擎不符。
plaita/core/flow.py:408-411
def _get_target_node(self, current: Node, branch=None) -> str:
if (not current.branching) and current.next:
return current.next
return self._get_branch_target(current, branch)
证据 6:仓库自带的集成用例就给 event 写了 next,正面反证「event 没有 next」的假设。
tests/integration/server/test_flow.json:43
"next": "end"
(上下文:"id": "wait_for_event"、"type": "event"。)
证据 7:codeflow 前端产出的 EVENT 节点同样不带 next、由后续阶段接线,形态与 LinearBuilder 一致但语义上节点本身可续跑。
plaita/dsl/codeflow/_nodes.py:89-95
if kind == "EVENT":
etype = _const(kw.get("type") or kw.get("event_type") or kw.get("eventType"))
if etype is None and pos:
etype = pos[0].value if isinstance(pos[0], ast.Constant) else _compile_expr(pos[0], ctx)
if etype is None:
raise _CodeflowError("EVENT 需要 type", node)
spec = {"type": "event", "id": nid, "eventType": etype}
证据 8:linear / LinearBuilder 是 plaita.dsl 的公开导出,且被用户文档与 AI 技能推荐。
plaita/dsl/__init__.py:36-40
from .builder import (
FlowBuilder,
LinearBuilder,
build,
linear,
plaita/dsl/__init__.py:81-83
"LinearBuilder",
$ grep -rn "linear(" docs-site/docs/guide/yaml-and-dsl.md plaita-ai/plaita_ai/skills/plaita-flow-builder/SKILL.md
docs-site/docs/guide/yaml-and-dsl.md:233: linear("adult_check", input_type="object", desc="判断成年")
docs-site/docs/guide/yaml-and-dsl.md:248: linear("echo_upper", input_type="object")
docs-site/docs/guide/yaml-and-dsl.md:262: linear("guard", input_type="object")
docs-site/docs/guide/yaml-and-dsl.md:319:| `plaita.dsl.linear(flow_id, ...)` | 创建 `LinearBuilder`(隐式 next,id 按需) |
plaita-ai/plaita_ai/skills/plaita-flow-builder/SKILL.md:145: linear("adult_check", input_type="object")
复现(实跑,HEAD=ea53e46):
$ cd /Users/jeff.jie/projects/infra4agent/plaita && /tmp/plaita-audit-venv/bin/python -c "from plaita.dsl import linear; f=linear('ev').start().event(event_type='approval').end('done', output='x').build(); print('OK')"
FAILED: FlowIRValidationError [nodes] 以下节点从 start 不可达,运行期会被静默跳过(结果无声丢失): ['done']。请把它们接入主流程或删除。
根因确认——节点 spec 里 event 确实没有 next,断链点在 event 而非报错所指的 end:
$ /tmp/plaita-audit-venv/bin/python -c "from plaita.dsl import linear; print(linear('t').start().event(event_type='e').to_dict()['nodes'])"
spec: [{'type': 'start', 'id': '_n1', 'next': '_n2'}, {'type': 'event', 'id': '_n2', 'eventType': 'e'}]
影响
- 踩坑者:用
plaita.dsl.linear(...)链式编写线性流程的流程作者(含按文档docs-site/docs/guide/yaml-and-dsl.md与plaita-flow-builder技能写 flow 的 AI agent)。只要在.event(...)之后再声明任何一个节点(.end(...)、.assignment(...)、.http(...)等),build()/validate()/to_dict()就会抛FlowIRValidationError。 - 代价是误导性诊断,不是静默错误数据:构建期就失败,不会跑出错误结果;但报错文本把
['done'](end)列为「从 start 不可达」,而 end 本身没有任何问题——真正的断链点是 event 缺少next。使用者按报错去改 end 是徒劳的,只会反复怀疑 end 的 id/声明位置。 - 若作者为了绕过报错而删掉 event 之后的节点,流程会在 event 处停下:节点语义上仍是 suspending 节点,续跑需要一个由
next指向的后继;删节点等于把「等待后继续」变成「等待即终点」,属于容易发生的行为偏差。 - 生产可达性:完全可达(纯库内 DSL 构建期行为,不需要任何特殊部署形态)。缓解因素有两点,故定 P2 而非 P1:其一,单纯以
.event()结尾、后面没有节点的线性流程合法且不报错;其二,作者可改用FlowBuilder/build(...)显式连线绕过。 - 不修的后果:LinearBuilder 的「隐式 next」契约对 event 永久失效,凡是「等一个事件再继续」的线性流程都必须退化成显式 builder 或 JSON,公开 API 与文档承诺的便利性不一致。
建议
- 修
plaita/dsl/builder.py:721:把event从_TERMINAL_TYPES移出,令其回到_auto_chain的默认分支(第 1019-1020 行)参与顺序串接。真正没有 next 的只有end。 - 同时处理「以 event 结尾」的合法场景:
_auto_chain对最后一个节点本就不写next(有i + 1 < len(nodes)保护),所以移出后.event()收尾的 flow 行为不变,无需额外分支。 - 若担心 event 的续跑语义有例外(例如必须由外部 resume 指定后继),应在
LinearBuilder.event()增加显式next参数并在文档中说明,而不是隐式放弃串接。 - 验收条件:新增单测(建议放
tests/unit/dsl/下 builder/linear 相关测试文件),断言linear("ev").start().event(event_type="approval").end("done", output="x").build()不再抛异常,且构建产物中 event 节点的next == "done";再加一条反向用例,断言linear("t").start().event(event_type="e")的 event 节点仍无next(末节点不臆造后继)。上面的复现命令应从FAILED: FlowIRValidationError ...变为打印OK。 - 顺带回归:
tests/integration/server/test_flow.json这类显式给了 eventnext的 JSON flow 行为不得改变。
边界
- 只修
LinearBuilder._auto_chain/_TERMINAL_TYPES对event的归类;不改end的终端语义,不改FlowIRValidationError的不可达检查规则本身,也不改FlowBuilder/ JSON / codeflow 前端的连线方式。 - 报错信息「把注意力引向 end」属于本次一并说明的次生现象;若要让校验器报出「event 缺 next」这种更精确的提示,是另一个独立议题,不在本单范围。
- 不涉及 event 的
eventFilter/eventType键名问题(那是 codeflow emit 侧的独立缺陷),本单只处理 builder 的 next 串接。 - 与既有 issue 不重叠:
gh issue list -R jeffkit/plaita --state all --limit 100(#11、#16、#22-#52)中无一涉及plaita/dslbuilder /LinearBuilder/_auto_chain;本单可独立提报。
- Dominant language
- Python
- Stars
- 0
- Forks
- 1
- Avg merge
- 3h 53m
- Merged PRs (30d)
- 3
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from jeffkit/plaita
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
MystenLabs/MemWal#1163 · 2 comments ·
Maintainers usually reply within 1 day
-
infertopics leaves new nodes without a topic when untopiced neighbours outnumber topiced onesPossibly taken @moneebullah25 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
ClanGenOfficial/clangen#6254 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
FinanceFlash/unvibecode#218 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day