pulse-coding-agent 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 (104) hide show
  1. pulse/__init__.py +5 -0
  2. pulse/__main__.py +4 -0
  3. pulse/agent.py +270 -0
  4. pulse/agent_manager.py +335 -0
  5. pulse/audit.py +70 -0
  6. pulse/auth.py +670 -0
  7. pulse/ci/github_client.py +66 -0
  8. pulse/ci/runner.py +28 -0
  9. pulse/cli.py +1075 -0
  10. pulse/cli_ui.py +977 -0
  11. pulse/config.py +167 -0
  12. pulse/context.py +960 -0
  13. pulse/conversations/__init__.py +8 -0
  14. pulse/conversations/manager.py +312 -0
  15. pulse/core/agent.py +188 -0
  16. pulse/core/planner.py +105 -0
  17. pulse/core/protocols.py +37 -0
  18. pulse/edits.py +65 -0
  19. pulse/episodic.py +93 -0
  20. pulse/eval/__init__.py +8 -0
  21. pulse/eval/trajectory_logger.py +91 -0
  22. pulse/eval/verifier.py +133 -0
  23. pulse/execution/__init__.py +5 -0
  24. pulse/execution/remote_task.py +76 -0
  25. pulse/git.py +162 -0
  26. pulse/interactive.py +234 -0
  27. pulse/mcp/__init__.py +4 -0
  28. pulse/mcp/client.py +215 -0
  29. pulse/mcp/local_tools.py +105 -0
  30. pulse/memory.py +212 -0
  31. pulse/mutations.py +283 -0
  32. pulse/orchestration/__init__.py +3 -0
  33. pulse/orchestration/orchestrator.py +162 -0
  34. pulse/patch.py +129 -0
  35. pulse/planner/__init__.py +3 -0
  36. pulse/planner/dag_planner.py +85 -0
  37. pulse/planner/execution_loop.py +159 -0
  38. pulse/production.py +235 -0
  39. pulse/provider.py +59 -0
  40. pulse/provider_keys.py +278 -0
  41. pulse/providers/__init__.py +26 -0
  42. pulse/providers/anthropic.py +65 -0
  43. pulse/providers/base.py +251 -0
  44. pulse/providers/deepseek.py +10 -0
  45. pulse/providers/failover.py +32 -0
  46. pulse/providers/gemini.py +66 -0
  47. pulse/providers/groq.py +10 -0
  48. pulse/providers/manager.py +262 -0
  49. pulse/providers/openai.py +40 -0
  50. pulse/providers/openrouter.py +20 -0
  51. pulse/py.typed +1 -0
  52. pulse/reasoning.py +570 -0
  53. pulse/refactor/__init__.py +3 -0
  54. pulse/refactor/impact_analyzer.py +44 -0
  55. pulse/repository.py +209 -0
  56. pulse/rpc.py +249 -0
  57. pulse/rule_synthesizer.py +54 -0
  58. pulse/runtime.py +217 -0
  59. pulse/safety/__init__.py +3 -0
  60. pulse/safety/safety_manager.py +97 -0
  61. pulse/sandbox/SECURITY.md +57 -0
  62. pulse/sandbox/__init__.py +57 -0
  63. pulse/sandbox/api.py +594 -0
  64. pulse/sandbox/audit.py +153 -0
  65. pulse/sandbox/backend/__init__.py +7 -0
  66. pulse/sandbox/backend/base.py +72 -0
  67. pulse/sandbox/backend/docker.py +498 -0
  68. pulse/sandbox/backend/host.py +140 -0
  69. pulse/sandbox/backend/remote.py +224 -0
  70. pulse/sandbox/errors.py +106 -0
  71. pulse/sandbox/filesystem.py +476 -0
  72. pulse/sandbox/git_safe.py +50 -0
  73. pulse/sandbox/lifecycle.py +88 -0
  74. pulse/sandbox/network.py +205 -0
  75. pulse/sandbox/path_validator.py +280 -0
  76. pulse/sandbox/policy.py +209 -0
  77. pulse/sandbox/process.py +331 -0
  78. pulse/sandbox/project.py +158 -0
  79. pulse/sandbox/python_safe.py +62 -0
  80. pulse/sandbox/remote/__init__.py +1 -0
  81. pulse/sandbox/remote/client.py +389 -0
  82. pulse/sandbox/remote/models.py +167 -0
  83. pulse/sandbox/remote/protocol.py +65 -0
  84. pulse/sandbox/remote/server.py +984 -0
  85. pulse/sandbox/remote/worker.py +175 -0
  86. pulse/sandbox/resources.py +236 -0
  87. pulse/sandbox/secrets.py +241 -0
  88. pulse/session_manager.py +365 -0
  89. pulse/software_engineer.py +189 -0
  90. pulse/storage.py +140 -0
  91. pulse/streaming.py +385 -0
  92. pulse/subprocesses.py +79 -0
  93. pulse/task_manager.py +2005 -0
  94. pulse/telemetry/__init__.py +25 -0
  95. pulse/telemetry/cost_tracker.py +95 -0
  96. pulse/telemetry/logger.py +110 -0
  97. pulse/tool_policy.py +197 -0
  98. pulse/tool_registry.py +163 -0
  99. pulse/tools.py +372 -0
  100. pulse/verification.py +118 -0
  101. pulse_coding_agent-0.1.0.dist-info/METADATA +211 -0
  102. pulse_coding_agent-0.1.0.dist-info/RECORD +104 -0
  103. pulse_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  104. pulse_coding_agent-0.1.0.dist-info/entry_points.txt +4 -0
pulse/reasoning.py ADDED
@@ -0,0 +1,570 @@
1
+ """Modular Reasoning Engine for Pulse.
2
+
3
+ Provides pre-execution intent analysis, risk-aware strategy selection, explicit
4
+ sanitized reasoning step generation (without chain-of-thought exposure),
5
+ integration with ContextManager, Planner, SafetyManager, and ToolRegistry,
6
+ along with retries, confidence scoring, and heuristic fallback logic.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ import logging
14
+ import re
15
+ from collections.abc import Sequence
16
+ from dataclasses import dataclass, field
17
+ from enum import Enum
18
+ from typing import Any
19
+
20
+ from pulse.core.planner import ExecutionPlan, PlanningRequest, RequestPlanner
21
+ from pulse.core.protocols import LLMProvider
22
+ from pulse.safety.safety_manager import RiskLevel, SafetyManager
23
+ from pulse.tool_registry import ToolRegistry
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Enums and Data Models
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ class IntentCategory(Enum):
34
+ """Primary intent categories for user requests."""
35
+
36
+ DIRECT_ANSWER = "direct_answer"
37
+ TOOL_EXECUTION = "tool_execution"
38
+ FILE_EDIT = "file_edit"
39
+ REPOSITORY_SEARCH = "repository_search"
40
+ CLARIFICATION_NEEDED = "clarification_needed"
41
+
42
+
43
+ class ExecutionStrategyType(Enum):
44
+ """Optimal execution strategy selected for fulfilling a request."""
45
+
46
+ DIRECT_RESPONSE = "direct_response"
47
+ PLANNED_TOOL_EXECUTION = "planned_tool_execution"
48
+ RECURSIVE_DECOMPOSITION = "recursive_decomposition"
49
+ SAFE_USER_CONFIRMATION = "safe_user_confirmation"
50
+ CLARIFICATION_REQUEST = "clarification_request"
51
+
52
+
53
+ @dataclass(slots=True)
54
+ class IntentAnalysis:
55
+ """Structured assessment of user prompt intent."""
56
+
57
+ category: IntentCategory
58
+ summary: str
59
+ confidence_score: float
60
+ detected_entities: list[str] = field(default_factory=list)
61
+ risk_level: RiskLevel = RiskLevel.LOW
62
+ ambiguities: list[str] = field(default_factory=list)
63
+
64
+
65
+ @dataclass(slots=True)
66
+ class ReasoningStep:
67
+ """One explicit, public reasoning step.
68
+
69
+ Internal LLM scratchpads / raw chain-of-thought tokens are stripped.
70
+ Only clean, high-level rationale is stored.
71
+ """
72
+
73
+ step_number: int
74
+ title: str
75
+ action: str
76
+ rationale: str
77
+ status: str = "completed"
78
+
79
+
80
+ @dataclass(slots=True)
81
+ class ExecutionStrategy:
82
+ """Strategy configuration chosen by the Reasoning Engine."""
83
+
84
+ strategy_type: ExecutionStrategyType
85
+ steps: list[ReasoningStep]
86
+ estimated_risk: RiskLevel
87
+ requires_planning: bool
88
+ requires_safety_approval: bool
89
+ selected_tools: list[str] = field(default_factory=list)
90
+ confidence: float = 1.0
91
+
92
+
93
+ @dataclass(slots=True)
94
+ class ReasoningResult:
95
+ """Final output object from the Reasoning Engine."""
96
+
97
+ intent: IntentAnalysis
98
+ strategy: ExecutionStrategy
99
+ reasoning_steps: list[ReasoningStep]
100
+ response_text: str | None = None
101
+ execution_plan: ExecutionPlan | Any | None = None
102
+ overall_confidence: float = 1.0
103
+ retries_used: int = 0
104
+ used_fallback: bool = False
105
+ metadata: dict[str, Any] = field(default_factory=dict)
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Reasoning Engine
110
+ # ---------------------------------------------------------------------------
111
+
112
+
113
+ class ReasoningEngine:
114
+ """Provider-agnostic, async Reasoning Engine for Pulse.
115
+
116
+ Coordinates intent classification, risk assessment, context gathering,
117
+ tool discovery, execution planning, and response formulation with full
118
+ retry capability and deterministic heuristic fallbacks.
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ *,
124
+ provider: LLMProvider | Any | None = None,
125
+ context_manager: Any | None = None,
126
+ planner: Any | None = None,
127
+ safety_manager: SafetyManager | None = None,
128
+ tool_registry: ToolRegistry | None = None,
129
+ max_retries: int = 3,
130
+ confidence_threshold: float = 0.6,
131
+ ) -> None:
132
+ self.provider = provider
133
+ self.context_manager = context_manager
134
+ self.planner = planner or RequestPlanner()
135
+ self.safety_manager = safety_manager or SafetyManager()
136
+ self.tool_registry = tool_registry
137
+ self.max_retries = max_retries
138
+ self.confidence_threshold = confidence_threshold
139
+
140
+ async def reason(
141
+ self,
142
+ request: str,
143
+ *,
144
+ active_file: str | None = None,
145
+ history: Sequence[Any] | None = None,
146
+ ) -> ReasoningResult:
147
+ """Main async entrypoint for analyzing and reasoning over a request."""
148
+ clean_request = request.strip()
149
+ if not clean_request:
150
+ return self._empty_request_result()
151
+
152
+ # 1. Gather context from ContextManager if available
153
+ context_strings: list[str] = []
154
+ if self.context_manager and hasattr(self.context_manager, "as_strings"):
155
+ try:
156
+ context_strings = await self.context_manager.as_strings(
157
+ clean_request, active_file=active_file
158
+ )
159
+ # Intentionally broad to isolate execution boundaries and prevent crashes.
160
+ except Exception as err: # noqa: BLE001
161
+ logger.warning(f"ContextManager gather failed: {err}")
162
+
163
+ # 2. Analyze intent with retry & fallback
164
+ intent, retries_used, used_fallback = await self._analyze_intent_with_fallback(
165
+ clean_request, context_strings
166
+ )
167
+
168
+ # 3. Select optimal execution strategy
169
+ strategy = await self.select_strategy(intent, context_strings)
170
+
171
+ # 4. Formulate explicit sanitized reasoning steps
172
+ reasoning_steps = self._sanitize_reasoning_steps(strategy.steps)
173
+
174
+ # 5. Build ExecutionPlan if strategy requires planning
175
+ execution_plan: ExecutionPlan | Any | None = None
176
+ if strategy.requires_planning and self.planner:
177
+ try:
178
+ planning_req = PlanningRequest(
179
+ message=clean_request,
180
+ metadata={
181
+ "category": intent.category.value,
182
+ "risk": intent.risk_level.value,
183
+ "tools": strategy.selected_tools,
184
+ },
185
+ )
186
+ if hasattr(self.planner, "plan"):
187
+ res = self.planner.plan(planning_req)
188
+ execution_plan = await res if asyncio.iscoroutine(res) else res
189
+ # Intentionally broad to isolate execution boundaries and prevent crashes.
190
+ except Exception as err: # noqa: BLE001
191
+ logger.warning(f"Planner failed during reasoning: {err}")
192
+
193
+ # 6. Formulate immediate direct response text if strategy is Direct Answer or Clarification
194
+ response_text: str | None = None
195
+ if strategy.strategy_type == ExecutionStrategyType.CLARIFICATION_REQUEST:
196
+ response_text = self._format_clarification_prompt(clean_request, intent)
197
+ elif strategy.strategy_type == ExecutionStrategyType.DIRECT_RESPONSE and self.provider:
198
+ response_text = await self._generate_direct_response(clean_request, context_strings)
199
+
200
+ overall_confidence = round(
201
+ min(intent.confidence_score, strategy.confidence), 2
202
+ )
203
+
204
+ return ReasoningResult(
205
+ intent=intent,
206
+ strategy=strategy,
207
+ reasoning_steps=reasoning_steps,
208
+ response_text=response_text,
209
+ execution_plan=execution_plan,
210
+ overall_confidence=overall_confidence,
211
+ retries_used=retries_used,
212
+ used_fallback=used_fallback,
213
+ metadata={
214
+ "context_items_used": len(context_strings),
215
+ "active_file": active_file,
216
+ "detected_entities_count": len(intent.detected_entities),
217
+ },
218
+ )
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Intent Analysis & Strategy Selection
222
+ # ---------------------------------------------------------------------------
223
+
224
+ async def analyze_intent(
225
+ self, request: str, context: list[str] | None = None
226
+ ) -> IntentAnalysis:
227
+ """Analyzes request to classify intent, risk level, and detected entities."""
228
+ if not request.strip():
229
+ return self._heuristic_intent_analysis("")
230
+
231
+ if self.provider and hasattr(self.provider, "chat"):
232
+ try:
233
+ # LLM-based intent extraction
234
+ intent_data = await self._llm_intent_classification(request, context)
235
+ if intent_data and intent_data.confidence_score >= self.confidence_threshold:
236
+ return intent_data
237
+ # Intentionally broad to isolate execution boundaries and prevent crashes.
238
+ except Exception as err: # noqa: BLE001
239
+ logger.debug(f"LLM intent analysis exception: {err}")
240
+
241
+ # Deterministic fallback
242
+ return self._heuristic_intent_analysis(request)
243
+
244
+ async def select_strategy(
245
+ self, intent: IntentAnalysis, context: list[str] | None = None
246
+ ) -> ExecutionStrategy:
247
+ """Determines the optimal execution strategy based on intent, safety, and tools."""
248
+ # Risk assessment via SafetyManager
249
+ risk = intent.risk_level
250
+ selected_tools: list[str] = []
251
+
252
+ # Tool discovery via ToolRegistry
253
+ if self.tool_registry:
254
+ registered_tools = self.tool_registry.discover()
255
+ for tool in registered_tools:
256
+ # Simple keyword/matching heuristic for tool selection
257
+ if any(
258
+ entity.lower() in tool.name.lower() or tool.name.lower() in entity.lower()
259
+ for entity in intent.detected_entities
260
+ ) or (intent.category == IntentCategory.TOOL_EXECUTION and tool.name):
261
+ selected_tools.append(tool.name)
262
+
263
+ # Strategy decision matrix
264
+ if intent.category == IntentCategory.CLARIFICATION_NEEDED:
265
+ strategy_type = ExecutionStrategyType.CLARIFICATION_REQUEST
266
+ requires_planning = False
267
+ requires_safety = False
268
+ elif risk == RiskLevel.HIGH:
269
+ strategy_type = ExecutionStrategyType.SAFE_USER_CONFIRMATION
270
+ requires_planning = True
271
+ requires_safety = True
272
+ elif intent.category == IntentCategory.FILE_EDIT or intent.category == IntentCategory.TOOL_EXECUTION:
273
+ strategy_type = ExecutionStrategyType.PLANNED_TOOL_EXECUTION
274
+ requires_planning = True
275
+ requires_safety = risk in (RiskLevel.MEDIUM, RiskLevel.HIGH)
276
+ elif intent.category == IntentCategory.REPOSITORY_SEARCH:
277
+ strategy_type = ExecutionStrategyType.PLANNED_TOOL_EXECUTION
278
+ requires_planning = False
279
+ requires_safety = False
280
+ else: # DIRECT_ANSWER
281
+ strategy_type = ExecutionStrategyType.DIRECT_RESPONSE
282
+ requires_planning = False
283
+ requires_safety = False
284
+
285
+ steps = self._build_strategy_steps(intent, strategy_type, risk, selected_tools)
286
+
287
+ return ExecutionStrategy(
288
+ strategy_type=strategy_type,
289
+ steps=steps,
290
+ estimated_risk=risk,
291
+ requires_planning=requires_planning,
292
+ requires_safety_approval=requires_safety,
293
+ selected_tools=selected_tools,
294
+ confidence=intent.confidence_score,
295
+ )
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # Internal Helpers, Fallbacks & Sanitization
299
+ # ---------------------------------------------------------------------------
300
+
301
+ async def _analyze_intent_with_fallback(
302
+ self, request: str, context: list[str]
303
+ ) -> tuple[IntentAnalysis, int, bool]:
304
+ """Runs intent analysis with retries and deterministic fallback."""
305
+ if not self.provider:
306
+ return self._heuristic_intent_analysis(request), 0, False
307
+
308
+ retries = 0
309
+ for attempt in range(self.max_retries):
310
+ try:
311
+ intent = await self._llm_intent_classification(request, context)
312
+ if intent and intent.confidence_score >= self.confidence_threshold:
313
+ return intent, attempt, False
314
+ # Intentionally broad to isolate execution boundaries and prevent crashes.
315
+ except Exception as err: # noqa: BLE001
316
+ logger.warning(f"LLM intent classification attempt {attempt + 1} failed: {err}")
317
+ retries += 1
318
+
319
+ fallback_intent = self._heuristic_intent_analysis(request)
320
+ return fallback_intent, retries, True
321
+
322
+ def _heuristic_intent_analysis(self, request: str) -> IntentAnalysis:
323
+ """Deterministic heuristic analysis for rule-based fallback and instant parsing."""
324
+ req_lower = request.lower().strip()
325
+ if not req_lower:
326
+ return IntentAnalysis(
327
+ category=IntentCategory.CLARIFICATION_NEEDED,
328
+ summary="Empty user prompt provided.",
329
+ confidence_score=1.0,
330
+ risk_level=RiskLevel.LOW,
331
+ ambiguities=["No request content."],
332
+ )
333
+
334
+ # Detect files & symbols
335
+ entities = re.findall(r"[a-zA-Z0-9_./-]+\.[a-zA-Z0-9_]+", request)
336
+
337
+ # Risk assessment
338
+ risk = self.safety_manager.assess_risk(request)
339
+
340
+ # Heuristic rules
341
+ if any(kw in req_lower for kw in ["how much work", "what is", "explain", "describe", "why does", "tell me about"]):
342
+ category = IntentCategory.DIRECT_ANSWER
343
+ confidence = 0.85
344
+ elif any(kw in req_lower for kw in ["find", "search", "where is", "grep", "locate"]):
345
+ category = IntentCategory.REPOSITORY_SEARCH
346
+ confidence = 0.85
347
+ elif any(kw in req_lower for kw in ["fix", "edit", "update", "modify", "add", "change", "refactor", "implement", "write"]):
348
+ category = IntentCategory.FILE_EDIT
349
+ confidence = 0.80
350
+ elif any(kw in req_lower for kw in ["run", "execute", "test", "pytest", "build", "terminal", "command"]):
351
+ category = IntentCategory.TOOL_EXECUTION
352
+ confidence = 0.80
353
+ elif len(req_lower.split()) < 2:
354
+ category = IntentCategory.CLARIFICATION_NEEDED
355
+ confidence = 0.90
356
+ else:
357
+ category = IntentCategory.DIRECT_ANSWER
358
+ confidence = 0.70
359
+
360
+ return IntentAnalysis(
361
+ category=category,
362
+ summary=f"Parsed intent: {category.value}",
363
+ confidence_score=confidence,
364
+ detected_entities=list(dict.fromkeys(entities)),
365
+ risk_level=risk,
366
+ ambiguities=[],
367
+ )
368
+
369
+ async def _llm_intent_classification(
370
+ self, request: str, context: list[str] | None
371
+ ) -> IntentAnalysis | None:
372
+ """Queries LLM provider for structured intent classification."""
373
+ if not self.provider:
374
+ return None
375
+
376
+ prompt = (
377
+ "Analyze the following user prompt for a software engineering assistant.\n"
378
+ "Output JSON with fields:\n"
379
+ ' "category": one of ["direct_answer", "tool_execution", "file_edit", "repository_search", "clarification_needed"]\n'
380
+ ' "summary": concise string summary of user intent\n'
381
+ ' "confidence": float between 0.0 and 1.0\n'
382
+ ' "entities": array of file paths or code symbols referenced\n'
383
+ ' "ambiguities": array of missing or ambiguous parameters\n\n'
384
+ f"Prompt: {request}\n"
385
+ )
386
+
387
+ messages = [{"role": "user", "content": prompt}]
388
+ if hasattr(self.provider, "_chat") and asyncio.iscoroutinefunction(self.provider._chat):
389
+ raw_response = await self.provider._chat(messages, temperature=0.1)
390
+ else:
391
+ res = await asyncio.to_thread(self.provider.chat, messages, 0.1)
392
+ raw_response = str(await res) if (asyncio.iscoroutine(res) or hasattr(res, "__await__")) else str(res)
393
+
394
+ # Parse JSON from LLM response
395
+ match = re.search(r"\{.*\}", raw_response, re.DOTALL)
396
+ if not match:
397
+ return None
398
+
399
+ data = json.loads(match.group(0))
400
+ cat_str = data.get("category", "direct_answer")
401
+ category = next((c for c in IntentCategory if c.value == cat_str), IntentCategory.DIRECT_ANSWER)
402
+ risk = self.safety_manager.assess_risk(request)
403
+
404
+ return IntentAnalysis(
405
+ category=category,
406
+ summary=data.get("summary", "LLM Intent Analysis"),
407
+ confidence_score=float(data.get("confidence", 0.8)),
408
+ detected_entities=data.get("entities", []),
409
+ risk_level=risk,
410
+ ambiguities=data.get("ambiguities", []),
411
+ )
412
+
413
+ def _sanitize_reasoning_steps(
414
+ self, steps: Sequence[ReasoningStep | dict[str, Any]]
415
+ ) -> list[ReasoningStep]:
416
+ """Ensures reasoning steps are clean public summaries without raw CoT / scratchpads."""
417
+ sanitized: list[ReasoningStep] = []
418
+ for index, item in enumerate(steps, start=1):
419
+ if isinstance(item, ReasoningStep):
420
+ clean_rationale = self._strip_cot(item.rationale)
421
+ sanitized.append(
422
+ ReasoningStep(
423
+ step_number=index,
424
+ title=item.title,
425
+ action=item.action,
426
+ rationale=clean_rationale,
427
+ status=item.status,
428
+ )
429
+ )
430
+ elif isinstance(item, dict):
431
+ clean_rationale = self._strip_cot(str(item.get("rationale", "")))
432
+ sanitized.append(
433
+ ReasoningStep(
434
+ step_number=index,
435
+ title=str(item.get("title", f"Step {index}")),
436
+ action=str(item.get("action", "evaluate")),
437
+ rationale=clean_rationale,
438
+ status=str(item.get("status", "completed")),
439
+ )
440
+ )
441
+ return sanitized
442
+
443
+ @staticmethod
444
+ def _strip_cot(text: str) -> str:
445
+ """Removes internal chain-of-thought blocks (<think>, [scratchpad], etc.)."""
446
+ cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
447
+ cleaned = re.sub(r"\[scratchpad\].*?\[/scratchpad\]", "", cleaned, flags=re.DOTALL)
448
+ return cleaned.strip()
449
+
450
+ def _build_strategy_steps(
451
+ self,
452
+ intent: IntentAnalysis,
453
+ strategy_type: ExecutionStrategyType,
454
+ risk: RiskLevel,
455
+ tools: list[str],
456
+ ) -> list[ReasoningStep]:
457
+ """Generates standard public reasoning steps based on strategy decision."""
458
+ steps: list[ReasoningStep] = [
459
+ ReasoningStep(
460
+ step_number=1,
461
+ title="Intent Analysis",
462
+ action="analyze_intent",
463
+ rationale=f"Assessed user request as '{intent.category.value}' with {risk.value} risk level.",
464
+ ),
465
+ ReasoningStep(
466
+ step_number=2,
467
+ title="Safety & Context Check",
468
+ action="verify_safety",
469
+ rationale=f"Evaluated execution safety ({risk.value} risk) and gathered active context.",
470
+ ),
471
+ ]
472
+
473
+ if strategy_type == ExecutionStrategyType.CLARIFICATION_REQUEST:
474
+ steps.append(
475
+ ReasoningStep(
476
+ step_number=3,
477
+ title="Formulate Clarification Request",
478
+ action="request_clarification",
479
+ rationale="Identified ambiguity or underspecified requirements in prompt.",
480
+ )
481
+ )
482
+ elif strategy_type == ExecutionStrategyType.SAFE_USER_CONFIRMATION:
483
+ steps.append(
484
+ ReasoningStep(
485
+ step_number=3,
486
+ title="Prepare User Confirmation",
487
+ action="request_confirmation",
488
+ rationale=f"High-risk action detected ({risk.value}). Requires explicit user approval.",
489
+ )
490
+ )
491
+ elif strategy_type == ExecutionStrategyType.PLANNED_TOOL_EXECUTION:
492
+ steps.append(
493
+ ReasoningStep(
494
+ step_number=3,
495
+ title="Formulate Execution Plan",
496
+ action="generate_plan",
497
+ rationale=f"Constructed structured execution plan utilizing tools: {tools or ['default_tools']}.",
498
+ )
499
+ )
500
+ else: # DIRECT_RESPONSE
501
+ steps.append(
502
+ ReasoningStep(
503
+ step_number=3,
504
+ title="Generate Direct Response",
505
+ action="direct_answer",
506
+ rationale="Formulated direct textual answer from gathered context.",
507
+ )
508
+ )
509
+
510
+ return steps
511
+
512
+ def _format_clarification_prompt(self, request: str, intent: IntentAnalysis) -> str:
513
+ """Builds user-facing clarification message."""
514
+ ambiguities = ", ".join(intent.ambiguities) if intent.ambiguities else "underspecified parameters"
515
+ return (
516
+ f"Could you please clarify your request ('{request}')? "
517
+ f"Additional details regarding {ambiguities} will help me provide an accurate solution."
518
+ )
519
+
520
+ async def _generate_direct_response(
521
+ self, request: str, context: list[str]
522
+ ) -> str:
523
+ """Generates direct response via LLM provider."""
524
+ if not self.provider:
525
+ return f"Processed request: {request}"
526
+
527
+ prompt = "Answer the request based on the context:\n\nContext:\n" + "\n".join(context) + f"\n\nRequest: {request}"
528
+ messages = [{"role": "user", "content": prompt}]
529
+ try:
530
+ if hasattr(self.provider, "_chat") and asyncio.iscoroutinefunction(self.provider._chat):
531
+ return await self.provider._chat(messages, temperature=0.2)
532
+ res = await asyncio.to_thread(self.provider.chat, messages, 0.2)
533
+ if asyncio.iscoroutine(res) or hasattr(res, "__await__"):
534
+ return str(await res)
535
+ return str(res)
536
+ # Intentionally broad to isolate execution boundaries and prevent crashes.
537
+ except Exception as err: # noqa: BLE001
538
+ logger.warning(f"Direct response generation failed: {err}")
539
+ return f"Direct response unavailable due to provider error ({err})."
540
+
541
+ def _empty_request_result(self) -> ReasoningResult:
542
+ """Handles empty requests gracefully."""
543
+ intent = IntentAnalysis(
544
+ category=IntentCategory.CLARIFICATION_NEEDED,
545
+ summary="Empty user prompt.",
546
+ confidence_score=1.0,
547
+ risk_level=RiskLevel.LOW,
548
+ ambiguities=["No message provided."],
549
+ )
550
+ strategy = ExecutionStrategy(
551
+ strategy_type=ExecutionStrategyType.CLARIFICATION_REQUEST,
552
+ steps=[
553
+ ReasoningStep(
554
+ step_number=1,
555
+ title="Empty Request",
556
+ action="clarify",
557
+ rationale="User submitted empty text prompt.",
558
+ )
559
+ ],
560
+ estimated_risk=RiskLevel.LOW,
561
+ requires_planning=False,
562
+ requires_safety_approval=False,
563
+ )
564
+ return ReasoningResult(
565
+ intent=intent,
566
+ strategy=strategy,
567
+ reasoning_steps=strategy.steps,
568
+ response_text="Please provide a message or question to process.",
569
+ overall_confidence=1.0,
570
+ )
@@ -0,0 +1,3 @@
1
+ from pulse.refactor.impact_analyzer import ASTImpactAnalyzer
2
+
3
+ __all__ = ["ASTImpactAnalyzer"]
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import Path
5
+
6
+
7
+ class ASTImpactAnalyzer:
8
+ """Parses Python files to construct symbol cross-references and calculate refactoring impact."""
9
+
10
+ def __init__(self, workspace: Path) -> None:
11
+ self.workspace = workspace.resolve()
12
+ self._symbol_map: dict[str, set[str]] = {}
13
+ self._analyzed = False
14
+
15
+ def build_symbol_map(self) -> None:
16
+ self._symbol_map.clear()
17
+ for path in self.workspace.rglob("*.py"):
18
+ # Skip ignored directories
19
+ if any(part in {".git", ".agent", ".venv", "venv", "__pycache__", ".pytest_cache"} for part in path.parts):
20
+ continue
21
+ relative_path = path.relative_to(self.workspace).as_posix()
22
+ try:
23
+ content = path.read_text(encoding="utf-8", errors="replace")
24
+ tree = ast.parse(content)
25
+ for node in ast.walk(tree):
26
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
27
+ self._add_symbol(node.name, relative_path)
28
+ elif isinstance(node, ast.Name):
29
+ self._add_symbol(node.id, relative_path)
30
+ elif isinstance(node, ast.Attribute):
31
+ self._add_symbol(node.attr, relative_path)
32
+ except SyntaxError:
33
+ continue
34
+ self._analyzed = True
35
+
36
+ def get_affected_files(self, symbol_name: str) -> list[str]:
37
+ if not self._analyzed:
38
+ self.build_symbol_map()
39
+ return sorted(self._symbol_map.get(symbol_name, set()))
40
+
41
+ def _add_symbol(self, symbol: str, file_path: str) -> None:
42
+ if symbol not in self._symbol_map:
43
+ self._symbol_map[symbol] = set()
44
+ self._symbol_map[symbol].add(file_path)