tactus 0.34.1__py3-none-any.whl → 0.35.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.
- tactus/__init__.py +1 -1
- tactus/adapters/broker_log.py +17 -14
- tactus/adapters/channels/__init__.py +17 -15
- tactus/adapters/channels/base.py +16 -7
- tactus/adapters/channels/broker.py +43 -13
- tactus/adapters/channels/cli.py +19 -15
- tactus/adapters/channels/host.py +15 -6
- tactus/adapters/channels/ipc.py +82 -31
- tactus/adapters/channels/sse.py +41 -23
- tactus/adapters/cli_hitl.py +19 -19
- tactus/adapters/cli_log.py +4 -4
- tactus/adapters/control_loop.py +138 -99
- tactus/adapters/cost_collector_log.py +9 -9
- tactus/adapters/file_storage.py +56 -52
- tactus/adapters/http_callback_log.py +23 -13
- tactus/adapters/ide_log.py +17 -9
- tactus/adapters/lua_tools.py +4 -5
- tactus/adapters/mcp.py +16 -19
- tactus/adapters/mcp_manager.py +46 -30
- tactus/adapters/memory.py +9 -9
- tactus/adapters/plugins.py +42 -42
- tactus/broker/client.py +75 -78
- tactus/broker/protocol.py +57 -57
- tactus/broker/server.py +252 -197
- tactus/cli/app.py +3 -1
- tactus/cli/control.py +2 -2
- tactus/core/config_manager.py +181 -135
- tactus/core/dependencies/registry.py +66 -48
- tactus/core/dsl_stubs.py +222 -163
- tactus/core/exceptions.py +10 -1
- tactus/core/execution_context.py +152 -112
- tactus/core/lua_sandbox.py +72 -64
- tactus/core/message_history_manager.py +138 -43
- tactus/core/mocking.py +41 -27
- tactus/core/output_validator.py +49 -44
- tactus/core/registry.py +94 -80
- tactus/core/runtime.py +211 -176
- tactus/core/template_resolver.py +16 -16
- tactus/core/yaml_parser.py +55 -45
- tactus/docs/extractor.py +7 -6
- tactus/ide/server.py +119 -78
- tactus/primitives/control.py +10 -6
- tactus/primitives/file.py +48 -46
- tactus/primitives/handles.py +47 -35
- tactus/primitives/host.py +29 -27
- tactus/primitives/human.py +154 -137
- tactus/primitives/json.py +22 -23
- tactus/primitives/log.py +26 -26
- tactus/primitives/message_history.py +285 -31
- tactus/primitives/model.py +15 -9
- tactus/primitives/procedure.py +86 -64
- tactus/primitives/procedure_callable.py +58 -51
- tactus/primitives/retry.py +31 -29
- tactus/primitives/session.py +42 -29
- tactus/primitives/state.py +54 -43
- tactus/primitives/step.py +9 -13
- tactus/primitives/system.py +34 -21
- tactus/primitives/tool.py +44 -31
- tactus/primitives/tool_handle.py +76 -54
- tactus/primitives/toolset.py +25 -22
- tactus/sandbox/config.py +4 -4
- tactus/sandbox/container_runner.py +161 -107
- tactus/sandbox/docker_manager.py +20 -20
- tactus/sandbox/entrypoint.py +16 -14
- tactus/sandbox/protocol.py +15 -15
- tactus/stdlib/classify/llm.py +1 -3
- tactus/stdlib/core/validation.py +0 -3
- tactus/testing/pydantic_eval_runner.py +1 -1
- tactus/utils/asyncio_helpers.py +27 -0
- tactus/utils/cost_calculator.py +7 -7
- tactus/utils/model_pricing.py +11 -12
- tactus/utils/safe_file_library.py +156 -132
- tactus/utils/safe_libraries.py +27 -27
- tactus/validation/error_listener.py +18 -5
- tactus/validation/semantic_visitor.py +392 -333
- tactus/validation/validator.py +89 -49
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
tactus/core/exceptions.py
CHANGED
|
@@ -22,11 +22,20 @@ class ProcedureWaitingForHuman(Exception):
|
|
|
22
22
|
4. Wait for resume trigger
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
+
message_template = (
|
|
26
|
+
"Procedure {procedure_id} waiting for human response to message {pending_message_id}"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
procedure_id: str
|
|
30
|
+
pending_message_id: str
|
|
31
|
+
|
|
25
32
|
def __init__(self, procedure_id: str, pending_message_id: str):
|
|
26
33
|
self.procedure_id = procedure_id
|
|
27
34
|
self.pending_message_id = pending_message_id
|
|
28
35
|
super().__init__(
|
|
29
|
-
|
|
36
|
+
self.message_template.format(
|
|
37
|
+
procedure_id=procedure_id, pending_message_id=pending_message_id
|
|
38
|
+
)
|
|
30
39
|
)
|
|
31
40
|
|
|
32
41
|
|
tactus/core/execution_context.py
CHANGED
|
@@ -6,7 +6,7 @@ Uses pluggable storage and HITL handlers via protocols.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
|
-
from typing import Any,
|
|
9
|
+
from typing import Any, Callable
|
|
10
10
|
from datetime import datetime, timezone
|
|
11
11
|
import logging
|
|
12
12
|
import time
|
|
@@ -39,7 +39,7 @@ class ExecutionContext(ABC):
|
|
|
39
39
|
self,
|
|
40
40
|
fn: Callable[[], Any],
|
|
41
41
|
checkpoint_type: str,
|
|
42
|
-
source_info:
|
|
42
|
+
source_info: dict[str, Any] | None = None,
|
|
43
43
|
) -> Any:
|
|
44
44
|
"""
|
|
45
45
|
Execute fn with position-based checkpointing. On replay, return stored result.
|
|
@@ -59,9 +59,9 @@ class ExecutionContext(ABC):
|
|
|
59
59
|
self,
|
|
60
60
|
request_type: str,
|
|
61
61
|
message: str,
|
|
62
|
-
timeout_seconds:
|
|
62
|
+
timeout_seconds: int | None,
|
|
63
63
|
default_value: Any,
|
|
64
|
-
options:
|
|
64
|
+
options: list[dict] | None,
|
|
65
65
|
metadata: dict,
|
|
66
66
|
) -> HITLResponse:
|
|
67
67
|
"""
|
|
@@ -121,7 +121,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
121
121
|
self,
|
|
122
122
|
procedure_id: str,
|
|
123
123
|
storage_backend: StorageBackend,
|
|
124
|
-
hitl_handler:
|
|
124
|
+
hitl_handler: HITLHandler | None = None,
|
|
125
125
|
strict_determinism: bool = False,
|
|
126
126
|
log_handler=None,
|
|
127
127
|
):
|
|
@@ -145,14 +145,14 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
145
145
|
self._inside_checkpoint = False
|
|
146
146
|
|
|
147
147
|
# Run ID tracking for distinguishing between different executions
|
|
148
|
-
self.current_run_id:
|
|
148
|
+
self.current_run_id: str | None = None
|
|
149
149
|
|
|
150
150
|
# .tac file tracking for accurate source locations
|
|
151
|
-
self.current_tac_file:
|
|
152
|
-
self.current_tac_content:
|
|
151
|
+
self.current_tac_file: str | None = None
|
|
152
|
+
self.current_tac_content: str | None = None
|
|
153
153
|
|
|
154
154
|
# Lua sandbox reference for debug.getinfo access
|
|
155
|
-
self.lua_sandbox:
|
|
155
|
+
self.lua_sandbox: Any | None = None
|
|
156
156
|
|
|
157
157
|
# Rich metadata for HITL notifications
|
|
158
158
|
self.procedure_name: str = procedure_id # Use procedure_id as default name
|
|
@@ -172,7 +172,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
172
172
|
"""Set the run_id for subsequent checkpoints in this execution."""
|
|
173
173
|
self.current_run_id = run_id
|
|
174
174
|
|
|
175
|
-
def set_tac_file(self, file_path: str, content:
|
|
175
|
+
def set_tac_file(self, file_path: str, content: str | None = None) -> None:
|
|
176
176
|
"""
|
|
177
177
|
Store the currently executing .tac file for accurate source location capture.
|
|
178
178
|
|
|
@@ -188,7 +188,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
188
188
|
self.lua_sandbox = lua_sandbox
|
|
189
189
|
|
|
190
190
|
def set_procedure_metadata(
|
|
191
|
-
self, procedure_name:
|
|
191
|
+
self, procedure_name: str | None = None, input_data: Any = None
|
|
192
192
|
) -> None:
|
|
193
193
|
"""
|
|
194
194
|
Set rich metadata for HITL notifications.
|
|
@@ -206,7 +206,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
206
206
|
self,
|
|
207
207
|
fn: Callable[[], Any],
|
|
208
208
|
checkpoint_type: str,
|
|
209
|
-
source_info:
|
|
209
|
+
source_info: dict[str, Any] | None = None,
|
|
210
210
|
) -> Any:
|
|
211
211
|
"""
|
|
212
212
|
Execute fn with position-based checkpointing and source tracking.
|
|
@@ -215,52 +215,67 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
215
215
|
On first execution, runs fn(), records in log, and returns result.
|
|
216
216
|
"""
|
|
217
217
|
logger.info(
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
"[CHECKPOINT] checkpoint() called, type=%s, position=%s, current_run_id=%s, "
|
|
219
|
+
"has_log_handler=%s",
|
|
220
|
+
checkpoint_type,
|
|
221
|
+
self.metadata.replay_index,
|
|
222
|
+
self.current_run_id,
|
|
223
|
+
self.log_handler is not None,
|
|
220
224
|
)
|
|
221
|
-
|
|
225
|
+
checkpoint_position = self.metadata.replay_index
|
|
222
226
|
|
|
223
227
|
# Check if we're in replay mode (checkpoint exists at this position)
|
|
224
|
-
if
|
|
225
|
-
|
|
228
|
+
if checkpoint_position < len(self.metadata.execution_log):
|
|
229
|
+
checkpoint_entry = self.metadata.execution_log[checkpoint_position]
|
|
226
230
|
logger.info(
|
|
227
|
-
|
|
228
|
-
|
|
231
|
+
"[CHECKPOINT] Found existing checkpoint at position %s: type=%s, run_id=%s, "
|
|
232
|
+
"result_type=%s",
|
|
233
|
+
checkpoint_position,
|
|
234
|
+
checkpoint_entry.type,
|
|
235
|
+
checkpoint_entry.run_id,
|
|
236
|
+
type(checkpoint_entry.result).__name__,
|
|
229
237
|
)
|
|
230
238
|
|
|
231
239
|
# CRITICAL: Only replay checkpoints from the CURRENT run
|
|
232
240
|
# Each new run should execute fresh, not use cached results from previous runs
|
|
233
|
-
if
|
|
241
|
+
if checkpoint_entry.run_id != self.current_run_id:
|
|
234
242
|
logger.info(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
243
|
+
"[CHECKPOINT] Checkpoint is from DIFFERENT run (checkpoint run_id=%s, "
|
|
244
|
+
"current run_id=%s), executing fresh (NOT replaying)",
|
|
245
|
+
checkpoint_entry.run_id,
|
|
246
|
+
self.current_run_id,
|
|
238
247
|
)
|
|
239
248
|
# Fall through to execute mode - this is a new run
|
|
240
249
|
# Special case: HITL checkpoints may have result=None if saved before response arrived
|
|
241
250
|
# In this case, re-execute to check for cached response from control loop
|
|
242
|
-
elif
|
|
251
|
+
elif checkpoint_entry.result is None and checkpoint_type.startswith("hitl_"):
|
|
243
252
|
logger.info(
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
"[CHECKPOINT] HITL checkpoint at position %s has no result, re-executing "
|
|
254
|
+
"to check for cached response",
|
|
255
|
+
checkpoint_position,
|
|
246
256
|
)
|
|
247
257
|
# Fall through to execute mode - will check for cached response
|
|
248
258
|
else:
|
|
249
259
|
# Normal replay: return cached result from CURRENT run
|
|
250
260
|
self.metadata.replay_index += 1
|
|
251
261
|
logger.info(
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
"[CHECKPOINT] REPLAYING checkpoint at position %s, type=%s, run_id=%s, "
|
|
263
|
+
"returning cached result",
|
|
264
|
+
checkpoint_position,
|
|
265
|
+
checkpoint_entry.type,
|
|
266
|
+
checkpoint_entry.run_id,
|
|
254
267
|
)
|
|
255
|
-
return
|
|
268
|
+
return checkpoint_entry.result
|
|
256
269
|
else:
|
|
257
270
|
logger.info(
|
|
258
|
-
|
|
259
|
-
|
|
271
|
+
"[CHECKPOINT] No checkpoint at position %s (only %s checkpoints exist), "
|
|
272
|
+
"executing fresh",
|
|
273
|
+
checkpoint_position,
|
|
274
|
+
len(self.metadata.execution_log),
|
|
260
275
|
)
|
|
261
276
|
|
|
262
277
|
# Execute mode: run function with checkpoint scope tracking
|
|
263
|
-
|
|
278
|
+
previous_checkpoint_state = self._inside_checkpoint
|
|
264
279
|
self._inside_checkpoint = True
|
|
265
280
|
|
|
266
281
|
# Capture source location if provided
|
|
@@ -282,17 +297,17 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
282
297
|
)
|
|
283
298
|
|
|
284
299
|
try:
|
|
285
|
-
|
|
300
|
+
execution_start_time = time.time()
|
|
286
301
|
result = fn()
|
|
287
|
-
|
|
302
|
+
execution_duration_ms = (time.time() - execution_start_time) * 1000
|
|
288
303
|
|
|
289
304
|
# Create checkpoint entry with source location and run_id (if available)
|
|
290
|
-
|
|
291
|
-
position=
|
|
305
|
+
checkpoint_entry = CheckpointEntry(
|
|
306
|
+
position=checkpoint_position,
|
|
292
307
|
type=checkpoint_type,
|
|
293
308
|
result=result,
|
|
294
309
|
timestamp=datetime.now(timezone.utc),
|
|
295
|
-
duration_ms=
|
|
310
|
+
duration_ms=execution_duration_ms,
|
|
296
311
|
run_id=self.current_run_id, # Can be None for backward compatibility
|
|
297
312
|
source_location=source_location,
|
|
298
313
|
captured_vars=(
|
|
@@ -303,13 +318,13 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
303
318
|
# CRITICAL: For HITL checkpoints, we need to save the checkpoint BEFORE exiting
|
|
304
319
|
# This enables transparent resume - on restart, we'll have a checkpoint at this position
|
|
305
320
|
# with result=None, and the control loop will check for cached responses
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
position=
|
|
321
|
+
execution_duration_ms = (time.time() - execution_start_time) * 1000
|
|
322
|
+
checkpoint_entry = CheckpointEntry(
|
|
323
|
+
position=checkpoint_position,
|
|
309
324
|
type=checkpoint_type,
|
|
310
325
|
result=None, # Will be filled in when response arrives
|
|
311
326
|
timestamp=datetime.now(timezone.utc),
|
|
312
|
-
duration_ms=
|
|
327
|
+
duration_ms=execution_duration_ms,
|
|
313
328
|
run_id=self.current_run_id,
|
|
314
329
|
source_location=source_location,
|
|
315
330
|
captured_vars=(
|
|
@@ -317,38 +332,41 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
317
332
|
),
|
|
318
333
|
)
|
|
319
334
|
# Only append if checkpoint doesn't already exist (from previous failed attempt)
|
|
320
|
-
if
|
|
335
|
+
if checkpoint_position < len(self.metadata.execution_log):
|
|
321
336
|
# Checkpoint already exists - update it
|
|
322
337
|
logger.debug(
|
|
323
|
-
|
|
338
|
+
"[CHECKPOINT] Updating existing HITL checkpoint at position %s " "before exit",
|
|
339
|
+
checkpoint_position,
|
|
324
340
|
)
|
|
325
|
-
self.metadata.execution_log[
|
|
341
|
+
self.metadata.execution_log[checkpoint_position] = checkpoint_entry
|
|
326
342
|
else:
|
|
327
343
|
# New checkpoint - append and increment
|
|
328
344
|
logger.debug(
|
|
329
|
-
|
|
345
|
+
"[CHECKPOINT] Creating new HITL checkpoint at position %s before exit",
|
|
346
|
+
checkpoint_position,
|
|
330
347
|
)
|
|
331
|
-
self.metadata.execution_log.append(
|
|
348
|
+
self.metadata.execution_log.append(checkpoint_entry)
|
|
332
349
|
self.metadata.replay_index += 1
|
|
333
350
|
|
|
334
351
|
self.storage.save_procedure_metadata(self.procedure_id, self.metadata)
|
|
335
352
|
# Restore checkpoint flag and re-raise
|
|
336
|
-
self._inside_checkpoint =
|
|
353
|
+
self._inside_checkpoint = previous_checkpoint_state
|
|
337
354
|
raise
|
|
338
355
|
finally:
|
|
339
356
|
# Always restore checkpoint flag, even if fn() raises
|
|
340
|
-
self._inside_checkpoint =
|
|
357
|
+
self._inside_checkpoint = previous_checkpoint_state
|
|
341
358
|
|
|
342
359
|
# Add to execution log (or update if checkpoint already exists from HITL exit)
|
|
343
|
-
if
|
|
360
|
+
if checkpoint_position < len(self.metadata.execution_log):
|
|
344
361
|
# Checkpoint already exists (saved during HITL exit) - update it with the result
|
|
345
362
|
logger.debug(
|
|
346
|
-
|
|
363
|
+
"[CHECKPOINT] Updating existing HITL checkpoint at position %s with result",
|
|
364
|
+
checkpoint_position,
|
|
347
365
|
)
|
|
348
|
-
self.metadata.execution_log[
|
|
366
|
+
self.metadata.execution_log[checkpoint_position] = checkpoint_entry
|
|
349
367
|
else:
|
|
350
368
|
# New checkpoint - append to log
|
|
351
|
-
self.metadata.execution_log.append(
|
|
369
|
+
self.metadata.execution_log.append(checkpoint_entry)
|
|
352
370
|
self.metadata.replay_index += 1
|
|
353
371
|
|
|
354
372
|
# Emit checkpoint created event if we have a log handler
|
|
@@ -357,18 +375,22 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
357
375
|
from tactus.protocols.models import CheckpointCreatedEvent
|
|
358
376
|
|
|
359
377
|
event = CheckpointCreatedEvent(
|
|
360
|
-
checkpoint_position=
|
|
378
|
+
checkpoint_position=checkpoint_position,
|
|
361
379
|
checkpoint_type=checkpoint_type,
|
|
362
|
-
duration_ms=
|
|
380
|
+
duration_ms=execution_duration_ms,
|
|
363
381
|
source_location=source_location,
|
|
364
382
|
procedure_id=self.procedure_id,
|
|
365
383
|
)
|
|
366
384
|
logger.debug(
|
|
367
|
-
|
|
385
|
+
"[CHECKPOINT] Emitting CheckpointCreatedEvent: position=%s, type=%s, "
|
|
386
|
+
"duration_ms=%s",
|
|
387
|
+
checkpoint_position,
|
|
388
|
+
checkpoint_type,
|
|
389
|
+
execution_duration_ms,
|
|
368
390
|
)
|
|
369
391
|
self.log_handler.log(event)
|
|
370
|
-
except Exception as
|
|
371
|
-
logger.warning(
|
|
392
|
+
except Exception as exception:
|
|
393
|
+
logger.warning("Failed to emit checkpoint event: %s", exception)
|
|
372
394
|
else:
|
|
373
395
|
logger.warning("[CHECKPOINT] No log_handler available to emit checkpoint event")
|
|
374
396
|
|
|
@@ -377,14 +399,16 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
377
399
|
|
|
378
400
|
return result
|
|
379
401
|
|
|
380
|
-
def _get_code_context(
|
|
402
|
+
def _get_code_context(
|
|
403
|
+
self, file_path: str, line_number: int, context_lines: int = 3
|
|
404
|
+
) -> str | None:
|
|
381
405
|
"""Read source file and extract surrounding lines for debugging."""
|
|
382
406
|
try:
|
|
383
|
-
with open(file_path, "r") as
|
|
384
|
-
lines =
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return "".join(lines[
|
|
407
|
+
with open(file_path, "r") as source_file:
|
|
408
|
+
lines = source_file.readlines()
|
|
409
|
+
start_index = max(0, line_number - context_lines - 1)
|
|
410
|
+
end_index = min(len(lines), line_number + context_lines)
|
|
411
|
+
return "".join(lines[start_index:end_index])
|
|
388
412
|
except Exception:
|
|
389
413
|
return None
|
|
390
414
|
|
|
@@ -392,9 +416,9 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
392
416
|
self,
|
|
393
417
|
request_type: str,
|
|
394
418
|
message: str,
|
|
395
|
-
timeout_seconds:
|
|
419
|
+
timeout_seconds: int | None,
|
|
396
420
|
default_value: Any,
|
|
397
|
-
options:
|
|
421
|
+
options: list[dict] | None,
|
|
398
422
|
metadata: dict,
|
|
399
423
|
) -> HITLResponse:
|
|
400
424
|
"""
|
|
@@ -402,20 +426,25 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
402
426
|
|
|
403
427
|
Delegates to the HITLHandler protocol implementation.
|
|
404
428
|
"""
|
|
429
|
+
message_preview = message[:50] if message else "None"
|
|
405
430
|
logger.debug(
|
|
406
|
-
|
|
431
|
+
"[HITL] wait_for_human called: type=%s, message=%s, hitl_handler=%s",
|
|
432
|
+
request_type,
|
|
433
|
+
message_preview,
|
|
434
|
+
self.hitl,
|
|
407
435
|
)
|
|
408
436
|
if not self.hitl:
|
|
409
437
|
# No HITL handler - return default immediately
|
|
410
438
|
logger.warning(
|
|
411
|
-
|
|
439
|
+
"[HITL] No HITL handler configured - returning default value: %s",
|
|
440
|
+
default_value,
|
|
412
441
|
)
|
|
413
442
|
return HITLResponse(
|
|
414
443
|
value=default_value, responded_at=datetime.now(timezone.utc), timed_out=True
|
|
415
444
|
)
|
|
416
445
|
|
|
417
446
|
# Create HITL request
|
|
418
|
-
|
|
447
|
+
hitl_request = HITLRequest(
|
|
419
448
|
request_type=request_type,
|
|
420
449
|
message=message,
|
|
421
450
|
timeout_seconds=timeout_seconds,
|
|
@@ -426,7 +455,9 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
426
455
|
|
|
427
456
|
# Delegate to HITL handler (may raise ProcedureWaitingForHuman)
|
|
428
457
|
# Pass self (execution_context) for deterministic request ID generation
|
|
429
|
-
return self.hitl.request_interaction(
|
|
458
|
+
return self.hitl.request_interaction(
|
|
459
|
+
self.procedure_id, hitl_request, execution_context=self
|
|
460
|
+
)
|
|
430
461
|
|
|
431
462
|
def sleep(self, seconds: int) -> None:
|
|
432
463
|
"""
|
|
@@ -465,14 +496,11 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
465
496
|
Args:
|
|
466
497
|
handle: ProcedureHandle instance
|
|
467
498
|
"""
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
self.metadata["async_procedures"] = {}
|
|
471
|
-
|
|
472
|
-
self.metadata["async_procedures"][handle.procedure_id] = handle.to_dict()
|
|
499
|
+
async_procedure_handles = self._get_async_procedures()
|
|
500
|
+
async_procedure_handles[handle.procedure_id] = handle.to_dict()
|
|
473
501
|
self.storage.save_procedure_metadata(self.procedure_id, self.metadata)
|
|
474
502
|
|
|
475
|
-
def get_procedure_handle(self, procedure_id: str) ->
|
|
503
|
+
def get_procedure_handle(self, procedure_id: str) -> dict[str, Any] | None:
|
|
476
504
|
"""
|
|
477
505
|
Retrieve procedure handle.
|
|
478
506
|
|
|
@@ -482,17 +510,16 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
482
510
|
Returns:
|
|
483
511
|
Handle dict or None
|
|
484
512
|
"""
|
|
485
|
-
|
|
486
|
-
return async_procedures.get(procedure_id)
|
|
513
|
+
return self._get_async_procedures().get(procedure_id)
|
|
487
514
|
|
|
488
|
-
def list_pending_procedures(self) ->
|
|
515
|
+
def list_pending_procedures(self) -> list[dict[str, Any]]:
|
|
489
516
|
"""
|
|
490
517
|
List all pending async procedures.
|
|
491
518
|
|
|
492
519
|
Returns:
|
|
493
520
|
List of handle dicts for procedures with status "running" or "waiting"
|
|
494
521
|
"""
|
|
495
|
-
async_procedures = self.
|
|
522
|
+
async_procedures = self._get_async_procedures()
|
|
496
523
|
return [
|
|
497
524
|
handle
|
|
498
525
|
for handle in async_procedures.values()
|
|
@@ -511,11 +538,9 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
511
538
|
result: Optional result value
|
|
512
539
|
error: Optional error message
|
|
513
540
|
"""
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if procedure_id in self.metadata["async_procedures"]:
|
|
518
|
-
handle = self.metadata["async_procedures"][procedure_id]
|
|
541
|
+
async_procedures = self._get_async_procedures()
|
|
542
|
+
if procedure_id in async_procedures:
|
|
543
|
+
handle = async_procedures[procedure_id]
|
|
519
544
|
handle["status"] = status
|
|
520
545
|
if result is not None:
|
|
521
546
|
handle["result"] = result
|
|
@@ -526,6 +551,17 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
526
551
|
|
|
527
552
|
self.storage.save_procedure_metadata(self.procedure_id, self.metadata)
|
|
528
553
|
|
|
554
|
+
def _get_async_procedures(self) -> dict[str, Any]:
|
|
555
|
+
"""Return the async procedures map stored on metadata."""
|
|
556
|
+
if isinstance(self.metadata, dict):
|
|
557
|
+
return self.metadata.setdefault("async_procedures", {})
|
|
558
|
+
store = getattr(self.metadata, "__dict__", None)
|
|
559
|
+
if store is None:
|
|
560
|
+
return {}
|
|
561
|
+
if "async_procedures" not in store:
|
|
562
|
+
store["async_procedures"] = {}
|
|
563
|
+
return store["async_procedures"]
|
|
564
|
+
|
|
529
565
|
def save_execution_run(
|
|
530
566
|
self, procedure_name: str, file_path: str, status: str = "COMPLETED"
|
|
531
567
|
) -> str:
|
|
@@ -568,19 +604,19 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
568
604
|
|
|
569
605
|
return run_id
|
|
570
606
|
|
|
571
|
-
def get_subject(self) ->
|
|
607
|
+
def get_subject(self) -> str | None:
|
|
572
608
|
"""
|
|
573
609
|
Return a human-readable subject line for this execution.
|
|
574
610
|
|
|
575
611
|
Returns:
|
|
576
612
|
Subject line combining procedure name and current checkpoint position
|
|
577
613
|
"""
|
|
578
|
-
|
|
614
|
+
checkpoint_position = self.next_position()
|
|
579
615
|
if self.procedure_name:
|
|
580
|
-
return f"{self.procedure_name} (checkpoint {
|
|
581
|
-
return f"Procedure {self.procedure_id} (checkpoint {
|
|
616
|
+
return f"{self.procedure_name} (checkpoint {checkpoint_position})"
|
|
617
|
+
return f"Procedure {self.procedure_id} (checkpoint {checkpoint_position})"
|
|
582
618
|
|
|
583
|
-
def get_started_at(self) ->
|
|
619
|
+
def get_started_at(self) -> datetime | None:
|
|
584
620
|
"""
|
|
585
621
|
Return when this execution started.
|
|
586
622
|
|
|
@@ -589,7 +625,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
589
625
|
"""
|
|
590
626
|
return self._started_at
|
|
591
627
|
|
|
592
|
-
def get_input_summary(self) ->
|
|
628
|
+
def get_input_summary(self) -> dict[str, Any] | None:
|
|
593
629
|
"""
|
|
594
630
|
Return a summary of the initial input to this procedure.
|
|
595
631
|
|
|
@@ -606,7 +642,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
606
642
|
# Otherwise wrap it in a dict
|
|
607
643
|
return {"value": self._input_data}
|
|
608
644
|
|
|
609
|
-
def get_conversation_history(self) ->
|
|
645
|
+
def get_conversation_history(self) -> list[dict] | None:
|
|
610
646
|
"""
|
|
611
647
|
Return conversation history if available.
|
|
612
648
|
|
|
@@ -617,7 +653,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
617
653
|
# in future implementations
|
|
618
654
|
return None
|
|
619
655
|
|
|
620
|
-
def get_prior_control_interactions(self) ->
|
|
656
|
+
def get_prior_control_interactions(self) -> list[dict] | None:
|
|
621
657
|
"""
|
|
622
658
|
Return list of prior HITL interactions in this execution.
|
|
623
659
|
|
|
@@ -641,7 +677,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
641
677
|
|
|
642
678
|
return hitl_checkpoints if hitl_checkpoints else None
|
|
643
679
|
|
|
644
|
-
def get_lua_source_line(self) ->
|
|
680
|
+
def get_lua_source_line(self) -> int | None:
|
|
645
681
|
"""
|
|
646
682
|
Get the current source line from Lua debug.getinfo.
|
|
647
683
|
|
|
@@ -653,27 +689,31 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
653
689
|
|
|
654
690
|
try:
|
|
655
691
|
# Access Lua debug module to get current line
|
|
656
|
-
|
|
657
|
-
if
|
|
692
|
+
debug_module = self.lua_sandbox.globals().debug
|
|
693
|
+
if debug_module and hasattr(debug_module, "getinfo"):
|
|
658
694
|
# getinfo(2) gets info about the calling function
|
|
659
695
|
# We need to go up the stack to find the user's code
|
|
660
696
|
for level in range(2, 10):
|
|
661
697
|
try:
|
|
662
|
-
|
|
663
|
-
if
|
|
664
|
-
|
|
665
|
-
|
|
698
|
+
debug_info = debug_module.getinfo(level, "Sl")
|
|
699
|
+
if debug_info:
|
|
700
|
+
current_line = debug_info.get("currentline")
|
|
701
|
+
source_name = debug_info.get("source", "")
|
|
666
702
|
# Skip internal sources (start with @)
|
|
667
|
-
if
|
|
668
|
-
|
|
703
|
+
if (
|
|
704
|
+
current_line
|
|
705
|
+
and current_line > 0
|
|
706
|
+
and not source_name.startswith("@")
|
|
707
|
+
):
|
|
708
|
+
return int(current_line)
|
|
669
709
|
except Exception:
|
|
670
710
|
break
|
|
671
|
-
except Exception as
|
|
672
|
-
logger.debug(
|
|
711
|
+
except Exception as exception:
|
|
712
|
+
logger.debug("Could not get Lua source line: %s", exception)
|
|
673
713
|
|
|
674
714
|
return None
|
|
675
715
|
|
|
676
|
-
def get_runtime_context(self) ->
|
|
716
|
+
def get_runtime_context(self) -> dict[str, Any]:
|
|
677
717
|
"""
|
|
678
718
|
Build RuntimeContext dict for HITL requests.
|
|
679
719
|
|
|
@@ -683,9 +723,9 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
683
723
|
Dict with runtime context fields
|
|
684
724
|
"""
|
|
685
725
|
# Calculate elapsed time
|
|
686
|
-
|
|
726
|
+
elapsed_seconds = 0.0
|
|
687
727
|
if self._started_at:
|
|
688
|
-
|
|
728
|
+
elapsed_seconds = (datetime.now(timezone.utc) - self._started_at).total_seconds()
|
|
689
729
|
|
|
690
730
|
# Get current source location
|
|
691
731
|
source_line = self.get_lua_source_line()
|
|
@@ -694,14 +734,14 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
694
734
|
backtrace = []
|
|
695
735
|
if self.metadata and self.metadata.execution_log:
|
|
696
736
|
for entry in self.metadata.execution_log:
|
|
697
|
-
|
|
737
|
+
backtrace_entry = {
|
|
698
738
|
"checkpoint_type": entry.type,
|
|
699
739
|
"duration_ms": entry.duration_ms,
|
|
700
740
|
}
|
|
701
741
|
if entry.source_location:
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
backtrace.append(
|
|
742
|
+
backtrace_entry["line"] = entry.source_location.line
|
|
743
|
+
backtrace_entry["function_name"] = entry.source_location.function
|
|
744
|
+
backtrace.append(backtrace_entry)
|
|
705
745
|
|
|
706
746
|
return {
|
|
707
747
|
"source_line": source_line,
|
|
@@ -710,7 +750,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
710
750
|
"procedure_name": self.procedure_name,
|
|
711
751
|
"invocation_id": self.invocation_id,
|
|
712
752
|
"started_at": self._started_at.isoformat() if self._started_at else None,
|
|
713
|
-
"elapsed_seconds":
|
|
753
|
+
"elapsed_seconds": elapsed_seconds,
|
|
714
754
|
"backtrace": backtrace,
|
|
715
755
|
}
|
|
716
756
|
|
|
@@ -723,7 +763,7 @@ class InMemoryExecutionContext(BaseExecutionContext):
|
|
|
723
763
|
and simple CLI workflows that don't need to survive restarts.
|
|
724
764
|
"""
|
|
725
765
|
|
|
726
|
-
def __init__(self, procedure_id: str, hitl_handler:
|
|
766
|
+
def __init__(self, procedure_id: str, hitl_handler: HITLHandler | None = None):
|
|
727
767
|
"""
|
|
728
768
|
Initialize with in-memory storage.
|
|
729
769
|
|