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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +8 -0
  3. data/agents/codebase-analyzer.md +1 -1
  4. data/agents/codebase-pattern-finder.md +1 -1
  5. data/agents/documentation-researcher.md +1 -1
  6. data/agents/thoughts-analyzer.md +1 -1
  7. data/agents/web-search-researcher.md +1 -1
  8. data/app/channels/session_channel.rb +46 -49
  9. data/app/decorators/agent_message_decorator.rb +2 -2
  10. data/app/decorators/{event_decorator.rb → message_decorator.rb} +40 -40
  11. data/app/decorators/system_message_decorator.rb +2 -2
  12. data/app/decorators/tool_call_decorator.rb +2 -2
  13. data/app/decorators/tool_decorator.rb +4 -4
  14. data/app/decorators/tool_response_decorator.rb +2 -2
  15. data/app/decorators/user_message_decorator.rb +3 -3
  16. data/app/decorators/web_get_tool_decorator.rb +41 -9
  17. data/app/jobs/agent_request_job.rb +20 -20
  18. data/app/jobs/count_message_tokens_job.rb +39 -0
  19. data/app/jobs/passive_recall_job.rb +4 -4
  20. data/app/models/concerns/{event → message}/broadcasting.rb +16 -16
  21. data/app/models/goal.rb +4 -4
  22. data/app/models/goal_pinned_message.rb +11 -0
  23. data/app/models/message.rb +132 -0
  24. data/app/models/pinned_message.rb +41 -0
  25. data/app/models/session.rb +232 -192
  26. data/app/models/snapshot.rb +25 -25
  27. data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
  28. data/lib/agent_loop.rb +17 -9
  29. data/lib/agents/registry.rb +1 -1
  30. data/lib/analytical_brain/runner.rb +35 -35
  31. data/lib/analytical_brain/tools/activate_skill.rb +5 -9
  32. data/lib/analytical_brain/tools/assign_nickname.rb +2 -4
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +5 -9
  34. data/lib/analytical_brain/tools/everything_is_ready.rb +1 -2
  35. data/lib/analytical_brain/tools/finish_goal.rb +5 -8
  36. data/lib/analytical_brain/tools/read_workflow.rb +5 -9
  37. data/lib/analytical_brain/tools/rename_session.rb +3 -10
  38. data/lib/analytical_brain/tools/set_goal.rb +3 -7
  39. data/lib/analytical_brain/tools/update_goal.rb +3 -7
  40. data/lib/anima/settings.rb +19 -4
  41. data/lib/anima/version.rb +1 -1
  42. data/lib/events/bounce_back.rb +7 -7
  43. data/lib/events/subscribers/persister.rb +7 -7
  44. data/lib/events/subscribers/subagent_message_router.rb +12 -12
  45. data/lib/events/subscribers/transient_broadcaster.rb +2 -2
  46. data/lib/llm/client.rb +5 -2
  47. data/lib/mneme/compressed_viewport.rb +57 -57
  48. data/lib/mneme/l2_runner.rb +4 -4
  49. data/lib/mneme/passive_recall.rb +2 -2
  50. data/lib/mneme/runner.rb +57 -75
  51. data/lib/mneme/search.rb +55 -38
  52. data/lib/mneme/tools/attach_messages_to_goals.rb +103 -0
  53. data/lib/mneme/tools/everything_ok.rb +1 -3
  54. data/lib/mneme/tools/save_snapshot.rb +12 -16
  55. data/lib/skills/registry.rb +1 -1
  56. data/lib/tools/bash.rb +82 -7
  57. data/lib/tools/edit.rb +4 -6
  58. data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
  59. data/lib/tools/read.rb +4 -4
  60. data/lib/tools/registry.rb +1 -1
  61. data/lib/tools/remember.rb +46 -55
  62. data/lib/tools/spawn_specialist.rb +12 -23
  63. data/lib/tools/spawn_subagent.rb +9 -19
  64. data/lib/tools/subagent_prompts.rb +0 -2
  65. data/lib/tools/think.rb +3 -10
  66. data/lib/tools/web_get.rb +23 -4
  67. data/lib/tools/write.rb +3 -3
  68. data/lib/tui/cable_client.rb +3 -3
  69. data/lib/tui/message_store.rb +37 -37
  70. data/lib/tui/screens/chat.rb +27 -15
  71. data/lib/workflows/registry.rb +1 -1
  72. data/skills/activerecord/SKILL.md +1 -1
  73. data/skills/dragonruby/SKILL.md +1 -1
  74. data/skills/draper-decorators/SKILL.md +1 -1
  75. data/skills/gh-issue.md +1 -1
  76. data/skills/mcp-server/SKILL.md +1 -1
  77. data/skills/ratatui-ruby/SKILL.md +1 -1
  78. data/skills/rspec/SKILL.md +1 -1
  79. data/templates/config.toml +21 -4
  80. data/templates/soul.md +7 -19
  81. data/workflows/create_handoff.md +1 -1
  82. data/workflows/create_note.md +1 -1
  83. data/workflows/create_plan.md +1 -1
  84. data/workflows/implement_plan.md +1 -1
  85. data/workflows/iterate_plan.md +1 -1
  86. data/workflows/research_codebase.md +1 -1
  87. data/workflows/resume_handoff.md +1 -1
  88. data/workflows/review_pr.md +78 -16
  89. data/workflows/thoughts_init.md +1 -1
  90. data/workflows/validate_plan.md +1 -1
  91. metadata +10 -9
  92. data/app/jobs/count_event_tokens_job.rb +0 -39
  93. data/app/models/event.rb +0 -110
  94. data/app/models/goal_pinned_event.rb +0 -11
  95. data/app/models/pinned_event.rb +0 -41
  96. data/lib/mneme/tools/attach_events_to_goals.rb +0 -107
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: resume_handoff
3
- description: "Resume work from a handoff document with context analysis and validation."
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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: review_pr
3
- description: "Multi-agent PR review with three modes: review, re-review, self-review."
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) — if found, fetch full issue details for requirements and acceptance criteria via `gh issue view`
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/` without reading them:
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 | tee /tmp/pr_<NUMBER>_reviews.json | jq length
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 | tee /tmp/pr_<NUMBER>_inline_comments.json | jq length
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 | tee /tmp/pr_<NUMBER>_conversation.json | jq length
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 before proceeding.**
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
- ### Step 4: Spawn Review Subagents
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
- If self-review mode is activated, skip to **Self-review** below.
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
- #### Self-review
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. **Assign reviewer** — Assign the user and request review from anyone mentioned in additional instructions.
281
- 4. **Wait for CI** — Monitor CI status. Once green, mark the PR as ready for review.
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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: thoughts_init
3
- description: "Initialize the thoughts system for the current repository."
3
+ description: "Bootstrap ./thoughts for a new project that doesn't have one yet."
4
4
  ---
5
5
 
6
6
  # Initialize Thoughts System
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: validate_plan
3
- description: "Validate implementation against plan, verify success criteria, and identify issues."
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.1.2
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/event_decorator.rb
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/count_event_tokens_job.rb
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/event/broadcasting.rb
277
- - app/models/event.rb
276
+ - app/models/concerns/message/broadcasting.rb
278
277
  - app/models/goal.rb
279
- - app/models/goal_pinned_event.rb
280
- - app/models/pinned_event.rb
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/attach_events_to_goals.rb
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
@@ -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