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.
Files changed (131) hide show
  1. nexus/__init__.py +4 -0
  2. nexus/application/__init__.py +2 -0
  3. nexus/application/approval_service.py +130 -0
  4. nexus/application/diff_service.py +177 -0
  5. nexus/application/event_publisher.py +180 -0
  6. nexus/application/execution_context.py +29 -0
  7. nexus/application/execution_ledger.py +126 -0
  8. nexus/application/model_call_context.py +29 -0
  9. nexus/application/observed_model_gateway.py +194 -0
  10. nexus/application/plan_approval_service.py +250 -0
  11. nexus/application/planning.py +668 -0
  12. nexus/application/runtime.py +624 -0
  13. nexus/application/session_service.py +411 -0
  14. nexus/application/structured_output.py +96 -0
  15. nexus/application/telemetry.py +396 -0
  16. nexus/application/telemetry_subscriber.py +216 -0
  17. nexus/application/tool_runtime.py +496 -0
  18. nexus/application/tracing_dispatcher.py +283 -0
  19. nexus/application/validation.py +293 -0
  20. nexus/config/__init__.py +17 -0
  21. nexus/config/loader.py +253 -0
  22. nexus/config/models.py +310 -0
  23. nexus/context/__init__.py +6 -0
  24. nexus/context/budget.py +24 -0
  25. nexus/context/builder.py +92 -0
  26. nexus/context/chunking.py +65 -0
  27. nexus/context/explorer.py +246 -0
  28. nexus/context/integration.py +63 -0
  29. nexus/context/manager.py +328 -0
  30. nexus/context/retrieval.py +261 -0
  31. nexus/domain/__init__.py +2 -0
  32. nexus/domain/agent_decision.py +113 -0
  33. nexus/domain/agent_state.py +49 -0
  34. nexus/domain/approvals.py +64 -0
  35. nexus/domain/context.py +108 -0
  36. nexus/domain/exploration.py +86 -0
  37. nexus/domain/mcp.py +51 -0
  38. nexus/domain/model.py +173 -0
  39. nexus/domain/observability.py +326 -0
  40. nexus/domain/persistence.py +122 -0
  41. nexus/domain/planning.py +329 -0
  42. nexus/domain/ports/__init__.py +61 -0
  43. nexus/domain/ports/agent_decision.py +9 -0
  44. nexus/domain/ports/approval_unit_of_work.py +27 -0
  45. nexus/domain/ports/checkpoint_provider.py +11 -0
  46. nexus/domain/ports/context.py +71 -0
  47. nexus/domain/ports/events.py +21 -0
  48. nexus/domain/ports/graph_runtime.py +30 -0
  49. nexus/domain/ports/mcp.py +23 -0
  50. nexus/domain/ports/model_gateway.py +21 -0
  51. nexus/domain/ports/model_input_budget.py +15 -0
  52. nexus/domain/ports/planning.py +62 -0
  53. nexus/domain/ports/repositories.py +54 -0
  54. nexus/domain/ports/repository_context.py +18 -0
  55. nexus/domain/ports/session_unit_of_work.py +33 -0
  56. nexus/domain/ports/skills.py +41 -0
  57. nexus/domain/ports/tooling.py +39 -0
  58. nexus/domain/ports/tracer.py +13 -0
  59. nexus/domain/ports/validation.py +32 -0
  60. nexus/domain/runtime_events.py +445 -0
  61. nexus/domain/skills.py +129 -0
  62. nexus/domain/tooling.py +163 -0
  63. nexus/domain/validation.py +161 -0
  64. nexus/errors/__init__.py +82 -0
  65. nexus/evaluation/__init__.py +7 -0
  66. nexus/evaluation/assertions.py +4 -0
  67. nexus/evaluation/evaluator.py +263 -0
  68. nexus/evaluation/fixtures.py +36 -0
  69. nexus/evaluation/harness.py +606 -0
  70. nexus/evaluation/loader.py +415 -0
  71. nexus/evaluation/metrics.py +27 -0
  72. nexus/evaluation/models.py +362 -0
  73. nexus/evaluation/report.py +43 -0
  74. nexus/evaluation/runner.py +480 -0
  75. nexus/infrastructure/__init__.py +2 -0
  76. nexus/infrastructure/asyncio_compat.py +13 -0
  77. nexus/infrastructure/bootstrap/__init__.py +17 -0
  78. nexus/infrastructure/bootstrap/composition.py +663 -0
  79. nexus/infrastructure/checkpoint/__init__.py +5 -0
  80. nexus/infrastructure/checkpoint/postgres.py +62 -0
  81. nexus/infrastructure/database.py +39 -0
  82. nexus/infrastructure/embedding.py +61 -0
  83. nexus/infrastructure/graph/__init__.py +6 -0
  84. nexus/infrastructure/graph/day4_runtime.py +966 -0
  85. nexus/infrastructure/graph/langgraph_runtime.py +172 -0
  86. nexus/infrastructure/mcp/__init__.py +5 -0
  87. nexus/infrastructure/mcp/manager.py +380 -0
  88. nexus/infrastructure/model_gateway/__init__.py +6 -0
  89. nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
  90. nexus/infrastructure/observability/__init__.py +7 -0
  91. nexus/infrastructure/observability/console.py +31 -0
  92. nexus/infrastructure/observability/langsmith.py +236 -0
  93. nexus/infrastructure/observability/structured_logger.py +70 -0
  94. nexus/infrastructure/persistence/__init__.py +13 -0
  95. nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
  96. nexus/infrastructure/persistence/models.py +146 -0
  97. nexus/infrastructure/persistence/repositories.py +326 -0
  98. nexus/infrastructure/persistence/unit_of_work.py +82 -0
  99. nexus/infrastructure/repository_files.py +117 -0
  100. nexus/infrastructure/sandbox/__init__.py +5 -0
  101. nexus/infrastructure/sandbox/local_process.py +353 -0
  102. nexus/infrastructure/semantic.py +276 -0
  103. nexus/infrastructure/semantic_models.py +56 -0
  104. nexus/interfaces/__init__.py +2 -0
  105. nexus/interfaces/cli/__init__.py +2 -0
  106. nexus/interfaces/cli/__main__.py +6 -0
  107. nexus/interfaces/cli/app.py +266 -0
  108. nexus/interfaces/cli/renderer.py +118 -0
  109. nexus/security/__init__.py +14 -0
  110. nexus/security/approval_policies.py +73 -0
  111. nexus/security/command_policy.py +117 -0
  112. nexus/security/executables.py +107 -0
  113. nexus/security/git_commands.py +65 -0
  114. nexus/security/workspace.py +93 -0
  115. nexus/skills/__init__.py +7 -0
  116. nexus/skills/builtin/__init__.py +1 -0
  117. nexus/skills/builtin/debug-python/SKILL.md +26 -0
  118. nexus/skills/builtin/review-repository/SKILL.md +26 -0
  119. nexus/skills/builtin/write-tests/SKILL.md +26 -0
  120. nexus/skills/loader.py +439 -0
  121. nexus/skills/registry.py +146 -0
  122. nexus/skills/selector.py +152 -0
  123. nexus/tools/__init__.py +6 -0
  124. nexus/tools/editing.py +312 -0
  125. nexus/tools/mcp.py +160 -0
  126. nexus/tools/native.py +654 -0
  127. nexus/tools/registry.py +27 -0
  128. nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
  129. nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
  130. nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  131. nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
nexus/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Nexus coding-agent runtime."""
2
+
3
+ __version__ = "0.1.0"
4
+
@@ -0,0 +1,2 @@
1
+ """Nexus application services."""
2
+
@@ -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