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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- 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
|
+
)
|