D-226 layer 4, rebuilt against the post-D-255 stepped Atlas after the Phase-1 reconciliation (the original intent list targeted the retired continuous-zoom viewer). Eleven intents, each backed by the exact production handler a click calls — select/open for systems and bodies (extracted shared by-id tails so click and intent paths are one code path), scroll_rung, reset_view, back, open/close_atlas, set_overlay — plus two new first-class capabilities: jump_to_center (the fixed-center revisit pattern proven by five eyeball drivers, via a new StepCanvasViewer.jump_to seam that reuses _scroll_rung's exact request tail — same extent cap, same cache keys) and get_current_canvas_summary (allocation-light reads off the raw wire dict, courses-by-class, draw-matched settlement dedup — no PNG decode). Contract shape: dumb AtlasAgentBridge autoload holding the app handle (untyped per the parse-order rule), all logic in the static AtlasAgentInterface class. observe() is side-effect-free: current state + a generic Control-walk affordance tree. In-process consumers only this ticket (documented); the committed reference driver (atlas_agent_driver.gd, InputSwallower + settle-until-ready from the T-1157 inventory) replaces the scratch eyeball drivers as the sanctioned headless-drive pattern. select_city/open_regional dropped with recorded rationale (no settlement hit-test affordance exists post-D-255) — diff on the ticket. 33 new tests across three suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
165 lines
6.1 KiB
GDScript
165 lines
6.1 KiB
GDScript
## Reference in-process Atlas agent driver (T-971, D-226 item 4) — the
|
|
## SANCTIONED way to drive the Atlas headlessly via AtlasAgentInterface,
|
|
## replacing the five hand-rolled scratch eyeball drivers the T-1183/T-1157
|
|
## harness rounds produced ad hoc. Every real navigation call below goes
|
|
## through AtlasAgentInterface.act()/observe() — this file does not touch
|
|
## `_gui_input`, does not synthesize pixel events, and does not reach into
|
|
## any screen's private fields.
|
|
##
|
|
## Folds in two of the T-1157 harness-techniques inventory patterns
|
|
## (pql ticket show T-1157):
|
|
## 1. InputSwallower — a root-level Node, added first, low process_priority,
|
|
## whose _input()/_unhandled_input() both call
|
|
## get_viewport().set_input_as_handled() unconditionally. Neutralizes
|
|
## real desktop input reaching this `-s` SceneTree driver's window
|
|
## (X11/XWayland can deliver pointer events to an unfocused window under
|
|
## the cursor on a shared desktop) — this driver runs real production
|
|
## code via method calls, but the WINDOW still exists and can still
|
|
## receive stray input, so the guard applies here too.
|
|
## 4. Full-run-restart-on-anomaly (documented, not code-enforced — see
|
|
## _run() header note below): any assertion failure here should discard
|
|
## the whole run rather than retrying mid-sequence, matching the
|
|
## inventory's "partial retries silently corrupt the state the
|
|
## comparison depends on" finding. This driver's own job list is short
|
|
## and idempotent per invocation (fresh process each run), so the
|
|
## discipline is "don't loop-retry a failed step in place" rather than a
|
|
## literal restart mechanism.
|
|
##
|
|
## Technique 5 (fixed-center revisit) is what `jump_to_center` (this file's
|
|
## own `_run_job("jump_to_center", ...)` dispatch) exists to exercise —
|
|
## AtlasAgentInterface's production seam for it, not reimplemented here.
|
|
##
|
|
## Usage (JSON job list on stdin path, matching visual_capture.gd's own
|
|
## `-- --flag value` CLI convention):
|
|
## godot --rendering-driver opengl3 --path client \
|
|
## -s res://tests/atlas_agent_driver.gd -- \
|
|
## --jobs res://tests/fixtures/atlas_agent_smoke_jobs.json \
|
|
## --output /abs/path/observe_log.json
|
|
##
|
|
## Each job is {"intent": "...", "params": {...}}; "observe" is a
|
|
## pseudo-intent this driver special-cases to call
|
|
## AtlasAgentInterface.observe() instead of act() (observe takes no params).
|
|
## The full sequence of act()/observe() results is written to --output as a
|
|
## JSON array — the smoke test (test_atlas_agent_driver_smoke.gd) reads this
|
|
## back to assert the reference driver actually completes a real session.
|
|
extends SceneTree
|
|
|
|
var _jobs_path: String = ""
|
|
var _output_path: String = ""
|
|
var _results: Array = []
|
|
|
|
|
|
func _init():
|
|
_run.call_deferred()
|
|
|
|
|
|
func _run() -> void:
|
|
_parse_args()
|
|
|
|
if _jobs_path.is_empty():
|
|
push_error("atlas_agent_driver: --jobs PATH is required")
|
|
quit(1)
|
|
return
|
|
if _output_path.is_empty():
|
|
push_error("atlas_agent_driver: --output PATH is required")
|
|
quit(1)
|
|
return
|
|
|
|
var jobs: Array = _load_jobs(_jobs_path)
|
|
if jobs.is_empty():
|
|
push_error("atlas_agent_driver: no jobs loaded from %s" % _jobs_path)
|
|
quit(1)
|
|
return
|
|
|
|
var main_scene = load("res://scenes/main.tscn")
|
|
var main_node = main_scene.instantiate()
|
|
|
|
# Technique 1 (T-1157 inventory) — InputSwallower, added FIRST so its low
|
|
# process_priority still runs before anything that might otherwise react
|
|
# to stray desktop input reaching this unfocused/off-screen window.
|
|
var swallower := _InputSwallower.new()
|
|
swallower.process_priority = -1000
|
|
root.add_child(swallower)
|
|
|
|
root.add_child(main_node)
|
|
|
|
# Settle frames — camera smoothing, fog uniform init, UI layout (same
|
|
# rationale as visual_capture.gd's own settle-frame pass).
|
|
for i in range(10):
|
|
await process_frame
|
|
|
|
var atlas_agent_interface := load("res://ui/implant/apps/atlas/atlas_agent_interface.gd")
|
|
var atlas_agent_bridge: Node = root.get_node("/root/AtlasAgentBridge")
|
|
|
|
for job: Dictionary in jobs:
|
|
var intent: String = str(job.get("intent", ""))
|
|
var params: Dictionary = job.get("params", {})
|
|
var result: Dictionary
|
|
if intent == "observe":
|
|
result = atlas_agent_interface.observe(atlas_agent_bridge.current_app)
|
|
else:
|
|
result = atlas_agent_interface.act(atlas_agent_bridge.current_app, intent, params)
|
|
# Settle-until-ready (T-1157 inventory item 2's underlying discipline,
|
|
# applied generically here rather than per-navigation-intent): a few
|
|
# frames after every mutating call so the resulting screen/viewer
|
|
# state (panel rebuilds, queue_redraw()) has actually applied before
|
|
# the NEXT job's observe() reads it.
|
|
for i in range(4):
|
|
await process_frame
|
|
_results.append({"intent": intent, "params": params, "result": result})
|
|
|
|
_write_output(_output_path, _results)
|
|
quit()
|
|
|
|
|
|
func _parse_args() -> void:
|
|
var args := OS.get_cmdline_user_args()
|
|
var i := 0
|
|
while i < args.size():
|
|
match args[i]:
|
|
"--jobs":
|
|
i += 1
|
|
if i < args.size():
|
|
_jobs_path = args[i]
|
|
"--output":
|
|
i += 1
|
|
if i < args.size():
|
|
_output_path = args[i]
|
|
i += 1
|
|
|
|
|
|
static func _load_jobs(path: String) -> Array:
|
|
var abs_path: String = path
|
|
if path.begins_with("res://"):
|
|
abs_path = ProjectSettings.globalize_path(path)
|
|
var f := FileAccess.open(abs_path, FileAccess.READ)
|
|
if f == null:
|
|
return []
|
|
var text := f.get_as_text()
|
|
f.close()
|
|
var parsed: Variant = JSON.parse_string(text)
|
|
return parsed if parsed is Array else []
|
|
|
|
|
|
static func _write_output(path: String, results: Array) -> void:
|
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
|
if f == null:
|
|
push_error("atlas_agent_driver: cannot open output path %s" % path)
|
|
return
|
|
f.store_string(JSON.stringify(results, " "))
|
|
f.close()
|
|
|
|
|
|
## T-1157 inventory item 1 — see this file's own header doc for the full
|
|
## rationale. A tiny inner class rather than a separate file: this pattern
|
|
## has exactly one consumer (this driver) today: promote to a shared
|
|
## `client/tests/` helper the moment a second `-s` driver needs it.
|
|
class _InputSwallower:
|
|
extends Node
|
|
|
|
func _input(_event: InputEvent) -> void:
|
|
get_viewport().set_input_as_handled()
|
|
|
|
func _unhandled_input(_event: InputEvent) -> void:
|
|
get_viewport().set_input_as_handled()
|