BLOGE
0.9.8-RC1在线版 · 事实校验 2026-09-15 · English
第 16 章 —— 组合 Session 与状态机
承诺: 读完本章后,你将知道如何组合
session和state_machine,而不会弄丢信号所有权、超时语义和输出路径;更重要的是,你会理解这两种嵌套方向都很有用,但并不对称。
学习目标
- 对比两种组合模式:session 在外,状态机在内;状态机在外,session 在内。
- 决定在嵌套流程里,究竟由哪个运行时拥有生命周期、信号、恢复和超时语义。
- 在嵌套编排中追踪输出路径,而不是把重要结构压平成一层。
- 理解当前实现里的不对称约束,避免设计出运行时根本不支持的组合方式。
- 选择最小但足够清晰的组合边界。
前置条件
- 第 14 章 —— 多轮 Session —— 先理解 session 的生命周期
- 第 15 章 —— 状态机 —— 先理解显式状态迁移
- 第 13 章 —— 持久执行 —— 嵌套运行时仍然站在同一套持久执行基础上
源示例
| 文件 | 展示内容 |
|---|---|
ch15/order-session-with-state-machine.bloge | session 在外:某个 phase 内嵌一个完整订单状态机 |
ch15/review-state-machine-with-session.bloge | 状态机在外:某个状态内嵌一个短小 review session |
ch15/ivr-customer-service.bloge | 一个更长生命周期的 session,可作为未来嵌套模式的延展 |
OrderSessionWithStateMachineExample.java | session → 内嵌状态机 的 Java Fluent API 版本 |
ReviewStateMachineWithSessionExample.java | 状态机 → 内嵌 session 的 Java Fluent API 版本 |
OrderSessionWithStateMachineExampleTest.java / ReviewStateMachineWithSessionExampleTest.java | 证明嵌套形状能编译并运行的测试 |
为什么这很重要
真实业务常常同时需要两种抽象:
- 一段客户对话里,包含一个多步骤订单生命周期
- 一个显式生命周期状态里,需要执行一小段自包含的 review 交互,然后才能决定往哪里走
如果把一切都压平到一张 graph 里,模型会变得模糊;如果 把一切都拆成独立服务,生命周期边界又会变得模糊。
组合的价值在于,它让业务模型本身保持可见:
- 外层运行时决定“主要生命周期”是什么
- 内层运行时只负责处理其中一个局部编排问题
所以真正该问的问题不是“它们能不能嵌套”,而是:哪个运行时才应该拥有这段业务真正重要的生命周期?
心智模型
当前支持两种方向:
| 模式 | 外层所有者 | 内层所有者 | 最适合什么 |
|---|---|---|---|
session → 内嵌 state_machine | SessionExecutor | 以合成 node 包装的状态机运行逻辑 | 主要是长生命周期交互,但其中某个 phase 更适合显式状态迁移 |
state_machine → 内嵌 session | StateMachineExecutor | 以合成 node 包装的 session 运行逻辑 | 主要是状态生命周期,但其中某个状态需要一个短小多步骤交互 |
两种模式都很有用,但运行时约束并不一样。
先选外层 owner,再选语法
组合首先只问一个问题:内层活动结束后,哪个 runtime 还必须继续拥有这个 case?
| 主导事实 | 外层 owner | 内层活动示例 |
|---|---|---|
| 客户还会继续多轮对话 | Session | ordering phase 内的订单状态机 |
| 订单还会继续经历持久业务状态 | State machine | review state 内的评审 Session |
| 工作是一次性的依赖流程 | Graph | 不需要第二种生命周期模型 |
对 orderWorkflow,订单到达 shipped 后,调用方还要回到对话,所以 Session
在外。对 reviewWorkflow,交互评审结束后 case 必须进入 approved 或
rejected,所以状态机在外。嵌套方向是 ownership 决策,不是格式偏好。
第一个可运行的示例
更灵活的一种方向是:session 在外,状态机嵌在某个 phase 内部。
session orderWorkflow {
idle_timeout = 72h
max_rounds = 10
phase ordering {
state_machine orderFlow {
max_transitions = 20
max_state_visits = 5
state draft [initial] {
graph {
node collectInfo : CollectOrderInfoOperator { }
}
on submit -> pendingPayment
}
state pendingPayment {
graph {
node chargePayment : ChargePaymentOperator { }
}
on payment_confirmed -> processing
on payment_failed -> draft
}
state processing {
graph {
node shipOrder : ShipOrderOperator { }
}
on * -> shipped
}
state shipped [terminal] { }
}
then -> fulfillment
}
}
用业务语言读它:
- 外层是一段订单 session
- 其中
ordering这个 phase 内部,用状态机表达订单生命周期 - 内层状态机结束后,session 再继续前往
fulfillment
外层 session 仍然拥有持久化、发信号入口和整段交互的身份。
signal 向内传,terminal output 向外传
内层 runtime 拥有自己的 current state,所以外部事件不能直接跳到
chargePayment 或 shipOrder。它先进入外层 Session,再被翻译成状态机
signal,最后才选择 transition。
跟随一笔订单穿过两个 owner
| 方向 | 穿越边界的合同 | 错误捷径 |
|---|---|---|
| 向内 | customer submit → Session input → 状态机 submit signal | webhook 直接寻址内层 node id |
| 向内 | payment callback → owning runtime → payment_confirmed event | 直接修改 currentState |
| 向外 | 状态机 terminal output 包含 shipped 与 shipmentId | caller 读取任意内层 workspace |
| 向外 | Session fulfillment phase 调用 notifyCustomer | 让内层 Operator 发送不受治理的对话消息 |
这样两个方向都有 receipt:向内是 accepted signal 与 state transition;向外是 namespaced terminal output 与 Session result。signal 被拒绝时,应先检查 owner、 instance id 与 allowed transition,再检查业务 node 代码。
拆解分析
模式 A —— session 在外,状态机在 phase 内
这正是 order-session-with-state-machine.bloge 使用的形状。
关键行为如下:
- 真正的运行时入口仍然是外层
SessionExecutor - 外部
signal(...)也是先打到 session handle,而不是直接发给内层状态机 - 当内层状态机在等待事件时,外层 session 会表现为
SUSPENDED - 当内层状态机恢复运行时,外层 session 又会回到
ACTIVE
输出路径之所以长,是因为它保留了所有权信息:
finalState = ctx.ordering.output.orderFlow.stateMachine.currentStateId
shipmentId = ctx.ordering.output.orderFlow.processing.output.shipOrder.shipmentId
这条路径明确说明数据来自:
- 外层 session 的
orderingphase - 内嵌状态机 node
orderFlow - 内层状态
processing - 状态内部 graph 的
shipOrdernode
模式 B —— 状态机在外,session 在状态内
这是 review-state-machine-with-session.bloge 使用的形状。
review 状态里内嵌了一个 session:
state review [initial] {
session reviewSession {
phase collect { ... then -> finalize }
phase finalize { ... }
}
on * when ctx.review.output.reviewSession.finalize.output.decideReview.decision == "approved" -> approved
on * -> rejected
}
也正是在这里,不对称性最明显。
内嵌在状态机里的 session 目前必须同步完成。如果这个内层 session 还想等待后续外部信号,SessionOperator 会直接快速失败,而不会让外层状态机处于一种“半暂停”状态。
所以,这种模式适合:
- 一个短小的两阶段 review
- 一个很紧凑的 enrich → decide 交互
- 一次状态访问内就能结束的内层生命周期
它不适合长时间的人机多轮对话。
当前实现里的组合约束
现在的嵌套形式是故意收窄的:
- 一个 phase 最多只能有一个顶层内嵌
state_machine - 这个内嵌
state_machine必须是该 phase 中唯一的可执行成员 - 一个状态可以内嵌
session,但该 session 必须同步完成
这些限制的目的,是让生命周期所有权保持绝对清晰。
约束 —— 避免信号死锁
嵌在
state_machine里的session不能挂起等待外部信号,只能同步 完成。原因是所有权:外层状态机执行器持有生命周期锁,不会把控制权让 给另一个信号接收者。如果嵌套 session 试图挂起等信号,编译器会抛出SM_NESTED_SESSION_AWAITS_SIGNAL并拒绝发布该 graph。反方向 ——
sessionphase 里嵌套的state_machine—— 可以自由挂起, 因为外层 session 就是信号所有者。任何可能需要外部信号的场景,优先 选 模式 A(session 在最外层)。
生命周期所有权检查清单
在开始嵌套之前,先回答这些问题:
| 问题 | 如果答案偏向 session | 如果答案偏向 state_machine |
|---|---|---|
| 这段业务最重要的形状是什么? | 人机对话 / 多轮交互 | 对象生命周期 / 状态推进 |
| 外部信号应该打给谁? | session | 状态机 |
| 谁拥有空闲超时 / 访问控制? | session 运行时 | 状态机运行时 |
| 面板首先应该显示什么? | 当前 phase / 轮次 | 当前状态 |
| 内层是否需要跨外部轮次等待? | 外层是 session 时可以 | 状态机内嵌 session 时不可以 |
先选外层所有者,再决定是否嵌套
设计嵌套编排时,最稳妥的顺序是:
- 如果人们主要是按对话轮次来理解这段流程,就让
session在外层 - 如果人们主要是按命名状态来理解这段流程,就让状态机在外层
只有在这一步明确之后,你才应该判断某个局部片段是否真的值得单独引入另一套编排模型。
常见陷阱
❌ 误以为两种嵌套方向是对称的
它们不是。
- session 内嵌状态机时,内层状态机可以通过外层 session 的生命周期来等待和恢复
- 状态机内嵌 session 时,内层 session 必须同步结束
如果你按“两个方向都能自由挂起”来设计,就会在很后面才发现模型本身就选错了。
常见故障
信号被发到了错误的运行时边界
在“session 在外”的模式里,逻辑上等待的也许是内层状态机,但外部信号仍然必须从外层 session 入口进入。
所以这是一种错误心智模型:
// 错误:试图直接给内层状态机发信号
stateMachineExecutor.signal(...)
正确做法应该是:
sessionHandle.signal(Map.of("event", "payment_confirmed"));
永远把信号打给外层所有者。
引导式重写
对照两个 companion 示例,练习在真正写 DSL 之前先决定外层所有者。
- 先问业务问题。 这更像一段对话,还是更像一个显式生命周期?
- 先选外层运行时。 对话用 session,生命周期用状态机。
- 只把“形状不同”的那一小段内嵌出来。
- 订单流程:外层是 customer/order session,但其中
ordering更适合状态机 - review 流程:外层是 review 生命周期,但其中一个状态需要 collect → finalize 的短交互
- 订单流程:外层是 customer/order session,但其中
- 写一条下游表达式,验证输出路径是否仍然可读。
如果输出路径已经变得难以理解,那往往意味着你的边界选错了。
思维检查
-
在“session 在外”的模式里,外部信号应该打给谁?
(打给外层
SessionExecutor/SessionHandle。) -
在“状态机在外”的模式里,内嵌 session 能不能等待下一次外部信号?
(不能。它目前必须同步完成。)
-
为什么
ctx.ordering.output.orderFlow.stateMachine.currentStateId这样的长路径反而是好事?(因为它保留了嵌套边界上的所有权和来源信息。)
-
设计嵌套流程时,第一件应该决定的事是什么?
(哪一个运行时拥有主要业务生命周期。)
-
什么时候“session 在外”通常是更安全的默认值?
(当流程是长生命周期、由外部信号驱动,并且核心是人机多轮交互时。)
-
把两种组合方向当成对称结构,最大的风险是什么?
(你可能会设计出一个需要挂起的内嵌 session,但“状态机在外”的模式并不支持这种用法。)
实验
目标: 为两个“看起来相似”的流程选出正确的外层所有者,并实现出来。
- 建模一个订单流程:客户可能会在几个小时后才回复支付提示。
- 建模一个内部审批流程:review 交互可以从现有数据立即完成。
- 对每个流程都写下:
- 外层运行时
- 内层运行时(如果需要)
- 谁接收信号
- 一条示例输出路径
- 各实现一份 DSL 文件。
进阶: 增加一个测试,证明你可以通过外层 session handle 给“session 在外”的示例发信号,并最终抵达内层状态机的终止状态。
实验验收卡
- 预期与观察: 外层 owner 决定生命周期;signal 向内,terminal output 向外。
- 失败与恢复: 让 Session 和状态机同时拥有外层;恢复为唯一 owner。
- 证明边界: 证明组合责任和信号方向,不证明任意嵌套无死锁。
- 练习合同: 客服组合;只交换外层 owner;交付选择卡和时序图;只剩一个 owner 即停止。