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
nexus/__init__.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Day 3 approval persistence and Run/Session correlation rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from nexus.domain.approvals import ApprovalRequest
|
|
9
|
+
from nexus.domain.ports.approval_unit_of_work import ApprovalUnitOfWork, ApprovalUnitOfWorkFactory
|
|
10
|
+
from nexus.domain.tooling import ApprovalDecision, RiskLevel, ToolInvocation
|
|
11
|
+
from nexus.errors import PermissionDeniedError, ToolExecutionError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApprovalService:
|
|
15
|
+
def __init__(self, unit_of_work_factory: ApprovalUnitOfWorkFactory) -> None:
|
|
16
|
+
self._unit_of_work_factory = unit_of_work_factory
|
|
17
|
+
|
|
18
|
+
async def create_pending(
|
|
19
|
+
self,
|
|
20
|
+
invocation: ToolInvocation,
|
|
21
|
+
*,
|
|
22
|
+
risk_level: RiskLevel,
|
|
23
|
+
summary: str,
|
|
24
|
+
) -> ApprovalRequest:
|
|
25
|
+
approval = ApprovalRequest(
|
|
26
|
+
approval_id=str(uuid4()),
|
|
27
|
+
run_id=invocation.run_id,
|
|
28
|
+
session_id=invocation.session_id,
|
|
29
|
+
operation=invocation.tool_name,
|
|
30
|
+
risk_level=risk_level,
|
|
31
|
+
resource_or_command_summary=summary,
|
|
32
|
+
decision=ApprovalDecision.PENDING,
|
|
33
|
+
actor="runtime",
|
|
34
|
+
reason=None,
|
|
35
|
+
created_at=datetime.now(UTC),
|
|
36
|
+
decided_at=None,
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
async with self._unit_of_work_factory() as unit_of_work:
|
|
40
|
+
await _validate_correlation(unit_of_work, approval)
|
|
41
|
+
await unit_of_work.approvals.add(approval)
|
|
42
|
+
return approval
|
|
43
|
+
except (PermissionDeniedError, ToolExecutionError):
|
|
44
|
+
raise
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
raise ToolExecutionError(
|
|
47
|
+
"Nexus could not persist the pending approval.",
|
|
48
|
+
code="APPROVAL_PERSISTENCE_ERROR",
|
|
49
|
+
retryable=True,
|
|
50
|
+
) from exc
|
|
51
|
+
|
|
52
|
+
async def persist_decision(self, approval: ApprovalRequest) -> ApprovalRequest:
|
|
53
|
+
if approval.decision is ApprovalDecision.PENDING:
|
|
54
|
+
raise ToolExecutionError(
|
|
55
|
+
"A pending approval cannot be persisted as a terminal decision.",
|
|
56
|
+
code="INVALID_APPROVAL_TRANSITION",
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
async with self._unit_of_work_factory() as unit_of_work:
|
|
60
|
+
await _validate_correlation(unit_of_work, approval)
|
|
61
|
+
current = await unit_of_work.approvals.get(approval.approval_id)
|
|
62
|
+
if current is None:
|
|
63
|
+
raise ToolExecutionError(
|
|
64
|
+
"The approval does not exist.",
|
|
65
|
+
code="APPROVAL_NOT_FOUND",
|
|
66
|
+
)
|
|
67
|
+
if current.decision is not ApprovalDecision.PENDING:
|
|
68
|
+
raise ToolExecutionError(
|
|
69
|
+
"The approval already has a terminal decision.",
|
|
70
|
+
code="INVALID_APPROVAL_TRANSITION",
|
|
71
|
+
)
|
|
72
|
+
await unit_of_work.approvals.update(approval)
|
|
73
|
+
return approval
|
|
74
|
+
except (PermissionDeniedError, ToolExecutionError):
|
|
75
|
+
raise
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
raise ToolExecutionError(
|
|
78
|
+
"Nexus could not persist the approval decision.",
|
|
79
|
+
code="APPROVAL_PERSISTENCE_ERROR",
|
|
80
|
+
retryable=True,
|
|
81
|
+
) from exc
|
|
82
|
+
|
|
83
|
+
async def record_denied(
|
|
84
|
+
self,
|
|
85
|
+
invocation: ToolInvocation,
|
|
86
|
+
*,
|
|
87
|
+
risk_level: RiskLevel,
|
|
88
|
+
summary: str,
|
|
89
|
+
reason: str,
|
|
90
|
+
) -> ApprovalRequest:
|
|
91
|
+
now = datetime.now(UTC)
|
|
92
|
+
approval = ApprovalRequest(
|
|
93
|
+
approval_id=str(uuid4()),
|
|
94
|
+
run_id=invocation.run_id,
|
|
95
|
+
session_id=invocation.session_id,
|
|
96
|
+
operation=invocation.tool_name,
|
|
97
|
+
risk_level=risk_level,
|
|
98
|
+
resource_or_command_summary=summary,
|
|
99
|
+
decision=ApprovalDecision.DENIED,
|
|
100
|
+
actor="security_policy",
|
|
101
|
+
reason=reason,
|
|
102
|
+
created_at=now,
|
|
103
|
+
decided_at=now,
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
async with self._unit_of_work_factory() as unit_of_work:
|
|
107
|
+
await _validate_correlation(unit_of_work, approval)
|
|
108
|
+
await unit_of_work.approvals.add(approval)
|
|
109
|
+
return approval
|
|
110
|
+
except (PermissionDeniedError, ToolExecutionError):
|
|
111
|
+
raise
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
raise ToolExecutionError(
|
|
114
|
+
"Nexus could not persist the denied security decision.",
|
|
115
|
+
code="APPROVAL_PERSISTENCE_ERROR",
|
|
116
|
+
retryable=True,
|
|
117
|
+
) from exc
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def _validate_correlation(
|
|
121
|
+
unit_of_work: ApprovalUnitOfWork,
|
|
122
|
+
approval: ApprovalRequest,
|
|
123
|
+
) -> None:
|
|
124
|
+
session = await unit_of_work.sessions.get(approval.session_id)
|
|
125
|
+
run = await unit_of_work.runs.get(approval.run_id)
|
|
126
|
+
if session is None or run is None or run.session_id != approval.session_id:
|
|
127
|
+
raise PermissionDeniedError(
|
|
128
|
+
"The approval Run/Session correlation is invalid.",
|
|
129
|
+
code="APPROVAL_CORRELATION_DENIED",
|
|
130
|
+
)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Exact Day 4 final-diff collection through Tool Runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from nexus.application.tool_runtime import ToolRuntime
|
|
9
|
+
from nexus.domain.planning import ChangedFile, ChangeKind
|
|
10
|
+
from nexus.domain.tooling import ToolInvocation, ToolResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class FinalDiffEvidence:
|
|
15
|
+
diff: str | None
|
|
16
|
+
includes_preexisting_changes: bool
|
|
17
|
+
tool_results: tuple[ToolResult, ...]
|
|
18
|
+
error_code: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FinalDiffCollector:
|
|
22
|
+
def __init__(self, tool_runtime: ToolRuntime) -> None:
|
|
23
|
+
self._tool_runtime = tool_runtime
|
|
24
|
+
|
|
25
|
+
async def collect(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
run_id: str,
|
|
29
|
+
session_id: str,
|
|
30
|
+
changed_files: tuple[ChangedFile, ...],
|
|
31
|
+
initial_git_status: ToolResult | None,
|
|
32
|
+
) -> FinalDiffEvidence:
|
|
33
|
+
if not changed_files:
|
|
34
|
+
return FinalDiffEvidence(
|
|
35
|
+
"",
|
|
36
|
+
_dirty(initial_git_status),
|
|
37
|
+
(),
|
|
38
|
+
)
|
|
39
|
+
if initial_git_status is None or not _exact_success(initial_git_status):
|
|
40
|
+
return FinalDiffEvidence(None, False, (), "DIFF_UNAVAILABLE")
|
|
41
|
+
results: list[ToolResult] = []
|
|
42
|
+
|
|
43
|
+
async def invoke(name: str, arguments: dict[str, object]) -> ToolResult:
|
|
44
|
+
result = await self._tool_runtime.execute(
|
|
45
|
+
ToolInvocation(str(uuid4()), name, arguments, run_id, session_id)
|
|
46
|
+
)
|
|
47
|
+
results.append(result)
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
status = await invoke("git_status", {})
|
|
51
|
+
tracked = await invoke("git_diff", {"staged": False})
|
|
52
|
+
if not _exact_success(status) or not _exact_success(tracked):
|
|
53
|
+
return FinalDiffEvidence(
|
|
54
|
+
None,
|
|
55
|
+
_dirty(initial_git_status),
|
|
56
|
+
tuple(results),
|
|
57
|
+
"DIFF_UNAVAILABLE",
|
|
58
|
+
)
|
|
59
|
+
untracked = _untracked_paths(status)
|
|
60
|
+
if any(
|
|
61
|
+
changed.change_kind is ChangeKind.MODIFIED and changed.path in untracked
|
|
62
|
+
for changed in changed_files
|
|
63
|
+
):
|
|
64
|
+
return FinalDiffEvidence(
|
|
65
|
+
None,
|
|
66
|
+
_dirty(initial_git_status),
|
|
67
|
+
tuple(results),
|
|
68
|
+
"DIFF_UNAVAILABLE",
|
|
69
|
+
)
|
|
70
|
+
tracked_text = _stdout(tracked)
|
|
71
|
+
new_patches: list[str] = []
|
|
72
|
+
for changed in changed_files:
|
|
73
|
+
if changed.change_kind is not ChangeKind.ADDED:
|
|
74
|
+
continue
|
|
75
|
+
content = await self._read_complete(
|
|
76
|
+
changed.path,
|
|
77
|
+
run_id=run_id,
|
|
78
|
+
session_id=session_id,
|
|
79
|
+
results=results,
|
|
80
|
+
)
|
|
81
|
+
if content is None:
|
|
82
|
+
return FinalDiffEvidence(
|
|
83
|
+
None,
|
|
84
|
+
_dirty(initial_git_status),
|
|
85
|
+
tuple(results),
|
|
86
|
+
"DIFF_UNAVAILABLE",
|
|
87
|
+
)
|
|
88
|
+
new_patches.append(_new_file_patch(changed.path, content))
|
|
89
|
+
pieces = [piece for piece in (tracked_text, *new_patches) if piece]
|
|
90
|
+
return FinalDiffEvidence(
|
|
91
|
+
"\n".join(piece.rstrip("\n") for piece in pieces) + ("\n" if pieces else ""),
|
|
92
|
+
_dirty(initial_git_status),
|
|
93
|
+
tuple(results),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def _read_complete(
|
|
97
|
+
self,
|
|
98
|
+
path: str,
|
|
99
|
+
*,
|
|
100
|
+
run_id: str,
|
|
101
|
+
session_id: str,
|
|
102
|
+
results: list[ToolResult],
|
|
103
|
+
) -> str | None:
|
|
104
|
+
chunks: list[str] = []
|
|
105
|
+
start_line = 1
|
|
106
|
+
while True:
|
|
107
|
+
result = await self._tool_runtime.execute(
|
|
108
|
+
ToolInvocation(
|
|
109
|
+
str(uuid4()),
|
|
110
|
+
"read_file",
|
|
111
|
+
{"path": path, "start_line": start_line, "max_lines": 400},
|
|
112
|
+
run_id,
|
|
113
|
+
session_id,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
results.append(result)
|
|
117
|
+
if not result.success or result.output is None:
|
|
118
|
+
return None
|
|
119
|
+
content = result.output.get("content")
|
|
120
|
+
end_line = result.output.get("end_line")
|
|
121
|
+
if not isinstance(content, str) or not isinstance(end_line, int):
|
|
122
|
+
return None
|
|
123
|
+
chunks.append(content)
|
|
124
|
+
if result.output.get("truncated") is not True:
|
|
125
|
+
return "".join(chunks)
|
|
126
|
+
if end_line < start_line:
|
|
127
|
+
return None
|
|
128
|
+
start_line = end_line + 1
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _exact_success(result: ToolResult) -> bool:
|
|
132
|
+
return bool(
|
|
133
|
+
result.success
|
|
134
|
+
and result.output is not None
|
|
135
|
+
and result.output.get("truncated") is not True
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _stdout(result: ToolResult) -> str:
|
|
140
|
+
value = None if result.output is None else result.output.get("stdout")
|
|
141
|
+
return value if isinstance(value, str) else ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _dirty(result: ToolResult | None) -> bool:
|
|
145
|
+
if result is None or not _exact_success(result):
|
|
146
|
+
return False
|
|
147
|
+
return any(
|
|
148
|
+
line.strip() and not line.startswith("## ")
|
|
149
|
+
for line in _stdout(result).splitlines()
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _untracked_paths(result: ToolResult) -> set[str]:
|
|
154
|
+
return {
|
|
155
|
+
line[3:]
|
|
156
|
+
for line in _stdout(result).splitlines()
|
|
157
|
+
if line.startswith("?? ") and not line[3:].startswith('"')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _new_file_patch(path: str, content: str) -> str:
|
|
162
|
+
lines = content.splitlines(keepends=True)
|
|
163
|
+
count = len(lines)
|
|
164
|
+
header = (
|
|
165
|
+
f"diff --git a/{path} b/{path}\n"
|
|
166
|
+
"new file mode 100644\n"
|
|
167
|
+
"--- /dev/null\n"
|
|
168
|
+
f"+++ b/{path}\n"
|
|
169
|
+
f"@@ -0,0 +1,{count} @@\n"
|
|
170
|
+
)
|
|
171
|
+
body = "".join(
|
|
172
|
+
f"+{line}" if line.endswith(("\n", "\r")) else f"+{line}\n"
|
|
173
|
+
for line in lines
|
|
174
|
+
)
|
|
175
|
+
if lines and not lines[-1].endswith(("\n", "\r")):
|
|
176
|
+
body += "\\n"
|
|
177
|
+
return header + body
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""In-process Day 8 live event publisher with isolated execution streams."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from nexus.application.execution_context import current_execution_context
|
|
12
|
+
from nexus.domain.observability import RunExecutionContext
|
|
13
|
+
from nexus.domain.ports.events import EventSubscriber, EventSubscription
|
|
14
|
+
from nexus.domain.runtime_events import (
|
|
15
|
+
ApprovalRequested,
|
|
16
|
+
ErrorOccurred,
|
|
17
|
+
FinalResult,
|
|
18
|
+
RunInterrupted,
|
|
19
|
+
RuntimeEvent,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_END = object()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class PublishedObservation:
|
|
27
|
+
"""Private metadata copied at the synchronous publication boundary."""
|
|
28
|
+
|
|
29
|
+
context: RunExecutionContext
|
|
30
|
+
sequence: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_OBSERVATION: ContextVar[PublishedObservation | None] = ContextVar(
|
|
34
|
+
"nexus_published_observation", default=None
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def current_published_observation() -> PublishedObservation:
|
|
39
|
+
observation = _OBSERVATION.get()
|
|
40
|
+
if observation is None:
|
|
41
|
+
raise RuntimeError("No published RuntimeEvent observation is bound.")
|
|
42
|
+
return observation
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True)
|
|
46
|
+
class _ExecutionStream:
|
|
47
|
+
context: RunExecutionContext
|
|
48
|
+
queue: asyncio.Queue[RuntimeEvent | object]
|
|
49
|
+
sequence: int = 0
|
|
50
|
+
terminal_published: bool = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _Subscription(EventSubscription):
|
|
54
|
+
def __init__(
|
|
55
|
+
self, publisher: InProcessEventPublisher, subscriber: EventSubscriber
|
|
56
|
+
) -> None:
|
|
57
|
+
self._publisher = publisher
|
|
58
|
+
self._subscriber = subscriber
|
|
59
|
+
self._closed = False
|
|
60
|
+
|
|
61
|
+
async def aclose(self) -> None:
|
|
62
|
+
if self._closed:
|
|
63
|
+
return
|
|
64
|
+
self._closed = True
|
|
65
|
+
self._publisher._unsubscribe(self._subscriber)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class InProcessEventPublisher:
|
|
69
|
+
"""Required lossless live lane plus bounded subscriber handoff callbacks."""
|
|
70
|
+
|
|
71
|
+
def __init__(self) -> None:
|
|
72
|
+
self._subscribers: list[EventSubscriber] = []
|
|
73
|
+
self._streams: dict[str, _ExecutionStream] = {}
|
|
74
|
+
self._seen_plan_approvals: dict[str, set[str]] = {}
|
|
75
|
+
|
|
76
|
+
def subscribe(self, subscriber: EventSubscriber) -> EventSubscription:
|
|
77
|
+
if subscriber not in self._subscribers:
|
|
78
|
+
self._subscribers.append(subscriber)
|
|
79
|
+
return _Subscription(self, subscriber)
|
|
80
|
+
|
|
81
|
+
def open_execution(self, context: RunExecutionContext) -> None:
|
|
82
|
+
if context.execution_id in self._streams:
|
|
83
|
+
raise RuntimeError("Execution stream already exists.")
|
|
84
|
+
self._streams[context.execution_id] = _ExecutionStream(
|
|
85
|
+
context=context,
|
|
86
|
+
queue=asyncio.Queue(),
|
|
87
|
+
)
|
|
88
|
+
self._seen_plan_approvals[context.execution_id] = set()
|
|
89
|
+
|
|
90
|
+
async def publish(self, event: RuntimeEvent) -> None:
|
|
91
|
+
context = current_execution_context()
|
|
92
|
+
stream = self._streams.get(context.execution_id)
|
|
93
|
+
if stream is None:
|
|
94
|
+
raise RuntimeError("Execution stream is not open.")
|
|
95
|
+
if stream.terminal_published:
|
|
96
|
+
raise RuntimeError("Cannot publish after a terminal RuntimeEvent.")
|
|
97
|
+
if event.run_id != context.run_id or event.session_id != context.session_id:
|
|
98
|
+
raise ValueError("RuntimeEvent identity does not match execution context.")
|
|
99
|
+
if isinstance(event, ApprovalRequested) and event.plan_id is not None:
|
|
100
|
+
seen = self._seen_plan_approvals[context.execution_id]
|
|
101
|
+
if event.approval_id in seen:
|
|
102
|
+
return
|
|
103
|
+
seen.add(event.approval_id)
|
|
104
|
+
|
|
105
|
+
stream.sequence += 1
|
|
106
|
+
observation = PublishedObservation(context=context, sequence=stream.sequence)
|
|
107
|
+
stream.queue.put_nowait(event)
|
|
108
|
+
token = _OBSERVATION.set(observation)
|
|
109
|
+
try:
|
|
110
|
+
for subscriber in tuple(self._subscribers):
|
|
111
|
+
try:
|
|
112
|
+
await subscriber.on_event(event)
|
|
113
|
+
except Exception:
|
|
114
|
+
continue
|
|
115
|
+
finally:
|
|
116
|
+
_OBSERVATION.reset(token)
|
|
117
|
+
|
|
118
|
+
if _is_terminal(event):
|
|
119
|
+
stream.terminal_published = True
|
|
120
|
+
stream.queue.put_nowait(_END)
|
|
121
|
+
|
|
122
|
+
emit = publish
|
|
123
|
+
|
|
124
|
+
async def events(self, execution_id: str) -> AsyncIterator[RuntimeEvent]:
|
|
125
|
+
stream = self._streams.get(execution_id)
|
|
126
|
+
if stream is None:
|
|
127
|
+
raise RuntimeError("Execution stream is not open.")
|
|
128
|
+
while True:
|
|
129
|
+
item = await stream.queue.get()
|
|
130
|
+
if item is _END:
|
|
131
|
+
return
|
|
132
|
+
if not isinstance(item, RuntimeEvent):
|
|
133
|
+
raise RuntimeError("Invalid live event queue item.")
|
|
134
|
+
yield item
|
|
135
|
+
|
|
136
|
+
def close_execution(self, execution_id: str) -> None:
|
|
137
|
+
stream = self._streams.pop(execution_id, None)
|
|
138
|
+
self._seen_plan_approvals.pop(execution_id, None)
|
|
139
|
+
if stream is not None and not stream.terminal_published:
|
|
140
|
+
stream.queue.put_nowait(_END)
|
|
141
|
+
|
|
142
|
+
@asynccontextmanager
|
|
143
|
+
async def execution(
|
|
144
|
+
self, context: RunExecutionContext
|
|
145
|
+
) -> AsyncIterator[AsyncIterator[RuntimeEvent]]:
|
|
146
|
+
self.open_execution(context)
|
|
147
|
+
try:
|
|
148
|
+
yield self.events(context.execution_id)
|
|
149
|
+
finally:
|
|
150
|
+
self.close_execution(context.execution_id)
|
|
151
|
+
|
|
152
|
+
def _unsubscribe(self, subscriber: EventSubscriber) -> None:
|
|
153
|
+
try:
|
|
154
|
+
self._subscribers.remove(subscriber)
|
|
155
|
+
except ValueError:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_terminal(event: RuntimeEvent) -> bool:
|
|
160
|
+
return isinstance(event, (FinalResult, RunInterrupted, ErrorOccurred))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class LiveRuntimeEventBridge:
|
|
164
|
+
"""Publish graph progress live while deferring its terminal result to Runtime."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, publisher: InProcessEventPublisher) -> None:
|
|
167
|
+
self._publisher = publisher
|
|
168
|
+
self._terminals: dict[str, FinalResult] = {}
|
|
169
|
+
|
|
170
|
+
async def emit(self, event: RuntimeEvent) -> None:
|
|
171
|
+
if isinstance(event, FinalResult):
|
|
172
|
+
if event.run_id in self._terminals:
|
|
173
|
+
raise RuntimeError("Graph emitted multiple final results.")
|
|
174
|
+
self._terminals[event.run_id] = event
|
|
175
|
+
return
|
|
176
|
+
await self._publisher.publish(event)
|
|
177
|
+
|
|
178
|
+
def drain(self, run_id: str) -> tuple[RuntimeEvent, ...]:
|
|
179
|
+
terminal = self._terminals.pop(run_id, None)
|
|
180
|
+
return () if terminal is None else (terminal,)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Context-local Day 8 execution identity propagation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from contextvars import ContextVar
|
|
8
|
+
|
|
9
|
+
from nexus.domain.observability import RunExecutionContext
|
|
10
|
+
|
|
11
|
+
_EXECUTION_CONTEXT: ContextVar[RunExecutionContext | None] = ContextVar(
|
|
12
|
+
"nexus_run_execution_context", default=None
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def bind_execution_context(context: RunExecutionContext) -> Iterator[None]:
|
|
18
|
+
token = _EXECUTION_CONTEXT.set(context)
|
|
19
|
+
try:
|
|
20
|
+
yield
|
|
21
|
+
finally:
|
|
22
|
+
_EXECUTION_CONTEXT.reset(token)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def current_execution_context() -> RunExecutionContext:
|
|
26
|
+
context = _EXECUTION_CONTEXT.get()
|
|
27
|
+
if context is None:
|
|
28
|
+
raise RuntimeError("No RunExecutionContext is bound to this async execution.")
|
|
29
|
+
return context
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""In-process Day 4 event and Tool-attempt ledgers shared by composed services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
from nexus.domain.model import TokenUsage, TokenUsageAggregate
|
|
8
|
+
from nexus.domain.runtime_events import ApprovalRequested, RuntimeEvent
|
|
9
|
+
from nexus.domain.tooling import ToolInvocation, ToolResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ToolExecutionLedger:
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self._counts: dict[str, int] = defaultdict(int)
|
|
15
|
+
self._model_counts: dict[str, int] = defaultdict(int)
|
|
16
|
+
self._step_counts: dict[str, int] = defaultdict(int)
|
|
17
|
+
self._repair_counts: dict[str, int] = defaultdict(int)
|
|
18
|
+
self._token_usage: dict[str, TokenUsageAggregate] = defaultdict(
|
|
19
|
+
TokenUsageAggregate
|
|
20
|
+
)
|
|
21
|
+
self._results: dict[str, list[ToolResult]] = defaultdict(list)
|
|
22
|
+
self._restored_runs: set[str] = set()
|
|
23
|
+
|
|
24
|
+
def restore(
|
|
25
|
+
self,
|
|
26
|
+
run_id: str,
|
|
27
|
+
*,
|
|
28
|
+
tool_call_count: int,
|
|
29
|
+
model_call_count: int,
|
|
30
|
+
step_count: int,
|
|
31
|
+
repair_count: int,
|
|
32
|
+
tool_results: tuple[ToolResult, ...],
|
|
33
|
+
token_usage: TokenUsageAggregate | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Merge one durable checkpoint snapshot into this process-local ledger."""
|
|
36
|
+
|
|
37
|
+
if run_id not in self._restored_runs:
|
|
38
|
+
self._counts[run_id] += tool_call_count
|
|
39
|
+
self._model_counts[run_id] += model_call_count
|
|
40
|
+
self._step_counts[run_id] += step_count
|
|
41
|
+
self._repair_counts[run_id] += repair_count
|
|
42
|
+
if token_usage is not None:
|
|
43
|
+
self._token_usage[run_id] = token_usage
|
|
44
|
+
current_results = self._results[run_id]
|
|
45
|
+
checkpoint_ids = {item.invocation_id for item in tool_results}
|
|
46
|
+
self._results[run_id] = [
|
|
47
|
+
*tool_results,
|
|
48
|
+
*(
|
|
49
|
+
item
|
|
50
|
+
for item in current_results
|
|
51
|
+
if item.invocation_id not in checkpoint_ids
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
self._restored_runs.add(run_id)
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
self._counts[run_id] = max(self._counts[run_id], tool_call_count)
|
|
58
|
+
self._model_counts[run_id] = max(
|
|
59
|
+
self._model_counts[run_id], model_call_count
|
|
60
|
+
)
|
|
61
|
+
self._step_counts[run_id] = max(self._step_counts[run_id], step_count)
|
|
62
|
+
self._repair_counts[run_id] = max(self._repair_counts[run_id], repair_count)
|
|
63
|
+
known_ids = {item.invocation_id for item in self._results[run_id]}
|
|
64
|
+
missing_results = [
|
|
65
|
+
item for item in tool_results if item.invocation_id not in known_ids
|
|
66
|
+
]
|
|
67
|
+
if missing_results:
|
|
68
|
+
self._results[run_id] = [*missing_results, *self._results[run_id]]
|
|
69
|
+
|
|
70
|
+
def begin(self, invocation: ToolInvocation) -> None:
|
|
71
|
+
self._counts[invocation.run_id] += 1
|
|
72
|
+
|
|
73
|
+
def record(self, invocation: ToolInvocation, result: ToolResult) -> None:
|
|
74
|
+
self._results[invocation.run_id].append(result)
|
|
75
|
+
|
|
76
|
+
def count(self, run_id: str) -> int:
|
|
77
|
+
return self._counts[run_id]
|
|
78
|
+
|
|
79
|
+
def begin_model(self, run_id: str) -> None:
|
|
80
|
+
self._model_counts[run_id] += 1
|
|
81
|
+
|
|
82
|
+
def model_count(self, run_id: str) -> int:
|
|
83
|
+
return self._model_counts[run_id]
|
|
84
|
+
|
|
85
|
+
def record_model_usage(self, run_id: str, usage: TokenUsage) -> None:
|
|
86
|
+
self._token_usage[run_id] = self._token_usage[run_id].add(usage)
|
|
87
|
+
|
|
88
|
+
def token_usage(self, run_id: str) -> TokenUsageAggregate:
|
|
89
|
+
return self._token_usage[run_id]
|
|
90
|
+
|
|
91
|
+
def begin_step(self, run_id: str) -> None:
|
|
92
|
+
self._step_counts[run_id] += 1
|
|
93
|
+
|
|
94
|
+
def step_count(self, run_id: str) -> int:
|
|
95
|
+
return self._step_counts[run_id]
|
|
96
|
+
|
|
97
|
+
def begin_repair(self, run_id: str) -> None:
|
|
98
|
+
self._repair_counts[run_id] += 1
|
|
99
|
+
|
|
100
|
+
def repair_count(self, run_id: str) -> int:
|
|
101
|
+
return self._repair_counts[run_id]
|
|
102
|
+
|
|
103
|
+
def results(self, run_id: str) -> tuple[ToolResult, ...]:
|
|
104
|
+
return tuple(self._results[run_id])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RuntimeEventBuffer:
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
self._events: dict[str, list[RuntimeEvent]] = defaultdict(list)
|
|
110
|
+
self._seen_plan_approvals: set[str] = set()
|
|
111
|
+
|
|
112
|
+
async def emit(self, event: RuntimeEvent) -> None:
|
|
113
|
+
if isinstance(event, ApprovalRequested) and event.plan_id is not None:
|
|
114
|
+
if event.approval_id in self._seen_plan_approvals:
|
|
115
|
+
return
|
|
116
|
+
self._seen_plan_approvals.add(event.approval_id)
|
|
117
|
+
self._events[event.run_id].append(event)
|
|
118
|
+
|
|
119
|
+
def drain(self, run_id: str) -> tuple[RuntimeEvent, ...]:
|
|
120
|
+
events = tuple(self._events.pop(run_id, ()))
|
|
121
|
+
self._seen_plan_approvals.difference_update(
|
|
122
|
+
event.approval_id
|
|
123
|
+
for event in events
|
|
124
|
+
if isinstance(event, ApprovalRequested) and event.plan_id is not None
|
|
125
|
+
)
|
|
126
|
+
return events
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Context-local semantic phase for the next real ModelGateway invocation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from contextvars import ContextVar
|
|
8
|
+
|
|
9
|
+
from nexus.domain.model import ModelCallPhase
|
|
10
|
+
|
|
11
|
+
_MODEL_CALL_PHASE: ContextVar[ModelCallPhase | None] = ContextVar(
|
|
12
|
+
"nexus_model_call_phase", default=None
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def bind_model_call_phase(phase: ModelCallPhase) -> Iterator[None]:
|
|
18
|
+
token = _MODEL_CALL_PHASE.set(phase)
|
|
19
|
+
try:
|
|
20
|
+
yield
|
|
21
|
+
finally:
|
|
22
|
+
_MODEL_CALL_PHASE.reset(token)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def current_model_call_phase() -> ModelCallPhase:
|
|
26
|
+
phase = _MODEL_CALL_PHASE.get()
|
|
27
|
+
if phase is None:
|
|
28
|
+
raise RuntimeError("No ModelCallPhase is bound to this model invocation.")
|
|
29
|
+
return phase
|