Skip to content

Declare transitions locally with branches, stay, commands, and update records - #268

Merged
SandroMaglione merged 1 commit into
mainfrom
agent-first-transitions
Nov 7, 2026
Merged

SandroMaglione merged 1 commit into
mainfrom
agent-first-transitions

Conversation

@SandroMaglione

@SandroMaglione SandroMaglione commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Summary

Breaking change across all four packages. Each transition now holds everything needed to read and edit it.

  • branches moves from make groups into a list next to resolve. select.target(path, ...), select.update, select.history, select.initialize, select.stay, and select.decline replace select.<key>, and the compiler checks the calls against the list in both directions. Branch keys, declinable, branch title, and the decline context function are gone.
  • { stay: true } and { reenter: true } replace none. { none: true, resolve } becomes { stay: true, commands }.
  • commands works on every plain transition.
  • update: { owner: value } is a record with one owner, so data only ever means the target's value. A branches entry names the owner as a string.
  • guard and decline exist only on on and always, in types and at runtime. A declined invocation outcome used to strand the machine.
  • A region's stay preempting a parallel ancestor's handler is existing behaviour, now documented and covered by a public test and a differential case.

Errors name the fix, for example resolve selects undeclared { stay: true }. Add it to branches. The exactness check also rejects undefined, Promise, any, unknown, duplicate entries, and widened paths.

Choices made while building this:

  • Retained branchKey and definition key stay, derived from the entry as written (target:Paid, update:root, stay). Coverage and MachineTest.check therefore name branches without an authored key.
  • Two old keys that reached one destination are now one entry, so coverage can no longer tell apart two conditions with the same destination.
  • no-conditional-none-branch is ported to no-conditional-stay-branch rather than retired, because it still catches a real bug: a disabled control that Machine.can reports as enabled. It flagged a devtools example while I migrated it. no-redundant-resolve is retired.

Type-performance tradeoff: most scenarios are flat or slightly lower. declared transition (10 named branches) rises about 11% and parallel/history/choice about 12%, which pays for checking resolve against branches and owners by path. This is deliberate, per AGENTS.md.

Changeset

  • Added or updated for a library or package-metadata change
  • Not required because this PR does not change a publishable package

Validation

  • pnpm check
  • Automated type-performance measurement passed or was not required
  • Automated runtime- and memory-performance measurement passed or was not required

🤖 Generated with Claude Code

… records

Move conditional destinations from make into an inline branches list that the
compiler checks against resolve in both directions, so one transition can be
edited without reading anything else. Replace none with stay, add commands to
plain transitions, and make update a record so data only ever means the
target's value. Restrict guard and decline to on and always, which have another
candidate to fall through to.

Retained branch keys are now the entry as written. Retire no-redundant-resolve,
which the compiler now covers, and port the conditional rule to stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Type performance

Measured with TypeScript 6.0.3 and skipLibCheck=true.

Scenario Base PR Difference
Effect only 55 55 0 (0.0%)
Import effect-machine 55 55 0 (0.0%)
Machine.state (3 children) 2,664 2,664 0 (0.0%)
Machine.make (3 states, 2 events) 8,932 8,849 -83 (-0.9%)
machine.handle (3 states, 2 transitions) 39,449 38,693 -756 (-1.9%)
declared transition (10 named branches) 60,402 67,072 +6,670 (+11.0%)
atomic construction, guards, and reentry 85,710 77,731 -7,979 (-9.3%)
registered invocation (input-taking Effect) 39,819 38,733 -1,086 (-2.7%)
machine.handle (depth 24) 194,531 194,030 -501 (-0.3%)
machine.handle (wide depth 16) 218,238 217,737 -501 (-0.2%)
machine.handle (parallel/history/choice) 91,642 102,455 +10,813 (+11.8%)
machine definition (3 independent implementations) 99,173 92,828 -6,345 (-6.4%)
machine exact input/output/error/services 82,941 80,179 -2,762 (-3.3%)
execution adapter readiness 71,428 73,484 +2,056 (+2.9%)

Marginal instantiations are measured against the matching setup without that API call:

Scenario Base PR Difference
Import effect-machine 0 0 0
Machine.state (3 children) 2,609 2,609 0 (0.0%)
Machine.make (3 states, 2 events) 6,260 6,177 -83 (-1.3%)
machine.handle (3 states, 2 transitions) 30,517 29,844 -673 (-2.2%)
declared transition (10 named branches) 51,849 58,602 +6,753 (+13.0%)
atomic construction, guards, and reentry 70,370 62,474 -7,896 (-11.2%)
registered invocation (input-taking Effect) 32,682 31,679 -1,003 (-3.1%)
machine.handle (depth 24) 177,222 176,804 -418 (-0.2%)
machine.handle (wide depth 16) 201,707 201,289 -418 (-0.2%)
machine.handle (parallel/history/choice) 79,144 90,040 +10,896 (+13.8%)
machine definition (3 independent implementations) 88,492 82,230 -6,262 (-7.1%)
machine exact input/output/error/services 70,732 68,053 -2,679 (-3.8%)
execution adapter readiness 31,922 31,884 -38 (-0.1%)
Check times (informational)
Scenario Base PR
Effect only 0.02s 0.02s
Import effect-machine 0.02s 0.02s
Machine.state (3 children) 0.06s 0.07s
Machine.make (3 states, 2 events) 0.12s 0.12s
machine.handle (3 states, 2 transitions) 0.22s 0.22s
declared transition (10 named branches) 0.29s 0.33s
atomic construction, guards, and reentry 0.38s 0.37s
registered invocation (input-taking Effect) 0.23s 0.22s
machine.handle (depth 24) 0.47s 0.46s
machine.handle (wide depth 16) 0.50s 0.52s
machine.handle (parallel/history/choice) 0.38s 0.41s
machine definition (3 independent implementations) 0.40s 0.38s
machine exact input/output/error/services 0.36s 0.38s
execution adapter readiness 0.32s 0.33s

Type instantiations are the comparison metric. Check time varies with runner load and is informational only.

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Runtime performance

Median of 5 independent benchmark processes on AMD EPYC 7763 64-Core Processor with Node v24.22.2.

Pull request baseline

Scenario Effect Machine
Plan counter transitions 114,650 transitions/s
Drain burst with terminal fence 398,207 increments/s
Drain burst with a change observer 368,815 increments/s
Lookup and send to one child 290,535 increments/s
Start and stop a machine 117,426 machines/s
Start and stop a parent with one child 23,765 families/s
Complete an Effect invocation with tracing enabled 6,302 invocations/s
Complete an Effect invocation with tracing disabled 7,470 invocations/s
Plan transitions through a compound state 99,921 transitions/s
Plan transitions through parallel regions 79,062 transitions/s
Drain burst through a compound state 339,256 events/s
Drain burst through two parallel regions 312,941 events/s
Drain a compound-state burst with a change observer 314,273 events/s

Process runtime reference points

Scenario Effect Machine
Start and stop a raw generic process 30,055 processes/s
Start and stop a raw compiled process 69,075 processes/s
Memory profile Effect Machine
Idle machine 1.8 KiB
Raw generic managed process 11.4 KiB
Raw compiled process 3.1 KiB
Two independent idle machines 3.6 KiB
Idle parent with one child 6.0 KiB
Parent with observed child registry 9.6 KiB
Parent with observed invoked child snapshots 7.0 KiB

Effect Machine change from base

Metric Base Base variability PR PR variability Difference
Plan counter transitions 113,379 transitions/s 1.2% MAD 114,650 transitions/s 1.0% MAD +1.1%
Drain burst with terminal fence 403,670 increments/s 0.3% MAD 398,207 increments/s 0.8% MAD -1.4%
Drain burst with a change observer 371,080 increments/s 0.2% MAD 368,815 increments/s 0.7% MAD -0.6%
Lookup and send to one child 292,313 increments/s 0.2% MAD 290,535 increments/s 1.0% MAD -0.6%
Start and stop a machine 116,877 machines/s 0.8% MAD 117,426 machines/s 0.5% MAD +0.5%
Start and stop a parent with one child 24,116 families/s 1.9% MAD 23,765 families/s 0.3% MAD -1.5%
Complete an Effect invocation with tracing enabled 6,316 invocations/s 0.9% MAD 6,302 invocations/s 1.1% MAD -0.2%
Complete an Effect invocation with tracing disabled 7,494 invocations/s 0.9% MAD 7,470 invocations/s 0.5% MAD -0.3%
Plan transitions through a compound state 100,194 transitions/s 0.7% MAD 99,921 transitions/s 1.7% MAD -0.3%
Plan transitions through parallel regions 78,140 transitions/s 0.2% MAD 79,062 transitions/s 1.1% MAD +1.2%
Drain burst through a compound state 344,197 events/s 0.1% MAD 339,256 events/s 0.6% MAD -1.4%
Drain burst through two parallel regions 316,332 events/s 0.4% MAD 312,941 events/s 0.6% MAD -1.1%
Drain a compound-state burst with a change observer 316,638 events/s 0.4% MAD 314,273 events/s 0.4% MAD -0.7%
Idle machine heap per unit 1.8 KiB 0.0% MAD 1.8 KiB 0.2% MAD +0.2%
Raw generic managed process heap per unit 11.3 KiB 0.0% MAD 11.4 KiB 0.0% MAD +0.9%
Raw compiled process heap per unit 3.1 KiB 0.0% MAD 3.1 KiB 0.1% MAD +0.0%
Two independent idle machines heap per unit 3.6 KiB 1.1% MAD 3.6 KiB 0.2% MAD -0.0%
Idle parent with one child heap per unit 6.0 KiB 0.0% MAD 6.0 KiB 0.0% MAD +0.0%
Parent with observed child registry heap per unit 9.6 KiB 0.0% MAD 9.6 KiB 0.0% MAD +0.0%
Parent with observed invoked child snapshots heap per unit 7.0 KiB 0.0% MAD 7.0 KiB 0.1% MAD -0.2%

Process runtime reference change from base

Metric Base Base variability PR PR variability Difference
Start and stop a raw generic process 30,495 processes/s 1.0% MAD 30,055 processes/s 1.4% MAD -1.4%
Start and stop a raw compiled process 67,395 processes/s 0.3% MAD 69,075 processes/s 0.2% MAD +2.5%

Regression guard

No large, noise-adjusted throughput or heap regressions detected.

Versions and interpretation
  • Effect Machine: 0.46.0

Higher throughput is better; lower heap is better. Variability is the median absolute deviation across independent processes, relative to their median. Small differences on shared GitHub-hosted hardware remain informational; the required guard rejects only large changes beyond the measured noise allowance.

@SandroMaglione
SandroMaglione merged commit 42e2d90 into main Nov 7, 2026
9 checks passed
@SandroMaglione
SandroMaglione deleted the agent-first-transitions branch November 7, 2026 06:04
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