On this page

The frame viewer facade๏ƒ

๐ŸŸ  Advanced ยท Lesson 13

THE FRAME VIEWER - the room's read surface, and the first place in melder where the AIX claim stops being a design philosophy and becomes methods you can call.

IT IS A FACADE, AND IT SAYS SO. FrameViewer's own docstrings read "FACADE PASS-THROUGH to ViewMultiFrame.list_frame_names(...)" and note that it builds "a ViewMultiFrame per invocation against a freshly resolved" source. That second half matters more than the first: THERE IS NO CACHED SNAPSHOT. Every read resolves fresh, so a viewer you held onto for an hour cannot hand you an hour-old world. You trade a little work per call for never having to ask "is this stale?"

FOUR SPECIALIZED VIEWS BEHIND IT, IN TWO GROUPS get_view_multiframe() HOST-SCOPED - all frames, needs no name get_view_frame() FRAME-SCOPED - targets, visibility, topology get_view_conduit() FRAME-SCOPED - records, roots, relationships get_view_spell() FRAME-SCOPED - identity, source, binding

AND THERE IS NO DEFAULT FRAME. The three frame-scoped accessors REQUIRE a frame name, because "the viewer no longer supports default-frame routing for frame-local operations". A freshly opened rift is contracted to no frames, so until you assign one there is no name to pass - and that is the honest state, not a bug in your code.

SIGNATURE DEFECT FOUND AND FIXED (2026-08-02): these were typed frame_name: Optional[str] = None and then rejected None unconditionally - the documented default was never valid, so a reader who trusted the signature got an error for using it. 33 methods on FrameViewer now declare frame_name: str, so omitting it fails at the CALL with a TypeError instead of inside the body.

NOTE THE DISTINCTION THAT SURVIVED THE FIX, because it is real: FrameViewer.describe_missing_surface(frame_name) SELECTOR, required ViewFrame.describe_missing_surface(frame_name=None) ASSERTION, optional A ViewFrame is ALREADY bound to one frame, so naming it there is a guard against reading the wrong world, not a choice of which to read. Optional is correct on one and a lie on the other.

AND THEN THE PART WORTH THE WHOLE LESSON.

The viewer carries a surface built FOR AGENTS, by name:

describe_agent_onboarding_json() how to use me describe_viewer_agent_purpose_json() what I am for describe_viewer_method_surface() what I can do list_viewer_method_names_ast_json() my methods, from the AST describe_viewer_class_surface_ast_json()

Most libraries expect a reader to arrive already knowing the API - docs live in a website, and the object tells you nothing about itself. These methods invert that. The object onboards its own caller, in JSON, at runtime.

For a human that is a curiosity. For an agent it is the difference between guessing a surface and reading it - and it is the same idea as list_supported_command_methods() in lesson 11. Twice now, melder has answered "what may I do here?" with a method instead of a manual.

Before you run๏ƒ

Use the Advanced guide for prerequisite concepts. Run from a checkout with Melder installed and Python 3.14 free-threading selected. The collection download includes the level's local helper modules.

Run the saved script๏ƒ

python UX_and_AIX_experiences/03_advanced/13_the_frame_viewer_facade.py
py -3.14t UX_and_AIX_experiences/03_advanced/13_the_frame_viewer_facade.py

Download this collection ยท Source on GitHub

Public surface๏ƒ

room.frame_viewer, get_view_* accessors, describe_available_views, describe_viewer_method_surface, describe_agent_onboarding_json

Code๏ƒ

  1"""
  2TIER: advanced (13)
  3GOAL: THE FRAME VIEWER - the room's read surface, and the first place in
  4      melder where the AIX claim stops being a design philosophy and
  5      becomes methods you can call.
  6
  7      IT IS A FACADE, AND IT SAYS SO.
  8      FrameViewer's own docstrings read "FACADE PASS-THROUGH to
  9      ViewMultiFrame.list_frame_names(...)" and note that it builds "a
 10      ViewMultiFrame per invocation against a freshly resolved" source.
 11      That second half matters more than the first: THERE IS NO CACHED
 12      SNAPSHOT. Every read resolves fresh, so a viewer you held onto for
 13      an hour cannot hand you an hour-old world. You trade a little work
 14      per call for never having to ask "is this stale?"
 15
 16      FOUR SPECIALIZED VIEWS BEHIND IT, IN TWO GROUPS
 17        get_view_multiframe() HOST-SCOPED - all frames, needs no name
 18        get_view_frame()      FRAME-SCOPED - targets, visibility, topology
 19        get_view_conduit()    FRAME-SCOPED - records, roots, relationships
 20        get_view_spell()      FRAME-SCOPED - identity, source, binding
 21
 22      AND THERE IS NO DEFAULT FRAME.
 23      The three frame-scoped accessors REQUIRE a frame name, because "the
 24      viewer no longer supports default-frame routing for frame-local
 25      operations". A freshly opened rift is contracted to no frames, so
 26      until you assign one there is no name to pass - and that is the
 27      honest state, not a bug in your code.
 28
 29      SIGNATURE DEFECT FOUND AND FIXED (2026-08-02): these were typed
 30      `frame_name: Optional[str] = None` and then rejected None
 31      unconditionally - the documented default was never valid, so a
 32      reader who trusted the signature got an error for using it. 33
 33      methods on FrameViewer now declare `frame_name: str`, so omitting
 34      it fails at the CALL with a TypeError instead of inside the body.
 35
 36      NOTE THE DISTINCTION THAT SURVIVED THE FIX, because it is real:
 37        FrameViewer.describe_missing_surface(frame_name)   SELECTOR, required
 38        ViewFrame.describe_missing_surface(frame_name=None) ASSERTION, optional
 39      A ViewFrame is ALREADY bound to one frame, so naming it there is a
 40      guard against reading the wrong world, not a choice of which to
 41      read. Optional is correct on one and a lie on the other.
 42
 43      AND THEN THE PART WORTH THE WHOLE LESSON.
 44
 45      The viewer carries a surface built FOR AGENTS, by name:
 46
 47        describe_agent_onboarding_json()      how to use me
 48        describe_viewer_agent_purpose_json()  what I am for
 49        describe_viewer_method_surface()      what I can do
 50        list_viewer_method_names_ast_json()   my methods, from the AST
 51        describe_viewer_class_surface_ast_json()
 52
 53      Most libraries expect a reader to arrive already knowing the API -
 54      docs live in a website, and the object tells you nothing about
 55      itself. These methods invert that. The object onboards its own
 56      caller, in JSON, at runtime.
 57
 58      For a human that is a curiosity. For an agent it is the difference
 59      between guessing a surface and reading it - and it is the same idea
 60      as list_supported_command_methods() in lesson 11. Twice now, melder
 61      has answered "what may I do here?" with a method instead of a
 62      manual.
 63SURFACE EXERCISED: room.frame_viewer, get_view_* accessors,
 64                   describe_available_views, describe_viewer_method_surface,
 65                   describe_agent_onboarding_json
 66VERIFY: rides the owner's 3.14t run; asserts are the contract.
 67"""
 68import json
 69
 70import melder as md
 71
 72
 73def main() -> None:
 74    nexus = md.Nexus()
 75    system_config = nexus.create_configuration()
 76    system_config.with_rift_creation_enabled(True)
 77    nexus.activate(system_config)
 78
 79    rift_config = nexus.create_rift_configuration()
 80    rift_config.with_space_type("capability")
 81    rift = nexus.create_rift(configuration=rift_config, rift_name="observatory")
 82    rift.mark_active()
 83
 84    viewer = rift.space.frame_viewer
 85    assert isinstance(viewer, md.FrameViewer)
 86    print("viewer:", viewer.id)
 87
 88    # THE FACADE'S COMMON READS. A fresh rift has no assigned frames yet,
 89    # so these are honest zeros rather than errors - the read surface
 90    # works on an empty world.
 91    frame_names = viewer.list_frame_names()
 92    print("frames visible:", viewer.count_frames(), frame_names)
 93    assert isinstance(frame_names, list)
 94    assert viewer.count_frames() == len(frame_names)
 95
 96    # THE VIEWS SPLIT INTO TWO GROUPS, AND THAT SPLIT IS THE LESSON.
 97    #
 98    # get_view_multiframe() is HOST-SCOPED - it asks about all frames, so
 99    # it needs no frame name and works right now.
100    multiframe = viewer.get_view_multiframe()
101    assert isinstance(multiframe, md.ViewMultiFrame)
102    print("view_multiframe:", type(multiframe).__name__, "(host-scoped)")
103
104    # get_view_frame / get_view_conduit / get_view_spell are FRAME-SCOPED.
105    # THERE IS NO DEFAULT FRAME, and since the signature fix that is now
106    # enforced by the signature itself - omitting the name is a TypeError
107    # at the call, not a ValueError from somewhere inside.
108    for accessor in ("get_view_frame", "get_view_conduit", "get_view_spell"):
109        try:
110            getattr(viewer, accessor)()
111            raise AssertionError(f"{accessor} should require a frame name")
112        except TypeError as error:
113            print(f"  {accessor:18s} requires a frame name: {error}")
114
115    # This rift is contracted to no frames, so there is no name to pass -
116    # which is the honest state of a freshly opened rift.
117    assert rift.list_assigned_frame_names() == ()
118    print("assigned frames:", rift.list_assigned_frame_names(),
119          "- nothing to scope a frame-local view to yet")
120
121    # The view TYPES are still inspectable without an instance, which is
122    # how the next two lessons map their surfaces.
123    for view_type in (md.ViewFrame, md.ViewConduit, md.ViewSpell,
124                      md.ViewMultiFrame):
125        assert isinstance(view_type, type)
126    print("four view types exported:", ", ".join(
127        t.__name__ for t in (md.ViewFrame, md.ViewConduit, md.ViewSpell,
128                             md.ViewMultiFrame)))
129
130    # NO CACHED SNAPSHOT. The facade resolves per invocation, so two calls
131    # hand back two view objects rather than one memoized one.
132    assert viewer.get_view_multiframe() is not viewer.get_view_multiframe()
133    print("fresh view per invocation - nothing to go stale")
134
135    # WHAT VIEWS EXIST? Ask, do not assume.
136    available = viewer.describe_available_views()
137    assert isinstance(available, list)
138    print("describe_available_views ->", len(available), "entries")
139
140    # THE AIX SURFACE. The viewer describes its own method surface...
141    surface = viewer.describe_viewer_method_surface()
142    assert isinstance(surface, dict)
143    print("describe_viewer_method_surface ->", len(surface), "keys")
144
145    # ...and onboards an agent in JSON, at runtime, from the object itself.
146    onboarding = viewer.describe_agent_onboarding_json()
147    assert isinstance(onboarding, str)
148    parsed = json.loads(onboarding)
149    print("describe_agent_onboarding_json -> valid JSON,",
150          len(onboarding), "chars")
151    print("  top-level keys:", sorted(parsed)[:6])
152
153    purpose = viewer.describe_viewer_agent_purpose_json()
154    assert isinstance(purpose, str)
155    json.loads(purpose)
156    print("describe_viewer_agent_purpose_json -> valid JSON")
157
158    # clone() hands back an independent facade over the same world.
159    twin = viewer.clone()
160    assert isinstance(twin, md.FrameViewer)
161    assert twin is not viewer
162    assert twin.count_frames() == viewer.count_frames()
163    print("clone: independent object, same reading")
164
165    print()
166    print("a facade with no snapshot - every read is a fresh resolve")
167    print("and the object onboards its own caller instead of assuming docs")
168
169
170if __name__ == "__main__":
171    main()

Check the outcome๏ƒ

The script contains its own assertions or demonstrated refusal paths. Run it to evaluate those checks against your installed version. The code above is taken directly from the saved file; no run output is invented here.

More advanced examples ยท Level guide