tactus 0.34.0__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.0.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ Stores procedure metadata and execution log as JSON files on disk.
6
6
 
7
7
  import json
8
8
  from pathlib import Path
9
- from typing import Any, Optional, Dict, List
9
+ from typing import Any, Optional
10
10
  from datetime import datetime
11
11
 
12
12
  from tactus.protocols.models import (
@@ -67,20 +67,20 @@ class FileStorage:
67
67
  return {}
68
68
 
69
69
  try:
70
- with open(file_path, "r") as f:
71
- return json.load(f)
72
- except (json.JSONDecodeError, IOError) as e:
73
- raise RuntimeError(f"Failed to read procedure file {file_path}: {e}")
70
+ with open(file_path, "r") as file_handle:
71
+ return json.load(file_handle)
72
+ except (json.JSONDecodeError, IOError) as error:
73
+ raise RuntimeError(f"Failed to read procedure file {file_path}: {error}")
74
74
 
75
75
  def _write_file(self, procedure_id: str, data: dict) -> None:
76
76
  """Write procedure data to file."""
77
77
  file_path = self._get_file_path(procedure_id)
78
78
 
79
79
  try:
80
- with open(file_path, "w") as f:
81
- json.dump(data, f, indent=2, default=str)
82
- except (IOError, OSError) as e:
83
- raise RuntimeError(f"Failed to write procedure file {file_path}: {e}")
80
+ with open(file_path, "w") as file_handle:
81
+ json.dump(data, file_handle, indent=2, default=str)
82
+ except (IOError, OSError) as error:
83
+ raise RuntimeError(f"Failed to write procedure file {file_path}: {error}")
84
84
 
85
85
  def _deserialize_result(self, result: Any) -> Any:
86
86
  """Deserialize checkpoint result, reconstructing Pydantic models."""
@@ -189,25 +189,25 @@ class FileStorage:
189
189
  self, procedure_id: str, status: str, waiting_on_message_id: Optional[str] = None
190
190
  ) -> None:
191
191
  """Update procedure status."""
192
- metadata = self.load_procedure_metadata(procedure_id)
193
- metadata.status = status
194
- metadata.waiting_on_message_id = waiting_on_message_id
195
- self.save_procedure_metadata(procedure_id, metadata)
192
+ procedure_metadata = self.load_procedure_metadata(procedure_id)
193
+ procedure_metadata.status = status
194
+ procedure_metadata.waiting_on_message_id = waiting_on_message_id
195
+ self.save_procedure_metadata(procedure_id, procedure_metadata)
196
196
 
197
- def get_state(self, procedure_id: str) -> Dict[str, Any]:
197
+ def get_state(self, procedure_id: str) -> dict[str, Any]:
198
198
  """Get mutable state dictionary."""
199
- metadata = self.load_procedure_metadata(procedure_id)
200
- return metadata.state
199
+ procedure_metadata = self.load_procedure_metadata(procedure_id)
200
+ return procedure_metadata.state
201
201
 
202
- def set_state(self, procedure_id: str, state: Dict[str, Any]) -> None:
202
+ def set_state(self, procedure_id: str, state: dict[str, Any]) -> None:
203
203
  """Set mutable state dictionary."""
204
- metadata = self.load_procedure_metadata(procedure_id)
205
- metadata.state = state
206
- self.save_procedure_metadata(procedure_id, metadata)
204
+ procedure_metadata = self.load_procedure_metadata(procedure_id)
205
+ procedure_metadata.state = state
206
+ self.save_procedure_metadata(procedure_id, procedure_metadata)
207
207
 
208
208
  # Tracing & Debugging Methods
209
209
 
210
- def _load_index(self) -> Dict[str, Any]:
210
+ def _load_index(self) -> dict[str, Any]:
211
211
  """Load the run index."""
212
212
  if not self.index_file.exists():
213
213
  return {}
@@ -218,13 +218,13 @@ class FileStorage:
218
218
  except (json.JSONDecodeError, IOError):
219
219
  return {}
220
220
 
221
- def _save_index(self, index: Dict[str, Any]) -> None:
221
+ def _save_index(self, index: dict[str, Any]) -> None:
222
222
  """Save the run index."""
223
223
  try:
224
- with open(self.index_file, "w") as f:
225
- json.dump(index, f, indent=2, default=str)
226
- except (IOError, OSError) as e:
227
- raise RuntimeError(f"Failed to write index file: {e}")
224
+ with open(self.index_file, "w") as file_handle:
225
+ json.dump(index, file_handle, indent=2, default=str)
226
+ except (IOError, OSError) as error:
227
+ raise RuntimeError(f"Failed to write index file: {error}")
228
228
 
229
229
  def _update_index(self, run: ExecutionRun) -> None:
230
230
  """Update index with run metadata."""
@@ -263,10 +263,10 @@ class FileStorage:
263
263
  checkpoint["timestamp"] = checkpoint["timestamp"].isoformat()
264
264
 
265
265
  try:
266
- with open(run_path, "w") as f:
267
- json.dump(data, f, indent=2, default=str)
268
- except (IOError, OSError) as e:
269
- raise RuntimeError(f"Failed to save run {run.run_id}: {e}")
266
+ with open(run_path, "w") as file_handle:
267
+ json.dump(data, file_handle, indent=2, default=str)
268
+ except (IOError, OSError) as error:
269
+ raise RuntimeError(f"Failed to save run {run.run_id}: {error}")
270
270
 
271
271
  # Update index
272
272
  self._update_index(run)
@@ -290,10 +290,10 @@ class FileStorage:
290
290
  raise FileNotFoundError(f"Run {run_id} not found")
291
291
 
292
292
  try:
293
- with open(run_path, "r") as f:
294
- data = json.load(f)
295
- except (json.JSONDecodeError, IOError) as e:
296
- raise RuntimeError(f"Failed to load run {run_id}: {e}")
293
+ with open(run_path, "r") as file_handle:
294
+ data = json.load(file_handle)
295
+ except (json.JSONDecodeError, IOError) as error:
296
+ raise RuntimeError(f"Failed to load run {run_id}: {error}")
297
297
 
298
298
  # Convert timestamps back to datetime objects
299
299
  if data.get("start_time"):
@@ -303,28 +303,30 @@ class FileStorage:
303
303
 
304
304
  # Convert checkpoint timestamps and rebuild CheckpointEntry objects
305
305
  execution_log = []
306
- for cp_data in data.get("execution_log", []):
307
- if cp_data.get("timestamp"):
308
- cp_data["timestamp"] = datetime.fromisoformat(cp_data["timestamp"])
306
+ for checkpoint_data in data.get("execution_log", []):
307
+ if checkpoint_data.get("timestamp"):
308
+ checkpoint_data["timestamp"] = datetime.fromisoformat(checkpoint_data["timestamp"])
309
309
 
310
310
  # Rebuild SourceLocation if present
311
- if cp_data.get("source_location"):
312
- cp_data["source_location"] = SourceLocation(**cp_data["source_location"])
311
+ if checkpoint_data.get("source_location"):
312
+ checkpoint_data["source_location"] = SourceLocation(
313
+ **checkpoint_data["source_location"]
314
+ )
313
315
 
314
- execution_log.append(CheckpointEntry(**cp_data))
316
+ execution_log.append(CheckpointEntry(**checkpoint_data))
315
317
 
316
318
  data["execution_log"] = execution_log
317
319
 
318
320
  # Rebuild Breakpoint objects
319
321
  breakpoints = []
320
- for bp_data in data.get("breakpoints", []):
321
- breakpoints.append(Breakpoint(**bp_data))
322
+ for breakpoint_data in data.get("breakpoints", []):
323
+ breakpoints.append(Breakpoint(**breakpoint_data))
322
324
 
323
325
  data["breakpoints"] = breakpoints
324
326
 
325
327
  return ExecutionRun(**data)
326
328
 
327
- def list_runs(self, procedure_name: Optional[str] = None) -> List[ExecutionRun]:
329
+ def list_runs(self, procedure_name: Optional[str] = None) -> list[ExecutionRun]:
328
330
  """
329
331
  List all runs, optionally filtered by procedure name.
330
332
 
@@ -339,7 +341,9 @@ class FileStorage:
339
341
  # Filter by procedure name if specified
340
342
  if procedure_name:
341
343
  run_ids = [
342
- rid for rid, info in index.items() if info.get("procedure_name") == procedure_name
344
+ run_id
345
+ for run_id, info in index.items()
346
+ if info.get("procedure_name") == procedure_name
343
347
  ]
344
348
  else:
345
349
  run_ids = list(index.keys())
@@ -358,7 +362,7 @@ class FileStorage:
358
362
 
359
363
  return runs
360
364
 
361
- def save_breakpoints(self, procedure_name: str, breakpoints: List[Breakpoint]) -> None:
365
+ def save_breakpoints(self, procedure_name: str, breakpoints: list[Breakpoint]) -> None:
362
366
  """
363
367
  Save breakpoints for a procedure.
364
368
 
@@ -371,12 +375,12 @@ class FileStorage:
371
375
  data = [bp.model_dump() for bp in breakpoints]
372
376
 
373
377
  try:
374
- with open(bp_path, "w") as f:
375
- json.dump(data, f, indent=2)
376
- except (IOError, OSError) as e:
377
- raise RuntimeError(f"Failed to save breakpoints for {procedure_name}: {e}")
378
+ with open(bp_path, "w") as file_handle:
379
+ json.dump(data, file_handle, indent=2)
380
+ except (IOError, OSError) as error:
381
+ raise RuntimeError(f"Failed to save breakpoints for {procedure_name}: {error}")
378
382
 
379
- def load_breakpoints(self, procedure_name: str) -> List[Breakpoint]:
383
+ def load_breakpoints(self, procedure_name: str) -> list[Breakpoint]:
380
384
  """
381
385
  Load breakpoints for a procedure.
382
386
 
@@ -392,8 +396,8 @@ class FileStorage:
392
396
  return []
393
397
 
394
398
  try:
395
- with open(bp_path, "r") as f:
396
- data = json.load(f)
399
+ with open(bp_path, "r") as file_handle:
400
+ data = json.load(file_handle)
397
401
  except (json.JSONDecodeError, IOError):
398
402
  return []
399
403
 
@@ -7,7 +7,7 @@ Used when TACTUS_CALLBACK_URL environment variable is set.
7
7
 
8
8
  import logging
9
9
  import os
10
- from typing import Optional, List
10
+ from typing import Optional
11
11
 
12
12
  import requests
13
13
  from requests.adapters import HTTPAdapter
@@ -42,7 +42,7 @@ class HTTPCallbackLogHandler:
42
42
  """
43
43
  self.callback_url = callback_url
44
44
  self.timeout = timeout
45
- self.cost_events: List[CostEvent] = [] # Track cost events for aggregation
45
+ self.cost_events: list[CostEvent] = [] # Track cost events for aggregation
46
46
 
47
47
  # Setup session with retry logic
48
48
  self.session = requests.Session()
@@ -55,7 +55,7 @@ class HTTPCallbackLogHandler:
55
55
  self.session.mount("http://", adapter)
56
56
  self.session.mount("https://", adapter)
57
57
 
58
- logger.info(f"[HTTP_CALLBACK] Initialized with URL: {callback_url}")
58
+ logger.info("[HTTP_CALLBACK] Initialized with URL: %s", callback_url)
59
59
 
60
60
  def log(self, event: LogEvent) -> None:
61
61
  """
@@ -70,29 +70,36 @@ class HTTPCallbackLogHandler:
70
70
 
71
71
  try:
72
72
  # Serialize event to JSON
73
- event_dict = event.model_dump(mode="json")
73
+ event_payload = event.model_dump(mode="json")
74
74
 
75
75
  # Format timestamp to ensure ISO format with Z suffix
76
76
  iso_string = event.timestamp.isoformat()
77
- if not (iso_string.endswith("Z") or "+" in iso_string or iso_string.count("-") > 2):
77
+ has_timezone_marker = (
78
+ iso_string.endswith("Z") or "+" in iso_string or iso_string.count("-") > 2
79
+ )
80
+ if not has_timezone_marker:
78
81
  iso_string += "Z"
79
- event_dict["timestamp"] = iso_string
82
+ event_payload["timestamp"] = iso_string
80
83
 
81
84
  # POST to callback URL
82
85
  response = self.session.post(
83
86
  self.callback_url,
84
- json=event_dict,
87
+ json=event_payload,
85
88
  timeout=self.timeout,
86
89
  )
87
90
  response.raise_for_status()
88
- logger.debug(f"[HTTP_CALLBACK] Event posted: type={event.event_type}")
91
+ logger.debug("[HTTP_CALLBACK] Event posted: type=%s", event.event_type)
89
92
 
90
- except requests.exceptions.RequestException as e:
93
+ except requests.exceptions.RequestException as error:
91
94
  # Log but don't fail - event streaming is best-effort
92
- logger.warning(f"[HTTP_CALLBACK] Failed to POST event to {self.callback_url}: {e}")
93
- except Exception as e:
95
+ logger.warning(
96
+ "[HTTP_CALLBACK] Failed to POST event to %s: %s",
97
+ self.callback_url,
98
+ error,
99
+ )
100
+ except Exception as error:
94
101
  # Catch any other errors to prevent crashing the procedure
95
- logger.warning(f"[HTTP_CALLBACK] Unexpected error posting event: {e}")
102
+ logger.warning("[HTTP_CALLBACK] Unexpected error posting event: %s", error)
96
103
 
97
104
  @classmethod
98
105
  def from_environment(cls) -> Optional["HTTPCallbackLogHandler"]:
@@ -104,6 +111,9 @@ class HTTPCallbackLogHandler:
104
111
  """
105
112
  callback_url = os.environ.get("TACTUS_CALLBACK_URL")
106
113
  if callback_url:
107
- logger.info(f"[HTTP_CALLBACK] Creating handler from environment: {callback_url}")
114
+ logger.info(
115
+ "[HTTP_CALLBACK] Creating handler from environment: %s",
116
+ callback_url,
117
+ )
108
118
  return cls(callback_url=callback_url)
109
119
  return None
@@ -6,9 +6,8 @@ Collects log events in a queue for streaming to IDE frontend.
6
6
 
7
7
  import logging
8
8
  import queue
9
- from typing import List
10
9
 
11
- from tactus.protocols.models import LogEvent
10
+ from tactus.protocols.models import CostEvent, LogEvent
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
@@ -25,8 +24,8 @@ class IDELogHandler:
25
24
 
26
25
  def __init__(self):
27
26
  """Initialize IDE log handler."""
28
- self.events = queue.Queue()
29
- self.cost_events = [] # Track cost events for aggregation
27
+ self.events: queue.Queue[LogEvent] = queue.Queue()
28
+ self.cost_events: list[CostEvent] = [] # Track cost events for aggregation
30
29
  logger.debug("IDELogHandler initialized")
31
30
 
32
31
  def log(self, event: LogEvent) -> None:
@@ -37,7 +36,10 @@ class IDELogHandler:
37
36
  event: Structured log event
38
37
  """
39
38
  # CRITICAL DEBUG: Log every call to this method
40
- logger.info(f"[IDE_LOG] log() called with event type: {type(event).__name__}")
39
+ logger.info(
40
+ "[IDE_LOG] log() called with event type: %s",
41
+ type(event).__name__,
42
+ )
41
43
 
42
44
  # Track cost events for aggregation
43
45
  from tactus.protocols.models import CostEvent, AgentStreamChunkEvent
@@ -48,16 +50,22 @@ class IDELogHandler:
48
50
  # Debug logging for streaming events
49
51
  if isinstance(event, AgentStreamChunkEvent):
50
52
  logger.info(
51
- f"[IDE_LOG] Received AgentStreamChunkEvent: agent={event.agent_name}, chunk_len={len(event.chunk_text)}, accumulated_len={len(event.accumulated_text)}"
53
+ "[IDE_LOG] Received AgentStreamChunkEvent: agent=%s, "
54
+ "chunk_len=%s, accumulated_len=%s",
55
+ event.agent_name,
56
+ len(event.chunk_text),
57
+ len(event.accumulated_text),
52
58
  )
53
59
 
54
60
  self.events.put(event)
55
61
  # Use INFO level to ensure we see this in logs
56
62
  logger.info(
57
- f"[IDE_LOG] Event queued: type={type(event).__name__}, queue_size={self.events.qsize()}"
63
+ "[IDE_LOG] Event queued: type=%s, queue_size=%s",
64
+ type(event).__name__,
65
+ self.events.qsize(),
58
66
  )
59
67
 
60
- def get_events(self, timeout: float = 0.1) -> List[LogEvent]:
68
+ def get_events(self, timeout: float = 0.1) -> list[LogEvent]:
61
69
  """
62
70
  Get all available events from the queue.
63
71
 
@@ -67,7 +75,7 @@ class IDELogHandler:
67
75
  Returns:
68
76
  List of LogEvent objects
69
77
  """
70
- events = []
78
+ events: list[LogEvent] = []
71
79
  while True:
72
80
  try:
73
81
  event = self.events.get(timeout=timeout)
@@ -193,8 +193,7 @@ class LuaToolsAdapter:
193
193
  # Track the mock call
194
194
  if self.tool_primitive:
195
195
  self.tool_primitive.record_call(tool_name, kwargs, result_str)
196
- if self.mock_manager:
197
- self.mock_manager.record_call(tool_name, kwargs, result_str)
196
+ self.mock_manager.record_call(tool_name, kwargs, result_str)
198
197
  return result_str
199
198
 
200
199
  # Call Lua function directly (Lupa is NOT thread-safe, so we can't use executor)
@@ -221,8 +220,8 @@ class LuaToolsAdapter:
221
220
  logger.debug(f"Lua tool '{tool_name}' executed successfully")
222
221
  return result_str
223
222
 
224
- except Exception as e:
225
- error_msg = f"Error executing Lua tool '{tool_name}': {str(e)}"
223
+ except Exception as error:
224
+ error_msg = f"Error executing Lua tool '{tool_name}': {str(error)}"
226
225
  logger.error(error_msg, exc_info=True)
227
226
 
228
227
  # Record failed call
@@ -230,7 +229,7 @@ class LuaToolsAdapter:
230
229
  self.tool_primitive.record_call(tool_name, kwargs, error_msg)
231
230
 
232
231
  # Re-raise to let agent handle it
233
- raise RuntimeError(error_msg) from e
232
+ raise RuntimeError(error_msg) from error
234
233
 
235
234
  # Build proper signature for Pydantic AI tool discovery
236
235
  sig_params = []
tactus/adapters/mcp.py CHANGED
@@ -54,8 +54,8 @@ class PydanticAIMCPAdapter:
54
54
  "MCP client doesn't have list_tools() or get_tools(), trying direct call"
55
55
  )
56
56
  mcp_tools = await self.mcp_client() if callable(self.mcp_client) else []
57
- except Exception as e:
58
- logger.error(f"Failed to load tools from MCP server: {e}", exc_info=True)
57
+ except Exception as error:
58
+ logger.error(f"Failed to load tools from MCP server: {error}", exc_info=True)
59
59
  return []
60
60
 
61
61
  if not mcp_tools:
@@ -71,9 +71,9 @@ class PydanticAIMCPAdapter:
71
71
  tool = self._convert_mcp_tool_to_pydantic_ai(mcp_tool)
72
72
  if tool:
73
73
  pydantic_tools.append(tool)
74
- except Exception as e:
74
+ except Exception as error:
75
75
  logger.error(
76
- f"Failed to convert MCP tool {getattr(mcp_tool, 'name', 'unknown')}: {e}",
76
+ f"Failed to convert MCP tool {getattr(mcp_tool, 'name', 'unknown')}: {error}",
77
77
  exc_info=True,
78
78
  )
79
79
 
@@ -91,16 +91,12 @@ class PydanticAIMCPAdapter:
91
91
  pydantic_ai.Tool instance or None if conversion fails
92
92
  """
93
93
  # Extract tool metadata
94
- tool_name = (
95
- getattr(mcp_tool, "name", None) or mcp_tool.get("name")
96
- if isinstance(mcp_tool, dict)
97
- else None
98
- )
99
- tool_description = (
100
- getattr(mcp_tool, "description", None) or mcp_tool.get("description", "")
101
- if isinstance(mcp_tool, dict)
102
- else ""
103
- )
94
+ if isinstance(mcp_tool, dict):
95
+ tool_name = mcp_tool.get("name")
96
+ tool_description = mcp_tool.get("description", "")
97
+ else:
98
+ tool_name = getattr(mcp_tool, "name", None)
99
+ tool_description = getattr(mcp_tool, "description", None) or ""
104
100
 
105
101
  if not tool_name:
106
102
  logger.warning(f"MCP tool missing name: {mcp_tool}")
@@ -120,9 +116,10 @@ class PydanticAIMCPAdapter:
120
116
  if input_schema:
121
117
  try:
122
118
  args_model = self._json_schema_to_pydantic_model(input_schema, tool_name)
123
- except Exception as e:
119
+ except Exception as error:
124
120
  logger.error(
125
- f"Failed to create Pydantic model for tool '{tool_name}': {e}", exc_info=True
121
+ f"Failed to create Pydantic model for tool '{tool_name}': {error}",
122
+ exc_info=True,
126
123
  )
127
124
  # Fallback: create a simple model that accepts any dict
128
125
  args_model = create_model(
@@ -186,9 +183,9 @@ class PydanticAIMCPAdapter:
186
183
  logger.debug(f"Tool '{tool_name}' returned: {result_str[:100]}...")
187
184
  return result_str
188
185
 
189
- except Exception as e:
190
- logger.error(f"MCP tool '{tool_name}' execution failed: {e}", exc_info=True)
191
- error_msg = f"Error executing tool '{tool_name}': {str(e)}"
186
+ except Exception as error:
187
+ logger.error(f"MCP tool '{tool_name}' execution failed: {error}", exc_info=True)
188
+ error_msg = f"Error executing tool '{tool_name}': {str(error)}"
192
189
  # Still record the failed call
193
190
  if self.tool_primitive:
194
191
  self.tool_primitive.record_call(tool_name, args_dict, error_msg)
@@ -10,7 +10,7 @@ import os
10
10
  import re
11
11
  import asyncio
12
12
  from contextlib import AsyncExitStack
13
- from typing import Dict, Any, List
13
+ from typing import Any
14
14
 
15
15
  from pydantic_ai.mcp import MCPServerStdio
16
16
 
@@ -29,10 +29,10 @@ def substitute_env_vars(value: Any) -> Any:
29
29
  """
30
30
  if isinstance(value, str):
31
31
  # Replace ${VAR} or $VAR with environment variable value
32
- return re.sub(r"\$\{(\w+)\}", lambda m: os.getenv(m.group(1), ""), value)
33
- elif isinstance(value, dict):
32
+ return re.sub(r"\$\{(\w+)\}", lambda match: os.getenv(match.group(1), ""), value)
33
+ if isinstance(value, dict):
34
34
  return {k: substitute_env_vars(v) for k, v in value.items()}
35
- elif isinstance(value, list):
35
+ if isinstance(value, list):
36
36
  return [substitute_env_vars(v) for v in value]
37
37
  return value
38
38
 
@@ -45,7 +45,7 @@ class MCPServerManager:
45
45
  tool prefixing. Handles connection lifecycle and tool call tracking.
46
46
  """
47
47
 
48
- def __init__(self, server_configs: Dict[str, Dict[str, Any]], tool_primitive=None):
48
+ def __init__(self, server_configs: dict[str, dict[str, Any]], tool_primitive=None):
49
49
  """
50
50
  Initialize MCP server manager.
51
51
 
@@ -55,10 +55,10 @@ class MCPServerManager:
55
55
  """
56
56
  self.configs = server_configs
57
57
  self.tool_primitive = tool_primitive
58
- self.servers: List[MCPServerStdio] = []
59
- self.server_toolsets: Dict[str, MCPServerStdio] = {} # Map server names to toolsets
58
+ self.servers: list[MCPServerStdio] = []
59
+ self.server_toolsets: dict[str, MCPServerStdio] = {} # Map server names to toolsets
60
60
  self._exit_stack = AsyncExitStack()
61
- logger.info(f"MCPServerManager initialized with {len(server_configs)} server(s)")
61
+ logger.info("MCPServerManager initialized with %s server(s)", len(server_configs))
62
62
 
63
63
  async def __aenter__(self):
64
64
  """Connect to all configured MCP servers."""
@@ -67,17 +67,21 @@ class MCPServerManager:
67
67
  last_error: Exception | None = None
68
68
  for attempt in range(1, 4):
69
69
  try:
70
- logger.info(f"Connecting to MCP server '{name}' (attempt {attempt}/3)...")
70
+ logger.info(
71
+ "Connecting to MCP server '%s' (attempt %s/3)...",
72
+ name,
73
+ attempt,
74
+ )
71
75
 
72
76
  # Substitute environment variables in config
73
- config = substitute_env_vars(config)
77
+ resolved_config = substitute_env_vars(config)
74
78
 
75
79
  # Create base server
76
80
  server = MCPServerStdio(
77
- command=config["command"],
78
- args=config.get("args", []),
79
- env=config.get("env"),
80
- cwd=config.get("cwd"),
81
+ command=resolved_config["command"],
82
+ args=resolved_config.get("args", []),
83
+ env=resolved_config.get("env"),
84
+ cwd=resolved_config.get("cwd"),
81
85
  process_tool_call=self._create_trace_callback(name), # Tracking hook
82
86
  )
83
87
 
@@ -93,17 +97,19 @@ class MCPServerManager:
93
97
  )
94
98
  last_error = None
95
99
  break
96
- except Exception as e:
97
- last_error = e
100
+ except Exception as error:
101
+ last_error = error
98
102
 
99
103
  # Check if this is a fileno error (common in test environments)
100
104
  import io
101
105
 
102
- error_str = str(e)
103
- if "fileno" in error_str or isinstance(e, io.UnsupportedOperation):
106
+ error_str = str(error)
107
+ if "fileno" in error_str or isinstance(error, io.UnsupportedOperation):
104
108
  logger.warning(
105
- f"Failed to connect to MCP server '{name}': {e} "
106
- f"(test environment with redirected streams)"
109
+ "Failed to connect to MCP server '%s': %s "
110
+ "(test environment with redirected streams)",
111
+ name,
112
+ error,
107
113
  )
108
114
  # Allow procedures to continue without MCP in this environment.
109
115
  last_error = None
@@ -115,12 +121,19 @@ class MCPServerManager:
115
121
  or "unhandled errors in a TaskGroup" in error_str
116
122
  ):
117
123
  logger.warning(
118
- f"Transient MCP connection failure for '{name}': {e} (retrying)"
124
+ "Transient MCP connection failure for '%s': %s (retrying)",
125
+ name,
126
+ error,
119
127
  )
120
128
  await asyncio.sleep(0.05 * attempt)
121
129
  continue
122
130
 
123
- logger.error(f"Failed to connect to MCP server '{name}': {e}", exc_info=True)
131
+ logger.error(
132
+ "Failed to connect to MCP server '%s': %s",
133
+ name,
134
+ error,
135
+ exc_info=True,
136
+ )
124
137
  break
125
138
 
126
139
  if last_error is not None:
@@ -146,14 +159,17 @@ class MCPServerManager:
146
159
  Async callback function for process_tool_call
147
160
  """
148
161
 
149
- async def trace_tool_call(ctx, next_call, tool_name, tool_args):
162
+ async def trace_tool_call(execution_context, invoke_next, tool_name, tool_args):
150
163
  """Middleware to record tool calls in Tactus ToolPrimitive."""
151
164
  logger.debug(
152
- f"MCP server '{server_name}' calling tool '{tool_name}' with args: {tool_args}"
165
+ "MCP server '%s' calling tool '%s' with args: %s",
166
+ server_name,
167
+ tool_name,
168
+ tool_args,
153
169
  )
154
170
 
155
171
  try:
156
- result = await next_call(tool_name, tool_args)
172
+ result = await invoke_next(tool_name, tool_args)
157
173
 
158
174
  # Record in ToolPrimitive if available
159
175
  if self.tool_primitive:
@@ -162,19 +178,19 @@ class MCPServerManager:
162
178
  result_str = str(result) if not isinstance(result, str) else result
163
179
  self.tool_primitive.record_call(tool_name, tool_args, result_str)
164
180
 
165
- logger.debug(f"Tool '{tool_name}' completed successfully")
181
+ logger.debug("Tool '%s' completed successfully", tool_name)
166
182
  return result
167
- except Exception as e:
168
- logger.error(f"Tool '{tool_name}' failed: {e}", exc_info=True)
183
+ except Exception as error:
184
+ logger.error("Tool '%s' failed: %s", tool_name, error, exc_info=True)
169
185
  # Still record the failed call
170
186
  if self.tool_primitive:
171
- error_msg = f"Error: {str(e)}"
187
+ error_msg = f"Error: {str(error)}"
172
188
  self.tool_primitive.record_call(tool_name, tool_args, error_msg)
173
189
  raise
174
190
 
175
191
  return trace_tool_call
176
192
 
177
- def get_toolsets(self) -> List[MCPServerStdio]:
193
+ def get_toolsets(self) -> list[MCPServerStdio]:
178
194
  """
179
195
  Return list of connected servers as toolsets.
180
196
 
tactus/adapters/memory.py CHANGED
@@ -36,18 +36,18 @@ class MemoryStorage:
36
36
  self, procedure_id: str, status: str, waiting_on_message_id: Optional[str] = None
37
37
  ) -> None:
38
38
  """Update procedure status."""
39
- metadata = self.load_procedure_metadata(procedure_id)
40
- metadata.status = status
41
- metadata.waiting_on_message_id = waiting_on_message_id
42
- self.save_procedure_metadata(procedure_id, metadata)
39
+ procedure_metadata = self.load_procedure_metadata(procedure_id)
40
+ procedure_metadata.status = status
41
+ procedure_metadata.waiting_on_message_id = waiting_on_message_id
42
+ self.save_procedure_metadata(procedure_id, procedure_metadata)
43
43
 
44
44
  def get_state(self, procedure_id: str) -> Dict[str, Any]:
45
45
  """Get mutable state dictionary."""
46
- metadata = self.load_procedure_metadata(procedure_id)
47
- return metadata.state
46
+ procedure_metadata = self.load_procedure_metadata(procedure_id)
47
+ return procedure_metadata.state
48
48
 
49
49
  def set_state(self, procedure_id: str, state: Dict[str, Any]) -> None:
50
50
  """Set mutable state dictionary."""
51
- metadata = self.load_procedure_metadata(procedure_id)
52
- metadata.state = state
53
- self.save_procedure_metadata(procedure_id, metadata)
51
+ procedure_metadata = self.load_procedure_metadata(procedure_id)
52
+ procedure_metadata.state = state
53
+ self.save_procedure_metadata(procedure_id, procedure_metadata)