samagotchi 0.102.0 → 0.103.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 +40 -1
- data/bin/chi +23 -191
- data/docs/architecture.md +54 -0
- data/docs/cli.md +114 -7
- data/docs/configuration.md +7 -1
- data/docs/guardrails.md +2 -1
- data/docs/internals/engine.md +1 -1
- data/docs/internals/llm-context-forget.md +11 -1
- data/docs/internals/remote-sessions.md +303 -0
- data/docs/internals/working-set.md +95 -11
- data/docs/plugins.md +26 -2
- data/docs/sessions.md +12 -5
- data/docs/sub-agent.md +8 -0
- data/lib/samagotchi/answer_command.rb +27 -25
- data/lib/samagotchi/bridge.rb +24 -4
- data/lib/samagotchi/bridge_client/event_stream.rb +81 -12
- data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
- data/lib/samagotchi/bridge_client.rb +22 -38
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/chi_invocation.rb +43 -0
- data/lib/samagotchi/cli/command.rb +3 -2
- data/lib/samagotchi/cli/flags.rb +5 -0
- data/lib/samagotchi/cli/parent_wait.rb +22 -13
- data/lib/samagotchi/context_command.rb +80 -73
- data/lib/samagotchi/engine.rb +12 -8
- data/lib/samagotchi/idle_recap.rb +7 -1
- data/lib/samagotchi/kernel_loop.rb +13 -1
- data/lib/samagotchi/launch_mode.rb +22 -0
- data/lib/samagotchi/llm_context_forget.rb +13 -6
- data/lib/samagotchi/note_command.rb +8 -7
- data/lib/samagotchi/parent_report.rb +55 -43
- data/lib/samagotchi/remote_command.rb +170 -0
- data/lib/samagotchi/remotes.rb +130 -0
- data/lib/samagotchi/run_options.rb +233 -0
- data/lib/samagotchi/send_command/busy.rb +58 -0
- data/lib/samagotchi/send_command/parent_gate.rb +23 -10
- data/lib/samagotchi/send_command/wait_target.rb +16 -3
- data/lib/samagotchi/send_command.rb +173 -108
- data/lib/samagotchi/session.rb +30 -5
- data/lib/samagotchi/session_access/contract.rb +129 -0
- data/lib/samagotchi/session_access/dispatch.rb +133 -0
- data/lib/samagotchi/session_access/http_transport.rb +109 -0
- data/lib/samagotchi/session_access/local.rb +349 -0
- data/lib/samagotchi/session_access/remote.rb +638 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +181 -0
- data/lib/samagotchi/session_access.rb +141 -0
- data/lib/samagotchi/session_archive_command.rb +9 -5
- data/lib/samagotchi/session_commands.rb +20 -15
- data/lib/samagotchi/session_delete_command.rb +8 -4
- data/lib/samagotchi/session_manager.rb +7 -2
- data/lib/samagotchi/session_setup.rb +11 -6
- data/lib/samagotchi/sessions_command.rb +36 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +51 -15
- data/lib/samagotchi/terminal_ui/attach_link.rb +104 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +80 -75
- data/lib/samagotchi/terminal_ui/local_link.rb +130 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +5 -3
- data/lib/samagotchi/terminal_ui.rb +2 -2
- data/lib/samagotchi/title_command.rb +9 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +360 -69
- data/lib/samagotchi/web/public/data.js +3 -2
- data/lib/samagotchi/web/public/index.html +20 -4
- data/lib/samagotchi/web/public/stage_frame.js +7 -2
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/stream_handlers.js +3 -1
- data/lib/samagotchi/web/public/working_set.js +159 -28
- data/lib/samagotchi/web/public/working_set_rail.js +39 -12
- data/lib/samagotchi/web/public/working_set_view.js +91 -33
- data/lib/samagotchi/web/session_access_routes.rb +343 -0
- data/lib/samagotchi/worker_sidecar.rb +12 -5
- data/lib/samagotchi/working_set.rb +88 -14
- data/lib/samagotchi/working_set_command.rb +133 -30
- data/lib/samagotchi/working_set_pin.rb +34 -0
- data/lib/samagotchi/working_set_steer.rb +191 -27
- metadata +19 -1
|
@@ -25,6 +25,8 @@ require_relative "cli/flags"
|
|
|
25
25
|
require_relative "cli/parent_wait"
|
|
26
26
|
require_relative "send_command/parent_gate"
|
|
27
27
|
require_relative "send_command/wait_target"
|
|
28
|
+
require_relative "send_command/busy"
|
|
29
|
+
require_relative "session_access/local"
|
|
28
30
|
|
|
29
31
|
module Samagotchi
|
|
30
32
|
# `chi send`: put text into sessions as the user's message, the same as
|
|
@@ -67,7 +69,8 @@ module Samagotchi
|
|
|
67
69
|
--new start a new session with the message instead, as the
|
|
68
70
|
web does, and print its id
|
|
69
71
|
--dir DIR (--new) its folder, the project it belongs to; default
|
|
70
|
-
the current one
|
|
72
|
+
the current one (with --remote: required, that
|
|
73
|
+
machine's absolute path)
|
|
71
74
|
--continues ID
|
|
72
75
|
(--new) start the next link of that session's chain:
|
|
73
76
|
in its folder, on its model (or --model's; one whose
|
|
@@ -145,8 +148,9 @@ module Samagotchi
|
|
|
145
148
|
queue), with --cut also cut (now, waits, off)
|
|
146
149
|
Exit with --wait: 0 answered, 1 failed or gone, 2 usage, 3 a
|
|
147
150
|
question waits, 4 still running (--timeout), 130 Ctrl-C.
|
|
148
|
-
|
|
149
|
-
or web page, not
|
|
151
|
+
Sessions on this machine, or another's: chi --remote NAME send …
|
|
152
|
+
(chi remote). Answers show in the attached TUI or web page, not
|
|
153
|
+
here.
|
|
150
154
|
Find ids with: chi sessions list --live [--scope=all] [--format tsv]
|
|
151
155
|
TEXT
|
|
152
156
|
|
|
@@ -206,18 +210,44 @@ module Samagotchi
|
|
|
206
210
|
|
|
207
211
|
UNKNOWN_VISION = VisionSupport::Answer.new(value: nil, reason: nil)
|
|
208
212
|
|
|
213
|
+
# What to say when +answer+ (.vision_answer's for +model_name+) is a
|
|
214
|
+
# no, or nil: the words chi send and chi web's turn and create routes
|
|
215
|
+
# refuse images with.
|
|
216
|
+
def self.vision_refusal(model_name, answer)
|
|
217
|
+
return nil unless answer.no?
|
|
218
|
+
|
|
219
|
+
name = model_name.to_s.strip
|
|
220
|
+
name = Samagotchi::Config.get("default.model").to_s.strip if name.empty?
|
|
221
|
+
name = "the model" if name.empty?
|
|
222
|
+
"#{name} can't take images (#{answer.reason}); send text only or switch the model (/model)"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# The model a --continues link of +previous+ runs, for the images
|
|
226
|
+
# check: +model+'s (blank: the previous link's, else the default it
|
|
227
|
+
# falls back to; SessionContinue.model_for).
|
|
228
|
+
# @return [Array(String, String)] [model_name, model_typed]
|
|
229
|
+
# @raise [ModelProfile::MissingModel] a model it can't start on
|
|
230
|
+
def self.continued_model(previous, model)
|
|
231
|
+
choice = SessionContinue.model_for(previous, model)
|
|
232
|
+
inherited = model.to_s.strip.empty? && choice.warning.nil?
|
|
233
|
+
inherited ? [previous.model_name, previous.model_typed] : [choice.model, nil]
|
|
234
|
+
end
|
|
235
|
+
|
|
209
236
|
# A no from the config (models: / hosts: vision: false) holds without
|
|
210
237
|
# the server.
|
|
211
238
|
def self.configured_no?(answer) = answer.reason.to_s.include?("sets vision: false")
|
|
212
239
|
private_class_method :configured_no?
|
|
213
240
|
|
|
214
241
|
# @param argv [Array<String>] the arguments after "send"
|
|
215
|
-
|
|
242
|
+
# @param access [SessionAccess::Local, nil] where the sessions are;
|
|
243
|
+
# default this machine's, in +state_dir+
|
|
244
|
+
def initialize(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, state_dir: nil, access: nil)
|
|
216
245
|
@argv = argv.dup
|
|
217
246
|
@stdin = stdin
|
|
218
247
|
@stdout = stdout
|
|
219
248
|
@stderr = stderr
|
|
220
249
|
@state_dir = state_dir || Session.default_state_dir
|
|
250
|
+
@access = access || SessionAccess::Local.new(state_dir: @state_dir)
|
|
221
251
|
end
|
|
222
252
|
|
|
223
253
|
private
|
|
@@ -259,17 +289,24 @@ module Samagotchi
|
|
|
259
289
|
|
|
260
290
|
# A parent agent's /continue yes held to guardrails.parent_answers,
|
|
261
291
|
# and a /parent-answers that loosens: refused before anything is sent.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
292
|
+
# Machine-local (the targets' session files, this machine's
|
|
293
|
+
# config.yml): remotely the server's (its /turn and create routes
|
|
294
|
+
# check the line chi send's client_id marks; the worker checks a
|
|
295
|
+
# CLI_ANSWER /continue again).
|
|
296
|
+
unless @access.remote?
|
|
297
|
+
gate = ParentGate.new(options, state_dir: @state_dir)
|
|
298
|
+
if prompt && (refusal = gate.continue_refusal(prompt))
|
|
299
|
+
@stderr.puts("chi send: #{refusal}")
|
|
300
|
+
return CLI::Exit::FAILED
|
|
301
|
+
end
|
|
302
|
+
if prompt && gate.loosens?(prompt)
|
|
303
|
+
error_line("chi send: refused: #{ParentAnswersCommand::REFUSED}")
|
|
304
|
+
return CLI::Exit::FAILED
|
|
305
|
+
end
|
|
270
306
|
end
|
|
271
307
|
# A file that is not there or not an image: a run-time failure (1),
|
|
272
308
|
# reported in the --format json line too; --image itself was fine.
|
|
309
|
+
# Client-side on any backend: the files are this machine's.
|
|
273
310
|
return CLI::Exit::FAILED unless ingest_images(options[:images])
|
|
274
311
|
|
|
275
312
|
return run_new(prompt, options) if options[:new]
|
|
@@ -372,6 +409,8 @@ module Samagotchi
|
|
|
372
409
|
|
|
373
410
|
options[:parent_answers] = level
|
|
374
411
|
end
|
|
412
|
+
return remote_new_options(options, words) if @access.remote?
|
|
413
|
+
|
|
375
414
|
toolkit = Toolkit.from_flags(name: options[:toolkit], add: options[:tools], remove: options[:no_tools])
|
|
376
415
|
options[:toolkit_value] = toolkit if toolkit
|
|
377
416
|
begin
|
|
@@ -387,6 +426,35 @@ module Samagotchi
|
|
|
387
426
|
options
|
|
388
427
|
end
|
|
389
428
|
|
|
429
|
+
# --new against a remote: its folder is given, as that machine's
|
|
430
|
+
# absolute path (D4: none is guessed from this one's), and the setup
|
|
431
|
+
# flags go as typed (SetupWords): the toolkit's default, the bundles a
|
|
432
|
+
# --set names and the folder are the remote's to check.
|
|
433
|
+
def remote_new_options(options, llm_words)
|
|
434
|
+
unless options[:continues]
|
|
435
|
+
# One line: the fix is in it, the whole usage would bury it.
|
|
436
|
+
return usage_error("--new on #{remote_name} needs --dir: a folder there", usage: nil) unless options[:dir]
|
|
437
|
+
unless options[:dir].start_with?("/")
|
|
438
|
+
return usage_error("--dir is a folder on #{remote_name}: give its absolute path", usage: nil)
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
settings = Array(options[:sets]).to_h do |pair|
|
|
443
|
+
key, sep, value = pair.partition("=")
|
|
444
|
+
return usage_error("--set takes KEY=VALUE, not #{pair}") if sep.empty? || key.strip.empty?
|
|
445
|
+
|
|
446
|
+
[key.strip, value]
|
|
447
|
+
end
|
|
448
|
+
options[:setup_words] = SessionAccess::SetupWords.new(
|
|
449
|
+
llm_context: (llm_words unless llm_words.empty?), thinking: options[:thinking]&.to_s,
|
|
450
|
+
toolkit: options[:toolkit], tools: Array(options[:tools]), no_tools: Array(options[:no_tools]),
|
|
451
|
+
settings: (settings unless settings.empty?), parent_answers: options[:parent_answers]
|
|
452
|
+
)
|
|
453
|
+
options
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def remote_name = @access.respond_to?(:name) ? @access.name : "the remote"
|
|
457
|
+
|
|
390
458
|
# With both, stdin is the context quoted above the message; with one,
|
|
391
459
|
# it goes in as is. nil when both are blank.
|
|
392
460
|
def compose(context, message)
|
|
@@ -409,6 +477,8 @@ module Samagotchi
|
|
|
409
477
|
model, typed = continued_model(options)
|
|
410
478
|
return 1 if model == false
|
|
411
479
|
|
|
480
|
+
# Machine-local (this machine's model registry): remotely the
|
|
481
|
+
# server's (P2: the create route checks vision).
|
|
412
482
|
if images && (refusal = vision_refusal(model, typed: typed))
|
|
413
483
|
error_line("chi send: refused: #{refusal}")
|
|
414
484
|
return 1
|
|
@@ -419,21 +489,11 @@ module Samagotchi
|
|
|
419
489
|
# says whether it ran as one.
|
|
420
490
|
through_worker = images || command_like?(prompt)
|
|
421
491
|
begin
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
SessionManager.spawn_session(**start, working_directory: options[:dir],
|
|
428
|
-
model_name: options[:model],
|
|
429
|
-
setup: SessionSetup.new(llm_context: options[:llm_context],
|
|
430
|
-
thinking: options[:thinking],
|
|
431
|
-
toolkit: options[:toolkit_value],
|
|
432
|
-
settings: options[:settings],
|
|
433
|
-
parent_answers: options[:parent_answers]),
|
|
434
|
-
state_dir: @state_dir)
|
|
435
|
-
end
|
|
436
|
-
rescue SessionContinue::Refused, SessionManager::ArchiveRefused, SessionManager::OwnedByTUI => e
|
|
492
|
+
session = @access.create(new_session_spec(prompt, options, through_worker: through_worker))
|
|
493
|
+
rescue SessionAccess::BadRequest => e
|
|
494
|
+
return usage_error(e.message)
|
|
495
|
+
rescue SessionContinue::Refused, SessionManager::ArchiveRefused, SessionManager::OwnedByTUI,
|
|
496
|
+
SessionAccess::Refused => e
|
|
437
497
|
error_line("chi send: refused: #{e.message}")
|
|
438
498
|
return 1
|
|
439
499
|
rescue StandardError => e
|
|
@@ -441,9 +501,13 @@ module Samagotchi
|
|
|
441
501
|
return 1
|
|
442
502
|
end
|
|
443
503
|
error_line("chi send: warning: #{session.model_warning}") if session.model_warning
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
504
|
+
# Remotely the server's config.yml caps it (its create's warning).
|
|
505
|
+
capped = if @access.remote?
|
|
506
|
+
session.parent_answers_warning
|
|
507
|
+
else
|
|
508
|
+
Guardrails::ParentPolicy.capped_note(options[:parent_answers])
|
|
509
|
+
end
|
|
510
|
+
error_line("chi send: warning: --parent-answers #{capped}") if capped
|
|
447
511
|
if through_worker
|
|
448
512
|
return 1 unless deliver_new(session, prompt)
|
|
449
513
|
else
|
|
@@ -453,23 +517,45 @@ module Samagotchi
|
|
|
453
517
|
return 0 unless options[:wait]
|
|
454
518
|
return report_command(session.id) if @sent_command
|
|
455
519
|
|
|
456
|
-
|
|
520
|
+
start = @access.prepare_wait(session.id, session: session, created: true)
|
|
521
|
+
wait_for_reply(session.id, cursor: start.cursor, baseline: start.baseline,
|
|
457
522
|
timeout: options[:timeout], **WaitTarget.first_message(@delivered).to_h)
|
|
458
523
|
end
|
|
459
524
|
|
|
525
|
+
# What chi send --new asks to create: through the worker (images, a
|
|
526
|
+
# session command), it starts idle with the message as its preview.
|
|
527
|
+
# Without --dir its folder is the current one (spawn_session's
|
|
528
|
+
# default): machine-local, so remotely --dir is required (D4).
|
|
529
|
+
# @return [SessionAccess::NewSession]
|
|
530
|
+
def new_session_spec(prompt, options, through_worker:)
|
|
531
|
+
start = through_worker ? { prompt: nil, preview: prompt } : { prompt: prompt }
|
|
532
|
+
# For a remote's create route, which checks them as this machine does
|
|
533
|
+
# before it creates (the first line's client_id, the images' model).
|
|
534
|
+
start = start.merge(client_id: client_id_for(prompt), images: !@images.empty?)
|
|
535
|
+
if options[:continues]
|
|
536
|
+
return SessionAccess::NewSession.new(**start, continues: options[:continues], model_name: options[:model])
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
setup = options[:setup_words] || SessionSetup.new(llm_context: options[:llm_context], thinking: options[:thinking],
|
|
540
|
+
toolkit: options[:toolkit_value], settings: options[:settings],
|
|
541
|
+
parent_answers: options[:parent_answers])
|
|
542
|
+
SessionAccess::NewSession.new(**start, working_directory: options[:dir], model_name: options[:model], setup: setup)
|
|
543
|
+
end
|
|
544
|
+
|
|
460
545
|
# The model the new session will run, for the images check: --model
|
|
461
546
|
# (nil: the default), or with --continues the one the new link will run
|
|
462
547
|
# (SessionContinue.model_for: --model's, else the previous link's, else
|
|
463
|
-
# the default it falls back to).
|
|
548
|
+
# the default it falls back to). Machine-local (the previous link's
|
|
549
|
+
# file, this machine's models): remotely the server's, with the vision
|
|
550
|
+
# check (P2).
|
|
464
551
|
# @return [Array, false] [model_name, model_typed], or false after the
|
|
465
552
|
# error line (no such session, a model it can't start on)
|
|
466
553
|
def continued_model(options)
|
|
467
|
-
|
|
554
|
+
# Remotely the server reads the previous link (its vision check).
|
|
555
|
+
return [options[:model], nil] unless options[:continues] && !@access.remote?
|
|
468
556
|
|
|
469
557
|
previous = Session.load(SessionChain.resolve(options[:continues], state_dir: @state_dir), state_dir: @state_dir)
|
|
470
|
-
|
|
471
|
-
inherited = options[:model].to_s.strip.empty? && choice.warning.nil?
|
|
472
|
-
inherited ? [previous.model_name, previous.model_typed] : [choice.model, nil]
|
|
558
|
+
self.class.continued_model(previous, options[:model])
|
|
473
559
|
rescue ModelProfile::MissingModel => e
|
|
474
560
|
error_line("chi send: could not start a session: #{e.message}")
|
|
475
561
|
false
|
|
@@ -495,40 +581,21 @@ module Samagotchi
|
|
|
495
581
|
# @return [Integer] the exit status
|
|
496
582
|
def run_wait(prompt, options)
|
|
497
583
|
id = resolve(options[:ids].first) or return 1
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
#
|
|
503
|
-
if (waiting = session.waiting_question(live: owner.worker?))
|
|
504
|
-
# The whole question (its options, its tags) for what a parent may
|
|
505
|
-
# give: config.yml tightened by the session's own parent_answers.
|
|
506
|
-
strategy = Guardrails::ParentPolicy.for_session(session)
|
|
507
|
-
question = ParentReport.question(session.pending_question, strategy: strategy)
|
|
508
|
-
if ParentReport.approval?(waiting)
|
|
509
|
-
error_line("#{id[0, 8]} waiting for an approval: #{ParentReport.approval_advice(question, strategy: strategy)} " \
|
|
510
|
-
"(#{ParentReport.answer_with(id, question)})")
|
|
511
|
-
else
|
|
512
|
-
error_line("#{id[0, 8]} waiting for an answer: #{ParentReport.answer_with(id, question)} (or --text); " \
|
|
513
|
-
"chi --attach #{id} shows it")
|
|
514
|
-
end
|
|
515
|
-
else
|
|
516
|
-
error_line("#{id[0, 8]} busy: a turn is running; wait or attach")
|
|
517
|
-
end
|
|
584
|
+
# Local: one read for the check and the baseline; remotely the
|
|
585
|
+
# server's wait prepare route reads both (Busy's words).
|
|
586
|
+
session = @access.show(id) unless @access.remote?
|
|
587
|
+
if (busy = @access.busy(id, queue: @queue, session: session))
|
|
588
|
+
error_line("#{id[0, 8]} #{busy.message(via: @access.invocation)}")
|
|
518
589
|
return 1
|
|
519
590
|
end
|
|
520
591
|
|
|
521
|
-
|
|
522
|
-
baseline = baseline_of(session)
|
|
523
|
-
running_turn = if session.status == Session::STATUS_RUNNING
|
|
524
|
-
ReplyWait.own_turn(session, session_dir: Session.session_dir(id, state_dir: @state_dir))
|
|
525
|
-
end
|
|
592
|
+
start = @access.prepare_wait(id, session: session)
|
|
526
593
|
return 1 unless deliver(id, prompt)
|
|
527
594
|
return report_command(id) if @sent_command
|
|
528
595
|
|
|
529
|
-
target = WaitTarget.message(@delivered, queue: @queue, running:
|
|
530
|
-
running_turn:
|
|
531
|
-
wait_for_reply(id, cursor: cursor, baseline: baseline, timeout: options[:timeout], **target.to_h)
|
|
596
|
+
target = WaitTarget.message(@delivered, queue: @queue, running: start.running,
|
|
597
|
+
running_turn: (start.own_turn if start.running))
|
|
598
|
+
wait_for_reply(id, cursor: start.cursor, baseline: start.baseline, timeout: options[:timeout], **target.to_h)
|
|
532
599
|
end
|
|
533
600
|
|
|
534
601
|
# --wait with no message: the session's next reply, sending nothing,
|
|
@@ -546,24 +613,15 @@ module Samagotchi
|
|
|
546
613
|
# @return [Integer] the exit status
|
|
547
614
|
def run_wait_only(options)
|
|
548
615
|
id = resolve(options[:ids].first) or return 1
|
|
549
|
-
session = Session.load(id, state_dir: @state_dir)
|
|
550
|
-
session_dir = Session.session_dir(id, state_dir: @state_dir)
|
|
551
616
|
requested_turn = options[:turn]&.to_s
|
|
552
|
-
|
|
617
|
+
start = @access.prepare_wait(id, turn: requested_turn)
|
|
618
|
+
if requested_turn && !start.known_turn
|
|
553
619
|
error_line("chi send: turn #{requested_turn} is not a running, queued, or last ended turn")
|
|
554
620
|
return 1
|
|
555
621
|
end
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
baseline: baseline_of(session),
|
|
560
|
-
timeout: options[:timeout], owner_grace: live ? self.class::WORKER_GONE_AFTER : nil, **target.to_h)
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
# The session as it was before the message went in (ReplyWait's
|
|
564
|
-
# baseline).
|
|
565
|
-
def baseline_of(session, **)
|
|
566
|
-
ReplyWait.baseline_of(session, **)
|
|
622
|
+
target = WaitTarget.after(start, requested: requested_turn)
|
|
623
|
+
wait_for_reply(id, cursor: start.cursor, baseline: start.baseline,
|
|
624
|
+
timeout: options[:timeout], owner_grace: start.live ? self.class::WORKER_GONE_AFTER : nil, **target.to_h)
|
|
567
625
|
end
|
|
568
626
|
|
|
569
627
|
# A new idle session's first turn, with the images. Its worker's Bridge
|
|
@@ -571,14 +629,15 @@ module Samagotchi
|
|
|
571
629
|
# and the spawned one takes its lock only once it runs.
|
|
572
630
|
# @return [Boolean] whether the message was queued
|
|
573
631
|
def deliver_new(session, prompt)
|
|
574
|
-
dir = Session.session_dir(session.id, state_dir: @state_dir)
|
|
575
632
|
result = nil
|
|
576
633
|
begin
|
|
577
|
-
refs =
|
|
578
|
-
if
|
|
579
|
-
result =
|
|
580
|
-
state_dir: @state_dir)
|
|
634
|
+
refs = upload_images(session.id)
|
|
635
|
+
if @access.await_worker(session.id, timeout: SessionManager::TURN_BRIDGE_WAIT)
|
|
636
|
+
result = @access.deliver_turn(session.id, prompt: prompt, client_id: client_id_for(prompt), images: refs)
|
|
581
637
|
end
|
|
638
|
+
rescue SessionAccess::Refused => e
|
|
639
|
+
@info.puts("#{session.id} refused: #{e.message}")
|
|
640
|
+
return false
|
|
582
641
|
rescue StandardError => e
|
|
583
642
|
@info.puts("#{session.id} failed: #{e.message}")
|
|
584
643
|
return false
|
|
@@ -586,11 +645,12 @@ module Samagotchi
|
|
|
586
645
|
# Kept, not deleted: it holds the images, and the message shows as
|
|
587
646
|
# its preview; the user can attach and send it again.
|
|
588
647
|
unless result
|
|
589
|
-
@
|
|
648
|
+
attach = @access.invocation.attach(session.id)
|
|
649
|
+
@info.puts("#{session.id} failed: its worker did not start; the session is kept#{" (#{attach})" if attach}")
|
|
590
650
|
return false
|
|
591
651
|
end
|
|
592
652
|
unless result.accepted?
|
|
593
|
-
@info.puts("#{session.id}
|
|
653
|
+
@info.puts("#{session.id} #{refused_word(result)}: #{result.detail || "could not queue it"}")
|
|
594
654
|
return false
|
|
595
655
|
end
|
|
596
656
|
|
|
@@ -602,6 +662,19 @@ module Samagotchi
|
|
|
602
662
|
|
|
603
663
|
SENT_COMMAND = "sent as a session command"
|
|
604
664
|
|
|
665
|
+
# A remote's refusals of what this machine checks before sending (a
|
|
666
|
+
# parent's /continue yes, a loosening /parent-answers, images to a
|
|
667
|
+
# model that can't see them): its turn route's error codes.
|
|
668
|
+
REMOTE_REFUSALS = %w[parent_answers_refused parent_continue_refused no_vision].freeze
|
|
669
|
+
|
|
670
|
+
# How a delivery that didn't go in reads: refused (a worker older than
|
|
671
|
+
# queueing would steer it; a remote refused it as chi send refuses it
|
|
672
|
+
# here) or failed.
|
|
673
|
+
def refused_word(result)
|
|
674
|
+
refused = result.error == Delivery::QUEUE_UNAVAILABLE["error"] || REMOTE_REFUSALS.include?(result.error)
|
|
675
|
+
refused ? "refused" : "failed"
|
|
676
|
+
end
|
|
677
|
+
|
|
605
678
|
# chi send's client id, or chi answer's for a parent agent's /continue
|
|
606
679
|
# yes (the worker holds that to its guardrails.parent_answers).
|
|
607
680
|
def client_id_for(prompt) = ParentGate.parent_continue?(prompt) ? ClientId::CLI_ANSWER : CLIENT_ID
|
|
@@ -636,13 +709,13 @@ module Samagotchi
|
|
|
636
709
|
CLI::Exit::FAILED
|
|
637
710
|
end
|
|
638
711
|
|
|
639
|
-
def local_refusal(name, id) = SessionCommands.ui_only_refusal(name, id, runner: "chi send")
|
|
712
|
+
def local_refusal(name, id) = SessionCommands.ui_only_refusal(name, id, runner: "chi send", remote: @access.invocation.remote)
|
|
640
713
|
|
|
641
714
|
# --wait after a message that ran as a session command: no reply comes
|
|
642
715
|
# for it. Its output shows where the session is open.
|
|
643
716
|
# @return [Integer] 0
|
|
644
717
|
def report_command(id)
|
|
645
|
-
detail = "#{SENT_COMMAND}: no reply to wait for (its output shows in
|
|
718
|
+
detail = "#{SENT_COMMAND}: no reply to wait for (its output shows in #{@access.invocation.open_it(id)})"
|
|
646
719
|
if @json
|
|
647
720
|
@stdout.puts(JSON.generate({ status: "command", session_id: id, detail: detail }))
|
|
648
721
|
@stdout.flush
|
|
@@ -656,14 +729,16 @@ module Samagotchi
|
|
|
656
729
|
# @return [Boolean] whether the message was queued
|
|
657
730
|
def deliver(id, prompt)
|
|
658
731
|
short = id[0, 8]
|
|
659
|
-
session =
|
|
732
|
+
session = @access.show(id)
|
|
733
|
+
# Machine-local (this machine's model registry): remotely the
|
|
734
|
+
# server's (P2), as for --new.
|
|
660
735
|
if !@images.empty? && (refusal = vision_refusal(session.model_name, typed: session.model_typed))
|
|
661
736
|
@info.puts("#{short} refused: #{refusal}")
|
|
662
737
|
return false
|
|
663
738
|
end
|
|
664
|
-
owner =
|
|
739
|
+
owner = @access.owner(id)
|
|
665
740
|
running = owner && session.status == Session::STATUS_RUNNING
|
|
666
|
-
refs =
|
|
741
|
+
refs = upload_images(id)
|
|
667
742
|
# --cut asks the running turn for a cut (to an idle session an
|
|
668
743
|
# ordinary message: nothing to cut). --queue always goes as a queue,
|
|
669
744
|
# idle or not: the Bridge acks next_step to an idle worker and the
|
|
@@ -673,12 +748,9 @@ module Samagotchi
|
|
|
673
748
|
delivery = if @queue then Delivery::QUEUE
|
|
674
749
|
elsif @cut && running then Delivery::CUT
|
|
675
750
|
end
|
|
676
|
-
result =
|
|
677
|
-
delivery: delivery)
|
|
751
|
+
result = @access.deliver_turn(id, prompt: prompt, client_id: client_id_for(prompt), images: refs, delivery: delivery)
|
|
678
752
|
unless result.accepted?
|
|
679
|
-
#
|
|
680
|
-
word = result.error == Delivery::QUEUE_UNAVAILABLE["error"] ? "refused" : "failed"
|
|
681
|
-
@info.puts("#{short} #{word}: #{result.detail || "could not queue it"}")
|
|
753
|
+
@info.puts("#{short} #{refused_word(result)}: #{result.detail || "could not queue it"}")
|
|
682
754
|
return false
|
|
683
755
|
end
|
|
684
756
|
|
|
@@ -755,14 +827,12 @@ module Samagotchi
|
|
|
755
827
|
# What to say when +model_name+ is known not to take images, or nil
|
|
756
828
|
# (it does, or it is unknown: sent as before). Asked once per model.
|
|
757
829
|
def vision_refusal(model_name, typed: nil)
|
|
830
|
+
# Remotely the server's (check_vision on its turn and create routes).
|
|
831
|
+
return nil if @access.remote?
|
|
832
|
+
|
|
758
833
|
@vision ||= {}
|
|
759
834
|
answer = @vision[[model_name.to_s, typed.to_s]] ||= self.class.vision_answer(model_name, typed: typed)
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
name = model_name.to_s.strip
|
|
763
|
-
name = Samagotchi::Config.get("default.model").to_s.strip if name.empty?
|
|
764
|
-
name = "the model" if name.empty?
|
|
765
|
-
"#{name} can't take images (#{answer.reason}); send text only or switch the model (/model)"
|
|
835
|
+
self.class.vision_refusal(model_name, answer)
|
|
766
836
|
end
|
|
767
837
|
|
|
768
838
|
# Each --image read, converted and downscaled once, into a scratch
|
|
@@ -781,13 +851,8 @@ module Samagotchi
|
|
|
781
851
|
false
|
|
782
852
|
end
|
|
783
853
|
|
|
784
|
-
# The images' files copied into
|
|
785
|
-
def
|
|
786
|
-
@images.map do |ref|
|
|
787
|
-
ImageStore.copy_file(ref, from: @image_dir, to: session_dir)
|
|
788
|
-
{ file: ref[:file], name: ref[:name] }
|
|
789
|
-
end
|
|
790
|
-
end
|
|
854
|
+
# The images' files copied into the session, as the refs a turn takes.
|
|
855
|
+
def upload_images(id) = @access.upload_images(id, @images, from: @image_dir)
|
|
791
856
|
|
|
792
857
|
def with_images
|
|
793
858
|
case @images.size
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative "pending_question"
|
|
|
12
12
|
require_relative "llm_context_override"
|
|
13
13
|
require_relative "session_setup"
|
|
14
14
|
require_relative "prompt_note"
|
|
15
|
+
require_relative "working_set_pin"
|
|
15
16
|
|
|
16
17
|
require_relative "paths"
|
|
17
18
|
require_relative "project_scope"
|
|
@@ -249,6 +250,17 @@ module Samagotchi
|
|
|
249
250
|
end
|
|
250
251
|
end
|
|
251
252
|
|
|
253
|
+
# The working set's items the user pinned (/working-set pin;
|
|
254
|
+
# WorkingSetPin), [] with none: the model's forget_outputs refuses
|
|
255
|
+
# their outputs. A respawn, a continue and a fork keep them
|
|
256
|
+
# (SessionSetup.of); a delegate child starts without.
|
|
257
|
+
attr_reader :working_set_pins
|
|
258
|
+
|
|
259
|
+
# @param value [Array<WorkingSetPin, Hash>] the session file's form too
|
|
260
|
+
def working_set_pins=(value)
|
|
261
|
+
@working_set_pins = WorkingSetPin.list(value)
|
|
262
|
+
end
|
|
263
|
+
|
|
252
264
|
# The hook that stopped the last turn (stop_turn, or a cut with no retry
|
|
253
265
|
# left), e.g. "loop-guard"; nil when it ended otherwise.
|
|
254
266
|
def stopped_by
|
|
@@ -283,7 +295,8 @@ module Samagotchi
|
|
|
283
295
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
284
296
|
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [],
|
|
285
297
|
continues: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [],
|
|
286
|
-
toolkit: nil, settings: nil, interrupted_turn: nil, parent_answers: nil, title: nil
|
|
298
|
+
toolkit: nil, settings: nil, interrupted_turn: nil, parent_answers: nil, title: nil,
|
|
299
|
+
working_set_pins: [])
|
|
287
300
|
@id = id
|
|
288
301
|
@metadata_version = metadata_version
|
|
289
302
|
@mode = mode
|
|
@@ -316,6 +329,7 @@ module Samagotchi
|
|
|
316
329
|
self.toolkit = toolkit
|
|
317
330
|
self.settings = settings
|
|
318
331
|
self.parent_answers = parent_answers
|
|
332
|
+
self.working_set_pins = working_set_pins
|
|
319
333
|
self.interrupted_turn = interrupted_turn
|
|
320
334
|
@turn_checkpoint = nil
|
|
321
335
|
@archived = false
|
|
@@ -415,6 +429,7 @@ module Samagotchi
|
|
|
415
429
|
toolkit: setup.toolkit,
|
|
416
430
|
settings: setup.settings,
|
|
417
431
|
parent_answers: setup.parent_answers,
|
|
432
|
+
working_set_pins: setup.working_set_pins,
|
|
418
433
|
continues: continues
|
|
419
434
|
)
|
|
420
435
|
end
|
|
@@ -484,7 +499,8 @@ module Samagotchi
|
|
|
484
499
|
"bundle_settings" => {},
|
|
485
500
|
"config_settings" => {},
|
|
486
501
|
"interrupted_turn" => nil,
|
|
487
|
-
"parent_answers" => nil
|
|
502
|
+
"parent_answers" => nil,
|
|
503
|
+
"working_set_pins" => []
|
|
488
504
|
}.freeze
|
|
489
505
|
|
|
490
506
|
# A session from a parsed session file. +messages+ false leaves the
|
|
@@ -499,8 +515,16 @@ module Samagotchi
|
|
|
499
515
|
new(**attrs)
|
|
500
516
|
end
|
|
501
517
|
|
|
502
|
-
# A prefix that names more than one session
|
|
503
|
-
|
|
518
|
+
# A prefix that names more than one session: +ids+ are the sessions it
|
|
519
|
+
# names (sorted), the message lists them with their first previews.
|
|
520
|
+
class AmbiguousId < ArgumentError
|
|
521
|
+
attr_reader :ids
|
|
522
|
+
|
|
523
|
+
def initialize(message = nil, ids: [])
|
|
524
|
+
@ids = ids
|
|
525
|
+
super(message)
|
|
526
|
+
end
|
|
527
|
+
end
|
|
504
528
|
|
|
505
529
|
# A session id or a unique prefix of one (like git's): the full id. An
|
|
506
530
|
# unknown one comes back as is, for the caller's own "not found"; one that
|
|
@@ -518,7 +542,7 @@ module Samagotchi
|
|
|
518
542
|
rescue JSON::ParserError, SystemCallError
|
|
519
543
|
" #{match}"
|
|
520
544
|
end
|
|
521
|
-
raise AmbiguousId
|
|
545
|
+
raise AmbiguousId.new("session id #{id} matches #{matches.size} sessions:\n#{lines.join("\n")}", ids: matches)
|
|
522
546
|
end
|
|
523
547
|
|
|
524
548
|
# Return all saved sessions sorted by updated_at desc by default (newest first).
|
|
@@ -627,6 +651,7 @@ module Samagotchi
|
|
|
627
651
|
when "config_settings" then @settings.core
|
|
628
652
|
when "prompt_notes" then @prompt_notes.map(&:to_file)
|
|
629
653
|
when "last_turn" then @last_turn&.to_file
|
|
654
|
+
when "working_set_pins" then @working_set_pins.map(&:to_file)
|
|
630
655
|
when "used_memory_names", "preloaded_memory_names", "muted_memory_names", "appended_prompt_files"
|
|
631
656
|
Array(instance_variable_get(:"@#{key}"))
|
|
632
657
|
else instance_variable_get(:"@#{key}")
|