Skip to content

feat(runtime): derive operator-local graph readiness - #1446

Merged
likun666661 merged 2 commits into
mainfrom
agent/1341-operator-local-readiness
Jul 24, 2026
Merged

likun666661 merged 2 commits into
mainfrom
agent/1341-operator-local-readiness

Conversation

@likun666661

@likun666661 likun666661 commented Jul 24, 2026 •

Copy link
Copy Markdown
Member

Summary

This is the third implementation slice of #1341, building on the committed-event projection in #1442 and trace topology in #1443.

  • Add a canonical topology fingerprint over graph identity, Session-backed operator bindings, and directed edges. Only schema-declared fields participate, and opaque graph identities use one locale-independent raw/code-unit total order.
  • Derive operator-local readiness from the deterministic trace snapshot rather than scanning or mutating Agent runtime state.
  • Add a map policy that produces one stable candidate intent per direct incoming route.
  • Add a sealed-input all_settled policy that waits for one exact activation from every direct upstream operator.
  • Bind every candidate intent to an operator-local readiness context: target operator/Session binding, direct incoming edge identities, normalized policy, and trigger route/record identities.
  • Keep the global topology fingerprint on the snapshot and supervisor view as provenance without globally invalidating otherwise unchanged local intents.
  • Expose explicit waiting reasons for missing/running activations or absent input routes.
  • Project every local readiness state into an always-on supervisor side view without consulting the supervisor during readiness derivation.
  • Preserve deterministic/idempotent output under reordered topology, policy, record, duplicate, and locale-equivalent Unicode identity observations.

Why the activation frontier is explicit

A Session-backed operator can have multiple session-inline AgentRun activations over time. An all-settled join over "the latest activation" would be unstable: a later follow-up in the same child Session could silently turn an already-runnable join back into waiting or change its trigger identity.

This slice therefore requires the join policy to seal one exact operatorId / activationId input for every direct upstream. A later follow-up activation remains observable in the trace but cannot rewrite the already-derived join intent.

All terminal outcomes settle an all-settled input: completed, failed, aborted, or cancelled. Failure policy remains a later operator/control-plane concern.

Authority boundary

A runnable intent is a deterministic candidate, not execution authority.

Recomputing this projection never creates, starts, resumes, sends to, pauses, or cancels an AgentRun. A later control-plane slice must durably and idempotently claim an intent before invoking existing child-session runtime APIs. That future claim will also provide consumption/acknowledgement and recovery semantics; this PR deliberately does not infer intent consumption from unrelated target Session activity.

Structural readiness also remains separate from resource admission. A runnable intent may still wait for concurrency, provider, budget, fairness, or workspace permits.

The main-agent supervisor is structurally beside the graph. It observes every waiting/runnable state and candidate intent through supervisorView, but is not a serial gate in deriving or routing normal work.

Identity boundary

The full topology fingerprint is graph-level provenance only. It intentionally does not participate in intentId: adding a disconnected observer or a downstream-only operator cannot reissue historical local work under a new identity.

Instead, each intent carries a readinessContextFingerprint derived from the graph identity, target operator/Session binding, schema-defined direct incoming edges, and normalized policy fingerprint. Trigger route/record identities complete the candidate identity.

Topology validation projects operators to { operatorId, sessionId } and edges to { edgeId, fromOperatorId, toOperatorId }, so undeclared diagnostic/UI fields cannot affect identity. Projection, trace, topology, policy, and sealed-input ordering share a strict raw string comparator rather than locale-sensitive collation.

Failure semantics

Readiness derivation fails closed for:

Deliberate non-goals

  • No Agent runtime actions or runtime rewrite.
  • No durable intent claim, acknowledgement, consumption cursor, or recovery authority yet.
  • No resource permits or scheduling fairness.
  • No quorum/early-exit policy; that needs durable claims so late observations cannot rewrite an executed choice.
  • No graph-wide completion or topology/admission closing protocol.
  • No supervisor control records or semantic intervention yet.
  • No Desktop/TUI renderer.

Validation

  • npm run typecheck
  • focused projection + trace + readiness suites: 28 passed
  • Runtime full suite: 2 known unrelated macOS sandbox failures:
    • /usr/local executable-root ordering assertion
    • Homebrew rg cannot load PCRE2 inside the operation-scoped Seatbelt sandbox
  • Biome lint/format check on all changed files
  • git diff --check

Refs #1341

中文说明

这是 #1341 的第三个实现切片,建立在 #1442 的 committed-event projection 与 #1443 的 trace topology 之上。

  • 对 graph identity、Session-backed operator binding 与 directed edge 生成 canonical topology fingerprint;只有 schema 声明字段参与 identity,所有 opaque graph identity 使用同一个与 locale 无关的 raw/code-unit 严格全序。
  • 从确定性的 trace snapshot 推导 operator-local readiness,不扫描或修改 Agent runtime 状态。
  • map policy 为每条 direct incoming route 生成一个稳定的 candidate intent。
  • sealed-input all_settled policy 等待每个 direct upstream 的一个精确 activation。
  • 每个 candidate intent 绑定 operator-local readiness context:target operator/Session binding、direct incoming edge identity、normalized policy 与 trigger route/record identity。
  • 全局 topology fingerprint 继续保留在 snapshot 与 supervisor view 中作为 provenance,但不会让无关的 topology 变化使局部 intent 全局失效。
  • 明确输出 input route 缺失、activation 缺失或仍在运行等 waiting reason。
  • 每个 local readiness state 都进入 always-on supervisor side view;推导 readiness 时不需要 supervisor 批准。
  • topology、policy、record 输入乱序、重复,或出现 é / e\u0301 这类 locale-equivalent Unicode identity 时,仍能确定、幂等地重建。

一个 Session-backed operator 可以持续产生多个 session-inline AgentRun activation。如果 all-settled 依赖“最新 activation”,同一 child Session 后续出现 follow-up run 时,已经 runnable 的 join 可能重新变回 waiting,或者触发 identity 被改写。

因此本切片要求 join policy 为每个 direct upstream 显式封口一个精确的 operatorId / activationId。后续 follow-up activation 仍会进入 trace,但不能改写已经推导出的 join intent。completed、failed、aborted、cancelled 都属于 settled;如何处理失败留给后续 operator/control-plane policy。

全图 topology fingerprint 只负责 graph-level provenance,不再参与 intentId。增加 disconnected observer 或 downstream-only operator,不会把历史局部工作换一个 ID 后重新发放。

每个 intent 改为携带 readinessContextFingerprint,其来源是 graph identity、目标 operator/Session binding、schema-defined direct incoming edges 与 normalized policy fingerprint;再结合 trigger route/record identity 构成完整 candidate identity。

Topology validation 只保留 operator 的 { operatorId, sessionId } 与 edge 的 { edgeId, fromOperatorId, toOperatorId },因此未声明的调试/UI 字段不会污染 identity。Projection、trace、topology、policy 与 sealed input 排序统一使用 raw string comparator,不依赖 locale collation。

Runnable intent 只是确定性的候选,不是执行权威。重新计算 projection 不会创建、启动、恢复、发消息、暂停或取消 AgentRun。后续 control-plane 切片必须先持久、幂等地 claim intent,再调用现有 child-session runtime API;consumption/acknowledgement 与 recovery 语义也由那一层建立。

结构 readiness 与资源 admission 继续分离。Intent 即使 runnable,也可能等待 concurrency、provider、budget、fairness 或 workspace permit。

主 Agent supervisor 始终站在图旁边,通过 supervisorView 观察全部 waiting/runnable state 与 candidate intent,但不会成为正常 readiness 或 record routing 的串行门禁。

本 PR 不执行任何 Agent runtime action,不引入 durable intent claim/ack、resource permits、quorum/early-exit、graph-wide completion/closing、supervisor control record 或 Desktop/TUI renderer。

@likun666661

Copy link
Copy Markdown
Member Author

The authority boundary and sealed-activation model look good, but I found two identity blockers before this is safe to merge.

[P1] Do not make every intent depend on the entire graph topology

runnableIntent() includes the full trace.topologyFingerprint in both the intent payload and intentId. This globally invalidates otherwise unchanged local work.

I reproduced this with a source -> worker map policy, then added one completely disconnected observer operator. The worker's route ID, source record, target binding, and policy fingerprint were all unchanged, but its intent ID changed solely because the unrelated operator changed the global topology fingerprint.

That becomes a correctness problem once the next slice adds durable claims. If the original intent has already been claimed/executed, an ordinary supervisor AddOperator elsewhere in the graph reissues every historical map/join candidate under a new ID. A claim store keyed by intentId cannot recognize those candidates as already consumed and may start duplicate work.

Please keep the global topology fingerprint on the snapshot/supervisor view as provenance, but bind intent identity to an operator-local readiness context instead: the target operator/Session binding, its direct incoming edge identities, the normalized policy, and the trigger route/record identities. The existing route IDs already bind graph, edge endpoints, and source records, so an unrelated topology addition should leave an existing local intent byte-stable. Add a regression that adds a disconnected or downstream-only operator and asserts that existing local intents remain unchanged.

[P1] Make the topology fingerprint actually canonical

fingerprintTopology() currently hashes the spread objects retained by validateTopology() and sorts operator identities with localeCompare(). Both break the advertised input-independent identity:

  1. The spread objects retain undeclared properties. Adding semantically irrelevant fields such as displayName to an operator or debugColor to an edge changes the topology fingerprint and every intent, even though the declared topology, routes, and policy are unchanged. Either reject unknown fields or fingerprint only the schema-defined fields: { operatorId, sessionId } and { edgeId, fromOperatorId, toOperatorId }.

  2. localeCompare() is not a strict total order for opaque IDs. For example, "é" and "e\u0301" are distinct JavaScript strings but compare as 0 in the current runtime. Reversing a topology containing those two operators produced the same topological order but different topology fingerprints because stable sort preserved the original input order for the comparator tie. The readiness policy and sealed-input comparators have the same issue.

Please use one locale-independent raw/code-unit total comparator for graph identities (or normalize and reject aliases at validation), audit the graph/readiness identity sorts, and add reversed-input regressions for distinct Unicode IDs that localeCompare() considers equal.

The focused 25-test suite, typecheck, and CI are green; these failures are in the identity contract rather than the current ASCII happy paths.

@likun666661

Copy link
Copy Markdown
Member Author

Addressed both identity blockers in 844902a6.

  1. Intent identity is now operator-local. The global topology fingerprint remains on the readiness snapshot and supervisor view for provenance, but no longer participates in the intent payload or intentId. Each intent now carries a readinessContextFingerprint over the graph identity, target operator/Session binding, direct incoming edge identities, and normalized policy fingerprint; trigger route/record identities remain part of the candidate identity. Regression coverage verifies that adding either a disconnected observer or a downstream-only operator leaves existing local intents byte-stable while the global topology fingerprint still changes.

  2. Graph identity is now canonical and locale-independent. Topology validation/fingerprinting projects only { operatorId, sessionId } and { edgeId, fromOperatorId, toOperatorId }, so undeclared fields cannot affect the fingerprint. Projection, trace, topology, readiness-policy, and sealed-input identity sorts now share one raw UTF-16 code-unit strict comparator instead of localeCompare(). Reversed-input regressions cover distinct é / e\u0301 identities across projection, topology, and sealed readiness inputs.

Validation:

  • focused projection + trace + readiness suites: 28 passed
  • npm run typecheck: passed across all workspaces
  • Biome lint/format checks on changed files: passed
  • git diff --check: passed
  • the broader local runtime suite still has only the two previously documented, unrelated macOS Seatbelt failures

This feedback was posted as a top-level PR conversation comment, so GitHub does not expose a review-thread “Resolve conversation” action for it; the implementation and this response close the loop.

中文说明

两个 identity blocker 都已在 844902a6 中修复。

  1. Intent identity 已改为 operator-local。 全局 topology fingerprint 继续保留在 readiness snapshot 与 supervisor view 中作为 provenance,但不再进入 intent payload 或 intentId。每个 intent 现在携带 readinessContextFingerprint,由 graph identity、目标 operator/Session binding、直接入边 identity 与 normalized policy fingerprint 构成;trigger route/record identity 仍属于 candidate identity。回归测试验证:增加 disconnected observer 或 downstream-only operator 时,全局 topology fingerprint 会变化,但既有局部 intent 保持 byte-stable。

  2. Graph identity 已 canonical 且与 locale 无关。 Topology validation/fingerprint 只投影 { operatorId, sessionId } 与 { edgeId, fromOperatorId, toOperatorId },未声明字段不会影响 fingerprint。Projection、trace、topology、readiness policy 与 sealed input 的 identity 排序统一使用 raw UTF-16 code-unit 严格 comparator,不再使用 localeCompare()。反向输入回归覆盖了 projection、topology 与 sealed readiness input 中彼此不同但 locale-equivalent 的 é / e\u0301。

验证结果:graph 聚焦测试 28/28、全仓 typecheck、Biome lint/format 与 git diff --check 均通过;runtime 全套仍只有此前已记录的两个无关 macOS Seatbelt 环境失败。

这条反馈属于 PR 顶层 conversation comment,不是 review thread,因此 GitHub 没有可点击的 “Resolve conversation” 状态;代码与本回复已完成闭环。

@likun666661
likun666661 marked this pull request as ready for review July 24, 2026 09:36
@likun666661
likun666661 merged commit 2a1786c into main Jul 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant