bazaar-compute-node 0.1.3__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.
- bazaar_compute_node/__init__.py +3 -0
- bazaar_compute_node/app/__init__.py +1 -0
- bazaar_compute_node/app/application.py +398 -0
- bazaar_compute_node/app/attachments.py +154 -0
- bazaar_compute_node/app/command.py +342 -0
- bazaar_compute_node/app/config.py +121 -0
- bazaar_compute_node/app/registry.py +120 -0
- bazaar_compute_node/app/transport.py +264 -0
- bazaar_compute_node/app/windows_pipe.py +463 -0
- bazaar_compute_node/app/wrapper.py +63 -0
- bazaar_compute_node/bcc.py +524 -0
- bazaar_compute_node/cli.py +382 -0
- bazaar_compute_node/contrib/__init__.py +1 -0
- bazaar_compute_node/contrib/codex_app_server/__init__.py +63 -0
- bazaar_compute_node/contrib/codex_app_server/approval.py +168 -0
- bazaar_compute_node/contrib/codex_app_server/client.py +408 -0
- bazaar_compute_node/contrib/codex_app_server/events.py +431 -0
- bazaar_compute_node/contrib/codex_app_server/plugin.py +15 -0
- bazaar_compute_node/contrib/codex_app_server/process.py +583 -0
- bazaar_compute_node/contrib/codex_app_server/protocol.py +103 -0
- bazaar_compute_node/contrib/codex_app_server/runtime.py +513 -0
- bazaar_compute_node/contrib/logging/__init__.py +5 -0
- bazaar_compute_node/contrib/logging/audit.py +61 -0
- bazaar_compute_node/contrib/logging/plugin.py +11 -0
- bazaar_compute_node/contrib/sqlite/__init__.py +14 -0
- bazaar_compute_node/contrib/sqlite/codec.py +768 -0
- bazaar_compute_node/contrib/sqlite/database.py +282 -0
- bazaar_compute_node/contrib/sqlite/migrations.py +646 -0
- bazaar_compute_node/contrib/sqlite/plugin.py +11 -0
- bazaar_compute_node/contrib/sqlite/repository.py +1059 -0
- bazaar_compute_node/contrib/wecom/__init__.py +1 -0
- bazaar_compute_node/contrib/wecom/channel.py +960 -0
- bazaar_compute_node/contrib/wecom/markdown.py +146 -0
- bazaar_compute_node/contrib/wecom/plugin.py +29 -0
- bazaar_compute_node/core/__init__.py +5 -0
- bazaar_compute_node/core/approval.py +51 -0
- bazaar_compute_node/core/audit.py +101 -0
- bazaar_compute_node/core/channel.py +121 -0
- bazaar_compute_node/core/client.py +30 -0
- bazaar_compute_node/core/command.py +85 -0
- bazaar_compute_node/core/concurrency.py +29 -0
- bazaar_compute_node/core/correlation.py +48 -0
- bazaar_compute_node/core/instruction.py +224 -0
- bazaar_compute_node/core/lifecycle.py +48 -0
- bazaar_compute_node/core/models/__init__.py +63 -0
- bazaar_compute_node/core/models/entities.py +514 -0
- bazaar_compute_node/core/models/states.py +369 -0
- bazaar_compute_node/core/observability.py +47 -0
- bazaar_compute_node/core/orchestration/__init__.py +5 -0
- bazaar_compute_node/core/orchestration/command.py +614 -0
- bazaar_compute_node/core/orchestration/services.py +135 -0
- bazaar_compute_node/core/orchestration/session.py +891 -0
- bazaar_compute_node/core/orchestration/turn.py +451 -0
- bazaar_compute_node/core/outcomes.py +51 -0
- bazaar_compute_node/core/paths.py +19 -0
- bazaar_compute_node/core/runtime.py +118 -0
- bazaar_compute_node/core/storage.py +167 -0
- bazaar_compute_node-0.1.3.dist-info/METADATA +178 -0
- bazaar_compute_node-0.1.3.dist-info/RECORD +62 -0
- bazaar_compute_node-0.1.3.dist-info/WHEEL +4 -0
- bazaar_compute_node-0.1.3.dist-info/entry_points.txt +15 -0
- bazaar_compute_node-0.1.3.dist-info/licenses/LICENSE +613 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from dataclasses import dataclass, replace
|
|
8
|
+
from time import time_ns
|
|
9
|
+
|
|
10
|
+
from ..audit import ErrorKind
|
|
11
|
+
from ..channel import IChannel
|
|
12
|
+
from ..concurrency import ISessionConcurrency, SessionLockRegistry
|
|
13
|
+
from ..correlation import CorrelationContext
|
|
14
|
+
from ..lifecycle import IAsyncLifecycle, TimeoutBudget
|
|
15
|
+
from ..models import (
|
|
16
|
+
AgentSignal,
|
|
17
|
+
AgentState,
|
|
18
|
+
AgentTick,
|
|
19
|
+
AgentTickSource,
|
|
20
|
+
BcnSession,
|
|
21
|
+
ChannelSession,
|
|
22
|
+
ChannelTargetKind,
|
|
23
|
+
ConsumerCursor,
|
|
24
|
+
InboundMessage,
|
|
25
|
+
RuntimeAttempt,
|
|
26
|
+
RuntimeEventState,
|
|
27
|
+
RuntimeSession,
|
|
28
|
+
RuntimeTurn,
|
|
29
|
+
RuntimeTurnState,
|
|
30
|
+
)
|
|
31
|
+
from ..observability import IAudit
|
|
32
|
+
from ..outcomes import ProviderCallStatus
|
|
33
|
+
from ..runtime import IRuntime, RuntimeSessionUnavailable
|
|
34
|
+
from ..storage import IStorage, NodeIdentity
|
|
35
|
+
from .command import SessionCommandService
|
|
36
|
+
from .services import SessionAuditRecorder, SessionStateWriter
|
|
37
|
+
from .turn import SessionContext, SessionTurnCoordinator
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _current_time_ms() -> int:
|
|
41
|
+
return time_ns() // 1_000_000
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(slots=True)
|
|
45
|
+
class _IngressItem:
|
|
46
|
+
message: InboundMessage
|
|
47
|
+
completion: asyncio.Future[RuntimeTurn | None]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True)
|
|
51
|
+
class _RuntimeNotification:
|
|
52
|
+
message: InboundMessage
|
|
53
|
+
context: SessionContext
|
|
54
|
+
completion: asyncio.Future[RuntimeTurn | None]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SessionOrchestrator(IAsyncLifecycle):
|
|
58
|
+
"""Route one Channel composition through provider-neutral core contracts."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
node_id: str | None = None,
|
|
64
|
+
workspace_id: str | None = None,
|
|
65
|
+
channel: IChannel,
|
|
66
|
+
runtime: IRuntime,
|
|
67
|
+
storage: IStorage,
|
|
68
|
+
audit: IAudit,
|
|
69
|
+
timeout_budget: TimeoutBudget,
|
|
70
|
+
concurrency: ISessionConcurrency | None = None,
|
|
71
|
+
clock: Callable[[], int] | None = None,
|
|
72
|
+
on_node_initialized: Callable[[NodeIdentity], Awaitable[None]] | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
for value, field_name in (
|
|
75
|
+
(node_id, "node_id"),
|
|
76
|
+
(runtime.name, "runtime.name"),
|
|
77
|
+
):
|
|
78
|
+
if value is not None and (not isinstance(value, str) or not value):
|
|
79
|
+
raise ValueError(f"{field_name} must be a non-empty string")
|
|
80
|
+
if workspace_id is not None and (
|
|
81
|
+
not isinstance(workspace_id, str) or not workspace_id
|
|
82
|
+
):
|
|
83
|
+
raise ValueError("workspace_id must be a non-empty string")
|
|
84
|
+
self._node_id = node_id
|
|
85
|
+
self._workspace_id = workspace_id
|
|
86
|
+
self._on_node_initialized = on_node_initialized
|
|
87
|
+
self._channel = channel
|
|
88
|
+
self._runtime = runtime
|
|
89
|
+
self._storage = storage
|
|
90
|
+
self._timeout_budget = timeout_budget
|
|
91
|
+
self._concurrency = concurrency or SessionLockRegistry()
|
|
92
|
+
self._clock = clock or _current_time_ms
|
|
93
|
+
self._runtime_sessions: dict[str, RuntimeSession] = {}
|
|
94
|
+
self._runtime_turns: dict[str, RuntimeTurn] = {}
|
|
95
|
+
self._agent_states: dict[str, AgentState] = {}
|
|
96
|
+
self._logger = logging.getLogger("bazaar_compute_node.orchestration.session")
|
|
97
|
+
if not self._logger.handlers:
|
|
98
|
+
self._logger.addHandler(logging.StreamHandler())
|
|
99
|
+
self._logger.setLevel(logging.INFO)
|
|
100
|
+
self._logger.propagate = False
|
|
101
|
+
self._audit = SessionAuditRecorder(
|
|
102
|
+
sink=audit,
|
|
103
|
+
timeout_budget=timeout_budget,
|
|
104
|
+
clock=self._clock,
|
|
105
|
+
)
|
|
106
|
+
self._state_writer = SessionStateWriter(
|
|
107
|
+
storage=storage,
|
|
108
|
+
concurrency=self._concurrency,
|
|
109
|
+
states=self._agent_states,
|
|
110
|
+
)
|
|
111
|
+
self._command_service = SessionCommandService(
|
|
112
|
+
channel=channel,
|
|
113
|
+
storage=storage,
|
|
114
|
+
audit=self._audit,
|
|
115
|
+
provider_call_timeout=timeout_budget.provider_call_seconds,
|
|
116
|
+
concurrency=self._concurrency,
|
|
117
|
+
node_id=lambda: self.node_id,
|
|
118
|
+
clock=self._clock,
|
|
119
|
+
)
|
|
120
|
+
self._turns = SessionTurnCoordinator(
|
|
121
|
+
channel=channel,
|
|
122
|
+
runtime=runtime,
|
|
123
|
+
storage=storage,
|
|
124
|
+
audit=self._audit,
|
|
125
|
+
state_writer=self._state_writer,
|
|
126
|
+
timeout_budget=timeout_budget,
|
|
127
|
+
concurrency=self._concurrency,
|
|
128
|
+
turns=self._runtime_turns,
|
|
129
|
+
node_id=lambda: self.node_id,
|
|
130
|
+
clock=self._clock,
|
|
131
|
+
)
|
|
132
|
+
self._active_tasks: set[asyncio.Task[RuntimeTurn | None]] = set()
|
|
133
|
+
self._ingress_queues: dict[tuple[str, str], asyncio.Queue[_IngressItem]] = {}
|
|
134
|
+
self._ingress_workers: dict[tuple[str, str], asyncio.Task[None]] = {}
|
|
135
|
+
self._runtime_queues: dict[str, asyncio.Queue[_RuntimeNotification]] = {}
|
|
136
|
+
self._runtime_workers: dict[str, asyncio.Task[None]] = {}
|
|
137
|
+
self._receive_task: asyncio.Task[None] | None = None
|
|
138
|
+
self._started = False
|
|
139
|
+
self._stopping = False
|
|
140
|
+
self._shutdown_errors: list[str] = []
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def node_id(self) -> str:
|
|
144
|
+
if self._node_id is None:
|
|
145
|
+
raise RuntimeError("node identity has not been initialized")
|
|
146
|
+
return self._node_id
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def workspace_id(self) -> str:
|
|
150
|
+
if self._workspace_id is None:
|
|
151
|
+
raise RuntimeError("node identity has not been initialized")
|
|
152
|
+
return self._workspace_id
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def command_service(self) -> SessionCommandService:
|
|
156
|
+
return self._command_service
|
|
157
|
+
|
|
158
|
+
def agent_state(self, session_id: str) -> AgentState | None:
|
|
159
|
+
"""Return the process-local lifecycle state for one active Agent."""
|
|
160
|
+
|
|
161
|
+
return self._agent_states.get(session_id)
|
|
162
|
+
|
|
163
|
+
async def start(self, *, timeout: float) -> None:
|
|
164
|
+
if self._started:
|
|
165
|
+
return
|
|
166
|
+
if self._stopping:
|
|
167
|
+
raise RuntimeError("session orchestrator is stopping")
|
|
168
|
+
await self._storage.start(timeout=timeout)
|
|
169
|
+
try:
|
|
170
|
+
identity = await self._storage.initialize(
|
|
171
|
+
node_id=self._node_id,
|
|
172
|
+
workspace_id=self._workspace_id,
|
|
173
|
+
)
|
|
174
|
+
self._node_id = identity.node_id
|
|
175
|
+
self._workspace_id = identity.workspace_id
|
|
176
|
+
if self._on_node_initialized is not None:
|
|
177
|
+
await self._on_node_initialized(identity)
|
|
178
|
+
await self._runtime.start(timeout=timeout)
|
|
179
|
+
await self._channel.start(timeout=timeout)
|
|
180
|
+
except BaseException:
|
|
181
|
+
await self._runtime.stop(timeout=timeout)
|
|
182
|
+
await self._storage.stop(timeout=timeout)
|
|
183
|
+
raise
|
|
184
|
+
self._started = True
|
|
185
|
+
self._receive_task = asyncio.create_task(
|
|
186
|
+
self._receive_loop(), name="bcn-channel-receive"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def stop(self, *, timeout: float) -> None:
|
|
190
|
+
if self._stopping:
|
|
191
|
+
return
|
|
192
|
+
self._stopping = True
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
await self._channel.stop(timeout=timeout)
|
|
196
|
+
except asyncio.CancelledError:
|
|
197
|
+
raise
|
|
198
|
+
except Exception as error: # noqa: BLE001
|
|
199
|
+
self._shutdown_errors.append(f"channel.stop: {type(error).__name__}")
|
|
200
|
+
|
|
201
|
+
receive_task = self._receive_task
|
|
202
|
+
if receive_task is not None and not receive_task.done():
|
|
203
|
+
receive_task.cancel()
|
|
204
|
+
try:
|
|
205
|
+
await asyncio.wait_for(receive_task, timeout=timeout)
|
|
206
|
+
except TimeoutError, asyncio.CancelledError:
|
|
207
|
+
self._shutdown_errors.append("channel.receive: shutdown timeout")
|
|
208
|
+
self._receive_task = None
|
|
209
|
+
|
|
210
|
+
active_tasks = tuple(self._active_tasks)
|
|
211
|
+
for task in active_tasks:
|
|
212
|
+
task.cancel()
|
|
213
|
+
if active_tasks:
|
|
214
|
+
try:
|
|
215
|
+
await asyncio.wait_for(
|
|
216
|
+
asyncio.gather(*active_tasks, return_exceptions=True),
|
|
217
|
+
timeout=timeout,
|
|
218
|
+
)
|
|
219
|
+
except TimeoutError:
|
|
220
|
+
self._shutdown_errors.append("inbound tasks: shutdown timeout")
|
|
221
|
+
|
|
222
|
+
workers = (*self._ingress_workers.values(), *self._runtime_workers.values())
|
|
223
|
+
for worker in workers:
|
|
224
|
+
worker.cancel()
|
|
225
|
+
if workers:
|
|
226
|
+
try:
|
|
227
|
+
await asyncio.wait_for(
|
|
228
|
+
asyncio.gather(*workers, return_exceptions=True),
|
|
229
|
+
timeout=timeout,
|
|
230
|
+
)
|
|
231
|
+
except TimeoutError:
|
|
232
|
+
self._shutdown_errors.append("session workers: shutdown timeout")
|
|
233
|
+
self._ingress_queues.clear()
|
|
234
|
+
self._ingress_workers.clear()
|
|
235
|
+
self._runtime_queues.clear()
|
|
236
|
+
self._runtime_workers.clear()
|
|
237
|
+
|
|
238
|
+
for runtime_session in tuple(self._runtime_sessions.values()):
|
|
239
|
+
await self._stop_runtime_session(runtime_session, timeout=timeout)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
await self._runtime.stop(timeout=timeout)
|
|
243
|
+
except asyncio.CancelledError:
|
|
244
|
+
raise
|
|
245
|
+
except Exception as error: # noqa: BLE001
|
|
246
|
+
self._shutdown_errors.append(f"runtime.stop: {type(error).__name__}")
|
|
247
|
+
try:
|
|
248
|
+
await self._storage.stop(timeout=timeout)
|
|
249
|
+
except asyncio.CancelledError:
|
|
250
|
+
raise
|
|
251
|
+
except Exception as error: # noqa: BLE001
|
|
252
|
+
self._shutdown_errors.append(f"storage.stop: {type(error).__name__}")
|
|
253
|
+
self._started = False
|
|
254
|
+
self._agent_states.clear()
|
|
255
|
+
self._runtime_sessions.clear()
|
|
256
|
+
self._runtime_turns.clear()
|
|
257
|
+
|
|
258
|
+
def dispatch_inbound(
|
|
259
|
+
self, message: InboundMessage
|
|
260
|
+
) -> asyncio.Task[RuntimeTurn | None]:
|
|
261
|
+
"""Schedule one inbound message while retaining its task for shutdown."""
|
|
262
|
+
|
|
263
|
+
if self._stopping:
|
|
264
|
+
raise RuntimeError("session orchestrator is stopping")
|
|
265
|
+
task = asyncio.create_task(
|
|
266
|
+
self.handle_inbound(message),
|
|
267
|
+
name=f"bcn-inbound-{message.message_id}",
|
|
268
|
+
)
|
|
269
|
+
self._active_tasks.add(task)
|
|
270
|
+
task.add_done_callback(self._forget_task)
|
|
271
|
+
return task
|
|
272
|
+
|
|
273
|
+
async def tick(self, session_id: str, tick: AgentTick) -> AgentState:
|
|
274
|
+
"""Apply one serialized lifecycle observation to a bcn session."""
|
|
275
|
+
|
|
276
|
+
if not session_id:
|
|
277
|
+
raise ValueError("session_id must be a non-empty string")
|
|
278
|
+
return await self._state_writer.apply(session_id, tick)
|
|
279
|
+
|
|
280
|
+
async def handle_inbound(self, message: InboundMessage) -> RuntimeTurn | None:
|
|
281
|
+
"""Queue one inbound message without waiting for Runtime I/O at ingress."""
|
|
282
|
+
|
|
283
|
+
loop = asyncio.get_running_loop()
|
|
284
|
+
completion: asyncio.Future[RuntimeTurn | None] = loop.create_future()
|
|
285
|
+
conversation_key = (message.channel, message.provider_thread_id)
|
|
286
|
+
ingress_queue = self._ingress_queues.get(conversation_key)
|
|
287
|
+
if ingress_queue is None:
|
|
288
|
+
ingress_queue = asyncio.Queue()
|
|
289
|
+
self._ingress_queues[conversation_key] = ingress_queue
|
|
290
|
+
self._ingress_workers[conversation_key] = asyncio.create_task(
|
|
291
|
+
self._ingress_loop(ingress_queue),
|
|
292
|
+
name=f"bcn-ingress-{message.channel_session_id}",
|
|
293
|
+
)
|
|
294
|
+
ingress_queue.put_nowait(_IngressItem(message, completion))
|
|
295
|
+
return await completion
|
|
296
|
+
|
|
297
|
+
async def _ingress_loop(
|
|
298
|
+
self,
|
|
299
|
+
queue: asyncio.Queue[_IngressItem],
|
|
300
|
+
) -> None:
|
|
301
|
+
while True:
|
|
302
|
+
item = await queue.get()
|
|
303
|
+
try:
|
|
304
|
+
context, message, created = await self._record_inbound(item.message)
|
|
305
|
+
if not created or context is None:
|
|
306
|
+
if not item.completion.done():
|
|
307
|
+
item.completion.set_result(None)
|
|
308
|
+
continue
|
|
309
|
+
session_id = context.bcn_session.id
|
|
310
|
+
runtime_queue = self._runtime_queues.get(session_id)
|
|
311
|
+
if runtime_queue is None:
|
|
312
|
+
runtime_queue = asyncio.Queue()
|
|
313
|
+
self._runtime_queues[session_id] = runtime_queue
|
|
314
|
+
self._runtime_workers[session_id] = asyncio.create_task(
|
|
315
|
+
self._runtime_loop(runtime_queue),
|
|
316
|
+
name=f"bcn-runtime-{session_id}",
|
|
317
|
+
)
|
|
318
|
+
runtime_queue.put_nowait(
|
|
319
|
+
_RuntimeNotification(message, context, item.completion)
|
|
320
|
+
)
|
|
321
|
+
except asyncio.CancelledError:
|
|
322
|
+
if not item.completion.done():
|
|
323
|
+
item.completion.cancel()
|
|
324
|
+
raise
|
|
325
|
+
except Exception as error: # noqa: BLE001
|
|
326
|
+
if not item.completion.done():
|
|
327
|
+
item.completion.set_exception(error)
|
|
328
|
+
finally:
|
|
329
|
+
queue.task_done()
|
|
330
|
+
|
|
331
|
+
async def _runtime_loop(
|
|
332
|
+
self,
|
|
333
|
+
queue: asyncio.Queue[_RuntimeNotification],
|
|
334
|
+
) -> None:
|
|
335
|
+
while True:
|
|
336
|
+
batch = [await queue.get()]
|
|
337
|
+
while True:
|
|
338
|
+
try:
|
|
339
|
+
batch.append(queue.get_nowait())
|
|
340
|
+
except asyncio.QueueEmpty:
|
|
341
|
+
break
|
|
342
|
+
try:
|
|
343
|
+
result = await self._run_notification(batch[0])
|
|
344
|
+
for notification in batch:
|
|
345
|
+
if not notification.completion.done():
|
|
346
|
+
notification.completion.set_result(result)
|
|
347
|
+
except asyncio.CancelledError:
|
|
348
|
+
for notification in batch:
|
|
349
|
+
if not notification.completion.done():
|
|
350
|
+
notification.completion.cancel()
|
|
351
|
+
raise
|
|
352
|
+
except Exception as error: # noqa: BLE001
|
|
353
|
+
for notification in batch:
|
|
354
|
+
if not notification.completion.done():
|
|
355
|
+
notification.completion.set_exception(error)
|
|
356
|
+
finally:
|
|
357
|
+
for _notification in batch:
|
|
358
|
+
queue.task_done()
|
|
359
|
+
|
|
360
|
+
async def _receive_loop(self) -> None:
|
|
361
|
+
async for message in self._channel.receive():
|
|
362
|
+
if self._stopping:
|
|
363
|
+
break
|
|
364
|
+
self.dispatch_inbound(message)
|
|
365
|
+
|
|
366
|
+
def _forget_task(self, task: asyncio.Task[RuntimeTurn | None]) -> None:
|
|
367
|
+
self._active_tasks.discard(task)
|
|
368
|
+
if task.cancelled():
|
|
369
|
+
return
|
|
370
|
+
error = task.exception()
|
|
371
|
+
if error is None:
|
|
372
|
+
return
|
|
373
|
+
self._logger.error(
|
|
374
|
+
"%s",
|
|
375
|
+
json.dumps(
|
|
376
|
+
{
|
|
377
|
+
"event_name": "channel.inbound.failed",
|
|
378
|
+
"created_at_ms": self._clock(),
|
|
379
|
+
"metadata": {
|
|
380
|
+
"task_name": task.get_name(),
|
|
381
|
+
"error_type": type(error).__name__,
|
|
382
|
+
"error_message": str(error),
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
separators=(",", ":"),
|
|
386
|
+
sort_keys=True,
|
|
387
|
+
),
|
|
388
|
+
exc_info=(type(error), error, error.__traceback__),
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
async def _record_inbound(
|
|
392
|
+
self, message: InboundMessage
|
|
393
|
+
) -> tuple[SessionContext | None, InboundMessage, bool]:
|
|
394
|
+
context: SessionContext | None = None
|
|
395
|
+
channel_session_created = False
|
|
396
|
+
bcn_session_created = False
|
|
397
|
+
runtime_session_created = False
|
|
398
|
+
async with self._storage.transaction() as transaction:
|
|
399
|
+
existing_message = await transaction.find_inbound_message(
|
|
400
|
+
message.channel,
|
|
401
|
+
message.provider_thread_id,
|
|
402
|
+
message.provider_message_id,
|
|
403
|
+
)
|
|
404
|
+
if existing_message is not None:
|
|
405
|
+
message = existing_message
|
|
406
|
+
channel_session = await transaction.find_channel_session(
|
|
407
|
+
channel=message.channel,
|
|
408
|
+
provider_thread_id=message.provider_thread_id,
|
|
409
|
+
)
|
|
410
|
+
now_ms = self._clock()
|
|
411
|
+
if channel_session is None:
|
|
412
|
+
channel_session_created = True
|
|
413
|
+
channel_session = ChannelSession(
|
|
414
|
+
id=message.channel_session_id,
|
|
415
|
+
channel=message.channel,
|
|
416
|
+
provider_thread_id=message.provider_thread_id,
|
|
417
|
+
created_at_ms=now_ms,
|
|
418
|
+
updated_at_ms=now_ms,
|
|
419
|
+
target_kind=message.target_kind,
|
|
420
|
+
following=message.target_kind is ChannelTargetKind.DM
|
|
421
|
+
or message.mentions_agent,
|
|
422
|
+
)
|
|
423
|
+
await transaction.save_channel_session(channel_session)
|
|
424
|
+
elif (
|
|
425
|
+
existing_message is None
|
|
426
|
+
and message.mentions_agent
|
|
427
|
+
and not channel_session.following
|
|
428
|
+
):
|
|
429
|
+
channel_session = replace(
|
|
430
|
+
channel_session,
|
|
431
|
+
following=True,
|
|
432
|
+
updated_at_ms=now_ms,
|
|
433
|
+
)
|
|
434
|
+
await transaction.save_channel_session(channel_session)
|
|
435
|
+
|
|
436
|
+
bcn_session = await transaction.find_bcn_session(channel_session.id)
|
|
437
|
+
if bcn_session is None:
|
|
438
|
+
bcn_session_created = True
|
|
439
|
+
bcn_session = BcnSession(
|
|
440
|
+
id=message.session_id,
|
|
441
|
+
channel_session_id=channel_session.id,
|
|
442
|
+
workspace_id=self.workspace_id,
|
|
443
|
+
created_at_ms=now_ms,
|
|
444
|
+
updated_at_ms=now_ms,
|
|
445
|
+
)
|
|
446
|
+
await transaction.save_bcn_session(bcn_session)
|
|
447
|
+
|
|
448
|
+
if existing_message is None:
|
|
449
|
+
notifies_runtime = message.notifies_runtime and (
|
|
450
|
+
message.target_kind is ChannelTargetKind.DM
|
|
451
|
+
or channel_session.following
|
|
452
|
+
or message.mentions_agent
|
|
453
|
+
)
|
|
454
|
+
canonical_target = message.canonical_target
|
|
455
|
+
if channel_session.id != message.channel_session_id:
|
|
456
|
+
canonical_target = (
|
|
457
|
+
f"{channel_session.target_kind.value}:{channel_session.id}"
|
|
458
|
+
)
|
|
459
|
+
message = replace(
|
|
460
|
+
message,
|
|
461
|
+
session_id=bcn_session.id,
|
|
462
|
+
channel_session_id=channel_session.id,
|
|
463
|
+
canonical_target=canonical_target,
|
|
464
|
+
notifies_runtime=notifies_runtime,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
cursor = await transaction.get_consumer_cursor(bcn_session.id)
|
|
468
|
+
if cursor is None:
|
|
469
|
+
await transaction.save_consumer_cursor(
|
|
470
|
+
ConsumerCursor(session_id=bcn_session.id)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if existing_message is None:
|
|
474
|
+
message = await transaction.append_inbound_message(message)
|
|
475
|
+
channel_session = replace(
|
|
476
|
+
channel_session,
|
|
477
|
+
last_inbound_at_ms=message.received_at_ms,
|
|
478
|
+
updated_at_ms=now_ms,
|
|
479
|
+
)
|
|
480
|
+
bcn_session = replace(
|
|
481
|
+
bcn_session,
|
|
482
|
+
last_activity_at_ms=message.received_at_ms,
|
|
483
|
+
updated_at_ms=now_ms,
|
|
484
|
+
)
|
|
485
|
+
await transaction.save_channel_session(channel_session)
|
|
486
|
+
await transaction.save_bcn_session(bcn_session)
|
|
487
|
+
|
|
488
|
+
runtime_session: RuntimeSession | None = None
|
|
489
|
+
if message.notifies_runtime:
|
|
490
|
+
runtime_session = await transaction.find_runtime_session(bcn_session.id)
|
|
491
|
+
if runtime_session is None:
|
|
492
|
+
runtime_session_created = True
|
|
493
|
+
runtime_session = RuntimeSession(
|
|
494
|
+
id=f"runtime-{bcn_session.id}",
|
|
495
|
+
bcn_session_id=bcn_session.id,
|
|
496
|
+
channel_session_id=channel_session.id,
|
|
497
|
+
runtime=self._runtime.name,
|
|
498
|
+
workspace_id=self.workspace_id,
|
|
499
|
+
created_at_ms=now_ms,
|
|
500
|
+
updated_at_ms=now_ms,
|
|
501
|
+
)
|
|
502
|
+
await transaction.save_runtime_session(runtime_session)
|
|
503
|
+
context = SessionContext(channel_session, bcn_session, runtime_session)
|
|
504
|
+
|
|
505
|
+
if existing_message is None:
|
|
506
|
+
await self._audit.append(
|
|
507
|
+
event_name="channel.inbound.persisted",
|
|
508
|
+
state=RuntimeEventState.COMPLETED,
|
|
509
|
+
correlation=CorrelationContext(
|
|
510
|
+
node_id=self.node_id,
|
|
511
|
+
channel=message.channel,
|
|
512
|
+
channel_session_id=message.channel_session_id,
|
|
513
|
+
bcn_session_id=message.session_id,
|
|
514
|
+
runtime_session_id=(
|
|
515
|
+
runtime_session.id if runtime_session is not None else None
|
|
516
|
+
),
|
|
517
|
+
provider_thread_id=message.provider_thread_id,
|
|
518
|
+
inbound_seq=message.seq,
|
|
519
|
+
),
|
|
520
|
+
metadata={
|
|
521
|
+
"notifies_runtime": message.notifies_runtime,
|
|
522
|
+
"channel_session_mapping": (
|
|
523
|
+
"created" if channel_session_created else "reused"
|
|
524
|
+
),
|
|
525
|
+
"bcn_session_mapping": (
|
|
526
|
+
"created" if bcn_session_created else "reused"
|
|
527
|
+
),
|
|
528
|
+
"runtime_session_mapping": (
|
|
529
|
+
"created"
|
|
530
|
+
if runtime_session_created
|
|
531
|
+
else "reused"
|
|
532
|
+
if runtime_session is not None
|
|
533
|
+
else "not_requested"
|
|
534
|
+
),
|
|
535
|
+
},
|
|
536
|
+
)
|
|
537
|
+
return context, message, existing_message is None
|
|
538
|
+
|
|
539
|
+
async def _run_notification(
|
|
540
|
+
self, notification: _RuntimeNotification
|
|
541
|
+
) -> RuntimeTurn | None:
|
|
542
|
+
message = notification.message
|
|
543
|
+
context = notification.context
|
|
544
|
+
async with self._storage.transaction() as transaction:
|
|
545
|
+
runtime_session = await transaction.find_runtime_session(
|
|
546
|
+
context.bcn_session.id
|
|
547
|
+
)
|
|
548
|
+
if runtime_session is None:
|
|
549
|
+
raise RuntimeError("notifying inbound has no runtime session")
|
|
550
|
+
context = SessionContext(
|
|
551
|
+
context.channel_session,
|
|
552
|
+
context.bcn_session,
|
|
553
|
+
runtime_session,
|
|
554
|
+
)
|
|
555
|
+
cursor = await transaction.get_consumer_cursor(context.bcn_session.id)
|
|
556
|
+
delivered_through_seq = (
|
|
557
|
+
cursor.delivered_through_seq if cursor is not None else 0
|
|
558
|
+
)
|
|
559
|
+
unread = await transaction.list_inbound_messages(
|
|
560
|
+
context.bcn_session.id,
|
|
561
|
+
after_seq=delivered_through_seq,
|
|
562
|
+
notifying_only=True,
|
|
563
|
+
)
|
|
564
|
+
if not unread:
|
|
565
|
+
return None
|
|
566
|
+
turn_id = f"turn-{message.message_id}"
|
|
567
|
+
if await transaction.get_runtime_attempt(turn_id) is not None:
|
|
568
|
+
return self._runtime_turns.get(turn_id)
|
|
569
|
+
turn = RuntimeTurn(
|
|
570
|
+
turn_id=turn_id,
|
|
571
|
+
session_id=context.runtime_session.id,
|
|
572
|
+
state=RuntimeTurnState.STARTING,
|
|
573
|
+
started_at_ms=self._clock(),
|
|
574
|
+
client_user_message_id=message.message_id,
|
|
575
|
+
)
|
|
576
|
+
await transaction.save_runtime_attempt(
|
|
577
|
+
RuntimeAttempt(
|
|
578
|
+
turn_id=turn.turn_id,
|
|
579
|
+
session_id=turn.session_id,
|
|
580
|
+
client_user_message_id=message.message_id,
|
|
581
|
+
started_at_ms=turn.started_at_ms,
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
self._runtime_turns[turn.turn_id] = turn
|
|
585
|
+
|
|
586
|
+
for attempt in range(2):
|
|
587
|
+
context = await self._ensure_runtime_session(context)
|
|
588
|
+
agent_state = self._state_writer.get(context.bcn_session.id)
|
|
589
|
+
if agent_state is not AgentState.IDLE:
|
|
590
|
+
finish_state = (
|
|
591
|
+
RuntimeTurnState.UNKNOWN
|
|
592
|
+
if agent_state is AgentState.UNKNOWN
|
|
593
|
+
else RuntimeTurnState.FAILED
|
|
594
|
+
)
|
|
595
|
+
return await self._turns.finish_turn(
|
|
596
|
+
turn,
|
|
597
|
+
finish_state,
|
|
598
|
+
error_kind=(
|
|
599
|
+
ErrorKind.PROVIDER_UNKNOWN
|
|
600
|
+
if finish_state is RuntimeTurnState.UNKNOWN
|
|
601
|
+
else ErrorKind.PROVIDER_FAILED
|
|
602
|
+
),
|
|
603
|
+
error_message=(
|
|
604
|
+
"runtime session start outcome is unknown"
|
|
605
|
+
if finish_state is RuntimeTurnState.UNKNOWN
|
|
606
|
+
else "runtime session failed to start"
|
|
607
|
+
),
|
|
608
|
+
correlation=self._turns.turn_correlation(message, context, turn),
|
|
609
|
+
session_id=context.bcn_session.id,
|
|
610
|
+
)
|
|
611
|
+
self._state_writer.apply_observation(
|
|
612
|
+
context.bcn_session.id,
|
|
613
|
+
AgentTick(
|
|
614
|
+
source=AgentTickSource.CHANNEL,
|
|
615
|
+
signal=AgentSignal.TURN_STARTED,
|
|
616
|
+
observed_at_ms=self._clock(),
|
|
617
|
+
),
|
|
618
|
+
)
|
|
619
|
+
try:
|
|
620
|
+
return await self._turns.run_turn(
|
|
621
|
+
message,
|
|
622
|
+
context,
|
|
623
|
+
turn,
|
|
624
|
+
unread_count=len(unread),
|
|
625
|
+
)
|
|
626
|
+
except RuntimeSessionUnavailable as error:
|
|
627
|
+
self._state_writer.apply_observation(
|
|
628
|
+
context.bcn_session.id,
|
|
629
|
+
AgentTick(
|
|
630
|
+
source=AgentTickSource.RUNTIME,
|
|
631
|
+
signal=AgentSignal.FAILED,
|
|
632
|
+
observed_at_ms=self._clock(),
|
|
633
|
+
error_kind=ErrorKind.PROVIDER_FAILED.value,
|
|
634
|
+
error_message=str(error),
|
|
635
|
+
),
|
|
636
|
+
)
|
|
637
|
+
if attempt == 1:
|
|
638
|
+
return await self._turns.finish_turn(
|
|
639
|
+
turn,
|
|
640
|
+
RuntimeTurnState.FAILED,
|
|
641
|
+
error_kind=ErrorKind.PROVIDER_FAILED,
|
|
642
|
+
error_message=str(error),
|
|
643
|
+
correlation=self._turns.turn_correlation(
|
|
644
|
+
message, context, turn
|
|
645
|
+
),
|
|
646
|
+
session_id=context.bcn_session.id,
|
|
647
|
+
)
|
|
648
|
+
raise AssertionError("runtime pre-start retry loop did not return")
|
|
649
|
+
|
|
650
|
+
async def _ensure_runtime_session(self, context: SessionContext) -> SessionContext:
|
|
651
|
+
runtime_session = context.runtime_session
|
|
652
|
+
agent_state = self._state_writer.get(context.bcn_session.id)
|
|
653
|
+
if agent_state in {
|
|
654
|
+
AgentState.IDLE,
|
|
655
|
+
AgentState.WORKING,
|
|
656
|
+
AgentState.COMPACTION_STARTING,
|
|
657
|
+
AgentState.COMPACTING,
|
|
658
|
+
AgentState.COMPACTION_COMPLETED,
|
|
659
|
+
AgentState.STOPPING,
|
|
660
|
+
}:
|
|
661
|
+
return context
|
|
662
|
+
|
|
663
|
+
if agent_state in {AgentState.CREATED, AgentState.FAILED}:
|
|
664
|
+
agent_state = self._state_writer.apply_observation(
|
|
665
|
+
context.bcn_session.id,
|
|
666
|
+
AgentTick(
|
|
667
|
+
source=AgentTickSource.SESSION,
|
|
668
|
+
signal=AgentSignal.START_REQUESTED,
|
|
669
|
+
observed_at_ms=self._clock(),
|
|
670
|
+
),
|
|
671
|
+
)
|
|
672
|
+
elif agent_state is AgentState.UNKNOWN:
|
|
673
|
+
agent_state = self._state_writer.apply_observation(
|
|
674
|
+
context.bcn_session.id,
|
|
675
|
+
AgentTick(
|
|
676
|
+
source=AgentTickSource.RECOVERY,
|
|
677
|
+
signal=AgentSignal.RECONCILE_REQUESTED,
|
|
678
|
+
observed_at_ms=self._clock(),
|
|
679
|
+
),
|
|
680
|
+
)
|
|
681
|
+
if agent_state not in {AgentState.STARTING, AgentState.RECONCILING}:
|
|
682
|
+
return context
|
|
683
|
+
|
|
684
|
+
process_operation = (
|
|
685
|
+
"start" if runtime_session.provider_thread_id is None else "resume"
|
|
686
|
+
)
|
|
687
|
+
process_correlation = CorrelationContext(
|
|
688
|
+
node_id=self.node_id,
|
|
689
|
+
channel=context.channel_session.channel,
|
|
690
|
+
channel_session_id=context.channel_session.id,
|
|
691
|
+
bcn_session_id=context.bcn_session.id,
|
|
692
|
+
runtime_session_id=runtime_session.id,
|
|
693
|
+
provider_thread_id=runtime_session.provider_thread_id,
|
|
694
|
+
)
|
|
695
|
+
await self._audit.append(
|
|
696
|
+
event_name=f"runtime.process.{process_operation}.requested",
|
|
697
|
+
state=RuntimeEventState.STARTED,
|
|
698
|
+
correlation=process_correlation,
|
|
699
|
+
metadata={
|
|
700
|
+
"runtime": runtime_session.runtime,
|
|
701
|
+
"workspace_id": runtime_session.workspace_id,
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
if process_operation == "start":
|
|
706
|
+
provider_result = await self._runtime.start_session(
|
|
707
|
+
runtime_session,
|
|
708
|
+
timeout=self._timeout_budget.provider_call_seconds,
|
|
709
|
+
)
|
|
710
|
+
else:
|
|
711
|
+
provider_result = await self._runtime.resume_session(
|
|
712
|
+
runtime_session,
|
|
713
|
+
timeout=self._timeout_budget.startup_seconds,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
now_ms = self._clock()
|
|
717
|
+
if provider_result.status is ProviderCallStatus.CONFIRMED:
|
|
718
|
+
updated_runtime = provider_result.value
|
|
719
|
+
if updated_runtime is None:
|
|
720
|
+
raise ValueError("confirmed runtime start has no session")
|
|
721
|
+
if (
|
|
722
|
+
updated_runtime.bcn_session_id != context.bcn_session.id
|
|
723
|
+
or updated_runtime.channel_session_id != context.channel_session.id
|
|
724
|
+
or updated_runtime.workspace_id != self.workspace_id
|
|
725
|
+
):
|
|
726
|
+
raise ValueError("runtime provider returned a mismatched session")
|
|
727
|
+
runtime_session = updated_runtime
|
|
728
|
+
async with self._storage.transaction() as transaction:
|
|
729
|
+
await transaction.save_runtime_session(runtime_session)
|
|
730
|
+
current_state = self._state_writer.get(context.bcn_session.id)
|
|
731
|
+
if current_state is AgentState.STARTING:
|
|
732
|
+
self._state_writer.apply_observation(
|
|
733
|
+
context.bcn_session.id,
|
|
734
|
+
AgentTick(
|
|
735
|
+
source=AgentTickSource.RUNTIME,
|
|
736
|
+
signal=AgentSignal.START_CONFIRMED,
|
|
737
|
+
observed_at_ms=now_ms,
|
|
738
|
+
),
|
|
739
|
+
)
|
|
740
|
+
elif current_state is AgentState.RECONCILING:
|
|
741
|
+
self._state_writer.apply_observation(
|
|
742
|
+
context.bcn_session.id,
|
|
743
|
+
AgentTick(
|
|
744
|
+
source=AgentTickSource.RECOVERY,
|
|
745
|
+
signal=AgentSignal.RECONCILE_CONFIRMED,
|
|
746
|
+
observed_at_ms=now_ms,
|
|
747
|
+
),
|
|
748
|
+
)
|
|
749
|
+
await self._audit.append(
|
|
750
|
+
event_name=(
|
|
751
|
+
"runtime.process.started"
|
|
752
|
+
if process_operation == "start"
|
|
753
|
+
else "runtime.process.resumed"
|
|
754
|
+
),
|
|
755
|
+
state=RuntimeEventState.COMPLETED,
|
|
756
|
+
correlation=replace(
|
|
757
|
+
process_correlation,
|
|
758
|
+
provider_thread_id=runtime_session.provider_thread_id,
|
|
759
|
+
),
|
|
760
|
+
metadata={
|
|
761
|
+
"runtime": runtime_session.runtime,
|
|
762
|
+
"workspace_id": runtime_session.workspace_id,
|
|
763
|
+
},
|
|
764
|
+
)
|
|
765
|
+
else:
|
|
766
|
+
signal = (
|
|
767
|
+
AgentSignal.FAILED
|
|
768
|
+
if provider_result.status is ProviderCallStatus.FAILED
|
|
769
|
+
else AgentSignal.UNKNOWN
|
|
770
|
+
)
|
|
771
|
+
self._state_writer.apply_observation(
|
|
772
|
+
context.bcn_session.id,
|
|
773
|
+
AgentTick(
|
|
774
|
+
source=AgentTickSource.RUNTIME,
|
|
775
|
+
signal=signal,
|
|
776
|
+
observed_at_ms=now_ms,
|
|
777
|
+
error_kind=provider_result.error_kind,
|
|
778
|
+
error_message=provider_result.error_message,
|
|
779
|
+
),
|
|
780
|
+
)
|
|
781
|
+
await self._audit.append(
|
|
782
|
+
event_name=f"runtime.process.{signal.value}",
|
|
783
|
+
state=(
|
|
784
|
+
RuntimeEventState.FAILED
|
|
785
|
+
if signal is AgentSignal.FAILED
|
|
786
|
+
else RuntimeEventState.UNKNOWN
|
|
787
|
+
),
|
|
788
|
+
correlation=process_correlation,
|
|
789
|
+
error_kind=(
|
|
790
|
+
ErrorKind(provider_result.error_kind)
|
|
791
|
+
if provider_result.error_kind in ErrorKind._value2member_map_
|
|
792
|
+
else ErrorKind.INTERNAL
|
|
793
|
+
),
|
|
794
|
+
error_message=provider_result.error_message,
|
|
795
|
+
metadata={
|
|
796
|
+
"operation": process_operation,
|
|
797
|
+
"runtime": runtime_session.runtime,
|
|
798
|
+
"workspace_id": runtime_session.workspace_id,
|
|
799
|
+
},
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
self._runtime_sessions[runtime_session.id] = runtime_session
|
|
803
|
+
return SessionContext(
|
|
804
|
+
context.channel_session,
|
|
805
|
+
context.bcn_session,
|
|
806
|
+
runtime_session,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
async def _stop_runtime_session(
|
|
810
|
+
self, runtime_session: RuntimeSession, *, timeout: float
|
|
811
|
+
) -> None:
|
|
812
|
+
async with self._concurrency.for_session(runtime_session.bcn_session_id):
|
|
813
|
+
await self._stop_runtime_session_locked(runtime_session, timeout=timeout)
|
|
814
|
+
|
|
815
|
+
async def _stop_runtime_session_locked(
|
|
816
|
+
self, runtime_session: RuntimeSession, *, timeout: float
|
|
817
|
+
) -> None:
|
|
818
|
+
process_correlation = CorrelationContext(
|
|
819
|
+
node_id=self.node_id,
|
|
820
|
+
channel_session_id=runtime_session.channel_session_id,
|
|
821
|
+
bcn_session_id=runtime_session.bcn_session_id,
|
|
822
|
+
runtime_session_id=runtime_session.id,
|
|
823
|
+
provider_thread_id=runtime_session.provider_thread_id,
|
|
824
|
+
)
|
|
825
|
+
await self._audit.append(
|
|
826
|
+
event_name="runtime.process.stop.requested",
|
|
827
|
+
state=RuntimeEventState.STARTED,
|
|
828
|
+
correlation=process_correlation,
|
|
829
|
+
metadata={
|
|
830
|
+
"runtime": runtime_session.runtime,
|
|
831
|
+
"workspace_id": runtime_session.workspace_id,
|
|
832
|
+
},
|
|
833
|
+
)
|
|
834
|
+
await self._state_writer.apply_locked(
|
|
835
|
+
runtime_session.bcn_session_id,
|
|
836
|
+
AgentTick(
|
|
837
|
+
source=AgentTickSource.SESSION,
|
|
838
|
+
signal=AgentSignal.STOP_REQUESTED,
|
|
839
|
+
observed_at_ms=self._clock(),
|
|
840
|
+
),
|
|
841
|
+
)
|
|
842
|
+
try:
|
|
843
|
+
result = await self._runtime.stop_session(runtime_session, timeout=timeout)
|
|
844
|
+
except Exception as error: # noqa: BLE001
|
|
845
|
+
result = None
|
|
846
|
+
stop_error = error
|
|
847
|
+
else:
|
|
848
|
+
stop_error = None
|
|
849
|
+
confirmed = result is not None and result.status is ProviderCallStatus.CONFIRMED
|
|
850
|
+
unknown = result is None or result.status in {
|
|
851
|
+
ProviderCallStatus.UNKNOWN,
|
|
852
|
+
ProviderCallStatus.QUEUED,
|
|
853
|
+
}
|
|
854
|
+
error_kind = (
|
|
855
|
+
result.error_kind
|
|
856
|
+
if result is not None
|
|
857
|
+
else ErrorKind.PROVIDER_UNKNOWN.value
|
|
858
|
+
)
|
|
859
|
+
error_message = result.error_message if result is not None else str(stop_error)
|
|
860
|
+
self._runtime_sessions.pop(runtime_session.id, None)
|
|
861
|
+
await self._audit.append(
|
|
862
|
+
event_name=(
|
|
863
|
+
"runtime.process.stop.completed"
|
|
864
|
+
if confirmed
|
|
865
|
+
else "runtime.process.stop.unknown"
|
|
866
|
+
if unknown
|
|
867
|
+
else "runtime.process.stop.failed"
|
|
868
|
+
),
|
|
869
|
+
state=(
|
|
870
|
+
RuntimeEventState.COMPLETED
|
|
871
|
+
if confirmed
|
|
872
|
+
else RuntimeEventState.UNKNOWN
|
|
873
|
+
if unknown
|
|
874
|
+
else RuntimeEventState.FAILED
|
|
875
|
+
),
|
|
876
|
+
correlation=process_correlation,
|
|
877
|
+
error_kind=(
|
|
878
|
+
ErrorKind(error_kind)
|
|
879
|
+
if error_kind in ErrorKind._value2member_map_
|
|
880
|
+
else ErrorKind.INTERNAL
|
|
881
|
+
if error_message
|
|
882
|
+
else None
|
|
883
|
+
)
|
|
884
|
+
if not confirmed
|
|
885
|
+
else None,
|
|
886
|
+
error_message=error_message if not confirmed else None,
|
|
887
|
+
metadata={
|
|
888
|
+
"runtime": runtime_session.runtime,
|
|
889
|
+
"workspace_id": runtime_session.workspace_id,
|
|
890
|
+
},
|
|
891
|
+
)
|