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.
- monoid_agent_kernel/__init__.py +11 -0
- monoid_agent_kernel/_policy_util.py +40 -0
- monoid_agent_kernel/_proc.py +86 -0
- monoid_agent_kernel/builder.py +351 -0
- monoid_agent_kernel/cli.py +1244 -0
- monoid_agent_kernel/conformance/__init__.py +32 -0
- monoid_agent_kernel/conformance/harness.py +209 -0
- monoid_agent_kernel/conformance/profiles/__init__.py +55 -0
- monoid_agent_kernel/conformance/profiles/_metadata.py +16 -0
- monoid_agent_kernel/conformance/profiles/capability_security.py +216 -0
- monoid_agent_kernel/conformance/profiles/control_plane.py +41 -0
- monoid_agent_kernel/conformance/profiles/durable_runner.py +42 -0
- monoid_agent_kernel/conformance/profiles/message_fabric.py +42 -0
- monoid_agent_kernel/conformance/profiles/minimal_agent.py +13 -0
- monoid_agent_kernel/conformance/profiles/multi_agent.py +112 -0
- monoid_agent_kernel/conformance/profiles/provider_gateway.py +107 -0
- monoid_agent_kernel/conformance/profiles/reference_full.py +135 -0
- monoid_agent_kernel/conformance/profiles/side_effect_tool_agent.py +36 -0
- monoid_agent_kernel/conformance/profiles/tool_agent.py +39 -0
- monoid_agent_kernel/contracts.py +329 -0
- monoid_agent_kernel/core/__init__.py +1 -0
- monoid_agent_kernel/core/_util.py +145 -0
- monoid_agent_kernel/core/agents.py +883 -0
- monoid_agent_kernel/core/cancellation.py +16 -0
- monoid_agent_kernel/core/capability.py +376 -0
- monoid_agent_kernel/core/capability_revocation.py +92 -0
- monoid_agent_kernel/core/checkpoint.py +350 -0
- monoid_agent_kernel/core/content.py +101 -0
- monoid_agent_kernel/core/context.py +67 -0
- monoid_agent_kernel/core/control.py +124 -0
- monoid_agent_kernel/core/control_audit.py +99 -0
- monoid_agent_kernel/core/durable_metadata.py +116 -0
- monoid_agent_kernel/core/event_sequencing.py +164 -0
- monoid_agent_kernel/core/events.py +204 -0
- monoid_agent_kernel/core/external_agent_envelope.py +338 -0
- monoid_agent_kernel/core/frontmatter.py +140 -0
- monoid_agent_kernel/core/inbox.py +110 -0
- monoid_agent_kernel/core/lease_admission.py +57 -0
- monoid_agent_kernel/core/lifecycle.py +440 -0
- monoid_agent_kernel/core/manifest.py +88 -0
- monoid_agent_kernel/core/media.py +393 -0
- monoid_agent_kernel/core/outbox.py +212 -0
- monoid_agent_kernel/core/output_validator.py +103 -0
- monoid_agent_kernel/core/packages.py +742 -0
- monoid_agent_kernel/core/projections.py +213 -0
- monoid_agent_kernel/core/prompt.py +37 -0
- monoid_agent_kernel/core/proposal_file.py +84 -0
- monoid_agent_kernel/core/result.py +131 -0
- monoid_agent_kernel/core/schemas.py +1146 -0
- monoid_agent_kernel/core/scope.py +185 -0
- monoid_agent_kernel/core/side_effect_policy.py +153 -0
- monoid_agent_kernel/core/spec.py +325 -0
- monoid_agent_kernel/core/streaming.py +208 -0
- monoid_agent_kernel/core/subagent_runtime.py +187 -0
- monoid_agent_kernel/core/tool_approval.py +175 -0
- monoid_agent_kernel/core/tool_surface.py +505 -0
- monoid_agent_kernel/core/trace_context.py +76 -0
- monoid_agent_kernel/core/wire_validation.py +156 -0
- monoid_agent_kernel/core/workspace.py +161 -0
- monoid_agent_kernel/core/workspace_index.py +127 -0
- monoid_agent_kernel/env.py +32 -0
- monoid_agent_kernel/errors.py +105 -0
- monoid_agent_kernel/event_loader.py +47 -0
- monoid_agent_kernel/identifiers.py +50 -0
- monoid_agent_kernel/loop.py +4140 -0
- monoid_agent_kernel/loop_phases.py +561 -0
- monoid_agent_kernel/mcp/__init__.py +10 -0
- monoid_agent_kernel/mcp/client.py +227 -0
- monoid_agent_kernel/mcp/provider.py +407 -0
- monoid_agent_kernel/narration.py +92 -0
- monoid_agent_kernel/observability/__init__.py +9 -0
- monoid_agent_kernel/observability/otel.py +264 -0
- monoid_agent_kernel/permissions.py +74 -0
- monoid_agent_kernel/providers/__init__.py +7 -0
- monoid_agent_kernel/providers/_common.py +122 -0
- monoid_agent_kernel/providers/base.py +312 -0
- monoid_agent_kernel/providers/fake.py +76 -0
- monoid_agent_kernel/providers/gateway.py +474 -0
- monoid_agent_kernel/providers/openai.py +487 -0
- monoid_agent_kernel/public_view.py +173 -0
- monoid_agent_kernel/py.typed +0 -0
- monoid_agent_kernel/recorder.py +421 -0
- monoid_agent_kernel/reference/__init__.py +15 -0
- monoid_agent_kernel/reference/_shared/__init__.py +4 -0
- monoid_agent_kernel/reference/_shared/http_util.py +111 -0
- monoid_agent_kernel/reference/_shared/tokens.py +314 -0
- monoid_agent_kernel/reference/backend/__init__.py +21 -0
- monoid_agent_kernel/reference/backend/commands.py +375 -0
- monoid_agent_kernel/reference/backend/http.py +439 -0
- monoid_agent_kernel/reference/backend/jobs.py +68 -0
- monoid_agent_kernel/reference/backend/loop_factory.py +253 -0
- monoid_agent_kernel/reference/backend/outbox_dispatch.py +130 -0
- monoid_agent_kernel/reference/backend/ports.py +188 -0
- monoid_agent_kernel/reference/backend/projection.py +286 -0
- monoid_agent_kernel/reference/backend/proposal.py +220 -0
- monoid_agent_kernel/reference/backend/proposal_reader.py +16 -0
- monoid_agent_kernel/reference/backend/recovery.py +253 -0
- monoid_agent_kernel/reference/backend/run_execution.py +152 -0
- monoid_agent_kernel/reference/backend/run_preparation.py +197 -0
- monoid_agent_kernel/reference/backend/run_state.py +243 -0
- monoid_agent_kernel/reference/backend/run_types.py +128 -0
- monoid_agent_kernel/reference/backend/runtime_config.py +147 -0
- monoid_agent_kernel/reference/backend/service.py +1664 -0
- monoid_agent_kernel/reference/backend/session.py +280 -0
- monoid_agent_kernel/reference/backend/session_drive.py +231 -0
- monoid_agent_kernel/reference/capability.py +101 -0
- monoid_agent_kernel/reference/conformance.py +1777 -0
- monoid_agent_kernel/reference/llm_gateway/__init__.py +14 -0
- monoid_agent_kernel/reference/llm_gateway/http.py +225 -0
- monoid_agent_kernel/reference/llm_gateway/providers.py +96 -0
- monoid_agent_kernel/reference/llm_gateway/service.py +404 -0
- monoid_agent_kernel/reference/mcp_gateway/__init__.py +26 -0
- monoid_agent_kernel/reference/mcp_gateway/http.py +187 -0
- monoid_agent_kernel/reference/mcp_gateway/service.py +206 -0
- monoid_agent_kernel/reference/outbox.py +135 -0
- monoid_agent_kernel/reference/stores/__init__.py +20 -0
- monoid_agent_kernel/reference/stores/lease.py +116 -0
- monoid_agent_kernel/reference/stores/sqlite.py +295 -0
- monoid_agent_kernel/reference/studio/README.md +97 -0
- monoid_agent_kernel/reference/studio/__init__.py +21 -0
- monoid_agent_kernel/reference/studio/activity.py +53 -0
- monoid_agent_kernel/reference/studio/cli.py +481 -0
- monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/SKILL.md +25 -0
- monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/references/review-checklist.md +8 -0
- monoid_agent_kernel/reference/studio/sample-skills/commit-message/SKILL.md +32 -0
- monoid_agent_kernel/reference/studio/sample-skills/incident-summary/SKILL.md +24 -0
- monoid_agent_kernel/reference/studio/sample-skills/incident-summary/references/incident-template.md +32 -0
- monoid_agent_kernel/reference/studio/sample-skills/release-notes/SKILL.md +24 -0
- monoid_agent_kernel/reference/studio/sample-skills/release-notes/references/release-note-template.md +33 -0
- monoid_agent_kernel/reference/studio/sample-skills/release-notes/scripts/collect_changes.py +51 -0
- monoid_agent_kernel/reference/studio/server.py +1922 -0
- monoid_agent_kernel/reference/studio/web/index.html +1877 -0
- monoid_agent_kernel/reference/studio/web/settings.html +108 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/auto-render.min.js +1 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.css +1 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.js +1 -0
- monoid_agent_kernel/reference/studio/window.py +72 -0
- monoid_agent_kernel/reference/web_gateway/__init__.py +6 -0
- monoid_agent_kernel/reference/web_gateway/http.py +155 -0
- monoid_agent_kernel/reference/web_gateway/providers.py +807 -0
- monoid_agent_kernel/reference/web_gateway/service.py +559 -0
- monoid_agent_kernel/shell.py +722 -0
- monoid_agent_kernel/skills/__init__.py +20 -0
- monoid_agent_kernel/skills/definition.py +82 -0
- monoid_agent_kernel/skills/loader.py +55 -0
- monoid_agent_kernel/skills/provider.py +378 -0
- monoid_agent_kernel/subagent_loader.py +56 -0
- monoid_agent_kernel/tasks.py +1326 -0
- monoid_agent_kernel/tool_loader.py +51 -0
- monoid_agent_kernel/tool_services/__init__.py +8 -0
- monoid_agent_kernel/tool_services/base.py +25 -0
- monoid_agent_kernel/tool_services/jobs.py +47 -0
- monoid_agent_kernel/tool_services/shell.py +255 -0
- monoid_agent_kernel/tool_services/web.py +356 -0
- monoid_agent_kernel/tools/__init__.py +2 -0
- monoid_agent_kernel/tools/base.py +205 -0
- monoid_agent_kernel/tools/builtin.py +958 -0
- monoid_agent_kernel/tools/decorator.py +132 -0
- monoid_agent_kernel/tools/tool_ids.py +62 -0
- monoid_agent_kernel/web.py +171 -0
- monoid_agent_kernel/workspace/__init__.py +2 -0
- monoid_agent_kernel/workspace/local.py +1004 -0
- monoid_agent_kernel/workspace/paths.py +30 -0
- monoid_agent_kernel-0.16.1.dist-info/METADATA +709 -0
- monoid_agent_kernel-0.16.1.dist-info/RECORD +191 -0
- monoid_agent_kernel-0.16.1.dist-info/WHEEL +4 -0
- monoid_agent_kernel-0.16.1.dist-info/entry_points.txt +3 -0
- monoid_agent_kernel-0.16.1.dist-info/licenses/LICENSE +202 -0
- monoid_agent_kernel-0.16.1.dist-info/licenses/NOTICE +8 -0
- native_agent_runner/__init__.py +103 -0
- 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
|
+
'''
|