Skip to content

ADR-0021: Canonical source as the WM's internal action identifier after runner initialization

  • Status: accepted
  • Date: 2026-04-14
  • Deciders: @Aksem
  • Tags: actions, wm, er, architecture

Context

ADR-0019 establishes that external callers identify actions by import-path aliases. Internally the WM also holds a canonical_source field on each Action domain object — the fully qualified class path derived from cls.__module__ + "." + cls.__qualname__.

Canonical sources are resolved by the ER after finecodeRunner/updateConfig via finecodeRunner/resolveActionMeta. The original protocol returned a sparse map: only entries where the alias differed from the canonical were included. The WM inferred that absent entries meant "alias == canonical" and left canonical_source = None for those actions.

This created a dual-lookup pattern throughout the WM (e.g. a.source == x or (a.canonical_source is not None and a.canonical_source == x)): code had to accommodate both the pre-resolution state (canonical_source is None) and the post-resolution state where the alias and canonical might differ. The result was scattered defensive logic and an unclear invariant about when canonical_source could be trusted.

  • ADR-0019 — this ADR is its internal complement: ADR-0019 governs external string identifiers; this ADR governs the internal identifier the WM uses after runner initialization.
  • ADR-0006 — type-level identity rule; canonical source is the string projection of that rule inside the WM.

Decision

The ER returns a complete (non-sparse) map from resolveActionMeta, including every action whose class can be imported — even those where the alias already equals the canonical path. Actions that fail to import are omitted (import failure = cannot run = no canonical to report).

After update_runner_config completes, canonical_source is the WM's sole internal identifier for each action whose class was successfully imported. No lookup code in the WM may match an action by its config source for internal dispatch purposes; it must match by canonical_source.

The only allowed exception is the external API boundary (find_action_by_source), which accepts an import-path alias from callers and must still resolve it to an action — it does so by matching a.source == alias to retrieve the action, after which all downstream code uses canonical_source.

Actions whose class cannot be imported in any runner keep canonical_source = None. They cannot be executed and any lookup by canonical will correctly return no match.

Consequences

  • Single internal identifier. After runner initialization, every importable action has a non-None canonical_source. Internal dispatch code (e.g. ProjectExecutor) can match by canonical_source alone without any source-alias fallback.
  • No "absent means equal" inference. The WM no longer infers a relationship between alias and canonical from the absence of an entry in the ER response. The ER states the canonical explicitly for every action it can import.
  • Multiple runners, first wins. When several runners initialize for the same project, the first one to successfully report a canonical for an action sets it. Subsequent runners skip already-resolved actions. All runners for the same project should agree (same class = same canonical), so ordering does not affect correctness.
  • Import failures surface cleanly. An action absent from the ER's response is one the ER could not import. Its canonical_source stays None, and execution attempts on it will fail at the lookup stage rather than silently matching by alias.
  • Refines ADR-0019 — adds the internal-identifier rule that ADR-0019 left implicit.
  • Depends on ADR-0006 — canonical source is the string form of the type-level identity established there.