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
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.