samagotchi 0.63.0 → 0.65.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 +86 -1
- data/docs/architecture.md +32 -5
- data/docs/cli.md +41 -9
- data/docs/configuration.md +143 -11
- data/docs/internals/engine.md +25 -6
- data/docs/plugins.md +7 -0
- data/docs/sessions.md +14 -10
- data/docs/sub-agent.md +21 -4
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +5 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
- data/lib/samagotchi/bridge.rb +29 -5
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -3
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_reports.rb +7 -2
- data/lib/samagotchi/cli/parent_wait.rb +9 -2
- data/lib/samagotchi/client.rb +4 -2
- data/lib/samagotchi/composer_route.rb +90 -0
- data/lib/samagotchi/config.rb +11 -7
- data/lib/samagotchi/continue_offer.rb +17 -3
- data/lib/samagotchi/delivery.rb +13 -8
- data/lib/samagotchi/engine.rb +41 -7
- data/lib/samagotchi/generation_phase.rb +17 -0
- data/lib/samagotchi/host_config.rb +33 -12
- data/lib/samagotchi/host_model.rb +2 -2
- data/lib/samagotchi/host_registry.rb +34 -11
- data/lib/samagotchi/idle_client.rb +62 -25
- data/lib/samagotchi/idle_recap.rb +5 -2
- data/lib/samagotchi/idle_target.rb +30 -7
- data/lib/samagotchi/interrupted_turn.rb +7 -3
- data/lib/samagotchi/last_turn.rb +5 -2
- data/lib/samagotchi/llm/adapters.rb +41 -0
- data/lib/samagotchi/llm/anthropic_assembly.rb +210 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +416 -0
- data/lib/samagotchi/llm/anthropic_wire.rb +238 -0
- data/lib/samagotchi/llm/api_key.rb +26 -9
- data/lib/samagotchi/llm/chat_loop.rb +113 -14
- data/lib/samagotchi/llm/dialect.rb +57 -0
- data/lib/samagotchi/llm/errors.rb +81 -12
- data/lib/samagotchi/llm/model_listing.rb +111 -0
- data/lib/samagotchi/llm/model_result.rb +9 -2
- data/lib/samagotchi/llm/openai_chat.rb +46 -93
- data/lib/samagotchi/model_profile.rb +1 -1
- data/lib/samagotchi/pending_input_queue.rb +10 -3
- data/lib/samagotchi/plugin/sessions.rb +27 -5
- data/lib/samagotchi/reasoning_history.rb +14 -4
- data/lib/samagotchi/recap_setup.rb +2 -1
- data/lib/samagotchi/reply_wait.rb +171 -6
- data/lib/samagotchi/self_report.rb +18 -28
- data/lib/samagotchi/send_command.rb +63 -19
- data/lib/samagotchi/session.rb +3 -2
- data/lib/samagotchi/session_commands.rb +1 -1
- data/lib/samagotchi/session_inbox.rb +63 -7
- data/lib/samagotchi/session_manager.rb +49 -20
- data/lib/samagotchi/session_metrics.rb +6 -11
- data/lib/samagotchi/steer_cut.rb +4 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +55 -28
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -22
- data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
- data/lib/samagotchi/terminal_ui.rb +72 -57
- data/lib/samagotchi/tool_activity.rb +5 -5
- data/lib/samagotchi/tools/delegate.rb +28 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +9 -4
- data/lib/samagotchi/tools/delegate_wait.rb +17 -5
- data/lib/samagotchi/tools/execute.rb +11 -4
- data/lib/samagotchi/turn_checkpoint.rb +9 -3
- data/lib/samagotchi/turn_note.rb +22 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -1
- data/lib/samagotchi/web/public/activity.js +3 -0
- data/lib/samagotchi/web/public/app.js +96 -13
- data/lib/samagotchi/web/public/commands.js +8 -8
- data/lib/samagotchi/web/public/composer.js +3 -3
- data/lib/samagotchi/web/public/delivery_line.js +228 -0
- data/lib/samagotchi/web/public/index.html +29 -0
- data/lib/samagotchi/web/public/live_turn.js +8 -0
- data/lib/samagotchi/web/public/stage_model.js +27 -7
- data/lib/samagotchi/web/public/stage_view.js +32 -10
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +2 -1
- data/lib/samagotchi/web/public/turn_events.js +22 -8
- data/lib/samagotchi/web/public/turn_html.js +20 -4
- data/lib/samagotchi/web/public/turn_model.js +29 -12
- data/lib/samagotchi/web/public/turn_view.js +6 -4
- data/lib/samagotchi/worker.rb +7 -5
- metadata +9 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../client_id"
|
|
4
|
+
require_relative "../composer_route"
|
|
4
5
|
require_relative "event_renderer"
|
|
5
6
|
require_relative "formatting"
|
|
6
7
|
require_relative "../prompt_history"
|
|
@@ -259,13 +260,17 @@ module Samagotchi
|
|
|
259
260
|
take_session_state(event[:session_state_snapshot] || {})
|
|
260
261
|
return render_snapshot(event[:snapshot] || {}, reset: event[:type] == :reset)
|
|
261
262
|
when :turn_enqueued then show_enqueued(event)
|
|
262
|
-
when :turn_started
|
|
263
|
+
when :turn_started
|
|
264
|
+
@status.take_event(event)
|
|
265
|
+
start_turn(event)
|
|
263
266
|
when :turn_completed
|
|
267
|
+
@status.take_event(event)
|
|
264
268
|
complete_turn(event)
|
|
265
269
|
refresh_children
|
|
266
270
|
own_turn_ended(event)
|
|
267
271
|
# The renderer says how it ended (the REPL's words too).
|
|
268
272
|
when :turn_canceled, :turn_failed
|
|
273
|
+
@status.take_event(event)
|
|
269
274
|
@renderer.call(event)
|
|
270
275
|
end_turn
|
|
271
276
|
refresh_children
|
|
@@ -387,61 +392,72 @@ module Samagotchi
|
|
|
387
392
|
end
|
|
388
393
|
|
|
389
394
|
# /cut TEXT and /queue TEXT are this UI's own deliveries, read before
|
|
390
|
-
# every command check (SessionCommands registers them
|
|
391
|
-
# /help only). A plain line sends no delivery: it goes in
|
|
392
|
-
# running turn's next step, and never cuts.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
end
|
|
395
|
+
# every command check (ComposerRoute; SessionCommands registers them
|
|
396
|
+
# for Tab and /help only). A plain line sends no delivery: it goes in
|
|
397
|
+
# at the running turn's next step, and never cuts.
|
|
398
|
+
route = ComposerRoute.call(text, running: @running, registry: command_registry)
|
|
399
|
+
return submit_delivery(route) if route.choice
|
|
396
400
|
|
|
397
401
|
# The terminal's own commands (SessionCommands registers them, the
|
|
398
402
|
# REPL reads the same words). Before the continue offer: the
|
|
399
403
|
# leaving ones are no answer to it.
|
|
400
|
-
local =
|
|
404
|
+
local = route.local
|
|
401
405
|
return submit(nil) if local == :detach
|
|
402
406
|
return SessionCommands.delete_on_exit?(text) ? exit_and_delete : exit_worker if local == :exit
|
|
403
407
|
return archive_after_turn if local == :archive && (@running || !@queued_ids.empty?)
|
|
404
408
|
return exit_and_archive if local == :archive
|
|
405
409
|
|
|
406
410
|
if @continue_offer
|
|
407
|
-
return answer_continue(text) unless local ||
|
|
411
|
+
return answer_continue(text) unless local || route.kind == :command
|
|
408
412
|
|
|
409
413
|
# Not an answer: the read left no echo, so show what ran.
|
|
410
414
|
echo_answer(text)
|
|
411
415
|
end
|
|
412
|
-
return if
|
|
416
|
+
return if route.kind == :empty
|
|
413
417
|
|
|
414
418
|
if local == :stats
|
|
415
419
|
show_stats
|
|
416
420
|
elsif local == :recap
|
|
417
421
|
show_recap
|
|
418
|
-
elsif
|
|
422
|
+
elsif route.kind == :command
|
|
419
423
|
# The history keeps !cmds, as the REPL's does (prompts: #send_prompt).
|
|
420
424
|
persist_recent_history(text) if PromptHistory.shell_line?(text)
|
|
421
425
|
send_command(text)
|
|
422
|
-
elsif
|
|
426
|
+
elsif route.kind == :typo
|
|
423
427
|
# A typo of a command (/modle) is not a prompt: say so, send nothing.
|
|
424
|
-
@screen.commit(
|
|
428
|
+
@screen.commit(route.reply)
|
|
425
429
|
else
|
|
426
430
|
send_prompt(text)
|
|
427
431
|
end
|
|
428
432
|
nil
|
|
429
433
|
end
|
|
430
434
|
|
|
431
|
-
# /cut TEXT and /queue TEXT
|
|
432
|
-
#
|
|
433
|
-
#
|
|
434
|
-
#
|
|
435
|
+
# /cut TEXT and /queue TEXT (+route+ has a choice). During a turn the
|
|
436
|
+
# text goes with its delivery: a cut, or queued to run as a turn of
|
|
437
|
+
# its own after this one. A worker that doesn't queue (its sidecar
|
|
438
|
+
# names no "delivery" feature) would steer a queue, so it is not sent.
|
|
439
|
+
# At the prompt both send the text as typed, a normal turn: no local
|
|
440
|
+
# word or typo check (the worker's Bridge runs a session command as
|
|
441
|
+
# the command).
|
|
442
|
+
# @param route [ComposerRoute::Route]
|
|
435
443
|
# @return [nil]
|
|
436
|
-
def submit_delivery(
|
|
437
|
-
return @screen.commit(STEER_CHOICE_USAGE) if
|
|
438
|
-
return @screen.commit(
|
|
444
|
+
def submit_delivery(route)
|
|
445
|
+
return @screen.commit(STEER_CHOICE_USAGE) if route.kind == :usage
|
|
446
|
+
return @screen.commit(QUEUE_NEEDS_NEWER_WORKER) if Delivery.queue?(route.delivery) && !worker_queues?
|
|
439
447
|
|
|
440
448
|
# To an idle session it is an ordinary prompt: no delivery to send.
|
|
441
|
-
send_prompt(text, delivery:
|
|
449
|
+
send_prompt(route.text, delivery: route.delivery)
|
|
442
450
|
nil
|
|
443
451
|
end
|
|
444
452
|
|
|
453
|
+
# Whether the attached worker runs delivery: queue (its sidecar names
|
|
454
|
+
# Delivery::QUEUE_FEATURE). No sidecar to read: true, its Bridge
|
|
455
|
+
# answers (a P0-P2 worker refuses a queue: #send_prompt).
|
|
456
|
+
def worker_queues?
|
|
457
|
+
sidecar = WorkerSidecar.read(Session.session_dir(@client.session_id, state_dir: @state_dir))
|
|
458
|
+
sidecar.nil? || sidecar.features.include?(Delivery::QUEUE_FEATURE)
|
|
459
|
+
end
|
|
460
|
+
|
|
445
461
|
# @return [Symbol] :detach
|
|
446
462
|
def detach(line)
|
|
447
463
|
@view.finish_thinking_spinner
|
|
@@ -601,9 +617,9 @@ module Samagotchi
|
|
|
601
617
|
end
|
|
602
618
|
|
|
603
619
|
QUEUED_NOTE = "(queued: runs after this turn)"
|
|
604
|
-
|
|
605
|
-
#
|
|
606
|
-
|
|
620
|
+
QUEUE_RUNS_NOW = "(the turn had ended: runs as the next turn)"
|
|
621
|
+
# /queue to a worker from before queueing, which would steer it.
|
|
622
|
+
QUEUE_NEEDS_NEWER_WORKER = "(/queue needs a newer worker: restart it with chi sessions restart, then send again)"
|
|
607
623
|
|
|
608
624
|
# A command queued for after the running turn (command_queued with
|
|
609
625
|
# waits): its line now (another client's) and a note; its command_ran
|
|
@@ -709,14 +725,20 @@ module Samagotchi
|
|
|
709
725
|
# What a prompt's 202 says became of it, or nil when there is nothing
|
|
710
726
|
# to say: the dim line both TUIs print for a delivery. A plain line
|
|
711
727
|
# for a running turn goes in at its next step; a cut's outcome comes
|
|
712
|
-
# from the ack (the worker knows whether cutting is on for its model)
|
|
728
|
+
# from the ack (the worker knows whether cutting is on for its model);
|
|
729
|
+
# a queued one waits for the turn's end (the ack says queue).
|
|
713
730
|
# An idle session's prompt simply runs, and a prompt with images runs
|
|
714
731
|
# as its own next turn (never merging mid-turn), so nothing is said.
|
|
715
732
|
# @param ack [Hash, nil] the Bridge's 202 body
|
|
716
733
|
# @param delivery [String, nil] what was asked for
|
|
717
734
|
# @return [String, nil]
|
|
718
735
|
def sent_note(ack, delivery: nil, images: [], running: false)
|
|
719
|
-
return nil unless running &&
|
|
736
|
+
return nil unless running && ack.is_a?(Hash)
|
|
737
|
+
# The Bridge acks queue only while a turn ran at enqueue; a /queue
|
|
738
|
+
# it acks next_step found the turn over: it is the next turn.
|
|
739
|
+
return QUEUED_NOTE if ack["delivery"] == Delivery::QUEUE
|
|
740
|
+
return QUEUE_RUNS_NOW if Delivery.queue?(delivery)
|
|
741
|
+
return nil unless images.empty?
|
|
720
742
|
|
|
721
743
|
return NEXT_STEP_NOTE unless delivery == Delivery::CUT
|
|
722
744
|
|
|
@@ -748,6 +770,8 @@ module Samagotchi
|
|
|
748
770
|
detail = reply.json&.fetch("error", nil)
|
|
749
771
|
# Read after its deadline and dropped (BridgeClient::DEADLINE_SHARE).
|
|
750
772
|
return @screen.commit("could not send the prompt (#{LATE})") if detail == "deadline_passed"
|
|
773
|
+
# A worker from 0.51 to 0.63 refuses a queue (nothing was sent).
|
|
774
|
+
return @screen.commit(QUEUE_NEEDS_NEWER_WORKER) if detail == "delivery_unavailable"
|
|
751
775
|
|
|
752
776
|
@screen.commit("could not send the prompt (#{[reply.status, detail].compact.join(" ")})")
|
|
753
777
|
rescue SystemCallError, IOError => e
|
|
@@ -1219,7 +1243,7 @@ module Samagotchi
|
|
|
1219
1243
|
# A turn that ended with no answer: its notice, as live.
|
|
1220
1244
|
empty = messages.drop(from + 1).filter_map { |m| TurnNote.empty_answer(m) }.last
|
|
1221
1245
|
if empty
|
|
1222
|
-
@screen.commit(format_empty_answer_line(empty[:retries] || empty["retries"]))
|
|
1246
|
+
@screen.commit(format_empty_answer_line(empty[:retries] || empty["retries"], empty[:refusal] || empty["refusal"]))
|
|
1223
1247
|
elsif failed
|
|
1224
1248
|
# A failed turn whose steps stayed: the line the live view left,
|
|
1225
1249
|
# and no step text as if it were the answer (the web keeps a
|
|
@@ -1359,6 +1383,8 @@ module Samagotchi
|
|
|
1359
1383
|
def render_current_turn(turn)
|
|
1360
1384
|
@running = !turn.nil?
|
|
1361
1385
|
@joined_mid_turn = @running
|
|
1386
|
+
# A turn running at the join: the status row's steer hint.
|
|
1387
|
+
@status.update(running: @running)
|
|
1362
1388
|
return unless turn
|
|
1363
1389
|
|
|
1364
1390
|
continues = turn[:prompt].nil?
|
|
@@ -1411,7 +1437,8 @@ module Samagotchi
|
|
|
1411
1437
|
return if own?(event[:client_id])
|
|
1412
1438
|
|
|
1413
1439
|
@shown_enqueued << event[:enqueued_id]
|
|
1414
|
-
|
|
1440
|
+
line = prompt_line(event[:client_id], event[:prompt])
|
|
1441
|
+
@screen.commit(event[:delivery] == Delivery::QUEUE ? "#{line} #{QUEUED_NOTE}" : line)
|
|
1415
1442
|
end
|
|
1416
1443
|
|
|
1417
1444
|
def start_turn(event)
|
|
@@ -179,7 +179,8 @@ module Samagotchi
|
|
|
179
179
|
end
|
|
180
180
|
# No answer: chi's muted notice, never an empty answer line.
|
|
181
181
|
if summary[:empty_answer]
|
|
182
|
-
|
|
182
|
+
empty = summary[:empty_answer]
|
|
183
|
+
@view.print_line(@view.format_empty_answer_line(empty[:retries], empty[:refusal]))
|
|
183
184
|
else
|
|
184
185
|
@view.print_line(summary[:output])
|
|
185
186
|
end
|
|
@@ -108,14 +108,8 @@ module Samagotchi
|
|
|
108
108
|
CUT_NOW_NOTE = "(cut in now)"
|
|
109
109
|
CUT_WAITS_NOTE = "(cuts in once the thinking passes %d s)"
|
|
110
110
|
CUT_OFF_NOTE = "(cutting is off; goes in at the next step)"
|
|
111
|
+
# A bare /cut or /queue (ComposerRoute's :usage).
|
|
111
112
|
STEER_CHOICE_USAGE = "(/cut and /queue need a message: /cut TEXT, /queue TEXT)"
|
|
112
|
-
# The UIs' own words for a delivery, listed for Tab and /help only
|
|
113
|
-
# (a local: entry, so no command or bundle may use the names).
|
|
114
|
-
CUT_COMMAND = "/cut"
|
|
115
|
-
QUEUE_COMMAND = "/queue"
|
|
116
|
-
# The word, then any whitespace and the text (as the registry's
|
|
117
|
-
# entries match them, so a line Tab or /help knows is never a prompt).
|
|
118
|
-
STEER_CHOICE = /\A(#{CUT_COMMAND}|#{QUEUE_COMMAND})(?:\s+(.*))?\z/m
|
|
119
113
|
|
|
120
114
|
# @param outcome [Symbol] SteerCut#cut_for_steer's answer: :now, :waits or :off
|
|
121
115
|
# @param after [Integer, nil] the run's cut wait (the model's own
|
|
@@ -133,17 +127,6 @@ module Samagotchi
|
|
|
133
127
|
# The same words, for a UI that includes the module.
|
|
134
128
|
def steer_cut_note(outcome, after = nil) = Formatting.steer_cut_note(outcome, after)
|
|
135
129
|
|
|
136
|
-
# The choice a mid-turn line carries, as [:cut, "TEXT"] or
|
|
137
|
-
# [:queue, "TEXT"]; nil for any other line. The text is "" when the
|
|
138
|
-
# line named the choice without one.
|
|
139
|
-
# @param line [String, nil]
|
|
140
|
-
# @return [Array(Symbol, String), nil]
|
|
141
|
-
def steer_choice(line)
|
|
142
|
-
match = STEER_CHOICE.match(line.to_s.strip) or return nil
|
|
143
|
-
|
|
144
|
-
[match[1] == CUT_COMMAND ? :cut : :queue, match[2].to_s.strip]
|
|
145
|
-
end
|
|
146
|
-
|
|
147
130
|
# "✂ forgot 2 outputs, stubbed 1 stale read · frees ~4.1k tokens (paid
|
|
148
131
|
# off)": a batch of LLM context edits went in (LLMContextNotice builds
|
|
149
132
|
# the line; the web prints the same), dim like the retry row.
|
|
@@ -152,9 +135,10 @@ module Samagotchi
|
|
|
152
135
|
end
|
|
153
136
|
|
|
154
137
|
# "no answer: the model returned nothing (after 1 retry)", dim like the
|
|
155
|
-
# retry row: a turn that ended with no answer
|
|
156
|
-
|
|
157
|
-
|
|
138
|
+
# retry row: a turn that ended with no answer, or "no answer: refused
|
|
139
|
+
# by the provider (cyber)" for a +refusal+ (TurnNote.empty_answer_line).
|
|
140
|
+
def format_empty_answer_line(retries, refusal = nil)
|
|
141
|
+
paint(TurnNote.empty_answer_line(retries, refusal: refusal), 90)
|
|
158
142
|
end
|
|
159
143
|
|
|
160
144
|
# "↻ retrying (503) in 4.0s, 2/5": the live status while the provider
|
|
@@ -363,7 +347,8 @@ module Samagotchi
|
|
|
363
347
|
|
|
364
348
|
# A cancel's reason as the UIs name it (anything else as it is);
|
|
365
349
|
# spec/shared/labels_matrix.json pins it for both.
|
|
366
|
-
CANCEL_REASONS = { "ctrl_c" => "Ctrl-C", "user" => "stopped", "hook" => "by a hook", "worker_stopped" => "worker stopped"
|
|
350
|
+
CANCEL_REASONS = { "ctrl_c" => "Ctrl-C", "user" => "stopped", "hook" => "by a hook", "worker_stopped" => "worker stopped",
|
|
351
|
+
"session_stopped" => "session stopped" }.freeze
|
|
367
352
|
# A canceled turn's mark: a stop someone chose (Ctrl-C, Stop, a hook),
|
|
368
353
|
# not an error's ✕ (timing.js STOP_MARK; the labels matrix pins it).
|
|
369
354
|
STOP_MARK = "■"
|
|
@@ -28,7 +28,15 @@ module Samagotchi
|
|
|
28
28
|
# preloaded its --memory list (shown until a turn
|
|
29
29
|
# records them as used)
|
|
30
30
|
# muted its --mute list
|
|
31
|
-
|
|
31
|
+
# running a turn runs: the row trails STEER_HINT
|
|
32
|
+
FIELDS = %i[model default_model served parent_id children context used_memories preloaded muted running].freeze
|
|
33
|
+
|
|
34
|
+
# The tail while a turn runs: where a typed line goes (the next step)
|
|
35
|
+
# and the two other choices. Dropped whole first when the row is cut.
|
|
36
|
+
STEER_HINT = " · typing goes in at the next step (/cut, /queue)"
|
|
37
|
+
|
|
38
|
+
# The turn events that end a turn (the hint goes).
|
|
39
|
+
TURN_END_EVENTS = %i[turn_completed turn_canceled turn_failed].freeze
|
|
32
40
|
|
|
33
41
|
# @param surface [Surface] with #columns
|
|
34
42
|
def initialize(surface)
|
|
@@ -77,13 +85,16 @@ module Samagotchi
|
|
|
77
85
|
|
|
78
86
|
# Take what a turn event says about the row (the Engine's events in
|
|
79
87
|
# the REPL, the Bridge's in attached mode): ctx, the memories used,
|
|
80
|
-
# the model the server served
|
|
88
|
+
# the model the server served, whether a turn runs (the steer
|
|
89
|
+
# hint). Other events change nothing.
|
|
81
90
|
def take_event(event)
|
|
82
91
|
case event[:type]
|
|
83
92
|
when :context_status then update(context: { est_pct: event.dig(:usage, :estimated_pct), bucket: event[:bucket] })
|
|
84
93
|
when :used_memories_updated then update(used_memories: Array(event[:used_memory_names]))
|
|
85
94
|
when :generation_completed
|
|
86
95
|
update(served: [event[:served_model], event[:requested_model], event[:served_expected_by]]) if event[:served_model]
|
|
96
|
+
when :turn_started then update(running: true)
|
|
97
|
+
when *TURN_END_EVENTS then update(running: false)
|
|
87
98
|
end
|
|
88
99
|
end
|
|
89
100
|
|
|
@@ -119,11 +130,21 @@ module Samagotchi
|
|
|
119
130
|
status_context_text(estimate: @values[:context]),
|
|
120
131
|
status_memory_text(Array(@values[:used_memories]) | Array(@values[:preloaded]), MEMORY_LIMIT),
|
|
121
132
|
status_memory_text(@values[:muted], MEMORY_LIMIT, label: "muted")].reject(&:empty?)
|
|
122
|
-
status_rows(segments, width)
|
|
133
|
+
status_rows(with_steer_hint(segments, width), width)
|
|
123
134
|
end
|
|
124
135
|
|
|
125
136
|
private
|
|
126
137
|
|
|
138
|
+
# +segments+ with STEER_HINT after the last while a turn runs, when
|
|
139
|
+
# the whole row fits +width+ with it: on a narrow terminal the hint
|
|
140
|
+
# goes first, whole, before anything else is cut.
|
|
141
|
+
def with_steer_hint(segments, width)
|
|
142
|
+
return segments unless @values[:running] && segments.any?
|
|
143
|
+
return segments if "status> #{segments.join(" | ")}#{STEER_HINT}".length > width
|
|
144
|
+
|
|
145
|
+
segments[0..-2] + ["#{segments.last}#{STEER_HINT}"]
|
|
146
|
+
end
|
|
147
|
+
|
|
127
148
|
# `⑂ 2 running · 1 waiting`, the counts there are; "" with neither (no
|
|
128
149
|
# children, or all ended). Not "not reported": a REPL parent is never
|
|
129
150
|
# given its children's replies (no wakes there), and a worker moves
|
|
@@ -25,6 +25,7 @@ require_relative "tools/memory"
|
|
|
25
25
|
require_relative "output_formatter"
|
|
26
26
|
require_relative "turn_flow"
|
|
27
27
|
require_relative "session_commands"
|
|
28
|
+
require_relative "composer_route"
|
|
28
29
|
require_relative "terminal_ui/attached_view"
|
|
29
30
|
require_relative "terminal_ui/between_turns"
|
|
30
31
|
require_relative "terminal_ui/event_renderer"
|
|
@@ -296,11 +297,11 @@ module Samagotchi
|
|
|
296
297
|
|
|
297
298
|
if @prompt && @non_interactive
|
|
298
299
|
# A /cut TEXT / /queue TEXT prompt runs its text as the turn's
|
|
299
|
-
# prompt, the same as the idle REPL does (
|
|
300
|
+
# prompt, the same as the idle REPL does (ComposerRoute.steer_choice);
|
|
300
301
|
# a bare /cut / /queue (no text) is refused above as a UI command,
|
|
301
302
|
# so only the text here. The images are the text's, not the words'.
|
|
302
303
|
one_shot_prompt =
|
|
303
|
-
if (choice = steer_choice(@prompt)) && !choice.last.empty?
|
|
304
|
+
if (choice = ComposerRoute.steer_choice(@prompt)) && !choice.last.empty?
|
|
304
305
|
choice.last
|
|
305
306
|
else
|
|
306
307
|
@prompt
|
|
@@ -834,34 +835,35 @@ module Samagotchi
|
|
|
834
835
|
return if input.empty?
|
|
835
836
|
|
|
836
837
|
# /cut TEXT and /queue TEXT are deliveries only while a turn runs; at
|
|
837
|
-
# the prompt both run the text as a normal turn, as their usage says
|
|
838
|
-
|
|
839
|
-
|
|
838
|
+
# the prompt both run the text as a normal turn, as their usage says
|
|
839
|
+
# (the route is the text's).
|
|
840
|
+
route = ComposerRoute.call(input, running: false, registry: command_registry)
|
|
841
|
+
return @surface.commit(STEER_CHOICE_USAGE) if route.kind == :usage
|
|
840
842
|
|
|
841
|
-
|
|
842
|
-
end
|
|
843
|
+
input = route.text if route.choice
|
|
843
844
|
|
|
844
845
|
# /model, /models, !rollback, !cmd, /continue (shared with workers);
|
|
845
846
|
# an anytime command's cards print as it shows them (btw's
|
|
846
847
|
# "thinking…" before the answer). Piped input has nobody to answer a
|
|
847
848
|
# confirm (the next line is already written): a costly /thinking or
|
|
848
849
|
# /reasoning-history applies at once.
|
|
849
|
-
|
|
850
|
-
command
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
850
|
+
case route.kind
|
|
851
|
+
when :command
|
|
852
|
+
confirm = !piped_input?
|
|
853
|
+
command = if command_registry.anytime?(input)
|
|
854
|
+
@engine.running_anytime { @commands.run(input, confirm: confirm) }
|
|
855
|
+
else
|
|
856
|
+
@commands.run(input, confirm: confirm)
|
|
857
|
+
end
|
|
858
|
+
if command
|
|
859
|
+
show_command_result(command)
|
|
860
|
+
persist_recent_history(input) if command.shell
|
|
861
|
+
return
|
|
862
|
+
end
|
|
863
|
+
when :local then return if show_local_command(input)
|
|
862
864
|
# A typo of a command (/modle) is not a prompt: say so, send nothing.
|
|
863
|
-
|
|
864
|
-
|
|
865
|
+
when :typo then return @surface.commit(route.reply)
|
|
866
|
+
end
|
|
865
867
|
|
|
866
868
|
@turn_flow.before_prompt_turn
|
|
867
869
|
persist_recent_history(input)
|
|
@@ -1164,29 +1166,42 @@ module Samagotchi
|
|
|
1164
1166
|
#
|
|
1165
1167
|
# A command that waits for the turn's end (/model X, !cmd) and every line
|
|
1166
1168
|
# after it wait in @after_turn, in the order typed (D1), with the image
|
|
1167
|
-
# lines (which never steer): once the turn ends they come
|
|
1168
|
-
# steering it didn't merge
|
|
1169
|
-
# after
|
|
1169
|
+
# lines (which never steer) and /queue's: once the turn ends they come
|
|
1170
|
+
# back with the steering it didn't merge, in the order sent (each line
|
|
1171
|
+
# the turn takes gets the next @input_seq), ahead of anything sent after
|
|
1172
|
+
# Ctrl-C.
|
|
1170
1173
|
def with_steering(&)
|
|
1171
1174
|
return yield unless @repl_input
|
|
1172
1175
|
|
|
1173
1176
|
@after_turn = []
|
|
1174
1177
|
@command_waits = false
|
|
1175
|
-
@
|
|
1178
|
+
@input_seq = 0
|
|
1179
|
+
@repl_input.during_turn(method(:steer_line), leftovers: -> { leftover_lines }, &)
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
# The turn's leftovers in send order: steering it didn't merge and the
|
|
1183
|
+
# lines that waited for its end.
|
|
1184
|
+
def leftover_lines
|
|
1185
|
+
(@pending_input_queue.drain_entries + lines_after_turn).sort_by { |seq, _| seq || 0 }.map(&:last)
|
|
1176
1186
|
end
|
|
1177
1187
|
|
|
1178
|
-
# The
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1188
|
+
# The next line's place in send order (#leftover_lines).
|
|
1189
|
+
def next_input_seq
|
|
1190
|
+
@input_seq = (@input_seq || 0) + 1
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
# The [seq, line] entries that waited for the turn's end, taken from
|
|
1194
|
+
# @after_turn. After a canceled or failed turn (Ctrl-C raises through
|
|
1195
|
+
# here; a failure too) a !cmd among them is dropped (D4): running it
|
|
1196
|
+
# would close the !rollback the turn left.
|
|
1182
1197
|
def lines_after_turn
|
|
1183
|
-
|
|
1198
|
+
entries = @after_turn.slice!(0..)
|
|
1184
1199
|
ended = if @active_cancel_controller&.cancelled? || $!.is_a?(Interrupt) then "canceled"
|
|
1185
1200
|
elsif $! then "failed"
|
|
1186
1201
|
end
|
|
1187
|
-
return
|
|
1202
|
+
return entries unless ended
|
|
1188
1203
|
|
|
1189
|
-
|
|
1204
|
+
entries.reject do |_seq, line|
|
|
1190
1205
|
next false unless command_registry.lookup(line)&.id == :shell
|
|
1191
1206
|
|
|
1192
1207
|
@surface.commit("turn #{ended}: #{line.strip} not run; send it again")
|
|
@@ -1207,28 +1222,32 @@ module Samagotchi
|
|
|
1207
1222
|
# to end and runs as a turn of its own.
|
|
1208
1223
|
# @return [Boolean, :back] whether the turn took it, :back to put it back
|
|
1209
1224
|
def steer_line(line)
|
|
1210
|
-
|
|
1225
|
+
route = ComposerRoute.call(line, running: true, registry: command_registry) unless line.nil?
|
|
1226
|
+
local = route&.local
|
|
1211
1227
|
return exit_after_turn(delete: SessionCommands.delete_on_exit?(line)) if line.nil? || local == :exit
|
|
1212
1228
|
return archive_after_turn if local == :archive
|
|
1213
1229
|
return detach_note if local == :detach
|
|
1214
1230
|
return false if @active_cancel_controller&.cancelled?
|
|
1215
1231
|
|
|
1216
|
-
|
|
1217
|
-
#
|
|
1218
|
-
|
|
1219
|
-
|
|
1232
|
+
case route.kind
|
|
1233
|
+
# Before every command check (ComposerRoute): /cut and /queue are the
|
|
1234
|
+
# UIs' own words for a delivery (listed for Tab and /help, never run).
|
|
1235
|
+
when :usage
|
|
1236
|
+
@surface.commit(STEER_CHOICE_USAGE)
|
|
1237
|
+
return true
|
|
1238
|
+
when :deliver then return submit_steer_choice(route.delivery, route.text)
|
|
1239
|
+
when :command
|
|
1240
|
+
policy = command_registry.mid_turn(line)
|
|
1241
|
+
return start_anytime_command(line) if policy == :anytime
|
|
1242
|
+
return command_queued(line) if policy == :queue
|
|
1220
1243
|
end
|
|
1221
|
-
|
|
1222
|
-
policy = command_registry.mid_turn(line)
|
|
1223
|
-
return start_anytime_command(line) if policy == :anytime
|
|
1224
|
-
return command_queued(line) if policy == :queue
|
|
1225
1244
|
# A command, never steering text.
|
|
1226
|
-
return command_during_turn(line) if local
|
|
1227
|
-
return true if
|
|
1245
|
+
return command_during_turn(line) if %i[local command].include?(route.kind)
|
|
1246
|
+
return true if route.kind == :empty
|
|
1228
1247
|
|
|
1229
1248
|
# A typo of a command (/modle) is not steering text: say so, take it.
|
|
1230
|
-
if
|
|
1231
|
-
@surface.commit(
|
|
1249
|
+
if route.kind == :typo
|
|
1250
|
+
@surface.commit(route.reply)
|
|
1232
1251
|
return true
|
|
1233
1252
|
end
|
|
1234
1253
|
# Steering merges text only: a line with images runs as the next turn.
|
|
@@ -1236,7 +1255,7 @@ module Samagotchi
|
|
|
1236
1255
|
# Behind a command that waits for the turn's end, a line waits too.
|
|
1237
1256
|
return wait_for_turn_end(line) if @command_waits
|
|
1238
1257
|
|
|
1239
|
-
@pending_input_queue.push(
|
|
1258
|
+
@pending_input_queue.push(route.text, seq: next_input_seq)
|
|
1240
1259
|
@surface.commit(NEXT_STEP_NOTE)
|
|
1241
1260
|
true
|
|
1242
1261
|
end
|
|
@@ -1245,25 +1264,21 @@ module Samagotchi
|
|
|
1245
1264
|
# otherwise read as prompts (SessionCommands registers them for Tab and
|
|
1246
1265
|
# help only). A cut asks the engine to cut the generation only thinking
|
|
1247
1266
|
# too long; a queue waits for the turn's end (the REPL's own after-turn
|
|
1248
|
-
# list).
|
|
1249
|
-
#
|
|
1267
|
+
# list). A line with images never steers (as a plain one doesn't).
|
|
1268
|
+
# @param delivery [String] Delivery::CUT or Delivery::QUEUE (ComposerRoute)
|
|
1250
1269
|
# @return [Boolean] whether the turn took it
|
|
1251
|
-
def submit_steer_choice(
|
|
1252
|
-
if text.empty?
|
|
1253
|
-
@surface.commit(STEER_CHOICE_USAGE)
|
|
1254
|
-
return true
|
|
1255
|
-
end
|
|
1270
|
+
def submit_steer_choice(delivery, text)
|
|
1256
1271
|
return image_line_waits(text) if ImageInput.extract(text).any?
|
|
1257
1272
|
|
|
1258
1273
|
# A /queue waits for the turn's end; a /cut behind a command that
|
|
1259
1274
|
# already waits there goes after it too.
|
|
1260
|
-
if
|
|
1275
|
+
if Delivery.queue?(delivery) || @command_waits
|
|
1261
1276
|
@surface.commit(COMMAND_QUEUED)
|
|
1262
1277
|
return wait_for_turn_end(text)
|
|
1263
1278
|
end
|
|
1264
1279
|
|
|
1265
1280
|
epoch = @engine.input_epoch
|
|
1266
|
-
@pending_input_queue.push(text)
|
|
1281
|
+
@pending_input_queue.push(text, seq: next_input_seq)
|
|
1267
1282
|
# With no session running the cut is :off; the text still waits for
|
|
1268
1283
|
# the running turn's next step.
|
|
1269
1284
|
@surface.commit(steer_cut_note(@engine.cut_for_steer(nil, epoch: epoch), @engine.steer_cut_after))
|
|
@@ -1278,7 +1293,7 @@ module Samagotchi
|
|
|
1278
1293
|
# +line+ runs once the turn has ended, in the order typed (#with_steering).
|
|
1279
1294
|
# @return [true] the turn took it
|
|
1280
1295
|
def wait_for_turn_end(line)
|
|
1281
|
-
@after_turn << line
|
|
1296
|
+
@after_turn << [next_input_seq, line]
|
|
1282
1297
|
true
|
|
1283
1298
|
end
|
|
1284
1299
|
|
|
@@ -211,14 +211,14 @@ module Samagotchi
|
|
|
211
211
|
# "stopped" for a
|
|
212
212
|
# task_wait the user's Stop ended (the task itself runs on), for one
|
|
213
213
|
# whose task was stopped (the stop-task button, or the model's task_stop),
|
|
214
|
-
# and for a command the user's Stop (or a signal to chi
|
|
215
|
-
# from starting: a cancel, not a failure (its text still
|
|
216
|
-
# for the model).
|
|
214
|
+
# and for a command the user's Stop (or a signal to chi, a session stop)
|
|
215
|
+
# killed or kept from starting: a cancel, not a failure (its text still
|
|
216
|
+
# starts "Error:" for the model).
|
|
217
217
|
def tool_activity_status(result, tool_name = nil, command: nil)
|
|
218
218
|
text = result.to_s
|
|
219
|
-
return "stopped" if [Tools::Execute::NOT_RUN_ON_STOP, Tools::Execute::
|
|
219
|
+
return "stopped" if [Tools::Execute::NOT_RUN_ON_STOP, *Tools::Execute::NOT_RUN.values].include?(text)
|
|
220
220
|
if tool_name == Tools::Execute::NAME &&
|
|
221
|
-
text.start_with?(Tools::Execute::STOPPED_BY_USER, Tools::Execute::
|
|
221
|
+
text.start_with?(Tools::Execute::STOPPED_BY_USER, *Tools::Execute::STOPPED_BY.values)
|
|
222
222
|
return "stopped"
|
|
223
223
|
end
|
|
224
224
|
return "error" if text.start_with?("Error:")
|
|
@@ -203,7 +203,9 @@ module Samagotchi
|
|
|
203
203
|
delivered = SessionManager.deliver_turn(id, prompt: task, client_id: "#{ClientId::DELEGATE_PREFIX}#{parent.id[0, 8]}",
|
|
204
204
|
state_dir: state_dir)
|
|
205
205
|
case delivered[:status]
|
|
206
|
-
when :accepted
|
|
206
|
+
when :accepted
|
|
207
|
+
DelegateWait.mark_turn(parent.id, id, follow_up_turn(id, delivered[:ack], state_dir: state_dir), state_dir: state_dir)
|
|
208
|
+
id
|
|
207
209
|
when :refused then "Error: session #{id[0, 8]} refused the message (#{delivered.dig(:ack, "error")})"
|
|
208
210
|
when :timeout then "Error: the worker of session #{id[0, 8]} did not answer in time, so the message was not sent"
|
|
209
211
|
else "Error: the message to session #{id[0, 8]} could not be written"
|
|
@@ -215,6 +217,31 @@ module Samagotchi
|
|
|
215
217
|
end
|
|
216
218
|
private_class_method :follow_up
|
|
217
219
|
|
|
220
|
+
# The child's turn a delivered follow-up joins, for the wait
|
|
221
|
+
# (DelegateCursor#turn): the running turn's (its checkpoint mark), the
|
|
222
|
+
# follow-up merging into it; else the ack's enqueued_id, the follow-up
|
|
223
|
+
# running as a turn of its own. Only from a worker that names
|
|
224
|
+
# turn_ids; nil otherwise, or when the running turn has no id (a
|
|
225
|
+
# delegate's first turn): the wait takes the next reply.
|
|
226
|
+
# A follow-up steered into a turn that ended before it merged runs as
|
|
227
|
+
# its own turn, yet the wait is on the ended one (LOW, as before).
|
|
228
|
+
# One the child's idle look missed being merged into a turn starting
|
|
229
|
+
# then never runs as its own: ReplyWait falls back to that turn's
|
|
230
|
+
# reply (merged_elsewhere?).
|
|
231
|
+
# @return [String, nil]
|
|
232
|
+
def self.follow_up_turn(id, ack, state_dir:)
|
|
233
|
+
return nil unless ReplyWait.turn_ids?(Session.session_dir(id, state_dir: state_dir))
|
|
234
|
+
|
|
235
|
+
child = Session.load(id, state_dir: state_dir)
|
|
236
|
+
return child.interrupted_turn&.enqueued_id if child.status == Session::STATUS_RUNNING
|
|
237
|
+
|
|
238
|
+
enqueued_id = ack.is_a?(Hash) ? (ack["enqueued_id"] || ack[:enqueued_id]) : nil
|
|
239
|
+
enqueued_id.to_s.empty? ? nil : enqueued_id.to_s
|
|
240
|
+
rescue ArgumentError
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
private_class_method :follow_up_turn
|
|
244
|
+
|
|
218
245
|
# The model as typed (spawn_session stores its resolved ref), else the
|
|
219
246
|
# parent's. No existence check here: spawn_session checks the host
|
|
220
247
|
# (check_host!, its UnknownHost comes back as this tool's Error: line)
|
|
@@ -10,9 +10,14 @@ module Samagotchi
|
|
|
10
10
|
# What a parent was already told about one delegate child: the newest
|
|
11
11
|
# reply it was given (+reply_file+, ReplyWait's cursor) and the child as
|
|
12
12
|
# it was then (+messages+, +question_id+, +last_turn+: ReplyWait's
|
|
13
|
-
# baseline). nil +messages+ means no baseline.
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
# baseline). nil +messages+ means no baseline. +turn+: the enqueued_id
|
|
14
|
+
# of the child's turn the last follow-up went to (Delegate.follow_up),
|
|
15
|
+
# whose own reply a wait takes (ReplyWait's turn:); nil once that turn
|
|
16
|
+
# was reported, or when unknown (a new child, an older worker).
|
|
17
|
+
DelegateCursor = Data.define(:reply_file, :last_turn, :question_id, :messages, :turn) do
|
|
18
|
+
def initialize(reply_file: nil, last_turn: nil, question_id: nil, messages: nil, turn: nil) = super
|
|
19
|
+
|
|
20
|
+
def self.empty = new
|
|
16
21
|
|
|
17
22
|
# @param baseline [Hash, nil] ReplyWait.baseline_of's
|
|
18
23
|
def with_baseline(baseline)
|
|
@@ -93,7 +98,7 @@ module Samagotchi
|
|
|
93
98
|
return DelegateCursor.empty unless data.is_a?(Hash)
|
|
94
99
|
|
|
95
100
|
DelegateCursor.new(reply_file: data["reply_file"], last_turn: data["last_turn"],
|
|
96
|
-
question_id: data["question_id"], messages: data["messages"])
|
|
101
|
+
question_id: data["question_id"], messages: data["messages"], turn: data["turn"])
|
|
97
102
|
end
|
|
98
103
|
|
|
99
104
|
def path(parent_id, state_dir:)
|