tsugite-cli 0.3.3__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 (101) hide show
  1. tsugite/__init__.py +6 -0
  2. tsugite/agent_composition.py +163 -0
  3. tsugite/agent_inheritance.py +479 -0
  4. tsugite/agent_preparation.py +236 -0
  5. tsugite/agent_runner/__init__.py +45 -0
  6. tsugite/agent_runner/helpers.py +106 -0
  7. tsugite/agent_runner/history_integration.py +248 -0
  8. tsugite/agent_runner/metrics.py +100 -0
  9. tsugite/agent_runner/runner.py +1879 -0
  10. tsugite/agent_runner/validation.py +70 -0
  11. tsugite/agent_utils.py +167 -0
  12. tsugite/attachments/__init__.py +65 -0
  13. tsugite/attachments/auto_context.py +199 -0
  14. tsugite/attachments/base.py +34 -0
  15. tsugite/attachments/file.py +51 -0
  16. tsugite/attachments/inline.py +31 -0
  17. tsugite/attachments/storage.py +178 -0
  18. tsugite/attachments/url.py +59 -0
  19. tsugite/attachments/youtube.py +101 -0
  20. tsugite/benchmark/__init__.py +62 -0
  21. tsugite/benchmark/config.py +183 -0
  22. tsugite/benchmark/core.py +292 -0
  23. tsugite/benchmark/discovery.py +377 -0
  24. tsugite/benchmark/evaluators.py +671 -0
  25. tsugite/benchmark/execution.py +657 -0
  26. tsugite/benchmark/metrics.py +204 -0
  27. tsugite/benchmark/reports.py +420 -0
  28. tsugite/benchmark/utils.py +288 -0
  29. tsugite/builtin_agents/chat-assistant.md +53 -0
  30. tsugite/builtin_agents/default.md +140 -0
  31. tsugite/builtin_agents.py +5 -0
  32. tsugite/cache.py +195 -0
  33. tsugite/cli/__init__.py +1042 -0
  34. tsugite/cli/agents.py +148 -0
  35. tsugite/cli/attachments.py +193 -0
  36. tsugite/cli/benchmark.py +663 -0
  37. tsugite/cli/cache.py +113 -0
  38. tsugite/cli/config.py +272 -0
  39. tsugite/cli/helpers.py +534 -0
  40. tsugite/cli/history.py +193 -0
  41. tsugite/cli/init.py +387 -0
  42. tsugite/cli/mcp.py +193 -0
  43. tsugite/cli/tools.py +419 -0
  44. tsugite/config.py +204 -0
  45. tsugite/console.py +48 -0
  46. tsugite/constants.py +21 -0
  47. tsugite/core/__init__.py +19 -0
  48. tsugite/core/agent.py +774 -0
  49. tsugite/core/executor.py +300 -0
  50. tsugite/core/memory.py +67 -0
  51. tsugite/core/tools.py +271 -0
  52. tsugite/docker_cli.py +270 -0
  53. tsugite/events/__init__.py +55 -0
  54. tsugite/events/base.py +46 -0
  55. tsugite/events/bus.py +62 -0
  56. tsugite/events/events.py +224 -0
  57. tsugite/exceptions.py +40 -0
  58. tsugite/history/__init__.py +29 -0
  59. tsugite/history/index.py +210 -0
  60. tsugite/history/models.py +106 -0
  61. tsugite/history/storage.py +157 -0
  62. tsugite/mcp_client.py +219 -0
  63. tsugite/mcp_config.py +174 -0
  64. tsugite/md_agents.py +751 -0
  65. tsugite/models.py +257 -0
  66. tsugite/renderer.py +151 -0
  67. tsugite/shell_tool_config.py +265 -0
  68. tsugite/templates/assistant.md +14 -0
  69. tsugite/tools/__init__.py +265 -0
  70. tsugite/tools/agents.py +312 -0
  71. tsugite/tools/edit_strategies.py +393 -0
  72. tsugite/tools/fs.py +329 -0
  73. tsugite/tools/http.py +239 -0
  74. tsugite/tools/interactive.py +430 -0
  75. tsugite/tools/shell.py +129 -0
  76. tsugite/tools/shell_tools.py +214 -0
  77. tsugite/tools/tasks.py +339 -0
  78. tsugite/tsugite.py +7 -0
  79. tsugite/ui/__init__.py +46 -0
  80. tsugite/ui/base.py +638 -0
  81. tsugite/ui/chat.py +265 -0
  82. tsugite/ui/chat.tcss +92 -0
  83. tsugite/ui/chat_history.py +286 -0
  84. tsugite/ui/helpers.py +102 -0
  85. tsugite/ui/jsonl.py +125 -0
  86. tsugite/ui/live_template.py +529 -0
  87. tsugite/ui/plain.py +419 -0
  88. tsugite/ui/textual_chat.py +642 -0
  89. tsugite/ui/textual_handler.py +225 -0
  90. tsugite/ui/widgets/__init__.py +6 -0
  91. tsugite/ui/widgets/base_scroll_log.py +27 -0
  92. tsugite/ui/widgets/message_list.py +121 -0
  93. tsugite/ui/widgets/thought_log.py +80 -0
  94. tsugite/ui_context.py +90 -0
  95. tsugite/utils.py +367 -0
  96. tsugite/xdg.py +104 -0
  97. tsugite_cli-0.3.3.dist-info/METADATA +325 -0
  98. tsugite_cli-0.3.3.dist-info/RECORD +101 -0
  99. tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
  100. tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
  101. tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
@@ -0,0 +1,300 @@
1
+ """Code execution backends for agents.
2
+
3
+ Provides local execution using Python's exec().
4
+ WARNING: Not secure! Only use for development.
5
+
6
+ Maintains state (variables persist between runs).
7
+ """
8
+
9
+ import ast
10
+ import io
11
+ import pprint
12
+ import sys
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ PPRINT_WIDTH = 100
18
+
19
+
20
+ @dataclass
21
+ class ExecutionResult:
22
+ """Result from code execution."""
23
+
24
+ output: str
25
+ error: Optional[str]
26
+ stdout: str
27
+ stderr: str
28
+ final_answer: Optional[Any] = None
29
+ tools_called: List[str] = field(default_factory=list)
30
+
31
+
32
+ class CodeExecutor(ABC):
33
+ """Abstract interface for code execution.
34
+
35
+ Defines the contract for code executors used by agents.
36
+ """
37
+
38
+ @abstractmethod
39
+ async def execute(self, code: str) -> ExecutionResult:
40
+ """Execute Python code and return results.
41
+
42
+ Args:
43
+ code: Python code to execute
44
+
45
+ Returns:
46
+ ExecutionResult with output, errors, etc.
47
+ """
48
+ pass
49
+
50
+ @abstractmethod
51
+ async def send_variables(self, variables: Dict[str, Any]):
52
+ """Inject variables into execution namespace.
53
+
54
+ This is used for multi-step agents to pass results between steps.
55
+
56
+ Args:
57
+ variables: Dict of {name: value} to make available in code
58
+ """
59
+ pass
60
+
61
+
62
+ class LocalExecutor(CodeExecutor):
63
+ """Simple local code executor using Python's exec().
64
+
65
+ WARNING: This is NOT secure! Only use for development.
66
+
67
+ Uses Python's built-in exec() function. State persists between
68
+ runs by maintaining a shared namespace dict.
69
+
70
+ Example:
71
+ executor = LocalExecutor()
72
+
73
+ # First run
74
+ result = await executor.execute("x = 5")
75
+
76
+ # Second run - x still exists!
77
+ result = await executor.execute("print(x + 3)") # Prints: 8
78
+ """
79
+
80
+ def __init__(self):
81
+ """Initialize executor with empty namespace."""
82
+ self.namespace = {}
83
+ self._final_answer_value = None
84
+ self._tools_called = []
85
+
86
+ # Inject final_answer function into namespace
87
+ def final_answer(value):
88
+ self._final_answer_value = value
89
+ # Don't print here - the UI handler will display it properly via FINAL_ANSWER event
90
+
91
+ self.namespace["final_answer"] = final_answer
92
+
93
+ def _split_code_for_last_expr(self, code: str) -> tuple[str, Optional[str]]:
94
+ """Split code into setup and last expression if applicable.
95
+
96
+ If the last statement is an expression, return (setup, last_expr).
97
+ Otherwise, return (code, None).
98
+
99
+ Args:
100
+ code: Python code to analyze
101
+
102
+ Returns:
103
+ Tuple of (setup_code, last_expression_or_none)
104
+ """
105
+ try:
106
+ tree = ast.parse(code)
107
+ if not tree.body:
108
+ return (code, None)
109
+
110
+ # Check if last statement is an expression
111
+ last_node = tree.body[-1]
112
+ if not isinstance(last_node, ast.Expr):
113
+ return (code, None)
114
+
115
+ # Split: everything except last statement vs last statement
116
+ if len(tree.body) == 1:
117
+ # Only one statement and it's an expression
118
+ setup_code = ""
119
+ last_expr = ast.unparse(last_node.value) # Unparse the expression itself
120
+ else:
121
+ # Multiple statements - use ast.unparse to reconstruct
122
+ setup_tree = ast.Module(body=tree.body[:-1], type_ignores=[])
123
+ setup_code = ast.unparse(setup_tree)
124
+ last_expr = ast.unparse(last_node.value)
125
+
126
+ return (setup_code, last_expr)
127
+
128
+ except SyntaxError:
129
+ # Code has syntax errors, let exec handle it normally
130
+ return (code, None)
131
+
132
+ def _format_value(self, value: Any) -> str:
133
+ """Format a value for display.
134
+
135
+ Uses pprint for complex objects, repr for simple ones.
136
+
137
+ Args:
138
+ value: Value to format
139
+
140
+ Returns:
141
+ Formatted string representation
142
+ """
143
+ # For dicts and lists, use pprint for nice formatting
144
+ if isinstance(value, (dict, list, tuple, set)):
145
+ return pprint.pformat(value, width=PPRINT_WIDTH, compact=False)
146
+ else:
147
+ return repr(value)
148
+
149
+ def _check_code_safety(self, code: str) -> Optional[str]:
150
+ """Check code for anti-patterns before execution.
151
+
152
+ Detects common mistakes where LLMs use built-in Python functions
153
+ instead of the provided tools.
154
+
155
+ Args:
156
+ code: Python code to check
157
+
158
+ Returns:
159
+ Error message string if violations found, None if code is safe
160
+ """
161
+ import re
162
+
163
+ # Check for file operations using open() instead of read_file/write_file
164
+ # Match: word boundary before 'open', then whitespace, then '('
165
+ # This avoids false positives like 'reopen(' or 'is_open()'
166
+ # Pattern explanation:
167
+ # \b - word boundary (not preceded by alphanumeric or _)
168
+ # open - literal 'open'
169
+ # \s* - optional whitespace
170
+ # \( - opening parenthesis
171
+ if re.search(r"\bopen\s*\(", code):
172
+ # Quick check to avoid false positives in strings/comments
173
+ # Remove strings and comments before checking
174
+ # This is a simple heuristic - not perfect but good enough
175
+ code_without_strings = re.sub(r'["\'].*?["\']', "", code) # Remove string contents
176
+ code_without_comments = re.sub(r"#.*$", "", code_without_strings, flags=re.MULTILINE) # Remove comments
177
+
178
+ # Check again after removing strings/comments
179
+ if re.search(r"\bopen\s*\(", code_without_comments):
180
+ return """Code Safety Check Failed: Detected use of 'open()' for file operations.
181
+
182
+ Please use the provided tools instead:
183
+ - read_file(path) - to read file contents
184
+ - write_file(path, content) - to write to files
185
+
186
+ Example:
187
+ # Instead of:
188
+ with open('file.txt') as f:
189
+ content = f.read()
190
+
191
+ # Use:
192
+ content = read_file('file.txt')
193
+
194
+ # Instead of:
195
+ with open('output.txt', 'w') as f:
196
+ f.write(data)
197
+
198
+ # Use:
199
+ write_file('output.txt', data)"""
200
+
201
+ # Code passed all safety checks
202
+ return None
203
+
204
+ async def execute(self, code: str) -> ExecutionResult:
205
+ """Execute code using exec().
206
+
207
+ Automatically displays the value of the last expression (REPL-like behavior).
208
+
209
+ Args:
210
+ code: Python code to execute
211
+
212
+ Returns:
213
+ ExecutionResult with output, error, stdout, stderr, final_answer, and tools_called
214
+ """
215
+ # Reset final answer and tool tracking
216
+ self._final_answer_value = None
217
+ self._tools_called = []
218
+
219
+ # Check code safety before execution
220
+ safety_error = self._check_code_safety(code)
221
+ if safety_error:
222
+ return ExecutionResult(
223
+ output="",
224
+ error=safety_error,
225
+ stdout="",
226
+ stderr=safety_error,
227
+ final_answer=None,
228
+ tools_called=[],
229
+ )
230
+
231
+ # Capture stdout/stderr
232
+ stdout_capture = io.StringIO()
233
+ stderr_capture = io.StringIO()
234
+
235
+ old_stdout = sys.stdout
236
+ old_stderr = sys.stderr
237
+
238
+ try:
239
+ # Redirect output
240
+ sys.stdout = stdout_capture
241
+ sys.stderr = stderr_capture
242
+
243
+ # Check if we should handle the last expression specially
244
+ setup_code, last_expr = self._split_code_for_last_expr(code)
245
+
246
+ if last_expr:
247
+ # Execute setup code first (if any)
248
+ if setup_code.strip():
249
+ exec(setup_code, self.namespace)
250
+
251
+ # Evaluate the last expression and capture its value
252
+ result = eval(last_expr, self.namespace)
253
+
254
+ # Display the result if it's not None
255
+ if result is not None:
256
+ formatted = self._format_value(result)
257
+ print(formatted)
258
+ else:
259
+ # No special handling needed, execute normally
260
+ exec(code, self.namespace)
261
+
262
+ # Get output
263
+ output = stdout_capture.getvalue()
264
+ stderr_output = stderr_capture.getvalue()
265
+
266
+ return ExecutionResult(
267
+ output=output,
268
+ error=None,
269
+ stdout=output,
270
+ stderr=stderr_output,
271
+ final_answer=self._final_answer_value,
272
+ tools_called=self._tools_called.copy(),
273
+ )
274
+
275
+ except Exception as e:
276
+ # Code execution failed
277
+ error_msg = f"{type(e).__name__}: {str(e)}"
278
+ return ExecutionResult(
279
+ output=stdout_capture.getvalue(),
280
+ error=error_msg,
281
+ stdout=stdout_capture.getvalue(),
282
+ stderr=stderr_capture.getvalue() + "\n" + error_msg,
283
+ final_answer=None,
284
+ tools_called=self._tools_called.copy(),
285
+ )
286
+
287
+ finally:
288
+ # Restore stdout/stderr
289
+ sys.stdout = old_stdout
290
+ sys.stderr = old_stderr
291
+
292
+ async def send_variables(self, variables: Dict[str, Any]):
293
+ """Inject variables into namespace.
294
+
295
+ Simply updates the shared namespace dict.
296
+
297
+ Args:
298
+ variables: Dict of {name: value} to inject
299
+ """
300
+ self.namespace.update(variables)
tsugite/core/memory.py ADDED
@@ -0,0 +1,67 @@
1
+ """Agent memory system.
2
+
3
+ Tracks execution history for building conversation context.
4
+ """
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, List, Optional
8
+
9
+
10
+ @dataclass
11
+ class StepResult:
12
+ """Result from a single agent step."""
13
+
14
+ step_number: int
15
+ thought: str
16
+ code: str
17
+ output: str
18
+ error: Optional[str] = None
19
+ tools_called: List[str] = field(default_factory=list)
20
+
21
+
22
+ @dataclass
23
+ class AgentMemory:
24
+ """Memory of agent execution.
25
+
26
+ Stores:
27
+ - The task
28
+ - All steps (thought/code/observation)
29
+ - Reasoning content (for o1/o3/Claude)
30
+ - Final answer
31
+ """
32
+
33
+ task: str = ""
34
+ steps: List[StepResult] = field(default_factory=list)
35
+ reasoning_history: List[str] = field(default_factory=list)
36
+ final_answer: Optional[Any] = None
37
+
38
+ def add_task(self, task: str) -> None:
39
+ """Set the task."""
40
+ self.task = task
41
+
42
+ def add_step(
43
+ self,
44
+ thought: str,
45
+ code: str,
46
+ output: str,
47
+ error: Optional[str] = None,
48
+ tools_called: Optional[List[str]] = None,
49
+ ) -> None:
50
+ """Add a step to history."""
51
+ step = StepResult(
52
+ step_number=len(self.steps) + 1,
53
+ thought=thought,
54
+ code=code,
55
+ output=output,
56
+ error=error,
57
+ tools_called=tools_called or [],
58
+ )
59
+ self.steps.append(step)
60
+
61
+ def add_reasoning(self, reasoning: str) -> None:
62
+ """Add reasoning content (from o1/o3/Claude thinking)."""
63
+ self.reasoning_history.append(reasoning)
64
+
65
+ def add_final_answer(self, answer: Any) -> None:
66
+ """Set final answer."""
67
+ self.final_answer = answer
tsugite/core/tools.py ADDED
@@ -0,0 +1,271 @@
1
+ """Tool system for agents.
2
+
3
+ Provides a simple Tool class and converters to wrap:
4
+ - Existing tsugite tools
5
+ - MCP tools
6
+ - Custom functions
7
+ """
8
+
9
+ import asyncio
10
+ import inspect
11
+ from dataclasses import dataclass
12
+ from typing import Any, Callable, Dict, Optional
13
+
14
+
15
+ @dataclass
16
+ class Tool:
17
+ """A tool that agents can use.
18
+
19
+ Tools are Python functions with:
20
+ - name: Identifier
21
+ - description: What it does
22
+ - parameters: JSON schema of arguments
23
+ - function: The actual callable
24
+
25
+ Example:
26
+ def add(a: int, b: int) -> int:
27
+ '''Add two numbers'''
28
+ return a + b
29
+
30
+ tool = Tool(
31
+ name="add",
32
+ description="Add two numbers",
33
+ parameters={
34
+ "type": "object",
35
+ "properties": {
36
+ "a": {"type": "integer"},
37
+ "b": {"type": "integer"}
38
+ },
39
+ "required": ["a", "b"]
40
+ },
41
+ function=add
42
+ )
43
+ """
44
+
45
+ name: str
46
+ description: str
47
+ parameters: Dict[str, Any]
48
+ function: Callable
49
+
50
+ def to_code_prompt(self) -> str:
51
+ """Format tool as Python function for system prompt.
52
+
53
+ The agent sees tools as Python functions it can call.
54
+
55
+ Returns:
56
+ str: Python function signature with docstring
57
+
58
+ Example output:
59
+ def add(a: int, b: int) -> Any:
60
+ '''Add two numbers
61
+
62
+ Args:
63
+ a: First number
64
+ b: Second number
65
+ '''
66
+ pass
67
+ """
68
+ # Extract parameter info from JSON schema
69
+ props = self.parameters.get("properties", {})
70
+ required = self.parameters.get("required", [])
71
+
72
+ # Build function signature
73
+ params = []
74
+ for param_name, param_info in props.items():
75
+ # Get type (default to Any)
76
+ param_type = param_info.get("type", "Any")
77
+
78
+ # Convert JSON schema types to Python types
79
+ type_map = {
80
+ "string": "str",
81
+ "integer": "int",
82
+ "number": "float",
83
+ "boolean": "bool",
84
+ "array": "list",
85
+ "object": "dict",
86
+ }
87
+ python_type = type_map.get(param_type, "Any")
88
+
89
+ # Add to params list
90
+ params.append(f"{param_name}: {python_type}")
91
+
92
+ # Add * to indicate keyword-only parameters
93
+ param_str = f"*, {', '.join(params)}" if params else ""
94
+
95
+ # Build docstring with parameter descriptions
96
+ param_docs = []
97
+ for param_name, param_info in props.items():
98
+ desc = param_info.get("description", "")
99
+ required_marker = " (required)" if param_name in required else ""
100
+ param_docs.append(f" {param_name}: {desc}{required_marker}")
101
+
102
+ param_doc_str = "\n".join(param_docs) if param_docs else " No parameters"
103
+
104
+ # Build usage example with keyword arguments
105
+ example_args = []
106
+ for param_name, param_info in props.items():
107
+ # Create example values based on type
108
+ param_type = param_info.get("type", "string")
109
+ if param_type == "string":
110
+ example_value = f'"{param_name}_value"'
111
+ elif param_type == "integer":
112
+ example_value = "42"
113
+ elif param_type == "number":
114
+ example_value = "3.14"
115
+ elif param_type == "boolean":
116
+ example_value = "True"
117
+ elif param_type == "array":
118
+ example_value = '["item1", "item2"]'
119
+ elif param_type == "object":
120
+ example_value = '{"key": "value"}'
121
+ else:
122
+ example_value = "value"
123
+
124
+ example_args.append(f"{param_name}={example_value}")
125
+
126
+ usage_example = (
127
+ f"result = {self.name}({', '.join(example_args)})" if example_args else f"result = {self.name}()"
128
+ )
129
+
130
+ # Build full function definition
131
+ return f'''def {self.name}({param_str}) -> Any:
132
+ """{self.description}
133
+
134
+ Args:
135
+ {param_doc_str}
136
+
137
+ Usage:
138
+ {usage_example}
139
+ """
140
+ pass
141
+ '''
142
+
143
+ async def execute(self, **kwargs) -> Any:
144
+ """Execute the tool with given arguments.
145
+
146
+ Handles both sync and async functions.
147
+
148
+ Args:
149
+ **kwargs: Arguments to pass to the tool
150
+
151
+ Returns:
152
+ Tool execution result
153
+ """
154
+ # Check if function is async
155
+ if asyncio.iscoroutinefunction(self.function):
156
+ return await self.function(**kwargs)
157
+ else:
158
+ # Run sync function
159
+ return self.function(**kwargs)
160
+
161
+
162
+ def create_tool_from_function(func: Callable, name: Optional[str] = None, description: Optional[str] = None) -> Tool:
163
+ """Create a Tool from a Python function.
164
+
165
+ Extracts parameter info from function signature and docstring.
166
+
167
+ Args:
168
+ func: The function to wrap
169
+ name: Tool name (defaults to function name)
170
+ description: Tool description (defaults to docstring)
171
+
172
+ Returns:
173
+ Tool: Wrapped function
174
+
175
+ Example:
176
+ def multiply(a: int, b: int) -> int:
177
+ '''Multiply two numbers'''
178
+ return a * b
179
+
180
+ tool = create_tool_from_function(multiply)
181
+ """
182
+ # Get name and description
183
+ tool_name = name or func.__name__
184
+ tool_description = description or (func.__doc__ or "").strip().split("\n")[0]
185
+
186
+ # Extract parameters from signature
187
+ sig = inspect.signature(func)
188
+ parameters = {"type": "object", "properties": {}, "required": []}
189
+
190
+ for param_name, param in sig.parameters.items():
191
+ # Skip self/cls
192
+ if param_name in ("self", "cls"):
193
+ continue
194
+
195
+ # Get type annotation and convert to JSON schema type
196
+ if param.annotation != inspect.Parameter.empty:
197
+ # Convert Python type to JSON schema type
198
+ type_map = {
199
+ str: "string",
200
+ int: "integer",
201
+ float: "number",
202
+ bool: "boolean",
203
+ list: "array",
204
+ dict: "object",
205
+ }
206
+ param_type = type_map.get(param.annotation)
207
+
208
+ if param_type:
209
+ # Known type - add type constraint
210
+ parameters["properties"][param_name] = {"type": param_type}
211
+ else:
212
+ # Unknown type - omit type constraint (accepts any value, but valid JSON Schema)
213
+ parameters["properties"][param_name] = {}
214
+ else:
215
+ # No annotation - omit type constraint
216
+ parameters["properties"][param_name] = {}
217
+
218
+ # Check if required (no default value)
219
+ if param.default == inspect.Parameter.empty:
220
+ parameters["required"].append(param_name)
221
+
222
+ return Tool(
223
+ name=tool_name,
224
+ description=tool_description,
225
+ parameters=parameters,
226
+ function=func,
227
+ )
228
+
229
+
230
+ def create_tool_from_tsugite(tool_name: str) -> Tool:
231
+ """Convert existing tsugite tool to Tool object.
232
+
233
+ Tsugite has its own tool registry. This function wraps those
234
+ tools in our Tool interface.
235
+
236
+ Args:
237
+ tool_name: Name of tool in tsugite registry
238
+
239
+ Returns:
240
+ Tool: Wrapped tsugite tool
241
+
242
+ Example:
243
+ tool = create_tool_from_tsugite("read_file")
244
+ result = await tool.execute(file_path="/path/to/file")
245
+ """
246
+ from tsugite.tools import call_tool, get_tool
247
+
248
+ # Get tool info from registry
249
+ tool_info = get_tool(tool_name)
250
+
251
+ # Create async wrapper function that preserves signature
252
+ sig = inspect.signature(tool_info.func)
253
+
254
+ async def tool_wrapper(**kwargs):
255
+ """Wrapper that calls tsugite tool."""
256
+ # call_tool might be sync or async, handle both
257
+ result = call_tool(tool_name, **kwargs)
258
+ # If result is a coroutine, await it
259
+ if inspect.iscoroutine(result):
260
+ return await result
261
+ return result
262
+
263
+ # Set the signature on the wrapper
264
+ tool_wrapper.__signature__ = sig
265
+
266
+ # Create Tool using function converter
267
+ return create_tool_from_function(
268
+ tool_wrapper,
269
+ name=tool_name,
270
+ description=tool_info.description,
271
+ )