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
@@ -5,9 +5,9 @@ This module provides the infrastructure for declaring, creating, and managing
5
5
  external dependencies (HTTP clients, databases, caches) that procedures need.
6
6
  """
7
7
 
8
- from enum import Enum
9
- from typing import Dict, Any
10
8
  import logging
9
+ from enum import Enum
10
+ from typing import Any
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
@@ -29,13 +29,13 @@ class ResourceFactory:
29
29
  """
30
30
 
31
31
  @staticmethod
32
- async def create(resource_type: str, config: Dict[str, Any]) -> Any:
32
+ async def create(resource_type: str, resource_config: dict[str, Any]) -> Any:
33
33
  """
34
34
  Create a real resource from configuration.
35
35
 
36
36
  Args:
37
37
  resource_type: Type of resource (http_client, postgres, redis)
38
- config: Configuration dictionary from procedure DSL
38
+ resource_config: Configuration dictionary from procedure DSL
39
39
 
40
40
  Returns:
41
41
  Configured resource instance
@@ -45,16 +45,15 @@ class ResourceFactory:
45
45
  ImportError: If required library is not installed
46
46
  """
47
47
  if resource_type == ResourceType.HTTP_CLIENT.value:
48
- return await ResourceFactory._create_http_client(config)
49
- elif resource_type == ResourceType.POSTGRES.value:
50
- return await ResourceFactory._create_postgres(config)
51
- elif resource_type == ResourceType.REDIS.value:
52
- return await ResourceFactory._create_redis(config)
53
- else:
54
- raise ValueError(f"Unknown resource type: {resource_type}")
48
+ return await ResourceFactory._create_http_client(resource_config)
49
+ if resource_type == ResourceType.POSTGRES.value:
50
+ return await ResourceFactory._create_postgres(resource_config)
51
+ if resource_type == ResourceType.REDIS.value:
52
+ return await ResourceFactory._create_redis(resource_config)
53
+ raise ValueError(f"Unknown resource type: {resource_type}")
55
54
 
56
55
  @staticmethod
57
- async def _create_http_client(config: Dict[str, Any]) -> Any:
56
+ async def _create_http_client(resource_config: dict[str, Any]) -> Any:
58
57
  """Create HTTP client (httpx.AsyncClient)."""
59
58
  try:
60
59
  import httpx
@@ -63,16 +62,20 @@ class ResourceFactory:
63
62
  "httpx is required for HTTP client dependencies. Install it with: pip install httpx"
64
63
  )
65
64
 
66
- base_url = config.get("base_url")
67
- headers = config.get("headers", {})
68
- timeout = config.get("timeout", 30.0)
65
+ base_url = resource_config.get("base_url")
66
+ headers = resource_config.get("headers", {})
67
+ timeout_seconds = resource_config.get("timeout", 30.0)
69
68
 
70
- logger.info(f"Creating HTTP client for base_url={base_url}")
69
+ logger.info("Creating HTTP client for base_url=%s", base_url)
71
70
 
72
- return httpx.AsyncClient(base_url=base_url, headers=headers, timeout=timeout)
71
+ return httpx.AsyncClient(
72
+ base_url=base_url,
73
+ headers=headers,
74
+ timeout=timeout_seconds,
75
+ )
73
76
 
74
77
  @staticmethod
75
- async def _create_postgres(config: Dict[str, Any]) -> Any:
78
+ async def _create_postgres(resource_config: dict[str, Any]) -> Any:
76
79
  """Create PostgreSQL connection pool (asyncpg.Pool)."""
77
80
  try:
78
81
  import asyncpg
@@ -82,18 +85,18 @@ class ResourceFactory:
82
85
  "Install it with: pip install asyncpg"
83
86
  )
84
87
 
85
- connection_string = config["connection_string"]
86
- pool_size = config.get("pool_size", 10)
87
- max_pool_size = config.get("max_pool_size", 20)
88
+ connection_string = resource_config["connection_string"]
89
+ pool_size = resource_config.get("pool_size", 10)
90
+ max_pool_size = resource_config.get("max_pool_size", 20)
88
91
 
89
- logger.info(f"Creating PostgreSQL pool with size={pool_size}")
92
+ logger.info("Creating PostgreSQL pool with size=%s", pool_size)
90
93
 
91
94
  return await asyncpg.create_pool(
92
95
  connection_string, min_size=pool_size, max_size=max_pool_size
93
96
  )
94
97
 
95
98
  @staticmethod
96
- async def _create_redis(config: Dict[str, Any]) -> Any:
99
+ async def _create_redis(resource_config: dict[str, Any]) -> Any:
97
100
  """Create Redis client (redis.asyncio.Redis)."""
98
101
  try:
99
102
  import redis.asyncio as redis
@@ -102,14 +105,14 @@ class ResourceFactory:
102
105
  "redis is required for Redis dependencies. Install it with: pip install redis"
103
106
  )
104
107
 
105
- url = config["url"]
108
+ url = resource_config["url"]
106
109
 
107
- logger.info(f"Creating Redis client for url={url}")
110
+ logger.info("Creating Redis client for url=%s", url)
108
111
 
109
112
  return redis.from_url(url, encoding="utf-8", decode_responses=True)
110
113
 
111
114
  @staticmethod
112
- async def create_all(dependencies_config: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
115
+ async def create_all(dependencies_config: dict[str, dict[str, Any]]) -> dict[str, Any]:
113
116
  """
114
117
  Create all dependencies from configuration.
115
118
 
@@ -119,15 +122,21 @@ class ResourceFactory:
119
122
  Returns:
120
123
  Dict mapping dependency name to created resource
121
124
  """
122
- resources = {}
125
+ resources: dict[str, Any] = {}
123
126
 
124
- for name, config in dependencies_config.items():
125
- resource_type = config.get("type")
127
+ for dependency_name, dependency_config in dependencies_config.items():
128
+ resource_type = dependency_config.get("type")
126
129
  if not resource_type:
127
- raise ValueError(f"Dependency '{name}' missing 'type' field")
130
+ raise ValueError(f"Dependency '{dependency_name}' missing 'type' field")
128
131
 
129
- logger.info(f"Creating dependency '{name}' of type '{resource_type}'")
130
- resources[name] = await ResourceFactory.create(resource_type, config)
132
+ logger.info(
133
+ "Creating dependency '%s' of type '%s'",
134
+ dependency_name,
135
+ resource_type,
136
+ )
137
+ resources[dependency_name] = await ResourceFactory.create(
138
+ resource_type, dependency_config
139
+ )
131
140
 
132
141
  return resources
133
142
 
@@ -141,40 +150,49 @@ class ResourceManager:
141
150
  """
142
151
 
143
152
  def __init__(self):
144
- self.resources: Dict[str, Any] = {}
153
+ self.resources: dict[str, Any] = {}
145
154
 
146
155
  async def add_resource(self, name: str, resource: Any) -> None:
147
156
  """Add a resource to be managed."""
148
157
  self.resources[name] = resource
149
- logger.debug(f"Added resource '{name}' to manager")
158
+ logger.debug("Added resource '%s' to manager", name)
150
159
 
151
160
  async def cleanup(self) -> None:
152
161
  """Clean up all managed resources."""
153
- logger.info(f"Cleaning up {len(self.resources)} resources")
162
+ logger.info("Cleaning up %s resources", len(self.resources))
154
163
 
155
- for name, resource in self.resources.items():
164
+ for resource_name, resource in self.resources.items():
156
165
  try:
157
- await self._cleanup_resource(name, resource)
158
- except Exception as e:
159
- logger.error(f"Error cleaning up resource '{name}': {e}")
160
-
161
- async def _cleanup_resource(self, name: str, resource: Any) -> None:
166
+ await self._cleanup_resource(resource_name, resource)
167
+ except Exception as exception:
168
+ logger.error(
169
+ "Error cleaning up resource '%s': %s",
170
+ resource_name,
171
+ exception,
172
+ )
173
+
174
+ async def _cleanup_resource(self, resource_name: str, resource: Any) -> None:
162
175
  """Clean up a single resource based on its type."""
163
176
  # HTTP client cleanup
164
177
  if hasattr(resource, "aclose"):
165
- logger.debug(f"Closing HTTP client '{name}'")
178
+ logger.debug("Closing HTTP client '%s'", resource_name)
166
179
  await resource.aclose()
180
+ return
167
181
 
168
182
  # PostgreSQL pool cleanup
169
- elif hasattr(resource, "close") and hasattr(resource, "wait_closed"):
170
- logger.debug(f"Closing PostgreSQL pool '{name}'")
183
+ if hasattr(resource, "close") and hasattr(resource, "wait_closed"):
184
+ logger.debug("Closing PostgreSQL pool '%s'", resource_name)
171
185
  await resource.close()
172
186
  await resource.wait_closed()
187
+ return
173
188
 
174
189
  # Redis client cleanup
175
- elif hasattr(resource, "close") and not hasattr(resource, "wait_closed"):
176
- logger.debug(f"Closing Redis client '{name}'")
190
+ if hasattr(resource, "close") and not hasattr(resource, "wait_closed"):
191
+ logger.debug("Closing Redis client '%s'", resource_name)
177
192
  await resource.close()
193
+ return
178
194
 
179
- else:
180
- logger.warning(f"Unknown resource type for '{name}', no cleanup performed")
195
+ logger.warning(
196
+ "Unknown resource type for '%s', no cleanup performed",
197
+ resource_name,
198
+ )