"""Game tools — entity CRUD, environment manipulation, RAG, triggers, calc, etc.""" from __future__ import annotations import random import re import uuid from typing import Any from sqlalchemy import select from app.core.logging import get_logger from app.core.state_validator import apply_patch, validate_state from app.engine.tools.base import Tool, ToolContext, ToolResult, get_entity_by_query from app.models import Entity _logger = get_logger(__name__) # --------------------------------------------------------------------------- # # entity_create # --------------------------------------------------------------------------- # class EntityCreateTool(Tool): name = "entity_create" category = "game" stages = {"world_builder", "world_builder_schema", "world_builder_env", "world_builder_entities", "world_editor", "orchestrator_phase1", "subagent", "intro_scene"} description = ( "Create a new entity in the current world. The entity_type must exist in " "world.schemas. The data must conform to the schema for that type." ) parameters_schema = { "type": "object", "required": ["entity_type", "name", "data"], "properties": { "entity_type": { "type": "string", "description": "Type from world.schemas (character, item, location, ...)", }, "name": { "type": "string", "description": "Entity name (unique within (world_id, entity_type))", }, "data": { "type": "object", "description": "Full entity data per schema", }, "add_to_environment": { "type": "boolean", "default": False, "description": "Add to environment for fast LLM access", }, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: et = arguments.get("entity_type") nm = arguments.get("name") data = arguments.get("data") or {} add_env = arguments.get("add_to_environment", False) if not et or not nm: return ToolResult( ok=False, error_code="validation_error", error_message="entity_type and name are required", ) # Validate type exists in schemas schemas = ctx.world.schemas or [] type_names = {s.get("type") for s in schemas} if et not in type_names: return ToolResult( ok=False, error_code="unknown_entity_type", error_message=f"Entity type {et!r} not in world.schemas", ) # Name uniqueness within (world, type) existing = ( await ctx.db.execute( select(Entity).where( Entity.world_id == ctx.world.id, Entity.entity_type == et, Entity.name == nm, Entity.deleted_at.is_(None), ) ) ).scalar_one_or_none() if existing is not None: return ToolResult( ok=False, error_code="name_conflict", error_message=f"Entity {et}/{nm!r} already exists", ) entity = Entity( world_id=ctx.world.id, entity_type=et, name=nm, data=data, is_in_environment=add_env, embedding_status="pending", ) ctx.db.add(entity) await ctx.db.flush() if add_env: env = dict(ctx.world.environment or {}) env.setdefault("entities", []).append( {"id": str(entity.id), "entity_type": et, "name": nm} ) ctx.world.environment = env return ToolResult( ok=True, data={"entity_id": str(entity.id)}, message=f"Created {et} {nm!r}", ) # --------------------------------------------------------------------------- # # entity_get # --------------------------------------------------------------------------- # class EntityGetTool(Tool): name = "entity_get" category = "game" stages = { "world_builder", "world_editor", "orchestrator_phase1", "subagent", "intro_scene", } description = "Get an entity by id or by {entity_type, name}." parameters_schema = { "type": "object", "required": ["query"], "properties": { "query": { "oneOf": [ {"type": "string", "description": "entity_id (UUID)"}, { "type": "object", "properties": { "entity_type": {"type": "string"}, "name": {"type": "string"}, }, "required": ["entity_type", "name"], }, ] } }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: q = arguments.get("query") ent = await get_entity_by_query(ctx.db, ctx.world.id, q) if ent is None or ent.deleted_at is not None: return ToolResult( ok=False, error_code="not_found", error_message="Entity not found", ) return ToolResult( ok=True, data={ "id": str(ent.id), "entity_type": ent.entity_type, "name": ent.name, "data": ent.data, "is_in_environment": ent.is_in_environment, }, message=f"Got {ent.entity_type} {ent.name!r}", ) # --------------------------------------------------------------------------- # # entity_list # --------------------------------------------------------------------------- # class EntityListTool(Tool): name = "entity_list" category = "game" stages = { "world_builder", "world_editor", "orchestrator_phase1", "subagent", "intro_scene", } description = "List entities in the world, optionally filtered." parameters_schema = { "type": "object", "properties": { "entity_type": {"type": "string"}, "in_environment_only": {"type": "boolean", "default": False}, "name_contains": {"type": "string"}, "limit": {"type": "integer", "default": 50, "max": 200}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: stmt = select(Entity).where( Entity.world_id == ctx.world.id, Entity.deleted_at.is_(None) ) et = arguments.get("entity_type") if et: stmt = stmt.where(Entity.entity_type == et) if arguments.get("in_environment_only"): stmt = stmt.where(Entity.is_in_environment.is_(True)) nc = arguments.get("name_contains") if nc: stmt = stmt.where(Entity.name.ilike(f"%{nc}%")) limit = min(arguments.get("limit", 50), 200) stmt = stmt.limit(limit) rows = (await ctx.db.execute(stmt)).scalars().all() return ToolResult( ok=True, data={ "items": [ { "id": str(r.id), "entity_type": r.entity_type, "name": r.name, "data": r.data, "is_in_environment": r.is_in_environment, } for r in rows ], "count": len(rows), }, message=f"Listed {len(rows)} entities", ) # --------------------------------------------------------------------------- # # entity_update # --------------------------------------------------------------------------- # class EntityUpdateTool(Tool): name = "entity_update" category = "game" stages = {"world_editor", "orchestrator_phase1", "subagent"} description = "Update entity fields via JSON-patch." parameters_schema = { "type": "object", "required": ["entity_id", "patch"], "properties": { "entity_id": {"type": "string"}, "patch": { "type": "object", "description": "JSON-patch: {field_path: new_value | {op, by}}", }, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: eid = arguments.get("entity_id") patch = arguments.get("patch") or {} try: ent_uuid = uuid.UUID(eid) except (ValueError, TypeError): return ToolResult( ok=False, error_code="validation_error", error_message="Invalid entity_id" ) ent = ( await ctx.db.execute( select(Entity).where( Entity.id == ent_uuid, Entity.world_id == ctx.world.id, Entity.deleted_at.is_(None), ) ) ).scalar_one_or_none() if ent is None: return ToolResult( ok=False, error_code="not_found", error_message="Entity not found" ) new_data, errors = apply_patch(ent.data or {}, patch) if errors: return ToolResult( ok=False, error_code="validation_error", error_message="; ".join(errors), ) ent.data = new_data await ctx.db.flush() return ToolResult( ok=True, data={"applied_paths": list(patch.keys())}, message=f"Updated {ent.entity_type} {ent.name!r}", ) # --------------------------------------------------------------------------- # # entity_delete # --------------------------------------------------------------------------- # class EntityDeleteTool(Tool): name = "entity_delete" category = "game" stages = {"world_editor", "orchestrator_phase1", "subagent"} description = "Soft-delete an entity." parameters_schema = { "type": "object", "required": ["entity_id"], "properties": { "entity_id": {"type": "string"}, "reason": {"type": "string"}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: eid = arguments.get("entity_id") try: ent_uuid = uuid.UUID(eid) except (ValueError, TypeError): return ToolResult( ok=False, error_code="validation_error", error_message="Invalid entity_id" ) ent = ( await ctx.db.execute( select(Entity).where( Entity.id == ent_uuid, Entity.world_id == ctx.world.id ) ) ).scalar_one_or_none() if ent is None: return ToolResult( ok=False, error_code="not_found", error_message="Entity not found" ) from datetime import datetime, timezone ent.deleted_at = datetime.now(timezone.utc) await ctx.db.flush() return ToolResult( ok=True, data={"entity_id": str(ent.id)}, message=f"Soft-deleted {ent.entity_type} {ent.name!r}", ) # --------------------------------------------------------------------------- # # env_update / env_get # --------------------------------------------------------------------------- # class EnvUpdateTool(Tool): name = "env_update" category = "game" stages = { "world_builder", "world_editor", "orchestrator_phase1", "subagent", "intro_scene", } description = ( "Apply a JSON-patch to environment. Patch is validated by state_validator." ) parameters_schema = { "type": "object", "required": ["patch"], "properties": { "patch": { "type": "object", "description": "Map field_path -> new_value | {op, by/value}. Ops: set, inc, dec, append, remove.", } }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: patch = arguments.get("patch") or {} new_env, errors = apply_patch(dict(ctx.world.environment or {}), patch) if errors: return ToolResult( ok=False, error_code="validation_error", error_message="; ".join(errors), ) # Validate against environment_schema ok, verrors = validate_state(new_env, ctx.world.environment_schema or []) if not ok: return ToolResult( ok=False, error_code="schema_violation", error_message="; ".join(verrors), ) ctx.world.environment = new_env await ctx.db.flush() return ToolResult( ok=True, data={"applied_paths": list(patch.keys())}, message="Environment updated", ) class EnvGetTool(Tool): name = "env_get" category = "game" stages = { "world_builder", "world_editor", "orchestrator_phase1", "subagent", "intro_scene", } description = "Get current environment value (or sub-path)." parameters_schema = { "type": "object", "properties": { "path": { "type": "string", "description": "e.g. 'player.stats' or 'plot_rails.current_goals'", } }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: path = arguments.get("path") env = ctx.world.environment or {} if not path: return ToolResult(ok=True, data=env, message="Full environment") # Walk path cur: Any = env for part in path.split("."): if isinstance(cur, dict) and part in cur: cur = cur[part] else: return ToolResult( ok=False, error_code="not_found", error_message=f"Path {path!r} not found in environment", ) return ToolResult(ok=True, data={"value": cur}, message=f"Value at {path!r}") # --------------------------------------------------------------------------- # # update_plot_rails # --------------------------------------------------------------------------- # class UpdatePlotRailsTool(Tool): name = "update_plot_rails" category = "game" stages = { "world_builder", "world_editor", "orchestrator_phase1", "intro_scene", } description = "Add/remove hooks and goals in plot_rails." parameters_schema = { "type": "object", "required": ["operation"], "properties": { "operation": { "type": "string", "enum": ["add_hook", "remove_hook", "add_goal", "remove_goal", "complete_goal"], }, "value": {"type": "string"}, "index": {"type": "integer"}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: op = arguments.get("operation") val = arguments.get("value") idx = arguments.get("index") pr = dict(ctx.world.plot_rails or {}) pr.setdefault("hooks", []) pr.setdefault("current_goals", []) pr.setdefault("completed_goals", []) if op == "add_hook": if not val: return ToolResult(ok=False, error_code="validation_error", error_message="value required for add_hook") pr["hooks"] = list(pr["hooks"]) + [val] elif op == "remove_hook": if idx is None or idx >= len(pr["hooks"]): return ToolResult(ok=False, error_code="validation_error", error_message="invalid index") pr["hooks"] = [h for i, h in enumerate(pr["hooks"]) if i != idx] elif op == "add_goal": if not val: return ToolResult(ok=False, error_code="validation_error", error_message="value required for add_goal") pr["current_goals"] = list(pr["current_goals"]) + [val] elif op == "remove_goal": if idx is None or idx >= len(pr["current_goals"]): return ToolResult(ok=False, error_code="validation_error", error_message="invalid index") pr["current_goals"] = [g for i, g in enumerate(pr["current_goals"]) if i != idx] elif op == "complete_goal": if idx is None or idx >= len(pr["current_goals"]): return ToolResult(ok=False, error_code="validation_error", error_message="invalid index") goal = pr["current_goals"][idx] pr["current_goals"] = [g for i, g in enumerate(pr["current_goals"]) if i != idx] pr["completed_goals"] = list(pr["completed_goals"]) + [goal] else: return ToolResult( ok=False, error_code="validation_error", error_message=f"Unknown operation {op!r}", ) ctx.world.plot_rails = pr await ctx.db.flush() return ToolResult(ok=True, data=pr, message=f"plot_rails.{op} applied") # --------------------------------------------------------------------------- # # advance_time # --------------------------------------------------------------------------- # class AdvanceTimeTool(Tool): name = "advance_time" category = "game" stages = {"orchestrator_phase1", "subagent"} description = "Advance world time by a delta." parameters_schema = { "type": "object", "required": ["delta"], "properties": { "delta": { "type": "string", "description": "Format: [year_Y][days_D][hours_H][min_M]", } }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: from app.core.time_utils import advance_time delta = arguments.get("delta") try: new_time = advance_time(ctx.world.current_time, delta, ctx.world.time_schema) except ValueError as e: return ToolResult(ok=False, error_code="validation_error", error_message=str(e)) ctx.world.current_time = new_time await ctx.db.flush() return ToolResult( ok=True, data={"new_time": new_time}, message=f"Time advanced by {delta} to {new_time}", ) # --------------------------------------------------------------------------- # # schedule_trigger # --------------------------------------------------------------------------- # class ScheduleTriggerTool(Tool): name = "schedule_trigger" category = "game" stages = {"orchestrator_phase1", "subagent"} description = "Schedule a deferred trigger to fire at a specific game time." parameters_schema = { "type": "object", "required": ["fire_at", "event_type", "payload"], "properties": { "fire_at": {"type": "string", "description": "Format: [year_Y_]day_D_hour_H[_min_M]"}, "event_type": { "type": "string", "enum": ["spawn_enemy", "weather_change", "quest_update", "npc_action", "custom"], }, "payload": {"type": "object"}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: from app.models import DeferredTrigger fa = arguments.get("fire_at") et = arguments.get("event_type") pl = arguments.get("payload") or {} if not fa or not et: return ToolResult( ok=False, error_code="validation_error", error_message="fire_at and event_type required", ) trig = DeferredTrigger( world_id=ctx.world.id, fire_at=fa, event_type=et, payload=pl ) ctx.db.add(trig) await ctx.db.flush() return ToolResult( ok=True, data={"trigger_id": str(trig.id)}, message=f"Scheduled {et} at {fa}", ) # --------------------------------------------------------------------------- # # calc # --------------------------------------------------------------------------- # _DICE_RE = re.compile(r"(\d*)d(\d+)") _SAFE_RE = re.compile(r"^[0-9+\-*/%().,\s\wd]+$") class CalcTool(Tool): name = "calc" category = "game" stages = {"orchestrator_phase1", "subagent"} description = ( "Evaluate a math expression with dice support. Allowed: + - * / %, " "min(), max(), round(), and dice notation like 2d6+3." ) parameters_schema = { "type": "object", "required": ["expression"], "properties": { "expression": {"type": "string", "example": "max(1, 2d6+3 - enemy.armor)"}, "variables": { "type": "object", "description": "Variable substitutions, e.g. {\"enemy.armor\": 5}", }, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: expr = arguments.get("expression", "") variables = arguments.get("variables") or {} # Substitute variables trace_parts: list[str] = [] for var, val in variables.items(): expr = expr.replace(var, str(val)) trace_parts.append(f"{var}={val}") # Dice rolls rolls: list[int] = [] def _roll(match: re.Match) -> str: count = int(match.group(1) or "1") sides = int(match.group(2)) if sides < 1 or count < 1 or count > 100: return "0" results = [random.randint(1, sides) for _ in range(count)] rolls.extend(results) return str(sum(results)) expr_with_rolls = _DICE_RE.sub(_roll, expr) if not _SAFE_RE.match(expr_with_rolls): return ToolResult( ok=False, error_code="validation_error", error_message="Expression contains disallowed characters", ) # Replace min/max/round with safe builtins try: result = eval( # noqa: S307 expr_with_rolls, {"__builtins__": {}}, {"min": min, "max": max, "round": round, "abs": abs}, ) if isinstance(result, float) and result.is_integer(): result = int(result) except Exception as e: return ToolResult( ok=False, error_code="evaluation_error", error_message=str(e) ) return ToolResult( ok=True, data={"result": result, "rolls": rolls, "trace": "; ".join(trace_parts)}, message=f"= {result}", ) # --------------------------------------------------------------------------- # # random_choice # --------------------------------------------------------------------------- # class RandomChoiceTool(Tool): name = "random_choice" category = "game" stages = {"orchestrator_phase1", "subagent"} description = "Pick an option deterministically (seeded by world+step)." parameters_schema = { "type": "object", "required": ["options"], "properties": { "options": {"type": "array", "items": {}, "minItems": 2}, "weights": {"type": "array", "items": {"type": "number"}}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: opts = arguments.get("options") or [] weights = arguments.get("weights") if len(opts) < 2: return ToolResult( ok=False, error_code="validation_error", error_message="Need at least 2 options", ) seed_str = f"{ctx.world.id}:{ctx.step_id or 'noid'}" rng = random.Random(hash(seed_str)) if weights: if len(weights) != len(opts): return ToolResult( ok=False, error_code="validation_error", error_message="options and weights length mismatch", ) pick = rng.choices(opts, weights=weights, k=1)[0] else: pick = rng.choice(opts) return ToolResult(ok=True, data={"choice": pick}, message=f"Picked: {pick!r}") # --------------------------------------------------------------------------- # # rag_query / rag_add (deferred to app.core.rag) # --------------------------------------------------------------------------- # class RagQueryTool(Tool): name = "rag_query" category = "game" stages = { "world_builder", "world_editor", "orchestrator_phase1", "subagent", "intro_scene", } description = ( "Semantic search over entities and story entries. Use when you need to recall " "past details, NPC names, world facts. Do NOT rely on memory." ) parameters_schema = { "type": "object", "required": ["query"], "properties": { "query": {"type": "string"}, "limit": {"type": "integer", "default": 5, "max": 20}, "filter_type": { "type": "string", "enum": ["all", "entities", "story_entries"], "default": "all", }, "min_score": {"type": "number", "default": 0.7}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: from app.core.rag import rag_query try: results = await rag_query( db=ctx.db, world_id=ctx.world.id, query=arguments.get("query", ""), limit=arguments.get("limit", 5), filter_type=arguments.get("filter_type", "all"), min_score=arguments.get("min_score", 0.0), ) except Exception as e: # noqa: BLE001 _logger.warning("rag_query_tool_failed", error=str(e)) return ToolResult( ok=True, data={"results": []}, message="RAG unavailable, returning empty results", ) return ToolResult( ok=True, data={"results": results}, message=f"Found {len(results)} matches", ) class RagAddTool(Tool): name = "rag_add" category = "game" stages = { "world_builder", "orchestrator_phase1", "subagent", "intro_scene", } description = ( "Persist a fact/event as a story entry and index it for semantic search. " "Use when the player learns a persistent fact (NPC secret, lore, quest outcome)." ) parameters_schema = { "type": "object", "required": ["content", "entry_type"], "properties": { "content": {"type": "string"}, "entry_type": { "type": "string", "enum": ["fact", "event", "relationship", "secret"], }, "metadata": {"type": "object"}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: from app.core.rag import rag_add entry = await rag_add( db=ctx.db, world_id=ctx.world.id, content=arguments.get("content", ""), entry_type=arguments.get("entry_type", "fact"), metadata=arguments.get("metadata"), step_id=ctx.step_id, ) return ToolResult( ok=True, data={"id": str(entry.id), "status": entry.embedding_status}, message=f"Added story entry ({entry.entry_type})", ) # --------------------------------------------------------------------------- # # submit_plan / submit_step / suggest_actions — terminal tools # --------------------------------------------------------------------------- # class SubmitPlanTool(Tool): name = "submit_plan" category = "game" stages = {"world_builder", "world_builder_schema", "world_builder_env", "world_builder_entities", "orchestrator_phase1"} description = "End Phase 1. Pass the plan + summary to Phase 2 writer." parameters_schema = { "type": "object", "required": ["plan", "summary"], "properties": { "plan": {"type": "string"}, "summary": {"type": "array", "items": {"type": "object"}}, "offscreen_events": {"type": "array", "items": {"type": "string"}}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: # Signal handled by orchestrator loop — just echo back return ToolResult( ok=True, data={ "plan": arguments.get("plan", ""), "summary": arguments.get("summary", []), "offscreen_events": arguments.get("offscreen_events", []), }, message="Phase 1 complete", ) class SubmitStepTool(Tool): name = "submit_step" category = "game" stages = {"orchestrator_phase2", "intro_scene"} description = "End Phase 2. Writer returns the final narrative + time delta." parameters_schema = { "type": "object", "required": ["scene_text", "delta_time"], "properties": { "scene_text": {"type": "string", "minLength": 100, "maxLength": 4000}, "delta_time": {"type": "string"}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: text = arguments.get("scene_text", "") if len(text) < 100: return ToolResult( ok=False, error_code="validation_error", error_message=f"scene_text must be >= 100 chars (got {len(text)})", ) return ToolResult( ok=True, data={"scene_text": text, "delta_time": arguments.get("delta_time", "hours_1")}, message="Phase 2 complete", ) class SuggestActionsTool(Tool): name = "suggest_actions" category = "game" stages = {"orchestrator_phase3_suggest", "intro_scene"} description = "Generate 1-3 next actions for the player." parameters_schema = { "type": "object", "required": ["actions"], "properties": { "actions": {"type": "array", "items": {"type": "string"}, "minItems": 1, "maxItems": 3} }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: actions = arguments.get("actions") or [] if not actions or len(actions) > 3: return ToolResult( ok=False, error_code="validation_error", error_message="Need 1-3 actions", ) return ToolResult(ok=True, data={"actions": actions}, message="Suggestions ready") # --------------------------------------------------------------------------- # # Interaction tools (used in world_builder / world_editor) # --------------------------------------------------------------------------- # class AskUserTool(Tool): name = "ask_user" category = "interaction" stages = {"world_builder", "world_builder_schema", "world_builder_env", "world_builder_entities", "world_editor"} description = "Ask the player a clarification question. Blocks until answer." parameters_schema = { "type": "object", "required": ["question"], "properties": { "question": {"type": "string"}, "options": {"type": "array", "items": {"type": "string"}}, "allow_free_text": {"type": "boolean", "default": True}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: # The orchestrator/world_builder loop must intercept this tool before execution # and emit a clarification SSE event; here we just echo back the question. return ToolResult( ok=True, data={ "question": arguments.get("question"), "options": arguments.get("options"), "allow_free_text": arguments.get("allow_free_text", True), "_blocking": True, }, message="Awaiting user answer", ) class ProposeChangesTool(Tool): name = "propose_changes" category = "interaction" stages = {"world_editor"} description = "Propose a diff to the player for accept/reject." parameters_schema = { "type": "object", "required": ["diff"], "properties": { "diff": { "type": "array", "items": { "type": "object", "properties": { "path": {"type": "string"}, "op": {"type": "string", "enum": ["add", "remove", "replace"]}, "old": {}, "new": {}, }, }, }, "comment": {"type": "string"}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: return ToolResult( ok=True, data={"diff": arguments.get("diff", []), "comment": arguments.get("comment", "")}, message="Proposed changes", ) class CommentToUserTool(Tool): name = "comment_to_user" category = "interaction" stages = {"world_builder", "world_builder_schema", "world_builder_env", "world_builder_entities", "world_editor"} description = "Send a text comment to the user (no answer expected)." parameters_schema = { "type": "object", "required": ["text"], "properties": {"text": {"type": "string"}}, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: return ToolResult(ok=True, data={"text": arguments.get("text", "")}, message="Comment sent") # --------------------------------------------------------------------------- # # run_subagent # --------------------------------------------------------------------------- # class RunSubagentTool(Tool): name = "run_subagent" category = "game" stages = {"orchestrator_phase1"} description = "Run an offscreen sub-LLM call for background events." parameters_schema = { "type": "object", "required": ["task", "tools"], "properties": { "task": {"type": "string"}, "tools": {"type": "array", "items": {"type": "string"}}, "context": {"type": "object"}, "max_iterations": {"type": "integer", "default": 5, "max": 10}, }, } async def execute(self, arguments: dict[str, Any], ctx: ToolContext) -> ToolResult: # Full implementation in app.engine.subagent return ToolResult( ok=True, data={ "task": arguments.get("task"), "tools": arguments.get("tools"), "context": arguments.get("context"), "max_iterations": arguments.get("max_iterations", 5), "_deferred": True, }, message="Subagent requested (executor handles)", )