claude-smart 0.2.47 → 0.2.48
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/pyproject.toml +1 -1
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +8 -5
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +133 -3273
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +29 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +56 -351
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +6 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +13 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +33 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +44 -86
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Batch-progress + rerun orchestration for ``BaseGenerationService`` (Tier-1b decomposition).
|
|
2
|
+
|
|
3
|
+
``BatchProgressMixin`` holds the shared batch-with-progress driver
|
|
4
|
+
(``_run_batch_with_progress``) plus the ``run_rerun`` public template method and the
|
|
5
|
+
overridable rerun hooks subclasses implement (``_get_rerun_user_ids``,
|
|
6
|
+
``_build_rerun_request_params``, ``_create_run_request_for_item``,
|
|
7
|
+
``_create_rerun_response``, ``_get_generated_count``, ``_pre_process_rerun``).
|
|
8
|
+
|
|
9
|
+
``_run_batch_with_progress`` is both subclass-called (``self._run_batch_with_progress``
|
|
10
|
+
in the profile/playbook manual-run paths, resolved via MRO) and instance-patched in
|
|
11
|
+
tests (``patch.object(svc, "_run_batch_with_progress")``, which is location-agnostic).
|
|
12
|
+
It is the sole WRITER of ``self._is_batch_mode`` (read by the base ``run``); that field
|
|
13
|
+
is initialised on the base ``__init__`` and stubbed annotation-only below. ``run_rerun``
|
|
14
|
+
is PUBLIC (dispatched via ``run_method="run_rerun"`` from ``lib/_generation.py``) so its
|
|
15
|
+
signature is byte-identical. Method bodies are moved VERBATIM from the former monolithic
|
|
16
|
+
``base_generation_service.py``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
21
|
+
|
|
22
|
+
from reflexio.server.services.operation_state_utils import OperationStateManager
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
TRequest = TypeVar("TRequest")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BatchProgressMixin(Generic[TRequest]): # noqa: UP046
|
|
30
|
+
"""Batch-with-progress driver + rerun template method and hooks.
|
|
31
|
+
|
|
32
|
+
Mixed into ``BaseGenerationService`` ahead of the other mixins and ``ABC``. Every
|
|
33
|
+
``self.`` call resolves either to a sibling hook on this mixin or, via MRO, to a
|
|
34
|
+
base-owned method (``run``, ``_create_state_manager``, ``_get_base_service_name``)
|
|
35
|
+
or the base-initialised ``_is_batch_mode`` field — the annotation-only / type-only
|
|
36
|
+
stubs below give pyright the types without introducing shared class-level state.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Annotation-only stub for the base-owned field this driver WRITES (init'd in the
|
|
40
|
+
# base ``__init__``). NEVER assign here — a class-level default would be a
|
|
41
|
+
# shared-state footgun on a mixin.
|
|
42
|
+
_is_batch_mode: bool
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
# Abstract on the base ABC (stays there per SINK-2); declared type-only so
|
|
46
|
+
# pyright can resolve the ``self._get_base_service_name()`` call. No runtime
|
|
47
|
+
# attribute is added, so ``__abstractmethods__`` is unaffected.
|
|
48
|
+
def _get_base_service_name(self) -> str: ...
|
|
49
|
+
# Concrete on the base (the FIFO-drain orchestrator + lock/queue helper);
|
|
50
|
+
# resolved via MRO. Declared type-only so pyright can resolve the calls.
|
|
51
|
+
def run(self, request: TRequest) -> None: ...
|
|
52
|
+
def _create_state_manager(self) -> OperationStateManager: ...
|
|
53
|
+
|
|
54
|
+
# ===============================
|
|
55
|
+
# Batch with progress (shared by rerun + manual)
|
|
56
|
+
# ===============================
|
|
57
|
+
|
|
58
|
+
def _run_batch_with_progress(
|
|
59
|
+
self,
|
|
60
|
+
user_ids: list[str],
|
|
61
|
+
request: TRequest,
|
|
62
|
+
request_params: dict,
|
|
63
|
+
state_manager: OperationStateManager,
|
|
64
|
+
) -> tuple[int, int]:
|
|
65
|
+
"""Run a batch of users with progress tracking.
|
|
66
|
+
|
|
67
|
+
Shared logic for both run_rerun() and run_manual_regular().
|
|
68
|
+
Initializes progress, processes each user, and finalizes.
|
|
69
|
+
Checks for cancellation before each user.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
user_ids: List of user IDs to process
|
|
73
|
+
request: The original request object
|
|
74
|
+
request_params: Parameters dict for progress state
|
|
75
|
+
state_manager: OperationStateManager instance
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Tuple of (users_processed, total_generated)
|
|
79
|
+
"""
|
|
80
|
+
total_users = len(user_ids)
|
|
81
|
+
self._is_batch_mode = True
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# Initialize progress. Kept inside the try so a raise here still
|
|
85
|
+
# resets _is_batch_mode via the finally (no leak into the next op).
|
|
86
|
+
state_manager.initialize_progress(
|
|
87
|
+
total_users=total_users,
|
|
88
|
+
request_params=request_params,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Process each user
|
|
92
|
+
users_processed = 0
|
|
93
|
+
processed_user_ids: list[str] = []
|
|
94
|
+
for user_id in user_ids:
|
|
95
|
+
# Check for cancellation before starting next user
|
|
96
|
+
if state_manager.is_cancellation_requested():
|
|
97
|
+
logger.info(
|
|
98
|
+
"Cancellation requested for %s, stopping after %d/%d users",
|
|
99
|
+
self._get_base_service_name(),
|
|
100
|
+
users_processed,
|
|
101
|
+
total_users,
|
|
102
|
+
)
|
|
103
|
+
state_manager.mark_cancelled()
|
|
104
|
+
return users_processed, self._get_generated_count(
|
|
105
|
+
request, processed_user_ids=processed_user_ids
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
state_manager.set_current_item(user_id)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
run_request = self._create_run_request_for_item(user_id, request)
|
|
112
|
+
self.run(run_request)
|
|
113
|
+
users_processed += 1
|
|
114
|
+
processed_user_ids.append(user_id)
|
|
115
|
+
|
|
116
|
+
state_manager.update_progress(
|
|
117
|
+
item_id=user_id,
|
|
118
|
+
count=0, # Extractors collect their own data
|
|
119
|
+
success=True,
|
|
120
|
+
total_users=total_users,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(
|
|
125
|
+
"Failed to process user %s for %s: %s",
|
|
126
|
+
user_id,
|
|
127
|
+
self._get_base_service_name(),
|
|
128
|
+
str(e),
|
|
129
|
+
)
|
|
130
|
+
state_manager.update_progress(
|
|
131
|
+
item_id=user_id,
|
|
132
|
+
count=0,
|
|
133
|
+
success=False,
|
|
134
|
+
total_users=total_users,
|
|
135
|
+
error=str(e),
|
|
136
|
+
)
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
# Get generated count and finalize
|
|
140
|
+
total_generated = self._get_generated_count(
|
|
141
|
+
request, processed_user_ids=processed_user_ids
|
|
142
|
+
)
|
|
143
|
+
state_manager.finalize_progress(users_processed, total_generated)
|
|
144
|
+
|
|
145
|
+
return users_processed, total_generated
|
|
146
|
+
finally:
|
|
147
|
+
self._is_batch_mode = False
|
|
148
|
+
|
|
149
|
+
# ===============================
|
|
150
|
+
# Rerun methods (optional - override to enable rerun functionality)
|
|
151
|
+
# ===============================
|
|
152
|
+
|
|
153
|
+
def _get_rerun_user_ids(self, request: TRequest) -> list[str]:
|
|
154
|
+
"""Get user IDs to process during rerun.
|
|
155
|
+
|
|
156
|
+
Override this method to enable rerun functionality for the service.
|
|
157
|
+
Returns a list of user IDs that have interactions matching the request filters.
|
|
158
|
+
Each extractor collects its own data using its configured window_size.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
request: The rerun request object
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of user IDs to process
|
|
165
|
+
"""
|
|
166
|
+
raise NotImplementedError("Rerun not supported by this service")
|
|
167
|
+
|
|
168
|
+
def _build_rerun_request_params(self, request: TRequest) -> dict:
|
|
169
|
+
"""Build request params dict for operation state tracking.
|
|
170
|
+
|
|
171
|
+
Override this method to enable rerun functionality for the service.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
request: The rerun request object
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Dictionary of request parameters for state tracking
|
|
178
|
+
"""
|
|
179
|
+
raise NotImplementedError("Rerun not supported by this service")
|
|
180
|
+
|
|
181
|
+
def _create_run_request_for_item(self, user_id: str, request: TRequest) -> TRequest:
|
|
182
|
+
"""Create the request object to pass to self.run() for a single user.
|
|
183
|
+
|
|
184
|
+
Override this method to enable rerun functionality for the service.
|
|
185
|
+
Each extractor collects its own data using its configured window_size.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
user_id: The user ID to process
|
|
189
|
+
request: The original rerun request object
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
A request object suitable for self.run()
|
|
193
|
+
"""
|
|
194
|
+
raise NotImplementedError("Rerun not supported by this service")
|
|
195
|
+
|
|
196
|
+
def _create_rerun_response(self, success: bool, msg: str, count: int) -> Any:
|
|
197
|
+
"""Create the rerun response object.
|
|
198
|
+
|
|
199
|
+
Override this method to enable rerun functionality for the service.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
success: Whether the operation succeeded
|
|
203
|
+
msg: Status message
|
|
204
|
+
count: Number of items generated
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
A response object (e.g., RerunProfileGenerationResponse)
|
|
208
|
+
"""
|
|
209
|
+
raise NotImplementedError("Rerun not supported by this service")
|
|
210
|
+
|
|
211
|
+
def _get_generated_count(
|
|
212
|
+
self,
|
|
213
|
+
request: TRequest,
|
|
214
|
+
processed_user_ids: list[str] | None = None,
|
|
215
|
+
) -> int:
|
|
216
|
+
"""Get the count of generated items (profiles or playbooks) after rerun.
|
|
217
|
+
|
|
218
|
+
Override this method to enable rerun functionality for the service.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
request: The rerun request object (for filtering)
|
|
222
|
+
processed_user_ids: List of user IDs that were successfully processed
|
|
223
|
+
in the batch. Provided by _run_batch_with_progress so overrides
|
|
224
|
+
don't need to handle user_id=None from batch requests.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Number of items generated during rerun
|
|
228
|
+
"""
|
|
229
|
+
raise NotImplementedError("Rerun not supported by this service")
|
|
230
|
+
|
|
231
|
+
def _pre_process_rerun(self, request: TRequest) -> None: # noqa: B027
|
|
232
|
+
"""Hook called before processing rerun items.
|
|
233
|
+
|
|
234
|
+
Override in subclasses to perform cleanup or preparation before rerun.
|
|
235
|
+
Default implementation does nothing.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
request: The rerun request object
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def run_rerun(self, request: TRequest) -> Any:
|
|
242
|
+
"""Run the rerun workflow for the service.
|
|
243
|
+
|
|
244
|
+
This template method orchestrates the rerun process:
|
|
245
|
+
1. Check for existing in-progress operations
|
|
246
|
+
2. Get user IDs to process
|
|
247
|
+
3. Pre-process hook
|
|
248
|
+
4. Run batch with progress tracking
|
|
249
|
+
5. Return response
|
|
250
|
+
|
|
251
|
+
Child classes must implement the hook methods to enable rerun functionality:
|
|
252
|
+
- _get_rerun_user_ids()
|
|
253
|
+
- _build_rerun_request_params()
|
|
254
|
+
- _create_run_request_for_item()
|
|
255
|
+
- _create_rerun_response()
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
request: The rerun request object
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
A response object with success status, message, and count
|
|
262
|
+
"""
|
|
263
|
+
state_manager = self._create_state_manager()
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# 1. Check for existing in-progress operation
|
|
267
|
+
error = state_manager.check_in_progress()
|
|
268
|
+
if error:
|
|
269
|
+
return self._create_rerun_response(False, error, 0)
|
|
270
|
+
|
|
271
|
+
# 2. Get user IDs to process
|
|
272
|
+
user_ids = self._get_rerun_user_ids(request)
|
|
273
|
+
if not user_ids:
|
|
274
|
+
return self._create_rerun_response(
|
|
275
|
+
False, "No interactions found matching the specified filters", 0
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# 3. Pre-process hook (e.g., delete existing pending items)
|
|
279
|
+
self._pre_process_rerun(request)
|
|
280
|
+
|
|
281
|
+
# 4. Run batch with progress tracking
|
|
282
|
+
users_processed, total_generated = self._run_batch_with_progress(
|
|
283
|
+
user_ids=user_ids,
|
|
284
|
+
request=request,
|
|
285
|
+
request_params=self._build_rerun_request_params(request),
|
|
286
|
+
state_manager=state_manager,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
msg = f"Completed for {users_processed} user(s)"
|
|
290
|
+
return self._create_rerun_response(True, msg, total_generated)
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
state_manager.mark_progress_failed(str(e))
|
|
294
|
+
return self._create_rerun_response(
|
|
295
|
+
False,
|
|
296
|
+
f"Failed to run {self._get_base_service_name()}: {str(e)}",
|
|
297
|
+
0,
|
|
298
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Config-filtering helpers for ``BaseGenerationService`` (Tier-1b decomposition).
|
|
2
|
+
|
|
3
|
+
``ConfigFilterMixin`` holds the concrete, non-abstract config-filter helpers:
|
|
4
|
+
``_filter_extractor_config_by_service_config`` (source/manual-trigger filtering),
|
|
5
|
+
``_get_extractor_state_service_name`` (stride bookmark service name, overridable),
|
|
6
|
+
and ``_filter_config_by_stride`` (pre-LLM stride_size gate). The abstract config
|
|
7
|
+
LOADERS (``_load_extractor_config``, ``_load_generation_service_config``,
|
|
8
|
+
``_create_extractor``) stay physically on the base ABC — an ``@abstractmethod`` on a
|
|
9
|
+
plain mixin is dropped from ``__abstractmethods__``, so this mixin carries only the
|
|
10
|
+
concrete helpers. Method bodies are moved verbatim from the former monolithic
|
|
11
|
+
``base_generation_service.py``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Generic, TypeVar
|
|
16
|
+
|
|
17
|
+
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
18
|
+
from reflexio.server.services.extractor_config_utils import (
|
|
19
|
+
filter_extractor_configs,
|
|
20
|
+
get_extractor_name,
|
|
21
|
+
)
|
|
22
|
+
from reflexio.server.services.extractor_interaction_utils import (
|
|
23
|
+
get_effective_source_filter,
|
|
24
|
+
get_extractor_window_params,
|
|
25
|
+
should_extractor_run_by_stride,
|
|
26
|
+
)
|
|
27
|
+
from reflexio.server.services.operation_state_utils import OperationStateManager
|
|
28
|
+
from reflexio.server.services.storage.storage_base import BaseStorage
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
TExtractorConfig = TypeVar("TExtractorConfig")
|
|
33
|
+
TGenerationServiceConfig = TypeVar("TGenerationServiceConfig")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConfigFilterMixin(Generic[TExtractorConfig, TGenerationServiceConfig]): # noqa: UP046
|
|
37
|
+
"""Concrete config-filter helpers mixed into ``BaseGenerationService``.
|
|
38
|
+
|
|
39
|
+
Mixed into the base ahead of ``ABC``; the per-run ``self`` attributes these
|
|
40
|
+
helpers read (``service_config``, ``storage``, ``org_id``, ``request_context``)
|
|
41
|
+
are initialised on the base ``__init__`` — the annotation-only stubs below give
|
|
42
|
+
pyright the types without introducing shared class-level mutable state.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# Annotation-only stubs for base-owned attributes these helpers read (init'd in
|
|
46
|
+
# the base ``__init__``). NEVER assign here — a class-level default would be a
|
|
47
|
+
# shared-state footgun on a mixin.
|
|
48
|
+
service_config: TGenerationServiceConfig | None
|
|
49
|
+
storage: BaseStorage | None
|
|
50
|
+
org_id: str
|
|
51
|
+
request_context: RequestContext
|
|
52
|
+
|
|
53
|
+
def _filter_extractor_config_by_service_config(
|
|
54
|
+
self,
|
|
55
|
+
extractor_config: TExtractorConfig,
|
|
56
|
+
service_config: TGenerationServiceConfig,
|
|
57
|
+
) -> TExtractorConfig | None:
|
|
58
|
+
"""
|
|
59
|
+
Filter the extractor config based on request_sources_enabled and manual_trigger.
|
|
60
|
+
"""
|
|
61
|
+
filtered = filter_extractor_configs(
|
|
62
|
+
extractor_configs=[extractor_config],
|
|
63
|
+
source=getattr(service_config, "source", None),
|
|
64
|
+
allow_manual_trigger=getattr(service_config, "allow_manual_trigger", False),
|
|
65
|
+
)
|
|
66
|
+
return filtered[0] if filtered else None
|
|
67
|
+
|
|
68
|
+
def _get_extractor_state_service_name(self) -> str | None:
|
|
69
|
+
"""
|
|
70
|
+
Get the service name used for extractor state (stride_size bookmark) lookups.
|
|
71
|
+
|
|
72
|
+
Override in subclasses that support stride_size-based pre-filtering to return
|
|
73
|
+
the OperationStateManager service name (e.g., "profile_extractor", "playbook_extractor").
|
|
74
|
+
Returns None by default, meaning stride_size pre-filtering is skipped.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Optional[str]: Service name for OperationStateManager, or None to skip stride_size pre-filtering
|
|
78
|
+
"""
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def _filter_config_by_stride(
|
|
82
|
+
self, extractor_config: TExtractorConfig
|
|
83
|
+
) -> TExtractorConfig | None:
|
|
84
|
+
"""
|
|
85
|
+
Filter extractor config by stride_size check before the should_run LLM call.
|
|
86
|
+
|
|
87
|
+
Skips filtering when:
|
|
88
|
+
- _get_extractor_state_service_name() returns None (service doesn't support stride_size)
|
|
89
|
+
- auto_run is False (rerun/manual flows skip stride_size)
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
extractor_config: Extractor config after source/manual_trigger filtering
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Extractor config when it passes the stride_size check, otherwise None.
|
|
96
|
+
"""
|
|
97
|
+
state_service_name = self._get_extractor_state_service_name()
|
|
98
|
+
if state_service_name is None:
|
|
99
|
+
return extractor_config
|
|
100
|
+
|
|
101
|
+
if not getattr(self.service_config, "auto_run", True):
|
|
102
|
+
return extractor_config
|
|
103
|
+
|
|
104
|
+
if getattr(self.service_config, "force_extraction", False):
|
|
105
|
+
return extractor_config
|
|
106
|
+
|
|
107
|
+
root_config = self.request_context.configurator.get_config()
|
|
108
|
+
global_window_size = (
|
|
109
|
+
getattr(root_config, "window_size", None) if root_config else None
|
|
110
|
+
)
|
|
111
|
+
global_stride_size = (
|
|
112
|
+
getattr(root_config, "stride_size", None) if root_config else None
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
state_manager = OperationStateManager(
|
|
116
|
+
self.storage, # type: ignore[reportArgumentType]
|
|
117
|
+
self.org_id,
|
|
118
|
+
state_service_name, # type: ignore[reportArgumentType]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
name = get_extractor_name(extractor_config)
|
|
122
|
+
_, stride_size = get_extractor_window_params(
|
|
123
|
+
extractor_config, global_window_size, global_stride_size
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Resolve effective source filter for this extractor
|
|
127
|
+
should_skip, effective_source = get_effective_source_filter(
|
|
128
|
+
extractor_config, getattr(self.service_config, "source", None)
|
|
129
|
+
)
|
|
130
|
+
if should_skip:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
(
|
|
134
|
+
_,
|
|
135
|
+
new_interactions,
|
|
136
|
+
) = state_manager.get_extractor_state_with_new_interactions(
|
|
137
|
+
extractor_name=name,
|
|
138
|
+
user_id=getattr(self.service_config, "user_id", None),
|
|
139
|
+
sources=effective_source,
|
|
140
|
+
)
|
|
141
|
+
new_count = sum(len(ri.interactions) for ri in new_interactions)
|
|
142
|
+
|
|
143
|
+
if should_extractor_run_by_stride(new_count, stride_size):
|
|
144
|
+
return extractor_config
|
|
145
|
+
|
|
146
|
+
logger.info(
|
|
147
|
+
"Stride pre-filter: skipping extractor '%s' (new=%d, stride_size=%s)",
|
|
148
|
+
name,
|
|
149
|
+
new_count,
|
|
150
|
+
stride_size,
|
|
151
|
+
)
|
|
152
|
+
return None
|
package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Extraction-run lifecycle helpers for ``BaseGenerationService`` (Tier-1b decomposition).
|
|
2
|
+
|
|
3
|
+
``ExtractionRunLifecycleMixin`` holds the four concrete methods that run the
|
|
4
|
+
configured extractor and drive the agent-run rows through their terminal states:
|
|
5
|
+
``_execute_extractor`` (thread-pool run + timeout guard), ``_fail_active_extraction_runs``
|
|
6
|
+
(timeout cleanup), ``_finalize_extraction_runs`` (success finalization), and
|
|
7
|
+
``_mark_extraction_runs_finalization_failed`` (failure/backoff).
|
|
8
|
+
|
|
9
|
+
``_execute_extractor`` writes the per-run billing accumulators — ``_last_extractor_run_stats``,
|
|
10
|
+
``_last_extraction_run_ids`` (appended on an ``ExtractionOutcome`` run id), and
|
|
11
|
+
``_last_token_totals`` (set ONLY under ``isinstance(result, ExtractionOutcome)``). Those
|
|
12
|
+
writes and their guards are moved VERBATIM: the ``isinstance`` write-guard is the double-bill
|
|
13
|
+
idempotency backstop (a non-``ExtractionOutcome`` result must NOT overwrite the accumulator),
|
|
14
|
+
and ``_finalize_extraction_runs`` / ``_mark_extraction_runs_finalization_failed`` are invoked
|
|
15
|
+
from ``_run_generation`` (still on the base) via ``self.`` MRO — the atomicity invariant
|
|
16
|
+
(``_mark_..._failed`` on a ``_process_results`` failure, not finalize) depends on both bodies
|
|
17
|
+
being unchanged.
|
|
18
|
+
|
|
19
|
+
``ExtractorExecutionError`` and ``EXTRACTOR_TIMEOUT_SECONDS`` live in
|
|
20
|
+
``base_generation_service`` and are imported function-locally to avoid a circular import
|
|
21
|
+
(the base module imports this package at load time). Method bodies are otherwise moved
|
|
22
|
+
verbatim from the former monolithic ``base_generation_service.py``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import contextvars
|
|
26
|
+
import logging
|
|
27
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
28
|
+
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
|
29
|
+
from datetime import UTC, datetime, timedelta
|
|
30
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
31
|
+
|
|
32
|
+
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
33
|
+
from reflexio.server.llm.token_accounting import RunTokenTotals
|
|
34
|
+
from reflexio.server.services.extraction.outcome import ExtractionOutcome
|
|
35
|
+
from reflexio.server.services.extractor_config_utils import get_extractor_name
|
|
36
|
+
from reflexio.server.services.storage.storage_base import AgentRunStatus, BaseStorage
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
TExtractorConfig = TypeVar("TExtractorConfig")
|
|
41
|
+
TGenerationServiceConfig = TypeVar("TGenerationServiceConfig")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ExtractionRunLifecycleMixin(Generic[TExtractorConfig, TGenerationServiceConfig]): # noqa: UP046
|
|
45
|
+
"""Extractor execution + agent-run terminal-state transitions.
|
|
46
|
+
|
|
47
|
+
Mixed into ``BaseGenerationService`` ahead of the other mixins and ``ABC``; the
|
|
48
|
+
per-run ``self`` attributes these methods read/write (``service_config``,
|
|
49
|
+
``storage``, ``request_context`` and the billing accumulators
|
|
50
|
+
``_last_extractor_run_stats`` / ``_last_extraction_run_ids`` / ``_last_token_totals``)
|
|
51
|
+
are initialised on the base ``__init__`` — the annotation-only stubs below give
|
|
52
|
+
pyright the types without introducing shared class-level mutable state.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# Annotation-only stubs for base-owned attributes these helpers read/write (init'd
|
|
56
|
+
# in the base ``__init__``). NEVER assign here — a class-level default would be a
|
|
57
|
+
# shared-state footgun on a mixin.
|
|
58
|
+
service_config: TGenerationServiceConfig | None
|
|
59
|
+
storage: BaseStorage | None
|
|
60
|
+
request_context: RequestContext
|
|
61
|
+
_last_extractor_run_stats: dict[str, int]
|
|
62
|
+
_last_extraction_run_ids: list[str]
|
|
63
|
+
_last_token_totals: RunTokenTotals | None
|
|
64
|
+
|
|
65
|
+
if TYPE_CHECKING:
|
|
66
|
+
# Abstract on the base ABC (stays there per SINK-2); declared here type-only so
|
|
67
|
+
# pyright can resolve the ``self._create_extractor()`` / ``self._get_service_name()``
|
|
68
|
+
# calls. No runtime attribute is added, so ``__abstractmethods__`` is unaffected.
|
|
69
|
+
def _create_extractor(
|
|
70
|
+
self,
|
|
71
|
+
extractor_config: TExtractorConfig,
|
|
72
|
+
service_config: TGenerationServiceConfig,
|
|
73
|
+
) -> Any: ...
|
|
74
|
+
def _get_service_name(self) -> str: ...
|
|
75
|
+
|
|
76
|
+
def _execute_extractor(
|
|
77
|
+
self,
|
|
78
|
+
extractor_config: TExtractorConfig,
|
|
79
|
+
identifier: str,
|
|
80
|
+
) -> Any | None:
|
|
81
|
+
"""
|
|
82
|
+
Run the configured extractor with timeout and error handling.
|
|
83
|
+
|
|
84
|
+
The extractor runs in a thread pool with a timeout guard so providers that
|
|
85
|
+
ignore their own timeout cannot block generation forever.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
extractor_config: Filtered extractor config to execute
|
|
89
|
+
identifier: Logging context identifier (user_id or request_id)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Extractor result, or None if the extractor succeeded with no output.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ExtractorExecutionError: If the extractor fails with an exception or timeout.
|
|
96
|
+
"""
|
|
97
|
+
from reflexio.server.services.base_generation_service import (
|
|
98
|
+
EXTRACTOR_TIMEOUT_SECONDS,
|
|
99
|
+
ExtractorExecutionError,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
self.service_config is None
|
|
104
|
+
): # pragma: no cover — set by _prepare_generation_run
|
|
105
|
+
raise RuntimeError("service_config must be set before executing extractor")
|
|
106
|
+
|
|
107
|
+
self._last_extractor_run_stats = {"total": 1, "failed": 0, "timed_out": 0}
|
|
108
|
+
extractor = self._create_extractor(extractor_config, self.service_config)
|
|
109
|
+
executor: ThreadPoolExecutor | None = None
|
|
110
|
+
try:
|
|
111
|
+
executor = ThreadPoolExecutor(max_workers=1)
|
|
112
|
+
# Copy context so correlation ID propagates to worker thread
|
|
113
|
+
ctx = contextvars.copy_context()
|
|
114
|
+
future = executor.submit(ctx.run, extractor.run) # type: ignore[reportAttributeAccessIssue]
|
|
115
|
+
result = future.result(timeout=EXTRACTOR_TIMEOUT_SECONDS)
|
|
116
|
+
if isinstance(result, ExtractionOutcome):
|
|
117
|
+
if result.run_id:
|
|
118
|
+
self._last_extraction_run_ids.append(result.run_id)
|
|
119
|
+
self._last_token_totals = result.token_totals
|
|
120
|
+
if result.status == "completed" and result.items:
|
|
121
|
+
return result.items
|
|
122
|
+
logger.info(
|
|
123
|
+
"No results generated for %s identifier: %s",
|
|
124
|
+
self._get_service_name(),
|
|
125
|
+
identifier,
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
if result:
|
|
129
|
+
return result
|
|
130
|
+
logger.info(
|
|
131
|
+
"No results generated for %s identifier: %s",
|
|
132
|
+
self._get_service_name(),
|
|
133
|
+
identifier,
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
except FuturesTimeoutError as exc:
|
|
137
|
+
self._last_extractor_run_stats = {"total": 1, "failed": 1, "timed_out": 1}
|
|
138
|
+
error_msg = (
|
|
139
|
+
f"Extractor timed out after {EXTRACTOR_TIMEOUT_SECONDS} seconds "
|
|
140
|
+
f"for {self._get_service_name()} identifier={identifier}"
|
|
141
|
+
)
|
|
142
|
+
logger.error(error_msg)
|
|
143
|
+
self._fail_active_extraction_runs(
|
|
144
|
+
extractor_kind=get_extractor_name(extractor_config),
|
|
145
|
+
last_error=error_msg,
|
|
146
|
+
)
|
|
147
|
+
raise ExtractorExecutionError(error_msg) from exc
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
self._last_extractor_run_stats = {"total": 1, "failed": 1, "timed_out": 0}
|
|
150
|
+
error_msg = (
|
|
151
|
+
f"Extractor failed for {self._get_service_name()} "
|
|
152
|
+
f"identifier={identifier}: {exc} (type={type(exc).__name__})"
|
|
153
|
+
)
|
|
154
|
+
logger.error(error_msg)
|
|
155
|
+
raise ExtractorExecutionError(error_msg) from exc
|
|
156
|
+
finally:
|
|
157
|
+
if executor is not None:
|
|
158
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
159
|
+
|
|
160
|
+
def _fail_active_extraction_runs(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
extractor_kind: str,
|
|
164
|
+
last_error: str,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Mark active agent-run rows failed when the service timeout fires."""
|
|
167
|
+
if self.storage is None or self.service_config is None:
|
|
168
|
+
return
|
|
169
|
+
request_id = getattr(self.service_config, "request_id", None)
|
|
170
|
+
if not request_id:
|
|
171
|
+
return
|
|
172
|
+
user_id = getattr(self.service_config, "user_id", None)
|
|
173
|
+
try:
|
|
174
|
+
failed_count = self.storage.fail_running_agent_runs_for_request(
|
|
175
|
+
org_id=self.request_context.org_id,
|
|
176
|
+
extractor_kind=extractor_kind,
|
|
177
|
+
user_id=user_id,
|
|
178
|
+
request_id=request_id,
|
|
179
|
+
last_error=last_error,
|
|
180
|
+
)
|
|
181
|
+
except NotImplementedError:
|
|
182
|
+
return
|
|
183
|
+
except Exception as exc: # noqa: BLE001 - keep timeout error primary
|
|
184
|
+
logger.warning(
|
|
185
|
+
"Failed to mark timed-out %s agent runs failed: %s",
|
|
186
|
+
extractor_kind,
|
|
187
|
+
exc,
|
|
188
|
+
)
|
|
189
|
+
return
|
|
190
|
+
if failed_count:
|
|
191
|
+
logger.warning(
|
|
192
|
+
"Marked %d timed-out %s agent run(s) failed for request_id=%s",
|
|
193
|
+
failed_count,
|
|
194
|
+
extractor_kind,
|
|
195
|
+
request_id,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def _finalize_extraction_runs(self) -> None:
|
|
199
|
+
if self.storage is None:
|
|
200
|
+
return
|
|
201
|
+
for run_id in self._last_extraction_run_ids:
|
|
202
|
+
run = self.storage.get_agent_run(run_id)
|
|
203
|
+
if run is None:
|
|
204
|
+
continue
|
|
205
|
+
status = (
|
|
206
|
+
AgentRunStatus.FINALIZED_PENDING_TOOL
|
|
207
|
+
if run.pending_tool_call_ids
|
|
208
|
+
else AgentRunStatus.FINALIZED
|
|
209
|
+
)
|
|
210
|
+
self.storage.update_agent_run_status(
|
|
211
|
+
run_id,
|
|
212
|
+
status,
|
|
213
|
+
pending_tool_call_ids=run.pending_tool_call_ids,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _mark_extraction_runs_finalization_failed(self, exc: Exception) -> None:
|
|
217
|
+
if self.storage is None:
|
|
218
|
+
return
|
|
219
|
+
root_config = self.request_context.configurator.get_config()
|
|
220
|
+
pending_config = getattr(root_config, "pending_tool_call_config", None)
|
|
221
|
+
for run_id in self._last_extraction_run_ids:
|
|
222
|
+
run = self.storage.get_agent_run(run_id)
|
|
223
|
+
if run is None or run.committed_output is None:
|
|
224
|
+
continue
|
|
225
|
+
next_attempt_count = run.finalization_attempts + 1
|
|
226
|
+
max_attempts = (
|
|
227
|
+
pending_config.max_finalization_attempts
|
|
228
|
+
if pending_config is not None
|
|
229
|
+
else 3
|
|
230
|
+
)
|
|
231
|
+
status = (
|
|
232
|
+
AgentRunStatus.FAILED
|
|
233
|
+
if next_attempt_count >= max_attempts
|
|
234
|
+
else AgentRunStatus.FINALIZATION_FAILED
|
|
235
|
+
)
|
|
236
|
+
delay_seconds = min(300, max(1, 2 ** max(0, next_attempt_count - 1)))
|
|
237
|
+
self.storage.update_agent_run_status(
|
|
238
|
+
run_id,
|
|
239
|
+
status,
|
|
240
|
+
next_resume_at=datetime.now(UTC) + timedelta(seconds=delay_seconds),
|
|
241
|
+
last_error=str(exc),
|
|
242
|
+
increment_finalization_attempts=True,
|
|
243
|
+
)
|