samagotchi 0.108.0 → 0.110.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 +66 -1
  3. data/bin/chi +5 -1
  4. data/docs/architecture.md +7 -2
  5. data/docs/cli.md +40 -1
  6. data/docs/configuration.md +177 -2
  7. data/docs/internals/engine.md +19 -4
  8. data/docs/internals/participants.md +28 -8
  9. data/docs/internals/prompt-caching.md +5 -0
  10. data/docs/internals/remote-sessions.md +6 -2
  11. data/docs/plugins.md +1 -1
  12. data/docs/sessions.md +3 -1
  13. data/docs/sub-agent.md +1 -1
  14. data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
  15. data/lib/samagotchi/bridge.rb +25 -0
  16. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  17. data/lib/samagotchi/config.rb +4 -0
  18. data/lib/samagotchi/context_note.rb +7 -3
  19. data/lib/samagotchi/engine.rb +20 -9
  20. data/lib/samagotchi/idle_recap.rb +1 -1
  21. data/lib/samagotchi/idle_reminders.rb +1 -1
  22. data/lib/samagotchi/kernel_loop.rb +12 -7
  23. data/lib/samagotchi/llm/chat_loop.rb +9 -2
  24. data/lib/samagotchi/message_time.rb +104 -0
  25. data/lib/samagotchi/proxy_trace.rb +45 -0
  26. data/lib/samagotchi/reminder_queue.rb +20 -3
  27. data/lib/samagotchi/reply_wait/poll.rb +39 -1
  28. data/lib/samagotchi/run_options.rb +4 -1
  29. data/lib/samagotchi/session_access/contract.rb +1 -1
  30. data/lib/samagotchi/session_access/local.rb +52 -10
  31. data/lib/samagotchi/session_inbox.rb +7 -3
  32. data/lib/samagotchi/steer.rb +5 -3
  33. data/lib/samagotchi/terminal_ui/attached_loop.rb +70 -9
  34. data/lib/samagotchi/terminal_ui/between_turns.rb +8 -3
  35. data/lib/samagotchi/terminal_ui/formatting.rb +13 -5
  36. data/lib/samagotchi/terminal_ui/gap_clock.rb +78 -0
  37. data/lib/samagotchi/terminal_ui/reline_seam.rb +20 -2
  38. data/lib/samagotchi/terminal_ui.rb +40 -5
  39. data/lib/samagotchi/tool_activity.rb +11 -0
  40. data/lib/samagotchi/turn_checkpoint.rb +6 -1
  41. data/lib/samagotchi/version.rb +1 -1
  42. data/lib/samagotchi/web/app.rb +246 -32
  43. data/lib/samagotchi/web/grants.rb +6 -3
  44. data/lib/samagotchi/web/info_probe.rb +5 -1
  45. data/lib/samagotchi/web/link_codes.rb +146 -0
  46. data/lib/samagotchi/web/participant_access.rb +9 -2
  47. data/lib/samagotchi/web/participant_review.rb +1 -1
  48. data/lib/samagotchi/web/participant_view.rb +12 -5
  49. data/lib/samagotchi/web/participants_admin.rb +13 -6
  50. data/lib/samagotchi/web/proxy_gate.rb +174 -0
  51. data/lib/samagotchi/web/public/app.js +35 -3
  52. data/lib/samagotchi/web/public/auth_lost.js +76 -0
  53. data/lib/samagotchi/web/public/bubbles.js +29 -7
  54. data/lib/samagotchi/web/public/data.js +29 -5
  55. data/lib/samagotchi/web/public/format.js +72 -6
  56. data/lib/samagotchi/web/public/index.html +2 -0
  57. data/lib/samagotchi/web/public/participant.html +2 -0
  58. data/lib/samagotchi/web/public/participant.js +47 -10
  59. data/lib/samagotchi/web/public/participant_model.js +25 -2
  60. data/lib/samagotchi/web/public/stream_handlers.js +32 -4
  61. data/lib/samagotchi/web/public/time_gaps.js +98 -0
  62. data/lib/samagotchi/web/public/token_fragment.js +57 -0
  63. data/lib/samagotchi/web/public/tokens.css +2 -2
  64. data/lib/samagotchi/web/public/turn_events.js +3 -1
  65. data/lib/samagotchi/web/public/turn_html.js +15 -8
  66. data/lib/samagotchi/web/public/turn_view.js +8 -3
  67. data/lib/samagotchi/web/public_url.rb +87 -0
  68. data/lib/samagotchi/web/running.rb +39 -4
  69. data/lib/samagotchi/web/server.rb +138 -22
  70. data/lib/samagotchi/web/tunnel/tailscale.rb +325 -0
  71. data/lib/samagotchi/web/tunnel.rb +133 -0
  72. data/lib/samagotchi/worker_sidecar.rb +4 -3
  73. metadata +12 -1
@@ -30,6 +30,7 @@ require_relative "terminal_ui/attached_view"
30
30
  require_relative "terminal_ui/between_turns"
31
31
  require_relative "terminal_ui/event_renderer"
32
32
  require_relative "terminal_ui/formatting"
33
+ require_relative "terminal_ui/gap_clock"
33
34
  require_relative "terminal_ui/input_support"
34
35
  require_relative "terminal_ui/image_input"
35
36
  require_relative "terminal_ui/plain_surface"
@@ -114,7 +115,8 @@ module Samagotchi
114
115
  # +toolkit_flags+ then)
115
116
  def initialize(prompt: nil, client: nil, host_registry: nil, profile: nil, session_id: nil, no_interrupt: false, no_default_input: false, model_name: nil, memories: [], muted_memories: [], non_interactive: false, surface: nil,
116
117
  spinner_tick_interval: AttachedView::TICK_INTERVAL, spinner_clock: nil, scratch: false, llm_context: {}, thinking: nil,
117
- appended_prompt_files: [], toolkit: nil, toolkit_flags: [], settings: nil, parent_answers: nil)
118
+ appended_prompt_files: [], toolkit: nil, toolkit_flags: [], settings: nil, parent_answers: nil,
119
+ now: -> { MessageTime.now })
118
120
  @scratch = scratch
119
121
  @parent_answers = parent_answers
120
122
  @toolkit = toolkit
@@ -190,8 +192,12 @@ module Samagotchi
190
192
  @renderer = EventRenderer.new(@view)
191
193
  # Cards, notices and a recap announced between turns, printed by the
192
194
  # main thread at the open prompt (#poll_input_with_reminder_check).
195
+ # The session's last activity, for the gap dividers (+now+: the wall
196
+ # clock, a Time).
197
+ @gaps = GapClock.new(now: now)
193
198
  @between_turns = BetweenTurns.new(surface: @surface, view: @view, renderer: @renderer,
194
- turn_running: -> { @engine.turn_running? }, quiet: @non_interactive)
199
+ turn_running: -> { @engine.turn_running? }, quiet: @non_interactive,
200
+ gaps: @gaps)
195
201
  @render_event = ->(event) { handle_stream_event(event) }
196
202
  @engine = Engine.new(
197
203
  client: client,
@@ -213,7 +219,7 @@ module Samagotchi
213
219
  # When a reminder is due, queue a synthetic turn that
214
220
  # run_assist_loop checks before read_input. The synthetic turn
215
221
  # uses an empty prompt so the agent can see
216
- # [SYSTEM: REMINDERS DUE] and act on them.
222
+ # [SYSTEM: REMINDERS DUE <local time>] and act on them.
217
223
  @engine.note_due_reminders(due_names)
218
224
  }
219
225
  }
@@ -593,7 +599,9 @@ module Samagotchi
593
599
  messages = ContextNote.with_system_head(session.messages.dup, system_message)
594
600
  banner("Resumed session: #{session.id}")
595
601
  saved = @engine.saved_recap
596
- banner(recap_block(saved[:text], turns_since: saved[:turns_since])) if saved
602
+ banner(recap_block(saved[:text], turns_since: saved[:turns_since], at: saved[:created_at])) if saved
603
+ # The first prompt after a quiet gap has its divider.
604
+ @gaps.seed(session.messages, fallback: saved&.dig(:created_at))
597
605
  elsif @scratch
598
606
  messages = [system_message]
599
607
  banner("Scratch session: nothing is kept, it is deleted when you leave.")
@@ -721,6 +729,8 @@ module Samagotchi
721
729
  ensure
722
730
  @active_cancel_controller = nil
723
731
  @view.finish_thinking_spinner
732
+ # The turn's end is the last activity.
733
+ @gaps.touch
724
734
  end
725
735
 
726
736
  # How the REPL leaves its session once the loop ended (#run): :scratch
@@ -909,6 +919,9 @@ module Samagotchi
909
919
 
910
920
  @turn_flow.before_prompt_turn
911
921
  persist_recent_history(input)
922
+ # Off a live region (no echo seam) the divider comes here; after
923
+ # #gap_above_echo it marks none.
924
+ mark_activity
912
925
  begin
913
926
  # Engine#run_turn injects due reminders as a tail message, appends
914
927
  # the prompt, and renders through @renderer via on_event.
@@ -1051,6 +1064,7 @@ module Samagotchi
1051
1064
  sync_continue_slot(false)
1052
1065
  @surface.commit(QuestionSlot.continue_summary("(dropped: a reminder ran)", paint: method(:paint)))
1053
1066
  end
1067
+ mark_activity
1054
1068
  @surface.commit(reminder_line(names))
1055
1069
  hint = "#{reminder_line(names)} · Ctrl-C cancels it"
1056
1070
  @surface.set_slot(:hints, [color_output? ? paint(hint, 90) : hint])
@@ -1147,13 +1161,34 @@ module Samagotchi
1147
1161
  # a question's ? prompt. +prefill+ is typed in first.
1148
1162
  def read_repl_line(prompt, prefill)
1149
1163
  queue_input_prefill(prefill) if prefill
1150
- return read_prompt_line(prompt) if prompt == paint("> ", 92)
1164
+ return RelineSeam.echo_above(method(:gap_above_echo)) { read_prompt_line(prompt) } if prompt == paint("> ", 92)
1151
1165
 
1152
1166
  read = -> { with_next_input_prefill { Reline.readline(prompt, true) }&.strip }
1153
1167
  # A question's answer leaves only its summary line.
1154
1168
  prompt == paint(QUESTION_PROMPT, 33) ? RelineSeam.without_echo(&read) : read.call
1155
1169
  end
1156
1170
 
1171
+ # Activity starts now: a gap's divider first when one passed since the
1172
+ # last (none while a turn runs: a steer is no gap; none in a
1173
+ # --non-interactive run, whose stdout is the answer's alone).
1174
+ def mark_activity
1175
+ line = @gaps.divider(quiet: @non_interactive || @engine.turn_running?)
1176
+ @surface.commit(gap_divider(line)) if line
1177
+ end
1178
+
1179
+ # The divider above a typed line's echo (RelineSeam.echo_above, on the
1180
+ # reader thread): a prompt typed at the idle prompt after a quiet gap.
1181
+ # It marks the activity, so #run_input_line marks none.
1182
+ def gap_above_echo(text)
1183
+ return nil if @engine.turn_running? || @turn_flow.awaiting_continue?
1184
+
1185
+ route = ComposerRoute.call(text.to_s.strip, running: false, registry: command_registry)
1186
+ return nil unless %i[prompt deliver].include?(route.kind)
1187
+
1188
+ line = @gaps.divider
1189
+ line && gap_divider(line)
1190
+ end
1191
+
1157
1192
  # Lines come from a pipe or a file (`chi -p X </dev/null`, `echo … | chi`),
1158
1193
  # not a terminal: no prompt is restored for a retry.
1159
1194
  def piped_input? = !$stdin.tty?
@@ -43,6 +43,17 @@ module Samagotchi
43
43
  # Without a "[tool]" lead (an older or hand-made output) it stays as it is.
44
44
  def saved_result_text(output) = output.to_s.sub(SAVED_LEAD, "")
45
45
 
46
+ # A call the guardrails denied, as its saved text reads after the
47
+ # "[tool]" lead (ToolRunner#denied, Guardrails::Verdict#deny_text): a
48
+ # rule's or hook's deny, a legacy hook veto, or the user declining (or
49
+ # cancelling) the approval.
50
+ GUARDRAIL_DENIED = /\AError: (?:(?:denied|blocked) by guardrail\b|The (?:user declined this call|approval was cancelled)\b.*? It needed approval \()/m
51
+
52
+ # Whether a saved result text (#saved_result_text) is a guardrail's
53
+ # deny: "blocked" as live (its activity's status), not "error", for a
54
+ # call with no saved tool record.
55
+ def guardrail_denied?(text) = text.to_s.scrub.match?(GUARDRAIL_DENIED)
56
+
46
57
  # @param registry [Tools::Registry, nil] the session's tools
47
58
  # @param cwd [String, nil] what a file tool's title is relative to (the
48
59
  # worker runs in its session's working directory)
@@ -7,6 +7,7 @@ require_relative "atomic_file"
7
7
  require_relative "interrupted_turn"
8
8
  require_relative "last_turn"
9
9
  require_relative "log"
10
+ require_relative "message_time"
10
11
  require_relative "session"
11
12
  require_relative "session_inbox"
12
13
  require_relative "steer"
@@ -181,7 +182,9 @@ module Samagotchi
181
182
  @written_at = nil
182
183
  @started_at = Time.now.iso8601(3)
183
184
  messages = Array(@session.messages).dup
184
- messages << { role: "user", content: prompt, **author } unless prompt.to_s.empty?
185
+ # Stamped by the wall clock here, a moment before the turn's own
186
+ # stamp (Engine's Turn#wall), which the turn's first boundary saves.
187
+ messages << { role: "user", content: prompt, **author, MessageTime::KEY => MessageTime.iso(MessageTime.now) } unless prompt.to_s.empty?
185
188
  @session.turn_checkpoint = { messages: messages, marker: InterruptedTurn.new(started_at: @started_at, at: @started_at, iteration: 0,
186
189
  enqueued_id: @enqueued_id) }
187
190
  end
@@ -202,6 +205,8 @@ module Samagotchi
202
205
  return false unless conversation
203
206
 
204
207
  @boundaries += 1
208
+ # The merge's stamps are this copy's own; Steer.inject! stamps the
209
+ # live merge a moment later.
205
210
  merged = merging.empty? ? [] : Steer.merge(merging).messages
206
211
  # One level deep: the loop may edit an entry in place later (a
207
212
  # forget stub) while a save on another thread serializes this one.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.108.0"
4
+ VERSION = "0.110.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end