forked from Rockachopa/Timmy-time-dashboard
feat: give Timmy hands — artifact tools for conversation (#337)
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
This commit is contained in:
@@ -1403,6 +1403,83 @@ def memory_forget(query: str) -> str:
|
||||
return f"Failed to forget: {exc}"
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
# Artifact Tools — "hands" for producing artifacts during conversation
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
NOTES_DIR = Path.home() / ".timmy" / "notes"
|
||||
DECISION_LOG = Path.home() / ".timmy" / "decisions.md"
|
||||
|
||||
|
||||
def jot_note(title: str, body: str) -> str:
|
||||
"""Write a markdown note to Timmy's workspace (~/.timmy/notes/).
|
||||
|
||||
Use this tool to capture ideas, drafts, summaries, or any artifact that
|
||||
should persist beyond the conversation. Each note is saved as a
|
||||
timestamped markdown file.
|
||||
|
||||
Args:
|
||||
title: Short descriptive title (used as filename slug).
|
||||
body: Markdown content of the note.
|
||||
|
||||
Returns:
|
||||
Confirmation with the file path of the saved note.
|
||||
"""
|
||||
if not title or not title.strip():
|
||||
return "Cannot jot — title is empty."
|
||||
if not body or not body.strip():
|
||||
return "Cannot jot — body is empty."
|
||||
|
||||
NOTES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
slug = re.sub(r"[^a-z0-9]+", "-", title.strip().lower()).strip("-")[:60]
|
||||
timestamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
||||
filename = f"{timestamp}_{slug}.md"
|
||||
filepath = NOTES_DIR / filename
|
||||
|
||||
content = f"# {title.strip()}\n\n> Created: {datetime.now(UTC).isoformat()}\n\n{body.strip()}\n"
|
||||
filepath.write_text(content)
|
||||
logger.info("jot_note: wrote %s", filepath)
|
||||
return f"Note saved: {filepath}"
|
||||
|
||||
|
||||
def log_decision(decision: str, rationale: str = "") -> str:
|
||||
"""Append an architectural or design decision to the running decision log.
|
||||
|
||||
Use this tool when a significant decision is made during conversation —
|
||||
technology choices, design trade-offs, scope changes, etc.
|
||||
|
||||
Args:
|
||||
decision: One-line summary of the decision.
|
||||
rationale: Why this decision was made (optional but encouraged).
|
||||
|
||||
Returns:
|
||||
Confirmation that the decision was logged.
|
||||
"""
|
||||
if not decision or not decision.strip():
|
||||
return "Cannot log — decision is empty."
|
||||
|
||||
DECISION_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create file with header if it doesn't exist
|
||||
if not DECISION_LOG.exists():
|
||||
DECISION_LOG.write_text(
|
||||
"# Decision Log\n\nRunning log of architectural and design decisions.\n\n"
|
||||
)
|
||||
|
||||
stamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC")
|
||||
entry = f"## {stamp} — {decision.strip()}\n\n"
|
||||
if rationale and rationale.strip():
|
||||
entry += f"{rationale.strip()}\n\n"
|
||||
entry += "---\n\n"
|
||||
|
||||
with open(DECISION_LOG, "a") as f:
|
||||
f.write(entry)
|
||||
|
||||
logger.info("log_decision: %s", decision.strip()[:80])
|
||||
return f"Decision logged: {decision.strip()}"
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
# Memory System (Central Coordinator)
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -48,6 +48,9 @@ SAFE_TOOLS = frozenset(
|
||||
"check_ollama_health",
|
||||
"get_memory_status",
|
||||
"list_swarm_agents",
|
||||
# Artifact tools
|
||||
"jot_note",
|
||||
"log_decision",
|
||||
# MCP Gitea tools
|
||||
"issue_write",
|
||||
"issue_read",
|
||||
|
||||
@@ -619,6 +619,18 @@ def _register_gematria_tool(toolkit: Toolkit) -> None:
|
||||
logger.debug("Gematria tool not available")
|
||||
|
||||
|
||||
def _register_artifact_tools(toolkit: Toolkit) -> None:
|
||||
"""Register artifact tools — notes and decision logging."""
|
||||
try:
|
||||
from timmy.memory_system import jot_note, log_decision
|
||||
|
||||
toolkit.register(jot_note, name="jot_note")
|
||||
toolkit.register(log_decision, name="log_decision")
|
||||
except (ImportError, AttributeError) as exc:
|
||||
logger.warning("Tool execution failed (Artifact tools registration): %s", exc)
|
||||
logger.debug("Artifact tools not available")
|
||||
|
||||
|
||||
def _register_thinking_tools(toolkit: Toolkit) -> None:
|
||||
"""Register thinking/introspection tools for self-reflection."""
|
||||
try:
|
||||
@@ -657,6 +669,7 @@ def create_full_toolkit(base_dir: str | Path | None = None):
|
||||
_register_introspection_tools(toolkit)
|
||||
_register_delegation_tools(toolkit)
|
||||
_register_gematria_tool(toolkit)
|
||||
_register_artifact_tools(toolkit)
|
||||
_register_thinking_tools(toolkit)
|
||||
|
||||
# Gitea issue management is now provided by the gitea-mcp server
|
||||
|
||||
Reference in New Issue
Block a user