feat: add journal entry update, disable, append, prepend tools
Four new tools for full journal entry lifecycle management: - ae_journal_entry_update — PATCH any combination of fields (title, content, summary, tags, enable); only provided fields are changed - ae_journal_entry_disable — soft-delete via enable=false - ae_journal_entry_append — fetch entry, append timestamped section to the bottom (ideal for running logs / data logs) - ae_journal_entry_prepend — fetch entry, prepend timestamped section to the top (most-recent-first pattern) Shared _get_entry / _patch_entry helpers keep the read-modify-write logic DRY. Also fixed journal_entry_create to prefer the canonical journal_entry_id field over the legacy id_random alias. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,12 @@ IMPORTANT: These tools are separate from the ae_* MCP tools used by the fleet ag
|
||||
from google.genai import types
|
||||
from tools.web import search as _web_search
|
||||
from tools.ae_knowledge import journal_search as _ae_journal_search
|
||||
from tools.ae_knowledge import journal_entry_create as _ae_journal_entry_create
|
||||
from tools.ae_knowledge import journal_list as _ae_journal_list
|
||||
from tools.ae_knowledge import journal_entry_create as _ae_journal_entry_create
|
||||
from tools.ae_knowledge import journal_entry_update as _ae_journal_entry_update
|
||||
from tools.ae_knowledge import journal_entry_disable as _ae_journal_entry_disable
|
||||
from tools.ae_knowledge import journal_entry_append as _ae_journal_entry_append
|
||||
from tools.ae_knowledge import journal_entry_prepend as _ae_journal_entry_prepend
|
||||
from tools.ae_tasks import task_list as _ae_task_list
|
||||
from tools.files import file_read as _file_read
|
||||
from tools.system import claude_allow_dir as _claude_allow_dir, shell_exec as _shell_exec
|
||||
@@ -148,6 +152,79 @@ _ae_journal_entry_create_declaration = types.FunctionDeclaration(
|
||||
),
|
||||
)
|
||||
|
||||
_ae_journal_entry_update_declaration = types.FunctionDeclaration(
|
||||
name="ae_journal_entry_update",
|
||||
description=(
|
||||
"Update fields on an existing journal entry. Only the fields you provide are changed — "
|
||||
"omitted fields are left as-is. Use ae_journal_search to find the entry_id first. "
|
||||
"To soft-delete, use ae_journal_entry_disable instead."
|
||||
),
|
||||
parameters=types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"entry_id": types.Schema(type=types.Type.STRING, description="Journal entry id_random"),
|
||||
"title": types.Schema(type=types.Type.STRING, description="New title"),
|
||||
"content": types.Schema(type=types.Type.STRING, description="Replacement content (full, markdown supported)"),
|
||||
"summary": types.Schema(type=types.Type.STRING, description="New summary"),
|
||||
"tags": types.Schema(type=types.Type.STRING, description="Replacement comma-separated tags"),
|
||||
"enable": types.Schema(type=types.Type.BOOLEAN, description="Set false to hide/disable the entry"),
|
||||
},
|
||||
required=["entry_id"],
|
||||
),
|
||||
)
|
||||
|
||||
_ae_journal_entry_disable_declaration = types.FunctionDeclaration(
|
||||
name="ae_journal_entry_disable",
|
||||
description=(
|
||||
"Soft-delete a journal entry by setting enable=false. "
|
||||
"The entry is hidden but not permanently removed. "
|
||||
"Use ae_journal_search to find the entry_id first."
|
||||
),
|
||||
parameters=types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"entry_id": types.Schema(type=types.Type.STRING, description="Journal entry id_random"),
|
||||
},
|
||||
required=["entry_id"],
|
||||
),
|
||||
)
|
||||
|
||||
_ae_journal_entry_append_declaration = types.FunctionDeclaration(
|
||||
name="ae_journal_entry_append",
|
||||
description=(
|
||||
"Append a new section to the bottom of a journal entry's content. "
|
||||
"Each section gets a UTC timestamp heading unless you provide one. "
|
||||
"Ideal for timestamped logs, running notes, or data logs."
|
||||
),
|
||||
parameters=types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"entry_id": types.Schema(type=types.Type.STRING, description="Journal entry id_random"),
|
||||
"content": types.Schema(type=types.Type.STRING, description="The text to append (markdown supported)"),
|
||||
"heading": types.Schema(type=types.Type.STRING, description="Optional section heading (defaults to current UTC timestamp)"),
|
||||
},
|
||||
required=["entry_id", "content"],
|
||||
),
|
||||
)
|
||||
|
||||
_ae_journal_entry_prepend_declaration = types.FunctionDeclaration(
|
||||
name="ae_journal_entry_prepend",
|
||||
description=(
|
||||
"Prepend a new section to the top of a journal entry's content. "
|
||||
"Each section gets a UTC timestamp heading unless you provide one. "
|
||||
"Useful for most-recent-first logs."
|
||||
),
|
||||
parameters=types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"entry_id": types.Schema(type=types.Type.STRING, description="Journal entry id_random"),
|
||||
"content": types.Schema(type=types.Type.STRING, description="The text to prepend (markdown supported)"),
|
||||
"heading": types.Schema(type=types.Type.STRING, description="Optional section heading (defaults to current UTC timestamp)"),
|
||||
},
|
||||
required=["entry_id", "content"],
|
||||
),
|
||||
)
|
||||
|
||||
_ae_task_list_declaration = types.FunctionDeclaration(
|
||||
name="ae_task_list",
|
||||
description=(
|
||||
@@ -202,6 +279,10 @@ _CALLABLES: dict[str, callable] = {
|
||||
"ae_journal_list": _ae_journal_list,
|
||||
"ae_journal_search": _ae_journal_search,
|
||||
"ae_journal_entry_create": _ae_journal_entry_create,
|
||||
"ae_journal_entry_update": _ae_journal_entry_update,
|
||||
"ae_journal_entry_disable": _ae_journal_entry_disable,
|
||||
"ae_journal_entry_append": _ae_journal_entry_append,
|
||||
"ae_journal_entry_prepend": _ae_journal_entry_prepend,
|
||||
"ae_task_list": _ae_task_list,
|
||||
"file_read": _file_read,
|
||||
"claude_allow_dir": _claude_allow_dir,
|
||||
@@ -567,6 +648,10 @@ TOOL_DECLARATIONS = [
|
||||
_ae_journal_list_declaration,
|
||||
_ae_journal_search_declaration,
|
||||
_ae_journal_entry_create_declaration,
|
||||
_ae_journal_entry_update_declaration,
|
||||
_ae_journal_entry_disable_declaration,
|
||||
_ae_journal_entry_append_declaration,
|
||||
_ae_journal_entry_prepend_declaration,
|
||||
_ae_task_list_declaration,
|
||||
_file_read_declaration,
|
||||
_claude_allow_dir_declaration,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""
|
||||
Aether Platform knowledge tools — journal search and entry creation.
|
||||
Aether Platform knowledge tools — journal search, listing, and entry management.
|
||||
|
||||
These tools give the orchestrator read/write access to the AE Journals module,
|
||||
which serves as the primary long-term knowledge base.
|
||||
|
||||
Auth: x-aether-api-key + x-account-id headers (same pattern as agents_sync scripts).
|
||||
API: V3 CRUD — POST /v3/crud/journal_entry/search, POST /v3/crud/journal/{id}/journal_entry/
|
||||
PATCH /v3/crud/journal_entry/{entry_id}, GET /v3/crud/journal_entry/{entry_id}
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -216,8 +217,170 @@ def _sync_journal_entry_create(
|
||||
return f"Journal entry creation error: {e}"
|
||||
|
||||
entry_id = (
|
||||
result.get("data", {}).get("id_random")
|
||||
result.get("data", {}).get("journal_entry_id")
|
||||
or result.get("data", {}).get("id_random")
|
||||
or result.get("id_random")
|
||||
or "unknown"
|
||||
)
|
||||
return f"Journal entry created. id: `{entry_id}`, title: \"{title}\", journal: `{journal_id}`"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared helper: fetch a single journal entry by id
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _get_entry(entry_id: str) -> dict | str:
|
||||
"""Return the entry dict, or an error string on failure."""
|
||||
import requests
|
||||
url = f"{settings.ae_api_url}/v3/crud/journal_entry/{entry_id}"
|
||||
try:
|
||||
resp = requests.get(url, headers=_headers(), timeout=settings.ae_api_timeout)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
entry = data.get("data") or data
|
||||
if not isinstance(entry, dict):
|
||||
return f"Unexpected response shape for entry {entry_id}"
|
||||
return entry
|
||||
except Exception as e:
|
||||
logger.warning("_get_entry %s failed: %s", entry_id, e)
|
||||
return f"Error fetching entry {entry_id}: {e}"
|
||||
|
||||
|
||||
def _patch_entry(entry_id: str, payload: dict) -> str:
|
||||
"""PATCH a journal entry. Returns a success/error string."""
|
||||
import requests
|
||||
url = f"{settings.ae_api_url}/v3/crud/journal_entry/{entry_id}"
|
||||
try:
|
||||
resp = requests.patch(
|
||||
url,
|
||||
headers=_headers(),
|
||||
json=payload,
|
||||
timeout=settings.ae_api_timeout,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return "ok"
|
||||
except Exception as e:
|
||||
logger.warning("_patch_entry %s failed: %s", entry_id, e)
|
||||
return f"Error updating entry {entry_id}: {e}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: ae_journal_entry_update
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def journal_entry_update(
|
||||
entry_id: str,
|
||||
title: str = "",
|
||||
content: str = "",
|
||||
summary: str = "",
|
||||
tags: str = "",
|
||||
enable: bool | None = None,
|
||||
) -> str:
|
||||
"""Update fields on an existing journal entry. Only provided fields are changed."""
|
||||
err = _check_config()
|
||||
if err:
|
||||
return err
|
||||
return await asyncio.to_thread(_sync_journal_entry_update, entry_id, title, content, summary, tags, enable)
|
||||
|
||||
|
||||
def _sync_journal_entry_update(
|
||||
entry_id: str,
|
||||
title: str,
|
||||
content: str,
|
||||
summary: str,
|
||||
tags: str,
|
||||
enable: bool | None,
|
||||
) -> str:
|
||||
payload: dict = {}
|
||||
if title:
|
||||
payload["name"] = title
|
||||
if content:
|
||||
payload["content"] = content
|
||||
if summary:
|
||||
payload["summary"] = summary
|
||||
if tags:
|
||||
payload["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
||||
if enable is not None:
|
||||
payload["enable"] = enable
|
||||
|
||||
if not payload:
|
||||
return "Nothing to update — no fields provided."
|
||||
|
||||
result = _patch_entry(entry_id, payload)
|
||||
if result != "ok":
|
||||
return result
|
||||
|
||||
updated = ", ".join(payload.keys())
|
||||
return f"Journal entry `{entry_id}` updated. Fields changed: {updated}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: ae_journal_entry_disable
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def journal_entry_disable(entry_id: str) -> str:
|
||||
"""Soft-delete a journal entry by setting enable=false."""
|
||||
err = _check_config()
|
||||
if err:
|
||||
return err
|
||||
return await asyncio.to_thread(_patch_entry, entry_id, {"enable": False})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: ae_journal_entry_append
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def journal_entry_append(entry_id: str, content: str, heading: str = "") -> str:
|
||||
"""Append a timestamped section to the bottom of a journal entry's content."""
|
||||
err = _check_config()
|
||||
if err:
|
||||
return err
|
||||
return await asyncio.to_thread(_sync_journal_entry_append, entry_id, content, heading)
|
||||
|
||||
|
||||
def _sync_journal_entry_append(entry_id: str, content: str, heading: str) -> str:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
entry = _get_entry(entry_id)
|
||||
if isinstance(entry, str):
|
||||
return entry
|
||||
|
||||
existing = (entry.get("content") or "").rstrip()
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
section_heading = heading or ts
|
||||
new_content = f"{existing}\n\n### {section_heading}\n{content.strip()}"
|
||||
|
||||
result = _patch_entry(entry_id, {"content": new_content})
|
||||
if result != "ok":
|
||||
return result
|
||||
return f"Appended to journal entry `{entry_id}` under heading \"{section_heading}\"."
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: ae_journal_entry_prepend
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def journal_entry_prepend(entry_id: str, content: str, heading: str = "") -> str:
|
||||
"""Prepend a timestamped section to the top of a journal entry's content."""
|
||||
err = _check_config()
|
||||
if err:
|
||||
return err
|
||||
return await asyncio.to_thread(_sync_journal_entry_prepend, entry_id, content, heading)
|
||||
|
||||
|
||||
def _sync_journal_entry_prepend(entry_id: str, content: str, heading: str) -> str:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
entry = _get_entry(entry_id)
|
||||
if isinstance(entry, str):
|
||||
return entry
|
||||
|
||||
existing = (entry.get("content") or "").lstrip()
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
section_heading = heading or ts
|
||||
new_content = f"### {section_heading}\n{content.strip()}\n\n{existing}"
|
||||
|
||||
result = _patch_entry(entry_id, {"content": new_content})
|
||||
if result != "ok":
|
||||
return result
|
||||
return f"Prepended to journal entry `{entry_id}` under heading \"{section_heading}\"."
|
||||
|
||||
Reference in New Issue
Block a user