strix-agent 0.4.0__py3-none-any.whl → 0.6.2__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.
- strix/agents/StrixAgent/strix_agent.py +3 -3
- strix/agents/StrixAgent/system_prompt.jinja +30 -26
- strix/agents/base_agent.py +159 -75
- strix/agents/state.py +5 -2
- strix/config/__init__.py +12 -0
- strix/config/config.py +172 -0
- strix/interface/assets/tui_styles.tcss +195 -230
- strix/interface/cli.py +16 -41
- strix/interface/main.py +151 -74
- strix/interface/streaming_parser.py +119 -0
- strix/interface/tool_components/__init__.py +4 -0
- strix/interface/tool_components/agent_message_renderer.py +190 -0
- strix/interface/tool_components/agents_graph_renderer.py +54 -38
- strix/interface/tool_components/base_renderer.py +68 -36
- strix/interface/tool_components/browser_renderer.py +106 -91
- strix/interface/tool_components/file_edit_renderer.py +117 -36
- strix/interface/tool_components/finish_renderer.py +43 -10
- strix/interface/tool_components/notes_renderer.py +63 -38
- strix/interface/tool_components/proxy_renderer.py +133 -92
- strix/interface/tool_components/python_renderer.py +121 -8
- strix/interface/tool_components/registry.py +19 -12
- strix/interface/tool_components/reporting_renderer.py +196 -28
- strix/interface/tool_components/scan_info_renderer.py +22 -19
- strix/interface/tool_components/terminal_renderer.py +270 -90
- strix/interface/tool_components/thinking_renderer.py +8 -6
- strix/interface/tool_components/todo_renderer.py +225 -0
- strix/interface/tool_components/user_message_renderer.py +26 -19
- strix/interface/tool_components/web_search_renderer.py +7 -6
- strix/interface/tui.py +907 -262
- strix/interface/utils.py +236 -4
- strix/llm/__init__.py +6 -2
- strix/llm/config.py +8 -5
- strix/llm/dedupe.py +217 -0
- strix/llm/llm.py +209 -356
- strix/llm/memory_compressor.py +6 -5
- strix/llm/utils.py +17 -8
- strix/runtime/__init__.py +12 -3
- strix/runtime/docker_runtime.py +121 -202
- strix/runtime/tool_server.py +55 -95
- strix/skills/README.md +64 -0
- strix/skills/__init__.py +110 -0
- strix/{prompts → skills}/frameworks/nextjs.jinja +26 -0
- strix/skills/scan_modes/deep.jinja +145 -0
- strix/skills/scan_modes/quick.jinja +63 -0
- strix/skills/scan_modes/standard.jinja +91 -0
- strix/telemetry/README.md +38 -0
- strix/telemetry/__init__.py +7 -1
- strix/telemetry/posthog.py +137 -0
- strix/telemetry/tracer.py +194 -54
- strix/tools/__init__.py +11 -4
- strix/tools/agents_graph/agents_graph_actions.py +20 -21
- strix/tools/agents_graph/agents_graph_actions_schema.xml +8 -8
- strix/tools/browser/browser_actions.py +10 -6
- strix/tools/browser/browser_actions_schema.xml +6 -1
- strix/tools/browser/browser_instance.py +96 -48
- strix/tools/browser/tab_manager.py +121 -102
- strix/tools/context.py +12 -0
- strix/tools/executor.py +63 -4
- strix/tools/file_edit/file_edit_actions.py +6 -3
- strix/tools/file_edit/file_edit_actions_schema.xml +45 -3
- strix/tools/finish/finish_actions.py +80 -105
- strix/tools/finish/finish_actions_schema.xml +121 -14
- strix/tools/notes/notes_actions.py +6 -33
- strix/tools/notes/notes_actions_schema.xml +50 -46
- strix/tools/proxy/proxy_actions.py +14 -2
- strix/tools/proxy/proxy_actions_schema.xml +0 -1
- strix/tools/proxy/proxy_manager.py +28 -16
- strix/tools/python/python_actions.py +2 -2
- strix/tools/python/python_actions_schema.xml +9 -1
- strix/tools/python/python_instance.py +39 -37
- strix/tools/python/python_manager.py +43 -31
- strix/tools/registry.py +73 -12
- strix/tools/reporting/reporting_actions.py +218 -31
- strix/tools/reporting/reporting_actions_schema.xml +256 -8
- strix/tools/terminal/terminal_actions.py +2 -2
- strix/tools/terminal/terminal_actions_schema.xml +6 -0
- strix/tools/terminal/terminal_manager.py +41 -30
- strix/tools/thinking/thinking_actions_schema.xml +27 -25
- strix/tools/todo/__init__.py +18 -0
- strix/tools/todo/todo_actions.py +568 -0
- strix/tools/todo/todo_actions_schema.xml +225 -0
- strix/utils/__init__.py +0 -0
- strix/utils/resource_paths.py +13 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/METADATA +90 -65
- strix_agent-0.6.2.dist-info/RECORD +134 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/WHEEL +1 -1
- strix/llm/request_queue.py +0 -87
- strix/prompts/README.md +0 -64
- strix/prompts/__init__.py +0 -109
- strix_agent-0.4.0.dist-info/RECORD +0 -118
- /strix/{prompts → skills}/cloud/.gitkeep +0 -0
- /strix/{prompts → skills}/coordination/root_agent.jinja +0 -0
- /strix/{prompts → skills}/custom/.gitkeep +0 -0
- /strix/{prompts → skills}/frameworks/fastapi.jinja +0 -0
- /strix/{prompts → skills}/protocols/graphql.jinja +0 -0
- /strix/{prompts → skills}/reconnaissance/.gitkeep +0 -0
- /strix/{prompts → skills}/technologies/firebase_firestore.jinja +0 -0
- /strix/{prompts → skills}/technologies/supabase.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/authentication_jwt.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/broken_function_level_authorization.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/business_logic.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/csrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/idor.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/information_disclosure.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/insecure_file_uploads.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/mass_assignment.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/open_redirect.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/path_traversal_lfi_rfi.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/race_conditions.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/rce.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/sql_injection.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/ssrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/subdomain_takeover.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xss.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xxe.jinja +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/entry_points.txt +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from strix.tools.registry import register_tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
VALID_PRIORITIES = ["low", "normal", "high", "critical"]
|
|
10
|
+
VALID_STATUSES = ["pending", "in_progress", "done"]
|
|
11
|
+
|
|
12
|
+
_todos_storage: dict[str, dict[str, dict[str, Any]]] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_agent_todos(agent_id: str) -> dict[str, dict[str, Any]]:
|
|
16
|
+
if agent_id not in _todos_storage:
|
|
17
|
+
_todos_storage[agent_id] = {}
|
|
18
|
+
return _todos_storage[agent_id]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _normalize_priority(priority: str | None, default: str = "normal") -> str:
|
|
22
|
+
candidate = (priority or default or "normal").lower()
|
|
23
|
+
if candidate not in VALID_PRIORITIES:
|
|
24
|
+
raise ValueError(f"Invalid priority. Must be one of: {', '.join(VALID_PRIORITIES)}")
|
|
25
|
+
return candidate
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _sorted_todos(agent_id: str) -> list[dict[str, Any]]:
|
|
29
|
+
agent_todos = _get_agent_todos(agent_id)
|
|
30
|
+
|
|
31
|
+
todos_list: list[dict[str, Any]] = []
|
|
32
|
+
for todo_id, todo in agent_todos.items():
|
|
33
|
+
entry = todo.copy()
|
|
34
|
+
entry["todo_id"] = todo_id
|
|
35
|
+
todos_list.append(entry)
|
|
36
|
+
|
|
37
|
+
priority_order = {"critical": 0, "high": 1, "normal": 2, "low": 3}
|
|
38
|
+
status_order = {"done": 0, "in_progress": 1, "pending": 2}
|
|
39
|
+
|
|
40
|
+
todos_list.sort(
|
|
41
|
+
key=lambda x: (
|
|
42
|
+
status_order.get(x.get("status", "pending"), 99),
|
|
43
|
+
priority_order.get(x.get("priority", "normal"), 99),
|
|
44
|
+
x.get("created_at", ""),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return todos_list
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _normalize_todo_ids(raw_ids: Any) -> list[str]:
|
|
51
|
+
if raw_ids is None:
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
if isinstance(raw_ids, str):
|
|
55
|
+
stripped = raw_ids.strip()
|
|
56
|
+
if not stripped:
|
|
57
|
+
return []
|
|
58
|
+
try:
|
|
59
|
+
data = json.loads(stripped)
|
|
60
|
+
except json.JSONDecodeError:
|
|
61
|
+
data = stripped.split(",") if "," in stripped else [stripped]
|
|
62
|
+
if isinstance(data, list):
|
|
63
|
+
return [str(item).strip() for item in data if str(item).strip()]
|
|
64
|
+
return [str(data).strip()]
|
|
65
|
+
|
|
66
|
+
if isinstance(raw_ids, list):
|
|
67
|
+
return [str(item).strip() for item in raw_ids if str(item).strip()]
|
|
68
|
+
|
|
69
|
+
return [str(raw_ids).strip()]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _normalize_bulk_updates(raw_updates: Any) -> list[dict[str, Any]]:
|
|
73
|
+
if raw_updates is None:
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
data = raw_updates
|
|
77
|
+
if isinstance(raw_updates, str):
|
|
78
|
+
stripped = raw_updates.strip()
|
|
79
|
+
if not stripped:
|
|
80
|
+
return []
|
|
81
|
+
try:
|
|
82
|
+
data = json.loads(stripped)
|
|
83
|
+
except json.JSONDecodeError as e:
|
|
84
|
+
raise ValueError("Updates must be valid JSON") from e
|
|
85
|
+
|
|
86
|
+
if isinstance(data, dict):
|
|
87
|
+
data = [data]
|
|
88
|
+
|
|
89
|
+
if not isinstance(data, list):
|
|
90
|
+
raise TypeError("Updates must be a list of update objects")
|
|
91
|
+
|
|
92
|
+
normalized: list[dict[str, Any]] = []
|
|
93
|
+
for item in data:
|
|
94
|
+
if not isinstance(item, dict):
|
|
95
|
+
raise TypeError("Each update must be an object with todo_id")
|
|
96
|
+
|
|
97
|
+
todo_id = item.get("todo_id") or item.get("id")
|
|
98
|
+
if not todo_id:
|
|
99
|
+
raise ValueError("Each update must include 'todo_id'")
|
|
100
|
+
|
|
101
|
+
normalized.append(
|
|
102
|
+
{
|
|
103
|
+
"todo_id": str(todo_id).strip(),
|
|
104
|
+
"title": item.get("title"),
|
|
105
|
+
"description": item.get("description"),
|
|
106
|
+
"priority": item.get("priority"),
|
|
107
|
+
"status": item.get("status"),
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return normalized
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _normalize_bulk_todos(raw_todos: Any) -> list[dict[str, Any]]:
|
|
115
|
+
if raw_todos is None:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
data = raw_todos
|
|
119
|
+
if isinstance(raw_todos, str):
|
|
120
|
+
stripped = raw_todos.strip()
|
|
121
|
+
if not stripped:
|
|
122
|
+
return []
|
|
123
|
+
try:
|
|
124
|
+
data = json.loads(stripped)
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
entries = [line.strip(" -*\t") for line in stripped.splitlines() if line.strip(" -*\t")]
|
|
127
|
+
return [{"title": entry} for entry in entries]
|
|
128
|
+
|
|
129
|
+
if isinstance(data, dict):
|
|
130
|
+
data = [data]
|
|
131
|
+
|
|
132
|
+
if not isinstance(data, list):
|
|
133
|
+
raise TypeError("Todos must be provided as a list, dict, or JSON string")
|
|
134
|
+
|
|
135
|
+
normalized: list[dict[str, Any]] = []
|
|
136
|
+
for item in data:
|
|
137
|
+
if isinstance(item, str):
|
|
138
|
+
title = item.strip()
|
|
139
|
+
if title:
|
|
140
|
+
normalized.append({"title": title})
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if not isinstance(item, dict):
|
|
144
|
+
raise TypeError("Each todo entry must be a string or object with a title")
|
|
145
|
+
|
|
146
|
+
title = item.get("title", "")
|
|
147
|
+
if not isinstance(title, str) or not title.strip():
|
|
148
|
+
raise ValueError("Each todo entry must include a non-empty 'title'")
|
|
149
|
+
|
|
150
|
+
normalized.append(
|
|
151
|
+
{
|
|
152
|
+
"title": title.strip(),
|
|
153
|
+
"description": (item.get("description") or "").strip() or None,
|
|
154
|
+
"priority": item.get("priority"),
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return normalized
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@register_tool(sandbox_execution=False)
|
|
162
|
+
def create_todo(
|
|
163
|
+
agent_state: Any,
|
|
164
|
+
title: str | None = None,
|
|
165
|
+
description: str | None = None,
|
|
166
|
+
priority: str = "normal",
|
|
167
|
+
todos: Any | None = None,
|
|
168
|
+
) -> dict[str, Any]:
|
|
169
|
+
try:
|
|
170
|
+
agent_id = agent_state.agent_id
|
|
171
|
+
default_priority = _normalize_priority(priority)
|
|
172
|
+
|
|
173
|
+
tasks_to_create: list[dict[str, Any]] = []
|
|
174
|
+
|
|
175
|
+
if todos is not None:
|
|
176
|
+
tasks_to_create.extend(_normalize_bulk_todos(todos))
|
|
177
|
+
|
|
178
|
+
if title and title.strip():
|
|
179
|
+
tasks_to_create.append(
|
|
180
|
+
{
|
|
181
|
+
"title": title.strip(),
|
|
182
|
+
"description": description.strip() if description else None,
|
|
183
|
+
"priority": default_priority,
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not tasks_to_create:
|
|
188
|
+
return {
|
|
189
|
+
"success": False,
|
|
190
|
+
"error": "Provide a title or 'todos' list to create.",
|
|
191
|
+
"todo_id": None,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
agent_todos = _get_agent_todos(agent_id)
|
|
195
|
+
created: list[dict[str, Any]] = []
|
|
196
|
+
|
|
197
|
+
for task in tasks_to_create:
|
|
198
|
+
task_priority = _normalize_priority(task.get("priority"), default_priority)
|
|
199
|
+
todo_id = str(uuid.uuid4())[:6]
|
|
200
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
201
|
+
|
|
202
|
+
todo = {
|
|
203
|
+
"title": task["title"],
|
|
204
|
+
"description": task.get("description"),
|
|
205
|
+
"priority": task_priority,
|
|
206
|
+
"status": "pending",
|
|
207
|
+
"created_at": timestamp,
|
|
208
|
+
"updated_at": timestamp,
|
|
209
|
+
"completed_at": None,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
agent_todos[todo_id] = todo
|
|
213
|
+
created.append(
|
|
214
|
+
{
|
|
215
|
+
"todo_id": todo_id,
|
|
216
|
+
"title": task["title"],
|
|
217
|
+
"priority": task_priority,
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
except (ValueError, TypeError) as e:
|
|
222
|
+
return {"success": False, "error": f"Failed to create todo: {e}", "todo_id": None}
|
|
223
|
+
else:
|
|
224
|
+
todos_list = _sorted_todos(agent_id)
|
|
225
|
+
|
|
226
|
+
response: dict[str, Any] = {
|
|
227
|
+
"success": True,
|
|
228
|
+
"created": created,
|
|
229
|
+
"count": len(created),
|
|
230
|
+
"todos": todos_list,
|
|
231
|
+
"total_count": len(todos_list),
|
|
232
|
+
}
|
|
233
|
+
return response
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@register_tool(sandbox_execution=False)
|
|
237
|
+
def list_todos(
|
|
238
|
+
agent_state: Any,
|
|
239
|
+
status: str | None = None,
|
|
240
|
+
priority: str | None = None,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
try:
|
|
243
|
+
agent_id = agent_state.agent_id
|
|
244
|
+
agent_todos = _get_agent_todos(agent_id)
|
|
245
|
+
|
|
246
|
+
status_filter = status.lower() if isinstance(status, str) else None
|
|
247
|
+
priority_filter = priority.lower() if isinstance(priority, str) else None
|
|
248
|
+
|
|
249
|
+
todos_list = []
|
|
250
|
+
for todo_id, todo in agent_todos.items():
|
|
251
|
+
if status_filter and todo.get("status") != status_filter:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
if priority_filter and todo.get("priority") != priority_filter:
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
todo_with_id = todo.copy()
|
|
258
|
+
todo_with_id["todo_id"] = todo_id
|
|
259
|
+
todos_list.append(todo_with_id)
|
|
260
|
+
|
|
261
|
+
priority_order = {"critical": 0, "high": 1, "normal": 2, "low": 3}
|
|
262
|
+
status_order = {"done": 0, "in_progress": 1, "pending": 2}
|
|
263
|
+
|
|
264
|
+
todos_list.sort(
|
|
265
|
+
key=lambda x: (
|
|
266
|
+
status_order.get(x.get("status", "pending"), 99),
|
|
267
|
+
priority_order.get(x.get("priority", "normal"), 99),
|
|
268
|
+
x.get("created_at", ""),
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
summary_counts = {
|
|
273
|
+
"pending": 0,
|
|
274
|
+
"in_progress": 0,
|
|
275
|
+
"done": 0,
|
|
276
|
+
}
|
|
277
|
+
for todo in todos_list:
|
|
278
|
+
status_value = todo.get("status", "pending")
|
|
279
|
+
if status_value not in summary_counts:
|
|
280
|
+
summary_counts[status_value] = 0
|
|
281
|
+
summary_counts[status_value] += 1
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
"success": True,
|
|
285
|
+
"todos": todos_list,
|
|
286
|
+
"total_count": len(todos_list),
|
|
287
|
+
"summary": summary_counts,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
except (ValueError, TypeError) as e:
|
|
291
|
+
return {
|
|
292
|
+
"success": False,
|
|
293
|
+
"error": f"Failed to list todos: {e}",
|
|
294
|
+
"todos": [],
|
|
295
|
+
"total_count": 0,
|
|
296
|
+
"summary": {"pending": 0, "in_progress": 0, "done": 0},
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _apply_single_update(
|
|
301
|
+
agent_todos: dict[str, dict[str, Any]],
|
|
302
|
+
todo_id: str,
|
|
303
|
+
title: str | None = None,
|
|
304
|
+
description: str | None = None,
|
|
305
|
+
priority: str | None = None,
|
|
306
|
+
status: str | None = None,
|
|
307
|
+
) -> dict[str, Any] | None:
|
|
308
|
+
if todo_id not in agent_todos:
|
|
309
|
+
return {"todo_id": todo_id, "error": f"Todo with ID '{todo_id}' not found"}
|
|
310
|
+
|
|
311
|
+
todo = agent_todos[todo_id]
|
|
312
|
+
|
|
313
|
+
if title is not None:
|
|
314
|
+
if not title.strip():
|
|
315
|
+
return {"todo_id": todo_id, "error": "Title cannot be empty"}
|
|
316
|
+
todo["title"] = title.strip()
|
|
317
|
+
|
|
318
|
+
if description is not None:
|
|
319
|
+
todo["description"] = description.strip() if description else None
|
|
320
|
+
|
|
321
|
+
if priority is not None:
|
|
322
|
+
try:
|
|
323
|
+
todo["priority"] = _normalize_priority(priority, str(todo.get("priority", "normal")))
|
|
324
|
+
except ValueError as exc:
|
|
325
|
+
return {"todo_id": todo_id, "error": str(exc)}
|
|
326
|
+
|
|
327
|
+
if status is not None:
|
|
328
|
+
status_candidate = status.lower()
|
|
329
|
+
if status_candidate not in VALID_STATUSES:
|
|
330
|
+
return {
|
|
331
|
+
"todo_id": todo_id,
|
|
332
|
+
"error": f"Invalid status. Must be one of: {', '.join(VALID_STATUSES)}",
|
|
333
|
+
}
|
|
334
|
+
todo["status"] = status_candidate
|
|
335
|
+
if status_candidate == "done":
|
|
336
|
+
todo["completed_at"] = datetime.now(UTC).isoformat()
|
|
337
|
+
else:
|
|
338
|
+
todo["completed_at"] = None
|
|
339
|
+
|
|
340
|
+
todo["updated_at"] = datetime.now(UTC).isoformat()
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@register_tool(sandbox_execution=False)
|
|
345
|
+
def update_todo(
|
|
346
|
+
agent_state: Any,
|
|
347
|
+
todo_id: str | None = None,
|
|
348
|
+
title: str | None = None,
|
|
349
|
+
description: str | None = None,
|
|
350
|
+
priority: str | None = None,
|
|
351
|
+
status: str | None = None,
|
|
352
|
+
updates: Any | None = None,
|
|
353
|
+
) -> dict[str, Any]:
|
|
354
|
+
try:
|
|
355
|
+
agent_id = agent_state.agent_id
|
|
356
|
+
agent_todos = _get_agent_todos(agent_id)
|
|
357
|
+
|
|
358
|
+
updates_to_apply: list[dict[str, Any]] = []
|
|
359
|
+
|
|
360
|
+
if updates is not None:
|
|
361
|
+
updates_to_apply.extend(_normalize_bulk_updates(updates))
|
|
362
|
+
|
|
363
|
+
if todo_id is not None:
|
|
364
|
+
updates_to_apply.append(
|
|
365
|
+
{
|
|
366
|
+
"todo_id": todo_id,
|
|
367
|
+
"title": title,
|
|
368
|
+
"description": description,
|
|
369
|
+
"priority": priority,
|
|
370
|
+
"status": status,
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if not updates_to_apply:
|
|
375
|
+
return {
|
|
376
|
+
"success": False,
|
|
377
|
+
"error": "Provide todo_id or 'updates' list to update.",
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
updated: list[str] = []
|
|
381
|
+
errors: list[dict[str, Any]] = []
|
|
382
|
+
|
|
383
|
+
for update in updates_to_apply:
|
|
384
|
+
error = _apply_single_update(
|
|
385
|
+
agent_todos,
|
|
386
|
+
update["todo_id"],
|
|
387
|
+
update.get("title"),
|
|
388
|
+
update.get("description"),
|
|
389
|
+
update.get("priority"),
|
|
390
|
+
update.get("status"),
|
|
391
|
+
)
|
|
392
|
+
if error:
|
|
393
|
+
errors.append(error)
|
|
394
|
+
else:
|
|
395
|
+
updated.append(update["todo_id"])
|
|
396
|
+
|
|
397
|
+
todos_list = _sorted_todos(agent_id)
|
|
398
|
+
|
|
399
|
+
response: dict[str, Any] = {
|
|
400
|
+
"success": len(errors) == 0,
|
|
401
|
+
"updated": updated,
|
|
402
|
+
"updated_count": len(updated),
|
|
403
|
+
"todos": todos_list,
|
|
404
|
+
"total_count": len(todos_list),
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if errors:
|
|
408
|
+
response["errors"] = errors
|
|
409
|
+
|
|
410
|
+
except (ValueError, TypeError) as e:
|
|
411
|
+
return {"success": False, "error": str(e)}
|
|
412
|
+
else:
|
|
413
|
+
return response
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@register_tool(sandbox_execution=False)
|
|
417
|
+
def mark_todo_done(
|
|
418
|
+
agent_state: Any,
|
|
419
|
+
todo_id: str | None = None,
|
|
420
|
+
todo_ids: Any | None = None,
|
|
421
|
+
) -> dict[str, Any]:
|
|
422
|
+
try:
|
|
423
|
+
agent_id = agent_state.agent_id
|
|
424
|
+
agent_todos = _get_agent_todos(agent_id)
|
|
425
|
+
|
|
426
|
+
ids_to_mark: list[str] = []
|
|
427
|
+
if todo_ids is not None:
|
|
428
|
+
ids_to_mark.extend(_normalize_todo_ids(todo_ids))
|
|
429
|
+
if todo_id is not None:
|
|
430
|
+
ids_to_mark.append(todo_id)
|
|
431
|
+
|
|
432
|
+
if not ids_to_mark:
|
|
433
|
+
return {"success": False, "error": "Provide todo_id or todo_ids to mark as done."}
|
|
434
|
+
|
|
435
|
+
marked: list[str] = []
|
|
436
|
+
errors: list[dict[str, Any]] = []
|
|
437
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
438
|
+
|
|
439
|
+
for tid in ids_to_mark:
|
|
440
|
+
if tid not in agent_todos:
|
|
441
|
+
errors.append({"todo_id": tid, "error": f"Todo with ID '{tid}' not found"})
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
todo = agent_todos[tid]
|
|
445
|
+
todo["status"] = "done"
|
|
446
|
+
todo["completed_at"] = timestamp
|
|
447
|
+
todo["updated_at"] = timestamp
|
|
448
|
+
marked.append(tid)
|
|
449
|
+
|
|
450
|
+
todos_list = _sorted_todos(agent_id)
|
|
451
|
+
|
|
452
|
+
response: dict[str, Any] = {
|
|
453
|
+
"success": len(errors) == 0,
|
|
454
|
+
"marked_done": marked,
|
|
455
|
+
"marked_count": len(marked),
|
|
456
|
+
"todos": todos_list,
|
|
457
|
+
"total_count": len(todos_list),
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if errors:
|
|
461
|
+
response["errors"] = errors
|
|
462
|
+
|
|
463
|
+
except (ValueError, TypeError) as e:
|
|
464
|
+
return {"success": False, "error": str(e)}
|
|
465
|
+
else:
|
|
466
|
+
return response
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@register_tool(sandbox_execution=False)
|
|
470
|
+
def mark_todo_pending(
|
|
471
|
+
agent_state: Any,
|
|
472
|
+
todo_id: str | None = None,
|
|
473
|
+
todo_ids: Any | None = None,
|
|
474
|
+
) -> dict[str, Any]:
|
|
475
|
+
try:
|
|
476
|
+
agent_id = agent_state.agent_id
|
|
477
|
+
agent_todos = _get_agent_todos(agent_id)
|
|
478
|
+
|
|
479
|
+
ids_to_mark: list[str] = []
|
|
480
|
+
if todo_ids is not None:
|
|
481
|
+
ids_to_mark.extend(_normalize_todo_ids(todo_ids))
|
|
482
|
+
if todo_id is not None:
|
|
483
|
+
ids_to_mark.append(todo_id)
|
|
484
|
+
|
|
485
|
+
if not ids_to_mark:
|
|
486
|
+
return {"success": False, "error": "Provide todo_id or todo_ids to mark as pending."}
|
|
487
|
+
|
|
488
|
+
marked: list[str] = []
|
|
489
|
+
errors: list[dict[str, Any]] = []
|
|
490
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
491
|
+
|
|
492
|
+
for tid in ids_to_mark:
|
|
493
|
+
if tid not in agent_todos:
|
|
494
|
+
errors.append({"todo_id": tid, "error": f"Todo with ID '{tid}' not found"})
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
todo = agent_todos[tid]
|
|
498
|
+
todo["status"] = "pending"
|
|
499
|
+
todo["completed_at"] = None
|
|
500
|
+
todo["updated_at"] = timestamp
|
|
501
|
+
marked.append(tid)
|
|
502
|
+
|
|
503
|
+
todos_list = _sorted_todos(agent_id)
|
|
504
|
+
|
|
505
|
+
response: dict[str, Any] = {
|
|
506
|
+
"success": len(errors) == 0,
|
|
507
|
+
"marked_pending": marked,
|
|
508
|
+
"marked_count": len(marked),
|
|
509
|
+
"todos": todos_list,
|
|
510
|
+
"total_count": len(todos_list),
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if errors:
|
|
514
|
+
response["errors"] = errors
|
|
515
|
+
|
|
516
|
+
except (ValueError, TypeError) as e:
|
|
517
|
+
return {"success": False, "error": str(e)}
|
|
518
|
+
else:
|
|
519
|
+
return response
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@register_tool(sandbox_execution=False)
|
|
523
|
+
def delete_todo(
|
|
524
|
+
agent_state: Any,
|
|
525
|
+
todo_id: str | None = None,
|
|
526
|
+
todo_ids: Any | None = None,
|
|
527
|
+
) -> dict[str, Any]:
|
|
528
|
+
try:
|
|
529
|
+
agent_id = agent_state.agent_id
|
|
530
|
+
agent_todos = _get_agent_todos(agent_id)
|
|
531
|
+
|
|
532
|
+
ids_to_delete: list[str] = []
|
|
533
|
+
if todo_ids is not None:
|
|
534
|
+
ids_to_delete.extend(_normalize_todo_ids(todo_ids))
|
|
535
|
+
if todo_id is not None:
|
|
536
|
+
ids_to_delete.append(todo_id)
|
|
537
|
+
|
|
538
|
+
if not ids_to_delete:
|
|
539
|
+
return {"success": False, "error": "Provide todo_id or todo_ids to delete."}
|
|
540
|
+
|
|
541
|
+
deleted: list[str] = []
|
|
542
|
+
errors: list[dict[str, Any]] = []
|
|
543
|
+
|
|
544
|
+
for tid in ids_to_delete:
|
|
545
|
+
if tid not in agent_todos:
|
|
546
|
+
errors.append({"todo_id": tid, "error": f"Todo with ID '{tid}' not found"})
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
del agent_todos[tid]
|
|
550
|
+
deleted.append(tid)
|
|
551
|
+
|
|
552
|
+
todos_list = _sorted_todos(agent_id)
|
|
553
|
+
|
|
554
|
+
response: dict[str, Any] = {
|
|
555
|
+
"success": len(errors) == 0,
|
|
556
|
+
"deleted": deleted,
|
|
557
|
+
"deleted_count": len(deleted),
|
|
558
|
+
"todos": todos_list,
|
|
559
|
+
"total_count": len(todos_list),
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if errors:
|
|
563
|
+
response["errors"] = errors
|
|
564
|
+
|
|
565
|
+
except (ValueError, TypeError) as e:
|
|
566
|
+
return {"success": False, "error": str(e)}
|
|
567
|
+
else:
|
|
568
|
+
return response
|