crabcode 0.1.6__py3-none-win_amd64.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 (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,683 @@
1
+ """Schedule tools — create, list, cancel, and inspect scheduled jobs.
2
+
3
+ These tools allow the agent to manage time-based scheduled tasks
4
+ (cron, interval, one-shot) through the ScheduleManager.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from crabcode_core.schedule.models import ScheduleType
12
+ from crabcode_core.schedule.timing import (
13
+ compute_next_run,
14
+ next_cron_run,
15
+ parse_interval,
16
+ parse_iso_timestamp,
17
+ )
18
+ from crabcode_core.types.tool import Tool, ToolContext, ToolResult
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Helpers
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _VALID_SCHEDULE_TYPES = {"cron", "interval", "once"}
26
+
27
+
28
+ def _format_job_brief(job: dict[str, Any]) -> str:
29
+ """Format a single job dict into a one-line summary for list output."""
30
+ job_id = job.get("id", "?")
31
+ short_id = job_id[:8] if len(job_id) > 8 else job_id
32
+ name = job.get("name", "(unnamed)")
33
+ stype = job.get("schedule_type", "?")
34
+ schedule = job.get("schedule", "?")
35
+ status = job.get("status", "?")
36
+ run_count = job.get("run_count", 0)
37
+ max_runs = job.get("max_runs")
38
+ next_run = job.get("next_run")
39
+ runs_str = f"{run_count}/{max_runs}" if max_runs else str(run_count)
40
+ next_str = next_run[:19] if next_run else "—"
41
+ return f" [{short_id}] {name} | {stype}={schedule} | status={status} | runs={runs_str} | next={next_str}"
42
+
43
+
44
+ def _format_job_detail(job: dict[str, Any]) -> str:
45
+ """Format a full job detail for status output."""
46
+ lines = [
47
+ f"Job ID: {job.get('id', '?')}",
48
+ f"Name: {job.get('name', '(unnamed)')}",
49
+ f"Prompt: {job.get('prompt', '')[:200]}",
50
+ f"Schedule: {job.get('schedule_type', '?')} — {job.get('schedule', '?')}",
51
+ f"CWD: {job.get('cwd') or '(default)'}",
52
+ f"Status: {job.get('status', '?')}",
53
+ f"Enabled: {job.get('enabled', True)}",
54
+ f"Run count: {job.get('run_count', 0)}",
55
+ f"Max runs: {job.get('max_runs') or 'unlimited'}",
56
+ f"Last run: {job.get('last_run') or '—'}",
57
+ f"Next run: {job.get('next_run') or '—'}",
58
+ f"Created at: {job.get('created_at', '?')}",
59
+ f"Timeout: {job.get('timeout') or '(default)'}",
60
+ f"Model profile: {job.get('model_profile') or '(default)'}",
61
+ ]
62
+ if job.get("tags"):
63
+ lines.append(f"Tags: {', '.join(job['tags'])}")
64
+ if job.get("description"):
65
+ lines.append(f"Description: {job['description']}")
66
+ return "\n".join(lines)
67
+
68
+
69
+ def _format_run_brief(run: dict[str, Any]) -> str:
70
+ """Format a single run dict into a one-line summary."""
71
+ run_id = run.get("id", "?")
72
+ short_id = run_id[:8] if len(run_id) > 8 else run_id
73
+ status = run.get("status", "?")
74
+ started = run.get("started_at")
75
+ duration = run.get("duration_seconds")
76
+ started_str = started[:19] if started else "—"
77
+ dur_str = f"{duration:.1f}s" if duration is not None else "—"
78
+ error = run.get("error_message", "")
79
+ err_str = f" | error: {error[:80]}" if error else ""
80
+ summary = run.get("result_summary", "")
81
+ sum_str = f" | {summary[:80]}" if summary else ""
82
+ return f" [{short_id}] {status} | started={started_str} | duration={dur_str}{sum_str}{err_str}"
83
+
84
+
85
+ def _model_dict(value: Any) -> dict[str, Any]:
86
+ return value if isinstance(value, dict) else value.model_dump(mode="json")
87
+
88
+
89
+ def _compute_next_run(schedule: str, schedule_type: str) -> str | None:
90
+ """Best-effort computation of the next run timestamp.
91
+
92
+ For 'once' type, the schedule can be an ISO timestamp or a relative
93
+ offset prefixed with '+' (e.g. '+30m', '+2h', '+1d').
94
+ For 'interval', we add the interval to now.
95
+ For 'cron', we return None (requires a real cron parser).
96
+ """
97
+ try:
98
+ return compute_next_run(schedule, ScheduleType(schedule_type))
99
+ except (ValueError, TypeError):
100
+ return None
101
+
102
+
103
+ def _parse_interval(schedule: str) -> int:
104
+ """Parse an interval string (e.g. '30m', '2h', '1d') into seconds.
105
+
106
+ Supports: s (seconds), m (minutes), h (hours), d (days).
107
+ Also accepts plain integer strings (interpreted as seconds).
108
+ """
109
+ return parse_interval(schedule)
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # ScheduleCreateTool
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ class ScheduleCreateTool(Tool):
118
+ name = "ScheduleCreate"
119
+ description = "Create a new scheduled task."
120
+ is_read_only = False
121
+ is_concurrency_safe = True
122
+ input_schema = {
123
+ "type": "object",
124
+ "properties": {
125
+ "name": {
126
+ "type": "string",
127
+ "minLength": 1,
128
+ "description": "Human-readable name for the scheduled task.",
129
+ },
130
+ "prompt": {
131
+ "type": "string",
132
+ "minLength": 1,
133
+ "description": "The prompt to execute when the task fires.",
134
+ },
135
+ "schedule": {
136
+ "type": "string",
137
+ "minLength": 1,
138
+ "description": (
139
+ "Schedule definition. For cron: a cron expression (e.g. '0 */6 * * *'). "
140
+ "For interval: seconds as integer or shorthand like '30m', '2h', '1d'. "
141
+ "For once: an ISO 8601 timestamp (e.g. '2025-03-01T09:00:00Z') "
142
+ "or a relative offset prefixed with '+' (e.g. '+30m', '+2h', '+1d')."
143
+ ),
144
+ },
145
+ "schedule_type": {
146
+ "type": "string",
147
+ "enum": ["cron", "interval", "once"],
148
+ "description": (
149
+ "Type of schedule: "
150
+ "'cron' = recurring via cron expression, "
151
+ "'interval' = recurring at fixed seconds, "
152
+ "'once' = one-shot at a specific timestamp."
153
+ ),
154
+ },
155
+ "cwd": {
156
+ "type": "string",
157
+ "description": "Working directory for execution (default: current session cwd).",
158
+ },
159
+ "enabled": {
160
+ "type": "boolean",
161
+ "description": "Whether the task is active immediately (default: true).",
162
+ "default": True,
163
+ },
164
+ "max_runs": {
165
+ "type": "integer",
166
+ "description": "Maximum number of executions (default: unlimited).",
167
+ },
168
+ "next_run": {
169
+ "type": "string",
170
+ "description": "Optional ISO timestamp override for the next execution.",
171
+ },
172
+ "description": {
173
+ "type": "string",
174
+ "description": "Optional longer description of the job.",
175
+ },
176
+ "tags": {
177
+ "type": "array",
178
+ "items": {"type": "string"},
179
+ "description": "Optional tags used to classify the job.",
180
+ },
181
+ "timeout": {
182
+ "type": "integer",
183
+ "description": "Per-job timeout in seconds.",
184
+ },
185
+ "model_profile": {
186
+ "type": "string",
187
+ "description": "Model profile to use for this job's agent.",
188
+ },
189
+ "session_id": {
190
+ "type": "string",
191
+ "description": "Optional session to reuse for each execution.",
192
+ },
193
+ "extra": {
194
+ "type": "object",
195
+ "description": "Optional extension metadata stored with the job.",
196
+ },
197
+ },
198
+ "required": ["name", "prompt", "schedule", "schedule_type"],
199
+ }
200
+
201
+ async def get_prompt(self, **kwargs: Any) -> str:
202
+ return (
203
+ "Create a new scheduled task that will be executed automatically "
204
+ "based on a time-based schedule.\n\n"
205
+ "Schedule types:\n"
206
+ "- 'cron': Recurring schedule using a cron expression "
207
+ "(e.g. '0 */6 * * *' for every 6 hours).\n"
208
+ "- 'interval': Recurring at a fixed interval. "
209
+ "Provide seconds or shorthand like '30m', '2h', '1d'.\n"
210
+ "- 'once': One-shot execution at a specific ISO 8601 timestamp "
211
+ "(e.g. '2025-03-01T09:00:00Z') or a relative offset "
212
+ "(e.g. '+30m' = 30 minutes from now, '+2h' = 2 hours from now).\n\n"
213
+ "Parameters:\n"
214
+ "- name (required): A human-readable name for the task.\n"
215
+ "- prompt (required): The prompt to send to the agent when the task fires.\n"
216
+ "- schedule (required): The schedule definition (format depends on schedule_type).\n"
217
+ "- schedule_type (required): One of 'cron', 'interval', or 'once'.\n"
218
+ "- cwd (optional): Working directory (defaults to current session cwd).\n"
219
+ "- enabled (optional): Whether the task starts active (default: true).\n"
220
+ "- max_runs (optional): Maximum number of times the task should run.\n"
221
+ "- next_run (optional): Explicit ISO timestamp override for the next run.\n"
222
+ "- session_id (optional): Session to reuse for each execution.\n\n"
223
+ "Use this tool when you need to set up recurring or delayed tasks, "
224
+ "such as periodic checks, scheduled builds, or reminders."
225
+ )
226
+
227
+ async def validate_input(self, tool_input: dict[str, Any]) -> str | None:
228
+ schedule_type = tool_input.get("schedule_type")
229
+ if schedule_type not in _VALID_SCHEDULE_TYPES:
230
+ return f"schedule_type must be one of: {', '.join(sorted(_VALID_SCHEDULE_TYPES))}"
231
+ if not tool_input.get("name"):
232
+ return "name is required"
233
+ if not tool_input.get("prompt"):
234
+ return "prompt is required"
235
+ if not tool_input.get("schedule"):
236
+ return "schedule is required"
237
+
238
+ # Validate interval format
239
+ if schedule_type == "interval":
240
+ try:
241
+ _parse_interval(tool_input["schedule"])
242
+ except (ValueError, TypeError) as e:
243
+ return f"Invalid interval format: {e}"
244
+
245
+ # Validate once timestamp
246
+ if schedule_type == "once":
247
+ schedule_value = tool_input["schedule"]
248
+ # Allow relative offset: +30m, +2h, +1d, +90s, +60
249
+ if schedule_value.startswith("+"):
250
+ offset_str = schedule_value[1:]
251
+ try:
252
+ _parse_interval(offset_str)
253
+ except (ValueError, TypeError) as e:
254
+ return f"Invalid relative offset for once schedule: {e}"
255
+ else:
256
+ try:
257
+ parse_iso_timestamp(schedule_value)
258
+ except (ValueError, TypeError) as e:
259
+ return f"Invalid ISO timestamp for once schedule: {e}"
260
+
261
+ if schedule_type == "cron":
262
+ try:
263
+ next_cron_run(tool_input["schedule"])
264
+ except (ValueError, TypeError) as e:
265
+ return f"Invalid cron expression: {e}"
266
+
267
+ next_run = tool_input.get("next_run")
268
+ if next_run:
269
+ try:
270
+ parse_iso_timestamp(next_run)
271
+ except (ValueError, TypeError) as e:
272
+ return f"Invalid next_run timestamp: {e}"
273
+
274
+ if tool_input.get("session_id") is not None and not str(tool_input["session_id"]).strip():
275
+ return "session_id must not be blank"
276
+ if tool_input.get("extra") is not None and not isinstance(tool_input["extra"], dict):
277
+ return "extra must be an object"
278
+
279
+ return None
280
+
281
+ async def call(
282
+ self,
283
+ tool_input: dict[str, Any],
284
+ context: ToolContext,
285
+ ) -> ToolResult:
286
+ manager = context.schedule_manager
287
+ if not manager:
288
+ return ToolResult(
289
+ result_for_model="Error: schedule manager unavailable",
290
+ is_error=True,
291
+ )
292
+
293
+ schedule_type = tool_input["schedule_type"]
294
+ schedule = tool_input["schedule"]
295
+ name = tool_input["name"]
296
+ prompt = tool_input["prompt"]
297
+ cwd = tool_input.get("cwd") or context.cwd
298
+ max_runs = tool_input.get("max_runs")
299
+
300
+ # Compute next_run
301
+ next_run = _compute_next_run(schedule, schedule_type)
302
+
303
+ try:
304
+ job = manager.create_job(
305
+ name=name,
306
+ prompt=prompt,
307
+ schedule=schedule,
308
+ schedule_type=ScheduleType(schedule_type),
309
+ cwd=cwd,
310
+ enabled=bool(tool_input.get("enabled", True)),
311
+ max_runs=max_runs,
312
+ next_run=tool_input.get("next_run") or next_run,
313
+ description=tool_input.get("description", ""),
314
+ tags=list(tool_input.get("tags") or []),
315
+ timeout=tool_input.get("timeout"),
316
+ model_profile=tool_input.get("model_profile"),
317
+ session_id=tool_input.get("session_id"),
318
+ extra=dict(tool_input.get("extra") or {}),
319
+ )
320
+ except Exception as exc:
321
+ return ToolResult(
322
+ result_for_model=f"Error creating scheduled task: {exc}",
323
+ is_error=True,
324
+ )
325
+
326
+ job_dict = _model_dict(job)
327
+ detail = _format_job_detail(job_dict)
328
+ return ToolResult(
329
+ data=job_dict,
330
+ result_for_model=f"Scheduled task created:\n{detail}",
331
+ )
332
+
333
+
334
+ # ---------------------------------------------------------------------------
335
+ # ScheduleListTool
336
+ # ---------------------------------------------------------------------------
337
+
338
+
339
+ class ScheduleListTool(Tool):
340
+ name = "ScheduleList"
341
+ description = "List all scheduled tasks with a status summary."
342
+ is_read_only = True
343
+ is_concurrency_safe = True
344
+ input_schema = {
345
+ "type": "object",
346
+ "properties": {
347
+ "status": {
348
+ "type": "string",
349
+ "description": "Filter by job status (active, paused, completed, disabled, error).",
350
+ },
351
+ "schedule_type": {
352
+ "type": "string",
353
+ "enum": ["cron", "interval", "once"],
354
+ "description": "Filter by schedule type.",
355
+ },
356
+ },
357
+ }
358
+
359
+ async def get_prompt(self, **kwargs: Any) -> str:
360
+ return (
361
+ "List all scheduled tasks and their status summaries.\n\n"
362
+ "Returns a compact overview of each job including its name, "
363
+ "schedule type, status, run count, and next execution time.\n\n"
364
+ "You can optionally filter by status or schedule type.\n\n"
365
+ "Use this to review what tasks are currently scheduled, "
366
+ "check if any jobs have failed, or see upcoming executions."
367
+ )
368
+
369
+ async def call(
370
+ self,
371
+ tool_input: dict[str, Any],
372
+ context: ToolContext,
373
+ ) -> ToolResult:
374
+ manager = context.schedule_manager
375
+ if not manager:
376
+ return ToolResult(
377
+ result_for_model="Error: schedule manager unavailable",
378
+ is_error=True,
379
+ )
380
+
381
+ status_filter = tool_input.get("status")
382
+ type_filter = tool_input.get("schedule_type")
383
+
384
+ try:
385
+ jobs = manager.list_jobs(
386
+ status=status_filter,
387
+ schedule_type=type_filter,
388
+ )
389
+ except Exception as exc:
390
+ return ToolResult(
391
+ result_for_model=f"Error listing scheduled tasks: {exc}",
392
+ is_error=True,
393
+ )
394
+
395
+ if not jobs:
396
+ return ToolResult(
397
+ data={"count": 0, "jobs": []},
398
+ result_for_model="No scheduled tasks found.",
399
+ )
400
+
401
+ # Normalize to dicts
402
+ job_dicts = [
403
+ _model_dict(j)
404
+ for j in jobs
405
+ ]
406
+
407
+ # Summary statistics
408
+ status_counts: dict[str, int] = {}
409
+ for j in job_dicts:
410
+ s = j.get("status", "unknown")
411
+ status_counts[s] = status_counts.get(s, 0) + 1
412
+
413
+ summary_parts = [f"{count} {s}" for s, count in sorted(status_counts.items())]
414
+ summary = ", ".join(summary_parts)
415
+
416
+ lines = [f"Scheduled tasks ({summary}):"]
417
+ for j in job_dicts:
418
+ lines.append(_format_job_brief(j))
419
+
420
+ return ToolResult(
421
+ data={"count": len(job_dicts), "jobs": job_dicts},
422
+ result_for_model="\n".join(lines),
423
+ )
424
+
425
+
426
+ # ---------------------------------------------------------------------------
427
+ # ScheduleCancelTool
428
+ # ---------------------------------------------------------------------------
429
+
430
+
431
+ class ScheduleCancelTool(Tool):
432
+ name = "ScheduleCancel"
433
+ description = "Cancel and delete a scheduled task."
434
+ is_read_only = False
435
+ is_concurrency_safe = True
436
+ input_schema = {
437
+ "type": "object",
438
+ "properties": {
439
+ "job_id": {
440
+ "type": "string",
441
+ "minLength": 1,
442
+ "description": "The ID of the scheduled task to cancel/delete.",
443
+ },
444
+ },
445
+ "required": ["job_id"],
446
+ }
447
+
448
+ async def get_prompt(self, **kwargs: Any) -> str:
449
+ return (
450
+ "Cancel (delete) a scheduled task by its ID.\n\n"
451
+ "This permanently removes the task and all its execution history. "
452
+ "The task will not fire again after cancellation.\n\n"
453
+ "Use ScheduleList to find the job ID, then use this tool to cancel it."
454
+ )
455
+
456
+ async def call(
457
+ self,
458
+ tool_input: dict[str, Any],
459
+ context: ToolContext,
460
+ ) -> ToolResult:
461
+ manager = context.schedule_manager
462
+ if not manager:
463
+ return ToolResult(
464
+ result_for_model="Error: schedule manager unavailable",
465
+ is_error=True,
466
+ )
467
+
468
+ job_id = tool_input.get("job_id", "")
469
+ if not job_id:
470
+ return ToolResult(
471
+ result_for_model="Error: job_id is required",
472
+ is_error=True,
473
+ )
474
+
475
+ # Fetch current job info for the response message
476
+ try:
477
+ job = manager.get_job(job_id)
478
+ except Exception as exc:
479
+ return ToolResult(
480
+ result_for_model=f"Error fetching job: {exc}",
481
+ is_error=True,
482
+ )
483
+
484
+ if not job:
485
+ return ToolResult(
486
+ result_for_model=f"Error: scheduled task '{job_id}' not found.",
487
+ is_error=True,
488
+ )
489
+
490
+ job_dict = _model_dict(job)
491
+ job_name = job_dict.get("name", "(unnamed)")
492
+
493
+ try:
494
+ manager.cancel_job(job_id)
495
+ except Exception as exc:
496
+ return ToolResult(
497
+ result_for_model=f"Error cancelling scheduled task: {exc}",
498
+ is_error=True,
499
+ )
500
+
501
+ return ToolResult(
502
+ data={"job_id": job_id, "cancelled": True},
503
+ result_for_model=f"Scheduled task cancelled and deleted: {job_name} (id: {job_id[:8]})",
504
+ )
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # ScheduleStatusTool
509
+ # ---------------------------------------------------------------------------
510
+
511
+
512
+ class ScheduleStatusTool(Tool):
513
+ name = "ScheduleStatus"
514
+ description = "View details and recent execution history of a scheduled task."
515
+ is_read_only = True
516
+ is_concurrency_safe = True
517
+ input_schema = {
518
+ "type": "object",
519
+ "properties": {
520
+ "job_id": {
521
+ "type": "string",
522
+ "minLength": 1,
523
+ "description": "The ID of the scheduled task to inspect.",
524
+ },
525
+ "run_limit": {
526
+ "type": "integer",
527
+ "description": "Maximum number of recent runs to show (default: 10).",
528
+ },
529
+ },
530
+ "required": ["job_id"],
531
+ }
532
+
533
+ async def get_prompt(self, **kwargs: Any) -> str:
534
+ return (
535
+ "View detailed information about a single scheduled task, "
536
+ "including its configuration and recent execution history.\n\n"
537
+ "Shows:\n"
538
+ "- Full job details (name, prompt, schedule, status, etc.)\n"
539
+ "- Recent run history with status, timing, and any error messages\n\n"
540
+ "Use this to diagnose issues with a specific task, "
541
+ "check when it last ran, or verify its configuration."
542
+ )
543
+
544
+ async def call(
545
+ self,
546
+ tool_input: dict[str, Any],
547
+ context: ToolContext,
548
+ ) -> ToolResult:
549
+ manager = context.schedule_manager
550
+ if not manager:
551
+ return ToolResult(
552
+ result_for_model="Error: schedule manager unavailable",
553
+ is_error=True,
554
+ )
555
+
556
+ job_id = tool_input.get("job_id", "")
557
+ if not job_id:
558
+ return ToolResult(
559
+ result_for_model="Error: job_id is required",
560
+ is_error=True,
561
+ )
562
+
563
+ run_limit = tool_input.get("run_limit", 10)
564
+
565
+ # Fetch job
566
+ try:
567
+ job = manager.get_job(job_id)
568
+ except Exception as exc:
569
+ return ToolResult(
570
+ result_for_model=f"Error fetching job: {exc}",
571
+ is_error=True,
572
+ )
573
+
574
+ if not job:
575
+ return ToolResult(
576
+ result_for_model=f"Error: scheduled task '{job_id}' not found.",
577
+ is_error=True,
578
+ )
579
+
580
+ job_dict = _model_dict(job)
581
+
582
+ # Fetch recent runs
583
+ try:
584
+ runs = manager.list_runs(job_id, limit=run_limit)
585
+ except Exception as exc:
586
+ return ToolResult(
587
+ result_for_model=f"Error fetching run history: {exc}",
588
+ is_error=True,
589
+ )
590
+
591
+ # Build output
592
+ lines = [_format_job_detail(job_dict)]
593
+
594
+ # Normalize runs to dicts
595
+ run_dicts = [
596
+ _model_dict(r)
597
+ for r in runs
598
+ ]
599
+
600
+ if run_dicts:
601
+ lines.append("")
602
+ lines.append(f"Recent runs (showing last {len(run_dicts)}):")
603
+ for r in run_dicts:
604
+ lines.append(_format_run_brief(r))
605
+ else:
606
+ lines.append("")
607
+ lines.append("No execution history yet.")
608
+
609
+ return ToolResult(
610
+ data={"job": job_dict, "runs": run_dicts},
611
+ result_for_model="\n".join(lines),
612
+ )
613
+
614
+
615
+ class SchedulePauseTool(Tool):
616
+ name = "SchedulePause"
617
+ description = "Pause an active scheduled task without deleting its history."
618
+ is_read_only = False
619
+ is_concurrency_safe = True
620
+ input_schema = {
621
+ "type": "object",
622
+ "properties": {"job_id": {"type": "string", "minLength": 1}},
623
+ "required": ["job_id"],
624
+ }
625
+
626
+ async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
627
+ manager = context.schedule_manager
628
+ if manager is None:
629
+ return ToolResult(result_for_model="Error: schedule manager unavailable", is_error=True)
630
+ job = manager.pause_job(tool_input.get("job_id", ""))
631
+ if job is None:
632
+ return ToolResult(result_for_model="Error: scheduled task not found or ID is ambiguous", is_error=True)
633
+ data = _model_dict(job)
634
+ return ToolResult(data=data, result_for_model=f"Scheduled task status: {data['status']}")
635
+
636
+
637
+ class ScheduleResumeTool(Tool):
638
+ name = "ScheduleResume"
639
+ description = "Resume a paused or failed scheduled task."
640
+ is_read_only = False
641
+ is_concurrency_safe = True
642
+ input_schema = {
643
+ "type": "object",
644
+ "properties": {"job_id": {"type": "string", "minLength": 1}},
645
+ "required": ["job_id"],
646
+ }
647
+
648
+ async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
649
+ manager = context.schedule_manager
650
+ if manager is None:
651
+ return ToolResult(result_for_model="Error: schedule manager unavailable", is_error=True)
652
+ job = manager.resume_job(tool_input.get("job_id", ""))
653
+ if job is None:
654
+ return ToolResult(result_for_model="Error: scheduled task not found or ID is ambiguous", is_error=True)
655
+ data = _model_dict(job)
656
+ return ToolResult(data=data, result_for_model=f"Scheduled task status: {data['status']}")
657
+
658
+
659
+ class ScheduleRunTool(Tool):
660
+ name = "ScheduleRun"
661
+ description = "Trigger a scheduled task immediately."
662
+ is_read_only = False
663
+ is_concurrency_safe = True
664
+ input_schema = {
665
+ "type": "object",
666
+ "properties": {"job_id": {"type": "string", "minLength": 1}},
667
+ "required": ["job_id"],
668
+ }
669
+
670
+ async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
671
+ manager = context.schedule_manager
672
+ if manager is None:
673
+ return ToolResult(result_for_model="Error: schedule manager unavailable", is_error=True)
674
+ started = await manager.trigger_job(tool_input.get("job_id", ""))
675
+ if not started:
676
+ return ToolResult(
677
+ result_for_model="Error: scheduled task is missing, inactive, or already running",
678
+ is_error=True,
679
+ )
680
+ return ToolResult(
681
+ data={"job_id": tool_input["job_id"], "started": True},
682
+ result_for_model="Scheduled task started.",
683
+ )