feat: session_search tool + tool expansion docs update
session_search (tools/files.py): - Full-text search across past session logs, exposed to the orchestrator - Params: query (required), limit (default 5, max 20) - Returns dated excerpts, newest first; own sessions only via ContextVars - User-level — no TOOL_ROLES gating needed - Registered in __init__.py callables + TOOL_CATEGORIES["Files"] ARCH__FUTURE.md §2: updated tool count to 44, marked prior tools complete, added Round 2 planned tools table (session_search now done, reminders due dates, http_post, nc_talk_history, task_list priority filter, http_fetch max_chars), noted datetime_now is not needed (already in system prompt via context_loader) TODO__Agents.md: session_search checked off, Round 2 task list added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,7 @@ 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, file_list as _file_list, file_write as _file_write
|
||||
from tools.files import file_read as _file_read, file_list as _file_list, file_write as _file_write, session_search as _session_search
|
||||
from tools.system import (
|
||||
shell_exec as _shell_exec,
|
||||
claude_allow_dir as _claude_allow_dir,
|
||||
@@ -89,7 +89,7 @@ import tools.agent_notes as _mod_agent_notes
|
||||
|
||||
TOOL_CATEGORIES: dict[str, list[str]] = {
|
||||
"Web": ["web_search", "http_fetch"],
|
||||
"Files": ["file_read", "file_list", "file_write"],
|
||||
"Files": ["file_read", "file_list", "file_write", "session_search"],
|
||||
"Shell": ["shell_exec", "claude_allow_dir"],
|
||||
"System": ["cortex_restart", "cortex_logs", "cortex_status", "cortex_update"],
|
||||
"Tasks": ["task_list", "task_create", "task_update", "task_complete"],
|
||||
@@ -126,6 +126,7 @@ _CALLABLES: dict[str, callable] = {
|
||||
"file_read": _file_read,
|
||||
"file_list": _file_list,
|
||||
"file_write": _file_write,
|
||||
"session_search": _session_search,
|
||||
"shell_exec": _shell_exec,
|
||||
"claude_allow_dir": _claude_allow_dir,
|
||||
"cortex_restart": _cortex_restart,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""
|
||||
File read tool — restricted to known-safe directory roots.
|
||||
File read/write/search tools — restricted to known-safe directory roots.
|
||||
|
||||
Lets the orchestrator read local files (documentation, notes, config references)
|
||||
without exposing arbitrary filesystem access. All paths are resolved and checked
|
||||
against an allowlist of roots before any read is performed.
|
||||
and search past session logs without exposing arbitrary filesystem access.
|
||||
All paths are resolved and checked against an allowlist of roots before any
|
||||
read or write is performed.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from google.genai import types
|
||||
@@ -225,6 +227,55 @@ def _sync_file_write(path: str, content: str, mode: str) -> str:
|
||||
return f"Write error: {e}"
|
||||
|
||||
|
||||
_SEARCH_EXCERPT_CHARS = 150
|
||||
|
||||
|
||||
async def session_search(query: str, limit: int = 5) -> str:
|
||||
"""Search past session logs for a keyword or phrase.
|
||||
|
||||
Returns up to `limit` matching excerpts with session dates, newest first.
|
||||
Only searches the current user's own sessions (per-persona isolation via ContextVars).
|
||||
"""
|
||||
return await asyncio.to_thread(_sync_session_search, query, limit)
|
||||
|
||||
|
||||
def _sync_session_search(query: str, limit: int) -> str:
|
||||
from persona import persona_path
|
||||
sessions_dir = persona_path() / "sessions"
|
||||
if not sessions_dir.exists():
|
||||
return "No session logs found."
|
||||
|
||||
limit = max(1, min(limit, 20))
|
||||
pattern = re.compile(re.escape(query), re.IGNORECASE)
|
||||
session_files = sorted(sessions_dir.glob("*.md"), reverse=True)
|
||||
|
||||
matches = []
|
||||
for sf in session_files:
|
||||
if len(matches) >= limit:
|
||||
break
|
||||
try:
|
||||
text = sf.read_text()
|
||||
except OSError:
|
||||
continue
|
||||
for m in pattern.finditer(text):
|
||||
if len(matches) >= limit:
|
||||
break
|
||||
start = max(0, m.start() - _SEARCH_EXCERPT_CHARS)
|
||||
end = min(len(text), m.end() + _SEARCH_EXCERPT_CHARS)
|
||||
excerpt = text[start:end].strip()
|
||||
if start > 0:
|
||||
excerpt = "…" + excerpt
|
||||
if end < len(text):
|
||||
excerpt = excerpt + "…"
|
||||
matches.append(f"[{sf.stem}] {excerpt}")
|
||||
|
||||
if not matches:
|
||||
return f"No matches for '{query}' across {len(session_files)} session logs."
|
||||
|
||||
header = f"Session search: '{query}' — {len(matches)} match(es) across {len(session_files)} logs\n"
|
||||
return header + "\n\n".join(matches)
|
||||
|
||||
|
||||
DECLARATIONS = [
|
||||
types.FunctionDeclaration(
|
||||
name="file_read",
|
||||
@@ -278,4 +329,22 @@ DECLARATIONS = [
|
||||
required=["path", "content"],
|
||||
),
|
||||
),
|
||||
types.FunctionDeclaration(
|
||||
name="session_search",
|
||||
description=(
|
||||
"Search past conversation session logs for a keyword or phrase. "
|
||||
"Use this to recall what was discussed in previous sessions — "
|
||||
"e.g. 'what did we decide about X?', 'when did we set up Y?'. "
|
||||
"Returns matching excerpts with session dates, newest first. "
|
||||
"Only searches this user's own sessions."
|
||||
),
|
||||
parameters=types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"query": types.Schema(type=types.Type.STRING, description="Keyword or phrase to search for"),
|
||||
"limit": types.Schema(type=types.Type.INTEGER, description="Max results to return (default 5, max 20)"),
|
||||
},
|
||||
required=["query"],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user