oagi-core 0.9.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 (60) hide show
  1. oagi/__init__.py +108 -0
  2. oagi/agent/__init__.py +31 -0
  3. oagi/agent/default.py +75 -0
  4. oagi/agent/factories.py +50 -0
  5. oagi/agent/protocol.py +55 -0
  6. oagi/agent/registry.py +155 -0
  7. oagi/agent/tasker/__init__.py +35 -0
  8. oagi/agent/tasker/memory.py +184 -0
  9. oagi/agent/tasker/models.py +83 -0
  10. oagi/agent/tasker/planner.py +385 -0
  11. oagi/agent/tasker/taskee_agent.py +395 -0
  12. oagi/agent/tasker/tasker_agent.py +323 -0
  13. oagi/async_pyautogui_action_handler.py +44 -0
  14. oagi/async_screenshot_maker.py +47 -0
  15. oagi/async_single_step.py +85 -0
  16. oagi/cli/__init__.py +11 -0
  17. oagi/cli/agent.py +125 -0
  18. oagi/cli/main.py +77 -0
  19. oagi/cli/server.py +94 -0
  20. oagi/cli/utils.py +82 -0
  21. oagi/client/__init__.py +12 -0
  22. oagi/client/async_.py +293 -0
  23. oagi/client/base.py +465 -0
  24. oagi/client/sync.py +296 -0
  25. oagi/exceptions.py +118 -0
  26. oagi/logging.py +47 -0
  27. oagi/pil_image.py +102 -0
  28. oagi/pyautogui_action_handler.py +268 -0
  29. oagi/screenshot_maker.py +41 -0
  30. oagi/server/__init__.py +13 -0
  31. oagi/server/agent_wrappers.py +98 -0
  32. oagi/server/config.py +46 -0
  33. oagi/server/main.py +157 -0
  34. oagi/server/models.py +98 -0
  35. oagi/server/session_store.py +116 -0
  36. oagi/server/socketio_server.py +405 -0
  37. oagi/single_step.py +87 -0
  38. oagi/task/__init__.py +14 -0
  39. oagi/task/async_.py +97 -0
  40. oagi/task/async_short.py +64 -0
  41. oagi/task/base.py +121 -0
  42. oagi/task/short.py +64 -0
  43. oagi/task/sync.py +97 -0
  44. oagi/types/__init__.py +28 -0
  45. oagi/types/action_handler.py +30 -0
  46. oagi/types/async_action_handler.py +30 -0
  47. oagi/types/async_image_provider.py +37 -0
  48. oagi/types/image.py +17 -0
  49. oagi/types/image_provider.py +34 -0
  50. oagi/types/models/__init__.py +32 -0
  51. oagi/types/models/action.py +33 -0
  52. oagi/types/models/client.py +64 -0
  53. oagi/types/models/image_config.py +47 -0
  54. oagi/types/models/step.py +17 -0
  55. oagi/types/url_image.py +47 -0
  56. oagi_core-0.9.0.dist-info/METADATA +257 -0
  57. oagi_core-0.9.0.dist-info/RECORD +60 -0
  58. oagi_core-0.9.0.dist-info/WHEEL +4 -0
  59. oagi_core-0.9.0.dist-info/entry_points.txt +2 -0
  60. oagi_core-0.9.0.dist-info/licenses/LICENSE +21 -0
oagi/__init__.py ADDED
@@ -0,0 +1,108 @@
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
+ import importlib
9
+
10
+ from oagi.async_single_step import async_single_step
11
+ from oagi.client import AsyncClient, SyncClient
12
+ from oagi.exceptions import (
13
+ APIError,
14
+ AuthenticationError,
15
+ ConfigurationError,
16
+ NetworkError,
17
+ NotFoundError,
18
+ OAGIError,
19
+ RateLimitError,
20
+ RequestTimeoutError,
21
+ ServerError,
22
+ ValidationError,
23
+ )
24
+ from oagi.single_step import single_step
25
+ from oagi.task import AsyncShortTask, AsyncTask, ShortTask, Task
26
+ from oagi.types import (
27
+ AsyncActionHandler,
28
+ AsyncImageProvider,
29
+ ImageConfig,
30
+ )
31
+ from oagi.types.models import ErrorDetail, ErrorResponse, LLMResponse
32
+
33
+ # Lazy imports for pyautogui-dependent modules
34
+ # These will only be imported when actually accessed
35
+ _LAZY_IMPORTS = {
36
+ "AsyncPyautoguiActionHandler": "oagi.async_pyautogui_action_handler",
37
+ "AsyncScreenshotMaker": "oagi.async_screenshot_maker",
38
+ "PILImage": "oagi.pil_image",
39
+ "PyautoguiActionHandler": "oagi.pyautogui_action_handler",
40
+ "PyautoguiConfig": "oagi.pyautogui_action_handler",
41
+ "ScreenshotMaker": "oagi.screenshot_maker",
42
+ # Agent modules (to avoid circular imports)
43
+ "TaskerAgent": "oagi.agent.tasker",
44
+ # Server modules (optional - requires server dependencies)
45
+ "create_app": "oagi.server.main",
46
+ "ServerConfig": "oagi.server.config",
47
+ "sio": "oagi.server.socketio_server",
48
+ }
49
+
50
+
51
+ def __getattr__(name: str):
52
+ """Lazy import for pyautogui-dependent modules."""
53
+ if name in _LAZY_IMPORTS:
54
+ module_name = _LAZY_IMPORTS[name]
55
+ module = importlib.import_module(module_name)
56
+ return getattr(module, name)
57
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
58
+
59
+
60
+ __all__ = [
61
+ # Core sync classes
62
+ "Task",
63
+ "ShortTask",
64
+ "SyncClient",
65
+ # Core async classes
66
+ "AsyncTask",
67
+ "AsyncShortTask",
68
+ "AsyncClient",
69
+ # Agent classes
70
+ "TaskerAgent",
71
+ # Functions
72
+ "single_step",
73
+ "async_single_step",
74
+ # Async protocols
75
+ "AsyncActionHandler",
76
+ "AsyncImageProvider",
77
+ # Configuration
78
+ "ImageConfig",
79
+ # Response models
80
+ "LLMResponse",
81
+ "ErrorResponse",
82
+ "ErrorDetail",
83
+ # Exceptions
84
+ "OAGIError",
85
+ "APIError",
86
+ "AuthenticationError",
87
+ "ConfigurationError",
88
+ "NetworkError",
89
+ "NotFoundError",
90
+ "RateLimitError",
91
+ "ServerError",
92
+ "RequestTimeoutError",
93
+ "ValidationError",
94
+ # Lazy imports
95
+ # Image classes
96
+ "PILImage",
97
+ # Handler classes
98
+ "PyautoguiActionHandler",
99
+ "PyautoguiConfig",
100
+ "ScreenshotMaker",
101
+ # Async handler classes
102
+ "AsyncPyautoguiActionHandler",
103
+ "AsyncScreenshotMaker",
104
+ # Server modules (optional)
105
+ "create_app",
106
+ "ServerConfig",
107
+ "sio",
108
+ ]
oagi/agent/__init__.py ADDED
@@ -0,0 +1,31 @@
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 factories to trigger registration
10
+ from . import factories # noqa: F401
11
+ from .default import AsyncDefaultAgent
12
+ from .protocol import Agent, AsyncAgent
13
+ from .registry import (
14
+ async_agent_register,
15
+ create_agent,
16
+ get_agent_factory,
17
+ list_agent_modes,
18
+ )
19
+ from .tasker import TaskeeAgent, TaskerAgent
20
+
21
+ __all__ = [
22
+ "Agent",
23
+ "AsyncAgent",
24
+ "AsyncDefaultAgent",
25
+ "TaskerAgent",
26
+ "TaskeeAgent",
27
+ "async_agent_register",
28
+ "create_agent",
29
+ "get_agent_factory",
30
+ "list_agent_modes",
31
+ ]
oagi/agent/default.py ADDED
@@ -0,0 +1,75 @@
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 logging
10
+
11
+ from .. import AsyncTask
12
+ from ..types import (
13
+ AsyncActionHandler,
14
+ AsyncImageProvider,
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class AsyncDefaultAgent:
21
+ """Default asynchronous agent implementation using OAGI client."""
22
+
23
+ def __init__(
24
+ self,
25
+ api_key: str | None = None,
26
+ base_url: str | None = None,
27
+ model: str = "lux-v1",
28
+ max_steps: int = 30,
29
+ temperature: float | None = None,
30
+ ):
31
+ self.api_key = api_key
32
+ self.base_url = base_url
33
+ self.model = model
34
+ self.max_steps = max_steps
35
+ self.temperature = temperature
36
+
37
+ async def execute(
38
+ self,
39
+ instruction: str,
40
+ action_handler: AsyncActionHandler,
41
+ image_provider: AsyncImageProvider,
42
+ ) -> bool:
43
+ async with AsyncTask(
44
+ api_key=self.api_key, base_url=self.base_url, model=self.model
45
+ ) as self.task:
46
+ logger.info(f"Starting async task execution: {instruction}")
47
+ await self.task.init_task(instruction, max_steps=self.max_steps)
48
+
49
+ for i in range(self.max_steps):
50
+ logger.debug(f"Executing step {i + 1}/{self.max_steps}")
51
+
52
+ # Capture current state
53
+ image = await image_provider()
54
+
55
+ # Get next step from OAGI
56
+ step = await self.task.step(image, temperature=self.temperature)
57
+
58
+ # Log reasoning
59
+ if step.reason:
60
+ logger.debug(f"Step {i + 1} reasoning: {step.reason}")
61
+
62
+ # Execute actions if any
63
+ if step.actions:
64
+ logger.debug(f"Executing {len(step.actions)} actions")
65
+ await action_handler(step.actions)
66
+
67
+ # Check if task is complete
68
+ if step.stop:
69
+ logger.info(f"Task completed successfully after {i + 1} steps")
70
+ return True
71
+
72
+ logger.warning(
73
+ f"Task reached max steps ({self.max_steps}) without completion"
74
+ )
75
+ return False
@@ -0,0 +1,50 @@
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
+ from oagi.agent.tasker import TaskerAgent
9
+
10
+ from .default import AsyncDefaultAgent
11
+ from .protocol import AsyncAgent
12
+ from .registry import async_agent_register
13
+
14
+
15
+ @async_agent_register(mode="actor")
16
+ def create_default_agent(
17
+ api_key: str | None = None,
18
+ base_url: str | None = None,
19
+ model: str = "lux-v1",
20
+ max_steps: int = 20,
21
+ temperature: float = 0.1,
22
+ ) -> AsyncAgent:
23
+ return AsyncDefaultAgent(
24
+ api_key=api_key,
25
+ base_url=base_url,
26
+ model=model,
27
+ max_steps=max_steps,
28
+ temperature=temperature,
29
+ )
30
+
31
+
32
+ @async_agent_register(mode="tasker")
33
+ def create_planner_agent(
34
+ api_key: str | None = None,
35
+ base_url: str | None = None,
36
+ model: str = "lux-v1",
37
+ max_steps: int = 30,
38
+ temperature: float = 0.0,
39
+ reflection_interval: int = 20,
40
+ ) -> AsyncAgent:
41
+ tasker = TaskerAgent(
42
+ api_key=api_key,
43
+ base_url=base_url,
44
+ model=model,
45
+ max_steps=max_steps,
46
+ temperature=temperature,
47
+ reflection_interval=reflection_interval,
48
+ )
49
+ # tasker.set_task()
50
+ return tasker
oagi/agent/protocol.py ADDED
@@ -0,0 +1,55 @@
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 Protocol
10
+
11
+ from ..types import ActionHandler, AsyncActionHandler, AsyncImageProvider, ImageProvider
12
+
13
+
14
+ class Agent(Protocol):
15
+ """Protocol for synchronous task execution agents."""
16
+
17
+ def execute(
18
+ self,
19
+ instruction: str,
20
+ action_handler: ActionHandler,
21
+ image_provider: ImageProvider,
22
+ ) -> bool:
23
+ """Execute a task with the given handlers.
24
+
25
+ Args:
26
+ instruction: Task instruction to execute
27
+ action_handler: Handler for executing actions
28
+ image_provider: Provider for capturing images
29
+
30
+ Returns:
31
+ True if task completed successfully, False otherwise
32
+ """
33
+ ...
34
+
35
+
36
+ class AsyncAgent(Protocol):
37
+ """Protocol for asynchronous task execution agents."""
38
+
39
+ async def execute(
40
+ self,
41
+ instruction: str,
42
+ action_handler: AsyncActionHandler,
43
+ image_provider: AsyncImageProvider,
44
+ ) -> bool:
45
+ """Asynchronously execute a task with the given handlers.
46
+
47
+ Args:
48
+ instruction: Task instruction to execute
49
+ action_handler: Handler for executing actions
50
+ image_provider: Provider for capturing images
51
+
52
+ Returns:
53
+ True if task completed successfully, False otherwise
54
+ """
55
+ ...
oagi/agent/registry.py ADDED
@@ -0,0 +1,155 @@
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 inspect
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ from .protocol import AsyncAgent
14
+
15
+ # Type alias for agent factory functions
16
+ AgentFactory = Callable[..., AsyncAgent]
17
+
18
+ # Global registry mapping mode names to factory functions
19
+ _agent_registry: dict[str, AgentFactory] = {}
20
+
21
+
22
+ def async_agent_register(mode: str) -> Callable[[AgentFactory], AgentFactory]:
23
+ """Decorator to register agent factory functions for specific modes.
24
+
25
+ The decorator performs the following:
26
+ 1. Registers the factory function under the specified mode name
27
+ 2. Validates that duplicate modes are not registered
28
+ 3. Enables runtime validation of returned AsyncAgent instances
29
+
30
+ Args:
31
+ mode: The agent mode identifier (e.g., "actor", "planner", "todo")
32
+
33
+ Returns:
34
+ Decorator function that registers the factory
35
+
36
+ Raises:
37
+ ValueError: If the mode is already registered
38
+ """
39
+
40
+ def decorator(func: AgentFactory) -> AgentFactory:
41
+ # Check if mode is already registered
42
+ if mode in _agent_registry:
43
+ raise ValueError(
44
+ f"Agent mode '{mode}' is already registered. "
45
+ f"Cannot register the same mode twice."
46
+ )
47
+
48
+ # Register the factory
49
+ _agent_registry[mode] = func
50
+ return func
51
+
52
+ return decorator
53
+
54
+
55
+ def get_agent_factory(mode: str) -> AgentFactory:
56
+ """Get the registered agent factory for a mode.
57
+
58
+ Args:
59
+ mode: The agent mode identifier
60
+
61
+ Returns:
62
+ The registered factory function
63
+
64
+ Raises:
65
+ ValueError: If the mode is not registered
66
+ """
67
+ if mode not in _agent_registry:
68
+ available_modes = list(_agent_registry.keys())
69
+ raise ValueError(
70
+ f"Unknown agent mode: '{mode}'. Available modes: {available_modes}"
71
+ )
72
+ return _agent_registry[mode]
73
+
74
+
75
+ def list_agent_modes() -> list[str]:
76
+ """List all registered agent modes.
77
+
78
+ Returns:
79
+ List of registered mode names
80
+ """
81
+ return list(_agent_registry.keys())
82
+
83
+
84
+ def create_agent(mode: str, **kwargs: Any) -> AsyncAgent:
85
+ """Create an agent instance using the registered factory for the given mode.
86
+
87
+ This function automatically introspects the factory's signature and only passes
88
+ parameters that the factory accepts. This allows factories to have flexible
89
+ signatures while callers can provide a standard set of parameters.
90
+
91
+ Standard parameters typically include:
92
+ - api_key: OAGI API key
93
+ - base_url: OAGI API base URL
94
+ - model: Model identifier (e.g., "lux-v1")
95
+ - max_steps: Maximum number of steps to execute
96
+ - temperature: Sampling temperature
97
+
98
+ Args:
99
+ mode: The agent mode identifier
100
+ **kwargs: Parameters to pass to the factory function
101
+
102
+ Returns:
103
+ AsyncAgent instance created by the factory
104
+
105
+ Raises:
106
+ ValueError: If the mode is not registered
107
+ TypeError: If the factory returns an object that doesn't implement AsyncAgent
108
+
109
+ Example:
110
+ agent = create_agent(
111
+ mode="actor",
112
+ api_key="...",
113
+ base_url="...",
114
+ model="lux-v1",
115
+ max_steps=30,
116
+ temperature=0.0,
117
+ )
118
+ """
119
+ factory = get_agent_factory(mode)
120
+
121
+ # Introspect factory signature to determine which parameters it accepts
122
+ sig = inspect.signature(factory)
123
+
124
+ # Check if factory has **kwargs parameter (VAR_KEYWORD)
125
+ has_var_keyword = any(
126
+ param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values()
127
+ )
128
+
129
+ if has_var_keyword:
130
+ # If factory has **kwargs, pass all parameters
131
+ filtered_kwargs = kwargs
132
+ else:
133
+ # Otherwise, filter kwargs to only include parameters the factory accepts
134
+ accepted_params = set(sig.parameters.keys())
135
+ filtered_kwargs = {
136
+ key: value for key, value in kwargs.items() if key in accepted_params
137
+ }
138
+
139
+ agent = factory(**filtered_kwargs)
140
+
141
+ if not hasattr(agent, "execute"):
142
+ raise TypeError(
143
+ f"Factory for mode '{mode}' returned an object that doesn't "
144
+ f"implement AsyncAgent protocol. Expected an object with an "
145
+ f"'execute' method, got {type(agent).__name__}"
146
+ )
147
+
148
+ if not inspect.iscoroutinefunction(agent.execute):
149
+ raise TypeError(
150
+ f"Factory for mode '{mode}' returned an object with a non-async "
151
+ f"'execute' method. AsyncAgent protocol requires 'execute' to be "
152
+ f"an async method."
153
+ )
154
+
155
+ return agent
@@ -0,0 +1,35 @@
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 .memory import PlannerMemory
10
+ from .models import (
11
+ Action,
12
+ Deliverable,
13
+ PlannerOutput,
14
+ ReflectionOutput,
15
+ Todo,
16
+ TodoHistory,
17
+ TodoStatus,
18
+ )
19
+ from .planner import Planner
20
+ from .taskee_agent import TaskeeAgent
21
+ from .tasker_agent import TaskerAgent
22
+
23
+ __all__ = [
24
+ "TaskerAgent",
25
+ "TaskeeAgent",
26
+ "PlannerMemory",
27
+ "Planner",
28
+ "Todo",
29
+ "TodoStatus",
30
+ "Deliverable",
31
+ "Action",
32
+ "TodoHistory",
33
+ "PlannerOutput",
34
+ "ReflectionOutput",
35
+ ]
@@ -0,0 +1,184 @@
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, Deliverable, 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/deliverable 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.deliverables: list[Deliverable] = []
31
+ self.history: list[TodoHistory] = []
32
+ self.task_execution_summary: str = ""
33
+ self.todo_execution_summaries: dict[int, str] = {}
34
+
35
+ def set_task(
36
+ self,
37
+ task_description: str,
38
+ todos: list[str] | list[Todo],
39
+ deliverables: list[str] | list[Deliverable] | None = None,
40
+ ) -> None:
41
+ """Set the task, todos, and deliverables.
42
+
43
+ Args:
44
+ task_description: Overall task description
45
+ todos: List of todo items (strings or Todo objects)
46
+ deliverables: Optional list of deliverables (strings or Deliverable objects)
47
+ """
48
+ self.task_description = task_description
49
+
50
+ # Convert todos
51
+ self.todos = []
52
+ for todo in todos:
53
+ if isinstance(todo, str):
54
+ self.todos.append(Todo(description=todo))
55
+ else:
56
+ self.todos.append(todo)
57
+
58
+ # Convert deliverables
59
+ self.deliverables = []
60
+ if deliverables:
61
+ for deliverable in deliverables:
62
+ if isinstance(deliverable, str):
63
+ self.deliverables.append(Deliverable(description=deliverable))
64
+ else:
65
+ self.deliverables.append(deliverable)
66
+
67
+ def get_current_todo(self) -> tuple[Todo | None, int]:
68
+ """Get the next pending or in-progress todo.
69
+
70
+ Returns:
71
+ Tuple of (Todo object, index) or (None, -1) if no todos remain
72
+ """
73
+ for idx, todo in enumerate(self.todos):
74
+ if todo.status in [TodoStatus.PENDING, TodoStatus.IN_PROGRESS]:
75
+ return todo, idx
76
+ return None, -1
77
+
78
+ def update_todo(
79
+ self,
80
+ index: int,
81
+ status: TodoStatus | str,
82
+ summary: str | None = None,
83
+ ) -> None:
84
+ """Update a todo's status and optionally its summary.
85
+
86
+ Args:
87
+ index: Index of the todo to update
88
+ status: New status for the todo
89
+ summary: Optional execution summary
90
+ """
91
+ if 0 <= index < len(self.todos):
92
+ if isinstance(status, str):
93
+ status = TodoStatus(status)
94
+ self.todos[index].status = status
95
+ if summary:
96
+ self.todo_execution_summaries[index] = summary
97
+
98
+ def add_history(
99
+ self,
100
+ todo_index: int,
101
+ actions: list[Action],
102
+ summary: str | None = None,
103
+ completed: bool = False,
104
+ ) -> None:
105
+ """Add execution history for a todo.
106
+
107
+ Args:
108
+ todo_index: Index of the todo
109
+ actions: List of actions taken
110
+ summary: Optional execution summary
111
+ completed: Whether the todo was completed
112
+ """
113
+ if 0 <= todo_index < len(self.todos):
114
+ self.history.append(
115
+ TodoHistory(
116
+ todo_index=todo_index,
117
+ todo=self.todos[todo_index].description,
118
+ actions=actions,
119
+ summary=summary,
120
+ completed=completed,
121
+ )
122
+ )
123
+
124
+ def get_context(self) -> dict[str, Any]:
125
+ """Get the full context for planning/reflection.
126
+
127
+ Returns:
128
+ Dictionary containing all memory state
129
+ """
130
+ return {
131
+ "task_description": self.task_description,
132
+ "todos": [
133
+ {"index": i, "description": t.description, "status": t.status}
134
+ for i, t in enumerate(self.todos)
135
+ ],
136
+ "deliverables": [
137
+ {"description": d.description, "achieved": d.achieved}
138
+ for d in self.deliverables
139
+ ],
140
+ "history": [
141
+ {
142
+ "todo_index": h.todo_index,
143
+ "todo": h.todo,
144
+ "action_count": len(h.actions),
145
+ "summary": h.summary,
146
+ "completed": h.completed,
147
+ }
148
+ for h in self.history
149
+ ],
150
+ "task_execution_summary": self.task_execution_summary,
151
+ "todo_execution_summaries": self.todo_execution_summaries,
152
+ }
153
+
154
+ def get_todo_status_summary(self) -> dict[str, int]:
155
+ """Get a summary of todo statuses.
156
+
157
+ Returns:
158
+ Dictionary with counts for each status
159
+ """
160
+ summary = {
161
+ TodoStatus.PENDING: 0,
162
+ TodoStatus.IN_PROGRESS: 0,
163
+ TodoStatus.COMPLETED: 0,
164
+ TodoStatus.SKIPPED: 0,
165
+ }
166
+ for todo in self.todos:
167
+ summary[todo.status] += 1
168
+ return summary
169
+
170
+ def append_todo(self, description: str) -> None:
171
+ """Append a new todo to the list.
172
+
173
+ Args:
174
+ description: Description of the new todo
175
+ """
176
+ self.todos.append(Todo(description=description))
177
+
178
+ def append_deliverable(self, description: str) -> None:
179
+ """Append a new deliverable to the list.
180
+
181
+ Args:
182
+ description: Description of the new deliverable
183
+ """
184
+ self.deliverables.append(Deliverable(description=description))