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.
Files changed (56) hide show
  1. krnl_agent/__init__.py +9 -0
  2. krnl_agent/__main__.py +7 -0
  3. krnl_agent/agent_registry.py +95 -0
  4. krnl_agent/agent_selector.py +69 -0
  5. krnl_agent/audit_log.py +155 -0
  6. krnl_agent/background.py +94 -0
  7. krnl_agent/checkpoints.py +67 -0
  8. krnl_agent/ci.py +73 -0
  9. krnl_agent/cli.py +1458 -0
  10. krnl_agent/commands.py +42 -0
  11. krnl_agent/config.py +425 -0
  12. krnl_agent/context.py +352 -0
  13. krnl_agent/depaudit.py +63 -0
  14. krnl_agent/deploy.py +245 -0
  15. krnl_agent/doctor.py +106 -0
  16. krnl_agent/events.py +141 -0
  17. krnl_agent/gitignore.py +47 -0
  18. krnl_agent/graph.py +928 -0
  19. krnl_agent/guardrails.py +70 -0
  20. krnl_agent/headless.py +60 -0
  21. krnl_agent/history.py +49 -0
  22. krnl_agent/hooks.py +72 -0
  23. krnl_agent/ingest.py +129 -0
  24. krnl_agent/llm.py +456 -0
  25. krnl_agent/loop.py +779 -0
  26. krnl_agent/mcp_client.py +128 -0
  27. krnl_agent/memory.py +61 -0
  28. krnl_agent/modelrouter.py +151 -0
  29. krnl_agent/monitor.py +112 -0
  30. krnl_agent/notify.py +119 -0
  31. krnl_agent/parallel_executor.py +139 -0
  32. krnl_agent/permissions.py +128 -0
  33. krnl_agent/plugins.py +105 -0
  34. krnl_agent/pricing.py +85 -0
  35. krnl_agent/prompts.py +60 -0
  36. krnl_agent/repomap.py +133 -0
  37. krnl_agent/sandbox.py +69 -0
  38. krnl_agent/scaffold.py +167 -0
  39. krnl_agent/schedules.py +137 -0
  40. krnl_agent/secrets.py +100 -0
  41. krnl_agent/selfheal.py +87 -0
  42. krnl_agent/server.py +302 -0
  43. krnl_agent/sessions.py +258 -0
  44. krnl_agent/settings.py +59 -0
  45. krnl_agent/skills.py +73 -0
  46. krnl_agent/teams.py +38 -0
  47. krnl_agent/tool_schemas.py +431 -0
  48. krnl_agent/tools.py +694 -0
  49. krnl_agent/webtools.py +139 -0
  50. krnl_code-1.0.4.dist-info/METADATA +214 -0
  51. krnl_code-1.0.4.dist-info/RECORD +56 -0
  52. krnl_code-1.0.4.dist-info/WHEEL +5 -0
  53. krnl_code-1.0.4.dist-info/entry_points.txt +2 -0
  54. krnl_code-1.0.4.dist-info/licenses/LICENSE +147 -0
  55. krnl_code-1.0.4.dist-info/licenses/NOTICE +4 -0
  56. 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))
@@ -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