caudate-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api/__init__.py +5 -0
- api/anthropic_compat.py +1518 -0
- api/artifact_viewer.py +366 -0
- api/caudate_middleware.py +618 -0
- api/forge_bootstrapper_routes.py +377 -0
- api/forge_routes.py +630 -0
- api/forge_system_routes.py +294 -0
- api/openai_compat.py +1993 -0
- api/server.py +667 -0
- api/storyboard_page.py +677 -0
- caudate_cli-0.1.0.dist-info/METADATA +354 -0
- caudate_cli-0.1.0.dist-info/RECORD +153 -0
- caudate_cli-0.1.0.dist-info/WHEEL +5 -0
- caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
- caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
- cognos_mcp/__init__.py +4 -0
- cognos_mcp/bridge.py +41 -0
- cognos_mcp/client.py +70 -0
- cognos_mcp/config.py +49 -0
- cognos_mcp/server.py +66 -0
- config.py +82 -0
- core/__init__.py +0 -0
- core/agent.py +468 -0
- core/agentic_loop.py +731 -0
- core/anthropic_auth.py +91 -0
- core/background.py +113 -0
- core/banner.py +134 -0
- core/bootstrap.py +292 -0
- core/citations.py +131 -0
- core/compaction.py +109 -0
- core/constitution.py +198 -0
- core/diff_viewer.py +87 -0
- core/export.py +85 -0
- core/file_refs.py +119 -0
- core/files.py +199 -0
- core/hooks.py +209 -0
- core/image.py +599 -0
- core/input.py +91 -0
- core/loop.py +238 -0
- core/memory_md.py +147 -0
- core/notifications.py +99 -0
- core/ownership.py +181 -0
- core/paste.py +81 -0
- core/permissions.py +210 -0
- core/plan_mode.py +215 -0
- core/sandbox_prompt.py +185 -0
- core/scheduler.py +195 -0
- core/schemas.py +202 -0
- core/session.py +90 -0
- core/settings.py +132 -0
- core/skills.py +398 -0
- core/slash_commands.py +977 -0
- core/statusline.py +61 -0
- core/subagent.py +300 -0
- core/thinking.py +50 -0
- core/updater.py +122 -0
- core/usage.py +109 -0
- core/worktree.py +93 -0
- execution/__init__.py +0 -0
- execution/executor.py +329 -0
- execution/plugins.py +108 -0
- execution/tools/__init__.py +0 -0
- execution/tools/agent_tool.py +107 -0
- execution/tools/agentic_tool.py +297 -0
- execution/tools/artifact_tool.py +191 -0
- execution/tools/ask_user_question_tool.py +137 -0
- execution/tools/base.py +81 -0
- execution/tools/calculator_tool.py +137 -0
- execution/tools/cognos_card_tool.py +124 -0
- execution/tools/cron_tool.py +215 -0
- execution/tools/datetime_tool.py +215 -0
- execution/tools/describe_image_tool.py +161 -0
- execution/tools/draw_tool.py +164 -0
- execution/tools/edit_image_tool.py +262 -0
- execution/tools/edit_tool.py +245 -0
- execution/tools/file_tool.py +90 -0
- execution/tools/find_anywhere_tool.py +255 -0
- execution/tools/forge_feature_tools.py +377 -0
- execution/tools/glob_tool.py +59 -0
- execution/tools/grep_tool.py +89 -0
- execution/tools/http_request_tool.py +224 -0
- execution/tools/load_skill_tool.py +104 -0
- execution/tools/longcat_avatar_tool.py +384 -0
- execution/tools/mcp_tool.py +100 -0
- execution/tools/notebook_tool.py +279 -0
- execution/tools/openapi_tool.py +440 -0
- execution/tools/plan_mode_tool.py +95 -0
- execution/tools/push_notification_tool.py +157 -0
- execution/tools/python_tool.py +61 -0
- execution/tools/respond_tool.py +40 -0
- execution/tools/sandbox_tool.py +378 -0
- execution/tools/search_tool.py +153 -0
- execution/tools/semantic_search_tool.py +106 -0
- execution/tools/shell_tool.py +283 -0
- execution/tools/speak_tool.py +134 -0
- execution/tools/storyboard_tool.py +727 -0
- execution/tools/system_info_tool.py +212 -0
- execution/tools/task_tool.py +323 -0
- execution/tools/think_tool.py +49 -0
- execution/tools/transcribe_audio_tool.py +86 -0
- execution/tools/update_memory_tool.py +92 -0
- execution/tools/web_fetch_tool.py +82 -0
- execution/tools/worktree_tool.py +174 -0
- llm/__init__.py +0 -0
- llm/fallback.py +116 -0
- llm/models.py +320 -0
- llm/provider.py +1356 -0
- llm/router.py +373 -0
- main.py +1889 -0
- memory/__init__.py +0 -0
- memory/episodic.py +99 -0
- memory/procedural.py +145 -0
- memory/semantic.py +71 -0
- memory/working.py +64 -0
- nn/__init__.py +43 -0
- nn/auto_evolve.py +245 -0
- nn/caudate.py +136 -0
- nn/config.py +141 -0
- nn/consolidator.py +81 -0
- nn/data.py +1635 -0
- nn/encoder.py +258 -0
- nn/forge_advisor.py +303 -0
- nn/format.py +235 -0
- nn/heads.py +432 -0
- nn/observer.py +994 -0
- nn/policy.py +214 -0
- nn/runtime.py +343 -0
- nn/scorer.py +175 -0
- nn/trainer.py +515 -0
- nn/vision.py +352 -0
- personality/__init__.py +23 -0
- personality/engine.py +129 -0
- personality/identity.py +144 -0
- personality/inner_voice.py +100 -0
- personality/mood.py +205 -0
- planning/__init__.py +0 -0
- planning/dev_server.py +221 -0
- planning/forge_models.py +718 -0
- planning/orchestrator.py +1363 -0
- planning/planner.py +451 -0
- planning/task_graph.py +61 -0
- reflection/__init__.py +0 -0
- reflection/meta_learner.py +156 -0
- reflection/reflector.py +127 -0
- ui/__init__.py +5 -0
- ui/display.py +88 -0
- voice/__init__.py +0 -0
- voice/conversation.py +125 -0
- voice/listener.py +111 -0
- voice/speaker.py +59 -0
- voice/stt.py +126 -0
- voice/tts.py +214 -0
execution/tools/base.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Base tool interface — Claude SDK compatible tool protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from core.schemas import ToolResult, ToolResultStatus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseTool(ABC):
|
|
12
|
+
"""Abstract base class for all tools.
|
|
13
|
+
|
|
14
|
+
Tools define their interface via `input_schema` (JSON Schema),
|
|
15
|
+
matching the Claude SDK tool definition format.
|
|
16
|
+
|
|
17
|
+
Plan-mode contract: subclasses that perform any side-effect
|
|
18
|
+
(writing files, running shell commands, mutating external state)
|
|
19
|
+
set `mutates = True`. The executor refuses to run mutating tools
|
|
20
|
+
while plan mode is on. Read-only tools (Read, Grep, Glob,
|
|
21
|
+
SystemInfo, Calculator, etc.) leave `mutates = False`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Whether this tool produces side effects. Default False — every
|
|
25
|
+
# subclass that writes files / runs commands / sends network
|
|
26
|
+
# requests / changes durable state should override to True.
|
|
27
|
+
mutates: bool = False
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
"""Unique tool name."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def description(self) -> str:
|
|
38
|
+
"""Human-readable description of what the tool does."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def input_schema(self) -> dict[str, Any]:
|
|
43
|
+
"""JSON Schema defining the tool's input parameters.
|
|
44
|
+
|
|
45
|
+
Override this in subclasses. Default returns empty object schema.
|
|
46
|
+
"""
|
|
47
|
+
return {"type": "object", "properties": {}, "required": []}
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def parameters(self) -> dict[str, str]:
|
|
51
|
+
"""Legacy parameter descriptions for LLM planning.
|
|
52
|
+
|
|
53
|
+
Auto-generated from input_schema for backward compatibility.
|
|
54
|
+
"""
|
|
55
|
+
props = self.input_schema.get("properties", {})
|
|
56
|
+
return {k: v.get("description", "") for k, v in props.items()}
|
|
57
|
+
|
|
58
|
+
def to_tool_definition(self) -> dict[str, Any]:
|
|
59
|
+
"""Return LiteLLM-compatible tool definition.
|
|
60
|
+
|
|
61
|
+
Format: {"type": "function", "function": {"name", "description", "parameters"}}
|
|
62
|
+
"""
|
|
63
|
+
return {
|
|
64
|
+
"type": "function",
|
|
65
|
+
"function": {
|
|
66
|
+
"name": self.name,
|
|
67
|
+
"description": self.description,
|
|
68
|
+
"parameters": self.input_schema,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
74
|
+
"""Execute the tool with given arguments."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
def _success(self, output: Any) -> ToolResult:
|
|
78
|
+
return ToolResult(tool_name=self.name, status=ToolResultStatus.SUCCESS, output=output)
|
|
79
|
+
|
|
80
|
+
def _error(self, error: str) -> ToolResult:
|
|
81
|
+
return ToolResult(tool_name=self.name, status=ToolResultStatus.ERROR, error=error)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Calculator — exact arithmetic and symbolic math.
|
|
2
|
+
|
|
3
|
+
LLMs hallucinate digits past ~10-digit precision and get basic
|
|
4
|
+
multi-step arithmetic wrong surprisingly often. This tool does the
|
|
5
|
+
computation deterministically using Python's expression evaluator,
|
|
6
|
+
then optionally falls through to `sympy` for symbolic ops if the
|
|
7
|
+
expression contains symbolic markers.
|
|
8
|
+
|
|
9
|
+
Safety: uses a strict allowlist of names + AST validation. No
|
|
10
|
+
imports, no attribute access, no comprehensions, no function calls
|
|
11
|
+
except a curated math whitelist. Untrusted expressions can't escape
|
|
12
|
+
the sandbox.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
import logging
|
|
19
|
+
import math
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from core.schemas import ToolResult
|
|
23
|
+
from execution.tools.base import BaseTool
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Allowed top-level names — math constants and a curated set of
|
|
29
|
+
# functions. Anything not in this dict raises NameError.
|
|
30
|
+
_ALLOWED_NAMES: dict[str, Any] = {
|
|
31
|
+
# constants
|
|
32
|
+
"pi": math.pi, "e": math.e, "tau": math.tau, "inf": math.inf,
|
|
33
|
+
# safe scalar functions
|
|
34
|
+
"abs": abs, "round": round, "min": min, "max": max,
|
|
35
|
+
"sum": sum, "pow": pow,
|
|
36
|
+
# math module
|
|
37
|
+
"sqrt": math.sqrt, "log": math.log, "log2": math.log2,
|
|
38
|
+
"log10": math.log10, "exp": math.exp,
|
|
39
|
+
"sin": math.sin, "cos": math.cos, "tan": math.tan,
|
|
40
|
+
"asin": math.asin, "acos": math.acos, "atan": math.atan, "atan2": math.atan2,
|
|
41
|
+
"sinh": math.sinh, "cosh": math.cosh, "tanh": math.tanh,
|
|
42
|
+
"ceil": math.ceil, "floor": math.floor, "trunc": math.trunc,
|
|
43
|
+
"factorial": math.factorial, "gcd": math.gcd, "lcm": math.lcm,
|
|
44
|
+
"degrees": math.degrees, "radians": math.radians,
|
|
45
|
+
"hypot": math.hypot, "fabs": math.fabs,
|
|
46
|
+
"fmod": math.fmod, "modf": math.modf,
|
|
47
|
+
"isclose": math.isclose, "isfinite": math.isfinite, "isnan": math.isnan,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# AST node types that are NOT allowed in the expression. We allow
|
|
52
|
+
# arithmetic, comparisons, conditionals, name lookups (gated by the
|
|
53
|
+
# allowlist above), and function calls (also gated).
|
|
54
|
+
_FORBIDDEN_NODES = (
|
|
55
|
+
ast.Import, ast.ImportFrom,
|
|
56
|
+
ast.Lambda, ast.FunctionDef, ast.ClassDef,
|
|
57
|
+
ast.Attribute, # no x.y access — blocks os.system etc
|
|
58
|
+
ast.Subscript, # no list/dict subscripting
|
|
59
|
+
ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp,
|
|
60
|
+
ast.Yield, ast.YieldFrom,
|
|
61
|
+
ast.Await,
|
|
62
|
+
ast.Assign, ast.AugAssign, ast.AnnAssign,
|
|
63
|
+
ast.Delete,
|
|
64
|
+
ast.Global, ast.Nonlocal,
|
|
65
|
+
ast.Try,
|
|
66
|
+
ast.With, ast.AsyncWith, ast.AsyncFor, ast.AsyncFunctionDef,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate_ast(tree: ast.AST) -> None:
|
|
71
|
+
"""Raise ValueError if tree contains forbidden nodes."""
|
|
72
|
+
for node in ast.walk(tree):
|
|
73
|
+
if isinstance(node, _FORBIDDEN_NODES):
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"forbidden expression element: {type(node).__name__}"
|
|
76
|
+
)
|
|
77
|
+
# Function calls must use names from the allowlist
|
|
78
|
+
if isinstance(node, ast.Call):
|
|
79
|
+
func = node.func
|
|
80
|
+
if not isinstance(func, ast.Name):
|
|
81
|
+
raise ValueError("function calls must be on bare names")
|
|
82
|
+
if func.id not in _ALLOWED_NAMES:
|
|
83
|
+
raise ValueError(f"function not allowed: {func.id}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _eval_safe(expr: str) -> Any:
|
|
87
|
+
"""Parse + validate + eval an arithmetic expression."""
|
|
88
|
+
tree = ast.parse(expr, mode="eval")
|
|
89
|
+
_validate_ast(tree)
|
|
90
|
+
return eval(compile(tree, "<calculator>", "eval"),
|
|
91
|
+
{"__builtins__": {}}, _ALLOWED_NAMES)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CalculatorTool(BaseTool):
|
|
95
|
+
name = "Calculator"
|
|
96
|
+
description = (
|
|
97
|
+
"Evaluate an arithmetic or scalar-math expression exactly. "
|
|
98
|
+
"Use this whenever the user asks for arithmetic, percentages, "
|
|
99
|
+
"unit conversions, or anything where digits past ~10 places "
|
|
100
|
+
"matter. Supports +, -, *, /, **, math constants (pi, e), and "
|
|
101
|
+
"common math functions (sqrt, log, sin, cos, factorial, etc.). "
|
|
102
|
+
"No variables, no imports — pure expressions."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def input_schema(self) -> dict[str, Any]:
|
|
107
|
+
return {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"properties": {
|
|
110
|
+
"expression": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"description": "Expression to evaluate, e.g. "
|
|
113
|
+
"'2 ** 32 - 1' or 'sqrt(2) * 5'.",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"required": ["expression"],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
120
|
+
expr = (kwargs.get("expression") or "").strip()
|
|
121
|
+
if not expr:
|
|
122
|
+
return ToolResult(
|
|
123
|
+
tool_name=self.name, status="error",
|
|
124
|
+
error="empty expression",
|
|
125
|
+
)
|
|
126
|
+
try:
|
|
127
|
+
result = _eval_safe(expr)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return ToolResult(
|
|
130
|
+
tool_name=self.name, status="error",
|
|
131
|
+
error=f"{type(e).__name__}: {e}",
|
|
132
|
+
)
|
|
133
|
+
return ToolResult(
|
|
134
|
+
tool_name=self.name, status="success",
|
|
135
|
+
output=f"{expr} = {result}",
|
|
136
|
+
metadata={"expression": expr, "result": result},
|
|
137
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""CognosCard — read the Cognos self-spec card.
|
|
2
|
+
|
|
3
|
+
Mirrors the structure of the Anthropic Opus self-card the user
|
|
4
|
+
showed: confirmable facts / architecture / training / capabilities /
|
|
5
|
+
**what we can't honestly tell you**. The card lives at
|
|
6
|
+
`data/COGNOS_CARD.md` and is human-edited (not auto-generated).
|
|
7
|
+
|
|
8
|
+
The LLM can call this tool to:
|
|
9
|
+
- Answer "what is Cognos?" / "what can you do?" honestly
|
|
10
|
+
- Self-introspect before claiming a capability ("can I do X?")
|
|
11
|
+
- Anchor on the explicit boundaries section before responding
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from core.schemas import ToolResult
|
|
21
|
+
from execution.tools.base import BaseTool
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_CARD_PATH = Path("/home/raveuk/cognos/data/COGNOS_CARD.md")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def render_cognos_card_short() -> str:
|
|
30
|
+
"""One-paragraph reference to the card, suitable for system-prompt
|
|
31
|
+
injection. Tells the LLM the card exists + how to read it; doesn't
|
|
32
|
+
bloat the prompt with the full card body.
|
|
33
|
+
"""
|
|
34
|
+
return (
|
|
35
|
+
"## About Cognos\n\n"
|
|
36
|
+
"You are running inside **Cognos** — a local-first cognitive "
|
|
37
|
+
"agent on the user's own machine. The full structured "
|
|
38
|
+
"self-spec lives at `data/COGNOS_CARD.md` and is also "
|
|
39
|
+
"available via the `CognosCard` tool: confirmable facts, "
|
|
40
|
+
"architecture, capabilities, and an explicit list of what "
|
|
41
|
+
"Cognos cannot honestly tell you. **Call CognosCard before "
|
|
42
|
+
"claiming a capability you're unsure about** — the card is "
|
|
43
|
+
"the source of truth, not your model's training defaults."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CognosCardTool(BaseTool):
|
|
48
|
+
name = "CognosCard"
|
|
49
|
+
description = (
|
|
50
|
+
"Read the Cognos self-spec card — verifiable facts about the "
|
|
51
|
+
"agent you're running inside, its architecture, what it can "
|
|
52
|
+
"do, and (importantly) what it CANNOT honestly tell you. "
|
|
53
|
+
"Call this whenever the user asks 'what are you', 'what can "
|
|
54
|
+
"you do', or whenever you'd otherwise speculate about Cognos "
|
|
55
|
+
"internals. Returns the full markdown card."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def input_schema(self) -> dict[str, Any]:
|
|
60
|
+
return {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"section": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"enum": [
|
|
66
|
+
"all",
|
|
67
|
+
"confirm",
|
|
68
|
+
"architecture",
|
|
69
|
+
"training",
|
|
70
|
+
"capabilities",
|
|
71
|
+
"boundaries",
|
|
72
|
+
],
|
|
73
|
+
"description": "Which section to return. 'all' returns the full card; the others return only that section.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
"required": [],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
80
|
+
if not _CARD_PATH.exists():
|
|
81
|
+
return ToolResult(
|
|
82
|
+
tool_name=self.name, status="error",
|
|
83
|
+
error=f"card not found at {_CARD_PATH}",
|
|
84
|
+
)
|
|
85
|
+
text = _CARD_PATH.read_text()
|
|
86
|
+
section = (kwargs.get("section") or "all").lower().strip()
|
|
87
|
+
if section == "all":
|
|
88
|
+
return ToolResult(
|
|
89
|
+
tool_name=self.name, status="success",
|
|
90
|
+
output=text,
|
|
91
|
+
metadata={"path": str(_CARD_PATH), "section": "all"},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Section extraction — find the relevant H2 heading and return
|
|
95
|
+
# everything until the next H2.
|
|
96
|
+
section_headings = {
|
|
97
|
+
"confirm": "## What I can publicly confirm",
|
|
98
|
+
"architecture": "## Architecture (publicly known shape)",
|
|
99
|
+
"training": "## Caudate training pipeline (general shape)",
|
|
100
|
+
"capabilities": "## Capabilities at inference",
|
|
101
|
+
"boundaries": "## What I cannot honestly tell you",
|
|
102
|
+
}
|
|
103
|
+
marker = section_headings.get(section)
|
|
104
|
+
if not marker:
|
|
105
|
+
return ToolResult(
|
|
106
|
+
tool_name=self.name, status="error",
|
|
107
|
+
error=f"unknown section {section!r}",
|
|
108
|
+
)
|
|
109
|
+
idx = text.find(marker)
|
|
110
|
+
if idx < 0:
|
|
111
|
+
return ToolResult(
|
|
112
|
+
tool_name=self.name, status="error",
|
|
113
|
+
error=f"section heading not found: {marker!r}",
|
|
114
|
+
)
|
|
115
|
+
# Slice from the heading to the next H2 (or end of file)
|
|
116
|
+
end = text.find("\n## ", idx + len(marker))
|
|
117
|
+
if end < 0:
|
|
118
|
+
end = len(text)
|
|
119
|
+
body = text[idx:end].rstrip()
|
|
120
|
+
return ToolResult(
|
|
121
|
+
tool_name=self.name, status="success",
|
|
122
|
+
output=body,
|
|
123
|
+
metadata={"path": str(_CARD_PATH), "section": section},
|
|
124
|
+
)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Cron tools — schedule, list, and delete recurring agent prompts.
|
|
2
|
+
|
|
3
|
+
Three thin wrappers on top of `core/scheduler.py::CronStore`:
|
|
4
|
+
|
|
5
|
+
- **`CronCreate`** — register a new recurring prompt.
|
|
6
|
+
- **`CronList`** — show all registered jobs (with status).
|
|
7
|
+
- **`CronDelete`** — remove a job by id.
|
|
8
|
+
|
|
9
|
+
Schedule syntax (matching `core/scheduler.py::_parse_schedule`):
|
|
10
|
+
|
|
11
|
+
- `every 30s` / `every 5m` / `every 2h` / `every 1d`
|
|
12
|
+
- `daily HH:MM` (e.g. `daily 09:30`)
|
|
13
|
+
- `weekly mon HH:MM` (mon|tue|wed|thu|fri|sat|sun)
|
|
14
|
+
- `at YYYY-MM-DD HH:MM` (one-shot)
|
|
15
|
+
|
|
16
|
+
Persistence: jobs live in `data/cron.json`. The runtime fires them
|
|
17
|
+
when `cognos cron run` is active (a separate daemon process). These
|
|
18
|
+
tools just manage the registry — they don't fire anything immediately.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from core.schemas import ToolResult
|
|
26
|
+
from execution.tools.base import BaseTool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _store():
|
|
30
|
+
"""Lazy-init the CronStore against the configured data dir."""
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from config import DATA_DIR
|
|
33
|
+
from core.scheduler import CronStore
|
|
34
|
+
return CronStore(Path(DATA_DIR) / "cron.json")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _format_job(job) -> str:
|
|
38
|
+
status = "enabled " if job.enabled else "disabled"
|
|
39
|
+
nxt = job.next_run or "—"
|
|
40
|
+
last = job.last_run or "never"
|
|
41
|
+
return (
|
|
42
|
+
f" {job.id} [{status}] schedule={job.schedule!r}\n"
|
|
43
|
+
f" next: {nxt}\n"
|
|
44
|
+
f" last: {last}\n"
|
|
45
|
+
f" prompt: {job.prompt[:80]}{'…' if len(job.prompt) > 80 else ''}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CronCreateTool(BaseTool):
|
|
50
|
+
mutates = True
|
|
51
|
+
name = "CronCreate"
|
|
52
|
+
description = (
|
|
53
|
+
"Schedule a recurring prompt that Cognos will run automatically "
|
|
54
|
+
"when the cron daemon is active. Pass a `prompt` (what the agent "
|
|
55
|
+
"should do each fire) and a `schedule` string. Schedule syntax: "
|
|
56
|
+
"`every 30s|5m|2h|1d`, `daily HH:MM`, `weekly mon HH:MM`, or "
|
|
57
|
+
"`at YYYY-MM-DD HH:MM` for one-shot. Returns the new job id. "
|
|
58
|
+
"Note: the daemon runs separately via `cognos cron run`; this "
|
|
59
|
+
"tool only registers the job."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def input_schema(self) -> dict[str, Any]:
|
|
64
|
+
return {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"properties": {
|
|
67
|
+
"prompt": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "What the agent should do each fire (e.g. 'Summarize today's storyboard runs').",
|
|
70
|
+
},
|
|
71
|
+
"schedule": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": (
|
|
74
|
+
"When to fire. Examples: 'every 5m', 'daily "
|
|
75
|
+
"09:30', 'weekly mon 09:00', 'at 2026-12-25 08:00'."
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
"required": ["prompt", "schedule"],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
83
|
+
prompt = (kwargs.get("prompt") or "").strip()
|
|
84
|
+
schedule = (kwargs.get("schedule") or "").strip()
|
|
85
|
+
if not prompt or not schedule:
|
|
86
|
+
return ToolResult(
|
|
87
|
+
tool_name=self.name, status="error",
|
|
88
|
+
error="`prompt` and `schedule` are both required",
|
|
89
|
+
)
|
|
90
|
+
try:
|
|
91
|
+
store = _store()
|
|
92
|
+
job = store.add(prompt, schedule)
|
|
93
|
+
except ValueError as e:
|
|
94
|
+
return ToolResult(
|
|
95
|
+
tool_name=self.name, status="error",
|
|
96
|
+
error=(
|
|
97
|
+
f"{e}. Valid schedules: 'every 5m', 'daily 09:30', "
|
|
98
|
+
"'weekly mon 09:00', 'at YYYY-MM-DD HH:MM'."
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
return ToolResult(
|
|
103
|
+
tool_name=self.name, status="error",
|
|
104
|
+
error=f"failed to create cron job: {e}",
|
|
105
|
+
)
|
|
106
|
+
return ToolResult(
|
|
107
|
+
tool_name=self.name, status="success",
|
|
108
|
+
output=(
|
|
109
|
+
f"Cron job created.\n"
|
|
110
|
+
f" id: {job.id}\n"
|
|
111
|
+
f" schedule: {job.schedule}\n"
|
|
112
|
+
f" next run: {job.next_run}\n"
|
|
113
|
+
f" prompt: {job.prompt[:100]}"
|
|
114
|
+
),
|
|
115
|
+
metadata={
|
|
116
|
+
"job_id": job.id,
|
|
117
|
+
"schedule": job.schedule,
|
|
118
|
+
"next_run": job.next_run,
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CronListTool(BaseTool):
|
|
124
|
+
name = "CronList"
|
|
125
|
+
description = (
|
|
126
|
+
"List all registered cron jobs with their schedule, next run "
|
|
127
|
+
"time, last run time, and the prompt. No arguments. Returns a "
|
|
128
|
+
"human-readable list; the metadata field has the structured "
|
|
129
|
+
"version."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def input_schema(self) -> dict[str, Any]:
|
|
134
|
+
return {"type": "object", "properties": {}}
|
|
135
|
+
|
|
136
|
+
async def execute(self, **_: Any) -> ToolResult:
|
|
137
|
+
try:
|
|
138
|
+
jobs = _store().list()
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return ToolResult(
|
|
141
|
+
tool_name=self.name, status="error",
|
|
142
|
+
error=f"failed to read cron store: {e}",
|
|
143
|
+
)
|
|
144
|
+
if not jobs:
|
|
145
|
+
return ToolResult(
|
|
146
|
+
tool_name=self.name, status="success",
|
|
147
|
+
output="No cron jobs registered.",
|
|
148
|
+
metadata={"jobs": []},
|
|
149
|
+
)
|
|
150
|
+
lines = [f"{len(jobs)} cron job(s):", ""]
|
|
151
|
+
lines.extend(_format_job(j) for j in jobs)
|
|
152
|
+
return ToolResult(
|
|
153
|
+
tool_name=self.name, status="success",
|
|
154
|
+
output="\n".join(lines),
|
|
155
|
+
metadata={
|
|
156
|
+
"jobs": [
|
|
157
|
+
{
|
|
158
|
+
"id": j.id,
|
|
159
|
+
"schedule": j.schedule,
|
|
160
|
+
"enabled": j.enabled,
|
|
161
|
+
"next_run": j.next_run,
|
|
162
|
+
"last_run": j.last_run,
|
|
163
|
+
"prompt": j.prompt,
|
|
164
|
+
}
|
|
165
|
+
for j in jobs
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class CronDeleteTool(BaseTool):
|
|
172
|
+
mutates = True
|
|
173
|
+
name = "CronDelete"
|
|
174
|
+
description = (
|
|
175
|
+
"Delete a cron job by its id. Use `CronList` first to find the "
|
|
176
|
+
"id. Returns success or 'not found'."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def input_schema(self) -> dict[str, Any]:
|
|
181
|
+
return {
|
|
182
|
+
"type": "object",
|
|
183
|
+
"properties": {
|
|
184
|
+
"job_id": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"description": "Cron job id from CronList.",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
"required": ["job_id"],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
193
|
+
job_id = (kwargs.get("job_id") or "").strip()
|
|
194
|
+
if not job_id:
|
|
195
|
+
return ToolResult(
|
|
196
|
+
tool_name=self.name, status="error",
|
|
197
|
+
error="`job_id` is required",
|
|
198
|
+
)
|
|
199
|
+
try:
|
|
200
|
+
ok = _store().remove(job_id)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
return ToolResult(
|
|
203
|
+
tool_name=self.name, status="error",
|
|
204
|
+
error=f"failed to delete cron job: {e}",
|
|
205
|
+
)
|
|
206
|
+
if not ok:
|
|
207
|
+
return ToolResult(
|
|
208
|
+
tool_name=self.name, status="error",
|
|
209
|
+
error=f"no cron job with id {job_id!r}",
|
|
210
|
+
)
|
|
211
|
+
return ToolResult(
|
|
212
|
+
tool_name=self.name, status="success",
|
|
213
|
+
output=f"Deleted cron job {job_id}.",
|
|
214
|
+
metadata={"job_id": job_id},
|
|
215
|
+
)
|