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,624 @@
|
|
|
1
|
+
"""Application-level orchestration for the Day 1 runtime lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from nexus.application.event_publisher import InProcessEventPublisher, LiveRuntimeEventBridge
|
|
12
|
+
from nexus.application.execution_context import bind_execution_context
|
|
13
|
+
from nexus.application.execution_ledger import RuntimeEventBuffer, ToolExecutionLedger
|
|
14
|
+
from nexus.application.session_service import SessionService
|
|
15
|
+
from nexus.application.telemetry_subscriber import TelemetrySubscriber
|
|
16
|
+
from nexus.domain.agent_state import AgentState
|
|
17
|
+
from nexus.domain.model import ModelMessage, TokenUsageAggregate
|
|
18
|
+
from nexus.domain.observability import (
|
|
19
|
+
ExecutionOutcome,
|
|
20
|
+
RunExecutionContext,
|
|
21
|
+
TraceRunFinish,
|
|
22
|
+
TraceRunStart,
|
|
23
|
+
TraceValidationStatus,
|
|
24
|
+
)
|
|
25
|
+
from nexus.domain.persistence import Run
|
|
26
|
+
from nexus.domain.planning import PlanApprovalResumeInput
|
|
27
|
+
from nexus.domain.ports.graph_runtime import GraphRuntime
|
|
28
|
+
from nexus.domain.runtime_events import (
|
|
29
|
+
ErrorOccurred,
|
|
30
|
+
FinalResult,
|
|
31
|
+
RunInterrupted,
|
|
32
|
+
RuntimeEvent,
|
|
33
|
+
RuntimeStatus,
|
|
34
|
+
TaskStarted,
|
|
35
|
+
)
|
|
36
|
+
from nexus.errors import NexusError, SessionError
|
|
37
|
+
|
|
38
|
+
_LiveOperation = Callable[[], Awaitable[tuple[RuntimeEvent, AgentState | None]]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NexusRuntime:
|
|
42
|
+
"""Stable, async-first entry point shared by all future adapters."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
graph_runtime: GraphRuntime,
|
|
47
|
+
*,
|
|
48
|
+
session_service: SessionService | None = None,
|
|
49
|
+
model_metadata: dict[str, object] | None = None,
|
|
50
|
+
event_buffer: RuntimeEventBuffer | LiveRuntimeEventBridge | None = None,
|
|
51
|
+
event_publisher: InProcessEventPublisher | None = None,
|
|
52
|
+
telemetry_subscriber: TelemetrySubscriber | None = None,
|
|
53
|
+
ledger: ToolExecutionLedger | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._graph_runtime = graph_runtime
|
|
56
|
+
self._session_service = session_service
|
|
57
|
+
self._model_metadata = model_metadata
|
|
58
|
+
self._event_buffer = event_buffer
|
|
59
|
+
self._event_publisher = event_publisher
|
|
60
|
+
self._telemetry_subscriber = telemetry_subscriber
|
|
61
|
+
self._ledger = ledger
|
|
62
|
+
|
|
63
|
+
async def run(
|
|
64
|
+
self,
|
|
65
|
+
task: str,
|
|
66
|
+
session_id: str | None = None,
|
|
67
|
+
) -> AsyncIterator[RuntimeEvent]:
|
|
68
|
+
"""Emit the minimal model-backed Day 1 lifecycle."""
|
|
69
|
+
|
|
70
|
+
if self._event_publisher is not None:
|
|
71
|
+
async for event in self._run_live(task, session_id):
|
|
72
|
+
yield event
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
run_id = str(uuid4())
|
|
76
|
+
normalized_task = task.strip()
|
|
77
|
+
if not normalized_task:
|
|
78
|
+
yield TaskStarted(run_id=run_id, session_id=session_id, task=task)
|
|
79
|
+
yield ErrorOccurred(
|
|
80
|
+
run_id=run_id,
|
|
81
|
+
session_id=session_id,
|
|
82
|
+
code="INVALID_TASK",
|
|
83
|
+
message="Task must be a non-empty string.",
|
|
84
|
+
retryable=False,
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
persisted_run = None
|
|
89
|
+
if self._session_service is not None:
|
|
90
|
+
try:
|
|
91
|
+
persisted_run = await self._session_service.start_run(
|
|
92
|
+
run_id=run_id,
|
|
93
|
+
task=normalized_task,
|
|
94
|
+
session_id=session_id,
|
|
95
|
+
model_metadata=self._model_metadata,
|
|
96
|
+
)
|
|
97
|
+
session_id = persisted_run.session_id
|
|
98
|
+
except NexusError as exc:
|
|
99
|
+
yield TaskStarted(run_id=run_id, session_id=session_id, task=normalized_task)
|
|
100
|
+
yield ErrorOccurred(
|
|
101
|
+
run_id=run_id,
|
|
102
|
+
session_id=session_id,
|
|
103
|
+
code=exc.code,
|
|
104
|
+
message=str(exc),
|
|
105
|
+
retryable=exc.retryable,
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
yield TaskStarted(run_id=run_id, session_id=session_id, task=normalized_task)
|
|
110
|
+
state = AgentState(
|
|
111
|
+
task=normalized_task,
|
|
112
|
+
messages=[ModelMessage(role="user", content=normalized_task)],
|
|
113
|
+
run_id=run_id,
|
|
114
|
+
session_id=session_id,
|
|
115
|
+
status=RuntimeStatus.STARTED,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
if persisted_run is None:
|
|
120
|
+
result = await self._graph_runtime.run(state)
|
|
121
|
+
else:
|
|
122
|
+
result = await self._graph_runtime.run(
|
|
123
|
+
state,
|
|
124
|
+
thread_id=persisted_run.graph_thread_id,
|
|
125
|
+
)
|
|
126
|
+
if result.status is RuntimeStatus.INTERRUPTED:
|
|
127
|
+
interrupted_events = self._drain(run_id)
|
|
128
|
+
if self._session_service is None:
|
|
129
|
+
raise SessionError(
|
|
130
|
+
"Interrupted execution requires session persistence.",
|
|
131
|
+
code="SESSION_PERSISTENCE_REQUIRED",
|
|
132
|
+
)
|
|
133
|
+
await self._session_service.mark_interrupted(run_id)
|
|
134
|
+
for event in interrupted_events:
|
|
135
|
+
yield event
|
|
136
|
+
yield RunInterrupted(run_id=run_id, session_id=session_id)
|
|
137
|
+
return
|
|
138
|
+
buffered = self._drain(run_id)
|
|
139
|
+
final = _one_final_result(buffered)
|
|
140
|
+
if final is None:
|
|
141
|
+
content = self._final_content(result)
|
|
142
|
+
final = FinalResult(run_id=run_id, session_id=session_id, content=content)
|
|
143
|
+
if self._session_service is not None:
|
|
144
|
+
await self._persist_final(run_id, result, final)
|
|
145
|
+
except NexusError as exc:
|
|
146
|
+
if self._session_service is not None and persisted_run is not None:
|
|
147
|
+
try:
|
|
148
|
+
await self._session_service.fail_run(
|
|
149
|
+
run_id,
|
|
150
|
+
code=exc.code,
|
|
151
|
+
message=str(exc),
|
|
152
|
+
)
|
|
153
|
+
except NexusError as persistence_exc:
|
|
154
|
+
exc = persistence_exc
|
|
155
|
+
yield ErrorOccurred(
|
|
156
|
+
run_id=run_id,
|
|
157
|
+
session_id=session_id,
|
|
158
|
+
code=exc.code,
|
|
159
|
+
message=str(exc),
|
|
160
|
+
retryable=exc.retryable,
|
|
161
|
+
)
|
|
162
|
+
return
|
|
163
|
+
except Exception:
|
|
164
|
+
if self._session_service is not None and persisted_run is not None:
|
|
165
|
+
try:
|
|
166
|
+
await self._session_service.fail_run(
|
|
167
|
+
run_id,
|
|
168
|
+
code="RUNTIME_ERROR",
|
|
169
|
+
message="Nexus could not complete the task.",
|
|
170
|
+
)
|
|
171
|
+
except NexusError:
|
|
172
|
+
pass
|
|
173
|
+
yield ErrorOccurred(
|
|
174
|
+
run_id=run_id,
|
|
175
|
+
session_id=session_id,
|
|
176
|
+
code="RUNTIME_ERROR",
|
|
177
|
+
message="Nexus could not complete the task.",
|
|
178
|
+
retryable=False,
|
|
179
|
+
)
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
for event in buffered:
|
|
183
|
+
yield event
|
|
184
|
+
if final not in buffered:
|
|
185
|
+
yield final
|
|
186
|
+
|
|
187
|
+
async def resume(
|
|
188
|
+
self,
|
|
189
|
+
session_id: str,
|
|
190
|
+
*,
|
|
191
|
+
resume_input: PlanApprovalResumeInput | None = None,
|
|
192
|
+
) -> AsyncIterator[RuntimeEvent]:
|
|
193
|
+
"""Resolve and resume the latest durable interrupted run for a session."""
|
|
194
|
+
|
|
195
|
+
if self._event_publisher is not None:
|
|
196
|
+
async for event in self._resume_live(session_id, resume_input=resume_input):
|
|
197
|
+
yield event
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if self._session_service is None:
|
|
201
|
+
raise SessionError(
|
|
202
|
+
"Session persistence is not configured.",
|
|
203
|
+
code="SESSION_PERSISTENCE_REQUIRED",
|
|
204
|
+
)
|
|
205
|
+
run = await self._session_service.resolve_resumable_run(session_id)
|
|
206
|
+
yield TaskStarted(run_id=run.run_id, session_id=run.session_id, task=run.task)
|
|
207
|
+
try:
|
|
208
|
+
result = await self._graph_runtime.resume(
|
|
209
|
+
thread_id=run.graph_thread_id,
|
|
210
|
+
resume_input=resume_input,
|
|
211
|
+
)
|
|
212
|
+
if result.status is RuntimeStatus.INTERRUPTED:
|
|
213
|
+
for event in self._drain(run.run_id):
|
|
214
|
+
yield event
|
|
215
|
+
yield RunInterrupted(run_id=run.run_id, session_id=run.session_id)
|
|
216
|
+
return
|
|
217
|
+
buffered = self._drain(run.run_id)
|
|
218
|
+
final = _one_final_result(buffered)
|
|
219
|
+
if final is None:
|
|
220
|
+
content = self._final_content(result)
|
|
221
|
+
final = FinalResult(
|
|
222
|
+
run_id=run.run_id,
|
|
223
|
+
session_id=run.session_id,
|
|
224
|
+
content=content,
|
|
225
|
+
)
|
|
226
|
+
await self._persist_final(run.run_id, result, final)
|
|
227
|
+
except NexusError as exc:
|
|
228
|
+
try:
|
|
229
|
+
await self._session_service.fail_run(
|
|
230
|
+
run.run_id,
|
|
231
|
+
code=exc.code,
|
|
232
|
+
message=str(exc),
|
|
233
|
+
)
|
|
234
|
+
except NexusError as persistence_exc:
|
|
235
|
+
exc = persistence_exc
|
|
236
|
+
yield ErrorOccurred(
|
|
237
|
+
run_id=run.run_id,
|
|
238
|
+
session_id=run.session_id,
|
|
239
|
+
code=exc.code,
|
|
240
|
+
message=str(exc),
|
|
241
|
+
retryable=exc.retryable,
|
|
242
|
+
)
|
|
243
|
+
return
|
|
244
|
+
except Exception:
|
|
245
|
+
code = "RUNTIME_ERROR"
|
|
246
|
+
message = "Nexus could not resume the task."
|
|
247
|
+
retryable = False
|
|
248
|
+
try:
|
|
249
|
+
await self._session_service.fail_run(
|
|
250
|
+
run.run_id,
|
|
251
|
+
code=code,
|
|
252
|
+
message=message,
|
|
253
|
+
)
|
|
254
|
+
except NexusError as persistence_exc:
|
|
255
|
+
code = persistence_exc.code
|
|
256
|
+
message = str(persistence_exc)
|
|
257
|
+
retryable = persistence_exc.retryable
|
|
258
|
+
yield ErrorOccurred(
|
|
259
|
+
run_id=run.run_id,
|
|
260
|
+
session_id=run.session_id,
|
|
261
|
+
code=code,
|
|
262
|
+
message=message,
|
|
263
|
+
retryable=retryable,
|
|
264
|
+
)
|
|
265
|
+
return
|
|
266
|
+
for event in buffered:
|
|
267
|
+
yield event
|
|
268
|
+
if final not in buffered:
|
|
269
|
+
yield final
|
|
270
|
+
|
|
271
|
+
async def _run_live(
|
|
272
|
+
self, task: str, session_id: str | None
|
|
273
|
+
) -> AsyncIterator[RuntimeEvent]:
|
|
274
|
+
run_id = str(uuid4())
|
|
275
|
+
execution_id = str(uuid4())
|
|
276
|
+
normalized_task = task.strip()
|
|
277
|
+
persisted_run: Run | None = None
|
|
278
|
+
start_error: NexusError | None = None
|
|
279
|
+
if normalized_task and self._session_service is not None:
|
|
280
|
+
try:
|
|
281
|
+
persisted_run = await self._session_service.start_run(
|
|
282
|
+
run_id=run_id,
|
|
283
|
+
task=normalized_task,
|
|
284
|
+
session_id=session_id,
|
|
285
|
+
model_metadata=self._model_metadata,
|
|
286
|
+
)
|
|
287
|
+
session_id = persisted_run.session_id
|
|
288
|
+
except NexusError as exc:
|
|
289
|
+
start_error = exc
|
|
290
|
+
|
|
291
|
+
context = RunExecutionContext(
|
|
292
|
+
trace_id=run_id,
|
|
293
|
+
execution_id=execution_id,
|
|
294
|
+
run_id=run_id,
|
|
295
|
+
session_id=session_id,
|
|
296
|
+
is_resume=False,
|
|
297
|
+
started_at=datetime.now(UTC),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
async def operation() -> tuple[RuntimeEvent, AgentState | None]:
|
|
301
|
+
if not normalized_task:
|
|
302
|
+
return (
|
|
303
|
+
ErrorOccurred(
|
|
304
|
+
run_id=run_id,
|
|
305
|
+
session_id=session_id,
|
|
306
|
+
code="INVALID_TASK",
|
|
307
|
+
message="Task must be a non-empty string.",
|
|
308
|
+
retryable=False,
|
|
309
|
+
),
|
|
310
|
+
None,
|
|
311
|
+
)
|
|
312
|
+
if start_error is not None:
|
|
313
|
+
return (
|
|
314
|
+
ErrorOccurred(
|
|
315
|
+
run_id=run_id,
|
|
316
|
+
session_id=session_id,
|
|
317
|
+
code=start_error.code,
|
|
318
|
+
message=str(start_error),
|
|
319
|
+
retryable=start_error.retryable,
|
|
320
|
+
),
|
|
321
|
+
None,
|
|
322
|
+
)
|
|
323
|
+
state = AgentState(
|
|
324
|
+
task=normalized_task,
|
|
325
|
+
messages=[ModelMessage(role="user", content=normalized_task)],
|
|
326
|
+
run_id=run_id,
|
|
327
|
+
session_id=session_id,
|
|
328
|
+
status=RuntimeStatus.STARTED,
|
|
329
|
+
)
|
|
330
|
+
return await self._execute_live_run(state, persisted_run)
|
|
331
|
+
|
|
332
|
+
display_task = normalized_task if normalized_task else task
|
|
333
|
+
async for event in self._stream_live(context, display_task, operation):
|
|
334
|
+
yield event
|
|
335
|
+
|
|
336
|
+
async def _resume_live(
|
|
337
|
+
self,
|
|
338
|
+
session_id: str,
|
|
339
|
+
*,
|
|
340
|
+
resume_input: PlanApprovalResumeInput | None,
|
|
341
|
+
) -> AsyncIterator[RuntimeEvent]:
|
|
342
|
+
if self._session_service is None:
|
|
343
|
+
raise SessionError(
|
|
344
|
+
"Session persistence is not configured.",
|
|
345
|
+
code="SESSION_PERSISTENCE_REQUIRED",
|
|
346
|
+
)
|
|
347
|
+
run = await self._session_service.resolve_resumable_run(session_id)
|
|
348
|
+
context = RunExecutionContext(
|
|
349
|
+
trace_id=run.run_id,
|
|
350
|
+
execution_id=str(uuid4()),
|
|
351
|
+
run_id=run.run_id,
|
|
352
|
+
session_id=run.session_id,
|
|
353
|
+
is_resume=True,
|
|
354
|
+
started_at=datetime.now(UTC),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
async def operation() -> tuple[RuntimeEvent, AgentState | None]:
|
|
358
|
+
return await self._execute_live_resume(run, resume_input)
|
|
359
|
+
|
|
360
|
+
async for event in self._stream_live(context, run.task, operation):
|
|
361
|
+
yield event
|
|
362
|
+
|
|
363
|
+
async def _stream_live(
|
|
364
|
+
self,
|
|
365
|
+
context: RunExecutionContext,
|
|
366
|
+
task: str,
|
|
367
|
+
operation: _LiveOperation,
|
|
368
|
+
) -> AsyncIterator[RuntimeEvent]:
|
|
369
|
+
publisher = self._event_publisher
|
|
370
|
+
if publisher is None:
|
|
371
|
+
raise RuntimeError("Live event publisher is not configured.")
|
|
372
|
+
publisher.open_execution(context)
|
|
373
|
+
if self._telemetry_subscriber is not None:
|
|
374
|
+
self._telemetry_subscriber.start_execution(
|
|
375
|
+
TraceRunStart(
|
|
376
|
+
context=context,
|
|
377
|
+
task_character_count=len(task),
|
|
378
|
+
model_provider=_metadata_text(self._model_metadata, "provider"),
|
|
379
|
+
model_name=_metadata_text(self._model_metadata, "model"),
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
started = time.perf_counter()
|
|
384
|
+
with bind_execution_context(context):
|
|
385
|
+
await publisher.publish(
|
|
386
|
+
TaskStarted(
|
|
387
|
+
run_id=context.run_id,
|
|
388
|
+
session_id=context.session_id,
|
|
389
|
+
task=task,
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
async def produce() -> None:
|
|
394
|
+
with bind_execution_context(context):
|
|
395
|
+
terminal, state = await operation()
|
|
396
|
+
await publisher.publish(terminal)
|
|
397
|
+
if self._telemetry_subscriber is not None:
|
|
398
|
+
self._telemetry_subscriber.finish_execution(
|
|
399
|
+
self._trace_finish(context, terminal, state, started)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
producer = asyncio.create_task(produce())
|
|
403
|
+
try:
|
|
404
|
+
async for event in publisher.events(context.execution_id):
|
|
405
|
+
yield event
|
|
406
|
+
await producer
|
|
407
|
+
finally:
|
|
408
|
+
if not producer.done():
|
|
409
|
+
producer.cancel()
|
|
410
|
+
await asyncio.gather(producer, return_exceptions=True)
|
|
411
|
+
publisher.close_execution(context.execution_id)
|
|
412
|
+
|
|
413
|
+
async def _execute_live_run(
|
|
414
|
+
self, state: AgentState, persisted_run: Run | None
|
|
415
|
+
) -> tuple[RuntimeEvent, AgentState | None]:
|
|
416
|
+
try:
|
|
417
|
+
if persisted_run is None:
|
|
418
|
+
result = await self._graph_runtime.run(state)
|
|
419
|
+
else:
|
|
420
|
+
result = await self._graph_runtime.run(
|
|
421
|
+
state, thread_id=persisted_run.graph_thread_id
|
|
422
|
+
)
|
|
423
|
+
if result.status is RuntimeStatus.INTERRUPTED:
|
|
424
|
+
self._drain(state.run_id)
|
|
425
|
+
if self._session_service is None:
|
|
426
|
+
raise SessionError(
|
|
427
|
+
"Interrupted execution requires session persistence.",
|
|
428
|
+
code="SESSION_PERSISTENCE_REQUIRED",
|
|
429
|
+
)
|
|
430
|
+
await self._session_service.mark_interrupted(state.run_id)
|
|
431
|
+
return (
|
|
432
|
+
RunInterrupted(run_id=state.run_id, session_id=state.session_id),
|
|
433
|
+
result,
|
|
434
|
+
)
|
|
435
|
+
buffered = self._drain(state.run_id)
|
|
436
|
+
final = _one_final_result(buffered)
|
|
437
|
+
if final is None:
|
|
438
|
+
final = FinalResult(
|
|
439
|
+
run_id=state.run_id,
|
|
440
|
+
session_id=state.session_id,
|
|
441
|
+
content=self._final_content(result),
|
|
442
|
+
)
|
|
443
|
+
await self._persist_final(state.run_id, result, final)
|
|
444
|
+
return final, result
|
|
445
|
+
except NexusError as exc:
|
|
446
|
+
return await self._live_failure(state.run_id, state.session_id, persisted_run, exc)
|
|
447
|
+
except Exception:
|
|
448
|
+
error = NexusError(
|
|
449
|
+
"Nexus could not complete the task.",
|
|
450
|
+
code="RUNTIME_ERROR",
|
|
451
|
+
)
|
|
452
|
+
return await self._live_failure(
|
|
453
|
+
state.run_id, state.session_id, persisted_run, error
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
async def _execute_live_resume(
|
|
457
|
+
self, run: Run, resume_input: PlanApprovalResumeInput | None
|
|
458
|
+
) -> tuple[RuntimeEvent, AgentState | None]:
|
|
459
|
+
try:
|
|
460
|
+
result = await self._graph_runtime.resume(
|
|
461
|
+
thread_id=run.graph_thread_id,
|
|
462
|
+
resume_input=resume_input,
|
|
463
|
+
)
|
|
464
|
+
if result.status is RuntimeStatus.INTERRUPTED:
|
|
465
|
+
self._drain(run.run_id)
|
|
466
|
+
return RunInterrupted(run_id=run.run_id, session_id=run.session_id), result
|
|
467
|
+
buffered = self._drain(run.run_id)
|
|
468
|
+
final = _one_final_result(buffered)
|
|
469
|
+
if final is None:
|
|
470
|
+
final = FinalResult(
|
|
471
|
+
run_id=run.run_id,
|
|
472
|
+
session_id=run.session_id,
|
|
473
|
+
content=self._final_content(result),
|
|
474
|
+
)
|
|
475
|
+
await self._persist_final(run.run_id, result, final)
|
|
476
|
+
return final, result
|
|
477
|
+
except NexusError as exc:
|
|
478
|
+
return await self._live_failure(run.run_id, run.session_id, run, exc)
|
|
479
|
+
except Exception:
|
|
480
|
+
error = NexusError(
|
|
481
|
+
"Nexus could not resume the task.", code="RUNTIME_ERROR"
|
|
482
|
+
)
|
|
483
|
+
return await self._live_failure(run.run_id, run.session_id, run, error)
|
|
484
|
+
|
|
485
|
+
async def _live_failure(
|
|
486
|
+
self,
|
|
487
|
+
run_id: str,
|
|
488
|
+
session_id: str | None,
|
|
489
|
+
persisted_run: Run | None,
|
|
490
|
+
error: NexusError,
|
|
491
|
+
) -> tuple[RuntimeEvent, None]:
|
|
492
|
+
if self._session_service is not None and persisted_run is not None:
|
|
493
|
+
try:
|
|
494
|
+
await self._session_service.fail_run(
|
|
495
|
+
run_id, code=error.code, message=str(error)
|
|
496
|
+
)
|
|
497
|
+
except NexusError as persistence_error:
|
|
498
|
+
error = persistence_error
|
|
499
|
+
return (
|
|
500
|
+
ErrorOccurred(
|
|
501
|
+
run_id=run_id,
|
|
502
|
+
session_id=session_id,
|
|
503
|
+
code=error.code,
|
|
504
|
+
message=str(error),
|
|
505
|
+
retryable=error.retryable,
|
|
506
|
+
),
|
|
507
|
+
None,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def _trace_finish(
|
|
511
|
+
self,
|
|
512
|
+
context: RunExecutionContext,
|
|
513
|
+
terminal: RuntimeEvent,
|
|
514
|
+
state: AgentState | None,
|
|
515
|
+
started: float,
|
|
516
|
+
) -> TraceRunFinish:
|
|
517
|
+
if isinstance(terminal, FinalResult):
|
|
518
|
+
outcome = (
|
|
519
|
+
ExecutionOutcome.COMPLETED
|
|
520
|
+
if terminal.status is RuntimeStatus.COMPLETED
|
|
521
|
+
else ExecutionOutcome.FAILED
|
|
522
|
+
)
|
|
523
|
+
terminal_status = terminal.terminal_status
|
|
524
|
+
elif isinstance(terminal, RunInterrupted):
|
|
525
|
+
outcome = ExecutionOutcome.INTERRUPTED
|
|
526
|
+
terminal_status = None
|
|
527
|
+
else:
|
|
528
|
+
outcome = ExecutionOutcome.FAILED
|
|
529
|
+
terminal_status = None
|
|
530
|
+
aggregate = TokenUsageAggregate() if state is None else state.token_usage
|
|
531
|
+
if self._ledger is not None:
|
|
532
|
+
ledger_usage = self._ledger.token_usage(context.run_id)
|
|
533
|
+
if ledger_usage.call_count:
|
|
534
|
+
aggregate = ledger_usage
|
|
535
|
+
validation = TraceValidationStatus.NOT_RUN
|
|
536
|
+
if state is not None and state.validation_result is not None:
|
|
537
|
+
validation = TraceValidationStatus(state.validation_result.status.value)
|
|
538
|
+
llm_call_count = 0 if state is None else state.llm_call_count
|
|
539
|
+
step_count = 0 if state is None else state.step_count
|
|
540
|
+
tool_call_count = 0 if state is None else state.tool_call_count
|
|
541
|
+
repair_count = 0 if state is None else state.repair_count
|
|
542
|
+
if self._ledger is not None:
|
|
543
|
+
llm_call_count = max(llm_call_count, self._ledger.model_count(context.run_id))
|
|
544
|
+
step_count = max(step_count, self._ledger.step_count(context.run_id))
|
|
545
|
+
tool_call_count = max(tool_call_count, self._ledger.count(context.run_id))
|
|
546
|
+
repair_count = max(repair_count, self._ledger.repair_count(context.run_id))
|
|
547
|
+
return TraceRunFinish(
|
|
548
|
+
context=context,
|
|
549
|
+
finished_at=datetime.now(UTC),
|
|
550
|
+
execution_outcome=outcome,
|
|
551
|
+
runtime_status=terminal.status,
|
|
552
|
+
terminal_status=terminal_status,
|
|
553
|
+
duration_ms=max(0, int((time.perf_counter() - started) * 1000)),
|
|
554
|
+
step_count=step_count,
|
|
555
|
+
llm_call_count=llm_call_count,
|
|
556
|
+
tool_call_count=tool_call_count,
|
|
557
|
+
replan_count=0 if state is None else state.replan_count,
|
|
558
|
+
repair_count=repair_count,
|
|
559
|
+
token_usage=aggregate.to_usage(),
|
|
560
|
+
changed_file_count=0 if state is None else len(state.changed_files),
|
|
561
|
+
validation_status=validation,
|
|
562
|
+
error_code=terminal.code if isinstance(terminal, ErrorOccurred) else None,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def _drain(self, run_id: str) -> tuple[RuntimeEvent, ...]:
|
|
566
|
+
return () if self._event_buffer is None else self._event_buffer.drain(run_id)
|
|
567
|
+
|
|
568
|
+
async def _persist_final(
|
|
569
|
+
self,
|
|
570
|
+
run_id: str,
|
|
571
|
+
state: AgentState,
|
|
572
|
+
event: FinalResult,
|
|
573
|
+
) -> None:
|
|
574
|
+
if self._session_service is None:
|
|
575
|
+
return
|
|
576
|
+
if state.terminal_status is None and self._event_publisher is None:
|
|
577
|
+
await self._session_service.complete_run(run_id, event.content)
|
|
578
|
+
return
|
|
579
|
+
aggregate = state.token_usage
|
|
580
|
+
if self._ledger is not None:
|
|
581
|
+
ledger_usage = self._ledger.token_usage(run_id)
|
|
582
|
+
if ledger_usage.call_count:
|
|
583
|
+
aggregate = ledger_usage
|
|
584
|
+
llm_call_count = state.llm_call_count
|
|
585
|
+
if self._ledger is not None:
|
|
586
|
+
llm_call_count = self._ledger.model_count(run_id)
|
|
587
|
+
await self._session_service.finalize_run(
|
|
588
|
+
run_id,
|
|
589
|
+
event,
|
|
590
|
+
tool_call_count=state.tool_call_count,
|
|
591
|
+
step_count=state.step_count,
|
|
592
|
+
llm_call_count=llm_call_count,
|
|
593
|
+
replan_count=state.replan_count,
|
|
594
|
+
repair_count=state.repair_count,
|
|
595
|
+
token_usage=aggregate,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
@staticmethod
|
|
599
|
+
def _final_content(state: AgentState) -> str:
|
|
600
|
+
if state.status is not RuntimeStatus.COMPLETED:
|
|
601
|
+
raise NexusError("The graph did not complete successfully.", code="GRAPH_INCOMPLETE")
|
|
602
|
+
if not state.messages or state.messages[-1].role != "assistant":
|
|
603
|
+
raise NexusError("The graph returned no assistant response.", code="GRAPH_NO_RESULT")
|
|
604
|
+
content = state.messages[-1].content.strip()
|
|
605
|
+
if not content:
|
|
606
|
+
raise NexusError(
|
|
607
|
+
"The graph returned an empty assistant response.",
|
|
608
|
+
code="EMPTY_MODEL_OUTPUT",
|
|
609
|
+
)
|
|
610
|
+
return content
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _one_final_result(events: tuple[RuntimeEvent, ...]) -> FinalResult | None:
|
|
614
|
+
finals = tuple(event for event in events if isinstance(event, FinalResult))
|
|
615
|
+
if len(finals) > 1:
|
|
616
|
+
raise NexusError("The graph emitted multiple final results.", code="GRAPH_INVALID_STATE")
|
|
617
|
+
return None if not finals else finals[0]
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _metadata_text(metadata: dict[str, object] | None, key: str) -> str | None:
|
|
621
|
+
if metadata is None:
|
|
622
|
+
return None
|
|
623
|
+
value = metadata.get(key)
|
|
624
|
+
return value if isinstance(value, str) else None
|