2026-06-20 19:13:05 +03:00
|
|
|
"""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"
|
2026-06-21 06:12:28 +03:00
|
|
|
stages = {"world_builder", "world_builder_schema", "world_builder_env", "world_builder_entities", "world_editor", "orchestrator_phase1", "subagent", "intro_scene"}
|
2026-06-20 19:13:05 +03:00
|
|
|
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"
|
2026-06-21 06:12:28 +03:00
|
|
|
stages = {"world_builder", "world_builder_schema", "world_builder_env", "world_builder_entities", "orchestrator_phase1"}
|
2026-06-20 19:13:05 +03:00
|
|
|
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"
|
2026-06-21 06:12:28 +03:00
|
|
|
stages = {"world_builder", "world_builder_schema", "world_builder_env", "world_builder_entities", "world_editor"}
|
2026-06-20 19:13:05 +03:00
|
|
|
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"
|
2026-06-21 06:12:28 +03:00
|
|
|
stages = {"world_builder", "world_builder_schema", "world_builder_env", "world_builder_entities", "world_editor"}
|
2026-06-20 19:13:05 +03:00
|
|
|
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)",
|
|
|
|
|
)
|