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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -1
- data/bin/chi +5 -1
- data/docs/architecture.md +7 -2
- data/docs/cli.md +40 -1
- data/docs/configuration.md +177 -2
- data/docs/internals/engine.md +19 -4
- data/docs/internals/participants.md +28 -8
- data/docs/internals/prompt-caching.md +5 -0
- data/docs/internals/remote-sessions.md +6 -2
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +3 -1
- data/docs/sub-agent.md +1 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +25 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +4 -0
- data/lib/samagotchi/context_note.rb +7 -3
- data/lib/samagotchi/engine.rb +20 -9
- data/lib/samagotchi/idle_recap.rb +1 -1
- data/lib/samagotchi/idle_reminders.rb +1 -1
- data/lib/samagotchi/kernel_loop.rb +12 -7
- data/lib/samagotchi/llm/chat_loop.rb +9 -2
- data/lib/samagotchi/message_time.rb +104 -0
- data/lib/samagotchi/proxy_trace.rb +45 -0
- data/lib/samagotchi/reminder_queue.rb +20 -3
- data/lib/samagotchi/reply_wait/poll.rb +39 -1
- data/lib/samagotchi/run_options.rb +4 -1
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +52 -10
- data/lib/samagotchi/session_inbox.rb +7 -3
- data/lib/samagotchi/steer.rb +5 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +70 -9
- data/lib/samagotchi/terminal_ui/between_turns.rb +8 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +13 -5
- data/lib/samagotchi/terminal_ui/gap_clock.rb +78 -0
- data/lib/samagotchi/terminal_ui/reline_seam.rb +20 -2
- data/lib/samagotchi/terminal_ui.rb +40 -5
- data/lib/samagotchi/tool_activity.rb +11 -0
- data/lib/samagotchi/turn_checkpoint.rb +6 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +246 -32
- data/lib/samagotchi/web/grants.rb +6 -3
- data/lib/samagotchi/web/info_probe.rb +5 -1
- data/lib/samagotchi/web/link_codes.rb +146 -0
- data/lib/samagotchi/web/participant_access.rb +9 -2
- data/lib/samagotchi/web/participant_review.rb +1 -1
- data/lib/samagotchi/web/participant_view.rb +12 -5
- data/lib/samagotchi/web/participants_admin.rb +13 -6
- data/lib/samagotchi/web/proxy_gate.rb +174 -0
- data/lib/samagotchi/web/public/app.js +35 -3
- data/lib/samagotchi/web/public/auth_lost.js +76 -0
- data/lib/samagotchi/web/public/bubbles.js +29 -7
- data/lib/samagotchi/web/public/data.js +29 -5
- data/lib/samagotchi/web/public/format.js +72 -6
- data/lib/samagotchi/web/public/index.html +2 -0
- data/lib/samagotchi/web/public/participant.html +2 -0
- data/lib/samagotchi/web/public/participant.js +47 -10
- data/lib/samagotchi/web/public/participant_model.js +25 -2
- data/lib/samagotchi/web/public/stream_handlers.js +32 -4
- data/lib/samagotchi/web/public/time_gaps.js +98 -0
- data/lib/samagotchi/web/public/token_fragment.js +57 -0
- data/lib/samagotchi/web/public/tokens.css +2 -2
- data/lib/samagotchi/web/public/turn_events.js +3 -1
- data/lib/samagotchi/web/public/turn_html.js +15 -8
- data/lib/samagotchi/web/public/turn_view.js +8 -3
- data/lib/samagotchi/web/public_url.rb +87 -0
- data/lib/samagotchi/web/running.rb +39 -4
- data/lib/samagotchi/web/server.rb +138 -22
- data/lib/samagotchi/web/tunnel/tailscale.rb +325 -0
- data/lib/samagotchi/web/tunnel.rb +133 -0
- data/lib/samagotchi/worker_sidecar.rb +4 -3
- 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
|
-
|
|
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.
|