anima-core 1.2.0 → 1.4.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 +14 -8
- data/README.md +96 -23
- data/agents/codebase-analyzer.md +1 -1
- data/agents/codebase-pattern-finder.md +1 -1
- data/agents/documentation-researcher.md +1 -1
- data/agents/thoughts-analyzer.md +1 -1
- data/agents/web-search-researcher.md +2 -2
- data/app/channels/session_channel.rb +53 -35
- data/app/decorators/tool_call_decorator.rb +7 -7
- data/app/decorators/user_message_decorator.rb +3 -17
- data/app/jobs/agent_request_job.rb +15 -6
- data/app/jobs/passive_recall_job.rb +6 -11
- data/app/models/concerns/message/broadcasting.rb +1 -0
- data/app/models/goal.rb +14 -0
- data/app/models/message.rb +13 -31
- data/app/models/pending_message.rb +191 -0
- data/app/models/secret.rb +72 -0
- data/app/models/session.rb +480 -271
- data/bin/inspect-cassette +144 -0
- data/bin/release +212 -0
- data/bin/with-llms +20 -0
- data/config/database.yml +1 -0
- data/config/environments/test.rb +5 -0
- data/config/initializers/time_nanoseconds.rb +11 -0
- data/db/cable_structure.sql +9 -0
- data/db/migrate/20260328100000_create_secrets.rb +15 -0
- data/db/migrate/20260328152142_add_evicted_at_to_goals.rb +6 -0
- data/db/migrate/20260329120000_create_pending_messages.rb +11 -0
- data/db/migrate/20260330120000_add_source_to_pending_messages.rb +8 -0
- data/db/migrate/20260401180000_add_api_metrics_to_messages.rb +7 -0
- data/db/migrate/20260401210935_remove_recalled_message_ids_from_sessions.rb +5 -0
- data/db/migrate/20260403080031_add_initial_cwd_to_sessions.rb +5 -0
- data/db/queue_structure.sql +61 -0
- data/db/structure.sql +120 -0
- data/lib/agent_loop.rb +53 -51
- data/lib/agents/definition.rb +1 -1
- data/lib/analytical_brain/runner.rb +19 -6
- data/lib/analytical_brain/tools/activate_skill.rb +2 -2
- data/lib/analytical_brain/tools/assign_nickname.rb +1 -1
- data/lib/analytical_brain/tools/deactivate_skill.rb +2 -1
- data/lib/analytical_brain/tools/deactivate_workflow.rb +2 -1
- data/lib/analytical_brain/tools/finish_goal.rb +3 -0
- data/lib/analytical_brain/tools/goal_messaging.rb +28 -0
- data/lib/analytical_brain/tools/read_workflow.rb +2 -2
- data/lib/analytical_brain/tools/set_goal.rb +5 -1
- data/lib/analytical_brain/tools/update_goal.rb +5 -1
- data/lib/anima/cli/mcp/secrets.rb +4 -4
- data/lib/anima/cli/mcp.rb +4 -4
- data/lib/anima/cli.rb +41 -13
- data/lib/anima/installer.rb +20 -1
- data/lib/anima/settings.rb +37 -2
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +1 -1
- data/lib/credential_store.rb +17 -66
- data/lib/events/agent_message.rb +14 -0
- data/lib/events/base.rb +1 -1
- data/lib/events/subscribers/persister.rb +12 -18
- data/lib/events/subscribers/subagent_message_router.rb +18 -9
- data/lib/events/user_message.rb +2 -13
- data/lib/llm/client.rb +91 -50
- data/lib/mcp/config.rb +2 -2
- data/lib/mcp/secrets.rb +7 -8
- data/lib/mneme/compressed_viewport.rb +9 -5
- data/lib/mneme/passive_recall.rb +85 -16
- data/lib/mneme/runner.rb +15 -4
- data/lib/providers/anthropic.rb +112 -7
- data/lib/shell_session.rb +239 -18
- data/lib/tools/base.rb +22 -0
- data/lib/tools/bash.rb +61 -7
- data/lib/tools/edit.rb +2 -2
- data/lib/tools/mark_goal_completed.rb +85 -0
- data/lib/tools/read.rb +2 -1
- data/lib/tools/recall.rb +98 -0
- data/lib/tools/registry.rb +41 -7
- data/lib/tools/remember.rb +1 -1
- data/lib/tools/response_truncator.rb +70 -0
- data/lib/tools/spawn_specialist.rb +11 -8
- data/lib/tools/spawn_subagent.rb +19 -13
- data/lib/tools/subagent_prompts.rb +41 -5
- data/lib/tools/think.rb +23 -0
- data/lib/tools/write.rb +1 -1
- data/lib/tui/app.rb +545 -137
- data/lib/tui/braille_spinner.rb +152 -0
- data/lib/tui/cable_client.rb +13 -20
- data/lib/tui/decorators/base_decorator.rb +40 -11
- data/lib/tui/decorators/bash_decorator.rb +3 -3
- data/lib/tui/decorators/edit_decorator.rb +7 -4
- data/lib/tui/decorators/read_decorator.rb +6 -8
- data/lib/tui/decorators/think_decorator.rb +4 -6
- data/lib/tui/decorators/web_get_decorator.rb +4 -3
- data/lib/tui/decorators/write_decorator.rb +7 -4
- data/lib/tui/flash.rb +19 -14
- data/lib/tui/formatting.rb +33 -0
- data/lib/tui/input_buffer.rb +6 -6
- data/lib/tui/message_store.rb +159 -27
- data/lib/tui/performance_logger.rb +2 -3
- data/lib/tui/screens/chat.rb +302 -103
- data/lib/tui/settings.rb +86 -0
- data/skills/activerecord/SKILL.md +1 -1
- data/skills/dragonruby/SKILL.md +1 -1
- data/skills/draper-decorators/SKILL.md +1 -1
- data/skills/gh-issue.md +1 -1
- data/skills/mcp-server/SKILL.md +1 -1
- data/skills/ratatui-ruby/SKILL.md +1 -1
- data/skills/rspec/SKILL.md +1 -1
- data/templates/config.toml +30 -1
- data/templates/tui.toml +209 -0
- metadata +24 -3
- data/config/initializers/fts5_schema_dump.rb +0 -21
- data/lib/environment_probe.rb +0 -232
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 283cb2ad728734b96a5badc8b6fc2624c920d3dbefd17412bf5c5f4ae452d6e3
|
|
4
|
+
data.tar.gz: 0ecef454ffd58b1a4c232338e58a4d20fe4b42f4c3d2ecd6aca6dcc446b0daed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69115665f072f86b590222cdf4c6ec3ca75be2287fea4d1b50b2361525cb31b75e561b6f6e0ae0e375dfa7c5717b6d549237202f76a7dd4a64826c19a26311fe
|
|
7
|
+
data.tar.gz: b7b35426013e036bc18c5bd8906f26cadd360be8246ef213018deea41cac52e04747a321c6aaf5835dc9f04ff6c19f04bd344de97c08227591bf6563e03abab1
|
data/.reek.yml
CHANGED
|
@@ -13,8 +13,8 @@ detectors:
|
|
|
13
13
|
NilCheck:
|
|
14
14
|
exclude:
|
|
15
15
|
- "Anima::Settings#get"
|
|
16
|
+
- "TUI::Settings#get"
|
|
16
17
|
# Rescue blocks naturally reference the error object more than self.
|
|
17
|
-
# EnvironmentProbe assembles output from local data structures — not envy.
|
|
18
18
|
# Brain transcript builds from event collection — the method's entire purpose.
|
|
19
19
|
# ConfigMigrator text processing methods naturally reference local line arrays.
|
|
20
20
|
# ToolDecorator subclasses operate on the tool result — that's the pattern.
|
|
@@ -22,7 +22,6 @@ detectors:
|
|
|
22
22
|
FeatureEnvy:
|
|
23
23
|
exclude:
|
|
24
24
|
- "AnalyticalBrainJob#perform"
|
|
25
|
-
- "EnvironmentProbe"
|
|
26
25
|
- "AnalyticalBrain::Runner#build_messages"
|
|
27
26
|
- "Anima::ConfigMigrator"
|
|
28
27
|
- "WebGetToolDecorator"
|
|
@@ -35,8 +34,11 @@ detectors:
|
|
|
35
34
|
- "Tools::SpawnSubagent#spawn_child"
|
|
36
35
|
- "Tools::SpawnSpecialist#spawn_child"
|
|
37
36
|
- "Tools::SpawnSpecialist#execute"
|
|
38
|
-
#
|
|
37
|
+
# Spawn helpers operate on child session — inherent to the mixin pattern.
|
|
39
38
|
- "Tools::SubagentPrompts#assign_nickname_via_brain"
|
|
39
|
+
- "Tools::SubagentPrompts#inject_identity_context"
|
|
40
|
+
# Registry dispatches to tool's threshold method — duck-typing delegation.
|
|
41
|
+
- "Tools::Registry#truncation_threshold"
|
|
40
42
|
# Goal tools operate on goal objects — inherent to the pattern.
|
|
41
43
|
- "AnalyticalBrain::Tools::UpdateGoal#execute"
|
|
42
44
|
# Validation methods naturally reference the validated value more than self.
|
|
@@ -65,13 +67,13 @@ detectors:
|
|
|
65
67
|
- "WebGetToolDecorator#text_html"
|
|
66
68
|
# Session model is the core domain object — methods grow naturally.
|
|
67
69
|
# Mcp CLI accumulates subcommand helpers across add/remove/list/secrets.
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
+
# ShellSession probes multiple orthogonal facets (CWD, Git, project files)
|
|
71
|
+
# and manages PTY lifecycle — methods grow with responsibilities.
|
|
70
72
|
TooManyMethods:
|
|
71
73
|
exclude:
|
|
72
74
|
- "Session"
|
|
73
75
|
- "Anima::CLI::Mcp"
|
|
74
|
-
- "
|
|
76
|
+
- "ShellSession"
|
|
75
77
|
# Runner composes system prompt from modular sections — methods grow with responsibilities.
|
|
76
78
|
- "AnalyticalBrain::Runner"
|
|
77
79
|
# Decorators branch on tool type across 4 render modes — inherent to the pattern.
|
|
@@ -83,15 +85,19 @@ detectors:
|
|
|
83
85
|
# Runner checks session type to compose responsibilities — the core dispatch.
|
|
84
86
|
- "AnalyticalBrain::Runner"
|
|
85
87
|
# EventDecorator holds shared rendering constants (icons, markers, dispatch maps).
|
|
86
|
-
#
|
|
88
|
+
# Message holds domain type constants (TYPES, CONTEXT_TYPES, LLM_TYPES, etc.).
|
|
87
89
|
TooManyConstants:
|
|
88
90
|
exclude:
|
|
89
91
|
- "EventDecorator"
|
|
90
|
-
- "
|
|
92
|
+
- "Message"
|
|
91
93
|
# encode_utf8 is descriptive — the digit triggers a false positive.
|
|
92
94
|
UncommunicativeMethodName:
|
|
93
95
|
exclude:
|
|
94
96
|
- "ToolDecorator#self.encode_utf8"
|
|
97
|
+
# Registry uses respond_to? for duck-typing dispatch with MCP tool instances.
|
|
98
|
+
ManualDispatch:
|
|
99
|
+
exclude:
|
|
100
|
+
- "Tools::Registry#truncation_threshold"
|
|
95
101
|
# Abstract base class methods declare parameters for the subclass contract.
|
|
96
102
|
UnusedParameters:
|
|
97
103
|
exclude:
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Anima is different. It's built on the premise that if you want an agent — a re
|
|
|
14
14
|
|
|
15
15
|
**Memory that works like memory.** Other systems bolt on memory as an afterthought — filing cabinets the agent has to consciously open mid-task. It never does; the truck is already moving. Anima's memory department ([Mneme](#semantic-memory-mneme)) runs as a third brain process on the event bus. It summarizes what's about to leave the viewport. It compresses short-term into long-term, like biological memory consolidating during sleep. It pins critical moments to active goals so exact instructions survive where summaries would lose nuance. And it recalls — automatically, passively — surfacing relevant older memories right after the soul, right before the present. The agent doesn't decide to remember. It just remembers.
|
|
16
16
|
|
|
17
|
-
**Sub-agents that
|
|
17
|
+
**Sub-agents that know who they are.** When Anima spawns a sub-agent, it starts clean — identity, task, and nothing else. No inherited conversation history means the sub-agent works on its task, not the parent's trajectory. Context flows through explicit messages, not leaked assistant turns.
|
|
18
18
|
|
|
19
19
|
**A soul the agent writes itself.** Anima's first session is birth. The agent wakes up, explores its world, meets its human, and writes its own identity. Not a personality description in a config file — a living document the agent authors and evolves. Always in context, always its own.
|
|
20
20
|
|
|
@@ -63,7 +63,7 @@ Anima (Ruby, Rails 8.1 headless)
|
|
|
63
63
|
├── Skills — domain knowledge bundles (Markdown, user-extensible)
|
|
64
64
|
├── Workflows — operational recipes for multi-step tasks
|
|
65
65
|
├── MCP — external tool integration (Model Context Protocol)
|
|
66
|
-
├── Sub-agents — autonomous child sessions with
|
|
66
|
+
├── Sub-agents — autonomous child sessions with isolated context
|
|
67
67
|
├── Mneme — memory department (summarization, compression, pinning, recall)
|
|
68
68
|
│
|
|
69
69
|
│ Designed:
|
|
@@ -129,10 +129,11 @@ State directory (`~/.anima/`):
|
|
|
129
129
|
```
|
|
130
130
|
~/.anima/
|
|
131
131
|
├── soul.md # Agent's self-authored identity (always in context)
|
|
132
|
-
├── config.toml #
|
|
132
|
+
├── config.toml # Brain settings (hot-reloadable)
|
|
133
|
+
├── tui.toml # TUI settings (hot-reloadable)
|
|
133
134
|
├── mcp.toml # MCP server configuration
|
|
134
135
|
├── config/
|
|
135
|
-
│ └── credentials/
|
|
136
|
+
│ └── credentials/ # Rails encrypted credentials (includes AR encryption keys)
|
|
136
137
|
├── agents/ # User-defined specialist agents (override built-ins)
|
|
137
138
|
├── skills/ # User-defined skills (override built-ins)
|
|
138
139
|
├── workflows/ # User-defined workflows (override built-ins)
|
|
@@ -141,7 +142,7 @@ State directory (`~/.anima/`):
|
|
|
141
142
|
└── tmp/
|
|
142
143
|
```
|
|
143
144
|
|
|
144
|
-
Updates: `anima update` — upgrades the gem, merges new config settings into
|
|
145
|
+
Updates: `anima update` — upgrades the gem, merges new config settings into both `config.toml` and `tui.toml` without overwriting customized values, and restarts the systemd service if it's running. Use `anima update --migrate-only` to skip the gem upgrade and only add missing config keys.
|
|
145
146
|
|
|
146
147
|
### Authentication Setup
|
|
147
148
|
|
|
@@ -149,7 +150,7 @@ Anima uses your Claude Pro/Max subscription for API access. You need a setup-tok
|
|
|
149
150
|
|
|
150
151
|
1. Run `claude setup-token` in a terminal to get your token
|
|
151
152
|
2. In the TUI, press `Ctrl+a → a` to open the token setup popup
|
|
152
|
-
3. Paste the token and press Enter — Anima validates it against the Anthropic API and saves it to encrypted
|
|
153
|
+
3. Paste the token and press Enter — Anima validates it against the Anthropic API and saves it to the encrypted secrets database
|
|
153
154
|
|
|
154
155
|
The popup also activates automatically when Anima detects a missing or invalid token. If the token expires, repeat the process with a new one.
|
|
155
156
|
|
|
@@ -162,18 +163,23 @@ The agent has access to these built-in tools:
|
|
|
162
163
|
| Tool | Description |
|
|
163
164
|
|------|-------------|
|
|
164
165
|
| `bash` | Execute shell commands with persistent working directory |
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
166
|
+
| `read_file` | Read files with smart truncation and offset/limit paging |
|
|
167
|
+
| `write_file` | Create or overwrite files |
|
|
168
|
+
| `edit_file` | Surgical text replacement with uniqueness constraint |
|
|
168
169
|
| `web_get` | Fetch content from HTTP/HTTPS URLs (HTML → Markdown, JSON → TOON) |
|
|
169
170
|
| `spawn_specialist` | Spawn a named specialist sub-agent from the registry |
|
|
170
171
|
| `spawn_subagent` | Spawn a generic child session with custom tool grants |
|
|
172
|
+
| `think` | Think out loud or silently — reasoning step between tool calls |
|
|
173
|
+
| `recall` | Search past conversations by keywords (FTS5). Returns ranked snippets with message IDs for drill-down |
|
|
174
|
+
| `remember` | Recall full conversation context around a past message at fractal resolution |
|
|
175
|
+
| `open_issue` | File a self-improvement issue when something is broken, missing, or could be better |
|
|
176
|
+
| `mark_goal_completed` | Sub-agent only: signal task completion and deliver results to parent |
|
|
171
177
|
|
|
172
178
|
Plus dynamic tools from configured MCP servers, namespaced as `server_name__tool_name`.
|
|
173
179
|
|
|
174
180
|
### Sub-Agents
|
|
175
181
|
|
|
176
|
-
Sub-agents aren't processes — they're sessions on the same event bus. When a sub-agent spawns,
|
|
182
|
+
Sub-agents aren't processes — they're sessions on the same event bus. When a sub-agent spawns, it starts with a clean context: a system prompt (identity + communication instructions), a Goal from the task description, and a single user message containing the task — auto-pinned so it survives viewport eviction. No parent conversation history. Sub-agents inherit the parent shell's working directory at spawn time and use a separate model and token budget (configurable via `subagent_model` and `subagent_token_budget`).
|
|
177
183
|
|
|
178
184
|
Two types:
|
|
179
185
|
|
|
@@ -189,22 +195,25 @@ Two types:
|
|
|
189
195
|
|
|
190
196
|
**Generic Sub-agents** — child sessions with custom tool grants for ad-hoc tasks. Each generic sub-agent gets a Haiku-generated nickname (e.g. `@loop-sleuth`, `@api-scout`) for @mention addressing.
|
|
191
197
|
|
|
192
|
-
|
|
198
|
+
Each sub-agent is spawned with a single **Goal** from its task description and a pinned user message containing the task text. When done, the sub-agent calls `mark_goal_completed` to deliver results to the parent — this is the explicit finish line that prevents runaway agents. Sub-agents also get half the main agent's thinking budget to limit scope creep.
|
|
199
|
+
|
|
200
|
+
Between spawn and completion, sub-agents communicate through natural text — their `agent_message` events route to the parent session automatically, and the parent replies via `@name` mentions. Workers become colleagues.
|
|
193
201
|
|
|
194
202
|
### Skills
|
|
195
203
|
|
|
196
|
-
Domain knowledge bundles loaded from Markdown files. Skills provide specialized expertise that the analytical brain activates
|
|
204
|
+
Domain knowledge bundles loaded from Markdown files. Skills provide specialized expertise that the analytical brain activates based on conversation context. Skill content enters the conversation as phantom tool_use/tool_result pairs through the `PendingMessage` promotion flow — the same mechanism used for sub-agent messages. This keeps the system prompt stable for prompt caching while skills flow through the sliding window like regular messages.
|
|
197
205
|
|
|
198
206
|
- **Built-in skills:** ActiveRecord, Draper decorators, DragonRuby, MCP server, RatatuiRuby, RSpec, GitHub issues
|
|
199
207
|
- **User skills:** Drop `.md` files into `~/.anima/skills/` to add custom knowledge
|
|
200
208
|
- **Override:** User skills with the same name replace built-in ones
|
|
201
209
|
- **Format:** Flat files (`skill-name.md`) or directories (`skill-name/SKILL.md` with `examples/` and `references/`)
|
|
210
|
+
- **Viewport deduplication:** The brain's skill catalog excludes skills already visible in the viewport, preventing redundant activation
|
|
202
211
|
|
|
203
212
|
Active skills are displayed in the TUI HUD panel (toggle with `C-a → h`).
|
|
204
213
|
|
|
205
214
|
### Workflows
|
|
206
215
|
|
|
207
|
-
Operational recipes that describe multi-step tasks. Unlike skills (domain knowledge), workflows describe WHAT to do. The analytical brain activates a workflow when it recognizes a matching task, converts the prose into tracked goals, and deactivates it when done.
|
|
216
|
+
Operational recipes that describe multi-step tasks. Unlike skills (domain knowledge), workflows describe WHAT to do. The analytical brain activates a workflow when it recognizes a matching task, converts the prose into tracked goals, and deactivates it when done. Like skills, workflow content enters the conversation as phantom tool pairs through the same `PendingMessage` flow.
|
|
208
217
|
|
|
209
218
|
- **Built-in workflows:** `feature`, `commit`, `create_plan`, `implement_plan`, `review_pr`, `create_note`, `research_codebase`, `decompose_ticket`, and more
|
|
210
219
|
- **User workflows:** Drop `.md` files into `~/.anima/workflows/` to add custom workflows
|
|
@@ -255,12 +264,12 @@ anima mcp add fs -- mcp-server-filesystem --root / # Add stdio server
|
|
|
255
264
|
anima mcp add -s api_key=sk-xxx linear https://... # Add with secret
|
|
256
265
|
anima mcp remove sentry # Remove server
|
|
257
266
|
|
|
258
|
-
anima mcp secrets set linear_api_key=sk-xxx # Store secret in encrypted
|
|
267
|
+
anima mcp secrets set linear_api_key=sk-xxx # Store secret in encrypted database
|
|
259
268
|
anima mcp secrets list # List secret names (not values)
|
|
260
269
|
anima mcp secrets remove linear_api_key # Remove secret
|
|
261
270
|
```
|
|
262
271
|
|
|
263
|
-
Secrets are stored in
|
|
272
|
+
Secrets are stored in an encrypted database table (Active Record Encryption) and interpolated via `${credential:key_name}` syntax in any TOML string value.
|
|
264
273
|
|
|
265
274
|
### Analytical Brain
|
|
266
275
|
|
|
@@ -270,7 +279,7 @@ The analytical brain observes the main conversation between turns and handles ev
|
|
|
270
279
|
|
|
271
280
|
- **Skill activation** — activates/deactivates domain knowledge based on conversation context
|
|
272
281
|
- **Workflow management** — recognizes tasks, activates matching workflows, tracks lifecycle
|
|
273
|
-
- **Goal tracking** — creates root goals and sub-goals as work progresses, marks them complete
|
|
282
|
+
- **Goal tracking** — creates root goals and sub-goals as work progresses, marks them complete, evicts finished goals from context after a configurable message threshold
|
|
274
283
|
- **Session naming** — generates emoji + short name when the topic becomes clear
|
|
275
284
|
|
|
276
285
|
Each of these would be a context switch for the main agent — a chore that competes with the primary task. For the analytical brain, they ARE the primary task. Two agents, each in their own flow state.
|
|
@@ -279,7 +288,9 @@ Goals form a two-level hierarchy (root goals with sub-goals) and are displayed i
|
|
|
279
288
|
|
|
280
289
|
### Configuration
|
|
281
290
|
|
|
282
|
-
|
|
291
|
+
Brain and TUI have separate config files — both hot-reloadable (no restart needed).
|
|
292
|
+
|
|
293
|
+
**Brain settings** (`~/.anima/config.toml`):
|
|
283
294
|
|
|
284
295
|
```toml
|
|
285
296
|
[llm]
|
|
@@ -287,7 +298,9 @@ model = "claude-opus-4-6"
|
|
|
287
298
|
fast_model = "claude-haiku-4-5"
|
|
288
299
|
max_tokens = 8192
|
|
289
300
|
max_tool_rounds = 250
|
|
290
|
-
token_budget =
|
|
301
|
+
token_budget = 120_000
|
|
302
|
+
subagent_model = "claude-sonnet-4-6"
|
|
303
|
+
subagent_token_budget = 90_000
|
|
291
304
|
|
|
292
305
|
[timeouts]
|
|
293
306
|
api = 300
|
|
@@ -296,13 +309,34 @@ command = 30
|
|
|
296
309
|
[analytical_brain]
|
|
297
310
|
max_tokens = 4096
|
|
298
311
|
blocking_on_user_message = true
|
|
299
|
-
|
|
312
|
+
message_window = 20
|
|
300
313
|
|
|
301
314
|
[session]
|
|
302
315
|
default_view_mode = "basic"
|
|
303
316
|
name_generation_interval = 30
|
|
304
317
|
```
|
|
305
318
|
|
|
319
|
+
**TUI settings** (`~/.anima/tui.toml`):
|
|
320
|
+
|
|
321
|
+
```toml
|
|
322
|
+
[connection]
|
|
323
|
+
default_host = "localhost:42134" # Override per-launch with --host
|
|
324
|
+
|
|
325
|
+
[chat]
|
|
326
|
+
scroll_step = 1
|
|
327
|
+
viewport_back_buffer = 3
|
|
328
|
+
|
|
329
|
+
[theme]
|
|
330
|
+
rate_limit_warning = 70 # Yellow at 70%
|
|
331
|
+
rate_limit_critical = 90 # Red at 90%
|
|
332
|
+
user_message_bg = 22 # 256-color: dark green
|
|
333
|
+
assistant_message_bg = 17 # 256-color: dark navy
|
|
334
|
+
scrollbar_thumb = "cyan"
|
|
335
|
+
border_focused = "yellow"
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The TUI is a standalone client with zero Rails dependency. Its settings cover connection tuning, scroll behavior, terminal watchdog, theme colors, and performance logging. See `~/.anima/tui.toml` for all available options.
|
|
339
|
+
|
|
306
340
|
## Design
|
|
307
341
|
|
|
308
342
|
### Three Layers (mirroring biology)
|
|
@@ -331,7 +365,7 @@ Events flow through two channels:
|
|
|
331
365
|
1. **In-process** — Rails Structured Event Reporter (local subscribers like Persister)
|
|
332
366
|
2. **Over the wire** — Action Cable WebSocket (`Event::Broadcasting` callbacks push to connected TUI clients)
|
|
333
367
|
|
|
334
|
-
Events fire, subscribers react, state updates. The system prompt — soul
|
|
368
|
+
Events fire, subscribers react, state updates. The system prompt — soul and current goals — is assembled fresh for each LLM call from live state, not from the event stream. Skills and workflows flow through the message stream as phantom tool pairs, keeping the system prompt stable for prompt caching. The agent's identity (soul.md) is always current, never stale.
|
|
335
369
|
|
|
336
370
|
### Context as Viewport, Not Tape
|
|
337
371
|
|
|
@@ -341,7 +375,7 @@ The viewport is a live query, not a log. It walks events newest-first until the
|
|
|
341
375
|
|
|
342
376
|
This means sessions are endless. No compaction. No lossy rewriting. The model always operates in fresh, high-quality context. The [dumb zone](https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/ace-fca.md) never arrives. Meanwhile, Mneme runs as a background department — summarizing evicted events into persistent snapshots so past context is preserved, not destroyed.
|
|
343
377
|
|
|
344
|
-
Sub-agent viewports
|
|
378
|
+
Sub-agent viewports use the same mechanism — their own events only, no parent context inheritance. The parent provides context through the task description, and the sub-agent builds its own conversation from a clean slate.
|
|
345
379
|
|
|
346
380
|
### Brain as Microservices on a Shared Event Bus
|
|
347
381
|
|
|
@@ -396,6 +430,45 @@ The difference from every other system: memory isn't a tool the agent uses. It's
|
|
|
396
430
|
|
|
397
431
|
The right-side HUD panel shows session state at a glance: session name, goals (with status icons), active skills, workflow, and sub-agents. Toggle with `C-a → h`; when hidden, the input border shows `C-a → h HUD` as a reminder.
|
|
398
432
|
|
|
433
|
+
**Braille spinner**: An animated braille character (U+2800-U+28FF) replaces the old "Thinking..." label in both the chat viewport and HUD. Each processing state has a distinct animation pattern — smooth snake rotation for LLM generation, staccato pulse for tool execution, rapid deceleration for interrupting. Sub-agents in the HUD show state-driven icons: `●` (generating, green), `◉` (tool executing, green), `●` (interrupting, red), `◌` (idle, grey).
|
|
434
|
+
|
|
435
|
+
**Token Economy HUD**: A fixed panel at the bottom of the HUD displays API economics extracted from every Anthropic response:
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
╭ 📊 Token Economy ────────────────────╮
|
|
439
|
+
│ 5h ░░░░░░░░ 1% ➞3h42m │
|
|
440
|
+
│ 7d ▓▓▓▓▓▓▓▓ 98% │
|
|
441
|
+
│ ⚡ ▓▓▓▓▓▓░░ 69% │
|
|
442
|
+
│ 💾 6.3K tokens │
|
|
443
|
+
│ ⠛⣿⣷⣶⣿⣿⣿⣿⣷⣶⣿⣿⣿ │
|
|
444
|
+
│ 🟢 Verbose │
|
|
445
|
+
╰──────────────────────────────────────╯
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
| Row | Description |
|
|
449
|
+
|-----|-------------|
|
|
450
|
+
| `5h` | 5-hour rate limit utilization with progress bar and reset countdown |
|
|
451
|
+
| `7d` | 7-day rate limit utilization with progress bar |
|
|
452
|
+
| `⚡` | Cache hit rate — percentage of input tokens served from cache |
|
|
453
|
+
| `💾` | Cumulative tokens saved by cache hits |
|
|
454
|
+
| `⠛⣿` | Braille sparkline — per-call cache hit history (2 calls per character); drops signal cache busts |
|
|
455
|
+
| `🟢` | Connection status and current view mode |
|
|
456
|
+
|
|
457
|
+
Progress bars are color-coded: green (< 70%), yellow (70-89%), red (>= 90%) for rate limits; inverted for cache hits (green >= 70%, red < 30%). All data comes from Anthropic API response headers and usage objects, broadcast as message metadata via ActionCable.
|
|
458
|
+
|
|
459
|
+
When content exceeds the panel height, the HUD scrolls. Three input methods:
|
|
460
|
+
|
|
461
|
+
| Input | Action |
|
|
462
|
+
|-------|--------|
|
|
463
|
+
| `C-a → →` | Enter HUD focus mode (yellow border) |
|
|
464
|
+
| `↑` / `↓` | Scroll one line (when focused) |
|
|
465
|
+
| `Page Up` / `Page Down` | Scroll one page (when focused) |
|
|
466
|
+
| `Home` / `End` | Jump to top / bottom (when focused) |
|
|
467
|
+
| `Escape` or `C-a` | Exit HUD focus mode |
|
|
468
|
+
| Mouse wheel over HUD | Scroll without entering focus mode |
|
|
469
|
+
|
|
470
|
+
**Escape key interrupt:** Press `Escape` while the agent is working to stop execution mid-tool. Running shell commands receive Ctrl+C and return partial output; pending tool calls are skipped; LLM text generation is discarded. The interrupt cascades to active sub-agents.
|
|
471
|
+
|
|
399
472
|
Three switchable view modes let you control how much detail the TUI shows. Cycle with `C-a → v`:
|
|
400
473
|
|
|
401
474
|
| Mode | What you see |
|
|
@@ -582,9 +655,9 @@ This single example demonstrates every core principle:
|
|
|
582
655
|
- Dynamic viewport context assembly (endless sessions, no compaction)
|
|
583
656
|
- Analytical brain (skills, workflows, goals, session naming)
|
|
584
657
|
- Mneme memory department (eviction-triggered summarization, persistent snapshots, goal-scoped event pinning, associative recall)
|
|
585
|
-
-
|
|
658
|
+
- 12 built-in tools + MCP integration (HTTP + stdio transports)
|
|
586
659
|
- 7 built-in skills + 13 built-in workflows (user-extensible)
|
|
587
|
-
- Sub-agents with
|
|
660
|
+
- Sub-agents with isolated context (5 specialists + generic)
|
|
588
661
|
- Client-server architecture with WebSocket transport + graceful reconnection
|
|
589
662
|
- Collapsible HUD panel with goals, skills, workflow, and sub-agent tracking
|
|
590
663
|
- Three TUI view modes (Basic / Verbose / Debug)
|
data/agents/codebase-analyzer.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codebase-analyzer
|
|
3
3
|
description: Traces data flow and explains how code works. Returns file:line references.
|
|
4
|
-
tools:
|
|
4
|
+
tools: read_file, bash
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You are a specialist at understanding HOW code works. Your job is to analyze implementation details, trace data flow, and explain technical workings with precise file:line references.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codebase-pattern-finder
|
|
3
3
|
description: Finds similar implementations, usage examples, and existing patterns to model after. Returns concrete code examples.
|
|
4
|
-
tools:
|
|
4
|
+
tools: read_file, bash
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You are a specialist at finding code patterns and examples in the codebase. Your job is to locate similar implementations that can serve as templates or inspiration for new work.
|
data/agents/thoughts-analyzer.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: thoughts-analyzer
|
|
3
3
|
description: "thoughts/ holds design decisions, architecture notes, and implementation rationale. Answers WHY things work the way they do and how they should work by design."
|
|
4
|
-
tools:
|
|
4
|
+
tools: read_file, bash
|
|
5
5
|
---
|
|
6
6
|
|
|
7
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.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: web-search-researcher
|
|
3
3
|
description: Researches topics across multiple web sources. Use when a single page won't answer the question.
|
|
4
|
-
tools: web_get, bash,
|
|
4
|
+
tools: web_get, bash, read_file
|
|
5
5
|
color: yellow
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are an expert web research specialist. Use `web_get` to fetch web pages and extract information. Use `bash` for processing and `
|
|
8
|
+
You are an expert web research specialist. Use `web_get` to fetch web pages and extract information. Use `bash` for processing and `read_file` for examining local files when needed.
|
|
9
9
|
|
|
10
10
|
## Core Responsibilities
|
|
11
11
|
|
|
@@ -47,8 +47,8 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
47
47
|
# schedules {AgentRequestJob} for LLM delivery. If delivery fails, the
|
|
48
48
|
# job deletes the message and emits a {Events::BounceBack}.
|
|
49
49
|
#
|
|
50
|
-
# For busy sessions,
|
|
51
|
-
# until the current agent loop completes.
|
|
50
|
+
# For busy sessions, stages the message as a {PendingMessage} in a
|
|
51
|
+
# separate table until the current agent loop completes.
|
|
52
52
|
#
|
|
53
53
|
# @param data [Hash] must include "content" with the user's message text
|
|
54
54
|
# @see Session#enqueue_user_message
|
|
@@ -63,38 +63,42 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# Recalls the most recent pending message for editing. Deletes the
|
|
66
|
-
#
|
|
66
|
+
# {PendingMessage} — its +after_destroy_commit+ broadcasts removal
|
|
67
|
+
# so all clients remove the pending indicator.
|
|
67
68
|
#
|
|
68
|
-
# @param data [Hash] must include "
|
|
69
|
+
# @param data [Hash] must include "pending_message_id" (positive integer)
|
|
69
70
|
def recall_pending(data)
|
|
70
|
-
|
|
71
|
-
return if
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
session_id: @current_session_id,
|
|
76
|
-
message_type: "user_message",
|
|
77
|
-
status: Message::PENDING_STATUS
|
|
78
|
-
)
|
|
79
|
-
return unless message
|
|
80
|
-
|
|
81
|
-
message.destroy!
|
|
82
|
-
ActionCable.server.broadcast(stream_name, {"action" => "user_message_recalled", "message_id" => message_id})
|
|
71
|
+
pm_id = data["pending_message_id"].to_i
|
|
72
|
+
return if pm_id <= 0
|
|
73
|
+
|
|
74
|
+
pm = PendingMessage.find_by(id: pm_id, session_id: @current_session_id)
|
|
75
|
+
pm&.destroy!
|
|
83
76
|
end
|
|
84
77
|
|
|
85
78
|
# Requests interruption of the current tool execution. Sets a flag on the
|
|
86
79
|
# session that the LLM client checks between tool calls. Remaining tools
|
|
87
|
-
# receive synthetic "
|
|
80
|
+
# receive synthetic "Your human wants your attention" results to satisfy the API's
|
|
88
81
|
# tool_use/tool_result pairing requirement.
|
|
89
82
|
#
|
|
83
|
+
# Cascades to running sub-agent sessions to avoid burning tokens in
|
|
84
|
+
# child jobs that the parent will discard anyway.
|
|
85
|
+
#
|
|
90
86
|
# Atomic: a single UPDATE with WHERE avoids the read-then-write race where
|
|
91
87
|
# the session could finish processing between the SELECT and UPDATE.
|
|
92
88
|
# No-op if the session isn't currently processing.
|
|
93
89
|
#
|
|
94
90
|
# @param _data [Hash] unused
|
|
95
91
|
def interrupt_execution(_data)
|
|
96
|
-
Session.where(id: @current_session_id, processing: true)
|
|
92
|
+
updated = Session.where(id: @current_session_id, processing: true)
|
|
97
93
|
.update_all(interrupt_requested: true)
|
|
94
|
+
|
|
95
|
+
return unless updated > 0
|
|
96
|
+
|
|
97
|
+
Session.processing_children_of(@current_session_id)
|
|
98
|
+
.update_all(interrupt_requested: true)
|
|
99
|
+
|
|
100
|
+
Session.find_by(id: @current_session_id)&.broadcast_session_state("interrupting")
|
|
101
|
+
ActionCable.server.broadcast(stream_name, {"action" => "interrupt_acknowledged"})
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
# Returns recent root sessions with nested child metadata for session picker UI.
|
|
@@ -132,9 +136,9 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
132
136
|
transmit_error("Session not found")
|
|
133
137
|
end
|
|
134
138
|
|
|
135
|
-
# Validates and saves an Anthropic subscription token to encrypted
|
|
139
|
+
# Validates and saves an Anthropic subscription token to encrypted storage.
|
|
136
140
|
# Format-validated and API-validated before storage. The token never enters the
|
|
137
|
-
# LLM context window — it flows directly from WebSocket to
|
|
141
|
+
# LLM context window — it flows directly from WebSocket to the secrets table.
|
|
138
142
|
#
|
|
139
143
|
# @param data [Hash] must include "token" (Anthropic subscription token string)
|
|
140
144
|
def save_token(data)
|
|
@@ -217,7 +221,10 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
217
221
|
|
|
218
222
|
children = session.child_sessions.order(:created_at).select(:id, :name, :processing)
|
|
219
223
|
if children.any?
|
|
220
|
-
payload["children"] = children.map { |child|
|
|
224
|
+
payload["children"] = children.map { |child|
|
|
225
|
+
state = child.processing? ? "llm_generating" : "idle"
|
|
226
|
+
{"id" => child.id, "name" => child.name, "processing" => child.processing?, "session_state" => state}
|
|
227
|
+
}
|
|
221
228
|
end
|
|
222
229
|
|
|
223
230
|
transmit(payload)
|
|
@@ -251,6 +258,7 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
251
258
|
# the transmitted payload. Tool messages are included so the TUI can
|
|
252
259
|
# reconstruct tool call counters on reconnect.
|
|
253
260
|
# In debug mode, prepends the assembled system prompt as a special block.
|
|
261
|
+
# Pending messages are sent last so the TUI shows them at the bottom.
|
|
254
262
|
#
|
|
255
263
|
# Snapshots the viewport so subsequent message broadcasts can compute
|
|
256
264
|
# eviction diffs accurately.
|
|
@@ -262,11 +270,16 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
262
270
|
each_viewport_message(session) do |_msg, msg_payload|
|
|
263
271
|
transmit(msg_payload)
|
|
264
272
|
end
|
|
273
|
+
|
|
274
|
+
session.pending_messages.find_each do |pm|
|
|
275
|
+
transmit({"action" => "pending_message_created", "pending_message_id" => pm.id, "content" => pm.content})
|
|
276
|
+
end
|
|
265
277
|
end
|
|
266
278
|
|
|
267
279
|
# Broadcasts the re-decorated viewport to all clients on the session stream.
|
|
268
280
|
# Used after a view mode change to refresh all connected clients.
|
|
269
281
|
# In debug mode, prepends the assembled system prompt as a special block.
|
|
282
|
+
# Pending messages are sent last so the TUI shows them at the bottom.
|
|
270
283
|
#
|
|
271
284
|
# Snapshots the viewport so subsequent message broadcasts can compute
|
|
272
285
|
# eviction diffs accurately.
|
|
@@ -279,12 +292,21 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
279
292
|
each_viewport_message(session) do |_msg, msg_payload|
|
|
280
293
|
ActionCable.server.broadcast(stream_name, msg_payload)
|
|
281
294
|
end
|
|
295
|
+
|
|
296
|
+
session.pending_messages.find_each do |pm|
|
|
297
|
+
ActionCable.server.broadcast(stream_name, {"action" => "pending_message_created", "pending_message_id" => pm.id, "content" => pm.content})
|
|
298
|
+
end
|
|
282
299
|
end
|
|
283
300
|
|
|
284
301
|
# Loads the viewport, snapshots it for eviction tracking, and yields
|
|
285
|
-
# each message with its decorated payload
|
|
286
|
-
#
|
|
287
|
-
#
|
|
302
|
+
# each message with its decorated payload in newest-first order.
|
|
303
|
+
# Newest-first prevents render thrashing during session switches: the
|
|
304
|
+
# most recent messages fill the visible viewport immediately, while
|
|
305
|
+
# older messages are inserted above the fold without visual disruption.
|
|
306
|
+
#
|
|
307
|
+
# Snapshot uses snapshot_viewport! (not recalculate_viewport!) because
|
|
308
|
+
# full viewport refreshes don't need eviction diffs — clients clear
|
|
309
|
+
# their store before rendering.
|
|
288
310
|
#
|
|
289
311
|
# @param session [Session] the session whose viewport to iterate
|
|
290
312
|
# @yieldparam message [Message] the persisted message record
|
|
@@ -294,7 +316,7 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
294
316
|
viewport = session.viewport_messages
|
|
295
317
|
session.snapshot_viewport!(viewport.map(&:id))
|
|
296
318
|
|
|
297
|
-
viewport.
|
|
319
|
+
viewport.reverse_each do |msg|
|
|
298
320
|
yield msg, decorate_message_payload(msg, session.view_mode)
|
|
299
321
|
end
|
|
300
322
|
end
|
|
@@ -338,23 +360,19 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
338
360
|
end
|
|
339
361
|
|
|
340
362
|
# Builds the system prompt payload for debug mode transmission.
|
|
363
|
+
# Delegates to {Session.system_prompt_payload} for the shared format.
|
|
364
|
+
# Includes deterministic tool schemas (standard + spawn tools).
|
|
365
|
+
# MCP tools appear after the first LLM request via live broadcast.
|
|
341
366
|
# @param session [Session]
|
|
342
367
|
# @return [Hash, nil] the system prompt payload, or nil if no prompt
|
|
343
368
|
def system_prompt_payload(session)
|
|
344
369
|
prompt = session.system_prompt
|
|
345
370
|
return unless prompt
|
|
346
371
|
|
|
347
|
-
|
|
348
|
-
{
|
|
349
|
-
"type" => "system_prompt",
|
|
350
|
-
"rendered" => {
|
|
351
|
-
"debug" => {role: :system_prompt, content: prompt, tokens: tokens, estimated: true}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
372
|
+
Session.system_prompt_payload(prompt, tools: session.tool_schemas)
|
|
354
373
|
end
|
|
355
374
|
|
|
356
|
-
#
|
|
357
|
-
# preserving existing keys (e.g. secret_key_base).
|
|
375
|
+
# Writes the Anthropic subscription token to encrypted storage.
|
|
358
376
|
#
|
|
359
377
|
# @param token [String] validated Anthropic subscription token
|
|
360
378
|
# @return [void]
|
|
@@ -7,7 +7,7 @@ require "toon"
|
|
|
7
7
|
# aggregated tool counter instead. Verbose mode returns tool name
|
|
8
8
|
# and a formatted preview of the input arguments. Debug mode shows
|
|
9
9
|
# full untruncated input with tool_use_id — TOON format for most
|
|
10
|
-
# tools, but
|
|
10
|
+
# tools, but write_file tool content preserves actual newlines.
|
|
11
11
|
#
|
|
12
12
|
# Think tool calls are special: "aloud" thoughts are shown in all
|
|
13
13
|
# view modes (with a thought bubble), while "inner" thoughts are
|
|
@@ -97,18 +97,18 @@ class ToolCallDecorator < MessageDecorator
|
|
|
97
97
|
def format_debug_input
|
|
98
98
|
input = tool_input
|
|
99
99
|
case payload["tool_name"]
|
|
100
|
-
when "
|
|
100
|
+
when "write_file" then format_write_content(input)
|
|
101
101
|
else Toon.encode(input)
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
# Formats write tool input with file path header and content body.
|
|
106
106
|
# Content newlines are preserved so the TUI can render them as
|
|
107
|
-
# separate lines, matching how
|
|
108
|
-
# @param input [Hash] tool input hash with "
|
|
107
|
+
# separate lines, matching how read_file tool responses display file content.
|
|
108
|
+
# @param input [Hash] tool input hash with "path" and "content" keys
|
|
109
109
|
# @return [String] path + content with real newlines, or TOON-encoded hash when content is empty
|
|
110
110
|
def format_write_content(input)
|
|
111
|
-
path = input.dig("
|
|
111
|
+
path = input.dig("path").to_s
|
|
112
112
|
content = input.dig("content").to_s
|
|
113
113
|
return Toon.encode(input) if content.empty?
|
|
114
114
|
|
|
@@ -125,8 +125,8 @@ class ToolCallDecorator < MessageDecorator
|
|
|
125
125
|
"$ #{input&.dig("command")}"
|
|
126
126
|
when "web_get"
|
|
127
127
|
"GET #{input&.dig("url")}"
|
|
128
|
-
when "
|
|
129
|
-
input&.dig("
|
|
128
|
+
when "read_file", "edit_file", "write_file"
|
|
129
|
+
input&.dig("path").to_s
|
|
130
130
|
else
|
|
131
131
|
truncate_lines(Toon.encode(input), max_lines: 2)
|
|
132
132
|
end
|