Skip to main content

ITool architecture

Every capability in Buril is an ITool. A tool is a C# class that:

  1. Implements the ITool interface.
  2. Has a unique, lowercase, underscore-separated name (e.g., scene_open, gameobject_create, vroid_export_vrm).
  3. Takes a JObject input and returns an object result.
  4. Lives under Editor/Tools/<family>/ so the registry can find it.

That's the entire contract. Everything else — multi-agent orchestration, DCC bridges, AI pipelines — composes from this single interface.

The interface

public interface ITool
{
string Name { get; }
string Description { get; }
JSchema InputSchema { get; }
object Run(JObject input);
}
MemberPurpose
NameStable identifier the LLM uses to call the tool. Never rename a shipped tool without a migration shim — agents have its name in their planning context.
DescriptionOne-paragraph English description the LLM sees when planning. This is the most under-rated field — clarity here directly improves tool selection.
InputSchemaJSON Schema (via JSchema) describing the expected input shape. The dispatcher validates against this before invoking Run.
RunSynchronous. Long-running work delegates to a coroutine or task and returns a job handle the agent can poll. Editor-thread restrictions apply — see "Threading" below.

Naming convention

Tools follow <family>_<verb>_<noun>:

FamilyExamples
editor_editor_state, editor_window_open, editor_play_mode_toggle
scene_scene_open, scene_save, scene_list_gameobjects
gameobject_gameobject_create, gameobject_set_position, gameobject_destroy
asset_asset_import_fbx, asset_create_material, asset_move
script_script_create, script_compile_status, script_find_references
vroid_vroid_export_vrm, vroid_apply_blendshape
cascadeur_cascadeur_keyframe_run, cascadeur_export_alembic
agent_team_agent_team_handoff, agent_team_query
mcp_mcp_register_external, mcp_list_servers

Family prefixes are not enforced by code, but are enforced by review. Inconsistent naming hurts the LLM's planning hit-rate.

Threading

Most Unity APIs (AssetDatabase, scene mutations, EditorApplication) require the main editor thread. Buril's dispatcher pins ITool execution to the main thread by default — your Run method runs synchronously inside an EditorApplication.update tick.

For tools that genuinely need to run off-thread (HTTP calls, large file I/O), use the [OffThread] attribute:

[OffThread]
public class MyHttpTool : ITool
{
public object Run(JObject input)
{
// Safe to do blocking I/O here.
// Do NOT touch AssetDatabase or scene state directly.
}
}

If an off-thread tool needs to apply changes to Unity, marshal back via EditorApplication.delayCall or use the MainThreadDispatcher helper.

Error handling

Tools have three legal outcomes:

OutcomeHow
SuccessReturn any JSON-serializable object. Wrap user-meaningful data in { "ok": true, "data": ... }.
Validation failureThrow ToolValidationException("reason"). The dispatcher catches, formats the message, and returns it to the LLM. The agent can retry with adjusted input.
Runtime failureThrow any other exception. The dispatcher logs it, returns { "ok": false, "error": "...", "type": "..." }. The agent can decide whether to retry, ask the user, or abandon the plan.

Never return null to signal failure — null tools confuse the LLM into thinking the call succeeded with empty data.

Writing a new tool

Minimal example:

using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using Buril.Tools;

namespace Buril.Tools.MyFamily
{
public class MyFamilyGreetTool : ITool
{
public string Name => "myfamily_greet";

public string Description =>
"Returns a friendly greeting for the named user. " +
"Use when the user explicitly asks to be greeted.";

public JSchema InputSchema => JSchema.Parse(@"{
'type': 'object',
'properties': {
'name': { 'type': 'string', 'minLength': 1 }
},
'required': ['name']
}");

public object Run(JObject input)
{
var name = input.Value<string>("name");
return new { ok = true, greeting = $"Hello, {name}!" };
}
}
}

Drop this into Editor/Tools/MyFamily/MyFamilyGreetTool.cs. On the next compile, the registry auto-discovers it. No manifest, no registration call.

What an ITool is not

  • It is not a runtime feature. Tools live in Editor/, compile under the UNITY_EDITOR define, and never ship in builds.
  • It is not a state container. Tools should be stateless — anything persistent goes in EditorPrefs, Library/BurilCache/, or a project-tracked asset.
  • It is not a UI element. Tools are headless. If you need UI, build an EditorWindow that calls the tool — keep them separated.