Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

fix(builder): LinearBuilder 把 event 当终端节点——event 之后的节点永不串接,`linear().event().end()` 构建期报「end 不可达」并把注意力引向错误节点

Open Beginner friendly
#108 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
devtools

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 这类显式给了 event next 的 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/dsl builder / 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from jeffkit/plaita

All issues in jeffkit/plaita

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.