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
@@ -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