tactus 0.31.2__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 (160) hide show
  1. tactus/__init__.py +49 -0
  2. tactus/adapters/__init__.py +9 -0
  3. tactus/adapters/broker_log.py +76 -0
  4. tactus/adapters/cli_hitl.py +189 -0
  5. tactus/adapters/cli_log.py +223 -0
  6. tactus/adapters/cost_collector_log.py +56 -0
  7. tactus/adapters/file_storage.py +367 -0
  8. tactus/adapters/http_callback_log.py +109 -0
  9. tactus/adapters/ide_log.py +71 -0
  10. tactus/adapters/lua_tools.py +336 -0
  11. tactus/adapters/mcp.py +289 -0
  12. tactus/adapters/mcp_manager.py +196 -0
  13. tactus/adapters/memory.py +53 -0
  14. tactus/adapters/plugins.py +419 -0
  15. tactus/backends/http_backend.py +58 -0
  16. tactus/backends/model_backend.py +35 -0
  17. tactus/backends/pytorch_backend.py +110 -0
  18. tactus/broker/__init__.py +12 -0
  19. tactus/broker/client.py +247 -0
  20. tactus/broker/protocol.py +183 -0
  21. tactus/broker/server.py +1123 -0
  22. tactus/broker/stdio.py +12 -0
  23. tactus/cli/__init__.py +7 -0
  24. tactus/cli/app.py +2245 -0
  25. tactus/cli/commands/__init__.py +0 -0
  26. tactus/core/__init__.py +32 -0
  27. tactus/core/config_manager.py +790 -0
  28. tactus/core/dependencies/__init__.py +14 -0
  29. tactus/core/dependencies/registry.py +180 -0
  30. tactus/core/dsl_stubs.py +2117 -0
  31. tactus/core/exceptions.py +66 -0
  32. tactus/core/execution_context.py +480 -0
  33. tactus/core/lua_sandbox.py +508 -0
  34. tactus/core/message_history_manager.py +236 -0
  35. tactus/core/mocking.py +286 -0
  36. tactus/core/output_validator.py +291 -0
  37. tactus/core/registry.py +499 -0
  38. tactus/core/runtime.py +2907 -0
  39. tactus/core/template_resolver.py +142 -0
  40. tactus/core/yaml_parser.py +301 -0
  41. tactus/docker/Dockerfile +61 -0
  42. tactus/docker/entrypoint.sh +69 -0
  43. tactus/dspy/__init__.py +39 -0
  44. tactus/dspy/agent.py +1144 -0
  45. tactus/dspy/broker_lm.py +181 -0
  46. tactus/dspy/config.py +212 -0
  47. tactus/dspy/history.py +196 -0
  48. tactus/dspy/module.py +405 -0
  49. tactus/dspy/prediction.py +318 -0
  50. tactus/dspy/signature.py +185 -0
  51. tactus/formatting/__init__.py +7 -0
  52. tactus/formatting/formatter.py +437 -0
  53. tactus/ide/__init__.py +9 -0
  54. tactus/ide/coding_assistant.py +343 -0
  55. tactus/ide/server.py +2223 -0
  56. tactus/primitives/__init__.py +49 -0
  57. tactus/primitives/control.py +168 -0
  58. tactus/primitives/file.py +229 -0
  59. tactus/primitives/handles.py +378 -0
  60. tactus/primitives/host.py +94 -0
  61. tactus/primitives/human.py +342 -0
  62. tactus/primitives/json.py +189 -0
  63. tactus/primitives/log.py +187 -0
  64. tactus/primitives/message_history.py +157 -0
  65. tactus/primitives/model.py +163 -0
  66. tactus/primitives/procedure.py +564 -0
  67. tactus/primitives/procedure_callable.py +318 -0
  68. tactus/primitives/retry.py +155 -0
  69. tactus/primitives/session.py +152 -0
  70. tactus/primitives/state.py +182 -0
  71. tactus/primitives/step.py +209 -0
  72. tactus/primitives/system.py +93 -0
  73. tactus/primitives/tool.py +375 -0
  74. tactus/primitives/tool_handle.py +279 -0
  75. tactus/primitives/toolset.py +229 -0
  76. tactus/protocols/__init__.py +38 -0
  77. tactus/protocols/chat_recorder.py +81 -0
  78. tactus/protocols/config.py +97 -0
  79. tactus/protocols/cost.py +31 -0
  80. tactus/protocols/hitl.py +71 -0
  81. tactus/protocols/log_handler.py +27 -0
  82. tactus/protocols/models.py +355 -0
  83. tactus/protocols/result.py +33 -0
  84. tactus/protocols/storage.py +90 -0
  85. tactus/providers/__init__.py +13 -0
  86. tactus/providers/base.py +92 -0
  87. tactus/providers/bedrock.py +117 -0
  88. tactus/providers/google.py +105 -0
  89. tactus/providers/openai.py +98 -0
  90. tactus/sandbox/__init__.py +63 -0
  91. tactus/sandbox/config.py +171 -0
  92. tactus/sandbox/container_runner.py +1099 -0
  93. tactus/sandbox/docker_manager.py +433 -0
  94. tactus/sandbox/entrypoint.py +227 -0
  95. tactus/sandbox/protocol.py +213 -0
  96. tactus/stdlib/__init__.py +10 -0
  97. tactus/stdlib/io/__init__.py +13 -0
  98. tactus/stdlib/io/csv.py +88 -0
  99. tactus/stdlib/io/excel.py +136 -0
  100. tactus/stdlib/io/file.py +90 -0
  101. tactus/stdlib/io/fs.py +154 -0
  102. tactus/stdlib/io/hdf5.py +121 -0
  103. tactus/stdlib/io/json.py +109 -0
  104. tactus/stdlib/io/parquet.py +83 -0
  105. tactus/stdlib/io/tsv.py +88 -0
  106. tactus/stdlib/loader.py +274 -0
  107. tactus/stdlib/tac/tactus/tools/done.tac +33 -0
  108. tactus/stdlib/tac/tactus/tools/log.tac +50 -0
  109. tactus/testing/README.md +273 -0
  110. tactus/testing/__init__.py +61 -0
  111. tactus/testing/behave_integration.py +380 -0
  112. tactus/testing/context.py +486 -0
  113. tactus/testing/eval_models.py +114 -0
  114. tactus/testing/evaluation_runner.py +222 -0
  115. tactus/testing/evaluators.py +634 -0
  116. tactus/testing/events.py +94 -0
  117. tactus/testing/gherkin_parser.py +134 -0
  118. tactus/testing/mock_agent.py +315 -0
  119. tactus/testing/mock_dependencies.py +234 -0
  120. tactus/testing/mock_hitl.py +171 -0
  121. tactus/testing/mock_registry.py +168 -0
  122. tactus/testing/mock_tools.py +133 -0
  123. tactus/testing/models.py +115 -0
  124. tactus/testing/pydantic_eval_runner.py +508 -0
  125. tactus/testing/steps/__init__.py +13 -0
  126. tactus/testing/steps/builtin.py +902 -0
  127. tactus/testing/steps/custom.py +69 -0
  128. tactus/testing/steps/registry.py +68 -0
  129. tactus/testing/test_runner.py +489 -0
  130. tactus/tracing/__init__.py +5 -0
  131. tactus/tracing/trace_manager.py +417 -0
  132. tactus/utils/__init__.py +1 -0
  133. tactus/utils/cost_calculator.py +72 -0
  134. tactus/utils/model_pricing.py +132 -0
  135. tactus/utils/safe_file_library.py +502 -0
  136. tactus/utils/safe_libraries.py +234 -0
  137. tactus/validation/LuaLexerBase.py +66 -0
  138. tactus/validation/LuaParserBase.py +23 -0
  139. tactus/validation/README.md +224 -0
  140. tactus/validation/__init__.py +7 -0
  141. tactus/validation/error_listener.py +21 -0
  142. tactus/validation/generated/LuaLexer.interp +231 -0
  143. tactus/validation/generated/LuaLexer.py +5548 -0
  144. tactus/validation/generated/LuaLexer.tokens +124 -0
  145. tactus/validation/generated/LuaLexerBase.py +66 -0
  146. tactus/validation/generated/LuaParser.interp +173 -0
  147. tactus/validation/generated/LuaParser.py +6439 -0
  148. tactus/validation/generated/LuaParser.tokens +124 -0
  149. tactus/validation/generated/LuaParserBase.py +23 -0
  150. tactus/validation/generated/LuaParserVisitor.py +118 -0
  151. tactus/validation/generated/__init__.py +7 -0
  152. tactus/validation/grammar/LuaLexer.g4 +123 -0
  153. tactus/validation/grammar/LuaParser.g4 +178 -0
  154. tactus/validation/semantic_visitor.py +817 -0
  155. tactus/validation/validator.py +157 -0
  156. tactus-0.31.2.dist-info/METADATA +1809 -0
  157. tactus-0.31.2.dist-info/RECORD +160 -0
  158. tactus-0.31.2.dist-info/WHEEL +4 -0
  159. tactus-0.31.2.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,229 @@
1
+ """
2
+ Toolset Primitive - Manages and composes tool collections in Tactus.
3
+
4
+ Provides first-class support for Pydantic AI's composable toolset architecture.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, Callable
9
+ from pydantic_ai.toolsets import AbstractToolset, CombinedToolset, FilteredToolset
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ToolsetPrimitive:
15
+ """
16
+ Toolset primitive for managing and composing tool collections.
17
+
18
+ Exposes Pydantic AI's toolset composition features to the Lua DSL.
19
+
20
+ Example Lua usage:
21
+ toolset("financial", {type = "plugin", paths = {"./tools/financial"}})
22
+
23
+ local ts = Toolset.get("financial")
24
+ local combined = Toolset.combine(ts1, ts2)
25
+ local filtered = Toolset.filter(ts, function(name) return name:match("^web_") end)
26
+ """
27
+
28
+ def __init__(self, runtime):
29
+ """
30
+ Initialize toolset primitive.
31
+
32
+ Args:
33
+ runtime: TactusRuntime instance for resolving toolset references
34
+ """
35
+ self.runtime = runtime
36
+ self.definitions = {} # name -> toolset config (from DSL)
37
+ logger.debug("ToolsetPrimitive initialized")
38
+
39
+ def define(self, name: str, config: Dict[str, Any]) -> None:
40
+ """
41
+ Register a toolset definition from the DSL.
42
+
43
+ This is called when the Lua DSL uses: toolset("name", {...})
44
+
45
+ Args:
46
+ name: Toolset name
47
+ config: Configuration dict with type and type-specific params
48
+
49
+ Example config:
50
+ {
51
+ "type": "plugin",
52
+ "paths": ["./tools/financial"]
53
+ }
54
+ {
55
+ "type": "mcp",
56
+ "server": "plexus"
57
+ }
58
+ {
59
+ "type": "combined",
60
+ "sources": ["financial", "plexus"]
61
+ }
62
+ """
63
+ self.definitions[name] = config
64
+ logger.info(f"Defined toolset '{name}' of type '{config.get('type')}'")
65
+
66
+ def get(self, name: str) -> AbstractToolset:
67
+ """
68
+ Get a toolset by name.
69
+
70
+ Resolves the toolset from runtime's registered toolsets or creates it
71
+ from a DSL definition.
72
+
73
+ Args:
74
+ name: Toolset name
75
+
76
+ Returns:
77
+ AbstractToolset instance
78
+
79
+ Raises:
80
+ ValueError: If toolset not found
81
+ """
82
+ # Try to resolve from runtime first (config-defined toolsets)
83
+ toolset = self.runtime.resolve_toolset(name)
84
+ if toolset:
85
+ return toolset
86
+
87
+ # Try DSL definitions
88
+ if name in self.definitions:
89
+ return self._create_toolset_from_definition(name, self.definitions[name])
90
+
91
+ raise ValueError(f"Toolset '{name}' not found (not in config or DSL)")
92
+
93
+ def combine(self, *toolsets) -> CombinedToolset:
94
+ """
95
+ Combine multiple toolsets into one.
96
+
97
+ Args:
98
+ *toolsets: Variable number of AbstractToolset instances
99
+
100
+ Returns:
101
+ CombinedToolset containing all input toolsets
102
+ """
103
+ toolset_list = list(toolsets)
104
+ logger.debug(f"Combining {len(toolset_list)} toolsets")
105
+ return CombinedToolset(toolset_list)
106
+
107
+ def filter(self, toolset: AbstractToolset, predicate: Callable[[str], bool]) -> FilteredToolset:
108
+ """
109
+ Filter tools in a toolset based on a predicate function.
110
+
111
+ Args:
112
+ toolset: Toolset to filter
113
+ predicate: Function that takes tool name and returns bool
114
+
115
+ Returns:
116
+ FilteredToolset with only matching tools
117
+
118
+ Example:
119
+ filtered = Toolset.filter(ts, function(name)
120
+ return name:match("^web_")
121
+ end)
122
+ """
123
+
124
+ # Wrap Lua function for Pydantic AI's filter API
125
+ # Pydantic AI's filtered() expects: lambda ctx, tool: bool
126
+ def pydantic_filter(ctx, tool):
127
+ # Call Lua predicate with just the tool name
128
+ return predicate(tool.name)
129
+
130
+ logger.debug("Creating filtered toolset")
131
+ return toolset.filtered(pydantic_filter)
132
+
133
+ def _create_toolset_from_definition(self, name: str, config: Dict[str, Any]) -> AbstractToolset:
134
+ """
135
+ Create a toolset from a DSL definition.
136
+
137
+ Args:
138
+ name: Toolset name
139
+ config: Toolset configuration
140
+
141
+ Returns:
142
+ Created toolset instance
143
+
144
+ Raises:
145
+ ValueError: If toolset type is unknown
146
+ """
147
+ toolset_type = config.get("type")
148
+
149
+ if toolset_type == "plugin":
150
+ return self._create_plugin_toolset(name, config)
151
+ elif toolset_type == "mcp":
152
+ return self._create_mcp_toolset_reference(name, config)
153
+ elif toolset_type == "combined":
154
+ return self._create_combined_toolset(name, config)
155
+ elif toolset_type == "filtered":
156
+ return self._create_filtered_toolset(name, config)
157
+ else:
158
+ raise ValueError(f"Unknown toolset type: {toolset_type}")
159
+
160
+ def _create_plugin_toolset(self, name: str, config: Dict[str, Any]) -> AbstractToolset:
161
+ """Create a plugin toolset from paths."""
162
+ from tactus.adapters.plugins import PluginLoader
163
+
164
+ paths = config.get("paths", [])
165
+ if not paths:
166
+ raise ValueError(f"Plugin toolset '{name}' must specify 'paths'")
167
+
168
+ loader = PluginLoader(tool_primitive=self.runtime.tool_primitive)
169
+ toolset = loader.create_toolset(paths, name=name)
170
+ return toolset
171
+
172
+ def _create_mcp_toolset_reference(self, name: str, config: Dict[str, Any]) -> AbstractToolset:
173
+ """Get reference to an MCP toolset."""
174
+ server_name = config.get("server")
175
+ if not server_name:
176
+ raise ValueError(f"MCP toolset '{name}' must specify 'server' name")
177
+
178
+ # MCP toolsets are created during runtime initialization
179
+ # Look them up from runtime's MCP manager
180
+ if not hasattr(self.runtime, "mcp_manager") or not self.runtime.mcp_manager:
181
+ raise ValueError(f"MCP server '{server_name}' not configured")
182
+
183
+ # Get the toolset by server name
184
+ toolset = self.runtime.mcp_manager.get_toolset_by_name(server_name)
185
+ if toolset:
186
+ logger.info(f"Found MCP toolset for server '{server_name}'")
187
+ return toolset
188
+
189
+ raise ValueError(f"MCP server toolset '{server_name}' not found")
190
+
191
+ def _create_combined_toolset(self, name: str, config: Dict[str, Any]) -> CombinedToolset:
192
+ """Create a combined toolset from sources."""
193
+ sources = config.get("sources", [])
194
+ if not sources:
195
+ raise ValueError(f"Combined toolset '{name}' must specify 'sources'")
196
+
197
+ # Resolve each source toolset
198
+ resolved_toolsets = []
199
+ for source_name in sources:
200
+ toolset = self.get(source_name)
201
+ resolved_toolsets.append(toolset)
202
+
203
+ return CombinedToolset(resolved_toolsets)
204
+
205
+ def _create_filtered_toolset(self, name: str, config: Dict[str, Any]) -> FilteredToolset:
206
+ """Create a filtered toolset."""
207
+ source = config.get("source")
208
+ filter_pattern = config.get("filter")
209
+
210
+ if not source:
211
+ raise ValueError(f"Filtered toolset '{name}' must specify 'source'")
212
+ if not filter_pattern:
213
+ raise ValueError(f"Filtered toolset '{name}' must specify 'filter' pattern")
214
+
215
+ # Get source toolset
216
+ source_toolset = self.get(source)
217
+
218
+ # Create filter function (simple regex match for now)
219
+ import re
220
+
221
+ pattern = re.compile(filter_pattern)
222
+
223
+ def filter_func(ctx, tool):
224
+ return pattern.match(tool.name) is not None
225
+
226
+ return source_toolset.filtered(filter_func)
227
+
228
+ def __repr__(self) -> str:
229
+ return f"ToolsetPrimitive({len(self.definitions)} definitions)"
@@ -0,0 +1,38 @@
1
+ """
2
+ Tactus protocols and models.
3
+
4
+ This module exports all Pydantic models and protocol definitions for Tactus.
5
+ """
6
+
7
+ # Core models
8
+ from tactus.protocols.models import (
9
+ CheckpointEntry,
10
+ ProcedureMetadata,
11
+ HITLRequest,
12
+ HITLResponse,
13
+ ChatMessage,
14
+ )
15
+
16
+ # Protocols
17
+ from tactus.protocols.storage import StorageBackend
18
+ from tactus.protocols.hitl import HITLHandler
19
+ from tactus.protocols.chat_recorder import ChatRecorder
20
+
21
+ # Configuration
22
+ from tactus.protocols.config import TactusConfig, ProcedureConfig
23
+
24
+ __all__ = [
25
+ # Models
26
+ "CheckpointEntry",
27
+ "ProcedureMetadata",
28
+ "HITLRequest",
29
+ "HITLResponse",
30
+ "ChatMessage",
31
+ # Protocols
32
+ "StorageBackend",
33
+ "HITLHandler",
34
+ "ChatRecorder",
35
+ # Config
36
+ "TactusConfig",
37
+ "ProcedureConfig",
38
+ ]
@@ -0,0 +1,81 @@
1
+ """
2
+ Chat recorder protocol for Tactus.
3
+
4
+ Defines the interface for recording conversation history during workflow execution.
5
+ Implementations can store chat logs anywhere (memory, files, databases, APIs, etc.).
6
+ """
7
+
8
+ from typing import Protocol, Optional, Dict, Any
9
+ from tactus.protocols.models import ChatMessage
10
+
11
+
12
+ class ChatRecorder(Protocol):
13
+ """
14
+ Protocol for chat recorders.
15
+
16
+ Implementations record conversation history between agents, tools, and humans.
17
+ This is optional - procedures can run without chat recording.
18
+ """
19
+
20
+ async def start_session(
21
+ self, procedure_id: str, context: Optional[Dict[str, Any]] = None
22
+ ) -> str:
23
+ """
24
+ Start a new chat session for a procedure.
25
+
26
+ Args:
27
+ procedure_id: Unique procedure identifier
28
+ context: Optional context data for the session
29
+
30
+ Returns:
31
+ Session ID
32
+
33
+ Raises:
34
+ ChatRecorderError: If session creation fails
35
+ """
36
+ ...
37
+
38
+ async def record_message(self, message: ChatMessage) -> str:
39
+ """
40
+ Record a message in the current session.
41
+
42
+ Args:
43
+ message: ChatMessage to record
44
+
45
+ Returns:
46
+ Message ID
47
+
48
+ Raises:
49
+ ChatRecorderError: If recording fails
50
+ """
51
+ ...
52
+
53
+ async def end_session(self, session_id: str, status: str = "COMPLETED") -> None:
54
+ """
55
+ End a chat session.
56
+
57
+ Args:
58
+ session_id: Session ID to end
59
+ status: Final status (COMPLETED, FAILED, etc.)
60
+
61
+ Raises:
62
+ ChatRecorderError: If ending session fails
63
+ """
64
+ ...
65
+
66
+ async def get_session_messages(
67
+ self, session_id: str, limit: Optional[int] = None
68
+ ) -> list[ChatMessage]:
69
+ """
70
+ Get messages from a session.
71
+
72
+ Optional method for implementations that support retrieval.
73
+
74
+ Args:
75
+ session_id: Session ID
76
+ limit: Optional limit on number of messages
77
+
78
+ Returns:
79
+ List of ChatMessage objects
80
+ """
81
+ ...
@@ -0,0 +1,97 @@
1
+ """
2
+ Configuration models for Tactus runtime.
3
+
4
+ Defines Pydantic models for runtime configuration and procedure definitions.
5
+ """
6
+
7
+ from typing import Optional, Dict, Any, List
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class TactusConfig(BaseModel):
12
+ """
13
+ Runtime configuration for Tactus.
14
+
15
+ This model defines all runtime settings for a Tactus instance,
16
+ including storage backend, HITL handler, and LLM settings.
17
+ """
18
+
19
+ # Storage backend
20
+ storage_backend: str = Field(
21
+ default="memory", description="Storage backend type: 'memory', 'file', or 'custom'"
22
+ )
23
+ storage_options: Dict[str, Any] = Field(
24
+ default_factory=dict, description="Options passed to storage backend constructor"
25
+ )
26
+
27
+ # HITL handler
28
+ hitl_handler: str = Field(
29
+ default="cli", description="HITL handler type: 'cli', 'none', or 'custom'"
30
+ )
31
+ hitl_options: Dict[str, Any] = Field(
32
+ default_factory=dict, description="Options passed to HITL handler constructor"
33
+ )
34
+
35
+ # Chat recorder
36
+ chat_recorder: Optional[str] = Field(
37
+ default=None, description="Chat recorder type: None, 'memory', 'file', or 'custom'"
38
+ )
39
+ chat_recorder_options: Dict[str, Any] = Field(
40
+ default_factory=dict, description="Options passed to chat recorder constructor"
41
+ )
42
+
43
+ # LLM settings
44
+ openai_api_key: Optional[str] = Field(default=None, description="OpenAI API key for LLM calls")
45
+ default_model: str = Field(default="gpt-4o", description="Default LLM model to use")
46
+ llm_temperature: float = Field(default=0.7, description="Temperature for LLM calls")
47
+
48
+ # Execution settings
49
+ max_iterations: int = Field(default=100, description="Maximum iterations before stopping")
50
+ enable_checkpoints: bool = Field(
51
+ default=True, description="Whether to enable checkpoint/resume"
52
+ )
53
+
54
+ # MCP server
55
+ mcp_server_url: Optional[str] = Field(
56
+ default=None, description="MCP server URL for tool loading"
57
+ )
58
+ mcp_tools: List[str] = Field(default_factory=list, description="List of MCP tools to load")
59
+
60
+
61
+ class ProcedureConfig(BaseModel):
62
+ """
63
+ Parsed procedure configuration from YAML.
64
+
65
+ This model represents a validated procedure definition,
66
+ ready for execution by the Tactus runtime.
67
+ """
68
+
69
+ name: str = Field(..., description="Procedure name")
70
+ version: str = Field(..., description="Procedure version")
71
+ description: Optional[str] = Field(None, description="Optional description")
72
+
73
+ # Parameters (inputs)
74
+ params: Dict[str, Any] = Field(
75
+ default_factory=dict, description="Parameter definitions with types and defaults"
76
+ )
77
+
78
+ # Outputs (schema)
79
+ outputs: Dict[str, Any] = Field(
80
+ default_factory=dict, description="Output schema definitions with types and validation"
81
+ )
82
+
83
+ # Agents
84
+ agents: Dict[str, Any] = Field(..., description="Agent definitions with prompts and tools")
85
+
86
+ # Procedure
87
+ procedure: str = Field(..., description="Lua procedure code")
88
+
89
+ # HITL declarations
90
+ hitl: Dict[str, Any] = Field(default_factory=dict, description="Pre-defined HITL interactions")
91
+
92
+ # Sub-procedures (future)
93
+ procedures: Dict[str, Any] = Field(
94
+ default_factory=dict, description="Inline sub-procedure definitions (future feature)"
95
+ )
96
+
97
+ model_config = {"arbitrary_types_allowed": True}
@@ -0,0 +1,31 @@
1
+ """
2
+ Cost and usage models shared across Tactus.
3
+
4
+ These models are intentionally small and stable so they can be reused anywhere
5
+ that may incur cost (agents, results, future primitives).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+
15
+ class UsageStats(BaseModel):
16
+ """Token usage for a single call or an aggregate."""
17
+
18
+ prompt_tokens: int = Field(default=0, ge=0)
19
+ completion_tokens: int = Field(default=0, ge=0)
20
+ total_tokens: int = Field(default=0, ge=0)
21
+
22
+
23
+ class CostStats(BaseModel):
24
+ """Cost for a single call or an aggregate."""
25
+
26
+ total_cost: float = Field(default=0.0, ge=0.0, description="Total cost in USD")
27
+ prompt_cost: float = Field(default=0.0, ge=0.0)
28
+ completion_cost: float = Field(default=0.0, ge=0.0)
29
+
30
+ model: Optional[str] = Field(default=None, description="Model identifier, if known")
31
+ provider: Optional[str] = Field(default=None, description="Provider identifier, if known")
@@ -0,0 +1,71 @@
1
+ """
2
+ HITL (Human-in-the-Loop) handler protocol for Tactus.
3
+
4
+ Defines the interface for managing human interactions during workflow execution.
5
+ Implementations can use any UI (web, CLI, API, etc.).
6
+ """
7
+
8
+ from typing import Protocol, Optional
9
+ from tactus.protocols.models import HITLRequest, HITLResponse
10
+
11
+
12
+ class HITLHandler(Protocol):
13
+ """
14
+ Protocol for HITL handlers.
15
+
16
+ Implementations manage human interactions (approval, input, review, escalation).
17
+ This allows Tactus to work with any UI or interaction system.
18
+ """
19
+
20
+ def request_interaction(self, procedure_id: str, request: HITLRequest) -> HITLResponse:
21
+ """
22
+ Request human interaction (blocking).
23
+
24
+ This method should:
25
+ 1. Present the request to a human (via UI, CLI, API, etc.)
26
+ 2. Wait for response (with timeout handling)
27
+ 3. Return HITLResponse with the human's answer
28
+
29
+ For exit-and-resume patterns, this may raise
30
+ ProcedureWaitingForHuman to signal workflow suspension.
31
+
32
+ Args:
33
+ procedure_id: Unique procedure identifier
34
+ request: HITLRequest with interaction details
35
+
36
+ Returns:
37
+ HITLResponse with human's answer
38
+
39
+ Raises:
40
+ ProcedureWaitingForHuman: (Optional) To trigger exit-and-resume
41
+ HITLError: If interaction fails
42
+ """
43
+ ...
44
+
45
+ def check_pending_response(self, procedure_id: str, message_id: str) -> Optional[HITLResponse]:
46
+ """
47
+ Check if there's a response to a pending HITL request.
48
+
49
+ Used during resume flow to check if human has responded while
50
+ procedure was suspended.
51
+
52
+ Args:
53
+ procedure_id: Unique procedure identifier
54
+ message_id: Message/request ID to check
55
+
56
+ Returns:
57
+ HITLResponse if response exists, None otherwise
58
+ """
59
+ ...
60
+
61
+ def cancel_pending_request(self, procedure_id: str, message_id: str) -> None:
62
+ """
63
+ Cancel a pending HITL request.
64
+
65
+ Optional method for implementations that support cancellation.
66
+
67
+ Args:
68
+ procedure_id: Unique procedure identifier
69
+ message_id: Message/request ID to cancel
70
+ """
71
+ ...
@@ -0,0 +1,27 @@
1
+ """
2
+ Log handler protocol for Tactus.
3
+
4
+ Defines the interface for handling log events during workflow execution.
5
+ Implementations can render logs differently (CLI with Rich, IDE with React, etc.).
6
+ """
7
+
8
+ from typing import Protocol, Union
9
+ from tactus.protocols.models import LogEvent, ExecutionSummaryEvent
10
+
11
+
12
+ class LogHandler(Protocol):
13
+ """
14
+ Protocol for log handlers.
15
+
16
+ Implementations handle log events from procedures, rendering them
17
+ appropriately for different environments (CLI, IDE, API, etc.).
18
+ """
19
+
20
+ def log(self, event: Union[LogEvent, ExecutionSummaryEvent]) -> None:
21
+ """
22
+ Handle a log or summary event.
23
+
24
+ Args:
25
+ event: Structured event (LogEvent or ExecutionSummaryEvent)
26
+ """
27
+ ...