anima-core 1.1.3 → 1.3.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 +10 -1
- data/README.md +36 -11
- data/agents/codebase-analyzer.md +2 -2
- data/agents/codebase-pattern-finder.md +2 -2
- data/agents/documentation-researcher.md +2 -2
- data/agents/thoughts-analyzer.md +2 -2
- data/agents/web-search-researcher.md +3 -3
- data/app/channels/session_channel.rb +83 -64
- data/app/decorators/agent_message_decorator.rb +2 -2
- data/app/decorators/{event_decorator.rb → message_decorator.rb} +40 -40
- data/app/decorators/system_message_decorator.rb +2 -2
- data/app/decorators/tool_call_decorator.rb +6 -6
- data/app/decorators/tool_decorator.rb +4 -4
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +5 -19
- data/app/decorators/web_get_tool_decorator.rb +41 -9
- data/app/jobs/agent_request_job.rb +33 -24
- data/app/jobs/count_message_tokens_job.rb +39 -0
- data/app/jobs/passive_recall_job.rb +4 -4
- data/app/models/concerns/{event → message}/broadcasting.rb +16 -16
- data/app/models/goal.rb +17 -4
- data/app/models/goal_pinned_message.rb +11 -0
- data/app/models/message.rb +127 -0
- data/app/models/pending_message.rb +43 -0
- data/app/models/pinned_message.rb +41 -0
- data/app/models/secret.rb +72 -0
- data/app/models/session.rb +385 -226
- data/app/models/snapshot.rb +25 -25
- data/config/environments/test.rb +5 -0
- data/config/initializers/time_nanoseconds.rb +11 -0
- data/db/migrate/20260326180000_rename_event_to_message.rb +172 -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/lib/agent_loop.rb +14 -41
- data/lib/agents/definition.rb +1 -1
- data/lib/analytical_brain/runner.rb +40 -37
- data/lib/analytical_brain/tools/activate_skill.rb +5 -9
- data/lib/analytical_brain/tools/assign_nickname.rb +2 -4
- data/lib/analytical_brain/tools/deactivate_skill.rb +5 -9
- data/lib/analytical_brain/tools/everything_is_ready.rb +1 -2
- data/lib/analytical_brain/tools/finish_goal.rb +5 -8
- data/lib/analytical_brain/tools/read_workflow.rb +5 -9
- data/lib/analytical_brain/tools/rename_session.rb +3 -10
- data/lib/analytical_brain/tools/set_goal.rb +3 -7
- data/lib/analytical_brain/tools/update_goal.rb +3 -7
- data/lib/anima/cli/mcp/secrets.rb +4 -4
- data/lib/anima/cli/mcp.rb +4 -4
- data/lib/anima/installer.rb +7 -1
- data/lib/anima/settings.rb +46 -6
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +1 -1
- data/lib/credential_store.rb +17 -66
- data/lib/events/base.rb +1 -1
- data/lib/events/bounce_back.rb +7 -7
- data/lib/events/subscribers/persister.rb +15 -22
- data/lib/events/subscribers/subagent_message_router.rb +20 -8
- data/lib/events/subscribers/transient_broadcaster.rb +2 -2
- data/lib/events/user_message.rb +2 -13
- data/lib/llm/client.rb +54 -20
- data/lib/mcp/config.rb +2 -2
- data/lib/mcp/secrets.rb +7 -8
- data/lib/mneme/compressed_viewport.rb +57 -57
- data/lib/mneme/l2_runner.rb +4 -4
- data/lib/mneme/passive_recall.rb +2 -2
- data/lib/mneme/runner.rb +57 -75
- data/lib/mneme/search.rb +38 -38
- data/lib/mneme/tools/attach_messages_to_goals.rb +103 -0
- data/lib/mneme/tools/everything_ok.rb +1 -3
- data/lib/mneme/tools/save_snapshot.rb +12 -16
- data/lib/shell_session.rb +54 -16
- data/lib/tools/base.rb +23 -0
- data/lib/tools/bash.rb +60 -16
- data/lib/tools/edit.rb +6 -8
- data/lib/tools/mark_goal_completed.rb +86 -0
- data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
- data/lib/tools/read.rb +6 -5
- data/lib/tools/recall.rb +98 -0
- data/lib/tools/registry.rb +37 -8
- data/lib/tools/remember.rb +46 -55
- data/lib/tools/response_truncator.rb +70 -0
- data/lib/tools/spawn_specialist.rb +15 -25
- data/lib/tools/spawn_subagent.rb +14 -22
- data/lib/tools/subagent_prompts.rb +42 -6
- data/lib/tools/think.rb +26 -10
- data/lib/tools/web_get.rb +23 -4
- data/lib/tools/write.rb +4 -4
- data/lib/tui/app.rb +178 -13
- data/lib/tui/braille_spinner.rb +152 -0
- data/lib/tui/cable_client.rb +4 -4
- data/lib/tui/decorators/base_decorator.rb +17 -8
- data/lib/tui/decorators/bash_decorator.rb +2 -2
- data/lib/tui/decorators/edit_decorator.rb +5 -4
- data/lib/tui/decorators/read_decorator.rb +4 -8
- data/lib/tui/decorators/think_decorator.rb +3 -5
- data/lib/tui/decorators/web_get_decorator.rb +4 -3
- data/lib/tui/decorators/write_decorator.rb +5 -4
- data/lib/tui/flash.rb +1 -1
- data/lib/tui/formatting.rb +22 -0
- data/lib/tui/message_store.rb +103 -59
- data/lib/tui/screens/chat.rb +293 -78
- 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 +42 -5
- data/templates/soul.md +7 -19
- data/workflows/create_handoff.md +1 -1
- data/workflows/create_note.md +1 -1
- data/workflows/create_plan.md +1 -1
- data/workflows/implement_plan.md +1 -1
- data/workflows/iterate_plan.md +1 -1
- data/workflows/research_codebase.md +1 -1
- data/workflows/resume_handoff.md +1 -1
- data/workflows/review_pr.md +78 -16
- data/workflows/thoughts_init.md +1 -1
- data/workflows/validate_plan.md +1 -1
- metadata +20 -9
- data/app/jobs/count_event_tokens_job.rb +0 -39
- data/app/models/event.rb +0 -129
- data/app/models/goal_pinned_event.rb +0 -11
- data/app/models/pinned_event.rb +0 -41
- data/lib/mneme/tools/attach_events_to_goals.rb +0 -107
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ec0963d3bb57afc7a12414258c90d645dd200ca489bd7ab34e6ace19ea11927
|
|
4
|
+
data.tar.gz: 712d1904e5ddf7b0c244ddeffb2ed993f6265d097eb18000b139cdb08a9bfc4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b70800258af5296bf243e7b0c020efd6f7736e3f1f58734352e022ca8ec7a15599ffce938fc1adc511bc37a7141472845dcead21b6512cdc09dfceae8b04b87
|
|
7
|
+
data.tar.gz: e70536705ac4d1cc468a951079799edd8677cae3c167251c60cccfbb51f79b0e2e7f896b5420f4972c8a59ee3d377ae0fd284d3603db441bd45313a1030f60ca
|
data/.reek.yml
CHANGED
|
@@ -35,8 +35,13 @@ detectors:
|
|
|
35
35
|
- "Tools::SpawnSubagent#spawn_child"
|
|
36
36
|
- "Tools::SpawnSpecialist#spawn_child"
|
|
37
37
|
- "Tools::SpawnSpecialist#execute"
|
|
38
|
-
#
|
|
38
|
+
# Spawn helpers operate on child session — inherent to the mixin pattern.
|
|
39
39
|
- "Tools::SubagentPrompts#assign_nickname_via_brain"
|
|
40
|
+
- "Tools::SubagentPrompts#inject_identity_context"
|
|
41
|
+
# Registry dispatches to tool's threshold method — duck-typing delegation.
|
|
42
|
+
- "Tools::Registry#truncation_threshold"
|
|
43
|
+
# Goal tools operate on goal objects — inherent to the pattern.
|
|
44
|
+
- "AnalyticalBrain::Tools::UpdateGoal#execute"
|
|
40
45
|
# Validation methods naturally reference the validated value more than self.
|
|
41
46
|
- "AnalyticalBrain::Tools::AssignNickname#validate"
|
|
42
47
|
# Tool execute methods naturally reference input hash and shell result hash.
|
|
@@ -90,6 +95,10 @@ detectors:
|
|
|
90
95
|
UncommunicativeMethodName:
|
|
91
96
|
exclude:
|
|
92
97
|
- "ToolDecorator#self.encode_utf8"
|
|
98
|
+
# Registry uses respond_to? for duck-typing dispatch with MCP tool instances.
|
|
99
|
+
ManualDispatch:
|
|
100
|
+
exclude:
|
|
101
|
+
- "Tools::Registry#truncation_threshold"
|
|
93
102
|
# Abstract base class methods declare parameters for the subclass contract.
|
|
94
103
|
UnusedParameters:
|
|
95
104
|
exclude:
|
data/README.md
CHANGED
|
@@ -132,7 +132,7 @@ State directory (`~/.anima/`):
|
|
|
132
132
|
├── config.toml # Main settings (hot-reloadable)
|
|
133
133
|
├── mcp.toml # MCP server configuration
|
|
134
134
|
├── config/
|
|
135
|
-
│ └── credentials/
|
|
135
|
+
│ └── credentials/ # Rails encrypted credentials (includes AR encryption keys)
|
|
136
136
|
├── agents/ # User-defined specialist agents (override built-ins)
|
|
137
137
|
├── skills/ # User-defined skills (override built-ins)
|
|
138
138
|
├── workflows/ # User-defined workflows (override built-ins)
|
|
@@ -149,7 +149,7 @@ Anima uses your Claude Pro/Max subscription for API access. You need a setup-tok
|
|
|
149
149
|
|
|
150
150
|
1. Run `claude setup-token` in a terminal to get your token
|
|
151
151
|
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
|
|
152
|
+
3. Paste the token and press Enter — Anima validates it against the Anthropic API and saves it to the encrypted secrets database
|
|
153
153
|
|
|
154
154
|
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
155
|
|
|
@@ -162,12 +162,17 @@ The agent has access to these built-in tools:
|
|
|
162
162
|
| Tool | Description |
|
|
163
163
|
|------|-------------|
|
|
164
164
|
| `bash` | Execute shell commands with persistent working directory |
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
165
|
+
| `read_file` | Read files with smart truncation and offset/limit paging |
|
|
166
|
+
| `write_file` | Create or overwrite files |
|
|
167
|
+
| `edit_file` | Surgical text replacement with uniqueness constraint |
|
|
168
168
|
| `web_get` | Fetch content from HTTP/HTTPS URLs (HTML → Markdown, JSON → TOON) |
|
|
169
169
|
| `spawn_specialist` | Spawn a named specialist sub-agent from the registry |
|
|
170
170
|
| `spawn_subagent` | Spawn a generic child session with custom tool grants |
|
|
171
|
+
| `think` | Think out loud or silently — reasoning step between tool calls |
|
|
172
|
+
| `recall` | Search past conversations by keywords (FTS5). Returns ranked snippets with message IDs for drill-down |
|
|
173
|
+
| `remember` | Recall full conversation context around a past message at fractal resolution |
|
|
174
|
+
| `open_issue` | File a self-improvement issue when something is broken, missing, or could be better |
|
|
175
|
+
| `mark_goal_completed` | Sub-agent only: signal task completion and deliver results to parent |
|
|
171
176
|
|
|
172
177
|
Plus dynamic tools from configured MCP servers, namespaced as `server_name__tool_name`.
|
|
173
178
|
|
|
@@ -189,7 +194,9 @@ Two types:
|
|
|
189
194
|
|
|
190
195
|
**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
196
|
|
|
192
|
-
|
|
197
|
+
Each sub-agent is spawned with a single **Goal** pinned from its task description and a framing message that redirects attention away from inherited parent goals. 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.
|
|
198
|
+
|
|
199
|
+
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
200
|
|
|
194
201
|
### Skills
|
|
195
202
|
|
|
@@ -255,12 +262,12 @@ anima mcp add fs -- mcp-server-filesystem --root / # Add stdio server
|
|
|
255
262
|
anima mcp add -s api_key=sk-xxx linear https://... # Add with secret
|
|
256
263
|
anima mcp remove sentry # Remove server
|
|
257
264
|
|
|
258
|
-
anima mcp secrets set linear_api_key=sk-xxx # Store secret in encrypted
|
|
265
|
+
anima mcp secrets set linear_api_key=sk-xxx # Store secret in encrypted database
|
|
259
266
|
anima mcp secrets list # List secret names (not values)
|
|
260
267
|
anima mcp secrets remove linear_api_key # Remove secret
|
|
261
268
|
```
|
|
262
269
|
|
|
263
|
-
Secrets are stored in
|
|
270
|
+
Secrets are stored in an encrypted database table (Active Record Encryption) and interpolated via `${credential:key_name}` syntax in any TOML string value.
|
|
264
271
|
|
|
265
272
|
### Analytical Brain
|
|
266
273
|
|
|
@@ -270,7 +277,7 @@ The analytical brain observes the main conversation between turns and handles ev
|
|
|
270
277
|
|
|
271
278
|
- **Skill activation** — activates/deactivates domain knowledge based on conversation context
|
|
272
279
|
- **Workflow management** — recognizes tasks, activates matching workflows, tracks lifecycle
|
|
273
|
-
- **Goal tracking** — creates root goals and sub-goals as work progresses, marks them complete
|
|
280
|
+
- **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
281
|
- **Session naming** — generates emoji + short name when the topic becomes clear
|
|
275
282
|
|
|
276
283
|
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.
|
|
@@ -293,10 +300,13 @@ token_budget = 190_000
|
|
|
293
300
|
api = 300
|
|
294
301
|
command = 30
|
|
295
302
|
|
|
303
|
+
[goals]
|
|
304
|
+
completed_decay_messages = 5
|
|
305
|
+
|
|
296
306
|
[analytical_brain]
|
|
297
307
|
max_tokens = 4096
|
|
298
308
|
blocking_on_user_message = true
|
|
299
|
-
|
|
309
|
+
message_window = 20
|
|
300
310
|
|
|
301
311
|
[session]
|
|
302
312
|
default_view_mode = "basic"
|
|
@@ -396,6 +406,21 @@ The difference from every other system: memory isn't a tool the agent uses. It's
|
|
|
396
406
|
|
|
397
407
|
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
408
|
|
|
409
|
+
**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).
|
|
410
|
+
|
|
411
|
+
When content exceeds the panel height, the HUD scrolls. Three input methods:
|
|
412
|
+
|
|
413
|
+
| Input | Action |
|
|
414
|
+
|-------|--------|
|
|
415
|
+
| `C-a → →` | Enter HUD focus mode (yellow border) |
|
|
416
|
+
| `↑` / `↓` | Scroll one line (when focused) |
|
|
417
|
+
| `Page Up` / `Page Down` | Scroll one page (when focused) |
|
|
418
|
+
| `Home` / `End` | Jump to top / bottom (when focused) |
|
|
419
|
+
| `Escape` or `C-a` | Exit HUD focus mode |
|
|
420
|
+
| Mouse wheel over HUD | Scroll without entering focus mode |
|
|
421
|
+
|
|
422
|
+
**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.
|
|
423
|
+
|
|
399
424
|
Three switchable view modes let you control how much detail the TUI shows. Cycle with `C-a → v`:
|
|
400
425
|
|
|
401
426
|
| Mode | What you see |
|
|
@@ -582,7 +607,7 @@ This single example demonstrates every core principle:
|
|
|
582
607
|
- Dynamic viewport context assembly (endless sessions, no compaction)
|
|
583
608
|
- Analytical brain (skills, workflows, goals, session naming)
|
|
584
609
|
- Mneme memory department (eviction-triggered summarization, persistent snapshots, goal-scoped event pinning, associative recall)
|
|
585
|
-
-
|
|
610
|
+
- 12 built-in tools + MCP integration (HTTP + stdio transports)
|
|
586
611
|
- 7 built-in skills + 13 built-in workflows (user-extensible)
|
|
587
612
|
- Sub-agents with lossless context inheritance (5 specialists + generic)
|
|
588
613
|
- Client-server architecture with WebSocket transport + graceful reconnection
|
data/agents/codebase-analyzer.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codebase-analyzer
|
|
3
|
-
description:
|
|
4
|
-
tools:
|
|
3
|
+
description: Traces data flow and explains how code works. Returns file:line references.
|
|
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
|
-
description: Finds similar implementations, usage examples, and existing patterns
|
|
4
|
-
tools:
|
|
3
|
+
description: Finds similar implementations, usage examples, and existing patterns to model after. Returns concrete code examples.
|
|
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.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: documentation-researcher
|
|
3
|
-
description: Fetches
|
|
4
|
-
tools: web_get,
|
|
3
|
+
description: Fetches official docs. Returns ready-to-use code examples.
|
|
4
|
+
tools: web_get, read_file
|
|
5
5
|
color: cyan
|
|
6
6
|
---
|
|
7
7
|
|
data/agents/thoughts-analyzer.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: thoughts-analyzer
|
|
3
|
-
description:
|
|
4
|
-
tools:
|
|
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: 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
|
-
description:
|
|
4
|
-
tools: web_get, bash,
|
|
3
|
+
description: Researches topics across multiple web sources. Use when a single page won't answer the question.
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Streams
|
|
4
|
-
# Part of the Brain/TUI separation: the Brain broadcasts
|
|
3
|
+
# Streams messages for a specific session to connected clients.
|
|
4
|
+
# Part of the Brain/TUI separation: the Brain broadcasts messages through
|
|
5
5
|
# this channel, and any number of clients (TUI, web, API) can subscribe.
|
|
6
6
|
#
|
|
7
7
|
# On subscription, sends the session's chat history so the client can
|
|
@@ -42,13 +42,13 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
42
42
|
ActionCable.server.broadcast(stream_name, data)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# Processes user input. For idle sessions, persists the
|
|
46
|
-
# so
|
|
47
|
-
#
|
|
48
|
-
#
|
|
45
|
+
# Processes user input. For idle sessions, persists the message immediately
|
|
46
|
+
# so it appears in the TUI without waiting for the background job, then
|
|
47
|
+
# schedules {AgentRequestJob} for LLM delivery. If delivery fails, the
|
|
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
|
-
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})
|
|
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.
|
|
@@ -106,7 +110,7 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
106
110
|
limit = (data["limit"] || DEFAULT_LIST_LIMIT).to_i.clamp(1, MAX_LIST_LIMIT)
|
|
107
111
|
sessions = Session.root_sessions.recent(limit).includes(:child_sessions)
|
|
108
112
|
all_ids = sessions.flat_map { |session| [session.id] + session.child_sessions.map(&:id) }
|
|
109
|
-
counts =
|
|
113
|
+
counts = Message.where(session_id: all_ids).llm_messages.group(:session_id).count
|
|
110
114
|
|
|
111
115
|
result = sessions.map { |session| serialize_session_with_children(session, counts) }
|
|
112
116
|
transmit({"action" => "sessions_list", "sessions" => result})
|
|
@@ -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)
|
|
@@ -196,7 +200,7 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
196
200
|
# Used on initial subscription and after session switches so the
|
|
197
201
|
# client can handle both paths with a single code path.
|
|
198
202
|
#
|
|
199
|
-
# Payload: session_id, name, parent_session_id, message_count,
|
|
203
|
+
# Payload: session_id, name, agent_name, parent_session_id, message_count,
|
|
200
204
|
# view_mode, active_skills, goals, children (when present).
|
|
201
205
|
#
|
|
202
206
|
# @param session [Session] the session to announce
|
|
@@ -206,8 +210,9 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
206
210
|
"action" => "session_changed",
|
|
207
211
|
"session_id" => session.id,
|
|
208
212
|
"name" => session.name,
|
|
213
|
+
"agent_name" => Anima::Settings.agent_name,
|
|
209
214
|
"parent_session_id" => session.parent_session_id,
|
|
210
|
-
"message_count" => session.
|
|
215
|
+
"message_count" => session.messages.llm_messages.count,
|
|
211
216
|
"view_mode" => session.view_mode,
|
|
212
217
|
"active_skills" => session.active_skills,
|
|
213
218
|
"active_workflow" => session.active_workflow,
|
|
@@ -216,7 +221,10 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
216
221
|
|
|
217
222
|
children = session.child_sessions.order(:created_at).select(:id, :name, :processing)
|
|
218
223
|
if children.any?
|
|
219
|
-
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
|
+
}
|
|
220
228
|
end
|
|
221
229
|
|
|
222
230
|
transmit(payload)
|
|
@@ -244,30 +252,36 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
244
252
|
transmit({"action" => "view_mode", "view_mode" => session.view_mode})
|
|
245
253
|
end
|
|
246
254
|
|
|
247
|
-
# Sends decorated context
|
|
248
|
-
# the LLM's viewport to the subscribing client. Each
|
|
249
|
-
# in
|
|
250
|
-
# the transmitted payload. Tool
|
|
255
|
+
# Sends decorated context messages (conversation + tool interactions) from
|
|
256
|
+
# the LLM's viewport to the subscribing client. Each message is wrapped
|
|
257
|
+
# in a {MessageDecorator} and the pre-rendered output is included in
|
|
258
|
+
# the transmitted payload. Tool messages are included so the TUI can
|
|
251
259
|
# reconstruct tool call counters on reconnect.
|
|
252
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.
|
|
253
262
|
#
|
|
254
|
-
# Snapshots the viewport so subsequent
|
|
263
|
+
# Snapshots the viewport so subsequent message broadcasts can compute
|
|
255
264
|
# eviction diffs accurately.
|
|
256
265
|
#
|
|
257
266
|
# @param session [Session] the session whose history to transmit
|
|
258
267
|
def transmit_history(session)
|
|
259
268
|
transmit_system_prompt(session) if session.view_mode == "debug"
|
|
260
269
|
|
|
261
|
-
|
|
262
|
-
transmit(
|
|
270
|
+
each_viewport_message(session) do |_msg, msg_payload|
|
|
271
|
+
transmit(msg_payload)
|
|
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})
|
|
263
276
|
end
|
|
264
277
|
end
|
|
265
278
|
|
|
266
279
|
# Broadcasts the re-decorated viewport to all clients on the session stream.
|
|
267
280
|
# Used after a view mode change to refresh all connected clients.
|
|
268
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.
|
|
269
283
|
#
|
|
270
|
-
# Snapshots the viewport so subsequent
|
|
284
|
+
# Snapshots the viewport so subsequent message broadcasts can compute
|
|
271
285
|
# eviction diffs accurately.
|
|
272
286
|
#
|
|
273
287
|
# @param session [Session] the session whose viewport to broadcast
|
|
@@ -275,40 +289,49 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
275
289
|
def broadcast_viewport(session)
|
|
276
290
|
broadcast_system_prompt(session) if session.view_mode == "debug"
|
|
277
291
|
|
|
278
|
-
|
|
279
|
-
ActionCable.server.broadcast(stream_name,
|
|
292
|
+
each_viewport_message(session) do |_msg, msg_payload|
|
|
293
|
+
ActionCable.server.broadcast(stream_name, msg_payload)
|
|
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})
|
|
280
298
|
end
|
|
281
299
|
end
|
|
282
300
|
|
|
283
301
|
# Loads the viewport, snapshots it for eviction tracking, and yields
|
|
284
|
-
# each
|
|
285
|
-
#
|
|
286
|
-
#
|
|
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.
|
|
287
310
|
#
|
|
288
311
|
# @param session [Session] the session whose viewport to iterate
|
|
289
|
-
# @yieldparam
|
|
312
|
+
# @yieldparam message [Message] the persisted message record
|
|
290
313
|
# @yieldparam payload [Hash] decorated payload ready for transmission
|
|
291
314
|
# @return [void]
|
|
292
|
-
def
|
|
293
|
-
viewport = session.
|
|
315
|
+
def each_viewport_message(session)
|
|
316
|
+
viewport = session.viewport_messages
|
|
294
317
|
session.snapshot_viewport!(viewport.map(&:id))
|
|
295
318
|
|
|
296
|
-
viewport.
|
|
297
|
-
yield
|
|
319
|
+
viewport.reverse_each do |msg|
|
|
320
|
+
yield msg, decorate_message_payload(msg, session.view_mode)
|
|
298
321
|
end
|
|
299
322
|
end
|
|
300
323
|
|
|
301
|
-
# Decorates
|
|
324
|
+
# Decorates a message for transmission to clients. Merges the message's
|
|
302
325
|
# database ID and structured decorator output into the payload.
|
|
303
326
|
# Used by {#transmit_history} and {#broadcast_viewport} for historical
|
|
304
|
-
# and viewport re-broadcast — live broadcasts use {
|
|
327
|
+
# and viewport re-broadcast — live broadcasts use {Message::Broadcasting}.
|
|
305
328
|
#
|
|
306
|
-
# @param
|
|
329
|
+
# @param message [Message] persisted message record
|
|
307
330
|
# @param mode [String] view mode for decoration (default: "basic")
|
|
308
331
|
# @return [Hash] payload with "id" and optional "rendered" key
|
|
309
|
-
def
|
|
310
|
-
payload =
|
|
311
|
-
decorator =
|
|
332
|
+
def decorate_message_payload(message, mode = "basic")
|
|
333
|
+
payload = message.payload.merge("id" => message.id)
|
|
334
|
+
decorator = MessageDecorator.for(message)
|
|
312
335
|
return payload unless decorator
|
|
313
336
|
|
|
314
337
|
payload.merge("rendered" => {mode => decorator.render(mode)})
|
|
@@ -337,23 +360,19 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
337
360
|
end
|
|
338
361
|
|
|
339
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.
|
|
340
366
|
# @param session [Session]
|
|
341
367
|
# @return [Hash, nil] the system prompt payload, or nil if no prompt
|
|
342
368
|
def system_prompt_payload(session)
|
|
343
369
|
prompt = session.system_prompt
|
|
344
370
|
return unless prompt
|
|
345
371
|
|
|
346
|
-
|
|
347
|
-
{
|
|
348
|
-
"type" => "system_prompt",
|
|
349
|
-
"rendered" => {
|
|
350
|
-
"debug" => {role: :system_prompt, content: prompt, tokens: tokens, estimated: true}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
372
|
+
Session.system_prompt_payload(prompt, tools: session.tool_schemas)
|
|
353
373
|
end
|
|
354
374
|
|
|
355
|
-
#
|
|
356
|
-
# preserving existing keys (e.g. secret_key_base).
|
|
375
|
+
# Writes the Anthropic subscription token to encrypted storage.
|
|
357
376
|
#
|
|
358
377
|
# @param token [String] validated Anthropic subscription token
|
|
359
378
|
# @return [void]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Decorates agent_message
|
|
3
|
+
# Decorates agent_message records 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
|
-
class AgentMessageDecorator <
|
|
6
|
+
class AgentMessageDecorator < MessageDecorator
|
|
7
7
|
# @return [Hash] structured agent message data
|
|
8
8
|
# `{role: :assistant, content: String}`
|
|
9
9
|
def render_basic
|