@tool
extends RefCounted
## Agent loop: message → LLM → tool_calls → execute → repeat → final text.
##
## State machine:
##   IDLE → REQUESTING → [tool_calls?]
##     yes → EXECUTING_TOOLS → back to REQUESTING
##     no  → text response → IDLE
##
## All calls use await (triggered from UI signal handler, not _process).

const GolemTools = preload("golem_tools.gd")

signal tool_started(tool_name: String, args: Dictionary)
signal tool_finished(tool_name: String, result: String, is_image: bool)
signal response_text(text: String)
signal agent_error(message: String)
signal agent_done()

## Maximum tool-call rounds before forcing a text response.
const MAX_ROUNDS := 10

var _api_client: Node
var _server: Node  # GolemServer — call_handler(name, args)
var _conversation: RefCounted
var _running := false


func setup(api_client: Node, server: Node, conversation: RefCounted) -> void:
	_api_client = api_client
	_server = server
	_conversation = conversation


func is_running() -> bool:
	return _running


func run() -> void:
	## Start the agent loop. The conversation must already contain the user message.
	if _running:
		agent_error.emit("Agent is already running")
		return

	if _api_client == null or _server == null or _conversation == null:
		agent_error.emit("Agent not configured — call setup() first")
		return

	_running = true

	var tools: Array = GolemTools.get_tools_array()
	var rounds := 0

	while rounds < MAX_ROUNDS:
		rounds += 1

		# --- LLM request (non-streaming, with tools) ---
		var result: Dictionary = await _api_client.chat_completion_with_tools(
			_conversation.get_messages(), tools
		)

		if result.get("error", false):
			agent_error.emit(result.get("message", "Unknown API error"))
			_running = false
			agent_done.emit()
			return

		# --- Check for tool calls ---
		var tool_calls: Variant = result.get("tool_calls")
		if tool_calls is Array and tool_calls.size() > 0:
			# Store assistant message with tool_calls
			_conversation.add_assistant_tool_calls(tool_calls)

			# Execute each tool call
			for tc in tool_calls:
				if not tc is Dictionary:
					continue
				var tc_id: String = tc.get("id", "")
				var fn: Variant = tc.get("function", {})
				if not fn is Dictionary:
					continue
				var fn_name: String = fn.get("name", "")
				var fn_args_raw: Variant = fn.get("arguments", "{}")

				# Parse arguments JSON string
				var args: Dictionary = {}
				if fn_args_raw is String:
					var json := JSON.new()
					if json.parse(fn_args_raw) == OK and json.data is Dictionary:
						args = json.data
				elif fn_args_raw is Dictionary:
					args = fn_args_raw

				tool_started.emit(fn_name, args)

				# Execute via GolemServer handler
				var tool_result: Dictionary = await _server.call_handler(fn_name, args)

				var result_text := ""
				var is_image := false
				if tool_result.get("ok", false):
					var res_type: String = str(tool_result.get("type", "text"))
					if res_type == "image":
						# Store base64 as text in the conversation for now
						result_text = "[image: base64, %d bytes]" % str(tool_result.get("result", "")).length()
						is_image = true
					else:
						result_text = str(tool_result.get("result", ""))
				else:
					result_text = "Error: " + str(tool_result.get("error", "Unknown error"))

				_conversation.add_tool_result(tc_id, result_text)
				tool_finished.emit(fn_name, result_text, is_image)

			# Loop back for next LLM call with tool results
			continue

		# --- No tool calls → final text response ---
		var content: String = str(result.get("content", ""))
		if not content.is_empty() and content != "null":
			_conversation.add_assistant_message(content)
			response_text.emit(content)
		else:
			# Edge case: empty response with stop finish_reason
			_conversation.add_assistant_message("")
			response_text.emit("")

		break

	if rounds >= MAX_ROUNDS:
		agent_error.emit("Agent hit maximum tool rounds (%d). Stopping." % MAX_ROUNDS)

	_running = false
	agent_done.emit()
