Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .agent/mcp-servers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"mcpServers": {
"serena": {
"command": "/home/sebas/.local/bin/uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--project",
"/home/sebas/monitor2",

Copilot AI Jan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded absolute path /home/sebas/monitor2 in the MCP server configuration makes this file non-portable across different development environments. This should use a relative path or environment variable.

Consider using ${workspaceFolder} or similar placeholder that can be resolved per environment.

Suggested change
"/home/sebas/monitor2",
"${workspaceFolder}",

Copilot uses AI. Check for mistakes.
"--context",
"ide"
]
}
}
}
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,18 @@ htmlcov/

# Package lock files
uv.lock

# Infrastructure data
infra/mongodb/data
infra/mongodb/configdb
infra/mongodb/init
infra/neo4j/data
infra/neo4j/logs
infra/neo4j/import
infra/neo4j/plugins
infra/qdrant/storage
infra/minio/data
infra/opensearch/data

# IDE
.vscode/
33 changes: 29 additions & 4 deletions packages/agents/src/monitor_agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,35 @@ async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
Raises:
PermissionError: If agent lacks authority for this tool
"""
# TODO: Implement MCP tool calling
# context = {"agent_id": self.agent_id, "agent_type": self.agent_type}
# return await mcp_client.call(tool_name, arguments, context=context)
raise NotImplementedError("Tool calling not yet implemented")
# Import here to avoid circular dependencies if any
# Layer 2 imports Layer 1
from monitor_data.server import call_tool as server_call_tool, discover_tools

# Ensure tools are discovered
if not hasattr(server_call_tool, "_discovered"):
discover_tools()
server_call_tool._discovered = True

# Inject agent context for Auth middleware
full_args = arguments.copy()
full_args["agent_type"] = self.agent_type
full_args["agent_id"] = self.agent_id

# Call the tool via the server's decorated function
# Note: server.call_tool returns [TextContent], we need to parse it back if possible
# or return the raw result. For internal use, we might want the direct result.
# However, the server.call_tool serializes to JSON/TextContent.
#
# For efficiency and type safety in internal calls, we should ideally use the
# registry directly, but we want the Middleware (Auth/Validation).
# The server.call_tool provides that.

result_contents = await server_call_tool(tool_name, full_args)

# Parse result - usually it's a single TextContent with JSON
if result_contents and len(result_contents) > 0:
return result_contents[0].text
return None

@abstractmethod
async def run(self) -> None:
Expand Down
168 changes: 168 additions & 0 deletions packages/agents/src/monitor_agents/resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
Resolver Agent implementation.

LAYER: 2 (agents)
Authority: MongoDB (resolutions, proposals), Character State
"""

from monitor_agents.base import BaseAgent


"""
Resolver Agent implementation.

LAYER: 2 (agents)
Authority: MongoDB (resolutions, proposals), Character State
"""

Comment on lines +8 to +17

Copilot AI Jan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate docstring and import statement suggests a copy-paste error during file creation. The first docstring block (lines 1-6) and import (line 8) are followed by an identical second docstring block (lines 11-16).

Remove the duplicate docstring and organize imports properly at the top of the file.

Suggested change
from monitor_agents.base import BaseAgent
"""
Resolver Agent implementation.
LAYER: 2 (agents)
Authority: MongoDB (resolutions, proposals), Character State
"""

Copilot uses AI. Check for mistakes.
import json
import logging
from typing import Dict, Any, Optional

Copilot AI Jan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'Optional' is not used.

Suggested change
from typing import Dict, Any, Optional
from typing import Dict, Any

Copilot uses AI. Check for mistakes.

from monitor_agents.base import BaseAgent
from monitor_data.utils.dice import roll_dice, calculate_modifier

logger = logging.getLogger(__name__)

class Resolver(BaseAgent):
"""
Agent responsible for resolving rules, mechanic checks, and character state changes.
"""

def __init__(self, agent_id: str = "resolver-cli-1") -> None:
super().__init__(agent_type="Resolver", agent_id=agent_id)

async def run(self) -> None:
pass

async def resolve_check(
self, entity_id: str, stat_name: str, dc: int = 15
) -> Dict[str, Any]:
"""
Resolve a statistic check (Attribute/Skill) for an entity.

Workflow:
1. Get Entity -> Universe -> Multiverse -> System Name.
2. Get Game System rules.
3. Get Entity working state (not strictly needed for basic attrib check, but good for context).
4. Get Entity properties (attributes).
5. Calculate modifier.
6. Roll dice.
7. Determine outcome.
"""
try:
# 1. Get Entity to find Universe/System
# We need to parse the JSON result from call_tool
entity_json = await self.call_tool("neo4j_get_entity", {"entity_id": entity_id})
if not entity_json:
return {"error": "Entity not found"}

entity_data = json.loads(entity_json)
# entity_data is { ... } response model

universe_id = entity_data.get("universe_id")

# 2. Get Universe to find Multiverse
universe_json = await self.call_tool("neo4j_get_universe", {"universe_id": str(universe_id)})
if not universe_json:
return {"error": "Universe not found"}
universe_data = json.loads(universe_json)
multiverse_id = universe_data.get("multiverse_id")

# 3. Get Multiverse to find System Name
multiverse_json = await self.call_tool("neo4j_get_multiverse", {"multiverse_id": str(multiverse_id)})
if not multiverse_json:
# Fallback: try to guess system or default
system_name = "Standard Fantasy v5"
else:
multiverse_data = json.loads(multiverse_json)
system_name = multiverse_data.get("system_name")

# 4. Get Game System
# list systems and filter by name (since we don't have get_by_name yet, or we simulate it)
# For now, we grab the first matching one
systems_json = await self.call_tool("mongodb_list_game_systems", {"limit": 100, "include_builtin": True, "offset": 0})
systems_data = json.loads(systems_json)

system = None
for sys in systems_data.get("systems", []):
if sys["name"] == system_name:
system = sys
break

if not system:
return {"error": f"Game system '{system_name}' not found"}

# 5. Get Entity Attributes (Properties)
# Assuming 'properties' field in Entity contains attributes map: {"Strength": 16, ...}
# Or formatted as {"attributes": {"Strength": 16}}
props = entity_data.get("properties", {})
attributes = props.get("attributes", {})

# Handle case where props form is flat {"Strength": 16}
if not attributes:
attributes = props

# Find attribute definition in system
target_attr_def = next((a for a in system["attributes"] if a["name"].lower() == stat_name.lower()), None)

if not target_attr_def:
# Is it a skill?
target_skill_def = next((s for s in system["skills"] if s["name"].lower() == stat_name.lower()), None)
if target_skill_def:
# It's a skill check
linked_attr = target_skill_def["linked_attribute"]
stat_value = attributes.get(linked_attr, 10) # Default 10 if missing
# We might add a proficiency bonus here if we knew user's level/proficiency
# For MVP, we treat it as Attribute Check using linked attribute
else:
return {"error": f"Stat '{stat_name}' not found in system '{system_name}'"}
else:
# It's an attribute
stat_value = attributes.get(target_attr_def["name"], target_attr_def.get("default_value", 10))

# 6. Calculate Modifier
mod_formula = target_attr_def.get("modifier_formula")
if mod_formula:
Comment on lines +124 to +126

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle skill checks without attribute definition

When stat_name is a skill, target_attr_def stays None (lines 109–118), but later mod_formula = target_attr_def.get(...) dereferences it unconditionally, which raises AttributeError and makes every skill check return an error instead of a result. This happens whenever a skill (not an attribute) is passed, so resolve_check cannot resolve any skill-based checks unless you set target_attr_def to the linked attribute definition or branch the modifier calculation for the skill path.

Useful? React with 👍 / 👎.

modifier = calculate_modifier(stat_value, mod_formula)
else:
modifier = 0

# 7. Roll Dice
core_mechanic = system["core_mechanic"]
# e.g., "1d20 + ATTRIBUTE_MOD" -> we simplify to just rolling d20 + mod
# A real parser would replace variables in formula string.

# Simple implementation for D20 and Dice Pool
if "d20" in core_mechanic["type"]:
base_roll = roll_dice("1d20")
total = base_roll.total + modifier
success = total >= dc
details = f"Rolled {base_roll.total} + Mod {modifier} = {total} (DC {dc})"

elif "dice_pool" in core_mechanic["type"]:
# Assume attribute = number of dice
pool_size = stat_value # + skill if applicable
dice_res = roll_dice(f"{pool_size}d10") # Vampire uses d10s
# Count successes >= threshold
threshold = int(core_mechanic.get("success_threshold", 6))
Comment on lines +143 to +148

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept non-numeric success_threshold values

The dice-pool path does threshold = int(core_mechanic.get("success_threshold", 6)), but the built-in systems you added define success_threshold as descriptive strings (e.g., Fate Core and PbtA in packages/data-layer/src/monitor_data/data/builtin_systems.json), so int(...) raises ValueError and resolve_check returns an error for those systems. This means dice-pool checks for those bundled systems are unusable until you parse a numeric threshold or handle string thresholds explicitly.

Useful? React with 👍 / 👎.

successes = sum(1 for d in dice_res.rolls if d >= threshold)
total = successes
success = successes > 0 # Simple success
details = f"Rolled {pool_size}d10: {dice_res.rolls} -> {successes} successes"

else:
return {"error": f"Unsupported mechanic type: {core_mechanic['type']}"}

return {
"success": success,
"total": total,
"details": details,
"system": system_name,
"stat": stat_name,
"modifier": modifier
}

except Exception as e:
logger.exception("Error resolving check")
return {"error": str(e)}
60 changes: 60 additions & 0 deletions packages/cli/src/monitor_cli/commands/mechanics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
CLI commands for resolving mechanics (DL-24).
"""

import asyncio
import json

Copilot AI Jan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'json' is not used.

Suggested change
import json

Copilot uses AI. Check for mistakes.
import typer
from rich.console import Console
from rich.panel import Panel

from monitor_agents.resolver import Resolver

app = typer.Typer(help="Resolve game mechanics (checks, saves, combat)")
console = Console()


def _get_agent():
return Resolver()


def _run_async(coro):
return asyncio.run(coro)


@app.command("check")
def check_stat(
entity_id: str = typer.Argument(..., help="UUID of the entity"),
stat: str = typer.Argument(..., help="Name of the stat (Strength, Stealth, etc.)"),
dc: int = typer.Option(15, help="Difficulty Class"),
):
"""
Perform a mechanic check (Attribute or Skill).
"""
agent = _get_agent()

console.print(f"[bold]Resolving {stat} check for {entity_id} vs DC {dc}...[/bold]")

try:
result = _run_async(agent.resolve_check(entity_id, stat, dc))

if "error" in result:
console.print(f"[red]Error: {result['error']}[/red]")
return

# Display result
success_color = "green" if result["success"] else "red"
success_text = "SUCCESS" if result["success"] else "FAILURE"

panel_content = f"""
[bold {success_color}]{success_text}[/bold {success_color}]

System: {result['system']}
Stat: {result['stat']} (Mod: {result['modifier']})
Details: {result['details']}
"""

console.print(Panel(panel_content, title="Resolution Result", border_style=success_color))

except Exception as e:
console.print(f"[red]Error: {e}[/red]")
Loading