akernel-runtime 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.
- akernel_runtime-0.1.0.dist-info/METADATA +270 -0
- akernel_runtime-0.1.0.dist-info/RECORD +40 -0
- akernel_runtime-0.1.0.dist-info/WHEEL +5 -0
- akernel_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- akernel_runtime-0.1.0.dist-info/licenses/LICENSE +201 -0
- akernel_runtime-0.1.0.dist-info/licenses/NOTICE +4 -0
- akernel_runtime-0.1.0.dist-info/top_level.txt +1 -0
- context_kernel/__init__.py +4 -0
- context_kernel/__main__.py +5 -0
- context_kernel/agent_reports.py +188 -0
- context_kernel/benchmarks.py +493 -0
- context_kernel/budget.py +72 -0
- context_kernel/cli.py +2953 -0
- context_kernel/context.py +161 -0
- context_kernel/evals.py +347 -0
- context_kernel/global_memory.py +126 -0
- context_kernel/loop.py +1617 -0
- context_kernel/marketplace.py +194 -0
- context_kernel/marketplace_data/skills/context_budget.json +27 -0
- context_kernel/marketplace_data/skills/context_compaction.json +27 -0
- context_kernel/marketplace_data/skills/edit_file.json +27 -0
- context_kernel/marketplace_data/skills/index.json +66 -0
- context_kernel/marketplace_data/skills/long_task_planning.json +27 -0
- context_kernel/marketplace_data/skills/multi_file_bugfix.json +28 -0
- context_kernel/memory.py +515 -0
- context_kernel/models.py +144 -0
- context_kernel/planner.py +155 -0
- context_kernel/policy.py +271 -0
- context_kernel/project.py +317 -0
- context_kernel/providers.py +1264 -0
- context_kernel/report_costs.py +375 -0
- context_kernel/runner.py +78 -0
- context_kernel/skills.py +318 -0
- context_kernel/state_writer.py +108 -0
- context_kernel/storage.py +171 -0
- context_kernel/tasks.py +549 -0
- context_kernel/text.py +42 -0
- context_kernel/tokenizer.py +22 -0
- context_kernel/tools.py +544 -0
- context_kernel/verifier.py +77 -0
context_kernel/skills.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .models import SelectedSkill, Skill
|
|
10
|
+
from .providers import get_provider
|
|
11
|
+
from .storage import Workspace
|
|
12
|
+
from .text import matched_terms
|
|
13
|
+
from .tokenizer import estimate_tokens
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SkillRegistry:
|
|
17
|
+
def __init__(self, workspace: Workspace):
|
|
18
|
+
self.workspace = workspace
|
|
19
|
+
|
|
20
|
+
def register(self, source: Path) -> Skill:
|
|
21
|
+
data = Workspace.read_json(source)
|
|
22
|
+
skill = Skill.from_dict(data)
|
|
23
|
+
destination = self.workspace.skills_dir / f"{skill.id}.json"
|
|
24
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
shutil.copyfile(source, destination)
|
|
26
|
+
return skill
|
|
27
|
+
|
|
28
|
+
def all(self) -> list[Skill]:
|
|
29
|
+
skills: list[Skill] = []
|
|
30
|
+
for path in sorted(self.workspace.skills_dir.glob("*.json")):
|
|
31
|
+
skills.append(Skill.from_dict(Workspace.read_json(path)))
|
|
32
|
+
return skills
|
|
33
|
+
|
|
34
|
+
def get(self, skill_id: str) -> Skill:
|
|
35
|
+
path = self.workspace.skills_dir / f"{skill_id}.json"
|
|
36
|
+
if not path.exists():
|
|
37
|
+
raise KeyError(f"Unknown skill: {skill_id}")
|
|
38
|
+
return Skill.from_dict(Workspace.read_json(path))
|
|
39
|
+
|
|
40
|
+
def select(self, request: str, budget_tokens: int, limit: int = 3) -> list[SelectedSkill]:
|
|
41
|
+
candidates: list[SelectedSkill] = []
|
|
42
|
+
for skill in self.all():
|
|
43
|
+
haystack = " ".join(
|
|
44
|
+
[skill.id, skill.name, skill.summary, skill.intent, " ".join(skill.inputs), " ".join(skill.outputs)]
|
|
45
|
+
)
|
|
46
|
+
matches = matched_terms(request, haystack)
|
|
47
|
+
score = len(matches)
|
|
48
|
+
if score > 0:
|
|
49
|
+
candidates.append(
|
|
50
|
+
SelectedSkill(
|
|
51
|
+
skill=skill,
|
|
52
|
+
level="l1",
|
|
53
|
+
score=score,
|
|
54
|
+
reason=f"matched terms: {', '.join(matches)}",
|
|
55
|
+
matched_terms=matches,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not candidates:
|
|
60
|
+
fallback = self.all()[:1]
|
|
61
|
+
candidates = [
|
|
62
|
+
SelectedSkill(
|
|
63
|
+
skill=skill,
|
|
64
|
+
level="l0",
|
|
65
|
+
score=0,
|
|
66
|
+
reason="fallback summary loaded because no skill matched",
|
|
67
|
+
matched_terms=[],
|
|
68
|
+
)
|
|
69
|
+
for skill in fallback
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
selected = sorted(candidates, key=lambda item: item.score, reverse=True)[:limit]
|
|
73
|
+
remaining = budget_tokens
|
|
74
|
+
adjusted: list[SelectedSkill] = []
|
|
75
|
+
for item in selected:
|
|
76
|
+
level = item.level if item.score == 0 else "l2" if remaining > 350 else "l1"
|
|
77
|
+
rendered_tokens = estimate_tokens(item.skill.render_level(level))
|
|
78
|
+
if rendered_tokens > remaining and level == "l2":
|
|
79
|
+
level = "l1"
|
|
80
|
+
rendered_tokens = estimate_tokens(item.skill.render_level(level))
|
|
81
|
+
if rendered_tokens <= remaining:
|
|
82
|
+
adjusted.append(
|
|
83
|
+
SelectedSkill(
|
|
84
|
+
skill=item.skill,
|
|
85
|
+
level=level,
|
|
86
|
+
score=item.score,
|
|
87
|
+
reason=item.reason,
|
|
88
|
+
matched_terms=item.matched_terms,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
remaining -= rendered_tokens
|
|
92
|
+
return adjusted
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
SECTION_ALIASES = {
|
|
96
|
+
"inputs": "inputs",
|
|
97
|
+
"input": "inputs",
|
|
98
|
+
"outputs": "outputs",
|
|
99
|
+
"output": "outputs",
|
|
100
|
+
"constraints": "constraints",
|
|
101
|
+
"constraint": "constraints",
|
|
102
|
+
"rules": "constraints",
|
|
103
|
+
"failure modes": "failure_modes",
|
|
104
|
+
"failure mode": "failure_modes",
|
|
105
|
+
"failures": "failure_modes",
|
|
106
|
+
"procedure": "procedure",
|
|
107
|
+
"workflow": "procedure",
|
|
108
|
+
"steps": "procedure",
|
|
109
|
+
"process": "procedure",
|
|
110
|
+
"examples": "examples",
|
|
111
|
+
"example": "examples",
|
|
112
|
+
"intent": "intent",
|
|
113
|
+
"summary": "summary",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def compile_markdown_skill(path: Path, skill_id: str | None = None) -> Skill:
|
|
118
|
+
text = path.read_text(encoding="utf-8")
|
|
119
|
+
title = first_heading(text) or title_from_filename(path)
|
|
120
|
+
sections = markdown_sections(text)
|
|
121
|
+
body_intro = intro_text(text)
|
|
122
|
+
|
|
123
|
+
name = title.strip()
|
|
124
|
+
inferred_id = skill_id or slugify(name)
|
|
125
|
+
summary = first_sentence(section_text(sections, "summary") or body_intro or name)
|
|
126
|
+
intent = first_sentence(section_text(sections, "intent") or summary)
|
|
127
|
+
|
|
128
|
+
data = {
|
|
129
|
+
"id": inferred_id,
|
|
130
|
+
"name": name,
|
|
131
|
+
"summary": summary,
|
|
132
|
+
"intent": intent,
|
|
133
|
+
"inputs": section_items(sections, "inputs"),
|
|
134
|
+
"outputs": section_items(sections, "outputs"),
|
|
135
|
+
"constraints": section_items(sections, "constraints"),
|
|
136
|
+
"failure_modes": section_items(sections, "failure_modes"),
|
|
137
|
+
"procedure": section_items(sections, "procedure") or fallback_procedure(body_intro),
|
|
138
|
+
"examples": section_items(sections, "examples"),
|
|
139
|
+
}
|
|
140
|
+
return Skill.from_dict(data)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def compile_markdown_skill_with_provider(
|
|
144
|
+
path: Path,
|
|
145
|
+
provider_name: str,
|
|
146
|
+
model: str | None = None,
|
|
147
|
+
base_url: str | None = None,
|
|
148
|
+
skill_id: str | None = None,
|
|
149
|
+
) -> tuple[Skill, dict[str, Any]]:
|
|
150
|
+
markdown = path.read_text(encoding="utf-8")
|
|
151
|
+
provider = get_provider(provider_name, model=model, base_url=base_url)
|
|
152
|
+
requested_id = skill_id or slugify(first_heading(markdown) or title_from_filename(path))
|
|
153
|
+
packet = {
|
|
154
|
+
"request": "Compile this Markdown skill into Context Kernel skill JSON.",
|
|
155
|
+
"runtime": {
|
|
156
|
+
"instructions": [
|
|
157
|
+
"Return only valid JSON with no commentary.",
|
|
158
|
+
"Preserve concrete constraints, failure modes, and procedures.",
|
|
159
|
+
"Use concise strings. Do not invent capabilities absent from the Markdown.",
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
"schema": {
|
|
163
|
+
"id": "string",
|
|
164
|
+
"name": "string",
|
|
165
|
+
"summary": "string",
|
|
166
|
+
"intent": "string",
|
|
167
|
+
"inputs": ["string"],
|
|
168
|
+
"outputs": ["string"],
|
|
169
|
+
"constraints": ["string"],
|
|
170
|
+
"failure_modes": ["string"],
|
|
171
|
+
"procedure": ["string"],
|
|
172
|
+
"examples": ["string"],
|
|
173
|
+
},
|
|
174
|
+
"requested_id": requested_id,
|
|
175
|
+
"source_markdown": markdown,
|
|
176
|
+
"budget": {"estimated_used": estimate_tokens(markdown)},
|
|
177
|
+
}
|
|
178
|
+
response = provider.run(packet)
|
|
179
|
+
data = extract_json_object(response.text)
|
|
180
|
+
data["id"] = skill_id or data.get("id") or requested_id
|
|
181
|
+
skill = Skill.from_dict(data)
|
|
182
|
+
metadata = {
|
|
183
|
+
"provider": provider.name,
|
|
184
|
+
"model": getattr(provider, "model", model),
|
|
185
|
+
"input_tokens": response.input_tokens,
|
|
186
|
+
"output_tokens": response.output_tokens,
|
|
187
|
+
"total_tokens": response.input_tokens + response.output_tokens,
|
|
188
|
+
}
|
|
189
|
+
return skill, metadata
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def validate_skill_file(path: Path) -> dict[str, Any]:
|
|
193
|
+
skill = Skill.from_dict(Workspace.read_json(path))
|
|
194
|
+
levels = {level: estimate_tokens(skill.render_level(level)) for level in ["l0", "l1", "l2", "l3"]}
|
|
195
|
+
warnings: list[str] = []
|
|
196
|
+
if not skill.inputs:
|
|
197
|
+
warnings.append("inputs is empty")
|
|
198
|
+
if not skill.outputs:
|
|
199
|
+
warnings.append("outputs is empty")
|
|
200
|
+
if not skill.constraints:
|
|
201
|
+
warnings.append("constraints is empty")
|
|
202
|
+
if not skill.procedure:
|
|
203
|
+
warnings.append("procedure is empty")
|
|
204
|
+
return {
|
|
205
|
+
"ok": True,
|
|
206
|
+
"id": skill.id,
|
|
207
|
+
"name": skill.name,
|
|
208
|
+
"level_tokens": levels,
|
|
209
|
+
"warnings": warnings,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def inspect_skill(skill: Skill, budget: int) -> dict[str, Any]:
|
|
214
|
+
levels: list[dict[str, Any]] = []
|
|
215
|
+
selected = "l0"
|
|
216
|
+
for level in ["l0", "l1", "l2", "l3"]:
|
|
217
|
+
rendered = skill.render_level(level)
|
|
218
|
+
tokens = estimate_tokens(rendered)
|
|
219
|
+
fits = tokens <= budget
|
|
220
|
+
if fits:
|
|
221
|
+
selected = level
|
|
222
|
+
levels.append({"level": level, "tokens": tokens, "fits": fits, "content": rendered})
|
|
223
|
+
return {"id": skill.id, "budget": budget, "selected_level": selected, "levels": levels}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def markdown_sections(text: str) -> dict[str, list[str]]:
|
|
227
|
+
sections: dict[str, list[str]] = {}
|
|
228
|
+
current: str | None = None
|
|
229
|
+
for line in text.splitlines():
|
|
230
|
+
heading = parse_heading(line)
|
|
231
|
+
if heading:
|
|
232
|
+
current = SECTION_ALIASES.get(heading.lower())
|
|
233
|
+
if current:
|
|
234
|
+
sections.setdefault(current, [])
|
|
235
|
+
continue
|
|
236
|
+
if current:
|
|
237
|
+
sections[current].append(line)
|
|
238
|
+
return sections
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def section_items(sections: dict[str, list[str]], name: str) -> list[str]:
|
|
242
|
+
lines = sections.get(name, [])
|
|
243
|
+
items: list[str] = []
|
|
244
|
+
for line in lines:
|
|
245
|
+
stripped = line.strip()
|
|
246
|
+
if not stripped:
|
|
247
|
+
continue
|
|
248
|
+
bullet = re.sub(r"^[-*]\s+", "", stripped)
|
|
249
|
+
bullet = re.sub(r"^\d+[.)]\s+", "", bullet)
|
|
250
|
+
items.append(bullet.strip())
|
|
251
|
+
return items
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def section_text(sections: dict[str, list[str]], name: str) -> str:
|
|
255
|
+
return " ".join(line.strip() for line in sections.get(name, []) if line.strip())
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def intro_text(text: str) -> str:
|
|
259
|
+
lines: list[str] = []
|
|
260
|
+
for line in text.splitlines():
|
|
261
|
+
if parse_heading(line):
|
|
262
|
+
if lines:
|
|
263
|
+
break
|
|
264
|
+
continue
|
|
265
|
+
stripped = line.strip()
|
|
266
|
+
if stripped:
|
|
267
|
+
lines.append(stripped)
|
|
268
|
+
return " ".join(lines)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def first_heading(text: str) -> str | None:
|
|
272
|
+
for line in text.splitlines():
|
|
273
|
+
heading = parse_heading(line)
|
|
274
|
+
if heading:
|
|
275
|
+
return heading
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def parse_heading(line: str) -> str | None:
|
|
280
|
+
match = re.match(r"^\s{0,3}#{1,6}\s+(.+?)\s*$", line)
|
|
281
|
+
return match.group(1).strip() if match else None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def first_sentence(text: str) -> str:
|
|
285
|
+
compact = " ".join(text.split())
|
|
286
|
+
if not compact:
|
|
287
|
+
return ""
|
|
288
|
+
match = re.match(r"^(.+?[.!?])\s", compact)
|
|
289
|
+
return match.group(1) if match else compact[:180]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def fallback_procedure(text: str) -> list[str]:
|
|
293
|
+
return [first_sentence(text)] if text else []
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def title_from_filename(path: Path) -> str:
|
|
297
|
+
return path.stem.replace("_", " ").replace("-", " ").title()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def slugify(value: str) -> str:
|
|
301
|
+
slug = re.sub(r"[^a-zA-Z0-9]+", "_", value.lower()).strip("_")
|
|
302
|
+
return slug or "compiled_skill"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def extract_json_object(text: str) -> dict[str, Any]:
|
|
306
|
+
stripped = text.strip()
|
|
307
|
+
fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", stripped, flags=re.DOTALL)
|
|
308
|
+
if fenced:
|
|
309
|
+
stripped = fenced.group(1)
|
|
310
|
+
elif not stripped.startswith("{"):
|
|
311
|
+
start = stripped.find("{")
|
|
312
|
+
end = stripped.rfind("}")
|
|
313
|
+
if start >= 0 and end > start:
|
|
314
|
+
stripped = stripped[start : end + 1]
|
|
315
|
+
data = json.loads(stripped)
|
|
316
|
+
if not isinstance(data, dict):
|
|
317
|
+
raise ValueError("Provider did not return a JSON object.")
|
|
318
|
+
return data
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .memory import MemoryStore
|
|
7
|
+
from .storage import Workspace
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
MARKER_KINDS = {
|
|
11
|
+
"decision": "decision",
|
|
12
|
+
"decided": "decision",
|
|
13
|
+
"fact": "fact",
|
|
14
|
+
"preference": "preference",
|
|
15
|
+
"project state": "project_state",
|
|
16
|
+
"project_state": "project_state",
|
|
17
|
+
"task state": "task_state",
|
|
18
|
+
"task_state": "task_state",
|
|
19
|
+
}
|
|
20
|
+
SECRET_PATTERNS = [
|
|
21
|
+
re.compile(r"sk-[A-Za-z0-9_\-]{8,}"),
|
|
22
|
+
re.compile(r"(?i)(api[_-]?key\s*[:=]\s*)[^\s]+"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StateWriter:
|
|
27
|
+
def __init__(self, workspace: Workspace):
|
|
28
|
+
self.workspace = workspace
|
|
29
|
+
self.memory = MemoryStore(workspace)
|
|
30
|
+
|
|
31
|
+
def propose_from_trace(self, trace: dict[str, Any]) -> list[dict[str, Any]]:
|
|
32
|
+
candidates = [self._task_state_candidate(trace)]
|
|
33
|
+
if trace.get("verifier", {}).get("ok"):
|
|
34
|
+
candidates.extend(marker_candidates(trace))
|
|
35
|
+
return [candidate for candidate in candidates if candidate["text"]]
|
|
36
|
+
|
|
37
|
+
def write_from_trace(self, trace: dict[str, Any]) -> dict[str, Any]:
|
|
38
|
+
candidates = self.propose_from_trace(trace)
|
|
39
|
+
records = [
|
|
40
|
+
self.memory.add(candidate["kind"], candidate["text"], candidate["tags"])
|
|
41
|
+
for candidate in candidates
|
|
42
|
+
]
|
|
43
|
+
return {
|
|
44
|
+
"enabled": True,
|
|
45
|
+
"candidate_count": len(candidates),
|
|
46
|
+
"written_count": len(records),
|
|
47
|
+
"records": [record.to_dict() for record in records],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def _task_state_candidate(self, trace: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
response = trace.get("response", {})
|
|
52
|
+
verifier = trace.get("verifier", {})
|
|
53
|
+
status = "ok" if verifier.get("ok") else "failed"
|
|
54
|
+
text = (
|
|
55
|
+
f"Trace {trace.get('id')}: request '{compact(trace.get('request', ''))}' "
|
|
56
|
+
f"completed with provider {trace.get('provider')} status={status}; "
|
|
57
|
+
f"tokens={response.get('total_tokens', 0)}."
|
|
58
|
+
)
|
|
59
|
+
return {
|
|
60
|
+
"kind": "task_state",
|
|
61
|
+
"text": redact(text),
|
|
62
|
+
"tags": trace_tags(trace),
|
|
63
|
+
"source": "trace_summary",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def marker_candidates(trace: dict[str, Any]) -> list[dict[str, Any]]:
|
|
68
|
+
candidates: list[dict[str, Any]] = []
|
|
69
|
+
response_text = trace.get("response", {}).get("text", "")
|
|
70
|
+
for line in response_text.splitlines():
|
|
71
|
+
match = re.match(r"^\s*([A-Za-z _]+)\s*:\s*(.+?)\s*$", line)
|
|
72
|
+
if not match:
|
|
73
|
+
continue
|
|
74
|
+
marker = match.group(1).strip().casefold()
|
|
75
|
+
kind = MARKER_KINDS.get(marker)
|
|
76
|
+
if not kind:
|
|
77
|
+
continue
|
|
78
|
+
candidates.append(
|
|
79
|
+
{
|
|
80
|
+
"kind": kind,
|
|
81
|
+
"text": redact(compact(match.group(2))),
|
|
82
|
+
"tags": trace_tags(trace) + ["marker:" + marker.replace(" ", "_")],
|
|
83
|
+
"source": "response_marker",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
return candidates
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def trace_tags(trace: dict[str, Any]) -> list[str]:
|
|
90
|
+
tags = ["auto", "trace:" + str(trace.get("id", ""))]
|
|
91
|
+
provider = trace.get("provider")
|
|
92
|
+
if provider:
|
|
93
|
+
tags.append("provider:" + str(provider))
|
|
94
|
+
return tags
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def compact(text: str, limit: int = 280) -> str:
|
|
98
|
+
normalized = " ".join(str(text).split())
|
|
99
|
+
if len(normalized) <= limit:
|
|
100
|
+
return normalized
|
|
101
|
+
return normalized[: limit - 3].rstrip() + "..."
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def redact(text: str) -> str:
|
|
105
|
+
redacted = text
|
|
106
|
+
for pattern in SECRET_PATTERNS:
|
|
107
|
+
redacted = pattern.sub(lambda match: match.group(1) + "[REDACTED]" if match.groups() else "[REDACTED]", redacted)
|
|
108
|
+
return redacted
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Iterable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
STATE_DIR = ".akernel"
|
|
10
|
+
DEFAULT_CONFIG_VERSION = 2
|
|
11
|
+
DEFAULT_RUNTIME_INSTRUCTIONS = [
|
|
12
|
+
"Use the smallest context packet that can answer the request.",
|
|
13
|
+
"Prefer structured memory and skill contracts over full history.",
|
|
14
|
+
"Report budget pressure and omitted context explicitly.",
|
|
15
|
+
]
|
|
16
|
+
DEFAULT_COMMAND_POLICY = {
|
|
17
|
+
"allowed_roots": [
|
|
18
|
+
"akernel",
|
|
19
|
+
"akernel.exe",
|
|
20
|
+
"git",
|
|
21
|
+
"py",
|
|
22
|
+
"pytest",
|
|
23
|
+
"python",
|
|
24
|
+
"python.exe",
|
|
25
|
+
],
|
|
26
|
+
"blocked_terms": [],
|
|
27
|
+
}
|
|
28
|
+
DEFAULT_CONFIG = {
|
|
29
|
+
"version": DEFAULT_CONFIG_VERSION,
|
|
30
|
+
"default_budget": 1200,
|
|
31
|
+
"runtime_instructions": DEFAULT_RUNTIME_INSTRUCTIONS,
|
|
32
|
+
"command_policy": DEFAULT_COMMAND_POLICY,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Workspace:
|
|
37
|
+
def __init__(self, root: Path):
|
|
38
|
+
self.root = root.resolve()
|
|
39
|
+
self.state = self.root / STATE_DIR
|
|
40
|
+
self.skills_dir = self.state / "skills"
|
|
41
|
+
self.traces_dir = self.state / "traces"
|
|
42
|
+
self.tool_traces_dir = self.state / "tool_traces"
|
|
43
|
+
self.agent_runs_dir = self.state / "agent_runs"
|
|
44
|
+
self.tasks_dir = self.state / "tasks"
|
|
45
|
+
self.evals_dir = self.state / "evals"
|
|
46
|
+
self.benchmarks_dir = self.state / "benchmarks"
|
|
47
|
+
self.memory_file = self.state / "memory.jsonl"
|
|
48
|
+
self.memory_db = self.state / "memory.sqlite3"
|
|
49
|
+
self.config_file = self.state / "config.json"
|
|
50
|
+
self.project_file = self.state / "project.json"
|
|
51
|
+
|
|
52
|
+
def init(self) -> None:
|
|
53
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
self.skills_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
self.traces_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
self.tool_traces_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
self.agent_runs_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
self.tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
self.evals_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
self.benchmarks_dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
if not self.memory_file.exists():
|
|
62
|
+
self.memory_file.write_text("", encoding="utf-8")
|
|
63
|
+
if not self.config_file.exists():
|
|
64
|
+
self.write_json(self.config_file, default_config())
|
|
65
|
+
|
|
66
|
+
def require_initialized(self) -> None:
|
|
67
|
+
if not self.state.exists():
|
|
68
|
+
raise FileNotFoundError(
|
|
69
|
+
f"Workspace is not initialized: {self.root}. Run `akernel init {self.root}` first."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def load_config(self) -> dict[str, Any]:
|
|
73
|
+
if not self.config_file.exists():
|
|
74
|
+
return default_config()
|
|
75
|
+
return normalize_config(self.read_json(self.config_file))
|
|
76
|
+
|
|
77
|
+
def save_config(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
78
|
+
normalized = normalize_config(data)
|
|
79
|
+
self.write_json(self.config_file, normalized)
|
|
80
|
+
return normalized
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def read_json(path: Path) -> dict[str, Any]:
|
|
84
|
+
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def write_json(path: Path, data: dict[str, Any]) -> None:
|
|
88
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def append_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
|
|
93
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
95
|
+
for row in rows:
|
|
96
|
+
handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def read_jsonl(path: Path) -> list[dict[str, Any]]:
|
|
100
|
+
if not path.exists():
|
|
101
|
+
return []
|
|
102
|
+
rows: list[dict[str, Any]] = []
|
|
103
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
104
|
+
if line.strip():
|
|
105
|
+
rows.append(json.loads(line))
|
|
106
|
+
return rows
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def default_config() -> dict[str, Any]:
|
|
110
|
+
return deepcopy(DEFAULT_CONFIG)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def normalize_config(data: dict[str, Any] | None) -> dict[str, Any]:
|
|
114
|
+
merged = merge_dicts(default_config(), data if isinstance(data, dict) else {})
|
|
115
|
+
merged["version"] = max(DEFAULT_CONFIG_VERSION, safe_int(merged.get("version"), DEFAULT_CONFIG_VERSION))
|
|
116
|
+
merged["default_budget"] = safe_int(merged.get("default_budget"), int(DEFAULT_CONFIG["default_budget"]))
|
|
117
|
+
merged["runtime_instructions"] = dedupe_strings(
|
|
118
|
+
merged.get("runtime_instructions"),
|
|
119
|
+
fallback=list(DEFAULT_RUNTIME_INSTRUCTIONS),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
command_policy = merged.get("command_policy")
|
|
123
|
+
if not isinstance(command_policy, dict):
|
|
124
|
+
command_policy = {}
|
|
125
|
+
command_policy["allowed_roots"] = dedupe_strings(
|
|
126
|
+
command_policy.get("allowed_roots"),
|
|
127
|
+
fallback=list(DEFAULT_COMMAND_POLICY["allowed_roots"]),
|
|
128
|
+
casefold=True,
|
|
129
|
+
)
|
|
130
|
+
command_policy["blocked_terms"] = dedupe_strings(
|
|
131
|
+
command_policy.get("blocked_terms"),
|
|
132
|
+
fallback=[],
|
|
133
|
+
casefold=True,
|
|
134
|
+
)
|
|
135
|
+
merged["command_policy"] = command_policy
|
|
136
|
+
return merged
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def merge_dicts(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
|
|
140
|
+
merged = deepcopy(base)
|
|
141
|
+
for key, value in overrides.items():
|
|
142
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
143
|
+
merged[key] = merge_dicts(merged[key], value)
|
|
144
|
+
else:
|
|
145
|
+
merged[key] = deepcopy(value)
|
|
146
|
+
return merged
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def dedupe_strings(values: Any, *, fallback: list[str], casefold: bool = False) -> list[str]:
|
|
150
|
+
source = values if isinstance(values, list) else fallback
|
|
151
|
+
result: list[str] = []
|
|
152
|
+
seen: set[str] = set()
|
|
153
|
+
for item in source:
|
|
154
|
+
if not isinstance(item, str):
|
|
155
|
+
continue
|
|
156
|
+
text = item.strip()
|
|
157
|
+
if not text:
|
|
158
|
+
continue
|
|
159
|
+
key = text.casefold() if casefold else text
|
|
160
|
+
if key in seen:
|
|
161
|
+
continue
|
|
162
|
+
seen.add(key)
|
|
163
|
+
result.append(text.casefold() if casefold else text)
|
|
164
|
+
return result or list(fallback)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def safe_int(value: Any, fallback: int) -> int:
|
|
168
|
+
try:
|
|
169
|
+
return int(value)
|
|
170
|
+
except (TypeError, ValueError):
|
|
171
|
+
return fallback
|