siada-cli 0.0.1__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 (215) hide show
  1. agent_config.yaml +34 -0
  2. siada/__init__.py +13 -0
  3. siada/agent_hub/__init__.py +10 -0
  4. siada/agent_hub/coder/bug_fix_agent.py +398 -0
  5. siada/agent_hub/coder/code_gen_agent.py +235 -0
  6. siada/agent_hub/coder/fe_gen_agent.py +37 -0
  7. siada/agent_hub/coder/issue_review_agent.py +68 -0
  8. siada/agent_hub/coder/prompt/__init__.py +0 -0
  9. siada/agent_hub/coder/prompt/base/__init__.py +0 -0
  10. siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
  11. siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
  12. siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
  13. siada/agent_hub/coder/prompt/base/rules.py +40 -0
  14. siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
  15. siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
  16. siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
  17. siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
  18. siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
  19. siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
  20. siada/agent_hub/coder/tracing/__init__.py +22 -0
  21. siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
  22. siada/agent_hub/hooks/__init__.py +0 -0
  23. siada/agent_hub/hooks/io_hooks.py +30 -0
  24. siada/agent_hub/siada_agent.py +214 -0
  25. siada/config/__init__.py +0 -0
  26. siada/config/config_loader.py +67 -0
  27. siada/entrypoint/__init__.py +3 -0
  28. siada/entrypoint/args_parser/__init__.py +0 -0
  29. siada/entrypoint/args_parser/args.py +195 -0
  30. siada/entrypoint/interaction/__init__.py +0 -0
  31. siada/entrypoint/interaction/base_turn.py +107 -0
  32. siada/entrypoint/interaction/config.py +30 -0
  33. siada/entrypoint/interaction/controller.py +122 -0
  34. siada/entrypoint/interaction/nointeractive_controller.py +27 -0
  35. siada/entrypoint/interaction/turn/interface.py +116 -0
  36. siada/entrypoint/interaction/turn/models.py +34 -0
  37. siada/entrypoint/interaction/turn/run_turn.py +641 -0
  38. siada/entrypoint/interaction/turn_factory.py +35 -0
  39. siada/entrypoint/interaction/turn_models.py +28 -0
  40. siada/entrypoint/siadahub.py +413 -0
  41. siada/foundation/__init__.py +0 -0
  42. siada/foundation/code_agent_context.py +48 -0
  43. siada/foundation/config.py +59 -0
  44. siada/foundation/constants.py +2 -0
  45. siada/foundation/context.py +76 -0
  46. siada/foundation/logging.py +341 -0
  47. siada/foundation/tools/__init__.py +0 -0
  48. siada/foundation/tools/get_git_diff.py +105 -0
  49. siada/io/__init__.py +0 -0
  50. siada/io/banner.py +148 -0
  51. siada/io/color_settings.py +207 -0
  52. siada/io/color_utils.py +16 -0
  53. siada/io/components/__init__.py +0 -0
  54. siada/io/components/mdstream.py +204 -0
  55. siada/io/console_printer.py +68 -0
  56. siada/io/io.py +661 -0
  57. siada/io/key_bindings.py +129 -0
  58. siada/io/notification_command.py +40 -0
  59. siada/io/test_banner.py +64 -0
  60. siada/models/__init__.py +0 -0
  61. siada/models/agent.py +47 -0
  62. siada/models/coder/__init__.py +0 -0
  63. siada/models/converter.py +27 -0
  64. siada/models/model_base_config.py +121 -0
  65. siada/models/model_run_config.py +153 -0
  66. siada/provider/__init__.py +0 -0
  67. siada/provider/client_factory.py +112 -0
  68. siada/provider/llm_client.py +11 -0
  69. siada/provider/openrouter/__init__.py +0 -0
  70. siada/provider/openrouter/coverter.py +17 -0
  71. siada/provider/openrouter/openroute_provider.py +33 -0
  72. siada/provider/provider_factory.py +53 -0
  73. siada/queries/tree-sitter-language-pack/README.md +7 -0
  74. siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  91. siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  92. siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  93. siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  94. siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  95. siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  96. siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  97. siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  98. siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  99. siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  100. siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  101. siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  102. siada/queries/tree-sitter-languages/README.md +23 -0
  103. siada/queries/tree-sitter-languages/c-tags.scm +9 -0
  104. siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  105. siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  106. siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
  107. siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  108. siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  109. siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
  110. siada/queries/tree-sitter-languages/go-tags.scm +30 -0
  111. siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  112. siada/queries/tree-sitter-languages/java-tags.scm +20 -0
  113. siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  114. siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  115. siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  116. siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  117. siada/queries/tree-sitter-languages/php-tags.scm +26 -0
  118. siada/queries/tree-sitter-languages/python-tags.scm +12 -0
  119. siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
  120. siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  121. siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
  122. siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
  123. siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  124. siada/services/__init__.py +30 -0
  125. siada/services/at_file_recommendation_design_document.md +1213 -0
  126. siada/services/code_context_manager.py +136 -0
  127. siada/services/enhanced_fix_result_check.py +546 -0
  128. siada/services/execution_trace_collector.py +207 -0
  129. siada/services/file_recommendation/__init__.py +136 -0
  130. siada/services/file_recommendation/core/__init__.py +17 -0
  131. siada/services/file_recommendation/core/completion_engine.py +351 -0
  132. siada/services/file_recommendation/core/config.py +62 -0
  133. siada/services/file_recommendation/core/file_discovery.py +353 -0
  134. siada/services/file_recommendation/core/suggestion.py +110 -0
  135. siada/services/file_recommendation/utils/__init__.py +22 -0
  136. siada/services/file_recommendation/utils/path_utils.py +138 -0
  137. siada/services/file_recommendation/utils/text_utils.py +253 -0
  138. siada/services/fix_result_check.py +377 -0
  139. siada/services/handle_at_command/README.md +247 -0
  140. siada/services/handle_at_command/__init__.py +115 -0
  141. siada/services/handle_at_command/exceptions.py +46 -0
  142. siada/services/handle_at_command/models.py +92 -0
  143. siada/services/handle_at_command/parser.py +189 -0
  144. siada/services/handle_at_command/processor.py +321 -0
  145. siada/services/handle_at_command/resolver.py +318 -0
  146. siada/services/handle_at_command/utils.py +345 -0
  147. siada/services/model_info_service.py +103 -0
  148. siada/services/siada_runner.py +170 -0
  149. siada/session/__init__.py +29 -0
  150. siada/session/session_manager.py +66 -0
  151. siada/session/session_models.py +43 -0
  152. siada/support/__init__.py +0 -0
  153. siada/support/completer.py +204 -0
  154. siada/support/editor.py +146 -0
  155. siada/support/envprocessor.py +54 -0
  156. siada/support/repo.py +51 -0
  157. siada/support/slash_commands.py +410 -0
  158. siada/support/spinner.py +320 -0
  159. siada/tools/__init__.py +0 -0
  160. siada/tools/ast/README.md +345 -0
  161. siada/tools/ast/ast_tool.py +341 -0
  162. siada/tools/ast/models.py +11 -0
  163. siada/tools/coder/__init__.py +0 -0
  164. siada/tools/coder/ask_followup_question.py +70 -0
  165. siada/tools/coder/cmd_runner.py +132 -0
  166. siada/tools/coder/file_operator.py +284 -0
  167. siada/tools/coder/file_search/README.md +223 -0
  168. siada/tools/coder/file_search/__init__.py +10 -0
  169. siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
  170. siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
  171. siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
  172. siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
  173. siada/tools/coder/file_search/bin/rg.exe +0 -0
  174. siada/tools/coder/file_search/search.py +437 -0
  175. siada/tools/coder/file_search/setup_package.py +150 -0
  176. siada/tools/coder/file_search/test_search.py +142 -0
  177. siada/tools/coder/files.py +147 -0
  178. siada/tools/coder/fix_attempt_completion.py +58 -0
  179. siada/tools/coder/issue_review_completion.py +29 -0
  180. siada/tools/coder/observation/__init__.py +0 -0
  181. siada/tools/coder/observation/error.py +22 -0
  182. siada/tools/coder/observation/file_observation.py +216 -0
  183. siada/tools/coder/observation/observation.py +71 -0
  184. siada/tools/coder/repo_map/__init__.py +157 -0
  185. siada/tools/coder/repo_map/dump.py +29 -0
  186. siada/tools/coder/repo_map/io.py +226 -0
  187. siada/tools/coder/repo_map/repo_map.py +881 -0
  188. siada/tools/coder/repo_map/special.py +203 -0
  189. siada/tools/coder/repo_map/token_counter.py +154 -0
  190. siada/tools/coder/repo_map/waiting.py +221 -0
  191. siada/tools/coder/reproduce_completion.py +39 -0
  192. siada/tools/coder/run_cmd.py +60 -0
  193. siada/tools/coder/test_completion.py +25 -0
  194. siada/tools/coder/tool_docs.py +44 -0
  195. siada/tools/compression_tool.py +200 -0
  196. siada/tools/read_many_files/README.md +220 -0
  197. siada/tools/read_many_files/__init__.py +25 -0
  198. siada/tools/read_many_files/file_processor.py +486 -0
  199. siada/tools/read_many_files/filters.py +326 -0
  200. siada/tools/read_many_files/formatters.py +204 -0
  201. siada/tools/read_many_files/models.py +189 -0
  202. siada/tools/read_many_files_tool.py +233 -0
  203. siada/tools/tool_call_format/__init__.py +55 -0
  204. siada/tools/tool_call_format/file_to_language.py +231 -0
  205. siada/tools/tool_call_format/formatter_factory.py +80 -0
  206. siada/tools/tool_call_format/formatters.py +282 -0
  207. siada/tools/tool_call_format/tool_call_formatter.py +54 -0
  208. siada/tools/web/__init__.py +0 -0
  209. siada/tools/web/web_crawl.py +208 -0
  210. siada/utils.py +194 -0
  211. siada_cli-0.0.1.dist-info/LICENSE +201 -0
  212. siada_cli-0.0.1.dist-info/METADATA +457 -0
  213. siada_cli-0.0.1.dist-info/RECORD +215 -0
  214. siada_cli-0.0.1.dist-info/WHEEL +4 -0
  215. siada_cli-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,641 @@
1
+ """
2
+ Run Turn Module
3
+
4
+ Manages individual interaction turns between user and AI, including command processing
5
+ and model conversations. Encapsulates the logic for a single interaction cycle.
6
+ """
7
+
8
+ import grep_ast
9
+ from siada.foundation.logging import logger
10
+ import re
11
+ import siada.io.components.mdstream
12
+ from typing import Optional, Dict, Any, Tuple
13
+
14
+ from agents import (
15
+ RawResponsesStreamEvent,
16
+ RunItemStreamEvent,
17
+ RunResultStreaming,
18
+ ToolCallOutputItem,
19
+ )
20
+
21
+ from siada.support.spinner import WaitingSpinner
22
+ from siada.tools.coder.observation.observation import FunctionCallResult
23
+ from siada.tools.tool_call_format.formatter_factory import ToolCallFormatterFactory
24
+
25
+ # Import existing InteractionConfig
26
+ from ..config import RunningConfig
27
+
28
+ # Import models and interface from the same directory
29
+ from .models import TurnType, TurnInput, TurnOutput
30
+ from .interface import RunTurn
31
+ from rich.markdown import Markdown
32
+ from rich.text import Text
33
+
34
+
35
+ # Standard tag identifier
36
+ REASONING_TAG = "thinking-content-" + "7bbeb8e1441453ad999a0bbba8a46d4b"
37
+
38
+ SPLIT_TAG = "\n--------------\n"
39
+ # Output formatting
40
+
41
+ REASONING_START = "► **THINKING**"
42
+
43
+ REASONING_END = "► **ANSWER**"
44
+
45
+ TOOL_CALL_START = "► **TOOL USE**"
46
+
47
+
48
+ class ConversationTurn(RunTurn):
49
+ """Handles regular AI conversation turns"""
50
+
51
+ mdstream: siada.io.components.mdstream.MarkdownRender = None
52
+
53
+ def _process_thinking_tags(self, text: str) -> Tuple[str, bool]:
54
+ """
55
+ Process thinking tags and return (processed_text, should_render)
56
+
57
+ Args:
58
+ text (str): Streaming input text
59
+
60
+ Returns:
61
+ tuple: (processed_text, should_render)
62
+ - If text ends with incomplete thinking tag, return (previous_safe_text, False)
63
+ - Otherwise remove all thinking tags (preserve content) and return (clean_text, True)
64
+ """
65
+ # Define complete tags
66
+ thinking_start = '<thinking>'
67
+ thinking_end = '</thinking>'
68
+
69
+ # Check if text ends with incomplete thinking tag
70
+ def is_partial_tag_at_end(text: str, full_tag: str) -> bool:
71
+ """Check if text ends with incomplete part of specified tag"""
72
+ for i in range(1, len(full_tag)):
73
+ if text.endswith(full_tag[:i]):
74
+ return True
75
+ return False
76
+
77
+ # If text ends with incomplete <thinking> or </thinking> part, pause rendering
78
+ if is_partial_tag_at_end(text, thinking_start) or is_partial_tag_at_end(text, thinking_end):
79
+ # Find the last complete tag position and get safe part
80
+ safe_end = len(text)
81
+ for i in range(len(text) - 1, -1, -1):
82
+ if text[i] == '<':
83
+ # Check if this position might be start of incomplete thinking tag
84
+ remaining = text[i:]
85
+ if thinking_start.startswith(remaining) or thinking_end.startswith(remaining):
86
+ safe_end = i
87
+ break
88
+
89
+ safe_text = text[:safe_end]
90
+ clean_text = self._remove_thinking_content(safe_text)
91
+ return clean_text, False
92
+
93
+ # No incomplete tags, remove all thinking tags
94
+ clean_text = self._remove_thinking_content(text)
95
+ return clean_text, True
96
+
97
+ def _remove_thinking_content(self, text: str) -> str:
98
+ """
99
+ Remove thinking tags from text but preserve content inside
100
+
101
+ Args:
102
+ text (str): Input text
103
+
104
+ Returns:
105
+ str: Text with thinking tags removed
106
+ """
107
+ # Remove start tag <thinking> and possible whitespace
108
+ text = re.sub(r'<thinking>\s?', '', text)
109
+
110
+ # Remove end tag </thinking> and possible surrounding whitespace
111
+ text = re.sub(r'\s?</thinking>', '', text)
112
+
113
+ return text
114
+ tool_calls: Dict[str, Dict[str, Any]] = None
115
+ tool_call_mdstreams: Dict[str, siada.io.components.mdstream.MarkdownRender] = None
116
+ response_content: str = None
117
+ current_active_call_id: Optional[str] = None
118
+ got_content_part: bool = False
119
+ got_reasoning_part: bool = False
120
+ got_tool_result_part: bool = False
121
+ got_function_call_part: bool = False
122
+
123
+ # Class-level dedicated event loop and thread, shared by all instances
124
+ _dedicated_loop = None
125
+ _dedicated_thread = None
126
+ _loop_ready = None
127
+
128
+ def __init__(self, config: RunningConfig, session: Any, slash_commands: Any):
129
+ super().__init__(config, session, slash_commands)
130
+ self.mdargs = dict(
131
+ style=self.config.running_color_settings.split_line_color,
132
+ code_theme=self.config.running_color_settings.code_theme,
133
+ inline_code_lexer="text",
134
+ )
135
+
136
+ @classmethod
137
+ def _ensure_dedicated_loop(cls):
138
+ """Ensure dedicated event loop is started"""
139
+ if cls._dedicated_loop is None or cls._dedicated_loop.is_closed():
140
+ import threading
141
+ import asyncio
142
+
143
+ # Detect if main thread has a running event loop (prompt_toolkit might be using it)
144
+ try:
145
+ main_loop = asyncio.get_running_loop()
146
+ logger.info(f"📋 Detected main thread event loop: {id(main_loop)}")
147
+ except RuntimeError:
148
+ logger.info("📋 No event loop in main thread") # Normal case
149
+
150
+ # Create event for synchronization
151
+ cls._loop_ready = threading.Event()
152
+
153
+ def run_dedicated_loop():
154
+ """Run event loop in dedicated thread"""
155
+ # Create new event loop
156
+ loop = asyncio.new_event_loop()
157
+ asyncio.set_event_loop(loop)
158
+ cls._dedicated_loop = loop
159
+
160
+ # Notify main thread that loop is ready
161
+ cls._loop_ready.set()
162
+
163
+ try:
164
+ # Run event loop until stopped
165
+ loop.run_forever()
166
+ finally:
167
+ # Cleanup
168
+ loop.close()
169
+ cls._dedicated_loop = None
170
+
171
+ # Start dedicated thread
172
+ cls._dedicated_thread = threading.Thread(
173
+ target=run_dedicated_loop,
174
+ daemon=True, # Daemon thread, auto-terminate when main program exits
175
+ name="ConversationTurn-AsyncLoop",
176
+ )
177
+ cls._dedicated_thread.start()
178
+
179
+ # Wait for event loop to be ready
180
+ cls._loop_ready.wait()
181
+
182
+ @classmethod
183
+ def _cleanup_dedicated_loop(cls):
184
+ """Cleanup dedicated event loop (optional, mainly for testing or graceful shutdown)"""
185
+ if cls._dedicated_loop and not cls._dedicated_loop.is_closed():
186
+ cls._dedicated_loop.call_soon_threadsafe(cls._dedicated_loop.stop)
187
+ if cls._dedicated_thread and cls._dedicated_thread.is_alive():
188
+ cls._dedicated_thread.join(timeout=5.0)
189
+
190
+ def get_turn_type(self) -> TurnType:
191
+ return TurnType.CONVERSATION
192
+
193
+ def can_handle(self, user_input: str) -> bool:
194
+ """Handle non-command input"""
195
+ return not self.slash_commands.is_command(user_input)
196
+
197
+ async def output_stream_content(self, result: RunResultStreaming) -> None:
198
+ """Process stream events and handle real-time output"""
199
+ from openai.types.responses import (
200
+ ResponseTextDeltaEvent,
201
+ ResponseReasoningSummaryTextDeltaEvent,
202
+ ResponseFunctionCallArgumentsDeltaEvent,
203
+ ResponseContentPartAddedEvent,
204
+ ResponseOutputItemAddedEvent,
205
+ ResponseCompletedEvent,
206
+ ResponseCreatedEvent,
207
+ ResponseReasoningSummaryPartAddedEvent,
208
+ ResponseFunctionToolCall,
209
+ ResponseOutputItemDoneEvent,
210
+ ResponseContentPartDoneEvent,
211
+ )
212
+ from openai.types.responses.response_input_item_param import FunctionCallOutput
213
+
214
+ stream_iterator = None
215
+ try:
216
+ stream_iterator = result.stream_events()
217
+ async for event in stream_iterator:
218
+ if isinstance(event, RawResponsesStreamEvent):
219
+ self._stop_waiting_spinner()
220
+
221
+ # Handle the raw response stream event
222
+ stream_data = event.data
223
+
224
+ # Handle different types of stream events
225
+ if isinstance(stream_data, ResponseCreatedEvent):
226
+ # Response started
227
+ self.response_content = ""
228
+ self.tool_calls = {}
229
+ self.tool_call_mdstreams = {}
230
+ self.got_content_part = False
231
+ self.got_reasoning_part = False
232
+ self.current_active_call_id = None
233
+ self.got_tool_result_part = False
234
+ self.got_function_call_part = False
235
+
236
+ elif isinstance(
237
+ stream_data, ResponseReasoningSummaryPartAddedEvent
238
+ ):
239
+ if self.mdstream is None:
240
+ self.mdstream = (
241
+ self.get_response_mdstream()
242
+ if self.config.io.pretty
243
+ else None
244
+ )
245
+ continue
246
+
247
+ elif isinstance(
248
+ stream_data, ResponseReasoningSummaryTextDeltaEvent
249
+ ):
250
+ if not self.got_reasoning_part and stream_data.delta:
251
+ self.got_reasoning_part = True
252
+ self.print_split_line()
253
+ delta_text = f"\n{REASONING_START}\n\n{stream_data.delta}"
254
+ self.response_content += delta_text
255
+ else:
256
+ delta_text = stream_data.delta
257
+ self.response_content += delta_text
258
+ self._live_incremental_response(
259
+ delta_text, self.response_content
260
+ )
261
+
262
+ elif isinstance(stream_data, ResponseContentPartAddedEvent):
263
+ if self.mdstream is None:
264
+ self.mdstream = (
265
+ self.get_response_mdstream()
266
+ if self.config.io.pretty
267
+ else None
268
+ )
269
+ continue
270
+
271
+ elif isinstance(stream_data, ResponseTextDeltaEvent):
272
+ if not self.got_content_part and stream_data.delta:
273
+ self.got_content_part = True
274
+ if not self.got_reasoning_part:
275
+ self.print_split_line()
276
+ delta_text = f"\n\n{REASONING_END}\n\n{stream_data.delta}"
277
+ self.response_content += delta_text
278
+ else:
279
+ delta_text = stream_data.delta
280
+ self.response_content += delta_text
281
+ self._live_incremental_response(
282
+ delta_text, self.response_content
283
+ )
284
+
285
+ elif isinstance(stream_data, ResponseContentPartDoneEvent):
286
+ if not self.got_function_call_part:
287
+ # if not got function call part, flush the response content
288
+ self._live_incremental_response(
289
+ "\n", self.response_content, final=True
290
+ )
291
+ self.mdstream = None
292
+
293
+ elif isinstance(stream_data, ResponseOutputItemAddedEvent):
294
+ if isinstance(stream_data.item, ResponseFunctionToolCall):
295
+ # if have got the function call part, must flush the response before the tool call
296
+ if not self.got_function_call_part:
297
+ self.got_function_call_part = True
298
+ # flush the response content
299
+ self._live_incremental_response(
300
+ "\n", self.response_content, final=True
301
+ )
302
+ self.mdstream = None
303
+
304
+ call_id = stream_data.item.call_id
305
+ tool_name = stream_data.item.name
306
+ self.tool_calls[call_id] = {
307
+ "name": tool_name,
308
+ "arguments": "",
309
+ "arguments_render": "",
310
+ }
311
+
312
+ tool_call_formatter = ToolCallFormatterFactory.get_formatter(tool_name)
313
+
314
+ if (
315
+ self.config.io.pretty
316
+ and tool_call_formatter.supports_streaming()
317
+ ):
318
+ self.tool_call_mdstreams[call_id] = (
319
+ self.get_response_mdstream()
320
+ )
321
+
322
+ # process the previous tool call stream, stop the live
323
+ if (
324
+ self.current_active_call_id
325
+ and self.current_active_call_id
326
+ in self.tool_call_mdstreams
327
+ ):
328
+ self.tool_call_mdstreams[
329
+ self.current_active_call_id
330
+ ].update(
331
+ tool_call_formatter.format_input(
332
+ self.current_active_call_id,
333
+ self.tool_calls[self.current_active_call_id]["name"],
334
+ self.tool_calls[self.current_active_call_id][
335
+ "arguments"
336
+ ],
337
+ )[0],
338
+ final=True,
339
+ )
340
+ if self.current_active_call_id in self.tool_call_mdstreams:
341
+ del self.tool_call_mdstreams[
342
+ self.current_active_call_id
343
+ ]
344
+
345
+ self.current_active_call_id = call_id
346
+ self.print_split_line()
347
+ self.config.io.print_tool_call(
348
+ f"{TOOL_CALL_START}\n\nSiada wants to use the tool: {tool_name}\n"
349
+ )
350
+
351
+ elif isinstance(
352
+ stream_data, ResponseFunctionCallArgumentsDeltaEvent
353
+ ):
354
+ delta = stream_data.delta
355
+ if self.current_active_call_id:
356
+ self.tool_calls[self.current_active_call_id][
357
+ "arguments"
358
+ ] += delta
359
+
360
+ tool_call_formatter = ToolCallFormatterFactory.get_formatter(
361
+ self.tool_calls[self.current_active_call_id]["name"]
362
+ )
363
+
364
+ # if supports streaming, update the tool call mdstream
365
+ if tool_call_formatter.supports_streaming():
366
+ content, is_complete = tool_call_formatter.format_input(
367
+ self.current_active_call_id,
368
+ self.tool_calls[self.current_active_call_id][
369
+ "name"
370
+ ],
371
+ self.tool_calls[self.current_active_call_id][
372
+ "arguments"
373
+ ],
374
+ )
375
+
376
+ # compute the content_delta
377
+ arguments_delta = content[len(self.tool_calls[self.current_active_call_id]["arguments_render"]):]
378
+ self.tool_calls[self.current_active_call_id]["arguments_render"] = content
379
+
380
+ if self.current_active_call_id in self.tool_call_mdstreams:
381
+ self.tool_call_mdstreams[
382
+ self.current_active_call_id
383
+ ].update(content, final=False)
384
+ else:
385
+ self.config.io.console.print(
386
+ arguments_delta, sep="", end=""
387
+ )
388
+
389
+ elif isinstance(stream_data, ResponseOutputItemDoneEvent):
390
+ if isinstance(stream_data.item, ResponseFunctionToolCall):
391
+ call_id = stream_data.item.call_id
392
+ if call_id in self.tool_calls:
393
+ tool_name = self.tool_calls[call_id]["name"]
394
+ full_arguments = self.tool_calls[call_id]["arguments"]
395
+
396
+ tool_call_formatter = (
397
+ ToolCallFormatterFactory.get_formatter(tool_name)
398
+ )
399
+ content, _ = tool_call_formatter.format_input(
400
+ call_id, tool_name, full_arguments
401
+ )
402
+ style = tool_call_formatter.get_style()
403
+ # if not streaming, only create the mdstream and update the final content
404
+ if tool_call_formatter.supports_streaming():
405
+ # process the last tool call stream, stop the live
406
+ if call_id in self.tool_call_mdstreams:
407
+ self.tool_call_mdstreams[call_id].update(
408
+ content,
409
+ final=True,
410
+ )
411
+ if call_id in self.tool_call_mdstreams:
412
+ del self.tool_call_mdstreams[call_id]
413
+ else:
414
+ if style == "markdown" and self.config.io.pretty:
415
+ self.tool_call_mdstreams[call_id] = (
416
+ self.get_response_mdstream()
417
+ )
418
+ self.tool_call_mdstreams[call_id].update(
419
+ content, final=True
420
+ )
421
+ if call_id in self.tool_call_mdstreams:
422
+ del self.tool_call_mdstreams[call_id]
423
+ else:
424
+ self.config.io.print_tool_call(content)
425
+
426
+ elif isinstance(stream_data, ResponseCompletedEvent):
427
+ pass
428
+
429
+ elif isinstance(event, RunItemStreamEvent):
430
+ stream_data = event.item
431
+ if isinstance(stream_data, ToolCallOutputItem):
432
+ call_id = stream_data.raw_item.get("call_id", None)
433
+ if call_id:
434
+ if call_id in self.tool_calls:
435
+ tool_name = self.tool_calls[call_id]["name"]
436
+ self.print_split_line()
437
+ output = stream_data.output
438
+ if isinstance(output, FunctionCallResult):
439
+ self.config.io.print_tool_result(
440
+ output.format_for_display()
441
+ )
442
+ else:
443
+ self.config.io.print_tool_result(str(output))
444
+
445
+ except Exception as e:
446
+ # Clean up MarkdownStream if it exists on stream error
447
+ if hasattr(self, "mdstream") and self.mdstream is not None:
448
+ try:
449
+ self.mdstream.close() # Direct close, no need to render on error
450
+ self.mdstream = None
451
+ except Exception:
452
+ pass # Ignore cleanup errors
453
+
454
+ # Clean up tool call streams on error
455
+ if hasattr(self, "tool_call_mdstreams") and self.tool_call_mdstreams:
456
+ try:
457
+ for stream in self.tool_call_mdstreams.values():
458
+ if stream:
459
+ try:
460
+ stream.close() # Direct close, no need to render on error
461
+ except Exception:
462
+ pass # Ignore individual stream cleanup errors
463
+ self.tool_call_mdstreams.clear()
464
+ except Exception:
465
+ pass # Ignore cleanup errors
466
+ raise e
467
+
468
+ def _live_incremental_response(
469
+ self,
470
+ delta_text: str,
471
+ response_content: str,
472
+ final: bool = False,
473
+ ):
474
+ if self.mdstream:
475
+ # Process thinking tags
476
+ processed_content, should_render = self._process_thinking_tags(response_content)
477
+
478
+ if should_render or final:
479
+ self.mdstream.update(processed_content if processed_content else "", final)
480
+ # If should_render is False, pause mdstream.update temporarily
481
+ else:
482
+ if not self.config.io.pretty:
483
+ # For non-pretty mode, also need to process thinking tags
484
+ processed_delta, should_render = self._process_thinking_tags(delta_text)
485
+ if should_render or final:
486
+ self.config.io.console.print(processed_delta, sep="", end="")
487
+
488
+ def execute(self, turn_input: TurnInput) -> TurnOutput:
489
+ """Execute AI conversation turn
490
+
491
+ Args:
492
+ turn_input: User input for conversation
493
+
494
+ Returns:
495
+ TurnOutput: AI response
496
+ """
497
+ self.input_data = turn_input
498
+ self.start_time = self._get_timestamp()
499
+ self.spinner = None
500
+ if self.config.io.pretty:
501
+ self.spinner = WaitingSpinner(f"Waiting for Agent {self.config.agent_name}...")
502
+ self.spinner.start()
503
+
504
+ try:
505
+ # Import here to avoid circular imports
506
+ from siada.services.siada_runner import SiadaRunner
507
+ import asyncio
508
+
509
+ # Define async execution logic
510
+ async def _async_execute():
511
+ # Run agent for conversation
512
+ result: RunResultStreaming = await SiadaRunner.run_agent(
513
+ agent_name=self.config.agent_name,
514
+ user_input=turn_input.use_input,
515
+ workspace=self.config.workspace,
516
+ session=self.session,
517
+ stream=True,
518
+ )
519
+
520
+ await self.output_stream_content(result)
521
+ return result
522
+
523
+ # Use dedicated event loop to execute async tasks (reuse loop, maintain connection pool advantages)
524
+ self._ensure_dedicated_loop()
525
+
526
+ # Execute async task in dedicated loop
527
+ future = asyncio.run_coroutine_threadsafe(
528
+ _async_execute(), self._dedicated_loop
529
+ )
530
+ result = future.result()
531
+
532
+ self.end_time = self._get_timestamp()
533
+
534
+ output = TurnOutput(
535
+ output=result.final_output,
536
+ metadata={
537
+ "agent_used": self.config.agent_name,
538
+ "execution_time": self.end_time - self.start_time,
539
+ },
540
+ next_action=None,
541
+ )
542
+
543
+ self.output_data = output
544
+ return output
545
+
546
+ except Exception as e:
547
+ self.end_time = self._get_timestamp()
548
+ return self.handle_error(e)
549
+
550
+ def print_split_line(self):
551
+ if self.config.io.pretty:
552
+ self.config.io.rule(color=self.config.running_color_settings.split_line_color)
553
+ else:
554
+ self.config.io.console.print(SPLIT_TAG, end="")
555
+
556
+ def get_response_mdstream(self):
557
+ mdargs = dict(
558
+ style=self.config.running_color_settings.assistant_output_color,
559
+ code_theme=self.config.running_color_settings.code_theme,
560
+ inline_code_lexer="text",
561
+ )
562
+ mdStream = siada.io.components.mdstream.MarkdownRender(mdargs=mdargs)
563
+ return mdStream
564
+
565
+ def _stop_waiting_spinner(self):
566
+ """Stop and clear the waiting spinner if it is running."""
567
+ spinner = getattr(self, "spinner", None)
568
+ if spinner:
569
+ try:
570
+ spinner.stop()
571
+ finally:
572
+ self.spinner = None
573
+
574
+
575
+ class CommandTurn(RunTurn):
576
+ """Handles slash command turns"""
577
+
578
+ def get_turn_type(self) -> TurnType:
579
+ return TurnType.COMMAND
580
+
581
+ def can_handle(self, user_input: str) -> bool:
582
+ """Handle slash commands"""
583
+ return self.slash_commands.is_command(user_input)
584
+
585
+ def execute(self, turn_input: TurnInput) -> TurnOutput:
586
+ """Execute slash command
587
+
588
+ Args:
589
+ turn_input: Command input
590
+
591
+ Returns:
592
+ TurnOutput: Command result
593
+ """
594
+ self.input_data = turn_input
595
+ self.start_time = self._get_timestamp()
596
+
597
+ try:
598
+ result = self.slash_commands.run(self.session, turn_input.use_input)
599
+ self.end_time = self._get_timestamp()
600
+
601
+ output = TurnOutput(
602
+ output=result,
603
+ metadata={"execution_time": self.end_time - self.start_time},
604
+ next_action=None,
605
+ )
606
+
607
+ return output
608
+
609
+ except Exception as e:
610
+ self.end_time = self._get_timestamp()
611
+ return self.handle_error(e)
612
+
613
+
614
+ class TurnFactory:
615
+ """Factory for creating appropriate turn instances"""
616
+
617
+ @staticmethod
618
+ def create_turn(
619
+ config: RunningConfig, session: Any, slash_commands: Any, user_input: str
620
+ ) -> RunTurn:
621
+ """Create appropriate turn for user input
622
+
623
+ Args:
624
+ user_input: Raw user input
625
+
626
+ Returns:
627
+ RunTurn: Appropriate turn handler
628
+ """
629
+ # Always create new instances to avoid state pollution
630
+ turn_types = [
631
+ CommandTurn,
632
+ ConversationTurn,
633
+ ]
634
+
635
+ for turn_class in turn_types:
636
+ # Create a temporary instance to test if it can handle the input
637
+ temp_turn = turn_class(config, session, slash_commands)
638
+ if temp_turn.can_handle(user_input):
639
+ return temp_turn
640
+
641
+ raise ValueError(f"No turn can handle the input: {user_input}")
@@ -0,0 +1,35 @@
1
+ from typing import Any
2
+
3
+ from siada.entrypoint.interaction.base_turn import RunTurn
4
+ from siada.entrypoint.interaction.config import RunningConfig
5
+ from siada.entrypoint.interaction.turn.run_turn import CommandTurn, ConversationTurn
6
+
7
+
8
+ class TurnFactory:
9
+ """Factory for creating appropriate turn instances"""
10
+
11
+ @staticmethod
12
+ def create_turn(
13
+ config: RunningConfig, session: Any, slash_commands: Any, user_input: str
14
+ ) -> RunTurn:
15
+ """Create appropriate turn for user input
16
+
17
+ Args:
18
+ user_input: Raw user input
19
+
20
+ Returns:
21
+ RunTurn: Appropriate turn handler
22
+ """
23
+ # Always create new instances to avoid state pollution
24
+ turn_types = [
25
+ CommandTurn,
26
+ ConversationTurn,
27
+ ]
28
+
29
+ for turn_class in turn_types:
30
+ # Create a temporary instance to test if it can handle the input
31
+ temp_turn = turn_class(config, session, slash_commands)
32
+ if temp_turn.can_handle(user_input):
33
+ return temp_turn
34
+
35
+ raise ValueError(f"No turn can handle the input: {user_input}")