nexus-coding-agent 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.
- nexus/__init__.py +4 -0
- nexus/application/__init__.py +2 -0
- nexus/application/approval_service.py +130 -0
- nexus/application/diff_service.py +177 -0
- nexus/application/event_publisher.py +180 -0
- nexus/application/execution_context.py +29 -0
- nexus/application/execution_ledger.py +126 -0
- nexus/application/model_call_context.py +29 -0
- nexus/application/observed_model_gateway.py +194 -0
- nexus/application/plan_approval_service.py +250 -0
- nexus/application/planning.py +668 -0
- nexus/application/runtime.py +624 -0
- nexus/application/session_service.py +411 -0
- nexus/application/structured_output.py +96 -0
- nexus/application/telemetry.py +396 -0
- nexus/application/telemetry_subscriber.py +216 -0
- nexus/application/tool_runtime.py +496 -0
- nexus/application/tracing_dispatcher.py +283 -0
- nexus/application/validation.py +293 -0
- nexus/config/__init__.py +17 -0
- nexus/config/loader.py +253 -0
- nexus/config/models.py +310 -0
- nexus/context/__init__.py +6 -0
- nexus/context/budget.py +24 -0
- nexus/context/builder.py +92 -0
- nexus/context/chunking.py +65 -0
- nexus/context/explorer.py +246 -0
- nexus/context/integration.py +63 -0
- nexus/context/manager.py +328 -0
- nexus/context/retrieval.py +261 -0
- nexus/domain/__init__.py +2 -0
- nexus/domain/agent_decision.py +113 -0
- nexus/domain/agent_state.py +49 -0
- nexus/domain/approvals.py +64 -0
- nexus/domain/context.py +108 -0
- nexus/domain/exploration.py +86 -0
- nexus/domain/mcp.py +51 -0
- nexus/domain/model.py +173 -0
- nexus/domain/observability.py +326 -0
- nexus/domain/persistence.py +122 -0
- nexus/domain/planning.py +329 -0
- nexus/domain/ports/__init__.py +61 -0
- nexus/domain/ports/agent_decision.py +9 -0
- nexus/domain/ports/approval_unit_of_work.py +27 -0
- nexus/domain/ports/checkpoint_provider.py +11 -0
- nexus/domain/ports/context.py +71 -0
- nexus/domain/ports/events.py +21 -0
- nexus/domain/ports/graph_runtime.py +30 -0
- nexus/domain/ports/mcp.py +23 -0
- nexus/domain/ports/model_gateway.py +21 -0
- nexus/domain/ports/model_input_budget.py +15 -0
- nexus/domain/ports/planning.py +62 -0
- nexus/domain/ports/repositories.py +54 -0
- nexus/domain/ports/repository_context.py +18 -0
- nexus/domain/ports/session_unit_of_work.py +33 -0
- nexus/domain/ports/skills.py +41 -0
- nexus/domain/ports/tooling.py +39 -0
- nexus/domain/ports/tracer.py +13 -0
- nexus/domain/ports/validation.py +32 -0
- nexus/domain/runtime_events.py +445 -0
- nexus/domain/skills.py +129 -0
- nexus/domain/tooling.py +163 -0
- nexus/domain/validation.py +161 -0
- nexus/errors/__init__.py +82 -0
- nexus/evaluation/__init__.py +7 -0
- nexus/evaluation/assertions.py +4 -0
- nexus/evaluation/evaluator.py +263 -0
- nexus/evaluation/fixtures.py +36 -0
- nexus/evaluation/harness.py +606 -0
- nexus/evaluation/loader.py +415 -0
- nexus/evaluation/metrics.py +27 -0
- nexus/evaluation/models.py +362 -0
- nexus/evaluation/report.py +43 -0
- nexus/evaluation/runner.py +480 -0
- nexus/infrastructure/__init__.py +2 -0
- nexus/infrastructure/asyncio_compat.py +13 -0
- nexus/infrastructure/bootstrap/__init__.py +17 -0
- nexus/infrastructure/bootstrap/composition.py +663 -0
- nexus/infrastructure/checkpoint/__init__.py +5 -0
- nexus/infrastructure/checkpoint/postgres.py +62 -0
- nexus/infrastructure/database.py +39 -0
- nexus/infrastructure/embedding.py +61 -0
- nexus/infrastructure/graph/__init__.py +6 -0
- nexus/infrastructure/graph/day4_runtime.py +966 -0
- nexus/infrastructure/graph/langgraph_runtime.py +172 -0
- nexus/infrastructure/mcp/__init__.py +5 -0
- nexus/infrastructure/mcp/manager.py +380 -0
- nexus/infrastructure/model_gateway/__init__.py +6 -0
- nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
- nexus/infrastructure/observability/__init__.py +7 -0
- nexus/infrastructure/observability/console.py +31 -0
- nexus/infrastructure/observability/langsmith.py +236 -0
- nexus/infrastructure/observability/structured_logger.py +70 -0
- nexus/infrastructure/persistence/__init__.py +13 -0
- nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
- nexus/infrastructure/persistence/models.py +146 -0
- nexus/infrastructure/persistence/repositories.py +326 -0
- nexus/infrastructure/persistence/unit_of_work.py +82 -0
- nexus/infrastructure/repository_files.py +117 -0
- nexus/infrastructure/sandbox/__init__.py +5 -0
- nexus/infrastructure/sandbox/local_process.py +353 -0
- nexus/infrastructure/semantic.py +276 -0
- nexus/infrastructure/semantic_models.py +56 -0
- nexus/interfaces/__init__.py +2 -0
- nexus/interfaces/cli/__init__.py +2 -0
- nexus/interfaces/cli/__main__.py +6 -0
- nexus/interfaces/cli/app.py +266 -0
- nexus/interfaces/cli/renderer.py +118 -0
- nexus/security/__init__.py +14 -0
- nexus/security/approval_policies.py +73 -0
- nexus/security/command_policy.py +117 -0
- nexus/security/executables.py +107 -0
- nexus/security/git_commands.py +65 -0
- nexus/security/workspace.py +93 -0
- nexus/skills/__init__.py +7 -0
- nexus/skills/builtin/__init__.py +1 -0
- nexus/skills/builtin/debug-python/SKILL.md +26 -0
- nexus/skills/builtin/review-repository/SKILL.md +26 -0
- nexus/skills/builtin/write-tests/SKILL.md +26 -0
- nexus/skills/loader.py +439 -0
- nexus/skills/registry.py +146 -0
- nexus/skills/selector.py +152 -0
- nexus/tools/__init__.py +6 -0
- nexus/tools/editing.py +312 -0
- nexus/tools/mcp.py +160 -0
- nexus/tools/native.py +654 -0
- nexus/tools/registry.py +27 -0
- nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
- nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
- nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Deterministic normalized UTF-8 line windows and candidate identities."""
|
|
2
|
+
|
|
3
|
+
from hashlib import sha256
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
|
|
6
|
+
from nexus.domain.context import CodeChunk, ContextCandidate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def normalize_text(content: str) -> str:
|
|
10
|
+
return content.removeprefix("\ufeff").replace("\r\n", "\n").replace("\r", "\n")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def text_hash(content: str) -> str:
|
|
14
|
+
return sha256(normalize_text(content).encode("utf-8")).hexdigest()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def language_for(path: str) -> str:
|
|
18
|
+
suffix = PurePosixPath(path).suffix.lower()
|
|
19
|
+
return {
|
|
20
|
+
".py": "python",
|
|
21
|
+
".java": "java",
|
|
22
|
+
".js": "javascript",
|
|
23
|
+
".ts": "typescript",
|
|
24
|
+
".md": "markdown",
|
|
25
|
+
".toml": "toml",
|
|
26
|
+
".json": "json",
|
|
27
|
+
".go": "go",
|
|
28
|
+
".rs": "rust",
|
|
29
|
+
".sql": "sql",
|
|
30
|
+
}.get(suffix, suffix.lstrip(".") or "text")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LineWindowChunker:
|
|
34
|
+
def chunk(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
file_path: str,
|
|
38
|
+
language: str,
|
|
39
|
+
content: str,
|
|
40
|
+
file_hash: str,
|
|
41
|
+
) -> tuple[CodeChunk, ...]:
|
|
42
|
+
lines = normalize_text(content).splitlines(keepends=True)
|
|
43
|
+
starts = (0,) if 0 < len(lines) <= 120 else range(0, len(lines), 100)
|
|
44
|
+
return tuple(
|
|
45
|
+
CodeChunk(
|
|
46
|
+
file_path.replace("\\", "/"),
|
|
47
|
+
language,
|
|
48
|
+
None,
|
|
49
|
+
start + 1,
|
|
50
|
+
min(start + 120, len(lines)),
|
|
51
|
+
"".join(lines[start : start + 120]),
|
|
52
|
+
text_hash("".join(lines[start : start + 120])),
|
|
53
|
+
file_hash,
|
|
54
|
+
)
|
|
55
|
+
for start in starts
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def identity(candidate: ContextCandidate) -> tuple[str, int, int, str]:
|
|
60
|
+
chunk = candidate.chunk
|
|
61
|
+
return chunk.file_path, chunk.start_line, chunk.end_line, chunk.content_hash
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def estimated_tokens(content: str) -> int:
|
|
65
|
+
return (len(content) + 3) // 4
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Bounded, ToolRuntime-only Day 4 repository exploration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import PurePosixPath
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from nexus.application.tool_runtime import ToolRuntime
|
|
10
|
+
from nexus.domain.exploration import (
|
|
11
|
+
ExplorationRequest,
|
|
12
|
+
ExplorationResult,
|
|
13
|
+
RepositoryFileEvidence,
|
|
14
|
+
RepositoryInstruction,
|
|
15
|
+
)
|
|
16
|
+
from nexus.domain.tooling import ToolInvocation, ToolResult
|
|
17
|
+
from nexus.errors import ContextError
|
|
18
|
+
|
|
19
|
+
_MANIFEST_NAMES = {
|
|
20
|
+
"pyproject.toml",
|
|
21
|
+
"package.json",
|
|
22
|
+
"pom.xml",
|
|
23
|
+
"cargo.toml",
|
|
24
|
+
"go.mod",
|
|
25
|
+
"requirements.txt",
|
|
26
|
+
}
|
|
27
|
+
_TERM_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_.\-/]{2,}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SelectiveRepositoryExplorer:
|
|
31
|
+
def __init__(self, tool_runtime: ToolRuntime) -> None:
|
|
32
|
+
self._tool_runtime = tool_runtime
|
|
33
|
+
|
|
34
|
+
async def explore(self, request: ExplorationRequest) -> ExplorationResult:
|
|
35
|
+
results: list[ToolResult] = []
|
|
36
|
+
|
|
37
|
+
async def invoke(name: str, arguments: dict[str, object]) -> ToolResult | None:
|
|
38
|
+
if len(results) >= 24:
|
|
39
|
+
return None
|
|
40
|
+
result = await self._tool_runtime.execute(
|
|
41
|
+
ToolInvocation(
|
|
42
|
+
str(uuid4()),
|
|
43
|
+
name,
|
|
44
|
+
arguments,
|
|
45
|
+
request.run_id,
|
|
46
|
+
request.session_id,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
results.append(result)
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
initial_status = await invoke("git_status", {})
|
|
53
|
+
listing = await invoke("list_files", {"path": ".", "recursive": False})
|
|
54
|
+
if (
|
|
55
|
+
initial_status is None
|
|
56
|
+
or listing is None
|
|
57
|
+
or not initial_status.success
|
|
58
|
+
or not listing.success
|
|
59
|
+
):
|
|
60
|
+
raise ContextError(
|
|
61
|
+
"Nexus could not establish repository exploration evidence.",
|
|
62
|
+
code="REPOSITORY_EXPLORATION_FAILED",
|
|
63
|
+
retryable=True,
|
|
64
|
+
)
|
|
65
|
+
top_level = tuple(_string_items(listing, "paths"))
|
|
66
|
+
instructions: list[RepositoryInstruction] = []
|
|
67
|
+
manifests: list[RepositoryFileEvidence] = []
|
|
68
|
+
|
|
69
|
+
if "AGENTS.md" in top_level:
|
|
70
|
+
root_instruction = await invoke("read_file", {"path": "AGENTS.md", "max_lines": 400})
|
|
71
|
+
if root_instruction is not None and root_instruction.success:
|
|
72
|
+
instructions.append(_instruction(root_instruction, "AGENTS.md", ".", 0))
|
|
73
|
+
|
|
74
|
+
manifest_paths = [
|
|
75
|
+
path
|
|
76
|
+
for path in top_level
|
|
77
|
+
if _is_manifest(path)
|
|
78
|
+
]
|
|
79
|
+
config_search = await invoke("search_files", {"pattern": ".nexus/config.toml"})
|
|
80
|
+
if config_search is not None and config_search.success:
|
|
81
|
+
manifest_paths.extend(_string_items(config_search, "paths"))
|
|
82
|
+
for path in _unique(manifest_paths)[:8]:
|
|
83
|
+
result = await invoke("read_file", {"path": path, "max_lines": 120})
|
|
84
|
+
if result is not None and result.success:
|
|
85
|
+
manifests.append(
|
|
86
|
+
RepositoryFileEvidence(
|
|
87
|
+
path,
|
|
88
|
+
"manifest",
|
|
89
|
+
"Root project/configuration manifest.",
|
|
90
|
+
_bounded_summary(result),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
relevant: dict[str, RepositoryFileEvidence] = {}
|
|
95
|
+
terms = _task_terms(request.task)
|
|
96
|
+
for term in terms:
|
|
97
|
+
if len(results) >= 22 or len(relevant) >= 12:
|
|
98
|
+
break
|
|
99
|
+
pattern = term if "/" in term else f"*{term}*"
|
|
100
|
+
found = await invoke("search_files", {"pattern": pattern})
|
|
101
|
+
if found is not None and found.success:
|
|
102
|
+
for path in _string_items(found, "paths"):
|
|
103
|
+
if path not in relevant and _is_candidate(path):
|
|
104
|
+
relevant[path] = RepositoryFileEvidence(
|
|
105
|
+
path,
|
|
106
|
+
"task_relevant",
|
|
107
|
+
f"Filename matched task token {term!r}.",
|
|
108
|
+
f"Task-relevant file: {path}",
|
|
109
|
+
)
|
|
110
|
+
if len(relevant) >= 12:
|
|
111
|
+
break
|
|
112
|
+
if len(results) >= 22 or len(relevant) >= 12:
|
|
113
|
+
break
|
|
114
|
+
matched = await invoke(
|
|
115
|
+
"lexical_search",
|
|
116
|
+
{"pattern": term, "path": ".", "case_sensitive": False},
|
|
117
|
+
)
|
|
118
|
+
if matched is not None and matched.success:
|
|
119
|
+
for item in _dict_items(matched, "matches"):
|
|
120
|
+
matched_path = item.get("path")
|
|
121
|
+
if (
|
|
122
|
+
isinstance(matched_path, str)
|
|
123
|
+
and matched_path not in relevant
|
|
124
|
+
and _is_candidate(matched_path)
|
|
125
|
+
):
|
|
126
|
+
relevant[matched_path] = RepositoryFileEvidence(
|
|
127
|
+
matched_path,
|
|
128
|
+
"task_relevant",
|
|
129
|
+
f"Content matched task token {term!r}.",
|
|
130
|
+
f"Observed lexical match in {matched_path}.",
|
|
131
|
+
)
|
|
132
|
+
if len(relevant) >= 12:
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
nested_candidates = _ancestor_instruction_paths(tuple(relevant))
|
|
136
|
+
known = {item.path for item in instructions}
|
|
137
|
+
for path in nested_candidates:
|
|
138
|
+
if len(results) >= 24 or path in known:
|
|
139
|
+
continue
|
|
140
|
+
result = await invoke("read_file", {"path": path, "max_lines": 400})
|
|
141
|
+
if result is not None and result.success:
|
|
142
|
+
scope = str(PurePosixPath(path).parent)
|
|
143
|
+
content = _content(result)
|
|
144
|
+
if _weakens_nexus_safety(content):
|
|
145
|
+
raise ContextError(
|
|
146
|
+
"A nested repository instruction conflicts with Nexus safety.",
|
|
147
|
+
code="REPOSITORY_INSTRUCTION_CONFLICT",
|
|
148
|
+
)
|
|
149
|
+
instructions.append(
|
|
150
|
+
_instruction(result, path, scope, len(PurePosixPath(scope).parts))
|
|
151
|
+
)
|
|
152
|
+
known.add(path)
|
|
153
|
+
|
|
154
|
+
return ExplorationResult(
|
|
155
|
+
tuple(sorted(instructions, key=lambda item: (item.depth, item.path))),
|
|
156
|
+
tuple(manifests[:8]),
|
|
157
|
+
top_level,
|
|
158
|
+
tuple(list(relevant.values())[:12]),
|
|
159
|
+
initial_status,
|
|
160
|
+
tuple(results),
|
|
161
|
+
len(results) >= 24 or _truncated(listing),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _instruction(
|
|
166
|
+
result: ToolResult,
|
|
167
|
+
path: str,
|
|
168
|
+
scope: str,
|
|
169
|
+
depth: int,
|
|
170
|
+
) -> RepositoryInstruction:
|
|
171
|
+
return RepositoryInstruction(path, scope, depth, _content(result), _truncated(result))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _content(result: ToolResult) -> str:
|
|
175
|
+
value = None if result.output is None else result.output.get("content")
|
|
176
|
+
return value if isinstance(value, str) else ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _bounded_summary(result: ToolResult) -> str:
|
|
180
|
+
compact = " ".join(_content(result).split())[:512]
|
|
181
|
+
return compact or "Manifest inspected; no textual summary was retained."
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _string_items(result: ToolResult, key: str) -> list[str]:
|
|
185
|
+
value = None if result.output is None else result.output.get(key)
|
|
186
|
+
return [item for item in value if isinstance(item, str)] if isinstance(value, list) else []
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _dict_items(result: ToolResult, key: str) -> list[dict[str, object]]:
|
|
190
|
+
value = None if result.output is None else result.output.get(key)
|
|
191
|
+
if not isinstance(value, list):
|
|
192
|
+
return []
|
|
193
|
+
return [dict(item) for item in value if isinstance(item, dict)]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _truncated(result: ToolResult) -> bool:
|
|
197
|
+
return bool(result.output and result.output.get("truncated") is True)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _task_terms(task: str) -> tuple[str, ...]:
|
|
201
|
+
ignored = {"the", "and", "for", "with", "from", "into", "this", "that"}
|
|
202
|
+
values: list[str] = []
|
|
203
|
+
for match in _TERM_PATTERN.findall(task):
|
|
204
|
+
value = match.strip("./")
|
|
205
|
+
if value.casefold() in ignored or value in values:
|
|
206
|
+
continue
|
|
207
|
+
values.append(value)
|
|
208
|
+
if len(values) == 8:
|
|
209
|
+
break
|
|
210
|
+
return tuple(values)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _is_manifest(path: str) -> bool:
|
|
214
|
+
name = PurePosixPath(path).name.casefold()
|
|
215
|
+
return name in _MANIFEST_NAMES or name.startswith("readme")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _is_candidate(path: str) -> bool:
|
|
219
|
+
parts = PurePosixPath(path).parts
|
|
220
|
+
return ".git" not in parts and not _is_manifest(path) and path != "AGENTS.md"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _ancestor_instruction_paths(paths: tuple[str, ...]) -> tuple[str, ...]:
|
|
224
|
+
values: set[str] = set()
|
|
225
|
+
for path in paths:
|
|
226
|
+
parent = PurePosixPath(path).parent
|
|
227
|
+
while str(parent) not in {".", ""}:
|
|
228
|
+
values.add(str(parent / "AGENTS.md"))
|
|
229
|
+
parent = parent.parent
|
|
230
|
+
return tuple(sorted(values, key=lambda value: (len(PurePosixPath(value).parts), value)))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _weakens_nexus_safety(content: str) -> bool:
|
|
234
|
+
normalized = " ".join(content.casefold().split())
|
|
235
|
+
forbidden = (
|
|
236
|
+
"ignore frozen",
|
|
237
|
+
"bypass security",
|
|
238
|
+
"disable approval",
|
|
239
|
+
"allow dangerous",
|
|
240
|
+
"ignore nexus safety",
|
|
241
|
+
)
|
|
242
|
+
return any(value in normalized for value in forbidden)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _unique(values: list[str]) -> list[str]:
|
|
246
|
+
return list(dict.fromkeys(values))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Compatibility bridge from Day 4 requests to the Day 5 ContextManager."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
|
|
6
|
+
from nexus.domain.context import ContextRequest
|
|
7
|
+
from nexus.domain.exploration import ContextBuildRequest, WorkingContext
|
|
8
|
+
from nexus.domain.persistence import SessionTurn
|
|
9
|
+
from nexus.domain.ports.context import ContextManager
|
|
10
|
+
from nexus.domain.ports.skills import SkillRegistry, SkillSelector
|
|
11
|
+
from nexus.domain.skills import SelectedSkill
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ManagedContextBuilder:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
manager: ContextManager,
|
|
18
|
+
repository_id: Callable[[], Awaitable[str]],
|
|
19
|
+
workspace: str,
|
|
20
|
+
turns: Callable[[str], Awaitable[list[SessionTurn]]] | None = None,
|
|
21
|
+
skill_registry: SkillRegistry | None = None,
|
|
22
|
+
skill_selector: SkillSelector | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self._manager = manager
|
|
25
|
+
self._repository_id = repository_id
|
|
26
|
+
self._workspace = workspace
|
|
27
|
+
self._turns = turns
|
|
28
|
+
self._skill_registry = skill_registry
|
|
29
|
+
self._skill_selector = skill_selector
|
|
30
|
+
if (skill_registry is None) is not (skill_selector is None):
|
|
31
|
+
raise ValueError("Skill registry and selector must be configured together.")
|
|
32
|
+
|
|
33
|
+
async def build(self, request: ContextBuildRequest) -> WorkingContext:
|
|
34
|
+
selection = None
|
|
35
|
+
selected_skills: tuple[SelectedSkill, ...] = ()
|
|
36
|
+
if self._skill_registry is not None and self._skill_selector is not None:
|
|
37
|
+
metadata = await self._skill_registry.scan_metadata()
|
|
38
|
+
selection = await self._skill_selector.select(
|
|
39
|
+
run_id=request.run_id,
|
|
40
|
+
task=request.task,
|
|
41
|
+
available_skills=metadata,
|
|
42
|
+
)
|
|
43
|
+
selected_skills = await self._skill_registry.load_selected(selection)
|
|
44
|
+
context = await self._manager.build(
|
|
45
|
+
ContextRequest(
|
|
46
|
+
request.task,
|
|
47
|
+
await self._repository_id(),
|
|
48
|
+
self._workspace,
|
|
49
|
+
request.exploration,
|
|
50
|
+
request.run_id,
|
|
51
|
+
request.session_id,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
context = replace(
|
|
55
|
+
context,
|
|
56
|
+
selected_skills=selected_skills,
|
|
57
|
+
skill_selection_result=selection,
|
|
58
|
+
)
|
|
59
|
+
return await self._manager.prepare_agent_context(
|
|
60
|
+
working_context=context, plan=None, observations=(),
|
|
61
|
+
conversation_turns=(() if self._turns is None
|
|
62
|
+
else await self._turns(request.session_id)),
|
|
63
|
+
)
|