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.
Files changed (81) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/broker_log.py +17 -14
  3. tactus/adapters/channels/__init__.py +17 -15
  4. tactus/adapters/channels/base.py +16 -7
  5. tactus/adapters/channels/broker.py +43 -13
  6. tactus/adapters/channels/cli.py +19 -15
  7. tactus/adapters/channels/host.py +15 -6
  8. tactus/adapters/channels/ipc.py +82 -31
  9. tactus/adapters/channels/sse.py +41 -23
  10. tactus/adapters/cli_hitl.py +19 -19
  11. tactus/adapters/cli_log.py +4 -4
  12. tactus/adapters/control_loop.py +138 -99
  13. tactus/adapters/cost_collector_log.py +9 -9
  14. tactus/adapters/file_storage.py +56 -52
  15. tactus/adapters/http_callback_log.py +23 -13
  16. tactus/adapters/ide_log.py +17 -9
  17. tactus/adapters/lua_tools.py +4 -5
  18. tactus/adapters/mcp.py +16 -19
  19. tactus/adapters/mcp_manager.py +46 -30
  20. tactus/adapters/memory.py +9 -9
  21. tactus/adapters/plugins.py +42 -42
  22. tactus/broker/client.py +75 -78
  23. tactus/broker/protocol.py +57 -57
  24. tactus/broker/server.py +252 -197
  25. tactus/cli/app.py +3 -1
  26. tactus/cli/control.py +2 -2
  27. tactus/core/config_manager.py +181 -135
  28. tactus/core/dependencies/registry.py +66 -48
  29. tactus/core/dsl_stubs.py +222 -163
  30. tactus/core/exceptions.py +10 -1
  31. tactus/core/execution_context.py +152 -112
  32. tactus/core/lua_sandbox.py +72 -64
  33. tactus/core/message_history_manager.py +138 -43
  34. tactus/core/mocking.py +41 -27
  35. tactus/core/output_validator.py +49 -44
  36. tactus/core/registry.py +94 -80
  37. tactus/core/runtime.py +211 -176
  38. tactus/core/template_resolver.py +16 -16
  39. tactus/core/yaml_parser.py +55 -45
  40. tactus/docs/extractor.py +7 -6
  41. tactus/ide/server.py +119 -78
  42. tactus/primitives/control.py +10 -6
  43. tactus/primitives/file.py +48 -46
  44. tactus/primitives/handles.py +47 -35
  45. tactus/primitives/host.py +29 -27
  46. tactus/primitives/human.py +154 -137
  47. tactus/primitives/json.py +22 -23
  48. tactus/primitives/log.py +26 -26
  49. tactus/primitives/message_history.py +285 -31
  50. tactus/primitives/model.py +15 -9
  51. tactus/primitives/procedure.py +86 -64
  52. tactus/primitives/procedure_callable.py +58 -51
  53. tactus/primitives/retry.py +31 -29
  54. tactus/primitives/session.py +42 -29
  55. tactus/primitives/state.py +54 -43
  56. tactus/primitives/step.py +9 -13
  57. tactus/primitives/system.py +34 -21
  58. tactus/primitives/tool.py +44 -31
  59. tactus/primitives/tool_handle.py +76 -54
  60. tactus/primitives/toolset.py +25 -22
  61. tactus/sandbox/config.py +4 -4
  62. tactus/sandbox/container_runner.py +161 -107
  63. tactus/sandbox/docker_manager.py +20 -20
  64. tactus/sandbox/entrypoint.py +16 -14
  65. tactus/sandbox/protocol.py +15 -15
  66. tactus/stdlib/classify/llm.py +1 -3
  67. tactus/stdlib/core/validation.py +0 -3
  68. tactus/testing/pydantic_eval_runner.py +1 -1
  69. tactus/utils/asyncio_helpers.py +27 -0
  70. tactus/utils/cost_calculator.py +7 -7
  71. tactus/utils/model_pricing.py +11 -12
  72. tactus/utils/safe_file_library.py +156 -132
  73. tactus/utils/safe_libraries.py +27 -27
  74. tactus/validation/error_listener.py +18 -5
  75. tactus/validation/semantic_visitor.py +392 -333
  76. tactus/validation/validator.py +89 -49
  77. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -16,7 +16,7 @@ Deprecated:
16
16
  """
17
17
 
18
18
  import logging
19
- from typing import Any, Dict, List, Optional
19
+ from typing import Any, Optional
20
20
 
21
21
  logger = logging.getLogger(__name__)
22
22
 
@@ -29,7 +29,7 @@ class HumanPrimitive:
29
29
  actual human interactions (via CLI, web UI, API, etc.).
30
30
  """
31
31
 
32
- def __init__(self, execution_context, hitl_config: Optional[Dict[str, Any]] = None):
32
+ def __init__(self, execution_context, hitl_config: Optional[dict[str, Any]] = None):
33
33
  """
34
34
  Initialize Human primitive.
35
35
 
@@ -41,39 +41,41 @@ class HumanPrimitive:
41
41
  self.hitl_config = hitl_config or {}
42
42
  logger.debug("HumanPrimitive initialized")
43
43
 
44
- def _convert_lua_to_python(self, obj: Any) -> Any:
44
+ def _convert_lua_to_python(self, lua_value: Any) -> Any:
45
45
  """Recursively convert Lua tables to Python dicts or lists."""
46
- if obj is None:
46
+ if lua_value is None:
47
47
  return None
48
48
  # Check if it's a Lua table (has .items() but not a dict)
49
- if hasattr(obj, "items") and not isinstance(obj, dict):
49
+ if hasattr(lua_value, "items") and not isinstance(lua_value, dict):
50
50
  # Get all items from the Lua table
51
- items = list(obj.items())
51
+ table_items = list(lua_value.items())
52
52
 
53
53
  # Check if this is an array-like table (numeric keys starting from 1)
54
- if items and all(isinstance(k, int) for k, v in items):
54
+ if table_items and all(isinstance(key, int) for key, _ in table_items):
55
55
  # Sort by key and extract values to create a Python list
56
- sorted_items = sorted(items, key=lambda x: x[0])
56
+ sorted_items = sorted(table_items, key=lambda item: item[0])
57
57
  # Check if keys are consecutive starting from 1
58
- if [k for k, v in sorted_items] == list(range(1, len(sorted_items) + 1)):
59
- return [self._convert_lua_to_python(v) for k, v in sorted_items]
58
+ sorted_keys = [key for key, _ in sorted_items]
59
+ expected_keys = list(range(1, len(sorted_items) + 1))
60
+ if sorted_keys == expected_keys:
61
+ return [self._convert_lua_to_python(value) for _, value in sorted_items]
60
62
 
61
63
  # Otherwise, convert to dict (string keys or mixed)
62
64
  result = {}
63
- for key, value in items:
65
+ for key, value in table_items:
64
66
  result[key] = self._convert_lua_to_python(value)
65
67
  return result
66
- elif isinstance(obj, dict):
68
+ elif isinstance(lua_value, dict):
67
69
  # Recursively convert nested dicts
68
- return {k: self._convert_lua_to_python(v) for k, v in obj.items()}
69
- elif isinstance(obj, (list, tuple)):
70
+ return {key: self._convert_lua_to_python(value) for key, value in lua_value.items()}
71
+ elif isinstance(lua_value, (list, tuple)):
70
72
  # Recursively convert lists
71
- return [self._convert_lua_to_python(item) for item in obj]
73
+ return [self._convert_lua_to_python(item) for item in lua_value]
72
74
  else:
73
75
  # Primitive type, return as-is
74
- return obj
76
+ return lua_value
75
77
 
76
- def approve(self, options: Optional[Dict[str, Any]] = None) -> bool:
78
+ def approve(self, options: Optional[dict[str, Any]] = None) -> bool:
77
79
  """
78
80
  Request yes/no approval from human (BLOCKING).
79
81
 
@@ -105,26 +107,26 @@ class HumanPrimitive:
105
107
  end
106
108
  """
107
109
  # Convert Lua tables to Python dicts recursively
108
- opts = self._convert_lua_to_python(options) or {}
110
+ options_dict = self._convert_lua_to_python(options) or {}
109
111
 
110
112
  # Support string message shorthand: Human.approve("message")
111
- if isinstance(opts, str):
112
- opts = {"message": opts}
113
+ if isinstance(options_dict, str):
114
+ options_dict = {"message": options_dict}
113
115
 
114
116
  # Check for config reference
115
- config_key = opts.get("config_key")
117
+ config_key = options_dict.get("config_key")
116
118
  if config_key and config_key in self.hitl_config:
117
119
  # Merge config with runtime options (runtime wins)
118
120
  config_opts = self.hitl_config[config_key].copy()
119
- config_opts.update(opts)
120
- opts = config_opts
121
+ config_opts.update(options_dict)
122
+ options_dict = config_opts
121
123
 
122
- message = opts.get("message", "Approval requested")
123
- context = opts.get("context", {})
124
- timeout = opts.get("timeout")
125
- default = opts.get("default", False)
124
+ message = options_dict.get("message", "Approval requested")
125
+ context = options_dict.get("context", {})
126
+ timeout = options_dict.get("timeout")
127
+ default = options_dict.get("default", False)
126
128
 
127
- logger.info(f"Human approval requested: {message[:50]}...")
129
+ logger.info("Human approval requested: %s...", message[:50])
128
130
 
129
131
  # CRITICAL: Wrap HITL call in checkpoint for transparent durability
130
132
  # This allows kill/resume to work - procedure can be restarted and will resume from this point
@@ -141,16 +143,19 @@ class HumanPrimitive:
141
143
  )
142
144
 
143
145
  response = self.execution_context.checkpoint(checkpoint_fn, "hitl_approval")
144
- logger.debug(f"[CHECKPOINT] Human.approve() checkpoint completed, response={response}")
146
+ logger.debug(
147
+ "[CHECKPOINT] Human.approve() checkpoint completed, response=%s",
148
+ response,
149
+ )
145
150
 
146
151
  return response.value
147
152
 
148
- def input(self, options: Optional[Dict[str, Any]] = None) -> Optional[str]:
153
+ def input(self, options: Optional[dict[str, Any]] = None) -> Optional[str]:
149
154
  """
150
155
  Request free-form input from human (BLOCKING).
151
156
 
152
157
  Args:
153
- options: Dict with:
158
+ options: dict with:
154
159
  - message: str - Prompt for human
155
160
  - placeholder: str - Input placeholder
156
161
  - timeout: int - Timeout in seconds
@@ -172,21 +177,21 @@ class HumanPrimitive:
172
177
  end
173
178
  """
174
179
  # Convert Lua table to dict if needed
175
- opts = self._convert_lua_to_python(options) or {}
180
+ options_dict = self._convert_lua_to_python(options) or {}
176
181
 
177
182
  # Check for config reference
178
- config_key = opts.get("config_key")
183
+ config_key = options_dict.get("config_key")
179
184
  if config_key and config_key in self.hitl_config:
180
185
  config_opts = self.hitl_config[config_key].copy()
181
- config_opts.update(opts)
182
- opts = config_opts
186
+ config_opts.update(options_dict)
187
+ options_dict = config_opts
183
188
 
184
- message = opts.get("message", "Input requested")
185
- placeholder = opts.get("placeholder", "")
186
- timeout = opts.get("timeout")
187
- default = opts.get("default")
189
+ message = options_dict.get("message", "Input requested")
190
+ placeholder = options_dict.get("placeholder", "")
191
+ timeout = options_dict.get("timeout")
192
+ default = options_dict.get("default")
188
193
 
189
- logger.info(f"Human input requested: {message[:50]}...")
194
+ logger.info("Human input requested: %s...", message[:50])
190
195
 
191
196
  # CRITICAL: Wrap HITL call in checkpoint for transparent durability
192
197
  def checkpoint_fn():
@@ -203,21 +208,21 @@ class HumanPrimitive:
203
208
 
204
209
  return response.value
205
210
 
206
- def review(self, options: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
211
+ def review(self, options: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
207
212
  """
208
213
  Request human review (BLOCKING).
209
214
 
210
215
  Args:
211
- options: Dict with:
216
+ options: dict with:
212
217
  - message: str - Review prompt
213
218
  - artifact: Any - Thing to review
214
219
  - artifact_type: str - Type of artifact
215
- - options: List[str] - Available actions
220
+ - options: list[str] - Available actions
216
221
  - timeout: int - Timeout in seconds
217
222
  - config_key: str - Reference to hitl: declaration
218
223
 
219
224
  Returns:
220
- Dict with:
225
+ dict with:
221
226
  - decision: str - Selected option
222
227
  - edited_artifact: Any - Modified artifact (if edited)
223
228
  - feedback: str - Human feedback
@@ -235,35 +240,35 @@ class HumanPrimitive:
235
240
  end
236
241
  """
237
242
  # Convert Lua table to dict if needed
238
- opts = self._convert_lua_to_python(options) or {}
243
+ options_dict = self._convert_lua_to_python(options) or {}
239
244
 
240
245
  # Check for config reference
241
- config_key = opts.get("config_key")
246
+ config_key = options_dict.get("config_key")
242
247
  if config_key and config_key in self.hitl_config:
243
248
  config_opts = self.hitl_config[config_key].copy()
244
- config_opts.update(opts)
245
- opts = config_opts
249
+ config_opts.update(options_dict)
250
+ options_dict = config_opts
246
251
 
247
- message = opts.get("message", "Review requested")
248
- artifact = opts.get("artifact")
249
- options_list = opts.get("options", ["approve", "reject"])
250
- artifact_type = opts.get("artifact_type", "artifact")
251
- timeout = opts.get("timeout")
252
+ message = options_dict.get("message", "Review requested")
253
+ artifact = options_dict.get("artifact")
254
+ options_list = options_dict.get("options", ["approve", "reject"])
255
+ artifact_type = options_dict.get("artifact_type", "artifact")
256
+ timeout = options_dict.get("timeout")
252
257
 
253
- logger.info(f"Human review requested: {message[:50]}...")
258
+ logger.info("Human review requested: %s...", message[:50])
254
259
 
255
260
  # Convert artifact from Lua table to Python dict
256
261
  artifact_python = self._convert_lua_to_python(artifact) if artifact is not None else None
257
262
 
258
263
  # Convert options list to format expected by protocol: [{label, type}, ...]
259
264
  formatted_options = []
260
- for opt in options_list:
265
+ for option_entry in options_list:
261
266
  # If already a dict with label/type, use as-is
262
- if isinstance(opt, dict) and "label" in opt:
263
- formatted_options.append(opt)
267
+ if isinstance(option_entry, dict) and "label" in option_entry:
268
+ formatted_options.append(option_entry)
264
269
  # Otherwise treat as string label, default to "action" type
265
270
  else:
266
- formatted_options.append({"label": str(opt).title(), "type": "action"})
271
+ formatted_options.append({"label": str(option_entry).title(), "type": "action"})
267
272
 
268
273
  # CRITICAL: Wrap HITL call in checkpoint for transparent durability
269
274
  def checkpoint_fn():
@@ -284,7 +289,7 @@ class HumanPrimitive:
284
289
 
285
290
  return response.value
286
291
 
287
- def notify(self, options: Optional[Dict[str, Any]] = None) -> None:
292
+ def notify(self, options: Optional[dict[str, Any]] = None) -> None:
288
293
  """
289
294
  Send notification to human (NON-BLOCKING).
290
295
 
@@ -293,7 +298,7 @@ class HumanPrimitive:
293
298
  delivery should use a custom notification system.
294
299
 
295
300
  Args:
296
- options: Dict with:
301
+ options: dict with:
297
302
  - message: str - Notification message (required)
298
303
  - level: str - info, warning, error (default: info)
299
304
 
@@ -304,17 +309,17 @@ class HumanPrimitive:
304
309
  })
305
310
  """
306
311
  # Convert Lua table to dict if needed
307
- opts = self._convert_lua_to_python(options) or {}
312
+ options_dict = self._convert_lua_to_python(options) or {}
308
313
 
309
- message = opts.get("message", "Notification")
310
- level = opts.get("level", "info")
314
+ message = options_dict.get("message", "Notification")
315
+ level = options_dict.get("level", "info")
311
316
 
312
- logger.info(f"Human notification: [{level}] {message}")
317
+ logger.info("Human notification: [%s] %s", level, message)
313
318
 
314
319
  # In base Tactus, notifications are just logged
315
320
  # Implementations can override this to send actual notifications
316
321
 
317
- def escalate(self, options: Optional[Dict[str, Any]] = None) -> None:
322
+ def escalate(self, options: Optional[dict[str, Any]] = None) -> None:
318
323
  """
319
324
  Escalate to human (BLOCKING).
320
325
 
@@ -323,7 +328,7 @@ class HumanPrimitive:
323
328
  indefinitely until a human manually resumes the procedure.
324
329
 
325
330
  Args:
326
- options: Dict with:
331
+ options: dict with:
327
332
  - message: str - Escalation message
328
333
  - context: Dict - Error context
329
334
  - severity: str - Severity level (info/warning/error/critical)
@@ -343,21 +348,21 @@ class HumanPrimitive:
343
348
  end
344
349
  """
345
350
  # Convert Lua tables to Python dicts recursively
346
- opts = self._convert_lua_to_python(options) or {}
351
+ options_dict = self._convert_lua_to_python(options) or {}
347
352
 
348
353
  # Check for config reference
349
- config_key = opts.get("config_key")
354
+ config_key = options_dict.get("config_key")
350
355
  if config_key and config_key in self.hitl_config:
351
356
  # Merge config with runtime options (runtime wins)
352
357
  config_opts = self.hitl_config[config_key].copy()
353
- config_opts.update(opts)
354
- opts = config_opts
358
+ config_opts.update(options_dict)
359
+ options_dict = config_opts
355
360
 
356
- message = opts.get("message", "Escalation required")
357
- context = opts.get("context", {})
358
- severity = opts.get("severity", "error")
361
+ message = options_dict.get("message", "Escalation required")
362
+ context = options_dict.get("context", {})
363
+ severity = options_dict.get("severity", "error")
359
364
 
360
- logger.warning(f"Human escalation: {message[:50]}... (severity: {severity})")
365
+ logger.warning("Human escalation: %s... (severity: %s)", message[:50], severity)
361
366
 
362
367
  # Prepare metadata with severity and context
363
368
  metadata = {"severity": severity, "context": context}
@@ -377,7 +382,7 @@ class HumanPrimitive:
377
382
 
378
383
  logger.info("Human escalation resolved - resuming workflow")
379
384
 
380
- def select(self, options: Optional[Dict[str, Any]] = None) -> Any:
385
+ def select(self, options: Optional[dict[str, Any]] = None) -> Any:
381
386
  """
382
387
  Request selection from options (BLOCKING).
383
388
 
@@ -386,7 +391,7 @@ class HumanPrimitive:
386
391
  Args:
387
392
  options: Dict with:
388
393
  - message: str - Prompt for human
389
- - options: List[str] or List[Dict] - Available choices
394
+ - options: list[str] or list[dict] - Available choices
390
395
  - mode: str - "single" (default) or "multiple"
391
396
  - style: str - UI hint: "radio", "dropdown", "checkbox" (optional)
392
397
  - min: int - Minimum selections required (for multiple mode)
@@ -397,7 +402,7 @@ class HumanPrimitive:
397
402
 
398
403
  Returns:
399
404
  For single mode: str - Selected option value
400
- For multiple mode: List[str] - Selected option values
405
+ For multiple mode: list[str] - Selected option values
401
406
 
402
407
  Example (Lua):
403
408
  -- Single select (radio buttons)
@@ -418,38 +423,40 @@ class HumanPrimitive:
418
423
  })
419
424
  """
420
425
  # Convert Lua tables to Python dicts recursively
421
- opts = self._convert_lua_to_python(options) or {}
426
+ options_dict = self._convert_lua_to_python(options) or {}
422
427
 
423
428
  # Check for config reference
424
- config_key = opts.get("config_key")
429
+ config_key = options_dict.get("config_key")
425
430
  if config_key and config_key in self.hitl_config:
426
431
  config_opts = self.hitl_config[config_key].copy()
427
- config_opts.update(opts)
428
- opts = config_opts
432
+ config_opts.update(options_dict)
433
+ options_dict = config_opts
429
434
 
430
- message = opts.get("message", "Selection required")
431
- options_list = opts.get("options", [])
432
- mode = opts.get("mode", "single")
433
- style = opts.get("style") # UI hint: radio, dropdown, checkbox
434
- min_selections = opts.get("min", 1 if mode == "multiple" else None)
435
- max_selections = opts.get("max")
436
- timeout = opts.get("timeout")
437
- default = opts.get("default", [] if mode == "multiple" else None)
435
+ message = options_dict.get("message", "Selection required")
436
+ options_list = options_dict.get("options", [])
437
+ mode = options_dict.get("mode", "single")
438
+ style = options_dict.get("style") # UI hint: radio, dropdown, checkbox
439
+ min_selections = options_dict.get("min", 1 if mode == "multiple" else None)
440
+ max_selections = options_dict.get("max")
441
+ timeout = options_dict.get("timeout")
442
+ default = options_dict.get("default", [] if mode == "multiple" else None)
438
443
 
439
- logger.info(f"Human selection requested ({mode}): {message[:50]}...")
444
+ logger.info("Human selection requested (%s): %s...", mode, message[:50])
440
445
 
441
446
  # Convert options list to format expected by protocol: [{label, value}, ...]
442
447
  formatted_options = []
443
- for opt in options_list:
444
- if isinstance(opt, dict) and "label" in opt:
448
+ for option_entry in options_list:
449
+ if isinstance(option_entry, dict) and "label" in option_entry:
445
450
  # Already formatted: {label: "...", value: "..."}
446
- formatted_options.append(opt)
447
- elif isinstance(opt, dict) and "value" in opt:
451
+ formatted_options.append(option_entry)
452
+ elif isinstance(option_entry, dict) and "value" in option_entry:
448
453
  # Has value but no label - use value as label
449
- formatted_options.append({"label": str(opt["value"]), "value": opt["value"]})
454
+ formatted_options.append(
455
+ {"label": str(option_entry["value"]), "value": option_entry["value"]}
456
+ )
450
457
  else:
451
458
  # Simple string - use as both label and value
452
- formatted_options.append({"label": str(opt), "value": opt})
459
+ formatted_options.append({"label": str(option_entry), "value": option_entry})
453
460
 
454
461
  # Build metadata with select-specific fields
455
462
  metadata = {
@@ -475,7 +482,7 @@ class HumanPrimitive:
475
482
 
476
483
  return response.value
477
484
 
478
- def upload(self, options: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
485
+ def upload(self, options: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
479
486
  """
480
487
  Request file upload from human (BLOCKING).
481
488
 
@@ -485,14 +492,14 @@ class HumanPrimitive:
485
492
  Args:
486
493
  options: Dict with:
487
494
  - message: str - Upload prompt
488
- - accept: str or List[str] - Accepted file types (e.g., ".pdf,.doc" or ["image/*"])
495
+ - accept: str or list[str] - Accepted file types (e.g., ".pdf,.doc" or ["image/*"])
489
496
  - max_size: str or int - Maximum file size (e.g., "10MB" or 10485760)
490
497
  - multiple: bool - Allow multiple files (default: False)
491
498
  - timeout: int - Timeout in seconds
492
499
  - config_key: str - Reference to hitl: declaration
493
500
 
494
501
  Returns:
495
- Dict with file info (or List[Dict] if multiple=True):
502
+ dict with file info (or list[dict] if multiple=True):
496
503
  - path: str - Local filesystem path to uploaded file
497
504
  - name: str - Original filename
498
505
  - size: int - File size in bytes
@@ -527,22 +534,22 @@ class HumanPrimitive:
527
534
  end
528
535
  """
529
536
  # Convert Lua tables to Python dicts recursively
530
- opts = self._convert_lua_to_python(options) or {}
537
+ options_dict = self._convert_lua_to_python(options) or {}
531
538
 
532
539
  # Check for config reference
533
- config_key = opts.get("config_key")
540
+ config_key = options_dict.get("config_key")
534
541
  if config_key and config_key in self.hitl_config:
535
542
  config_opts = self.hitl_config[config_key].copy()
536
- config_opts.update(opts)
537
- opts = config_opts
543
+ config_opts.update(options_dict)
544
+ options_dict = config_opts
538
545
 
539
- message = opts.get("message", "File upload requested")
540
- accept = opts.get("accept") # File type filter
541
- max_size = opts.get("max_size") # Size limit
542
- multiple = opts.get("multiple", False)
543
- timeout = opts.get("timeout")
546
+ message = options_dict.get("message", "File upload requested")
547
+ accept = options_dict.get("accept") # File type filter
548
+ max_size = options_dict.get("max_size") # Size limit
549
+ multiple = options_dict.get("multiple", False)
550
+ timeout = options_dict.get("timeout")
544
551
 
545
- logger.info(f"Human file upload requested: {message[:50]}...")
552
+ logger.info("Human file upload requested: %s...", message[:50])
546
553
 
547
554
  # Normalize accept to list
548
555
  if isinstance(accept, str):
@@ -593,10 +600,10 @@ class HumanPrimitive:
593
600
  try:
594
601
  return int(size_str)
595
602
  except ValueError:
596
- logger.warning(f"Could not parse size '{size_str}', using default")
603
+ logger.warning("Could not parse size '%s', using default", size_str)
597
604
  return 10 * 1024 * 1024 # Default 10MB
598
605
 
599
- def inputs(self, items: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
606
+ def inputs(self, items: Optional[list[dict[str, Any]]] = None) -> dict[str, Any]:
600
607
  """
601
608
  Request multiple inputs from human in a single interaction (BLOCKING).
602
609
 
@@ -607,19 +614,19 @@ class HumanPrimitive:
607
614
  before submitting a single response.
608
615
 
609
616
  Args:
610
- items: List of input items, each with:
617
+ items: list of input items, each with:
611
618
  - id: str - Unique ID for this item (required)
612
619
  - label: str - Short label for tabs (required)
613
620
  - type: str - Request type: "approval", "input", "select", etc. (required)
614
621
  - message: str - Prompt for this input (required)
615
- - options: List - Options for select/review types
622
+ - options: list - Options for select/review types
616
623
  - required: bool - Whether this input is required (default: True)
617
- - metadata: Dict - Type-specific metadata
624
+ - metadata: dict - Type-specific metadata
618
625
  - timeout: int - Timeout in seconds
619
626
  - default: Any - Default value
620
627
 
621
628
  Returns:
622
- Dict keyed by item ID with response values:
629
+ dict keyed by item ID with response values:
623
630
  {
624
631
  "target": "production",
625
632
  "confirm": True,
@@ -661,10 +668,12 @@ class HumanPrimitive:
661
668
  )
662
669
 
663
670
  # Convert Lua tables to Python dicts recursively
664
- logger.debug(f"Human.inputs() called with items type: {type(items)}")
671
+ logger.debug("Human.inputs() called with items type: %s", type(items))
665
672
  items_list = self._convert_lua_to_python(items) or []
666
673
  logger.debug(
667
- f"Converted to items_list, length: {len(items_list)}, type: {type(items_list)}"
674
+ "Converted to items_list, length: %s, type: %s",
675
+ len(items_list),
676
+ type(items_list),
668
677
  )
669
678
 
670
679
  if not items_list:
@@ -672,15 +681,18 @@ class HumanPrimitive:
672
681
 
673
682
  # Validate items
674
683
  seen_ids = set()
675
- for idx, item in enumerate(items_list):
684
+ for index, item in enumerate(items_list):
676
685
  logger.debug(
677
- f"Validating item {idx}: type={type(item)}, keys={list(item.keys()) if isinstance(item, dict) else 'NOT A DICT'}"
686
+ "Validating item %s: type=%s, keys=%s",
687
+ index,
688
+ type(item),
689
+ list(item.keys()) if isinstance(item, dict) else "NOT A DICT",
678
690
  )
679
691
 
680
692
  # Ensure item is a dict
681
693
  if not isinstance(item, dict):
682
694
  raise ValueError(
683
- f"Item {idx} is not a dictionary (got {type(item).__name__}): {item}"
695
+ f"Item {index} is not a dictionary (got {type(item).__name__}): {item}"
684
696
  )
685
697
 
686
698
  # Validate required fields
@@ -699,7 +711,7 @@ class HumanPrimitive:
699
711
  raise ValueError(f"Duplicate item ID: {item_id}")
700
712
  seen_ids.add(item_id)
701
713
 
702
- logger.info(f"Human inputs requested: {len(items_list)} items")
714
+ logger.info("Human inputs requested: %s items", len(items_list))
703
715
 
704
716
  # Build ControlRequestItem list
705
717
  from tactus.protocols.control import ControlRequestItem
@@ -709,13 +721,18 @@ class HumanPrimitive:
709
721
  # Convert options if present
710
722
  options_list = item.get("options", [])
711
723
  formatted_options = []
712
- for opt in options_list:
713
- if isinstance(opt, dict) and "label" in opt:
714
- formatted_options.append(opt)
715
- elif isinstance(opt, dict) and "value" in opt:
716
- formatted_options.append({"label": str(opt["value"]), "value": opt["value"]})
724
+ for option_entry in options_list:
725
+ if isinstance(option_entry, dict) and "label" in option_entry:
726
+ formatted_options.append(option_entry)
727
+ elif isinstance(option_entry, dict) and "value" in option_entry:
728
+ formatted_options.append(
729
+ {
730
+ "label": str(option_entry["value"]),
731
+ "value": option_entry["value"],
732
+ }
733
+ )
717
734
  else:
718
- formatted_options.append({"label": str(opt), "value": opt})
735
+ formatted_options.append({"label": str(option_entry), "value": option_entry})
719
736
 
720
737
  # Build metadata
721
738
  metadata = item.get("metadata", {})
@@ -762,7 +779,7 @@ class HumanPrimitive:
762
779
 
763
780
  return lua_runtime.table_from(converted_result)
764
781
 
765
- def multiple(self, items: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
782
+ def multiple(self, items: Optional[list[dict[str, Any]]] = None) -> dict[str, Any]:
766
783
  """
767
784
  Request multiple inputs from human in a single interaction (BLOCKING).
768
785
 
@@ -773,19 +790,19 @@ class HumanPrimitive:
773
790
  them all before submitting a single response.
774
791
 
775
792
  Args:
776
- items: List of input items, each with:
793
+ items: list of input items, each with:
777
794
  - id: str - Unique ID for this item (required)
778
795
  - label: str - Short label for tabs (required)
779
796
  - type: str - Request type: "approval", "input", "select", etc. (required)
780
797
  - message: str - Prompt for this input (required)
781
- - options: List - Options for select/review types
798
+ - options: list - Options for select/review types
782
799
  - required: bool - Whether this input is required (default: True)
783
- - metadata: Dict - Type-specific metadata
800
+ - metadata: dict - Type-specific metadata
784
801
  - timeout: int - Timeout in seconds
785
802
  - default: Any - Default value
786
803
 
787
804
  Returns:
788
- Dict keyed by item ID with response values:
805
+ dict keyed by item ID with response values:
789
806
  {
790
807
  "target": "production",
791
808
  "confirm": True,
@@ -822,7 +839,7 @@ class HumanPrimitive:
822
839
  """
823
840
  return self.inputs(items)
824
841
 
825
- def custom(self, options: Optional[Dict[str, Any]] = None) -> Any:
842
+ def custom(self, options: Optional[dict[str, Any]] = None) -> Any:
826
843
  """
827
844
  Request custom component interaction from human (BLOCKING).
828
845
 
@@ -830,11 +847,11 @@ class HumanPrimitive:
830
847
  The component receives all metadata and can return arbitrary values.
831
848
 
832
849
  Args:
833
- options: Dict with:
850
+ options: dict with:
834
851
  - component_type: str - Which custom component to render (required)
835
852
  - message: str - Message to display (required)
836
- - data: Dict - Component-specific data (images, options, etc.)
837
- - actions: List[Dict] - Optional action buttons
853
+ - data: dict - Component-specific data (images, options, etc.)
854
+ - actions: list[dict] - Optional action buttons
838
855
  - timeout: int - Timeout in seconds
839
856
  - default: Any - Default value if timeout
840
857
  - config_key: str - Reference to hitl: declaration
@@ -892,7 +909,7 @@ class HumanPrimitive:
892
909
  "actions": actions,
893
910
  }
894
911
 
895
- logger.info(f"Human custom component requested: {component_type}")
912
+ logger.info("Human custom component requested: %s", component_type)
896
913
 
897
914
  # CRITICAL: Wrap HITL call in checkpoint for transparent durability
898
915
  def checkpoint_fn():