@beremaran/godot-agent-loop 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +925 -0
  3. package/addons/godot_agent_loop/LICENSE +23 -0
  4. package/addons/godot_agent_loop/README.md +31 -0
  5. package/addons/godot_agent_loop/plugin.cfg +8 -0
  6. package/addons/godot_agent_loop/plugin.gd +417 -0
  7. package/agent-plugin/.claude-plugin/plugin.json +19 -0
  8. package/agent-plugin/.codex-plugin/plugin.json +37 -0
  9. package/agent-plugin/.mcp.json +14 -0
  10. package/agent-plugin/adapter-manifest.json +45 -0
  11. package/agent-plugin/pi/extension.ts +136 -0
  12. package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
  13. package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
  14. package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
  15. package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
  16. package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
  17. package/build/authoring-session-manager.js +237 -0
  18. package/build/domain-tool-registries.js +207 -0
  19. package/build/editor-bridge-protocol.js +1 -0
  20. package/build/editor-connection.js +112 -0
  21. package/build/editor-mutation-guard.js +30 -0
  22. package/build/editor-plugin-installer.js +220 -0
  23. package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
  24. package/build/game-command-service.js +49 -0
  25. package/build/game-connection.js +337 -0
  26. package/build/godot-executable.js +156 -0
  27. package/build/godot-process-manager.js +112 -0
  28. package/build/godot-subprocess.js +23 -0
  29. package/build/headless-operation-runner.js +68 -0
  30. package/build/headless-operation-service.js +65 -0
  31. package/build/index.js +478 -0
  32. package/build/interaction-server-installer.js +234 -0
  33. package/build/opencode-setup.js +199 -0
  34. package/build/project-support.js +219 -0
  35. package/build/runtime-protocol.js +202 -0
  36. package/build/scripts/godot_operations.gd +1534 -0
  37. package/build/scripts/mcp_editor_plugin.gd +417 -0
  38. package/build/scripts/mcp_interaction_server.gd +1255 -0
  39. package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
  40. package/build/scripts/mcp_runtime/command_params.gd +339 -0
  41. package/build/scripts/mcp_runtime/core_domain.gd +591 -0
  42. package/build/scripts/mcp_runtime/input_domain.gd +695 -0
  43. package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
  44. package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
  45. package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
  46. package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
  47. package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
  48. package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
  49. package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
  50. package/build/scripts/mcp_runtime/system_domain.gd +277 -0
  51. package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
  52. package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
  53. package/build/scripts/validate_script.gd +28 -0
  54. package/build/server-instructions.js +11 -0
  55. package/build/session-timing.js +20 -0
  56. package/build/tool-argument-validation.js +120 -0
  57. package/build/tool-definitions.js +2727 -0
  58. package/build/tool-handlers/game-tool-handlers.js +1345 -0
  59. package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
  60. package/build/tool-handlers/project-handler-services.js +1458 -0
  61. package/build/tool-handlers/project-tool-handlers.js +1506 -0
  62. package/build/tool-handlers/visual-regression-service.js +139 -0
  63. package/build/tool-manifest.js +1193 -0
  64. package/build/tool-mutation-policy.js +122 -0
  65. package/build/tool-registry.js +34 -0
  66. package/build/tool-surface.js +70 -0
  67. package/build/utils.js +273 -0
  68. package/package.json +110 -0
  69. package/product.json +52 -0
  70. package/scripts/prepare-package.js +42 -0
@@ -0,0 +1,356 @@
1
+ extends "res://mcp_runtime/runtime_domain.gd"
2
+
3
+ # Networking and multiplayer runtime commands. This domain owns the persistent
4
+ # WebSocket peer so its lifetime follows the command domain rather than the
5
+ # transport composition root.
6
+
7
+ var _websocket: WebSocketPeer = null
8
+ const MAX_WEBSOCKET_MESSAGE_BYTES := 1024 * 1024
9
+ const MAX_HTTP_BODY_BYTES := 1024 * 1024
10
+ const MAX_HTTP_RESPONSE_BYTES := 4 * 1024 * 1024
11
+ const MAX_HTTP_HEADER_BYTES := 32 * 1024
12
+ const MAX_HTTP_HEADERS := 64
13
+ const MAX_HTTP_URL_BYTES := 8192
14
+ const MAX_RPC_ARGS := 64
15
+
16
+ var _active_http: HTTPRequest = null
17
+ var _http_result: Array = []
18
+ var _rpc_call_local: Dictionary = {}
19
+ var _rpc_authority_only: Dictionary = {}
20
+
21
+
22
+ func register_commands() -> void:
23
+ register_command("http_request", _cmd_http_request)
24
+ register_command("websocket", _cmd_websocket)
25
+ register_command("multiplayer", _cmd_multiplayer)
26
+ register_command("rpc", _cmd_rpc)
27
+
28
+
29
+ func _cmd_http_request(params: Dictionary) -> void:
30
+ var reader := CommandParams.new(params)
31
+ var url: String = reader.required_string("url")
32
+ var method_str: String = reader.optional_enum("method", "GET", ["GET", "POST", "PUT", "DELETE"])
33
+ var timeout: float = reader.optional_number("timeout", 30.0, 0.01, 30.0)
34
+ var header_values: Dictionary = reader.optional_dictionary("headers")
35
+ var body: String = reader.optional_string("body")
36
+ if params_invalid(reader):
37
+ return
38
+ if url.is_empty():
39
+ reader.fail("url must be non-empty", {"param": "url", "reason": "invalid_value"})
40
+ send_params_error(reader)
41
+ return
42
+ if not (url.begins_with("http://") or url.begins_with("https://")):
43
+ reader.fail("url must use http:// or https://", {"param": "url", "reason": "invalid_value"})
44
+ if url.to_utf8_buffer().size() > MAX_HTTP_URL_BYTES:
45
+ reader.fail("HTTP URL exceeds the configured limit", {"param": "url", "reason": "limit_exceeded", "max_bytes": MAX_HTTP_URL_BYTES})
46
+ if body.to_utf8_buffer().size() > MAX_HTTP_BODY_BYTES:
47
+ reader.fail("HTTP body exceeds the configured limit", {"param": "body", "reason": "payload_too_large", "max_bytes": MAX_HTTP_BODY_BYTES})
48
+ if header_values.size() > MAX_HTTP_HEADERS:
49
+ reader.fail("HTTP headers exceed the configured count limit", {"param": "headers", "reason": "limit_exceeded", "max_items": MAX_HTTP_HEADERS})
50
+ var header_bytes: int = 0
51
+ for key: Variant in header_values:
52
+ if not key is String or not header_values[key] is String:
53
+ reader.fail("HTTP header names and values must be strings", {"param": "headers", "reason": "invalid_type"})
54
+ break
55
+ header_bytes += (str(key) + ": " + str(header_values[key])).to_utf8_buffer().size()
56
+ if header_bytes > MAX_HTTP_HEADER_BYTES:
57
+ reader.fail("HTTP headers exceed the configured byte limit", {"param": "headers", "reason": "limit_exceeded", "max_bytes": MAX_HTTP_HEADER_BYTES})
58
+ if params_invalid(reader):
59
+ return
60
+
61
+ _cleanup_http()
62
+ var http := HTTPRequest.new()
63
+ _active_http = http
64
+ _http_result = []
65
+ http.timeout = timeout
66
+ add_child(http)
67
+ @warning_ignore("return_value_discarded")
68
+ http.request_completed.connect(_capture_http_result, CONNECT_ONE_SHOT)
69
+ var headers := PackedStringArray()
70
+ for key: Variant in header_values:
71
+ @warning_ignore("return_value_discarded")
72
+ headers.append("%s: %s" % [key, str(header_values[key])])
73
+ var method_enum: int = {
74
+ "GET": HTTPClient.METHOD_GET,
75
+ "POST": HTTPClient.METHOD_POST,
76
+ "PUT": HTTPClient.METHOD_PUT,
77
+ "DELETE": HTTPClient.METHOD_DELETE,
78
+ }[method_str]
79
+ var err: int = http.request(url, headers, method_enum, body)
80
+ if err != OK:
81
+ _cleanup_http()
82
+ reader.fail("HTTP request failed to start", godot_error_data(err))
83
+ send_params_error(reader)
84
+ return
85
+ while _http_result.is_empty():
86
+ if cancellation_requested():
87
+ _cleanup_http()
88
+ respond({})
89
+ return
90
+ await get_tree().process_frame
91
+ var result: Array = _http_result.duplicate()
92
+ _cleanup_http()
93
+ var request_result: int = result[0]
94
+ var status_code: int = result[1]
95
+ var response_headers: PackedStringArray = result[2]
96
+ var body_bytes: PackedByteArray = result[3]
97
+ if request_result != HTTPRequest.RESULT_SUCCESS:
98
+ respond({"error": "HTTP request did not complete successfully", "error_data": {"reason": "http_request_failed", "result": request_result, "status_code": status_code}})
99
+ return
100
+ if body_bytes.size() > MAX_HTTP_RESPONSE_BYTES:
101
+ respond({"error": "HTTP response exceeds the configured payload limit", "error_data": {"reason": "payload_too_large", "bytes": body_bytes.size(), "max_bytes": MAX_HTTP_RESPONSE_BYTES, "status_code": status_code}})
102
+ return
103
+ respond({
104
+ "success": true,
105
+ "result": request_result,
106
+ "status_code": status_code,
107
+ "headers": Array(response_headers),
108
+ "body": body_bytes.get_string_from_utf8(),
109
+ "bytes": body_bytes.size(),
110
+ })
111
+
112
+
113
+ func _capture_http_result(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
114
+ _http_result = [result, response_code, headers, body]
115
+
116
+
117
+ func _cleanup_http() -> void:
118
+ if _active_http != null:
119
+ _active_http.cancel_request()
120
+ _active_http.queue_free()
121
+ _active_http = null
122
+ _http_result = []
123
+
124
+
125
+ func _cmd_websocket(params: Dictionary) -> void:
126
+ var reader := CommandParams.new(params)
127
+ var action: String = reader.required_enum("action", ["connect", "disconnect", "send", "receive", "status"])
128
+ var url: String = reader.optional_string("url")
129
+ var message: String = reader.optional_string("message")
130
+ var timeout: float = reader.optional_number("timeout", 5.0, 0.0, 10.0)
131
+ if params_invalid(reader):
132
+ return
133
+ if action == "connect" and url.is_empty():
134
+ reader.fail("url is required for connect", {"param": "url", "reason": "missing"})
135
+ send_params_error(reader)
136
+ return
137
+
138
+ match action:
139
+ "connect":
140
+ _close_websocket()
141
+ _websocket = WebSocketPeer.new()
142
+ var err: int = _websocket.connect_to_url(url)
143
+ if err != OK:
144
+ _websocket = null
145
+ reader.fail("WebSocket connection failed", godot_error_data(err))
146
+ send_params_error(reader)
147
+ return
148
+ var deadline: int = Time.get_ticks_msec() + ceili(timeout * 1000.0)
149
+ while _websocket != null and _websocket.get_ready_state() == WebSocketPeer.STATE_CONNECTING and Time.get_ticks_msec() <= deadline:
150
+ _websocket.poll()
151
+ await get_tree().process_frame
152
+ if _websocket == null or _websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
153
+ var state: String = _websocket_state_name(_websocket)
154
+ _close_websocket()
155
+ reader.fail("WebSocket connection did not open", {"param": "url", "reason": "connection_failed", "state": state})
156
+ send_params_error(reader)
157
+ return
158
+ respond({"success": true, "action": action, "url": url, "status": "open"})
159
+ "disconnect":
160
+ _close_websocket()
161
+ respond({"success": true, "action": action})
162
+ "send":
163
+ if not reader.has_param("message"):
164
+ reader.fail("message is required for send", {"param": "message", "reason": "missing"})
165
+ if message.to_utf8_buffer().size() > MAX_WEBSOCKET_MESSAGE_BYTES:
166
+ reader.fail("WebSocket message exceeds the payload limit", {"param": "message", "reason": "payload_too_large", "max_bytes": MAX_WEBSOCKET_MESSAGE_BYTES})
167
+ if _websocket == null or _websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
168
+ reader.fail("No open WebSocket connection", {"reason": "invalid_state", "state": _websocket_state_name(_websocket)})
169
+ if params_invalid(reader):
170
+ return
171
+ _websocket.poll()
172
+ var err: int = _websocket.send_text(message)
173
+ if err != OK:
174
+ reader.fail("WebSocket send failed", godot_error_data(err))
175
+ send_params_error(reader)
176
+ return
177
+ respond({"success": true, "action": action})
178
+ "receive":
179
+ if _websocket == null or _websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
180
+ reader.fail("No open WebSocket connection", {"reason": "invalid_state", "state": _websocket_state_name(_websocket)})
181
+ if params_invalid(reader):
182
+ return
183
+ var deadline: int = Time.get_ticks_msec() + ceili(timeout * 1000.0)
184
+ while _websocket.get_available_packet_count() == 0 and Time.get_ticks_msec() <= deadline:
185
+ _websocket.poll()
186
+ if _websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
187
+ reader.fail("WebSocket closed while waiting for a message", {"reason": "connection_closed", "state": _websocket_state_name(_websocket)})
188
+ break
189
+ await get_tree().process_frame
190
+ if reader.failed():
191
+ send_params_error(reader)
192
+ return
193
+ if _websocket.get_available_packet_count() == 0:
194
+ reader.fail("WebSocket receive timed out", {"reason": "timeout", "timeout": timeout})
195
+ send_params_error(reader)
196
+ return
197
+ var packet: PackedByteArray = _websocket.get_packet()
198
+ if packet.size() > MAX_WEBSOCKET_MESSAGE_BYTES:
199
+ reader.fail("Received WebSocket message exceeds the payload limit", {"reason": "payload_too_large", "max_bytes": MAX_WEBSOCKET_MESSAGE_BYTES, "bytes": packet.size()})
200
+ send_params_error(reader)
201
+ return
202
+ respond({"success": true, "action": action, "message": packet.get_string_from_utf8(), "bytes": packet.size()})
203
+ "status":
204
+ if _websocket != null:
205
+ _websocket.poll()
206
+ respond({"success": true, "status": _websocket_state_name(_websocket)})
207
+
208
+
209
+ func _cmd_multiplayer(params: Dictionary) -> void:
210
+ var reader := CommandParams.new(params)
211
+ var action: String = reader.required_enum("action", ["create_server", "create_client", "disconnect", "status"])
212
+ var port: int = reader.optional_int("port", 7000, 1, 65535)
213
+ var max_clients: int = reader.optional_int("max_clients", 32, 1)
214
+ var address: String = reader.optional_string("address", "127.0.0.1")
215
+ if params_invalid(reader):
216
+ return
217
+
218
+ match action:
219
+ "create_server":
220
+ _close_multiplayer()
221
+ var peer := ENetMultiplayerPeer.new()
222
+ var err: int = peer.create_server(port, max_clients)
223
+ if err != OK:
224
+ reader.fail("Failed to create multiplayer server", godot_error_data(err))
225
+ send_params_error(reader)
226
+ return
227
+ multiplayer.multiplayer_peer = peer
228
+ respond({"success": true, "action": action, "port": port})
229
+ "create_client":
230
+ _close_multiplayer()
231
+ var peer := ENetMultiplayerPeer.new()
232
+ var err: int = peer.create_client(address, port)
233
+ if err != OK:
234
+ reader.fail("Failed to create multiplayer client", godot_error_data(err))
235
+ send_params_error(reader)
236
+ return
237
+ multiplayer.multiplayer_peer = peer
238
+ respond({"success": true, "action": action, "address": address, "port": port})
239
+ "disconnect":
240
+ _close_multiplayer()
241
+ respond({"success": true, "action": action})
242
+ "status":
243
+ var peer: MultiplayerPeer = multiplayer.multiplayer_peer
244
+ if peer == null:
245
+ respond({"success": true, "connected": false, "status": "disconnected", "peer_count": 0})
246
+ return
247
+ var connection_status: MultiplayerPeer.ConnectionStatus = peer.get_connection_status()
248
+ var status_name: String = {MultiplayerPeer.CONNECTION_DISCONNECTED: "disconnected", MultiplayerPeer.CONNECTION_CONNECTING: "connecting", MultiplayerPeer.CONNECTION_CONNECTED: "connected"}.get(connection_status, "unknown")
249
+ respond({"success": true, "connected": connection_status == MultiplayerPeer.CONNECTION_CONNECTED, "status": status_name, "unique_id": multiplayer.get_unique_id(), "is_server": multiplayer.is_server(), "peer_count": multiplayer.get_peers().size()})
250
+
251
+
252
+ func _cmd_rpc(params: Dictionary) -> void:
253
+ var reader := CommandParams.new(params)
254
+ var node: Node = require_node(reader)
255
+ var action: String = reader.optional_enum("action", "call", ["call", "configure"])
256
+ var method: String = reader.required_string("method")
257
+ var args: Array = reader.optional_array("args")
258
+ var peer_id: int = reader.optional_int("peer_id", 0, 0)
259
+ if params_invalid(reader):
260
+ return
261
+ if method.is_empty():
262
+ reader.fail("method must be non-empty", {"param": "method", "reason": "invalid_value"})
263
+ send_params_error(reader)
264
+ return
265
+ if args.size() > MAX_RPC_ARGS:
266
+ reader.fail("RPC arguments exceed the configured limit", {"param": "args", "reason": "limit_exceeded", "max_items": MAX_RPC_ARGS})
267
+ if not node.has_method(method):
268
+ reader.fail("RPC method not found", {"param": "method", "reason": "method_not_found", "value": method})
269
+ if params_invalid(reader):
270
+ return
271
+
272
+ if action == "call":
273
+ var call_config_key: String = "%s:%s" % [node.get_instance_id(), method]
274
+ if _rpc_authority_only.get(call_config_key, false) and not node.is_multiplayer_authority():
275
+ reader.fail("Only the multiplayer authority may call this RPC method", {"param": "method", "reason": "not_multiplayer_authority", "value": method})
276
+ send_params_error(reader)
277
+ return
278
+ if _rpc_call_local.get(call_config_key, false):
279
+ Callable(node, method).callv(args)
280
+ var call_args: Array = [method]
281
+ call_args.append_array(args)
282
+ var call_result: Variant
283
+ if peer_id == 0:
284
+ call_result = Callable(node, "rpc").callv(call_args)
285
+ else:
286
+ call_args.push_front(peer_id)
287
+ call_result = Callable(node, "rpc_id").callv(call_args)
288
+ var rpc_error: int = CommandParams.to_int(call_result, FAILED)
289
+ if rpc_error != OK:
290
+ reader.fail("RPC call failed", godot_error_data(rpc_error))
291
+ send_params_error(reader)
292
+ return
293
+ respond({"success": true, "action": action, "method": method, "peer_id": peer_id, "argument_count": args.size()})
294
+ return
295
+
296
+ var config: Dictionary = {}
297
+ if reader.has_param("mode"):
298
+ var mode: Variant = reader.raw("mode")
299
+ if mode is String:
300
+ var mode_text: String = mode
301
+ var mode_name: String = mode_text.to_lower()
302
+ if not ["any_peer", "authority"].has(mode_name):
303
+ reader.fail("mode must be one of: any_peer, authority", {"param": "mode", "reason": "invalid_value", "allowed": ["any_peer", "authority"], "value": mode})
304
+ else:
305
+ config["rpc_mode"] = MultiplayerAPI.RPC_MODE_ANY_PEER if mode_name == "any_peer" else MultiplayerAPI.RPC_MODE_AUTHORITY
306
+ elif mode is int or mode is float:
307
+ config["rpc_mode"] = CommandParams.to_int(mode)
308
+ else:
309
+ reader.fail("mode must be a string or integer", {"param": "mode", "reason": "invalid_type"})
310
+ if reader.has_param("sync"):
311
+ var sync: String = reader.optional_enum("sync", "call_remote", ["call_local", "call_remote"])
312
+ config["call_local"] = sync == "call_local"
313
+ if reader.has_param("transfer_mode"):
314
+ var transfer_mode: String = reader.optional_enum("transfer_mode", "unreliable", ["unreliable", "unreliable_ordered", "reliable"])
315
+ config["transfer_mode"] = {
316
+ "unreliable": MultiplayerPeer.TRANSFER_MODE_UNRELIABLE,
317
+ "unreliable_ordered": MultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED,
318
+ "reliable": MultiplayerPeer.TRANSFER_MODE_RELIABLE,
319
+ }[transfer_mode]
320
+ if reader.has_param("channel"):
321
+ config["channel"] = reader.optional_int("channel", 0, 0)
322
+ if params_invalid(reader):
323
+ return
324
+ var requested_config: Dictionary = config.duplicate()
325
+ var config_key: String = "%s:%s" % [node.get_instance_id(), method]
326
+ _rpc_call_local[config_key] = config.get("call_local", false)
327
+ _rpc_authority_only[config_key] = config.get("rpc_mode", MultiplayerAPI.RPC_MODE_AUTHORITY) == MultiplayerAPI.RPC_MODE_AUTHORITY
328
+ # Invoke locally ourselves so broadcast and rpc_id have identical semantics.
329
+ config["call_local"] = false
330
+ node.rpc_config(method, config)
331
+ respond({"success": true, "action": action, "method": method, "config": requested_config})
332
+
333
+
334
+ func _close_websocket() -> void:
335
+ if _websocket != null:
336
+ _websocket.close()
337
+ _websocket = null
338
+
339
+
340
+ func _websocket_state_name(peer: WebSocketPeer) -> String:
341
+ if peer == null:
342
+ return "disconnected"
343
+ return {WebSocketPeer.STATE_CONNECTING: "connecting", WebSocketPeer.STATE_OPEN: "open", WebSocketPeer.STATE_CLOSING: "closing", WebSocketPeer.STATE_CLOSED: "closed"}.get(peer.get_ready_state(), "unknown")
344
+
345
+
346
+ func _close_multiplayer() -> void:
347
+ var peer: MultiplayerPeer = multiplayer.multiplayer_peer
348
+ if peer != null:
349
+ peer.close()
350
+ multiplayer.multiplayer_peer = null
351
+
352
+
353
+ func _exit_tree() -> void:
354
+ _cleanup_http()
355
+ _close_websocket()
356
+ _close_multiplayer()