monoid-agent-kernel 0.16.1__py3-none-any.whl

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 (191) hide show
  1. monoid_agent_kernel/__init__.py +11 -0
  2. monoid_agent_kernel/_policy_util.py +40 -0
  3. monoid_agent_kernel/_proc.py +86 -0
  4. monoid_agent_kernel/builder.py +351 -0
  5. monoid_agent_kernel/cli.py +1244 -0
  6. monoid_agent_kernel/conformance/__init__.py +32 -0
  7. monoid_agent_kernel/conformance/harness.py +209 -0
  8. monoid_agent_kernel/conformance/profiles/__init__.py +55 -0
  9. monoid_agent_kernel/conformance/profiles/_metadata.py +16 -0
  10. monoid_agent_kernel/conformance/profiles/capability_security.py +216 -0
  11. monoid_agent_kernel/conformance/profiles/control_plane.py +41 -0
  12. monoid_agent_kernel/conformance/profiles/durable_runner.py +42 -0
  13. monoid_agent_kernel/conformance/profiles/message_fabric.py +42 -0
  14. monoid_agent_kernel/conformance/profiles/minimal_agent.py +13 -0
  15. monoid_agent_kernel/conformance/profiles/multi_agent.py +112 -0
  16. monoid_agent_kernel/conformance/profiles/provider_gateway.py +107 -0
  17. monoid_agent_kernel/conformance/profiles/reference_full.py +135 -0
  18. monoid_agent_kernel/conformance/profiles/side_effect_tool_agent.py +36 -0
  19. monoid_agent_kernel/conformance/profiles/tool_agent.py +39 -0
  20. monoid_agent_kernel/contracts.py +329 -0
  21. monoid_agent_kernel/core/__init__.py +1 -0
  22. monoid_agent_kernel/core/_util.py +145 -0
  23. monoid_agent_kernel/core/agents.py +883 -0
  24. monoid_agent_kernel/core/cancellation.py +16 -0
  25. monoid_agent_kernel/core/capability.py +376 -0
  26. monoid_agent_kernel/core/capability_revocation.py +92 -0
  27. monoid_agent_kernel/core/checkpoint.py +350 -0
  28. monoid_agent_kernel/core/content.py +101 -0
  29. monoid_agent_kernel/core/context.py +67 -0
  30. monoid_agent_kernel/core/control.py +124 -0
  31. monoid_agent_kernel/core/control_audit.py +99 -0
  32. monoid_agent_kernel/core/durable_metadata.py +116 -0
  33. monoid_agent_kernel/core/event_sequencing.py +164 -0
  34. monoid_agent_kernel/core/events.py +204 -0
  35. monoid_agent_kernel/core/external_agent_envelope.py +338 -0
  36. monoid_agent_kernel/core/frontmatter.py +140 -0
  37. monoid_agent_kernel/core/inbox.py +110 -0
  38. monoid_agent_kernel/core/lease_admission.py +57 -0
  39. monoid_agent_kernel/core/lifecycle.py +440 -0
  40. monoid_agent_kernel/core/manifest.py +88 -0
  41. monoid_agent_kernel/core/media.py +393 -0
  42. monoid_agent_kernel/core/outbox.py +212 -0
  43. monoid_agent_kernel/core/output_validator.py +103 -0
  44. monoid_agent_kernel/core/packages.py +742 -0
  45. monoid_agent_kernel/core/projections.py +213 -0
  46. monoid_agent_kernel/core/prompt.py +37 -0
  47. monoid_agent_kernel/core/proposal_file.py +84 -0
  48. monoid_agent_kernel/core/result.py +131 -0
  49. monoid_agent_kernel/core/schemas.py +1146 -0
  50. monoid_agent_kernel/core/scope.py +185 -0
  51. monoid_agent_kernel/core/side_effect_policy.py +153 -0
  52. monoid_agent_kernel/core/spec.py +325 -0
  53. monoid_agent_kernel/core/streaming.py +208 -0
  54. monoid_agent_kernel/core/subagent_runtime.py +187 -0
  55. monoid_agent_kernel/core/tool_approval.py +175 -0
  56. monoid_agent_kernel/core/tool_surface.py +505 -0
  57. monoid_agent_kernel/core/trace_context.py +76 -0
  58. monoid_agent_kernel/core/wire_validation.py +156 -0
  59. monoid_agent_kernel/core/workspace.py +161 -0
  60. monoid_agent_kernel/core/workspace_index.py +127 -0
  61. monoid_agent_kernel/env.py +32 -0
  62. monoid_agent_kernel/errors.py +105 -0
  63. monoid_agent_kernel/event_loader.py +47 -0
  64. monoid_agent_kernel/identifiers.py +50 -0
  65. monoid_agent_kernel/loop.py +4140 -0
  66. monoid_agent_kernel/loop_phases.py +561 -0
  67. monoid_agent_kernel/mcp/__init__.py +10 -0
  68. monoid_agent_kernel/mcp/client.py +227 -0
  69. monoid_agent_kernel/mcp/provider.py +407 -0
  70. monoid_agent_kernel/narration.py +92 -0
  71. monoid_agent_kernel/observability/__init__.py +9 -0
  72. monoid_agent_kernel/observability/otel.py +264 -0
  73. monoid_agent_kernel/permissions.py +74 -0
  74. monoid_agent_kernel/providers/__init__.py +7 -0
  75. monoid_agent_kernel/providers/_common.py +122 -0
  76. monoid_agent_kernel/providers/base.py +312 -0
  77. monoid_agent_kernel/providers/fake.py +76 -0
  78. monoid_agent_kernel/providers/gateway.py +474 -0
  79. monoid_agent_kernel/providers/openai.py +487 -0
  80. monoid_agent_kernel/public_view.py +173 -0
  81. monoid_agent_kernel/py.typed +0 -0
  82. monoid_agent_kernel/recorder.py +421 -0
  83. monoid_agent_kernel/reference/__init__.py +15 -0
  84. monoid_agent_kernel/reference/_shared/__init__.py +4 -0
  85. monoid_agent_kernel/reference/_shared/http_util.py +111 -0
  86. monoid_agent_kernel/reference/_shared/tokens.py +314 -0
  87. monoid_agent_kernel/reference/backend/__init__.py +21 -0
  88. monoid_agent_kernel/reference/backend/commands.py +375 -0
  89. monoid_agent_kernel/reference/backend/http.py +439 -0
  90. monoid_agent_kernel/reference/backend/jobs.py +68 -0
  91. monoid_agent_kernel/reference/backend/loop_factory.py +253 -0
  92. monoid_agent_kernel/reference/backend/outbox_dispatch.py +130 -0
  93. monoid_agent_kernel/reference/backend/ports.py +188 -0
  94. monoid_agent_kernel/reference/backend/projection.py +286 -0
  95. monoid_agent_kernel/reference/backend/proposal.py +220 -0
  96. monoid_agent_kernel/reference/backend/proposal_reader.py +16 -0
  97. monoid_agent_kernel/reference/backend/recovery.py +253 -0
  98. monoid_agent_kernel/reference/backend/run_execution.py +152 -0
  99. monoid_agent_kernel/reference/backend/run_preparation.py +197 -0
  100. monoid_agent_kernel/reference/backend/run_state.py +243 -0
  101. monoid_agent_kernel/reference/backend/run_types.py +128 -0
  102. monoid_agent_kernel/reference/backend/runtime_config.py +147 -0
  103. monoid_agent_kernel/reference/backend/service.py +1664 -0
  104. monoid_agent_kernel/reference/backend/session.py +280 -0
  105. monoid_agent_kernel/reference/backend/session_drive.py +231 -0
  106. monoid_agent_kernel/reference/capability.py +101 -0
  107. monoid_agent_kernel/reference/conformance.py +1777 -0
  108. monoid_agent_kernel/reference/llm_gateway/__init__.py +14 -0
  109. monoid_agent_kernel/reference/llm_gateway/http.py +225 -0
  110. monoid_agent_kernel/reference/llm_gateway/providers.py +96 -0
  111. monoid_agent_kernel/reference/llm_gateway/service.py +404 -0
  112. monoid_agent_kernel/reference/mcp_gateway/__init__.py +26 -0
  113. monoid_agent_kernel/reference/mcp_gateway/http.py +187 -0
  114. monoid_agent_kernel/reference/mcp_gateway/service.py +206 -0
  115. monoid_agent_kernel/reference/outbox.py +135 -0
  116. monoid_agent_kernel/reference/stores/__init__.py +20 -0
  117. monoid_agent_kernel/reference/stores/lease.py +116 -0
  118. monoid_agent_kernel/reference/stores/sqlite.py +295 -0
  119. monoid_agent_kernel/reference/studio/README.md +97 -0
  120. monoid_agent_kernel/reference/studio/__init__.py +21 -0
  121. monoid_agent_kernel/reference/studio/activity.py +53 -0
  122. monoid_agent_kernel/reference/studio/cli.py +481 -0
  123. monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/SKILL.md +25 -0
  124. monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/references/review-checklist.md +8 -0
  125. monoid_agent_kernel/reference/studio/sample-skills/commit-message/SKILL.md +32 -0
  126. monoid_agent_kernel/reference/studio/sample-skills/incident-summary/SKILL.md +24 -0
  127. monoid_agent_kernel/reference/studio/sample-skills/incident-summary/references/incident-template.md +32 -0
  128. monoid_agent_kernel/reference/studio/sample-skills/release-notes/SKILL.md +24 -0
  129. monoid_agent_kernel/reference/studio/sample-skills/release-notes/references/release-note-template.md +33 -0
  130. monoid_agent_kernel/reference/studio/sample-skills/release-notes/scripts/collect_changes.py +51 -0
  131. monoid_agent_kernel/reference/studio/server.py +1922 -0
  132. monoid_agent_kernel/reference/studio/web/index.html +1877 -0
  133. monoid_agent_kernel/reference/studio/web/settings.html +108 -0
  134. monoid_agent_kernel/reference/studio/web/vendor/katex/auto-render.min.js +1 -0
  135. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  136. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  137. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  138. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  139. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  140. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  141. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  142. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  143. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  144. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  145. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  146. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  147. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  148. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  149. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  150. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  151. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  152. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  153. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  154. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  155. monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.css +1 -0
  156. monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.js +1 -0
  157. monoid_agent_kernel/reference/studio/window.py +72 -0
  158. monoid_agent_kernel/reference/web_gateway/__init__.py +6 -0
  159. monoid_agent_kernel/reference/web_gateway/http.py +155 -0
  160. monoid_agent_kernel/reference/web_gateway/providers.py +807 -0
  161. monoid_agent_kernel/reference/web_gateway/service.py +559 -0
  162. monoid_agent_kernel/shell.py +722 -0
  163. monoid_agent_kernel/skills/__init__.py +20 -0
  164. monoid_agent_kernel/skills/definition.py +82 -0
  165. monoid_agent_kernel/skills/loader.py +55 -0
  166. monoid_agent_kernel/skills/provider.py +378 -0
  167. monoid_agent_kernel/subagent_loader.py +56 -0
  168. monoid_agent_kernel/tasks.py +1326 -0
  169. monoid_agent_kernel/tool_loader.py +51 -0
  170. monoid_agent_kernel/tool_services/__init__.py +8 -0
  171. monoid_agent_kernel/tool_services/base.py +25 -0
  172. monoid_agent_kernel/tool_services/jobs.py +47 -0
  173. monoid_agent_kernel/tool_services/shell.py +255 -0
  174. monoid_agent_kernel/tool_services/web.py +356 -0
  175. monoid_agent_kernel/tools/__init__.py +2 -0
  176. monoid_agent_kernel/tools/base.py +205 -0
  177. monoid_agent_kernel/tools/builtin.py +958 -0
  178. monoid_agent_kernel/tools/decorator.py +132 -0
  179. monoid_agent_kernel/tools/tool_ids.py +62 -0
  180. monoid_agent_kernel/web.py +171 -0
  181. monoid_agent_kernel/workspace/__init__.py +2 -0
  182. monoid_agent_kernel/workspace/local.py +1004 -0
  183. monoid_agent_kernel/workspace/paths.py +30 -0
  184. monoid_agent_kernel-0.16.1.dist-info/METADATA +709 -0
  185. monoid_agent_kernel-0.16.1.dist-info/RECORD +191 -0
  186. monoid_agent_kernel-0.16.1.dist-info/WHEEL +4 -0
  187. monoid_agent_kernel-0.16.1.dist-info/entry_points.txt +3 -0
  188. monoid_agent_kernel-0.16.1.dist-info/licenses/LICENSE +202 -0
  189. monoid_agent_kernel-0.16.1.dist-info/licenses/NOTICE +8 -0
  190. native_agent_runner/__init__.py +103 -0
  191. native_agent_runner/py.typed +0 -0
@@ -0,0 +1,11 @@
1
+ """Monoid Agent Kernel: the stable contracts and core engine entrypoints.
2
+
3
+ The top-level package mirrors ``monoid_agent_kernel.contracts``. Helper kit,
4
+ provider, recorder, MCP, observability, and reference implementations are imported
5
+ from their explicit modules.
6
+ """
7
+
8
+ from monoid_agent_kernel import contracts as contracts
9
+ from monoid_agent_kernel.contracts import * # noqa: F401,F403
10
+
11
+ __all__ = [*contracts.__all__]
@@ -0,0 +1,40 @@
1
+ """Shared coercion helpers for execution-boundary dataclasses.
2
+
3
+ Used by permission and runtime execution parsers to keep JSON array handling
4
+ consistent. Internal helper.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Iterable
10
+ from typing import Any, Callable
11
+
12
+
13
+ def str_tuple(
14
+ value: Any,
15
+ *,
16
+ type_error: str,
17
+ empty_error: str | None = None,
18
+ normalize: bool = False,
19
+ error: Callable[[str], Exception] = ValueError,
20
+ ) -> tuple[str, ...]:
21
+ """Coerce a JSON array into a tuple of strings.
22
+
23
+ A bare string (or any non-array) is rejected with ``type_error``. With
24
+ ``normalize=True`` each item is stripped, lowercased, and empties are
25
+ dropped (domain lists). Otherwise, when ``empty_error`` is given, an
26
+ empty/whitespace item raises it. ``error`` selects the exception type.
27
+ """
28
+ if isinstance(value, str) or not isinstance(value, (list, tuple)):
29
+ raise error(type_error)
30
+ items = tuple(str(item) for item in value)
31
+ if normalize:
32
+ return tuple(item.strip().lower() for item in items if item.strip())
33
+ if empty_error is not None and any(not item.strip() for item in items):
34
+ raise error(empty_error)
35
+ return items
36
+
37
+
38
+ def dedupe(values: Iterable[str]) -> tuple[str, ...]:
39
+ """Order-preserving de-duplication."""
40
+ return tuple(dict.fromkeys(values))
@@ -0,0 +1,86 @@
1
+ """Low-level process and filesystem primitives shared by shell and jobs.
2
+
3
+ Domain-neutral helpers that both the foreground shell runner (``shell.py``) and
4
+ the background job manager (``jobs.py``) depend on. Internal only; not part of
5
+ the supported public surface.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import signal
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import IO
15
+
16
+
17
+ def file_size(path: Path) -> int:
18
+ """Size of ``path`` in bytes, or ``0`` if it does not exist."""
19
+ try:
20
+ return path.stat().st_size
21
+ except FileNotFoundError:
22
+ return 0
23
+
24
+
25
+ def proc_group_kwargs() -> dict[str, object]:
26
+ """Platform kwargs that put a child in its own process group so the whole tree can
27
+ be terminated together by :func:`terminate_process`. Shared by the sync ``Popen`` and
28
+ the asyncio (``create_subprocess_exec``) spawn paths so they behave identically."""
29
+ if os.name == "nt":
30
+ return {"creationflags": getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)}
31
+ return {"preexec_fn": os.setsid}
32
+
33
+
34
+ def spawn_process(
35
+ argv: list[str],
36
+ *,
37
+ cwd: Path,
38
+ env: dict[str, str],
39
+ stdout: IO[bytes],
40
+ stderr: IO[bytes],
41
+ ) -> subprocess.Popen[bytes]:
42
+ """Start ``argv`` in its own process group with stdin disabled.
43
+
44
+ A new process group (Windows ``CREATE_NEW_PROCESS_GROUP`` / POSIX
45
+ ``setsid``) lets the whole child tree be terminated together by
46
+ :func:`terminate_process`.
47
+ """
48
+ return subprocess.Popen(
49
+ argv,
50
+ cwd=cwd,
51
+ env=env,
52
+ stdin=subprocess.DEVNULL,
53
+ stdout=stdout,
54
+ stderr=stderr,
55
+ **proc_group_kwargs(), # type: ignore[arg-type]
56
+ )
57
+
58
+
59
+ # The external group-killer (``taskkill``) is itself a spawned process; under load/AV its
60
+ # spawn can stall. Bound it so a stalled kill can never block the caller — the foreground
61
+ # shell loop or the background job monitor thread — indefinitely.
62
+ _TERMINATE_TIMEOUT_S = 10.0
63
+
64
+
65
+ def terminate_process(process: subprocess.Popen[bytes]) -> None:
66
+ """Terminate ``process`` and its group, falling back to a direct ``kill`` on error or if
67
+ the group kill does not return promptly. The group kill (Windows ``taskkill``) is bounded
68
+ by a timeout so a stalled killer escalates to ``process.kill()`` instead of hanging."""
69
+ try:
70
+ if os.name == "nt":
71
+ subprocess.run(
72
+ ["taskkill", "/F", "/T", "/PID", str(process.pid)],
73
+ stdin=subprocess.DEVNULL,
74
+ stdout=subprocess.DEVNULL,
75
+ stderr=subprocess.DEVNULL,
76
+ check=False,
77
+ timeout=_TERMINATE_TIMEOUT_S,
78
+ )
79
+ else:
80
+ os.killpg(process.pid, signal.SIGTERM)
81
+ except Exception:
82
+ # taskkill timed out / failed, or killpg failed: terminate the child handle directly.
83
+ try:
84
+ process.kill()
85
+ except Exception:
86
+ pass
@@ -0,0 +1,351 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import replace
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import click
9
+
10
+ from monoid_agent_kernel.core.agents import (
11
+ AgentDefinition,
12
+ AgentRuntimeConfig,
13
+ ToolBinding,
14
+ collect_runtime_config_issues,
15
+ )
16
+ from monoid_agent_kernel.skills import SkillProvider, load_skill_definitions
17
+ from monoid_agent_kernel.subagent_loader import load_subagent_definitions
18
+ from monoid_agent_kernel.tool_loader import load_tool_provider
19
+ from monoid_agent_kernel.tools.base import ToolRegistry, ToolSpec
20
+ from monoid_agent_kernel.tools.builtin import agent_spawn_tool
21
+ from monoid_agent_kernel.tools.tool_ids import list_builtin_tools
22
+
23
+
24
+ @click.group("builder")
25
+ def builder_group() -> None:
26
+ """Scaffold and inspect Monoid builder configuration."""
27
+
28
+
29
+ @builder_group.command("init")
30
+ @click.option("--target", type=click.Path(path_type=Path), required=True)
31
+ @click.option("--force", is_flag=True, help="Overwrite existing scaffold files.")
32
+ @click.option("--custom-tool-template", is_flag=True, help="Also write a tools.py provider template.")
33
+ def builder_init(target: Path, force: bool, custom_tool_template: bool) -> None:
34
+ """Create minimal files for a local monoid run."""
35
+ target.mkdir(parents=True, exist_ok=True)
36
+ files: dict[str, str] = {
37
+ "runtime-config.json": _pretty_json(_default_runtime_config()),
38
+ "run-spec.json": _pretty_json(_default_run_spec()),
39
+ }
40
+ if custom_tool_template:
41
+ files["tools.py"] = _CUSTOM_TOOLS_TEMPLATE
42
+
43
+ paths = {name: target / name for name in files}
44
+ if not force:
45
+ collisions = [path for path in paths.values() if path.exists()]
46
+ if collisions:
47
+ names = ", ".join(str(path) for path in collisions)
48
+ raise click.ClickException(f"{names} already exists; pass --force to overwrite")
49
+
50
+ written: list[Path] = []
51
+ for name, content in files.items():
52
+ path = paths[name]
53
+ path.write_text(content, encoding="utf-8")
54
+ written.append(path)
55
+
56
+ for path in written:
57
+ click.echo(f"created: {path}")
58
+
59
+
60
+ @builder_group.group("config")
61
+ def builder_config_group() -> None:
62
+ """Inspect and validate builder config files."""
63
+
64
+
65
+ @builder_config_group.command("validate")
66
+ @click.option("--runtime-config-file", type=click.Path(path_type=Path), default=None)
67
+ @click.option("--agent-definition-file", type=click.Path(path_type=Path), default=None)
68
+ @click.option("--tool-module", multiple=True, help="Load custom tools from path.py:function.")
69
+ @click.option(
70
+ "--skills-directory",
71
+ type=click.Path(path_type=Path),
72
+ default=None,
73
+ help="Load Agent Skills from a directory.",
74
+ )
75
+ @click.option(
76
+ "--agents-directory",
77
+ type=click.Path(path_type=Path),
78
+ default=None,
79
+ help="Load subagent definitions from a directory.",
80
+ )
81
+ @click.option("--json", "json_output", is_flag=True, help="Print machine-readable JSON.")
82
+ @click.pass_context
83
+ def builder_config_validate(
84
+ ctx: click.Context,
85
+ *,
86
+ runtime_config_file: Path | None,
87
+ agent_definition_file: Path | None,
88
+ tool_module: tuple[str, ...],
89
+ skills_directory: Path | None,
90
+ agents_directory: Path | None,
91
+ json_output: bool,
92
+ ) -> None:
93
+ """Validate a runtime config against the tools the run would register."""
94
+ try:
95
+ config = _load_agent_runtime_config(runtime_config_file, agent_definition_file)
96
+ registry, registration_issues, skill_bindings = _build_tool_registry(
97
+ tool_modules=tool_module,
98
+ skills_directory=skills_directory,
99
+ agents_directory=agents_directory,
100
+ )
101
+ effective_config = (
102
+ replace(config, tools=config.tools + skill_bindings) if skill_bindings else config
103
+ )
104
+ issues = registration_issues + collect_runtime_config_issues(effective_config, registry)
105
+ except click.ClickException:
106
+ raise
107
+ except Exception as exc:
108
+ raise click.ClickException(str(exc)) from exc
109
+
110
+ payload = {
111
+ "ok": not issues,
112
+ "definition_id": config.definition_id,
113
+ "tool_count": len(registry.specs()),
114
+ "bound_tool_count": len(effective_config.tools)
115
+ + (1 if effective_config.tool_search.enabled else 0),
116
+ "issues": issues,
117
+ }
118
+ if json_output:
119
+ click.echo(json.dumps(payload, ensure_ascii=False, sort_keys=True))
120
+ elif issues:
121
+ click.echo("invalid")
122
+ for issue in issues:
123
+ click.echo(f"- {issue}")
124
+ else:
125
+ click.echo("valid")
126
+
127
+ if issues:
128
+ ctx.exit(1)
129
+
130
+
131
+ @builder_group.group("tools")
132
+ def builder_tools_group() -> None:
133
+ """Inspect available builder tools."""
134
+
135
+
136
+ @builder_tools_group.command("list")
137
+ @click.option("--tool-module", multiple=True, help="Load custom tools from path.py:function.")
138
+ @click.option(
139
+ "--skills-directory",
140
+ type=click.Path(path_type=Path),
141
+ default=None,
142
+ help="Load Agent Skills from a directory.",
143
+ )
144
+ @click.option(
145
+ "--runtime-config-file",
146
+ type=click.Path(path_type=Path),
147
+ default=None,
148
+ help="Mark tools bound by a runtime config.",
149
+ )
150
+ @click.option("--json", "json_output", is_flag=True, help="Print machine-readable JSON.")
151
+ def builder_tools_list(
152
+ *,
153
+ tool_module: tuple[str, ...],
154
+ skills_directory: Path | None,
155
+ runtime_config_file: Path | None,
156
+ json_output: bool,
157
+ ) -> None:
158
+ """List builtin, custom, and skill tools."""
159
+ try:
160
+ registry, registration_issues, skill_bindings = _build_tool_registry(
161
+ tool_modules=tool_module,
162
+ skills_directory=skills_directory,
163
+ agents_directory=None,
164
+ )
165
+ if registration_issues:
166
+ raise click.ClickException("; ".join(registration_issues))
167
+ config = _load_runtime_config_file(runtime_config_file) if runtime_config_file else None
168
+ effective_config = (
169
+ replace(config, tools=config.tools + skill_bindings)
170
+ if config is not None and skill_bindings
171
+ else config
172
+ )
173
+ except click.ClickException:
174
+ raise
175
+ except Exception as exc:
176
+ raise click.ClickException(str(exc)) from exc
177
+
178
+ binding_ids_by_tool = _binding_ids_by_tool(effective_config)
179
+ tools = [
180
+ _tool_payload(spec, binding_ids_by_tool.get(spec.id, ()))
181
+ for spec in sorted(registry.specs(), key=lambda item: item.id)
182
+ ]
183
+ payload = {"tools": tools}
184
+ if json_output:
185
+ click.echo(json.dumps(payload, ensure_ascii=False, sort_keys=True))
186
+ return
187
+
188
+ for tool in tools:
189
+ marker = " [bound]" if tool["bound"] else ""
190
+ click.echo(f"{tool['id']}{marker} - {tool['description']}")
191
+
192
+
193
+ def _load_agent_runtime_config(
194
+ runtime_config_file: Path | None,
195
+ agent_definition_file: Path | None,
196
+ ) -> AgentRuntimeConfig:
197
+ if runtime_config_file is not None and agent_definition_file is not None:
198
+ raise click.ClickException("--runtime-config-file and --agent-definition-file are mutually exclusive")
199
+ if runtime_config_file is None and agent_definition_file is None:
200
+ raise click.ClickException("--runtime-config-file or --agent-definition-file is required")
201
+ if runtime_config_file is not None:
202
+ return _load_runtime_config_file(runtime_config_file)
203
+
204
+ assert agent_definition_file is not None
205
+ try:
206
+ payload = json.loads(agent_definition_file.read_text(encoding="utf-8"))
207
+ except json.JSONDecodeError as exc:
208
+ raise click.ClickException(f"invalid agent config JSON: {exc.msg}") from exc
209
+ try:
210
+ return AgentRuntimeConfig.from_definition(AgentDefinition.from_json(payload))
211
+ except Exception as exc:
212
+ raise click.ClickException(f"failed to load agent runtime config: {exc}") from exc
213
+
214
+
215
+ def _load_runtime_config_file(path: Path) -> AgentRuntimeConfig:
216
+ try:
217
+ payload = json.loads(path.read_text(encoding="utf-8"))
218
+ except json.JSONDecodeError as exc:
219
+ raise click.ClickException(f"invalid agent config JSON: {exc.msg}") from exc
220
+ try:
221
+ return AgentRuntimeConfig.from_json(payload)
222
+ except Exception as exc:
223
+ raise click.ClickException(f"failed to load agent runtime config: {exc}") from exc
224
+
225
+
226
+ def _build_tool_registry(
227
+ *,
228
+ tool_modules: tuple[str, ...],
229
+ skills_directory: Path | None,
230
+ agents_directory: Path | None,
231
+ ) -> tuple[ToolRegistry, list[str], tuple[ToolBinding, ...]]:
232
+ registry = ToolRegistry()
233
+ issues: list[str] = []
234
+ skill_bindings: tuple[ToolBinding, ...] = ()
235
+ subagents: dict[str, Any] = {}
236
+
237
+ _register_many(registry, list_builtin_tools(), issues)
238
+
239
+ for item in tool_modules:
240
+ provider = load_tool_provider(item)
241
+ _register_many(registry, provider.get_tools(None), issues) # type: ignore[arg-type]
242
+
243
+ if skills_directory is not None:
244
+ skill_definitions = load_skill_definitions(skills_directory)
245
+ if skill_definitions:
246
+ skill_provider = SkillProvider(skill_definitions)
247
+ skill_tools = tuple(skill_provider.get_tools(None))
248
+ _register_many(registry, skill_tools, issues)
249
+ skill_bindings = skill_provider.tool_bindings()
250
+ subagents.update(skill_provider.subagent_definitions())
251
+
252
+ if agents_directory is not None:
253
+ subagents.update(load_subagent_definitions(agents_directory))
254
+ if subagents:
255
+ _register_many(
256
+ registry,
257
+ [agent_spawn_tool({name: definition.description for name, definition in subagents.items()})],
258
+ issues,
259
+ )
260
+
261
+ return registry, issues, skill_bindings
262
+
263
+
264
+ def _register_many(registry: ToolRegistry, specs: Any, issues: list[str]) -> None:
265
+ for spec in specs:
266
+ try:
267
+ registry.register(spec)
268
+ except ValueError as exc:
269
+ issues.append(str(exc))
270
+
271
+
272
+ def _binding_ids_by_tool(config: AgentRuntimeConfig | None) -> dict[str, tuple[str, ...]]:
273
+ if config is None:
274
+ return {}
275
+ bindings: dict[str, list[str]] = {}
276
+ for binding in config.tools:
277
+ bindings.setdefault(binding.ref.tool_id, []).append(binding.binding_id)
278
+ if config.tool_search.enabled:
279
+ bindings.setdefault("tool.search", []).append(config.tool_search.binding_id)
280
+ return {tool_id: tuple(ids) for tool_id, ids in bindings.items()}
281
+
282
+
283
+ def _tool_payload(spec: ToolSpec, binding_ids: tuple[str, ...]) -> dict[str, Any]:
284
+ return {
285
+ "id": spec.id,
286
+ "exported_name": spec.exported_name,
287
+ "capability": spec.capability,
288
+ "side_effect": spec.side_effect,
289
+ "description": spec.description.split("\n", 1)[0],
290
+ "bound": bool(binding_ids),
291
+ "binding_ids": list(binding_ids),
292
+ }
293
+
294
+
295
+ def _default_runtime_config() -> dict[str, Any]:
296
+ return {
297
+ "definition_id": "builder-agent",
298
+ "config_version": 1,
299
+ "model": {
300
+ "provider": "gateway",
301
+ "model": "gpt-5.5",
302
+ "gateway_url": "http://127.0.0.1:8080/internal/llm/turns",
303
+ "reasoning": {"effort": "low", "summary": "off"},
304
+ },
305
+ "prompt": {
306
+ "persona_segments": ["Work directly in the workspace and keep changes focused."],
307
+ "runtime_segments": [],
308
+ },
309
+ "tools": [
310
+ {"binding_id": "read_file", "model_name": "read_file", "ref": {"kind": "registry", "tool_id": "fs.read"}},
311
+ {
312
+ "binding_id": "write_file",
313
+ "model_name": "write_file",
314
+ "ref": {"kind": "registry", "tool_id": "fs.write"},
315
+ },
316
+ {"binding_id": "finish", "model_name": "finish", "ref": {"kind": "registry", "tool_id": "run.finish"}},
317
+ ],
318
+ "tool_search": {"enabled": True, "top_k": 5},
319
+ }
320
+
321
+
322
+ def _default_run_spec() -> dict[str, Any]:
323
+ return {
324
+ "workspace_root": ".",
325
+ "run_root": "runs",
326
+ "mode": "propose",
327
+ "workspace_backend": "overlay",
328
+ "limits": {"max_steps": 30},
329
+ }
330
+
331
+
332
+ def _pretty_json(payload: dict[str, Any]) -> str:
333
+ return json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
334
+
335
+
336
+ _CUSTOM_TOOLS_TEMPLATE = '''from __future__ import annotations
337
+
338
+ from monoid_agent_kernel import tool
339
+
340
+
341
+ @tool(id="custom.word_count", side_effect="read")
342
+ def word_count(text: str) -> dict:
343
+ """Count words in a text string."""
344
+ words = [part for part in text.split() if part]
345
+ return {"words": len(words)}
346
+
347
+
348
+ def get_tools(context) -> list:
349
+ del context
350
+ return [word_count]
351
+ '''