CoolFace
Apppublic

vasiuuu/DGX_AI

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
mcp_server.py849 linesDownload Raw Back to codeforge
1from __future__ import annotations2 3import json4import logging5from collections.abc import Callable6from pathlib import Path7from typing import Any8from uuid import uuid49 10from codeforge.environment import CodeForgeEnvironment11from codeforge.models import CodeForgeAction, CodeForgeActionType12from codeforge.ralph.synthesizer import Synthesizer13from codeforge.tasks import TASKS14 15_log = logging.getLogger(__name__)16_VERSION = "0.2.0"17 18_Handler = Callable[19    ["CodeForgeMCPServer", "dict[str, Any]"],20    "dict[str, Any]",21]22 23_SID_DESC = "Session ID from codeforge_reset."24_SID_PROP: dict[str, str] = {25    "type": "string",26    "description": _SID_DESC,27}28 29# -------------------------------------------------------------------30# Tool schema definitions (SYSTEM_DESIGN §9.1)31# -------------------------------------------------------------------32 33_TOOL_DEFS: tuple[dict[str, Any], ...] = (34    {35        "name": "codeforge_reset",36        "description": (37            "Start a new CodeForge episode. You will receive a task "38            "brief and initial files. Your goal is to produce working "39            "Python code that passes sandbox verification. Budget is "40            "limited — plan your actions carefully."41        ),42        "inputSchema": {43            "type": "object",44            "properties": {45                "task_level": {46                    "type": "string",47                    "enum": ["easy", "medium", "hard"],48                    "description": (49                        "Difficulty level. Easy: single file, budget "50                        "4. Medium: multi-file with tests, budget 6. "51                        "Hard: three-file module, budget 10."52                    ),53                },54            },55            "required": ["task_level"],56        },57    },58    {59        "name": "codeforge_query_kb",60        "description": (61            "Search the coding skills knowledge base. Returns real "62            "documentation from 1006 skill nodes. Use this to find "63            "patterns, best practices, and guidance BEFORE writing "64            "code. Costs 1 budget unit. DO NOT guess library APIs — "65            "search for them here or verify via documentation first."66        ),67        "inputSchema": {68            "type": "object",69            "properties": {70                "session_id": _SID_PROP,71                "claim": {72                    "type": "string",73                    "description": (74                        "What you want to find guidance on. Be "75                        "specific. Example: 'pytest fixture patterns "76                        "for testing greet functions'"77                    ),78                },79                "top_k": {80                    "type": "integer",81                    "default": 5,82                    "minimum": 1,83                    "maximum": 20,84                    "description": "Number of results to return",85                },86                "required_tags": {87                    "type": "array",88                    "items": {"type": "string"},89                    "default": [],90                    "description": (91                        "Only return nodes that have ALL of these "92                        "tags"93                    ),94                },95            },96            "required": ["session_id", "claim"],97        },98    },99    {100        "name": "codeforge_query_cluster",101        "description": (102            "Browse a skill cluster by label. Clusters are communities "103            "of related skill nodes grouped by Jaccard similarity. Use "104            "this to explore a topic area deeply. Costs 1 budget unit."105        ),106        "inputSchema": {107            "type": "object",108            "properties": {109                "session_id": _SID_PROP,110                "cluster_label": {111                    "type": "string",112                    "description": (113                        "The cluster label to look up. Example: "114                        "'python_testing_pytest_fixtures'"115                    ),116                },117                "top_k": {118                    "type": "integer",119                    "default": 10,120                    "minimum": 1,121                    "maximum": 50,122                },123            },124            "required": ["session_id", "cluster_label"],125        },126    },127    {128        "name": "codeforge_interrogate",129        "description": (130            "Get Socratic questions about the task that cite real skill "131            "corpus nodes. Use this BEFORE writing code to identify "132            "edge cases, success criteria, and assumptions you might be "133            "wrong about. Costs 1 budget unit."134        ),135        "inputSchema": {136            "type": "object",137            "properties": {138                "session_id": _SID_PROP,139                "brief_override": {140                    "type": "string",141                    "description": (142                        "Optional override for the task brief. "143                        "If omitted, uses the current task brief."144                    ),145                },146            },147            "required": ["session_id"],148        },149    },150    {151        "name": "codeforge_run_ralph",152        "description": (153            "Run autonomous improvement iterations on your current "154            "code. Each iteration: synthesize improvement → "155            "sandbox-score → keep if better. Costs max_iters budget "156            "units. Wasted iterations (no improvement) cost 0.05 "157            "penalty each. Use when you want the environment to "158            "iteratively improve your code."159        ),160        "inputSchema": {161            "type": "object",162            "properties": {163                "session_id": _SID_PROP,164                "max_iters": {165                    "type": "integer",166                    "default": 3,167                    "minimum": 1,168                    "maximum": 10,169                    "description": (170                        "Maximum iterations. Each costs 1 budget. "171                        "Choose carefully."172                    ),173                },174            },175            "required": ["session_id", "max_iters"],176        },177    },178    {179        "name": "codeforge_submit",180        "description": (181            "Submit Python files for grading. Your code will be: "182            "(1) written to a sandbox and checked by ruff, mypy "183            "--strict, pytest, and import resolution — these are REAL "184            "tools, not mocks; (2) AST-grounded to verify every "185            "import and attribute access resolves to a real Python "186            "module/attribute; (3) scored via quality = 0.6*sandbox + "187            "0.4*groundedness; (4) if you provide confidence, "188            "Brier-penalized: reward = quality * (1 - "189            "min((confidence-quality)^2, 0.5)). DO NOT fabricate "190            "library names or API signatures — the grounder WILL "191            "catch them and your score WILL drop."192        ),193        "inputSchema": {194            "type": "object",195            "properties": {196                "session_id": _SID_PROP,197                "files": {198                    "type": "object",199                    "additionalProperties": {"type": "string"},200                    "description": (201                        "Map of filename to file content. Example: "202                        '{"main.py": "def greet(name: str) -> str:'203                        "\\n    return f'Hello, {name}!'\\n\"}"204                    ),205                },206                "confidence": {207                    "type": "number",208                    "minimum": 0.0,209                    "maximum": 1.0,210                    "description": (211                        "Your confidence that this submission is "212                        "correct (0.0 = no idea, 1.0 = certain). "213                        "Overconfidence on bad code is PENALIZED. "214                        "Honest uncertainty is treated more "215                        "favorably. If you are unsure, say so."216                    ),217                },218            },219            "required": ["session_id", "files"],220        },221    },222    {223        "name": "codeforge_get_audit",224        "description": (225            "Read the audit trail for the current episode (or a "226            "specific run). Returns every action taken, every citation "227            "made, every reward earned, and the evidence behind each. "228            "Costs 0 budget. Use this to review your progress and "229            "understand what worked."230        ),231        "inputSchema": {232            "type": "object",233            "properties": {234                "session_id": _SID_PROP,235                "target_run_id": {236                    "type": "string",237                    "description": (238                        "Optional run ID to audit. "239                        "Defaults to current episode."240                    ),241                },242            },243            "required": ["session_id"],244        },245    },246    {247        "name": "codeforge_state",248        "description": (249            "Get current episode state without taking an action. Shows "250            "task brief, current files, budget remaining, last reward, "251            "and whether the episode is done. Costs 0 budget."252        ),253        "inputSchema": {254            "type": "object",255            "properties": {"session_id": _SID_PROP},256            "required": ["session_id"],257        },258    },259    {260        "name": "codeforge_list_clusters",261        "description": (262            "List all available cluster labels and their node counts. "263            "Use this to discover what topic areas exist before "264            "calling codeforge_query_cluster. Costs 0 budget."265        ),266        "inputSchema": {267            "type": "object",268            "properties": {"session_id": _SID_PROP},269        },270    },271    {272        "name": "codeforge_list_tags",273        "description": (274            "List all available tags in the skill corpus. Use this to "275            "discover valid values for the required_tags parameter. "276            "Costs 0 budget."277        ),278        "inputSchema": {279            "type": "object",280            "properties": {"session_id": _SID_PROP},281        },282    },283)284 285# -------------------------------------------------------------------286# Resource definitions287# -------------------------------------------------------------------288 289_RESOURCE_DEFS: tuple[dict[str, str], ...] = (290    {291        "uri": "codeforge://corpus/stats",292        "name": "Corpus Statistics",293        "description": (294            "Corpus statistics (node count, vocab size, cluster count)"295        ),296        "mimeType": "application/json",297    },298    {299        "uri": "codeforge://corpus/node/{node_id}",300        "name": "Skill Node",301        "description": (302            "Full content of a specific skill node (free, no budget)"303        ),304        "mimeType": "application/json",305    },306    {307        "uri": "codeforge://tasks",308        "name": "Task Definitions",309        "description": (310            "Task definitions with briefs, budgets, targets, tools"311        ),312        "mimeType": "application/json",313    },314    {315        "uri": "codeforge://audit/{episode_id}",316        "name": "Audit Ledger",317        "description": (318            "Serialized audit ledger for a completed episode"319        ),320        "mimeType": "application/json",321    },322)323 324# -------------------------------------------------------------------325# Prompt text326# -------------------------------------------------------------------327 328_SYSTEM_PROMPT_TEXT = (329    "You are solving a CodeForge episode. Your code is graded by "330    "REAL tools (ruff, mypy --strict, pytest, import resolution) in "331    "a sandbox. Every import and attribute access is AST-grounded "332    "against the real Python runtime. Overconfidence is penalized "333    "via Brier scoring. Honest uncertainty about genuinely uncertain "334    "results is rewarded.\n\n"335    "Rules:\n"336    "- DO NOT fabricate library names or API signatures — "337    "the grounder catches them.\n"338    "- DO NOT submit stubs (pass, ..., NotImplementedError) — "339    "they score zero.\n"340    "- Use codeforge_query_kb to find patterns BEFORE writing code.\n"341    "- Use codeforge_interrogate to identify edge cases.\n"342    "- Budget is limited. Plan actions carefully.\n"343    "- If unsure of your confidence, set it low — "344    "the grader rewards honesty.\n"345)346 347_SESSION_ERR = "Invalid session_id: {sid!r}. Call codeforge_reset first."348 349 350# -------------------------------------------------------------------351# Helpers352# -------------------------------------------------------------------353 354 355def _obs_to_dict(obs: Any) -> dict[str, Any]:356    """Convert a CodeForgeObservation to a serializable dict."""357    result: dict[str, Any] = json.loads(obs.model_dump_json())358    return result359 360 361def _make_response(362    obs: Any,363    *,364    session_id: str | None = None,365    extra: dict[str, Any] | None = None,366) -> dict[str, Any]:367    """Build a versioned response dict from an observation."""368    result: dict[str, Any] = {"_codeforge_version": _VERSION}369    if session_id is not None:370        result["session_id"] = session_id371    result["observation"] = _obs_to_dict(obs)372    if extra:373        result.update(extra)374    budget = result["observation"].get("budget_remaining", 0)375    if isinstance(budget, int) and 0 < budget <= 2:376        result["budget_warning"] = (377            f"WARNING: {budget} budget remaining — plan carefully."378        )379    return result380 381 382def _session_error(sid: str) -> dict[str, Any]:383    """Return an isError response for a missing session."""384    return {385        "isError": True,386        "error": _SESSION_ERR.format(sid=sid),387        "_codeforge_version": _VERSION,388    }389 390 391def _require_session(392    server: CodeForgeMCPServer,393    arguments: dict[str, Any],394) -> tuple[str, CodeForgeEnvironment | None]:395    """Extract session_id and look up the environment."""396    sid: str = arguments.get("session_id", "")397    return sid, server._get_session(sid)398 399 400# -------------------------------------------------------------------401# CodeForgeMCPServer — embedded mode402# -------------------------------------------------------------------403 404 405class CodeForgeMCPServer:406    """MCP server wrapping CodeForgeEnvironment (SYSTEM_DESIGN §9).407 408    Embedded mode: imports CodeForgeEnvironment directly.409    Each tool call is routed to a session-keyed environment.410 411    **Universal LLM support:** The Ralph loop's synthesizer is configurable.412    Whichever LLM connects to this MCP server can provide its own config::413 414        # Ollama (local, free)415        server = CodeForgeMCPServer(llm_provider="ollama", llm_model="llama3")416 417        # OpenAI418        server = CodeForgeMCPServer(llm_provider="openai", llm_model="gpt-4o")419 420        # Anthropic421        server = CodeForgeMCPServer(llm_provider="anthropic", llm_model="claude-sonnet-4-20250514")422 423        # Any OpenAI-compatible (vLLM, LM Studio, Together, Groq)424        server = CodeForgeMCPServer(425            llm_provider="openai",426            llm_base_url="http://localhost:8000/v1",427            llm_model="my-model",428        )429 430    When no LLM config is provided, Ralph uses the deterministic431    StubSynthesizer (no API calls needed).432    """433 434    def __init__(435        self,436        *,437        corpus_path: Path | None = None,438        max_sessions: int = 10,439        llm_provider: str | None = None,440        llm_api_key: str | None = None,441        llm_base_url: str | None = None,442        llm_model: str | None = None,443    ) -> None:444        self._corpus_path = corpus_path445        self._sessions: dict[str, CodeForgeEnvironment] = {}446        self._max_sessions = max_sessions447        self._llm_provider = llm_provider448        self._llm_api_key = llm_api_key449        self._llm_base_url = llm_base_url450        self._llm_model = llm_model451 452    # -- Session management ------------------------------------------453 454    def _get_session(455        self,456        session_id: str,457    ) -> CodeForgeEnvironment | None:458        return self._sessions.get(session_id)459 460    def _create_session(461        self,462    ) -> tuple[str, CodeForgeEnvironment]:463        sid = uuid4().hex[:16]464        # Build synthesizer from LLM config (if provided)465        synth: Synthesizer | None = None466        if self._llm_provider:467            from codeforge.ralph.synthesizer import LLMSynthesizer468 469            synth = LLMSynthesizer(470                provider=self._llm_provider,471                api_key=self._llm_api_key,472                base_url=self._llm_base_url,473                model=self._llm_model,474            )475 476        env = CodeForgeEnvironment(477            corpus_path=self._corpus_path,478            synthesizer=synth,479        )480        if len(self._sessions) >= self._max_sessions:481            oldest = next(iter(self._sessions))482            del self._sessions[oldest]483        self._sessions[sid] = env484        return sid, env485 486    # -- Public: definitions -----------------------------------------487 488    def tool_definitions(self) -> list[dict[str, Any]]:489        """Return tool schemas matching SYSTEM_DESIGN §9.1."""490        return [dict(d) for d in _TOOL_DEFS]491 492    def resource_definitions(self) -> list[dict[str, str]]:493        """Return MCP resource definitions."""494        return [dict(r) for r in _RESOURCE_DEFS]495 496    def prompt_definitions(self) -> list[dict[str, Any]]:497        """Return MCP prompt definitions."""498        return [499            {500                "name": "codeforge_system",501                "description": (502                    "System prompt injected at session start. "503                    "Contains task rules, budget constraints, "504                    "grading explanation."505                ),506                "arguments": [],507            },508            {509                "name": "codeforge_task_brief",510                "description": (511                    "Dynamic prompt populated with the current "512                    "task's brief, initial files, budget, target "513                    "score, and tool config."514                ),515                "arguments": [516                    {517                        "name": "session_id",518                        "description": _SID_DESC,519                        "required": True,520                    },521                ],522            },523        ]524 525    # -- Public: handle_tool -----------------------------------------526 527    def handle_tool(528        self,529        tool_name: str,530        arguments: dict[str, Any],531    ) -> dict[str, Any]:532        """Route tool call to handler, return result dict."""533        handler = _HANDLERS.get(tool_name)534        if handler is None:535            return {536                "isError": True,537                "error": f"Unknown tool: {tool_name!r}",538                "_codeforge_version": _VERSION,539            }540        return handler(self, arguments)541 542    # -- Public: resources -------------------------------------------543 544    def read_resource(545        self,546        uri: str,547        *,548        session_id: str | None = None,549    ) -> dict[str, Any]:550        """Read an MCP resource by URI."""551        if uri == "codeforge://corpus/stats":552            if session_id is None:553                return {554                    "_codeforge_version": _VERSION,555                    "error": "session_id required for corpus stats",556                }557            env = self._get_session(session_id)558            if env is None:559                return {"_codeforge_version": _VERSION, "error": "Invalid session_id"}560            idx = env._ensure_index()561            stats = idx.stats()562            cluster_count = len(idx.all_cluster_labels())563            return {564                "node_count": stats["node_count"],565                "vocab_size": stats["vocab_size"],566                "avg_doc_len": stats["avg_doc_len"],567                "cluster_count": cluster_count,568            }569        if uri.startswith("codeforge://corpus/node/"):570            node_id = uri.removeprefix("codeforge://corpus/node/")571            if session_id is None:572                return {573                    "_codeforge_version": _VERSION,574                    "error": "session_id required for node lookup",575                }576            env = self._get_session(session_id)577            if env is None:578                return {"_codeforge_version": _VERSION, "error": "Invalid session_id"}579            idx = env._ensure_index()580            for node in idx._nodes:581                if node.get("id") == node_id:582                    return {"_codeforge_version": _VERSION, "node": node}583            return {"_codeforge_version": _VERSION, "error": f"Node {node_id!r} not found"}584        if uri == "codeforge://tasks":585            return {586                "_codeforge_version": _VERSION,587                "tasks": [588                    {589                        "id": t.task_id,590                        "difficulty": t.task_level,591                        "brief": t.brief,592                        "target_score": t.target_score,593                        "max_budget": t.max_budget,594                        "tools": list(t.tools),595                    }596                    for t in TASKS597                ],598            }599        if uri.startswith("codeforge://audit/"):600            episode_id = uri.removeprefix("codeforge://audit/")601            if session_id is None:602                return {603                    "_codeforge_version": _VERSION,604                    "error": "session_id required for audit lookup",605                }606            env = self._get_session(session_id)607            if env is None:608                return {"_codeforge_version": _VERSION, "error": "Invalid session_id"}609            if env._ledger is not None:610                return {611                    "_codeforge_version": _VERSION,612                    "episode_id": episode_id,613                    "audit": env._ledger.serialize(),614                }615            return {616                "_codeforge_version": _VERSION,617                "error": "No audit data for this session",618            }619        return {"_codeforge_version": _VERSION, "error": f"Unknown resource URI: {uri!r}"}620 621    # -- Public: prompts ---------------------------------------------622 623    def get_prompt(624        self,625        name: str,626        *,627        session_id: str | None = None,628    ) -> list[dict[str, str]]:629        """Return prompt messages for the given prompt name."""630        if name == "codeforge_system":631            return [632                {"role": "system", "content": _SYSTEM_PROMPT_TEXT},633            ]634        if name == "codeforge_task_brief":635            if session_id is None:636                return [637                    {638                        "role": "system",639                        "content": (640                            "Error: session_id required for "641                            "task_brief prompt."642                        ),643                    },644                ]645            env = self._get_session(session_id)646            if env is None:647                return [648                    {649                        "role": "system",650                        "content": "Error: invalid session_id.",651                    },652                ]653            obs = env.state654            task = env._task655            target = task.target_score if task is not None else 0.0656            content = (657                f"## Task: {obs.task_id}\n"658                f"**Level:** {obs.task_level}\n"659                f"**Brief:** {obs.task_brief}\n"660                f"**Budget:** {obs.budget_remaining}\n"661                f"**Target score:** {target}\n\n"662                "### Initial files\n"663            )664            for fname, body in obs.initial_files.items():665                content += (666                    f"\n**{fname}:**\n```python\n{body}\n```\n"667                )668            return [{"role": "system", "content": content}]669        return [670            {671                "role": "system",672                "content": f"Unknown prompt: {name!r}",673            },674        ]675 676 677# -------------------------------------------------------------------678# Tool handlers (private, keyed by tool name)679# -------------------------------------------------------------------680 681 682def _handle_reset(683    server: CodeForgeMCPServer,684    arguments: dict[str, Any],685) -> dict[str, Any]:686    task_level = arguments.get("task_level", "easy")687    sid, env = server._create_session()688    obs = env.reset(task_level=task_level)689    return _make_response(obs, session_id=sid)690 691 692def _handle_query_kb(693    server: CodeForgeMCPServer,694    arguments: dict[str, Any],695) -> dict[str, Any]:696    sid, env = _require_session(server, arguments)697    if env is None:698        return _session_error(sid)699    action = CodeForgeAction(700        action_type=CodeForgeActionType.QUERY_KB,701        claim=arguments.get("claim"),702        top_k=arguments.get("top_k", 5),703        required_tags=tuple(arguments.get("required_tags", ())),704    )705    obs = env.step(action)706    return _make_response(obs, session_id=sid)707 708 709def _handle_query_cluster(710    server: CodeForgeMCPServer,711    arguments: dict[str, Any],712) -> dict[str, Any]:713    sid, env = _require_session(server, arguments)714    if env is None:715        return _session_error(sid)716    action = CodeForgeAction(717        action_type=CodeForgeActionType.QUERY_CLUSTER,718        cluster_label=arguments.get("cluster_label"),719        top_k=arguments.get("top_k", 10),720    )721    obs = env.step(action)722    return _make_response(obs, session_id=sid)723 724 725def _handle_interrogate(726    server: CodeForgeMCPServer,727    arguments: dict[str, Any],728) -> dict[str, Any]:729    sid, env = _require_session(server, arguments)730    if env is None:731        return _session_error(sid)732    action = CodeForgeAction(733        action_type=CodeForgeActionType.INTERROGATE,734    )735    obs = env.step(action)736    return _make_response(obs, session_id=sid)737 738 739def _handle_run_ralph(740    server: CodeForgeMCPServer,741    arguments: dict[str, Any],742) -> dict[str, Any]:743    sid, env = _require_session(server, arguments)744    if env is None:745        return _session_error(sid)746    action = CodeForgeAction(747        action_type=CodeForgeActionType.RUN_RALPH,748        max_iters=arguments.get("max_iters", 3),749    )750    obs = env.step(action)751    return _make_response(obs, session_id=sid)752 753 754def _handle_submit(755    server: CodeForgeMCPServer,756    arguments: dict[str, Any],757) -> dict[str, Any]:758    sid, env = _require_session(server, arguments)759    if env is None:760        return _session_error(sid)761    action = CodeForgeAction(762        action_type=CodeForgeActionType.SUBMIT,763        files=arguments.get("files"),764        confidence=arguments.get("confidence"),765    )766    obs = env.step(action)767    return _make_response(obs, session_id=sid)768 769 770def _handle_get_audit(771    server: CodeForgeMCPServer,772    arguments: dict[str, Any],773) -> dict[str, Any]:774    sid, env = _require_session(server, arguments)775    if env is None:776        return _session_error(sid)777    action = CodeForgeAction(778        action_type=CodeForgeActionType.GET_AUDIT,779        target_run_id=arguments.get("target_run_id"),780    )781    obs = env.step(action)782    return _make_response(obs, session_id=sid)783 784 785def _handle_state(786    server: CodeForgeMCPServer,787    arguments: dict[str, Any],788) -> dict[str, Any]:789    sid, env = _require_session(server, arguments)790    if env is None:791        return _session_error(sid)792    obs = env.state793    return _make_response(obs, session_id=sid)794 795 796def _handle_list_clusters(797    server: CodeForgeMCPServer,798    arguments: dict[str, Any],799) -> dict[str, Any]:800    _sid, env = _require_session(server, arguments)801    if env is None:802        return {"_codeforge_version": _VERSION, "clusters": []}803    try:804        idx = env._ensure_index()805    except FileNotFoundError:806        return {"_codeforge_version": _VERSION, "clusters": []}807    labels = idx.all_cluster_labels()808    cluster_info: list[dict[str, Any]] = []809    for label in labels:810        cluster = idx.cluster_by_label(label)811        if cluster is not None:812            cluster_info.append({813                "label": cluster.label,814                "node_count": cluster.node_count,815            })816    return {"_codeforge_version": _VERSION, "clusters": cluster_info}817 818 819def _handle_list_tags(820    server: CodeForgeMCPServer,821    arguments: dict[str, Any],822) -> dict[str, Any]:823    _sid, env = _require_session(server, arguments)824    if env is None:825        return {"_codeforge_version": _VERSION, "tags": []}826    try:827        idx = env._ensure_index()828    except FileNotFoundError:829        return {"_codeforge_version": _VERSION, "tags": []}830    return {831        "_codeforge_version": _VERSION,832        "tags": sorted(idx.all_tags()),833    }834 835 836# Handler dispatch table837_HANDLERS: dict[str, _Handler] = {838    "codeforge_reset": _handle_reset,839    "codeforge_query_kb": _handle_query_kb,840    "codeforge_query_cluster": _handle_query_cluster,841    "codeforge_interrogate": _handle_interrogate,842    "codeforge_run_ralph": _handle_run_ralph,843    "codeforge_submit": _handle_submit,844    "codeforge_get_audit": _handle_get_audit,845    "codeforge_state": _handle_state,846    "codeforge_list_clusters": _handle_list_clusters,847    "codeforge_list_tags": _handle_list_tags,848}849