mvge 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. mvge-0.1.1/PKG-INFO +18 -0
  2. mvge-0.1.1/pyproject.toml +112 -0
  3. mvge-0.1.1/pyproject.toml.orig +101 -0
  4. mvge-0.1.1/src/mvge/__init__.py +7 -0
  5. mvge-0.1.1/src/mvge/__main__.py +6 -0
  6. mvge-0.1.1/src/mvge/agent/__init__.py +100 -0
  7. mvge-0.1.1/src/mvge/agent/agent_name.py +152 -0
  8. mvge-0.1.1/src/mvge/agent/agent_session.py +712 -0
  9. mvge-0.1.1/src/mvge/agent/auth.py +111 -0
  10. mvge-0.1.1/src/mvge/agent/commands.py +773 -0
  11. mvge-0.1.1/src/mvge/agent/compatibility.py +175 -0
  12. mvge-0.1.1/src/mvge/agent/config_manager.py +331 -0
  13. mvge-0.1.1/src/mvge/agent/environment.py +775 -0
  14. mvge-0.1.1/src/mvge/agent/function_spell.py +546 -0
  15. mvge-0.1.1/src/mvge/agent/harness/__init__.py +35 -0
  16. mvge-0.1.1/src/mvge/agent/harness/_state.py +64 -0
  17. mvge-0.1.1/src/mvge/agent/harness/capabilities.py +66 -0
  18. mvge-0.1.1/src/mvge/agent/harness/compaction/__init__.py +35 -0
  19. mvge-0.1.1/src/mvge/agent/harness/compaction/compaction.py +400 -0
  20. mvge-0.1.1/src/mvge/agent/harness/compaction/compaction_runner.py +357 -0
  21. mvge-0.1.1/src/mvge/agent/harness/harness.py +1599 -0
  22. mvge-0.1.1/src/mvge/agent/harness/reload.py +121 -0
  23. mvge-0.1.1/src/mvge/agent/installer.py +495 -0
  24. mvge-0.1.1/src/mvge/agent/mvge.py +1536 -0
  25. mvge-0.1.1/src/mvge/agent/protocol.py +220 -0
  26. mvge-0.1.1/src/mvge/agent/py.typed +0 -0
  27. mvge-0.1.1/src/mvge/agent/rune_lifecycle.py +222 -0
  28. mvge-0.1.1/src/mvge/agent/sigil_payloads.py +100 -0
  29. mvge-0.1.1/src/mvge/agent/snapshot.py +230 -0
  30. mvge-0.1.1/src/mvge/agent/sub_mvge.py +406 -0
  31. mvge-0.1.1/src/mvge/cli/__init__.py +3 -0
  32. mvge-0.1.1/src/mvge/cli/__main__.py +6 -0
  33. mvge-0.1.1/src/mvge/cli/agent_factory.py +158 -0
  34. mvge-0.1.1/src/mvge/cli/agent_name.py +35 -0
  35. mvge-0.1.1/src/mvge/cli/approval_binding.py +145 -0
  36. mvge-0.1.1/src/mvge/cli/approval_presenter.py +584 -0
  37. mvge-0.1.1/src/mvge/cli/approval_types.py +72 -0
  38. mvge-0.1.1/src/mvge/cli/commands/__init__.py +1 -0
  39. mvge-0.1.1/src/mvge/cli/commands/build.py +184 -0
  40. mvge-0.1.1/src/mvge/cli/commands/config.py +200 -0
  41. mvge-0.1.1/src/mvge/cli/commands/dispatcher.py +183 -0
  42. mvge-0.1.1/src/mvge/cli/commands/info.py +273 -0
  43. mvge-0.1.1/src/mvge/cli/commands/mvge.py +96 -0
  44. mvge-0.1.1/src/mvge/cli/commands/repl.py +774 -0
  45. mvge-0.1.1/src/mvge/cli/commands/rune.py +95 -0
  46. mvge-0.1.1/src/mvge/cli/commands/setup.py +532 -0
  47. mvge-0.1.1/src/mvge/cli/commands/tome.py +437 -0
  48. mvge-0.1.1/src/mvge/cli/commands/tui.py +560 -0
  49. mvge-0.1.1/src/mvge/cli/console.py +74 -0
  50. mvge-0.1.1/src/mvge/cli/dynamic_commands.py +313 -0
  51. mvge-0.1.1/src/mvge/cli/formatting.py +344 -0
  52. mvge-0.1.1/src/mvge/cli/main.py +731 -0
  53. mvge-0.1.1/src/mvge/cli/py.typed +0 -0
  54. mvge-0.1.1/src/mvge/cli/rpc.py +479 -0
  55. mvge-0.1.1/src/mvge/core/__init__.py +211 -0
  56. mvge-0.1.1/src/mvge/core/abort.py +105 -0
  57. mvge-0.1.1/src/mvge/core/approval.py +221 -0
  58. mvge-0.1.1/src/mvge/core/channel.py +140 -0
  59. mvge-0.1.1/src/mvge/core/constants.py +69 -0
  60. mvge-0.1.1/src/mvge/core/dispatcher.py +656 -0
  61. mvge-0.1.1/src/mvge/core/errors.py +185 -0
  62. mvge-0.1.1/src/mvge/core/event_bus.py +45 -0
  63. mvge-0.1.1/src/mvge/core/events.py +92 -0
  64. mvge-0.1.1/src/mvge/core/invocations.py +135 -0
  65. mvge-0.1.1/src/mvge/core/layers.py +386 -0
  66. mvge-0.1.1/src/mvge/core/loop.py +511 -0
  67. mvge-0.1.1/src/mvge/core/py.typed +0 -0
  68. mvge-0.1.1/src/mvge/core/sandbox.py +342 -0
  69. mvge-0.1.1/src/mvge/core/spell_schema.py +176 -0
  70. mvge-0.1.1/src/mvge/core/spells.py +384 -0
  71. mvge-0.1.1/src/mvge/core/truncate.py +218 -0
  72. mvge-0.1.1/src/mvge/core/worklog.py +132 -0
  73. mvge-0.1.1/src/mvge/provider/__init__.py +74 -0
  74. mvge-0.1.1/src/mvge/provider/base.py +159 -0
  75. mvge-0.1.1/src/mvge/provider/model_registry.py +553 -0
  76. mvge-0.1.1/src/mvge/provider/models.json +8011 -0
  77. mvge-0.1.1/src/mvge/provider/py.typed +0 -0
  78. mvge-0.1.1/src/mvge/provider/realm_seam.py +212 -0
  79. mvge-0.1.1/src/mvge/provider/realms.py +197 -0
  80. mvge-0.1.1/src/mvge/provider/registry.py +454 -0
  81. mvge-0.1.1/src/mvge/provider/retry.py +551 -0
  82. mvge-0.1.1/src/mvge/provider/sse.py +755 -0
  83. mvge-0.1.1/src/mvge/py.typed +0 -0
  84. mvge-0.1.1/src/mvge/runes/__init__.py +112 -0
  85. mvge-0.1.1/src/mvge/runes/catalog.py +215 -0
  86. mvge-0.1.1/src/mvge/runes/codecs.py +139 -0
  87. mvge-0.1.1/src/mvge/runes/contract.py +240 -0
  88. mvge-0.1.1/src/mvge/runes/deps.py +168 -0
  89. mvge-0.1.1/src/mvge/runes/installer.py +415 -0
  90. mvge-0.1.1/src/mvge/runes/loader.py +442 -0
  91. mvge-0.1.1/src/mvge/runes/manifest.py +135 -0
  92. mvge-0.1.1/src/mvge/runes/py.typed +0 -0
  93. mvge-0.1.1/src/mvge/runes/rune_api.py +328 -0
  94. mvge-0.1.1/src/mvge/runes/rune_audit.py +174 -0
  95. mvge-0.1.1/src/mvge/runes/rune_runner.py +1123 -0
  96. mvge-0.1.1/src/mvge/runes/skill_catalog.py +96 -0
  97. mvge-0.1.1/src/mvge/runes/skill_installer.py +102 -0
  98. mvge-0.1.1/src/mvge/runes/trust.py +256 -0
  99. mvge-0.1.1/src/mvge/runes/types.py +585 -0
  100. mvge-0.1.1/src/mvge/runes/watcher.py +301 -0
  101. mvge-0.1.1/src/mvge/tome/__init__.py +29 -0
  102. mvge-0.1.1/src/mvge/tome/codec.py +305 -0
  103. mvge-0.1.1/src/mvge/tome/handle.py +1446 -0
  104. mvge-0.1.1/src/mvge/tome/py.typed +0 -0
  105. mvge-0.1.1/src/mvge/tome/types.py +145 -0
mvge-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: mvge
3
+ Version: 0.1.1
4
+ Summary: mvge — a Python-based AI coding agent
5
+ License-Expression: MIT
6
+ Requires-Dist: click>=8.0
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: packaging>=24.0
9
+ Requires-Dist: portalocker>=3.0.0
10
+ Requires-Dist: prompt-toolkit>=3.0
11
+ Requires-Dist: pydantic>=2.0
12
+ Requires-Dist: python-dotenv>=1.2.2
13
+ Requires-Dist: pyyaml>=6.0
14
+ Requires-Dist: rich>=13.0
15
+ Requires-Dist: typer>=0.27.1
16
+ Requires-Dist: watchdog>=6.0.0
17
+ Requires-Python: >=3.13
18
+ Project-URL: Repository, https://github.com/IAmNo1Special/mvge
@@ -0,0 +1,112 @@
1
+ [project]
2
+ name = "mvge"
3
+ version = "0.1.1"
4
+ description = "mvge — a Python-based AI coding agent"
5
+ requires-python = ">=3.13"
6
+ license = "MIT"
7
+ dependencies = [
8
+ "click>=8.0",
9
+ "httpx>=0.28.1",
10
+ "packaging>=24.0",
11
+ "portalocker>=3.0.0",
12
+ "prompt-toolkit>=3.0",
13
+ "pydantic>=2.0",
14
+ "python-dotenv>=1.2.2",
15
+ "pyyaml>=6.0",
16
+ "rich>=13.0",
17
+ "typer>=0.27.1",
18
+ "watchdog>=6.0.0",
19
+ ]
20
+
21
+ [project.scripts]
22
+ mvge = "mvge.cli.main:main"
23
+
24
+ [project.urls]
25
+ Repository = "https://github.com/IAmNo1Special/mvge"
26
+
27
+ [build-system]
28
+ requires = ["uv_build>=0.11.2,<0.14"]
29
+ build-backend = "uv_build"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "git-cliff>=2.13.1",
34
+ "mypy>=2.3.1",
35
+ "pre-commit>=4.6.2",
36
+ "pytest>=9.1.1",
37
+ "pytest-asyncio>=1.4.0",
38
+ "pytest-cov>=7.1.0",
39
+ "ruff>=0.16.2",
40
+ "types-pyyaml>=6.0.12.20260815",
41
+ ]
42
+
43
+ [tool.ruff]
44
+ line-length = 88
45
+ target-version = "py313"
46
+ exclude = ["prototypes"]
47
+
48
+ [tool.ruff.lint]
49
+ select = [
50
+ "E",
51
+ "W",
52
+ "F",
53
+ "I",
54
+ "N",
55
+ "UP",
56
+ "B",
57
+ "SIM",
58
+ "C4",
59
+ "PT",
60
+ "T20",
61
+ "RSE",
62
+ "PGH",
63
+ ]
64
+
65
+ [tool.ruff.lint.per-file-ignores]
66
+ "tests/**" = [
67
+ "T20",
68
+ "PT001",
69
+ ]
70
+ "*/__init__.py" = [
71
+ "E402",
72
+ "F401",
73
+ ]
74
+ "scripts/**" = ["T20"]
75
+
76
+ [tool.mypy]
77
+ python_version = "3.13"
78
+ strict = true
79
+ warn_unreachable = true
80
+ exclude = [
81
+ "(^|/)(tests|scripts|prototypes)/",
82
+ 'test_keys\.py',
83
+ "benchmarks",
84
+ ]
85
+
86
+ [tool.pytest.ini_options]
87
+ testpaths = [
88
+ "tests",
89
+ "scripts/tests",
90
+ ]
91
+ pythonpath = [
92
+ ".",
93
+ "src",
94
+ "scripts",
95
+ "tests/unit/provider",
96
+ ]
97
+ python_files = "*.py"
98
+ python_classes = "Test*"
99
+ python_functions = "test_*"
100
+ consider_namespace_packages = false
101
+ addopts = "--import-mode=importlib -W ignore::RuntimeWarning"
102
+
103
+ [tool.coverage.run]
104
+ branch = true
105
+ source = ["mvge"]
106
+ omit = [
107
+ "src/mvge/__main__.py",
108
+ "src/mvge/cli/__main__.py",
109
+ ]
110
+
111
+ [tool.coverage.report]
112
+ fail_under = 90
@@ -0,0 +1,101 @@
1
+ [project]
2
+ name = "mvge"
3
+ version = "0.1.1"
4
+ description = "mvge — a Python-based AI coding agent"
5
+ requires-python = ">=3.13"
6
+ license = "MIT"
7
+ dependencies = [
8
+ "click>=8.0",
9
+ "httpx>=0.28.1",
10
+ "packaging>=24.0",
11
+ "portalocker>=3.0.0",
12
+ "prompt-toolkit>=3.0",
13
+ "pydantic>=2.0",
14
+ "python-dotenv>=1.2.2",
15
+ "pyyaml>=6.0",
16
+ "rich>=13.0",
17
+ "typer>=0.27.1",
18
+ "watchdog>=6.0.0",
19
+ ]
20
+
21
+ [project.scripts]
22
+ mvge = "mvge.cli.main:main"
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.11.2,<0.14"]
26
+ build-backend = "uv_build"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "git-cliff>=2.13.1",
31
+ "mypy>=2.3.1",
32
+ "pre-commit>=4.6.2",
33
+ "pytest>=9.1.1",
34
+ "pytest-asyncio>=1.4.0",
35
+ "pytest-cov>=7.1.0",
36
+ "ruff>=0.16.2",
37
+ "types-pyyaml>=6.0.12.20260815",
38
+ ]
39
+
40
+ [project.urls]
41
+ Repository = "https://github.com/IAmNo1Special/mvge"
42
+
43
+ [tool.ruff]
44
+ line-length = 88
45
+ target-version = "py313"
46
+ exclude = ["prototypes"]
47
+
48
+ [tool.ruff.lint]
49
+ select = [
50
+ "E",
51
+ "W",
52
+ "F",
53
+ "I",
54
+ "N",
55
+ "UP",
56
+ "B",
57
+ "SIM",
58
+ "C4",
59
+ "PT",
60
+ "T20",
61
+ "RSE",
62
+ "PGH",
63
+ ]
64
+
65
+ [tool.ruff.lint.per-file-ignores]
66
+ "tests/**" = ["T20", "PT001"]
67
+ "*/__init__.py" = ["E402", "F401"]
68
+ # Release tooling reports to a terminal; printing is its interface, not a slip.
69
+ # mypy already excludes /scripts/ for the same reason.
70
+ "scripts/**" = ["T20"]
71
+
72
+ [tool.mypy]
73
+ python_version = "3.13"
74
+ strict = true
75
+ warn_unreachable = true
76
+ exclude = ["(^|/)(tests|scripts|prototypes)/", "test_keys\\.py", "benchmarks"]
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = [
80
+ "tests",
81
+ "scripts/tests",
82
+ ]
83
+ pythonpath = [".", "src", "scripts", "tests/unit/provider"]
84
+ python_files = "*.py"
85
+ python_classes = "Test*"
86
+ python_functions = "test_*"
87
+ consider_namespace_packages = false
88
+ addopts = "--import-mode=importlib -W ignore::RuntimeWarning"
89
+
90
+ [tool.coverage.run]
91
+ branch = true
92
+ source = [
93
+ "mvge",
94
+ ]
95
+ omit = [
96
+ "src/mvge/__main__.py",
97
+ "src/mvge/cli/__main__.py",
98
+ ]
99
+
100
+ [tool.coverage.report]
101
+ fail_under = 90
@@ -0,0 +1,7 @@
1
+ """The `mvge` package -- an operating system for AI agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from mvge.cli.main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,100 @@
1
+ from mvge.agent.agent_session import MvgeTome
2
+ from mvge.agent.auth import load_api_key_from_auth, save_api_key_to_auth
3
+ from mvge.agent.commands import (
4
+ SLASH_COMMANDS,
5
+ CommandAction,
6
+ CommandDispatcher,
7
+ CommandOutcome,
8
+ )
9
+ from mvge.agent.environment import (
10
+ AgentConfig,
11
+ MvgeEnvironment,
12
+ PromptSource,
13
+ ResolvedPrompt,
14
+ )
15
+ from mvge.agent.function_spell import (
16
+ FunctionSpell,
17
+ PEP723Metadata,
18
+ PEP723ScriptSpell,
19
+ RuneSpellWrapper,
20
+ SpellUnion,
21
+ coerce_spell,
22
+ discover_spells_from_dir,
23
+ parse_pep723_metadata,
24
+ )
25
+ from mvge.agent.harness import MvgeHarness
26
+ from mvge.agent.installer import (
27
+ DEFAULT_PROFILES,
28
+ ProfileInstallResult,
29
+ fetch_marketplace_mvges,
30
+ fetch_marketplace_profiles,
31
+ install_mvge,
32
+ install_profile,
33
+ list_installed_mvges,
34
+ uninstall_mvge,
35
+ )
36
+ from mvge.agent.mvge import Mvge
37
+ from mvge.agent.protocol import AgentFactory, MvgeAgent, ReloadResult
38
+ from mvge.agent.snapshot import (
39
+ RuntimeSnapshot,
40
+ SnapshotConfigEntry,
41
+ SnapshotDiagnostic,
42
+ SnapshotPrompt,
43
+ SnapshotRune,
44
+ SnapshotSkill,
45
+ SnapshotSpell,
46
+ SpellSource,
47
+ )
48
+ from mvge.agent.sub_mvge import (
49
+ SubMvgeManager,
50
+ SubMvgeRecord,
51
+ SubMvgeSpec,
52
+ SubMvgeState,
53
+ )
54
+
55
+ __all__ = [
56
+ "AgentConfig",
57
+ "AgentFactory",
58
+ "CommandAction",
59
+ "CommandDispatcher",
60
+ "CommandOutcome",
61
+ "DEFAULT_PROFILES",
62
+ "FunctionSpell",
63
+ "Mvge",
64
+ "MvgeAgent",
65
+ "MvgeEnvironment",
66
+ "MvgeHarness",
67
+ "MvgeTome",
68
+ "PEP723Metadata",
69
+ "PEP723ScriptSpell",
70
+ "ProfileInstallResult",
71
+ "PromptSource",
72
+ "ResolvedPrompt",
73
+ "ReloadResult",
74
+ "RuneSpellWrapper",
75
+ "RuntimeSnapshot",
76
+ "SLASH_COMMANDS",
77
+ "SnapshotConfigEntry",
78
+ "SnapshotDiagnostic",
79
+ "SnapshotPrompt",
80
+ "SnapshotRune",
81
+ "SnapshotSkill",
82
+ "SnapshotSpell",
83
+ "SpellSource",
84
+ "SpellUnion",
85
+ "SubMvgeManager",
86
+ "SubMvgeRecord",
87
+ "SubMvgeSpec",
88
+ "SubMvgeState",
89
+ "coerce_spell",
90
+ "discover_spells_from_dir",
91
+ "fetch_marketplace_mvges",
92
+ "fetch_marketplace_profiles",
93
+ "install_mvge",
94
+ "install_profile",
95
+ "list_installed_mvges",
96
+ "load_api_key_from_auth",
97
+ "parse_pep723_metadata",
98
+ "save_api_key_to_auth",
99
+ "uninstall_mvge",
100
+ ]
@@ -0,0 +1,152 @@
1
+ """Which Mvge an invocation runs.
2
+
3
+ Every question about an agent name is answered here, because a second
4
+ answerer is how the engine came to carry a marketplace artifact's slug:
5
+ two call sites, two different rules, the same hardcoded name, and a
6
+ second precedence pass that ADR-0014 was written to end.
7
+
8
+ The engine discovers what is installed and never names one. That is what
9
+ lets the marketplace change its catalog, rename an artifact, or ship a
10
+ second Mvge without a release here.
11
+
12
+ The Summoner's own naming always wins. Discovery only speaks when they
13
+ said nothing actionable and there is nothing to guess wrong.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from pathlib import Path
20
+
21
+ from mvge.core.constants import DEFAULT_AGENT_NAME
22
+ from mvge.core.layers import agents_dir
23
+
24
+ AGENT_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
25
+
26
+ #: The one name the engine answers for without looking at the disk. It names
27
+ #: the engine's own fallback agent, not an artifact anyone ships, so it is a
28
+ #: constant rather than a discovery result.
29
+ ENGINE_DEFAULT_AGENT = DEFAULT_AGENT_NAME
30
+
31
+
32
+ def installed_agent_names(*, agent_config_base: Path | None = None) -> list[str]:
33
+ """Agent names with an agent-scope directory, sorted.
34
+
35
+ The agent scope is ``<global>/agents/<name>/``. Only directories count:
36
+ a file at that depth is not an agent, and `sys.path` receives the agent
37
+ root, so a stray file here is a latent import collision rather than a Mvge.
38
+ """
39
+ base = agent_config_base if agent_config_base is not None else agents_dir()
40
+ if not base.is_dir():
41
+ return []
42
+ return sorted(child.name for child in base.iterdir() if child.is_dir())
43
+
44
+
45
+ def is_known_agent(
46
+ agent_name: str,
47
+ *,
48
+ agent_config_base: Path | None = None,
49
+ project_dir: Path | None = None,
50
+ ) -> bool:
51
+ """Whether an agent name is valid and usable.
52
+
53
+ An agent is known when the name is syntactically right *and* something
54
+ backs it: the engine's own default, an agent-scope directory, or a
55
+ project-scope one. A syntactically valid name with nothing behind it is
56
+ a typo, and saying so is the point.
57
+ """
58
+ if not isinstance(agent_name, str) or not AGENT_NAME_PATTERN.match(agent_name):
59
+ return False
60
+
61
+ if agent_name == DEFAULT_AGENT_NAME:
62
+ return True
63
+
64
+ base = (
65
+ agent_config_base.expanduser()
66
+ if agent_config_base is not None
67
+ else agents_dir()
68
+ )
69
+ if (base / agent_name).is_dir():
70
+ return True
71
+
72
+ proj_dir = project_dir or Path.cwd()
73
+ return (proj_dir / ".agents" / "agents" / agent_name).is_dir()
74
+
75
+
76
+ def validate_agent_name(
77
+ agent_name: str,
78
+ *,
79
+ agent_config_base: Path | None = None,
80
+ project_dir: Path | None = None,
81
+ allow_create: bool = False,
82
+ ) -> None:
83
+ """Validate that an agent name exists and is syntactically valid.
84
+
85
+ Raises ValueError with an actionable message if invalid or unknown.
86
+ """
87
+ if not isinstance(agent_name, str) or not AGENT_NAME_PATTERN.match(agent_name):
88
+ raise ValueError(
89
+ f"Invalid agent name '{agent_name}'. Agent names must contain "
90
+ "only alphanumeric characters, dashes, and underscores."
91
+ )
92
+
93
+ if allow_create:
94
+ return
95
+
96
+ if is_known_agent(
97
+ agent_name, agent_config_base=agent_config_base, project_dir=project_dir
98
+ ):
99
+ return
100
+
101
+ base = (
102
+ agent_config_base.expanduser()
103
+ if agent_config_base is not None
104
+ else agents_dir()
105
+ )
106
+ agent_dir = base / agent_name
107
+ raise ValueError(
108
+ f"Unknown agent '{agent_name}'. Agent configuration or directory "
109
+ f"does not exist at '{agent_dir}'. Configure it with "
110
+ f"'mvge config set --agent-name {agent_name} <key> <value>'."
111
+ )
112
+
113
+
114
+ def resolve_active_agent_name(
115
+ requested: str = DEFAULT_AGENT_NAME,
116
+ *,
117
+ agent_config_base: Path | None = None,
118
+ ) -> str:
119
+ """The Mvge an invocation runs, given what the Summoner asked for.
120
+
121
+ Three rules, in order. Each is a reason not to guess.
122
+
123
+ 1. **What they asked for, when it is backed.** A name with an agent-scope
124
+ directory is that Mvge, spelled their way.
125
+ 2. **What is installed, when they named nothing.** ``DEFAULT_AGENT_NAME``
126
+ means the Summoner asked for no particular Mvge. With exactly one
127
+ agent in the agent scope, adopt it: one candidate means there is
128
+ nothing to guess wrong, and it is how a first run works with an
129
+ installed Mvge and no flag.
130
+ 3. **Their name, unchanged.** Any name other than the default was a
131
+ request, so it survives even when nothing backs it -- an unknown name
132
+ then surfaces where it is actionable instead of being silently swapped
133
+ for another. Two or more installed agents is the same case: a real
134
+ choice, and an engine that picks for you here is the second precedence
135
+ pass this module exists to end.
136
+ """
137
+ if not isinstance(requested, str) or not AGENT_NAME_PATTERN.match(requested):
138
+ raise ValueError(
139
+ f"Invalid agent name '{requested}'. Agent names must contain "
140
+ "only alphanumeric characters, dashes, and underscores."
141
+ )
142
+
143
+ base = agent_config_base if agent_config_base is not None else agents_dir()
144
+ if (base / requested).is_dir():
145
+ return requested
146
+
147
+ if requested == DEFAULT_AGENT_NAME:
148
+ candidates = installed_agent_names(agent_config_base=agent_config_base)
149
+ if len(candidates) == 1:
150
+ return candidates[0]
151
+
152
+ return requested