oagi-core 0.10.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 (68) hide show
  1. oagi/__init__.py +148 -0
  2. oagi/agent/__init__.py +33 -0
  3. oagi/agent/default.py +124 -0
  4. oagi/agent/factories.py +74 -0
  5. oagi/agent/observer/__init__.py +38 -0
  6. oagi/agent/observer/agent_observer.py +99 -0
  7. oagi/agent/observer/events.py +28 -0
  8. oagi/agent/observer/exporters.py +445 -0
  9. oagi/agent/observer/protocol.py +12 -0
  10. oagi/agent/protocol.py +55 -0
  11. oagi/agent/registry.py +155 -0
  12. oagi/agent/tasker/__init__.py +33 -0
  13. oagi/agent/tasker/memory.py +160 -0
  14. oagi/agent/tasker/models.py +77 -0
  15. oagi/agent/tasker/planner.py +408 -0
  16. oagi/agent/tasker/taskee_agent.py +512 -0
  17. oagi/agent/tasker/tasker_agent.py +324 -0
  18. oagi/cli/__init__.py +11 -0
  19. oagi/cli/agent.py +281 -0
  20. oagi/cli/display.py +56 -0
  21. oagi/cli/main.py +77 -0
  22. oagi/cli/server.py +94 -0
  23. oagi/cli/tracking.py +55 -0
  24. oagi/cli/utils.py +89 -0
  25. oagi/client/__init__.py +12 -0
  26. oagi/client/async_.py +290 -0
  27. oagi/client/base.py +457 -0
  28. oagi/client/sync.py +293 -0
  29. oagi/exceptions.py +118 -0
  30. oagi/handler/__init__.py +24 -0
  31. oagi/handler/_macos.py +55 -0
  32. oagi/handler/async_pyautogui_action_handler.py +44 -0
  33. oagi/handler/async_screenshot_maker.py +47 -0
  34. oagi/handler/pil_image.py +102 -0
  35. oagi/handler/pyautogui_action_handler.py +291 -0
  36. oagi/handler/screenshot_maker.py +41 -0
  37. oagi/logging.py +55 -0
  38. oagi/server/__init__.py +13 -0
  39. oagi/server/agent_wrappers.py +98 -0
  40. oagi/server/config.py +46 -0
  41. oagi/server/main.py +157 -0
  42. oagi/server/models.py +98 -0
  43. oagi/server/session_store.py +116 -0
  44. oagi/server/socketio_server.py +405 -0
  45. oagi/task/__init__.py +21 -0
  46. oagi/task/async_.py +101 -0
  47. oagi/task/async_short.py +76 -0
  48. oagi/task/base.py +157 -0
  49. oagi/task/short.py +76 -0
  50. oagi/task/sync.py +99 -0
  51. oagi/types/__init__.py +50 -0
  52. oagi/types/action_handler.py +30 -0
  53. oagi/types/async_action_handler.py +30 -0
  54. oagi/types/async_image_provider.py +38 -0
  55. oagi/types/image.py +17 -0
  56. oagi/types/image_provider.py +35 -0
  57. oagi/types/models/__init__.py +32 -0
  58. oagi/types/models/action.py +33 -0
  59. oagi/types/models/client.py +68 -0
  60. oagi/types/models/image_config.py +47 -0
  61. oagi/types/models/step.py +17 -0
  62. oagi/types/step_observer.py +93 -0
  63. oagi/types/url.py +3 -0
  64. oagi_core-0.10.1.dist-info/METADATA +245 -0
  65. oagi_core-0.10.1.dist-info/RECORD +68 -0
  66. oagi_core-0.10.1.dist-info/WHEEL +4 -0
  67. oagi_core-0.10.1.dist-info/entry_points.txt +2 -0
  68. oagi_core-0.10.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,160 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ from typing import Any
10
+
11
+ from .models import Action, Todo, TodoHistory, TodoStatus
12
+
13
+
14
+ class PlannerMemory:
15
+ """In-memory state management for the planner agent.
16
+
17
+ This class manages the hierarchical task execution state for TaskerAgent.
18
+ It provides methods for:
19
+ - Task/todo management
20
+ - Execution history tracking
21
+ - Memory state serialization
22
+
23
+ Context formatting for backend API calls is handled by the backend.
24
+ """
25
+
26
+ def __init__(self):
27
+ """Initialize empty memory."""
28
+ self.task_description: str = ""
29
+ self.todos: list[Todo] = []
30
+ self.history: list[TodoHistory] = []
31
+ self.task_execution_summary: str = ""
32
+ self.todo_execution_summaries: dict[int, str] = {}
33
+
34
+ def set_task(
35
+ self,
36
+ task_description: str,
37
+ todos: list[str] | list[Todo],
38
+ ) -> None:
39
+ """Set the task and todos.
40
+
41
+ Args:
42
+ task_description: Overall task description
43
+ todos: List of todo items (strings or Todo objects)
44
+ """
45
+ self.task_description = task_description
46
+
47
+ # Convert todos
48
+ self.todos = []
49
+ for todo in todos:
50
+ if isinstance(todo, str):
51
+ self.todos.append(Todo(description=todo))
52
+ else:
53
+ self.todos.append(todo)
54
+
55
+ def get_current_todo(self) -> tuple[Todo | None, int]:
56
+ """Get the next pending or in-progress todo.
57
+
58
+ Returns:
59
+ Tuple of (Todo object, index) or (None, -1) if no todos remain
60
+ """
61
+ for idx, todo in enumerate(self.todos):
62
+ if todo.status in [TodoStatus.PENDING, TodoStatus.IN_PROGRESS]:
63
+ return todo, idx
64
+ return None, -1
65
+
66
+ def update_todo(
67
+ self,
68
+ index: int,
69
+ status: TodoStatus | str,
70
+ summary: str | None = None,
71
+ ) -> None:
72
+ """Update a todo's status and optionally its summary.
73
+
74
+ Args:
75
+ index: Index of the todo to update
76
+ status: New status for the todo
77
+ summary: Optional execution summary
78
+ """
79
+ if 0 <= index < len(self.todos):
80
+ if isinstance(status, str):
81
+ status = TodoStatus(status)
82
+ self.todos[index].status = status
83
+ if summary:
84
+ self.todo_execution_summaries[index] = summary
85
+
86
+ def add_history(
87
+ self,
88
+ todo_index: int,
89
+ actions: list[Action],
90
+ summary: str | None = None,
91
+ completed: bool = False,
92
+ ) -> None:
93
+ """Add execution history for a todo.
94
+
95
+ Args:
96
+ todo_index: Index of the todo
97
+ actions: List of actions taken
98
+ summary: Optional execution summary
99
+ completed: Whether the todo was completed
100
+ """
101
+ if 0 <= todo_index < len(self.todos):
102
+ self.history.append(
103
+ TodoHistory(
104
+ todo_index=todo_index,
105
+ todo=self.todos[todo_index].description,
106
+ actions=actions,
107
+ summary=summary,
108
+ completed=completed,
109
+ )
110
+ )
111
+
112
+ def get_context(self) -> dict[str, Any]:
113
+ """Get the full context for planning/reflection.
114
+
115
+ Returns:
116
+ Dictionary containing all memory state
117
+ """
118
+ return {
119
+ "task_description": self.task_description,
120
+ "todos": [
121
+ {"index": i, "description": t.description, "status": t.status}
122
+ for i, t in enumerate(self.todos)
123
+ ],
124
+ "history": [
125
+ {
126
+ "todo_index": h.todo_index,
127
+ "todo": h.todo,
128
+ "action_count": len(h.actions),
129
+ "summary": h.summary,
130
+ "completed": h.completed,
131
+ }
132
+ for h in self.history
133
+ ],
134
+ "task_execution_summary": self.task_execution_summary,
135
+ "todo_execution_summaries": self.todo_execution_summaries,
136
+ }
137
+
138
+ def get_todo_status_summary(self) -> dict[str, int]:
139
+ """Get a summary of todo statuses.
140
+
141
+ Returns:
142
+ Dictionary with counts for each status
143
+ """
144
+ summary = {
145
+ TodoStatus.PENDING: 0,
146
+ TodoStatus.IN_PROGRESS: 0,
147
+ TodoStatus.COMPLETED: 0,
148
+ TodoStatus.SKIPPED: 0,
149
+ }
150
+ for todo in self.todos:
151
+ summary[todo.status] += 1
152
+ return summary
153
+
154
+ def append_todo(self, description: str) -> None:
155
+ """Append a new todo to the list.
156
+
157
+ Args:
158
+ description: Description of the new todo
159
+ """
160
+ self.todos.append(Todo(description=description))
@@ -0,0 +1,77 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+
15
+ class TodoStatus(str, Enum):
16
+ """Status of a todo item in the workflow."""
17
+
18
+ PENDING = "pending"
19
+ IN_PROGRESS = "in_progress"
20
+ COMPLETED = "completed"
21
+ SKIPPED = "skipped"
22
+
23
+
24
+ class Todo(BaseModel):
25
+ """A single todo item in the workflow."""
26
+
27
+ description: str
28
+ status: TodoStatus = TodoStatus.PENDING
29
+
30
+
31
+ class Action(BaseModel):
32
+ """An action taken during execution."""
33
+
34
+ timestamp: str
35
+ action_type: str # "plan", "reflect", "click", "type", "scroll", etc.
36
+ target: str | None = None
37
+ details: dict[str, Any] = Field(default_factory=dict)
38
+ reasoning: str | None = None
39
+ result: str | None = None
40
+ screenshot_uuid: str | None = None # UUID of uploaded screenshot for this action
41
+
42
+
43
+ class TodoHistory(BaseModel):
44
+ """Execution history for a specific todo."""
45
+
46
+ todo_index: int
47
+ todo: str
48
+ actions: list[Action]
49
+ summary: str | None = None
50
+ completed: bool = False
51
+
52
+
53
+ class PlannerOutput(BaseModel):
54
+ """Output from the LLM planner's initial planning."""
55
+
56
+ instruction: str # Clear instruction for the todo
57
+ reasoning: str # Planner's reasoning
58
+ subtodos: list[str] = Field(default_factory=list) # Optional subtasks
59
+
60
+
61
+ class ReflectionOutput(BaseModel):
62
+ """Output from the LLM planner's reflection."""
63
+
64
+ continue_current: bool # Whether to continue with current approach
65
+ new_instruction: str | None = None # New instruction if pivoting
66
+ reasoning: str # Reflection reasoning
67
+ success_assessment: bool = False # Whether the task appears successful
68
+
69
+
70
+ class ExecutionResult(BaseModel):
71
+ """Result from executing a single todo."""
72
+
73
+ success: bool
74
+ actions: list[Action]
75
+ summary: str
76
+ error: str | None = None
77
+ total_steps: int = 0
@@ -0,0 +1,408 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ import json
10
+ from typing import Any
11
+
12
+ from ...client import AsyncClient
13
+ from ...types import URL, Image
14
+ from .memory import PlannerMemory
15
+ from .models import Action, PlannerOutput, ReflectionOutput
16
+
17
+
18
+ class Planner:
19
+ """Planner for task decomposition and reflection.
20
+
21
+ This class provides planning and reflection capabilities using OAGI workers.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ client: AsyncClient | None = None,
27
+ api_key: str | None = None,
28
+ base_url: str | None = None,
29
+ ):
30
+ """Initialize the planner.
31
+
32
+ Args:
33
+ client: AsyncClient for OAGI API calls. If None, one will be created when needed.
34
+ api_key: API key for creating internal client
35
+ base_url: Base URL for creating internal client
36
+ """
37
+ self.client = client
38
+ self.api_key = api_key
39
+ self.base_url = base_url
40
+ self._owns_client = False # Track if we created the client
41
+
42
+ def _ensure_client(self) -> AsyncClient:
43
+ """Ensure we have a client, creating one if needed."""
44
+ if not self.client:
45
+ self.client = AsyncClient(api_key=self.api_key, base_url=self.base_url)
46
+ self._owns_client = True
47
+ return self.client
48
+
49
+ async def close(self):
50
+ """Close the client if we own it."""
51
+ if self._owns_client and self.client:
52
+ await self.client.close()
53
+
54
+ async def __aenter__(self):
55
+ return self
56
+
57
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
58
+ await self.close()
59
+
60
+ def _extract_memory_data(
61
+ self,
62
+ memory: PlannerMemory | None,
63
+ context: dict[str, Any],
64
+ todo_index: int | None = None,
65
+ ) -> tuple[str, list, list, str | None, str]:
66
+ """Extract memory data for API calls.
67
+
68
+ Args:
69
+ memory: Optional PlannerMemory instance
70
+ context: Fallback context dictionary
71
+ todo_index: Optional todo index for extracting overall_todo
72
+
73
+ Returns:
74
+ Tuple of (task_description, todos, history,
75
+ task_execution_summary, overall_todo)
76
+ """
77
+ if memory and todo_index is not None:
78
+ # Use memory data
79
+ task_description = memory.task_description
80
+ todos = [
81
+ {
82
+ "index": i,
83
+ "description": t.description,
84
+ "status": t.status.value,
85
+ "execution_summary": memory.todo_execution_summaries.get(i),
86
+ }
87
+ for i, t in enumerate(memory.todos)
88
+ ]
89
+ history = [
90
+ {
91
+ "todo_index": h.todo_index,
92
+ "todo_description": h.todo,
93
+ "action_count": len(h.actions),
94
+ "summary": h.summary,
95
+ "completed": h.completed,
96
+ }
97
+ for h in memory.history
98
+ ]
99
+ task_execution_summary = memory.task_execution_summary or None
100
+ overall_todo = memory.todos[todo_index].description if memory.todos else ""
101
+ else:
102
+ # Fallback to basic context
103
+ task_description = context.get("task_description", "")
104
+ todos = context.get("todos", [])
105
+ history = context.get("history", [])
106
+ task_execution_summary = None
107
+ overall_todo = context.get("current_todo", "")
108
+
109
+ return (
110
+ task_description,
111
+ todos,
112
+ history,
113
+ task_execution_summary,
114
+ overall_todo,
115
+ )
116
+
117
+ async def initial_plan(
118
+ self,
119
+ todo: str,
120
+ context: dict[str, Any],
121
+ screenshot: Image | URL | None = None,
122
+ memory: PlannerMemory | None = None,
123
+ todo_index: int | None = None,
124
+ ) -> PlannerOutput:
125
+ """Generate initial plan for a todo.
126
+
127
+ Args:
128
+ todo: The todo description to plan for
129
+ context: Full context including task, todos, deliverables, and history
130
+ screenshot: Optional screenshot for visual context
131
+ memory: Optional PlannerMemory for formatting contexts
132
+ todo_index: Optional todo index for formatting internal context
133
+
134
+ Returns:
135
+ PlannerOutput with instruction, reasoning, and optional subtodos
136
+ """
137
+ # Ensure we have a client
138
+ client = self._ensure_client()
139
+
140
+ # Upload screenshot if provided
141
+ screenshot_uuid = None
142
+ if screenshot:
143
+ upload_response = await client.put_s3_presigned_url(screenshot)
144
+ screenshot_uuid = upload_response.uuid
145
+
146
+ # Extract memory data if provided
147
+ (
148
+ task_description,
149
+ todos,
150
+ history,
151
+ task_execution_summary,
152
+ _, # overall_todo not needed here, we use the `todo` parameter
153
+ ) = self._extract_memory_data(memory, context, todo_index)
154
+
155
+ # Call OAGI worker
156
+ response = await client.call_worker(
157
+ worker_id="oagi_first",
158
+ overall_todo=todo,
159
+ task_description=task_description,
160
+ todos=todos,
161
+ history=history,
162
+ current_todo_index=todo_index,
163
+ task_execution_summary=task_execution_summary,
164
+ current_screenshot=screenshot_uuid,
165
+ )
166
+
167
+ # Parse response
168
+ return self._parse_planner_output(response.response)
169
+
170
+ async def reflect(
171
+ self,
172
+ actions: list[Action],
173
+ context: dict[str, Any],
174
+ screenshot: Image | URL | None = None,
175
+ memory: PlannerMemory | None = None,
176
+ todo_index: int | None = None,
177
+ current_instruction: str | None = None,
178
+ reflection_interval: int = 4,
179
+ ) -> ReflectionOutput:
180
+ """Reflect on recent actions and progress.
181
+
182
+ Args:
183
+ actions: Recent actions to reflect on
184
+ context: Full context including task, todos, deliverables, and history
185
+ screenshot: Optional current screenshot
186
+ memory: Optional PlannerMemory for formatting contexts
187
+ todo_index: Optional todo index for formatting internal context
188
+ current_instruction: Current subtask instruction being executed
189
+ reflection_interval: Window size for recent actions/screenshots
190
+
191
+ Returns:
192
+ ReflectionOutput with continuation decision and reasoning
193
+ """
194
+ # Ensure we have a client
195
+ client = self._ensure_client()
196
+
197
+ # Upload screenshot if provided
198
+ result_screenshot_uuid = None
199
+ if screenshot:
200
+ upload_response = await client.put_s3_presigned_url(screenshot)
201
+ result_screenshot_uuid = upload_response.uuid
202
+
203
+ # Extract memory data if provided
204
+ (
205
+ task_description,
206
+ todos,
207
+ history,
208
+ task_execution_summary,
209
+ overall_todo,
210
+ ) = self._extract_memory_data(memory, context, todo_index)
211
+
212
+ # Get window of recent actions based on reflection_interval
213
+ window_actions = actions[-reflection_interval:]
214
+
215
+ # Convert actions to window_steps format
216
+ window_steps = [
217
+ {
218
+ "step_number": i + 1,
219
+ "action_type": action.action_type,
220
+ "target": action.target or "",
221
+ "reasoning": action.reasoning or "",
222
+ }
223
+ for i, action in enumerate(window_actions)
224
+ ]
225
+
226
+ # Extract screenshot UUIDs from window actions
227
+ window_screenshots = [
228
+ action.screenshot_uuid
229
+ for action in window_actions
230
+ if action.screenshot_uuid
231
+ ]
232
+
233
+ # Format prior notes from context (still needed as a simple string summary)
234
+ prior_notes = self._format_execution_notes(context)
235
+
236
+ # Call OAGI worker
237
+ response = await client.call_worker(
238
+ worker_id="oagi_follow",
239
+ overall_todo=overall_todo,
240
+ task_description=task_description,
241
+ todos=todos,
242
+ history=history,
243
+ current_todo_index=todo_index,
244
+ task_execution_summary=task_execution_summary,
245
+ current_subtask_instruction=current_instruction or "",
246
+ window_steps=window_steps,
247
+ window_screenshots=window_screenshots,
248
+ result_screenshot=result_screenshot_uuid,
249
+ prior_notes=prior_notes,
250
+ )
251
+
252
+ # Parse response
253
+ return self._parse_reflection_output(response.response)
254
+
255
+ async def summarize(
256
+ self,
257
+ execution_history: list[Action],
258
+ context: dict[str, Any],
259
+ memory: PlannerMemory | None = None,
260
+ todo_index: int | None = None,
261
+ ) -> str:
262
+ """Generate execution summary.
263
+
264
+ Args:
265
+ execution_history: Complete execution history
266
+ context: Full context including task, todos, deliverables
267
+ memory: Optional PlannerMemory for formatting contexts
268
+ todo_index: Optional todo index for formatting internal context
269
+
270
+ Returns:
271
+ String summary of the execution
272
+ """
273
+ # Ensure we have a client
274
+ client = self._ensure_client()
275
+
276
+ # Extract memory data if provided
277
+ (
278
+ task_description,
279
+ todos,
280
+ history,
281
+ task_execution_summary,
282
+ overall_todo,
283
+ ) = self._extract_memory_data(memory, context, todo_index)
284
+
285
+ # Extract latest_todo_summary (specific to summarize method)
286
+ if memory and todo_index is not None:
287
+ latest_todo_summary = memory.todo_execution_summaries.get(todo_index, "")
288
+ else:
289
+ latest_todo_summary = ""
290
+
291
+ # Call OAGI worker
292
+ response = await client.call_worker(
293
+ worker_id="oagi_task_summary",
294
+ overall_todo=overall_todo,
295
+ task_description=task_description,
296
+ todos=todos,
297
+ history=history,
298
+ current_todo_index=todo_index,
299
+ task_execution_summary=task_execution_summary,
300
+ latest_todo_summary=latest_todo_summary,
301
+ )
302
+
303
+ # Parse response and extract summary
304
+ try:
305
+ result = json.loads(response.response)
306
+ return result.get("task_summary", response.response)
307
+ except json.JSONDecodeError:
308
+ return response.response
309
+
310
+ def _format_execution_notes(self, context: dict[str, Any]) -> str:
311
+ """Format execution history notes.
312
+
313
+ Args:
314
+ context: Context dictionary
315
+
316
+ Returns:
317
+ Formatted execution notes
318
+ """
319
+ if not context.get("history"):
320
+ return ""
321
+
322
+ parts = []
323
+ for hist in context["history"]:
324
+ parts.append(
325
+ f"Todo {hist['todo_index']}: {hist['action_count']} actions, "
326
+ f"completed: {hist['completed']}"
327
+ )
328
+ if hist.get("summary"):
329
+ parts.append(f"Summary: {hist['summary']}")
330
+
331
+ return "\n".join(parts)
332
+
333
+ def _parse_planner_output(self, response: str) -> PlannerOutput:
334
+ """Parse OAGI worker response into structured planner output.
335
+
336
+ Args:
337
+ response: Raw string response from OAGI worker (oagi_first)
338
+
339
+ Returns:
340
+ Structured PlannerOutput
341
+ """
342
+ try:
343
+ # Try to parse as JSON (oagi_first format)
344
+ # Extract JSON string to handle Markdown code blocks
345
+ json_response = self._extract_json_str(response)
346
+ data = json.loads(json_response)
347
+ # oagi_first returns: {"reasoning": "...", "subtask": "..."}
348
+ return PlannerOutput(
349
+ instruction=data.get("subtask", data.get("instruction", "")),
350
+ reasoning=data.get("reasoning", ""),
351
+ subtodos=data.get(
352
+ "subtodos", []
353
+ ), # Not typically returned by oagi_first
354
+ )
355
+ except (json.JSONDecodeError, KeyError):
356
+ # Fallback: use the entire response as instruction
357
+ return PlannerOutput(
358
+ instruction="",
359
+ reasoning="Failed to parse structured response",
360
+ subtodos=[],
361
+ )
362
+
363
+ def _parse_reflection_output(self, response: str) -> ReflectionOutput:
364
+ """Parse reflection response into structured output.
365
+
366
+ Args:
367
+ response: Raw string response from OAGI worker (oagi_follow)
368
+
369
+ Returns:
370
+ Structured ReflectionOutput
371
+ """
372
+ try:
373
+ # Try to parse as JSON (oagi_follow format)
374
+ json_response = self._extract_json_str(response)
375
+ data = json.loads(json_response)
376
+ # oagi_follow returns:
377
+ # {"assessment": "...", "summary": "...", "reflection": "...",
378
+ # "success": "yes" | "no", "subtask_instruction": "..."}
379
+
380
+ # Determine if we should continue or pivot
381
+ success = data.get("success", "no") == "yes"
382
+ new_subtask = data.get("subtask_instruction", "").strip()
383
+
384
+ # Continue current if success is not achieved and no new subtask provided
385
+ # Pivot if a new subtask instruction is provided
386
+ continue_current = not success and not new_subtask
387
+
388
+ return ReflectionOutput(
389
+ continue_current=continue_current,
390
+ new_instruction=new_subtask if new_subtask else None,
391
+ reasoning=data.get("reflection", data.get("reasoning", "")),
392
+ success_assessment=success,
393
+ )
394
+ except (json.JSONDecodeError, KeyError):
395
+ # Fallback: continue with current approach
396
+ return ReflectionOutput(
397
+ continue_current=True,
398
+ new_instruction=None,
399
+ reasoning="Failed to parse reflection response, continuing current approach",
400
+ success_assessment=False,
401
+ )
402
+
403
+ def _extract_json_str(self, text: str) -> str:
404
+ start = text.find("{")
405
+ end = text.rfind("}") + 1
406
+ if start < 0 or end <= start:
407
+ return ""
408
+ return text[start:end]