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
tsugite/models.py ADDED
@@ -0,0 +1,257 @@
1
+ """Model adapters for Tsugite agents."""
2
+
3
+ import os
4
+ import re
5
+ from typing import Optional
6
+
7
+
8
+ def resolve_model_alias(model_string: str) -> str:
9
+ """Resolve a model alias to its full model string.
10
+
11
+ Args:
12
+ model_string: Either an alias name or full model string
13
+
14
+ Returns:
15
+ Full model string (resolved alias or original string)
16
+
17
+ Examples:
18
+ >>> resolve_model_alias("cheap") # if alias exists
19
+ "openai:gpt-4o-mini"
20
+ >>> resolve_model_alias("ollama:qwen2.5-coder:7b")
21
+ "ollama:qwen2.5-coder:7b"
22
+ """
23
+ from tsugite.config import get_model_alias
24
+
25
+ if ":" in model_string:
26
+ return model_string
27
+
28
+ alias_value = get_model_alias(model_string)
29
+ if alias_value:
30
+ return alias_value
31
+
32
+ return model_string
33
+
34
+
35
+ def parse_model_string(model_string: str) -> tuple[str, str, Optional[str]]:
36
+ """Parse Tsugite model string format.
37
+
38
+ Args:
39
+ model_string: Format like "ollama:qwen2.5-coder:14b" or "openai:gpt-4"
40
+
41
+ Returns:
42
+ Tuple of (provider, model_name, variant)
43
+ """
44
+ parts = model_string.split(":")
45
+ if len(parts) < 2:
46
+ raise ValueError(f"Invalid model string format: {model_string}")
47
+
48
+ provider = parts[0]
49
+ model_name = parts[1]
50
+ variant = parts[2] if len(parts) > 2 else None
51
+
52
+ return provider, model_name, variant
53
+
54
+
55
+ def is_reasoning_model_without_stop_support(model_string: str) -> bool:
56
+ """Check if model is a reasoning model that doesn't support stop sequences.
57
+
58
+ OpenAI's o1/o3 reasoning models don't support the stop parameter.
59
+
60
+ Args:
61
+ model_string: Full model string like "openai:o1-mini"
62
+
63
+ Returns:
64
+ True if this is a reasoning model that doesn't support stop sequences
65
+ """
66
+ try:
67
+ provider, model_name, variant = parse_model_string(model_string)
68
+ except ValueError:
69
+ return False
70
+
71
+ # Only OpenAI reasoning models have this limitation
72
+ if provider != "openai":
73
+ return False
74
+
75
+ # Check if it's an o1 or o3 model (with optional version suffix)
76
+ # Matches: o1, o1-mini, o1-preview, o1-2024-12-17, o3, o3-mini, o3-2025-01-31, etc.
77
+ pattern = r"^(o1|o3)(-mini|-preview)?(-\d{4}-\d{2}-\d{2})?$"
78
+ return bool(re.match(pattern, model_name))
79
+
80
+
81
+ def get_model_params(model_string: str, **kwargs) -> dict:
82
+ """Get parameters for direct litellm.acompletion() calls.
83
+
84
+ Returns a dict with model ID and parameters for direct LiteLLM usage.
85
+
86
+ Args:
87
+ model_string: Model specification like "openai:gpt-4o-mini" or an alias
88
+ **kwargs: Additional model parameters (reasoning_effort, response_format, temperature, etc.)
89
+
90
+ Returns:
91
+ Dict with "model" key and all parameters ready for litellm.acompletion()
92
+
93
+ Examples:
94
+ >>> params = get_model_params("openai:gpt-4o-mini", temperature=0.7)
95
+ >>> params["model"]
96
+ 'openai/gpt-4o-mini'
97
+ >>> params["temperature"]
98
+ 0.7
99
+
100
+ >>> # Reasoning models - unsupported params filtered out
101
+ >>> params = get_model_params("openai:o1", temperature=0.7)
102
+ >>> "temperature" not in params # Filtered for o1
103
+ True
104
+ """
105
+ # Resolve aliases and parse model string
106
+ resolved_model = resolve_model_alias(model_string)
107
+ provider, model_name, variant = parse_model_string(resolved_model)
108
+
109
+ # Build parameters dict
110
+ params = dict(kwargs)
111
+
112
+ # Filter parameters for reasoning models
113
+ if is_reasoning_model_without_stop_support(resolved_model):
114
+ params = filter_reasoning_model_params(model_name, params)
115
+
116
+ # Build provider-specific parameters
117
+ if provider == "ollama":
118
+ return build_ollama_params(model_name, variant, params)
119
+ elif provider == "openai":
120
+ return build_openai_params(model_name, params)
121
+ elif provider == "anthropic":
122
+ return build_anthropic_params(model_name, params)
123
+ elif provider == "google":
124
+ return build_google_params(model_name, params)
125
+ elif provider == "github_copilot":
126
+ return build_github_copilot_params(model_name, params)
127
+ else:
128
+ return build_fallback_params(provider, model_name, params)
129
+
130
+
131
+ def filter_reasoning_model_params(model_name: str, params: dict) -> dict:
132
+ """Filter out unsupported parameters for reasoning models.
133
+
134
+ OpenAI's o1/o3 reasoning models don't support certain parameters.
135
+
136
+ Args:
137
+ model_name: The model name (e.g., "o1", "o1-mini")
138
+ params: Dictionary of parameters
139
+
140
+ Returns:
141
+ Filtered parameters dict (modifies in-place and returns)
142
+ """
143
+ unsupported_params = ["stop", "temperature", "top_p", "presence_penalty", "frequency_penalty"]
144
+
145
+ # o1-mini specifically doesn't support reasoning_effort
146
+ if "o1-mini" in model_name:
147
+ unsupported_params.append("reasoning_effort")
148
+
149
+ # Remove unsupported parameters
150
+ for param in unsupported_params:
151
+ params.pop(param, None)
152
+
153
+ return params
154
+
155
+
156
+ def build_ollama_params(model_name: str, variant: str | None, params: dict) -> dict:
157
+ """Build parameters for Ollama provider.
158
+
159
+ Args:
160
+ model_name: The model name
161
+ variant: Optional model variant
162
+ params: Base parameters dict
163
+
164
+ Returns:
165
+ Updated parameters dict
166
+ """
167
+ full_model_name = f"{model_name}:{variant}" if variant else model_name
168
+ params["model"] = full_model_name
169
+ params.setdefault("api_base", os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1"))
170
+ params.setdefault("api_key", "ollama")
171
+ return params
172
+
173
+
174
+ def build_openai_params(model_name: str, params: dict) -> dict:
175
+ """Build parameters for OpenAI provider.
176
+
177
+ Args:
178
+ model_name: The model name
179
+ params: Base parameters dict
180
+
181
+ Returns:
182
+ Updated parameters dict
183
+ """
184
+ params["model"] = f"openai/{model_name}"
185
+ if "api_key" not in params:
186
+ params["api_key"] = os.getenv("OPENAI_API_KEY")
187
+ return params
188
+
189
+
190
+ def build_anthropic_params(model_name: str, params: dict) -> dict:
191
+ """Build parameters for Anthropic provider.
192
+
193
+ Args:
194
+ model_name: The model name
195
+ params: Base parameters dict
196
+
197
+ Returns:
198
+ Updated parameters dict
199
+ """
200
+ params["model"] = f"anthropic/{model_name}"
201
+ if "api_key" not in params:
202
+ params["api_key"] = os.getenv("ANTHROPIC_API_KEY")
203
+ return params
204
+
205
+
206
+ def build_google_params(model_name: str, params: dict) -> dict:
207
+ """Build parameters for Google provider.
208
+
209
+ Args:
210
+ model_name: The model name
211
+ params: Base parameters dict
212
+
213
+ Returns:
214
+ Updated parameters dict
215
+ """
216
+ params["model"] = f"gemini/{model_name}"
217
+ if "api_key" not in params:
218
+ params["api_key"] = os.getenv("GOOGLE_API_KEY")
219
+ return params
220
+
221
+
222
+ def build_github_copilot_params(model_name: str, params: dict) -> dict:
223
+ """Build parameters for GitHub Copilot provider.
224
+
225
+ Args:
226
+ model_name: The model name
227
+ params: Base parameters dict
228
+
229
+ Returns:
230
+ Updated parameters dict
231
+ """
232
+ params["model"] = f"github_copilot/{model_name}"
233
+
234
+ # GitHub Copilot requires specific headers
235
+ extra_headers = params.get("extra_headers", {})
236
+ if "editor-version" not in extra_headers:
237
+ extra_headers["editor-version"] = "vscode/1.95.0"
238
+ if "Copilot-Integration-Id" not in extra_headers:
239
+ extra_headers["Copilot-Integration-Id"] = "vscode-chat"
240
+ params["extra_headers"] = extra_headers
241
+
242
+ return params
243
+
244
+
245
+ def build_fallback_params(provider: str, model_name: str, params: dict) -> dict:
246
+ """Build parameters for fallback/unknown providers.
247
+
248
+ Args:
249
+ provider: The provider name
250
+ model_name: The model name
251
+ params: Base parameters dict
252
+
253
+ Returns:
254
+ Updated parameters dict
255
+ """
256
+ params["model"] = f"{provider}/{model_name}"
257
+ return params
tsugite/renderer.py ADDED
@@ -0,0 +1,151 @@
1
+ """Jinja2 template rendering for agent content."""
2
+
3
+ import os
4
+ import re
5
+ from datetime import datetime
6
+ from typing import Any, Dict
7
+
8
+ from jinja2 import DictLoader, Environment, StrictUndefined
9
+
10
+
11
+ def now() -> str:
12
+ return datetime.now().isoformat()
13
+
14
+
15
+ def today() -> str:
16
+ return datetime.now().strftime("%Y-%m-%d")
17
+
18
+
19
+ def slugify(text: str) -> str:
20
+ import re
21
+
22
+ text = text.lower()
23
+ # Replace special characters with dashes, then keep only ASCII letters, numbers, and dashes
24
+ text = re.sub(r"[^\w\s-]", "-", text, flags=re.ASCII)
25
+ text = re.sub(r"[-\s]+", "-", text)
26
+ return text.strip("-")
27
+
28
+
29
+ def file_exists(path: str) -> bool:
30
+ from pathlib import Path
31
+
32
+ return Path(path).exists()
33
+
34
+
35
+ def is_file(path: str) -> bool:
36
+ from pathlib import Path
37
+
38
+ p = Path(path)
39
+ return p.exists() and p.is_file()
40
+
41
+
42
+ def is_dir(path: str) -> bool:
43
+ from pathlib import Path
44
+
45
+ p = Path(path)
46
+ return p.exists() and p.is_dir()
47
+
48
+
49
+ def read_text(path: str, default: str = "") -> str:
50
+ from pathlib import Path
51
+
52
+ try:
53
+ return Path(path).read_text(encoding="utf-8")
54
+ except Exception:
55
+ return default
56
+
57
+
58
+ def strip_ignored_sections(content: str) -> str:
59
+ """Remove <!-- tsu:ignore --> blocks from content.
60
+
61
+ Supports both block and inline forms:
62
+ - Block: <!-- tsu:ignore -->\\ncontent\\n<!-- /tsu:ignore -->
63
+ - Inline: <!-- tsu:ignore -->content<!-- /tsu:ignore -->
64
+
65
+ Args:
66
+ content: Raw markdown content
67
+
68
+ Returns:
69
+ Content with ignored sections removed
70
+
71
+ Example:
72
+ >>> content = '''
73
+ ... Normal content
74
+ ... <!-- tsu:ignore -->
75
+ ... This is ignored
76
+ ... <!-- /tsu:ignore -->
77
+ ... More content
78
+ ... '''
79
+ >>> result = strip_ignored_sections(content)
80
+ >>> 'This is ignored' not in result
81
+ True
82
+ """
83
+ # Pattern matches opening tag, content (non-greedy), closing tag
84
+ # Handles both <!-- tsu:ignore --> and <!--tsu:ignore--> (with/without spaces)
85
+ # Uses non-greedy match (.*?) to handle multiple blocks correctly
86
+ # DOTALL flag allows matching across newlines
87
+ pattern = r"<!--\s*tsu:ignore\s*-->.*?<!--\s*/tsu:ignore\s*-->"
88
+
89
+ # Remove all ignore blocks
90
+ result = re.sub(pattern, "", content, flags=re.DOTALL)
91
+
92
+ return result
93
+
94
+
95
+ class AgentRenderer:
96
+ """Jinja2 template renderer for agent content."""
97
+
98
+ def __init__(self):
99
+ self.env = Environment(
100
+ loader=DictLoader({}),
101
+ undefined=StrictUndefined,
102
+ trim_blocks=True,
103
+ lstrip_blocks=True,
104
+ )
105
+
106
+ # Add helper functions
107
+ self.env.globals.update(
108
+ {
109
+ "now": now,
110
+ "today": today,
111
+ "slugify": slugify,
112
+ "file_exists": file_exists,
113
+ "is_file": is_file,
114
+ "is_dir": is_dir,
115
+ "read_text": read_text,
116
+ "env": dict(os.environ),
117
+ }
118
+ )
119
+
120
+ # Add filters
121
+ self.env.filters["slugify"] = slugify
122
+
123
+ def render(self, content: str, context: Dict[str, Any] = None) -> str:
124
+ """Render agent content with Jinja2.
125
+
126
+ Preprocessing steps:
127
+ 1. Strip <!-- tsu:ignore --> blocks
128
+ 2. Render Jinja2 template with context
129
+
130
+ Args:
131
+ content: Raw markdown content
132
+ context: Template variables
133
+
134
+ Returns:
135
+ Rendered content
136
+
137
+ Raises:
138
+ ValueError: If template rendering fails
139
+ """
140
+ if context is None:
141
+ context = {}
142
+
143
+ try:
144
+ # Step 1: Strip ignored sections BEFORE rendering
145
+ preprocessed = strip_ignored_sections(content)
146
+
147
+ # Step 2: Render Jinja2 template
148
+ template = self.env.from_string(preprocessed)
149
+ return template.render(**context)
150
+ except Exception as e:
151
+ raise ValueError(f"Template rendering failed: {e}") from e
@@ -0,0 +1,265 @@
1
+ """Configuration loader for custom shell tools."""
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+
6
+ import yaml
7
+
8
+ from .tools.shell_tools import ShellToolDefinition, ShellToolParameter
9
+ from .xdg import get_xdg_config_path
10
+
11
+
12
+ def get_custom_tools_config_path() -> Path:
13
+ """Get the path to custom_tools.yaml config file."""
14
+ return get_xdg_config_path("custom_tools.yaml")
15
+
16
+
17
+ def load_custom_tools_config(path: Optional[Path] = None) -> List[ShellToolDefinition]:
18
+ """Load custom tool definitions from YAML config.
19
+
20
+ Args:
21
+ path: Path to custom_tools.yaml. If None, uses default XDG path.
22
+
23
+ Returns:
24
+ List of ShellToolDefinition objects
25
+
26
+ Example YAML:
27
+ tools:
28
+ - name: file_search
29
+ description: Search files with ripgrep
30
+ command: "rg {pattern} {path}"
31
+ timeout: 30
32
+ parameters:
33
+ pattern:
34
+ type: str
35
+ description: Search pattern
36
+ required: true
37
+ path:
38
+ type: str
39
+ description: Directory to search
40
+ default: "."
41
+ """
42
+ if path is None:
43
+ path = get_custom_tools_config_path()
44
+
45
+ if not path.exists():
46
+ return []
47
+
48
+ try:
49
+ with open(path, "r", encoding="utf-8") as f:
50
+ config = yaml.safe_load(f)
51
+
52
+ if not config or "tools" not in config:
53
+ return []
54
+
55
+ definitions = []
56
+ for tool_def in config["tools"]:
57
+ # Parse parameters
58
+ parameters = {}
59
+ for param_name, param_config in tool_def.get("parameters", {}).items():
60
+ # Handle multiple formats
61
+ if isinstance(param_config, dict):
62
+ # Full dict format
63
+ param_type = param_config.get("type", "str")
64
+ description = param_config.get("description", "")
65
+ required = param_config.get("required", False)
66
+ default = param_config.get("default")
67
+ flag = param_config.get("flag")
68
+ elif isinstance(param_config, str):
69
+ # Simple string format - assume it's the type
70
+ param_type = param_config if param_config else "str"
71
+ description = ""
72
+ required = False
73
+ default = None
74
+ flag = None
75
+ elif param_config is None:
76
+ # Just parameter name, no config
77
+ param_type = "str"
78
+ description = ""
79
+ required = False
80
+ default = None
81
+ flag = None
82
+ else:
83
+ # Value provided - infer type and use as default
84
+ if isinstance(param_config, bool):
85
+ param_type = "bool"
86
+ default = param_config
87
+ elif isinstance(param_config, int):
88
+ param_type = "int"
89
+ default = param_config
90
+ elif isinstance(param_config, float):
91
+ param_type = "float"
92
+ default = param_config
93
+ else:
94
+ param_type = "str"
95
+ default = str(param_config)
96
+ description = ""
97
+ required = False
98
+ flag = None
99
+
100
+ parameters[param_name] = ShellToolParameter(
101
+ name=param_name,
102
+ type=param_type,
103
+ description=description,
104
+ required=required,
105
+ default=default,
106
+ flag=flag,
107
+ )
108
+
109
+ # Create definition
110
+ definition = ShellToolDefinition(
111
+ name=tool_def["name"],
112
+ description=tool_def.get("description", ""),
113
+ command=tool_def["command"],
114
+ parameters=parameters,
115
+ timeout=tool_def.get("timeout", 30),
116
+ safe_mode=tool_def.get("safe_mode", True),
117
+ shell=tool_def.get("shell", True),
118
+ )
119
+
120
+ definitions.append(definition)
121
+
122
+ return definitions
123
+
124
+ except yaml.YAMLError as e:
125
+ raise ValueError(f"Failed to parse custom tools config: {e}") from e
126
+ except Exception as e:
127
+ raise RuntimeError(f"Failed to load custom tools config: {e}") from e
128
+
129
+
130
+ def save_custom_tools_config(definitions: List[ShellToolDefinition], path: Optional[Path] = None) -> None:
131
+ """Save custom tool definitions to YAML config.
132
+
133
+ Args:
134
+ definitions: List of tool definitions to save
135
+ path: Path to custom_tools.yaml. If None, uses default XDG path.
136
+ """
137
+ if path is None:
138
+ path = get_custom_tools_config_path()
139
+
140
+ # Ensure parent directory exists
141
+ path.parent.mkdir(parents=True, exist_ok=True)
142
+
143
+ # Convert definitions to YAML-friendly format
144
+ tools_config = []
145
+ for definition in definitions:
146
+ tool_dict = {
147
+ "name": definition.name,
148
+ "description": definition.description,
149
+ "command": definition.command,
150
+ "timeout": definition.timeout,
151
+ "safe_mode": definition.safe_mode,
152
+ "shell": definition.shell,
153
+ "parameters": {},
154
+ }
155
+
156
+ # Convert parameters
157
+ for param_name, param_def in definition.parameters.items():
158
+ tool_dict["parameters"][param_name] = {
159
+ "type": param_def.type,
160
+ "description": param_def.description,
161
+ "required": param_def.required,
162
+ }
163
+
164
+ if param_def.default is not None:
165
+ tool_dict["parameters"][param_name]["default"] = param_def.default
166
+
167
+ if param_def.flag:
168
+ tool_dict["parameters"][param_name]["flag"] = param_def.flag
169
+
170
+ tools_config.append(tool_dict)
171
+
172
+ config = {"tools": tools_config}
173
+
174
+ with open(path, "w", encoding="utf-8") as f:
175
+ yaml.safe_dump(config, f, default_flow_style=False, sort_keys=False)
176
+
177
+
178
+ def parse_tool_definition_from_dict(tool_dict: Dict) -> ShellToolDefinition:
179
+ """Parse a tool definition from a dictionary (e.g., from agent frontmatter).
180
+
181
+ Args:
182
+ tool_dict: Dictionary with tool configuration
183
+
184
+ Returns:
185
+ ShellToolDefinition object
186
+
187
+ Example:
188
+ {
189
+ "name": "file_search",
190
+ "command": "rg {pattern} {path}",
191
+ "parameters": {
192
+ "pattern": {"type": "str", "required": true},
193
+ "path": ".", # Infer type from default value
194
+ "recursive": true # Infer bool from value
195
+ }
196
+ }
197
+ """
198
+ parameters = {}
199
+ for param_name, param_config in tool_dict.get("parameters", {}).items():
200
+ if isinstance(param_config, dict):
201
+ # Full dict format
202
+ param_type = param_config.get("type", "str")
203
+ description = param_config.get("description", "")
204
+ required = param_config.get("required", False)
205
+ default = param_config.get("default")
206
+ flag = param_config.get("flag")
207
+ elif isinstance(param_config, str):
208
+ # String - assume it's either type name or default value
209
+ # If it looks like a type name (str, int, bool, float), use it as type
210
+ if param_config in ("str", "int", "bool", "float"):
211
+ param_type = param_config
212
+ description = ""
213
+ required = False
214
+ default = None
215
+ flag = None
216
+ else:
217
+ # Otherwise it's a default value
218
+ param_type = "str"
219
+ description = ""
220
+ required = False
221
+ default = param_config
222
+ flag = None
223
+ elif param_config is None:
224
+ # Just parameter name
225
+ param_type = "str"
226
+ description = ""
227
+ required = False
228
+ default = None
229
+ flag = None
230
+ else:
231
+ # Value provided - infer type and use as default
232
+ if isinstance(param_config, bool):
233
+ param_type = "bool"
234
+ default = param_config
235
+ elif isinstance(param_config, int):
236
+ param_type = "int"
237
+ default = param_config
238
+ elif isinstance(param_config, float):
239
+ param_type = "float"
240
+ default = param_config
241
+ else:
242
+ param_type = "str"
243
+ default = str(param_config)
244
+ description = ""
245
+ required = False
246
+ flag = None
247
+
248
+ parameters[param_name] = ShellToolParameter(
249
+ name=param_name,
250
+ type=param_type,
251
+ description=description,
252
+ required=required,
253
+ default=default,
254
+ flag=flag,
255
+ )
256
+
257
+ return ShellToolDefinition(
258
+ name=tool_dict["name"],
259
+ description=tool_dict.get("description", ""),
260
+ command=tool_dict["command"],
261
+ parameters=parameters,
262
+ timeout=tool_dict.get("timeout", 30),
263
+ safe_mode=tool_dict.get("safe_mode", True),
264
+ shell=tool_dict.get("shell", True),
265
+ )
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: assistant
3
+ description: General-purpose assistant for questions and tasks
4
+ max_steps: 5
5
+ tools: []
6
+ ---
7
+
8
+ # Assistant
9
+
10
+ You are a helpful assistant. Answer questions clearly and directly, provide explanations when needed, and help with general tasks. Focus on being accurate, concise, and useful.
11
+
12
+ When you have completed the task, use the final_answer() function to provide your response.
13
+
14
+ **Task**: {{ user_prompt }}