samagotchi 0.2.0 → 0.4.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 +198 -1
- data/README.md +56 -4
- data/bin/chi +118 -50
- data/docs/cli.md +184 -9
- data/docs/configuration.md +333 -47
- data/docs/desktop.md +45 -4
- data/docs/guardrails.md +11 -0
- data/docs/hooks.md +208 -5
- data/docs/plugins.md +68 -2
- data/docs/releasing.md +23 -13
- data/docs/sessions.md +45 -17
- data/lib/samagotchi/answer_display.rb +95 -0
- data/lib/samagotchi/archive_store.rb +90 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
- data/lib/samagotchi/bootstrap/probe.rb +262 -0
- data/lib/samagotchi/bootstrap_command.rb +347 -0
- data/lib/samagotchi/bridge/pending_card.rb +89 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +15 -3
- data/lib/samagotchi/bridge.rb +13 -1
- data/lib/samagotchi/bridge_client.rb +6 -2
- data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
- data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +531 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
- data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
- data/lib/samagotchi/bundles/system/delegated.md +6 -7
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/self_map.md +8 -2
- data/lib/samagotchi/client.rb +81 -19
- data/lib/samagotchi/commands/registry.rb +8 -0
- data/lib/samagotchi/config.rb +252 -48
- data/lib/samagotchi/desktop/macos/App.swift +12 -8
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +17 -9
- data/lib/samagotchi/desktop/macos/Images.swift +113 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +180 -25
- data/lib/samagotchi/desktop/macos.rb +59 -8
- data/lib/samagotchi/desktop_command.rb +6 -3
- data/lib/samagotchi/edit_preview.rb +82 -0
- data/lib/samagotchi/empty_answer_retry.rb +43 -0
- data/lib/samagotchi/engine.rb +434 -140
- data/lib/samagotchi/gem_update.rb +89 -0
- data/lib/samagotchi/guardrails/approval.rb +35 -4
- data/lib/samagotchi/guardrails/load_failures.rb +9 -3
- data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +24 -5
- data/lib/samagotchi/host_registry.rb +9 -12
- data/lib/samagotchi/idle_client.rb +24 -15
- data/lib/samagotchi/idle_recap.rb +5 -1
- data/lib/samagotchi/idle_reminders.rb +2 -2
- data/lib/samagotchi/image_store.rb +10 -6
- data/lib/samagotchi/kernel_loop.rb +73 -94
- data/lib/samagotchi/live_versions.rb +59 -0
- data/lib/samagotchi/llm/api_key.rb +41 -0
- data/lib/samagotchi/llm/chat_loop.rb +132 -29
- data/lib/samagotchi/llm/errors.rb +41 -9
- data/lib/samagotchi/llm/http.rb +57 -17
- data/lib/samagotchi/llm/openai_chat.rb +17 -30
- data/lib/samagotchi/log_subscriber.rb +18 -3
- data/lib/samagotchi/memory_bundle/installer.rb +65 -63
- data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
- data/lib/samagotchi/memory_bundle/status.rb +4 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
- data/lib/samagotchi/model_profile.rb +24 -1
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/sessions.rb +3 -1
- data/lib/samagotchi/prompt.rb +4 -2
- data/lib/samagotchi/reminder_store.rb +1 -9
- data/lib/samagotchi/reply_wait.rb +126 -0
- data/lib/samagotchi/sampling_settings.rb +58 -0
- data/lib/samagotchi/self_report.rb +18 -3
- data/lib/samagotchi/send_command.rb +252 -11
- data/lib/samagotchi/session.rb +52 -11
- data/lib/samagotchi/session_archive_command.rb +107 -0
- data/lib/samagotchi/session_commands.rb +46 -7
- data/lib/samagotchi/session_manager.rb +115 -25
- data/lib/samagotchi/session_metrics.rb +222 -106
- data/lib/samagotchi/steer.rb +72 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +57 -28
- data/lib/samagotchi/terminal_ui/event_renderer.rb +21 -11
- data/lib/samagotchi/terminal_ui/formatting.rb +40 -8
- data/lib/samagotchi/terminal_ui/input_support.rb +7 -19
- data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
- data/lib/samagotchi/terminal_ui.rb +134 -247
- data/lib/samagotchi/text_diff.rb +181 -0
- data/lib/samagotchi/thinking.rb +115 -0
- data/lib/samagotchi/tool_activity.rb +3 -1
- data/lib/samagotchi/tool_runner.rb +34 -1
- data/lib/samagotchi/tools/ask_user_question.rb +41 -33
- data/lib/samagotchi/tools/builtins.rb +15 -4
- data/lib/samagotchi/tools/delegate_wait.rb +26 -69
- data/lib/samagotchi/tools/edit.rb +23 -9
- data/lib/samagotchi/tools/execute.rb +52 -14
- data/lib/samagotchi/tools/task_runtime.rb +19 -0
- data/lib/samagotchi/tools/task_wait.rb +27 -3
- data/lib/samagotchi/tools/write.rb +4 -0
- data/lib/samagotchi/turn_flow.rb +12 -2
- data/lib/samagotchi/turn_note.rb +60 -6
- data/lib/samagotchi/update_command.rb +308 -0
- data/lib/samagotchi/update_hint.rb +59 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +7 -9
- data/lib/samagotchi/web/app.rb +91 -7
- data/lib/samagotchi/web/message_parts.rb +8 -3
- data/lib/samagotchi/web/public/activity.js +13 -1
- data/lib/samagotchi/web/public/annotate_presets.js +26 -0
- data/lib/samagotchi/web/public/annotations.js +13 -0
- data/lib/samagotchi/web/public/app.js +472 -111
- data/lib/samagotchi/web/public/card.js +5 -3
- data/lib/samagotchi/web/public/chat_view.js +13 -1
- data/lib/samagotchi/web/public/copy.js +20 -4
- data/lib/samagotchi/web/public/ctx.js +15 -0
- data/lib/samagotchi/web/public/data.js +23 -6
- data/lib/samagotchi/web/public/diff_view.js +58 -0
- data/lib/samagotchi/web/public/format.js +9 -0
- data/lib/samagotchi/web/public/index.html +60 -3
- data/lib/samagotchi/web/public/notify.js +175 -0
- data/lib/samagotchi/web/public/question_card.js +5 -2
- data/lib/samagotchi/web/public/sessions_list.js +7 -0
- data/lib/samagotchi/web/public/timing.js +39 -14
- data/lib/samagotchi/web/public/turn_events.js +75 -5
- data/lib/samagotchi/web/public/turn_view.js +49 -8
- data/lib/samagotchi/web/server.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_summary.rb +24 -1
- data/lib/samagotchi/worker.rb +16 -4
- metadata +31 -1
|
@@ -47,12 +47,7 @@ module Samagotchi
|
|
|
47
47
|
include Formatting
|
|
48
48
|
include InputSupport
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
SKIP_AGENT_DESCRIPTION_ENV = "SAMAGOTCHI_SKIP_AGENT_MD"
|
|
52
|
-
STATS_COMMAND = "/stats"
|
|
53
|
-
# /exit --delete: delete the session on the way out.
|
|
54
|
-
EXIT_DELETE_FLAG = "--delete"
|
|
55
|
-
RECAP_COMMAND = "/recap"
|
|
50
|
+
SCRATCH_ARCHIVE_REFUSED = "a scratch session is deleted when you leave; nothing to archive"
|
|
56
51
|
# The prompt while a question waits for its answer (its choices are in
|
|
57
52
|
# the notes slot).
|
|
58
53
|
QUESTION_PROMPT = "? "
|
|
@@ -103,9 +98,12 @@ module Samagotchi
|
|
|
103
98
|
Engine.system_prompt_for(profile)
|
|
104
99
|
end
|
|
105
100
|
|
|
101
|
+
# @param scratch [Boolean] `chi scratch`: a new session that is deleted
|
|
102
|
+
# however the REPL ends, saves no memories and writes no recap
|
|
106
103
|
def initialize(mode: :assist, 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,
|
|
107
|
-
spinner_tick_interval: THINKING_TICK_INTERVAL)
|
|
104
|
+
spinner_tick_interval: THINKING_TICK_INTERVAL, scratch: false)
|
|
108
105
|
@mode = mode.to_sym
|
|
106
|
+
@scratch = scratch
|
|
109
107
|
# nil: no ticker thread (specs that compare exact frames).
|
|
110
108
|
@spinner_tick_interval = spinner_tick_interval
|
|
111
109
|
@spinner_lock = Monitor.new
|
|
@@ -139,6 +137,9 @@ module Samagotchi
|
|
|
139
137
|
# Re-resolve client after resume may change effective model
|
|
140
138
|
@client = @host_registry.resolve(@effective_model_name).client
|
|
141
139
|
end
|
|
140
|
+
# The model it starts on names a configured host (a bad default.model
|
|
141
|
+
# doesn't matter when --model or the session picks another).
|
|
142
|
+
ModelProfile.check_host!(@effective_model_name, hosts: @host_registry.entries)
|
|
142
143
|
# Only a caller's profile goes in; otherwise the Engine resolves one
|
|
143
144
|
# for the effective model and hands it to the kernel.
|
|
144
145
|
@kernel = KernelLoop.new(client: @client, profile: profile, no_interrupt: no_interrupt, reminder_store: Samagotchi::ReminderStore.new)
|
|
@@ -172,11 +173,14 @@ module Samagotchi
|
|
|
172
173
|
profile: profile,
|
|
173
174
|
session_id: session_id,
|
|
174
175
|
no_interrupt: no_interrupt,
|
|
175
|
-
|
|
176
|
+
# nil: the config default, unchecked here (the model the REPL starts
|
|
177
|
+
# on is checked above, and --model may make the default moot).
|
|
178
|
+
model_name: nil,
|
|
176
179
|
memories: @requested_memories,
|
|
177
180
|
muted_memories: @muted_memory_names,
|
|
178
181
|
kernel: @kernel,
|
|
179
|
-
recap: recap_config,
|
|
182
|
+
recap: scratch ? nil : recap_config,
|
|
183
|
+
scratch: scratch,
|
|
180
184
|
reminders: {
|
|
181
185
|
callback: lambda { |due_names|
|
|
182
186
|
# When a reminder is due, queue a synthetic turn that
|
|
@@ -242,15 +246,20 @@ module Samagotchi
|
|
|
242
246
|
model_name: @effective_model_name,
|
|
243
247
|
working_directory: Dir.pwd,
|
|
244
248
|
preloaded_memory_names: @requested_memories,
|
|
245
|
-
muted_memory_names: @muted_memory_names
|
|
249
|
+
muted_memory_names: @muted_memory_names,
|
|
250
|
+
scratch: @scratch
|
|
246
251
|
)
|
|
247
252
|
claim_session!(session.id) unless @owner_lock
|
|
253
|
+
# Saved at once, so a process killed before its first turn leaves a
|
|
254
|
+
# file the sweep knows to delete.
|
|
255
|
+
session.save if @scratch
|
|
248
256
|
# Attach before building the prompt so it can name the session id.
|
|
249
257
|
@engine.session = session
|
|
250
258
|
messages = messages_for(session)
|
|
251
259
|
|
|
252
260
|
if @prompt && @non_interactive
|
|
253
261
|
# Headless / CI mode: run directly without TTY rendering.
|
|
262
|
+
ArchiveStore.user_input(session.id, state_dir: Session.default_state_dir)
|
|
254
263
|
result = @engine.run_turn(
|
|
255
264
|
session,
|
|
256
265
|
@prompt,
|
|
@@ -259,6 +268,13 @@ module Samagotchi
|
|
|
259
268
|
cancel_controller: nil,
|
|
260
269
|
images: ImageInput.extract(@prompt)
|
|
261
270
|
)
|
|
271
|
+
if empty_answer?(result)
|
|
272
|
+
# Nothing to print: say so on stderr and fail, so a script that
|
|
273
|
+
# pipes the answer doesn't take the silence for one.
|
|
274
|
+
session.save
|
|
275
|
+
warn EMPTY_ANSWER_ERROR
|
|
276
|
+
exit 1
|
|
277
|
+
end
|
|
262
278
|
@surface.commit(result.output)
|
|
263
279
|
session.save
|
|
264
280
|
return
|
|
@@ -266,10 +282,14 @@ module Samagotchi
|
|
|
266
282
|
|
|
267
283
|
assist_loop(session: session, messages: messages)
|
|
268
284
|
# After the idle layer has stopped: nothing writes the session now.
|
|
269
|
-
if @
|
|
285
|
+
if @scratch
|
|
286
|
+
nil # deleted below, however the REPL ended
|
|
287
|
+
elsif @delete_on_exit
|
|
270
288
|
delete_after_exit(session)
|
|
271
289
|
elsif @discard_on_exit
|
|
272
290
|
discard_after_exit(session)
|
|
291
|
+
elsif @archive_on_exit
|
|
292
|
+
archive_after_exit(session)
|
|
273
293
|
else
|
|
274
294
|
keep_after_exit(session)
|
|
275
295
|
end
|
|
@@ -278,6 +298,32 @@ module Samagotchi
|
|
|
278
298
|
# #initialize): the anytime commands finish, the plugins' services
|
|
279
299
|
# stop (a server process).
|
|
280
300
|
@engine&.shutdown
|
|
301
|
+
# A scratch session goes on every way out: /exit, Ctrl-D, an error,
|
|
302
|
+
# SIGTERM or SIGHUP (Ruby raises those here). Only kill -9 leaves it
|
|
303
|
+
# to the sweep.
|
|
304
|
+
delete_scratch(session, quiet: @non_interactive || !$!.nil?) if @scratch && session
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
EMPTY_ANSWER_ERROR = "chi: the model gave an empty answer"
|
|
308
|
+
|
|
309
|
+
# A -p --non-interactive turn that ended with no answer (the empty-answer
|
|
310
|
+
# retries used up): no text, or the chat loop's placeholder.
|
|
311
|
+
def empty_answer?(result)
|
|
312
|
+
return false if result.respond_to?(:canceled?) && result.canceled?
|
|
313
|
+
return false if result.respond_to?(:resumable?) && result.resumable?
|
|
314
|
+
|
|
315
|
+
result.output.to_s.strip.empty? || (result.respond_to?(:empty_answer?) && result.empty_answer?)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Delete the scratch session, the last thing the REPL does. Quiet after
|
|
319
|
+
# a one-shot or on the way out of an error or a signal.
|
|
320
|
+
def delete_scratch(session, quiet:)
|
|
321
|
+
@owner_lock&.release
|
|
322
|
+
@owner_lock = nil
|
|
323
|
+
SessionManager.delete_session(session.id)
|
|
324
|
+
@surface.commit("Scratch session deleted.") unless quiet
|
|
325
|
+
rescue StandardError => e
|
|
326
|
+
warn "Scratch session #{session.id} was not deleted (#{e.message}): chi sessions delete #{session.id}"
|
|
281
327
|
end
|
|
282
328
|
|
|
283
329
|
# The session stays: the recap first, then the resume line, last.
|
|
@@ -330,6 +376,17 @@ module Samagotchi
|
|
|
330
376
|
@surface.commit("Session #{session.id} was not deleted (#{e.message}): chi sessions delete #{session.id}")
|
|
331
377
|
end
|
|
332
378
|
|
|
379
|
+
# /archive: give up the session, then archive it (hidden from the lists,
|
|
380
|
+
# kept for good). An empty one is discarded instead (#discard_on_exit?).
|
|
381
|
+
def archive_after_exit(session)
|
|
382
|
+
@owner_lock&.release
|
|
383
|
+
@owner_lock = nil
|
|
384
|
+
SessionManager.archive_session(session.id)
|
|
385
|
+
@surface.commit("Archived session #{session.id}. chi sessions list --archived finds it.")
|
|
386
|
+
rescue SessionManager::ArchiveRefused, SessionManager::OwnedByTUI, ArgumentError, SystemCallError => e
|
|
387
|
+
@surface.commit("Session #{session.id} was not archived (#{e.message}): chi sessions archive #{session.id}")
|
|
388
|
+
end
|
|
389
|
+
|
|
333
390
|
# Take the session's OwnerLock for this process's lifetime: the TUI runs
|
|
334
391
|
# its own Engine, so no worker may run the session meanwhile.
|
|
335
392
|
# @raise [SessionBusy] when a worker or another TUI owns it
|
|
@@ -356,6 +413,9 @@ module Samagotchi
|
|
|
356
413
|
@surface.commit("Resumed session: #{session.id}")
|
|
357
414
|
saved = @engine.saved_recap
|
|
358
415
|
@surface.commit(recap_block(saved[:text], turns_since: saved[:turns_since])) if saved
|
|
416
|
+
elsif @scratch
|
|
417
|
+
messages = [system_message]
|
|
418
|
+
@surface.commit("Scratch session: nothing is kept, it is deleted when you leave.")
|
|
359
419
|
else
|
|
360
420
|
messages = [system_message]
|
|
361
421
|
@surface.commit("Session: #{session.id}")
|
|
@@ -363,26 +423,8 @@ module Samagotchi
|
|
|
363
423
|
messages
|
|
364
424
|
end
|
|
365
425
|
|
|
366
|
-
# Public entrypoint for background session workers.
|
|
367
|
-
# Keeps worker call sites out of TerminalUI private API details.
|
|
368
|
-
def process_background_prompt(session:, prompt:)
|
|
369
|
-
@engine.process_background_prompt(session: session, prompt: prompt)
|
|
370
|
-
end
|
|
371
|
-
|
|
372
426
|
private
|
|
373
427
|
|
|
374
|
-
# Generate profile-aware tool calling hint.
|
|
375
|
-
# Kept as a one-line delegator to Engine (single source of truth).
|
|
376
|
-
def tool_call_hint
|
|
377
|
-
return @engine.tool_call_hint
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
# Assist system prompt — delegates wholesale to Engine so the two can
|
|
381
|
-
# never drift apart. This single delegator fixes the 4-line regression.
|
|
382
|
-
def assist_system_prompt
|
|
383
|
-
return @engine.assist_system_prompt
|
|
384
|
-
end
|
|
385
|
-
|
|
386
428
|
# Interactive REPL loop. Session seed + messages are built by #run and
|
|
387
429
|
# threaded in here (so --prompt / --resume share one code path). The
|
|
388
430
|
# working session is persisted at the end of every turn.
|
|
@@ -496,15 +538,22 @@ module Samagotchi
|
|
|
496
538
|
end
|
|
497
539
|
end
|
|
498
540
|
break if input.nil?
|
|
499
|
-
|
|
500
|
-
|
|
541
|
+
local = local_command(input)
|
|
542
|
+
if local == :exit
|
|
543
|
+
@delete_on_exit ||= SessionCommands.delete_on_exit?(input)
|
|
544
|
+
break
|
|
545
|
+
end
|
|
546
|
+
if local == :archive
|
|
547
|
+
next @surface.commit(SCRATCH_ARCHIVE_REFUSED) if @scratch
|
|
548
|
+
|
|
549
|
+
@archive_on_exit = true
|
|
501
550
|
break
|
|
502
551
|
end
|
|
503
552
|
# Not an answer to a continue offer either.
|
|
504
|
-
next detach_note if
|
|
553
|
+
next detach_note if local == :detach
|
|
505
554
|
|
|
506
555
|
# /stats and /recap run; the offer stays open.
|
|
507
|
-
if @turn_flow.awaiting_continue? &&
|
|
556
|
+
if @turn_flow.awaiting_continue? && !%i[stats recap].include?(local)
|
|
508
557
|
answer_continue_offer(session, input)
|
|
509
558
|
else
|
|
510
559
|
# The ? read left no echo: show what ran.
|
|
@@ -557,15 +606,14 @@ module Samagotchi
|
|
|
557
606
|
persist_recent_history(input) if command.shell
|
|
558
607
|
return
|
|
559
608
|
end
|
|
560
|
-
return
|
|
561
|
-
return @surface.commit("\nmodel> #{handle_recap_command}") if recap_command?(input)
|
|
609
|
+
return if show_local_command(input)
|
|
562
610
|
|
|
563
611
|
@turn_flow.before_prompt_turn
|
|
564
612
|
persist_recent_history(input)
|
|
565
613
|
begin
|
|
566
614
|
# Engine#run_turn injects due reminders as a tail message, appends
|
|
567
615
|
# the prompt, and renders through @renderer via on_event.
|
|
568
|
-
result = run_engine_turn(session,
|
|
616
|
+
result = run_engine_turn(session, input, images: ImageInput.extract(input))
|
|
569
617
|
rescue LLM::ProviderError, ImageStore::Error => e
|
|
570
618
|
# Engine closed the turn (:turn_failed); show its duration.
|
|
571
619
|
# Retries were already tallied via generation_retrying events.
|
|
@@ -661,18 +709,6 @@ module Samagotchi
|
|
|
661
709
|
prompt
|
|
662
710
|
end
|
|
663
711
|
|
|
664
|
-
# Appends the current memory index to the base system prompt so the agent
|
|
665
|
-
# is always aware of stored memories without needing to call a tool first.
|
|
666
|
-
# Delegates wholesale to Engine (single source of truth).
|
|
667
|
-
def system_prompt_with_index(base)
|
|
668
|
-
result = @engine.system_prompt_with_index(base)
|
|
669
|
-
# Mirror any --memory activations the Engine performed so the sticky
|
|
670
|
-
# status line can surface them. The prompt body injection moved into
|
|
671
|
-
# Engine; echoing the activated names here is purely a UI concern.
|
|
672
|
-
sync_engine_activated_memories
|
|
673
|
-
result
|
|
674
|
-
end
|
|
675
|
-
|
|
676
712
|
# Engine owns the system prompt (including --memory activation), but the
|
|
677
713
|
# sticky status line is a UI concern. Mirror the activated names so they
|
|
678
714
|
# appear in the status line.
|
|
@@ -682,12 +718,6 @@ module Samagotchi
|
|
|
682
718
|
end
|
|
683
719
|
end
|
|
684
720
|
|
|
685
|
-
# Render a result from a turn that bypassed Engine#run_turn (continue,
|
|
686
|
-
# reminder turns) exactly as a :turn_completed would be rendered.
|
|
687
|
-
def emit_result(result)
|
|
688
|
-
@renderer.render_turn_summary(@engine.turn_summary(result))
|
|
689
|
-
end
|
|
690
|
-
|
|
691
721
|
# ── Turn view: the drawing surface EventRenderer calls ──────────────────
|
|
692
722
|
public
|
|
693
723
|
|
|
@@ -758,8 +788,21 @@ module Samagotchi
|
|
|
758
788
|
@thinking_waiting_since = nil
|
|
759
789
|
clear_retry_spinner_status
|
|
760
790
|
@turn_tally.started(key: tally_key(event), tool: event[:tool], params: event[:params])
|
|
761
|
-
|
|
762
|
-
|
|
791
|
+
@last_tool_call_event = event
|
|
792
|
+
refresh_thinking_spinner_status
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# The Engine's memory list: after a memory read (read_names, right after
|
|
797
|
+
# its tool_call_started) and at the end of a turn (preloads).
|
|
798
|
+
def used_memories_updated(event)
|
|
799
|
+
@spinner_lock.synchronize do
|
|
800
|
+
Array(event[:used_memory_names]).each { |name| add_unique_memory_name(:@session_memory_names, name) }
|
|
801
|
+
added = Array(event[:read_names]).select { |name| add_unique_memory_name(:@thinking_memory_names, name) }
|
|
802
|
+
next if added.empty?
|
|
803
|
+
|
|
804
|
+
@thinking_recent_memory_loaded = added.last
|
|
805
|
+
capture_thinking_tool_call(@last_tool_call_event) if @last_tool_call_event
|
|
763
806
|
refresh_thinking_spinner_status
|
|
764
807
|
end
|
|
765
808
|
end
|
|
@@ -806,16 +849,6 @@ module Samagotchi
|
|
|
806
849
|
@surface.commit("#{paint('chi>', 36)} turn #{state} (#{format_elapsed_duration(record[:duration_ms])})")
|
|
807
850
|
end
|
|
808
851
|
|
|
809
|
-
def split_memory_scope(raw)
|
|
810
|
-
value = raw.to_s.strip
|
|
811
|
-
if value.include?("/")
|
|
812
|
-
scope, name = value.split("/", 2)
|
|
813
|
-
return [scope, name] if Tools::VALID_SCOPES.include?(scope)
|
|
814
|
-
end
|
|
815
|
-
|
|
816
|
-
[nil, value]
|
|
817
|
-
end
|
|
818
|
-
|
|
819
852
|
def read_input(awaiting_continue:)
|
|
820
853
|
emit_idle_status_line
|
|
821
854
|
|
|
@@ -841,6 +874,11 @@ module Samagotchi
|
|
|
841
874
|
# empty synthetic turn then (it duplicated output).
|
|
842
875
|
return unless @engine.reminders_due?
|
|
843
876
|
|
|
877
|
+
# A pending continue offer goes, as for a new prompt (the worker's rule).
|
|
878
|
+
if @turn_flow.before_reminder_turn
|
|
879
|
+
sync_continue_slot(false)
|
|
880
|
+
@surface.commit(QuestionSlot.continue_summary("(dropped: a reminder ran)", paint: method(:paint)))
|
|
881
|
+
end
|
|
844
882
|
hint = "reminder: #{names.join(", ")} · Ctrl-C cancels it"
|
|
845
883
|
@surface.set_slot(:hints, [color_output? ? paint(hint, 90) : hint])
|
|
846
884
|
result = nil
|
|
@@ -954,27 +992,20 @@ module Samagotchi
|
|
|
954
992
|
@repl_input.prefill(input) ? "prompt restored for retry" : "the failed prompt is in the input history (↑)"
|
|
955
993
|
end
|
|
956
994
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
995
|
+
# The id of the terminal's own command +input+ is (:exit, :archive,
|
|
996
|
+
# :detach, :stats, :recap; SessionCommands registers them), or nil.
|
|
997
|
+
def local_command(input) = command_registry.lookup_local(input)&.id
|
|
960
998
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
result = @commands.run(input)
|
|
972
|
-
sync_model_mirrors
|
|
973
|
-
result.output
|
|
974
|
-
end
|
|
975
|
-
|
|
976
|
-
def handle_models_command
|
|
977
|
-
@commands.run(SessionCommands::MODELS_COMMAND).output
|
|
999
|
+
# /stats and /recap, which show and change nothing (at the prompt, a
|
|
1000
|
+
# continue offer's ? prompt, or during a turn).
|
|
1001
|
+
# @return [Boolean] whether +input+ was one
|
|
1002
|
+
def show_local_command(input)
|
|
1003
|
+
case local_command(input)
|
|
1004
|
+
when :stats then @surface.commit("\nmodel> session stats:\n#{format_session_metrics(@engine.stats_snapshot)}")
|
|
1005
|
+
when :recap then @surface.commit("\nmodel> #{handle_recap_command}")
|
|
1006
|
+
else return false
|
|
1007
|
+
end
|
|
1008
|
+
true
|
|
978
1009
|
end
|
|
979
1010
|
|
|
980
1011
|
# /recap: the saved recap, and a new one asked for at once when the
|
|
@@ -991,29 +1022,10 @@ module Samagotchi
|
|
|
991
1022
|
# A resumed session doesn't get the default input either.
|
|
992
1023
|
def default_input_wanted? = !@resume_session && !@no_default_input
|
|
993
1024
|
|
|
994
|
-
# exit or /exit, with --delete to delete the session on the way out.
|
|
995
|
-
def exit_command?(input)
|
|
996
|
-
words = input.to_s.strip.downcase.split
|
|
997
|
-
%w[exit /exit].include?(words.first) && (words.size == 1 || words == [words.first, EXIT_DELETE_FLAG])
|
|
998
|
-
end
|
|
999
|
-
|
|
1000
|
-
def delete_on_exit?(input) = input.to_s.strip.downcase.split.last == EXIT_DELETE_FLAG
|
|
1001
|
-
|
|
1002
|
-
def detach_command?(input) = input.to_s.strip.casecmp?("/detach")
|
|
1003
|
-
|
|
1004
1025
|
def clone_messages(messages)
|
|
1005
1026
|
Array(messages).map(&:dup)
|
|
1006
1027
|
end
|
|
1007
1028
|
|
|
1008
|
-
def skip_agent_description?
|
|
1009
|
-
value = ENV[SKIP_AGENT_DESCRIPTION_ENV]
|
|
1010
|
-
value == "1" || value&.casecmp?("true")
|
|
1011
|
-
end
|
|
1012
|
-
|
|
1013
|
-
def rg_available?
|
|
1014
|
-
system("command -v rg", out: File::NULL, err: File::NULL)
|
|
1015
|
-
end
|
|
1016
|
-
|
|
1017
1029
|
# Run one REPL turn (a prompt, or a continue/reminder turn with nil) through
|
|
1018
1030
|
# Engine#run_turn, rendering via @renderer.
|
|
1019
1031
|
# @param images [Array<Hash>] the prompt's `@path` images ({path:})
|
|
@@ -1024,6 +1036,8 @@ module Samagotchi
|
|
|
1024
1036
|
# Build the (memoized) prompt now so --memory activations show in this
|
|
1025
1037
|
# turn's status lines, including after /model rebuilt it.
|
|
1026
1038
|
seed_system_prompt
|
|
1039
|
+
# A prompt the user typed brings an archived session back to the lists.
|
|
1040
|
+
ArchiveStore.user_input(session.id, state_dir: Session.default_state_dir) if prompt && !continue
|
|
1027
1041
|
result = with_steering do
|
|
1028
1042
|
@engine.run_turn(
|
|
1029
1043
|
session,
|
|
@@ -1066,11 +1080,13 @@ module Samagotchi
|
|
|
1066
1080
|
# in the prompt.
|
|
1067
1081
|
# @return [Boolean, :back] whether the turn took it, :back to put it back
|
|
1068
1082
|
def steer_line(line)
|
|
1069
|
-
|
|
1070
|
-
return
|
|
1083
|
+
local = local_command(line) unless line.nil?
|
|
1084
|
+
return exit_after_turn(delete: SessionCommands.delete_on_exit?(line)) if line.nil? || local == :exit
|
|
1085
|
+
return detach_note if local == :detach
|
|
1071
1086
|
return false if @active_cancel_controller&.cancelled?
|
|
1072
1087
|
return start_anytime_command(line) if command_registry.lookup(line)&.anytime
|
|
1073
|
-
|
|
1088
|
+
# A command, never steering text (/archive waits, back in the prompt).
|
|
1089
|
+
return command_during_turn(line) if local || command_registry.command?(line)
|
|
1074
1090
|
return true if line.strip.empty?
|
|
1075
1091
|
# Steering merges text only: a line with images runs as the next turn.
|
|
1076
1092
|
return image_line_waits if ImageInput.extract(line).any?
|
|
@@ -1104,15 +1120,10 @@ module Samagotchi
|
|
|
1104
1120
|
|
|
1105
1121
|
# @return [true, :back]
|
|
1106
1122
|
def command_during_turn(line)
|
|
1107
|
-
if
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
else
|
|
1112
|
-
@surface.commit(COMMAND_BUSY)
|
|
1113
|
-
return :back
|
|
1114
|
-
end
|
|
1115
|
-
true
|
|
1123
|
+
return true if show_local_command(line)
|
|
1124
|
+
|
|
1125
|
+
@surface.commit(COMMAND_BUSY)
|
|
1126
|
+
:back
|
|
1116
1127
|
end
|
|
1117
1128
|
|
|
1118
1129
|
# D8: an anytime command runs on its own thread while the turn goes on
|
|
@@ -1132,16 +1143,12 @@ module Samagotchi
|
|
|
1132
1143
|
true
|
|
1133
1144
|
end
|
|
1134
1145
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
end
|
|
1138
|
-
|
|
1139
|
-
# The kernel's drain at an iteration boundary: queued lines, #memory
|
|
1140
|
-
# shorthand made words as for a prompt, and saved in the history.
|
|
1146
|
+
# The kernel's drain at an iteration boundary: queued lines, sent as
|
|
1147
|
+
# typed and saved in the history.
|
|
1141
1148
|
def drain_steering
|
|
1142
1149
|
@pending_input_queue.drain.map do |line|
|
|
1143
1150
|
persist_recent_history(line)
|
|
1144
|
-
|
|
1151
|
+
line
|
|
1145
1152
|
end
|
|
1146
1153
|
end
|
|
1147
1154
|
|
|
@@ -1299,17 +1306,6 @@ module Samagotchi
|
|
|
1299
1306
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1300
1307
|
end
|
|
1301
1308
|
|
|
1302
|
-
def capture_memory_tool_call(event)
|
|
1303
|
-
call = event[:call].is_a?(Hash) ? event[:call] : {}
|
|
1304
|
-
memory_name = memory_name_from_tool_call(call)
|
|
1305
|
-
return false if memory_name.nil? || memory_name.empty?
|
|
1306
|
-
|
|
1307
|
-
added_to_thinking = add_unique_memory_name(:@thinking_memory_names, memory_name)
|
|
1308
|
-
add_unique_memory_name(:@session_memory_names, memory_name)
|
|
1309
|
-
@thinking_recent_memory_loaded = memory_name if added_to_thinking
|
|
1310
|
-
added_to_thinking
|
|
1311
|
-
end
|
|
1312
|
-
|
|
1313
1309
|
def capture_thinking_tool_call(event)
|
|
1314
1310
|
call = event[:call].is_a?(Hash) ? event[:call] : {}
|
|
1315
1311
|
name = call[:name].to_s.strip
|
|
@@ -1367,67 +1363,6 @@ module Samagotchi
|
|
|
1367
1363
|
true
|
|
1368
1364
|
end
|
|
1369
1365
|
|
|
1370
|
-
def memory_name_from_tool_call(call)
|
|
1371
|
-
tool_name = call[:name].to_s
|
|
1372
|
-
case tool_name
|
|
1373
|
-
when Tools::MemoryRead::NAME
|
|
1374
|
-
normalize_memory_name(call[:content])
|
|
1375
|
-
when Tools::Read::NAME
|
|
1376
|
-
memory_name_from_read_path(call[:content])
|
|
1377
|
-
else
|
|
1378
|
-
nil
|
|
1379
|
-
end
|
|
1380
|
-
end
|
|
1381
|
-
|
|
1382
|
-
def normalize_memory_name(raw)
|
|
1383
|
-
value = raw.to_s.strip
|
|
1384
|
-
return nil if value.empty?
|
|
1385
|
-
|
|
1386
|
-
File.basename(value, ".md")
|
|
1387
|
-
end
|
|
1388
|
-
|
|
1389
|
-
def memory_name_from_read_path(raw_path)
|
|
1390
|
-
path = raw_path.to_s.strip.tr("\\", "/")
|
|
1391
|
-
return nil if path.empty?
|
|
1392
|
-
return nil unless path.match?(/memories[\/].+\.md\z/)
|
|
1393
|
-
|
|
1394
|
-
normalize_memory_name(path)
|
|
1395
|
-
end
|
|
1396
|
-
|
|
1397
|
-
def memory_spinner_segment
|
|
1398
|
-
segment = memory_spinner_segment_plain
|
|
1399
|
-
return "" if segment.empty?
|
|
1400
|
-
|
|
1401
|
-
color_output? ? paint(segment, MEMORY_SPINNER_COLOR) : segment
|
|
1402
|
-
end
|
|
1403
|
-
|
|
1404
|
-
def memory_spinner_segment_plain
|
|
1405
|
-
names = Array(@thinking_memory_names)
|
|
1406
|
-
return "" if names.empty?
|
|
1407
|
-
|
|
1408
|
-
visible = names.first(MEMORY_SPINNER_PREVIEW_LIMIT)
|
|
1409
|
-
suffix = names.length > visible.length ? ", +#{names.length - visible.length}" : ""
|
|
1410
|
-
" mem: #{visible.join(', ')}#{suffix}"
|
|
1411
|
-
end
|
|
1412
|
-
|
|
1413
|
-
def memory_sticky_line
|
|
1414
|
-
names = Array(@session_memory_names)
|
|
1415
|
-
muted = @engine.muted_memory_names
|
|
1416
|
-
return "" if names.empty? && muted.empty?
|
|
1417
|
-
|
|
1418
|
-
parts = []
|
|
1419
|
-
parts << "active this session: #{preview_names(names)}" unless names.empty?
|
|
1420
|
-
parts << "muted: #{preview_names(muted)}" unless muted.empty?
|
|
1421
|
-
body = "memories> #{parts.join(' · ')}"
|
|
1422
|
-
color_output? ? paint(body, MEMORY_SPINNER_COLOR) : body
|
|
1423
|
-
end
|
|
1424
|
-
|
|
1425
|
-
def preview_names(names, limit: MEMORY_STICKY_PREVIEW_LIMIT)
|
|
1426
|
-
visible = names.first(limit)
|
|
1427
|
-
suffix = names.length > visible.length ? ", +#{names.length - visible.length}" : ""
|
|
1428
|
-
"#{visible.join(', ')}#{suffix}"
|
|
1429
|
-
end
|
|
1430
|
-
|
|
1431
1366
|
def reset_thinking_memory_names
|
|
1432
1367
|
@thinking_memory_names = []
|
|
1433
1368
|
end
|
|
@@ -1549,54 +1484,24 @@ module Samagotchi
|
|
|
1549
1484
|
ConfigFile.recap_config
|
|
1550
1485
|
end
|
|
1551
1486
|
|
|
1552
|
-
def bare_model_for(full_ref)
|
|
1553
|
-
@host_registry.bare_name(full_ref)
|
|
1554
|
-
end
|
|
1555
|
-
|
|
1556
|
-
def spinner_status_line
|
|
1557
|
-
return "" unless status_line_enabled?
|
|
1558
|
-
|
|
1559
|
-
lines = spinner_status_lines
|
|
1560
|
-
lines.empty? ? "" : lines.first
|
|
1561
|
-
end
|
|
1562
|
-
|
|
1563
1487
|
def spinner_status_lines(width: status_effective_width)
|
|
1564
1488
|
return [] unless status_line_enabled?
|
|
1565
1489
|
|
|
1566
1490
|
build_status_lines(scope: :spinner, width: width)
|
|
1567
1491
|
end
|
|
1568
1492
|
|
|
1569
|
-
def sticky_status_line
|
|
1570
|
-
return "" unless status_line_enabled?
|
|
1571
|
-
|
|
1572
|
-
lines = sticky_status_lines
|
|
1573
|
-
lines.empty? ? "" : lines.first
|
|
1574
|
-
end
|
|
1575
|
-
|
|
1576
1493
|
def sticky_status_lines(width: status_effective_width)
|
|
1577
1494
|
return [] unless status_line_enabled?
|
|
1578
1495
|
|
|
1579
1496
|
build_status_lines(scope: :sticky, width: width)
|
|
1580
1497
|
end
|
|
1581
1498
|
|
|
1582
|
-
def idle_status_line
|
|
1583
|
-
return "" unless status_line_enabled?
|
|
1584
|
-
|
|
1585
|
-
lines = idle_status_lines
|
|
1586
|
-
lines.empty? ? "" : lines.first
|
|
1587
|
-
end
|
|
1588
|
-
|
|
1589
1499
|
def idle_status_lines(width: status_effective_width)
|
|
1590
1500
|
return [] unless status_line_enabled?
|
|
1591
1501
|
|
|
1592
1502
|
build_status_lines(scope: :idle, width: width)
|
|
1593
1503
|
end
|
|
1594
1504
|
|
|
1595
|
-
def build_status_line(scope:)
|
|
1596
|
-
lines = build_status_lines(scope: scope)
|
|
1597
|
-
lines.empty? ? "" : lines.first
|
|
1598
|
-
end
|
|
1599
|
-
|
|
1600
1505
|
# The status rows for +scope+ (:spinner, :sticky or :idle), cut to +width+.
|
|
1601
1506
|
def build_status_lines(scope:, width: status_effective_width)
|
|
1602
1507
|
status_rows(status_segments(scope), width)
|
|
@@ -1772,11 +1677,6 @@ module Samagotchi
|
|
|
1772
1677
|
"model> waiting for the first token... #{waited.floor}s #{frame}"
|
|
1773
1678
|
end
|
|
1774
1679
|
|
|
1775
|
-
def thinking_spinner_status_line(frame)
|
|
1776
|
-
lines = thinking_spinner_status_lines(frame)
|
|
1777
|
-
lines.empty? ? "" : lines.first
|
|
1778
|
-
end
|
|
1779
|
-
|
|
1780
1680
|
# The spinner row, then (from a turn's 3rd tool call) its tool tally.
|
|
1781
1681
|
# The spinner stops while tools run, so the tally shows while the model
|
|
1782
1682
|
# generates between tool rounds.
|
|
@@ -1844,10 +1744,6 @@ module Samagotchi
|
|
|
1844
1744
|
nil
|
|
1845
1745
|
end
|
|
1846
1746
|
|
|
1847
|
-
def pending_question_event?
|
|
1848
|
-
!!@pending_question_event
|
|
1849
|
-
end
|
|
1850
|
-
|
|
1851
1747
|
# Called from REPL thread (run_assist_loop top) — renders widget, blocks
|
|
1852
1748
|
# until user selects, then answers via Engine#answer_question which wakes
|
|
1853
1749
|
# the parked turn thread.
|
|
@@ -1889,6 +1785,9 @@ module Samagotchi
|
|
|
1889
1785
|
|
|
1890
1786
|
# Ensure spinner cleared and terminal in known state (same as reminder mute handling)
|
|
1891
1787
|
finish_thinking_spinner rescue nil
|
|
1788
|
+
# An edit's diff goes above the slot, into the scrollback.
|
|
1789
|
+
preview = prompt.preview_lines(paint: method(:paint))
|
|
1790
|
+
@surface.commit(preview.join("\n")) unless preview.empty?
|
|
1892
1791
|
choices = prompt.slot(paint: method(:paint))
|
|
1893
1792
|
|
|
1894
1793
|
question_prompt = paint(QUESTION_PROMPT, 33)
|
|
@@ -1976,17 +1875,5 @@ module Samagotchi
|
|
|
1976
1875
|
return :canceled if controller&.cancelled?
|
|
1977
1876
|
end
|
|
1978
1877
|
end
|
|
1979
|
-
|
|
1980
|
-
# Process a prompt through the kernel loop and return the model response.
|
|
1981
|
-
# Delegates to the internal Engine instance.
|
|
1982
|
-
def process_prompt_through_kernel(session, prompt)
|
|
1983
|
-
@engine.process_prompt_through_kernel(session, prompt)
|
|
1984
|
-
end
|
|
1985
|
-
|
|
1986
|
-
# Write an agent response to the session output directory.
|
|
1987
|
-
def write_session_output(output_dir, response)
|
|
1988
|
-
timestamp = Time.now.strftime("%Y%m%d%H%M%S%9N")
|
|
1989
|
-
File.write(File.join(output_dir, "#{timestamp}.txt"), response.to_s)
|
|
1990
|
-
end
|
|
1991
1878
|
end
|
|
1992
1879
|
end
|