samagotchi 0.76.0 → 0.78.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -1
  3. data/docs/cli.md +2 -1
  4. data/docs/configuration.md +10 -6
  5. data/docs/internals/anthropic.md +73 -5
  6. data/docs/internals/engine.md +2 -1
  7. data/docs/internals/prompt-caching.md +3 -1
  8. data/docs/plugins.md +42 -0
  9. data/docs/sessions.md +1 -0
  10. data/lib/samagotchi/answer_command.rb +5 -5
  11. data/lib/samagotchi/bridge.rb +3 -3
  12. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  13. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +12 -10
  14. data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
  15. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  16. data/lib/samagotchi/child_move.rb +2 -2
  17. data/lib/samagotchi/child_reports.rb +3 -3
  18. data/lib/samagotchi/children_status.rb +2 -2
  19. data/lib/samagotchi/engine.rb +21 -12
  20. data/lib/samagotchi/guardrails/parent_approvals.rb +4 -4
  21. data/lib/samagotchi/guardrails/parent_continue.rb +2 -2
  22. data/lib/samagotchi/llm/anthropic_assembly.rb +14 -0
  23. data/lib/samagotchi/llm/anthropic_messages.rb +210 -13
  24. data/lib/samagotchi/llm/anthropic_wire.rb +22 -0
  25. data/lib/samagotchi/llm/chat_loop.rb +49 -2
  26. data/lib/samagotchi/llm/errors.rb +6 -4
  27. data/lib/samagotchi/llm/http.rb +2 -1
  28. data/lib/samagotchi/llm/openai_chat.rb +12 -2
  29. data/lib/samagotchi/memory_bundle/builder.rb +2 -0
  30. data/lib/samagotchi/message_parts.rb +272 -0
  31. data/lib/samagotchi/note_delivery.rb +2 -2
  32. data/lib/samagotchi/parent_report.rb +30 -22
  33. data/lib/samagotchi/pending_question.rb +49 -0
  34. data/lib/samagotchi/plugin/sessions.rb +2 -2
  35. data/lib/samagotchi/question_desk.rb +39 -32
  36. data/lib/samagotchi/relay.rb +8 -9
  37. data/lib/samagotchi/relay_verifier.rb +2 -2
  38. data/lib/samagotchi/relay_watcher.rb +2 -2
  39. data/lib/samagotchi/reply_wait.rb +6 -7
  40. data/lib/samagotchi/row_refs.rb +1 -1
  41. data/lib/samagotchi/session.rb +22 -14
  42. data/lib/samagotchi/session_commands.rb +4 -1
  43. data/lib/samagotchi/session_manager.rb +0 -1
  44. data/lib/samagotchi/session_metrics/totals.rb +4 -3
  45. data/lib/samagotchi/session_metrics.rb +9 -0
  46. data/lib/samagotchi/terminal_ui/attach_launcher.rb +3 -2
  47. data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
  48. data/lib/samagotchi/terminal_ui/formatting.rb +6 -106
  49. data/lib/samagotchi/terminal_ui/model_segments_text.rb +118 -0
  50. data/lib/samagotchi/terminal_ui.rb +7 -5
  51. data/lib/samagotchi/tool_output_cut.rb +1 -1
  52. data/lib/samagotchi/tool_response.rb +13 -13
  53. data/lib/samagotchi/tool_row_fields.rb +1 -1
  54. data/lib/samagotchi/tool_runner.rb +16 -9
  55. data/lib/samagotchi/tools/delegate.rb +2 -2
  56. data/lib/samagotchi/tools/delegate_relay.rb +7 -7
  57. data/lib/samagotchi/tools/delegate_result.rb +2 -2
  58. data/lib/samagotchi/tools/delegate_wait.rb +1 -1
  59. data/lib/samagotchi/tools/list_sessions.rb +2 -2
  60. data/lib/samagotchi/tools/web_fetch.rb +3 -1
  61. data/lib/samagotchi/version.rb +1 -1
  62. data/lib/samagotchi/web/app.rb +2 -2
  63. data/lib/samagotchi/web/public/app.js +234 -74
  64. data/lib/samagotchi/web/public/composer_fold.js +58 -0
  65. data/lib/samagotchi/web/public/composer_size.js +10 -0
  66. data/lib/samagotchi/web/public/ctx.js +12 -1
  67. data/lib/samagotchi/web/public/index.html +29 -4
  68. data/lib/samagotchi/web/public/markdown_place.js +53 -0
  69. data/lib/samagotchi/web/public/stream_handlers.js +7 -3
  70. data/lib/samagotchi/web/public/toasts.js +69 -0
  71. data/lib/samagotchi/worker.rb +1 -0
  72. metadata +7 -2
  73. data/lib/samagotchi/web/message_parts.rb +0 -274
@@ -42,8 +42,8 @@ require_relative "children_status"
42
42
  require_relative "log"
43
43
 
44
44
  module Samagotchi
45
- # Loaded on first use: session_manager requires this file (its worker
46
- # process loads the whole UI stack through it), a require cycle otherwise.
45
+ # Loaded on first use: this autoload keeps SessionManager and its ~80
46
+ # files unloaded until the delegate/session path needs them.
47
47
  autoload :SessionManager, File.expand_path("session_manager", __dir__)
48
48
 
49
49
  # The in-process terminal front end (`chi --no-shared`, `chi scratch`,
@@ -873,6 +873,7 @@ module Samagotchi
873
873
  end
874
874
  if command
875
875
  show_command_result(command)
876
+ refresh_status_row(clear_unknown_context: true) if command.changed.include?(:model)
876
877
  persist_recent_history(input) if command.shell
877
878
  return
878
879
  end
@@ -1400,9 +1401,10 @@ module Samagotchi
1400
1401
 
1401
1402
  # The status row under the prompt, from the Engine's state (what a
1402
1403
  # worker hands an attached TUI when it joins): drawn when it changed.
1403
- def refresh_status_row
1404
+ def refresh_status_row(clear_unknown_context: false)
1404
1405
  state = @engine.session_state_snapshot.merge(model_name: @effective_model_name)
1405
1406
  @status_row.take_state(state, default_model: @default_model_name)
1407
+ @status_row.update(context: nil) if clear_unknown_context && state.key?(:context_status) && state[:context_status].nil?
1406
1408
  refresh_children_status
1407
1409
  end
1408
1410
 
@@ -1463,13 +1465,13 @@ module Samagotchi
1463
1465
  def drain_pending_question?
1464
1466
  pq = @pending_question_event
1465
1467
  # Also check Engine's persisted pending (covers resume)
1466
- pq ||= @engine.pending_question
1468
+ pq ||= @engine.pending_question&.to_h
1467
1469
  return false unless pq
1468
1470
 
1469
1471
  # If the pending no longer matches the engine's current pending (stale
1470
1472
  # event after sync handler already answered and cleared), discard.
1471
1473
  current = @engine.pending_question
1472
- if current && (pq[:id] || pq["id"]).to_s != (current[:id] || current["id"]).to_s
1474
+ if current && (pq[:id] || pq["id"]).to_s != current.id.to_s
1473
1475
  @pending_question_event = nil
1474
1476
  return false
1475
1477
  end
@@ -38,7 +38,7 @@ module Samagotchi
38
38
  HEAD_CUT = /\[cut: \d+ of \d+ chars; read it in parts\]\z/
39
39
 
40
40
  # What a web row keeps of a tool's output, on a reload
41
- # (Web::MessageParts) and a mid-turn join (Bridge::TurnAccumulator): the
41
+ # (MessageParts) and a mid-turn join (Bridge::TurnAccumulator): the
42
42
  # row shows 300 chars and the rest on hover. A longer one keeps its head
43
43
  # and tail (preview).
44
44
  WEB_PREVIEW_CHARS = 2000
@@ -29,7 +29,7 @@ module Samagotchi
29
29
  # Runs +calls+ in order (ToolRunner#run each), yielding each run and its
30
30
  # index as it finishes. +emit+ takes the dispatch events, the runner's
31
31
  # own go to +on_stream_event+.
32
- # @return [Array<Hash>] the runs
32
+ # @return [Array<ToolRun>] the runs
33
33
  def run_batch(runner, calls, iteration:, emit:, on_stream_event:, cap:)
34
34
  emit.call({ type: :tool_dispatch_started, iteration: iteration, call_count: calls.length })
35
35
  runs = calls.each_with_index.map do |call, index|
@@ -45,7 +45,7 @@ module Samagotchi
45
45
  # The runs' activity, for the turn's tool list (none for a call whose
46
46
  # dispatcher raised).
47
47
  def activities(runs)
48
- runs.filter_map { |run| run[:activity] }
48
+ runs.filter_map(&:activity)
49
49
  end
50
50
 
51
51
  # The native loop's entry for a batch: the capped outputs joined, every image
@@ -53,14 +53,14 @@ module Samagotchi
53
53
  # each on its own tool row), and the saved-only fields one per call:
54
54
  # +ids+ (ToolIds.next_ids) as tool_ids.
55
55
  def joined(runs, ids:)
56
- images = runs.flat_map { |run| Array(run[:images]) }
57
- entry = { role: "tool_response", content: runs.map { |run| run[:capped_output] }.join(SEPARATOR) }
56
+ images = runs.flat_map { |run| Array(run.images) }
57
+ entry = { role: "tool_response", content: runs.map(&:capped_output).join(SEPARATOR) }
58
58
  unless images.empty?
59
59
  entry[:images] = images
60
- entry[:image_counts] = runs.map { |run| Array(run[:images]).size }
60
+ entry[:image_counts] = runs.map { |run| Array(run.images).size }
61
61
  end
62
- saved = { tool_params: runs.map { |run| run[:shown_params] }, tool_labels: runs.map { |run| run[:shown_label] },
63
- tool_diffs: runs.map { |run| run[:diff] }, tool_refs: runs.map { |run| run[:refs] } }
62
+ saved = { tool_params: runs.map(&:shown_params), tool_labels: runs.map(&:shown_label),
63
+ tool_diffs: runs.map(&:diff), tool_refs: runs.map(&:refs) }
64
64
  saved.each { |key, values| entry[key] = values if values.any? }
65
65
  entry[:tool_ids] = ids
66
66
  entry
@@ -70,12 +70,12 @@ module Samagotchi
70
70
  # call by +tool_call_id+, and its chi-owned id (a one-id +ids+, as
71
71
  # #joined's tool_ids).
72
72
  def single(run, tool_call_id:, ids:)
73
- entry = { role: "tool_response", content: run[:capped_output], tool_call_id: tool_call_id }
74
- entry[:images] = run[:images] if run[:images]&.any?
75
- entry[:tool_params] = run[:shown_params] if run[:shown_params]
76
- entry[:tool_labels] = run[:shown_label] if run[:shown_label]
77
- entry[:tool_diffs] = run[:diff] if run[:diff]
78
- entry[:tool_refs] = run[:refs] if run[:refs]
73
+ entry = { role: "tool_response", content: run.capped_output, tool_call_id: tool_call_id }
74
+ entry[:images] = run.images if run.images&.any?
75
+ entry[:tool_params] = run.shown_params if run.shown_params
76
+ entry[:tool_labels] = run.shown_label if run.shown_label
77
+ entry[:tool_diffs] = run.diff if run.diff
78
+ entry[:tool_refs] = run.refs if run.refs
79
79
  entry[:tool_ids] = ids
80
80
  entry
81
81
  end
@@ -7,7 +7,7 @@ require_relative "file_ref"
7
7
  module Samagotchi
8
8
  # The fields a tool row shows beyond its params, built from the call in
9
9
  # one place for every path: the live tool_call_started (ToolRunner#run)
10
- # and a reload from the saved call (Web::MessageParts#tool_part). The
10
+ # and a reload from the saved call (MessageParts#tool_part). The
11
11
  # bridge's snapshot and replay (Bridge::TurnAccumulator) copy them by
12
12
  # these lists, so a new field is added here and travels everywhere.
13
13
  #
@@ -15,6 +15,17 @@ require_relative "memory_bundle/index_sync"
15
15
  require_relative "memory_bundle/index_size"
16
16
 
17
17
  module Samagotchi
18
+ # One call's result from ToolRunner#run, read by ToolResponse (the loops'
19
+ # tool_response entries) and the loops' activity list. Fields a call didn't
20
+ # produce (images, shown_params, shown_label, refs, diff, output_file) are nil.
21
+ ToolRun = Data.define(:output, :capped_output, :truncated, :activity, :images, :shown_params, :shown_label, :refs, :diff,
22
+ :output_file) do
23
+ def initialize(output: nil, capped_output: nil, truncated: nil, activity: nil, images: nil, shown_params: nil,
24
+ shown_label: nil, refs: nil, diff: nil, output_file: nil)
25
+ super
26
+ end
27
+ end
28
+
18
29
  # The single per-call path both loops use: the tool_call_started and
19
30
  # tool_call_completed events, the guardrail gate (before_tool_call hooks
20
31
  # and their veto), dispatch through KernelLoop, the after_tool_call hook
@@ -40,7 +51,7 @@ module Samagotchi
40
51
  end
41
52
 
42
53
  # @param call_index [Integer] 1-based position of the call in its batch
43
- # @return [Hash] output:, capped_output:, truncated:, activity:,
54
+ # @return [ToolRun] output:, capped_output:, truncated:, activity:,
44
55
  # output_file: (the saved whole output of a cut execute result),
45
56
  # images: (refs) when the tool returned images the model gets to see, and
46
57
  # shown_params: the params line the live row showed, and shown_label:
@@ -135,14 +146,10 @@ module Samagotchi
135
146
  fire(:after_tool_call, { type: :after_tool_call, iteration: iteration, tool: call[:name], output: capped,
136
147
  status: status || "error", call: call.dup, elapsed_ms: elapsed_ms, result: tool_text })
137
148
 
138
- run = { output: output, capped_output: capped, truncated: truncated, activity: result[:activity] }
139
- run[:images] = images if images&.any?
140
- run[:shown_params] = params if params && plugin_tool?(call[:name])
141
- run[:shown_label] = label if label
142
- run[:refs] = refs unless refs.empty?
143
- run[:diff] = diff if diff
144
- run[:output_file] = output_file if output_file
145
- run
149
+ ToolRun.new(output: output, capped_output: capped, truncated: truncated, activity: result[:activity],
150
+ images: images&.any? ? images : nil,
151
+ shown_params: params && plugin_tool?(call[:name]) ? params : nil,
152
+ shown_label: label, refs: refs.empty? ? nil : refs, diff: diff, output_file: output_file)
146
153
  end
147
154
 
148
155
  private
@@ -10,8 +10,8 @@ require_relative "delegate_wait"
10
10
  require_relative "../child_reports"
11
11
 
12
12
  module Samagotchi
13
- # Loaded on first use: session_manager requires terminal_ui, which
14
- # requires KernelLoop and so these tools (a require cycle otherwise).
13
+ # Loaded on first use: this autoload keeps SessionManager and its ~80
14
+ # files unloaded until the delegate/session path needs them.
15
15
  autoload :SessionManager, File.expand_path("../session_manager", __dir__)
16
16
 
17
17
  module Tools
@@ -37,7 +37,7 @@ module Samagotchi
37
37
  # @param relay [#open_question, #relay_desk] Peers#relay
38
38
  # @return [Boolean] a relay can take this question
39
39
  def relayable?(relay, question)
40
- !relay.nil? && question.is_a?(Hash) && question[:kind].to_s == Guardrails::Approval::KIND
40
+ !relay.nil? && question.is_a?(PendingQuestion) && question.kind.to_s == Guardrails::Approval::KIND
41
41
  end
42
42
 
43
43
  # Relay the child's pending approval and wait until it settles.
@@ -45,7 +45,7 @@ module Samagotchi
45
45
  # @return [Outcome]
46
46
  def call(child_id, question, relay:, state_dir:, more: 0)
47
47
  child = Session.load(child_id, state_dir: state_dir)
48
- qid = question[:id].to_s
48
+ qid = question.id.to_s
49
49
  desk = relay.relay_desk
50
50
  relay_id = desk.open(child_id: child_id, child_question_id: qid)
51
51
  client = BridgeClient.discover(child_id, session_dir: Session.session_dir(child_id, state_dir: state_dir))
@@ -70,7 +70,7 @@ module Samagotchi
70
70
  end
71
71
 
72
72
  pending = Session.load(child_id, state_dir: state_dir).pending_question
73
- pending && pending[:id].to_s == qid ? nil : "answered_on_child"
73
+ pending && pending.id.to_s == qid ? nil : "answered_on_child"
74
74
  rescue ArgumentError
75
75
  "child_gone"
76
76
  end
@@ -175,7 +175,7 @@ module Samagotchi
175
175
  return if waiting.empty?
176
176
 
177
177
  child_id, question = waiting.first
178
- @done[question[:id].to_s] = true
178
+ @done[question.id.to_s] = true
179
179
  outcome = DelegateRelay.call(child_id, question, relay: @relay, state_dir: @state_dir, more: waiting.size - 1)
180
180
  @on_outcome.call(child_id, outcome)
181
181
  # The next one, if any, right after.
@@ -186,7 +186,7 @@ module Samagotchi
186
186
  # @return [Integer]
187
187
  def count = waiting.size
188
188
 
189
- # @return [Array<Array(String, Hash)>] [child id, pending question],
189
+ # @return [Array<Array(String, PendingQuestion)>] [child id, pending question],
190
190
  # oldest question first
191
191
  def waiting
192
192
  rows = SessionManager.children_of(@parent_id, state_dir: @state_dir).select do |row|
@@ -194,10 +194,10 @@ module Samagotchi
194
194
  end
195
195
  rows.filter_map do |row|
196
196
  pending = Session.load(row[:id], state_dir: @state_dir).pending_question
197
- [row[:id], pending] if pending && pending[:id].to_s == row[:waiting_id].to_s
197
+ [row[:id], pending] if pending && pending.id.to_s == row[:waiting_id].to_s
198
198
  rescue ArgumentError
199
199
  nil
200
- end.sort_by { |_, pending| pending[:created_at].to_s }
200
+ end.sort_by { |_, pending| pending.created_at.to_s }
201
201
  end
202
202
 
203
203
  private
@@ -6,8 +6,8 @@ require_relative "delegate"
6
6
  require_relative "delegate_wait"
7
7
 
8
8
  module Samagotchi
9
- # Loaded on first use: session_manager requires terminal_ui, which
10
- # requires KernelLoop and so these tools (a require cycle otherwise).
9
+ # Loaded on first use: this autoload keeps SessionManager and its ~80
10
+ # files unloaded until the delegate/session path needs them.
11
11
  autoload :SessionManager, File.expand_path("../session_manager", __dir__)
12
12
 
13
13
  module Tools
@@ -91,7 +91,7 @@ module Samagotchi
91
91
  # asked), the relayed question no longer reported: a turn that
92
92
  # ends with no reply during the relay's POST or after it still
93
93
  # ends the wait.
94
- baseline = ReplyWait.baseline_of(wait.session, question_id: wait.question[:id])
94
+ baseline = ReplyWait.baseline_of(wait.session, question_id: wait.question.id)
95
95
  end
96
96
  rescue ArgumentError => e
97
97
  "Error: #{e.message}"
@@ -6,8 +6,8 @@ require_relative "../session"
6
6
  require_relative "peers"
7
7
 
8
8
  module Samagotchi
9
- # Loaded on first use: session_manager requires terminal_ui, which
10
- # requires KernelLoop and so these tools (a require cycle otherwise).
9
+ # Loaded on first use: this autoload keeps SessionManager and its ~80
10
+ # files unloaded until the delegate/session path needs them.
11
11
  autoload :SessionManager, File.expand_path("../session_manager", __dir__)
12
12
 
13
13
  module Tools
@@ -3,7 +3,6 @@
3
3
  require "net/http"
4
4
  require "uri"
5
5
  require "ipaddr"
6
- require "nokogiri"
7
6
  require_relative "../version"
8
7
 
9
8
  module Samagotchi
@@ -128,6 +127,9 @@ module Samagotchi
128
127
  def self.extract_text_from_html(html)
129
128
  return "Error: empty HTML response" if html.nil? || html.empty?
130
129
 
130
+ # Load lazily so workers don't pay for Nokogiri on every start.
131
+ require "nokogiri"
132
+
131
133
  doc = Nokogiri::HTML(html)
132
134
 
133
135
  # Extract noscript text before removing it
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.76.0"
4
+ VERSION = "0.78.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
@@ -43,7 +43,7 @@ require_relative "../recap_store"
43
43
  require_relative "markdown_renderer"
44
44
  require_relative "capabilities"
45
45
  require_relative "editor"
46
- require_relative "message_parts"
46
+ require_relative "../message_parts"
47
47
  require_relative "session_summary"
48
48
  require_relative "../log"
49
49
  require_relative "../paths"
@@ -855,7 +855,7 @@ module Samagotchi
855
855
  current_turn = turn_snapshot["current_turn"]
856
856
  current_turn ? current_turn["pending_question"] : turn_snapshot["pending_question"]
857
857
  elsif owner&.worker?
858
- session.pending_question
858
+ session.pending_question&.to_h
859
859
  end
860
860
  end
861
861