Lucky Robots Blog Open Roles

2.18 · LuckyEditor-Agent

Editor-side agent functionality (LLM orchestration, tool dispatch, MCP bridges, chat panel) packaged as its own static library, with a plugin-shaped public surface so it can later be lifted out of the editor without re-architecture.

LuckyEditor-Agent/ namespace LuckyEditorAgent StaticLib (plugin-shaped) links into LuckyEditor depends on Hazel + Hazel-Bridge
LuckyEditor-Agent (StaticLib, plugin-shaped) Public surface (~3 headers) EditorAgent.h Attach(host, config) IEditorAgentHost.h 14 virtuals EditorAgentConfig MCP ports · welcome flag opaque pimpl Façade & LLM EditorAgent.cpp LLM/AgentClient · ToolRegistry Tools HazelAgentInterface (~30 ops) Get/ Set/ Set/PropertySetters/ MCP adapters McpAdapters/HarnessBridge McpAdapters/ToolBridge Plumbing & UI MainThreadDispatcher Panels/AgentChatPanel
Library layout: a small public surface in front of façade + LLM + tools + MCP adapters + UI / plumbing.

Overview

Editor-side agent functionality lives in its own static library, LuckyEditor-Agent, at LuckyEditor-Agent/. New code uses namespace LuckyEditorAgent for the public surface; legacy types still being moved sit in Hazel::. The lib links into LuckyEditor today, but is deliberately shaped so the same code can later be lifted into a plugin (SharedLib) with no further re-architecture. The public header <LuckyEditorAgent/EditorAgent.h> is already plugin-shaped — a single class with an opaque pimpl.

Public surface

HeaderRole
<LuckyEditorAgent/EditorAgent.h> The editor instantiates one of these in Application::OnInit and drives the Construct → Attach → use → Detach lifecycle via Attach(host, config) / Detach(). Owns the MCP server, harness / tool bridges, and the AgentChatPanel registration via the host.
<LuckyEditorAgent/IEditorAgentHost.h> Abstract dependency surface implemented by EditorLayer today (alternative hosts on the roadmap: headless app, plugin host, test fixture). 14 virtuals for scene / viewport / panel queries, scene + project mutations (NewEmptyScene, OpenSceneAtPath, … renamed from EditorLayer defaults to avoid override-vs-default-arg ambiguity), and editor-wide actions (ReloadCSharp, SetAutoDismissWelcome, HarnessEnterPlayMode / ExitPlayMode).
EditorAgentConfig (in EditorAgent.h) Small POD: MCP TCP / HTTP ports and a welcome-dismiss flag.

Internal layout

Under LuckyEditor-Agent/src/LuckyEditorAgent/:

PathRole
EditorAgent.cppFaçade impl. Pimpl owns HarnessBridge, ToolBridge, and the Hazel::Bridge::McpServer instances; binds bridges to the host.
HazelAgentInterface.{h,cpp}Front-of-house tool dispatch: ~30 Get* / Set* / *Property entry points the LLM can call. Holds a single IEditorAgentHost* for editor access.
LLM/AgentClient.{h,cpp}Conversation orchestrator. Sibling files: StreamingHttpClient (libcurl SSE), ToolRegistry, LocalClaudeRunner, LocalClaudeExecutable (optional local-Claude subprocess backend), AgentEvent.h.
Get/, Set/, Set/PropertySetters/Actual tool implementations: entity CRUD, components, properties, prefabs, materials, screenshots.
Panels/AgentChatPanel.{h,cpp}EditorPanel subclass; the chat UI. Still in namespace Hazel (registration via EditorPanelIDs::Agent in EditorLayer.cpp).
McpAdapters/HarnessBridge.{h,cpp}Registers the harness/* surface (drive the in-editor agent end-to-end). Bound to IEditorAgentHost*.
McpAdapters/ToolBridge.{h,cpp}Registers tool/<name> (mirrors every ToolRegistry tool for direct testing).
MainThreadDispatcher.{h,cpp}Background-thread → main-thread marshaling, drained by EditorLayer::OnUpdate. Used by every MCP tool handler.
AgentHelpers.{h,cpp}UUID parsing / JSON formatting helpers.

Build-system shape

LuckyEditor-Agent/premake5.lua is kind "StaticLib". Includes Net/Hazel-Bridge/Source plus the standard Hazel / imgui / mid / simdjson / SimdjsonBridge / base64 vendor directories.

One transitional include

A transitional includedir on LuckyEditor/src remains, used in exactly one place: Viewport/Viewport.h, because the editor's Viewport class lives there and IEditorAgentHost::GetMainViewport() returns a Hazel::Ref<Hazel::Viewport> that agent-side tools dereference. Future cleanup either moves Viewport into Hazel core or hides the viewport-using ops behind the host interface.

Dependency direction

LuckyEditor ConsoleApp LuckyEditor-Agent StaticLib Hazel StaticLib Hazel-Bridge StaticLib LuckyEditor-Agent also registers tools directly on Hazel-Bridge's McpServer
Acyclic dependency direction. Engine code never includes editor code (send-pr §19 rule).

LuckyEditor-Agent depends on Hazel-Bridge and Hazel. Hazel-Bridge depends only on Hazel. The arrow from LuckyEditor-Agent back to Hazel-Bridge in the diagram is the registration edge: bridges plug into the live McpServer.

Resources

LuckyEditor/Resources/AgentDocs/ holds the knowledge-retrieval markdown corpus the agent loads at runtime. It is resolved via current_path() / "Resources" / "AgentDocs", so it sits on the editor binary side rather than inside the static lib — the lib provides loader logic, the editor binary owns its resources. Same split that should make the eventual plugin migration painless.

Pitfalls

Don't reach across the dependency arrow

Engine code (Hazel, Hazel-Bridge) must not include editor or agent headers. The single transitional include into LuckyEditor/src is a known wart; do not add more. Any new editor-only type that agent code needs should go behind IEditorAgentHost.

Tool handlers run off-main

Every MCP tool handler enters on a connection-worker thread (see § 2.17). Touching scene, NVRHI, or ImGui from there is a bug — go through MainThreadDispatcher, which EditorLayer::OnUpdate drains each tick.

Extending

  • New LLM-callable tool — add a handler under Get/ or Set/, register it on ToolRegistry; ToolBridge will automatically mirror it under tool/<name>.
  • New harness operation — register in HarnessBridge. Use MainThreadDispatcher::Execute for any engine touch.
  • New host (headless, plugin, test fixture) — implement IEditorAgentHost's 14 virtuals, construct an EditorAgent, call Attach(host, config). No further hooks needed.
  • Move the lib into a plugin — flip premake5.lua to SharedLib; the public surface is already designed to survive that.