On this page

The workstation an agents workbench๏ƒ

๐Ÿ”ต Expert ยท Lesson 20

THE WORKBENCH. A codegen room is not just a place to run code - it has a WORKSTATION, a room-local canvas where an agent keeps named handles on things between commands. Advanced 12 introduced it as a store. This is what it is FOR.

THE LOOP THAT MAKES IT A WORKBENCH workstation.bind_object("service", obj) workstation.set_target("service") commands.execute_target_method("compute", bind_as_name="result") workstation.set_target("result") A command's RETURN VALUE becomes the next named binding. So an agent works in steps, each one addressable by a name it chose, without carrying objects around in its own head or re-deriving them from the runtime on every call.

THREE STORES, ONE NAMESPACE EACH bind_object / bind_attribute / bind_method Binding one name into two stores is allowed - they do not collide on WRITE. But a bare get(name) must resolve UNIQUELY, and when two stores answer it REFUSES as ambiguous rather than picking one. So the stores are separate for writing and deliberately unmerged for reading; get(name, store=...) is how you mean one of them.

describe_bindings() reports FIVE keys, not four: the fifth is target_store, which names WHICH store the active target came from, so a target round-trips through get(name, store=...).

STRONG OR WEAK, AND THE CHOICE IS ENFORCED weak_ref=True on something that cannot be weak-referenced RAISES rather than silently storing it strongly. A silent degrade would pin an object the caller believed was collectable - the bug you find three weeks later as a memory graph that never shrinks.

AND A COLLECTED WEAK BINDING TELLS THE ROOM. Weak-binding collection publishes an event into the room's own event system, so "the thing I was holding went away" is something an agent can be TOLD rather than discover by dereferencing a hole.

TWO WAYS TO STOP POINTING AT SOMETHING, AND THEY DIFFER clear_target() deselect; the binding stays, the object lives cleanup_target() call cleanup ON the target, then deselect One is putting the tool down. The other is dismantling it.

THE SECURITY LINE, STATED PLAINLY CommandSystem gates runtime access BEFORE a bind and leaves already-bound workstation objects OUTSIDE post-bind ACL policing. Getting a handle is the checkpoint; using the handle you were granted is not re-litigated on every call. Know which side of that line you are on when you bind something.

AND THE WORKSTATION NEVER FABRICATES. It stores room-local bindings only - it will not construct, resolve, or meld anything for you. Resolution is the command system's job; holding is the workstation's. cleanup() clears the stores and deliberately does NOT clean the objects inside them, because it did not make them.

Before you run๏ƒ

Use the Expert 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/04_expert/20_the_workstation_an_agents_workbench.py
py -3.14t UX_and_AIX_experiences/04_expert/20_the_workstation_an_agents_workbench.py

Download this collection ยท Source on GitHub

Public surface๏ƒ

rift.space.workstation - bind_object / bind_attribute / bind_method / get / release / describe_bindings / set_target / get_target / clear_target, and command_system.execute_target_method(bind_as_name=...)

Code๏ƒ

  1"""
  2TIER: expert (20)
  3GOAL: THE WORKBENCH. A codegen room is not just a place to run code -
  4      it has a WORKSTATION, a room-local canvas where an agent keeps
  5      named handles on things between commands. Advanced 12 introduced
  6      it as a store. This is what it is FOR.
  7
  8      THE LOOP THAT MAKES IT A WORKBENCH
  9        workstation.bind_object("service", obj)
 10        workstation.set_target("service")
 11        commands.execute_target_method("compute", bind_as_name="result")
 12        workstation.set_target("result")
 13      A command's RETURN VALUE becomes the next named binding. So an
 14      agent works in steps, each one addressable by a name it chose,
 15      without carrying objects around in its own head or re-deriving
 16      them from the runtime on every call.
 17
 18      THREE STORES, ONE NAMESPACE EACH
 19        bind_object / bind_attribute / bind_method
 20      Binding one name into two stores is allowed - they do not collide
 21      on WRITE. But a bare `get(name)` must resolve UNIQUELY, and when
 22      two stores answer it REFUSES as ambiguous rather than picking one.
 23      So the stores are separate for writing and deliberately unmerged
 24      for reading; `get(name, store=...)` is how you mean one of them.
 25
 26      `describe_bindings()` reports FIVE keys, not four: the fifth is
 27      `target_store`, which names WHICH store the active target came
 28      from, so a target round-trips through `get(name, store=...)`.
 29
 30      STRONG OR WEAK, AND THE CHOICE IS ENFORCED
 31      `weak_ref=True` on something that cannot be weak-referenced RAISES
 32      rather than silently storing it strongly. A silent degrade would
 33      pin an object the caller believed was collectable - the bug you
 34      find three weeks later as a memory graph that never shrinks.
 35
 36      AND A COLLECTED WEAK BINDING TELLS THE ROOM. Weak-binding
 37      collection publishes an event into the room's own event system, so
 38      "the thing I was holding went away" is something an agent can be
 39      TOLD rather than discover by dereferencing a hole.
 40
 41      TWO WAYS TO STOP POINTING AT SOMETHING, AND THEY DIFFER
 42        clear_target()    deselect; the binding stays, the object lives
 43        cleanup_target()  call cleanup ON the target, then deselect
 44      One is putting the tool down. The other is dismantling it.
 45
 46      THE SECURITY LINE, STATED PLAINLY
 47      `CommandSystem` gates runtime access BEFORE a bind and leaves
 48      already-bound workstation objects OUTSIDE post-bind ACL policing.
 49      Getting a handle is the checkpoint; using the handle you were
 50      granted is not re-litigated on every call. Know which side of that
 51      line you are on when you bind something.
 52
 53      AND THE WORKSTATION NEVER FABRICATES. It stores room-local
 54      bindings only - it will not construct, resolve, or meld anything
 55      for you. Resolution is the command system's job; holding is the
 56      workstation's. `cleanup()` clears the stores and deliberately does
 57      NOT clean the objects inside them, because it did not make them.
 58SURFACE EXERCISED: rift.space.workstation - bind_object / bind_attribute
 59                   / bind_method / get / release / describe_bindings /
 60                   set_target / get_target / clear_target, and
 61                   command_system.execute_target_method(bind_as_name=...)
 62VERIFY: rides the owner's 3.14t harness; asserts are the contract.
 63"""
 64import melder as md
 65
 66
 67class Ledger:
 68    """A plain object an agent might want to keep a handle on."""
 69
 70    def __init__(self) -> None:
 71        self.entries = []
 72
 73    def record(self, amount: int) -> int:
 74        self.entries.append(amount)
 75        return sum(self.entries)
 76
 77
 78def main() -> None:
 79    # A postured world, then a codegen room pointed at it (expert 11).
 80    book = md.Spellbook(aetheric_frame="bench-world")
 81    book.bind(spell=Ledger, existence="unique", binding_name="bench-ledger")
 82    book.configure_aether_frame(
 83        system_state="dynamic",
 84        disposal=None,
 85        disposal_method_names=None,
 86        rift_enabled=True,
 87        ai_native=True,
 88    )
 89    conduit = book.conjure(name="bench-root")
 90
 91    nexus = md.Nexus()
 92    system_configuration = nexus.create_configuration()
 93    system_configuration.with_rift_creation_enabled(True)
 94    system_configuration.with_allowed_target_frame_names(["bench-world"])
 95    nexus.activate(system_configuration)
 96
 97    rift_configuration = nexus.create_rift_configuration()
 98    rift_configuration.with_space_type("codegen")
 99    rift = nexus.create_rift(configuration=rift_configuration,
100                             rift_name="bench")
101    rift.mark_active()
102    rift.create_frame_link("bench-world")
103
104    room = rift.space
105    workstation = room.workstation
106    commands = room.command_system
107    print("room:", type(room).__name__,
108          " workstation:", workstation.workstation_id)
109    assert workstation.owner_space_id
110    print("the workstation belongs to THIS room - it is room-local")
111
112    # THE WORKSTATION HOLDS; IT DOES NOT MAKE. The object comes from the
113    # runtime; the bench just keeps a name on it.
114    ledger = conduit.meld(spell=Ledger, binding_name="bench-ledger")
115    workstation.bind_object("ledger", ledger)
116    # A bare get() works RIGHT NOW because the name is unique. Watch what
117    # happens to this exact call a few lines below.
118    assert workstation.get("ledger") is ledger
119    print()
120    print("bound 'ledger' - the SAME object, not a copy or a proxy")
121
122    # THREE STORES. Binding the same name in two of them is ALLOWED -
123    # they are separate namespaces and neither write disturbs the other.
124    workstation.bind_attribute("ledger", "a note about the ledger")
125    assert workstation.get("ledger", store="objects") is ledger
126    assert workstation.get("ledger", store="attributes") != ledger
127    print("the name 'ledger' lives in TWO stores; addressed with store=,")
128    print("  each answers its own value")
129
130    # ...BUT A BARE READ ACROSS THEM REFUSES. Writing is per-store;
131    # READING without naming a store must resolve UNIQUELY, and when it
132    # cannot, melder says so instead of picking a winner.
133    try:
134        workstation.get("ledger")
135        raise AssertionError("expected an ambiguity refusal")
136    except ValueError as error:
137        print()
138        print("get('ledger') with no store ->", error)
139        print("  the SAME call succeeded twenty lines ago. Nothing about")
140        print("  it changed - the WORLD did. A bare name is only an")
141        print("  address while it happens to be unique, and melder tells")
142        print("  you the moment it stops being one instead of guessing")
143
144    summary = workstation.describe_bindings()
145    print("describe_bindings() keys:", sorted(summary))
146    print("  five, not four - `target_store` names WHICH store the")
147    print("  active target came from, so a target round-trips")
148
149    # SELECT A TARGET, THEN WORK THROUGH IT.
150    workstation.set_target("ledger", store="objects")
151    assert workstation.get_target() is ledger
152    print()
153    print("target set ->", type(workstation.get_target()).__name__)
154
155    # THE LOOP: a command's RESULT becomes the next named binding.
156    commands.execute_target_method(
157        "record", 100, bind_as_name="running_total",
158    )
159    total = workstation.get("running_total")
160    assert total == 100
161    print("execute_target_method('record', 100, bind_as_name=...)")
162    print("   -> workstation['running_total'] =", total)
163
164    commands.execute_target_method(
165        "record", 250, bind_as_name="running_total",
166    )
167    assert workstation.get("running_total") == 350
168    print("   ran again ->", workstation.get("running_total"))
169    print("  each step is addressable by a name the AGENT chose")
170
171    # STRONG VS WEAK IS ENFORCED, NOT COERCED.
172    workstation.bind_object("weak_ledger", ledger, weak_ref=True)
173    print()
174    print("weak binding stored; collection publishes a ROOM EVENT, so")
175    print("  'what I was holding went away' is told, not discovered")
176    try:
177        workstation.bind_object("weak_int", 42, weak_ref=True)
178        print("  (an int accepted a weak binding on this build)")
179    except Exception as error:
180        print("  explicit weak on a non-weakreferenceable value refused -",
181              type(error).__name__)
182        print("  it will NOT quietly store it strongly instead")
183
184    # TWO WAYS TO STOP POINTING. Only one of them touches the object.
185    workstation.clear_target()
186    assert workstation.get("ledger", store="objects") is ledger
187    print()
188    print("clear_target(): deselected, and the binding still holds it")
189    print("  cleanup_target() is the other one - it CALLS cleanup first")
190
191    # AND AN EMPTY BENCH REFUSES RATHER THAN ANSWERING None. `get_target()`
192    # raises when nothing is selected: "no target" is not a value you can
193    # accidentally use, it is a question you should not have asked yet.
194    try:
195        workstation.get_target()
196        raise AssertionError("expected a refusal on an empty target")
197    except ValueError as error:
198        print("get_target() with nothing selected refused -", error)
199        print("  never-substitute: None would be a value, and a caller")
200        print("  would call a method on it before noticing")
201
202    # RELEASE RETURNS WHAT IT REMOVED, so a handoff is one call.
203    removed = workstation.release("running_total")
204    assert removed == 350
205    print()
206    print("release() returned the value it removed:", removed)
207
208    print()
209    print("the workstation is where an agent keeps its work between")
210    print("commands - it holds, it never fabricates, and the handle you")
211    print("were granted is not re-checked every time you use it")
212
213
214if __name__ == "__main__":
215    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 expert examples ยท Level guide