anima-core 1.3.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 +6 -7
- data/README.md +64 -16
- data/app/decorators/tool_call_decorator.rb +3 -3
- data/app/jobs/agent_request_job.rb +2 -2
- data/app/jobs/passive_recall_job.rb +6 -11
- data/app/models/concerns/message/broadcasting.rb +1 -0
- data/app/models/goal.rb +2 -1
- data/app/models/message.rb +0 -13
- data/app/models/pending_message.rb +150 -2
- data/app/models/session.rb +324 -266
- data/bin/inspect-cassette +144 -0
- data/bin/release +212 -0
- data/bin/with-llms +20 -0
- data/config/database.yml +1 -0
- data/db/cable_structure.sql +9 -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 +42 -13
- data/lib/analytical_brain/runner.rb +12 -2
- 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.rb +41 -13
- data/lib/anima/installer.rb +13 -0
- data/lib/anima/settings.rb +13 -7
- data/lib/anima/version.rb +1 -1
- data/lib/events/agent_message.rb +14 -0
- data/lib/events/subscribers/persister.rb +2 -1
- data/lib/events/subscribers/subagent_message_router.rb +4 -7
- data/lib/llm/client.rb +37 -30
- data/lib/mneme/compressed_viewport.rb +8 -4
- 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 +185 -2
- data/lib/tools/base.rb +0 -1
- data/lib/tools/bash.rb +16 -14
- data/lib/tools/mark_goal_completed.rb +4 -5
- data/lib/tools/registry.rb +6 -1
- data/lib/tools/response_truncator.rb +1 -1
- data/lib/tools/spawn_specialist.rb +10 -8
- data/lib/tools/spawn_subagent.rb +17 -13
- data/lib/tools/subagent_prompts.rb +13 -15
- data/lib/tui/app.rb +389 -146
- data/lib/tui/cable_client.rb +9 -16
- data/lib/tui/decorators/base_decorator.rb +24 -4
- data/lib/tui/decorators/bash_decorator.rb +1 -1
- data/lib/tui/decorators/edit_decorator.rb +4 -2
- data/lib/tui/decorators/read_decorator.rb +4 -2
- data/lib/tui/decorators/think_decorator.rb +2 -2
- data/lib/tui/decorators/web_get_decorator.rb +1 -1
- data/lib/tui/decorators/write_decorator.rb +4 -2
- data/lib/tui/flash.rb +19 -14
- data/lib/tui/formatting.rb +20 -9
- data/lib/tui/input_buffer.rb +6 -6
- data/lib/tui/message_store.rb +89 -1
- data/lib/tui/performance_logger.rb +2 -3
- data/lib/tui/screens/chat.rb +56 -60
- data/lib/tui/settings.rb +86 -0
- data/templates/config.toml +12 -9
- data/templates/tui.toml +209 -0
- metadata +14 -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"
|
|
@@ -68,13 +67,13 @@ detectors:
|
|
|
68
67
|
- "WebGetToolDecorator#text_html"
|
|
69
68
|
# Session model is the core domain object — methods grow naturally.
|
|
70
69
|
# Mcp CLI accumulates subcommand helpers across add/remove/list/secrets.
|
|
71
|
-
#
|
|
72
|
-
#
|
|
70
|
+
# ShellSession probes multiple orthogonal facets (CWD, Git, project files)
|
|
71
|
+
# and manages PTY lifecycle — methods grow with responsibilities.
|
|
73
72
|
TooManyMethods:
|
|
74
73
|
exclude:
|
|
75
74
|
- "Session"
|
|
76
75
|
- "Anima::CLI::Mcp"
|
|
77
|
-
- "
|
|
76
|
+
- "ShellSession"
|
|
78
77
|
# Runner composes system prompt from modular sections — methods grow with responsibilities.
|
|
79
78
|
- "AnalyticalBrain::Runner"
|
|
80
79
|
# Decorators branch on tool type across 4 render modes — inherent to the pattern.
|
|
@@ -86,11 +85,11 @@ detectors:
|
|
|
86
85
|
# Runner checks session type to compose responsibilities — the core dispatch.
|
|
87
86
|
- "AnalyticalBrain::Runner"
|
|
88
87
|
# EventDecorator holds shared rendering constants (icons, markers, dispatch maps).
|
|
89
|
-
#
|
|
88
|
+
# Message holds domain type constants (TYPES, CONTEXT_TYPES, LLM_TYPES, etc.).
|
|
90
89
|
TooManyConstants:
|
|
91
90
|
exclude:
|
|
92
91
|
- "EventDecorator"
|
|
93
|
-
- "
|
|
92
|
+
- "Message"
|
|
94
93
|
# encode_utf8 is descriptive — the digit triggers a false positive.
|
|
95
94
|
UncommunicativeMethodName:
|
|
96
95
|
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,7 +129,8 @@ 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
136
|
│ └── credentials/ # Rails encrypted credentials (includes AR encryption keys)
|
|
@@ -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
|
|
|
@@ -178,7 +179,7 @@ Plus dynamic tools from configured MCP servers, namespaced as `server_name__tool
|
|
|
178
179
|
|
|
179
180
|
### Sub-Agents
|
|
180
181
|
|
|
181
|
-
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`).
|
|
182
183
|
|
|
183
184
|
Two types:
|
|
184
185
|
|
|
@@ -194,24 +195,25 @@ Two types:
|
|
|
194
195
|
|
|
195
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.
|
|
196
197
|
|
|
197
|
-
Each sub-agent is spawned with a single **Goal**
|
|
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.
|
|
198
199
|
|
|
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.
|
|
200
201
|
|
|
201
202
|
### Skills
|
|
202
203
|
|
|
203
|
-
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.
|
|
204
205
|
|
|
205
206
|
- **Built-in skills:** ActiveRecord, Draper decorators, DragonRuby, MCP server, RatatuiRuby, RSpec, GitHub issues
|
|
206
207
|
- **User skills:** Drop `.md` files into `~/.anima/skills/` to add custom knowledge
|
|
207
208
|
- **Override:** User skills with the same name replace built-in ones
|
|
208
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
|
|
209
211
|
|
|
210
212
|
Active skills are displayed in the TUI HUD panel (toggle with `C-a → h`).
|
|
211
213
|
|
|
212
214
|
### Workflows
|
|
213
215
|
|
|
214
|
-
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.
|
|
215
217
|
|
|
216
218
|
- **Built-in workflows:** `feature`, `commit`, `create_plan`, `implement_plan`, `review_pr`, `create_note`, `research_codebase`, `decompose_ticket`, and more
|
|
217
219
|
- **User workflows:** Drop `.md` files into `~/.anima/workflows/` to add custom workflows
|
|
@@ -286,7 +288,9 @@ Goals form a two-level hierarchy (root goals with sub-goals) and are displayed i
|
|
|
286
288
|
|
|
287
289
|
### Configuration
|
|
288
290
|
|
|
289
|
-
|
|
291
|
+
Brain and TUI have separate config files — both hot-reloadable (no restart needed).
|
|
292
|
+
|
|
293
|
+
**Brain settings** (`~/.anima/config.toml`):
|
|
290
294
|
|
|
291
295
|
```toml
|
|
292
296
|
[llm]
|
|
@@ -294,15 +298,14 @@ model = "claude-opus-4-6"
|
|
|
294
298
|
fast_model = "claude-haiku-4-5"
|
|
295
299
|
max_tokens = 8192
|
|
296
300
|
max_tool_rounds = 250
|
|
297
|
-
token_budget =
|
|
301
|
+
token_budget = 120_000
|
|
302
|
+
subagent_model = "claude-sonnet-4-6"
|
|
303
|
+
subagent_token_budget = 90_000
|
|
298
304
|
|
|
299
305
|
[timeouts]
|
|
300
306
|
api = 300
|
|
301
307
|
command = 30
|
|
302
308
|
|
|
303
|
-
[goals]
|
|
304
|
-
completed_decay_messages = 5
|
|
305
|
-
|
|
306
309
|
[analytical_brain]
|
|
307
310
|
max_tokens = 4096
|
|
308
311
|
blocking_on_user_message = true
|
|
@@ -313,6 +316,27 @@ default_view_mode = "basic"
|
|
|
313
316
|
name_generation_interval = 30
|
|
314
317
|
```
|
|
315
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
|
+
|
|
316
340
|
## Design
|
|
317
341
|
|
|
318
342
|
### Three Layers (mirroring biology)
|
|
@@ -341,7 +365,7 @@ Events flow through two channels:
|
|
|
341
365
|
1. **In-process** — Rails Structured Event Reporter (local subscribers like Persister)
|
|
342
366
|
2. **Over the wire** — Action Cable WebSocket (`Event::Broadcasting` callbacks push to connected TUI clients)
|
|
343
367
|
|
|
344
|
-
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.
|
|
345
369
|
|
|
346
370
|
### Context as Viewport, Not Tape
|
|
347
371
|
|
|
@@ -351,7 +375,7 @@ The viewport is a live query, not a log. It walks events newest-first until the
|
|
|
351
375
|
|
|
352
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.
|
|
353
377
|
|
|
354
|
-
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.
|
|
355
379
|
|
|
356
380
|
### Brain as Microservices on a Shared Event Bus
|
|
357
381
|
|
|
@@ -408,6 +432,30 @@ The right-side HUD panel shows session state at a glance: session name, goals (w
|
|
|
408
432
|
|
|
409
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).
|
|
410
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
|
+
|
|
411
459
|
When content exceeds the panel height, the HUD scrolls. Three input methods:
|
|
412
460
|
|
|
413
461
|
| Input | Action |
|
|
@@ -609,7 +657,7 @@ This single example demonstrates every core principle:
|
|
|
609
657
|
- Mneme memory department (eviction-triggered summarization, persistent snapshots, goal-scoped event pinning, associative recall)
|
|
610
658
|
- 12 built-in tools + MCP integration (HTTP + stdio transports)
|
|
611
659
|
- 7 built-in skills + 13 built-in workflows (user-extensible)
|
|
612
|
-
- Sub-agents with
|
|
660
|
+
- Sub-agents with isolated context (5 specialists + generic)
|
|
613
661
|
- Client-server architecture with WebSocket transport + graceful reconnection
|
|
614
662
|
- Collapsible HUD panel with goals, skills, workflow, and sub-agent tracking
|
|
615
663
|
- Three TUI view modes (Basic / Verbose / Debug)
|
|
@@ -105,10 +105,10 @@ class ToolCallDecorator < MessageDecorator
|
|
|
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
107
|
# separate lines, matching how read_file tool responses display file content.
|
|
108
|
-
# @param input [Hash] tool input hash with "
|
|
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
|
|
|
@@ -126,7 +126,7 @@ class ToolCallDecorator < MessageDecorator
|
|
|
126
126
|
when "web_get"
|
|
127
127
|
"GET #{input&.dig("url")}"
|
|
128
128
|
when "read_file", "edit_file", "write_file"
|
|
129
|
-
input&.dig("
|
|
129
|
+
input&.dig("path").to_s
|
|
130
130
|
else
|
|
131
131
|
truncate_lines(Toon.encode(input), max_lines: 2)
|
|
132
132
|
end
|
|
@@ -66,10 +66,10 @@ class AgentRequestJob < ApplicationJob
|
|
|
66
66
|
agent_loop.run
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
# Process any pending messages
|
|
69
|
+
# Process any pending messages that arrived after the last tool round.
|
|
70
70
|
loop do
|
|
71
71
|
promoted = session.promote_pending_messages!
|
|
72
|
-
break if promoted
|
|
72
|
+
break if promoted[:texts].empty? && promoted[:pairs].empty?
|
|
73
73
|
agent_loop.run
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Runs passive recall after goal updates — searches message history for
|
|
4
|
-
# context relevant to active goals and
|
|
5
|
-
#
|
|
4
|
+
# context relevant to active goals and injects phantom tool_call/tool_response
|
|
5
|
+
# pairs into the session's message stream.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Phantom pairs ride the conveyor belt like regular messages, getting
|
|
8
|
+
# cached, evicted, and compressed by Mneme naturally.
|
|
9
9
|
#
|
|
10
10
|
# @example
|
|
11
11
|
# PassiveRecallJob.perform_later(session.id)
|
|
@@ -17,13 +17,8 @@ class PassiveRecallJob < ApplicationJob
|
|
|
17
17
|
# @param session_id [Integer]
|
|
18
18
|
def perform(session_id)
|
|
19
19
|
session = Session.find(session_id)
|
|
20
|
-
|
|
20
|
+
count = Mneme::PassiveRecall.new(session).call
|
|
21
21
|
|
|
22
|
-
if
|
|
23
|
-
session.update_column(:recalled_message_ids, results.map(&:message_id))
|
|
24
|
-
Mneme.logger.info("session=#{session_id} — passive recall found #{results.size} memories")
|
|
25
|
-
elsif session.recalled_message_ids.present?
|
|
26
|
-
session.update_column(:recalled_message_ids, [])
|
|
27
|
-
end
|
|
22
|
+
Mneme.logger.info("session=#{session_id} — passive recall injected #{count} phantom pairs") if count > 0
|
|
28
23
|
end
|
|
29
24
|
end
|
|
@@ -68,6 +68,7 @@ module Message::Broadcasting
|
|
|
68
68
|
mode = session.view_mode
|
|
69
69
|
decorator = MessageDecorator.for(self)
|
|
70
70
|
broadcast_payload = payload.merge("id" => id, "action" => action)
|
|
71
|
+
broadcast_payload["api_metrics"] = api_metrics if api_metrics.present?
|
|
71
72
|
|
|
72
73
|
if decorator
|
|
73
74
|
broadcast_payload["rendered"] = {mode => decorator.render(mode)}
|
data/app/models/goal.rb
CHANGED
|
@@ -31,7 +31,8 @@ class Goal < ApplicationRecord
|
|
|
31
31
|
scope :not_evicted, -> { where(evicted_at: nil) }
|
|
32
32
|
|
|
33
33
|
# @!method self.evictable
|
|
34
|
-
# Completed goals
|
|
34
|
+
# Completed goals not yet evicted — their phantom pairs remain in the
|
|
35
|
+
# sliding window until Mneme compresses them during the eviction cycle.
|
|
35
36
|
# @return [ActiveRecord::Relation]
|
|
36
37
|
scope :evictable, -> { completed.where(evicted_at: nil) }
|
|
37
38
|
|
data/app/models/message.rb
CHANGED
|
@@ -28,8 +28,6 @@ class Message < ApplicationRecord
|
|
|
28
28
|
CONTEXT_TYPES = %w[system_message user_message agent_message tool_call tool_response].freeze
|
|
29
29
|
CONVERSATION_TYPES = %w[user_message agent_message system_message].freeze
|
|
30
30
|
THINK_TOOL = "think"
|
|
31
|
-
SPAWN_TOOLS = %w[spawn_subagent spawn_specialist].freeze
|
|
32
|
-
|
|
33
31
|
# Message types that require a tool_use_id to pair call with response.
|
|
34
32
|
TOOL_TYPES = %w[tool_call tool_response].freeze
|
|
35
33
|
|
|
@@ -71,17 +69,6 @@ class Message < ApplicationRecord
|
|
|
71
69
|
# @return [ActiveRecord::Relation]
|
|
72
70
|
scope :context_messages, -> { where(message_type: CONTEXT_TYPES) }
|
|
73
71
|
|
|
74
|
-
# @!method self.excluding_spawn_messages
|
|
75
|
-
# Excludes spawn_subagent/spawn_specialist tool_call and tool_response messages.
|
|
76
|
-
# Used when building parent context for sub-agents — spawn messages cause role
|
|
77
|
-
# confusion because the sub-agent sees sibling spawn results and mistakes
|
|
78
|
-
# itself for the parent.
|
|
79
|
-
# @return [ActiveRecord::Relation]
|
|
80
|
-
scope :excluding_spawn_messages, -> {
|
|
81
|
-
where.not("message_type IN (?) AND json_extract(payload, '$.tool_name') IN (?)",
|
|
82
|
-
TOOL_TYPES, SPAWN_TOOLS)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
72
|
# Maps message_type to the Anthropic Messages API role.
|
|
86
73
|
# @return [String] "user" or "assistant"
|
|
87
74
|
def api_role
|
|
@@ -1,27 +1,175 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# A
|
|
3
|
+
# A message waiting to enter a session's conversation history.
|
|
4
4
|
# Pending messages live in their own table — they are NOT part of the
|
|
5
5
|
# message stream and have no database ID that could interleave with
|
|
6
6
|
# tool_call/tool_response pairs.
|
|
7
7
|
#
|
|
8
|
-
# Created when a
|
|
8
|
+
# Created when a message arrives while the session is processing.
|
|
9
9
|
# Promoted to a real {Message} (delete + create in transaction) when
|
|
10
10
|
# the current agent loop completes, giving the new message an ID that
|
|
11
11
|
# naturally follows the tool batch.
|
|
12
12
|
#
|
|
13
|
+
# Each pending message knows its source (+source_type+, +source_name+)
|
|
14
|
+
# and how to serialize itself for the LLM conversation via {#to_llm_messages}.
|
|
15
|
+
# Non-user messages (sub-agent results, recalled skills, workflows, recall,
|
|
16
|
+
# goal events) become synthetic tool_use/tool_result pairs so the LLM sees
|
|
17
|
+
# "a tool I invoked returned a result" rather than "a user wrote me."
|
|
18
|
+
#
|
|
13
19
|
# @see Session#enqueue_user_message
|
|
14
20
|
# @see Session#promote_pending_messages!
|
|
15
21
|
class PendingMessage < ApplicationRecord
|
|
22
|
+
# Synthetic tool names used in tool_use/tool_result pairs injected into
|
|
23
|
+
# the parent LLM conversation when non-user messages are promoted.
|
|
24
|
+
# These tools don't exist in the agent's registry — the agent sees
|
|
25
|
+
# them as its own past actions (phantom tool calls).
|
|
26
|
+
SUBAGENT_TOOL = "subagent_message"
|
|
27
|
+
RECALL_SKILL_TOOL = "recall_skill"
|
|
28
|
+
RECALL_WORKFLOW_TOOL = "recall_workflow"
|
|
29
|
+
RECALL_MEMORY_TOOL = "recall_memory"
|
|
30
|
+
RECALL_GOAL_TOOL = "recall_goal"
|
|
31
|
+
|
|
32
|
+
# Source types that produce phantom tool_use/tool_result pairs on promotion.
|
|
33
|
+
# User messages produce plain text blocks instead.
|
|
34
|
+
PHANTOM_PAIR_TYPES = %w[subagent skill workflow recall goal].freeze
|
|
35
|
+
|
|
36
|
+
# Maps each phantom pair source type to its synthetic tool name.
|
|
37
|
+
PHANTOM_TOOL_NAMES = {
|
|
38
|
+
"subagent" => SUBAGENT_TOOL,
|
|
39
|
+
"skill" => RECALL_SKILL_TOOL,
|
|
40
|
+
"workflow" => RECALL_WORKFLOW_TOOL,
|
|
41
|
+
"recall" => RECALL_MEMORY_TOOL,
|
|
42
|
+
"goal" => RECALL_GOAL_TOOL
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# Maps each phantom pair source type to a lambda building its tool input.
|
|
46
|
+
PHANTOM_TOOL_INPUTS = {
|
|
47
|
+
"subagent" => ->(name) { {from: name} },
|
|
48
|
+
"skill" => ->(name) { {skill: name} },
|
|
49
|
+
"workflow" => ->(name) { {workflow: name} },
|
|
50
|
+
"recall" => ->(name) { {message_id: name.to_i} },
|
|
51
|
+
"goal" => ->(name) { {goal_id: name.to_i} }
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
16
54
|
belongs_to :session
|
|
17
55
|
|
|
18
56
|
validates :content, presence: true
|
|
57
|
+
validates :source_type, inclusion: {in: %w[user subagent skill workflow recall goal]}
|
|
58
|
+
validates :source_name, presence: true, unless: :user?
|
|
19
59
|
|
|
20
60
|
after_create_commit :broadcast_created
|
|
21
61
|
after_destroy_commit :broadcast_removed
|
|
22
62
|
|
|
63
|
+
# @return [Boolean] true when this is a plain user message
|
|
64
|
+
def user?
|
|
65
|
+
source_type == "user"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Boolean] true when this message originated from a sub-agent
|
|
69
|
+
def subagent?
|
|
70
|
+
source_type == "subagent"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Boolean] true when this message carries recalled skill content
|
|
74
|
+
def skill?
|
|
75
|
+
source_type == "skill"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Boolean] true when this message carries recalled workflow content
|
|
79
|
+
def workflow?
|
|
80
|
+
source_type == "workflow"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Boolean] true when this message is an associative recall phantom pair
|
|
84
|
+
def recall?
|
|
85
|
+
source_type == "recall"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Boolean] true when this message carries a goal event
|
|
89
|
+
def goal?
|
|
90
|
+
source_type == "goal"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Boolean] true when promotion produces phantom tool_use/tool_result pairs
|
|
94
|
+
def phantom_pair?
|
|
95
|
+
source_type.in?(PHANTOM_PAIR_TYPES)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Phantom tool name for DB persistence and LLM injection.
|
|
99
|
+
# Each phantom pair source type maps to a synthetic tool name.
|
|
100
|
+
#
|
|
101
|
+
# @return [String] phantom tool name
|
|
102
|
+
def phantom_tool_name
|
|
103
|
+
PHANTOM_TOOL_NAMES.fetch(source_type)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Phantom tool input hash for DB persistence and LLM injection.
|
|
107
|
+
#
|
|
108
|
+
# @return [Hash] tool input hash
|
|
109
|
+
def phantom_tool_input
|
|
110
|
+
PHANTOM_TOOL_INPUTS.fetch(source_type).call(source_name)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Content formatted for display and history persistence.
|
|
114
|
+
# Sub-agent messages include an attribution prefix. Skill/workflow
|
|
115
|
+
# messages include a recall label. User messages pass through unchanged.
|
|
116
|
+
#
|
|
117
|
+
# @return [String]
|
|
118
|
+
def display_content
|
|
119
|
+
case source_type
|
|
120
|
+
when "subagent"
|
|
121
|
+
format(Tools::ResponseTruncator::ATTRIBUTION_FORMAT, source_name, content)
|
|
122
|
+
when "skill"
|
|
123
|
+
"[recalled skill: #{source_name}]\n#{content}"
|
|
124
|
+
when "workflow"
|
|
125
|
+
"[recalled workflow: #{source_name}]\n#{content}"
|
|
126
|
+
when "goal"
|
|
127
|
+
"[goal #{source_name}]\n#{content}"
|
|
128
|
+
else
|
|
129
|
+
content
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Builds LLM message hashes for this pending message.
|
|
134
|
+
#
|
|
135
|
+
# Phantom pair types become synthetic tool_use/tool_result pairs so the
|
|
136
|
+
# LLM sees them as its own past invocations. User messages return plain
|
|
137
|
+
# content for injection as text blocks within the current tool_results turn.
|
|
138
|
+
#
|
|
139
|
+
# @return [Array<Hash>] synthetic tool pair for phantom pair types
|
|
140
|
+
# @return [String] raw content for user messages
|
|
141
|
+
def to_llm_messages
|
|
142
|
+
return content unless phantom_pair?
|
|
143
|
+
|
|
144
|
+
build_phantom_pair(phantom_tool_name, phantom_tool_input)
|
|
145
|
+
end
|
|
146
|
+
|
|
23
147
|
private
|
|
24
148
|
|
|
149
|
+
# Builds a phantom tool_use/tool_result message pair.
|
|
150
|
+
# Follows the same format for all non-user source types — the only
|
|
151
|
+
# difference is the tool name and input hash.
|
|
152
|
+
#
|
|
153
|
+
# Phantom pairs keep the system prompt stable for prompt caching (#395).
|
|
154
|
+
# Instead of injecting skills/workflows into the system prompt (which
|
|
155
|
+
# busts the cache on every change), they flow through the sliding window
|
|
156
|
+
# as messages the LLM "recalls" via phantom tool invocations.
|
|
157
|
+
#
|
|
158
|
+
# @param tool_name [String] phantom tool name (not in the agent's registry)
|
|
159
|
+
# @param input [Hash] tool input hash
|
|
160
|
+
# @return [Array<Hash>] two-element array: assistant tool_use + user tool_result
|
|
161
|
+
def build_phantom_pair(tool_name, input)
|
|
162
|
+
tool_use_id = "#{tool_name}_#{id}"
|
|
163
|
+
[
|
|
164
|
+
{role: "assistant", content: [
|
|
165
|
+
{type: "tool_use", id: tool_use_id, name: tool_name, input: input}
|
|
166
|
+
]},
|
|
167
|
+
{role: "user", content: [
|
|
168
|
+
{type: "tool_result", tool_use_id: tool_use_id, content: content}
|
|
169
|
+
]}
|
|
170
|
+
]
|
|
171
|
+
end
|
|
172
|
+
|
|
25
173
|
# Broadcasts a pending message appearance so TUI clients render the
|
|
26
174
|
# dimmed indicator immediately.
|
|
27
175
|
def broadcast_created
|