krnl-code 1.0.4__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.
- krnl_agent/__init__.py +9 -0
- krnl_agent/__main__.py +7 -0
- krnl_agent/agent_registry.py +95 -0
- krnl_agent/agent_selector.py +69 -0
- krnl_agent/audit_log.py +155 -0
- krnl_agent/background.py +94 -0
- krnl_agent/checkpoints.py +67 -0
- krnl_agent/ci.py +73 -0
- krnl_agent/cli.py +1458 -0
- krnl_agent/commands.py +42 -0
- krnl_agent/config.py +425 -0
- krnl_agent/context.py +352 -0
- krnl_agent/depaudit.py +63 -0
- krnl_agent/deploy.py +245 -0
- krnl_agent/doctor.py +106 -0
- krnl_agent/events.py +141 -0
- krnl_agent/gitignore.py +47 -0
- krnl_agent/graph.py +928 -0
- krnl_agent/guardrails.py +70 -0
- krnl_agent/headless.py +60 -0
- krnl_agent/history.py +49 -0
- krnl_agent/hooks.py +72 -0
- krnl_agent/ingest.py +129 -0
- krnl_agent/llm.py +456 -0
- krnl_agent/loop.py +779 -0
- krnl_agent/mcp_client.py +128 -0
- krnl_agent/memory.py +61 -0
- krnl_agent/modelrouter.py +151 -0
- krnl_agent/monitor.py +112 -0
- krnl_agent/notify.py +119 -0
- krnl_agent/parallel_executor.py +139 -0
- krnl_agent/permissions.py +128 -0
- krnl_agent/plugins.py +105 -0
- krnl_agent/pricing.py +85 -0
- krnl_agent/prompts.py +60 -0
- krnl_agent/repomap.py +133 -0
- krnl_agent/sandbox.py +69 -0
- krnl_agent/scaffold.py +167 -0
- krnl_agent/schedules.py +137 -0
- krnl_agent/secrets.py +100 -0
- krnl_agent/selfheal.py +87 -0
- krnl_agent/server.py +302 -0
- krnl_agent/sessions.py +258 -0
- krnl_agent/settings.py +59 -0
- krnl_agent/skills.py +73 -0
- krnl_agent/teams.py +38 -0
- krnl_agent/tool_schemas.py +431 -0
- krnl_agent/tools.py +694 -0
- krnl_agent/webtools.py +139 -0
- krnl_code-1.0.4.dist-info/METADATA +214 -0
- krnl_code-1.0.4.dist-info/RECORD +56 -0
- krnl_code-1.0.4.dist-info/WHEEL +5 -0
- krnl_code-1.0.4.dist-info/entry_points.txt +2 -0
- krnl_code-1.0.4.dist-info/licenses/LICENSE +147 -0
- krnl_code-1.0.4.dist-info/licenses/NOTICE +4 -0
- krnl_code-1.0.4.dist-info/top_level.txt +1 -0
krnl_agent/doctor.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""`krnl-agent doctor` — environment self-check.
|
|
2
|
+
|
|
3
|
+
Checks the things that commonly break a setup (Python version, package version, a
|
|
4
|
+
configured provider + reachable key, optional tools like git/ripgrep/gh, and the
|
|
5
|
+
writable user-data dir) and prints a clear PASS/WARN/FAIL report with a fix hint
|
|
6
|
+
for anything wrong. Read-only and safe to run anytime.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .config import Config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Check:
|
|
20
|
+
def __init__(self, name: str, status: str, detail: str = "", hint: str = ""):
|
|
21
|
+
self.name = name
|
|
22
|
+
self.status = status # "pass" | "warn" | "fail"
|
|
23
|
+
self.detail = detail
|
|
24
|
+
self.hint = hint
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_checks(config: Config | None = None, workspace: str = ".") -> list[Check]:
|
|
28
|
+
checks: list[Check] = []
|
|
29
|
+
|
|
30
|
+
# Python version
|
|
31
|
+
v = sys.version_info
|
|
32
|
+
if v >= (3, 9):
|
|
33
|
+
checks.append(Check("Python", "pass", f"{v.major}.{v.minor}.{v.micro}"))
|
|
34
|
+
else:
|
|
35
|
+
checks.append(Check("Python", "fail", f"{v.major}.{v.minor}",
|
|
36
|
+
"Python 3.9+ is required."))
|
|
37
|
+
|
|
38
|
+
checks.append(Check("krnl-coding-agent", "pass", f"v{__version__}"))
|
|
39
|
+
|
|
40
|
+
# Provider + key
|
|
41
|
+
if config is not None:
|
|
42
|
+
p = config.provider
|
|
43
|
+
checks.append(Check("Active provider", "pass", f"{p.name} ({p.model})"))
|
|
44
|
+
needs_key = bool(p.api_key_env) # local providers (ollama/lmstudio) need none
|
|
45
|
+
if not needs_key:
|
|
46
|
+
checks.append(Check("API key", "pass", "not required (local provider)"))
|
|
47
|
+
elif p.api_key:
|
|
48
|
+
checks.append(Check("API key", "pass", f"set via {p.api_key_env}"))
|
|
49
|
+
else:
|
|
50
|
+
checks.append(Check("API key", "warn", f"{p.api_key_env} not set",
|
|
51
|
+
f"export {p.api_key_env}=… or run `/key` in chat."))
|
|
52
|
+
if p.base_url:
|
|
53
|
+
checks.append(Check("Base URL", "pass", p.base_url))
|
|
54
|
+
|
|
55
|
+
# Optional external tools
|
|
56
|
+
for exe, why in [
|
|
57
|
+
("git", "version control + git tools"),
|
|
58
|
+
("rg", "fast search (ripgrep)"),
|
|
59
|
+
("gh", "open pull requests"),
|
|
60
|
+
("pip-audit", "Python dependency audit"),
|
|
61
|
+
]:
|
|
62
|
+
if shutil.which(exe):
|
|
63
|
+
checks.append(Check(f"tool: {exe}", "pass", "found"))
|
|
64
|
+
else:
|
|
65
|
+
checks.append(Check(f"tool: {exe}", "warn", "not found",
|
|
66
|
+
f"optional — install for {why}."))
|
|
67
|
+
|
|
68
|
+
# Writable user-data dir
|
|
69
|
+
data_dir = Path(os.path.expanduser("~")) / ".krnl-agent"
|
|
70
|
+
try:
|
|
71
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
probe = data_dir / ".doctor_probe"
|
|
73
|
+
probe.write_text("ok", encoding="utf-8")
|
|
74
|
+
probe.unlink()
|
|
75
|
+
checks.append(Check("User data dir", "pass", str(data_dir)))
|
|
76
|
+
except Exception as e: # noqa: BLE001
|
|
77
|
+
checks.append(Check("User data dir", "fail", str(e),
|
|
78
|
+
"Check permissions on your home directory."))
|
|
79
|
+
|
|
80
|
+
# Workspace writability
|
|
81
|
+
ws = Path(workspace)
|
|
82
|
+
checks.append(Check("Workspace", "pass" if os.access(ws, os.W_OK) else "warn",
|
|
83
|
+
str(ws.resolve()),
|
|
84
|
+
"" if os.access(ws, os.W_OK) else "workspace is not writable."))
|
|
85
|
+
return checks
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def format_report(checks: list[Check]) -> str:
|
|
89
|
+
icon = {"pass": "✔", "warn": "⚠", "fail": "✗"}
|
|
90
|
+
lines = ["Krnl Agent — environment check", ""]
|
|
91
|
+
for c in checks:
|
|
92
|
+
mark = icon.get(c.status, "?")
|
|
93
|
+
line = f" {mark} {c.name}: {c.detail}".rstrip()
|
|
94
|
+
lines.append(line)
|
|
95
|
+
if c.hint and c.status != "pass":
|
|
96
|
+
lines.append(f" ↳ {c.hint}")
|
|
97
|
+
fails = sum(1 for c in checks if c.status == "fail")
|
|
98
|
+
warns = sum(1 for c in checks if c.status == "warn")
|
|
99
|
+
lines.append("")
|
|
100
|
+
if fails:
|
|
101
|
+
lines.append(f"Result: {fails} failure(s), {warns} warning(s) — fix failures above.")
|
|
102
|
+
elif warns:
|
|
103
|
+
lines.append(f"Result: all critical checks passed, {warns} optional warning(s).")
|
|
104
|
+
else:
|
|
105
|
+
lines.append("Result: all checks passed. You're good to go.")
|
|
106
|
+
return "\n".join(lines)
|
krnl_agent/events.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Event protocol and the AgentIO interface.
|
|
2
|
+
|
|
3
|
+
The agent loop is transport-agnostic: it talks to the outside world only
|
|
4
|
+
through an `AgentIO` implementation. The CLI implements it with the terminal;
|
|
5
|
+
the FastAPI server implements it over a WebSocket. Both speak the same set of
|
|
6
|
+
event dicts so the VS Code webview and the CLI render identical information.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import abc
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# --------------------------------------------------------------------------- #
|
|
16
|
+
# Event constructors (plain dicts so they serialize straight to JSON)
|
|
17
|
+
# --------------------------------------------------------------------------- #
|
|
18
|
+
def status(message: str) -> dict:
|
|
19
|
+
return {"type": "status", "message": message}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def token(text: str) -> dict:
|
|
23
|
+
"""A streamed chunk of assistant text."""
|
|
24
|
+
return {"type": "token", "text": text}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def assistant_message(text: str) -> dict:
|
|
28
|
+
"""A complete assistant message (sent once a turn finishes)."""
|
|
29
|
+
return {"type": "assistant_message", "text": text}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def tool_start(call_id: str, name: str, args: dict) -> dict:
|
|
33
|
+
return {"type": "tool_start", "id": call_id, "name": name, "args": args}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def tool_result(call_id: str, name: str, ok: bool, output: str) -> dict:
|
|
37
|
+
return {"type": "tool_result", "id": call_id, "name": name, "ok": ok, "output": output}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def diff(path: str, patch: str, is_new: bool = False) -> dict:
|
|
41
|
+
return {"type": "diff", "path": path, "patch": patch, "is_new": is_new}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def approval_request(call_id: str, name: str, args: dict, preview: Optional[str]) -> dict:
|
|
45
|
+
return {
|
|
46
|
+
"type": "approval_request",
|
|
47
|
+
"id": call_id,
|
|
48
|
+
"name": name,
|
|
49
|
+
"args": args,
|
|
50
|
+
"preview": preview,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def done(summary: str = "") -> dict:
|
|
55
|
+
return {"type": "done", "summary": summary}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def error(message: str) -> dict:
|
|
59
|
+
return {"type": "error", "message": message}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def command_output(call_id: str, text: str) -> dict:
|
|
63
|
+
"""A streamed chunk of stdout/stderr from a running command."""
|
|
64
|
+
return {"type": "command_output", "id": call_id, "text": text}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def usage(prompt: int, completion: int, total_session: int, cost: float = 0.0) -> dict:
|
|
68
|
+
return {
|
|
69
|
+
"type": "usage",
|
|
70
|
+
"prompt_tokens": prompt,
|
|
71
|
+
"completion_tokens": completion,
|
|
72
|
+
"session_tokens": total_session,
|
|
73
|
+
"session_cost": round(cost, 4),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cancelled() -> dict:
|
|
78
|
+
return {"type": "cancelled"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def info(message: str) -> dict:
|
|
82
|
+
"""A neutral notice (compaction, checkpoint, undo, …)."""
|
|
83
|
+
return {"type": "info", "message": message}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def thinking(text: str) -> dict:
|
|
87
|
+
"""A streamed chunk of the model's reasoning."""
|
|
88
|
+
return {"type": "thinking", "text": text}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def plan(text: str) -> dict:
|
|
92
|
+
"""A proposed plan emitted in plan mode, awaiting approval."""
|
|
93
|
+
return {"type": "plan", "text": text}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def todos(items: list) -> dict:
|
|
97
|
+
"""The agent's current task checklist."""
|
|
98
|
+
return {"type": "todos", "items": items}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def subagent_start(call_id: str, description: str) -> dict:
|
|
102
|
+
return {"type": "subagent_start", "id": call_id, "description": description}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def subagent_end(call_id: str, summary: str) -> dict:
|
|
106
|
+
return {"type": "subagent_end", "id": call_id, "summary": summary}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --------------------------------------------------------------------------- #
|
|
110
|
+
# Approval decision + IO interface
|
|
111
|
+
# --------------------------------------------------------------------------- #
|
|
112
|
+
@dataclass
|
|
113
|
+
class ApprovalDecision:
|
|
114
|
+
approved: bool
|
|
115
|
+
feedback: Optional[str] = None # optional user note fed back to the model
|
|
116
|
+
always: bool = False # "always allow" — persist an allow rule for this tool
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AgentIO(abc.ABC):
|
|
120
|
+
"""How the agent loop emits output and requests human approval."""
|
|
121
|
+
|
|
122
|
+
@abc.abstractmethod
|
|
123
|
+
def emit_sync(self, event: dict) -> None:
|
|
124
|
+
"""Emit an event from anywhere — including a worker thread. Used for
|
|
125
|
+
streamed `token` and `command_output`. Implementations must be
|
|
126
|
+
thread-safe / non-blocking."""
|
|
127
|
+
|
|
128
|
+
@abc.abstractmethod
|
|
129
|
+
async def emit(self, event: dict) -> None:
|
|
130
|
+
"""Emit a structured event (always from the event loop)."""
|
|
131
|
+
|
|
132
|
+
@abc.abstractmethod
|
|
133
|
+
async def request_approval(self, request: dict) -> ApprovalDecision:
|
|
134
|
+
"""Ask the human to approve a gated action and block until answered."""
|
|
135
|
+
|
|
136
|
+
# Back-compat convenience used by the LLM stream callback.
|
|
137
|
+
def on_token(self, text: str, is_thinking: bool = False) -> None:
|
|
138
|
+
if is_thinking:
|
|
139
|
+
self.emit_sync({"type": "thinking_token", "text": text})
|
|
140
|
+
else:
|
|
141
|
+
self.emit_sync(token(text))
|
krnl_agent/gitignore.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Best-effort .gitignore → ignore-glob conversion.
|
|
2
|
+
|
|
3
|
+
This is intentionally lightweight (no `pathspec` dependency). It converts the
|
|
4
|
+
common .gitignore patterns into the fnmatch-style globs that ``ToolContext``
|
|
5
|
+
already understands. Negations (`!pattern`) are skipped — we only ever *add*
|
|
6
|
+
ignores, never un-ignore, which is the safe direction for an agent.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_gitignore_patterns(root: Path) -> list[str]:
|
|
14
|
+
gi = root / ".gitignore"
|
|
15
|
+
if not gi.is_file():
|
|
16
|
+
return []
|
|
17
|
+
patterns: list[str] = []
|
|
18
|
+
try:
|
|
19
|
+
lines = gi.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
20
|
+
except Exception:
|
|
21
|
+
return []
|
|
22
|
+
for raw in lines:
|
|
23
|
+
line = raw.strip()
|
|
24
|
+
if not line or line.startswith("#") or line.startswith("!"):
|
|
25
|
+
continue
|
|
26
|
+
# strip a leading slash (anchored to root); we match on relative paths
|
|
27
|
+
line = line.lstrip("/")
|
|
28
|
+
# a trailing slash means "a directory"
|
|
29
|
+
is_dir = line.endswith("/")
|
|
30
|
+
line = line.rstrip("/")
|
|
31
|
+
if not line:
|
|
32
|
+
continue
|
|
33
|
+
patterns.append(line)
|
|
34
|
+
# match contents of a directory too
|
|
35
|
+
patterns.append(f"{line}/**")
|
|
36
|
+
if not is_dir and "/" not in line and "*" not in line:
|
|
37
|
+
# a bare name like "node_modules" should match at any depth
|
|
38
|
+
patterns.append(f"**/{line}")
|
|
39
|
+
patterns.append(f"**/{line}/**")
|
|
40
|
+
# de-dup, preserve order
|
|
41
|
+
seen: set[str] = set()
|
|
42
|
+
out: list[str] = []
|
|
43
|
+
for p in patterns:
|
|
44
|
+
if p not in seen:
|
|
45
|
+
seen.add(p)
|
|
46
|
+
out.append(p)
|
|
47
|
+
return out
|