跳到正文

运行时与图执行实施规格 ​

重建边界(2026-10-15):旧业务实现已移除。本页保留为待调整的设计草案;用户、应用、发布与配置版本属于平台,SDK 只接收已解析配置并负责执行与恢复。涉及平台身份、发布绑定及表结构的内容需按 SDK / Platform 边界重新划分,不直接作为 SDK 的必填契约或已实现功能。

日期:2026-10-15。状态:目标实施规格,尚未实现。本篇将 图与 Node、执行协议、数据契约 和 检查点协议 落为唯一推荐算法。公共类型复用这些文档;以下字段补充属于该类型的新版本,不创建第二套状态。物理表、事务实现与 API 分别见 持久化、目录与 API。调用管道 和 上下文与能力 负责模型、工具、Hook、压缩的领域实现。

本轮实现范围为 ReAct、Plan–Executor、停止和故障接管,尚未编写这些新实现。外层 Workflow 计划复制并适配 LangGraph 的状态、同步与检查点;本轮不开放外层编排执行及用户入口。 第 10 节 AgentNode 的稳定调用边界保留,fork/Join/循环的自研算法及其他章节的外层专属索引、字段和断言均须按源码适配决策重新映射,不作为外层最终编码指令。用户运行操作仍为输入、补充和停止;waiting、接管和重新激活是内部状态。

1. 所有权、配置与共同类型 ​

内部 Agent 的 AgentGraphEngine 是调度权威;本文内部算法中简称 GraphEngine。NodeDriver 是其执行组件,CallBatchCoordinator 只准备批次变更。外层由独立 WorkflowEngine 推进;两个内核共用根 Run 的租约、控制、调用与存储,不共用图 ready 队列。恢复按 engine_kind 分派,整体接管执行树;详细边界见双引擎协议。节点实现只含不可变配置,任务私有数据只在 continuation。平台事实均通过 CommitCoordinator.commit(RuntimeCommit, ExecutionLease) 提交,不得让 BudgetService、批次协调器或恢复器单独提交同一事实。

固定默认值:业务 Loop 合计 100;Run 从创建起 3600 秒;并行关闭;开启后每个 Planner 批次最多 2 个活跃 Executor;自动重试关闭,开启时同 Operation 共 2 次 Attempt、间隔 2 秒;工具单次 10 秒;模型首有效输出 60 秒。Executor 和压缩继承主模型;压缩不占业务票据。有效截止取根与适用 Invocation 局部截止的最小值,排队、Hook、压缩、重试与恢复均计时。

python
Id = str                             # 服务端 UUID;示例短 ID 仅用于文档
Revision = int                       # 普通实体初始1;Context effective revision/record_head可0;实际变更递增
JsonValue = None | bool | int | float | str | list | dict
GraphRef = GraphExecutionRef
TaskRef = NodeTaskRef
Phase = Literal["prepare_context", "prepare_model", "compact_context", "call_model",
                "interpret_response", "wait_batch", "consume_results", "validate_output", "completed"]

@dataclass(frozen=True)
class ExpectedVersions:
    run_revision: Revision | None     # 控制门以锁内最新值检查;不要求所有独立调用共用旧 revision
    graph_revisions: dict[Id, Revision]
    task_revisions: dict[Id, Revision]
    operation_revisions: dict[Id, Revision]
    context_revisions: dict[Id, Revision]

@dataclass(frozen=True)
class WaitCondition:
    kind: Literal["batch_settled", "budget_available", "operation_completed", "child_completed"]
    ref_id: Id                       # WaitGroup / budget scope / Operation / child invocation
    generation: int                  # 固定批次代次;budget 使用申请记录的代次
    observed_revision: Revision

@dataclass(frozen=True)
class ReactContinuationV1:
    protocol_version: Literal[1]
    phase: Phase
    role_key: str
    role_binding_ref: DefinitionRef
    context_revision_ref: ContextRevisionRef
    role_loop_index: int             # 已创建业务模型 Operation 的最大序号;初始 0
    current_loop_ticket_id: Id | None
    model_operation_id: Id | None
    pending_batch_id: Id | None
    pending_operation_refs: tuple[PendingOperationRef, ...]
    compaction_ref: Id | None
    supplement_delivery_watermark: int
    projection_ref: PayloadRef | None
    accepted_decision_ref: PayloadRef | None
    validation_feedback_ref: PayloadRef | None
    final_output_ref: PayloadRef | None

NodeTask 额外保存 ready_seq:int、resume_input_ref:PayloadRef?、activation_no:int;ready_seq 由所属图提交分配,不使用进程时间排序。resume_input_ref 保存结果引用和 WaitCondition 的 generation,不保存 Future。CallBatch.delivery_state=unconsumed|consumed;成员 status=queued|admitting|admitted|settled。admitting 只表示准入命令已提交、正在 begin/前置处理,不能当作 Executor 已启动。WaitMember 同步使用对应状态;成员集合、ordinal、call_id 一经冻结不可改动。

所有系统 ID 初次由服务端分配并随创建意图提交;重试按稳定逻辑键查回 ID,不按参数 hash 合并调用。相同键与不同载荷返回 IDEMPOTENCY_CONFLICT。通知仅提供“重新读取事实”的提示,不能携带覆盖当前状态的权威值。

2. GraphCompiler:输入、编译产物及确定校验 ​

python
@dataclass(frozen=True)
class CompileOptions:
    compiler_version: str
    supported_node_abis: dict[str, tuple[str, ...]]
    structured_workflow_enabled: bool  # 发布能力;本轮运行部署为 False

@dataclass(frozen=True)
class Diagnostic:
    code: str
    path: str                        # JSON Pointer,定位 node/route/join/loop
    related_paths: tuple[str, ...]
    message: str

@dataclass(frozen=True)
class CompiledGraphDescriptor:
    graph_ref: GraphDefinitionRef
    engine_kind: Literal["agent"]    # 本结构为内层描述;外层使用专属描述
    engine_protocol_version: int
    bindings_hash: str
    compiler_version: str
    node_abi_manifest: tuple[NodeABIRef, ...]
    node_plans: dict[str, CompiledNodePlan]
    routes_by_source: dict[str, tuple[CompiledRoute, ...]]
    region_by_node: dict[str, str | None]
    loop_stack_by_node: dict[str, tuple[str, ...]]
    join_plans: dict[str, CompiledJoinPlan]
    entry_node_key: str
    max_task_creations: int           # 服务端保护常量 10000,不是业务 Loop 配额
    descriptor_hash: str

class GraphCompiler(Protocol):
    async def validate(self, graph: GraphDefinitionRef, bindings: tuple[NodeBinding, ...],
                       options: CompileOptions) -> tuple[Diagnostic, ...]: ...
    async def compile(self, graph: GraphDefinitionRef, bindings: tuple[NodeBinding, ...],
                      options: CompileOptions) -> CompiledGraphDescriptor: ...

前置:引用为不可变版本,载荷 hash 正确,bindings 已解析为发布期授权快照。validate 只读且无运行副作用;诊断按 (path,code,related_paths) 排序。compile 调用相同校验,存在任何 error 就抛 GRAPH_INVALID(diagnostics),不返回部分计划;成功只返回可序列化不可变描述,不缓存凭据或可执行实例。启动和恢复重新验证部署 NodeRegistry 含固定 ABI;缺版本报 NODE_ABI_UNAVAILABLE,不能替换最新实现。

编译采用以下顺序,前一轮缺失引用只抑制依赖它的检查,其他独立错误继续报告:

  1. 校验定义 schema/version/hash;建立 node、route、join、loop 字典,重复键报 GRAPH_DUPLICATE_KEY。拒绝未知 node_type、悬空目标、未知字段 read/write;一个节点恰有一个匹配 binding。GraphDefinition 的规范字段以 data-contracts 为准,旧 entrypoints/edges/state_schema 字段不能并行生效。
  2. 校验类型:react/planner/executor 绑定 role 且 kind 一致;capability 绑定固定能力版本;agent 绑定固定 AgentRelease。生成 ReAct 图必须一个 react 入口且无 executor;生成 Plan 图必须一个 planner 入口、零到多个 executor,只有 planner 的 dispatch 可到 executor;executor 不准 dispatch,根角色不能作为委派目标。
  3. 校验映射:映射只允许已注册的纯表达式 AST(literal、授权 path、object、array、固定纯 converter);禁止 eval、任意 Python、网络、读取私有 context。每个读取路径必须在 read_set/授权映射内,输出字段必须在 write_set/allowed_writer_nodes 内;节点 input/output 与映射结果经过冻结 JSON Schema 校验器检查。
  4. 静态类型关系使用受限 schema profile:type、properties、required、additionalProperties、items、enum、const、nullable;required 目标必须由映射提供,源枚举须为目标子集,数字范围须包含关系。无法证明兼容即 GRAPH_TYPE_UNPROVEN;复杂业务约束仍在每次运行前后验证,不能将静态未知当兼容。复杂 schema 可用于运行验证,但映射类型须附发布时可验证的 profile。
  5. 注册的 predicate/reducer/converter 必须固定版本、hash 和 pure ABI;编译器只接受平台纯函数注册表,不相信用户输入的 pure=true。conditional 为固定有限分支并含 default;一次完成位置只准选择一个 direct/conditional/branch_exit,或一个 fork。dispatch 不属于普通完成边,不参与拓扑环判定。普通多出边、多个隐式入口均报 GRAPH_IMPLICIT_FANOUT;多个逻辑起点须由一个显式 fork 入口产生。
  6. 入口恰有一个;先构造含普通路由、fork 入口、虚拟 Join 后继及 loop 回边的控制流图。检查所有运行位置从入口可达、每个位置可达合法 finish/branch_exit/loop exit;executor 的可达性按 planner dispatch 单独计算。无出口报 GRAPH_NO_EXIT。发布期不执行 predicate,运行期只选择预编译枚举项。
  7. 校验 loop 集合两两不交或严格包含;header 在 body 中,所有从外部进入 body 的边均指向 header。每条 back_route 从 body 指向本 loop header;每条 exit_route 从 body 指向外层同级。计算 dominator,header 必须支配 body 全部位置;删去已声明 back_routes 后做 Kahn 排序,残余环报 GRAPH_UNDECLARED_CYCLE。循环声明不能通过遗漏 body 节点规避检查。
  8. 校验每个 branch_scope Join 恰配一个 fork;fork targets 为有序入口列表。删除回边后,从每个入口沿区域内路由枚举可达节点,遇该 Join 的 branch_exit 停止;各分支集合必须互斥,集合并集必须等于 region_node_keys;逃逸、跨分支边、交叉 Join 和未闭合分支内 fork 报 GRAPH_UNSTRUCTURED_REGION。分支中 loop 必须完整包含于本分支;包围 fork 的 loop 必须同时包围配对 Join 和后继,回边不能穿过 open Join。
  9. 校验每个分支所有 sink 都是声明的 branch_exit;Join 的成功/错误目标必须位于 fork 的包围 loop 层,不能跨层返回。没有成功目标须符合 finish_rule;错误目标可空,表示归并失败即图失败。静态算出每分支可能写集;两个可同时选中分支可能写同 single_writer 字段时报 GRAPH_WRITE_CONFLICT,除非选择器已以枚举证明二者互斥。reducer 字段允许多写且固定顺序。
  10. 规范化 descriptor:对象键按 UTF-8 字节排序;语义顺序数组(fork target、Hook、映射写序)保留,不按名称重排;bindings/node ABI 集合先按其稳定键排序,删除易变时间/缓存字段及 descriptor_hash 自身;JSON 禁止 NaN/Infinity,字符串保留原 Unicode 值。以规范 JSON 的 SHA-256 固定 descriptor_hash,连同 compiler_version 和 node ABI 写入发布。工作流任务创建保护计数达到 10000 后拒绝第 10001 次并报 GRAPH_TASK_LIMIT;它不取代 root/local LoopTicket;GRAPH_TASK_LIMIT 为 Runtime 系统控制失败,不把它交给 LLM 虚构重试。

编译产物中的 region/loop 索引是运行时唯一依据;运行期不通过搜索当前任务猜结构。纯函数计算可在事务外运行,提交必须验证所读版本;纯函数抛错只产生确定失败结果,不重选默认路径。

3. NodeOutcome、Driver 和激活状态机 ​

python
@dataclass(frozen=True)
class Progress:
    continuation: ContinuationRef
    effects: tuple[PreparedEffect, ...]  # 只允许本阶段的强类型 RuntimeCommit 子命令
@dataclass(frozen=True)
class Wait:
    continuation: ContinuationRef
    condition: WaitCondition
    resume_input_ref: PayloadRef
    effects: tuple[PreparedEffect, ...]
@dataclass(frozen=True)
class Complete:
    output_ref: PayloadRef
    continuation: ContinuationRef
    writes: tuple[PreparedTaskWrite, ...]
@dataclass(frozen=True)
class Fail:
    error: RuntimeErrorRecord
    partial_result_ref: PayloadRef | None
    accounting: LoopAccountingOutcome | None
NodeOutcome = Progress | Wait | Complete | Fail
ActivationStream = AsyncGenerator[NodeOutcome, CommitReceipt]

class Node(Protocol):
    def activate(self, input: NodeInput, continuation: ContinuationRef | None,
                 runtime: NodeRuntime) -> ActivationStream: ...
class NodeDriver(Protocol):
    async def activate(self, task: TaskRef, claim: ExecutionLease) -> ActivationReceipt: ...
class GraphEngine(Protocol):
    async def start(self, request: CreateGraphExecution, scope: ExecutionScope) -> GraphRef: ...
    async def dispatch(self, request: DispatchTasks, scope: ExecutionScope) -> DispatchReceipt: ...
    async def drive(self, run_id: Id, claim: ExecutionLease) -> RunHandle: ...
    async def notify(self, hint: CommittedEvent, claim: ExecutionLease) -> None: ...
    async def request_stop(self, scope: ControlScope, reason: StopReason,
                           claim: ExecutionLease) -> StopReceipt: ...

RuntimeErrorRecord 固定字段为 {code:str, message:str, scope_kind:run|invocation|task, scope_id:Id, source_operation_id:Id?, details_ref:PayloadRef?};message 已脱敏,控制优先级由 code/scope 决定,不按错误文案猜测。activate 只能被 GraphEngine 调用。前置:任务 ready,所属图未终态、scope 允许推进、ABI 匹配、lease 有效;提交 ActivateTask 后才创建生成器。后置:返回值是最后已提交 receipt;任何终结 outcome 后不再调用生成器。dispatch 只登记授权位置、专属输入、稳定成员键,不直接启动协程;notify 幂等重新读取相关事实,不修改模型输入。start 要求父子关联或根初始化已授权,提交初始图与入口意图后返回。

当前 Task.status输入/条件原子结果后续
readyActivateTask,CAS 命中running、新 activation_no/id、固定 start revision重建 NodeRuntime 和 generator
runningProgressrunning、revision+1、NodeCheckpoint/effectsreceipt 传回 generator 后继续
runningWait,条件未满足waiting、wait/resume 引用、activation ended释放 Driver worker
runningWait,提交时条件已满足ready、同一 wait/resume 引用、ready_seq、activation ended重新入队;避免丢唤醒
waiting条件已满足且 scope 开放ready、持久 resume 引用、ready_seq新 activation,原 task/context
runningCompletesucceeded、唯一 TaskResult/TaskWrite/后继意图释放 Executor 槽、通知父管道
runningFailfailed、唯一 TaskResult/错误路由父成员错误或 Invocation 收束
ready/running/waiting控制收束先关 scope;静默后 cancelled/failed保留原因,不再业务激活
succeeded/failed/cancelled重复结果/通知返回原结果,无 revision 变化不再执行 Node

Driver 唯一实现选用 async generator 的 asend(receipt) 握手,禁止“yield 之后立即自行继续”的后台任务:

python
async def activate(task_ref, lease):
    start = await commits.commit(plan_activate(task_ref), lease)
    state = await reader.load_task(task_ref)               # 读取已提交 continuation
    stream = registry.node(state.binding).activate(state.input, state.continuation, runtime(state))
    try:
        outcome = await anext(stream)
        while True:
            validate_outcome(state, outcome)              # phase、写集、所属身份、引用完整性
            prepared = await engine.prepare_outcome(state, outcome)  # 纯路由/映射;不持锁
            receipt = await commits.commit(prepared, lease)
            state = receipt.task_view
            if isinstance(outcome, (Wait, Complete, Fail)):
                await stream.aclose()
                return ActivationReceipt(receipt)
            outcome = await stream.asend(receipt)          # 只有已提交 Progress 可以继续
    except CommitConflict as conflict:
        await stream.aclose()
        return await reconcile_committed_or_requeue(task_ref, conflict.commit_key, lease)
    except (LeaseLost, PersistenceUnavailable):
        await stream.aclose()                             # 不伪造业务 Fail;由接管续原事实
        raise
    except StopAsyncIteration:
        return await fail_contract(task_ref, "NODE_MISSING_TERMINAL_OUTCOME", lease)
    except Exception as exc:
        return await fail_contract(task_ref, "NODE_IMPLEMENTATION_FAILED", lease, sanitize(exc))

validate_outcome 拒绝越权状态写、不可序列化对象、跨角色 context、未提交 result 引用,以及 completed phase 搭配 Progress。CommitConflict 单独处理:先关闭旧 generator,按提交键查 receipt;已提交且任务终态则返回原 receipt,否则从已提交 continuation 将原任务重新排入 ready。未提交则重读并重新准备原阶段。不得盲目重放旧 generator 的副作用。取消必须由已持久控制信号触发,进程异常不是用户停止。

4. ReAct 各阶段的确定推进 ​

共同入口:每次从准备阶段开始新业务模型前先检查 root/local 已消费额度、停止和截止;耗尽时不先压缩。已有最后一张已消费决策的动作不做“consumed==limit”拒绝,只检查 scope 是否已经关闭、停止及截止。Operation 的 begin/prepare/execute/recover 由调用管道提供,以下 prepare_* 返回可供同一 RuntimeCommit 提交的强类型 effect;它们不能偷偷推进节点状态。

阶段必需引用/入口事实唯一成功输出与下一阶段
prepare_context本 task context;上批已 consumed 或首次输入固定 projection/ref/revision/补充读取水位 → prepare_model
prepare_modelprojection;尚无本轮 operation 或已有同 purpose operation固定 operation/role_loop_index,请求 Hook 完成 → compact_context 或 call_model
compact_context当前 operation、compaction_ref、请求有效 revision压缩提交新 revision、重组并硬窗口验证 → call_model
call_modeleffective 请求冻结、ticket 已预留或可申请completed EventResponse 引用 → interpret_response;非终态则等待同 operation
interpret_responsecompleted effective 响应,未接受过该 operation接受决策+消费 ticket+冻结 batch → wait_batch;无动作 → validate_output
wait_batchpending_batch_id、固定 WaitGroup全成员 settled → consume_results;否则 Wait
consume_results全成员结果引用、未 consumed一次 ContextDelta+consumed+清空本轮引用 → prepare_context
validate_output已接受候选+已消费 ticket本地 schema 合规 → completed;不合规反馈+清空本轮引用 → prepare_context
completedfinal_output_refComplete;不请求模型或能力

4.1 prepare_context 与 prepare_model ​

  1. 检查根控制,再检查 Invocation 控制;返回控制错误时走第 8 节。对新 Loop,任何层 consumed==limit 就持久关闭对应 scope。reserved 暂满仍可读取已有上下文,但不得运行压缩,先登记 budget_available 等待。
  2. 根 ReAct/Planner 领取顺序水位以内的 pending UserInputDelivery;Executor 不领取用户补充。此时只是选入 projection,冻结请求时才 claimed。ContextManager 按固定发布 recent_runs=2、角色私有范围、已提交 effective revision 生成 projection;同 input_id 去重。已发生的上下文 Hook 修改通过重新读取 authoritative revision 生效。
  3. 以 (task_id,"context-prepare",role_loop_index+1,source_revision,input_watermark) 形成准备键,提交 projection_ref/context revision/水位和 phase=prepare_model。准备期间新到补充留下一轮;不能补写到已经冻结请求。
  4. 新模型 Operation 的 purpose_key 固定 business-model:{role_loop_index+1}。不存在时在 Progress 同事务保存 prepared Operation 意图、递增角色序号、写 model_operation_id;存在则复用,不递增。压缩 Operation 使用不同 purpose,不增加此序号。
  5. 调用 InvocationPipeline.prepare_business_model(op_ref, scope) -> PreparedModelRequest | FinalResponseRef,请求 Hook 已提交而 Operation 仍保持 request_hooks,等待模型执行许可。前置 deny 作为模型失败按角色范围处理;Hook 自身错误按调用规范 fail-open。请求与 context 的有效 revision 必须一致,不允许 Driver 从原 projection 覆盖 Hook 结果。
  6. ContextManager 用 Hook 后有效请求计算软阈值和硬窗口。触发压缩则创建/复用 compaction:{model_operation_id} 的 CompactionEpisode,保存 compaction_ref,Progress 到 compact_context;不触发则到 call_model。没有工具调用、网络等待或 Hook 在数据库锁内发生。

4.2 compact_context 与 call_model ​

python
async def compact_context(c, runtime):
    check_new_loop_control(c.task_scope)                     # 耗尽先终结,不白做压缩
    prior = await compactions.inspect(c.compaction_ref, runtime.context_read_authority)
    episode = await compactions.advance(c.compaction_ref, prior.episode_ref.revision, runtime.lease)
    if episode.pending_operation_ref:
        return Wait(c, operation_condition(episode.pending_operation_ref), resume_ref(episode), ())
    if episode.status == "cancelled": return Fail(error=current_control_error(runtime.scope))
    if episode.status == "failed":
        return Fail(error="COMPACTION_FAILED", scope="invocation")
    if episode.chunk_cursor < episode.chunk_count:
        return Progress(c, effects=())                    # 安全点后再推进下一块
    result = await compactions.publish(c.compaction_ref, episode.episode_ref.revision, runtime.lease)
    candidate = await pipeline.prepare_business_model(c.model_operation_id, runtime.scope)
    if is_final_response(candidate): return Progress(phase="interpret_response", effects=(reference(candidate),))
    rebuilt = await contexts.materialize_prepared_model(candidate.ref, current_context_revision(c), result.snapshot_id)
    if rebuilt.over_hard_limit:
        return Fail(error="CONTEXT_WINDOW_EXCEEDED", scope="invocation")
    return Progress(phase="call_model", context_revision_ref=rebuilt.revision,
                    effects=(rebuilt.request_binding_effect,))

async def call_model(c, runtime):  # 阶段伪代码:yield用于commit-ack,return表示下一outcome
    op = await invocations.get(c.model_operation_id)
    if op.completed:
        return Progress(phase="interpret_response", effects=(reference(op.response),))
    if not op.request_frozen:
        candidate = await pipeline.prepare_business_model(op.ref, runtime.scope)
        if is_final_response(candidate): return Progress(phase="interpret_response", effects=(reference(candidate),))
        prepared = await contexts.materialize_prepared_model(candidate.ref, current_context_revision(c), applicable_snapshot(c))
        admission = await budget.prepare_reserve_loop(c.task_ref, op.ref)
        if admission.wait:
            return Wait(c, admission.condition, admission.resume_ref, ())
        if admission.control_error:
            return Fail(admission.error)
        yield Progress(c.with_ticket(admission.ticket), effects=(
            prepared.freeze_request, prepared.claim_inputs, admission.reserve_ticket,
            prepared.execution_permit))
        # receipt 后才能出网;事务内二次检查 revision、lease、root/local 控制与容量
    await pipeline.schedule_resume(op.operation_id)       # 只提示调用管道推进,不等网络
    # 未结束只返回可恢复 operation wait;完整响应已提交才推进
    response = await invocations.get_result(op.ref)
    return Wait(c, operation_condition(op), resume_ref(op), ()) if response is None else \
           Progress(phase="interpret_response", effects=(reference(response),))

压缩的内部冲突、覆盖范围与增量接回算法由 context-capabilities 规定;本模块恢复固定 episode,不创建第二个压缩节点或调度器。阶段伪代码中的 Progress(phase=...) 是构造新 continuation 和 effects 的简写,实际对象严格使用第 3 节签名;Fail(error=...,scope=...) 同样封装为 RuntimeErrorRecord,不接受未声明 kwargs。压缩失败在任何角色都终结所属 Invocation。已有冻结请求不可热改;最终冻结时 revision 冲突要重建未发请求,不重复已提交请求 Hook;同业务 Operation 最多一个 CompactionEpisode,已有 episode 成功后不能为了换源 revision 再压一次。旧摘要不适用时按上下文契约重建视图,仍超硬窗口即失败;没有已有 episode 时才按软阈值判断首次压缩。commit_model_ready 在锁内发现容量被其他任务预占时以 BUDGET_ADMISSION_CHANGED 拒绝本次未提交效果;Driver 重读后落 budget Wait,不能把准备阶段的 granted 当成已授予。该码按 CommitConflict 分支处理。Pipeline.schedule_resume(operation_id) -> None 只合并本机提示;实际由 start_or_resume(operation_id, lease) -> PipelineProgress 推进唯一 Attempt/重试循环。提示丢失由持久待处理事实扫描补齐,不使等待父任务占 Driver worker。

4.3 interpret_response、wait_batch、consume_results ​

  1. 读取 EventResponse,先检查是否已经接受该 operation;已接受就读取其下一 phase,不再次 append、consume 或分派。failed/blocked 不构造空 ModelDecision,按第 7 节角色失败会计处理。
  2. normalizer 必须得到完整消息;正文和 actions 都空、重复 provider_call_id、无法定位参数片段为 MODEL_PROTOCOL_ERROR。该检查在 ModelGateway completed 前已执行,响应 Hook 修改后的有效结果还须再次验证;已有 completed 结果不退回新 Attempt。
  3. 仅按当前角色冻结目录解释动作名称。每项复用raw结果提交时固定的 ordinal 和服务端 call_id;未知名称、未授权、单项参数 JSON/schema 错误成为 rejected 成员,其错误含原 provider_call_id/ordinal,不影响其他合法项。文本与动作共存时文本只是过程消息。
  4. 调用 ContextManager.prepare_accept_model(context_ref, operation_response, mutation_key="model:{operation_id}:accept") -> ContextDelta,它不独立提交;同 RuntimeCommit 保存消息引用、accepted_decision_ref、ticket consumed、business_loop_no、phase。有动作同时创建 CallBatch/WaitGroup,全部成员及本地 rejected 结果一次冻结;此事务不调用 begin。无动作固定 final_output_ref 并进入 validate_output。
  5. 有 batch 则 yield Wait,条件为 (batch_settled,wait_id,generation);冻结 batch 时已提交父等待事实/准入意图,故响应恰好先结算也不会丢唤醒。GraphEngine 在同一事实上执行第 5 节准入。
  6. 恢复 wait_batch 时,依据所有成员是否 settled 计算,不能根据最后一条通知推断;全部 settled 就 Progress 到 consume_results,否则再次 Wait 原 wait_id,不建立新 batch。
  7. consume_results 按 ordinal 读取唯一 effective EventResponse 或 local_outcome。ContextManager.prepare_batch_delivery(context_ref, batch_id, ordinal_results) -> ContextDelta 生成 mutation_id=batch:{batch_id}:deliver:填充原模型消息的结果槽,读取实际有效参数/结果,替换同一 pending feedback 槽。提交 ContextDelta、batch consumed、task 的消费键/水位及 prepare_context phase;同事务不得留下“上下文追加了但 consumed 未写”。
  8. 清空 current_loop_ticket_id/model_operation_id/pending_batch_id/accepted_decision_ref/compaction_ref/final_output_ref,保留 role_loop_index、context revision 与补充水位。旧引用仍在不可变历史;无新模型调用前不产生新票据。

4.4 validate_output ​

无 output_schema 时以 effective 完整正文为自由文本;有 schema 时按配置编码解析候选,再执行本地冻结 schema 验证。无效 JSON 也为输出校验错误,不是供应商网络重试。诊断最多 20 个,按 JSON Pointer 排序,每项保留 code/path/expected,单项说明最多 512 字符;完整原候选仍保留。

通过则直接 Complete(continuation.phase=completed),生成唯一 TaskResult;不通过则以 validation:{model_operation_id} 保存候选与修正反馈 ContextDelta、phase=prepare_context,并清空本轮引用。该决策此前已 consumed;修正必须申请下一张票据,不能直接再次执行旧 Operation。根最终交付还须通过 Run 终态事务;Executor Complete 只给父委派管道 raw 结果,不直接给用户答案。完成与新输入竞态由输入篇规定的 Session/Run 事务仲裁;本模块不得让迟到 Complete 覆盖已经关闭的 scope。

5. 批次准入、Executor 槽位和结果汇合 ​

python
@dataclass(frozen=True)
class BatchPolicy:
    batch_key: str                     # business-model Operation 对应的稳定逻辑键
    parallel_enabled: bool
    executor_max_parallel: int         # >=1;并行关闭时仍按串行闸门
@dataclass(frozen=True)
class AdmissionToken:
    token_id: Id
    batch_id: Id
    member_id: Id
    state: Literal["reserved", "converted", "released"]
    child_task_id: Id | None
@dataclass(frozen=True)
class PreparedBatch:
    batch_id: Id
    wait_id: Id
    generation: int
    effects: tuple[PreparedEffect, ...]
class CallBatchCoordinator(Protocol):
    async def freeze(self, owner: TaskRef, decision: AcceptedModelDecision,
                     policy: BatchPolicy) -> PreparedBatch: ...
    async def freeze_requests(self, owner: TaskRef, events: tuple[EventRequest, ...],
                              policy: BatchPolicy) -> PreparedBatch: ...
    async def plan_settlement(self, batch: CallBatchRef, member_id: Id,
                              response: FinalResponseRef) -> PreparedMemberSettlement: ...
    async def plan_delivery(self, batch: CallBatchRef,
                            context: ContextRevisionRef) -> PreparedBatchDelivery: ...
class GraphEngineBatchPort(Protocol):
    async def advance_batch(self, batch: CallBatchRef,
                            claim: ExecutionLease) -> AdmissionReceipt: ...
    async def register_executor(self, request: PreparedDelegate,
                                claim: ExecutionLease) -> DispatchReceipt: ...

freeze 前置为同一未接受决策,后置仅准备全部成员与 WaitGroup;同 model_operation_id 只能一批。plan_settlement 只接受该成员原 Operation 的最终响应或经校验的 local_outcome,后置包含成员结果、settled 与可能的父唤醒。plan_delivery 前置全体 settled,后置带 ordinal 排序的结果与一次性 ContextDelta。freeze_requests 是 EventBus.request_many 的纯准备入口,复用相同冻结内核;要求可信 scope 含 node_task_id,缺少则拒绝。所有准备方法均不建协程、不提交状态;调用门面把 prepared effects 交统一 Coordinator,随后只由 GraphEngine 准入。

Executor 活跃数严格等于此 batch 下已经创建并准入、尚无终态 TaskResult 的 Executor NodeTask 数;waiting 子任务仍活跃。单独的 admission token 是防止并发前置 Hook 越过上限的内部预留,不记为活跃实例;queued 成员没有 token。开启并行时保持 active_executor_count + reserved_admission_tokens <= executor_max_parallel。token 不按 TTL 自动释放:结果未知时恢复原准入流程,不能因为进程死了便再放另一项。

GraphEngine 对每个批次采用以下唯一算法;任何重复 advance 都在相同锁集合中重读,不依赖本地 Semaphore:

python
def plan_admissions(batch, root, invocation, active_tasks, tokens):
    if scope_closed_or_expired(root, invocation):
        return cancel_unbegun_members(batch)              # 无 Operation 的成员才用 local_outcome
    remaining = [m for m in batch.members if m.status != "settled"]  # ordinal 已冻结
    if not remaining:
        return wake_owner_once(batch.wait_id, batch.generation)
    if not batch.parallel_enabled:
        first = remaining[0]
        if first.status != "queued":
            return []
        effects = reserve_admission_token(first) if first.action_kind == "executor" else []
        return effects + begin_intent(first)              # 同一准入事务,串行也需要令牌
    free = batch.executor_max_parallel - count_active(active_tasks) - count_reserved(tokens)
    effects = []
    for m in remaining:                                  # 固定 ordinal 扫描,不由完成快慢重排
        if m.status != "queued":
            continue
        if m.action_kind == "executor":
            if free == 0:
                continue                                 # 普通工具仍可启动,避免被满槽位阻塞
            effects += reserve_admission_token(m)
            free -= 1
        effects += begin_intent(m)
    return effects

reserve_admission_token 和 begin_intent 都只返回写集,不自行提交。选中 Executor 时,无论串行还是并行,T_MEMBER_ADMIT 都将令牌、成员 queued→admitting、服务端 call_id/purpose_key 及待 begin 意图放在同一事务;令牌唯一键为 (batch_id,member_id)。普通工具不创建 Executor 令牌。串行上一成员未 settled 不准入下一成员,因此最多一项占用/预留;重复 advance 看到 admitting 后不再预留。

提交后才由调用设施 begin,返回 PendingOperationRef;重复投递意图复用同 Operation。begin 从可信主体+purpose_key 取准入意图,在同一 CommitOperation 中创建 processing_phase=prepared,status=pending 的 Operation,并写 BatchMember.operation_id、WaitMember.result_operation_id 和 operation target;之后才推进前置处理,不授权创建 Executor。T_MEMBER_ADMIT 已提交而 T_BEGIN 未提交时,BatchMember/WaitMember 都为 admitting,调用和目标引用均为空;此现场合法,恢复只能补投原 begin 意图,不能退回 queued 或另建令牌。T_BEGIN 提交后、子任务创建前仍为 admitting,target_ref 指向原 Operation。快速完成也已有父成员关联;begin 回执丢失按原 key 取回同关联。串行有任意 admitting/admitted 成员时禁止 begin 后续成员,普通工具与 Executor 共用此闸门。

委派前置链完成后,register_executor 必须读 batch/member/admission token/control;验证目标是本图允许 executor binding、最终输入已通过授权/schema,再在一笔提交中创建 child Task/context、保存父 Operation 和 WaitMember 的 child 引用、将 token reserved→converted、登记 ready 意图。此时 active+1/token−1;child 的 graph_execution_id/invocation_id 与 Planner 相同。不能为 Executor 调 AgentInvocationService.start。

前置 deny 不创建 child;已有 Operation 形成 blocked 响应后,target_ref 指向 Operation,提交 member settled 和 token released。参数在 begin 前拒绝则保存 local_outcome、无 Operation/token/child。停止时尚未 begin 的成员同样 local_outcome cancelled;已经 begin 必须沿原 Operation 结算,不能删除引用改为本地取消。admitting 但 begin 未落盘时,停止事务先关闭 begin 意图并释放 token,迟到 begin 必须重新检查 scope。

子任务终态事务同时保存 TaskResult/TaskWrite 和父 delegate Operation 的 raw-result 关联,token 已 converted,不另扣减 token;active 投影自然减一。并行立即可补位,串行仍等本成员最终 effective 响应完成。后置 Hook 慢、拒绝、失败处理与原 Operation 关联;member 仅在最终响应提交后 settled。最后一个成员结算事务登记原 owner ready;owner 不因单项失败提前恢复模型。

空 actions 走最终输出路径,不创建批次;供其他调用方使用的显式空 request_many 则冻结空批次并立即 settled,返回空列表。每个成员只有一个响应槽;批次输入顺序不依赖调用完成顺序。批次 consumed 仅一次,重复通知返回原 delivery receipt。

6. 就绪队列与公平推进 ​

AgentGraphEngine 的内存 ready heap 是内部 Agent 已提交任务的投影,按 (ready_seq,graph_execution_id,node_task_id) 排序;每个任务最多一个当前 heap 项。同一 Run 的多个内层 Agent 图可以由该引擎统一公平推进,外层 Workflow 的原生任务不加入此 heap,由 WorkflowEngine 自己调度。RunRuntime 仅分配根控制和驱动各所属引擎,不建立第三份任务队列。内层每次 drain 先处理已提交结果/控制事件,再准入批次,再激活 ready 任务;每个 task coroutine 在一次已提交 Progress 后 await asyncio.sleep(0) 交还事件循环。此操作不创建新 NodeActivation,也不改变业务顺序。

Driver worker 只限制短阶段推进的本机资源,不用于业务 Executor 配额;等待 operation、批次、预算、子图时提交 Wait 并释放 worker。Invocation 管道的网络任务不持 Driver worker。不能让父 Planner 持本机所有 worker 等待子任务。配置 parallel_enabled 只控制同批成员准入;Executor 内部模型/工具仍按该 Invocation 发布规则运行。

唤醒采用“提交等待时再查谓词+结果提交时登记 ready”双向检查:若结果先提交,Wait 事务直接产生 ready;若 Wait 先提交,结果事务唤醒;两个事务按相同父 task/graph 锁串行。已 ready 或 terminal 的重复 wake 是 no-op。预算唤醒按持久申请序 (request_seq,task_id) 重新检查,每次只授权满足两级容量的前项;释放或消费任一票据都触发重查,控制事件优先。没有忙循环轮询模型/工具状态。

7. LoopTicket:单账本、根与局部约束 ​

python
@dataclass(frozen=True)
class LoopAdmission:
    kind: Literal["granted", "wait", "control_error"]
    ticket: LoopTicket | None
    condition: WaitCondition | None
    error: RuntimeErrorRecord | None
    effects: tuple[PreparedEffect, ...]
class BudgetService(Protocol):
    async def prepare_reserve_loop(self, owner: TaskRef,
                                   model_operation: OperationRef) -> LoopAdmission: ...
    async def prepare_settle_loop(self, ticket: LoopTicketRef,
                                  outcome: LoopAccountingOutcome) -> PreparedBudgetEffect: ...
    async def check_deadline(self, scope: ExecutionScope) -> ControlDecision: ...

LoopTicket 补充 consumption_kind:decision|error_feedback|null:reserved/released 为 null,consume 同事务写明完整决策或失败反馈;无 Attempt 的拒绝反馈必须为 error_feedback。这些是现有 reserve_loop/settle_loop 门面的事务准备接口,预算权威仍为 LoopTicket 账本。真正 grant 必须随 commit_model_ready 在 Coordinator 锁内重算,准备时的 admission 不是可出网凭证。TaskRef 解析 run/invocation,不接受调用方传入 limit/deadline。local 限制仅 budget_scope=invocation 存在;直接 Agent budget_scope=run,不重复限制一遍。

python
def validate_and_reserve(tx, task, op, lease):
    root, local = lock_and_read_root_invocation(tx, task)   # 所有后续锁遵守第 9 节
    validate_lease(root, lease, tx.database_now)
    old = lookup_ticket(op.operation_id)
    if old is not None:
        return old                                       # 既有事实;执行仍另查 control/permit
    if root.is_closing or now() >= root.deadline_at:
        return persist_root_close(root.reason_or_timeout)
    if root.consumed == root.limit:
        return persist_root_close("RUN_LOOP_EXHAUSTED")
    if local and (local.is_closing or now() >= local.deadline_at):
        return persist_local_close(local.reason_or_timeout)
    if local and local.consumed == local.limit:
        return persist_local_close("INVOCATION_LOOP_EXHAUSTED")
    if root.consumed + root.reserved == root.limit or local_reserved_full(local):
        return persist_budget_wait(task, op)
    ticket = new_ticket(op, task, state="reserved", business_loop_no=None)
    increment_reserved(root, local)                       # 同一票据的事务投影,不独立扣款
    return ticket

now() 在整笔事务使用同一个 database_now;同准入点根错误优先于局部错误。consume 仅允许 reserved→consumed,递减 reserved、递增 consumed,business_loop_no 取根 consumed 的新值;release 仅 reserved→released。终态票据重复同 outcome 返回原 receipt;consume↔release 冲突报 LOOP_TICKET_STATE_CONFLICT,禁止重新分配。始终断言两级 0<=consumed+reserved<=limit。

情况票据与关联事实控制结果
完整 effective 决策被接受consume + 模型接受 ContextDelta + phase/batch允许本决策动作,包括最后一次
输出不合格式原票据保持 consumed,修正反馈提交下一 Operation 另申请;无额外修正池
同模型透明重试复用 reserved 与冻结请求新 Attempt 仍查停止/截止/permit
根 ReAct/Planner 模型最终 failed/blockedrelease + 角色失败 + Invocation close直接 Agent 结束 Run;外层交失败
Executor 模型最终 failed/blocked已有ticket则consume;无ticket按下述失败反馈准入;与子 TaskResult/父 PendingFeedbackSlot同提交父批次仍等全体;错误只占该模型的一张票
无完整决策且控制停止、不再反馈release + 取消/未完成事实不启动动作,不补总结
原决策已经消费,随后用户停止保留 consumed取消尚未执行动作,不回滚额度

模型前置 deny 可能发生在 model-ready 之前,此时没有 ticket。根角色失败直接收束 Invocation,无票据就没有 release 动作。Executor 需要给父角色反馈时,对原 model_operation_id执行同一两级预算准入:可用则同事务创建 reserved 票据并立即以 consumption_kind=error_feedback consume、保存 TaskResult/PendingFeedbackSlot;不创建 execution_permit 或 Attempt。仅 reserved 暂满则保存 budget Wait,恢复只处理已存 blocked 响应,不重跑 Hook;实际耗尽或 scope 关闭优先走控制收束,不再承诺父业务反馈。该事务的净 reserved 增量为 0,角色序号不增加,失败票据幂等键仍为原 model_operation_id。

Executor 错误反馈槽键为 (parent_context_id,batch_id,member_id);记录 error_ref、source_model_operation_id、状态 pending/finalized。该槽先形成将来的父反馈承诺并结清票据,待父委派后置链完成后用同槽 effective 结果定稿;父模型仍只在批次全部 settled 后读取。这样失败 Executor 不持最后一张 reserved 票据卡住兄弟。若父 scope 已关闭且不再反馈,改为 release,与失败提交在同一锁内决定。

在 99 consumed+1 reserved 时另一角色申请返回 wait;旧票据 release 后可获第 100 张,consume 后下一申请触发根耗尽。第 100 张消费本身不关闭 scope,否则会错误取消其合法工具动作。任何其他角色真正要求新模型并观察 consumed==100 时才持久触发整树耗尽;因此已有动作也可能被该控制终结收束,这是根控制优先的确定结果。

8. 停止、失败与恢复 ​

8.1 作用域收束 ​

控制错误带 scope_kind=run|invocation、scope_id、reason、observed_at、control_revision。用户停止/发布停止/删除停止/根超时/根耗尽作用于 Run;局部超时、局部耗尽、根角色模型失败及任意角色压缩失败作用于所属 Invocation;直接 Agent Invocation 收束映射为整个 Run 结束。普通工具失败和 Executor 模型失败是父批次响应,不取消其他成员。

收束严格按以下步骤执行:

  1. 短事务锁控制范围并固定首个终结原因,关闭新 Loop、未启动 action、Executor、Attempt 的门;同一竞争中以事务先提交者为准,迟到成功不能改写。根截止检查早于局部检查。
  2. 取消未 begin 成员、本机 ready 意图和未 converted token;已存在 Operation 保留原引用,由调用管道保存已取得事实及取消结果。所有业务后置 Hook 在关闭 scope 下不得发起新业务调用。调用管道使用control_finalize_operation将未启动Hook记skipped_control并结清Operation;不是卡在raw_saved等一条不能运行的后链。
  3. 向已开始模型/工具/脚本发送支持的取消信号,登记 WriterBarrier;不把取消本机 Future 当成外部写入已结束。不可取消外部操作按剩余尝试/scope 时间收束,未确认结果记录 unknown;环境写能力由代次 fence 撤销。
  4. 对当前无完整决策且不反馈票据 release;接受过决策的保持 consumed。所有子任务达到终态或其写权限已确认撤销后,保存 scope 终态、唯一结果与父通知;根终态同时处理补充交接及 cleanup intent,需预锁 Conversation。
  5. 根全部写入者静默后才清理临时工作文件。Invocation 局部结束不清理全 Run 共享目录。cleanup_state 独立,清理失败只重试清理,不重新打开业务。

控制期间允许的提交仅限:记录已取得 raw/effective 事实、Hook 已有决定/失联故障、取消/错误结算、票据 release/已有决策 consume、输入交接、WriterBarrier、终态与清理。禁止新 dispatch、模型准备、Attempt 及普通完成后继。真实迟到结果可以记审计,但不得让 cancelled task 成功或重新唤醒终态 scope。

8.2 恢复算法 ​

python
async def recover_run(run_id, new_lease):
    latest_control = await controls.read(run_id)           # 不使用旧快照 control_ref 作授权
    if latest_control.terminal:
        return latest_control.result
    await fences.revoke_previous_writer_epoch(run_id, new_lease.epoch)
    bundles = await checkpoints.load_run_consistently(run_id, new_lease)
    validate_hashes_abis_protocols(bundles)                 # 失败禁止任何外部调用
    folded = restore_by_engine_adapter(bundles)            # agent / workflow 各用自身格式;固定读水位
    verify_ticket_projections(folded)                      # 不发新票据修补错误余额
    for graph in sorted(folded.graphs, key=lambda g: g.id):
        if graph.engine_kind == "workflow":
            register_workflow_recovery(graph)              # 原生 checkpoint + pending writes;此处不重新创建任务
            continue
        for task in sorted(graph.tasks, key=lambda t: t.id):
            if task.terminal:
                restore_result_only(task)
            elif scope_is_closing(task) or scope_expired(task):
                enqueue_control_settlement(task)
            elif task.status == "running":
                persist_old_activation_abandoned_and_ready(task, new_lease)
            elif task.status == "waiting":
                reevaluate_saved_condition(task)           # 满足则ready,否则原wait
            else:
                restore_ready_intent(task)
        rebuild_active_slots_and_admission_tokens(graph)  # 从Task/令牌事实,非WaitMember数
        enqueue_unfinished_operation_pipelines(graph)      # 复用原Operation;只补必要阶段
        enqueue_unconsumed_batches_and_join_intents(graph)
    return await runtime.drive_registered_engines(run_id, new_lease)

load_run_consistently 为持久化层聚合读取能力,不另建状态存储;跨 scope 先固定一致读水位。旧 running Activation 记录为 abandoned 后新建 Activation,task/context/graph/invocation/op/ticket 均不变。未完成调用的具体恢复交 invocation 模块:prepared 继续前置,raw_saved 只续后置,completed 直接复用;executing 无可靠结果按已识别异常策略处理,缺记录本身不能猜成可重试网络错误。

错误码检测点保存/恢复分支
GRAPH_INVALID / NODE_ABI_UNAVAILABLE发布/启动/恢复保存可定位诊断;不执行节点;修复部署后内部接管原版本
NODE_PROTOCOL_UNSUPPORTEDcontinuation 解码禁止猜字段;运行失败并保留现场供诊断
NODE_OUTCOME_INVALID / NODE_IMPLEMENTATION_FAILEDDriver保存 TaskResult 失败;按编译错误路由或 Invocation 规则处理
REVISION_CONFLICTCoordinator先查幂等 receipt;未提交则重读并重新准备,禁止旧请求出网
LEASE_LOST任意推进提交旧 Worker 停止提交/出网,关闭本机句柄;新 lease 接管
PERSISTENCE_UNAVAILABLE必要提交关闭本机新工作门,不返回未持久成功;存储恢复后按原 key 查证
MODEL_PROTOCOL_ERROR / MODEL_CALL_FAILED模型最终响应根角色 release并收束Invocation;Executor反馈槽+consume
COMPACTION_FAILED / CONTEXT_WINDOW_EXCEEDED压缩/最终窗口检查收束所属Invocation,不追加总结模型
RUN_LOOP_EXHAUSTED / RUN_TIMED_OUT / RUN_STOPPED控制门整树停止;无用户恢复入口
INVOCATION_LOOP_EXHAUSTED / INVOCATION_TIMED_OUT局部门仅本调用静默后失败,父读取原结果,兄弟继续
GRAPH_STATE_CONFLICT / REDUCER_FAILED / GRAPH_TASK_LIMIT图写/Join/任务创建固定错误结果,合法错误后继或图失败;不改基线再试成功
LOOP_TICKET_STATE_CONFLICT / RECOVERY_INVARIANT_BROKEN恢复/结算停止业务推进并报告系统失败;不得改账或重跑任务掩盖

9. RuntimeCommit 的读写集合、锁序与幂等 ​

事务总序为 Conversation(仅触及会话时)→root Run→Invocation 按 ID→Graph 按 ID→NodeTask 按 ID→Operation 按 ID→Context 按 ID→子记录按类型/ID。通常 RuntimeCommit 从 Run 开始;终态/补充交接必须预先声明 Conversation,禁止拿 Run 后临时补锁 Conversation。跨图、父子任务、父反馈 Context 均先收集完整集合再按序加锁。发布/删除 owner barrier 与停止分事务,不能拿 owner 锁等待 Run。

所有下表提交都额外读最新 lease/epoch/数据库时钟/control,写 ScopeCommit 与所需 outbox;表内“读”含 expected_revision 和相应引用归属。“任务终态”同时结束 Activation,state/result/next intent 不得拆开。纯映射、schema、reducer 在锁外基于冻结版本计算;锁内只重验版本、约束和写事实,不等待 LLM、工具、Hook、文件 IO。

RuntimeCommit 场景事务读集合原子写集合稳定幂等键
ActivateTaskgraph/task/旧 activationtask running、activation、scope seqactivate:{task}:{ready_seq}:{lease_epoch}
SaveNodeProgresstask/有关 op/contextcontinuation、NodeCheckpoint、对应 PreparedEffectprogress:{task}:{from_revision}:{next_phase}
commit_model_ready(CommitOperation 子效果)run/local/task/op/context、输入delivery冻结payload/hash/revision、ticket reserved、输入claimed、execution_permit、continuationmodel-ready:{operation}
AcceptModelDecisiontask/op/ticket/context模型接受ContextDelta、ticket consume、batch+WaitGroup或final候选、continuationaccept-model:{operation}
Freeze/准入批次成员graph/owner/batch/成员/active任务/tokenadmitting、token、begin意图admit:{batch}:{member}
BatchBegin(CommitOperation)graph/owner/admit intent/memberprepared Operation、batch operation_id、wait result_operation_id/operation targetbegin:{trusted_owner}:{purpose_key}
StartExecutorgraph/owner/delegate op/member/tokenchild task+context、关联、token converted、ready意图executor:{batch}:{member}
CompleteExecutorgraph/child/parent op/member、父context若模型失败child终态/结果/写、delegate raw关联、必要反馈槽/票据结算、补位意图task-result:{child}
SettleMember(CommitOperation 子效果)op/batch/member/owner/context最终EventResponse、member settled/结果、释放token、父ready意图member-result:{batch}:{member}
ConsumeBatchowner/batch/all结果/contextContextDelta、consumed、消费水位、phaseconsume-batch:{batch}
CompleteTask/普通后继graph/task/state/input view终态/TaskResult/TaskWrite、路由事实、state revision、next tasktask-result:{task}
CompleteTask/branch移交graph/task/wait/member/前缀终态/TaskWrite、路由、prefix、cursor、next task/loop_pathtask-result:{task}
SettleJoingraph/base revision/wait/all关闭成员/写集合并状态或错误、消费键、Join outcome、后继join:{wait}:{generation}
StartChild父/子graph、父task/op、子invocation父wait、子invocation/graph/root task、关联和启动意图agent-child:{parent_task}:{logical_call_key}
CompleteChild两图/child invocation/parent op子终态、父op raw关联、后处理意图invocation-result:{child_invocation}
控制收束/根终态Conversation如需要、run/调用树/writer barriercontrol、取消/票据、终态、补充交接、cleanup intentclose:{scope}:{control_revision} / terminal:{run}

RuntimeCommit 的 read_set 必须完整声明其 PreparedEffect 所触及实体;Coordinator 以 expected revisions + stable input hash 校验。一个逻辑提交键只对应一种内容;lease_epoch、提交时间和纯重投传输字段不进入业务效果 hash,已存在 receipt 先于重复执行检查返回,但旧 lease 不因此获得新执行权。任务结果键与 branch 后继键使恢复不会多创建下一任务。

批次、wait、ticket、admission token、TaskWrite 消费等都是所在 graph/task/op 的从属记录,不是另一套锁根或调度 authority。读 active Executor/task 与 token 必须在批次准入的同一 graph 锁范围,防止 phantom 超额;持久化篇负责具体索引/隔离级别。事务失败时所有 effect 均不可见;Blob 先持久后提交引用,未提交 blob 可回收但不视作成功。

10. 外层 AgentNode 与结构化流程(设计,不在本轮实现) ​

适用状态: 已确定双引擎,10.1 是 AgentNode/CapabilityNode 的平台协议,需要由 Workflow 节点适配器接入外层。父结果消费与外层原生 pending writes 的提交关系以双引擎第 5 节为准;不能直接使用内层 CompleteTask 为外层创建后继。10.2–10.3 是旧自研候选,不是 WorkflowEngine 的编码指令,见范围表。

10.1 AgentNode 和 CapabilityNode ​

python
@dataclass(frozen=True)
class AgentNodeContinuationV1:
    protocol_version: Literal[1]
    phase: Literal["prepare", "wait_child", "consume", "completed"]
    parent_operation_id: Id | None
    child_invocation_id: Id | None
    child_graph_execution_id: Id | None
    result_ref: FinalResponseRef | None
@dataclass(frozen=True)
class PreparedChildStart:
    parent_task: TaskRef
    logical_call_key: str
    parent_operation: OperationRef
    release_ref: AgentReleaseRef
    input_ref: PayloadRef
    effects: tuple[PreparedEffect, ...]
class AgentNodePort(Protocol):
    async def prepare_start(self, request: AgentInvocationRequest,
                            scope: ExecutionScope) -> PreparedChildStart: ...
    async def prepare_complete(self, child: AgentInvocationRef,
                               result: InvocationResultRef) -> PreparedChildCompletion: ...

AgentNode prepare 通过统一调用管道取得父 Operation,前置 Hook 合法 allow 后使用 (parent_task_id,logical_call_key) 创建一次 Invocation/child graph;StartChild 同事务保存父等待和子初始现场,之后才激活。child started_at/局部 deadline 取首次数据库创建时刻,恢复不延长。wait_child 由外层适配器异步等待或释放本次激活,不占内层 Driver worker;child 完成先给父 Operation raw 结果、父后置 Hook 完成后才得到 effective 响应;consume 用该响应完成父任务,错误按冻结外层错误路由处理。恢复始终查 child 引用,既有结果不重新调用 AgentRelease。

外层有效 failed/blocked 交付按正常任务输出携带业务失败,不直接抛给原生 runner 取消兄弟。仅取消父等待不终结子调用;根停止、局部失败和基础设施异常分别处理。节点输出、结果消费与原生pending writes同事务且等待持久确认,之后才允许应用状态和启动后继;原生ERROR/INTERRUPT不自动消费结果。不可再叠加重跑业务的原生RetryPolicy。完整规则见失败映射与提交屏障。

CapabilityNode 保存 operation_id 和 prepare/wait/consume/completed 阶段,复用相同能力代理;它属于 Workflow 的 WorkspaceScope,无 invocation_id 合法,不申请业务 LoopTicket。完整 Agent 与外层的文件/历史授权通过显式 ArtifactRef/ConversationScope;不能继承外层私有历史或完整可写目录。

10.2 Fork、分支 cursor 与循环身份 ​

python
@dataclass(frozen=True)
class BranchCursor:
    wait_id: Id
    member_id: Id
    cursor_task_id: Id | None
    next_task_sequence: int
    view_write_ids: tuple[Id, ...]
    terminal_reason: str | None
@dataclass(frozen=True)
class RouteDecision:
    route_key: str
    selected_target: str | None
    selected_fork_targets: tuple[str, ...]
    source_state_revision: int
class StructuredGraphPort(Protocol):
    async def prepare_route(self, task: TaskRef, result: TaskResultDraft,
                            view: FrozenStateView) -> PreparedRouteEffect: ...
    async def prepare_fork(self, source: TaskRef,
                           decision: RouteDecision) -> PreparedForkEffect: ...
    async def prepare_join(self, wait: WaitGroupRef,
                           generation: int) -> PreparedJoinEffect: ...

prepare_route 前置为冻结输入和声明节点输出;只执行发布纯 predicate,选择结果与 TaskResult 同事务。prepare_fork 只接受编译索引中的 fork 及允许目标,目标按定义 ordinal 排序,未选择目标不进入冻结成员;generation 在 (graph,fork_route_key) 下原子+1,保存新 wait_id、base_state_revision、loop_path。空选择的 Join 立即可归并,不留下无 cursor active 成员。branch_scope 的 target_ref/result_operation_id 为空,身份由 branch_data 表达。

python
def branch_complete(task, result, writes, decision, member):
    assert member.cursor_task_id == task.id and not member.closed
    assert is_prefix(task.input_state_view.write_ids, member.view_write_ids)
    prefix = member.view_write_ids + immutable_write_ids(writes)
    save_task_result_and_writes(task, result, writes)
    save_route_decision(task.id, decision)
    if decision.is_branch_exit:
        close_member(member, prefix, decision.exit_status) # reached_join/skipped
        create_join_intent_if_all_closed(member.wait_id)
    elif decision.legal_single_successor:
        next_path = transition_loop_path(task.loop_path, decision)
        child = new_task_once(key=(member.wait_id, member.id, member.next_task_sequence),
                             input_view=(member.base_revision, prefix), loop_path=next_path)
        move_cursor(member, child.id, prefix, next_sequence=member.next_task_sequence + 1)
    else:
        close_member(member, prefix, "failed")            # 无合法错误后继
        create_join_intent_if_all_closed(member.wait_id)

该函数所有写入属于一次 CompleteTask 提交;失败 task 不生成新的业务 TaskWrite,但前驱已经成功的写保留。取消成员须确认 cursor 不再写入。分支内读取将 frozen GraphState 基线与本成员 task_sequence/write_ordinal 的原始增量依次应用;兄弟写不在其 view_write_ids 中。输入映射结果也随任务意图冻结,恢复不能再次读取最新 GraphState 替换。

loop_path 外到内排列:进入 header 时新建 loop_instance_id、iteration=0;声明 back_route 仅修改栈顶对应 frame 的 iteration+1,并创建新 header task;exit_route 弹出对应 frame;嵌套循环重新进入总是新的 instance_id。嵌套多个 frame 的跨层退出必须经过各层声明出口,单条路由不得隐式多次 pop。路由、loop_path 和 next task 同事务,重复通知不重复自增。React role_loop_index 不改变工作流 loop_path。

10.3 Join 确定归并 ​

  1. 读固定 generation 的全部成员,任何未 closed 则返回未就绪。按 (member.ordinal, task_sequence, write_ordinal) 枚举 TaskWrite;每个引用必须属于此成员且在保存前缀中,不能出现兄弟写或未提交写。
  2. 验证公共 GraphState revision 等于 fork 的 base_state_revision;open fork 期间不允许另一普通路径改公共状态。不同则固定 GRAPH_STATE_CONFLICT,不能切换新基线继续。
  3. 从基线值开始,single_writer 字段按实际写入成员检查至多一个成员;同成员顺序覆盖合法。reducer 字段每个原始增量按固定纯函数依次应用、输出逐次 schema 校验。不得再应用一份成员 overlay 快照。
  4. 在锁外准备新值或 REDUCER_FAILED,提交时重验上述版本;以 (wait_id,generation,write_id) 登记消费键。Join outcome、新公共 revision/值或错误、消费集合及后继意图一次提交。
  5. 成员 failed/skipped 不等于归并失败;归并成功总是选 success_target,携带完整成员结果,让后继决定业务。只有基线冲突、写集冲突、reducer 错误选 error_target;目标为空则图失败。不得创建虚构 Join NodeTask。
  6. 循环下一次 fork 有新 generation/wait_id;迟到上一批结果只在原批次去重,不按相同 node_key 归入当前批次。消费已提交后恢复直接复用结果,纯 reducer 即便可重算也不得再次施加已消费增量。

11. 具体输入输出与可执行验收断言 ​

以下输入/输出中的 op/ticket/batch/task 均为服务端分配,测试使用固定 ID 工厂。网络模型使用脚本化 FakeModel;Hook、Blob、Coordinator 和时钟均可在精确切点注入故障。断言使用实际记录数量、状态和出网次数,不能只匹配日志文本。

json
{
  "input": {
    "task": "P1", "graph": "G1", "invocation": "I1", "role_loop_index": 1,
    "model_operation": "M1", "ticket": "L1", "parallel_enabled": false,
    "effective_model_response": {"text": "先查两份材料", "tool_calls": [
      {"id": "provider-a", "name": "research", "arguments": {"task": "核对甲"}},
      {"id": "provider-b", "name": "read_file", "arguments": {"path": "notes.txt"}},
      {"id": "provider-c", "name": "missing", "arguments": {}}
    ]}
  },
  "after_accept": {
    "task": "P1", "phase": "wait_batch", "ticket_state": "consumed", "business_loop_no": 1,
    "batch": "B1", "wait": "W1", "graph_count": 1, "invocation_count": 1,
    "members": [
      {"call_id": "C0", "ordinal": 0, "kind": "executor", "status": "queued"},
      {"call_id": "C1", "ordinal": 1, "kind": "tool", "status": "queued"},
      {"call_id": "C2", "ordinal": 2, "kind": "rejected", "status": "settled", "code": "CAPABILITY_NOT_FOUND"}
    ]
  },
  "after_full_delivery": {
    "task": "P1", "phase": "prepare_context", "role_loop_index": 1, "batch_consumed": true,
    "results": [
      {"call_id": "C0", "status": "succeeded", "text": "甲已核对"},
      {"call_id": "C1", "status": "failed", "code": "FILE_NOT_FOUND"},
      {"call_id": "C2", "status": "failed", "code": "CAPABILITY_NOT_FOUND"}
    ]
  }
}

此例断言:初始接收响应不启动工具;准入后先创建 E1,仍只有 G1/I1;E1 TaskResult 后父 Hook 未完时 read_file.begin_count==0;C0 settled 后 C1 才 begin;C2 从未有 Operation;P1 下一次模型 Operation 只能在 B1 consumed 以后创建,context 中每个 call_id 恰一个结果槽。

编号/输入与故障切点必须断言
C01:重复 node_key、悬空 route、ReAct 绑定 executorvalidate 返回三个独立定位诊断,compile 无 descriptor;外部调用数=0
C02:A→B→A 未声明 back;声明嵌套单入口 loop 的对照图前者 GRAPH_UNDECLARED_CYCLE;后者 hash 在输入对象键顺序改变后相同
C03:fork 的 A1→A2 与 B1 提前汇合于普通 CGRAPH_UNSTRUCTURED_REGION,不能用共享 C 代替 Join
C04:两个分支 single_writer x;换为固定 append reducer前者编译冲突;后者成功,运行输出按成员ordinal而非完成顺序
N01:Progress 提交前注入 DB failuregenerator 的下一行计数=0,外部 begin_count 不增加;接管从原 phase
N02:Progress commit成功、receipt返回前宕机恢复任务revision只增加一次,ContextMutation/Operation唯一;原 receipt 可查
N03:Wait提交前结果已完成/Wait提交后结果才完成两种顺序均最终 ready,同 task ID,两次重复通知不多建 activation
R01:ReAct 两个工具轮次再最终答复Invocation=1、Graph=1、NodeTask=1、business tickets consumed=3,工具Operation=2
R02:Hook将请求扩到窗口上限外未压缩/最终硬检查通过之前 Attempt=0、permit=false;失败只收束所属Invocation
R03:模型含文本和半截 tool参数;随后流失败工具执行数=0;重试片段分attempt;无过程文本被标最终答案
R04:模型完整输出3次schema失败,第4次合规consumed=4、role_loop_index=4、最终输出只来自第4次;失败候选不拼接
B01:串行 E1、T2、E3,E1父结果Hook阻塞E1终态后active=0但T2.begin=0;E1settled后T2开始;无第二图
B02:并行max=2,E1/E2/E3,E1父Hook阻塞E1/E2先active=2;E1终态后E3启动,active<=2;owner仍waiting
B03:并行前置Hook同时通过/一个deny/创建child前宕机任意时active+reserved<=2;deny令牌released且无child;恢复未converted令牌不多发名额
B04:同批一项失败,其余成功,重复member通知3次所有结果ordinal不变,单项失败不取消兄弟,consume mutation仅1条
B05:默认串行Executor,准入后、begin前宕机并重复advance恰一reserved token、恰一begin意图;admitting无Operation合法;接回原键创建一次child并converted,不多预留
B06:串行Executor前置deny;另案在begin前停止前者原Operation blocked且token released;后者原意图关闭、token released、local_outcome cancelled;两者均无child
L01:根99 consumed+1 reserved,第二task申请返回budget wait而非exhausted;旧票据release后新票据可reserved;consume后下一申请根关闭
L02:上限100,第100次最终输出;另案第100次工具后请求第101次前者succeeded;后者工具可执行但第101模型Attempt=0,Run budget_exhausted,无总结调用
L03:root100/local A5/B20,A已5、root仍20A新申请只关闭I_A,B可继续;root满时一次控制关闭全部,账本投影精确一致
L04:Executor模型失败持最后reserved,兄弟等票据;另案前置deny无ticket首案子失败/反馈槽/consume原子提交,reserved=0;后案无Attempt且一张票据直接结算,暂无容量只等待已存拒绝,控制优先,不消费空票据
S01:串行A已发B未发,stop提交后模型迟到动作B.begin_count=0,迟到动作均不执行;A结果真实记录,终态原因不可被成功覆盖
S02:一个Executor退出而另一个脚本仍写文件cleanup_started=false;全树writer退出或fence确认后才true;局部终态不清理根
F01:E1完成E2执行中宕机;raw存而Hook未完成再宕机E1执行数不增加,E2复用身份;raw已存工具invoke_once不增加,只补原Hook链
F02:旧epoch迟到commit、旧checkpoint含runningLEASE_LOST,当前context/revision/额度不变;新机器不重置deadline
W01:外层AgentNode子图终态后父未消费宕机child invocation/graph数各1;父等effective Hook,不能从raw直接完成
W02:基线x=0,A1写x=1→A2读x,B1读xA2读1、B1读0;A1终态不能关闭A分支;A2完成后才Join
W03:append基线[],A1写[a]、A2写[b]、B1写[c],B先完成Join=[a,b,c];消费键3条;恢复后仍3条且无overlay重复追加
W04:loop iteration0 W7已合并,iteration1 W8,W7迟到结果W8成员状态不变;W7 reducer不再应用;iteration不重复自增
W05:Join基线被非法并发更新或reducer抛错保存唯一错误,成功后继数=0;错误后继最多1个,不能换新基线成功

阶段参数化故障注入覆盖每个 RuntimeCommit 的“前、事务中、成功后通知前”三点:统一断言 (TaskResult唯一、Operation身份唯一、ticket无重复consume、batch交付唯一、后继不丢失、终态不重开)。外层测试先作为规格测试数据及纯编译/规划器验收保存,运行器实现前不得把其列为已通过产品能力。

AgentNode的完整调用创建事务在Run锁之前取得目标Agent发布gate共享锁,检查未publishing/deleting;与发布gate的排他写串行化。这样发布扫描不会漏掉刚提交的Invocation。GraphEngine不能仅在事务外读一次发布状态就启动子图;该锁序例外和目录入口一致,具体见ID-2。