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/commands.py ADDED
@@ -0,0 +1,42 @@
1
+ """Custom slash commands loaded from markdown files.
2
+
3
+ Files in `.krnl/commands/*.md` (project) or `~/.krnl-agent/commands/*.md`
4
+ (global) become `/name` commands. The file body is the prompt template;
5
+ `$ARGUMENTS` is replaced with everything after the command, and `$1`, `$2`, … with
6
+ individual whitespace-separated args.
7
+
8
+ Example `.krnl/commands/review.md`:
9
+ Review the changes in $ARGUMENTS for bugs and suggest fixes.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+ from .settings import SETTINGS_DIR
16
+
17
+
18
+ def _dirs(workspace: str) -> list[Path]:
19
+ from . import plugins
20
+
21
+ return [SETTINGS_DIR / "commands", *plugins.plugin_command_dirs(),
22
+ Path(workspace) / ".krnl" / "commands"]
23
+
24
+
25
+ def load_commands(workspace: str) -> dict[str, str]:
26
+ cmds: dict[str, str] = {}
27
+ for d in _dirs(workspace):
28
+ if d.is_dir():
29
+ for f in sorted(d.glob("*.md")):
30
+ try:
31
+ cmds[f.stem] = f.read_text(encoding="utf-8")
32
+ except Exception:
33
+ continue
34
+ return cmds
35
+
36
+
37
+ def expand_command(body: str, args: str) -> str:
38
+ parts = args.split()
39
+ out = body.replace("$ARGUMENTS", args)
40
+ for i, p in enumerate(parts, 1):
41
+ out = out.replace(f"${i}", p)
42
+ return out
krnl_agent/config.py ADDED
@@ -0,0 +1,425 @@
1
+ """Configuration loading and resolution.
2
+
3
+ Config is layered, in order of precedence (later wins):
4
+ 1. built-in defaults
5
+ 2. config.yaml (next to the package, the cwd, or $KRNL_AGENT_CONFIG)
6
+ 3. environment variables (.env is loaded automatically)
7
+ 4. explicit overrides passed on the CLI / API call
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from dataclasses import dataclass, field, replace
13
+ from pathlib import Path
14
+ from typing import Any, Optional
15
+
16
+ import yaml
17
+ from dotenv import load_dotenv
18
+
19
+ load_dotenv() # pull .env into os.environ if present
20
+
21
+
22
+ # --------------------------------------------------------------------------- #
23
+ # Dataclasses
24
+ # --------------------------------------------------------------------------- #
25
+ @dataclass
26
+ class ProviderConfig:
27
+ name: str
28
+ type: str = "openai" # "openai" (OpenAI-compatible) or "anthropic"
29
+ base_url: Optional[str] = None
30
+ api_key_env: Optional[str] = None
31
+ api_key: Optional[str] = None # resolved from api_key_env at load time
32
+ model: str = "gpt-4o-mini"
33
+ temperature: float = 0.2
34
+ max_tokens: int = 4096
35
+ extra_headers: dict = field(default_factory=dict)
36
+
37
+
38
+ @dataclass
39
+ class AgentConfig:
40
+ max_steps: int = 30
41
+ stream: bool = True
42
+ auto_approve_reads: bool = True
43
+ auto_approve_writes: bool = False
44
+ auto_approve_commands: bool = False
45
+ max_file_bytes: int = 120_000
46
+ max_command_seconds: int = 60
47
+ # context-window management
48
+ max_context_tokens: int = 24_000 # compact history above this estimate
49
+ compact_history: bool = True
50
+ # resilience
51
+ retry_attempts: int = 3
52
+ retry_backoff: float = 1.5
53
+ # ignore .gitignore-listed paths in addition to the configured globs
54
+ use_gitignore: bool = True
55
+ # auto-scaffold a .krnl/ wrapper (memory + skill + project doc) on first run
56
+ auto_onboard: bool = True
57
+ # tamper-evident append-only audit log of tool actions (.krnl/audit/)
58
+ audit_log: bool = True
59
+ # post-edit verifier (critic) sub-agent that reviews the turn's diff
60
+ verify_edits: bool = False
61
+ # self-heal: auto-retry a failed test/command via a fix-it sub-agent (N times)
62
+ self_heal: int = 0
63
+ # nested sub-agent budgets (agent-of-agent guardrails)
64
+ subagent_max_calls: int = 12
65
+ subagent_token_budget: int = 400_000
66
+ # extended thinking / reasoning (best-effort, provider-dependent)
67
+ thinking: bool = False
68
+ reasoning_effort: Optional[str] = None # "low" | "medium" | "high"
69
+ # try these models (same provider) in order if the primary call keeps failing
70
+ fallback_models: list = field(default_factory=list)
71
+
72
+
73
+ @dataclass
74
+ class GraphConfig:
75
+ enabled: bool = False
76
+ backend: str = "networkx"
77
+ db_path: str = ".krnl/graph.db"
78
+ languages: list[str] = field(default_factory=list)
79
+
80
+
81
+ @dataclass
82
+ class ContextConfig:
83
+ graph_aware: bool = False
84
+ graph_hop_limit: int = 1
85
+ differential_updates: bool = False
86
+ compaction_summarization: bool = False
87
+
88
+
89
+ @dataclass
90
+ class MemoryConfig:
91
+ per_session: bool = False
92
+ staleness_check_against_graph: bool = True
93
+
94
+
95
+ @dataclass
96
+ class AgentsConfig:
97
+ specialization: bool = False
98
+ archetypes: list[str] = field(default_factory=lambda: ["code_reviewer", "test_generator", "debugger"])
99
+ parallel_execution: bool = False
100
+
101
+
102
+ @dataclass
103
+ class Config:
104
+ provider: ProviderConfig
105
+ agent: AgentConfig
106
+ ignore: list[str] = field(default_factory=list)
107
+ all_providers: dict[str, ProviderConfig] = field(default_factory=dict)
108
+ mcp_servers: dict = field(default_factory=dict)
109
+ web: dict = field(default_factory=dict)
110
+ subagent_model: Optional[str] = None
111
+ permissions: dict = field(default_factory=dict)
112
+ hooks: dict = field(default_factory=dict)
113
+ notifications: dict = field(default_factory=dict)
114
+ pricing: dict = field(default_factory=dict) # model -> {input, output} per 1M tokens
115
+ # model routing: {"cheap": "<model>", "heavy": "<model>"} — cheap is used for
116
+ # sub-agents / verifier / self-heal when set (auto-downshift).
117
+ router: dict = field(default_factory=dict)
118
+ # multi-model: named model roles, each may target a different provider, e.g.
119
+ # {"planner": {"provider": "anthropic", "model": "claude-opus-4-8"}, ...}
120
+ models: dict = field(default_factory=dict)
121
+ # phase -> role mapping + strategy: {"strategy": "auto", "plan": "planner",
122
+ # "execute": "executor", "subagent": "cheap", "escalate": "planner", ...}
123
+ routing: dict = field(default_factory=dict)
124
+ # run_command sandbox/egress policy: deny_commands (regex), allow_commands
125
+ # (allowlist; if non-empty everything else is denied), block_network (bool).
126
+ sandbox: dict = field(default_factory=dict)
127
+ # auto-deploy policy: {"default_target": "cloudrun", "free_tier_only": true,
128
+ # "allow_billable": false, "cost_ceiling_usd": 0}
129
+ deploy: dict = field(default_factory=dict)
130
+ # monitoring providers / endpoints (mostly read from env; see monitor.py)
131
+ monitoring: dict = field(default_factory=dict)
132
+ # self-heal policy: {"strategy": "rollback-auto-fix-pr", "auto_merge_low_risk": false}
133
+ selfheal: dict = field(default_factory=dict)
134
+ # Phase 1: Code Knowledge Graph
135
+ graph: GraphConfig = field(default_factory=GraphConfig)
136
+ # Phase 2: Context Selection
137
+ context: ContextConfig = field(default_factory=ContextConfig)
138
+ # Phase 3: Per-Session Memory
139
+ memory: MemoryConfig = field(default_factory=MemoryConfig)
140
+ # Phase 4: Agent Specialization
141
+ agents: AgentsConfig = field(default_factory=AgentsConfig)
142
+
143
+
144
+ DEFAULT_IGNORE = [
145
+ ".git/**", "node_modules/**", "__pycache__/**", ".venv/**", "venv/**",
146
+ "dist/**", "build/**", "*.lock", ".env", ".env.*",
147
+ ]
148
+
149
+ # Built-in provider profiles so the extension (and a fresh install with no
150
+ # config.yaml) works out of the box: the user just picks a provider and supplies
151
+ # a key. Any of these can be overridden by a user-defined provider of the same
152
+ # name in config.yaml.
153
+ BUILTIN_PROVIDERS: dict[str, dict] = {
154
+ "krnl": {
155
+ "type": "openai",
156
+ "base_url": "https://api.krnl.one/api/v1",
157
+ "api_key_env": "KRL_API_KEY",
158
+ "model": "gpt-4.1-mini",
159
+ },
160
+ "openai": {
161
+ "type": "openai",
162
+ "base_url": "https://api.openai.com/v1",
163
+ "api_key_env": "OPENAI_API_KEY",
164
+ "model": "gpt-4o-mini",
165
+ },
166
+ "openrouter": {
167
+ "type": "openai",
168
+ "base_url": "https://openrouter.ai/api/v1",
169
+ "api_key_env": "OPENROUTER_API_KEY",
170
+ "model": "nex-agi/nex-n2-pro:free",
171
+ },
172
+ "ollama": {
173
+ "type": "openai",
174
+ "base_url": "http://localhost:11434/v1",
175
+ "api_key_env": None,
176
+ "model": "qwen3.5-coder:7b",
177
+ },
178
+ "anthropic": {
179
+ "type": "anthropic",
180
+ "base_url": None,
181
+ "api_key_env": "ANTHROPIC_API_KEY",
182
+ "model": "claude-sonnet-4-6",
183
+ },
184
+ "gemini": {
185
+ "type": "openai", # Google's OpenAI-compatible endpoint
186
+ "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/",
187
+ "api_key_env": "GEMINI_API_KEY",
188
+ "model": "gemini-3.5-flash",
189
+ },
190
+ "groq": {
191
+ "type": "openai",
192
+ "base_url": "https://api.groq.com/openai/v1",
193
+ "api_key_env": "GROQ_API_KEY",
194
+ "model": "llama-3.3-70b-versatile",
195
+ },
196
+ "cerebras": {
197
+ "type": "openai",
198
+ "base_url": "https://api.cerebras.ai/v1",
199
+ "api_key_env": "CEREBRAS_API_KEY",
200
+ "model": "llama-3.3-70b",
201
+ },
202
+ "deepseek": {
203
+ "type": "openai",
204
+ "base_url": "https://api.deepseek.com",
205
+ "api_key_env": "DEEPSEEK_API_KEY",
206
+ "model": "deepseek-chat",
207
+ },
208
+ "together": {
209
+ "type": "openai",
210
+ "base_url": "https://api.together.xyz/v1",
211
+ "api_key_env": "TOGETHER_API_KEY",
212
+ "model": "moonshotai/Kimi-K2-Instruct",
213
+ },
214
+ "mistral": {
215
+ "type": "openai",
216
+ "base_url": "https://api.mistral.ai/v1",
217
+ "api_key_env": "MISTRAL_API_KEY",
218
+ "model": "mistral-large-latest",
219
+ },
220
+ "xai": {
221
+ "type": "openai",
222
+ "base_url": "https://api.x.ai/v1",
223
+ "api_key_env": "XAI_API_KEY",
224
+ "model": "grok-2-latest",
225
+ },
226
+ "vercel": {
227
+ "type": "openai", # Vercel AI Gateway
228
+ "base_url": "https://ai-gateway.vercel.sh/v1",
229
+ "api_key_env": "AI_GATEWAY_API_KEY",
230
+ "model": "anthropic/claude-sonnet-4.5",
231
+ },
232
+ "lmstudio": {
233
+ "type": "openai",
234
+ "base_url": "http://localhost:1234/v1",
235
+ "api_key_env": None,
236
+ "model": "local-model",
237
+ },
238
+ # Generic OpenAI-compatible endpoint; base_url/model supplied at runtime.
239
+ # Covers AWS Bedrock / Azure / GCP Vertex via an OpenAI-compatible proxy.
240
+ "custom": {
241
+ "type": "openai",
242
+ "base_url": "http://localhost:8001/v1",
243
+ "api_key_env": None,
244
+ "model": "local-model",
245
+ },
246
+ }
247
+
248
+
249
+ # --------------------------------------------------------------------------- #
250
+ # Loading
251
+ # --------------------------------------------------------------------------- #
252
+ def _candidate_paths(explicit: Optional[str]) -> list[Path]:
253
+ paths = []
254
+ if explicit:
255
+ paths.append(Path(explicit))
256
+ env = os.getenv("KRNL_AGENT_CONFIG")
257
+ if env:
258
+ paths.append(Path(env))
259
+ paths.append(Path.cwd() / "config.yaml")
260
+ paths.append(Path(__file__).resolve().parent.parent / "config.yaml")
261
+ return paths
262
+
263
+
264
+ def _find_config_file(explicit: Optional[str]) -> Optional[Path]:
265
+ for p in _candidate_paths(explicit):
266
+ if p and p.is_file():
267
+ return p
268
+ return None
269
+
270
+
271
+ def _provider_from_dict(name: str, d: dict) -> ProviderConfig:
272
+ api_key_env = d.get("api_key_env")
273
+ api_key = os.getenv(api_key_env) if api_key_env else None
274
+ return ProviderConfig(
275
+ name=name,
276
+ type=d.get("type", "openai"),
277
+ base_url=d.get("base_url"),
278
+ api_key_env=api_key_env,
279
+ api_key=api_key,
280
+ model=d.get("model", "gpt-4o-mini"),
281
+ temperature=float(d.get("temperature", 0.2)),
282
+ max_tokens=int(d.get("max_tokens", 4096)),
283
+ extra_headers=d.get("extra_headers", {}) or {},
284
+ )
285
+
286
+
287
+ def load_config(
288
+ config_path: Optional[str] = None,
289
+ *,
290
+ provider: Optional[str] = None,
291
+ model: Optional[str] = None,
292
+ api_key: Optional[str] = None,
293
+ base_url: Optional[str] = None,
294
+ ) -> Config:
295
+ """Load configuration, applying optional overrides.
296
+
297
+ `api_key` / `base_url` let a caller (e.g. the VS Code extension) inject a
298
+ secret and endpoint at runtime so nothing has to live in a file.
299
+ """
300
+ raw: dict[str, Any] = {}
301
+ cfg_file = _find_config_file(config_path)
302
+ if cfg_file:
303
+ raw = yaml.safe_load(cfg_file.read_text(encoding="utf-8")) or {}
304
+
305
+ # Start from the built-in profiles, then let config.yaml override/extend them.
306
+ merged: dict[str, dict] = {k: dict(v) for k, v in BUILTIN_PROVIDERS.items()}
307
+ for name, d in (raw.get("providers", {}) or {}).items():
308
+ merged[name] = {**merged.get(name, {}), **d}
309
+ all_providers = {name: _provider_from_dict(name, d) for name, d in merged.items()}
310
+
311
+ active = provider or raw.get("active") or "openai"
312
+ if active not in all_providers:
313
+ raise ValueError(
314
+ f"Provider '{active}' not found. Available: {', '.join(all_providers)}"
315
+ )
316
+ selected = all_providers[active]
317
+ overrides: dict[str, Any] = {}
318
+ if model:
319
+ overrides["model"] = model
320
+ if api_key: # non-empty only
321
+ overrides["api_key"] = api_key
322
+ if base_url:
323
+ overrides["base_url"] = base_url
324
+ if overrides:
325
+ selected = replace(selected, **overrides)
326
+
327
+ agent_raw = raw.get("agent", {}) or {}
328
+ agent = AgentConfig(
329
+ max_steps=int(agent_raw.get("max_steps", 30)),
330
+ stream=bool(agent_raw.get("stream", True)),
331
+ auto_approve_reads=bool(agent_raw.get("auto_approve_reads", True)),
332
+ auto_approve_writes=bool(agent_raw.get("auto_approve_writes", False)),
333
+ auto_approve_commands=bool(agent_raw.get("auto_approve_commands", False)),
334
+ max_file_bytes=int(agent_raw.get("max_file_bytes", 120_000)),
335
+ max_command_seconds=int(agent_raw.get("max_command_seconds", 60)),
336
+ max_context_tokens=int(agent_raw.get("max_context_tokens", 24_000)),
337
+ compact_history=bool(agent_raw.get("compact_history", True)),
338
+ retry_attempts=int(agent_raw.get("retry_attempts", 3)),
339
+ retry_backoff=float(agent_raw.get("retry_backoff", 1.5)),
340
+ use_gitignore=bool(agent_raw.get("use_gitignore", True)),
341
+ auto_onboard=bool(agent_raw.get("auto_onboard", True)),
342
+ audit_log=bool(agent_raw.get("audit_log", True)),
343
+ verify_edits=bool(agent_raw.get("verify_edits", False)),
344
+ self_heal=int(agent_raw.get("self_heal", 0)),
345
+ subagent_max_calls=int(agent_raw.get("subagent_max_calls", 12)),
346
+ subagent_token_budget=int(agent_raw.get("subagent_token_budget", 400_000)),
347
+ thinking=bool(agent_raw.get("thinking", False)),
348
+ reasoning_effort=agent_raw.get("reasoning_effort"),
349
+ fallback_models=list(agent_raw.get("fallback_models", []) or []),
350
+ )
351
+
352
+ # Load Phase 1-4 configs
353
+ graph_raw = raw.get("graph", {}) or {}
354
+ graph = GraphConfig(
355
+ enabled=bool(graph_raw.get("enabled", False)),
356
+ backend=graph_raw.get("backend", "networkx"),
357
+ db_path=graph_raw.get("db_path", ".krnl/graph.db"),
358
+ languages=list(graph_raw.get("languages", []) or []),
359
+ )
360
+
361
+ context_raw = raw.get("context", {}) or {}
362
+ context_cfg = ContextConfig(
363
+ graph_aware=bool(context_raw.get("graph_aware", False)),
364
+ graph_hop_limit=int(context_raw.get("graph_hop_limit", 1)),
365
+ differential_updates=bool(context_raw.get("differential_updates", False)),
366
+ compaction_summarization=bool(context_raw.get("compaction_summarization", False)),
367
+ )
368
+
369
+ memory_raw = raw.get("memory", {}) or {}
370
+ memory_cfg = MemoryConfig(
371
+ per_session=bool(memory_raw.get("per_session", False)),
372
+ staleness_check_against_graph=bool(memory_raw.get("staleness_check_against_graph", True)),
373
+ )
374
+
375
+ agents_raw = raw.get("agents", {}) or {}
376
+ agents_cfg = AgentsConfig(
377
+ specialization=bool(agents_raw.get("specialization", False)),
378
+ archetypes=list(agents_raw.get("archetypes", ["code_reviewer", "test_generator", "debugger"]) or []),
379
+ parallel_execution=bool(agents_raw.get("parallel_execution", False)),
380
+ )
381
+
382
+ ignore = raw.get("ignore") or DEFAULT_IGNORE
383
+
384
+ mcp_servers = (raw.get("mcp", {}) or {}).get("servers", {}) or {}
385
+
386
+ web = dict(raw.get("web", {}) or {})
387
+ if not web.get("provider"):
388
+ # auto-detect a search backend from env (TAVILY/BRAVE/SERPAPI), else keyless
389
+ for env_key, name in (
390
+ ("TAVILY_API_KEY", "tavily"),
391
+ ("BRAVE_API_KEY", "brave"),
392
+ ("SERPAPI_API_KEY", "serpapi"),
393
+ ):
394
+ if os.getenv(env_key):
395
+ web = {"provider": name, "api_key": os.environ[env_key]}
396
+ break
397
+ else:
398
+ web = {"provider": "duckduckgo", "api_key": ""}
399
+ elif web.get("api_key_env"):
400
+ web["api_key"] = os.getenv(web["api_key_env"], "")
401
+
402
+ return Config(
403
+ provider=selected,
404
+ agent=agent,
405
+ ignore=list(ignore),
406
+ all_providers=all_providers,
407
+ mcp_servers=mcp_servers,
408
+ web=web,
409
+ subagent_model=raw.get("subagent_model"),
410
+ permissions=raw.get("permissions", {}) or {},
411
+ hooks=raw.get("hooks", {}) or {},
412
+ notifications=raw.get("notifications", {}) or {},
413
+ pricing=raw.get("pricing", {}) or {},
414
+ router=raw.get("router", {}) or {},
415
+ models=raw.get("models", {}) or {},
416
+ routing=raw.get("routing", {}) or {},
417
+ sandbox=raw.get("sandbox", {}) or {},
418
+ deploy=raw.get("deploy", {}) or {},
419
+ monitoring=raw.get("monitoring", {}) or {},
420
+ selfheal=raw.get("selfheal", {}) or {},
421
+ graph=graph,
422
+ context=context_cfg,
423
+ memory=memory_cfg,
424
+ agents=agents_cfg,
425
+ )