Skip to content

DX improvements: IDE typing, guard transitions, structured logging, and API stability guarantees - #115

Closed
sanbales wants to merge 11 commits into
gtri:devfrom
sanbales:dev
Closed

DX improvements: IDE typing, guard transitions, structured logging, and API stability guarantees#115
sanbales wants to merge 11 commits into
gtri:devfrom
sanbales:dev

Conversation

@sanbales

@sanbales sanbales commented May 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Eight commits' worth of developer-experience improvements for UPSTAGE, focused
on making the framework easier and faster to use for both human modelers and
AI-assisted development (Copilot, Cursor, Claude Code, etc.). All changes are
backwards-compatible — existing simulations keep working; every new API is
additive.

What's in this PR

Actor type-checking (PEP 681)

  • @dataclass_transform() on Actor — type checkers (Pylance, mypy, pyright)
    now auto-generate __init__ signatures for Actor subclasses from their
    State field declarations. No more .pyi stubs for user models.
  • State(init=True|False) parameter replacing the inverted no_init, so
    PEP 681 can recognize it as a field specifier.

Task networks: ergonomics and introspection

  • Class-reference API for TaskNetworkFactory. Users pass Task
    classes as keys instead of stringifying them by hand:

    TaskNetworkFactory(
        "Cashier",
        task_links={
            WaitInLane: TaskLinks(transitions=[(DoCheckout, None)]),
            DoCheckout: TaskLinks(transitions=[(WaitInLane, None)]),
        },
    )
  • Construction-time validation — unknown task-link references now raise
    immediately with a helpful error instead of failing at simulation time.

  • Guard-based transitions — declarative branching without needing a
    DecisionTask:

    TaskLinks(transitions=[
        (Break, needs_break),
        (DoCheckout, None),  # fallback
    ])

    A new Transition NamedTuple is the normalized internal form and is
    re-exported via UP.Transition.

  • on_enter() / on_exit() hooks on Task — zero-time setup/teardown
    without introducing a separate task.

  • to_mermaid() and to_dot() on TaskNetwork and TaskNetworkFactory
    for quick network visualization (renders inline in Jupyter, GitHub
    Markdown, any Mermaid viewer). Covered by golden-file snapshot tests.

Structured logging

  • actor.log() now routes through Python's standard logging module in
    addition to the existing _debug_log list. Logger hierarchy is
    upstage_des.actor.<name> (.rehearsal suffix for rehearsal clones) so
    users can filter per-actor or suppress rehearsal noise with standard
    logging config. A NullHandler is installed on import — library use
    is silent by default.
  • actor.log(msg, *args, level=logging.INFO) accepts printf-style
    arguments; formatting is deferred until at least one sink wants the
    record. Benchmarks show ~15% faster log-heavy loops when both sinks are
    off vs. legacy f-string call sites.
  • actor.log() is now write-only. The canonical reader is the new
    actor.logs property (actor.get_log() also still works). Calling
    actor.log() with no args emits a DeprecationWarning.

API stability guarantees

  • New docs/source/user_guide/how_tos/api_stability.rst — lists every
    public symbol in stable / experimental / internal tiers, documents
    the deprecation policy, and explains the logging hierarchy. This gives
    users (and AI agents) a single authoritative reference for what's safe
    to depend on.
  • api.py gains re-exports for TASK_GEN, SIMPY_GEN, ROUTINE_GEN,
    and Transition so modelers can import upstage_des.api as UP and
    never reach into private modules.

Docs and examples

  • How-to guides updated for guard transitions, hooks, and class-reference
    factories.
  • Cashier jupyterlite example normalized to the UP. import idiom so AI
    agents mimicking the example pick up consistent patterns.

Performance

Bench (same workload, 10 trials each, 20 actors × 200 log calls × 5s sim):

Workload gtri/dev This PR
f-string calls, debug_log=True 334.1 ms 338.7 ms
f-string calls, debug_log=False 155.5 ms 166.0 ms
printf calls, debug_log=False + logger off n/a 135.5 ms
network step loop, debug_log=True 50.6 ms 52.7 ms

Legacy f-string call sites: within a few percent of baseline. Users who
migrate hot logs to printf-style and disable both sinks: ~13% faster than
baseline. No regression in the network-step path.

Back-compat

  • All previously supported TaskLinks(default=..., allowed=...) usage
    works unchanged.
  • actor.get_log() / actor._debug_log semantics unchanged.
  • actor.log() with no args still returns the list (with a
    DeprecationWarning).
  • The original string-keyed TaskNetworkFactory(name, task_classes, task_links)
    signature still works.

Test plan

  • pixi run -e dev test274 tests pass (was 262 pre-PR; 12 new
    tests added for logging, diagram snapshots, and guard transitions).
  • pixi run -e dev lint-check — ruff, ssort, mypy, pyproject-fmt all
    clean.
  • Existing tests untouched except where behavior intentionally changed
    (5 edits total).
  • Microbenchmarks compared against gtri/dev tip to verify no
    meaningful regression.

Housekeeping (post-initial review)

  • RunCashier.ipynb stripped of outputs and execution counts.
  • pixi.lock regenerated with pixi 0.41.0 to match CI's pinned version.
  • Sphinx docs build warnings fixed — two malformed grid tables in api_stability.rst converted to list-tables, and a missing guard-based transitions xref anchor added to task_networks.rst.

Docs now build with zero warnings.

Commits in order

  1. 02ae210 Add @dataclass_transform to Actor for IDE type-checking support
  2. 8901f4b Add class-reference API and validation to TaskNetworkFactory
  3. de91174 Add guard-based transitions, on_enter/on_exit hooks, and network visualization
  4. 2baa23e Document task network improvements and polish cashier example
  5. fbe6c9c Exempt jupyterlite example model from docstring lint
  6. e06d46f DX improvements: Transition NamedTuple, normalized imports, API stability doc
  7. e459b19 Route actor events through Python logging; defer format interpolation
  8. 3673928 Make Actor.log() write-only; add logs property as canonical reader
  9. 408a49c Strip outputs and execution counts from RunCashier.ipynb
  10. 113f760 Regenerate pixi.lock with pixi 0.41.0 to match CI
  11. 8cf9f7c Fix Sphinx warnings: malformed tables and missing xref anchor

Santiago [C] Balestrini Robinson and others added 11 commits April 1, 2026 23:55
Apply PEP 681 @dataclass_transform to Actor so type checkers
(Pylance, mypy, pyright) automatically generate __init__ signatures
for Actor subclasses from their State field declarations. This means
modelers get autocomplete, type errors, and hover docs without
writing .pyi stubs for every model class.

Also replaces the `no_init` parameter on State with `init` (inverted)
to match the PEP 681 field specifier protocol.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TaskNetworkFactory now accepts class references as keys in task_links,
deriving task_classes automatically:

    TaskNetworkFactory("Net", task_links={
        GoToWork: TaskLinks(default=TalkToBoss, allowed=[TalkToBoss]),
        ...
    })

TaskLinks.default and .allowed also accept class references alongside
strings. All references are resolved to __name__ strings internally.

Construction-time validation checks that all referenced task names
exist in task_classes and warns about orphaned entries.

The existing string-keyed API continues to work unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…alization

TaskLinks now supports a `transitions` field: an ordered list of
(target, guard) tuples where the first matching guard picks the next
task. This enables declarative branching without DecisionTask.

Task gains on_enter() and on_exit() hooks — zero-time methods called
before/after task() by the network loop. These replace the setup and
teardown roles of DecisionTask.

TaskNetworkFactory gains to_mermaid() and to_dot() methods for
rendering task network diagrams.

Guard evaluation priority: task queue (imperative) > guards > default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update how-to docs for guard transitions, on_enter/on_exit hooks, and
network visualization.  Expand the cashier JupyterLite example and add
jupyterlite model sources to lint tasks.
The jupyterlite example code is pedagogical and shouldn't need the full
Google-style docstrings that the src/ tree enforces.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lity doc

- Introduce Transition NamedTuple so task-network transitions expose
  .target/.guard/.label and consumers no longer branch on tuple length.
  Raw (target, guard[, label]) tuples remain accepted.
- Declare every task node in to_mermaid()/to_dot() output so hooked and
  unhooked tasks render consistently.
- Rename internal State._no_init to _init to match the PEP 681 public
  parameter and drop the double-negation.
- Re-export TASK_GEN/SIMPY_GEN/ROUTINE_GEN and Transition through
  upstage_des.api; update the cashier example to import only via UP.
- Add an API stability guide (stable / experimental / internal tiers and
  deprecation policy) under user_guide/how_tos/.
- Add golden-file snapshot tests for to_mermaid()/to_dot().

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Actor.log() now gates on two independent sinks: the existing
per-actor _debug_log list (controlled by the `debug_log` flag set at
Actor construction) and the standard Python logging module rooted at
`upstage_des`.  The library attaches a NullHandler on import and
defaults to WARNING, so it remains silent unless the user opts in.

Call sites use printf-style arguments so interpolation only runs when
at least one sink will actually consume the record.  Internal callers in
task.py, actor.py, task_network.py, and nucleus.py were converted to
this form, and the expensive inspect.stack() walk inside
Actor._log_caller is now skipped when both sinks are disabled.

Actor loggers are named `upstage_des.actor.<name>` so users can filter
per-actor; rehearsal clones log to `<name>.rehearsal`.

Back-compat: the `_debug_log` list shape and `actor.log()` (read) /
`actor.log(msg)` (write) surface are unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reading the in-memory event log via `actor.log()` (no args) is now
deprecated.  The canonical readers are:

* `actor.logs` — new read-only property returning the list.
* `actor.get_log()` — existing method, unchanged.

`actor.log()` with no arguments still returns the list but raises
DeprecationWarning pointing users to the replacements.  This resolves
the dual-purpose signature that was particularly confusing for
code-generation agents, which often pass `None` intending to write.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI pins prefix-dev/setup-pixi@v0.9.4 with pixi-version v0.41.0, but the
lock in 2baa23e was written by a newer pixi (0.67.0).  The format
difference (older pixi uses `.` instead of `./` for the workspace
package URL and omits the `options.pypi-prerelease-mode` block) was
causing `pixi install --locked` to fail in GitHub Actions.

No dependency scope changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The notebook was inadvertently committed with executed outputs in
2baa23e.  This clears them forward; prior revisions still carry the
outputs in history but that's acceptable given the risk of rewriting
a pushed branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Convert two grid tables in api_stability.rst to list-tables.  The
  grid-table variants were flagged by docutils for an unterminated
  inline literal (`` ``UP.Event`` `` with no trailing space before the
  column separator) and a malformed table (same issue on
  `` ``UP.MessageContent`` ``).  list-tables are whitespace-robust and
  easier to maintain.
- Add an explicit `guard-based transitions` ref target above the
  section heading in task_networks.rst so the cross-reference from
  task.rst resolves.

Build now completes with zero warnings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@JamesArruda

Copy link
Copy Markdown
Collaborator

Thanks for this! These are some great suggestions.

A yet un-posted version 1.0 branch is being made that incorporates some of this advice already (the dataclass_transform) but not yet the task network updates or the logging integration.

When I get it posted, I will ask you to put in an MR into that branch so you can get contribution credit. I will likely not approve/merge this in given the changes in the background that you weren't aware of!

@sanbales

sanbales commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for this! These are some great suggestions.

A yet un-posted version 1.0 branch is being made that incorporates some of this advice already (the dataclass_transform) but not yet the task network updates or the logging integration.

When I get it posted, I will ask you to put in an MR into that branch so you can get contribution credit. I will likely not approve/merge this in given the changes in the background that you weren't aware of!

Thanks, James!

FWIW, not worried about the credit (Opus did most of the work 🤣 anyways), and not expecting these to be included in main. These were just some scattered thoughts.

@sanbales sanbales closed this May 5, 2026
@JamesArruda JamesArruda mentioned this pull request May 6, 2026
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.

2 participants