BLOGE
0.9.9-RC1在线版 · 编制 2026-11-02 · 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