teddy-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. teddy_cli-0.1.0.dist-info/LICENSE +677 -0
  2. teddy_cli-0.1.0.dist-info/METADATA +33 -0
  3. teddy_cli-0.1.0.dist-info/RECORD +143 -0
  4. teddy_cli-0.1.0.dist-info/WHEEL +4 -0
  5. teddy_cli-0.1.0.dist-info/entry_points.txt +3 -0
  6. teddy_executor/__init__.py +1 -0
  7. teddy_executor/__main__.py +335 -0
  8. teddy_executor/adapters/__init__.py +0 -0
  9. teddy_executor/adapters/inbound/__init__.py +0 -0
  10. teddy_executor/adapters/inbound/cli_formatter.py +107 -0
  11. teddy_executor/adapters/inbound/cli_helpers.py +249 -0
  12. teddy_executor/adapters/inbound/console_plan_reviewer.py +69 -0
  13. teddy_executor/adapters/inbound/session_cli_handlers.py +366 -0
  14. teddy_executor/adapters/inbound/textual_plan_reviewer.py +78 -0
  15. teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +367 -0
  16. teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +281 -0
  17. teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +213 -0
  18. teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +308 -0
  19. teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +345 -0
  20. teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +227 -0
  21. teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +246 -0
  22. teddy_executor/adapters/outbound/__init__.py +7 -0
  23. teddy_executor/adapters/outbound/console_interactor.py +212 -0
  24. teddy_executor/adapters/outbound/console_interactor_ask_loop.py +121 -0
  25. teddy_executor/adapters/outbound/console_interactor_helpers.py +95 -0
  26. teddy_executor/adapters/outbound/console_tooling.py +62 -0
  27. teddy_executor/adapters/outbound/filesystem_helpers.py +61 -0
  28. teddy_executor/adapters/outbound/litellm_adapter.py +462 -0
  29. teddy_executor/adapters/outbound/local_file_system_adapter.py +300 -0
  30. teddy_executor/adapters/outbound/local_repo_tree_generator.py +96 -0
  31. teddy_executor/adapters/outbound/openrouter_hydrator.py +89 -0
  32. teddy_executor/adapters/outbound/shell_adapter.py +344 -0
  33. teddy_executor/adapters/outbound/shell_command_builder.py +105 -0
  34. teddy_executor/adapters/outbound/system_environment_adapter.py +62 -0
  35. teddy_executor/adapters/outbound/system_environment_inspector.py +54 -0
  36. teddy_executor/adapters/outbound/system_time_adapter.py +22 -0
  37. teddy_executor/adapters/outbound/web_scraper_adapter.py +346 -0
  38. teddy_executor/adapters/outbound/web_searcher_adapter.py +122 -0
  39. teddy_executor/adapters/outbound/yaml_config_adapter.py +105 -0
  40. teddy_executor/container.py +333 -0
  41. teddy_executor/core/__init__.py +0 -0
  42. teddy_executor/core/domain/__init__.py +0 -0
  43. teddy_executor/core/domain/models/__init__.py +44 -0
  44. teddy_executor/core/domain/models/action_ports.py +28 -0
  45. teddy_executor/core/domain/models/change_set.py +10 -0
  46. teddy_executor/core/domain/models/exceptions.py +40 -0
  47. teddy_executor/core/domain/models/execution_report.py +65 -0
  48. teddy_executor/core/domain/models/orchestrator_ports.py +26 -0
  49. teddy_executor/core/domain/models/plan.py +85 -0
  50. teddy_executor/core/domain/models/planning_ports.py +43 -0
  51. teddy_executor/core/domain/models/project_context.py +56 -0
  52. teddy_executor/core/domain/models/report_assembly_data.py +18 -0
  53. teddy_executor/core/domain/models/session.py +17 -0
  54. teddy_executor/core/domain/models/shell_output.py +12 -0
  55. teddy_executor/core/domain/models/web_search_results.py +26 -0
  56. teddy_executor/core/ports/__init__.py +0 -0
  57. teddy_executor/core/ports/inbound/__init__.py +0 -0
  58. teddy_executor/core/ports/inbound/edit_simulator.py +33 -0
  59. teddy_executor/core/ports/inbound/get_context_use_case.py +32 -0
  60. teddy_executor/core/ports/inbound/init.py +15 -0
  61. teddy_executor/core/ports/inbound/plan_parser.py +52 -0
  62. teddy_executor/core/ports/inbound/plan_reviewer.py +44 -0
  63. teddy_executor/core/ports/inbound/plan_validator.py +26 -0
  64. teddy_executor/core/ports/inbound/planning_use_case.py +30 -0
  65. teddy_executor/core/ports/inbound/run_plan_use_case.py +60 -0
  66. teddy_executor/core/ports/outbound/__init__.py +34 -0
  67. teddy_executor/core/ports/outbound/config_service.py +29 -0
  68. teddy_executor/core/ports/outbound/environment_inspector.py +30 -0
  69. teddy_executor/core/ports/outbound/execution_report_assembler.py +19 -0
  70. teddy_executor/core/ports/outbound/file_system_manager.py +131 -0
  71. teddy_executor/core/ports/outbound/llm_client.py +90 -0
  72. teddy_executor/core/ports/outbound/markdown_report_formatter.py +26 -0
  73. teddy_executor/core/ports/outbound/prompt_manager.py +55 -0
  74. teddy_executor/core/ports/outbound/repo_tree_generator.py +17 -0
  75. teddy_executor/core/ports/outbound/session_loop_guard.py +16 -0
  76. teddy_executor/core/ports/outbound/session_manager.py +97 -0
  77. teddy_executor/core/ports/outbound/session_repository.py +65 -0
  78. teddy_executor/core/ports/outbound/shell_executor.py +24 -0
  79. teddy_executor/core/ports/outbound/system_environment.py +25 -0
  80. teddy_executor/core/ports/outbound/time_service.py +28 -0
  81. teddy_executor/core/ports/outbound/user_interactor.py +126 -0
  82. teddy_executor/core/ports/outbound/web_scraper.py +24 -0
  83. teddy_executor/core/ports/outbound/web_searcher.py +25 -0
  84. teddy_executor/core/services/__init__.py +0 -0
  85. teddy_executor/core/services/action_changeset_builder.py +90 -0
  86. teddy_executor/core/services/action_diff_manager.py +110 -0
  87. teddy_executor/core/services/action_dispatcher.py +142 -0
  88. teddy_executor/core/services/action_executor.py +209 -0
  89. teddy_executor/core/services/action_factory.py +197 -0
  90. teddy_executor/core/services/action_parser_complex.py +216 -0
  91. teddy_executor/core/services/action_parser_strategies.py +84 -0
  92. teddy_executor/core/services/context_service.py +437 -0
  93. teddy_executor/core/services/edit_simulator.py +128 -0
  94. teddy_executor/core/services/execution_orchestrator.py +295 -0
  95. teddy_executor/core/services/execution_report_assembler.py +62 -0
  96. teddy_executor/core/services/init_service.py +80 -0
  97. teddy_executor/core/services/markdown_plan_parser.py +309 -0
  98. teddy_executor/core/services/markdown_report_formatter.py +143 -0
  99. teddy_executor/core/services/parser_infrastructure.py +222 -0
  100. teddy_executor/core/services/parser_metadata.py +153 -0
  101. teddy_executor/core/services/parser_reporting.py +267 -0
  102. teddy_executor/core/services/plan_validator.py +82 -0
  103. teddy_executor/core/services/planning_service.py +242 -0
  104. teddy_executor/core/services/prompt_manager.py +146 -0
  105. teddy_executor/core/services/session_lifecycle_manager.py +228 -0
  106. teddy_executor/core/services/session_loop_guard.py +46 -0
  107. teddy_executor/core/services/session_orchestrator.py +538 -0
  108. teddy_executor/core/services/session_planner.py +43 -0
  109. teddy_executor/core/services/session_pruning_service.py +438 -0
  110. teddy_executor/core/services/session_replanner.py +105 -0
  111. teddy_executor/core/services/session_repository.py +194 -0
  112. teddy_executor/core/services/session_service.py +529 -0
  113. teddy_executor/core/services/templates/execution_report.md.j2 +290 -0
  114. teddy_executor/core/services/validation_rules/__init__.py +4 -0
  115. teddy_executor/core/services/validation_rules/edit.py +207 -0
  116. teddy_executor/core/services/validation_rules/edit_matcher.py +247 -0
  117. teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +84 -0
  118. teddy_executor/core/services/validation_rules/execute.py +37 -0
  119. teddy_executor/core/services/validation_rules/filesystem.py +73 -0
  120. teddy_executor/core/services/validation_rules/helpers.py +178 -0
  121. teddy_executor/core/services/validation_rules/message.py +29 -0
  122. teddy_executor/core/utils/__init__.py +1 -0
  123. teddy_executor/core/utils/diff.py +57 -0
  124. teddy_executor/core/utils/io.py +75 -0
  125. teddy_executor/core/utils/markdown.py +131 -0
  126. teddy_executor/core/utils/serialization.py +39 -0
  127. teddy_executor/core/utils/string.py +351 -0
  128. teddy_executor/prompts.py +45 -0
  129. teddy_executor/registries/__init__.py +1 -0
  130. teddy_executor/registries/infrastructure.py +147 -0
  131. teddy_executor/registries/reviewer.py +57 -0
  132. teddy_executor/registries/validators.py +47 -0
  133. teddy_executor/resources/__init__.py +1 -0
  134. teddy_executor/resources/config/.gitignore +2 -0
  135. teddy_executor/resources/config/__init__.py +1 -0
  136. teddy_executor/resources/config/config.yaml +49 -0
  137. teddy_executor/resources/config/init.context +5 -0
  138. teddy_executor/resources/config/prompts/architect.xml +462 -0
  139. teddy_executor/resources/config/prompts/assistant.xml +336 -0
  140. teddy_executor/resources/config/prompts/debugger.xml +456 -0
  141. teddy_executor/resources/config/prompts/developer.xml +481 -0
  142. teddy_executor/resources/config/prompts/pathfinder.xml +502 -0
  143. teddy_executor/resources/config/prompts/prototyper.xml +425 -0
@@ -0,0 +1,197 @@
1
+ from typing import Any, Dict, Optional
2
+ from teddy_executor.core.domain.models.action_ports import ActionPorts
3
+ from teddy_executor.core.domain.models.plan import DEFAULT_SIMILARITY_THRESHOLD
4
+ from teddy_executor.core.services.action_dispatcher import IAction, IActionFactory
5
+
6
+
7
+ class ActionFactory(IActionFactory):
8
+ """
9
+ A protocol-compliant factory that resolves action handlers from injected ports.
10
+ """
11
+
12
+ # Maps uppercase verbs from Markdown plans to the internal, descriptive keys.
13
+ _MARKDOWN_ACTION_MAP = {
14
+ "CREATE": "create_file",
15
+ "EDIT": "edit",
16
+ "READ": "read",
17
+ "EXECUTE": "execute",
18
+ "MESSAGE": "message",
19
+ "RESEARCH": "research",
20
+ }
21
+
22
+ def __init__(self, ports: ActionPorts):
23
+ self._shell_executor = ports.shell_executor
24
+ self._file_system_manager = ports.file_system_manager
25
+ self._user_interactor = ports.user_interactor
26
+ self._web_scraper = ports.web_scraper
27
+ self._web_searcher = ports.web_searcher
28
+ self._config_service = ports.config_service
29
+ self._standalone_actions: set[str] = set()
30
+ self._action_map: Dict[str, Any] = {
31
+ "execute": self._shell_executor,
32
+ "create_file": self._file_system_manager,
33
+ "edit": self._file_system_manager,
34
+ "read_file": self._file_system_manager,
35
+ "message": self._user_interactor,
36
+ "research": self._web_searcher,
37
+ }
38
+
39
+ def _normalize_action_type(self, action_type: str) -> str:
40
+ """
41
+ Normalizes action types from different plan formats to the internal key format.
42
+ """
43
+ # First, check the explicit mapping for Markdown verbs.
44
+ if action_type in self._MARKDOWN_ACTION_MAP:
45
+ return self._MARKDOWN_ACTION_MAP[action_type]
46
+ # Fallback to lowercasing for YAML/other formats.
47
+ return action_type.lower()
48
+
49
+ def _create_read_action(self, params: Optional[dict] = None) -> IAction:
50
+ """Handles the special routing for the READ action."""
51
+ safe_params = params or {}
52
+ resource = safe_params.get("resource", safe_params.get("path", ""))
53
+ if resource.startswith("http"):
54
+ # Return a wrapper instead of monkeypatching the adapter
55
+ class WebReadAction:
56
+ def __init__(self, scraper):
57
+ self._scraper = scraper
58
+
59
+ def execute(self, **kwargs: Any) -> Any:
60
+ return self._scraper.get_content(url=kwargs["path"])
61
+
62
+ return WebReadAction(self._web_scraper)
63
+
64
+ if safe_params.get("lines"):
65
+ # Lines-aware read: use read_raw_file (bypass truncation) and extract range
66
+ from teddy_executor.core.utils.string import extract_lines_range # noqa: PLC0415
67
+
68
+ class LinesAwareReadAction:
69
+ def __init__(self, fs):
70
+ self._fs = fs
71
+
72
+ def execute(self, **kwargs: Any) -> Any:
73
+ path = kwargs.get("path", kwargs.get("resource", ""))
74
+ content = self._fs.read_raw_file(path=path)
75
+ lines_spec = kwargs.get("lines", "")
76
+ return extract_lines_range(content, lines_spec)
77
+
78
+ return LinesAwareReadAction(self._file_system_manager)
79
+
80
+ # Fall through to the standard file system handler for local files
81
+ return self._create_standard_action("read_file", params)
82
+
83
+ def _get_action_wrapper(self, handler: Any, method_name: str) -> IAction:
84
+ """Returns an IAction wrapper around an adapter method."""
85
+ original_method = getattr(handler, method_name)
86
+
87
+ class ActionWrapper:
88
+ def __init__(self, factory, method):
89
+ self._factory = factory
90
+ self._method = method
91
+
92
+ def execute(self, **kwargs: Any) -> Any:
93
+ if "resource" in kwargs and "path" not in kwargs:
94
+ kwargs["path"] = kwargs.pop("resource")
95
+
96
+ if method_name == "execute":
97
+ return self._factory._handle_execute_protocol(self._method, kwargs)
98
+ if method_name == "edit_file":
99
+ return self._factory._handle_edit_protocol(self._method, kwargs)
100
+ if method_name == "ask_question":
101
+ return self._factory._handle_message_protocol(self._method, kwargs)
102
+ return self._method(**kwargs)
103
+
104
+ return ActionWrapper(self, original_method)
105
+
106
+ def _handle_execute_protocol(self, method: Any, kwargs: dict) -> Any:
107
+ """Handles the complex parameter injection for the EXECUTE action."""
108
+ execute_params = {
109
+ k: v
110
+ for k, v in kwargs.items()
111
+ if k in ("command", "cwd", "env", "background", "timeout", "max_lines")
112
+ and v is not None
113
+ }
114
+ if "command" not in execute_params:
115
+ raise ValueError("'command' parameter is required for the execute action.")
116
+
117
+ # Extract Tail override from action params and convert to max_lines
118
+ tail = kwargs.get("tail")
119
+ if tail is not None:
120
+ try:
121
+ tail_int = int(tail)
122
+ if tail_int > 0:
123
+ execute_params["max_lines"] = tail_int
124
+ except (ValueError, TypeError):
125
+ pass # Invalid tail value, fall back to default
126
+
127
+ # Inject global timeout if not already specified in kwargs
128
+ if "timeout" not in execute_params and self._config_service:
129
+ # Safe-by-Default: Provide hardcoded 60.0 fallback if config is missing
130
+ default_timeout = self._config_service.get_setting(
131
+ "execution.default_timeout_seconds", 60.0
132
+ )
133
+ if default_timeout is not None:
134
+ execute_params["timeout"] = float(default_timeout)
135
+
136
+ return method(**execute_params)
137
+
138
+ def _handle_edit_protocol(self, method: Any, kwargs: dict) -> Any:
139
+ """Handles the similarity threshold injection for the EDIT action."""
140
+ # 1. Inject from config if missing
141
+ if "similarity_threshold" not in kwargs and self._config_service:
142
+ # Safe-by-Default: Provide domain default if config is missing
143
+ global_threshold = self._config_service.get_setting(
144
+ "execution.similarity_threshold", DEFAULT_SIMILARITY_THRESHOLD
145
+ )
146
+ if global_threshold is not None:
147
+ kwargs["similarity_threshold"] = float(global_threshold)
148
+ return method(**kwargs)
149
+
150
+ def _handle_message_protocol(self, method: Any, kwargs: dict) -> Any:
151
+ """Handles the positional argument mapping for the MESSAGE action."""
152
+ prompt = kwargs.get("prompt", kwargs.get("content", "")) or ""
153
+ return method(
154
+ prompt,
155
+ resources=kwargs.get("handoff_resources"),
156
+ agent_name=kwargs.get("agent_name"),
157
+ )
158
+
159
+ def _create_standard_action(
160
+ self, action_type: str, params: Optional[dict] = None
161
+ ) -> IAction:
162
+ """Creates an action handler for any action other than 'read'."""
163
+ action_type_key = self._normalize_action_type(action_type)
164
+ if action_type_key not in self._action_map:
165
+ raise ValueError(f"Unknown action type: '{action_type}'")
166
+
167
+ action_handler = self._action_map[action_type_key]
168
+ if action_handler in self._standalone_actions:
169
+ return action_handler()
170
+
171
+ method_map = {
172
+ "create_file": "create_file",
173
+ "edit": "edit_file",
174
+ "read_file": "read_file",
175
+ "message": "ask_question",
176
+ "research": "search",
177
+ "execute": "execute",
178
+ }
179
+
180
+ if action_type_key not in method_map:
181
+ if not hasattr(action_handler, "execute"):
182
+ raise NotImplementedError(
183
+ f"Adapter for {action_type} does not have a mapped method "
184
+ "or a default 'execute' method."
185
+ )
186
+ return action_handler
187
+
188
+ return self._get_action_wrapper(action_handler, method_map[action_type_key])
189
+
190
+ def create_action(self, action_type: str, params: Optional[dict] = None) -> IAction:
191
+ """
192
+ Looks up the adapter protocol for the given action type and binds the correct
193
+ adapter method to the `execute` method required by the IAction protocol.
194
+ """
195
+ if action_type.lower() == "read":
196
+ return self._create_read_action(params)
197
+ return self._create_standard_action(action_type, params)
@@ -0,0 +1,216 @@
1
+ from typing import Any, Optional
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ pass
6
+
7
+ from teddy_executor.core.domain.models import ActionData, ActionType
8
+ from teddy_executor.core.ports.inbound.plan_parser import InvalidPlanError
9
+ from teddy_executor.core.services.parser_infrastructure import (
10
+ _PeekableStream,
11
+ get_child_text,
12
+ get_action_heading,
13
+ consume_content_until_next_action,
14
+ )
15
+ from teddy_executor.core.services.parser_metadata import (
16
+ parse_action_metadata,
17
+ parse_env_from_metadata,
18
+ )
19
+
20
+
21
+ def parse_find_replace_pair(stream: _PeekableStream) -> Optional[dict[str, Any]]:
22
+ from mistletoe.block_token import Heading, CodeFence, BlockCode
23
+
24
+ find_heading = stream.peek()
25
+ if not (
26
+ isinstance(find_heading, Heading) and "FIND:" in get_child_text(find_heading)
27
+ ):
28
+ return None
29
+
30
+ stream.next()
31
+ find_code = stream.next()
32
+ if not isinstance(find_code, (CodeFence, BlockCode)):
33
+ raise InvalidPlanError(
34
+ "Missing code block for FIND in EDIT action.", offending_node=find_code
35
+ )
36
+ find_content = get_child_text(find_code).rstrip("\n")
37
+
38
+ replace_heading = stream.next()
39
+ if not (
40
+ isinstance(replace_heading, Heading)
41
+ and "REPLACE:" in get_child_text(replace_heading)
42
+ ):
43
+ raise InvalidPlanError(
44
+ "Missing REPLACE block after FIND block",
45
+ offending_node=replace_heading,
46
+ )
47
+
48
+ replace_code = stream.next()
49
+ if not isinstance(replace_code, (CodeFence, BlockCode)):
50
+ raise InvalidPlanError(
51
+ "Missing REPLACE block after FIND block",
52
+ offending_node=replace_code,
53
+ )
54
+ replace_content = get_child_text(replace_code).rstrip("\n")
55
+
56
+ return {
57
+ "find": find_content,
58
+ "replace": replace_content,
59
+ "find_node": find_code,
60
+ }
61
+
62
+
63
+ def parse_edit_action(
64
+ stream: _PeekableStream, valid_actions: set[str], node: Optional[Any] = None
65
+ ) -> ActionData:
66
+ from mistletoe.block_token import List as MdList
67
+
68
+ metadata_list = stream.next()
69
+ if not isinstance(metadata_list, MdList):
70
+ raise InvalidPlanError(
71
+ "EDIT action is missing metadata list.", offending_node=metadata_list
72
+ )
73
+ description, params = parse_action_metadata(
74
+ metadata_list,
75
+ link_key_map={"File Path": "path"},
76
+ text_key_map={
77
+ "Match All": "match_all",
78
+ },
79
+ )
80
+
81
+ if "match_all" in params:
82
+ params["match_all"] = str(params["match_all"]).lower() == "true"
83
+
84
+ edits = []
85
+ while stream.has_next():
86
+ if get_action_heading(stream.peek(), valid_actions):
87
+ break
88
+
89
+ pair = parse_find_replace_pair(stream)
90
+ if pair:
91
+ edits.append(pair)
92
+ else:
93
+ break
94
+
95
+ if not edits:
96
+ raise InvalidPlanError(
97
+ "EDIT action found no valid FIND/REPLACE blocks.",
98
+ offending_node=node,
99
+ )
100
+ params["edits"] = edits
101
+ return ActionData(type="EDIT", description=description, params=params, node=node)
102
+
103
+
104
+ def parse_execute_action(
105
+ stream: _PeekableStream, node: Optional[Any] = None
106
+ ) -> ActionData:
107
+ from mistletoe.block_token import List as MdList, CodeFence
108
+
109
+ metadata_list = stream.next()
110
+ if not isinstance(metadata_list, MdList):
111
+ raise InvalidPlanError(
112
+ "EXECUTE action is missing metadata list.", offending_node=metadata_list
113
+ )
114
+
115
+ description, params = parse_action_metadata(
116
+ metadata_list,
117
+ text_key_map={
118
+ "Expected Outcome": "expected_outcome",
119
+ "cwd": "cwd",
120
+ "Allow Failure": "allow_failure",
121
+ "Background": "background",
122
+ "Timeout": "timeout",
123
+ "Tail": "tail",
124
+ },
125
+ )
126
+
127
+ if "allow_failure" in params:
128
+ params["allow_failure"] = params["allow_failure"].lower() == "true"
129
+
130
+ if "background" in params:
131
+ params["background"] = params["background"].lower() == "true"
132
+
133
+ if "timeout" in params and params["timeout"]:
134
+ try:
135
+ params["timeout"] = int(params["timeout"])
136
+ except ValueError:
137
+ # Leave as string, ActionFactory or validation will handle it
138
+ pass
139
+
140
+ env_from_meta = parse_env_from_metadata(metadata_list)
141
+ if env_from_meta:
142
+ params["env"] = env_from_meta
143
+
144
+ command_block = stream.next()
145
+ if not isinstance(command_block, CodeFence):
146
+ raise InvalidPlanError(
147
+ "EXECUTE action is missing command code block.",
148
+ offending_node=command_block,
149
+ )
150
+
151
+ params["command"] = get_child_text(command_block).strip()
152
+
153
+ return ActionData(type="EXECUTE", description=description, params=params, node=node)
154
+
155
+
156
+ def parse_research_action(
157
+ stream: _PeekableStream, valid_actions: set[str], node: Optional[Any] = None
158
+ ) -> ActionData:
159
+ from mistletoe.block_token import List as MdList, CodeFence
160
+
161
+ metadata_list = stream.next()
162
+ if not isinstance(metadata_list, MdList):
163
+ raise InvalidPlanError("RESEARCH action is missing metadata list.")
164
+
165
+ description, _ = parse_action_metadata(metadata_list)
166
+ content_nodes = consume_content_until_next_action(stream, valid_actions)
167
+ queries = []
168
+ for content_node in content_nodes:
169
+ if isinstance(content_node, CodeFence):
170
+ raw_content = get_child_text(content_node)
171
+ for line in raw_content.splitlines():
172
+ query = line.strip()
173
+ if query:
174
+ queries.append(query)
175
+ if not queries:
176
+ raise InvalidPlanError("RESEARCH action found no query code blocks.")
177
+ return ActionData(
178
+ type="RESEARCH",
179
+ description=description,
180
+ params={"queries": queries},
181
+ node=node,
182
+ )
183
+
184
+
185
+ def parse_message_action(
186
+ stream: _PeekableStream,
187
+ node: Optional[Any] = None,
188
+ raw_content: Optional[str] = None,
189
+ ) -> ActionData:
190
+ """
191
+ Parses a MESSAGE action. If raw_content is provided (extracted directly
192
+ from the original plan text via heading line_number), use it as-is to
193
+ preserve blank lines and internal structure. Otherwise, fall back to
194
+ rendering AST nodes.
195
+ """
196
+ content = ""
197
+ if raw_content is not None:
198
+ content = raw_content.rstrip("\n")
199
+ elif stream.has_next():
200
+ from mistletoe.markdown_renderer import MarkdownRenderer
201
+
202
+ nodes = []
203
+ while stream.has_next():
204
+ nodes.append(stream.next())
205
+
206
+ if nodes:
207
+ with MarkdownRenderer() as renderer:
208
+ rendered_parts = [renderer.render(node) for node in nodes]
209
+ content = "".join(rendered_parts).strip()
210
+
211
+ return ActionData(
212
+ type=ActionType.MESSAGE,
213
+ description="Message to user",
214
+ params={"content": content},
215
+ node=node,
216
+ )
@@ -0,0 +1,84 @@
1
+ from typing import Any, Optional
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ pass
6
+
7
+ from teddy_executor.core.domain.models import ActionData
8
+ from teddy_executor.core.ports.inbound.plan_parser import InvalidPlanError
9
+ from teddy_executor.core.services.parser_infrastructure import (
10
+ _PeekableStream,
11
+ )
12
+ from teddy_executor.core.services.parser_metadata import (
13
+ parse_action_metadata,
14
+ )
15
+
16
+
17
+ def parse_create_action(
18
+ stream: _PeekableStream, node: Optional[Any] = None
19
+ ) -> ActionData:
20
+ from mistletoe.block_token import List as MdList, CodeFence
21
+
22
+ metadata_list = stream.next()
23
+ if not isinstance(metadata_list, MdList):
24
+ raise InvalidPlanError(
25
+ "CREATE action is missing metadata list.", offending_node=metadata_list
26
+ )
27
+
28
+ description, params = parse_action_metadata(
29
+ metadata_list,
30
+ link_key_map={"File Path": "path"},
31
+ text_key_map={"Overwrite": "overwrite"},
32
+ )
33
+
34
+ if "overwrite" in params:
35
+ params["overwrite"] = params["overwrite"].lower() == "true"
36
+
37
+ code_block = stream.next()
38
+ if not isinstance(code_block, CodeFence):
39
+ raise InvalidPlanError(
40
+ "CREATE action is missing a content code block.", offending_node=code_block
41
+ )
42
+
43
+ params["content"] = ""
44
+ if code_block.children:
45
+ children = list(code_block.children)
46
+ if children:
47
+ child = children[0]
48
+ if hasattr(child, "content"):
49
+ params["content"] = child.content.rstrip("\n")
50
+
51
+ return ActionData(type="CREATE", description=description, params=params, node=node)
52
+
53
+
54
+ def parse_resource_action(
55
+ stream: _PeekableStream, action_type: str, node: Optional[Any] = None
56
+ ) -> ActionData:
57
+ from mistletoe.block_token import List as MdList
58
+
59
+ metadata_list = stream.next()
60
+ if not isinstance(metadata_list, MdList):
61
+ raise InvalidPlanError(
62
+ f"{action_type} action is missing metadata list.",
63
+ offending_node=metadata_list,
64
+ )
65
+
66
+ description, params = parse_action_metadata(
67
+ metadata_list,
68
+ link_key_map={"Resource": "resource", "File Path": "path_alias"},
69
+ text_key_map={"Lines": "lines"},
70
+ )
71
+
72
+ if "path_alias" in params:
73
+ params["resource"] = params.pop("path_alias")
74
+ params["metadata_used_file_path_alias"] = True
75
+
76
+ return ActionData(
77
+ type=action_type, description=description, params=params, node=node
78
+ )
79
+
80
+
81
+ def parse_read_action(
82
+ stream: _PeekableStream, node: Optional[Any] = None
83
+ ) -> ActionData:
84
+ return parse_resource_action(stream, "READ", node=node)