From 0050be8cc77a1e35d3e47faa83e04dc91bc380da Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Fri, 17 Apr 2026 13:28:21 -0600 Subject: [PATCH] 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/` to `/api/talents/` and return both `uses` (invocations) and `talents` (config metadata) in the payload. - Rename the journal layout from `/agents/` to `/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 `/agents/` dirs to `/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) --- AGENTS.md | 14 +- Makefile | 8 +- apps/entities/routes.py | 12 +- apps/entities/talent/entity_observer.md | 2 +- apps/entities/workspace.html | 16 +- apps/health/talent/health/SKILL.md | 2 +- apps/health/tests/test_call.py | 4 +- apps/health/tests/test_routes.py | 2 +- apps/health/workspace.html | 34 +- apps/home/events.py | 12 +- apps/home/routes.py | 9 +- apps/search/routes.py | 4 +- .../sol/maint/004_rename_agents_to_talents.py | 124 +++++ apps/sol/routes.py | 220 +++++---- apps/sol/workspace.html | 34 +- apps/speakers/attribution.py | 8 +- apps/speakers/bootstrap.py | 2 +- apps/speakers/routes.py | 24 +- apps/speakers/status.py | 2 +- apps/speakers/suggest.py | 6 +- apps/speakers/tests/conftest.py | 6 +- apps/speakers/tests/test_attribution.py | 6 +- apps/speakers/tests/test_discovery.py | 6 +- apps/speakers/tests/test_merge_names.py | 12 +- apps/speakers/tests/test_routes.py | 14 +- apps/speakers/tests/test_suggest.py | 2 +- apps/stats/static/dashboard.js | 6 +- apps/todos/routes.py | 52 +- apps/todos/talent/daily.md | 2 +- apps/todos/workspace.html | 12 +- apps/transcripts/routes.py | 12 +- convey/apps.py | 4 +- convey/templates/app.html | 14 +- convey/triage.py | 20 +- convey/utils.py | 8 +- docs/APPS.md | 16 +- docs/CALLOSUM.md | 6 +- docs/CORTEX.md | 74 +-- docs/DOCTOR.md | 20 +- docs/JOURNAL.md | 38 +- docs/PROMPT_TEMPLATES.md | 2 +- docs/PROVIDERS.md | 2 +- docs/THINK.md | 36 +- docs/design/yesterdays-processing-card.md | 16 +- scripts/gate_agents_rename.py | 122 +++++ sol.py | 10 +- talent/activities.py | 4 +- talent/activity_state.py | 6 +- talent/daily_schedule.md | 2 +- talent/decisions.md | 2 +- talent/documents.md | 2 +- talent/entities.md | 2 +- talent/facet_newsletter.md | 2 +- talent/flow.md | 2 +- talent/followups.md | 2 +- talent/heartbeat.md | 4 +- talent/knowledge_graph.md | 2 +- talent/meetings.md | 2 +- talent/messaging.md | 2 +- talent/morning_briefing.md | 8 +- talent/schedule.md | 2 +- talent/screen.md | 2 +- talent/sense.md | 2 +- talent/skills.md | 2 +- talent/speaker_attribution.md | 2 +- talent/speaker_attribution.py | 4 +- talent/timeline.md | 2 +- tests/baselines/api/calendar/day-events.json | 8 +- tests/baselines/api/search/search.json | 2 +- tests/baselines/api/sol/run-detail.json | 22 +- .../sol/{agents-day.json => talents-day.json} | 4 +- tests/baselines/api/stats/stats.json | 32 +- .../api/{agents => talents}/badge-count.json | 0 .../api/{agents => talents}/preview.json | 0 .../api/{agents => talents}/run-detail.json | 22 +- .../api/{agents => talents}/stats-month.json | 0 .../talents-day.json} | 4 +- .../api/{agents => talents}/updated-days.json | 0 tests/fixtures/journal/AGENTS.md | 38 +- tests/fixtures/journal/agents/20231113.jsonl | 2 - tests/fixtures/journal/agents/20231114.jsonl | 4 - tests/fixtures/journal/agents/20260304.jsonl | 3 - tests/fixtures/journal/agents/20260305.jsonl | 3 - tests/fixtures/journal/agents/20260306.jsonl | 2 - tests/fixtures/journal/agents/20260307.jsonl | 2 - tests/fixtures/journal/agents/20260308.jsonl | 3 - tests/fixtures/journal/agents/20260309.jsonl | 1 - tests/fixtures/journal/agents/20260310.jsonl | 4 - tests/fixtures/journal/agents/default.log | 1 - .../journal/agents/flow/1700000000002.jsonl | 3 - .../123456_300/{agents => talents}/audio.md | 0 .../123456_300/{agents => talents}/screen.md | 0 .../20240101/{agents => talents}/flow.md | 0 .../{agents => talents}/knowledge_graph.md | 0 .../20240101/{agents => talents}/meetings.md | 0 .../234567_300/{agents => talents}/audio.md | 0 .../234567_300/{agents => talents}/screen.md | 0 .../20240102/{agents => talents}/flow.md | 0 .../090000_300/{agents => talents}/audio.md | 0 .../090000_300/{agents => talents}/screen.md | 0 .../{agents => talents}/speaker_labels.json | 0 .../{agents => talents}/speakers.json | 0 .../140000_300/{agents => talents}/audio.md | 0 .../140000_300/{agents => talents}/screen.md | 0 .../180000_300/{agents => talents}/audio.md | 0 .../180000_300/{agents => talents}/screen.md | 0 .../20260304/{agents => talents}/flow.md | 0 .../{agents => talents}/knowledge_graph.md | 0 .../20260304/{agents => talents}/meetings.md | 0 .../090000_300/{agents => talents}/audio.md | 0 .../090000_300/{agents => talents}/screen.md | 0 .../133000_300/{agents => talents}/audio.md | 0 .../133000_300/{agents => talents}/screen.md | 0 .../220000_300/{agents => talents}/audio.md | 0 .../220000_300/{agents => talents}/screen.md | 0 .../20260305/{agents => talents}/flow.md | 0 .../20260305/{agents => talents}/meetings.md | 0 .../093000_300/{agents => talents}/audio.md | 0 .../093000_300/{agents => talents}/screen.md | 0 .../110000_300/{agents => talents}/audio.md | 0 .../110000_300/{agents => talents}/screen.md | 0 .../143000_300/{agents => talents}/audio.md | 0 .../143000_300/{agents => talents}/screen.md | 0 .../170000_300/{agents => talents}/audio.md | 0 .../170000_300/{agents => talents}/screen.md | 0 .../20260306/{agents => talents}/flow.md | 0 .../{agents => talents}/knowledge_graph.md | 0 .../100000_300/{agents => talents}/audio.md | 0 .../150000_300/{agents => talents}/audio.md | 0 .../20260307/{agents => talents}/flow.md | 0 .../20260307/{agents => talents}/meetings.md | 0 .../100000_300/{agents => talents}/audio.md | 0 .../153000_300/{agents => talents}/audio.md | 0 .../20260308/{agents => talents}/flow.md | 0 .../{agents => talents}/knowledge_graph.md | 0 .../20260308/{agents => talents}/meetings.md | 0 .../090000_300/{agents => talents}/audio.md | 0 .../090000_300/{agents => talents}/screen.md | 0 .../133000_300/{agents => talents}/audio.md | 0 .../133000_300/{agents => talents}/screen.md | 0 .../193000_300/{agents => talents}/audio.md | 0 .../193000_300/{agents => talents}/screen.md | 0 .../20260309/{agents => talents}/flow.md | 0 .../083000_300/{agents => talents}/audio.md | 0 .../083000_300/{agents => talents}/screen.md | 0 .../100000_300/{agents => talents}/audio.md | 0 .../170000_300/{agents => talents}/audio.md | 0 .../170000_300/{agents => talents}/screen.md | 0 .../20260310/{agents => talents}/flow.md | 0 .../{agents => talents}/knowledge_graph.md | 0 .../20260310/{agents => talents}/meetings.md | 0 .../{agents => talents}/knowledge_graph.md | 0 .../facets/capulet/events/20260304.jsonl | 4 +- .../facets/capulet/events/20260306.jsonl | 2 +- .../facets/capulet/events/20260307.jsonl | 2 +- .../facets/capulet/events/20260310.jsonl | 2 +- .../facets/montague/events/20260304.jsonl | 4 +- .../facets/montague/events/20260305.jsonl | 2 +- .../facets/montague/events/20260306.jsonl | 2 +- .../facets/montague/events/20260307.jsonl | 4 +- .../facets/montague/events/20260309.jsonl | 2 +- .../facets/montague/events/20260310.jsonl | 2 +- .../facets/personal/events/20240101.jsonl | 2 +- .../facets/verona/events/20260305.jsonl | 2 +- .../facets/verona/events/20260306.jsonl | 2 +- .../facets/verona/events/20260308.jsonl | 2 +- .../facets/verona/events/20260309.jsonl | 2 +- .../facets/verona/events/20260310.jsonl | 2 +- .../journal/facets/work/events/20240101.jsonl | 4 +- .../journal/facets/work/events/20240105.jsonl | 2 +- .../000_migrate_agent_layout.jsonl | 2 +- .../001_migrate_agent_run_logs.jsonl | 2 +- tests/fixtures/journal/sol/briefing.md | 4 +- tests/fixtures/journal/talents/20231113.jsonl | 2 + tests/fixtures/journal/talents/20231114.jsonl | 4 + tests/fixtures/journal/talents/20260304.jsonl | 3 + tests/fixtures/journal/talents/20260305.jsonl | 3 + tests/fixtures/journal/talents/20260306.jsonl | 2 + tests/fixtures/journal/talents/20260307.jsonl | 2 + tests/fixtures/journal/talents/20260308.jsonl | 3 + tests/fixtures/journal/talents/20260309.jsonl | 1 + tests/fixtures/journal/talents/20260310.jsonl | 4 + tests/fixtures/journal/talents/default.log | 7 + .../default/1700000000001.jsonl | 14 +- .../default/1772737200001.jsonl | 14 +- .../default/1773187200001.jsonl | 10 +- .../journal/talents/flow/1700000000002.jsonl | 3 + tests/integration/test_anthropic_provider.py | 8 +- tests/integration/test_callosum.py | 4 +- tests/integration/test_cortex.py | 28 +- tests/integration/test_google_provider.py | 8 +- tests/integration/test_openai_provider.py | 14 +- tests/test_activities.py | 8 +- tests/test_activity_state.py | 104 ++-- tests/test_anthropic.py | 30 +- tests/test_app_calendar.py | 2 +- tests/test_app_sol.py | 38 +- tests/test_cluster.py | 48 +- tests/test_conversation.py | 10 +- tests/test_convey_apps.py | 26 +- tests/test_cortex.py | 285 +++++------ tests/test_cortex_client.py | 310 ++++++------ tests/test_dream_activity.py | 22 +- tests/test_dream_segment.py | 58 +-- tests/test_engage.py | 8 +- tests/test_entities.py | 4 +- tests/test_entity_observer_context.py | 4 +- ...ntity_agents.py => test_entity_talents.py} | 20 +- tests/test_facets.py | 12 +- tests/test_formatters.py | 24 +- tests/test_generate_full.py | 14 +- tests/test_generate_scan_day.py | 18 +- ...ate_agents.py => test_generate_talents.py} | 0 tests/test_google.py | 8 +- tests/test_google_thinking.py | 4 +- tests/test_heartbeat.py | 6 +- tests/test_home_events.py | 28 +- tests/test_home_yesterdays_processing.py | 78 ++- tests/test_journal_index.py | 51 +- tests/test_journal_stats.py | 10 +- tests/test_maint_004_rename.py | 98 ++++ tests/test_output_hooks.py | 16 +- tests/test_output_path.py | 16 +- tests/test_pipeline_health.py | 114 +++-- tests/test_pipeline_smoke.py | 14 +- ...gents_check.py => test_providers_check.py} | 136 +++--- tests/test_retention.py | 12 +- tests/test_routines.py | 22 +- tests/test_segment.py | 24 +- tests/test_sense_splitter.py | 20 +- tests/test_sol.py | 2 +- tests/test_speaker_attribution_hook.py | 10 +- tests/test_stats_contract.py | 12 +- tests/test_talent.py | 24 +- tests/test_talent_cli.py | 6 +- ...nt_fallback.py => test_talent_fallback.py} | 40 +- ...gents_ndjson.py => test_talents_ndjson.py} | 26 +- tests/verify_api.py | 4 +- think/activities.py | 2 +- think/chat_cli.py | 10 +- think/cluster.py | 10 +- think/conversation.py | 10 +- think/cortex.py | 313 ++++++------ think/cortex_client.py | 204 ++++---- think/dream.py | 258 +++++----- think/engage.py | 18 +- think/entities/activity.py | 2 +- think/entities/context.py | 2 +- think/events.py | 2 +- think/facets.py | 22 +- think/formatters.py | 24 +- think/heartbeat.py | 14 +- think/hooks.py | 12 +- think/indexer/journal.py | 18 +- think/journal_stats.py | 16 +- think/models.py | 2 +- think/pipeline_health.py | 43 +- think/providers/anthropic.py | 2 +- think/providers/google.py | 2 +- think/providers/openai.py | 4 +- think/providers/shared.py | 16 +- think/providers_cli.py | 337 +++++++++++++ think/retention.py | 4 +- think/routines.py | 12 +- think/segment.py | 38 +- think/sense_splitter.py | 4 +- think/stats_schema.py | 6 +- think/talent.py | 136 +++--- think/talent_cli.py | 74 +-- think/{agents.py => talents.py} | 445 +++--------------- think/tools/call.py | 14 +- think/tools/sol.py | 8 +- 272 files changed, 2915 insertions(+), 2456 deletions(-) create mode 100644 apps/sol/maint/004_rename_agents_to_talents.py create mode 100644 scripts/gate_agents_rename.py rename tests/baselines/api/sol/{agents-day.json => talents-day.json} (99%) rename tests/baselines/api/{agents => talents}/badge-count.json (100%) rename tests/baselines/api/{agents => talents}/preview.json (100%) rename tests/baselines/api/{agents => talents}/run-detail.json (86%) rename tests/baselines/api/{agents => talents}/stats-month.json (100%) rename tests/baselines/api/{agents/agents-day.json => talents/talents-day.json} (99%) rename tests/baselines/api/{agents => talents}/updated-days.json (100%) delete mode 100644 tests/fixtures/journal/agents/20231113.jsonl delete mode 100644 tests/fixtures/journal/agents/20231114.jsonl delete mode 100644 tests/fixtures/journal/agents/20260304.jsonl delete mode 100644 tests/fixtures/journal/agents/20260305.jsonl delete mode 100644 tests/fixtures/journal/agents/20260306.jsonl delete mode 100644 tests/fixtures/journal/agents/20260307.jsonl delete mode 100644 tests/fixtures/journal/agents/20260308.jsonl delete mode 100644 tests/fixtures/journal/agents/20260309.jsonl delete mode 100644 tests/fixtures/journal/agents/20260310.jsonl delete mode 120000 tests/fixtures/journal/agents/default.log delete mode 100644 tests/fixtures/journal/agents/flow/1700000000002.jsonl rename tests/fixtures/journal/chronicle/20240101/default/123456_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20240101/default/123456_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20240101/{agents => talents}/flow.md (100%) rename tests/fixtures/journal/chronicle/20240101/{agents => talents}/knowledge_graph.md (100%) rename tests/fixtures/journal/chronicle/20240101/{agents => talents}/meetings.md (100%) rename tests/fixtures/journal/chronicle/20240102/default/234567_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20240102/default/234567_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20240102/{agents => talents}/flow.md (100%) rename tests/fixtures/journal/chronicle/20260304/default/090000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260304/default/090000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260304/default/090000_300/{agents => talents}/speaker_labels.json (100%) rename tests/fixtures/journal/chronicle/20260304/default/090000_300/{agents => talents}/speakers.json (100%) rename tests/fixtures/journal/chronicle/20260304/default/140000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260304/default/140000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260304/default/180000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260304/default/180000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260304/{agents => talents}/flow.md (100%) rename tests/fixtures/journal/chronicle/20260304/{agents => talents}/knowledge_graph.md (100%) rename tests/fixtures/journal/chronicle/20260304/{agents => talents}/meetings.md (100%) rename tests/fixtures/journal/chronicle/20260305/default/090000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260305/default/090000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260305/default/133000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260305/default/133000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260305/default/220000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260305/default/220000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260305/{agents => talents}/flow.md (100%) rename tests/fixtures/journal/chronicle/20260305/{agents => talents}/meetings.md (100%) rename tests/fixtures/journal/chronicle/20260306/default/093000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260306/default/093000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260306/default/110000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260306/default/110000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260306/default/143000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260306/default/143000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260306/default/170000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260306/default/170000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260306/{agents => talents}/flow.md (100%) rename tests/fixtures/journal/chronicle/20260306/{agents => talents}/knowledge_graph.md (100%) rename tests/fixtures/journal/chronicle/20260307/default/100000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260307/default/150000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260307/{agents => talents}/flow.md (100%) rename tests/fixtures/journal/chronicle/20260307/{agents => talents}/meetings.md (100%) rename tests/fixtures/journal/chronicle/20260308/default/100000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260308/default/153000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260308/{agents => talents}/flow.md (100%) rename tests/fixtures/journal/chronicle/20260308/{agents => talents}/knowledge_graph.md (100%) rename tests/fixtures/journal/chronicle/20260308/{agents => talents}/meetings.md (100%) rename tests/fixtures/journal/chronicle/20260309/default/090000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260309/default/090000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260309/default/133000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260309/default/133000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260309/default/193000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260309/default/193000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260309/{agents => talents}/flow.md (100%) rename tests/fixtures/journal/chronicle/20260310/default/083000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260310/default/083000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260310/default/100000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260310/default/170000_300/{agents => talents}/audio.md (100%) rename tests/fixtures/journal/chronicle/20260310/default/170000_300/{agents => talents}/screen.md (100%) rename tests/fixtures/journal/chronicle/20260310/{agents => talents}/flow.md (100%) rename tests/fixtures/journal/chronicle/20260310/{agents => talents}/knowledge_graph.md (100%) rename tests/fixtures/journal/chronicle/20260310/{agents => talents}/meetings.md (100%) rename tests/fixtures/journal/chronicle/20260415/{agents => talents}/knowledge_graph.md (100%) rename tests/fixtures/journal/maint/{agents => sol}/000_migrate_agent_layout.jsonl (66%) rename tests/fixtures/journal/maint/{agents => sol}/001_migrate_agent_run_logs.jsonl (74%) create mode 100644 tests/fixtures/journal/talents/20231113.jsonl create mode 100644 tests/fixtures/journal/talents/20231114.jsonl create mode 100644 tests/fixtures/journal/talents/20260304.jsonl create mode 100644 tests/fixtures/journal/talents/20260305.jsonl create mode 100644 tests/fixtures/journal/talents/20260306.jsonl create mode 100644 tests/fixtures/journal/talents/20260307.jsonl create mode 100644 tests/fixtures/journal/talents/20260308.jsonl create mode 100644 tests/fixtures/journal/talents/20260309.jsonl create mode 100644 tests/fixtures/journal/talents/20260310.jsonl create mode 100644 tests/fixtures/journal/talents/default.log rename tests/fixtures/journal/{agents => talents}/default/1700000000001.jsonl (55%) rename tests/fixtures/journal/{agents => talents}/default/1772737200001.jsonl (62%) rename tests/fixtures/journal/{agents => talents}/default/1773187200001.jsonl (64%) create mode 100644 tests/fixtures/journal/talents/flow/1700000000002.jsonl rename tests/{test_entity_agents.py => test_entity_talents.py} (88%) rename tests/{test_generate_agents.py => test_generate_talents.py} (100%) create mode 100644 tests/test_maint_004_rename.py rename tests/{test_agents_check.py => test_providers_check.py} (76%) rename tests/{test_agent_fallback.py => test_talent_fallback.py} (92%) rename tests/{test_agents_ndjson.py => test_talents_ndjson.py} (91%) create mode 100644 think/providers_cli.py rename think/{agents.py => talents.py} (73%) diff --git a/AGENTS.md b/AGENTS.md index 40f42c7e5..a7ad38578 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ This is the developer-facing documentation for the solstone codebase. If you're - **Journal**: Central data structure organized as `journal/YYYYMMDD/` directories. All captured data, transcripts, and analysis artifacts are stored here. - **Facets**: Project/context organization system that groups related content and provides scoped views of entities, tasks, and activities. - **Entities**: Extracted information tracked over time across transcripts and interactions and associated with facets for semantic navigation. -- **Agents**: AI processors with configurable prompts that analyze content, extract insights, and respond to queries. +- **Talents**: AI processors with configurable prompts that analyze content, extract insights, and respond to queries. - **Callosum**: Message bus that enables asynchronous communication between components. - **Indexer**: Builds and maintains a SQLite database from journal data, enabling fast search and retrieval. @@ -24,37 +24,37 @@ This is the developer-facing documentation for the solstone codebase. If you're **Component communication**: - Callosum enables async communication between services. -- Cortex orchestrates AI agent execution via `sol cortex`, spawning agent subprocesses with agent configurations. +- Cortex orchestrates AI talent execution via `sol cortex`, spawning talent subprocesses with talent configurations. - The unified CLI is `sol`. Run `sol` to see status and available commands. ## Quick Commands ```bash make install # Install package (includes all deps) -make skills # Discover and symlink Agent Skills from talent/ dirs +make skills # Discover and symlink Anthropic Skills from talent/ dirs make format # Auto-fix formatting, then report remaining issues make test # Run unit tests make ci # Full CI check (format check + lint + test) make dev # Start stack (Ctrl+C to stop) ``` -## Agent CLI Boundaries +## Talent CLI Boundaries -Cogitate agents have access to all `sol` commands. The following infrastructure commands must never be called by agents because they manage services and data pipelines that should only be operated by the supervisor or a human operator: +Cogitate talents have access to all `sol` commands. The following infrastructure commands must never be called by talents because they manage services and data pipelines that should only be operated by the supervisor or a human operator: - `sol supervisor` / `sol start` - `sol dream` except heartbeat's targeted `sol dream --segment` - `sol import` - `sol config` - `sol cortex` -- `sol agents` +- `sol providers check` - `sol callosum` - `sol observer` / `sol observe-*` - `sol sense` - `sol transcribe` / `sol describe` - `sol indexer --reset` -Agents should use `sol call` commands for journal interaction and `sol health` / `sol talent logs` for diagnostics. +Talents should use `sol call` commands for journal interaction and `sol health` / `sol talent logs` for diagnostics. ## Reference diff --git a/Makefile b/Makefile index 404b894d4..c9fa2b187 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # solstone Makefile # Python-based AI-driven desktop journaling toolkit -.PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format format-check ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sail upgrade sandbox sandbox-stop install-pinchtab verify-browser update-browser-baselines review verify-api update-api-baselines install-service uninstall-service service-logs +.PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format format-check ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sail upgrade sandbox sandbox-stop install-pinchtab verify-browser update-browser-baselines review verify-api update-api-baselines install-service uninstall-service service-logs gate-agents-rename # Default target - install package in editable mode all: install @@ -404,6 +404,9 @@ ci: .installed @echo "=== Running ruff ===" @$(RUFF) check . || { echo "Run 'make format' to auto-fix"; exit 1; } @echo "" + @echo "=== Running rename gate ===" + @$(MAKE) gate-agents-rename + @echo "" @echo "=== Running mypy ===" @$(MYPY) . || true @echo "" @@ -451,3 +454,6 @@ pre-commit: .installed @$(UV) pip show pre-commit >/dev/null 2>&1 || { echo "Installing pre-commit..."; $(UV) pip install pre-commit; } $(VENV_BIN)/pre-commit install @echo "Pre-commit hooks installed!" +# Rename guard for the agents -> talents transition +gate-agents-rename: .installed + $(VENV_BIN)/python scripts/gate_agents_rename.py diff --git a/apps/entities/routes.py b/apps/entities/routes.py index 05388a040..7abdf94ae 100644 --- a/apps/entities/routes.py +++ b/apps/entities/routes.py @@ -521,15 +521,15 @@ def generate_description(facet_name: str) -> Any: f"Current Description: {current_desc}" ) - agent_id = spawn_agent( + use_id = spawn_agent( prompt=prompt, name="entities:entity_describe", provider="google", ) - if agent_id is None: + if use_id is None: return jsonify({"error": "Failed to connect to agent service"}), 503 - return jsonify({"success": True, "agent_id": agent_id}) + return jsonify({"success": True, "use_id": use_id}) except Exception as e: return ( @@ -556,14 +556,14 @@ def assist_add(facet_name: str) -> Any: prompt = f"For the '{facet_name}' facet, this is the user's request to attach a new entity: {name}" # Create agent request - entity_assist agent already has provider configured - agent_id = spawn_agent( + use_id = spawn_agent( prompt=prompt, name="entities:entity_assist", ) - if agent_id is None: + if use_id is None: return jsonify({"error": "Failed to connect to agent service"}), 503 - return jsonify({"success": True, "agent_id": agent_id}) + return jsonify({"success": True, "use_id": use_id}) except Exception as e: return jsonify({"error": f"Failed to start entity assistant: {str(e)}"}), 500 diff --git a/apps/entities/talent/entity_observer.md b/apps/entities/talent/entity_observer.md index 6705bd4fa..4740ecc96 100644 --- a/apps/entities/talent/entity_observer.md +++ b/apps/entities/talent/entity_observer.md @@ -12,7 +12,7 @@ "output": "json", "thinking_budget": 2048, "hook": {"pre": "entities:entity_observer", "post": "entities:entity_observer"}, - "load": {"transcripts": false, "percepts": false, "agents": false} + "load": {"transcripts": false, "percepts": false, "talents": false} } ## Core Mission diff --git a/apps/entities/workspace.html b/apps/entities/workspace.html index 7c561a196..a8eadc23e 100644 --- a/apps/entities/workspace.html +++ b/apps/entities/workspace.html @@ -1480,8 +1480,8 @@ let entitiesData = null; let detectedPage = 1; let journalEntitiesData = null; // For all-facet mode let currentDetailEntity = null; -const pendingEntities = new Map(); // agent_id → { name, element } -const pendingAgentCallbacks = new Map(); // agent_id → callback function +const pendingEntities = new Map(); // use_id → { name, element } +const pendingAgentCallbacks = new Map(); // use_id → callback function const _errorTimers = {}; // Standard entity types - fetched from server @@ -2361,8 +2361,8 @@ function startDescriptionEdit(entity) { }) .then(response => response.json()) .then(data => { - if (data.success && data.agent_id) { - listenForAgentCompletion(data.agent_id, (result) => { + if (data.success && data.use_id) { + listenForAgentCompletion(data.use_id, (result) => { if (result.success && result.response) { saveDescription(entity, result.response); } else { @@ -3185,12 +3185,12 @@ function submitEntityAssist() { }) .then(response => response.json()) .then(data => { - if (data.success && data.agent_id) { + if (data.success && data.use_id) { const pending = pendingEntities.get(tempId); if (pending) { pendingEntities.delete(tempId); - pendingEntities.set(data.agent_id, pending); - pending.element.dataset.agentId = data.agent_id; + pendingEntities.set(data.use_id, pending); + pending.element.dataset.agentId = data.use_id; } } else { throw new Error(data.error || 'Failed to start assistant'); @@ -3210,7 +3210,7 @@ function setupCortexListener() { } window.appEvents.listen('cortex', (msg) => { - const agentId = msg.agent_id; + const agentId = msg.use_id; if (!agentId) return; // Pending entity additions diff --git a/apps/health/talent/health/SKILL.md b/apps/health/talent/health/SKILL.md index 74e2b5fc6..fa642ee3a 100644 --- a/apps/health/talent/health/SKILL.md +++ b/apps/health/talent/health/SKILL.md @@ -80,7 +80,7 @@ List recent agent runs. Flags compose with AND logic. For example, `--daily --errors` shows only daily runs that errored. -Output columns: agent_id, time, name, status, runtime, cost, events, tools, output_size, model, facet. +Output columns: use_id, time, name, status, runtime, cost, events, tools, output_size, model, facet. Examples: diff --git a/apps/health/tests/test_call.py b/apps/health/tests/test_call.py index 38c1f9c88..5c9f4c61b 100644 --- a/apps/health/tests/test_call.py +++ b/apps/health/tests/test_call.py @@ -63,8 +63,8 @@ def test_pipeline_with_real_fixture(health_env): "\n".join( [ json.dumps({"event": "run.start", "mode": "segment"}), - json.dumps({"event": "agent.dispatch", "mode": "segment"}), - json.dumps({"event": "agent.complete", "mode": "segment"}), + json.dumps({"event": "talent.dispatch", "mode": "segment"}), + json.dumps({"event": "talent.complete", "mode": "segment"}), json.dumps( {"event": "run.complete", "mode": "segment", "duration_ms": 42} ), diff --git a/apps/health/tests/test_routes.py b/apps/health/tests/test_routes.py index 87221fba7..ec2dd487c 100644 --- a/apps/health/tests/test_routes.py +++ b/apps/health/tests/test_routes.py @@ -29,7 +29,7 @@ class TestLogRoute: def test_path_outside_health_dir_rejected(self, health_env): env = health_env() - resp = env.client.get("/app/health/api/log?path=20260322/agents/something.log") + resp = env.client.get("/app/health/api/log?path=20260322/talents/something.log") assert resp.status_code == 400 def test_missing_file_returns_404(self, health_env): diff --git a/apps/health/workspace.html b/apps/health/workspace.html index 858f33f3f..3728f5826 100644 --- a/apps/health/workspace.html +++ b/apps/health/workspace.html @@ -2278,12 +2278,12 @@ const k = child.getAttribute('data-key'); if (k) existingByKey.set(k, child); } - const newKeys = new Set(activeAgents.map(a => a.agent_id)); + const newKeys = new Set(activeAgents.map(a => a.use_id)); for (const [k, child] of existingByKey) { if (!newKeys.has(k)) container.removeChild(child); } for (const agent of activeAgents) { - const key = agent.agent_id; + const key = agent.use_id; let card = existingByKey.get(key); if (!card) { card = document.createElement('div'); @@ -2309,7 +2309,7 @@ const stateLabel = agent.event === 'thinking' ? 'Thinking...' : (agent.event === 'tool_start' || agent.event === 'tool_end') ? 'Working...' : 'Running...'; const elapsed = agent.elapsed_seconds ? formatElapsed(agent.elapsed_seconds) : '0s'; - card.children[0].textContent = '...' + getAgentId(agent.agent_id); + card.children[0].textContent = '...' + getAgentId(agent.use_id); card.children[1].textContent = agent.name || 'default'; card.children[2].textContent = stateLabel; card.children[3].textContent = elapsed; @@ -2696,27 +2696,27 @@ } function handleCortexEvent(msg) { - // Handle status event first (no agent_id at top level) + // Handle status event first (no use_id at top level) if (msg.event === 'status') { // Update agent count for vitals - state.agentCount = msg.running_agents || 0; + state.agentCount = msg.running_uses || 0; - // Status event contains array of agents - if (msg.agents) { - // Clear agents not in status (they finished) - const activeIds = new Set(msg.agents.map(a => a.agent_id)); + // Status event contains array of uses + if (msg.uses) { + // Clear uses not in status (they finished) + const activeIds = new Set(msg.uses.map(a => a.use_id)); state.agents.forEach((_, id) => { if (!activeIds.has(id)) { state.agents.delete(id); } }); - // Update/add agents from status - msg.agents.forEach(agent => { - const existing = state.agents.get(agent.agent_id) || {}; - state.agents.set(agent.agent_id, { + // Update/add uses from status + msg.uses.forEach(agent => { + const existing = state.agents.get(agent.use_id) || {}; + state.agents.set(agent.use_id, { ...existing, - agent_id: agent.agent_id, + use_id: agent.use_id, name: agent.name, provider: agent.provider, elapsed_seconds: agent.elapsed_seconds, @@ -2730,8 +2730,8 @@ return; } - // Individual agent events require agent_id - const agentId = msg.agent_id; + // Individual agent events require use_id + const agentId = msg.use_id; if (!agentId) return; // Track start time for client-side elapsed updates @@ -2740,7 +2740,7 @@ state.agents.set(agentId, { ...existing, - agent_id: agentId, + use_id: agentId, name: msg.name || existing.name, provider: msg.provider || existing.provider, event: msg.event, diff --git a/apps/home/events.py b/apps/home/events.py index c47550b31..29769590a 100644 --- a/apps/home/events.py +++ b/apps/home/events.py @@ -11,7 +11,7 @@ import logging from apps.events import EventContext, on_event from think.conversation import record_exchange -from think.cortex_client import read_agent_events +from think.cortex_client import read_use_events logger = logging.getLogger(__name__) @@ -25,12 +25,12 @@ def record_triage_exchange(ctx: EventContext) -> None: if name not in TRIAGE_AGENT_NAMES: return - agent_id = ctx.msg.get("agent_id") - if not agent_id: + use_id = ctx.msg.get("use_id") + if not use_id: return try: - events = read_agent_events(agent_id) + events = read_use_events(use_id) facet = "" app = "" path = "" @@ -51,11 +51,11 @@ def record_triage_exchange(ctx: EventContext) -> None: user_message=user_message, agent_response=result, talent=name, - agent_id=agent_id, + use_id=use_id, ) except Exception: logger.debug( "Failed to record conversation exchange for agent %s", - agent_id, + use_id, exc_info=True, ) diff --git a/apps/home/routes.py b/apps/home/routes.py index e85f72317..14c0f4435 100644 --- a/apps/home/routes.py +++ b/apps/home/routes.py @@ -97,7 +97,7 @@ def _load_flow_md(today: str) -> tuple[str | None, float | None]: """Load today's flow.md content and mtime. Returns (content, mtime) or (None, None).""" try: journal = Path(get_journal()) - flow_path = journal / today / "agents" / "flow.md" + flow_path = journal / today / "talents" / "flow.md" if flow_path.exists(): return flow_path.read_text(), flow_path.stat().st_mtime except Exception: @@ -508,7 +508,7 @@ def _top_heatmap_hours(stats_data: dict[str, Any]) -> list[int]: def _knowledge_graph_freshness(yesterday: str) -> dict[str, Any]: path = ( - Path(get_journal()) / "chronicle" / yesterday / "agents" / "knowledge_graph.md" + Path(get_journal()) / "chronicle" / yesterday / "talents" / "knowledge_graph.md" ) if not path.exists(): return {"exists": False, "fresh": False, "updated_label": None} @@ -584,8 +584,9 @@ def _newsletter_attempts_from_dream_logs(yesterday: str) -> tuple[int, int]: record = json.loads(line) except json.JSONDecodeError: continue + # HISTORICAL SHIM: accept legacy agent.* chronicle event names from before 2026-04-17; sunset 2026-05-01 if ( - record.get("event") == "agent.fail" + record.get("event") in {"agent.fail", "talent.fail"} and record.get("facet") and record.get("name") == "facet_newsletter" ): @@ -765,7 +766,7 @@ def _format_gap_bullets( has_activity = any( anomaly.get("kind") == "activity_agents_missing" for anomaly in anomalies ) - has_failure = any(anomaly.get("kind") == "agent_failure" for anomaly in anomalies) + has_failure = any(anomaly.get("kind") == "talent_failure" for anomaly in anomalies) if has_daily: bullets.append("I didn't finish the full overnight review.") diff --git a/apps/search/routes.py b/apps/search/routes.py index f35b98ef9..2e295a42e 100644 --- a/apps/search/routes.py +++ b/apps/search/routes.py @@ -138,7 +138,7 @@ def search_journal_api() -> Any: - total: Total match count - days: List of day groups, each with date info and results - facets: List of facets with counts for filter sidebar - - agents: List of agents with counts for filter sidebar + - talents: List of talents with counts for filter sidebar """ query = request.args.get("q", "").strip() @@ -233,7 +233,7 @@ def search_journal_api() -> Any: "showing_days": len(days_response), "days": days_response, "facets": facets_list, - "agents": agents_list, + "talents": agents_list, } ) diff --git a/apps/sol/maint/004_rename_agents_to_talents.py b/apps/sol/maint/004_rename_agents_to_talents.py new file mode 100644 index 000000000..ccff9d855 --- /dev/null +++ b/apps/sol/maint/004_rename_agents_to_talents.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Rename live journal agents paths to talents.""" + +from __future__ import annotations + +import argparse +import sys +from dataclasses import dataclass +from pathlib import Path + +from think.utils import day_dirs, get_journal, iter_segments, setup_cli + + +@dataclass +class RenameSummary: + discovered: int = 0 + moved: int = 0 + skipped: int = 0 + errors: int = 0 + collisions: int = 0 + + +def discover_moves(journal_path: Path) -> tuple[list[tuple[Path, Path]], list[Path]]: + """Return planned (src, dst) moves and already-migrated destinations.""" + planned: list[tuple[Path, Path]] = [] + skipped: list[Path] = [] + + def add_pair(src: Path, dst: Path) -> None: + if src.exists(): + planned.append((src, dst)) + elif dst.exists(): + skipped.append(dst) + + add_pair(journal_path / "agents", journal_path / "talents") + add_pair( + journal_path / "health" / "agents.json", + journal_path / "health" / "talents.json", + ) + + for day_name, day_abs in sorted(day_dirs().items()): + day_dir = Path(day_abs) + if not day_dir.is_dir(): + continue + + add_pair(day_dir / "agents", day_dir / "talents") + + for _stream, _segment, seg_path in iter_segments(day_name): + add_pair(seg_path / "agents", seg_path / "talents") + + return planned, skipped + + +def run_migration( + journal_path: Path, *, dry_run: bool +) -> tuple[RenameSummary, list[tuple[Path, Path]]]: + """Run or preview the agents->talents path rename.""" + summary = RenameSummary() + planned, skipped = discover_moves(journal_path) + summary.discovered = len(planned) + summary.skipped = len(skipped) + + collisions = [(src, dst) for src, dst in planned if dst.exists()] + summary.collisions = len(collisions) + if collisions: + return summary, collisions + + for src, dst in planned: + print(f"{'[DRY-RUN] ' if dry_run else ''}move {src} -> {dst}") + if dry_run: + continue + + try: + dst.parent.mkdir(parents=True, exist_ok=True) + src.rename(dst) + summary.moved += 1 + except Exception as exc: + summary.errors += 1 + print(f"[ERROR] move failed: {src} -> {dst}: {exc}") + + if dry_run: + summary.moved = len(planned) + + return summary, [] + + +def _print_summary(summary: RenameSummary) -> None: + print("Summary") + print(f" discovered: {summary.discovered}") + print(f" moved: {summary.moved}") + print(f" skipped: {summary.skipped}") + print(f" errors: {summary.errors}") + print(f" collisions: {summary.collisions}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Rename live journal agents paths to talents." + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Preview planned renames without writing files.", + ) + args = setup_cli(parser) + + journal_path = Path(get_journal()) + summary, collisions = run_migration(journal_path, dry_run=args.dry_run) + + if collisions: + print("Collision(s) detected; no files were moved:") + for src, dst in collisions: + print(f" {src} -> {dst}") + _print_summary(summary) + sys.exit(2) + + _print_summary(summary) + if summary.errors: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/apps/sol/routes.py b/apps/sol/routes.py index 8ab8b7922..d2cbdd3f6 100644 --- a/apps/sol/routes.py +++ b/apps/sol/routes.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Agents app - browse historical agent runs by day and facet.""" +"""Talents app - browse historical talent uses by day and facet.""" from __future__ import annotations @@ -73,19 +73,19 @@ def _get_facet_filter() -> str | None: return facet -def _agent_id_to_day(agent_id: str) -> str: - """Convert agent_id (millisecond timestamp) to YYYYMMDD day string.""" +def _use_id_to_day(use_id: str) -> str: + """Convert use_id (millisecond timestamp) to YYYYMMDD day string.""" try: - ts = int(agent_id) / 1000 + ts = int(use_id) / 1000 return datetime.fromtimestamp(ts).strftime("%Y%m%d") except (ValueError, OSError): return "" -def _parse_agent_events( +def _parse_use_events( lines: list[str], *, collect_events: bool = False ) -> dict[str, Any]: - """Parse agent event lines and extract counts and cost data. + """Parse use event lines and extract counts and cost data. Args: lines: List of JSONL lines @@ -142,18 +142,18 @@ def _parse_agent_events( return result -def _parse_agent_file(agent_file: Path) -> dict[str, Any] | None: - """Parse agent JSONL file and extract metadata. +def _parse_use_file(use_file: Path) -> dict[str, Any] | None: + """Parse a use JSONL file and extract metadata. Returns dict with: id, name, start, status, prompt, facet, failed, runtime_seconds, thinking_count, tool_count, cost, model, provider, error_message. Returns None if file cannot be parsed. """ - from think.cortex_client import get_agent_end_state + from think.cortex_client import get_use_end_state try: - with open(agent_file, "r") as f: + with open(use_file, "r") as f: lines = f.readlines() if not lines: @@ -167,15 +167,14 @@ def _parse_agent_file(agent_file: Path) -> dict[str, Any] | None: if request_event.get("event") != "request": return None - # Extract agent ID from filename - is_active = "_active.jsonl" in agent_file.name - agent_id = agent_file.stem.replace("_active", "") + is_active = "_active.jsonl" in use_file.name + use_id = use_file.stem.replace("_active", "") # Parse events using shared helper - event_data = _parse_agent_events(lines[1:]) + event_data = _parse_use_events(lines[1:]) - agent_info: dict[str, Any] = { - "id": agent_id, + use_info: dict[str, Any] = { + "id": use_id, "name": request_event.get("name", "unified"), "start": request_event.get("ts", 0), "status": "running" if is_active else "completed", @@ -203,38 +202,36 @@ def _parse_agent_file(agent_file: Path) -> dict[str, Any] | None: output_file = str(out_path.relative_to(day_dir)) else: output_file = str(out_path.relative_to(state.journal_root)) - agent_info["output_file"] = output_file + use_info["output_file"] = output_file - # For completed agents, determine end state and calculate cost + # For completed uses, determine end state and calculate cost if not is_active: - end_state = get_agent_end_state(agent_id) - agent_info["failed"] = end_state in ("error", "unknown") + end_state = get_use_end_state(use_id) + use_info["failed"] = end_state in ("error", "unknown") # Calculate runtime from finish or error timestamp end_ts = event_data["finish_ts"] or event_data["error_ts"] - if end_ts and agent_info["start"]: - agent_info["runtime_seconds"] = (end_ts - agent_info["start"]) / 1000.0 + if end_ts and use_info["start"]: + use_info["runtime_seconds"] = (end_ts - use_info["start"]) / 1000.0 # Calculate cost - agent_info["cost"] = calc_agent_cost( - event_data["model"], event_data["usage"] - ) + use_info["cost"] = calc_agent_cost(event_data["model"], event_data["usage"]) - return agent_info + return use_info except (json.JSONDecodeError, IOError): return None -def _get_agent_day(agent_file: Path) -> str: - """Get the logical day for an agent from its request event. +def _get_use_day(use_file: Path) -> str: + """Get the logical day for a use from its request event. Prefers the ``day`` field from the request event (the day being processed) - over the agent_id timestamp (when the agent actually ran). This ensures - overnight dream agents appear under the day they processed. + over the use_id timestamp (when the agent actually ran). This ensures + overnight dream uses appear under the day they processed. """ - agent_id = agent_file.stem.replace("_active", "") + use_id = use_file.stem.replace("_active", "") try: - with open(agent_file, "r") as f: + with open(use_file, "r") as f: first_line = f.readline().strip() if first_line: request_event = json.loads(first_line) @@ -243,29 +240,29 @@ def _get_agent_day(agent_file: Path) -> str: return req_day except (json.JSONDecodeError, IOError): pass - return _agent_id_to_day(agent_id) + return _use_id_to_day(use_id) -def _get_agents_for_day(day: str, facet_filter: str | None = None) -> list[dict]: - """Get all agent runs for a specific day. +def _get_uses_for_day(day: str, facet_filter: str | None = None) -> list[dict]: + """Get all talent uses for a specific day. - Uses the day index file for fast lookup instead of scanning all agent files. + Uses the day index file for fast lookup instead of scanning all use files. Args: day: YYYYMMDD day string facet_filter: Optional facet to filter by (None = all facets) Returns: - List of agent info dicts sorted by start time (newest first) + List of use info dicts sorted by start time (newest first) """ - agents_dir = Path(state.journal_root) / "agents" - if not agents_dir.exists(): + talents_dir = Path(state.journal_root) / "talents" + if not talents_dir.exists(): return [] - agents = [] + uses = [] - # Read day index for completed agents - day_index_path = agents_dir / f"{day}.jsonl" + # Read day index for completed uses + day_index_path = talents_dir / f"{day}.jsonl" if day_index_path.exists(): try: with open(day_index_path, "r") as f: @@ -283,53 +280,53 @@ def _get_agents_for_day(day: str, facet_filter: str | None = None) -> list[dict] continue # Locate the actual file for full parsing - agent_id = entry.get("agent_id", "") + use_id = entry.get("use_id", "") name = entry.get("name", "unified") safe_name = name.replace(":", "--") - agent_file = agents_dir / safe_name / f"{agent_id}.jsonl" - if not agent_file.exists(): + use_file = talents_dir / safe_name / f"{use_id}.jsonl" + if not use_file.exists(): continue - agent_info = _parse_agent_file(agent_file) - if agent_info: - agents.append(agent_info) + use_info = _parse_use_file(use_file) + if use_info: + uses.append(use_info) except IOError: pass - # Also check for running agents (only have _active files, no day index entry yet) - for agent_file in agents_dir.glob("*/*_active.jsonl"): - if "_pending" in agent_file.name: + # Also check for running uses (only have _active files, no day index entry yet) + for use_file in talents_dir.glob("*/*_active.jsonl"): + if "_pending" in use_file.name: continue - if _get_agent_day(agent_file) != day: + if _get_use_day(use_file) != day: continue - agent_info = _parse_agent_file(agent_file) - if not agent_info: + use_info = _parse_use_file(use_file) + if not use_info: continue - if facet_filter is not None and agent_info.get("facet") != facet_filter: + if facet_filter is not None and use_info.get("facet") != facet_filter: continue - agents.append(agent_info) + uses.append(use_info) # Sort by start time (newest first) - agents.sort(key=lambda x: x["start"], reverse=True) - return agents + uses.sort(key=lambda x: x["start"], reverse=True) + return uses @lru_cache(maxsize=1) -def _build_agents_meta() -> dict[str, dict[str, Any]]: - """Build agent metadata dict from all talent configs. +def _build_talents_meta() -> dict[str, dict[str, Any]]: + """Build talent metadata dict from all talent configs. - Returns dict mapping agent name to metadata with capability fields + Returns dict mapping talent name to metadata with capability fields for frontend display. Cached for process lifetime since talent configs are static. """ configs = get_talent_configs(include_disabled=True) - agents: dict[str, dict[str, Any]] = {} + talents: dict[str, dict[str, Any]] = {} for name, config in configs.items(): - agents[name] = { + talents[name] = { "title": config.get("title", name), "description": config.get("description"), "color": config.get("color", "#6c757d"), @@ -341,7 +338,7 @@ def _build_agents_meta() -> dict[str, dict[str, Any]]: "multi_facet": bool(config.get("multi_facet")), } - return agents + return talents # ============================================================================= @@ -351,14 +348,14 @@ def _build_agents_meta() -> dict[str, dict[str, Any]]: @sol_bp.route("/") def index() -> Any: - """Redirect to today's agent history.""" + """Redirect to today's talent history.""" today = date.today().strftime("%Y%m%d") - return redirect(url_for("app:sol.agents_day", day=today)) + return redirect(url_for("app:sol.talents_day", day=today)) @sol_bp.route("/") -def agents_day(day: str) -> str: - """Render agent history viewer for a specific day.""" +def talents_day(day: str) -> str: + """Render talent history viewer for a specific day.""" if not DATE_RE.fullmatch(day): return "", 404 @@ -372,9 +369,9 @@ def agents_day(day: str) -> str: # ============================================================================= -@sol_bp.route("/api/agents/") -def api_agents_day(day: str) -> Any: - """Get agent runs and metadata for a specific day. +@sol_bp.route("/api/talents/") +def api_talents_day(day: str) -> Any: + """Get talent uses and metadata for a specific day. Returns flat data for frontend grouping/rendering. @@ -383,8 +380,8 @@ def api_agents_day(day: str) -> Any: Returns: { - "runs": [run objects...], - "agents": {name: metadata...}, + "uses": [use objects...], + "talents": {name: metadata...}, "facets": {name: {title, color}...} } """ @@ -393,8 +390,8 @@ def api_agents_day(day: str) -> Any: facet_filter = _get_facet_filter() - runs = _get_agents_for_day(day, facet_filter) - agents = _build_agents_meta() + uses = _get_uses_for_day(day, facet_filter) + talents = _build_talents_meta() facets = { name: {"title": f.get("title", name), "color": f.get("color")} for name, f in get_facets().items() @@ -402,49 +399,48 @@ def api_agents_day(day: str) -> Any: return jsonify( { - "runs": runs, - "agents": agents, + "uses": uses, + "talents": talents, "facets": facets, } ) -@sol_bp.route("/api/run/") -def api_agent_run(agent_id: str) -> Any: - """Return full agent run detail with metadata and parsed events.""" - # Locate the agent JSONL file +@sol_bp.route("/api/run/") +def api_agent_run(use_id: str) -> Any: + """Return full talent-use detail with metadata and parsed events.""" + # Locate the use JSONL file journal_path = Path(state.journal_root) - agents_dir = journal_path / "agents" - # Search subdirectories for the agent file - agent_file = None - for match in agents_dir.glob(f"*/{agent_id}.jsonl"): - agent_file = match + talents_dir = journal_path / "talents" + # Search subdirectories for the use file + use_file = None + for match in talents_dir.glob(f"*/{use_id}.jsonl"): + use_file = match break - if not agent_file: - # Check if the agent is still running - for match in agents_dir.glob(f"*/{agent_id}_active.jsonl"): - return jsonify({"error": "Agent run is still in progress"}), 202 - return jsonify({"error": f"Agent run {agent_id} not found"}), 404 + if not use_file: + for match in talents_dir.glob(f"*/{use_id}_active.jsonl"): + return jsonify({"error": "Talent run is still in progress"}), 202 + return jsonify({"error": f"Talent run {use_id} not found"}), 404 try: - from think.cortex_client import get_agent_end_state + from think.cortex_client import get_use_end_state - with open(agent_file, "r", encoding="utf-8") as f: + with open(use_file, "r", encoding="utf-8") as f: lines = f.readlines() if not lines: - return jsonify({"error": f"Agent run {agent_id} is malformed"}), 500 + return jsonify({"error": f"Talent run {use_id} is malformed"}), 500 first_line = lines[0].strip() if not first_line: - return jsonify({"error": f"Agent run {agent_id} is malformed"}), 500 + return jsonify({"error": f"Talent run {use_id} is malformed"}), 500 request_event = json.loads(first_line) if request_event.get("event") != "request": - return jsonify({"error": f"Agent run {agent_id} is malformed"}), 500 + return jsonify({"error": f"Talent run {use_id} is malformed"}), 500 - event_data = _parse_agent_events(lines[1:], collect_events=True) + event_data = _parse_use_events(lines[1:], collect_events=True) output_file = None req_output = request_event.get("output") @@ -464,10 +460,10 @@ def api_agent_run(agent_id: str) -> Any: if end_ts and start_ts: runtime_seconds = (end_ts - start_ts) / 1000.0 - end_state = get_agent_end_state(agent_id) + end_state = get_use_end_state(use_id) run: dict[str, Any] = { - "id": agent_id, + "id": use_id, "name": request_event.get("name", "unified"), "start": start_ts, "status": "completed", @@ -484,7 +480,7 @@ def api_agent_run(agent_id: str) -> Any: "output_file": output_file, "events": event_data.get("events", []), } - run["day"] = request_event.get("day") or _agent_id_to_day(agent_id) + run["day"] = request_event.get("day") or _use_id_to_day(use_id) return jsonify(run) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -498,7 +494,7 @@ def api_output_file(day: str, filename: str) -> Any: Path is validated to stay within the journal directory. Supports two path styles: - - Day-relative: ``agents/flow.md`` → resolved under ``{day}/`` + - Day-relative: ``talents/flow.md`` → resolved under ``{day}/`` - Journal-relative: ``facets/work/activities/...`` → resolved under journal root """ if not DATE_RE.fullmatch(day): @@ -550,9 +546,9 @@ def api_preview_prompt(name: str) -> Any: } """ try: - from think.talent import get_agent + from think.talent import get_talent - config = get_agent(name) + config = get_talent(name) system_instruction = config.get("system_instruction", "") extra_context = config.get("extra_context", "") @@ -576,14 +572,14 @@ def api_preview_prompt(name: str) -> Any: } ) except FileNotFoundError: - return jsonify({"error": f"Agent '{name}' not found"}), 404 + return jsonify({"error": f"Talent '{name}' not found"}), 404 except Exception as e: return jsonify({"error": str(e)}), 500 @sol_bp.route("/api/stats/") def api_stats(month: str) -> Any: - """Return agent run counts per day per facet for a month. + """Return talent-use counts per day per facet for a month. Args: month: YYYYMM format month string @@ -595,14 +591,14 @@ def api_stats(month: str) -> Any: if not re.fullmatch(r"\d{6}", month): return jsonify({"error": "Invalid month format, expected YYYYMM"}), 400 - agents_dir = Path(state.journal_root) / "agents" - if not agents_dir.exists(): + talents_dir = Path(state.journal_root) / "talents" + if not talents_dir.exists(): return jsonify({}) stats: dict[str, dict[str, int]] = {} # Read day index files for the month - for day_index_file in agents_dir.glob(f"{month}*.jsonl"): + for day_index_file in talents_dir.glob(f"{month}*.jsonl"): day = day_index_file.stem if not re.fullmatch(r"\d{8}", day): continue @@ -630,10 +626,10 @@ def api_stats(month: str) -> Any: @sol_bp.route("/api/badge-count") def api_badge_count() -> Any: - """Get count of failed agent runs for today (for app icon badge).""" + """Get count of failed talent runs for today (for app icon badge).""" today = date.today().strftime("%Y%m%d") - agents = _get_agents_for_day(today, facet_filter=None) - failed_count = sum(1 for a in agents if a.get("failed")) + uses = _get_uses_for_day(today, facet_filter=None) + failed_count = sum(1 for a in uses if a.get("failed")) return jsonify({"count": failed_count}) @@ -649,7 +645,7 @@ def api_updated_days() -> Any: @sol_bp.route("/api/identity") def api_identity() -> Any: - """Return agent identity and thickness signals.""" + """Return talent identity and thickness signals.""" try: from think.awareness import compute_thickness from think.utils import get_config diff --git a/apps/sol/workspace.html b/apps/sol/workspace.html index 0dc2d39d8..862e56a86 100644 --- a/apps/sol/workspace.html +++ b/apps/sol/workspace.html @@ -766,7 +766,7 @@ padding: 0; } -/* Flow events (agent_updated, continue) */ +/* Flow events (talent_updated, continue) */ .event-flow { border-left-color: #f9a825; background: #fffde7; @@ -1387,7 +1387,7 @@ .catch(() => { banner.style.display = 'none'; }); } - async function loadAgents() { + async function loadTalents() { currentDay = getDayFromUrl(); if (!currentDay) return; @@ -1396,29 +1396,29 @@ document.getElementById('agents-status').textContent = ''; try { - const response = await fetch(`api/agents/${currentDay}`); + const response = await fetch(`api/talents/${currentDay}`); const data = await response.json(); - allRuns = data.runs || []; - agentsMeta = data.agents || {}; + allRuns = data.uses || []; + agentsMeta = data.talents || {}; facetsMeta = data.facets || {}; renderGridView(); document.getElementById('agents-status').textContent = - allRuns.length + ' agent run' + (allRuns.length !== 1 ? 's' : '') + ' loaded'; + allRuns.length + ' talent run' + (allRuns.length !== 1 ? 's' : '') + ' loaded'; // Restore hash-based view after data load if (location.hash) { handleHashChange(); } } catch (error) { - console.error('Error loading agents:', error); + console.error('Error loading talents:', error); document.getElementById('loading-view').innerHTML = '
' + '
⚠️
' + - '
Unable to load agents
' + + '
Unable to load talents
' + '
The server may be temporarily unavailable. Check your connection and try again.
' + - '' + + '' + '
'; } } @@ -1533,7 +1533,7 @@ const badges = document.createElement('div'); badges.className = 'agent-card-badges'; - const successCount = agent.run_count - agent.failed_count; + const successCount = agent.run_count - talent.failed_count; if (successCount > 0) { const successBadge = document.createElement('span'); successBadge.className = 'badge badge-success'; @@ -1541,10 +1541,10 @@ badges.appendChild(successBadge); } - if (agent.failed_count > 0) { + if (talent.failed_count > 0) { const failBadge = document.createElement('span'); failBadge.className = 'badge badge-failed'; - failBadge.textContent = '✗ ' + agent.failed_count; + failBadge.textContent = '✗ ' + talent.failed_count; badges.appendChild(failBadge); } @@ -2204,10 +2204,10 @@ timeline.appendChild(renderInfoEvent(event)); } else if (type === 'finish') { timeline.appendChild(renderFinishEvent(event)); - } else if (type === 'agent_updated') { - timeline.appendChild(renderFlowEvent('Switched to agent: ' + (event.agent || 'unknown'), event.ts)); + } else if (type === 'talent_updated') { + timeline.appendChild(renderFlowEvent('Switched to talent: ' + (event.talent || 'unknown'), event.ts)); } else if (type === 'continue') { - timeline.appendChild(renderFlowEvent('Continued in agent: ' + (event.to || 'unknown'), event.ts)); + timeline.appendChild(renderFlowEvent('Continued in talent: ' + (event.to || 'unknown'), event.ts)); } } @@ -2527,12 +2527,12 @@ // Listen for facet changes window.addEventListener('facet.switch', () => { loadUpdatedBanner(); - loadAgents(); + loadTalents(); }); // Initial load loadIdentity(); loadUpdatedBanner(); - loadAgents(); + loadTalents(); })(); diff --git a/apps/speakers/attribution.py b/apps/speakers/attribution.py index c538c2ca8..446da130e 100644 --- a/apps/speakers/attribution.py +++ b/apps/speakers/attribution.py @@ -124,7 +124,7 @@ def _extract_screen_participants(seg_dir: Path) -> list[str]: screen.md captures video-call participant panels. The content is free-form markdown so extraction is best-effort. """ - screen_path = seg_dir / "agents" / "screen.md" + screen_path = seg_dir / "talents" / "screen.md" if not screen_path.exists(): return [] try: @@ -159,7 +159,7 @@ def _extract_screen_participants(seg_dir: Path) -> list[str]: def _extract_meeting_participants(day: str, segment_key: str) -> list[str]: """Extract participant names from daily meetings.md.""" - meetings_path = day_path(day) / "agents" / "meetings.md" + meetings_path = day_path(day) / "talents" / "meetings.md" if not meetings_path.exists(): return [] try: @@ -468,7 +468,7 @@ def save_speaker_labels( sentence that was corrected by the user keeps the corrected attribution rather than being overwritten by a fresh pipeline run. """ - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) # Load existing corrections to preserve user overrides @@ -653,7 +653,7 @@ def _has_audio_embeddings(seg_dir: Path) -> bool: def _has_speaker_labels(seg_dir: Path) -> bool: """Check if the segment already has speaker_labels.json.""" - return (seg_dir / "agents" / "speaker_labels.json").exists() + return (seg_dir / "talents" / "speaker_labels.json").exists() def backfill_segments( diff --git a/apps/speakers/bootstrap.py b/apps/speakers/bootstrap.py index a9743ddb7..ed94c8c03 100644 --- a/apps/speakers/bootstrap.py +++ b/apps/speakers/bootstrap.py @@ -555,7 +555,7 @@ def merge_names(alias_name: str, canonical_name: str) -> dict[str, Any]: for day in sorted(day_dirs().keys()): for _stream, _seg_key, seg_path in iter_segments(day): segments_scanned += 1 - agents_dir = seg_path / "agents" + agents_dir = seg_path / "talents" # Rewrite speaker_labels.json labels_path = agents_dir / "speaker_labels.json" diff --git a/apps/speakers/routes.py b/apps/speakers/routes.py index c6d0221bd..91d3cbae6 100644 --- a/apps/speakers/routes.py +++ b/apps/speakers/routes.py @@ -118,7 +118,7 @@ def _load_segment_speakers(segment_dir: Path) -> list[str]: Returns: List of speaker name strings, or empty list if not found/invalid. """ - speakers_path = segment_dir / "agents" / "speakers.json" + speakers_path = segment_dir / "talents" / "speakers.json" if not speakers_path.exists(): return [] @@ -303,11 +303,11 @@ def _remove_voiceprint( def _load_speaker_labels(segment_dir: Path) -> dict | None: - """Load speaker_labels.json from a segment's agents/ directory. + """Load speaker_labels.json from a segment's talents/ directory. Returns the parsed JSON dict, or None if not found/invalid. """ - labels_path = segment_dir / "agents" / "speaker_labels.json" + labels_path = segment_dir / "talents" / "speaker_labels.json" if not labels_path.is_file(): return None try: @@ -318,10 +318,10 @@ def _load_speaker_labels(segment_dir: Path) -> dict | None: def _save_speaker_labels(segment_dir: Path, labels_data: dict) -> None: - """Atomically write speaker_labels.json to a segment's agents/ directory.""" - agents_dir = segment_dir / "agents" - agents_dir.mkdir(parents=True, exist_ok=True) - out_path = agents_dir / "speaker_labels.json" + """Atomically write speaker_labels.json to a segment's talents/ directory.""" + talents_dir = segment_dir / "talents" + talents_dir.mkdir(parents=True, exist_ok=True) + out_path = talents_dir / "speaker_labels.json" tmp_path = out_path.with_suffix(".tmp") with open(tmp_path, "w", encoding="utf-8") as f: json.dump(labels_data, f, indent=2) @@ -329,11 +329,11 @@ def _save_speaker_labels(segment_dir: Path, labels_data: dict) -> None: def _load_speaker_corrections(segment_dir: Path) -> list[dict]: - """Load speaker_corrections.json from a segment's agents/ directory. + """Load speaker_corrections.json from a segment's talents/ directory. Returns list of correction entries, or empty list if not found. """ - corr_path = segment_dir / "agents" / "speaker_corrections.json" + corr_path = segment_dir / "talents" / "speaker_corrections.json" if not corr_path.is_file(): return [] try: @@ -348,9 +348,9 @@ def _append_speaker_correction(segment_dir: Path, correction: dict) -> None: """Append a correction entry to speaker_corrections.json (atomic write).""" corrections = _load_speaker_corrections(segment_dir) corrections.append(correction) - agents_dir = segment_dir / "agents" - agents_dir.mkdir(parents=True, exist_ok=True) - out_path = agents_dir / "speaker_corrections.json" + talents_dir = segment_dir / "talents" + talents_dir.mkdir(parents=True, exist_ok=True) + out_path = talents_dir / "speaker_corrections.json" tmp_path = out_path.with_suffix(".tmp") with open(tmp_path, "w", encoding="utf-8") as f: json.dump({"corrections": corrections}, f, indent=2) diff --git a/apps/speakers/status.py b/apps/speakers/status.py index 4d02c8901..95047ad46 100644 --- a/apps/speakers/status.py +++ b/apps/speakers/status.py @@ -179,7 +179,7 @@ def _attribution_section() -> dict[str, Any]: for seg_dir in sorted(stream_dir.iterdir()): if not seg_dir.is_dir(): continue - labels_file = seg_dir / "agents" / "speaker_labels.json" + labels_file = seg_dir / "talents" / "speaker_labels.json" if not labels_file.exists(): continue try: diff --git a/apps/speakers/suggest.py b/apps/speakers/suggest.py index 3a519019a..2ccd552c7 100644 --- a/apps/speakers/suggest.py +++ b/apps/speakers/suggest.py @@ -54,7 +54,7 @@ def _name_matches_entity(participant: str, names: set[str]) -> bool: def _parse_meetings(day_path: str) -> list[dict[str, Any]]: - meetings_path = Path(day_path) / "agents" / "meetings.md" + meetings_path = Path(day_path) / "talents" / "meetings.md" if not meetings_path.exists(): return [] @@ -296,7 +296,7 @@ def _low_confidence_review() -> list[dict[str, Any]]: for day in sorted(day_dirs().keys()): for stream, segment_key, seg_path in iter_segments(day): - labels_path = seg_path / "agents" / "speaker_labels.json" + labels_path = seg_path / "talents" / "speaker_labels.json" if not labels_path.exists(): continue try: @@ -323,7 +323,7 @@ def _low_confidence_review() -> list[dict[str, Any]]: if medium_or_null <= 10: continue - speakers_path = seg_path / "agents" / "speakers.json" + speakers_path = seg_path / "talents" / "speakers.json" has_speakers = speakers_path.is_file() null_proportion = null_count / total if total else 0.0 results.append( diff --git a/apps/speakers/tests/conftest.py b/apps/speakers/tests/conftest.py index 1689ebba3..56514e9ce 100644 --- a/apps/speakers/tests/conftest.py +++ b/apps/speakers/tests/conftest.py @@ -188,7 +188,7 @@ def speakers_env(tmp_path, monkeypatch): segment_key: Segment key (HHMMSS_LEN) speakers: List of speaker names """ - agents_dir = self.journal / day / STREAM / segment_key / "agents" + agents_dir = self.journal / day / STREAM / segment_key / "talents" agents_dir.mkdir(parents=True, exist_ok=True) speakers_path = agents_dir / "speakers.json" @@ -214,7 +214,7 @@ def speakers_env(tmp_path, monkeypatch): metadata: Optional extra metadata (owner_centroid_version, voiceprint_versions) """ - agents_dir = self.journal / day / STREAM / segment_key / "agents" + agents_dir = self.journal / day / STREAM / segment_key / "talents" agents_dir.mkdir(parents=True, exist_ok=True) data = {"labels": labels} @@ -248,7 +248,7 @@ def speakers_env(tmp_path, monkeypatch): stream: Optional stream name (defaults to STREAM) """ agents_dir = ( - self.journal / day / (stream or STREAM) / segment_key / "agents" + self.journal / day / (stream or STREAM) / segment_key / "talents" ) agents_dir.mkdir(parents=True, exist_ok=True) diff --git a/apps/speakers/tests/test_attribution.py b/apps/speakers/tests/test_attribution.py index 3b053139c..6b23ca510 100644 --- a/apps/speakers/tests/test_attribution.py +++ b/apps/speakers/tests/test_attribution.py @@ -171,7 +171,7 @@ def test_layer2_single_speaker(speakers_env): seg_dir = _write_controlled_segment(env, "20240101", "090000_300", embeddings) # speakers.json with exactly 1 speaker - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) (agents_dir / "speakers.json").write_text(json.dumps(["Ryan Bennett"])) @@ -535,7 +535,7 @@ def test_backfill_skips_already_labeled(speakers_env): env, "20260201", "090000_300", np.vstack([_normalized([1.0, 0.0])]) ) # Pre-create speaker_labels.json - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) (agents_dir / "speaker_labels.json").write_text('{"labels": []}') @@ -584,7 +584,7 @@ def test_backfill_processes_chronologically(speakers_env): # Labels written for day, seg_key in [("20260201", "080000_300"), ("20260210", "090000_300")]: labels_path = ( - env.journal / day / STREAM / seg_key / "agents" / "speaker_labels.json" + env.journal / day / STREAM / seg_key / "talents" / "speaker_labels.json" ) assert labels_path.exists() diff --git a/apps/speakers/tests/test_discovery.py b/apps/speakers/tests/test_discovery.py index 2bae74467..d3cd8e2dd 100644 --- a/apps/speakers/tests/test_discovery.py +++ b/apps/speakers/tests/test_discovery.py @@ -110,7 +110,7 @@ def _load_voiceprint_count(journal: Path, entity_id: str) -> int: def _load_corrections_count(journal: Path, day: str, segment_key: str) -> int: """Return number of correction entries for a segment.""" - path = journal / day / "test" / segment_key / "agents" / "speaker_corrections.json" + path = journal / day / "test" / segment_key / "talents" / "speaker_corrections.json" if not path.exists(): return 0 return len(json.loads(path.read_text(encoding="utf-8")).get("corrections", [])) @@ -199,14 +199,14 @@ def test_identify_creates_entity(speakers_env): for day, segment_key, _sentence_count in segments: labels_path = ( - env.journal / day / "test" / segment_key / "agents" / "speaker_labels.json" + env.journal / day / "test" / segment_key / "talents" / "speaker_labels.json" ) corrections_path = ( env.journal / day / "test" / segment_key - / "agents" + / "talents" / "speaker_corrections.json" ) labels_data = json.loads(labels_path.read_text(encoding="utf-8")) diff --git a/apps/speakers/tests/test_merge_names.py b/apps/speakers/tests/test_merge_names.py index 369f1c3c8..733343d04 100644 --- a/apps/speakers/tests/test_merge_names.py +++ b/apps/speakers/tests/test_merge_names.py @@ -127,7 +127,7 @@ def test_deep_merge_full(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: @@ -142,7 +142,7 @@ def test_deep_merge_full(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_corrections.json" ) with open(corr_path) as f: @@ -418,7 +418,7 @@ def test_speaker_labels_rewritten(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: @@ -459,7 +459,7 @@ def test_speaker_corrections_rewritten(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_corrections.json" ) with open(corr_path) as f: @@ -493,7 +493,7 @@ def test_fast_path_skips_unrelated_files(speakers_env): / "20240101" / STREAM / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) mtime_before = labels_path.stat().st_mtime_ns @@ -511,7 +511,7 @@ def test_corrupted_labels_logged_not_aborted(speakers_env): env.create_entity("Corrupt Canon") # Write corrupted file containing the alias_id string - agents_dir = env.journal / "20240101" / STREAM / "143022_300" / "agents" + agents_dir = env.journal / "20240101" / STREAM / "143022_300" / "talents" agents_dir.mkdir(parents=True, exist_ok=True) (agents_dir / "speaker_labels.json").write_text("corrupt_alias {not valid json") diff --git a/apps/speakers/tests/test_routes.py b/apps/speakers/tests/test_routes.py index 379be590c..f32b1c0ee 100644 --- a/apps/speakers/tests/test_routes.py +++ b/apps/speakers/tests/test_routes.py @@ -327,7 +327,7 @@ def test_load_segment_speakers_invalid_json(speakers_env): env = speakers_env() segment_dir = env.journal / "20240101" / "test" / "143022_300" - agents_dir = segment_dir / "agents" + agents_dir = segment_dir / "talents" agents_dir.mkdir(parents=True) # Write invalid JSON @@ -346,7 +346,7 @@ def test_load_segment_speakers_not_list(speakers_env): env = speakers_env() segment_dir = env.journal / "20240101" / "test" / "143022_300" - agents_dir = segment_dir / "agents" + agents_dir = segment_dir / "talents" agents_dir.mkdir(parents=True) # Write object instead of list @@ -583,7 +583,7 @@ def test_api_review_corrections_excludes_confirmed(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_corrections.json" ) corr_path.write_text( @@ -668,7 +668,7 @@ def test_api_confirm_attribution(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: @@ -682,7 +682,7 @@ def test_api_confirm_attribution(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_corrections.json" ) assert corr_path.exists() @@ -826,7 +826,7 @@ def test_api_correct_attribution(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: @@ -969,7 +969,7 @@ def test_api_assign_attribution(speakers_env): / "20240101" / "test" / "143022_300" - / "agents" + / "talents" / "speaker_labels.json" ) with open(labels_path) as f: diff --git a/apps/speakers/tests/test_suggest.py b/apps/speakers/tests/test_suggest.py index 2ecfd067c..d50019fa6 100644 --- a/apps/speakers/tests/test_suggest.py +++ b/apps/speakers/tests/test_suggest.py @@ -18,7 +18,7 @@ from apps.speakers.suggest import ( def create_meetings_md(env, day: str, content: str) -> Path: - meetings_path = env.journal / day / "agents" / "meetings.md" + meetings_path = env.journal / day / "talents" / "meetings.md" meetings_path.parent.mkdir(parents=True, exist_ok=True) meetings_path.write_text(content, encoding="utf-8") return meetings_path diff --git a/apps/stats/static/dashboard.js b/apps/stats/static/dashboard.js index 72f6645b9..ceb2b10e7 100644 --- a/apps/stats/static/dashboard.js +++ b/apps/stats/static/dashboard.js @@ -5,7 +5,7 @@ const Dashboard = (function() { 'use strict'; - const EXPECTED_SCHEMA_VERSION = 2; + const EXPECTED_SCHEMA_VERSION = 3; const DISPLAY_LABELS = { transcript: 'Audio', percept: 'Screen' }; // DOM element factory @@ -576,7 +576,7 @@ const Dashboard = (function() { } // Required-field validation (blocking — stops rendering if fields missing) - const requiredFields = ['days', 'totals', 'heatmap', 'tokens', 'agents', 'facets']; + const requiredFields = ['days', 'totals', 'heatmap', 'tokens', 'talents', 'facets']; const missingFields = requiredFields.filter(f => !(f in stats)); if (missingFields.length > 0) { document.getElementById('notice').appendChild( @@ -725,7 +725,7 @@ const Dashboard = (function() { // Render Events stacked bar chart buildStackedCategoryChart( document.getElementById('eventsChart'), - stats.agents.counts_by_day || {}, + stats.talents.counts_by_day || {}, Object.assign({}, data.generators || {}, { emptyIcon: '⚡', emptyText: 'No event data recorded', diff --git a/apps/todos/routes.py b/apps/todos/routes.py index 10aa64a2f..2d990f5fa 100644 --- a/apps/todos/routes.py +++ b/apps/todos/routes.py @@ -625,7 +625,7 @@ Write the generated checklist to facets/{facet}/todos/{day}.jsonl""" try: from convey.utils import spawn_agent - agent_id = spawn_agent( + use_id = spawn_agent( prompt=prompt, name="todos:todo", provider="openai", @@ -634,14 +634,14 @@ Write the generated checklist to facets/{facet}/todos/{day}.jsonl""" except Exception as exc: # pragma: no cover - network/agent failure return jsonify({"error": f"Failed to spawn agent: {exc}"}), 500 - if agent_id is None: + if use_id is None: return jsonify({"error": "Failed to connect to agent service"}), 503 if not hasattr(state, "todo_generation_agents"): state.todo_generation_agents = {} - state.todo_generation_agents[day] = agent_id + state.todo_generation_agents[day] = use_id - return jsonify({"agent_id": agent_id, "status": "started"}) + return jsonify({"use_id": use_id, "status": "started"}) @todos_bp.route("//generation-status") @@ -650,21 +650,21 @@ def todo_generation_status(day: str): # type: ignore[override] return "", 404 facet = request.args.get("facet", "personal") - agent_id = request.args.get("agent_id") - if not agent_id and hasattr(state, "todo_generation_agents"): - agent_id = state.todo_generation_agents.get(day) + use_id = request.args.get("use_id") + if not use_id and hasattr(state, "todo_generation_agents"): + use_id = state.todo_generation_agents.get(day) - if not agent_id: - return jsonify({"status": "none", "agent_id": None}) + if not use_id: + return jsonify({"status": "none", "use_id": None}) - from think.cortex_client import cortex_agents + from think.cortex_client import cortex_uses todo_path = _todo_path(day, facet) - agents_dir = Path(state.journal_root) / "agents" - agent_file = next(agents_dir.glob(f"*/{agent_id}.jsonl"), None) + talents_dir = Path(state.journal_root) / "talents" + use_file = next(talents_dir.glob(f"*/{use_id}.jsonl"), None) - if agent_file and agent_file.exists(): + if use_file and use_file.exists(): if todo_path.exists(): if ( hasattr(state, "todo_generation_agents") @@ -672,24 +672,22 @@ def todo_generation_status(day: str): # type: ignore[override] ): del state.todo_generation_agents[day] return jsonify( - {"status": "finished", "agent_id": agent_id, "todo_created": True} + {"status": "finished", "use_id": use_id, "todo_created": True} ) - return jsonify( - {"status": "finished", "agent_id": agent_id, "todo_created": False} - ) + return jsonify({"status": "finished", "use_id": use_id, "todo_created": False}) try: - response = cortex_agents(limit=100, offset=0) + response = cortex_uses(limit=100, offset=0) if response: - agents = response.get("agents", []) - for agent in agents: - if agent.get("id") == agent_id: - return jsonify({"status": "running", "agent_id": agent_id}) - return jsonify({"status": "unknown", "agent_id": agent_id}) + uses = response.get("uses", []) + for use in uses: + if use.get("id") == use_id: + return jsonify({"status": "running", "use_id": use_id}) + return jsonify({"status": "unknown", "use_id": use_id}) except Exception: # pragma: no cover - external call failure pass - return jsonify({"status": "unknown", "agent_id": agent_id}) + return jsonify({"status": "unknown", "use_id": use_id}) @todos_bp.route("//generate-weekly/", methods=["POST"]) @@ -712,7 +710,7 @@ Focus on surfacing the most important unfinished work from the past 7 days.""" try: from convey.utils import spawn_agent - agent_id = spawn_agent( + use_id = spawn_agent( prompt=prompt, name="todos:weekly", provider="openai", @@ -721,7 +719,7 @@ Focus on surfacing the most important unfinished work from the past 7 days.""" except Exception as exc: # pragma: no cover - network/agent failure return jsonify({"error": f"Failed to spawn agent: {exc}"}), 500 - if agent_id is None: + if use_id is None: return jsonify({"error": "Failed to connect to agent service"}), 503 - return jsonify({"agent_id": agent_id, "status": "started"}) + return jsonify({"use_id": use_id, "status": "started"}) diff --git a/apps/todos/talent/daily.md b/apps/todos/talent/daily.md index 6411462c2..9a11fc3c9 100644 --- a/apps/todos/talent/daily.md +++ b/apps/todos/talent/daily.md @@ -10,7 +10,7 @@ "multi_facet": true, "group": "Todos", "load": { - "agents": True, + "talents": True, "journal": True } } diff --git a/apps/todos/workspace.html b/apps/todos/workspace.html index 21980c060..ba81f5528 100644 --- a/apps/todos/workspace.html +++ b/apps/todos/workspace.html @@ -1285,8 +1285,8 @@ document.addEventListener('DOMContentLoaded', function() { // Listen for cortex events (agent completion) if (window.appEvents) { window.appEvents.listen('cortex', msg => { - // Find button by agent_id - const btn = document.querySelector(`.facet-generate-btn[data-agent-id="${msg.agent_id}"]`); + // Find button by use_id + const btn = document.querySelector(`.facet-generate-btn[data-agent-id="${msg.use_id}"]`); if (!btn) return; if (msg.event === 'finish') { @@ -1321,7 +1321,7 @@ document.addEventListener('DOMContentLoaded', function() { const day = btn.dataset.day; const facet = btn.dataset.facet; - // If already has agent_id, navigate to agents page + // If already has use_id, navigate to agents page if (btn.dataset.agentId) { window.location.href = btn.dataset.agentUrl; return; @@ -1342,9 +1342,9 @@ document.addEventListener('DOMContentLoaded', function() { const data = await response.json(); - // Store agent_id for event matching and navigation - btn.dataset.agentId = data.agent_id; - btn.dataset.agentUrl = `/agents#${data.agent_id}`; + // Store use_id for event matching and navigation + btn.dataset.agentId = data.use_id; + btn.dataset.agentUrl = `/agents#${data.use_id}`; } catch (error) { showMessage('Failed to start todo generation', 'error'); diff --git a/apps/transcripts/routes.py b/apps/transcripts/routes.py index a46027ba6..c0650a5bf 100644 --- a/apps/transcripts/routes.py +++ b/apps/transcripts/routes.py @@ -224,7 +224,7 @@ def segment_content(day: str, stream: str, segment_key: str) -> Any: warnings = 0 # Load speaker labels if available. - speaker_labels_path = Path(segment_dir) / "agents" / "speaker_labels.json" + speaker_labels_path = Path(segment_dir) / "talents" / "speaker_labels.json" speaker_map: dict[int, dict] = {} if speaker_labels_path.is_file(): try: @@ -414,13 +414,13 @@ def segment_content(day: str, stream: str, segment_key: str) -> Any: # Get cost data for this segment cost_data = get_usage_cost(day, segment=segment_key) - # Collect agent .md files + # Collect talent .md files md_files = {} - agents_dir = Path(segment_dir) / "agents" - if agents_dir.is_dir(): - for md_path in sorted(agents_dir.rglob("*.md")): + talents_dir = Path(segment_dir) / "talents" + if talents_dir.is_dir(): + for md_path in sorted(talents_dir.rglob("*.md")): try: - key = md_path.relative_to(agents_dir).with_suffix("").as_posix() + key = md_path.relative_to(talents_dir).with_suffix("").as_posix() md_files[key] = md_path.read_text() except Exception: continue diff --git a/convey/apps.py b/convey/apps.py index 73ef95d48..fbeaa909a 100644 --- a/convey/apps.py +++ b/convey/apps.py @@ -95,7 +95,7 @@ def _resolve_attention(awareness_current: dict) -> AttentionItem | None: journal = Path(get_journal()) today = datetime.now().strftime("%Y%m%d") - day_index = journal / "agents" / f"{today}.jsonl" + day_index = journal / "talents" / f"{today}.jsonl" if day_index.exists(): errors: dict[str, float] = {} successes: dict[str, float] = {} @@ -174,7 +174,7 @@ def _resolve_attention(awareness_current: dict) -> AttentionItem | None: journal = Path(get_journal()) today = datetime.now().strftime("%Y%m%d") - agents_dir = journal / today / "agents" + agents_dir = journal / today / "talents" if agents_dir.is_dir(): outputs = sorted(p.stem for p in agents_dir.glob("*.md")) if outputs: diff --git a/convey/templates/app.html b/convey/templates/app.html index d22304e7c..994ca9bfc 100644 --- a/convey/templates/app.html +++ b/convey/templates/app.html @@ -452,16 +452,16 @@ // Subscribe to WS first, then check GET if (window.appEvents) { recoveryCleanup = window.appEvents.listen('cortex', function(msg) { - if (msg.agent_id === agentId) { + if (msg.use_id === agentId) { if (recoveryWatchdog) { clearTimeout(recoveryWatchdog); recoveryWatchdog = null; } recoveryWatchdog = setTimeout(function() { recoverDeliver('', 'panel', 'request timed out. the server took too long to respond. try a shorter question, or check if solstone services are running.'); }, 180000); } - if (msg.agent_id === agentId && msg.event === 'finish') { + if (msg.use_id === agentId && msg.event === 'finish') { var resp = msg.result || ''; recoverDeliver(resp, msg.display || 'panel', null); - } else if (msg.agent_id === agentId && msg.event === 'error') { + } else if (msg.use_id === agentId && msg.event === 'error') { recoverDeliver('', 'panel', 'something went wrong. the server returned an unexpected response. try sending your message again, or check the health page if it keeps happening.'); } }); @@ -710,7 +710,7 @@ } var data = await r.json(); - var agentId = data.agent_id; + var agentId = data.use_id; if (!agentId) { deliverResult('', 'panel', 'something went wrong. the server returned an unexpected response. try sending your message again, or check the health page if it keeps happening.'); return; @@ -751,17 +751,17 @@ if (window.appEvents) { cleanupCortex = window.appEvents.listen('cortex', function(msg) { // Reset inactivity watchdog and update thinking label for our agent - if (msg.agent_id === agentId) { + if (msg.use_id === agentId) { var label = getProgressLabel(msg); if (label) updateThinkingLabel(label); startWatchdog(agentId); } // Handle finish/error for our agent - if (msg.agent_id === agentId && msg.event === 'finish') { + if (msg.use_id === agentId && msg.event === 'finish') { var resp = msg.result || ''; deliverResult(resp, msg.display || 'panel', null); - } else if (msg.agent_id === agentId && msg.event === 'error') { + } else if (msg.use_id === agentId && msg.event === 'error') { deliverResult('', 'panel', 'something went wrong. the server returned an unexpected response. try sending your message again, or check the health page if it keeps happening.'); } }); diff --git a/convey/triage.py b/convey/triage.py index b30c15e25..03b987a76 100644 --- a/convey/triage.py +++ b/convey/triage.py @@ -37,10 +37,10 @@ def triage() -> Any: """Accept a message from the conversation panel and spawn a triage agent. Expects JSON: {message, app, path, facet} - Returns JSON: {agent_id} + Returns JSON: {use_id} The agent runs asynchronously. The browser receives the result via - WebSocket (cortex/finish event). For reload recovery, use GET /result/. + WebSocket (cortex/finish event). For reload recovery, use GET /result/. All journals route to the unified talent. """ @@ -96,33 +96,33 @@ def triage() -> Any: config["path"] = path config["user_message"] = message - agent_id = spawn_agent( + use_id = spawn_agent( prompt=full_prompt, name=agent_name, provider=None, config=config, ) - if agent_id is None: + if use_id is None: return error_response("Failed to connect to agent service", 503) - return jsonify(agent_id=agent_id) + return jsonify(use_id=use_id) except Exception: logger.exception("Triage request failed") return error_response("Failed to process triage request", 500) -@bp.route("/result/", methods=["GET"]) -def triage_result(agent_id: str) -> Any: +@bp.route("/result/", methods=["GET"]) +def triage_result(use_id: str) -> Any: """Return the result of a completed triage agent. Returns {response, display} if the agent has finished, 404 otherwise. Used for page-reload recovery when the WebSocket may have missed the finish event. """ try: - from think.cortex_client import read_agent_events + from think.cortex_client import read_use_events - events = read_agent_events(agent_id) + events = read_use_events(use_id) for event in reversed(events): if event.get("event") == "finish": result = event.get("result", "") @@ -130,5 +130,5 @@ def triage_result(agent_id: str) -> Any: except FileNotFoundError: pass except Exception: - logger.debug("Failed to read triage result for %s", agent_id, exc_info=True) + logger.debug("Failed to read triage result for %s", use_id, exc_info=True) return jsonify(error="not found"), 404 diff --git a/convey/utils.py b/convey/utils.py index b41092d18..b3dd4d4ff 100644 --- a/convey/utils.py +++ b/convey/utils.py @@ -88,10 +88,10 @@ def spawn_agent( provider: Optional[str] = None, config: Optional[dict[str, Any]] = None, ) -> str | None: - """Spawn a Cortex agent and return the agent_id. + """Spawn a Cortex agent and return the use_id. Thin wrapper around cortex_request that ensures imports are handled - and returns the agent_id directly. + and returns the use_id directly. Args: prompt: The task or question for the agent @@ -100,7 +100,7 @@ def spawn_agent( config: Additional configuration (max_tokens, facet, session_id, etc.) Returns: - agent_id string (timestamp-based), or None if the request could not be sent. + use_id string (timestamp-based), or None if the request could not be sent. Raises: ValueError: If config is invalid @@ -245,7 +245,7 @@ def success_response( Example: return success_response() # Returns {"success": True} - return success_response({"agent_id": "123"}) # Returns {"success": True, "agent_id": "123"} + return success_response({"use_id": "123"}) # Returns {"success": True, "use_id": "123"} """ from flask import jsonify diff --git a/docs/APPS.md b/docs/APPS.md index 12d234bda..e3840cf50 100644 --- a/docs/APPS.md +++ b/docs/APPS.md @@ -281,7 +281,7 @@ Define custom generator prompts that integrate with solstone's output generation - Create `talent/` directory with `.md` files containing JSON frontmatter - App generators are automatically discovered alongside system generators - Keys are namespaced as `{app}:{agent}` (e.g., `my_app:weekly_summary`) -- Outputs go to `JOURNAL/YYYYMMDD/agents/__.md` (or `.json` if `output: "json"`) +- Outputs go to `JOURNAL/YYYYMMDD/talents/__.md` (or `.json` if `output: "json"`) **Metadata format:** Same schema as system generators in `talent/*.md` - JSON frontmatter includes `title`, `description`, `color`, `schedule` (required), `priority` (required for scheduled prompts), `hook`, `output`, `max_output_tokens`, and `thinking_budget` fields. The `schedule` field must be `"segment"` or `"daily"`. The `priority` field is required for all scheduled prompts - prompts without explicit priority will fail validation. Set `output: "json"` for structured JSON output instead of markdown. Optional `max_output_tokens` sets the maximum response length; `thinking_budget` sets the model's thinking token budget (provider-specific defaults apply if omitted). Generators reject a `cwd` field entirely; working-directory control is only available for `type: "cogitate"` prompts. @@ -307,7 +307,7 @@ The `occurrences` field (optional string) provides agent-specific extraction gui } ``` -**App-data outputs:** For outputs from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/agents/*.md` - these are automatically indexed. +**App-data outputs:** For outputs from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/talents/*.md` - these are automatically indexed. **Template variables:** Generator prompts can use template variables like `$name`, `$preferred`, `$daily_preamble`, and context variables like `$day` and `$day_YYYYMMDD`. See [PROMPT_TEMPLATES.md](PROMPT_TEMPLATES.md) for the complete template system documentation. @@ -321,13 +321,13 @@ The `occurrences` field (optional string) provides agent-specific extraction gui - Resolution: `"name"` → `talent/{name}.py`, `"app:name"` → `apps/{app}/talent/{name}.py`, or explicit path **Pre-hooks** (`pre_process`): Modify inputs before the LLM call -- `context` is the full config dict with: `name`, `agent_id`, `provider`, `model`, `prompt`, `system_instruction` (if set), `user_instruction`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` +- `context` is the full config dict with: `name`, `use_id`, `provider`, `model`, `prompt`, `system_instruction` (if set), `user_instruction`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` - Return a dict of modified fields to merge back (e.g., `{"prompt": "modified"}`) - Return `None` for no changes **Post-hooks** (`post_process`): Transform output after the LLM call - `result` is the LLM output (markdown or JSON string) -- `context` is the full config dict with: `name`, `agent_id`, `provider`, `model`, `prompt`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` +- `context` is the full config dict with: `name`, `use_id`, `provider`, `model`, `prompt`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` - Return modified string, or `None` to use original result **Flush hooks:** Segment agents can declare `"hook": {"flush": true}` to participate in segment flush. When no new segments arrive for an extended period, the supervisor triggers `sol dream --flush --segment `, which runs only flush-enabled agents with `context["flush"] = True` and `context["refresh"] = True`. This lets agents close out dangling state (e.g., end active activities that would otherwise wait indefinitely for the next segment). The timeout is managed by the supervisor — agents should trust the flush signal without their own timeout logic. @@ -371,7 +371,7 @@ Define custom agents and generator templates that integrate with solstone's Cort **Reference implementations:** - System agent examples: `talent/*.md` (files with `tools` field) -- Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=True)`, `get_agent()` +- Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=True)`, `get_talent()` #### Prompt Context Configuration @@ -379,7 +379,7 @@ Both generators and agents support an optional `load` key for configuring source ```json { - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } ``` @@ -394,7 +394,7 @@ Context is provided inline in the `.md` body via template variables: - `$facets` - focused facet context or all available facets - `$activity_context` - activity metadata, segment state, and analysis focus sections -**Authoritative source:** `think/talent.py` - `_DEFAULT_LOAD`, `source_is_enabled()`, `source_is_required()`, `get_agent_filter()` +**Authoritative source:** `think/talent.py` - `_DEFAULT_LOAD`, `source_is_enabled()`, `source_is_required()`, `get_talent_filter()` --- @@ -535,7 +535,7 @@ Available in `convey/utils.py`: - `format_date(date_str)` - Format YYYYMMDD as "Wednesday January 14th" ### Agent Spawning -- `spawn_agent(prompt, name, provider, config)` - Spawn Cortex agent, returns agent_id +- `spawn_agent(prompt, name, provider, config)` - Spawn Cortex agent, returns use_id ### JSON Utilities - `load_json(path)` - Load JSON file with error handling (returns None on error) diff --git a/docs/CALLOSUM.md b/docs/CALLOSUM.md index 688c9884d..5d358786c 100644 --- a/docs/CALLOSUM.md +++ b/docs/CALLOSUM.md @@ -36,7 +36,7 @@ Callosum is a JSON-per-line message bus for real-time event distribution across ### `cortex` - Agent execution events **Source:** `think/cortex.py` -**Events:** `request`, `start`, `thinking`, `tool_start`, `tool_end`, `finish`, `error`, `agent_updated`, `info`, `status` +**Events:** `request`, `start`, `thinking`, `tool_start`, `tool_end`, `finish`, `error`, `talent_updated`, `info`, `status` **Details:** See [CORTEX.md](CORTEX.md) for agent lifecycle, configuration, and event schemas ### `supervisor` - Process lifecycle management @@ -105,7 +105,7 @@ Callosum is a JSON-per-line message bus for real-time event distribution across ### `dream` - Generator and agent processing **Source:** `think/dream.py` -**Events:** `started`, `status`, `group_started`, `group_completed`, `agent_started`, `agent_completed`, `completed`, `segments_started`, `segments_completed` +**Events:** `started`, `status`, `group_started`, `group_completed`, `talent_started`, `talent_completed`, `completed`, `segments_started`, `segments_completed` **Key fields:** `mode` ("daily"/"segment"/"activity"/"flush"), `day`, `segment` (when mode="segment" or "flush"), `activity` and `facet` (when mode="activity") **Purpose:** Track dream processing from generators through scheduled agents **`status`** - Periodic progress (every ~5s). Fields: `mode`, `day`, `segment`, `stream`, `agents_completed`, `agents_total`, `current_group_priority`, `current_agents` (list of running agent names). In `--segments` batch mode, also includes `segments_completed`, `segments_total`. In activity mode, includes `activity`, `facet`. @@ -257,7 +257,7 @@ emit("supervisor", "request", ref=task_id, cmd=["sol", "import", path]) For agent requests, use the cortex client: ```python from think.cortex_client import cortex_request -agent_id = cortex_request(prompt="...", name="default") +use_id = cortex_request(prompt="...", name="default") ``` See `think/cortex_client.py` for the full API. diff --git a/docs/CORTEX.md b/docs/CORTEX.md index 3a042c6e3..0eecc59f3 100644 --- a/docs/CORTEX.md +++ b/docs/CORTEX.md @@ -1,6 +1,6 @@ # Cortex API and Eventing -The Cortex system manages AI agent execution through the Callosum message bus with file-based persistence. It acts as a process manager for agent instances, receiving requests via Callosum and writing execution events to both JSONL files (for persistence) and the message bus (for real-time distribution). +The Cortex system manages AI talent execution through the Callosum message bus with file-based persistence. It acts as a process manager for talent instances, receiving requests via Callosum and writing execution events to both JSONL files (for persistence) and the message bus (for real-time distribution). For details on the Callosum protocol and message format, see [CALLOSUM.md](CALLOSUM.md). @@ -9,22 +9,22 @@ For details on the Callosum protocol and message format, see [CALLOSUM.md](CALLO ### Event Flow 1. **Request Creation**: Client calls `cortex_request()` which broadcasts to Callosum (`tract="cortex"`, `event="request"`) 2. **Request Reception**: Cortex receives message via Callosum callback and creates `/_active.jsonl` -3. **Agent Spawning**: Cortex spawns agent process via `sol agents` with merged configuration -4. **Event Emission**: Agents write JSON events to stdout (captured by Cortex) +3. **Talent Spawning**: Cortex spawns a talent process via `python -m think.talents` with merged configuration +4. **Event Emission**: Talents write JSON events to stdout (captured by Cortex) 5. **Event Distribution**: Cortex appends events to JSONL file AND broadcasts to Callosum 6. **Agent Completion**: Cortex renames file to `/.jsonl` when agent finishes ### Key Components - **Message Bus Integration**: Cortex connects to Callosum to receive requests and broadcast events -- **Process Management**: Spawns agent subprocesses (both tool agents and generators) -- **Configuration Delegation**: Passes raw requests to `sol agents`, which handles all config loading, validation, and hydration +- **Process Management**: Spawns talent subprocesses (both tool talents and generators) +- **Configuration Delegation**: Passes raw requests to `python -m think.talents`, which handles all config loading, validation, and hydration - **Event Capture**: Monitors agent stdout/stderr and appends to JSONL files - **Dual Event Distribution**: Events go to both persistent files and real-time message bus - **NDJSON Input Mode**: Agent processes accept newline-delimited JSON via stdin containing the full merged configuration ### File States -- `/_active.jsonl`: Agent currently executing (Cortex is appending events) -- `/.jsonl`: Agent completed (contains full event history) +- `/_active.jsonl`: Talent currently executing (Cortex is appending events) +- `/.jsonl`: Talent completed (contains full event history) **Note**: Files provide persistence and historical record, while Callosum provides real-time event distribution to all interested services. @@ -35,16 +35,16 @@ Requests are created via `cortex_request()` from `think.cortex_client`, which br ```json { "event": "request", - "ts": 1234567890123, // Required: millisecond timestamp (must match agent_id in filename) - "prompt": "Analyze this code for security issues", // Required for agents (not generators) - "name": "default", // Optional: agent name from talent/*.md + "ts": 1234567890123, // Required: millisecond timestamp (must match use_id in filename) + "prompt": "Analyze this code for security issues", // Required for talents (not generators) + "name": "default", // Optional: talent name from talent/*.md "provider": "openai", // Optional: override provider (openai, google, anthropic) "max_output_tokens": 8192, // Optional: maximum response tokens "thinking_budget": 10000, // Optional: thinking token budget (ignored by OpenAI) "session_id": "sess-abc123", // Optional: CLI session ID for continuation "chat_id": "1234567890122", // Optional: chat ID for reverse lookup "facet": "my-project", // Optional: project context - "output": "md", // Optional: output format ("md" or "json"), writes to agents/ + "output": "md", // Optional: output format ("md" or "json"), writes to talents/ "day": "20250109", // Optional: YYYYMMDD format, defaults to current day "env": { // Optional: environment variables for subprocess "API_KEY": "secret", @@ -79,7 +79,7 @@ Generators are spawned via Cortex when a request has an `output` field but no `t ### Generator Events -Generators emit the same event types as agents: +Generators emit the same event types as talents: - `start` - When generation begins - `finish` - On completion, with `result` containing generated content - `error` - On failure @@ -92,16 +92,16 @@ The `finish` event may include a `skipped` field when generation is skipped: All providers (Anthropic, OpenAI, Google) support continuing conversations via CLI session resumption. Include a `session_id` field in the request with the CLI session -ID from a previous agent's finish event. The provider CLI tool resumes the conversation +ID from a previous talent's finish event. The provider CLI tool resumes the conversation internally using its native session management (e.g., `claude --resume`, `codex exec resume`). Chats are locked to their original provider — continuations must use the same provider that started the conversation. The `chat_id` field enables reverse lookup from an -agent back to its parent chat. +talent back to its parent chat. ## Agent Event Format -All subsequent lines are JSON objects with `event` and millisecond `ts` fields. The `ts` field is automatically added by Cortex if not provided by the provider. Additionally, Cortex automatically adds an `agent_id` field (matching the timestamp component in the filename) to all events for tracking purposes. +All subsequent lines are JSON objects with `event` and millisecond `ts` fields. The `ts` field is automatically added by Cortex if not provided by the provider. Additionally, Cortex automatically adds an `use_id` field (matching the timestamp component in the filename) to all events for tracking purposes. ### request The initial spawn request (first line of file, written by client). @@ -109,7 +109,7 @@ The initial spawn request (first line of file, written by client). { "event": "request", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "prompt": "User's task or question", "provider": "openai", "name": "default", @@ -119,12 +119,12 @@ The initial spawn request (first line of file, written by client). ``` ### start -Emitted when an agent run begins. +Emitted when a talent run begins. ```json { "event": "start", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "name": "default", "model": "gpt-4o", "session_id": "sess-abc", @@ -138,7 +138,7 @@ Emitted when a tool execution begins. { "event": "tool_start", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "tool": "search_journal", "args": {"query": "search terms", "limit": 10}, "call_id": "search_journal-1" @@ -151,7 +151,7 @@ Emitted when a tool execution completes. { "event": "tool_end", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "tool": "search_journal", "args": {"query": "search terms"}, "result": ["result", "array", "or", "object"], @@ -165,30 +165,30 @@ Emitted when the model produces reasoning/thinking content (model-dependent, pri { "event": "thinking", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "summary": "Model's internal reasoning about the task...", "model": "o1-mini" } ``` -### agent_updated +### talent_updated Emitted when control is handed off to a different agent (multi-agent scenarios). ```json { - "event": "agent_updated", + "event": "talent_updated", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "agent": "SpecializedAgent" } ``` ### finish -Emitted when the agent run completes successfully. +Emitted when the talent run completes successfully. ```json { "event": "finish", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "result": "Final response text to the owner" } ``` @@ -199,7 +199,7 @@ Emitted when an error occurs during execution. { "event": "error", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "error": "Error message", "trace": "Full stack trace..." } @@ -211,7 +211,7 @@ Emitted when non-JSON output is captured from agent stdout. { "event": "info", "ts": 1234567890123, - "agent_id": "1234567890123", + "use_id": "1234567890123", "message": "Non-JSON output line from agent" } ``` @@ -232,23 +232,23 @@ When an agent completes successfully, its result can be automatically written to - Include an `output` field in the agent's frontmatter with the format ("md" or "json") - Output path is derived from agent name + format + schedule: - - Daily agents: `YYYYMMDD/agents/{name}.{ext}` + - Daily agents: `YYYYMMDD/talents/{name}.{ext}` - Segment agents: `YYYYMMDD/{segment}/{name}.{ext}` - Writing occurs before completion - Write failures are logged but don't interrupt the agent flow - Commonly used for scheduled agents that generate daily reports -## Agent Configuration +## Talent Configuration -Agents use configurations stored in the `talent/` directory. Each agent is a `.md` file containing: +Talents use configurations stored in the `talent/` directory. Each talent is a `.md` file containing: - JSON frontmatter with metadata and configuration -- The agent-specific prompt and instructions in the content +- The talent-specific prompt and instructions in the content -When spawning an agent: -1. Cortex passes the raw request to `sol agents` via stdin (NDJSON format) -2. The agent process (`think/agents.py`) handles all config loading via `prepare_config()`: - - Loads agent configuration using `get_agent()` from `think/talent.py` - - Merges request parameters with agent defaults +When spawning a talent: +1. Cortex passes the raw request to `python -m think.talents` via stdin (NDJSON format) +2. The talent process (`think/talents.py`) handles all config loading via `prepare_config()`: + - Loads talent configuration using `get_talent()` from `think/talent.py` + - Merges request parameters with talent defaults - Resolves provider and model based on context 3. The agent validates the config via `validate_config()` before execution 4. Instructions are built with three components: diff --git a/docs/DOCTOR.md b/docs/DOCTOR.md index cc1fb68aa..2d174eac1 100644 --- a/docs/DOCTOR.md +++ b/docs/DOCTOR.md @@ -12,7 +12,7 @@ pgrep -af "sol:observer|sol:sense|sol:supervisor" ls -la journal/health/callosum.sock # Check for stuck agents (should be empty or short-lived) -ls journal/agents/*/*_active.jsonl 2>/dev/null +ls journal/talents/*/*_active.jsonl 2>/dev/null ``` **Healthy state:** @@ -45,7 +45,7 @@ See [CALLOSUM.md](CALLOSUM.md) for message protocol and [CORTEX.md](CORTEX.md) f |------|-------| | Current service logs | `journal/health/{service}.log` (symlinks) | | Day's process logs | `journal/{YYYYMMDD}/health/{ref}_{name}.log` | -| Agent execution | `journal/agents//*.jsonl` | +| Agent execution | `journal/talents//*.jsonl` | | Journal task log | `journal/task_log.txt` | **Symlink structure:** Journal-level symlinks point to current day's logs. Day-level symlinks point to current process instance (by ref). @@ -89,7 +89,7 @@ See [CALLOSUM.md](CALLOSUM.md) Tract Registry for event schemas. ## Reading Agent Files -**Location:** `journal/agents/` +**Location:** `journal/talents/` **File states:** - `{name}/{timestamp}_active.jsonl` - Agent currently running @@ -105,10 +105,10 @@ See [CALLOSUM.md](CALLOSUM.md) Tract Registry for event schemas. ```bash # View an agent's final result -jq -r 'select(.event=="finish") | .result' journal/agents/default/1234567890123.jsonl +jq -r 'select(.event=="finish") | .result' journal/talents/default/1234567890123.jsonl # List today's agents with their prompts -for id in $(jq -r '.agent_id' journal/agents/$(date +%Y%m%d).jsonl 2>/dev/null); do +for id in $(jq -r '.use_id' journal/talents/$(date +%Y%m%d).jsonl 2>/dev/null); do f=$(find journal/agents -maxdepth 2 -path "*/${id}.jsonl" -print -quit) [ -n "$f" ] || continue echo "=== $(basename "$f") ===" @@ -138,10 +138,10 @@ Causes: DBus issues, screencast permissions, audio device unavailable. ```bash # Find active agents -ls -la journal/agents/*/*_active.jsonl +ls -la journal/talents/*/*_active.jsonl # Check last event in active agent -tail -1 journal/agents/*/*_active.jsonl | jq . +tail -1 journal/talents/*/*_active.jsonl | jq . ``` Causes: Backend timeout, tool hanging, network issues. @@ -176,11 +176,11 @@ Causes: Slow transcription, describe API rate limits. tail -f journal/health/*.log # Count today's agents by status -echo "Completed: $([ -f journal/agents/$(date +%Y%m%d).jsonl ] && wc -l < journal/agents/$(date +%Y%m%d).jsonl || echo 0)" -echo "Running: $(ls journal/agents/*/*_active.jsonl 2>/dev/null | wc -l)" +echo "Completed: $([ -f journal/talents/$(date +%Y%m%d).jsonl ] && wc -l < journal/talents/$(date +%Y%m%d).jsonl || echo 0)" +echo "Running: $(ls journal/talents/*/*_active.jsonl 2>/dev/null | wc -l)" # Find agents that errored today -jq -r 'select(.status=="error") | .agent_id' journal/agents/$(date +%Y%m%d).jsonl 2>/dev/null +jq -r 'select(.status=="error") | .use_id' journal/talents/$(date +%Y%m%d).jsonl 2>/dev/null # Check token usage for today wc -l journal/tokens/$(date +%Y%m%d).jsonl diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index 4f866fe8e..2019cce80 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -14,7 +14,7 @@ solstone transforms raw recordings into actionable understanding through a three ┌─────────────────────────────────────┐ │ LAYER 3: AGENT OUTPUTS │ Narrative summaries │ (Markdown files) │ "What it means" -│ - agents/*.md (daily outputs) │ +│ - talents/*.md (daily outputs) │ │ - *.md (segment outputs) │ └─────────────────────────────────────┘ ↑ synthesized from @@ -42,7 +42,7 @@ solstone transforms raw recordings into actionable understanding through a three |------|------------|----------| | **Capture** | Raw audio/video recording | `*.flac`, `*.ogg`, `*.opus`, `*.wav`, `*.webm` | | **Extract** | Structured data from captures | `*.jsonl` | -| **Agent Output** | AI-generated narrative summary | `agents/*.md`, `HHMMSS_LEN/*.md` | +| **Agent Output** | AI-generated narrative summary | `talents/*.md`, `HHMMSS_LEN/*.md` | **Organization** @@ -67,7 +67,7 @@ solstone transforms raw recordings into actionable understanding through a three | `chronicle/` | Container for daily capture folders (`YYYYMMDD/`) containing segments, extracts, and agent outputs | | `entities/` | Journal-level entity identity records (`/entity.json`) | | `facets/` | Facet-specific data: entity relationships, todos, events, news, action logs | -| `agents/` | Agent run logs in per-agent subdirectories (`/.jsonl`), day indexes (`.jsonl`), and latest-run symlinks (`.log`) | +| `talents/` | Talent run logs in per-talent subdirectories (`/.jsonl`), day indexes (`.jsonl`), and latest-run symlinks (`.log`) | | `apps/` | App-specific storage (distinct from codebase `apps/`) | | `streams/` | Per-stream state files (`.json`) tracking segment chains and sequence numbers | | `imports/` | Imported audio files and processing artifacts | @@ -186,14 +186,14 @@ Fields: "Raw media" means layer 1 capture files only: audio files (`.flac`, `.opus`, `.ogg`, `.m4a`, `.wav`), video files (`.webm`, `.mov`, `.mp4`), and screen diffs (`monitor_*_diff.png`). -All layer 2 and layer 3 content is always preserved regardless of retention policy: transcripts (`audio.jsonl`, `screen.jsonl`), agent outputs (`agents/*.md`), speaker labels (`agents/speaker_labels.json`), facet events (`events/*.jsonl`), entity data, segment metadata (`stream.json`), and search index entries. +All layer 2 and layer 3 content is always preserved regardless of retention policy: transcripts (`audio.jsonl`, `screen.jsonl`), talent outputs (`talents/*.md`), speaker labels (`talents/speaker_labels.json`), facet events (`events/*.jsonl`), entity data, segment metadata (`stream.json`), and search index entries. Raw media is never deleted from segments that haven't finished processing. A segment is considered complete only when all four checks pass: -- No `_active.jsonl` files in `agents/` (no running agents) +- No `_active.jsonl` files in `talents/` (no running talents) - `audio.jsonl` (or `*_audio.jsonl`) exists if audio raw media was captured - `screen.jsonl` (or `*_screen.jsonl`) exists if video raw media was captured -- `agents/speaker_labels.json` exists if voice embeddings (`.npz`) are present +- `talents/speaker_labels.json` exists if voice embeddings (`.npz`) are present Purged segments remain fully navigable in convey. Transcripts, entities, speaker labels, and summaries are all intact. The only difference is that audio/video playback is unavailable. @@ -733,7 +733,7 @@ The `logs/` directory within each facet records facet-scoped actions. Logs are o "text": "Review project proposal" }, "facet": "work", - "agent_id": "1765870373972" + "use_id": "1765870373972" } ``` @@ -747,7 +747,7 @@ Both log types share the same structure: - `action` – Action name (e.g., "todo_add", "identity_update") - `params` – Action-specific parameters - `facet` – Facet name (only present in facet-scoped logs) -- `agent_id` – Agent ID (only present for agent tool actions) +- `use_id` – Agent ID (only present for agent tool actions) These logs enable auditing, debugging, and potential rollback of automated actions. @@ -778,7 +778,7 @@ Each line in a token log file is a JSON object with the following structure: Required fields: - `timestamp` – Unix timestamp in milliseconds (13 digits) - `model` – Model identifier (e.g., "gemini-2.5-flash", "gpt-5", "claude-sonnet-4-5") -- `context` – Calling context (e.g., "agent.name.agent_id" or "module.function:line") +- `context` – Calling context (e.g., "agent.name.use_id" or "module.function:line") - `usage` – Token counts dictionary with normalized field names Optional fields: @@ -796,16 +796,16 @@ The logging system normalizes provider-specific formats (OpenAI, Gemini, Anthrop ## Agent Event Logs -The `agents/` directory stores event logs for all AI agent sessions managed by Cortex. Each agent session produces a JSONL file containing the complete event history. +The `talents/` directory stores event logs for all AI talent sessions managed by Cortex. Each talent session produces a JSONL file containing the complete event history. **Directory layout:** - `/` – per-agent subdirectory (e.g., `default/`, `entities--observer/`) -- `/_active.jsonl` – currently running agent (renamed when complete) -- `/.jsonl` – completed agent session +- `/_active.jsonl` – currently running agent (renamed when complete) +- `/.jsonl` – completed agent session - `.log` – symlink to the latest completed run for each agent name - `.jsonl` – day index with one summary line per agent that completed on that day -The `agent_id` is a Unix timestamp in milliseconds that uniquely identifies the session. +The `use_id` is a Unix timestamp in milliseconds that uniquely identifies the session. **Event format (JSONL):** @@ -1040,8 +1040,8 @@ There are two types of events: - **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) ```jsonl -{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/agents/meetings.md", "details": "Sprint planning discussion"} -{"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/agents/schedule.md", "details": "Final review before release"} +{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/talents/meetings.md", "details": "Sprint planning discussion"} +{"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/talents/schedule.md", "details": "Final review before release"} ``` **Common fields:** @@ -1069,7 +1069,7 @@ After captures are processed, segment-level outputs are generated within each se #### Daily outputs -Post-processing generates day-level outputs in the `agents/` directory that synthesize all segments. +Post-processing generates day-level outputs in the `talents/` directory that synthesize all segments. **Generator discovery:** Available generator types are discovered at runtime from: - `talent/*.md` – system generator templates (files with `schedule` field but no `tools` field) @@ -1078,8 +1078,8 @@ Post-processing generates day-level outputs in the `agents/` directory that synt Each template is a `.md` file with JSON frontmatter containing metadata (title, description, schedule, output format). The `schedule` field is required and must be `"segment"` or `"daily"` - generators with missing or invalid schedule are skipped. Use `get_talent_configs(has_tools=False)` from `think/talent.py` to retrieve all available generators, or `get_talent_configs(has_tools=False, schedule="daily")` to get generators filtered by schedule. **Output naming:** -- System outputs: `agents/{agent}.md` (e.g., `agents/flow.md`, `agents/meetings.md`) -- App outputs: `agents/_{app}_{agent}.md` (e.g., `agents/_entities_observer.md`) -- JSON output: `agents/{agent}.json` when metadata specifies `"output": "json"` +- System outputs: `talents/{agent}.md` (e.g., `talents/flow.md`, `talents/meetings.md`) +- App outputs: `talents/_{app}_{agent}.md` (e.g., `talents/_entities_observer.md`) +- JSON output: `talents/{agent}.json` when metadata specifies `"output": "json"` Each generator type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form. diff --git a/docs/PROMPT_TEMPLATES.md b/docs/PROMPT_TEMPLATES.md index d21c20089..cd298a130 100644 --- a/docs/PROMPT_TEMPLATES.md +++ b/docs/PROMPT_TEMPLATES.md @@ -148,7 +148,7 @@ You are a helpful assistant... **Optional model configuration:** Add `max_output_tokens` (response length limit) and `thinking_budget` (model thinking token budget) to override provider defaults. Note: OpenAI uses fixed reasoning and ignores `thinking_budget`. -**Reference:** `think/talent.py` → `get_agent()` for agent configuration loading +**Reference:** `think/talent.py` → `get_talent()` for agent configuration loading ### The load_prompt() Function diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index e567e85a7..ffbf5a9a4 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -121,7 +121,7 @@ async def run_cogitate( - `extra_context`: Runtime context (facets, insights list, datetime) as first user message - `user_instruction`: Agent-specific prompt as second user message - `tools`: Optional list of allowed tool names -- `agent_id`, `name`: Identity for logging and tool calls +- `use_id`, `name`: Identity for logging and tool calls - `session_id`: CLI session ID for conversation continuation - `chat_id`: Chat ID for reverse lookup from agent to chat diff --git a/docs/THINK.md b/docs/THINK.md index 4d4c749d3..2cb8c45ec 100644 --- a/docs/THINK.md +++ b/docs/THINK.md @@ -17,7 +17,7 @@ The package exposes several commands: - `sol call transcripts read` groups audio and screen transcripts into report sections. Use `--start` and `--length` to limit the report to a specific time range. See `sol call transcripts --help` for additional commands. - `sol dream` runs generators and agents for a single day via Cortex. -- `sol agents` is the unified CLI for tool agents and generators (spawned by Cortex, NDJSON protocol). +- `python -m think.talents` is the unified execution module for tool talents and generators spawned by Cortex (NDJSON protocol). - `sol supervisor` monitors observation heartbeats. Use `--no-observers` to disable local capture (sense still runs for observer uploads and imports). - `sol cortex` starts a Callosum-based service for managing AI agent instances and generators. - `sol talent` lists available agents and generators with their configuration. Use `sol talent show ` to see details, and `sol talent show --prompt` to see the fully composed prompt that would be sent to the LLM. @@ -91,30 +91,30 @@ All scheduled prompts (both generators and tool-using agents) share a unified pr After each generator completes and creates output, the indexer runs `--rescan-file` for incremental indexing. A full `--rescan` runs in the post phase. -### Cortex: Central Agent Manager +### Cortex: Central Talent Manager -The Cortex service (`sol cortex`) is the central system for managing AI agent instances and generators. It monitors the journal's `agents/` directory for new requests and manages execution. All agent spawning should go through Cortex for proper event tracking and management. +The Cortex service (`sol cortex`) is the central system for managing AI talent instances and generators. It monitors the journal's `talents/` directory for new requests and manages execution. All talent spawning should go through Cortex for proper event tracking and management. Cortex routes requests based on configuration: -- Requests with `tools` field → tool-using agents (`sol agents`) -- Requests with `output` field (no `tools`) → generators (`sol agents`) +- Requests with `tools` field → tool-using talents (`python -m think.talents`) +- Requests with `output` field (no `tools`) → generators (`python -m think.talents`) -Both types are handled by the unified `sol agents` CLI which routes internally. +Both types are handled by the unified `python -m think.talents` execution module. -To spawn agents programmatically, use the cortex_client functions: +To spawn talents programmatically, use the cortex_client functions: ```python from think.cortex_client import cortex_request from think.callosum import CallosumConnection # Create a request -agent_id = cortex_request( +use_id = cortex_request( prompt="Your task here", name="default", provider="openai" # or "google", "anthropic", "claude" ) -# Watch for agent events via Callosum +# Watch for talent events via Callosum def on_event(message): # Filter for cortex tract events if message.get('tract') != 'cortex': @@ -135,10 +135,10 @@ watcher.stop() Generators can also be spawned via `cortex_request` by including an `output` field: ```python -from think.cortex_client import cortex_request, wait_for_agents +from think.cortex_client import cortex_request, wait_for_uses # Spawn a generator -agent_id = cortex_request( +use_id = cortex_request( prompt="", # Generators don't use prompts name="activity", config={ @@ -149,15 +149,15 @@ agent_id = cortex_request( ) # Wait for completion -completed, timed_out = wait_for_agents([agent_id], timeout=300) +completed, timed_out = wait_for_uses([use_id], timeout=300) ``` ### Direct CLI Usage (Testing Only) -The `sol agents` command is primarily used internally by Cortex. For testing purposes, it can be invoked directly: +The `sol providers check` command is an ad-hoc provider check CLI. Cortex does not use it as the talent spawn path. For testing purposes, it can be invoked directly: ```bash -sol agents [TASK_FILE] [--provider PROVIDER] [--model MODEL] [--max-tokens N] [-o OUT_FILE] +sol providers check [TASK_FILE] [--provider PROVIDER] [--model MODEL] [--max-tokens N] [-o OUT_FILE] ``` The provider can be ``openai`` (default), ``google``, ``anthropic``, or ``ollama``. Configure the corresponding API key in the ``env`` section of ``journal/config/journal.json`` (e.g., ``OPENAI_API_KEY``, ``GOOGLE_API_KEY``, or ``ANTHROPIC_API_KEY``). The ``ollama`` provider requires no API key — it connects to a local Ollama instance. Keys are loaded into ``os.environ`` by ``setup_cli()`` at process startup. @@ -196,7 +196,7 @@ Cortex is the central agent management system that all agent spawning should go The `think.cortex_client` module provides functions for interacting with Cortex: ```python -from think.cortex_client import cortex_request, cortex_agents +from think.cortex_client import cortex_request, cortex_uses # Create an agent request request_file = cortex_request( @@ -206,7 +206,7 @@ request_file = cortex_request( ) # List running and completed agents -agents_info = cortex_agents(limit=10, agent_type="live") +agents_info = cortex_uses(limit=10, use_type="live") print(f"Found {agents_info['live_count']} running agents") ``` # Talent Module @@ -218,7 +218,7 @@ AI agent system and tool-calling support for solstone. | Command | Purpose | |---------|---------| | `sol cortex` | Agent orchestration service | -| `sol agents` | Direct agent invocation (testing only) | +| `sol providers check` | Ad-hoc provider check (testing only) | ## Architecture @@ -245,7 +245,7 @@ Providers implement `run_generate()`, `run_agenerate()`, and `run_cogitate()` fu ## Key Components - **cortex.py** - Central agent manager, file watcher, event distribution, spawns agents.py -- **cortex_client.py** - Client functions: `cortex_request()`, `cortex_agents()`, `wait_for_agents()` +- **cortex_client.py** - Client functions: `cortex_request()`, `cortex_uses()`, `wait_for_uses()` - **agents.py** - Unified CLI entry point for both tool-using agents and generators (NDJSON protocol) - **models.py** - Unified `generate()`/`agenerate()` API, provider routing, token logging - **batch.py** - `Batch` class for concurrent LLM requests with dynamic queuing diff --git a/docs/design/yesterdays-processing-card.md b/docs/design/yesterdays-processing-card.md index 6868ae9d4..a265d8e92 100644 --- a/docs/design/yesterdays-processing-card.md +++ b/docs/design/yesterdays-processing-card.md @@ -32,7 +32,7 @@ Internal helpers called only by `_summarize_yesterday_processing`: Reads `stats_data["heatmap_data"]["hours"]`, keeps the top 3 non-zero hours, sorts by minutes desc then hour asc. - `_knowledge_graph_freshness(yesterday: str) -> dict` - Reads `chronicle/{yesterday}/agents/knowledge_graph.md`, checks existence and `st_mtime` freshness using the relaxed rule in section 4. + Reads `chronicle/{yesterday}/talents/knowledge_graph.md`, checks existence and `st_mtime` freshness using the relaxed rule in section 4. - `_briefing_freshness(today: str) -> dict` Reads `journal/sol/briefing.md` with local `frontmatter.load`. Valid only when frontmatter has `type: morning_briefing` and a parseable `generated` timestamp whose local date is `today`. @@ -125,7 +125,7 @@ Recommended rendered content by mode: ### Ground truth -- `agent.fail` records include `name`, `agent_id`, `state`, and optional `facet`, but `summarize_pipeline_day()` counts every failure and drops `facet` from `failed_list`. See `think/pipeline_health.py:81-99`. +- `talent.fail` records include `name`, `use_id`, `state`, and optional `facet`, but `summarize_pipeline_day()` counts every failure and drops `facet` from `failed_list`. See `think/pipeline_health.py:81-99`. - `stats.json.facet_data` is not a newsletter ledger. It is built from `events.jsonl` durations in `think/journal_stats.py:296-319` and surfaced in `apps/home/routes.py:616-621`. - The facet newsletter writer is `sol call journal news`, implemented by `think/tools/facets.py:61-106`. - The newsletter prompt key is stable: `facet_newsletter`. @@ -135,9 +135,9 @@ Recommended rendered content by mode: ### Option A — re-parse dream JSONL for newsletter-specific facet fails -Read `chronicle/{yesterday}/health/*_daily_dream.jsonl` and count `agent.fail` records where: +Read `chronicle/{yesterday}/health/*_daily_dream.jsonl` and count `talent.fail` records where: -- `event == "agent.fail"` +- `event == "talent.fail"` - `facet` is present - `name == "facet_newsletter"` @@ -163,7 +163,7 @@ Cons: ### Option B — re-parse any facet-scoped fail -Count every `agent.fail` with a `facet` field, regardless of `name`. +Count every `talent.fail` with a `facet` field, regardless of `name`. Pros: @@ -224,7 +224,7 @@ Do not require `mtime` to fall strictly within yesterday’s wall-clock day. Rationale: -- Prep already found a real case where `chronicle/20260415/agents/knowledge_graph.md` had `mtime` on `2026-04-16 07:23:43`. +- Prep already found a real case where `chronicle/20260415/talents/knowledge_graph.md` had `mtime` on `2026-04-16 07:23:43`. - The intent of the card is “did the overnight processing refresh yesterday’s graph?”, not “did the write finish before midnight”. - This rule admits same-day and overnight-after-midnight completions without introducing an arbitrary 36-hour window. @@ -350,7 +350,7 @@ Supporting non-chronicle fixture: Fixture minimization rule: - Seed only the fields each test asserts on. -- Keep dream logs to the minimum lines needed: `run.start`, `agent.dispatch`, `agent.complete` or `agent.fail`, `run.complete`. +- Keep dream logs to the minimum lines needed: `run.start`, `talent.dispatch`, `talent.complete` or `talent.fail`, `run.complete`. ## 9. Non-goals @@ -383,7 +383,7 @@ Fixture minimization rule: All three gate items resolved. Proceed to `implement` stage. -- **Q2 denominator:** Go with **Option A** as recommended. Successes from `facets/*/news/{yesterday}.md`. Failures from dream-log `agent.fail` where `name == "facet_newsletter"` and `facet` is present. When current pipeline emits no `facet_newsletter` fails (which is the common case today), `M == N` and the `N of M` sentence degenerates into a simple `N` — that's fine, honest, and forward-compatible for when we start logging newsletter failures under that exact key. Use the sparse fallback "I didn't produce any facet newsletters." when both are zero. +- **Q2 denominator:** Go with **Option A** as recommended. Successes from `facets/*/news/{yesterday}.md`. Failures from dream-log `talent.fail` where `name == "facet_newsletter"` and `facet` is present. When current pipeline emits no `facet_newsletter` fails (which is the common case today), `M == N` and the `N of M` sentence degenerates into a simple `N` — that's fine, honest, and forward-compatible for when we start logging newsletter failures under that exact key. Use the sparse fallback "I didn't produce any facet newsletters." when both are zero. - **Q3 knowledge-graph freshness:** Use the **relaxed rule**: fresh when `knowledge_graph.md` exists and `st_mtime >= start_of_yesterday_local`. Overnight-after-midnight completions count. Use local time boundaries. Don't use birth/ctime. - **First-week framing copy (verbatim):** The exact copy IS in the scope (top-level note) and in the approved CPO spec. Use this text, unchanged, when `journal_age_days <= 7` and `mode != "sparse"`: diff --git a/scripts/gate_agents_rename.py b/scripts/gate_agents_rename.py new file mode 100644 index 000000000..d24486906 --- /dev/null +++ b/scripts/gate_agents_rename.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: AGPL-3.0-only + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SHIM_FILES = { + Path("think/pipeline_health.py"), + Path("apps/home/routes.py"), +} +ALLOWLIST_RE = re.compile(r"^apps/sol/maint/00[0-4]_.+\.py$") +PRODUCTION_PREFIXES = ("think/", "apps/", "talent/", "convey/", "observe/") +SHIM_WINDOW = 20 + +RULES = [ + ( + "legacy dream emitter", + re.compile(r'_jsonl_log\(\s*["\']agent\.(fail|dispatch|complete|skip)["\']'), + None, + ), + ( + "legacy callosum emitter", + re.compile(r'emit\(\s*["\']agent_(started|completed)["\']'), + None, + ), + ("legacy module path", re.compile(r"\bthink\.agents\b"), None), + ("legacy/new CLI command", re.compile(r"\bsol agents\b|\bsol talents\b"), None), + ("legacy payload key", re.compile(r'["\']agent_id["\']\s*:'), "production"), + ("legacy wire event", re.compile(r'["\']agent_updated["\']'), "production"), + ( + "legacy summary/anomaly key", + re.compile( + r'summary\["agents"\]|["\']agent_failure["\']|["\']agents_fired["\']' + ), + "production", + ), +] + + +def tracked_files() -> list[Path]: + result = subprocess.run( + ["git", "ls-files"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + return [Path(line) for line in result.stdout.splitlines() if line] + + +def is_allowed(path: Path) -> bool: + path_str = path.as_posix() + if path == Path("AGENTS.md"): + return True + if path == Path("tests/test_maint_004_rename.py"): + return True + if path == Path("scripts/gate_agents_rename.py"): + return True + if path_str.startswith(".agents/skills/"): + return True + if ALLOWLIST_RE.match(path_str): + return True + return False + + +def is_production(path: Path) -> bool: + path_str = path.as_posix() + return path_str == "sol.py" or path_str.startswith(PRODUCTION_PREFIXES) + + +def iter_lines(path: Path) -> list[tuple[int, str]]: + lines = (ROOT / path).read_text(encoding="utf-8").splitlines() + if path not in SHIM_FILES: + return list(enumerate(lines, start=1)) + + visible: list[tuple[int, str]] = [] + suppress_until = 0 + for line_no, line in enumerate(lines, start=1): + if line_no <= suppress_until: + continue + if "HISTORICAL SHIM:" in line: + suppress_until = line_no + SHIM_WINDOW + continue + visible.append((line_no, line)) + return visible + + +def main() -> int: + failures: list[str] = [] + for path in tracked_files(): + if is_allowed(path): + continue + if not (ROOT / path).is_file(): + continue + try: + lines = iter_lines(path) + except UnicodeDecodeError: + continue + for line_no, line in lines: + for label, pattern, scope in RULES: + if scope == "production" and not is_production(path): + continue + if pattern.search(line): + failures.append(f"{path}:{line_no}: {label}: {line.strip()}") + + if failures: + print("agents rename gate failed:", file=sys.stderr) + for failure in failures: + print(f" {failure}", file=sys.stderr) + return 1 + + print("agents rename gate passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/sol.py b/sol.py index 5632a68e2..3f4c3457b 100644 --- a/sol.py +++ b/sol.py @@ -11,7 +11,7 @@ Usage: Examples: sol import data.json Import data into journal sol dream 20250101 Run daily processing for a day - sol think.agents -h Show help for specific module + sol think.talents -h Show help for specific module """ from __future__ import annotations @@ -57,8 +57,8 @@ COMMANDS: dict[str, str] = { "transfer": "observe.transfer", "export": "observe.export", "observer": "observe.observer_cli", - # AI agents (talent package) - "agents": "think.agents", + # AI providers and talent execution + "providers": "think.providers_cli", "cortex": "think.cortex", "talent": "think.talent_cli", "call": "think.call", @@ -111,8 +111,8 @@ GROUPS: dict[str, list[str]] = { "export", "observer", ], - "Talent (AI agents)": [ - "agents", + "Talent": [ + "providers", "cortex", "talent", "engage", diff --git a/talent/activities.py b/talent/activities.py index a974d091f..1b510fa72 100644 --- a/talent/activities.py +++ b/talent/activities.py @@ -46,7 +46,7 @@ logger = logging.getLogger(__name__) def _list_facets_with_activity_state(day: str, segment: str, stream: str) -> list[str]: """Find all facets that have activity_state.json in a segment.""" - agents_dir = segment_path(day, segment, stream) / "agents" + agents_dir = segment_path(day, segment, stream) / "talents" if not agents_dir.is_dir(): return [] @@ -61,7 +61,7 @@ def _list_facets_with_activity_state(day: str, segment: str, stream: str) -> lis def _load_activity_state(day: str, segment: str, facet: str, stream: str) -> list[dict]: """Load activity_state.json for a facet in a segment. Returns [] on failure.""" state_path = ( - segment_path(day, segment, stream) / "agents" / facet / "activity_state.json" + segment_path(day, segment, stream) / "talents" / facet / "activity_state.json" ) if not state_path.exists(): return [] diff --git a/talent/activity_state.py b/talent/activity_state.py index 1fd126679..2146f4edc 100644 --- a/talent/activity_state.py +++ b/talent/activity_state.py @@ -34,7 +34,7 @@ def _extract_facet_from_output_path(output_path: str) -> str | None: """Extract facet name from output path. Output paths for faceted generators follow the pattern: - {day}/{stream}/{segment}/agents/{facet}/activity_state.json + {day}/{stream}/{segment}/talents/{facet}/activity_state.json Returns None if facet cannot be extracted. """ @@ -46,7 +46,7 @@ def _extract_facet_from_output_path(output_path: str) -> str | None: return None parent = os.path.basename(os.path.dirname(output_path)) - if parent and parent != "agents": + if parent and parent != "talents": return parent return None @@ -124,7 +124,7 @@ def load_previous_state( parsed JSON array or None if not found/invalid. """ state_path = ( - segment_path(day, segment, stream) / "agents" / facet / "activity_state.json" + segment_path(day, segment, stream) / "talents" / facet / "activity_state.json" ) if not state_path.exists(): return None, None diff --git a/talent/daily_schedule.md b/talent/daily_schedule.md index 18f5e6d70..b454f9fa9 100644 --- a/talent/daily_schedule.md +++ b/talent/daily_schedule.md @@ -10,7 +10,7 @@ "color": "#455a64", "thinking_budget": 4096, "max_output_tokens": 512, - "load": {"transcripts": false, "percepts": false, "agents": false} + "load": {"transcripts": false, "percepts": false, "talents": false} } $facets diff --git a/talent/decisions.md b/talent/decisions.md index 080925078..4ed93aaf7 100644 --- a/talent/decisions.md +++ b/talent/decisions.md @@ -10,7 +10,7 @@ "activities": ["meeting", "call", "messaging", "email"], "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/documents.md b/talent/documents.md index da557128b..fdbc765b3 100644 --- a/talent/documents.md +++ b/talent/documents.md @@ -10,7 +10,7 @@ "thinking_budget": 8192, "max_output_tokens": 8192, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": false} + "load": {"transcripts": true, "percepts": false, "talents": false} } diff --git a/talent/entities.md b/talent/entities.md index 76807a9c2..64046b4d7 100644 --- a/talent/entities.md +++ b/talent/entities.md @@ -10,7 +10,7 @@ "thinking_budget": 4096, "max_output_tokens": 1024, "output": "md", - "load": {"transcripts": true, "percepts": true, "agents": false} + "load": {"transcripts": true, "percepts": true, "talents": false} } diff --git a/talent/facet_newsletter.md b/talent/facet_newsletter.md index c97d116b8..cf7642e05 100644 --- a/talent/facet_newsletter.md +++ b/talent/facet_newsletter.md @@ -9,7 +9,7 @@ "priority": 40, "multi_facet": true, "load": { - "agents": True, + "talents": True, "journal": True } } diff --git a/talent/flow.md b/talent/flow.md index 07a6cc1fd..975c7a4c1 100644 --- a/talent/flow.md +++ b/talent/flow.md @@ -9,7 +9,7 @@ "schedule": "daily", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/followups.md b/talent/followups.md index 13a06e5db..fc65dc6e4 100644 --- a/talent/followups.md +++ b/talent/followups.md @@ -10,7 +10,7 @@ "activities": ["meeting", "call", "messaging", "email"], "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/heartbeat.md b/talent/heartbeat.md index b1afe760c..89d8de5cb 100644 --- a/talent/heartbeat.md +++ b/talent/heartbeat.md @@ -34,9 +34,9 @@ If you find issues: update agency.md's `## system` section via ## Step 2: Check journal quality -Run `sol talent logs --daily -c 10` to review recent agent runs and +Run `sol talent logs --daily -c 10` to review recent talent runs and `sol talent logs --errors -c 10` for recent errors. Look for: -- Broken segments (transcription failures, missing agent output) +- Broken segments (transcription failures, missing talent output) - Processing gaps (capture with no dream processing) - Orphaned entities (zero observations after 7+ days) diff --git a/talent/knowledge_graph.md b/talent/knowledge_graph.md index c421812bb..bfa171e2a 100644 --- a/talent/knowledge_graph.md +++ b/talent/knowledge_graph.md @@ -9,7 +9,7 @@ "schedule": "daily", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/meetings.md b/talent/meetings.md index e68ca6fe9..4f2fc35b7 100644 --- a/talent/meetings.md +++ b/talent/meetings.md @@ -10,7 +10,7 @@ "activities": ["meeting"], "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/messaging.md b/talent/messaging.md index cf511e6ce..c7a7c1809 100644 --- a/talent/messaging.md +++ b/talent/messaging.md @@ -10,7 +10,7 @@ "activities": ["messaging", "email"], "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } $facets diff --git a/talent/morning_briefing.md b/talent/morning_briefing.md index b9c051154..c15280f65 100644 --- a/talent/morning_briefing.md +++ b/talent/morning_briefing.md @@ -70,8 +70,8 @@ Build five sections from the gathered data. **Omit any section entirely if it ha **Source attribution.** Attribute high-consequence factual claims to their source using inline parenthetical links with `sol://` URIs. Not every claim needs attribution — calendar events are self-evident and the Reading section is inherently attributed. `sol://` URI construction: -- **Search results:** The header includes an `id` (e.g. `20260304/archon/143022_300/agents/followups.md:2`). Strip `:idx`, then strip `/agents/{agent}.md` → `sol://20260304/archon/143022_300`. -- **Entity intelligence:** `activity[].path` contains a journal-relative path. Strip `/agents/{agent}.md` to get the segment or day path. If no stream/segment_key: `sol://{day}/agents/{agent}`. +- **Search results:** The header includes an `id` (e.g. `20260304/archon/143022_300/talents/followups.md:2`). Strip `:idx`, then strip `/talents/{agent}.md` → `sol://20260304/archon/143022_300`. +- **Entity intelligence:** `activity[].path` contains a journal-relative path. Strip `/talents/{agent}.md` to get the segment or day path. If no stream/segment_key: `sol://{day}/talents/{agent}`. - **Facet newsletters:** `sol://facets/{facet}/news/{day_YYYYMMDD}`. **Your Day** — What's ahead today. Lead with calendar events in chronological order. For each meeting, include who's attending and one line of entity-informed context (e.g., "last met 2 weeks ago, discussed product roadmap"). Include relevant todos due today. If no calendar events exist, lead with the highest-priority todos. @@ -90,7 +90,7 @@ Grade highlights by evidence strength. **High** (corroborated by multiple source 4. Unscheduled todos (action items with no calendar time blocked) Pipeline gaps owner-facing phrasings (from `pipeline_anomalies`). Use these verbatim, substituting real counts and agent names from the summary: - `activity_agents_missing` → "**Pipeline gap:** N activities ended yesterday but activity agents didn't fire — meeting notes, decisions, and follow-ups may be missing." - - `agent_failure` → "**Pipeline issue:** N agents timed out during yesterday's processing (name1, name2). Some insights may be incomplete." (Use "timed out" when every failed agent has `state == "timeout"`; otherwise use "failed".) + - `talent_failure` → "**Pipeline issue:** N agents timed out during yesterday's processing (name1, name2). Some insights may be incomplete." (Use "timed out" when every failed agent has `state == "timeout"`; otherwise use "failed".) - `daily_agents_missing` → "**Pipeline gap:** Daily agents didn't run yesterday despite journal data. Facet newsletters and digest may be missing." Do NOT include this section when pipeline status is `healthy` (status == "healthy" or anomalies list is empty). Zero noise on normal days. @@ -141,7 +141,7 @@ gaps: [list of gap descriptions, or empty list [] if none] ## Forward Look - Board meeting Thursday — slides need review (confirmed on [calendar](sol://20260327/calendar)) -- May want to prepare quarterly metrics based on last quarter's timing (from [anticipation](sol://20260327/agents/anticipation)) +- May want to prepare quarterly metrics based on last quarter's timing (from [anticipation](sol://20260327/talents/anticipation)) [more items...] ## Reading diff --git a/talent/schedule.md b/talent/schedule.md index 52d80daf8..20154aa77 100644 --- a/talent/schedule.md +++ b/talent/schedule.md @@ -8,7 +8,7 @@ "schedule": "daily", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } diff --git a/talent/screen.md b/talent/screen.md index 5de4d69d1..3b008312c 100644 --- a/talent/screen.md +++ b/talent/screen.md @@ -7,7 +7,7 @@ "schedule": "segment", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": "required", "agents": false} + "load": {"transcripts": true, "percepts": "required", "talents": false} } diff --git a/talent/sense.md b/talent/sense.md index 9dd57b503..e69c2b244 100644 --- a/talent/sense.md +++ b/talent/sense.md @@ -10,7 +10,7 @@ "thinking_budget": 4096, "max_output_tokens": 4096, "output": "json", - "load": {"transcripts": true, "percepts": true, "agents": false} + "load": {"transcripts": true, "percepts": true, "talents": false} } $facets diff --git a/talent/skills.md b/talent/skills.md index 60bde3d83..79754d306 100644 --- a/talent/skills.md +++ b/talent/skills.md @@ -7,7 +7,7 @@ "activities": ["*"], "priority": 90, "output": "json", - "load": {"transcripts": false, "percepts": false, "agents": false} + "load": {"transcripts": false, "percepts": false, "talents": false} } You are analyzing recurring activity patterns to identify and document the owner's skills. diff --git a/talent/speaker_attribution.md b/talent/speaker_attribution.md index 30f54788c..c69cb0d3d 100644 --- a/talent/speaker_attribution.md +++ b/talent/speaker_attribution.md @@ -8,7 +8,7 @@ "output": "json", "color": "#d84315", "hook": {"pre": "speaker_attribution", "post": "speaker_attribution"}, - "load": {"transcripts": true, "agents": {"speakers": true, "screen": true}} + "load": {"transcripts": true, "talents": {"speakers": true, "screen": true}} } diff --git a/talent/speaker_attribution.py b/talent/speaker_attribution.py index c7d8e6227..b75f440f5 100644 --- a/talent/speaker_attribution.py +++ b/talent/speaker_attribution.py @@ -42,7 +42,7 @@ def pre_process(context: dict) -> dict | None: logger.info("Attribution skipped: %s", result["error"]) reason = result["error"] if any(seg_dir.glob("*.npz")): - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) out_path = agents_dir / "speaker_labels.json" with open(out_path, "w", encoding="utf-8") as fh: @@ -58,7 +58,7 @@ def pre_process(context: dict) -> dict | None: if not labels: reason = "no_embeddings" if any(seg_dir.glob("*.npz")): - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" agents_dir.mkdir(parents=True, exist_ok=True) out_path = agents_dir / "speaker_labels.json" with open(out_path, "w", encoding="utf-8") as fh: diff --git a/talent/timeline.md b/talent/timeline.md index d0247f876..f4bd11b2a 100644 --- a/talent/timeline.md +++ b/talent/timeline.md @@ -9,7 +9,7 @@ "schedule": "daily", "priority": 10, "output": "md", - "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} + "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} } diff --git a/tests/baselines/api/calendar/day-events.json b/tests/baselines/api/calendar/day-events.json index 852bae516..1e91a3b8d 100644 --- a/tests/baselines/api/calendar/day-events.json +++ b/tests/baselines/api/calendar/day-events.json @@ -10,7 +10,7 @@ "Romeo Montague", "Mercutio Escalus" ], - "source": "20260304/agents/flow.md", + "source": "20260304/talents/flow.md", "startTime": "2026-03-04T09:00:00", "subject": "", "summary": "Conference keynote featuring Juliet Capulet", @@ -27,7 +27,7 @@ "Juliet Capulet", "Romeo Montague" ], - "source": "20260304/agents/flow.md", + "source": "20260304/talents/flow.md", "startTime": "2026-03-04T18:00:00", "subject": "", "summary": "Networking event", @@ -43,7 +43,7 @@ "participants": [ "Juliet Capulet" ], - "source": "20260304/agents/flow.md", + "source": "20260304/talents/flow.md", "startTime": "2026-03-04T09:00:00", "subject": "", "summary": "Juliet presented on unified API gateways", @@ -60,7 +60,7 @@ "Romeo Montague", "Mercutio Escalus" ], - "source": "20260304/agents/flow.md", + "source": "20260304/talents/flow.md", "startTime": "2026-03-04T14:00:00", "subject": "", "summary": "Built API bridge prototype", diff --git a/tests/baselines/api/search/search.json b/tests/baselines/api/search/search.json index 93aea6bf1..4056145de 100644 --- a/tests/baselines/api/search/search.json +++ b/tests/baselines/api/search/search.json @@ -1,5 +1,5 @@ { - "agents": [], + "talents": [], "days": [], "facets": [ { diff --git a/tests/baselines/api/sol/run-detail.json b/tests/baselines/api/sol/run-detail.json index 0c753ef0d..456e79420 100644 --- a/tests/baselines/api/sol/run-detail.json +++ b/tests/baselines/api/sol/run-detail.json @@ -4,13 +4,7 @@ "error_message": null, "events": [ { - "agent": "solstone", - "agent_id": "1700000000001", - "event": "agent_updated", - "ts": 1700000000200 - }, - { - "agent_id": "1700000000001", + "use_id": "1700000000001", "args": null, "call_id": "call_001", "event": "tool_end", @@ -19,7 +13,7 @@ "ts": 1700000000500 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "args": { "limit": 5, "query": "project updates" @@ -30,13 +24,13 @@ "ts": 1700000000400 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "event": "thinking", "ts": 1700000000300 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, @@ -46,13 +40,19 @@ } }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "event": "start", "model": "gpt-4o", "name": "default", "prompt": "Search for meetings about project updates", "provider": "openai", "ts": 1700000000100 + }, + { + "talent": "solstone", + "use_id": "1700000000001", + "event": "talent_updated", + "ts": 1700000000200 } ], "facet": null, diff --git a/tests/baselines/api/sol/agents-day.json b/tests/baselines/api/sol/talents-day.json similarity index 99% rename from tests/baselines/api/sol/agents-day.json rename to tests/baselines/api/sol/talents-day.json index 8450a8a39..d2b64df7b 100644 --- a/tests/baselines/api/sol/agents-day.json +++ b/tests/baselines/api/sol/talents-day.json @@ -1,5 +1,5 @@ { - "agents": { + "talents": { "anticipation": { "app": null, "color": "#4527a0", @@ -523,5 +523,5 @@ "title": "Verona" } }, - "runs": [] + "uses": [] } diff --git a/tests/baselines/api/stats/stats.json b/tests/baselines/api/stats/stats.json index ea2540952..fb04925a5 100644 --- a/tests/baselines/api/stats/stats.json +++ b/tests/baselines/api/stats/stats.json @@ -8,7 +8,7 @@ "pre": "daily_schedule" }, "load": { - "agents": false, + "talents": false, "percepts": false, "transcripts": false }, @@ -36,7 +36,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -59,7 +59,7 @@ "pre": "documents" }, "load": { - "agents": false, + "talents": false, "percepts": false, "transcripts": true }, @@ -81,7 +81,7 @@ "post": "entities" }, "load": { - "agents": false, + "talents": false, "percepts": true, "transcripts": true }, @@ -106,7 +106,7 @@ "pre": "entities:entity_observer" }, "load": { - "agents": false, + "talents": false, "percepts": false, "transcripts": false }, @@ -129,7 +129,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -158,7 +158,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -181,7 +181,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -207,7 +207,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -234,7 +234,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -257,7 +257,7 @@ "post": "anticipation" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, @@ -276,7 +276,7 @@ "color": "#9c27b0", "description": "Creates a detailed documentary record of screen activity. Focuses on the 'what' - chronological account with preserved details, excerpts, and entities.", "load": { - "agents": false, + "talents": false, "percepts": "required", "transcripts": true }, @@ -293,7 +293,7 @@ "color": "#ff6f00", "description": "Unified segment understanding — density, content type, entities, facets, speakers, and routing recommendations in a single pass", "load": { - "agents": false, + "talents": false, "percepts": true, "transcripts": true }, @@ -320,7 +320,7 @@ "pre": "skills" }, "load": { - "agents": false, + "talents": false, "percepts": false, "transcripts": false }, @@ -341,7 +341,7 @@ "pre": "speaker_attribution" }, "load": { - "agents": { + "talents": { "screen": true, "speakers": true }, @@ -363,7 +363,7 @@ "post": "occurrence" }, "load": { - "agents": { + "talents": { "screen": true }, "percepts": false, diff --git a/tests/baselines/api/agents/badge-count.json b/tests/baselines/api/talents/badge-count.json similarity index 100% rename from tests/baselines/api/agents/badge-count.json rename to tests/baselines/api/talents/badge-count.json diff --git a/tests/baselines/api/agents/preview.json b/tests/baselines/api/talents/preview.json similarity index 100% rename from tests/baselines/api/agents/preview.json rename to tests/baselines/api/talents/preview.json diff --git a/tests/baselines/api/agents/run-detail.json b/tests/baselines/api/talents/run-detail.json similarity index 86% rename from tests/baselines/api/agents/run-detail.json rename to tests/baselines/api/talents/run-detail.json index 0c753ef0d..456e79420 100644 --- a/tests/baselines/api/agents/run-detail.json +++ b/tests/baselines/api/talents/run-detail.json @@ -4,13 +4,7 @@ "error_message": null, "events": [ { - "agent": "solstone", - "agent_id": "1700000000001", - "event": "agent_updated", - "ts": 1700000000200 - }, - { - "agent_id": "1700000000001", + "use_id": "1700000000001", "args": null, "call_id": "call_001", "event": "tool_end", @@ -19,7 +13,7 @@ "ts": 1700000000500 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "args": { "limit": 5, "query": "project updates" @@ -30,13 +24,13 @@ "ts": 1700000000400 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "event": "thinking", "ts": 1700000000300 }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, @@ -46,13 +40,19 @@ } }, { - "agent_id": "1700000000001", + "use_id": "1700000000001", "event": "start", "model": "gpt-4o", "name": "default", "prompt": "Search for meetings about project updates", "provider": "openai", "ts": 1700000000100 + }, + { + "talent": "solstone", + "use_id": "1700000000001", + "event": "talent_updated", + "ts": 1700000000200 } ], "facet": null, diff --git a/tests/baselines/api/agents/stats-month.json b/tests/baselines/api/talents/stats-month.json similarity index 100% rename from tests/baselines/api/agents/stats-month.json rename to tests/baselines/api/talents/stats-month.json diff --git a/tests/baselines/api/agents/agents-day.json b/tests/baselines/api/talents/talents-day.json similarity index 99% rename from tests/baselines/api/agents/agents-day.json rename to tests/baselines/api/talents/talents-day.json index b313af1f3..cb8827f2e 100644 --- a/tests/baselines/api/agents/agents-day.json +++ b/tests/baselines/api/talents/talents-day.json @@ -1,5 +1,5 @@ { - "agents": { + "talents": { "anticipation": { "app": null, "color": "#4527a0", @@ -457,5 +457,5 @@ "title": "Verona" } }, - "runs": [] + "uses": [] } diff --git a/tests/baselines/api/agents/updated-days.json b/tests/baselines/api/talents/updated-days.json similarity index 100% rename from tests/baselines/api/agents/updated-days.json rename to tests/baselines/api/talents/updated-days.json diff --git a/tests/fixtures/journal/AGENTS.md b/tests/fixtures/journal/AGENTS.md index 5f0183cee..45a9f6667 100644 --- a/tests/fixtures/journal/AGENTS.md +++ b/tests/fixtures/journal/AGENTS.md @@ -14,7 +14,7 @@ solstone transforms raw recordings into actionable understanding through a three ┌─────────────────────────────────────┐ │ LAYER 3: AGENT OUTPUTS │ Narrative summaries │ (Markdown files) │ "What it means" -│ - agents/*.md (daily outputs) │ +│ - talents/*.md (daily outputs) │ │ - *.md (segment outputs) │ └─────────────────────────────────────┘ ↑ synthesized from @@ -42,7 +42,7 @@ solstone transforms raw recordings into actionable understanding through a three |------|------------|----------| | **Capture** | Raw audio/video recording | `*.flac`, `*.ogg`, `*.opus`, `*.wav`, `*.webm` | | **Extract** | Structured data from captures | `*.jsonl` | -| **Agent Output** | AI-generated narrative summary | `agents/*.md`, `HHMMSS_LEN/*.md` | +| **Agent Output** | AI-generated narrative summary | `talents/*.md`, `HHMMSS_LEN/*.md` | **Organization** @@ -67,7 +67,7 @@ solstone transforms raw recordings into actionable understanding through a three | `chronicle/` | Container for daily capture folders (`YYYYMMDD/`) containing segments, extracts, and agent outputs | | `entities/` | Journal-level entity identity records (`/entity.json`) | | `facets/` | Facet-specific data: entity relationships, todos, events, news, action logs | -| `agents/` | Agent run logs in per-agent subdirectories (`/.jsonl`), day indexes (`.jsonl`), and latest-run symlinks (`.log`) | +| `talents/` | Talent run logs in per-talent subdirectories (`/.jsonl`), day indexes (`.jsonl`), and latest-run symlinks (`.log`) | | `apps/` | App-specific storage (distinct from codebase `apps/`) | | `streams/` | Per-stream state files (`.json`) tracking segment chains and sequence numbers | | `imports/` | Imported audio files and processing artifacts | @@ -186,14 +186,14 @@ Fields: "Raw media" means layer 1 capture files only: audio files (`.flac`, `.opus`, `.ogg`, `.m4a`, `.wav`), video files (`.webm`, `.mov`, `.mp4`), and screen diffs (`monitor_*_diff.png`). -All layer 2 and layer 3 content is always preserved regardless of retention policy: transcripts (`audio.jsonl`, `screen.jsonl`), agent outputs (`agents/*.md`), speaker labels (`agents/speaker_labels.json`), facet events (`events/*.jsonl`), entity data, segment metadata (`stream.json`), and search index entries. +All layer 2 and layer 3 content is always preserved regardless of retention policy: transcripts (`audio.jsonl`, `screen.jsonl`), talent outputs (`talents/*.md`), speaker labels (`talents/speaker_labels.json`), facet events (`events/*.jsonl`), entity data, segment metadata (`stream.json`), and search index entries. Raw media is never deleted from segments that haven't finished processing. A segment is considered complete only when all four checks pass: -- No `_active.jsonl` files in `agents/` (no running agents) +- No `_active.jsonl` files in `talents/` (no running talents) - `audio.jsonl` (or `*_audio.jsonl`) exists if audio raw media was captured - `screen.jsonl` (or `*_screen.jsonl`) exists if video raw media was captured -- `agents/speaker_labels.json` exists if voice embeddings (`.npz`) are present +- `talents/speaker_labels.json` exists if voice embeddings (`.npz`) are present Purged segments remain fully navigable in convey. Transcripts, entities, speaker labels, and summaries are all intact. The only difference is that audio/video playback is unavailable. @@ -732,7 +732,7 @@ The `logs/` directory within each facet records facet-scoped actions. Logs are o "text": "Review project proposal" }, "facet": "work", - "agent_id": "1765870373972" + "use_id": "1765870373972" } ``` @@ -746,7 +746,7 @@ Both log types share the same structure: - `action` – Action name (e.g., "todo_add", "identity_update") - `params` – Action-specific parameters - `facet` – Facet name (only present in facet-scoped logs) -- `agent_id` – Agent ID (only present for agent tool actions) +- `use_id` – Agent ID (only present for agent tool actions) These logs enable auditing, debugging, and potential rollback of automated actions. @@ -777,7 +777,7 @@ Each line in a token log file is a JSON object with the following structure: Required fields: - `timestamp` – Unix timestamp in milliseconds (13 digits) - `model` – Model identifier (e.g., "gemini-2.5-flash", "gpt-5", "claude-sonnet-4-5") -- `context` – Calling context (e.g., "agent.name.agent_id" or "module.function:line") +- `context` – Calling context (e.g., "agent.name.use_id" or "module.function:line") - `usage` – Token counts dictionary with normalized field names Optional fields: @@ -795,16 +795,16 @@ The logging system normalizes provider-specific formats (OpenAI, Gemini, Anthrop ## Agent Event Logs -The `agents/` directory stores event logs for all AI agent sessions managed by Cortex. Each agent session produces a JSONL file containing the complete event history. +The `talents/` directory stores event logs for all AI talent sessions managed by Cortex. Each talent session produces a JSONL file containing the complete event history. **Directory layout:** - `/` – per-agent subdirectory (e.g., `default/`, `entities--observer/`) -- `/_active.jsonl` – currently running agent (renamed when complete) -- `/.jsonl` – completed agent session +- `/_active.jsonl` – currently running agent (renamed when complete) +- `/.jsonl` – completed agent session - `.log` – symlink to the latest completed run for each agent name - `.jsonl` – day index with one summary line per agent that completed on that day -The `agent_id` is a Unix timestamp in milliseconds that uniquely identifies the session. +The `use_id` is a Unix timestamp in milliseconds that uniquely identifies the session. **Event format (JSONL):** @@ -1039,8 +1039,8 @@ There are two types of events: - **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) ```jsonl -{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/agents/meetings.md", "details": "Sprint planning discussion"} -{"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/agents/schedule.md", "details": "Final review before release"} +{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/talents/meetings.md", "details": "Sprint planning discussion"} +{"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/talents/schedule.md", "details": "Final review before release"} ``` **Common fields:** @@ -1068,7 +1068,7 @@ After captures are processed, segment-level outputs are generated within each se #### Daily outputs -Post-processing generates day-level outputs in the `agents/` directory that synthesize all segments. +Post-processing generates day-level outputs in the `talents/` directory that synthesize all segments. **Generator discovery:** Available generator types are discovered at runtime from: - `talent/*.md` – system generator templates (files with `schedule` field but no `tools` field) @@ -1077,8 +1077,8 @@ Post-processing generates day-level outputs in the `agents/` directory that synt Each template is a `.md` file with JSON frontmatter containing metadata (title, description, schedule, output format). The `schedule` field is required and must be `"segment"` or `"daily"` - generators with missing or invalid schedule are skipped. Use `get_talent_configs(has_tools=False)` from `think/talent.py` to retrieve all available generators, or `get_talent_configs(has_tools=False, schedule="daily")` to get generators filtered by schedule. **Output naming:** -- System outputs: `agents/{agent}.md` (e.g., `agents/flow.md`, `agents/meetings.md`) -- App outputs: `agents/_{app}_{agent}.md` (e.g., `agents/_entities_observer.md`) -- JSON output: `agents/{agent}.json` when metadata specifies `"output": "json"` +- System outputs: `talents/{agent}.md` (e.g., `talents/flow.md`, `talents/meetings.md`) +- App outputs: `talents/_{app}_{agent}.md` (e.g., `talents/_entities_observer.md`) +- JSON output: `talents/{agent}.json` when metadata specifies `"output": "json"` Each generator type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form. diff --git a/tests/fixtures/journal/agents/20231113.jsonl b/tests/fixtures/journal/agents/20231113.jsonl deleted file mode 100644 index 9669989c8..000000000 --- a/tests/fixtures/journal/agents/20231113.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent_id": "1699900000001", "name": "entities", "day": "20231113", "facet": "personal", "ts": 1699900000001, "status": "completed", "runtime_seconds": 8.4, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "daily"} -{"agent_id": "1699900000002", "name": "flow", "day": "20231113", "facet": null, "ts": 1699900060000, "status": "completed", "runtime_seconds": 4.7, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"} diff --git a/tests/fixtures/journal/agents/20231114.jsonl b/tests/fixtures/journal/agents/20231114.jsonl deleted file mode 100644 index 5fe91211a..000000000 --- a/tests/fixtures/journal/agents/20231114.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"agent_id": "1700000000001", "name": "default", "day": "20231114", "facet": null, "ts": 1700000000001, "status": "completed", "runtime_seconds": 0.6, "provider": "openai", "model": "gpt-4o", "schedule": "daily"} -{"agent_id": "1700000000002", "name": "flow", "day": "20231114", "facet": null, "ts": 1700000060000, "status": "error", "runtime_seconds": 13.2, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"} -{"agent_id": "1700000000003", "name": "activity", "day": "20231114", "facet": "work", "ts": 1700000120000, "status": "completed", "runtime_seconds": 6.2, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "activity"} -{"agent_id": "1700000000004", "name": "default", "day": "20231114", "facet": null, "ts": 1700000180000, "status": "completed", "runtime_seconds": 2.1, "provider": "openai", "model": "gpt-4o"} diff --git a/tests/fixtures/journal/agents/20260304.jsonl b/tests/fixtures/journal/agents/20260304.jsonl deleted file mode 100644 index 768a5c9a4..000000000 --- a/tests/fixtures/journal/agents/20260304.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"agent_id": "1772640000001", "name": "flow", "day": "20260304", "facet": null, "ts": 1772676000000, "status": "completed", "runtime_seconds": 5.2, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772640000002", "name": "meetings", "day": "20260304", "facet": null, "ts": 1772676060000, "status": "completed", "runtime_seconds": 3.1, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} -{"agent_id": "1772640000003", "name": "knowledge_graph", "day": "20260304", "facet": null, "ts": 1772676120000, "status": "completed", "runtime_seconds": 8.7, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/agents/20260305.jsonl b/tests/fixtures/journal/agents/20260305.jsonl deleted file mode 100644 index 5c5802631..000000000 --- a/tests/fixtures/journal/agents/20260305.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"agent_id": "1772726400001", "name": "flow", "day": "20260305", "facet": null, "ts": 1772762400000, "status": "completed", "runtime_seconds": 4.8, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772726400002", "name": "meetings", "day": "20260305", "facet": null, "ts": 1772762460000, "status": "completed", "runtime_seconds": 2.9, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} -{"agent_id": "1772737200001", "name": "default", "day": "20260305", "facet": "verona", "ts": 1772737200000, "status": "completed", "runtime_seconds": 12.3, "provider": "openai", "model": "gpt-4o", "schedule": "segment"} diff --git a/tests/fixtures/journal/agents/20260306.jsonl b/tests/fixtures/journal/agents/20260306.jsonl deleted file mode 100644 index a95d3fc76..000000000 --- a/tests/fixtures/journal/agents/20260306.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent_id": "1772812800001", "name": "flow", "day": "20260306", "facet": null, "ts": 1772848800000, "status": "completed", "runtime_seconds": 5.5, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772812800002", "name": "knowledge_graph", "day": "20260306", "facet": null, "ts": 1772848860000, "status": "completed", "runtime_seconds": 9.1, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/agents/20260307.jsonl b/tests/fixtures/journal/agents/20260307.jsonl deleted file mode 100644 index 78a16c176..000000000 --- a/tests/fixtures/journal/agents/20260307.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent_id": "1772899200001", "name": "flow", "day": "20260307", "facet": null, "ts": 1772935200000, "status": "completed", "runtime_seconds": 6.1, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772899200002", "name": "meetings", "day": "20260307", "facet": null, "ts": 1772935260000, "status": "completed", "runtime_seconds": 3.4, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} diff --git a/tests/fixtures/journal/agents/20260308.jsonl b/tests/fixtures/journal/agents/20260308.jsonl deleted file mode 100644 index dcadbe560..000000000 --- a/tests/fixtures/journal/agents/20260308.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"agent_id": "1772985600001", "name": "flow", "day": "20260308", "facet": null, "ts": 1773021600000, "status": "completed", "runtime_seconds": 4.9, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1772985600002", "name": "meetings", "day": "20260308", "facet": null, "ts": 1773021660000, "status": "completed", "runtime_seconds": 2.7, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} -{"agent_id": "1772985600003", "name": "knowledge_graph", "day": "20260308", "facet": null, "ts": 1773021720000, "status": "completed", "runtime_seconds": 7.8, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/agents/20260309.jsonl b/tests/fixtures/journal/agents/20260309.jsonl deleted file mode 100644 index 151f1244b..000000000 --- a/tests/fixtures/journal/agents/20260309.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"agent_id": "1773072000001", "name": "flow", "day": "20260309", "facet": null, "ts": 1773108000000, "status": "completed", "runtime_seconds": 5.3, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} diff --git a/tests/fixtures/journal/agents/20260310.jsonl b/tests/fixtures/journal/agents/20260310.jsonl deleted file mode 100644 index a421d4841..000000000 --- a/tests/fixtures/journal/agents/20260310.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"agent_id": "1773158400001", "name": "flow", "day": "20260310", "facet": null, "ts": 1773194400000, "status": "completed", "runtime_seconds": 6.4, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} -{"agent_id": "1773158400002", "name": "meetings", "day": "20260310", "facet": null, "ts": 1773194460000, "status": "completed", "runtime_seconds": 3.8, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} -{"agent_id": "1773158400003", "name": "knowledge_graph", "day": "20260310", "facet": null, "ts": 1773194520000, "status": "completed", "runtime_seconds": 10.2, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} -{"agent_id": "1773187200001", "name": "default", "day": "20260310", "facet": "verona", "ts": 1773187200000, "status": "completed", "runtime_seconds": 15.7, "provider": "openai", "model": "gpt-4o", "schedule": "segment"} diff --git a/tests/fixtures/journal/agents/default.log b/tests/fixtures/journal/agents/default.log deleted file mode 120000 index 11a1fd824..000000000 --- a/tests/fixtures/journal/agents/default.log +++ /dev/null @@ -1 +0,0 @@ -default/1700000000001.jsonl \ No newline at end of file diff --git a/tests/fixtures/journal/agents/flow/1700000000002.jsonl b/tests/fixtures/journal/agents/flow/1700000000002.jsonl deleted file mode 100644 index fbac917b9..000000000 --- a/tests/fixtures/journal/agents/flow/1700000000002.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"event": "request", "ts": 1700000060000, "agent_id": "1700000000002", "prompt": "Analyze conversation flow", "name": "flow", "provider": "anthropic"} -{"event": "start", "prompt": "Analyze conversation flow", "name": "flow", "model": "claude-3-haiku", "provider": "anthropic", "ts": 1700000060100, "agent_id": "1700000000002"} -{"event": "error", "ts": 1700000060200, "agent_id": "1700000000002", "error": "Rate limit exceeded: too many requests"} diff --git a/tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/audio.md b/tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/screen.md b/tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/default/123456_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20240101/default/123456_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20240101/agents/flow.md b/tests/fixtures/journal/chronicle/20240101/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/agents/flow.md rename to tests/fixtures/journal/chronicle/20240101/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20240101/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20240101/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20240101/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20240101/agents/meetings.md b/tests/fixtures/journal/chronicle/20240101/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240101/agents/meetings.md rename to tests/fixtures/journal/chronicle/20240101/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/audio.md b/tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/screen.md b/tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240102/default/234567_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20240102/default/234567_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20240102/agents/flow.md b/tests/fixtures/journal/chronicle/20240102/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20240102/agents/flow.md rename to tests/fixtures/journal/chronicle/20240102/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speaker_labels.json b/tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speaker_labels.json similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speaker_labels.json rename to tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speaker_labels.json diff --git a/tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speakers.json b/tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speakers.json similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/090000_300/agents/speakers.json rename to tests/fixtures/journal/chronicle/20260304/default/090000_300/talents/speakers.json diff --git a/tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/140000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260304/default/140000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/default/180000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260304/default/180000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260304/agents/flow.md b/tests/fixtures/journal/chronicle/20260304/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/agents/flow.md rename to tests/fixtures/journal/chronicle/20260304/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260304/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260304/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260304/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20260304/agents/meetings.md b/tests/fixtures/journal/chronicle/20260304/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260304/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260304/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/090000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260305/default/090000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/133000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260305/default/133000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/default/220000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260305/default/220000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260305/agents/flow.md b/tests/fixtures/journal/chronicle/20260305/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/agents/flow.md rename to tests/fixtures/journal/chronicle/20260305/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260305/agents/meetings.md b/tests/fixtures/journal/chronicle/20260305/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260305/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260305/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/093000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260306/default/093000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/110000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260306/default/110000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/143000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260306/default/143000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/default/170000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260306/default/170000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260306/agents/flow.md b/tests/fixtures/journal/chronicle/20260306/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/agents/flow.md rename to tests/fixtures/journal/chronicle/20260306/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260306/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260306/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260306/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260306/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20260307/default/100000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260307/default/100000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260307/default/100000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260307/default/100000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260307/default/150000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260307/default/150000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260307/default/150000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260307/default/150000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260307/agents/flow.md b/tests/fixtures/journal/chronicle/20260307/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260307/agents/flow.md rename to tests/fixtures/journal/chronicle/20260307/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260307/agents/meetings.md b/tests/fixtures/journal/chronicle/20260307/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260307/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260307/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260308/default/100000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260308/default/100000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/default/100000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260308/default/100000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260308/default/153000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260308/default/153000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/default/153000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260308/default/153000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260308/agents/flow.md b/tests/fixtures/journal/chronicle/20260308/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/agents/flow.md rename to tests/fixtures/journal/chronicle/20260308/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260308/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260308/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260308/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20260308/agents/meetings.md b/tests/fixtures/journal/chronicle/20260308/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260308/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260308/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/090000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260309/default/090000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/133000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260309/default/133000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/default/193000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260309/default/193000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260309/agents/flow.md b/tests/fixtures/journal/chronicle/20260309/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260309/agents/flow.md rename to tests/fixtures/journal/chronicle/20260309/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/083000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260310/default/083000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/100000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260310/default/100000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/100000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260310/default/100000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/audio.md b/tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/audio.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/audio.md rename to tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/audio.md diff --git a/tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/screen.md b/tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/screen.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/default/170000_300/agents/screen.md rename to tests/fixtures/journal/chronicle/20260310/default/170000_300/talents/screen.md diff --git a/tests/fixtures/journal/chronicle/20260310/agents/flow.md b/tests/fixtures/journal/chronicle/20260310/talents/flow.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/agents/flow.md rename to tests/fixtures/journal/chronicle/20260310/talents/flow.md diff --git a/tests/fixtures/journal/chronicle/20260310/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260310/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260310/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/chronicle/20260310/agents/meetings.md b/tests/fixtures/journal/chronicle/20260310/talents/meetings.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260310/agents/meetings.md rename to tests/fixtures/journal/chronicle/20260310/talents/meetings.md diff --git a/tests/fixtures/journal/chronicle/20260415/agents/knowledge_graph.md b/tests/fixtures/journal/chronicle/20260415/talents/knowledge_graph.md similarity index 100% rename from tests/fixtures/journal/chronicle/20260415/agents/knowledge_graph.md rename to tests/fixtures/journal/chronicle/20260415/talents/knowledge_graph.md diff --git a/tests/fixtures/journal/facets/capulet/events/20260304.jsonl b/tests/fixtures/journal/facets/capulet/events/20260304.jsonl index 400666264..f686fd58b 100644 --- a/tests/fixtures/journal/facets/capulet/events/20260304.jsonl +++ b/tests/fixtures/journal/facets/capulet/events/20260304.jsonl @@ -1,2 +1,2 @@ -{"type": "conference", "start": "09:00:00", "end": "10:00:00", "title": "Denver Tech Summit - Juliet's Keynote", "summary": "Juliet presented on unified API gateways", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260304/agents/flow.md", "participants": ["Juliet Capulet"], "work": true, "details": "Standing ovation for architecture presentation"} -{"type": "social", "start": "18:00:00", "end": "20:00:00", "title": "Conference Mixer", "summary": "Networking event", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260304/agents/flow.md", "participants": ["Juliet Capulet", "Romeo Montague"], "work": false, "details": "Juliet and Romeo exchanged Signal contacts"} +{"type": "conference", "start": "09:00:00", "end": "10:00:00", "title": "Denver Tech Summit - Juliet's Keynote", "summary": "Juliet presented on unified API gateways", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260304/talents/flow.md", "participants": ["Juliet Capulet"], "work": true, "details": "Standing ovation for architecture presentation"} +{"type": "social", "start": "18:00:00", "end": "20:00:00", "title": "Conference Mixer", "summary": "Networking event", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260304/talents/flow.md", "participants": ["Juliet Capulet", "Romeo Montague"], "work": false, "details": "Juliet and Romeo exchanged Signal contacts"} diff --git a/tests/fixtures/journal/facets/capulet/events/20260306.jsonl b/tests/fixtures/journal/facets/capulet/events/20260306.jsonl index 2aaefa066..71b4298c8 100644 --- a/tests/fixtures/journal/facets/capulet/events/20260306.jsonl +++ b/tests/fixtures/journal/facets/capulet/events/20260306.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "11:00:00", "end": "12:00:00", "title": "Capulet Industries Board Meeting", "summary": "Tybalt pitched competing proposal", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260306/agents/flow.md", "participants": ["Tybalt Capulet"], "work": true, "details": "Proposed building mesh routing from scratch"} +{"type": "meeting", "start": "11:00:00", "end": "12:00:00", "title": "Capulet Industries Board Meeting", "summary": "Tybalt pitched competing proposal", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260306/talents/flow.md", "participants": ["Tybalt Capulet"], "work": true, "details": "Proposed building mesh routing from scratch"} diff --git a/tests/fixtures/journal/facets/capulet/events/20260307.jsonl b/tests/fixtures/journal/facets/capulet/events/20260307.jsonl index c031a5e00..6ccba2fba 100644 --- a/tests/fixtures/journal/facets/capulet/events/20260307.jsonl +++ b/tests/fixtures/journal/facets/capulet/events/20260307.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "10:30:00", "title": "Tybalt Confrontation Call", "summary": "Tybalt discovered secret collaboration", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260307/agents/flow.md", "participants": ["Tybalt Capulet", "Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Terminated Mercutio's consulting contract"} +{"type": "meeting", "start": "10:00:00", "end": "10:30:00", "title": "Tybalt Confrontation Call", "summary": "Tybalt discovered secret collaboration", "facet": "capulet", "agent": "flow", "occurred": true, "source": "20260307/talents/flow.md", "participants": ["Tybalt Capulet", "Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Terminated Mercutio's consulting contract"} diff --git a/tests/fixtures/journal/facets/capulet/events/20260310.jsonl b/tests/fixtures/journal/facets/capulet/events/20260310.jsonl index f032a38c7..7f40d15aa 100644 --- a/tests/fixtures/journal/facets/capulet/events/20260310.jsonl +++ b/tests/fixtures/journal/facets/capulet/events/20260310.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Meeting", "summary": "Verona Platform approved as joint venture", "facet": "capulet", "agent": "meetings", "occurred": true, "source": "20260310/agents/meetings.md", "participants": ["Juliet Capulet", "Romeo Montague", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Juliet named co-lead"} +{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Meeting", "summary": "Verona Platform approved as joint venture", "facet": "capulet", "agent": "meetings", "occurred": true, "source": "20260310/talents/meetings.md", "participants": ["Juliet Capulet", "Romeo Montague", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Juliet named co-lead"} diff --git a/tests/fixtures/journal/facets/montague/events/20260304.jsonl b/tests/fixtures/journal/facets/montague/events/20260304.jsonl index e7e34c75a..09e3cb0c0 100644 --- a/tests/fixtures/journal/facets/montague/events/20260304.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260304.jsonl @@ -1,2 +1,2 @@ -{"type": "conference", "start": "09:00:00", "end": "12:00:00", "title": "Denver Tech Summit - Morning Keynote", "summary": "Conference keynote featuring Juliet Capulet", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260304/agents/flow.md", "participants": ["Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Attended keynote on unified API gateways"} -{"type": "hackathon", "start": "14:00:00", "end": "18:00:00", "title": "Hackathon - API Bridge Challenge", "summary": "Built API bridge prototype", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260304/agents/flow.md", "participants": ["Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Tybalt confronted Romeo"} +{"type": "conference", "start": "09:00:00", "end": "12:00:00", "title": "Denver Tech Summit - Morning Keynote", "summary": "Conference keynote featuring Juliet Capulet", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260304/talents/flow.md", "participants": ["Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Attended keynote on unified API gateways"} +{"type": "hackathon", "start": "14:00:00", "end": "18:00:00", "title": "Hackathon - API Bridge Challenge", "summary": "Built API bridge prototype", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260304/talents/flow.md", "participants": ["Romeo Montague", "Mercutio Escalus"], "work": true, "details": "Tybalt confronted Romeo"} diff --git a/tests/fixtures/journal/facets/montague/events/20260305.jsonl b/tests/fixtures/journal/facets/montague/events/20260305.jsonl index 5b9b53c27..2c3adc496 100644 --- a/tests/fixtures/journal/facets/montague/events/20260305.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260305.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Montague Tech Daily Standup", "summary": "Team standup", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260305/agents/meetings.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Romeo mentioned conference ideas"} +{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Montague Tech Daily Standup", "summary": "Team standup", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260305/talents/meetings.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Romeo mentioned conference ideas"} diff --git a/tests/fixtures/journal/facets/montague/events/20260306.jsonl b/tests/fixtures/journal/facets/montague/events/20260306.jsonl index 785030182..8bb73076d 100644 --- a/tests/fixtures/journal/facets/montague/events/20260306.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260306.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "09:30:00", "end": "10:00:00", "title": "Montague Tech Daily Standup", "summary": "Team standup with Benvolio's questions", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260306/agents/flow.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Benvolio noticed late-night commits"} +{"type": "meeting", "start": "09:30:00", "end": "10:00:00", "title": "Montague Tech Daily Standup", "summary": "Team standup with Benvolio's questions", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260306/talents/flow.md", "participants": ["Romeo Montague", "Benvolio Montague", "Mercutio Escalus"], "work": true, "details": "Benvolio noticed late-night commits"} diff --git a/tests/fixtures/journal/facets/montague/events/20260307.jsonl b/tests/fixtures/journal/facets/montague/events/20260307.jsonl index 0ba76be31..692a86789 100644 --- a/tests/fixtures/journal/facets/montague/events/20260307.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260307.jsonl @@ -1,2 +1,2 @@ -{"type": "meeting", "start": "10:00:00", "end": "10:30:00", "title": "Confrontation with Tybalt", "summary": "Tybalt accused Romeo of IP theft", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260307/agents/flow.md", "participants": ["Romeo Montague", "Tybalt Capulet", "Mercutio Escalus"], "work": true, "details": "Mercutio fired from Capulet contract"} -{"type": "meeting", "start": "15:00:00", "end": "16:00:00", "title": "Emergency Team Meeting", "summary": "Crisis response meeting", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260307/agents/meetings.md", "participants": ["Romeo Montague", "Benvolio Montague"], "work": true, "details": "Discussed legal exposure and mediation plan"} +{"type": "meeting", "start": "10:00:00", "end": "10:30:00", "title": "Confrontation with Tybalt", "summary": "Tybalt accused Romeo of IP theft", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260307/talents/flow.md", "participants": ["Romeo Montague", "Tybalt Capulet", "Mercutio Escalus"], "work": true, "details": "Mercutio fired from Capulet contract"} +{"type": "meeting", "start": "15:00:00", "end": "16:00:00", "title": "Emergency Team Meeting", "summary": "Crisis response meeting", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260307/talents/meetings.md", "participants": ["Romeo Montague", "Benvolio Montague"], "work": true, "details": "Discussed legal exposure and mediation plan"} diff --git a/tests/fixtures/journal/facets/montague/events/20260309.jsonl b/tests/fixtures/journal/facets/montague/events/20260309.jsonl index 852f66857..3065e4670 100644 --- a/tests/fixtures/journal/facets/montague/events/20260309.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260309.jsonl @@ -1 +1 @@ -{"type": "task", "start": "09:00:00", "end": "11:00:00", "title": "Infrastructure Setup", "summary": "Benvolio deployed Kubernetes cluster", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260309/agents/flow.md", "participants": ["Benvolio Montague"], "work": true, "details": "Staging cluster for Verona Platform demo"} +{"type": "task", "start": "09:00:00", "end": "11:00:00", "title": "Infrastructure Setup", "summary": "Benvolio deployed Kubernetes cluster", "facet": "montague", "agent": "flow", "occurred": true, "source": "20260309/talents/flow.md", "participants": ["Benvolio Montague"], "work": true, "details": "Staging cluster for Verona Platform demo"} diff --git a/tests/fixtures/journal/facets/montague/events/20260310.jsonl b/tests/fixtures/journal/facets/montague/events/20260310.jsonl index 430237595..7d77834ba 100644 --- a/tests/fixtures/journal/facets/montague/events/20260310.jsonl +++ b/tests/fixtures/journal/facets/montague/events/20260310.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Meeting", "summary": "Verona Platform presentation to both boards", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260310/agents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards approved joint venture"} +{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Meeting", "summary": "Verona Platform presentation to both boards", "facet": "montague", "agent": "meetings", "occurred": true, "source": "20260310/talents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards approved joint venture"} diff --git a/tests/fixtures/journal/facets/personal/events/20240101.jsonl b/tests/fixtures/journal/facets/personal/events/20240101.jsonl index 83bbd10a0..c7b92d7ce 100644 --- a/tests/fixtures/journal/facets/personal/events/20240101.jsonl +++ b/tests/fixtures/journal/facets/personal/events/20240101.jsonl @@ -1 +1 @@ -{"type": "appointment", "start": "18:00:00", "end": "19:00:00", "title": "Gym session", "summary": "Evening workout", "facet": "personal", "agent": "activity", "occurred": true, "source": "20240101/agents/activity.md", "participants": [], "work": false, "details": "Strength training day"} +{"type": "appointment", "start": "18:00:00", "end": "19:00:00", "title": "Gym session", "summary": "Evening workout", "facet": "personal", "agent": "activity", "occurred": true, "source": "20240101/talents/activity.md", "participants": [], "work": false, "details": "Strength training day"} diff --git a/tests/fixtures/journal/facets/verona/events/20260305.jsonl b/tests/fixtures/journal/facets/verona/events/20260305.jsonl index 6bafb2a4e..6e4fab100 100644 --- a/tests/fixtures/journal/facets/verona/events/20260305.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260305.jsonl @@ -1 +1 @@ -{"type": "task", "start": "22:00:00", "end": "23:59:00", "title": "Balcony App Prototype Session", "summary": "Late night coding session", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260305/agents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Created Balcony App with sub-ms latency"} +{"type": "task", "start": "22:00:00", "end": "23:59:00", "title": "Balcony App Prototype Session", "summary": "Late night coding session", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260305/talents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Created Balcony App with sub-ms latency"} diff --git a/tests/fixtures/journal/facets/verona/events/20260306.jsonl b/tests/fixtures/journal/facets/verona/events/20260306.jsonl index 87b7592fa..cf0d2be68 100644 --- a/tests/fixtures/journal/facets/verona/events/20260306.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260306.jsonl @@ -1 +1 @@ -{"type": "task", "start": "14:30:00", "end": "19:30:00", "title": "Verona Platform Integration", "summary": "End-to-end integration work", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260306/agents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Full e2e integration achieved"} +{"type": "task", "start": "14:30:00", "end": "19:30:00", "title": "Verona Platform Integration", "summary": "End-to-end integration work", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260306/talents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet"], "work": true, "details": "Full e2e integration achieved"} diff --git a/tests/fixtures/journal/facets/verona/events/20260308.jsonl b/tests/fixtures/journal/facets/verona/events/20260308.jsonl index 286ee8530..cdd55a574 100644 --- a/tests/fixtures/journal/facets/verona/events/20260308.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260308.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "11:00:00", "title": "Strategy Call with Professor Lawrence", "summary": "Joint venture strategy planning", "facet": "verona", "agent": "meetings", "occurred": true, "source": "20260308/agents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence"], "work": true, "details": "Proposed board presentation strategy"} +{"type": "meeting", "start": "10:00:00", "end": "11:00:00", "title": "Strategy Call with Professor Lawrence", "summary": "Joint venture strategy planning", "facet": "verona", "agent": "meetings", "occurred": true, "source": "20260308/talents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence"], "work": true, "details": "Proposed board presentation strategy"} diff --git a/tests/fixtures/journal/facets/verona/events/20260309.jsonl b/tests/fixtures/journal/facets/verona/events/20260309.jsonl index b54aa4c4a..711fc9949 100644 --- a/tests/fixtures/journal/facets/verona/events/20260309.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260309.jsonl @@ -1 +1 @@ -{"type": "task", "start": "09:00:00", "end": "21:00:00", "title": "Demo Sprint Day", "summary": "Full day preparing board demo", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260309/agents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet", "Benvolio Montague", "Nurse Angela"], "work": true, "details": "Demo deployed, presentation rehearsed"} +{"type": "task", "start": "09:00:00", "end": "21:00:00", "title": "Demo Sprint Day", "summary": "Full day preparing board demo", "facet": "verona", "agent": "flow", "occurred": true, "source": "20260309/talents/flow.md", "participants": ["Romeo Montague", "Juliet Capulet", "Benvolio Montague", "Nurse Angela"], "work": true, "details": "Demo deployed, presentation rehearsed"} diff --git a/tests/fixtures/journal/facets/verona/events/20260310.jsonl b/tests/fixtures/journal/facets/verona/events/20260310.jsonl index 2392e5397..ca18c46d6 100644 --- a/tests/fixtures/journal/facets/verona/events/20260310.jsonl +++ b/tests/fixtures/journal/facets/verona/events/20260310.jsonl @@ -1 +1 @@ -{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Presentation", "summary": "Verona Platform approved", "facet": "verona", "agent": "meetings", "occurred": true, "source": "20260310/agents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards voted yes"} +{"type": "meeting", "start": "10:00:00", "end": "12:00:00", "title": "Joint Board Presentation", "summary": "Verona Platform approved", "facet": "verona", "agent": "meetings", "occurred": true, "source": "20260310/talents/meetings.md", "participants": ["Romeo Montague", "Juliet Capulet", "Friar Lawrence", "Paris Duke", "Tybalt Capulet"], "work": true, "details": "Both boards voted yes"} diff --git a/tests/fixtures/journal/facets/work/events/20240101.jsonl b/tests/fixtures/journal/facets/work/events/20240101.jsonl index 0a0015e41..c2bcece97 100644 --- a/tests/fixtures/journal/facets/work/events/20240101.jsonl +++ b/tests/fixtures/journal/facets/work/events/20240101.jsonl @@ -1,2 +1,2 @@ -{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team standup", "summary": "Daily sync meeting", "facet": "work", "agent": "meetings", "occurred": true, "source": "20240101/agents/meetings.md", "participants": ["Alice", "Bob"], "work": true, "details": "Discussed sprint progress"} -{"type": "task", "start": "10:00:00", "end": "12:00:00", "title": "Code review", "summary": "Review PR #123", "facet": "work", "agent": "activity", "occurred": true, "source": "20240101/agents/activity.md", "participants": [], "work": true, "details": "Reviewed authentication changes"} +{"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team standup", "summary": "Daily sync meeting", "facet": "work", "agent": "meetings", "occurred": true, "source": "20240101/talents/meetings.md", "participants": ["Alice", "Bob"], "work": true, "details": "Discussed sprint progress"} +{"type": "task", "start": "10:00:00", "end": "12:00:00", "title": "Code review", "summary": "Review PR #123", "facet": "work", "agent": "activity", "occurred": true, "source": "20240101/talents/activity.md", "participants": [], "work": true, "details": "Reviewed authentication changes"} diff --git a/tests/fixtures/journal/facets/work/events/20240105.jsonl b/tests/fixtures/journal/facets/work/events/20240105.jsonl index 832393e7e..fd1813909 100644 --- a/tests/fixtures/journal/facets/work/events/20240105.jsonl +++ b/tests/fixtures/journal/facets/work/events/20240105.jsonl @@ -1 +1 @@ -{"type": "meeting", "date": "2024-01-05", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "agent": "schedule", "occurred": false, "source": "20240101/agents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"} +{"type": "meeting", "date": "2024-01-05", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "agent": "schedule", "occurred": false, "source": "20240101/talents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"} diff --git a/tests/fixtures/journal/maint/agents/000_migrate_agent_layout.jsonl b/tests/fixtures/journal/maint/sol/000_migrate_agent_layout.jsonl similarity index 66% rename from tests/fixtures/journal/maint/agents/000_migrate_agent_layout.jsonl rename to tests/fixtures/journal/maint/sol/000_migrate_agent_layout.jsonl index 0ac26cdff..71e3c662d 100644 --- a/tests/fixtures/journal/maint/agents/000_migrate_agent_layout.jsonl +++ b/tests/fixtures/journal/maint/sol/000_migrate_agent_layout.jsonl @@ -1,4 +1,4 @@ -{"event": "exec", "ts": 1770778598011, "app": "agents", "task": "000_migrate_agent_layout", "cmd": ["/home/jer/projects/sunstone/.venv/bin/python3", "-m", "apps.agents.maint.000_migrate_agent_layout"]} +{"event": "exec", "ts": 1770778598011, "app": "sol", "task": "000_migrate_agent_layout", "cmd": ["/home/jer/projects/sunstone/.venv/bin/python3", "-m", "apps.sol.maint.000_migrate_agent_layout"]} {"event": "line", "ts": 1770778598147, "line": "Migration complete"} {"event": "line", "ts": 1770778598147, "line": " moved: 0"} {"event": "line", "ts": 1770778598147, "line": " cleaned: 0"} diff --git a/tests/fixtures/journal/maint/agents/001_migrate_agent_run_logs.jsonl b/tests/fixtures/journal/maint/sol/001_migrate_agent_run_logs.jsonl similarity index 74% rename from tests/fixtures/journal/maint/agents/001_migrate_agent_run_logs.jsonl rename to tests/fixtures/journal/maint/sol/001_migrate_agent_run_logs.jsonl index 04c41f556..84bfa8260 100644 --- a/tests/fixtures/journal/maint/agents/001_migrate_agent_run_logs.jsonl +++ b/tests/fixtures/journal/maint/sol/001_migrate_agent_run_logs.jsonl @@ -1,4 +1,4 @@ -{"event": "exec", "ts": 1770952499902, "app": "agents", "task": "001_migrate_agent_run_logs", "cmd": ["/home/jer/.local/share/hopper/lodes/vymuqdvr/worktree/.venv/bin/python", "-m", "apps.agents.maint.001_migrate_agent_run_logs"]} +{"event": "exec", "ts": 1770952499902, "app": "sol", "task": "001_migrate_agent_run_logs", "cmd": ["/home/jer/.local/share/hopper/lodes/vymuqdvr/worktree/.venv/bin/python", "-m", "apps.sol.maint.001_migrate_agent_run_logs"]} {"event": "line", "ts": 1770952500054, "line": "Migrating agent run logs in: tests/fixtures/journal/agents"} {"event": "line", "ts": 1770952500054, "line": "Migration complete"} {"event": "line", "ts": 1770952500054, "line": " moved: 0"} diff --git a/tests/fixtures/journal/sol/briefing.md b/tests/fixtures/journal/sol/briefing.md index b48957d30..be4318521 100644 --- a/tests/fixtures/journal/sol/briefing.md +++ b/tests/fixtures/journal/sol/briefing.md @@ -36,9 +36,9 @@ gaps: [] ## Forward Look -- **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [anticipation](sol://20260327/agents/anticipation)). +- **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [anticipation](sol://20260327/talents/anticipation)). - **Wednesday** — Deadline for the compliance audit documentation. -- Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [anticipation](sol://20260327/agents/anticipation)). +- Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [anticipation](sol://20260327/talents/anticipation)). ## Reading diff --git a/tests/fixtures/journal/talents/20231113.jsonl b/tests/fixtures/journal/talents/20231113.jsonl new file mode 100644 index 000000000..0877834df --- /dev/null +++ b/tests/fixtures/journal/talents/20231113.jsonl @@ -0,0 +1,2 @@ +{"use_id": "1699900000001", "name": "entities", "day": "20231113", "facet": "personal", "ts": 1699900000001, "status": "completed", "runtime_seconds": 8.4, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "daily"} +{"use_id": "1699900000002", "name": "flow", "day": "20231113", "facet": null, "ts": 1699900060000, "status": "completed", "runtime_seconds": 4.7, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"} diff --git a/tests/fixtures/journal/talents/20231114.jsonl b/tests/fixtures/journal/talents/20231114.jsonl new file mode 100644 index 000000000..aff95e599 --- /dev/null +++ b/tests/fixtures/journal/talents/20231114.jsonl @@ -0,0 +1,4 @@ +{"use_id": "1700000000001", "name": "default", "day": "20231114", "facet": null, "ts": 1700000000001, "status": "completed", "runtime_seconds": 0.6, "provider": "openai", "model": "gpt-4o", "schedule": "daily"} +{"use_id": "1700000000002", "name": "flow", "day": "20231114", "facet": null, "ts": 1700000060000, "status": "error", "runtime_seconds": 13.2, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"} +{"use_id": "1700000000003", "name": "activity", "day": "20231114", "facet": "work", "ts": 1700000120000, "status": "completed", "runtime_seconds": 6.2, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "activity"} +{"use_id": "1700000000004", "name": "default", "day": "20231114", "facet": null, "ts": 1700000180000, "status": "completed", "runtime_seconds": 2.1, "provider": "openai", "model": "gpt-4o"} diff --git a/tests/fixtures/journal/talents/20260304.jsonl b/tests/fixtures/journal/talents/20260304.jsonl new file mode 100644 index 000000000..800e77d28 --- /dev/null +++ b/tests/fixtures/journal/talents/20260304.jsonl @@ -0,0 +1,3 @@ +{"use_id": "1772640000001", "name": "flow", "day": "20260304", "facet": null, "ts": 1772676000000, "status": "completed", "runtime_seconds": 5.2, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772640000002", "name": "meetings", "day": "20260304", "facet": null, "ts": 1772676060000, "status": "completed", "runtime_seconds": 3.1, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} +{"use_id": "1772640000003", "name": "knowledge_graph", "day": "20260304", "facet": null, "ts": 1772676120000, "status": "completed", "runtime_seconds": 8.7, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/talents/20260305.jsonl b/tests/fixtures/journal/talents/20260305.jsonl new file mode 100644 index 000000000..b09e35a9e --- /dev/null +++ b/tests/fixtures/journal/talents/20260305.jsonl @@ -0,0 +1,3 @@ +{"use_id": "1772726400001", "name": "flow", "day": "20260305", "facet": null, "ts": 1772762400000, "status": "completed", "runtime_seconds": 4.8, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772726400002", "name": "meetings", "day": "20260305", "facet": null, "ts": 1772762460000, "status": "completed", "runtime_seconds": 2.9, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} +{"use_id": "1772737200001", "name": "default", "day": "20260305", "facet": "verona", "ts": 1772737200000, "status": "completed", "runtime_seconds": 12.3, "provider": "openai", "model": "gpt-4o", "schedule": "segment"} diff --git a/tests/fixtures/journal/talents/20260306.jsonl b/tests/fixtures/journal/talents/20260306.jsonl new file mode 100644 index 000000000..c4f08949b --- /dev/null +++ b/tests/fixtures/journal/talents/20260306.jsonl @@ -0,0 +1,2 @@ +{"use_id": "1772812800001", "name": "flow", "day": "20260306", "facet": null, "ts": 1772848800000, "status": "completed", "runtime_seconds": 5.5, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772812800002", "name": "knowledge_graph", "day": "20260306", "facet": null, "ts": 1772848860000, "status": "completed", "runtime_seconds": 9.1, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/talents/20260307.jsonl b/tests/fixtures/journal/talents/20260307.jsonl new file mode 100644 index 000000000..da09fabc1 --- /dev/null +++ b/tests/fixtures/journal/talents/20260307.jsonl @@ -0,0 +1,2 @@ +{"use_id": "1772899200001", "name": "flow", "day": "20260307", "facet": null, "ts": 1772935200000, "status": "completed", "runtime_seconds": 6.1, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772899200002", "name": "meetings", "day": "20260307", "facet": null, "ts": 1772935260000, "status": "completed", "runtime_seconds": 3.4, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} diff --git a/tests/fixtures/journal/talents/20260308.jsonl b/tests/fixtures/journal/talents/20260308.jsonl new file mode 100644 index 000000000..d7d0d8b9e --- /dev/null +++ b/tests/fixtures/journal/talents/20260308.jsonl @@ -0,0 +1,3 @@ +{"use_id": "1772985600001", "name": "flow", "day": "20260308", "facet": null, "ts": 1773021600000, "status": "completed", "runtime_seconds": 4.9, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1772985600002", "name": "meetings", "day": "20260308", "facet": null, "ts": 1773021660000, "status": "completed", "runtime_seconds": 2.7, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} +{"use_id": "1772985600003", "name": "knowledge_graph", "day": "20260308", "facet": null, "ts": 1773021720000, "status": "completed", "runtime_seconds": 7.8, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} diff --git a/tests/fixtures/journal/talents/20260309.jsonl b/tests/fixtures/journal/talents/20260309.jsonl new file mode 100644 index 000000000..7b099008d --- /dev/null +++ b/tests/fixtures/journal/talents/20260309.jsonl @@ -0,0 +1 @@ +{"use_id": "1773072000001", "name": "flow", "day": "20260309", "facet": null, "ts": 1773108000000, "status": "completed", "runtime_seconds": 5.3, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} diff --git a/tests/fixtures/journal/talents/20260310.jsonl b/tests/fixtures/journal/talents/20260310.jsonl new file mode 100644 index 000000000..98d12d0f7 --- /dev/null +++ b/tests/fixtures/journal/talents/20260310.jsonl @@ -0,0 +1,4 @@ +{"use_id": "1773158400001", "name": "flow", "day": "20260310", "facet": null, "ts": 1773194400000, "status": "completed", "runtime_seconds": 6.4, "provider": "google", "model": "gemini-2.5-flash", "schedule": "segment"} +{"use_id": "1773158400002", "name": "meetings", "day": "20260310", "facet": null, "ts": 1773194460000, "status": "completed", "runtime_seconds": 3.8, "provider": "google", "model": "gemini-2.5-flash", "schedule": "daily"} +{"use_id": "1773158400003", "name": "knowledge_graph", "day": "20260310", "facet": null, "ts": 1773194520000, "status": "completed", "runtime_seconds": 10.2, "provider": "anthropic", "model": "claude-sonnet-4-5", "schedule": "daily"} +{"use_id": "1773187200001", "name": "default", "day": "20260310", "facet": "verona", "ts": 1773187200000, "status": "completed", "runtime_seconds": 15.7, "provider": "openai", "model": "gpt-4o", "schedule": "segment"} diff --git a/tests/fixtures/journal/talents/default.log b/tests/fixtures/journal/talents/default.log new file mode 100644 index 000000000..c93129833 --- /dev/null +++ b/tests/fixtures/journal/talents/default.log @@ -0,0 +1,7 @@ +{"event": "request", "ts": 1700000000001, "use_id": "1700000000001", "prompt": "Search for meetings about project updates", "name": "default", "provider": "openai"} +{"event": "start", "prompt": "Search for meetings about project updates", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1700000000100, "use_id": "1700000000001"} +{"event": "talent_updated", "talent": "solstone", "ts": 1700000000200, "use_id": "1700000000001"} +{"event": "thinking", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "ts": 1700000000300, "use_id": "1700000000001"} +{"event": "tool_start", "tool": "search_events", "args": {"query": "project updates", "limit": 5}, "call_id": "call_001", "ts": 1700000000400, "use_id": "1700000000001"} +{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", "call_id": "call_001", "ts": 1700000000500, "use_id": "1700000000001"} +{"event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, "use_id": "1700000000001", "usage": {"input_tokens": 150, "output_tokens": 80}} diff --git a/tests/fixtures/journal/agents/default/1700000000001.jsonl b/tests/fixtures/journal/talents/default/1700000000001.jsonl similarity index 55% rename from tests/fixtures/journal/agents/default/1700000000001.jsonl rename to tests/fixtures/journal/talents/default/1700000000001.jsonl index c45cc665e..c93129833 100644 --- a/tests/fixtures/journal/agents/default/1700000000001.jsonl +++ b/tests/fixtures/journal/talents/default/1700000000001.jsonl @@ -1,7 +1,7 @@ -{"event": "request", "ts": 1700000000001, "agent_id": "1700000000001", "prompt": "Search for meetings about project updates", "name": "default", "provider": "openai"} -{"event": "start", "prompt": "Search for meetings about project updates", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1700000000100, "agent_id": "1700000000001"} -{"event": "agent_updated", "agent": "solstone", "ts": 1700000000200, "agent_id": "1700000000001"} -{"event": "thinking", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "ts": 1700000000300, "agent_id": "1700000000001"} -{"event": "tool_start", "tool": "search_events", "args": {"query": "project updates", "limit": 5}, "call_id": "call_001", "ts": 1700000000400, "agent_id": "1700000000001"} -{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", "call_id": "call_001", "ts": 1700000000500, "agent_id": "1700000000001"} -{"event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, "agent_id": "1700000000001", "usage": {"input_tokens": 150, "output_tokens": 80}} +{"event": "request", "ts": 1700000000001, "use_id": "1700000000001", "prompt": "Search for meetings about project updates", "name": "default", "provider": "openai"} +{"event": "start", "prompt": "Search for meetings about project updates", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1700000000100, "use_id": "1700000000001"} +{"event": "talent_updated", "talent": "solstone", "ts": 1700000000200, "use_id": "1700000000001"} +{"event": "thinking", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "ts": 1700000000300, "use_id": "1700000000001"} +{"event": "tool_start", "tool": "search_events", "args": {"query": "project updates", "limit": 5}, "call_id": "call_001", "ts": 1700000000400, "use_id": "1700000000001"} +{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", "call_id": "call_001", "ts": 1700000000500, "use_id": "1700000000001"} +{"event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, "use_id": "1700000000001", "usage": {"input_tokens": 150, "output_tokens": 80}} diff --git a/tests/fixtures/journal/agents/default/1772737200001.jsonl b/tests/fixtures/journal/talents/default/1772737200001.jsonl similarity index 62% rename from tests/fixtures/journal/agents/default/1772737200001.jsonl rename to tests/fixtures/journal/talents/default/1772737200001.jsonl index aaed98e5e..3129eec87 100644 --- a/tests/fixtures/journal/agents/default/1772737200001.jsonl +++ b/tests/fixtures/journal/talents/default/1772737200001.jsonl @@ -1,7 +1,7 @@ -{"event": "request", "ts": 1772737200000, "agent_id": "1772737200001", "prompt": "What do you think about collaborating with Capulet Industries on a unified API gateway?", "name": "default", "provider": "openai"} -{"event": "start", "prompt": "What do you think about collaborating with Capulet Industries on a unified API gateway?", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1772737200100, "agent_id": "1772737200001"} -{"event": "agent_updated", "agent": "solstone", "ts": 1772737200200, "agent_id": "1772737200001"} -{"event": "thinking", "content": "The user is asking about a potential collaboration between competing companies. I should consider the technical merits and business implications.", "ts": 1772737200300, "agent_id": "1772737200001"} -{"event": "tool_start", "tool": "search_entities", "args": {"query": "Capulet Industries API", "limit": 5}, "call_id": "call_rj001", "ts": 1772737200400, "agent_id": "1772737200001"} -{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 1, \"results\": [{\"name\": \"Capulet Industries\", \"type\": \"Company\", \"description\": \"Enterprise software company\"}]}", "call_id": "call_rj001", "ts": 1772737200500, "agent_id": "1772737200001"} -{"event": "finish", "result": "A collaboration between Montague Tech and Capulet Industries on a unified API gateway could be transformative. Their enterprise schema translation combined with your mesh routing would create a product neither could build alone. However, the business politics will be the real challenge. I suggest starting with a proof of concept before approaching either board.", "ts": 1772737200600, "agent_id": "1772737200001", "usage": {"input_tokens": 245, "output_tokens": 156}} +{"event": "request", "ts": 1772737200000, "use_id": "1772737200001", "prompt": "What do you think about collaborating with Capulet Industries on a unified API gateway?", "name": "default", "provider": "openai"} +{"event": "start", "prompt": "What do you think about collaborating with Capulet Industries on a unified API gateway?", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1772737200100, "use_id": "1772737200001"} +{"event": "talent_updated", "talent": "solstone", "ts": 1772737200200, "use_id": "1772737200001"} +{"event": "thinking", "content": "The user is asking about a potential collaboration between competing companies. I should consider the technical merits and business implications.", "ts": 1772737200300, "use_id": "1772737200001"} +{"event": "tool_start", "tool": "search_entities", "args": {"query": "Capulet Industries API", "limit": 5}, "call_id": "call_rj001", "ts": 1772737200400, "use_id": "1772737200001"} +{"event": "tool_end", "tool": "tool", "args": null, "result": "{\"total\": 1, \"results\": [{\"name\": \"Capulet Industries\", \"type\": \"Company\", \"description\": \"Enterprise software company\"}]}", "call_id": "call_rj001", "ts": 1772737200500, "use_id": "1772737200001"} +{"event": "finish", "result": "A collaboration between Montague Tech and Capulet Industries on a unified API gateway could be transformative. Their enterprise schema translation combined with your mesh routing would create a product neither could build alone. However, the business politics will be the real challenge. I suggest starting with a proof of concept before approaching either board.", "ts": 1772737200600, "use_id": "1772737200001", "usage": {"input_tokens": 245, "output_tokens": 156}} diff --git a/tests/fixtures/journal/agents/default/1773187200001.jsonl b/tests/fixtures/journal/talents/default/1773187200001.jsonl similarity index 64% rename from tests/fixtures/journal/agents/default/1773187200001.jsonl rename to tests/fixtures/journal/talents/default/1773187200001.jsonl index 29f3b5494..360a6c0e5 100644 --- a/tests/fixtures/journal/agents/default/1773187200001.jsonl +++ b/tests/fixtures/journal/talents/default/1773187200001.jsonl @@ -1,5 +1,5 @@ -{"event": "request", "ts": 1773187200000, "agent_id": "1773187200001", "prompt": "We just got board approval for the Verona Platform joint venture! What should our first priorities be?", "name": "default", "provider": "openai"} -{"event": "start", "prompt": "We just got board approval for the Verona Platform joint venture! What should our first priorities be?", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1773187200100, "agent_id": "1773187200001"} -{"event": "agent_updated", "agent": "solstone", "ts": 1773187200200, "agent_id": "1773187200001"} -{"event": "thinking", "content": "The joint venture has been approved. I should recommend practical next steps for launching the Verona Platform as a formal business unit.", "ts": 1773187200300, "agent_id": "1773187200001"} -{"event": "finish", "result": "Congratulations on the approval! Here are your first priorities:\n\n1. **Security Audit** - Mercutio should start with a comprehensive security review of the platform\n2. **Legal Framework** - Formalize IP sharing agreements between both companies\n3. **Team Building** - Recruit cross-company engineering team members\n4. **Client Pilot** - Identify 2-3 enterprise clients for a pilot program\n5. **Infrastructure** - Migrate from staging to production-grade infrastructure\n\nThe demo showed the technical viability. Now focus on making it production-ready.", "ts": 1773187200600, "agent_id": "1773187200001", "usage": {"input_tokens": 312, "output_tokens": 198}} +{"event": "request", "ts": 1773187200000, "use_id": "1773187200001", "prompt": "We just got board approval for the Verona Platform joint venture! What should our first priorities be?", "name": "default", "provider": "openai"} +{"event": "start", "prompt": "We just got board approval for the Verona Platform joint venture! What should our first priorities be?", "name": "default", "model": "gpt-4o", "provider": "openai", "ts": 1773187200100, "use_id": "1773187200001"} +{"event": "talent_updated", "talent": "solstone", "ts": 1773187200200, "use_id": "1773187200001"} +{"event": "thinking", "content": "The joint venture has been approved. I should recommend practical next steps for launching the Verona Platform as a formal business unit.", "ts": 1773187200300, "use_id": "1773187200001"} +{"event": "finish", "result": "Congratulations on the approval! Here are your first priorities:\n\n1. **Security Audit** - Mercutio should start with a comprehensive security review of the platform\n2. **Legal Framework** - Formalize IP sharing agreements between both companies\n3. **Team Building** - Recruit cross-company engineering team members\n4. **Client Pilot** - Identify 2-3 enterprise clients for a pilot program\n5. **Infrastructure** - Migrate from staging to production-grade infrastructure\n\nThe demo showed the technical viability. Now focus on making it production-ready.", "ts": 1773187200600, "use_id": "1773187200001", "usage": {"input_tokens": 312, "output_tokens": 198}} diff --git a/tests/fixtures/journal/talents/flow/1700000000002.jsonl b/tests/fixtures/journal/talents/flow/1700000000002.jsonl new file mode 100644 index 000000000..36c1fea97 --- /dev/null +++ b/tests/fixtures/journal/talents/flow/1700000000002.jsonl @@ -0,0 +1,3 @@ +{"event": "request", "ts": 1700000060000, "use_id": "1700000000002", "prompt": "Analyze conversation flow", "name": "flow", "provider": "anthropic"} +{"event": "start", "prompt": "Analyze conversation flow", "name": "flow", "model": "claude-3-haiku", "provider": "anthropic", "ts": 1700000060100, "use_id": "1700000000002"} +{"event": "error", "ts": 1700000060200, "use_id": "1700000000002", "error": "Rate limit exceeded: too many requests"} diff --git a/tests/integration/test_anthropic_provider.py b/tests/integration/test_anthropic_provider.py index b93801af8..ace5e8bcb 100644 --- a/tests/integration/test_anthropic_provider.py +++ b/tests/integration/test_anthropic_provider.py @@ -62,8 +62,8 @@ def test_anthropic_provider_basic(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, @@ -170,8 +170,8 @@ def test_anthropic_provider_with_thinking(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, diff --git a/tests/integration/test_callosum.py b/tests/integration/test_callosum.py index 840bae296..cb820c7dc 100644 --- a/tests/integration/test_callosum.py +++ b/tests/integration/test_callosum.py @@ -152,7 +152,7 @@ def test_multiple_clients_broadcast(callosum_server): # Wait for client to connect time.sleep(0.2) - client.emit("cortex", "agent_start", agent_id="123", name="analyst") + client.emit("cortex", "agent_start", use_id="123", name="analyst") # Wait for broadcast time.sleep(0.2) @@ -171,7 +171,7 @@ def test_multiple_clients_broadcast(callosum_server): msg = received[0] assert msg["tract"] == "cortex" assert msg["event"] == "agent_start" - assert msg["agent_id"] == "123" + assert msg["use_id"] == "123" assert msg["name"] == "analyst" # Cleanup diff --git a/tests/integration/test_cortex.py b/tests/integration/test_cortex.py index eef9a809e..bad3efacb 100644 --- a/tests/integration/test_cortex.py +++ b/tests/integration/test_cortex.py @@ -12,7 +12,7 @@ import pytest from think.callosum import CallosumServer from think.cortex import CortexService -from think.cortex_client import cortex_agents, cortex_request +from think.cortex_client import cortex_request, cortex_uses from think.utils import now_ms @@ -47,13 +47,13 @@ def test_cortex_service_startup(integration_journal_path, callosum_server): cortex = CortexService(journal_path=str(integration_journal_path)) # Verify agents directory was created - agents_dir = integration_journal_path / "agents" + agents_dir = integration_journal_path / "talents" assert agents_dir.exists() assert agents_dir.is_dir() # Verify service initializes correctly status = cortex.get_status() - assert status["running_agents"] == 0 + assert status["running_uses"] == 0 assert status["agent_ids"] == [] @@ -75,7 +75,7 @@ def test_cortex_request_creation(integration_journal_path, callosum_server): time.sleep(0.1) # Create a request - agent_id = cortex_request(prompt="Test prompt", name="default", provider="openai") + use_id = cortex_request(prompt="Test prompt", name="default", provider="openai") time.sleep(0.2) @@ -85,7 +85,7 @@ def test_cortex_request_creation(integration_journal_path, callosum_server): assert request["prompt"] == "Test prompt" assert request["name"] == "default" assert request["provider"] == "openai" - assert request["agent_id"] == agent_id + assert request["use_id"] == use_id listener.stop() @@ -96,7 +96,7 @@ def test_cortex_end_to_end_with_echo_agent(integration_journal_path, callosum_se os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(integration_journal_path) # Create a mock agent script that just echoes - agents_dir = integration_journal_path / "agents" + agents_dir = integration_journal_path / "talents" agents_dir.mkdir(parents=True, exist_ok=True) # Start Cortex service in background @@ -124,9 +124,7 @@ def test_cortex_end_to_end_with_echo_agent(integration_journal_path, callosum_se time.sleep(0.2) # Make a request (this will fail because no real agent, but we can verify the flow) - agent_id = cortex_request( - prompt="Test end-to-end", name="default", provider="openai" - ) + use_id = cortex_request(prompt="Test end-to-end", name="default", provider="openai") # Wait for at least request event time.sleep(1.0) @@ -134,23 +132,23 @@ def test_cortex_end_to_end_with_echo_agent(integration_journal_path, callosum_se # Should have received the request event request_events = [e for e in received_events if e.get("event") == "request"] assert len(request_events) >= 1 - assert request_events[0]["agent_id"] == agent_id + assert request_events[0]["use_id"] == use_id watcher.stop() cortex.stop() @pytest.mark.integration -def test_cortex_agents_listing(integration_journal_path): - """Test listing agents from the cortex_agents function.""" +def test_cortex_uses_listing(integration_journal_path): + """Test listing agents from the cortex_uses function.""" os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(integration_journal_path) # Create some test agent files - agents_dir = integration_journal_path / "agents" + agents_dir = integration_journal_path / "talents" agents_dir.mkdir(parents=True, exist_ok=True) # Get initial count - initial_result = cortex_agents() + initial_result = cortex_uses() initial_count = len(initial_result["agents"]) ts = now_ms() @@ -175,7 +173,7 @@ def test_cortex_agents_listing(integration_journal_path): f.write("\n") # List agents - result = cortex_agents() + result = cortex_uses() # Should have one more than before assert len(result["agents"]) == initial_count + 1 diff --git a/tests/integration/test_google_provider.py b/tests/integration/test_google_provider.py index 1c85998d6..b25fee0a6 100644 --- a/tests/integration/test_google_provider.py +++ b/tests/integration/test_google_provider.py @@ -62,8 +62,8 @@ def test_google_provider_basic(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, @@ -154,8 +154,8 @@ def test_google_provider_with_thinking(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, diff --git a/tests/integration/test_openai_provider.py b/tests/integration/test_openai_provider.py index d127b93ec..002b5296a 100644 --- a/tests/integration/test_openai_provider.py +++ b/tests/integration/test_openai_provider.py @@ -62,8 +62,8 @@ def test_openai_provider_basic(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, @@ -162,8 +162,8 @@ def test_openai_provider_with_reasoning(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, @@ -236,7 +236,7 @@ def test_openai_provider_with_extra_context(): env["_SOLSTONE_JOURNAL_OVERRIDE"] = journal_path env["OPENAI_API_KEY"] = api_key - # Include extra_context like get_agent() does in production + # Include extra_context like get_talent() does in production # This exercises the _convert_turns_to_items() code path ndjson_input = json.dumps( { @@ -249,8 +249,8 @@ def test_openai_provider_with_extra_context(): } ) - # Run the sol agents command - cmd = ["sol", "agents"] + # Run the sol think.talents command + cmd = ["sol", "providers", "check"] result = subprocess.run( cmd, env=env, diff --git a/tests/test_activities.py b/tests/test_activities.py index 601dae860..e084d037c 100644 --- a/tests/test_activities.py +++ b/tests/test_activities.py @@ -638,11 +638,11 @@ class TestActivityRecordIO: def _setup_segment(tmpdir, day, segment, facet, state): """Helper to create an activity_state.json file in a segment.""" - agents_dir = ( - Path(tmpdir) / "chronicle" / day / "default" / segment / "agents" / facet + talents_dir = ( + Path(tmpdir) / "chronicle" / day / "default" / segment / "talents" / facet ) - agents_dir.mkdir(parents=True, exist_ok=True) - state_file = agents_dir / "activity_state.json" + talents_dir.mkdir(parents=True, exist_ok=True) + state_file = talents_dir / "activity_state.json" state_file.write_text(json.dumps(state)) diff --git a/tests/test_activity_state.py b/tests/test_activity_state.py index 1eb56ff3d..bb49220ab 100644 --- a/tests/test_activity_state.py +++ b/tests/test_activity_state.py @@ -15,13 +15,13 @@ class TestExtractFacetFromOutputPath: def test_extracts_facet_from_valid_path(self): from talent.activity_state import _extract_facet_from_output_path - path = "/journal/20260130/143000_300/agents/work/activity_state.json" + path = "/journal/20260130/143000_300/talents/work/activity_state.json" assert _extract_facet_from_output_path(path) == "work" def test_extracts_facet_with_hyphen(self): from talent.activity_state import _extract_facet_from_output_path - path = "/journal/20260130/143000_300/agents/my-project/activity_state.json" + path = "/journal/20260130/143000_300/talents/my-project/activity_state.json" assert _extract_facet_from_output_path(path) == "my-project" def test_returns_none_for_empty_path(self): @@ -37,7 +37,7 @@ class TestExtractFacetFromOutputPath: assert _extract_facet_from_output_path("/path/to/facets.json") is None # No facet directory assert ( - _extract_facet_from_output_path("/path/to/agents/activity_state.json") + _extract_facet_from_output_path("/path/to/talents/activity_state.json") is None ) @@ -146,7 +146,7 @@ class TestLoadPreviousState: Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" ) segment_dir.mkdir(parents=True) - (segment_dir / "agents" / "work").mkdir(parents=True) + (segment_dir / "talents" / "work").mkdir(parents=True) state = [ { @@ -157,7 +157,7 @@ class TestLoadPreviousState: "level": "high", } ] - (segment_dir / "agents/work/activity_state.json").write_text( + (segment_dir / "talents/work/activity_state.json").write_text( json.dumps(state) ) @@ -184,7 +184,7 @@ class TestLoadPreviousState: Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" ) segment_dir.mkdir(parents=True) - (segment_dir / "agents" / "work").mkdir(parents=True) + (segment_dir / "talents" / "work").mkdir(parents=True) loaded, segment = load_previous_state( "20260130", "100000_300", "work", stream="default" @@ -208,10 +208,10 @@ class TestLoadPreviousState: Path(tmpdir) / "chronicle" / "20260130" / "default" / "100000_300" ) segment_dir.mkdir(parents=True) - (segment_dir / "agents" / "work").mkdir(parents=True) + (segment_dir / "talents" / "work").mkdir(parents=True) # Write a dict (old format) — should be rejected - (segment_dir / "agents/work/activity_state.json").write_text( + (segment_dir / "talents/work/activity_state.json").write_text( '{"active": [], "ended": []}' ) @@ -364,7 +364,7 @@ class TestPreProcess: day_dir = Path(tmpdir) / "chronicle" / "20260130" day_dir.mkdir(parents=True) (day_dir / "default" / "100000_300").mkdir(parents=True) - (day_dir / "default" / "100000_300" / "agents" / "work").mkdir( + (day_dir / "default" / "100000_300" / "talents" / "work").mkdir( parents=True ) segment_dir = day_dir / "default" / "110000_300" @@ -391,14 +391,14 @@ class TestPreProcess: day_dir / "default" / "100000_300" - / "agents/work/activity_state.json" + / "talents/work/activity_state.json" ).write_text(json.dumps(prev_state)) context = { "day": "20260130", "segment": "110000_300", "stream": "default", - "output_path": "/journal/20260130/110000_300/agents/work/activity_state.json", + "output_path": "/journal/20260130/110000_300/talents/work/activity_state.json", "transcript": "User is typing code...", "meta": {}, } @@ -424,7 +424,7 @@ class TestPreProcess: context = { "segment": "100000_300", - "output_path": "/path/to/agents/work/activity_state.json", + "output_path": "/path/to/talents/work/activity_state.json", } assert pre_process(context) is None @@ -433,7 +433,7 @@ class TestPreProcess: context = { "day": "20260130", - "output_path": "/path/to/agents/work/activity_state.json", + "output_path": "/path/to/talents/work/activity_state.json", } assert pre_process(context) is None @@ -487,7 +487,7 @@ class TestPostProcess: # Previous segment with active meeting prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -497,7 +497,7 @@ class TestPostProcess: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -519,7 +519,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -545,7 +545,7 @@ class TestPostProcess: # Previous segment with active meeting prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -555,7 +555,7 @@ class TestPostProcess: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -575,7 +575,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -669,7 +669,7 @@ class TestPostProcess: # Previous segment — email already ended prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "email", @@ -678,7 +678,7 @@ class TestPostProcess: "description": "Replied to boss", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -698,7 +698,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -725,7 +725,7 @@ class TestPostProcess: # Previous segment — email ended with different description prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "email", @@ -734,7 +734,7 @@ class TestPostProcess: "description": "Replied to boss", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -754,7 +754,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -806,7 +806,7 @@ class TestPostProcess: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -816,7 +816,7 @@ class TestPostProcess: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -842,7 +842,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -927,7 +927,7 @@ class TestPostProcess: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -937,7 +937,7 @@ class TestPostProcess: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -958,7 +958,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -984,7 +984,7 @@ class TestPostProcess: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -1001,7 +1001,7 @@ class TestPostProcess: "level": "medium", }, ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1022,7 +1022,7 @@ class TestPostProcess: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -1069,7 +1069,7 @@ class TestActivityId: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -1079,7 +1079,7 @@ class TestActivityId: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1100,7 +1100,7 @@ class TestActivityId: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -1124,7 +1124,7 @@ class TestActivityId: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -1134,7 +1134,7 @@ class TestActivityId: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1154,7 +1154,7 @@ class TestActivityId: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } result = post_process(llm_output, context) @@ -1208,7 +1208,7 @@ class TestActivityLiveEvents: context = { "day": "20260130", "segment": "143000_300", - "output_path": "/j/20260130/143000_300/agents/work/activity_state.json", + "output_path": "/j/20260130/143000_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send") as mock_send: @@ -1244,7 +1244,7 @@ class TestActivityLiveEvents: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "coding", @@ -1254,7 +1254,7 @@ class TestActivityLiveEvents: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1275,7 +1275,7 @@ class TestActivityLiveEvents: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send") as mock_send: @@ -1306,7 +1306,7 @@ class TestActivityLiveEvents: prev_dir = day_dir / "default" / "100000_300" prev_dir.mkdir(parents=True) - (prev_dir / "agents" / "work").mkdir(parents=True) + (prev_dir / "talents" / "work").mkdir(parents=True) prev_state = [ { "activity": "meeting", @@ -1316,7 +1316,7 @@ class TestActivityLiveEvents: "level": "high", } ] - (prev_dir / "agents/work/activity_state.json").write_text( + (prev_dir / "talents/work/activity_state.json").write_text( json.dumps(prev_state) ) @@ -1336,7 +1336,7 @@ class TestActivityLiveEvents: "day": "20260130", "segment": "100500_300", "stream": "default", - "output_path": f"{tmpdir}/20260130/100500_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/100500_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send") as mock_send: @@ -1387,7 +1387,7 @@ class TestActivityLiveEvents: context = { "day": "20260130", "segment": "143000_300", - "output_path": "/j/20260130/143000_300/agents/work/activity_state.json", + "output_path": "/j/20260130/143000_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send") as mock_send: @@ -1440,7 +1440,7 @@ class TestActivityIdValidation: context = { "segment": "143000_300", - "output_path": f"{tmpdir}/20260130/143000_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/143000_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send"): @@ -1483,7 +1483,7 @@ class TestActivityIdValidation: context = { "segment": "143000_300", - "output_path": f"{tmpdir}/20260130/143000_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/143000_300/talents/work/activity_state.json", } with caplog.at_level(logging.WARNING, logger="talent.activity_state"): @@ -1539,7 +1539,7 @@ class TestActivityIdValidation: context = { "segment": "143000_300", - "output_path": f"{tmpdir}/20260130/143000_300/agents/work/activity_state.json", + "output_path": f"{tmpdir}/20260130/143000_300/talents/work/activity_state.json", } with patch("talent.activity_state.callosum_send"): @@ -1589,7 +1589,7 @@ class TestActivityIdValidation: context = { "segment": "143000_300", - "output_path": f"{tmpdir}/20260130/143000_300/agents/new_facet/activity_state.json", + "output_path": f"{tmpdir}/20260130/143000_300/talents/new_facet/activity_state.json", } with patch("talent.activity_state.callosum_send"): diff --git a/tests/test_anthropic.py b/tests/test_anthropic.py index 0f2c4979d..e7d519b82 100644 --- a/tests/test_anthropic.py +++ b/tests/test_anthropic.py @@ -203,11 +203,11 @@ def test_claude_main(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -221,7 +221,7 @@ def test_claude_main(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] @@ -246,11 +246,11 @@ def test_claude_outfile(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -264,7 +264,7 @@ def test_claude_outfile(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) # Output file functionality was removed in NDJSON-only mode # Check stdout instead @@ -293,11 +293,11 @@ def test_claude_thinking_events(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod, with_thinking=True) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -311,7 +311,7 @@ def test_claude_thinking_events(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] @@ -335,11 +335,11 @@ def test_claude_redacted_thinking_events(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod, with_redacted_thinking=True) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -353,7 +353,7 @@ def test_claude_redacted_thinking_events(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] @@ -375,11 +375,11 @@ def test_claude_outfile_error(monkeypatch, tmp_path, capsys): importlib.import_module("think.providers.anthropic") ) _setup_claude_cli_stub(monkeypatch, provider_mod, error=True) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() - agents_dir = journal / "agents" + agents_dir = journal / "talents" agents_dir.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -393,7 +393,7 @@ def test_claude_outfile_error(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) # Error events should be written to stdout out_lines = capsys.readouterr().out.strip().splitlines() diff --git a/tests/test_app_calendar.py b/tests/test_app_calendar.py index f9d584845..724c5db66 100644 --- a/tests/test_app_calendar.py +++ b/tests/test_app_calendar.py @@ -134,7 +134,7 @@ class TestCalendarActivityOutput: def test_rejects_non_facets_path(self, calendar_client): """Paths not starting with facets/ are rejected.""" resp = calendar_client.get( - "/app/calendar/api/activity_output/20260214/agents/flow.md" + "/app/calendar/api/activity_output/20260214/talents/flow.md" ) assert resp.status_code == 400 diff --git a/tests/test_app_sol.py b/tests/test_app_sol.py index c4f991ef0..aaff47c78 100644 --- a/tests/test_app_sol.py +++ b/tests/test_app_sol.py @@ -10,7 +10,7 @@ from pathlib import Path import pytest from apps.sol.routes import _resolve_output_path -from think.talent import _resolve_agent_path, get_agent, get_talent_configs +from think.talent import _resolve_talent_path, get_talent, get_talent_configs @pytest.fixture @@ -69,16 +69,16 @@ def app_with_agent(tmp_path, monkeypatch): def test_resolve_agent_path_system_agent(): - """Test _resolve_agent_path returns correct path for system agents.""" - agent_dir, agent_name = _resolve_agent_path("unified") + """Test _resolve_talent_path returns correct path for system agents.""" + agent_dir, agent_name = _resolve_talent_path("unified") assert agent_name == "chat" assert agent_dir.name == "talent" def test_resolve_agent_path_app_agent(): - """Test _resolve_agent_path returns correct path for app agents.""" - agent_dir, agent_name = _resolve_agent_path("support:support") + """Test _resolve_talent_path returns correct path for app agents.""" + agent_dir, agent_name = _resolve_talent_path("support:support") assert agent_name == "support" assert agent_dir.name == "talent" @@ -87,16 +87,16 @@ def test_resolve_agent_path_app_agent(): def test_resolve_agent_path_app_agent_with_underscores(): - """Test _resolve_agent_path handles app names with underscores.""" - agent_dir, agent_name = _resolve_agent_path("my_app:my_agent") + """Test _resolve_talent_path handles app names with underscores.""" + agent_dir, agent_name = _resolve_talent_path("my_app:my_agent") assert agent_name == "my_agent" assert agent_dir.parent.name == "my_app" def test_get_agent_system_agent(fixture_journal): - """Test get_agent loads system agents correctly.""" - config = get_agent("unified") + """Test get_talent loads system agents correctly.""" + config = get_talent("unified") assert config["name"] == "unified" assert "user_instruction" in config @@ -104,17 +104,17 @@ def test_get_agent_system_agent(fixture_journal): def test_get_agent_nonexistent_raises(): - """Test get_agent raises FileNotFoundError for nonexistent agents.""" + """Test get_talent raises FileNotFoundError for nonexistent agents.""" with pytest.raises(FileNotFoundError) as exc_info: - get_agent("nonexistent_agent_xyz") + get_talent("nonexistent_agent_xyz") assert "nonexistent_agent_xyz" in str(exc_info.value) def test_get_agent_nonexistent_app_agent_raises(): - """Test get_agent raises FileNotFoundError for nonexistent app agents.""" + """Test get_talent raises FileNotFoundError for nonexistent app agents.""" with pytest.raises(FileNotFoundError) as exc_info: - get_agent("fakeapp:fakeagent") + get_talent("fakeapp:fakeagent") assert "fakeapp:fakeagent" in str(exc_info.value) @@ -147,7 +147,7 @@ def test_get_talent_configs_excludes_private_apps( ): """Test get_talent_configs skips apps starting with underscore.""" # Create a private app with an agent - private_app = tmp_path / "_private_app" / "agents" + private_app = tmp_path / "_private_app" / "talents" private_app.mkdir(parents=True) (private_app / "secret.md").write_text("Secret agent") @@ -258,8 +258,8 @@ def agents_client(tmp_path): # Create test files day_dir = tmp_path / "chronicle" / "20260214" day_dir.mkdir(parents=True) - (day_dir / "agents" / "flow.md").parent.mkdir(parents=True) - (day_dir / "agents" / "flow.md").write_text("# Day agent output") + (day_dir / "talents" / "flow.md").parent.mkdir(parents=True) + (day_dir / "talents" / "flow.md").write_text("# Day agent output") facet_dir = tmp_path / "facets" / "work" / "activities" / "20260214" / "coding_100" facet_dir.mkdir(parents=True) @@ -273,7 +273,7 @@ class TestApiOutputFile: def test_serves_day_relative_file(self, agents_client): """Day-relative paths resolve under {journal}/{day}/.""" - resp = agents_client.get("/app/sol/api/output/20260214/agents/flow.md") + resp = agents_client.get("/app/sol/api/output/20260214/talents/flow.md") assert resp.status_code == 200 data = resp.get_json() assert data["content"] == "# Day agent output" @@ -293,7 +293,7 @@ class TestApiOutputFile: def test_rejects_invalid_day_format(self, agents_client): """Non-YYYYMMDD day returns 400.""" - resp = agents_client.get("/app/sol/api/output/bad-day/agents/flow.md") + resp = agents_client.get("/app/sol/api/output/bad-day/talents/flow.md") assert resp.status_code == 400 def test_rejects_path_traversal(self, agents_client): @@ -303,5 +303,5 @@ class TestApiOutputFile: def test_missing_file_returns_404(self, agents_client): """Non-existent file returns 404.""" - resp = agents_client.get("/app/sol/api/output/20260214/agents/nonexistent.md") + resp = agents_client.get("/app/sol/api/output/20260214/talents/nonexistent.md") assert resp.status_code == 404 diff --git a/tests/test_cluster.py b/tests/test_cluster.py index ddf1e83c1..e048b7fda 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -20,8 +20,8 @@ def test_cluster(tmp_path, monkeypatch): '{}\n{"text": "hi"}\n' ) (day_dir / "default" / "120500_300").mkdir(parents=True) - (day_dir / "default" / "120500_300" / "agents").mkdir() - (day_dir / "default" / "120500_300" / "agents" / "screen.md").write_text( + (day_dir / "default" / "120500_300" / "talents").mkdir() + (day_dir / "default" / "120500_300" / "talents" / "screen.md").write_text( "screen summary" ) result, counts = mod.cluster( @@ -46,8 +46,8 @@ def test_cluster_range(tmp_path, monkeypatch): '{"raw": "raw.flac", "model": "whisper-1"}\n' '{"start": "00:00:01", "source": "mic", "text": "hi from audio"}\n' ) - (day_dir / "default" / "120000_300" / "agents").mkdir() - (day_dir / "default" / "120000_300" / "agents" / "screen.md").write_text( + (day_dir / "default" / "120000_300" / "talents").mkdir() + (day_dir / "default" / "120000_300" / "talents" / "screen.md").write_text( "screen summary content" ) # Test with agents=True to include *.md files @@ -170,8 +170,8 @@ def test_cluster_period_uses_raw_screen(tmp_path, monkeypatch): '"visual_description": "VS Code with Python file"}}\n' ) # Also create screen.md (insight) to verify it's NOT used by cluster_period - (segment / "agents").mkdir() - (segment / "agents" / "screen.md").write_text("This insight should NOT appear") + (segment / "talents").mkdir() + (segment / "talents" / "screen.md").write_text("This insight should NOT appear") result, counts = mod.cluster_period( "20240101", @@ -220,12 +220,12 @@ def test_cluster_range_with_agents(tmp_path, monkeypatch): # Create segment with multiple insight files segment = day_dir / "default" / "100000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text( '{"raw": "audio.flac"}\n{"start": "00:00:01", "text": "hello"}\n' ) - (segment / "agents" / "screen.md").write_text("Screen activity summary") - (segment / "agents" / "activity.md").write_text("Activity insight content") + (segment / "talents" / "screen.md").write_text("Screen activity summary") + (segment / "talents" / "activity.md").write_text("Activity insight content") # Also create screen.jsonl to verify it's NOT used when agents=True, screen=False (segment / "screen.jsonl").write_text( '{"raw": "screen.webm"}\n' @@ -260,12 +260,12 @@ def test_cluster_range_with_screen(tmp_path, monkeypatch): # Create segment with raw screen data and insight file segment = day_dir / "default" / "100000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "screen.jsonl").write_text( '{"raw": "screen.webm"}\n' '{"timestamp": 10, "analysis": {"primary": "code_editor"}}\n' ) - (segment / "agents" / "screen.md").write_text("Screen summary insight") + (segment / "talents" / "screen.md").write_text("Screen summary insight") # Test screen=True returns raw screen data, not agent outputs result = mod.cluster_range( @@ -434,11 +434,11 @@ def test_cluster_with_agent_filter_dict(tmp_path, monkeypatch): # Create segment with multiple agent output files segment = day_dir / "default" / "120000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') - (segment / "agents" / "entities.md").write_text("Entity extraction results") - (segment / "agents" / "meetings.md").write_text("Meeting summary results") - (segment / "agents" / "flow.md").write_text("Flow analysis results") + (segment / "talents" / "entities.md").write_text("Entity extraction results") + (segment / "talents" / "meetings.md").write_text("Meeting summary results") + (segment / "talents" / "flow.md").write_text("Flow analysis results") # Test filtering to only include entities result, counts = mod.cluster( @@ -463,11 +463,11 @@ def test_cluster_with_agent_filter_multiple(tmp_path, monkeypatch): # Create segment with multiple agent output files segment = day_dir / "default" / "120000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') - (segment / "agents" / "entities.md").write_text("Entity extraction results") - (segment / "agents" / "meetings.md").write_text("Meeting summary results") - (segment / "agents" / "flow.md").write_text("Flow analysis results") + (segment / "talents" / "entities.md").write_text("Entity extraction results") + (segment / "talents" / "meetings.md").write_text("Meeting summary results") + (segment / "talents" / "flow.md").write_text("Flow analysis results") # Test filtering to include entities and meetings but not flow result, counts = mod.cluster( @@ -497,10 +497,10 @@ def test_cluster_with_agent_filter_app_namespaced(tmp_path, monkeypatch): # App agent output naming: "app:agent" -> "_app_agent.md" segment = day_dir / "default" / "120000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') - (segment / "agents" / "entities.md").write_text("System entity results") - (segment / "agents" / "_todos_review.md").write_text("Todos review results") + (segment / "talents" / "entities.md").write_text("System entity results") + (segment / "talents" / "_todos_review.md").write_text("Todos review results") # Test filtering to include app-namespaced agent result, counts = mod.cluster( @@ -527,9 +527,9 @@ def test_cluster_with_empty_agent_filter(tmp_path, monkeypatch): segment = day_dir / "default" / "120000_300" segment.mkdir(parents=True) - (segment / "agents").mkdir() + (segment / "talents").mkdir() (segment / "audio.jsonl").write_text('{}\n{"text": "hello"}\n') - (segment / "agents" / "entities.md").write_text("Entity extraction results") + (segment / "talents" / "entities.md").write_text("Entity extraction results") # Empty dict should mean no agents result, counts = mod.cluster( diff --git a/tests/test_conversation.py b/tests/test_conversation.py index 104c3a76a..e09ccd558 100644 --- a/tests/test_conversation.py +++ b/tests/test_conversation.py @@ -37,7 +37,7 @@ def test_record_exchange_writes_jsonl(journal_dir): user_message="what's our history with adrian?", agent_response="You met Adrian at betaworks.", talent="unified", - agent_id="12345", + use_id="12345", ) jsonl_path = journal_dir / "conversation" / "exchanges.jsonl" @@ -54,7 +54,7 @@ def test_record_exchange_writes_jsonl(journal_dir): assert ex["user_message"] == "what's our history with adrian?" assert ex["agent_response"] == "You met Adrian at betaworks." assert ex["talent"] == "unified" - assert ex["agent_id"] == "12345" + assert ex["use_id"] == "12345" def test_record_exchange_writes_journal_segment(journal_dir): @@ -72,10 +72,10 @@ def test_record_exchange_writes_journal_segment(journal_dir): user_message="move my 3pm to 4pm", agent_response="Done — moved 'DVD sync' to 4pm.", talent="unified", - agent_id="67890", + use_id="67890", ) - # Check journal segment directory: YYYYMMDD/conversation/HHMMSS_1/agents/ + # Check journal segment directory: YYYYMMDD/conversation/HHMMSS_1/talents/ day = datetime.fromtimestamp(ts / 1000).strftime("%Y%m%d") time_key = datetime.fromtimestamp(ts / 1000).strftime("%H%M%S") md_path = ( @@ -84,7 +84,7 @@ def test_record_exchange_writes_journal_segment(journal_dir): / day / "conversation" / f"{time_key}_1" - / "agents" + / "talents" / "conversation.md" ) diff --git a/tests/test_convey_apps.py b/tests/test_convey_apps.py index ec89f482c..aa18a76fc 100644 --- a/tests/test_convey_apps.py +++ b/tests/test_convey_apps.py @@ -20,9 +20,9 @@ def _run_triage(): app = Flask(__name__) with ( patch("convey.utils.spawn_agent", return_value="agent-1") as mock_spawn, - patch("think.cortex_client.wait_for_agents", return_value=({}, [])), + patch("think.cortex_client.wait_for_uses", return_value=({}, [])), patch( - "think.cortex_client.read_agent_events", + "think.cortex_client.read_use_events", return_value=[{"event": "finish", "result": "ok"}], ), ): @@ -140,13 +140,13 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / "agents" + agents_dir = tmp_path / "talents" agents_dir.mkdir() day_index = agents_dir / f"{today}.jsonl" day_index.write_text( json.dumps( { - "agent_id": "1", + "use_id": "1", "name": "flow", "day": today, "ts": 1000, @@ -156,7 +156,7 @@ class TestAttentionResolution: + "\n" + json.dumps( { - "agent_id": "2", + "use_id": "2", "name": "meetings", "day": today, "ts": 1001, @@ -182,13 +182,13 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / "agents" + agents_dir = tmp_path / "talents" agents_dir.mkdir() day_index = agents_dir / f"{today}.jsonl" day_index.write_text( json.dumps( { - "agent_id": "1", + "use_id": "1", "name": "flow", "day": today, "ts": 1000, @@ -198,7 +198,7 @@ class TestAttentionResolution: + "\n" + json.dumps( { - "agent_id": "3", + "use_id": "3", "name": "flow", "day": today, "ts": 2000, @@ -221,13 +221,13 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / "agents" + agents_dir = tmp_path / "talents" agents_dir.mkdir() day_index = agents_dir / f"{today}.jsonl" day_index.write_text( json.dumps( { - "agent_id": "1", + "use_id": "1", "name": "flow", "day": today, "ts": 1000, @@ -266,11 +266,11 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / "agents" + agents_dir = tmp_path / "talents" agents_dir.mkdir() day_index = agents_dir / f"{today}.jsonl" day_index.write_text( - json.dumps({"agent_id": "1", "name": "flow", "ts": 1000, "status": "error"}) + json.dumps({"use_id": "1", "name": "flow", "ts": 1000, "status": "error"}) + "\n" ) result = _resolve_attention({}) @@ -299,7 +299,7 @@ class TestAttentionResolution: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) today = datetime.now().strftime("%Y%m%d") - agents_dir = tmp_path / today / "agents" + agents_dir = tmp_path / today / "talents" agents_dir.mkdir(parents=True) (agents_dir / "flow.md").write_text("# Flow") (agents_dir / "meetings.md").write_text("# Meetings") diff --git a/tests/test_cortex.py b/tests/test_cortex.py index 7d3abb61a..9ef3ea515 100644 --- a/tests/test_cortex.py +++ b/tests/test_cortex.py @@ -5,6 +5,7 @@ import json import os +import sys from pathlib import Path from unittest.mock import MagicMock, patch @@ -41,7 +42,7 @@ def mock_journal(tmp_path, monkeypatch): """Set up a temporary journal directory.""" journal_path = tmp_path / "journal" journal_path.mkdir() - agents_path = journal_path / "agents" + agents_path = journal_path / "talents" agents_path.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_path)) @@ -57,17 +58,17 @@ def cortex_service(mock_journal): def test_agent_process_creation(): - """Test AgentProcess class initialization and methods.""" - from think.cortex import AgentProcess + """Test TalentProcess class initialization and methods.""" + from think.cortex import TalentProcess mock_process = MagicMock() mock_process.poll.return_value = None # Running mock_process.pid = 12345 log_path = Path("/tmp/test.jsonl") - agent = AgentProcess("123456789", mock_process, log_path) + agent = TalentProcess("123456789", mock_process, log_path) - assert agent.agent_id == "123456789" + assert agent.use_id == "123456789" assert agent.process == mock_process assert agent.log_path == log_path assert agent.is_running() is True @@ -81,9 +82,9 @@ def test_agent_process_creation(): def test_cortex_service_initialization(cortex_service, mock_journal): """Test CortexService initialization.""" assert cortex_service.journal_path == mock_journal - assert cortex_service.agents_dir == mock_journal / "agents" - assert cortex_service.running_agents == {} - assert cortex_service.agents_dir.exists() + assert cortex_service.talents_dir == mock_journal / "talents" + assert cortex_service.running_uses == {} + assert cortex_service.talents_dir.exists() @patch("think.cortex.subprocess.Popen") @@ -105,8 +106,8 @@ def test_spawn_subprocess( mock_timer_instance = MagicMock() mock_timer.return_value = mock_timer_instance - agent_id = "123456789" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" request = { "event": "request", @@ -118,17 +119,17 @@ def test_spawn_subprocess( } cortex_service._spawn_subprocess( - agent_id, + use_id, file_path, request, - ["sol", "agents"], - "agent", + [sys.executable, "-m", "think.talents"], + "talent", ) # Check subprocess was called mock_popen.assert_called_once() call_args = mock_popen.call_args - assert call_args[0][0] == ["sol", "agents"] + assert call_args[0][0] == [sys.executable, "-m", "think.talents"] assert call_args[1]["stdin"] is not None assert call_args[1]["stdout"] is not None assert call_args[1]["stderr"] is not None @@ -147,9 +148,9 @@ def test_spawn_subprocess( mock_process.stdin.close.assert_called_once() # Check agent was tracked - assert agent_id in cortex_service.running_agents - agent = cortex_service.running_agents[agent_id] - assert agent.agent_id == agent_id + assert use_id in cortex_service.running_uses + agent = cortex_service.running_uses[use_id] + assert agent.use_id == use_id assert agent.log_path == file_path # Check monitoring threads were started @@ -179,8 +180,8 @@ def test_spawn_generator_via_subprocess( mock_timer_instance = MagicMock() mock_timer.return_value = mock_timer_instance - agent_id = "987654321" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "987654321" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" # Generator config has "output" instead of "tools" config = { @@ -193,17 +194,17 @@ def test_spawn_generator_via_subprocess( # Generators route through _spawn_subprocess cortex_service._spawn_subprocess( - agent_id, + use_id, file_path, config, - ["sol", "agents"], - "agent", + [sys.executable, "-m", "think.talents"], + "talent", ) # Check subprocess was called with agents command (generators route through agents) mock_popen.assert_called_once() call_args = mock_popen.call_args - assert call_args[0][0] == ["sol", "agents"] + assert call_args[0][0] == [sys.executable, "-m", "think.talents"] assert call_args[1]["stdin"] is not None assert call_args[1]["stdout"] is not None assert call_args[1]["stderr"] is not None @@ -221,9 +222,9 @@ def test_spawn_generator_via_subprocess( mock_process.stdin.close.assert_called_once() # Check generator was tracked - assert agent_id in cortex_service.running_agents - agent = cortex_service.running_agents[agent_id] - assert agent.agent_id == agent_id + assert use_id in cortex_service.running_uses + agent = cortex_service.running_uses[use_id] + assert agent.use_id == use_id assert agent.log_path == file_path # Check monitoring threads were started @@ -234,7 +235,7 @@ def test_spawn_generator_via_subprocess( mock_timer_instance.start.assert_called_once() -@patch("think.talent.get_agent") +@patch("think.talent.get_talent") @patch("think.cortex.subprocess.Popen") @patch("think.cortex.threading.Thread") @patch("think.cortex.threading.Timer") @@ -258,8 +259,8 @@ def test_spawn_subprocess_uses_cwd_from_talent( mock_timer_instance = MagicMock() mock_timer.return_value = mock_timer_instance - agent_id = "24680" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "24680" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" request = { "event": "request", "ts": 24680, @@ -270,17 +271,17 @@ def test_spawn_subprocess_uses_cwd_from_talent( } cortex_service._spawn_subprocess( - agent_id, + use_id, file_path, request, - ["sol", "agents"], - "agent", + [sys.executable, "-m", "think.talents"], + "talent", ) assert mock_popen.call_args.kwargs["cwd"] == str(mock_journal) -@patch("think.talent.get_agent") +@patch("think.talent.get_talent") @patch("think.cortex.subprocess.Popen") @patch("think.cortex.threading.Thread") @patch("think.cortex.threading.Timer") @@ -304,8 +305,8 @@ def test_spawn_subprocess_skips_cwd_for_generate( mock_timer_instance = MagicMock() mock_timer.return_value = mock_timer_instance - agent_id = "13579" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "13579" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" request = { "event": "request", "ts": 13579, @@ -315,11 +316,11 @@ def test_spawn_subprocess_skips_cwd_for_generate( } cortex_service._spawn_subprocess( - agent_id, + use_id, file_path, request, - ["sol", "agents"], - "agent", + [sys.executable, "-m", "think.talents"], + "talent", ) assert mock_popen.call_args.kwargs["cwd"] is None @@ -329,10 +330,10 @@ def test_monitor_stdout_json_events(cortex_service, mock_journal): """Test monitoring stdout with JSON events.""" from io import StringIO - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "123456789" - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" mock_process = MagicMock() mock_process.poll.return_value = 0 # Process exits @@ -341,10 +342,10 @@ def test_monitor_stdout_json_events(cortex_service, mock_journal): '{"event": "finish", "ts": 1234567891, "result": "Done"}\n' ) - agent = AgentProcess(agent_id, mock_process, log_path) - cortex_service.running_agents[agent_id] = agent + agent = TalentProcess(use_id, mock_process, log_path) + cortex_service.running_uses[use_id] = agent - with patch.object(cortex_service, "_complete_agent_file") as mock_complete: + with patch.object(cortex_service, "_complete_use_file") as mock_complete: cortex_service._monitor_stdout(agent) # Check events were written to file @@ -355,20 +356,20 @@ def test_monitor_stdout_json_events(cortex_service, mock_journal): assert json.loads(lines[1])["event"] == "finish" # Check file was completed - mock_complete.assert_called_once_with(agent_id, log_path) + mock_complete.assert_called_once_with(use_id, log_path) # Check agent was removed - assert agent_id not in cortex_service.running_agents + assert use_id not in cortex_service.running_uses def test_monitor_stdout_non_json_output(cortex_service, mock_journal): """Test monitoring stdout with non-JSON output.""" from io import StringIO - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "123456789" - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" mock_process = MagicMock() mock_process.poll.return_value = 0 @@ -376,10 +377,10 @@ def test_monitor_stdout_non_json_output(cortex_service, mock_journal): 'Plain text output\n{"event": "finish", "ts": 1234567890}\n' ) - agent = AgentProcess(agent_id, mock_process, log_path) - cortex_service.running_agents[agent_id] = agent + agent = TalentProcess(use_id, mock_process, log_path) + cortex_service.running_uses[use_id] = agent - with patch.object(cortex_service, "_complete_agent_file"): + with patch.object(cortex_service, "_complete_use_file"): cortex_service._monitor_stdout(agent) # Check info event was created for non-JSON @@ -396,19 +397,19 @@ def test_monitor_stdout_no_finish_event(cortex_service, mock_journal): """Test monitoring stdout when process exits without finish event.""" from io import StringIO - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "123456789" - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" mock_process = MagicMock() mock_process.wait.return_value = 1 # Non-zero exit mock_process.stdout = StringIO('{"event": "start", "ts": 1234567890}\n') - agent = AgentProcess(agent_id, mock_process, log_path) - cortex_service.running_agents[agent_id] = agent + agent = TalentProcess(use_id, mock_process, log_path) + cortex_service.running_uses[use_id] = agent - with patch.object(cortex_service, "_complete_agent_file"): + with patch.object(cortex_service, "_complete_use_file"): cortex_service._monitor_stdout(agent) # Check error event was added @@ -425,10 +426,10 @@ def test_monitor_stderr(cortex_service, mock_journal): """Test monitoring stderr for errors.""" from io import StringIO - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "123456789" - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + log_path = mock_journal / "talents" / f"{use_id}_active.jsonl" mock_process = MagicMock() mock_process.poll.return_value = 1 # Error exit @@ -436,7 +437,7 @@ def test_monitor_stderr(cortex_service, mock_journal): "Error: Something went wrong\nStack trace line 1\nStack trace line 2\n" ) - agent = AgentProcess(agent_id, mock_process, log_path) + agent = TalentProcess(use_id, mock_process, log_path) cortex_service._monitor_stderr(agent) @@ -454,7 +455,7 @@ def test_monitor_stderr(cortex_service, mock_journal): def test_has_finish_event(cortex_service, mock_journal): """Test checking for finish event in JSONL file.""" - file_path = mock_journal / "agents" / "test.jsonl" + file_path = mock_journal / "talents" / "test.jsonl" # File with finish event file_path.write_text( @@ -477,89 +478,89 @@ def test_has_finish_event(cortex_service, mock_journal): assert cortex_service._has_finish_event(file_path) is False -def test_complete_agent_file(cortex_service, mock_journal): +def test_complete_use_file(cortex_service, mock_journal): """Test completing an agent file (rename from active to completed).""" - agent_id = "123456789" - unified_dir = mock_journal / "agents" / "unified" + use_id = "123456789" + unified_dir = mock_journal / "talents" / "unified" unified_dir.mkdir() - active_path = unified_dir / f"{agent_id}_active.jsonl" + active_path = unified_dir / f"{use_id}_active.jsonl" active_path.touch() - cortex_service.agent_requests[agent_id] = {"name": "unified", "agent_id": agent_id} + cortex_service.use_requests[use_id] = {"name": "unified", "use_id": use_id} - cortex_service._complete_agent_file(agent_id, active_path) + cortex_service._complete_use_file(use_id, active_path) # Check file was renamed assert not active_path.exists() - completed_path = unified_dir / f"{agent_id}.jsonl" + completed_path = unified_dir / f"{use_id}.jsonl" assert completed_path.exists() - symlink_path = mock_journal / "agents" / "unified.log" + symlink_path = mock_journal / "talents" / "unified.log" assert symlink_path.is_symlink() - assert os.readlink(symlink_path) == f"unified/{agent_id}.jsonl" + assert os.readlink(symlink_path) == f"unified/{use_id}.jsonl" -def test_complete_agent_file_replaces_symlink(cortex_service, mock_journal): +def test_complete_use_file_replaces_symlink(cortex_service, mock_journal): """Test completing agent file replaces convenience symlink for same name.""" - unified_dir = mock_journal / "agents" / "unified" + unified_dir = mock_journal / "talents" / "unified" unified_dir.mkdir() first_agent_id = "111" first_active_path = unified_dir / f"{first_agent_id}_active.jsonl" first_active_path.touch() - cortex_service.agent_requests[first_agent_id] = {"name": "unified"} + cortex_service.use_requests[first_agent_id] = {"name": "unified"} - cortex_service._complete_agent_file(first_agent_id, first_active_path) + cortex_service._complete_use_file(first_agent_id, first_active_path) second_agent_id = "222" second_active_path = unified_dir / f"{second_agent_id}_active.jsonl" second_active_path.touch() - cortex_service.agent_requests[second_agent_id] = {"name": "unified"} + cortex_service.use_requests[second_agent_id] = {"name": "unified"} - cortex_service._complete_agent_file(second_agent_id, second_active_path) + cortex_service._complete_use_file(second_agent_id, second_active_path) - symlink_path = mock_journal / "agents" / "unified.log" + symlink_path = mock_journal / "talents" / "unified.log" assert symlink_path.is_symlink() assert os.readlink(symlink_path) == f"unified/{second_agent_id}.jsonl" -def test_complete_agent_file_colon_name(cortex_service, mock_journal): +def test_complete_use_file_colon_name(cortex_service, mock_journal): """Test completing agent file sanitizes colon in convenience symlink name.""" - agent_id = "123456789" - entities_dir = mock_journal / "agents" / "entities--entity_assist" + use_id = "123456789" + entities_dir = mock_journal / "talents" / "entities--entity_assist" entities_dir.mkdir() - active_path = entities_dir / f"{agent_id}_active.jsonl" + active_path = entities_dir / f"{use_id}_active.jsonl" active_path.touch() - cortex_service.agent_requests[agent_id] = {"name": "entities:entity_assist"} + cortex_service.use_requests[use_id] = {"name": "entities:entity_assist"} - cortex_service._complete_agent_file(agent_id, active_path) + cortex_service._complete_use_file(use_id, active_path) - symlink_path = mock_journal / "agents" / "entities--entity_assist.log" + symlink_path = mock_journal / "talents" / "entities--entity_assist.log" assert symlink_path.is_symlink() - assert os.readlink(symlink_path) == f"entities--entity_assist/{agent_id}.jsonl" + assert os.readlink(symlink_path) == f"entities--entity_assist/{use_id}.jsonl" -def test_complete_agent_file_no_name(cortex_service, mock_journal): +def test_complete_use_file_no_name(cortex_service, mock_journal): """Test completing agent file skips symlink when request name is missing.""" - agent_id = "123456789" - active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + active_path = mock_journal / "talents" / f"{use_id}_active.jsonl" active_path.touch() - cortex_service._complete_agent_file(agent_id, active_path) + cortex_service._complete_use_file(use_id, active_path) - completed_path = mock_journal / "agents" / f"{agent_id}.jsonl" + completed_path = mock_journal / "talents" / f"{use_id}.jsonl" assert completed_path.exists() - assert not any(path.is_symlink() for path in (mock_journal / "agents").iterdir()) + assert not any(path.is_symlink() for path in (mock_journal / "talents").iterdir()) def test_write_error_and_complete(cortex_service, mock_journal): """Test writing error and completing file.""" - agent_id = "123456789" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "123456789" + file_path = mock_journal / "talents" / f"{use_id}_active.jsonl" file_path.touch() cortex_service._write_error_and_complete(file_path, "Test error message") # Check error was written - completed_path = mock_journal / "agents" / f"{agent_id}.jsonl" + completed_path = mock_journal / "talents" / f"{use_id}.jsonl" assert completed_path.exists() assert not file_path.exists() @@ -572,34 +573,34 @@ def test_write_error_and_complete(cortex_service, mock_journal): def test_get_status(cortex_service): """Test getting service status.""" - from think.cortex import AgentProcess + from think.cortex import TalentProcess # Empty status status = cortex_service.get_status() - assert status["running_agents"] == 0 - assert status["agent_ids"] == [] + assert status["running_uses"] == 0 + assert status["use_ids"] == [] # Add running agents mock_process = MagicMock() - agent1 = AgentProcess("111", mock_process, Path("/tmp/1.jsonl")) - agent2 = AgentProcess("222", mock_process, Path("/tmp/2.jsonl")) + agent1 = TalentProcess("111", mock_process, Path("/tmp/1.jsonl")) + agent2 = TalentProcess("222", mock_process, Path("/tmp/2.jsonl")) - cortex_service.running_agents["111"] = agent1 - cortex_service.running_agents["222"] = agent2 + cortex_service.running_uses["111"] = agent1 + cortex_service.running_uses["222"] = agent2 status = cortex_service.get_status() - assert status["running_agents"] == 2 - assert set(status["agent_ids"]) == {"111", "222"} + assert status["running_uses"] == 2 + assert set(status["use_ids"]) == {"111", "222"} def test_write_output(cortex_service, mock_journal): """Test writing agent output using explicit output_path.""" - agent_id = "test_agent" + use_id = "test_agent" result = "This is the agent result content" - expected_path = mock_journal / "20240115" / "agents" / "my_agent.md" + expected_path = mock_journal / "20240115" / "talents" / "my_agent.md" config = {"output": "md", "name": "my_agent", "output_path": str(expected_path)} - cortex_service._write_output(agent_id, result, config) + cortex_service._write_output(use_id, result, config) assert expected_path.exists() assert expected_path.read_text() == result @@ -610,20 +611,20 @@ def test_write_output_with_error(cortex_service, mock_journal, caplog): """Test write output handles errors gracefully.""" import logging - output_path = mock_journal / "20240115" / "agents" / "test.md" + output_path = mock_journal / "20240115" / "talents" / "test.md" with patch("builtins.open", side_effect=PermissionError("Cannot write")): with caplog.at_level(logging.ERROR): config = {"output": "md", "name": "test", "output_path": str(output_path)} - cortex_service._write_output("agent_id", "result", config) + cortex_service._write_output("use_id", "result", config) # Check error was logged but didn't raise - assert "Failed to write agent agent_id output" in caplog.text + assert "Failed to write talent use_id output" in caplog.text def test_write_output_missing_path_skips(cortex_service, mock_journal, caplog): """Test write output skips when output_path is missing.""" config = {"output": "md", "name": "test"} - cortex_service._write_output("agent_id", "result", config) + cortex_service._write_output("use_id", "result", config) # No output written, no error — silent skip is expected assert "Failed to write" not in caplog.text @@ -631,10 +632,10 @@ def test_write_output_missing_path_skips(cortex_service, mock_journal, caplog): def test_write_output_with_day_parameter(cortex_service, mock_journal): """Test writing agent output to a specific day directory.""" - agent_id = "test_agent" + use_id = "test_agent" result = "This is the agent result content" specified_day = "20240201" - expected_path = mock_journal / specified_day / "agents" / "reporter.md" + expected_path = mock_journal / specified_day / "talents" / "reporter.md" config = { "output": "md", "name": "reporter", @@ -642,7 +643,7 @@ def test_write_output_with_day_parameter(cortex_service, mock_journal): "output_path": str(expected_path), } - cortex_service._write_output(agent_id, result, config) + cortex_service._write_output(use_id, result, config) assert expected_path.exists() assert expected_path.read_text() == result @@ -651,9 +652,9 @@ def test_write_output_with_day_parameter(cortex_service, mock_journal): def test_write_output_with_segment(cortex_service, mock_journal): """Test writing segment agent output to segment agents directory.""" - agent_id = "segment_agent" + use_id = "segment_agent" result = "Segment analysis content" - expected_path = mock_journal / "20240115" / "143000_600" / "agents" / "analyzer.md" + expected_path = mock_journal / "20240115" / "143000_600" / "talents" / "analyzer.md" config = { "output": "md", "name": "analyzer", @@ -661,7 +662,7 @@ def test_write_output_with_segment(cortex_service, mock_journal): "output_path": str(expected_path), } - cortex_service._write_output(agent_id, result, config) + cortex_service._write_output(use_id, result, config) assert expected_path.exists() assert expected_path.read_text() == result @@ -669,16 +670,16 @@ def test_write_output_with_segment(cortex_service, mock_journal): def test_write_output_json_format(cortex_service, mock_journal): """Test writing agent output in JSON format.""" - agent_id = "json_agent" + use_id = "json_agent" result = '{"key": "value"}' - expected_path = mock_journal / "20240115" / "agents" / "data_agent.json" + expected_path = mock_journal / "20240115" / "talents" / "data_agent.json" config = { "output": "json", "name": "data_agent", "output_path": str(expected_path), } - cortex_service._write_output(agent_id, result, config) + cortex_service._write_output(use_id, result, config) assert expected_path.exists() assert expected_path.read_text() == result @@ -686,15 +687,15 @@ def test_write_output_json_format(cortex_service, mock_journal): def test_monitor_stdout_with_output(cortex_service, mock_journal): """Test monitor_stdout writes output when output_path is present.""" - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "output_test" - active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" - output_path = mock_journal / "20240115" / "agents" / "test_agent.md" + use_id = "output_test" + active_path = mock_journal / "talents" / f"{use_id}_active.jsonl" + output_path = mock_journal / "20240115" / "talents" / "test_agent.md" # Store request with explicit output_path - cortex_service.agent_requests = { - agent_id: { + cortex_service.use_requests = { + use_id: { "event": "request", "prompt": "test", "output": "md", @@ -711,9 +712,9 @@ def test_monitor_stdout_with_output(cortex_service, mock_journal): mock_process.stdout = MockPipe(mock_stdout) mock_process.wait.return_value = 0 - agent = AgentProcess(agent_id, mock_process, active_path) + agent = TalentProcess(use_id, mock_process, active_path) - with patch.object(cortex_service, "_complete_agent_file"): + with patch.object(cortex_service, "_complete_use_file"): with patch.object(cortex_service, "_has_finish_event", return_value=True): cortex_service._monitor_stdout(agent) @@ -723,16 +724,16 @@ def test_monitor_stdout_with_output(cortex_service, mock_journal): def test_monitor_stdout_with_output_and_day(cortex_service, mock_journal): """Test monitor_stdout writes output to specific day via output_path.""" - from think.cortex import AgentProcess + from think.cortex import TalentProcess - agent_id = "output_day_test" - active_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" + use_id = "output_day_test" + active_path = mock_journal / "talents" / f"{use_id}_active.jsonl" specified_day = "20240220" - output_path = mock_journal / specified_day / "agents" / "daily_reporter.md" + output_path = mock_journal / specified_day / "talents" / "daily_reporter.md" # Store request with explicit output_path and day - cortex_service.agent_requests = { - agent_id: { + cortex_service.use_requests = { + use_id: { "event": "request", "prompt": "test", "output": "md", @@ -750,9 +751,9 @@ def test_monitor_stdout_with_output_and_day(cortex_service, mock_journal): mock_process.stdout = MockPipe(mock_stdout) mock_process.wait.return_value = 0 - agent = AgentProcess(agent_id, mock_process, active_path) + agent = TalentProcess(use_id, mock_process, active_path) - with patch.object(cortex_service, "_complete_agent_file"): + with patch.object(cortex_service, "_complete_use_file"): with patch.object(cortex_service, "_has_finish_event", return_value=True): cortex_service._monitor_stdout(agent) @@ -760,11 +761,11 @@ def test_monitor_stdout_with_output_and_day(cortex_service, mock_journal): assert output_path.read_text() == "Daily report content" -def test_recover_orphaned_agents(cortex_service, mock_journal): +def test_recover_orphaned_uses(cortex_service, mock_journal): """Test recovery of orphaned active agent files.""" # Create orphaned active files - agents_dir = mock_journal / "agents" - unified_dir = agents_dir / "unified" + talents_dir = mock_journal / "talents" + unified_dir = talents_dir / "unified" unified_dir.mkdir() agent1_active = unified_dir / "111_active.jsonl" agent2_active = unified_dir / "222_active.jsonl" @@ -773,7 +774,7 @@ def test_recover_orphaned_agents(cortex_service, mock_journal): agent2_active.write_text('{"event": "start", "ts": 2000}\n') active_files = [agent1_active, agent2_active] - cortex_service._recover_orphaned_agents(active_files) + cortex_service._recover_orphaned_uses(active_files) # Check active files were renamed to completed assert not agent1_active.exists() @@ -788,7 +789,7 @@ def test_recover_orphaned_agents(cortex_service, mock_journal): error_event = json.loads(lines1[1]) assert error_event["event"] == "error" assert "Recovered" in error_event["error"] - assert error_event["agent_id"] == "111" + assert error_event["use_id"] == "111" content2 = (unified_dir / "222.jsonl").read_text() lines2 = content2.strip().split("\n") diff --git a/tests/test_cortex_client.py b/tests/test_cortex_client.py index 3982ae076..34b228c0d 100644 --- a/tests/test_cortex_client.py +++ b/tests/test_cortex_client.py @@ -14,11 +14,11 @@ import pytest from think.callosum import CallosumConnection, CallosumServer from think.cortex_client import ( - cortex_agents, cortex_request, - get_agent_end_state, - get_agent_log_status, - wait_for_agents, + cortex_uses, + get_use_end_state, + get_use_log_status, + wait_for_uses, ) from think.models import GPT_5 from think.utils import now_ms @@ -36,7 +36,7 @@ def callosum_server(monkeypatch): tmp_path = Path(tmp_dir) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - (tmp_path / "agents").mkdir(parents=True, exist_ok=True) + (tmp_path / "talents").mkdir(parents=True, exist_ok=True) server = CallosumServer() server_thread = threading.Thread(target=server.start, daemon=True) @@ -84,7 +84,7 @@ def test_cortex_request_broadcasts_to_callosum(callosum_listener): messages = callosum_listener # Create a request - agent_id = cortex_request( + use_id = cortex_request( prompt="Test prompt", name="unified", provider="openai", @@ -102,20 +102,20 @@ def test_cortex_request_broadcasts_to_callosum(callosum_listener): assert msg["name"] == "unified" assert msg["provider"] == "openai" assert msg["model"] == GPT_5 - assert msg["agent_id"] == agent_id + assert msg["use_id"] == use_id assert "ts" in msg def test_cortex_request_returns_agent_id(callosum_server): - """Test that cortex_request returns agent_id string.""" + """Test that cortex_request returns use_id string.""" _ = callosum_server # Needed for side effects only - agent_id = cortex_request(prompt="Test", name="unified", provider="openai") + use_id = cortex_request(prompt="Test", name="unified", provider="openai") - # Verify agent_id is a string timestamp - assert isinstance(agent_id, str) - assert agent_id.isdigit() - assert len(agent_id) == 13 # Millisecond timestamp + # Verify use_id is a string timestamp + assert isinstance(use_id, str) + assert use_id.isdigit() + assert len(use_id) == 13 # Millisecond timestamp def test_cortex_request_unique_agent_ids(callosum_server): @@ -124,8 +124,8 @@ def test_cortex_request_unique_agent_ids(callosum_server): agent_ids = [] for i in range(3): - agent_id = cortex_request(prompt=f"Test {i}", name="unified", provider="openai") - agent_ids.append(agent_id) + use_id = cortex_request(prompt=f"Test {i}", name="unified", provider="openai") + agent_ids.append(use_id) time.sleep(0.002) # All agent IDs should be unique @@ -136,9 +136,9 @@ def test_cortex_request_returns_none_on_send_failure(callosum_server, monkeypatc """Test cortex_request returns None when callosum_send fails.""" monkeypatch.setattr("think.cortex_client.callosum_send", lambda *a, **kw: False) - agent_id = cortex_request(prompt="Test", name="unified", provider="openai") + use_id = cortex_request(prompt="Test", name="unified", provider="openai") - assert agent_id is None + assert use_id is None def test_cortex_request_empty_journal(tmp_path, monkeypatch): @@ -146,21 +146,21 @@ def test_cortex_request_empty_journal(tmp_path, monkeypatch): monkeypatch.setattr("think.cortex_client.callosum_send", lambda *a, **kw: True) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agent_id = cortex_request("test", "unified", "openai") - assert agent_id is not None - assert len(agent_id) > 0 + use_id = cortex_request("test", "unified", "openai") + assert use_id is not None + assert len(use_id) > 0 -# Tests for cortex_agents remain mostly unchanged as they read from files +# Tests for cortex_uses remain mostly unchanged as they read from files def test_cortex_agents_empty(tmp_path, monkeypatch): - """Test cortex_agents with no agents.""" + """Test cortex_uses with no agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - result = cortex_agents() + result = cortex_uses() - assert result["agents"] == [] + assert result["uses"] == [] assert result["pagination"]["total"] == 0 assert result["pagination"]["has_more"] is False assert result["live_count"] == 0 @@ -168,17 +168,17 @@ def test_cortex_agents_empty(tmp_path, monkeypatch): def test_cortex_agents_with_active(tmp_path, monkeypatch): - """Test cortex_agents with active (running) agents.""" + """Test cortex_uses with active (running) agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() + talents_dir = tmp_path / "talents" + talents_dir.mkdir() # Create active agent files ts1 = now_ms() ts2 = ts1 + 1000 - unified_dir = agents_dir / "unified" - tester_dir = agents_dir / "tester" + unified_dir = talents_dir / "unified" + tester_dir = talents_dir / "tester" unified_dir.mkdir() tester_dir.mkdir() @@ -210,22 +210,22 @@ def test_cortex_agents_with_active(tmp_path, monkeypatch): ) f.write("\n") - result = cortex_agents() + result = cortex_uses() - assert len(result["agents"]) == 2 + assert len(result["uses"]) == 2 assert result["live_count"] == 2 assert result["historical_count"] == 0 def test_cortex_agents_with_completed(tmp_path, monkeypatch): - """Test cortex_agents with completed (historical) agents.""" + """Test cortex_uses with completed (historical) agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() + talents_dir = tmp_path / "talents" + talents_dir.mkdir() # Create completed agent files ts1 = now_ms() - reviewer_dir = agents_dir / "reviewer" + reviewer_dir = talents_dir / "reviewer" reviewer_dir.mkdir() completed_file1 = reviewer_dir / f"{ts1}.jsonl" @@ -244,23 +244,23 @@ def test_cortex_agents_with_completed(tmp_path, monkeypatch): json.dump({"event": "finish", "ts": ts1 + 100, "result": "Done"}, f) f.write("\n") - result = cortex_agents() + result = cortex_uses() - assert len(result["agents"]) == 1 + assert len(result["uses"]) == 1 assert result["live_count"] == 0 assert result["historical_count"] == 1 - assert result["agents"][0]["status"] == "completed" + assert result["uses"][0]["status"] == "completed" def test_cortex_agents_pagination(tmp_path, monkeypatch): - """Test cortex_agents pagination.""" + """Test cortex_uses pagination.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() + talents_dir = tmp_path / "talents" + talents_dir.mkdir() # Create multiple agents base_ts = now_ms() - unified_dir = agents_dir / "unified" + unified_dir = talents_dir / "unified" unified_dir.mkdir() for i in range(5): ts = base_ts + (i * 1000) @@ -278,151 +278,151 @@ def test_cortex_agents_pagination(tmp_path, monkeypatch): f.write("\n") # Test limit - result = cortex_agents(limit=2) - assert len(result["agents"]) == 2 + result = cortex_uses(limit=2) + assert len(result["uses"]) == 2 assert result["pagination"]["limit"] == 2 assert result["pagination"]["total"] == 5 assert result["pagination"]["has_more"] is True def test_cortex_agents_empty_journal(tmp_path, monkeypatch): - """Test cortex_agents works with an empty journal directory.""" + """Test cortex_uses works with an empty journal directory.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - result = cortex_agents() - assert "agents" in result + result = cortex_uses() + assert "uses" in result assert "pagination" in result - assert isinstance(result["agents"], list) + assert isinstance(result["uses"], list) def test_get_agent_log_status_completed(tmp_path, monkeypatch): - """Test get_agent_log_status returns 'completed' for finished agents.""" + """Test get_use_log_status returns 'completed' for finished agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + use_id = "1234567890123" + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') - assert get_agent_log_status(agent_id) == "completed" + assert get_use_log_status(use_id) == "completed" def test_get_agent_log_status_running(tmp_path, monkeypatch): - """Test get_agent_log_status returns 'running' for active agents.""" + """Test get_use_log_status returns 'running' for active agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') + use_id = "1234567890123" + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') - assert get_agent_log_status(agent_id) == "running" + assert get_use_log_status(use_id) == "running" def test_get_agent_log_status_not_found(tmp_path, monkeypatch): - """Test get_agent_log_status returns 'not_found' for missing agents.""" + """Test get_use_log_status returns 'not_found' for missing agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - (tmp_path / "agents").mkdir() + (tmp_path / "talents").mkdir() - assert get_agent_log_status("nonexistent") == "not_found" + assert get_use_log_status("nonexistent") == "not_found" def test_get_agent_log_status_prefers_completed(tmp_path, monkeypatch): - """Test get_agent_log_status returns 'completed' when both files exist.""" + """Test get_use_log_status returns 'completed' when both files exist.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() # Edge case: both files exist (shouldn't happen, but check precedence) - agent_id = "1234567890123" - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') + use_id = "1234567890123" + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') - assert get_agent_log_status(agent_id) == "completed" + assert get_use_log_status(use_id) == "completed" def test_get_agent_end_state_finish(tmp_path, monkeypatch): - """Test get_agent_end_state returns 'finish' for successful agents.""" + """Test get_use_end_state returns 'finish' for successful agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}.jsonl").write_text( + use_id = "1234567890123" + (unified_dir / f"{use_id}.jsonl").write_text( '{"event": "request", "prompt": "hello"}\n' '{"event": "finish", "result": "done"}\n' ) - assert get_agent_end_state(agent_id) == "finish" + assert get_use_end_state(use_id) == "finish" def test_get_agent_end_state_error(tmp_path, monkeypatch): - """Test get_agent_end_state returns 'error' for failed agents.""" + """Test get_use_end_state returns 'error' for failed agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}.jsonl").write_text( + use_id = "1234567890123" + (unified_dir / f"{use_id}.jsonl").write_text( '{"event": "request", "prompt": "hello"}\n' '{"event": "error", "error": "something went wrong"}\n' ) - assert get_agent_end_state(agent_id) == "error" + assert get_use_end_state(use_id) == "error" def test_get_agent_end_state_running(tmp_path, monkeypatch): - """Test get_agent_end_state returns 'running' for active agents.""" + """Test get_use_end_state returns 'running' for active agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() - agent_id = "1234567890123" - (unified_dir / f"{agent_id}_active.jsonl").write_text( + use_id = "1234567890123" + (unified_dir / f"{use_id}_active.jsonl").write_text( '{"event": "request", "prompt": "hello"}\n' ) - assert get_agent_end_state(agent_id) == "running" + assert get_use_end_state(use_id) == "running" def test_get_agent_end_state_unknown(tmp_path, monkeypatch): - """Test get_agent_end_state returns 'unknown' for missing agents.""" + """Test get_use_end_state returns 'unknown' for missing agents.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - (tmp_path / "agents").mkdir() + (tmp_path / "talents").mkdir() - assert get_agent_end_state("nonexistent") == "unknown" + assert get_use_end_state("nonexistent") == "unknown" -# Tests for wait_for_agents +# Tests for wait_for_uses def test_wait_for_agents_already_complete(tmp_path, monkeypatch): - """Test wait_for_agents returns immediately if agents already completed.""" + """Test wait_for_uses returns immediately if agents already completed.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() (tmp_path / "health").mkdir() # Create completed agents agent_ids = ["1000", "2000"] - for agent_id in agent_ids: - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + for use_id in agent_ids: + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') - completed, timed_out = wait_for_agents(agent_ids, timeout=1) + completed, timed_out = wait_for_uses(agent_ids, timeout=1) assert set(completed.keys()) == set(agent_ids) assert all(v == "finish" for v in completed.values()) @@ -430,21 +430,19 @@ def test_wait_for_agents_already_complete(tmp_path, monkeypatch): def test_wait_for_agents_event_completion(callosum_server): - """Test wait_for_agents completes when finish event is received.""" + """Test wait_for_uses completes when finish event is received.""" tmp_path = callosum_server - agents_dir = tmp_path / "agents" - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + unified_dir = talents_dir / "unified" unified_dir.mkdir(exist_ok=True) - agent_id = "1234567890123" + use_id = "1234567890123" # Start wait in background thread result = {"completed": None, "timed_out": None} def wait_thread(): - result["completed"], result["timed_out"] = wait_for_agents( - [agent_id], timeout=5 - ) + result["completed"], result["timed_out"] = wait_for_uses([use_id], timeout=5) waiter = threading.Thread(target=wait_thread) waiter.start() @@ -453,103 +451,101 @@ def test_wait_for_agents_event_completion(callosum_server): time.sleep(0.2) # Create the completed file and emit finish event - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') # Emit finish event via Callosum client = CallosumConnection() client.start() time.sleep(0.1) - client.emit("cortex", "finish", agent_id=agent_id, result="done") + client.emit("cortex", "finish", use_id=use_id, result="done") time.sleep(0.2) client.stop() waiter.join(timeout=3) - assert result["completed"] == {agent_id: "finish"} + assert result["completed"] == {use_id: "finish"} assert result["timed_out"] == [] def test_wait_for_agents_error_event(callosum_server): - """Test wait_for_agents completes on error event too.""" + """Test wait_for_uses completes on error event too.""" tmp_path = callosum_server - agents_dir = tmp_path / "agents" - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + unified_dir = talents_dir / "unified" unified_dir.mkdir(exist_ok=True) - agent_id = "1234567890124" + use_id = "1234567890124" result = {"completed": None, "timed_out": None} def wait_thread(): - result["completed"], result["timed_out"] = wait_for_agents( - [agent_id], timeout=5 - ) + result["completed"], result["timed_out"] = wait_for_uses([use_id], timeout=5) waiter = threading.Thread(target=wait_thread) waiter.start() time.sleep(0.2) # Create completed file and emit error event - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "error"}\n') + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "error"}\n') client = CallosumConnection() client.start() time.sleep(0.1) - client.emit("cortex", "error", agent_id=agent_id, error="something failed") + client.emit("cortex", "error", use_id=use_id, error="something failed") time.sleep(0.2) client.stop() waiter.join(timeout=3) - assert result["completed"] == {agent_id: "error"} + assert result["completed"] == {use_id: "error"} assert result["timed_out"] == [] def test_wait_for_agents_initial_file_check(tmp_path, monkeypatch): - """Test wait_for_agents finds already-completed agents via initial file check.""" + """Test wait_for_uses finds already-completed agents via initial file check.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() (tmp_path / "health").mkdir() - agent_id = "1234567890125" + use_id = "1234567890125" # Agent already completed before we start waiting - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') - completed, timed_out = wait_for_agents([agent_id], timeout=1) + completed, timed_out = wait_for_uses([use_id], timeout=1) # Should find via initial file check - assert completed == {agent_id: "finish"} + assert completed == {use_id: "finish"} assert timed_out == [] def test_wait_for_agents_timeout_actual(tmp_path, monkeypatch): - """Test wait_for_agents times out for agents that never complete.""" + """Test wait_for_uses times out for agents that never complete.""" monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() (tmp_path / "health").mkdir() - agent_id = "1234567890126" + use_id = "1234567890126" # Create active file (not completed) - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') - completed, timed_out = wait_for_agents([agent_id], timeout=1) + completed, timed_out = wait_for_uses([use_id], timeout=1) assert completed == {} - assert timed_out == [agent_id] + assert timed_out == [use_id] def test_wait_for_agents_partial(callosum_server): - """Test wait_for_agents with some completing and some timing out.""" + """Test wait_for_uses with some completing and some timing out.""" tmp_path = callosum_server - agents_dir = tmp_path / "agents" - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + unified_dir = talents_dir / "unified" unified_dir.mkdir(exist_ok=True) completing_agent = "1111" @@ -561,7 +557,7 @@ def test_wait_for_agents_partial(callosum_server): result = {"completed": None, "timed_out": None} def wait_thread(): - result["completed"], result["timed_out"] = wait_for_agents( + result["completed"], result["timed_out"] = wait_for_uses( [completing_agent, timeout_agent], timeout=2 ) @@ -575,7 +571,7 @@ def test_wait_for_agents_partial(callosum_server): client = CallosumConnection() client.start() time.sleep(0.1) - client.emit("cortex", "finish", agent_id=completing_agent, result="done") + client.emit("cortex", "finish", use_id=completing_agent, result="done") time.sleep(0.1) client.stop() @@ -590,41 +586,39 @@ def test_wait_for_agents_missed_event_recovery(tmp_path, monkeypatch, caplog): import logging monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - agents_dir = tmp_path / "agents" - agents_dir.mkdir() - unified_dir = agents_dir / "unified" + talents_dir = tmp_path / "talents" + talents_dir.mkdir() + unified_dir = talents_dir / "unified" unified_dir.mkdir() (tmp_path / "health").mkdir() - agent_id = "1234567890127" + use_id = "1234567890127" # Start with active file - (unified_dir / f"{agent_id}_active.jsonl").write_text('{"event": "start"}\n') + (unified_dir / f"{use_id}_active.jsonl").write_text('{"event": "start"}\n') result = {"completed": None, "timed_out": None} def wait_and_complete(): # Wait a bit then "complete" the agent by renaming file time.sleep(0.3) - (unified_dir / f"{agent_id}_active.jsonl").unlink() - (unified_dir / f"{agent_id}.jsonl").write_text('{"event": "finish"}\n') + (unified_dir / f"{use_id}_active.jsonl").unlink() + (unified_dir / f"{use_id}.jsonl").write_text('{"event": "finish"}\n') completer = threading.Thread(target=wait_and_complete) completer.start() with caplog.at_level(logging.INFO): - result["completed"], result["timed_out"] = wait_for_agents( - [agent_id], timeout=1 - ) + result["completed"], result["timed_out"] = wait_for_uses([use_id], timeout=1) completer.join() # Should recover via final file check - assert result["completed"] == {agent_id: "finish"} + assert result["completed"] == {use_id: "finish"} assert result["timed_out"] == [] # Should log about missed event assert any( - "completion event not received but agent completed" in record.message + "completion event not received but use completed" in record.message for record in caplog.records ) diff --git a/tests/test_dream_activity.py b/tests/test_dream_activity.py index 9ff8e0839..09fa8a05d 100644 --- a/tests/test_dream_activity.py +++ b/tests/test_dream_activity.py @@ -114,7 +114,7 @@ class TestRunActivityPrompts: monkeypatch.setattr("think.dream.cortex_request", mock_cortex_request) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "finish" for aid in ids}, []), ) @@ -170,7 +170,7 @@ class TestRunActivityPrompts: monkeypatch.setattr("think.dream.cortex_request", mock_cortex_request) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "finish" for aid in ids}, []), ) @@ -220,7 +220,7 @@ class TestRunActivityPrompts: monkeypatch.setattr("think.dream.cortex_request", mock_cortex_request) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "finish" for aid in ids}, []), ) @@ -282,7 +282,7 @@ class TestRunActivityPrompts: lambda prompt, name, config: "agent-1", ) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "error" for aid in ids}, []), ) @@ -358,7 +358,7 @@ class TestRunActivityPrompts: lambda prompt, name, config: "agent-1", ) monkeypatch.setattr( - "think.dream.wait_for_agents", + "think.dream.wait_for_uses", lambda ids, timeout: ({aid: "finish" for aid in ids}, []), ) @@ -376,8 +376,8 @@ class TestRunActivityPrompts: events = [e[0] for e in emitted] assert "started" in events assert "group_started" in events - assert "agent_started" in events - assert "agent_completed" in events + assert "talent_started" in events + assert "talent_completed" in events assert "group_completed" in events assert "completed" in events @@ -889,7 +889,7 @@ class TestActivityTemplateVars: """Tests for activity template variables in _build_prompt_context.""" def test_activity_vars_populated(self): - from think.agents import _build_prompt_context + from think.talents import _build_prompt_context activity = { "id": "coding_100000_300", @@ -916,7 +916,7 @@ class TestActivityTemplateVars: assert int(ctx["activity_duration"]) == 10 # 2 * 300s = 10 min def test_no_activity_no_vars(self): - from think.agents import _build_prompt_context + from think.talents import _build_prompt_context ctx = _build_prompt_context("20260209", None, None) @@ -924,7 +924,7 @@ class TestActivityTemplateVars: assert "activity_type" not in ctx def test_empty_entities(self): - from think.agents import _build_prompt_context + from think.talents import _build_prompt_context activity = { "id": "browsing_100000_300", @@ -940,7 +940,7 @@ class TestActivityTemplateVars: assert ctx["activity_entities"] == "" def test_duration_minimum_one(self): - from think.agents import _build_prompt_context + from think.talents import _build_prompt_context activity = { "id": "test_bad_seg", diff --git a/tests/test_dream_segment.py b/tests/test_dream_segment.py index 0e0a3f3d8..71fe677ab 100644 --- a/tests/test_dream_segment.py +++ b/tests/test_dream_segment.py @@ -17,7 +17,7 @@ def segment_dir(tmp_path, monkeypatch): day_dir = journal / "chronicle" / "20240115" segment_path = day_dir / "default" / "120000_300" segment_path.mkdir(parents=True) - (segment_path / "agents").mkdir(parents=True) + (segment_path / "talents").mkdir(parents=True) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) return segment_path @@ -57,7 +57,7 @@ def _segment_configs(*names: str) -> dict[str, dict]: def _write_sense_output(segment_dir: Path, sense_json: dict) -> None: - (segment_dir / "agents" / "sense.json").write_text( + (segment_dir / "talents" / "sense.json").write_text( json.dumps(sense_json), encoding="utf-8", ) @@ -74,13 +74,13 @@ class TestLoadSegmentFacets: def test_empty_file_returns_empty(self, segment_dir): from think.facets import load_segment_facets - (segment_dir / "agents" / "facets.json").write_text("") + (segment_dir / "talents" / "facets.json").write_text("") assert load_segment_facets("20240115", "120000_300") == [] def test_empty_array_returns_empty(self, segment_dir): from think.facets import load_segment_facets - (segment_dir / "agents" / "facets.json").write_text("[]") + (segment_dir / "talents" / "facets.json").write_text("[]") assert load_segment_facets("20240115", "120000_300") == [] def test_valid_facets_extracted(self, segment_dir): @@ -90,21 +90,21 @@ class TestLoadSegmentFacets: {"facet": "work", "activity": "Code review", "level": "high"}, {"facet": "personal", "activity": "Email check", "level": "low"}, ] - (segment_dir / "agents" / "facets.json").write_text(json.dumps(facets_data)) + (segment_dir / "talents" / "facets.json").write_text(json.dumps(facets_data)) assert load_segment_facets("20240115", "120000_300") == ["work", "personal"] def test_malformed_json_returns_empty(self, segment_dir, caplog): from think.facets import load_segment_facets - (segment_dir / "agents" / "facets.json").write_text("{ invalid json") + (segment_dir / "talents" / "facets.json").write_text("{ invalid json") assert load_segment_facets("20240115", "120000_300") == [] assert "Failed to parse facets.json" in caplog.text def test_non_array_returns_empty(self, segment_dir, caplog): from think.facets import load_segment_facets - (segment_dir / "agents" / "facets.json").write_text('{"facet": "work"}') + (segment_dir / "talents" / "facets.json").write_text('{"facet": "work"}') assert load_segment_facets("20240115", "120000_300") == [] assert "not an array" in caplog.text @@ -116,7 +116,7 @@ class TestLoadSegmentFacets: {"activity": "Unknown"}, {"facet": "personal", "activity": "Email"}, ] - (segment_dir / "agents" / "facets.json").write_text(json.dumps(facets_data)) + (segment_dir / "talents" / "facets.json").write_text(json.dumps(facets_data)) assert load_segment_facets("20240115", "120000_300") == ["work", "personal"] @@ -143,7 +143,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -197,7 +197,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -221,7 +221,7 @@ class TestRunSegmentSense: "20240115", ) ] - density = json.loads((segment_dir / "agents" / "density.json").read_text()) + density = json.loads((segment_dir / "talents" / "density.json").read_text()) assert density["classification"] == "idle" # Verify activity state persisted even on idle path @@ -255,7 +255,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -315,7 +315,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -351,7 +351,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -392,7 +392,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -431,7 +431,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -469,7 +469,7 @@ class TestRunSegmentSense: def mock_wait_for_agents(agent_ids, timeout=600): return ({agent_ids[0]: "error"}, []) - monkeypatch.setattr(dream, "wait_for_agents", mock_wait_for_agents) + monkeypatch.setattr(dream, "wait_for_uses", mock_wait_for_agents) monkeypatch.setattr(dream, "_callosum", None) success, failed, failed_names = dream.run_segment_sense( @@ -529,7 +529,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr( @@ -582,7 +582,7 @@ class TestRunSegmentSense: segment_dir, {"density": "active", "recommend": {}, "facets": []}, ) - (segment_dir / "agents" / "entities.md").write_text( + (segment_dir / "talents" / "entities.md").write_text( "entities", encoding="utf-8" ) @@ -606,7 +606,7 @@ class TestRunSegmentSense: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr( @@ -652,7 +652,7 @@ class TestRunSegmentSense: monkeypatch.setattr(dream, "_SEND_RETRY_DELAYS", (0.0, 0.0)) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -886,7 +886,9 @@ class TestDreamJSONLWriter: path = tmp_path / "test.jsonl" writer = DreamJSONLWriter(str(path)) writer.log("run.start", mode="segment", day="20240115") - writer.log("agent.skip", name="screen", reason="not_recommended", detail="test") + writer.log( + "talent.skip", name="screen", reason="not_recommended", detail="test" + ) writer.close() lines = path.read_text().strip().split("\n") @@ -899,7 +901,7 @@ class TestDreamJSONLWriter: assert first["mode"] == "segment" second = json.loads(lines[1]) - assert second["event"] == "agent.skip" + assert second["event"] == "talent.skip" assert writer.skip_count == 1 def test_creates_parent_dirs(self, tmp_path): @@ -917,7 +919,7 @@ class TestDreamJSONLEvents: """Tests for JSONL event emission during segment orchestration.""" def test_density_idle_skip_event(self, segment_dir, monkeypatch): - """JSONL emits agent.skip with reason=density_idle for idle segments.""" + """JSONL emits talent.skip with reason=density_idle for idle segments.""" from think import dream from think.dream import DreamJSONLWriter @@ -941,7 +943,7 @@ class TestDreamJSONLEvents: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -960,7 +962,7 @@ class TestDreamJSONLEvents: json.loads(line) for line in jsonl_path.read_text(encoding="utf-8").strip().splitlines() ] - skips = [event for event in events if event["event"] == "agent.skip"] + skips = [event for event in events if event["event"] == "talent.skip"] assert any(skip["reason"] == "density_idle" for skip in skips) @@ -996,7 +998,7 @@ class TestDreamJSONLEvents: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -1017,7 +1019,7 @@ class TestDreamJSONLEvents: ] assert "sense.complete" in [event["event"] for event in events] - skips = [event for event in events if event["event"] == "agent.skip"] + skips = [event for event in events if event["event"] == "talent.skip"] skip_pairs = {(event["name"], event["reason"]) for event in skips} assert ("documents", "no_config") in skip_pairs assert ("screen", "not_recommended") in skip_pairs diff --git a/tests/test_engage.py b/tests/test_engage.py index 10d703ef3..2c3195b6f 100644 --- a/tests/test_engage.py +++ b/tests/test_engage.py @@ -50,11 +50,11 @@ class TestEngage: with ( patch("think.cortex_client.cortex_request", return_value="agent-123"), patch( - "think.cortex_client.wait_for_agents", + "think.cortex_client.wait_for_uses", return_value=({"agent-123": "finish"}, []), ), patch( - "think.cortex_client.read_agent_events", + "think.cortex_client.read_use_events", return_value=[{"event": "finish", "result": "All fixed!"}], ), ): @@ -67,7 +67,7 @@ class TestEngage: with ( patch("think.cortex_client.cortex_request", return_value="agent-123"), patch( - "think.cortex_client.wait_for_agents", + "think.cortex_client.wait_for_uses", return_value=({"agent-123": "error"}, []), ), ): @@ -79,7 +79,7 @@ class TestEngage: with ( patch("think.cortex_client.cortex_request", return_value="agent-123"), patch( - "think.cortex_client.wait_for_agents", + "think.cortex_client.wait_for_uses", return_value=({}, ["agent-123"]), ), ): diff --git a/tests/test_entities.py b/tests/test_entities.py index a70915477..4a81ed0f1 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -1742,7 +1742,7 @@ def test_parse_knowledge_graph_entities(tmp_path): os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) # Create a knowledge graph file - day_dir = tmp_path / "chronicle" / "20260108" / "agents" + day_dir = tmp_path / "chronicle" / "20260108" / "talents" day_dir.mkdir(parents=True) kg_content = """# Knowledge Graph Report @@ -1790,7 +1790,7 @@ def test_parse_knowledge_graph_entities_empty_file(tmp_path): """Test parsing returns empty list for empty KG.""" os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) - day_dir = tmp_path / "chronicle" / "20260108" / "agents" + day_dir = tmp_path / "chronicle" / "20260108" / "talents" day_dir.mkdir(parents=True) (day_dir / "knowledge_graph.md").write_text("") diff --git a/tests/test_entity_observer_context.py b/tests/test_entity_observer_context.py index 94389ed55..c1c17a32d 100644 --- a/tests/test_entity_observer_context.py +++ b/tests/test_entity_observer_context.py @@ -13,7 +13,7 @@ from think.entities.journal import clear_journal_entity_cache from think.entities.loading import clear_entity_loading_cache from think.entities.observations import clear_observation_cache, load_observations from think.entities.relationships import clear_relationship_caches -from think.talent import get_agent +from think.talent import get_talent def _set_journal(path: str) -> None: @@ -323,7 +323,7 @@ def test_post_process_handles_malformed_json(): def test_entity_observer_agent_config(): _set_journal("tests/fixtures/journal") - config = get_agent("entities:entity_observer") + config = get_talent("entities:entity_observer") assert config["type"] == "generate" assert config.get("output") == "json" diff --git a/tests/test_entity_agents.py b/tests/test_entity_talents.py similarity index 88% rename from tests/test_entity_agents.py rename to tests/test_entity_talents.py index dc6191ce1..490261559 100644 --- a/tests/test_entity_agents.py +++ b/tests/test_entity_talents.py @@ -7,7 +7,7 @@ import os import pytest -from think.talent import get_agent +from think.talent import get_talent @pytest.fixture @@ -21,7 +21,7 @@ def fixture_journal(): def test_entities_agent_config(fixture_journal): """Test detection agent configuration loads correctly.""" # Entity agents are in apps/entities/talent/ so use app-qualified name - config = get_agent("entities:entities") + config = get_talent("entities:entities") # Verify required fields assert config["name"] == "entities:entities" @@ -38,7 +38,7 @@ def test_entities_agent_config(fixture_journal): def test_entities_review_agent_config(fixture_journal): """Test review agent configuration loads correctly.""" # Entity agents are in apps/entities/talent/ so use app-qualified name - config = get_agent("entities:entities_review") + config = get_talent("entities:entities_review") # Verify required fields assert config["name"] == "entities:entities_review" @@ -54,7 +54,7 @@ def test_entities_review_agent_config(fixture_journal): def test_entities_agent_instruction_content(fixture_journal): """Test detection agent instruction contains expected sections.""" - config = get_agent("entities:entities") + config = get_talent("entities:entities") prompt = config["user_instruction"] # Check for key sections in the agent prompt @@ -67,7 +67,7 @@ def test_entities_agent_instruction_content(fixture_journal): def test_entities_review_agent_instruction_content(fixture_journal): """Test review agent instruction contains expected sections.""" - config = get_agent("entities:entities_review") + config = get_talent("entities:entities_review") prompt = config["user_instruction"] # Check for key sections in the agent prompt @@ -80,7 +80,7 @@ def test_entities_review_agent_instruction_content(fixture_journal): def test_agent_context_includes_entities_by_facet(fixture_journal): """Test that agent context includes entities grouped by facet.""" - config = get_agent("entities:entities") + config = get_talent("entities:entities") prompt = config["user_instruction"] assert "Available Facets" in prompt @@ -97,8 +97,8 @@ def test_agent_context_includes_entities_by_facet(fixture_journal): def test_agent_context_with_facet_focus(fixture_journal): - """Test that get_agent with facet parameter uses focused single-facet context.""" - config = get_agent("unified", facet="full-featured") + """Test that get_talent with facet parameter uses focused single-facet context.""" + config = get_talent("unified", facet="full-featured") prompt = config["user_instruction"] @@ -117,8 +117,8 @@ def test_agent_context_with_facet_focus(fixture_journal): def test_agent_priority_ordering(fixture_journal): """Test that entity agents have correct priority ordering.""" - detection_config = get_agent("entities:entities") - review_config = get_agent("entities:entities_review") + detection_config = get_talent("entities:entities") + review_config = get_talent("entities:entities_review") detection_priority = detection_config["priority"] review_priority = review_config["priority"] diff --git a/tests/test_facets.py b/tests/test_facets.py index 4ca40da2c..0124aadfd 100644 --- a/tests/test_facets.py +++ b/tests/test_facets.py @@ -346,7 +346,7 @@ def test_get_active_facets_from_segment_facets(monkeypatch, tmp_path): day_dir = journal / "chronicle" / "20240115" # Create segment with facets.json containing two facets (stream layout) - seg1 = day_dir / "archon" / "100000_300" / "agents" + seg1 = day_dir / "archon" / "100000_300" / "talents" seg1.mkdir(parents=True) (seg1 / "facets.json").write_text( json.dumps( @@ -358,7 +358,7 @@ def test_get_active_facets_from_segment_facets(monkeypatch, tmp_path): ) # Create another segment with overlapping + new facet - seg2 = day_dir / "archon" / "110000_300" / "agents" + seg2 = day_dir / "archon" / "110000_300" / "talents" seg2.mkdir(parents=True) (seg2 / "facets.json").write_text( json.dumps( @@ -382,12 +382,12 @@ def test_get_active_facets_empty_segments(monkeypatch, tmp_path): day_dir = journal / "chronicle" / "20240115" # Segment with empty facets array (stream layout) - seg1 = day_dir / "archon" / "100000_300" / "agents" + seg1 = day_dir / "archon" / "100000_300" / "talents" seg1.mkdir(parents=True) (seg1 / "facets.json").write_text("[]") # Segment with empty file - seg2 = day_dir / "archon" / "110000_300" / "agents" + seg2 = day_dir / "archon" / "110000_300" / "talents" seg2.mkdir(parents=True) (seg2 / "facets.json").write_text("") @@ -428,12 +428,12 @@ def test_get_active_facets_malformed_json(monkeypatch, tmp_path): day_dir = journal / "chronicle" / "20240115" # Malformed JSON segment (stream layout) - seg1 = day_dir / "archon" / "100000_300" / "agents" + seg1 = day_dir / "archon" / "100000_300" / "talents" seg1.mkdir(parents=True) (seg1 / "facets.json").write_text("{ invalid json") # Valid segment - seg2 = day_dir / "archon" / "110000_300" / "agents" + seg2 = day_dir / "archon" / "110000_300" / "talents" seg2.mkdir(parents=True) (seg2 / "facets.json").write_text( json.dumps( diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 79b75c847..ad06f273f 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -1016,7 +1016,7 @@ class TestFormatEvents: "title": "Project kickoff", "start": "14:00:00", "occurred": False, - "source": "20240101/agents/schedule.md", + "source": "20240101/talents/schedule.md", "participants": ["Alice", "Bob"], } ] @@ -1039,7 +1039,7 @@ class TestFormatEvents: "title": "Team standup", "start": "09:00:00", "occurred": True, - "source": "20240101/agents/meetings.md", + "source": "20240101/talents/meetings.md", "participants": ["Alice"], } ] @@ -1168,7 +1168,7 @@ class TestFormatMarkdown: """Test pattern matching for .md files.""" from think.formatters import get_formatter - formatter = get_formatter("20240101/agents/flow.md") + formatter = get_formatter("20240101/talents/flow.md") assert formatter is not None assert formatter.__name__ == "format_markdown" @@ -1176,7 +1176,7 @@ class TestFormatMarkdown: """Test pattern matching for segment screen.md files.""" from think.formatters import get_formatter - formatter = get_formatter("20240101/default/123456_300/agents/screen.md") + formatter = get_formatter("20240101/default/123456_300/talents/screen.md") assert formatter is not None assert formatter.__name__ == "format_markdown" @@ -1282,7 +1282,7 @@ class TestFormatMarkdown: path = ( Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) - / "chronicle/20240101/agents/flow.md" + / "chronicle/20240101/talents/flow.md" ) chunks, meta = format_file(path) @@ -1296,7 +1296,7 @@ class TestFormatMarkdown: path = ( Path(os.environ["_SOLSTONE_JOURNAL_OVERRIDE"]) - / "chronicle/20240101/agents/flow.md" + / "chronicle/20240101/talents/flow.md" ) text = load_markdown(path) @@ -1373,7 +1373,7 @@ class TestExtractPathMetadata: """Test day extraction from daily agent output path.""" from think.formatters import extract_path_metadata - meta = extract_path_metadata("20240101/agents/flow.md") + meta = extract_path_metadata("20240101/talents/flow.md") assert meta["day"] == "20240101" assert meta["facet"] == "" assert meta["agent"] == "flow" @@ -1382,7 +1382,7 @@ class TestExtractPathMetadata: """Test day and agent extraction from segment markdown.""" from think.formatters import extract_path_metadata - meta = extract_path_metadata("20240101/100000/agents/screen.md") + meta = extract_path_metadata("20240101/100000/talents/screen.md") assert meta["day"] == "20240101" assert meta["facet"] == "" assert meta["agent"] == "screen" @@ -1445,7 +1445,7 @@ class TestExtractPathMetadata: """Test app output path extraction.""" from think.formatters import extract_path_metadata - meta = extract_path_metadata("apps/myapp/agents/custom.md") + meta = extract_path_metadata("apps/myapp/talents/custom.md") assert meta["day"] == "" assert meta["facet"] == "" assert meta["agent"] == "myapp:custom" @@ -1727,7 +1727,7 @@ class TestFormatLogs: assert "- text: Test task" in chunks[0]["markdown"] def test_format_logs_with_agent_id(self): - """Test that agent_id renders as a link.""" + """Test that use_id renders as a link.""" from think.facets import format_logs entries = [ @@ -1737,7 +1737,7 @@ class TestFormatLogs: "actor": "entities", "action": "entity_attach", "params": {"type": "Person", "name": "Alice"}, - "agent_id": "1765870373972", + "use_id": "1765870373972", } ] @@ -1745,7 +1745,7 @@ class TestFormatLogs: assert len(chunks) == 1 assert ( - "**Agent:** [1765870373972](/app/sol/1765870373972)" + "**Talent:** [1765870373972](/app/sol/1765870373972)" in chunks[0]["markdown"] ) diff --git a/tests/test_generate_full.py b/tests/test_generate_full.py index 814e6cd22..182482609 100644 --- a/tests/test_generate_full.py +++ b/tests/test_generate_full.py @@ -65,7 +65,7 @@ def run_generator_with_config(mod, config: dict, monkeypatch) -> list[dict]: def test_generate_output_ndjson(tmp_path, monkeypatch): """Test basic output generation via NDJSON protocol.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -111,7 +111,7 @@ def test_generate_output_ndjson(tmp_path, monkeypatch): def test_generate_hook_invoked_with_context(tmp_path, monkeypatch): """Test that hooks receive correct context including span flag.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -171,7 +171,7 @@ def post_process(result, context): # Read captured context captured_path = ( - tmp_path / "chronicle" / "20240101" / "agents" / "context_captured.json" + tmp_path / "chronicle" / "20240101" / "talents" / "context_captured.json" ) captured = json.loads(captured_path.read_text()) @@ -186,7 +186,7 @@ def post_process(result, context): def test_generate_without_hook_succeeds(tmp_path, monkeypatch): """Test that generators without hooks still work correctly.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -228,7 +228,7 @@ def test_generate_without_hook_succeeds(tmp_path, monkeypatch): def test_generate_error_event_on_missing_generator(tmp_path, monkeypatch): """Test that missing generator name emits error event.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) @@ -249,7 +249,7 @@ def test_generate_error_event_on_missing_generator(tmp_path, monkeypatch): def test_generate_skipped_on_no_input(tmp_path, monkeypatch): """Test that generator emits skipped finish when no input.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") # Create empty day directory (no transcripts) os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) @@ -286,7 +286,7 @@ def test_generate_skipped_on_no_input(tmp_path, monkeypatch): def test_cogitate_not_skipped_without_sources(tmp_path, monkeypatch): """Test that cogitate agents with day but no sources are not skipped.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") # Create empty day directory (no transcripts) os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) diff --git a/tests/test_generate_scan_day.py b/tests/test_generate_scan_day.py index 5aea3d137..f6c005bd8 100644 --- a/tests/test_generate_scan_day.py +++ b/tests/test_generate_scan_day.py @@ -16,22 +16,22 @@ def copy_day(tmp_path: Path) -> Path: dest = day_path("20240101") src = FIXTURES / "journal" / "chronicle" / "20240101" copytree_tracked(src, dest) - agents_dir = dest / "agents" - agents_dir.mkdir(exist_ok=True) # Allow existing directory - (agents_dir / "flow.md").write_text("done") + talents_dir = dest / "talents" + talents_dir.mkdir(exist_ok=True) # Allow existing directory + (talents_dir / "flow.md").write_text("done") return dest def test_scan_day(tmp_path, monkeypatch): - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") day_dir = copy_day(tmp_path) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) info = mod.scan_day("20240101") - assert "agents/flow.md" in info["processed"] - assert "agents/timeline.md" in info["repairable"] + assert "talents/flow.md" in info["processed"] + assert "talents/timeline.md" in info["repairable"] - (day_dir / "agents" / "timeline.md").write_text("done") + (day_dir / "talents" / "timeline.md").write_text("done") info_after = mod.scan_day("20240101") - assert "agents/timeline.md" in info_after["processed"] - assert "agents/timeline.md" not in info_after["repairable"] + assert "talents/timeline.md" in info_after["processed"] + assert "talents/timeline.md" not in info_after["repairable"] diff --git a/tests/test_generate_agents.py b/tests/test_generate_talents.py similarity index 100% rename from tests/test_generate_agents.py rename to tests/test_generate_talents.py diff --git a/tests/test_google.py b/tests/test_google.py index 0e479df2c..6e7c081e1 100644 --- a/tests/test_google.py +++ b/tests/test_google.py @@ -71,7 +71,7 @@ def test_google_main(monkeypatch, tmp_path, capsys): setup_google_genai_stub(monkeypatch, with_thinking=False) sys.modules.pop("think.providers.google", None) importlib.reload(importlib.import_module("think.providers.google")) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() @@ -127,7 +127,7 @@ def test_google_main(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] @@ -149,7 +149,7 @@ def test_google_cli_not_found_error(monkeypatch, tmp_path, capsys): sys.modules.pop("think.providers.google", None) importlib.reload(importlib.import_module("think.providers.google")) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() @@ -166,7 +166,7 @@ def test_google_cli_not_found_error(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) # Check stdout for error event out_lines = capsys.readouterr().out.strip().splitlines() diff --git a/tests/test_google_thinking.py b/tests/test_google_thinking.py index d027dca81..2da0639af 100644 --- a/tests/test_google_thinking.py +++ b/tests/test_google_thinking.py @@ -26,7 +26,7 @@ def test_google_thinking_events(monkeypatch, tmp_path, capsys): sys.modules.pop("think.providers.google", None) importlib.reload(importlib.import_module("think.providers.google")) - mod = importlib.reload(importlib.import_module("think.agents")) + mod = importlib.reload(importlib.import_module("think.talents")) journal = tmp_path / "journal" journal.mkdir() @@ -109,7 +109,7 @@ def test_google_thinking_events(monkeypatch, tmp_path, capsys): "tools": ["search_insights"], } ) - asyncio.run(run_main(mod, ["sol agents"], stdin_data=ndjson_input)) + asyncio.run(run_main(mod, ["sol think.talents"], stdin_data=ndjson_input)) out_lines = capsys.readouterr().out.strip().splitlines() events = [json.loads(line) for line in out_lines] diff --git a/tests/test_heartbeat.py b/tests/test_heartbeat.py index a4c4352ef..f1865ec89 100644 --- a/tests/test_heartbeat.py +++ b/tests/test_heartbeat.py @@ -25,7 +25,7 @@ def heartbeat_mocks(monkeypatch): "think.heartbeat.cortex_request", lambda *args, **kwargs: "agent-123" ) monkeypatch.setattr( - "think.heartbeat.wait_for_agents", + "think.heartbeat.wait_for_uses", lambda *args, **kwargs: ({"agent-123": "finish"}, []), ) @@ -129,7 +129,7 @@ def test_pid_file_removed_on_timeout(journal_path, heartbeat_mocks): import think.heartbeat as mod pid_file = journal_path / "health" / "heartbeat.pid" - mod.wait_for_agents = lambda *a, **kw: ({}, ["agent-123"]) + mod.wait_for_uses = lambda *a, **kw: ({}, ["agent-123"]) with pytest.raises(SystemExit) as exc_info: mod.main() @@ -246,7 +246,7 @@ def test_force_flag_bypasses_recency_check(journal_path, monkeypatch): ) monkeypatch.setattr("think.heartbeat.ensure_sol_directory", lambda: None) monkeypatch.setattr( - "think.heartbeat.wait_for_agents", + "think.heartbeat.wait_for_uses", lambda *args, **kwargs: ({"agent-123": "finish"}, []), ) diff --git a/tests/test_home_events.py b/tests/test_home_events.py index 8b63cedbf..907188c22 100644 --- a/tests/test_home_events.py +++ b/tests/test_home_events.py @@ -32,7 +32,7 @@ class TestRecordTriageExchange: "tract": "cortex", "event": "finish", "name": "reviewer", - "agent_id": "123", + "use_id": "123", "result": "hello", } ) @@ -41,7 +41,7 @@ class TestRecordTriageExchange: mock_record.assert_not_called() def test_ignores_missing_agent_id(self): - """Handler returns early if agent_id is missing.""" + """Handler returns early if use_id is missing.""" ctx = self._make_ctx( { "tract": "cortex", @@ -61,7 +61,7 @@ class TestRecordTriageExchange: { "event": "request", "ts": 1700000000000, - "agent_id": "abc123", + "use_id": "abc123", "facet": "work", "app": "home", "path": "/home", @@ -70,7 +70,7 @@ class TestRecordTriageExchange: { "event": "finish", "ts": 1700000001000, - "agent_id": "abc123", + "use_id": "abc123", "result": "hi there", }, ] @@ -79,11 +79,11 @@ class TestRecordTriageExchange: "tract": "cortex", "event": "finish", "name": agent_name, - "agent_id": "abc123", + "use_id": "abc123", "result": "hi there", } ) - with patch("apps.home.events.read_agent_events", return_value=events): + with patch("apps.home.events.read_use_events", return_value=events): with patch("apps.home.events.record_exchange") as mock_record: record_triage_exchange(ctx) mock_record.assert_called_once_with( @@ -93,24 +93,24 @@ class TestRecordTriageExchange: user_message="hello world", agent_response="hi there", talent=agent_name, - agent_id="abc123", + use_id="abc123", ) def test_handles_missing_request_event(self): """Handler uses empty strings for metadata if request event not found.""" events = [ - {"event": "finish", "agent_id": "abc123", "result": "done"}, + {"event": "finish", "use_id": "abc123", "result": "done"}, ] ctx = self._make_ctx( { "tract": "cortex", "event": "finish", "name": "unified", - "agent_id": "abc123", + "use_id": "abc123", "result": "done", } ) - with patch("apps.home.events.read_agent_events", return_value=events): + with patch("apps.home.events.read_use_events", return_value=events): with patch("apps.home.events.record_exchange") as mock_record: record_triage_exchange(ctx) mock_record.assert_called_once_with( @@ -120,22 +120,22 @@ class TestRecordTriageExchange: user_message="", agent_response="done", talent="unified", - agent_id="abc123", + use_id="abc123", ) def test_handles_read_error_gracefully(self): - """Handler logs and swallows exceptions from read_agent_events.""" + """Handler logs and swallows exceptions from read_use_events.""" ctx = self._make_ctx( { "tract": "cortex", "event": "finish", "name": "unified", - "agent_id": "abc123", + "use_id": "abc123", "result": "done", } ) with patch( - "apps.home.events.read_agent_events", + "apps.home.events.read_use_events", side_effect=FileNotFoundError("not found"), ): with patch("apps.home.events.record_exchange") as mock_record: diff --git a/tests/test_home_yesterdays_processing.py b/tests/test_home_yesterdays_processing.py index 413a0ea39..905280cc9 100644 --- a/tests/test_home_yesterdays_processing.py +++ b/tests/test_home_yesterdays_processing.py @@ -58,11 +58,43 @@ def _seed_journal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: journal.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) + for day, transcript_seconds in (("20260415", 3600), ("20260414", 2700)): + facet_data = {"work": {"count": 1, "minutes": 15}} + if day == "20260414": + facet_data = {} + stats_path = journal / "chronicle" / day / "stats.json" + stats_path.parent.mkdir(parents=True, exist_ok=True) + stats_path.write_text( + json.dumps( + { + "stats": { + "transcript_segments": 3, + "transcript_duration": transcript_seconds, + }, + "facet_data": facet_data, + "heatmap_data": {"weekday": 2, "hours": {"9": 45.0}}, + } + ), + encoding="utf-8", + ) + + health_path = ( + journal / "chronicle" / "20260415" / "health" / "100_daily_dream.jsonl" + ) + health_path.parent.mkdir(parents=True, exist_ok=True) + health_path.write_text("", encoding="utf-8") + sparse_health_path = ( + journal / "chronicle" / "20260414" / "health" / "100_daily_dream.jsonl" + ) + sparse_health_path.parent.mkdir(parents=True, exist_ok=True) + sparse_health_path.write_text( + json.dumps({"event": "run.complete", "mode": "daily", "duration_ms": 10}) + + "\n", + encoding="utf-8", + ) + for rel_path in [ - "chronicle/20260415/stats.json", - "chronicle/20260415/agents/knowledge_graph.md", - "chronicle/20260415/health/100_daily_dream.jsonl", - "chronicle/20260414/stats.json", + "chronicle/20260415/talents/knowledge_graph.md", ]: _copy_fixture_file(journal, rel_path) @@ -139,21 +171,21 @@ def _seed_entities(journal: Path, day: str = "20260415") -> None: VALUES (?, ?, NULL, NULL, NULL, ?, ?, NULL, NULL, ?) """, [ - ("mention", "jane_doe", day, "work", f"{day}/agents/flow.md"), - ("mention", "alice_johnson", day, "work", f"{day}/agents/flow.md"), + ("mention", "jane_doe", day, "work", f"{day}/talents/flow.md"), + ("mention", "alice_johnson", day, "work", f"{day}/talents/flow.md"), ( "mention", "product_roadmap", day, "work", - f"{day}/agents/knowledge_graph.md", + f"{day}/talents/knowledge_graph.md", ), ( "mention", "launch_decision", day, "work", - f"{day}/agents/knowledge_graph.md", + f"{day}/talents/knowledge_graph.md", ), ], ) @@ -163,13 +195,18 @@ def _seed_entities(journal: Path, day: str = "20260415") -> None: def _append_dream_log( - journal: Path, day: str, name: str, *, facet: str | None = None + journal: Path, + day: str, + name: str, + *, + facet: str | None = None, + event: str = "talent.fail", ) -> None: path = journal / "chronicle" / day / "health" / "101_daily_dream.jsonl" path.parent.mkdir(parents=True, exist_ok=True) with path.open("a", encoding="utf-8") as handle: record = { - "event": "agent.fail", + "event": event, "mode": "daily", "name": name, "state": "error", @@ -220,6 +257,10 @@ def test_yesterdays_card_sparse_mode_copy(tmp_path, monkeypatch): _write_briefing(journal, "2026-04-15T06:45:00") monkeypatch.setattr("apps.home.routes._today", lambda: "20260415") + monkeypatch.setattr( + "apps.home.routes._knowledge_graph_freshness", + lambda _day: {"fresh": True}, + ) summary = _summarize_yesterday_processing("20260414", 2) @@ -378,7 +419,7 @@ def test_knowledge_graph_refresh_detection_yesterday_and_overnight( tmp_path, monkeypatch ): journal = _seed_journal(tmp_path, monkeypatch) - path = journal / "chronicle" / "20260415" / "agents" / "knowledge_graph.md" + path = journal / "chronicle" / "20260415" / "talents" / "knowledge_graph.md" _set_mtime(path, datetime(2026, 4, 15, 12, 0, 0)) assert _knowledge_graph_freshness("20260415")["fresh"] is True @@ -411,7 +452,7 @@ def test_gap_bullets_show_specific_daily_and_activity_without_generic(): "anomalies": [ {"kind": "daily_agents_missing"}, {"kind": "activity_agents_missing"}, - {"kind": "agent_failure"}, + {"kind": "talent_failure"}, ] }, {"fresh": True}, @@ -435,6 +476,19 @@ def test_newsletter_attempts_option_a_matches_facet_newsletter_failures_only( assert _newsletter_attempts_from_dream_logs("20260415") == (2, 3) +def test_newsletter_attempts_accept_legacy_agent_fail(tmp_path, monkeypatch): + journal = _seed_journal(tmp_path, monkeypatch) + _append_dream_log( + journal, + "20260415", + "facet_newsletter", + facet="work", + event="agent.fail", + ) + + assert _newsletter_attempts_from_dream_logs("20260415") == (2, 3) + + def test_build_pulse_context_includes_yesterday_processing(monkeypatch): monkeypatch.setattr( "apps.home.routes.get_capture_health", diff --git a/tests/test_journal_index.py b/tests/test_journal_index.py index 7096cd453..4cdc65750 100644 --- a/tests/test_journal_index.py +++ b/tests/test_journal_index.py @@ -281,7 +281,7 @@ def journal_fixture(tmp_path): # Create daily insight day = journal / "chronicle" / "20240101" day.mkdir(parents=True) - agents_dir = day / "agents" + agents_dir = day / "talents" agents_dir.mkdir() (agents_dir / "flow.md").write_text("# Flow Summary\n\nWorked on project alpha.\n") @@ -290,8 +290,8 @@ def journal_fixture(tmp_path): stream_dir.mkdir() segment = stream_dir / "100000_300" segment.mkdir() - (segment / "agents").mkdir() - (segment / "agents" / "screen.md").write_text( + (segment / "talents").mkdir() + (segment / "talents" / "screen.md").write_text( "# Screen Summary\n\nViewed documentation.\n" ) # Add stream.json for segment stream metadata @@ -299,15 +299,15 @@ def journal_fixture(tmp_path): write_segment_stream(str(segment), "default", None, None, 1) # Add second agent file for cross-file segment testing - (segment / "agents" / "activity.md").write_text( + (segment / "talents" / "activity.md").write_text( "# Activity Summary\n\nMet with Scott Ward about Acme deal.\n" ) # Create evening segment for time_bucket testing evening_segment = stream_dir / "200000_300" evening_segment.mkdir() - (evening_segment / "agents").mkdir() - (evening_segment / "agents" / "screen.md").write_text( + (evening_segment / "talents").mkdir() + (evening_segment / "talents" / "screen.md").write_text( "# Evening Screen\n\nReviewed evening reports.\n" ) write_segment_stream(str(evening_segment), "default", None, None, 1) @@ -636,17 +636,17 @@ def test_is_historical_day(): # Non-day paths are never historical assert _is_historical_day("facets/work/events/20240101.jsonl") is False assert _is_historical_day("imports/123/summary.md") is False - assert _is_historical_day("apps/home/agents/foo.md") is False + assert _is_historical_day("apps/home/talents/foo.md") is False # Future dates are not historical - assert _is_historical_day("29991231/agents/flow.md") is False + assert _is_historical_day("29991231/talents/flow.md") is False # Path without slash is not historical assert _is_historical_day("20240101") is False assert _is_historical_day("") is False # Day paths before today are historical (tested with a very old date) - assert _is_historical_day("20000101/agents/flow.md") is True + assert _is_historical_day("20000101/talents/flow.md") is True def test_scan_journal_full_mode(journal_fixture): @@ -672,10 +672,10 @@ def test_find_formattable_files(journal_fixture): paths = set(files.keys()) # Daily agent outputs - assert "20240101/agents/flow.md" in paths + assert "20240101/talents/flow.md" in paths # Segment agent outputs - assert "20240101/default/100000_300/agents/screen.md" in paths + assert "20240101/default/100000_300/talents/screen.md" in paths # Facet content assert "facets/work/events/20240101.jsonl" in paths @@ -909,7 +909,7 @@ def test_light_scan_removes_deleted_today_segment(tmp_path): today = datetime.now().strftime("%Y%m%d") day_dir = journal / today day_dir.mkdir(parents=True) - agents_dir = day_dir / "agents" + agents_dir = day_dir / "talents" agents_dir.mkdir() output_file = agents_dir / "flow.md" output_file.write_text("# Today Flow\n\nWorked on unique_today_content.\n") @@ -943,7 +943,7 @@ def test_light_scan_preserves_historical_content(tmp_path): # Create historical day content day_dir = journal / "chronicle" / "20200101" day_dir.mkdir(parents=True) - agents_dir = day_dir / "agents" + agents_dir = day_dir / "talents" agents_dir.mkdir() output_file = agents_dir / "flow.md" output_file.write_text("# Historical Flow\n\nWorked on historical_content.\n") @@ -978,7 +978,7 @@ def test_full_scan_removes_historical_content(tmp_path): # Create historical day content day_dir = journal / "chronicle" / "20200101" day_dir.mkdir(parents=True) - agents_dir = day_dir / "agents" + agents_dir = day_dir / "talents" agents_dir.mkdir() output_file = agents_dir / "flow.md" output_file.write_text("# Historical Flow\n\nWorked on historical_full_test.\n") @@ -1007,7 +1007,7 @@ def test_index_file_valid(journal_fixture): from think.indexer.journal import index_file, search_journal # Index a specific file - result = index_file(str(journal_fixture), "20240101/agents/flow.md", verbose=True) + result = index_file(str(journal_fixture), "20240101/talents/flow.md", verbose=True) assert result is True # Should be searchable @@ -1019,7 +1019,7 @@ def test_index_file_absolute_path(journal_fixture): """Test indexing with absolute path.""" from think.indexer.journal import index_file, search_journal - abs_path = str(journal_fixture / "chronicle" / "20240101" / "agents" / "flow.md") + abs_path = str(journal_fixture / "chronicle" / "20240101" / "talents" / "flow.md") result = index_file(str(journal_fixture), abs_path, verbose=True) assert result is True @@ -1057,13 +1057,13 @@ def test_index_file_updates_existing(journal_fixture): from think.indexer.journal import index_file, search_journal # Index the file - index_file(str(journal_fixture), "20240101/agents/flow.md") + index_file(str(journal_fixture), "20240101/talents/flow.md") # Get initial count total1, _ = search_journal("project alpha") # Re-index the same file - index_file(str(journal_fixture), "20240101/agents/flow.md") + index_file(str(journal_fixture), "20240101/talents/flow.md") # Count should be the same (not doubled) total2, _ = search_journal("project alpha") @@ -1117,7 +1117,7 @@ def test_extract_stream_segment_path(tmp_path): write_segment_stream(seg_dir, "archon", None, None, 1) result = _extract_stream( - str(tmp_path), "20240101/default/123456_300/agents/work/flow.md" + str(tmp_path), "20240101/default/123456_300/talents/work/flow.md" ) assert result == "archon" @@ -1126,7 +1126,7 @@ def test_extract_stream_non_segment_path(tmp_path): """_extract_stream returns None for non-segment paths.""" from think.indexer.journal import _extract_stream - result = _extract_stream(str(tmp_path), "20240101/agents/flow.md") + result = _extract_stream(str(tmp_path), "20240101/talents/flow.md") assert result is None result = _extract_stream(str(tmp_path), "facets/work/events/20240101.jsonl") @@ -1141,7 +1141,7 @@ def test_extract_stream_missing_marker(tmp_path): seg_dir.mkdir(parents=True) result = _extract_stream( - str(tmp_path), "20240101/default/123456_300/agents/work/flow.md" + str(tmp_path), "20240101/default/123456_300/talents/work/flow.md" ) assert result is None @@ -1302,12 +1302,13 @@ def test_scan_entities_incremental_noop(): assert count1 == count2 -def test_scan_entities_deletion(tmp_path): +def test_scan_entities_deletion(tmp_path, monkeypatch): """Verify entity rows are removed when source file is deleted.""" src = Path("tests/fixtures/journal") dst = tmp_path / "journal" copytree_tracked(src, dst) j = str(dst) + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", j) from think.indexer.journal import scan_journal @@ -1467,7 +1468,7 @@ def test_scan_signals_deletion(tmp_path): conn.close() assert initial == 45 - kg_file = dst / "chronicle" / "20240101" / "agents" / "knowledge_graph.md" + kg_file = dst / "chronicle" / "20240101" / "talents" / "knowledge_graph.md" kg_file.unlink() scan_journal(j, full=True) @@ -1711,10 +1712,10 @@ class TestSegmentChunks: scan_journal(str(journal_fixture), verbose=True, full=True) conn, _ = get_journal_index(str(journal_fixture)) screen_chunks = conn.execute( - "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/agents/screen.md'" + "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/talents/screen.md'" ).fetchone()[0] activity_chunks = conn.execute( - "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/agents/activity.md'" + "SELECT count(*) FROM chunks WHERE path='20240101/default/100000_300/talents/activity.md'" ).fetchone()[0] segment_chunks = conn.execute( "SELECT count(*) FROM chunks WHERE agent='segment'" diff --git a/tests/test_journal_stats.py b/tests/test_journal_stats.py index ba5d1c129..926843c12 100644 --- a/tests/test_journal_stats.py +++ b/tests/test_journal_stats.py @@ -27,8 +27,8 @@ def test_scan_day(tmp_path, monkeypatch): (ts_dir2 / "center_DP-1_screen.webm").write_bytes(b"WEBM") (day / "entities.md").write_text("") - (day / "agents").mkdir() - (day / "agents" / "flow.md").write_text("") + (day / "talents").mkdir() + (day / "talents" / "flow.md").write_text("") # Create event in new JSONL format: facets/{facet}/events/YYYYMMDD.jsonl events_dir = journal / "facets" / "work" / "events" @@ -45,7 +45,7 @@ def test_scan_day(tmp_path, monkeypatch): "facet": "work", "agent": "meetings", "occurred": True, - "source": "20240101/agents/meetings.md", + "source": "20240101/talents/meetings.md", } (events_dir / "20240101.jsonl").write_text(json.dumps(event)) @@ -169,7 +169,7 @@ def test_token_usage(tmp_path, monkeypatch): # Test JSON output includes token usage data = js.to_dict() - assert data["schema_version"] == 2 + assert data["schema_version"] == 3 assert "generated_at" in data assert data["day_count"] == 2 assert "tokens" in data @@ -252,7 +252,7 @@ def test_facet_event_mtime_invalidates_cache(tmp_path, monkeypatch): "facet": "work", "agent": "meetings", "occurred": True, - "source": "20240101/agents/meetings.md", + "source": "20240101/talents/meetings.md", } (events_dir / "20240101.jsonl").write_text(json.dumps(event)) diff --git a/tests/test_maint_004_rename.py b/tests/test_maint_004_rename.py new file mode 100644 index 000000000..ade83d186 --- /dev/null +++ b/tests/test_maint_004_rename.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +import importlib +from pathlib import Path + +mod = importlib.import_module("apps.sol.maint.004_rename_agents_to_talents") + + +def _patch_journal(monkeypatch, journal: Path, day: str = "20260417") -> None: + monkeypatch.setattr(mod, "get_journal", lambda: str(journal)) + monkeypatch.setattr(mod, "day_dirs", lambda: {day: str(journal / day)}) + monkeypatch.setattr( + mod, + "iter_segments", + lambda _day: [ + ("default", "090000_300", journal / day / "default" / "090000_300") + ], + ) + + +def test_run_migration_moves_all_paths(tmp_path, monkeypatch): + journal = tmp_path / "journal" + day = journal / "20260417" + segment = day / "default" / "090000_300" + + (journal / "agents").mkdir(parents=True) + (journal / "agents" / "root.jsonl").write_text("{}\n", encoding="utf-8") + (journal / "health").mkdir(parents=True) + (journal / "health" / "agents.json").write_text("{}", encoding="utf-8") + (day / "agents").mkdir(parents=True) + (day / "agents" / "flow.md").write_text("# flow\n", encoding="utf-8") + (segment / "agents").mkdir(parents=True) + (segment / "agents" / "screen.md").write_text("# screen\n", encoding="utf-8") + + _patch_journal(monkeypatch, journal) + + summary, collisions = mod.run_migration(journal, dry_run=False) + + assert collisions == [] + assert summary.discovered == 4 + assert summary.moved == 4 + assert summary.skipped == 0 + assert summary.errors == 0 + assert summary.collisions == 0 + assert (journal / "talents" / "root.jsonl").exists() + assert (journal / "health" / "talents.json").exists() + assert (day / "talents" / "flow.md").exists() + assert (segment / "talents" / "screen.md").exists() + + +def test_run_migration_aborts_on_collision(tmp_path, monkeypatch): + journal = tmp_path / "journal" + day = journal / "20260417" + + (journal / "agents").mkdir(parents=True) + (journal / "agents" / "root.jsonl").write_text("{}\n", encoding="utf-8") + (journal / "talents").mkdir(parents=True) + (day / "agents").mkdir(parents=True) + (day / "agents" / "flow.md").write_text("# flow\n", encoding="utf-8") + + _patch_journal(monkeypatch, journal) + + summary, collisions = mod.run_migration(journal, dry_run=False) + + assert summary.collisions == 1 + assert summary.moved == 0 + assert len(collisions) == 1 + assert (journal / "agents" / "root.jsonl").exists() + assert not (day / "talents").exists() + + +def test_run_migration_reports_already_migrated(tmp_path, monkeypatch): + journal = tmp_path / "journal" + day = journal / "20260417" + segment = day / "default" / "090000_300" + + (journal / "talents").mkdir(parents=True) + (journal / "talents" / "root.jsonl").write_text("{}\n", encoding="utf-8") + (journal / "health").mkdir(parents=True) + (journal / "health" / "talents.json").write_text("{}", encoding="utf-8") + (day / "talents").mkdir(parents=True) + (day / "talents" / "flow.md").write_text("# flow\n", encoding="utf-8") + (segment / "talents").mkdir(parents=True) + (segment / "talents" / "screen.md").write_text("# screen\n", encoding="utf-8") + + _patch_journal(monkeypatch, journal) + + summary, collisions = mod.run_migration(journal, dry_run=False) + + assert collisions == [] + assert summary.discovered == 0 + assert summary.moved == 0 + assert summary.skipped == 4 + assert summary.errors == 0 + assert summary.collisions == 0 diff --git a/tests/test_output_hooks.py b/tests/test_output_hooks.py index bd0d8ac77..6e94a457f 100644 --- a/tests/test_output_hooks.py +++ b/tests/test_output_hooks.py @@ -18,9 +18,9 @@ from pathlib import Path import talent.occurrence as occurrence from tests.conftest import copytree_tracked -from think.agents import _apply_template_vars from think.hooks import write_events_jsonl from think.talent import load_post_hook, load_pre_hook +from think.talents import _apply_template_vars from think.utils import day_path FIXTURES = Path("tests/fixtures") @@ -160,7 +160,7 @@ def test_prompt_metadata_no_hook_path(tmp_path): def test_output_hook_invocation(tmp_path, monkeypatch): """Test that agents.py invokes hook and uses transformed result.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) # Use tmp_path as talent directory to avoid polluting real talent/ @@ -215,7 +215,7 @@ def post_process(result, context): def test_output_hook_returns_none(tmp_path, monkeypatch): """Test that hook returning None uses original result.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -261,7 +261,7 @@ def post_process(result, context): def test_output_hook_error_fallback(tmp_path, monkeypatch): """Test that hook errors fall back to original result.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -658,7 +658,7 @@ def test_load_pre_hook_file_not_found(tmp_path): def test_pre_hook_invocation(tmp_path, monkeypatch): """Test that agents.py invokes pre-hook and uses modified inputs.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -824,7 +824,7 @@ def test_template_vars_popped_from_modifications(): def test_pre_hook_template_vars_integration(tmp_path, monkeypatch): """Test pre-hook template_vars reach the model as substituted text.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -875,7 +875,7 @@ def pre_process(context): def test_pre_hook_template_vars_with_field_mods(tmp_path, monkeypatch): """Test pre-hook can return field mods and template_vars together.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent @@ -929,7 +929,7 @@ def pre_process(context): def test_both_pre_and_post_hooks(tmp_path, monkeypatch): """Test that both pre and post hooks can be configured together.""" - mod = importlib.import_module("think.agents") + mod = importlib.import_module("think.talents") copy_day(tmp_path) import think.talent diff --git a/tests/test_output_path.py b/tests/test_output_path.py index e8c882766..1c174c051 100644 --- a/tests/test_output_path.py +++ b/tests/test_output_path.py @@ -29,30 +29,30 @@ class TestGetOutputPath: def test_daily_output_md(self): path = get_output_path("/journal/20250101", "activity", output_format="md") - assert path == Path("/journal/20250101/agents/activity.md") + assert path == Path("/journal/20250101/talents/activity.md") def test_daily_output_json(self): path = get_output_path("/journal/20250101", "facets", output_format="json") - assert path == Path("/journal/20250101/agents/facets.json") + assert path == Path("/journal/20250101/talents/facets.json") def test_segment_output(self): path = get_output_path( "/journal/20250101", "activity", segment="120000_300", output_format="md" ) - assert path == Path("/journal/20250101/120000_300/agents/activity.md") + assert path == Path("/journal/20250101/120000_300/talents/activity.md") def test_app_key_output(self): path = get_output_path( "/journal/20250101", "entities:observer", output_format="md" ) - assert path == Path("/journal/20250101/agents/_entities_observer.md") + assert path == Path("/journal/20250101/talents/_entities_observer.md") def test_facet_daily_output(self): """Multi-facet agent output uses a facet subdirectory.""" path = get_output_path( "/journal/20250101", "newsletter", output_format="md", facet="work" ) - assert path == Path("/journal/20250101/agents/work/newsletter.md") + assert path == Path("/journal/20250101/talents/work/newsletter.md") def test_facet_segment_output(self): """Multi-facet segment output uses a facet subdirectory.""" @@ -63,14 +63,16 @@ class TestGetOutputPath: output_format="json", facet="personal", ) - assert path == Path("/journal/20250101/120000_300/agents/personal/summary.json") + assert path == Path( + "/journal/20250101/120000_300/talents/personal/summary.json" + ) def test_facet_with_app_key(self): """App-qualified key with facet uses both prefixes.""" path = get_output_path( "/journal/20250101", "entities:observer", output_format="md", facet="work" ) - assert path == Path("/journal/20250101/agents/work/_entities_observer.md") + assert path == Path("/journal/20250101/talents/work/_entities_observer.md") def test_facet_none_same_as_omitted(self): """Explicit facet=None produces same path as omitting facet.""" diff --git a/tests/test_pipeline_health.py b/tests/test_pipeline_health.py index b6d0a6fd6..8ebe75651 100644 --- a/tests/test_pipeline_health.py +++ b/tests/test_pipeline_health.py @@ -34,7 +34,7 @@ def test_empty_day_is_healthy(pipeline_journal): assert summary["status"] == "healthy" assert summary["anomalies"] == [] - assert summary["agents"] == { + assert summary["talents"] == { "dispatched": 0, "completed": 0, "failed": 0, @@ -45,7 +45,7 @@ def test_empty_day_is_healthy(pipeline_journal): assert summary["activities"] == { "detected": 0, "persisted": 0, - "agents_fired": False, + "talents_fired": False, } assert all( run == {"count": 0, "duration_ms_total": 0} for run in summary["runs"].values() @@ -69,8 +69,8 @@ def test_healthy_day_with_all_modes(pipeline_journal): base / "1_segment_dream.jsonl", [ {"event": "run.start", "mode": "segment"}, - {"event": "agent.dispatch", "mode": "segment"}, - {"event": "agent.complete", "mode": "segment"}, + {"event": "talent.dispatch", "mode": "segment"}, + {"event": "talent.complete", "mode": "segment"}, {"event": "run.complete", "mode": "segment", "duration_ms": 10}, ], ) @@ -78,8 +78,8 @@ def test_healthy_day_with_all_modes(pipeline_journal): base / "2_daily_dream.jsonl", [ {"event": "run.start", "mode": "daily"}, - {"event": "agent.dispatch", "mode": "daily"}, - {"event": "agent.complete", "mode": "daily"}, + {"event": "talent.dispatch", "mode": "daily"}, + {"event": "talent.complete", "mode": "daily"}, {"event": "run.complete", "mode": "daily", "duration_ms": 20}, ], ) @@ -87,8 +87,8 @@ def test_healthy_day_with_all_modes(pipeline_journal): base / "3_activity_dream.jsonl", [ {"event": "run.start", "mode": "activity"}, - {"event": "agent.dispatch", "mode": "activity"}, - {"event": "agent.complete", "mode": "activity"}, + {"event": "talent.dispatch", "mode": "activity"}, + {"event": "talent.complete", "mode": "activity"}, {"event": "run.complete", "mode": "activity", "duration_ms": 30}, ], ) @@ -96,12 +96,12 @@ def test_healthy_day_with_all_modes(pipeline_journal): summary = summarize_pipeline_day(day) assert summary["status"] == "healthy" - assert summary["agents"]["dispatched"] == 3 - assert summary["agents"]["completed"] == 3 + assert summary["talents"]["dispatched"] == 3 + assert summary["talents"]["completed"] == 3 assert summary["runs"]["segment"] == {"count": 1, "duration_ms_total": 10} assert summary["runs"]["daily"] == {"count": 1, "duration_ms_total": 20} assert summary["runs"]["activity"] == {"count": 1, "duration_ms_total": 30} - assert summary["activities"]["agents_fired"] is True + assert summary["activities"]["talents_fired"] is True def test_agent_failure_promotes_warning(pipeline_journal): @@ -110,10 +110,10 @@ def test_agent_failure_promotes_warning(pipeline_journal): pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", [ { - "event": "agent.fail", + "event": "talent.fail", "mode": "segment", "name": "screen", - "agent_id": "a-1", + "use_id": "a-1", "state": "timeout", } ], @@ -122,29 +122,65 @@ def test_agent_failure_promotes_warning(pipeline_journal): summary = summarize_pipeline_day(day) assert summary["status"] == "warning" - assert summary["agents"]["failed"] == 1 - assert summary["agents"]["failed_list"] == [ - {"mode": "segment", "name": "screen", "agent_id": "a-1", "state": "timeout"} + assert summary["talents"]["failed"] == 1 + assert summary["talents"]["failed_list"] == [ + {"mode": "segment", "name": "screen", "use_id": "a-1", "state": "timeout"} ] assert summary["anomalies"] == [ { - "kind": "agent_failure", + "kind": "talent_failure", "mode": "segment", "name": "screen", - "agent_id": "a-1", + "use_id": "a-1", "state": "timeout", } ] +@pytest.mark.parametrize("event_name", ["agent.fail", "talent.fail"]) +def test_failure_reader_accepts_legacy_and_new_names(pipeline_journal, event_name): + day = "20990107" + _write_jsonl( + pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", + [{"event": event_name, "mode": "segment", "name": "screen", "use_id": "u-1"}], + ) + + summary = summarize_pipeline_day(day) + + assert summary["talents"]["failed"] == 1 + + +@pytest.mark.parametrize( + ("event_name", "field"), + [ + ("agent.dispatch", "dispatched"), + ("talent.dispatch", "dispatched"), + ("agent.complete", "completed"), + ("talent.complete", "completed"), + ("agent.skip", "skipped"), + ("talent.skip", "skipped"), + ], +) +def test_reader_accepts_legacy_and_new_event_names(pipeline_journal, event_name, field): + day = "20990108" + _write_jsonl( + pipeline_journal / "chronicle" / day / "health" / "1_segment_dream.jsonl", + [{"event": event_name, "mode": "segment"}], + ) + + summary = summarize_pipeline_day(day) + + assert summary["talents"][field] == 1 + + def test_failed_list_truncates_at_20(pipeline_journal): day = "20990103" events = [ { - "event": "agent.fail", + "event": "talent.fail", "mode": "daily", "name": f"agent-{idx}", - "agent_id": f"id-{idx}", + "use_id": f"id-{idx}", "state": "error", } for idx in range(25) @@ -155,10 +191,10 @@ def test_failed_list_truncates_at_20(pipeline_journal): summary = summarize_pipeline_day(day) - assert summary["agents"]["failed"] == 25 - assert len(summary["agents"]["failed_list"]) == 20 - assert summary["agents"]["failed_list_truncated"] is True - assert sum(1 for a in summary["anomalies"] if a["kind"] == "agent_failure") == 20 + assert summary["talents"]["failed"] == 25 + assert len(summary["talents"]["failed_list"]) == 20 + assert summary["talents"]["failed_list_truncated"] is True + assert sum(1 for a in summary["anomalies"] if a["kind"] == "talent_failure") == 20 def test_activity_detected_without_run_is_stale(pipeline_journal): @@ -235,7 +271,7 @@ def test_invalid_day_returns_healthy_empty(pipeline_journal): assert summary["status"] == "healthy" assert summary["anomalies"] == [] - assert summary["agents"] == { + assert summary["talents"] == { "dispatched": 0, "completed": 0, "failed": 0, @@ -252,7 +288,7 @@ def test_malformed_json_lines_skipped(pipeline_journal): path.write_text( json.dumps({"event": "run.start", "mode": "segment"}) + "\nnot json at all\n" - + json.dumps({"event": "agent.dispatch", "mode": "segment"}) + + json.dumps({"event": "talent.dispatch", "mode": "segment"}) + "\n", encoding="utf-8", ) @@ -260,7 +296,7 @@ def test_malformed_json_lines_skipped(pipeline_journal): summary = summarize_pipeline_day(day) assert summary["runs"]["segment"]["count"] == 1 - assert summary["agents"]["dispatched"] == 1 + assert summary["talents"]["dispatched"] == 1 @pytest.mark.parametrize( @@ -270,7 +306,7 @@ def test_malformed_json_lines_skipped(pipeline_journal): { "status": "healthy", "anomalies": [], - "agents": {"failed": 0}, + "talents": {"failed": 0}, "day": "20260101", }, None, @@ -281,9 +317,9 @@ def test_malformed_json_lines_skipped(pipeline_journal): "anomalies": [ {"kind": "activity_agents_missing"}, {"kind": "daily_agents_missing"}, - {"kind": "agent_failure"}, + {"kind": "talent_failure"}, ], - "agents": {"failed": 3}, + "talents": {"failed": 3}, "day": "20260101", }, { @@ -296,9 +332,9 @@ def test_malformed_json_lines_skipped(pipeline_journal): "status": "stale", "anomalies": [ {"kind": "daily_agents_missing"}, - {"kind": "agent_failure"}, + {"kind": "talent_failure"}, ], - "agents": {"failed": 2}, + "talents": {"failed": 2}, "day": "20260102", }, { @@ -309,26 +345,26 @@ def test_malformed_json_lines_skipped(pipeline_journal): ( { "status": "warning", - "anomalies": [{"kind": "agent_failure"}], - "agents": {"failed": 1}, + "anomalies": [{"kind": "talent_failure"}], + "talents": {"failed": 1}, "day": "20260101", }, - {"status": "warning", "message": "1 agent error today"}, + {"status": "warning", "message": "1 talent error today"}, ), ( { "status": "warning", - "anomalies": [{"kind": "agent_failure"}] * 3, - "agents": {"failed": 3}, + "anomalies": [{"kind": "talent_failure"}] * 3, + "talents": {"failed": 3}, "day": "20260101", }, - {"status": "warning", "message": "3 agent errors today"}, + {"status": "warning", "message": "3 talent errors today"}, ), ( { "status": "healthy", "anomalies": [{"kind": "segment_runs_missing"}], - "agents": {"failed": 0}, + "talents": {"failed": 0}, "day": "20260101", }, None, diff --git a/tests/test_pipeline_smoke.py b/tests/test_pipeline_smoke.py index 177f88e8c..edd4627ec 100644 --- a/tests/test_pipeline_smoke.py +++ b/tests/test_pipeline_smoke.py @@ -125,7 +125,7 @@ class TestPipelineSmokeTest: ) monkeypatch.setattr( dream, - "wait_for_agents", + "wait_for_uses", lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), ) monkeypatch.setattr(dream, "_callosum", None) @@ -142,8 +142,8 @@ class TestPipelineSmokeTest: for segment_key, sense_dict in SEGMENTS: seg_dir = journal / "chronicle" / DAY / STREAM / segment_key - (seg_dir / "agents").mkdir(parents=True, exist_ok=True) - (seg_dir / "agents" / "sense.json").write_text(json.dumps(sense_dict)) + (seg_dir / "talents").mkdir(parents=True, exist_ok=True) + (seg_dir / "talents" / "sense.json").write_text(json.dumps(sense_dict)) dream.run_segment_sense( day=DAY, @@ -161,7 +161,7 @@ class TestPipelineSmokeTest: "091500_300", "100000_300", ]: - seg_agents = journal / "chronicle" / DAY / STREAM / seg_key / "agents" + seg_agents = journal / "chronicle" / DAY / STREAM / seg_key / "talents" assert (seg_agents / "sense.json").exists() assert (seg_agents / "activity.md").exists() assert (seg_agents / "density.json").exists() @@ -179,7 +179,7 @@ class TestPipelineSmokeTest: / DAY / STREAM / seg_key - / "agents" + / "talents" / "speakers.json" ).read_text() ) @@ -192,7 +192,7 @@ class TestPipelineSmokeTest: / DAY / STREAM / seg_key - / "agents" + / "talents" / "speakers.json" ).exists() @@ -203,7 +203,7 @@ class TestPipelineSmokeTest: / DAY / STREAM / "092000_300" - / "agents" + / "talents" / "density.json" ).read_text() ) diff --git a/tests/test_agents_check.py b/tests/test_providers_check.py similarity index 76% rename from tests/test_agents_check.py rename to tests/test_providers_check.py index e54025edf..ab861c73c 100644 --- a/tests/test_agents_check.py +++ b/tests/test_providers_check.py @@ -11,8 +11,8 @@ import pytest def test_run_check_writes_health_file(tmp_path, monkeypatch): - """_run_check writes agents health results to _SOLSTONE_JOURNAL_OVERRIDE/health/agents.json.""" - import think.agents as agents + """_run_check writes provider health results to _SOLSTONE_JOURNAL_OVERRIDE/health/talents.json.""" + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = { @@ -25,13 +25,13 @@ def test_run_check_writes_health_file(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "ok")) async def mock_check_cogitate(*_args): return "ok", "ok" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -43,11 +43,11 @@ def test_run_check_writes_health_file(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" assert health_file.exists() payload = json.loads(health_file.read_text()) @@ -61,7 +61,7 @@ def test_run_check_writes_health_file(tmp_path, monkeypatch): def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): """_run_check exits 1 when any check fails.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = { @@ -74,13 +74,13 @@ def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "ok")) async def mock_check_cogitate(*_args): return "fail", "FAIL: timeout" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -92,11 +92,11 @@ def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 1 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" payload = json.loads(health_file.read_text()) assert payload["summary"]["passed"] == 3 assert payload["summary"]["skipped"] == 0 @@ -105,7 +105,7 @@ def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch): """_run_check exits 1 when all checks for a provider fail.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = { @@ -118,15 +118,15 @@ def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) monkeypatch.setattr( - agents, "_check_generate", lambda *_args: ("fail", "FAIL: key not set") + providers_cli, "_check_generate", lambda *_args: ("fail", "FAIL: key not set") ) async def mock_check_cogitate(*_args): return "fail", "FAIL: key not set" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -138,11 +138,11 @@ def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 1 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" payload = json.loads(health_file.read_text()) assert payload["summary"]["passed"] == 0 assert payload["summary"]["skipped"] == 0 @@ -151,7 +151,7 @@ def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch): def test_run_check_dedup_same_model(tmp_path, monkeypatch): """_run_check deduplicates checks when tiers resolve to the same model.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = { @@ -164,17 +164,17 @@ def test_run_check_dedup_same_model(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) gen_mock = MagicMock(return_value=("ok", "ok")) - monkeypatch.setattr(agents, "_check_generate", gen_mock) + monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) cog_inner = MagicMock(return_value=("ok", "ok")) async def mock_check_cogitate(*args): return cog_inner(*args) - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -186,13 +186,13 @@ def test_run_check_dedup_same_model(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 assert gen_mock.call_count == 1 assert cog_inner.call_count == 1 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" assert health_file.exists() payload = json.loads(health_file.read_text()) @@ -212,7 +212,7 @@ def test_run_check_dedup_same_model(tmp_path, monkeypatch): def test_run_check_targeted_filters_to_configured_pairs(tmp_path, monkeypatch): """--targeted filters checks to only configured provider+tier pairs.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"provA": object(), "provB": object(), "provC": object()} fake_defaults = { @@ -228,13 +228,13 @@ def test_run_check_targeted_filters_to_configured_pairs(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) monkeypatch.setattr("think.models.TYPE_DEFAULTS", fake_type_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "ok")) async def mock_check_cogitate(*_args): return "ok", "ok" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) # Mock get_config to return no overrides (use TYPE_DEFAULTS) monkeypatch.setattr("think.utils.get_config", lambda: {}) @@ -258,11 +258,11 @@ def test_run_check_targeted_filters_to_configured_pairs(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 - health_file = tmp_path / "health" / "agents.json" + health_file = tmp_path / "health" / "talents.json" payload = json.loads(health_file.read_text()) # Expected targeted pairs: (provA, 2), (provB, 2), (provC, 2) = 3 pairs × 2 interfaces = 6 checks assert payload["summary"]["total"] == 6 @@ -274,7 +274,7 @@ def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch): """--targeted exits silently when another targeted check holds the lock.""" import fcntl - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = {"fake": {1: "m", 2: "m", 3: "m"}} @@ -286,7 +286,7 @@ def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) monkeypatch.setattr("think.models.TYPE_DEFAULTS", fake_type_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) monkeypatch.setattr("think.utils.get_config", lambda: {}) monkeypatch.setattr("think.models.get_backup_provider", lambda _: None) @@ -297,7 +297,7 @@ def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch): fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) gen_mock = MagicMock(return_value=("ok", "ok")) - monkeypatch.setattr(agents, "_check_generate", gen_mock) + monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) args = argparse.Namespace( provider=None, @@ -309,18 +309,18 @@ def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch): ) # Should return silently (no SystemExit, no checks run) - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert gen_mock.call_count == 0 # No health file written - assert not (tmp_path / "health" / "agents.json").exists() + assert not (tmp_path / "health" / "talents.json").exists() lock_file.close() def test_check_generate_logs_token_usage(monkeypatch): """_check_generate logs token usage when result includes usage data.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_module = MagicMock() fake_module.run_generate.return_value = { @@ -339,7 +339,7 @@ def test_check_generate_logs_token_usage(monkeypatch): log_mock = MagicMock() monkeypatch.setattr("think.models.log_token_usage", log_mock) - status, msg = agents._check_generate("fake", 2, 30) + status, msg = providers_cli._check_generate("fake", 2, 30) assert status == "ok" assert msg == "OK" @@ -351,8 +351,8 @@ def test_check_generate_logs_token_usage(monkeypatch): ) -def test_cortex_start_emits_agents_check(tmp_path): - """Cortex startup requests an agents health check via supervisor.""" +def test_cortex_start_emits_providers_check(tmp_path): + """Cortex startup requests a providers health check via supervisor.""" from think.cortex import CortexService cortex = CortexService(journal_path=str(tmp_path)) @@ -366,13 +366,13 @@ def test_cortex_start_emits_agents_check(tmp_path): cortex.start() cortex.callosum.emit.assert_any_call( - "supervisor", "request", cmd=["sol", "agents", "check"] + "supervisor", "request", cmd=["sol", "providers", "check"] ) def test_missing_env_key_returns_skip(monkeypatch): """_check_generate returns skip status when env key is not set.""" - import think.agents as agents + import think.providers_cli as providers_cli monkeypatch.setattr( "think.providers.PROVIDER_METADATA", @@ -380,7 +380,7 @@ def test_missing_env_key_returns_skip(monkeypatch): ) monkeypatch.delenv("FAKE_API_KEY", raising=False) - status, msg = agents._check_generate("fake", 2, 30) + status, msg = providers_cli._check_generate("fake", 2, 30) assert status == "skip" assert "Fake Provider not configured" in msg assert "FAKE_API_KEY" in msg @@ -388,8 +388,8 @@ def test_missing_env_key_returns_skip(monkeypatch): def test_cogitate_missing_binary_returns_skip(monkeypatch): """_check_cogitate returns skip when CLI binary is not installed.""" - import think.agents as agents import think.providers as providers + import think.providers_cli as providers_cli monkeypatch.setitem( providers.PROVIDER_METADATA, @@ -403,29 +403,29 @@ def test_cogitate_missing_binary_returns_skip(monkeypatch): monkeypatch.setenv("FAKE_API_KEY", "test-key") monkeypatch.setattr("shutil.which", lambda _: None) - status, msg = asyncio.run(agents._check_cogitate("fake", 2, 30)) + status, msg = asyncio.run(providers_cli._check_cogitate("fake", 2, 30)) assert status == "skip" assert "nonexistent-binary-xyz CLI not installed" in msg def test_all_skip_exits_zero(tmp_path, monkeypatch): """Exit code is 0 when all results are skipped (no fails).""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) monkeypatch.setattr( - agents, "_check_generate", lambda *_args: ("skip", "not configured") + providers_cli, "_check_generate", lambda *_args: ("skip", "not configured") ) async def mock_check_cogitate(*_args): return "skip", "not configured" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -437,11 +437,11 @@ def test_all_skip_exits_zero(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 - payload = json.loads((tmp_path / "health" / "agents.json").read_text()) + payload = json.loads((tmp_path / "health" / "talents.json").read_text()) assert payload["summary"]["skipped"] == 6 assert payload["summary"]["failed"] == 0 assert payload["summary"]["passed"] == 0 @@ -452,22 +452,22 @@ def test_all_skip_exits_zero(tmp_path, monkeypatch): def test_mix_skip_and_fail_exits_one(tmp_path, monkeypatch): """Exit code is 1 when there's a mix of skip and fail results.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) monkeypatch.setattr( - agents, "_check_generate", lambda *_args: ("skip", "not configured") + providers_cli, "_check_generate", lambda *_args: ("skip", "not configured") ) async def mock_check_cogitate(*_args): return "fail", "FAIL: broken" - monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) args = argparse.Namespace( provider=None, @@ -479,18 +479,18 @@ def test_mix_skip_and_fail_exits_one(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 1 - payload = json.loads((tmp_path / "health" / "agents.json").read_text()) + payload = json.loads((tmp_path / "health" / "talents.json").read_text()) assert payload["summary"]["skipped"] == 3 assert payload["summary"]["failed"] == 3 def test_skipped_count_in_summary(tmp_path, monkeypatch): """Summary total equals passed + skipped + failed.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"okp": object(), "skipP": object()} fake_defaults = { @@ -500,21 +500,21 @@ def test_skipped_count_in_summary(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) def mock_gen(provider, tier, timeout): if provider == "okp": return "ok", "OK" return "skip", "not configured" - monkeypatch.setattr(agents, "_check_generate", mock_gen) + monkeypatch.setattr(providers_cli, "_check_generate", mock_gen) async def mock_cog(provider, tier, timeout): if provider == "okp": return "ok", "OK" return "skip", "not configured" - monkeypatch.setattr(agents, "_check_cogitate", mock_cog) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_cog) args = argparse.Namespace( provider=None, @@ -526,10 +526,10 @@ def test_skipped_count_in_summary(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as exc_info: - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) assert exc_info.value.code == 0 - payload = json.loads((tmp_path / "health" / "agents.json").read_text()) + payload = json.loads((tmp_path / "health" / "talents.json").read_text()) summary = payload["summary"] assert ( summary["total"] == summary["passed"] + summary["skipped"] + summary["failed"] @@ -541,20 +541,20 @@ def test_skipped_count_in_summary(tmp_path, monkeypatch): def test_status_field_in_json_output(tmp_path, monkeypatch, capsys): """JSON output includes status per result and skipped in summary.""" - import think.agents as agents + import think.providers_cli as providers_cli fake_registry = {"fake": object()} fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) - monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "OK")) + monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) + monkeypatch.setattr(providers_cli, "_check_generate", lambda *_args: ("ok", "OK")) async def mock_cog(*_args): return "ok", "OK" - monkeypatch.setattr(agents, "_check_cogitate", mock_cog) + monkeypatch.setattr(providers_cli, "_check_cogitate", mock_cog) args = argparse.Namespace( provider=None, @@ -566,7 +566,7 @@ def test_status_field_in_json_output(tmp_path, monkeypatch, capsys): ) with pytest.raises(SystemExit): - asyncio.run(agents._run_check(args)) + asyncio.run(providers_cli._run_check(args)) captured = capsys.readouterr() data = json.loads(captured.out) diff --git a/tests/test_retention.py b/tests/test_retention.py index 2fb7b39ba..2df801f20 100644 --- a/tests/test_retention.py +++ b/tests/test_retention.py @@ -107,7 +107,7 @@ def _make_segment( """Create a segment directory with specified contents.""" seg = tmp_path / "segment" seg.mkdir(exist_ok=True) - agents_dir = seg / "agents" + agents_dir = seg / "talents" agents_dir.mkdir(exist_ok=True) if audio: @@ -167,7 +167,7 @@ class TestIsSegmentComplete: def test_complete_with_stub_speaker_labels(self, tmp_path): """Stub speaker_labels.json (skipped=True, labels=[]) unblocks retention.""" seg = _make_segment(tmp_path, audio=True, embeddings=True, speaker_labels=False) - stub = seg / "agents" / "speaker_labels.json" + stub = seg / "talents" / "speaker_labels.json" stub.write_text( json.dumps({"labels": [], "skipped": True, "reason": "no_owner_centroid"}) ) @@ -286,14 +286,14 @@ class TestPurge: (day1 / "audio.flac").write_bytes(b"x" * 1000) (day1 / "audio.jsonl").write_text('{"raw":"audio.flac"}\n') (day1 / "stream.json").write_text('{"stream":"default"}') - (day1 / "agents").mkdir() + (day1 / "talents").mkdir() day1b = journal / "chronicle" / "20260115" / "plaud" / "103000_300" day1b.mkdir(parents=True) (day1b / "audio.m4a").write_bytes(b"x" * 500) (day1b / "audio.jsonl").write_text('{"raw":"audio.m4a"}\n') (day1b / "stream.json").write_text('{"stream":"plaud"}') - (day1b / "agents").mkdir() + (day1b / "talents").mkdir() # Day 2: recent — one complete segment (must stay within 30d window) day2 = journal / "chronicle" / "20260401" / "default" / "120000_300" @@ -301,7 +301,7 @@ class TestPurge: (day2 / "audio.flac").write_bytes(b"x" * 800) (day2 / "audio.jsonl").write_text('{"raw":"audio.flac"}\n') (day2 / "stream.json").write_text('{"stream":"default"}') - (day2 / "agents").mkdir() + (day2 / "talents").mkdir() # Day 3: incomplete segment (no audio.jsonl) day3 = journal / "chronicle" / "20260101" / "default" / "140000_300" @@ -449,7 +449,7 @@ class TestPurgeProvenance: segment = journal / "chronicle" / "20260115" / "default" / "100000_300" audio_jsonl = segment / "audio.jsonl" alternate_audio_jsonl = segment / "meeting_audio.jsonl" - speaker_labels = segment / "agents" / "speaker_labels.json" + speaker_labels = segment / "talents" / "speaker_labels.json" alternate_audio_jsonl.write_text('{"raw":"audio.flac"}\n') speaker_labels.write_text("{}") diff --git a/tests/test_routines.py b/tests/test_routines.py index 25bfe4928..28008197b 100644 --- a/tests/test_routines.py +++ b/tests/test_routines.py @@ -201,7 +201,7 @@ class TestCheck: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -237,7 +237,7 @@ class TestCheck: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -273,7 +273,7 @@ class TestCheck: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -309,7 +309,7 @@ class TestCheck: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -508,7 +508,7 @@ class TestEventTrigger: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -530,7 +530,7 @@ class TestEventTrigger: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -552,7 +552,7 @@ class TestEventTrigger: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -574,7 +574,7 @@ class TestEventTrigger: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -786,7 +786,7 @@ class TestResumeDate: with ( patch("think.routines.cortex_request", return_value="fake_agent_id"), patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -825,7 +825,7 @@ class TestResumeDate: with ( patch("think.routines.cortex_request", return_value="fake_agent_id"), patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), @@ -1576,7 +1576,7 @@ class TestMetaFiltering: "think.routines.cortex_request", return_value="fake_agent_id" ) as mock_req, patch( - "think.routines.wait_for_agents", + "think.routines.wait_for_uses", return_value=({"fake_agent_id": "finish"}, []), ), patch("think.routines.callosum_send", return_value=True), diff --git a/tests/test_segment.py b/tests/test_segment.py index 577bd94ec..2517f3b28 100644 --- a/tests/test_segment.py +++ b/tests/test_segment.py @@ -20,7 +20,7 @@ def _make_segment( stream_json=None, audio=True, screen=True, - agents=None, + talents=None, ): """Create a minimal segment fixture directory.""" seg_dir = base / "chronicle" / day / stream / segment @@ -31,11 +31,11 @@ def _make_segment( (seg_dir / "audio.jsonl").write_text('{"t":0}\n') if screen: (seg_dir / "screen.jsonl").write_text('{"t":0}\n') - if agents: - agents_dir = seg_dir / "agents" - agents_dir.mkdir() - for name in agents: - (agents_dir / name).write_text("# agent output\n") + if talents: + talents_dir = seg_dir / "talents" + talents_dir.mkdir() + for name in talents: + (talents_dir / name).write_text("# talent output\n") return seg_dir @@ -52,7 +52,7 @@ def test_list_basic(tmp_path, monkeypatch, capsys): "prev_segment": None, "seq": 1, }, - agents=["audio.md"], + talents=["audio.md"], ) _make_segment( tmp_path, @@ -65,7 +65,7 @@ def test_list_basic(tmp_path, monkeypatch, capsys): "prev_segment": "090000_300", "seq": 2, }, - agents=["audio.md", "screen.md"], + talents=["audio.md", "screen.md"], ) args = argparse.Namespace( @@ -131,7 +131,7 @@ def test_list_json(tmp_path, monkeypatch, capsys): "prev_segment": None, "seq": 1, }, - agents=["audio.md"], + talents=["audio.md"], ) args = argparse.Namespace( @@ -143,7 +143,7 @@ def test_list_json(tmp_path, monkeypatch, capsys): assert isinstance(data, list) assert data[0]["stream"] == "default" assert data[0]["segment"] == "090000_300" - assert data[0]["agents"] == 1 + assert data[0]["talents"] == 1 def test_list_empty_day(tmp_path, monkeypatch, capsys): @@ -171,7 +171,7 @@ def test_inspect_basic(tmp_path, monkeypatch, capsys): "prev_segment": None, "seq": 1, }, - agents=["audio.md"], + talents=["audio.md"], ) args = argparse.Namespace( @@ -226,7 +226,7 @@ def test_inspect_json(tmp_path, monkeypatch, capsys): "prev_segment": None, "seq": 1, }, - agents=["audio.md"], + talents=["audio.md"], ) args = argparse.Namespace( diff --git a/tests/test_sense_splitter.py b/tests/test_sense_splitter.py index 2114a3190..d21e9166b 100644 --- a/tests/test_sense_splitter.py +++ b/tests/test_sense_splitter.py @@ -40,7 +40,7 @@ class TestWriteSenseOutputs: write_sense_outputs(sense_json, seg_dir) - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" assert (agents_dir / "activity.md").exists() assert (agents_dir / "facets.json").exists() assert (agents_dir / "density.json").exists() @@ -77,7 +77,7 @@ class TestWriteSenseOutputs: write_sense_outputs(sense_json, seg_dir) - stored = json.loads((seg_dir / "agents" / "sense.json").read_text("utf-8")) + stored = json.loads((seg_dir / "talents" / "sense.json").read_text("utf-8")) assert stored["foo"] == "bar" assert stored == sense_json @@ -94,7 +94,7 @@ class TestMeetingDetection: write_sense_outputs(sense_json, seg_dir) - speakers_path = seg_dir / "agents" / "speakers.json" + speakers_path = seg_dir / "talents" / "speakers.json" assert speakers_path.exists() assert json.loads(speakers_path.read_text(encoding="utf-8")) == ["Alice", "Bob"] @@ -105,7 +105,7 @@ class TestMeetingDetection: write_sense_outputs(_make_sense_output(meeting_detected=False), seg_dir) - assert not (seg_dir / "agents" / "speakers.json").exists() + assert not (seg_dir / "talents" / "speakers.json").exists() def test_meeting_with_no_speakers_writes_empty_array(self, tmp_path): from think.sense_splitter import write_sense_outputs @@ -115,7 +115,7 @@ class TestMeetingDetection: write_sense_outputs(sense_json, seg_dir) - speakers_path = seg_dir / "agents" / "speakers.json" + speakers_path = seg_dir / "talents" / "speakers.json" assert speakers_path.exists() assert json.loads(speakers_path.read_text(encoding="utf-8")) == [] @@ -128,7 +128,7 @@ class TestEdgeCases: write_sense_outputs({}, seg_dir) - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" assert (agents_dir / "activity.md").exists() assert (agents_dir / "facets.json").exists() assert (agents_dir / "density.json").exists() @@ -156,7 +156,7 @@ class TestEdgeCases: write_sense_outputs(sense_json, seg_dir) - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" assert (agents_dir / "activity.md").read_text(encoding="utf-8") == "" assert ( json.loads((agents_dir / "facets.json").read_text(encoding="utf-8")) == [] @@ -172,7 +172,7 @@ class TestEdgeCases: write_sense_outputs(_make_sense_output(activity_summary=""), seg_dir) - assert (seg_dir / "agents" / "activity.md").read_text(encoding="utf-8") == "" + assert (seg_dir / "talents" / "activity.md").read_text(encoding="utf-8") == "" class TestMultipleFacets: @@ -187,7 +187,7 @@ class TestMultipleFacets: write_sense_outputs(_make_sense_output(facets=facets), seg_dir) - assert json.loads((seg_dir / "agents" / "facets.json").read_text("utf-8")) == ( + assert json.loads((seg_dir / "talents" / "facets.json").read_text("utf-8")) == ( facets ) @@ -200,7 +200,7 @@ class TestWriteIdleStubs: write_idle_stubs(seg_dir) - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" assert (agents_dir / "density.json").exists() density = json.loads((agents_dir / "density.json").read_text(encoding="utf-8")) assert density["classification"] == "idle" diff --git a/tests/test_sol.py b/tests/test_sol.py index 26fae0429..02002e678 100644 --- a/tests/test_sol.py +++ b/tests/test_sol.py @@ -281,6 +281,6 @@ class TestCommandRegistry: def test_critical_commands_registered(self): """Test that critical commands are registered.""" - critical = ["import", "agents", "dream", "indexer", "transcribe"] + critical = ["import", "providers", "dream", "indexer", "transcribe"] for cmd in critical: assert cmd in sol.COMMANDS, f"Critical command '{cmd}' not registered" diff --git a/tests/test_speaker_attribution_hook.py b/tests/test_speaker_attribution_hook.py index 8a7354345..e7573615c 100644 --- a/tests/test_speaker_attribution_hook.py +++ b/tests/test_speaker_attribution_hook.py @@ -36,7 +36,7 @@ class TestPreProcessStub: tmp_path, {"error": "no_owner_centroid"}, ) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert stub_path.exists() data = json.loads(stub_path.read_text()) assert data == {"labels": [], "skipped": True, "reason": "no_owner_centroid"} @@ -49,7 +49,7 @@ class TestPreProcessStub: tmp_path, {"error": "no_owner_centroid"}, ) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert not stub_path.exists() assert result == {"skip_reason": "no_owner_centroid"} @@ -61,7 +61,7 @@ class TestPreProcessStub: tmp_path, {"labels": []}, ) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert stub_path.exists() data = json.loads(stub_path.read_text()) assert data == {"labels": [], "skipped": True, "reason": "no_embeddings"} @@ -74,7 +74,7 @@ class TestPreProcessStub: tmp_path, {"labels": []}, ) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert not stub_path.exists() assert result == {"skip_reason": "no_embeddings"} @@ -85,7 +85,7 @@ class TestPreProcessStub: from talent.speaker_attribution import pre_process result = pre_process({"stream": "default"}) - stub_path = tmp_path / "agents" / "speaker_labels.json" + stub_path = tmp_path / "talents" / "speaker_labels.json" assert not stub_path.exists() assert result == {"skip_reason": "no_segment_context"} diff --git a/tests/test_stats_contract.py b/tests/test_stats_contract.py index f7cebeac1..402873f99 100644 --- a/tests/test_stats_contract.py +++ b/tests/test_stats_contract.py @@ -20,7 +20,7 @@ CONTRACT_FIELDS = [ ("tokens.by_model", "tokens.by_model"), ("tokens.by_day", "tokens.by_day"), ("facets.counts_by_day", "facets.counts_by_day"), - ("agents.counts_by_day", "agents.counts_by_day"), + ("talents.counts_by_day", "talents.counts_by_day"), ("days.*.transcript_duration", "transcript_duration"), ("days.*.percept_duration", "percept_duration"), ("tokens.by_day.*.*.input_tokens", "input_tokens"), @@ -61,7 +61,7 @@ def _build_journal(base_path): seg2 = day / "default" / "134500_300" seg1.mkdir(parents=True) seg2.mkdir(parents=True) - (day / "agents").mkdir(parents=True) + (day / "talents").mkdir(parents=True) audio_lines = [ {"raw": "raw.flac"}, @@ -82,7 +82,7 @@ def _build_journal(base_path): ) (seg2 / "audio.flac").write_bytes(b"fLaC") - (day / "agents" / "flow.md").write_text("") + (day / "talents" / "flow.md").write_text("") events_dir = journal / "facets" / "work" / "events" events_dir.mkdir(parents=True) @@ -98,7 +98,7 @@ def _build_journal(base_path): "facet": "work", "agent": "meetings", "occurred": True, - "source": "20240101/agents/meetings.md", + "source": "20240101/talents/meetings.md", } (events_dir / "20240101.jsonl").write_text(json.dumps(event) + "\n") @@ -186,7 +186,7 @@ def test_schema_rejects_missing_required_key(): "totals": {}, "heatmap": [], "tokens": {}, - "agents": {}, + "talents": {}, "facets": {}, } del output["totals"] @@ -209,7 +209,7 @@ def test_schema_rejects_wrong_version(): "totals": {}, "heatmap": [], "tokens": {}, - "agents": {}, + "talents": {}, "facets": {}, } ) diff --git a/tests/test_talent.py b/tests/test_talent.py index 5f6458c10..0be352919 100644 --- a/tests/test_talent.py +++ b/tests/test_talent.py @@ -7,8 +7,8 @@ import pytest from think.talent import ( _validate_cwd, - get_agent, - get_agent_filter, + get_talent, + get_talent_filter, source_is_enabled, source_is_required, ) @@ -52,21 +52,21 @@ def test_source_is_required_dict(): def test_get_agent_filter_bool(): - """Test get_agent_filter with bool values.""" - assert get_agent_filter(True) is None - assert get_agent_filter(False) == {} + """Test get_talent_filter with bool values.""" + assert get_talent_filter(True) is None + assert get_talent_filter(False) == {} def test_get_agent_filter_required_string(): - """Test get_agent_filter with 'required' string.""" - assert get_agent_filter("required") is None + """Test get_talent_filter with 'required' string.""" + assert get_talent_filter("required") is None def test_get_agent_filter_dict(): - """Test get_agent_filter with dict values.""" + """Test get_talent_filter with dict values.""" filter_dict = {"entities": True, "meetings": "required", "flow": False} - assert get_agent_filter(filter_dict) == filter_dict - assert get_agent_filter({}) == {} + assert get_talent_filter(filter_dict) == filter_dict + assert get_talent_filter({}) == {} def test_validate_cwd_defaults_cogitate_to_journal(): @@ -98,10 +98,10 @@ def test_validate_cwd_rejects_invalid_value(): def test_get_agent_normalizes_cwd_for_cogitate(): - config = get_agent("chat") + config = get_talent("chat") assert config["cwd"] == "journal" def test_get_agent_preserves_repo_cwd_for_coder(): - config = get_agent("coder") + config = get_talent("coder") assert config["cwd"] == "repo" diff --git a/tests/test_talent_cli.py b/tests/test_talent_cli.py index 8f0777000..73f9d798d 100644 --- a/tests/test_talent_cli.py +++ b/tests/test_talent_cli.py @@ -359,7 +359,7 @@ def test_logs_runs_new_columns(capsys): output = capsys.readouterr().out lines = [line for line in output.strip().splitlines() if line.strip()] - # Find the line for agent_id 1700000000001 (has JSONL file) + # Find the line for use_id 1700000000001 (has JSONL file) enriched_line = None for line in lines: if "1700000000001" in line: @@ -488,7 +488,7 @@ def test_parse_run_stats(): """Parse run stats extracts correct counts from fixture JSONL.""" from pathlib import Path - jsonl = Path("tests/fixtures/journal/agents/default/1700000000001.jsonl") + jsonl = Path("tests/fixtures/journal/talents/default/1700000000001.jsonl") stats = _parse_run_stats(jsonl) assert stats["event_count"] == 6 # all except request assert stats["tool_count"] == 1 # one tool_start @@ -502,7 +502,7 @@ def test_parse_run_stats_error(): """Parse run stats handles error run JSONL correctly.""" from pathlib import Path - jsonl = Path("tests/fixtures/journal/agents/flow/1700000000002.jsonl") + jsonl = Path("tests/fixtures/journal/talents/flow/1700000000002.jsonl") stats = _parse_run_stats(jsonl) assert stats["event_count"] == 2 # start + error (not request) assert stats["tool_count"] == 0 diff --git a/tests/test_agent_fallback.py b/tests/test_talent_fallback.py similarity index 92% rename from tests/test_agent_fallback.py rename to tests/test_talent_fallback.py index a63a779dc..403450d38 100644 --- a/tests/test_agent_fallback.py +++ b/tests/test_talent_fallback.py @@ -10,13 +10,13 @@ from unittest.mock import MagicMock import pytest -from think.agents import _is_retryable_error from think.models import ( TYPE_DEFAULTS, get_backup_provider, is_provider_healthy, should_recheck_health, ) +from think.talents import _is_retryable_error def test_is_provider_healthy_all_failed(): @@ -100,7 +100,7 @@ def _mock_base_agent_config() -> dict: def _patch_prepare_config_dependencies(monkeypatch): monkeypatch.setattr( - "think.talent.get_agent", lambda *args, **kwargs: _mock_base_agent_config() + "think.talent.get_talent", lambda *args, **kwargs: _mock_base_agent_config() ) monkeypatch.setattr( "think.talent.key_to_context", lambda _name: "talent.system.default" @@ -112,7 +112,7 @@ def _patch_prepare_config_dependencies(monkeypatch): def test_preflight_swap_unhealthy_primary(monkeypatch): - from think.agents import prepare_config + from think.talents import prepare_config _patch_prepare_config_dependencies(monkeypatch) monkeypatch.setattr( @@ -135,7 +135,7 @@ def test_preflight_swap_unhealthy_primary(monkeypatch): def test_preflight_no_swap_healthy_primary(monkeypatch): - from think.agents import prepare_config + from think.talents import prepare_config _patch_prepare_config_dependencies(monkeypatch) monkeypatch.setattr( @@ -151,7 +151,7 @@ def test_preflight_no_swap_healthy_primary(monkeypatch): def test_preflight_no_swap_no_backup_key(monkeypatch): - from think.agents import prepare_config + from think.talents import prepare_config _patch_prepare_config_dependencies(monkeypatch) monkeypatch.setattr( @@ -169,7 +169,7 @@ def test_preflight_no_swap_no_backup_key(monkeypatch): def test_on_failure_retry_cogitate(monkeypatch): - from think.agents import _execute_with_tools + from think.talents import _execute_with_tools events = [] attempts = {"primary": 0, "backup": 0} @@ -219,7 +219,7 @@ def test_on_failure_retry_cogitate(monkeypatch): def test_on_failure_retry_cogitate_uses_context_from_name(monkeypatch): - from think.agents import _execute_with_tools + from think.talents import _execute_with_tools events = [] seen = {} @@ -267,7 +267,7 @@ def test_on_failure_retry_cogitate_uses_context_from_name(monkeypatch): def test_on_failure_retry_generate(monkeypatch): - from think.agents import _execute_generate + from think.talents import _execute_generate events = [] calls = {"count": 0} @@ -310,7 +310,7 @@ def test_on_failure_retry_generate(monkeypatch): def test_on_failure_no_retry_value_error(monkeypatch): - from think.agents import _execute_generate + from think.talents import _execute_generate events = [] assert _is_retryable_error(ValueError("bad input")) is False @@ -338,7 +338,7 @@ def test_on_failure_no_retry_value_error(monkeypatch): def test_on_failure_both_fail_raises_original(monkeypatch): - from think.agents import _execute_generate + from think.talents import _execute_generate events = [] calls = {"count": 0} @@ -375,7 +375,7 @@ def test_on_failure_both_fail_raises_original(monkeypatch): def test_fallback_event_emitted(): - from think.agents import _run_agent + from think.talents import _run_talent events = [] config = { @@ -387,7 +387,7 @@ def test_fallback_event_emitted(): "fallback_from": "google", } - asyncio.run(_run_agent(config, events.append, dry_run=True)) + asyncio.run(_run_talent(config, events.append, dry_run=True)) fallback_events = [e for e in events if e.get("event") == "fallback"] assert len(fallback_events) == 1 @@ -395,7 +395,7 @@ def test_fallback_event_emitted(): def test_recheck_requested_on_stale(monkeypatch): - from think.agents import _execute_with_tools + from think.talents import _execute_with_tools async def pass_cogitate(*_args, **kwargs): on_event = kwargs.get("on_event") @@ -425,12 +425,12 @@ def test_recheck_requested_on_stale(monkeypatch): def test_main_async_no_duplicate_error_when_evented(monkeypatch, capsys): - from think.agents import main_async + from think.talents import main_async ndjson_input = json.dumps({"name": "unified", "prompt": "hello"}) monkeypatch.setattr("sys.stdin", StringIO(ndjson_input)) - async def fake_run_agent(_config, emit_event, dry_run=False): + async def fake_run_talent(_config, emit_event, dry_run=False): emit_event({"event": "error", "error": "provider failed"}) exc = RuntimeError("provider failed") setattr(exc, "_evented", True) @@ -441,16 +441,16 @@ def test_main_async_no_duplicate_error_when_evented(monkeypatch, capsys): mock_args.dry_run = False mock_args.subcommand = None - monkeypatch.setattr("think.agents.setup_cli", lambda _parser: mock_args) + monkeypatch.setattr("think.talents.setup_cli", lambda _parser: mock_args) monkeypatch.setattr( - "think.agents.setup_logging", + "think.talents.setup_logging", lambda _verbose=False: MagicMock(), ) monkeypatch.setattr( - "think.agents.prepare_config", lambda _request: {"type": "cogitate"} + "think.talents.prepare_config", lambda _request: {"type": "cogitate"} ) - monkeypatch.setattr("think.agents.validate_config", lambda _config: None) - monkeypatch.setattr("think.agents._run_agent", fake_run_agent) + monkeypatch.setattr("think.talents.validate_config", lambda _config: None) + monkeypatch.setattr("think.talents._run_talent", fake_run_talent) asyncio.run(main_async()) diff --git a/tests/test_agents_ndjson.py b/tests/test_talents_ndjson.py similarity index 91% rename from tests/test_agents_ndjson.py rename to tests/test_talents_ndjson.py index 66593fce4..63b26243b 100644 --- a/tests/test_agents_ndjson.py +++ b/tests/test_talents_ndjson.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Tests for NDJSON-only input in think.agents.""" +"""Tests for NDJSON-only input in think.talents.""" import asyncio import json @@ -19,7 +19,7 @@ def mock_journal(tmp_path, monkeypatch): """Set up a temporary journal directory.""" journal_path = tmp_path / "journal" journal_path.mkdir() - agents_path = journal_path / "agents" + agents_path = journal_path / "talents" agents_path.mkdir() monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_path)) @@ -87,7 +87,7 @@ def mock_all_providers(monkeypatch): monkeypatch.setitem(sys.modules, "agents", MagicMock()) # Mock prepare_config to avoid needing real agent configs - monkeypatch.setattr("think.agents.prepare_config", mock_prepare_config) + monkeypatch.setattr("think.talents.prepare_config", mock_prepare_config) def test_ndjson_single_request(mock_journal, monkeypatch, capsys): @@ -110,9 +110,9 @@ def test_ndjson_single_request(mock_journal, monkeypatch, capsys): mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() @@ -162,9 +162,9 @@ def test_ndjson_multiple_requests(mock_journal, monkeypatch, capsys): mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() @@ -198,9 +198,9 @@ not valid json mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() @@ -233,9 +233,9 @@ def test_ndjson_missing_prompt(mock_journal, monkeypatch, capsys): mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() @@ -263,9 +263,9 @@ def test_ndjson_empty_lines(mock_journal, monkeypatch, capsys): mock_all_providers(monkeypatch) - from think.agents import main_async + from think.talents import main_async - with patch("think.agents.setup_cli", return_value=mock_args): + with patch("think.talents.setup_cli", return_value=mock_args): asyncio.run(main_async()) captured = capsys.readouterr() diff --git a/tests/verify_api.py b/tests/verify_api.py index cd0e6bddd..6eb1a5781 100644 --- a/tests/verify_api.py +++ b/tests/verify_api.py @@ -46,8 +46,8 @@ ENDPOINTS = [ # apps/sol/routes.py { "app": "sol", - "name": "agents-day", - "path": "/app/sol/api/agents/20260304", + "name": "talents-day", + "path": "/app/sol/api/talents/20260304", "params": {"facet": "work"}, "status": 200, }, diff --git a/think/activities.py b/think/activities.py index fb96b9e80..1c34fc44c 100644 --- a/think/activities.py +++ b/think/activities.py @@ -639,7 +639,7 @@ def load_segment_activity_state( if not seg_dir: return None - state_path = seg_dir / "agents" / facet / "activity_state.json" + state_path = seg_dir / "talents" / facet / "activity_state.json" if not state_path.exists(): return None diff --git a/think/chat_cli.py b/think/chat_cli.py index 0b49ad428..a3cf5a812 100644 --- a/think/chat_cli.py +++ b/think/chat_cli.py @@ -10,7 +10,7 @@ import sys import threading from think.callosum import CallosumConnection -from think.cortex_client import cortex_request, read_agent_events +from think.cortex_client import cortex_request, read_use_events from think.utils import require_solstone, setup_cli @@ -43,13 +43,13 @@ def main() -> None: if args.facet: config["facet"] = args.facet - agent_id = cortex_request( + use_id = cortex_request( prompt=message, name=args.talent, provider=args.provider, config=config if config else None, ) - if agent_id is None: + if use_id is None: print( "Error: failed to connect to cortex (is the stack running?)", file=sys.stderr, @@ -63,7 +63,7 @@ def main() -> None: def on_event(msg: dict) -> None: if msg.get("tract") != "cortex": return - if msg.get("agent_id") != agent_id: + if msg.get("use_id") != use_id: return event_type = msg.get("event") @@ -122,7 +122,7 @@ def main() -> None: sys.exit(1) try: - events = read_agent_events(agent_id) + events = read_use_events(use_id) for event in reversed(events): event_type = event.get("event") if event_type == "finish": diff --git a/think/cluster.py b/think/cluster.py index adb4bd2e0..57ed56a60 100644 --- a/think/cluster.py +++ b/think/cluster.py @@ -217,9 +217,9 @@ def _process_segment( agent_filter = ( None if agents is True else agents if isinstance(agents, dict) else None ) - agents_dir = segment_path / "agents" - if agents_dir.is_dir(): - for md_file in sorted(agents_dir.rglob("*.md")): + talents_dir = segment_path / "talents" + if talents_dir.is_dir(): + for md_file in sorted(talents_dir.rglob("*.md")): if not md_file.is_file(): continue @@ -230,7 +230,7 @@ def _process_segment( try: content = md_file.read_text() if content.strip(): - rel_md_path = md_file.relative_to(agents_dir).as_posix() + rel_md_path = md_file.relative_to(talents_dir).as_posix() entries.append( { "timestamp": segment_start, @@ -240,7 +240,7 @@ def _process_segment( "prefix": "agent_output", "output_name": md_file.stem, "content": content, - "name": f"{segment_path.name}/agents/{rel_md_path}", + "name": f"{segment_path.name}/talents/{rel_md_path}", "stream": stream, } ) diff --git a/think/conversation.py b/think/conversation.py index f07fb41d3..a2c71cf00 100644 --- a/think/conversation.py +++ b/think/conversation.py @@ -57,14 +57,14 @@ def record_exchange( user_message: str = "", agent_response: str = "", talent: str = "", - agent_id: str = "", + use_id: str = "", ) -> None: """Record a conversation exchange to journal storage. Writes to two locations: 1. conversation/exchanges.jsonl — append-only quick-read index - 2. YYYYMMDD/conversation/HHMMSS_1/agents/conversation.md — journal entry - for FTS5 search indexing (matches */*/*/agents/*.md formatter pattern) + 2. YYYYMMDD/conversation/HHMMSS_1/talents/conversation.md — journal entry + for FTS5 search indexing (matches */*/*/talents/*.md formatter pattern) Also runs lightweight entity extraction on the conversation text. """ @@ -84,7 +84,7 @@ def record_exchange( "user_message": user_message, "agent_response": agent_response, "talent": talent, - "agent_id": agent_id, + "use_id": use_id, } # 1. Append to exchanges.jsonl (fast-read index) @@ -102,7 +102,7 @@ def record_exchange( time_key = dt.strftime("%H%M%S") segment = f"{time_key}_1" - seg_dir = day_path(day) / CONVERSATION_STREAM / segment / "agents" + seg_dir = day_path(day) / CONVERSATION_STREAM / segment / "talents" seg_dir.mkdir(parents=True, exist_ok=True) time_str = dt.strftime("%Y-%m-%d %H:%M:%S") diff --git a/think/cortex.py b/think/cortex.py index 06dfec8f0..967b651fd 100644 --- a/think/cortex.py +++ b/think/cortex.py @@ -1,17 +1,17 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Callosum-based agent process manager for solstone. +"""Callosum-based talent process manager for solstone. -Cortex listens for agent requests via the Callosum message bus and manages -agent process lifecycle: +Cortex listens for talent requests via the Callosum message bus and manages +talent process lifecycle: - Receives requests via Callosum (tract="cortex", event="request") -- Creates /_active.jsonl files to track active agents -- Spawns agent processes and captures their stdout events -- Broadcasts all agent events back to Callosum -- Renames to /.jsonl when complete +- Creates /_active.jsonl files to track active uses +- Spawns talent processes and captures their stdout events +- Broadcasts all talent events back to Callosum +- Renames to /.jsonl when complete -Agent files provide persistence and historical record, while Callosum provides +Talent files provide persistence and historical record, while Callosum provides real-time event distribution to all interested services. """ @@ -29,14 +29,15 @@ from typing import Any, Dict, Optional from think.callosum import CallosumConnection from think.runner import _atomic_symlink +from think.talents import TALENT_EXECUTION_MODULE from think.utils import get_journal, get_project_root, get_rev, now_ms -class AgentProcess: - """Manages a running agent subprocess.""" +class TalentProcess: + """Manages a running talent subprocess.""" - def __init__(self, agent_id: str, process: subprocess.Popen, log_path: Path): - self.agent_id = agent_id + def __init__(self, use_id: str, process: subprocess.Popen, log_path: Path): + self.use_id = use_id self.process = process self.log_path = log_path self.stop_event = threading.Event() @@ -62,23 +63,23 @@ class AgentProcess: self.process.wait(timeout=10) # Give more time for graceful shutdown except subprocess.TimeoutExpired: logging.getLogger(__name__).warning( - f"Agent {self.agent_id} didn't stop gracefully, killing" + f"Talent {self.use_id} didn't stop gracefully, killing" ) self.process.kill() self.process.wait() # Ensure zombie is reaped class CortexService: - """Callosum-based agent process manager.""" + """Callosum-based talent process manager.""" def __init__(self, journal_path: Optional[str] = None): self.journal_path = Path(journal_path or get_journal()) - self.agents_dir = self.journal_path / "agents" - self.agents_dir.mkdir(parents=True, exist_ok=True) + self.talents_dir = self.journal_path / "talents" + self.talents_dir.mkdir(parents=True, exist_ok=True) self.logger = logging.getLogger(__name__) - self.running_agents: Dict[str, AgentProcess] = {} - self.agent_requests: Dict[str, Dict[str, Any]] = {} # Store agent requests + self.running_uses: Dict[str, TalentProcess] = {} + self.use_requests: Dict[str, Dict[str, Any]] = {} # Store use requests self.lock = threading.RLock() self.stop_event = threading.Event() self.shutdown_requested = threading.Event() @@ -88,7 +89,7 @@ class CortexService: def _create_error_event( self, - agent_id: str, + use_id: str, error: str, trace: Optional[str] = None, exit_code: Optional[int] = None, @@ -97,7 +98,7 @@ class CortexService: event = { "event": "error", "ts": now_ms(), - "agent_id": agent_id, + "use_id": use_id, "error": error, } if trace: @@ -106,42 +107,44 @@ class CortexService: event["exit_code"] = exit_code return event - def _recover_orphaned_agents(self, active_files: list) -> None: - """Recover orphaned active agent files from a previous crash. + def _recover_orphaned_uses(self, active_files: list) -> None: + """Recover orphaned active talent files from a previous crash. Appends an error event to each file and renames to completed. """ for file_path in active_files: - agent_id = file_path.stem.replace("_active", "") + use_id = file_path.stem.replace("_active", "") try: error_event = self._create_error_event( - agent_id, "Recovered: Cortex restarted while agent was running" + use_id, "Recovered: Cortex restarted while talent was running" ) with open(file_path, "a") as f: f.write(json.dumps(error_event) + "\n") - completed_path = file_path.parent / f"{agent_id}.jsonl" + completed_path = file_path.parent / f"{use_id}.jsonl" file_path.rename(completed_path) - self.logger.warning(f"Recovered orphaned agent: {agent_id}") + self.logger.warning(f"Recovered orphaned talent: {use_id}") except Exception as e: - self.logger.error(f"Failed to recover agent {agent_id}: {e}") + self.logger.error(f"Failed to recover talent {use_id}: {e}") def start(self) -> None: - """Start listening for agent requests via Callosum.""" + """Start listening for talent requests via Callosum.""" # Recover any orphaned active files from previous crash - active_files = list(self.agents_dir.glob("*/*_active.jsonl")) + active_files = list(self.talents_dir.glob("*/*_active.jsonl")) if active_files: self.logger.warning( - f"Found {len(active_files)} orphaned agent(s), recovering..." + f"Found {len(active_files)} orphaned talent use(s), recovering..." ) - self._recover_orphaned_agents(active_files) + self._recover_orphaned_uses(active_files) # Connect to Callosum to receive requests try: self.callosum.start(callback=self._handle_callosum_message) self.logger.info("Connected to Callosum message bus") - self.callosum.emit("supervisor", "request", cmd=["sol", "agents", "check"]) - self.logger.info("Requested agents health check via supervisor") + self.callosum.emit( + "supervisor", "request", cmd=["sol", "providers", "check"] + ) + self.logger.info("Requested providers health check via supervisor") except Exception as e: self.logger.error(f"Failed to connect to Callosum: {e}") sys.exit(1) @@ -153,7 +156,7 @@ class CortexService: daemon=True, ).start() - self.logger.info("Cortex service started, listening for agent requests") + self.logger.info("Cortex service started, listening for talent requests") while True: try: @@ -162,9 +165,9 @@ class CortexService: # Exit when idle during shutdown if self.shutdown_requested.is_set(): with self.lock: - if len(self.running_agents) == 0: + if len(self.running_uses) == 0: self.logger.info( - "No agents running, exiting gracefully" + "No talent uses running, exiting gracefully" ) return break @@ -185,36 +188,36 @@ class CortexService: self.logger.exception(f"Error handling request: {e}") def _handle_request(self, request: Dict[str, Any]) -> None: - """Handle a new agent request from Callosum. + """Handle a new talent request from Callosum. Cortex is a minimal process manager - it only handles: - - File lifecycle (/_active.jsonl -> /.jsonl) + - File lifecycle (/_active.jsonl -> /.jsonl) - Process spawning and monitoring - Event relay to Callosum - All config loading, validation, and hydration is done by agents.py. + All config loading, validation, and hydration is done by think.talents. Cortex only resolves talent cwd early so the child process starts in the correct working directory. """ - agent_id = request.get("agent_id") - if not agent_id: - self.logger.error("Received request without agent_id") + use_id = request.get("use_id") + if not use_id: + self.logger.error("Received request without use_id") return - # Skip if this agent is already being processed + # Skip if this use is already being processed with self.lock: - if agent_id in self.running_agents: - self.logger.debug(f"Agent {agent_id} already running, skipping") + if use_id in self.running_uses: + self.logger.debug(f"Talent use {use_id} already running, skipping") return # Create _active.jsonl file (exclusive creation to prevent race conditions) name = request.get("name", "unified") safe_name = name.replace(":", "--") - agent_subdir = self.agents_dir / safe_name - agent_subdir.mkdir(parents=True, exist_ok=True) - file_path = agent_subdir / f"{agent_id}_active.jsonl" + talent_subdir = self.talents_dir / safe_name + talent_subdir.mkdir(parents=True, exist_ok=True) + file_path = talent_subdir / f"{use_id}_active.jsonl" if file_path.exists(): - self.logger.debug(f"Agent {agent_id} already claimed by another process") + self.logger.debug(f"Talent use {use_id} already claimed by another process") return try: @@ -223,24 +226,28 @@ class CortexService: except FileExistsError: return - self.logger.info(f"Processing agent request: {agent_id}") + self.logger.info(f"Processing talent request: {use_id}") # Store request for later use (output writing) with self.lock: - self.agent_requests[agent_id] = request + self.use_requests[use_id] = request - # Spawn agent process - it handles all validation/hydration + # Spawn talent process - it handles all validation/hydration try: self._spawn_subprocess( - agent_id, file_path, request, ["sol", "agents"], "agent" + use_id, + file_path, + request, + [sys.executable, "-m", TALENT_EXECUTION_MODULE], + "talent", ) except Exception as e: - self.logger.exception(f"Failed to spawn agent {agent_id}: {e}") - self._write_error_and_complete(file_path, f"Failed to spawn agent: {e}") + self.logger.exception(f"Failed to spawn talent {use_id}: {e}") + self._write_error_and_complete(file_path, f"Failed to spawn talent: {e}") def _spawn_subprocess( self, - agent_id: str, + use_id: str, file_path: Path, config: Dict[str, Any], cmd: list[str], @@ -249,16 +256,16 @@ class CortexService: """Spawn a subprocess and monitor its output. Args: - agent_id: Unique identifier for this process + use_id: Unique identifier for this process file_path: Path to the JSONL log file config: Configuration dict to pass via NDJSON stdin - cmd: Command to run (e.g., ["sol", "agents"]) - process_type: Label for logging ("agent") + cmd: Command to run (e.g., [sys.executable, "-m", TALENT_EXECUTION_MODULE]) + process_type: Label for logging ("talent") """ try: # Store the config for later use - thread safe with self.lock: - self.agent_requests[agent_id] = config + self.use_requests[use_id] = config # Pass the full config through as NDJSON ndjson_input = json.dumps(config) @@ -280,16 +287,16 @@ class CortexService: env.update({k: str(v) for k, v in env_overrides.items()}) # Spawn the subprocess - self.logger.info(f"Spawning {process_type} {agent_id}: {cmd}") + self.logger.info(f"Spawning {process_type} {use_id}: {cmd}") self.logger.debug(f"NDJSON input: {ndjson_input}") subprocess_cwd = None - if process_type == "agent": - from think.talent import get_agent + if process_type == "talent": + from think.talent import get_talent talent_key = str(config.get("name", "unified")) - talent_config = get_agent(talent_key) + talent_config = get_talent(talent_key) if talent_config.get("type") == "cogitate": - # Resolve here because prepare_config() runs inside sol agents. + # Resolve here because prepare_config() runs inside think.talents. cwd_value = talent_config.get("cwd") if cwd_value == "journal": try: @@ -321,15 +328,15 @@ class CortexService: process.stdin.close() # Track the running process - agent = AgentProcess(agent_id, process, file_path) + agent = TalentProcess(use_id, process, file_path) with self.lock: - self.running_agents[agent_id] = agent + self.running_uses[use_id] = agent # Set up timeout (default to 10 minutes if not specified) timeout_seconds = config.get("timeout_seconds", 600) agent.timeout_timer = threading.Timer( timeout_seconds, - lambda: self._timeout_agent(agent_id, agent, timeout_seconds), + lambda: self._timeout_talent(use_id, agent, timeout_seconds), ) agent.timeout_timer.start() @@ -343,26 +350,26 @@ class CortexService: ).start() self.logger.info( - f"{process_type.capitalize()} {agent_id} spawned successfully " + f"{process_type.capitalize()} {use_id} spawned successfully " f"(PID: {process.pid})" ) except Exception as e: - self.logger.exception(f"Failed to spawn {process_type} {agent_id}: {e}") + self.logger.exception(f"Failed to spawn {process_type} {use_id}: {e}") self._write_error_and_complete( file_path, f"Failed to spawn {process_type}: {e}" ) - def _timeout_agent( - self, agent_id: str, agent: AgentProcess, timeout_seconds: int + def _timeout_talent( + self, use_id: str, agent: TalentProcess, timeout_seconds: int ) -> None: - """Handle agent timeout.""" + """Handle talent timeout.""" if agent.is_running(): self.logger.warning( - f"Agent {agent_id} timed out after {timeout_seconds} seconds" + f"Talent {use_id} timed out after {timeout_seconds} seconds" ) error_event = self._create_error_event( - agent_id, f"Agent timed out after {timeout_seconds} seconds" + use_id, f"Talent timed out after {timeout_seconds} seconds" ) try: with open(agent.log_path, "a") as f: @@ -370,7 +377,7 @@ class CortexService: except Exception as e: self.logger.error(f"Failed to write timeout event: {e}") - # Broadcast to callosum so wait_for_agents detects immediately + # Broadcast to callosum so wait_for_uses detects immediately try: event_copy = error_event.copy() event_type = event_copy.pop("event", "error") @@ -380,8 +387,8 @@ class CortexService: agent.stop() - def _monitor_stdout(self, agent: AgentProcess) -> None: - """Monitor agent stdout and append events to the JSONL file.""" + def _monitor_stdout(self, agent: TalentProcess) -> None: + """Monitor talent stdout and append events to the JSONL file.""" if not agent.process.stdout: return @@ -399,18 +406,18 @@ class CortexService: # Parse JSON event event = json.loads(line) - # Ensure event has timestamp and agent_id + # Ensure event has timestamp and use_id if "ts" not in event: event["ts"] = now_ms() - if "agent_id" not in event: - event["agent_id"] = agent.agent_id + if "use_id" not in event: + event["use_id"] = agent.use_id # Inject agent name for WebSocket consumers with self.lock: - _req = self.agent_requests.get(agent.agent_id) + _req = self.use_requests.get(agent.use_id) if _req and "name" not in event: event["name"] = _req.get("name", "") - # Inject display mode for triage agent finish events + # Inject display mode for triage talent finish events if event.get("event") == "finish" and _req: try: from apps.home.events import TRIAGE_AGENT_NAMES @@ -441,17 +448,15 @@ class CortexService: if event.get("event") == "start": # Capture model and provider for status reporting with self.lock: - if agent.agent_id in self.agent_requests: + if agent.use_id in self.use_requests: model = event.get("model") if model: - self.agent_requests[agent.agent_id]["model"] = ( - model - ) + self.use_requests[agent.use_id]["model"] = model provider = event.get("provider") if provider: - self.agent_requests[agent.agent_id][ - "provider" - ] = provider + self.use_requests[agent.use_id]["provider"] = ( + provider + ) # Handle finish or error event if event.get("event") in ["finish", "error"]: @@ -461,8 +466,8 @@ class CortexService: # Get original request (thread-safe access) with self.lock: - original_request = self.agent_requests.get( - agent.agent_id + original_request = self.use_requests.get( + agent.use_id ) # Log token usage if available @@ -493,13 +498,13 @@ class CortexService: ) except Exception as e: self.logger.warning( - f"Failed to log token usage for agent {agent.agent_id}: {e}" + f"Failed to log token usage for talent {agent.use_id}: {e}" ) # Write output if requested if original_request and original_request.get("output"): self._write_output( - agent.agent_id, + agent.use_id, result, original_request, ) @@ -513,19 +518,17 @@ class CortexService: "event": "info", "ts": now_ms(), "message": line, - "agent_id": agent.agent_id, + "use_id": agent.use_id, } with open(agent.log_path, "a") as f: f.write(json.dumps(info_event) + "\n") except Exception as e: - self.logger.error( - f"Error monitoring stdout for agent {agent.agent_id}: {e}" - ) + self.logger.error(f"Error monitoring stdout for agent {agent.use_id}: {e}") finally: # Wait for process to fully exit (reaps zombie) exit_code = agent.process.wait() - self.logger.info(f"Agent {agent.agent_id} exited with code {exit_code}") + self.logger.info(f"Talent {agent.use_id} exited with code {exit_code}") # Check if finish event was emitted has_finish = self._has_finish_event(agent.log_path) @@ -533,26 +536,26 @@ class CortexService: if not has_finish: # Write error event if no finish using standardized format error_event = self._create_error_event( - agent.agent_id, - f"Agent exited with code {exit_code} without finish event", + agent.use_id, + f"Talent exited with code {exit_code} without finish event", exit_code=exit_code, ) with open(agent.log_path, "a") as f: f.write(json.dumps(error_event) + "\n") # Complete the file (rename from _active.jsonl to .jsonl) - self._complete_agent_file(agent.agent_id, agent.log_path) + self._complete_use_file(agent.use_id, agent.log_path) # Remove from running agents and clean up stored request (thread-safe) with self.lock: - if agent.agent_id in self.running_agents: - del self.running_agents[agent.agent_id] + if agent.use_id in self.running_uses: + del self.running_uses[agent.use_id] # Clean up stored request - if agent.agent_id in self.agent_requests: - del self.agent_requests[agent.agent_id] + if agent.use_id in self.use_requests: + del self.use_requests[agent.use_id] - def _monitor_stderr(self, agent: AgentProcess) -> None: - """Monitor agent stderr for errors.""" + def _monitor_stderr(self, agent: TalentProcess) -> None: + """Monitor talent stderr for errors.""" if not agent.process.stderr: return @@ -565,24 +568,22 @@ class CortexService: stripped = line.strip() if stripped: stderr_lines.append(stripped) - # Pass through to cortex stderr with agent prefix for traceability + # Pass through to cortex stderr with talent prefix for traceability print( - f"[agent:{agent.agent_id}:stderr] {stripped}", + f"[talent:{agent.use_id}:stderr] {stripped}", file=sys.stderr, flush=True, ) except Exception as e: - self.logger.error( - f"Error monitoring stderr for agent {agent.agent_id}: {e}" - ) + self.logger.error(f"Error monitoring stderr for agent {agent.use_id}: {e}") finally: # If process failed with stderr output, write error event if stderr_lines: exit_code = agent.process.poll() if exit_code is not None and exit_code != 0: error_event = self._create_error_event( - agent.agent_id, + agent.use_id, "Process failed with stderr output", trace="\n".join(stderr_lines), exit_code=exit_code, @@ -608,45 +609,45 @@ class CortexService: pass return False - def _complete_agent_file(self, agent_id: str, file_path: Path) -> None: - """Complete an agent by renaming the file from _active.jsonl to .jsonl.""" + def _complete_use_file(self, use_id: str, file_path: Path) -> None: + """Complete a talent use by renaming the file from _active.jsonl to .jsonl.""" try: - completed_path = file_path.parent / f"{agent_id}.jsonl" + completed_path = file_path.parent / f"{use_id}.jsonl" file_path.rename(completed_path) - self.logger.info(f"Completed agent {agent_id}: {completed_path}") + self.logger.info(f"Completed talent use {use_id}: {completed_path}") - # Create convenience symlink: {name}.log -> {name}/{agent_id}.jsonl - request = self.agent_requests.get(agent_id) + # Create convenience symlink: {name}.log -> {name}/{use_id}.jsonl + request = self.use_requests.get(use_id) if request: name = request.get("name") if name: safe_name = name.replace(":", "--") - link_path = self.agents_dir / f"{safe_name}.log" - _atomic_symlink(link_path, f"{safe_name}/{agent_id}.jsonl") + link_path = self.talents_dir / f"{safe_name}.log" + _atomic_symlink(link_path, f"{safe_name}/{use_id}.jsonl") self.logger.debug( - f"Symlinked {safe_name}.log -> {safe_name}/{agent_id}.jsonl" + f"Symlinked {safe_name}.log -> {safe_name}/{use_id}.jsonl" ) # Append summary to day index - self._append_day_index(agent_id, request, completed_path) + self._append_day_index(use_id, request, completed_path) else: self.logger.debug( - f"No name in request for {agent_id}, skipping symlink" + f"No name in request for {use_id}, skipping symlink" ) except Exception as e: - self.logger.error(f"Failed to complete agent file {agent_id}: {e}") + self.logger.error(f"Failed to complete talent file {use_id}: {e}") def _append_day_index( - self, agent_id: str, request: Dict[str, Any], completed_path: Path + self, use_id: str, request: Dict[str, Any], completed_path: Path ) -> None: - """Append agent summary to day index file.""" + """Append talent-use summary to the day index file.""" try: - # Determine day from request or agent_id timestamp + # Determine day from request or use_id timestamp day = request.get("day") if not day: from datetime import datetime - ts_seconds = int(agent_id) / 1000 + ts_seconds = int(use_id) / 1000 day = datetime.fromtimestamp(ts_seconds).strftime("%Y%m%d") start_ts = request.get("ts", 0) @@ -681,7 +682,7 @@ class CortexService: pass summary = { - "agent_id": agent_id, + "use_id": use_id, "name": request.get("name", "unified"), "day": day, "facet": request.get("facet"), @@ -693,33 +694,33 @@ class CortexService: "schedule": request.get("schedule"), } - day_index_path = self.agents_dir / f"{day}.jsonl" + day_index_path = self.talents_dir / f"{day}.jsonl" with open(day_index_path, "a") as f: f.write(json.dumps(summary) + "\n") f.flush() except Exception as e: - self.logger.error(f"Failed to append day index for {agent_id}: {e}") + self.logger.error(f"Failed to append day index for {use_id}: {e}") def _write_error_and_complete(self, file_path: Path, error_message: str) -> None: """Write an error event to the file and mark it as complete.""" try: - agent_id = file_path.stem.replace("_active", "") - error_event = self._create_error_event(agent_id, error_message) + use_id = file_path.stem.replace("_active", "") + error_event = self._create_error_event(use_id, error_message) with open(file_path, "a") as f: f.write(json.dumps(error_event) + "\n") # Complete the file - self._complete_agent_file(agent_id, file_path) + self._complete_use_file(use_id, file_path) except Exception as e: self.logger.error(f"Failed to write error and complete: {e}") - def _write_output(self, agent_id: str, result: str, config: Dict[str, Any]) -> None: - """Write agent output to config["output_path"]. + def _write_output(self, use_id: str, result: str, config: Dict[str, Any]) -> None: + """Write talent output to config["output_path"]. The output path is set by the caller — either derived by - prepare_config in agents.py (day/segment agents) or computed - by dream.py via get_activity_output_path (activity agents). + prepare_config in think.talents (day/segment talents) or computed + by dream.py via get_activity_output_path (activity talents). Cortex does not derive paths itself. """ output_path_str = config.get("output_path") @@ -733,10 +734,10 @@ class CortexService: with open(output_path, "w", encoding="utf-8") as f: f.write(result) - self.logger.info(f"Wrote agent {agent_id} output to {output_path}") + self.logger.info(f"Wrote talent {use_id} output to {output_path}") except Exception as e: - self.logger.error(f"Failed to write agent {agent_id} output: {e}") + self.logger.error(f"Failed to write talent {use_id} output: {e}") def stop(self) -> None: """Stop the Cortex service.""" @@ -746,9 +747,9 @@ class CortexService: if self.callosum: self.callosum.stop() - # Stop all running agents + # Stop all running talent uses with self.lock: - for agent in self.running_agents.values(): + for agent in self.running_uses.values(): agent.stop() def _emit_periodic_status(self) -> None: @@ -756,12 +757,12 @@ class CortexService: while not self.stop_event.is_set(): try: with self.lock: - agents = [] - for agent_id, agent_proc in self.running_agents.items(): - config = self.agent_requests.get(agent_id, {}) - agents.append( + uses = [] + for use_id, agent_proc in self.running_uses.items(): + config = self.use_requests.get(use_id, {}) + uses.append( { - "agent_id": agent_id, + "use_id": use_id, "name": config.get("name", "unknown"), "provider": config.get("provider", "unknown"), "elapsed_seconds": int( @@ -770,13 +771,13 @@ class CortexService: } ) - # Only emit status when there are active agents - if agents: + # Only emit status when there are active talent uses + if uses: self.callosum.emit( "cortex", "status", - running_agents=len(agents), - agents=agents, + running_uses=len(uses), + uses=uses, ) except Exception as e: self.logger.debug(f"Status emission failed: {e}") @@ -787,8 +788,8 @@ class CortexService: """Get service status information.""" with self.lock: return { - "running_agents": len(self.running_agents), - "agent_ids": list(self.running_agents.keys()), + "running_uses": len(self.running_uses), + "use_ids": list(self.running_uses.keys()), } @@ -798,7 +799,7 @@ def main() -> None: from think.utils import require_solstone, setup_cli - parser = argparse.ArgumentParser(description="solstone Cortex Agent Manager") + parser = argparse.ArgumentParser(description="solstone Cortex Talent Manager") args = setup_cli(parser) require_solstone() diff --git a/think/cortex_client.py b/think/cortex_client.py index 26429c6c0..6d4047346 100644 --- a/think/cortex_client.py +++ b/think/cortex_client.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Cortex client for managing AI agent requests.""" +"""Cortex client for managing AI talent requests.""" import json import logging @@ -18,16 +18,16 @@ logger = logging.getLogger(__name__) _last_ts = 0 -def _find_agent_file(agents_dir: Path, agent_id: str) -> tuple[Path | None, str]: - """Find an agent log file in per-agent subdirectories. +def _find_use_file(talents_dir: Path, use_id: str) -> tuple[Path | None, str]: + """Find a use log file in per-talent subdirectories. Returns: Tuple of (file_path, status) where status is "completed", "running", or "not_found". """ - for match in agents_dir.glob(f"*/{agent_id}.jsonl"): + for match in talents_dir.glob(f"*/{use_id}.jsonl"): return match, "completed" - for match in agents_dir.glob(f"*/{agent_id}_active.jsonl"): + for match in talents_dir.glob(f"*/{use_id}_active.jsonl"): return match, "running" return None, "not_found" @@ -38,23 +38,23 @@ def cortex_request( provider: Optional[str] = None, config: Optional[Dict[str, Any]] = None, ) -> str | None: - """Create a Cortex agent request via Callosum broadcast. + """Create a Cortex talent request via Callosum broadcast. Args: - prompt: The task or question for the agent - name: Agent name - system (e.g., "unified") or app-qualified (e.g., "entities:entity_assist") + prompt: The task or question for the talent + name: Talent name - system (e.g., "unified") or app-qualified (e.g., "entities:entity_assist") provider: AI provider - openai, google, or anthropic config: Provider-specific configuration (model, max_output_tokens, thinking_budget, etc.) Returns: - Agent ID (timestamp-based string), or None if the Callosum send failed. + Use ID (timestamp-based string), or None if the Callosum send failed. """ - # Get journal path (for agent_id uniqueness check) + # Get journal path (for use_id uniqueness check) journal_path = get_journal() - # Create agents directory if it doesn't exist - agents_dir = Path(journal_path) / "agents" - agents_dir.mkdir(parents=True, exist_ok=True) + # Create talents directory if it doesn't exist + talents_dir = Path(journal_path) / "talents" + talents_dir.mkdir(parents=True, exist_ok=True) # Generate monotonic timestamp in milliseconds, ensuring uniqueness global _last_ts @@ -65,13 +65,13 @@ def cortex_request( ts = _last_ts + 1 _last_ts = ts - agent_id = str(ts) + use_id = str(ts) # Build request object request = { "event": "request", "ts": ts, - "agent_id": agent_id, + "use_id": use_id, "prompt": prompt, "provider": provider, "name": name, @@ -91,49 +91,49 @@ def cortex_request( sent = callosum_send("cortex", "request", **request_fields) if not sent: - logger.info("Failed to send cortex request for agent '%s'", name) + logger.info("Failed to send cortex request for talent '%s'", name) return None - return agent_id + return use_id -def get_agent_log_status(agent_id: str) -> str: - """Get the status of a specific agent from its log file. +def get_use_log_status(use_id: str) -> str: + """Get the status of a specific use from its log file. Args: - agent_id: The agent ID (timestamp) + use_id: The use ID (timestamp) Returns: - "completed" - Agent finished (*.jsonl exists) - "running" - Agent still active (*_active.jsonl exists) - "not_found" - No agent file exists + "completed" - Use finished (*.jsonl exists) + "running" - Use still active (*_active.jsonl exists) + "not_found" - No use file exists """ - agents_dir = Path(get_journal()) / "agents" - _, status = _find_agent_file(agents_dir, agent_id) + talents_dir = Path(get_journal()) / "talents" + _, status = _find_use_file(talents_dir, use_id) return status -def wait_for_agents( - agent_ids: list[str], +def wait_for_uses( + use_ids: list[str], timeout: int | None = 600, ) -> tuple[dict[str, str], list[str]]: - """Wait for agents to complete via Callosum events. + """Wait for uses to complete via Callosum events. Listens for cortex.finish and cortex.error events. Sets up the event - listener first, then does an initial file check for agents that may have + listener first, then does an initial file check for uses that may have already completed, and a final file check at timeout as a backstop for any missed events. Args: - agent_ids: List of agent IDs to wait for + use_ids: List of use IDs to wait for timeout: Maximum wait time in seconds (default 600 = 10 minutes) Returns: Tuple of (completed, timed_out) where completed is a dict mapping - agent_id to end state ("finish" or "error"), and timed_out is a - list of agent IDs that did not complete within the timeout. + use_id to end state ("finish" or "error"), and timed_out is a + list of use IDs that did not complete within the timeout. """ - pending = set(agent_ids) + pending = set(use_ids) completed: dict[str, str] = {} lock = threading.Lock() all_done = threading.Event() @@ -141,16 +141,16 @@ def wait_for_agents( def on_message(msg: dict) -> None: if msg.get("tract") != "cortex": return - agent_id = msg.get("agent_id") - if not agent_id: + use_id = msg.get("use_id") + if not use_id: return event_type = msg.get("event") if event_type in ("finish", "error"): with lock: - if agent_id in pending: - completed[agent_id] = event_type - pending.discard(agent_id) + if use_id in pending: + completed[use_id] = event_type + pending.discard(use_id) if not pending: all_done.set() @@ -161,11 +161,11 @@ def wait_for_agents( try: # Initial file check (with lock since callback may be running) with lock: - for agent_id in list(pending): - end_state = get_agent_end_state(agent_id) + for use_id in list(pending): + end_state = get_use_end_state(use_id) if end_state in ("finish", "error"): - completed[agent_id] = end_state - pending.discard(agent_id) + completed[use_id] = end_state + pending.discard(use_id) if not pending: return completed, [] @@ -178,41 +178,41 @@ def wait_for_agents( # Final file check for any remaining (backstop for missed events) # Listener is stopped, so no lock needed - for agent_id in list(pending): - end_state = get_agent_end_state(agent_id) + for use_id in list(pending): + end_state = get_use_end_state(use_id) if end_state in ("finish", "error"): logger.info( - f"Agent {agent_id} completion event not received but agent completed" + f"Talent use {use_id} completion event not received but use completed" ) - completed[agent_id] = end_state - pending.discard(agent_id) + completed[use_id] = end_state + pending.discard(use_id) return completed, list(pending) -def get_agent_end_state(agent_id: str) -> str: - """Get how a completed agent ended (finish or error). +def get_use_end_state(use_id: str) -> str: + """Get how a completed use ended (finish or error). Checks file contents for terminal events even if file is still _active.jsonl, since Callosum broadcasts happen before file rename. Args: - agent_id: The agent ID (timestamp) + use_id: The use ID (timestamp) Returns: - "finish" - Agent completed successfully - "error" - Agent ended with an error - "running" - Agent is still active (no terminal event in file) - "unknown" - Agent file not found + "finish" - Use completed successfully + "error" - Use ended with an error + "running" - Use is still active (no terminal event in file) + "unknown" - Use file not found """ - status = get_agent_log_status(agent_id) + status = get_use_log_status(use_id) if status == "not_found": return "unknown" # Read events to find terminal state (even for "running" files that may # have finish event - Callosum broadcast happens before file rename) try: - events = read_agent_events(agent_id) + events = read_use_events(use_id) # Find last finish or error event for event in reversed(events): event_type = event.get("event") @@ -226,25 +226,25 @@ def get_agent_end_state(agent_id: str) -> str: return "unknown" -def read_agent_events(agent_id: str) -> list[Dict[str, Any]]: - """Read all events from an agent's JSONL log file. +def read_use_events(use_id: str) -> list[Dict[str, Any]]: + """Read all events from a use's JSONL log file. Args: - agent_id: The agent ID (timestamp) + use_id: The use ID (timestamp) Returns: List of event dictionaries in chronological order Raises: - FileNotFoundError: If agent log doesn't exist + FileNotFoundError: If the use log doesn't exist """ - agents_dir = Path(get_journal()) / "agents" - agent_file, _status = _find_agent_file(agents_dir, agent_id) - if agent_file is None: - raise FileNotFoundError(f"Agent log not found: {agent_id}") + talents_dir = Path(get_journal()) / "talents" + use_file, _status = _find_use_file(talents_dir, use_id) + if use_file is None: + raise FileNotFoundError(f"Talent log not found: {use_id}") events = [] - with open(agent_file, "r") as f: + with open(use_file, "r") as f: for line in f: line = line.strip() if not line: @@ -253,38 +253,38 @@ def read_agent_events(agent_id: str) -> list[Dict[str, Any]]: event = json.loads(line) events.append(event) except json.JSONDecodeError: - logger.debug(f"Skipping malformed JSON in {agent_file}") + logger.debug(f"Skipping malformed JSON in {use_file}") continue return events -def cortex_agents( +def cortex_uses( limit: int = 10, offset: int = 0, - agent_type: str = "all", + use_type: str = "all", facet: Optional[str] = None, ) -> Dict[str, Any]: - """List agents from the journal with pagination and filtering. + """List talent uses from the journal with pagination and filtering. Args: - limit: Maximum number of agents to return (1-100) - offset: Number of agents to skip - agent_type: Filter by "live", "historical", or "all" - facet: Optional facet to filter by. If provided, only returns agents + limit: Maximum number of uses to return (1-100) + offset: Number of uses to skip + use_type: Filter by "live", "historical", or "all" + facet: Optional facet to filter by. If provided, only returns uses that were run in this facet context. None means no filtering. Returns: - Dictionary with agents list and pagination info + Dictionary with use list and pagination info """ # Validate parameters limit = max(1, min(limit, 100)) offset = max(0, offset) - agents_dir = Path(get_journal()) / "agents" - if not agents_dir.exists(): + talents_dir = Path(get_journal()) / "talents" + if not talents_dir.exists(): return { - "agents": [], + "uses": [], "pagination": { "limit": limit, "offset": offset, @@ -295,15 +295,15 @@ def cortex_agents( "historical_count": 0, } - # Collect all agent files - all_agents = [] + # Collect all use files + all_uses = [] live_count = 0 historical_count = 0 - for agent_file in agents_dir.glob("*/*.jsonl"): + for use_file in talents_dir.glob("*/*.jsonl"): # Determine status from filename - is_active = "_active.jsonl" in agent_file.name - is_pending = "_pending.jsonl" in agent_file.name + is_active = "_active.jsonl" in use_file.name + is_pending = "_pending.jsonl" in use_file.name # Skip pending files if is_pending: @@ -318,17 +318,17 @@ def cortex_agents( historical_count += 1 # Filter by requested type - if agent_type == "live" and status != "running": + if use_type == "live" and status != "running": continue - if agent_type == "historical" and status != "completed": + if use_type == "historical" and status != "completed": continue - # Extract agent ID from filename - agent_id = agent_file.stem.replace("_active", "") + # Extract use ID from filename + use_id = use_file.stem.replace("_active", "") - # Read agent file to get request info and calculate runtime + # Read use file to get request info and calculate runtime try: - with open(agent_file, "r") as f: + with open(use_file, "r") as f: lines = f.readlines() if not lines: continue @@ -343,24 +343,24 @@ def cortex_agents( continue # Extract facet from request - agent_facet = request.get("facet") + use_facet = request.get("facet") # Filter by facet if specified - if facet is not None and agent_facet != facet: + if facet is not None and use_facet != facet: continue # Extract basic info - agent_info = { - "id": agent_id, + use_info = { + "id": use_id, "name": request.get("name", "unified"), "start": request.get("ts", 0), "status": status, "prompt": request.get("prompt", ""), "provider": request.get("provider", "openai"), - "facet": agent_facet, + "facet": use_facet, } - # For completed agents, find finish event to calculate runtime + # For completed uses, find finish event to calculate runtime if status == "completed" and len(lines) > 1: # Read last few lines to find finish event (reading backwards is more efficient) for line in reversed(lines[-10:]): # Check last 10 lines @@ -371,29 +371,29 @@ def cortex_agents( event = json.loads(line) if event.get("event") == "finish": end_ts = event.get("ts", 0) - if end_ts and agent_info["start"]: + if end_ts and use_info["start"]: # Calculate runtime in seconds - agent_info["runtime_seconds"] = ( - end_ts - agent_info["start"] + use_info["runtime_seconds"] = ( + end_ts - use_info["start"] ) / 1000.0 break except json.JSONDecodeError: continue - all_agents.append(agent_info) + all_uses.append(use_info) except (json.JSONDecodeError, IOError): # Skip malformed files continue # Sort by start time (newest first) - all_agents.sort(key=lambda x: x["start"], reverse=True) + all_uses.sort(key=lambda x: x["start"], reverse=True) # Apply pagination - total = len(all_agents) - paginated = all_agents[offset : offset + limit] + total = len(all_uses) + paginated = all_uses[offset : offset + limit] return { - "agents": paginated, + "uses": paginated, "pagination": { "limit": limit, "offset": offset, diff --git a/think/dream.py b/think/dream.py index ee44198c4..42972d15d 100644 --- a/think/dream.py +++ b/think/dream.py @@ -28,7 +28,7 @@ from think.activities import ( from think.activity_state_machine import ActivityStateMachine from think.callosum import CallosumConnection from think.cluster import cluster_segments -from think.cortex_client import cortex_request, wait_for_agents +from think.cortex_client import cortex_request, wait_for_uses from think.facets import ( get_active_facets, get_enabled_facets, @@ -76,7 +76,7 @@ class DreamJSONLWriter: if not self.file: return data = {"event": event, "ts": now_ms(), **fields} - if event == "agent.skip": + if event == "talent.skip": self.skip_count += 1 try: self.file.write(json.dumps(data, ensure_ascii=False) + "\n") @@ -102,8 +102,8 @@ def _jsonl_log(event: str, **fields) -> None: def _log_skip(name: str, reason: str, detail: str, **extra) -> None: - """Emit an agent.skip JSONL event.""" - _jsonl_log("agent.skip", name=name, reason=reason, detail=detail, **extra) + """Emit an talent.skip JSONL event.""" + _jsonl_log("talent.skip", name=name, reason=reason, detail=detail, **extra) def _update_status(**fields) -> None: @@ -227,19 +227,19 @@ def _cortex_request_with_retry(**kwargs) -> str | None: """Call cortex_request with retries on Callosum send failure. Retries up to len(_SEND_RETRY_DELAYS) times with short sleeps in between. - Returns the agent_id on success, or None if all attempts failed. + Returns the use_id on success, or None if all attempts failed. """ - agent_id = cortex_request(**kwargs) - if agent_id is not None: - return agent_id + use_id = cortex_request(**kwargs) + if use_id is not None: + return use_id name = kwargs.get("name", "unknown") for i, delay in enumerate(_SEND_RETRY_DELAYS, 1): logging.warning("Retrying cortex request for '%s' (attempt %d)", name, i + 1) time.sleep(delay) - agent_id = cortex_request(**kwargs) - if agent_id is not None: - return agent_id + use_id = cortex_request(**kwargs) + if use_id is not None: + return use_id logging.error("All cortex request attempts failed for '%s'", name) return None @@ -259,7 +259,7 @@ def _drain_priority_batch( emits completion events, and runs incremental indexing for generators. Args: - spawned: List of (agent_id, prompt_name, config, facet) tuples + spawned: List of (use_id, prompt_name, config, facet) tuples target_schedule: "segment" or "daily" day: Day in YYYYMMDD format segment: Optional segment key @@ -273,10 +273,10 @@ def _drain_priority_batch( if not spawned: return (0, 0, []) - agent_ids = [agent_id for agent_id, _, _, _ in spawned] + agent_ids = [use_id for use_id, _, _, _ in spawned] logging.info(f"Waiting for {len(agent_ids)} agents...") - completed, timed_out = wait_for_agents(agent_ids, timeout=timeout) + completed, timed_out = wait_for_uses(agent_ids, timeout=timeout) success = 0 failed = 0 @@ -285,59 +285,59 @@ def _drain_priority_batch( if timed_out: logging.warning(f"{len(timed_out)} agents timed out: {timed_out}") failed += len(timed_out) - for agent_id in timed_out: + for use_id in timed_out: timed_name = next( - (n for aid, n, _, _ in spawned if aid == agent_id), "unknown" + (n for aid, n, _, _ in spawned if aid == use_id), "unknown" ) - timed_facet = next((f for aid, _, _, f in spawned if aid == agent_id), None) + timed_facet = next((f for aid, _, _, f in spawned if aid == use_id), None) label = f"{timed_name}/{timed_facet}" if timed_facet else timed_name failed_names.append(f"{label} (timeout)") emit( - "agent_completed", + "talent_completed", mode=target_schedule, day=day, segment=segment, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", **({"facet": timed_facet} if timed_facet else {}), ) _jsonl_log( - "agent.fail", + "talent.fail", mode=target_schedule, day=day, segment=segment, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", **({"facet": timed_facet} if timed_facet else {}), ) - for agent_id, prompt_name, config, agent_facet in spawned: - if agent_id in timed_out: + for use_id, prompt_name, config, agent_facet in spawned: + if use_id in timed_out: continue - end_state = completed.get(agent_id, "unknown") + end_state = completed.get(use_id, "unknown") if end_state == "finish": logging.info(f"{prompt_name} completed successfully") success += 1 emit( - "agent_completed", + "talent_completed", mode=target_schedule, day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state="finish", **({"facet": agent_facet} if agent_facet else {}), ) _jsonl_log( - "agent.complete", + "talent.complete", mode=target_schedule, day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state="finish", **({"facet": agent_facet} if agent_facet else {}), ) @@ -368,22 +368,22 @@ def _drain_priority_batch( failed += 1 failed_names.append(f"{label} ({end_state})") emit( - "agent_completed", + "talent_completed", mode=target_schedule, day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, **({"facet": agent_facet} if agent_facet else {}), ) _jsonl_log( - "agent.fail", + "talent.fail", mode=target_schedule, day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, **({"facet": agent_facet} if agent_facet else {}), ) @@ -568,20 +568,20 @@ def run_segment_sense( return (0, 1, ["sense (send)"]) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, segment=segment, name="sense", - agent_id=sense_agent_id, + use_id=sense_agent_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, segment=segment, name="sense", - agent_id=sense_agent_id, + use_id=sense_agent_id, ) _update_status(current_agents=["sense"]) @@ -822,8 +822,8 @@ def run_segment_sense( spawned: list[tuple[str, str, dict, str | None]] = [] for agent_name, config in agents_to_run: - agent_id = _dispatch_agent(agent_name, config) - if agent_id is None: + use_id = _dispatch_agent(agent_name, config) + if use_id is None: _log_skip( agent_name, "send_failed", @@ -837,22 +837,22 @@ def run_segment_sense( _update_status(agents_completed=total_success + total_failed) continue - spawned.append((agent_id, agent_name, config, None)) + spawned.append((use_id, agent_name, config, None)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, segment=segment, name=agent_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, segment=segment, name=agent_name, - agent_id=agent_id, + use_id=use_id, ) if max_concurrency and len(spawned) >= max_concurrency: @@ -971,20 +971,20 @@ def run_segment_sense( _update_status(agents_completed=total_success + total_failed) else: emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, segment=segment, name="awareness_tender", - agent_id=at_agent_id, + use_id=at_agent_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, segment=segment, name="awareness_tender", - agent_id=at_agent_id, + use_id=at_agent_id, ) _update_status(current_agents=["awareness_tender"]) s, f, fn = _drain_priority_batch( @@ -1019,20 +1019,20 @@ def run_segment_sense( _update_status(agents_completed=total_success + total_failed) else: emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, segment=segment, name="pulse", - agent_id=pulse_agent_id, + use_id=pulse_agent_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, segment=segment, name="pulse", - agent_id=pulse_agent_id, + use_id=pulse_agent_id, ) _update_status(current_agents=["pulse"]) s, f, fn = _drain_priority_batch( @@ -1187,7 +1187,7 @@ def run_daily_prompts( spawned: list[ tuple[str, str, dict, str | None] - ] = [] # (agent_id, name, config, facet) + ] = [] # (use_id, name, config, facet) group_success = 0 group_failed = 0 @@ -1254,12 +1254,12 @@ def run_daily_prompts( else f"Processing facet '{facet_name}' for {day_formatted}: {input_summary}. Use get_facet('{facet_name}') to load context." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -1273,25 +1273,25 @@ def run_daily_prompts( f"{prompt_name}/{facet_name} (send)" ) continue - spawned.append((agent_id, prompt_name, config, facet_name)) + spawned.append((use_id, prompt_name, config, facet_name)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, facet=facet_name, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, facet=facet_name, ) logging.info( - f"Started {prompt_name} for {facet_name} (ID: {agent_id})" + f"Started {prompt_name} for {facet_name} (ID: {use_id})" ) # Drain batch when concurrency limit reached @@ -1338,12 +1338,12 @@ def run_daily_prompts( else f"Running scheduled task for {day_formatted}: {input_summary}." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -1354,22 +1354,22 @@ def run_daily_prompts( group_failed += 1 all_failed_names.append(f"{prompt_name} (send)") continue - spawned.append((agent_id, prompt_name, config, None)) + spawned.append((use_id, prompt_name, config, None)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) - logging.info(f"Started {prompt_name} (ID: {agent_id})") + logging.info(f"Started {prompt_name} (ID: {use_id})") # Drain batch when concurrency limit reached if max_concurrency and len(spawned) >= max_concurrency: @@ -1543,7 +1543,7 @@ def run_weekly_prompts( spawned: list[ tuple[str, str, dict, str | None] - ] = [] # (agent_id, name, config, facet) + ] = [] # (use_id, name, config, facet) group_success = 0 group_failed = 0 @@ -1610,12 +1610,12 @@ def run_weekly_prompts( else f"Processing facet '{facet_name}' for {day_formatted}: {input_summary}. Use get_facet('{facet_name}') to load context." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -1629,25 +1629,25 @@ def run_weekly_prompts( f"{prompt_name}/{facet_name} (send)" ) continue - spawned.append((agent_id, prompt_name, config, facet_name)) + spawned.append((use_id, prompt_name, config, facet_name)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, facet=facet_name, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, facet=facet_name, ) logging.info( - f"Started {prompt_name} for {facet_name} (ID: {agent_id})" + f"Started {prompt_name} for {facet_name} (ID: {use_id})" ) # Drain batch when concurrency limit reached @@ -1694,12 +1694,12 @@ def run_weekly_prompts( else f"Running scheduled task for {day_formatted}: {input_summary}." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -1710,22 +1710,22 @@ def run_weekly_prompts( group_failed += 1 all_failed_names.append(f"{prompt_name} (send)") continue - spawned.append((agent_id, prompt_name, config, None)) + spawned.append((use_id, prompt_name, config, None)) emit( - "agent_started", + "talent_started", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode=target_schedule, day=day, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) - logging.info(f"Started {prompt_name} (ID: {agent_id})") + logging.info(f"Started {prompt_name} (ID: {use_id})") # Drain batch when concurrency limit reached if max_concurrency and len(spawned) >= max_concurrency: @@ -1942,7 +1942,7 @@ def run_activity_prompts( count=len(prompts_list), ) - spawned: list[tuple[str, str, dict]] = [] # (agent_id, name, config) + spawned: list[tuple[str, str, dict]] = [] # (use_id, name, config) group_success = 0 group_failed = 0 @@ -1955,41 +1955,41 @@ def run_activity_prompts( agent_ids = [aid for aid, _, _ in spawned] logging.info(f"Waiting for {len(agent_ids)} agents...") - completed, timed_out = wait_for_agents(agent_ids, timeout=610) + completed, timed_out = wait_for_uses(agent_ids, timeout=610) if timed_out: logging.warning(f"{len(timed_out)} agents timed out") group_failed += len(timed_out) - for agent_id in timed_out: + for use_id in timed_out: timed_name = next( - (n for aid, n, _ in spawned if aid == agent_id), "unknown" + (n for aid, n, _ in spawned if aid == use_id), "unknown" ) emit( - "agent_completed", + "talent_completed", mode="activity", day=day, activity=activity_id, facet=facet, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", ) _jsonl_log( - "agent.fail", + "talent.fail", mode="activity", day=day, activity=activity_id, facet=facet, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", ) - for agent_id, prompt_name, config in spawned: - if agent_id in timed_out: + for use_id, prompt_name, config in spawned: + if use_id in timed_out: continue - end_state = completed.get(agent_id, "unknown") + end_state = completed.get(use_id, "unknown") if end_state == "finish": logging.info(f"{prompt_name} completed successfully") group_success += 1 @@ -2017,23 +2017,23 @@ def run_activity_prompts( group_failed += 1 emit( - "agent_completed", + "talent_completed", mode="activity", day=day, activity=activity_id, facet=facet, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, ) _jsonl_log( - "agent.complete" if end_state == "finish" else "agent.fail", + "talent.complete" if end_state == "finish" else "talent.fail", mode="activity", day=day, activity=activity_id, facet=facet, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, ) @@ -2078,12 +2078,12 @@ def run_activity_prompts( else f"Processing activity '{activity_id}' ({activity_type}) in facet '{facet}' for {day_formatted}." ) - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt=prompt, name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -2095,26 +2095,26 @@ def run_activity_prompts( ) total_failed += 1 continue - spawned.append((agent_id, prompt_name, config)) + spawned.append((use_id, prompt_name, config)) emit( - "agent_started", + "talent_started", mode="activity", day=day, activity=activity_id, facet=facet, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode="activity", day=day, activity=activity_id, facet=facet, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) - logging.info(f"Started {prompt_name} (ID: {agent_id})") + logging.info(f"Started {prompt_name} (ID: {use_id})") # Drain batch when concurrency limit reached if max_concurrency and len(spawned) >= max_concurrency: @@ -2234,7 +2234,7 @@ def run_flush_prompts( total_success = 0 total_failed = 0 - spawned: list[tuple[str, str, dict]] = [] # (agent_id, name, config) + spawned: list[tuple[str, str, dict]] = [] # (use_id, name, config) _update_status( mode="flush", day=day, @@ -2268,12 +2268,12 @@ def run_flush_prompts( if is_generate: request_config["output"] = config.get("output", "md") - agent_id = _cortex_request_with_retry( + use_id = _cortex_request_with_retry( prompt="", name=prompt_name, config=request_config, ) - if agent_id is None: + if use_id is None: _log_skip( prompt_name, "send_failed", @@ -2284,24 +2284,24 @@ def run_flush_prompts( ) total_failed += 1 continue - spawned.append((agent_id, prompt_name, config)) + spawned.append((use_id, prompt_name, config)) emit( - "agent_started", + "talent_started", mode="flush", day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) _jsonl_log( - "agent.dispatch", + "talent.dispatch", mode="flush", day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, ) - logging.info(f"Started flush agent {prompt_name} (ID: {agent_id})") + logging.info(f"Started flush agent {prompt_name} (ID: {use_id})") except Exception as e: logging.error(f"Failed to spawn flush agent {prompt_name}: {e}") @@ -2310,29 +2310,29 @@ def run_flush_prompts( if spawned: _update_status(current_agents=[name for _, name, _ in spawned]) agent_ids = [aid for aid, _, _ in spawned] - completed, timed_out = wait_for_agents(agent_ids, timeout=610) + completed, timed_out = wait_for_uses(agent_ids, timeout=610) if timed_out: logging.warning(f"Flush: {len(timed_out)} agents timed out") total_failed += len(timed_out) - for agent_id in timed_out: + for use_id in timed_out: timed_name = next( - (n for aid, n, _ in spawned if aid == agent_id), "unknown" + (n for aid, n, _ in spawned if aid == use_id), "unknown" ) _jsonl_log( - "agent.fail", + "talent.fail", mode="flush", day=day, segment=segment, name=timed_name, - agent_id=agent_id, + use_id=use_id, state="timeout", ) - for agent_id, prompt_name, config in spawned: - if agent_id in timed_out: + for use_id, prompt_name, config in spawned: + if use_id in timed_out: continue - end_state = completed.get(agent_id, "unknown") + end_state = completed.get(use_id, "unknown") if end_state == "finish": logging.info(f"Flush agent {prompt_name} completed") total_success += 1 @@ -2343,21 +2343,21 @@ def run_flush_prompts( total_failed += 1 emit( - "agent_completed", + "talent_completed", mode="flush", day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, ) _jsonl_log( - "agent.complete" if end_state == "finish" else "agent.fail", + "talent.complete" if end_state == "finish" else "talent.fail", mode="flush", day=day, segment=segment, name=prompt_name, - agent_id=agent_id, + use_id=use_id, state=end_state, ) _update_status( diff --git a/think/engage.py b/think/engage.py index 1121b3e2d..7f512262b 100644 --- a/think/engage.py +++ b/think/engage.py @@ -32,28 +32,28 @@ def _engage( from think.cortex_client import cortex_request - agent_id = cortex_request(prompt=prompt, name=name, config=config) - if agent_id is None: + use_id = cortex_request(prompt=prompt, name=name, config=config) + if use_id is None: typer.echo("Error: failed to send cortex request.", err=True) raise typer.Exit(1) if not wait: - typer.echo(agent_id) + typer.echo(use_id) return - from think.cortex_client import read_agent_events, wait_for_agents + from think.cortex_client import read_use_events, wait_for_uses - completed, timed_out = wait_for_agents([agent_id]) - if agent_id in timed_out: + completed, timed_out = wait_for_uses([use_id]) + if use_id in timed_out: typer.echo("Error: agent timed out.", err=True) raise typer.Exit(1) - end_state = completed.get(agent_id, "error") + end_state = completed.get(use_id, "error") if end_state != "finish": typer.echo(f"Error: agent ended with state: {end_state}", err=True) raise typer.Exit(1) - events = read_agent_events(agent_id) + events = read_use_events(use_id) result = "" for event in reversed(events): if event.get("event") == "finish": @@ -81,7 +81,7 @@ def engage( """Delegate work to a cogitate agent. Reads a prompt from stdin, sends it to cortex as an agent request. - By default, prints the agent_id and exits immediately (fire-and-forget). + By default, prints the use_id and exits immediately (fire-and-forget). Example:: diff --git a/think/entities/activity.py b/think/entities/activity.py index b17cf5f31..f8236165d 100644 --- a/think/entities/activity.py +++ b/think/entities/activity.py @@ -80,7 +80,7 @@ def parse_knowledge_graph_entities(day: str) -> list[str]: >>> parse_knowledge_graph_entities("20260108") ["Jeremie Miller (Jer)", "Neal Satterfield", "Flightline", ...] """ - kg_path = day_path(day, create=False) / "agents" / "knowledge_graph.md" + kg_path = day_path(day, create=False) / "talents" / "knowledge_graph.md" if not kg_path.exists(): return [] diff --git a/think/entities/context.py b/think/entities/context.py index f51e3a494..e36bee60b 100644 --- a/think/entities/context.py +++ b/think/entities/context.py @@ -38,7 +38,7 @@ def _active_entity_ids(facet: str, day: str, attached: list[dict]) -> set[str]: def _load_knowledge_graph(day: str) -> str: - kg_path = day_path(day, create=False) / "agents" / "knowledge_graph.md" + kg_path = day_path(day, create=False) / "talents" / "knowledge_graph.md" if not kg_path.exists(): return "No knowledge graph available for this day." diff --git a/think/events.py b/think/events.py index 76311b0d2..5839bbd68 100644 --- a/think/events.py +++ b/think/events.py @@ -129,7 +129,7 @@ def format_events( # For anticipations, show when it was created (from source path) if not occurred: source = event.get("source", "") - # Extract YYYYMMDD from source path like "20240101/agents/schedule.md" + # Extract YYYYMMDD from source path like "20240101/talents/schedule.md" source_match = re.match(r"(\d{8})/", source) if source_match: created_day = source_match.group(1) diff --git a/think/facets.py b/think/facets.py index 7698461be..7fc299c61 100644 --- a/think/facets.py +++ b/think/facets.py @@ -119,7 +119,7 @@ def _write_action_log( source: str, actor: str, day: str | None = None, - agent_id: str | None = None, + use_id: str | None = None, ) -> None: """Write action to the daily audit log. @@ -136,7 +136,7 @@ def _write_action_log( source: Origin type - "tool" for agents, "app" for web UI actor: For tools: agent name. For apps: app name day: Day in YYYYMMDD format (defaults to today) - agent_id: Optional agent ID (only for tool actions) + use_id: Optional agent ID (only for tool actions) """ journal = get_journal() @@ -165,9 +165,9 @@ def _write_action_log( if facet is not None: entry["facet"] = facet - # Add agent_id only if available - if agent_id is not None: - entry["agent_id"] = agent_id + # Add use_id only if available + if use_id is not None: + entry["use_id"] = use_id # Append to log file with open(log_path, "a", encoding="utf-8") as f: @@ -476,13 +476,13 @@ def load_segment_facets(day: str, segment: str, stream: str | None = None) -> li List of facet ID strings found in the segment's facets.json """ if stream: - candidates = [day_path(day) / stream / segment / "agents" / "facets.json"] + candidates = [day_path(day) / stream / segment / "talents" / "facets.json"] else: # Search all streams for this segment candidates = [] for _s, seg_key, seg_path in iter_segments(day): if seg_key == segment: - candidates.append(seg_path / "agents" / "facets.json") + candidates.append(seg_path / "talents" / "facets.json") for facets_file in candidates: if not facets_file.exists(): @@ -564,7 +564,7 @@ def aggregate_speculative_facets(days: list[str] | None = None) -> list[dict]: for day in scan_days: for _stream, _seg_key, seg_path in iter_segments(day): - facets_file = seg_path / "agents" / "facets.json" + facets_file = seg_path / "talents" / "facets.json" if not facets_file.exists(): continue @@ -1066,7 +1066,7 @@ def format_logs( source = entry.get("source", "unknown") actor = entry.get("actor", "unknown") params = entry.get("params", {}) - agent_id = entry.get("agent_id") + use_id = entry.get("use_id") # Format action name for display (e.g., "todo_add" -> "Todo Add") action_display = action.replace("_", " ").title() @@ -1081,8 +1081,8 @@ def format_logs( lines.append(" | ".join(meta_parts)) # Agent link if present - if agent_id: - lines.append(f"**Agent:** [{agent_id}](/app/sol/{agent_id})") + if use_id: + lines.append(f"**Talent:** [{use_id}](/app/sol/{use_id})") lines.append("") diff --git a/think/formatters.py b/think/formatters.py index 8d96c2cbe..6000357f7 100644 --- a/think/formatters.py +++ b/think/formatters.py @@ -51,7 +51,7 @@ def extract_path_metadata(rel_path: str) -> dict[str, str]: by the formatter via meta["indexer"]["agent"]. Args: - rel_path: Journal-relative path (e.g., "20240101/agents/flow.md") + rel_path: Journal-relative path (e.g., "20240101/talents/flow.md") Returns: Dict with keys: day, facet, agent @@ -72,11 +72,11 @@ def extract_path_metadata(rel_path: str) -> dict[str, str]: if parts[0] and DATE_RE.fullmatch(parts[0]): day = parts[0] - # Extract facet from agents/{facet}/... paths + # Extract facet from talents/{facet}/... paths try: - agents_idx = parts.index("agents") - if agents_idx + 2 < len(parts): - facet = parts[agents_idx + 1] + talents_idx = parts.index("talents") + if talents_idx + 2 < len(parts): + facet = parts[talents_idx + 1] except ValueError: pass @@ -191,15 +191,15 @@ FORMATTERS: dict[str, tuple[str, str, bool]] = { "*/*/*/*_transcript.jsonl": ("observe.hear", "format_audio", False), "*/*/*/screen.jsonl": ("observe.screen", "format_screen", False), "*/*/*/*_screen.jsonl": ("observe.screen", "format_screen", False), - # Markdown — day-level agents output and segment-level (day/stream/segment/agents/) - "*/agents/*.md": ("think.markdown", "format_markdown", True), - # Layout: day/stream/segment/agents/*.md - "*/*/*/agents/*.md": ("think.markdown", "format_markdown", True), - "*/*/*/agents/*/*.md": ("think.markdown", "format_markdown", True), + # Markdown — day-level agents output and segment-level (day/stream/segment/talents/) + "*/talents/*.md": ("think.markdown", "format_markdown", True), + # Layout: day/stream/segment/talents/*.md + "*/*/*/talents/*.md": ("think.markdown", "format_markdown", True), + "*/*/*/talents/*/*.md": ("think.markdown", "format_markdown", True), "facets/*/activities/*/*/*.md": ("think.markdown", "format_markdown", True), "facets/*/news/*.md": ("think.markdown", "format_markdown", True), "imports/*/summary.md": ("think.markdown", "format_markdown", True), - "apps/*/agents/*.md": ("think.markdown", "format_markdown", True), + "apps/*/talents/*.md": ("think.markdown", "format_markdown", True), } _DAY_ROOTED_PATTERNS = [p for p in FORMATTERS if p.startswith("*/")] @@ -212,7 +212,7 @@ def get_formatter(file_path: str) -> Callable | None: Matches against registered glob patterns (regardless of indexed flag). Args: - file_path: Journal-relative path (e.g., "20240101/agents/flow.md") + file_path: Journal-relative path (e.g., "20240101/talents/flow.md") Returns: Formatter function or None if no pattern matches diff --git a/think/heartbeat.py b/think/heartbeat.py index 2c11fa9e2..dff461271 100644 --- a/think/heartbeat.py +++ b/think/heartbeat.py @@ -14,7 +14,7 @@ from datetime import datetime from pathlib import Path from think.awareness import ensure_sol_directory -from think.cortex_client import cortex_request, wait_for_agents +from think.cortex_client import cortex_request, wait_for_uses from think.utils import get_journal, require_solstone, setup_cli logger = logging.getLogger(__name__) @@ -104,27 +104,27 @@ def main() -> None: pid_file.write_text(str(os.getpid())) start_time = time.monotonic() - agent_id = cortex_request( + use_id = cortex_request( prompt="Run heartbeat check.", name="heartbeat", ) - if agent_id is None: + if use_id is None: logger.error("Failed to send heartbeat request to cortex") _log_run(health_dir, start_time, "error") sys.exit(1) - logger.info("Heartbeat agent started (ID: %s)", agent_id) + logger.info("Heartbeat agent started (ID: %s)", use_id) # Wait for completion - completed, timed_out = wait_for_agents([agent_id], timeout=600) + completed, timed_out = wait_for_uses([use_id], timeout=600) # Determine outcome - if agent_id in timed_out: + if use_id in timed_out: logger.error("Heartbeat agent timed out") _log_run(health_dir, start_time, "timeout") sys.exit(2) - end_state = completed.get(agent_id, "unknown") + end_state = completed.get(use_id, "unknown") if end_state == "finish": logger.info("Heartbeat completed successfully") _log_run(health_dir, start_time, "success") diff --git a/think/hooks.py b/think/hooks.py index 2737ab3b2..e6cf94b2e 100644 --- a/think/hooks.py +++ b/think/hooks.py @@ -190,7 +190,7 @@ def compute_output_source(context: dict) -> str: context: Hook context dict with day, segment, name, output_path, meta. Returns: - Relative path like "20240101/agents/meetings.md". + Relative path like "20240101/talents/meetings.md". """ from think.talent import get_output_name from think.utils import CHRONICLE_DIR, get_journal @@ -206,14 +206,14 @@ def compute_output_source(context: dict) -> str: except ValueError: segment = context.get("segment") output_name = get_output_name(name) - # Check for facet in meta (for multi-facet agents) + # Check for facet in meta (for multi-facet talents) meta = context.get("meta", {}) facet = meta.get("facet") if meta else None filename = f"{output_name}.md" if segment and facet: - return os.path.join(day, segment, "agents", facet, filename) + return os.path.join(day, segment, "talents", facet, filename) if segment: - return os.path.join(day, segment, "agents", filename) + return os.path.join(day, segment, "talents", filename) if facet: - return os.path.join(day, "agents", facet, filename) - return os.path.join(day, "agents", filename) + return os.path.join(day, "talents", facet, filename) + return os.path.join(day, "talents", filename) diff --git a/think/indexer/journal.py b/think/indexer/journal.py index bcc595494..cdaeeefc6 100644 --- a/think/indexer/journal.py +++ b/think/indexer/journal.py @@ -258,7 +258,7 @@ def _find_signal_files(journal: str) -> dict[str, tuple[str, str]]: else journal_path ) - for path in day_root.glob("*/agents/knowledge_graph.md"): + for path in day_root.glob("*/talents/knowledge_graph.md"): if path.is_file(): rel = path.relative_to(day_root).as_posix() files[rel] = (str(path), "kg") @@ -972,7 +972,7 @@ def _extract_stream(journal: str, rel: str) -> str | None: """Extract stream name from a journal-relative path's segment directory. Reads stream.json from the segment dir if the path is inside a segment - (e.g., "20240101/142500_300/agents/facet/flow.md"). + (e.g., "20240101/142500_300/talents/facet/flow.md"). Returns stream name string or None for non-segment paths or pre-stream segments. """ @@ -1052,18 +1052,18 @@ def _index_segment_chunks( ) -> int: """Index concatenated markdown content for one segment.""" segment_path = Path(segment_dir) - agent_files = sorted( + talent_files = sorted( [ - *segment_path.glob("agents/*.md"), - *segment_path.glob("agents/*/*.md"), + *segment_path.glob("talents/*.md"), + *segment_path.glob("talents/*/*.md"), ], key=lambda path: str(path), ) - if not agent_files: + if not talent_files: return 0 content = "\n\n---\n\n".join( - path.read_text(encoding="utf-8") for path in agent_files + path.read_text(encoding="utf-8") for path in talent_files ) chunks, _meta = format_markdown(content) day = rel_segment.replace("\\", "/").split("/")[0] @@ -1324,7 +1324,7 @@ def scan_signals( for path in db_signal_paths if not _is_historical_signal_file( path.split("#")[0], - "kg" if "/agents/knowledge_graph.md" in path else "event", + "kg" if "/talents/knowledge_graph.md" in path else "event", ) } removed = {p for p in in_scope_db if p.split("#")[0] not in in_scope} @@ -1506,7 +1506,7 @@ def consolidate_segment_entities(journal: str, full: bool = False) -> int: # Collect all matching segment entity files across day/stream/segment dirs segment_files = [] - for path in day_root.glob("**/agents/entities.jsonl"): + for path in day_root.glob("**/talents/entities.jsonl"): if not path.is_file(): continue try: diff --git a/think/journal_stats.py b/think/journal_stats.py index a16dc0dd2..ee670c172 100644 --- a/think/journal_stats.py +++ b/think/journal_stats.py @@ -12,9 +12,9 @@ from typing import Dict from observe.sense import scan_day as sense_scan_day from observe.utils import VIDEO_EXTENSIONS, load_analysis_frames -from think.agents import scan_day as generate_scan_day from think.stats_schema import DAY_FIELDS, SCHEMA_VERSION from think.stats_schema import validate as validate_stats +from think.talents import scan_day as generate_scan_day from think.utils import day_dirs, get_journal, setup_cli logger = logging.getLogger(__name__) @@ -54,12 +54,12 @@ class JournalStats: for ext in VIDEO_EXTENSIONS: files.extend(day_dir.glob(f"*{ext}")) - agents_dir = day_dir / "agents" - if agents_dir.is_dir(): - files.extend(agents_dir.glob("*.json")) - files.extend(agents_dir.glob("*.md")) - files.extend(agents_dir.glob("*/*.json")) - files.extend(agents_dir.glob("*/*.md")) + talents_dir = day_dir / "talents" + if talents_dir.is_dir(): + files.extend(talents_dir.glob("*.json")) + files.extend(talents_dir.glob("*.md")) + files.extend(talents_dir.glob("*/*.json")) + files.extend(talents_dir.glob("*/*.md")) # Check facet event files for this day journal_root = Path(get_journal()) @@ -524,7 +524,7 @@ class JournalStats: "by_day": self.token_usage, "by_model": self.token_totals, }, - "agents": { + "talents": { "counts": dict(self.agent_counts), "minutes": {k: round(v, 2) for k, v in self.agent_minutes.items()}, "counts_by_day": self.agent_counts_by_day, diff --git a/think/models.py b/think/models.py index b5cc38052..e770e511e 100644 --- a/think/models.py +++ b/think/models.py @@ -1082,7 +1082,7 @@ def request_health_recheck() -> None: """ try: subprocess.Popen( - ["sol", "agents", "check", "--targeted"], + ["sol", "providers", "check", "--targeted"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) diff --git a/think/pipeline_health.py b/think/pipeline_health.py index 64dea989c..31473f5f8 100644 --- a/think/pipeline_health.py +++ b/think/pipeline_health.py @@ -28,7 +28,7 @@ def summarize_pipeline_day(day: str) -> dict: "status": "healthy", "anomalies": [], "runs": {mode: {"count": 0, "duration_ms_total": 0} for mode in _MODES}, - "agents": { + "talents": { "dispatched": 0, "completed": 0, "failed": 0, @@ -39,7 +39,7 @@ def summarize_pipeline_day(day: str) -> dict: "activities": { "detected": 0, "persisted": 0, - "agents_fired": False, + "talents_fired": False, }, } @@ -78,25 +78,26 @@ def summarize_pipeline_day(day: str) -> dict: continue event = rec["event"] - if event == "agent.dispatch": - summary["agents"]["dispatched"] += 1 - elif event == "agent.complete": - summary["agents"]["completed"] += 1 - elif event == "agent.fail": - summary["agents"]["failed"] += 1 - if len(summary["agents"]["failed_list"]) < _FAILED_LIST_CAP: - summary["agents"]["failed_list"].append( + # HISTORICAL SHIM: accept legacy agent.* chronicle event names from before 2026-04-17; sunset 2026-05-01 + if event in {"agent.dispatch", "talent.dispatch"}: + summary["talents"]["dispatched"] += 1 + elif event in {"agent.complete", "talent.complete"}: + summary["talents"]["completed"] += 1 + elif event in {"agent.fail", "talent.fail"}: + summary["talents"]["failed"] += 1 + if len(summary["talents"]["failed_list"]) < _FAILED_LIST_CAP: + summary["talents"]["failed_list"].append( { "mode": rec.get("mode") or mode, "name": rec.get("name"), - "agent_id": rec.get("agent_id"), + "use_id": rec.get("use_id"), "state": rec.get("state"), } ) else: - summary["agents"]["failed_list_truncated"] = True - elif event == "agent.skip": - summary["agents"]["skipped"] += 1 + summary["talents"]["failed_list_truncated"] = True + elif event in {"agent.skip", "talent.skip"}: + summary["talents"]["skipped"] += 1 elif event == "activity.detected": summary["activities"]["detected"] += 1 elif event == "activity.persisted": @@ -110,7 +111,7 @@ def summarize_pipeline_day(day: str) -> dict: elif ( event == "run.start" and (rec.get("mode") or mode) == "activity" ): - summary["activities"]["agents_fired"] = True + summary["activities"]["talents_fired"] = True except Exception: logger.warning( "pipeline_health: unexpected error summarizing %s", @@ -119,8 +120,8 @@ def summarize_pipeline_day(day: str) -> dict: ) return summary - for failure in summary["agents"]["failed_list"]: - summary["anomalies"].append({"kind": "agent_failure", **failure}) + for failure in summary["talents"]["failed_list"]: + summary["anomalies"].append({"kind": "talent_failure", **failure}) if ( summary["activities"]["detected"] > 0 @@ -149,7 +150,7 @@ def summarize_pipeline_day(day: str) -> dict: for anomaly in summary["anomalies"] ) has_failure = any( - anomaly["kind"] == "agent_failure" for anomaly in summary["anomalies"] + anomaly["kind"] == "talent_failure" for anomaly in summary["anomalies"] ) if has_stale: summary["status"] = "stale" @@ -175,11 +176,11 @@ def pipeline_status_message(summary: dict) -> dict | None: "status": "stale", "message": "Daily processing hasn't run yet", } - if any(anomaly.get("kind") == "agent_failure" for anomaly in anomalies): - count = summary.get("agents", {}).get("failed", 0) + if any(anomaly.get("kind") == "talent_failure" for anomaly in anomalies): + count = summary.get("talents", {}).get("failed", 0) plural = "s" if count != 1 else "" return { "status": "warning", - "message": f"{count} agent error{plural} today", + "message": f"{count} talent error{plural} today", } return None diff --git a/think/providers/anthropic.py b/think/providers/anthropic.py index 0ae37fcb5..25f7f4bfe 100644 --- a/think/providers/anthropic.py +++ b/think/providers/anthropic.py @@ -4,7 +4,7 @@ """Anthropic Claude provider for agents and direct LLM generation. -This module provides the Anthropic Claude provider for the ``sol agents`` CLI +This module provides the Anthropic Claude provider for the ``sol providers check`` CLI and run_generate/run_agenerate functions returning GenerateResult. Common Parameters diff --git a/think/providers/google.py b/think/providers/google.py index fa43766f7..5e30ffcf1 100644 --- a/think/providers/google.py +++ b/think/providers/google.py @@ -4,7 +4,7 @@ """Gemini provider for agents and direct LLM generation. -This module provides the Google Gemini provider for the ``sol agents`` CLI +This module provides the Google Gemini provider for the ``sol providers check`` CLI and run_generate/run_agenerate functions returning GenerateResult. Common Parameters diff --git a/think/providers/openai.py b/think/providers/openai.py index 725db492e..32e029cd8 100644 --- a/think/providers/openai.py +++ b/think/providers/openai.py @@ -4,7 +4,7 @@ """OpenAI provider for agents and direct LLM generation. -This module provides the OpenAI provider for the ``sol agents`` CLI +This module provides the OpenAI provider for the ``sol providers check`` CLI and run_generate/run_agenerate functions returning GenerateResult. Common Parameters @@ -54,7 +54,7 @@ from .shared import ( safe_raw, ) -# Agent configuration is now loaded via get_agent() in cortex.py +# Agent configuration is now loaded via get_talent() in cortex.py LOG = logging.getLogger("think.providers.openai") diff --git a/think/providers/shared.py b/think/providers/shared.py index 46a7e75e0..a50676a9e 100644 --- a/think/providers/shared.py +++ b/think/providers/shared.py @@ -4,7 +4,7 @@ """Shared utilities and types for AI providers. This module contains: -- Event TypedDicts emitted by providers during agent execution +- Event TypedDicts emitted by providers during talent execution - GenerateResult TypedDict returned by run_generate/run_agenerate - JSONEventCallback for event emission - Utility functions for common provider operations @@ -48,7 +48,7 @@ class ToolEndEvent(TypedDict, total=False): class StartEvent(TypedDict, total=False): - """Event emitted when an agent run begins.""" + """Event emitted when a talent run begins.""" event: Required[Literal["start"]] ts: Required[int] @@ -62,7 +62,7 @@ class StartEvent(TypedDict, total=False): class FinishEvent(TypedDict, total=False): - """Event emitted when an agent run finishes successfully.""" + """Event emitted when a talent run finishes successfully.""" event: Required[Literal["finish"]] ts: Required[int] @@ -82,12 +82,12 @@ class ErrorEvent(TypedDict, total=False): raw: Optional[list[dict[str, Any]]] # Original provider JSON event(s) -class AgentUpdatedEvent(TypedDict, total=False): - """Event emitted when the agent context changes.""" +class TalentUpdatedEvent(TypedDict, total=False): + """Event emitted when the talent context changes.""" - event: Required[Literal["agent_updated"]] + event: Required[Literal["talent_updated"]] ts: Required[int] - agent: Required[str] + talent: Required[str] raw: Optional[list[dict[str, Any]]] # Original provider JSON event(s) @@ -127,7 +127,7 @@ Event = Union[ FinishEvent, ErrorEvent, ThinkingEvent, - AgentUpdatedEvent, + TalentUpdatedEvent, FallbackEvent, ] diff --git a/think/providers_cli.py b/think/providers_cli.py new file mode 100644 index 000000000..0fc5eb267 --- /dev/null +++ b/think/providers_cli.py @@ -0,0 +1,337 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""CLI entrypoint for provider connectivity checks.""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import shutil +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +from think.utils import get_journal, require_solstone, setup_cli + + +def _check_generate(provider_name: str, tier: int, timeout: int) -> tuple[str, str]: + """Check generate interface for a provider.""" + from think.models import PROVIDER_DEFAULTS + from think.providers import PROVIDER_METADATA, get_provider_module + + env_key = PROVIDER_METADATA[provider_name]["env_key"] + if env_key and not os.getenv(env_key): + label = PROVIDER_METADATA[provider_name]["label"] + return "skip", f"{label} not configured (no {env_key})" + + if not env_key: + from think.providers import validate_key + + result = validate_key(provider_name, "") + if not result.get("valid"): + return ( + "skip", + f"Ollama not reachable ({result.get('error', 'unreachable')})", + ) + + try: + module = get_provider_module(provider_name) + model = PROVIDER_DEFAULTS[provider_name][tier] + result = module.run_generate( + contents="Say OK", + model=model, + temperature=0, + max_output_tokens=16, + system_instruction=None, + json_output=False, + thinking_budget=None, + timeout_s=timeout, + ) + text = result.get("text", "") if isinstance(result, dict) else "" + if text: + usage = result.get("usage") if isinstance(result, dict) else None + if usage: + from think.models import log_token_usage + + log_token_usage( + model=PROVIDER_DEFAULTS[provider_name][tier], + usage=usage, + context="health.check.generate", + type="generate", + ) + return "ok", "OK" + return "fail", "FAIL: empty response text" + except Exception as exc: + return "fail", f"FAIL: {exc}" + + +async def _check_cogitate( + provider_name: str, tier: int, timeout: int +) -> tuple[str, str]: + """Check cogitate interface for a provider by running a real prompt.""" + from think.models import PROVIDER_DEFAULTS + from think.providers import PROVIDER_METADATA, get_provider_module + + env_key = PROVIDER_METADATA[provider_name]["env_key"] + if env_key and not os.getenv(env_key): + label = PROVIDER_METADATA[provider_name]["label"] + return "skip", f"{label} not configured (no {env_key})" + + if not env_key: + from think.providers import validate_key + + result = validate_key(provider_name, "") + if not result.get("valid"): + return ( + "skip", + f"Ollama not reachable ({result.get('error', 'unreachable')})", + ) + + binary = PROVIDER_METADATA[provider_name].get("cogitate_cli", "") + if binary and not shutil.which(binary): + return "skip", f"{binary} CLI not installed" + + try: + module = get_provider_module(provider_name) + model = PROVIDER_DEFAULTS[provider_name][tier] + config = {"prompt": "Say OK", "model": model} + result = await asyncio.wait_for( + module.run_cogitate(config=config, on_event=None), + timeout=timeout, + ) + if result: + return "ok", "OK" + return "fail", "FAIL: empty response" + except asyncio.TimeoutError: + return "fail", f"FAIL: timed out after {timeout}s" + except Exception as exc: + return "fail", f"FAIL: {exc}" + + +async def _run_check(args: argparse.Namespace) -> None: + """Run connectivity checks against AI providers.""" + from think.models import PROVIDER_DEFAULTS, TIER_FLASH, TIER_LITE, TIER_PRO + from think.providers import PROVIDER_REGISTRY + + targeted_pairs = None + if args.targeted and not args.provider and not args.tier: + import fcntl + + from think.models import TYPE_DEFAULTS, get_backup_provider + from think.utils import get_config + + targeted_pairs = set() + config = get_config() + providers_config = config.get("providers", {}) + for talent_type, defaults in TYPE_DEFAULTS.items(): + type_config = providers_config.get(talent_type, {}) + provider = type_config.get("provider", defaults["provider"]) + tier = type_config.get("tier", defaults["tier"]) + targeted_pairs.add((provider, tier)) + backup = get_backup_provider(talent_type) + if backup: + targeted_pairs.add((backup, tier)) + + lock_dir = Path(get_journal()) / "health" + lock_dir.mkdir(parents=True, exist_ok=True) + lock_fd = open(lock_dir / "recheck.lock", "w") + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + lock_fd.close() + return + + if args.provider: + providers = args.provider + for name in providers: + if name not in PROVIDER_REGISTRY: + available = ", ".join(PROVIDER_REGISTRY.keys()) + print( + f"Unknown provider: {name}. Available providers: {available}", + file=sys.stderr, + ) + sys.exit(1) + else: + providers = list(PROVIDER_REGISTRY.keys()) + + interfaces = [args.interface] if args.interface else ["generate", "cogitate"] + tier_names = {1: "pro", 2: "flash", 3: "lite"} + tiers = [args.tier] if args.tier else [TIER_PRO, TIER_FLASH, TIER_LITE] + + provider_width = max(len(n) for n in providers) if providers else 0 + tier_width = max(len(tier_names[t]) for t in tiers) + model_names = {PROVIDER_DEFAULTS[p][t] for p in providers for t in tiers} + model_width = max(len(m) for m in model_names) if model_names else 0 + interface_width = max(len(n) for n in interfaces) if interfaces else 0 + + total = 0 + passed = 0 + failed = 0 + skipped = 0 + results: list[dict[str, object]] = [] + cache: dict[tuple[str, str, str], tuple[str, str, str]] = {} + + for provider_name in providers: + for tier in tiers: + if ( + targeted_pairs is not None + and (provider_name, tier) not in targeted_pairs + ): + continue + model = PROVIDER_DEFAULTS[provider_name][tier] + for interface_name in interfaces: + cache_key = (provider_name, model, interface_name) + if cache_key in cache: + status, message, source_tier = cache[cache_key] + elapsed_s = 0.0 + elapsed_s_rounded = 0.0 + reused_from = source_tier + else: + start = time.perf_counter() + if interface_name == "generate": + status, message = _check_generate( + provider_name, tier, args.timeout + ) + else: + status, message = await _check_cogitate( + provider_name, tier, args.timeout + ) + elapsed_s = time.perf_counter() - start + elapsed_s_rounded = round(elapsed_s, 1) + cache[cache_key] = (status, message, tier_names[tier]) + reused_from = None + + result: dict[str, object] = { + "provider": provider_name, + "tier": tier_names[tier], + "model": model, + "interface": interface_name, + "ok": status != "fail", + "status": status, + "message": str(message), + "elapsed_s": elapsed_s_rounded, + } + if reused_from: + result["reused_from"] = reused_from + results.append(result) + + if not args.json: + if reused_from: + mark = "=" + display_message = f"{message} (={reused_from})" + else: + if status == "ok": + mark = "✓" + elif status == "skip": + mark = "-" + else: + mark = "✗" + display_message = str(message) + print( + f"{mark} " + f"{provider_name:<{provider_width}} " + f"{tier_names[tier]:<{tier_width}} " + f"{model:<{model_width}} " + f"{interface_name:<{interface_width}} " + f"{display_message} ({elapsed_s:.1f}s)" + ) + + total += 1 + if status == "ok": + passed += 1 + elif status == "skip": + skipped += 1 + else: + failed += 1 + + any_failed = any(r["status"] == "fail" for r in results) + + payload = { + "results": results, + "summary": { + "total": total, + "passed": passed, + "skipped": skipped, + "failed": failed, + }, + "checked_at": datetime.now(timezone.utc).isoformat(), + } + health_dir = Path(get_journal()) / "health" + health_dir.mkdir(parents=True, exist_ok=True) + (health_dir / "talents.json").write_text(json.dumps(payload, indent=2)) + + if args.json: + print( + json.dumps( + { + "results": results, + "summary": { + "total": total, + "passed": passed, + "skipped": skipped, + "failed": failed, + }, + }, + indent=2, + ) + ) + else: + print(f"{total} checks: {passed} passed, {skipped} skipped, {failed} failed") + sys.exit(1 if any_failed else 0) + + +async def main_async() -> None: + """CLI entrypoint for provider connectivity checks.""" + from think.providers import PROVIDER_REGISTRY + + parser = argparse.ArgumentParser(description="solstone Provider CLI") + subparsers = parser.add_subparsers(dest="subcommand") + check_parser = subparsers.add_parser("check", help="Check AI provider connectivity") + check_parser.add_argument( + "--provider", + action="append", + help=f"Provider to check (repeatable). Available: {', '.join(PROVIDER_REGISTRY.keys())}", + ) + check_parser.add_argument( + "--interface", + choices=["generate", "cogitate"], + default=None, + help="Interface to check (default: both)", + ) + check_parser.add_argument( + "--timeout", + type=int, + default=30, + help="Timeout in seconds for generate checks (default: 30)", + ) + check_parser.add_argument( + "--tier", + type=int, + choices=[1, 2, 3], + default=None, + help="Tier to check (1=pro, 2=flash, 3=lite; default: all)", + ) + check_parser.add_argument( + "--json", action="store_true", help="Output results as JSON" + ) + check_parser.add_argument( + "--targeted", + action="store_true", + help="Only check configured provider+tier pairs (used by automated rechecks)", + ) + + args = setup_cli(parser) + require_solstone() + if args.subcommand != "check": + parser.print_help() + sys.exit(1) + await _run_check(args) + + +def main() -> None: + """Entry point wrapper.""" + asyncio.run(main_async()) diff --git a/think/retention.py b/think/retention.py index e1317f219..a806f4150 100644 --- a/think/retention.py +++ b/think/retention.py @@ -74,7 +74,7 @@ def is_segment_complete(segment_path: Path) -> bool: 3. screen.jsonl exists if any video raw media was captured 4. agents/speaker_labels.json exists if embeddings (.npz) are present """ - agents_dir = segment_path / "agents" + agents_dir = segment_path / "talents" # Check 1: no active agent files if agents_dir.is_dir(): @@ -126,7 +126,7 @@ def _get_completion_files(segment_path: Path) -> list[Path]: if path.is_file() ) - speaker_labels = segment_path / "agents" / "speaker_labels.json" + speaker_labels = segment_path / "talents" / "speaker_labels.json" if speaker_labels.exists(): completion_files.append(speaker_labels) diff --git a/think/routines.py b/think/routines.py index a2c93a16a..bcdf4ca0a 100644 --- a/think/routines.py +++ b/think/routines.py @@ -24,7 +24,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from apps.calendar.event import EventDay from think.callosum import callosum_send -from think.cortex_client import cortex_request, wait_for_agents +from think.cortex_client import cortex_request, wait_for_uses from think.facets import get_facets from think.utils import get_journal @@ -302,13 +302,13 @@ def _run_routine(routine: dict, event_context: dict | None = None) -> None: ) callosum_send("routines", "started", routine_id=routine_id, name=name) - agent_id = cortex_request( + use_id = cortex_request( prompt=prompt, name="routine", config={"output_path": str(output_path), "output": "md"}, ) - if agent_id is None: + if use_id is None: duration = int(time.monotonic() - start_time) logger.error("Failed to start routine %s", routine_id) _log_health(routine_id, name, duration, "error") @@ -323,11 +323,11 @@ def _run_routine(routine: dict, event_context: dict | None = None) -> None: ) return - completed, timed_out = wait_for_agents([agent_id], timeout=600) - if agent_id in timed_out: + completed, timed_out = wait_for_uses([use_id], timeout=600) + if use_id in timed_out: outcome = "timeout" else: - end_state = completed.get(agent_id, "error") + end_state = completed.get(use_id, "error") outcome = "success" if end_state == "finish" else "error" duration = int(time.monotonic() - start_time) diff --git a/think/segment.py b/think/segment.py index 355352a04..7539ae3e3 100644 --- a/think/segment.py +++ b/think/segment.py @@ -58,17 +58,17 @@ def _format_size(size_bytes: int) -> str: def _segment_stats(seg_path: Path) -> dict[str, int]: - """Return recursive file, agent, and byte counts for a segment.""" + """Return recursive file, talent, and byte counts for a segment.""" files = 0 - agents = 0 + talents = 0 size = 0 for path in seg_path.rglob("*"): if path.is_file(): files += 1 size += path.stat().st_size - if "agents" in path.parts: - agents += 1 - return {"files": files, "agents": agents, "size": size} + if "talents" in path.parts: + talents += 1 + return {"files": files, "talents": talents, "size": size} def _split_segment_path(path: str) -> tuple[str, str, str]: @@ -161,11 +161,11 @@ def _segment_files(seg_dir: Path) -> list[str]: def _agent_files(seg_dir: Path) -> list[str]: - """Return top-level file names from agents/ if present.""" - agents_dir = seg_dir / "agents" - if not agents_dir.is_dir(): + """Return top-level file names from talents/ if present.""" + talents_dir = seg_dir / "talents" + if not talents_dir.is_dir(): return [] - return sorted(path.name for path in agents_dir.iterdir() if path.is_file()) + return sorted(path.name for path in talents_dir.iterdir() if path.is_file()) def _events_summary(seg_dir: Path) -> dict[str, object]: @@ -609,7 +609,7 @@ def cmd_move(args: argparse.Namespace) -> None: _touch_health_marker(to_day) print(f" touched health markers: {src_day}, {to_day}") if verbose: - print(" dream will re-run daily agents on both days") + print(" dream will re-run daily talents on both days") # Post-move verify is informational — the move already completed. print() @@ -641,7 +641,7 @@ def cmd_list(args: argparse.Namespace) -> None: "end": end, "duration": _segment_duration(seg_key), "files": stats["files"], - "agents": stats["agents"], + "talents": stats["talents"], "size": stats["size"], } ) @@ -652,9 +652,9 @@ def cmd_list(args: argparse.Namespace) -> None: print( f"{'STREAM':<20} {'SEGMENT':<14} {'TIME':<15} " - f"{'DUR':>5} {'FILES':>5} {'AGENTS':>6} {'SIZE':>8}" + f"{'DUR':>5} {'FILES':>5} {'TALENTS':>7} {'SIZE':>8}" ) - print("-" * 77) + print("-" * 78) for row in rows: time_str = ( f"{row['start']}-{row['end']}" @@ -664,7 +664,7 @@ def cmd_list(args: argparse.Namespace) -> None: dur_str = f"{row['duration']}s" print( f"{row['stream']:<20} {row['segment']:<14} {time_str:<15} " - f"{dur_str:>5} {row['files']:>5} {row['agents']:>6} " + f"{dur_str:>5} {row['files']:>5} {row['talents']:>7} " f"{_format_size(int(row['size'])):>8}" ) @@ -684,7 +684,7 @@ def cmd_inspect(args: argparse.Namespace) -> None: prev_desc = _describe_prev(day, stream_name, marker) next_desc = _describe_next(day, stream_name, segment) files = _segment_files(seg_dir) - agents = _agent_files(seg_dir) + talents = _agent_files(seg_dir) stats = _segment_stats(seg_dir) events = _events_summary(seg_dir) index_info = _segment_index_info(day, stream_name, segment) @@ -701,7 +701,7 @@ def cmd_inspect(args: argparse.Namespace) -> None: "duration": duration, "chain": {"prev": prev_desc, "next": next_desc}, "files": files, - "agents": agents, + "talents": talents, "stats": stats, "events": events, "index": index_info, @@ -730,9 +730,9 @@ def cmd_inspect(args: argparse.Namespace) -> None: if files: print(f" {', '.join(files)}") print() - print(f"Agents ({len(agents)}):") - if agents: - print(f" {', '.join(agents)}") + print(f"Talents ({len(talents)}):") + if talents: + print(f" {', '.join(talents)}") print() print(f"Size: {_format_size(stats['size'])}") if index_info["available"]: diff --git a/think/sense_splitter.py b/think/sense_splitter.py index a639fb6cc..83ec3e781 100644 --- a/think/sense_splitter.py +++ b/think/sense_splitter.py @@ -28,7 +28,7 @@ def write_sense_outputs( sense_json: dict, seg_dir: Path, stream: str | None = None ) -> None: """Write unified Sense output into per-agent files.""" - agents_dir = seg_dir / "agents" + agents_dir = seg_dir / "talents" density = sense_json.get("density") or "active" activity_summary = sense_json.get("activity_summary") or "" @@ -56,7 +56,7 @@ def write_sense_outputs( def write_idle_stubs(seg_dir: Path) -> None: """Write minimal idle output files for a segment.""" _write_json_atomic( - seg_dir / "agents" / "density.json", + seg_dir / "talents" / "density.json", { "classification": "idle", "transcript_lines": 0, diff --git a/think/stats_schema.py b/think/stats_schema.py index a85339088..c827a3d06 100644 --- a/think/stats_schema.py +++ b/think/stats_schema.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -SCHEMA_VERSION = 2 +SCHEMA_VERSION = 3 DAY_FIELDS = ( "transcript_sessions", @@ -39,13 +39,13 @@ REQUIRED_TOP_LEVEL = ( "totals", "heatmap", "tokens", - "agents", + "talents", "facets", ) def validate(data: dict) -> list[str]: - """Validate stats output against schema v2. Returns list of error strings (empty = valid).""" + """Validate stats output against schema v3. Returns list of error strings (empty = valid).""" errors = [] # Check schema_version diff --git a/think/talent.py b/think/talent.py index 897d83365..03c5323ed 100644 --- a/think/talent.py +++ b/think/talent.py @@ -1,14 +1,14 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Talent agent and generator orchestration utilities. +"""Talent and generator orchestration utilities. -This module provides functionality for configuring and orchestrating talent agents +This module provides functionality for configuring and orchestrating talents and generators from talent/*.md and apps/*/talent/*.md. Key functions: - get_talent_configs(): Discover all talent configs with filtering -- get_agent(): Load complete agent configuration by name +- get_talent(): Load complete talent configuration by name - Hook loading: load_pre_hook(), load_post_hook() For simple prompt loading without orchestration (observe/, think/*.md prompts), @@ -95,7 +95,7 @@ def key_to_context(key: str) -> str: def get_output_name(key: str) -> str: - """Convert agent/generator key to filesystem-safe filename stem. + """Convert talent/generator key to a filesystem-safe filename stem. Parameters ---------- @@ -128,37 +128,37 @@ def get_output_path( facet: str | None = None, stream: str | None = None, ) -> Path: - """Return output path for generator agent output. + """Return output path for generator/talent output. Shared utility for determining where to write generator results. - Used by think/agents.py and think/cortex.py. + Used by think.talents and think.cortex. Parameters ---------- day_dir: Day directory path (YYYYMMDD). key: - Generator key or agent name (e.g., "activity", "chat:sentiment", + Generator key or talent name (e.g., "activity", "chat:sentiment", "decisionalizer", "entities:observer"). segment: Optional segment key (HHMMSS_LEN) for segment-level output. output_format: Output format - "json" for JSON, anything else for markdown. facet: - Optional facet name for multi-facet agents. When provided, output is - written under an agents/{facet}/ subdirectory. + Optional facet name for multi-facet talents. When provided, output is + written under a talents/{facet}/ subdirectory. stream: Optional stream name for segment-level output. When provided with - segment, constructs path as YYYYMMDD/{stream}/{segment}/agents/... + segment, constructs path as YYYYMMDD/{stream}/{segment}/talents/... Returns ------- Path Output file path: - - Segment + no facet: YYYYMMDD/{stream}/{segment}/agents/{name}.{ext} - - Segment + facet: YYYYMMDD/{stream}/{segment}/agents/{facet}/{name}.{ext} - - Daily + no facet: YYYYMMDD/agents/{name}.{ext} - - Daily + facet: YYYYMMDD/agents/{facet}/{name}.{ext} + - Segment + no facet: YYYYMMDD/{stream}/{segment}/talents/{name}.{ext} + - Segment + facet: YYYYMMDD/{stream}/{segment}/talents/{facet}/{name}.{ext} + - Daily + no facet: YYYYMMDD/talents/{name}.{ext} + - Daily + facet: YYYYMMDD/talents/{facet}/{name}.{ext} Where name is derived from key and ext is "json" or "md". """ day = Path(day_dir) @@ -172,11 +172,11 @@ def get_output_path( else: seg_dir = day / segment if facet: - return seg_dir / "agents" / facet / filename - return seg_dir / "agents" / filename + return seg_dir / "talents" / facet / filename + return seg_dir / "talents" / filename if facet: - return day / "agents" / facet / filename - return day / "agents" / filename + return day / "talents" / facet / filename + return day / "talents" / filename def get_talent_configs( @@ -328,44 +328,44 @@ def get_talent_configs( # --------------------------------------------------------------------------- -# Agent Resolution +# Talent Resolution # --------------------------------------------------------------------------- -def _resolve_agent_path(name: str) -> tuple[Path, str]: - """Resolve agent name to directory path and agent filename. +def _resolve_talent_path(name: str) -> tuple[Path, str]: + """Resolve talent name to directory path and filename. Parameters ---------- name: - Agent name - either system agent (e.g., "unified") or - app-namespaced agent (e.g., "support:support"). + Talent name - either system talent (e.g., "unified") or + app-namespaced talent (e.g., "support:support"). Returns ------- tuple[Path, str] - (agent_directory, agent_name) tuple. + (talent_directory, talent_name) tuple. """ if ":" in name: - # App agent: "support:support" -> apps/support/talent/support - app, agent_name = name.split(":", 1) - agent_dir = Path(__file__).parent.parent / "apps" / app / "talent" + # App talent: "support:support" -> apps/support/talent/support + app, talent_name = name.split(":", 1) + talent_dir = Path(__file__).parent.parent / "apps" / app / "talent" elif name == "unified": - # Chat agent: "unified" -> talent/chat - agent_dir = TALENT_DIR - agent_name = "chat" + # Chat talent: "unified" -> talent/chat + talent_dir = TALENT_DIR + talent_name = "chat" else: - # System agent: bare name -> talent/{name} - agent_dir = TALENT_DIR - agent_name = name - return agent_dir, agent_name + # System talent: bare name -> talent/{name} + talent_dir = TALENT_DIR + talent_name = name + return talent_dir, talent_name # Default load configuration - prompts must explicitly opt into source loading _DEFAULT_LOAD = { "transcripts": False, "percepts": False, - "agents": False, + "talents": False, } @@ -381,13 +381,13 @@ def source_is_enabled(value: bool | str | dict) -> bool: - False: don't load - True: load if available - "required": load (and generation will fail if none found) - - dict: for agents source, selective loading (e.g., {"entities": true}) + - dict: for talents source, selective loading (e.g., {"entities": true}) Both True and "required" mean the source should be loaded. A non-empty dict means the source should be loaded (with filtering). Args: - value: The source config value (bool, "required" string, or dict for agents) + value: The source config value (bool, "required" string, or dict for talents) Returns: True if the source should be loaded, False otherwise. @@ -402,7 +402,7 @@ def source_is_required(value: bool | str | dict) -> bool: """Check if a source must have content for generation to proceed. Args: - value: The source config value (bool, "required" string, or dict for agents) + value: The source config value (bool, "required" string, or dict for talents) Returns: True if the source is required (generation should skip if no content). @@ -413,46 +413,46 @@ def source_is_required(value: bool | str | dict) -> bool: return value == "required" -def get_agent_filter(value: bool | str | dict) -> dict[str, bool | str] | None: - """Extract agent filter from sources config. +def get_talent_filter(value: bool | str | dict) -> dict[str, bool | str] | None: + """Extract talent filter from sources config. - When agents source is a dict, returns it as filter mapping agent names - to their enabled/required status. When agents source is bool or "required", - returns None to indicate all agents should be loaded. + When talents source is a dict, returns it as filter mapping talent names + to their enabled/required status. When talents source is bool or "required", + returns None to indicate all talents should be loaded. Args: - value: The agents source config value + value: The talents source config value Returns: - Dict mapping agent names to bool/"required", or None for all agents. - Returns empty dict if value is False (no agents). + Dict mapping talent names to bool/"required", or None for all talents. + Returns empty dict if value is False (no talents). Examples: - >>> get_agent_filter(True) - None # All agents - >>> get_agent_filter(False) - {} # No agents - >>> get_agent_filter({"entities": True, "meetings": "required"}) + >>> get_talent_filter(True) + None # All talents + >>> get_talent_filter(False) + {} # No talents + >>> get_talent_filter({"entities": True, "meetings": "required"}) {"entities": True, "meetings": "required"} """ if isinstance(value, dict): return value if value is False: - return {} # No agents - return None # All agents (True or "required") + return {} # No talents + return None # All talents (True or "required") # --------------------------------------------------------------------------- -# Agent Loading +# Talent Loading # --------------------------------------------------------------------------- -def get_agent( +def get_talent( name: str = "unified", facet: str | None = None, analysis_day: str | None = None, ) -> dict: - """Return complete agent configuration by name. + """Return a complete talent configuration by name. Loads configuration from .md file with JSON frontmatter and instruction text. Template variables like $facets are resolved during prompt loading. @@ -461,8 +461,8 @@ def get_agent( Parameters ---------- name: - Agent name to load. Can be a system agent (e.g., "unified") - or an app-namespaced agent (e.g., "support:support" for apps/support/talent/support). + Talent name to load. Can be a system talent (e.g., "unified") + or an app-namespaced talent (e.g., "support:support" for apps/support/talent/support). facet: Optional facet name to focus on. Controls $facets template variable. analysis_day: @@ -472,8 +472,8 @@ def get_agent( Returns ------- dict - Complete agent configuration including: - - name: Agent name + Complete talent configuration including: + - name: Talent name - path: Path to the .md file - user_instruction: Composed prompt with template vars resolved - sources: Source config from 'load' key @@ -481,13 +481,13 @@ def get_agent( """ from think.prompts import _resolve_facets - # Resolve agent path based on namespace - agent_dir, agent_name = _resolve_agent_path(name) + # Resolve talent path based on namespace + talent_dir, talent_name = _resolve_talent_path(name) - # Verify agent prompt file exists - md_path = agent_dir / f"{agent_name}.md" + # Verify talent prompt file exists + md_path = talent_dir / f"{talent_name}.md" if not md_path.exists(): - raise FileNotFoundError(f"Agent not found: {name}") + raise FileNotFoundError(f"Talent not found: {name}") # Load config from frontmatter - preserve all fields post = frontmatter.load(md_path) @@ -508,10 +508,10 @@ def get_agent( prompt_context: dict[str, str] = {} prompt_context["facets"] = _resolve_facets(facet) - agent_prompt = load_prompt(agent_name, base_dir=agent_dir, context=prompt_context) - config["user_instruction"] = agent_prompt.text + prompt_obj = load_prompt(talent_name, base_dir=talent_dir, context=prompt_context) + config["user_instruction"] = prompt_obj.text - # Set agent name + # Set talent name config["name"] = name return config diff --git a/think/talent_cli.py b/think/talent_cli.py index e1dd55678..4cdc43479 100644 --- a/think/talent_cli.py +++ b/think/talent_cli.py @@ -13,9 +13,9 @@ Usage: sol talent show Show details for a specific prompt sol talent show --json Output a single prompt as JSONL sol talent show --prompt Show full prompt context (dry-run) - sol talent logs Show recent agent runs - sol talent logs -c 5 Show last 5 runs for an agent - sol talent log Show events for an agent run + sol talent logs Show recent talent runs + sol talent logs -c 5 Show last 5 runs for a talent + sol talent log Show events for a talent run sol talent log --json Output raw JSONL events sol talent log --full Show expanded event details """ @@ -79,14 +79,14 @@ def _scan_variables(body: str) -> list[str]: return result -def _format_last_run(key: str, agents_dir: Path) -> tuple[str, bool]: +def _format_last_run(key: str, talents_dir: Path) -> tuple[str, bool]: """Format age of last run with optional runtime duration. Returns (display_string, failed) where failed is True if the last event in the log was an error. """ safe_name = key.replace(":", "--") - link_path = agents_dir / f"{safe_name}.log" + link_path = talents_dir / f"{safe_name}.log" if not link_path.exists(): return "-", False @@ -197,7 +197,7 @@ def list_prompts( ) from think.utils import get_journal - agents_dir = Path(get_journal()) / "agents" + talents_dir = Path(get_journal()) / "talents" if not configs: print("No prompts found matching filters.") @@ -248,7 +248,7 @@ def list_prompts( for key, info in items: title = info.get("title", "")[:title_width] - last_run_str, failed = _format_last_run(key, agents_dir) + last_run_str, failed = _format_last_run(key, talents_dir) last_run = last_run_str[:last_run_width] tags = _format_tags(info, failed=failed) src = "" @@ -420,7 +420,7 @@ def show_prompt_context( ) -> None: """Show full prompt context via dry-run. - Builds config and pipes to `sol agents --dry-run` to show exactly + Builds config and pipes to `sol think.talents --dry-run` to show exactly what would be sent to the LLM provider. """ # Load prompt metadata @@ -570,14 +570,14 @@ def show_prompt_context( if facet: config["facet"] = facet else: - # Cogitate prompt - use get_agent() to build full config with instructions - from think.talent import get_agent + # Cogitate prompt - use get_talent() to build full config with instructions + from think.talent import get_talent try: - agent_config = get_agent(name, facet=facet) + agent_config = get_talent(name, facet=facet) config.update(agent_config) except Exception as e: - print(f"Failed to load agent config: {e}", file=sys.stderr) + print(f"Failed to load talent config: {e}", file=sys.stderr) sys.exit(1) # Override prompt with user query @@ -586,11 +586,11 @@ def show_prompt_context( else: config["prompt"] = "(no --query provided)" - # Run sol agents --dry-run + # Run sol think.talents --dry-run config_json = json.dumps(config) try: result = subprocess.run( - ["sol", "agents", "--dry-run"], + ["sol", "think.talents", "--dry-run"], input=config_json + "\n", capture_output=True, text=True, @@ -728,11 +728,11 @@ def show_prompt_context( print() -def _find_run_file(agents_dir: Path, agent_id: str) -> Path | None: - """Locate an agent run JSONL file by ID.""" - for match in agents_dir.glob(f"*/{agent_id}.jsonl"): +def _find_run_file(talents_dir: Path, use_id: str) -> Path | None: + """Locate a talent run JSONL file by ID.""" + for match in talents_dir.glob(f"*/{use_id}.jsonl"): return match - for match in agents_dir.glob(f"*/{agent_id}_active.jsonl"): + for match in talents_dir.glob(f"*/{use_id}_active.jsonl"): return match return None @@ -823,7 +823,7 @@ def _get_output_size(request_event: dict[str, Any], journal_root: str) -> int | def _print_summary(records: list[dict[str, Any]]) -> None: - """Print grouped summary of agent runs.""" + """Print grouped summary of talent runs.""" from collections import defaultdict groups: dict[str, list[dict[str, Any]]] = defaultdict(list) @@ -874,13 +874,13 @@ def logs_runs( errors: bool = False, summary: bool = False, ) -> None: - """Print one-line summaries of recent agent runs from day-index files.""" + """Print one-line summaries of recent talent runs from day-index files.""" from think.models import calc_agent_cost from think.utils import get_journal journal_root = get_journal() - agents_dir = Path(journal_root) / "agents" - if not agents_dir.is_dir(): + talents_dir = Path(journal_root) / "talents" + if not talents_dir.is_dir(): return # Validate --day format @@ -894,10 +894,10 @@ def logs_runs( # Find day-index files, most recent first if day: - day_file = agents_dir / f"{day}.jsonl" + day_file = talents_dir / f"{day}.jsonl" day_files = [day_file] if day_file.is_file() else [] else: - day_files = sorted(agents_dir.glob("????????.jsonl"), reverse=True) + day_files = sorted(talents_dir.glob("????????.jsonl"), reverse=True) if not day_files: return @@ -948,9 +948,9 @@ def logs_runs( name_width = max(name_width, 10) for r in records: - agent_id = r.get("agent_id") + use_id = r.get("use_id") run_file = ( - _find_run_file(agents_dir, agent_id) if isinstance(agent_id, str) else None + _find_run_file(talents_dir, use_id) if isinstance(use_id, str) else None ) stats: dict[str, Any] = { "event_count": 0, @@ -980,7 +980,7 @@ def logs_runs( stats = r.get("_stats") or {} cost_usd = r.get("_cost_usd") output_size = r.get("_output_size") - agent_id = r.get("agent_id", "") + use_id = r.get("use_id", "") ts = r.get("ts", 0) dt = datetime.fromtimestamp(ts / 1000) @@ -1014,7 +1014,7 @@ def logs_runs( facet_part = f" {facet}" if facet else "" line = ( - f"{agent_id:<15}{time_str:>12} {name:<{name_width}} {status_sym} " + f"{use_id:<15}{time_str:>12} {name:<{name_width}} {status_sym} " f"{runtime_str:>7} {cost_str:>4} {events_str:>3} {tools_str:>3} " f"{output_str:>5} {model}{facet_part}" ) @@ -1046,8 +1046,8 @@ def _event_detail(event: dict[str, Any], etype: str) -> str: tool = event.get("tool", "") result = event.get("result", "") return f"{tool} → {result}" - elif etype == "agent_updated": - return event.get("agent", "") + elif etype == "talent_updated": + return event.get("talent", "") elif etype == "finish": result = event.get("result", "") usage = event.get("usage") @@ -1074,7 +1074,7 @@ def _format_event_line(event: dict[str, Any], *, full: bool = False) -> str: "thinking": "think", "tool_start": "tool", "tool_end": "tool_end", - "agent_updated": "updated", + "talent_updated": "updated", "finish": "finish", "error": "error", } @@ -1093,14 +1093,14 @@ def _format_event_line(event: dict[str, Any], *, full: bool = False) -> str: return f"{time_str} {label:<8} {detail}" -def log_run(agent_id: str, *, json_mode: bool = False, full: bool = False) -> None: - """Show events for a single agent run.""" +def log_run(use_id: str, *, json_mode: bool = False, full: bool = False) -> None: + """Show events for a single talent run.""" from think.utils import get_journal - agents_dir = Path(get_journal()) / "agents" - run_file = _find_run_file(agents_dir, agent_id) + talents_dir = Path(get_journal()) / "talents" + run_file = _find_run_file(talents_dir, use_id) if run_file is None: - print(f"Agent run not found: {agent_id}", file=sys.stderr) + print(f"Talent run not found: {use_id}", file=sys.stderr) sys.exit(1) if json_mode: @@ -1164,7 +1164,7 @@ def main() -> None: ) # --- logs subcommand --- - logs_parser = subparsers.add_parser("logs", help="Show recent agent run log") + logs_parser = subparsers.add_parser("logs", help="Show recent talent run log") logs_parser.add_argument("agent", nargs="?", help="Filter to a specific agent") logs_parser.add_argument( "-c", diff --git a/think/agents.py b/think/talents.py similarity index 73% rename from think/agents.py rename to think/talents.py index 1e579b907..3b0db8257 100644 --- a/think/agents.py +++ b/think/talents.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Unified agent CLI for solstone. +"""Unified talent execution module for solstone. -Spawned by cortex for all agent types: -- Tool-using agents (with configured tools) +Spawned by cortex for all talent types: +- Tool-using talents (with configured tools) - Generators (transcript analysis, no tools) Both paths share unified config preparation and execution flow. @@ -18,11 +18,9 @@ import asyncio import json import logging import os -import shutil import sys -import time import traceback -from datetime import datetime, timezone +from datetime import datetime from pathlib import Path from string import Template from typing import Any, Callable, Optional @@ -30,9 +28,9 @@ from typing import Any, Callable, Optional from think.cluster import cluster, cluster_period, cluster_span from think.providers.shared import Event from think.talent import ( - get_agent_filter, get_output_path, get_talent_configs, + get_talent_filter, load_post_hook, load_pre_hook, load_prompt, @@ -52,7 +50,9 @@ from think.utils import ( setup_cli, ) -LOG = logging.getLogger("think.agents") +TALENT_EXECUTION_MODULE = "think.talents" + +LOG = logging.getLogger("think.talents") # Minimum content length for transcript-based generation MIN_INPUT_CHARS = 50 @@ -105,7 +105,7 @@ class JSONEventWriter: def _stream_content_description(stream: str | None) -> str: """Return a human-readable content description for a stream. - Used in preamble templates so agents know what kind of content they're + Used in preamble templates so talents know what kind of content they're analyzing (live capture vs imported conversations, notes, etc.). """ if not stream: @@ -218,7 +218,7 @@ def _build_prompt_context( day: Day in YYYYMMDD format segment: Segment key (HHMMSS_LEN) span: List of segment keys - activity: Optional activity record dict for activity-scheduled agents + activity: Optional activity record dict for activity-scheduled talents Returns: Dict with template variables: @@ -392,17 +392,19 @@ def _load_transcript( elif span: os.environ["SOL_SEGMENT"] = span[0] - # Convert sources config for clustering + # Convert sources config for clustering. + # Frontmatter now uses ``load.talents`` but cluster still consumes the + # normalized ``agents`` source key internally. cluster_sources: dict = {} for k, v in sources.items(): - if k == "agents": - agent_filter = get_agent_filter(v) - if agent_filter is None: - cluster_sources[k] = source_is_enabled(v) - elif not agent_filter: - cluster_sources[k] = False + if k == "talents": + talent_filter = get_talent_filter(v) + if talent_filter is None: + cluster_sources["agents"] = source_is_enabled(v) + elif not talent_filter: + cluster_sources["agents"] = False else: - cluster_sources[k] = agent_filter + cluster_sources["agents"] = talent_filter else: cluster_sources[k] = source_is_enabled(v) @@ -417,15 +419,15 @@ def _load_transcript( def prepare_config(request: dict) -> dict: - """Prepare complete agent config from request. + """Prepare a complete talent config from a request. - Single unified preparation path for all agent types. Takes raw request + Single unified preparation path for all talent types. Takes raw request from cortex and returns fully prepared config ready for execution. Config fields produced: - - name: Agent name + - name: Talent name - provider, model: Resolved from context/request - - user_instruction: Agent instruction from .md file + - user_instruction: Talent instruction from .md file - prompt: User's runtime query/request - transcript: Clustered transcript (if day provided) - output_path: Where to write output (if output format set) @@ -438,7 +440,7 @@ def prepare_config(request: dict) -> dict: Fully prepared config dict """ from think.models import resolve_model_for_provider, resolve_provider - from think.talent import get_agent, key_to_context + from think.talent import get_talent, key_to_context name = request.get("name", "unified") facet = request.get("facet") @@ -450,8 +452,8 @@ def prepare_config(request: dict) -> dict: output_path_override = request.get("output_path") user_prompt = request.get("prompt", "") - # Load complete agent config - config = get_agent(name, facet=facet, analysis_day=day) + # Load complete talent config + config = get_talent(name, facet=facet, analysis_day=day) # Config now contains all frontmatter fields plus: # - path: Path to the .md file @@ -459,11 +461,11 @@ def prepare_config(request: dict) -> dict: # - All frontmatter: tools, hook, disabled, thinking_budget, max_output_tokens, etc. # Convert path string to Path object for convenience - agent_path = Path(config["path"]) if config.get("path") else None + talent_path = Path(config["path"]) if config.get("path") else None sources = config.get("sources", {}) talent_cwd = config.get("cwd") - # Merge request values (request overrides agent defaults) + # Merge request values (request overrides talent defaults) config.update({k: v for k, v in request.items() if v is not None}) request_cwd = request.get("cwd") if request_cwd is not None and request_cwd != talent_cwd: @@ -501,14 +503,14 @@ def prepare_config(request: dict) -> dict: # Resolve provider and model from context context = key_to_context(name) - agent_type = config["type"] - default_provider, default_model = resolve_provider(context, agent_type) + talent_type = config["type"] + default_provider, default_model = resolve_provider(context, talent_type) provider = config.get("provider") or default_provider model = config.get("model") if not model: if provider != default_provider: - model = resolve_model_for_provider(context, provider, agent_type) + model = resolve_model_for_provider(context, provider, talent_type) else: model = default_model @@ -529,14 +531,14 @@ def prepare_config(request: dict) -> dict: config["health_stale"] = should_recheck_health(health_data) if not is_provider_healthy(provider, health_data): - backup = get_backup_provider(agent_type) + backup = get_backup_provider(talent_type) if backup and backup != provider: env_key = PROVIDER_METADATA.get(backup, {}).get("env_key") if not env_key or os.getenv(env_key): config["fallback_from"] = provider config["provider"] = backup config["model"] = resolve_model_for_provider( - context, backup, agent_type + context, backup, talent_type ) # Check if disabled @@ -546,7 +548,7 @@ def prepare_config(request: dict) -> dict: # Day-based processing: load transcript and apply template substitution if day: - # Load transcript (only when agent has enabled sources to consume) + # Load transcript (only when the talent has enabled sources to consume) if any(source_is_enabled(v) for v in sources.values()): transcript, source_counts = _load_transcript(day, segment, span, sources) config["transcript"] = transcript @@ -571,8 +573,8 @@ def prepare_config(request: dict) -> dict: "Scale analysis to available input.\n\n" + transcript ) - # Reload agent instruction with template substitution for day/segment context - if agent_path and agent_path.exists(): + # Reload talent instruction with template substitution for day/segment context + if talent_path and talent_path.exists(): from think.prompts import _resolve_facets prompt_context = _build_prompt_context( @@ -585,13 +587,13 @@ def prepare_config(request: dict) -> dict: if activity_ctx: prompt_context["activity_context"] = activity_ctx - agent_prompt_obj = load_prompt( - agent_path.stem, base_dir=agent_path.parent, context=prompt_context + talent_prompt_obj = load_prompt( + talent_path.stem, base_dir=talent_path.parent, context=prompt_context ) - config["user_instruction"] = agent_prompt_obj.text + config["user_instruction"] = talent_prompt_obj.text # Set prompt (user's runtime query) - # For tool agents: prompt is the user's question + # For tool talents: prompt is the user's question # For generators: prompt is typically empty (instruction is in user_instruction) config["prompt"] = user_prompt @@ -628,9 +630,9 @@ def validate_config(config: dict) -> str | None: has_user_instruction = bool(config.get("user_instruction")) has_day = bool(config.get("day")) - # Cogitate agents need a prompt (user's question) + # Cogitate talents need a prompt (user's question) if is_cogitate and not has_prompt: - return "Missing 'prompt' field for cogitate agent" + return "Missing 'prompt' field for cogitate talent" # Generate prompts need either day (transcript) or user_instruction if not is_cogitate and not has_day and not has_user_instruction and not has_prompt: @@ -716,7 +718,7 @@ def _run_post_hooks(result: str, config: dict) -> str: # ============================================================================= -# Unified Agent Execution +# Unified Talent Execution # ============================================================================= @@ -730,12 +732,12 @@ def _write_output(output_path: Path, result: str) -> None: def _build_dry_run_event(config: dict, before_values: dict) -> dict: """Build a dry-run event with all context.""" - agent_type = config["type"] + talent_type = config["type"] event: dict[str, Any] = { "event": "dry_run", "ts": now_ms(), - "type": agent_type, + "type": talent_type, "name": config.get("name", "unified"), "provider": config.get("provider", ""), "model": config.get("model") or "unknown", @@ -798,7 +800,7 @@ async def _execute_with_tools( config: dict, emit_event: Callable[[dict], None], ) -> None: - """Execute tool-using agent via provider's run_cogitate. + """Execute a tool-using talent via the provider's run_cogitate. Args: config: Prepared config dict @@ -816,7 +818,7 @@ async def _execute_with_tools( provider_mod = get_provider_module(provider) # Wrapper to intercept finish event for post-processing - def agent_emit_event(data: Event) -> None: + def talent_emit_event(data: Event) -> None: if data.get("event") == "finish": result = data.get("result", "") result = _run_post_hooks(result, config) @@ -832,7 +834,7 @@ async def _execute_with_tools( emit_event(data) try: - await provider_mod.run_cogitate(config=config, on_event=agent_emit_event) + await provider_mod.run_cogitate(config=config, on_event=talent_emit_event) except Exception as exc: if not _is_retryable_error(exc) or config.get("fallback_from"): raise @@ -878,7 +880,7 @@ async def _execute_with_tools( def backup_emit(data: Event) -> None: if data.get("event") == "error": return - agent_emit_event(data) + talent_emit_event(data) try: await backup_mod.run_cogitate(config=config, on_event=backup_emit) @@ -1030,14 +1032,14 @@ async def _execute_generate( emit_event(finish_event) -async def _run_agent( +async def _run_talent( config: dict, emit_event: Callable[[dict], None], dry_run: bool = False, ) -> None: - """Execute agent based on config. + """Execute a talent based on config. - Unified execution path for all agent types. Handles: + Unified execution path for all talent types. Handles: - Skip conditions (disabled, no input, etc.) - Output existence checking (skip if exists unless refresh) - Pre/post hooks @@ -1096,10 +1098,10 @@ async def _run_agent( } ) if config.get("day"): - day_log(config["day"], f"agent {name} skipped ({skip_reason})") + day_log(config["day"], f"talent {name} skipped ({skip_reason})") return - # Check if output already exists (applies to both tool agents and generators) + # Check if output already exists (applies to both tool talents and generators) if output_path and not refresh and not dry_run: if output_path.exists() and output_path.stat().st_size > 0: LOG.info("Output exists, loading: %s", output_path) @@ -1145,7 +1147,7 @@ async def _run_agent( } ) if config.get("day"): - day_log(config["day"], f"agent {name} skipped ({skip_reason})") + day_log(config["day"], f"talent {name} skipped ({skip_reason})") return # Dry-run mode @@ -1153,7 +1155,7 @@ async def _run_agent( emit_event(_build_dry_run_event(config, before_values)) return - # Execute based on agent type + # Execute based on talent type if is_cogitate: await _execute_with_tools(config, emit_event) else: @@ -1161,7 +1163,7 @@ async def _run_agent( # Log completion if config.get("day"): - day_log(config["day"], f"agent {name} ok") + day_log(config["day"], f"talent {name} ok") # ============================================================================= @@ -1185,351 +1187,30 @@ def scan_day(day: str) -> dict[str, list[str]]: output_format = meta.get("output") output_file = get_output_path(day_dir, key, output_format=output_format) if output_file.exists(): - processed.append(os.path.join("agents", output_file.name)) + processed.append(os.path.join("talents", output_file.name)) else: - pending.append(os.path.join("agents", output_file.name)) + pending.append(os.path.join("talents", output_file.name)) return {"processed": sorted(processed), "repairable": sorted(pending)} -def _check_generate(provider_name: str, tier: int, timeout: int) -> tuple[str, str]: - """Check generate interface for a provider.""" - from think.models import PROVIDER_DEFAULTS - from think.providers import PROVIDER_METADATA, get_provider_module - - env_key = PROVIDER_METADATA[provider_name]["env_key"] - if env_key and not os.getenv(env_key): - label = PROVIDER_METADATA[provider_name]["label"] - # Google Vertex AI can work without GOOGLE_API_KEY, but this health check - # treats missing env as "not configured" for the standard API path. - return "skip", f"{label} not configured (no {env_key})" - - # For keyless providers (e.g., Ollama), check reachability instead - if not env_key: - from think.providers import validate_key - - result = validate_key(provider_name, "") - if not result.get("valid"): - return ( - "skip", - f"Ollama not reachable ({result.get('error', 'unreachable')})", - ) - - try: - module = get_provider_module(provider_name) - model = PROVIDER_DEFAULTS[provider_name][tier] - result = module.run_generate( - contents="Say OK", - model=model, - temperature=0, - max_output_tokens=16, - system_instruction=None, - json_output=False, - thinking_budget=None, - timeout_s=timeout, - ) - text = result.get("text", "") if isinstance(result, dict) else "" - if text: - usage = result.get("usage") if isinstance(result, dict) else None - if usage: - from think.models import log_token_usage - - log_token_usage( - model=PROVIDER_DEFAULTS[provider_name][tier], - usage=usage, - context="health.check.generate", - type="generate", - ) - return "ok", "OK" - return "fail", "FAIL: empty response text" - except Exception as exc: - return "fail", f"FAIL: {exc}" - - -async def _check_cogitate( - provider_name: str, tier: int, timeout: int -) -> tuple[str, str]: - """Check cogitate interface for a provider by running a real prompt.""" - from think.models import PROVIDER_DEFAULTS - from think.providers import PROVIDER_METADATA, get_provider_module - - # Pre-flight: check provider is configured - env_key = PROVIDER_METADATA[provider_name]["env_key"] - if env_key and not os.getenv(env_key): - label = PROVIDER_METADATA[provider_name]["label"] - return "skip", f"{label} not configured (no {env_key})" - - # For keyless providers (e.g., Ollama), check reachability - if not env_key: - from think.providers import validate_key - - result = validate_key(provider_name, "") - if not result.get("valid"): - return ( - "skip", - f"Ollama not reachable ({result.get('error', 'unreachable')})", - ) - - # Pre-flight: check cogitate CLI binary is installed - binary = PROVIDER_METADATA[provider_name].get("cogitate_cli", "") - if binary and not shutil.which(binary): - return "skip", f"{binary} CLI not installed" - - try: - module = get_provider_module(provider_name) - model = PROVIDER_DEFAULTS[provider_name][tier] - config = {"prompt": "Say OK", "model": model} - result = await asyncio.wait_for( - module.run_cogitate(config=config, on_event=None), - timeout=timeout, - ) - if result: - return "ok", "OK" - return "fail", "FAIL: empty response" - except asyncio.TimeoutError: - return "fail", f"FAIL: timed out after {timeout}s" - except Exception as exc: - return "fail", f"FAIL: {exc}" - - -async def _run_check(args: argparse.Namespace) -> None: - """Run connectivity checks against AI providers.""" - from think.models import PROVIDER_DEFAULTS, TIER_FLASH, TIER_LITE, TIER_PRO - from think.providers import PROVIDER_REGISTRY - - # --targeted: only check configured provider+tier pairs - targeted_pairs = None - if args.targeted and not args.provider and not args.tier: - import fcntl - - from think.models import TYPE_DEFAULTS, get_backup_provider - from think.utils import get_config - - targeted_pairs = set() - config = get_config() - providers_config = config.get("providers", {}) - for agent_type, defaults in TYPE_DEFAULTS.items(): - type_config = providers_config.get(agent_type, {}) - provider = type_config.get("provider", defaults["provider"]) - tier = type_config.get("tier", defaults["tier"]) - targeted_pairs.add((provider, tier)) - backup = get_backup_provider(agent_type) - if backup: - targeted_pairs.add((backup, tier)) - - # flock dedup: only one targeted check runs at a time - lock_dir = Path(get_journal()) / "health" - lock_dir.mkdir(parents=True, exist_ok=True) - lock_fd = open(lock_dir / "recheck.lock", "w") - try: - fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except OSError: - lock_fd.close() - return - - if args.provider: - providers = args.provider - for name in providers: - if name not in PROVIDER_REGISTRY: - available = ", ".join(PROVIDER_REGISTRY.keys()) - print( - f"Unknown provider: {name}. Available providers: {available}", - file=sys.stderr, - ) - sys.exit(1) - else: - providers = list(PROVIDER_REGISTRY.keys()) - - interfaces = [args.interface] if args.interface else ["generate", "cogitate"] - - tier_names = {1: "pro", 2: "flash", 3: "lite"} - tiers = [args.tier] if args.tier else [TIER_PRO, TIER_FLASH, TIER_LITE] - - # Pre-compute column widths - provider_width = max(len(n) for n in providers) if providers else 0 - tier_width = max(len(tier_names[t]) for t in tiers) - # Resolve all model names to get max width - model_names = set() - for p in providers: - for t in tiers: - model_names.add(PROVIDER_DEFAULTS[p][t]) - model_width = max(len(m) for m in model_names) if model_names else 0 - interface_width = max(len(n) for n in interfaces) if interfaces else 0 - - total = 0 - passed = 0 - failed = 0 - skipped = 0 - results = [] - cache = {} # (provider, model, interface) -> (status, message, source_tier) - - for provider_name in providers: - for tier in tiers: - if ( - targeted_pairs is not None - and (provider_name, tier) not in targeted_pairs - ): - continue - model = PROVIDER_DEFAULTS[provider_name][tier] - for interface_name in interfaces: - cache_key = (provider_name, model, interface_name) - if cache_key in cache: - status, message, source_tier = cache[cache_key] - elapsed_s = 0.0 - elapsed_s_rounded = 0.0 - reused_from = source_tier - else: - start = time.perf_counter() - if interface_name == "generate": - status, message = _check_generate( - provider_name, tier, args.timeout - ) - else: - status, message = await _check_cogitate( - provider_name, tier, args.timeout - ) - elapsed_s = time.perf_counter() - start - elapsed_s_rounded = round(elapsed_s, 1) - cache[cache_key] = (status, message, tier_names[tier]) - reused_from = None - - result = { - "provider": provider_name, - "tier": tier_names[tier], - "model": model, - "interface": interface_name, - "ok": status != "fail", - "status": status, - "message": str(message), - "elapsed_s": elapsed_s_rounded, - } - if reused_from: - result["reused_from"] = reused_from - results.append(result) - - if not args.json: - if reused_from: - mark = "=" - display_message = f"{message} (={reused_from})" - else: - if status == "ok": - mark = "✓" - elif status == "skip": - mark = "-" - else: - mark = "✗" - display_message = str(message) - print( - f"{mark} " - f"{provider_name:<{provider_width}} " - f"{tier_names[tier]:<{tier_width}} " - f"{model:<{model_width}} " - f"{interface_name:<{interface_width}} " - f"{display_message} ({elapsed_s:.1f}s)" - ) - - total += 1 - if status == "ok": - passed += 1 - elif status == "skip": - skipped += 1 - else: - failed += 1 - - any_failed = any(r["status"] == "fail" for r in results) - - # Write results to health file - payload = { - "results": results, - "summary": { - "total": total, - "passed": passed, - "skipped": skipped, - "failed": failed, - }, - "checked_at": datetime.now(timezone.utc).isoformat(), - } - health_dir = Path(get_journal()) / "health" - health_dir.mkdir(parents=True, exist_ok=True) - (health_dir / "agents.json").write_text(json.dumps(payload, indent=2)) - - if args.json: - print( - json.dumps( - { - "results": results, - "summary": { - "total": total, - "passed": passed, - "skipped": skipped, - "failed": failed, - }, - }, - indent=2, - ) - ) - else: - print(f"{total} checks: {passed} passed, {skipped} skipped, {failed} failed") - sys.exit(1 if any_failed else 0) - - # ============================================================================= # Main Entry Point # ============================================================================= async def main_async() -> None: - """NDJSON-based CLI for agents.""" - from think.providers import PROVIDER_REGISTRY + """NDJSON-based CLI for talents.""" parser = argparse.ArgumentParser( - description="solstone Agent CLI - Accepts NDJSON input via stdin" + description="solstone Talent CLI - Accepts NDJSON input via stdin" ) parser.add_argument( "--dry-run", action="store_true", help="Show what would be sent to the provider without calling the LLM", ) - subparsers = parser.add_subparsers(dest="subcommand") - check_parser = subparsers.add_parser("check", help="Check AI provider connectivity") - check_parser.add_argument( - "--provider", - action="append", - help=f"Provider to check (repeatable). Available: {', '.join(PROVIDER_REGISTRY.keys())}", - ) - check_parser.add_argument( - "--interface", - choices=["generate", "cogitate"], - default=None, - help="Interface to check (default: both)", - ) - check_parser.add_argument( - "--timeout", - type=int, - default=30, - help="Timeout in seconds for generate checks (default: 30)", - ) - check_parser.add_argument( - "--tier", - type=int, - choices=[1, 2, 3], - default=None, - help="Tier to check (1=pro, 2=flash, 3=lite; default: all)", - ) - check_parser.add_argument( - "--json", action="store_true", help="Output results as JSON" - ) - check_parser.add_argument( - "--targeted", - action="store_true", - help="Only check configured provider+tier pairs (used by automated rechecks)", - ) - args = setup_cli(parser) require_solstone() - if args.subcommand == "check": - await _run_check(args) - return - dry_run = args.dry_run app_logger = setup_logging(args.verbose) @@ -1556,7 +1237,7 @@ async def main_async() -> None: emit_event({"event": "error", "error": error, "ts": now_ms()}) continue - await _run_agent(config, emit_event, dry_run=dry_run) + await _run_talent(config, emit_event, dry_run=dry_run) except json.JSONDecodeError as e: emit_event( diff --git a/think/tools/call.py b/think/tools/call.py index 0bb641178..74d8518ca 100644 --- a/think/tools/call.py +++ b/think/tools/call.py @@ -606,7 +606,7 @@ def agents( if segment: # List outputs in a specific segment directory - seg_path = day_dir / segment / "agents" + seg_path = day_dir / segment / "talents" if not seg_path.is_dir(): typer.echo(f"Segment {segment} not found for {day}.") return @@ -614,7 +614,7 @@ def agents( return # List daily agent outputs - agents_path = day_dir / "agents" + agents_path = day_dir / "talents" if agents_path.is_dir(): _list_outputs(agents_path, "Daily agents") @@ -623,8 +623,8 @@ def agents( if seg_list: typer.echo(f"\nSegments: {len(seg_list)}") for stream_name, seg_key, seg_path_obj in seg_list: - agents_dir = seg_path_obj / "agents" - outputs = _get_output_names(agents_dir) + talents_dir = seg_path_obj / "talents" + outputs = _get_output_names(talents_dir) label = f" {stream_name}/{seg_key}" if stream_name else f" {seg_key}" if outputs: typer.echo(f"{label}: {', '.join(outputs)}") @@ -686,12 +686,12 @@ def read( raise typer.Exit(1) if segment: - base_dir = day_dir / segment / "agents" + base_dir = day_dir / segment / "talents" else: - base_dir = day_dir / "agents" + base_dir = day_dir / "talents" if not base_dir.is_dir(): - location = f"segment {segment}" if segment else "agents" + location = f"segment {segment}" if segment else "talents" typer.echo(f"No {location} directory for {day}.", err=True) raise typer.Exit(1) diff --git a/think/tools/sol.py b/think/tools/sol.py index 7fcb43540..73bd3d0a9 100644 --- a/think/tools/sol.py +++ b/think/tools/sol.py @@ -8,7 +8,7 @@ Provides read and write access to ``{journal}/sol/self.md``, ``{journal}/sol/pulse.md``, and ``{journal}/sol/awareness.md`` — sol's identity and initiative files. Also provides read access to the morning briefing at -``{journal}/YYYYMMDD/agents/morning_briefing.md``. +``{journal}/YYYYMMDD/talents/morning_briefing.md``. Mounted by ``think.call`` as ``sol call identity ...``. """ @@ -318,9 +318,9 @@ def awareness_cmd( def briefing_cmd( day: str | None = typer.Option(None, "--day", "-d", help="Specific day YYYYMMDD."), ) -> None: - """Read the morning briefing from YYYYMMDD/agents/morning_briefing.md.""" + """Read the morning briefing from YYYYMMDD/talents/morning_briefing.md.""" if day: - path = day_path(day, create=False) / "agents" / "morning_briefing.md" + path = day_path(day, create=False) / "talents" / "morning_briefing.md" if not path.exists(): typer.echo("No briefing found.", err=True) raise typer.Exit(1) @@ -329,7 +329,7 @@ def briefing_cmd( # No day specified — find most recent for day in sorted(day_dirs().keys(), reverse=True): - agents_dir = day_path(day, create=False) / "agents" + agents_dir = day_path(day, create=False) / "talents" briefing = agents_dir / "morning_briefing.md" if briefing.exists() and briefing.stat().st_size > 0: typer.echo(briefing.read_text(encoding="utf-8")) -- 2.51.2