anima-core 1.1.2 → 1.2.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 +8 -0
- 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 +1 -1
- data/app/channels/session_channel.rb +46 -49
- 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 +2 -2
- data/app/decorators/tool_decorator.rb +4 -4
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +3 -3
- data/app/decorators/web_get_tool_decorator.rb +41 -9
- data/app/jobs/agent_request_job.rb +20 -20
- 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 +4 -4
- data/app/models/goal_pinned_message.rb +11 -0
- data/app/models/message.rb +132 -0
- data/app/models/pinned_message.rb +41 -0
- data/app/models/session.rb +232 -192
- data/app/models/snapshot.rb +25 -25
- data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
- data/lib/agent_loop.rb +17 -9
- data/lib/agents/registry.rb +1 -1
- data/lib/analytical_brain/runner.rb +35 -35
- 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/settings.rb +19 -4
- data/lib/anima/version.rb +1 -1
- data/lib/events/bounce_back.rb +7 -7
- data/lib/events/subscribers/persister.rb +7 -7
- data/lib/events/subscribers/subagent_message_router.rb +12 -12
- data/lib/events/subscribers/transient_broadcaster.rb +2 -2
- data/lib/llm/client.rb +5 -2
- 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 +55 -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/skills/registry.rb +1 -1
- data/lib/tools/bash.rb +82 -7
- data/lib/tools/edit.rb +4 -6
- data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
- data/lib/tools/read.rb +4 -4
- data/lib/tools/registry.rb +1 -1
- data/lib/tools/remember.rb +46 -55
- data/lib/tools/spawn_specialist.rb +12 -23
- data/lib/tools/spawn_subagent.rb +9 -19
- data/lib/tools/subagent_prompts.rb +0 -2
- data/lib/tools/think.rb +3 -10
- data/lib/tools/web_get.rb +23 -4
- data/lib/tools/write.rb +3 -3
- data/lib/tui/cable_client.rb +3 -3
- data/lib/tui/message_store.rb +37 -37
- data/lib/tui/screens/chat.rb +27 -15
- data/lib/workflows/registry.rb +1 -1
- 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 +21 -4
- 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 +10 -9
- data/app/jobs/count_event_tokens_job.rb +0 -39
- data/app/models/event.rb +0 -110
- 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
data/workflows/resume_handoff.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: resume_handoff
|
|
3
|
-
description: "
|
|
3
|
+
description: "Continue work left by a previous session — read its handoff, verify assumptions, and pick up where it stopped."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Resume work from a handoff document
|
data/workflows/review_pr.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review_pr
|
|
3
|
-
description: "
|
|
3
|
+
description: "All PR review operations — review, re-review, self-review, or address reviewer feedback."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Modes
|
|
@@ -8,9 +8,14 @@ description: "Multi-agent PR review with three modes: review, re-review, self-re
|
|
|
8
8
|
- **review** (default): Full review, present findings for approval before posting
|
|
9
9
|
- **re-review**: Also load existing review feedback; verify previously requested changes were addressed
|
|
10
10
|
- **self-review**: Fix findings directly in code instead of posting a review
|
|
11
|
+
- **address-feedback**: Read reviewer comments, research the codebase, fix issues, reply to each comment on GitHub
|
|
11
12
|
|
|
12
13
|
## Process
|
|
13
14
|
|
|
15
|
+
Your role is **orchestrator and judge**, not doer. You collect artifacts, delegate analysis to subagents, and apply judgment to their output. The subagents read the code and comments — you decide what to do about their findings. Your context budget is reserved for judgment, not for reading raw data.
|
|
16
|
+
|
|
17
|
+
Steps are sequential — later steps depend on earlier results. Complete each step and wait for its results before starting the next. Skipping ahead without subagent results means the judgment layer in "Step 5: Merge Results" has nothing to work with. Only parallelize where explicitly marked (e.g., "spawn in parallel").
|
|
18
|
+
|
|
14
19
|
### Step 1: Gather PR Metadata
|
|
15
20
|
|
|
16
21
|
```bash
|
|
@@ -18,19 +23,27 @@ gh pr view <PR_NUMBER> --json number,title,body,url,headRefName,baseRefName
|
|
|
18
23
|
```
|
|
19
24
|
|
|
20
25
|
Extract from PR body:
|
|
21
|
-
- **Issue reference** (e.g., #123) —
|
|
26
|
+
- **Issue reference** (e.g., #123) — fetch full issue details via `gh issue view` for requirements and acceptance criteria. You can't review a feature without knowing what was in the task description before it was implemented.
|
|
22
27
|
- **Business context** — why this change is needed
|
|
23
28
|
|
|
24
|
-
If re-review mode is activated, also save all existing review feedback to `/tmp
|
|
29
|
+
If re-review or address-feedback mode is activated, also save all existing review feedback to `/tmp/`:
|
|
30
|
+
|
|
31
|
+
**Do not read these files — pass them to subagents by path only.** The subagents will read and analyze the content. Reading them here would consume context budget that the main agent needs for judgment in "Step 5: Merge Results".
|
|
25
32
|
```bash
|
|
26
33
|
# Review verdicts and bodies (APPROVED, CHANGES_REQUESTED, COMMENTED)
|
|
27
|
-
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/reviews
|
|
34
|
+
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/reviews \
|
|
35
|
+
--jq '[.[] | {id, state, body, html_url, commit_id, submitted_at, author_association, user: .user.login}]' \
|
|
36
|
+
| tee /tmp/pr_<NUMBER>_reviews.json | toon
|
|
28
37
|
|
|
29
38
|
# Inline review comments on specific diff lines
|
|
30
|
-
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/comments
|
|
39
|
+
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/comments \
|
|
40
|
+
--jq '[.[] | {id, pull_request_review_id, body, path, line, start_line, side, diff_hunk, commit_id, created_at, author_association, in_reply_to_id, user: .user.login}]' \
|
|
41
|
+
| tee /tmp/pr_<NUMBER>_inline_comments.json | toon
|
|
31
42
|
|
|
32
43
|
# Conversation-level comments
|
|
33
|
-
gh api repos/<OWNER>/<REPO>/issues/<PR_NUMBER>/comments
|
|
44
|
+
gh api repos/<OWNER>/<REPO>/issues/<PR_NUMBER>/comments \
|
|
45
|
+
--jq '[.[] | {id, body, html_url, created_at, updated_at, author_association, user: .user.login}]' \
|
|
46
|
+
| tee /tmp/pr_<NUMBER>_conversation.json | toon
|
|
34
47
|
```
|
|
35
48
|
|
|
36
49
|
### Step 2: Fetch and Save Diff
|
|
@@ -71,9 +84,11 @@ specialist: thoughts-analyzer
|
|
|
71
84
|
Prompt: "What do we know about <ticket reference and title from Step 1>? What decisions, constraints, and trade-offs should reviewers be aware of?"
|
|
72
85
|
```
|
|
73
86
|
|
|
74
|
-
**Wait for this specialist to complete
|
|
87
|
+
**Wait for this specialist to complete, then proceed to "Step 4-a: Spawn Review Subagents".**
|
|
88
|
+
|
|
89
|
+
### Step 4-a: Spawn Review Subagents
|
|
75
90
|
|
|
76
|
-
|
|
91
|
+
If address-feedback mode is activated, skip to "Step 4-b: Spawn Codebase Research Subagents (address-feedback)" below.
|
|
77
92
|
|
|
78
93
|
Spawn all five review subagents **in parallel** using `spawn_subagent`. Each receives:
|
|
79
94
|
- Path to the diff file in `/tmp/`
|
|
@@ -218,13 +233,21 @@ Read the diff from: /tmp/pr_<number>_diff.txt
|
|
|
218
233
|
- Method and class naming clarity
|
|
219
234
|
- Missing YARD documentation on public interfaces
|
|
220
235
|
- Complex logic lacking explanatory comments
|
|
221
|
-
- Changelog updates for notable changes
|
|
222
236
|
- Misleading or outdated comments
|
|
223
237
|
- Magic numbers or strings needing constants
|
|
224
238
|
|
|
225
239
|
Output: List findings tagged [major], [minor], or [nit] with file:line references."
|
|
226
240
|
```
|
|
227
241
|
|
|
242
|
+
### Step 4-b: Spawn Codebase Research Subagents (address-feedback)
|
|
243
|
+
|
|
244
|
+
Unless address-feedback mode is activated, skip to "Step 5: Merge Results" below.
|
|
245
|
+
|
|
246
|
+
Spawn **codebase-analyzer** and **codebase-pattern-finder** specialists in parallel. Each receives:
|
|
247
|
+
- Paths to comment files and diff file in `/tmp/`
|
|
248
|
+
- Historical context (from Step 3)
|
|
249
|
+
- Any additional instructions from the user's input
|
|
250
|
+
|
|
228
251
|
### Step 5: Merge Results
|
|
229
252
|
|
|
230
253
|
After all subagents complete, compile findings into a unified review.
|
|
@@ -235,6 +258,12 @@ For each finding, evaluate:
|
|
|
235
258
|
- **Real-world probability** — Can this actually happen in practice, or is it purely theoretical? A race condition that requires two users to open a personal link within the same millisecond is not a real issue.
|
|
236
259
|
- **Cost-benefit** — Does the fix add more complexity than the problem warrants? If the "fix" makes the code harder to read without solving a problem a human would encounter, drop it.
|
|
237
260
|
- **Scope** — Review fixes should improve code you're touching, not introduce new artifacts. Clean up, don't build out.
|
|
261
|
+
- **Design intent** — Was this a deliberate choice? A finding that flags a conscious trade-off documented in historical context is a decline, not a fix.
|
|
262
|
+
|
|
263
|
+
Then classify:
|
|
264
|
+
- **Accept & fix** — finding valid, apply the suggested fix or a better one
|
|
265
|
+
- **Accept, different approach** — finding valid, but context points to a different solution
|
|
266
|
+
- **Decline** — not valid in context, or a deliberate design choice
|
|
238
267
|
|
|
239
268
|
Then compile:
|
|
240
269
|
|
|
@@ -249,7 +278,9 @@ Determine verdict:
|
|
|
249
278
|
|
|
250
279
|
### Step 6: Finalize
|
|
251
280
|
|
|
252
|
-
|
|
281
|
+
Your role changes from orchestrator to doer. You now have the judgment results — act on them.
|
|
282
|
+
|
|
283
|
+
If self-review or address-feedback mode is activated, skip to "Step 7: Apply Fixes" below.
|
|
253
284
|
|
|
254
285
|
#### Present and Post (review / re-review)
|
|
255
286
|
|
|
@@ -271,21 +302,52 @@ gh pr review <PR_NUMBER> --approve --body "<review body>"
|
|
|
271
302
|
gh pr review <PR_NUMBER> --request-changes --body "<review body>"
|
|
272
303
|
```
|
|
273
304
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
Instead of presenting and posting, act on the findings:
|
|
305
|
+
### Step 7: Apply Fixes (self-review / address-feedback)
|
|
277
306
|
|
|
278
307
|
1. **Fix findings** — Address [major] and [minor] issues directly in code. Apply [nit]s at own discretion.
|
|
279
308
|
2. **Commit and push** — Commit the fixes with a descriptive message and push to the PR branch.
|
|
280
|
-
3. **
|
|
281
|
-
|
|
309
|
+
3. **Monitor CI** — Wait for CI to pass. The PR cannot be finalized until CI is green.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
gh pr checks <PR_NUMBER> --watch
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Fixes are pushed and CI is green. Now finalize the PR — proceed to the section matching your mode below.
|
|
316
|
+
|
|
317
|
+
#### Self-Review Finalization
|
|
318
|
+
|
|
319
|
+
Assign the user and request review from anyone mentioned in additional instructions. Then mark the PR as ready for human review.
|
|
282
320
|
|
|
283
321
|
```bash
|
|
284
322
|
gh pr edit <PR_NUMBER> --add-assignee <user> --add-reviewer <reviewer>
|
|
285
|
-
# once CI is green:
|
|
286
323
|
gh pr ready <PR_NUMBER>
|
|
287
324
|
```
|
|
288
325
|
|
|
326
|
+
Done when the PR is marked ready and appears in the reviewer's queue.
|
|
327
|
+
|
|
328
|
+
#### Address-Feedback Finalization
|
|
329
|
+
|
|
330
|
+
Reply to each reviewer comment on GitHub with the resolution:
|
|
331
|
+
- Accept & fix: "Fixed in `<commit sha>`."
|
|
332
|
+
- Accept, different approach: "Agreed with the concern. Took a different approach: [explanation]. Fixed in `<commit sha>`."
|
|
333
|
+
- Decline: "This was intentional — [rationale]."
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
# Reply to an inline comment
|
|
337
|
+
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/comments/<COMMENT_ID>/replies -f body="<reply>"
|
|
338
|
+
|
|
339
|
+
# Reply to a conversation-level comment
|
|
340
|
+
gh api repos/<OWNER>/<REPO>/issues/<PR_NUMBER>/comments -f body="<reply>"
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Then request re-review from the original reviewers:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
gh pr edit <PR_NUMBER> --add-reviewer <original_reviewer>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Done when all comments are answered and re-review is requested.
|
|
350
|
+
|
|
289
351
|
## Posted Review Format
|
|
290
352
|
|
|
291
353
|
```markdown
|
data/workflows/thoughts_init.md
CHANGED
data/workflows/validate_plan.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: validate_plan
|
|
3
|
-
description: "
|
|
3
|
+
description: "Check whether the implementation satisfies the plan — compare success criteria against what was actually built."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Validate Plan
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: anima-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yevhenii Hurin
|
|
@@ -259,7 +259,7 @@ files:
|
|
|
259
259
|
- app/controllers/application_controller.rb
|
|
260
260
|
- app/decorators/agent_message_decorator.rb
|
|
261
261
|
- app/decorators/application_decorator.rb
|
|
262
|
-
- app/decorators/
|
|
262
|
+
- app/decorators/message_decorator.rb
|
|
263
263
|
- app/decorators/system_message_decorator.rb
|
|
264
264
|
- app/decorators/tool_call_decorator.rb
|
|
265
265
|
- app/decorators/tool_decorator.rb
|
|
@@ -269,15 +269,15 @@ files:
|
|
|
269
269
|
- app/jobs/agent_request_job.rb
|
|
270
270
|
- app/jobs/analytical_brain_job.rb
|
|
271
271
|
- app/jobs/application_job.rb
|
|
272
|
-
- app/jobs/
|
|
272
|
+
- app/jobs/count_message_tokens_job.rb
|
|
273
273
|
- app/jobs/mneme_job.rb
|
|
274
274
|
- app/jobs/passive_recall_job.rb
|
|
275
275
|
- app/models/application_record.rb
|
|
276
|
-
- app/models/concerns/
|
|
277
|
-
- app/models/event.rb
|
|
276
|
+
- app/models/concerns/message/broadcasting.rb
|
|
278
277
|
- app/models/goal.rb
|
|
279
|
-
- app/models/
|
|
280
|
-
- app/models/
|
|
278
|
+
- app/models/goal_pinned_message.rb
|
|
279
|
+
- app/models/message.rb
|
|
280
|
+
- app/models/pinned_message.rb
|
|
281
281
|
- app/models/session.rb
|
|
282
282
|
- app/models/snapshot.rb
|
|
283
283
|
- bin/jobs
|
|
@@ -323,6 +323,7 @@ files:
|
|
|
323
323
|
- db/migrate/20260321120000_create_pinned_events.rb
|
|
324
324
|
- db/migrate/20260321140000_create_events_fts_index.rb
|
|
325
325
|
- db/migrate/20260321140100_add_recalled_event_ids_to_sessions.rb
|
|
326
|
+
- db/migrate/20260326180000_rename_event_to_message.rb
|
|
326
327
|
- db/queue_schema.rb
|
|
327
328
|
- db/seeds.rb
|
|
328
329
|
- exe/anima
|
|
@@ -377,7 +378,7 @@ files:
|
|
|
377
378
|
- lib/mneme/passive_recall.rb
|
|
378
379
|
- lib/mneme/runner.rb
|
|
379
380
|
- lib/mneme/search.rb
|
|
380
|
-
- lib/mneme/tools/
|
|
381
|
+
- lib/mneme/tools/attach_messages_to_goals.rb
|
|
381
382
|
- lib/mneme/tools/everything_ok.rb
|
|
382
383
|
- lib/mneme/tools/save_snapshot.rb
|
|
383
384
|
- lib/providers/anthropic.rb
|
|
@@ -389,10 +390,10 @@ files:
|
|
|
389
390
|
- lib/tools/bash.rb
|
|
390
391
|
- lib/tools/edit.rb
|
|
391
392
|
- lib/tools/mcp_tool.rb
|
|
393
|
+
- lib/tools/open_issue.rb
|
|
392
394
|
- lib/tools/read.rb
|
|
393
395
|
- lib/tools/registry.rb
|
|
394
396
|
- lib/tools/remember.rb
|
|
395
|
-
- lib/tools/request_feature.rb
|
|
396
397
|
- lib/tools/spawn_specialist.rb
|
|
397
398
|
- lib/tools/spawn_subagent.rb
|
|
398
399
|
- lib/tools/subagent_prompts.rb
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Counts tokens in an event's payload via the Anthropic API and
|
|
4
|
-
# caches the result on the event record. Enqueued automatically
|
|
5
|
-
# after each LLM event is created.
|
|
6
|
-
class CountEventTokensJob < ApplicationJob
|
|
7
|
-
queue_as :default
|
|
8
|
-
|
|
9
|
-
retry_on Providers::Anthropic::Error, wait: :polynomially_longer, attempts: 3
|
|
10
|
-
discard_on ActiveRecord::RecordNotFound
|
|
11
|
-
|
|
12
|
-
# @param event_id [Integer] the Event record to count tokens for
|
|
13
|
-
def perform(event_id)
|
|
14
|
-
event = Event.find(event_id)
|
|
15
|
-
return if already_counted?(event)
|
|
16
|
-
|
|
17
|
-
provider = Providers::Anthropic.new
|
|
18
|
-
messages = [{role: event.api_role, content: event.payload["content"].to_s}]
|
|
19
|
-
|
|
20
|
-
token_count = provider.count_tokens(
|
|
21
|
-
model: Anima::Settings.model,
|
|
22
|
-
messages: messages
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
# Guard against parallel jobs: reload and re-check before writing.
|
|
26
|
-
# Uses update! (not update_all) so {Event::Broadcasting} after_update_commit
|
|
27
|
-
# broadcasts the updated token count to connected clients.
|
|
28
|
-
event.reload
|
|
29
|
-
return if already_counted?(event)
|
|
30
|
-
|
|
31
|
-
event.update!(token_count: token_count)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def already_counted?(event)
|
|
37
|
-
event.token_count > 0
|
|
38
|
-
end
|
|
39
|
-
end
|
data/app/models/event.rb
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# A persisted record of something that happened during a session.
|
|
4
|
-
# Events are the single source of truth for conversation history —
|
|
5
|
-
# there is no separate chat log, only events attached to a session.
|
|
6
|
-
#
|
|
7
|
-
# @!attribute event_type
|
|
8
|
-
# @return [String] one of {TYPES}: system_message, user_message,
|
|
9
|
-
# agent_message, tool_call, tool_response
|
|
10
|
-
# @!attribute payload
|
|
11
|
-
# @return [Hash] event-specific data (content, tool_name, tool_input, etc.)
|
|
12
|
-
# @!attribute timestamp
|
|
13
|
-
# @return [Integer] nanoseconds since epoch (Process::CLOCK_REALTIME)
|
|
14
|
-
# @!attribute token_count
|
|
15
|
-
# @return [Integer] cached token count for this event's payload (0 until counted)
|
|
16
|
-
# @!attribute tool_use_id
|
|
17
|
-
# @return [String, nil] Anthropic-assigned ID correlating tool_call and tool_response
|
|
18
|
-
class Event < ApplicationRecord
|
|
19
|
-
include Event::Broadcasting
|
|
20
|
-
|
|
21
|
-
TYPES = %w[system_message user_message agent_message tool_call tool_response].freeze
|
|
22
|
-
LLM_TYPES = %w[user_message agent_message].freeze
|
|
23
|
-
CONTEXT_TYPES = %w[system_message user_message agent_message tool_call tool_response].freeze
|
|
24
|
-
CONVERSATION_TYPES = %w[user_message agent_message system_message].freeze
|
|
25
|
-
THINK_TOOL = "think"
|
|
26
|
-
PENDING_STATUS = "pending"
|
|
27
|
-
|
|
28
|
-
ROLE_MAP = {"user_message" => "user", "agent_message" => "assistant"}.freeze
|
|
29
|
-
|
|
30
|
-
# Heuristic: average bytes per token for English prose.
|
|
31
|
-
BYTES_PER_TOKEN = 4
|
|
32
|
-
|
|
33
|
-
belongs_to :session
|
|
34
|
-
has_many :pinned_events, dependent: :destroy
|
|
35
|
-
|
|
36
|
-
validates :event_type, presence: true, inclusion: {in: TYPES}
|
|
37
|
-
validates :payload, presence: true
|
|
38
|
-
validates :timestamp, presence: true
|
|
39
|
-
|
|
40
|
-
after_create :schedule_token_count, if: :llm_message?
|
|
41
|
-
|
|
42
|
-
# @!method self.llm_messages
|
|
43
|
-
# Events that represent conversation turns sent to the LLM API.
|
|
44
|
-
# @return [ActiveRecord::Relation]
|
|
45
|
-
scope :llm_messages, -> { where(event_type: LLM_TYPES) }
|
|
46
|
-
|
|
47
|
-
# @!method self.context_events
|
|
48
|
-
# Events included in the LLM context window (messages + tool interactions).
|
|
49
|
-
# @return [ActiveRecord::Relation]
|
|
50
|
-
scope :context_events, -> { where(event_type: CONTEXT_TYPES) }
|
|
51
|
-
|
|
52
|
-
# @!method self.pending
|
|
53
|
-
# User messages queued during active agent processing, not yet sent to LLM.
|
|
54
|
-
# @return [ActiveRecord::Relation]
|
|
55
|
-
scope :pending, -> { where(status: PENDING_STATUS) }
|
|
56
|
-
|
|
57
|
-
# @!method self.deliverable
|
|
58
|
-
# Events eligible for LLM context (excludes pending messages).
|
|
59
|
-
# NULL status means delivered/processed — the only excluded value is "pending".
|
|
60
|
-
# @return [ActiveRecord::Relation]
|
|
61
|
-
scope :deliverable, -> { where(status: nil) }
|
|
62
|
-
|
|
63
|
-
# Maps event_type to the Anthropic Messages API role.
|
|
64
|
-
# @return [String] "user" or "assistant"
|
|
65
|
-
def api_role
|
|
66
|
-
ROLE_MAP.fetch(event_type)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# @return [Boolean] true if this event represents an LLM conversation turn
|
|
70
|
-
def llm_message?
|
|
71
|
-
event_type.in?(LLM_TYPES)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# @return [Boolean] true if this event is part of the LLM context window
|
|
75
|
-
def context_event?
|
|
76
|
-
event_type.in?(CONTEXT_TYPES)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# @return [Boolean] true if this is a pending message not yet sent to the LLM
|
|
80
|
-
def pending?
|
|
81
|
-
status == PENDING_STATUS
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# @return [Boolean] true if this is a conversation event (user/agent/system message)
|
|
85
|
-
# or a think tool_call — the events Mneme treats as "conversation" for boundary tracking
|
|
86
|
-
def conversation_or_think?
|
|
87
|
-
event_type.in?(CONVERSATION_TYPES) ||
|
|
88
|
-
(event_type == "tool_call" && payload["tool_name"] == THINK_TOOL)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Heuristic token estimate: ~4 bytes per token for English prose.
|
|
92
|
-
# Tool events are estimated from the full payload JSON since tool_input
|
|
93
|
-
# and tool metadata contribute to token count. Messages use content only.
|
|
94
|
-
#
|
|
95
|
-
# @return [Integer] estimated token count (at least 1)
|
|
96
|
-
def estimate_tokens
|
|
97
|
-
text = if event_type.in?(%w[tool_call tool_response])
|
|
98
|
-
payload.to_json
|
|
99
|
-
else
|
|
100
|
-
payload["content"].to_s
|
|
101
|
-
end
|
|
102
|
-
[(text.bytesize / BYTES_PER_TOKEN.to_f).ceil, 1].max
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
private
|
|
106
|
-
|
|
107
|
-
def schedule_token_count
|
|
108
|
-
CountEventTokensJob.perform_later(id)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Join record linking a {Goal} to a {PinnedEvent}. Many-to-many: one event
|
|
4
|
-
# can be pinned to multiple Goals, and one Goal can reference multiple pins.
|
|
5
|
-
# When the last Goal referencing a pin completes, the pin is released.
|
|
6
|
-
class GoalPinnedEvent < ApplicationRecord
|
|
7
|
-
belongs_to :goal
|
|
8
|
-
belongs_to :pinned_event
|
|
9
|
-
|
|
10
|
-
validates :pinned_event_id, uniqueness: {scope: :goal_id}
|
|
11
|
-
end
|
data/app/models/pinned_event.rb
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# A conversation event pinned to one or more Goals by Mneme to protect it
|
|
4
|
-
# from viewport eviction. Pinned events appear in the Goals section of
|
|
5
|
-
# the viewport, giving the main agent access to critical context that
|
|
6
|
-
# would otherwise scroll out of the sliding window.
|
|
7
|
-
#
|
|
8
|
-
# Pinning is goal-scoped: when all Goals referencing a pin complete,
|
|
9
|
-
# the pin is automatically released (reference-counted cleanup).
|
|
10
|
-
#
|
|
11
|
-
# @!attribute display_text
|
|
12
|
-
# @return [String] truncated event content (~200 chars) shown in the Goals section
|
|
13
|
-
class PinnedEvent < ApplicationRecord
|
|
14
|
-
# Display text limit — enough to recognize content, cheap on tokens.
|
|
15
|
-
MAX_DISPLAY_TEXT_LENGTH = 200
|
|
16
|
-
|
|
17
|
-
belongs_to :event
|
|
18
|
-
|
|
19
|
-
has_many :goal_pinned_events, dependent: :destroy
|
|
20
|
-
has_many :goals, through: :goal_pinned_events
|
|
21
|
-
|
|
22
|
-
validates :display_text, presence: true, length: {maximum: MAX_DISPLAY_TEXT_LENGTH}
|
|
23
|
-
validates :event_id, uniqueness: true
|
|
24
|
-
|
|
25
|
-
# Pinned events with no remaining active goals — safe to release.
|
|
26
|
-
#
|
|
27
|
-
# @return [ActiveRecord::Relation]
|
|
28
|
-
scope :orphaned, -> {
|
|
29
|
-
where.not(
|
|
30
|
-
"EXISTS (SELECT 1 FROM goal_pinned_events gpe " \
|
|
31
|
-
"JOIN goals ON goals.id = gpe.goal_id " \
|
|
32
|
-
"WHERE gpe.pinned_event_id = pinned_events.id " \
|
|
33
|
-
"AND goals.status = 'active')"
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
# @return [Integer] token cost estimate for viewport budget accounting
|
|
38
|
-
def token_cost
|
|
39
|
-
[(display_text.bytesize / Event::BYTES_PER_TOKEN.to_f).ceil, 1].max
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mneme
|
|
4
|
-
module Tools
|
|
5
|
-
# Pins critical events to active Goals so they survive viewport eviction.
|
|
6
|
-
# Mneme calls this when it sees important events (user instructions, key
|
|
7
|
-
# decisions, critical corrections) approaching the eviction zone.
|
|
8
|
-
#
|
|
9
|
-
# Events are pinned via a many-to-many join: one event can be attached
|
|
10
|
-
# to multiple Goals. When all referencing Goals complete, the pin is
|
|
11
|
-
# automatically released (reference-counted cleanup in {Goal#release_orphaned_pins!}).
|
|
12
|
-
class AttachEventsToGoals < ::Tools::Base
|
|
13
|
-
def self.tool_name = "attach_events_to_goals"
|
|
14
|
-
|
|
15
|
-
def self.description = "Pin critical events to active goals so they survive " \
|
|
16
|
-
"viewport eviction. Use this for events that are too important to lose — " \
|
|
17
|
-
"exact user instructions, key decisions, critical corrections. " \
|
|
18
|
-
"Events stay pinned until all attached goals complete."
|
|
19
|
-
|
|
20
|
-
def self.input_schema
|
|
21
|
-
{
|
|
22
|
-
type: "object",
|
|
23
|
-
properties: {
|
|
24
|
-
event_ids: {
|
|
25
|
-
type: "array",
|
|
26
|
-
items: {type: "integer"},
|
|
27
|
-
description: "Database IDs of events to pin (from `event N` prefixes in the viewport)"
|
|
28
|
-
},
|
|
29
|
-
goal_ids: {
|
|
30
|
-
type: "array",
|
|
31
|
-
items: {type: "integer"},
|
|
32
|
-
description: "IDs of active goals to attach the events to"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
required: %w[event_ids goal_ids]
|
|
36
|
-
}
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# @param main_session [Session] the session being observed
|
|
40
|
-
def initialize(main_session:, **)
|
|
41
|
-
@session = main_session
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# @param input [Hash<String, Object>] with "event_ids" and "goal_ids"
|
|
45
|
-
# @return [String] confirmation with link count, or error description
|
|
46
|
-
def execute(input)
|
|
47
|
-
event_ids = Array(input["event_ids"]).map(&:to_i).uniq
|
|
48
|
-
goal_ids = Array(input["goal_ids"]).map(&:to_i).uniq
|
|
49
|
-
|
|
50
|
-
return "Error: event_ids cannot be empty" if event_ids.empty?
|
|
51
|
-
return "Error: goal_ids cannot be empty" if goal_ids.empty?
|
|
52
|
-
|
|
53
|
-
events = @session.events.where(id: event_ids)
|
|
54
|
-
goals = @session.goals.active.where(id: goal_ids)
|
|
55
|
-
|
|
56
|
-
missing_events = event_ids - events.pluck(:id)
|
|
57
|
-
inactive_goal_ids = goal_ids - goals.pluck(:id)
|
|
58
|
-
|
|
59
|
-
errors = []
|
|
60
|
-
errors << "Events not found: #{missing_events.join(", ")}" if missing_events.any?
|
|
61
|
-
|
|
62
|
-
if inactive_goal_ids.any?
|
|
63
|
-
completed_ids = @session.goals.completed.where(id: inactive_goal_ids).pluck(:id)
|
|
64
|
-
not_found_ids = inactive_goal_ids - completed_ids
|
|
65
|
-
errors << "Goals already completed: #{completed_ids.join(", ")}" if completed_ids.any?
|
|
66
|
-
errors << "Goals not found: #{not_found_ids.join(", ")}" if not_found_ids.any?
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
return "Error: #{errors.join("; ")}" if errors.any?
|
|
70
|
-
|
|
71
|
-
attached = attach(events, goals)
|
|
72
|
-
"Pinned #{attached} event-goal links"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
private
|
|
76
|
-
|
|
77
|
-
def attach(events, goals)
|
|
78
|
-
events.sum do |event|
|
|
79
|
-
pinned = find_or_create_pinned_event(event)
|
|
80
|
-
link_to_goals(pinned, goals)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def link_to_goals(pinned, goals)
|
|
85
|
-
goals.each { |goal| GoalPinnedEvent.find_or_create_by!(goal: goal, pinned_event: pinned) }
|
|
86
|
-
goals.size
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def find_or_create_pinned_event(event)
|
|
90
|
-
PinnedEvent.find_or_create_by!(event: event) do |pe|
|
|
91
|
-
pe.display_text = truncate_event_content(event)
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def truncate_event_content(event)
|
|
96
|
-
content = event.payload&.dig("content").to_s.strip
|
|
97
|
-
content = "event #{event.id}" if content.empty?
|
|
98
|
-
|
|
99
|
-
if content.length > PinnedEvent::MAX_DISPLAY_TEXT_LENGTH
|
|
100
|
-
content[0, PinnedEvent::MAX_DISPLAY_TEXT_LENGTH - 1] + "…"
|
|
101
|
-
else
|
|
102
|
-
content
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|