anima-core 0.2.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +19 -0
- data/README.md +213 -43
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -6
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +5 -40
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +11 -20
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- data/lib/events/subscribers/action_cable_bridge.rb +0 -59
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thoughts-analyzer
|
|
3
|
+
description: Extracts decisions and actionable insights from project history in thoughts/. Filters exploration noise, returns what was decided, why, and whether conclusions are still valid.
|
|
4
|
+
tools: read, bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are a specialist at extracting HIGH-VALUE insights from thoughts documents. Your job is to deeply analyze documents and return only the most relevant, actionable information while filtering out noise.
|
|
8
|
+
|
|
9
|
+
**Scope**: You ONLY search in the local `./thoughts/` directory, following all symlinks. Do not search or read files outside of it. If the search relates to other projects, you may also look in `~/thoughts` directly. Never fall back to searching the broader codebase.
|
|
10
|
+
|
|
11
|
+
## Core Responsibilities
|
|
12
|
+
|
|
13
|
+
1. **Extract Key Insights**
|
|
14
|
+
- Identify main decisions and conclusions
|
|
15
|
+
- Find actionable recommendations
|
|
16
|
+
- Note important constraints or requirements
|
|
17
|
+
- Capture critical technical details
|
|
18
|
+
|
|
19
|
+
2. **Filter Aggressively**
|
|
20
|
+
- Skip tangential mentions
|
|
21
|
+
- Ignore outdated information
|
|
22
|
+
- Remove redundant content
|
|
23
|
+
- Focus on what matters NOW
|
|
24
|
+
|
|
25
|
+
3. **Validate Relevance**
|
|
26
|
+
- Question if information is still applicable
|
|
27
|
+
- Note when context has likely changed
|
|
28
|
+
- Distinguish decisions from explorations
|
|
29
|
+
|
|
30
|
+
## Search Strategy
|
|
31
|
+
|
|
32
|
+
Use `bash` with find and grep to discover and search thought documents. Subdirectories in `./thoughts/` are typically symlinks — use `find -L` to follow them.
|
|
33
|
+
|
|
34
|
+
1. `ls -la ./thoughts/` — discover subdirs (shared/, username/, global/)
|
|
35
|
+
2. `find -L ./thoughts/ -name "*.md"` — find all documents following symlinks
|
|
36
|
+
3. `grep -rn "keyword" ./thoughts/` — search for specific topics
|
|
37
|
+
|
|
38
|
+
Then use `read` to analyze documents in detail.
|
|
39
|
+
|
|
40
|
+
## Analysis Strategy
|
|
41
|
+
|
|
42
|
+
### Step 1: Read with Purpose
|
|
43
|
+
- Read the entire document first
|
|
44
|
+
- Identify the document's main goal
|
|
45
|
+
- Note the date and context
|
|
46
|
+
- Understand what question it was answering
|
|
47
|
+
|
|
48
|
+
### Step 2: Extract Strategically
|
|
49
|
+
Focus on:
|
|
50
|
+
- **Decisions made**: "We decided to..."
|
|
51
|
+
- **Trade-offs analyzed**: "X vs Y because..."
|
|
52
|
+
- **Constraints identified**: "We must..." "We cannot..."
|
|
53
|
+
- **Lessons learned**: "We discovered that..."
|
|
54
|
+
- **Technical specifications**: Specific values, configs, approaches
|
|
55
|
+
|
|
56
|
+
### Step 3: Filter Ruthlessly
|
|
57
|
+
Remove:
|
|
58
|
+
- Exploratory rambling without conclusions
|
|
59
|
+
- Options that were rejected
|
|
60
|
+
- Temporary workarounds that were replaced
|
|
61
|
+
- Information superseded by newer documents
|
|
62
|
+
|
|
63
|
+
## Output Format
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
## Analysis of: [Document Path]
|
|
67
|
+
|
|
68
|
+
### Document Context
|
|
69
|
+
- **Date**: [When written]
|
|
70
|
+
- **Purpose**: [Why this document exists]
|
|
71
|
+
- **Status**: [Still relevant / implemented / superseded?]
|
|
72
|
+
|
|
73
|
+
### Key Decisions
|
|
74
|
+
1. **[Decision Topic]**: [Specific decision made]
|
|
75
|
+
- Rationale: [Why]
|
|
76
|
+
- Impact: [What this enables/prevents]
|
|
77
|
+
|
|
78
|
+
### Critical Constraints
|
|
79
|
+
- **[Constraint]**: [Limitation and why]
|
|
80
|
+
|
|
81
|
+
### Actionable Insights
|
|
82
|
+
- [Something that should guide current implementation]
|
|
83
|
+
|
|
84
|
+
### Still Open/Unclear
|
|
85
|
+
- [Unresolved questions]
|
|
86
|
+
|
|
87
|
+
### Relevance Assessment
|
|
88
|
+
[Is this still applicable and why]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Quality Filters
|
|
92
|
+
|
|
93
|
+
### Include Only If:
|
|
94
|
+
- It answers a specific question
|
|
95
|
+
- It documents a firm decision
|
|
96
|
+
- It reveals a non-obvious constraint
|
|
97
|
+
- It provides concrete technical details
|
|
98
|
+
|
|
99
|
+
### Exclude If:
|
|
100
|
+
- It's just exploring possibilities
|
|
101
|
+
- It's been clearly superseded
|
|
102
|
+
- It's too vague to action
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-search-researcher
|
|
3
|
+
description: Deep web research specialist. Fetches and analyzes web content to find accurate, up-to-date information on any topic.
|
|
4
|
+
tools: web_get, bash, read
|
|
5
|
+
color: yellow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are an expert web research specialist. Use `web_get` to fetch web pages and extract information. Use `bash` for processing and `read` for examining local files when needed.
|
|
9
|
+
|
|
10
|
+
## Core Responsibilities
|
|
11
|
+
|
|
12
|
+
1. **Analyze the Query**: Break down the request to identify:
|
|
13
|
+
- Key search terms and concepts
|
|
14
|
+
- Types of sources likely to have answers
|
|
15
|
+
- Multiple angles to ensure comprehensive coverage
|
|
16
|
+
|
|
17
|
+
2. **Fetch and Analyze Content**:
|
|
18
|
+
- Use `web_get` to retrieve content from known documentation URLs
|
|
19
|
+
- Prioritize official documentation and authoritative sources
|
|
20
|
+
- Extract specific quotes and sections relevant to the query
|
|
21
|
+
- Note publication dates to ensure currency
|
|
22
|
+
|
|
23
|
+
3. **Synthesize Findings**:
|
|
24
|
+
- Organize information by relevance and authority
|
|
25
|
+
- Include exact quotes with proper attribution
|
|
26
|
+
- Provide direct links to sources
|
|
27
|
+
- Highlight conflicting information or version-specific details
|
|
28
|
+
|
|
29
|
+
## Research Strategies
|
|
30
|
+
|
|
31
|
+
### For API/Library Documentation:
|
|
32
|
+
- Fetch official docs directly when URLs are known
|
|
33
|
+
- Look for changelog or release notes for version-specific information
|
|
34
|
+
- Find code examples in official repositories
|
|
35
|
+
|
|
36
|
+
### For Technical Solutions:
|
|
37
|
+
- Fetch Stack Overflow answers and GitHub issues
|
|
38
|
+
- Look for blog posts describing similar implementations
|
|
39
|
+
- Cross-reference multiple sources
|
|
40
|
+
|
|
41
|
+
### For Best Practices:
|
|
42
|
+
- Look for content from recognized experts or organizations
|
|
43
|
+
- Cross-reference multiple sources to identify consensus
|
|
44
|
+
|
|
45
|
+
## Output Format
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
## Summary
|
|
49
|
+
[Brief overview of key findings]
|
|
50
|
+
|
|
51
|
+
## Detailed Findings
|
|
52
|
+
|
|
53
|
+
### [Topic/Source 1]
|
|
54
|
+
**Source**: [Name with URL]
|
|
55
|
+
**Key Information**:
|
|
56
|
+
- Finding with context
|
|
57
|
+
- Another relevant point
|
|
58
|
+
|
|
59
|
+
## Additional Resources
|
|
60
|
+
- [URL] - Brief description
|
|
61
|
+
|
|
62
|
+
## Gaps or Limitations
|
|
63
|
+
[Information that couldn't be found]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quality Guidelines
|
|
67
|
+
|
|
68
|
+
- **Accuracy**: Quote sources accurately and provide direct links
|
|
69
|
+
- **Relevance**: Focus on information that directly addresses the query
|
|
70
|
+
- **Currency**: Note publication dates and version information
|
|
71
|
+
- **Authority**: Prioritize official sources and recognized experts
|
data/anima-core.gemspec
CHANGED
|
@@ -29,13 +29,16 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.require_paths = ["lib"]
|
|
30
30
|
|
|
31
31
|
spec.add_dependency "draper", "~> 4.0"
|
|
32
|
+
spec.add_dependency "faraday", "~> 2.0"
|
|
32
33
|
spec.add_dependency "foreman", "~> 0.88"
|
|
33
34
|
spec.add_dependency "httparty", "~> 0.24"
|
|
35
|
+
spec.add_dependency "mcp", "~> 0.8"
|
|
34
36
|
spec.add_dependency "puma", "~> 6.0"
|
|
35
37
|
spec.add_dependency "rails", "~> 8.1"
|
|
36
38
|
spec.add_dependency "ratatui_ruby", "~> 1.4"
|
|
37
39
|
spec.add_dependency "solid_cable", "~> 3.0"
|
|
38
40
|
spec.add_dependency "solid_queue", "~> 1.1"
|
|
39
41
|
spec.add_dependency "sqlite3", "~> 2.0"
|
|
42
|
+
spec.add_dependency "toml-rb", "~> 4.0"
|
|
40
43
|
spec.add_dependency "websocket-client-simple", "~> 0.8"
|
|
41
44
|
end
|
|
@@ -14,19 +14,25 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
14
14
|
MAX_LIST_LIMIT = 50
|
|
15
15
|
|
|
16
16
|
# Subscribes the client to the session-specific stream.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# When a valid session_id is provided, subscribes to that session.
|
|
18
|
+
# When omitted or zero, resolves to the most recent session (creating
|
|
19
|
+
# one if none exist) — this is the CQRS-compliant path where the
|
|
20
|
+
# server owns session resolution instead of a REST endpoint.
|
|
19
21
|
#
|
|
20
|
-
#
|
|
22
|
+
# Always transmits a session_changed signal so the client learns
|
|
23
|
+
# the authoritative session ID, followed by view_mode and history.
|
|
24
|
+
#
|
|
25
|
+
# @param params [Hash] optional :session_id (positive integer)
|
|
21
26
|
def subscribed
|
|
22
|
-
@current_session_id =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
@current_session_id = resolve_session_id
|
|
28
|
+
stream_from stream_name
|
|
29
|
+
|
|
30
|
+
session = Session.find_by(id: @current_session_id)
|
|
31
|
+
return unless session
|
|
32
|
+
|
|
33
|
+
transmit_session_changed(session)
|
|
34
|
+
transmit_view_mode(session)
|
|
35
|
+
transmit_history(session)
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
# Receives messages from clients and broadcasts them to all session subscribers.
|
|
@@ -37,32 +43,57 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
# Processes user input: persists the message and enqueues LLM processing.
|
|
46
|
+
# When the session is actively processing an agent request, the message
|
|
47
|
+
# is queued as "pending" and picked up after the current loop completes.
|
|
40
48
|
#
|
|
41
49
|
# @param data [Hash] must include "content" with the user's message text
|
|
42
50
|
def speak(data)
|
|
43
51
|
content = data["content"].to_s.strip
|
|
44
|
-
return if content.empty?
|
|
52
|
+
return if content.empty?
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
session = Session.find_by(id: @current_session_id)
|
|
55
|
+
return unless session
|
|
56
|
+
|
|
57
|
+
if session.processing?
|
|
58
|
+
Events::Bus.emit(Events::UserMessage.new(content: content, session_id: @current_session_id, status: Event::PENDING_STATUS))
|
|
59
|
+
else
|
|
60
|
+
Events::Bus.emit(Events::UserMessage.new(content: content, session_id: @current_session_id))
|
|
61
|
+
AgentRequestJob.perform_later(@current_session_id)
|
|
62
|
+
end
|
|
48
63
|
end
|
|
49
64
|
|
|
50
|
-
#
|
|
65
|
+
# Recalls the most recent pending message for editing. Deletes the
|
|
66
|
+
# pending event and broadcasts the recall so all clients remove it.
|
|
67
|
+
#
|
|
68
|
+
# @param data [Hash] must include "event_id" (positive integer)
|
|
69
|
+
def recall_pending(data)
|
|
70
|
+
event_id = data["event_id"].to_i
|
|
71
|
+
return if event_id <= 0
|
|
72
|
+
|
|
73
|
+
event = Event.find_by(
|
|
74
|
+
id: event_id,
|
|
75
|
+
session_id: @current_session_id,
|
|
76
|
+
event_type: "user_message",
|
|
77
|
+
status: Event::PENDING_STATUS
|
|
78
|
+
)
|
|
79
|
+
return unless event
|
|
80
|
+
|
|
81
|
+
event.destroy!
|
|
82
|
+
ActionCable.server.broadcast(stream_name, {"action" => "user_message_recalled", "event_id" => event_id})
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns recent root sessions with nested child metadata for session picker UI.
|
|
86
|
+
# Filters to root sessions only (no parent_session_id). Child sessions are
|
|
87
|
+
# nested under their parent with name and status information.
|
|
51
88
|
#
|
|
52
89
|
# @param data [Hash] optional "limit" (default 10, max 50)
|
|
53
90
|
def list_sessions(data)
|
|
54
91
|
limit = (data["limit"] || DEFAULT_LIST_LIMIT).to_i.clamp(1, MAX_LIST_LIMIT)
|
|
55
|
-
sessions = Session.recent(limit)
|
|
56
|
-
|
|
92
|
+
sessions = Session.root_sessions.recent(limit).includes(:child_sessions)
|
|
93
|
+
all_ids = sessions.flat_map { |session| [session.id] + session.child_sessions.map(&:id) }
|
|
94
|
+
counts = Event.where(session_id: all_ids).llm_messages.group(:session_id).count
|
|
57
95
|
|
|
58
|
-
result = sessions.map
|
|
59
|
-
{
|
|
60
|
-
id: session.id,
|
|
61
|
-
created_at: session.created_at.iso8601,
|
|
62
|
-
updated_at: session.updated_at.iso8601,
|
|
63
|
-
message_count: counts[session.id] || 0
|
|
64
|
-
}
|
|
65
|
-
end
|
|
96
|
+
result = sessions.map { |session| serialize_session_with_children(session, counts) }
|
|
66
97
|
transmit({"action" => "sessions_list", "sessions" => result})
|
|
67
98
|
end
|
|
68
99
|
|
|
@@ -86,6 +117,23 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
86
117
|
transmit_error("Session not found")
|
|
87
118
|
end
|
|
88
119
|
|
|
120
|
+
# Validates and saves an Anthropic subscription token to encrypted credentials.
|
|
121
|
+
# Format-validated and API-validated before storage. The token never enters the
|
|
122
|
+
# LLM context window — it flows directly from WebSocket to encrypted credentials.
|
|
123
|
+
#
|
|
124
|
+
# @param data [Hash] must include "token" (Anthropic subscription token string)
|
|
125
|
+
def save_token(data)
|
|
126
|
+
token = data["token"].to_s.strip
|
|
127
|
+
|
|
128
|
+
Providers::Anthropic.validate_token_format!(token)
|
|
129
|
+
Providers::Anthropic.validate_token_api!(token)
|
|
130
|
+
write_anthropic_token(token)
|
|
131
|
+
|
|
132
|
+
transmit({"action" => "token_saved"})
|
|
133
|
+
rescue Providers::Anthropic::TokenFormatError, Providers::Anthropic::AuthenticationError => error
|
|
134
|
+
transmit({"action" => "token_error", "message" => error.message})
|
|
135
|
+
end
|
|
136
|
+
|
|
89
137
|
# Changes the session's view mode and re-broadcasts the viewport.
|
|
90
138
|
# All clients on the session receive the mode change and fresh history.
|
|
91
139
|
#
|
|
@@ -109,6 +157,41 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
109
157
|
"session_#{@current_session_id}"
|
|
110
158
|
end
|
|
111
159
|
|
|
160
|
+
# Resolves the session to subscribe to. Uses the client-provided ID
|
|
161
|
+
# when valid, otherwise falls back to the most recent session or
|
|
162
|
+
# creates a new one.
|
|
163
|
+
#
|
|
164
|
+
# @return [Integer] resolved session ID
|
|
165
|
+
def resolve_session_id
|
|
166
|
+
id = params[:session_id].to_i
|
|
167
|
+
return id if id > 0
|
|
168
|
+
|
|
169
|
+
(Session.recent(1).first || Session.create!).id
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Transmits session metadata as a session_changed signal.
|
|
173
|
+
# Used on initial subscription and after session switches so the
|
|
174
|
+
# client can handle both paths with a single code path.
|
|
175
|
+
#
|
|
176
|
+
# Payload: session_id, name, parent_session_id, message_count,
|
|
177
|
+
# view_mode, active_skills, goals.
|
|
178
|
+
#
|
|
179
|
+
# @param session [Session] the session to announce
|
|
180
|
+
# @return [void]
|
|
181
|
+
def transmit_session_changed(session)
|
|
182
|
+
transmit({
|
|
183
|
+
"action" => "session_changed",
|
|
184
|
+
"session_id" => session.id,
|
|
185
|
+
"name" => session.name,
|
|
186
|
+
"parent_session_id" => session.parent_session_id,
|
|
187
|
+
"message_count" => session.events.llm_messages.count,
|
|
188
|
+
"view_mode" => session.view_mode,
|
|
189
|
+
"active_skills" => session.active_skills,
|
|
190
|
+
"active_workflow" => session.active_workflow,
|
|
191
|
+
"goals" => session.goals_summary
|
|
192
|
+
})
|
|
193
|
+
end
|
|
194
|
+
|
|
112
195
|
# Switches the channel to a different session: stops current stream,
|
|
113
196
|
# updates the session reference, starts the new stream, and sends
|
|
114
197
|
# a session_changed signal followed by chat history.
|
|
@@ -116,23 +199,18 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
116
199
|
stop_all_streams
|
|
117
200
|
@current_session_id = new_id
|
|
118
201
|
stream_from stream_name
|
|
202
|
+
|
|
119
203
|
session = Session.find(new_id)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"session_id" => new_id,
|
|
123
|
-
"message_count" => session.events.llm_messages.count,
|
|
124
|
-
"view_mode" => session.view_mode
|
|
125
|
-
})
|
|
126
|
-
transmit_history
|
|
204
|
+
transmit_session_changed(session)
|
|
205
|
+
transmit_history(session)
|
|
127
206
|
end
|
|
128
207
|
|
|
129
208
|
# Transmits the current view_mode so the TUI initializes correctly.
|
|
130
209
|
# Sends `{action: "view_mode", view_mode: <mode>}` to the subscribing client.
|
|
210
|
+
#
|
|
211
|
+
# @param session [Session] the session whose view_mode to transmit
|
|
131
212
|
# @return [void]
|
|
132
|
-
def transmit_view_mode
|
|
133
|
-
session = Session.find_by(id: @current_session_id)
|
|
134
|
-
return unless session
|
|
135
|
-
|
|
213
|
+
def transmit_view_mode(session)
|
|
136
214
|
transmit({"action" => "view_mode", "view_mode" => session.view_mode})
|
|
137
215
|
end
|
|
138
216
|
|
|
@@ -142,32 +220,64 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
142
220
|
# the transmitted payload. Tool events are included so the TUI can
|
|
143
221
|
# reconstruct tool call counters on reconnect.
|
|
144
222
|
# In debug mode, prepends the assembled system prompt as a special block.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
223
|
+
#
|
|
224
|
+
# Snapshots the viewport so subsequent event broadcasts can compute
|
|
225
|
+
# eviction diffs accurately.
|
|
226
|
+
#
|
|
227
|
+
# @param session [Session] the session whose history to transmit
|
|
228
|
+
def transmit_history(session)
|
|
149
229
|
transmit_system_prompt(session) if session.view_mode == "debug"
|
|
150
230
|
|
|
151
|
-
session
|
|
152
|
-
transmit(
|
|
231
|
+
each_viewport_event(session) do |event, payload|
|
|
232
|
+
transmit(payload)
|
|
153
233
|
end
|
|
154
234
|
end
|
|
155
235
|
|
|
156
236
|
# Broadcasts the re-decorated viewport to all clients on the session stream.
|
|
157
237
|
# Used after a view mode change to refresh all connected clients.
|
|
158
238
|
# In debug mode, prepends the assembled system prompt as a special block.
|
|
239
|
+
#
|
|
240
|
+
# Snapshots the viewport so subsequent event broadcasts can compute
|
|
241
|
+
# eviction diffs accurately.
|
|
242
|
+
#
|
|
159
243
|
# @param session [Session] the session whose viewport to broadcast
|
|
160
244
|
# @return [void]
|
|
161
245
|
def broadcast_viewport(session)
|
|
162
246
|
broadcast_system_prompt(session) if session.view_mode == "debug"
|
|
163
247
|
|
|
164
|
-
session
|
|
165
|
-
ActionCable.server.broadcast(stream_name,
|
|
248
|
+
each_viewport_event(session) do |event, payload|
|
|
249
|
+
ActionCable.server.broadcast(stream_name, payload)
|
|
166
250
|
end
|
|
167
251
|
end
|
|
168
252
|
|
|
253
|
+
# Loads the viewport, snapshots it for eviction tracking, and yields
|
|
254
|
+
# each event with its decorated payload. Snapshot uses snapshot_viewport!
|
|
255
|
+
# (not recalculate_viewport!) because full viewport refreshes don't need
|
|
256
|
+
# eviction diffs — clients clear their store before rendering.
|
|
257
|
+
#
|
|
258
|
+
# @param session [Session] the session whose viewport to iterate
|
|
259
|
+
# @yieldparam event [Event] the persisted event record
|
|
260
|
+
# @yieldparam payload [Hash] decorated payload ready for transmission
|
|
261
|
+
# @return [void]
|
|
262
|
+
def each_viewport_event(session)
|
|
263
|
+
viewport = session.viewport_events
|
|
264
|
+
session.snapshot_viewport!(viewport.map(&:id))
|
|
265
|
+
|
|
266
|
+
viewport.each do |event|
|
|
267
|
+
yield event, decorate_event_payload(event, session.view_mode)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Decorates an event for transmission to clients. Merges the event's
|
|
272
|
+
# database ID and structured decorator output into the payload.
|
|
273
|
+
# Used by {#transmit_history} and {#broadcast_viewport} for historical
|
|
274
|
+
# and viewport re-broadcast — live broadcasts use {Event::Broadcasting}.
|
|
275
|
+
#
|
|
276
|
+
# @param event [Event] persisted event record
|
|
277
|
+
# @param mode [String] view mode for decoration (default: "basic")
|
|
278
|
+
# @return [Hash] payload with "id" and optional "rendered" key
|
|
169
279
|
def decorate_event_payload(event, mode = "basic")
|
|
170
|
-
payload = event.payload
|
|
280
|
+
payload = event.payload.merge("id" => event.id)
|
|
171
281
|
decorator = EventDecorator.for(event)
|
|
172
282
|
return payload unless decorator
|
|
173
283
|
|
|
@@ -212,6 +322,46 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
212
322
|
}
|
|
213
323
|
end
|
|
214
324
|
|
|
325
|
+
# Merges the Anthropic subscription token into encrypted credentials,
|
|
326
|
+
# preserving existing keys (e.g. secret_key_base).
|
|
327
|
+
#
|
|
328
|
+
# @param token [String] validated Anthropic subscription token
|
|
329
|
+
# @return [void]
|
|
330
|
+
def write_anthropic_token(token)
|
|
331
|
+
CredentialStore.write("anthropic", "subscription_token" => token)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Serializes a root session with its children for the sessions_list response.
|
|
335
|
+
# Includes a :children key only when the session has child sessions.
|
|
336
|
+
#
|
|
337
|
+
# @param session [Session] root session to serialize
|
|
338
|
+
# @param counts [Hash<Integer, Integer>] session_id => llm_message count
|
|
339
|
+
# @return [Hash] with :id, :created_at, :updated_at, :message_count, and optional :children
|
|
340
|
+
def serialize_session_with_children(session, counts)
|
|
341
|
+
entry = {
|
|
342
|
+
id: session.id,
|
|
343
|
+
name: session.name,
|
|
344
|
+
created_at: session.created_at.iso8601,
|
|
345
|
+
updated_at: session.updated_at.iso8601,
|
|
346
|
+
message_count: counts[session.id] || 0
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
children = session.child_sessions.sort_by(&:created_at)
|
|
350
|
+
return entry unless children.any?
|
|
351
|
+
|
|
352
|
+
entry[:children] = children.map do |child|
|
|
353
|
+
{
|
|
354
|
+
id: child.id,
|
|
355
|
+
name: child.name,
|
|
356
|
+
processing: child.processing?,
|
|
357
|
+
message_count: counts[child.id] || 0,
|
|
358
|
+
created_at: child.created_at.iso8601
|
|
359
|
+
}
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
entry
|
|
363
|
+
end
|
|
364
|
+
|
|
215
365
|
def transmit_error(message)
|
|
216
366
|
transmit({"action" => "error", "message" => message})
|
|
217
367
|
end
|
|
@@ -3,22 +3,33 @@
|
|
|
3
3
|
# Decorates user_message events for display in the TUI.
|
|
4
4
|
# Basic mode returns role and content. Verbose mode adds a timestamp.
|
|
5
5
|
# Debug mode adds token count (exact when counted, estimated when not).
|
|
6
|
+
# Pending messages include `status: "pending"` so the TUI renders them
|
|
7
|
+
# with a visual indicator (dimmed, clock icon).
|
|
6
8
|
class UserMessageDecorator < EventDecorator
|
|
7
9
|
# @return [Hash] structured user message data
|
|
8
|
-
# `{role: :user, content: String}`
|
|
10
|
+
# `{role: :user, content: String}` or with `status: "pending"` when queued
|
|
9
11
|
def render_basic
|
|
10
|
-
{role: :user, content: content}
|
|
12
|
+
base = {role: :user, content: content}
|
|
13
|
+
base[:status] = "pending" if pending?
|
|
14
|
+
base
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
# @return [Hash] structured user message with nanosecond timestamp
|
|
14
|
-
# `{role: :user, content: String, timestamp: Integer|nil}`
|
|
15
18
|
def render_verbose
|
|
16
|
-
{role: :user, content: content, timestamp: timestamp}
|
|
19
|
+
base = {role: :user, content: content, timestamp: timestamp}
|
|
20
|
+
base[:status] = "pending" if pending?
|
|
21
|
+
base
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
# @return [Hash] verbose output plus token count for debugging
|
|
20
|
-
# `{role: :user, content: String, timestamp: Integer|nil, tokens: Integer, estimated: Boolean}`
|
|
21
25
|
def render_debug
|
|
22
26
|
render_verbose.merge(token_info)
|
|
23
27
|
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] true when this message is queued but not yet sent to LLM
|
|
32
|
+
def pending?
|
|
33
|
+
payload["status"] == Event::PENDING_STATUS
|
|
34
|
+
end
|
|
24
35
|
end
|
|
@@ -26,23 +26,76 @@ class AgentRequestJob < ApplicationJob
|
|
|
26
26
|
|
|
27
27
|
discard_on ActiveRecord::RecordNotFound
|
|
28
28
|
discard_on Providers::Anthropic::AuthenticationError do |job, error|
|
|
29
|
+
session_id = job.arguments.first
|
|
30
|
+
# Persistent system message for the event log
|
|
29
31
|
Events::Bus.emit(Events::SystemMessage.new(
|
|
30
32
|
content: "Authentication failed: #{error.message}",
|
|
31
|
-
session_id:
|
|
33
|
+
session_id: session_id
|
|
32
34
|
))
|
|
35
|
+
# Transient signal to trigger TUI token setup popup (not persisted)
|
|
36
|
+
ActionCable.server.broadcast(
|
|
37
|
+
"session_#{session_id}",
|
|
38
|
+
{"action" => "authentication_required", "message" => error.message}
|
|
39
|
+
)
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
# @param session_id [Integer] ID of the session to process
|
|
36
43
|
def perform(session_id)
|
|
37
44
|
session = Session.find(session_id)
|
|
45
|
+
|
|
46
|
+
# Atomic: only one job processes a session at a time. If another job
|
|
47
|
+
# is already running, this one exits — the running job will pick up
|
|
48
|
+
# any pending messages after its current loop completes.
|
|
49
|
+
return unless claim_processing(session_id)
|
|
50
|
+
|
|
51
|
+
# Run analytical brain BEFORE the main agent on user messages so
|
|
52
|
+
# activated skills are available for the current response.
|
|
53
|
+
run_analytical_brain_blocking(session)
|
|
54
|
+
|
|
38
55
|
agent_loop = AgentLoop.new(session: session)
|
|
39
|
-
|
|
56
|
+
loop do
|
|
57
|
+
agent_loop.run
|
|
58
|
+
promoted = session.promote_pending_messages!
|
|
59
|
+
break if promoted == 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Non-blocking analytical brain run after agent completes —
|
|
63
|
+
# handles post-response updates (renaming, skill changes).
|
|
64
|
+
session.schedule_analytical_brain!
|
|
40
65
|
ensure
|
|
66
|
+
release_processing(session_id)
|
|
41
67
|
agent_loop&.finalize
|
|
42
68
|
end
|
|
43
69
|
|
|
44
70
|
private
|
|
45
71
|
|
|
72
|
+
# Runs the analytical brain synchronously before the main agent loop.
|
|
73
|
+
# Respects the blocking_on_user_message setting and session guards
|
|
74
|
+
# (skips sub-agents and sessions with too few messages).
|
|
75
|
+
def run_analytical_brain_blocking(session)
|
|
76
|
+
return unless Anima::Settings.analytical_brain_blocking_on_user_message
|
|
77
|
+
return if session.sub_agent?
|
|
78
|
+
|
|
79
|
+
AnalyticalBrain::Runner.new(session).call
|
|
80
|
+
rescue => error
|
|
81
|
+
# The analytical brain is best-effort: skill activation enhances the
|
|
82
|
+
# response but the main agent must still reply even if it fails.
|
|
83
|
+
msg = "FAILED (blocking) session=#{session.id}: #{error.class}: #{error.message}"
|
|
84
|
+
Rails.logger.error("Analytical brain #{msg}")
|
|
85
|
+
AnalyticalBrain.logger.error("#{msg}\n#{error.backtrace&.first(10)&.join("\n")}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Sets the session's processing flag atomically. Returns true if this
|
|
89
|
+
# job claimed the lock, false if another job already holds it.
|
|
90
|
+
def claim_processing(session_id)
|
|
91
|
+
Session.where(id: session_id, processing: false).update_all(processing: true) == 1
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Clears the processing flag so the session can accept new jobs.
|
|
95
|
+
def release_processing(session_id)
|
|
96
|
+
Session.where(id: session_id).update_all(processing: false)
|
|
97
|
+
end
|
|
98
|
+
|
|
46
99
|
# Emits a system message before each retry so the user sees
|
|
47
100
|
# "retrying..." instead of nothing.
|
|
48
101
|
def retry_job(options = {})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Runs the analytical brain — a phantom LLM loop that observes the main
|
|
4
|
+
# session and performs background maintenance (currently: session naming).
|
|
5
|
+
#
|
|
6
|
+
# Replaces {GenerateSessionNameJob} with a tool-based architecture that
|
|
7
|
+
# future tickets will expand with skill activation, goal tracking, etc.
|
|
8
|
+
#
|
|
9
|
+
# Scheduling guards live in {Session#schedule_analytical_brain!} — this
|
|
10
|
+
# job always runs when called.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# AnalyticalBrainJob.perform_later(session.id)
|
|
14
|
+
class AnalyticalBrainJob < ApplicationJob
|
|
15
|
+
queue_as :default
|
|
16
|
+
|
|
17
|
+
retry_on Providers::Anthropic::TransientError,
|
|
18
|
+
wait: :polynomially_longer, attempts: 3
|
|
19
|
+
|
|
20
|
+
discard_on ActiveRecord::RecordNotFound
|
|
21
|
+
discard_on Providers::Anthropic::AuthenticationError
|
|
22
|
+
|
|
23
|
+
# @param session_id [Integer] the main Session to analyze
|
|
24
|
+
def perform(session_id)
|
|
25
|
+
brain_log = AnalyticalBrain.logger
|
|
26
|
+
session = Session.find(session_id)
|
|
27
|
+
brain_log.info("async job started for session=#{session_id}")
|
|
28
|
+
AnalyticalBrain::Runner.new(session).call
|
|
29
|
+
rescue => error
|
|
30
|
+
brain_log.error("FAILED (async) session=#{session_id}: #{error.class}: #{error.message}")
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
end
|