agentkernel-cli 0.1.0__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 (74) hide show
  1. agentkernel/__init__.py +7 -0
  2. agentkernel/__main__.py +5 -0
  3. agentkernel/agent.py +311 -0
  4. agentkernel/approval/__init__.py +23 -0
  5. agentkernel/approval/base.py +34 -0
  6. agentkernel/approval/cli.py +129 -0
  7. agentkernel/approval/policy.py +58 -0
  8. agentkernel/approval/risk.py +91 -0
  9. agentkernel/approval/sandbox.py +201 -0
  10. agentkernel/budget.py +64 -0
  11. agentkernel/checkpoint.py +50 -0
  12. agentkernel/cli.py +1482 -0
  13. agentkernel/config.py +224 -0
  14. agentkernel/context/__init__.py +17 -0
  15. agentkernel/context/manager.py +216 -0
  16. agentkernel/context/truncate.py +35 -0
  17. agentkernel/cron.py +146 -0
  18. agentkernel/curation.py +183 -0
  19. agentkernel/doctor.py +141 -0
  20. agentkernel/embeddings.py +132 -0
  21. agentkernel/evaluation.py +186 -0
  22. agentkernel/improvement.py +133 -0
  23. agentkernel/insights.py +141 -0
  24. agentkernel/kanban.py +114 -0
  25. agentkernel/knowledge.py +383 -0
  26. agentkernel/loops.py +145 -0
  27. agentkernel/mcp/__init__.py +23 -0
  28. agentkernel/mcp/client.py +181 -0
  29. agentkernel/mcp/config.py +59 -0
  30. agentkernel/mcp/tools.py +96 -0
  31. agentkernel/memory.py +1208 -0
  32. agentkernel/paths.py +73 -0
  33. agentkernel/plugins.py +76 -0
  34. agentkernel/profiles.py +70 -0
  35. agentkernel/progress.py +89 -0
  36. agentkernel/providers/__init__.py +35 -0
  37. agentkernel/providers/_http.py +157 -0
  38. agentkernel/providers/anthropic.py +282 -0
  39. agentkernel/providers/base.py +38 -0
  40. agentkernel/providers/credentials.py +65 -0
  41. agentkernel/providers/local.py +34 -0
  42. agentkernel/providers/openai.py +260 -0
  43. agentkernel/redaction.py +77 -0
  44. agentkernel/semantic_index.py +139 -0
  45. agentkernel/semantic_memory.py +253 -0
  46. agentkernel/skills.py +268 -0
  47. agentkernel/subagent.py +161 -0
  48. agentkernel/telemetry.py +199 -0
  49. agentkernel/templates/README.md +35 -0
  50. agentkernel/templates/SKILL.md +28 -0
  51. agentkernel/templates/eval-suite.toml +22 -0
  52. agentkernel/templates/loop.toml +29 -0
  53. agentkernel/templates/mcp-servers.toml +22 -0
  54. agentkernel/templates/profile.toml +29 -0
  55. agentkernel/templates/tool_module.py +64 -0
  56. agentkernel/tools/__init__.py +5 -0
  57. agentkernel/tools/base.py +100 -0
  58. agentkernel/tools/builtin/__init__.py +37 -0
  59. agentkernel/tools/builtin/checkpoint_tool.py +33 -0
  60. agentkernel/tools/builtin/clarify.py +60 -0
  61. agentkernel/tools/builtin/files.py +221 -0
  62. agentkernel/tools/builtin/kanban_tool.py +100 -0
  63. agentkernel/tools/builtin/search.py +225 -0
  64. agentkernel/tools/builtin/shell.py +67 -0
  65. agentkernel/tools/builtin/todo.py +106 -0
  66. agentkernel/tui/__init__.py +50 -0
  67. agentkernel/tui/app.py +594 -0
  68. agentkernel/types.py +127 -0
  69. agentkernel/worktree.py +64 -0
  70. agentkernel_cli-0.1.0.dist-info/METADATA +426 -0
  71. agentkernel_cli-0.1.0.dist-info/RECORD +74 -0
  72. agentkernel_cli-0.1.0.dist-info/WHEEL +4 -0
  73. agentkernel_cli-0.1.0.dist-info/entry_points.txt +2 -0
  74. agentkernel_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,181 @@
1
+ """A minimal MCP client over JSON-RPC 2.0 stdio (Phase 2).
2
+
3
+ Messages are newline-delimited JSON objects exchanged with the server
4
+ subprocess's stdin/stdout. A background reader thread drains stdout into a
5
+ queue so requests can wait with a timeout (a misbehaving server must never hang
6
+ the agent loop).
7
+
8
+ This client supports exactly what tool discovery and invocation need:
9
+ ``initialize`` → ``notifications/initialized`` → ``tools/list`` / ``tools/call``.
10
+ Server-initiated requests (sampling, roots) are ignored in v1.
11
+
12
+ When ``log_dir`` is provided, each server's stderr is captured to a file
13
+ ``<log_dir>/<server-name>.log`` for post-mortem debugging.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import queue
21
+ import subprocess
22
+ import threading
23
+ import time
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from agentkernel import __version__
28
+ from agentkernel.mcp.config import MCPServerConfig
29
+
30
+ PROTOCOL_VERSION = "2024-11-05"
31
+
32
+
33
+ class MCPError(RuntimeError):
34
+ """An MCP transport or protocol fault (connection, timeout, JSON-RPC error)."""
35
+
36
+
37
+ class MCPClient:
38
+ """Connects to one MCP server over stdio and issues JSON-RPC requests."""
39
+
40
+ def __init__(
41
+ self,
42
+ config: MCPServerConfig,
43
+ *,
44
+ timeout: float = 30.0,
45
+ log_dir: str | None = None,
46
+ ) -> None:
47
+ self._config = config
48
+ self._timeout = timeout
49
+ self._log_dir = log_dir
50
+ self._proc: subprocess.Popen | None = None
51
+ self._reader: threading.Thread | None = None
52
+ self._stderr_reader: threading.Thread | None = None
53
+ self._incoming: queue.Queue[dict[str, Any]] = queue.Queue()
54
+ self._next_id = 0
55
+ self.server_info: dict[str, Any] | None = None
56
+ self.stderr_log_path: str | None = None
57
+
58
+ # --- lifecycle ---------------------------------------------------------
59
+
60
+ def connect(self) -> MCPClient:
61
+ if self._proc is not None:
62
+ raise MCPError(f"MCP client {self._config.name!r} is already connected")
63
+ env = {**os.environ, **(self._config.env or {})}
64
+
65
+ stderr_target: int = subprocess.DEVNULL
66
+ if self._log_dir:
67
+ log_path = Path(self._log_dir)
68
+ log_path.mkdir(parents=True, exist_ok=True)
69
+ self.stderr_log_path = str(log_path / f"{self._config.name}.log")
70
+ stderr_target = subprocess.PIPE
71
+
72
+ try:
73
+ self._proc = subprocess.Popen(
74
+ [self._config.command, *self._config.args],
75
+ stdin=subprocess.PIPE,
76
+ stdout=subprocess.PIPE,
77
+ stderr=stderr_target,
78
+ text=True,
79
+ bufsize=1,
80
+ env=env,
81
+ cwd=self._config.cwd,
82
+ )
83
+ except (OSError, ValueError) as exc:
84
+ raise MCPError(f"failed to launch MCP server {self._config.name!r}: {exc}")
85
+
86
+ self._reader = threading.Thread(target=self._read_loop, daemon=True)
87
+ self._reader.start()
88
+
89
+ if self.stderr_log_path is not None and self._proc.stderr is not None:
90
+ self._stderr_reader = threading.Thread(
91
+ target=self._read_loop_stderr,
92
+ args=(self.stderr_log_path, self._proc.stderr),
93
+ daemon=True,
94
+ )
95
+ self._stderr_reader.start()
96
+
97
+ result = self._request(
98
+ "initialize",
99
+ {
100
+ "protocolVersion": PROTOCOL_VERSION,
101
+ "capabilities": {},
102
+ "clientInfo": {"name": "agentkernel", "version": __version__},
103
+ },
104
+ )
105
+ self.server_info = result.get("serverInfo")
106
+ self._notify("notifications/initialized", {})
107
+ return self
108
+
109
+ def close(self) -> None:
110
+ proc, self._proc = self._proc, None
111
+ if proc is None or proc.poll() is not None:
112
+ return
113
+ try:
114
+ proc.terminate()
115
+ proc.wait(timeout=5)
116
+ except Exception: # noqa: BLE001 - best effort shutdown
117
+ proc.kill()
118
+ # Give the stderr logger a moment to drain and flush.
119
+ if self._stderr_reader is not None:
120
+ self._stderr_reader.join(timeout=1.0)
121
+
122
+ # --- MCP operations ----------------------------------------------------
123
+
124
+ def list_tools(self) -> list[dict[str, Any]]:
125
+ return self._request("tools/list", {}).get("tools", [])
126
+
127
+ def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
128
+ return self._request("tools/call", {"name": name, "arguments": arguments})
129
+
130
+ # --- JSON-RPC plumbing -------------------------------------------------
131
+
132
+ def _read_loop(self) -> None:
133
+ assert self._proc is not None and self._proc.stdout is not None
134
+ for line in self._proc.stdout:
135
+ line = line.strip()
136
+ if not line:
137
+ continue
138
+ try:
139
+ self._incoming.put(json.loads(line))
140
+ except json.JSONDecodeError:
141
+ continue # ignore non-JSON noise on stdout
142
+
143
+ def _read_loop_stderr(self, log_path: str, stream) -> None:
144
+ with open(log_path, "a", encoding="utf-8") as fh:
145
+ for line in stream:
146
+ fh.write(line)
147
+ fh.flush()
148
+
149
+ def _send(self, message: dict[str, Any]) -> None:
150
+ if self._proc is None or self._proc.poll() is not None:
151
+ raise MCPError(f"MCP server {self._config.name!r} is not running")
152
+ assert self._proc.stdin is not None
153
+ self._proc.stdin.write(json.dumps(message) + "\n")
154
+ self._proc.stdin.flush()
155
+
156
+ def _notify(self, method: str, params: dict[str, Any]) -> None:
157
+ self._send({"jsonrpc": "2.0", "method": method, "params": params})
158
+
159
+ def _request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
160
+ request_id = self._next_id
161
+ self._next_id += 1
162
+ self._send({"jsonrpc": "2.0", "id": request_id, "method": method, "params": params})
163
+
164
+ deadline = time.monotonic() + self._timeout
165
+ while True:
166
+ remaining = deadline - time.monotonic()
167
+ if remaining <= 0:
168
+ raise MCPError(f"timed out waiting for response to {method!r}")
169
+ try:
170
+ msg = self._incoming.get(timeout=remaining)
171
+ except queue.Empty:
172
+ raise MCPError(f"timed out waiting for response to {method!r}")
173
+ # Skip server-initiated requests/notifications and stale responses.
174
+ if msg.get("method") is not None or msg.get("id") != request_id:
175
+ continue
176
+ if "error" in msg:
177
+ err = msg["error"]
178
+ raise MCPError(
179
+ f"{method!r} failed: {err.get('message')} (code {err.get('code')})"
180
+ )
181
+ return msg.get("result", {})
@@ -0,0 +1,59 @@
1
+ """MCP server declarations (Phase 2).
2
+
3
+ Servers are declared in the config TOML as an array of tables:
4
+
5
+ [[mcp_servers]]
6
+ name = "filesystem"
7
+ command = "npx"
8
+ args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
9
+ timeout = 60
10
+
11
+ [[mcp_servers]]
12
+ name = "git"
13
+ command = "uvx"
14
+ args = ["mcp-server-git"]
15
+ env = { GIT_AUTHOR_NAME = "agent" }
16
+
17
+ This loader is kept separate from ``Config`` because the scalar config loader
18
+ (``config.py``) only handles flat values, while server definitions are nested.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import tomllib
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+
27
+
28
+ @dataclass
29
+ class MCPServerConfig:
30
+ """How to launch and connect to one MCP server over stdio."""
31
+
32
+ name: str
33
+ command: str
34
+ args: list[str] = field(default_factory=list)
35
+ env: dict[str, str] | None = None
36
+ cwd: str | None = None
37
+ timeout: float | None = None # seconds; falls back to MCPClient default (30)
38
+
39
+
40
+ def load_mcp_servers(config_file: str | Path) -> list[MCPServerConfig]:
41
+ """Read ``[[mcp_servers]]`` from the config TOML. Missing file → no servers."""
42
+ path = Path(config_file)
43
+ if not path.is_file():
44
+ return []
45
+ with path.open("rb") as fh:
46
+ data = tomllib.load(fh)
47
+ servers: list[MCPServerConfig] = []
48
+ for entry in data.get("mcp_servers", []):
49
+ servers.append(
50
+ MCPServerConfig(
51
+ name=entry["name"],
52
+ command=entry["command"],
53
+ args=list(entry.get("args", [])),
54
+ env=entry.get("env"),
55
+ cwd=entry.get("cwd"),
56
+ timeout=entry.get("timeout"),
57
+ )
58
+ )
59
+ return servers
@@ -0,0 +1,96 @@
1
+ """Adapt MCP tools into the kernel's ``ToolSpec`` (Phase 2, design §13).
2
+
3
+ Each discovered MCP tool becomes a ``ToolSpec`` whose handler issues a
4
+ ``tools/call`` and maps the MCP result back to a canonical ``ToolResult``. The
5
+ registry and loop are untouched — this module is the entire MCP seam.
6
+
7
+ Gating: MCP tools can do anything, so they default to ``requires_approval``.
8
+ A tool that advertises ``annotations.readOnlyHint`` is treated as read-only and
9
+ left ungated.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from agentkernel.mcp.client import MCPClient, MCPError
17
+ from agentkernel.tools.base import ToolSpec
18
+ from agentkernel.types import ToolResult
19
+
20
+ if TYPE_CHECKING:
21
+ from agentkernel.mcp.config import MCPServerConfig
22
+ from agentkernel.tools import ToolRegistry
23
+
24
+
25
+ def _result_to_tool_result(result: dict[str, Any]) -> ToolResult:
26
+ """Flatten an MCP call result's content blocks into a ToolResult."""
27
+ text_parts = [
28
+ block.get("text", "")
29
+ for block in result.get("content", [])
30
+ if block.get("type") == "text"
31
+ ]
32
+ content = "\n".join(p for p in text_parts if p) or "(no text content)"
33
+ return ToolResult(
34
+ "",
35
+ content,
36
+ is_error=bool(result.get("isError", False)),
37
+ data={"mcp_result": result},
38
+ )
39
+
40
+
41
+ def _make_handler(client: MCPClient, name: str):
42
+ def handler(arguments: dict[str, Any]) -> ToolResult:
43
+ try:
44
+ result = client.call_tool(name, arguments)
45
+ except MCPError as exc:
46
+ # Transport/protocol faults become error results, not raises, so the
47
+ # model can recover (design §8.3).
48
+ return ToolResult("", f"MCP error: {exc}", is_error=True)
49
+ return _result_to_tool_result(result)
50
+
51
+ return handler
52
+
53
+
54
+ def mcp_tool_specs(client: MCPClient) -> list[ToolSpec]:
55
+ """Discover the client's tools and wrap each as a ToolSpec."""
56
+ specs: list[ToolSpec] = []
57
+ for tool in client.list_tools():
58
+ annotations = tool.get("annotations") or {}
59
+ read_only = bool(annotations.get("readOnlyHint", False))
60
+ specs.append(
61
+ ToolSpec(
62
+ name=tool["name"],
63
+ description=tool.get("description", ""),
64
+ parameters=tool.get("inputSchema") or {"type": "object"},
65
+ handler=_make_handler(client, tool["name"]),
66
+ requires_approval=not read_only,
67
+ mutates=not read_only,
68
+ category="mcp",
69
+ )
70
+ )
71
+ return specs
72
+
73
+
74
+ def register_mcp_servers(
75
+ registry: ToolRegistry, servers: list[MCPServerConfig], log_dir: str | None = None
76
+ ) -> list[MCPClient]:
77
+ """Connect each server and register its tools. Returns the open clients so
78
+ the caller can close them. On any failure, already-opened clients are
79
+ closed before the error propagates.
80
+
81
+ ``log_dir`` optionally captures each server's stderr to
82
+ ``<log_dir>/<server-name>.log``."""
83
+ clients: list[MCPClient] = []
84
+ try:
85
+ for server in servers:
86
+ client = MCPClient(
87
+ server, timeout=server.timeout or 30.0, log_dir=log_dir
88
+ ).connect()
89
+ clients.append(client)
90
+ for spec in mcp_tool_specs(client):
91
+ registry.register(spec)
92
+ except Exception:
93
+ for client in clients:
94
+ client.close()
95
+ raise
96
+ return clients