personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: rename solstone agents concept to talents

Land the pre-release agents -> talents vocabulary rename in one coordinated
commit while the surface area is still cheap to change and there are no
external consumers to preserve. This keeps the product language coherent
before release and avoids shipping a half-renamed runtime, API, and journal
layout.

Key changes:
- Split the old `think/agents.py` surface into `think/talents.py` for runtime
execution and `think/providers_cli.py` for the standalone `sol providers
check` health-check CLI.
- Remove `sol agents`; add `sol providers check` for provider health, and have
Cortex spawn talents via `[sys.executable, "-m", TALENT_EXECUTION_MODULE]`
instead of shelling out through `sol`.
- Apply the vocabulary split consistently: `talent` means config/prompt
definition, `use` means one invocation (`agent_id` -> `use_id`,
`running_agents` -> `running_uses`, `agent_requests` -> `use_requests`,
and related helpers/attrs).
- Rename wire/runtime events: dream chronicle `agent.{fail,dispatch,complete,skip}`
-> `talent.*`, Callosum `agent_{started,completed}` -> `talent_*`, and
`agent_updated` -> `talent_updated`.
- Rename the day route from `/api/agents/<day>` to `/api/talents/<day>` and
return both `uses` (invocations) and `talents` (config metadata) in the
payload.
- Rename the journal layout from `<journal>/agents/` to `<journal>/talents/`
across root/day/segment/health locations and add
`apps/sol/maint/004_rename_agents_to_talents.py` for automatic live-journal
migration on supervisor startup.
- Bump the stats schema from v2 to v3 and require top-level `"talents"`.
- Keep the only allowed compatibility shim in `think/pipeline_health.py` and
`apps/home/routes.py`, where historical dream chronicle readers accept both
legacy `agent.*` and new `talent.*` event names through 2026-05-01.
- Add `scripts/gate_agents_rename.py` and wire it into `make ci` to prevent
reintroduction of legacy rename patterns.

Out-of-scope followups:
- Decompose `think/talents.py` (1,300+ lines) into submodules.
- Restructure `think/talent.py` + `think/talent_cli.py` into a `think/talent/`
package.
- Remove the historical read-side shims after the 2026-05-01 sunset.

Operator verification after supervisor restart on a live journal:
- Migration script runs at startup and renames pre-existing `<journal>/agents/`
dirs to `<journal>/talents/`.
- Cortex spawn logs show `[sys.executable, -m, think.talents]`.
- `sol providers check` reports all providers (Anthropic, OpenAI, Google,
Ollama).
- `sol agents` fails with unknown command.
- Home yesterdays-card renders on a journal that has both pre- and post-rename
chronicle events.
- `make ci` is green and the rename gate passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+2915 -2456
+7 -7
AGENTS.md
··· 7 7 - **Journal**: Central data structure organized as `journal/YYYYMMDD/` directories. All captured data, transcripts, and analysis artifacts are stored here. 8 8 - **Facets**: Project/context organization system that groups related content and provides scoped views of entities, tasks, and activities. 9 9 - **Entities**: Extracted information tracked over time across transcripts and interactions and associated with facets for semantic navigation. 10 - - **Agents**: AI processors with configurable prompts that analyze content, extract insights, and respond to queries. 10 + - **Talents**: AI processors with configurable prompts that analyze content, extract insights, and respond to queries. 11 11 - **Callosum**: Message bus that enables asynchronous communication between components. 12 12 - **Indexer**: Builds and maintains a SQLite database from journal data, enabling fast search and retrieval. 13 13 ··· 24 24 25 25 **Component communication**: 26 26 - Callosum enables async communication between services. 27 - - Cortex orchestrates AI agent execution via `sol cortex`, spawning agent subprocesses with agent configurations. 27 + - Cortex orchestrates AI talent execution via `sol cortex`, spawning talent subprocesses with talent configurations. 28 28 - The unified CLI is `sol`. Run `sol` to see status and available commands. 29 29 30 30 ## Quick Commands 31 31 32 32 ```bash 33 33 make install # Install package (includes all deps) 34 - make skills # Discover and symlink Agent Skills from talent/ dirs 34 + make skills # Discover and symlink Anthropic Skills from talent/ dirs 35 35 make format # Auto-fix formatting, then report remaining issues 36 36 make test # Run unit tests 37 37 make ci # Full CI check (format check + lint + test) 38 38 make dev # Start stack (Ctrl+C to stop) 39 39 ``` 40 40 41 - ## Agent CLI Boundaries 41 + ## Talent CLI Boundaries 42 42 43 - 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: 43 + 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: 44 44 45 45 - `sol supervisor` / `sol start` 46 46 - `sol dream` except heartbeat's targeted `sol dream --segment` 47 47 - `sol import` 48 48 - `sol config` 49 49 - `sol cortex` 50 - - `sol agents` 50 + - `sol providers check` 51 51 - `sol callosum` 52 52 - `sol observer` / `sol observe-*` 53 53 - `sol sense` 54 54 - `sol transcribe` / `sol describe` 55 55 - `sol indexer --reset` 56 56 57 - Agents should use `sol call` commands for journal interaction and `sol health` / `sol talent logs` for diagnostics. 57 + Talents should use `sol call` commands for journal interaction and `sol health` / `sol talent logs` for diagnostics. 58 58 59 59 ## Reference 60 60
+7 -1
Makefile
··· 1 1 # solstone Makefile 2 2 # Python-based AI-driven desktop journaling toolkit 3 3 4 - .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 4 + .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 5 5 6 6 # Default target - install package in editable mode 7 7 all: install ··· 404 404 @echo "=== Running ruff ===" 405 405 @$(RUFF) check . || { echo "Run 'make format' to auto-fix"; exit 1; } 406 406 @echo "" 407 + @echo "=== Running rename gate ===" 408 + @$(MAKE) gate-agents-rename 409 + @echo "" 407 410 @echo "=== Running mypy ===" 408 411 @$(MYPY) . || true 409 412 @echo "" ··· 451 454 @$(UV) pip show pre-commit >/dev/null 2>&1 || { echo "Installing pre-commit..."; $(UV) pip install pre-commit; } 452 455 $(VENV_BIN)/pre-commit install 453 456 @echo "Pre-commit hooks installed!" 457 + # Rename guard for the agents -> talents transition 458 + gate-agents-rename: .installed 459 + $(VENV_BIN)/python scripts/gate_agents_rename.py
+6 -6
apps/entities/routes.py
··· 521 521 f"Current Description: {current_desc}" 522 522 ) 523 523 524 - agent_id = spawn_agent( 524 + use_id = spawn_agent( 525 525 prompt=prompt, 526 526 name="entities:entity_describe", 527 527 provider="google", 528 528 ) 529 - if agent_id is None: 529 + if use_id is None: 530 530 return jsonify({"error": "Failed to connect to agent service"}), 503 531 531 532 - return jsonify({"success": True, "agent_id": agent_id}) 532 + return jsonify({"success": True, "use_id": use_id}) 533 533 534 534 except Exception as e: 535 535 return ( ··· 556 556 prompt = f"For the '{facet_name}' facet, this is the user's request to attach a new entity: {name}" 557 557 558 558 # Create agent request - entity_assist agent already has provider configured 559 - agent_id = spawn_agent( 559 + use_id = spawn_agent( 560 560 prompt=prompt, 561 561 name="entities:entity_assist", 562 562 ) 563 - if agent_id is None: 563 + if use_id is None: 564 564 return jsonify({"error": "Failed to connect to agent service"}), 503 565 565 566 - return jsonify({"success": True, "agent_id": agent_id}) 566 + return jsonify({"success": True, "use_id": use_id}) 567 567 568 568 except Exception as e: 569 569 return jsonify({"error": f"Failed to start entity assistant: {str(e)}"}), 500
+1 -1
apps/entities/talent/entity_observer.md
··· 12 12 "output": "json", 13 13 "thinking_budget": 2048, 14 14 "hook": {"pre": "entities:entity_observer", "post": "entities:entity_observer"}, 15 - "load": {"transcripts": false, "percepts": false, "agents": false} 15 + "load": {"transcripts": false, "percepts": false, "talents": false} 16 16 } 17 17 18 18 ## Core Mission
+8 -8
apps/entities/workspace.html
··· 1480 1480 let detectedPage = 1; 1481 1481 let journalEntitiesData = null; // For all-facet mode 1482 1482 let currentDetailEntity = null; 1483 - const pendingEntities = new Map(); // agent_id → { name, element } 1484 - const pendingAgentCallbacks = new Map(); // agent_id → callback function 1483 + const pendingEntities = new Map(); // use_id → { name, element } 1484 + const pendingAgentCallbacks = new Map(); // use_id → callback function 1485 1485 const _errorTimers = {}; 1486 1486 1487 1487 // Standard entity types - fetched from server ··· 2361 2361 }) 2362 2362 .then(response => response.json()) 2363 2363 .then(data => { 2364 - if (data.success && data.agent_id) { 2365 - listenForAgentCompletion(data.agent_id, (result) => { 2364 + if (data.success && data.use_id) { 2365 + listenForAgentCompletion(data.use_id, (result) => { 2366 2366 if (result.success && result.response) { 2367 2367 saveDescription(entity, result.response); 2368 2368 } else { ··· 3185 3185 }) 3186 3186 .then(response => response.json()) 3187 3187 .then(data => { 3188 - if (data.success && data.agent_id) { 3188 + if (data.success && data.use_id) { 3189 3189 const pending = pendingEntities.get(tempId); 3190 3190 if (pending) { 3191 3191 pendingEntities.delete(tempId); 3192 - pendingEntities.set(data.agent_id, pending); 3193 - pending.element.dataset.agentId = data.agent_id; 3192 + pendingEntities.set(data.use_id, pending); 3193 + pending.element.dataset.agentId = data.use_id; 3194 3194 } 3195 3195 } else { 3196 3196 throw new Error(data.error || 'Failed to start assistant'); ··· 3210 3210 } 3211 3211 3212 3212 window.appEvents.listen('cortex', (msg) => { 3213 - const agentId = msg.agent_id; 3213 + const agentId = msg.use_id; 3214 3214 if (!agentId) return; 3215 3215 3216 3216 // Pending entity additions
+1 -1
apps/health/talent/health/SKILL.md
··· 80 80 81 81 Flags compose with AND logic. For example, `--daily --errors` shows only daily runs that errored. 82 82 83 - Output columns: agent_id, time, name, status, runtime, cost, events, tools, output_size, model, facet. 83 + Output columns: use_id, time, name, status, runtime, cost, events, tools, output_size, model, facet. 84 84 85 85 Examples: 86 86
+2 -2
apps/health/tests/test_call.py
··· 63 63 "\n".join( 64 64 [ 65 65 json.dumps({"event": "run.start", "mode": "segment"}), 66 - json.dumps({"event": "agent.dispatch", "mode": "segment"}), 67 - json.dumps({"event": "agent.complete", "mode": "segment"}), 66 + json.dumps({"event": "talent.dispatch", "mode": "segment"}), 67 + json.dumps({"event": "talent.complete", "mode": "segment"}), 68 68 json.dumps( 69 69 {"event": "run.complete", "mode": "segment", "duration_ms": 42} 70 70 ),
+1 -1
apps/health/tests/test_routes.py
··· 29 29 30 30 def test_path_outside_health_dir_rejected(self, health_env): 31 31 env = health_env() 32 - resp = env.client.get("/app/health/api/log?path=20260322/agents/something.log") 32 + resp = env.client.get("/app/health/api/log?path=20260322/talents/something.log") 33 33 assert resp.status_code == 400 34 34 35 35 def test_missing_file_returns_404(self, health_env):
+17 -17
apps/health/workspace.html
··· 2278 2278 const k = child.getAttribute('data-key'); 2279 2279 if (k) existingByKey.set(k, child); 2280 2280 } 2281 - const newKeys = new Set(activeAgents.map(a => a.agent_id)); 2281 + const newKeys = new Set(activeAgents.map(a => a.use_id)); 2282 2282 for (const [k, child] of existingByKey) { 2283 2283 if (!newKeys.has(k)) container.removeChild(child); 2284 2284 } 2285 2285 for (const agent of activeAgents) { 2286 - const key = agent.agent_id; 2286 + const key = agent.use_id; 2287 2287 let card = existingByKey.get(key); 2288 2288 if (!card) { 2289 2289 card = document.createElement('div'); ··· 2309 2309 const stateLabel = agent.event === 'thinking' ? 'Thinking...' : 2310 2310 (agent.event === 'tool_start' || agent.event === 'tool_end') ? 'Working...' : 'Running...'; 2311 2311 const elapsed = agent.elapsed_seconds ? formatElapsed(agent.elapsed_seconds) : '0s'; 2312 - card.children[0].textContent = '...' + getAgentId(agent.agent_id); 2312 + card.children[0].textContent = '...' + getAgentId(agent.use_id); 2313 2313 card.children[1].textContent = agent.name || 'default'; 2314 2314 card.children[2].textContent = stateLabel; 2315 2315 card.children[3].textContent = elapsed; ··· 2696 2696 } 2697 2697 2698 2698 function handleCortexEvent(msg) { 2699 - // Handle status event first (no agent_id at top level) 2699 + // Handle status event first (no use_id at top level) 2700 2700 if (msg.event === 'status') { 2701 2701 // Update agent count for vitals 2702 - state.agentCount = msg.running_agents || 0; 2702 + state.agentCount = msg.running_uses || 0; 2703 2703 2704 - // Status event contains array of agents 2705 - if (msg.agents) { 2706 - // Clear agents not in status (they finished) 2707 - const activeIds = new Set(msg.agents.map(a => a.agent_id)); 2704 + // Status event contains array of uses 2705 + if (msg.uses) { 2706 + // Clear uses not in status (they finished) 2707 + const activeIds = new Set(msg.uses.map(a => a.use_id)); 2708 2708 state.agents.forEach((_, id) => { 2709 2709 if (!activeIds.has(id)) { 2710 2710 state.agents.delete(id); 2711 2711 } 2712 2712 }); 2713 2713 2714 - // Update/add agents from status 2715 - msg.agents.forEach(agent => { 2716 - const existing = state.agents.get(agent.agent_id) || {}; 2717 - state.agents.set(agent.agent_id, { 2714 + // Update/add uses from status 2715 + msg.uses.forEach(agent => { 2716 + const existing = state.agents.get(agent.use_id) || {}; 2717 + state.agents.set(agent.use_id, { 2718 2718 ...existing, 2719 - agent_id: agent.agent_id, 2719 + use_id: agent.use_id, 2720 2720 name: agent.name, 2721 2721 provider: agent.provider, 2722 2722 elapsed_seconds: agent.elapsed_seconds, ··· 2730 2730 return; 2731 2731 } 2732 2732 2733 - // Individual agent events require agent_id 2734 - const agentId = msg.agent_id; 2733 + // Individual agent events require use_id 2734 + const agentId = msg.use_id; 2735 2735 if (!agentId) return; 2736 2736 2737 2737 // Track start time for client-side elapsed updates ··· 2740 2740 2741 2741 state.agents.set(agentId, { 2742 2742 ...existing, 2743 - agent_id: agentId, 2743 + use_id: agentId, 2744 2744 name: msg.name || existing.name, 2745 2745 provider: msg.provider || existing.provider, 2746 2746 event: msg.event,
+6 -6
apps/home/events.py
··· 11 11 12 12 from apps.events import EventContext, on_event 13 13 from think.conversation import record_exchange 14 - from think.cortex_client import read_agent_events 14 + from think.cortex_client import read_use_events 15 15 16 16 logger = logging.getLogger(__name__) 17 17 ··· 25 25 if name not in TRIAGE_AGENT_NAMES: 26 26 return 27 27 28 - agent_id = ctx.msg.get("agent_id") 29 - if not agent_id: 28 + use_id = ctx.msg.get("use_id") 29 + if not use_id: 30 30 return 31 31 32 32 try: 33 - events = read_agent_events(agent_id) 33 + events = read_use_events(use_id) 34 34 facet = "" 35 35 app = "" 36 36 path = "" ··· 51 51 user_message=user_message, 52 52 agent_response=result, 53 53 talent=name, 54 - agent_id=agent_id, 54 + use_id=use_id, 55 55 ) 56 56 except Exception: 57 57 logger.debug( 58 58 "Failed to record conversation exchange for agent %s", 59 - agent_id, 59 + use_id, 60 60 exc_info=True, 61 61 )
+5 -4
apps/home/routes.py
··· 97 97 """Load today's flow.md content and mtime. Returns (content, mtime) or (None, None).""" 98 98 try: 99 99 journal = Path(get_journal()) 100 - flow_path = journal / today / "agents" / "flow.md" 100 + flow_path = journal / today / "talents" / "flow.md" 101 101 if flow_path.exists(): 102 102 return flow_path.read_text(), flow_path.stat().st_mtime 103 103 except Exception: ··· 508 508 509 509 def _knowledge_graph_freshness(yesterday: str) -> dict[str, Any]: 510 510 path = ( 511 - Path(get_journal()) / "chronicle" / yesterday / "agents" / "knowledge_graph.md" 511 + Path(get_journal()) / "chronicle" / yesterday / "talents" / "knowledge_graph.md" 512 512 ) 513 513 if not path.exists(): 514 514 return {"exists": False, "fresh": False, "updated_label": None} ··· 584 584 record = json.loads(line) 585 585 except json.JSONDecodeError: 586 586 continue 587 + # HISTORICAL SHIM: accept legacy agent.* chronicle event names from before 2026-04-17; sunset 2026-05-01 587 588 if ( 588 - record.get("event") == "agent.fail" 589 + record.get("event") in {"agent.fail", "talent.fail"} 589 590 and record.get("facet") 590 591 and record.get("name") == "facet_newsletter" 591 592 ): ··· 765 766 has_activity = any( 766 767 anomaly.get("kind") == "activity_agents_missing" for anomaly in anomalies 767 768 ) 768 - has_failure = any(anomaly.get("kind") == "agent_failure" for anomaly in anomalies) 769 + has_failure = any(anomaly.get("kind") == "talent_failure" for anomaly in anomalies) 769 770 770 771 if has_daily: 771 772 bullets.append("I didn't finish the full overnight review.")
+2 -2
apps/search/routes.py
··· 138 138 - total: Total match count 139 139 - days: List of day groups, each with date info and results 140 140 - facets: List of facets with counts for filter sidebar 141 - - agents: List of agents with counts for filter sidebar 141 + - talents: List of talents with counts for filter sidebar 142 142 """ 143 143 query = request.args.get("q", "").strip() 144 144 ··· 233 233 "showing_days": len(days_response), 234 234 "days": days_response, 235 235 "facets": facets_list, 236 - "agents": agents_list, 236 + "talents": agents_list, 237 237 } 238 238 ) 239 239
+124
apps/sol/maint/004_rename_agents_to_talents.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Rename live journal agents paths to talents.""" 5 + 6 + from __future__ import annotations 7 + 8 + import argparse 9 + import sys 10 + from dataclasses import dataclass 11 + from pathlib import Path 12 + 13 + from think.utils import day_dirs, get_journal, iter_segments, setup_cli 14 + 15 + 16 + @dataclass 17 + class RenameSummary: 18 + discovered: int = 0 19 + moved: int = 0 20 + skipped: int = 0 21 + errors: int = 0 22 + collisions: int = 0 23 + 24 + 25 + def discover_moves(journal_path: Path) -> tuple[list[tuple[Path, Path]], list[Path]]: 26 + """Return planned (src, dst) moves and already-migrated destinations.""" 27 + planned: list[tuple[Path, Path]] = [] 28 + skipped: list[Path] = [] 29 + 30 + def add_pair(src: Path, dst: Path) -> None: 31 + if src.exists(): 32 + planned.append((src, dst)) 33 + elif dst.exists(): 34 + skipped.append(dst) 35 + 36 + add_pair(journal_path / "agents", journal_path / "talents") 37 + add_pair( 38 + journal_path / "health" / "agents.json", 39 + journal_path / "health" / "talents.json", 40 + ) 41 + 42 + for day_name, day_abs in sorted(day_dirs().items()): 43 + day_dir = Path(day_abs) 44 + if not day_dir.is_dir(): 45 + continue 46 + 47 + add_pair(day_dir / "agents", day_dir / "talents") 48 + 49 + for _stream, _segment, seg_path in iter_segments(day_name): 50 + add_pair(seg_path / "agents", seg_path / "talents") 51 + 52 + return planned, skipped 53 + 54 + 55 + def run_migration( 56 + journal_path: Path, *, dry_run: bool 57 + ) -> tuple[RenameSummary, list[tuple[Path, Path]]]: 58 + """Run or preview the agents->talents path rename.""" 59 + summary = RenameSummary() 60 + planned, skipped = discover_moves(journal_path) 61 + summary.discovered = len(planned) 62 + summary.skipped = len(skipped) 63 + 64 + collisions = [(src, dst) for src, dst in planned if dst.exists()] 65 + summary.collisions = len(collisions) 66 + if collisions: 67 + return summary, collisions 68 + 69 + for src, dst in planned: 70 + print(f"{'[DRY-RUN] ' if dry_run else ''}move {src} -> {dst}") 71 + if dry_run: 72 + continue 73 + 74 + try: 75 + dst.parent.mkdir(parents=True, exist_ok=True) 76 + src.rename(dst) 77 + summary.moved += 1 78 + except Exception as exc: 79 + summary.errors += 1 80 + print(f"[ERROR] move failed: {src} -> {dst}: {exc}") 81 + 82 + if dry_run: 83 + summary.moved = len(planned) 84 + 85 + return summary, [] 86 + 87 + 88 + def _print_summary(summary: RenameSummary) -> None: 89 + print("Summary") 90 + print(f" discovered: {summary.discovered}") 91 + print(f" moved: {summary.moved}") 92 + print(f" skipped: {summary.skipped}") 93 + print(f" errors: {summary.errors}") 94 + print(f" collisions: {summary.collisions}") 95 + 96 + 97 + def main() -> None: 98 + parser = argparse.ArgumentParser( 99 + description="Rename live journal agents paths to talents." 100 + ) 101 + parser.add_argument( 102 + "--dry-run", 103 + action="store_true", 104 + help="Preview planned renames without writing files.", 105 + ) 106 + args = setup_cli(parser) 107 + 108 + journal_path = Path(get_journal()) 109 + summary, collisions = run_migration(journal_path, dry_run=args.dry_run) 110 + 111 + if collisions: 112 + print("Collision(s) detected; no files were moved:") 113 + for src, dst in collisions: 114 + print(f" {src} -> {dst}") 115 + _print_summary(summary) 116 + sys.exit(2) 117 + 118 + _print_summary(summary) 119 + if summary.errors: 120 + sys.exit(1) 121 + 122 + 123 + if __name__ == "__main__": 124 + main()
+108 -112
apps/sol/routes.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Agents app - browse historical agent runs by day and facet.""" 4 + """Talents app - browse historical talent uses by day and facet.""" 5 5 6 6 from __future__ import annotations 7 7 ··· 73 73 return facet 74 74 75 75 76 - def _agent_id_to_day(agent_id: str) -> str: 77 - """Convert agent_id (millisecond timestamp) to YYYYMMDD day string.""" 76 + def _use_id_to_day(use_id: str) -> str: 77 + """Convert use_id (millisecond timestamp) to YYYYMMDD day string.""" 78 78 try: 79 - ts = int(agent_id) / 1000 79 + ts = int(use_id) / 1000 80 80 return datetime.fromtimestamp(ts).strftime("%Y%m%d") 81 81 except (ValueError, OSError): 82 82 return "" 83 83 84 84 85 - def _parse_agent_events( 85 + def _parse_use_events( 86 86 lines: list[str], *, collect_events: bool = False 87 87 ) -> dict[str, Any]: 88 - """Parse agent event lines and extract counts and cost data. 88 + """Parse use event lines and extract counts and cost data. 89 89 90 90 Args: 91 91 lines: List of JSONL lines ··· 142 142 return result 143 143 144 144 145 - def _parse_agent_file(agent_file: Path) -> dict[str, Any] | None: 146 - """Parse agent JSONL file and extract metadata. 145 + def _parse_use_file(use_file: Path) -> dict[str, Any] | None: 146 + """Parse a use JSONL file and extract metadata. 147 147 148 148 Returns dict with: id, name, start, status, prompt, facet, failed, 149 149 runtime_seconds, thinking_count, tool_count, cost, model, provider, 150 150 error_message. 151 151 Returns None if file cannot be parsed. 152 152 """ 153 - from think.cortex_client import get_agent_end_state 153 + from think.cortex_client import get_use_end_state 154 154 155 155 try: 156 - with open(agent_file, "r") as f: 156 + with open(use_file, "r") as f: 157 157 lines = f.readlines() 158 158 159 159 if not lines: ··· 167 167 if request_event.get("event") != "request": 168 168 return None 169 169 170 - # Extract agent ID from filename 171 - is_active = "_active.jsonl" in agent_file.name 172 - agent_id = agent_file.stem.replace("_active", "") 170 + is_active = "_active.jsonl" in use_file.name 171 + use_id = use_file.stem.replace("_active", "") 173 172 174 173 # Parse events using shared helper 175 - event_data = _parse_agent_events(lines[1:]) 174 + event_data = _parse_use_events(lines[1:]) 176 175 177 - agent_info: dict[str, Any] = { 178 - "id": agent_id, 176 + use_info: dict[str, Any] = { 177 + "id": use_id, 179 178 "name": request_event.get("name", "unified"), 180 179 "start": request_event.get("ts", 0), 181 180 "status": "running" if is_active else "completed", ··· 203 202 output_file = str(out_path.relative_to(day_dir)) 204 203 else: 205 204 output_file = str(out_path.relative_to(state.journal_root)) 206 - agent_info["output_file"] = output_file 205 + use_info["output_file"] = output_file 207 206 208 - # For completed agents, determine end state and calculate cost 207 + # For completed uses, determine end state and calculate cost 209 208 if not is_active: 210 - end_state = get_agent_end_state(agent_id) 211 - agent_info["failed"] = end_state in ("error", "unknown") 209 + end_state = get_use_end_state(use_id) 210 + use_info["failed"] = end_state in ("error", "unknown") 212 211 213 212 # Calculate runtime from finish or error timestamp 214 213 end_ts = event_data["finish_ts"] or event_data["error_ts"] 215 - if end_ts and agent_info["start"]: 216 - agent_info["runtime_seconds"] = (end_ts - agent_info["start"]) / 1000.0 214 + if end_ts and use_info["start"]: 215 + use_info["runtime_seconds"] = (end_ts - use_info["start"]) / 1000.0 217 216 218 217 # Calculate cost 219 - agent_info["cost"] = calc_agent_cost( 220 - event_data["model"], event_data["usage"] 221 - ) 218 + use_info["cost"] = calc_agent_cost(event_data["model"], event_data["usage"]) 222 219 223 - return agent_info 220 + return use_info 224 221 except (json.JSONDecodeError, IOError): 225 222 return None 226 223 227 224 228 - def _get_agent_day(agent_file: Path) -> str: 229 - """Get the logical day for an agent from its request event. 225 + def _get_use_day(use_file: Path) -> str: 226 + """Get the logical day for a use from its request event. 230 227 231 228 Prefers the ``day`` field from the request event (the day being processed) 232 - over the agent_id timestamp (when the agent actually ran). This ensures 233 - overnight dream agents appear under the day they processed. 229 + over the use_id timestamp (when the agent actually ran). This ensures 230 + overnight dream uses appear under the day they processed. 234 231 """ 235 - agent_id = agent_file.stem.replace("_active", "") 232 + use_id = use_file.stem.replace("_active", "") 236 233 try: 237 - with open(agent_file, "r") as f: 234 + with open(use_file, "r") as f: 238 235 first_line = f.readline().strip() 239 236 if first_line: 240 237 request_event = json.loads(first_line) ··· 243 240 return req_day 244 241 except (json.JSONDecodeError, IOError): 245 242 pass 246 - return _agent_id_to_day(agent_id) 243 + return _use_id_to_day(use_id) 247 244 248 245 249 - def _get_agents_for_day(day: str, facet_filter: str | None = None) -> list[dict]: 250 - """Get all agent runs for a specific day. 246 + def _get_uses_for_day(day: str, facet_filter: str | None = None) -> list[dict]: 247 + """Get all talent uses for a specific day. 251 248 252 - Uses the day index file for fast lookup instead of scanning all agent files. 249 + Uses the day index file for fast lookup instead of scanning all use files. 253 250 254 251 Args: 255 252 day: YYYYMMDD day string 256 253 facet_filter: Optional facet to filter by (None = all facets) 257 254 258 255 Returns: 259 - List of agent info dicts sorted by start time (newest first) 256 + List of use info dicts sorted by start time (newest first) 260 257 """ 261 - agents_dir = Path(state.journal_root) / "agents" 262 - if not agents_dir.exists(): 258 + talents_dir = Path(state.journal_root) / "talents" 259 + if not talents_dir.exists(): 263 260 return [] 264 261 265 - agents = [] 262 + uses = [] 266 263 267 - # Read day index for completed agents 268 - day_index_path = agents_dir / f"{day}.jsonl" 264 + # Read day index for completed uses 265 + day_index_path = talents_dir / f"{day}.jsonl" 269 266 if day_index_path.exists(): 270 267 try: 271 268 with open(day_index_path, "r") as f: ··· 283 280 continue 284 281 285 282 # Locate the actual file for full parsing 286 - agent_id = entry.get("agent_id", "") 283 + use_id = entry.get("use_id", "") 287 284 name = entry.get("name", "unified") 288 285 safe_name = name.replace(":", "--") 289 - agent_file = agents_dir / safe_name / f"{agent_id}.jsonl" 290 - if not agent_file.exists(): 286 + use_file = talents_dir / safe_name / f"{use_id}.jsonl" 287 + if not use_file.exists(): 291 288 continue 292 289 293 - agent_info = _parse_agent_file(agent_file) 294 - if agent_info: 295 - agents.append(agent_info) 290 + use_info = _parse_use_file(use_file) 291 + if use_info: 292 + uses.append(use_info) 296 293 except IOError: 297 294 pass 298 295 299 - # Also check for running agents (only have _active files, no day index entry yet) 300 - for agent_file in agents_dir.glob("*/*_active.jsonl"): 301 - if "_pending" in agent_file.name: 296 + # Also check for running uses (only have _active files, no day index entry yet) 297 + for use_file in talents_dir.glob("*/*_active.jsonl"): 298 + if "_pending" in use_file.name: 302 299 continue 303 - if _get_agent_day(agent_file) != day: 300 + if _get_use_day(use_file) != day: 304 301 continue 305 302 306 - agent_info = _parse_agent_file(agent_file) 307 - if not agent_info: 303 + use_info = _parse_use_file(use_file) 304 + if not use_info: 308 305 continue 309 306 310 - if facet_filter is not None and agent_info.get("facet") != facet_filter: 307 + if facet_filter is not None and use_info.get("facet") != facet_filter: 311 308 continue 312 309 313 - agents.append(agent_info) 310 + uses.append(use_info) 314 311 315 312 # Sort by start time (newest first) 316 - agents.sort(key=lambda x: x["start"], reverse=True) 317 - return agents 313 + uses.sort(key=lambda x: x["start"], reverse=True) 314 + return uses 318 315 319 316 320 317 @lru_cache(maxsize=1) 321 - def _build_agents_meta() -> dict[str, dict[str, Any]]: 322 - """Build agent metadata dict from all talent configs. 318 + def _build_talents_meta() -> dict[str, dict[str, Any]]: 319 + """Build talent metadata dict from all talent configs. 323 320 324 - Returns dict mapping agent name to metadata with capability fields 321 + Returns dict mapping talent name to metadata with capability fields 325 322 for frontend display. Cached for process lifetime since talent configs 326 323 are static. 327 324 """ 328 325 configs = get_talent_configs(include_disabled=True) 329 - agents: dict[str, dict[str, Any]] = {} 326 + talents: dict[str, dict[str, Any]] = {} 330 327 331 328 for name, config in configs.items(): 332 - agents[name] = { 329 + talents[name] = { 333 330 "title": config.get("title", name), 334 331 "description": config.get("description"), 335 332 "color": config.get("color", "#6c757d"), ··· 341 338 "multi_facet": bool(config.get("multi_facet")), 342 339 } 343 340 344 - return agents 341 + return talents 345 342 346 343 347 344 # ============================================================================= ··· 351 348 352 349 @sol_bp.route("/") 353 350 def index() -> Any: 354 - """Redirect to today's agent history.""" 351 + """Redirect to today's talent history.""" 355 352 today = date.today().strftime("%Y%m%d") 356 - return redirect(url_for("app:sol.agents_day", day=today)) 353 + return redirect(url_for("app:sol.talents_day", day=today)) 357 354 358 355 359 356 @sol_bp.route("/<day>") 360 - def agents_day(day: str) -> str: 361 - """Render agent history viewer for a specific day.""" 357 + def talents_day(day: str) -> str: 358 + """Render talent history viewer for a specific day.""" 362 359 if not DATE_RE.fullmatch(day): 363 360 return "", 404 364 361 ··· 372 369 # ============================================================================= 373 370 374 371 375 - @sol_bp.route("/api/agents/<day>") 376 - def api_agents_day(day: str) -> Any: 377 - """Get agent runs and metadata for a specific day. 372 + @sol_bp.route("/api/talents/<day>") 373 + def api_talents_day(day: str) -> Any: 374 + """Get talent uses and metadata for a specific day. 378 375 379 376 Returns flat data for frontend grouping/rendering. 380 377 ··· 383 380 384 381 Returns: 385 382 { 386 - "runs": [run objects...], 387 - "agents": {name: metadata...}, 383 + "uses": [use objects...], 384 + "talents": {name: metadata...}, 388 385 "facets": {name: {title, color}...} 389 386 } 390 387 """ ··· 393 390 394 391 facet_filter = _get_facet_filter() 395 392 396 - runs = _get_agents_for_day(day, facet_filter) 397 - agents = _build_agents_meta() 393 + uses = _get_uses_for_day(day, facet_filter) 394 + talents = _build_talents_meta() 398 395 facets = { 399 396 name: {"title": f.get("title", name), "color": f.get("color")} 400 397 for name, f in get_facets().items() ··· 402 399 403 400 return jsonify( 404 401 { 405 - "runs": runs, 406 - "agents": agents, 402 + "uses": uses, 403 + "talents": talents, 407 404 "facets": facets, 408 405 } 409 406 ) 410 407 411 408 412 - @sol_bp.route("/api/run/<agent_id>") 413 - def api_agent_run(agent_id: str) -> Any: 414 - """Return full agent run detail with metadata and parsed events.""" 415 - # Locate the agent JSONL file 409 + @sol_bp.route("/api/run/<use_id>") 410 + def api_agent_run(use_id: str) -> Any: 411 + """Return full talent-use detail with metadata and parsed events.""" 412 + # Locate the use JSONL file 416 413 journal_path = Path(state.journal_root) 417 - agents_dir = journal_path / "agents" 418 - # Search subdirectories for the agent file 419 - agent_file = None 420 - for match in agents_dir.glob(f"*/{agent_id}.jsonl"): 421 - agent_file = match 414 + talents_dir = journal_path / "talents" 415 + # Search subdirectories for the use file 416 + use_file = None 417 + for match in talents_dir.glob(f"*/{use_id}.jsonl"): 418 + use_file = match 422 419 break 423 420 424 - if not agent_file: 425 - # Check if the agent is still running 426 - for match in agents_dir.glob(f"*/{agent_id}_active.jsonl"): 427 - return jsonify({"error": "Agent run is still in progress"}), 202 428 - return jsonify({"error": f"Agent run {agent_id} not found"}), 404 421 + if not use_file: 422 + for match in talents_dir.glob(f"*/{use_id}_active.jsonl"): 423 + return jsonify({"error": "Talent run is still in progress"}), 202 424 + return jsonify({"error": f"Talent run {use_id} not found"}), 404 429 425 430 426 try: 431 - from think.cortex_client import get_agent_end_state 427 + from think.cortex_client import get_use_end_state 432 428 433 - with open(agent_file, "r", encoding="utf-8") as f: 429 + with open(use_file, "r", encoding="utf-8") as f: 434 430 lines = f.readlines() 435 431 436 432 if not lines: 437 - return jsonify({"error": f"Agent run {agent_id} is malformed"}), 500 433 + return jsonify({"error": f"Talent run {use_id} is malformed"}), 500 438 434 439 435 first_line = lines[0].strip() 440 436 if not first_line: 441 - return jsonify({"error": f"Agent run {agent_id} is malformed"}), 500 437 + return jsonify({"error": f"Talent run {use_id} is malformed"}), 500 442 438 443 439 request_event = json.loads(first_line) 444 440 if request_event.get("event") != "request": 445 - return jsonify({"error": f"Agent run {agent_id} is malformed"}), 500 441 + return jsonify({"error": f"Talent run {use_id} is malformed"}), 500 446 442 447 - event_data = _parse_agent_events(lines[1:], collect_events=True) 443 + event_data = _parse_use_events(lines[1:], collect_events=True) 448 444 449 445 output_file = None 450 446 req_output = request_event.get("output") ··· 464 460 if end_ts and start_ts: 465 461 runtime_seconds = (end_ts - start_ts) / 1000.0 466 462 467 - end_state = get_agent_end_state(agent_id) 463 + end_state = get_use_end_state(use_id) 468 464 469 465 run: dict[str, Any] = { 470 - "id": agent_id, 466 + "id": use_id, 471 467 "name": request_event.get("name", "unified"), 472 468 "start": start_ts, 473 469 "status": "completed", ··· 484 480 "output_file": output_file, 485 481 "events": event_data.get("events", []), 486 482 } 487 - run["day"] = request_event.get("day") or _agent_id_to_day(agent_id) 483 + run["day"] = request_event.get("day") or _use_id_to_day(use_id) 488 484 return jsonify(run) 489 485 except Exception as e: 490 486 return jsonify({"error": str(e)}), 500 ··· 498 494 Path is validated to stay within the journal directory. 499 495 500 496 Supports two path styles: 501 - - Day-relative: ``agents/flow.md`` → resolved under ``{day}/`` 497 + - Day-relative: ``talents/flow.md`` → resolved under ``{day}/`` 502 498 - Journal-relative: ``facets/work/activities/...`` → resolved under journal root 503 499 """ 504 500 if not DATE_RE.fullmatch(day): ··· 550 546 } 551 547 """ 552 548 try: 553 - from think.talent import get_agent 549 + from think.talent import get_talent 554 550 555 - config = get_agent(name) 551 + config = get_talent(name) 556 552 557 553 system_instruction = config.get("system_instruction", "") 558 554 extra_context = config.get("extra_context", "") ··· 576 572 } 577 573 ) 578 574 except FileNotFoundError: 579 - return jsonify({"error": f"Agent '{name}' not found"}), 404 575 + return jsonify({"error": f"Talent '{name}' not found"}), 404 580 576 except Exception as e: 581 577 return jsonify({"error": str(e)}), 500 582 578 583 579 584 580 @sol_bp.route("/api/stats/<month>") 585 581 def api_stats(month: str) -> Any: 586 - """Return agent run counts per day per facet for a month. 582 + """Return talent-use counts per day per facet for a month. 587 583 588 584 Args: 589 585 month: YYYYMM format month string ··· 595 591 if not re.fullmatch(r"\d{6}", month): 596 592 return jsonify({"error": "Invalid month format, expected YYYYMM"}), 400 597 593 598 - agents_dir = Path(state.journal_root) / "agents" 599 - if not agents_dir.exists(): 594 + talents_dir = Path(state.journal_root) / "talents" 595 + if not talents_dir.exists(): 600 596 return jsonify({}) 601 597 602 598 stats: dict[str, dict[str, int]] = {} 603 599 604 600 # Read day index files for the month 605 - for day_index_file in agents_dir.glob(f"{month}*.jsonl"): 601 + for day_index_file in talents_dir.glob(f"{month}*.jsonl"): 606 602 day = day_index_file.stem 607 603 if not re.fullmatch(r"\d{8}", day): 608 604 continue ··· 630 626 631 627 @sol_bp.route("/api/badge-count") 632 628 def api_badge_count() -> Any: 633 - """Get count of failed agent runs for today (for app icon badge).""" 629 + """Get count of failed talent runs for today (for app icon badge).""" 634 630 today = date.today().strftime("%Y%m%d") 635 - agents = _get_agents_for_day(today, facet_filter=None) 636 - failed_count = sum(1 for a in agents if a.get("failed")) 631 + uses = _get_uses_for_day(today, facet_filter=None) 632 + failed_count = sum(1 for a in uses if a.get("failed")) 637 633 return jsonify({"count": failed_count}) 638 634 639 635 ··· 649 645 650 646 @sol_bp.route("/api/identity") 651 647 def api_identity() -> Any: 652 - """Return agent identity and thickness signals.""" 648 + """Return talent identity and thickness signals.""" 653 649 try: 654 650 from think.awareness import compute_thickness 655 651 from think.utils import get_config
+17 -17
apps/sol/workspace.html
··· 766 766 padding: 0; 767 767 } 768 768 769 - /* Flow events (agent_updated, continue) */ 769 + /* Flow events (talent_updated, continue) */ 770 770 .event-flow { 771 771 border-left-color: #f9a825; 772 772 background: #fffde7; ··· 1387 1387 .catch(() => { banner.style.display = 'none'; }); 1388 1388 } 1389 1389 1390 - async function loadAgents() { 1390 + async function loadTalents() { 1391 1391 currentDay = getDayFromUrl(); 1392 1392 if (!currentDay) return; 1393 1393 ··· 1396 1396 document.getElementById('agents-status').textContent = ''; 1397 1397 1398 1398 try { 1399 - const response = await fetch(`api/agents/${currentDay}`); 1399 + const response = await fetch(`api/talents/${currentDay}`); 1400 1400 const data = await response.json(); 1401 1401 1402 - allRuns = data.runs || []; 1403 - agentsMeta = data.agents || {}; 1402 + allRuns = data.uses || []; 1403 + agentsMeta = data.talents || {}; 1404 1404 facetsMeta = data.facets || {}; 1405 1405 1406 1406 renderGridView(); 1407 1407 document.getElementById('agents-status').textContent = 1408 - allRuns.length + ' agent run' + (allRuns.length !== 1 ? 's' : '') + ' loaded'; 1408 + allRuns.length + ' talent run' + (allRuns.length !== 1 ? 's' : '') + ' loaded'; 1409 1409 1410 1410 // Restore hash-based view after data load 1411 1411 if (location.hash) { 1412 1412 handleHashChange(); 1413 1413 } 1414 1414 } catch (error) { 1415 - console.error('Error loading agents:', error); 1415 + console.error('Error loading talents:', error); 1416 1416 document.getElementById('loading-view').innerHTML = 1417 1417 '<div class="empty-state" aria-live="polite">' + 1418 1418 '<div class="empty-state-icon">⚠️</div>' + 1419 - '<div class="empty-state-text">Unable to load agents</div>' + 1419 + '<div class="empty-state-text">Unable to load talents</div>' + 1420 1420 '<div class="empty-state-hint">The server may be temporarily unavailable. Check your connection and try again.</div>' + 1421 - '<button class="back-btn" onclick="loadAgents()" style="margin-top: 1rem;">Try again</button>' + 1421 + '<button class="back-btn" onclick="loadTalents()" style="margin-top: 1rem;">Try again</button>' + 1422 1422 '</div>'; 1423 1423 } 1424 1424 } ··· 1533 1533 const badges = document.createElement('div'); 1534 1534 badges.className = 'agent-card-badges'; 1535 1535 1536 - const successCount = agent.run_count - agent.failed_count; 1536 + const successCount = agent.run_count - talent.failed_count; 1537 1537 if (successCount > 0) { 1538 1538 const successBadge = document.createElement('span'); 1539 1539 successBadge.className = 'badge badge-success'; ··· 1541 1541 badges.appendChild(successBadge); 1542 1542 } 1543 1543 1544 - if (agent.failed_count > 0) { 1544 + if (talent.failed_count > 0) { 1545 1545 const failBadge = document.createElement('span'); 1546 1546 failBadge.className = 'badge badge-failed'; 1547 - failBadge.textContent = '✗ ' + agent.failed_count; 1547 + failBadge.textContent = '✗ ' + talent.failed_count; 1548 1548 badges.appendChild(failBadge); 1549 1549 } 1550 1550 ··· 2204 2204 timeline.appendChild(renderInfoEvent(event)); 2205 2205 } else if (type === 'finish') { 2206 2206 timeline.appendChild(renderFinishEvent(event)); 2207 - } else if (type === 'agent_updated') { 2208 - timeline.appendChild(renderFlowEvent('Switched to agent: ' + (event.agent || 'unknown'), event.ts)); 2207 + } else if (type === 'talent_updated') { 2208 + timeline.appendChild(renderFlowEvent('Switched to talent: ' + (event.talent || 'unknown'), event.ts)); 2209 2209 } else if (type === 'continue') { 2210 - timeline.appendChild(renderFlowEvent('Continued in agent: ' + (event.to || 'unknown'), event.ts)); 2210 + timeline.appendChild(renderFlowEvent('Continued in talent: ' + (event.to || 'unknown'), event.ts)); 2211 2211 } 2212 2212 } 2213 2213 ··· 2527 2527 // Listen for facet changes 2528 2528 window.addEventListener('facet.switch', () => { 2529 2529 loadUpdatedBanner(); 2530 - loadAgents(); 2530 + loadTalents(); 2531 2531 }); 2532 2532 2533 2533 // Initial load 2534 2534 loadIdentity(); 2535 2535 loadUpdatedBanner(); 2536 - loadAgents(); 2536 + loadTalents(); 2537 2537 })(); 2538 2538 </script>
+4 -4
apps/speakers/attribution.py
··· 124 124 screen.md captures video-call participant panels. The content is 125 125 free-form markdown so extraction is best-effort. 126 126 """ 127 - screen_path = seg_dir / "agents" / "screen.md" 127 + screen_path = seg_dir / "talents" / "screen.md" 128 128 if not screen_path.exists(): 129 129 return [] 130 130 try: ··· 159 159 160 160 def _extract_meeting_participants(day: str, segment_key: str) -> list[str]: 161 161 """Extract participant names from daily meetings.md.""" 162 - meetings_path = day_path(day) / "agents" / "meetings.md" 162 + meetings_path = day_path(day) / "talents" / "meetings.md" 163 163 if not meetings_path.exists(): 164 164 return [] 165 165 try: ··· 468 468 sentence that was corrected by the user keeps the corrected attribution 469 469 rather than being overwritten by a fresh pipeline run. 470 470 """ 471 - agents_dir = seg_dir / "agents" 471 + agents_dir = seg_dir / "talents" 472 472 agents_dir.mkdir(parents=True, exist_ok=True) 473 473 474 474 # Load existing corrections to preserve user overrides ··· 653 653 654 654 def _has_speaker_labels(seg_dir: Path) -> bool: 655 655 """Check if the segment already has speaker_labels.json.""" 656 - return (seg_dir / "agents" / "speaker_labels.json").exists() 656 + return (seg_dir / "talents" / "speaker_labels.json").exists() 657 657 658 658 659 659 def backfill_segments(
+1 -1
apps/speakers/bootstrap.py
··· 555 555 for day in sorted(day_dirs().keys()): 556 556 for _stream, _seg_key, seg_path in iter_segments(day): 557 557 segments_scanned += 1 558 - agents_dir = seg_path / "agents" 558 + agents_dir = seg_path / "talents" 559 559 560 560 # Rewrite speaker_labels.json 561 561 labels_path = agents_dir / "speaker_labels.json"
+12 -12
apps/speakers/routes.py
··· 118 118 Returns: 119 119 List of speaker name strings, or empty list if not found/invalid. 120 120 """ 121 - speakers_path = segment_dir / "agents" / "speakers.json" 121 + speakers_path = segment_dir / "talents" / "speakers.json" 122 122 if not speakers_path.exists(): 123 123 return [] 124 124 ··· 303 303 304 304 305 305 def _load_speaker_labels(segment_dir: Path) -> dict | None: 306 - """Load speaker_labels.json from a segment's agents/ directory. 306 + """Load speaker_labels.json from a segment's talents/ directory. 307 307 308 308 Returns the parsed JSON dict, or None if not found/invalid. 309 309 """ 310 - labels_path = segment_dir / "agents" / "speaker_labels.json" 310 + labels_path = segment_dir / "talents" / "speaker_labels.json" 311 311 if not labels_path.is_file(): 312 312 return None 313 313 try: ··· 318 318 319 319 320 320 def _save_speaker_labels(segment_dir: Path, labels_data: dict) -> None: 321 - """Atomically write speaker_labels.json to a segment's agents/ directory.""" 322 - agents_dir = segment_dir / "agents" 323 - agents_dir.mkdir(parents=True, exist_ok=True) 324 - out_path = agents_dir / "speaker_labels.json" 321 + """Atomically write speaker_labels.json to a segment's talents/ directory.""" 322 + talents_dir = segment_dir / "talents" 323 + talents_dir.mkdir(parents=True, exist_ok=True) 324 + out_path = talents_dir / "speaker_labels.json" 325 325 tmp_path = out_path.with_suffix(".tmp") 326 326 with open(tmp_path, "w", encoding="utf-8") as f: 327 327 json.dump(labels_data, f, indent=2) ··· 329 329 330 330 331 331 def _load_speaker_corrections(segment_dir: Path) -> list[dict]: 332 - """Load speaker_corrections.json from a segment's agents/ directory. 332 + """Load speaker_corrections.json from a segment's talents/ directory. 333 333 334 334 Returns list of correction entries, or empty list if not found. 335 335 """ 336 - corr_path = segment_dir / "agents" / "speaker_corrections.json" 336 + corr_path = segment_dir / "talents" / "speaker_corrections.json" 337 337 if not corr_path.is_file(): 338 338 return [] 339 339 try: ··· 348 348 """Append a correction entry to speaker_corrections.json (atomic write).""" 349 349 corrections = _load_speaker_corrections(segment_dir) 350 350 corrections.append(correction) 351 - agents_dir = segment_dir / "agents" 352 - agents_dir.mkdir(parents=True, exist_ok=True) 353 - out_path = agents_dir / "speaker_corrections.json" 351 + talents_dir = segment_dir / "talents" 352 + talents_dir.mkdir(parents=True, exist_ok=True) 353 + out_path = talents_dir / "speaker_corrections.json" 354 354 tmp_path = out_path.with_suffix(".tmp") 355 355 with open(tmp_path, "w", encoding="utf-8") as f: 356 356 json.dump({"corrections": corrections}, f, indent=2)
+1 -1
apps/speakers/status.py
··· 179 179 for seg_dir in sorted(stream_dir.iterdir()): 180 180 if not seg_dir.is_dir(): 181 181 continue 182 - labels_file = seg_dir / "agents" / "speaker_labels.json" 182 + labels_file = seg_dir / "talents" / "speaker_labels.json" 183 183 if not labels_file.exists(): 184 184 continue 185 185 try:
+3 -3
apps/speakers/suggest.py
··· 54 54 55 55 56 56 def _parse_meetings(day_path: str) -> list[dict[str, Any]]: 57 - meetings_path = Path(day_path) / "agents" / "meetings.md" 57 + meetings_path = Path(day_path) / "talents" / "meetings.md" 58 58 if not meetings_path.exists(): 59 59 return [] 60 60 ··· 296 296 297 297 for day in sorted(day_dirs().keys()): 298 298 for stream, segment_key, seg_path in iter_segments(day): 299 - labels_path = seg_path / "agents" / "speaker_labels.json" 299 + labels_path = seg_path / "talents" / "speaker_labels.json" 300 300 if not labels_path.exists(): 301 301 continue 302 302 try: ··· 323 323 if medium_or_null <= 10: 324 324 continue 325 325 326 - speakers_path = seg_path / "agents" / "speakers.json" 326 + speakers_path = seg_path / "talents" / "speakers.json" 327 327 has_speakers = speakers_path.is_file() 328 328 null_proportion = null_count / total if total else 0.0 329 329 results.append(
+3 -3
apps/speakers/tests/conftest.py
··· 188 188 segment_key: Segment key (HHMMSS_LEN) 189 189 speakers: List of speaker names 190 190 """ 191 - agents_dir = self.journal / day / STREAM / segment_key / "agents" 191 + agents_dir = self.journal / day / STREAM / segment_key / "talents" 192 192 agents_dir.mkdir(parents=True, exist_ok=True) 193 193 194 194 speakers_path = agents_dir / "speakers.json" ··· 214 214 metadata: Optional extra metadata (owner_centroid_version, 215 215 voiceprint_versions) 216 216 """ 217 - agents_dir = self.journal / day / STREAM / segment_key / "agents" 217 + agents_dir = self.journal / day / STREAM / segment_key / "talents" 218 218 agents_dir.mkdir(parents=True, exist_ok=True) 219 219 220 220 data = {"labels": labels} ··· 248 248 stream: Optional stream name (defaults to STREAM) 249 249 """ 250 250 agents_dir = ( 251 - self.journal / day / (stream or STREAM) / segment_key / "agents" 251 + self.journal / day / (stream or STREAM) / segment_key / "talents" 252 252 ) 253 253 agents_dir.mkdir(parents=True, exist_ok=True) 254 254
+3 -3
apps/speakers/tests/test_attribution.py
··· 171 171 seg_dir = _write_controlled_segment(env, "20240101", "090000_300", embeddings) 172 172 173 173 # speakers.json with exactly 1 speaker 174 - agents_dir = seg_dir / "agents" 174 + agents_dir = seg_dir / "talents" 175 175 agents_dir.mkdir(parents=True, exist_ok=True) 176 176 (agents_dir / "speakers.json").write_text(json.dumps(["Ryan Bennett"])) 177 177 ··· 535 535 env, "20260201", "090000_300", np.vstack([_normalized([1.0, 0.0])]) 536 536 ) 537 537 # Pre-create speaker_labels.json 538 - agents_dir = seg_dir / "agents" 538 + agents_dir = seg_dir / "talents" 539 539 agents_dir.mkdir(parents=True, exist_ok=True) 540 540 (agents_dir / "speaker_labels.json").write_text('{"labels": []}') 541 541 ··· 584 584 # Labels written 585 585 for day, seg_key in [("20260201", "080000_300"), ("20260210", "090000_300")]: 586 586 labels_path = ( 587 - env.journal / day / STREAM / seg_key / "agents" / "speaker_labels.json" 587 + env.journal / day / STREAM / seg_key / "talents" / "speaker_labels.json" 588 588 ) 589 589 assert labels_path.exists() 590 590
+3 -3
apps/speakers/tests/test_discovery.py
··· 110 110 111 111 def _load_corrections_count(journal: Path, day: str, segment_key: str) -> int: 112 112 """Return number of correction entries for a segment.""" 113 - path = journal / day / "test" / segment_key / "agents" / "speaker_corrections.json" 113 + path = journal / day / "test" / segment_key / "talents" / "speaker_corrections.json" 114 114 if not path.exists(): 115 115 return 0 116 116 return len(json.loads(path.read_text(encoding="utf-8")).get("corrections", [])) ··· 199 199 200 200 for day, segment_key, _sentence_count in segments: 201 201 labels_path = ( 202 - env.journal / day / "test" / segment_key / "agents" / "speaker_labels.json" 202 + env.journal / day / "test" / segment_key / "talents" / "speaker_labels.json" 203 203 ) 204 204 corrections_path = ( 205 205 env.journal 206 206 / day 207 207 / "test" 208 208 / segment_key 209 - / "agents" 209 + / "talents" 210 210 / "speaker_corrections.json" 211 211 ) 212 212 labels_data = json.loads(labels_path.read_text(encoding="utf-8"))
+6 -6
apps/speakers/tests/test_merge_names.py
··· 127 127 / "20240101" 128 128 / STREAM 129 129 / "143022_300" 130 - / "agents" 130 + / "talents" 131 131 / "speaker_labels.json" 132 132 ) 133 133 with open(labels_path) as f: ··· 142 142 / "20240101" 143 143 / STREAM 144 144 / "143022_300" 145 - / "agents" 145 + / "talents" 146 146 / "speaker_corrections.json" 147 147 ) 148 148 with open(corr_path) as f: ··· 418 418 / "20240101" 419 419 / STREAM 420 420 / "143022_300" 421 - / "agents" 421 + / "talents" 422 422 / "speaker_labels.json" 423 423 ) 424 424 with open(labels_path) as f: ··· 459 459 / "20240101" 460 460 / STREAM 461 461 / "143022_300" 462 - / "agents" 462 + / "talents" 463 463 / "speaker_corrections.json" 464 464 ) 465 465 with open(corr_path) as f: ··· 493 493 / "20240101" 494 494 / STREAM 495 495 / "143022_300" 496 - / "agents" 496 + / "talents" 497 497 / "speaker_labels.json" 498 498 ) 499 499 mtime_before = labels_path.stat().st_mtime_ns ··· 511 511 env.create_entity("Corrupt Canon") 512 512 513 513 # Write corrupted file containing the alias_id string 514 - agents_dir = env.journal / "20240101" / STREAM / "143022_300" / "agents" 514 + agents_dir = env.journal / "20240101" / STREAM / "143022_300" / "talents" 515 515 agents_dir.mkdir(parents=True, exist_ok=True) 516 516 (agents_dir / "speaker_labels.json").write_text("corrupt_alias {not valid json") 517 517
+7 -7
apps/speakers/tests/test_routes.py
··· 327 327 328 328 env = speakers_env() 329 329 segment_dir = env.journal / "20240101" / "test" / "143022_300" 330 - agents_dir = segment_dir / "agents" 330 + agents_dir = segment_dir / "talents" 331 331 agents_dir.mkdir(parents=True) 332 332 333 333 # Write invalid JSON ··· 346 346 347 347 env = speakers_env() 348 348 segment_dir = env.journal / "20240101" / "test" / "143022_300" 349 - agents_dir = segment_dir / "agents" 349 + agents_dir = segment_dir / "talents" 350 350 agents_dir.mkdir(parents=True) 351 351 352 352 # Write object instead of list ··· 583 583 / "20240101" 584 584 / "test" 585 585 / "143022_300" 586 - / "agents" 586 + / "talents" 587 587 / "speaker_corrections.json" 588 588 ) 589 589 corr_path.write_text( ··· 668 668 / "20240101" 669 669 / "test" 670 670 / "143022_300" 671 - / "agents" 671 + / "talents" 672 672 / "speaker_labels.json" 673 673 ) 674 674 with open(labels_path) as f: ··· 682 682 / "20240101" 683 683 / "test" 684 684 / "143022_300" 685 - / "agents" 685 + / "talents" 686 686 / "speaker_corrections.json" 687 687 ) 688 688 assert corr_path.exists() ··· 826 826 / "20240101" 827 827 / "test" 828 828 / "143022_300" 829 - / "agents" 829 + / "talents" 830 830 / "speaker_labels.json" 831 831 ) 832 832 with open(labels_path) as f: ··· 969 969 / "20240101" 970 970 / "test" 971 971 / "143022_300" 972 - / "agents" 972 + / "talents" 973 973 / "speaker_labels.json" 974 974 ) 975 975 with open(labels_path) as f:
+1 -1
apps/speakers/tests/test_suggest.py
··· 18 18 19 19 20 20 def create_meetings_md(env, day: str, content: str) -> Path: 21 - meetings_path = env.journal / day / "agents" / "meetings.md" 21 + meetings_path = env.journal / day / "talents" / "meetings.md" 22 22 meetings_path.parent.mkdir(parents=True, exist_ok=True) 23 23 meetings_path.write_text(content, encoding="utf-8") 24 24 return meetings_path
+3 -3
apps/stats/static/dashboard.js
··· 5 5 const Dashboard = (function() { 6 6 'use strict'; 7 7 8 - const EXPECTED_SCHEMA_VERSION = 2; 8 + const EXPECTED_SCHEMA_VERSION = 3; 9 9 const DISPLAY_LABELS = { transcript: 'Audio', percept: 'Screen' }; 10 10 11 11 // DOM element factory ··· 576 576 } 577 577 578 578 // Required-field validation (blocking — stops rendering if fields missing) 579 - const requiredFields = ['days', 'totals', 'heatmap', 'tokens', 'agents', 'facets']; 579 + const requiredFields = ['days', 'totals', 'heatmap', 'tokens', 'talents', 'facets']; 580 580 const missingFields = requiredFields.filter(f => !(f in stats)); 581 581 if (missingFields.length > 0) { 582 582 document.getElementById('notice').appendChild( ··· 725 725 // Render Events stacked bar chart 726 726 buildStackedCategoryChart( 727 727 document.getElementById('eventsChart'), 728 - stats.agents.counts_by_day || {}, 728 + stats.talents.counts_by_day || {}, 729 729 Object.assign({}, data.generators || {}, { 730 730 emptyIcon: '⚡', 731 731 emptyText: 'No event data recorded',
+25 -27
apps/todos/routes.py
··· 625 625 try: 626 626 from convey.utils import spawn_agent 627 627 628 - agent_id = spawn_agent( 628 + use_id = spawn_agent( 629 629 prompt=prompt, 630 630 name="todos:todo", 631 631 provider="openai", ··· 634 634 except Exception as exc: # pragma: no cover - network/agent failure 635 635 return jsonify({"error": f"Failed to spawn agent: {exc}"}), 500 636 636 637 - if agent_id is None: 637 + if use_id is None: 638 638 return jsonify({"error": "Failed to connect to agent service"}), 503 639 639 640 640 if not hasattr(state, "todo_generation_agents"): 641 641 state.todo_generation_agents = {} 642 - state.todo_generation_agents[day] = agent_id 642 + state.todo_generation_agents[day] = use_id 643 643 644 - return jsonify({"agent_id": agent_id, "status": "started"}) 644 + return jsonify({"use_id": use_id, "status": "started"}) 645 645 646 646 647 647 @todos_bp.route("/<day>/generation-status") ··· 650 650 return "", 404 651 651 652 652 facet = request.args.get("facet", "personal") 653 - agent_id = request.args.get("agent_id") 654 - if not agent_id and hasattr(state, "todo_generation_agents"): 655 - agent_id = state.todo_generation_agents.get(day) 653 + use_id = request.args.get("use_id") 654 + if not use_id and hasattr(state, "todo_generation_agents"): 655 + use_id = state.todo_generation_agents.get(day) 656 656 657 - if not agent_id: 658 - return jsonify({"status": "none", "agent_id": None}) 657 + if not use_id: 658 + return jsonify({"status": "none", "use_id": None}) 659 659 660 - from think.cortex_client import cortex_agents 660 + from think.cortex_client import cortex_uses 661 661 662 662 todo_path = _todo_path(day, facet) 663 663 664 - agents_dir = Path(state.journal_root) / "agents" 665 - agent_file = next(agents_dir.glob(f"*/{agent_id}.jsonl"), None) 664 + talents_dir = Path(state.journal_root) / "talents" 665 + use_file = next(talents_dir.glob(f"*/{use_id}.jsonl"), None) 666 666 667 - if agent_file and agent_file.exists(): 667 + if use_file and use_file.exists(): 668 668 if todo_path.exists(): 669 669 if ( 670 670 hasattr(state, "todo_generation_agents") ··· 672 672 ): 673 673 del state.todo_generation_agents[day] 674 674 return jsonify( 675 - {"status": "finished", "agent_id": agent_id, "todo_created": True} 675 + {"status": "finished", "use_id": use_id, "todo_created": True} 676 676 ) 677 - return jsonify( 678 - {"status": "finished", "agent_id": agent_id, "todo_created": False} 679 - ) 677 + return jsonify({"status": "finished", "use_id": use_id, "todo_created": False}) 680 678 681 679 try: 682 - response = cortex_agents(limit=100, offset=0) 680 + response = cortex_uses(limit=100, offset=0) 683 681 if response: 684 - agents = response.get("agents", []) 685 - for agent in agents: 686 - if agent.get("id") == agent_id: 687 - return jsonify({"status": "running", "agent_id": agent_id}) 688 - return jsonify({"status": "unknown", "agent_id": agent_id}) 682 + uses = response.get("uses", []) 683 + for use in uses: 684 + if use.get("id") == use_id: 685 + return jsonify({"status": "running", "use_id": use_id}) 686 + return jsonify({"status": "unknown", "use_id": use_id}) 689 687 except Exception: # pragma: no cover - external call failure 690 688 pass 691 689 692 - return jsonify({"status": "unknown", "agent_id": agent_id}) 690 + return jsonify({"status": "unknown", "use_id": use_id}) 693 691 694 692 695 693 @todos_bp.route("/<day>/generate-weekly/<facet>", methods=["POST"]) ··· 712 710 try: 713 711 from convey.utils import spawn_agent 714 712 715 - agent_id = spawn_agent( 713 + use_id = spawn_agent( 716 714 prompt=prompt, 717 715 name="todos:weekly", 718 716 provider="openai", ··· 721 719 except Exception as exc: # pragma: no cover - network/agent failure 722 720 return jsonify({"error": f"Failed to spawn agent: {exc}"}), 500 723 721 724 - if agent_id is None: 722 + if use_id is None: 725 723 return jsonify({"error": "Failed to connect to agent service"}), 503 726 724 727 - return jsonify({"agent_id": agent_id, "status": "started"}) 725 + return jsonify({"use_id": use_id, "status": "started"})
+1 -1
apps/todos/talent/daily.md
··· 10 10 "multi_facet": true, 11 11 "group": "Todos", 12 12 "load": { 13 - "agents": True, 13 + "talents": True, 14 14 "journal": True 15 15 } 16 16 }
+6 -6
apps/todos/workspace.html
··· 1285 1285 // Listen for cortex events (agent completion) 1286 1286 if (window.appEvents) { 1287 1287 window.appEvents.listen('cortex', msg => { 1288 - // Find button by agent_id 1289 - const btn = document.querySelector(`.facet-generate-btn[data-agent-id="${msg.agent_id}"]`); 1288 + // Find button by use_id 1289 + const btn = document.querySelector(`.facet-generate-btn[data-agent-id="${msg.use_id}"]`); 1290 1290 if (!btn) return; 1291 1291 1292 1292 if (msg.event === 'finish') { ··· 1321 1321 const day = btn.dataset.day; 1322 1322 const facet = btn.dataset.facet; 1323 1323 1324 - // If already has agent_id, navigate to agents page 1324 + // If already has use_id, navigate to agents page 1325 1325 if (btn.dataset.agentId) { 1326 1326 window.location.href = btn.dataset.agentUrl; 1327 1327 return; ··· 1342 1342 1343 1343 const data = await response.json(); 1344 1344 1345 - // Store agent_id for event matching and navigation 1346 - btn.dataset.agentId = data.agent_id; 1347 - btn.dataset.agentUrl = `/agents#${data.agent_id}`; 1345 + // Store use_id for event matching and navigation 1346 + btn.dataset.agentId = data.use_id; 1347 + btn.dataset.agentUrl = `/agents#${data.use_id}`; 1348 1348 1349 1349 } catch (error) { 1350 1350 showMessage('Failed to start todo generation', 'error');
+6 -6
apps/transcripts/routes.py
··· 224 224 warnings = 0 225 225 226 226 # Load speaker labels if available. 227 - speaker_labels_path = Path(segment_dir) / "agents" / "speaker_labels.json" 227 + speaker_labels_path = Path(segment_dir) / "talents" / "speaker_labels.json" 228 228 speaker_map: dict[int, dict] = {} 229 229 if speaker_labels_path.is_file(): 230 230 try: ··· 414 414 # Get cost data for this segment 415 415 cost_data = get_usage_cost(day, segment=segment_key) 416 416 417 - # Collect agent .md files 417 + # Collect talent .md files 418 418 md_files = {} 419 - agents_dir = Path(segment_dir) / "agents" 420 - if agents_dir.is_dir(): 421 - for md_path in sorted(agents_dir.rglob("*.md")): 419 + talents_dir = Path(segment_dir) / "talents" 420 + if talents_dir.is_dir(): 421 + for md_path in sorted(talents_dir.rglob("*.md")): 422 422 try: 423 - key = md_path.relative_to(agents_dir).with_suffix("").as_posix() 423 + key = md_path.relative_to(talents_dir).with_suffix("").as_posix() 424 424 md_files[key] = md_path.read_text() 425 425 except Exception: 426 426 continue
+2 -2
convey/apps.py
··· 95 95 96 96 journal = Path(get_journal()) 97 97 today = datetime.now().strftime("%Y%m%d") 98 - day_index = journal / "agents" / f"{today}.jsonl" 98 + day_index = journal / "talents" / f"{today}.jsonl" 99 99 if day_index.exists(): 100 100 errors: dict[str, float] = {} 101 101 successes: dict[str, float] = {} ··· 174 174 175 175 journal = Path(get_journal()) 176 176 today = datetime.now().strftime("%Y%m%d") 177 - agents_dir = journal / today / "agents" 177 + agents_dir = journal / today / "talents" 178 178 if agents_dir.is_dir(): 179 179 outputs = sorted(p.stem for p in agents_dir.glob("*.md")) 180 180 if outputs:
+7 -7
convey/templates/app.html
··· 452 452 // Subscribe to WS first, then check GET 453 453 if (window.appEvents) { 454 454 recoveryCleanup = window.appEvents.listen('cortex', function(msg) { 455 - if (msg.agent_id === agentId) { 455 + if (msg.use_id === agentId) { 456 456 if (recoveryWatchdog) { clearTimeout(recoveryWatchdog); recoveryWatchdog = null; } 457 457 recoveryWatchdog = setTimeout(function() { 458 458 recoverDeliver('', 'panel', 'request timed out. the server took too long to respond. try a shorter question, or check if solstone services are running.'); 459 459 }, 180000); 460 460 } 461 - if (msg.agent_id === agentId && msg.event === 'finish') { 461 + if (msg.use_id === agentId && msg.event === 'finish') { 462 462 var resp = msg.result || ''; 463 463 recoverDeliver(resp, msg.display || 'panel', null); 464 - } else if (msg.agent_id === agentId && msg.event === 'error') { 464 + } else if (msg.use_id === agentId && msg.event === 'error') { 465 465 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.'); 466 466 } 467 467 }); ··· 710 710 } 711 711 712 712 var data = await r.json(); 713 - var agentId = data.agent_id; 713 + var agentId = data.use_id; 714 714 if (!agentId) { 715 715 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.'); 716 716 return; ··· 751 751 if (window.appEvents) { 752 752 cleanupCortex = window.appEvents.listen('cortex', function(msg) { 753 753 // Reset inactivity watchdog and update thinking label for our agent 754 - if (msg.agent_id === agentId) { 754 + if (msg.use_id === agentId) { 755 755 var label = getProgressLabel(msg); 756 756 if (label) updateThinkingLabel(label); 757 757 startWatchdog(agentId); 758 758 } 759 759 760 760 // Handle finish/error for our agent 761 - if (msg.agent_id === agentId && msg.event === 'finish') { 761 + if (msg.use_id === agentId && msg.event === 'finish') { 762 762 var resp = msg.result || ''; 763 763 deliverResult(resp, msg.display || 'panel', null); 764 - } else if (msg.agent_id === agentId && msg.event === 'error') { 764 + } else if (msg.use_id === agentId && msg.event === 'error') { 765 765 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.'); 766 766 } 767 767 });
+10 -10
convey/triage.py
··· 37 37 """Accept a message from the conversation panel and spawn a triage agent. 38 38 39 39 Expects JSON: {message, app, path, facet} 40 - Returns JSON: {agent_id} 40 + Returns JSON: {use_id} 41 41 42 42 The agent runs asynchronously. The browser receives the result via 43 - WebSocket (cortex/finish event). For reload recovery, use GET /result/<agent_id>. 43 + WebSocket (cortex/finish event). For reload recovery, use GET /result/<use_id>. 44 44 45 45 All journals route to the unified talent. 46 46 """ ··· 96 96 config["path"] = path 97 97 config["user_message"] = message 98 98 99 - agent_id = spawn_agent( 99 + use_id = spawn_agent( 100 100 prompt=full_prompt, 101 101 name=agent_name, 102 102 provider=None, 103 103 config=config, 104 104 ) 105 - if agent_id is None: 105 + if use_id is None: 106 106 return error_response("Failed to connect to agent service", 503) 107 107 108 - return jsonify(agent_id=agent_id) 108 + return jsonify(use_id=use_id) 109 109 110 110 except Exception: 111 111 logger.exception("Triage request failed") 112 112 return error_response("Failed to process triage request", 500) 113 113 114 114 115 - @bp.route("/result/<agent_id>", methods=["GET"]) 116 - def triage_result(agent_id: str) -> Any: 115 + @bp.route("/result/<use_id>", methods=["GET"]) 116 + def triage_result(use_id: str) -> Any: 117 117 """Return the result of a completed triage agent. 118 118 119 119 Returns {response, display} if the agent has finished, 404 otherwise. 120 120 Used for page-reload recovery when the WebSocket may have missed the finish event. 121 121 """ 122 122 try: 123 - from think.cortex_client import read_agent_events 123 + from think.cortex_client import read_use_events 124 124 125 - events = read_agent_events(agent_id) 125 + events = read_use_events(use_id) 126 126 for event in reversed(events): 127 127 if event.get("event") == "finish": 128 128 result = event.get("result", "") ··· 130 130 except FileNotFoundError: 131 131 pass 132 132 except Exception: 133 - logger.debug("Failed to read triage result for %s", agent_id, exc_info=True) 133 + logger.debug("Failed to read triage result for %s", use_id, exc_info=True) 134 134 return jsonify(error="not found"), 404
+4 -4
convey/utils.py
··· 88 88 provider: Optional[str] = None, 89 89 config: Optional[dict[str, Any]] = None, 90 90 ) -> str | None: 91 - """Spawn a Cortex agent and return the agent_id. 91 + """Spawn a Cortex agent and return the use_id. 92 92 93 93 Thin wrapper around cortex_request that ensures imports are handled 94 - and returns the agent_id directly. 94 + and returns the use_id directly. 95 95 96 96 Args: 97 97 prompt: The task or question for the agent ··· 100 100 config: Additional configuration (max_tokens, facet, session_id, etc.) 101 101 102 102 Returns: 103 - agent_id string (timestamp-based), or None if the request could not be sent. 103 + use_id string (timestamp-based), or None if the request could not be sent. 104 104 105 105 Raises: 106 106 ValueError: If config is invalid ··· 245 245 246 246 Example: 247 247 return success_response() # Returns {"success": True} 248 - return success_response({"agent_id": "123"}) # Returns {"success": True, "agent_id": "123"} 248 + return success_response({"use_id": "123"}) # Returns {"success": True, "use_id": "123"} 249 249 """ 250 250 from flask import jsonify 251 251
+8 -8
docs/APPS.md
··· 281 281 - Create `talent/` directory with `.md` files containing JSON frontmatter 282 282 - App generators are automatically discovered alongside system generators 283 283 - Keys are namespaced as `{app}:{agent}` (e.g., `my_app:weekly_summary`) 284 - - Outputs go to `JOURNAL/YYYYMMDD/agents/_<app>_<agent>.md` (or `.json` if `output: "json"`) 284 + - Outputs go to `JOURNAL/YYYYMMDD/talents/_<app>_<agent>.md` (or `.json` if `output: "json"`) 285 285 286 286 **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. 287 287 ··· 307 307 } 308 308 ``` 309 309 310 - **App-data outputs:** For outputs from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/agents/*.md` - these are automatically indexed. 310 + **App-data outputs:** For outputs from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/talents/*.md` - these are automatically indexed. 311 311 312 312 **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. 313 313 ··· 321 321 - Resolution: `"name"` → `talent/{name}.py`, `"app:name"` → `apps/{app}/talent/{name}.py`, or explicit path 322 322 323 323 **Pre-hooks** (`pre_process`): Modify inputs before the LLM call 324 - - `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` 324 + - `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` 325 325 - Return a dict of modified fields to merge back (e.g., `{"prompt": "modified"}`) 326 326 - Return `None` for no changes 327 327 328 328 **Post-hooks** (`post_process`): Transform output after the LLM call 329 329 - `result` is the LLM output (markdown or JSON string) 330 - - `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` 330 + - `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` 331 331 - Return modified string, or `None` to use original result 332 332 333 333 **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 <last>`, 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 371 372 372 **Reference implementations:** 373 373 - System agent examples: `talent/*.md` (files with `tools` field) 374 - - Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=True)`, `get_agent()` 374 + - Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=True)`, `get_talent()` 375 375 376 376 #### Prompt Context Configuration 377 377 ··· 379 379 380 380 ```json 381 381 { 382 - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 382 + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 383 383 } 384 384 ``` 385 385 ··· 394 394 - `$facets` - focused facet context or all available facets 395 395 - `$activity_context` - activity metadata, segment state, and analysis focus sections 396 396 397 - **Authoritative source:** `think/talent.py` - `_DEFAULT_LOAD`, `source_is_enabled()`, `source_is_required()`, `get_agent_filter()` 397 + **Authoritative source:** `think/talent.py` - `_DEFAULT_LOAD`, `source_is_enabled()`, `source_is_required()`, `get_talent_filter()` 398 398 399 399 --- 400 400 ··· 535 535 - `format_date(date_str)` - Format YYYYMMDD as "Wednesday January 14th" 536 536 537 537 ### Agent Spawning 538 - - `spawn_agent(prompt, name, provider, config)` - Spawn Cortex agent, returns agent_id 538 + - `spawn_agent(prompt, name, provider, config)` - Spawn Cortex agent, returns use_id 539 539 540 540 ### JSON Utilities 541 541 - `load_json(path)` - Load JSON file with error handling (returns None on error)
+3 -3
docs/CALLOSUM.md
··· 36 36 37 37 ### `cortex` - Agent execution events 38 38 **Source:** `think/cortex.py` 39 - **Events:** `request`, `start`, `thinking`, `tool_start`, `tool_end`, `finish`, `error`, `agent_updated`, `info`, `status` 39 + **Events:** `request`, `start`, `thinking`, `tool_start`, `tool_end`, `finish`, `error`, `talent_updated`, `info`, `status` 40 40 **Details:** See [CORTEX.md](CORTEX.md) for agent lifecycle, configuration, and event schemas 41 41 42 42 ### `supervisor` - Process lifecycle management ··· 105 105 106 106 ### `dream` - Generator and agent processing 107 107 **Source:** `think/dream.py` 108 - **Events:** `started`, `status`, `group_started`, `group_completed`, `agent_started`, `agent_completed`, `completed`, `segments_started`, `segments_completed` 108 + **Events:** `started`, `status`, `group_started`, `group_completed`, `talent_started`, `talent_completed`, `completed`, `segments_started`, `segments_completed` 109 109 **Key fields:** `mode` ("daily"/"segment"/"activity"/"flush"), `day`, `segment` (when mode="segment" or "flush"), `activity` and `facet` (when mode="activity") 110 110 **Purpose:** Track dream processing from generators through scheduled agents 111 111 **`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 257 For agent requests, use the cortex client: 258 258 ```python 259 259 from think.cortex_client import cortex_request 260 - agent_id = cortex_request(prompt="...", name="default") 260 + use_id = cortex_request(prompt="...", name="default") 261 261 ``` 262 262 263 263 See `think/cortex_client.py` for the full API.
+37 -37
docs/CORTEX.md
··· 1 1 # Cortex API and Eventing 2 2 3 - 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). 3 + 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). 4 4 5 5 For details on the Callosum protocol and message format, see [CALLOSUM.md](CALLOSUM.md). 6 6 ··· 9 9 ### Event Flow 10 10 1. **Request Creation**: Client calls `cortex_request()` which broadcasts to Callosum (`tract="cortex"`, `event="request"`) 11 11 2. **Request Reception**: Cortex receives message via Callosum callback and creates `<name>/<timestamp>_active.jsonl` 12 - 3. **Agent Spawning**: Cortex spawns agent process via `sol agents` with merged configuration 13 - 4. **Event Emission**: Agents write JSON events to stdout (captured by Cortex) 12 + 3. **Talent Spawning**: Cortex spawns a talent process via `python -m think.talents` with merged configuration 13 + 4. **Event Emission**: Talents write JSON events to stdout (captured by Cortex) 14 14 5. **Event Distribution**: Cortex appends events to JSONL file AND broadcasts to Callosum 15 15 6. **Agent Completion**: Cortex renames file to `<name>/<timestamp>.jsonl` when agent finishes 16 16 17 17 ### Key Components 18 18 - **Message Bus Integration**: Cortex connects to Callosum to receive requests and broadcast events 19 - - **Process Management**: Spawns agent subprocesses (both tool agents and generators) 20 - - **Configuration Delegation**: Passes raw requests to `sol agents`, which handles all config loading, validation, and hydration 19 + - **Process Management**: Spawns talent subprocesses (both tool talents and generators) 20 + - **Configuration Delegation**: Passes raw requests to `python -m think.talents`, which handles all config loading, validation, and hydration 21 21 - **Event Capture**: Monitors agent stdout/stderr and appends to JSONL files 22 22 - **Dual Event Distribution**: Events go to both persistent files and real-time message bus 23 23 - **NDJSON Input Mode**: Agent processes accept newline-delimited JSON via stdin containing the full merged configuration 24 24 25 25 ### File States 26 - - `<name>/<timestamp>_active.jsonl`: Agent currently executing (Cortex is appending events) 27 - - `<name>/<timestamp>.jsonl`: Agent completed (contains full event history) 26 + - `<name>/<timestamp>_active.jsonl`: Talent currently executing (Cortex is appending events) 27 + - `<name>/<timestamp>.jsonl`: Talent completed (contains full event history) 28 28 29 29 **Note**: Files provide persistence and historical record, while Callosum provides real-time event distribution to all interested services. 30 30 ··· 35 35 ```json 36 36 { 37 37 "event": "request", 38 - "ts": 1234567890123, // Required: millisecond timestamp (must match agent_id in filename) 39 - "prompt": "Analyze this code for security issues", // Required for agents (not generators) 40 - "name": "default", // Optional: agent name from talent/*.md 38 + "ts": 1234567890123, // Required: millisecond timestamp (must match use_id in filename) 39 + "prompt": "Analyze this code for security issues", // Required for talents (not generators) 40 + "name": "default", // Optional: talent name from talent/*.md 41 41 "provider": "openai", // Optional: override provider (openai, google, anthropic) 42 42 "max_output_tokens": 8192, // Optional: maximum response tokens 43 43 "thinking_budget": 10000, // Optional: thinking token budget (ignored by OpenAI) 44 44 "session_id": "sess-abc123", // Optional: CLI session ID for continuation 45 45 "chat_id": "1234567890122", // Optional: chat ID for reverse lookup 46 46 "facet": "my-project", // Optional: project context 47 - "output": "md", // Optional: output format ("md" or "json"), writes to agents/ 47 + "output": "md", // Optional: output format ("md" or "json"), writes to talents/ 48 48 "day": "20250109", // Optional: YYYYMMDD format, defaults to current day 49 49 "env": { // Optional: environment variables for subprocess 50 50 "API_KEY": "secret", ··· 79 79 80 80 ### Generator Events 81 81 82 - Generators emit the same event types as agents: 82 + Generators emit the same event types as talents: 83 83 - `start` - When generation begins 84 84 - `finish` - On completion, with `result` containing generated content 85 85 - `error` - On failure ··· 92 92 93 93 All providers (Anthropic, OpenAI, Google) support continuing conversations via CLI 94 94 session resumption. Include a `session_id` field in the request with the CLI session 95 - ID from a previous agent's finish event. The provider CLI tool resumes the conversation 95 + ID from a previous talent's finish event. The provider CLI tool resumes the conversation 96 96 internally using its native session management (e.g., `claude --resume`, `codex exec resume`). 97 97 98 98 Chats are locked to their original provider — continuations must use the same provider 99 99 that started the conversation. The `chat_id` field enables reverse lookup from an 100 - agent back to its parent chat. 100 + talent back to its parent chat. 101 101 102 102 ## Agent Event Format 103 103 104 - 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. 104 + 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. 105 105 106 106 ### request 107 107 The initial spawn request (first line of file, written by client). ··· 109 109 { 110 110 "event": "request", 111 111 "ts": 1234567890123, 112 - "agent_id": "1234567890123", 112 + "use_id": "1234567890123", 113 113 "prompt": "User's task or question", 114 114 "provider": "openai", 115 115 "name": "default", ··· 119 119 ``` 120 120 121 121 ### start 122 - Emitted when an agent run begins. 122 + Emitted when a talent run begins. 123 123 ```json 124 124 { 125 125 "event": "start", 126 126 "ts": 1234567890123, 127 - "agent_id": "1234567890123", 127 + "use_id": "1234567890123", 128 128 "name": "default", 129 129 "model": "gpt-4o", 130 130 "session_id": "sess-abc", ··· 138 138 { 139 139 "event": "tool_start", 140 140 "ts": 1234567890123, 141 - "agent_id": "1234567890123", 141 + "use_id": "1234567890123", 142 142 "tool": "search_journal", 143 143 "args": {"query": "search terms", "limit": 10}, 144 144 "call_id": "search_journal-1" ··· 151 151 { 152 152 "event": "tool_end", 153 153 "ts": 1234567890123, 154 - "agent_id": "1234567890123", 154 + "use_id": "1234567890123", 155 155 "tool": "search_journal", 156 156 "args": {"query": "search terms"}, 157 157 "result": ["result", "array", "or", "object"], ··· 165 165 { 166 166 "event": "thinking", 167 167 "ts": 1234567890123, 168 - "agent_id": "1234567890123", 168 + "use_id": "1234567890123", 169 169 "summary": "Model's internal reasoning about the task...", 170 170 "model": "o1-mini" 171 171 } 172 172 ``` 173 173 174 - ### agent_updated 174 + ### talent_updated 175 175 Emitted when control is handed off to a different agent (multi-agent scenarios). 176 176 ```json 177 177 { 178 - "event": "agent_updated", 178 + "event": "talent_updated", 179 179 "ts": 1234567890123, 180 - "agent_id": "1234567890123", 180 + "use_id": "1234567890123", 181 181 "agent": "SpecializedAgent" 182 182 } 183 183 ``` 184 184 185 185 ### finish 186 - Emitted when the agent run completes successfully. 186 + Emitted when the talent run completes successfully. 187 187 ```json 188 188 { 189 189 "event": "finish", 190 190 "ts": 1234567890123, 191 - "agent_id": "1234567890123", 191 + "use_id": "1234567890123", 192 192 "result": "Final response text to the owner" 193 193 } 194 194 ``` ··· 199 199 { 200 200 "event": "error", 201 201 "ts": 1234567890123, 202 - "agent_id": "1234567890123", 202 + "use_id": "1234567890123", 203 203 "error": "Error message", 204 204 "trace": "Full stack trace..." 205 205 } ··· 211 211 { 212 212 "event": "info", 213 213 "ts": 1234567890123, 214 - "agent_id": "1234567890123", 214 + "use_id": "1234567890123", 215 215 "message": "Non-JSON output line from agent" 216 216 } 217 217 ``` ··· 232 232 233 233 - Include an `output` field in the agent's frontmatter with the format ("md" or "json") 234 234 - Output path is derived from agent name + format + schedule: 235 - - Daily agents: `YYYYMMDD/agents/{name}.{ext}` 235 + - Daily agents: `YYYYMMDD/talents/{name}.{ext}` 236 236 - Segment agents: `YYYYMMDD/{segment}/{name}.{ext}` 237 237 - Writing occurs before completion 238 238 - Write failures are logged but don't interrupt the agent flow 239 239 - Commonly used for scheduled agents that generate daily reports 240 240 241 - ## Agent Configuration 241 + ## Talent Configuration 242 242 243 - Agents use configurations stored in the `talent/` directory. Each agent is a `.md` file containing: 243 + Talents use configurations stored in the `talent/` directory. Each talent is a `.md` file containing: 244 244 - JSON frontmatter with metadata and configuration 245 - - The agent-specific prompt and instructions in the content 245 + - The talent-specific prompt and instructions in the content 246 246 247 - When spawning an agent: 248 - 1. Cortex passes the raw request to `sol agents` via stdin (NDJSON format) 249 - 2. The agent process (`think/agents.py`) handles all config loading via `prepare_config()`: 250 - - Loads agent configuration using `get_agent()` from `think/talent.py` 251 - - Merges request parameters with agent defaults 247 + When spawning a talent: 248 + 1. Cortex passes the raw request to `python -m think.talents` via stdin (NDJSON format) 249 + 2. The talent process (`think/talents.py`) handles all config loading via `prepare_config()`: 250 + - Loads talent configuration using `get_talent()` from `think/talent.py` 251 + - Merges request parameters with talent defaults 252 252 - Resolves provider and model based on context 253 253 3. The agent validates the config via `validate_config()` before execution 254 254 4. Instructions are built with three components:
+10 -10
docs/DOCTOR.md
··· 12 12 ls -la journal/health/callosum.sock 13 13 14 14 # Check for stuck agents (should be empty or short-lived) 15 - ls journal/agents/*/*_active.jsonl 2>/dev/null 15 + ls journal/talents/*/*_active.jsonl 2>/dev/null 16 16 ``` 17 17 18 18 **Healthy state:** ··· 45 45 |------|-------| 46 46 | Current service logs | `journal/health/{service}.log` (symlinks) | 47 47 | Day's process logs | `journal/{YYYYMMDD}/health/{ref}_{name}.log` | 48 - | Agent execution | `journal/agents/<name>/*.jsonl` | 48 + | Agent execution | `journal/talents/<name>/*.jsonl` | 49 49 | Journal task log | `journal/task_log.txt` | 50 50 51 51 **Symlink structure:** Journal-level symlinks point to current day's logs. Day-level symlinks point to current process instance (by ref). ··· 89 89 90 90 ## Reading Agent Files 91 91 92 - **Location:** `journal/agents/` 92 + **Location:** `journal/talents/` 93 93 94 94 **File states:** 95 95 - `{name}/{timestamp}_active.jsonl` - Agent currently running ··· 105 105 106 106 ```bash 107 107 # View an agent's final result 108 - jq -r 'select(.event=="finish") | .result' journal/agents/default/1234567890123.jsonl 108 + jq -r 'select(.event=="finish") | .result' journal/talents/default/1234567890123.jsonl 109 109 110 110 # List today's agents with their prompts 111 - for id in $(jq -r '.agent_id' journal/agents/$(date +%Y%m%d).jsonl 2>/dev/null); do 111 + for id in $(jq -r '.use_id' journal/talents/$(date +%Y%m%d).jsonl 2>/dev/null); do 112 112 f=$(find journal/agents -maxdepth 2 -path "*/${id}.jsonl" -print -quit) 113 113 [ -n "$f" ] || continue 114 114 echo "=== $(basename "$f") ===" ··· 138 138 139 139 ```bash 140 140 # Find active agents 141 - ls -la journal/agents/*/*_active.jsonl 141 + ls -la journal/talents/*/*_active.jsonl 142 142 143 143 # Check last event in active agent 144 - tail -1 journal/agents/*/*_active.jsonl | jq . 144 + tail -1 journal/talents/*/*_active.jsonl | jq . 145 145 ``` 146 146 147 147 Causes: Backend timeout, tool hanging, network issues. ··· 176 176 tail -f journal/health/*.log 177 177 178 178 # Count today's agents by status 179 - echo "Completed: $([ -f journal/agents/$(date +%Y%m%d).jsonl ] && wc -l < journal/agents/$(date +%Y%m%d).jsonl || echo 0)" 180 - echo "Running: $(ls journal/agents/*/*_active.jsonl 2>/dev/null | wc -l)" 179 + echo "Completed: $([ -f journal/talents/$(date +%Y%m%d).jsonl ] && wc -l < journal/talents/$(date +%Y%m%d).jsonl || echo 0)" 180 + echo "Running: $(ls journal/talents/*/*_active.jsonl 2>/dev/null | wc -l)" 181 181 182 182 # Find agents that errored today 183 - jq -r 'select(.status=="error") | .agent_id' journal/agents/$(date +%Y%m%d).jsonl 2>/dev/null 183 + jq -r 'select(.status=="error") | .use_id' journal/talents/$(date +%Y%m%d).jsonl 2>/dev/null 184 184 185 185 # Check token usage for today 186 186 wc -l journal/tokens/$(date +%Y%m%d).jsonl
+19 -19
docs/JOURNAL.md
··· 14 14 ┌─────────────────────────────────────┐ 15 15 │ LAYER 3: AGENT OUTPUTS │ Narrative summaries 16 16 │ (Markdown files) │ "What it means" 17 - │ - agents/*.md (daily outputs) │ 17 + │ - talents/*.md (daily outputs) │ 18 18 │ - *.md (segment outputs) │ 19 19 └─────────────────────────────────────┘ 20 20 ↑ synthesized from ··· 42 42 |------|------------|----------| 43 43 | **Capture** | Raw audio/video recording | `*.flac`, `*.ogg`, `*.opus`, `*.wav`, `*.webm` | 44 44 | **Extract** | Structured data from captures | `*.jsonl` | 45 - | **Agent Output** | AI-generated narrative summary | `agents/*.md`, `HHMMSS_LEN/*.md` | 45 + | **Agent Output** | AI-generated narrative summary | `talents/*.md`, `HHMMSS_LEN/*.md` | 46 46 47 47 **Organization** 48 48 ··· 67 67 | `chronicle/` | Container for daily capture folders (`YYYYMMDD/`) containing segments, extracts, and agent outputs | 68 68 | `entities/` | Journal-level entity identity records (`<id>/entity.json`) | 69 69 | `facets/` | Facet-specific data: entity relationships, todos, events, news, action logs | 70 - | `agents/` | Agent run logs in per-agent subdirectories (`<name>/<id>.jsonl`), day indexes (`<day>.jsonl`), and latest-run symlinks (`<name>.log`) | 70 + | `talents/` | Talent run logs in per-talent subdirectories (`<name>/<id>.jsonl`), day indexes (`<day>.jsonl`), and latest-run symlinks (`<name>.log`) | 71 71 | `apps/` | App-specific storage (distinct from codebase `apps/`) | 72 72 | `streams/` | Per-stream state files (`<name>.json`) tracking segment chains and sequence numbers | 73 73 | `imports/` | Imported audio files and processing artifacts | ··· 186 186 187 187 "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`). 188 188 189 - 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. 189 + 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. 190 190 191 191 Raw media is never deleted from segments that haven't finished processing. A segment is considered complete only when all four checks pass: 192 192 193 - - No `_active.jsonl` files in `agents/` (no running agents) 193 + - No `_active.jsonl` files in `talents/` (no running talents) 194 194 - `audio.jsonl` (or `*_audio.jsonl`) exists if audio raw media was captured 195 195 - `screen.jsonl` (or `*_screen.jsonl`) exists if video raw media was captured 196 - - `agents/speaker_labels.json` exists if voice embeddings (`.npz`) are present 196 + - `talents/speaker_labels.json` exists if voice embeddings (`.npz`) are present 197 197 198 198 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. 199 199 ··· 733 733 "text": "Review project proposal" 734 734 }, 735 735 "facet": "work", 736 - "agent_id": "1765870373972" 736 + "use_id": "1765870373972" 737 737 } 738 738 ``` 739 739 ··· 747 747 - `action` – Action name (e.g., "todo_add", "identity_update") 748 748 - `params` – Action-specific parameters 749 749 - `facet` – Facet name (only present in facet-scoped logs) 750 - - `agent_id` – Agent ID (only present for agent tool actions) 750 + - `use_id` – Agent ID (only present for agent tool actions) 751 751 752 752 These logs enable auditing, debugging, and potential rollback of automated actions. 753 753 ··· 778 778 Required fields: 779 779 - `timestamp` – Unix timestamp in milliseconds (13 digits) 780 780 - `model` – Model identifier (e.g., "gemini-2.5-flash", "gpt-5", "claude-sonnet-4-5") 781 - - `context` – Calling context (e.g., "agent.name.agent_id" or "module.function:line") 781 + - `context` – Calling context (e.g., "agent.name.use_id" or "module.function:line") 782 782 - `usage` – Token counts dictionary with normalized field names 783 783 784 784 Optional fields: ··· 796 796 797 797 ## Agent Event Logs 798 798 799 - 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. 799 + 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. 800 800 801 801 **Directory layout:** 802 802 - `<name>/` – per-agent subdirectory (e.g., `default/`, `entities--observer/`) 803 - - `<name>/<agent_id>_active.jsonl` – currently running agent (renamed when complete) 804 - - `<name>/<agent_id>.jsonl` – completed agent session 803 + - `<name>/<use_id>_active.jsonl` – currently running agent (renamed when complete) 804 + - `<name>/<use_id>.jsonl` – completed agent session 805 805 - `<name>.log` – symlink to the latest completed run for each agent name 806 806 - `<day>.jsonl` – day index with one summary line per agent that completed on that day 807 807 808 - The `agent_id` is a Unix timestamp in milliseconds that uniquely identifies the session. 808 + The `use_id` is a Unix timestamp in milliseconds that uniquely identifies the session. 809 809 810 810 **Event format (JSONL):** 811 811 ··· 1040 1040 - **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) 1041 1041 1042 1042 ```jsonl 1043 - {"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"} 1044 - {"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"} 1043 + {"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"} 1044 + {"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"} 1045 1045 ``` 1046 1046 1047 1047 **Common fields:** ··· 1069 1069 1070 1070 #### Daily outputs 1071 1071 1072 - Post-processing generates day-level outputs in the `agents/` directory that synthesize all segments. 1072 + Post-processing generates day-level outputs in the `talents/` directory that synthesize all segments. 1073 1073 1074 1074 **Generator discovery:** Available generator types are discovered at runtime from: 1075 1075 - `talent/*.md` – system generator templates (files with `schedule` field but no `tools` field) ··· 1078 1078 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. 1079 1079 1080 1080 **Output naming:** 1081 - - System outputs: `agents/{agent}.md` (e.g., `agents/flow.md`, `agents/meetings.md`) 1082 - - App outputs: `agents/_{app}_{agent}.md` (e.g., `agents/_entities_observer.md`) 1083 - - JSON output: `agents/{agent}.json` when metadata specifies `"output": "json"` 1081 + - System outputs: `talents/{agent}.md` (e.g., `talents/flow.md`, `talents/meetings.md`) 1082 + - App outputs: `talents/_{app}_{agent}.md` (e.g., `talents/_entities_observer.md`) 1083 + - JSON output: `talents/{agent}.json` when metadata specifies `"output": "json"` 1084 1084 1085 1085 Each generator type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form.
+1 -1
docs/PROMPT_TEMPLATES.md
··· 148 148 149 149 **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`. 150 150 151 - **Reference:** `think/talent.py` → `get_agent()` for agent configuration loading 151 + **Reference:** `think/talent.py` → `get_talent()` for agent configuration loading 152 152 153 153 ### The load_prompt() Function 154 154
+1 -1
docs/PROVIDERS.md
··· 121 121 - `extra_context`: Runtime context (facets, insights list, datetime) as first user message 122 122 - `user_instruction`: Agent-specific prompt as second user message 123 123 - `tools`: Optional list of allowed tool names 124 - - `agent_id`, `name`: Identity for logging and tool calls 124 + - `use_id`, `name`: Identity for logging and tool calls 125 125 - `session_id`: CLI session ID for conversation continuation 126 126 - `chat_id`: Chat ID for reverse lookup from agent to chat 127 127
+18 -18
docs/THINK.md
··· 17 17 - `sol call transcripts read` groups audio and screen transcripts into report sections. Use `--start` and 18 18 `--length` to limit the report to a specific time range. See `sol call transcripts --help` for additional commands. 19 19 - `sol dream` runs generators and agents for a single day via Cortex. 20 - - `sol agents` is the unified CLI for tool agents and generators (spawned by Cortex, NDJSON protocol). 20 + - `python -m think.talents` is the unified execution module for tool talents and generators spawned by Cortex (NDJSON protocol). 21 21 - `sol supervisor` monitors observation heartbeats. Use `--no-observers` to disable local capture (sense still runs for observer uploads and imports). 22 22 - `sol cortex` starts a Callosum-based service for managing AI agent instances and generators. 23 23 - `sol talent` lists available agents and generators with their configuration. Use `sol talent show <name>` to see details, and `sol talent show <name> --prompt` to see the fully composed prompt that would be sent to the LLM. ··· 91 91 92 92 After each generator completes and creates output, the indexer runs `--rescan-file` for incremental indexing. A full `--rescan` runs in the post phase. 93 93 94 - ### Cortex: Central Agent Manager 94 + ### Cortex: Central Talent Manager 95 95 96 - 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. 96 + 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. 97 97 98 98 Cortex routes requests based on configuration: 99 - - Requests with `tools` field → tool-using agents (`sol agents`) 100 - - Requests with `output` field (no `tools`) → generators (`sol agents`) 99 + - Requests with `tools` field → tool-using talents (`python -m think.talents`) 100 + - Requests with `output` field (no `tools`) → generators (`python -m think.talents`) 101 101 102 - Both types are handled by the unified `sol agents` CLI which routes internally. 102 + Both types are handled by the unified `python -m think.talents` execution module. 103 103 104 - To spawn agents programmatically, use the cortex_client functions: 104 + To spawn talents programmatically, use the cortex_client functions: 105 105 106 106 ```python 107 107 from think.cortex_client import cortex_request 108 108 from think.callosum import CallosumConnection 109 109 110 110 # Create a request 111 - agent_id = cortex_request( 111 + use_id = cortex_request( 112 112 prompt="Your task here", 113 113 name="default", 114 114 provider="openai" # or "google", "anthropic", "claude" 115 115 ) 116 116 117 - # Watch for agent events via Callosum 117 + # Watch for talent events via Callosum 118 118 def on_event(message): 119 119 # Filter for cortex tract events 120 120 if message.get('tract') != 'cortex': ··· 135 135 Generators can also be spawned via `cortex_request` by including an `output` field: 136 136 137 137 ```python 138 - from think.cortex_client import cortex_request, wait_for_agents 138 + from think.cortex_client import cortex_request, wait_for_uses 139 139 140 140 # Spawn a generator 141 - agent_id = cortex_request( 141 + use_id = cortex_request( 142 142 prompt="", # Generators don't use prompts 143 143 name="activity", 144 144 config={ ··· 149 149 ) 150 150 151 151 # Wait for completion 152 - completed, timed_out = wait_for_agents([agent_id], timeout=300) 152 + completed, timed_out = wait_for_uses([use_id], timeout=300) 153 153 ``` 154 154 155 155 ### Direct CLI Usage (Testing Only) 156 156 157 - The `sol agents` command is primarily used internally by Cortex. For testing purposes, it can be invoked directly: 157 + 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: 158 158 159 159 ```bash 160 - sol agents [TASK_FILE] [--provider PROVIDER] [--model MODEL] [--max-tokens N] [-o OUT_FILE] 160 + sol providers check [TASK_FILE] [--provider PROVIDER] [--model MODEL] [--max-tokens N] [-o OUT_FILE] 161 161 ``` 162 162 163 163 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 196 The `think.cortex_client` module provides functions for interacting with Cortex: 197 197 198 198 ```python 199 - from think.cortex_client import cortex_request, cortex_agents 199 + from think.cortex_client import cortex_request, cortex_uses 200 200 201 201 # Create an agent request 202 202 request_file = cortex_request( ··· 206 206 ) 207 207 208 208 # List running and completed agents 209 - agents_info = cortex_agents(limit=10, agent_type="live") 209 + agents_info = cortex_uses(limit=10, use_type="live") 210 210 print(f"Found {agents_info['live_count']} running agents") 211 211 ``` 212 212 # Talent Module ··· 218 218 | Command | Purpose | 219 219 |---------|---------| 220 220 | `sol cortex` | Agent orchestration service | 221 - | `sol agents` | Direct agent invocation (testing only) | 221 + | `sol providers check` | Ad-hoc provider check (testing only) | 222 222 223 223 ## Architecture 224 224 ··· 245 245 ## Key Components 246 246 247 247 - **cortex.py** - Central agent manager, file watcher, event distribution, spawns agents.py 248 - - **cortex_client.py** - Client functions: `cortex_request()`, `cortex_agents()`, `wait_for_agents()` 248 + - **cortex_client.py** - Client functions: `cortex_request()`, `cortex_uses()`, `wait_for_uses()` 249 249 - **agents.py** - Unified CLI entry point for both tool-using agents and generators (NDJSON protocol) 250 250 - **models.py** - Unified `generate()`/`agenerate()` API, provider routing, token logging 251 251 - **batch.py** - `Batch` class for concurrent LLM requests with dynamic queuing
+8 -8
docs/design/yesterdays-processing-card.md
··· 32 32 Reads `stats_data["heatmap_data"]["hours"]`, keeps the top 3 non-zero hours, sorts by minutes desc then hour asc. 33 33 34 34 - `_knowledge_graph_freshness(yesterday: str) -> dict` 35 - Reads `chronicle/{yesterday}/agents/knowledge_graph.md`, checks existence and `st_mtime` freshness using the relaxed rule in section 4. 35 + Reads `chronicle/{yesterday}/talents/knowledge_graph.md`, checks existence and `st_mtime` freshness using the relaxed rule in section 4. 36 36 37 37 - `_briefing_freshness(today: str) -> dict` 38 38 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 125 126 126 ### Ground truth 127 127 128 - - `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`. 128 + - `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`. 129 129 - `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`. 130 130 - The facet newsletter writer is `sol call journal news`, implemented by `think/tools/facets.py:61-106`. 131 131 - The newsletter prompt key is stable: `facet_newsletter`. ··· 135 135 136 136 ### Option A — re-parse dream JSONL for newsletter-specific facet fails 137 137 138 - Read `chronicle/{yesterday}/health/*_daily_dream.jsonl` and count `agent.fail` records where: 138 + Read `chronicle/{yesterday}/health/*_daily_dream.jsonl` and count `talent.fail` records where: 139 139 140 - - `event == "agent.fail"` 140 + - `event == "talent.fail"` 141 141 - `facet` is present 142 142 - `name == "facet_newsletter"` 143 143 ··· 163 163 164 164 ### Option B — re-parse any facet-scoped fail 165 165 166 - Count every `agent.fail` with a `facet` field, regardless of `name`. 166 + Count every `talent.fail` with a `facet` field, regardless of `name`. 167 167 168 168 Pros: 169 169 ··· 224 224 225 225 Rationale: 226 226 227 - - Prep already found a real case where `chronicle/20260415/agents/knowledge_graph.md` had `mtime` on `2026-04-16 07:23:43`. 227 + - Prep already found a real case where `chronicle/20260415/talents/knowledge_graph.md` had `mtime` on `2026-04-16 07:23:43`. 228 228 - The intent of the card is “did the overnight processing refresh yesterday’s graph?”, not “did the write finish before midnight”. 229 229 - This rule admits same-day and overnight-after-midnight completions without introducing an arbitrary 36-hour window. 230 230 ··· 350 350 Fixture minimization rule: 351 351 352 352 - Seed only the fields each test asserts on. 353 - - Keep dream logs to the minimum lines needed: `run.start`, `agent.dispatch`, `agent.complete` or `agent.fail`, `run.complete`. 353 + - Keep dream logs to the minimum lines needed: `run.start`, `talent.dispatch`, `talent.complete` or `talent.fail`, `run.complete`. 354 354 355 355 ## 9. Non-goals 356 356 ··· 383 383 384 384 All three gate items resolved. Proceed to `implement` stage. 385 385 386 - - **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. 386 + - **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. 387 387 - **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. 388 388 - **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"`: 389 389
+122
scripts/gate_agents_rename.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: AGPL-3.0-only 3 + 4 + from __future__ import annotations 5 + 6 + import re 7 + import subprocess 8 + import sys 9 + from pathlib import Path 10 + 11 + ROOT = Path(__file__).resolve().parent.parent 12 + SHIM_FILES = { 13 + Path("think/pipeline_health.py"), 14 + Path("apps/home/routes.py"), 15 + } 16 + ALLOWLIST_RE = re.compile(r"^apps/sol/maint/00[0-4]_.+\.py$") 17 + PRODUCTION_PREFIXES = ("think/", "apps/", "talent/", "convey/", "observe/") 18 + SHIM_WINDOW = 20 19 + 20 + RULES = [ 21 + ( 22 + "legacy dream emitter", 23 + re.compile(r'_jsonl_log\(\s*["\']agent\.(fail|dispatch|complete|skip)["\']'), 24 + None, 25 + ), 26 + ( 27 + "legacy callosum emitter", 28 + re.compile(r'emit\(\s*["\']agent_(started|completed)["\']'), 29 + None, 30 + ), 31 + ("legacy module path", re.compile(r"\bthink\.agents\b"), None), 32 + ("legacy/new CLI command", re.compile(r"\bsol agents\b|\bsol talents\b"), None), 33 + ("legacy payload key", re.compile(r'["\']agent_id["\']\s*:'), "production"), 34 + ("legacy wire event", re.compile(r'["\']agent_updated["\']'), "production"), 35 + ( 36 + "legacy summary/anomaly key", 37 + re.compile( 38 + r'summary\["agents"\]|["\']agent_failure["\']|["\']agents_fired["\']' 39 + ), 40 + "production", 41 + ), 42 + ] 43 + 44 + 45 + def tracked_files() -> list[Path]: 46 + result = subprocess.run( 47 + ["git", "ls-files"], 48 + cwd=ROOT, 49 + check=True, 50 + capture_output=True, 51 + text=True, 52 + ) 53 + return [Path(line) for line in result.stdout.splitlines() if line] 54 + 55 + 56 + def is_allowed(path: Path) -> bool: 57 + path_str = path.as_posix() 58 + if path == Path("AGENTS.md"): 59 + return True 60 + if path == Path("tests/test_maint_004_rename.py"): 61 + return True 62 + if path == Path("scripts/gate_agents_rename.py"): 63 + return True 64 + if path_str.startswith(".agents/skills/"): 65 + return True 66 + if ALLOWLIST_RE.match(path_str): 67 + return True 68 + return False 69 + 70 + 71 + def is_production(path: Path) -> bool: 72 + path_str = path.as_posix() 73 + return path_str == "sol.py" or path_str.startswith(PRODUCTION_PREFIXES) 74 + 75 + 76 + def iter_lines(path: Path) -> list[tuple[int, str]]: 77 + lines = (ROOT / path).read_text(encoding="utf-8").splitlines() 78 + if path not in SHIM_FILES: 79 + return list(enumerate(lines, start=1)) 80 + 81 + visible: list[tuple[int, str]] = [] 82 + suppress_until = 0 83 + for line_no, line in enumerate(lines, start=1): 84 + if line_no <= suppress_until: 85 + continue 86 + if "HISTORICAL SHIM:" in line: 87 + suppress_until = line_no + SHIM_WINDOW 88 + continue 89 + visible.append((line_no, line)) 90 + return visible 91 + 92 + 93 + def main() -> int: 94 + failures: list[str] = [] 95 + for path in tracked_files(): 96 + if is_allowed(path): 97 + continue 98 + if not (ROOT / path).is_file(): 99 + continue 100 + try: 101 + lines = iter_lines(path) 102 + except UnicodeDecodeError: 103 + continue 104 + for line_no, line in lines: 105 + for label, pattern, scope in RULES: 106 + if scope == "production" and not is_production(path): 107 + continue 108 + if pattern.search(line): 109 + failures.append(f"{path}:{line_no}: {label}: {line.strip()}") 110 + 111 + if failures: 112 + print("agents rename gate failed:", file=sys.stderr) 113 + for failure in failures: 114 + print(f" {failure}", file=sys.stderr) 115 + return 1 116 + 117 + print("agents rename gate passed") 118 + return 0 119 + 120 + 121 + if __name__ == "__main__": 122 + raise SystemExit(main())
+5 -5
sol.py
··· 11 11 Examples: 12 12 sol import data.json Import data into journal 13 13 sol dream 20250101 Run daily processing for a day 14 - sol think.agents -h Show help for specific module 14 + sol think.talents -h Show help for specific module 15 15 """ 16 16 17 17 from __future__ import annotations ··· 57 57 "transfer": "observe.transfer", 58 58 "export": "observe.export", 59 59 "observer": "observe.observer_cli", 60 - # AI agents (talent package) 61 - "agents": "think.agents", 60 + # AI providers and talent execution 61 + "providers": "think.providers_cli", 62 62 "cortex": "think.cortex", 63 63 "talent": "think.talent_cli", 64 64 "call": "think.call", ··· 111 111 "export", 112 112 "observer", 113 113 ], 114 - "Talent (AI agents)": [ 115 - "agents", 114 + "Talent": [ 115 + "providers", 116 116 "cortex", 117 117 "talent", 118 118 "engage",
+2 -2
talent/activities.py
··· 46 46 47 47 def _list_facets_with_activity_state(day: str, segment: str, stream: str) -> list[str]: 48 48 """Find all facets that have activity_state.json in a segment.""" 49 - agents_dir = segment_path(day, segment, stream) / "agents" 49 + agents_dir = segment_path(day, segment, stream) / "talents" 50 50 if not agents_dir.is_dir(): 51 51 return [] 52 52 ··· 61 61 def _load_activity_state(day: str, segment: str, facet: str, stream: str) -> list[dict]: 62 62 """Load activity_state.json for a facet in a segment. Returns [] on failure.""" 63 63 state_path = ( 64 - segment_path(day, segment, stream) / "agents" / facet / "activity_state.json" 64 + segment_path(day, segment, stream) / "talents" / facet / "activity_state.json" 65 65 ) 66 66 if not state_path.exists(): 67 67 return []
+3 -3
talent/activity_state.py
··· 34 34 """Extract facet name from output path. 35 35 36 36 Output paths for faceted generators follow the pattern: 37 - {day}/{stream}/{segment}/agents/{facet}/activity_state.json 37 + {day}/{stream}/{segment}/talents/{facet}/activity_state.json 38 38 39 39 Returns None if facet cannot be extracted. 40 40 """ ··· 46 46 return None 47 47 48 48 parent = os.path.basename(os.path.dirname(output_path)) 49 - if parent and parent != "agents": 49 + if parent and parent != "talents": 50 50 return parent 51 51 return None 52 52 ··· 124 124 parsed JSON array or None if not found/invalid. 125 125 """ 126 126 state_path = ( 127 - segment_path(day, segment, stream) / "agents" / facet / "activity_state.json" 127 + segment_path(day, segment, stream) / "talents" / facet / "activity_state.json" 128 128 ) 129 129 if not state_path.exists(): 130 130 return None, None
+1 -1
talent/daily_schedule.md
··· 10 10 "color": "#455a64", 11 11 "thinking_budget": 4096, 12 12 "max_output_tokens": 512, 13 - "load": {"transcripts": false, "percepts": false, "agents": false} 13 + "load": {"transcripts": false, "percepts": false, "talents": false} 14 14 } 15 15 16 16 $facets
+1 -1
talent/decisions.md
··· 10 10 "activities": ["meeting", "call", "messaging", "email"], 11 11 "priority": 10, 12 12 "output": "md", 13 - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 13 + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 14 14 } 15 15 16 16 $facets
+1 -1
talent/documents.md
··· 10 10 "thinking_budget": 8192, 11 11 "max_output_tokens": 8192, 12 12 "output": "md", 13 - "load": {"transcripts": true, "percepts": false, "agents": false} 13 + "load": {"transcripts": true, "percepts": false, "talents": false} 14 14 15 15 } 16 16
+1 -1
talent/entities.md
··· 10 10 "thinking_budget": 4096, 11 11 "max_output_tokens": 1024, 12 12 "output": "md", 13 - "load": {"transcripts": true, "percepts": true, "agents": false} 13 + "load": {"transcripts": true, "percepts": true, "talents": false} 14 14 15 15 } 16 16
+1 -1
talent/facet_newsletter.md
··· 9 9 "priority": 40, 10 10 "multi_facet": true, 11 11 "load": { 12 - "agents": True, 12 + "talents": True, 13 13 "journal": True 14 14 } 15 15 }
+1 -1
talent/flow.md
··· 9 9 "schedule": "daily", 10 10 "priority": 10, 11 11 "output": "md", 12 - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 12 + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 13 13 } 14 14 15 15 $facets
+1 -1
talent/followups.md
··· 10 10 "activities": ["meeting", "call", "messaging", "email"], 11 11 "priority": 10, 12 12 "output": "md", 13 - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 13 + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 14 14 } 15 15 16 16 $facets
+2 -2
talent/heartbeat.md
··· 34 34 35 35 ## Step 2: Check journal quality 36 36 37 - Run `sol talent logs --daily -c 10` to review recent agent runs and 37 + Run `sol talent logs --daily -c 10` to review recent talent runs and 38 38 `sol talent logs --errors -c 10` for recent errors. Look for: 39 - - Broken segments (transcription failures, missing agent output) 39 + - Broken segments (transcription failures, missing talent output) 40 40 - Processing gaps (capture with no dream processing) 41 41 - Orphaned entities (zero observations after 7+ days) 42 42
+1 -1
talent/knowledge_graph.md
··· 9 9 "schedule": "daily", 10 10 "priority": 10, 11 11 "output": "md", 12 - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 12 + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 13 13 } 14 14 15 15 $facets
+1 -1
talent/meetings.md
··· 10 10 "activities": ["meeting"], 11 11 "priority": 10, 12 12 "output": "md", 13 - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 13 + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 14 14 } 15 15 16 16 $facets
+1 -1
talent/messaging.md
··· 10 10 "activities": ["messaging", "email"], 11 11 "priority": 10, 12 12 "output": "md", 13 - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 13 + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 14 14 } 15 15 16 16 $facets
+4 -4
talent/morning_briefing.md
··· 70 70 **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. 71 71 72 72 `sol://` URI construction: 73 - - **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`. 74 - - **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}`. 73 + - **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`. 74 + - **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}`. 75 75 - **Facet newsletters:** `sol://facets/{facet}/news/{day_YYYYMMDD}`. 76 76 77 77 **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 90 4. Unscheduled todos (action items with no calendar time blocked) 91 91 Pipeline gaps owner-facing phrasings (from `pipeline_anomalies`). Use these verbatim, substituting real counts and agent names from the summary: 92 92 - `activity_agents_missing` → "**Pipeline gap:** N activities ended yesterday but activity agents didn't fire — meeting notes, decisions, and follow-ups may be missing." 93 - - `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".) 93 + - `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".) 94 94 - `daily_agents_missing` → "**Pipeline gap:** Daily agents didn't run yesterday despite journal data. Facet newsletters and digest may be missing." 95 95 96 96 Do NOT include this section when pipeline status is `healthy` (status == "healthy" or anomalies list is empty). Zero noise on normal days. ··· 141 141 142 142 ## Forward Look 143 143 - Board meeting Thursday — slides need review (confirmed on [calendar](sol://20260327/calendar)) 144 - - May want to prepare quarterly metrics based on last quarter's timing (from [anticipation](sol://20260327/agents/anticipation)) 144 + - May want to prepare quarterly metrics based on last quarter's timing (from [anticipation](sol://20260327/talents/anticipation)) 145 145 [more items...] 146 146 147 147 ## Reading
+1 -1
talent/schedule.md
··· 8 8 "schedule": "daily", 9 9 "priority": 10, 10 10 "output": "md", 11 - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 11 + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 12 12 13 13 } 14 14
+1 -1
talent/screen.md
··· 7 7 "schedule": "segment", 8 8 "priority": 10, 9 9 "output": "md", 10 - "load": {"transcripts": true, "percepts": "required", "agents": false} 10 + "load": {"transcripts": true, "percepts": "required", "talents": false} 11 11 12 12 } 13 13
+1 -1
talent/sense.md
··· 10 10 "thinking_budget": 4096, 11 11 "max_output_tokens": 4096, 12 12 "output": "json", 13 - "load": {"transcripts": true, "percepts": true, "agents": false} 13 + "load": {"transcripts": true, "percepts": true, "talents": false} 14 14 } 15 15 16 16 $facets
+1 -1
talent/skills.md
··· 7 7 "activities": ["*"], 8 8 "priority": 90, 9 9 "output": "json", 10 - "load": {"transcripts": false, "percepts": false, "agents": false} 10 + "load": {"transcripts": false, "percepts": false, "talents": false} 11 11 } 12 12 13 13 You are analyzing recurring activity patterns to identify and document the owner's skills.
+1 -1
talent/speaker_attribution.md
··· 8 8 "output": "json", 9 9 "color": "#d84315", 10 10 "hook": {"pre": "speaker_attribution", "post": "speaker_attribution"}, 11 - "load": {"transcripts": true, "agents": {"speakers": true, "screen": true}} 11 + "load": {"transcripts": true, "talents": {"speakers": true, "screen": true}} 12 12 13 13 } 14 14
+2 -2
talent/speaker_attribution.py
··· 42 42 logger.info("Attribution skipped: %s", result["error"]) 43 43 reason = result["error"] 44 44 if any(seg_dir.glob("*.npz")): 45 - agents_dir = seg_dir / "agents" 45 + agents_dir = seg_dir / "talents" 46 46 agents_dir.mkdir(parents=True, exist_ok=True) 47 47 out_path = agents_dir / "speaker_labels.json" 48 48 with open(out_path, "w", encoding="utf-8") as fh: ··· 58 58 if not labels: 59 59 reason = "no_embeddings" 60 60 if any(seg_dir.glob("*.npz")): 61 - agents_dir = seg_dir / "agents" 61 + agents_dir = seg_dir / "talents" 62 62 agents_dir.mkdir(parents=True, exist_ok=True) 63 63 out_path = agents_dir / "speaker_labels.json" 64 64 with open(out_path, "w", encoding="utf-8") as fh:
+1 -1
talent/timeline.md
··· 9 9 "schedule": "daily", 10 10 "priority": 10, 11 11 "output": "md", 12 - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 12 + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 13 13 14 14 } 15 15
+2 -2
tests/baselines/api/agents/agents-day.json tests/baselines/api/talents/talents-day.json
··· 1 1 { 2 - "agents": { 2 + "talents": { 3 3 "anticipation": { 4 4 "app": null, 5 5 "color": "#4527a0", ··· 457 457 "title": "Verona" 458 458 } 459 459 }, 460 - "runs": [] 460 + "uses": [] 461 461 }
tests/baselines/api/agents/badge-count.json tests/baselines/api/talents/badge-count.json
tests/baselines/api/agents/preview.json tests/baselines/api/talents/preview.json
+11 -11
tests/baselines/api/agents/run-detail.json tests/baselines/api/talents/run-detail.json
··· 4 4 "error_message": null, 5 5 "events": [ 6 6 { 7 - "agent": "solstone", 8 - "agent_id": "1700000000001", 9 - "event": "agent_updated", 10 - "ts": 1700000000200 11 - }, 12 - { 13 - "agent_id": "1700000000001", 7 + "use_id": "1700000000001", 14 8 "args": null, 15 9 "call_id": "call_001", 16 10 "event": "tool_end", ··· 19 13 "ts": 1700000000500 20 14 }, 21 15 { 22 - "agent_id": "1700000000001", 16 + "use_id": "1700000000001", 23 17 "args": { 24 18 "limit": 5, 25 19 "query": "project updates" ··· 30 24 "ts": 1700000000400 31 25 }, 32 26 { 33 - "agent_id": "1700000000001", 27 + "use_id": "1700000000001", 34 28 "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", 35 29 "event": "thinking", 36 30 "ts": 1700000000300 37 31 }, 38 32 { 39 - "agent_id": "1700000000001", 33 + "use_id": "1700000000001", 40 34 "event": "finish", 41 35 "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", 42 36 "ts": 1700000000600, ··· 46 40 } 47 41 }, 48 42 { 49 - "agent_id": "1700000000001", 43 + "use_id": "1700000000001", 50 44 "event": "start", 51 45 "model": "gpt-4o", 52 46 "name": "default", 53 47 "prompt": "Search for meetings about project updates", 54 48 "provider": "openai", 55 49 "ts": 1700000000100 50 + }, 51 + { 52 + "talent": "solstone", 53 + "use_id": "1700000000001", 54 + "event": "talent_updated", 55 + "ts": 1700000000200 56 56 } 57 57 ], 58 58 "facet": null,
tests/baselines/api/agents/stats-month.json tests/baselines/api/talents/stats-month.json
tests/baselines/api/agents/updated-days.json tests/baselines/api/talents/updated-days.json
+4 -4
tests/baselines/api/calendar/day-events.json
··· 10 10 "Romeo Montague", 11 11 "Mercutio Escalus" 12 12 ], 13 - "source": "20260304/agents/flow.md", 13 + "source": "20260304/talents/flow.md", 14 14 "startTime": "2026-03-04T09:00:00", 15 15 "subject": "", 16 16 "summary": "Conference keynote featuring Juliet Capulet", ··· 27 27 "Juliet Capulet", 28 28 "Romeo Montague" 29 29 ], 30 - "source": "20260304/agents/flow.md", 30 + "source": "20260304/talents/flow.md", 31 31 "startTime": "2026-03-04T18:00:00", 32 32 "subject": "", 33 33 "summary": "Networking event", ··· 43 43 "participants": [ 44 44 "Juliet Capulet" 45 45 ], 46 - "source": "20260304/agents/flow.md", 46 + "source": "20260304/talents/flow.md", 47 47 "startTime": "2026-03-04T09:00:00", 48 48 "subject": "", 49 49 "summary": "Juliet presented on unified API gateways", ··· 60 60 "Romeo Montague", 61 61 "Mercutio Escalus" 62 62 ], 63 - "source": "20260304/agents/flow.md", 63 + "source": "20260304/talents/flow.md", 64 64 "startTime": "2026-03-04T14:00:00", 65 65 "subject": "", 66 66 "summary": "Built API bridge prototype",
+1 -1
tests/baselines/api/search/search.json
··· 1 1 { 2 - "agents": [], 2 + "talents": [], 3 3 "days": [], 4 4 "facets": [ 5 5 {
+2 -2
tests/baselines/api/sol/agents-day.json tests/baselines/api/sol/talents-day.json
··· 1 1 { 2 - "agents": { 2 + "talents": { 3 3 "anticipation": { 4 4 "app": null, 5 5 "color": "#4527a0", ··· 523 523 "title": "Verona" 524 524 } 525 525 }, 526 - "runs": [] 526 + "uses": [] 527 527 }
+11 -11
tests/baselines/api/sol/run-detail.json
··· 4 4 "error_message": null, 5 5 "events": [ 6 6 { 7 - "agent": "solstone", 8 - "agent_id": "1700000000001", 9 - "event": "agent_updated", 10 - "ts": 1700000000200 11 - }, 12 - { 13 - "agent_id": "1700000000001", 7 + "use_id": "1700000000001", 14 8 "args": null, 15 9 "call_id": "call_001", 16 10 "event": "tool_end", ··· 19 13 "ts": 1700000000500 20 14 }, 21 15 { 22 - "agent_id": "1700000000001", 16 + "use_id": "1700000000001", 23 17 "args": { 24 18 "limit": 5, 25 19 "query": "project updates" ··· 30 24 "ts": 1700000000400 31 25 }, 32 26 { 33 - "agent_id": "1700000000001", 27 + "use_id": "1700000000001", 34 28 "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", 35 29 "event": "thinking", 36 30 "ts": 1700000000300 37 31 }, 38 32 { 39 - "agent_id": "1700000000001", 33 + "use_id": "1700000000001", 40 34 "event": "finish", 41 35 "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", 42 36 "ts": 1700000000600, ··· 46 40 } 47 41 }, 48 42 { 49 - "agent_id": "1700000000001", 43 + "use_id": "1700000000001", 50 44 "event": "start", 51 45 "model": "gpt-4o", 52 46 "name": "default", 53 47 "prompt": "Search for meetings about project updates", 54 48 "provider": "openai", 55 49 "ts": 1700000000100 50 + }, 51 + { 52 + "talent": "solstone", 53 + "use_id": "1700000000001", 54 + "event": "talent_updated", 55 + "ts": 1700000000200 56 56 } 57 57 ], 58 58 "facet": null,
+16 -16
tests/baselines/api/stats/stats.json
··· 8 8 "pre": "daily_schedule" 9 9 }, 10 10 "load": { 11 - "agents": false, 11 + "talents": false, 12 12 "percepts": false, 13 13 "transcripts": false 14 14 }, ··· 36 36 "post": "occurrence" 37 37 }, 38 38 "load": { 39 - "agents": { 39 + "talents": { 40 40 "screen": true 41 41 }, 42 42 "percepts": false, ··· 59 59 "pre": "documents" 60 60 }, 61 61 "load": { 62 - "agents": false, 62 + "talents": false, 63 63 "percepts": false, 64 64 "transcripts": true 65 65 }, ··· 81 81 "post": "entities" 82 82 }, 83 83 "load": { 84 - "agents": false, 84 + "talents": false, 85 85 "percepts": true, 86 86 "transcripts": true 87 87 }, ··· 106 106 "pre": "entities:entity_observer" 107 107 }, 108 108 "load": { 109 - "agents": false, 109 + "talents": false, 110 110 "percepts": false, 111 111 "transcripts": false 112 112 }, ··· 129 129 "post": "occurrence" 130 130 }, 131 131 "load": { 132 - "agents": { 132 + "talents": { 133 133 "screen": true 134 134 }, 135 135 "percepts": false, ··· 158 158 "post": "occurrence" 159 159 }, 160 160 "load": { 161 - "agents": { 161 + "talents": { 162 162 "screen": true 163 163 }, 164 164 "percepts": false, ··· 181 181 "post": "occurrence" 182 182 }, 183 183 "load": { 184 - "agents": { 184 + "talents": { 185 185 "screen": true 186 186 }, 187 187 "percepts": false, ··· 207 207 "post": "occurrence" 208 208 }, 209 209 "load": { 210 - "agents": { 210 + "talents": { 211 211 "screen": true 212 212 }, 213 213 "percepts": false, ··· 234 234 "post": "occurrence" 235 235 }, 236 236 "load": { 237 - "agents": { 237 + "talents": { 238 238 "screen": true 239 239 }, 240 240 "percepts": false, ··· 257 257 "post": "anticipation" 258 258 }, 259 259 "load": { 260 - "agents": { 260 + "talents": { 261 261 "screen": true 262 262 }, 263 263 "percepts": false, ··· 276 276 "color": "#9c27b0", 277 277 "description": "Creates a detailed documentary record of screen activity. Focuses on the 'what' - chronological account with preserved details, excerpts, and entities.", 278 278 "load": { 279 - "agents": false, 279 + "talents": false, 280 280 "percepts": "required", 281 281 "transcripts": true 282 282 }, ··· 293 293 "color": "#ff6f00", 294 294 "description": "Unified segment understanding — density, content type, entities, facets, speakers, and routing recommendations in a single pass", 295 295 "load": { 296 - "agents": false, 296 + "talents": false, 297 297 "percepts": true, 298 298 "transcripts": true 299 299 }, ··· 320 320 "pre": "skills" 321 321 }, 322 322 "load": { 323 - "agents": false, 323 + "talents": false, 324 324 "percepts": false, 325 325 "transcripts": false 326 326 }, ··· 341 341 "pre": "speaker_attribution" 342 342 }, 343 343 "load": { 344 - "agents": { 344 + "talents": { 345 345 "screen": true, 346 346 "speakers": true 347 347 }, ··· 363 363 "post": "occurrence" 364 364 }, 365 365 "load": { 366 - "agents": { 366 + "talents": { 367 367 "screen": true 368 368 }, 369 369 "percepts": false,
+19 -19
tests/fixtures/journal/AGENTS.md
··· 14 14 ┌─────────────────────────────────────┐ 15 15 │ LAYER 3: AGENT OUTPUTS │ Narrative summaries 16 16 │ (Markdown files) │ "What it means" 17 - │ - agents/*.md (daily outputs) │ 17 + │ - talents/*.md (daily outputs) │ 18 18 │ - *.md (segment outputs) │ 19 19 └─────────────────────────────────────┘ 20 20 ↑ synthesized from ··· 42 42 |------|------------|----------| 43 43 | **Capture** | Raw audio/video recording | `*.flac`, `*.ogg`, `*.opus`, `*.wav`, `*.webm` | 44 44 | **Extract** | Structured data from captures | `*.jsonl` | 45 - | **Agent Output** | AI-generated narrative summary | `agents/*.md`, `HHMMSS_LEN/*.md` | 45 + | **Agent Output** | AI-generated narrative summary | `talents/*.md`, `HHMMSS_LEN/*.md` | 46 46 47 47 **Organization** 48 48 ··· 67 67 | `chronicle/` | Container for daily capture folders (`YYYYMMDD/`) containing segments, extracts, and agent outputs | 68 68 | `entities/` | Journal-level entity identity records (`<id>/entity.json`) | 69 69 | `facets/` | Facet-specific data: entity relationships, todos, events, news, action logs | 70 - | `agents/` | Agent run logs in per-agent subdirectories (`<name>/<id>.jsonl`), day indexes (`<day>.jsonl`), and latest-run symlinks (`<name>.log`) | 70 + | `talents/` | Talent run logs in per-talent subdirectories (`<name>/<id>.jsonl`), day indexes (`<day>.jsonl`), and latest-run symlinks (`<name>.log`) | 71 71 | `apps/` | App-specific storage (distinct from codebase `apps/`) | 72 72 | `streams/` | Per-stream state files (`<name>.json`) tracking segment chains and sequence numbers | 73 73 | `imports/` | Imported audio files and processing artifacts | ··· 186 186 187 187 "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`). 188 188 189 - 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. 189 + 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. 190 190 191 191 Raw media is never deleted from segments that haven't finished processing. A segment is considered complete only when all four checks pass: 192 192 193 - - No `_active.jsonl` files in `agents/` (no running agents) 193 + - No `_active.jsonl` files in `talents/` (no running talents) 194 194 - `audio.jsonl` (or `*_audio.jsonl`) exists if audio raw media was captured 195 195 - `screen.jsonl` (or `*_screen.jsonl`) exists if video raw media was captured 196 - - `agents/speaker_labels.json` exists if voice embeddings (`.npz`) are present 196 + - `talents/speaker_labels.json` exists if voice embeddings (`.npz`) are present 197 197 198 198 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. 199 199 ··· 732 732 "text": "Review project proposal" 733 733 }, 734 734 "facet": "work", 735 - "agent_id": "1765870373972" 735 + "use_id": "1765870373972" 736 736 } 737 737 ``` 738 738 ··· 746 746 - `action` – Action name (e.g., "todo_add", "identity_update") 747 747 - `params` – Action-specific parameters 748 748 - `facet` – Facet name (only present in facet-scoped logs) 749 - - `agent_id` – Agent ID (only present for agent tool actions) 749 + - `use_id` – Agent ID (only present for agent tool actions) 750 750 751 751 These logs enable auditing, debugging, and potential rollback of automated actions. 752 752 ··· 777 777 Required fields: 778 778 - `timestamp` – Unix timestamp in milliseconds (13 digits) 779 779 - `model` – Model identifier (e.g., "gemini-2.5-flash", "gpt-5", "claude-sonnet-4-5") 780 - - `context` – Calling context (e.g., "agent.name.agent_id" or "module.function:line") 780 + - `context` – Calling context (e.g., "agent.name.use_id" or "module.function:line") 781 781 - `usage` – Token counts dictionary with normalized field names 782 782 783 783 Optional fields: ··· 795 795 796 796 ## Agent Event Logs 797 797 798 - 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. 798 + 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. 799 799 800 800 **Directory layout:** 801 801 - `<name>/` – per-agent subdirectory (e.g., `default/`, `entities--observer/`) 802 - - `<name>/<agent_id>_active.jsonl` – currently running agent (renamed when complete) 803 - - `<name>/<agent_id>.jsonl` – completed agent session 802 + - `<name>/<use_id>_active.jsonl` – currently running agent (renamed when complete) 803 + - `<name>/<use_id>.jsonl` – completed agent session 804 804 - `<name>.log` – symlink to the latest completed run for each agent name 805 805 - `<day>.jsonl` – day index with one summary line per agent that completed on that day 806 806 807 - The `agent_id` is a Unix timestamp in milliseconds that uniquely identifies the session. 807 + The `use_id` is a Unix timestamp in milliseconds that uniquely identifies the session. 808 808 809 809 **Event format (JSONL):** 810 810 ··· 1039 1039 - **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) 1040 1040 1041 1041 ```jsonl 1042 - {"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"} 1043 - {"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"} 1042 + {"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"} 1043 + {"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"} 1044 1044 ``` 1045 1045 1046 1046 **Common fields:** ··· 1068 1068 1069 1069 #### Daily outputs 1070 1070 1071 - Post-processing generates day-level outputs in the `agents/` directory that synthesize all segments. 1071 + Post-processing generates day-level outputs in the `talents/` directory that synthesize all segments. 1072 1072 1073 1073 **Generator discovery:** Available generator types are discovered at runtime from: 1074 1074 - `talent/*.md` – system generator templates (files with `schedule` field but no `tools` field) ··· 1077 1077 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. 1078 1078 1079 1079 **Output naming:** 1080 - - System outputs: `agents/{agent}.md` (e.g., `agents/flow.md`, `agents/meetings.md`) 1081 - - App outputs: `agents/_{app}_{agent}.md` (e.g., `agents/_entities_observer.md`) 1082 - - JSON output: `agents/{agent}.json` when metadata specifies `"output": "json"` 1080 + - System outputs: `talents/{agent}.md` (e.g., `talents/flow.md`, `talents/meetings.md`) 1081 + - App outputs: `talents/_{app}_{agent}.md` (e.g., `talents/_entities_observer.md`) 1082 + - JSON output: `talents/{agent}.json` when metadata specifies `"output": "json"` 1083 1083 1084 1084 Each generator type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form.
-2
tests/fixtures/journal/agents/20231113.jsonl
··· 1 - {"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"} 2 - {"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"}
-4
tests/fixtures/journal/agents/20231114.jsonl
··· 1 - {"agent_id": "1700000000001", "name": "default", "day": "20231114", "facet": null, "ts": 1700000000001, "status": "completed", "runtime_seconds": 0.6, "provider": "openai", "model": "gpt-4o", "schedule": "daily"} 2 - {"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"} 3 - {"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"} 4 - {"agent_id": "1700000000004", "name": "default", "day": "20231114", "facet": null, "ts": 1700000180000, "status": "completed", "runtime_seconds": 2.1, "provider": "openai", "model": "gpt-4o"}
-3
tests/fixtures/journal/agents/20260304.jsonl
··· 1 - {"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"} 2 - {"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"} 3 - {"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"}
-3
tests/fixtures/journal/agents/20260305.jsonl
··· 1 - {"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"} 2 - {"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"} 3 - {"agent_id": "1772737200001", "name": "default", "day": "20260305", "facet": "verona", "ts": 1772737200000, "status": "completed", "runtime_seconds": 12.3, "provider": "openai", "model": "gpt-4o", "schedule": "segment"}
-2
tests/fixtures/journal/agents/20260306.jsonl
··· 1 - {"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"} 2 - {"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"}
-2
tests/fixtures/journal/agents/20260307.jsonl
··· 1 - {"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"} 2 - {"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"}
-3
tests/fixtures/journal/agents/20260308.jsonl
··· 1 - {"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"} 2 - {"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"} 3 - {"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"}
-1
tests/fixtures/journal/agents/20260309.jsonl
··· 1 - {"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"}
-4
tests/fixtures/journal/agents/20260310.jsonl
··· 1 - {"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"} 2 - {"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"} 3 - {"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"} 4 - {"agent_id": "1773187200001", "name": "default", "day": "20260310", "facet": "verona", "ts": 1773187200000, "status": "completed", "runtime_seconds": 15.7, "provider": "openai", "model": "gpt-4o", "schedule": "segment"}
-1
tests/fixtures/journal/agents/default.log
··· 1 - default/1700000000001.jsonl
-7
tests/fixtures/journal/agents/default/1700000000001.jsonl
··· 1 - {"event": "request", "ts": 1700000000001, "agent_id": "1700000000001", "prompt": "Search for meetings about project updates", "name": "default", "provider": "openai"} 2 - {"event": "start", "prompt": "Search for meetings about project updates", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1700000000100, "agent_id": "1700000000001"} 3 - {"event": "agent_updated", "agent": "solstone", "ts": 1700000000200, "agent_id": "1700000000001"} 4 - {"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"} 5 - {"event": "tool_start", "tool": "search_events", "args": {"query": "project updates", "limit": 5}, "call_id": "call_001", "ts": 1700000000400, "agent_id": "1700000000001"} 6 - {"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"} 7 - {"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}}
+7 -7
tests/fixtures/journal/agents/default/1772737200001.jsonl tests/fixtures/journal/talents/default/1772737200001.jsonl
··· 1 - {"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"} 2 - {"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"} 3 - {"event": "agent_updated", "agent": "solstone", "ts": 1772737200200, "agent_id": "1772737200001"} 4 - {"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"} 5 - {"event": "tool_start", "tool": "search_entities", "args": {"query": "Capulet Industries API", "limit": 5}, "call_id": "call_rj001", "ts": 1772737200400, "agent_id": "1772737200001"} 6 - {"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"} 7 - {"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}} 1 + {"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"} 2 + {"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"} 3 + {"event": "talent_updated", "talent": "solstone", "ts": 1772737200200, "use_id": "1772737200001"} 4 + {"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"} 5 + {"event": "tool_start", "tool": "search_entities", "args": {"query": "Capulet Industries API", "limit": 5}, "call_id": "call_rj001", "ts": 1772737200400, "use_id": "1772737200001"} 6 + {"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"} 7 + {"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}}
+5 -5
tests/fixtures/journal/agents/default/1773187200001.jsonl tests/fixtures/journal/talents/default/1773187200001.jsonl
··· 1 - {"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"} 2 - {"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"} 3 - {"event": "agent_updated", "agent": "solstone", "ts": 1773187200200, "agent_id": "1773187200001"} 4 - {"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"} 5 - {"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}} 1 + {"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"} 2 + {"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"} 3 + {"event": "talent_updated", "talent": "solstone", "ts": 1773187200200, "use_id": "1773187200001"} 4 + {"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"} 5 + {"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}}
-3
tests/fixtures/journal/agents/flow/1700000000002.jsonl
··· 1 - {"event": "request", "ts": 1700000060000, "agent_id": "1700000000002", "prompt": "Analyze conversation flow", "name": "flow", "provider": "anthropic"} 2 - {"event": "start", "prompt": "Analyze conversation flow", "name": "flow", "model": "claude-3-haiku", "provider": "anthropic", "ts": 1700000060100, "agent_id": "1700000000002"} 3 - {"event": "error", "ts": 1700000060200, "agent_id": "1700000000002", "error": "Rate limit exceeded: too many requests"}
tests/fixtures/journal/chronicle/20240101/agents/flow.md tests/fixtures/journal/chronicle/20240101/talents/flow.md
tests/fixtures/journal/chronicle/20240101/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20240101/talents/knowledge_graph.md
tests/fixtures/journal/chronicle/20240101/agents/meetings.md tests/fixtures/journal/chronicle/20240101/talents/meetings.md
tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/audio.md tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/audio.md
tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/screen.md tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/screen.md
tests/fixtures/journal/chronicle/20240102/agents/flow.md tests/fixtures/journal/chronicle/20240102/talents/flow.md
tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/audio.md tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/audio.md
tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/screen.md tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/screen.md
tests/fixtures/journal/chronicle/20260304/agents/flow.md tests/fixtures/journal/chronicle/20260304/talents/flow.md
tests/fixtures/journal/chronicle/20260304/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20260304/talents/knowledge_graph.md
tests/fixtures/journal/chronicle/20260304/agents/meetings.md tests/fixtures/journal/chronicle/20260304/talents/meetings.md
tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/audio.md tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/screen.md tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speaker_labels.json tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speaker_labels.json
tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speakers.json tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speakers.json
tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/audio.md tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/screen.md tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/audio.md tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/screen.md tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260305/agents/flow.md tests/fixtures/journal/chronicle/20260305/talents/flow.md
tests/fixtures/journal/chronicle/20260305/agents/meetings.md tests/fixtures/journal/chronicle/20260305/talents/meetings.md
tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/audio.md tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/screen.md tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/audio.md tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/screen.md tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/audio.md tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/screen.md tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260306/agents/flow.md tests/fixtures/journal/chronicle/20260306/talents/flow.md
tests/fixtures/journal/chronicle/20260306/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20260306/talents/knowledge_graph.md
tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/audio.md tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/screen.md tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/audio.md tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/screen.md tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/audio.md tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/screen.md tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/audio.md tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/screen.md tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260307/agents/flow.md tests/fixtures/journal/chronicle/20260307/talents/flow.md
tests/fixtures/journal/chronicle/20260307/agents/meetings.md tests/fixtures/journal/chronicle/20260307/talents/meetings.md
tests/fixtures/journal/chronicle/20260307/default/100000_300/agents/audio.md tests/fixtures/journal/chronicle/20260307/default/100000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260307/default/150000_300/agents/audio.md tests/fixtures/journal/chronicle/20260307/default/150000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260308/agents/flow.md tests/fixtures/journal/chronicle/20260308/talents/flow.md
tests/fixtures/journal/chronicle/20260308/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20260308/talents/knowledge_graph.md
tests/fixtures/journal/chronicle/20260308/agents/meetings.md tests/fixtures/journal/chronicle/20260308/talents/meetings.md
tests/fixtures/journal/chronicle/20260308/default/100000_300/agents/audio.md tests/fixtures/journal/chronicle/20260308/default/100000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260308/default/153000_300/agents/audio.md tests/fixtures/journal/chronicle/20260308/default/153000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260309/agents/flow.md tests/fixtures/journal/chronicle/20260309/talents/flow.md
tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/audio.md tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/screen.md tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/audio.md tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/screen.md tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/audio.md tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/screen.md tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260310/agents/flow.md tests/fixtures/journal/chronicle/20260310/talents/flow.md
tests/fixtures/journal/chronicle/20260310/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20260310/talents/knowledge_graph.md
tests/fixtures/journal/chronicle/20260310/agents/meetings.md tests/fixtures/journal/chronicle/20260310/talents/meetings.md
tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/audio.md tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/screen.md tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260310/default/100000_300/agents/audio.md tests/fixtures/journal/chronicle/20260310/default/100000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/audio.md tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/audio.md
tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/screen.md tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/screen.md
tests/fixtures/journal/chronicle/20260415/agents/knowledge_graph.md tests/fixtures/journal/chronicle/20260415/talents/knowledge_graph.md
+2 -2
tests/fixtures/journal/facets/capulet/events/20260304.jsonl
··· 1 - {"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"} 2 - {"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"} 1 + {"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"} 2 + {"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"}
+1 -1
tests/fixtures/journal/facets/capulet/events/20260306.jsonl
··· 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"} 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/talents/flow.md", "participants": ["Tybalt Capulet"], "work": true, "details": "Proposed building mesh routing from scratch"}
+1 -1
tests/fixtures/journal/facets/capulet/events/20260307.jsonl
··· 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"} 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/talents/flow.md", "participants": ["Tybalt Capulet", "Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Terminated Mercutio's consulting contract"}
+1 -1
tests/fixtures/journal/facets/capulet/events/20260310.jsonl
··· 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"} 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/talents/meetings.md", "participants": ["Juliet Capulet", "Romeo Montague", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Juliet named co-lead"}
+2 -2
tests/fixtures/journal/facets/montague/events/20260304.jsonl
··· 1 - {"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"} 2 - {"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"} 1 + {"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"} 2 + {"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"}
+1 -1
tests/fixtures/journal/facets/montague/events/20260305.jsonl
··· 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"} 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/talents/meetings.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Romeo mentioned conference ideas"}
+1 -1
tests/fixtures/journal/facets/montague/events/20260306.jsonl
··· 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"} 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/talents/flow.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Benvolio noticed late-night commits"}
+2 -2
tests/fixtures/journal/facets/montague/events/20260307.jsonl
··· 1 - {"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"} 2 - {"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"} 1 + {"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"} 2 + {"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"}
+1 -1
tests/fixtures/journal/facets/montague/events/20260309.jsonl
··· 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"} 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/talents/flow.md", "participants": ["Benvolio Montague"], "work": true, "details": "Staging cluster for Verona Platform demo"}
+1 -1
tests/fixtures/journal/facets/montague/events/20260310.jsonl
··· 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"} 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/talents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards approved joint venture"}
+1 -1
tests/fixtures/journal/facets/personal/events/20240101.jsonl
··· 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"} 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/talents/activity.md", "participants": [], "work": false, "details": "Strength training day"}
+1 -1
tests/fixtures/journal/facets/verona/events/20260305.jsonl
··· 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"} 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/talents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Created Balcony App with sub-ms latency"}
+1 -1
tests/fixtures/journal/facets/verona/events/20260306.jsonl
··· 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"} 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/talents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Full e2e integration achieved"}
+1 -1
tests/fixtures/journal/facets/verona/events/20260308.jsonl
··· 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"} 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/talents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence"], "work": true, "details": "Proposed board presentation strategy"}
+1 -1
tests/fixtures/journal/facets/verona/events/20260309.jsonl
··· 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"} 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/talents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet", "Benvolio Montague", "Nurse Angela"], "work": true, "details": "Demo deployed, presentation rehearsed"}
+1 -1
tests/fixtures/journal/facets/verona/events/20260310.jsonl
··· 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"} 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/talents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards voted yes"}
+2 -2
tests/fixtures/journal/facets/work/events/20240101.jsonl
··· 1 - {"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"} 2 - {"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"} 1 + {"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"} 2 + {"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"}
+1 -1
tests/fixtures/journal/facets/work/events/20240105.jsonl
··· 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"} 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/talents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"}
+1 -1
tests/fixtures/journal/maint/agents/000_migrate_agent_layout.jsonl tests/fixtures/journal/maint/sol/000_migrate_agent_layout.jsonl
··· 1 - {"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"]} 1 + {"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"]} 2 2 {"event": "line", "ts": 1770778598147, "line": "Migration complete"} 3 3 {"event": "line", "ts": 1770778598147, "line": " moved: 0"} 4 4 {"event": "line", "ts": 1770778598147, "line": " cleaned: 0"}
+1 -1
tests/fixtures/journal/maint/agents/001_migrate_agent_run_logs.jsonl tests/fixtures/journal/maint/sol/001_migrate_agent_run_logs.jsonl
··· 1 - {"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"]} 1 + {"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"]} 2 2 {"event": "line", "ts": 1770952500054, "line": "Migrating agent run logs in: tests/fixtures/journal/agents"} 3 3 {"event": "line", "ts": 1770952500054, "line": "Migration complete"} 4 4 {"event": "line", "ts": 1770952500054, "line": " moved: 0"}
+2 -2
tests/fixtures/journal/sol/briefing.md
··· 36 36 37 37 ## Forward Look 38 38 39 - - **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [anticipation](sol://20260327/agents/anticipation)). 39 + - **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [anticipation](sol://20260327/talents/anticipation)). 40 40 - **Wednesday** — Deadline for the compliance audit documentation. 41 - - Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [anticipation](sol://20260327/agents/anticipation)). 41 + - Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [anticipation](sol://20260327/talents/anticipation)). 42 42 43 43 ## Reading 44 44
+2
tests/fixtures/journal/talents/20231113.jsonl
··· 1 + {"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"} 2 + {"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"}
+4
tests/fixtures/journal/talents/20231114.jsonl
··· 1 + {"use_id": "1700000000001", "name": "default", "day": "20231114", "facet": null, "ts": 1700000000001, "status": "completed", "runtime_seconds": 0.6, "provider": "openai", "model": "gpt-4o", "schedule": "daily"} 2 + {"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"} 3 + {"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"} 4 + {"use_id": "1700000000004", "name": "default", "day": "20231114", "facet": null, "ts": 1700000180000, "status": "completed", "runtime_seconds": 2.1, "provider": "openai", "model": "gpt-4o"}
+3
tests/fixtures/journal/talents/20260304.jsonl
··· 1 + {"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"} 2 + {"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"} 3 + {"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"}
+3
tests/fixtures/journal/talents/20260305.jsonl
··· 1 + {"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"} 2 + {"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"} 3 + {"use_id": "1772737200001", "name": "default", "day": "20260305", "facet": "verona", "ts": 1772737200000, "status": "completed", "runtime_seconds": 12.3, "provider": "openai", "model": "gpt-4o", "schedule": "segment"}
+2
tests/fixtures/journal/talents/20260306.jsonl
··· 1 + {"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"} 2 + {"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"}
+2
tests/fixtures/journal/talents/20260307.jsonl
··· 1 + {"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"} 2 + {"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"}
+3
tests/fixtures/journal/talents/20260308.jsonl
··· 1 + {"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"} 2 + {"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"} 3 + {"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"}
+1
tests/fixtures/journal/talents/20260309.jsonl
··· 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"}
+4
tests/fixtures/journal/talents/20260310.jsonl
··· 1 + {"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"} 2 + {"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"} 3 + {"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"} 4 + {"use_id": "1773187200001", "name": "default", "day": "20260310", "facet": "verona", "ts": 1773187200000, "status": "completed", "runtime_seconds": 15.7, "provider": "openai", "model": "gpt-4o", "schedule": "segment"}
+7
tests/fixtures/journal/talents/default.log
··· 1 + {"event": "request", "ts": 1700000000001, "use_id": "1700000000001", "prompt": "Search for meetings about project updates", "name": "default", "provider": "openai"} 2 + {"event": "start", "prompt": "Search for meetings about project updates", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1700000000100, "use_id": "1700000000001"} 3 + {"event": "talent_updated", "talent": "solstone", "ts": 1700000000200, "use_id": "1700000000001"} 4 + {"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"} 5 + {"event": "tool_start", "tool": "search_events", "args": {"query": "project updates", "limit": 5}, "call_id": "call_001", "ts": 1700000000400, "use_id": "1700000000001"} 6 + {"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"} 7 + {"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}}
+7
tests/fixtures/journal/talents/default/1700000000001.jsonl
··· 1 + {"event": "request", "ts": 1700000000001, "use_id": "1700000000001", "prompt": "Search for meetings about project updates", "name": "default", "provider": "openai"} 2 + {"event": "start", "prompt": "Search for meetings about project updates", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1700000000100, "use_id": "1700000000001"} 3 + {"event": "talent_updated", "talent": "solstone", "ts": 1700000000200, "use_id": "1700000000001"} 4 + {"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"} 5 + {"event": "tool_start", "tool": "search_events", "args": {"query": "project updates", "limit": 5}, "call_id": "call_001", "ts": 1700000000400, "use_id": "1700000000001"} 6 + {"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"} 7 + {"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}}
+3
tests/fixtures/journal/talents/flow/1700000000002.jsonl
··· 1 + {"event": "request", "ts": 1700000060000, "use_id": "1700000000002", "prompt": "Analyze conversation flow", "name": "flow", "provider": "anthropic"} 2 + {"event": "start", "prompt": "Analyze conversation flow", "name": "flow", "model": "claude-3-haiku", "provider": "anthropic", "ts": 1700000060100, "use_id": "1700000000002"} 3 + {"event": "error", "ts": 1700000060200, "use_id": "1700000000002", "error": "Rate limit exceeded: too many requests"}
+4 -4
tests/integration/test_anthropic_provider.py
··· 62 62 } 63 63 ) 64 64 65 - # Run the sol agents command 66 - cmd = ["sol", "agents"] 65 + # Run the sol think.talents command 66 + cmd = ["sol", "providers", "check"] 67 67 result = subprocess.run( 68 68 cmd, 69 69 env=env, ··· 170 170 } 171 171 ) 172 172 173 - # Run the sol agents command 174 - cmd = ["sol", "agents"] 173 + # Run the sol think.talents command 174 + cmd = ["sol", "providers", "check"] 175 175 result = subprocess.run( 176 176 cmd, 177 177 env=env,
+2 -2
tests/integration/test_callosum.py
··· 152 152 # Wait for client to connect 153 153 time.sleep(0.2) 154 154 155 - client.emit("cortex", "agent_start", agent_id="123", name="analyst") 155 + client.emit("cortex", "agent_start", use_id="123", name="analyst") 156 156 157 157 # Wait for broadcast 158 158 time.sleep(0.2) ··· 171 171 msg = received[0] 172 172 assert msg["tract"] == "cortex" 173 173 assert msg["event"] == "agent_start" 174 - assert msg["agent_id"] == "123" 174 + assert msg["use_id"] == "123" 175 175 assert msg["name"] == "analyst" 176 176 177 177 # Cleanup
+13 -15
tests/integration/test_cortex.py
··· 12 12 13 13 from think.callosum import CallosumServer 14 14 from think.cortex import CortexService 15 - from think.cortex_client import cortex_agents, cortex_request 15 + from think.cortex_client import cortex_request, cortex_uses 16 16 from think.utils import now_ms 17 17 18 18 ··· 47 47 cortex = CortexService(journal_path=str(integration_journal_path)) 48 48 49 49 # Verify agents directory was created 50 - agents_dir = integration_journal_path / "agents" 50 + agents_dir = integration_journal_path / "talents" 51 51 assert agents_dir.exists() 52 52 assert agents_dir.is_dir() 53 53 54 54 # Verify service initializes correctly 55 55 status = cortex.get_status() 56 - assert status["running_agents"] == 0 56 + assert status["running_uses"] == 0 57 57 assert status["agent_ids"] == [] 58 58 59 59 ··· 75 75 time.sleep(0.1) 76 76 77 77 # Create a request 78 - agent_id = cortex_request(prompt="Test prompt", name="default", provider="openai") 78 + use_id = cortex_request(prompt="Test prompt", name="default", provider="openai") 79 79 80 80 time.sleep(0.2) 81 81 ··· 85 85 assert request["prompt"] == "Test prompt" 86 86 assert request["name"] == "default" 87 87 assert request["provider"] == "openai" 88 - assert request["agent_id"] == agent_id 88 + assert request["use_id"] == use_id 89 89 90 90 listener.stop() 91 91 ··· 96 96 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(integration_journal_path) 97 97 98 98 # Create a mock agent script that just echoes 99 - agents_dir = integration_journal_path / "agents" 99 + agents_dir = integration_journal_path / "talents" 100 100 agents_dir.mkdir(parents=True, exist_ok=True) 101 101 102 102 # Start Cortex service in background ··· 124 124 time.sleep(0.2) 125 125 126 126 # Make a request (this will fail because no real agent, but we can verify the flow) 127 - agent_id = cortex_request( 128 - prompt="Test end-to-end", name="default", provider="openai" 129 - ) 127 + use_id = cortex_request(prompt="Test end-to-end", name="default", provider="openai") 130 128 131 129 # Wait for at least request event 132 130 time.sleep(1.0) ··· 134 132 # Should have received the request event 135 133 request_events = [e for e in received_events if e.get("event") == "request"] 136 134 assert len(request_events) >= 1 137 - assert request_events[0]["agent_id"] == agent_id 135 + assert request_events[0]["use_id"] == use_id 138 136 139 137 watcher.stop() 140 138 cortex.stop() 141 139 142 140 143 141 @pytest.mark.integration 144 - def test_cortex_agents_listing(integration_journal_path): 145 - """Test listing agents from the cortex_agents function.""" 142 + def test_cortex_uses_listing(integration_journal_path): 143 + """Test listing agents from the cortex_uses function.""" 146 144 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(integration_journal_path) 147 145 148 146 # Create some test agent files 149 - agents_dir = integration_journal_path / "agents" 147 + agents_dir = integration_journal_path / "talents" 150 148 agents_dir.mkdir(parents=True, exist_ok=True) 151 149 152 150 # Get initial count 153 - initial_result = cortex_agents() 151 + initial_result = cortex_uses() 154 152 initial_count = len(initial_result["agents"]) 155 153 156 154 ts = now_ms() ··· 175 173 f.write("\n") 176 174 177 175 # List agents 178 - result = cortex_agents() 176 + result = cortex_uses() 179 177 180 178 # Should have one more than before 181 179 assert len(result["agents"]) == initial_count + 1
+4 -4
tests/integration/test_google_provider.py
··· 62 62 } 63 63 ) 64 64 65 - # Run the sol agents command 66 - cmd = ["sol", "agents"] 65 + # Run the sol think.talents command 66 + cmd = ["sol", "providers", "check"] 67 67 result = subprocess.run( 68 68 cmd, 69 69 env=env, ··· 154 154 } 155 155 ) 156 156 157 - # Run the sol agents command 158 - cmd = ["sol", "agents"] 157 + # Run the sol think.talents command 158 + cmd = ["sol", "providers", "check"] 159 159 result = subprocess.run( 160 160 cmd, 161 161 env=env,
+7 -7
tests/integration/test_openai_provider.py
··· 62 62 } 63 63 ) 64 64 65 - # Run the sol agents command 66 - cmd = ["sol", "agents"] 65 + # Run the sol think.talents command 66 + cmd = ["sol", "providers", "check"] 67 67 result = subprocess.run( 68 68 cmd, 69 69 env=env, ··· 162 162 } 163 163 ) 164 164 165 - # Run the sol agents command 166 - cmd = ["sol", "agents"] 165 + # Run the sol think.talents command 166 + cmd = ["sol", "providers", "check"] 167 167 result = subprocess.run( 168 168 cmd, 169 169 env=env, ··· 236 236 env["_SOLSTONE_JOURNAL_OVERRIDE"] = journal_path 237 237 env["OPENAI_API_KEY"] = api_key 238 238 239 - # Include extra_context like get_agent() does in production 239 + # Include extra_context like get_talent() does in production 240 240 # This exercises the _convert_turns_to_items() code path 241 241 ndjson_input = json.dumps( 242 242 { ··· 249 249 } 250 250 ) 251 251 252 - # Run the sol agents command 253 - cmd = ["sol", "agents"] 252 + # Run the sol think.talents command 253 + cmd = ["sol", "providers", "check"] 254 254 result = subprocess.run( 255 255 cmd, 256 256 env=env,
+4 -4
tests/test_activities.py
··· 638 638 639 639 def _setup_segment(tmpdir, day, segment, facet, state): 640 640 """Helper to create an activity_state.json file in a segment.""" 641 - agents_dir = ( 642 - Path(tmpdir) / "chronicle" / day / "default" / segment / "agents" / facet 641 + talents_dir = ( 642 + Path(tmpdir) / "chronicle" / day / "default" / segment / "talents" / facet 643 643 ) 644 - agents_dir.mkdir(parents=True, exist_ok=True) 645 - state_file = agents_dir / "activity_state.json" 644 + talents_dir.mkdir(parents=True, exist_ok=True) 645 + state_file = talents_dir / "activity_state.json" 646 646 state_file.write_text(json.dumps(state)) 647 647 648 648
+52 -52
tests/test_activity_state.py
··· 15 15 def test_extracts_facet_from_valid_path(self): 16 16 from talent.activity_state import _extract_facet_from_output_path 17 17 18 - path = "/journal/20260130/143000_300/agents/work/activity_state.json" 18 + path = "/journal/20260130/143000_300/talents/work/activity_state.json" 19 19 assert _extract_facet_from_output_path(path) == "work" 20 20 21 21 def test_extracts_facet_with_hyphen(self): 22 22 from talent.activity_state import _extract_facet_from_output_path 23 23 24 - path = "/journal/20260130/143000_300/agents/my-project/activity_state.json" 24 + path = "/journal/20260130/143000_300/talents/my-project/activity_state.json" 25 25 assert _extract_facet_from_output_path(path) == "my-project" 26 26 27 27 def test_returns_none_for_empty_path(self): ··· 37 37 assert _extract_facet_from_output_path("/path/to/facets.json") is None 38 38 # No facet directory 39 39 assert ( 40 - _extract_facet_from_output_path("/path/to/agents/activity_state.json") 40 + _extract_facet_from_output_path("/path/to/talents/activity_state.json") 41 41 is None 42 42 ) 43 43 ··· 146 146 Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" 147 147 ) 148 148 segment_dir.mkdir(parents=True) 149 - (segment_dir / "agents" / "work").mkdir(parents=True) 149 + (segment_dir / "talents" / "work").mkdir(parents=True) 150 150 151 151 state = [ 152 152 { ··· 157 157 "level": "high", 158 158 } 159 159 ] 160 - (segment_dir / "agents/work/activity_state.json").write_text( 160 + (segment_dir / "talents/work/activity_state.json").write_text( 161 161 json.dumps(state) 162 162 ) 163 163 ··· 184 184 Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" 185 185 ) 186 186 segment_dir.mkdir(parents=True) 187 - (segment_dir / "agents" / "work").mkdir(parents=True) 187 + (segment_dir / "talents" / "work").mkdir(parents=True) 188 188 189 189 loaded, segment = load_previous_state( 190 190 "20260130", "100000_300", "work", stream="default" ··· 208 208 Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" 209 209 ) 210 210 segment_dir.mkdir(parents=True) 211 - (segment_dir / "agents" / "work").mkdir(parents=True) 211 + (segment_dir / "talents" / "work").mkdir(parents=True) 212 212 213 213 # Write a dict (old format) — should be rejected 214 - (segment_dir / "agents/work/activity_state.json").write_text( 214 + (segment_dir / "talents/work/activity_state.json").write_text( 215 215 '{"active": [], "ended": []}' 216 216 ) 217 217 ··· 364 364 day_dir = Path(tmpdir) / "chronicle" / "20260130" 365 365 day_dir.mkdir(parents=True) 366 366 (day_dir / "default" / "100000_300").mkdir(parents=True) 367 - (day_dir / "default" / "100000_300" / "agents" / "work").mkdir( 367 + (day_dir / "default" / "100000_300" / "talents" / "work").mkdir( 368 368 parents=True 369 369 ) 370 370 segment_dir = day_dir / "default" / "110000_300" ··· 391 391 day_dir 392 392 / "default" 393 393 / "100000_300" 394 - / "agents/work/activity_state.json" 394 + / "talents/work/activity_state.json" 395 395 ).write_text(json.dumps(prev_state)) 396 396 397 397 context = { 398 398 "day": "20260130", 399 399 "segment": "110000_300", 400 400 "stream": "default", 401 - "output_path": "/journal/20260130/110000_300/agents/work/activity_state.json", 401 + "output_path": "/journal/20260130/110000_300/talents/work/activity_state.json", 402 402 "transcript": "User is typing code...", 403 403 "meta": {}, 404 404 } ··· 424 424 425 425 context = { 426 426 "segment": "100000_300", 427 - "output_path": "/path/to/agents/work/activity_state.json", 427 + "output_path": "/path/to/talents/work/activity_state.json", 428 428 } 429 429 assert pre_process(context) is None 430 430 ··· 433 433 434 434 context = { 435 435 "day": "20260130", 436 - "output_path": "/path/to/agents/work/activity_state.json", 436 + "output_path": "/path/to/talents/work/activity_state.json", 437 437 } 438 438 assert pre_process(context) is None 439 439 ··· 487 487 # Previous segment with active meeting 488 488 prev_dir = day_dir / "default" / "100000_300" 489 489 prev_dir.mkdir(parents=True) 490 - (prev_dir / "agents" / "work").mkdir(parents=True) 490 + (prev_dir / "talents" / "work").mkdir(parents=True) 491 491 prev_state = [ 492 492 { 493 493 "activity": "meeting", ··· 497 497 "level": "high", 498 498 } 499 499 ] 500 - (prev_dir / "agents/work/activity_state.json").write_text( 500 + (prev_dir / "talents/work/activity_state.json").write_text( 501 501 json.dumps(prev_state) 502 502 ) 503 503 ··· 519 519 "day": "20260130", 520 520 "segment": "100500_300", 521 521 "stream": "default", 522 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 522 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 523 523 } 524 524 525 525 result = post_process(llm_output, context) ··· 545 545 # Previous segment with active meeting 546 546 prev_dir = day_dir / "default" / "100000_300" 547 547 prev_dir.mkdir(parents=True) 548 - (prev_dir / "agents" / "work").mkdir(parents=True) 548 + (prev_dir / "talents" / "work").mkdir(parents=True) 549 549 prev_state = [ 550 550 { 551 551 "activity": "meeting", ··· 555 555 "level": "high", 556 556 } 557 557 ] 558 - (prev_dir / "agents/work/activity_state.json").write_text( 558 + (prev_dir / "talents/work/activity_state.json").write_text( 559 559 json.dumps(prev_state) 560 560 ) 561 561 ··· 575 575 "day": "20260130", 576 576 "segment": "100500_300", 577 577 "stream": "default", 578 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 578 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 579 579 } 580 580 581 581 result = post_process(llm_output, context) ··· 669 669 # Previous segment — email already ended 670 670 prev_dir = day_dir / "default" / "100000_300" 671 671 prev_dir.mkdir(parents=True) 672 - (prev_dir / "agents" / "work").mkdir(parents=True) 672 + (prev_dir / "talents" / "work").mkdir(parents=True) 673 673 prev_state = [ 674 674 { 675 675 "activity": "email", ··· 678 678 "description": "Replied to boss", 679 679 } 680 680 ] 681 - (prev_dir / "agents/work/activity_state.json").write_text( 681 + (prev_dir / "talents/work/activity_state.json").write_text( 682 682 json.dumps(prev_state) 683 683 ) 684 684 ··· 698 698 "day": "20260130", 699 699 "segment": "100500_300", 700 700 "stream": "default", 701 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 701 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 702 702 } 703 703 704 704 result = post_process(llm_output, context) ··· 725 725 # Previous segment — email ended with different description 726 726 prev_dir = day_dir / "default" / "100000_300" 727 727 prev_dir.mkdir(parents=True) 728 - (prev_dir / "agents" / "work").mkdir(parents=True) 728 + (prev_dir / "talents" / "work").mkdir(parents=True) 729 729 prev_state = [ 730 730 { 731 731 "activity": "email", ··· 734 734 "description": "Replied to boss", 735 735 } 736 736 ] 737 - (prev_dir / "agents/work/activity_state.json").write_text( 737 + (prev_dir / "talents/work/activity_state.json").write_text( 738 738 json.dumps(prev_state) 739 739 ) 740 740 ··· 754 754 "day": "20260130", 755 755 "segment": "100500_300", 756 756 "stream": "default", 757 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 757 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 758 758 } 759 759 760 760 result = post_process(llm_output, context) ··· 806 806 807 807 prev_dir = day_dir / "default" / "100000_300" 808 808 prev_dir.mkdir(parents=True) 809 - (prev_dir / "agents" / "work").mkdir(parents=True) 809 + (prev_dir / "talents" / "work").mkdir(parents=True) 810 810 prev_state = [ 811 811 { 812 812 "activity": "meeting", ··· 816 816 "level": "high", 817 817 } 818 818 ] 819 - (prev_dir / "agents/work/activity_state.json").write_text( 819 + (prev_dir / "talents/work/activity_state.json").write_text( 820 820 json.dumps(prev_state) 821 821 ) 822 822 ··· 842 842 "day": "20260130", 843 843 "segment": "100500_300", 844 844 "stream": "default", 845 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 845 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 846 846 } 847 847 848 848 result = post_process(llm_output, context) ··· 927 927 928 928 prev_dir = day_dir / "default" / "100000_300" 929 929 prev_dir.mkdir(parents=True) 930 - (prev_dir / "agents" / "work").mkdir(parents=True) 930 + (prev_dir / "talents" / "work").mkdir(parents=True) 931 931 prev_state = [ 932 932 { 933 933 "activity": "meeting", ··· 937 937 "level": "high", 938 938 } 939 939 ] 940 - (prev_dir / "agents/work/activity_state.json").write_text( 940 + (prev_dir / "talents/work/activity_state.json").write_text( 941 941 json.dumps(prev_state) 942 942 ) 943 943 ··· 958 958 "day": "20260130", 959 959 "segment": "100500_300", 960 960 "stream": "default", 961 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 961 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 962 962 } 963 963 964 964 result = post_process(llm_output, context) ··· 984 984 985 985 prev_dir = day_dir / "default" / "100000_300" 986 986 prev_dir.mkdir(parents=True) 987 - (prev_dir / "agents" / "work").mkdir(parents=True) 987 + (prev_dir / "talents" / "work").mkdir(parents=True) 988 988 prev_state = [ 989 989 { 990 990 "activity": "meeting", ··· 1001 1001 "level": "medium", 1002 1002 }, 1003 1003 ] 1004 - (prev_dir / "agents/work/activity_state.json").write_text( 1004 + (prev_dir / "talents/work/activity_state.json").write_text( 1005 1005 json.dumps(prev_state) 1006 1006 ) 1007 1007 ··· 1022 1022 "day": "20260130", 1023 1023 "segment": "100500_300", 1024 1024 "stream": "default", 1025 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 1025 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 1026 1026 } 1027 1027 1028 1028 result = post_process(llm_output, context) ··· 1069 1069 1070 1070 prev_dir = day_dir / "default" / "100000_300" 1071 1071 prev_dir.mkdir(parents=True) 1072 - (prev_dir / "agents" / "work").mkdir(parents=True) 1072 + (prev_dir / "talents" / "work").mkdir(parents=True) 1073 1073 prev_state = [ 1074 1074 { 1075 1075 "activity": "meeting", ··· 1079 1079 "level": "high", 1080 1080 } 1081 1081 ] 1082 - (prev_dir / "agents/work/activity_state.json").write_text( 1082 + (prev_dir / "talents/work/activity_state.json").write_text( 1083 1083 json.dumps(prev_state) 1084 1084 ) 1085 1085 ··· 1100 1100 "day": "20260130", 1101 1101 "segment": "100500_300", 1102 1102 "stream": "default", 1103 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 1103 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 1104 1104 } 1105 1105 1106 1106 result = post_process(llm_output, context) ··· 1124 1124 1125 1125 prev_dir = day_dir / "default" / "100000_300" 1126 1126 prev_dir.mkdir(parents=True) 1127 - (prev_dir / "agents" / "work").mkdir(parents=True) 1127 + (prev_dir / "talents" / "work").mkdir(parents=True) 1128 1128 prev_state = [ 1129 1129 { 1130 1130 "activity": "meeting", ··· 1134 1134 "level": "high", 1135 1135 } 1136 1136 ] 1137 - (prev_dir / "agents/work/activity_state.json").write_text( 1137 + (prev_dir / "talents/work/activity_state.json").write_text( 1138 1138 json.dumps(prev_state) 1139 1139 ) 1140 1140 ··· 1154 1154 "day": "20260130", 1155 1155 "segment": "100500_300", 1156 1156 "stream": "default", 1157 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 1157 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 1158 1158 } 1159 1159 1160 1160 result = post_process(llm_output, context) ··· 1208 1208 context = { 1209 1209 "day": "20260130", 1210 1210 "segment": "143000_300", 1211 - "output_path": "/j/20260130/143000_300/agents/work/activity_state.json", 1211 + "output_path": "/j/20260130/143000_300/talents/work/activity_state.json", 1212 1212 } 1213 1213 1214 1214 with patch("talent.activity_state.callosum_send") as mock_send: ··· 1244 1244 1245 1245 prev_dir = day_dir / "default" / "100000_300" 1246 1246 prev_dir.mkdir(parents=True) 1247 - (prev_dir / "agents" / "work").mkdir(parents=True) 1247 + (prev_dir / "talents" / "work").mkdir(parents=True) 1248 1248 prev_state = [ 1249 1249 { 1250 1250 "activity": "coding", ··· 1254 1254 "level": "high", 1255 1255 } 1256 1256 ] 1257 - (prev_dir / "agents/work/activity_state.json").write_text( 1257 + (prev_dir / "talents/work/activity_state.json").write_text( 1258 1258 json.dumps(prev_state) 1259 1259 ) 1260 1260 ··· 1275 1275 "day": "20260130", 1276 1276 "segment": "100500_300", 1277 1277 "stream": "default", 1278 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 1278 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 1279 1279 } 1280 1280 1281 1281 with patch("talent.activity_state.callosum_send") as mock_send: ··· 1306 1306 1307 1307 prev_dir = day_dir / "default" / "100000_300" 1308 1308 prev_dir.mkdir(parents=True) 1309 - (prev_dir / "agents" / "work").mkdir(parents=True) 1309 + (prev_dir / "talents" / "work").mkdir(parents=True) 1310 1310 prev_state = [ 1311 1311 { 1312 1312 "activity": "meeting", ··· 1316 1316 "level": "high", 1317 1317 } 1318 1318 ] 1319 - (prev_dir / "agents/work/activity_state.json").write_text( 1319 + (prev_dir / "talents/work/activity_state.json").write_text( 1320 1320 json.dumps(prev_state) 1321 1321 ) 1322 1322 ··· 1336 1336 "day": "20260130", 1337 1337 "segment": "100500_300", 1338 1338 "stream": "default", 1339 - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", 1339 + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", 1340 1340 } 1341 1341 1342 1342 with patch("talent.activity_state.callosum_send") as mock_send: ··· 1387 1387 context = { 1388 1388 "day": "20260130", 1389 1389 "segment": "143000_300", 1390 - "output_path": "/j/20260130/143000_300/agents/work/activity_state.json", 1390 + "output_path": "/j/20260130/143000_300/talents/work/activity_state.json", 1391 1391 } 1392 1392 1393 1393 with patch("talent.activity_state.callosum_send") as mock_send: ··· 1440 1440 1441 1441 context = { 1442 1442 "segment": "143000_300", 1443 - "output_path": f"{tmpdir}/20260130/143000_300/agents/work/activity_state.json", 1443 + "output_path": f"{tmpdir}/20260130/143000_300/talents/work/activity_state.json", 1444 1444 } 1445 1445 1446 1446 with patch("talent.activity_state.callosum_send"): ··· 1483 1483 1484 1484 context = { 1485 1485 "segment": "143000_300", 1486 - "output_path": f"{tmpdir}/20260130/143000_300/agents/work/activity_state.json", 1486 + "output_path": f"{tmpdir}/20260130/143000_300/talents/work/activity_state.json", 1487 1487 } 1488 1488 1489 1489 with caplog.at_level(logging.WARNING, logger="talent.activity_state"): ··· 1539 1539 1540 1540 context = { 1541 1541 "segment": "143000_300", 1542 - "output_path": f"{tmpdir}/20260130/143000_300/agents/work/activity_state.json", 1542 + "output_path": f"{tmpdir}/20260130/143000_300/talents/work/activity_state.json", 1543 1543 } 1544 1544 1545 1545 with patch("talent.activity_state.callosum_send"): ··· 1589 1589 1590 1590 context = { 1591 1591 "segment": "143000_300", 1592 - "output_path": f"{tmpdir}/20260130/143000_300/agents/new_facet/activity_state.json", 1592 + "output_path": f"{tmpdir}/20260130/143000_300/talents/new_facet/activity_state.json", 1593 1593 } 1594 1594 1595 1595 with patch("talent.activity_state.callosum_send"):
+20 -20
tests/test_agent_fallback.py tests/test_talent_fallback.py
··· 10 10 11 11 import pytest 12 12 13 - from think.agents import _is_retryable_error 14 13 from think.models import ( 15 14 TYPE_DEFAULTS, 16 15 get_backup_provider, 17 16 is_provider_healthy, 18 17 should_recheck_health, 19 18 ) 19 + from think.talents import _is_retryable_error 20 20 21 21 22 22 def test_is_provider_healthy_all_failed(): ··· 100 100 101 101 def _patch_prepare_config_dependencies(monkeypatch): 102 102 monkeypatch.setattr( 103 - "think.talent.get_agent", lambda *args, **kwargs: _mock_base_agent_config() 103 + "think.talent.get_talent", lambda *args, **kwargs: _mock_base_agent_config() 104 104 ) 105 105 monkeypatch.setattr( 106 106 "think.talent.key_to_context", lambda _name: "talent.system.default" ··· 112 112 113 113 114 114 def test_preflight_swap_unhealthy_primary(monkeypatch): 115 - from think.agents import prepare_config 115 + from think.talents import prepare_config 116 116 117 117 _patch_prepare_config_dependencies(monkeypatch) 118 118 monkeypatch.setattr( ··· 135 135 136 136 137 137 def test_preflight_no_swap_healthy_primary(monkeypatch): 138 - from think.agents import prepare_config 138 + from think.talents import prepare_config 139 139 140 140 _patch_prepare_config_dependencies(monkeypatch) 141 141 monkeypatch.setattr( ··· 151 151 152 152 153 153 def test_preflight_no_swap_no_backup_key(monkeypatch): 154 - from think.agents import prepare_config 154 + from think.talents import prepare_config 155 155 156 156 _patch_prepare_config_dependencies(monkeypatch) 157 157 monkeypatch.setattr( ··· 169 169 170 170 171 171 def test_on_failure_retry_cogitate(monkeypatch): 172 - from think.agents import _execute_with_tools 172 + from think.talents import _execute_with_tools 173 173 174 174 events = [] 175 175 attempts = {"primary": 0, "backup": 0} ··· 219 219 220 220 221 221 def test_on_failure_retry_cogitate_uses_context_from_name(monkeypatch): 222 - from think.agents import _execute_with_tools 222 + from think.talents import _execute_with_tools 223 223 224 224 events = [] 225 225 seen = {} ··· 267 267 268 268 269 269 def test_on_failure_retry_generate(monkeypatch): 270 - from think.agents import _execute_generate 270 + from think.talents import _execute_generate 271 271 272 272 events = [] 273 273 calls = {"count": 0} ··· 310 310 311 311 312 312 def test_on_failure_no_retry_value_error(monkeypatch): 313 - from think.agents import _execute_generate 313 + from think.talents import _execute_generate 314 314 315 315 events = [] 316 316 assert _is_retryable_error(ValueError("bad input")) is False ··· 338 338 339 339 340 340 def test_on_failure_both_fail_raises_original(monkeypatch): 341 - from think.agents import _execute_generate 341 + from think.talents import _execute_generate 342 342 343 343 events = [] 344 344 calls = {"count": 0} ··· 375 375 376 376 377 377 def test_fallback_event_emitted(): 378 - from think.agents import _run_agent 378 + from think.talents import _run_talent 379 379 380 380 events = [] 381 381 config = { ··· 387 387 "fallback_from": "google", 388 388 } 389 389 390 - asyncio.run(_run_agent(config, events.append, dry_run=True)) 390 + asyncio.run(_run_talent(config, events.append, dry_run=True)) 391 391 392 392 fallback_events = [e for e in events if e.get("event") == "fallback"] 393 393 assert len(fallback_events) == 1 ··· 395 395 396 396 397 397 def test_recheck_requested_on_stale(monkeypatch): 398 - from think.agents import _execute_with_tools 398 + from think.talents import _execute_with_tools 399 399 400 400 async def pass_cogitate(*_args, **kwargs): 401 401 on_event = kwargs.get("on_event") ··· 425 425 426 426 427 427 def test_main_async_no_duplicate_error_when_evented(monkeypatch, capsys): 428 - from think.agents import main_async 428 + from think.talents import main_async 429 429 430 430 ndjson_input = json.dumps({"name": "unified", "prompt": "hello"}) 431 431 monkeypatch.setattr("sys.stdin", StringIO(ndjson_input)) 432 432 433 - async def fake_run_agent(_config, emit_event, dry_run=False): 433 + async def fake_run_talent(_config, emit_event, dry_run=False): 434 434 emit_event({"event": "error", "error": "provider failed"}) 435 435 exc = RuntimeError("provider failed") 436 436 setattr(exc, "_evented", True) ··· 441 441 mock_args.dry_run = False 442 442 mock_args.subcommand = None 443 443 444 - monkeypatch.setattr("think.agents.setup_cli", lambda _parser: mock_args) 444 + monkeypatch.setattr("think.talents.setup_cli", lambda _parser: mock_args) 445 445 monkeypatch.setattr( 446 - "think.agents.setup_logging", 446 + "think.talents.setup_logging", 447 447 lambda _verbose=False: MagicMock(), 448 448 ) 449 449 monkeypatch.setattr( 450 - "think.agents.prepare_config", lambda _request: {"type": "cogitate"} 450 + "think.talents.prepare_config", lambda _request: {"type": "cogitate"} 451 451 ) 452 - monkeypatch.setattr("think.agents.validate_config", lambda _config: None) 453 - monkeypatch.setattr("think.agents._run_agent", fake_run_agent) 452 + monkeypatch.setattr("think.talents.validate_config", lambda _config: None) 453 + monkeypatch.setattr("think.talents._run_talent", fake_run_talent) 454 454 455 455 asyncio.run(main_async()) 456 456
+68 -68
tests/test_agents_check.py tests/test_providers_check.py
··· 11 11 12 12 13 13 def test_run_check_writes_health_file(tmp_path, monkeypatch): 14 - """_run_check writes agents health results to _SOLSTONE_JOURNAL_OVERRIDE/health/agents.json.""" 15 - import think.agents as agents 14 + """_run_check writes provider health results to _SOLSTONE_JOURNAL_OVERRIDE/health/talents.json.""" 15 + import think.providers_cli as providers_cli 16 16 17 17 fake_registry = {"fake": object()} 18 18 fake_defaults = { ··· 25 25 26 26 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 27 27 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 28 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 29 - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) 28 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 29 + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "ok")) 30 30 31 31 async def mock_check_cogitate(*_args): 32 32 return "ok", "ok" 33 33 34 - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 34 + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) 35 35 36 36 args = argparse.Namespace( 37 37 provider=None, ··· 43 43 ) 44 44 45 45 with pytest.raises(SystemExit) as exc_info: 46 - asyncio.run(agents._run_check(args)) 46 + asyncio.run(providers_cli._run_check(args)) 47 47 48 48 assert exc_info.value.code == 0 49 49 50 - health_file = tmp_path / "health" / "agents.json" 50 + health_file = tmp_path / "health" / "talents.json" 51 51 assert health_file.exists() 52 52 53 53 payload = json.loads(health_file.read_text()) ··· 61 61 62 62 def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): 63 63 """_run_check exits 1 when any check fails.""" 64 - import think.agents as agents 64 + import think.providers_cli as providers_cli 65 65 66 66 fake_registry = {"fake": object()} 67 67 fake_defaults = { ··· 74 74 75 75 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 76 76 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 77 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 78 - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) 77 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 78 + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "ok")) 79 79 80 80 async def mock_check_cogitate(*_args): 81 81 return "fail", "FAIL: timeout" 82 82 83 - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 83 + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) 84 84 85 85 args = argparse.Namespace( 86 86 provider=None, ··· 92 92 ) 93 93 94 94 with pytest.raises(SystemExit) as exc_info: 95 - asyncio.run(agents._run_check(args)) 95 + asyncio.run(providers_cli._run_check(args)) 96 96 97 97 assert exc_info.value.code == 1 98 98 99 - health_file = tmp_path / "health" / "agents.json" 99 + health_file = tmp_path / "health" / "talents.json" 100 100 payload = json.loads(health_file.read_text()) 101 101 assert payload["summary"]["passed"] == 3 102 102 assert payload["summary"]["skipped"] == 0 ··· 105 105 106 106 def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch): 107 107 """_run_check exits 1 when all checks for a provider fail.""" 108 - import think.agents as agents 108 + import think.providers_cli as providers_cli 109 109 110 110 fake_registry = {"fake": object()} 111 111 fake_defaults = { ··· 118 118 119 119 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 120 120 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 121 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 121 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 122 122 monkeypatch.setattr( 123 - agents, "_check_generate", lambda *_args: ("fail", "FAIL: key not set") 123 + providers_cli, "_check_generate", lambda *_args: ("fail", "FAIL: key not set") 124 124 ) 125 125 126 126 async def mock_check_cogitate(*_args): 127 127 return "fail", "FAIL: key not set" 128 128 129 - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 129 + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) 130 130 131 131 args = argparse.Namespace( 132 132 provider=None, ··· 138 138 ) 139 139 140 140 with pytest.raises(SystemExit) as exc_info: 141 - asyncio.run(agents._run_check(args)) 141 + asyncio.run(providers_cli._run_check(args)) 142 142 143 143 assert exc_info.value.code == 1 144 144 145 - health_file = tmp_path / "health" / "agents.json" 145 + health_file = tmp_path / "health" / "talents.json" 146 146 payload = json.loads(health_file.read_text()) 147 147 assert payload["summary"]["passed"] == 0 148 148 assert payload["summary"]["skipped"] == 0 ··· 151 151 152 152 def test_run_check_dedup_same_model(tmp_path, monkeypatch): 153 153 """_run_check deduplicates checks when tiers resolve to the same model.""" 154 - import think.agents as agents 154 + import think.providers_cli as providers_cli 155 155 156 156 fake_registry = {"fake": object()} 157 157 fake_defaults = { ··· 164 164 165 165 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 166 166 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 167 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 167 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 168 168 169 169 gen_mock = MagicMock(return_value=("ok", "ok")) 170 - monkeypatch.setattr(agents, "_check_generate", gen_mock) 170 + monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) 171 171 172 172 cog_inner = MagicMock(return_value=("ok", "ok")) 173 173 174 174 async def mock_check_cogitate(*args): 175 175 return cog_inner(*args) 176 176 177 - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 177 + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) 178 178 179 179 args = argparse.Namespace( 180 180 provider=None, ··· 186 186 ) 187 187 188 188 with pytest.raises(SystemExit) as exc_info: 189 - asyncio.run(agents._run_check(args)) 189 + asyncio.run(providers_cli._run_check(args)) 190 190 191 191 assert exc_info.value.code == 0 192 192 assert gen_mock.call_count == 1 193 193 assert cog_inner.call_count == 1 194 194 195 - health_file = tmp_path / "health" / "agents.json" 195 + health_file = tmp_path / "health" / "talents.json" 196 196 assert health_file.exists() 197 197 198 198 payload = json.loads(health_file.read_text()) ··· 212 212 213 213 def test_run_check_targeted_filters_to_configured_pairs(tmp_path, monkeypatch): 214 214 """--targeted filters checks to only configured provider+tier pairs.""" 215 - import think.agents as agents 215 + import think.providers_cli as providers_cli 216 216 217 217 fake_registry = {"provA": object(), "provB": object(), "provC": object()} 218 218 fake_defaults = { ··· 228 228 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 229 229 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 230 230 monkeypatch.setattr("think.models.TYPE_DEFAULTS", fake_type_defaults) 231 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 232 - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) 231 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 232 + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "ok")) 233 233 234 234 async def mock_check_cogitate(*_args): 235 235 return "ok", "ok" 236 236 237 - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 237 + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) 238 238 239 239 # Mock get_config to return no overrides (use TYPE_DEFAULTS) 240 240 monkeypatch.setattr("think.utils.get_config", lambda: {}) ··· 258 258 ) 259 259 260 260 with pytest.raises(SystemExit) as exc_info: 261 - asyncio.run(agents._run_check(args)) 261 + asyncio.run(providers_cli._run_check(args)) 262 262 263 263 assert exc_info.value.code == 0 264 264 265 - health_file = tmp_path / "health" / "agents.json" 265 + health_file = tmp_path / "health" / "talents.json" 266 266 payload = json.loads(health_file.read_text()) 267 267 # Expected targeted pairs: (provA, 2), (provB, 2), (provC, 2) = 3 pairs × 2 interfaces = 6 checks 268 268 assert payload["summary"]["total"] == 6 ··· 274 274 """--targeted exits silently when another targeted check holds the lock.""" 275 275 import fcntl 276 276 277 - import think.agents as agents 277 + import think.providers_cli as providers_cli 278 278 279 279 fake_registry = {"fake": object()} 280 280 fake_defaults = {"fake": {1: "m", 2: "m", 3: "m"}} ··· 286 286 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 287 287 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 288 288 monkeypatch.setattr("think.models.TYPE_DEFAULTS", fake_type_defaults) 289 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 289 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 290 290 monkeypatch.setattr("think.utils.get_config", lambda: {}) 291 291 monkeypatch.setattr("think.models.get_backup_provider", lambda _: None) 292 292 ··· 297 297 fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) 298 298 299 299 gen_mock = MagicMock(return_value=("ok", "ok")) 300 - monkeypatch.setattr(agents, "_check_generate", gen_mock) 300 + monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) 301 301 302 302 args = argparse.Namespace( 303 303 provider=None, ··· 309 309 ) 310 310 311 311 # Should return silently (no SystemExit, no checks run) 312 - asyncio.run(agents._run_check(args)) 312 + asyncio.run(providers_cli._run_check(args)) 313 313 assert gen_mock.call_count == 0 314 314 315 315 # No health file written 316 - assert not (tmp_path / "health" / "agents.json").exists() 316 + assert not (tmp_path / "health" / "talents.json").exists() 317 317 318 318 lock_file.close() 319 319 320 320 321 321 def test_check_generate_logs_token_usage(monkeypatch): 322 322 """_check_generate logs token usage when result includes usage data.""" 323 - import think.agents as agents 323 + import think.providers_cli as providers_cli 324 324 325 325 fake_module = MagicMock() 326 326 fake_module.run_generate.return_value = { ··· 339 339 log_mock = MagicMock() 340 340 monkeypatch.setattr("think.models.log_token_usage", log_mock) 341 341 342 - status, msg = agents._check_generate("fake", 2, 30) 342 + status, msg = providers_cli._check_generate("fake", 2, 30) 343 343 344 344 assert status == "ok" 345 345 assert msg == "OK" ··· 351 351 ) 352 352 353 353 354 - def test_cortex_start_emits_agents_check(tmp_path): 355 - """Cortex startup requests an agents health check via supervisor.""" 354 + def test_cortex_start_emits_providers_check(tmp_path): 355 + """Cortex startup requests a providers health check via supervisor.""" 356 356 from think.cortex import CortexService 357 357 358 358 cortex = CortexService(journal_path=str(tmp_path)) ··· 366 366 cortex.start() 367 367 368 368 cortex.callosum.emit.assert_any_call( 369 - "supervisor", "request", cmd=["sol", "agents", "check"] 369 + "supervisor", "request", cmd=["sol", "providers", "check"] 370 370 ) 371 371 372 372 373 373 def test_missing_env_key_returns_skip(monkeypatch): 374 374 """_check_generate returns skip status when env key is not set.""" 375 - import think.agents as agents 375 + import think.providers_cli as providers_cli 376 376 377 377 monkeypatch.setattr( 378 378 "think.providers.PROVIDER_METADATA", ··· 380 380 ) 381 381 monkeypatch.delenv("FAKE_API_KEY", raising=False) 382 382 383 - status, msg = agents._check_generate("fake", 2, 30) 383 + status, msg = providers_cli._check_generate("fake", 2, 30) 384 384 assert status == "skip" 385 385 assert "Fake Provider not configured" in msg 386 386 assert "FAKE_API_KEY" in msg ··· 388 388 389 389 def test_cogitate_missing_binary_returns_skip(monkeypatch): 390 390 """_check_cogitate returns skip when CLI binary is not installed.""" 391 - import think.agents as agents 392 391 import think.providers as providers 392 + import think.providers_cli as providers_cli 393 393 394 394 monkeypatch.setitem( 395 395 providers.PROVIDER_METADATA, ··· 403 403 monkeypatch.setenv("FAKE_API_KEY", "test-key") 404 404 monkeypatch.setattr("shutil.which", lambda _: None) 405 405 406 - status, msg = asyncio.run(agents._check_cogitate("fake", 2, 30)) 406 + status, msg = asyncio.run(providers_cli._check_cogitate("fake", 2, 30)) 407 407 assert status == "skip" 408 408 assert "nonexistent-binary-xyz CLI not installed" in msg 409 409 410 410 411 411 def test_all_skip_exits_zero(tmp_path, monkeypatch): 412 412 """Exit code is 0 when all results are skipped (no fails).""" 413 - import think.agents as agents 413 + import think.providers_cli as providers_cli 414 414 415 415 fake_registry = {"fake": object()} 416 416 fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} 417 417 418 418 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 419 419 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 420 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 420 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 421 421 monkeypatch.setattr( 422 - agents, "_check_generate", lambda *_args: ("skip", "not configured") 422 + providers_cli, "_check_generate", lambda *_args: ("skip", "not configured") 423 423 ) 424 424 425 425 async def mock_check_cogitate(*_args): 426 426 return "skip", "not configured" 427 427 428 - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 428 + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) 429 429 430 430 args = argparse.Namespace( 431 431 provider=None, ··· 437 437 ) 438 438 439 439 with pytest.raises(SystemExit) as exc_info: 440 - asyncio.run(agents._run_check(args)) 440 + asyncio.run(providers_cli._run_check(args)) 441 441 442 442 assert exc_info.value.code == 0 443 443 444 - payload = json.loads((tmp_path / "health" / "agents.json").read_text()) 444 + payload = json.loads((tmp_path / "health" / "talents.json").read_text()) 445 445 assert payload["summary"]["skipped"] == 6 446 446 assert payload["summary"]["failed"] == 0 447 447 assert payload["summary"]["passed"] == 0 ··· 452 452 453 453 def test_mix_skip_and_fail_exits_one(tmp_path, monkeypatch): 454 454 """Exit code is 1 when there's a mix of skip and fail results.""" 455 - import think.agents as agents 455 + import think.providers_cli as providers_cli 456 456 457 457 fake_registry = {"fake": object()} 458 458 fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} 459 459 460 460 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 461 461 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 462 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 462 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 463 463 monkeypatch.setattr( 464 - agents, "_check_generate", lambda *_args: ("skip", "not configured") 464 + providers_cli, "_check_generate", lambda *_args: ("skip", "not configured") 465 465 ) 466 466 467 467 async def mock_check_cogitate(*_args): 468 468 return "fail", "FAIL: broken" 469 469 470 - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 470 + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) 471 471 472 472 args = argparse.Namespace( 473 473 provider=None, ··· 479 479 ) 480 480 481 481 with pytest.raises(SystemExit) as exc_info: 482 - asyncio.run(agents._run_check(args)) 482 + asyncio.run(providers_cli._run_check(args)) 483 483 484 484 assert exc_info.value.code == 1 485 485 486 - payload = json.loads((tmp_path / "health" / "agents.json").read_text()) 486 + payload = json.loads((tmp_path / "health" / "talents.json").read_text()) 487 487 assert payload["summary"]["skipped"] == 3 488 488 assert payload["summary"]["failed"] == 3 489 489 490 490 491 491 def test_skipped_count_in_summary(tmp_path, monkeypatch): 492 492 """Summary total equals passed + skipped + failed.""" 493 - import think.agents as agents 493 + import think.providers_cli as providers_cli 494 494 495 495 fake_registry = {"okp": object(), "skipP": object()} 496 496 fake_defaults = { ··· 500 500 501 501 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 502 502 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 503 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 503 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 504 504 505 505 def mock_gen(provider, tier, timeout): 506 506 if provider == "okp": 507 507 return "ok", "OK" 508 508 return "skip", "not configured" 509 509 510 - monkeypatch.setattr(agents, "_check_generate", mock_gen) 510 + monkeypatch.setattr(providers_cli, "_check_generate", mock_gen) 511 511 512 512 async def mock_cog(provider, tier, timeout): 513 513 if provider == "okp": 514 514 return "ok", "OK" 515 515 return "skip", "not configured" 516 516 517 - monkeypatch.setattr(agents, "_check_cogitate", mock_cog) 517 + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_cog) 518 518 519 519 args = argparse.Namespace( 520 520 provider=None, ··· 526 526 ) 527 527 528 528 with pytest.raises(SystemExit) as exc_info: 529 - asyncio.run(agents._run_check(args)) 529 + asyncio.run(providers_cli._run_check(args)) 530 530 531 531 assert exc_info.value.code == 0 532 - payload = json.loads((tmp_path / "health" / "agents.json").read_text()) 532 + payload = json.loads((tmp_path / "health" / "talents.json").read_text()) 533 533 summary = payload["summary"] 534 534 assert ( 535 535 summary["total"] == summary["passed"] + summary["skipped"] + summary["failed"] ··· 541 541 542 542 def test_status_field_in_json_output(tmp_path, monkeypatch, capsys): 543 543 """JSON output includes status per result and skipped in summary.""" 544 - import think.agents as agents 544 + import think.providers_cli as providers_cli 545 545 546 546 fake_registry = {"fake": object()} 547 547 fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} 548 548 549 549 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 550 550 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 551 - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 552 - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "OK")) 551 + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) 552 + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "OK")) 553 553 554 554 async def mock_cog(*_args): 555 555 return "ok", "OK" 556 556 557 - monkeypatch.setattr(agents, "_check_cogitate", mock_cog) 557 + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_cog) 558 558 559 559 args = argparse.Namespace( 560 560 provider=None, ··· 566 566 ) 567 567 568 568 with pytest.raises(SystemExit): 569 - asyncio.run(agents._run_check(args)) 569 + asyncio.run(providers_cli._run_check(args)) 570 570 571 571 captured = capsys.readouterr() 572 572 data = json.loads(captured.out)
+13 -13
tests/test_agents_ndjson.py tests/test_talents_ndjson.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Tests for NDJSON-only input in think.agents.""" 4 + """Tests for NDJSON-only input in think.talents.""" 5 5 6 6 import asyncio 7 7 import json ··· 19 19 """Set up a temporary journal directory.""" 20 20 journal_path = tmp_path / "journal" 21 21 journal_path.mkdir() 22 - agents_path = journal_path / "agents" 22 + agents_path = journal_path / "talents" 23 23 agents_path.mkdir() 24 24 25 25 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_path)) ··· 87 87 monkeypatch.setitem(sys.modules, "agents", MagicMock()) 88 88 89 89 # Mock prepare_config to avoid needing real agent configs 90 - monkeypatch.setattr("think.agents.prepare_config", mock_prepare_config) 90 + monkeypatch.setattr("think.talents.prepare_config", mock_prepare_config) 91 91 92 92 93 93 def test_ndjson_single_request(mock_journal, monkeypatch, capsys): ··· 110 110 111 111 mock_all_providers(monkeypatch) 112 112 113 - from think.agents import main_async 113 + from think.talents import main_async 114 114 115 - with patch("think.agents.setup_cli", return_value=mock_args): 115 + with patch("think.talents.setup_cli", return_value=mock_args): 116 116 asyncio.run(main_async()) 117 117 118 118 captured = capsys.readouterr() ··· 162 162 163 163 mock_all_providers(monkeypatch) 164 164 165 - from think.agents import main_async 165 + from think.talents import main_async 166 166 167 - with patch("think.agents.setup_cli", return_value=mock_args): 167 + with patch("think.talents.setup_cli", return_value=mock_args): 168 168 asyncio.run(main_async()) 169 169 170 170 captured = capsys.readouterr() ··· 198 198 199 199 mock_all_providers(monkeypatch) 200 200 201 - from think.agents import main_async 201 + from think.talents import main_async 202 202 203 - with patch("think.agents.setup_cli", return_value=mock_args): 203 + with patch("think.talents.setup_cli", return_value=mock_args): 204 204 asyncio.run(main_async()) 205 205 206 206 captured = capsys.readouterr() ··· 233 233 234 234 mock_all_providers(monkeypatch) 235 235 236 - from think.agents import main_async 236 + from think.talents import main_async 237 237 238 - with patch("think.agents.setup_cli", return_value=mock_args): 238 + with patch("think.talents.setup_cli", return_value=mock_args): 239 239 asyncio.run(main_async()) 240 240 241 241 captured = capsys.readouterr() ··· 263 263 264 264 mock_all_providers(monkeypatch) 265 265 266 - from think.agents import main_async 266 + from think.talents import main_async 267 267 268 - with patch("think.agents.setup_cli", return_value=mock_args): 268 + with patch("think.talents.setup_cli", return_value=mock_args): 269 269 asyncio.run(main_async()) 270 270 271 271 captured = capsys.readouterr()
+15 -15
tests/test_anthropic.py
··· 203 203 importlib.import_module("think.providers.anthropic") 204 204 ) 205 205 _setup_claude_cli_stub(monkeypatch, provider_mod) 206 - mod = importlib.reload(importlib.import_module("think.agents")) 206 + mod = importlib.reload(importlib.import_module("think.talents")) 207 207 208 208 journal = tmp_path / "journal" 209 209 journal.mkdir() 210 - agents_dir = journal / "agents" 210 + agents_dir = journal / "talents" 211 211 agents_dir.mkdir() 212 212 213 213 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) ··· 221 221 "tools": ["search_insights"], 222 222 } 223 223 ) 224 - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) 224 + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) 225 225 226 226 out_lines = capsys.readouterr().out.strip().splitlines() 227 227 events = [json.loads(line) for line in out_lines] ··· 246 246 importlib.import_module("think.providers.anthropic") 247 247 ) 248 248 _setup_claude_cli_stub(monkeypatch, provider_mod) 249 - mod = importlib.reload(importlib.import_module("think.agents")) 249 + mod = importlib.reload(importlib.import_module("think.talents")) 250 250 251 251 journal = tmp_path / "journal" 252 252 journal.mkdir() 253 - agents_dir = journal / "agents" 253 + agents_dir = journal / "talents" 254 254 agents_dir.mkdir() 255 255 256 256 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) ··· 264 264 "tools": ["search_insights"], 265 265 } 266 266 ) 267 - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) 267 + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) 268 268 269 269 # Output file functionality was removed in NDJSON-only mode 270 270 # Check stdout instead ··· 293 293 importlib.import_module("think.providers.anthropic") 294 294 ) 295 295 _setup_claude_cli_stub(monkeypatch, provider_mod, with_thinking=True) 296 - mod = importlib.reload(importlib.import_module("think.agents")) 296 + mod = importlib.reload(importlib.import_module("think.talents")) 297 297 298 298 journal = tmp_path / "journal" 299 299 journal.mkdir() 300 - agents_dir = journal / "agents" 300 + agents_dir = journal / "talents" 301 301 agents_dir.mkdir() 302 302 303 303 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) ··· 311 311 "tools": ["search_insights"], 312 312 } 313 313 ) 314 - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) 314 + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) 315 315 316 316 out_lines = capsys.readouterr().out.strip().splitlines() 317 317 events = [json.loads(line) for line in out_lines] ··· 335 335 importlib.import_module("think.providers.anthropic") 336 336 ) 337 337 _setup_claude_cli_stub(monkeypatch, provider_mod, with_redacted_thinking=True) 338 - mod = importlib.reload(importlib.import_module("think.agents")) 338 + mod = importlib.reload(importlib.import_module("think.talents")) 339 339 340 340 journal = tmp_path / "journal" 341 341 journal.mkdir() 342 - agents_dir = journal / "agents" 342 + agents_dir = journal / "talents" 343 343 agents_dir.mkdir() 344 344 345 345 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) ··· 353 353 "tools": ["search_insights"], 354 354 } 355 355 ) 356 - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) 356 + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) 357 357 358 358 out_lines = capsys.readouterr().out.strip().splitlines() 359 359 events = [json.loads(line) for line in out_lines] ··· 375 375 importlib.import_module("think.providers.anthropic") 376 376 ) 377 377 _setup_claude_cli_stub(monkeypatch, provider_mod, error=True) 378 - mod = importlib.reload(importlib.import_module("think.agents")) 378 + mod = importlib.reload(importlib.import_module("think.talents")) 379 379 380 380 journal = tmp_path / "journal" 381 381 journal.mkdir() 382 - agents_dir = journal / "agents" 382 + agents_dir = journal / "talents" 383 383 agents_dir.mkdir() 384 384 385 385 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) ··· 393 393 "tools": ["search_insights"], 394 394 } 395 395 ) 396 - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) 396 + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) 397 397 398 398 # Error events should be written to stdout 399 399 out_lines = capsys.readouterr().out.strip().splitlines()
+1 -1
tests/test_app_calendar.py
··· 134 134 def test_rejects_non_facets_path(self, calendar_client): 135 135 """Paths not starting with facets/ are rejected.""" 136 136 resp = calendar_client.get( 137 - "/app/calendar/api/activity_output/20260214/agents/flow.md" 137 + "/app/calendar/api/activity_output/20260214/talents/flow.md" 138 138 ) 139 139 assert resp.status_code == 400 140 140
+19 -19
tests/test_app_sol.py
··· 10 10 import pytest 11 11 12 12 from apps.sol.routes import _resolve_output_path 13 - from think.talent import _resolve_agent_path, get_agent, get_talent_configs 13 + from think.talent import _resolve_talent_path, get_talent, get_talent_configs 14 14 15 15 16 16 @pytest.fixture ··· 69 69 70 70 71 71 def test_resolve_agent_path_system_agent(): 72 - """Test _resolve_agent_path returns correct path for system agents.""" 73 - agent_dir, agent_name = _resolve_agent_path("unified") 72 + """Test _resolve_talent_path returns correct path for system agents.""" 73 + agent_dir, agent_name = _resolve_talent_path("unified") 74 74 75 75 assert agent_name == "chat" 76 76 assert agent_dir.name == "talent" 77 77 78 78 79 79 def test_resolve_agent_path_app_agent(): 80 - """Test _resolve_agent_path returns correct path for app agents.""" 81 - agent_dir, agent_name = _resolve_agent_path("support:support") 80 + """Test _resolve_talent_path returns correct path for app agents.""" 81 + agent_dir, agent_name = _resolve_talent_path("support:support") 82 82 83 83 assert agent_name == "support" 84 84 assert agent_dir.name == "talent" ··· 87 87 88 88 89 89 def test_resolve_agent_path_app_agent_with_underscores(): 90 - """Test _resolve_agent_path handles app names with underscores.""" 91 - agent_dir, agent_name = _resolve_agent_path("my_app:my_agent") 90 + """Test _resolve_talent_path handles app names with underscores.""" 91 + agent_dir, agent_name = _resolve_talent_path("my_app:my_agent") 92 92 93 93 assert agent_name == "my_agent" 94 94 assert agent_dir.parent.name == "my_app" 95 95 96 96 97 97 def test_get_agent_system_agent(fixture_journal): 98 - """Test get_agent loads system agents correctly.""" 99 - config = get_agent("unified") 98 + """Test get_talent loads system agents correctly.""" 99 + config = get_talent("unified") 100 100 101 101 assert config["name"] == "unified" 102 102 assert "user_instruction" in config ··· 104 104 105 105 106 106 def test_get_agent_nonexistent_raises(): 107 - """Test get_agent raises FileNotFoundError for nonexistent agents.""" 107 + """Test get_talent raises FileNotFoundError for nonexistent agents.""" 108 108 with pytest.raises(FileNotFoundError) as exc_info: 109 - get_agent("nonexistent_agent_xyz") 109 + get_talent("nonexistent_agent_xyz") 110 110 111 111 assert "nonexistent_agent_xyz" in str(exc_info.value) 112 112 113 113 114 114 def test_get_agent_nonexistent_app_agent_raises(): 115 - """Test get_agent raises FileNotFoundError for nonexistent app agents.""" 115 + """Test get_talent raises FileNotFoundError for nonexistent app agents.""" 116 116 with pytest.raises(FileNotFoundError) as exc_info: 117 - get_agent("fakeapp:fakeagent") 117 + get_talent("fakeapp:fakeagent") 118 118 119 119 assert "fakeapp:fakeagent" in str(exc_info.value) 120 120 ··· 147 147 ): 148 148 """Test get_talent_configs skips apps starting with underscore.""" 149 149 # Create a private app with an agent 150 - private_app = tmp_path / "_private_app" / "agents" 150 + private_app = tmp_path / "_private_app" / "talents" 151 151 private_app.mkdir(parents=True) 152 152 (private_app / "secret.md").write_text("Secret agent") 153 153 ··· 258 258 # Create test files 259 259 day_dir = tmp_path / "chronicle" / "20260214" 260 260 day_dir.mkdir(parents=True) 261 - (day_dir / "agents" / "flow.md").parent.mkdir(parents=True) 262 - (day_dir / "agents" / "flow.md").write_text("# Day agent output") 261 + (day_dir / "talents" / "flow.md").parent.mkdir(parents=True) 262 + (day_dir / "talents" / "flow.md").write_text("# Day agent output") 263 263 264 264 facet_dir = tmp_path / "facets" / "work" / "activities" / "20260214" / "coding_100" 265 265 facet_dir.mkdir(parents=True) ··· 273 273 274 274 def test_serves_day_relative_file(self, agents_client): 275 275 """Day-relative paths resolve under {journal}/{day}/.""" 276 - resp = agents_client.get("/app/sol/api/output/20260214/agents/flow.md") 276 + resp = agents_client.get("/app/sol/api/output/20260214/talents/flow.md") 277 277 assert resp.status_code == 200 278 278 data = resp.get_json() 279 279 assert data["content"] == "# Day agent output" ··· 293 293 294 294 def test_rejects_invalid_day_format(self, agents_client): 295 295 """Non-YYYYMMDD day returns 400.""" 296 - resp = agents_client.get("/app/sol/api/output/bad-day/agents/flow.md") 296 + resp = agents_client.get("/app/sol/api/output/bad-day/talents/flow.md") 297 297 assert resp.status_code == 400 298 298 299 299 def test_rejects_path_traversal(self, agents_client): ··· 303 303 304 304 def test_missing_file_returns_404(self, agents_client): 305 305 """Non-existent file returns 404.""" 306 - resp = agents_client.get("/app/sol/api/output/20260214/agents/nonexistent.md") 306 + resp = agents_client.get("/app/sol/api/output/20260214/talents/nonexistent.md") 307 307 assert resp.status_code == 404
+24 -24
tests/test_cluster.py
··· 20 20 '{}\n{"text": "hi"}\n' 21 21 ) 22 22 (day_dir / "default" / "120500_300").mkdir(parents=True) 23 - (day_dir / "default" / "120500_300" / "agents").mkdir() 24 - (day_dir / "default" / "120500_300" / "agents" / "screen.md").write_text( 23 + (day_dir / "default" / "120500_300" / "talents").mkdir() 24 + (day_dir / "default" / "120500_300" / "talents" / "screen.md").write_text( 25 25 "screen summary" 26 26 ) 27 27 result, counts = mod.cluster( ··· 46 46 '{"raw": "raw.flac", "model": "whisper-1"}\n' 47 47 '{"start": "00:00:01", "source": "mic", "text": "hi from audio"}\n' 48 48 ) 49 - (day_dir / "default" / "120000_300" / "agents").mkdir() 50 - (day_dir / "default" / "120000_300" / "agents" / "screen.md").write_text( 49 + (day_dir / "default" / "120000_300" / "talents").mkdir() 50 + (day_dir / "default" / "120000_300" / "talents" / "screen.md").write_text( 51 51 "screen summary content" 52 52 ) 53 53 # Test with agents=True to include *.md files ··· 170 170 '"visual_description": "VS Code with Python file"}}\n' 171 171 ) 172 172 # Also create screen.md (insight) to verify it's NOT used by cluster_period 173 - (segment / "agents").mkdir() 174 - (segment / "agents" / "screen.md").write_text("This insight should NOT appear") 173 + (segment / "talents").mkdir() 174 + (segment / "talents" / "screen.md").write_text("This insight should NOT appear") 175 175 176 176 result, counts = mod.cluster_period( 177 177 "20240101", ··· 220 220 # Create segment with multiple insight files 221 221 segment = day_dir / "default" / "100000_300" 222 222 segment.mkdir(parents=True) 223 - (segment / "agents").mkdir() 223 + (segment / "talents").mkdir() 224 224 (segment / "audio.jsonl").write_text( 225 225 '{"raw": "audio.flac"}\n{"start": "00:00:01", "text": "hello"}\n' 226 226 ) 227 - (segment / "agents" / "screen.md").write_text("Screen activity summary") 228 - (segment / "agents" / "activity.md").write_text("Activity insight content") 227 + (segment / "talents" / "screen.md").write_text("Screen activity summary") 228 + (segment / "talents" / "activity.md").write_text("Activity insight content") 229 229 # Also create screen.jsonl to verify it's NOT used when agents=True, screen=False 230 230 (segment / "screen.jsonl").write_text( 231 231 '{"raw": "screen.webm"}\n' ··· 260 260 # Create segment with raw screen data and insight file 261 261 segment = day_dir / "default" / "100000_300" 262 262 segment.mkdir(parents=True) 263 - (segment / "agents").mkdir() 263 + (segment / "talents").mkdir() 264 264 (segment / "screen.jsonl").write_text( 265 265 '{"raw": "screen.webm"}\n' 266 266 '{"timestamp": 10, "analysis": {"primary": "code_editor"}}\n' 267 267 ) 268 - (segment / "agents" / "screen.md").write_text("Screen summary insight") 268 + (segment / "talents" / "screen.md").write_text("Screen summary insight") 269 269 270 270 # Test screen=True returns raw screen data, not agent outputs 271 271 result = mod.cluster_range( ··· 434 434 # Create segment with multiple agent output files 435 435 segment = day_dir / "default" / "120000_300" 436 436 segment.mkdir(parents=True) 437 - (segment / "agents").mkdir() 437 + (segment / "talents").mkdir() 438 438 (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') 439 - (segment / "agents" / "entities.md").write_text("Entity extraction results") 440 - (segment / "agents" / "meetings.md").write_text("Meeting summary results") 441 - (segment / "agents" / "flow.md").write_text("Flow analysis results") 439 + (segment / "talents" / "entities.md").write_text("Entity extraction results") 440 + (segment / "talents" / "meetings.md").write_text("Meeting summary results") 441 + (segment / "talents" / "flow.md").write_text("Flow analysis results") 442 442 443 443 # Test filtering to only include entities 444 444 result, counts = mod.cluster( ··· 463 463 # Create segment with multiple agent output files 464 464 segment = day_dir / "default" / "120000_300" 465 465 segment.mkdir(parents=True) 466 - (segment / "agents").mkdir() 466 + (segment / "talents").mkdir() 467 467 (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') 468 - (segment / "agents" / "entities.md").write_text("Entity extraction results") 469 - (segment / "agents" / "meetings.md").write_text("Meeting summary results") 470 - (segment / "agents" / "flow.md").write_text("Flow analysis results") 468 + (segment / "talents" / "entities.md").write_text("Entity extraction results") 469 + (segment / "talents" / "meetings.md").write_text("Meeting summary results") 470 + (segment / "talents" / "flow.md").write_text("Flow analysis results") 471 471 472 472 # Test filtering to include entities and meetings but not flow 473 473 result, counts = mod.cluster( ··· 497 497 # App agent output naming: "app:agent" -> "_app_agent.md" 498 498 segment = day_dir / "default" / "120000_300" 499 499 segment.mkdir(parents=True) 500 - (segment / "agents").mkdir() 500 + (segment / "talents").mkdir() 501 501 (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') 502 - (segment / "agents" / "entities.md").write_text("System entity results") 503 - (segment / "agents" / "_todos_review.md").write_text("Todos review results") 502 + (segment / "talents" / "entities.md").write_text("System entity results") 503 + (segment / "talents" / "_todos_review.md").write_text("Todos review results") 504 504 505 505 # Test filtering to include app-namespaced agent 506 506 result, counts = mod.cluster( ··· 527 527 528 528 segment = day_dir / "default" / "120000_300" 529 529 segment.mkdir(parents=True) 530 - (segment / "agents").mkdir() 530 + (segment / "talents").mkdir() 531 531 (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') 532 - (segment / "agents" / "entities.md").write_text("Entity extraction results") 532 + (segment / "talents" / "entities.md").write_text("Entity extraction results") 533 533 534 534 # Empty dict should mean no agents 535 535 result, counts = mod.cluster(
+5 -5
tests/test_conversation.py
··· 37 37 user_message="what's our history with adrian?", 38 38 agent_response="You met Adrian at betaworks.", 39 39 talent="unified", 40 - agent_id="12345", 40 + use_id="12345", 41 41 ) 42 42 43 43 jsonl_path = journal_dir / "conversation" / "exchanges.jsonl" ··· 54 54 assert ex["user_message"] == "what's our history with adrian?" 55 55 assert ex["agent_response"] == "You met Adrian at betaworks." 56 56 assert ex["talent"] == "unified" 57 - assert ex["agent_id"] == "12345" 57 + assert ex["use_id"] == "12345" 58 58 59 59 60 60 def test_record_exchange_writes_journal_segment(journal_dir): ··· 72 72 user_message="move my 3pm to 4pm", 73 73 agent_response="Done — moved 'DVD sync' to 4pm.", 74 74 talent="unified", 75 - agent_id="67890", 75 + use_id="67890", 76 76 ) 77 77 78 - # Check journal segment directory: YYYYMMDD/conversation/HHMMSS_1/agents/ 78 + # Check journal segment directory: YYYYMMDD/conversation/HHMMSS_1/talents/ 79 79 day = datetime.fromtimestamp(ts / 1000).strftime("%Y%m%d") 80 80 time_key = datetime.fromtimestamp(ts / 1000).strftime("%H%M%S") 81 81 md_path = ( ··· 84 84 / day 85 85 / "conversation" 86 86 / f"{time_key}_1" 87 - / "agents" 87 + / "talents" 88 88 / "conversation.md" 89 89 ) 90 90
+13 -13
tests/test_convey_apps.py
··· 20 20 app = Flask(__name__) 21 21 with ( 22 22 patch("convey.utils.spawn_agent", return_value="agent-1") as mock_spawn, 23 - patch("think.cortex_client.wait_for_agents", return_value=({}, [])), 23 + patch("think.cortex_client.wait_for_uses", return_value=({}, [])), 24 24 patch( 25 - "think.cortex_client.read_agent_events", 25 + "think.cortex_client.read_use_events", 26 26 return_value=[{"event": "finish", "result": "ok"}], 27 27 ), 28 28 ): ··· 140 140 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 141 141 142 142 today = datetime.now().strftime("%Y%m%d") 143 - agents_dir = tmp_path / "agents" 143 + agents_dir = tmp_path / "talents" 144 144 agents_dir.mkdir() 145 145 day_index = agents_dir / f"{today}.jsonl" 146 146 day_index.write_text( 147 147 json.dumps( 148 148 { 149 - "agent_id": "1", 149 + "use_id": "1", 150 150 "name": "flow", 151 151 "day": today, 152 152 "ts": 1000, ··· 156 156 + "\n" 157 157 + json.dumps( 158 158 { 159 - "agent_id": "2", 159 + "use_id": "2", 160 160 "name": "meetings", 161 161 "day": today, 162 162 "ts": 1001, ··· 182 182 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 183 183 184 184 today = datetime.now().strftime("%Y%m%d") 185 - agents_dir = tmp_path / "agents" 185 + agents_dir = tmp_path / "talents" 186 186 agents_dir.mkdir() 187 187 day_index = agents_dir / f"{today}.jsonl" 188 188 day_index.write_text( 189 189 json.dumps( 190 190 { 191 - "agent_id": "1", 191 + "use_id": "1", 192 192 "name": "flow", 193 193 "day": today, 194 194 "ts": 1000, ··· 198 198 + "\n" 199 199 + json.dumps( 200 200 { 201 - "agent_id": "3", 201 + "use_id": "3", 202 202 "name": "flow", 203 203 "day": today, 204 204 "ts": 2000, ··· 221 221 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 222 222 223 223 today = datetime.now().strftime("%Y%m%d") 224 - agents_dir = tmp_path / "agents" 224 + agents_dir = tmp_path / "talents" 225 225 agents_dir.mkdir() 226 226 day_index = agents_dir / f"{today}.jsonl" 227 227 day_index.write_text( 228 228 json.dumps( 229 229 { 230 - "agent_id": "1", 230 + "use_id": "1", 231 231 "name": "flow", 232 232 "day": today, 233 233 "ts": 1000, ··· 266 266 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 267 267 268 268 today = datetime.now().strftime("%Y%m%d") 269 - agents_dir = tmp_path / "agents" 269 + agents_dir = tmp_path / "talents" 270 270 agents_dir.mkdir() 271 271 day_index = agents_dir / f"{today}.jsonl" 272 272 day_index.write_text( 273 - json.dumps({"agent_id": "1", "name": "flow", "ts": 1000, "status": "error"}) 273 + json.dumps({"use_id": "1", "name": "flow", "ts": 1000, "status": "error"}) 274 274 + "\n" 275 275 ) 276 276 result = _resolve_attention({}) ··· 299 299 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 300 300 301 301 today = datetime.now().strftime("%Y%m%d") 302 - agents_dir = tmp_path / today / "agents" 302 + agents_dir = tmp_path / today / "talents" 303 303 agents_dir.mkdir(parents=True) 304 304 (agents_dir / "flow.md").write_text("# Flow") 305 305 (agents_dir / "meetings.md").write_text("# Meetings")
+143 -142
tests/test_cortex.py
··· 5 5 6 6 import json 7 7 import os 8 + import sys 8 9 from pathlib import Path 9 10 from unittest.mock import MagicMock, patch 10 11 ··· 41 42 """Set up a temporary journal directory.""" 42 43 journal_path = tmp_path / "journal" 43 44 journal_path.mkdir() 44 - agents_path = journal_path / "agents" 45 + agents_path = journal_path / "talents" 45 46 agents_path.mkdir() 46 47 47 48 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_path)) ··· 57 58 58 59 59 60 def test_agent_process_creation(): 60 - """Test AgentProcess class initialization and methods.""" 61 - from think.cortex import AgentProcess 61 + """Test TalentProcess class initialization and methods.""" 62 + from think.cortex import TalentProcess 62 63 63 64 mock_process = MagicMock() 64 65 mock_process.poll.return_value = None # Running 65 66 mock_process.pid = 12345 66 67 67 68 log_path = Path("/tmp/test.jsonl") 68 - agent = AgentProcess("123456789", mock_process, log_path) 69 + agent = TalentProcess("123456789", mock_process, log_path) 69 70 70 - assert agent.agent_id == "123456789" 71 + assert agent.use_id == "123456789" 71 72 assert agent.process == mock_process 72 73 assert agent.log_path == log_path 73 74 assert agent.is_running() is True ··· 81 82 def test_cortex_service_initialization(cortex_service, mock_journal): 82 83 """Test CortexService initialization.""" 83 84 assert cortex_service.journal_path == mock_journal 84 - assert cortex_service.agents_dir == mock_journal / "agents" 85 - assert cortex_service.running_agents == {} 86 - assert cortex_service.agents_dir.exists() 85 + assert cortex_service.talents_dir == mock_journal / "talents" 86 + assert cortex_service.running_uses == {} 87 + assert cortex_service.talents_dir.exists() 87 88 88 89 89 90 @patch("think.cortex.subprocess.Popen") ··· 105 106 mock_timer_instance = MagicMock() 106 107 mock_timer.return_value = mock_timer_instance 107 108 108 - agent_id = "123456789" 109 - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 109 + use_id = "123456789" 110 + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 110 111 111 112 request = { 112 113 "event": "request", ··· 118 119 } 119 120 120 121 cortex_service._spawn_subprocess( 121 - agent_id, 122 + use_id, 122 123 file_path, 123 124 request, 124 - ["sol", "agents"], 125 - "agent", 125 + [sys.executable, "-m", "think.talents"], 126 + "talent", 126 127 ) 127 128 128 129 # Check subprocess was called 129 130 mock_popen.assert_called_once() 130 131 call_args = mock_popen.call_args 131 - assert call_args[0][0] == ["sol", "agents"] 132 + assert call_args[0][0] == [sys.executable, "-m", "think.talents"] 132 133 assert call_args[1]["stdin"] is not None 133 134 assert call_args[1]["stdout"] is not None 134 135 assert call_args[1]["stderr"] is not None ··· 147 148 mock_process.stdin.close.assert_called_once() 148 149 149 150 # Check agent was tracked 150 - assert agent_id in cortex_service.running_agents 151 - agent = cortex_service.running_agents[agent_id] 152 - assert agent.agent_id == agent_id 151 + assert use_id in cortex_service.running_uses 152 + agent = cortex_service.running_uses[use_id] 153 + assert agent.use_id == use_id 153 154 assert agent.log_path == file_path 154 155 155 156 # Check monitoring threads were started ··· 179 180 mock_timer_instance = MagicMock() 180 181 mock_timer.return_value = mock_timer_instance 181 182 182 - agent_id = "987654321" 183 - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 183 + use_id = "987654321" 184 + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 184 185 185 186 # Generator config has "output" instead of "tools" 186 187 config = { ··· 193 194 194 195 # Generators route through _spawn_subprocess 195 196 cortex_service._spawn_subprocess( 196 - agent_id, 197 + use_id, 197 198 file_path, 198 199 config, 199 - ["sol", "agents"], 200 - "agent", 200 + [sys.executable, "-m", "think.talents"], 201 + "talent", 201 202 ) 202 203 203 204 # Check subprocess was called with agents command (generators route through agents) 204 205 mock_popen.assert_called_once() 205 206 call_args = mock_popen.call_args 206 - assert call_args[0][0] == ["sol", "agents"] 207 + assert call_args[0][0] == [sys.executable, "-m", "think.talents"] 207 208 assert call_args[1]["stdin"] is not None 208 209 assert call_args[1]["stdout"] is not None 209 210 assert call_args[1]["stderr"] is not None ··· 221 222 mock_process.stdin.close.assert_called_once() 222 223 223 224 # Check generator was tracked 224 - assert agent_id in cortex_service.running_agents 225 - agent = cortex_service.running_agents[agent_id] 226 - assert agent.agent_id == agent_id 225 + assert use_id in cortex_service.running_uses 226 + agent = cortex_service.running_uses[use_id] 227 + assert agent.use_id == use_id 227 228 assert agent.log_path == file_path 228 229 229 230 # Check monitoring threads were started ··· 234 235 mock_timer_instance.start.assert_called_once() 235 236 236 237 237 - @patch("think.talent.get_agent") 238 + @patch("think.talent.get_talent") 238 239 @patch("think.cortex.subprocess.Popen") 239 240 @patch("think.cortex.threading.Thread") 240 241 @patch("think.cortex.threading.Timer") ··· 258 259 mock_timer_instance = MagicMock() 259 260 mock_timer.return_value = mock_timer_instance 260 261 261 - agent_id = "24680" 262 - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 262 + use_id = "24680" 263 + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 263 264 request = { 264 265 "event": "request", 265 266 "ts": 24680, ··· 270 271 } 271 272 272 273 cortex_service._spawn_subprocess( 273 - agent_id, 274 + use_id, 274 275 file_path, 275 276 request, 276 - ["sol", "agents"], 277 - "agent", 277 + [sys.executable, "-m", "think.talents"], 278 + "talent", 278 279 ) 279 280 280 281 assert mock_popen.call_args.kwargs["cwd"] == str(mock_journal) 281 282 282 283 283 - @patch("think.talent.get_agent") 284 + @patch("think.talent.get_talent") 284 285 @patch("think.cortex.subprocess.Popen") 285 286 @patch("think.cortex.threading.Thread") 286 287 @patch("think.cortex.threading.Timer") ··· 304 305 mock_timer_instance = MagicMock() 305 306 mock_timer.return_value = mock_timer_instance 306 307 307 - agent_id = "13579" 308 - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 308 + use_id = "13579" 309 + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 309 310 request = { 310 311 "event": "request", 311 312 "ts": 13579, ··· 315 316 } 316 317 317 318 cortex_service._spawn_subprocess( 318 - agent_id, 319 + use_id, 319 320 file_path, 320 321 request, 321 - ["sol", "agents"], 322 - "agent", 322 + [sys.executable, "-m", "think.talents"], 323 + "talent", 323 324 ) 324 325 325 326 assert mock_popen.call_args.kwargs["cwd"] is None ··· 329 330 """Test monitoring stdout with JSON events.""" 330 331 from io import StringIO 331 332 332 - from think.cortex import AgentProcess 333 + from think.cortex import TalentProcess 333 334 334 - agent_id = "123456789" 335 - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 335 + use_id = "123456789" 336 + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 336 337 337 338 mock_process = MagicMock() 338 339 mock_process.poll.return_value = 0 # Process exits ··· 341 342 '{"event": "finish", "ts": 1234567891, "result": "Done"}\n' 342 343 ) 343 344 344 - agent = AgentProcess(agent_id, mock_process, log_path) 345 - cortex_service.running_agents[agent_id] = agent 345 + agent = TalentProcess(use_id, mock_process, log_path) 346 + cortex_service.running_uses[use_id] = agent 346 347 347 - with patch.object(cortex_service, "_complete_agent_file") as mock_complete: 348 + with patch.object(cortex_service, "_complete_use_file") as mock_complete: 348 349 cortex_service._monitor_stdout(agent) 349 350 350 351 # Check events were written to file ··· 355 356 assert json.loads(lines[1])["event"] == "finish" 356 357 357 358 # Check file was completed 358 - mock_complete.assert_called_once_with(agent_id, log_path) 359 + mock_complete.assert_called_once_with(use_id, log_path) 359 360 360 361 # Check agent was removed 361 - assert agent_id not in cortex_service.running_agents 362 + assert use_id not in cortex_service.running_uses 362 363 363 364 364 365 def test_monitor_stdout_non_json_output(cortex_service, mock_journal): 365 366 """Test monitoring stdout with non-JSON output.""" 366 367 from io import StringIO 367 368 368 - from think.cortex import AgentProcess 369 + from think.cortex import TalentProcess 369 370 370 - agent_id = "123456789" 371 - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 371 + use_id = "123456789" 372 + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 372 373 373 374 mock_process = MagicMock() 374 375 mock_process.poll.return_value = 0 ··· 376 377 'Plain text output\n{"event": "finish", "ts": 1234567890}\n' 377 378 ) 378 379 379 - agent = AgentProcess(agent_id, mock_process, log_path) 380 - cortex_service.running_agents[agent_id] = agent 380 + agent = TalentProcess(use_id, mock_process, log_path) 381 + cortex_service.running_uses[use_id] = agent 381 382 382 - with patch.object(cortex_service, "_complete_agent_file"): 383 + with patch.object(cortex_service, "_complete_use_file"): 383 384 cortex_service._monitor_stdout(agent) 384 385 385 386 # Check info event was created for non-JSON ··· 396 397 """Test monitoring stdout when process exits without finish event.""" 397 398 from io import StringIO 398 399 399 - from think.cortex import AgentProcess 400 + from think.cortex import TalentProcess 400 401 401 - agent_id = "123456789" 402 - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 402 + use_id = "123456789" 403 + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 403 404 404 405 mock_process = MagicMock() 405 406 mock_process.wait.return_value = 1 # Non-zero exit 406 407 mock_process.stdout = StringIO('{"event": "start", "ts": 1234567890}\n') 407 408 408 - agent = AgentProcess(agent_id, mock_process, log_path) 409 - cortex_service.running_agents[agent_id] = agent 409 + agent = TalentProcess(use_id, mock_process, log_path) 410 + cortex_service.running_uses[use_id] = agent 410 411 411 - with patch.object(cortex_service, "_complete_agent_file"): 412 + with patch.object(cortex_service, "_complete_use_file"): 412 413 cortex_service._monitor_stdout(agent) 413 414 414 415 # Check error event was added ··· 425 426 """Test monitoring stderr for errors.""" 426 427 from io import StringIO 427 428 428 - from think.cortex import AgentProcess 429 + from think.cortex import TalentProcess 429 430 430 - agent_id = "123456789" 431 - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 431 + use_id = "123456789" 432 + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 432 433 433 434 mock_process = MagicMock() 434 435 mock_process.poll.return_value = 1 # Error exit ··· 436 437 "Error: Something went wrong\nStack trace line 1\nStack trace line 2\n" 437 438 ) 438 439 439 - agent = AgentProcess(agent_id, mock_process, log_path) 440 + agent = TalentProcess(use_id, mock_process, log_path) 440 441 441 442 cortex_service._monitor_stderr(agent) 442 443 ··· 454 455 455 456 def test_has_finish_event(cortex_service, mock_journal): 456 457 """Test checking for finish event in JSONL file.""" 457 - file_path = mock_journal / "agents" / "test.jsonl" 458 + file_path = mock_journal / "talents" / "test.jsonl" 458 459 459 460 # File with finish event 460 461 file_path.write_text( ··· 477 478 assert cortex_service._has_finish_event(file_path) is False 478 479 479 480 480 - def test_complete_agent_file(cortex_service, mock_journal): 481 + def test_complete_use_file(cortex_service, mock_journal): 481 482 """Test completing an agent file (rename from active to completed).""" 482 - agent_id = "123456789" 483 - unified_dir = mock_journal / "agents" / "unified" 483 + use_id = "123456789" 484 + unified_dir = mock_journal / "talents" / "unified" 484 485 unified_dir.mkdir() 485 - active_path = unified_dir / f"{agent_id}_active.jsonl" 486 + active_path = unified_dir / f"{use_id}_active.jsonl" 486 487 active_path.touch() 487 - cortex_service.agent_requests[agent_id] = {"name": "unified", "agent_id": agent_id} 488 + cortex_service.use_requests[use_id] = {"name": "unified", "use_id": use_id} 488 489 489 - cortex_service._complete_agent_file(agent_id, active_path) 490 + cortex_service._complete_use_file(use_id, active_path) 490 491 491 492 # Check file was renamed 492 493 assert not active_path.exists() 493 - completed_path = unified_dir / f"{agent_id}.jsonl" 494 + completed_path = unified_dir / f"{use_id}.jsonl" 494 495 assert completed_path.exists() 495 - symlink_path = mock_journal / "agents" / "unified.log" 496 + symlink_path = mock_journal / "talents" / "unified.log" 496 497 assert symlink_path.is_symlink() 497 - assert os.readlink(symlink_path) == f"unified/{agent_id}.jsonl" 498 + assert os.readlink(symlink_path) == f"unified/{use_id}.jsonl" 498 499 499 500 500 - def test_complete_agent_file_replaces_symlink(cortex_service, mock_journal): 501 + def test_complete_use_file_replaces_symlink(cortex_service, mock_journal): 501 502 """Test completing agent file replaces convenience symlink for same name.""" 502 - unified_dir = mock_journal / "agents" / "unified" 503 + unified_dir = mock_journal / "talents" / "unified" 503 504 unified_dir.mkdir() 504 505 505 506 first_agent_id = "111" 506 507 first_active_path = unified_dir / f"{first_agent_id}_active.jsonl" 507 508 first_active_path.touch() 508 - cortex_service.agent_requests[first_agent_id] = {"name": "unified"} 509 + cortex_service.use_requests[first_agent_id] = {"name": "unified"} 509 510 510 - cortex_service._complete_agent_file(first_agent_id, first_active_path) 511 + cortex_service._complete_use_file(first_agent_id, first_active_path) 511 512 512 513 second_agent_id = "222" 513 514 second_active_path = unified_dir / f"{second_agent_id}_active.jsonl" 514 515 second_active_path.touch() 515 - cortex_service.agent_requests[second_agent_id] = {"name": "unified"} 516 + cortex_service.use_requests[second_agent_id] = {"name": "unified"} 516 517 517 - cortex_service._complete_agent_file(second_agent_id, second_active_path) 518 + cortex_service._complete_use_file(second_agent_id, second_active_path) 518 519 519 - symlink_path = mock_journal / "agents" / "unified.log" 520 + symlink_path = mock_journal / "talents" / "unified.log" 520 521 assert symlink_path.is_symlink() 521 522 assert os.readlink(symlink_path) == f"unified/{second_agent_id}.jsonl" 522 523 523 524 524 - def test_complete_agent_file_colon_name(cortex_service, mock_journal): 525 + def test_complete_use_file_colon_name(cortex_service, mock_journal): 525 526 """Test completing agent file sanitizes colon in convenience symlink name.""" 526 - agent_id = "123456789" 527 - entities_dir = mock_journal / "agents" / "entities--entity_assist" 527 + use_id = "123456789" 528 + entities_dir = mock_journal / "talents" / "entities--entity_assist" 528 529 entities_dir.mkdir() 529 - active_path = entities_dir / f"{agent_id}_active.jsonl" 530 + active_path = entities_dir / f"{use_id}_active.jsonl" 530 531 active_path.touch() 531 - cortex_service.agent_requests[agent_id] = {"name": "entities:entity_assist"} 532 + cortex_service.use_requests[use_id] = {"name": "entities:entity_assist"} 532 533 533 - cortex_service._complete_agent_file(agent_id, active_path) 534 + cortex_service._complete_use_file(use_id, active_path) 534 535 535 - symlink_path = mock_journal / "agents" / "entities--entity_assist.log" 536 + symlink_path = mock_journal / "talents" / "entities--entity_assist.log" 536 537 assert symlink_path.is_symlink() 537 - assert os.readlink(symlink_path) == f"entities--entity_assist/{agent_id}.jsonl" 538 + assert os.readlink(symlink_path) == f"entities--entity_assist/{use_id}.jsonl" 538 539 539 540 540 - def test_complete_agent_file_no_name(cortex_service, mock_journal): 541 + def test_complete_use_file_no_name(cortex_service, mock_journal): 541 542 """Test completing agent file skips symlink when request name is missing.""" 542 - agent_id = "123456789" 543 - active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 543 + use_id = "123456789" 544 + active_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 544 545 active_path.touch() 545 546 546 - cortex_service._complete_agent_file(agent_id, active_path) 547 + cortex_service._complete_use_file(use_id, active_path) 547 548 548 - completed_path = mock_journal / "agents" / f"{agent_id}.jsonl" 549 + completed_path = mock_journal / "talents" / f"{use_id}.jsonl" 549 550 assert completed_path.exists() 550 - assert not any(path.is_symlink() for path in (mock_journal / "agents").iterdir()) 551 + assert not any(path.is_symlink() for path in (mock_journal / "talents").iterdir()) 551 552 552 553 553 554 def test_write_error_and_complete(cortex_service, mock_journal): 554 555 """Test writing error and completing file.""" 555 - agent_id = "123456789" 556 - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 556 + use_id = "123456789" 557 + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 557 558 file_path.touch() 558 559 559 560 cortex_service._write_error_and_complete(file_path, "Test error message") 560 561 561 562 # Check error was written 562 - completed_path = mock_journal / "agents" / f"{agent_id}.jsonl" 563 + completed_path = mock_journal / "talents" / f"{use_id}.jsonl" 563 564 assert completed_path.exists() 564 565 assert not file_path.exists() 565 566 ··· 572 573 573 574 def test_get_status(cortex_service): 574 575 """Test getting service status.""" 575 - from think.cortex import AgentProcess 576 + from think.cortex import TalentProcess 576 577 577 578 # Empty status 578 579 status = cortex_service.get_status() 579 - assert status["running_agents"] == 0 580 - assert status["agent_ids"] == [] 580 + assert status["running_uses"] == 0 581 + assert status["use_ids"] == [] 581 582 582 583 # Add running agents 583 584 mock_process = MagicMock() 584 - agent1 = AgentProcess("111", mock_process, Path("/tmp/1.jsonl")) 585 - agent2 = AgentProcess("222", mock_process, Path("/tmp/2.jsonl")) 585 + agent1 = TalentProcess("111", mock_process, Path("/tmp/1.jsonl")) 586 + agent2 = TalentProcess("222", mock_process, Path("/tmp/2.jsonl")) 586 587 587 - cortex_service.running_agents["111"] = agent1 588 - cortex_service.running_agents["222"] = agent2 588 + cortex_service.running_uses["111"] = agent1 589 + cortex_service.running_uses["222"] = agent2 589 590 590 591 status = cortex_service.get_status() 591 - assert status["running_agents"] == 2 592 - assert set(status["agent_ids"]) == {"111", "222"} 592 + assert status["running_uses"] == 2 593 + assert set(status["use_ids"]) == {"111", "222"} 593 594 594 595 595 596 def test_write_output(cortex_service, mock_journal): 596 597 """Test writing agent output using explicit output_path.""" 597 - agent_id = "test_agent" 598 + use_id = "test_agent" 598 599 result = "This is the agent result content" 599 - expected_path = mock_journal / "20240115" / "agents" / "my_agent.md" 600 + expected_path = mock_journal / "20240115" / "talents" / "my_agent.md" 600 601 config = {"output": "md", "name": "my_agent", "output_path": str(expected_path)} 601 602 602 - cortex_service._write_output(agent_id, result, config) 603 + cortex_service._write_output(use_id, result, config) 603 604 604 605 assert expected_path.exists() 605 606 assert expected_path.read_text() == result ··· 610 611 """Test write output handles errors gracefully.""" 611 612 import logging 612 613 613 - output_path = mock_journal / "20240115" / "agents" / "test.md" 614 + output_path = mock_journal / "20240115" / "talents" / "test.md" 614 615 with patch("builtins.open", side_effect=PermissionError("Cannot write")): 615 616 with caplog.at_level(logging.ERROR): 616 617 config = {"output": "md", "name": "test", "output_path": str(output_path)} 617 - cortex_service._write_output("agent_id", "result", config) 618 + cortex_service._write_output("use_id", "result", config) 618 619 619 620 # Check error was logged but didn't raise 620 - assert "Failed to write agent agent_id output" in caplog.text 621 + assert "Failed to write talent use_id output" in caplog.text 621 622 622 623 623 624 def test_write_output_missing_path_skips(cortex_service, mock_journal, caplog): 624 625 """Test write output skips when output_path is missing.""" 625 626 config = {"output": "md", "name": "test"} 626 - cortex_service._write_output("agent_id", "result", config) 627 + cortex_service._write_output("use_id", "result", config) 627 628 628 629 # No output written, no error — silent skip is expected 629 630 assert "Failed to write" not in caplog.text ··· 631 632 632 633 def test_write_output_with_day_parameter(cortex_service, mock_journal): 633 634 """Test writing agent output to a specific day directory.""" 634 - agent_id = "test_agent" 635 + use_id = "test_agent" 635 636 result = "This is the agent result content" 636 637 specified_day = "20240201" 637 - expected_path = mock_journal / specified_day / "agents" / "reporter.md" 638 + expected_path = mock_journal / specified_day / "talents" / "reporter.md" 638 639 config = { 639 640 "output": "md", 640 641 "name": "reporter", ··· 642 643 "output_path": str(expected_path), 643 644 } 644 645 645 - cortex_service._write_output(agent_id, result, config) 646 + cortex_service._write_output(use_id, result, config) 646 647 647 648 assert expected_path.exists() 648 649 assert expected_path.read_text() == result ··· 651 652 652 653 def test_write_output_with_segment(cortex_service, mock_journal): 653 654 """Test writing segment agent output to segment agents directory.""" 654 - agent_id = "segment_agent" 655 + use_id = "segment_agent" 655 656 result = "Segment analysis content" 656 - expected_path = mock_journal / "20240115" / "143000_600" / "agents" / "analyzer.md" 657 + expected_path = mock_journal / "20240115" / "143000_600" / "talents" / "analyzer.md" 657 658 config = { 658 659 "output": "md", 659 660 "name": "analyzer", ··· 661 662 "output_path": str(expected_path), 662 663 } 663 664 664 - cortex_service._write_output(agent_id, result, config) 665 + cortex_service._write_output(use_id, result, config) 665 666 666 667 assert expected_path.exists() 667 668 assert expected_path.read_text() == result ··· 669 670 670 671 def test_write_output_json_format(cortex_service, mock_journal): 671 672 """Test writing agent output in JSON format.""" 672 - agent_id = "json_agent" 673 + use_id = "json_agent" 673 674 result = '{"key": "value"}' 674 - expected_path = mock_journal / "20240115" / "agents" / "data_agent.json" 675 + expected_path = mock_journal / "20240115" / "talents" / "data_agent.json" 675 676 config = { 676 677 "output": "json", 677 678 "name": "data_agent", 678 679 "output_path": str(expected_path), 679 680 } 680 681 681 - cortex_service._write_output(agent_id, result, config) 682 + cortex_service._write_output(use_id, result, config) 682 683 683 684 assert expected_path.exists() 684 685 assert expected_path.read_text() == result ··· 686 687 687 688 def test_monitor_stdout_with_output(cortex_service, mock_journal): 688 689 """Test monitor_stdout writes output when output_path is present.""" 689 - from think.cortex import AgentProcess 690 + from think.cortex import TalentProcess 690 691 691 - agent_id = "output_test" 692 - active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 693 - output_path = mock_journal / "20240115" / "agents" / "test_agent.md" 692 + use_id = "output_test" 693 + active_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 694 + output_path = mock_journal / "20240115" / "talents" / "test_agent.md" 694 695 695 696 # Store request with explicit output_path 696 - cortex_service.agent_requests = { 697 - agent_id: { 697 + cortex_service.use_requests = { 698 + use_id: { 698 699 "event": "request", 699 700 "prompt": "test", 700 701 "output": "md", ··· 711 712 mock_process.stdout = MockPipe(mock_stdout) 712 713 mock_process.wait.return_value = 0 713 714 714 - agent = AgentProcess(agent_id, mock_process, active_path) 715 + agent = TalentProcess(use_id, mock_process, active_path) 715 716 716 - with patch.object(cortex_service, "_complete_agent_file"): 717 + with patch.object(cortex_service, "_complete_use_file"): 717 718 with patch.object(cortex_service, "_has_finish_event", return_value=True): 718 719 cortex_service._monitor_stdout(agent) 719 720 ··· 723 724 724 725 def test_monitor_stdout_with_output_and_day(cortex_service, mock_journal): 725 726 """Test monitor_stdout writes output to specific day via output_path.""" 726 - from think.cortex import AgentProcess 727 + from think.cortex import TalentProcess 727 728 728 - agent_id = "output_day_test" 729 - active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 729 + use_id = "output_day_test" 730 + active_path = mock_journal / "talents" / f"{use_id}_active.jsonl" 730 731 specified_day = "20240220" 731 - output_path = mock_journal / specified_day / "agents" / "daily_reporter.md" 732 + output_path = mock_journal / specified_day / "talents" / "daily_reporter.md" 732 733 733 734 # Store request with explicit output_path and day 734 - cortex_service.agent_requests = { 735 - agent_id: { 735 + cortex_service.use_requests = { 736 + use_id: { 736 737 "event": "request", 737 738 "prompt": "test", 738 739 "output": "md", ··· 750 751 mock_process.stdout = MockPipe(mock_stdout) 751 752 mock_process.wait.return_value = 0 752 753 753 - agent = AgentProcess(agent_id, mock_process, active_path) 754 + agent = TalentProcess(use_id, mock_process, active_path) 754 755 755 - with patch.object(cortex_service, "_complete_agent_file"): 756 + with patch.object(cortex_service, "_complete_use_file"): 756 757 with patch.object(cortex_service, "_has_finish_event", return_value=True): 757 758 cortex_service._monitor_stdout(agent) 758 759 ··· 760 761 assert output_path.read_text() == "Daily report content" 761 762 762 763 763 - def test_recover_orphaned_agents(cortex_service, mock_journal): 764 + def test_recover_orphaned_uses(cortex_service, mock_journal): 764 765 """Test recovery of orphaned active agent files.""" 765 766 # Create orphaned active files 766 - agents_dir = mock_journal / "agents" 767 - unified_dir = agents_dir / "unified" 767 + talents_dir = mock_journal / "talents" 768 + unified_dir = talents_dir / "unified" 768 769 unified_dir.mkdir() 769 770 agent1_active = unified_dir / "111_active.jsonl" 770 771 agent2_active = unified_dir / "222_active.jsonl" ··· 773 774 agent2_active.write_text('{"event": "start", "ts": 2000}\n') 774 775 775 776 active_files = [agent1_active, agent2_active] 776 - cortex_service._recover_orphaned_agents(active_files) 777 + cortex_service._recover_orphaned_uses(active_files) 777 778 778 779 # Check active files were renamed to completed 779 780 assert not agent1_active.exists() ··· 788 789 error_event = json.loads(lines1[1]) 789 790 assert error_event["event"] == "error" 790 791 assert "Recovered" in error_event["error"] 791 - assert error_event["agent_id"] == "111" 792 + assert error_event["use_id"] == "111" 792 793 793 794 content2 = (unified_dir / "222.jsonl").read_text() 794 795 lines2 = content2.strip().split("\n")
+152 -158
tests/test_cortex_client.py
··· 14 14 15 15 from think.callosum import CallosumConnection, CallosumServer 16 16 from think.cortex_client import ( 17 - cortex_agents, 18 17 cortex_request, 19 - get_agent_end_state, 20 - get_agent_log_status, 21 - wait_for_agents, 18 + cortex_uses, 19 + get_use_end_state, 20 + get_use_log_status, 21 + wait_for_uses, 22 22 ) 23 23 from think.models import GPT_5 24 24 from think.utils import now_ms ··· 36 36 tmp_path = Path(tmp_dir) 37 37 38 38 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 39 - (tmp_path / "agents").mkdir(parents=True, exist_ok=True) 39 + (tmp_path / "talents").mkdir(parents=True, exist_ok=True) 40 40 41 41 server = CallosumServer() 42 42 server_thread = threading.Thread(target=server.start, daemon=True) ··· 84 84 messages = callosum_listener 85 85 86 86 # Create a request 87 - agent_id = cortex_request( 87 + use_id = cortex_request( 88 88 prompt="Test prompt", 89 89 name="unified", 90 90 provider="openai", ··· 102 102 assert msg["name"] == "unified" 103 103 assert msg["provider"] == "openai" 104 104 assert msg["model"] == GPT_5 105 - assert msg["agent_id"] == agent_id 105 + assert msg["use_id"] == use_id 106 106 assert "ts" in msg 107 107 108 108 109 109 def test_cortex_request_returns_agent_id(callosum_server): 110 - """Test that cortex_request returns agent_id string.""" 110 + """Test that cortex_request returns use_id string.""" 111 111 _ = callosum_server # Needed for side effects only 112 112 113 - agent_id = cortex_request(prompt="Test", name="unified", provider="openai") 113 + use_id = cortex_request(prompt="Test", name="unified", provider="openai") 114 114 115 - # Verify agent_id is a string timestamp 116 - assert isinstance(agent_id, str) 117 - assert agent_id.isdigit() 118 - assert len(agent_id) == 13 # Millisecond timestamp 115 + # Verify use_id is a string timestamp 116 + assert isinstance(use_id, str) 117 + assert use_id.isdigit() 118 + assert len(use_id) == 13 # Millisecond timestamp 119 119 120 120 121 121 def test_cortex_request_unique_agent_ids(callosum_server): ··· 124 124 125 125 agent_ids = [] 126 126 for i in range(3): 127 - agent_id = cortex_request(prompt=f"Test {i}", name="unified", provider="openai") 128 - agent_ids.append(agent_id) 127 + use_id = cortex_request(prompt=f"Test {i}", name="unified", provider="openai") 128 + agent_ids.append(use_id) 129 129 time.sleep(0.002) 130 130 131 131 # All agent IDs should be unique ··· 136 136 """Test cortex_request returns None when callosum_send fails.""" 137 137 monkeypatch.setattr("think.cortex_client.callosum_send", lambda *a, **kw: False) 138 138 139 - agent_id = cortex_request(prompt="Test", name="unified", provider="openai") 139 + use_id = cortex_request(prompt="Test", name="unified", provider="openai") 140 140 141 - assert agent_id is None 141 + assert use_id is None 142 142 143 143 144 144 def test_cortex_request_empty_journal(tmp_path, monkeypatch): ··· 146 146 monkeypatch.setattr("think.cortex_client.callosum_send", lambda *a, **kw: True) 147 147 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 148 148 149 - agent_id = cortex_request("test", "unified", "openai") 150 - assert agent_id is not None 151 - assert len(agent_id) > 0 149 + use_id = cortex_request("test", "unified", "openai") 150 + assert use_id is not None 151 + assert len(use_id) > 0 152 152 153 153 154 - # Tests for cortex_agents remain mostly unchanged as they read from files 154 + # Tests for cortex_uses remain mostly unchanged as they read from files 155 155 156 156 157 157 def test_cortex_agents_empty(tmp_path, monkeypatch): 158 - """Test cortex_agents with no agents.""" 158 + """Test cortex_uses with no agents.""" 159 159 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 160 160 161 - result = cortex_agents() 161 + result = cortex_uses() 162 162 163 - assert result["agents"] == [] 163 + assert result["uses"] == [] 164 164 assert result["pagination"]["total"] == 0 165 165 assert result["pagination"]["has_more"] is False 166 166 assert result["live_count"] == 0 ··· 168 168 169 169 170 170 def test_cortex_agents_with_active(tmp_path, monkeypatch): 171 - """Test cortex_agents with active (running) agents.""" 171 + """Test cortex_uses with active (running) agents.""" 172 172 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 173 - agents_dir = tmp_path / "agents" 174 - agents_dir.mkdir() 173 + talents_dir = tmp_path / "talents" 174 + talents_dir.mkdir() 175 175 176 176 # Create active agent files 177 177 ts1 = now_ms() 178 178 ts2 = ts1 + 1000 179 179 180 - unified_dir = agents_dir / "unified" 181 - tester_dir = agents_dir / "tester" 180 + unified_dir = talents_dir / "unified" 181 + tester_dir = talents_dir / "tester" 182 182 unified_dir.mkdir() 183 183 tester_dir.mkdir() 184 184 ··· 210 210 ) 211 211 f.write("\n") 212 212 213 - result = cortex_agents() 213 + result = cortex_uses() 214 214 215 - assert len(result["agents"]) == 2 215 + assert len(result["uses"]) == 2 216 216 assert result["live_count"] == 2 217 217 assert result["historical_count"] == 0 218 218 219 219 220 220 def test_cortex_agents_with_completed(tmp_path, monkeypatch): 221 - """Test cortex_agents with completed (historical) agents.""" 221 + """Test cortex_uses with completed (historical) agents.""" 222 222 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 223 - agents_dir = tmp_path / "agents" 224 - agents_dir.mkdir() 223 + talents_dir = tmp_path / "talents" 224 + talents_dir.mkdir() 225 225 226 226 # Create completed agent files 227 227 ts1 = now_ms() 228 - reviewer_dir = agents_dir / "reviewer" 228 + reviewer_dir = talents_dir / "reviewer" 229 229 reviewer_dir.mkdir() 230 230 231 231 completed_file1 = reviewer_dir / f"{ts1}.jsonl" ··· 244 244 json.dump({"event": "finish", "ts": ts1 + 100, "result": "Done"}, f) 245 245 f.write("\n") 246 246 247 - result = cortex_agents() 247 + result = cortex_uses() 248 248 249 - assert len(result["agents"]) == 1 249 + assert len(result["uses"]) == 1 250 250 assert result["live_count"] == 0 251 251 assert result["historical_count"] == 1 252 - assert result["agents"][0]["status"] == "completed" 252 + assert result["uses"][0]["status"] == "completed" 253 253 254 254 255 255 def test_cortex_agents_pagination(tmp_path, monkeypatch): 256 - """Test cortex_agents pagination.""" 256 + """Test cortex_uses pagination.""" 257 257 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 258 - agents_dir = tmp_path / "agents" 259 - agents_dir.mkdir() 258 + talents_dir = tmp_path / "talents" 259 + talents_dir.mkdir() 260 260 261 261 # Create multiple agents 262 262 base_ts = now_ms() 263 - unified_dir = agents_dir / "unified" 263 + unified_dir = talents_dir / "unified" 264 264 unified_dir.mkdir() 265 265 for i in range(5): 266 266 ts = base_ts + (i * 1000) ··· 278 278 f.write("\n") 279 279 280 280 # Test limit 281 - result = cortex_agents(limit=2) 282 - assert len(result["agents"]) == 2 281 + result = cortex_uses(limit=2) 282 + assert len(result["uses"]) == 2 283 283 assert result["pagination"]["limit"] == 2 284 284 assert result["pagination"]["total"] == 5 285 285 assert result["pagination"]["has_more"] is True 286 286 287 287 288 288 def test_cortex_agents_empty_journal(tmp_path, monkeypatch): 289 - """Test cortex_agents works with an empty journal directory.""" 289 + """Test cortex_uses works with an empty journal directory.""" 290 290 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 291 291 292 - result = cortex_agents() 293 - assert "agents" in result 292 + result = cortex_uses() 293 + assert "uses" in result 294 294 assert "pagination" in result 295 - assert isinstance(result["agents"], list) 295 + assert isinstance(result["uses"], list) 296 296 297 297 298 298 def test_get_agent_log_status_completed(tmp_path, monkeypatch): 299 - """Test get_agent_log_status returns 'completed' for finished agents.""" 299 + """Test get_use_log_status returns 'completed' for finished agents.""" 300 300 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 301 - agents_dir = tmp_path / "agents" 302 - agents_dir.mkdir() 303 - unified_dir = agents_dir / "unified" 301 + talents_dir = tmp_path / "talents" 302 + talents_dir.mkdir() 303 + unified_dir = talents_dir / "unified" 304 304 unified_dir.mkdir() 305 305 306 - agent_id = "1234567890123" 307 - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') 306 + use_id = "1234567890123" 307 + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') 308 308 309 - assert get_agent_log_status(agent_id) == "completed" 309 + assert get_use_log_status(use_id) == "completed" 310 310 311 311 312 312 def test_get_agent_log_status_running(tmp_path, monkeypatch): 313 - """Test get_agent_log_status returns 'running' for active agents.""" 313 + """Test get_use_log_status returns 'running' for active agents.""" 314 314 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 315 - agents_dir = tmp_path / "agents" 316 - agents_dir.mkdir() 317 - unified_dir = agents_dir / "unified" 315 + talents_dir = tmp_path / "talents" 316 + talents_dir.mkdir() 317 + unified_dir = talents_dir / "unified" 318 318 unified_dir.mkdir() 319 319 320 - agent_id = "1234567890123" 321 - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') 320 + use_id = "1234567890123" 321 + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') 322 322 323 - assert get_agent_log_status(agent_id) == "running" 323 + assert get_use_log_status(use_id) == "running" 324 324 325 325 326 326 def test_get_agent_log_status_not_found(tmp_path, monkeypatch): 327 - """Test get_agent_log_status returns 'not_found' for missing agents.""" 327 + """Test get_use_log_status returns 'not_found' for missing agents.""" 328 328 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 329 - (tmp_path / "agents").mkdir() 329 + (tmp_path / "talents").mkdir() 330 330 331 - assert get_agent_log_status("nonexistent") == "not_found" 331 + assert get_use_log_status("nonexistent") == "not_found" 332 332 333 333 334 334 def test_get_agent_log_status_prefers_completed(tmp_path, monkeypatch): 335 - """Test get_agent_log_status returns 'completed' when both files exist.""" 335 + """Test get_use_log_status returns 'completed' when both files exist.""" 336 336 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 337 - agents_dir = tmp_path / "agents" 338 - agents_dir.mkdir() 339 - unified_dir = agents_dir / "unified" 337 + talents_dir = tmp_path / "talents" 338 + talents_dir.mkdir() 339 + unified_dir = talents_dir / "unified" 340 340 unified_dir.mkdir() 341 341 342 342 # Edge case: both files exist (shouldn't happen, but check precedence) 343 - agent_id = "1234567890123" 344 - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') 345 - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') 343 + use_id = "1234567890123" 344 + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') 345 + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') 346 346 347 - assert get_agent_log_status(agent_id) == "completed" 347 + assert get_use_log_status(use_id) == "completed" 348 348 349 349 350 350 def test_get_agent_end_state_finish(tmp_path, monkeypatch): 351 - """Test get_agent_end_state returns 'finish' for successful agents.""" 351 + """Test get_use_end_state returns 'finish' for successful agents.""" 352 352 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 353 - agents_dir = tmp_path / "agents" 354 - agents_dir.mkdir() 355 - unified_dir = agents_dir / "unified" 353 + talents_dir = tmp_path / "talents" 354 + talents_dir.mkdir() 355 + unified_dir = talents_dir / "unified" 356 356 unified_dir.mkdir() 357 357 358 - agent_id = "1234567890123" 359 - (unified_dir / f"{agent_id}.jsonl").write_text( 358 + use_id = "1234567890123" 359 + (unified_dir / f"{use_id}.jsonl").write_text( 360 360 '{"event": "request", "prompt": "hello"}\n' 361 361 '{"event": "finish", "result": "done"}\n' 362 362 ) 363 363 364 - assert get_agent_end_state(agent_id) == "finish" 364 + assert get_use_end_state(use_id) == "finish" 365 365 366 366 367 367 def test_get_agent_end_state_error(tmp_path, monkeypatch): 368 - """Test get_agent_end_state returns 'error' for failed agents.""" 368 + """Test get_use_end_state returns 'error' for failed agents.""" 369 369 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 370 - agents_dir = tmp_path / "agents" 371 - agents_dir.mkdir() 372 - unified_dir = agents_dir / "unified" 370 + talents_dir = tmp_path / "talents" 371 + talents_dir.mkdir() 372 + unified_dir = talents_dir / "unified" 373 373 unified_dir.mkdir() 374 374 375 - agent_id = "1234567890123" 376 - (unified_dir / f"{agent_id}.jsonl").write_text( 375 + use_id = "1234567890123" 376 + (unified_dir / f"{use_id}.jsonl").write_text( 377 377 '{"event": "request", "prompt": "hello"}\n' 378 378 '{"event": "error", "error": "something went wrong"}\n' 379 379 ) 380 380 381 - assert get_agent_end_state(agent_id) == "error" 381 + assert get_use_end_state(use_id) == "error" 382 382 383 383 384 384 def test_get_agent_end_state_running(tmp_path, monkeypatch): 385 - """Test get_agent_end_state returns 'running' for active agents.""" 385 + """Test get_use_end_state returns 'running' for active agents.""" 386 386 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 387 - agents_dir = tmp_path / "agents" 388 - agents_dir.mkdir() 389 - unified_dir = agents_dir / "unified" 387 + talents_dir = tmp_path / "talents" 388 + talents_dir.mkdir() 389 + unified_dir = talents_dir / "unified" 390 390 unified_dir.mkdir() 391 391 392 - agent_id = "1234567890123" 393 - (unified_dir / f"{agent_id}_active.jsonl").write_text( 392 + use_id = "1234567890123" 393 + (unified_dir / f"{use_id}_active.jsonl").write_text( 394 394 '{"event": "request", "prompt": "hello"}\n' 395 395 ) 396 396 397 - assert get_agent_end_state(agent_id) == "running" 397 + assert get_use_end_state(use_id) == "running" 398 398 399 399 400 400 def test_get_agent_end_state_unknown(tmp_path, monkeypatch): 401 - """Test get_agent_end_state returns 'unknown' for missing agents.""" 401 + """Test get_use_end_state returns 'unknown' for missing agents.""" 402 402 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 403 - (tmp_path / "agents").mkdir() 403 + (tmp_path / "talents").mkdir() 404 404 405 - assert get_agent_end_state("nonexistent") == "unknown" 405 + assert get_use_end_state("nonexistent") == "unknown" 406 406 407 407 408 - # Tests for wait_for_agents 408 + # Tests for wait_for_uses 409 409 410 410 411 411 def test_wait_for_agents_already_complete(tmp_path, monkeypatch): 412 - """Test wait_for_agents returns immediately if agents already completed.""" 412 + """Test wait_for_uses returns immediately if agents already completed.""" 413 413 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 414 - agents_dir = tmp_path / "agents" 415 - agents_dir.mkdir() 416 - unified_dir = agents_dir / "unified" 414 + talents_dir = tmp_path / "talents" 415 + talents_dir.mkdir() 416 + unified_dir = talents_dir / "unified" 417 417 unified_dir.mkdir() 418 418 (tmp_path / "health").mkdir() 419 419 420 420 # Create completed agents 421 421 agent_ids = ["1000", "2000"] 422 - for agent_id in agent_ids: 423 - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') 422 + for use_id in agent_ids: 423 + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') 424 424 425 - completed, timed_out = wait_for_agents(agent_ids, timeout=1) 425 + completed, timed_out = wait_for_uses(agent_ids, timeout=1) 426 426 427 427 assert set(completed.keys()) == set(agent_ids) 428 428 assert all(v == "finish" for v in completed.values()) ··· 430 430 431 431 432 432 def test_wait_for_agents_event_completion(callosum_server): 433 - """Test wait_for_agents completes when finish event is received.""" 433 + """Test wait_for_uses completes when finish event is received.""" 434 434 tmp_path = callosum_server 435 - agents_dir = tmp_path / "agents" 436 - unified_dir = agents_dir / "unified" 435 + talents_dir = tmp_path / "talents" 436 + unified_dir = talents_dir / "unified" 437 437 unified_dir.mkdir(exist_ok=True) 438 438 439 - agent_id = "1234567890123" 439 + use_id = "1234567890123" 440 440 441 441 # Start wait in background thread 442 442 result = {"completed": None, "timed_out": None} 443 443 444 444 def wait_thread(): 445 - result["completed"], result["timed_out"] = wait_for_agents( 446 - [agent_id], timeout=5 447 - ) 445 + result["completed"], result["timed_out"] = wait_for_uses([use_id], timeout=5) 448 446 449 447 waiter = threading.Thread(target=wait_thread) 450 448 waiter.start() ··· 453 451 time.sleep(0.2) 454 452 455 453 # Create the completed file and emit finish event 456 - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') 454 + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') 457 455 458 456 # Emit finish event via Callosum 459 457 client = CallosumConnection() 460 458 client.start() 461 459 time.sleep(0.1) 462 - client.emit("cortex", "finish", agent_id=agent_id, result="done") 460 + client.emit("cortex", "finish", use_id=use_id, result="done") 463 461 time.sleep(0.2) 464 462 client.stop() 465 463 466 464 waiter.join(timeout=3) 467 465 468 - assert result["completed"] == {agent_id: "finish"} 466 + assert result["completed"] == {use_id: "finish"} 469 467 assert result["timed_out"] == [] 470 468 471 469 472 470 def test_wait_for_agents_error_event(callosum_server): 473 - """Test wait_for_agents completes on error event too.""" 471 + """Test wait_for_uses completes on error event too.""" 474 472 tmp_path = callosum_server 475 - agents_dir = tmp_path / "agents" 476 - unified_dir = agents_dir / "unified" 473 + talents_dir = tmp_path / "talents" 474 + unified_dir = talents_dir / "unified" 477 475 unified_dir.mkdir(exist_ok=True) 478 476 479 - agent_id = "1234567890124" 477 + use_id = "1234567890124" 480 478 481 479 result = {"completed": None, "timed_out": None} 482 480 483 481 def wait_thread(): 484 - result["completed"], result["timed_out"] = wait_for_agents( 485 - [agent_id], timeout=5 486 - ) 482 + result["completed"], result["timed_out"] = wait_for_uses([use_id], timeout=5) 487 483 488 484 waiter = threading.Thread(target=wait_thread) 489 485 waiter.start() 490 486 time.sleep(0.2) 491 487 492 488 # Create completed file and emit error event 493 - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "error"}\n') 489 + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "error"}\n') 494 490 495 491 client = CallosumConnection() 496 492 client.start() 497 493 time.sleep(0.1) 498 - client.emit("cortex", "error", agent_id=agent_id, error="something failed") 494 + client.emit("cortex", "error", use_id=use_id, error="something failed") 499 495 time.sleep(0.2) 500 496 client.stop() 501 497 502 498 waiter.join(timeout=3) 503 499 504 - assert result["completed"] == {agent_id: "error"} 500 + assert result["completed"] == {use_id: "error"} 505 501 assert result["timed_out"] == [] 506 502 507 503 508 504 def test_wait_for_agents_initial_file_check(tmp_path, monkeypatch): 509 - """Test wait_for_agents finds already-completed agents via initial file check.""" 505 + """Test wait_for_uses finds already-completed agents via initial file check.""" 510 506 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 511 - agents_dir = tmp_path / "agents" 512 - agents_dir.mkdir() 513 - unified_dir = agents_dir / "unified" 507 + talents_dir = tmp_path / "talents" 508 + talents_dir.mkdir() 509 + unified_dir = talents_dir / "unified" 514 510 unified_dir.mkdir() 515 511 (tmp_path / "health").mkdir() 516 512 517 - agent_id = "1234567890125" 513 + use_id = "1234567890125" 518 514 519 515 # Agent already completed before we start waiting 520 - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') 516 + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') 521 517 522 - completed, timed_out = wait_for_agents([agent_id], timeout=1) 518 + completed, timed_out = wait_for_uses([use_id], timeout=1) 523 519 524 520 # Should find via initial file check 525 - assert completed == {agent_id: "finish"} 521 + assert completed == {use_id: "finish"} 526 522 assert timed_out == [] 527 523 528 524 529 525 def test_wait_for_agents_timeout_actual(tmp_path, monkeypatch): 530 - """Test wait_for_agents times out for agents that never complete.""" 526 + """Test wait_for_uses times out for agents that never complete.""" 531 527 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 532 - agents_dir = tmp_path / "agents" 533 - agents_dir.mkdir() 534 - unified_dir = agents_dir / "unified" 528 + talents_dir = tmp_path / "talents" 529 + talents_dir.mkdir() 530 + unified_dir = talents_dir / "unified" 535 531 unified_dir.mkdir() 536 532 (tmp_path / "health").mkdir() 537 533 538 - agent_id = "1234567890126" 534 + use_id = "1234567890126" 539 535 # Create active file (not completed) 540 - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') 536 + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') 541 537 542 - completed, timed_out = wait_for_agents([agent_id], timeout=1) 538 + completed, timed_out = wait_for_uses([use_id], timeout=1) 543 539 544 540 assert completed == {} 545 - assert timed_out == [agent_id] 541 + assert timed_out == [use_id] 546 542 547 543 548 544 def test_wait_for_agents_partial(callosum_server): 549 - """Test wait_for_agents with some completing and some timing out.""" 545 + """Test wait_for_uses with some completing and some timing out.""" 550 546 tmp_path = callosum_server 551 - agents_dir = tmp_path / "agents" 552 - unified_dir = agents_dir / "unified" 547 + talents_dir = tmp_path / "talents" 548 + unified_dir = talents_dir / "unified" 553 549 unified_dir.mkdir(exist_ok=True) 554 550 555 551 completing_agent = "1111" ··· 561 557 result = {"completed": None, "timed_out": None} 562 558 563 559 def wait_thread(): 564 - result["completed"], result["timed_out"] = wait_for_agents( 560 + result["completed"], result["timed_out"] = wait_for_uses( 565 561 [completing_agent, timeout_agent], timeout=2 566 562 ) 567 563 ··· 575 571 client = CallosumConnection() 576 572 client.start() 577 573 time.sleep(0.1) 578 - client.emit("cortex", "finish", agent_id=completing_agent, result="done") 574 + client.emit("cortex", "finish", use_id=completing_agent, result="done") 579 575 time.sleep(0.1) 580 576 client.stop() 581 577 ··· 590 586 import logging 591 587 592 588 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 593 - agents_dir = tmp_path / "agents" 594 - agents_dir.mkdir() 595 - unified_dir = agents_dir / "unified" 589 + talents_dir = tmp_path / "talents" 590 + talents_dir.mkdir() 591 + unified_dir = talents_dir / "unified" 596 592 unified_dir.mkdir() 597 593 (tmp_path / "health").mkdir() 598 594 599 - agent_id = "1234567890127" 595 + use_id = "1234567890127" 600 596 601 597 # Start with active file 602 - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') 598 + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') 603 599 604 600 result = {"completed": None, "timed_out": None} 605 601 606 602 def wait_and_complete(): 607 603 # Wait a bit then "complete" the agent by renaming file 608 604 time.sleep(0.3) 609 - (unified_dir / f"{agent_id}_active.jsonl").unlink() 610 - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') 605 + (unified_dir / f"{use_id}_active.jsonl").unlink() 606 + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') 611 607 612 608 completer = threading.Thread(target=wait_and_complete) 613 609 completer.start() 614 610 615 611 with caplog.at_level(logging.INFO): 616 - result["completed"], result["timed_out"] = wait_for_agents( 617 - [agent_id], timeout=1 618 - ) 612 + result["completed"], result["timed_out"] = wait_for_uses([use_id], timeout=1) 619 613 620 614 completer.join() 621 615 622 616 # Should recover via final file check 623 - assert result["completed"] == {agent_id: "finish"} 617 + assert result["completed"] == {use_id: "finish"} 624 618 assert result["timed_out"] == [] 625 619 626 620 # Should log about missed event 627 621 assert any( 628 - "completion event not received but agent completed" in record.message 622 + "completion event not received but use completed" in record.message 629 623 for record in caplog.records 630 624 )
+11 -11
tests/test_dream_activity.py
··· 114 114 115 115 monkeypatch.setattr("think.dream.cortex_request", mock_cortex_request) 116 116 monkeypatch.setattr( 117 - "think.dream.wait_for_agents", 117 + "think.dream.wait_for_uses", 118 118 lambda ids, timeout: ({aid: "finish" for aid in ids}, []), 119 119 ) 120 120 ··· 170 170 171 171 monkeypatch.setattr("think.dream.cortex_request", mock_cortex_request) 172 172 monkeypatch.setattr( 173 - "think.dream.wait_for_agents", 173 + "think.dream.wait_for_uses", 174 174 lambda ids, timeout: ({aid: "finish" for aid in ids}, []), 175 175 ) 176 176 ··· 220 220 221 221 monkeypatch.setattr("think.dream.cortex_request", mock_cortex_request) 222 222 monkeypatch.setattr( 223 - "think.dream.wait_for_agents", 223 + "think.dream.wait_for_uses", 224 224 lambda ids, timeout: ({aid: "finish" for aid in ids}, []), 225 225 ) 226 226 ··· 282 282 lambda prompt, name, config: "agent-1", 283 283 ) 284 284 monkeypatch.setattr( 285 - "think.dream.wait_for_agents", 285 + "think.dream.wait_for_uses", 286 286 lambda ids, timeout: ({aid: "error" for aid in ids}, []), 287 287 ) 288 288 ··· 358 358 lambda prompt, name, config: "agent-1", 359 359 ) 360 360 monkeypatch.setattr( 361 - "think.dream.wait_for_agents", 361 + "think.dream.wait_for_uses", 362 362 lambda ids, timeout: ({aid: "finish" for aid in ids}, []), 363 363 ) 364 364 ··· 376 376 events = [e[0] for e in emitted] 377 377 assert "started" in events 378 378 assert "group_started" in events 379 - assert "agent_started" in events 380 - assert "agent_completed" in events 379 + assert "talent_started" in events 380 + assert "talent_completed" in events 381 381 382 382 assert "group_completed" in events 383 383 assert "completed" in events ··· 889 889 """Tests for activity template variables in _build_prompt_context.""" 890 890 891 891 def test_activity_vars_populated(self): 892 - from think.agents import _build_prompt_context 892 + from think.talents import _build_prompt_context 893 893 894 894 activity = { 895 895 "id": "coding_100000_300", ··· 916 916 assert int(ctx["activity_duration"]) == 10 # 2 * 300s = 10 min 917 917 918 918 def test_no_activity_no_vars(self): 919 - from think.agents import _build_prompt_context 919 + from think.talents import _build_prompt_context 920 920 921 921 ctx = _build_prompt_context("20260209", None, None) 922 922 ··· 924 924 assert "activity_type" not in ctx 925 925 926 926 def test_empty_entities(self): 927 - from think.agents import _build_prompt_context 927 + from think.talents import _build_prompt_context 928 928 929 929 activity = { 930 930 "id": "browsing_100000_300", ··· 940 940 assert ctx["activity_entities"] == "" 941 941 942 942 def test_duration_minimum_one(self): 943 - from think.agents import _build_prompt_context 943 + from think.talents import _build_prompt_context 944 944 945 945 activity = { 946 946 "id": "test_bad_seg",
+30 -28
tests/test_dream_segment.py
··· 17 17 day_dir = journal / "chronicle" / "20240115" 18 18 segment_path = day_dir / "default" / "120000_300" 19 19 segment_path.mkdir(parents=True) 20 - (segment_path / "agents").mkdir(parents=True) 20 + (segment_path / "talents").mkdir(parents=True) 21 21 22 22 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 23 23 return segment_path ··· 57 57 58 58 59 59 def _write_sense_output(segment_dir: Path, sense_json: dict) -> None: 60 - (segment_dir / "agents" / "sense.json").write_text( 60 + (segment_dir / "talents" / "sense.json").write_text( 61 61 json.dumps(sense_json), 62 62 encoding="utf-8", 63 63 ) ··· 74 74 def test_empty_file_returns_empty(self, segment_dir): 75 75 from think.facets import load_segment_facets 76 76 77 - (segment_dir / "agents" / "facets.json").write_text("") 77 + (segment_dir / "talents" / "facets.json").write_text("") 78 78 assert load_segment_facets("20240115", "120000_300") == [] 79 79 80 80 def test_empty_array_returns_empty(self, segment_dir): 81 81 from think.facets import load_segment_facets 82 82 83 - (segment_dir / "agents" / "facets.json").write_text("[]") 83 + (segment_dir / "talents" / "facets.json").write_text("[]") 84 84 assert load_segment_facets("20240115", "120000_300") == [] 85 85 86 86 def test_valid_facets_extracted(self, segment_dir): ··· 90 90 {"facet": "work", "activity": "Code review", "level": "high"}, 91 91 {"facet": "personal", "activity": "Email check", "level": "low"}, 92 92 ] 93 - (segment_dir / "agents" / "facets.json").write_text(json.dumps(facets_data)) 93 + (segment_dir / "talents" / "facets.json").write_text(json.dumps(facets_data)) 94 94 95 95 assert load_segment_facets("20240115", "120000_300") == ["work", "personal"] 96 96 97 97 def test_malformed_json_returns_empty(self, segment_dir, caplog): 98 98 from think.facets import load_segment_facets 99 99 100 - (segment_dir / "agents" / "facets.json").write_text("{ invalid json") 100 + (segment_dir / "talents" / "facets.json").write_text("{ invalid json") 101 101 assert load_segment_facets("20240115", "120000_300") == [] 102 102 assert "Failed to parse facets.json" in caplog.text 103 103 104 104 def test_non_array_returns_empty(self, segment_dir, caplog): 105 105 from think.facets import load_segment_facets 106 106 107 - (segment_dir / "agents" / "facets.json").write_text('{"facet": "work"}') 107 + (segment_dir / "talents" / "facets.json").write_text('{"facet": "work"}') 108 108 assert load_segment_facets("20240115", "120000_300") == [] 109 109 assert "not an array" in caplog.text 110 110 ··· 116 116 {"activity": "Unknown"}, 117 117 {"facet": "personal", "activity": "Email"}, 118 118 ] 119 - (segment_dir / "agents" / "facets.json").write_text(json.dumps(facets_data)) 119 + (segment_dir / "talents" / "facets.json").write_text(json.dumps(facets_data)) 120 120 121 121 assert load_segment_facets("20240115", "120000_300") == ["work", "personal"] 122 122 ··· 143 143 ) 144 144 monkeypatch.setattr( 145 145 dream, 146 - "wait_for_agents", 146 + "wait_for_uses", 147 147 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 148 148 ) 149 149 monkeypatch.setattr(dream, "_callosum", None) ··· 197 197 ) 198 198 monkeypatch.setattr( 199 199 dream, 200 - "wait_for_agents", 200 + "wait_for_uses", 201 201 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 202 202 ) 203 203 monkeypatch.setattr(dream, "_callosum", None) ··· 221 221 "20240115", 222 222 ) 223 223 ] 224 - density = json.loads((segment_dir / "agents" / "density.json").read_text()) 224 + density = json.loads((segment_dir / "talents" / "density.json").read_text()) 225 225 assert density["classification"] == "idle" 226 226 227 227 # Verify activity state persisted even on idle path ··· 255 255 ) 256 256 monkeypatch.setattr( 257 257 dream, 258 - "wait_for_agents", 258 + "wait_for_uses", 259 259 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 260 260 ) 261 261 monkeypatch.setattr(dream, "_callosum", None) ··· 315 315 ) 316 316 monkeypatch.setattr( 317 317 dream, 318 - "wait_for_agents", 318 + "wait_for_uses", 319 319 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 320 320 ) 321 321 monkeypatch.setattr(dream, "_callosum", None) ··· 351 351 ) 352 352 monkeypatch.setattr( 353 353 dream, 354 - "wait_for_agents", 354 + "wait_for_uses", 355 355 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 356 356 ) 357 357 monkeypatch.setattr(dream, "_callosum", None) ··· 392 392 ) 393 393 monkeypatch.setattr( 394 394 dream, 395 - "wait_for_agents", 395 + "wait_for_uses", 396 396 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 397 397 ) 398 398 monkeypatch.setattr(dream, "_callosum", None) ··· 431 431 ) 432 432 monkeypatch.setattr( 433 433 dream, 434 - "wait_for_agents", 434 + "wait_for_uses", 435 435 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 436 436 ) 437 437 monkeypatch.setattr(dream, "_callosum", None) ··· 469 469 def mock_wait_for_agents(agent_ids, timeout=600): 470 470 return ({agent_ids[0]: "error"}, []) 471 471 472 - monkeypatch.setattr(dream, "wait_for_agents", mock_wait_for_agents) 472 + monkeypatch.setattr(dream, "wait_for_uses", mock_wait_for_agents) 473 473 monkeypatch.setattr(dream, "_callosum", None) 474 474 475 475 success, failed, failed_names = dream.run_segment_sense( ··· 529 529 ) 530 530 monkeypatch.setattr( 531 531 dream, 532 - "wait_for_agents", 532 + "wait_for_uses", 533 533 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 534 534 ) 535 535 monkeypatch.setattr( ··· 582 582 segment_dir, 583 583 {"density": "active", "recommend": {}, "facets": []}, 584 584 ) 585 - (segment_dir / "agents" / "entities.md").write_text( 585 + (segment_dir / "talents" / "entities.md").write_text( 586 586 "entities", encoding="utf-8" 587 587 ) 588 588 ··· 606 606 ) 607 607 monkeypatch.setattr( 608 608 dream, 609 - "wait_for_agents", 609 + "wait_for_uses", 610 610 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 611 611 ) 612 612 monkeypatch.setattr( ··· 652 652 monkeypatch.setattr(dream, "_SEND_RETRY_DELAYS", (0.0, 0.0)) 653 653 monkeypatch.setattr( 654 654 dream, 655 - "wait_for_agents", 655 + "wait_for_uses", 656 656 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 657 657 ) 658 658 monkeypatch.setattr(dream, "_callosum", None) ··· 886 886 path = tmp_path / "test.jsonl" 887 887 writer = DreamJSONLWriter(str(path)) 888 888 writer.log("run.start", mode="segment", day="20240115") 889 - writer.log("agent.skip", name="screen", reason="not_recommended", detail="test") 889 + writer.log( 890 + "talent.skip", name="screen", reason="not_recommended", detail="test" 891 + ) 890 892 writer.close() 891 893 892 894 lines = path.read_text().strip().split("\n") ··· 899 901 assert first["mode"] == "segment" 900 902 901 903 second = json.loads(lines[1]) 902 - assert second["event"] == "agent.skip" 904 + assert second["event"] == "talent.skip" 903 905 assert writer.skip_count == 1 904 906 905 907 def test_creates_parent_dirs(self, tmp_path): ··· 917 919 """Tests for JSONL event emission during segment orchestration.""" 918 920 919 921 def test_density_idle_skip_event(self, segment_dir, monkeypatch): 920 - """JSONL emits agent.skip with reason=density_idle for idle segments.""" 922 + """JSONL emits talent.skip with reason=density_idle for idle segments.""" 921 923 from think import dream 922 924 from think.dream import DreamJSONLWriter 923 925 ··· 941 943 ) 942 944 monkeypatch.setattr( 943 945 dream, 944 - "wait_for_agents", 946 + "wait_for_uses", 945 947 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 946 948 ) 947 949 monkeypatch.setattr(dream, "_callosum", None) ··· 960 962 json.loads(line) 961 963 for line in jsonl_path.read_text(encoding="utf-8").strip().splitlines() 962 964 ] 963 - skips = [event for event in events if event["event"] == "agent.skip"] 965 + skips = [event for event in events if event["event"] == "talent.skip"] 964 966 965 967 assert any(skip["reason"] == "density_idle" for skip in skips) 966 968 ··· 996 998 ) 997 999 monkeypatch.setattr( 998 1000 dream, 999 - "wait_for_agents", 1001 + "wait_for_uses", 1000 1002 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 1001 1003 ) 1002 1004 monkeypatch.setattr(dream, "_callosum", None) ··· 1017 1019 ] 1018 1020 assert "sense.complete" in [event["event"] for event in events] 1019 1021 1020 - skips = [event for event in events if event["event"] == "agent.skip"] 1022 + skips = [event for event in events if event["event"] == "talent.skip"] 1021 1023 skip_pairs = {(event["name"], event["reason"]) for event in skips} 1022 1024 assert ("documents", "no_config") in skip_pairs 1023 1025 assert ("screen", "not_recommended") in skip_pairs
+4 -4
tests/test_engage.py
··· 50 50 with ( 51 51 patch("think.cortex_client.cortex_request", return_value="agent-123"), 52 52 patch( 53 - "think.cortex_client.wait_for_agents", 53 + "think.cortex_client.wait_for_uses", 54 54 return_value=({"agent-123": "finish"}, []), 55 55 ), 56 56 patch( 57 - "think.cortex_client.read_agent_events", 57 + "think.cortex_client.read_use_events", 58 58 return_value=[{"event": "finish", "result": "All fixed!"}], 59 59 ), 60 60 ): ··· 67 67 with ( 68 68 patch("think.cortex_client.cortex_request", return_value="agent-123"), 69 69 patch( 70 - "think.cortex_client.wait_for_agents", 70 + "think.cortex_client.wait_for_uses", 71 71 return_value=({"agent-123": "error"}, []), 72 72 ), 73 73 ): ··· 79 79 with ( 80 80 patch("think.cortex_client.cortex_request", return_value="agent-123"), 81 81 patch( 82 - "think.cortex_client.wait_for_agents", 82 + "think.cortex_client.wait_for_uses", 83 83 return_value=({}, ["agent-123"]), 84 84 ), 85 85 ):
+2 -2
tests/test_entities.py
··· 1742 1742 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 1743 1743 1744 1744 # Create a knowledge graph file 1745 - day_dir = tmp_path / "chronicle" / "20260108" / "agents" 1745 + day_dir = tmp_path / "chronicle" / "20260108" / "talents" 1746 1746 day_dir.mkdir(parents=True) 1747 1747 1748 1748 kg_content = """# Knowledge Graph Report ··· 1790 1790 """Test parsing returns empty list for empty KG.""" 1791 1791 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 1792 1792 1793 - day_dir = tmp_path / "chronicle" / "20260108" / "agents" 1793 + day_dir = tmp_path / "chronicle" / "20260108" / "talents" 1794 1794 day_dir.mkdir(parents=True) 1795 1795 (day_dir / "knowledge_graph.md").write_text("") 1796 1796
+10 -10
tests/test_entity_agents.py tests/test_entity_talents.py
··· 7 7 8 8 import pytest 9 9 10 - from think.talent import get_agent 10 + from think.talent import get_talent 11 11 12 12 13 13 @pytest.fixture ··· 21 21 def test_entities_agent_config(fixture_journal): 22 22 """Test detection agent configuration loads correctly.""" 23 23 # Entity agents are in apps/entities/talent/ so use app-qualified name 24 - config = get_agent("entities:entities") 24 + config = get_talent("entities:entities") 25 25 26 26 # Verify required fields 27 27 assert config["name"] == "entities:entities" ··· 38 38 def test_entities_review_agent_config(fixture_journal): 39 39 """Test review agent configuration loads correctly.""" 40 40 # Entity agents are in apps/entities/talent/ so use app-qualified name 41 - config = get_agent("entities:entities_review") 41 + config = get_talent("entities:entities_review") 42 42 43 43 # Verify required fields 44 44 assert config["name"] == "entities:entities_review" ··· 54 54 55 55 def test_entities_agent_instruction_content(fixture_journal): 56 56 """Test detection agent instruction contains expected sections.""" 57 - config = get_agent("entities:entities") 57 + config = get_talent("entities:entities") 58 58 prompt = config["user_instruction"] 59 59 60 60 # Check for key sections in the agent prompt ··· 67 67 68 68 def test_entities_review_agent_instruction_content(fixture_journal): 69 69 """Test review agent instruction contains expected sections.""" 70 - config = get_agent("entities:entities_review") 70 + config = get_talent("entities:entities_review") 71 71 prompt = config["user_instruction"] 72 72 73 73 # Check for key sections in the agent prompt ··· 80 80 81 81 def test_agent_context_includes_entities_by_facet(fixture_journal): 82 82 """Test that agent context includes entities grouped by facet.""" 83 - config = get_agent("entities:entities") 83 + config = get_talent("entities:entities") 84 84 85 85 prompt = config["user_instruction"] 86 86 assert "Available Facets" in prompt ··· 97 97 98 98 99 99 def test_agent_context_with_facet_focus(fixture_journal): 100 - """Test that get_agent with facet parameter uses focused single-facet context.""" 101 - config = get_agent("unified", facet="full-featured") 100 + """Test that get_talent with facet parameter uses focused single-facet context.""" 101 + config = get_talent("unified", facet="full-featured") 102 102 103 103 prompt = config["user_instruction"] 104 104 ··· 117 117 118 118 def test_agent_priority_ordering(fixture_journal): 119 119 """Test that entity agents have correct priority ordering.""" 120 - detection_config = get_agent("entities:entities") 121 - review_config = get_agent("entities:entities_review") 120 + detection_config = get_talent("entities:entities") 121 + review_config = get_talent("entities:entities_review") 122 122 123 123 detection_priority = detection_config["priority"] 124 124 review_priority = review_config["priority"]
+2 -2
tests/test_entity_observer_context.py
··· 13 13 from think.entities.loading import clear_entity_loading_cache 14 14 from think.entities.observations import clear_observation_cache, load_observations 15 15 from think.entities.relationships import clear_relationship_caches 16 - from think.talent import get_agent 16 + from think.talent import get_talent 17 17 18 18 19 19 def _set_journal(path: str) -> None: ··· 323 323 def test_entity_observer_agent_config(): 324 324 _set_journal("tests/fixtures/journal") 325 325 326 - config = get_agent("entities:entity_observer") 326 + config = get_talent("entities:entity_observer") 327 327 328 328 assert config["type"] == "generate" 329 329 assert config.get("output") == "json"
+6 -6
tests/test_facets.py
··· 346 346 day_dir = journal / "chronicle" / "20240115" 347 347 348 348 # Create segment with facets.json containing two facets (stream layout) 349 - seg1 = day_dir / "archon" / "100000_300" / "agents" 349 + seg1 = day_dir / "archon" / "100000_300" / "talents" 350 350 seg1.mkdir(parents=True) 351 351 (seg1 / "facets.json").write_text( 352 352 json.dumps( ··· 358 358 ) 359 359 360 360 # Create another segment with overlapping + new facet 361 - seg2 = day_dir / "archon" / "110000_300" / "agents" 361 + seg2 = day_dir / "archon" / "110000_300" / "talents" 362 362 seg2.mkdir(parents=True) 363 363 (seg2 / "facets.json").write_text( 364 364 json.dumps( ··· 382 382 day_dir = journal / "chronicle" / "20240115" 383 383 384 384 # Segment with empty facets array (stream layout) 385 - seg1 = day_dir / "archon" / "100000_300" / "agents" 385 + seg1 = day_dir / "archon" / "100000_300" / "talents" 386 386 seg1.mkdir(parents=True) 387 387 (seg1 / "facets.json").write_text("[]") 388 388 389 389 # Segment with empty file 390 - seg2 = day_dir / "archon" / "110000_300" / "agents" 390 + seg2 = day_dir / "archon" / "110000_300" / "talents" 391 391 seg2.mkdir(parents=True) 392 392 (seg2 / "facets.json").write_text("") 393 393 ··· 428 428 day_dir = journal / "chronicle" / "20240115" 429 429 430 430 # Malformed JSON segment (stream layout) 431 - seg1 = day_dir / "archon" / "100000_300" / "agents" 431 + seg1 = day_dir / "archon" / "100000_300" / "talents" 432 432 seg1.mkdir(parents=True) 433 433 (seg1 / "facets.json").write_text("{ invalid json") 434 434 435 435 # Valid segment 436 - seg2 = day_dir / "archon" / "110000_300" / "agents" 436 + seg2 = day_dir / "archon" / "110000_300" / "talents" 437 437 seg2.mkdir(parents=True) 438 438 (seg2 / "facets.json").write_text( 439 439 json.dumps(
+12 -12
tests/test_formatters.py
··· 1016 1016 "title": "Project kickoff", 1017 1017 "start": "14:00:00", 1018 1018 "occurred": False, 1019 - "source": "20240101/agents/schedule.md", 1019 + "source": "20240101/talents/schedule.md", 1020 1020 "participants": ["Alice", "Bob"], 1021 1021 } 1022 1022 ] ··· 1039 1039 "title": "Team standup", 1040 1040 "start": "09:00:00", 1041 1041 "occurred": True, 1042 - "source": "20240101/agents/meetings.md", 1042 + "source": "20240101/talents/meetings.md", 1043 1043 "participants": ["Alice"], 1044 1044 } 1045 1045 ] ··· 1168 1168 """Test pattern matching for .md files.""" 1169 1169 from think.formatters import get_formatter 1170 1170 1171 - formatter = get_formatter("20240101/agents/flow.md") 1171 + formatter = get_formatter("20240101/talents/flow.md") 1172 1172 assert formatter is not None 1173 1173 assert formatter.__name__ == "format_markdown" 1174 1174 ··· 1176 1176 """Test pattern matching for segment screen.md files.""" 1177 1177 from think.formatters import get_formatter 1178 1178 1179 - formatter = get_formatter("20240101/default/123456_300/agents/screen.md") 1179 + formatter = get_formatter("20240101/default/123456_300/talents/screen.md") 1180 1180 assert formatter is not None 1181 1181 assert formatter.__name__ == "format_markdown" 1182 1182 ··· 1282 1282 1283 1283 path = ( 1284 1284 Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) 1285 - / "chronicle/20240101/agents/flow.md" 1285 + / "chronicle/20240101/talents/flow.md" 1286 1286 ) 1287 1287 chunks, meta = format_file(path) 1288 1288 ··· 1296 1296 1297 1297 path = ( 1298 1298 Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) 1299 - / "chronicle/20240101/agents/flow.md" 1299 + / "chronicle/20240101/talents/flow.md" 1300 1300 ) 1301 1301 text = load_markdown(path) 1302 1302 ··· 1373 1373 """Test day extraction from daily agent output path.""" 1374 1374 from think.formatters import extract_path_metadata 1375 1375 1376 - meta = extract_path_metadata("20240101/agents/flow.md") 1376 + meta = extract_path_metadata("20240101/talents/flow.md") 1377 1377 assert meta["day"] == "20240101" 1378 1378 assert meta["facet"] == "" 1379 1379 assert meta["agent"] == "flow" ··· 1382 1382 """Test day and agent extraction from segment markdown.""" 1383 1383 from think.formatters import extract_path_metadata 1384 1384 1385 - meta = extract_path_metadata("20240101/100000/agents/screen.md") 1385 + meta = extract_path_metadata("20240101/100000/talents/screen.md") 1386 1386 assert meta["day"] == "20240101" 1387 1387 assert meta["facet"] == "" 1388 1388 assert meta["agent"] == "screen" ··· 1445 1445 """Test app output path extraction.""" 1446 1446 from think.formatters import extract_path_metadata 1447 1447 1448 - meta = extract_path_metadata("apps/myapp/agents/custom.md") 1448 + meta = extract_path_metadata("apps/myapp/talents/custom.md") 1449 1449 assert meta["day"] == "" 1450 1450 assert meta["facet"] == "" 1451 1451 assert meta["agent"] == "myapp:custom" ··· 1727 1727 assert "- text: Test task" in chunks[0]["markdown"] 1728 1728 1729 1729 def test_format_logs_with_agent_id(self): 1730 - """Test that agent_id renders as a link.""" 1730 + """Test that use_id renders as a link.""" 1731 1731 from think.facets import format_logs 1732 1732 1733 1733 entries = [ ··· 1737 1737 "actor": "entities", 1738 1738 "action": "entity_attach", 1739 1739 "params": {"type": "Person", "name": "Alice"}, 1740 - "agent_id": "1765870373972", 1740 + "use_id": "1765870373972", 1741 1741 } 1742 1742 ] 1743 1743 ··· 1745 1745 1746 1746 assert len(chunks) == 1 1747 1747 assert ( 1748 - "**Agent:** [1765870373972](/app/sol/1765870373972)" 1748 + "**Talent:** [1765870373972](/app/sol/1765870373972)" 1749 1749 in chunks[0]["markdown"] 1750 1750 ) 1751 1751
tests/test_generate_agents.py tests/test_generate_talents.py
+7 -7
tests/test_generate_full.py
··· 65 65 66 66 def test_generate_output_ndjson(tmp_path, monkeypatch): 67 67 """Test basic output generation via NDJSON protocol.""" 68 - mod = importlib.import_module("think.agents") 68 + mod = importlib.import_module("think.talents") 69 69 copy_day(tmp_path) 70 70 71 71 import think.talent ··· 111 111 112 112 def test_generate_hook_invoked_with_context(tmp_path, monkeypatch): 113 113 """Test that hooks receive correct context including span flag.""" 114 - mod = importlib.import_module("think.agents") 114 + mod = importlib.import_module("think.talents") 115 115 copy_day(tmp_path) 116 116 117 117 import think.talent ··· 171 171 172 172 # Read captured context 173 173 captured_path = ( 174 - tmp_path / "chronicle" / "20240101" / "agents" / "context_captured.json" 174 + tmp_path / "chronicle" / "20240101" / "talents" / "context_captured.json" 175 175 ) 176 176 captured = json.loads(captured_path.read_text()) 177 177 ··· 186 186 187 187 def test_generate_without_hook_succeeds(tmp_path, monkeypatch): 188 188 """Test that generators without hooks still work correctly.""" 189 - mod = importlib.import_module("think.agents") 189 + mod = importlib.import_module("think.talents") 190 190 copy_day(tmp_path) 191 191 192 192 import think.talent ··· 228 228 229 229 def test_generate_error_event_on_missing_generator(tmp_path, monkeypatch): 230 230 """Test that missing generator name emits error event.""" 231 - mod = importlib.import_module("think.agents") 231 + mod = importlib.import_module("think.talents") 232 232 copy_day(tmp_path) 233 233 234 234 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) ··· 249 249 250 250 def test_generate_skipped_on_no_input(tmp_path, monkeypatch): 251 251 """Test that generator emits skipped finish when no input.""" 252 - mod = importlib.import_module("think.agents") 252 + mod = importlib.import_module("think.talents") 253 253 254 254 # Create empty day directory (no transcripts) 255 255 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) ··· 286 286 287 287 def test_cogitate_not_skipped_without_sources(tmp_path, monkeypatch): 288 288 """Test that cogitate agents with day but no sources are not skipped.""" 289 - mod = importlib.import_module("think.agents") 289 + mod = importlib.import_module("think.talents") 290 290 291 291 # Create empty day directory (no transcripts) 292 292 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path)
+9 -9
tests/test_generate_scan_day.py
··· 16 16 dest = day_path("20240101") 17 17 src = FIXTURES / "journal" / "chronicle" / "20240101" 18 18 copytree_tracked(src, dest) 19 - agents_dir = dest / "agents" 20 - agents_dir.mkdir(exist_ok=True) # Allow existing directory 21 - (agents_dir / "flow.md").write_text("done") 19 + talents_dir = dest / "talents" 20 + talents_dir.mkdir(exist_ok=True) # Allow existing directory 21 + (talents_dir / "flow.md").write_text("done") 22 22 return dest 23 23 24 24 25 25 def test_scan_day(tmp_path, monkeypatch): 26 - mod = importlib.import_module("think.agents") 26 + mod = importlib.import_module("think.talents") 27 27 day_dir = copy_day(tmp_path) 28 28 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 29 29 30 30 info = mod.scan_day("20240101") 31 - assert "agents/flow.md" in info["processed"] 32 - assert "agents/timeline.md" in info["repairable"] 31 + assert "talents/flow.md" in info["processed"] 32 + assert "talents/timeline.md" in info["repairable"] 33 33 34 - (day_dir / "agents" / "timeline.md").write_text("done") 34 + (day_dir / "talents" / "timeline.md").write_text("done") 35 35 info_after = mod.scan_day("20240101") 36 - assert "agents/timeline.md" in info_after["processed"] 37 - assert "agents/timeline.md" not in info_after["repairable"] 36 + assert "talents/timeline.md" in info_after["processed"] 37 + assert "talents/timeline.md" not in info_after["repairable"]
+4 -4
tests/test_google.py
··· 71 71 setup_google_genai_stub(monkeypatch, with_thinking=False) 72 72 sys.modules.pop("think.providers.google", None) 73 73 importlib.reload(importlib.import_module("think.providers.google")) 74 - mod = importlib.reload(importlib.import_module("think.agents")) 74 + mod = importlib.reload(importlib.import_module("think.talents")) 75 75 76 76 journal = tmp_path / "journal" 77 77 journal.mkdir() ··· 127 127 "tools": ["search_insights"], 128 128 } 129 129 ) 130 - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) 130 + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) 131 131 132 132 out_lines = capsys.readouterr().out.strip().splitlines() 133 133 events = [json.loads(line) for line in out_lines] ··· 149 149 150 150 sys.modules.pop("think.providers.google", None) 151 151 importlib.reload(importlib.import_module("think.providers.google")) 152 - mod = importlib.reload(importlib.import_module("think.agents")) 152 + mod = importlib.reload(importlib.import_module("think.talents")) 153 153 154 154 journal = tmp_path / "journal" 155 155 journal.mkdir() ··· 166 166 "tools": ["search_insights"], 167 167 } 168 168 ) 169 - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) 169 + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) 170 170 171 171 # Check stdout for error event 172 172 out_lines = capsys.readouterr().out.strip().splitlines()
+2 -2
tests/test_google_thinking.py
··· 26 26 27 27 sys.modules.pop("think.providers.google", None) 28 28 importlib.reload(importlib.import_module("think.providers.google")) 29 - mod = importlib.reload(importlib.import_module("think.agents")) 29 + mod = importlib.reload(importlib.import_module("think.talents")) 30 30 31 31 journal = tmp_path / "journal" 32 32 journal.mkdir() ··· 109 109 "tools": ["search_insights"], 110 110 } 111 111 ) 112 - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) 112 + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) 113 113 114 114 out_lines = capsys.readouterr().out.strip().splitlines() 115 115 events = [json.loads(line) for line in out_lines]
+3 -3
tests/test_heartbeat.py
··· 25 25 "think.heartbeat.cortex_request", lambda *args, **kwargs: "agent-123" 26 26 ) 27 27 monkeypatch.setattr( 28 - "think.heartbeat.wait_for_agents", 28 + "think.heartbeat.wait_for_uses", 29 29 lambda *args, **kwargs: ({"agent-123": "finish"}, []), 30 30 ) 31 31 ··· 129 129 import think.heartbeat as mod 130 130 131 131 pid_file = journal_path / "health" / "heartbeat.pid" 132 - mod.wait_for_agents = lambda *a, **kw: ({}, ["agent-123"]) 132 + mod.wait_for_uses = lambda *a, **kw: ({}, ["agent-123"]) 133 133 134 134 with pytest.raises(SystemExit) as exc_info: 135 135 mod.main() ··· 246 246 ) 247 247 monkeypatch.setattr("think.heartbeat.ensure_sol_directory", lambda: None) 248 248 monkeypatch.setattr( 249 - "think.heartbeat.wait_for_agents", 249 + "think.heartbeat.wait_for_uses", 250 250 lambda *args, **kwargs: ({"agent-123": "finish"}, []), 251 251 ) 252 252
+14 -14
tests/test_home_events.py
··· 32 32 "tract": "cortex", 33 33 "event": "finish", 34 34 "name": "reviewer", 35 - "agent_id": "123", 35 + "use_id": "123", 36 36 "result": "hello", 37 37 } 38 38 ) ··· 41 41 mock_record.assert_not_called() 42 42 43 43 def test_ignores_missing_agent_id(self): 44 - """Handler returns early if agent_id is missing.""" 44 + """Handler returns early if use_id is missing.""" 45 45 ctx = self._make_ctx( 46 46 { 47 47 "tract": "cortex", ··· 61 61 { 62 62 "event": "request", 63 63 "ts": 1700000000000, 64 - "agent_id": "abc123", 64 + "use_id": "abc123", 65 65 "facet": "work", 66 66 "app": "home", 67 67 "path": "/home", ··· 70 70 { 71 71 "event": "finish", 72 72 "ts": 1700000001000, 73 - "agent_id": "abc123", 73 + "use_id": "abc123", 74 74 "result": "hi there", 75 75 }, 76 76 ] ··· 79 79 "tract": "cortex", 80 80 "event": "finish", 81 81 "name": agent_name, 82 - "agent_id": "abc123", 82 + "use_id": "abc123", 83 83 "result": "hi there", 84 84 } 85 85 ) 86 - with patch("apps.home.events.read_agent_events", return_value=events): 86 + with patch("apps.home.events.read_use_events", return_value=events): 87 87 with patch("apps.home.events.record_exchange") as mock_record: 88 88 record_triage_exchange(ctx) 89 89 mock_record.assert_called_once_with( ··· 93 93 user_message="hello world", 94 94 agent_response="hi there", 95 95 talent=agent_name, 96 - agent_id="abc123", 96 + use_id="abc123", 97 97 ) 98 98 99 99 def test_handles_missing_request_event(self): 100 100 """Handler uses empty strings for metadata if request event not found.""" 101 101 events = [ 102 - {"event": "finish", "agent_id": "abc123", "result": "done"}, 102 + {"event": "finish", "use_id": "abc123", "result": "done"}, 103 103 ] 104 104 ctx = self._make_ctx( 105 105 { 106 106 "tract": "cortex", 107 107 "event": "finish", 108 108 "name": "unified", 109 - "agent_id": "abc123", 109 + "use_id": "abc123", 110 110 "result": "done", 111 111 } 112 112 ) 113 - with patch("apps.home.events.read_agent_events", return_value=events): 113 + with patch("apps.home.events.read_use_events", return_value=events): 114 114 with patch("apps.home.events.record_exchange") as mock_record: 115 115 record_triage_exchange(ctx) 116 116 mock_record.assert_called_once_with( ··· 120 120 user_message="", 121 121 agent_response="done", 122 122 talent="unified", 123 - agent_id="abc123", 123 + use_id="abc123", 124 124 ) 125 125 126 126 def test_handles_read_error_gracefully(self): 127 - """Handler logs and swallows exceptions from read_agent_events.""" 127 + """Handler logs and swallows exceptions from read_use_events.""" 128 128 ctx = self._make_ctx( 129 129 { 130 130 "tract": "cortex", 131 131 "event": "finish", 132 132 "name": "unified", 133 - "agent_id": "abc123", 133 + "use_id": "abc123", 134 134 "result": "done", 135 135 } 136 136 ) 137 137 with patch( 138 - "apps.home.events.read_agent_events", 138 + "apps.home.events.read_use_events", 139 139 side_effect=FileNotFoundError("not found"), 140 140 ): 141 141 with patch("apps.home.events.record_exchange") as mock_record:
+66 -12
tests/test_home_yesterdays_processing.py
··· 58 58 journal.mkdir() 59 59 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 60 60 61 + for day, transcript_seconds in (("20260415", 3600), ("20260414", 2700)): 62 + facet_data = {"work": {"count": 1, "minutes": 15}} 63 + if day == "20260414": 64 + facet_data = {} 65 + stats_path = journal / "chronicle" / day / "stats.json" 66 + stats_path.parent.mkdir(parents=True, exist_ok=True) 67 + stats_path.write_text( 68 + json.dumps( 69 + { 70 + "stats": { 71 + "transcript_segments": 3, 72 + "transcript_duration": transcript_seconds, 73 + }, 74 + "facet_data": facet_data, 75 + "heatmap_data": {"weekday": 2, "hours": {"9": 45.0}}, 76 + } 77 + ), 78 + encoding="utf-8", 79 + ) 80 + 81 + health_path = ( 82 + journal / "chronicle" / "20260415" / "health" / "100_daily_dream.jsonl" 83 + ) 84 + health_path.parent.mkdir(parents=True, exist_ok=True) 85 + health_path.write_text("", encoding="utf-8") 86 + sparse_health_path = ( 87 + journal / "chronicle" / "20260414" / "health" / "100_daily_dream.jsonl" 88 + ) 89 + sparse_health_path.parent.mkdir(parents=True, exist_ok=True) 90 + sparse_health_path.write_text( 91 + json.dumps({"event": "run.complete", "mode": "daily", "duration_ms": 10}) 92 + + "\n", 93 + encoding="utf-8", 94 + ) 95 + 61 96 for rel_path in [ 62 - "chronicle/20260415/stats.json", 63 - "chronicle/20260415/agents/knowledge_graph.md", 64 - "chronicle/20260415/health/100_daily_dream.jsonl", 65 - "chronicle/20260414/stats.json", 97 + "chronicle/20260415/talents/knowledge_graph.md", 66 98 ]: 67 99 _copy_fixture_file(journal, rel_path) 68 100 ··· 139 171 VALUES (?, ?, NULL, NULL, NULL, ?, ?, NULL, NULL, ?) 140 172 """, 141 173 [ 142 - ("mention", "jane_doe", day, "work", f"{day}/agents/flow.md"), 143 - ("mention", "alice_johnson", day, "work", f"{day}/agents/flow.md"), 174 + ("mention", "jane_doe", day, "work", f"{day}/talents/flow.md"), 175 + ("mention", "alice_johnson", day, "work", f"{day}/talents/flow.md"), 144 176 ( 145 177 "mention", 146 178 "product_roadmap", 147 179 day, 148 180 "work", 149 - f"{day}/agents/knowledge_graph.md", 181 + f"{day}/talents/knowledge_graph.md", 150 182 ), 151 183 ( 152 184 "mention", 153 185 "launch_decision", 154 186 day, 155 187 "work", 156 - f"{day}/agents/knowledge_graph.md", 188 + f"{day}/talents/knowledge_graph.md", 157 189 ), 158 190 ], 159 191 ) ··· 163 195 164 196 165 197 def _append_dream_log( 166 - journal: Path, day: str, name: str, *, facet: str | None = None 198 + journal: Path, 199 + day: str, 200 + name: str, 201 + *, 202 + facet: str | None = None, 203 + event: str = "talent.fail", 167 204 ) -> None: 168 205 path = journal / "chronicle" / day / "health" / "101_daily_dream.jsonl" 169 206 path.parent.mkdir(parents=True, exist_ok=True) 170 207 with path.open("a", encoding="utf-8") as handle: 171 208 record = { 172 - "event": "agent.fail", 209 + "event": event, 173 210 "mode": "daily", 174 211 "name": name, 175 212 "state": "error", ··· 220 257 _write_briefing(journal, "2026-04-15T06:45:00") 221 258 222 259 monkeypatch.setattr("apps.home.routes._today", lambda: "20260415") 260 + monkeypatch.setattr( 261 + "apps.home.routes._knowledge_graph_freshness", 262 + lambda _day: {"fresh": True}, 263 + ) 223 264 224 265 summary = _summarize_yesterday_processing("20260414", 2) 225 266 ··· 378 419 tmp_path, monkeypatch 379 420 ): 380 421 journal = _seed_journal(tmp_path, monkeypatch) 381 - path = journal / "chronicle" / "20260415" / "agents" / "knowledge_graph.md" 422 + path = journal / "chronicle" / "20260415" / "talents" / "knowledge_graph.md" 382 423 383 424 _set_mtime(path, datetime(2026, 4, 15, 12, 0, 0)) 384 425 assert _knowledge_graph_freshness("20260415")["fresh"] is True ··· 411 452 "anomalies": [ 412 453 {"kind": "daily_agents_missing"}, 413 454 {"kind": "activity_agents_missing"}, 414 - {"kind": "agent_failure"}, 455 + {"kind": "talent_failure"}, 415 456 ] 416 457 }, 417 458 {"fresh": True}, ··· 431 472 _append_dream_log(journal, "20260415", "facet_newsletter", facet="work") 432 473 _append_dream_log(journal, "20260415", "knowledge_graph", facet="work") 433 474 _append_dream_log(journal, "20260415", "facet_newsletter") 475 + 476 + assert _newsletter_attempts_from_dream_logs("20260415") == (2, 3) 477 + 478 + 479 + def test_newsletter_attempts_accept_legacy_agent_fail(tmp_path, monkeypatch): 480 + journal = _seed_journal(tmp_path, monkeypatch) 481 + _append_dream_log( 482 + journal, 483 + "20260415", 484 + "facet_newsletter", 485 + facet="work", 486 + event="agent.fail", 487 + ) 434 488 435 489 assert _newsletter_attempts_from_dream_logs("20260415") == (2, 3) 436 490
+26 -25
tests/test_journal_index.py
··· 281 281 # Create daily insight 282 282 day = journal / "chronicle" / "20240101" 283 283 day.mkdir(parents=True) 284 - agents_dir = day / "agents" 284 + agents_dir = day / "talents" 285 285 agents_dir.mkdir() 286 286 (agents_dir / "flow.md").write_text("# Flow Summary\n\nWorked on project alpha.\n") 287 287 ··· 290 290 stream_dir.mkdir() 291 291 segment = stream_dir / "100000_300" 292 292 segment.mkdir() 293 - (segment / "agents").mkdir() 294 - (segment / "agents" / "screen.md").write_text( 293 + (segment / "talents").mkdir() 294 + (segment / "talents" / "screen.md").write_text( 295 295 "# Screen Summary\n\nViewed documentation.\n" 296 296 ) 297 297 # Add stream.json for segment stream metadata ··· 299 299 300 300 write_segment_stream(str(segment), "default", None, None, 1) 301 301 # Add second agent file for cross-file segment testing 302 - (segment / "agents" / "activity.md").write_text( 302 + (segment / "talents" / "activity.md").write_text( 303 303 "# Activity Summary\n\nMet with Scott Ward about Acme deal.\n" 304 304 ) 305 305 306 306 # Create evening segment for time_bucket testing 307 307 evening_segment = stream_dir / "200000_300" 308 308 evening_segment.mkdir() 309 - (evening_segment / "agents").mkdir() 310 - (evening_segment / "agents" / "screen.md").write_text( 309 + (evening_segment / "talents").mkdir() 310 + (evening_segment / "talents" / "screen.md").write_text( 311 311 "# Evening Screen\n\nReviewed evening reports.\n" 312 312 ) 313 313 write_segment_stream(str(evening_segment), "default", None, None, 1) ··· 636 636 # Non-day paths are never historical 637 637 assert _is_historical_day("facets/work/events/20240101.jsonl") is False 638 638 assert _is_historical_day("imports/123/summary.md") is False 639 - assert _is_historical_day("apps/home/agents/foo.md") is False 639 + assert _is_historical_day("apps/home/talents/foo.md") is False 640 640 641 641 # Future dates are not historical 642 - assert _is_historical_day("29991231/agents/flow.md") is False 642 + assert _is_historical_day("29991231/talents/flow.md") is False 643 643 644 644 # Path without slash is not historical 645 645 assert _is_historical_day("20240101") is False 646 646 assert _is_historical_day("") is False 647 647 648 648 # Day paths before today are historical (tested with a very old date) 649 - assert _is_historical_day("20000101/agents/flow.md") is True 649 + assert _is_historical_day("20000101/talents/flow.md") is True 650 650 651 651 652 652 def test_scan_journal_full_mode(journal_fixture): ··· 672 672 paths = set(files.keys()) 673 673 674 674 # Daily agent outputs 675 - assert "20240101/agents/flow.md" in paths 675 + assert "20240101/talents/flow.md" in paths 676 676 677 677 # Segment agent outputs 678 - assert "20240101/default/100000_300/agents/screen.md" in paths 678 + assert "20240101/default/100000_300/talents/screen.md" in paths 679 679 680 680 # Facet content 681 681 assert "facets/work/events/20240101.jsonl" in paths ··· 909 909 today = datetime.now().strftime("%Y%m%d") 910 910 day_dir = journal / today 911 911 day_dir.mkdir(parents=True) 912 - agents_dir = day_dir / "agents" 912 + agents_dir = day_dir / "talents" 913 913 agents_dir.mkdir() 914 914 output_file = agents_dir / "flow.md" 915 915 output_file.write_text("# Today Flow\n\nWorked on unique_today_content.\n") ··· 943 943 # Create historical day content 944 944 day_dir = journal / "chronicle" / "20200101" 945 945 day_dir.mkdir(parents=True) 946 - agents_dir = day_dir / "agents" 946 + agents_dir = day_dir / "talents" 947 947 agents_dir.mkdir() 948 948 output_file = agents_dir / "flow.md" 949 949 output_file.write_text("# Historical Flow\n\nWorked on historical_content.\n") ··· 978 978 # Create historical day content 979 979 day_dir = journal / "chronicle" / "20200101" 980 980 day_dir.mkdir(parents=True) 981 - agents_dir = day_dir / "agents" 981 + agents_dir = day_dir / "talents" 982 982 agents_dir.mkdir() 983 983 output_file = agents_dir / "flow.md" 984 984 output_file.write_text("# Historical Flow\n\nWorked on historical_full_test.\n") ··· 1007 1007 from think.indexer.journal import index_file, search_journal 1008 1008 1009 1009 # Index a specific file 1010 - result = index_file(str(journal_fixture), "20240101/agents/flow.md", verbose=True) 1010 + result = index_file(str(journal_fixture), "20240101/talents/flow.md", verbose=True) 1011 1011 assert result is True 1012 1012 1013 1013 # Should be searchable ··· 1019 1019 """Test indexing with absolute path.""" 1020 1020 from think.indexer.journal import index_file, search_journal 1021 1021 1022 - abs_path = str(journal_fixture / "chronicle" / "20240101" / "agents" / "flow.md") 1022 + abs_path = str(journal_fixture / "chronicle" / "20240101" / "talents" / "flow.md") 1023 1023 result = index_file(str(journal_fixture), abs_path, verbose=True) 1024 1024 assert result is True 1025 1025 ··· 1057 1057 from think.indexer.journal import index_file, search_journal 1058 1058 1059 1059 # Index the file 1060 - index_file(str(journal_fixture), "20240101/agents/flow.md") 1060 + index_file(str(journal_fixture), "20240101/talents/flow.md") 1061 1061 1062 1062 # Get initial count 1063 1063 total1, _ = search_journal("project alpha") 1064 1064 1065 1065 # Re-index the same file 1066 - index_file(str(journal_fixture), "20240101/agents/flow.md") 1066 + index_file(str(journal_fixture), "20240101/talents/flow.md") 1067 1067 1068 1068 # Count should be the same (not doubled) 1069 1069 total2, _ = search_journal("project alpha") ··· 1117 1117 write_segment_stream(seg_dir, "archon", None, None, 1) 1118 1118 1119 1119 result = _extract_stream( 1120 - str(tmp_path), "20240101/default/123456_300/agents/work/flow.md" 1120 + str(tmp_path), "20240101/default/123456_300/talents/work/flow.md" 1121 1121 ) 1122 1122 assert result == "archon" 1123 1123 ··· 1126 1126 """_extract_stream returns None for non-segment paths.""" 1127 1127 from think.indexer.journal import _extract_stream 1128 1128 1129 - result = _extract_stream(str(tmp_path), "20240101/agents/flow.md") 1129 + result = _extract_stream(str(tmp_path), "20240101/talents/flow.md") 1130 1130 assert result is None 1131 1131 1132 1132 result = _extract_stream(str(tmp_path), "facets/work/events/20240101.jsonl") ··· 1141 1141 seg_dir.mkdir(parents=True) 1142 1142 1143 1143 result = _extract_stream( 1144 - str(tmp_path), "20240101/default/123456_300/agents/work/flow.md" 1144 + str(tmp_path), "20240101/default/123456_300/talents/work/flow.md" 1145 1145 ) 1146 1146 assert result is None 1147 1147 ··· 1302 1302 assert count1 == count2 1303 1303 1304 1304 1305 - def test_scan_entities_deletion(tmp_path): 1305 + def test_scan_entities_deletion(tmp_path, monkeypatch): 1306 1306 """Verify entity rows are removed when source file is deleted.""" 1307 1307 src = Path("tests/fixtures/journal") 1308 1308 dst = tmp_path / "journal" 1309 1309 copytree_tracked(src, dst) 1310 1310 j = str(dst) 1311 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", j) 1311 1312 1312 1313 from think.indexer.journal import scan_journal 1313 1314 ··· 1467 1468 conn.close() 1468 1469 assert initial == 45 1469 1470 1470 - kg_file = dst / "chronicle" / "20240101" / "agents" / "knowledge_graph.md" 1471 + kg_file = dst / "chronicle" / "20240101" / "talents" / "knowledge_graph.md" 1471 1472 kg_file.unlink() 1472 1473 1473 1474 scan_journal(j, full=True) ··· 1711 1712 scan_journal(str(journal_fixture), verbose=True, full=True) 1712 1713 conn, _ = get_journal_index(str(journal_fixture)) 1713 1714 screen_chunks = conn.execute( 1714 - "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/agents/screen.md'" 1715 + "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/talents/screen.md'" 1715 1716 ).fetchone()[0] 1716 1717 activity_chunks = conn.execute( 1717 - "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/agents/activity.md'" 1718 + "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/talents/activity.md'" 1718 1719 ).fetchone()[0] 1719 1720 segment_chunks = conn.execute( 1720 1721 "SELECT count(*) FROM chunks WHERE agent='segment'"
+5 -5
tests/test_journal_stats.py
··· 27 27 (ts_dir2 / "center_DP-1_screen.webm").write_bytes(b"WEBM") 28 28 29 29 (day / "entities.md").write_text("") 30 - (day / "agents").mkdir() 31 - (day / "agents" / "flow.md").write_text("") 30 + (day / "talents").mkdir() 31 + (day / "talents" / "flow.md").write_text("") 32 32 33 33 # Create event in new JSONL format: facets/{facet}/events/YYYYMMDD.jsonl 34 34 events_dir = journal / "facets" / "work" / "events" ··· 45 45 "facet": "work", 46 46 "agent": "meetings", 47 47 "occurred": True, 48 - "source": "20240101/agents/meetings.md", 48 + "source": "20240101/talents/meetings.md", 49 49 } 50 50 (events_dir / "20240101.jsonl").write_text(json.dumps(event)) 51 51 ··· 169 169 170 170 # Test JSON output includes token usage 171 171 data = js.to_dict() 172 - assert data["schema_version"] == 2 172 + assert data["schema_version"] == 3 173 173 assert "generated_at" in data 174 174 assert data["day_count"] == 2 175 175 assert "tokens" in data ··· 252 252 "facet": "work", 253 253 "agent": "meetings", 254 254 "occurred": True, 255 - "source": "20240101/agents/meetings.md", 255 + "source": "20240101/talents/meetings.md", 256 256 } 257 257 (events_dir / "20240101.jsonl").write_text(json.dumps(event)) 258 258
+98
tests/test_maint_004_rename.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import importlib 7 + from pathlib import Path 8 + 9 + mod = importlib.import_module("apps.sol.maint.004_rename_agents_to_talents") 10 + 11 + 12 + def _patch_journal(monkeypatch, journal: Path, day: str = "20260417") -> None: 13 + monkeypatch.setattr(mod, "get_journal", lambda: str(journal)) 14 + monkeypatch.setattr(mod, "day_dirs", lambda: {day: str(journal / day)}) 15 + monkeypatch.setattr( 16 + mod, 17 + "iter_segments", 18 + lambda _day: [ 19 + ("default", "090000_300", journal / day / "default" / "090000_300") 20 + ], 21 + ) 22 + 23 + 24 + def test_run_migration_moves_all_paths(tmp_path, monkeypatch): 25 + journal = tmp_path / "journal" 26 + day = journal / "20260417" 27 + segment = day / "default" / "090000_300" 28 + 29 + (journal / "agents").mkdir(parents=True) 30 + (journal / "agents" / "root.jsonl").write_text("{}\n", encoding="utf-8") 31 + (journal / "health").mkdir(parents=True) 32 + (journal / "health" / "agents.json").write_text("{}", encoding="utf-8") 33 + (day / "agents").mkdir(parents=True) 34 + (day / "agents" / "flow.md").write_text("# flow\n", encoding="utf-8") 35 + (segment / "agents").mkdir(parents=True) 36 + (segment / "agents" / "screen.md").write_text("# screen\n", encoding="utf-8") 37 + 38 + _patch_journal(monkeypatch, journal) 39 + 40 + summary, collisions = mod.run_migration(journal, dry_run=False) 41 + 42 + assert collisions == [] 43 + assert summary.discovered == 4 44 + assert summary.moved == 4 45 + assert summary.skipped == 0 46 + assert summary.errors == 0 47 + assert summary.collisions == 0 48 + assert (journal / "talents" / "root.jsonl").exists() 49 + assert (journal / "health" / "talents.json").exists() 50 + assert (day / "talents" / "flow.md").exists() 51 + assert (segment / "talents" / "screen.md").exists() 52 + 53 + 54 + def test_run_migration_aborts_on_collision(tmp_path, monkeypatch): 55 + journal = tmp_path / "journal" 56 + day = journal / "20260417" 57 + 58 + (journal / "agents").mkdir(parents=True) 59 + (journal / "agents" / "root.jsonl").write_text("{}\n", encoding="utf-8") 60 + (journal / "talents").mkdir(parents=True) 61 + (day / "agents").mkdir(parents=True) 62 + (day / "agents" / "flow.md").write_text("# flow\n", encoding="utf-8") 63 + 64 + _patch_journal(monkeypatch, journal) 65 + 66 + summary, collisions = mod.run_migration(journal, dry_run=False) 67 + 68 + assert summary.collisions == 1 69 + assert summary.moved == 0 70 + assert len(collisions) == 1 71 + assert (journal / "agents" / "root.jsonl").exists() 72 + assert not (day / "talents").exists() 73 + 74 + 75 + def test_run_migration_reports_already_migrated(tmp_path, monkeypatch): 76 + journal = tmp_path / "journal" 77 + day = journal / "20260417" 78 + segment = day / "default" / "090000_300" 79 + 80 + (journal / "talents").mkdir(parents=True) 81 + (journal / "talents" / "root.jsonl").write_text("{}\n", encoding="utf-8") 82 + (journal / "health").mkdir(parents=True) 83 + (journal / "health" / "talents.json").write_text("{}", encoding="utf-8") 84 + (day / "talents").mkdir(parents=True) 85 + (day / "talents" / "flow.md").write_text("# flow\n", encoding="utf-8") 86 + (segment / "talents").mkdir(parents=True) 87 + (segment / "talents" / "screen.md").write_text("# screen\n", encoding="utf-8") 88 + 89 + _patch_journal(monkeypatch, journal) 90 + 91 + summary, collisions = mod.run_migration(journal, dry_run=False) 92 + 93 + assert collisions == [] 94 + assert summary.discovered == 0 95 + assert summary.moved == 0 96 + assert summary.skipped == 4 97 + assert summary.errors == 0 98 + assert summary.collisions == 0
+8 -8
tests/test_output_hooks.py
··· 18 18 19 19 import talent.occurrence as occurrence 20 20 from tests.conftest import copytree_tracked 21 - from think.agents import _apply_template_vars 22 21 from think.hooks import write_events_jsonl 23 22 from think.talent import load_post_hook, load_pre_hook 23 + from think.talents import _apply_template_vars 24 24 from think.utils import day_path 25 25 26 26 FIXTURES = Path("tests/fixtures") ··· 160 160 161 161 def test_output_hook_invocation(tmp_path, monkeypatch): 162 162 """Test that agents.py invokes hook and uses transformed result.""" 163 - mod = importlib.import_module("think.agents") 163 + mod = importlib.import_module("think.talents") 164 164 copy_day(tmp_path) 165 165 166 166 # Use tmp_path as talent directory to avoid polluting real talent/ ··· 215 215 216 216 def test_output_hook_returns_none(tmp_path, monkeypatch): 217 217 """Test that hook returning None uses original result.""" 218 - mod = importlib.import_module("think.agents") 218 + mod = importlib.import_module("think.talents") 219 219 copy_day(tmp_path) 220 220 221 221 import think.talent ··· 261 261 262 262 def test_output_hook_error_fallback(tmp_path, monkeypatch): 263 263 """Test that hook errors fall back to original result.""" 264 - mod = importlib.import_module("think.agents") 264 + mod = importlib.import_module("think.talents") 265 265 copy_day(tmp_path) 266 266 267 267 import think.talent ··· 658 658 659 659 def test_pre_hook_invocation(tmp_path, monkeypatch): 660 660 """Test that agents.py invokes pre-hook and uses modified inputs.""" 661 - mod = importlib.import_module("think.agents") 661 + mod = importlib.import_module("think.talents") 662 662 copy_day(tmp_path) 663 663 664 664 import think.talent ··· 824 824 825 825 def test_pre_hook_template_vars_integration(tmp_path, monkeypatch): 826 826 """Test pre-hook template_vars reach the model as substituted text.""" 827 - mod = importlib.import_module("think.agents") 827 + mod = importlib.import_module("think.talents") 828 828 copy_day(tmp_path) 829 829 830 830 import think.talent ··· 875 875 876 876 def test_pre_hook_template_vars_with_field_mods(tmp_path, monkeypatch): 877 877 """Test pre-hook can return field mods and template_vars together.""" 878 - mod = importlib.import_module("think.agents") 878 + mod = importlib.import_module("think.talents") 879 879 copy_day(tmp_path) 880 880 881 881 import think.talent ··· 929 929 930 930 def test_both_pre_and_post_hooks(tmp_path, monkeypatch): 931 931 """Test that both pre and post hooks can be configured together.""" 932 - mod = importlib.import_module("think.agents") 932 + mod = importlib.import_module("think.talents") 933 933 copy_day(tmp_path) 934 934 935 935 import think.talent
+9 -7
tests/test_output_path.py
··· 29 29 30 30 def test_daily_output_md(self): 31 31 path = get_output_path("/journal/20250101", "activity", output_format="md") 32 - assert path == Path("/journal/20250101/agents/activity.md") 32 + assert path == Path("/journal/20250101/talents/activity.md") 33 33 34 34 def test_daily_output_json(self): 35 35 path = get_output_path("/journal/20250101", "facets", output_format="json") 36 - assert path == Path("/journal/20250101/agents/facets.json") 36 + assert path == Path("/journal/20250101/talents/facets.json") 37 37 38 38 def test_segment_output(self): 39 39 path = get_output_path( 40 40 "/journal/20250101", "activity", segment="120000_300", output_format="md" 41 41 ) 42 - assert path == Path("/journal/20250101/120000_300/agents/activity.md") 42 + assert path == Path("/journal/20250101/120000_300/talents/activity.md") 43 43 44 44 def test_app_key_output(self): 45 45 path = get_output_path( 46 46 "/journal/20250101", "entities:observer", output_format="md" 47 47 ) 48 - assert path == Path("/journal/20250101/agents/_entities_observer.md") 48 + assert path == Path("/journal/20250101/talents/_entities_observer.md") 49 49 50 50 def test_facet_daily_output(self): 51 51 """Multi-facet agent output uses a facet subdirectory.""" 52 52 path = get_output_path( 53 53 "/journal/20250101", "newsletter", output_format="md", facet="work" 54 54 ) 55 - assert path == Path("/journal/20250101/agents/work/newsletter.md") 55 + assert path == Path("/journal/20250101/talents/work/newsletter.md") 56 56 57 57 def test_facet_segment_output(self): 58 58 """Multi-facet segment output uses a facet subdirectory.""" ··· 63 63 output_format="json", 64 64 facet="personal", 65 65 ) 66 - assert path == Path("/journal/20250101/120000_300/agents/personal/summary.json") 66 + assert path == Path( 67 + "/journal/20250101/120000_300/talents/personal/summary.json" 68 + ) 67 69 68 70 def test_facet_with_app_key(self): 69 71 """App-qualified key with facet uses both prefixes.""" 70 72 path = get_output_path( 71 73 "/journal/20250101", "entities:observer", output_format="md", facet="work" 72 74 ) 73 - assert path == Path("/journal/20250101/agents/work/_entities_observer.md") 75 + assert path == Path("/journal/20250101/talents/work/_entities_observer.md") 74 76 75 77 def test_facet_none_same_as_omitted(self): 76 78 """Explicit facet=None produces same path as omitting facet."""
+75 -39
tests/test_pipeline_health.py
··· 34 34 35 35 assert summary["status"] == "healthy" 36 36 assert summary["anomalies"] == [] 37 - assert summary["agents"] == { 37 + assert summary["talents"] == { 38 38 "dispatched": 0, 39 39 "completed": 0, 40 40 "failed": 0, ··· 45 45 assert summary["activities"] == { 46 46 "detected": 0, 47 47 "persisted": 0, 48 - "agents_fired": False, 48 + "talents_fired": False, 49 49 } 50 50 assert all( 51 51 run == {"count": 0, "duration_ms_total": 0} for run in summary["runs"].values() ··· 69 69 base / "1_segment_dream.jsonl", 70 70 [ 71 71 {"event": "run.start", "mode": "segment"}, 72 - {"event": "agent.dispatch", "mode": "segment"}, 73 - {"event": "agent.complete", "mode": "segment"}, 72 + {"event": "talent.dispatch", "mode": "segment"}, 73 + {"event": "talent.complete", "mode": "segment"}, 74 74 {"event": "run.complete", "mode": "segment", "duration_ms": 10}, 75 75 ], 76 76 ) ··· 78 78 base / "2_daily_dream.jsonl", 79 79 [ 80 80 {"event": "run.start", "mode": "daily"}, 81 - {"event": "agent.dispatch", "mode": "daily"}, 82 - {"event": "agent.complete", "mode": "daily"}, 81 + {"event": "talent.dispatch", "mode": "daily"}, 82 + {"event": "talent.complete", "mode": "daily"}, 83 83 {"event": "run.complete", "mode": "daily", "duration_ms": 20}, 84 84 ], 85 85 ) ··· 87 87 base / "3_activity_dream.jsonl", 88 88 [ 89 89 {"event": "run.start", "mode": "activity"}, 90 - {"event": "agent.dispatch", "mode": "activity"}, 91 - {"event": "agent.complete", "mode": "activity"}, 90 + {"event": "talent.dispatch", "mode": "activity"}, 91 + {"event": "talent.complete", "mode": "activity"}, 92 92 {"event": "run.complete", "mode": "activity", "duration_ms": 30}, 93 93 ], 94 94 ) ··· 96 96 summary = summarize_pipeline_day(day) 97 97 98 98 assert summary["status"] == "healthy" 99 - assert summary["agents"]["dispatched"] == 3 100 - assert summary["agents"]["completed"] == 3 99 + assert summary["talents"]["dispatched"] == 3 100 + assert summary["talents"]["completed"] == 3 101 101 assert summary["runs"]["segment"] == {"count": 1, "duration_ms_total": 10} 102 102 assert summary["runs"]["daily"] == {"count": 1, "duration_ms_total": 20} 103 103 assert summary["runs"]["activity"] == {"count": 1, "duration_ms_total": 30} 104 - assert summary["activities"]["agents_fired"] is True 104 + assert summary["activities"]["talents_fired"] is True 105 105 106 106 107 107 def test_agent_failure_promotes_warning(pipeline_journal): ··· 110 110 pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", 111 111 [ 112 112 { 113 - "event": "agent.fail", 113 + "event": "talent.fail", 114 114 "mode": "segment", 115 115 "name": "screen", 116 - "agent_id": "a-1", 116 + "use_id": "a-1", 117 117 "state": "timeout", 118 118 } 119 119 ], ··· 122 122 summary = summarize_pipeline_day(day) 123 123 124 124 assert summary["status"] == "warning" 125 - assert summary["agents"]["failed"] == 1 126 - assert summary["agents"]["failed_list"] == [ 127 - {"mode": "segment", "name": "screen", "agent_id": "a-1", "state": "timeout"} 125 + assert summary["talents"]["failed"] == 1 126 + assert summary["talents"]["failed_list"] == [ 127 + {"mode": "segment", "name": "screen", "use_id": "a-1", "state": "timeout"} 128 128 ] 129 129 assert summary["anomalies"] == [ 130 130 { 131 - "kind": "agent_failure", 131 + "kind": "talent_failure", 132 132 "mode": "segment", 133 133 "name": "screen", 134 - "agent_id": "a-1", 134 + "use_id": "a-1", 135 135 "state": "timeout", 136 136 } 137 137 ] 138 138 139 139 140 + @pytest.mark.parametrize("event_name", ["agent.fail", "talent.fail"]) 141 + def test_failure_reader_accepts_legacy_and_new_names(pipeline_journal, event_name): 142 + day = "20990107" 143 + _write_jsonl( 144 + pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", 145 + [{"event": event_name, "mode": "segment", "name": "screen", "use_id": "u-1"}], 146 + ) 147 + 148 + summary = summarize_pipeline_day(day) 149 + 150 + assert summary["talents"]["failed"] == 1 151 + 152 + 153 + @pytest.mark.parametrize( 154 + ("event_name", "field"), 155 + [ 156 + ("agent.dispatch", "dispatched"), 157 + ("talent.dispatch", "dispatched"), 158 + ("agent.complete", "completed"), 159 + ("talent.complete", "completed"), 160 + ("agent.skip", "skipped"), 161 + ("talent.skip", "skipped"), 162 + ], 163 + ) 164 + def test_reader_accepts_legacy_and_new_event_names(pipeline_journal, event_name, field): 165 + day = "20990108" 166 + _write_jsonl( 167 + pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", 168 + [{"event": event_name, "mode": "segment"}], 169 + ) 170 + 171 + summary = summarize_pipeline_day(day) 172 + 173 + assert summary["talents"][field] == 1 174 + 175 + 140 176 def test_failed_list_truncates_at_20(pipeline_journal): 141 177 day = "20990103" 142 178 events = [ 143 179 { 144 - "event": "agent.fail", 180 + "event": "talent.fail", 145 181 "mode": "daily", 146 182 "name": f"agent-{idx}", 147 - "agent_id": f"id-{idx}", 183 + "use_id": f"id-{idx}", 148 184 "state": "error", 149 185 } 150 186 for idx in range(25) ··· 155 191 156 192 summary = summarize_pipeline_day(day) 157 193 158 - assert summary["agents"]["failed"] == 25 159 - assert len(summary["agents"]["failed_list"]) == 20 160 - assert summary["agents"]["failed_list_truncated"] is True 161 - assert sum(1 for a in summary["anomalies"] if a["kind"] == "agent_failure") == 20 194 + assert summary["talents"]["failed"] == 25 195 + assert len(summary["talents"]["failed_list"]) == 20 196 + assert summary["talents"]["failed_list_truncated"] is True 197 + assert sum(1 for a in summary["anomalies"] if a["kind"] == "talent_failure") == 20 162 198 163 199 164 200 def test_activity_detected_without_run_is_stale(pipeline_journal): ··· 235 271 236 272 assert summary["status"] == "healthy" 237 273 assert summary["anomalies"] == [] 238 - assert summary["agents"] == { 274 + assert summary["talents"] == { 239 275 "dispatched": 0, 240 276 "completed": 0, 241 277 "failed": 0, ··· 252 288 path.write_text( 253 289 json.dumps({"event": "run.start", "mode": "segment"}) 254 290 + "\nnot json at all\n" 255 - + json.dumps({"event": "agent.dispatch", "mode": "segment"}) 291 + + json.dumps({"event": "talent.dispatch", "mode": "segment"}) 256 292 + "\n", 257 293 encoding="utf-8", 258 294 ) ··· 260 296 summary = summarize_pipeline_day(day) 261 297 262 298 assert summary["runs"]["segment"]["count"] == 1 263 - assert summary["agents"]["dispatched"] == 1 299 + assert summary["talents"]["dispatched"] == 1 264 300 265 301 266 302 @pytest.mark.parametrize( ··· 270 306 { 271 307 "status": "healthy", 272 308 "anomalies": [], 273 - "agents": {"failed": 0}, 309 + "talents": {"failed": 0}, 274 310 "day": "20260101", 275 311 }, 276 312 None, ··· 281 317 "anomalies": [ 282 318 {"kind": "activity_agents_missing"}, 283 319 {"kind": "daily_agents_missing"}, 284 - {"kind": "agent_failure"}, 320 + {"kind": "talent_failure"}, 285 321 ], 286 - "agents": {"failed": 3}, 322 + "talents": {"failed": 3}, 287 323 "day": "20260101", 288 324 }, 289 325 { ··· 296 332 "status": "stale", 297 333 "anomalies": [ 298 334 {"kind": "daily_agents_missing"}, 299 - {"kind": "agent_failure"}, 335 + {"kind": "talent_failure"}, 300 336 ], 301 - "agents": {"failed": 2}, 337 + "talents": {"failed": 2}, 302 338 "day": "20260102", 303 339 }, 304 340 { ··· 309 345 ( 310 346 { 311 347 "status": "warning", 312 - "anomalies": [{"kind": "agent_failure"}], 313 - "agents": {"failed": 1}, 348 + "anomalies": [{"kind": "talent_failure"}], 349 + "talents": {"failed": 1}, 314 350 "day": "20260101", 315 351 }, 316 - {"status": "warning", "message": "1 agent error today"}, 352 + {"status": "warning", "message": "1 talent error today"}, 317 353 ), 318 354 ( 319 355 { 320 356 "status": "warning", 321 - "anomalies": [{"kind": "agent_failure"}] * 3, 322 - "agents": {"failed": 3}, 357 + "anomalies": [{"kind": "talent_failure"}] * 3, 358 + "talents": {"failed": 3}, 323 359 "day": "20260101", 324 360 }, 325 - {"status": "warning", "message": "3 agent errors today"}, 361 + {"status": "warning", "message": "3 talent errors today"}, 326 362 ), 327 363 ( 328 364 { 329 365 "status": "healthy", 330 366 "anomalies": [{"kind": "segment_runs_missing"}], 331 - "agents": {"failed": 0}, 367 + "talents": {"failed": 0}, 332 368 "day": "20260101", 333 369 }, 334 370 None,
+7 -7
tests/test_pipeline_smoke.py
··· 125 125 ) 126 126 monkeypatch.setattr( 127 127 dream, 128 - "wait_for_agents", 128 + "wait_for_uses", 129 129 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 130 130 ) 131 131 monkeypatch.setattr(dream, "_callosum", None) ··· 142 142 143 143 for segment_key, sense_dict in SEGMENTS: 144 144 seg_dir = journal / "chronicle" / DAY / STREAM / segment_key 145 - (seg_dir / "agents").mkdir(parents=True, exist_ok=True) 146 - (seg_dir / "agents" / "sense.json").write_text(json.dumps(sense_dict)) 145 + (seg_dir / "talents").mkdir(parents=True, exist_ok=True) 146 + (seg_dir / "talents" / "sense.json").write_text(json.dumps(sense_dict)) 147 147 148 148 dream.run_segment_sense( 149 149 day=DAY, ··· 161 161 "091500_300", 162 162 "100000_300", 163 163 ]: 164 - seg_agents = journal / "chronicle" / DAY / STREAM / seg_key / "agents" 164 + seg_agents = journal / "chronicle" / DAY / STREAM / seg_key / "talents" 165 165 assert (seg_agents / "sense.json").exists() 166 166 assert (seg_agents / "activity.md").exists() 167 167 assert (seg_agents / "density.json").exists() ··· 179 179 / DAY 180 180 / STREAM 181 181 / seg_key 182 - / "agents" 182 + / "talents" 183 183 / "speakers.json" 184 184 ).read_text() 185 185 ) ··· 192 192 / DAY 193 193 / STREAM 194 194 / seg_key 195 - / "agents" 195 + / "talents" 196 196 / "speakers.json" 197 197 ).exists() 198 198 ··· 203 203 / DAY 204 204 / STREAM 205 205 / "092000_300" 206 - / "agents" 206 + / "talents" 207 207 / "density.json" 208 208 ).read_text() 209 209 )
+6 -6
tests/test_retention.py
··· 107 107 """Create a segment directory with specified contents.""" 108 108 seg = tmp_path / "segment" 109 109 seg.mkdir(exist_ok=True) 110 - agents_dir = seg / "agents" 110 + agents_dir = seg / "talents" 111 111 agents_dir.mkdir(exist_ok=True) 112 112 113 113 if audio: ··· 167 167 def test_complete_with_stub_speaker_labels(self, tmp_path): 168 168 """Stub speaker_labels.json (skipped=True, labels=[]) unblocks retention.""" 169 169 seg = _make_segment(tmp_path, audio=True, embeddings=True, speaker_labels=False) 170 - stub = seg / "agents" / "speaker_labels.json" 170 + stub = seg / "talents" / "speaker_labels.json" 171 171 stub.write_text( 172 172 json.dumps({"labels": [], "skipped": True, "reason": "no_owner_centroid"}) 173 173 ) ··· 286 286 (day1 / "audio.flac").write_bytes(b"x" * 1000) 287 287 (day1 / "audio.jsonl").write_text('{"raw":"audio.flac"}\n') 288 288 (day1 / "stream.json").write_text('{"stream":"default"}') 289 - (day1 / "agents").mkdir() 289 + (day1 / "talents").mkdir() 290 290 291 291 day1b = journal / "chronicle" / "20260115" / "plaud" / "103000_300" 292 292 day1b.mkdir(parents=True) 293 293 (day1b / "audio.m4a").write_bytes(b"x" * 500) 294 294 (day1b / "audio.jsonl").write_text('{"raw":"audio.m4a"}\n') 295 295 (day1b / "stream.json").write_text('{"stream":"plaud"}') 296 - (day1b / "agents").mkdir() 296 + (day1b / "talents").mkdir() 297 297 298 298 # Day 2: recent — one complete segment (must stay within 30d window) 299 299 day2 = journal / "chronicle" / "20260401" / "default" / "120000_300" ··· 301 301 (day2 / "audio.flac").write_bytes(b"x" * 800) 302 302 (day2 / "audio.jsonl").write_text('{"raw":"audio.flac"}\n') 303 303 (day2 / "stream.json").write_text('{"stream":"default"}') 304 - (day2 / "agents").mkdir() 304 + (day2 / "talents").mkdir() 305 305 306 306 # Day 3: incomplete segment (no audio.jsonl) 307 307 day3 = journal / "chronicle" / "20260101" / "default" / "140000_300" ··· 449 449 segment = journal / "chronicle" / "20260115" / "default" / "100000_300" 450 450 audio_jsonl = segment / "audio.jsonl" 451 451 alternate_audio_jsonl = segment / "meeting_audio.jsonl" 452 - speaker_labels = segment / "agents" / "speaker_labels.json" 452 + speaker_labels = segment / "talents" / "speaker_labels.json" 453 453 454 454 alternate_audio_jsonl.write_text('{"raw":"audio.flac"}\n') 455 455 speaker_labels.write_text("{}")
+11 -11
tests/test_routines.py
··· 201 201 "think.routines.cortex_request", return_value="fake_agent_id" 202 202 ) as mock_req, 203 203 patch( 204 - "think.routines.wait_for_agents", 204 + "think.routines.wait_for_uses", 205 205 return_value=({"fake_agent_id": "finish"}, []), 206 206 ), 207 207 patch("think.routines.callosum_send", return_value=True), ··· 237 237 "think.routines.cortex_request", return_value="fake_agent_id" 238 238 ) as mock_req, 239 239 patch( 240 - "think.routines.wait_for_agents", 240 + "think.routines.wait_for_uses", 241 241 return_value=({"fake_agent_id": "finish"}, []), 242 242 ), 243 243 patch("think.routines.callosum_send", return_value=True), ··· 273 273 "think.routines.cortex_request", return_value="fake_agent_id" 274 274 ) as mock_req, 275 275 patch( 276 - "think.routines.wait_for_agents", 276 + "think.routines.wait_for_uses", 277 277 return_value=({"fake_agent_id": "finish"}, []), 278 278 ), 279 279 patch("think.routines.callosum_send", return_value=True), ··· 309 309 "think.routines.cortex_request", return_value="fake_agent_id" 310 310 ) as mock_req, 311 311 patch( 312 - "think.routines.wait_for_agents", 312 + "think.routines.wait_for_uses", 313 313 return_value=({"fake_agent_id": "finish"}, []), 314 314 ), 315 315 patch("think.routines.callosum_send", return_value=True), ··· 508 508 "think.routines.cortex_request", return_value="fake_agent_id" 509 509 ) as mock_req, 510 510 patch( 511 - "think.routines.wait_for_agents", 511 + "think.routines.wait_for_uses", 512 512 return_value=({"fake_agent_id": "finish"}, []), 513 513 ), 514 514 patch("think.routines.callosum_send", return_value=True), ··· 530 530 "think.routines.cortex_request", return_value="fake_agent_id" 531 531 ) as mock_req, 532 532 patch( 533 - "think.routines.wait_for_agents", 533 + "think.routines.wait_for_uses", 534 534 return_value=({"fake_agent_id": "finish"}, []), 535 535 ), 536 536 patch("think.routines.callosum_send", return_value=True), ··· 552 552 "think.routines.cortex_request", return_value="fake_agent_id" 553 553 ) as mock_req, 554 554 patch( 555 - "think.routines.wait_for_agents", 555 + "think.routines.wait_for_uses", 556 556 return_value=({"fake_agent_id": "finish"}, []), 557 557 ), 558 558 patch("think.routines.callosum_send", return_value=True), ··· 574 574 "think.routines.cortex_request", return_value="fake_agent_id" 575 575 ) as mock_req, 576 576 patch( 577 - "think.routines.wait_for_agents", 577 + "think.routines.wait_for_uses", 578 578 return_value=({"fake_agent_id": "finish"}, []), 579 579 ), 580 580 patch("think.routines.callosum_send", return_value=True), ··· 786 786 with ( 787 787 patch("think.routines.cortex_request", return_value="fake_agent_id"), 788 788 patch( 789 - "think.routines.wait_for_agents", 789 + "think.routines.wait_for_uses", 790 790 return_value=({"fake_agent_id": "finish"}, []), 791 791 ), 792 792 patch("think.routines.callosum_send", return_value=True), ··· 825 825 with ( 826 826 patch("think.routines.cortex_request", return_value="fake_agent_id"), 827 827 patch( 828 - "think.routines.wait_for_agents", 828 + "think.routines.wait_for_uses", 829 829 return_value=({"fake_agent_id": "finish"}, []), 830 830 ), 831 831 patch("think.routines.callosum_send", return_value=True), ··· 1576 1576 "think.routines.cortex_request", return_value="fake_agent_id" 1577 1577 ) as mock_req, 1578 1578 patch( 1579 - "think.routines.wait_for_agents", 1579 + "think.routines.wait_for_uses", 1580 1580 return_value=({"fake_agent_id": "finish"}, []), 1581 1581 ), 1582 1582 patch("think.routines.callosum_send", return_value=True),
+12 -12
tests/test_segment.py
··· 20 20 stream_json=None, 21 21 audio=True, 22 22 screen=True, 23 - agents=None, 23 + talents=None, 24 24 ): 25 25 """Create a minimal segment fixture directory.""" 26 26 seg_dir = base / "chronicle" / day / stream / segment ··· 31 31 (seg_dir / "audio.jsonl").write_text('{"t":0}\n') 32 32 if screen: 33 33 (seg_dir / "screen.jsonl").write_text('{"t":0}\n') 34 - if agents: 35 - agents_dir = seg_dir / "agents" 36 - agents_dir.mkdir() 37 - for name in agents: 38 - (agents_dir / name).write_text("# agent output\n") 34 + if talents: 35 + talents_dir = seg_dir / "talents" 36 + talents_dir.mkdir() 37 + for name in talents: 38 + (talents_dir / name).write_text("# talent output\n") 39 39 return seg_dir 40 40 41 41 ··· 52 52 "prev_segment": None, 53 53 "seq": 1, 54 54 }, 55 - agents=["audio.md"], 55 + talents=["audio.md"], 56 56 ) 57 57 _make_segment( 58 58 tmp_path, ··· 65 65 "prev_segment": "090000_300", 66 66 "seq": 2, 67 67 }, 68 - agents=["audio.md", "screen.md"], 68 + talents=["audio.md", "screen.md"], 69 69 ) 70 70 71 71 args = argparse.Namespace( ··· 131 131 "prev_segment": None, 132 132 "seq": 1, 133 133 }, 134 - agents=["audio.md"], 134 + talents=["audio.md"], 135 135 ) 136 136 137 137 args = argparse.Namespace( ··· 143 143 assert isinstance(data, list) 144 144 assert data[0]["stream"] == "default" 145 145 assert data[0]["segment"] == "090000_300" 146 - assert data[0]["agents"] == 1 146 + assert data[0]["talents"] == 1 147 147 148 148 149 149 def test_list_empty_day(tmp_path, monkeypatch, capsys): ··· 171 171 "prev_segment": None, 172 172 "seq": 1, 173 173 }, 174 - agents=["audio.md"], 174 + talents=["audio.md"], 175 175 ) 176 176 177 177 args = argparse.Namespace( ··· 226 226 "prev_segment": None, 227 227 "seq": 1, 228 228 }, 229 - agents=["audio.md"], 229 + talents=["audio.md"], 230 230 ) 231 231 232 232 args = argparse.Namespace(
+10 -10
tests/test_sense_splitter.py
··· 40 40 41 41 write_sense_outputs(sense_json, seg_dir) 42 42 43 - agents_dir = seg_dir / "agents" 43 + agents_dir = seg_dir / "talents" 44 44 assert (agents_dir / "activity.md").exists() 45 45 assert (agents_dir / "facets.json").exists() 46 46 assert (agents_dir / "density.json").exists() ··· 77 77 78 78 write_sense_outputs(sense_json, seg_dir) 79 79 80 - stored = json.loads((seg_dir / "agents" / "sense.json").read_text("utf-8")) 80 + stored = json.loads((seg_dir / "talents" / "sense.json").read_text("utf-8")) 81 81 assert stored["foo"] == "bar" 82 82 assert stored == sense_json 83 83 ··· 94 94 95 95 write_sense_outputs(sense_json, seg_dir) 96 96 97 - speakers_path = seg_dir / "agents" / "speakers.json" 97 + speakers_path = seg_dir / "talents" / "speakers.json" 98 98 assert speakers_path.exists() 99 99 assert json.loads(speakers_path.read_text(encoding="utf-8")) == ["Alice", "Bob"] 100 100 ··· 105 105 106 106 write_sense_outputs(_make_sense_output(meeting_detected=False), seg_dir) 107 107 108 - assert not (seg_dir / "agents" / "speakers.json").exists() 108 + assert not (seg_dir / "talents" / "speakers.json").exists() 109 109 110 110 def test_meeting_with_no_speakers_writes_empty_array(self, tmp_path): 111 111 from think.sense_splitter import write_sense_outputs ··· 115 115 116 116 write_sense_outputs(sense_json, seg_dir) 117 117 118 - speakers_path = seg_dir / "agents" / "speakers.json" 118 + speakers_path = seg_dir / "talents" / "speakers.json" 119 119 assert speakers_path.exists() 120 120 assert json.loads(speakers_path.read_text(encoding="utf-8")) == [] 121 121 ··· 128 128 129 129 write_sense_outputs({}, seg_dir) 130 130 131 - agents_dir = seg_dir / "agents" 131 + agents_dir = seg_dir / "talents" 132 132 assert (agents_dir / "activity.md").exists() 133 133 assert (agents_dir / "facets.json").exists() 134 134 assert (agents_dir / "density.json").exists() ··· 156 156 157 157 write_sense_outputs(sense_json, seg_dir) 158 158 159 - agents_dir = seg_dir / "agents" 159 + agents_dir = seg_dir / "talents" 160 160 assert (agents_dir / "activity.md").read_text(encoding="utf-8") == "" 161 161 assert ( 162 162 json.loads((agents_dir / "facets.json").read_text(encoding="utf-8")) == [] ··· 172 172 173 173 write_sense_outputs(_make_sense_output(activity_summary=""), seg_dir) 174 174 175 - assert (seg_dir / "agents" / "activity.md").read_text(encoding="utf-8") == "" 175 + assert (seg_dir / "talents" / "activity.md").read_text(encoding="utf-8") == "" 176 176 177 177 178 178 class TestMultipleFacets: ··· 187 187 188 188 write_sense_outputs(_make_sense_output(facets=facets), seg_dir) 189 189 190 - assert json.loads((seg_dir / "agents" / "facets.json").read_text("utf-8")) == ( 190 + assert json.loads((seg_dir / "talents" / "facets.json").read_text("utf-8")) == ( 191 191 facets 192 192 ) 193 193 ··· 200 200 201 201 write_idle_stubs(seg_dir) 202 202 203 - agents_dir = seg_dir / "agents" 203 + agents_dir = seg_dir / "talents" 204 204 assert (agents_dir / "density.json").exists() 205 205 density = json.loads((agents_dir / "density.json").read_text(encoding="utf-8")) 206 206 assert density["classification"] == "idle"
+1 -1
tests/test_sol.py
··· 281 281 282 282 def test_critical_commands_registered(self): 283 283 """Test that critical commands are registered.""" 284 - critical = ["import", "agents", "dream", "indexer", "transcribe"] 284 + critical = ["import", "providers", "dream", "indexer", "transcribe"] 285 285 for cmd in critical: 286 286 assert cmd in sol.COMMANDS, f"Critical command '{cmd}' not registered"
+5 -5
tests/test_speaker_attribution_hook.py
··· 36 36 tmp_path, 37 37 {"error": "no_owner_centroid"}, 38 38 ) 39 - stub_path = tmp_path / "agents" / "speaker_labels.json" 39 + stub_path = tmp_path / "talents" / "speaker_labels.json" 40 40 assert stub_path.exists() 41 41 data = json.loads(stub_path.read_text()) 42 42 assert data == {"labels": [], "skipped": True, "reason": "no_owner_centroid"} ··· 49 49 tmp_path, 50 50 {"error": "no_owner_centroid"}, 51 51 ) 52 - stub_path = tmp_path / "agents" / "speaker_labels.json" 52 + stub_path = tmp_path / "talents" / "speaker_labels.json" 53 53 assert not stub_path.exists() 54 54 assert result == {"skip_reason": "no_owner_centroid"} 55 55 ··· 61 61 tmp_path, 62 62 {"labels": []}, 63 63 ) 64 - stub_path = tmp_path / "agents" / "speaker_labels.json" 64 + stub_path = tmp_path / "talents" / "speaker_labels.json" 65 65 assert stub_path.exists() 66 66 data = json.loads(stub_path.read_text()) 67 67 assert data == {"labels": [], "skipped": True, "reason": "no_embeddings"} ··· 74 74 tmp_path, 75 75 {"labels": []}, 76 76 ) 77 - stub_path = tmp_path / "agents" / "speaker_labels.json" 77 + stub_path = tmp_path / "talents" / "speaker_labels.json" 78 78 assert not stub_path.exists() 79 79 assert result == {"skip_reason": "no_embeddings"} 80 80 ··· 85 85 from talent.speaker_attribution import pre_process 86 86 87 87 result = pre_process({"stream": "default"}) 88 - stub_path = tmp_path / "agents" / "speaker_labels.json" 88 + stub_path = tmp_path / "talents" / "speaker_labels.json" 89 89 assert not stub_path.exists() 90 90 assert result == {"skip_reason": "no_segment_context"} 91 91
+6 -6
tests/test_stats_contract.py
··· 20 20 ("tokens.by_model", "tokens.by_model"), 21 21 ("tokens.by_day", "tokens.by_day"), 22 22 ("facets.counts_by_day", "facets.counts_by_day"), 23 - ("agents.counts_by_day", "agents.counts_by_day"), 23 + ("talents.counts_by_day", "talents.counts_by_day"), 24 24 ("days.*.transcript_duration", "transcript_duration"), 25 25 ("days.*.percept_duration", "percept_duration"), 26 26 ("tokens.by_day.*.*.input_tokens", "input_tokens"), ··· 61 61 seg2 = day / "default" / "134500_300" 62 62 seg1.mkdir(parents=True) 63 63 seg2.mkdir(parents=True) 64 - (day / "agents").mkdir(parents=True) 64 + (day / "talents").mkdir(parents=True) 65 65 66 66 audio_lines = [ 67 67 {"raw": "raw.flac"}, ··· 82 82 ) 83 83 84 84 (seg2 / "audio.flac").write_bytes(b"fLaC") 85 - (day / "agents" / "flow.md").write_text("") 85 + (day / "talents" / "flow.md").write_text("") 86 86 87 87 events_dir = journal / "facets" / "work" / "events" 88 88 events_dir.mkdir(parents=True) ··· 98 98 "facet": "work", 99 99 "agent": "meetings", 100 100 "occurred": True, 101 - "source": "20240101/agents/meetings.md", 101 + "source": "20240101/talents/meetings.md", 102 102 } 103 103 (events_dir / "20240101.jsonl").write_text(json.dumps(event) + "\n") 104 104 ··· 186 186 "totals": {}, 187 187 "heatmap": [], 188 188 "tokens": {}, 189 - "agents": {}, 189 + "talents": {}, 190 190 "facets": {}, 191 191 } 192 192 del output["totals"] ··· 209 209 "totals": {}, 210 210 "heatmap": [], 211 211 "tokens": {}, 212 - "agents": {}, 212 + "talents": {}, 213 213 "facets": {}, 214 214 } 215 215 )
+12 -12
tests/test_talent.py
··· 7 7 8 8 from think.talent import ( 9 9 _validate_cwd, 10 - get_agent, 11 - get_agent_filter, 10 + get_talent, 11 + get_talent_filter, 12 12 source_is_enabled, 13 13 source_is_required, 14 14 ) ··· 52 52 53 53 54 54 def test_get_agent_filter_bool(): 55 - """Test get_agent_filter with bool values.""" 56 - assert get_agent_filter(True) is None 57 - assert get_agent_filter(False) == {} 55 + """Test get_talent_filter with bool values.""" 56 + assert get_talent_filter(True) is None 57 + assert get_talent_filter(False) == {} 58 58 59 59 60 60 def test_get_agent_filter_required_string(): 61 - """Test get_agent_filter with 'required' string.""" 62 - assert get_agent_filter("required") is None 61 + """Test get_talent_filter with 'required' string.""" 62 + assert get_talent_filter("required") is None 63 63 64 64 65 65 def test_get_agent_filter_dict(): 66 - """Test get_agent_filter with dict values.""" 66 + """Test get_talent_filter with dict values.""" 67 67 filter_dict = {"entities": True, "meetings": "required", "flow": False} 68 - assert get_agent_filter(filter_dict) == filter_dict 69 - assert get_agent_filter({}) == {} 68 + assert get_talent_filter(filter_dict) == filter_dict 69 + assert get_talent_filter({}) == {} 70 70 71 71 72 72 def test_validate_cwd_defaults_cogitate_to_journal(): ··· 98 98 99 99 100 100 def test_get_agent_normalizes_cwd_for_cogitate(): 101 - config = get_agent("chat") 101 + config = get_talent("chat") 102 102 assert config["cwd"] == "journal" 103 103 104 104 105 105 def test_get_agent_preserves_repo_cwd_for_coder(): 106 - config = get_agent("coder") 106 + config = get_talent("coder") 107 107 assert config["cwd"] == "repo"
+3 -3
tests/test_talent_cli.py
··· 359 359 output = capsys.readouterr().out 360 360 lines = [line for line in output.strip().splitlines() if line.strip()] 361 361 362 - # Find the line for agent_id 1700000000001 (has JSONL file) 362 + # Find the line for use_id 1700000000001 (has JSONL file) 363 363 enriched_line = None 364 364 for line in lines: 365 365 if "1700000000001" in line: ··· 488 488 """Parse run stats extracts correct counts from fixture JSONL.""" 489 489 from pathlib import Path 490 490 491 - jsonl = Path("tests/fixtures/journal/agents/default/1700000000001.jsonl") 491 + jsonl = Path("tests/fixtures/journal/talents/default/1700000000001.jsonl") 492 492 stats = _parse_run_stats(jsonl) 493 493 assert stats["event_count"] == 6 # all except request 494 494 assert stats["tool_count"] == 1 # one tool_start ··· 502 502 """Parse run stats handles error run JSONL correctly.""" 503 503 from pathlib import Path 504 504 505 - jsonl = Path("tests/fixtures/journal/agents/flow/1700000000002.jsonl") 505 + jsonl = Path("tests/fixtures/journal/talents/flow/1700000000002.jsonl") 506 506 stats = _parse_run_stats(jsonl) 507 507 assert stats["event_count"] == 2 # start + error (not request) 508 508 assert stats["tool_count"] == 0
+2 -2
tests/verify_api.py
··· 46 46 # apps/sol/routes.py 47 47 { 48 48 "app": "sol", 49 - "name": "agents-day", 50 - "path": "/app/sol/api/agents/20260304", 49 + "name": "talents-day", 50 + "path": "/app/sol/api/talents/20260304", 51 51 "params": {"facet": "work"}, 52 52 "status": 200, 53 53 },
+1 -1
think/activities.py
··· 639 639 if not seg_dir: 640 640 return None 641 641 642 - state_path = seg_dir / "agents" / facet / "activity_state.json" 642 + state_path = seg_dir / "talents" / facet / "activity_state.json" 643 643 if not state_path.exists(): 644 644 return None 645 645
+63 -382
think/agents.py think/talents.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Unified agent CLI for solstone. 4 + """Unified talent execution module for solstone. 5 5 6 - Spawned by cortex for all agent types: 7 - - Tool-using agents (with configured tools) 6 + Spawned by cortex for all talent types: 7 + - Tool-using talents (with configured tools) 8 8 - Generators (transcript analysis, no tools) 9 9 10 10 Both paths share unified config preparation and execution flow. ··· 18 18 import json 19 19 import logging 20 20 import os 21 - import shutil 22 21 import sys 23 - import time 24 22 import traceback 25 - from datetime import datetime, timezone 23 + from datetime import datetime 26 24 from pathlib import Path 27 25 from string import Template 28 26 from typing import Any, Callable, Optional ··· 30 28 from think.cluster import cluster, cluster_period, cluster_span 31 29 from think.providers.shared import Event 32 30 from think.talent import ( 33 - get_agent_filter, 34 31 get_output_path, 35 32 get_talent_configs, 33 + get_talent_filter, 36 34 load_post_hook, 37 35 load_pre_hook, 38 36 load_prompt, ··· 52 50 setup_cli, 53 51 ) 54 52 55 - LOG = logging.getLogger("think.agents") 53 + TALENT_EXECUTION_MODULE = "think.talents" 54 + 55 + LOG = logging.getLogger("think.talents") 56 56 57 57 # Minimum content length for transcript-based generation 58 58 MIN_INPUT_CHARS = 50 ··· 105 105 def _stream_content_description(stream: str | None) -> str: 106 106 """Return a human-readable content description for a stream. 107 107 108 - Used in preamble templates so agents know what kind of content they're 108 + Used in preamble templates so talents know what kind of content they're 109 109 analyzing (live capture vs imported conversations, notes, etc.). 110 110 """ 111 111 if not stream: ··· 218 218 day: Day in YYYYMMDD format 219 219 segment: Segment key (HHMMSS_LEN) 220 220 span: List of segment keys 221 - activity: Optional activity record dict for activity-scheduled agents 221 + activity: Optional activity record dict for activity-scheduled talents 222 222 223 223 Returns: 224 224 Dict with template variables: ··· 392 392 elif span: 393 393 os.environ["SOL_SEGMENT"] = span[0] 394 394 395 - # Convert sources config for clustering 395 + # Convert sources config for clustering. 396 + # Frontmatter now uses ``load.talents`` but cluster still consumes the 397 + # normalized ``agents`` source key internally. 396 398 cluster_sources: dict = {} 397 399 for k, v in sources.items(): 398 - if k == "agents": 399 - agent_filter = get_agent_filter(v) 400 - if agent_filter is None: 401 - cluster_sources[k] = source_is_enabled(v) 402 - elif not agent_filter: 403 - cluster_sources[k] = False 400 + if k == "talents": 401 + talent_filter = get_talent_filter(v) 402 + if talent_filter is None: 403 + cluster_sources["agents"] = source_is_enabled(v) 404 + elif not talent_filter: 405 + cluster_sources["agents"] = False 404 406 else: 405 - cluster_sources[k] = agent_filter 407 + cluster_sources["agents"] = talent_filter 406 408 else: 407 409 cluster_sources[k] = source_is_enabled(v) 408 410 ··· 417 419 418 420 419 421 def prepare_config(request: dict) -> dict: 420 - """Prepare complete agent config from request. 422 + """Prepare a complete talent config from a request. 421 423 422 - Single unified preparation path for all agent types. Takes raw request 424 + Single unified preparation path for all talent types. Takes raw request 423 425 from cortex and returns fully prepared config ready for execution. 424 426 425 427 Config fields produced: 426 - - name: Agent name 428 + - name: Talent name 427 429 - provider, model: Resolved from context/request 428 - - user_instruction: Agent instruction from .md file 430 + - user_instruction: Talent instruction from .md file 429 431 - prompt: User's runtime query/request 430 432 - transcript: Clustered transcript (if day provided) 431 433 - output_path: Where to write output (if output format set) ··· 438 440 Fully prepared config dict 439 441 """ 440 442 from think.models import resolve_model_for_provider, resolve_provider 441 - from think.talent import get_agent, key_to_context 443 + from think.talent import get_talent, key_to_context 442 444 443 445 name = request.get("name", "unified") 444 446 facet = request.get("facet") ··· 450 452 output_path_override = request.get("output_path") 451 453 user_prompt = request.get("prompt", "") 452 454 453 - # Load complete agent config 454 - config = get_agent(name, facet=facet, analysis_day=day) 455 + # Load complete talent config 456 + config = get_talent(name, facet=facet, analysis_day=day) 455 457 456 458 # Config now contains all frontmatter fields plus: 457 459 # - path: Path to the .md file ··· 459 461 # - All frontmatter: tools, hook, disabled, thinking_budget, max_output_tokens, etc. 460 462 461 463 # Convert path string to Path object for convenience 462 - agent_path = Path(config["path"]) if config.get("path") else None 464 + talent_path = Path(config["path"]) if config.get("path") else None 463 465 sources = config.get("sources", {}) 464 466 talent_cwd = config.get("cwd") 465 467 466 - # Merge request values (request overrides agent defaults) 468 + # Merge request values (request overrides talent defaults) 467 469 config.update({k: v for k, v in request.items() if v is not None}) 468 470 request_cwd = request.get("cwd") 469 471 if request_cwd is not None and request_cwd != talent_cwd: ··· 501 503 502 504 # Resolve provider and model from context 503 505 context = key_to_context(name) 504 - agent_type = config["type"] 505 - default_provider, default_model = resolve_provider(context, agent_type) 506 + talent_type = config["type"] 507 + default_provider, default_model = resolve_provider(context, talent_type) 506 508 507 509 provider = config.get("provider") or default_provider 508 510 model = config.get("model") 509 511 if not model: 510 512 if provider != default_provider: 511 - model = resolve_model_for_provider(context, provider, agent_type) 513 + model = resolve_model_for_provider(context, provider, talent_type) 512 514 else: 513 515 model = default_model 514 516 ··· 529 531 config["health_stale"] = should_recheck_health(health_data) 530 532 531 533 if not is_provider_healthy(provider, health_data): 532 - backup = get_backup_provider(agent_type) 534 + backup = get_backup_provider(talent_type) 533 535 if backup and backup != provider: 534 536 env_key = PROVIDER_METADATA.get(backup, {}).get("env_key") 535 537 if not env_key or os.getenv(env_key): 536 538 config["fallback_from"] = provider 537 539 config["provider"] = backup 538 540 config["model"] = resolve_model_for_provider( 539 - context, backup, agent_type 541 + context, backup, talent_type 540 542 ) 541 543 542 544 # Check if disabled ··· 546 548 547 549 # Day-based processing: load transcript and apply template substitution 548 550 if day: 549 - # Load transcript (only when agent has enabled sources to consume) 551 + # Load transcript (only when the talent has enabled sources to consume) 550 552 if any(source_is_enabled(v) for v in sources.values()): 551 553 transcript, source_counts = _load_transcript(day, segment, span, sources) 552 554 config["transcript"] = transcript ··· 571 573 "Scale analysis to available input.\n\n" + transcript 572 574 ) 573 575 574 - # Reload agent instruction with template substitution for day/segment context 575 - if agent_path and agent_path.exists(): 576 + # Reload talent instruction with template substitution for day/segment context 577 + if talent_path and talent_path.exists(): 576 578 from think.prompts import _resolve_facets 577 579 578 580 prompt_context = _build_prompt_context( ··· 585 587 if activity_ctx: 586 588 prompt_context["activity_context"] = activity_ctx 587 589 588 - agent_prompt_obj = load_prompt( 589 - agent_path.stem, base_dir=agent_path.parent, context=prompt_context 590 + talent_prompt_obj = load_prompt( 591 + talent_path.stem, base_dir=talent_path.parent, context=prompt_context 590 592 ) 591 - config["user_instruction"] = agent_prompt_obj.text 593 + config["user_instruction"] = talent_prompt_obj.text 592 594 593 595 # Set prompt (user's runtime query) 594 - # For tool agents: prompt is the user's question 596 + # For tool talents: prompt is the user's question 595 597 # For generators: prompt is typically empty (instruction is in user_instruction) 596 598 config["prompt"] = user_prompt 597 599 ··· 628 630 has_user_instruction = bool(config.get("user_instruction")) 629 631 has_day = bool(config.get("day")) 630 632 631 - # Cogitate agents need a prompt (user's question) 633 + # Cogitate talents need a prompt (user's question) 632 634 if is_cogitate and not has_prompt: 633 - return "Missing 'prompt' field for cogitate agent" 635 + return "Missing 'prompt' field for cogitate talent" 634 636 635 637 # Generate prompts need either day (transcript) or user_instruction 636 638 if not is_cogitate and not has_day and not has_user_instruction and not has_prompt: ··· 716 718 717 719 718 720 # ============================================================================= 719 - # Unified Agent Execution 721 + # Unified Talent Execution 720 722 # ============================================================================= 721 723 722 724 ··· 730 732 731 733 def _build_dry_run_event(config: dict, before_values: dict) -> dict: 732 734 """Build a dry-run event with all context.""" 733 - agent_type = config["type"] 735 + talent_type = config["type"] 734 736 735 737 event: dict[str, Any] = { 736 738 "event": "dry_run", 737 739 "ts": now_ms(), 738 - "type": agent_type, 740 + "type": talent_type, 739 741 "name": config.get("name", "unified"), 740 742 "provider": config.get("provider", ""), 741 743 "model": config.get("model") or "unknown", ··· 798 800 config: dict, 799 801 emit_event: Callable[[dict], None], 800 802 ) -> None: 801 - """Execute tool-using agent via provider's run_cogitate. 803 + """Execute a tool-using talent via the provider's run_cogitate. 802 804 803 805 Args: 804 806 config: Prepared config dict ··· 816 818 provider_mod = get_provider_module(provider) 817 819 818 820 # Wrapper to intercept finish event for post-processing 819 - def agent_emit_event(data: Event) -> None: 821 + def talent_emit_event(data: Event) -> None: 820 822 if data.get("event") == "finish": 821 823 result = data.get("result", "") 822 824 result = _run_post_hooks(result, config) ··· 832 834 emit_event(data) 833 835 834 836 try: 835 - await provider_mod.run_cogitate(config=config, on_event=agent_emit_event) 837 + await provider_mod.run_cogitate(config=config, on_event=talent_emit_event) 836 838 except Exception as exc: 837 839 if not _is_retryable_error(exc) or config.get("fallback_from"): 838 840 raise ··· 878 880 def backup_emit(data: Event) -> None: 879 881 if data.get("event") == "error": 880 882 return 881 - agent_emit_event(data) 883 + talent_emit_event(data) 882 884 883 885 try: 884 886 await backup_mod.run_cogitate(config=config, on_event=backup_emit) ··· 1030 1032 emit_event(finish_event) 1031 1033 1032 1034 1033 - async def _run_agent( 1035 + async def _run_talent( 1034 1036 config: dict, 1035 1037 emit_event: Callable[[dict], None], 1036 1038 dry_run: bool = False, 1037 1039 ) -> None: 1038 - """Execute agent based on config. 1040 + """Execute a talent based on config. 1039 1041 1040 - Unified execution path for all agent types. Handles: 1042 + Unified execution path for all talent types. Handles: 1041 1043 - Skip conditions (disabled, no input, etc.) 1042 1044 - Output existence checking (skip if exists unless refresh) 1043 1045 - Pre/post hooks ··· 1096 1098 } 1097 1099 ) 1098 1100 if config.get("day"): 1099 - day_log(config["day"], f"agent {name} skipped ({skip_reason})") 1101 + day_log(config["day"], f"talent {name} skipped ({skip_reason})") 1100 1102 return 1101 1103 1102 - # Check if output already exists (applies to both tool agents and generators) 1104 + # Check if output already exists (applies to both tool talents and generators) 1103 1105 if output_path and not refresh and not dry_run: 1104 1106 if output_path.exists() and output_path.stat().st_size > 0: 1105 1107 LOG.info("Output exists, loading: %s", output_path) ··· 1145 1147 } 1146 1148 ) 1147 1149 if config.get("day"): 1148 - day_log(config["day"], f"agent {name} skipped ({skip_reason})") 1150 + day_log(config["day"], f"talent {name} skipped ({skip_reason})") 1149 1151 return 1150 1152 1151 1153 # Dry-run mode ··· 1153 1155 emit_event(_build_dry_run_event(config, before_values)) 1154 1156 return 1155 1157 1156 - # Execute based on agent type 1158 + # Execute based on talent type 1157 1159 if is_cogitate: 1158 1160 await _execute_with_tools(config, emit_event) 1159 1161 else: ··· 1161 1163 1162 1164 # Log completion 1163 1165 if config.get("day"): 1164 - day_log(config["day"], f"agent {name} ok") 1166 + day_log(config["day"], f"talent {name} ok") 1165 1167 1166 1168 1167 1169 # ============================================================================= ··· 1185 1187 output_format = meta.get("output") 1186 1188 output_file = get_output_path(day_dir, key, output_format=output_format) 1187 1189 if output_file.exists(): 1188 - processed.append(os.path.join("agents", output_file.name)) 1190 + processed.append(os.path.join("talents", output_file.name)) 1189 1191 else: 1190 - pending.append(os.path.join("agents", output_file.name)) 1192 + pending.append(os.path.join("talents", output_file.name)) 1191 1193 return {"processed": sorted(processed), "repairable": sorted(pending)} 1192 1194 1193 1195 1194 - def _check_generate(provider_name: str, tier: int, timeout: int) -> tuple[str, str]: 1195 - """Check generate interface for a provider.""" 1196 - from think.models import PROVIDER_DEFAULTS 1197 - from think.providers import PROVIDER_METADATA, get_provider_module 1198 - 1199 - env_key = PROVIDER_METADATA[provider_name]["env_key"] 1200 - if env_key and not os.getenv(env_key): 1201 - label = PROVIDER_METADATA[provider_name]["label"] 1202 - # Google Vertex AI can work without GOOGLE_API_KEY, but this health check 1203 - # treats missing env as "not configured" for the standard API path. 1204 - return "skip", f"{label} not configured (no {env_key})" 1205 - 1206 - # For keyless providers (e.g., Ollama), check reachability instead 1207 - if not env_key: 1208 - from think.providers import validate_key 1209 - 1210 - result = validate_key(provider_name, "") 1211 - if not result.get("valid"): 1212 - return ( 1213 - "skip", 1214 - f"Ollama not reachable ({result.get('error', 'unreachable')})", 1215 - ) 1216 - 1217 - try: 1218 - module = get_provider_module(provider_name) 1219 - model = PROVIDER_DEFAULTS[provider_name][tier] 1220 - result = module.run_generate( 1221 - contents="Say OK", 1222 - model=model, 1223 - temperature=0, 1224 - max_output_tokens=16, 1225 - system_instruction=None, 1226 - json_output=False, 1227 - thinking_budget=None, 1228 - timeout_s=timeout, 1229 - ) 1230 - text = result.get("text", "") if isinstance(result, dict) else "" 1231 - if text: 1232 - usage = result.get("usage") if isinstance(result, dict) else None 1233 - if usage: 1234 - from think.models import log_token_usage 1235 - 1236 - log_token_usage( 1237 - model=PROVIDER_DEFAULTS[provider_name][tier], 1238 - usage=usage, 1239 - context="health.check.generate", 1240 - type="generate", 1241 - ) 1242 - return "ok", "OK" 1243 - return "fail", "FAIL: empty response text" 1244 - except Exception as exc: 1245 - return "fail", f"FAIL: {exc}" 1246 - 1247 - 1248 - async def _check_cogitate( 1249 - provider_name: str, tier: int, timeout: int 1250 - ) -> tuple[str, str]: 1251 - """Check cogitate interface for a provider by running a real prompt.""" 1252 - from think.models import PROVIDER_DEFAULTS 1253 - from think.providers import PROVIDER_METADATA, get_provider_module 1254 - 1255 - # Pre-flight: check provider is configured 1256 - env_key = PROVIDER_METADATA[provider_name]["env_key"] 1257 - if env_key and not os.getenv(env_key): 1258 - label = PROVIDER_METADATA[provider_name]["label"] 1259 - return "skip", f"{label} not configured (no {env_key})" 1260 - 1261 - # For keyless providers (e.g., Ollama), check reachability 1262 - if not env_key: 1263 - from think.providers import validate_key 1264 - 1265 - result = validate_key(provider_name, "") 1266 - if not result.get("valid"): 1267 - return ( 1268 - "skip", 1269 - f"Ollama not reachable ({result.get('error', 'unreachable')})", 1270 - ) 1271 - 1272 - # Pre-flight: check cogitate CLI binary is installed 1273 - binary = PROVIDER_METADATA[provider_name].get("cogitate_cli", "") 1274 - if binary and not shutil.which(binary): 1275 - return "skip", f"{binary} CLI not installed" 1276 - 1277 - try: 1278 - module = get_provider_module(provider_name) 1279 - model = PROVIDER_DEFAULTS[provider_name][tier] 1280 - config = {"prompt": "Say OK", "model": model} 1281 - result = await asyncio.wait_for( 1282 - module.run_cogitate(config=config, on_event=None), 1283 - timeout=timeout, 1284 - ) 1285 - if result: 1286 - return "ok", "OK" 1287 - return "fail", "FAIL: empty response" 1288 - except asyncio.TimeoutError: 1289 - return "fail", f"FAIL: timed out after {timeout}s" 1290 - except Exception as exc: 1291 - return "fail", f"FAIL: {exc}" 1292 - 1293 - 1294 - async def _run_check(args: argparse.Namespace) -> None: 1295 - """Run connectivity checks against AI providers.""" 1296 - from think.models import PROVIDER_DEFAULTS, TIER_FLASH, TIER_LITE, TIER_PRO 1297 - from think.providers import PROVIDER_REGISTRY 1298 - 1299 - # --targeted: only check configured provider+tier pairs 1300 - targeted_pairs = None 1301 - if args.targeted and not args.provider and not args.tier: 1302 - import fcntl 1303 - 1304 - from think.models import TYPE_DEFAULTS, get_backup_provider 1305 - from think.utils import get_config 1306 - 1307 - targeted_pairs = set() 1308 - config = get_config() 1309 - providers_config = config.get("providers", {}) 1310 - for agent_type, defaults in TYPE_DEFAULTS.items(): 1311 - type_config = providers_config.get(agent_type, {}) 1312 - provider = type_config.get("provider", defaults["provider"]) 1313 - tier = type_config.get("tier", defaults["tier"]) 1314 - targeted_pairs.add((provider, tier)) 1315 - backup = get_backup_provider(agent_type) 1316 - if backup: 1317 - targeted_pairs.add((backup, tier)) 1318 - 1319 - # flock dedup: only one targeted check runs at a time 1320 - lock_dir = Path(get_journal()) / "health" 1321 - lock_dir.mkdir(parents=True, exist_ok=True) 1322 - lock_fd = open(lock_dir / "recheck.lock", "w") 1323 - try: 1324 - fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 1325 - except OSError: 1326 - lock_fd.close() 1327 - return 1328 - 1329 - if args.provider: 1330 - providers = args.provider 1331 - for name in providers: 1332 - if name not in PROVIDER_REGISTRY: 1333 - available = ", ".join(PROVIDER_REGISTRY.keys()) 1334 - print( 1335 - f"Unknown provider: {name}. Available providers: {available}", 1336 - file=sys.stderr, 1337 - ) 1338 - sys.exit(1) 1339 - else: 1340 - providers = list(PROVIDER_REGISTRY.keys()) 1341 - 1342 - interfaces = [args.interface] if args.interface else ["generate", "cogitate"] 1343 - 1344 - tier_names = {1: "pro", 2: "flash", 3: "lite"} 1345 - tiers = [args.tier] if args.tier else [TIER_PRO, TIER_FLASH, TIER_LITE] 1346 - 1347 - # Pre-compute column widths 1348 - provider_width = max(len(n) for n in providers) if providers else 0 1349 - tier_width = max(len(tier_names[t]) for t in tiers) 1350 - # Resolve all model names to get max width 1351 - model_names = set() 1352 - for p in providers: 1353 - for t in tiers: 1354 - model_names.add(PROVIDER_DEFAULTS[p][t]) 1355 - model_width = max(len(m) for m in model_names) if model_names else 0 1356 - interface_width = max(len(n) for n in interfaces) if interfaces else 0 1357 - 1358 - total = 0 1359 - passed = 0 1360 - failed = 0 1361 - skipped = 0 1362 - results = [] 1363 - cache = {} # (provider, model, interface) -> (status, message, source_tier) 1364 - 1365 - for provider_name in providers: 1366 - for tier in tiers: 1367 - if ( 1368 - targeted_pairs is not None 1369 - and (provider_name, tier) not in targeted_pairs 1370 - ): 1371 - continue 1372 - model = PROVIDER_DEFAULTS[provider_name][tier] 1373 - for interface_name in interfaces: 1374 - cache_key = (provider_name, model, interface_name) 1375 - if cache_key in cache: 1376 - status, message, source_tier = cache[cache_key] 1377 - elapsed_s = 0.0 1378 - elapsed_s_rounded = 0.0 1379 - reused_from = source_tier 1380 - else: 1381 - start = time.perf_counter() 1382 - if interface_name == "generate": 1383 - status, message = _check_generate( 1384 - provider_name, tier, args.timeout 1385 - ) 1386 - else: 1387 - status, message = await _check_cogitate( 1388 - provider_name, tier, args.timeout 1389 - ) 1390 - elapsed_s = time.perf_counter() - start 1391 - elapsed_s_rounded = round(elapsed_s, 1) 1392 - cache[cache_key] = (status, message, tier_names[tier]) 1393 - reused_from = None 1394 - 1395 - result = { 1396 - "provider": provider_name, 1397 - "tier": tier_names[tier], 1398 - "model": model, 1399 - "interface": interface_name, 1400 - "ok": status != "fail", 1401 - "status": status, 1402 - "message": str(message), 1403 - "elapsed_s": elapsed_s_rounded, 1404 - } 1405 - if reused_from: 1406 - result["reused_from"] = reused_from 1407 - results.append(result) 1408 - 1409 - if not args.json: 1410 - if reused_from: 1411 - mark = "=" 1412 - display_message = f"{message} (={reused_from})" 1413 - else: 1414 - if status == "ok": 1415 - mark = "✓" 1416 - elif status == "skip": 1417 - mark = "-" 1418 - else: 1419 - mark = "✗" 1420 - display_message = str(message) 1421 - print( 1422 - f"{mark} " 1423 - f"{provider_name:<{provider_width}} " 1424 - f"{tier_names[tier]:<{tier_width}} " 1425 - f"{model:<{model_width}} " 1426 - f"{interface_name:<{interface_width}} " 1427 - f"{display_message} ({elapsed_s:.1f}s)" 1428 - ) 1429 - 1430 - total += 1 1431 - if status == "ok": 1432 - passed += 1 1433 - elif status == "skip": 1434 - skipped += 1 1435 - else: 1436 - failed += 1 1437 - 1438 - any_failed = any(r["status"] == "fail" for r in results) 1439 - 1440 - # Write results to health file 1441 - payload = { 1442 - "results": results, 1443 - "summary": { 1444 - "total": total, 1445 - "passed": passed, 1446 - "skipped": skipped, 1447 - "failed": failed, 1448 - }, 1449 - "checked_at": datetime.now(timezone.utc).isoformat(), 1450 - } 1451 - health_dir = Path(get_journal()) / "health" 1452 - health_dir.mkdir(parents=True, exist_ok=True) 1453 - (health_dir / "agents.json").write_text(json.dumps(payload, indent=2)) 1454 - 1455 - if args.json: 1456 - print( 1457 - json.dumps( 1458 - { 1459 - "results": results, 1460 - "summary": { 1461 - "total": total, 1462 - "passed": passed, 1463 - "skipped": skipped, 1464 - "failed": failed, 1465 - }, 1466 - }, 1467 - indent=2, 1468 - ) 1469 - ) 1470 - else: 1471 - print(f"{total} checks: {passed} passed, {skipped} skipped, {failed} failed") 1472 - sys.exit(1 if any_failed else 0) 1473 - 1474 - 1475 1196 # ============================================================================= 1476 1197 # Main Entry Point 1477 1198 # ============================================================================= 1478 1199 1479 1200 1480 1201 async def main_async() -> None: 1481 - """NDJSON-based CLI for agents.""" 1482 - from think.providers import PROVIDER_REGISTRY 1202 + """NDJSON-based CLI for talents.""" 1483 1203 1484 1204 parser = argparse.ArgumentParser( 1485 - description="solstone Agent CLI - Accepts NDJSON input via stdin" 1205 + description="solstone Talent CLI - Accepts NDJSON input via stdin" 1486 1206 ) 1487 1207 parser.add_argument( 1488 1208 "--dry-run", 1489 1209 action="store_true", 1490 1210 help="Show what would be sent to the provider without calling the LLM", 1491 1211 ) 1492 - subparsers = parser.add_subparsers(dest="subcommand") 1493 - check_parser = subparsers.add_parser("check", help="Check AI provider connectivity") 1494 - check_parser.add_argument( 1495 - "--provider", 1496 - action="append", 1497 - help=f"Provider to check (repeatable). Available: {', '.join(PROVIDER_REGISTRY.keys())}", 1498 - ) 1499 - check_parser.add_argument( 1500 - "--interface", 1501 - choices=["generate", "cogitate"], 1502 - default=None, 1503 - help="Interface to check (default: both)", 1504 - ) 1505 - check_parser.add_argument( 1506 - "--timeout", 1507 - type=int, 1508 - default=30, 1509 - help="Timeout in seconds for generate checks (default: 30)", 1510 - ) 1511 - check_parser.add_argument( 1512 - "--tier", 1513 - type=int, 1514 - choices=[1, 2, 3], 1515 - default=None, 1516 - help="Tier to check (1=pro, 2=flash, 3=lite; default: all)", 1517 - ) 1518 - check_parser.add_argument( 1519 - "--json", action="store_true", help="Output results as JSON" 1520 - ) 1521 - check_parser.add_argument( 1522 - "--targeted", 1523 - action="store_true", 1524 - help="Only check configured provider+tier pairs (used by automated rechecks)", 1525 - ) 1526 - 1527 1212 args = setup_cli(parser) 1528 1213 require_solstone() 1529 - if args.subcommand == "check": 1530 - await _run_check(args) 1531 - return 1532 - 1533 1214 dry_run = args.dry_run 1534 1215 1535 1216 app_logger = setup_logging(args.verbose) ··· 1556 1237 emit_event({"event": "error", "error": error, "ts": now_ms()}) 1557 1238 continue 1558 1239 1559 - await _run_agent(config, emit_event, dry_run=dry_run) 1240 + await _run_talent(config, emit_event, dry_run=dry_run) 1560 1241 1561 1242 except json.JSONDecodeError as e: 1562 1243 emit_event(
+5 -5
think/chat_cli.py
··· 10 10 import threading 11 11 12 12 from think.callosum import CallosumConnection 13 - from think.cortex_client import cortex_request, read_agent_events 13 + from think.cortex_client import cortex_request, read_use_events 14 14 from think.utils import require_solstone, setup_cli 15 15 16 16 ··· 43 43 if args.facet: 44 44 config["facet"] = args.facet 45 45 46 - agent_id = cortex_request( 46 + use_id = cortex_request( 47 47 prompt=message, 48 48 name=args.talent, 49 49 provider=args.provider, 50 50 config=config if config else None, 51 51 ) 52 - if agent_id is None: 52 + if use_id is None: 53 53 print( 54 54 "Error: failed to connect to cortex (is the stack running?)", 55 55 file=sys.stderr, ··· 63 63 def on_event(msg: dict) -> None: 64 64 if msg.get("tract") != "cortex": 65 65 return 66 - if msg.get("agent_id") != agent_id: 66 + if msg.get("use_id") != use_id: 67 67 return 68 68 69 69 event_type = msg.get("event") ··· 122 122 sys.exit(1) 123 123 124 124 try: 125 - events = read_agent_events(agent_id) 125 + events = read_use_events(use_id) 126 126 for event in reversed(events): 127 127 event_type = event.get("event") 128 128 if event_type == "finish":
+5 -5
think/cluster.py
··· 217 217 agent_filter = ( 218 218 None if agents is True else agents if isinstance(agents, dict) else None 219 219 ) 220 - agents_dir = segment_path / "agents" 221 - if agents_dir.is_dir(): 222 - for md_file in sorted(agents_dir.rglob("*.md")): 220 + talents_dir = segment_path / "talents" 221 + if talents_dir.is_dir(): 222 + for md_file in sorted(talents_dir.rglob("*.md")): 223 223 if not md_file.is_file(): 224 224 continue 225 225 ··· 230 230 try: 231 231 content = md_file.read_text() 232 232 if content.strip(): 233 - rel_md_path = md_file.relative_to(agents_dir).as_posix() 233 + rel_md_path = md_file.relative_to(talents_dir).as_posix() 234 234 entries.append( 235 235 { 236 236 "timestamp": segment_start, ··· 240 240 "prefix": "agent_output", 241 241 "output_name": md_file.stem, 242 242 "content": content, 243 - "name": f"{segment_path.name}/agents/{rel_md_path}", 243 + "name": f"{segment_path.name}/talents/{rel_md_path}", 244 244 "stream": stream, 245 245 } 246 246 )
+5 -5
think/conversation.py
··· 57 57 user_message: str = "", 58 58 agent_response: str = "", 59 59 talent: str = "", 60 - agent_id: str = "", 60 + use_id: str = "", 61 61 ) -> None: 62 62 """Record a conversation exchange to journal storage. 63 63 64 64 Writes to two locations: 65 65 1. conversation/exchanges.jsonl — append-only quick-read index 66 - 2. YYYYMMDD/conversation/HHMMSS_1/agents/conversation.md — journal entry 67 - for FTS5 search indexing (matches */*/*/agents/*.md formatter pattern) 66 + 2. YYYYMMDD/conversation/HHMMSS_1/talents/conversation.md — journal entry 67 + for FTS5 search indexing (matches */*/*/talents/*.md formatter pattern) 68 68 69 69 Also runs lightweight entity extraction on the conversation text. 70 70 """ ··· 84 84 "user_message": user_message, 85 85 "agent_response": agent_response, 86 86 "talent": talent, 87 - "agent_id": agent_id, 87 + "use_id": use_id, 88 88 } 89 89 90 90 # 1. Append to exchanges.jsonl (fast-read index) ··· 102 102 time_key = dt.strftime("%H%M%S") 103 103 segment = f"{time_key}_1" 104 104 105 - seg_dir = day_path(day) / CONVERSATION_STREAM / segment / "agents" 105 + seg_dir = day_path(day) / CONVERSATION_STREAM / segment / "talents" 106 106 seg_dir.mkdir(parents=True, exist_ok=True) 107 107 108 108 time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
+157 -156
think/cortex.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Callosum-based agent process manager for solstone. 4 + """Callosum-based talent process manager for solstone. 5 5 6 - Cortex listens for agent requests via the Callosum message bus and manages 7 - agent process lifecycle: 6 + Cortex listens for talent requests via the Callosum message bus and manages 7 + talent process lifecycle: 8 8 - Receives requests via Callosum (tract="cortex", event="request") 9 - - Creates <agent>/<timestamp>_active.jsonl files to track active agents 10 - - Spawns agent processes and captures their stdout events 11 - - Broadcasts all agent events back to Callosum 12 - - Renames to <agent>/<timestamp>.jsonl when complete 9 + - Creates <talent>/<timestamp>_active.jsonl files to track active uses 10 + - Spawns talent processes and captures their stdout events 11 + - Broadcasts all talent events back to Callosum 12 + - Renames to <talent>/<timestamp>.jsonl when complete 13 13 14 - Agent files provide persistence and historical record, while Callosum provides 14 + Talent files provide persistence and historical record, while Callosum provides 15 15 real-time event distribution to all interested services. 16 16 """ 17 17 ··· 29 29 30 30 from think.callosum import CallosumConnection 31 31 from think.runner import _atomic_symlink 32 + from think.talents import TALENT_EXECUTION_MODULE 32 33 from think.utils import get_journal, get_project_root, get_rev, now_ms 33 34 34 35 35 - class AgentProcess: 36 - """Manages a running agent subprocess.""" 36 + class TalentProcess: 37 + """Manages a running talent subprocess.""" 37 38 38 - def __init__(self, agent_id: str, process: subprocess.Popen, log_path: Path): 39 - self.agent_id = agent_id 39 + def __init__(self, use_id: str, process: subprocess.Popen, log_path: Path): 40 + self.use_id = use_id 40 41 self.process = process 41 42 self.log_path = log_path 42 43 self.stop_event = threading.Event() ··· 62 63 self.process.wait(timeout=10) # Give more time for graceful shutdown 63 64 except subprocess.TimeoutExpired: 64 65 logging.getLogger(__name__).warning( 65 - f"Agent {self.agent_id} didn't stop gracefully, killing" 66 + f"Talent {self.use_id} didn't stop gracefully, killing" 66 67 ) 67 68 self.process.kill() 68 69 self.process.wait() # Ensure zombie is reaped 69 70 70 71 71 72 class CortexService: 72 - """Callosum-based agent process manager.""" 73 + """Callosum-based talent process manager.""" 73 74 74 75 def __init__(self, journal_path: Optional[str] = None): 75 76 self.journal_path = Path(journal_path or get_journal()) 76 - self.agents_dir = self.journal_path / "agents" 77 - self.agents_dir.mkdir(parents=True, exist_ok=True) 77 + self.talents_dir = self.journal_path / "talents" 78 + self.talents_dir.mkdir(parents=True, exist_ok=True) 78 79 79 80 self.logger = logging.getLogger(__name__) 80 - self.running_agents: Dict[str, AgentProcess] = {} 81 - self.agent_requests: Dict[str, Dict[str, Any]] = {} # Store agent requests 81 + self.running_uses: Dict[str, TalentProcess] = {} 82 + self.use_requests: Dict[str, Dict[str, Any]] = {} # Store use requests 82 83 self.lock = threading.RLock() 83 84 self.stop_event = threading.Event() 84 85 self.shutdown_requested = threading.Event() ··· 88 89 89 90 def _create_error_event( 90 91 self, 91 - agent_id: str, 92 + use_id: str, 92 93 error: str, 93 94 trace: Optional[str] = None, 94 95 exit_code: Optional[int] = None, ··· 97 98 event = { 98 99 "event": "error", 99 100 "ts": now_ms(), 100 - "agent_id": agent_id, 101 + "use_id": use_id, 101 102 "error": error, 102 103 } 103 104 if trace: ··· 106 107 event["exit_code"] = exit_code 107 108 return event 108 109 109 - def _recover_orphaned_agents(self, active_files: list) -> None: 110 - """Recover orphaned active agent files from a previous crash. 110 + def _recover_orphaned_uses(self, active_files: list) -> None: 111 + """Recover orphaned active talent files from a previous crash. 111 112 112 113 Appends an error event to each file and renames to completed. 113 114 """ 114 115 for file_path in active_files: 115 - agent_id = file_path.stem.replace("_active", "") 116 + use_id = file_path.stem.replace("_active", "") 116 117 try: 117 118 error_event = self._create_error_event( 118 - agent_id, "Recovered: Cortex restarted while agent was running" 119 + use_id, "Recovered: Cortex restarted while talent was running" 119 120 ) 120 121 with open(file_path, "a") as f: 121 122 f.write(json.dumps(error_event) + "\n") 122 123 123 - completed_path = file_path.parent / f"{agent_id}.jsonl" 124 + completed_path = file_path.parent / f"{use_id}.jsonl" 124 125 file_path.rename(completed_path) 125 - self.logger.warning(f"Recovered orphaned agent: {agent_id}") 126 + self.logger.warning(f"Recovered orphaned talent: {use_id}") 126 127 except Exception as e: 127 - self.logger.error(f"Failed to recover agent {agent_id}: {e}") 128 + self.logger.error(f"Failed to recover talent {use_id}: {e}") 128 129 129 130 def start(self) -> None: 130 - """Start listening for agent requests via Callosum.""" 131 + """Start listening for talent requests via Callosum.""" 131 132 # Recover any orphaned active files from previous crash 132 - active_files = list(self.agents_dir.glob("*/*_active.jsonl")) 133 + active_files = list(self.talents_dir.glob("*/*_active.jsonl")) 133 134 if active_files: 134 135 self.logger.warning( 135 - f"Found {len(active_files)} orphaned agent(s), recovering..." 136 + f"Found {len(active_files)} orphaned talent use(s), recovering..." 136 137 ) 137 - self._recover_orphaned_agents(active_files) 138 + self._recover_orphaned_uses(active_files) 138 139 139 140 # Connect to Callosum to receive requests 140 141 try: 141 142 self.callosum.start(callback=self._handle_callosum_message) 142 143 self.logger.info("Connected to Callosum message bus") 143 - self.callosum.emit("supervisor", "request", cmd=["sol", "agents", "check"]) 144 - self.logger.info("Requested agents health check via supervisor") 144 + self.callosum.emit( 145 + "supervisor", "request", cmd=["sol", "providers", "check"] 146 + ) 147 + self.logger.info("Requested providers health check via supervisor") 145 148 except Exception as e: 146 149 self.logger.error(f"Failed to connect to Callosum: {e}") 147 150 sys.exit(1) ··· 153 156 daemon=True, 154 157 ).start() 155 158 156 - self.logger.info("Cortex service started, listening for agent requests") 159 + self.logger.info("Cortex service started, listening for talent requests") 157 160 158 161 while True: 159 162 try: ··· 162 165 # Exit when idle during shutdown 163 166 if self.shutdown_requested.is_set(): 164 167 with self.lock: 165 - if len(self.running_agents) == 0: 168 + if len(self.running_uses) == 0: 166 169 self.logger.info( 167 - "No agents running, exiting gracefully" 170 + "No talent uses running, exiting gracefully" 168 171 ) 169 172 return 170 173 break ··· 185 188 self.logger.exception(f"Error handling request: {e}") 186 189 187 190 def _handle_request(self, request: Dict[str, Any]) -> None: 188 - """Handle a new agent request from Callosum. 191 + """Handle a new talent request from Callosum. 189 192 190 193 Cortex is a minimal process manager - it only handles: 191 - - File lifecycle (<agent>/<id>_active.jsonl -> <agent>/<id>.jsonl) 194 + - File lifecycle (<talent>/<id>_active.jsonl -> <talent>/<id>.jsonl) 192 195 - Process spawning and monitoring 193 196 - Event relay to Callosum 194 197 195 - All config loading, validation, and hydration is done by agents.py. 198 + All config loading, validation, and hydration is done by think.talents. 196 199 Cortex only resolves talent cwd early so the child process starts in 197 200 the correct working directory. 198 201 """ 199 - agent_id = request.get("agent_id") 200 - if not agent_id: 201 - self.logger.error("Received request without agent_id") 202 + use_id = request.get("use_id") 203 + if not use_id: 204 + self.logger.error("Received request without use_id") 202 205 return 203 206 204 - # Skip if this agent is already being processed 207 + # Skip if this use is already being processed 205 208 with self.lock: 206 - if agent_id in self.running_agents: 207 - self.logger.debug(f"Agent {agent_id} already running, skipping") 209 + if use_id in self.running_uses: 210 + self.logger.debug(f"Talent use {use_id} already running, skipping") 208 211 return 209 212 210 213 # Create _active.jsonl file (exclusive creation to prevent race conditions) 211 214 name = request.get("name", "unified") 212 215 safe_name = name.replace(":", "--") 213 - agent_subdir = self.agents_dir / safe_name 214 - agent_subdir.mkdir(parents=True, exist_ok=True) 215 - file_path = agent_subdir / f"{agent_id}_active.jsonl" 216 + talent_subdir = self.talents_dir / safe_name 217 + talent_subdir.mkdir(parents=True, exist_ok=True) 218 + file_path = talent_subdir / f"{use_id}_active.jsonl" 216 219 if file_path.exists(): 217 - self.logger.debug(f"Agent {agent_id} already claimed by another process") 220 + self.logger.debug(f"Talent use {use_id} already claimed by another process") 218 221 return 219 222 220 223 try: ··· 223 226 except FileExistsError: 224 227 return 225 228 226 - self.logger.info(f"Processing agent request: {agent_id}") 229 + self.logger.info(f"Processing talent request: {use_id}") 227 230 228 231 # Store request for later use (output writing) 229 232 with self.lock: 230 - self.agent_requests[agent_id] = request 233 + self.use_requests[use_id] = request 231 234 232 - # Spawn agent process - it handles all validation/hydration 235 + # Spawn talent process - it handles all validation/hydration 233 236 try: 234 237 self._spawn_subprocess( 235 - agent_id, file_path, request, ["sol", "agents"], "agent" 238 + use_id, 239 + file_path, 240 + request, 241 + [sys.executable, "-m", TALENT_EXECUTION_MODULE], 242 + "talent", 236 243 ) 237 244 except Exception as e: 238 - self.logger.exception(f"Failed to spawn agent {agent_id}: {e}") 239 - self._write_error_and_complete(file_path, f"Failed to spawn agent: {e}") 245 + self.logger.exception(f"Failed to spawn talent {use_id}: {e}") 246 + self._write_error_and_complete(file_path, f"Failed to spawn talent: {e}") 240 247 241 248 def _spawn_subprocess( 242 249 self, 243 - agent_id: str, 250 + use_id: str, 244 251 file_path: Path, 245 252 config: Dict[str, Any], 246 253 cmd: list[str], ··· 249 256 """Spawn a subprocess and monitor its output. 250 257 251 258 Args: 252 - agent_id: Unique identifier for this process 259 + use_id: Unique identifier for this process 253 260 file_path: Path to the JSONL log file 254 261 config: Configuration dict to pass via NDJSON stdin 255 - cmd: Command to run (e.g., ["sol", "agents"]) 256 - process_type: Label for logging ("agent") 262 + cmd: Command to run (e.g., [sys.executable, "-m", TALENT_EXECUTION_MODULE]) 263 + process_type: Label for logging ("talent") 257 264 """ 258 265 try: 259 266 # Store the config for later use - thread safe 260 267 with self.lock: 261 - self.agent_requests[agent_id] = config 268 + self.use_requests[use_id] = config 262 269 263 270 # Pass the full config through as NDJSON 264 271 ndjson_input = json.dumps(config) ··· 280 287 env.update({k: str(v) for k, v in env_overrides.items()}) 281 288 282 289 # Spawn the subprocess 283 - self.logger.info(f"Spawning {process_type} {agent_id}: {cmd}") 290 + self.logger.info(f"Spawning {process_type} {use_id}: {cmd}") 284 291 self.logger.debug(f"NDJSON input: {ndjson_input}") 285 292 subprocess_cwd = None 286 - if process_type == "agent": 287 - from think.talent import get_agent 293 + if process_type == "talent": 294 + from think.talent import get_talent 288 295 289 296 talent_key = str(config.get("name", "unified")) 290 - talent_config = get_agent(talent_key) 297 + talent_config = get_talent(talent_key) 291 298 if talent_config.get("type") == "cogitate": 292 - # Resolve here because prepare_config() runs inside sol agents. 299 + # Resolve here because prepare_config() runs inside think.talents. 293 300 cwd_value = talent_config.get("cwd") 294 301 if cwd_value == "journal": 295 302 try: ··· 321 328 process.stdin.close() 322 329 323 330 # Track the running process 324 - agent = AgentProcess(agent_id, process, file_path) 331 + agent = TalentProcess(use_id, process, file_path) 325 332 with self.lock: 326 - self.running_agents[agent_id] = agent 333 + self.running_uses[use_id] = agent 327 334 328 335 # Set up timeout (default to 10 minutes if not specified) 329 336 timeout_seconds = config.get("timeout_seconds", 600) 330 337 agent.timeout_timer = threading.Timer( 331 338 timeout_seconds, 332 - lambda: self._timeout_agent(agent_id, agent, timeout_seconds), 339 + lambda: self._timeout_talent(use_id, agent, timeout_seconds), 333 340 ) 334 341 agent.timeout_timer.start() 335 342 ··· 343 350 ).start() 344 351 345 352 self.logger.info( 346 - f"{process_type.capitalize()} {agent_id} spawned successfully " 353 + f"{process_type.capitalize()} {use_id} spawned successfully " 347 354 f"(PID: {process.pid})" 348 355 ) 349 356 350 357 except Exception as e: 351 - self.logger.exception(f"Failed to spawn {process_type} {agent_id}: {e}") 358 + self.logger.exception(f"Failed to spawn {process_type} {use_id}: {e}") 352 359 self._write_error_and_complete( 353 360 file_path, f"Failed to spawn {process_type}: {e}" 354 361 ) 355 362 356 - def _timeout_agent( 357 - self, agent_id: str, agent: AgentProcess, timeout_seconds: int 363 + def _timeout_talent( 364 + self, use_id: str, agent: TalentProcess, timeout_seconds: int 358 365 ) -> None: 359 - """Handle agent timeout.""" 366 + """Handle talent timeout.""" 360 367 if agent.is_running(): 361 368 self.logger.warning( 362 - f"Agent {agent_id} timed out after {timeout_seconds} seconds" 369 + f"Talent {use_id} timed out after {timeout_seconds} seconds" 363 370 ) 364 371 error_event = self._create_error_event( 365 - agent_id, f"Agent timed out after {timeout_seconds} seconds" 372 + use_id, f"Talent timed out after {timeout_seconds} seconds" 366 373 ) 367 374 try: 368 375 with open(agent.log_path, "a") as f: ··· 370 377 except Exception as e: 371 378 self.logger.error(f"Failed to write timeout event: {e}") 372 379 373 - # Broadcast to callosum so wait_for_agents detects immediately 380 + # Broadcast to callosum so wait_for_uses detects immediately 374 381 try: 375 382 event_copy = error_event.copy() 376 383 event_type = event_copy.pop("event", "error") ··· 380 387 381 388 agent.stop() 382 389 383 - def _monitor_stdout(self, agent: AgentProcess) -> None: 384 - """Monitor agent stdout and append events to the JSONL file.""" 390 + def _monitor_stdout(self, agent: TalentProcess) -> None: 391 + """Monitor talent stdout and append events to the JSONL file.""" 385 392 if not agent.process.stdout: 386 393 return 387 394 ··· 399 406 # Parse JSON event 400 407 event = json.loads(line) 401 408 402 - # Ensure event has timestamp and agent_id 409 + # Ensure event has timestamp and use_id 403 410 if "ts" not in event: 404 411 event["ts"] = now_ms() 405 - if "agent_id" not in event: 406 - event["agent_id"] = agent.agent_id 412 + if "use_id" not in event: 413 + event["use_id"] = agent.use_id 407 414 408 415 # Inject agent name for WebSocket consumers 409 416 with self.lock: 410 - _req = self.agent_requests.get(agent.agent_id) 417 + _req = self.use_requests.get(agent.use_id) 411 418 if _req and "name" not in event: 412 419 event["name"] = _req.get("name", "") 413 - # Inject display mode for triage agent finish events 420 + # Inject display mode for triage talent finish events 414 421 if event.get("event") == "finish" and _req: 415 422 try: 416 423 from apps.home.events import TRIAGE_AGENT_NAMES ··· 441 448 if event.get("event") == "start": 442 449 # Capture model and provider for status reporting 443 450 with self.lock: 444 - if agent.agent_id in self.agent_requests: 451 + if agent.use_id in self.use_requests: 445 452 model = event.get("model") 446 453 if model: 447 - self.agent_requests[agent.agent_id]["model"] = ( 448 - model 449 - ) 454 + self.use_requests[agent.use_id]["model"] = model 450 455 provider = event.get("provider") 451 456 if provider: 452 - self.agent_requests[agent.agent_id][ 453 - "provider" 454 - ] = provider 457 + self.use_requests[agent.use_id]["provider"] = ( 458 + provider 459 + ) 455 460 456 461 # Handle finish or error event 457 462 if event.get("event") in ["finish", "error"]: ··· 461 466 462 467 # Get original request (thread-safe access) 463 468 with self.lock: 464 - original_request = self.agent_requests.get( 465 - agent.agent_id 469 + original_request = self.use_requests.get( 470 + agent.use_id 466 471 ) 467 472 468 473 # Log token usage if available ··· 493 498 ) 494 499 except Exception as e: 495 500 self.logger.warning( 496 - f"Failed to log token usage for agent {agent.agent_id}: {e}" 501 + f"Failed to log token usage for talent {agent.use_id}: {e}" 497 502 ) 498 503 499 504 # Write output if requested 500 505 if original_request and original_request.get("output"): 501 506 self._write_output( 502 - agent.agent_id, 507 + agent.use_id, 503 508 result, 504 509 original_request, 505 510 ) ··· 513 518 "event": "info", 514 519 "ts": now_ms(), 515 520 "message": line, 516 - "agent_id": agent.agent_id, 521 + "use_id": agent.use_id, 517 522 } 518 523 with open(agent.log_path, "a") as f: 519 524 f.write(json.dumps(info_event) + "\n") 520 525 521 526 except Exception as e: 522 - self.logger.error( 523 - f"Error monitoring stdout for agent {agent.agent_id}: {e}" 524 - ) 527 + self.logger.error(f"Error monitoring stdout for agent {agent.use_id}: {e}") 525 528 finally: 526 529 # Wait for process to fully exit (reaps zombie) 527 530 exit_code = agent.process.wait() 528 - self.logger.info(f"Agent {agent.agent_id} exited with code {exit_code}") 531 + self.logger.info(f"Talent {agent.use_id} exited with code {exit_code}") 529 532 530 533 # Check if finish event was emitted 531 534 has_finish = self._has_finish_event(agent.log_path) ··· 533 536 if not has_finish: 534 537 # Write error event if no finish using standardized format 535 538 error_event = self._create_error_event( 536 - agent.agent_id, 537 - f"Agent exited with code {exit_code} without finish event", 539 + agent.use_id, 540 + f"Talent exited with code {exit_code} without finish event", 538 541 exit_code=exit_code, 539 542 ) 540 543 with open(agent.log_path, "a") as f: 541 544 f.write(json.dumps(error_event) + "\n") 542 545 543 546 # Complete the file (rename from _active.jsonl to .jsonl) 544 - self._complete_agent_file(agent.agent_id, agent.log_path) 547 + self._complete_use_file(agent.use_id, agent.log_path) 545 548 546 549 # Remove from running agents and clean up stored request (thread-safe) 547 550 with self.lock: 548 - if agent.agent_id in self.running_agents: 549 - del self.running_agents[agent.agent_id] 551 + if agent.use_id in self.running_uses: 552 + del self.running_uses[agent.use_id] 550 553 # Clean up stored request 551 - if agent.agent_id in self.agent_requests: 552 - del self.agent_requests[agent.agent_id] 554 + if agent.use_id in self.use_requests: 555 + del self.use_requests[agent.use_id] 553 556 554 - def _monitor_stderr(self, agent: AgentProcess) -> None: 555 - """Monitor agent stderr for errors.""" 557 + def _monitor_stderr(self, agent: TalentProcess) -> None: 558 + """Monitor talent stderr for errors.""" 556 559 if not agent.process.stderr: 557 560 return 558 561 ··· 565 568 stripped = line.strip() 566 569 if stripped: 567 570 stderr_lines.append(stripped) 568 - # Pass through to cortex stderr with agent prefix for traceability 571 + # Pass through to cortex stderr with talent prefix for traceability 569 572 print( 570 - f"[agent:{agent.agent_id}:stderr] {stripped}", 573 + f"[talent:{agent.use_id}:stderr] {stripped}", 571 574 file=sys.stderr, 572 575 flush=True, 573 576 ) 574 577 575 578 except Exception as e: 576 - self.logger.error( 577 - f"Error monitoring stderr for agent {agent.agent_id}: {e}" 578 - ) 579 + self.logger.error(f"Error monitoring stderr for agent {agent.use_id}: {e}") 579 580 finally: 580 581 # If process failed with stderr output, write error event 581 582 if stderr_lines: 582 583 exit_code = agent.process.poll() 583 584 if exit_code is not None and exit_code != 0: 584 585 error_event = self._create_error_event( 585 - agent.agent_id, 586 + agent.use_id, 586 587 "Process failed with stderr output", 587 588 trace="\n".join(stderr_lines), 588 589 exit_code=exit_code, ··· 608 609 pass 609 610 return False 610 611 611 - def _complete_agent_file(self, agent_id: str, file_path: Path) -> None: 612 - """Complete an agent by renaming the file from _active.jsonl to .jsonl.""" 612 + def _complete_use_file(self, use_id: str, file_path: Path) -> None: 613 + """Complete a talent use by renaming the file from _active.jsonl to .jsonl.""" 613 614 try: 614 - completed_path = file_path.parent / f"{agent_id}.jsonl" 615 + completed_path = file_path.parent / f"{use_id}.jsonl" 615 616 file_path.rename(completed_path) 616 - self.logger.info(f"Completed agent {agent_id}: {completed_path}") 617 + self.logger.info(f"Completed talent use {use_id}: {completed_path}") 617 618 618 - # Create convenience symlink: {name}.log -> {name}/{agent_id}.jsonl 619 - request = self.agent_requests.get(agent_id) 619 + # Create convenience symlink: {name}.log -> {name}/{use_id}.jsonl 620 + request = self.use_requests.get(use_id) 620 621 if request: 621 622 name = request.get("name") 622 623 if name: 623 624 safe_name = name.replace(":", "--") 624 - link_path = self.agents_dir / f"{safe_name}.log" 625 - _atomic_symlink(link_path, f"{safe_name}/{agent_id}.jsonl") 625 + link_path = self.talents_dir / f"{safe_name}.log" 626 + _atomic_symlink(link_path, f"{safe_name}/{use_id}.jsonl") 626 627 self.logger.debug( 627 - f"Symlinked {safe_name}.log -> {safe_name}/{agent_id}.jsonl" 628 + f"Symlinked {safe_name}.log -> {safe_name}/{use_id}.jsonl" 628 629 ) 629 630 630 631 # Append summary to day index 631 - self._append_day_index(agent_id, request, completed_path) 632 + self._append_day_index(use_id, request, completed_path) 632 633 else: 633 634 self.logger.debug( 634 - f"No name in request for {agent_id}, skipping symlink" 635 + f"No name in request for {use_id}, skipping symlink" 635 636 ) 636 637 except Exception as e: 637 - self.logger.error(f"Failed to complete agent file {agent_id}: {e}") 638 + self.logger.error(f"Failed to complete talent file {use_id}: {e}") 638 639 639 640 def _append_day_index( 640 - self, agent_id: str, request: Dict[str, Any], completed_path: Path 641 + self, use_id: str, request: Dict[str, Any], completed_path: Path 641 642 ) -> None: 642 - """Append agent summary to day index file.""" 643 + """Append talent-use summary to the day index file.""" 643 644 try: 644 - # Determine day from request or agent_id timestamp 645 + # Determine day from request or use_id timestamp 645 646 day = request.get("day") 646 647 if not day: 647 648 from datetime import datetime 648 649 649 - ts_seconds = int(agent_id) / 1000 650 + ts_seconds = int(use_id) / 1000 650 651 day = datetime.fromtimestamp(ts_seconds).strftime("%Y%m%d") 651 652 652 653 start_ts = request.get("ts", 0) ··· 681 682 pass 682 683 683 684 summary = { 684 - "agent_id": agent_id, 685 + "use_id": use_id, 685 686 "name": request.get("name", "unified"), 686 687 "day": day, 687 688 "facet": request.get("facet"), ··· 693 694 "schedule": request.get("schedule"), 694 695 } 695 696 696 - day_index_path = self.agents_dir / f"{day}.jsonl" 697 + day_index_path = self.talents_dir / f"{day}.jsonl" 697 698 with open(day_index_path, "a") as f: 698 699 f.write(json.dumps(summary) + "\n") 699 700 f.flush() 700 701 701 702 except Exception as e: 702 - self.logger.error(f"Failed to append day index for {agent_id}: {e}") 703 + self.logger.error(f"Failed to append day index for {use_id}: {e}") 703 704 704 705 def _write_error_and_complete(self, file_path: Path, error_message: str) -> None: 705 706 """Write an error event to the file and mark it as complete.""" 706 707 try: 707 - agent_id = file_path.stem.replace("_active", "") 708 - error_event = self._create_error_event(agent_id, error_message) 708 + use_id = file_path.stem.replace("_active", "") 709 + error_event = self._create_error_event(use_id, error_message) 709 710 with open(file_path, "a") as f: 710 711 f.write(json.dumps(error_event) + "\n") 711 712 712 713 # Complete the file 713 - self._complete_agent_file(agent_id, file_path) 714 + self._complete_use_file(use_id, file_path) 714 715 except Exception as e: 715 716 self.logger.error(f"Failed to write error and complete: {e}") 716 717 717 - def _write_output(self, agent_id: str, result: str, config: Dict[str, Any]) -> None: 718 - """Write agent output to config["output_path"]. 718 + def _write_output(self, use_id: str, result: str, config: Dict[str, Any]) -> None: 719 + """Write talent output to config["output_path"]. 719 720 720 721 The output path is set by the caller — either derived by 721 - prepare_config in agents.py (day/segment agents) or computed 722 - by dream.py via get_activity_output_path (activity agents). 722 + prepare_config in think.talents (day/segment talents) or computed 723 + by dream.py via get_activity_output_path (activity talents). 723 724 Cortex does not derive paths itself. 724 725 """ 725 726 output_path_str = config.get("output_path") ··· 733 734 with open(output_path, "w", encoding="utf-8") as f: 734 735 f.write(result) 735 736 736 - self.logger.info(f"Wrote agent {agent_id} output to {output_path}") 737 + self.logger.info(f"Wrote talent {use_id} output to {output_path}") 737 738 738 739 except Exception as e: 739 - self.logger.error(f"Failed to write agent {agent_id} output: {e}") 740 + self.logger.error(f"Failed to write talent {use_id} output: {e}") 740 741 741 742 def stop(self) -> None: 742 743 """Stop the Cortex service.""" ··· 746 747 if self.callosum: 747 748 self.callosum.stop() 748 749 749 - # Stop all running agents 750 + # Stop all running talent uses 750 751 with self.lock: 751 - for agent in self.running_agents.values(): 752 + for agent in self.running_uses.values(): 752 753 agent.stop() 753 754 754 755 def _emit_periodic_status(self) -> None: ··· 756 757 while not self.stop_event.is_set(): 757 758 try: 758 759 with self.lock: 759 - agents = [] 760 - for agent_id, agent_proc in self.running_agents.items(): 761 - config = self.agent_requests.get(agent_id, {}) 762 - agents.append( 760 + uses = [] 761 + for use_id, agent_proc in self.running_uses.items(): 762 + config = self.use_requests.get(use_id, {}) 763 + uses.append( 763 764 { 764 - "agent_id": agent_id, 765 + "use_id": use_id, 765 766 "name": config.get("name", "unknown"), 766 767 "provider": config.get("provider", "unknown"), 767 768 "elapsed_seconds": int( ··· 770 771 } 771 772 ) 772 773 773 - # Only emit status when there are active agents 774 - if agents: 774 + # Only emit status when there are active talent uses 775 + if uses: 775 776 self.callosum.emit( 776 777 "cortex", 777 778 "status", 778 - running_agents=len(agents), 779 - agents=agents, 779 + running_uses=len(uses), 780 + uses=uses, 780 781 ) 781 782 except Exception as e: 782 783 self.logger.debug(f"Status emission failed: {e}") ··· 787 788 """Get service status information.""" 788 789 with self.lock: 789 790 return { 790 - "running_agents": len(self.running_agents), 791 - "agent_ids": list(self.running_agents.keys()), 791 + "running_uses": len(self.running_uses), 792 + "use_ids": list(self.running_uses.keys()), 792 793 } 793 794 794 795 ··· 798 799 799 800 from think.utils import require_solstone, setup_cli 800 801 801 - parser = argparse.ArgumentParser(description="solstone Cortex Agent Manager") 802 + parser = argparse.ArgumentParser(description="solstone Cortex Talent Manager") 802 803 args = setup_cli(parser) 803 804 require_solstone() 804 805
+102 -102
think/cortex_client.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Cortex client for managing AI agent requests.""" 4 + """Cortex client for managing AI talent requests.""" 5 5 6 6 import json 7 7 import logging ··· 18 18 _last_ts = 0 19 19 20 20 21 - def _find_agent_file(agents_dir: Path, agent_id: str) -> tuple[Path | None, str]: 22 - """Find an agent log file in per-agent subdirectories. 21 + def _find_use_file(talents_dir: Path, use_id: str) -> tuple[Path | None, str]: 22 + """Find a use log file in per-talent subdirectories. 23 23 24 24 Returns: 25 25 Tuple of (file_path, status) where status is 26 26 "completed", "running", or "not_found". 27 27 """ 28 - for match in agents_dir.glob(f"*/{agent_id}.jsonl"): 28 + for match in talents_dir.glob(f"*/{use_id}.jsonl"): 29 29 return match, "completed" 30 - for match in agents_dir.glob(f"*/{agent_id}_active.jsonl"): 30 + for match in talents_dir.glob(f"*/{use_id}_active.jsonl"): 31 31 return match, "running" 32 32 return None, "not_found" 33 33 ··· 38 38 provider: Optional[str] = None, 39 39 config: Optional[Dict[str, Any]] = None, 40 40 ) -> str | None: 41 - """Create a Cortex agent request via Callosum broadcast. 41 + """Create a Cortex talent request via Callosum broadcast. 42 42 43 43 Args: 44 - prompt: The task or question for the agent 45 - name: Agent name - system (e.g., "unified") or app-qualified (e.g., "entities:entity_assist") 44 + prompt: The task or question for the talent 45 + name: Talent name - system (e.g., "unified") or app-qualified (e.g., "entities:entity_assist") 46 46 provider: AI provider - openai, google, or anthropic 47 47 config: Provider-specific configuration (model, max_output_tokens, thinking_budget, etc.) 48 48 49 49 Returns: 50 - Agent ID (timestamp-based string), or None if the Callosum send failed. 50 + Use ID (timestamp-based string), or None if the Callosum send failed. 51 51 """ 52 - # Get journal path (for agent_id uniqueness check) 52 + # Get journal path (for use_id uniqueness check) 53 53 journal_path = get_journal() 54 54 55 - # Create agents directory if it doesn't exist 56 - agents_dir = Path(journal_path) / "agents" 57 - agents_dir.mkdir(parents=True, exist_ok=True) 55 + # Create talents directory if it doesn't exist 56 + talents_dir = Path(journal_path) / "talents" 57 + talents_dir.mkdir(parents=True, exist_ok=True) 58 58 59 59 # Generate monotonic timestamp in milliseconds, ensuring uniqueness 60 60 global _last_ts ··· 65 65 ts = _last_ts + 1 66 66 67 67 _last_ts = ts 68 - agent_id = str(ts) 68 + use_id = str(ts) 69 69 70 70 # Build request object 71 71 request = { 72 72 "event": "request", 73 73 "ts": ts, 74 - "agent_id": agent_id, 74 + "use_id": use_id, 75 75 "prompt": prompt, 76 76 "provider": provider, 77 77 "name": name, ··· 91 91 sent = callosum_send("cortex", "request", **request_fields) 92 92 93 93 if not sent: 94 - logger.info("Failed to send cortex request for agent '%s'", name) 94 + logger.info("Failed to send cortex request for talent '%s'", name) 95 95 return None 96 96 97 - return agent_id 97 + return use_id 98 98 99 99 100 - def get_agent_log_status(agent_id: str) -> str: 101 - """Get the status of a specific agent from its log file. 100 + def get_use_log_status(use_id: str) -> str: 101 + """Get the status of a specific use from its log file. 102 102 103 103 Args: 104 - agent_id: The agent ID (timestamp) 104 + use_id: The use ID (timestamp) 105 105 106 106 Returns: 107 - "completed" - Agent finished (*.jsonl exists) 108 - "running" - Agent still active (*_active.jsonl exists) 109 - "not_found" - No agent file exists 107 + "completed" - Use finished (*.jsonl exists) 108 + "running" - Use still active (*_active.jsonl exists) 109 + "not_found" - No use file exists 110 110 """ 111 - agents_dir = Path(get_journal()) / "agents" 112 - _, status = _find_agent_file(agents_dir, agent_id) 111 + talents_dir = Path(get_journal()) / "talents" 112 + _, status = _find_use_file(talents_dir, use_id) 113 113 return status 114 114 115 115 116 - def wait_for_agents( 117 - agent_ids: list[str], 116 + def wait_for_uses( 117 + use_ids: list[str], 118 118 timeout: int | None = 600, 119 119 ) -> tuple[dict[str, str], list[str]]: 120 - """Wait for agents to complete via Callosum events. 120 + """Wait for uses to complete via Callosum events. 121 121 122 122 Listens for cortex.finish and cortex.error events. Sets up the event 123 - listener first, then does an initial file check for agents that may have 123 + listener first, then does an initial file check for uses that may have 124 124 already completed, and a final file check at timeout as a backstop for 125 125 any missed events. 126 126 127 127 Args: 128 - agent_ids: List of agent IDs to wait for 128 + use_ids: List of use IDs to wait for 129 129 timeout: Maximum wait time in seconds (default 600 = 10 minutes) 130 130 131 131 Returns: 132 132 Tuple of (completed, timed_out) where completed is a dict mapping 133 - agent_id to end state ("finish" or "error"), and timed_out is a 134 - list of agent IDs that did not complete within the timeout. 133 + use_id to end state ("finish" or "error"), and timed_out is a 134 + list of use IDs that did not complete within the timeout. 135 135 """ 136 - pending = set(agent_ids) 136 + pending = set(use_ids) 137 137 completed: dict[str, str] = {} 138 138 lock = threading.Lock() 139 139 all_done = threading.Event() ··· 141 141 def on_message(msg: dict) -> None: 142 142 if msg.get("tract") != "cortex": 143 143 return 144 - agent_id = msg.get("agent_id") 145 - if not agent_id: 144 + use_id = msg.get("use_id") 145 + if not use_id: 146 146 return 147 147 148 148 event_type = msg.get("event") 149 149 if event_type in ("finish", "error"): 150 150 with lock: 151 - if agent_id in pending: 152 - completed[agent_id] = event_type 153 - pending.discard(agent_id) 151 + if use_id in pending: 152 + completed[use_id] = event_type 153 + pending.discard(use_id) 154 154 if not pending: 155 155 all_done.set() 156 156 ··· 161 161 try: 162 162 # Initial file check (with lock since callback may be running) 163 163 with lock: 164 - for agent_id in list(pending): 165 - end_state = get_agent_end_state(agent_id) 164 + for use_id in list(pending): 165 + end_state = get_use_end_state(use_id) 166 166 if end_state in ("finish", "error"): 167 - completed[agent_id] = end_state 168 - pending.discard(agent_id) 167 + completed[use_id] = end_state 168 + pending.discard(use_id) 169 169 170 170 if not pending: 171 171 return completed, [] ··· 178 178 179 179 # Final file check for any remaining (backstop for missed events) 180 180 # Listener is stopped, so no lock needed 181 - for agent_id in list(pending): 182 - end_state = get_agent_end_state(agent_id) 181 + for use_id in list(pending): 182 + end_state = get_use_end_state(use_id) 183 183 if end_state in ("finish", "error"): 184 184 logger.info( 185 - f"Agent {agent_id} completion event not received but agent completed" 185 + f"Talent use {use_id} completion event not received but use completed" 186 186 ) 187 - completed[agent_id] = end_state 188 - pending.discard(agent_id) 187 + completed[use_id] = end_state 188 + pending.discard(use_id) 189 189 190 190 return completed, list(pending) 191 191 192 192 193 - def get_agent_end_state(agent_id: str) -> str: 194 - """Get how a completed agent ended (finish or error). 193 + def get_use_end_state(use_id: str) -> str: 194 + """Get how a completed use ended (finish or error). 195 195 196 196 Checks file contents for terminal events even if file is still _active.jsonl, 197 197 since Callosum broadcasts happen before file rename. 198 198 199 199 Args: 200 - agent_id: The agent ID (timestamp) 200 + use_id: The use ID (timestamp) 201 201 202 202 Returns: 203 - "finish" - Agent completed successfully 204 - "error" - Agent ended with an error 205 - "running" - Agent is still active (no terminal event in file) 206 - "unknown" - Agent file not found 203 + "finish" - Use completed successfully 204 + "error" - Use ended with an error 205 + "running" - Use is still active (no terminal event in file) 206 + "unknown" - Use file not found 207 207 """ 208 - status = get_agent_log_status(agent_id) 208 + status = get_use_log_status(use_id) 209 209 if status == "not_found": 210 210 return "unknown" 211 211 212 212 # Read events to find terminal state (even for "running" files that may 213 213 # have finish event - Callosum broadcast happens before file rename) 214 214 try: 215 - events = read_agent_events(agent_id) 215 + events = read_use_events(use_id) 216 216 # Find last finish or error event 217 217 for event in reversed(events): 218 218 event_type = event.get("event") ··· 226 226 return "unknown" 227 227 228 228 229 - def read_agent_events(agent_id: str) -> list[Dict[str, Any]]: 230 - """Read all events from an agent's JSONL log file. 229 + def read_use_events(use_id: str) -> list[Dict[str, Any]]: 230 + """Read all events from a use's JSONL log file. 231 231 232 232 Args: 233 - agent_id: The agent ID (timestamp) 233 + use_id: The use ID (timestamp) 234 234 235 235 Returns: 236 236 List of event dictionaries in chronological order 237 237 238 238 Raises: 239 - FileNotFoundError: If agent log doesn't exist 239 + FileNotFoundError: If the use log doesn't exist 240 240 """ 241 - agents_dir = Path(get_journal()) / "agents" 242 - agent_file, _status = _find_agent_file(agents_dir, agent_id) 243 - if agent_file is None: 244 - raise FileNotFoundError(f"Agent log not found: {agent_id}") 241 + talents_dir = Path(get_journal()) / "talents" 242 + use_file, _status = _find_use_file(talents_dir, use_id) 243 + if use_file is None: 244 + raise FileNotFoundError(f"Talent log not found: {use_id}") 245 245 246 246 events = [] 247 - with open(agent_file, "r") as f: 247 + with open(use_file, "r") as f: 248 248 for line in f: 249 249 line = line.strip() 250 250 if not line: ··· 253 253 event = json.loads(line) 254 254 events.append(event) 255 255 except json.JSONDecodeError: 256 - logger.debug(f"Skipping malformed JSON in {agent_file}") 256 + logger.debug(f"Skipping malformed JSON in {use_file}") 257 257 continue 258 258 259 259 return events 260 260 261 261 262 - def cortex_agents( 262 + def cortex_uses( 263 263 limit: int = 10, 264 264 offset: int = 0, 265 - agent_type: str = "all", 265 + use_type: str = "all", 266 266 facet: Optional[str] = None, 267 267 ) -> Dict[str, Any]: 268 - """List agents from the journal with pagination and filtering. 268 + """List talent uses from the journal with pagination and filtering. 269 269 270 270 Args: 271 - limit: Maximum number of agents to return (1-100) 272 - offset: Number of agents to skip 273 - agent_type: Filter by "live", "historical", or "all" 274 - facet: Optional facet to filter by. If provided, only returns agents 271 + limit: Maximum number of uses to return (1-100) 272 + offset: Number of uses to skip 273 + use_type: Filter by "live", "historical", or "all" 274 + facet: Optional facet to filter by. If provided, only returns uses 275 275 that were run in this facet context. None means no filtering. 276 276 277 277 Returns: 278 - Dictionary with agents list and pagination info 278 + Dictionary with use list and pagination info 279 279 """ 280 280 # Validate parameters 281 281 limit = max(1, min(limit, 100)) 282 282 offset = max(0, offset) 283 283 284 - agents_dir = Path(get_journal()) / "agents" 285 - if not agents_dir.exists(): 284 + talents_dir = Path(get_journal()) / "talents" 285 + if not talents_dir.exists(): 286 286 return { 287 - "agents": [], 287 + "uses": [], 288 288 "pagination": { 289 289 "limit": limit, 290 290 "offset": offset, ··· 295 295 "historical_count": 0, 296 296 } 297 297 298 - # Collect all agent files 299 - all_agents = [] 298 + # Collect all use files 299 + all_uses = [] 300 300 live_count = 0 301 301 historical_count = 0 302 302 303 - for agent_file in agents_dir.glob("*/*.jsonl"): 303 + for use_file in talents_dir.glob("*/*.jsonl"): 304 304 # Determine status from filename 305 - is_active = "_active.jsonl" in agent_file.name 306 - is_pending = "_pending.jsonl" in agent_file.name 305 + is_active = "_active.jsonl" in use_file.name 306 + is_pending = "_pending.jsonl" in use_file.name 307 307 308 308 # Skip pending files 309 309 if is_pending: ··· 318 318 historical_count += 1 319 319 320 320 # Filter by requested type 321 - if agent_type == "live" and status != "running": 321 + if use_type == "live" and status != "running": 322 322 continue 323 - if agent_type == "historical" and status != "completed": 323 + if use_type == "historical" and status != "completed": 324 324 continue 325 325 326 - # Extract agent ID from filename 327 - agent_id = agent_file.stem.replace("_active", "") 326 + # Extract use ID from filename 327 + use_id = use_file.stem.replace("_active", "") 328 328 329 - # Read agent file to get request info and calculate runtime 329 + # Read use file to get request info and calculate runtime 330 330 try: 331 - with open(agent_file, "r") as f: 331 + with open(use_file, "r") as f: 332 332 lines = f.readlines() 333 333 if not lines: 334 334 continue ··· 343 343 continue 344 344 345 345 # Extract facet from request 346 - agent_facet = request.get("facet") 346 + use_facet = request.get("facet") 347 347 348 348 # Filter by facet if specified 349 - if facet is not None and agent_facet != facet: 349 + if facet is not None and use_facet != facet: 350 350 continue 351 351 352 352 # Extract basic info 353 - agent_info = { 354 - "id": agent_id, 353 + use_info = { 354 + "id": use_id, 355 355 "name": request.get("name", "unified"), 356 356 "start": request.get("ts", 0), 357 357 "status": status, 358 358 "prompt": request.get("prompt", ""), 359 359 "provider": request.get("provider", "openai"), 360 - "facet": agent_facet, 360 + "facet": use_facet, 361 361 } 362 362 363 - # For completed agents, find finish event to calculate runtime 363 + # For completed uses, find finish event to calculate runtime 364 364 if status == "completed" and len(lines) > 1: 365 365 # Read last few lines to find finish event (reading backwards is more efficient) 366 366 for line in reversed(lines[-10:]): # Check last 10 lines ··· 371 371 event = json.loads(line) 372 372 if event.get("event") == "finish": 373 373 end_ts = event.get("ts", 0) 374 - if end_ts and agent_info["start"]: 374 + if end_ts and use_info["start"]: 375 375 # Calculate runtime in seconds 376 - agent_info["runtime_seconds"] = ( 377 - end_ts - agent_info["start"] 376 + use_info["runtime_seconds"] = ( 377 + end_ts - use_info["start"] 378 378 ) / 1000.0 379 379 break 380 380 except json.JSONDecodeError: 381 381 continue 382 382 383 - all_agents.append(agent_info) 383 + all_uses.append(use_info) 384 384 except (json.JSONDecodeError, IOError): 385 385 # Skip malformed files 386 386 continue 387 387 388 388 # Sort by start time (newest first) 389 - all_agents.sort(key=lambda x: x["start"], reverse=True) 389 + all_uses.sort(key=lambda x: x["start"], reverse=True) 390 390 391 391 # Apply pagination 392 - total = len(all_agents) 393 - paginated = all_agents[offset : offset + limit] 392 + total = len(all_uses) 393 + paginated = all_uses[offset : offset + limit] 394 394 395 395 return { 396 - "agents": paginated, 396 + "uses": paginated, 397 397 "pagination": { 398 398 "limit": limit, 399 399 "offset": offset,
+129 -129
think/dream.py
··· 28 28 from think.activity_state_machine import ActivityStateMachine 29 29 from think.callosum import CallosumConnection 30 30 from think.cluster import cluster_segments 31 - from think.cortex_client import cortex_request, wait_for_agents 31 + from think.cortex_client import cortex_request, wait_for_uses 32 32 from think.facets import ( 33 33 get_active_facets, 34 34 get_enabled_facets, ··· 76 76 if not self.file: 77 77 return 78 78 data = {"event": event, "ts": now_ms(), **fields} 79 - if event == "agent.skip": 79 + if event == "talent.skip": 80 80 self.skip_count += 1 81 81 try: 82 82 self.file.write(json.dumps(data, ensure_ascii=False) + "\n") ··· 102 102 103 103 104 104 def _log_skip(name: str, reason: str, detail: str, **extra) -> None: 105 - """Emit an agent.skip JSONL event.""" 106 - _jsonl_log("agent.skip", name=name, reason=reason, detail=detail, **extra) 105 + """Emit an talent.skip JSONL event.""" 106 + _jsonl_log("talent.skip", name=name, reason=reason, detail=detail, **extra) 107 107 108 108 109 109 def _update_status(**fields) -> None: ··· 227 227 """Call cortex_request with retries on Callosum send failure. 228 228 229 229 Retries up to len(_SEND_RETRY_DELAYS) times with short sleeps in between. 230 - Returns the agent_id on success, or None if all attempts failed. 230 + Returns the use_id on success, or None if all attempts failed. 231 231 """ 232 - agent_id = cortex_request(**kwargs) 233 - if agent_id is not None: 234 - return agent_id 232 + use_id = cortex_request(**kwargs) 233 + if use_id is not None: 234 + return use_id 235 235 236 236 name = kwargs.get("name", "unknown") 237 237 for i, delay in enumerate(_SEND_RETRY_DELAYS, 1): 238 238 logging.warning("Retrying cortex request for '%s' (attempt %d)", name, i + 1) 239 239 time.sleep(delay) 240 - agent_id = cortex_request(**kwargs) 241 - if agent_id is not None: 242 - return agent_id 240 + use_id = cortex_request(**kwargs) 241 + if use_id is not None: 242 + return use_id 243 243 244 244 logging.error("All cortex request attempts failed for '%s'", name) 245 245 return None ··· 259 259 emits completion events, and runs incremental indexing for generators. 260 260 261 261 Args: 262 - spawned: List of (agent_id, prompt_name, config, facet) tuples 262 + spawned: List of (use_id, prompt_name, config, facet) tuples 263 263 target_schedule: "segment" or "daily" 264 264 day: Day in YYYYMMDD format 265 265 segment: Optional segment key ··· 273 273 if not spawned: 274 274 return (0, 0, []) 275 275 276 - agent_ids = [agent_id for agent_id, _, _, _ in spawned] 276 + agent_ids = [use_id for use_id, _, _, _ in spawned] 277 277 logging.info(f"Waiting for {len(agent_ids)} agents...") 278 278 279 - completed, timed_out = wait_for_agents(agent_ids, timeout=timeout) 279 + completed, timed_out = wait_for_uses(agent_ids, timeout=timeout) 280 280 281 281 success = 0 282 282 failed = 0 ··· 285 285 if timed_out: 286 286 logging.warning(f"{len(timed_out)} agents timed out: {timed_out}") 287 287 failed += len(timed_out) 288 - for agent_id in timed_out: 288 + for use_id in timed_out: 289 289 timed_name = next( 290 - (n for aid, n, _, _ in spawned if aid == agent_id), "unknown" 290 + (n for aid, n, _, _ in spawned if aid == use_id), "unknown" 291 291 ) 292 - timed_facet = next((f for aid, _, _, f in spawned if aid == agent_id), None) 292 + timed_facet = next((f for aid, _, _, f in spawned if aid == use_id), None) 293 293 label = f"{timed_name}/{timed_facet}" if timed_facet else timed_name 294 294 failed_names.append(f"{label} (timeout)") 295 295 emit( 296 - "agent_completed", 296 + "talent_completed", 297 297 mode=target_schedule, 298 298 day=day, 299 299 segment=segment, 300 300 name=timed_name, 301 - agent_id=agent_id, 301 + use_id=use_id, 302 302 state="timeout", 303 303 **({"facet": timed_facet} if timed_facet else {}), 304 304 ) 305 305 _jsonl_log( 306 - "agent.fail", 306 + "talent.fail", 307 307 mode=target_schedule, 308 308 day=day, 309 309 segment=segment, 310 310 name=timed_name, 311 - agent_id=agent_id, 311 + use_id=use_id, 312 312 state="timeout", 313 313 **({"facet": timed_facet} if timed_facet else {}), 314 314 ) 315 315 316 - for agent_id, prompt_name, config, agent_facet in spawned: 317 - if agent_id in timed_out: 316 + for use_id, prompt_name, config, agent_facet in spawned: 317 + if use_id in timed_out: 318 318 continue 319 319 320 - end_state = completed.get(agent_id, "unknown") 320 + end_state = completed.get(use_id, "unknown") 321 321 if end_state == "finish": 322 322 logging.info(f"{prompt_name} completed successfully") 323 323 success += 1 324 324 emit( 325 - "agent_completed", 325 + "talent_completed", 326 326 mode=target_schedule, 327 327 day=day, 328 328 segment=segment, 329 329 name=prompt_name, 330 - agent_id=agent_id, 330 + use_id=use_id, 331 331 state="finish", 332 332 **({"facet": agent_facet} if agent_facet else {}), 333 333 ) 334 334 _jsonl_log( 335 - "agent.complete", 335 + "talent.complete", 336 336 mode=target_schedule, 337 337 day=day, 338 338 segment=segment, 339 339 name=prompt_name, 340 - agent_id=agent_id, 340 + use_id=use_id, 341 341 state="finish", 342 342 **({"facet": agent_facet} if agent_facet else {}), 343 343 ) ··· 368 368 failed += 1 369 369 failed_names.append(f"{label} ({end_state})") 370 370 emit( 371 - "agent_completed", 371 + "talent_completed", 372 372 mode=target_schedule, 373 373 day=day, 374 374 segment=segment, 375 375 name=prompt_name, 376 - agent_id=agent_id, 376 + use_id=use_id, 377 377 state=end_state, 378 378 **({"facet": agent_facet} if agent_facet else {}), 379 379 ) 380 380 _jsonl_log( 381 - "agent.fail", 381 + "talent.fail", 382 382 mode=target_schedule, 383 383 day=day, 384 384 segment=segment, 385 385 name=prompt_name, 386 - agent_id=agent_id, 386 + use_id=use_id, 387 387 state=end_state, 388 388 **({"facet": agent_facet} if agent_facet else {}), 389 389 ) ··· 568 568 return (0, 1, ["sense (send)"]) 569 569 570 570 emit( 571 - "agent_started", 571 + "talent_started", 572 572 mode=target_schedule, 573 573 day=day, 574 574 segment=segment, 575 575 name="sense", 576 - agent_id=sense_agent_id, 576 + use_id=sense_agent_id, 577 577 ) 578 578 _jsonl_log( 579 - "agent.dispatch", 579 + "talent.dispatch", 580 580 mode=target_schedule, 581 581 day=day, 582 582 segment=segment, 583 583 name="sense", 584 - agent_id=sense_agent_id, 584 + use_id=sense_agent_id, 585 585 ) 586 586 _update_status(current_agents=["sense"]) 587 587 ··· 822 822 823 823 spawned: list[tuple[str, str, dict, str | None]] = [] 824 824 for agent_name, config in agents_to_run: 825 - agent_id = _dispatch_agent(agent_name, config) 826 - if agent_id is None: 825 + use_id = _dispatch_agent(agent_name, config) 826 + if use_id is None: 827 827 _log_skip( 828 828 agent_name, 829 829 "send_failed", ··· 837 837 _update_status(agents_completed=total_success + total_failed) 838 838 continue 839 839 840 - spawned.append((agent_id, agent_name, config, None)) 840 + spawned.append((use_id, agent_name, config, None)) 841 841 emit( 842 - "agent_started", 842 + "talent_started", 843 843 mode=target_schedule, 844 844 day=day, 845 845 segment=segment, 846 846 name=agent_name, 847 - agent_id=agent_id, 847 + use_id=use_id, 848 848 ) 849 849 _jsonl_log( 850 - "agent.dispatch", 850 + "talent.dispatch", 851 851 mode=target_schedule, 852 852 day=day, 853 853 segment=segment, 854 854 name=agent_name, 855 - agent_id=agent_id, 855 + use_id=use_id, 856 856 ) 857 857 858 858 if max_concurrency and len(spawned) >= max_concurrency: ··· 971 971 _update_status(agents_completed=total_success + total_failed) 972 972 else: 973 973 emit( 974 - "agent_started", 974 + "talent_started", 975 975 mode=target_schedule, 976 976 day=day, 977 977 segment=segment, 978 978 name="awareness_tender", 979 - agent_id=at_agent_id, 979 + use_id=at_agent_id, 980 980 ) 981 981 _jsonl_log( 982 - "agent.dispatch", 982 + "talent.dispatch", 983 983 mode=target_schedule, 984 984 day=day, 985 985 segment=segment, 986 986 name="awareness_tender", 987 - agent_id=at_agent_id, 987 + use_id=at_agent_id, 988 988 ) 989 989 _update_status(current_agents=["awareness_tender"]) 990 990 s, f, fn = _drain_priority_batch( ··· 1019 1019 _update_status(agents_completed=total_success + total_failed) 1020 1020 else: 1021 1021 emit( 1022 - "agent_started", 1022 + "talent_started", 1023 1023 mode=target_schedule, 1024 1024 day=day, 1025 1025 segment=segment, 1026 1026 name="pulse", 1027 - agent_id=pulse_agent_id, 1027 + use_id=pulse_agent_id, 1028 1028 ) 1029 1029 _jsonl_log( 1030 - "agent.dispatch", 1030 + "talent.dispatch", 1031 1031 mode=target_schedule, 1032 1032 day=day, 1033 1033 segment=segment, 1034 1034 name="pulse", 1035 - agent_id=pulse_agent_id, 1035 + use_id=pulse_agent_id, 1036 1036 ) 1037 1037 _update_status(current_agents=["pulse"]) 1038 1038 s, f, fn = _drain_priority_batch( ··· 1187 1187 1188 1188 spawned: list[ 1189 1189 tuple[str, str, dict, str | None] 1190 - ] = [] # (agent_id, name, config, facet) 1190 + ] = [] # (use_id, name, config, facet) 1191 1191 group_success = 0 1192 1192 group_failed = 0 1193 1193 ··· 1254 1254 else f"Processing facet '{facet_name}' for {day_formatted}: {input_summary}. Use get_facet('{facet_name}') to load context." 1255 1255 ) 1256 1256 1257 - agent_id = _cortex_request_with_retry( 1257 + use_id = _cortex_request_with_retry( 1258 1258 prompt=prompt, 1259 1259 name=prompt_name, 1260 1260 config=request_config, 1261 1261 ) 1262 - if agent_id is None: 1262 + if use_id is None: 1263 1263 _log_skip( 1264 1264 prompt_name, 1265 1265 "send_failed", ··· 1273 1273 f"{prompt_name}/{facet_name} (send)" 1274 1274 ) 1275 1275 continue 1276 - spawned.append((agent_id, prompt_name, config, facet_name)) 1276 + spawned.append((use_id, prompt_name, config, facet_name)) 1277 1277 emit( 1278 - "agent_started", 1278 + "talent_started", 1279 1279 mode=target_schedule, 1280 1280 day=day, 1281 1281 name=prompt_name, 1282 - agent_id=agent_id, 1282 + use_id=use_id, 1283 1283 facet=facet_name, 1284 1284 ) 1285 1285 _jsonl_log( 1286 - "agent.dispatch", 1286 + "talent.dispatch", 1287 1287 mode=target_schedule, 1288 1288 day=day, 1289 1289 name=prompt_name, 1290 - agent_id=agent_id, 1290 + use_id=use_id, 1291 1291 facet=facet_name, 1292 1292 ) 1293 1293 logging.info( 1294 - f"Started {prompt_name} for {facet_name} (ID: {agent_id})" 1294 + f"Started {prompt_name} for {facet_name} (ID: {use_id})" 1295 1295 ) 1296 1296 1297 1297 # Drain batch when concurrency limit reached ··· 1338 1338 else f"Running scheduled task for {day_formatted}: {input_summary}." 1339 1339 ) 1340 1340 1341 - agent_id = _cortex_request_with_retry( 1341 + use_id = _cortex_request_with_retry( 1342 1342 prompt=prompt, 1343 1343 name=prompt_name, 1344 1344 config=request_config, 1345 1345 ) 1346 - if agent_id is None: 1346 + if use_id is None: 1347 1347 _log_skip( 1348 1348 prompt_name, 1349 1349 "send_failed", ··· 1354 1354 group_failed += 1 1355 1355 all_failed_names.append(f"{prompt_name} (send)") 1356 1356 continue 1357 - spawned.append((agent_id, prompt_name, config, None)) 1357 + spawned.append((use_id, prompt_name, config, None)) 1358 1358 emit( 1359 - "agent_started", 1359 + "talent_started", 1360 1360 mode=target_schedule, 1361 1361 day=day, 1362 1362 name=prompt_name, 1363 - agent_id=agent_id, 1363 + use_id=use_id, 1364 1364 ) 1365 1365 _jsonl_log( 1366 - "agent.dispatch", 1366 + "talent.dispatch", 1367 1367 mode=target_schedule, 1368 1368 day=day, 1369 1369 name=prompt_name, 1370 - agent_id=agent_id, 1370 + use_id=use_id, 1371 1371 ) 1372 - logging.info(f"Started {prompt_name} (ID: {agent_id})") 1372 + logging.info(f"Started {prompt_name} (ID: {use_id})") 1373 1373 1374 1374 # Drain batch when concurrency limit reached 1375 1375 if max_concurrency and len(spawned) >= max_concurrency: ··· 1543 1543 1544 1544 spawned: list[ 1545 1545 tuple[str, str, dict, str | None] 1546 - ] = [] # (agent_id, name, config, facet) 1546 + ] = [] # (use_id, name, config, facet) 1547 1547 group_success = 0 1548 1548 group_failed = 0 1549 1549 ··· 1610 1610 else f"Processing facet '{facet_name}' for {day_formatted}: {input_summary}. Use get_facet('{facet_name}') to load context." 1611 1611 ) 1612 1612 1613 - agent_id = _cortex_request_with_retry( 1613 + use_id = _cortex_request_with_retry( 1614 1614 prompt=prompt, 1615 1615 name=prompt_name, 1616 1616 config=request_config, 1617 1617 ) 1618 - if agent_id is None: 1618 + if use_id is None: 1619 1619 _log_skip( 1620 1620 prompt_name, 1621 1621 "send_failed", ··· 1629 1629 f"{prompt_name}/{facet_name} (send)" 1630 1630 ) 1631 1631 continue 1632 - spawned.append((agent_id, prompt_name, config, facet_name)) 1632 + spawned.append((use_id, prompt_name, config, facet_name)) 1633 1633 emit( 1634 - "agent_started", 1634 + "talent_started", 1635 1635 mode=target_schedule, 1636 1636 day=day, 1637 1637 name=prompt_name, 1638 - agent_id=agent_id, 1638 + use_id=use_id, 1639 1639 facet=facet_name, 1640 1640 ) 1641 1641 _jsonl_log( 1642 - "agent.dispatch", 1642 + "talent.dispatch", 1643 1643 mode=target_schedule, 1644 1644 day=day, 1645 1645 name=prompt_name, 1646 - agent_id=agent_id, 1646 + use_id=use_id, 1647 1647 facet=facet_name, 1648 1648 ) 1649 1649 logging.info( 1650 - f"Started {prompt_name} for {facet_name} (ID: {agent_id})" 1650 + f"Started {prompt_name} for {facet_name} (ID: {use_id})" 1651 1651 ) 1652 1652 1653 1653 # Drain batch when concurrency limit reached ··· 1694 1694 else f"Running scheduled task for {day_formatted}: {input_summary}." 1695 1695 ) 1696 1696 1697 - agent_id = _cortex_request_with_retry( 1697 + use_id = _cortex_request_with_retry( 1698 1698 prompt=prompt, 1699 1699 name=prompt_name, 1700 1700 config=request_config, 1701 1701 ) 1702 - if agent_id is None: 1702 + if use_id is None: 1703 1703 _log_skip( 1704 1704 prompt_name, 1705 1705 "send_failed", ··· 1710 1710 group_failed += 1 1711 1711 all_failed_names.append(f"{prompt_name} (send)") 1712 1712 continue 1713 - spawned.append((agent_id, prompt_name, config, None)) 1713 + spawned.append((use_id, prompt_name, config, None)) 1714 1714 emit( 1715 - "agent_started", 1715 + "talent_started", 1716 1716 mode=target_schedule, 1717 1717 day=day, 1718 1718 name=prompt_name, 1719 - agent_id=agent_id, 1719 + use_id=use_id, 1720 1720 ) 1721 1721 _jsonl_log( 1722 - "agent.dispatch", 1722 + "talent.dispatch", 1723 1723 mode=target_schedule, 1724 1724 day=day, 1725 1725 name=prompt_name, 1726 - agent_id=agent_id, 1726 + use_id=use_id, 1727 1727 ) 1728 - logging.info(f"Started {prompt_name} (ID: {agent_id})") 1728 + logging.info(f"Started {prompt_name} (ID: {use_id})") 1729 1729 1730 1730 # Drain batch when concurrency limit reached 1731 1731 if max_concurrency and len(spawned) >= max_concurrency: ··· 1942 1942 count=len(prompts_list), 1943 1943 ) 1944 1944 1945 - spawned: list[tuple[str, str, dict]] = [] # (agent_id, name, config) 1945 + spawned: list[tuple[str, str, dict]] = [] # (use_id, name, config) 1946 1946 group_success = 0 1947 1947 group_failed = 0 1948 1948 ··· 1955 1955 agent_ids = [aid for aid, _, _ in spawned] 1956 1956 logging.info(f"Waiting for {len(agent_ids)} agents...") 1957 1957 1958 - completed, timed_out = wait_for_agents(agent_ids, timeout=610) 1958 + completed, timed_out = wait_for_uses(agent_ids, timeout=610) 1959 1959 1960 1960 if timed_out: 1961 1961 logging.warning(f"{len(timed_out)} agents timed out") 1962 1962 group_failed += len(timed_out) 1963 - for agent_id in timed_out: 1963 + for use_id in timed_out: 1964 1964 timed_name = next( 1965 - (n for aid, n, _ in spawned if aid == agent_id), "unknown" 1965 + (n for aid, n, _ in spawned if aid == use_id), "unknown" 1966 1966 ) 1967 1967 emit( 1968 - "agent_completed", 1968 + "talent_completed", 1969 1969 mode="activity", 1970 1970 day=day, 1971 1971 activity=activity_id, 1972 1972 facet=facet, 1973 1973 name=timed_name, 1974 - agent_id=agent_id, 1974 + use_id=use_id, 1975 1975 state="timeout", 1976 1976 ) 1977 1977 _jsonl_log( 1978 - "agent.fail", 1978 + "talent.fail", 1979 1979 mode="activity", 1980 1980 day=day, 1981 1981 activity=activity_id, 1982 1982 facet=facet, 1983 1983 name=timed_name, 1984 - agent_id=agent_id, 1984 + use_id=use_id, 1985 1985 state="timeout", 1986 1986 ) 1987 1987 1988 - for agent_id, prompt_name, config in spawned: 1989 - if agent_id in timed_out: 1988 + for use_id, prompt_name, config in spawned: 1989 + if use_id in timed_out: 1990 1990 continue 1991 1991 1992 - end_state = completed.get(agent_id, "unknown") 1992 + end_state = completed.get(use_id, "unknown") 1993 1993 if end_state == "finish": 1994 1994 logging.info(f"{prompt_name} completed successfully") 1995 1995 group_success += 1 ··· 2017 2017 group_failed += 1 2018 2018 2019 2019 emit( 2020 - "agent_completed", 2020 + "talent_completed", 2021 2021 mode="activity", 2022 2022 day=day, 2023 2023 activity=activity_id, 2024 2024 facet=facet, 2025 2025 name=prompt_name, 2026 - agent_id=agent_id, 2026 + use_id=use_id, 2027 2027 state=end_state, 2028 2028 ) 2029 2029 _jsonl_log( 2030 - "agent.complete" if end_state == "finish" else "agent.fail", 2030 + "talent.complete" if end_state == "finish" else "talent.fail", 2031 2031 mode="activity", 2032 2032 day=day, 2033 2033 activity=activity_id, 2034 2034 facet=facet, 2035 2035 name=prompt_name, 2036 - agent_id=agent_id, 2036 + use_id=use_id, 2037 2037 state=end_state, 2038 2038 ) 2039 2039 ··· 2078 2078 else f"Processing activity '{activity_id}' ({activity_type}) in facet '{facet}' for {day_formatted}." 2079 2079 ) 2080 2080 2081 - agent_id = _cortex_request_with_retry( 2081 + use_id = _cortex_request_with_retry( 2082 2082 prompt=prompt, 2083 2083 name=prompt_name, 2084 2084 config=request_config, 2085 2085 ) 2086 - if agent_id is None: 2086 + if use_id is None: 2087 2087 _log_skip( 2088 2088 prompt_name, 2089 2089 "send_failed", ··· 2095 2095 ) 2096 2096 total_failed += 1 2097 2097 continue 2098 - spawned.append((agent_id, prompt_name, config)) 2098 + spawned.append((use_id, prompt_name, config)) 2099 2099 emit( 2100 - "agent_started", 2100 + "talent_started", 2101 2101 mode="activity", 2102 2102 day=day, 2103 2103 activity=activity_id, 2104 2104 facet=facet, 2105 2105 name=prompt_name, 2106 - agent_id=agent_id, 2106 + use_id=use_id, 2107 2107 ) 2108 2108 _jsonl_log( 2109 - "agent.dispatch", 2109 + "talent.dispatch", 2110 2110 mode="activity", 2111 2111 day=day, 2112 2112 activity=activity_id, 2113 2113 facet=facet, 2114 2114 name=prompt_name, 2115 - agent_id=agent_id, 2115 + use_id=use_id, 2116 2116 ) 2117 - logging.info(f"Started {prompt_name} (ID: {agent_id})") 2117 + logging.info(f"Started {prompt_name} (ID: {use_id})") 2118 2118 2119 2119 # Drain batch when concurrency limit reached 2120 2120 if max_concurrency and len(spawned) >= max_concurrency: ··· 2234 2234 total_success = 0 2235 2235 total_failed = 0 2236 2236 2237 - spawned: list[tuple[str, str, dict]] = [] # (agent_id, name, config) 2237 + spawned: list[tuple[str, str, dict]] = [] # (use_id, name, config) 2238 2238 _update_status( 2239 2239 mode="flush", 2240 2240 day=day, ··· 2268 2268 if is_generate: 2269 2269 request_config["output"] = config.get("output", "md") 2270 2270 2271 - agent_id = _cortex_request_with_retry( 2271 + use_id = _cortex_request_with_retry( 2272 2272 prompt="", 2273 2273 name=prompt_name, 2274 2274 config=request_config, 2275 2275 ) 2276 - if agent_id is None: 2276 + if use_id is None: 2277 2277 _log_skip( 2278 2278 prompt_name, 2279 2279 "send_failed", ··· 2284 2284 ) 2285 2285 total_failed += 1 2286 2286 continue 2287 - spawned.append((agent_id, prompt_name, config)) 2287 + spawned.append((use_id, prompt_name, config)) 2288 2288 emit( 2289 - "agent_started", 2289 + "talent_started", 2290 2290 mode="flush", 2291 2291 day=day, 2292 2292 segment=segment, 2293 2293 name=prompt_name, 2294 - agent_id=agent_id, 2294 + use_id=use_id, 2295 2295 ) 2296 2296 _jsonl_log( 2297 - "agent.dispatch", 2297 + "talent.dispatch", 2298 2298 mode="flush", 2299 2299 day=day, 2300 2300 segment=segment, 2301 2301 name=prompt_name, 2302 - agent_id=agent_id, 2302 + use_id=use_id, 2303 2303 ) 2304 - logging.info(f"Started flush agent {prompt_name} (ID: {agent_id})") 2304 + logging.info(f"Started flush agent {prompt_name} (ID: {use_id})") 2305 2305 2306 2306 except Exception as e: 2307 2307 logging.error(f"Failed to spawn flush agent {prompt_name}: {e}") ··· 2310 2310 if spawned: 2311 2311 _update_status(current_agents=[name for _, name, _ in spawned]) 2312 2312 agent_ids = [aid for aid, _, _ in spawned] 2313 - completed, timed_out = wait_for_agents(agent_ids, timeout=610) 2313 + completed, timed_out = wait_for_uses(agent_ids, timeout=610) 2314 2314 2315 2315 if timed_out: 2316 2316 logging.warning(f"Flush: {len(timed_out)} agents timed out") 2317 2317 total_failed += len(timed_out) 2318 - for agent_id in timed_out: 2318 + for use_id in timed_out: 2319 2319 timed_name = next( 2320 - (n for aid, n, _ in spawned if aid == agent_id), "unknown" 2320 + (n for aid, n, _ in spawned if aid == use_id), "unknown" 2321 2321 ) 2322 2322 _jsonl_log( 2323 - "agent.fail", 2323 + "talent.fail", 2324 2324 mode="flush", 2325 2325 day=day, 2326 2326 segment=segment, 2327 2327 name=timed_name, 2328 - agent_id=agent_id, 2328 + use_id=use_id, 2329 2329 state="timeout", 2330 2330 ) 2331 2331 2332 - for agent_id, prompt_name, config in spawned: 2333 - if agent_id in timed_out: 2332 + for use_id, prompt_name, config in spawned: 2333 + if use_id in timed_out: 2334 2334 continue 2335 - end_state = completed.get(agent_id, "unknown") 2335 + end_state = completed.get(use_id, "unknown") 2336 2336 if end_state == "finish": 2337 2337 logging.info(f"Flush agent {prompt_name} completed") 2338 2338 total_success += 1 ··· 2343 2343 total_failed += 1 2344 2344 2345 2345 emit( 2346 - "agent_completed", 2346 + "talent_completed", 2347 2347 mode="flush", 2348 2348 day=day, 2349 2349 segment=segment, 2350 2350 name=prompt_name, 2351 - agent_id=agent_id, 2351 + use_id=use_id, 2352 2352 state=end_state, 2353 2353 ) 2354 2354 _jsonl_log( 2355 - "agent.complete" if end_state == "finish" else "agent.fail", 2355 + "talent.complete" if end_state == "finish" else "talent.fail", 2356 2356 mode="flush", 2357 2357 day=day, 2358 2358 segment=segment, 2359 2359 name=prompt_name, 2360 - agent_id=agent_id, 2360 + use_id=use_id, 2361 2361 state=end_state, 2362 2362 ) 2363 2363 _update_status(
+9 -9
think/engage.py
··· 32 32 33 33 from think.cortex_client import cortex_request 34 34 35 - agent_id = cortex_request(prompt=prompt, name=name, config=config) 36 - if agent_id is None: 35 + use_id = cortex_request(prompt=prompt, name=name, config=config) 36 + if use_id is None: 37 37 typer.echo("Error: failed to send cortex request.", err=True) 38 38 raise typer.Exit(1) 39 39 40 40 if not wait: 41 - typer.echo(agent_id) 41 + typer.echo(use_id) 42 42 return 43 43 44 - from think.cortex_client import read_agent_events, wait_for_agents 44 + from think.cortex_client import read_use_events, wait_for_uses 45 45 46 - completed, timed_out = wait_for_agents([agent_id]) 47 - if agent_id in timed_out: 46 + completed, timed_out = wait_for_uses([use_id]) 47 + if use_id in timed_out: 48 48 typer.echo("Error: agent timed out.", err=True) 49 49 raise typer.Exit(1) 50 50 51 - end_state = completed.get(agent_id, "error") 51 + end_state = completed.get(use_id, "error") 52 52 if end_state != "finish": 53 53 typer.echo(f"Error: agent ended with state: {end_state}", err=True) 54 54 raise typer.Exit(1) 55 55 56 - events = read_agent_events(agent_id) 56 + events = read_use_events(use_id) 57 57 result = "" 58 58 for event in reversed(events): 59 59 if event.get("event") == "finish": ··· 81 81 """Delegate work to a cogitate agent. 82 82 83 83 Reads a prompt from stdin, sends it to cortex as an agent request. 84 - By default, prints the agent_id and exits immediately (fire-and-forget). 84 + By default, prints the use_id and exits immediately (fire-and-forget). 85 85 86 86 Example:: 87 87
+1 -1
think/entities/activity.py
··· 80 80 >>> parse_knowledge_graph_entities("20260108") 81 81 ["Jeremie Miller (Jer)", "Neal Satterfield", "Flightline", ...] 82 82 """ 83 - kg_path = day_path(day, create=False) / "agents" / "knowledge_graph.md" 83 + kg_path = day_path(day, create=False) / "talents" / "knowledge_graph.md" 84 84 85 85 if not kg_path.exists(): 86 86 return []
+1 -1
think/entities/context.py
··· 38 38 39 39 40 40 def _load_knowledge_graph(day: str) -> str: 41 - kg_path = day_path(day, create=False) / "agents" / "knowledge_graph.md" 41 + kg_path = day_path(day, create=False) / "talents" / "knowledge_graph.md" 42 42 if not kg_path.exists(): 43 43 return "No knowledge graph available for this day." 44 44
+1 -1
think/events.py
··· 129 129 # For anticipations, show when it was created (from source path) 130 130 if not occurred: 131 131 source = event.get("source", "") 132 - # Extract YYYYMMDD from source path like "20240101/agents/schedule.md" 132 + # Extract YYYYMMDD from source path like "20240101/talents/schedule.md" 133 133 source_match = re.match(r"(\d{8})/", source) 134 134 if source_match: 135 135 created_day = source_match.group(1)
+11 -11
think/facets.py
··· 119 119 source: str, 120 120 actor: str, 121 121 day: str | None = None, 122 - agent_id: str | None = None, 122 + use_id: str | None = None, 123 123 ) -> None: 124 124 """Write action to the daily audit log. 125 125 ··· 136 136 source: Origin type - "tool" for agents, "app" for web UI 137 137 actor: For tools: agent name. For apps: app name 138 138 day: Day in YYYYMMDD format (defaults to today) 139 - agent_id: Optional agent ID (only for tool actions) 139 + use_id: Optional agent ID (only for tool actions) 140 140 """ 141 141 journal = get_journal() 142 142 ··· 165 165 if facet is not None: 166 166 entry["facet"] = facet 167 167 168 - # Add agent_id only if available 169 - if agent_id is not None: 170 - entry["agent_id"] = agent_id 168 + # Add use_id only if available 169 + if use_id is not None: 170 + entry["use_id"] = use_id 171 171 172 172 # Append to log file 173 173 with open(log_path, "a", encoding="utf-8") as f: ··· 476 476 List of facet ID strings found in the segment's facets.json 477 477 """ 478 478 if stream: 479 - candidates = [day_path(day) / stream / segment / "agents" / "facets.json"] 479 + candidates = [day_path(day) / stream / segment / "talents" / "facets.json"] 480 480 else: 481 481 # Search all streams for this segment 482 482 candidates = [] 483 483 for _s, seg_key, seg_path in iter_segments(day): 484 484 if seg_key == segment: 485 - candidates.append(seg_path / "agents" / "facets.json") 485 + candidates.append(seg_path / "talents" / "facets.json") 486 486 487 487 for facets_file in candidates: 488 488 if not facets_file.exists(): ··· 564 564 565 565 for day in scan_days: 566 566 for _stream, _seg_key, seg_path in iter_segments(day): 567 - facets_file = seg_path / "agents" / "facets.json" 567 + facets_file = seg_path / "talents" / "facets.json" 568 568 if not facets_file.exists(): 569 569 continue 570 570 ··· 1066 1066 source = entry.get("source", "unknown") 1067 1067 actor = entry.get("actor", "unknown") 1068 1068 params = entry.get("params", {}) 1069 - agent_id = entry.get("agent_id") 1069 + use_id = entry.get("use_id") 1070 1070 1071 1071 # Format action name for display (e.g., "todo_add" -> "Todo Add") 1072 1072 action_display = action.replace("_", " ").title() ··· 1081 1081 lines.append(" | ".join(meta_parts)) 1082 1082 1083 1083 # Agent link if present 1084 - if agent_id: 1085 - lines.append(f"**Agent:** [{agent_id}](/app/sol/{agent_id})") 1084 + if use_id: 1085 + lines.append(f"**Talent:** [{use_id}](/app/sol/{use_id})") 1086 1086 1087 1087 lines.append("") 1088 1088
+12 -12
think/formatters.py
··· 51 51 by the formatter via meta["indexer"]["agent"]. 52 52 53 53 Args: 54 - rel_path: Journal-relative path (e.g., "20240101/agents/flow.md") 54 + rel_path: Journal-relative path (e.g., "20240101/talents/flow.md") 55 55 56 56 Returns: 57 57 Dict with keys: day, facet, agent ··· 72 72 if parts[0] and DATE_RE.fullmatch(parts[0]): 73 73 day = parts[0] 74 74 75 - # Extract facet from agents/{facet}/... paths 75 + # Extract facet from talents/{facet}/... paths 76 76 try: 77 - agents_idx = parts.index("agents") 78 - if agents_idx + 2 < len(parts): 79 - facet = parts[agents_idx + 1] 77 + talents_idx = parts.index("talents") 78 + if talents_idx + 2 < len(parts): 79 + facet = parts[talents_idx + 1] 80 80 except ValueError: 81 81 pass 82 82 ··· 191 191 "*/*/*/*_transcript.jsonl": ("observe.hear", "format_audio", False), 192 192 "*/*/*/screen.jsonl": ("observe.screen", "format_screen", False), 193 193 "*/*/*/*_screen.jsonl": ("observe.screen", "format_screen", False), 194 - # Markdown — day-level agents output and segment-level (day/stream/segment/agents/) 195 - "*/agents/*.md": ("think.markdown", "format_markdown", True), 196 - # Layout: day/stream/segment/agents/*.md 197 - "*/*/*/agents/*.md": ("think.markdown", "format_markdown", True), 198 - "*/*/*/agents/*/*.md": ("think.markdown", "format_markdown", True), 194 + # Markdown — day-level agents output and segment-level (day/stream/segment/talents/) 195 + "*/talents/*.md": ("think.markdown", "format_markdown", True), 196 + # Layout: day/stream/segment/talents/*.md 197 + "*/*/*/talents/*.md": ("think.markdown", "format_markdown", True), 198 + "*/*/*/talents/*/*.md": ("think.markdown", "format_markdown", True), 199 199 "facets/*/activities/*/*/*.md": ("think.markdown", "format_markdown", True), 200 200 "facets/*/news/*.md": ("think.markdown", "format_markdown", True), 201 201 "imports/*/summary.md": ("think.markdown", "format_markdown", True), 202 - "apps/*/agents/*.md": ("think.markdown", "format_markdown", True), 202 + "apps/*/talents/*.md": ("think.markdown", "format_markdown", True), 203 203 } 204 204 205 205 _DAY_ROOTED_PATTERNS = [p for p in FORMATTERS if p.startswith("*/")] ··· 212 212 Matches against registered glob patterns (regardless of indexed flag). 213 213 214 214 Args: 215 - file_path: Journal-relative path (e.g., "20240101/agents/flow.md") 215 + file_path: Journal-relative path (e.g., "20240101/talents/flow.md") 216 216 217 217 Returns: 218 218 Formatter function or None if no pattern matches
+7 -7
think/heartbeat.py
··· 14 14 from pathlib import Path 15 15 16 16 from think.awareness import ensure_sol_directory 17 - from think.cortex_client import cortex_request, wait_for_agents 17 + from think.cortex_client import cortex_request, wait_for_uses 18 18 from think.utils import get_journal, require_solstone, setup_cli 19 19 20 20 logger = logging.getLogger(__name__) ··· 104 104 pid_file.write_text(str(os.getpid())) 105 105 start_time = time.monotonic() 106 106 107 - agent_id = cortex_request( 107 + use_id = cortex_request( 108 108 prompt="Run heartbeat check.", 109 109 name="heartbeat", 110 110 ) 111 - if agent_id is None: 111 + if use_id is None: 112 112 logger.error("Failed to send heartbeat request to cortex") 113 113 _log_run(health_dir, start_time, "error") 114 114 sys.exit(1) 115 115 116 - logger.info("Heartbeat agent started (ID: %s)", agent_id) 116 + logger.info("Heartbeat agent started (ID: %s)", use_id) 117 117 118 118 # Wait for completion 119 - completed, timed_out = wait_for_agents([agent_id], timeout=600) 119 + completed, timed_out = wait_for_uses([use_id], timeout=600) 120 120 121 121 # Determine outcome 122 - if agent_id in timed_out: 122 + if use_id in timed_out: 123 123 logger.error("Heartbeat agent timed out") 124 124 _log_run(health_dir, start_time, "timeout") 125 125 sys.exit(2) 126 126 127 - end_state = completed.get(agent_id, "unknown") 127 + end_state = completed.get(use_id, "unknown") 128 128 if end_state == "finish": 129 129 logger.info("Heartbeat completed successfully") 130 130 _log_run(health_dir, start_time, "success")
+6 -6
think/hooks.py
··· 190 190 context: Hook context dict with day, segment, name, output_path, meta. 191 191 192 192 Returns: 193 - Relative path like "20240101/agents/meetings.md". 193 + Relative path like "20240101/talents/meetings.md". 194 194 """ 195 195 from think.talent import get_output_name 196 196 from think.utils import CHRONICLE_DIR, get_journal ··· 206 206 except ValueError: 207 207 segment = context.get("segment") 208 208 output_name = get_output_name(name) 209 - # Check for facet in meta (for multi-facet agents) 209 + # Check for facet in meta (for multi-facet talents) 210 210 meta = context.get("meta", {}) 211 211 facet = meta.get("facet") if meta else None 212 212 filename = f"{output_name}.md" 213 213 if segment and facet: 214 - return os.path.join(day, segment, "agents", facet, filename) 214 + return os.path.join(day, segment, "talents", facet, filename) 215 215 if segment: 216 - return os.path.join(day, segment, "agents", filename) 216 + return os.path.join(day, segment, "talents", filename) 217 217 if facet: 218 - return os.path.join(day, "agents", facet, filename) 219 - return os.path.join(day, "agents", filename) 218 + return os.path.join(day, "talents", facet, filename) 219 + return os.path.join(day, "talents", filename)
+9 -9
think/indexer/journal.py
··· 258 258 else journal_path 259 259 ) 260 260 261 - for path in day_root.glob("*/agents/knowledge_graph.md"): 261 + for path in day_root.glob("*/talents/knowledge_graph.md"): 262 262 if path.is_file(): 263 263 rel = path.relative_to(day_root).as_posix() 264 264 files[rel] = (str(path), "kg") ··· 972 972 """Extract stream name from a journal-relative path's segment directory. 973 973 974 974 Reads stream.json from the segment dir if the path is inside a segment 975 - (e.g., "20240101/142500_300/agents/facet/flow.md"). 975 + (e.g., "20240101/142500_300/talents/facet/flow.md"). 976 976 977 977 Returns stream name string or None for non-segment paths or pre-stream segments. 978 978 """ ··· 1052 1052 ) -> int: 1053 1053 """Index concatenated markdown content for one segment.""" 1054 1054 segment_path = Path(segment_dir) 1055 - agent_files = sorted( 1055 + talent_files = sorted( 1056 1056 [ 1057 - *segment_path.glob("agents/*.md"), 1058 - *segment_path.glob("agents/*/*.md"), 1057 + *segment_path.glob("talents/*.md"), 1058 + *segment_path.glob("talents/*/*.md"), 1059 1059 ], 1060 1060 key=lambda path: str(path), 1061 1061 ) 1062 - if not agent_files: 1062 + if not talent_files: 1063 1063 return 0 1064 1064 1065 1065 content = "\n\n---\n\n".join( 1066 - path.read_text(encoding="utf-8") for path in agent_files 1066 + path.read_text(encoding="utf-8") for path in talent_files 1067 1067 ) 1068 1068 chunks, _meta = format_markdown(content) 1069 1069 day = rel_segment.replace("\\", "/").split("/")[0] ··· 1324 1324 for path in db_signal_paths 1325 1325 if not _is_historical_signal_file( 1326 1326 path.split("#")[0], 1327 - "kg" if "/agents/knowledge_graph.md" in path else "event", 1327 + "kg" if "/talents/knowledge_graph.md" in path else "event", 1328 1328 ) 1329 1329 } 1330 1330 removed = {p for p in in_scope_db if p.split("#")[0] not in in_scope} ··· 1506 1506 1507 1507 # Collect all matching segment entity files across day/stream/segment dirs 1508 1508 segment_files = [] 1509 - for path in day_root.glob("**/agents/entities.jsonl"): 1509 + for path in day_root.glob("**/talents/entities.jsonl"): 1510 1510 if not path.is_file(): 1511 1511 continue 1512 1512 try:
+8 -8
think/journal_stats.py
··· 12 12 13 13 from observe.sense import scan_day as sense_scan_day 14 14 from observe.utils import VIDEO_EXTENSIONS, load_analysis_frames 15 - from think.agents import scan_day as generate_scan_day 16 15 from think.stats_schema import DAY_FIELDS, SCHEMA_VERSION 17 16 from think.stats_schema import validate as validate_stats 17 + from think.talents import scan_day as generate_scan_day 18 18 from think.utils import day_dirs, get_journal, setup_cli 19 19 20 20 logger = logging.getLogger(__name__) ··· 54 54 for ext in VIDEO_EXTENSIONS: 55 55 files.extend(day_dir.glob(f"*{ext}")) 56 56 57 - agents_dir = day_dir / "agents" 58 - if agents_dir.is_dir(): 59 - files.extend(agents_dir.glob("*.json")) 60 - files.extend(agents_dir.glob("*.md")) 61 - files.extend(agents_dir.glob("*/*.json")) 62 - files.extend(agents_dir.glob("*/*.md")) 57 + talents_dir = day_dir / "talents" 58 + if talents_dir.is_dir(): 59 + files.extend(talents_dir.glob("*.json")) 60 + files.extend(talents_dir.glob("*.md")) 61 + files.extend(talents_dir.glob("*/*.json")) 62 + files.extend(talents_dir.glob("*/*.md")) 63 63 64 64 # Check facet event files for this day 65 65 journal_root = Path(get_journal()) ··· 524 524 "by_day": self.token_usage, 525 525 "by_model": self.token_totals, 526 526 }, 527 - "agents": { 527 + "talents": { 528 528 "counts": dict(self.agent_counts), 529 529 "minutes": {k: round(v, 2) for k, v in self.agent_minutes.items()}, 530 530 "counts_by_day": self.agent_counts_by_day,
+1 -1
think/models.py
··· 1082 1082 """ 1083 1083 try: 1084 1084 subprocess.Popen( 1085 - ["sol", "agents", "check", "--targeted"], 1085 + ["sol", "providers", "check", "--targeted"], 1086 1086 stdout=subprocess.DEVNULL, 1087 1087 stderr=subprocess.DEVNULL, 1088 1088 )
+22 -21
think/pipeline_health.py
··· 28 28 "status": "healthy", 29 29 "anomalies": [], 30 30 "runs": {mode: {"count": 0, "duration_ms_total": 0} for mode in _MODES}, 31 - "agents": { 31 + "talents": { 32 32 "dispatched": 0, 33 33 "completed": 0, 34 34 "failed": 0, ··· 39 39 "activities": { 40 40 "detected": 0, 41 41 "persisted": 0, 42 - "agents_fired": False, 42 + "talents_fired": False, 43 43 }, 44 44 } 45 45 ··· 78 78 continue 79 79 80 80 event = rec["event"] 81 - if event == "agent.dispatch": 82 - summary["agents"]["dispatched"] += 1 83 - elif event == "agent.complete": 84 - summary["agents"]["completed"] += 1 85 - elif event == "agent.fail": 86 - summary["agents"]["failed"] += 1 87 - if len(summary["agents"]["failed_list"]) < _FAILED_LIST_CAP: 88 - summary["agents"]["failed_list"].append( 81 + # HISTORICAL SHIM: accept legacy agent.* chronicle event names from before 2026-04-17; sunset 2026-05-01 82 + if event in {"agent.dispatch", "talent.dispatch"}: 83 + summary["talents"]["dispatched"] += 1 84 + elif event in {"agent.complete", "talent.complete"}: 85 + summary["talents"]["completed"] += 1 86 + elif event in {"agent.fail", "talent.fail"}: 87 + summary["talents"]["failed"] += 1 88 + if len(summary["talents"]["failed_list"]) < _FAILED_LIST_CAP: 89 + summary["talents"]["failed_list"].append( 89 90 { 90 91 "mode": rec.get("mode") or mode, 91 92 "name": rec.get("name"), 92 - "agent_id": rec.get("agent_id"), 93 + "use_id": rec.get("use_id"), 93 94 "state": rec.get("state"), 94 95 } 95 96 ) 96 97 else: 97 - summary["agents"]["failed_list_truncated"] = True 98 - elif event == "agent.skip": 99 - summary["agents"]["skipped"] += 1 98 + summary["talents"]["failed_list_truncated"] = True 99 + elif event in {"agent.skip", "talent.skip"}: 100 + summary["talents"]["skipped"] += 1 100 101 elif event == "activity.detected": 101 102 summary["activities"]["detected"] += 1 102 103 elif event == "activity.persisted": ··· 110 111 elif ( 111 112 event == "run.start" and (rec.get("mode") or mode) == "activity" 112 113 ): 113 - summary["activities"]["agents_fired"] = True 114 + summary["activities"]["talents_fired"] = True 114 115 except Exception: 115 116 logger.warning( 116 117 "pipeline_health: unexpected error summarizing %s", ··· 119 120 ) 120 121 return summary 121 122 122 - for failure in summary["agents"]["failed_list"]: 123 - summary["anomalies"].append({"kind": "agent_failure", **failure}) 123 + for failure in summary["talents"]["failed_list"]: 124 + summary["anomalies"].append({"kind": "talent_failure", **failure}) 124 125 125 126 if ( 126 127 summary["activities"]["detected"] > 0 ··· 149 150 for anomaly in summary["anomalies"] 150 151 ) 151 152 has_failure = any( 152 - anomaly["kind"] == "agent_failure" for anomaly in summary["anomalies"] 153 + anomaly["kind"] == "talent_failure" for anomaly in summary["anomalies"] 153 154 ) 154 155 if has_stale: 155 156 summary["status"] = "stale" ··· 175 176 "status": "stale", 176 177 "message": "Daily processing hasn't run yet", 177 178 } 178 - if any(anomaly.get("kind") == "agent_failure" for anomaly in anomalies): 179 - count = summary.get("agents", {}).get("failed", 0) 179 + if any(anomaly.get("kind") == "talent_failure" for anomaly in anomalies): 180 + count = summary.get("talents", {}).get("failed", 0) 180 181 plural = "s" if count != 1 else "" 181 182 return { 182 183 "status": "warning", 183 - "message": f"{count} agent error{plural} today", 184 + "message": f"{count} talent error{plural} today", 184 185 } 185 186 return None
+1 -1
think/providers/anthropic.py
··· 4 4 5 5 """Anthropic Claude provider for agents and direct LLM generation. 6 6 7 - This module provides the Anthropic Claude provider for the ``sol agents`` CLI 7 + This module provides the Anthropic Claude provider for the ``sol providers check`` CLI 8 8 and run_generate/run_agenerate functions returning GenerateResult. 9 9 10 10 Common Parameters
+1 -1
think/providers/google.py
··· 4 4 5 5 """Gemini provider for agents and direct LLM generation. 6 6 7 - This module provides the Google Gemini provider for the ``sol agents`` CLI 7 + This module provides the Google Gemini provider for the ``sol providers check`` CLI 8 8 and run_generate/run_agenerate functions returning GenerateResult. 9 9 10 10 Common Parameters
+2 -2
think/providers/openai.py
··· 4 4 5 5 """OpenAI provider for agents and direct LLM generation. 6 6 7 - This module provides the OpenAI provider for the ``sol agents`` CLI 7 + This module provides the OpenAI provider for the ``sol providers check`` CLI 8 8 and run_generate/run_agenerate functions returning GenerateResult. 9 9 10 10 Common Parameters ··· 54 54 safe_raw, 55 55 ) 56 56 57 - # Agent configuration is now loaded via get_agent() in cortex.py 57 + # Agent configuration is now loaded via get_talent() in cortex.py 58 58 59 59 LOG = logging.getLogger("think.providers.openai") 60 60
+8 -8
think/providers/shared.py
··· 4 4 """Shared utilities and types for AI providers. 5 5 6 6 This module contains: 7 - - Event TypedDicts emitted by providers during agent execution 7 + - Event TypedDicts emitted by providers during talent execution 8 8 - GenerateResult TypedDict returned by run_generate/run_agenerate 9 9 - JSONEventCallback for event emission 10 10 - Utility functions for common provider operations ··· 48 48 49 49 50 50 class StartEvent(TypedDict, total=False): 51 - """Event emitted when an agent run begins.""" 51 + """Event emitted when a talent run begins.""" 52 52 53 53 event: Required[Literal["start"]] 54 54 ts: Required[int] ··· 62 62 63 63 64 64 class FinishEvent(TypedDict, total=False): 65 - """Event emitted when an agent run finishes successfully.""" 65 + """Event emitted when a talent run finishes successfully.""" 66 66 67 67 event: Required[Literal["finish"]] 68 68 ts: Required[int] ··· 82 82 raw: Optional[list[dict[str, Any]]] # Original provider JSON event(s) 83 83 84 84 85 - class AgentUpdatedEvent(TypedDict, total=False): 86 - """Event emitted when the agent context changes.""" 85 + class TalentUpdatedEvent(TypedDict, total=False): 86 + """Event emitted when the talent context changes.""" 87 87 88 - event: Required[Literal["agent_updated"]] 88 + event: Required[Literal["talent_updated"]] 89 89 ts: Required[int] 90 - agent: Required[str] 90 + talent: Required[str] 91 91 raw: Optional[list[dict[str, Any]]] # Original provider JSON event(s) 92 92 93 93 ··· 127 127 FinishEvent, 128 128 ErrorEvent, 129 129 ThinkingEvent, 130 - AgentUpdatedEvent, 130 + TalentUpdatedEvent, 131 131 FallbackEvent, 132 132 ] 133 133
+337
think/providers_cli.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """CLI entrypoint for provider connectivity checks.""" 5 + 6 + from __future__ import annotations 7 + 8 + import argparse 9 + import asyncio 10 + import json 11 + import os 12 + import shutil 13 + import sys 14 + import time 15 + from datetime import datetime, timezone 16 + from pathlib import Path 17 + 18 + from think.utils import get_journal, require_solstone, setup_cli 19 + 20 + 21 + def _check_generate(provider_name: str, tier: int, timeout: int) -> tuple[str, str]: 22 + """Check generate interface for a provider.""" 23 + from think.models import PROVIDER_DEFAULTS 24 + from think.providers import PROVIDER_METADATA, get_provider_module 25 + 26 + env_key = PROVIDER_METADATA[provider_name]["env_key"] 27 + if env_key and not os.getenv(env_key): 28 + label = PROVIDER_METADATA[provider_name]["label"] 29 + return "skip", f"{label} not configured (no {env_key})" 30 + 31 + if not env_key: 32 + from think.providers import validate_key 33 + 34 + result = validate_key(provider_name, "") 35 + if not result.get("valid"): 36 + return ( 37 + "skip", 38 + f"Ollama not reachable ({result.get('error', 'unreachable')})", 39 + ) 40 + 41 + try: 42 + module = get_provider_module(provider_name) 43 + model = PROVIDER_DEFAULTS[provider_name][tier] 44 + result = module.run_generate( 45 + contents="Say OK", 46 + model=model, 47 + temperature=0, 48 + max_output_tokens=16, 49 + system_instruction=None, 50 + json_output=False, 51 + thinking_budget=None, 52 + timeout_s=timeout, 53 + ) 54 + text = result.get("text", "") if isinstance(result, dict) else "" 55 + if text: 56 + usage = result.get("usage") if isinstance(result, dict) else None 57 + if usage: 58 + from think.models import log_token_usage 59 + 60 + log_token_usage( 61 + model=PROVIDER_DEFAULTS[provider_name][tier], 62 + usage=usage, 63 + context="health.check.generate", 64 + type="generate", 65 + ) 66 + return "ok", "OK" 67 + return "fail", "FAIL: empty response text" 68 + except Exception as exc: 69 + return "fail", f"FAIL: {exc}" 70 + 71 + 72 + async def _check_cogitate( 73 + provider_name: str, tier: int, timeout: int 74 + ) -> tuple[str, str]: 75 + """Check cogitate interface for a provider by running a real prompt.""" 76 + from think.models import PROVIDER_DEFAULTS 77 + from think.providers import PROVIDER_METADATA, get_provider_module 78 + 79 + env_key = PROVIDER_METADATA[provider_name]["env_key"] 80 + if env_key and not os.getenv(env_key): 81 + label = PROVIDER_METADATA[provider_name]["label"] 82 + return "skip", f"{label} not configured (no {env_key})" 83 + 84 + if not env_key: 85 + from think.providers import validate_key 86 + 87 + result = validate_key(provider_name, "") 88 + if not result.get("valid"): 89 + return ( 90 + "skip", 91 + f"Ollama not reachable ({result.get('error', 'unreachable')})", 92 + ) 93 + 94 + binary = PROVIDER_METADATA[provider_name].get("cogitate_cli", "") 95 + if binary and not shutil.which(binary): 96 + return "skip", f"{binary} CLI not installed" 97 + 98 + try: 99 + module = get_provider_module(provider_name) 100 + model = PROVIDER_DEFAULTS[provider_name][tier] 101 + config = {"prompt": "Say OK", "model": model} 102 + result = await asyncio.wait_for( 103 + module.run_cogitate(config=config, on_event=None), 104 + timeout=timeout, 105 + ) 106 + if result: 107 + return "ok", "OK" 108 + return "fail", "FAIL: empty response" 109 + except asyncio.TimeoutError: 110 + return "fail", f"FAIL: timed out after {timeout}s" 111 + except Exception as exc: 112 + return "fail", f"FAIL: {exc}" 113 + 114 + 115 + async def _run_check(args: argparse.Namespace) -> None: 116 + """Run connectivity checks against AI providers.""" 117 + from think.models import PROVIDER_DEFAULTS, TIER_FLASH, TIER_LITE, TIER_PRO 118 + from think.providers import PROVIDER_REGISTRY 119 + 120 + targeted_pairs = None 121 + if args.targeted and not args.provider and not args.tier: 122 + import fcntl 123 + 124 + from think.models import TYPE_DEFAULTS, get_backup_provider 125 + from think.utils import get_config 126 + 127 + targeted_pairs = set() 128 + config = get_config() 129 + providers_config = config.get("providers", {}) 130 + for talent_type, defaults in TYPE_DEFAULTS.items(): 131 + type_config = providers_config.get(talent_type, {}) 132 + provider = type_config.get("provider", defaults["provider"]) 133 + tier = type_config.get("tier", defaults["tier"]) 134 + targeted_pairs.add((provider, tier)) 135 + backup = get_backup_provider(talent_type) 136 + if backup: 137 + targeted_pairs.add((backup, tier)) 138 + 139 + lock_dir = Path(get_journal()) / "health" 140 + lock_dir.mkdir(parents=True, exist_ok=True) 141 + lock_fd = open(lock_dir / "recheck.lock", "w") 142 + try: 143 + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 144 + except OSError: 145 + lock_fd.close() 146 + return 147 + 148 + if args.provider: 149 + providers = args.provider 150 + for name in providers: 151 + if name not in PROVIDER_REGISTRY: 152 + available = ", ".join(PROVIDER_REGISTRY.keys()) 153 + print( 154 + f"Unknown provider: {name}. Available providers: {available}", 155 + file=sys.stderr, 156 + ) 157 + sys.exit(1) 158 + else: 159 + providers = list(PROVIDER_REGISTRY.keys()) 160 + 161 + interfaces = [args.interface] if args.interface else ["generate", "cogitate"] 162 + tier_names = {1: "pro", 2: "flash", 3: "lite"} 163 + tiers = [args.tier] if args.tier else [TIER_PRO, TIER_FLASH, TIER_LITE] 164 + 165 + provider_width = max(len(n) for n in providers) if providers else 0 166 + tier_width = max(len(tier_names[t]) for t in tiers) 167 + model_names = {PROVIDER_DEFAULTS[p][t] for p in providers for t in tiers} 168 + model_width = max(len(m) for m in model_names) if model_names else 0 169 + interface_width = max(len(n) for n in interfaces) if interfaces else 0 170 + 171 + total = 0 172 + passed = 0 173 + failed = 0 174 + skipped = 0 175 + results: list[dict[str, object]] = [] 176 + cache: dict[tuple[str, str, str], tuple[str, str, str]] = {} 177 + 178 + for provider_name in providers: 179 + for tier in tiers: 180 + if ( 181 + targeted_pairs is not None 182 + and (provider_name, tier) not in targeted_pairs 183 + ): 184 + continue 185 + model = PROVIDER_DEFAULTS[provider_name][tier] 186 + for interface_name in interfaces: 187 + cache_key = (provider_name, model, interface_name) 188 + if cache_key in cache: 189 + status, message, source_tier = cache[cache_key] 190 + elapsed_s = 0.0 191 + elapsed_s_rounded = 0.0 192 + reused_from = source_tier 193 + else: 194 + start = time.perf_counter() 195 + if interface_name == "generate": 196 + status, message = _check_generate( 197 + provider_name, tier, args.timeout 198 + ) 199 + else: 200 + status, message = await _check_cogitate( 201 + provider_name, tier, args.timeout 202 + ) 203 + elapsed_s = time.perf_counter() - start 204 + elapsed_s_rounded = round(elapsed_s, 1) 205 + cache[cache_key] = (status, message, tier_names[tier]) 206 + reused_from = None 207 + 208 + result: dict[str, object] = { 209 + "provider": provider_name, 210 + "tier": tier_names[tier], 211 + "model": model, 212 + "interface": interface_name, 213 + "ok": status != "fail", 214 + "status": status, 215 + "message": str(message), 216 + "elapsed_s": elapsed_s_rounded, 217 + } 218 + if reused_from: 219 + result["reused_from"] = reused_from 220 + results.append(result) 221 + 222 + if not args.json: 223 + if reused_from: 224 + mark = "=" 225 + display_message = f"{message} (={reused_from})" 226 + else: 227 + if status == "ok": 228 + mark = "✓" 229 + elif status == "skip": 230 + mark = "-" 231 + else: 232 + mark = "✗" 233 + display_message = str(message) 234 + print( 235 + f"{mark} " 236 + f"{provider_name:<{provider_width}} " 237 + f"{tier_names[tier]:<{tier_width}} " 238 + f"{model:<{model_width}} " 239 + f"{interface_name:<{interface_width}} " 240 + f"{display_message} ({elapsed_s:.1f}s)" 241 + ) 242 + 243 + total += 1 244 + if status == "ok": 245 + passed += 1 246 + elif status == "skip": 247 + skipped += 1 248 + else: 249 + failed += 1 250 + 251 + any_failed = any(r["status"] == "fail" for r in results) 252 + 253 + payload = { 254 + "results": results, 255 + "summary": { 256 + "total": total, 257 + "passed": passed, 258 + "skipped": skipped, 259 + "failed": failed, 260 + }, 261 + "checked_at": datetime.now(timezone.utc).isoformat(), 262 + } 263 + health_dir = Path(get_journal()) / "health" 264 + health_dir.mkdir(parents=True, exist_ok=True) 265 + (health_dir / "talents.json").write_text(json.dumps(payload, indent=2)) 266 + 267 + if args.json: 268 + print( 269 + json.dumps( 270 + { 271 + "results": results, 272 + "summary": { 273 + "total": total, 274 + "passed": passed, 275 + "skipped": skipped, 276 + "failed": failed, 277 + }, 278 + }, 279 + indent=2, 280 + ) 281 + ) 282 + else: 283 + print(f"{total} checks: {passed} passed, {skipped} skipped, {failed} failed") 284 + sys.exit(1 if any_failed else 0) 285 + 286 + 287 + async def main_async() -> None: 288 + """CLI entrypoint for provider connectivity checks.""" 289 + from think.providers import PROVIDER_REGISTRY 290 + 291 + parser = argparse.ArgumentParser(description="solstone Provider CLI") 292 + subparsers = parser.add_subparsers(dest="subcommand") 293 + check_parser = subparsers.add_parser("check", help="Check AI provider connectivity") 294 + check_parser.add_argument( 295 + "--provider", 296 + action="append", 297 + help=f"Provider to check (repeatable). Available: {', '.join(PROVIDER_REGISTRY.keys())}", 298 + ) 299 + check_parser.add_argument( 300 + "--interface", 301 + choices=["generate", "cogitate"], 302 + default=None, 303 + help="Interface to check (default: both)", 304 + ) 305 + check_parser.add_argument( 306 + "--timeout", 307 + type=int, 308 + default=30, 309 + help="Timeout in seconds for generate checks (default: 30)", 310 + ) 311 + check_parser.add_argument( 312 + "--tier", 313 + type=int, 314 + choices=[1, 2, 3], 315 + default=None, 316 + help="Tier to check (1=pro, 2=flash, 3=lite; default: all)", 317 + ) 318 + check_parser.add_argument( 319 + "--json", action="store_true", help="Output results as JSON" 320 + ) 321 + check_parser.add_argument( 322 + "--targeted", 323 + action="store_true", 324 + help="Only check configured provider+tier pairs (used by automated rechecks)", 325 + ) 326 + 327 + args = setup_cli(parser) 328 + require_solstone() 329 + if args.subcommand != "check": 330 + parser.print_help() 331 + sys.exit(1) 332 + await _run_check(args) 333 + 334 + 335 + def main() -> None: 336 + """Entry point wrapper.""" 337 + asyncio.run(main_async())
+2 -2
think/retention.py
··· 74 74 3. screen.jsonl exists if any video raw media was captured 75 75 4. agents/speaker_labels.json exists if embeddings (.npz) are present 76 76 """ 77 - agents_dir = segment_path / "agents" 77 + agents_dir = segment_path / "talents" 78 78 79 79 # Check 1: no active agent files 80 80 if agents_dir.is_dir(): ··· 126 126 if path.is_file() 127 127 ) 128 128 129 - speaker_labels = segment_path / "agents" / "speaker_labels.json" 129 + speaker_labels = segment_path / "talents" / "speaker_labels.json" 130 130 if speaker_labels.exists(): 131 131 completion_files.append(speaker_labels) 132 132
+6 -6
think/routines.py
··· 24 24 25 25 from apps.calendar.event import EventDay 26 26 from think.callosum import callosum_send 27 - from think.cortex_client import cortex_request, wait_for_agents 27 + from think.cortex_client import cortex_request, wait_for_uses 28 28 from think.facets import get_facets 29 29 from think.utils import get_journal 30 30 ··· 302 302 ) 303 303 304 304 callosum_send("routines", "started", routine_id=routine_id, name=name) 305 - agent_id = cortex_request( 305 + use_id = cortex_request( 306 306 prompt=prompt, 307 307 name="routine", 308 308 config={"output_path": str(output_path), "output": "md"}, 309 309 ) 310 310 311 - if agent_id is None: 311 + if use_id is None: 312 312 duration = int(time.monotonic() - start_time) 313 313 logger.error("Failed to start routine %s", routine_id) 314 314 _log_health(routine_id, name, duration, "error") ··· 323 323 ) 324 324 return 325 325 326 - completed, timed_out = wait_for_agents([agent_id], timeout=600) 327 - if agent_id in timed_out: 326 + completed, timed_out = wait_for_uses([use_id], timeout=600) 327 + if use_id in timed_out: 328 328 outcome = "timeout" 329 329 else: 330 - end_state = completed.get(agent_id, "error") 330 + end_state = completed.get(use_id, "error") 331 331 outcome = "success" if end_state == "finish" else "error" 332 332 333 333 duration = int(time.monotonic() - start_time)
+19 -19
think/segment.py
··· 58 58 59 59 60 60 def _segment_stats(seg_path: Path) -> dict[str, int]: 61 - """Return recursive file, agent, and byte counts for a segment.""" 61 + """Return recursive file, talent, and byte counts for a segment.""" 62 62 files = 0 63 - agents = 0 63 + talents = 0 64 64 size = 0 65 65 for path in seg_path.rglob("*"): 66 66 if path.is_file(): 67 67 files += 1 68 68 size += path.stat().st_size 69 - if "agents" in path.parts: 70 - agents += 1 71 - return {"files": files, "agents": agents, "size": size} 69 + if "talents" in path.parts: 70 + talents += 1 71 + return {"files": files, "talents": talents, "size": size} 72 72 73 73 74 74 def _split_segment_path(path: str) -> tuple[str, str, str]: ··· 161 161 162 162 163 163 def _agent_files(seg_dir: Path) -> list[str]: 164 - """Return top-level file names from agents/ if present.""" 165 - agents_dir = seg_dir / "agents" 166 - if not agents_dir.is_dir(): 164 + """Return top-level file names from talents/ if present.""" 165 + talents_dir = seg_dir / "talents" 166 + if not talents_dir.is_dir(): 167 167 return [] 168 - return sorted(path.name for path in agents_dir.iterdir() if path.is_file()) 168 + return sorted(path.name for path in talents_dir.iterdir() if path.is_file()) 169 169 170 170 171 171 def _events_summary(seg_dir: Path) -> dict[str, object]: ··· 609 609 _touch_health_marker(to_day) 610 610 print(f" touched health markers: {src_day}, {to_day}") 611 611 if verbose: 612 - print(" dream will re-run daily agents on both days") 612 + print(" dream will re-run daily talents on both days") 613 613 614 614 # Post-move verify is informational — the move already completed. 615 615 print() ··· 641 641 "end": end, 642 642 "duration": _segment_duration(seg_key), 643 643 "files": stats["files"], 644 - "agents": stats["agents"], 644 + "talents": stats["talents"], 645 645 "size": stats["size"], 646 646 } 647 647 ) ··· 652 652 653 653 print( 654 654 f"{'STREAM':<20} {'SEGMENT':<14} {'TIME':<15} " 655 - f"{'DUR':>5} {'FILES':>5} {'AGENTS':>6} {'SIZE':>8}" 655 + f"{'DUR':>5} {'FILES':>5} {'TALENTS':>7} {'SIZE':>8}" 656 656 ) 657 - print("-" * 77) 657 + print("-" * 78) 658 658 for row in rows: 659 659 time_str = ( 660 660 f"{row['start']}-{row['end']}" ··· 664 664 dur_str = f"{row['duration']}s" 665 665 print( 666 666 f"{row['stream']:<20} {row['segment']:<14} {time_str:<15} " 667 - f"{dur_str:>5} {row['files']:>5} {row['agents']:>6} " 667 + f"{dur_str:>5} {row['files']:>5} {row['talents']:>7} " 668 668 f"{_format_size(int(row['size'])):>8}" 669 669 ) 670 670 ··· 684 684 prev_desc = _describe_prev(day, stream_name, marker) 685 685 next_desc = _describe_next(day, stream_name, segment) 686 686 files = _segment_files(seg_dir) 687 - agents = _agent_files(seg_dir) 687 + talents = _agent_files(seg_dir) 688 688 stats = _segment_stats(seg_dir) 689 689 events = _events_summary(seg_dir) 690 690 index_info = _segment_index_info(day, stream_name, segment) ··· 701 701 "duration": duration, 702 702 "chain": {"prev": prev_desc, "next": next_desc}, 703 703 "files": files, 704 - "agents": agents, 704 + "talents": talents, 705 705 "stats": stats, 706 706 "events": events, 707 707 "index": index_info, ··· 730 730 if files: 731 731 print(f" {', '.join(files)}") 732 732 print() 733 - print(f"Agents ({len(agents)}):") 734 - if agents: 735 - print(f" {', '.join(agents)}") 733 + print(f"Talents ({len(talents)}):") 734 + if talents: 735 + print(f" {', '.join(talents)}") 736 736 print() 737 737 print(f"Size: {_format_size(stats['size'])}") 738 738 if index_info["available"]:
+2 -2
think/sense_splitter.py
··· 28 28 sense_json: dict, seg_dir: Path, stream: str | None = None 29 29 ) -> None: 30 30 """Write unified Sense output into per-agent files.""" 31 - agents_dir = seg_dir / "agents" 31 + agents_dir = seg_dir / "talents" 32 32 33 33 density = sense_json.get("density") or "active" 34 34 activity_summary = sense_json.get("activity_summary") or "" ··· 56 56 def write_idle_stubs(seg_dir: Path) -> None: 57 57 """Write minimal idle output files for a segment.""" 58 58 _write_json_atomic( 59 - seg_dir / "agents" / "density.json", 59 + seg_dir / "talents" / "density.json", 60 60 { 61 61 "classification": "idle", 62 62 "transcript_lines": 0,
+3 -3
think/stats_schema.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - SCHEMA_VERSION = 2 4 + SCHEMA_VERSION = 3 5 5 6 6 DAY_FIELDS = ( 7 7 "transcript_sessions", ··· 39 39 "totals", 40 40 "heatmap", 41 41 "tokens", 42 - "agents", 42 + "talents", 43 43 "facets", 44 44 ) 45 45 46 46 47 47 def validate(data: dict) -> list[str]: 48 - """Validate stats output against schema v2. Returns list of error strings (empty = valid).""" 48 + """Validate stats output against schema v3. Returns list of error strings (empty = valid).""" 49 49 errors = [] 50 50 51 51 # Check schema_version
+68 -68
think/talent.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Talent agent and generator orchestration utilities. 4 + """Talent and generator orchestration utilities. 5 5 6 - This module provides functionality for configuring and orchestrating talent agents 6 + This module provides functionality for configuring and orchestrating talents 7 7 and generators from talent/*.md and apps/*/talent/*.md. 8 8 9 9 Key functions: 10 10 - get_talent_configs(): Discover all talent configs with filtering 11 - - get_agent(): Load complete agent configuration by name 11 + - get_talent(): Load complete talent configuration by name 12 12 - Hook loading: load_pre_hook(), load_post_hook() 13 13 14 14 For simple prompt loading without orchestration (observe/, think/*.md prompts), ··· 95 95 96 96 97 97 def get_output_name(key: str) -> str: 98 - """Convert agent/generator key to filesystem-safe filename stem. 98 + """Convert talent/generator key to a filesystem-safe filename stem. 99 99 100 100 Parameters 101 101 ---------- ··· 128 128 facet: str | None = None, 129 129 stream: str | None = None, 130 130 ) -> Path: 131 - """Return output path for generator agent output. 131 + """Return output path for generator/talent output. 132 132 133 133 Shared utility for determining where to write generator results. 134 - Used by think/agents.py and think/cortex.py. 134 + Used by think.talents and think.cortex. 135 135 136 136 Parameters 137 137 ---------- 138 138 day_dir: 139 139 Day directory path (YYYYMMDD). 140 140 key: 141 - Generator key or agent name (e.g., "activity", "chat:sentiment", 141 + Generator key or talent name (e.g., "activity", "chat:sentiment", 142 142 "decisionalizer", "entities:observer"). 143 143 segment: 144 144 Optional segment key (HHMMSS_LEN) for segment-level output. 145 145 output_format: 146 146 Output format - "json" for JSON, anything else for markdown. 147 147 facet: 148 - Optional facet name for multi-facet agents. When provided, output is 149 - written under an agents/{facet}/ subdirectory. 148 + Optional facet name for multi-facet talents. When provided, output is 149 + written under a talents/{facet}/ subdirectory. 150 150 stream: 151 151 Optional stream name for segment-level output. When provided with 152 - segment, constructs path as YYYYMMDD/{stream}/{segment}/agents/... 152 + segment, constructs path as YYYYMMDD/{stream}/{segment}/talents/... 153 153 154 154 Returns 155 155 ------- 156 156 Path 157 157 Output file path: 158 - - Segment + no facet: YYYYMMDD/{stream}/{segment}/agents/{name}.{ext} 159 - - Segment + facet: YYYYMMDD/{stream}/{segment}/agents/{facet}/{name}.{ext} 160 - - Daily + no facet: YYYYMMDD/agents/{name}.{ext} 161 - - Daily + facet: YYYYMMDD/agents/{facet}/{name}.{ext} 158 + - Segment + no facet: YYYYMMDD/{stream}/{segment}/talents/{name}.{ext} 159 + - Segment + facet: YYYYMMDD/{stream}/{segment}/talents/{facet}/{name}.{ext} 160 + - Daily + no facet: YYYYMMDD/talents/{name}.{ext} 161 + - Daily + facet: YYYYMMDD/talents/{facet}/{name}.{ext} 162 162 Where name is derived from key and ext is "json" or "md". 163 163 """ 164 164 day = Path(day_dir) ··· 172 172 else: 173 173 seg_dir = day / segment 174 174 if facet: 175 - return seg_dir / "agents" / facet / filename 176 - return seg_dir / "agents" / filename 175 + return seg_dir / "talents" / facet / filename 176 + return seg_dir / "talents" / filename 177 177 if facet: 178 - return day / "agents" / facet / filename 179 - return day / "agents" / filename 178 + return day / "talents" / facet / filename 179 + return day / "talents" / filename 180 180 181 181 182 182 def get_talent_configs( ··· 328 328 329 329 330 330 # --------------------------------------------------------------------------- 331 - # Agent Resolution 331 + # Talent Resolution 332 332 # --------------------------------------------------------------------------- 333 333 334 334 335 - def _resolve_agent_path(name: str) -> tuple[Path, str]: 336 - """Resolve agent name to directory path and agent filename. 335 + def _resolve_talent_path(name: str) -> tuple[Path, str]: 336 + """Resolve talent name to directory path and filename. 337 337 338 338 Parameters 339 339 ---------- 340 340 name: 341 - Agent name - either system agent (e.g., "unified") or 342 - app-namespaced agent (e.g., "support:support"). 341 + Talent name - either system talent (e.g., "unified") or 342 + app-namespaced talent (e.g., "support:support"). 343 343 344 344 Returns 345 345 ------- 346 346 tuple[Path, str] 347 - (agent_directory, agent_name) tuple. 347 + (talent_directory, talent_name) tuple. 348 348 """ 349 349 if ":" in name: 350 - # App agent: "support:support" -> apps/support/talent/support 351 - app, agent_name = name.split(":", 1) 352 - agent_dir = Path(__file__).parent.parent / "apps" / app / "talent" 350 + # App talent: "support:support" -> apps/support/talent/support 351 + app, talent_name = name.split(":", 1) 352 + talent_dir = Path(__file__).parent.parent / "apps" / app / "talent" 353 353 elif name == "unified": 354 - # Chat agent: "unified" -> talent/chat 355 - agent_dir = TALENT_DIR 356 - agent_name = "chat" 354 + # Chat talent: "unified" -> talent/chat 355 + talent_dir = TALENT_DIR 356 + talent_name = "chat" 357 357 else: 358 - # System agent: bare name -> talent/{name} 359 - agent_dir = TALENT_DIR 360 - agent_name = name 361 - return agent_dir, agent_name 358 + # System talent: bare name -> talent/{name} 359 + talent_dir = TALENT_DIR 360 + talent_name = name 361 + return talent_dir, talent_name 362 362 363 363 364 364 # Default load configuration - prompts must explicitly opt into source loading 365 365 _DEFAULT_LOAD = { 366 366 "transcripts": False, 367 367 "percepts": False, 368 - "agents": False, 368 + "talents": False, 369 369 } 370 370 371 371 ··· 381 381 - False: don't load 382 382 - True: load if available 383 383 - "required": load (and generation will fail if none found) 384 - - dict: for agents source, selective loading (e.g., {"entities": true}) 384 + - dict: for talents source, selective loading (e.g., {"entities": true}) 385 385 386 386 Both True and "required" mean the source should be loaded. 387 387 A non-empty dict means the source should be loaded (with filtering). 388 388 389 389 Args: 390 - value: The source config value (bool, "required" string, or dict for agents) 390 + value: The source config value (bool, "required" string, or dict for talents) 391 391 392 392 Returns: 393 393 True if the source should be loaded, False otherwise. ··· 402 402 """Check if a source must have content for generation to proceed. 403 403 404 404 Args: 405 - value: The source config value (bool, "required" string, or dict for agents) 405 + value: The source config value (bool, "required" string, or dict for talents) 406 406 407 407 Returns: 408 408 True if the source is required (generation should skip if no content). ··· 413 413 return value == "required" 414 414 415 415 416 - def get_agent_filter(value: bool | str | dict) -> dict[str, bool | str] | None: 417 - """Extract agent filter from sources config. 416 + def get_talent_filter(value: bool | str | dict) -> dict[str, bool | str] | None: 417 + """Extract talent filter from sources config. 418 418 419 - When agents source is a dict, returns it as filter mapping agent names 420 - to their enabled/required status. When agents source is bool or "required", 421 - returns None to indicate all agents should be loaded. 419 + When talents source is a dict, returns it as filter mapping talent names 420 + to their enabled/required status. When talents source is bool or "required", 421 + returns None to indicate all talents should be loaded. 422 422 423 423 Args: 424 - value: The agents source config value 424 + value: The talents source config value 425 425 426 426 Returns: 427 - Dict mapping agent names to bool/"required", or None for all agents. 428 - Returns empty dict if value is False (no agents). 427 + Dict mapping talent names to bool/"required", or None for all talents. 428 + Returns empty dict if value is False (no talents). 429 429 430 430 Examples: 431 - >>> get_agent_filter(True) 432 - None # All agents 433 - >>> get_agent_filter(False) 434 - {} # No agents 435 - >>> get_agent_filter({"entities": True, "meetings": "required"}) 431 + >>> get_talent_filter(True) 432 + None # All talents 433 + >>> get_talent_filter(False) 434 + {} # No talents 435 + >>> get_talent_filter({"entities": True, "meetings": "required"}) 436 436 {"entities": True, "meetings": "required"} 437 437 """ 438 438 if isinstance(value, dict): 439 439 return value 440 440 if value is False: 441 - return {} # No agents 442 - return None # All agents (True or "required") 441 + return {} # No talents 442 + return None # All talents (True or "required") 443 443 444 444 445 445 # --------------------------------------------------------------------------- 446 - # Agent Loading 446 + # Talent Loading 447 447 # --------------------------------------------------------------------------- 448 448 449 449 450 - def get_agent( 450 + def get_talent( 451 451 name: str = "unified", 452 452 facet: str | None = None, 453 453 analysis_day: str | None = None, 454 454 ) -> dict: 455 - """Return complete agent configuration by name. 455 + """Return a complete talent configuration by name. 456 456 457 457 Loads configuration from .md file with JSON frontmatter and instruction text. 458 458 Template variables like $facets are resolved during prompt loading. ··· 461 461 Parameters 462 462 ---------- 463 463 name: 464 - Agent name to load. Can be a system agent (e.g., "unified") 465 - or an app-namespaced agent (e.g., "support:support" for apps/support/talent/support). 464 + Talent name to load. Can be a system talent (e.g., "unified") 465 + or an app-namespaced talent (e.g., "support:support" for apps/support/talent/support). 466 466 facet: 467 467 Optional facet name to focus on. Controls $facets template variable. 468 468 analysis_day: ··· 472 472 Returns 473 473 ------- 474 474 dict 475 - Complete agent configuration including: 476 - - name: Agent name 475 + Complete talent configuration including: 476 + - name: Talent name 477 477 - path: Path to the .md file 478 478 - user_instruction: Composed prompt with template vars resolved 479 479 - sources: Source config from 'load' key ··· 481 481 """ 482 482 from think.prompts import _resolve_facets 483 483 484 - # Resolve agent path based on namespace 485 - agent_dir, agent_name = _resolve_agent_path(name) 484 + # Resolve talent path based on namespace 485 + talent_dir, talent_name = _resolve_talent_path(name) 486 486 487 - # Verify agent prompt file exists 488 - md_path = agent_dir / f"{agent_name}.md" 487 + # Verify talent prompt file exists 488 + md_path = talent_dir / f"{talent_name}.md" 489 489 if not md_path.exists(): 490 - raise FileNotFoundError(f"Agent not found: {name}") 490 + raise FileNotFoundError(f"Talent not found: {name}") 491 491 492 492 # Load config from frontmatter - preserve all fields 493 493 post = frontmatter.load(md_path) ··· 508 508 prompt_context: dict[str, str] = {} 509 509 prompt_context["facets"] = _resolve_facets(facet) 510 510 511 - agent_prompt = load_prompt(agent_name, base_dir=agent_dir, context=prompt_context) 512 - config["user_instruction"] = agent_prompt.text 511 + prompt_obj = load_prompt(talent_name, base_dir=talent_dir, context=prompt_context) 512 + config["user_instruction"] = prompt_obj.text 513 513 514 - # Set agent name 514 + # Set talent name 515 515 config["name"] = name 516 516 517 517 return config
+37 -37
think/talent_cli.py
··· 13 13 sol talent show <name> Show details for a specific prompt 14 14 sol talent show <name> --json Output a single prompt as JSONL 15 15 sol talent show <name> --prompt Show full prompt context (dry-run) 16 - sol talent logs Show recent agent runs 17 - sol talent logs <agent> -c 5 Show last 5 runs for an agent 18 - sol talent log <id> Show events for an agent run 16 + sol talent logs Show recent talent runs 17 + sol talent logs <agent> -c 5 Show last 5 runs for a talent 18 + sol talent log <id> Show events for a talent run 19 19 sol talent log <id> --json Output raw JSONL events 20 20 sol talent log <id> --full Show expanded event details 21 21 """ ··· 79 79 return result 80 80 81 81 82 - def _format_last_run(key: str, agents_dir: Path) -> tuple[str, bool]: 82 + def _format_last_run(key: str, talents_dir: Path) -> tuple[str, bool]: 83 83 """Format age of last run with optional runtime duration. 84 84 85 85 Returns (display_string, failed) where failed is True if the last 86 86 event in the log was an error. 87 87 """ 88 88 safe_name = key.replace(":", "--") 89 - link_path = agents_dir / f"{safe_name}.log" 89 + link_path = talents_dir / f"{safe_name}.log" 90 90 if not link_path.exists(): 91 91 return "-", False 92 92 ··· 197 197 ) 198 198 from think.utils import get_journal 199 199 200 - agents_dir = Path(get_journal()) / "agents" 200 + talents_dir = Path(get_journal()) / "talents" 201 201 202 202 if not configs: 203 203 print("No prompts found matching filters.") ··· 248 248 249 249 for key, info in items: 250 250 title = info.get("title", "")[:title_width] 251 - last_run_str, failed = _format_last_run(key, agents_dir) 251 + last_run_str, failed = _format_last_run(key, talents_dir) 252 252 last_run = last_run_str[:last_run_width] 253 253 tags = _format_tags(info, failed=failed) 254 254 src = "" ··· 420 420 ) -> None: 421 421 """Show full prompt context via dry-run. 422 422 423 - Builds config and pipes to `sol agents --dry-run` to show exactly 423 + Builds config and pipes to `sol think.talents --dry-run` to show exactly 424 424 what would be sent to the LLM provider. 425 425 """ 426 426 # Load prompt metadata ··· 570 570 if facet: 571 571 config["facet"] = facet 572 572 else: 573 - # Cogitate prompt - use get_agent() to build full config with instructions 574 - from think.talent import get_agent 573 + # Cogitate prompt - use get_talent() to build full config with instructions 574 + from think.talent import get_talent 575 575 576 576 try: 577 - agent_config = get_agent(name, facet=facet) 577 + agent_config = get_talent(name, facet=facet) 578 578 config.update(agent_config) 579 579 except Exception as e: 580 - print(f"Failed to load agent config: {e}", file=sys.stderr) 580 + print(f"Failed to load talent config: {e}", file=sys.stderr) 581 581 sys.exit(1) 582 582 583 583 # Override prompt with user query ··· 586 586 else: 587 587 config["prompt"] = "(no --query provided)" 588 588 589 - # Run sol agents --dry-run 589 + # Run sol think.talents --dry-run 590 590 config_json = json.dumps(config) 591 591 try: 592 592 result = subprocess.run( 593 - ["sol", "agents", "--dry-run"], 593 + ["sol", "think.talents", "--dry-run"], 594 594 input=config_json + "\n", 595 595 capture_output=True, 596 596 text=True, ··· 728 728 print() 729 729 730 730 731 - def _find_run_file(agents_dir: Path, agent_id: str) -> Path | None: 732 - """Locate an agent run JSONL file by ID.""" 733 - for match in agents_dir.glob(f"*/{agent_id}.jsonl"): 731 + def _find_run_file(talents_dir: Path, use_id: str) -> Path | None: 732 + """Locate a talent run JSONL file by ID.""" 733 + for match in talents_dir.glob(f"*/{use_id}.jsonl"): 734 734 return match 735 - for match in agents_dir.glob(f"*/{agent_id}_active.jsonl"): 735 + for match in talents_dir.glob(f"*/{use_id}_active.jsonl"): 736 736 return match 737 737 return None 738 738 ··· 823 823 824 824 825 825 def _print_summary(records: list[dict[str, Any]]) -> None: 826 - """Print grouped summary of agent runs.""" 826 + """Print grouped summary of talent runs.""" 827 827 from collections import defaultdict 828 828 829 829 groups: dict[str, list[dict[str, Any]]] = defaultdict(list) ··· 874 874 errors: bool = False, 875 875 summary: bool = False, 876 876 ) -> None: 877 - """Print one-line summaries of recent agent runs from day-index files.""" 877 + """Print one-line summaries of recent talent runs from day-index files.""" 878 878 from think.models import calc_agent_cost 879 879 from think.utils import get_journal 880 880 881 881 journal_root = get_journal() 882 - agents_dir = Path(journal_root) / "agents" 883 - if not agents_dir.is_dir(): 882 + talents_dir = Path(journal_root) / "talents" 883 + if not talents_dir.is_dir(): 884 884 return 885 885 886 886 # Validate --day format ··· 894 894 895 895 # Find day-index files, most recent first 896 896 if day: 897 - day_file = agents_dir / f"{day}.jsonl" 897 + day_file = talents_dir / f"{day}.jsonl" 898 898 day_files = [day_file] if day_file.is_file() else [] 899 899 else: 900 - day_files = sorted(agents_dir.glob("????????.jsonl"), reverse=True) 900 + day_files = sorted(talents_dir.glob("????????.jsonl"), reverse=True) 901 901 if not day_files: 902 902 return 903 903 ··· 948 948 name_width = max(name_width, 10) 949 949 950 950 for r in records: 951 - agent_id = r.get("agent_id") 951 + use_id = r.get("use_id") 952 952 run_file = ( 953 - _find_run_file(agents_dir, agent_id) if isinstance(agent_id, str) else None 953 + _find_run_file(talents_dir, use_id) if isinstance(use_id, str) else None 954 954 ) 955 955 stats: dict[str, Any] = { 956 956 "event_count": 0, ··· 980 980 stats = r.get("_stats") or {} 981 981 cost_usd = r.get("_cost_usd") 982 982 output_size = r.get("_output_size") 983 - agent_id = r.get("agent_id", "") 983 + use_id = r.get("use_id", "") 984 984 985 985 ts = r.get("ts", 0) 986 986 dt = datetime.fromtimestamp(ts / 1000) ··· 1014 1014 1015 1015 facet_part = f" {facet}" if facet else "" 1016 1016 line = ( 1017 - f"{agent_id:<15}{time_str:>12} {name:<{name_width}} {status_sym} " 1017 + f"{use_id:<15}{time_str:>12} {name:<{name_width}} {status_sym} " 1018 1018 f"{runtime_str:>7} {cost_str:>4} {events_str:>3} {tools_str:>3} " 1019 1019 f"{output_str:>5} {model}{facet_part}" 1020 1020 ) ··· 1046 1046 tool = event.get("tool", "") 1047 1047 result = event.get("result", "") 1048 1048 return f"{tool} → {result}" 1049 - elif etype == "agent_updated": 1050 - return event.get("agent", "") 1049 + elif etype == "talent_updated": 1050 + return event.get("talent", "") 1051 1051 elif etype == "finish": 1052 1052 result = event.get("result", "") 1053 1053 usage = event.get("usage") ··· 1074 1074 "thinking": "think", 1075 1075 "tool_start": "tool", 1076 1076 "tool_end": "tool_end", 1077 - "agent_updated": "updated", 1077 + "talent_updated": "updated", 1078 1078 "finish": "finish", 1079 1079 "error": "error", 1080 1080 } ··· 1093 1093 return f"{time_str} {label:<8} {detail}" 1094 1094 1095 1095 1096 - def log_run(agent_id: str, *, json_mode: bool = False, full: bool = False) -> None: 1097 - """Show events for a single agent run.""" 1096 + def log_run(use_id: str, *, json_mode: bool = False, full: bool = False) -> None: 1097 + """Show events for a single talent run.""" 1098 1098 from think.utils import get_journal 1099 1099 1100 - agents_dir = Path(get_journal()) / "agents" 1101 - run_file = _find_run_file(agents_dir, agent_id) 1100 + talents_dir = Path(get_journal()) / "talents" 1101 + run_file = _find_run_file(talents_dir, use_id) 1102 1102 if run_file is None: 1103 - print(f"Agent run not found: {agent_id}", file=sys.stderr) 1103 + print(f"Talent run not found: {use_id}", file=sys.stderr) 1104 1104 sys.exit(1) 1105 1105 1106 1106 if json_mode: ··· 1164 1164 ) 1165 1165 1166 1166 # --- logs subcommand --- 1167 - logs_parser = subparsers.add_parser("logs", help="Show recent agent run log") 1167 + logs_parser = subparsers.add_parser("logs", help="Show recent talent run log") 1168 1168 logs_parser.add_argument("agent", nargs="?", help="Filter to a specific agent") 1169 1169 logs_parser.add_argument( 1170 1170 "-c",
+7 -7
think/tools/call.py
··· 606 606 607 607 if segment: 608 608 # List outputs in a specific segment directory 609 - seg_path = day_dir / segment / "agents" 609 + seg_path = day_dir / segment / "talents" 610 610 if not seg_path.is_dir(): 611 611 typer.echo(f"Segment {segment} not found for {day}.") 612 612 return ··· 614 614 return 615 615 616 616 # List daily agent outputs 617 - agents_path = day_dir / "agents" 617 + agents_path = day_dir / "talents" 618 618 if agents_path.is_dir(): 619 619 _list_outputs(agents_path, "Daily agents") 620 620 ··· 623 623 if seg_list: 624 624 typer.echo(f"\nSegments: {len(seg_list)}") 625 625 for stream_name, seg_key, seg_path_obj in seg_list: 626 - agents_dir = seg_path_obj / "agents" 627 - outputs = _get_output_names(agents_dir) 626 + talents_dir = seg_path_obj / "talents" 627 + outputs = _get_output_names(talents_dir) 628 628 label = f" {stream_name}/{seg_key}" if stream_name else f" {seg_key}" 629 629 if outputs: 630 630 typer.echo(f"{label}: {', '.join(outputs)}") ··· 686 686 raise typer.Exit(1) 687 687 688 688 if segment: 689 - base_dir = day_dir / segment / "agents" 689 + base_dir = day_dir / segment / "talents" 690 690 else: 691 - base_dir = day_dir / "agents" 691 + base_dir = day_dir / "talents" 692 692 693 693 if not base_dir.is_dir(): 694 - location = f"segment {segment}" if segment else "agents" 694 + location = f"segment {segment}" if segment else "talents" 695 695 typer.echo(f"No {location} directory for {day}.", err=True) 696 696 raise typer.Exit(1) 697 697
+4 -4
think/tools/sol.py
··· 8 8 ``{journal}/sol/pulse.md``, and ``{journal}/sol/awareness.md`` — sol's 9 9 identity and initiative files. Also provides read access to the morning 10 10 briefing at 11 - ``{journal}/YYYYMMDD/agents/morning_briefing.md``. 11 + ``{journal}/YYYYMMDD/talents/morning_briefing.md``. 12 12 13 13 Mounted by ``think.call`` as ``sol call identity ...``. 14 14 """ ··· 318 318 def briefing_cmd( 319 319 day: str | None = typer.Option(None, "--day", "-d", help="Specific day YYYYMMDD."), 320 320 ) -> None: 321 - """Read the morning briefing from YYYYMMDD/agents/morning_briefing.md.""" 321 + """Read the morning briefing from YYYYMMDD/talents/morning_briefing.md.""" 322 322 if day: 323 - path = day_path(day, create=False) / "agents" / "morning_briefing.md" 323 + path = day_path(day, create=False) / "talents" / "morning_briefing.md" 324 324 if not path.exists(): 325 325 typer.echo("No briefing found.", err=True) 326 326 raise typer.Exit(1) ··· 329 329 330 330 # No day specified — find most recent 331 331 for day in sorted(day_dirs().keys(), reverse=True): 332 - agents_dir = day_path(day, create=False) / "agents" 332 + agents_dir = day_path(day, create=False) / "talents" 333 333 briefing = agents_dir / "morning_briefing.md" 334 334 if briefing.exists() and briefing.stat().st_size > 0: 335 335 typer.echo(briefing.read_text(encoding="utf-8"))