codecortex-context-engine 0.1.0a1__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.
- codecortex/__init__.py +3 -0
- codecortex/architecture/__init__.py +23 -0
- codecortex/architecture/drift.py +182 -0
- codecortex/architecture/inference.py +160 -0
- codecortex/backends/__init__.py +35 -0
- codecortex/backends/base.py +38 -0
- codecortex/backends/context.py +118 -0
- codecortex/backends/contracts.py +65 -0
- codecortex/backends/factory.py +60 -0
- codecortex/backends/graph.py +107 -0
- codecortex/backends/manager.py +303 -0
- codecortex/backends/mcp_client.py +196 -0
- codecortex/backends/pool.py +128 -0
- codecortex/backends/spec.py +83 -0
- codecortex/backends/symbols.py +189 -0
- codecortex/benchmark.py +211 -0
- codecortex/cli.py +409 -0
- codecortex/config.py +27 -0
- codecortex/context/__init__.py +13 -0
- codecortex/context/budget.py +62 -0
- codecortex/context/integrated.py +60 -0
- codecortex/context/pipeline.py +223 -0
- codecortex/core/__init__.py +1 -0
- codecortex/core/contracts.py +45 -0
- codecortex/core/errors.py +17 -0
- codecortex/core/models.py +69 -0
- codecortex/dashboard.py +259 -0
- codecortex/editing.py +41 -0
- codecortex/engines/__init__.py +5 -0
- codecortex/engines/builtin/__init__.py +5 -0
- codecortex/engines/builtin/factory.py +26 -0
- codecortex/engines/builtin/memory.py +35 -0
- codecortex/engines/builtin/repository.py +73 -0
- codecortex/engines/builtin/symbols.py +89 -0
- codecortex/engines/builtin/validation.py +54 -0
- codecortex/engines/registry.py +26 -0
- codecortex/entrypoint.py +266 -0
- codecortex/evaluation/__init__.py +55 -0
- codecortex/evaluation/external.py +265 -0
- codecortex/evaluation/production.py +670 -0
- codecortex/evaluation/regression.py +188 -0
- codecortex/gateway.py +38 -0
- codecortex/git_intelligence.py +252 -0
- codecortex/indexing/__init__.py +6 -0
- codecortex/indexing/graph.py +78 -0
- codecortex/indexing/impact.py +127 -0
- codecortex/indexing/incremental.py +163 -0
- codecortex/indexing/incremental_graph.py +189 -0
- codecortex/indexing/indexer.py +172 -0
- codecortex/indexing/relationships.py +179 -0
- codecortex/indexing/resolution.py +88 -0
- codecortex/integrations/__init__.py +5 -0
- codecortex/integrations/agents.py +235 -0
- codecortex/interfaces/__init__.py +1 -0
- codecortex/interfaces/mcp_bridge.py +66 -0
- codecortex/languages/__init__.py +5 -0
- codecortex/languages/native.py +166 -0
- codecortex/languages/registry.py +232 -0
- codecortex/mcp/__init__.py +5 -0
- codecortex/mcp/extended.py +114 -0
- codecortex/mcp/server.py +473 -0
- codecortex/memory/__init__.py +11 -0
- codecortex/memory/json_store.py +52 -0
- codecortex/memory/knowledge.py +193 -0
- codecortex/memory/team_store.py +193 -0
- codecortex/orchestrator.py +153 -0
- codecortex/pr_intelligence.py +214 -0
- codecortex/retrieval/__init__.py +16 -0
- codecortex/retrieval/hybrid.py +67 -0
- codecortex/retrieval/index.py +135 -0
- codecortex/retrieval/providers.py +67 -0
- codecortex/retrieval/repository.py +94 -0
- codecortex/router/__init__.py +5 -0
- codecortex/router/router.py +79 -0
- codecortex/runtime.py +69 -0
- codecortex/setup.py +100 -0
- codecortex/symbols/__init__.py +5 -0
- codecortex/symbols/providers.py +192 -0
- codecortex/telemetry/__init__.py +5 -0
- codecortex/telemetry/collector.py +43 -0
- codecortex/tracing/__init__.py +9 -0
- codecortex/tracing/task_trace.py +235 -0
- codecortex/workspace/__init__.py +9 -0
- codecortex/workspace/federation.py +173 -0
- codecortex_context_engine-0.1.0a1.dist-info/METADATA +381 -0
- codecortex_context_engine-0.1.0a1.dist-info/RECORD +90 -0
- codecortex_context_engine-0.1.0a1.dist-info/WHEEL +4 -0
- codecortex_context_engine-0.1.0a1.dist-info/entry_points.txt +3 -0
- codecortex_context_engine-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- codecortex_context_engine-0.1.0a1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Automatic project knowledge extraction and persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from codecortex.git_intelligence import GitIntelligence
|
|
11
|
+
from codecortex.memory.json_store import JsonMemoryStore
|
|
12
|
+
|
|
13
|
+
_LANGUAGE_BY_SUFFIX = {
|
|
14
|
+
".py": "Python",
|
|
15
|
+
".js": "JavaScript",
|
|
16
|
+
".jsx": "JavaScript",
|
|
17
|
+
".ts": "TypeScript",
|
|
18
|
+
".tsx": "TypeScript",
|
|
19
|
+
".go": "Go",
|
|
20
|
+
".rs": "Rust",
|
|
21
|
+
".java": "Java",
|
|
22
|
+
".c": "C",
|
|
23
|
+
".h": "C/C++",
|
|
24
|
+
".cc": "C++",
|
|
25
|
+
".cpp": "C++",
|
|
26
|
+
".cxx": "C++",
|
|
27
|
+
".hpp": "C++",
|
|
28
|
+
".cs": "C#",
|
|
29
|
+
".php": "PHP",
|
|
30
|
+
".rb": "Ruby",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_PACKAGE_FILES = {
|
|
34
|
+
"pyproject.toml": "Python/pip",
|
|
35
|
+
"requirements.txt": "Python/pip",
|
|
36
|
+
"package.json": "Node.js",
|
|
37
|
+
"pnpm-lock.yaml": "pnpm",
|
|
38
|
+
"yarn.lock": "Yarn",
|
|
39
|
+
"go.mod": "Go modules",
|
|
40
|
+
"Cargo.toml": "Cargo",
|
|
41
|
+
"pom.xml": "Maven",
|
|
42
|
+
"build.gradle": "Gradle",
|
|
43
|
+
"composer.json": "Composer",
|
|
44
|
+
"Gemfile": "Bundler",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_TEST_MARKERS = {
|
|
48
|
+
"pytest": ("pytest.ini", "conftest.py", "pytest"),
|
|
49
|
+
"vitest": ("vitest.config", "vitest"),
|
|
50
|
+
"jest": ("jest.config", "jest"),
|
|
51
|
+
"go test": ("_test.go",),
|
|
52
|
+
"cargo test": ("#[test]",),
|
|
53
|
+
"JUnit": ("junit", "@test"),
|
|
54
|
+
"RSpec": ("spec_helper", "rspec"),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_ARCHITECTURE_DIRS = {
|
|
58
|
+
"controllers": "controller layer",
|
|
59
|
+
"controller": "controller layer",
|
|
60
|
+
"services": "service layer",
|
|
61
|
+
"service": "service layer",
|
|
62
|
+
"repositories": "repository layer",
|
|
63
|
+
"repository": "repository layer",
|
|
64
|
+
"models": "model layer",
|
|
65
|
+
"domain": "domain layer",
|
|
66
|
+
"api": "API layer",
|
|
67
|
+
"routes": "routing layer",
|
|
68
|
+
"adapters": "adapter layer",
|
|
69
|
+
"ports": "ports layer",
|
|
70
|
+
"core": "core layer",
|
|
71
|
+
"infra": "infrastructure layer",
|
|
72
|
+
"infrastructure": "infrastructure layer",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_ENTRY_NAMES = {
|
|
76
|
+
"main.py",
|
|
77
|
+
"app.py",
|
|
78
|
+
"manage.py",
|
|
79
|
+
"index.js",
|
|
80
|
+
"index.ts",
|
|
81
|
+
"server.js",
|
|
82
|
+
"server.ts",
|
|
83
|
+
"main.go",
|
|
84
|
+
"main.rs",
|
|
85
|
+
"Program.cs",
|
|
86
|
+
"Main.java",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_EXCLUDED = {".git", ".codecortex", ".venv", "venv", "node_modules", "dist", "build"}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class ProjectKnowledge:
|
|
94
|
+
languages: tuple[tuple[str, int], ...]
|
|
95
|
+
package_systems: tuple[str, ...]
|
|
96
|
+
entry_points: tuple[str, ...]
|
|
97
|
+
test_frameworks: tuple[str, ...]
|
|
98
|
+
architecture: tuple[str, ...]
|
|
99
|
+
hot_files: tuple[str, ...]
|
|
100
|
+
|
|
101
|
+
def facts(self) -> dict[str, str]:
|
|
102
|
+
return {
|
|
103
|
+
"languages": ", ".join(f"{name} ({count})" for name, count in self.languages),
|
|
104
|
+
"package_systems": ", ".join(self.package_systems) or "unknown",
|
|
105
|
+
"entry_points": ", ".join(self.entry_points) or "not detected",
|
|
106
|
+
"test_frameworks": ", ".join(self.test_frameworks) or "not detected",
|
|
107
|
+
"architecture": ", ".join(self.architecture) or "no strong convention detected",
|
|
108
|
+
"hot_files": ", ".join(self.hot_files) or "not available",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ProjectKnowledgeExtractor:
|
|
113
|
+
def __init__(self, root: Path) -> None:
|
|
114
|
+
self.root = root.resolve()
|
|
115
|
+
|
|
116
|
+
def _files(self) -> list[Path]:
|
|
117
|
+
result: list[Path] = []
|
|
118
|
+
for path in self.root.rglob("*"):
|
|
119
|
+
if not path.is_file():
|
|
120
|
+
continue
|
|
121
|
+
relative = path.relative_to(self.root)
|
|
122
|
+
if any(part in _EXCLUDED for part in relative.parts):
|
|
123
|
+
continue
|
|
124
|
+
result.append(path)
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
def extract(self) -> ProjectKnowledge:
|
|
128
|
+
files = self._files()
|
|
129
|
+
languages: Counter[str] = Counter()
|
|
130
|
+
package_systems: set[str] = set()
|
|
131
|
+
entry_points: list[str] = []
|
|
132
|
+
architecture: set[str] = set()
|
|
133
|
+
searchable: list[str] = []
|
|
134
|
+
|
|
135
|
+
for path in files:
|
|
136
|
+
relative = path.relative_to(self.root)
|
|
137
|
+
language = _LANGUAGE_BY_SUFFIX.get(path.suffix.lower())
|
|
138
|
+
if language:
|
|
139
|
+
languages[language] += 1
|
|
140
|
+
package = _PACKAGE_FILES.get(path.name)
|
|
141
|
+
if package:
|
|
142
|
+
package_systems.add(package)
|
|
143
|
+
if path.name in _ENTRY_NAMES:
|
|
144
|
+
entry_points.append(relative.as_posix())
|
|
145
|
+
for part in relative.parts[:-1]:
|
|
146
|
+
marker = _ARCHITECTURE_DIRS.get(part.lower())
|
|
147
|
+
if marker:
|
|
148
|
+
architecture.add(marker)
|
|
149
|
+
if path.stat().st_size <= 512_000:
|
|
150
|
+
try:
|
|
151
|
+
searchable.append(path.read_text(encoding="utf-8").lower())
|
|
152
|
+
except (OSError, UnicodeDecodeError):
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
joined = "\n".join(searchable)
|
|
156
|
+
test_frameworks = {
|
|
157
|
+
name
|
|
158
|
+
for name, markers in _TEST_MARKERS.items()
|
|
159
|
+
if any(marker.lower() in joined or any(marker.lower() in p.name.lower() for p in files)
|
|
160
|
+
for marker in markers)
|
|
161
|
+
}
|
|
162
|
+
git = GitIntelligence(self.root).analyze(limit=300)
|
|
163
|
+
return ProjectKnowledge(
|
|
164
|
+
languages=tuple(languages.most_common()),
|
|
165
|
+
package_systems=tuple(sorted(package_systems)),
|
|
166
|
+
entry_points=tuple(sorted(set(entry_points))),
|
|
167
|
+
test_frameworks=tuple(sorted(test_frameworks)),
|
|
168
|
+
architecture=tuple(sorted(architecture)),
|
|
169
|
+
hot_files=tuple(item.path for item in git.hot_files[:10]),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def save(self, knowledge: ProjectKnowledge | None = None) -> Path:
|
|
173
|
+
knowledge = knowledge or self.extract()
|
|
174
|
+
path = self.root / ".codecortex" / "knowledge" / "project.json"
|
|
175
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
temp = path.with_suffix(".tmp")
|
|
177
|
+
temp.write_text(
|
|
178
|
+
json.dumps(asdict(knowledge), ensure_ascii=False, indent=2),
|
|
179
|
+
encoding="utf-8",
|
|
180
|
+
)
|
|
181
|
+
temp.replace(path)
|
|
182
|
+
return path
|
|
183
|
+
|
|
184
|
+
async def remember(
|
|
185
|
+
self,
|
|
186
|
+
store: JsonMemoryStore,
|
|
187
|
+
namespace: str = "project_knowledge",
|
|
188
|
+
) -> ProjectKnowledge:
|
|
189
|
+
knowledge = self.extract()
|
|
190
|
+
self.save(knowledge)
|
|
191
|
+
for key, value in knowledge.facts().items():
|
|
192
|
+
await store.put(namespace, key, value)
|
|
193
|
+
return knowledge
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Concurrent shared team memory backed by SQLite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from codecortex.core.contracts import MemoryStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class TeamMemoryEntry:
|
|
16
|
+
namespace: str
|
|
17
|
+
key: str
|
|
18
|
+
value: str
|
|
19
|
+
revision: int
|
|
20
|
+
actor: str
|
|
21
|
+
source: str
|
|
22
|
+
tags: tuple[str, ...]
|
|
23
|
+
updated_at: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RevisionConflict(RuntimeError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TeamMemoryStore(MemoryStore):
|
|
31
|
+
"""Shared memory with revisions, audit history, and optimistic concurrency."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, path: Path) -> None:
|
|
34
|
+
self.path = path
|
|
35
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
self._initialize()
|
|
37
|
+
|
|
38
|
+
def _connect(self) -> sqlite3.Connection:
|
|
39
|
+
connection = sqlite3.connect(self.path, timeout=5.0)
|
|
40
|
+
connection.row_factory = sqlite3.Row
|
|
41
|
+
connection.execute("PRAGMA journal_mode=WAL")
|
|
42
|
+
connection.execute("PRAGMA synchronous=NORMAL")
|
|
43
|
+
connection.execute("PRAGMA busy_timeout=5000")
|
|
44
|
+
return connection
|
|
45
|
+
|
|
46
|
+
def _initialize(self) -> None:
|
|
47
|
+
with self._connect() as connection:
|
|
48
|
+
connection.executescript(
|
|
49
|
+
"""
|
|
50
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
51
|
+
namespace TEXT NOT NULL,
|
|
52
|
+
key TEXT NOT NULL,
|
|
53
|
+
value TEXT NOT NULL,
|
|
54
|
+
revision INTEGER NOT NULL,
|
|
55
|
+
actor TEXT NOT NULL,
|
|
56
|
+
source TEXT NOT NULL,
|
|
57
|
+
tags TEXT NOT NULL,
|
|
58
|
+
updated_at TEXT NOT NULL,
|
|
59
|
+
PRIMARY KEY(namespace, key)
|
|
60
|
+
);
|
|
61
|
+
CREATE TABLE IF NOT EXISTS memory_history (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
namespace TEXT NOT NULL,
|
|
64
|
+
key TEXT NOT NULL,
|
|
65
|
+
value TEXT NOT NULL,
|
|
66
|
+
revision INTEGER NOT NULL,
|
|
67
|
+
actor TEXT NOT NULL,
|
|
68
|
+
source TEXT NOT NULL,
|
|
69
|
+
tags TEXT NOT NULL,
|
|
70
|
+
updated_at TEXT NOT NULL
|
|
71
|
+
);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_memory_namespace
|
|
73
|
+
ON memories(namespace);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_history_key
|
|
75
|
+
ON memory_history(namespace, key, revision);
|
|
76
|
+
"""
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async def put(self, namespace: str, key: str, value: str) -> None:
|
|
80
|
+
self.put_entry(namespace, key, value)
|
|
81
|
+
|
|
82
|
+
def put_entry(
|
|
83
|
+
self,
|
|
84
|
+
namespace: str,
|
|
85
|
+
key: str,
|
|
86
|
+
value: str,
|
|
87
|
+
*,
|
|
88
|
+
actor: str = "system",
|
|
89
|
+
source: str = "manual",
|
|
90
|
+
tags: tuple[str, ...] = (),
|
|
91
|
+
expected_revision: int | None = None,
|
|
92
|
+
) -> TeamMemoryEntry:
|
|
93
|
+
if not namespace.strip() or not key.strip():
|
|
94
|
+
raise ValueError("namespace and key are required")
|
|
95
|
+
now = datetime.now(UTC).isoformat()
|
|
96
|
+
encoded_tags = json.dumps(sorted(set(tags)), ensure_ascii=False)
|
|
97
|
+
with self._connect() as connection:
|
|
98
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
99
|
+
row = connection.execute(
|
|
100
|
+
"SELECT revision FROM memories WHERE namespace = ? AND key = ?",
|
|
101
|
+
(namespace, key),
|
|
102
|
+
).fetchone()
|
|
103
|
+
current_revision = int(row["revision"]) if row else 0
|
|
104
|
+
if expected_revision is not None and expected_revision != current_revision:
|
|
105
|
+
raise RevisionConflict(
|
|
106
|
+
f"expected revision {expected_revision}, found {current_revision}"
|
|
107
|
+
)
|
|
108
|
+
revision = current_revision + 1
|
|
109
|
+
connection.execute(
|
|
110
|
+
"""
|
|
111
|
+
INSERT INTO memories(namespace, key, value, revision, actor, source, tags, updated_at)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
113
|
+
ON CONFLICT(namespace, key) DO UPDATE SET
|
|
114
|
+
value = excluded.value,
|
|
115
|
+
revision = excluded.revision,
|
|
116
|
+
actor = excluded.actor,
|
|
117
|
+
source = excluded.source,
|
|
118
|
+
tags = excluded.tags,
|
|
119
|
+
updated_at = excluded.updated_at
|
|
120
|
+
""",
|
|
121
|
+
(namespace, key, value, revision, actor, source, encoded_tags, now),
|
|
122
|
+
)
|
|
123
|
+
connection.execute(
|
|
124
|
+
"""
|
|
125
|
+
INSERT INTO memory_history(namespace, key, value, revision, actor, source, tags, updated_at)
|
|
126
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
127
|
+
""",
|
|
128
|
+
(namespace, key, value, revision, actor, source, encoded_tags, now),
|
|
129
|
+
)
|
|
130
|
+
return TeamMemoryEntry(namespace, key, value, revision, actor, source, tags, now)
|
|
131
|
+
|
|
132
|
+
async def get(self, namespace: str, key: str) -> str | None:
|
|
133
|
+
entry = self.get_entry(namespace, key)
|
|
134
|
+
return entry.value if entry else None
|
|
135
|
+
|
|
136
|
+
def get_entry(self, namespace: str, key: str) -> TeamMemoryEntry | None:
|
|
137
|
+
with self._connect() as connection:
|
|
138
|
+
row = connection.execute(
|
|
139
|
+
"SELECT * FROM memories WHERE namespace = ? AND key = ?",
|
|
140
|
+
(namespace, key),
|
|
141
|
+
).fetchone()
|
|
142
|
+
return self._row_to_entry(row) if row else None
|
|
143
|
+
|
|
144
|
+
async def search(self, namespace: str, query: str, limit: int = 10) -> list[str]:
|
|
145
|
+
return [entry.value for entry in self.search_entries(namespace, query, limit)]
|
|
146
|
+
|
|
147
|
+
def search_entries(self, namespace: str, query: str, limit: int = 10) -> list[TeamMemoryEntry]:
|
|
148
|
+
terms = [term.lower() for term in query.split() if term.strip()]
|
|
149
|
+
with self._connect() as connection:
|
|
150
|
+
rows = connection.execute(
|
|
151
|
+
"SELECT * FROM memories WHERE namespace = ? ORDER BY updated_at DESC",
|
|
152
|
+
(namespace,),
|
|
153
|
+
).fetchall()
|
|
154
|
+
scored: list[tuple[int, TeamMemoryEntry]] = []
|
|
155
|
+
for row in rows:
|
|
156
|
+
entry = self._row_to_entry(row)
|
|
157
|
+
haystack = f"{entry.key} {entry.value} {' '.join(entry.tags)}".lower()
|
|
158
|
+
score = sum(3 if term in entry.key.lower() else 1 for term in terms if term in haystack)
|
|
159
|
+
if score or not terms:
|
|
160
|
+
scored.append((score, entry))
|
|
161
|
+
scored.sort(key=lambda item: (-item[0], item[1].updated_at), reverse=False)
|
|
162
|
+
return [entry for _, entry in scored[: max(1, limit)]]
|
|
163
|
+
|
|
164
|
+
def history(self, namespace: str, key: str, limit: int = 50) -> list[TeamMemoryEntry]:
|
|
165
|
+
with self._connect() as connection:
|
|
166
|
+
rows = connection.execute(
|
|
167
|
+
"""
|
|
168
|
+
SELECT namespace, key, value, revision, actor, source, tags, updated_at
|
|
169
|
+
FROM memory_history
|
|
170
|
+
WHERE namespace = ? AND key = ?
|
|
171
|
+
ORDER BY revision DESC
|
|
172
|
+
LIMIT ?
|
|
173
|
+
""",
|
|
174
|
+
(namespace, key, max(1, limit)),
|
|
175
|
+
).fetchall()
|
|
176
|
+
return [self._row_to_entry(row) for row in rows]
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _row_to_entry(row: sqlite3.Row) -> TeamMemoryEntry:
|
|
180
|
+
try:
|
|
181
|
+
tags = tuple(str(item) for item in json.loads(row["tags"]))
|
|
182
|
+
except (json.JSONDecodeError, TypeError):
|
|
183
|
+
tags = ()
|
|
184
|
+
return TeamMemoryEntry(
|
|
185
|
+
namespace=str(row["namespace"]),
|
|
186
|
+
key=str(row["key"]),
|
|
187
|
+
value=str(row["value"]),
|
|
188
|
+
revision=int(row["revision"]),
|
|
189
|
+
actor=str(row["actor"]),
|
|
190
|
+
source=str(row["source"]),
|
|
191
|
+
tags=tags,
|
|
192
|
+
updated_at=str(row["updated_at"]),
|
|
193
|
+
)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Core request orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from time import perf_counter
|
|
7
|
+
|
|
8
|
+
from codecortex.context import BudgetContextProcessor
|
|
9
|
+
from codecortex.core.models import AgentRequest, Capability, EngineResult, ExecutionResult
|
|
10
|
+
from codecortex.engines import EngineRegistry
|
|
11
|
+
from codecortex.router import AdaptiveRouter
|
|
12
|
+
from codecortex.telemetry import TelemetryCollector
|
|
13
|
+
from codecortex.tracing import TaskTraceRecorder
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Orchestrator:
|
|
17
|
+
"""Route requests, execute independent engines concurrently, and fit context."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
registry: EngineRegistry,
|
|
22
|
+
router: AdaptiveRouter,
|
|
23
|
+
context_processor: BudgetContextProcessor | None = None,
|
|
24
|
+
telemetry: TelemetryCollector | None = None,
|
|
25
|
+
tracer: TaskTraceRecorder | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.registry = registry
|
|
28
|
+
self.router = router
|
|
29
|
+
self.context_processor = context_processor or BudgetContextProcessor()
|
|
30
|
+
self.telemetry = telemetry or TelemetryCollector()
|
|
31
|
+
self.tracer = tracer
|
|
32
|
+
|
|
33
|
+
async def execute(self, request: AgentRequest) -> ExecutionResult:
|
|
34
|
+
if self.tracer is None:
|
|
35
|
+
return await self._execute(request, None, None)
|
|
36
|
+
trace_id = str(request.metadata.get("trace_id") or self.tracer.new_trace_id())
|
|
37
|
+
attributes: dict[str, object] = {"query_chars": len(request.query)}
|
|
38
|
+
async with self.tracer.async_span(
|
|
39
|
+
"request.execute",
|
|
40
|
+
trace_id=trace_id,
|
|
41
|
+
attributes=attributes,
|
|
42
|
+
) as root_span:
|
|
43
|
+
result = await self._execute(request, trace_id, root_span)
|
|
44
|
+
attributes["context_tokens"] = result.context_tokens
|
|
45
|
+
attributes["capabilities"] = [item.value for item in result.plan.selected]
|
|
46
|
+
return result.model_copy(
|
|
47
|
+
update={"metadata": {**result.metadata, "trace_id": trace_id}}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def _run_engine(
|
|
51
|
+
self,
|
|
52
|
+
capability: Capability,
|
|
53
|
+
request: AgentRequest,
|
|
54
|
+
trace_id: str | None,
|
|
55
|
+
parent_span: str | None,
|
|
56
|
+
) -> EngineResult | None:
|
|
57
|
+
engine = self.registry.get(capability)
|
|
58
|
+
if engine is None or not await engine.health():
|
|
59
|
+
self.telemetry.emit("engine.skipped", capability=capability.value)
|
|
60
|
+
return None
|
|
61
|
+
started = perf_counter()
|
|
62
|
+
try:
|
|
63
|
+
if self.tracer and trace_id:
|
|
64
|
+
attrs: dict[str, object] = {"capability": capability.value}
|
|
65
|
+
async with self.tracer.async_span(
|
|
66
|
+
"engine.execute",
|
|
67
|
+
trace_id=trace_id,
|
|
68
|
+
parent_id=parent_span,
|
|
69
|
+
attributes=attrs,
|
|
70
|
+
):
|
|
71
|
+
result = await engine.execute(request)
|
|
72
|
+
attrs["chunks"] = len(result.chunks)
|
|
73
|
+
attrs["context_tokens"] = sum(chunk.tokens for chunk in result.chunks)
|
|
74
|
+
else:
|
|
75
|
+
result = await engine.execute(request)
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
self.telemetry.emit(
|
|
78
|
+
"engine.failed",
|
|
79
|
+
capability=capability.value,
|
|
80
|
+
error_type=type(exc).__name__,
|
|
81
|
+
)
|
|
82
|
+
if request.metadata.get("strict_engines"):
|
|
83
|
+
raise
|
|
84
|
+
return None
|
|
85
|
+
self.telemetry.emit(
|
|
86
|
+
"engine.executed",
|
|
87
|
+
capability=capability.value,
|
|
88
|
+
duration_ms=(perf_counter() - started) * 1000,
|
|
89
|
+
)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
async def _execute(
|
|
93
|
+
self,
|
|
94
|
+
request: AgentRequest,
|
|
95
|
+
trace_id: str | None,
|
|
96
|
+
parent_span: str | None,
|
|
97
|
+
) -> ExecutionResult:
|
|
98
|
+
plan = self.router.route(request)
|
|
99
|
+
self.telemetry.emit(
|
|
100
|
+
"route.created",
|
|
101
|
+
kind=plan.request_kind.value,
|
|
102
|
+
capabilities=[capability.value for capability in plan.selected],
|
|
103
|
+
)
|
|
104
|
+
if self.tracer and trace_id:
|
|
105
|
+
self.tracer.record(
|
|
106
|
+
"route.created",
|
|
107
|
+
trace_id=trace_id,
|
|
108
|
+
parent_id=parent_span,
|
|
109
|
+
attributes={
|
|
110
|
+
"kind": plan.request_kind.value,
|
|
111
|
+
"capabilities": [item.value for item in plan.selected],
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
capabilities = [item for item in plan.selected if item != Capability.CONTEXT]
|
|
116
|
+
outcomes = await asyncio.gather(
|
|
117
|
+
*(
|
|
118
|
+
self._run_engine(capability, request, trace_id, parent_span)
|
|
119
|
+
for capability in capabilities
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
results = [result for result in outcomes if result is not None]
|
|
123
|
+
all_chunks = [chunk for result in results for chunk in result.chunks]
|
|
124
|
+
|
|
125
|
+
original_tokens = sum(chunk.tokens for chunk in all_chunks)
|
|
126
|
+
fitted = await self.context_processor.fit(all_chunks, plan.context_budget)
|
|
127
|
+
fitted_sources = {(chunk.source, chunk.content) for chunk in fitted}
|
|
128
|
+
normalized_results: list[EngineResult] = []
|
|
129
|
+
for result in results:
|
|
130
|
+
kept = [
|
|
131
|
+
chunk for chunk in result.chunks if (chunk.source, chunk.content) in fitted_sources
|
|
132
|
+
]
|
|
133
|
+
normalized_results.append(result.model_copy(update={"chunks": kept}))
|
|
134
|
+
|
|
135
|
+
context_tokens = sum(chunk.tokens for chunk in fitted)
|
|
136
|
+
self.telemetry.emit(
|
|
137
|
+
"context.fitted",
|
|
138
|
+
budget=plan.context_budget,
|
|
139
|
+
original=original_tokens,
|
|
140
|
+
used=context_tokens,
|
|
141
|
+
saved=max(0, original_tokens - context_tokens),
|
|
142
|
+
chunks=len(fitted),
|
|
143
|
+
)
|
|
144
|
+
return ExecutionResult(
|
|
145
|
+
request=request,
|
|
146
|
+
plan=plan,
|
|
147
|
+
results=normalized_results,
|
|
148
|
+
context_tokens=context_tokens,
|
|
149
|
+
metadata={
|
|
150
|
+
"original_context_tokens": original_tokens,
|
|
151
|
+
"context_tokens_saved": max(0, original_tokens - context_tokens),
|
|
152
|
+
},
|
|
153
|
+
)
|