diff --git a/AGENTS.md b/AGENTS.md index 40f42c7e5..a7ad38578 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ This is the developer-facing documentation for the solstone codebase. If you're - **Journal**: Central data structure organized as `journal/YYYYMMDD/` directories. All captured data, transcripts, and analysis artifacts are stored here. - **Facets**: Project/context organization system that groups related content and provides scoped views of entities, tasks, and activities. - **Entities**: Extracted information tracked over time across transcripts and interactions and associated with facets for semantic navigation. -- **Agents**: AI processors with configurable prompts that analyze content, extract insights, and respond to queries. +- **Talents**: AI processors with configurable prompts that analyze content, extract insights, and respond to queries. - **Callosum**: Message bus that enables asynchronous communication between components. - **Indexer**: Builds and maintains a SQLite database from journal data, enabling fast search and retrieval. @@ -24,37 +24,37 @@ This is the developer-facing documentation for the solstone codebase. If you're **Component communication**: - Callosum enables async communication between services. -- Cortex orchestrates AI agent execution via `sol cortex`, spawning agent subprocesses with agent configurations. +- Cortex orchestrates AI talent execution via `sol cortex`, spawning talent subprocesses with talent configurations. - The unified CLI is `sol`. Run `sol` to see status and available commands. ## Quick Commands ```bash make install # Install package (includes all deps) -make skills # Discover and symlink Agent Skills from talent/ dirs +make skills # Discover and symlink Anthropic Skills from talent/ dirs make format # Auto-fix formatting, then report remaining issues make test # Run unit tests make ci # Full CI check (format check + lint + test) make dev # Start stack (Ctrl+C to stop) ``` -## Agent CLI Boundaries +## Talent CLI Boundaries -Cogitate agents have access to all `sol` commands. The following infrastructure commands must never be called by agents because they manage services and data pipelines that should only be operated by the supervisor or a human operator: +Cogitate talents have access to all `sol` commands. The following infrastructure commands must never be called by talents because they manage services and data pipelines that should only be operated by the supervisor or a human operator: - `sol supervisor` / `sol start` - `sol dream` except heartbeat's targeted `sol dream --segment` - `sol import` - `sol config` - `sol cortex` -- `sol agents` +- `sol providers check` - `sol callosum` - `sol observer` / `sol observe-*` - `sol sense` - `sol transcribe` / `sol describe` - `sol indexer --reset` -Agents should use `sol call` commands for journal interaction and `sol health` / `sol talent logs` for diagnostics. +Talents should use `sol call` commands for journal interaction and `sol health` / `sol talent logs` for diagnostics. ## Reference diff --git a/Makefile b/Makefile index 404b894d4..c9fa2b187 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # solstone Makefile # Python-based AI-driven desktop journaling toolkit -.PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format format-check ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sail upgrade sandbox sandbox-stop install-pinchtab verify-browser update-browser-baselines review verify-api update-api-baselines install-service uninstall-service service-logs +.PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format format-check ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sail upgrade sandbox sandbox-stop install-pinchtab verify-browser update-browser-baselines review verify-api update-api-baselines install-service uninstall-service service-logs gate-agents-rename # Default target - install package in editable mode all: install @@ -404,6 +404,9 @@ ci: .installed @echo "=== Running ruff ===" @$(RUFF) check . || { echo "Run 'make format' to auto-fix"; exit 1; } @echo "" + @echo "=== Running rename gate ===" + @$(MAKE) gate-agents-rename + @echo "" @echo "=== Running mypy ===" @$(MYPY) . || true @echo "" @@ -451,3 +454,6 @@ pre-commit: .installed @$(UV) pip show pre-commit >/dev/null 2>&1 || { echo "Installing pre-commit..."; $(UV) pip install pre-commit; } $(VENV_BIN)/pre-commit install @echo "Pre-commit hooks installed!" +# Rename guard for the agents -> talents transition +gate-agents-rename: .installed + $(VENV_BIN)/python scripts/gate_agents_rename.py diff --git a/apps/entities/routes.py b/apps/entities/routes.py index 05388a040..7abdf94ae 100644 --- a/apps/entities/routes.py +++ b/apps/entities/routes.py @@ -521,15 +521,15 @@ def generate_description(facet_name: str) -> Any: f"Current Description: {current_desc}" ) - agent_id = spawn_agent( + use_id = spawn_agent( prompt=prompt, name="entities:entity_describe", provider="google", ) - if agent_id is None: + if use_id is None: return jsonify({"error": "Failed to connect to agent service"}), 503 - return jsonify({"success": True, "agent_id": agent_id}) + return jsonify({"success": True, "use_id": use_id}) except Exception as e: return ( @@ -556,14 +556,14 @@ def assist_add(facet_name: str) -> Any: prompt = f"For the '{facet_name}' facet, this is the user's request to attach a new entity: {name}" # Create agent request - entity_assist agent already has provider configured - agent_id = spawn_agent( + use_id = spawn_agent( prompt=prompt, name="entities:entity_assist", ) - if agent_id is None: + if use_id is None: return jsonify({"error": "Failed to connect to agent service"}), 503 - return jsonify({"success": True, "agent_id": agent_id}) + return jsonify({"success": True, "use_id": use_id}) except Exception as e: return jsonify({"error": f"Failed to start entity assistant: {str(e)}"}), 500 diff --git a/apps/entities/talent/entity_observer.md b/apps/entities/talent/entity_observer.md index 6705bd4fa..4740ecc96 100644 --- a/apps/entities/talent/entity_observer.md +++ b/apps/entities/talent/entity_observer.md @@ -12,7 +12,7 @@ "output": "json", "thinking_budget": 2048, "hook": {"pre": "entities:entity_observer", "post": "entities:entity_observer"}, - "load": {"transcripts": false, "percepts": false, "agents": false} + "load": {"transcripts": false, "percepts": false, "talents": false} } ## Core Mission diff --git a/apps/entities/workspace.html b/apps/entities/workspace.html index 7c561a196..a8eadc23e 100644 --- a/apps/entities/workspace.html +++ b/apps/entities/workspace.html @@ -1480,8 +1480,8 @@ let entitiesData = null; let detectedPage = 1; let journalEntitiesData = null; // For all-facet mode let currentDetailEntity = null; -const pendingEntities = new Map(); // agent_id → { name, element } -const pendingAgentCallbacks = new Map(); // agent_id → callback function +const pendingEntities = new Map(); // use_id → { name, element } +const pendingAgentCallbacks = new Map(); // use_id → callback function const _errorTimers = {}; // Standard entity types - fetched from server @@ -2361,8 +2361,8 @@ function startDescriptionEdit(entity) { }) .then(response => response.json()) .then(data => { - if (data.success && data.agent_id) { - listenForAgentCompletion(data.agent_id, (result) => { + if (data.success && data.use_id) { + listenForAgentCompletion(data.use_id, (result) => { if (result.success && result.response) { saveDescription(entity, result.response); } else { @@ -3185,12 +3185,12 @@ function submitEntityAssist() { }) .then(response => response.json()) .then(data => { - if (data.success && data.agent_id) { + if (data.success && data.use_id) { const pending = pendingEntities.get(tempId); if (pending) { pendingEntities.delete(tempId); - pendingEntities.set(data.agent_id, pending); - pending.element.dataset.agentId = data.agent_id; + pendingEntities.set(data.use_id, pending); + pending.element.dataset.agentId = data.use_id; } } else { throw new Error(data.error || 'Failed to start assistant'); @@ -3210,7 +3210,7 @@ function setupCortexListener() { } window.appEvents.listen('cortex', (msg) => { - const agentId = msg.agent_id; + const agentId = msg.use_id; if (!agentId) return; // Pending entity additions diff --git a/apps/health/talent/health/SKILL.md b/apps/health/talent/health/SKILL.md index 74e2b5fc6..fa642ee3a 100644 --- a/apps/health/talent/health/SKILL.md +++ b/apps/health/talent/health/SKILL.md @@ -80,7 +80,7 @@ List recent agent runs. Flags compose with AND logic. For example, `--daily --errors` shows only daily runs that errored. -Output columns: agent_id, time, name, status, runtime, cost, events, tools, output_size, model, facet. +Output columns: use_id, time, name, status, runtime, cost, events, tools, output_size, model, facet. Examples: diff --git a/apps/health/tests/test_call.py b/apps/health/tests/test_call.py index 38c1f9c88..5c9f4c61b 100644 --- a/apps/health/tests/test_call.py +++ b/apps/health/tests/test_call.py @@ -63,8 +63,8 @@ def test_pipeline_with_real_fixture(health_env): "\n".join( [ json.dumps({"event": "run.start", "mode": "segment"}), - json.dumps({"event": "agent.dispatch", "mode": "segment"}), - json.dumps({"event": "agent.complete", "mode": "segment"}), + json.dumps({"event": "talent.dispatch", "mode": "segment"}), + json.dumps({"event": "talent.complete", "mode": "segment"}), json.dumps( {"event": "run.complete", "mode": "segment", "duration_ms": 42} ), diff --git a/apps/health/tests/test_routes.py b/apps/health/tests/test_routes.py index 87221fba7..ec2dd487c 100644 --- a/apps/health/tests/test_routes.py +++ b/apps/health/tests/test_routes.py @@ -29,7 +29,7 @@ class TestLogRoute: def test_path_outside_health_dir_rejected(self, health_env): env = health_env() - resp = env.client.get("/app/health/api/log?path=20260322/agents/something.log") + resp = env.client.get("/app/health/api/log?path=20260322/talents/something.log") assert resp.status_code == 400 def test_missing_file_returns_404(self, health_env): diff --git a/apps/health/workspace.html b/apps/health/workspace.html index 858f33f3f..3728f5826 100644 --- a/apps/health/workspace.html +++ b/apps/health/workspace.html @@ -2278,12 +2278,12 @@ const k = child.getAttribute('data-key'); if (k) existingByKey.set(k, child); } - const newKeys = new Set(activeAgents.map(a => a.agent_id)); + const newKeys = new Set(activeAgents.map(a => a.use_id)); for (const [k, child] of existingByKey) { if (!newKeys.has(k)) container.removeChild(child); } for (const agent of activeAgents) { - const key = agent.agent_id; + const key = agent.use_id; let card = existingByKey.get(key); if (!card) { card = document.createElement('div'); @@ -2309,7 +2309,7 @@ const stateLabel = agent.event === 'thinking' ? 'Thinking...' : (agent.event === 'tool_start' || agent.event === 'tool_end') ? 'Working...' : 'Running...'; const elapsed = agent.elapsed_seconds ? formatElapsed(agent.elapsed_seconds) : '0s'; - card.children[0].textContent = '...' + getAgentId(agent.agent_id); + card.children[0].textContent = '...' + getAgentId(agent.use_id); card.children[1].textContent = agent.name || 'default'; card.children[2].textContent = stateLabel; card.children[3].textContent = elapsed; @@ -2696,27 +2696,27 @@ } function handleCortexEvent(msg) { - // Handle status event first (no agent_id at top level) + // Handle status event first (no use_id at top level) if (msg.event === 'status') { // Update agent count for vitals - state.agentCount = msg.running_agents || 0; + state.agentCount = msg.running_uses || 0; - // Status event contains array of agents - if (msg.agents) { - // Clear agents not in status (they finished) - const activeIds = new Set(msg.agents.map(a => a.agent_id)); + // Status event contains array of uses + if (msg.uses) { + // Clear uses not in status (they finished) + const activeIds = new Set(msg.uses.map(a => a.use_id)); state.agents.forEach((_, id) => { if (!activeIds.has(id)) { state.agents.delete(id); } }); - // Update/add agents from status - msg.agents.forEach(agent => { - const existing = state.agents.get(agent.agent_id) || {}; - state.agents.set(agent.agent_id, { + // Update/add uses from status + msg.uses.forEach(agent => { + const existing = state.agents.get(agent.use_id) || {}; + state.agents.set(agent.use_id, { ...existing, - agent_id: agent.agent_id, + use_id: agent.use_id, name: agent.name, provider: agent.provider, elapsed_seconds: agent.elapsed_seconds, @@ -2730,8 +2730,8 @@ return; } - // Individual agent events require agent_id - const agentId = msg.agent_id; + // Individual agent events require use_id + const agentId = msg.use_id; if (!agentId) return; // Track start time for client-side elapsed updates @@ -2740,7 +2740,7 @@ state.agents.set(agentId, { ...existing, - agent_id: agentId, + use_id: agentId, name: msg.name || existing.name, provider: msg.provider || existing.provider, event: msg.event, diff --git a/apps/home/events.py b/apps/home/events.py index c47550b31..29769590a 100644 --- a/apps/home/events.py +++ b/apps/home/events.py @@ -11,7 +11,7 @@ import logging from apps.events import EventContext, on_event from think.conversation import record_exchange -from think.cortex_client import read_agent_events +from think.cortex_client import read_use_events logger = logging.getLogger(__name__) @@ -25,12 +25,12 @@ def record_triage_exchange(ctx: EventContext) -> None: if name not in TRIAGE_AGENT_NAMES: return - agent_id = ctx.msg.get("agent_id") - if not agent_id: + use_id = ctx.msg.get("use_id") + if not use_id: return try: - events = read_agent_events(agent_id) + events = read_use_events(use_id) facet = "" app = "" path = "" @@ -51,11 +51,11 @@ def record_triage_exchange(ctx: EventContext) -> None: user_message=user_message, agent_response=result, talent=name, - agent_id=agent_id, + use_id=use_id, ) except Exception: logger.debug( "Failed to record conversation exchange for agent %s", - agent_id, + use_id, exc_info=True, ) diff --git a/apps/home/routes.py b/apps/home/routes.py index e85f72317..14c0f4435 100644 --- a/apps/home/routes.py +++ b/apps/home/routes.py @@ -97,7 +97,7 @@ def _load_flow_md(today: str) -> tuple[str | None, float | None]: """Load today's flow.md content and mtime. Returns (content, mtime) or (None, None).""" try: journal = Path(get_journal()) - flow_path = journal / today / "agents" / "flow.md" + flow_path = journal / today / "talents" / "flow.md" if flow_path.exists(): return flow_path.read_text(), flow_path.stat().st_mtime except Exception: @@ -508,7 +508,7 @@ def _top_heatmap_hours(stats_data: dict[str, Any]) -> list[int]: def _knowledge_graph_freshness(yesterday: str) -> dict[str, Any]: path = ( - Path(get_journal()) / "chronicle" / yesterday / "agents" / "knowledge_graph.md" + Path(get_journal()) / "chronicle" / yesterday / "talents" / "knowledge_graph.md" ) if not path.exists(): return {"exists": False, "fresh": False, "updated_label": None} @@ -584,8 +584,9 @@ def _newsletter_attempts_from_dream_logs(yesterday: str) -> tuple[int, int]: record = json.loads(line) except json.JSONDecodeError: continue + # HISTORICAL SHIM: accept legacy agent.* chronicle event names from before 2026-04-17; sunset 2026-05-01 if ( - record.get("event") == "agent.fail" + record.get("event") in {"agent.fail", "talent.fail"} and record.get("facet") and record.get("name") == "facet_newsletter" ): @@ -765,7 +766,7 @@ def _format_gap_bullets( has_activity = any( anomaly.get("kind") == "activity_agents_missing" for anomaly in anomalies ) - has_failure = any(anomaly.get("kind") == "agent_failure" for anomaly in anomalies) + has_failure = any(anomaly.get("kind") == "talent_failure" for anomaly in anomalies) if has_daily: bullets.append("I didn't finish the full overnight review.") diff --git a/apps/search/routes.py b/apps/search/routes.py index f35b98ef9..2e295a42e 100644 --- a/apps/search/routes.py +++ b/apps/search/routes.py @@ -138,7 +138,7 @@ def search_journal_api() -> Any: - total: Total match count - days: List of day groups, each with date info and results - facets: List of facets with counts for filter sidebar - - agents: List of agents with counts for filter sidebar + - talents: List of talents with counts for filter sidebar """ query = request.args.get("q", "").strip() @@ -233,7 +233,7 @@ def search_journal_api() -> Any: "showing_days": len(days_response), "days": days_response, "facets": facets_list, - "agents": agents_list, + "talents": agents_list, } ) diff --git a/apps/sol/maint/004_rename_agents_to_talents.py b/apps/sol/maint/004_rename_agents_to_talents.py new file mode 100644 index 000000000..ccff9d855 --- /dev/null +++ b/apps/sol/maint/004_rename_agents_to_talents.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Rename live journal agents paths to talents.""" + +from __future__ import annotations + +import argparse +import sys +from dataclasses import dataclass +from pathlib import Path + +from think.utils import day_dirs, get_journal, iter_segments, setup_cli + + +@dataclass +class RenameSummary: + discovered: int = 0 + moved: int = 0 + skipped: int = 0 + errors: int = 0 + collisions: int = 0 + + +def discover_moves(journal_path: Path) -> tuple[list[tuple[Path, Path]], list[Path]]: + """Return planned (src, dst) moves and already-migrated destinations.""" + planned: list[tuple[Path, Path]] = [] + skipped: list[Path] = [] + + def add_pair(src: Path, dst: Path) -> None: + if src.exists(): + planned.append((src, dst)) + elif dst.exists(): + skipped.append(dst) + + add_pair(journal_path / "agents", journal_path / "talents") + add_pair( + journal_path / "health" / "agents.json", + journal_path / "health" / "talents.json", + ) + + for day_name, day_abs in sorted(day_dirs().items()): + day_dir = Path(day_abs) + if not day_dir.is_dir(): + continue + + add_pair(day_dir / "agents", day_dir / "talents") + + for _stream, _segment, seg_path in iter_segments(day_name): + add_pair(seg_path / "agents", seg_path / "talents") + + return planned, skipped + + +def run_migration( + journal_path: Path, *, dry_run: bool +) -> tuple[RenameSummary, list[tuple[Path, Path]]]: + """Run or preview the agents->talents path rename.""" + summary = RenameSummary() + planned, skipped = discover_moves(journal_path) + summary.discovered = len(planned) + summary.skipped = len(skipped) + + collisions = [(src, dst) for src, dst in planned if dst.exists()] + summary.collisions = len(collisions) + if collisions: + return summary, collisions + + for src, dst in planned: + print(f"{'[DRY-RUN] ' if dry_run else ''}move {src} -> {dst}") + if dry_run: + continue + + try: + dst.parent.mkdir(parents=True, exist_ok=True) + src.rename(dst) + summary.moved += 1 + except Exception as exc: + summary.errors += 1 + print(f"[ERROR] move failed: {src} -> {dst}: {exc}") + + if dry_run: + summary.moved = len(planned) + + return summary, [] + + +def _print_summary(summary: RenameSummary) -> None: + print("Summary") + print(f" discovered: {summary.discovered}") + print(f" moved: {summary.moved}") + print(f" skipped: {summary.skipped}") + print(f" errors: {summary.errors}") + print(f" collisions: {summary.collisions}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Rename live journal agents paths to talents." + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Preview planned renames without writing files.", + ) + args = setup_cli(parser) + + journal_path = Path(get_journal()) + summary, collisions = run_migration(journal_path, dry_run=args.dry_run) + + if collisions: + print("Collision(s) detected; no files were moved:") + for src, dst in collisions: + print(f" {src} -> {dst}") + _print_summary(summary) + sys.exit(2) + + _print_summary(summary) + if summary.errors: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/apps/sol/routes.py b/apps/sol/routes.py index 8ab8b7922..d2cbdd3f6 100644 --- a/apps/sol/routes.py +++ b/apps/sol/routes.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Agents app - browse historical agent runs by day and facet.""" +"""Talents app - browse historical talent uses by day and facet.""" from __future__ import annotations @@ -73,19 +73,19 @@ def _get_facet_filter() -> str | None: return facet -def _agent_id_to_day(agent_id: str) -> str: - """Convert agent_id (millisecond timestamp) to YYYYMMDD day string.""" +def _use_id_to_day(use_id: str) -> str: + """Convert use_id (millisecond timestamp) to YYYYMMDD day string.""" try: - ts = int(agent_id) / 1000 + ts = int(use_id) / 1000 return datetime.fromtimestamp(ts).strftime("%Y%m%d") except (ValueError, OSError): return "" -def _parse_agent_events( +def _parse_use_events( lines: list[str], *, collect_events: bool = False ) -> dict[str, Any]: - """Parse agent event lines and extract counts and cost data. + """Parse use event lines and extract counts and cost data. Args: lines: List of JSONL lines @@ -142,18 +142,18 @@ def _parse_agent_events( return result -def _parse_agent_file(agent_file: Path) -> dict[str, Any] | None: - """Parse agent JSONL file and extract metadata. +def _parse_use_file(use_file: Path) -> dict[str, Any] | None: + """Parse a use JSONL file and extract metadata. Returns dict with: id, name, start, status, prompt, facet, failed, runtime_seconds, thinking_count, tool_count, cost, model, provider, error_message. Returns None if file cannot be parsed. """ - from think.cortex_client import get_agent_end_state + from think.cortex_client import get_use_end_state try: - with open(agent_file, "r") as f: + with open(use_file, "r") as f: lines = f.readlines() if not lines: @@ -167,15 +167,14 @@ def _parse_agent_file(agent_file: Path) -> dict[str, Any] | None: if request_event.get("event") != "request": return None - # Extract agent ID from filename - is_active = "_active.jsonl" in agent_file.name - agent_id = agent_file.stem.replace("_active", "") + is_active = "_active.jsonl" in use_file.name + use_id = use_file.stem.replace("_active", "") # Parse events using shared helper - event_data = _parse_agent_events(lines[1:]) + event_data = _parse_use_events(lines[1:]) - agent_info: dict[str, Any] = { - "id": agent_id, + use_info: dict[str, Any] = { + "id": use_id, "name": request_event.get("name", "unified"), "start": request_event.get("ts", 0), "status": "running" if is_active else "completed", @@ -203,38 +202,36 @@ def _parse_agent_file(agent_file: Path) -> dict[str, Any] | None: output_file = str(out_path.relative_to(day_dir)) else: output_file = str(out_path.relative_to(state.journal_root)) - agent_info["output_file"] = output_file + use_info["output_file"] = output_file - # For completed agents, determine end state and calculate cost + # For completed uses, determine end state and calculate cost if not is_active: - end_state = get_agent_end_state(agent_id) - agent_info["failed"] = end_state in ("error", "unknown") + end_state = get_use_end_state(use_id) + use_info["failed"] = end_state in ("error", "unknown") # Calculate runtime from finish or error timestamp end_ts = event_data["finish_ts"] or event_data["error_ts"] - if end_ts and agent_info["start"]: - agent_info["runtime_seconds"] = (end_ts - agent_info["start"]) / 1000.0 + if end_ts and use_info["start"]: + use_info["runtime_seconds"] = (end_ts - use_info["start"]) / 1000.0 # Calculate cost - agent_info["cost"] = calc_agent_cost( - event_data["model"], event_data["usage"] - ) + use_info["cost"] = calc_agent_cost(event_data["model"], event_data["usage"]) - return agent_info + return use_info except (json.JSONDecodeError, IOError): return None -def _get_agent_day(agent_file: Path) -> str: - """Get the logical day for an agent from its request event. +def _get_use_day(use_file: Path) -> str: + """Get the logical day for a use from its request event. Prefers the ``day`` field from the request event (the day being processed) - over the agent_id timestamp (when the agent actually ran). This ensures - overnight dream agents appear under the day they processed. + over the use_id timestamp (when the agent actually ran). This ensures + overnight dream uses appear under the day they processed. """ - agent_id = agent_file.stem.replace("_active", "") + use_id = use_file.stem.replace("_active", "") try: - with open(agent_file, "r") as f: + with open(use_file, "r") as f: first_line = f.readline().strip() if first_line: request_event = json.loads(first_line) @@ -243,29 +240,29 @@ def _get_agent_day(agent_file: Path) -> str: return req_day except (json.JSONDecodeError, IOError): pass - return _agent_id_to_day(agent_id) + return _use_id_to_day(use_id) -def _get_agents_for_day(day: str, facet_filter: str | None = None) -> list[dict]: - """Get all agent runs for a specific day. +def _get_uses_for_day(day: str, facet_filter: str | None = None) -> list[dict]: + """Get all talent uses for a specific day. - Uses the day index file for fast lookup instead of scanning all agent files. + Uses the day index file for fast lookup instead of scanning all use files. Args: day: YYYYMMDD day string facet_filter: Optional facet to filter by (None = all facets) Returns: - List of agent info dicts sorted by start time (newest first) + List of use info dicts sorted by start time (newest first) """ - agents_dir = Path(state.journal_root) / "agents" - if not agents_dir.exists(): + talents_dir = Path(state.journal_root) / "talents" + if not talents_dir.exists(): return [] - agents = [] + uses = [] - # Read day index for completed agents - day_index_path = agents_dir / f"{day}.jsonl" + # Read day index for completed uses + day_index_path = talents_dir / f"{day}.jsonl" if day_index_path.exists(): try: with open(day_index_path, "r") as f: @@ -283,53 +280,53 @@ def _get_agents_for_day(day: str, facet_filter: str | None = None) -> list[dict] continue # Locate the actual file for full parsing - agent_id = entry.get("agent_id", "") + use_id = entry.get("use_id", "") name = entry.get("name", "unified") safe_name = name.replace(":", "--") - agent_file = agents_dir / safe_name / f"{agent_id}.jsonl" - if not agent_file.exists(): + use_file = talents_dir / safe_name / f"{use_id}.jsonl" + if not use_file.exists(): continue - agent_info = _parse_agent_file(agent_file) - if agent_info: - agents.append(agent_info) + use_info = _parse_use_file(use_file) + if use_info: + uses.append(use_info) except IOError: pass - # Also check for running agents (only have _active files, no day index entry yet) - for agent_file in agents_dir.glob("*/*_active.jsonl"): - if "_pending" in agent_file.name: + # Also check for running uses (only have _active files, no day index entry yet) + for use_file in talents_dir.glob("*/*_active.jsonl"): + if "_pending" in use_file.name: continue - if _get_agent_day(agent_file) != day: + if _get_use_day(use_file) != day: continue - agent_info = _parse_agent_file(agent_file) - if not agent_info: + use_info = _parse_use_file(use_file) + if not use_info: continue - if facet_filter is not None and agent_info.get("facet") != facet_filter: + if facet_filter is not None and use_info.get("facet") != facet_filter: continue - agents.append(agent_info) + uses.append(use_info) # Sort by start time (newest first) - agents.sort(key=lambda x: x["start"], reverse=True) - return agents + uses.sort(key=lambda x: x["start"], reverse=True) + return uses @lru_cache(maxsize=1) -def _build_agents_meta() -> dict[str, dict[str, Any]]: - """Build agent metadata dict from all talent configs. +def _build_talents_meta() -> dict[str, dict[str, Any]]: + """Build talent metadata dict from all talent configs. - Returns dict mapping agent name to metadata with capability fields + Returns dict mapping talent name to metadata with capability fields for frontend display. Cached for process lifetime since talent configs are static. """ configs = get_talent_configs(include_disabled=True) - agents: dict[str, dict[str, Any]] = {} + talents: dict[str, dict[str, Any]] = {} for name, config in configs.items(): - agents[name] = { + talents[name] = { "title": config.get("title", name), "description": config.get("description"), "color": config.get("color", "#6c757d"), @@ -341,7 +338,7 @@ def _build_agents_meta() -> dict[str, dict[str, Any]]: "multi_facet": bool(config.get("multi_facet")), } - return agents + return talents # ============================================================================= @@ -351,14 +348,14 @@ def _build_agents_meta() -> dict[str, dict[str, Any]]: @sol_bp.route("/") def index() -> Any: - """Redirect to today's agent history.""" + """Redirect to today's talent history.""" today = date.today().strftime("%Y%m%d") - return redirect(url_for("app:sol.agents_day", day=today)) + return redirect(url_for("app:sol.talents_day", day=today)) @sol_bp.route("/") -def agents_day(day: str) -> str: - """Render agent history viewer for a specific day.""" +def talents_day(day: str) -> str: + """Render talent history viewer for a specific day.""" if not DATE_RE.fullmatch(day): return "", 404 @@ -372,9 +369,9 @@ def agents_day(day: str) -> str: # ============================================================================= -@sol_bp.route("/api/agents/") -def api_agents_day(day: str) -> Any: - """Get agent runs and metadata for a specific day. +@sol_bp.route("/api/talents/") +def api_talents_day(day: str) -> Any: + """Get talent uses and metadata for a specific day. Returns flat data for frontend grouping/rendering. @@ -383,8 +380,8 @@ def api_agents_day(day: str) -> Any: Returns: { - "runs": [run objects...], - "agents": {name: metadata...}, + "uses": [use objects...], + "talents": {name: metadata...}, "facets": {name: {title, color}...} } """ @@ -393,8 +390,8 @@ def api_agents_day(day: str) -> Any: facet_filter = _get_facet_filter() - runs = _get_agents_for_day(day, facet_filter) - agents = _build_agents_meta() + uses = _get_uses_for_day(day, facet_filter) + talents = _build_talents_meta() facets = { name: {"title": f.get("title", name), "color": f.get("color")} for name, f in get_facets().items() @@ -402,49 +399,48 @@ def api_agents_day(day: str) -> Any: return jsonify( { - "runs": runs, - "agents": agents, + "uses": uses, + "talents": talents, "facets": facets, } ) -@sol_bp.route("/api/run/") -def api_agent_run(agent_id: str) -> Any: - """Return full agent run detail with metadata and parsed events.""" - # Locate the agent JSONL file +@sol_bp.route("/api/run/") +def api_agent_run(use_id: str) -> Any: + """Return full talent-use detail with metadata and parsed events.""" + # Locate the use JSONL file journal_path = Path(state.journal_root) - agents_dir = journal_path / "agents" - # Search subdirectories for the agent file - agent_file = None - for match in agents_dir.glob(f"*/{agent_id}.jsonl"): - agent_file = match + talents_dir = journal_path / "talents" + # Search subdirectories for the use file + use_file = None + for match in talents_dir.glob(f"*/{use_id}.jsonl"): + use_file = match break - if not agent_file: - # Check if the agent is still running - for match in agents_dir.glob(f"*/{agent_id}_active.jsonl"): - return jsonify({"error": "Agent run is still in progress"}), 202 - return jsonify({"error": f"Agent run {agent_id} not found"}), 404 + if not use_file: + for match in talents_dir.glob(f"*/{use_id}_active.jsonl"): + return jsonify({"error": "Talent run is still in progress"}), 202 + return jsonify({"error": f"Talent run {use_id} not found"}), 404 try: - from think.cortex_client import get_agent_end_state + from think.cortex_client import get_use_end_state - with open(agent_file, "r", encoding="utf-8") as f: + with open(use_file, "r", encoding="utf-8") as f: lines = f.readlines() if not lines: - return jsonify({"error": f"Agent run {agent_id} is malformed"}), 500 + return jsonify({"error": f"Talent run {use_id} is malformed"}), 500 first_line = lines[0].strip() if not first_line: - return jsonify({"error": f"Agent run {agent_id} is malformed"}), 500 + return jsonify({"error": f"Talent run {use_id} is malformed"}), 500 request_event = json.loads(first_line) if request_event.get("event") != "request": - return jsonify({"error": f"Agent run {agent_id} is malformed"}), 500 + return jsonify({"error": f"Talent run {use_id} is malformed"}), 500 - event_data = _parse_agent_events(lines[1:], collect_events=True) + event_data = _parse_use_events(lines[1:], collect_events=True) output_file = None req_output = request_event.get("output") @@ -464,10 +460,10 @@ def api_agent_run(agent_id: str) -> Any: if end_ts and start_ts: runtime_seconds = (end_ts - start_ts) / 1000.0 - end_state = get_agent_end_state(agent_id) + end_state = get_use_end_state(use_id) run: dict[str, Any] = { - "id": agent_id, + "id": use_id, "name": request_event.get("name", "unified"), "start": start_ts, "status": "completed", @@ -484,7 +480,7 @@ def api_agent_run(agent_id: str) -> Any: "output_file": output_file, "events": event_data.get("events", []), } - run["day"] = request_event.get("day") or _agent_id_to_day(agent_id) + run["day"] = request_event.get("day") or _use_id_to_day(use_id) return jsonify(run) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -498,7 +494,7 @@ def api_output_file(day: str, filename: str) -> Any: Path is validated to stay within the journal directory. Supports two path styles: - - Day-relative: ``agents/flow.md`` → resolved under ``{day}/`` + - Day-relative: ``talents/flow.md`` → resolved under ``{day}/`` - Journal-relative: ``facets/work/activities/...`` → resolved under journal root """ if not DATE_RE.fullmatch(day): @@ -550,9 +546,9 @@ def api_preview_prompt(name: str) -> Any: } """ try: - from think.talent import get_agent + from think.talent import get_talent - config = get_agent(name) + config = get_talent(name) system_instruction = config.get("system_instruction", "") extra_context = config.get("extra_context", "") @@ -576,14 +572,14 @@ def api_preview_prompt(name: str) -> Any: } ) except FileNotFoundError: - return jsonify({"error": f"Agent '{name}' not found"}), 404 + return jsonify({"error": f"Talent '{name}' not found"}), 404 except Exception as e: return jsonify({"error": str(e)}), 500 @sol_bp.route("/api/stats/") def api_stats(month: str) -> Any: - """Return agent run counts per day per facet for a month. + """Return talent-use counts per day per facet for a month. Args: month: YYYYMM format month string @@ -595,14 +591,14 @@ def api_stats(month: str) -> Any: if not re.fullmatch(r"\d{6}", month): return jsonify({"error": "Invalid month format, expected YYYYMM"}), 400 - agents_dir = Path(state.journal_root) / "agents" - if not agents_dir.exists(): + talents_dir = Path(state.journal_root) / "talents" + if not talents_dir.exists(): return jsonify({}) stats: dict[str, dict[str, int]] = {} # Read day index files for the month - for day_index_file in agents_dir.glob(f"{month}*.jsonl"): + for day_index_file in talents_dir.glob(f"{month}*.jsonl"): day = day_index_file.stem if not re.fullmatch(r"\d{8}", day): continue @@ -630,10 +626,10 @@ def api_stats(month: str) -> Any: @sol_bp.route("/api/badge-count") def api_badge_count() -> Any: - """Get count of failed agent runs for today (for app icon badge).""" + """Get count of failed talent runs for today (for app icon badge).""" today = date.today().strftime("%Y%m%d") - agents = _get_agents_for_day(today, facet_filter=None) - failed_count = sum(1 for a in agents if a.get("failed")) + uses = _get_uses_for_day(today, facet_filter=None) + failed_count = sum(1 for a in uses if a.get("failed")) return jsonify({"count": failed_count}) @@ -649,7 +645,7 @@ def api_updated_days() -> Any: @sol_bp.route("/api/identity") def api_identity() -> Any: - """Return agent identity and thickness signals.""" + """Return talent identity and thickness signals.""" try: from think.awareness import compute_thickness from think.utils import get_config diff --git a/apps/sol/workspace.html b/apps/sol/workspace.html index 0dc2d39d8..862e56a86 100644 --- a/apps/sol/workspace.html +++ b/apps/sol/workspace.html @@ -766,7 +766,7 @@ padding: 0; } -/* Flow events (agent_updated, continue) */ +/* Flow events (talent_updated, continue) */ .event-flow { border-left-color: #f9a825; background: #fffde7; @@ -1387,7 +1387,7 @@ .catch(() => { banner.style.display = 'none'; }); } - async function loadAgents() { + async function loadTalents() { currentDay = getDayFromUrl(); if (!currentDay) return; @@ -1396,29 +1396,29 @@ document.getElementById('agents-status').textContent = ''; try { - const response = await fetch(`api/agents/${currentDay}`); + const response = await fetch(`api/talents/${currentDay}`); const data = await response.json(); - allRuns = data.runs || []; - agentsMeta = data.agents || {}; + allRuns = data.uses || []; + agentsMeta = data.talents || {}; facetsMeta = data.facets || {}; renderGridView(); document.getElementById('agents-status').textContent = - allRuns.length + ' agent run' + (allRuns.length !== 1 ? 's' : '') + ' loaded'; + allRuns.length + ' talent run' + (allRuns.length !== 1 ? 's' : '') + ' loaded'; // Restore hash-based view after data load if (location.hash) { handleHashChange(); } } catch (error) { - console.error('Error loading agents:', error); + console.error('Error loading talents:', error); document.getElementById('loading-view').innerHTML = '
' + '
⚠️
' + - '
Unable to load agents
' + + '
Unable to load talents
' + '
The server may be temporarily unavailable. Check your connection and try again.
' + - '' + + '' + '
'; } } @@ -1533,7 +1533,7 @@ const badges = document.createElement('div'); badges.className = 'agent-card-badges'; - const successCount = agent.run_count - agent.failed_count; + const successCount = agent.run_count - talent.failed_count; if (successCount > 0) { const successBadge = document.createElement('span'); successBadge.className = 'badge badge-success'; @@ -1541,10 +1541,10 @@ badges.appendChild(successBadge); } - if (agent.failed_count > 0) { + if (talent.failed_count > 0) { const failBadge = document.createElement('span'); failBadge.className = 'badge badge-failed'; - failBadge.textContent = '✗ ' + agent.failed_count; + failBadge.textContent = '✗ ' + talent.failed_count; badges.appendChild(failBadge); } @@ -2204,10 +2204,10 @@ timeline.appendChild(renderInfoEvent(event)); } else if (type === 'finish') { timeline.appendChild(renderFinishEvent(event)); - } else if (type === 'agent_updated') { - timeline.appendChild(renderFlowEvent('Switched to agent: ' + (event.agent || 'unknown'), event.ts)); + } else if (type === 'talent_updated') { + timeline.appendChild(renderFlowEvent('Switched to talent: ' + (event.talent || 'unknown'), event.ts)); } else if (type === 'continue') { - timeline.appendChild(renderFlowEvent('Continued in agent: ' + (event.to || 'unknown'), event.ts)); + timeline.appendChild(renderFlowEvent('Continued in talent: ' + (event.to || 'unknown'), event.ts)); } } @@ -2527,12 +2527,12 @@ // Listen for facet changes window.addEventListener('facet.switch', () => { loadUpdatedBanner(); - loadAgents(); + loadTalents(); }); // Initial load loadIdentity(); loadUpdatedBanner(); - loadAgents(); + loadTalents(); })(); diff --git a/apps/speakers/attribution.py b/apps/speakers/attribution.py index c538c2ca8..446da130e 100644 --- a/apps/speakers/attribution.py +++ b/apps/speakers/attribution.py @@ -124,7 +124,7 @@ def _extract_screen_participants(seg_dir: Path) -> list[str]: screen.md captures video-call participant panels. The content is free-form markdown so extraction is best-effort. """ - screen_path = seg_dir / "agents" / "screen.md" + screen_path = seg_dir / "talents" / "screen.md" if not screen_path.exists(): return [] try: @@ -159,7 +159,7 @@ def _extract_screen_participants(seg_dir: Path) -> list[str]: def _extract_meeting_participants(day: str, segment_key: str) -> list[str]: """Extract participant names from daily meetings.md.""" - meetings_path = day_path(day) / "agents" / "meetings.md" + meetings_path = day_path(day) / "talents" / "meetings.md" if not meetings_path.exists(): return [] try: @@ -468,7 +468,7 @@ def save_speaker_labels( sentence that was corrected by the user keeps the corrected attribution rather than being overwritten by a fresh pipeline run. """ - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) # Load existing corrections to preserve user overrides @@ -653,7 +653,7 @@ def _has_audio_embeddings(seg_dir: Path) -> bool: def _has_speaker_labels(seg_dir: Path) -> bool: """Check if the segment already has speaker_labels.json.""" - return (seg_dir / "agents" / "speaker_labels.json").exists() + return (seg_dir / "talents" / "speaker_labels.json").exists() def backfill_segments( diff --git a/apps/speakers/bootstrap.py b/apps/speakers/bootstrap.py index a9743ddb7..ed94c8c03 100644 --- a/apps/speakers/bootstrap.py +++ b/apps/speakers/bootstrap.py @@ -555,7 +555,7 @@ def merge_names(alias_name: str, canonical_name: str) -> dict[str, Any]: for day in sorted(day_dirs().keys()): for _stream, _seg_key, seg_path in iter_segments(day): segments_scanned += 1 - agents_dir = seg_path / "agents" + agents_dir = seg_path / "talents" # Rewrite speaker_labels.json labels_path = agents_dir / "speaker_labels.json" diff --git a/apps/speakers/routes.py b/apps/speakers/routes.py index c6d0221bd..91d3cbae6 100644 --- a/apps/speakers/routes.py +++ b/apps/speakers/routes.py @@ -118,7 +118,7 @@ def _load_segment_speakers(segment_dir: Path) -> list[str]: Returns: List of speaker name strings, or empty list if not found/invalid. """ - speakers_path = segment_dir / "agents" / "speakers.json" + speakers_path = segment_dir / "talents" / "speakers.json" if not speakers_path.exists(): return [] @@ -303,11 +303,11 @@ def _remove_voiceprint( def _load_speaker_labels(segment_dir: Path) -> dict | None: - """Load speaker_labels.json from a segment's agents/ directory. + """Load speaker_labels.json from a segment's talents/ directory. Returns the parsed JSON dict, or None if not found/invalid. """ - labels_path = segment_dir / "agents" / "speaker_labels.json" + labels_path = segment_dir / "talents" / "speaker_labels.json" if not labels_path.is_file(): return None try: @@ -318,10 +318,10 @@ def _load_speaker_labels(segment_dir: Path) -> dict | None: def _save_speaker_labels(segment_dir: Path, labels_data: dict) -> None: - """Atomically write speaker_labels.json to a segment's agents/ directory.""" - agents_dir = segment_dir / "agents" - agents_dir.mkdir(parents=True, exist_ok=True) - out_path = agents_dir / "speaker_labels.json" + """Atomically write speaker_labels.json to a segment's talents/ directory.""" + talents_dir = segment_dir / "talents" + talents_dir.mkdir(parents=True, exist_ok=True) + out_path = talents_dir / "speaker_labels.json" tmp_path = out_path.with_suffix(".tmp") with open(tmp_path, "w", encoding="utf-8") as f: json.dump(labels_data, f, indent=2) @@ -329,11 +329,11 @@ def _save_speaker_labels(segment_dir: Path, labels_data: dict) -> None: def _load_speaker_corrections(segment_dir: Path) -> list[dict]: - """Load speaker_corrections.json from a segment's agents/ directory. + """Load speaker_corrections.json from a segment's talents/ directory. Returns list of correction entries, or empty list if not found. """ - corr_path = segment_dir / "agents" / "speaker_corrections.json" + corr_path = segment_dir / "talents" / "speaker_corrections.json" if not corr_path.is_file(): return [] try: @@ -348,9 +348,9 @@ def _append_speaker_correction(segment_dir: Path, correction: dict) -> None: """Append a correction entry to speaker_corrections.json (atomic write).""" corrections = _load_speaker_corrections(segment_dir) corrections.append(correction) - agents_dir = segment_dir / "agents" - agents_dir.mkdir(parents=True, exist_ok=True) - out_path = agents_dir / "speaker_corrections.json" + talents_dir = segment_dir / "talents" + talents_dir.mkdir(parents=True, exist_ok=True) + out_path = talents_dir / "speaker_corrections.json" tmp_path = out_path.with_suffix(".tmp") with open(tmp_path, "w", encoding="utf-8") as f: json.dump({"corrections": corrections}, f, indent=2) diff --git a/apps/speakers/status.py b/apps/speakers/status.py index 4d02c8901..95047ad46 100644 --- a/apps/speakers/status.py +++ b/apps/speakers/status.py @@ -179,7 +179,7 @@ def _attribution_section() -> dict[str, Any]: for seg_dir in sorted(stream_dir.iterdir()): if not seg_dir.is_dir(): continue - labels_file = seg_dir / "agents" / "speaker_labels.json" + labels_file = seg_dir / "talents" / "speaker_labels.json" if not labels_file.exists(): continue try: diff --git a/apps/speakers/suggest.py b/apps/speakers/suggest.py index 3a519019a..2ccd552c7 100644 --- a/apps/speakers/suggest.py +++ b/apps/speakers/suggest.py @@ -54,7 +54,7 @@ def _name_matches_entity(participant: str, names: set[str]) -> bool: def _parse_meetings(day_path: str) -> list[dict[str, Any]]: - meetings_path = Path(day_path) / "agents" / "meetings.md" + meetings_path = Path(day_path) / "talents" / "meetings.md" if not meetings_path.exists(): return [] @@ -296,7 +296,7 @@ def _low_confidence_review() -> list[dict[str, Any]]: for day in sorted(day_dirs().keys()): for stream, segment_key, seg_path in iter_segments(day): - labels_path = seg_path / "agents" / "speaker_labels.json" + labels_path = seg_path / "talents" / "speaker_labels.json" if not labels_path.exists(): continue try: @@ -323,7 +323,7 @@ def _low_confidence_review() -> list[dict[str, Any]]: if medium_or_null <= 10: continue - speakers_path = seg_path / "agents" / "speakers.json" + speakers_path = seg_path / "talents" / "speakers.json" has_speakers = speakers_path.is_file() null_proportion = null_count / total if total else 0.0 results.append( diff --git a/apps/speakers/tests/conftest.py b/apps/speakers/tests/conftest.py index 1689ebba3..56514e9ce 100644 --- a/apps/speakers/tests/conftest.py +++ b/apps/speakers/tests/conftest.py @@ -188,7 +188,7 @@ def speakers_env(tmp_path, monkeypatch): segment_key: Segment key (HHMMSS_LEN) speakers: List of speaker names """ - agents_dir = self.journal / day / STREAM / segment_key / "agents" + agents_dir = self.journal / day / STREAM / segment_key / "talents" agents_dir.mkdir(parents=True, exist_ok=True) speakers_path = agents_dir / "speakers.json" @@ -214,7 +214,7 @@ def speakers_env(tmp_path, monkeypatch): metadata: Optional extra metadata (owner_centroid_version, voiceprint_versions) """ - agents_dir = self.journal / day / STREAM / segment_key / "agents" + agents_dir = self.journal / day / STREAM / segment_key / "talents" agents_dir.mkdir(parents=True, exist_ok=True) data = {"labels": labels} @@ -248,7 +248,7 @@ def speakers_env(tmp_path, monkeypatch): stream: Optional stream name (defaults to STREAM) """ agents_dir = ( - self.journal / day / (stream or STREAM) / segment_key / "agents" + self.journal / day / (stream or STREAM) / segment_key / "talents" ) agents_dir.mkdir(parents=True, exist_ok=True) diff --git a/apps/speakers/tests/test_attribution.py b/apps/speakers/tests/test_attribution.py index 3b053139c..6b23ca510 100644 --- a/apps/speakers/tests/test_attribution.py +++ b/apps/speakers/tests/test_attribution.py @@ -171,7 +171,7 @@ def test_layer2_single_speaker(speakers_env): seg_dir = _write_controlled_segment(env, "20240101", "090000_300", embeddings) # speakers.json with exactly 1 speaker - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) (agents_dir / "speakers.json").write_text(json.dumps(["Ryan Bennett"])) @@ -535,7 +535,7 @@ def test_backfill_skips_already_labeled(speakers_env): env, "20260201", "090000_300", np.vstack([_normalized([1.0, 0.0])]) ) # Pre-create speaker_labels.json - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) (agents_dir / "speaker_labels.json").write_text('{"labels": []}') @@ -584,7 +584,7 @@ def test_backfill_processes_chronologically(speakers_env): # Labels written for day, seg_key in [("20260201", "080000_300"), ("20260210", "090000_300")]: labels_path = ( - env.journal / day / STREAM / seg_key / "agents" / "speaker_labels.json" + env.journal / day / STREAM / seg_key / "talents" / "speaker_labels.json" ) assert labels_path.exists() diff --git a/apps/speakers/tests/test_discovery.py b/apps/speakers/tests/test_discovery.py index 2bae74467..d3cd8e2dd 100644 --- a/apps/speakers/tests/test_discovery.py +++ b/apps/speakers/tests/test_discovery.py @@ -110,7 +110,7 @@ def _load_voiceprint_count(journal: Path, entity_id: str) -> int: def _load_corrections_count(journal: Path, day: str, segment_key: str) -> int: """Return number of correction entries for a segment.""" - path = journal / day / "test" / segment_key / "agents" / "speaker_corrections.json" + path = journal / day / "test" / segment_key / "talents" / "speaker_corrections.json" if not path.exists(): return 0 return len(json.loads(path.read_text(encoding="utf-8")).get("corrections", [])) @@ -199,14 +199,14 @@ def test_identify_creates_entity(speakers_env): for day, segment_key, _sentence_count in segments: labels_path = ( - env.journal / day / "test" / segment_key / "agents" / "speaker_labels.json" + env.journal / day / "test" / segment_key / "talents" / "speaker_labels.json" ) corrections_path = ( env.journal / day / "test" / segment_key - / "agents" + / "talents" / "speaker_corrections.json" ) labels_data = json.loads(labels_path.read_text(encoding="utf-8")) diff --git a/apps/speakers/tests/test_merge_names.py b/apps/speakers/tests/test_merge_names.py index 369f1c3c8..733343d04 100644 --- a/apps/speakers/tests/test_merge_names.py +++ b/apps/speakers/tests/test_merge_names.py @@ -127,7 +127,7 @@ def test_deep_merge_full(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: @@ -142,7 +142,7 @@ def test_deep_merge_full(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_corrections.json" ) with open(corr_path) as f: @@ -418,7 +418,7 @@ def test_speaker_labels_rewritten(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: @@ -459,7 +459,7 @@ def test_speaker_corrections_rewritten(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_corrections.json" ) with open(corr_path) as f: @@ -493,7 +493,7 @@ def test_fast_path_skips_unrelated_files(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) mtime_before = labels_path.stat().st_mtime_ns @@ -511,7 +511,7 @@ def test_corrupted_labels_logged_not_aborted(speakers_env): env.create_entity("Corrupt Canon") # Write corrupted file containing the alias_id string - agents_dir = env.journal / "20240101" / STREAM / "143022_300" / "agents" + agents_dir = env.journal / "20240101" / STREAM / "143022_300" / "talents" agents_dir.mkdir(parents=True, exist_ok=True) (agents_dir / "speaker_labels.json").write_text("corrupt_alias {not valid json") diff --git a/apps/speakers/tests/test_routes.py b/apps/speakers/tests/test_routes.py index 379be590c..f32b1c0ee 100644 --- a/apps/speakers/tests/test_routes.py +++ b/apps/speakers/tests/test_routes.py @@ -327,7 +327,7 @@ def test_load_segment_speakers_invalid_json(speakers_env): env = speakers_env() segment_dir = env.journal / "20240101" / "test" / "143022_300" - agents_dir = segment_dir / "agents" + agents_dir = segment_dir / "talents" agents_dir.mkdir(parents=True) # Write invalid JSON @@ -346,7 +346,7 @@ def test_load_segment_speakers_not_list(speakers_env): env = speakers_env() segment_dir = env.journal / "20240101" / "test" / "143022_300" - agents_dir = segment_dir / "agents" + agents_dir = segment_dir / "talents" agents_dir.mkdir(parents=True) # Write object instead of list @@ -583,7 +583,7 @@ def test_api_review_corrections_excludes_confirmed(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_corrections.json" ) corr_path.write_text( @@ -668,7 +668,7 @@ def test_api_confirm_attribution(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: @@ -682,7 +682,7 @@ def test_api_confirm_attribution(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_corrections.json" ) assert corr_path.exists() @@ -826,7 +826,7 @@ def test_api_correct_attribution(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: @@ -969,7 +969,7 @@ def test_api_assign_attribution(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: diff --git a/apps/speakers/tests/test_suggest.py b/apps/speakers/tests/test_suggest.py index 2ecfd067c..d50019fa6 100644 --- a/apps/speakers/tests/test_suggest.py +++ b/apps/speakers/tests/test_suggest.py @@ -18,7 +18,7 @@ from apps.speakers.suggest import ( def create_meetings_md(env, day: str, content: str) -> Path: - meetings_path = env.journal / day / "agents" / "meetings.md" + meetings_path = env.journal / day / "talents" / "meetings.md" meetings_path.parent.mkdir(parents=True, exist_ok=True) meetings_path.write_text(content, encoding="utf-8") return meetings_path diff --git a/apps/stats/static/dashboard.js b/apps/stats/static/dashboard.js index 72f6645b9..ceb2b10e7 100644 --- a/apps/stats/static/dashboard.js +++ b/apps/stats/static/dashboard.js @@ -5,7 +5,7 @@ const Dashboard = (function() { 'use strict'; - const EXPECTED_SCHEMA_VERSION = 2; + const EXPECTED_SCHEMA_VERSION = 3; const DISPLAY_LABELS = { transcript: 'Audio', percept: 'Screen' }; // DOM element factory @@ -576,7 +576,7 @@ const Dashboard = (function() { } // Required-field validation (blocking — stops rendering if fields missing) - const requiredFields = ['days', 'totals', 'heatmap', 'tokens', 'agents', 'facets']; + const requiredFields = ['days', 'totals', 'heatmap', 'tokens', 'talents', 'facets']; const missingFields = requiredFields.filter(f => !(f in stats)); if (missingFields.length > 0) { document.getElementById('notice').appendChild( @@ -725,7 +725,7 @@ const Dashboard = (function() { // Render Events stacked bar chart buildStackedCategoryChart( document.getElementById('eventsChart'), - stats.agents.counts_by_day || {}, + stats.talents.counts_by_day || {}, Object.assign({}, data.generators || {}, { emptyIcon: '⚡', emptyText: 'No event data recorded', diff --git a/apps/todos/routes.py b/apps/todos/routes.py index 10aa64a2f..2d990f5fa 100644 --- a/apps/todos/routes.py +++ b/apps/todos/routes.py @@ -625,7 +625,7 @@ Write the generated checklist to facets/{facet}/todos/{day}.jsonl""" try: from convey.utils import spawn_agent - agent_id = spawn_agent( + use_id = spawn_agent( prompt=prompt, name="todos:todo", provider="openai", @@ -634,14 +634,14 @@ Write the generated checklist to facets/{facet}/todos/{day}.jsonl""" except Exception as exc: # pragma: no cover - network/agent failure return jsonify({"error": f"Failed to spawn agent: {exc}"}), 500 - if agent_id is None: + if use_id is None: return jsonify({"error": "Failed to connect to agent service"}), 503 if not hasattr(state, "todo_generation_agents"): state.todo_generation_agents = {} - state.todo_generation_agents[day] = agent_id + state.todo_generation_agents[day] = use_id - return jsonify({"agent_id": agent_id, "status": "started"}) + return jsonify({"use_id": use_id, "status": "started"}) @todos_bp.route("//generation-status") @@ -650,21 +650,21 @@ def todo_generation_status(day: str): # type: ignore[override] return "", 404 facet = request.args.get("facet", "personal") - agent_id = request.args.get("agent_id") - if not agent_id and hasattr(state, "todo_generation_agents"): - agent_id = state.todo_generation_agents.get(day) + use_id = request.args.get("use_id") + if not use_id and hasattr(state, "todo_generation_agents"): + use_id = state.todo_generation_agents.get(day) - if not agent_id: - return jsonify({"status": "none", "agent_id": None}) + if not use_id: + return jsonify({"status": "none", "use_id": None}) - from think.cortex_client import cortex_agents + from think.cortex_client import cortex_uses todo_path = _todo_path(day, facet) - agents_dir = Path(state.journal_root) / "agents" - agent_file = next(agents_dir.glob(f"*/{agent_id}.jsonl"), None) + talents_dir = Path(state.journal_root) / "talents" + use_file = next(talents_dir.glob(f"*/{use_id}.jsonl"), None) - if agent_file and agent_file.exists(): + if use_file and use_file.exists(): if todo_path.exists(): if ( hasattr(state, "todo_generation_agents") @@ -672,24 +672,22 @@ def todo_generation_status(day: str): # type: ignore[override] ): del state.todo_generation_agents[day] return jsonify( - {"status": "finished", "agent_id": agent_id, "todo_created": True} + {"status": "finished", "use_id": use_id, "todo_created": True} ) - return jsonify( - {"status": "finished", "agent_id": agent_id, "todo_created": False} - ) + return jsonify({"status": "finished", "use_id": use_id, "todo_created": False}) try: - response = cortex_agents(limit=100, offset=0) + response = cortex_uses(limit=100, offset=0) if response: - agents = response.get("agents", []) - for agent in agents: - if agent.get("id") == agent_id: - return jsonify({"status": "running", "agent_id": agent_id}) - return jsonify({"status": "unknown", "agent_id": agent_id}) + uses = response.get("uses", []) + for use in uses: + if use.get("id") == use_id: + return jsonify({"status": "running", "use_id": use_id}) + return jsonify({"status": "unknown", "use_id": use_id}) except Exception: # pragma: no cover - external call failure pass - return jsonify({"status": "unknown", "agent_id": agent_id}) + return jsonify({"status": "unknown", "use_id": use_id}) @todos_bp.route("//generate-weekly/", methods=["POST"]) @@ -712,7 +710,7 @@ Focus on surfacing the most important unfinished work from the past 7 days.""" try: from convey.utils import spawn_agent - agent_id = spawn_agent( + use_id = spawn_agent( prompt=prompt, name="todos:weekly", provider="openai", @@ -721,7 +719,7 @@ Focus on surfacing the most important unfinished work from the past 7 days.""" except Exception as exc: # pragma: no cover - network/agent failure return jsonify({"error": f"Failed to spawn agent: {exc}"}), 500 - if agent_id is None: + if use_id is None: return jsonify({"error": "Failed to connect to agent service"}), 503 - return jsonify({"agent_id": agent_id, "status": "started"}) + return jsonify({"use_id": use_id, "status": "started"}) diff --git a/apps/todos/talent/daily.md b/apps/todos/talent/daily.md index 6411462c2..9a11fc3c9 100644 --- a/apps/todos/talent/daily.md +++ b/apps/todos/talent/daily.md @@ -10,7 +10,7 @@ "multi_facet": true, "group": "Todos", "load": { - "agents": True, + "talents": True, "journal": True } } diff --git a/apps/todos/workspace.html b/apps/todos/workspace.html index 21980c060..ba81f5528 100644 --- a/apps/todos/workspace.html +++ b/apps/todos/workspace.html @@ -1285,8 +1285,8 @@ document.addEventListener('DOMContentLoaded', function() { // Listen for cortex events (agent completion) if (window.appEvents) { window.appEvents.listen('cortex', msg => { - // Find button by agent_id - const btn = document.querySelector(`.facet-generate-btn[data-agent-id="${msg.agent_id}"]`); + // Find button by use_id + const btn = document.querySelector(`.facet-generate-btn[data-agent-id="${msg.use_id}"]`); if (!btn) return; if (msg.event === 'finish') { @@ -1321,7 +1321,7 @@ document.addEventListener('DOMContentLoaded', function() { const day = btn.dataset.day; const facet = btn.dataset.facet; - // If already has agent_id, navigate to agents page + // If already has use_id, navigate to agents page if (btn.dataset.agentId) { window.location.href = btn.dataset.agentUrl; return; @@ -1342,9 +1342,9 @@ document.addEventListener('DOMContentLoaded', function() { const data = await response.json(); - // Store agent_id for event matching and navigation - btn.dataset.agentId = data.agent_id; - btn.dataset.agentUrl = `/agents#${data.agent_id}`; + // Store use_id for event matching and navigation + btn.dataset.agentId = data.use_id; + btn.dataset.agentUrl = `/agents#${data.use_id}`; } catch (error) { showMessage('Failed to start todo generation', 'error'); diff --git a/apps/transcripts/routes.py b/apps/transcripts/routes.py index a46027ba6..c0650a5bf 100644 --- a/apps/transcripts/routes.py +++ b/apps/transcripts/routes.py @@ -224,7 +224,7 @@ def segment_content(day: str, stream: str, segment_key: str) -> Any: warnings = 0 # Load speaker labels if available. - speaker_labels_path = Path(segment_dir) / "agents" / "speaker_labels.json" + speaker_labels_path = Path(segment_dir) / "talents" / "speaker_labels.json" speaker_map: dict[int, dict] = {} if speaker_labels_path.is_file(): try: @@ -414,13 +414,13 @@ def segment_content(day: str, stream: str, segment_key: str) -> Any: # Get cost data for this segment cost_data = get_usage_cost(day, segment=segment_key) - # Collect agent .md files + # Collect talent .md files md_files = {} - agents_dir = Path(segment_dir) / "agents" - if agents_dir.is_dir(): - for md_path in sorted(agents_dir.rglob("*.md")): + talents_dir = Path(segment_dir) / "talents" + if talents_dir.is_dir(): + for md_path in sorted(talents_dir.rglob("*.md")): try: - key = md_path.relative_to(agents_dir).with_suffix("").as_posix() + key = md_path.relative_to(talents_dir).with_suffix("").as_posix() md_files[key] = md_path.read_text() except Exception: continue diff --git a/convey/apps.py b/convey/apps.py index 73ef95d48..fbeaa909a 100644 --- a/convey/apps.py +++ b/convey/apps.py @@ -95,7 +95,7 @@ def _resolve_attention(awareness_current: dict) -> AttentionItem | None: journal = Path(get_journal()) today = datetime.now().strftime("%Y%m%d") - day_index = journal / "agents" / f"{today}.jsonl" + day_index = journal / "talents" / f"{today}.jsonl" if day_index.exists(): errors: dict[str, float] = {} successes: dict[str, float] = {} @@ -174,7 +174,7 @@ def _resolve_attention(awareness_current: dict) -> AttentionItem | None: journal = Path(get_journal()) today = datetime.now().strftime("%Y%m%d") - agents_dir = journal / today / "agents" + agents_dir = journal / today / "talents" if agents_dir.is_dir(): outputs = sorted(p.stem for p in agents_dir.glob("*.md")) if outputs: diff --git a/convey/templates/app.html b/convey/templates/app.html index d22304e7c..994ca9bfc 100644 --- a/convey/templates/app.html +++ b/convey/templates/app.html @@ -452,16 +452,16 @@ // Subscribe to WS first, then check GET if (window.appEvents) { recoveryCleanup = window.appEvents.listen('cortex', function(msg) { - if (msg.agent_id === agentId) { + if (msg.use_id === agentId) { if (recoveryWatchdog) { clearTimeout(recoveryWatchdog); recoveryWatchdog = null; } recoveryWatchdog = setTimeout(function() { recoverDeliver('', 'panel', 'request timed out. the server took too long to respond. try a shorter question, or check if solstone services are running.'); }, 180000); } - if (msg.agent_id === agentId && msg.event === 'finish') { + if (msg.use_id === agentId && msg.event === 'finish') { var resp = msg.result || ''; recoverDeliver(resp, msg.display || 'panel', null); - } else if (msg.agent_id === agentId && msg.event === 'error') { + } else if (msg.use_id === agentId && msg.event === 'error') { recoverDeliver('', 'panel', 'something went wrong. the server returned an unexpected response. try sending your message again, or check the health page if it keeps happening.'); } }); @@ -710,7 +710,7 @@ } var data = await r.json(); - var agentId = data.agent_id; + var agentId = data.use_id; if (!agentId) { deliverResult('', 'panel', 'something went wrong. the server returned an unexpected response. try sending your message again, or check the health page if it keeps happening.'); return; @@ -751,17 +751,17 @@ if (window.appEvents) { cleanupCortex = window.appEvents.listen('cortex', function(msg) { // Reset inactivity watchdog and update thinking label for our agent - if (msg.agent_id === agentId) { + if (msg.use_id === agentId) { var label = getProgressLabel(msg); if (label) updateThinkingLabel(label); startWatchdog(agentId); } // Handle finish/error for our agent - if (msg.agent_id === agentId && msg.event === 'finish') { + if (msg.use_id === agentId && msg.event === 'finish') { var resp = msg.result || ''; deliverResult(resp, msg.display || 'panel', null); - } else if (msg.agent_id === agentId && msg.event === 'error') { + } else if (msg.use_id === agentId && msg.event === 'error') { deliverResult('', 'panel', 'something went wrong. the server returned an unexpected response. try sending your message again, or check the health page if it keeps happening.'); } }); diff --git a/convey/triage.py b/convey/triage.py index b30c15e25..03b987a76 100644 --- a/convey/triage.py +++ b/convey/triage.py @@ -37,10 +37,10 @@ def triage() -> Any: """Accept a message from the conversation panel and spawn a triage agent. Expects JSON: {message, app, path, facet} - Returns JSON: {agent_id} + Returns JSON: {use_id} The agent runs asynchronously. The browser receives the result via - WebSocket (cortex/finish event). For reload recovery, use GET /result/. + WebSocket (cortex/finish event). For reload recovery, use GET /result/. All journals route to the unified talent. """ @@ -96,33 +96,33 @@ def triage() -> Any: config["path"] = path config["user_message"] = message - agent_id = spawn_agent( + use_id = spawn_agent( prompt=full_prompt, name=agent_name, provider=None, config=config, ) - if agent_id is None: + if use_id is None: return error_response("Failed to connect to agent service", 503) - return jsonify(agent_id=agent_id) + return jsonify(use_id=use_id) except Exception: logger.exception("Triage request failed") return error_response("Failed to process triage request", 500) -@bp.route("/result/", methods=["GET"]) -def triage_result(agent_id: str) -> Any: +@bp.route("/result/", methods=["GET"]) +def triage_result(use_id: str) -> Any: """Return the result of a completed triage agent. Returns {response, display} if the agent has finished, 404 otherwise. Used for page-reload recovery when the WebSocket may have missed the finish event. """ try: - from think.cortex_client import read_agent_events + from think.cortex_client import read_use_events - events = read_agent_events(agent_id) + events = read_use_events(use_id) for event in reversed(events): if event.get("event") == "finish": result = event.get("result", "") @@ -130,5 +130,5 @@ def triage_result(agent_id: str) -> Any: except FileNotFoundError: pass except Exception: - logger.debug("Failed to read triage result for %s", agent_id, exc_info=True) + logger.debug("Failed to read triage result for %s", use_id, exc_info=True) return jsonify(error="not found"), 404 diff --git a/convey/utils.py b/convey/utils.py index b41092d18..b3dd4d4ff 100644 --- a/convey/utils.py +++ b/convey/utils.py @@ -88,10 +88,10 @@ def spawn_agent( provider: Optional[str] = None, config: Optional[dict[str, Any]] = None, ) -> str | None: - """Spawn a Cortex agent and return the agent_id. + """Spawn a Cortex agent and return the use_id. Thin wrapper around cortex_request that ensures imports are handled - and returns the agent_id directly. + and returns the use_id directly. Args: prompt: The task or question for the agent @@ -100,7 +100,7 @@ def spawn_agent( config: Additional configuration (max_tokens, facet, session_id, etc.) Returns: - agent_id string (timestamp-based), or None if the request could not be sent. + use_id string (timestamp-based), or None if the request could not be sent. Raises: ValueError: If config is invalid @@ -245,7 +245,7 @@ def success_response( Example: return success_response() # Returns {"success": True} - return success_response({"agent_id": "123"}) # Returns {"success": True, "agent_id": "123"} + return success_response({"use_id": "123"}) # Returns {"success": True, "use_id": "123"} """ from flask import jsonify diff --git a/docs/APPS.md b/docs/APPS.md index 12d234bda..e3840cf50 100644 --- a/docs/APPS.md +++ b/docs/APPS.md @@ -281,7 +281,7 @@ Define custom generator prompts that integrate with solstone's output generation - Create `talent/` directory with `.md` files containing JSON frontmatter - App generators are automatically discovered alongside system generators - Keys are namespaced as `{app}:{agent}` (e.g., `my_app:weekly_summary`) -- Outputs go to `JOURNAL/YYYYMMDD/agents/__.md` (or `.json` if `output: "json"`) +- Outputs go to `JOURNAL/YYYYMMDD/talents/__.md` (or `.json` if `output: "json"`) **Metadata format:** Same schema as system generators in `talent/*.md` - JSON frontmatter includes `title`, `description`, `color`, `schedule` (required), `priority` (required for scheduled prompts), `hook`, `output`, `max_output_tokens`, and `thinking_budget` fields. The `schedule` field must be `"segment"` or `"daily"`. The `priority` field is required for all scheduled prompts - prompts without explicit priority will fail validation. Set `output: "json"` for structured JSON output instead of markdown. Optional `max_output_tokens` sets the maximum response length; `thinking_budget` sets the model's thinking token budget (provider-specific defaults apply if omitted). Generators reject a `cwd` field entirely; working-directory control is only available for `type: "cogitate"` prompts. @@ -307,7 +307,7 @@ The `occurrences` field (optional string) provides agent-specific extraction gui } ``` -**App-data outputs:** For outputs from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/agents/*.md` - these are automatically indexed. +**App-data outputs:** For outputs from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/talents/*.md` - these are automatically indexed. **Template variables:** Generator prompts can use template variables like `$name`, `$preferred`, `$daily_preamble`, and context variables like `$day` and `$day_YYYYMMDD`. See [PROMPT_TEMPLATES.md](PROMPT_TEMPLATES.md) for the complete template system documentation. @@ -321,13 +321,13 @@ The `occurrences` field (optional string) provides agent-specific extraction gui - Resolution: `"name"` → `talent/{name}.py`, `"app:name"` → `apps/{app}/talent/{name}.py`, or explicit path **Pre-hooks** (`pre_process`): Modify inputs before the LLM call -- `context` is the full config dict with: `name`, `agent_id`, `provider`, `model`, `prompt`, `system_instruction` (if set), `user_instruction`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` +- `context` is the full config dict with: `name`, `use_id`, `provider`, `model`, `prompt`, `system_instruction` (if set), `user_instruction`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` - Return a dict of modified fields to merge back (e.g., `{"prompt": "modified"}`) - Return `None` for no changes **Post-hooks** (`post_process`): Transform output after the LLM call - `result` is the LLM output (markdown or JSON string) -- `context` is the full config dict with: `name`, `agent_id`, `provider`, `model`, `prompt`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` +- `context` is the full config dict with: `name`, `use_id`, `provider`, `model`, `prompt`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` - Return modified string, or `None` to use original result **Flush hooks:** Segment agents can declare `"hook": {"flush": true}` to participate in segment flush. When no new segments arrive for an extended period, the supervisor triggers `sol dream --flush --segment `, which runs only flush-enabled agents with `context["flush"] = True` and `context["refresh"] = True`. This lets agents close out dangling state (e.g., end active activities that would otherwise wait indefinitely for the next segment). The timeout is managed by the supervisor — agents should trust the flush signal without their own timeout logic. @@ -371,7 +371,7 @@ Define custom agents and generator templates that integrate with solstone's Cort **Reference implementations:** - System agent examples: `talent/*.md` (files with `tools` field) -- Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=True)`, `get_agent()` +- Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=True)`, `get_talent()` #### Prompt Context Configuration @@ -379,7 +379,7 @@ Both generators and agents support an optional `load` key for configuring source ```json { - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } ``` @@ -394,7 +394,7 @@ Context is provided inline in the `.md` body via template variables: - `$facets` - focused facet context or all available facets - `$activity_context` - activity metadata, segment state, and analysis focus sections -**Authoritative source:** `think/talent.py` - `_DEFAULT_LOAD`, `source_is_enabled()`, `source_is_required()`, `get_agent_filter()` +**Authoritative source:** `think/talent.py` - `_DEFAULT_LOAD`, `source_is_enabled()`, `source_is_required()`, `get_talent_filter()` --- @@ -535,7 +535,7 @@ Available in `convey/utils.py`: - `format_date(date_str)` - Format YYYYMMDD as "Wednesday January 14th" ### Agent Spawning -- `spawn_agent(prompt, name, provider, config)` - Spawn Cortex agent, returns agent_id +- `spawn_agent(prompt, name, provider, config)` - Spawn Cortex agent, returns use_id ### JSON Utilities - `load_json(path)` - Load JSON file with error handling (returns None on error) diff --git a/docs/CALLOSUM.md b/docs/CALLOSUM.md index 688c9884d..5d358786c 100644 --- a/docs/CALLOSUM.md +++ b/docs/CALLOSUM.md @@ -36,7 +36,7 @@ Callosum is a JSON-per-line message bus for real-time event distribution across ### `cortex` - Agent execution events **Source:** `think/cortex.py` -**Events:** `request`, `start`, `thinking`, `tool_start`, `tool_end`, `finish`, `error`, `agent_updated`, `info`, `status` +**Events:** `request`, `start`, `thinking`, `tool_start`, `tool_end`, `finish`, `error`, `talent_updated`, `info`, `status` **Details:** See [CORTEX.md](CORTEX.md) for agent lifecycle, configuration, and event schemas ### `supervisor` - Process lifecycle management @@ -105,7 +105,7 @@ Callosum is a JSON-per-line message bus for real-time event distribution across ### `dream` - Generator and agent processing **Source:** `think/dream.py` -**Events:** `started`, `status`, `group_started`, `group_completed`, `agent_started`, `agent_completed`, `completed`, `segments_started`, `segments_completed` +**Events:** `started`, `status`, `group_started`, `group_completed`, `talent_started`, `talent_completed`, `completed`, `segments_started`, `segments_completed` **Key fields:** `mode` ("daily"/"segment"/"activity"/"flush"), `day`, `segment` (when mode="segment" or "flush"), `activity` and `facet` (when mode="activity") **Purpose:** Track dream processing from generators through scheduled agents **`status`** - Periodic progress (every ~5s). Fields: `mode`, `day`, `segment`, `stream`, `agents_completed`, `agents_total`, `current_group_priority`, `current_agents` (list of running agent names). In `--segments` batch mode, also includes `segments_completed`, `segments_total`. In activity mode, includes `activity`, `facet`. @@ -257,7 +257,7 @@ emit("supervisor", "request", ref=task_id, cmd=["sol", "import", path]) For agent requests, use the cortex client: ```python from think.cortex_client import cortex_request -agent_id = cortex_request(prompt="...", name="default") +use_id = cortex_request(prompt="...", name="default") ``` See `think/cortex_client.py` for the full API. diff --git a/docs/CORTEX.md b/docs/CORTEX.md index 3a042c6e3..0eecc59f3 100644 --- a/docs/CORTEX.md +++ b/docs/CORTEX.md @@ -1,6 +1,6 @@ # Cortex API and Eventing -The Cortex system manages AI agent execution through the Callosum message bus with file-based persistence. It acts as a process manager for agent instances, receiving requests via Callosum and writing execution events to both JSONL files (for persistence) and the message bus (for real-time distribution). +The Cortex system manages AI talent execution through the Callosum message bus with file-based persistence. It acts as a process manager for talent instances, receiving requests via Callosum and writing execution events to both JSONL files (for persistence) and the message bus (for real-time distribution). For details on the Callosum protocol and message format, see [CALLOSUM.md](CALLOSUM.md). @@ -9,22 +9,22 @@ For details on the Callosum protocol and message format, see [CALLOSUM.md](CALLO ### Event Flow 1. **Request Creation**: Client calls `cortex_request()` which broadcasts to Callosum (`tract="cortex"`, `event="request"`) 2. **Request Reception**: Cortex receives message via Callosum callback and creates `/_active.jsonl` -3. **Agent Spawning**: Cortex spawns agent process via `sol agents` with merged configuration -4. **Event Emission**: Agents write JSON events to stdout (captured by Cortex) +3. **Talent Spawning**: Cortex spawns a talent process via `python -m think.talents` with merged configuration +4. **Event Emission**: Talents write JSON events to stdout (captured by Cortex) 5. **Event Distribution**: Cortex appends events to JSONL file AND broadcasts to Callosum 6. **Agent Completion**: Cortex renames file to `/.jsonl` when agent finishes ### Key Components - **Message Bus Integration**: Cortex connects to Callosum to receive requests and broadcast events -- **Process Management**: Spawns agent subprocesses (both tool agents and generators) -- **Configuration Delegation**: Passes raw requests to `sol agents`, which handles all config loading, validation, and hydration +- **Process Management**: Spawns talent subprocesses (both tool talents and generators) +- **Configuration Delegation**: Passes raw requests to `python -m think.talents`, which handles all config loading, validation, and hydration - **Event Capture**: Monitors agent stdout/stderr and appends to JSONL files - **Dual Event Distribution**: Events go to both persistent files and real-time message bus - **NDJSON Input Mode**: Agent processes accept newline-delimited JSON via stdin containing the full merged configuration ### File States -- `/_active.jsonl`: Agent currently executing (Cortex is appending events) -- `/.jsonl`: Agent completed (contains full event history) +- `/_active.jsonl`: Talent currently executing (Cortex is appending events) +- `/.jsonl`: Talent completed (contains full event history) **Note**: Files provide persistence and historical record, while Callosum provides real-time event distribution to all interested services. @@ -35,16 +35,16 @@ Requests are created via `cortex_request()` from `think.cortex_client`, which br ```json { "event": "request", - "ts": 1234567890123, // Required: millisecond timestamp (must match agent_id in filename) - "prompt": "Analyze this code for security issues", // Required for agents (not generators) - "name": "default", // Optional: agent name from talent/*.md + "ts": 1234567890123, // Required: millisecond timestamp (must match use_id in filename) + "prompt": "Analyze this code for security issues", // Required for talents (not generators) + "name": "default", // Optional: talent name from talent/*.md "provider": "openai", // Optional: override provider (openai, google, anthropic) "max_output_tokens": 8192, // Optional: maximum response tokens "thinking_budget": 10000, // Optional: thinking token budget (ignored by OpenAI) "session_id": "sess-abc123", // Optional: CLI session ID for continuation "chat_id": "1234567890122", // Optional: chat ID for reverse lookup "facet": "my-project", // Optional: project context - "output": "md", // Optional: output format ("md" or "json"), writes to agents/ + "output": "md", // Optional: output format ("md" or "json"), writes to talents/ "day": "20250109", // Optional: YYYYMMDD format, defaults to current day "env": { // Optional: environment variables for subprocess "API_KEY": "secret", @@ -79,7 +79,7 @@ Generators are spawned via Cortex when a request has an `output` field but no `t ### Generator Events -Generators emit the same event types as agents: +Generators emit the same event types as talents: - `start` - When generation begins - `finish` - On completion, with `result` containing generated content - `error` - On failure @@ -92,16 +92,16 @@ The `finish` event may include a `skipped` field when generation is skipped: All providers (Anthropic, OpenAI, Google) support continuing conversations via CLI session resumption. Include a `session_id` field in the request with the CLI session -ID from a previous agent's finish event. The provider CLI tool resumes the conversation +ID from a previous talent's finish event. The provider CLI tool resumes the conversation internally using its native session management (e.g., `claude --resume`, `codex exec resume`). Chats are locked to their original provider — continuations must use the same provider that started the conversation. The `chat_id` field enables reverse lookup from an -agent back to its parent chat. +talent back to its parent chat. ## Agent Event Format -All subsequent lines are JSON objects with `event` and millisecond `ts` fields. The `ts` field is automatically added by Cortex if not provided by the provider. Additionally, Cortex automatically adds an `agent_id` field (matching the timestamp component in the filename) to all events for tracking purposes. +All subsequent lines are JSON objects with `event` and millisecond `ts` fields. The `ts` field is automatically added by Cortex if not provided by the provider. Additionally, Cortex automatically adds an `use_id` field (matching the timestamp component in the filename) to all events for tracking purposes. ### request The initial spawn request (first line of file, written by client). @@ -109,7 +109,7 @@ The initial spawn request (first line of file, written by client). { "event": "request", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "prompt": "User's task or question", "provider": "openai", "name": "default", @@ -119,12 +119,12 @@ The initial spawn request (first line of file, written by client). ``` ### start -Emitted when an agent run begins. +Emitted when a talent run begins. ```json { "event": "start", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "name": "default", "model": "gpt-4o", "session_id": "sess-abc", @@ -138,7 +138,7 @@ Emitted when a tool execution begins. { "event": "tool_start", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "tool": "search_journal", "args": {"query": "search terms", "limit": 10}, "call_id": "search_journal-1" @@ -151,7 +151,7 @@ Emitted when a tool execution completes. { "event": "tool_end", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "tool": "search_journal", "args": {"query": "search terms"}, "result": ["result", "array", "or", "object"], @@ -165,30 +165,30 @@ Emitted when the model produces reasoning/thinking content (model-dependent, pri { "event": "thinking", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "summary": "Model's internal reasoning about the task...", "model": "o1-mini" } ``` -### agent_updated +### talent_updated Emitted when control is handed off to a different agent (multi-agent scenarios). ```json { - "event": "agent_updated", + "event": "talent_updated", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "agent": "SpecializedAgent" } ``` ### finish -Emitted when the agent run completes successfully. +Emitted when the talent run completes successfully. ```json { "event": "finish", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "result": "Final response text to the owner" } ``` @@ -199,7 +199,7 @@ Emitted when an error occurs during execution. { "event": "error", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "error": "Error message", "trace": "Full stack trace..." } @@ -211,7 +211,7 @@ Emitted when non-JSON output is captured from agent stdout. { "event": "info", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "message": "Non-JSON output line from agent" } ``` @@ -232,23 +232,23 @@ When an agent completes successfully, its result can be automatically written to - Include an `output` field in the agent's frontmatter with the format ("md" or "json") - Output path is derived from agent name + format + schedule: - - Daily agents: `YYYYMMDD/agents/{name}.{ext}` + - Daily agents: `YYYYMMDD/talents/{name}.{ext}` - Segment agents: `YYYYMMDD/{segment}/{name}.{ext}` - Writing occurs before completion - Write failures are logged but don't interrupt the agent flow - Commonly used for scheduled agents that generate daily reports -## Agent Configuration +## Talent Configuration -Agents use configurations stored in the `talent/` directory. Each agent is a `.md` file containing: +Talents use configurations stored in the `talent/` directory. Each talent is a `.md` file containing: - JSON frontmatter with metadata and configuration -- The agent-specific prompt and instructions in the content +- The talent-specific prompt and instructions in the content -When spawning an agent: -1. Cortex passes the raw request to `sol agents` via stdin (NDJSON format) -2. The agent process (`think/agents.py`) handles all config loading via `prepare_config()`: - - Loads agent configuration using `get_agent()` from `think/talent.py` - - Merges request parameters with agent defaults +When spawning a talent: +1. Cortex passes the raw request to `python -m think.talents` via stdin (NDJSON format) +2. The talent process (`think/talents.py`) handles all config loading via `prepare_config()`: + - Loads talent configuration using `get_talent()` from `think/talent.py` + - Merges request parameters with talent defaults - Resolves provider and model based on context 3. The agent validates the config via `validate_config()` before execution 4. Instructions are built with three components: diff --git a/docs/DOCTOR.md b/docs/DOCTOR.md index cc1fb68aa..2d174eac1 100644 --- a/docs/DOCTOR.md +++ b/docs/DOCTOR.md @@ -12,7 +12,7 @@ pgrep -af "sol:observer|sol:sense|sol:supervisor" ls -la journal/health/callosum.sock # Check for stuck agents (should be empty or short-lived) -ls journal/agents/*/*_active.jsonl 2>/dev/null +ls journal/talents/*/*_active.jsonl 2>/dev/null ``` **Healthy state:** @@ -45,7 +45,7 @@ See [CALLOSUM.md](CALLOSUM.md) for message protocol and [CORTEX.md](CORTEX.md) f |------|-------| | Current service logs | `journal/health/{service}.log` (symlinks) | | Day's process logs | `journal/{YYYYMMDD}/health/{ref}_{name}.log` | -| Agent execution | `journal/agents//*.jsonl` | +| Agent execution | `journal/talents//*.jsonl` | | Journal task log | `journal/task_log.txt` | **Symlink structure:** Journal-level symlinks point to current day's logs. Day-level symlinks point to current process instance (by ref). @@ -89,7 +89,7 @@ See [CALLOSUM.md](CALLOSUM.md) Tract Registry for event schemas. ## Reading Agent Files -**Location:** `journal/agents/` +**Location:** `journal/talents/` **File states:** - `{name}/{timestamp}_active.jsonl` - Agent currently running @@ -105,10 +105,10 @@ See [CALLOSUM.md](CALLOSUM.md) Tract Registry for event schemas. ```bash # View an agent's final result -jq -r 'select(.event=="finish") | .result' journal/agents/default/1234567890123.jsonl +jq -r 'select(.event=="finish") | .result' journal/talents/default/1234567890123.jsonl # List today's agents with their prompts -for id in $(jq -r '.agent_id' journal/agents/$(date +%Y%m%d).jsonl 2>/dev/null); do +for id in $(jq -r '.use_id' journal/talents/$(date +%Y%m%d).jsonl 2>/dev/null); do f=$(find journal/agents -maxdepth 2 -path "*/${id}.jsonl" -print -quit) [ -n "$f" ] || continue echo "=== $(basename "$f") ===" @@ -138,10 +138,10 @@ Causes: DBus issues, screencast permissions, audio device unavailable. ```bash # Find active agents -ls -la journal/agents/*/*_active.jsonl +ls -la journal/talents/*/*_active.jsonl # Check last event in active agent -tail -1 journal/agents/*/*_active.jsonl | jq . +tail -1 journal/talents/*/*_active.jsonl | jq . ``` Causes: Backend timeout, tool hanging, network issues. @@ -176,11 +176,11 @@ Causes: Slow transcription, describe API rate limits. tail -f journal/health/*.log # Count today's agents by status -echo "Completed: $([ -f journal/agents/$(date +%Y%m%d).jsonl ] && wc -l < journal/agents/$(date +%Y%m%d).jsonl || echo 0)" -echo "Running: $(ls journal/agents/*/*_active.jsonl 2>/dev/null | wc -l)" +echo "Completed: $([ -f journal/talents/$(date +%Y%m%d).jsonl ] && wc -l < journal/talents/$(date +%Y%m%d).jsonl || echo 0)" +echo "Running: $(ls journal/talents/*/*_active.jsonl 2>/dev/null | wc -l)" # Find agents that errored today -jq -r 'select(.status=="error") | .agent_id' journal/agents/$(date +%Y%m%d).jsonl 2>/dev/null +jq -r 'select(.status=="error") | .use_id' journal/talents/$(date +%Y%m%d).jsonl 2>/dev/null # Check token usage for today wc -l journal/tokens/$(date +%Y%m%d).jsonl diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index 4f866fe8e..2019cce80 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -14,7 +14,7 @@ solstone transforms raw recordings into actionable understanding through a three ┌─────────────────────────────────────┐ │ LAYER 3: AGENT OUTPUTS │ Narrative summaries │ (Markdown files) │ "What it means" -│ - agents/*.md (daily outputs) │ +│ - talents/*.md (daily outputs) │ │ - *.md (segment outputs) │ └─────────────────────────────────────┘ ↑ synthesized from @@ -42,7 +42,7 @@ solstone transforms raw recordings into actionable understanding through a three |------|------------|----------| | **Capture** | Raw audio/video recording | `*.flac`, `*.ogg`, `*.opus`, `*.wav`, `*.webm` | | **Extract** | Structured data from captures | `*.jsonl` | -| **Agent Output** | AI-generated narrative summary | `agents/*.md`, `HHMMSS_LEN/*.md` | +| **Agent Output** | AI-generated narrative summary | `talents/*.md`, `HHMMSS_LEN/*.md` | **Organization** @@ -67,7 +67,7 @@ solstone transforms raw recordings into actionable understanding through a three | `chronicle/` | Container for daily capture folders (`YYYYMMDD/`) containing segments, extracts, and agent outputs | | `entities/` | Journal-level entity identity records (`/entity.json`) | | `facets/` | Facet-specific data: entity relationships, todos, events, news, action logs | -| `agents/` | Agent run logs in per-agent subdirectories (`/.jsonl`), day indexes (`.jsonl`), and latest-run symlinks (`.log`) | +| `talents/` | Talent run logs in per-talent subdirectories (`/.jsonl`), day indexes (`.jsonl`), and latest-run symlinks (`.log`) | | `apps/` | App-specific storage (distinct from codebase `apps/`) | | `streams/` | Per-stream state files (`.json`) tracking segment chains and sequence numbers | | `imports/` | Imported audio files and processing artifacts | @@ -186,14 +186,14 @@ Fields: "Raw media" means layer 1 capture files only: audio files (`.flac`, `.opus`, `.ogg`, `.m4a`, `.wav`), video files (`.webm`, `.mov`, `.mp4`), and screen diffs (`monitor_*_diff.png`). -All layer 2 and layer 3 content is always preserved regardless of retention policy: transcripts (`audio.jsonl`, `screen.jsonl`), agent outputs (`agents/*.md`), speaker labels (`agents/speaker_labels.json`), facet events (`events/*.jsonl`), entity data, segment metadata (`stream.json`), and search index entries. +All layer 2 and layer 3 content is always preserved regardless of retention policy: transcripts (`audio.jsonl`, `screen.jsonl`), talent outputs (`talents/*.md`), speaker labels (`talents/speaker_labels.json`), facet events (`events/*.jsonl`), entity data, segment metadata (`stream.json`), and search index entries. Raw media is never deleted from segments that haven't finished processing. A segment is considered complete only when all four checks pass: -- No `_active.jsonl` files in `agents/` (no running agents) +- No `_active.jsonl` files in `talents/` (no running talents) - `audio.jsonl` (or `*_audio.jsonl`) exists if audio raw media was captured - `screen.jsonl` (or `*_screen.jsonl`) exists if video raw media was captured -- `agents/speaker_labels.json` exists if voice embeddings (`.npz`) are present +- `talents/speaker_labels.json` exists if voice embeddings (`.npz`) are present Purged segments remain fully navigable in convey. Transcripts, entities, speaker labels, and summaries are all intact. The only difference is that audio/video playback is unavailable. @@ -733,7 +733,7 @@ The `logs/` directory within each facet records facet-scoped actions. Logs are o "text": "Review project proposal" }, "facet": "work", - "agent_id": "1765870373972" + "use_id": "1765870373972" } ``` @@ -747,7 +747,7 @@ Both log types share the same structure: - `action` – Action name (e.g., "todo_add", "identity_update") - `params` – Action-specific parameters - `facet` – Facet name (only present in facet-scoped logs) -- `agent_id` – Agent ID (only present for agent tool actions) +- `use_id` – Agent ID (only present for agent tool actions) These logs enable auditing, debugging, and potential rollback of automated actions. @@ -778,7 +778,7 @@ Each line in a token log file is a JSON object with the following structure: Required fields: - `timestamp` – Unix timestamp in milliseconds (13 digits) - `model` – Model identifier (e.g., "gemini-2.5-flash", "gpt-5", "claude-sonnet-4-5") -- `context` – Calling context (e.g., "agent.name.agent_id" or "module.function:line") +- `context` – Calling context (e.g., "agent.name.use_id" or "module.function:line") - `usage` – Token counts dictionary with normalized field names Optional fields: @@ -796,16 +796,16 @@ The logging system normalizes provider-specific formats (OpenAI, Gemini, Anthrop ## Agent Event Logs -The `agents/` directory stores event logs for all AI agent sessions managed by Cortex. Each agent session produces a JSONL file containing the complete event history. +The `talents/` directory stores event logs for all AI talent sessions managed by Cortex. Each talent session produces a JSONL file containing the complete event history. **Directory layout:** - `/` – per-agent subdirectory (e.g., `default/`, `entities--observer/`) -- `/_active.jsonl` – currently running agent (renamed when complete) -- `/.jsonl` – completed agent session +- `/_active.jsonl` – currently running agent (renamed when complete) +- `/.jsonl` – completed agent session - `.log` – symlink to the latest completed run for each agent name - `.jsonl` – day index with one summary line per agent that completed on that day -The `agent_id` is a Unix timestamp in milliseconds that uniquely identifies the session. +The `use_id` is a Unix timestamp in milliseconds that uniquely identifies the session. **Event format (JSONL):** @@ -1040,8 +1040,8 @@ There are two types of events: - **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) ```jsonl -{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/agents/meetings.md", "details": "Sprint planning discussion"} -{"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/agents/schedule.md", "details": "Final review before release"} +{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/talents/meetings.md", "details": "Sprint planning discussion"} +{"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/talents/schedule.md", "details": "Final review before release"} ``` **Common fields:** @@ -1069,7 +1069,7 @@ After captures are processed, segment-level outputs are generated within each se #### Daily outputs -Post-processing generates day-level outputs in the `agents/` directory that synthesize all segments. +Post-processing generates day-level outputs in the `talents/` directory that synthesize all segments. **Generator discovery:** Available generator types are discovered at runtime from: - `talent/*.md` – system generator templates (files with `schedule` field but no `tools` field) @@ -1078,8 +1078,8 @@ Post-processing generates day-level outputs in the `agents/` directory that synt Each template is a `.md` file with JSON frontmatter containing metadata (title, description, schedule, output format). The `schedule` field is required and must be `"segment"` or `"daily"` - generators with missing or invalid schedule are skipped. Use `get_talent_configs(has_tools=False)` from `think/talent.py` to retrieve all available generators, or `get_talent_configs(has_tools=False, schedule="daily")` to get generators filtered by schedule. **Output naming:** -- System outputs: `agents/{agent}.md` (e.g., `agents/flow.md`, `agents/meetings.md`) -- App outputs: `agents/_{app}_{agent}.md` (e.g., `agents/_entities_observer.md`) -- JSON output: `agents/{agent}.json` when metadata specifies `"output": "json"` +- System outputs: `talents/{agent}.md` (e.g., `talents/flow.md`, `talents/meetings.md`) +- App outputs: `talents/_{app}_{agent}.md` (e.g., `talents/_entities_observer.md`) +- JSON output: `talents/{agent}.json` when metadata specifies `"output": "json"` Each generator type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form. diff --git a/docs/PROMPT_TEMPLATES.md b/docs/PROMPT_TEMPLATES.md index d21c20089..cd298a130 100644 --- a/docs/PROMPT_TEMPLATES.md +++ b/docs/PROMPT_TEMPLATES.md @@ -148,7 +148,7 @@ You are a helpful assistant... **Optional model configuration:** Add `max_output_tokens` (response length limit) and `thinking_budget` (model thinking token budget) to override provider defaults. Note: OpenAI uses fixed reasoning and ignores `thinking_budget`. -**Reference:** `think/talent.py` → `get_agent()` for agent configuration loading +**Reference:** `think/talent.py` → `get_talent()` for agent configuration loading ### The load_prompt() Function diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index e567e85a7..ffbf5a9a4 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -121,7 +121,7 @@ async def run_cogitate( - `extra_context`: Runtime context (facets, insights list, datetime) as first user message - `user_instruction`: Agent-specific prompt as second user message - `tools`: Optional list of allowed tool names -- `agent_id`, `name`: Identity for logging and tool calls +- `use_id`, `name`: Identity for logging and tool calls - `session_id`: CLI session ID for conversation continuation - `chat_id`: Chat ID for reverse lookup from agent to chat diff --git a/docs/THINK.md b/docs/THINK.md index 4d4c749d3..2cb8c45ec 100644 --- a/docs/THINK.md +++ b/docs/THINK.md @@ -17,7 +17,7 @@ The package exposes several commands: - `sol call transcripts read` groups audio and screen transcripts into report sections. Use `--start` and `--length` to limit the report to a specific time range. See `sol call transcripts --help` for additional commands. - `sol dream` runs generators and agents for a single day via Cortex. -- `sol agents` is the unified CLI for tool agents and generators (spawned by Cortex, NDJSON protocol). +- `python -m think.talents` is the unified execution module for tool talents and generators spawned by Cortex (NDJSON protocol). - `sol supervisor` monitors observation heartbeats. Use `--no-observers` to disable local capture (sense still runs for observer uploads and imports). - `sol cortex` starts a Callosum-based service for managing AI agent instances and generators. - `sol talent` lists available agents and generators with their configuration. Use `sol talent show ` to see details, and `sol talent show --prompt` to see the fully composed prompt that would be sent to the LLM. @@ -91,30 +91,30 @@ All scheduled prompts (both generators and tool-using agents) share a unified pr After each generator completes and creates output, the indexer runs `--rescan-file` for incremental indexing. A full `--rescan` runs in the post phase. -### Cortex: Central Agent Manager +### Cortex: Central Talent Manager -The Cortex service (`sol cortex`) is the central system for managing AI agent instances and generators. It monitors the journal's `agents/` directory for new requests and manages execution. All agent spawning should go through Cortex for proper event tracking and management. +The Cortex service (`sol cortex`) is the central system for managing AI talent instances and generators. It monitors the journal's `talents/` directory for new requests and manages execution. All talent spawning should go through Cortex for proper event tracking and management. Cortex routes requests based on configuration: -- Requests with `tools` field → tool-using agents (`sol agents`) -- Requests with `output` field (no `tools`) → generators (`sol agents`) +- Requests with `tools` field → tool-using talents (`python -m think.talents`) +- Requests with `output` field (no `tools`) → generators (`python -m think.talents`) -Both types are handled by the unified `sol agents` CLI which routes internally. +Both types are handled by the unified `python -m think.talents` execution module. -To spawn agents programmatically, use the cortex_client functions: +To spawn talents programmatically, use the cortex_client functions: ```python from think.cortex_client import cortex_request from think.callosum import CallosumConnection # Create a request -agent_id = cortex_request( +use_id = cortex_request( prompt="Your task here", name="default", provider="openai" # or "google", "anthropic", "claude" ) -# Watch for agent events via Callosum +# Watch for talent events via Callosum def on_event(message): # Filter for cortex tract events if message.get('tract') != 'cortex': @@ -135,10 +135,10 @@ watcher.stop() Generators can also be spawned via `cortex_request` by including an `output` field: ```python -from think.cortex_client import cortex_request, wait_for_agents +from think.cortex_client import cortex_request, wait_for_uses # Spawn a generator -agent_id = cortex_request( +use_id = cortex_request( prompt="", # Generators don't use prompts name="activity", config={ @@ -149,15 +149,15 @@ agent_id = cortex_request( ) # Wait for completion -completed, timed_out = wait_for_agents([agent_id], timeout=300) +completed, timed_out = wait_for_uses([use_id], timeout=300) ``` ### Direct CLI Usage (Testing Only) -The `sol agents` command is primarily used internally by Cortex. For testing purposes, it can be invoked directly: +The `sol providers check` command is an ad-hoc provider check CLI. Cortex does not use it as the talent spawn path. For testing purposes, it can be invoked directly: ```bash -sol agents [TASK_FILE] [--provider PROVIDER] [--model MODEL] [--max-tokens N] [-o OUT_FILE] +sol providers check [TASK_FILE] [--provider PROVIDER] [--model MODEL] [--max-tokens N] [-o OUT_FILE] ``` The provider can be ``openai`` (default), ``google``, ``anthropic``, or ``ollama``. Configure the corresponding API key in the ``env`` section of ``journal/config/journal.json`` (e.g., ``OPENAI_API_KEY``, ``GOOGLE_API_KEY``, or ``ANTHROPIC_API_KEY``). The ``ollama`` provider requires no API key — it connects to a local Ollama instance. Keys are loaded into ``os.environ`` by ``setup_cli()`` at process startup. @@ -196,7 +196,7 @@ Cortex is the central agent management system that all agent spawning should go The `think.cortex_client` module provides functions for interacting with Cortex: ```python -from think.cortex_client import cortex_request, cortex_agents +from think.cortex_client import cortex_request, cortex_uses # Create an agent request request_file = cortex_request( @@ -206,7 +206,7 @@ request_file = cortex_request( ) # List running and completed agents -agents_info = cortex_agents(limit=10, agent_type="live") +agents_info = cortex_uses(limit=10, use_type="live") print(f"Found {agents_info['live_count']} running agents") ``` # Talent Module @@ -218,7 +218,7 @@ AI agent system and tool-calling support for solstone. | Command | Purpose | |---------|---------| | `sol cortex` | Agent orchestration service | -| `sol agents` | Direct agent invocation (testing only) | +| `sol providers check` | Ad-hoc provider check (testing only) | ## Architecture @@ -245,7 +245,7 @@ Providers implement `run_generate()`, `run_agenerate()`, and `run_cogitate()` fu ## Key Components - **cortex.py** - Central agent manager, file watcher, event distribution, spawns agents.py -- **cortex_client.py** - Client functions: `cortex_request()`, `cortex_agents()`, `wait_for_agents()` +- **cortex_client.py** - Client functions: `cortex_request()`, `cortex_uses()`, `wait_for_uses()` - **agents.py** - Unified CLI entry point for both tool-using agents and generators (NDJSON protocol) - **models.py** - Unified `generate()`/`agenerate()` API, provider routing, token logging - **batch.py** - `Batch` class for concurrent LLM requests with dynamic queuing diff --git a/docs/design/yesterdays-processing-card.md b/docs/design/yesterdays-processing-card.md index 6868ae9d4..a265d8e92 100644 --- a/docs/design/yesterdays-processing-card.md +++ b/docs/design/yesterdays-processing-card.md @@ -32,7 +32,7 @@ Internal helpers called only by `_summarize_yesterday_processing`: Reads `stats_data["heatmap_data"]["hours"]`, keeps the top 3 non-zero hours, sorts by minutes desc then hour asc. - `_knowledge_graph_freshness(yesterday: str) -> dict` - Reads `chronicle/{yesterday}/agents/knowledge_graph.md`, checks existence and `st_mtime` freshness using the relaxed rule in section 4. + Reads `chronicle/{yesterday}/talents/knowledge_graph.md`, checks existence and `st_mtime` freshness using the relaxed rule in section 4. - `_briefing_freshness(today: str) -> dict` Reads `journal/sol/briefing.md` with local `frontmatter.load`. Valid only when frontmatter has `type: morning_briefing` and a parseable `generated` timestamp whose local date is `today`. @@ -125,7 +125,7 @@ Recommended rendered content by mode: ### Ground truth -- `agent.fail` records include `name`, `agent_id`, `state`, and optional `facet`, but `summarize_pipeline_day()` counts every failure and drops `facet` from `failed_list`. See `think/pipeline_health.py:81-99`. +- `talent.fail` records include `name`, `use_id`, `state`, and optional `facet`, but `summarize_pipeline_day()` counts every failure and drops `facet` from `failed_list`. See `think/pipeline_health.py:81-99`. - `stats.json.facet_data` is not a newsletter ledger. It is built from `events.jsonl` durations in `think/journal_stats.py:296-319` and surfaced in `apps/home/routes.py:616-621`. - The facet newsletter writer is `sol call journal news`, implemented by `think/tools/facets.py:61-106`. - The newsletter prompt key is stable: `facet_newsletter`. @@ -135,9 +135,9 @@ Recommended rendered content by mode: ### Option A — re-parse dream JSONL for newsletter-specific facet fails -Read `chronicle/{yesterday}/health/*_daily_dream.jsonl` and count `agent.fail` records where: +Read `chronicle/{yesterday}/health/*_daily_dream.jsonl` and count `talent.fail` records where: -- `event == "agent.fail"` +- `event == "talent.fail"` - `facet` is present - `name == "facet_newsletter"` @@ -163,7 +163,7 @@ Cons: ### Option B — re-parse any facet-scoped fail -Count every `agent.fail` with a `facet` field, regardless of `name`. +Count every `talent.fail` with a `facet` field, regardless of `name`. Pros: @@ -224,7 +224,7 @@ Do not require `mtime` to fall strictly within yesterday’s wall-clock day. Rationale: -- Prep already found a real case where `chronicle/20260415/agents/knowledge_graph.md` had `mtime` on `2026-04-16 07:23:43`. +- Prep already found a real case where `chronicle/20260415/talents/knowledge_graph.md` had `mtime` on `2026-04-16 07:23:43`. - The intent of the card is “did the overnight processing refresh yesterday’s graph?”, not “did the write finish before midnight”. - This rule admits same-day and overnight-after-midnight completions without introducing an arbitrary 36-hour window. @@ -350,7 +350,7 @@ Supporting non-chronicle fixture: Fixture minimization rule: - Seed only the fields each test asserts on. -- Keep dream logs to the minimum lines needed: `run.start`, `agent.dispatch`, `agent.complete` or `agent.fail`, `run.complete`. +- Keep dream logs to the minimum lines needed: `run.start`, `talent.dispatch`, `talent.complete` or `talent.fail`, `run.complete`. ## 9. Non-goals @@ -383,7 +383,7 @@ Fixture minimization rule: All three gate items resolved. Proceed to `implement` stage. -- **Q2 denominator:** Go with **Option A** as recommended. Successes from `facets/*/news/{yesterday}.md`. Failures from dream-log `agent.fail` where `name == "facet_newsletter"` and `facet` is present. When current pipeline emits no `facet_newsletter` fails (which is the common case today), `M == N` and the `N of M` sentence degenerates into a simple `N` — that's fine, honest, and forward-compatible for when we start logging newsletter failures under that exact key. Use the sparse fallback "I didn't produce any facet newsletters." when both are zero. +- **Q2 denominator:** Go with **Option A** as recommended. Successes from `facets/*/news/{yesterday}.md`. Failures from dream-log `talent.fail` where `name == "facet_newsletter"` and `facet` is present. When current pipeline emits no `facet_newsletter` fails (which is the common case today), `M == N` and the `N of M` sentence degenerates into a simple `N` — that's fine, honest, and forward-compatible for when we start logging newsletter failures under that exact key. Use the sparse fallback "I didn't produce any facet newsletters." when both are zero. - **Q3 knowledge-graph freshness:** Use the **relaxed rule**: fresh when `knowledge_graph.md` exists and `st_mtime >= start_of_yesterday_local`. Overnight-after-midnight completions count. Use local time boundaries. Don't use birth/ctime. - **First-week framing copy (verbatim):** The exact copy IS in the scope (top-level note) and in the approved CPO spec. Use this text, unchanged, when `journal_age_days <= 7` and `mode != "sparse"`: diff --git a/scripts/gate_agents_rename.py b/scripts/gate_agents_rename.py new file mode 100644 index 000000000..d24486906 --- /dev/null +++ b/scripts/gate_agents_rename.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: AGPL-3.0-only + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SHIM_FILES = { + Path("think/pipeline_health.py"), + Path("apps/home/routes.py"), +} +ALLOWLIST_RE = re.compile(r"^apps/sol/maint/00[0-4]_.+\.py$") +PRODUCTION_PREFIXES = ("think/", "apps/", "talent/", "convey/", "observe/") +SHIM_WINDOW = 20 + +RULES = [ + ( + "legacy dream emitter", + re.compile(r'_jsonl_log\(\s*["\']agent\.(fail|dispatch|complete|skip)["\']'), + None, + ), + ( + "legacy callosum emitter", + re.compile(r'emit\(\s*["\']agent_(started|completed)["\']'), + None, + ), + ("legacy module path", re.compile(r"\bthink\.agents\b"), None), + ("legacy/new CLI command", re.compile(r"\bsol agents\b|\bsol talents\b"), None), + ("legacy payload key", re.compile(r'["\']agent_id["\']\s*:'), "production"), + ("legacy wire event", re.compile(r'["\']agent_updated["\']'), "production"), + ( + "legacy summary/anomaly key", + re.compile( + r'summary\["agents"\]|["\']agent_failure["\']|["\']agents_fired["\']' + ), + "production", + ), +] + + +def tracked_files() -> list[Path]: + result = subprocess.run( + ["git", "ls-files"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + return [Path(line) for line in result.stdout.splitlines() if line] + + +def is_allowed(path: Path) -> bool: + path_str = path.as_posix() + if path == Path("AGENTS.md"): + return True + if path == Path("tests/test_maint_004_rename.py"): + return True + if path == Path("scripts/gate_agents_rename.py"): + return True + if path_str.startswith(".agents/skills/"): + return True + if ALLOWLIST_RE.match(path_str): + return True + return False + + +def is_production(path: Path) -> bool: + path_str = path.as_posix() + return path_str == "sol.py" or path_str.startswith(PRODUCTION_PREFIXES) + + +def iter_lines(path: Path) -> list[tuple[int, str]]: + lines = (ROOT / path).read_text(encoding="utf-8").splitlines() + if path not in SHIM_FILES: + return list(enumerate(lines, start=1)) + + visible: list[tuple[int, str]] = [] + suppress_until = 0 + for line_no, line in enumerate(lines, start=1): + if line_no <= suppress_until: + continue + if "HISTORICAL SHIM:" in line: + suppress_until = line_no + SHIM_WINDOW + continue + visible.append((line_no, line)) + return visible + + +def main() -> int: + failures: list[str] = [] + for path in tracked_files(): + if is_allowed(path): + continue + if not (ROOT / path).is_file(): + continue + try: + lines = iter_lines(path) + except UnicodeDecodeError: + continue + for line_no, line in lines: + for label, pattern, scope in RULES: + if scope == "production" and not is_production(path): + continue + if pattern.search(line): + failures.append(f"{path}:{line_no}: {label}: {line.strip()}") + + if failures: + print("agents rename gate failed:", file=sys.stderr) + for failure in failures: + print(f" {failure}", file=sys.stderr) + return 1 + + print("agents rename gate passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/sol.py b/sol.py index 5632a68e2..3f4c3457b 100644 --- a/sol.py +++ b/sol.py @@ -11,7 +11,7 @@ Usage: Examples: sol import data.json Import data into journal sol dream 20250101 Run daily processing for a day - sol think.agents -h Show help for specific module + sol think.talents -h Show help for specific module """ from __future__ import annotations @@ -57,8 +57,8 @@ COMMANDS: dict[str, str] = { "transfer": "observe.transfer", "export": "observe.export", "observer": "observe.observer_cli", - # AI agents (talent package) - "agents": "think.agents", + # AI providers and talent execution + "providers": "think.providers_cli", "cortex": "think.cortex", "talent": "think.talent_cli", "call": "think.call", @@ -111,8 +111,8 @@ GROUPS: dict[str, list[str]] = { "export", "observer", ], - "Talent (AI agents)": [ - "agents", + "Talent": [ + "providers", "cortex", "talent", "engage", diff --git a/talent/activities.py b/talent/activities.py index a974d091f..1b510fa72 100644 --- a/talent/activities.py +++ b/talent/activities.py @@ -46,7 +46,7 @@ logger = logging.getLogger(__name__) def _list_facets_with_activity_state(day: str, segment: str, stream: str) -> list[str]: """Find all facets that have activity_state.json in a segment.""" - agents_dir = segment_path(day, segment, stream) / "agents" + agents_dir = segment_path(day, segment, stream) / "talents" if not agents_dir.is_dir(): return [] @@ -61,7 +61,7 @@ def _list_facets_with_activity_state(day: str, segment: str, stream: str) -> lis def _load_activity_state(day: str, segment: str, facet: str, stream: str) -> list[dict]: """Load activity_state.json for a facet in a segment. Returns [] on failure.""" state_path = ( - segment_path(day, segment, stream) / "agents" / facet / "activity_state.json" + segment_path(day, segment, stream) / "talents" / facet / "activity_state.json" ) if not state_path.exists(): return [] diff --git a/talent/activity_state.py b/talent/activity_state.py index 1fd126679..2146f4edc 100644 --- a/talent/activity_state.py +++ b/talent/activity_state.py @@ -34,7 +34,7 @@ def _extract_facet_from_output_path(output_path: str) -> str | None: """Extract facet name from output path. Output paths for faceted generators follow the pattern: - {day}/{stream}/{segment}/agents/{facet}/activity_state.json + {day}/{stream}/{segment}/talents/{facet}/activity_state.json Returns None if facet cannot be extracted. """ @@ -46,7 +46,7 @@ def _extract_facet_from_output_path(output_path: str) -> str | None: return None parent = os.path.basename(os.path.dirname(output_path)) - if parent and parent != "agents": + if parent and parent != "talents": return parent return None @@ -124,7 +124,7 @@ def load_previous_state( parsed JSON array or None if not found/invalid. """ state_path = ( - segment_path(day, segment, stream) / "agents" / facet / "activity_state.json" + segment_path(day, segment, stream) / "talents" / facet / "activity_state.json" ) if not state_path.exists(): return None, None diff --git a/talent/daily_schedule.md b/talent/daily_schedule.md index 18f5e6d70..b454f9fa9 100644 --- a/talent/daily_schedule.md +++ b/talent/daily_schedule.md @@ -10,7 +10,7 @@ "color": "#455a64", "thinking_budget": 4096, "max_output_tokens": 512, - "load": {"transcripts": false, "percepts": false, "agents": false} + "load": {"transcripts": false, "percepts": false, "talents": false} } $facets diff --git a/talent/decisions.md b/talent/decisions.md index 080925078..4ed93aaf7 100644 --- a/talent/decisions.md +++ b/talent/decisions.md @@ -10,7 +10,7 @@ "activities": ["meeting", "call", "messaging", "email"], "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/documents.md b/talent/documents.md index da557128b..fdbc765b3 100644 --- a/talent/documents.md +++ b/talent/documents.md @@ -10,7 +10,7 @@ "thinking_budget": 8192, "max_output_tokens": 8192, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": false} + "load": {"transcripts": true, "percepts": false, "talents": false} } diff --git a/talent/entities.md b/talent/entities.md index 76807a9c2..64046b4d7 100644 --- a/talent/entities.md +++ b/talent/entities.md @@ -10,7 +10,7 @@ "thinking_budget": 4096, "max_output_tokens": 1024, "output": "md", - "load": {"transcripts": true, "percepts": true, "agents": false} + "load": {"transcripts": true, "percepts": true, "talents": false} } diff --git a/talent/facet_newsletter.md b/talent/facet_newsletter.md index c97d116b8..cf7642e05 100644 --- a/talent/facet_newsletter.md +++ b/talent/facet_newsletter.md @@ -9,7 +9,7 @@ "priority": 40, "multi_facet": true, "load": { - "agents": True, + "talents": True, "journal": True } } diff --git a/talent/flow.md b/talent/flow.md index 07a6cc1fd..975c7a4c1 100644 --- a/talent/flow.md +++ b/talent/flow.md @@ -9,7 +9,7 @@ "schedule": "daily", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/followups.md b/talent/followups.md index 13a06e5db..fc65dc6e4 100644 --- a/talent/followups.md +++ b/talent/followups.md @@ -10,7 +10,7 @@ "activities": ["meeting", "call", "messaging", "email"], "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/heartbeat.md b/talent/heartbeat.md index b1afe760c..89d8de5cb 100644 --- a/talent/heartbeat.md +++ b/talent/heartbeat.md @@ -34,9 +34,9 @@ If you find issues: update agency.md's `## system` section via ## Step 2: Check journal quality -Run `sol talent logs --daily -c 10` to review recent agent runs and +Run `sol talent logs --daily -c 10` to review recent talent runs and `sol talent logs --errors -c 10` for recent errors. Look for: -- Broken segments (transcription failures, missing agent output) +- Broken segments (transcription failures, missing talent output) - Processing gaps (capture with no dream processing) - Orphaned entities (zero observations after 7+ days) diff --git a/talent/knowledge_graph.md b/talent/knowledge_graph.md index c421812bb..bfa171e2a 100644 --- a/talent/knowledge_graph.md +++ b/talent/knowledge_graph.md @@ -9,7 +9,7 @@ "schedule": "daily", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/meetings.md b/talent/meetings.md index e68ca6fe9..4f2fc35b7 100644 --- a/talent/meetings.md +++ b/talent/meetings.md @@ -10,7 +10,7 @@ "activities": ["meeting"], "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/messaging.md b/talent/messaging.md index cf511e6ce..c7a7c1809 100644 --- a/talent/messaging.md +++ b/talent/messaging.md @@ -10,7 +10,7 @@ "activities": ["messaging", "email"], "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/morning_briefing.md b/talent/morning_briefing.md index b9c051154..c15280f65 100644 --- a/talent/morning_briefing.md +++ b/talent/morning_briefing.md @@ -70,8 +70,8 @@ Build five sections from the gathered data. **Omit any section entirely if it ha **Source attribution.** Attribute high-consequence factual claims to their source using inline parenthetical links with `sol://` URIs. Not every claim needs attribution — calendar events are self-evident and the Reading section is inherently attributed. `sol://` URI construction: -- **Search results:** The header includes an `id` (e.g. `20260304/archon/143022_300/agents/followups.md:2`). Strip `:idx`, then strip `/agents/{agent}.md` → `sol://20260304/archon/143022_300`. -- **Entity intelligence:** `activity[].path` contains a journal-relative path. Strip `/agents/{agent}.md` to get the segment or day path. If no stream/segment_key: `sol://{day}/agents/{agent}`. +- **Search results:** The header includes an `id` (e.g. `20260304/archon/143022_300/talents/followups.md:2`). Strip `:idx`, then strip `/talents/{agent}.md` → `sol://20260304/archon/143022_300`. +- **Entity intelligence:** `activity[].path` contains a journal-relative path. Strip `/talents/{agent}.md` to get the segment or day path. If no stream/segment_key: `sol://{day}/talents/{agent}`. - **Facet newsletters:** `sol://facets/{facet}/news/{day_YYYYMMDD}`. **Your Day** — What's ahead today. Lead with calendar events in chronological order. For each meeting, include who's attending and one line of entity-informed context (e.g., "last met 2 weeks ago, discussed product roadmap"). Include relevant todos due today. If no calendar events exist, lead with the highest-priority todos. @@ -90,7 +90,7 @@ Grade highlights by evidence strength. **High** (corroborated by multiple source 4. Unscheduled todos (action items with no calendar time blocked) Pipeline gaps owner-facing phrasings (from `pipeline_anomalies`). Use these verbatim, substituting real counts and agent names from the summary: - `activity_agents_missing` → "**Pipeline gap:** N activities ended yesterday but activity agents didn't fire — meeting notes, decisions, and follow-ups may be missing." - - `agent_failure` → "**Pipeline issue:** N agents timed out during yesterday's processing (name1, name2). Some insights may be incomplete." (Use "timed out" when every failed agent has `state == "timeout"`; otherwise use "failed".) + - `talent_failure` → "**Pipeline issue:** N agents timed out during yesterday's processing (name1, name2). Some insights may be incomplete." (Use "timed out" when every failed agent has `state == "timeout"`; otherwise use "failed".) - `daily_agents_missing` → "**Pipeline gap:** Daily agents didn't run yesterday despite journal data. Facet newsletters and digest may be missing." Do NOT include this section when pipeline status is `healthy` (status == "healthy" or anomalies list is empty). Zero noise on normal days. @@ -141,7 +141,7 @@ gaps: [list of gap descriptions, or empty list [] if none] ## Forward Look - Board meeting Thursday — slides need review (confirmed on [calendar](sol://20260327/calendar)) -- May want to prepare quarterly metrics based on last quarter's timing (from [anticipation](sol://20260327/agents/anticipation)) +- May want to prepare quarterly metrics based on last quarter's timing (from [anticipation](sol://20260327/talents/anticipation)) [more items...] ## Reading diff --git a/talent/schedule.md b/talent/schedule.md index 52d80daf8..20154aa77 100644 --- a/talent/schedule.md +++ b/talent/schedule.md @@ -8,7 +8,7 @@ "schedule": "daily", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } diff --git a/talent/screen.md b/talent/screen.md index 5de4d69d1..3b008312c 100644 --- a/talent/screen.md +++ b/talent/screen.md @@ -7,7 +7,7 @@ "schedule": "segment", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": "required", "agents": false} + "load": {"transcripts": true, "percepts": "required", "talents": false} } diff --git a/talent/sense.md b/talent/sense.md index 9dd57b503..e69c2b244 100644 --- a/talent/sense.md +++ b/talent/sense.md @@ -10,7 +10,7 @@ "thinking_budget": 4096, "max_output_tokens": 4096, "output": "json", - "load": {"transcripts": true, "percepts": true, "agents": false} + "load": {"transcripts": true, "percepts": true, "talents": false} } $facets diff --git a/talent/skills.md b/talent/skills.md index 60bde3d83..79754d306 100644 --- a/talent/skills.md +++ b/talent/skills.md @@ -7,7 +7,7 @@ "activities": ["*"], "priority": 90, "output": "json", - "load": {"transcripts": false, "percepts": false, "agents": false} + "load": {"transcripts": false, "percepts": false, "talents": false} } You are analyzing recurring activity patterns to identify and document the owner's skills. diff --git a/talent/speaker_attribution.md b/talent/speaker_attribution.md index 30f54788c..c69cb0d3d 100644 --- a/talent/speaker_attribution.md +++ b/talent/speaker_attribution.md @@ -8,7 +8,7 @@ "output": "json", "color": "#d84315", "hook": {"pre": "speaker_attribution", "post": "speaker_attribution"}, - "load": {"transcripts": true, "agents": {"speakers": true, "screen": true}} + "load": {"transcripts": true, "talents": {"speakers": true, "screen": true}} } diff --git a/talent/speaker_attribution.py b/talent/speaker_attribution.py index c7d8e6227..b75f440f5 100644 --- a/talent/speaker_attribution.py +++ b/talent/speaker_attribution.py @@ -42,7 +42,7 @@ def pre_process(context: dict) -> dict | None: logger.info("Attribution skipped: %s", result["error"]) reason = result["error"] if any(seg_dir.glob("*.npz")): - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) out_path = agents_dir / "speaker_labels.json" with open(out_path, "w", encoding="utf-8") as fh: @@ -58,7 +58,7 @@ def pre_process(context: dict) -> dict | None: if not labels: reason = "no_embeddings" if any(seg_dir.glob("*.npz")): - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) out_path = agents_dir / "speaker_labels.json" with open(out_path, "w", encoding="utf-8") as fh: diff --git a/talent/timeline.md b/talent/timeline.md index d0247f876..f4bd11b2a 100644 --- a/talent/timeline.md +++ b/talent/timeline.md @@ -9,7 +9,7 @@ "schedule": "daily", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } diff --git a/tests/baselines/api/calendar/day-events.json b/tests/baselines/api/calendar/day-events.json index 852bae516..1e91a3b8d 100644 --- a/tests/baselines/api/calendar/day-events.json +++ b/tests/baselines/api/calendar/day-events.json @@ -10,7 +10,7 @@ "Romeo Montague", "Mercutio Escalus" ], - "source": "20260304/agents/flow.md", + "source": "20260304/talents/flow.md", "startTime": "2026-03-04T09:00:00", "subject": "", "summary": "Conference keynote featuring Juliet Capulet", @@ -27,7 +27,7 @@ "Juliet Capulet", "Romeo Montague" ], - "source": "20260304/agents/flow.md", + "source": "20260304/talents/flow.md", "startTime": "2026-03-04T18:00:00", "subject": "", "summary": "Networking event", @@ -43,7 +43,7 @@ "participants": [ "Juliet Capulet" ], - "source": "20260304/agents/flow.md", + "source": "20260304/talents/flow.md", "startTime": "2026-03-04T09:00:00", "subject": "", "summary": "Juliet presented on unified API gateways", @@ -60,7 +60,7 @@ "Romeo Montague", "Mercutio Escalus" ], - "source": "20260304/agents/flow.md", + "source": "20260304/talents/flow.md", "startTime": "2026-03-04T14:00:00", "subject": "", "summary": "Built API bridge prototype", diff --git a/tests/baselines/api/search/search.json b/tests/baselines/api/search/search.json index 93aea6bf1..4056145de 100644 --- a/tests/baselines/api/search/search.json +++ b/tests/baselines/api/search/search.json @@ -1,5 +1,5 @@ { - "agents": [], + "talents": [], "days": [], "facets": [ { diff --git a/tests/baselines/api/sol/run-detail.json b/tests/baselines/api/sol/run-detail.json index 0c753ef0d..456e79420 100644 --- a/tests/baselines/api/sol/run-detail.json +++ b/tests/baselines/api/sol/run-detail.json @@ -4,13 +4,7 @@ "error_message": null, "events": [ { - "agent": "solstone", - "agent_id": "1700000000001", - "event": "agent_updated", - "ts": 1700000000200 - }, - { - "agent_id": "1700000000001", + "use_id": "1700000000001", "args": null, "call_id": "call_001", "event": "tool_end", @@ -19,7 +13,7 @@ "ts": 1700000000500 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "args": { "limit": 5, "query": "project updates" @@ -30,13 +24,13 @@ "ts": 1700000000400 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "event": "thinking", "ts": 1700000000300 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, @@ -46,13 +40,19 @@ } }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "event": "start", "model": "gpt-4o", "name": "default", "prompt": "Search for meetings about project updates", "provider": "openai", "ts": 1700000000100 + }, + { + "talent": "solstone", + "use_id": "1700000000001", + "event": "talent_updated", + "ts": 1700000000200 } ], "facet": null, diff --git a/tests/baselines/api/sol/agents-day.json b/tests/baselines/api/sol/talents-day.json similarity index 99% rename from tests/baselines/api/sol/agents-day.json rename to tests/baselines/api/sol/talents-day.json index 8450a8a39..d2b64df7b 100644 --- a/tests/baselines/api/sol/agents-day.json +++ b/tests/baselines/api/sol/talents-day.json @@ -1,5 +1,5 @@ { - "agents": { + "talents": { "anticipation": { "app": null, "color": "#4527a0", @@ -523,5 +523,5 @@ "title": "Verona" } }, - "runs": [] + "uses": [] } diff --git a/tests/baselines/api/stats/stats.json b/tests/baselines/api/stats/stats.json index ea2540952..fb04925a5 100644 --- a/tests/baselines/api/stats/stats.json +++ b/tests/baselines/api/stats/stats.json @@ -8,7 +8,7 @@ "pre": "daily_schedule" }, "load": { - "agents": false, + "talents": false, "percepts": false, "transcripts": false }, @@ -36,7 +36,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -59,7 +59,7 @@ "pre": "documents" }, "load": { - "agents": false, + "talents": false, "percepts": false, "transcripts": true }, @@ -81,7 +81,7 @@ "post": "entities" }, "load": { - "agents": false, + "talents": false, "percepts": true, "transcripts": true }, @@ -106,7 +106,7 @@ "pre": "entities:entity_observer" }, "load": { - "agents": false, + "talents": false, "percepts": false, "transcripts": false }, @@ -129,7 +129,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -158,7 +158,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -181,7 +181,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -207,7 +207,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -234,7 +234,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -257,7 +257,7 @@ "post": "anticipation" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -276,7 +276,7 @@ "color": "#9c27b0", "description": "Creates a detailed documentary record of screen activity. Focuses on the 'what' - chronological account with preserved details, excerpts, and entities.", "load": { - "agents": false, + "talents": false, "percepts": "required", "transcripts": true }, @@ -293,7 +293,7 @@ "color": "#ff6f00", "description": "Unified segment understanding — density, content type, entities, facets, speakers, and routing recommendations in a single pass", "load": { - "agents": false, + "talents": false, "percepts": true, "transcripts": true }, @@ -320,7 +320,7 @@ "pre": "skills" }, "load": { - "agents": false, + "talents": false, "percepts": false, "transcripts": false }, @@ -341,7 +341,7 @@ "pre": "speaker_attribution" }, "load": { - "agents": { + "talents": { "screen": true, "speakers": true }, @@ -363,7 +363,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, diff --git a/tests/baselines/api/agents/badge-count.json b/tests/baselines/api/talents/badge-count.json similarity index 100% rename from tests/baselines/api/agents/badge-count.json rename to tests/baselines/api/talents/badge-count.json diff --git a/tests/baselines/api/agents/preview.json b/tests/baselines/api/talents/preview.json similarity index 100% rename from tests/baselines/api/agents/preview.json rename to tests/baselines/api/talents/preview.json diff --git a/tests/baselines/api/agents/run-detail.json b/tests/baselines/api/talents/run-detail.json similarity index 86% rename from tests/baselines/api/agents/run-detail.json rename to tests/baselines/api/talents/run-detail.json index 0c753ef0d..456e79420 100644 --- a/tests/baselines/api/agents/run-detail.json +++ b/tests/baselines/api/talents/run-detail.json @@ -4,13 +4,7 @@ "error_message": null, "events": [ { - "agent": "solstone", - "agent_id": "1700000000001", - "event": "agent_updated", - "ts": 1700000000200 - }, - { - "agent_id": "1700000000001", + "use_id": "1700000000001", "args": null, "call_id": "call_001", "event": "tool_end", @@ -19,7 +13,7 @@ "ts": 1700000000500 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "args": { "limit": 5, "query": "project updates" @@ -30,13 +24,13 @@ "ts": 1700000000400 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "event": "thinking", "ts": 1700000000300 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, @@ -46,13 +40,19 @@ } }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "event": "start", "model": "gpt-4o", "name": "default", "prompt": "Search for meetings about project updates", "provider": "openai", "ts": 1700000000100 + }, + { + "talent": "solstone", + "use_id": "1700000000001", + "event": "talent_updated", + "ts": 1700000000200 } ], "facet": null, diff --git a/tests/baselines/api/agents/stats-month.json b/tests/baselines/api/talents/stats-month.json similarity index 100% rename from tests/baselines/api/agents/stats-month.json rename to tests/baselines/api/talents/stats-month.json diff --git a/tests/baselines/api/agents/agents-day.json b/tests/baselines/api/talents/talents-day.json similarity index 99% rename from tests/baselines/api/agents/agents-day.json rename to tests/baselines/api/talents/talents-day.json index b313af1f3..cb8827f2e 100644 --- a/tests/baselines/api/agents/agents-day.json +++ b/tests/baselines/api/talents/talents-day.json @@ -1,5 +1,5 @@ { - "agents": { + "talents": { "anticipation": { "app": null, "color": "#4527a0", @@ -457,5 +457,5 @@ "title": "Verona" } }, - "runs": [] + "uses": [] } diff --git a/tests/baselines/api/agents/updated-days.json b/tests/baselines/api/talents/updated-days.json similarity index 100% rename from tests/baselines/api/agents/updated-days.json rename to tests/baselines/api/talents/updated-days.json diff --git a/tests/fixtures/journal/AGENTS.md b/tests/fixtures/journal/AGENTS.md index 5f0183cee..45a9f6667 100644 --- a/tests/fixtures/journal/AGENTS.md +++ b/tests/fixtures/journal/AGENTS.md @@ -14,7 +14,7 @@ solstone transforms raw recordings into actionable understanding through a three ┌─────────────────────────────────────┐ │ LAYER 3: AGENT OUTPUTS │ Narrative summaries │ (Markdown files) │ "What it means" -│ - agents/*.md (daily outputs) │ +│ - talents/*.md (daily outputs) │ │ - *.md (segment outputs) │ └─────────────────────────────────────┘ ↑ synthesized from @@ -42,7 +42,7 @@ solstone transforms raw recordings into actionable understanding through a three |------|------------|----------| | **Capture** | Raw audio/video recording | `*.flac`, `*.ogg`, `*.opus`, `*.wav`, `*.webm` | | **Extract** | Structured data from captures | `*.jsonl` | -| **Agent Output** | AI-generated narrative summary | `agents/*.md`, `HHMMSS_LEN/*.md` | +| **Agent Output** | AI-generated narrative summary | `talents/*.md`, `HHMMSS_LEN/*.md` | **Organization** @@ -67,7 +67,7 @@ solstone transforms raw recordings into actionable understanding through a three | `chronicle/` | Container for daily capture folders (`YYYYMMDD/`) containing segments, extracts, and agent outputs | | `entities/` | Journal-level entity identity records (`/entity.json`) | | `facets/` | Facet-specific data: entity relationships, todos, events, news, action logs | -| `agents/` | Agent run logs in per-agent subdirectories (`/.jsonl`), day indexes (`.jsonl`), and latest-run symlinks (`.log`) | +| `talents/` | Talent run logs in per-talent subdirectories (`/.jsonl`), day indexes (`.jsonl`), and latest-run symlinks (`.log`) | | `apps/` | App-specific storage (distinct from codebase `apps/`) | | `streams/` | Per-stream state files (`.json`) tracking segment chains and sequence numbers | | `imports/` | Imported audio files and processing artifacts | @@ -186,14 +186,14 @@ Fields: "Raw media" means layer 1 capture files only: audio files (`.flac`, `.opus`, `.ogg`, `.m4a`, `.wav`), video files (`.webm`, `.mov`, `.mp4`), and screen diffs (`monitor_*_diff.png`). -All layer 2 and layer 3 content is always preserved regardless of retention policy: transcripts (`audio.jsonl`, `screen.jsonl`), agent outputs (`agents/*.md`), speaker labels (`agents/speaker_labels.json`), facet events (`events/*.jsonl`), entity data, segment metadata (`stream.json`), and search index entries. +All layer 2 and layer 3 content is always preserved regardless of retention policy: transcripts (`audio.jsonl`, `screen.jsonl`), talent outputs (`talents/*.md`), speaker labels (`talents/speaker_labels.json`), facet events (`events/*.jsonl`), entity data, segment metadata (`stream.json`), and search index entries. Raw media is never deleted from segments that haven't finished processing. A segment is considered complete only when all four checks pass: -- No `_active.jsonl` files in `agents/` (no running agents) +- No `_active.jsonl` files in `talents/` (no running talents) - `audio.jsonl` (or `*_audio.jsonl`) exists if audio raw media was captured - `screen.jsonl` (or `*_screen.jsonl`) exists if video raw media was captured -- `agents/speaker_labels.json` exists if voice embeddings (`.npz`) are present +- `talents/speaker_labels.json` exists if voice embeddings (`.npz`) are present Purged segments remain fully navigable in convey. Transcripts, entities, speaker labels, and summaries are all intact. The only difference is that audio/video playback is unavailable. @@ -732,7 +732,7 @@ The `logs/` directory within each facet records facet-scoped actions. Logs are o "text": "Review project proposal" }, "facet": "work", - "agent_id": "1765870373972" + "use_id": "1765870373972" } ``` @@ -746,7 +746,7 @@ Both log types share the same structure: - `action` – Action name (e.g., "todo_add", "identity_update") - `params` – Action-specific parameters - `facet` – Facet name (only present in facet-scoped logs) -- `agent_id` – Agent ID (only present for agent tool actions) +- `use_id` – Agent ID (only present for agent tool actions) These logs enable auditing, debugging, and potential rollback of automated actions. @@ -777,7 +777,7 @@ Each line in a token log file is a JSON object with the following structure: Required fields: - `timestamp` – Unix timestamp in milliseconds (13 digits) - `model` – Model identifier (e.g., "gemini-2.5-flash", "gpt-5", "claude-sonnet-4-5") -- `context` – Calling context (e.g., "agent.name.agent_id" or "module.function:line") +- `context` – Calling context (e.g., "agent.name.use_id" or "module.function:line") - `usage` – Token counts dictionary with normalized field names Optional fields: @@ -795,16 +795,16 @@ The logging system normalizes provider-specific formats (OpenAI, Gemini, Anthrop ## Agent Event Logs -The `agents/` directory stores event logs for all AI agent sessions managed by Cortex. Each agent session produces a JSONL file containing the complete event history. +The `talents/` directory stores event logs for all AI talent sessions managed by Cortex. Each talent session produces a JSONL file containing the complete event history. **Directory layout:** - `/` – per-agent subdirectory (e.g., `default/`, `entities--observer/`) -- `/_active.jsonl` – currently running agent (renamed when complete) -- `/.jsonl` – completed agent session +- `/_active.jsonl` – currently running agent (renamed when complete) +- `/.jsonl` – completed agent session - `.log` – symlink to the latest completed run for each agent name - `.jsonl` – day index with one summary line per agent that completed on that day -The `agent_id` is a Unix timestamp in milliseconds that uniquely identifies the session. +The `use_id` is a Unix timestamp in milliseconds that uniquely identifies the session. **Event format (JSONL):** @@ -1039,8 +1039,8 @@ There are two types of events: - **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) ```jsonl -{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/agents/meetings.md", "details": "Sprint planning discussion"} -{"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/agents/schedule.md", "details": "Final review before release"} +{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/talents/meetings.md", "details": "Sprint planning discussion"} +{"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/talents/schedule.md", "details": "Final review before release"} ``` **Common fields:** @@ -1068,7 +1068,7 @@ After captures are processed, segment-level outputs are generated within each se #### Daily outputs -Post-processing generates day-level outputs in the `agents/` directory that synthesize all segments. +Post-processing generates day-level outputs in the `talents/` directory that synthesize all segments. **Generator discovery:** Available generator types are discovered at runtime from: - `talent/*.md` – system generator templates (files with `schedule` field but no `tools` field) @@ -1077,8 +1077,8 @@ Post-processing generates day-level outputs in the `agents/` directory that synt Each template is a `.md` file with JSON frontmatter containing metadata (title, description, schedule, output format). The `schedule` field is required and must be `"segment"` or `"daily"` - generators with missing or invalid schedule are skipped. Use `get_talent_configs(has_tools=False)` from `think/talent.py` to retrieve all available generators, or `get_talent_configs(has_tools=False, schedule="daily")` to get generators filtered by schedule. **Output naming:** -- System outputs: `agents/{agent}.md` (e.g., `agents/flow.md`, `agents/meetings.md`) -- App outputs: `agents/_{app}_{agent}.md` (e.g., `agents/_entities_observer.md`) -- JSON output: `agents/{agent}.json` when metadata specifies `"output": "json"` +- System outputs: `talents/{agent}.md` (e.g., `talents/flow.md`, `talents/meetings.md`) +- App outputs: `talents/_{app}_{agent}.md` (e.g., `talents/_entities_observer.md`) +- JSON output: `talents/{agent}.json` when metadata specifies `"output": "json"` Each generator type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form. diff --git a/tests/fixtures/journal/agents/20231113.jsonl b/tests/fixtures/journal/agents/20231113.jsonl deleted file mode 100644 index 9669989c8..000000000 --- a/tests/fixtures/journal/agents/20231113.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent_id": "1699900000001", "name": "entities", "day": "20231113", "facet": "personal", "ts": 1699900000001, "status": "completed", "runtime_seconds": 8.4, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "daily"} -{"agent_id": "1699900000002", "name": "flow", "day": "20231113", "facet": null, "ts": 1699900060000, "status": "completed", "runtime_seconds": 4.7, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"} diff --git a/tests/fixtures/journal/agents/20231114.jsonl b/tests/fixtures/journal/agents/20231114.jsonl deleted file mode 100644 index 5fe91211a..000000000 --- a/tests/fixtures/journal/agents/20231114.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"agent_id": "1700000000001", "name": "default", "day": "20231114", "facet": null, "ts": 1700000000001, "status": "completed", "runtime_seconds": 0.6, "provider": "openai", "model": "gpt-4o", "schedule": "daily"} -{"agent_id": "1700000000002", "name": "flow", "day": "20231114", "facet": null, "ts": 1700000060000, "status": "error", "runtime_seconds": 13.2, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"} -{"agent_id": "1700000000003", "name": "activity", "day": "20231114", "facet": "work", "ts": 1700000120000, "status": "completed", "runtime_seconds": 6.2, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "activity"} -{"agent_id": "1700000000004", "name": "default", "day": "20231114", "facet": null, "ts": 1700000180000, "status": "completed", "runtime_seconds": 2.1, "provider": "openai", "model": "gpt-4o"} diff --git a/tests/fixtures/journal/agents/20260304.jsonl b/tests/fixtures/journal/agents/20260304.jsonl deleted file mode 100644 index 768a5c9a4..000000000 --- a/tests/fixtures/journal/agents/20260304.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"agent_id": "1772640000001", "name": "flow", "day": "20260304", "facet": null, "ts": 1772676000000, "status": "completed", "runtime_seconds": 5.2, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772640000002", "name": "meetings", "day": "20260304", "facet": null, "ts": 1772676060000, "status": "completed", "runtime_seconds": 3.1, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} -{"agent_id": "1772640000003", "name": "knowledge_graph", "day": "20260304", "facet": null, "ts": 1772676120000, "status": "completed", "runtime_seconds": 8.7, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/agents/20260305.jsonl b/tests/fixtures/journal/agents/20260305.jsonl deleted file mode 100644 index 5c5802631..000000000 --- a/tests/fixtures/journal/agents/20260305.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"agent_id": "1772726400001", "name": "flow", "day": "20260305", "facet": null, "ts": 1772762400000, "status": "completed", "runtime_seconds": 4.8, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772726400002", "name": "meetings", "day": "20260305", "facet": null, "ts": 1772762460000, "status": "completed", "runtime_seconds": 2.9, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} -{"agent_id": "1772737200001", "name": "default", "day": "20260305", "facet": "verona", "ts": 1772737200000, "status": "completed", "runtime_seconds": 12.3, "provider": "openai", "model": "gpt-4o", "schedule": "segment"} diff --git a/tests/fixtures/journal/agents/20260306.jsonl b/tests/fixtures/journal/agents/20260306.jsonl deleted file mode 100644 index a95d3fc76..000000000 --- a/tests/fixtures/journal/agents/20260306.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent_id": "1772812800001", "name": "flow", "day": "20260306", "facet": null, "ts": 1772848800000, "status": "completed", "runtime_seconds": 5.5, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772812800002", "name": "knowledge_graph", "day": "20260306", "facet": null, "ts": 1772848860000, "status": "completed", "runtime_seconds": 9.1, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/agents/20260307.jsonl b/tests/fixtures/journal/agents/20260307.jsonl deleted file mode 100644 index 78a16c176..000000000 --- a/tests/fixtures/journal/agents/20260307.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent_id": "1772899200001", "name": "flow", "day": "20260307", "facet": null, "ts": 1772935200000, "status": "completed", "runtime_seconds": 6.1, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772899200002", "name": "meetings", "day": "20260307", "facet": null, "ts": 1772935260000, "status": "completed", "runtime_seconds": 3.4, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} diff --git a/tests/fixtures/journal/agents/20260308.jsonl b/tests/fixtures/journal/agents/20260308.jsonl deleted file mode 100644 index dcadbe560..000000000 --- a/tests/fixtures/journal/agents/20260308.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"agent_id": "1772985600001", "name": "flow", "day": "20260308", "facet": null, "ts": 1773021600000, "status": "completed", "runtime_seconds": 4.9, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772985600002", "name": "meetings", "day": "20260308", "facet": null, "ts": 1773021660000, "status": "completed", "runtime_seconds": 2.7, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} -{"agent_id": "1772985600003", "name": "knowledge_graph", "day": "20260308", "facet": null, "ts": 1773021720000, "status": "completed", "runtime_seconds": 7.8, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/agents/20260309.jsonl b/tests/fixtures/journal/agents/20260309.jsonl deleted file mode 100644 index 151f1244b..000000000 --- a/tests/fixtures/journal/agents/20260309.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"agent_id": "1773072000001", "name": "flow", "day": "20260309", "facet": null, "ts": 1773108000000, "status": "completed", "runtime_seconds": 5.3, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} diff --git a/tests/fixtures/journal/agents/20260310.jsonl b/tests/fixtures/journal/agents/20260310.jsonl deleted file mode 100644 index a421d4841..000000000 --- a/tests/fixtures/journal/agents/20260310.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"agent_id": "1773158400001", "name": "flow", "day": "20260310", "facet": null, "ts": 1773194400000, "status": "completed", "runtime_seconds": 6.4, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1773158400002", "name": "meetings", "day": "20260310", "facet": null, "ts": 1773194460000, "status": "completed", "runtime_seconds": 3.8, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} -{"agent_id": "1773158400003", "name": "knowledge_graph", "day": "20260310", "facet": null, "ts": 1773194520000, "status": "completed", "runtime_seconds": 10.2, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} -{"agent_id": "1773187200001", "name": "default", "day": "20260310", "facet": "verona", "ts": 1773187200000, "status": "completed", "runtime_seconds": 15.7, "provider": "openai", "model": "gpt-4o", "schedule": "segment"} diff --git a/tests/fixtures/journal/agents/default.log b/tests/fixtures/journal/agents/default.log deleted file mode 120000 index 11a1fd824..000000000 --- a/tests/fixtures/journal/agents/default.log +++ /dev/null @@ -1 +0,0 @@ -default/1700000000001.jsonl \ No newline at end of file diff --git a/tests/fixtures/journal/agents/flow/1700000000002.jsonl b/tests/fixtures/journal/agents/flow/1700000000002.jsonl deleted file mode 100644 index fbac917b9..000000000 --- a/tests/fixtures/journal/agents/flow/1700000000002.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"event": "request", "ts": 1700000060000, "agent_id": "1700000000002", "prompt": "Analyze conversation flow", "name": "flow", "provider": "anthropic"} -{"event": "start", "prompt": "Analyze conversation flow", "name": "flow", "model": "claude-3-haiku", "provider": "anthropic", "ts": 1700000060100, "agent_id": "1700000000002"} -{"event": "error", "ts": 1700000060200, "agent_id": "1700000000002", "error": "Rate limit exceeded: too many requests"} diff --git a/tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/audio.md b/tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/screen.md b/tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20240101/agents/flow.md b/tests/fixtures/journal/chronicle/20240101/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/agents/flow.md rename to tests/fixtures/journal/chronicle/20240101/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20240101/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20240101/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20240101/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20240101/agents/meetings.md b/tests/fixtures/journal/chronicle/20240101/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/agents/meetings.md rename to tests/fixtures/journal/chronicle/20240101/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/audio.md b/tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/screen.md b/tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20240102/agents/flow.md b/tests/fixtures/journal/chronicle/20240102/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240102/agents/flow.md rename to tests/fixtures/journal/chronicle/20240102/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speaker_labels.json b/tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speaker_labels.json similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speaker_labels.json rename to tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speaker_labels.json diff --git a/tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speakers.json b/tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speakers.json similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speakers.json rename to tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speakers.json diff --git a/tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260304/agents/flow.md b/tests/fixtures/journal/chronicle/20260304/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/agents/flow.md rename to tests/fixtures/journal/chronicle/20260304/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260304/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260304/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260304/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20260304/agents/meetings.md b/tests/fixtures/journal/chronicle/20260304/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260304/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260305/agents/flow.md b/tests/fixtures/journal/chronicle/20260305/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/agents/flow.md rename to tests/fixtures/journal/chronicle/20260305/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260305/agents/meetings.md b/tests/fixtures/journal/chronicle/20260305/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260305/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260306/agents/flow.md b/tests/fixtures/journal/chronicle/20260306/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/agents/flow.md rename to tests/fixtures/journal/chronicle/20260306/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260306/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260306/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260306/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20260307/default/100000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260307/default/100000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260307/default/100000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260307/default/100000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260307/default/150000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260307/default/150000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260307/default/150000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260307/default/150000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260307/agents/flow.md b/tests/fixtures/journal/chronicle/20260307/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260307/agents/flow.md rename to tests/fixtures/journal/chronicle/20260307/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260307/agents/meetings.md b/tests/fixtures/journal/chronicle/20260307/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260307/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260307/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260308/default/100000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260308/default/100000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/default/100000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260308/default/100000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260308/default/153000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260308/default/153000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/default/153000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260308/default/153000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260308/agents/flow.md b/tests/fixtures/journal/chronicle/20260308/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/agents/flow.md rename to tests/fixtures/journal/chronicle/20260308/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260308/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260308/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260308/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20260308/agents/meetings.md b/tests/fixtures/journal/chronicle/20260308/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260308/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260309/agents/flow.md b/tests/fixtures/journal/chronicle/20260309/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/agents/flow.md rename to tests/fixtures/journal/chronicle/20260309/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/100000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260310/default/100000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/100000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260310/default/100000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260310/agents/flow.md b/tests/fixtures/journal/chronicle/20260310/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/agents/flow.md rename to tests/fixtures/journal/chronicle/20260310/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260310/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260310/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260310/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20260310/agents/meetings.md b/tests/fixtures/journal/chronicle/20260310/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260310/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260415/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260415/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260415/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260415/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/facets/capulet/events/20260304.jsonl b/tests/fixtures/journal/facets/capulet/events/20260304.jsonl index 400666264..f686fd58b 100644 --- a/tests/fixtures/journal/facets/capulet/events/20260304.jsonl +++ b/tests/fixtures/journal/facets/capulet/events/20260304.jsonl @@ -1,2 +1,2 @@ -{"type": "conference", "start": "09:00:00", "end": "10:00:00", "title": "Denver Tech Summit - Juliet's Keynote", "summary": "Juliet presented on unified API gateways", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260304/agents/flow.md", "participants": ["Juliet Capulet"], "work": true, "details": "Standing ovation for architecture presentation"} -{"type": "social", "start": "18:00:00", "end": "20:00:00", "title": "Conference Mixer", "summary": "Networking event", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260304/agents/flow.md", "participants": ["Juliet Capulet", "Romeo Montague"], "work": false, "details": "Juliet and Romeo exchanged Signal contacts"} +{"type": "conference", "start": "09:00:00", "end": "10:00:00", "title": "Denver Tech Summit - Juliet's Keynote", "summary": "Juliet presented on unified API gateways", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260304/talents/flow.md", "participants": ["Juliet Capulet"], "work": true, "details": "Standing ovation for architecture presentation"} +{"type": "social", "start": "18:00:00", "end": "20:00:00", "title": "Conference Mixer", "summary": "Networking event", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260304/talents/flow.md", "participants": ["Juliet Capulet", "Romeo Montague"], "work": false, "details": "Juliet and Romeo exchanged Signal contacts"} diff --git a/tests/fixtures/journal/facets/capulet/events/20260306.jsonl b/tests/fixtures/journal/facets/capulet/events/20260306.jsonl index 2aaefa066..71b4298c8 100644 --- a/tests/fixtures/journal/facets/capulet/events/20260306.jsonl +++ b/tests/fixtures/journal/facets/capulet/events/20260306.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "11:00:00", "end": "12:00:00", "title": "Capulet Industries Board Meeting", "summary": "Tybalt pitched competing proposal", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260306/agents/flow.md", "participants": ["Tybalt Capulet"], "work": true, "details": "Proposed building mesh routing from scratch"} +{"type": "meeting", "start": "11:00:00", "end": "12:00:00", "title": "Capulet Industries Board Meeting", "summary": "Tybalt pitched competing proposal", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260306/talents/flow.md", "participants": ["Tybalt Capulet"], "work": true, "details": "Proposed building mesh routing from scratch"} diff --git a/tests/fixtures/journal/facets/capulet/events/20260307.jsonl b/tests/fixtures/journal/facets/capulet/events/20260307.jsonl index c031a5e00..6ccba2fba 100644 --- a/tests/fixtures/journal/facets/capulet/events/20260307.jsonl +++ b/tests/fixtures/journal/facets/capulet/events/20260307.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "10:30:00", "title": "Tybalt Confrontation Call", "summary": "Tybalt discovered secret collaboration", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260307/agents/flow.md", "participants": ["Tybalt Capulet", "Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Terminated Mercutio's consulting contract"} +{"type": "meeting", "start": "10:00:00", "end": "10:30:00", "title": "Tybalt Confrontation Call", "summary": "Tybalt discovered secret collaboration", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260307/talents/flow.md", "participants": ["Tybalt Capulet", "Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Terminated Mercutio's consulting contract"} diff --git a/tests/fixtures/journal/facets/capulet/events/20260310.jsonl b/tests/fixtures/journal/facets/capulet/events/20260310.jsonl index f032a38c7..7f40d15aa 100644 --- a/tests/fixtures/journal/facets/capulet/events/20260310.jsonl +++ b/tests/fixtures/journal/facets/capulet/events/20260310.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Meeting", "summary": "Verona Platform approved as joint venture", "facet": "capulet", "agent": "meetings", "occurred": true, "source": "20260310/agents/meetings.md", "participants": ["Juliet Capulet", "Romeo Montague", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Juliet named co-lead"} +{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Meeting", "summary": "Verona Platform approved as joint venture", "facet": "capulet", "agent": "meetings", "occurred": true, "source": "20260310/talents/meetings.md", "participants": ["Juliet Capulet", "Romeo Montague", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Juliet named co-lead"} diff --git a/tests/fixtures/journal/facets/montague/events/20260304.jsonl b/tests/fixtures/journal/facets/montague/events/20260304.jsonl index e7e34c75a..09e3cb0c0 100644 --- a/tests/fixtures/journal/facets/montague/events/20260304.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260304.jsonl @@ -1,2 +1,2 @@ -{"type": "conference", "start": "09:00:00", "end": "12:00:00", "title": "Denver Tech Summit - Morning Keynote", "summary": "Conference keynote featuring Juliet Capulet", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260304/agents/flow.md", "participants": ["Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Attended keynote on unified API gateways"} -{"type": "hackathon", "start": "14:00:00", "end": "18:00:00", "title": "Hackathon - API Bridge Challenge", "summary": "Built API bridge prototype", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260304/agents/flow.md", "participants": ["Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Tybalt confronted Romeo"} +{"type": "conference", "start": "09:00:00", "end": "12:00:00", "title": "Denver Tech Summit - Morning Keynote", "summary": "Conference keynote featuring Juliet Capulet", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260304/talents/flow.md", "participants": ["Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Attended keynote on unified API gateways"} +{"type": "hackathon", "start": "14:00:00", "end": "18:00:00", "title": "Hackathon - API Bridge Challenge", "summary": "Built API bridge prototype", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260304/talents/flow.md", "participants": ["Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Tybalt confronted Romeo"} diff --git a/tests/fixtures/journal/facets/montague/events/20260305.jsonl b/tests/fixtures/journal/facets/montague/events/20260305.jsonl index 5b9b53c27..2c3adc496 100644 --- a/tests/fixtures/journal/facets/montague/events/20260305.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260305.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Montague Tech Daily Standup", "summary": "Team standup", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260305/agents/meetings.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Romeo mentioned conference ideas"} +{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Montague Tech Daily Standup", "summary": "Team standup", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260305/talents/meetings.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Romeo mentioned conference ideas"} diff --git a/tests/fixtures/journal/facets/montague/events/20260306.jsonl b/tests/fixtures/journal/facets/montague/events/20260306.jsonl index 785030182..8bb73076d 100644 --- a/tests/fixtures/journal/facets/montague/events/20260306.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260306.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "09:30:00", "end": "10:00:00", "title": "Montague Tech Daily Standup", "summary": "Team standup with Benvolio's questions", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260306/agents/flow.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Benvolio noticed late-night commits"} +{"type": "meeting", "start": "09:30:00", "end": "10:00:00", "title": "Montague Tech Daily Standup", "summary": "Team standup with Benvolio's questions", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260306/talents/flow.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Benvolio noticed late-night commits"} diff --git a/tests/fixtures/journal/facets/montague/events/20260307.jsonl b/tests/fixtures/journal/facets/montague/events/20260307.jsonl index 0ba76be31..692a86789 100644 --- a/tests/fixtures/journal/facets/montague/events/20260307.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260307.jsonl @@ -1,2 +1,2 @@ -{"type": "meeting", "start": "10:00:00", "end": "10:30:00", "title": "Confrontation with Tybalt", "summary": "Tybalt accused Romeo of IP theft", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260307/agents/flow.md", "participants": ["Romeo Montague", "Tybalt Capulet", "Mercutio Escalus"], "work": true, "details": "Mercutio fired from Capulet contract"} -{"type": "meeting", "start": "15:00:00", "end": "16:00:00", "title": "Emergency Team Meeting", "summary": "Crisis response meeting", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260307/agents/meetings.md", "participants": ["Romeo Montague", "Benvolio Montague"], "work": true, "details": "Discussed legal exposure and mediation plan"} +{"type": "meeting", "start": "10:00:00", "end": "10:30:00", "title": "Confrontation with Tybalt", "summary": "Tybalt accused Romeo of IP theft", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260307/talents/flow.md", "participants": ["Romeo Montague", "Tybalt Capulet", "Mercutio Escalus"], "work": true, "details": "Mercutio fired from Capulet contract"} +{"type": "meeting", "start": "15:00:00", "end": "16:00:00", "title": "Emergency Team Meeting", "summary": "Crisis response meeting", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260307/talents/meetings.md", "participants": ["Romeo Montague", "Benvolio Montague"], "work": true, "details": "Discussed legal exposure and mediation plan"} diff --git a/tests/fixtures/journal/facets/montague/events/20260309.jsonl b/tests/fixtures/journal/facets/montague/events/20260309.jsonl index 852f66857..3065e4670 100644 --- a/tests/fixtures/journal/facets/montague/events/20260309.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260309.jsonl @@ -1 +1 @@ -{"type": "task", "start": "09:00:00", "end": "11:00:00", "title": "Infrastructure Setup", "summary": "Benvolio deployed Kubernetes cluster", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260309/agents/flow.md", "participants": ["Benvolio Montague"], "work": true, "details": "Staging cluster for Verona Platform demo"} +{"type": "task", "start": "09:00:00", "end": "11:00:00", "title": "Infrastructure Setup", "summary": "Benvolio deployed Kubernetes cluster", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260309/talents/flow.md", "participants": ["Benvolio Montague"], "work": true, "details": "Staging cluster for Verona Platform demo"} diff --git a/tests/fixtures/journal/facets/montague/events/20260310.jsonl b/tests/fixtures/journal/facets/montague/events/20260310.jsonl index 430237595..7d77834ba 100644 --- a/tests/fixtures/journal/facets/montague/events/20260310.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260310.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Meeting", "summary": "Verona Platform presentation to both boards", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260310/agents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards approved joint venture"} +{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Meeting", "summary": "Verona Platform presentation to both boards", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260310/talents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards approved joint venture"} diff --git a/tests/fixtures/journal/facets/personal/events/20240101.jsonl b/tests/fixtures/journal/facets/personal/events/20240101.jsonl index 83bbd10a0..c7b92d7ce 100644 --- a/tests/fixtures/journal/facets/personal/events/20240101.jsonl +++ b/tests/fixtures/journal/facets/personal/events/20240101.jsonl @@ -1 +1 @@ -{"type": "appointment", "start": "18:00:00", "end": "19:00:00", "title": "Gym session", "summary": "Evening workout", "facet": "personal", "agent": "activity", "occurred": true, "source": "20240101/agents/activity.md", "participants": [], "work": false, "details": "Strength training day"} +{"type": "appointment", "start": "18:00:00", "end": "19:00:00", "title": "Gym session", "summary": "Evening workout", "facet": "personal", "agent": "activity", "occurred": true, "source": "20240101/talents/activity.md", "participants": [], "work": false, "details": "Strength training day"} diff --git a/tests/fixtures/journal/facets/verona/events/20260305.jsonl b/tests/fixtures/journal/facets/verona/events/20260305.jsonl index 6bafb2a4e..6e4fab100 100644 --- a/tests/fixtures/journal/facets/verona/events/20260305.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260305.jsonl @@ -1 +1 @@ -{"type": "task", "start": "22:00:00", "end": "23:59:00", "title": "Balcony App Prototype Session", "summary": "Late night coding session", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260305/agents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Created Balcony App with sub-ms latency"} +{"type": "task", "start": "22:00:00", "end": "23:59:00", "title": "Balcony App Prototype Session", "summary": "Late night coding session", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260305/talents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Created Balcony App with sub-ms latency"} diff --git a/tests/fixtures/journal/facets/verona/events/20260306.jsonl b/tests/fixtures/journal/facets/verona/events/20260306.jsonl index 87b7592fa..cf0d2be68 100644 --- a/tests/fixtures/journal/facets/verona/events/20260306.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260306.jsonl @@ -1 +1 @@ -{"type": "task", "start": "14:30:00", "end": "19:30:00", "title": "Verona Platform Integration", "summary": "End-to-end integration work", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260306/agents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Full e2e integration achieved"} +{"type": "task", "start": "14:30:00", "end": "19:30:00", "title": "Verona Platform Integration", "summary": "End-to-end integration work", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260306/talents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Full e2e integration achieved"} diff --git a/tests/fixtures/journal/facets/verona/events/20260308.jsonl b/tests/fixtures/journal/facets/verona/events/20260308.jsonl index 286ee8530..cdd55a574 100644 --- a/tests/fixtures/journal/facets/verona/events/20260308.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260308.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "11:00:00", "title": "Strategy Call with Professor Lawrence", "summary": "Joint venture strategy planning", "facet": "verona", "agent": "meetings", "occurred": true, "source": "20260308/agents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence"], "work": true, "details": "Proposed board presentation strategy"} +{"type": "meeting", "start": "10:00:00", "end": "11:00:00", "title": "Strategy Call with Professor Lawrence", "summary": "Joint venture strategy planning", "facet": "verona", "agent": "meetings", "occurred": true, "source": "20260308/talents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence"], "work": true, "details": "Proposed board presentation strategy"} diff --git a/tests/fixtures/journal/facets/verona/events/20260309.jsonl b/tests/fixtures/journal/facets/verona/events/20260309.jsonl index b54aa4c4a..711fc9949 100644 --- a/tests/fixtures/journal/facets/verona/events/20260309.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260309.jsonl @@ -1 +1 @@ -{"type": "task", "start": "09:00:00", "end": "21:00:00", "title": "Demo Sprint Day", "summary": "Full day preparing board demo", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260309/agents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet", "Benvolio Montague", "Nurse Angela"], "work": true, "details": "Demo deployed, presentation rehearsed"} +{"type": "task", "start": "09:00:00", "end": "21:00:00", "title": "Demo Sprint Day", "summary": "Full day preparing board demo", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260309/talents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet", "Benvolio Montague", "Nurse Angela"], "work": true, "details": "Demo deployed, presentation rehearsed"} diff --git a/tests/fixtures/journal/facets/verona/events/20260310.jsonl b/tests/fixtures/journal/facets/verona/events/20260310.jsonl index 2392e5397..ca18c46d6 100644 --- a/tests/fixtures/journal/facets/verona/events/20260310.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260310.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Presentation", "summary": "Verona Platform approved", "facet": "verona", "agent": "meetings", "occurred": true, "source": "20260310/agents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards voted yes"} +{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Presentation", "summary": "Verona Platform approved", "facet": "verona", "agent": "meetings", "occurred": true, "source": "20260310/talents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards voted yes"} diff --git a/tests/fixtures/journal/facets/work/events/20240101.jsonl b/tests/fixtures/journal/facets/work/events/20240101.jsonl index 0a0015e41..c2bcece97 100644 --- a/tests/fixtures/journal/facets/work/events/20240101.jsonl +++ b/tests/fixtures/journal/facets/work/events/20240101.jsonl @@ -1,2 +1,2 @@ -{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team standup", "summary": "Daily sync meeting", "facet": "work", "agent": "meetings", "occurred": true, "source": "20240101/agents/meetings.md", "participants": ["Alice", "Bob"], "work": true, "details": "Discussed sprint progress"} -{"type": "task", "start": "10:00:00", "end": "12:00:00", "title": "Code review", "summary": "Review PR #123", "facet": "work", "agent": "activity", "occurred": true, "source": "20240101/agents/activity.md", "participants": [], "work": true, "details": "Reviewed authentication changes"} +{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team standup", "summary": "Daily sync meeting", "facet": "work", "agent": "meetings", "occurred": true, "source": "20240101/talents/meetings.md", "participants": ["Alice", "Bob"], "work": true, "details": "Discussed sprint progress"} +{"type": "task", "start": "10:00:00", "end": "12:00:00", "title": "Code review", "summary": "Review PR #123", "facet": "work", "agent": "activity", "occurred": true, "source": "20240101/talents/activity.md", "participants": [], "work": true, "details": "Reviewed authentication changes"} diff --git a/tests/fixtures/journal/facets/work/events/20240105.jsonl b/tests/fixtures/journal/facets/work/events/20240105.jsonl index 832393e7e..fd1813909 100644 --- a/tests/fixtures/journal/facets/work/events/20240105.jsonl +++ b/tests/fixtures/journal/facets/work/events/20240105.jsonl @@ -1 +1 @@ -{"type": "meeting", "date": "2024-01-05", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "agent": "schedule", "occurred": false, "source": "20240101/agents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"} +{"type": "meeting", "date": "2024-01-05", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "agent": "schedule", "occurred": false, "source": "20240101/talents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"} diff --git a/tests/fixtures/journal/maint/agents/000_migrate_agent_layout.jsonl b/tests/fixtures/journal/maint/sol/000_migrate_agent_layout.jsonl similarity index 66% rename from tests/fixtures/journal/maint/agents/000_migrate_agent_layout.jsonl rename to tests/fixtures/journal/maint/sol/000_migrate_agent_layout.jsonl index 0ac26cdff..71e3c662d 100644 --- a/tests/fixtures/journal/maint/agents/000_migrate_agent_layout.jsonl +++ b/tests/fixtures/journal/maint/sol/000_migrate_agent_layout.jsonl @@ -1,4 +1,4 @@ -{"event": "exec", "ts": 1770778598011, "app": "agents", "task": "000_migrate_agent_layout", "cmd": ["/home/jer/projects/sunstone/.venv/bin/python3", "-m", "apps.agents.maint.000_migrate_agent_layout"]} +{"event": "exec", "ts": 1770778598011, "app": "sol", "task": "000_migrate_agent_layout", "cmd": ["/home/jer/projects/sunstone/.venv/bin/python3", "-m", "apps.sol.maint.000_migrate_agent_layout"]} {"event": "line", "ts": 1770778598147, "line": "Migration complete"} {"event": "line", "ts": 1770778598147, "line": " moved: 0"} {"event": "line", "ts": 1770778598147, "line": " cleaned: 0"} diff --git a/tests/fixtures/journal/maint/agents/001_migrate_agent_run_logs.jsonl b/tests/fixtures/journal/maint/sol/001_migrate_agent_run_logs.jsonl similarity index 74% rename from tests/fixtures/journal/maint/agents/001_migrate_agent_run_logs.jsonl rename to tests/fixtures/journal/maint/sol/001_migrate_agent_run_logs.jsonl index 04c41f556..84bfa8260 100644 --- a/tests/fixtures/journal/maint/agents/001_migrate_agent_run_logs.jsonl +++ b/tests/fixtures/journal/maint/sol/001_migrate_agent_run_logs.jsonl @@ -1,4 +1,4 @@ -{"event": "exec", "ts": 1770952499902, "app": "agents", "task": "001_migrate_agent_run_logs", "cmd": ["/home/jer/.local/share/hopper/lodes/vymuqdvr/worktree/.venv/bin/python", "-m", "apps.agents.maint.001_migrate_agent_run_logs"]} +{"event": "exec", "ts": 1770952499902, "app": "sol", "task": "001_migrate_agent_run_logs", "cmd": ["/home/jer/.local/share/hopper/lodes/vymuqdvr/worktree/.venv/bin/python", "-m", "apps.sol.maint.001_migrate_agent_run_logs"]} {"event": "line", "ts": 1770952500054, "line": "Migrating agent run logs in: tests/fixtures/journal/agents"} {"event": "line", "ts": 1770952500054, "line": "Migration complete"} {"event": "line", "ts": 1770952500054, "line": " moved: 0"} diff --git a/tests/fixtures/journal/sol/briefing.md b/tests/fixtures/journal/sol/briefing.md index b48957d30..be4318521 100644 --- a/tests/fixtures/journal/sol/briefing.md +++ b/tests/fixtures/journal/sol/briefing.md @@ -36,9 +36,9 @@ gaps: [] ## Forward Look -- **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [anticipation](sol://20260327/agents/anticipation)). +- **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [anticipation](sol://20260327/talents/anticipation)). - **Wednesday** — Deadline for the compliance audit documentation. -- Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [anticipation](sol://20260327/agents/anticipation)). +- Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [anticipation](sol://20260327/talents/anticipation)). ## Reading diff --git a/tests/fixtures/journal/talents/20231113.jsonl b/tests/fixtures/journal/talents/20231113.jsonl new file mode 100644 index 000000000..0877834df --- /dev/null +++ b/tests/fixtures/journal/talents/20231113.jsonl @@ -0,0 +1,2 @@ +{"use_id": "1699900000001", "name": "entities", "day": "20231113", "facet": "personal", "ts": 1699900000001, "status": "completed", "runtime_seconds": 8.4, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "daily"} +{"use_id": "1699900000002", "name": "flow", "day": "20231113", "facet": null, "ts": 1699900060000, "status": "completed", "runtime_seconds": 4.7, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"} diff --git a/tests/fixtures/journal/talents/20231114.jsonl b/tests/fixtures/journal/talents/20231114.jsonl new file mode 100644 index 000000000..aff95e599 --- /dev/null +++ b/tests/fixtures/journal/talents/20231114.jsonl @@ -0,0 +1,4 @@ +{"use_id": "1700000000001", "name": "default", "day": "20231114", "facet": null, "ts": 1700000000001, "status": "completed", "runtime_seconds": 0.6, "provider": "openai", "model": "gpt-4o", "schedule": "daily"} +{"use_id": "1700000000002", "name": "flow", "day": "20231114", "facet": null, "ts": 1700000060000, "status": "error", "runtime_seconds": 13.2, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"} +{"use_id": "1700000000003", "name": "activity", "day": "20231114", "facet": "work", "ts": 1700000120000, "status": "completed", "runtime_seconds": 6.2, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "activity"} +{"use_id": "1700000000004", "name": "default", "day": "20231114", "facet": null, "ts": 1700000180000, "status": "completed", "runtime_seconds": 2.1, "provider": "openai", "model": "gpt-4o"} diff --git a/tests/fixtures/journal/talents/20260304.jsonl b/tests/fixtures/journal/talents/20260304.jsonl new file mode 100644 index 000000000..800e77d28 --- /dev/null +++ b/tests/fixtures/journal/talents/20260304.jsonl @@ -0,0 +1,3 @@ +{"use_id": "1772640000001", "name": "flow", "day": "20260304", "facet": null, "ts": 1772676000000, "status": "completed", "runtime_seconds": 5.2, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772640000002", "name": "meetings", "day": "20260304", "facet": null, "ts": 1772676060000, "status": "completed", "runtime_seconds": 3.1, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} +{"use_id": "1772640000003", "name": "knowledge_graph", "day": "20260304", "facet": null, "ts": 1772676120000, "status": "completed", "runtime_seconds": 8.7, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/talents/20260305.jsonl b/tests/fixtures/journal/talents/20260305.jsonl new file mode 100644 index 000000000..b09e35a9e --- /dev/null +++ b/tests/fixtures/journal/talents/20260305.jsonl @@ -0,0 +1,3 @@ +{"use_id": "1772726400001", "name": "flow", "day": "20260305", "facet": null, "ts": 1772762400000, "status": "completed", "runtime_seconds": 4.8, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772726400002", "name": "meetings", "day": "20260305", "facet": null, "ts": 1772762460000, "status": "completed", "runtime_seconds": 2.9, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} +{"use_id": "1772737200001", "name": "default", "day": "20260305", "facet": "verona", "ts": 1772737200000, "status": "completed", "runtime_seconds": 12.3, "provider": "openai", "model": "gpt-4o", "schedule": "segment"} diff --git a/tests/fixtures/journal/talents/20260306.jsonl b/tests/fixtures/journal/talents/20260306.jsonl new file mode 100644 index 000000000..c4f08949b --- /dev/null +++ b/tests/fixtures/journal/talents/20260306.jsonl @@ -0,0 +1,2 @@ +{"use_id": "1772812800001", "name": "flow", "day": "20260306", "facet": null, "ts": 1772848800000, "status": "completed", "runtime_seconds": 5.5, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772812800002", "name": "knowledge_graph", "day": "20260306", "facet": null, "ts": 1772848860000, "status": "completed", "runtime_seconds": 9.1, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/talents/20260307.jsonl b/tests/fixtures/journal/talents/20260307.jsonl new file mode 100644 index 000000000..da09fabc1 --- /dev/null +++ b/tests/fixtures/journal/talents/20260307.jsonl @@ -0,0 +1,2 @@ +{"use_id": "1772899200001", "name": "flow", "day": "20260307", "facet": null, "ts": 1772935200000, "status": "completed", "runtime_seconds": 6.1, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772899200002", "name": "meetings", "day": "20260307", "facet": null, "ts": 1772935260000, "status": "completed", "runtime_seconds": 3.4, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} diff --git a/tests/fixtures/journal/talents/20260308.jsonl b/tests/fixtures/journal/talents/20260308.jsonl new file mode 100644 index 000000000..d7d0d8b9e --- /dev/null +++ b/tests/fixtures/journal/talents/20260308.jsonl @@ -0,0 +1,3 @@ +{"use_id": "1772985600001", "name": "flow", "day": "20260308", "facet": null, "ts": 1773021600000, "status": "completed", "runtime_seconds": 4.9, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772985600002", "name": "meetings", "day": "20260308", "facet": null, "ts": 1773021660000, "status": "completed", "runtime_seconds": 2.7, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} +{"use_id": "1772985600003", "name": "knowledge_graph", "day": "20260308", "facet": null, "ts": 1773021720000, "status": "completed", "runtime_seconds": 7.8, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/talents/20260309.jsonl b/tests/fixtures/journal/talents/20260309.jsonl new file mode 100644 index 000000000..7b099008d --- /dev/null +++ b/tests/fixtures/journal/talents/20260309.jsonl @@ -0,0 +1 @@ +{"use_id": "1773072000001", "name": "flow", "day": "20260309", "facet": null, "ts": 1773108000000, "status": "completed", "runtime_seconds": 5.3, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} diff --git a/tests/fixtures/journal/talents/20260310.jsonl b/tests/fixtures/journal/talents/20260310.jsonl new file mode 100644 index 000000000..98d12d0f7 --- /dev/null +++ b/tests/fixtures/journal/talents/20260310.jsonl @@ -0,0 +1,4 @@ +{"use_id": "1773158400001", "name": "flow", "day": "20260310", "facet": null, "ts": 1773194400000, "status": "completed", "runtime_seconds": 6.4, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1773158400002", "name": "meetings", "day": "20260310", "facet": null, "ts": 1773194460000, "status": "completed", "runtime_seconds": 3.8, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} +{"use_id": "1773158400003", "name": "knowledge_graph", "day": "20260310", "facet": null, "ts": 1773194520000, "status": "completed", "runtime_seconds": 10.2, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} +{"use_id": "1773187200001", "name": "default", "day": "20260310", "facet": "verona", "ts": 1773187200000, "status": "completed", "runtime_seconds": 15.7, "provider": "openai", "model": "gpt-4o", "schedule": "segment"} diff --git a/tests/fixtures/journal/talents/default.log b/tests/fixtures/journal/talents/default.log new file mode 100644 index 000000000..c93129833 --- /dev/null +++ b/tests/fixtures/journal/talents/default.log @@ -0,0 +1,7 @@ +{"event": "request", "ts": 1700000000001, "use_id": "1700000000001", "prompt": "Search for meetings about project updates", "name": "default", "provider": "openai"} +{"event": "start", "prompt": "Search for meetings about project updates", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1700000000100, "use_id": "1700000000001"} +{"event": "talent_updated", "talent": "solstone", "ts": 1700000000200, "use_id": "1700000000001"} +{"event": "thinking", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "ts": 1700000000300, "use_id": "1700000000001"} +{"event": "tool_start", "tool": "search_events", "args": {"query": "project updates", "limit": 5}, "call_id": "call_001", "ts": 1700000000400, "use_id": "1700000000001"} +{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", "call_id": "call_001", "ts": 1700000000500, "use_id": "1700000000001"} +{"event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, "use_id": "1700000000001", "usage": {"input_tokens": 150, "output_tokens": 80}} diff --git a/tests/fixtures/journal/agents/default/1700000000001.jsonl b/tests/fixtures/journal/talents/default/1700000000001.jsonl similarity index 55% rename from tests/fixtures/journal/agents/default/1700000000001.jsonl rename to tests/fixtures/journal/talents/default/1700000000001.jsonl index c45cc665e..c93129833 100644 --- a/tests/fixtures/journal/agents/default/1700000000001.jsonl +++ b/tests/fixtures/journal/talents/default/1700000000001.jsonl @@ -1,7 +1,7 @@ -{"event": "request", "ts": 1700000000001, "agent_id": "1700000000001", "prompt": "Search for meetings about project updates", "name": "default", "provider": "openai"} -{"event": "start", "prompt": "Search for meetings about project updates", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1700000000100, "agent_id": "1700000000001"} -{"event": "agent_updated", "agent": "solstone", "ts": 1700000000200, "agent_id": "1700000000001"} -{"event": "thinking", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "ts": 1700000000300, "agent_id": "1700000000001"} -{"event": "tool_start", "tool": "search_events", "args": {"query": "project updates", "limit": 5}, "call_id": "call_001", "ts": 1700000000400, "agent_id": "1700000000001"} -{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", "call_id": "call_001", "ts": 1700000000500, "agent_id": "1700000000001"} -{"event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, "agent_id": "1700000000001", "usage": {"input_tokens": 150, "output_tokens": 80}} +{"event": "request", "ts": 1700000000001, "use_id": "1700000000001", "prompt": "Search for meetings about project updates", "name": "default", "provider": "openai"} +{"event": "start", "prompt": "Search for meetings about project updates", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1700000000100, "use_id": "1700000000001"} +{"event": "talent_updated", "talent": "solstone", "ts": 1700000000200, "use_id": "1700000000001"} +{"event": "thinking", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "ts": 1700000000300, "use_id": "1700000000001"} +{"event": "tool_start", "tool": "search_events", "args": {"query": "project updates", "limit": 5}, "call_id": "call_001", "ts": 1700000000400, "use_id": "1700000000001"} +{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", "call_id": "call_001", "ts": 1700000000500, "use_id": "1700000000001"} +{"event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, "use_id": "1700000000001", "usage": {"input_tokens": 150, "output_tokens": 80}} diff --git a/tests/fixtures/journal/agents/default/1772737200001.jsonl b/tests/fixtures/journal/talents/default/1772737200001.jsonl similarity index 62% rename from tests/fixtures/journal/agents/default/1772737200001.jsonl rename to tests/fixtures/journal/talents/default/1772737200001.jsonl index aaed98e5e..3129eec87 100644 --- a/tests/fixtures/journal/agents/default/1772737200001.jsonl +++ b/tests/fixtures/journal/talents/default/1772737200001.jsonl @@ -1,7 +1,7 @@ -{"event": "request", "ts": 1772737200000, "agent_id": "1772737200001", "prompt": "What do you think about collaborating with Capulet Industries on a unified API gateway?", "name": "default", "provider": "openai"} -{"event": "start", "prompt": "What do you think about collaborating with Capulet Industries on a unified API gateway?", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1772737200100, "agent_id": "1772737200001"} -{"event": "agent_updated", "agent": "solstone", "ts": 1772737200200, "agent_id": "1772737200001"} -{"event": "thinking", "content": "The user is asking about a potential collaboration between competing companies. I should consider the technical merits and business implications.", "ts": 1772737200300, "agent_id": "1772737200001"} -{"event": "tool_start", "tool": "search_entities", "args": {"query": "Capulet Industries API", "limit": 5}, "call_id": "call_rj001", "ts": 1772737200400, "agent_id": "1772737200001"} -{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 1, \"results\": [{\"name\": \"Capulet Industries\", \"type\": \"Company\", \"description\": \"Enterprise software company\"}]}", "call_id": "call_rj001", "ts": 1772737200500, "agent_id": "1772737200001"} -{"event": "finish", "result": "A collaboration between Montague Tech and Capulet Industries on a unified API gateway could be transformative. Their enterprise schema translation combined with your mesh routing would create a product neither could build alone. However, the business politics will be the real challenge. I suggest starting with a proof of concept before approaching either board.", "ts": 1772737200600, "agent_id": "1772737200001", "usage": {"input_tokens": 245, "output_tokens": 156}} +{"event": "request", "ts": 1772737200000, "use_id": "1772737200001", "prompt": "What do you think about collaborating with Capulet Industries on a unified API gateway?", "name": "default", "provider": "openai"} +{"event": "start", "prompt": "What do you think about collaborating with Capulet Industries on a unified API gateway?", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1772737200100, "use_id": "1772737200001"} +{"event": "talent_updated", "talent": "solstone", "ts": 1772737200200, "use_id": "1772737200001"} +{"event": "thinking", "content": "The user is asking about a potential collaboration between competing companies. I should consider the technical merits and business implications.", "ts": 1772737200300, "use_id": "1772737200001"} +{"event": "tool_start", "tool": "search_entities", "args": {"query": "Capulet Industries API", "limit": 5}, "call_id": "call_rj001", "ts": 1772737200400, "use_id": "1772737200001"} +{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 1, \"results\": [{\"name\": \"Capulet Industries\", \"type\": \"Company\", \"description\": \"Enterprise software company\"}]}", "call_id": "call_rj001", "ts": 1772737200500, "use_id": "1772737200001"} +{"event": "finish", "result": "A collaboration between Montague Tech and Capulet Industries on a unified API gateway could be transformative. Their enterprise schema translation combined with your mesh routing would create a product neither could build alone. However, the business politics will be the real challenge. I suggest starting with a proof of concept before approaching either board.", "ts": 1772737200600, "use_id": "1772737200001", "usage": {"input_tokens": 245, "output_tokens": 156}} diff --git a/tests/fixtures/journal/agents/default/1773187200001.jsonl b/tests/fixtures/journal/talents/default/1773187200001.jsonl similarity index 64% rename from tests/fixtures/journal/agents/default/1773187200001.jsonl rename to tests/fixtures/journal/talents/default/1773187200001.jsonl index 29f3b5494..360a6c0e5 100644 --- a/tests/fixtures/journal/agents/default/1773187200001.jsonl +++ b/tests/fixtures/journal/talents/default/1773187200001.jsonl @@ -1,5 +1,5 @@ -{"event": "request", "ts": 1773187200000, "agent_id": "1773187200001", "prompt": "We just got board approval for the Verona Platform joint venture! What should our first priorities be?", "name": "default", "provider": "openai"} -{"event": "start", "prompt": "We just got board approval for the Verona Platform joint venture! What should our first priorities be?", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1773187200100, "agent_id": "1773187200001"} -{"event": "agent_updated", "agent": "solstone", "ts": 1773187200200, "agent_id": "1773187200001"} -{"event": "thinking", "content": "The joint venture has been approved. I should recommend practical next steps for launching the Verona Platform as a formal business unit.", "ts": 1773187200300, "agent_id": "1773187200001"} -{"event": "finish", "result": "Congratulations on the approval! Here are your first priorities:\n\n1. **Security Audit** - Mercutio should start with a comprehensive security review of the platform\n2. **Legal Framework** - Formalize IP sharing agreements between both companies\n3. **Team Building** - Recruit cross-company engineering team members\n4. **Client Pilot** - Identify 2-3 enterprise clients for a pilot program\n5. **Infrastructure** - Migrate from staging to production-grade infrastructure\n\nThe demo showed the technical viability. Now focus on making it production-ready.", "ts": 1773187200600, "agent_id": "1773187200001", "usage": {"input_tokens": 312, "output_tokens": 198}} +{"event": "request", "ts": 1773187200000, "use_id": "1773187200001", "prompt": "We just got board approval for the Verona Platform joint venture! What should our first priorities be?", "name": "default", "provider": "openai"} +{"event": "start", "prompt": "We just got board approval for the Verona Platform joint venture! What should our first priorities be?", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1773187200100, "use_id": "1773187200001"} +{"event": "talent_updated", "talent": "solstone", "ts": 1773187200200, "use_id": "1773187200001"} +{"event": "thinking", "content": "The joint venture has been approved. I should recommend practical next steps for launching the Verona Platform as a formal business unit.", "ts": 1773187200300, "use_id": "1773187200001"} +{"event": "finish", "result": "Congratulations on the approval! Here are your first priorities:\n\n1. **Security Audit** - Mercutio should start with a comprehensive security review of the platform\n2. **Legal Framework** - Formalize IP sharing agreements between both companies\n3. **Team Building** - Recruit cross-company engineering team members\n4. **Client Pilot** - Identify 2-3 enterprise clients for a pilot program\n5. **Infrastructure** - Migrate from staging to production-grade infrastructure\n\nThe demo showed the technical viability. Now focus on making it production-ready.", "ts": 1773187200600, "use_id": "1773187200001", "usage": {"input_tokens": 312, "output_tokens": 198}} diff --git a/tests/fixtures/journal/talents/flow/1700000000002.jsonl b/tests/fixtures/journal/talents/flow/1700000000002.jsonl new file mode 100644 index 000000000..36c1fea97 --- /dev/null +++ b/tests/fixtures/journal/talents/flow/1700000000002.jsonl @@ -0,0 +1,3 @@ +{"event": "request", "ts": 1700000060000, "use_id": "1700000000002", "prompt": "Analyze conversation flow", "name": "flow", "provider": "anthropic"} +{"event": "start", "prompt": "Analyze conversation flow", "name": "flow", "model": "claude-3-haiku", "provider": "anthropic", "ts": 1700000060100, "use_id": "1700000000002"} +{"event": "error", "ts": 1700000060200, "use_id": "1700000000002", "error": "Rate limit exceeded: too many requests"} diff --git a/tests/integration/test_anthropic_provider.py b/tests/integration/test_anthropic_provider.py index b93801af8..ace5e8bcb 100644 --- a/tests/integration/test_anthropic_provider.py +++ b/tests/integration/test_anthropic_provider.py @@ -62,8 +62,8 @@ def test_anthropic_provider_basic(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, @@ -170,8 +170,8 @@ def test_anthropic_provider_with_thinking(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, diff --git a/tests/integration/test_callosum.py b/tests/integration/test_callosum.py index 840bae296..cb820c7dc 100644 --- a/tests/integration/test_callosum.py +++ b/tests/integration/test_callosum.py @@ -152,7 +152,7 @@ def test_multiple_clients_broadcast(callosum_server): # Wait for client to connect time.sleep(0.2) - client.emit("cortex", "agent_start", agent_id="123", name="analyst") + client.emit("cortex", "agent_start", use_id="123", name="analyst") # Wait for broadcast time.sleep(0.2) @@ -171,7 +171,7 @@ def test_multiple_clients_broadcast(callosum_server): msg = received[0] assert msg["tract"] == "cortex" assert msg["event"] == "agent_start" - assert msg["agent_id"] == "123" + assert msg["use_id"] == "123" assert msg["name"] == "analyst" # Cleanup diff --git a/tests/integration/test_cortex.py b/tests/integration/test_cortex.py index eef9a809e..bad3efacb 100644 --- a/tests/integration/test_cortex.py +++ b/tests/integration/test_cortex.py @@ -12,7 +12,7 @@ import pytest from think.callosum import CallosumServer from think.cortex import CortexService -from think.cortex_client import cortex_agents, cortex_request +from think.cortex_client import cortex_request, cortex_uses from think.utils import now_ms @@ -47,13 +47,13 @@ def test_cortex_service_startup(integration_journal_path, callosum_server): cortex = CortexService(journal_path=str(integration_journal_path)) # Verify agents directory was created - agents_dir = integration_journal_path / "agents" + agents_dir = integration_journal_path / "talents" assert agents_dir.exists() assert agents_dir.is_dir() # Verify service initializes correctly status = cortex.get_status() - assert status["running_agents"] == 0 + assert status["running_uses"] == 0 assert status["agent_ids"] == [] @@ -75,7 +75,7 @@ def test_cortex_request_creation(integration_journal_path, callosum_server): time.sleep(0.1) # Create a request - agent_id = cortex_request(prompt="Test prompt", name="default", provider="openai") + use_id = cortex_request(prompt="Test prompt", name="default", provider="openai") time.sleep(0.2) @@ -85,7 +85,7 @@ def test_cortex_request_creation(integration_journal_path, callosum_server): assert request["prompt"] == "Test prompt" assert request["name"] == "default" assert request["provider"] == "openai" - assert request["agent_id"] == agent_id + assert request["use_id"] == use_id listener.stop() @@ -96,7 +96,7 @@ def test_cortex_end_to_end_with_echo_agent(integration_journal_path, callosum_se os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(integration_journal_path) # Create a mock agent script that just echoes - agents_dir = integration_journal_path / "agents" + agents_dir = integration_journal_path / "talents" agents_dir.mkdir(parents=True, exist_ok=True) # Start Cortex service in background @@ -124,9 +124,7 @@ def test_cortex_end_to_end_with_echo_agent(integration_journal_path, callosum_se time.sleep(0.2) # Make a request (this will fail because no real agent, but we can verify the flow) - agent_id = cortex_request( - prompt="Test end-to-end", name="default", provider="openai" - ) + use_id = cortex_request(prompt="Test end-to-end", name="default", provider="openai") # Wait for at least request event time.sleep(1.0) @@ -134,23 +132,23 @@ def test_cortex_end_to_end_with_echo_agent(integration_journal_path, callosum_se # Should have received the request event request_events = [e for e in received_events if e.get("event") == "request"] assert len(request_events) >= 1 - assert request_events[0]["agent_id"] == agent_id + assert request_events[0]["use_id"] == use_id watcher.stop() cortex.stop() @pytest.mark.integration -def test_cortex_agents_listing(integration_journal_path): - """Test listing agents from the cortex_agents function.""" +def test_cortex_uses_listing(integration_journal_path): + """Test listing agents from the cortex_uses function.""" os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(integration_journal_path) # Create some test agent files - agents_dir = integration_journal_path / "agents" + agents_dir = integration_journal_path / "talents" agents_dir.mkdir(parents=True, exist_ok=True) # Get initial count - initial_result = cortex_agents() + initial_result = cortex_uses() initial_count = len(initial_result["agents"]) ts = now_ms() @@ -175,7 +173,7 @@ def test_cortex_agents_listing(integration_journal_path): f.write("\n") # List agents - result = cortex_agents() + result = cortex_uses() # Should have one more than before assert len(result["agents"]) == initial_count + 1 diff --git a/tests/integration/test_google_provider.py b/tests/integration/test_google_provider.py index 1c85998d6..b25fee0a6 100644 --- a/tests/integration/test_google_provider.py +++ b/tests/integration/test_google_provider.py @@ -62,8 +62,8 @@ def test_google_provider_basic(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, @@ -154,8 +154,8 @@ def test_google_provider_with_thinking(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, diff --git a/tests/integration/test_openai_provider.py b/tests/integration/test_openai_provider.py index d127b93ec..002b5296a 100644 --- a/tests/integration/test_openai_provider.py +++ b/tests/integration/test_openai_provider.py @@ -62,8 +62,8 @@ def test_openai_provider_basic(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, @@ -162,8 +162,8 @@ def test_openai_provider_with_reasoning(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, @@ -236,7 +236,7 @@ def test_openai_provider_with_extra_context(): env["_SOLSTONE_JOURNAL_OVERRIDE"] = journal_path env["OPENAI_API_KEY"] = api_key - # Include extra_context like get_agent() does in production + # Include extra_context like get_talent() does in production # This exercises the _convert_turns_to_items() code path ndjson_input = json.dumps( { @@ -249,8 +249,8 @@ def test_openai_provider_with_extra_context(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, diff --git a/tests/test_activities.py b/tests/test_activities.py index 601dae860..e084d037c 100644 --- a/tests/test_activities.py +++ b/tests/test_activities.py @@ -638,11 +638,11 @@ class TestActivityRecordIO: def _setup_segment(tmpdir, day, segment, facet, state): """Helper to create an activity_state.json file in a segment.""" - agents_dir = ( - Path(tmpdir) / "chronicle" / day / "default" / segment / "agents" / facet + talents_dir = ( + Path(tmpdir) / "chronicle" / day / "default" / segment / "talents" / facet ) - agents_dir.mkdir(parents=True, exist_ok=True) - state_file = agents_dir / "activity_state.json" + talents_dir.mkdir(parents=True, exist_ok=True) + state_file = talents_dir / "activity_state.json" state_file.write_text(json.dumps(state)) diff --git a/tests/test_activity_state.py b/tests/test_activity_state.py index 1eb56ff3d..bb49220ab 100644 --- a/tests/test_activity_state.py +++ b/tests/test_activity_state.py @@ -15,13 +15,13 @@ class TestExtractFacetFromOutputPath: def test_extracts_facet_from_valid_path(self): from talent.activity_state import _extract_facet_from_output_path - path = "/journal/20260130/143000_300/agents/work/activity_state.json" + path = "/journal/20260130/143000_300/talents/work/activity_state.json" assert _extract_facet_from_output_path(path) == "work" def test_extracts_facet_with_hyphen(self): from talent.activity_state import _extract_facet_from_output_path - path = "/journal/20260130/143000_300/agents/my-project/activity_state.json" + path = "/journal/20260130/143000_300/talents/my-project/activity_state.json" assert _extract_facet_from_output_path(path) == "my-project" def test_returns_none_for_empty_path(self): @@ -37,7 +37,7 @@ class TestExtractFacetFromOutputPath: assert _extract_facet_from_output_path("/path/to/facets.json") is None # No facet directory assert ( - _extract_facet_from_output_path("/path/to/agents/activity_state.json") + _extract_facet_from_output_path("/path/to/talents/activity_state.json") is None ) @@ -146,7 +146,7 @@ class TestLoadPreviousState: Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" ) segment_dir.mkdir(parents=True) - (segment_dir / "agents" / "work").mkdir(parents=True) + (segment_dir / "talents" / "work").mkdir(parents=True) state = [ { @@ -157,7 +157,7 @@ class TestLoadPreviousState: "level": "high", } ] - (segment_dir / "agents/work/activity_state.json").write_text( + (segment_dir / "talents/work/activity_state.json").write_text( json.dumps(state) ) @@ -184,7 +184,7 @@ class TestLoadPreviousState: Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" ) segment_dir.mkdir(parents=True) - (segment_dir / "agents" / "work").mkdir(parents=True) + (segment_dir / "talents" / "work").mkdir(parents=True) loaded, segment = load_previous_state( "20260130", "100000_300", "work", stream="default" @@ -208,10 +208,10 @@ class TestLoadPreviousState: Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" ) segment_dir.mkdir(parents=True) - (segment_dir / "agents" / "work").mkdir(parents=True) + (segment_dir / "talents" / "work").mkdir(parents=True) # Write a dict (old format) — should be rejected - (segment_dir / "agents/work/activity_state.json").write_text( + (segment_dir / "talents/work/activity_state.json").write_text( '{"active": [], "ended": []}' ) @@ -364,7 +364,7 @@ class TestPreProcess: day_dir = Path(tmpdir) / "chronicle" / "20260130" day_dir.mkdir(parents=True) (day_dir / "default" / "100000_300").mkdir(parents=True) - (day_dir / "default" / "100000_300" / "agents" / "work").mkdir( + (day_dir / "default" / "100000_300" / "talents" / "work").mkdir( parents=True ) segment_dir = day_dir / "default" / "110000_300" @@ -391,14 +391,14 @@ class TestPreProcess: day_dir / "default" / "100000_300" - / "agents/work/activity_state.json" + / "talents/work/activity_state.json" ).write_text(json.dumps(prev_state)) context = { "day": "20260130", "segment": "110000_300", "stream": "default", - "output_path": "/journal/20260130/110000_300/agents/work/activity_state.json", + "output_path": "/journal/20260130/110000_300/talents/work/activity_state.json", "transcript": "User is typing code...", "meta": {}, } @@ -424,7 +424,7 @@ class TestPreProcess: context = { "segment": "100000_300", - "output_path": "/path/to/agents/work/activity_state.json", + "output_path": "/path/to/talents/work/activity_state.json", } assert pre_process(context) is None @@ -433,7 +433,7 @@ class TestPreProcess: context = { "day": "20260130", - "output_path": "/path/to/agents/work/activity_state.json", + "output_path": "/path/to/talents/work/activity_state.json", } assert pre_process(context) is None @@ -487,7 +487,7 @@ class TestPostProcess: # Previous segment with active meeting prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -497,7 +497,7 @@ class TestPostProcess: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -519,7 +519,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -545,7 +545,7 @@ class TestPostProcess: # Previous segment with active meeting prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -555,7 +555,7 @@ class TestPostProcess: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -575,7 +575,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -669,7 +669,7 @@ class TestPostProcess: # Previous segment — email already ended prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "email", @@ -678,7 +678,7 @@ class TestPostProcess: "description": "Replied to boss", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -698,7 +698,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -725,7 +725,7 @@ class TestPostProcess: # Previous segment — email ended with different description prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "email", @@ -734,7 +734,7 @@ class TestPostProcess: "description": "Replied to boss", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -754,7 +754,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -806,7 +806,7 @@ class TestPostProcess: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -816,7 +816,7 @@ class TestPostProcess: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -842,7 +842,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -927,7 +927,7 @@ class TestPostProcess: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -937,7 +937,7 @@ class TestPostProcess: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -958,7 +958,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -984,7 +984,7 @@ class TestPostProcess: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -1001,7 +1001,7 @@ class TestPostProcess: "level": "medium", }, ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1022,7 +1022,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -1069,7 +1069,7 @@ class TestActivityId: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -1079,7 +1079,7 @@ class TestActivityId: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1100,7 +1100,7 @@ class TestActivityId: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -1124,7 +1124,7 @@ class TestActivityId: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -1134,7 +1134,7 @@ class TestActivityId: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1154,7 +1154,7 @@ class TestActivityId: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -1208,7 +1208,7 @@ class TestActivityLiveEvents: context = { "day": "20260130", "segment": "143000_300", - "output_path": "/j/20260130/143000_300/agents/work/activity_state.json", + "output_path": "/j/20260130/143000_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send") as mock_send: @@ -1244,7 +1244,7 @@ class TestActivityLiveEvents: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "coding", @@ -1254,7 +1254,7 @@ class TestActivityLiveEvents: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1275,7 +1275,7 @@ class TestActivityLiveEvents: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send") as mock_send: @@ -1306,7 +1306,7 @@ class TestActivityLiveEvents: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -1316,7 +1316,7 @@ class TestActivityLiveEvents: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1336,7 +1336,7 @@ class TestActivityLiveEvents: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send") as mock_send: @@ -1387,7 +1387,7 @@ class TestActivityLiveEvents: context = { "day": "20260130", "segment": "143000_300", - "output_path": "/j/20260130/143000_300/agents/work/activity_state.json", + "output_path": "/j/20260130/143000_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send") as mock_send: @@ -1440,7 +1440,7 @@ class TestActivityIdValidation: context = { "segment": "143000_300", - "output_path": f"{tmpdir}/20260130/143000_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/143000_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send"): @@ -1483,7 +1483,7 @@ class TestActivityIdValidation: context = { "segment": "143000_300", - "output_path": f"{tmpdir}/20260130/143000_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/143000_300/talents/work/activity_state.json", } with caplog.at_level(logging.WARNING, logger="talent.activity_state"): @@ -1539,7 +1539,7 @@ class TestActivityIdValidation: context = { "segment": "143000_300", - "output_path": f"{tmpdir}/20260130/143000_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/143000_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send"): @@ -1589,7 +1589,7 @@ class TestActivityIdValidation: context = { "segment": "143000_300", - "output_path": f"{tmpdir}/20260130/143000_300/agents/new_facet/activity_state.json", + "output_path": f"{tmpdir}/20260130/143000_300/talents/new_facet/activity_state.json", } with patch("talent.activity_state.callosum_send"): diff --git a/tests/test_anthropic.py b/tests/test_anthropic.py index 0f2c4979d..e7d519b82 100644 --- a/tests/test_anthropic.py +++ b/tests/test_anthropic.py @@ -203,11 +203,11 @@ def test_claude_main(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -221,7 +221,7 @@ def test_claude_main(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] @@ -246,11 +246,11 @@ def test_claude_outfile(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -264,7 +264,7 @@ def test_claude_outfile(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) # Output file functionality was removed in NDJSON-only mode # Check stdout instead @@ -293,11 +293,11 @@ def test_claude_thinking_events(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod, with_thinking=True) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -311,7 +311,7 @@ def test_claude_thinking_events(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] @@ -335,11 +335,11 @@ def test_claude_redacted_thinking_events(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod, with_redacted_thinking=True) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -353,7 +353,7 @@ def test_claude_redacted_thinking_events(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] @@ -375,11 +375,11 @@ def test_claude_outfile_error(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod, error=True) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -393,7 +393,7 @@ def test_claude_outfile_error(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) # Error events should be written to stdout out_lines = capsys.readouterr().out.strip().splitlines() diff --git a/tests/test_app_calendar.py b/tests/test_app_calendar.py index f9d584845..724c5db66 100644 --- a/tests/test_app_calendar.py +++ b/tests/test_app_calendar.py @@ -134,7 +134,7 @@ class TestCalendarActivityOutput: def test_rejects_non_facets_path(self, calendar_client): """Paths not starting with facets/ are rejected.""" resp = calendar_client.get( - "/app/calendar/api/activity_output/20260214/agents/flow.md" + "/app/calendar/api/activity_output/20260214/talents/flow.md" ) assert resp.status_code == 400 diff --git a/tests/test_app_sol.py b/tests/test_app_sol.py index c4f991ef0..aaff47c78 100644 --- a/tests/test_app_sol.py +++ b/tests/test_app_sol.py @@ -10,7 +10,7 @@ from pathlib import Path import pytest from apps.sol.routes import _resolve_output_path -from think.talent import _resolve_agent_path, get_agent, get_talent_configs +from think.talent import _resolve_talent_path, get_talent, get_talent_configs @pytest.fixture @@ -69,16 +69,16 @@ def app_with_agent(tmp_path, monkeypatch): def test_resolve_agent_path_system_agent(): - """Test _resolve_agent_path returns correct path for system agents.""" - agent_dir, agent_name = _resolve_agent_path("unified") + """Test _resolve_talent_path returns correct path for system agents.""" + agent_dir, agent_name = _resolve_talent_path("unified") assert agent_name == "chat" assert agent_dir.name == "talent" def test_resolve_agent_path_app_agent(): - """Test _resolve_agent_path returns correct path for app agents.""" - agent_dir, agent_name = _resolve_agent_path("support:support") + """Test _resolve_talent_path returns correct path for app agents.""" + agent_dir, agent_name = _resolve_talent_path("support:support") assert agent_name == "support" assert agent_dir.name == "talent" @@ -87,16 +87,16 @@ def test_resolve_agent_path_app_agent(): def test_resolve_agent_path_app_agent_with_underscores(): - """Test _resolve_agent_path handles app names with underscores.""" - agent_dir, agent_name = _resolve_agent_path("my_app:my_agent") + """Test _resolve_talent_path handles app names with underscores.""" + agent_dir, agent_name = _resolve_talent_path("my_app:my_agent") assert agent_name == "my_agent" assert agent_dir.parent.name == "my_app" def test_get_agent_system_agent(fixture_journal): - """Test get_agent loads system agents correctly.""" - config = get_agent("unified") + """Test get_talent loads system agents correctly.""" + config = get_talent("unified") assert config["name"] == "unified" assert "user_instruction" in config @@ -104,17 +104,17 @@ def test_get_agent_system_agent(fixture_journal): def test_get_agent_nonexistent_raises(): - """Test get_agent raises FileNotFoundError for nonexistent agents.""" + """Test get_talent raises FileNotFoundError for nonexistent agents.""" with pytest.raises(FileNotFoundError) as exc_info: - get_agent("nonexistent_agent_xyz") + get_talent("nonexistent_agent_xyz") assert "nonexistent_agent_xyz" in str(exc_info.value) def test_get_agent_nonexistent_app_agent_raises(): - """Test get_agent raises FileNotFoundError for nonexistent app agents.""" + """Test get_talent raises FileNotFoundError for nonexistent app agents.""" with pytest.raises(FileNotFoundError) as exc_info: - get_agent("fakeapp:fakeagent") + get_talent("fakeapp:fakeagent") assert "fakeapp:fakeagent" in str(exc_info.value) @@ -147,7 +147,7 @@ def test_get_talent_configs_excludes_private_apps( ): """Test get_talent_configs skips apps starting with underscore.""" # Create a private app with an agent - private_app = tmp_path / "_private_app" / "agents" + private_app = tmp_path / "_private_app" / "talents" private_app.mkdir(parents=True) (private_app / "secret.md").write_text("Secret agent") @@ -258,8 +258,8 @@ def agents_client(tmp_path): # Create test files day_dir = tmp_path / "chronicle" / "20260214" day_dir.mkdir(parents=True) - (day_dir / "agents" / "flow.md").parent.mkdir(parents=True) - (day_dir / "agents" / "flow.md").write_text("# Day agent output") + (day_dir / "talents" / "flow.md").parent.mkdir(parents=True) + (day_dir / "talents" / "flow.md").write_text("# Day agent output") facet_dir = tmp_path / "facets" / "work" / "activities" / "20260214" / "coding_100" facet_dir.mkdir(parents=True) @@ -273,7 +273,7 @@ class TestApiOutputFile: def test_serves_day_relative_file(self, agents_client): """Day-relative paths resolve under {journal}/{day}/.""" - resp = agents_client.get("/app/sol/api/output/20260214/agents/flow.md") + resp = agents_client.get("/app/sol/api/output/20260214/talents/flow.md") assert resp.status_code == 200 data = resp.get_json() assert data["content"] == "# Day agent output" @@ -293,7 +293,7 @@ class TestApiOutputFile: def test_rejects_invalid_day_format(self, agents_client): """Non-YYYYMMDD day returns 400.""" - resp = agents_client.get("/app/sol/api/output/bad-day/agents/flow.md") + resp = agents_client.get("/app/sol/api/output/bad-day/talents/flow.md") assert resp.status_code == 400 def test_rejects_path_traversal(self, agents_client): @@ -303,5 +303,5 @@ class TestApiOutputFile: def test_missing_file_returns_404(self, agents_client): """Non-existent file returns 404.""" - resp = agents_client.get("/app/sol/api/output/20260214/agents/nonexistent.md") + resp = agents_client.get("/app/sol/api/output/20260214/talents/nonexistent.md") assert resp.status_code == 404 diff --git a/tests/test_cluster.py b/tests/test_cluster.py index ddf1e83c1..e048b7fda 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -20,8 +20,8 @@ def test_cluster(tmp_path, monkeypatch): '{}\n{"text": "hi"}\n' ) (day_dir / "default" / "120500_300").mkdir(parents=True) - (day_dir / "default" / "120500_300" / "agents").mkdir() - (day_dir / "default" / "120500_300" / "agents" / "screen.md").write_text( + (day_dir / "default" / "120500_300" / "talents").mkdir() + (day_dir / "default" / "120500_300" / "talents" / "screen.md").write_text( "screen summary" ) result, counts = mod.cluster( @@ -46,8 +46,8 @@ def test_cluster_range(tmp_path, monkeypatch): '{"raw": "raw.flac", "model": "whisper-1"}\n' '{"start": "00:00:01", "source": "mic", "text": "hi from audio"}\n' ) - (day_dir / "default" / "120000_300" / "agents").mkdir() - (day_dir / "default" / "120000_300" / "agents" / "screen.md").write_text( + (day_dir / "default" / "120000_300" / "talents").mkdir() + (day_dir / "default" / "120000_300" / "talents" / "screen.md").write_text( "screen summary content" ) # Test with agents=True to include *.md files @@ -170,8 +170,8 @@ def test_cluster_period_uses_raw_screen(tmp_path, monkeypatch): '"visual_description": "VS Code with Python file"}}\n' ) # Also create screen.md (insight) to verify it's NOT used by cluster_period - (segment / "agents").mkdir() - (segment / "agents" / "screen.md").write_text("This insight should NOT appear") + (segment / "talents").mkdir() + (segment / "talents" / "screen.md").write_text("This insight should NOT appear") result, counts = mod.cluster_period( "20240101", @@ -220,12 +220,12 @@ def test_cluster_range_with_agents(tmp_path, monkeypatch): # Create segment with multiple insight files segment = day_dir / "default" / "100000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text( '{"raw": "audio.flac"}\n{"start": "00:00:01", "text": "hello"}\n' ) - (segment / "agents" / "screen.md").write_text("Screen activity summary") - (segment / "agents" / "activity.md").write_text("Activity insight content") + (segment / "talents" / "screen.md").write_text("Screen activity summary") + (segment / "talents" / "activity.md").write_text("Activity insight content") # Also create screen.jsonl to verify it's NOT used when agents=True, screen=False (segment / "screen.jsonl").write_text( '{"raw": "screen.webm"}\n' @@ -260,12 +260,12 @@ def test_cluster_range_with_screen(tmp_path, monkeypatch): # Create segment with raw screen data and insight file segment = day_dir / "default" / "100000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "screen.jsonl").write_text( '{"raw": "screen.webm"}\n' '{"timestamp": 10, "analysis": {"primary": "code_editor"}}\n' ) - (segment / "agents" / "screen.md").write_text("Screen summary insight") + (segment / "talents" / "screen.md").write_text("Screen summary insight") # Test screen=True returns raw screen data, not agent outputs result = mod.cluster_range( @@ -434,11 +434,11 @@ def test_cluster_with_agent_filter_dict(tmp_path, monkeypatch): # Create segment with multiple agent output files segment = day_dir / "default" / "120000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') - (segment / "agents" / "entities.md").write_text("Entity extraction results") - (segment / "agents" / "meetings.md").write_text("Meeting summary results") - (segment / "agents" / "flow.md").write_text("Flow analysis results") + (segment / "talents" / "entities.md").write_text("Entity extraction results") + (segment / "talents" / "meetings.md").write_text("Meeting summary results") + (segment / "talents" / "flow.md").write_text("Flow analysis results") # Test filtering to only include entities result, counts = mod.cluster( @@ -463,11 +463,11 @@ def test_cluster_with_agent_filter_multiple(tmp_path, monkeypatch): # Create segment with multiple agent output files segment = day_dir / "default" / "120000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') - (segment / "agents" / "entities.md").write_text("Entity extraction results") - (segment / "agents" / "meetings.md").write_text("Meeting summary results") - (segment / "agents" / "flow.md").write_text("Flow analysis results") + (segment / "talents" / "entities.md").write_text("Entity extraction results") + (segment / "talents" / "meetings.md").write_text("Meeting summary results") + (segment / "talents" / "flow.md").write_text("Flow analysis results") # Test filtering to include entities and meetings but not flow result, counts = mod.cluster( @@ -497,10 +497,10 @@ def test_cluster_with_agent_filter_app_namespaced(tmp_path, monkeypatch): # App agent output naming: "app:agent" -> "_app_agent.md" segment = day_dir / "default" / "120000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') - (segment / "agents" / "entities.md").write_text("System entity results") - (segment / "agents" / "_todos_review.md").write_text("Todos review results") + (segment / "talents" / "entities.md").write_text("System entity results") + (segment / "talents" / "_todos_review.md").write_text("Todos review results") # Test filtering to include app-namespaced agent result, counts = mod.cluster( @@ -527,9 +527,9 @@ def test_cluster_with_empty_agent_filter(tmp_path, monkeypatch): segment = day_dir / "default" / "120000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') - (segment / "agents" / "entities.md").write_text("Entity extraction results") + (segment / "talents" / "entities.md").write_text("Entity extraction results") # Empty dict should mean no agents result, counts = mod.cluster( diff --git a/tests/test_conversation.py b/tests/test_conversation.py index 104c3a76a..e09ccd558 100644 --- a/tests/test_conversation.py +++ b/tests/test_conversation.py @@ -37,7 +37,7 @@ def test_record_exchange_writes_jsonl(journal_dir): user_message="what's our history with adrian?", agent_response="You met Adrian at betaworks.", talent="unified", - agent_id="12345", + use_id="12345", ) jsonl_path = journal_dir / "conversation" / "exchanges.jsonl" @@ -54,7 +54,7 @@ def test_record_exchange_writes_jsonl(journal_dir): assert ex["user_message"] == "what's our history with adrian?" assert ex["agent_response"] == "You met Adrian at betaworks." assert ex["talent"] == "unified" - assert ex["agent_id"] == "12345" + assert ex["use_id"] == "12345" def test_record_exchange_writes_journal_segment(journal_dir): @@ -72,10 +72,10 @@ def test_record_exchange_writes_journal_segment(journal_dir): user_message="move my 3pm to 4pm", agent_response="Done — moved 'DVD sync' to 4pm.", talent="unified", - agent_id="67890", + use_id="67890", ) - # Check journal segment directory: YYYYMMDD/conversation/HHMMSS_1/agents/ + # Check journal segment directory: YYYYMMDD/conversation/HHMMSS_1/talents/ day = datetime.fromtimestamp(ts / 1000).strftime("%Y%m%d") time_key = datetime.fromtimestamp(ts / 1000).strftime("%H%M%S") md_path = ( @@ -84,7 +84,7 @@ def test_record_exchange_writes_journal_segment(journal_dir): / day / "conversation" / f"{time_key}_1" - / "agents" + / "talents" / "conversation.md" ) diff --git a/tests/test_convey_apps.py b/tests/test_convey_apps.py index ec89f482c..aa18a76fc 100644 --- a/tests/test_convey_apps.py +++ b/tests/test_convey_apps.py @@ -20,9 +20,9 @@ def _run_triage(): app = Flask(__name__) with ( patch("convey.utils.spawn_agent", return_value="agent-1") as mock_spawn, - patch("think.cortex_client.wait_for_agents", return_value=({}, [])), + patch("think.cortex_client.wait_for_uses", return_value=({}, [])), patch( - "think.cortex_client.read_agent_events", + "think.cortex_client.read_use_events", return_value=[{"event": "finish", "result": "ok"}], ), ): @@ -140,13 +140,13 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / "agents" + agents_dir = tmp_path / "talents" agents_dir.mkdir() day_index = agents_dir / f"{today}.jsonl" day_index.write_text( json.dumps( { - "agent_id": "1", + "use_id": "1", "name": "flow", "day": today, "ts": 1000, @@ -156,7 +156,7 @@ class TestAttentionResolution: + "\n" + json.dumps( { - "agent_id": "2", + "use_id": "2", "name": "meetings", "day": today, "ts": 1001, @@ -182,13 +182,13 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / "agents" + agents_dir = tmp_path / "talents" agents_dir.mkdir() day_index = agents_dir / f"{today}.jsonl" day_index.write_text( json.dumps( { - "agent_id": "1", + "use_id": "1", "name": "flow", "day": today, "ts": 1000, @@ -198,7 +198,7 @@ class TestAttentionResolution: + "\n" + json.dumps( { - "agent_id": "3", + "use_id": "3", "name": "flow", "day": today, "ts": 2000, @@ -221,13 +221,13 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / "agents" + agents_dir = tmp_path / "talents" agents_dir.mkdir() day_index = agents_dir / f"{today}.jsonl" day_index.write_text( json.dumps( { - "agent_id": "1", + "use_id": "1", "name": "flow", "day": today, "ts": 1000, @@ -266,11 +266,11 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / "agents" + agents_dir = tmp_path / "talents" agents_dir.mkdir() day_index = agents_dir / f"{today}.jsonl" day_index.write_text( - json.dumps({"agent_id": "1", "name": "flow", "ts": 1000, "status": "error"}) + json.dumps({"use_id": "1", "name": "flow", "ts": 1000, "status": "error"}) + "\n" ) result = _resolve_attention({}) @@ -299,7 +299,7 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / today / "agents" + agents_dir = tmp_path / today / "talents" agents_dir.mkdir(parents=True) (agents_dir / "flow.md").write_text("# Flow") (agents_dir / "meetings.md").write_text("# Meetings") diff --git a/tests/test_cortex.py b/tests/test_cortex.py index 7d3abb61a..9ef3ea515 100644 --- a/tests/test_cortex.py +++ b/tests/test_cortex.py @@ -5,6 +5,7 @@ import json import os +import sys from pathlib import Path from unittest.mock import MagicMock, patch @@ -41,7 +42,7 @@ def mock_journal(tmp_path, monkeypatch): """Set up a temporary journal directory.""" journal_path = tmp_path / "journal" journal_path.mkdir() - agents_path = journal_path / "agents" + agents_path = journal_path / "talents" agents_path.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_path)) @@ -57,17 +58,17 @@ def cortex_service(mock_journal): def test_agent_process_creation(): - """Test AgentProcess class initialization and methods.""" - from think.cortex import AgentProcess + """Test TalentProcess class initialization and methods.""" + from think.cortex import TalentProcess mock_process = MagicMock() mock_process.poll.return_value = None # Running mock_process.pid = 12345 log_path = Path("/tmp/test.jsonl") - agent = AgentProcess("123456789", mock_process, log_path) + agent = TalentProcess("123456789", mock_process, log_path) - assert agent.agent_id == "123456789" + assert agent.use_id == "123456789" assert agent.process == mock_process assert agent.log_path == log_path assert agent.is_running() is True @@ -81,9 +82,9 @@ def test_agent_process_creation(): def test_cortex_service_initialization(cortex_service, mock_journal): """Test CortexService initialization.""" assert cortex_service.journal_path == mock_journal - assert cortex_service.agents_dir == mock_journal / "agents" - assert cortex_service.running_agents == {} - assert cortex_service.agents_dir.exists() + assert cortex_service.talents_dir == mock_journal / "talents" + assert cortex_service.running_uses == {} + assert cortex_service.talents_dir.exists() @patch("think.cortex.subprocess.Popen") @@ -105,8 +106,8 @@ def test_spawn_subprocess( mock_timer_instance = MagicMock() mock_timer.return_value = mock_timer_instance - agent_id = "123456789" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" request = { "event": "request", @@ -118,17 +119,17 @@ def test_spawn_subprocess( } cortex_service._spawn_subprocess( - agent_id, + use_id, file_path, request, - ["sol", "agents"], - "agent", + [sys.executable, "-m", "think.talents"], + "talent", ) # Check subprocess was called mock_popen.assert_called_once() call_args = mock_popen.call_args - assert call_args[0][0] == ["sol", "agents"] + assert call_args[0][0] == [sys.executable, "-m", "think.talents"] assert call_args[1]["stdin"] is not None assert call_args[1]["stdout"] is not None assert call_args[1]["stderr"] is not None @@ -147,9 +148,9 @@ def test_spawn_subprocess( mock_process.stdin.close.assert_called_once() # Check agent was tracked - assert agent_id in cortex_service.running_agents - agent = cortex_service.running_agents[agent_id] - assert agent.agent_id == agent_id + assert use_id in cortex_service.running_uses + agent = cortex_service.running_uses[use_id] + assert agent.use_id == use_id assert agent.log_path == file_path # Check monitoring threads were started @@ -179,8 +180,8 @@ def test_spawn_generator_via_subprocess( mock_timer_instance = MagicMock() mock_timer.return_value = mock_timer_instance - agent_id = "987654321" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "987654321" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" # Generator config has "output" instead of "tools" config = { @@ -193,17 +194,17 @@ def test_spawn_generator_via_subprocess( # Generators route through _spawn_subprocess cortex_service._spawn_subprocess( - agent_id, + use_id, file_path, config, - ["sol", "agents"], - "agent", + [sys.executable, "-m", "think.talents"], + "talent", ) # Check subprocess was called with agents command (generators route through agents) mock_popen.assert_called_once() call_args = mock_popen.call_args - assert call_args[0][0] == ["sol", "agents"] + assert call_args[0][0] == [sys.executable, "-m", "think.talents"] assert call_args[1]["stdin"] is not None assert call_args[1]["stdout"] is not None assert call_args[1]["stderr"] is not None @@ -221,9 +222,9 @@ def test_spawn_generator_via_subprocess( mock_process.stdin.close.assert_called_once() # Check generator was tracked - assert agent_id in cortex_service.running_agents - agent = cortex_service.running_agents[agent_id] - assert agent.agent_id == agent_id + assert use_id in cortex_service.running_uses + agent = cortex_service.running_uses[use_id] + assert agent.use_id == use_id assert agent.log_path == file_path # Check monitoring threads were started @@ -234,7 +235,7 @@ def test_spawn_generator_via_subprocess( mock_timer_instance.start.assert_called_once() -@patch("think.talent.get_agent") +@patch("think.talent.get_talent") @patch("think.cortex.subprocess.Popen") @patch("think.cortex.threading.Thread") @patch("think.cortex.threading.Timer") @@ -258,8 +259,8 @@ def test_spawn_subprocess_uses_cwd_from_talent( mock_timer_instance = MagicMock() mock_timer.return_value = mock_timer_instance - agent_id = "24680" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "24680" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" request = { "event": "request", "ts": 24680, @@ -270,17 +271,17 @@ def test_spawn_subprocess_uses_cwd_from_talent( } cortex_service._spawn_subprocess( - agent_id, + use_id, file_path, request, - ["sol", "agents"], - "agent", + [sys.executable, "-m", "think.talents"], + "talent", ) assert mock_popen.call_args.kwargs["cwd"] == str(mock_journal) -@patch("think.talent.get_agent") +@patch("think.talent.get_talent") @patch("think.cortex.subprocess.Popen") @patch("think.cortex.threading.Thread") @patch("think.cortex.threading.Timer") @@ -304,8 +305,8 @@ def test_spawn_subprocess_skips_cwd_for_generate( mock_timer_instance = MagicMock() mock_timer.return_value = mock_timer_instance - agent_id = "13579" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "13579" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" request = { "event": "request", "ts": 13579, @@ -315,11 +316,11 @@ def test_spawn_subprocess_skips_cwd_for_generate( } cortex_service._spawn_subprocess( - agent_id, + use_id, file_path, request, - ["sol", "agents"], - "agent", + [sys.executable, "-m", "think.talents"], + "talent", ) assert mock_popen.call_args.kwargs["cwd"] is None @@ -329,10 +330,10 @@ def test_monitor_stdout_json_events(cortex_service, mock_journal): """Test monitoring stdout with JSON events.""" from io import StringIO - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "123456789" - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" mock_process = MagicMock() mock_process.poll.return_value = 0 # Process exits @@ -341,10 +342,10 @@ def test_monitor_stdout_json_events(cortex_service, mock_journal): '{"event": "finish", "ts": 1234567891, "result": "Done"}\n' ) - agent = AgentProcess(agent_id, mock_process, log_path) - cortex_service.running_agents[agent_id] = agent + agent = TalentProcess(use_id, mock_process, log_path) + cortex_service.running_uses[use_id] = agent - with patch.object(cortex_service, "_complete_agent_file") as mock_complete: + with patch.object(cortex_service, "_complete_use_file") as mock_complete: cortex_service._monitor_stdout(agent) # Check events were written to file @@ -355,20 +356,20 @@ def test_monitor_stdout_json_events(cortex_service, mock_journal): assert json.loads(lines[1])["event"] == "finish" # Check file was completed - mock_complete.assert_called_once_with(agent_id, log_path) + mock_complete.assert_called_once_with(use_id, log_path) # Check agent was removed - assert agent_id not in cortex_service.running_agents + assert use_id not in cortex_service.running_uses def test_monitor_stdout_non_json_output(cortex_service, mock_journal): """Test monitoring stdout with non-JSON output.""" from io import StringIO - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "123456789" - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" mock_process = MagicMock() mock_process.poll.return_value = 0 @@ -376,10 +377,10 @@ def test_monitor_stdout_non_json_output(cortex_service, mock_journal): 'Plain text output\n{"event": "finish", "ts": 1234567890}\n' ) - agent = AgentProcess(agent_id, mock_process, log_path) - cortex_service.running_agents[agent_id] = agent + agent = TalentProcess(use_id, mock_process, log_path) + cortex_service.running_uses[use_id] = agent - with patch.object(cortex_service, "_complete_agent_file"): + with patch.object(cortex_service, "_complete_use_file"): cortex_service._monitor_stdout(agent) # Check info event was created for non-JSON @@ -396,19 +397,19 @@ def test_monitor_stdout_no_finish_event(cortex_service, mock_journal): """Test monitoring stdout when process exits without finish event.""" from io import StringIO - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "123456789" - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" mock_process = MagicMock() mock_process.wait.return_value = 1 # Non-zero exit mock_process.stdout = StringIO('{"event": "start", "ts": 1234567890}\n') - agent = AgentProcess(agent_id, mock_process, log_path) - cortex_service.running_agents[agent_id] = agent + agent = TalentProcess(use_id, mock_process, log_path) + cortex_service.running_uses[use_id] = agent - with patch.object(cortex_service, "_complete_agent_file"): + with patch.object(cortex_service, "_complete_use_file"): cortex_service._monitor_stdout(agent) # Check error event was added @@ -425,10 +426,10 @@ def test_monitor_stderr(cortex_service, mock_journal): """Test monitoring stderr for errors.""" from io import StringIO - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "123456789" - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" mock_process = MagicMock() mock_process.poll.return_value = 1 # Error exit @@ -436,7 +437,7 @@ def test_monitor_stderr(cortex_service, mock_journal): "Error: Something went wrong\nStack trace line 1\nStack trace line 2\n" ) - agent = AgentProcess(agent_id, mock_process, log_path) + agent = TalentProcess(use_id, mock_process, log_path) cortex_service._monitor_stderr(agent) @@ -454,7 +455,7 @@ def test_monitor_stderr(cortex_service, mock_journal): def test_has_finish_event(cortex_service, mock_journal): """Test checking for finish event in JSONL file.""" - file_path = mock_journal / "agents" / "test.jsonl" + file_path = mock_journal / "talents" / "test.jsonl" # File with finish event file_path.write_text( @@ -477,89 +478,89 @@ def test_has_finish_event(cortex_service, mock_journal): assert cortex_service._has_finish_event(file_path) is False -def test_complete_agent_file(cortex_service, mock_journal): +def test_complete_use_file(cortex_service, mock_journal): """Test completing an agent file (rename from active to completed).""" - agent_id = "123456789" - unified_dir = mock_journal / "agents" / "unified" + use_id = "123456789" + unified_dir = mock_journal / "talents" / "unified" unified_dir.mkdir() - active_path = unified_dir / f"{agent_id}_active.jsonl" + active_path = unified_dir / f"{use_id}_active.jsonl" active_path.touch() - cortex_service.agent_requests[agent_id] = {"name": "unified", "agent_id": agent_id} + cortex_service.use_requests[use_id] = {"name": "unified", "use_id": use_id} - cortex_service._complete_agent_file(agent_id, active_path) + cortex_service._complete_use_file(use_id, active_path) # Check file was renamed assert not active_path.exists() - completed_path = unified_dir / f"{agent_id}.jsonl" + completed_path = unified_dir / f"{use_id}.jsonl" assert completed_path.exists() - symlink_path = mock_journal / "agents" / "unified.log" + symlink_path = mock_journal / "talents" / "unified.log" assert symlink_path.is_symlink() - assert os.readlink(symlink_path) == f"unified/{agent_id}.jsonl" + assert os.readlink(symlink_path) == f"unified/{use_id}.jsonl" -def test_complete_agent_file_replaces_symlink(cortex_service, mock_journal): +def test_complete_use_file_replaces_symlink(cortex_service, mock_journal): """Test completing agent file replaces convenience symlink for same name.""" - unified_dir = mock_journal / "agents" / "unified" + unified_dir = mock_journal / "talents" / "unified" unified_dir.mkdir() first_agent_id = "111" first_active_path = unified_dir / f"{first_agent_id}_active.jsonl" first_active_path.touch() - cortex_service.agent_requests[first_agent_id] = {"name": "unified"} + cortex_service.use_requests[first_agent_id] = {"name": "unified"} - cortex_service._complete_agent_file(first_agent_id, first_active_path) + cortex_service._complete_use_file(first_agent_id, first_active_path) second_agent_id = "222" second_active_path = unified_dir / f"{second_agent_id}_active.jsonl" second_active_path.touch() - cortex_service.agent_requests[second_agent_id] = {"name": "unified"} + cortex_service.use_requests[second_agent_id] = {"name": "unified"} - cortex_service._complete_agent_file(second_agent_id, second_active_path) + cortex_service._complete_use_file(second_agent_id, second_active_path) - symlink_path = mock_journal / "agents" / "unified.log" + symlink_path = mock_journal / "talents" / "unified.log" assert symlink_path.is_symlink() assert os.readlink(symlink_path) == f"unified/{second_agent_id}.jsonl" -def test_complete_agent_file_colon_name(cortex_service, mock_journal): +def test_complete_use_file_colon_name(cortex_service, mock_journal): """Test completing agent file sanitizes colon in convenience symlink name.""" - agent_id = "123456789" - entities_dir = mock_journal / "agents" / "entities--entity_assist" + use_id = "123456789" + entities_dir = mock_journal / "talents" / "entities--entity_assist" entities_dir.mkdir() - active_path = entities_dir / f"{agent_id}_active.jsonl" + active_path = entities_dir / f"{use_id}_active.jsonl" active_path.touch() - cortex_service.agent_requests[agent_id] = {"name": "entities:entity_assist"} + cortex_service.use_requests[use_id] = {"name": "entities:entity_assist"} - cortex_service._complete_agent_file(agent_id, active_path) + cortex_service._complete_use_file(use_id, active_path) - symlink_path = mock_journal / "agents" / "entities--entity_assist.log" + symlink_path = mock_journal / "talents" / "entities--entity_assist.log" assert symlink_path.is_symlink() - assert os.readlink(symlink_path) == f"entities--entity_assist/{agent_id}.jsonl" + assert os.readlink(symlink_path) == f"entities--entity_assist/{use_id}.jsonl" -def test_complete_agent_file_no_name(cortex_service, mock_journal): +def test_complete_use_file_no_name(cortex_service, mock_journal): """Test completing agent file skips symlink when request name is missing.""" - agent_id = "123456789" - active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + active_path = mock_journal / "talents" / f"{use_id}_active.jsonl" active_path.touch() - cortex_service._complete_agent_file(agent_id, active_path) + cortex_service._complete_use_file(use_id, active_path) - completed_path = mock_journal / "agents" / f"{agent_id}.jsonl" + completed_path = mock_journal / "talents" / f"{use_id}.jsonl" assert completed_path.exists() - assert not any(path.is_symlink() for path in (mock_journal / "agents").iterdir()) + assert not any(path.is_symlink() for path in (mock_journal / "talents").iterdir()) def test_write_error_and_complete(cortex_service, mock_journal): """Test writing error and completing file.""" - agent_id = "123456789" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" file_path.touch() cortex_service._write_error_and_complete(file_path, "Test error message") # Check error was written - completed_path = mock_journal / "agents" / f"{agent_id}.jsonl" + completed_path = mock_journal / "talents" / f"{use_id}.jsonl" assert completed_path.exists() assert not file_path.exists() @@ -572,34 +573,34 @@ def test_write_error_and_complete(cortex_service, mock_journal): def test_get_status(cortex_service): """Test getting service status.""" - from think.cortex import AgentProcess + from think.cortex import TalentProcess # Empty status status = cortex_service.get_status() - assert status["running_agents"] == 0 - assert status["agent_ids"] == [] + assert status["running_uses"] == 0 + assert status["use_ids"] == [] # Add running agents mock_process = MagicMock() - agent1 = AgentProcess("111", mock_process, Path("/tmp/1.jsonl")) - agent2 = AgentProcess("222", mock_process, Path("/tmp/2.jsonl")) + agent1 = TalentProcess("111", mock_process, Path("/tmp/1.jsonl")) + agent2 = TalentProcess("222", mock_process, Path("/tmp/2.jsonl")) - cortex_service.running_agents["111"] = agent1 - cortex_service.running_agents["222"] = agent2 + cortex_service.running_uses["111"] = agent1 + cortex_service.running_uses["222"] = agent2 status = cortex_service.get_status() - assert status["running_agents"] == 2 - assert set(status["agent_ids"]) == {"111", "222"} + assert status["running_uses"] == 2 + assert set(status["use_ids"]) == {"111", "222"} def test_write_output(cortex_service, mock_journal): """Test writing agent output using explicit output_path.""" - agent_id = "test_agent" + use_id = "test_agent" result = "This is the agent result content" - expected_path = mock_journal / "20240115" / "agents" / "my_agent.md" + expected_path = mock_journal / "20240115" / "talents" / "my_agent.md" config = {"output": "md", "name": "my_agent", "output_path": str(expected_path)} - cortex_service._write_output(agent_id, result, config) + cortex_service._write_output(use_id, result, config) assert expected_path.exists() assert expected_path.read_text() == result @@ -610,20 +611,20 @@ def test_write_output_with_error(cortex_service, mock_journal, caplog): """Test write output handles errors gracefully.""" import logging - output_path = mock_journal / "20240115" / "agents" / "test.md" + output_path = mock_journal / "20240115" / "talents" / "test.md" with patch("builtins.open", side_effect=PermissionError("Cannot write")): with caplog.at_level(logging.ERROR): config = {"output": "md", "name": "test", "output_path": str(output_path)} - cortex_service._write_output("agent_id", "result", config) + cortex_service._write_output("use_id", "result", config) # Check error was logged but didn't raise - assert "Failed to write agent agent_id output" in caplog.text + assert "Failed to write talent use_id output" in caplog.text def test_write_output_missing_path_skips(cortex_service, mock_journal, caplog): """Test write output skips when output_path is missing.""" config = {"output": "md", "name": "test"} - cortex_service._write_output("agent_id", "result", config) + cortex_service._write_output("use_id", "result", config) # No output written, no error — silent skip is expected assert "Failed to write" not in caplog.text @@ -631,10 +632,10 @@ def test_write_output_missing_path_skips(cortex_service, mock_journal, caplog): def test_write_output_with_day_parameter(cortex_service, mock_journal): """Test writing agent output to a specific day directory.""" - agent_id = "test_agent" + use_id = "test_agent" result = "This is the agent result content" specified_day = "20240201" - expected_path = mock_journal / specified_day / "agents" / "reporter.md" + expected_path = mock_journal / specified_day / "talents" / "reporter.md" config = { "output": "md", "name": "reporter", @@ -642,7 +643,7 @@ def test_write_output_with_day_parameter(cortex_service, mock_journal): "output_path": str(expected_path), } - cortex_service._write_output(agent_id, result, config) + cortex_service._write_output(use_id, result, config) assert expected_path.exists() assert expected_path.read_text() == result @@ -651,9 +652,9 @@ def test_write_output_with_day_parameter(cortex_service, mock_journal): def test_write_output_with_segment(cortex_service, mock_journal): """Test writing segment agent output to segment agents directory.""" - agent_id = "segment_agent" + use_id = "segment_agent" result = "Segment analysis content" - expected_path = mock_journal / "20240115" / "143000_600" / "agents" / "analyzer.md" + expected_path = mock_journal / "20240115" / "143000_600" / "talents" / "analyzer.md" config = { "output": "md", "name": "analyzer", @@ -661,7 +662,7 @@ def test_write_output_with_segment(cortex_service, mock_journal): "output_path": str(expected_path), } - cortex_service._write_output(agent_id, result, config) + cortex_service._write_output(use_id, result, config) assert expected_path.exists() assert expected_path.read_text() == result @@ -669,16 +670,16 @@ def test_write_output_with_segment(cortex_service, mock_journal): def test_write_output_json_format(cortex_service, mock_journal): """Test writing agent output in JSON format.""" - agent_id = "json_agent" + use_id = "json_agent" result = '{"key": "value"}' - expected_path = mock_journal / "20240115" / "agents" / "data_agent.json" + expected_path = mock_journal / "20240115" / "talents" / "data_agent.json" config = { "output": "json", "name": "data_agent", "output_path": str(expected_path), } - cortex_service._write_output(agent_id, result, config) + cortex_service._write_output(use_id, result, config) assert expected_path.exists() assert expected_path.read_text() == result @@ -686,15 +687,15 @@ def test_write_output_json_format(cortex_service, mock_journal): def test_monitor_stdout_with_output(cortex_service, mock_journal): """Test monitor_stdout writes output when output_path is present.""" - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "output_test" - active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" - output_path = mock_journal / "20240115" / "agents" / "test_agent.md" + use_id = "output_test" + active_path = mock_journal / "talents" / f"{use_id}_active.jsonl" + output_path = mock_journal / "20240115" / "talents" / "test_agent.md" # Store request with explicit output_path - cortex_service.agent_requests = { - agent_id: { + cortex_service.use_requests = { + use_id: { "event": "request", "prompt": "test", "output": "md", @@ -711,9 +712,9 @@ def test_monitor_stdout_with_output(cortex_service, mock_journal): mock_process.stdout = MockPipe(mock_stdout) mock_process.wait.return_value = 0 - agent = AgentProcess(agent_id, mock_process, active_path) + agent = TalentProcess(use_id, mock_process, active_path) - with patch.object(cortex_service, "_complete_agent_file"): + with patch.object(cortex_service, "_complete_use_file"): with patch.object(cortex_service, "_has_finish_event", return_value=True): cortex_service._monitor_stdout(agent) @@ -723,16 +724,16 @@ def test_monitor_stdout_with_output(cortex_service, mock_journal): def test_monitor_stdout_with_output_and_day(cortex_service, mock_journal): """Test monitor_stdout writes output to specific day via output_path.""" - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "output_day_test" - active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "output_day_test" + active_path = mock_journal / "talents" / f"{use_id}_active.jsonl" specified_day = "20240220" - output_path = mock_journal / specified_day / "agents" / "daily_reporter.md" + output_path = mock_journal / specified_day / "talents" / "daily_reporter.md" # Store request with explicit output_path and day - cortex_service.agent_requests = { - agent_id: { + cortex_service.use_requests = { + use_id: { "event": "request", "prompt": "test", "output": "md", @@ -750,9 +751,9 @@ def test_monitor_stdout_with_output_and_day(cortex_service, mock_journal): mock_process.stdout = MockPipe(mock_stdout) mock_process.wait.return_value = 0 - agent = AgentProcess(agent_id, mock_process, active_path) + agent = TalentProcess(use_id, mock_process, active_path) - with patch.object(cortex_service, "_complete_agent_file"): + with patch.object(cortex_service, "_complete_use_file"): with patch.object(cortex_service, "_has_finish_event", return_value=True): cortex_service._monitor_stdout(agent) @@ -760,11 +761,11 @@ def test_monitor_stdout_with_output_and_day(cortex_service, mock_journal): assert output_path.read_text() == "Daily report content" -def test_recover_orphaned_agents(cortex_service, mock_journal): +def test_recover_orphaned_uses(cortex_service, mock_journal): """Test recovery of orphaned active agent files.""" # Create orphaned active files - agents_dir = mock_journal / "agents" - unified_dir = agents_dir / "unified" + talents_dir = mock_journal / "talents" + unified_dir = talents_dir / "unified" unified_dir.mkdir() agent1_active = unified_dir / "111_active.jsonl" agent2_active = unified_dir / "222_active.jsonl" @@ -773,7 +774,7 @@ def test_recover_orphaned_agents(cortex_service, mock_journal): agent2_active.write_text('{"event": "start", "ts": 2000}\n') active_files = [agent1_active, agent2_active] - cortex_service._recover_orphaned_agents(active_files) + cortex_service._recover_orphaned_uses(active_files) # Check active files were renamed to completed assert not agent1_active.exists() @@ -788,7 +789,7 @@ def test_recover_orphaned_agents(cortex_service, mock_journal): error_event = json.loads(lines1[1]) assert error_event["event"] == "error" assert "Recovered" in error_event["error"] - assert error_event["agent_id"] == "111" + assert error_event["use_id"] == "111" content2 = (unified_dir / "222.jsonl").read_text() lines2 = content2.strip().split("\n") diff --git a/tests/test_cortex_client.py b/tests/test_cortex_client.py index 3982ae076..34b228c0d 100644 --- a/tests/test_cortex_client.py +++ b/tests/test_cortex_client.py @@ -14,11 +14,11 @@ import pytest from think.callosum import CallosumConnection, CallosumServer from think.cortex_client import ( - cortex_agents, cortex_request, - get_agent_end_state, - get_agent_log_status, - wait_for_agents, + cortex_uses, + get_use_end_state, + get_use_log_status, + wait_for_uses, ) from think.models import GPT_5 from think.utils import now_ms @@ -36,7 +36,7 @@ def callosum_server(monkeypatch): tmp_path = Path(tmp_dir) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - (tmp_path / "agents").mkdir(parents=True, exist_ok=True) + (tmp_path / "talents").mkdir(parents=True, exist_ok=True) server = CallosumServer() server_thread = threading.Thread(target=server.start, daemon=True) @@ -84,7 +84,7 @@ def test_cortex_request_broadcasts_to_callosum(callosum_listener): messages = callosum_listener # Create a request - agent_id = cortex_request( + use_id = cortex_request( prompt="Test prompt", name="unified", provider="openai", @@ -102,20 +102,20 @@ def test_cortex_request_broadcasts_to_callosum(callosum_listener): assert msg["name"] == "unified" assert msg["provider"] == "openai" assert msg["model"] == GPT_5 - assert msg["agent_id"] == agent_id + assert msg["use_id"] == use_id assert "ts" in msg def test_cortex_request_returns_agent_id(callosum_server): - """Test that cortex_request returns agent_id string.""" + """Test that cortex_request returns use_id string.""" _ = callosum_server # Needed for side effects only - agent_id = cortex_request(prompt="Test", name="unified", provider="openai") + use_id = cortex_request(prompt="Test", name="unified", provider="openai") - # Verify agent_id is a string timestamp - assert isinstance(agent_id, str) - assert agent_id.isdigit() - assert len(agent_id) == 13 # Millisecond timestamp + # Verify use_id is a string timestamp + assert isinstance(use_id, str) + assert use_id.isdigit() + assert len(use_id) == 13 # Millisecond timestamp def test_cortex_request_unique_agent_ids(callosum_server): @@ -124,8 +124,8 @@ def test_cortex_request_unique_agent_ids(callosum_server): agent_ids = [] for i in range(3): - agent_id = cortex_request(prompt=f"Test {i}", name="unified", provider="openai") - agent_ids.append(agent_id) + use_id = cortex_request(prompt=f"Test {i}", name="unified", provider="openai") + agent_ids.append(use_id) time.sleep(0.002) # All agent IDs should be unique @@ -136,9 +136,9 @@ def test_cortex_request_returns_none_on_send_failure(callosum_server, monkeypatc """Test cortex_request returns None when callosum_send fails.""" monkeypatch.setattr("think.cortex_client.callosum_send", lambda *a, **kw: False) - agent_id = cortex_request(prompt="Test", name="unified", provider="openai") + use_id = cortex_request(prompt="Test", name="unified", provider="openai") - assert agent_id is None + assert use_id is None def test_cortex_request_empty_journal(tmp_path, monkeypatch): @@ -146,21 +146,21 @@ def test_cortex_request_empty_journal(tmp_path, monkeypatch): monkeypatch.setattr("think.cortex_client.callosum_send", lambda *a, **kw: True) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agent_id = cortex_request("test", "unified", "openai") - assert agent_id is not None - assert len(agent_id) > 0 + use_id = cortex_request("test", "unified", "openai") + assert use_id is not None + assert len(use_id) > 0 -# Tests for cortex_agents remain mostly unchanged as they read from files +# Tests for cortex_uses remain mostly unchanged as they read from files def test_cortex_agents_empty(tmp_path, monkeypatch): - """Test cortex_agents with no agents.""" + """Test cortex_uses with no agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - result = cortex_agents() + result = cortex_uses() - assert result["agents"] == [] + assert result["uses"] == [] assert result["pagination"]["total"] == 0 assert result["pagination"]["has_more"] is False assert result["live_count"] == 0 @@ -168,17 +168,17 @@ def test_cortex_agents_empty(tmp_path, monkeypatch): def test_cortex_agents_with_active(tmp_path, monkeypatch): - """Test cortex_agents with active (running) agents.""" + """Test cortex_uses with active (running) agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() + talents_dir = tmp_path / "talents" + talents_dir.mkdir() # Create active agent files ts1 = now_ms() ts2 = ts1 + 1000 - unified_dir = agents_dir / "unified" - tester_dir = agents_dir / "tester" + unified_dir = talents_dir / "unified" + tester_dir = talents_dir / "tester" unified_dir.mkdir() tester_dir.mkdir() @@ -210,22 +210,22 @@ def test_cortex_agents_with_active(tmp_path, monkeypatch): ) f.write("\n") - result = cortex_agents() + result = cortex_uses() - assert len(result["agents"]) == 2 + assert len(result["uses"]) == 2 assert result["live_count"] == 2 assert result["historical_count"] == 0 def test_cortex_agents_with_completed(tmp_path, monkeypatch): - """Test cortex_agents with completed (historical) agents.""" + """Test cortex_uses with completed (historical) agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() + talents_dir = tmp_path / "talents" + talents_dir.mkdir() # Create completed agent files ts1 = now_ms() - reviewer_dir = agents_dir / "reviewer" + reviewer_dir = talents_dir / "reviewer" reviewer_dir.mkdir() completed_file1 = reviewer_dir / f"{ts1}.jsonl" @@ -244,23 +244,23 @@ def test_cortex_agents_with_completed(tmp_path, monkeypatch): json.dump({"event": "finish", "ts": ts1 + 100, "result": "Done"}, f) f.write("\n") - result = cortex_agents() + result = cortex_uses() - assert len(result["agents"]) == 1 + assert len(result["uses"]) == 1 assert result["live_count"] == 0 assert result["historical_count"] == 1 - assert result["agents"][0]["status"] == "completed" + assert result["uses"][0]["status"] == "completed" def test_cortex_agents_pagination(tmp_path, monkeypatch): - """Test cortex_agents pagination.""" + """Test cortex_uses pagination.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() + talents_dir = tmp_path / "talents" + talents_dir.mkdir() # Create multiple agents base_ts = now_ms() - unified_dir = agents_dir / "unified" + unified_dir = talents_dir / "unified" unified_dir.mkdir() for i in range(5): ts = base_ts + (i * 1000) @@ -278,151 +278,151 @@ def test_cortex_agents_pagination(tmp_path, monkeypatch): f.write("\n") # Test limit - result = cortex_agents(limit=2) - assert len(result["agents"]) == 2 + result = cortex_uses(limit=2) + assert len(result["uses"]) == 2 assert result["pagination"]["limit"] == 2 assert result["pagination"]["total"] == 5 assert result["pagination"]["has_more"] is True def test_cortex_agents_empty_journal(tmp_path, monkeypatch): - """Test cortex_agents works with an empty journal directory.""" + """Test cortex_uses works with an empty journal directory.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - result = cortex_agents() - assert "agents" in result + result = cortex_uses() + assert "uses" in result assert "pagination" in result - assert isinstance(result["agents"], list) + assert isinstance(result["uses"], list) def test_get_agent_log_status_completed(tmp_path, monkeypatch): - """Test get_agent_log_status returns 'completed' for finished agents.""" + """Test get_use_log_status returns 'completed' for finished agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + use_id = "1234567890123" + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') - assert get_agent_log_status(agent_id) == "completed" + assert get_use_log_status(use_id) == "completed" def test_get_agent_log_status_running(tmp_path, monkeypatch): - """Test get_agent_log_status returns 'running' for active agents.""" + """Test get_use_log_status returns 'running' for active agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') + use_id = "1234567890123" + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') - assert get_agent_log_status(agent_id) == "running" + assert get_use_log_status(use_id) == "running" def test_get_agent_log_status_not_found(tmp_path, monkeypatch): - """Test get_agent_log_status returns 'not_found' for missing agents.""" + """Test get_use_log_status returns 'not_found' for missing agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - (tmp_path / "agents").mkdir() + (tmp_path / "talents").mkdir() - assert get_agent_log_status("nonexistent") == "not_found" + assert get_use_log_status("nonexistent") == "not_found" def test_get_agent_log_status_prefers_completed(tmp_path, monkeypatch): - """Test get_agent_log_status returns 'completed' when both files exist.""" + """Test get_use_log_status returns 'completed' when both files exist.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() # Edge case: both files exist (shouldn't happen, but check precedence) - agent_id = "1234567890123" - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') + use_id = "1234567890123" + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') - assert get_agent_log_status(agent_id) == "completed" + assert get_use_log_status(use_id) == "completed" def test_get_agent_end_state_finish(tmp_path, monkeypatch): - """Test get_agent_end_state returns 'finish' for successful agents.""" + """Test get_use_end_state returns 'finish' for successful agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}.jsonl").write_text( + use_id = "1234567890123" + (unified_dir / f"{use_id}.jsonl").write_text( '{"event": "request", "prompt": "hello"}\n' '{"event": "finish", "result": "done"}\n' ) - assert get_agent_end_state(agent_id) == "finish" + assert get_use_end_state(use_id) == "finish" def test_get_agent_end_state_error(tmp_path, monkeypatch): - """Test get_agent_end_state returns 'error' for failed agents.""" + """Test get_use_end_state returns 'error' for failed agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}.jsonl").write_text( + use_id = "1234567890123" + (unified_dir / f"{use_id}.jsonl").write_text( '{"event": "request", "prompt": "hello"}\n' '{"event": "error", "error": "something went wrong"}\n' ) - assert get_agent_end_state(agent_id) == "error" + assert get_use_end_state(use_id) == "error" def test_get_agent_end_state_running(tmp_path, monkeypatch): - """Test get_agent_end_state returns 'running' for active agents.""" + """Test get_use_end_state returns 'running' for active agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}_active.jsonl").write_text( + use_id = "1234567890123" + (unified_dir / f"{use_id}_active.jsonl").write_text( '{"event": "request", "prompt": "hello"}\n' ) - assert get_agent_end_state(agent_id) == "running" + assert get_use_end_state(use_id) == "running" def test_get_agent_end_state_unknown(tmp_path, monkeypatch): - """Test get_agent_end_state returns 'unknown' for missing agents.""" + """Test get_use_end_state returns 'unknown' for missing agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - (tmp_path / "agents").mkdir() + (tmp_path / "talents").mkdir() - assert get_agent_end_state("nonexistent") == "unknown" + assert get_use_end_state("nonexistent") == "unknown" -# Tests for wait_for_agents +# Tests for wait_for_uses def test_wait_for_agents_already_complete(tmp_path, monkeypatch): - """Test wait_for_agents returns immediately if agents already completed.""" + """Test wait_for_uses returns immediately if agents already completed.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() (tmp_path / "health").mkdir() # Create completed agents agent_ids = ["1000", "2000"] - for agent_id in agent_ids: - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + for use_id in agent_ids: + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') - completed, timed_out = wait_for_agents(agent_ids, timeout=1) + completed, timed_out = wait_for_uses(agent_ids, timeout=1) assert set(completed.keys()) == set(agent_ids) assert all(v == "finish" for v in completed.values()) @@ -430,21 +430,19 @@ def test_wait_for_agents_already_complete(tmp_path, monkeypatch): def test_wait_for_agents_event_completion(callosum_server): - """Test wait_for_agents completes when finish event is received.""" + """Test wait_for_uses completes when finish event is received.""" tmp_path = callosum_server - agents_dir = tmp_path / "agents" - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + unified_dir = talents_dir / "unified" unified_dir.mkdir(exist_ok=True) - agent_id = "1234567890123" + use_id = "1234567890123" # Start wait in background thread result = {"completed": None, "timed_out": None} def wait_thread(): - result["completed"], result["timed_out"] = wait_for_agents( - [agent_id], timeout=5 - ) + result["completed"], result["timed_out"] = wait_for_uses([use_id], timeout=5) waiter = threading.Thread(target=wait_thread) waiter.start() @@ -453,103 +451,101 @@ def test_wait_for_agents_event_completion(callosum_server): time.sleep(0.2) # Create the completed file and emit finish event - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') # Emit finish event via Callosum client = CallosumConnection() client.start() time.sleep(0.1) - client.emit("cortex", "finish", agent_id=agent_id, result="done") + client.emit("cortex", "finish", use_id=use_id, result="done") time.sleep(0.2) client.stop() waiter.join(timeout=3) - assert result["completed"] == {agent_id: "finish"} + assert result["completed"] == {use_id: "finish"} assert result["timed_out"] == [] def test_wait_for_agents_error_event(callosum_server): - """Test wait_for_agents completes on error event too.""" + """Test wait_for_uses completes on error event too.""" tmp_path = callosum_server - agents_dir = tmp_path / "agents" - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + unified_dir = talents_dir / "unified" unified_dir.mkdir(exist_ok=True) - agent_id = "1234567890124" + use_id = "1234567890124" result = {"completed": None, "timed_out": None} def wait_thread(): - result["completed"], result["timed_out"] = wait_for_agents( - [agent_id], timeout=5 - ) + result["completed"], result["timed_out"] = wait_for_uses([use_id], timeout=5) waiter = threading.Thread(target=wait_thread) waiter.start() time.sleep(0.2) # Create completed file and emit error event - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "error"}\n') + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "error"}\n') client = CallosumConnection() client.start() time.sleep(0.1) - client.emit("cortex", "error", agent_id=agent_id, error="something failed") + client.emit("cortex", "error", use_id=use_id, error="something failed") time.sleep(0.2) client.stop() waiter.join(timeout=3) - assert result["completed"] == {agent_id: "error"} + assert result["completed"] == {use_id: "error"} assert result["timed_out"] == [] def test_wait_for_agents_initial_file_check(tmp_path, monkeypatch): - """Test wait_for_agents finds already-completed agents via initial file check.""" + """Test wait_for_uses finds already-completed agents via initial file check.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() (tmp_path / "health").mkdir() - agent_id = "1234567890125" + use_id = "1234567890125" # Agent already completed before we start waiting - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') - completed, timed_out = wait_for_agents([agent_id], timeout=1) + completed, timed_out = wait_for_uses([use_id], timeout=1) # Should find via initial file check - assert completed == {agent_id: "finish"} + assert completed == {use_id: "finish"} assert timed_out == [] def test_wait_for_agents_timeout_actual(tmp_path, monkeypatch): - """Test wait_for_agents times out for agents that never complete.""" + """Test wait_for_uses times out for agents that never complete.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() (tmp_path / "health").mkdir() - agent_id = "1234567890126" + use_id = "1234567890126" # Create active file (not completed) - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') - completed, timed_out = wait_for_agents([agent_id], timeout=1) + completed, timed_out = wait_for_uses([use_id], timeout=1) assert completed == {} - assert timed_out == [agent_id] + assert timed_out == [use_id] def test_wait_for_agents_partial(callosum_server): - """Test wait_for_agents with some completing and some timing out.""" + """Test wait_for_uses with some completing and some timing out.""" tmp_path = callosum_server - agents_dir = tmp_path / "agents" - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + unified_dir = talents_dir / "unified" unified_dir.mkdir(exist_ok=True) completing_agent = "1111" @@ -561,7 +557,7 @@ def test_wait_for_agents_partial(callosum_server): result = {"completed": None, "timed_out": None} def wait_thread(): - result["completed"], result["timed_out"] = wait_for_agents( + result["completed"], result["timed_out"] = wait_for_uses( [completing_agent, timeout_agent], timeout=2 ) @@ -575,7 +571,7 @@ def test_wait_for_agents_partial(callosum_server): client = CallosumConnection() client.start() time.sleep(0.1) - client.emit("cortex", "finish", agent_id=completing_agent, result="done") + client.emit("cortex", "finish", use_id=completing_agent, result="done") time.sleep(0.1) client.stop() @@ -590,41 +586,39 @@ def test_wait_for_agents_missed_event_recovery(tmp_path, monkeypatch, caplog): import logging monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() (tmp_path / "health").mkdir() - agent_id = "1234567890127" + use_id = "1234567890127" # Start with active file - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') result = {"completed": None, "timed_out": None} def wait_and_complete(): # Wait a bit then "complete" the agent by renaming file time.sleep(0.3) - (unified_dir / f"{agent_id}_active.jsonl").unlink() - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + (unified_dir / f"{use_id}_active.jsonl").unlink() + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') completer = threading.Thread(target=wait_and_complete) completer.start() with caplog.at_level(logging.INFO): - result["completed"], result["timed_out"] = wait_for_agents( - [agent_id], timeout=1 - ) + result["completed"], result["timed_out"] = wait_for_uses([use_id], timeout=1) completer.join() # Should recover via final file check - assert result["completed"] == {agent_id: "finish"} + assert result["completed"] == {use_id: "finish"} assert result["timed_out"] == [] # Should log about missed event assert any( - "completion event not received but agent completed" in record.message + "completion event not received but use completed" in record.message for record in caplog.records ) diff --git a/tests/test_dream_activity.py b/tests/test_dream_activity.py index 9ff8e0839..09fa8a05d 100644 --- a/tests/test_dream_activity.py +++ b/tests/test_dream_activity.py @@ -114,7 +114,7 @@ class TestRunActivityPrompts: monkeypatch.setattr("think.dream.cortex_request", mock_cortex_request) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "finish" for aid in ids}, []), ) @@ -170,7 +170,7 @@ class TestRunActivityPrompts: monkeypatch.setattr("think.dream.cortex_request", mock_cortex_request) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "finish" for aid in ids}, []), ) @@ -220,7 +220,7 @@ class TestRunActivityPrompts: monkeypatch.setattr("think.dream.cortex_request", mock_cortex_request) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "finish" for aid in ids}, []), ) @@ -282,7 +282,7 @@ class TestRunActivityPrompts: lambda prompt, name, config: "agent-1", ) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "error" for aid in ids}, []), ) @@ -358,7 +358,7 @@ class TestRunActivityPrompts: lambda prompt, name, config: "agent-1", ) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "finish" for aid in ids}, []), ) @@ -376,8 +376,8 @@ class TestRunActivityPrompts: events = [e[0] for e in emitted] assert "started" in events assert "group_started" in events - assert "agent_started" in events - assert "agent_completed" in events + assert "talent_started" in events + assert "talent_completed" in events assert "group_completed" in events assert "completed" in events @@ -889,7 +889,7 @@ class TestActivityTemplateVars: """Tests for activity template variables in _build_prompt_context.""" def test_activity_vars_populated(self): - from think.agents import _build_prompt_context + from think.talents import _build_prompt_context activity = { "id": "coding_100000_300", @@ -916,7 +916,7 @@ class TestActivityTemplateVars: assert int(ctx["activity_duration"]) == 10 # 2 * 300s = 10 min def test_no_activity_no_vars(self): - from think.agents import _build_prompt_context + from think.talents import _build_prompt_context ctx = _build_prompt_context("20260209", None, None) @@ -924,7 +924,7 @@ class TestActivityTemplateVars: assert "activity_type" not in ctx def test_empty_entities(self): - from think.agents import _build_prompt_context + from think.talents import _build_prompt_context activity = { "id": "browsing_100000_300", @@ -940,7 +940,7 @@ class TestActivityTemplateVars: assert ctx["activity_entities"] == "" def test_duration_minimum_one(self): - from think.agents import _build_prompt_context + from think.talents import _build_prompt_context activity = { "id": "test_bad_seg", diff --git a/tests/test_dream_segment.py b/tests/test_dream_segment.py index 0e0a3f3d8..71fe677ab 100644 --- a/tests/test_dream_segment.py +++ b/tests/test_dream_segment.py @@ -17,7 +17,7 @@ def segment_dir(tmp_path, monkeypatch): day_dir = journal / "chronicle" / "20240115" segment_path = day_dir / "default" / "120000_300" segment_path.mkdir(parents=True) - (segment_path / "agents").mkdir(parents=True) + (segment_path / "talents").mkdir(parents=True) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) return segment_path @@ -57,7 +57,7 @@ def _segment_configs(*names: str) -> dict[str, dict]: def _write_sense_output(segment_dir: Path, sense_json: dict) -> None: - (segment_dir / "agents" / "sense.json").write_text( + (segment_dir / "talents" / "sense.json").write_text( json.dumps(sense_json), encoding="utf-8", ) @@ -74,13 +74,13 @@ class TestLoadSegmentFacets: def test_empty_file_returns_empty(self, segment_dir): from think.facets import load_segment_facets - (segment_dir / "agents" / "facets.json").write_text("") + (segment_dir / "talents" / "facets.json").write_text("") assert load_segment_facets("20240115", "120000_300") == [] def test_empty_array_returns_empty(self, segment_dir): from think.facets import load_segment_facets - (segment_dir / "agents" / "facets.json").write_text("[]") + (segment_dir / "talents" / "facets.json").write_text("[]") assert load_segment_facets("20240115", "120000_300") == [] def test_valid_facets_extracted(self, segment_dir): @@ -90,21 +90,21 @@ class TestLoadSegmentFacets: {"facet": "work", "activity": "Code review", "level": "high"}, {"facet": "personal", "activity": "Email check", "level": "low"}, ] - (segment_dir / "agents" / "facets.json").write_text(json.dumps(facets_data)) + (segment_dir / "talents" / "facets.json").write_text(json.dumps(facets_data)) assert load_segment_facets("20240115", "120000_300") == ["work", "personal"] def test_malformed_json_returns_empty(self, segment_dir, caplog): from think.facets import load_segment_facets - (segment_dir / "agents" / "facets.json").write_text("{ invalid json") + (segment_dir / "talents" / "facets.json").write_text("{ invalid json") assert load_segment_facets("20240115", "120000_300") == [] assert "Failed to parse facets.json" in caplog.text def test_non_array_returns_empty(self, segment_dir, caplog): from think.facets import load_segment_facets - (segment_dir / "agents" / "facets.json").write_text('{"facet": "work"}') + (segment_dir / "talents" / "facets.json").write_text('{"facet": "work"}') assert load_segment_facets("20240115", "120000_300") == [] assert "not an array" in caplog.text @@ -116,7 +116,7 @@ class TestLoadSegmentFacets: {"activity": "Unknown"}, {"facet": "personal", "activity": "Email"}, ] - (segment_dir / "agents" / "facets.json").write_text(json.dumps(facets_data)) + (segment_dir / "talents" / "facets.json").write_text(json.dumps(facets_data)) assert load_segment_facets("20240115", "120000_300") == ["work", "personal"] @@ -143,7 +143,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -197,7 +197,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -221,7 +221,7 @@ class TestRunSegmentSense: "20240115", ) ] - density = json.loads((segment_dir / "agents" / "density.json").read_text()) + density = json.loads((segment_dir / "talents" / "density.json").read_text()) assert density["classification"] == "idle" # Verify activity state persisted even on idle path @@ -255,7 +255,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -315,7 +315,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -351,7 +351,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -392,7 +392,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -431,7 +431,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -469,7 +469,7 @@ class TestRunSegmentSense: def mock_wait_for_agents(agent_ids, timeout=600): return ({agent_ids[0]: "error"}, []) - monkeypatch.setattr(dream, "wait_for_agents", mock_wait_for_agents) + monkeypatch.setattr(dream, "wait_for_uses", mock_wait_for_agents) monkeypatch.setattr(dream, "_callosum", None) success, failed, failed_names = dream.run_segment_sense( @@ -529,7 +529,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr( @@ -582,7 +582,7 @@ class TestRunSegmentSense: segment_dir, {"density": "active", "recommend": {}, "facets": []}, ) - (segment_dir / "agents" / "entities.md").write_text( + (segment_dir / "talents" / "entities.md").write_text( "entities", encoding="utf-8" ) @@ -606,7 +606,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr( @@ -652,7 +652,7 @@ class TestRunSegmentSense: monkeypatch.setattr(dream, "_SEND_RETRY_DELAYS", (0.0, 0.0)) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -886,7 +886,9 @@ class TestDreamJSONLWriter: path = tmp_path / "test.jsonl" writer = DreamJSONLWriter(str(path)) writer.log("run.start", mode="segment", day="20240115") - writer.log("agent.skip", name="screen", reason="not_recommended", detail="test") + writer.log( + "talent.skip", name="screen", reason="not_recommended", detail="test" + ) writer.close() lines = path.read_text().strip().split("\n") @@ -899,7 +901,7 @@ class TestDreamJSONLWriter: assert first["mode"] == "segment" second = json.loads(lines[1]) - assert second["event"] == "agent.skip" + assert second["event"] == "talent.skip" assert writer.skip_count == 1 def test_creates_parent_dirs(self, tmp_path): @@ -917,7 +919,7 @@ class TestDreamJSONLEvents: """Tests for JSONL event emission during segment orchestration.""" def test_density_idle_skip_event(self, segment_dir, monkeypatch): - """JSONL emits agent.skip with reason=density_idle for idle segments.""" + """JSONL emits talent.skip with reason=density_idle for idle segments.""" from think import dream from think.dream import DreamJSONLWriter @@ -941,7 +943,7 @@ class TestDreamJSONLEvents: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -960,7 +962,7 @@ class TestDreamJSONLEvents: json.loads(line) for line in jsonl_path.read_text(encoding="utf-8").strip().splitlines() ] - skips = [event for event in events if event["event"] == "agent.skip"] + skips = [event for event in events if event["event"] == "talent.skip"] assert any(skip["reason"] == "density_idle" for skip in skips) @@ -996,7 +998,7 @@ class TestDreamJSONLEvents: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -1017,7 +1019,7 @@ class TestDreamJSONLEvents: ] assert "sense.complete" in [event["event"] for event in events] - skips = [event for event in events if event["event"] == "agent.skip"] + skips = [event for event in events if event["event"] == "talent.skip"] skip_pairs = {(event["name"], event["reason"]) for event in skips} assert ("documents", "no_config") in skip_pairs assert ("screen", "not_recommended") in skip_pairs diff --git a/tests/test_engage.py b/tests/test_engage.py index 10d703ef3..2c3195b6f 100644 --- a/tests/test_engage.py +++ b/tests/test_engage.py @@ -50,11 +50,11 @@ class TestEngage: with ( patch("think.cortex_client.cortex_request", return_value="agent-123"), patch( - "think.cortex_client.wait_for_agents", + "think.cortex_client.wait_for_uses", return_value=({"agent-123": "finish"}, []), ), patch( - "think.cortex_client.read_agent_events", + "think.cortex_client.read_use_events", return_value=[{"event": "finish", "result": "All fixed!"}], ), ): @@ -67,7 +67,7 @@ class TestEngage: with ( patch("think.cortex_client.cortex_request", return_value="agent-123"), patch( - "think.cortex_client.wait_for_agents", + "think.cortex_client.wait_for_uses", return_value=({"agent-123": "error"}, []), ), ): @@ -79,7 +79,7 @@ class TestEngage: with ( patch("think.cortex_client.cortex_request", return_value="agent-123"), patch( - "think.cortex_client.wait_for_agents", + "think.cortex_client.wait_for_uses", return_value=({}, ["agent-123"]), ), ): diff --git a/tests/test_entities.py b/tests/test_entities.py index a70915477..4a81ed0f1 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -1742,7 +1742,7 @@ def test_parse_knowledge_graph_entities(tmp_path): os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) # Create a knowledge graph file - day_dir = tmp_path / "chronicle" / "20260108" / "agents" + day_dir = tmp_path / "chronicle" / "20260108" / "talents" day_dir.mkdir(parents=True) kg_content = """# Knowledge Graph Report @@ -1790,7 +1790,7 @@ def test_parse_knowledge_graph_entities_empty_file(tmp_path): """Test parsing returns empty list for empty KG.""" os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) - day_dir = tmp_path / "chronicle" / "20260108" / "agents" + day_dir = tmp_path / "chronicle" / "20260108" / "talents" day_dir.mkdir(parents=True) (day_dir / "knowledge_graph.md").write_text("") diff --git a/tests/test_entity_observer_context.py b/tests/test_entity_observer_context.py index 94389ed55..c1c17a32d 100644 --- a/tests/test_entity_observer_context.py +++ b/tests/test_entity_observer_context.py @@ -13,7 +13,7 @@ from think.entities.journal import clear_journal_entity_cache from think.entities.loading import clear_entity_loading_cache from think.entities.observations import clear_observation_cache, load_observations from think.entities.relationships import clear_relationship_caches -from think.talent import get_agent +from think.talent import get_talent def _set_journal(path: str) -> None: @@ -323,7 +323,7 @@ def test_post_process_handles_malformed_json(): def test_entity_observer_agent_config(): _set_journal("tests/fixtures/journal") - config = get_agent("entities:entity_observer") + config = get_talent("entities:entity_observer") assert config["type"] == "generate" assert config.get("output") == "json" diff --git a/tests/test_entity_agents.py b/tests/test_entity_talents.py similarity index 88% rename from tests/test_entity_agents.py rename to tests/test_entity_talents.py index dc6191ce1..490261559 100644 --- a/tests/test_entity_agents.py +++ b/tests/test_entity_talents.py @@ -7,7 +7,7 @@ import os import pytest -from think.talent import get_agent +from think.talent import get_talent @pytest.fixture @@ -21,7 +21,7 @@ def fixture_journal(): def test_entities_agent_config(fixture_journal): """Test detection agent configuration loads correctly.""" # Entity agents are in apps/entities/talent/ so use app-qualified name - config = get_agent("entities:entities") + config = get_talent("entities:entities") # Verify required fields assert config["name"] == "entities:entities" @@ -38,7 +38,7 @@ def test_entities_agent_config(fixture_journal): def test_entities_review_agent_config(fixture_journal): """Test review agent configuration loads correctly.""" # Entity agents are in apps/entities/talent/ so use app-qualified name - config = get_agent("entities:entities_review") + config = get_talent("entities:entities_review") # Verify required fields assert config["name"] == "entities:entities_review" @@ -54,7 +54,7 @@ def test_entities_review_agent_config(fixture_journal): def test_entities_agent_instruction_content(fixture_journal): """Test detection agent instruction contains expected sections.""" - config = get_agent("entities:entities") + config = get_talent("entities:entities") prompt = config["user_instruction"] # Check for key sections in the agent prompt @@ -67,7 +67,7 @@ def test_entities_agent_instruction_content(fixture_journal): def test_entities_review_agent_instruction_content(fixture_journal): """Test review agent instruction contains expected sections.""" - config = get_agent("entities:entities_review") + config = get_talent("entities:entities_review") prompt = config["user_instruction"] # Check for key sections in the agent prompt @@ -80,7 +80,7 @@ def test_entities_review_agent_instruction_content(fixture_journal): def test_agent_context_includes_entities_by_facet(fixture_journal): """Test that agent context includes entities grouped by facet.""" - config = get_agent("entities:entities") + config = get_talent("entities:entities") prompt = config["user_instruction"] assert "Available Facets" in prompt @@ -97,8 +97,8 @@ def test_agent_context_includes_entities_by_facet(fixture_journal): def test_agent_context_with_facet_focus(fixture_journal): - """Test that get_agent with facet parameter uses focused single-facet context.""" - config = get_agent("unified", facet="full-featured") + """Test that get_talent with facet parameter uses focused single-facet context.""" + config = get_talent("unified", facet="full-featured") prompt = config["user_instruction"] @@ -117,8 +117,8 @@ def test_agent_context_with_facet_focus(fixture_journal): def test_agent_priority_ordering(fixture_journal): """Test that entity agents have correct priority ordering.""" - detection_config = get_agent("entities:entities") - review_config = get_agent("entities:entities_review") + detection_config = get_talent("entities:entities") + review_config = get_talent("entities:entities_review") detection_priority = detection_config["priority"] review_priority = review_config["priority"] diff --git a/tests/test_facets.py b/tests/test_facets.py index 4ca40da2c..0124aadfd 100644 --- a/tests/test_facets.py +++ b/tests/test_facets.py @@ -346,7 +346,7 @@ def test_get_active_facets_from_segment_facets(monkeypatch, tmp_path): day_dir = journal / "chronicle" / "20240115" # Create segment with facets.json containing two facets (stream layout) - seg1 = day_dir / "archon" / "100000_300" / "agents" + seg1 = day_dir / "archon" / "100000_300" / "talents" seg1.mkdir(parents=True) (seg1 / "facets.json").write_text( json.dumps( @@ -358,7 +358,7 @@ def test_get_active_facets_from_segment_facets(monkeypatch, tmp_path): ) # Create another segment with overlapping + new facet - seg2 = day_dir / "archon" / "110000_300" / "agents" + seg2 = day_dir / "archon" / "110000_300" / "talents" seg2.mkdir(parents=True) (seg2 / "facets.json").write_text( json.dumps( @@ -382,12 +382,12 @@ def test_get_active_facets_empty_segments(monkeypatch, tmp_path): day_dir = journal / "chronicle" / "20240115" # Segment with empty facets array (stream layout) - seg1 = day_dir / "archon" / "100000_300" / "agents" + seg1 = day_dir / "archon" / "100000_300" / "talents" seg1.mkdir(parents=True) (seg1 / "facets.json").write_text("[]") # Segment with empty file - seg2 = day_dir / "archon" / "110000_300" / "agents" + seg2 = day_dir / "archon" / "110000_300" / "talents" seg2.mkdir(parents=True) (seg2 / "facets.json").write_text("") @@ -428,12 +428,12 @@ def test_get_active_facets_malformed_json(monkeypatch, tmp_path): day_dir = journal / "chronicle" / "20240115" # Malformed JSON segment (stream layout) - seg1 = day_dir / "archon" / "100000_300" / "agents" + seg1 = day_dir / "archon" / "100000_300" / "talents" seg1.mkdir(parents=True) (seg1 / "facets.json").write_text("{ invalid json") # Valid segment - seg2 = day_dir / "archon" / "110000_300" / "agents" + seg2 = day_dir / "archon" / "110000_300" / "talents" seg2.mkdir(parents=True) (seg2 / "facets.json").write_text( json.dumps( diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 79b75c847..ad06f273f 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -1016,7 +1016,7 @@ class TestFormatEvents: "title": "Project kickoff", "start": "14:00:00", "occurred": False, - "source": "20240101/agents/schedule.md", + "source": "20240101/talents/schedule.md", "participants": ["Alice", "Bob"], } ] @@ -1039,7 +1039,7 @@ class TestFormatEvents: "title": "Team standup", "start": "09:00:00", "occurred": True, - "source": "20240101/agents/meetings.md", + "source": "20240101/talents/meetings.md", "participants": ["Alice"], } ] @@ -1168,7 +1168,7 @@ class TestFormatMarkdown: """Test pattern matching for .md files.""" from think.formatters import get_formatter - formatter = get_formatter("20240101/agents/flow.md") + formatter = get_formatter("20240101/talents/flow.md") assert formatter is not None assert formatter.__name__ == "format_markdown" @@ -1176,7 +1176,7 @@ class TestFormatMarkdown: """Test pattern matching for segment screen.md files.""" from think.formatters import get_formatter - formatter = get_formatter("20240101/default/123456_300/agents/screen.md") + formatter = get_formatter("20240101/default/123456_300/talents/screen.md") assert formatter is not None assert formatter.__name__ == "format_markdown" @@ -1282,7 +1282,7 @@ class TestFormatMarkdown: path = ( Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) - / "chronicle/20240101/agents/flow.md" + / "chronicle/20240101/talents/flow.md" ) chunks, meta = format_file(path) @@ -1296,7 +1296,7 @@ class TestFormatMarkdown: path = ( Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) - / "chronicle/20240101/agents/flow.md" + / "chronicle/20240101/talents/flow.md" ) text = load_markdown(path) @@ -1373,7 +1373,7 @@ class TestExtractPathMetadata: """Test day extraction from daily agent output path.""" from think.formatters import extract_path_metadata - meta = extract_path_metadata("20240101/agents/flow.md") + meta = extract_path_metadata("20240101/talents/flow.md") assert meta["day"] == "20240101" assert meta["facet"] == "" assert meta["agent"] == "flow" @@ -1382,7 +1382,7 @@ class TestExtractPathMetadata: """Test day and agent extraction from segment markdown.""" from think.formatters import extract_path_metadata - meta = extract_path_metadata("20240101/100000/agents/screen.md") + meta = extract_path_metadata("20240101/100000/talents/screen.md") assert meta["day"] == "20240101" assert meta["facet"] == "" assert meta["agent"] == "screen" @@ -1445,7 +1445,7 @@ class TestExtractPathMetadata: """Test app output path extraction.""" from think.formatters import extract_path_metadata - meta = extract_path_metadata("apps/myapp/agents/custom.md") + meta = extract_path_metadata("apps/myapp/talents/custom.md") assert meta["day"] == "" assert meta["facet"] == "" assert meta["agent"] == "myapp:custom" @@ -1727,7 +1727,7 @@ class TestFormatLogs: assert "- text: Test task" in chunks[0]["markdown"] def test_format_logs_with_agent_id(self): - """Test that agent_id renders as a link.""" + """Test that use_id renders as a link.""" from think.facets import format_logs entries = [ @@ -1737,7 +1737,7 @@ class TestFormatLogs: "actor": "entities", "action": "entity_attach", "params": {"type": "Person", "name": "Alice"}, - "agent_id": "1765870373972", + "use_id": "1765870373972", } ] @@ -1745,7 +1745,7 @@ class TestFormatLogs: assert len(chunks) == 1 assert ( - "**Agent:** [1765870373972](/app/sol/1765870373972)" + "**Talent:** [1765870373972](/app/sol/1765870373972)" in chunks[0]["markdown"] ) diff --git a/tests/test_generate_full.py b/tests/test_generate_full.py index 814e6cd22..182482609 100644 --- a/tests/test_generate_full.py +++ b/tests/test_generate_full.py @@ -65,7 +65,7 @@ def run_generator_with_config(mod, config: dict, monkeypatch) -> list[dict]: def test_generate_output_ndjson(tmp_path, monkeypatch): """Test basic output generation via NDJSON protocol.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -111,7 +111,7 @@ def test_generate_output_ndjson(tmp_path, monkeypatch): def test_generate_hook_invoked_with_context(tmp_path, monkeypatch): """Test that hooks receive correct context including span flag.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -171,7 +171,7 @@ def post_process(result, context): # Read captured context captured_path = ( - tmp_path / "chronicle" / "20240101" / "agents" / "context_captured.json" + tmp_path / "chronicle" / "20240101" / "talents" / "context_captured.json" ) captured = json.loads(captured_path.read_text()) @@ -186,7 +186,7 @@ def post_process(result, context): def test_generate_without_hook_succeeds(tmp_path, monkeypatch): """Test that generators without hooks still work correctly.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -228,7 +228,7 @@ def test_generate_without_hook_succeeds(tmp_path, monkeypatch): def test_generate_error_event_on_missing_generator(tmp_path, monkeypatch): """Test that missing generator name emits error event.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) @@ -249,7 +249,7 @@ def test_generate_error_event_on_missing_generator(tmp_path, monkeypatch): def test_generate_skipped_on_no_input(tmp_path, monkeypatch): """Test that generator emits skipped finish when no input.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") # Create empty day directory (no transcripts) os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) @@ -286,7 +286,7 @@ def test_generate_skipped_on_no_input(tmp_path, monkeypatch): def test_cogitate_not_skipped_without_sources(tmp_path, monkeypatch): """Test that cogitate agents with day but no sources are not skipped.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") # Create empty day directory (no transcripts) os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) diff --git a/tests/test_generate_scan_day.py b/tests/test_generate_scan_day.py index 5aea3d137..f6c005bd8 100644 --- a/tests/test_generate_scan_day.py +++ b/tests/test_generate_scan_day.py @@ -16,22 +16,22 @@ def copy_day(tmp_path: Path) -> Path: dest = day_path("20240101") src = FIXTURES / "journal" / "chronicle" / "20240101" copytree_tracked(src, dest) - agents_dir = dest / "agents" - agents_dir.mkdir(exist_ok=True) # Allow existing directory - (agents_dir / "flow.md").write_text("done") + talents_dir = dest / "talents" + talents_dir.mkdir(exist_ok=True) # Allow existing directory + (talents_dir / "flow.md").write_text("done") return dest def test_scan_day(tmp_path, monkeypatch): - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") day_dir = copy_day(tmp_path) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) info = mod.scan_day("20240101") - assert "agents/flow.md" in info["processed"] - assert "agents/timeline.md" in info["repairable"] + assert "talents/flow.md" in info["processed"] + assert "talents/timeline.md" in info["repairable"] - (day_dir / "agents" / "timeline.md").write_text("done") + (day_dir / "talents" / "timeline.md").write_text("done") info_after = mod.scan_day("20240101") - assert "agents/timeline.md" in info_after["processed"] - assert "agents/timeline.md" not in info_after["repairable"] + assert "talents/timeline.md" in info_after["processed"] + assert "talents/timeline.md" not in info_after["repairable"] diff --git a/tests/test_generate_agents.py b/tests/test_generate_talents.py similarity index 100% rename from tests/test_generate_agents.py rename to tests/test_generate_talents.py diff --git a/tests/test_google.py b/tests/test_google.py index 0e479df2c..6e7c081e1 100644 --- a/tests/test_google.py +++ b/tests/test_google.py @@ -71,7 +71,7 @@ def test_google_main(monkeypatch, tmp_path, capsys): setup_google_genai_stub(monkeypatch, with_thinking=False) sys.modules.pop("think.providers.google", None) importlib.reload(importlib.import_module("think.providers.google")) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() @@ -127,7 +127,7 @@ def test_google_main(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] @@ -149,7 +149,7 @@ def test_google_cli_not_found_error(monkeypatch, tmp_path, capsys): sys.modules.pop("think.providers.google", None) importlib.reload(importlib.import_module("think.providers.google")) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() @@ -166,7 +166,7 @@ def test_google_cli_not_found_error(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) # Check stdout for error event out_lines = capsys.readouterr().out.strip().splitlines() diff --git a/tests/test_google_thinking.py b/tests/test_google_thinking.py index d027dca81..2da0639af 100644 --- a/tests/test_google_thinking.py +++ b/tests/test_google_thinking.py @@ -26,7 +26,7 @@ def test_google_thinking_events(monkeypatch, tmp_path, capsys): sys.modules.pop("think.providers.google", None) importlib.reload(importlib.import_module("think.providers.google")) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() @@ -109,7 +109,7 @@ def test_google_thinking_events(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] diff --git a/tests/test_heartbeat.py b/tests/test_heartbeat.py index a4c4352ef..f1865ec89 100644 --- a/tests/test_heartbeat.py +++ b/tests/test_heartbeat.py @@ -25,7 +25,7 @@ def heartbeat_mocks(monkeypatch): "think.heartbeat.cortex_request", lambda *args, **kwargs: "agent-123" ) monkeypatch.setattr( - "think.heartbeat.wait_for_agents", + "think.heartbeat.wait_for_uses", lambda *args, **kwargs: ({"agent-123": "finish"}, []), ) @@ -129,7 +129,7 @@ def test_pid_file_removed_on_timeout(journal_path, heartbeat_mocks): import think.heartbeat as mod pid_file = journal_path / "health" / "heartbeat.pid" - mod.wait_for_agents = lambda *a, **kw: ({}, ["agent-123"]) + mod.wait_for_uses = lambda *a, **kw: ({}, ["agent-123"]) with pytest.raises(SystemExit) as exc_info: mod.main() @@ -246,7 +246,7 @@ def test_force_flag_bypasses_recency_check(journal_path, monkeypatch): ) monkeypatch.setattr("think.heartbeat.ensure_sol_directory", lambda: None) monkeypatch.setattr( - "think.heartbeat.wait_for_agents", + "think.heartbeat.wait_for_uses", lambda *args, **kwargs: ({"agent-123": "finish"}, []), ) diff --git a/tests/test_home_events.py b/tests/test_home_events.py index 8b63cedbf..907188c22 100644 --- a/tests/test_home_events.py +++ b/tests/test_home_events.py @@ -32,7 +32,7 @@ class TestRecordTriageExchange: "tract": "cortex", "event": "finish", "name": "reviewer", - "agent_id": "123", + "use_id": "123", "result": "hello", } ) @@ -41,7 +41,7 @@ class TestRecordTriageExchange: mock_record.assert_not_called() def test_ignores_missing_agent_id(self): - """Handler returns early if agent_id is missing.""" + """Handler returns early if use_id is missing.""" ctx = self._make_ctx( { "tract": "cortex", @@ -61,7 +61,7 @@ class TestRecordTriageExchange: { "event": "request", "ts": 1700000000000, - "agent_id": "abc123", + "use_id": "abc123", "facet": "work", "app": "home", "path": "/home", @@ -70,7 +70,7 @@ class TestRecordTriageExchange: { "event": "finish", "ts": 1700000001000, - "agent_id": "abc123", + "use_id": "abc123", "result": "hi there", }, ] @@ -79,11 +79,11 @@ class TestRecordTriageExchange: "tract": "cortex", "event": "finish", "name": agent_name, - "agent_id": "abc123", + "use_id": "abc123", "result": "hi there", } ) - with patch("apps.home.events.read_agent_events", return_value=events): + with patch("apps.home.events.read_use_events", return_value=events): with patch("apps.home.events.record_exchange") as mock_record: record_triage_exchange(ctx) mock_record.assert_called_once_with( @@ -93,24 +93,24 @@ class TestRecordTriageExchange: user_message="hello world", agent_response="hi there", talent=agent_name, - agent_id="abc123", + use_id="abc123", ) def test_handles_missing_request_event(self): """Handler uses empty strings for metadata if request event not found.""" events = [ - {"event": "finish", "agent_id": "abc123", "result": "done"}, + {"event": "finish", "use_id": "abc123", "result": "done"}, ] ctx = self._make_ctx( { "tract": "cortex", "event": "finish", "name": "unified", - "agent_id": "abc123", + "use_id": "abc123", "result": "done", } ) - with patch("apps.home.events.read_agent_events", return_value=events): + with patch("apps.home.events.read_use_events", return_value=events): with patch("apps.home.events.record_exchange") as mock_record: record_triage_exchange(ctx) mock_record.assert_called_once_with( @@ -120,22 +120,22 @@ class TestRecordTriageExchange: user_message="", agent_response="done", talent="unified", - agent_id="abc123", + use_id="abc123", ) def test_handles_read_error_gracefully(self): - """Handler logs and swallows exceptions from read_agent_events.""" + """Handler logs and swallows exceptions from read_use_events.""" ctx = self._make_ctx( { "tract": "cortex", "event": "finish", "name": "unified", - "agent_id": "abc123", + "use_id": "abc123", "result": "done", } ) with patch( - "apps.home.events.read_agent_events", + "apps.home.events.read_use_events", side_effect=FileNotFoundError("not found"), ): with patch("apps.home.events.record_exchange") as mock_record: diff --git a/tests/test_home_yesterdays_processing.py b/tests/test_home_yesterdays_processing.py index 413a0ea39..905280cc9 100644 --- a/tests/test_home_yesterdays_processing.py +++ b/tests/test_home_yesterdays_processing.py @@ -58,11 +58,43 @@ def _seed_journal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: journal.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) + for day, transcript_seconds in (("20260415", 3600), ("20260414", 2700)): + facet_data = {"work": {"count": 1, "minutes": 15}} + if day == "20260414": + facet_data = {} + stats_path = journal / "chronicle" / day / "stats.json" + stats_path.parent.mkdir(parents=True, exist_ok=True) + stats_path.write_text( + json.dumps( + { + "stats": { + "transcript_segments": 3, + "transcript_duration": transcript_seconds, + }, + "facet_data": facet_data, + "heatmap_data": {"weekday": 2, "hours": {"9": 45.0}}, + } + ), + encoding="utf-8", + ) + + health_path = ( + journal / "chronicle" / "20260415" / "health" / "100_daily_dream.jsonl" + ) + health_path.parent.mkdir(parents=True, exist_ok=True) + health_path.write_text("", encoding="utf-8") + sparse_health_path = ( + journal / "chronicle" / "20260414" / "health" / "100_daily_dream.jsonl" + ) + sparse_health_path.parent.mkdir(parents=True, exist_ok=True) + sparse_health_path.write_text( + json.dumps({"event": "run.complete", "mode": "daily", "duration_ms": 10}) + + "\n", + encoding="utf-8", + ) + for rel_path in [ - "chronicle/20260415/stats.json", - "chronicle/20260415/agents/knowledge_graph.md", - "chronicle/20260415/health/100_daily_dream.jsonl", - "chronicle/20260414/stats.json", + "chronicle/20260415/talents/knowledge_graph.md", ]: _copy_fixture_file(journal, rel_path) @@ -139,21 +171,21 @@ def _seed_entities(journal: Path, day: str = "20260415") -> None: VALUES (?, ?, NULL, NULL, NULL, ?, ?, NULL, NULL, ?) """, [ - ("mention", "jane_doe", day, "work", f"{day}/agents/flow.md"), - ("mention", "alice_johnson", day, "work", f"{day}/agents/flow.md"), + ("mention", "jane_doe", day, "work", f"{day}/talents/flow.md"), + ("mention", "alice_johnson", day, "work", f"{day}/talents/flow.md"), ( "mention", "product_roadmap", day, "work", - f"{day}/agents/knowledge_graph.md", + f"{day}/talents/knowledge_graph.md", ), ( "mention", "launch_decision", day, "work", - f"{day}/agents/knowledge_graph.md", + f"{day}/talents/knowledge_graph.md", ), ], ) @@ -163,13 +195,18 @@ def _seed_entities(journal: Path, day: str = "20260415") -> None: def _append_dream_log( - journal: Path, day: str, name: str, *, facet: str | None = None + journal: Path, + day: str, + name: str, + *, + facet: str | None = None, + event: str = "talent.fail", ) -> None: path = journal / "chronicle" / day / "health" / "101_daily_dream.jsonl" path.parent.mkdir(parents=True, exist_ok=True) with path.open("a", encoding="utf-8") as handle: record = { - "event": "agent.fail", + "event": event, "mode": "daily", "name": name, "state": "error", @@ -220,6 +257,10 @@ def test_yesterdays_card_sparse_mode_copy(tmp_path, monkeypatch): _write_briefing(journal, "2026-04-15T06:45:00") monkeypatch.setattr("apps.home.routes._today", lambda: "20260415") + monkeypatch.setattr( + "apps.home.routes._knowledge_graph_freshness", + lambda _day: {"fresh": True}, + ) summary = _summarize_yesterday_processing("20260414", 2) @@ -378,7 +419,7 @@ def test_knowledge_graph_refresh_detection_yesterday_and_overnight( tmp_path, monkeypatch ): journal = _seed_journal(tmp_path, monkeypatch) - path = journal / "chronicle" / "20260415" / "agents" / "knowledge_graph.md" + path = journal / "chronicle" / "20260415" / "talents" / "knowledge_graph.md" _set_mtime(path, datetime(2026, 4, 15, 12, 0, 0)) assert _knowledge_graph_freshness("20260415")["fresh"] is True @@ -411,7 +452,7 @@ def test_gap_bullets_show_specific_daily_and_activity_without_generic(): "anomalies": [ {"kind": "daily_agents_missing"}, {"kind": "activity_agents_missing"}, - {"kind": "agent_failure"}, + {"kind": "talent_failure"}, ] }, {"fresh": True}, @@ -435,6 +476,19 @@ def test_newsletter_attempts_option_a_matches_facet_newsletter_failures_only( assert _newsletter_attempts_from_dream_logs("20260415") == (2, 3) +def test_newsletter_attempts_accept_legacy_agent_fail(tmp_path, monkeypatch): + journal = _seed_journal(tmp_path, monkeypatch) + _append_dream_log( + journal, + "20260415", + "facet_newsletter", + facet="work", + event="agent.fail", + ) + + assert _newsletter_attempts_from_dream_logs("20260415") == (2, 3) + + def test_build_pulse_context_includes_yesterday_processing(monkeypatch): monkeypatch.setattr( "apps.home.routes.get_capture_health", diff --git a/tests/test_journal_index.py b/tests/test_journal_index.py index 7096cd453..4cdc65750 100644 --- a/tests/test_journal_index.py +++ b/tests/test_journal_index.py @@ -281,7 +281,7 @@ def journal_fixture(tmp_path): # Create daily insight day = journal / "chronicle" / "20240101" day.mkdir(parents=True) - agents_dir = day / "agents" + agents_dir = day / "talents" agents_dir.mkdir() (agents_dir / "flow.md").write_text("# Flow Summary\n\nWorked on project alpha.\n") @@ -290,8 +290,8 @@ def journal_fixture(tmp_path): stream_dir.mkdir() segment = stream_dir / "100000_300" segment.mkdir() - (segment / "agents").mkdir() - (segment / "agents" / "screen.md").write_text( + (segment / "talents").mkdir() + (segment / "talents" / "screen.md").write_text( "# Screen Summary\n\nViewed documentation.\n" ) # Add stream.json for segment stream metadata @@ -299,15 +299,15 @@ def journal_fixture(tmp_path): write_segment_stream(str(segment), "default", None, None, 1) # Add second agent file for cross-file segment testing - (segment / "agents" / "activity.md").write_text( + (segment / "talents" / "activity.md").write_text( "# Activity Summary\n\nMet with Scott Ward about Acme deal.\n" ) # Create evening segment for time_bucket testing evening_segment = stream_dir / "200000_300" evening_segment.mkdir() - (evening_segment / "agents").mkdir() - (evening_segment / "agents" / "screen.md").write_text( + (evening_segment / "talents").mkdir() + (evening_segment / "talents" / "screen.md").write_text( "# Evening Screen\n\nReviewed evening reports.\n" ) write_segment_stream(str(evening_segment), "default", None, None, 1) @@ -636,17 +636,17 @@ def test_is_historical_day(): # Non-day paths are never historical assert _is_historical_day("facets/work/events/20240101.jsonl") is False assert _is_historical_day("imports/123/summary.md") is False - assert _is_historical_day("apps/home/agents/foo.md") is False + assert _is_historical_day("apps/home/talents/foo.md") is False # Future dates are not historical - assert _is_historical_day("29991231/agents/flow.md") is False + assert _is_historical_day("29991231/talents/flow.md") is False # Path without slash is not historical assert _is_historical_day("20240101") is False assert _is_historical_day("") is False # Day paths before today are historical (tested with a very old date) - assert _is_historical_day("20000101/agents/flow.md") is True + assert _is_historical_day("20000101/talents/flow.md") is True def test_scan_journal_full_mode(journal_fixture): @@ -672,10 +672,10 @@ def test_find_formattable_files(journal_fixture): paths = set(files.keys()) # Daily agent outputs - assert "20240101/agents/flow.md" in paths + assert "20240101/talents/flow.md" in paths # Segment agent outputs - assert "20240101/default/100000_300/agents/screen.md" in paths + assert "20240101/default/100000_300/talents/screen.md" in paths # Facet content assert "facets/work/events/20240101.jsonl" in paths @@ -909,7 +909,7 @@ def test_light_scan_removes_deleted_today_segment(tmp_path): today = datetime.now().strftime("%Y%m%d") day_dir = journal / today day_dir.mkdir(parents=True) - agents_dir = day_dir / "agents" + agents_dir = day_dir / "talents" agents_dir.mkdir() output_file = agents_dir / "flow.md" output_file.write_text("# Today Flow\n\nWorked on unique_today_content.\n") @@ -943,7 +943,7 @@ def test_light_scan_preserves_historical_content(tmp_path): # Create historical day content day_dir = journal / "chronicle" / "20200101" day_dir.mkdir(parents=True) - agents_dir = day_dir / "agents" + agents_dir = day_dir / "talents" agents_dir.mkdir() output_file = agents_dir / "flow.md" output_file.write_text("# Historical Flow\n\nWorked on historical_content.\n") @@ -978,7 +978,7 @@ def test_full_scan_removes_historical_content(tmp_path): # Create historical day content day_dir = journal / "chronicle" / "20200101" day_dir.mkdir(parents=True) - agents_dir = day_dir / "agents" + agents_dir = day_dir / "talents" agents_dir.mkdir() output_file = agents_dir / "flow.md" output_file.write_text("# Historical Flow\n\nWorked on historical_full_test.\n") @@ -1007,7 +1007,7 @@ def test_index_file_valid(journal_fixture): from think.indexer.journal import index_file, search_journal # Index a specific file - result = index_file(str(journal_fixture), "20240101/agents/flow.md", verbose=True) + result = index_file(str(journal_fixture), "20240101/talents/flow.md", verbose=True) assert result is True # Should be searchable @@ -1019,7 +1019,7 @@ def test_index_file_absolute_path(journal_fixture): """Test indexing with absolute path.""" from think.indexer.journal import index_file, search_journal - abs_path = str(journal_fixture / "chronicle" / "20240101" / "agents" / "flow.md") + abs_path = str(journal_fixture / "chronicle" / "20240101" / "talents" / "flow.md") result = index_file(str(journal_fixture), abs_path, verbose=True) assert result is True @@ -1057,13 +1057,13 @@ def test_index_file_updates_existing(journal_fixture): from think.indexer.journal import index_file, search_journal # Index the file - index_file(str(journal_fixture), "20240101/agents/flow.md") + index_file(str(journal_fixture), "20240101/talents/flow.md") # Get initial count total1, _ = search_journal("project alpha") # Re-index the same file - index_file(str(journal_fixture), "20240101/agents/flow.md") + index_file(str(journal_fixture), "20240101/talents/flow.md") # Count should be the same (not doubled) total2, _ = search_journal("project alpha") @@ -1117,7 +1117,7 @@ def test_extract_stream_segment_path(tmp_path): write_segment_stream(seg_dir, "archon", None, None, 1) result = _extract_stream( - str(tmp_path), "20240101/default/123456_300/agents/work/flow.md" + str(tmp_path), "20240101/default/123456_300/talents/work/flow.md" ) assert result == "archon" @@ -1126,7 +1126,7 @@ def test_extract_stream_non_segment_path(tmp_path): """_extract_stream returns None for non-segment paths.""" from think.indexer.journal import _extract_stream - result = _extract_stream(str(tmp_path), "20240101/agents/flow.md") + result = _extract_stream(str(tmp_path), "20240101/talents/flow.md") assert result is None result = _extract_stream(str(tmp_path), "facets/work/events/20240101.jsonl") @@ -1141,7 +1141,7 @@ def test_extract_stream_missing_marker(tmp_path): seg_dir.mkdir(parents=True) result = _extract_stream( - str(tmp_path), "20240101/default/123456_300/agents/work/flow.md" + str(tmp_path), "20240101/default/123456_300/talents/work/flow.md" ) assert result is None @@ -1302,12 +1302,13 @@ def test_scan_entities_incremental_noop(): assert count1 == count2 -def test_scan_entities_deletion(tmp_path): +def test_scan_entities_deletion(tmp_path, monkeypatch): """Verify entity rows are removed when source file is deleted.""" src = Path("tests/fixtures/journal") dst = tmp_path / "journal" copytree_tracked(src, dst) j = str(dst) + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", j) from think.indexer.journal import scan_journal @@ -1467,7 +1468,7 @@ def test_scan_signals_deletion(tmp_path): conn.close() assert initial == 45 - kg_file = dst / "chronicle" / "20240101" / "agents" / "knowledge_graph.md" + kg_file = dst / "chronicle" / "20240101" / "talents" / "knowledge_graph.md" kg_file.unlink() scan_journal(j, full=True) @@ -1711,10 +1712,10 @@ class TestSegmentChunks: scan_journal(str(journal_fixture), verbose=True, full=True) conn, _ = get_journal_index(str(journal_fixture)) screen_chunks = conn.execute( - "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/agents/screen.md'" + "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/talents/screen.md'" ).fetchone()[0] activity_chunks = conn.execute( - "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/agents/activity.md'" + "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/talents/activity.md'" ).fetchone()[0] segment_chunks = conn.execute( "SELECT count(*) FROM chunks WHERE agent='segment'" diff --git a/tests/test_journal_stats.py b/tests/test_journal_stats.py index ba5d1c129..926843c12 100644 --- a/tests/test_journal_stats.py +++ b/tests/test_journal_stats.py @@ -27,8 +27,8 @@ def test_scan_day(tmp_path, monkeypatch): (ts_dir2 / "center_DP-1_screen.webm").write_bytes(b"WEBM") (day / "entities.md").write_text("") - (day / "agents").mkdir() - (day / "agents" / "flow.md").write_text("") + (day / "talents").mkdir() + (day / "talents" / "flow.md").write_text("") # Create event in new JSONL format: facets/{facet}/events/YYYYMMDD.jsonl events_dir = journal / "facets" / "work" / "events" @@ -45,7 +45,7 @@ def test_scan_day(tmp_path, monkeypatch): "facet": "work", "agent": "meetings", "occurred": True, - "source": "20240101/agents/meetings.md", + "source": "20240101/talents/meetings.md", } (events_dir / "20240101.jsonl").write_text(json.dumps(event)) @@ -169,7 +169,7 @@ def test_token_usage(tmp_path, monkeypatch): # Test JSON output includes token usage data = js.to_dict() - assert data["schema_version"] == 2 + assert data["schema_version"] == 3 assert "generated_at" in data assert data["day_count"] == 2 assert "tokens" in data @@ -252,7 +252,7 @@ def test_facet_event_mtime_invalidates_cache(tmp_path, monkeypatch): "facet": "work", "agent": "meetings", "occurred": True, - "source": "20240101/agents/meetings.md", + "source": "20240101/talents/meetings.md", } (events_dir / "20240101.jsonl").write_text(json.dumps(event)) diff --git a/tests/test_maint_004_rename.py b/tests/test_maint_004_rename.py new file mode 100644 index 000000000..ade83d186 --- /dev/null +++ b/tests/test_maint_004_rename.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +import importlib +from pathlib import Path + +mod = importlib.import_module("apps.sol.maint.004_rename_agents_to_talents") + + +def _patch_journal(monkeypatch, journal: Path, day: str = "20260417") -> None: + monkeypatch.setattr(mod, "get_journal", lambda: str(journal)) + monkeypatch.setattr(mod, "day_dirs", lambda: {day: str(journal / day)}) + monkeypatch.setattr( + mod, + "iter_segments", + lambda _day: [ + ("default", "090000_300", journal / day / "default" / "090000_300") + ], + ) + + +def test_run_migration_moves_all_paths(tmp_path, monkeypatch): + journal = tmp_path / "journal" + day = journal / "20260417" + segment = day / "default" / "090000_300" + + (journal / "agents").mkdir(parents=True) + (journal / "agents" / "root.jsonl").write_text("{}\n", encoding="utf-8") + (journal / "health").mkdir(parents=True) + (journal / "health" / "agents.json").write_text("{}", encoding="utf-8") + (day / "agents").mkdir(parents=True) + (day / "agents" / "flow.md").write_text("# flow\n", encoding="utf-8") + (segment / "agents").mkdir(parents=True) + (segment / "agents" / "screen.md").write_text("# screen\n", encoding="utf-8") + + _patch_journal(monkeypatch, journal) + + summary, collisions = mod.run_migration(journal, dry_run=False) + + assert collisions == [] + assert summary.discovered == 4 + assert summary.moved == 4 + assert summary.skipped == 0 + assert summary.errors == 0 + assert summary.collisions == 0 + assert (journal / "talents" / "root.jsonl").exists() + assert (journal / "health" / "talents.json").exists() + assert (day / "talents" / "flow.md").exists() + assert (segment / "talents" / "screen.md").exists() + + +def test_run_migration_aborts_on_collision(tmp_path, monkeypatch): + journal = tmp_path / "journal" + day = journal / "20260417" + + (journal / "agents").mkdir(parents=True) + (journal / "agents" / "root.jsonl").write_text("{}\n", encoding="utf-8") + (journal / "talents").mkdir(parents=True) + (day / "agents").mkdir(parents=True) + (day / "agents" / "flow.md").write_text("# flow\n", encoding="utf-8") + + _patch_journal(monkeypatch, journal) + + summary, collisions = mod.run_migration(journal, dry_run=False) + + assert summary.collisions == 1 + assert summary.moved == 0 + assert len(collisions) == 1 + assert (journal / "agents" / "root.jsonl").exists() + assert not (day / "talents").exists() + + +def test_run_migration_reports_already_migrated(tmp_path, monkeypatch): + journal = tmp_path / "journal" + day = journal / "20260417" + segment = day / "default" / "090000_300" + + (journal / "talents").mkdir(parents=True) + (journal / "talents" / "root.jsonl").write_text("{}\n", encoding="utf-8") + (journal / "health").mkdir(parents=True) + (journal / "health" / "talents.json").write_text("{}", encoding="utf-8") + (day / "talents").mkdir(parents=True) + (day / "talents" / "flow.md").write_text("# flow\n", encoding="utf-8") + (segment / "talents").mkdir(parents=True) + (segment / "talents" / "screen.md").write_text("# screen\n", encoding="utf-8") + + _patch_journal(monkeypatch, journal) + + summary, collisions = mod.run_migration(journal, dry_run=False) + + assert collisions == [] + assert summary.discovered == 0 + assert summary.moved == 0 + assert summary.skipped == 4 + assert summary.errors == 0 + assert summary.collisions == 0 diff --git a/tests/test_output_hooks.py b/tests/test_output_hooks.py index bd0d8ac77..6e94a457f 100644 --- a/tests/test_output_hooks.py +++ b/tests/test_output_hooks.py @@ -18,9 +18,9 @@ from pathlib import Path import talent.occurrence as occurrence from tests.conftest import copytree_tracked -from think.agents import _apply_template_vars from think.hooks import write_events_jsonl from think.talent import load_post_hook, load_pre_hook +from think.talents import _apply_template_vars from think.utils import day_path FIXTURES = Path("tests/fixtures") @@ -160,7 +160,7 @@ def test_prompt_metadata_no_hook_path(tmp_path): def test_output_hook_invocation(tmp_path, monkeypatch): """Test that agents.py invokes hook and uses transformed result.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) # Use tmp_path as talent directory to avoid polluting real talent/ @@ -215,7 +215,7 @@ def post_process(result, context): def test_output_hook_returns_none(tmp_path, monkeypatch): """Test that hook returning None uses original result.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -261,7 +261,7 @@ def post_process(result, context): def test_output_hook_error_fallback(tmp_path, monkeypatch): """Test that hook errors fall back to original result.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -658,7 +658,7 @@ def test_load_pre_hook_file_not_found(tmp_path): def test_pre_hook_invocation(tmp_path, monkeypatch): """Test that agents.py invokes pre-hook and uses modified inputs.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -824,7 +824,7 @@ def test_template_vars_popped_from_modifications(): def test_pre_hook_template_vars_integration(tmp_path, monkeypatch): """Test pre-hook template_vars reach the model as substituted text.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -875,7 +875,7 @@ def pre_process(context): def test_pre_hook_template_vars_with_field_mods(tmp_path, monkeypatch): """Test pre-hook can return field mods and template_vars together.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -929,7 +929,7 @@ def pre_process(context): def test_both_pre_and_post_hooks(tmp_path, monkeypatch): """Test that both pre and post hooks can be configured together.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent diff --git a/tests/test_output_path.py b/tests/test_output_path.py index e8c882766..1c174c051 100644 --- a/tests/test_output_path.py +++ b/tests/test_output_path.py @@ -29,30 +29,30 @@ class TestGetOutputPath: def test_daily_output_md(self): path = get_output_path("/journal/20250101", "activity", output_format="md") - assert path == Path("/journal/20250101/agents/activity.md") + assert path == Path("/journal/20250101/talents/activity.md") def test_daily_output_json(self): path = get_output_path("/journal/20250101", "facets", output_format="json") - assert path == Path("/journal/20250101/agents/facets.json") + assert path == Path("/journal/20250101/talents/facets.json") def test_segment_output(self): path = get_output_path( "/journal/20250101", "activity", segment="120000_300", output_format="md" ) - assert path == Path("/journal/20250101/120000_300/agents/activity.md") + assert path == Path("/journal/20250101/120000_300/talents/activity.md") def test_app_key_output(self): path = get_output_path( "/journal/20250101", "entities:observer", output_format="md" ) - assert path == Path("/journal/20250101/agents/_entities_observer.md") + assert path == Path("/journal/20250101/talents/_entities_observer.md") def test_facet_daily_output(self): """Multi-facet agent output uses a facet subdirectory.""" path = get_output_path( "/journal/20250101", "newsletter", output_format="md", facet="work" ) - assert path == Path("/journal/20250101/agents/work/newsletter.md") + assert path == Path("/journal/20250101/talents/work/newsletter.md") def test_facet_segment_output(self): """Multi-facet segment output uses a facet subdirectory.""" @@ -63,14 +63,16 @@ class TestGetOutputPath: output_format="json", facet="personal", ) - assert path == Path("/journal/20250101/120000_300/agents/personal/summary.json") + assert path == Path( + "/journal/20250101/120000_300/talents/personal/summary.json" + ) def test_facet_with_app_key(self): """App-qualified key with facet uses both prefixes.""" path = get_output_path( "/journal/20250101", "entities:observer", output_format="md", facet="work" ) - assert path == Path("/journal/20250101/agents/work/_entities_observer.md") + assert path == Path("/journal/20250101/talents/work/_entities_observer.md") def test_facet_none_same_as_omitted(self): """Explicit facet=None produces same path as omitting facet.""" diff --git a/tests/test_pipeline_health.py b/tests/test_pipeline_health.py index b6d0a6fd6..8ebe75651 100644 --- a/tests/test_pipeline_health.py +++ b/tests/test_pipeline_health.py @@ -34,7 +34,7 @@ def test_empty_day_is_healthy(pipeline_journal): assert summary["status"] == "healthy" assert summary["anomalies"] == [] - assert summary["agents"] == { + assert summary["talents"] == { "dispatched": 0, "completed": 0, "failed": 0, @@ -45,7 +45,7 @@ def test_empty_day_is_healthy(pipeline_journal): assert summary["activities"] == { "detected": 0, "persisted": 0, - "agents_fired": False, + "talents_fired": False, } assert all( run == {"count": 0, "duration_ms_total": 0} for run in summary["runs"].values() @@ -69,8 +69,8 @@ def test_healthy_day_with_all_modes(pipeline_journal): base / "1_segment_dream.jsonl", [ {"event": "run.start", "mode": "segment"}, - {"event": "agent.dispatch", "mode": "segment"}, - {"event": "agent.complete", "mode": "segment"}, + {"event": "talent.dispatch", "mode": "segment"}, + {"event": "talent.complete", "mode": "segment"}, {"event": "run.complete", "mode": "segment", "duration_ms": 10}, ], ) @@ -78,8 +78,8 @@ def test_healthy_day_with_all_modes(pipeline_journal): base / "2_daily_dream.jsonl", [ {"event": "run.start", "mode": "daily"}, - {"event": "agent.dispatch", "mode": "daily"}, - {"event": "agent.complete", "mode": "daily"}, + {"event": "talent.dispatch", "mode": "daily"}, + {"event": "talent.complete", "mode": "daily"}, {"event": "run.complete", "mode": "daily", "duration_ms": 20}, ], ) @@ -87,8 +87,8 @@ def test_healthy_day_with_all_modes(pipeline_journal): base / "3_activity_dream.jsonl", [ {"event": "run.start", "mode": "activity"}, - {"event": "agent.dispatch", "mode": "activity"}, - {"event": "agent.complete", "mode": "activity"}, + {"event": "talent.dispatch", "mode": "activity"}, + {"event": "talent.complete", "mode": "activity"}, {"event": "run.complete", "mode": "activity", "duration_ms": 30}, ], ) @@ -96,12 +96,12 @@ def test_healthy_day_with_all_modes(pipeline_journal): summary = summarize_pipeline_day(day) assert summary["status"] == "healthy" - assert summary["agents"]["dispatched"] == 3 - assert summary["agents"]["completed"] == 3 + assert summary["talents"]["dispatched"] == 3 + assert summary["talents"]["completed"] == 3 assert summary["runs"]["segment"] == {"count": 1, "duration_ms_total": 10} assert summary["runs"]["daily"] == {"count": 1, "duration_ms_total": 20} assert summary["runs"]["activity"] == {"count": 1, "duration_ms_total": 30} - assert summary["activities"]["agents_fired"] is True + assert summary["activities"]["talents_fired"] is True def test_agent_failure_promotes_warning(pipeline_journal): @@ -110,10 +110,10 @@ def test_agent_failure_promotes_warning(pipeline_journal): pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", [ { - "event": "agent.fail", + "event": "talent.fail", "mode": "segment", "name": "screen", - "agent_id": "a-1", + "use_id": "a-1", "state": "timeout", } ], @@ -122,29 +122,65 @@ def test_agent_failure_promotes_warning(pipeline_journal): summary = summarize_pipeline_day(day) assert summary["status"] == "warning" - assert summary["agents"]["failed"] == 1 - assert summary["agents"]["failed_list"] == [ - {"mode": "segment", "name": "screen", "agent_id": "a-1", "state": "timeout"} + assert summary["talents"]["failed"] == 1 + assert summary["talents"]["failed_list"] == [ + {"mode": "segment", "name": "screen", "use_id": "a-1", "state": "timeout"} ] assert summary["anomalies"] == [ { - "kind": "agent_failure", + "kind": "talent_failure", "mode": "segment", "name": "screen", - "agent_id": "a-1", + "use_id": "a-1", "state": "timeout", } ] +@pytest.mark.parametrize("event_name", ["agent.fail", "talent.fail"]) +def test_failure_reader_accepts_legacy_and_new_names(pipeline_journal, event_name): + day = "20990107" + _write_jsonl( + pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", + [{"event": event_name, "mode": "segment", "name": "screen", "use_id": "u-1"}], + ) + + summary = summarize_pipeline_day(day) + + assert summary["talents"]["failed"] == 1 + + +@pytest.mark.parametrize( + ("event_name", "field"), + [ + ("agent.dispatch", "dispatched"), + ("talent.dispatch", "dispatched"), + ("agent.complete", "completed"), + ("talent.complete", "completed"), + ("agent.skip", "skipped"), + ("talent.skip", "skipped"), + ], +) +def test_reader_accepts_legacy_and_new_event_names(pipeline_journal, event_name, field): + day = "20990108" + _write_jsonl( + pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", + [{"event": event_name, "mode": "segment"}], + ) + + summary = summarize_pipeline_day(day) + + assert summary["talents"][field] == 1 + + def test_failed_list_truncates_at_20(pipeline_journal): day = "20990103" events = [ { - "event": "agent.fail", + "event": "talent.fail", "mode": "daily", "name": f"agent-{idx}", - "agent_id": f"id-{idx}", + "use_id": f"id-{idx}", "state": "error", } for idx in range(25) @@ -155,10 +191,10 @@ def test_failed_list_truncates_at_20(pipeline_journal): summary = summarize_pipeline_day(day) - assert summary["agents"]["failed"] == 25 - assert len(summary["agents"]["failed_list"]) == 20 - assert summary["agents"]["failed_list_truncated"] is True - assert sum(1 for a in summary["anomalies"] if a["kind"] == "agent_failure") == 20 + assert summary["talents"]["failed"] == 25 + assert len(summary["talents"]["failed_list"]) == 20 + assert summary["talents"]["failed_list_truncated"] is True + assert sum(1 for a in summary["anomalies"] if a["kind"] == "talent_failure") == 20 def test_activity_detected_without_run_is_stale(pipeline_journal): @@ -235,7 +271,7 @@ def test_invalid_day_returns_healthy_empty(pipeline_journal): assert summary["status"] == "healthy" assert summary["anomalies"] == [] - assert summary["agents"] == { + assert summary["talents"] == { "dispatched": 0, "completed": 0, "failed": 0, @@ -252,7 +288,7 @@ def test_malformed_json_lines_skipped(pipeline_journal): path.write_text( json.dumps({"event": "run.start", "mode": "segment"}) + "\nnot json at all\n" - + json.dumps({"event": "agent.dispatch", "mode": "segment"}) + + json.dumps({"event": "talent.dispatch", "mode": "segment"}) + "\n", encoding="utf-8", ) @@ -260,7 +296,7 @@ def test_malformed_json_lines_skipped(pipeline_journal): summary = summarize_pipeline_day(day) assert summary["runs"]["segment"]["count"] == 1 - assert summary["agents"]["dispatched"] == 1 + assert summary["talents"]["dispatched"] == 1 @pytest.mark.parametrize( @@ -270,7 +306,7 @@ def test_malformed_json_lines_skipped(pipeline_journal): { "status": "healthy", "anomalies": [], - "agents": {"failed": 0}, + "talents": {"failed": 0}, "day": "20260101", }, None, @@ -281,9 +317,9 @@ def test_malformed_json_lines_skipped(pipeline_journal): "anomalies": [ {"kind": "activity_agents_missing"}, {"kind": "daily_agents_missing"}, - {"kind": "agent_failure"}, + {"kind": "talent_failure"}, ], - "agents": {"failed": 3}, + "talents": {"failed": 3}, "day": "20260101", }, { @@ -296,9 +332,9 @@ def test_malformed_json_lines_skipped(pipeline_journal): "status": "stale", "anomalies": [ {"kind": "daily_agents_missing"}, - {"kind": "agent_failure"}, + {"kind": "talent_failure"}, ], - "agents": {"failed": 2}, + "talents": {"failed": 2}, "day": "20260102", }, { @@ -309,26 +345,26 @@ def test_malformed_json_lines_skipped(pipeline_journal): ( { "status": "warning", - "anomalies": [{"kind": "agent_failure"}], - "agents": {"failed": 1}, + "anomalies": [{"kind": "talent_failure"}], + "talents": {"failed": 1}, "day": "20260101", }, - {"status": "warning", "message": "1 agent error today"}, + {"status": "warning", "message": "1 talent error today"}, ), ( { "status": "warning", - "anomalies": [{"kind": "agent_failure"}] * 3, - "agents": {"failed": 3}, + "anomalies": [{"kind": "talent_failure"}] * 3, + "talents": {"failed": 3}, "day": "20260101", }, - {"status": "warning", "message": "3 agent errors today"}, + {"status": "warning", "message": "3 talent errors today"}, ), ( { "status": "healthy", "anomalies": [{"kind": "segment_runs_missing"}], - "agents": {"failed": 0}, + "talents": {"failed": 0}, "day": "20260101", }, None, diff --git a/tests/test_pipeline_smoke.py b/tests/test_pipeline_smoke.py index 177f88e8c..edd4627ec 100644 --- a/tests/test_pipeline_smoke.py +++ b/tests/test_pipeline_smoke.py @@ -125,7 +125,7 @@ class TestPipelineSmokeTest: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -142,8 +142,8 @@ class TestPipelineSmokeTest: for segment_key, sense_dict in SEGMENTS: seg_dir = journal / "chronicle" / DAY / STREAM / segment_key - (seg_dir / "agents").mkdir(parents=True, exist_ok=True) - (seg_dir / "agents" / "sense.json").write_text(json.dumps(sense_dict)) + (seg_dir / "talents").mkdir(parents=True, exist_ok=True) + (seg_dir / "talents" / "sense.json").write_text(json.dumps(sense_dict)) dream.run_segment_sense( day=DAY, @@ -161,7 +161,7 @@ class TestPipelineSmokeTest: "091500_300", "100000_300", ]: - seg_agents = journal / "chronicle" / DAY / STREAM / seg_key / "agents" + seg_agents = journal / "chronicle" / DAY / STREAM / seg_key / "talents" assert (seg_agents / "sense.json").exists() assert (seg_agents / "activity.md").exists() assert (seg_agents / "density.json").exists() @@ -179,7 +179,7 @@ class TestPipelineSmokeTest: / DAY / STREAM / seg_key - / "agents" + / "talents" / "speakers.json" ).read_text() ) @@ -192,7 +192,7 @@ class TestPipelineSmokeTest: / DAY / STREAM / seg_key - / "agents" + / "talents" / "speakers.json" ).exists() @@ -203,7 +203,7 @@ class TestPipelineSmokeTest: / DAY / STREAM / "092000_300" - / "agents" + / "talents" / "density.json" ).read_text() ) diff --git a/tests/test_agents_check.py b/tests/test_providers_check.py similarity index 76% rename from tests/test_agents_check.py rename to tests/test_providers_check.py index e54025edf..ab861c73c 100644 --- a/tests/test_agents_check.py +++ b/tests/test_providers_check.py @@ -11,8 +11,8 @@ import pytest def test_run_check_writes_health_file(tmp_path, monkeypatch): - """_run_check writes agents health results to _SOLSTONE_JOURNAL_OVERRIDE/health/agents.json.""" - import think.agents as agents + """_run_check writes provider health results to _SOLSTONE_JOURNAL_OVERRIDE/health/talents.json.""" + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = { @@ -25,13 +25,13 @@ def test_run_check_writes_health_file(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "ok")) async def mock_check_cogitate(*_args): return "ok", "ok" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -43,11 +43,11 @@ def test_run_check_writes_health_file(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" assert health_file.exists() payload = json.loads(health_file.read_text()) @@ -61,7 +61,7 @@ def test_run_check_writes_health_file(tmp_path, monkeypatch): def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): """_run_check exits 1 when any check fails.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = { @@ -74,13 +74,13 @@ def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "ok")) async def mock_check_cogitate(*_args): return "fail", "FAIL: timeout" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -92,11 +92,11 @@ def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 1 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" payload = json.loads(health_file.read_text()) assert payload["summary"]["passed"] == 3 assert payload["summary"]["skipped"] == 0 @@ -105,7 +105,7 @@ def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch): """_run_check exits 1 when all checks for a provider fail.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = { @@ -118,15 +118,15 @@ def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) monkeypatch.setattr( - agents, "_check_generate", lambda *_args: ("fail", "FAIL: key not set") + providers_cli, "_check_generate", lambda *_args: ("fail", "FAIL: key not set") ) async def mock_check_cogitate(*_args): return "fail", "FAIL: key not set" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -138,11 +138,11 @@ def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 1 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" payload = json.loads(health_file.read_text()) assert payload["summary"]["passed"] == 0 assert payload["summary"]["skipped"] == 0 @@ -151,7 +151,7 @@ def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch): def test_run_check_dedup_same_model(tmp_path, monkeypatch): """_run_check deduplicates checks when tiers resolve to the same model.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = { @@ -164,17 +164,17 @@ def test_run_check_dedup_same_model(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) gen_mock = MagicMock(return_value=("ok", "ok")) - monkeypatch.setattr(agents, "_check_generate", gen_mock) + monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) cog_inner = MagicMock(return_value=("ok", "ok")) async def mock_check_cogitate(*args): return cog_inner(*args) - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -186,13 +186,13 @@ def test_run_check_dedup_same_model(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 assert gen_mock.call_count == 1 assert cog_inner.call_count == 1 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" assert health_file.exists() payload = json.loads(health_file.read_text()) @@ -212,7 +212,7 @@ def test_run_check_dedup_same_model(tmp_path, monkeypatch): def test_run_check_targeted_filters_to_configured_pairs(tmp_path, monkeypatch): """--targeted filters checks to only configured provider+tier pairs.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"provA": object(), "provB": object(), "provC": object()} fake_defaults = { @@ -228,13 +228,13 @@ def test_run_check_targeted_filters_to_configured_pairs(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) monkeypatch.setattr("think.models.TYPE_DEFAULTS", fake_type_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "ok")) async def mock_check_cogitate(*_args): return "ok", "ok" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) # Mock get_config to return no overrides (use TYPE_DEFAULTS) monkeypatch.setattr("think.utils.get_config", lambda: {}) @@ -258,11 +258,11 @@ def test_run_check_targeted_filters_to_configured_pairs(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" payload = json.loads(health_file.read_text()) # Expected targeted pairs: (provA, 2), (provB, 2), (provC, 2) = 3 pairs × 2 interfaces = 6 checks assert payload["summary"]["total"] == 6 @@ -274,7 +274,7 @@ def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch): """--targeted exits silently when another targeted check holds the lock.""" import fcntl - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = {"fake": {1: "m", 2: "m", 3: "m"}} @@ -286,7 +286,7 @@ def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) monkeypatch.setattr("think.models.TYPE_DEFAULTS", fake_type_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) monkeypatch.setattr("think.utils.get_config", lambda: {}) monkeypatch.setattr("think.models.get_backup_provider", lambda _: None) @@ -297,7 +297,7 @@ def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch): fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) gen_mock = MagicMock(return_value=("ok", "ok")) - monkeypatch.setattr(agents, "_check_generate", gen_mock) + monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) args = argparse.Namespace( provider=None, @@ -309,18 +309,18 @@ def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch): ) # Should return silently (no SystemExit, no checks run) - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert gen_mock.call_count == 0 # No health file written - assert not (tmp_path / "health" / "agents.json").exists() + assert not (tmp_path / "health" / "talents.json").exists() lock_file.close() def test_check_generate_logs_token_usage(monkeypatch): """_check_generate logs token usage when result includes usage data.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_module = MagicMock() fake_module.run_generate.return_value = { @@ -339,7 +339,7 @@ def test_check_generate_logs_token_usage(monkeypatch): log_mock = MagicMock() monkeypatch.setattr("think.models.log_token_usage", log_mock) - status, msg = agents._check_generate("fake", 2, 30) + status, msg = providers_cli._check_generate("fake", 2, 30) assert status == "ok" assert msg == "OK" @@ -351,8 +351,8 @@ def test_check_generate_logs_token_usage(monkeypatch): ) -def test_cortex_start_emits_agents_check(tmp_path): - """Cortex startup requests an agents health check via supervisor.""" +def test_cortex_start_emits_providers_check(tmp_path): + """Cortex startup requests a providers health check via supervisor.""" from think.cortex import CortexService cortex = CortexService(journal_path=str(tmp_path)) @@ -366,13 +366,13 @@ def test_cortex_start_emits_agents_check(tmp_path): cortex.start() cortex.callosum.emit.assert_any_call( - "supervisor", "request", cmd=["sol", "agents", "check"] + "supervisor", "request", cmd=["sol", "providers", "check"] ) def test_missing_env_key_returns_skip(monkeypatch): """_check_generate returns skip status when env key is not set.""" - import think.agents as agents + import think.providers_cli as providers_cli monkeypatch.setattr( "think.providers.PROVIDER_METADATA", @@ -380,7 +380,7 @@ def test_missing_env_key_returns_skip(monkeypatch): ) monkeypatch.delenv("FAKE_API_KEY", raising=False) - status, msg = agents._check_generate("fake", 2, 30) + status, msg = providers_cli._check_generate("fake", 2, 30) assert status == "skip" assert "Fake Provider not configured" in msg assert "FAKE_API_KEY" in msg @@ -388,8 +388,8 @@ def test_missing_env_key_returns_skip(monkeypatch): def test_cogitate_missing_binary_returns_skip(monkeypatch): """_check_cogitate returns skip when CLI binary is not installed.""" - import think.agents as agents import think.providers as providers + import think.providers_cli as providers_cli monkeypatch.setitem( providers.PROVIDER_METADATA, @@ -403,29 +403,29 @@ def test_cogitate_missing_binary_returns_skip(monkeypatch): monkeypatch.setenv("FAKE_API_KEY", "test-key") monkeypatch.setattr("shutil.which", lambda _: None) - status, msg = asyncio.run(agents._check_cogitate("fake", 2, 30)) + status, msg = asyncio.run(providers_cli._check_cogitate("fake", 2, 30)) assert status == "skip" assert "nonexistent-binary-xyz CLI not installed" in msg def test_all_skip_exits_zero(tmp_path, monkeypatch): """Exit code is 0 when all results are skipped (no fails).""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) monkeypatch.setattr( - agents, "_check_generate", lambda *_args: ("skip", "not configured") + providers_cli, "_check_generate", lambda *_args: ("skip", "not configured") ) async def mock_check_cogitate(*_args): return "skip", "not configured" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -437,11 +437,11 @@ def test_all_skip_exits_zero(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 - payload = json.loads((tmp_path / "health" / "agents.json").read_text()) + payload = json.loads((tmp_path / "health" / "talents.json").read_text()) assert payload["summary"]["skipped"] == 6 assert payload["summary"]["failed"] == 0 assert payload["summary"]["passed"] == 0 @@ -452,22 +452,22 @@ def test_all_skip_exits_zero(tmp_path, monkeypatch): def test_mix_skip_and_fail_exits_one(tmp_path, monkeypatch): """Exit code is 1 when there's a mix of skip and fail results.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) monkeypatch.setattr( - agents, "_check_generate", lambda *_args: ("skip", "not configured") + providers_cli, "_check_generate", lambda *_args: ("skip", "not configured") ) async def mock_check_cogitate(*_args): return "fail", "FAIL: broken" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -479,18 +479,18 @@ def test_mix_skip_and_fail_exits_one(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 1 - payload = json.loads((tmp_path / "health" / "agents.json").read_text()) + payload = json.loads((tmp_path / "health" / "talents.json").read_text()) assert payload["summary"]["skipped"] == 3 assert payload["summary"]["failed"] == 3 def test_skipped_count_in_summary(tmp_path, monkeypatch): """Summary total equals passed + skipped + failed.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"okp": object(), "skipP": object()} fake_defaults = { @@ -500,21 +500,21 @@ def test_skipped_count_in_summary(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) def mock_gen(provider, tier, timeout): if provider == "okp": return "ok", "OK" return "skip", "not configured" - monkeypatch.setattr(agents, "_check_generate", mock_gen) + monkeypatch.setattr(providers_cli, "_check_generate", mock_gen) async def mock_cog(provider, tier, timeout): if provider == "okp": return "ok", "OK" return "skip", "not configured" - monkeypatch.setattr(agents, "_check_cogitate", mock_cog) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_cog) args = argparse.Namespace( provider=None, @@ -526,10 +526,10 @@ def test_skipped_count_in_summary(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 - payload = json.loads((tmp_path / "health" / "agents.json").read_text()) + payload = json.loads((tmp_path / "health" / "talents.json").read_text()) summary = payload["summary"] assert ( summary["total"] == summary["passed"] + summary["skipped"] + summary["failed"] @@ -541,20 +541,20 @@ def test_skipped_count_in_summary(tmp_path, monkeypatch): def test_status_field_in_json_output(tmp_path, monkeypatch, capsys): """JSON output includes status per result and skipped in summary.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "OK")) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "OK")) async def mock_cog(*_args): return "ok", "OK" - monkeypatch.setattr(agents, "_check_cogitate", mock_cog) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_cog) args = argparse.Namespace( provider=None, @@ -566,7 +566,7 @@ def test_status_field_in_json_output(tmp_path, monkeypatch, capsys): ) with pytest.raises(SystemExit): - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) captured = capsys.readouterr() data = json.loads(captured.out) diff --git a/tests/test_retention.py b/tests/test_retention.py index 2fb7b39ba..2df801f20 100644 --- a/tests/test_retention.py +++ b/tests/test_retention.py @@ -107,7 +107,7 @@ def _make_segment( """Create a segment directory with specified contents.""" seg = tmp_path / "segment" seg.mkdir(exist_ok=True) - agents_dir = seg / "agents" + agents_dir = seg / "talents" agents_dir.mkdir(exist_ok=True) if audio: @@ -167,7 +167,7 @@ class TestIsSegmentComplete: def test_complete_with_stub_speaker_labels(self, tmp_path): """Stub speaker_labels.json (skipped=True, labels=[]) unblocks retention.""" seg = _make_segment(tmp_path, audio=True, embeddings=True, speaker_labels=False) - stub = seg / "agents" / "speaker_labels.json" + stub = seg / "talents" / "speaker_labels.json" stub.write_text( json.dumps({"labels": [], "skipped": True, "reason": "no_owner_centroid"}) ) @@ -286,14 +286,14 @@ class TestPurge: (day1 / "audio.flac").write_bytes(b"x" * 1000) (day1 / "audio.jsonl").write_text('{"raw":"audio.flac"}\n') (day1 / "stream.json").write_text('{"stream":"default"}') - (day1 / "agents").mkdir() + (day1 / "talents").mkdir() day1b = journal / "chronicle" / "20260115" / "plaud" / "103000_300" day1b.mkdir(parents=True) (day1b / "audio.m4a").write_bytes(b"x" * 500) (day1b / "audio.jsonl").write_text('{"raw":"audio.m4a"}\n') (day1b / "stream.json").write_text('{"stream":"plaud"}') - (day1b / "agents").mkdir() + (day1b / "talents").mkdir() # Day 2: recent — one complete segment (must stay within 30d window) day2 = journal / "chronicle" / "20260401" / "default" / "120000_300" @@ -301,7 +301,7 @@ class TestPurge: (day2 / "audio.flac").write_bytes(b"x" * 800) (day2 / "audio.jsonl").write_text('{"raw":"audio.flac"}\n') (day2 / "stream.json").write_text('{"stream":"default"}') - (day2 / "agents").mkdir() + (day2 / "talents").mkdir() # Day 3: incomplete segment (no audio.jsonl) day3 = journal / "chronicle" / "20260101" / "default" / "140000_300" @@ -449,7 +449,7 @@ class TestPurgeProvenance: segment = journal / "chronicle" / "20260115" / "default" / "100000_300" audio_jsonl = segment / "audio.jsonl" alternate_audio_jsonl = segment / "meeting_audio.jsonl" - speaker_labels = segment / "agents" / "speaker_labels.json" + speaker_labels = segment / "talents" / "speaker_labels.json" alternate_audio_jsonl.write_text('{"raw":"audio.flac"}\n') speaker_labels.write_text("{}") diff --git a/tests/test_routines.py b/tests/test_routines.py index 25bfe4928..28008197b 100644 --- a/tests/test_routines.py +++ b/tests/test_routines.py @@ -201,7 +201,7 @@ class TestCheck: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -237,7 +237,7 @@ class TestCheck: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -273,7 +273,7 @@ class TestCheck: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -309,7 +309,7 @@ class TestCheck: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -508,7 +508,7 @@ class TestEventTrigger: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -530,7 +530,7 @@ class TestEventTrigger: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -552,7 +552,7 @@ class TestEventTrigger: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -574,7 +574,7 @@ class TestEventTrigger: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -786,7 +786,7 @@ class TestResumeDate: with ( patch("think.routines.cortex_request", return_value="fake_agent_id"), patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -825,7 +825,7 @@ class TestResumeDate: with ( patch("think.routines.cortex_request", return_value="fake_agent_id"), patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -1576,7 +1576,7 @@ class TestMetaFiltering: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), diff --git a/tests/test_segment.py b/tests/test_segment.py index 577bd94ec..2517f3b28 100644 --- a/tests/test_segment.py +++ b/tests/test_segment.py @@ -20,7 +20,7 @@ def _make_segment( stream_json=None, audio=True, screen=True, - agents=None, + talents=None, ): """Create a minimal segment fixture directory.""" seg_dir = base / "chronicle" / day / stream / segment @@ -31,11 +31,11 @@ def _make_segment( (seg_dir / "audio.jsonl").write_text('{"t":0}\n') if screen: (seg_dir / "screen.jsonl").write_text('{"t":0}\n') - if agents: - agents_dir = seg_dir / "agents" - agents_dir.mkdir() - for name in agents: - (agents_dir / name).write_text("# agent output\n") + if talents: + talents_dir = seg_dir / "talents" + talents_dir.mkdir() + for name in talents: + (talents_dir / name).write_text("# talent output\n") return seg_dir @@ -52,7 +52,7 @@ def test_list_basic(tmp_path, monkeypatch, capsys): "prev_segment": None, "seq": 1, }, - agents=["audio.md"], + talents=["audio.md"], ) _make_segment( tmp_path, @@ -65,7 +65,7 @@ def test_list_basic(tmp_path, monkeypatch, capsys): "prev_segment": "090000_300", "seq": 2, }, - agents=["audio.md", "screen.md"], + talents=["audio.md", "screen.md"], ) args = argparse.Namespace( @@ -131,7 +131,7 @@ def test_list_json(tmp_path, monkeypatch, capsys): "prev_segment": None, "seq": 1, }, - agents=["audio.md"], + talents=["audio.md"], ) args = argparse.Namespace( @@ -143,7 +143,7 @@ def test_list_json(tmp_path, monkeypatch, capsys): assert isinstance(data, list) assert data[0]["stream"] == "default" assert data[0]["segment"] == "090000_300" - assert data[0]["agents"] == 1 + assert data[0]["talents"] == 1 def test_list_empty_day(tmp_path, monkeypatch, capsys): @@ -171,7 +171,7 @@ def test_inspect_basic(tmp_path, monkeypatch, capsys): "prev_segment": None, "seq": 1, }, - agents=["audio.md"], + talents=["audio.md"], ) args = argparse.Namespace( @@ -226,7 +226,7 @@ def test_inspect_json(tmp_path, monkeypatch, capsys): "prev_segment": None, "seq": 1, }, - agents=["audio.md"], + talents=["audio.md"], ) args = argparse.Namespace( diff --git a/tests/test_sense_splitter.py b/tests/test_sense_splitter.py index 2114a3190..d21e9166b 100644 --- a/tests/test_sense_splitter.py +++ b/tests/test_sense_splitter.py @@ -40,7 +40,7 @@ class TestWriteSenseOutputs: write_sense_outputs(sense_json, seg_dir) - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" assert (agents_dir / "activity.md").exists() assert (agents_dir / "facets.json").exists() assert (agents_dir / "density.json").exists() @@ -77,7 +77,7 @@ class TestWriteSenseOutputs: write_sense_outputs(sense_json, seg_dir) - stored = json.loads((seg_dir / "agents" / "sense.json").read_text("utf-8")) + stored = json.loads((seg_dir / "talents" / "sense.json").read_text("utf-8")) assert stored["foo"] == "bar" assert stored == sense_json @@ -94,7 +94,7 @@ class TestMeetingDetection: write_sense_outputs(sense_json, seg_dir) - speakers_path = seg_dir / "agents" / "speakers.json" + speakers_path = seg_dir / "talents" / "speakers.json" assert speakers_path.exists() assert json.loads(speakers_path.read_text(encoding="utf-8")) == ["Alice", "Bob"] @@ -105,7 +105,7 @@ class TestMeetingDetection: write_sense_outputs(_make_sense_output(meeting_detected=False), seg_dir) - assert not (seg_dir / "agents" / "speakers.json").exists() + assert not (seg_dir / "talents" / "speakers.json").exists() def test_meeting_with_no_speakers_writes_empty_array(self, tmp_path): from think.sense_splitter import write_sense_outputs @@ -115,7 +115,7 @@ class TestMeetingDetection: write_sense_outputs(sense_json, seg_dir) - speakers_path = seg_dir / "agents" / "speakers.json" + speakers_path = seg_dir / "talents" / "speakers.json" assert speakers_path.exists() assert json.loads(speakers_path.read_text(encoding="utf-8")) == [] @@ -128,7 +128,7 @@ class TestEdgeCases: write_sense_outputs({}, seg_dir) - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" assert (agents_dir / "activity.md").exists() assert (agents_dir / "facets.json").exists() assert (agents_dir / "density.json").exists() @@ -156,7 +156,7 @@ class TestEdgeCases: write_sense_outputs(sense_json, seg_dir) - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" assert (agents_dir / "activity.md").read_text(encoding="utf-8") == "" assert ( json.loads((agents_dir / "facets.json").read_text(encoding="utf-8")) == [] @@ -172,7 +172,7 @@ class TestEdgeCases: write_sense_outputs(_make_sense_output(activity_summary=""), seg_dir) - assert (seg_dir / "agents" / "activity.md").read_text(encoding="utf-8") == "" + assert (seg_dir / "talents" / "activity.md").read_text(encoding="utf-8") == "" class TestMultipleFacets: @@ -187,7 +187,7 @@ class TestMultipleFacets: write_sense_outputs(_make_sense_output(facets=facets), seg_dir) - assert json.loads((seg_dir / "agents" / "facets.json").read_text("utf-8")) == ( + assert json.loads((seg_dir / "talents" / "facets.json").read_text("utf-8")) == ( facets ) @@ -200,7 +200,7 @@ class TestWriteIdleStubs: write_idle_stubs(seg_dir) - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" assert (agents_dir / "density.json").exists() density = json.loads((agents_dir / "density.json").read_text(encoding="utf-8")) assert density["classification"] == "idle" diff --git a/tests/test_sol.py b/tests/test_sol.py index 26fae0429..02002e678 100644 --- a/tests/test_sol.py +++ b/tests/test_sol.py @@ -281,6 +281,6 @@ class TestCommandRegistry: def test_critical_commands_registered(self): """Test that critical commands are registered.""" - critical = ["import", "agents", "dream", "indexer", "transcribe"] + critical = ["import", "providers", "dream", "indexer", "transcribe"] for cmd in critical: assert cmd in sol.COMMANDS, f"Critical command '{cmd}' not registered" diff --git a/tests/test_speaker_attribution_hook.py b/tests/test_speaker_attribution_hook.py index 8a7354345..e7573615c 100644 --- a/tests/test_speaker_attribution_hook.py +++ b/tests/test_speaker_attribution_hook.py @@ -36,7 +36,7 @@ class TestPreProcessStub: tmp_path, {"error": "no_owner_centroid"}, ) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert stub_path.exists() data = json.loads(stub_path.read_text()) assert data == {"labels": [], "skipped": True, "reason": "no_owner_centroid"} @@ -49,7 +49,7 @@ class TestPreProcessStub: tmp_path, {"error": "no_owner_centroid"}, ) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert not stub_path.exists() assert result == {"skip_reason": "no_owner_centroid"} @@ -61,7 +61,7 @@ class TestPreProcessStub: tmp_path, {"labels": []}, ) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert stub_path.exists() data = json.loads(stub_path.read_text()) assert data == {"labels": [], "skipped": True, "reason": "no_embeddings"} @@ -74,7 +74,7 @@ class TestPreProcessStub: tmp_path, {"labels": []}, ) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert not stub_path.exists() assert result == {"skip_reason": "no_embeddings"} @@ -85,7 +85,7 @@ class TestPreProcessStub: from talent.speaker_attribution import pre_process result = pre_process({"stream": "default"}) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert not stub_path.exists() assert result == {"skip_reason": "no_segment_context"} diff --git a/tests/test_stats_contract.py b/tests/test_stats_contract.py index f7cebeac1..402873f99 100644 --- a/tests/test_stats_contract.py +++ b/tests/test_stats_contract.py @@ -20,7 +20,7 @@ CONTRACT_FIELDS = [ ("tokens.by_model", "tokens.by_model"), ("tokens.by_day", "tokens.by_day"), ("facets.counts_by_day", "facets.counts_by_day"), - ("agents.counts_by_day", "agents.counts_by_day"), + ("talents.counts_by_day", "talents.counts_by_day"), ("days.*.transcript_duration", "transcript_duration"), ("days.*.percept_duration", "percept_duration"), ("tokens.by_day.*.*.input_tokens", "input_tokens"), @@ -61,7 +61,7 @@ def _build_journal(base_path): seg2 = day / "default" / "134500_300" seg1.mkdir(parents=True) seg2.mkdir(parents=True) - (day / "agents").mkdir(parents=True) + (day / "talents").mkdir(parents=True) audio_lines = [ {"raw": "raw.flac"}, @@ -82,7 +82,7 @@ def _build_journal(base_path): ) (seg2 / "audio.flac").write_bytes(b"fLaC") - (day / "agents" / "flow.md").write_text("") + (day / "talents" / "flow.md").write_text("") events_dir = journal / "facets" / "work" / "events" events_dir.mkdir(parents=True) @@ -98,7 +98,7 @@ def _build_journal(base_path): "facet": "work", "agent": "meetings", "occurred": True, - "source": "20240101/agents/meetings.md", + "source": "20240101/talents/meetings.md", } (events_dir / "20240101.jsonl").write_text(json.dumps(event) + "\n") @@ -186,7 +186,7 @@ def test_schema_rejects_missing_required_key(): "totals": {}, "heatmap": [], "tokens": {}, - "agents": {}, + "talents": {}, "facets": {}, } del output["totals"] @@ -209,7 +209,7 @@ def test_schema_rejects_wrong_version(): "totals": {}, "heatmap": [], "tokens": {}, - "agents": {}, + "talents": {}, "facets": {}, } ) diff --git a/tests/test_talent.py b/tests/test_talent.py index 5f6458c10..0be352919 100644 --- a/tests/test_talent.py +++ b/tests/test_talent.py @@ -7,8 +7,8 @@ import pytest from think.talent import ( _validate_cwd, - get_agent, - get_agent_filter, + get_talent, + get_talent_filter, source_is_enabled, source_is_required, ) @@ -52,21 +52,21 @@ def test_source_is_required_dict(): def test_get_agent_filter_bool(): - """Test get_agent_filter with bool values.""" - assert get_agent_filter(True) is None - assert get_agent_filter(False) == {} + """Test get_talent_filter with bool values.""" + assert get_talent_filter(True) is None + assert get_talent_filter(False) == {} def test_get_agent_filter_required_string(): - """Test get_agent_filter with 'required' string.""" - assert get_agent_filter("required") is None + """Test get_talent_filter with 'required' string.""" + assert get_talent_filter("required") is None def test_get_agent_filter_dict(): - """Test get_agent_filter with dict values.""" + """Test get_talent_filter with dict values.""" filter_dict = {"entities": True, "meetings": "required", "flow": False} - assert get_agent_filter(filter_dict) == filter_dict - assert get_agent_filter({}) == {} + assert get_talent_filter(filter_dict) == filter_dict + assert get_talent_filter({}) == {} def test_validate_cwd_defaults_cogitate_to_journal(): @@ -98,10 +98,10 @@ def test_validate_cwd_rejects_invalid_value(): def test_get_agent_normalizes_cwd_for_cogitate(): - config = get_agent("chat") + config = get_talent("chat") assert config["cwd"] == "journal" def test_get_agent_preserves_repo_cwd_for_coder(): - config = get_agent("coder") + config = get_talent("coder") assert config["cwd"] == "repo" diff --git a/tests/test_talent_cli.py b/tests/test_talent_cli.py index 8f0777000..73f9d798d 100644 --- a/tests/test_talent_cli.py +++ b/tests/test_talent_cli.py @@ -359,7 +359,7 @@ def test_logs_runs_new_columns(capsys): output = capsys.readouterr().out lines = [line for line in output.strip().splitlines() if line.strip()] - # Find the line for agent_id 1700000000001 (has JSONL file) + # Find the line for use_id 1700000000001 (has JSONL file) enriched_line = None for line in lines: if "1700000000001" in line: @@ -488,7 +488,7 @@ def test_parse_run_stats(): """Parse run stats extracts correct counts from fixture JSONL.""" from pathlib import Path - jsonl = Path("tests/fixtures/journal/agents/default/1700000000001.jsonl") + jsonl = Path("tests/fixtures/journal/talents/default/1700000000001.jsonl") stats = _parse_run_stats(jsonl) assert stats["event_count"] == 6 # all except request assert stats["tool_count"] == 1 # one tool_start @@ -502,7 +502,7 @@ def test_parse_run_stats_error(): """Parse run stats handles error run JSONL correctly.""" from pathlib import Path - jsonl = Path("tests/fixtures/journal/agents/flow/1700000000002.jsonl") + jsonl = Path("tests/fixtures/journal/talents/flow/1700000000002.jsonl") stats = _parse_run_stats(jsonl) assert stats["event_count"] == 2 # start + error (not request) assert stats["tool_count"] == 0 diff --git a/tests/test_agent_fallback.py b/tests/test_talent_fallback.py similarity index 92% rename from tests/test_agent_fallback.py rename to tests/test_talent_fallback.py index a63a779dc..403450d38 100644 --- a/tests/test_agent_fallback.py +++ b/tests/test_talent_fallback.py @@ -10,13 +10,13 @@ from unittest.mock import MagicMock import pytest -from think.agents import _is_retryable_error from think.models import ( TYPE_DEFAULTS, get_backup_provider, is_provider_healthy, should_recheck_health, ) +from think.talents import _is_retryable_error def test_is_provider_healthy_all_failed(): @@ -100,7 +100,7 @@ def _mock_base_agent_config() -> dict: def _patch_prepare_config_dependencies(monkeypatch): monkeypatch.setattr( - "think.talent.get_agent", lambda *args, **kwargs: _mock_base_agent_config() + "think.talent.get_talent", lambda *args, **kwargs: _mock_base_agent_config() ) monkeypatch.setattr( "think.talent.key_to_context", lambda _name: "talent.system.default" @@ -112,7 +112,7 @@ def _patch_prepare_config_dependencies(monkeypatch): def test_preflight_swap_unhealthy_primary(monkeypatch): - from think.agents import prepare_config + from think.talents import prepare_config _patch_prepare_config_dependencies(monkeypatch) monkeypatch.setattr( @@ -135,7 +135,7 @@ def test_preflight_swap_unhealthy_primary(monkeypatch): def test_preflight_no_swap_healthy_primary(monkeypatch): - from think.agents import prepare_config + from think.talents import prepare_config _patch_prepare_config_dependencies(monkeypatch) monkeypatch.setattr( @@ -151,7 +151,7 @@ def test_preflight_no_swap_healthy_primary(monkeypatch): def test_preflight_no_swap_no_backup_key(monkeypatch): - from think.agents import prepare_config + from think.talents import prepare_config _patch_prepare_config_dependencies(monkeypatch) monkeypatch.setattr( @@ -169,7 +169,7 @@ def test_preflight_no_swap_no_backup_key(monkeypatch): def test_on_failure_retry_cogitate(monkeypatch): - from think.agents import _execute_with_tools + from think.talents import _execute_with_tools events = [] attempts = {"primary": 0, "backup": 0} @@ -219,7 +219,7 @@ def test_on_failure_retry_cogitate(monkeypatch): def test_on_failure_retry_cogitate_uses_context_from_name(monkeypatch): - from think.agents import _execute_with_tools + from think.talents import _execute_with_tools events = [] seen = {} @@ -267,7 +267,7 @@ def test_on_failure_retry_cogitate_uses_context_from_name(monkeypatch): def test_on_failure_retry_generate(monkeypatch): - from think.agents import _execute_generate + from think.talents import _execute_generate events = [] calls = {"count": 0} @@ -310,7 +310,7 @@ def test_on_failure_retry_generate(monkeypatch): def test_on_failure_no_retry_value_error(monkeypatch): - from think.agents import _execute_generate + from think.talents import _execute_generate events = [] assert _is_retryable_error(ValueError("bad input")) is False @@ -338,7 +338,7 @@ def test_on_failure_no_retry_value_error(monkeypatch): def test_on_failure_both_fail_raises_original(monkeypatch): - from think.agents import _execute_generate + from think.talents import _execute_generate events = [] calls = {"count": 0} @@ -375,7 +375,7 @@ def test_on_failure_both_fail_raises_original(monkeypatch): def test_fallback_event_emitted(): - from think.agents import _run_agent + from think.talents import _run_talent events = [] config = { @@ -387,7 +387,7 @@ def test_fallback_event_emitted(): "fallback_from": "google", } - asyncio.run(_run_agent(config, events.append, dry_run=True)) + asyncio.run(_run_talent(config, events.append, dry_run=True)) fallback_events = [e for e in events if e.get("event") == "fallback"] assert len(fallback_events) == 1 @@ -395,7 +395,7 @@ def test_fallback_event_emitted(): def test_recheck_requested_on_stale(monkeypatch): - from think.agents import _execute_with_tools + from think.talents import _execute_with_tools async def pass_cogitate(*_args, **kwargs): on_event = kwargs.get("on_event") @@ -425,12 +425,12 @@ def test_recheck_requested_on_stale(monkeypatch): def test_main_async_no_duplicate_error_when_evented(monkeypatch, capsys): - from think.agents import main_async + from think.talents import main_async ndjson_input = json.dumps({"name": "unified", "prompt": "hello"}) monkeypatch.setattr("sys.stdin", StringIO(ndjson_input)) - async def fake_run_agent(_config, emit_event, dry_run=False): + async def fake_run_talent(_config, emit_event, dry_run=False): emit_event({"event": "error", "error": "provider failed"}) exc = RuntimeError("provider failed") setattr(exc, "_evented", True) @@ -441,16 +441,16 @@ def test_main_async_no_duplicate_error_when_evented(monkeypatch, capsys): mock_args.dry_run = False mock_args.subcommand = None - monkeypatch.setattr("think.agents.setup_cli", lambda _parser: mock_args) + monkeypatch.setattr("think.talents.setup_cli", lambda _parser: mock_args) monkeypatch.setattr( - "think.agents.setup_logging", + "think.talents.setup_logging", lambda _verbose=False: MagicMock(), ) monkeypatch.setattr( - "think.agents.prepare_config", lambda _request: {"type": "cogitate"} + "think.talents.prepare_config", lambda _request: {"type": "cogitate"} ) - monkeypatch.setattr("think.agents.validate_config", lambda _config: None) - monkeypatch.setattr("think.agents._run_agent", fake_run_agent) + monkeypatch.setattr("think.talents.validate_config", lambda _config: None) + monkeypatch.setattr("think.talents._run_talent", fake_run_talent) asyncio.run(main_async()) diff --git a/tests/test_agents_ndjson.py b/tests/test_talents_ndjson.py similarity index 91% rename from tests/test_agents_ndjson.py rename to tests/test_talents_ndjson.py index 66593fce4..63b26243b 100644 --- a/tests/test_agents_ndjson.py +++ b/tests/test_talents_ndjson.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Tests for NDJSON-only input in think.agents.""" +"""Tests for NDJSON-only input in think.talents.""" import asyncio import json @@ -19,7 +19,7 @@ def mock_journal(tmp_path, monkeypatch): """Set up a temporary journal directory.""" journal_path = tmp_path / "journal" journal_path.mkdir() - agents_path = journal_path / "agents" + agents_path = journal_path / "talents" agents_path.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_path)) @@ -87,7 +87,7 @@ def mock_all_providers(monkeypatch): monkeypatch.setitem(sys.modules, "agents", MagicMock()) # Mock prepare_config to avoid needing real agent configs - monkeypatch.setattr("think.agents.prepare_config", mock_prepare_config) + monkeypatch.setattr("think.talents.prepare_config", mock_prepare_config) def test_ndjson_single_request(mock_journal, monkeypatch, capsys): @@ -110,9 +110,9 @@ def test_ndjson_single_request(mock_journal, monkeypatch, capsys): mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() @@ -162,9 +162,9 @@ def test_ndjson_multiple_requests(mock_journal, monkeypatch, capsys): mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() @@ -198,9 +198,9 @@ not valid json mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() @@ -233,9 +233,9 @@ def test_ndjson_missing_prompt(mock_journal, monkeypatch, capsys): mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() @@ -263,9 +263,9 @@ def test_ndjson_empty_lines(mock_journal, monkeypatch, capsys): mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() diff --git a/tests/verify_api.py b/tests/verify_api.py index cd0e6bddd..6eb1a5781 100644 --- a/tests/verify_api.py +++ b/tests/verify_api.py @@ -46,8 +46,8 @@ ENDPOINTS = [ # apps/sol/routes.py { "app": "sol", - "name": "agents-day", - "path": "/app/sol/api/agents/20260304", + "name": "talents-day", + "path": "/app/sol/api/talents/20260304", "params": {"facet": "work"}, "status": 200, }, diff --git a/think/activities.py b/think/activities.py index fb96b9e80..1c34fc44c 100644 --- a/think/activities.py +++ b/think/activities.py @@ -639,7 +639,7 @@ def load_segment_activity_state( if not seg_dir: return None - state_path = seg_dir / "agents" / facet / "activity_state.json" + state_path = seg_dir / "talents" / facet / "activity_state.json" if not state_path.exists(): return None diff --git a/think/chat_cli.py b/think/chat_cli.py index 0b49ad428..a3cf5a812 100644 --- a/think/chat_cli.py +++ b/think/chat_cli.py @@ -10,7 +10,7 @@ import sys import threading from think.callosum import CallosumConnection -from think.cortex_client import cortex_request, read_agent_events +from think.cortex_client import cortex_request, read_use_events from think.utils import require_solstone, setup_cli @@ -43,13 +43,13 @@ def main() -> None: if args.facet: config["facet"] = args.facet - agent_id = cortex_request( + use_id = cortex_request( prompt=message, name=args.talent, provider=args.provider, config=config if config else None, ) - if agent_id is None: + if use_id is None: print( "Error: failed to connect to cortex (is the stack running?)", file=sys.stderr, @@ -63,7 +63,7 @@ def main() -> None: def on_event(msg: dict) -> None: if msg.get("tract") != "cortex": return - if msg.get("agent_id") != agent_id: + if msg.get("use_id") != use_id: return event_type = msg.get("event") @@ -122,7 +122,7 @@ def main() -> None: sys.exit(1) try: - events = read_agent_events(agent_id) + events = read_use_events(use_id) for event in reversed(events): event_type = event.get("event") if event_type == "finish": diff --git a/think/cluster.py b/think/cluster.py index adb4bd2e0..57ed56a60 100644 --- a/think/cluster.py +++ b/think/cluster.py @@ -217,9 +217,9 @@ def _process_segment( agent_filter = ( None if agents is True else agents if isinstance(agents, dict) else None ) - agents_dir = segment_path / "agents" - if agents_dir.is_dir(): - for md_file in sorted(agents_dir.rglob("*.md")): + talents_dir = segment_path / "talents" + if talents_dir.is_dir(): + for md_file in sorted(talents_dir.rglob("*.md")): if not md_file.is_file(): continue @@ -230,7 +230,7 @@ def _process_segment( try: content = md_file.read_text() if content.strip(): - rel_md_path = md_file.relative_to(agents_dir).as_posix() + rel_md_path = md_file.relative_to(talents_dir).as_posix() entries.append( { "timestamp": segment_start, @@ -240,7 +240,7 @@ def _process_segment( "prefix": "agent_output", "output_name": md_file.stem, "content": content, - "name": f"{segment_path.name}/agents/{rel_md_path}", + "name": f"{segment_path.name}/talents/{rel_md_path}", "stream": stream, } ) diff --git a/think/conversation.py b/think/conversation.py index f07fb41d3..a2c71cf00 100644 --- a/think/conversation.py +++ b/think/conversation.py @@ -57,14 +57,14 @@ def record_exchange( user_message: str = "", agent_response: str = "", talent: str = "", - agent_id: str = "", + use_id: str = "", ) -> None: """Record a conversation exchange to journal storage. Writes to two locations: 1. conversation/exchanges.jsonl — append-only quick-read index - 2. YYYYMMDD/conversation/HHMMSS_1/agents/conversation.md — journal entry - for FTS5 search indexing (matches */*/*/agents/*.md formatter pattern) + 2. YYYYMMDD/conversation/HHMMSS_1/talents/conversation.md — journal entry + for FTS5 search indexing (matches */*/*/talents/*.md formatter pattern) Also runs lightweight entity extraction on the conversation text. """ @@ -84,7 +84,7 @@ def record_exchange( "user_message": user_message, "agent_response": agent_response, "talent": talent, - "agent_id": agent_id, + "use_id": use_id, } # 1. Append to exchanges.jsonl (fast-read index) @@ -102,7 +102,7 @@ def record_exchange( time_key = dt.strftime("%H%M%S") segment = f"{time_key}_1" - seg_dir = day_path(day) / CONVERSATION_STREAM / segment / "agents" + seg_dir = day_path(day) / CONVERSATION_STREAM / segment / "talents" seg_dir.mkdir(parents=True, exist_ok=True) time_str = dt.strftime("%Y-%m-%d %H:%M:%S") diff --git a/think/cortex.py b/think/cortex.py index 06dfec8f0..967b651fd 100644 --- a/think/cortex.py +++ b/think/cortex.py @@ -1,17 +1,17 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Callosum-based agent process manager for solstone. +"""Callosum-based talent process manager for solstone. -Cortex listens for agent requests via the Callosum message bus and manages -agent process lifecycle: +Cortex listens for talent requests via the Callosum message bus and manages +talent process lifecycle: - Receives requests via Callosum (tract="cortex", event="request") -- Creates /_active.jsonl files to track active agents -- Spawns agent processes and captures their stdout events -- Broadcasts all agent events back to Callosum -- Renames to /.jsonl when complete +- Creates /_active.jsonl files to track active uses +- Spawns talent processes and captures their stdout events +- Broadcasts all talent events back to Callosum +- Renames to /.jsonl when complete -Agent files provide persistence and historical record, while Callosum provides +Talent files provide persistence and historical record, while Callosum provides real-time event distribution to all interested services. """ @@ -29,14 +29,15 @@ from typing import Any, Dict, Optional from think.callosum import CallosumConnection from think.runner import _atomic_symlink +from think.talents import TALENT_EXECUTION_MODULE from think.utils import get_journal, get_project_root, get_rev, now_ms -class AgentProcess: - """Manages a running agent subprocess.""" +class TalentProcess: + """Manages a running talent subprocess.""" - def __init__(self, agent_id: str, process: subprocess.Popen, log_path: Path): - self.agent_id = agent_id + def __init__(self, use_id: str, process: subprocess.Popen, log_path: Path): + self.use_id = use_id self.process = process self.log_path = log_path self.stop_event = threading.Event() @@ -62,23 +63,23 @@ class AgentProcess: self.process.wait(timeout=10) # Give more time for graceful shutdown except subprocess.TimeoutExpired: logging.getLogger(__name__).warning( - f"Agent {self.agent_id} didn't stop gracefully, killing" + f"Talent {self.use_id} didn't stop gracefully, killing" ) self.process.kill() self.process.wait() # Ensure zombie is reaped class CortexService: - """Callosum-based agent process manager.""" + """Callosum-based talent process manager.""" def __init__(self, journal_path: Optional[str] = None): self.journal_path = Path(journal_path or get_journal()) - self.agents_dir = self.journal_path / "agents" - self.agents_dir.mkdir(parents=True, exist_ok=True) + self.talents_dir = self.journal_path / "talents" + self.talents_dir.mkdir(parents=True, exist_ok=True) self.logger = logging.getLogger(__name__) - self.running_agents: Dict[str, AgentProcess] = {} - self.agent_requests: Dict[str, Dict[str, Any]] = {} # Store agent requests + self.running_uses: Dict[str, TalentProcess] = {} + self.use_requests: Dict[str, Dict[str, Any]] = {} # Store use requests self.lock = threading.RLock() self.stop_event = threading.Event() self.shutdown_requested = threading.Event() @@ -88,7 +89,7 @@ class CortexService: def _create_error_event( self, - agent_id: str, + use_id: str, error: str, trace: Optional[str] = None, exit_code: Optional[int] = None, @@ -97,7 +98,7 @@ class CortexService: event = { "event": "error", "ts": now_ms(), - "agent_id": agent_id, + "use_id": use_id, "error": error, } if trace: @@ -106,42 +107,44 @@ class CortexService: event["exit_code"] = exit_code return event - def _recover_orphaned_agents(self, active_files: list) -> None: - """Recover orphaned active agent files from a previous crash. + def _recover_orphaned_uses(self, active_files: list) -> None: + """Recover orphaned active talent files from a previous crash. Appends an error event to each file and renames to completed. """ for file_path in active_files: - agent_id = file_path.stem.replace("_active", "") + use_id = file_path.stem.replace("_active", "") try: error_event = self._create_error_event( - agent_id, "Recovered: Cortex restarted while agent was running" + use_id, "Recovered: Cortex restarted while talent was running" ) with open(file_path, "a") as f: f.write(json.dumps(error_event) + "\n") - completed_path = file_path.parent / f"{agent_id}.jsonl" + completed_path = file_path.parent / f"{use_id}.jsonl" file_path.rename(completed_path) - self.logger.warning(f"Recovered orphaned agent: {agent_id}") + self.logger.warning(f"Recovered orphaned talent: {use_id}") except Exception as e: - self.logger.error(f"Failed to recover agent {agent_id}: {e}") + self.logger.error(f"Failed to recover talent {use_id}: {e}") def start(self) -> None: - """Start listening for agent requests via Callosum.""" + """Start listening for talent requests via Callosum.""" # Recover any orphaned active files from previous crash - active_files = list(self.agents_dir.glob("*/*_active.jsonl")) + active_files = list(self.talents_dir.glob("*/*_active.jsonl")) if active_files: self.logger.warning( - f"Found {len(active_files)} orphaned agent(s), recovering..." + f"Found {len(active_files)} orphaned talent use(s), recovering..." ) - self._recover_orphaned_agents(active_files) + self._recover_orphaned_uses(active_files) # Connect to Callosum to receive requests try: self.callosum.start(callback=self._handle_callosum_message) self.logger.info("Connected to Callosum message bus") - self.callosum.emit("supervisor", "request", cmd=["sol", "agents", "check"]) - self.logger.info("Requested agents health check via supervisor") + self.callosum.emit( + "supervisor", "request", cmd=["sol", "providers", "check"] + ) + self.logger.info("Requested providers health check via supervisor") except Exception as e: self.logger.error(f"Failed to connect to Callosum: {e}") sys.exit(1) @@ -153,7 +156,7 @@ class CortexService: daemon=True, ).start() - self.logger.info("Cortex service started, listening for agent requests") + self.logger.info("Cortex service started, listening for talent requests") while True: try: @@ -162,9 +165,9 @@ class CortexService: # Exit when idle during shutdown if self.shutdown_requested.is_set(): with self.lock: - if len(self.running_agents) == 0: + if len(self.running_uses) == 0: self.logger.info( - "No agents running, exiting gracefully" + "No talent uses running, exiting gracefully" ) return break @@ -185,36 +188,36 @@ class CortexService: self.logger.exception(f"Error handling request: {e}") def _handle_request(self, request: Dict[str, Any]) -> None: - """Handle a new agent request from Callosum. + """Handle a new talent request from Callosum. Cortex is a minimal process manager - it only handles: - - File lifecycle (/_active.jsonl -> /.jsonl) + - File lifecycle (/_active.jsonl -> /.jsonl) - Process spawning and monitoring - Event relay to Callosum - All config loading, validation, and hydration is done by agents.py. + All config loading, validation, and hydration is done by think.talents. Cortex only resolves talent cwd early so the child process starts in the correct working directory. """ - agent_id = request.get("agent_id") - if not agent_id: - self.logger.error("Received request without agent_id") + use_id = request.get("use_id") + if not use_id: + self.logger.error("Received request without use_id") return - # Skip if this agent is already being processed + # Skip if this use is already being processed with self.lock: - if agent_id in self.running_agents: - self.logger.debug(f"Agent {agent_id} already running, skipping") + if use_id in self.running_uses: + self.logger.debug(f"Talent use {use_id} already running, skipping") return # Create _active.jsonl file (exclusive creation to prevent race conditions) name = request.get("name", "unified") safe_name = name.replace(":", "--") - agent_subdir = self.agents_dir / safe_name - agent_subdir.mkdir(parents=True, exist_ok=True) - file_path = agent_subdir / f"{agent_id}_active.jsonl" + talent_subdir = self.talents_dir / safe_name + talent_subdir.mkdir(parents=True, exist_ok=True) + file_path = talent_subdir / f"{use_id}_active.jsonl" if file_path.exists(): - self.logger.debug(f"Agent {agent_id} already claimed by another process") + self.logger.debug(f"Talent use {use_id} already claimed by another process") return try: @@ -223,24 +226,28 @@ class CortexService: except FileExistsError: return - self.logger.info(f"Processing agent request: {agent_id}") + self.logger.info(f"Processing talent request: {use_id}") # Store request for later use (output writing) with self.lock: - self.agent_requests[agent_id] = request + self.use_requests[use_id] = request - # Spawn agent process - it handles all validation/hydration + # Spawn talent process - it handles all validation/hydration try: self._spawn_subprocess( - agent_id, file_path, request, ["sol", "agents"], "agent" + use_id, + file_path, + request, + [sys.executable, "-m", TALENT_EXECUTION_MODULE], + "talent", ) except Exception as e: - self.logger.exception(f"Failed to spawn agent {agent_id}: {e}") - self._write_error_and_complete(file_path, f"Failed to spawn agent: {e}") + self.logger.exception(f"Failed to spawn talent {use_id}: {e}") + self._write_error_and_complete(file_path, f"Failed to spawn talent: {e}") def _spawn_subprocess( self, - agent_id: str, + use_id: str, file_path: Path, config: Dict[str, Any], cmd: list[str], @@ -249,16 +256,16 @@ class CortexService: """Spawn a subprocess and monitor its output. Args: - agent_id: Unique identifier for this process + use_id: Unique identifier for this process file_path: Path to the JSONL log file config: Configuration dict to pass via NDJSON stdin - cmd: Command to run (e.g., ["sol", "agents"]) - process_type: Label for logging ("agent") + cmd: Command to run (e.g., [sys.executable, "-m", TALENT_EXECUTION_MODULE]) + process_type: Label for logging ("talent") """ try: # Store the config for later use - thread safe with self.lock: - self.agent_requests[agent_id] = config + self.use_requests[use_id] = config # Pass the full config through as NDJSON ndjson_input = json.dumps(config) @@ -280,16 +287,16 @@ class CortexService: env.update({k: str(v) for k, v in env_overrides.items()}) # Spawn the subprocess - self.logger.info(f"Spawning {process_type} {agent_id}: {cmd}") + self.logger.info(f"Spawning {process_type} {use_id}: {cmd}") self.logger.debug(f"NDJSON input: {ndjson_input}") subprocess_cwd = None - if process_type == "agent": - from think.talent import get_agent + if process_type == "talent": + from think.talent import get_talent talent_key = str(config.get("name", "unified")) - talent_config = get_agent(talent_key) + talent_config = get_talent(talent_key) if talent_config.get("type") == "cogitate": - # Resolve here because prepare_config() runs inside sol agents. + # Resolve here because prepare_config() runs inside think.talents. cwd_value = talent_config.get("cwd") if cwd_value == "journal": try: @@ -321,15 +328,15 @@ class CortexService: process.stdin.close() # Track the running process - agent = AgentProcess(agent_id, process, file_path) + agent = TalentProcess(use_id, process, file_path) with self.lock: - self.running_agents[agent_id] = agent + self.running_uses[use_id] = agent # Set up timeout (default to 10 minutes if not specified) timeout_seconds = config.get("timeout_seconds", 600) agent.timeout_timer = threading.Timer( timeout_seconds, - lambda: self._timeout_agent(agent_id, agent, timeout_seconds), + lambda: self._timeout_talent(use_id, agent, timeout_seconds), ) agent.timeout_timer.start() @@ -343,26 +350,26 @@ class CortexService: ).start() self.logger.info( - f"{process_type.capitalize()} {agent_id} spawned successfully " + f"{process_type.capitalize()} {use_id} spawned successfully " f"(PID: {process.pid})" ) except Exception as e: - self.logger.exception(f"Failed to spawn {process_type} {agent_id}: {e}") + self.logger.exception(f"Failed to spawn {process_type} {use_id}: {e}") self._write_error_and_complete( file_path, f"Failed to spawn {process_type}: {e}" ) - def _timeout_agent( - self, agent_id: str, agent: AgentProcess, timeout_seconds: int + def _timeout_talent( + self, use_id: str, agent: TalentProcess, timeout_seconds: int ) -> None: - """Handle agent timeout.""" + """Handle talent timeout.""" if agent.is_running(): self.logger.warning( - f"Agent {agent_id} timed out after {timeout_seconds} seconds" + f"Talent {use_id} timed out after {timeout_seconds} seconds" ) error_event = self._create_error_event( - agent_id, f"Agent timed out after {timeout_seconds} seconds" + use_id, f"Talent timed out after {timeout_seconds} seconds" ) try: with open(agent.log_path, "a") as f: @@ -370,7 +377,7 @@ class CortexService: except Exception as e: self.logger.error(f"Failed to write timeout event: {e}") - # Broadcast to callosum so wait_for_agents detects immediately + # Broadcast to callosum so wait_for_uses detects immediately try: event_copy = error_event.copy() event_type = event_copy.pop("event", "error") @@ -380,8 +387,8 @@ class CortexService: agent.stop() - def _monitor_stdout(self, agent: AgentProcess) -> None: - """Monitor agent stdout and append events to the JSONL file.""" + def _monitor_stdout(self, agent: TalentProcess) -> None: + """Monitor talent stdout and append events to the JSONL file.""" if not agent.process.stdout: return @@ -399,18 +406,18 @@ class CortexService: # Parse JSON event event = json.loads(line) - # Ensure event has timestamp and agent_id + # Ensure event has timestamp and use_id if "ts" not in event: event["ts"] = now_ms() - if "agent_id" not in event: - event["agent_id"] = agent.agent_id + if "use_id" not in event: + event["use_id"] = agent.use_id # Inject agent name for WebSocket consumers with self.lock: - _req = self.agent_requests.get(agent.agent_id) + _req = self.use_requests.get(agent.use_id) if _req and "name" not in event: event["name"] = _req.get("name", "") - # Inject display mode for triage agent finish events + # Inject display mode for triage talent finish events if event.get("event") == "finish" and _req: try: from apps.home.events import TRIAGE_AGENT_NAMES @@ -441,17 +448,15 @@ class CortexService: if event.get("event") == "start": # Capture model and provider for status reporting with self.lock: - if agent.agent_id in self.agent_requests: + if agent.use_id in self.use_requests: model = event.get("model") if model: - self.agent_requests[agent.agent_id]["model"] = ( - model - ) + self.use_requests[agent.use_id]["model"] = model provider = event.get("provider") if provider: - self.agent_requests[agent.agent_id][ - "provider" - ] = provider + self.use_requests[agent.use_id]["provider"] = ( + provider + ) # Handle finish or error event if event.get("event") in ["finish", "error"]: @@ -461,8 +466,8 @@ class CortexService: # Get original request (thread-safe access) with self.lock: - original_request = self.agent_requests.get( - agent.agent_id + original_request = self.use_requests.get( + agent.use_id ) # Log token usage if available @@ -493,13 +498,13 @@ class CortexService: ) except Exception as e: self.logger.warning( - f"Failed to log token usage for agent {agent.agent_id}: {e}" + f"Failed to log token usage for talent {agent.use_id}: {e}" ) # Write output if requested if original_request and original_request.get("output"): self._write_output( - agent.agent_id, + agent.use_id, result, original_request, ) @@ -513,19 +518,17 @@ class CortexService: "event": "info", "ts": now_ms(), "message": line, - "agent_id": agent.agent_id, + "use_id": agent.use_id, } with open(agent.log_path, "a") as f: f.write(json.dumps(info_event) + "\n") except Exception as e: - self.logger.error( - f"Error monitoring stdout for agent {agent.agent_id}: {e}" - ) + self.logger.error(f"Error monitoring stdout for agent {agent.use_id}: {e}") finally: # Wait for process to fully exit (reaps zombie) exit_code = agent.process.wait() - self.logger.info(f"Agent {agent.agent_id} exited with code {exit_code}") + self.logger.info(f"Talent {agent.use_id} exited with code {exit_code}") # Check if finish event was emitted has_finish = self._has_finish_event(agent.log_path) @@ -533,26 +536,26 @@ class CortexService: if not has_finish: # Write error event if no finish using standardized format error_event = self._create_error_event( - agent.agent_id, - f"Agent exited with code {exit_code} without finish event", + agent.use_id, + f"Talent exited with code {exit_code} without finish event", exit_code=exit_code, ) with open(agent.log_path, "a") as f: f.write(json.dumps(error_event) + "\n") # Complete the file (rename from _active.jsonl to .jsonl) - self._complete_agent_file(agent.agent_id, agent.log_path) + self._complete_use_file(agent.use_id, agent.log_path) # Remove from running agents and clean up stored request (thread-safe) with self.lock: - if agent.agent_id in self.running_agents: - del self.running_agents[agent.agent_id] + if agent.use_id in self.running_uses: + del self.running_uses[agent.use_id] # Clean up stored request - if agent.agent_id in self.agent_requests: - del self.agent_requests[agent.agent_id] + if agent.use_id in self.use_requests: + del self.use_requests[agent.use_id] - def _monitor_stderr(self, agent: AgentProcess) -> None: - """Monitor agent stderr for errors.""" + def _monitor_stderr(self, agent: TalentProcess) -> None: + """Monitor talent stderr for errors.""" if not agent.process.stderr: return @@ -565,24 +568,22 @@ class CortexService: stripped = line.strip() if stripped: stderr_lines.append(stripped) - # Pass through to cortex stderr with agent prefix for traceability + # Pass through to cortex stderr with talent prefix for traceability print( - f"[agent:{agent.agent_id}:stderr] {stripped}", + f"[talent:{agent.use_id}:stderr] {stripped}", file=sys.stderr, flush=True, ) except Exception as e: - self.logger.error( - f"Error monitoring stderr for agent {agent.agent_id}: {e}" - ) + self.logger.error(f"Error monitoring stderr for agent {agent.use_id}: {e}") finally: # If process failed with stderr output, write error event if stderr_lines: exit_code = agent.process.poll() if exit_code is not None and exit_code != 0: error_event = self._create_error_event( - agent.agent_id, + agent.use_id, "Process failed with stderr output", trace="\n".join(stderr_lines), exit_code=exit_code, @@ -608,45 +609,45 @@ class CortexService: pass return False - def _complete_agent_file(self, agent_id: str, file_path: Path) -> None: - """Complete an agent by renaming the file from _active.jsonl to .jsonl.""" + def _complete_use_file(self, use_id: str, file_path: Path) -> None: + """Complete a talent use by renaming the file from _active.jsonl to .jsonl.""" try: - completed_path = file_path.parent / f"{agent_id}.jsonl" + completed_path = file_path.parent / f"{use_id}.jsonl" file_path.rename(completed_path) - self.logger.info(f"Completed agent {agent_id}: {completed_path}") + self.logger.info(f"Completed talent use {use_id}: {completed_path}") - # Create convenience symlink: {name}.log -> {name}/{agent_id}.jsonl - request = self.agent_requests.get(agent_id) + # Create convenience symlink: {name}.log -> {name}/{use_id}.jsonl + request = self.use_requests.get(use_id) if request: name = request.get("name") if name: safe_name = name.replace(":", "--") - link_path = self.agents_dir / f"{safe_name}.log" - _atomic_symlink(link_path, f"{safe_name}/{agent_id}.jsonl") + link_path = self.talents_dir / f"{safe_name}.log" + _atomic_symlink(link_path, f"{safe_name}/{use_id}.jsonl") self.logger.debug( - f"Symlinked {safe_name}.log -> {safe_name}/{agent_id}.jsonl" + f"Symlinked {safe_name}.log -> {safe_name}/{use_id}.jsonl" ) # Append summary to day index - self._append_day_index(agent_id, request, completed_path) + self._append_day_index(use_id, request, completed_path) else: self.logger.debug( - f"No name in request for {agent_id}, skipping symlink" + f"No name in request for {use_id}, skipping symlink" ) except Exception as e: - self.logger.error(f"Failed to complete agent file {agent_id}: {e}") + self.logger.error(f"Failed to complete talent file {use_id}: {e}") def _append_day_index( - self, agent_id: str, request: Dict[str, Any], completed_path: Path + self, use_id: str, request: Dict[str, Any], completed_path: Path ) -> None: - """Append agent summary to day index file.""" + """Append talent-use summary to the day index file.""" try: - # Determine day from request or agent_id timestamp + # Determine day from request or use_id timestamp day = request.get("day") if not day: from datetime import datetime - ts_seconds = int(agent_id) / 1000 + ts_seconds = int(use_id) / 1000 day = datetime.fromtimestamp(ts_seconds).strftime("%Y%m%d") start_ts = request.get("ts", 0) @@ -681,7 +682,7 @@ class CortexService: pass summary = { - "agent_id": agent_id, + "use_id": use_id, "name": request.get("name", "unified"), "day": day, "facet": request.get("facet"), @@ -693,33 +694,33 @@ class CortexService: "schedule": request.get("schedule"), } - day_index_path = self.agents_dir / f"{day}.jsonl" + day_index_path = self.talents_dir / f"{day}.jsonl" with open(day_index_path, "a") as f: f.write(json.dumps(summary) + "\n") f.flush() except Exception as e: - self.logger.error(f"Failed to append day index for {agent_id}: {e}") + self.logger.error(f"Failed to append day index for {use_id}: {e}") def _write_error_and_complete(self, file_path: Path, error_message: str) -> None: """Write an error event to the file and mark it as complete.""" try: - agent_id = file_path.stem.replace("_active", "") - error_event = self._create_error_event(agent_id, error_message) + use_id = file_path.stem.replace("_active", "") + error_event = self._create_error_event(use_id, error_message) with open(file_path, "a") as f: f.write(json.dumps(error_event) + "\n") # Complete the file - self._complete_agent_file(agent_id, file_path) + self._complete_use_file(use_id, file_path) except Exception as e: self.logger.error(f"Failed to write error and complete: {e}") - def _write_output(self, agent_id: str, result: str, config: Dict[str, Any]) -> None: - """Write agent output to config["output_path"]. + def _write_output(self, use_id: str, result: str, config: Dict[str, Any]) -> None: + """Write talent output to config["output_path"]. The output path is set by the caller — either derived by - prepare_config in agents.py (day/segment agents) or computed - by dream.py via get_activity_output_path (activity agents). + prepare_config in think.talents (day/segment talents) or computed + by dream.py via get_activity_output_path (activity talents). Cortex does not derive paths itself. """ output_path_str = config.get("output_path") @@ -733,10 +734,10 @@ class CortexService: with open(output_path, "w", encoding="utf-8") as f: f.write(result) - self.logger.info(f"Wrote agent {agent_id} output to {output_path}") + self.logger.info(f"Wrote talent {use_id} output to {output_path}") except Exception as e: - self.logger.error(f"Failed to write agent {agent_id} output: {e}") + self.logger.error(f"Failed to write talent {use_id} output: {e}") def stop(self) -> None: """Stop the Cortex service.""" @@ -746,9 +747,9 @@ class CortexService: if self.callosum: self.callosum.stop() - # Stop all running agents + # Stop all running talent uses with self.lock: - for agent in self.running_agents.values(): + for agent in self.running_uses.values(): agent.stop() def _emit_periodic_status(self) -> None: @@ -756,12 +757,12 @@ class CortexService: while not self.stop_event.is_set(): try: with self.lock: - agents = [] - for agent_id, agent_proc in self.running_agents.items(): - config = self.agent_requests.get(agent_id, {}) - agents.append( + uses = [] + for use_id, agent_proc in self.running_uses.items(): + config = self.use_requests.get(use_id, {}) + uses.append( { - "agent_id": agent_id, + "use_id": use_id, "name": config.get("name", "unknown"), "provider": config.get("provider", "unknown"), "elapsed_seconds": int( @@ -770,13 +771,13 @@ class CortexService: } ) - # Only emit status when there are active agents - if agents: + # Only emit status when there are active talent uses + if uses: self.callosum.emit( "cortex", "status", - running_agents=len(agents), - agents=agents, + running_uses=len(uses), + uses=uses, ) except Exception as e: self.logger.debug(f"Status emission failed: {e}") @@ -787,8 +788,8 @@ class CortexService: """Get service status information.""" with self.lock: return { - "running_agents": len(self.running_agents), - "agent_ids": list(self.running_agents.keys()), + "running_uses": len(self.running_uses), + "use_ids": list(self.running_uses.keys()), } @@ -798,7 +799,7 @@ def main() -> None: from think.utils import require_solstone, setup_cli - parser = argparse.ArgumentParser(description="solstone Cortex Agent Manager") + parser = argparse.ArgumentParser(description="solstone Cortex Talent Manager") args = setup_cli(parser) require_solstone() diff --git a/think/cortex_client.py b/think/cortex_client.py index 26429c6c0..6d4047346 100644 --- a/think/cortex_client.py +++ b/think/cortex_client.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Cortex client for managing AI agent requests.""" +"""Cortex client for managing AI talent requests.""" import json import logging @@ -18,16 +18,16 @@ logger = logging.getLogger(__name__) _last_ts = 0 -def _find_agent_file(agents_dir: Path, agent_id: str) -> tuple[Path | None, str]: - """Find an agent log file in per-agent subdirectories. +def _find_use_file(talents_dir: Path, use_id: str) -> tuple[Path | None, str]: + """Find a use log file in per-talent subdirectories. Returns: Tuple of (file_path, status) where status is "completed", "running", or "not_found". """ - for match in agents_dir.glob(f"*/{agent_id}.jsonl"): + for match in talents_dir.glob(f"*/{use_id}.jsonl"): return match, "completed" - for match in agents_dir.glob(f"*/{agent_id}_active.jsonl"): + for match in talents_dir.glob(f"*/{use_id}_active.jsonl"): return match, "running" return None, "not_found" @@ -38,23 +38,23 @@ def cortex_request( provider: Optional[str] = None, config: Optional[Dict[str, Any]] = None, ) -> str | None: - """Create a Cortex agent request via Callosum broadcast. + """Create a Cortex talent request via Callosum broadcast. Args: - prompt: The task or question for the agent - name: Agent name - system (e.g., "unified") or app-qualified (e.g., "entities:entity_assist") + prompt: The task or question for the talent + name: Talent name - system (e.g., "unified") or app-qualified (e.g., "entities:entity_assist") provider: AI provider - openai, google, or anthropic config: Provider-specific configuration (model, max_output_tokens, thinking_budget, etc.) Returns: - Agent ID (timestamp-based string), or None if the Callosum send failed. + Use ID (timestamp-based string), or None if the Callosum send failed. """ - # Get journal path (for agent_id uniqueness check) + # Get journal path (for use_id uniqueness check) journal_path = get_journal() - # Create agents directory if it doesn't exist - agents_dir = Path(journal_path) / "agents" - agents_dir.mkdir(parents=True, exist_ok=True) + # Create talents directory if it doesn't exist + talents_dir = Path(journal_path) / "talents" + talents_dir.mkdir(parents=True, exist_ok=True) # Generate monotonic timestamp in milliseconds, ensuring uniqueness global _last_ts @@ -65,13 +65,13 @@ def cortex_request( ts = _last_ts + 1 _last_ts = ts - agent_id = str(ts) + use_id = str(ts) # Build request object request = { "event": "request", "ts": ts, - "agent_id": agent_id, + "use_id": use_id, "prompt": prompt, "provider": provider, "name": name, @@ -91,49 +91,49 @@ def cortex_request( sent = callosum_send("cortex", "request", **request_fields) if not sent: - logger.info("Failed to send cortex request for agent '%s'", name) + logger.info("Failed to send cortex request for talent '%s'", name) return None - return agent_id + return use_id -def get_agent_log_status(agent_id: str) -> str: - """Get the status of a specific agent from its log file. +def get_use_log_status(use_id: str) -> str: + """Get the status of a specific use from its log file. Args: - agent_id: The agent ID (timestamp) + use_id: The use ID (timestamp) Returns: - "completed" - Agent finished (*.jsonl exists) - "running" - Agent still active (*_active.jsonl exists) - "not_found" - No agent file exists + "completed" - Use finished (*.jsonl exists) + "running" - Use still active (*_active.jsonl exists) + "not_found" - No use file exists """ - agents_dir = Path(get_journal()) / "agents" - _, status = _find_agent_file(agents_dir, agent_id) + talents_dir = Path(get_journal()) / "talents" + _, status = _find_use_file(talents_dir, use_id) return status -def wait_for_agents( - agent_ids: list[str], +def wait_for_uses( + use_ids: list[str], timeout: int | None = 600, ) -> tuple[dict[str, str], list[str]]: - """Wait for agents to complete via Callosum events. + """Wait for uses to complete via Callosum events. Listens for cortex.finish and cortex.error events. Sets up the event - listener first, then does an initial file check for agents that may have + listener first, then does an initial file check for uses that may have already completed, and a final file check at timeout as a backstop for any missed events. Args: - agent_ids: List of agent IDs to wait for + use_ids: List of use IDs to wait for timeout: Maximum wait time in seconds (default 600 = 10 minutes) Returns: Tuple of (completed, timed_out) where completed is a dict mapping - agent_id to end state ("finish" or "error"), and timed_out is a - list of agent IDs that did not complete within the timeout. + use_id to end state ("finish" or "error"), and timed_out is a + list of use IDs that did not complete within the timeout. """ - pending = set(agent_ids) + pending = set(use_ids) completed: dict[str, str] = {} lock = threading.Lock() all_done = threading.Event() @@ -141,16 +141,16 @@ def wait_for_agents( def on_message(msg: dict) -> None: if msg.get("tract") != "cortex": return - agent_id = msg.get("agent_id") - if not agent_id: + use_id = msg.get("use_id") + if not use_id: return event_type = msg.get("event") if event_type in ("finish", "error"): with lock: - if agent_id in pending: - completed[agent_id] = event_type - pending.discard(agent_id) + if use_id in pending: + completed[use_id] = event_type + pending.discard(use_id) if not pending: all_done.set() @@ -161,11 +161,11 @@ def wait_for_agents( try: # Initial file check (with lock since callback may be running) with lock: - for agent_id in list(pending): - end_state = get_agent_end_state(agent_id) + for use_id in list(pending): + end_state = get_use_end_state(use_id) if end_state in ("finish", "error"): - completed[agent_id] = end_state - pending.discard(agent_id) + completed[use_id] = end_state + pending.discard(use_id) if not pending: return completed, [] @@ -178,41 +178,41 @@ def wait_for_agents( # Final file check for any remaining (backstop for missed events) # Listener is stopped, so no lock needed - for agent_id in list(pending): - end_state = get_agent_end_state(agent_id) + for use_id in list(pending): + end_state = get_use_end_state(use_id) if end_state in ("finish", "error"): logger.info( - f"Agent {agent_id} completion event not received but agent completed" + f"Talent use {use_id} completion event not received but use completed" ) - completed[agent_id] = end_state - pending.discard(agent_id) + completed[use_id] = end_state + pending.discard(use_id) return completed, list(pending) -def get_agent_end_state(agent_id: str) -> str: - """Get how a completed agent ended (finish or error). +def get_use_end_state(use_id: str) -> str: + """Get how a completed use ended (finish or error). Checks file contents for terminal events even if file is still _active.jsonl, since Callosum broadcasts happen before file rename. Args: - agent_id: The agent ID (timestamp) + use_id: The use ID (timestamp) Returns: - "finish" - Agent completed successfully - "error" - Agent ended with an error - "running" - Agent is still active (no terminal event in file) - "unknown" - Agent file not found + "finish" - Use completed successfully + "error" - Use ended with an error + "running" - Use is still active (no terminal event in file) + "unknown" - Use file not found """ - status = get_agent_log_status(agent_id) + status = get_use_log_status(use_id) if status == "not_found": return "unknown" # Read events to find terminal state (even for "running" files that may # have finish event - Callosum broadcast happens before file rename) try: - events = read_agent_events(agent_id) + events = read_use_events(use_id) # Find last finish or error event for event in reversed(events): event_type = event.get("event") @@ -226,25 +226,25 @@ def get_agent_end_state(agent_id: str) -> str: return "unknown" -def read_agent_events(agent_id: str) -> list[Dict[str, Any]]: - """Read all events from an agent's JSONL log file. +def read_use_events(use_id: str) -> list[Dict[str, Any]]: + """Read all events from a use's JSONL log file. Args: - agent_id: The agent ID (timestamp) + use_id: The use ID (timestamp) Returns: List of event dictionaries in chronological order Raises: - FileNotFoundError: If agent log doesn't exist + FileNotFoundError: If the use log doesn't exist """ - agents_dir = Path(get_journal()) / "agents" - agent_file, _status = _find_agent_file(agents_dir, agent_id) - if agent_file is None: - raise FileNotFoundError(f"Agent log not found: {agent_id}") + talents_dir = Path(get_journal()) / "talents" + use_file, _status = _find_use_file(talents_dir, use_id) + if use_file is None: + raise FileNotFoundError(f"Talent log not found: {use_id}") events = [] - with open(agent_file, "r") as f: + with open(use_file, "r") as f: for line in f: line = line.strip() if not line: @@ -253,38 +253,38 @@ def read_agent_events(agent_id: str) -> list[Dict[str, Any]]: event = json.loads(line) events.append(event) except json.JSONDecodeError: - logger.debug(f"Skipping malformed JSON in {agent_file}") + logger.debug(f"Skipping malformed JSON in {use_file}") continue return events -def cortex_agents( +def cortex_uses( limit: int = 10, offset: int = 0, - agent_type: str = "all", + use_type: str = "all", facet: Optional[str] = None, ) -> Dict[str, Any]: - """List agents from the journal with pagination and filtering. + """List talent uses from the journal with pagination and filtering. Args: - limit: Maximum number of agents to return (1-100) - offset: Number of agents to skip - agent_type: Filter by "live", "historical", or "all" - facet: Optional facet to filter by. If provided, only returns agents + limit: Maximum number of uses to return (1-100) + offset: Number of uses to skip + use_type: Filter by "live", "historical", or "all" + facet: Optional facet to filter by. If provided, only returns uses that were run in this facet context. None means no filtering. Returns: - Dictionary with agents list and pagination info + Dictionary with use list and pagination info """ # Validate parameters limit = max(1, min(limit, 100)) offset = max(0, offset) - agents_dir = Path(get_journal()) / "agents" - if not agents_dir.exists(): + talents_dir = Path(get_journal()) / "talents" + if not talents_dir.exists(): return { - "agents": [], + "uses": [], "pagination": { "limit": limit, "offset": offset, @@ -295,15 +295,15 @@ def cortex_agents( "historical_count": 0, } - # Collect all agent files - all_agents = [] + # Collect all use files + all_uses = [] live_count = 0 historical_count = 0 - for agent_file in agents_dir.glob("*/*.jsonl"): + for use_file in talents_dir.glob("*/*.jsonl"): # Determine status from filename - is_active = "_active.jsonl" in agent_file.name - is_pending = "_pending.jsonl" in agent_file.name + is_active = "_active.jsonl" in use_file.name + is_pending = "_pending.jsonl" in use_file.name # Skip pending files if is_pending: @@ -318,17 +318,17 @@ def cortex_agents( historical_count += 1 # Filter by requested type - if agent_type == "live" and status != "running": + if use_type == "live" and status != "running": continue - if agent_type == "historical" and status != "completed": + if use_type == "historical" and status != "completed": continue - # Extract agent ID from filename - agent_id = agent_file.stem.replace("_active", "") + # Extract use ID from filename + use_id = use_file.stem.replace("_active", "") - # Read agent file to get request info and calculate runtime + # Read use file to get request info and calculate runtime try: - with open(agent_file, "r") as f: + with open(use_file, "r") as f: lines = f.readlines() if not lines: continue @@ -343,24 +343,24 @@ def cortex_agents( continue # Extract facet from request - agent_facet = request.get("facet") + use_facet = request.get("facet") # Filter by facet if specified - if facet is not None and agent_facet != facet: + if facet is not None and use_facet != facet: continue # Extract basic info - agent_info = { - "id": agent_id, + use_info = { + "id": use_id, "name": request.get("name", "unified"), "start": request.get("ts", 0), "status": status, "prompt": request.get("prompt", ""), "provider": request.get("provider", "openai"), - "facet": agent_facet, + "facet": use_facet, } - # For completed agents, find finish event to calculate runtime + # For completed uses, find finish event to calculate runtime if status == "completed" and len(lines) > 1: # Read last few lines to find finish event (reading backwards is more efficient) for line in reversed(lines[-10:]): # Check last 10 lines @@ -371,29 +371,29 @@ def cortex_agents( event = json.loads(line) if event.get("event") == "finish": end_ts = event.get("ts", 0) - if end_ts and agent_info["start"]: + if end_ts and use_info["start"]: # Calculate runtime in seconds - agent_info["runtime_seconds"] = ( - end_ts - agent_info["start"] + use_info["runtime_seconds"] = ( + end_ts - use_info["start"] ) / 1000.0 break except json.JSONDecodeError: continue - all_agents.append(agent_info) + all_uses.append(use_info) except (json.JSONDecodeError, IOError): # Skip malformed files continue # Sort by start time (newest first) - all_agents.sort(key=lambda x: x["start"], reverse=True) + all_uses.sort(key=lambda x: x["start"], reverse=True) # Apply pagination - total = len(all_agents) - paginated = all_agents[offset : offset + limit] + total = len(all_uses) + paginated = all_uses[offset : offset + limit] return { - "agents": paginated, + "uses": paginated, "pagination": { "limit": limit, "offset": offset, diff --git a/think/dream.py b/think/dream.py index ee44198c4..42972d15d 100644 --- a/think/dream.py +++ b/think/dream.py @@ -28,7 +28,7 @@ from think.activities import ( from think.activity_state_machine import ActivityStateMachine from think.callosum import CallosumConnection from think.cluster import cluster_segments -from think.cortex_client import cortex_request, wait_for_agents +from think.cortex_client import cortex_request, wait_for_uses from think.facets import ( get_active_facets, get_enabled_facets, @@ -76,7 +76,7 @@ class DreamJSONLWriter: if not self.file: return data = {"event": event, "ts": now_ms(), **fields} - if event == "agent.skip": + if event == "talent.skip": self.skip_count += 1 try: self.file.write(json.dumps(data, ensure_ascii=False) + "\n") @@ -102,8 +102,8 @@ def _jsonl_log(event: str, **fields) -> None: def _log_skip(name: str, reason: str, detail: str, **extra) -> None: - """Emit an agent.skip JSONL event.""" - _jsonl_log("agent.skip", name=name, reason=reason, detail=detail, **extra) + """Emit an talent.skip JSONL event.""" + _jsonl_log("talent.skip", name=name, reason=reason, detail=detail, **extra) def _update_status(**fields) -> None: @@ -227,19 +227,19 @@ def _cortex_request_with_retry(**kwargs) -> str | None: """Call cortex_request with retries on Callosum send failure. Retries up to len(_SEND_RETRY_DELAYS) times with short sleeps in between. - Returns the agent_id on success, or None if all attempts failed. + Returns the use_id on success, or None if all attempts failed. """ - agent_id = cortex_request(**kwargs) - if agent_id is not None: - return agent_id + use_id = cortex_request(**kwargs) + if use_id is not None: + return use_id name = kwargs.get("name", "unknown") for i, delay in enumerate(_SEND_RETRY_DELAYS, 1): logging.warning("Retrying cortex request for '%s' (attempt %d)", name, i + 1) time.sleep(delay) - agent_id = cortex_request(**kwargs) - if agent_id is not None: - return agent_id + use_id = cortex_request(**kwargs) + if use_id is not None: + return use_id logging.error("All cortex request attempts failed for '%s'", name) return None @@ -259,7 +259,7 @@ def _drain_priority_batch( emits completion events, and runs incremental indexing for generators. Args: - spawned: List of (agent_id, prompt_name, config, facet) tuples + spawned: List of (use_id, prompt_name, config, facet) tuples target_schedule: "segment" or "daily" day: Day in YYYYMMDD format segment: Optional segment key @@ -273,10 +273,10 @@ def _drain_priority_batch( if not spawned: return (0, 0, []) - agent_ids = [agent_id for agent_id, _, _, _ in spawned] + agent_ids = [use_id for use_id, _, _, _ in spawned] logging.info(f"Waiting for {len(agent_ids)} agents...") - completed, timed_out = wait_for_agents(agent_ids, timeout=timeout) + completed, timed_out = wait_for_uses(agent_ids, timeout=timeout) success = 0 failed = 0 @@ -285,59 +285,59 @@ def _drain_priority_batch( if timed_out: logging.warning(f"{len(timed_out)} agents timed out: {timed_out}") failed += len(timed_out) - for agent_id in timed_out: + for use_id in timed_out: timed_name = next( - (n for aid, n, _, _ in spawned if aid == agent_id), "unknown" + (n for aid, n, _, _ in spawned if aid == use_id), "unknown" ) - timed_facet = next((f for aid, _, _, f in spawned if aid == agent_id), None) + timed_facet = next((f for aid, _, _, f in spawned if aid == use_id), None) label = f"{timed_name}/{timed_facet}" if timed_facet else timed_name failed_names.append(f"{label} (timeout)") emit( - "agent_completed", + "talent_completed", mode=target_schedule, day=day, segment=segment, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", **({"facet": timed_facet} if timed_facet else {}), ) _jsonl_log( - "agent.fail", + "talent.fail", mode=target_schedule, day=day, segment=segment, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", **({"facet": timed_facet} if timed_facet else {}), ) - for agent_id, prompt_name, config, agent_facet in spawned: - if agent_id in timed_out: + for use_id, prompt_name, config, agent_facet in spawned: + if use_id in timed_out: continue - end_state = completed.get(agent_id, "unknown") + end_state = completed.get(use_id, "unknown") if end_state == "finish": logging.info(f"{prompt_name} completed successfully") success += 1 emit( - "agent_completed", + "talent_completed", mode=target_schedule, day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state="finish", **({"facet": agent_facet} if agent_facet else {}), ) _jsonl_log( - "agent.complete", + "talent.complete", mode=target_schedule, day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state="finish", **({"facet": agent_facet} if agent_facet else {}), ) @@ -368,22 +368,22 @@ def _drain_priority_batch( failed += 1 failed_names.append(f"{label} ({end_state})") emit( - "agent_completed", + "talent_completed", mode=target_schedule, day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, **({"facet": agent_facet} if agent_facet else {}), ) _jsonl_log( - "agent.fail", + "talent.fail", mode=target_schedule, day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, **({"facet": agent_facet} if agent_facet else {}), ) @@ -568,20 +568,20 @@ def run_segment_sense( return (0, 1, ["sense (send)"]) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, segment=segment, name="sense", - agent_id=sense_agent_id, + use_id=sense_agent_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, segment=segment, name="sense", - agent_id=sense_agent_id, + use_id=sense_agent_id, ) _update_status(current_agents=["sense"]) @@ -822,8 +822,8 @@ def run_segment_sense( spawned: list[tuple[str, str, dict, str | None]] = [] for agent_name, config in agents_to_run: - agent_id = _dispatch_agent(agent_name, config) - if agent_id is None: + use_id = _dispatch_agent(agent_name, config) + if use_id is None: _log_skip( agent_name, "send_failed", @@ -837,22 +837,22 @@ def run_segment_sense( _update_status(agents_completed=total_success + total_failed) continue - spawned.append((agent_id, agent_name, config, None)) + spawned.append((use_id, agent_name, config, None)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, segment=segment, name=agent_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, segment=segment, name=agent_name, - agent_id=agent_id, + use_id=use_id, ) if max_concurrency and len(spawned) >= max_concurrency: @@ -971,20 +971,20 @@ def run_segment_sense( _update_status(agents_completed=total_success + total_failed) else: emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, segment=segment, name="awareness_tender", - agent_id=at_agent_id, + use_id=at_agent_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, segment=segment, name="awareness_tender", - agent_id=at_agent_id, + use_id=at_agent_id, ) _update_status(current_agents=["awareness_tender"]) s, f, fn = _drain_priority_batch( @@ -1019,20 +1019,20 @@ def run_segment_sense( _update_status(agents_completed=total_success + total_failed) else: emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, segment=segment, name="pulse", - agent_id=pulse_agent_id, + use_id=pulse_agent_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, segment=segment, name="pulse", - agent_id=pulse_agent_id, + use_id=pulse_agent_id, ) _update_status(current_agents=["pulse"]) s, f, fn = _drain_priority_batch( @@ -1187,7 +1187,7 @@ def run_daily_prompts( spawned: list[ tuple[str, str, dict, str | None] - ] = [] # (agent_id, name, config, facet) + ] = [] # (use_id, name, config, facet) group_success = 0 group_failed = 0 @@ -1254,12 +1254,12 @@ def run_daily_prompts( else f"Processing facet '{facet_name}' for {day_formatted}: {input_summary}. Use get_facet('{facet_name}') to load context." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -1273,25 +1273,25 @@ def run_daily_prompts( f"{prompt_name}/{facet_name} (send)" ) continue - spawned.append((agent_id, prompt_name, config, facet_name)) + spawned.append((use_id, prompt_name, config, facet_name)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, facet=facet_name, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, facet=facet_name, ) logging.info( - f"Started {prompt_name} for {facet_name} (ID: {agent_id})" + f"Started {prompt_name} for {facet_name} (ID: {use_id})" ) # Drain batch when concurrency limit reached @@ -1338,12 +1338,12 @@ def run_daily_prompts( else f"Running scheduled task for {day_formatted}: {input_summary}." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -1354,22 +1354,22 @@ def run_daily_prompts( group_failed += 1 all_failed_names.append(f"{prompt_name} (send)") continue - spawned.append((agent_id, prompt_name, config, None)) + spawned.append((use_id, prompt_name, config, None)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) - logging.info(f"Started {prompt_name} (ID: {agent_id})") + logging.info(f"Started {prompt_name} (ID: {use_id})") # Drain batch when concurrency limit reached if max_concurrency and len(spawned) >= max_concurrency: @@ -1543,7 +1543,7 @@ def run_weekly_prompts( spawned: list[ tuple[str, str, dict, str | None] - ] = [] # (agent_id, name, config, facet) + ] = [] # (use_id, name, config, facet) group_success = 0 group_failed = 0 @@ -1610,12 +1610,12 @@ def run_weekly_prompts( else f"Processing facet '{facet_name}' for {day_formatted}: {input_summary}. Use get_facet('{facet_name}') to load context." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -1629,25 +1629,25 @@ def run_weekly_prompts( f"{prompt_name}/{facet_name} (send)" ) continue - spawned.append((agent_id, prompt_name, config, facet_name)) + spawned.append((use_id, prompt_name, config, facet_name)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, facet=facet_name, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, facet=facet_name, ) logging.info( - f"Started {prompt_name} for {facet_name} (ID: {agent_id})" + f"Started {prompt_name} for {facet_name} (ID: {use_id})" ) # Drain batch when concurrency limit reached @@ -1694,12 +1694,12 @@ def run_weekly_prompts( else f"Running scheduled task for {day_formatted}: {input_summary}." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -1710,22 +1710,22 @@ def run_weekly_prompts( group_failed += 1 all_failed_names.append(f"{prompt_name} (send)") continue - spawned.append((agent_id, prompt_name, config, None)) + spawned.append((use_id, prompt_name, config, None)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) - logging.info(f"Started {prompt_name} (ID: {agent_id})") + logging.info(f"Started {prompt_name} (ID: {use_id})") # Drain batch when concurrency limit reached if max_concurrency and len(spawned) >= max_concurrency: @@ -1942,7 +1942,7 @@ def run_activity_prompts( count=len(prompts_list), ) - spawned: list[tuple[str, str, dict]] = [] # (agent_id, name, config) + spawned: list[tuple[str, str, dict]] = [] # (use_id, name, config) group_success = 0 group_failed = 0 @@ -1955,41 +1955,41 @@ def run_activity_prompts( agent_ids = [aid for aid, _, _ in spawned] logging.info(f"Waiting for {len(agent_ids)} agents...") - completed, timed_out = wait_for_agents(agent_ids, timeout=610) + completed, timed_out = wait_for_uses(agent_ids, timeout=610) if timed_out: logging.warning(f"{len(timed_out)} agents timed out") group_failed += len(timed_out) - for agent_id in timed_out: + for use_id in timed_out: timed_name = next( - (n for aid, n, _ in spawned if aid == agent_id), "unknown" + (n for aid, n, _ in spawned if aid == use_id), "unknown" ) emit( - "agent_completed", + "talent_completed", mode="activity", day=day, activity=activity_id, facet=facet, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", ) _jsonl_log( - "agent.fail", + "talent.fail", mode="activity", day=day, activity=activity_id, facet=facet, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", ) - for agent_id, prompt_name, config in spawned: - if agent_id in timed_out: + for use_id, prompt_name, config in spawned: + if use_id in timed_out: continue - end_state = completed.get(agent_id, "unknown") + end_state = completed.get(use_id, "unknown") if end_state == "finish": logging.info(f"{prompt_name} completed successfully") group_success += 1 @@ -2017,23 +2017,23 @@ def run_activity_prompts( group_failed += 1 emit( - "agent_completed", + "talent_completed", mode="activity", day=day, activity=activity_id, facet=facet, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, ) _jsonl_log( - "agent.complete" if end_state == "finish" else "agent.fail", + "talent.complete" if end_state == "finish" else "talent.fail", mode="activity", day=day, activity=activity_id, facet=facet, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, ) @@ -2078,12 +2078,12 @@ def run_activity_prompts( else f"Processing activity '{activity_id}' ({activity_type}) in facet '{facet}' for {day_formatted}." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -2095,26 +2095,26 @@ def run_activity_prompts( ) total_failed += 1 continue - spawned.append((agent_id, prompt_name, config)) + spawned.append((use_id, prompt_name, config)) emit( - "agent_started", + "talent_started", mode="activity", day=day, activity=activity_id, facet=facet, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode="activity", day=day, activity=activity_id, facet=facet, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) - logging.info(f"Started {prompt_name} (ID: {agent_id})") + logging.info(f"Started {prompt_name} (ID: {use_id})") # Drain batch when concurrency limit reached if max_concurrency and len(spawned) >= max_concurrency: @@ -2234,7 +2234,7 @@ def run_flush_prompts( total_success = 0 total_failed = 0 - spawned: list[tuple[str, str, dict]] = [] # (agent_id, name, config) + spawned: list[tuple[str, str, dict]] = [] # (use_id, name, config) _update_status( mode="flush", day=day, @@ -2268,12 +2268,12 @@ def run_flush_prompts( if is_generate: request_config["output"] = config.get("output", "md") - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt="", name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -2284,24 +2284,24 @@ def run_flush_prompts( ) total_failed += 1 continue - spawned.append((agent_id, prompt_name, config)) + spawned.append((use_id, prompt_name, config)) emit( - "agent_started", + "talent_started", mode="flush", day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode="flush", day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) - logging.info(f"Started flush agent {prompt_name} (ID: {agent_id})") + logging.info(f"Started flush agent {prompt_name} (ID: {use_id})") except Exception as e: logging.error(f"Failed to spawn flush agent {prompt_name}: {e}") @@ -2310,29 +2310,29 @@ def run_flush_prompts( if spawned: _update_status(current_agents=[name for _, name, _ in spawned]) agent_ids = [aid for aid, _, _ in spawned] - completed, timed_out = wait_for_agents(agent_ids, timeout=610) + completed, timed_out = wait_for_uses(agent_ids, timeout=610) if timed_out: logging.warning(f"Flush: {len(timed_out)} agents timed out") total_failed += len(timed_out) - for agent_id in timed_out: + for use_id in timed_out: timed_name = next( - (n for aid, n, _ in spawned if aid == agent_id), "unknown" + (n for aid, n, _ in spawned if aid == use_id), "unknown" ) _jsonl_log( - "agent.fail", + "talent.fail", mode="flush", day=day, segment=segment, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", ) - for agent_id, prompt_name, config in spawned: - if agent_id in timed_out: + for use_id, prompt_name, config in spawned: + if use_id in timed_out: continue - end_state = completed.get(agent_id, "unknown") + end_state = completed.get(use_id, "unknown") if end_state == "finish": logging.info(f"Flush agent {prompt_name} completed") total_success += 1 @@ -2343,21 +2343,21 @@ def run_flush_prompts( total_failed += 1 emit( - "agent_completed", + "talent_completed", mode="flush", day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, ) _jsonl_log( - "agent.complete" if end_state == "finish" else "agent.fail", + "talent.complete" if end_state == "finish" else "talent.fail", mode="flush", day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, ) _update_status( diff --git a/think/engage.py b/think/engage.py index 1121b3e2d..7f512262b 100644 --- a/think/engage.py +++ b/think/engage.py @@ -32,28 +32,28 @@ def _engage( from think.cortex_client import cortex_request - agent_id = cortex_request(prompt=prompt, name=name, config=config) - if agent_id is None: + use_id = cortex_request(prompt=prompt, name=name, config=config) + if use_id is None: typer.echo("Error: failed to send cortex request.", err=True) raise typer.Exit(1) if not wait: - typer.echo(agent_id) + typer.echo(use_id) return - from think.cortex_client import read_agent_events, wait_for_agents + from think.cortex_client import read_use_events, wait_for_uses - completed, timed_out = wait_for_agents([agent_id]) - if agent_id in timed_out: + completed, timed_out = wait_for_uses([use_id]) + if use_id in timed_out: typer.echo("Error: agent timed out.", err=True) raise typer.Exit(1) - end_state = completed.get(agent_id, "error") + end_state = completed.get(use_id, "error") if end_state != "finish": typer.echo(f"Error: agent ended with state: {end_state}", err=True) raise typer.Exit(1) - events = read_agent_events(agent_id) + events = read_use_events(use_id) result = "" for event in reversed(events): if event.get("event") == "finish": @@ -81,7 +81,7 @@ def engage( """Delegate work to a cogitate agent. Reads a prompt from stdin, sends it to cortex as an agent request. - By default, prints the agent_id and exits immediately (fire-and-forget). + By default, prints the use_id and exits immediately (fire-and-forget). Example:: diff --git a/think/entities/activity.py b/think/entities/activity.py index b17cf5f31..f8236165d 100644 --- a/think/entities/activity.py +++ b/think/entities/activity.py @@ -80,7 +80,7 @@ def parse_knowledge_graph_entities(day: str) -> list[str]: >>> parse_knowledge_graph_entities("20260108") ["Jeremie Miller (Jer)", "Neal Satterfield", "Flightline", ...] """ - kg_path = day_path(day, create=False) / "agents" / "knowledge_graph.md" + kg_path = day_path(day, create=False) / "talents" / "knowledge_graph.md" if not kg_path.exists(): return [] diff --git a/think/entities/context.py b/think/entities/context.py index f51e3a494..e36bee60b 100644 --- a/think/entities/context.py +++ b/think/entities/context.py @@ -38,7 +38,7 @@ def _active_entity_ids(facet: str, day: str, attached: list[dict]) -> set[str]: def _load_knowledge_graph(day: str) -> str: - kg_path = day_path(day, create=False) / "agents" / "knowledge_graph.md" + kg_path = day_path(day, create=False) / "talents" / "knowledge_graph.md" if not kg_path.exists(): return "No knowledge graph available for this day." diff --git a/think/events.py b/think/events.py index 76311b0d2..5839bbd68 100644 --- a/think/events.py +++ b/think/events.py @@ -129,7 +129,7 @@ def format_events( # For anticipations, show when it was created (from source path) if not occurred: source = event.get("source", "") - # Extract YYYYMMDD from source path like "20240101/agents/schedule.md" + # Extract YYYYMMDD from source path like "20240101/talents/schedule.md" source_match = re.match(r"(\d{8})/", source) if source_match: created_day = source_match.group(1) diff --git a/think/facets.py b/think/facets.py index 7698461be..7fc299c61 100644 --- a/think/facets.py +++ b/think/facets.py @@ -119,7 +119,7 @@ def _write_action_log( source: str, actor: str, day: str | None = None, - agent_id: str | None = None, + use_id: str | None = None, ) -> None: """Write action to the daily audit log. @@ -136,7 +136,7 @@ def _write_action_log( source: Origin type - "tool" for agents, "app" for web UI actor: For tools: agent name. For apps: app name day: Day in YYYYMMDD format (defaults to today) - agent_id: Optional agent ID (only for tool actions) + use_id: Optional agent ID (only for tool actions) """ journal = get_journal() @@ -165,9 +165,9 @@ def _write_action_log( if facet is not None: entry["facet"] = facet - # Add agent_id only if available - if agent_id is not None: - entry["agent_id"] = agent_id + # Add use_id only if available + if use_id is not None: + entry["use_id"] = use_id # Append to log file with open(log_path, "a", encoding="utf-8") as f: @@ -476,13 +476,13 @@ def load_segment_facets(day: str, segment: str, stream: str | None = None) -> li List of facet ID strings found in the segment's facets.json """ if stream: - candidates = [day_path(day) / stream / segment / "agents" / "facets.json"] + candidates = [day_path(day) / stream / segment / "talents" / "facets.json"] else: # Search all streams for this segment candidates = [] for _s, seg_key, seg_path in iter_segments(day): if seg_key == segment: - candidates.append(seg_path / "agents" / "facets.json") + candidates.append(seg_path / "talents" / "facets.json") for facets_file in candidates: if not facets_file.exists(): @@ -564,7 +564,7 @@ def aggregate_speculative_facets(days: list[str] | None = None) -> list[dict]: for day in scan_days: for _stream, _seg_key, seg_path in iter_segments(day): - facets_file = seg_path / "agents" / "facets.json" + facets_file = seg_path / "talents" / "facets.json" if not facets_file.exists(): continue @@ -1066,7 +1066,7 @@ def format_logs( source = entry.get("source", "unknown") actor = entry.get("actor", "unknown") params = entry.get("params", {}) - agent_id = entry.get("agent_id") + use_id = entry.get("use_id") # Format action name for display (e.g., "todo_add" -> "Todo Add") action_display = action.replace("_", " ").title() @@ -1081,8 +1081,8 @@ def format_logs( lines.append(" | ".join(meta_parts)) # Agent link if present - if agent_id: - lines.append(f"**Agent:** [{agent_id}](/app/sol/{agent_id})") + if use_id: + lines.append(f"**Talent:** [{use_id}](/app/sol/{use_id})") lines.append("") diff --git a/think/formatters.py b/think/formatters.py index 8d96c2cbe..6000357f7 100644 --- a/think/formatters.py +++ b/think/formatters.py @@ -51,7 +51,7 @@ def extract_path_metadata(rel_path: str) -> dict[str, str]: by the formatter via meta["indexer"]["agent"]. Args: - rel_path: Journal-relative path (e.g., "20240101/agents/flow.md") + rel_path: Journal-relative path (e.g., "20240101/talents/flow.md") Returns: Dict with keys: day, facet, agent @@ -72,11 +72,11 @@ def extract_path_metadata(rel_path: str) -> dict[str, str]: if parts[0] and DATE_RE.fullmatch(parts[0]): day = parts[0] - # Extract facet from agents/{facet}/... paths + # Extract facet from talents/{facet}/... paths try: - agents_idx = parts.index("agents") - if agents_idx + 2 < len(parts): - facet = parts[agents_idx + 1] + talents_idx = parts.index("talents") + if talents_idx + 2 < len(parts): + facet = parts[talents_idx + 1] except ValueError: pass @@ -191,15 +191,15 @@ FORMATTERS: dict[str, tuple[str, str, bool]] = { "*/*/*/*_transcript.jsonl": ("observe.hear", "format_audio", False), "*/*/*/screen.jsonl": ("observe.screen", "format_screen", False), "*/*/*/*_screen.jsonl": ("observe.screen", "format_screen", False), - # Markdown — day-level agents output and segment-level (day/stream/segment/agents/) - "*/agents/*.md": ("think.markdown", "format_markdown", True), - # Layout: day/stream/segment/agents/*.md - "*/*/*/agents/*.md": ("think.markdown", "format_markdown", True), - "*/*/*/agents/*/*.md": ("think.markdown", "format_markdown", True), + # Markdown — day-level agents output and segment-level (day/stream/segment/talents/) + "*/talents/*.md": ("think.markdown", "format_markdown", True), + # Layout: day/stream/segment/talents/*.md + "*/*/*/talents/*.md": ("think.markdown", "format_markdown", True), + "*/*/*/talents/*/*.md": ("think.markdown", "format_markdown", True), "facets/*/activities/*/*/*.md": ("think.markdown", "format_markdown", True), "facets/*/news/*.md": ("think.markdown", "format_markdown", True), "imports/*/summary.md": ("think.markdown", "format_markdown", True), - "apps/*/agents/*.md": ("think.markdown", "format_markdown", True), + "apps/*/talents/*.md": ("think.markdown", "format_markdown", True), } _DAY_ROOTED_PATTERNS = [p for p in FORMATTERS if p.startswith("*/")] @@ -212,7 +212,7 @@ def get_formatter(file_path: str) -> Callable | None: Matches against registered glob patterns (regardless of indexed flag). Args: - file_path: Journal-relative path (e.g., "20240101/agents/flow.md") + file_path: Journal-relative path (e.g., "20240101/talents/flow.md") Returns: Formatter function or None if no pattern matches diff --git a/think/heartbeat.py b/think/heartbeat.py index 2c11fa9e2..dff461271 100644 --- a/think/heartbeat.py +++ b/think/heartbeat.py @@ -14,7 +14,7 @@ from datetime import datetime from pathlib import Path from think.awareness import ensure_sol_directory -from think.cortex_client import cortex_request, wait_for_agents +from think.cortex_client import cortex_request, wait_for_uses from think.utils import get_journal, require_solstone, setup_cli logger = logging.getLogger(__name__) @@ -104,27 +104,27 @@ def main() -> None: pid_file.write_text(str(os.getpid())) start_time = time.monotonic() - agent_id = cortex_request( + use_id = cortex_request( prompt="Run heartbeat check.", name="heartbeat", ) - if agent_id is None: + if use_id is None: logger.error("Failed to send heartbeat request to cortex") _log_run(health_dir, start_time, "error") sys.exit(1) - logger.info("Heartbeat agent started (ID: %s)", agent_id) + logger.info("Heartbeat agent started (ID: %s)", use_id) # Wait for completion - completed, timed_out = wait_for_agents([agent_id], timeout=600) + completed, timed_out = wait_for_uses([use_id], timeout=600) # Determine outcome - if agent_id in timed_out: + if use_id in timed_out: logger.error("Heartbeat agent timed out") _log_run(health_dir, start_time, "timeout") sys.exit(2) - end_state = completed.get(agent_id, "unknown") + end_state = completed.get(use_id, "unknown") if end_state == "finish": logger.info("Heartbeat completed successfully") _log_run(health_dir, start_time, "success") diff --git a/think/hooks.py b/think/hooks.py index 2737ab3b2..e6cf94b2e 100644 --- a/think/hooks.py +++ b/think/hooks.py @@ -190,7 +190,7 @@ def compute_output_source(context: dict) -> str: context: Hook context dict with day, segment, name, output_path, meta. Returns: - Relative path like "20240101/agents/meetings.md". + Relative path like "20240101/talents/meetings.md". """ from think.talent import get_output_name from think.utils import CHRONICLE_DIR, get_journal @@ -206,14 +206,14 @@ def compute_output_source(context: dict) -> str: except ValueError: segment = context.get("segment") output_name = get_output_name(name) - # Check for facet in meta (for multi-facet agents) + # Check for facet in meta (for multi-facet talents) meta = context.get("meta", {}) facet = meta.get("facet") if meta else None filename = f"{output_name}.md" if segment and facet: - return os.path.join(day, segment, "agents", facet, filename) + return os.path.join(day, segment, "talents", facet, filename) if segment: - return os.path.join(day, segment, "agents", filename) + return os.path.join(day, segment, "talents", filename) if facet: - return os.path.join(day, "agents", facet, filename) - return os.path.join(day, "agents", filename) + return os.path.join(day, "talents", facet, filename) + return os.path.join(day, "talents", filename) diff --git a/think/indexer/journal.py b/think/indexer/journal.py index bcc595494..cdaeeefc6 100644 --- a/think/indexer/journal.py +++ b/think/indexer/journal.py @@ -258,7 +258,7 @@ def _find_signal_files(journal: str) -> dict[str, tuple[str, str]]: else journal_path ) - for path in day_root.glob("*/agents/knowledge_graph.md"): + for path in day_root.glob("*/talents/knowledge_graph.md"): if path.is_file(): rel = path.relative_to(day_root).as_posix() files[rel] = (str(path), "kg") @@ -972,7 +972,7 @@ def _extract_stream(journal: str, rel: str) -> str | None: """Extract stream name from a journal-relative path's segment directory. Reads stream.json from the segment dir if the path is inside a segment - (e.g., "20240101/142500_300/agents/facet/flow.md"). + (e.g., "20240101/142500_300/talents/facet/flow.md"). Returns stream name string or None for non-segment paths or pre-stream segments. """ @@ -1052,18 +1052,18 @@ def _index_segment_chunks( ) -> int: """Index concatenated markdown content for one segment.""" segment_path = Path(segment_dir) - agent_files = sorted( + talent_files = sorted( [ - *segment_path.glob("agents/*.md"), - *segment_path.glob("agents/*/*.md"), + *segment_path.glob("talents/*.md"), + *segment_path.glob("talents/*/*.md"), ], key=lambda path: str(path), ) - if not agent_files: + if not talent_files: return 0 content = "\n\n---\n\n".join( - path.read_text(encoding="utf-8") for path in agent_files + path.read_text(encoding="utf-8") for path in talent_files ) chunks, _meta = format_markdown(content) day = rel_segment.replace("\\", "/").split("/")[0] @@ -1324,7 +1324,7 @@ def scan_signals( for path in db_signal_paths if not _is_historical_signal_file( path.split("#")[0], - "kg" if "/agents/knowledge_graph.md" in path else "event", + "kg" if "/talents/knowledge_graph.md" in path else "event", ) } removed = {p for p in in_scope_db if p.split("#")[0] not in in_scope} @@ -1506,7 +1506,7 @@ def consolidate_segment_entities(journal: str, full: bool = False) -> int: # Collect all matching segment entity files across day/stream/segment dirs segment_files = [] - for path in day_root.glob("**/agents/entities.jsonl"): + for path in day_root.glob("**/talents/entities.jsonl"): if not path.is_file(): continue try: diff --git a/think/journal_stats.py b/think/journal_stats.py index a16dc0dd2..ee670c172 100644 --- a/think/journal_stats.py +++ b/think/journal_stats.py @@ -12,9 +12,9 @@ from typing import Dict from observe.sense import scan_day as sense_scan_day from observe.utils import VIDEO_EXTENSIONS, load_analysis_frames -from think.agents import scan_day as generate_scan_day from think.stats_schema import DAY_FIELDS, SCHEMA_VERSION from think.stats_schema import validate as validate_stats +from think.talents import scan_day as generate_scan_day from think.utils import day_dirs, get_journal, setup_cli logger = logging.getLogger(__name__) @@ -54,12 +54,12 @@ class JournalStats: for ext in VIDEO_EXTENSIONS: files.extend(day_dir.glob(f"*{ext}")) - agents_dir = day_dir / "agents" - if agents_dir.is_dir(): - files.extend(agents_dir.glob("*.json")) - files.extend(agents_dir.glob("*.md")) - files.extend(agents_dir.glob("*/*.json")) - files.extend(agents_dir.glob("*/*.md")) + talents_dir = day_dir / "talents" + if talents_dir.is_dir(): + files.extend(talents_dir.glob("*.json")) + files.extend(talents_dir.glob("*.md")) + files.extend(talents_dir.glob("*/*.json")) + files.extend(talents_dir.glob("*/*.md")) # Check facet event files for this day journal_root = Path(get_journal()) @@ -524,7 +524,7 @@ class JournalStats: "by_day": self.token_usage, "by_model": self.token_totals, }, - "agents": { + "talents": { "counts": dict(self.agent_counts), "minutes": {k: round(v, 2) for k, v in self.agent_minutes.items()}, "counts_by_day": self.agent_counts_by_day, diff --git a/think/models.py b/think/models.py index b5cc38052..e770e511e 100644 --- a/think/models.py +++ b/think/models.py @@ -1082,7 +1082,7 @@ def request_health_recheck() -> None: """ try: subprocess.Popen( - ["sol", "agents", "check", "--targeted"], + ["sol", "providers", "check", "--targeted"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) diff --git a/think/pipeline_health.py b/think/pipeline_health.py index 64dea989c..31473f5f8 100644 --- a/think/pipeline_health.py +++ b/think/pipeline_health.py @@ -28,7 +28,7 @@ def summarize_pipeline_day(day: str) -> dict: "status": "healthy", "anomalies": [], "runs": {mode: {"count": 0, "duration_ms_total": 0} for mode in _MODES}, - "agents": { + "talents": { "dispatched": 0, "completed": 0, "failed": 0, @@ -39,7 +39,7 @@ def summarize_pipeline_day(day: str) -> dict: "activities": { "detected": 0, "persisted": 0, - "agents_fired": False, + "talents_fired": False, }, } @@ -78,25 +78,26 @@ def summarize_pipeline_day(day: str) -> dict: continue event = rec["event"] - if event == "agent.dispatch": - summary["agents"]["dispatched"] += 1 - elif event == "agent.complete": - summary["agents"]["completed"] += 1 - elif event == "agent.fail": - summary["agents"]["failed"] += 1 - if len(summary["agents"]["failed_list"]) < _FAILED_LIST_CAP: - summary["agents"]["failed_list"].append( + # HISTORICAL SHIM: accept legacy agent.* chronicle event names from before 2026-04-17; sunset 2026-05-01 + if event in {"agent.dispatch", "talent.dispatch"}: + summary["talents"]["dispatched"] += 1 + elif event in {"agent.complete", "talent.complete"}: + summary["talents"]["completed"] += 1 + elif event in {"agent.fail", "talent.fail"}: + summary["talents"]["failed"] += 1 + if len(summary["talents"]["failed_list"]) < _FAILED_LIST_CAP: + summary["talents"]["failed_list"].append( { "mode": rec.get("mode") or mode, "name": rec.get("name"), - "agent_id": rec.get("agent_id"), + "use_id": rec.get("use_id"), "state": rec.get("state"), } ) else: - summary["agents"]["failed_list_truncated"] = True - elif event == "agent.skip": - summary["agents"]["skipped"] += 1 + summary["talents"]["failed_list_truncated"] = True + elif event in {"agent.skip", "talent.skip"}: + summary["talents"]["skipped"] += 1 elif event == "activity.detected": summary["activities"]["detected"] += 1 elif event == "activity.persisted": @@ -110,7 +111,7 @@ def summarize_pipeline_day(day: str) -> dict: elif ( event == "run.start" and (rec.get("mode") or mode) == "activity" ): - summary["activities"]["agents_fired"] = True + summary["activities"]["talents_fired"] = True except Exception: logger.warning( "pipeline_health: unexpected error summarizing %s", @@ -119,8 +120,8 @@ def summarize_pipeline_day(day: str) -> dict: ) return summary - for failure in summary["agents"]["failed_list"]: - summary["anomalies"].append({"kind": "agent_failure", **failure}) + for failure in summary["talents"]["failed_list"]: + summary["anomalies"].append({"kind": "talent_failure", **failure}) if ( summary["activities"]["detected"] > 0 @@ -149,7 +150,7 @@ def summarize_pipeline_day(day: str) -> dict: for anomaly in summary["anomalies"] ) has_failure = any( - anomaly["kind"] == "agent_failure" for anomaly in summary["anomalies"] + anomaly["kind"] == "talent_failure" for anomaly in summary["anomalies"] ) if has_stale: summary["status"] = "stale" @@ -175,11 +176,11 @@ def pipeline_status_message(summary: dict) -> dict | None: "status": "stale", "message": "Daily processing hasn't run yet", } - if any(anomaly.get("kind") == "agent_failure" for anomaly in anomalies): - count = summary.get("agents", {}).get("failed", 0) + if any(anomaly.get("kind") == "talent_failure" for anomaly in anomalies): + count = summary.get("talents", {}).get("failed", 0) plural = "s" if count != 1 else "" return { "status": "warning", - "message": f"{count} agent error{plural} today", + "message": f"{count} talent error{plural} today", } return None diff --git a/think/providers/anthropic.py b/think/providers/anthropic.py index 0ae37fcb5..25f7f4bfe 100644 --- a/think/providers/anthropic.py +++ b/think/providers/anthropic.py @@ -4,7 +4,7 @@ """Anthropic Claude provider for agents and direct LLM generation. -This module provides the Anthropic Claude provider for the ``sol agents`` CLI +This module provides the Anthropic Claude provider for the ``sol providers check`` CLI and run_generate/run_agenerate functions returning GenerateResult. Common Parameters diff --git a/think/providers/google.py b/think/providers/google.py index fa43766f7..5e30ffcf1 100644 --- a/think/providers/google.py +++ b/think/providers/google.py @@ -4,7 +4,7 @@ """Gemini provider for agents and direct LLM generation. -This module provides the Google Gemini provider for the ``sol agents`` CLI +This module provides the Google Gemini provider for the ``sol providers check`` CLI and run_generate/run_agenerate functions returning GenerateResult. Common Parameters diff --git a/think/providers/openai.py b/think/providers/openai.py index 725db492e..32e029cd8 100644 --- a/think/providers/openai.py +++ b/think/providers/openai.py @@ -4,7 +4,7 @@ """OpenAI provider for agents and direct LLM generation. -This module provides the OpenAI provider for the ``sol agents`` CLI +This module provides the OpenAI provider for the ``sol providers check`` CLI and run_generate/run_agenerate functions returning GenerateResult. Common Parameters @@ -54,7 +54,7 @@ from .shared import ( safe_raw, ) -# Agent configuration is now loaded via get_agent() in cortex.py +# Agent configuration is now loaded via get_talent() in cortex.py LOG = logging.getLogger("think.providers.openai") diff --git a/think/providers/shared.py b/think/providers/shared.py index 46a7e75e0..a50676a9e 100644 --- a/think/providers/shared.py +++ b/think/providers/shared.py @@ -4,7 +4,7 @@ """Shared utilities and types for AI providers. This module contains: -- Event TypedDicts emitted by providers during agent execution +- Event TypedDicts emitted by providers during talent execution - GenerateResult TypedDict returned by run_generate/run_agenerate - JSONEventCallback for event emission - Utility functions for common provider operations @@ -48,7 +48,7 @@ class ToolEndEvent(TypedDict, total=False): class StartEvent(TypedDict, total=False): - """Event emitted when an agent run begins.""" + """Event emitted when a talent run begins.""" event: Required[Literal["start"]] ts: Required[int] @@ -62,7 +62,7 @@ class StartEvent(TypedDict, total=False): class FinishEvent(TypedDict, total=False): - """Event emitted when an agent run finishes successfully.""" + """Event emitted when a talent run finishes successfully.""" event: Required[Literal["finish"]] ts: Required[int] @@ -82,12 +82,12 @@ class ErrorEvent(TypedDict, total=False): raw: Optional[list[dict[str, Any]]] # Original provider JSON event(s) -class AgentUpdatedEvent(TypedDict, total=False): - """Event emitted when the agent context changes.""" +class TalentUpdatedEvent(TypedDict, total=False): + """Event emitted when the talent context changes.""" - event: Required[Literal["agent_updated"]] + event: Required[Literal["talent_updated"]] ts: Required[int] - agent: Required[str] + talent: Required[str] raw: Optional[list[dict[str, Any]]] # Original provider JSON event(s) @@ -127,7 +127,7 @@ Event = Union[ FinishEvent, ErrorEvent, ThinkingEvent, - AgentUpdatedEvent, + TalentUpdatedEvent, FallbackEvent, ] diff --git a/think/providers_cli.py b/think/providers_cli.py new file mode 100644 index 000000000..0fc5eb267 --- /dev/null +++ b/think/providers_cli.py @@ -0,0 +1,337 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""CLI entrypoint for provider connectivity checks.""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import shutil +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +from think.utils import get_journal, require_solstone, setup_cli + + +def _check_generate(provider_name: str, tier: int, timeout: int) -> tuple[str, str]: + """Check generate interface for a provider.""" + from think.models import PROVIDER_DEFAULTS + from think.providers import PROVIDER_METADATA, get_provider_module + + env_key = PROVIDER_METADATA[provider_name]["env_key"] + if env_key and not os.getenv(env_key): + label = PROVIDER_METADATA[provider_name]["label"] + return "skip", f"{label} not configured (no {env_key})" + + if not env_key: + from think.providers import validate_key + + result = validate_key(provider_name, "") + if not result.get("valid"): + return ( + "skip", + f"Ollama not reachable ({result.get('error', 'unreachable')})", + ) + + try: + module = get_provider_module(provider_name) + model = PROVIDER_DEFAULTS[provider_name][tier] + result = module.run_generate( + contents="Say OK", + model=model, + temperature=0, + max_output_tokens=16, + system_instruction=None, + json_output=False, + thinking_budget=None, + timeout_s=timeout, + ) + text = result.get("text", "") if isinstance(result, dict) else "" + if text: + usage = result.get("usage") if isinstance(result, dict) else None + if usage: + from think.models import log_token_usage + + log_token_usage( + model=PROVIDER_DEFAULTS[provider_name][tier], + usage=usage, + context="health.check.generate", + type="generate", + ) + return "ok", "OK" + return "fail", "FAIL: empty response text" + except Exception as exc: + return "fail", f"FAIL: {exc}" + + +async def _check_cogitate( + provider_name: str, tier: int, timeout: int +) -> tuple[str, str]: + """Check cogitate interface for a provider by running a real prompt.""" + from think.models import PROVIDER_DEFAULTS + from think.providers import PROVIDER_METADATA, get_provider_module + + env_key = PROVIDER_METADATA[provider_name]["env_key"] + if env_key and not os.getenv(env_key): + label = PROVIDER_METADATA[provider_name]["label"] + return "skip", f"{label} not configured (no {env_key})" + + if not env_key: + from think.providers import validate_key + + result = validate_key(provider_name, "") + if not result.get("valid"): + return ( + "skip", + f"Ollama not reachable ({result.get('error', 'unreachable')})", + ) + + binary = PROVIDER_METADATA[provider_name].get("cogitate_cli", "") + if binary and not shutil.which(binary): + return "skip", f"{binary} CLI not installed" + + try: + module = get_provider_module(provider_name) + model = PROVIDER_DEFAULTS[provider_name][tier] + config = {"prompt": "Say OK", "model": model} + result = await asyncio.wait_for( + module.run_cogitate(config=config, on_event=None), + timeout=timeout, + ) + if result: + return "ok", "OK" + return "fail", "FAIL: empty response" + except asyncio.TimeoutError: + return "fail", f"FAIL: timed out after {timeout}s" + except Exception as exc: + return "fail", f"FAIL: {exc}" + + +async def _run_check(args: argparse.Namespace) -> None: + """Run connectivity checks against AI providers.""" + from think.models import PROVIDER_DEFAULTS, TIER_FLASH, TIER_LITE, TIER_PRO + from think.providers import PROVIDER_REGISTRY + + targeted_pairs = None + if args.targeted and not args.provider and not args.tier: + import fcntl + + from think.models import TYPE_DEFAULTS, get_backup_provider + from think.utils import get_config + + targeted_pairs = set() + config = get_config() + providers_config = config.get("providers", {}) + for talent_type, defaults in TYPE_DEFAULTS.items(): + type_config = providers_config.get(talent_type, {}) + provider = type_config.get("provider", defaults["provider"]) + tier = type_config.get("tier", defaults["tier"]) + targeted_pairs.add((provider, tier)) + backup = get_backup_provider(talent_type) + if backup: + targeted_pairs.add((backup, tier)) + + lock_dir = Path(get_journal()) / "health" + lock_dir.mkdir(parents=True, exist_ok=True) + lock_fd = open(lock_dir / "recheck.lock", "w") + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + lock_fd.close() + return + + if args.provider: + providers = args.provider + for name in providers: + if name not in PROVIDER_REGISTRY: + available = ", ".join(PROVIDER_REGISTRY.keys()) + print( + f"Unknown provider: {name}. Available providers: {available}", + file=sys.stderr, + ) + sys.exit(1) + else: + providers = list(PROVIDER_REGISTRY.keys()) + + interfaces = [args.interface] if args.interface else ["generate", "cogitate"] + tier_names = {1: "pro", 2: "flash", 3: "lite"} + tiers = [args.tier] if args.tier else [TIER_PRO, TIER_FLASH, TIER_LITE] + + provider_width = max(len(n) for n in providers) if providers else 0 + tier_width = max(len(tier_names[t]) for t in tiers) + model_names = {PROVIDER_DEFAULTS[p][t] for p in providers for t in tiers} + model_width = max(len(m) for m in model_names) if model_names else 0 + interface_width = max(len(n) for n in interfaces) if interfaces else 0 + + total = 0 + passed = 0 + failed = 0 + skipped = 0 + results: list[dict[str, object]] = [] + cache: dict[tuple[str, str, str], tuple[str, str, str]] = {} + + for provider_name in providers: + for tier in tiers: + if ( + targeted_pairs is not None + and (provider_name, tier) not in targeted_pairs + ): + continue + model = PROVIDER_DEFAULTS[provider_name][tier] + for interface_name in interfaces: + cache_key = (provider_name, model, interface_name) + if cache_key in cache: + status, message, source_tier = cache[cache_key] + elapsed_s = 0.0 + elapsed_s_rounded = 0.0 + reused_from = source_tier + else: + start = time.perf_counter() + if interface_name == "generate": + status, message = _check_generate( + provider_name, tier, args.timeout + ) + else: + status, message = await _check_cogitate( + provider_name, tier, args.timeout + ) + elapsed_s = time.perf_counter() - start + elapsed_s_rounded = round(elapsed_s, 1) + cache[cache_key] = (status, message, tier_names[tier]) + reused_from = None + + result: dict[str, object] = { + "provider": provider_name, + "tier": tier_names[tier], + "model": model, + "interface": interface_name, + "ok": status != "fail", + "status": status, + "message": str(message), + "elapsed_s": elapsed_s_rounded, + } + if reused_from: + result["reused_from"] = reused_from + results.append(result) + + if not args.json: + if reused_from: + mark = "=" + display_message = f"{message} (={reused_from})" + else: + if status == "ok": + mark = "✓" + elif status == "skip": + mark = "-" + else: + mark = "✗" + display_message = str(message) + print( + f"{mark} " + f"{provider_name:<{provider_width}} " + f"{tier_names[tier]:<{tier_width}} " + f"{model:<{model_width}} " + f"{interface_name:<{interface_width}} " + f"{display_message} ({elapsed_s:.1f}s)" + ) + + total += 1 + if status == "ok": + passed += 1 + elif status == "skip": + skipped += 1 + else: + failed += 1 + + any_failed = any(r["status"] == "fail" for r in results) + + payload = { + "results": results, + "summary": { + "total": total, + "passed": passed, + "skipped": skipped, + "failed": failed, + }, + "checked_at": datetime.now(timezone.utc).isoformat(), + } + health_dir = Path(get_journal()) / "health" + health_dir.mkdir(parents=True, exist_ok=True) + (health_dir / "talents.json").write_text(json.dumps(payload, indent=2)) + + if args.json: + print( + json.dumps( + { + "results": results, + "summary": { + "total": total, + "passed": passed, + "skipped": skipped, + "failed": failed, + }, + }, + indent=2, + ) + ) + else: + print(f"{total} checks: {passed} passed, {skipped} skipped, {failed} failed") + sys.exit(1 if any_failed else 0) + + +async def main_async() -> None: + """CLI entrypoint for provider connectivity checks.""" + from think.providers import PROVIDER_REGISTRY + + parser = argparse.ArgumentParser(description="solstone Provider CLI") + subparsers = parser.add_subparsers(dest="subcommand") + check_parser = subparsers.add_parser("check", help="Check AI provider connectivity") + check_parser.add_argument( + "--provider", + action="append", + help=f"Provider to check (repeatable). Available: {', '.join(PROVIDER_REGISTRY.keys())}", + ) + check_parser.add_argument( + "--interface", + choices=["generate", "cogitate"], + default=None, + help="Interface to check (default: both)", + ) + check_parser.add_argument( + "--timeout", + type=int, + default=30, + help="Timeout in seconds for generate checks (default: 30)", + ) + check_parser.add_argument( + "--tier", + type=int, + choices=[1, 2, 3], + default=None, + help="Tier to check (1=pro, 2=flash, 3=lite; default: all)", + ) + check_parser.add_argument( + "--json", action="store_true", help="Output results as JSON" + ) + check_parser.add_argument( + "--targeted", + action="store_true", + help="Only check configured provider+tier pairs (used by automated rechecks)", + ) + + args = setup_cli(parser) + require_solstone() + if args.subcommand != "check": + parser.print_help() + sys.exit(1) + await _run_check(args) + + +def main() -> None: + """Entry point wrapper.""" + asyncio.run(main_async()) diff --git a/think/retention.py b/think/retention.py index e1317f219..a806f4150 100644 --- a/think/retention.py +++ b/think/retention.py @@ -74,7 +74,7 @@ def is_segment_complete(segment_path: Path) -> bool: 3. screen.jsonl exists if any video raw media was captured 4. agents/speaker_labels.json exists if embeddings (.npz) are present """ - agents_dir = segment_path / "agents" + agents_dir = segment_path / "talents" # Check 1: no active agent files if agents_dir.is_dir(): @@ -126,7 +126,7 @@ def _get_completion_files(segment_path: Path) -> list[Path]: if path.is_file() ) - speaker_labels = segment_path / "agents" / "speaker_labels.json" + speaker_labels = segment_path / "talents" / "speaker_labels.json" if speaker_labels.exists(): completion_files.append(speaker_labels) diff --git a/think/routines.py b/think/routines.py index a2c93a16a..bcdf4ca0a 100644 --- a/think/routines.py +++ b/think/routines.py @@ -24,7 +24,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from apps.calendar.event import EventDay from think.callosum import callosum_send -from think.cortex_client import cortex_request, wait_for_agents +from think.cortex_client import cortex_request, wait_for_uses from think.facets import get_facets from think.utils import get_journal @@ -302,13 +302,13 @@ def _run_routine(routine: dict, event_context: dict | None = None) -> None: ) callosum_send("routines", "started", routine_id=routine_id, name=name) - agent_id = cortex_request( + use_id = cortex_request( prompt=prompt, name="routine", config={"output_path": str(output_path), "output": "md"}, ) - if agent_id is None: + if use_id is None: duration = int(time.monotonic() - start_time) logger.error("Failed to start routine %s", routine_id) _log_health(routine_id, name, duration, "error") @@ -323,11 +323,11 @@ def _run_routine(routine: dict, event_context: dict | None = None) -> None: ) return - completed, timed_out = wait_for_agents([agent_id], timeout=600) - if agent_id in timed_out: + completed, timed_out = wait_for_uses([use_id], timeout=600) + if use_id in timed_out: outcome = "timeout" else: - end_state = completed.get(agent_id, "error") + end_state = completed.get(use_id, "error") outcome = "success" if end_state == "finish" else "error" duration = int(time.monotonic() - start_time) diff --git a/think/segment.py b/think/segment.py index 355352a04..7539ae3e3 100644 --- a/think/segment.py +++ b/think/segment.py @@ -58,17 +58,17 @@ def _format_size(size_bytes: int) -> str: def _segment_stats(seg_path: Path) -> dict[str, int]: - """Return recursive file, agent, and byte counts for a segment.""" + """Return recursive file, talent, and byte counts for a segment.""" files = 0 - agents = 0 + talents = 0 size = 0 for path in seg_path.rglob("*"): if path.is_file(): files += 1 size += path.stat().st_size - if "agents" in path.parts: - agents += 1 - return {"files": files, "agents": agents, "size": size} + if "talents" in path.parts: + talents += 1 + return {"files": files, "talents": talents, "size": size} def _split_segment_path(path: str) -> tuple[str, str, str]: @@ -161,11 +161,11 @@ def _segment_files(seg_dir: Path) -> list[str]: def _agent_files(seg_dir: Path) -> list[str]: - """Return top-level file names from agents/ if present.""" - agents_dir = seg_dir / "agents" - if not agents_dir.is_dir(): + """Return top-level file names from talents/ if present.""" + talents_dir = seg_dir / "talents" + if not talents_dir.is_dir(): return [] - return sorted(path.name for path in agents_dir.iterdir() if path.is_file()) + return sorted(path.name for path in talents_dir.iterdir() if path.is_file()) def _events_summary(seg_dir: Path) -> dict[str, object]: @@ -609,7 +609,7 @@ def cmd_move(args: argparse.Namespace) -> None: _touch_health_marker(to_day) print(f" touched health markers: {src_day}, {to_day}") if verbose: - print(" dream will re-run daily agents on both days") + print(" dream will re-run daily talents on both days") # Post-move verify is informational — the move already completed. print() @@ -641,7 +641,7 @@ def cmd_list(args: argparse.Namespace) -> None: "end": end, "duration": _segment_duration(seg_key), "files": stats["files"], - "agents": stats["agents"], + "talents": stats["talents"], "size": stats["size"], } ) @@ -652,9 +652,9 @@ def cmd_list(args: argparse.Namespace) -> None: print( f"{'STREAM':<20} {'SEGMENT':<14} {'TIME':<15} " - f"{'DUR':>5} {'FILES':>5} {'AGENTS':>6} {'SIZE':>8}" + f"{'DUR':>5} {'FILES':>5} {'TALENTS':>7} {'SIZE':>8}" ) - print("-" * 77) + print("-" * 78) for row in rows: time_str = ( f"{row['start']}-{row['end']}" @@ -664,7 +664,7 @@ def cmd_list(args: argparse.Namespace) -> None: dur_str = f"{row['duration']}s" print( f"{row['stream']:<20} {row['segment']:<14} {time_str:<15} " - f"{dur_str:>5} {row['files']:>5} {row['agents']:>6} " + f"{dur_str:>5} {row['files']:>5} {row['talents']:>7} " f"{_format_size(int(row['size'])):>8}" ) @@ -684,7 +684,7 @@ def cmd_inspect(args: argparse.Namespace) -> None: prev_desc = _describe_prev(day, stream_name, marker) next_desc = _describe_next(day, stream_name, segment) files = _segment_files(seg_dir) - agents = _agent_files(seg_dir) + talents = _agent_files(seg_dir) stats = _segment_stats(seg_dir) events = _events_summary(seg_dir) index_info = _segment_index_info(day, stream_name, segment) @@ -701,7 +701,7 @@ def cmd_inspect(args: argparse.Namespace) -> None: "duration": duration, "chain": {"prev": prev_desc, "next": next_desc}, "files": files, - "agents": agents, + "talents": talents, "stats": stats, "events": events, "index": index_info, @@ -730,9 +730,9 @@ def cmd_inspect(args: argparse.Namespace) -> None: if files: print(f" {', '.join(files)}") print() - print(f"Agents ({len(agents)}):") - if agents: - print(f" {', '.join(agents)}") + print(f"Talents ({len(talents)}):") + if talents: + print(f" {', '.join(talents)}") print() print(f"Size: {_format_size(stats['size'])}") if index_info["available"]: diff --git a/think/sense_splitter.py b/think/sense_splitter.py index a639fb6cc..83ec3e781 100644 --- a/think/sense_splitter.py +++ b/think/sense_splitter.py @@ -28,7 +28,7 @@ def write_sense_outputs( sense_json: dict, seg_dir: Path, stream: str | None = None ) -> None: """Write unified Sense output into per-agent files.""" - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" density = sense_json.get("density") or "active" activity_summary = sense_json.get("activity_summary") or "" @@ -56,7 +56,7 @@ def write_sense_outputs( def write_idle_stubs(seg_dir: Path) -> None: """Write minimal idle output files for a segment.""" _write_json_atomic( - seg_dir / "agents" / "density.json", + seg_dir / "talents" / "density.json", { "classification": "idle", "transcript_lines": 0, diff --git a/think/stats_schema.py b/think/stats_schema.py index a85339088..c827a3d06 100644 --- a/think/stats_schema.py +++ b/think/stats_schema.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -SCHEMA_VERSION = 2 +SCHEMA_VERSION = 3 DAY_FIELDS = ( "transcript_sessions", @@ -39,13 +39,13 @@ REQUIRED_TOP_LEVEL = ( "totals", "heatmap", "tokens", - "agents", + "talents", "facets", ) def validate(data: dict) -> list[str]: - """Validate stats output against schema v2. Returns list of error strings (empty = valid).""" + """Validate stats output against schema v3. Returns list of error strings (empty = valid).""" errors = [] # Check schema_version diff --git a/think/talent.py b/think/talent.py index 897d83365..03c5323ed 100644 --- a/think/talent.py +++ b/think/talent.py @@ -1,14 +1,14 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Talent agent and generator orchestration utilities. +"""Talent and generator orchestration utilities. -This module provides functionality for configuring and orchestrating talent agents +This module provides functionality for configuring and orchestrating talents and generators from talent/*.md and apps/*/talent/*.md. Key functions: - get_talent_configs(): Discover all talent configs with filtering -- get_agent(): Load complete agent configuration by name +- get_talent(): Load complete talent configuration by name - Hook loading: load_pre_hook(), load_post_hook() For simple prompt loading without orchestration (observe/, think/*.md prompts), @@ -95,7 +95,7 @@ def key_to_context(key: str) -> str: def get_output_name(key: str) -> str: - """Convert agent/generator key to filesystem-safe filename stem. + """Convert talent/generator key to a filesystem-safe filename stem. Parameters ---------- @@ -128,37 +128,37 @@ def get_output_path( facet: str | None = None, stream: str | None = None, ) -> Path: - """Return output path for generator agent output. + """Return output path for generator/talent output. Shared utility for determining where to write generator results. - Used by think/agents.py and think/cortex.py. + Used by think.talents and think.cortex. Parameters ---------- day_dir: Day directory path (YYYYMMDD). key: - Generator key or agent name (e.g., "activity", "chat:sentiment", + Generator key or talent name (e.g., "activity", "chat:sentiment", "decisionalizer", "entities:observer"). segment: Optional segment key (HHMMSS_LEN) for segment-level output. output_format: Output format - "json" for JSON, anything else for markdown. facet: - Optional facet name for multi-facet agents. When provided, output is - written under an agents/{facet}/ subdirectory. + Optional facet name for multi-facet talents. When provided, output is + written under a talents/{facet}/ subdirectory. stream: Optional stream name for segment-level output. When provided with - segment, constructs path as YYYYMMDD/{stream}/{segment}/agents/... + segment, constructs path as YYYYMMDD/{stream}/{segment}/talents/... Returns ------- Path Output file path: - - Segment + no facet: YYYYMMDD/{stream}/{segment}/agents/{name}.{ext} - - Segment + facet: YYYYMMDD/{stream}/{segment}/agents/{facet}/{name}.{ext} - - Daily + no facet: YYYYMMDD/agents/{name}.{ext} - - Daily + facet: YYYYMMDD/agents/{facet}/{name}.{ext} + - Segment + no facet: YYYYMMDD/{stream}/{segment}/talents/{name}.{ext} + - Segment + facet: YYYYMMDD/{stream}/{segment}/talents/{facet}/{name}.{ext} + - Daily + no facet: YYYYMMDD/talents/{name}.{ext} + - Daily + facet: YYYYMMDD/talents/{facet}/{name}.{ext} Where name is derived from key and ext is "json" or "md". """ day = Path(day_dir) @@ -172,11 +172,11 @@ def get_output_path( else: seg_dir = day / segment if facet: - return seg_dir / "agents" / facet / filename - return seg_dir / "agents" / filename + return seg_dir / "talents" / facet / filename + return seg_dir / "talents" / filename if facet: - return day / "agents" / facet / filename - return day / "agents" / filename + return day / "talents" / facet / filename + return day / "talents" / filename def get_talent_configs( @@ -328,44 +328,44 @@ def get_talent_configs( # --------------------------------------------------------------------------- -# Agent Resolution +# Talent Resolution # --------------------------------------------------------------------------- -def _resolve_agent_path(name: str) -> tuple[Path, str]: - """Resolve agent name to directory path and agent filename. +def _resolve_talent_path(name: str) -> tuple[Path, str]: + """Resolve talent name to directory path and filename. Parameters ---------- name: - Agent name - either system agent (e.g., "unified") or - app-namespaced agent (e.g., "support:support"). + Talent name - either system talent (e.g., "unified") or + app-namespaced talent (e.g., "support:support"). Returns ------- tuple[Path, str] - (agent_directory, agent_name) tuple. + (talent_directory, talent_name) tuple. """ if ":" in name: - # App agent: "support:support" -> apps/support/talent/support - app, agent_name = name.split(":", 1) - agent_dir = Path(__file__).parent.parent / "apps" / app / "talent" + # App talent: "support:support" -> apps/support/talent/support + app, talent_name = name.split(":", 1) + talent_dir = Path(__file__).parent.parent / "apps" / app / "talent" elif name == "unified": - # Chat agent: "unified" -> talent/chat - agent_dir = TALENT_DIR - agent_name = "chat" + # Chat talent: "unified" -> talent/chat + talent_dir = TALENT_DIR + talent_name = "chat" else: - # System agent: bare name -> talent/{name} - agent_dir = TALENT_DIR - agent_name = name - return agent_dir, agent_name + # System talent: bare name -> talent/{name} + talent_dir = TALENT_DIR + talent_name = name + return talent_dir, talent_name # Default load configuration - prompts must explicitly opt into source loading _DEFAULT_LOAD = { "transcripts": False, "percepts": False, - "agents": False, + "talents": False, } @@ -381,13 +381,13 @@ def source_is_enabled(value: bool | str | dict) -> bool: - False: don't load - True: load if available - "required": load (and generation will fail if none found) - - dict: for agents source, selective loading (e.g., {"entities": true}) + - dict: for talents source, selective loading (e.g., {"entities": true}) Both True and "required" mean the source should be loaded. A non-empty dict means the source should be loaded (with filtering). Args: - value: The source config value (bool, "required" string, or dict for agents) + value: The source config value (bool, "required" string, or dict for talents) Returns: True if the source should be loaded, False otherwise. @@ -402,7 +402,7 @@ def source_is_required(value: bool | str | dict) -> bool: """Check if a source must have content for generation to proceed. Args: - value: The source config value (bool, "required" string, or dict for agents) + value: The source config value (bool, "required" string, or dict for talents) Returns: True if the source is required (generation should skip if no content). @@ -413,46 +413,46 @@ def source_is_required(value: bool | str | dict) -> bool: return value == "required" -def get_agent_filter(value: bool | str | dict) -> dict[str, bool | str] | None: - """Extract agent filter from sources config. +def get_talent_filter(value: bool | str | dict) -> dict[str, bool | str] | None: + """Extract talent filter from sources config. - When agents source is a dict, returns it as filter mapping agent names - to their enabled/required status. When agents source is bool or "required", - returns None to indicate all agents should be loaded. + When talents source is a dict, returns it as filter mapping talent names + to their enabled/required status. When talents source is bool or "required", + returns None to indicate all talents should be loaded. Args: - value: The agents source config value + value: The talents source config value Returns: - Dict mapping agent names to bool/"required", or None for all agents. - Returns empty dict if value is False (no agents). + Dict mapping talent names to bool/"required", or None for all talents. + Returns empty dict if value is False (no talents). Examples: - >>> get_agent_filter(True) - None # All agents - >>> get_agent_filter(False) - {} # No agents - >>> get_agent_filter({"entities": True, "meetings": "required"}) + >>> get_talent_filter(True) + None # All talents + >>> get_talent_filter(False) + {} # No talents + >>> get_talent_filter({"entities": True, "meetings": "required"}) {"entities": True, "meetings": "required"} """ if isinstance(value, dict): return value if value is False: - return {} # No agents - return None # All agents (True or "required") + return {} # No talents + return None # All talents (True or "required") # --------------------------------------------------------------------------- -# Agent Loading +# Talent Loading # --------------------------------------------------------------------------- -def get_agent( +def get_talent( name: str = "unified", facet: str | None = None, analysis_day: str | None = None, ) -> dict: - """Return complete agent configuration by name. + """Return a complete talent configuration by name. Loads configuration from .md file with JSON frontmatter and instruction text. Template variables like $facets are resolved during prompt loading. @@ -461,8 +461,8 @@ def get_agent( Parameters ---------- name: - Agent name to load. Can be a system agent (e.g., "unified") - or an app-namespaced agent (e.g., "support:support" for apps/support/talent/support). + Talent name to load. Can be a system talent (e.g., "unified") + or an app-namespaced talent (e.g., "support:support" for apps/support/talent/support). facet: Optional facet name to focus on. Controls $facets template variable. analysis_day: @@ -472,8 +472,8 @@ def get_agent( Returns ------- dict - Complete agent configuration including: - - name: Agent name + Complete talent configuration including: + - name: Talent name - path: Path to the .md file - user_instruction: Composed prompt with template vars resolved - sources: Source config from 'load' key @@ -481,13 +481,13 @@ def get_agent( """ from think.prompts import _resolve_facets - # Resolve agent path based on namespace - agent_dir, agent_name = _resolve_agent_path(name) + # Resolve talent path based on namespace + talent_dir, talent_name = _resolve_talent_path(name) - # Verify agent prompt file exists - md_path = agent_dir / f"{agent_name}.md" + # Verify talent prompt file exists + md_path = talent_dir / f"{talent_name}.md" if not md_path.exists(): - raise FileNotFoundError(f"Agent not found: {name}") + raise FileNotFoundError(f"Talent not found: {name}") # Load config from frontmatter - preserve all fields post = frontmatter.load(md_path) @@ -508,10 +508,10 @@ def get_agent( prompt_context: dict[str, str] = {} prompt_context["facets"] = _resolve_facets(facet) - agent_prompt = load_prompt(agent_name, base_dir=agent_dir, context=prompt_context) - config["user_instruction"] = agent_prompt.text + prompt_obj = load_prompt(talent_name, base_dir=talent_dir, context=prompt_context) + config["user_instruction"] = prompt_obj.text - # Set agent name + # Set talent name config["name"] = name return config diff --git a/think/talent_cli.py b/think/talent_cli.py index e1dd55678..4cdc43479 100644 --- a/think/talent_cli.py +++ b/think/talent_cli.py @@ -13,9 +13,9 @@ Usage: sol talent show Show details for a specific prompt sol talent show --json Output a single prompt as JSONL sol talent show --prompt Show full prompt context (dry-run) - sol talent logs Show recent agent runs - sol talent logs -c 5 Show last 5 runs for an agent - sol talent log Show events for an agent run + sol talent logs Show recent talent runs + sol talent logs -c 5 Show last 5 runs for a talent + sol talent log Show events for a talent run sol talent log --json Output raw JSONL events sol talent log --full Show expanded event details """ @@ -79,14 +79,14 @@ def _scan_variables(body: str) -> list[str]: return result -def _format_last_run(key: str, agents_dir: Path) -> tuple[str, bool]: +def _format_last_run(key: str, talents_dir: Path) -> tuple[str, bool]: """Format age of last run with optional runtime duration. Returns (display_string, failed) where failed is True if the last event in the log was an error. """ safe_name = key.replace(":", "--") - link_path = agents_dir / f"{safe_name}.log" + link_path = talents_dir / f"{safe_name}.log" if not link_path.exists(): return "-", False @@ -197,7 +197,7 @@ def list_prompts( ) from think.utils import get_journal - agents_dir = Path(get_journal()) / "agents" + talents_dir = Path(get_journal()) / "talents" if not configs: print("No prompts found matching filters.") @@ -248,7 +248,7 @@ def list_prompts( for key, info in items: title = info.get("title", "")[:title_width] - last_run_str, failed = _format_last_run(key, agents_dir) + last_run_str, failed = _format_last_run(key, talents_dir) last_run = last_run_str[:last_run_width] tags = _format_tags(info, failed=failed) src = "" @@ -420,7 +420,7 @@ def show_prompt_context( ) -> None: """Show full prompt context via dry-run. - Builds config and pipes to `sol agents --dry-run` to show exactly + Builds config and pipes to `sol think.talents --dry-run` to show exactly what would be sent to the LLM provider. """ # Load prompt metadata @@ -570,14 +570,14 @@ def show_prompt_context( if facet: config["facet"] = facet else: - # Cogitate prompt - use get_agent() to build full config with instructions - from think.talent import get_agent + # Cogitate prompt - use get_talent() to build full config with instructions + from think.talent import get_talent try: - agent_config = get_agent(name, facet=facet) + agent_config = get_talent(name, facet=facet) config.update(agent_config) except Exception as e: - print(f"Failed to load agent config: {e}", file=sys.stderr) + print(f"Failed to load talent config: {e}", file=sys.stderr) sys.exit(1) # Override prompt with user query @@ -586,11 +586,11 @@ def show_prompt_context( else: config["prompt"] = "(no --query provided)" - # Run sol agents --dry-run + # Run sol think.talents --dry-run config_json = json.dumps(config) try: result = subprocess.run( - ["sol", "agents", "--dry-run"], + ["sol", "think.talents", "--dry-run"], input=config_json + "\n", capture_output=True, text=True, @@ -728,11 +728,11 @@ def show_prompt_context( print() -def _find_run_file(agents_dir: Path, agent_id: str) -> Path | None: - """Locate an agent run JSONL file by ID.""" - for match in agents_dir.glob(f"*/{agent_id}.jsonl"): +def _find_run_file(talents_dir: Path, use_id: str) -> Path | None: + """Locate a talent run JSONL file by ID.""" + for match in talents_dir.glob(f"*/{use_id}.jsonl"): return match - for match in agents_dir.glob(f"*/{agent_id}_active.jsonl"): + for match in talents_dir.glob(f"*/{use_id}_active.jsonl"): return match return None @@ -823,7 +823,7 @@ def _get_output_size(request_event: dict[str, Any], journal_root: str) -> int | def _print_summary(records: list[dict[str, Any]]) -> None: - """Print grouped summary of agent runs.""" + """Print grouped summary of talent runs.""" from collections import defaultdict groups: dict[str, list[dict[str, Any]]] = defaultdict(list) @@ -874,13 +874,13 @@ def logs_runs( errors: bool = False, summary: bool = False, ) -> None: - """Print one-line summaries of recent agent runs from day-index files.""" + """Print one-line summaries of recent talent runs from day-index files.""" from think.models import calc_agent_cost from think.utils import get_journal journal_root = get_journal() - agents_dir = Path(journal_root) / "agents" - if not agents_dir.is_dir(): + talents_dir = Path(journal_root) / "talents" + if not talents_dir.is_dir(): return # Validate --day format @@ -894,10 +894,10 @@ def logs_runs( # Find day-index files, most recent first if day: - day_file = agents_dir / f"{day}.jsonl" + day_file = talents_dir / f"{day}.jsonl" day_files = [day_file] if day_file.is_file() else [] else: - day_files = sorted(agents_dir.glob("????????.jsonl"), reverse=True) + day_files = sorted(talents_dir.glob("????????.jsonl"), reverse=True) if not day_files: return @@ -948,9 +948,9 @@ def logs_runs( name_width = max(name_width, 10) for r in records: - agent_id = r.get("agent_id") + use_id = r.get("use_id") run_file = ( - _find_run_file(agents_dir, agent_id) if isinstance(agent_id, str) else None + _find_run_file(talents_dir, use_id) if isinstance(use_id, str) else None ) stats: dict[str, Any] = { "event_count": 0, @@ -980,7 +980,7 @@ def logs_runs( stats = r.get("_stats") or {} cost_usd = r.get("_cost_usd") output_size = r.get("_output_size") - agent_id = r.get("agent_id", "") + use_id = r.get("use_id", "") ts = r.get("ts", 0) dt = datetime.fromtimestamp(ts / 1000) @@ -1014,7 +1014,7 @@ def logs_runs( facet_part = f" {facet}" if facet else "" line = ( - f"{agent_id:<15}{time_str:>12} {name:<{name_width}} {status_sym} " + f"{use_id:<15}{time_str:>12} {name:<{name_width}} {status_sym} " f"{runtime_str:>7} {cost_str:>4} {events_str:>3} {tools_str:>3} " f"{output_str:>5} {model}{facet_part}" ) @@ -1046,8 +1046,8 @@ def _event_detail(event: dict[str, Any], etype: str) -> str: tool = event.get("tool", "") result = event.get("result", "") return f"{tool} → {result}" - elif etype == "agent_updated": - return event.get("agent", "") + elif etype == "talent_updated": + return event.get("talent", "") elif etype == "finish": result = event.get("result", "") usage = event.get("usage") @@ -1074,7 +1074,7 @@ def _format_event_line(event: dict[str, Any], *, full: bool = False) -> str: "thinking": "think", "tool_start": "tool", "tool_end": "tool_end", - "agent_updated": "updated", + "talent_updated": "updated", "finish": "finish", "error": "error", } @@ -1093,14 +1093,14 @@ def _format_event_line(event: dict[str, Any], *, full: bool = False) -> str: return f"{time_str} {label:<8} {detail}" -def log_run(agent_id: str, *, json_mode: bool = False, full: bool = False) -> None: - """Show events for a single agent run.""" +def log_run(use_id: str, *, json_mode: bool = False, full: bool = False) -> None: + """Show events for a single talent run.""" from think.utils import get_journal - agents_dir = Path(get_journal()) / "agents" - run_file = _find_run_file(agents_dir, agent_id) + talents_dir = Path(get_journal()) / "talents" + run_file = _find_run_file(talents_dir, use_id) if run_file is None: - print(f"Agent run not found: {agent_id}", file=sys.stderr) + print(f"Talent run not found: {use_id}", file=sys.stderr) sys.exit(1) if json_mode: @@ -1164,7 +1164,7 @@ def main() -> None: ) # --- logs subcommand --- - logs_parser = subparsers.add_parser("logs", help="Show recent agent run log") + logs_parser = subparsers.add_parser("logs", help="Show recent talent run log") logs_parser.add_argument("agent", nargs="?", help="Filter to a specific agent") logs_parser.add_argument( "-c", diff --git a/think/agents.py b/think/talents.py similarity index 73% rename from think/agents.py rename to think/talents.py index 1e579b907..3b0db8257 100644 --- a/think/agents.py +++ b/think/talents.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Unified agent CLI for solstone. +"""Unified talent execution module for solstone. -Spawned by cortex for all agent types: -- Tool-using agents (with configured tools) +Spawned by cortex for all talent types: +- Tool-using talents (with configured tools) - Generators (transcript analysis, no tools) Both paths share unified config preparation and execution flow. @@ -18,11 +18,9 @@ import asyncio import json import logging import os -import shutil import sys -import time import traceback -from datetime import datetime, timezone +from datetime import datetime from pathlib import Path from string import Template from typing import Any, Callable, Optional @@ -30,9 +28,9 @@ from typing import Any, Callable, Optional from think.cluster import cluster, cluster_period, cluster_span from think.providers.shared import Event from think.talent import ( - get_agent_filter, get_output_path, get_talent_configs, + get_talent_filter, load_post_hook, load_pre_hook, load_prompt, @@ -52,7 +50,9 @@ from think.utils import ( setup_cli, ) -LOG = logging.getLogger("think.agents") +TALENT_EXECUTION_MODULE = "think.talents" + +LOG = logging.getLogger("think.talents") # Minimum content length for transcript-based generation MIN_INPUT_CHARS = 50 @@ -105,7 +105,7 @@ class JSONEventWriter: def _stream_content_description(stream: str | None) -> str: """Return a human-readable content description for a stream. - Used in preamble templates so agents know what kind of content they're + Used in preamble templates so talents know what kind of content they're analyzing (live capture vs imported conversations, notes, etc.). """ if not stream: @@ -218,7 +218,7 @@ def _build_prompt_context( day: Day in YYYYMMDD format segment: Segment key (HHMMSS_LEN) span: List of segment keys - activity: Optional activity record dict for activity-scheduled agents + activity: Optional activity record dict for activity-scheduled talents Returns: Dict with template variables: @@ -392,17 +392,19 @@ def _load_transcript( elif span: os.environ["SOL_SEGMENT"] = span[0] - # Convert sources config for clustering + # Convert sources config for clustering. + # Frontmatter now uses ``load.talents`` but cluster still consumes the + # normalized ``agents`` source key internally. cluster_sources: dict = {} for k, v in sources.items(): - if k == "agents": - agent_filter = get_agent_filter(v) - if agent_filter is None: - cluster_sources[k] = source_is_enabled(v) - elif not agent_filter: - cluster_sources[k] = False + if k == "talents": + talent_filter = get_talent_filter(v) + if talent_filter is None: + cluster_sources["agents"] = source_is_enabled(v) + elif not talent_filter: + cluster_sources["agents"] = False else: - cluster_sources[k] = agent_filter + cluster_sources["agents"] = talent_filter else: cluster_sources[k] = source_is_enabled(v) @@ -417,15 +419,15 @@ def _load_transcript( def prepare_config(request: dict) -> dict: - """Prepare complete agent config from request. + """Prepare a complete talent config from a request. - Single unified preparation path for all agent types. Takes raw request + Single unified preparation path for all talent types. Takes raw request from cortex and returns fully prepared config ready for execution. Config fields produced: - - name: Agent name + - name: Talent name - provider, model: Resolved from context/request - - user_instruction: Agent instruction from .md file + - user_instruction: Talent instruction from .md file - prompt: User's runtime query/request - transcript: Clustered transcript (if day provided) - output_path: Where to write output (if output format set) @@ -438,7 +440,7 @@ def prepare_config(request: dict) -> dict: Fully prepared config dict """ from think.models import resolve_model_for_provider, resolve_provider - from think.talent import get_agent, key_to_context + from think.talent import get_talent, key_to_context name = request.get("name", "unified") facet = request.get("facet") @@ -450,8 +452,8 @@ def prepare_config(request: dict) -> dict: output_path_override = request.get("output_path") user_prompt = request.get("prompt", "") - # Load complete agent config - config = get_agent(name, facet=facet, analysis_day=day) + # Load complete talent config + config = get_talent(name, facet=facet, analysis_day=day) # Config now contains all frontmatter fields plus: # - path: Path to the .md file @@ -459,11 +461,11 @@ def prepare_config(request: dict) -> dict: # - All frontmatter: tools, hook, disabled, thinking_budget, max_output_tokens, etc. # Convert path string to Path object for convenience - agent_path = Path(config["path"]) if config.get("path") else None + talent_path = Path(config["path"]) if config.get("path") else None sources = config.get("sources", {}) talent_cwd = config.get("cwd") - # Merge request values (request overrides agent defaults) + # Merge request values (request overrides talent defaults) config.update({k: v for k, v in request.items() if v is not None}) request_cwd = request.get("cwd") if request_cwd is not None and request_cwd != talent_cwd: @@ -501,14 +503,14 @@ def prepare_config(request: dict) -> dict: # Resolve provider and model from context context = key_to_context(name) - agent_type = config["type"] - default_provider, default_model = resolve_provider(context, agent_type) + talent_type = config["type"] + default_provider, default_model = resolve_provider(context, talent_type) provider = config.get("provider") or default_provider model = config.get("model") if not model: if provider != default_provider: - model = resolve_model_for_provider(context, provider, agent_type) + model = resolve_model_for_provider(context, provider, talent_type) else: model = default_model @@ -529,14 +531,14 @@ def prepare_config(request: dict) -> dict: config["health_stale"] = should_recheck_health(health_data) if not is_provider_healthy(provider, health_data): - backup = get_backup_provider(agent_type) + backup = get_backup_provider(talent_type) if backup and backup != provider: env_key = PROVIDER_METADATA.get(backup, {}).get("env_key") if not env_key or os.getenv(env_key): config["fallback_from"] = provider config["provider"] = backup config["model"] = resolve_model_for_provider( - context, backup, agent_type + context, backup, talent_type ) # Check if disabled @@ -546,7 +548,7 @@ def prepare_config(request: dict) -> dict: # Day-based processing: load transcript and apply template substitution if day: - # Load transcript (only when agent has enabled sources to consume) + # Load transcript (only when the talent has enabled sources to consume) if any(source_is_enabled(v) for v in sources.values()): transcript, source_counts = _load_transcript(day, segment, span, sources) config["transcript"] = transcript @@ -571,8 +573,8 @@ def prepare_config(request: dict) -> dict: "Scale analysis to available input.\n\n" + transcript ) - # Reload agent instruction with template substitution for day/segment context - if agent_path and agent_path.exists(): + # Reload talent instruction with template substitution for day/segment context + if talent_path and talent_path.exists(): from think.prompts import _resolve_facets prompt_context = _build_prompt_context( @@ -585,13 +587,13 @@ def prepare_config(request: dict) -> dict: if activity_ctx: prompt_context["activity_context"] = activity_ctx - agent_prompt_obj = load_prompt( - agent_path.stem, base_dir=agent_path.parent, context=prompt_context + talent_prompt_obj = load_prompt( + talent_path.stem, base_dir=talent_path.parent, context=prompt_context ) - config["user_instruction"] = agent_prompt_obj.text + config["user_instruction"] = talent_prompt_obj.text # Set prompt (user's runtime query) - # For tool agents: prompt is the user's question + # For tool talents: prompt is the user's question # For generators: prompt is typically empty (instruction is in user_instruction) config["prompt"] = user_prompt @@ -628,9 +630,9 @@ def validate_config(config: dict) -> str | None: has_user_instruction = bool(config.get("user_instruction")) has_day = bool(config.get("day")) - # Cogitate agents need a prompt (user's question) + # Cogitate talents need a prompt (user's question) if is_cogitate and not has_prompt: - return "Missing 'prompt' field for cogitate agent" + return "Missing 'prompt' field for cogitate talent" # Generate prompts need either day (transcript) or user_instruction if not is_cogitate and not has_day and not has_user_instruction and not has_prompt: @@ -716,7 +718,7 @@ def _run_post_hooks(result: str, config: dict) -> str: # ============================================================================= -# Unified Agent Execution +# Unified Talent Execution # ============================================================================= @@ -730,12 +732,12 @@ def _write_output(output_path: Path, result: str) -> None: def _build_dry_run_event(config: dict, before_values: dict) -> dict: """Build a dry-run event with all context.""" - agent_type = config["type"] + talent_type = config["type"] event: dict[str, Any] = { "event": "dry_run", "ts": now_ms(), - "type": agent_type, + "type": talent_type, "name": config.get("name", "unified"), "provider": config.get("provider", ""), "model": config.get("model") or "unknown", @@ -798,7 +800,7 @@ async def _execute_with_tools( config: dict, emit_event: Callable[[dict], None], ) -> None: - """Execute tool-using agent via provider's run_cogitate. + """Execute a tool-using talent via the provider's run_cogitate. Args: config: Prepared config dict @@ -816,7 +818,7 @@ async def _execute_with_tools( provider_mod = get_provider_module(provider) # Wrapper to intercept finish event for post-processing - def agent_emit_event(data: Event) -> None: + def talent_emit_event(data: Event) -> None: if data.get("event") == "finish": result = data.get("result", "") result = _run_post_hooks(result, config) @@ -832,7 +834,7 @@ async def _execute_with_tools( emit_event(data) try: - await provider_mod.run_cogitate(config=config, on_event=agent_emit_event) + await provider_mod.run_cogitate(config=config, on_event=talent_emit_event) except Exception as exc: if not _is_retryable_error(exc) or config.get("fallback_from"): raise @@ -878,7 +880,7 @@ async def _execute_with_tools( def backup_emit(data: Event) -> None: if data.get("event") == "error": return - agent_emit_event(data) + talent_emit_event(data) try: await backup_mod.run_cogitate(config=config, on_event=backup_emit) @@ -1030,14 +1032,14 @@ async def _execute_generate( emit_event(finish_event) -async def _run_agent( +async def _run_talent( config: dict, emit_event: Callable[[dict], None], dry_run: bool = False, ) -> None: - """Execute agent based on config. + """Execute a talent based on config. - Unified execution path for all agent types. Handles: + Unified execution path for all talent types. Handles: - Skip conditions (disabled, no input, etc.) - Output existence checking (skip if exists unless refresh) - Pre/post hooks @@ -1096,10 +1098,10 @@ async def _run_agent( } ) if config.get("day"): - day_log(config["day"], f"agent {name} skipped ({skip_reason})") + day_log(config["day"], f"talent {name} skipped ({skip_reason})") return - # Check if output already exists (applies to both tool agents and generators) + # Check if output already exists (applies to both tool talents and generators) if output_path and not refresh and not dry_run: if output_path.exists() and output_path.stat().st_size > 0: LOG.info("Output exists, loading: %s", output_path) @@ -1145,7 +1147,7 @@ async def _run_agent( } ) if config.get("day"): - day_log(config["day"], f"agent {name} skipped ({skip_reason})") + day_log(config["day"], f"talent {name} skipped ({skip_reason})") return # Dry-run mode @@ -1153,7 +1155,7 @@ async def _run_agent( emit_event(_build_dry_run_event(config, before_values)) return - # Execute based on agent type + # Execute based on talent type if is_cogitate: await _execute_with_tools(config, emit_event) else: @@ -1161,7 +1163,7 @@ async def _run_agent( # Log completion if config.get("day"): - day_log(config["day"], f"agent {name} ok") + day_log(config["day"], f"talent {name} ok") # ============================================================================= @@ -1185,351 +1187,30 @@ def scan_day(day: str) -> dict[str, list[str]]: output_format = meta.get("output") output_file = get_output_path(day_dir, key, output_format=output_format) if output_file.exists(): - processed.append(os.path.join("agents", output_file.name)) + processed.append(os.path.join("talents", output_file.name)) else: - pending.append(os.path.join("agents", output_file.name)) + pending.append(os.path.join("talents", output_file.name)) return {"processed": sorted(processed), "repairable": sorted(pending)} -def _check_generate(provider_name: str, tier: int, timeout: int) -> tuple[str, str]: - """Check generate interface for a provider.""" - from think.models import PROVIDER_DEFAULTS - from think.providers import PROVIDER_METADATA, get_provider_module - - env_key = PROVIDER_METADATA[provider_name]["env_key"] - if env_key and not os.getenv(env_key): - label = PROVIDER_METADATA[provider_name]["label"] - # Google Vertex AI can work without GOOGLE_API_KEY, but this health check - # treats missing env as "not configured" for the standard API path. - return "skip", f"{label} not configured (no {env_key})" - - # For keyless providers (e.g., Ollama), check reachability instead - if not env_key: - from think.providers import validate_key - - result = validate_key(provider_name, "") - if not result.get("valid"): - return ( - "skip", - f"Ollama not reachable ({result.get('error', 'unreachable')})", - ) - - try: - module = get_provider_module(provider_name) - model = PROVIDER_DEFAULTS[provider_name][tier] - result = module.run_generate( - contents="Say OK", - model=model, - temperature=0, - max_output_tokens=16, - system_instruction=None, - json_output=False, - thinking_budget=None, - timeout_s=timeout, - ) - text = result.get("text", "") if isinstance(result, dict) else "" - if text: - usage = result.get("usage") if isinstance(result, dict) else None - if usage: - from think.models import log_token_usage - - log_token_usage( - model=PROVIDER_DEFAULTS[provider_name][tier], - usage=usage, - context="health.check.generate", - type="generate", - ) - return "ok", "OK" - return "fail", "FAIL: empty response text" - except Exception as exc: - return "fail", f"FAIL: {exc}" - - -async def _check_cogitate( - provider_name: str, tier: int, timeout: int -) -> tuple[str, str]: - """Check cogitate interface for a provider by running a real prompt.""" - from think.models import PROVIDER_DEFAULTS - from think.providers import PROVIDER_METADATA, get_provider_module - - # Pre-flight: check provider is configured - env_key = PROVIDER_METADATA[provider_name]["env_key"] - if env_key and not os.getenv(env_key): - label = PROVIDER_METADATA[provider_name]["label"] - return "skip", f"{label} not configured (no {env_key})" - - # For keyless providers (e.g., Ollama), check reachability - if not env_key: - from think.providers import validate_key - - result = validate_key(provider_name, "") - if not result.get("valid"): - return ( - "skip", - f"Ollama not reachable ({result.get('error', 'unreachable')})", - ) - - # Pre-flight: check cogitate CLI binary is installed - binary = PROVIDER_METADATA[provider_name].get("cogitate_cli", "") - if binary and not shutil.which(binary): - return "skip", f"{binary} CLI not installed" - - try: - module = get_provider_module(provider_name) - model = PROVIDER_DEFAULTS[provider_name][tier] - config = {"prompt": "Say OK", "model": model} - result = await asyncio.wait_for( - module.run_cogitate(config=config, on_event=None), - timeout=timeout, - ) - if result: - return "ok", "OK" - return "fail", "FAIL: empty response" - except asyncio.TimeoutError: - return "fail", f"FAIL: timed out after {timeout}s" - except Exception as exc: - return "fail", f"FAIL: {exc}" - - -async def _run_check(args: argparse.Namespace) -> None: - """Run connectivity checks against AI providers.""" - from think.models import PROVIDER_DEFAULTS, TIER_FLASH, TIER_LITE, TIER_PRO - from think.providers import PROVIDER_REGISTRY - - # --targeted: only check configured provider+tier pairs - targeted_pairs = None - if args.targeted and not args.provider and not args.tier: - import fcntl - - from think.models import TYPE_DEFAULTS, get_backup_provider - from think.utils import get_config - - targeted_pairs = set() - config = get_config() - providers_config = config.get("providers", {}) - for agent_type, defaults in TYPE_DEFAULTS.items(): - type_config = providers_config.get(agent_type, {}) - provider = type_config.get("provider", defaults["provider"]) - tier = type_config.get("tier", defaults["tier"]) - targeted_pairs.add((provider, tier)) - backup = get_backup_provider(agent_type) - if backup: - targeted_pairs.add((backup, tier)) - - # flock dedup: only one targeted check runs at a time - lock_dir = Path(get_journal()) / "health" - lock_dir.mkdir(parents=True, exist_ok=True) - lock_fd = open(lock_dir / "recheck.lock", "w") - try: - fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except OSError: - lock_fd.close() - return - - if args.provider: - providers = args.provider - for name in providers: - if name not in PROVIDER_REGISTRY: - available = ", ".join(PROVIDER_REGISTRY.keys()) - print( - f"Unknown provider: {name}. Available providers: {available}", - file=sys.stderr, - ) - sys.exit(1) - else: - providers = list(PROVIDER_REGISTRY.keys()) - - interfaces = [args.interface] if args.interface else ["generate", "cogitate"] - - tier_names = {1: "pro", 2: "flash", 3: "lite"} - tiers = [args.tier] if args.tier else [TIER_PRO, TIER_FLASH, TIER_LITE] - - # Pre-compute column widths - provider_width = max(len(n) for n in providers) if providers else 0 - tier_width = max(len(tier_names[t]) for t in tiers) - # Resolve all model names to get max width - model_names = set() - for p in providers: - for t in tiers: - model_names.add(PROVIDER_DEFAULTS[p][t]) - model_width = max(len(m) for m in model_names) if model_names else 0 - interface_width = max(len(n) for n in interfaces) if interfaces else 0 - - total = 0 - passed = 0 - failed = 0 - skipped = 0 - results = [] - cache = {} # (provider, model, interface) -> (status, message, source_tier) - - for provider_name in providers: - for tier in tiers: - if ( - targeted_pairs is not None - and (provider_name, tier) not in targeted_pairs - ): - continue - model = PROVIDER_DEFAULTS[provider_name][tier] - for interface_name in interfaces: - cache_key = (provider_name, model, interface_name) - if cache_key in cache: - status, message, source_tier = cache[cache_key] - elapsed_s = 0.0 - elapsed_s_rounded = 0.0 - reused_from = source_tier - else: - start = time.perf_counter() - if interface_name == "generate": - status, message = _check_generate( - provider_name, tier, args.timeout - ) - else: - status, message = await _check_cogitate( - provider_name, tier, args.timeout - ) - elapsed_s = time.perf_counter() - start - elapsed_s_rounded = round(elapsed_s, 1) - cache[cache_key] = (status, message, tier_names[tier]) - reused_from = None - - result = { - "provider": provider_name, - "tier": tier_names[tier], - "model": model, - "interface": interface_name, - "ok": status != "fail", - "status": status, - "message": str(message), - "elapsed_s": elapsed_s_rounded, - } - if reused_from: - result["reused_from"] = reused_from - results.append(result) - - if not args.json: - if reused_from: - mark = "=" - display_message = f"{message} (={reused_from})" - else: - if status == "ok": - mark = "✓" - elif status == "skip": - mark = "-" - else: - mark = "✗" - display_message = str(message) - print( - f"{mark} " - f"{provider_name:<{provider_width}} " - f"{tier_names[tier]:<{tier_width}} " - f"{model:<{model_width}} " - f"{interface_name:<{interface_width}} " - f"{display_message} ({elapsed_s:.1f}s)" - ) - - total += 1 - if status == "ok": - passed += 1 - elif status == "skip": - skipped += 1 - else: - failed += 1 - - any_failed = any(r["status"] == "fail" for r in results) - - # Write results to health file - payload = { - "results": results, - "summary": { - "total": total, - "passed": passed, - "skipped": skipped, - "failed": failed, - }, - "checked_at": datetime.now(timezone.utc).isoformat(), - } - health_dir = Path(get_journal()) / "health" - health_dir.mkdir(parents=True, exist_ok=True) - (health_dir / "agents.json").write_text(json.dumps(payload, indent=2)) - - if args.json: - print( - json.dumps( - { - "results": results, - "summary": { - "total": total, - "passed": passed, - "skipped": skipped, - "failed": failed, - }, - }, - indent=2, - ) - ) - else: - print(f"{total} checks: {passed} passed, {skipped} skipped, {failed} failed") - sys.exit(1 if any_failed else 0) - - # ============================================================================= # Main Entry Point # ============================================================================= async def main_async() -> None: - """NDJSON-based CLI for agents.""" - from think.providers import PROVIDER_REGISTRY + """NDJSON-based CLI for talents.""" parser = argparse.ArgumentParser( - description="solstone Agent CLI - Accepts NDJSON input via stdin" + description="solstone Talent CLI - Accepts NDJSON input via stdin" ) parser.add_argument( "--dry-run", action="store_true", help="Show what would be sent to the provider without calling the LLM", ) - subparsers = parser.add_subparsers(dest="subcommand") - check_parser = subparsers.add_parser("check", help="Check AI provider connectivity") - check_parser.add_argument( - "--provider", - action="append", - help=f"Provider to check (repeatable). Available: {', '.join(PROVIDER_REGISTRY.keys())}", - ) - check_parser.add_argument( - "--interface", - choices=["generate", "cogitate"], - default=None, - help="Interface to check (default: both)", - ) - check_parser.add_argument( - "--timeout", - type=int, - default=30, - help="Timeout in seconds for generate checks (default: 30)", - ) - check_parser.add_argument( - "--tier", - type=int, - choices=[1, 2, 3], - default=None, - help="Tier to check (1=pro, 2=flash, 3=lite; default: all)", - ) - check_parser.add_argument( - "--json", action="store_true", help="Output results as JSON" - ) - check_parser.add_argument( - "--targeted", - action="store_true", - help="Only check configured provider+tier pairs (used by automated rechecks)", - ) - args = setup_cli(parser) require_solstone() - if args.subcommand == "check": - await _run_check(args) - return - dry_run = args.dry_run app_logger = setup_logging(args.verbose) @@ -1556,7 +1237,7 @@ async def main_async() -> None: emit_event({"event": "error", "error": error, "ts": now_ms()}) continue - await _run_agent(config, emit_event, dry_run=dry_run) + await _run_talent(config, emit_event, dry_run=dry_run) except json.JSONDecodeError as e: emit_event( diff --git a/think/tools/call.py b/think/tools/call.py index 0bb641178..74d8518ca 100644 --- a/think/tools/call.py +++ b/think/tools/call.py @@ -606,7 +606,7 @@ def agents( if segment: # List outputs in a specific segment directory - seg_path = day_dir / segment / "agents" + seg_path = day_dir / segment / "talents" if not seg_path.is_dir(): typer.echo(f"Segment {segment} not found for {day}.") return @@ -614,7 +614,7 @@ def agents( return # List daily agent outputs - agents_path = day_dir / "agents" + agents_path = day_dir / "talents" if agents_path.is_dir(): _list_outputs(agents_path, "Daily agents") @@ -623,8 +623,8 @@ def agents( if seg_list: typer.echo(f"\nSegments: {len(seg_list)}") for stream_name, seg_key, seg_path_obj in seg_list: - agents_dir = seg_path_obj / "agents" - outputs = _get_output_names(agents_dir) + talents_dir = seg_path_obj / "talents" + outputs = _get_output_names(talents_dir) label = f" {stream_name}/{seg_key}" if stream_name else f" {seg_key}" if outputs: typer.echo(f"{label}: {', '.join(outputs)}") @@ -686,12 +686,12 @@ def read( raise typer.Exit(1) if segment: - base_dir = day_dir / segment / "agents" + base_dir = day_dir / segment / "talents" else: - base_dir = day_dir / "agents" + base_dir = day_dir / "talents" if not base_dir.is_dir(): - location = f"segment {segment}" if segment else "agents" + location = f"segment {segment}" if segment else "talents" typer.echo(f"No {location} directory for {day}.", err=True) raise typer.Exit(1) diff --git a/think/tools/sol.py b/think/tools/sol.py index 7fcb43540..73bd3d0a9 100644 --- a/think/tools/sol.py +++ b/think/tools/sol.py @@ -8,7 +8,7 @@ Provides read and write access to ``{journal}/sol/self.md``, ``{journal}/sol/pulse.md``, and ``{journal}/sol/awareness.md`` — sol's identity and initiative files. Also provides read access to the morning briefing at -``{journal}/YYYYMMDD/agents/morning_briefing.md``. +``{journal}/YYYYMMDD/talents/morning_briefing.md``. Mounted by ``think.call`` as ``sol call identity ...``. """ @@ -318,9 +318,9 @@ def awareness_cmd( def briefing_cmd( day: str | None = typer.Option(None, "--day", "-d", help="Specific day YYYYMMDD."), ) -> None: - """Read the morning briefing from YYYYMMDD/agents/morning_briefing.md.""" + """Read the morning briefing from YYYYMMDD/talents/morning_briefing.md.""" if day: - path = day_path(day, create=False) / "agents" / "morning_briefing.md" + path = day_path(day, create=False) / "talents" / "morning_briefing.md" if not path.exists(): typer.echo("No briefing found.", err=True) raise typer.Exit(1) @@ -329,7 +329,7 @@ def briefing_cmd( # No day specified — find most recent for day in sorted(day_dirs().keys(), reverse=True): - agents_dir = day_path(day, create=False) / "agents" + agents_dir = day_path(day, create=False) / "talents" briefing = agents_dir / "morning_briefing.md" if briefing.exists() and briefing.stat().st_size > 0: typer.echo(briefing.read_text(encoding="utf-8"))