samagotchi 0.131.0 → 0.133.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 +88 -1
- data/bin/chi +19 -1
- data/docs/architecture.md +59 -1
- data/docs/cli.md +29 -8
- data/docs/configuration.md +115 -16
- data/docs/desktop.md +16 -6
- data/docs/guardrails.md +10 -4
- data/docs/internals/anthropic.md +3 -2
- data/docs/internals/engine.md +4 -4
- data/docs/internals/prompt-caching.md +26 -6
- data/docs/memory.md +16 -2
- data/docs/plugins.md +68 -1
- data/docs/sessions.md +60 -6
- data/docs/testing.md +1 -1
- data/lib/samagotchi/bootstrap_command.rb +35 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -3
- data/lib/samagotchi/bundles/model-notes/manifest.yml +4 -1
- data/lib/samagotchi/bundles/model-notes/model_notes_tone.md +3 -0
- data/lib/samagotchi/bundles/model-notes/notes/README.md +14 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cache_confirm.rb +18 -0
- data/lib/samagotchi/chi_home.rb +311 -0
- data/lib/samagotchi/chi_shim.rb +77 -0
- data/lib/samagotchi/child_env.rb +5 -3
- data/lib/samagotchi/config.rb +2 -0
- data/lib/samagotchi/context_fetch.rb +4 -2
- data/lib/samagotchi/desktop/macos.rb +100 -18
- data/lib/samagotchi/engine.rb +35 -4
- data/lib/samagotchi/engine_snapshots.rb +5 -0
- data/lib/samagotchi/gem_update.rb +19 -10
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/pair_boundary.rb +21 -2
- data/lib/samagotchi/guardrails/pair_command.rb +57 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +6 -3
- data/lib/samagotchi/guardrails/review_paths.rb +7 -4
- data/lib/samagotchi/guardrails/verdict.rb +1 -1
- data/lib/samagotchi/home_command.rb +145 -0
- data/lib/samagotchi/home_doctor.rb +177 -0
- data/lib/samagotchi/home_notice.rb +80 -0
- data/lib/samagotchi/installed_versions.rb +5 -3
- data/lib/samagotchi/llm_context_command.rb +53 -7
- data/lib/samagotchi/llm_context_override.rb +3 -2
- data/lib/samagotchi/plugin/api.rb +45 -1
- data/lib/samagotchi/plugin/context.rb +34 -1
- data/lib/samagotchi/plugin/loader.rb +5 -2
- data/lib/samagotchi/plugin/prompt_sections.rb +124 -0
- data/lib/samagotchi/project_env.rb +360 -0
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +33 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +68 -19
- data/lib/samagotchi/session_manager.rb +54 -1
- data/lib/samagotchi/session_model_settings.rb +5 -2
- data/lib/samagotchi/system_prompt.rb +34 -3
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +3 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +3 -0
- data/lib/samagotchi/testing/boundary.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +22 -3
- data/lib/samagotchi/testing/host.rb +22 -1
- data/lib/samagotchi/testing/recorder.rb +6 -1
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/tools/builtins.rb +13 -2
- data/lib/samagotchi/update_command.rb +65 -7
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +61 -1
- data/lib/samagotchi/web/public/app.js +4 -1
- data/lib/samagotchi/web/public/chip_popover.js +5 -2
- data/lib/samagotchi/web/public/composer.css +11 -1
- data/lib/samagotchi/web/public/data.js +8 -4
- data/lib/samagotchi/web/public/index.html +52 -10
- data/lib/samagotchi/web/public/level_chip.js +22 -38
- data/lib/samagotchi/web/public/llm_context_chip.js +30 -73
- data/lib/samagotchi/web/public/model_pick.js +26 -0
- data/lib/samagotchi/web/public/model_picker.js +3 -1
- data/lib/samagotchi/web/public/review_start.js +13 -57
- data/lib/samagotchi/web/public/send_turn.js +3 -2
- data/lib/samagotchi/web/public/start_page.js +131 -99
- data/lib/samagotchi/web/public/start_settings.js +298 -0
- data/lib/samagotchi/web/public/start_toolkit.js +157 -0
- data/lib/samagotchi/web/public/thinking_chip.js +9 -22
- data/lib/samagotchi/web/server.rb +27 -8
- data/lib/samagotchi/web/update_check.rb +3 -2
- metadata +11 -1
|
@@ -5,6 +5,7 @@ require_relative "model_profile"
|
|
|
5
5
|
require_relative "served_model"
|
|
6
6
|
require_relative "prompt_note"
|
|
7
7
|
require_relative "turn_flow"
|
|
8
|
+
require_relative "cache_confirm"
|
|
8
9
|
require_relative "tools/execute"
|
|
9
10
|
require_relative "tools/builtins"
|
|
10
11
|
require_relative "tools/context_read"
|
|
@@ -61,7 +62,7 @@ module Samagotchi
|
|
|
61
62
|
|
|
62
63
|
# Commands that may ask before a change that restarts the prompt cache
|
|
63
64
|
# (CacheConfirm): #run passes them its confirm flag.
|
|
64
|
-
CONFIRMING = %i[thinking reasoning_history toolkit].freeze
|
|
65
|
+
CONFIRMING = %i[model llm_context thinking reasoning_history toolkit].freeze
|
|
65
66
|
|
|
66
67
|
# @!attribute status [Symbol] :ok, or :error when the line was refused
|
|
67
68
|
# @!attribute output [String, nil] what to tell the user
|
|
@@ -302,7 +303,7 @@ module Samagotchi
|
|
|
302
303
|
text = line.to_s.strip
|
|
303
304
|
entry = @registry.lookup(text)
|
|
304
305
|
return nil unless entry&.handler
|
|
305
|
-
return run_bundle_command(entry, text) unless entry.source == "core"
|
|
306
|
+
return run_bundle_command(entry, text, confirm: confirm) unless entry.source == "core"
|
|
306
307
|
|
|
307
308
|
# Only commands that may ask take the flag.
|
|
308
309
|
return send(entry.id, text, confirm: confirm) if CONFIRMING.include?(entry.id)
|
|
@@ -355,16 +356,32 @@ module Samagotchi
|
|
|
355
356
|
# The resolved ref (what the session stores and --alias/--default write).
|
|
356
357
|
def model_name = @engine.effective_model_ref
|
|
357
358
|
|
|
358
|
-
# A bundle plugin's command: its handler gets the text after the name
|
|
359
|
-
# what it returns is the output (nil: nothing to show),
|
|
360
|
-
#
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
359
|
+
# A bundle plugin's command: its handler gets the text after the name
|
|
360
|
+
# (--yes kept); what it returns is the output (nil: nothing to show),
|
|
361
|
+
# or [output, CacheConfirm::Confirm or nil] for a change that asks
|
|
362
|
+
# first (ctx.cache_confirm), and a raise is an error result. The
|
|
363
|
+
# sender's +confirm+ (false once the line has --yes) reaches
|
|
364
|
+
# ctx.cache_confirm through the thread (CacheConfirm.asking).
|
|
365
|
+
def run_bundle_command(entry, text, confirm: false)
|
|
366
|
+
args = text.delete_prefix(entry.name).strip
|
|
367
|
+
_, yes = CacheConfirm.split(args)
|
|
368
|
+
output, ask = bundle_reply(CacheConfirm.asking(confirm && !yes) { entry.handler.call(args) })
|
|
369
|
+
reply(output, confirm: ask)
|
|
364
370
|
rescue StandardError => e
|
|
365
371
|
reply("#{entry.name}: #{e.class}: #{e.message}", status: :error)
|
|
366
372
|
end
|
|
367
373
|
|
|
374
|
+
# A plugin command's return value as [output, confirm]: a String (or
|
|
375
|
+
# anything, shown as text) or nil, or [text, Confirm/nil].
|
|
376
|
+
def bundle_reply(value)
|
|
377
|
+
if value.is_a?(Array) && value.size == 2 && (value[1].nil? || value[1].is_a?(CacheConfirm::Confirm))
|
|
378
|
+
text, ask = value
|
|
379
|
+
return [text.nil? ? nil : text.to_s, ask]
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
[value.nil? ? nil : value.to_s, nil]
|
|
383
|
+
end
|
|
384
|
+
|
|
368
385
|
def help_group(entry)
|
|
369
386
|
return 2 if entry.local
|
|
370
387
|
|
|
@@ -393,14 +410,16 @@ module Samagotchi
|
|
|
393
410
|
# here (as in the REPL, the next turn saves it). No parent-session
|
|
394
411
|
# marker: the user runs this, not the model. A worker's `chi --model X`
|
|
395
412
|
# default is unset, though: it was that run's, not the default of a chi
|
|
396
|
-
# run here (Tools::Builtins.cli_model_unset).
|
|
413
|
+
# run here (Tools::Builtins.cli_model_unset). It runs in the session's
|
|
414
|
+
# project env (ProjectEnv), as the model's execute does.
|
|
397
415
|
def shell(text)
|
|
398
416
|
command = text.delete_prefix(SHELL_BANG_PREFIX).strip
|
|
399
417
|
return reply("!: please provide a shell command after '!'", status: :error) if command.empty?
|
|
400
418
|
|
|
401
419
|
# Killed at the limit, never handed over: the output goes in a user
|
|
402
420
|
# message, which no one reads a task id from (created_ids_in).
|
|
403
|
-
|
|
421
|
+
env = Samagotchi::ProjectEnv.tools_env.merge(Samagotchi::Tools::Builtins.cli_model_unset)
|
|
422
|
+
output = Samagotchi::Tools::Execute.call(command, env: env, on_timeout: :kill)
|
|
404
423
|
@engine.append_messages([{ role: "user", content: "!(#{command})\n#{output}" }])
|
|
405
424
|
# Rolling back past this would silently drop the command output.
|
|
406
425
|
@turn_flow.note_conversation_changed
|
|
@@ -583,13 +602,13 @@ module Samagotchi
|
|
|
583
602
|
|
|
584
603
|
# /llm-context (LLMContextCommand); :llm_context in changed when it set
|
|
585
604
|
# the session's values.
|
|
586
|
-
def llm_context(text)
|
|
587
|
-
output, changed = LLMContextCommand.new(engine: @engine, save: ->(session) { save_setup(session) })
|
|
588
|
-
|
|
605
|
+
def llm_context(text, confirm: true)
|
|
606
|
+
output, changed, ask = LLMContextCommand.new(engine: @engine, save: ->(session) { save_setup(session) })
|
|
607
|
+
.run(text.delete_prefix(LLM_CONTEXT_COMMAND), confirm: confirm)
|
|
589
608
|
# What the model has of each item is the next turn's strategy's: the
|
|
590
609
|
# working set goes out again (sent only when it differs).
|
|
591
610
|
@engine.announce_working_set if changed
|
|
592
|
-
reply(output, changed: changed ? [:llm_context] : [])
|
|
611
|
+
reply(output, changed: changed ? [:llm_context] : [], confirm: ask)
|
|
593
612
|
rescue ArgumentError => e
|
|
594
613
|
reply(e.message, status: :error)
|
|
595
614
|
end
|
|
@@ -672,16 +691,23 @@ module Samagotchi
|
|
|
672
691
|
@save.call(session)
|
|
673
692
|
end
|
|
674
693
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
694
|
+
# /model; a switch to another model asks first (CacheConfirm, cost
|
|
695
|
+
# :full: the new model has no cache of the conversation), --yes applies.
|
|
696
|
+
def model(text, confirm: true)
|
|
697
|
+
suffix, yes = CacheConfirm.split(text.delete_prefix(MODEL_COMMAND), downcase: false)
|
|
698
|
+
output, switched, ask = model_command("#{MODEL_COMMAND} #{suffix}", confirm: confirm && !yes)
|
|
699
|
+
reply(output, changed: switched ? [:model] : [], confirm: ask)
|
|
678
700
|
rescue ModelProfile::MissingModel => e
|
|
679
701
|
# A model qualified with an unknown host: nothing switched.
|
|
680
702
|
reply(e.message, status: :error)
|
|
681
703
|
end
|
|
682
704
|
|
|
683
|
-
# @
|
|
684
|
-
|
|
705
|
+
# @param input [String] the /model line, --yes taken out
|
|
706
|
+
# @param confirm [Boolean] a switch to another model asks first
|
|
707
|
+
# @return [Array(String, Boolean, CacheConfirm::Confirm|nil)] the message,
|
|
708
|
+
# whether the model changed, and the confirm it asks for (nothing
|
|
709
|
+
# changed then)
|
|
710
|
+
def model_command(input, confirm: false)
|
|
685
711
|
suffix = input.delete_prefix(MODEL_COMMAND).strip
|
|
686
712
|
if suffix.empty?
|
|
687
713
|
return ["runtime model: #{model_name}#{model_note}#{served_note}#{sampling_note}#{steer_cut_after_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", false] if @default_model.nil? || model_name == @engine.model_ref_for(@default_model)
|
|
@@ -735,6 +761,9 @@ module Samagotchi
|
|
|
735
761
|
invalid = ConfigFile.model_alias_error(alias_name, arg)
|
|
736
762
|
return ["invalid alias: #{invalid}", false] if invalid
|
|
737
763
|
|
|
764
|
+
ask = confirm && model_ask(arg, input)
|
|
765
|
+
return ask if ask
|
|
766
|
+
|
|
738
767
|
switch_model(arg, persist_default: persist_default)
|
|
739
768
|
|
|
740
769
|
begin
|
|
@@ -756,10 +785,16 @@ module Samagotchi
|
|
|
756
785
|
return ["--default cannot be combined with clear/default/none/off", false] if persist_default
|
|
757
786
|
raise ModelProfile::MissingModel, "no default model is configured (set default.model in config.yml, or /model <name> --default)" if @default_model.nil?
|
|
758
787
|
|
|
788
|
+
ask = confirm && model_ask(@default_model, input)
|
|
789
|
+
return ask if ask
|
|
790
|
+
|
|
759
791
|
switch_model(@default_model)
|
|
760
792
|
return ["runtime model reset to #{model_name}#{model_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", true]
|
|
761
793
|
end
|
|
762
794
|
|
|
795
|
+
ask = confirm && model_ask(arg, input)
|
|
796
|
+
return ask if ask
|
|
797
|
+
|
|
763
798
|
switch_model(arg, persist_default: persist_default)
|
|
764
799
|
if persist_default
|
|
765
800
|
["runtime model set to #{model_name}#{model_note} and default updated#{thinking_note}#{reasoning_history_note}#{notes_note}", true]
|
|
@@ -769,6 +804,20 @@ module Samagotchi
|
|
|
769
804
|
end
|
|
770
805
|
end
|
|
771
806
|
|
|
807
|
+
# The reply that asks before switching to +name+ (CacheConfirm), nil
|
|
808
|
+
# when it needn't: the name is checked first (a typo errors at once,
|
|
809
|
+
# MissingModel), and its resolved ref against the current one (the same
|
|
810
|
+
# model costs nothing; any other restarts the whole cache).
|
|
811
|
+
# @param line [String] the /model line as typed (its flags too), --yes taken out
|
|
812
|
+
def model_ask(name, line)
|
|
813
|
+
ref = @engine.model_switch_ref(name)
|
|
814
|
+
step = CacheConfirm.ask(line: line, cost: :full, changed: ref != model_name, engine: @engine)
|
|
815
|
+
return nil unless step
|
|
816
|
+
|
|
817
|
+
["runtime model: #{model_name}\nSwitching to #{ref} restarts the prompt cache: the new model has no cache of " \
|
|
818
|
+
"this conversation.\n#{step.text}", false, step]
|
|
819
|
+
end
|
|
820
|
+
|
|
772
821
|
# Engine owns the switch (alias resolution, profile, kernel, client and
|
|
773
822
|
# the optional default persist); the session keeps the new model.
|
|
774
823
|
def switch_model(name, persist_default: false)
|
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require "time"
|
|
6
6
|
require "securerandom"
|
|
7
7
|
require "rbconfig"
|
|
8
|
+
require_relative "chi_home"
|
|
8
9
|
require_relative "child_env"
|
|
9
10
|
require_relative "client_id"
|
|
10
11
|
require_relative "analytics_file"
|
|
@@ -33,6 +34,7 @@ require_relative "chi_invocation"
|
|
|
33
34
|
require_relative "log"
|
|
34
35
|
require_relative "log_path"
|
|
35
36
|
require_relative "paths"
|
|
37
|
+
require_relative "project_env"
|
|
36
38
|
require_relative "model_profile"
|
|
37
39
|
require_relative "installed_gem"
|
|
38
40
|
require_relative "installed_versions"
|
|
@@ -256,6 +258,8 @@ module Samagotchi
|
|
|
256
258
|
else
|
|
257
259
|
Log.warn(:worker, "cwd_gone", sid: session.id, dir: dir, cwd: Dir.pwd)
|
|
258
260
|
end
|
|
261
|
+
# (A chi a session's tool runs took that session's project env out at
|
|
262
|
+
# its start: bin/chi, ProjectEnv.take_orig!.)
|
|
259
263
|
child_env = {}
|
|
260
264
|
# A worker gets no CLI args: --model, --thinking, --log-level and the
|
|
261
265
|
# other flags this chi was started with.
|
|
@@ -1127,6 +1131,9 @@ module Samagotchi
|
|
|
1127
1131
|
fallback = ENV.delete(BOOT_FALLBACK_ENV)
|
|
1128
1132
|
Log.info(:worker, "start", cwd: Dir.pwd, version: VERSION)
|
|
1129
1133
|
Log.warn(:worker, "boot_fallback", version: VERSION, error: fallback) if fallback
|
|
1134
|
+
# Tools follow the project (its `mise env`, computed once per worker;
|
|
1135
|
+
# this process's ENV is left as it is); never in a loop run in-process.
|
|
1136
|
+
apply_project_env(session_id, state_dir: sd) if spawned
|
|
1130
1137
|
# A spawned worker stops its turn on TERM/HUP/INT and saves it; a
|
|
1131
1138
|
# loop run in-process (specs) traps nothing.
|
|
1132
1139
|
signals = spawned ? WorkerSignals.new.install! : WorkerSignals::NONE
|
|
@@ -1215,6 +1222,18 @@ module Samagotchi
|
|
|
1215
1222
|
Log.info(:worker, "kept_session", sid: session_id, error: e.class.name, msg: e.message)
|
|
1216
1223
|
end
|
|
1217
1224
|
|
|
1225
|
+
# The tools' ProjectEnv for the session's folder (its working_directory;
|
|
1226
|
+
# spawn_options chdirs there unless it is gone), installed for this
|
|
1227
|
+
# process's tool spawns. Never fatal: a worker without it still runs.
|
|
1228
|
+
private_class_method def self.apply_project_env(session_id, state_dir:)
|
|
1229
|
+
session = Session.summary_from_file(Session.session_file(session_id, state_dir: state_dir))
|
|
1230
|
+
dir = session&.working_directory.to_s
|
|
1231
|
+
overlay = ProjectEnv.install!(ProjectEnv.compute(dir))
|
|
1232
|
+
Log.info(:worker, "project_env_applied", dir: dir, ruby: overlay.ruby) if overlay.applied
|
|
1233
|
+
rescue StandardError => e
|
|
1234
|
+
Log.warn(:worker, "project_env", reason: "error", error: e.class.name, msg: e.message)
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1218
1237
|
def self.config_idle_exit_minutes
|
|
1219
1238
|
Samagotchi::Config.get("session.idle_exit_minutes")
|
|
1220
1239
|
rescue StandardError
|
|
@@ -1252,16 +1271,50 @@ module Samagotchi
|
|
|
1252
1271
|
# as it is read, so an rspec run by the worker's execute never forks.
|
|
1253
1272
|
WORKER_DETACH_ENV = "SAMAGOTCHI_WORKER_DETACH"
|
|
1254
1273
|
|
|
1255
|
-
|
|
1274
|
+
# The boot string's rescue under a chi home: one line in the spawner's
|
|
1275
|
+
# log file (SAMAGOTCHI_LOG_FILE, set by spawn_options), LogLine's shape,
|
|
1276
|
+
# then exit. Single-quoted: the #{} run in the worker. SID is replaced.
|
|
1277
|
+
# rubocop:disable-next Lint/InterpolationCheck -- the worker interpolates these, not this process
|
|
1278
|
+
HOME_BOOT_FAILED = 'rescue ScriptError, StandardError => e; f = ENV["SAMAGOTCHI_LOG_FILE"].to_s; begin; ' \
|
|
1279
|
+
'File.open(f, "a") { |io| io.puts(Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ") + ' \
|
|
1280
|
+
'" ERROR worker pid=#{Process.pid} sid=SID boot_failed ruby=#{RbConfig.ruby.inspect} error=" + ' \
|
|
1281
|
+
"e.message.lines.first.to_s.strip.inspect) } unless f.empty?; rescue SystemCallError; end; exit 1"
|
|
1282
|
+
|
|
1283
|
+
def self.worker_command(session_id, state_dir:, gem_spec: InstalledGem.spec,
|
|
1284
|
+
home: ChiHome.current(gem_spec: gem_spec))
|
|
1256
1285
|
boot = "require 'samagotchi/session_manager'; " \
|
|
1257
1286
|
"Samagotchi::SessionManager.run_session_loop('#{session_id}', state_dir: #{state_dir.inspect})"
|
|
1258
1287
|
return [RbConfig.ruby, "-I", File.expand_path("..", __dir__), "-e", boot] unless gem_spec
|
|
1288
|
+
return home_worker_command(session_id, state_dir: state_dir, gem_spec: gem_spec, home: home) if home
|
|
1259
1289
|
|
|
1260
1290
|
activate = "begin; gem 'samagotchi', '>= 0.a'; rescue LoadError => e; " \
|
|
1261
1291
|
"ENV['#{BOOT_FALLBACK_ENV}'] = e.message; gem 'samagotchi', '= #{gem_spec.version}'; end"
|
|
1262
1292
|
[RbConfig.ruby, "-e", "#{activate}; #{boot}"]
|
|
1263
1293
|
end
|
|
1264
1294
|
|
|
1295
|
+
# With a chi home (ChiHome.current, docs/architecture.md "How chi
|
|
1296
|
+
# launches itself"): the home's ruby, `Gem.paths` set to its gems in the
|
|
1297
|
+
# boot string (no GEM_* in the worker's env), the newest chi there, else
|
|
1298
|
+
# the fallback pin (ChiHome.fallback_version). A home that still can't
|
|
1299
|
+
# load chi (a broken install) logs HOME_BOOT_FAILED and exits: never a
|
|
1300
|
+
# silent spawn timeout.
|
|
1301
|
+
private_class_method def self.home_worker_command(session_id, state_dir:, gem_spec:, home:)
|
|
1302
|
+
newest = home.newest_version
|
|
1303
|
+
if newest && newest < Gem::Version.new(gem_spec.version.to_s)
|
|
1304
|
+
ChiHome.warn_once(:older_than_spawner, home: newest.to_s, spawner: gem_spec.version.to_s)
|
|
1305
|
+
end
|
|
1306
|
+
fallback = ChiHome.fallback_version(home, gem_spec)
|
|
1307
|
+
activate = if fallback
|
|
1308
|
+
"begin; gem 'samagotchi', '>= 0.a'; rescue LoadError => e; " \
|
|
1309
|
+
"ENV['#{BOOT_FALLBACK_ENV}'] = e.message; gem 'samagotchi', '= #{fallback}'; end"
|
|
1310
|
+
else
|
|
1311
|
+
"gem 'samagotchi', '>= 0.a'"
|
|
1312
|
+
end
|
|
1313
|
+
home.boot_argv(run: "begin; #{activate}; require 'samagotchi/session_manager'; " \
|
|
1314
|
+
"#{HOME_BOOT_FAILED.sub("SID", session_id)}; end; " \
|
|
1315
|
+
"Samagotchi::SessionManager.run_session_loop('#{session_id}', state_dir: #{state_dir.inspect})")
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1265
1318
|
private_class_method def self.spawn_worker_for_session(session, state_dir:)
|
|
1266
1319
|
session_dir = Session.session_dir(session.id, state_dir: state_dir)
|
|
1267
1320
|
FileUtils.mkdir_p(session_dir)
|
|
@@ -61,9 +61,12 @@ module Samagotchi
|
|
|
61
61
|
|
|
62
62
|
# #turn_llm_context with where each value came from
|
|
63
63
|
# (LLMContextStrategy.explain), nil when it can't be resolved.
|
|
64
|
+
# @param own [LLMContextOverride, nil] the session's own values to
|
|
65
|
+
# resolve with (the ones a change would set: /llm-context's confirm)
|
|
64
66
|
# @return [LLMContextStrategy::Explained, nil]
|
|
65
|
-
def llm_context_explained
|
|
66
|
-
|
|
67
|
+
def llm_context_explained(own: :session)
|
|
68
|
+
args = own == :session ? llm_context_session_args : own&.resolve_args || {}
|
|
69
|
+
model_setting(nil) { |model| model.llm_context_explained(args) }
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
# #llm_context_explained's summary (Explained#summary), nil when it
|
|
@@ -14,6 +14,7 @@ require_relative "memory_bundle/index_size"
|
|
|
14
14
|
require_relative "thinking"
|
|
15
15
|
require_relative "prompt_files"
|
|
16
16
|
require_relative "participant_mode"
|
|
17
|
+
require_relative "plugin/prompt_sections"
|
|
17
18
|
|
|
18
19
|
module Samagotchi
|
|
19
20
|
# The system prompt an Engine gives its model: the base prompt (tool
|
|
@@ -57,9 +58,12 @@ module Samagotchi
|
|
|
57
58
|
# @param participants [#call] → ParticipantMode::PAIR / REVIEW while the
|
|
58
59
|
# session has participants (SessionParticipants#prompt_mode; true reads
|
|
59
60
|
# as review), nil or false with none (the #participants_section line)
|
|
61
|
+
# @param sections [#call] → Array<Plugin::PromptSections::Built>, the
|
|
62
|
+
# plugins' prompt sections (chi.prompt_section) run for this build
|
|
60
63
|
def initialize(profile:, tools:, session:, thinking:, model: -> {}, model_notes: -> { [] }, memories: [],
|
|
61
|
-
muted_memory_names: [], appended_files: [], participants: -> { false })
|
|
64
|
+
muted_memory_names: [], appended_files: [], participants: -> { false }, sections: -> { [] })
|
|
62
65
|
@profile_lookup = profile
|
|
66
|
+
@sections_lookup = sections
|
|
63
67
|
@participants_lookup = participants
|
|
64
68
|
@model_lookup = model
|
|
65
69
|
@model_notes_lookup = model_notes
|
|
@@ -97,19 +101,23 @@ module Samagotchi
|
|
|
97
101
|
@index_sizes ||= {}
|
|
98
102
|
@notes_loaded ||= {}
|
|
99
103
|
@files_loaded ||= {}
|
|
104
|
+
@sections_loaded ||= {}
|
|
100
105
|
unless @built.key?(key)
|
|
101
106
|
@section_notes = nil
|
|
102
107
|
@section_files = []
|
|
108
|
+
@section_plugins = []
|
|
103
109
|
@built[key] = system_prompt_with_index(
|
|
104
110
|
assist_system_prompt(chat: chat, layers: layers), chat: chat, thinking: thinking, layers: layers
|
|
105
111
|
)
|
|
106
112
|
@index_sizes[key] = @measured_index
|
|
107
113
|
@notes_loaded[key] = @section_notes || []
|
|
108
114
|
@files_loaded[key] = @section_files.freeze
|
|
115
|
+
@sections_loaded[key] = @section_plugins.freeze
|
|
109
116
|
end
|
|
110
117
|
@memory_index = @index_sizes[key]
|
|
111
118
|
@prompt_notes = @notes_loaded[key]
|
|
112
119
|
@prompt_files = @files_loaded[key]
|
|
120
|
+
@prompt_sections = @sections_loaded[key]
|
|
113
121
|
@built[key]
|
|
114
122
|
end
|
|
115
123
|
|
|
@@ -135,6 +143,12 @@ module Samagotchi
|
|
|
135
143
|
# @return [Array<String>]
|
|
136
144
|
def prompt_files = @prompt_files || []
|
|
137
145
|
|
|
146
|
+
# The plugins' prompt sections the prompt #build returned last holds
|
|
147
|
+
# (Plugin::PromptSections::Built, prompt order), [] before a build and
|
|
148
|
+
# after #reset!.
|
|
149
|
+
# @return [Array<Plugin::PromptSections::Built>]
|
|
150
|
+
def prompt_sections = @prompt_sections || []
|
|
151
|
+
|
|
138
152
|
# Drops the built prompts: the next #build reads the profile, tools,
|
|
139
153
|
# indexes and memories again (a model or profile switch, changed tools).
|
|
140
154
|
def reset!
|
|
@@ -144,6 +158,8 @@ module Samagotchi
|
|
|
144
158
|
@prompt_notes = nil
|
|
145
159
|
@files_loaded = nil
|
|
146
160
|
@prompt_files = nil
|
|
161
|
+
@sections_loaded = nil
|
|
162
|
+
@prompt_sections = nil
|
|
147
163
|
@stable_lengths = nil
|
|
148
164
|
end
|
|
149
165
|
|
|
@@ -298,7 +314,8 @@ module Samagotchi
|
|
|
298
314
|
# prefix): what every session shares first, what changes least before
|
|
299
315
|
# what changes more, and the per-session lines last. The global parts
|
|
300
316
|
# (identity, preloads) come before the project's AGENTS.md, the
|
|
301
|
-
# toolkit's prompt text after the appended files, the
|
|
317
|
+
# toolkit's prompt text after the appended files, then the plugins'
|
|
318
|
+
# prompt sections, the memory indexes (a
|
|
302
319
|
# memory write changes them) after all of them; the model, the location
|
|
303
320
|
# and the session close the prompt (Gemma's tool declarations still
|
|
304
321
|
# follow them).
|
|
@@ -311,7 +328,7 @@ module Samagotchi
|
|
|
311
328
|
"System memories:\n#{indexes[:system]}"
|
|
312
329
|
].join("\n\n")
|
|
313
330
|
stable = [thinking_token + base, not_loaded_line(layers), rg_guidance, system_identity_section, model_notes_section, explicit_memory_section,
|
|
314
|
-
project_specific_description, appended_section, kit_prompt_section, memory_sections]
|
|
331
|
+
project_specific_description, appended_section, kit_prompt_section, plugin_prompt_section, memory_sections]
|
|
315
332
|
stable_text = stable.compact.join("\n")
|
|
316
333
|
volatile = [participants_section, current_model, project_location, current_session].compact
|
|
317
334
|
return with_tail_declarations(stable_text, chat: chat, layers: layers) if volatile.empty?
|
|
@@ -362,6 +379,20 @@ module Samagotchi
|
|
|
362
379
|
registry.respond_to?(:toolkit) ? registry.toolkit&.prompt : nil
|
|
363
380
|
end
|
|
364
381
|
|
|
382
|
+
# The plugins' prompt sections (chi.prompt_section), right after the
|
|
383
|
+
# kit prompt and before the memory indexes: a plugin's text depends on
|
|
384
|
+
# its settings and the model, not on a memory write. Each is
|
|
385
|
+
# "From <bundle> (<name>):\n<text>", sorted by (bundle, name)
|
|
386
|
+
# (Plugin::PromptSections). nil with none: the prompt is what it was
|
|
387
|
+
# before prompt sections.
|
|
388
|
+
def plugin_prompt_section
|
|
389
|
+
built = Array(@sections_lookup.call)
|
|
390
|
+
@section_plugins = built
|
|
391
|
+
Plugin::PromptSections.text(built)
|
|
392
|
+
rescue StandardError
|
|
393
|
+
nil
|
|
394
|
+
end
|
|
395
|
+
|
|
365
396
|
def with_tail_declarations(prompt, chat:, layers:)
|
|
366
397
|
chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations(layers)
|
|
367
398
|
end
|
|
@@ -87,9 +87,10 @@ module Samagotchi
|
|
|
87
87
|
toolkit_flags: toolkit_flags, settings: settings, parent_answers: parent_answers)
|
|
88
88
|
end
|
|
89
89
|
commands = []
|
|
90
|
-
|
|
90
|
+
# --yes on /model and /thinking: typing the flag is the choice (no
|
|
91
|
+
# confirm step); its applied reply ends the launch's command.
|
|
92
|
+
commands << "/model #{model} --yes" if model && (attach || resume)
|
|
91
93
|
commands << LLMContextOverride.command_line(llm_context) if (attach || resume) && !llm_context.empty?
|
|
92
|
-
# --yes: typing the flag is the choice (no confirm step).
|
|
93
94
|
commands << "#{ThinkingCommand::NAME} #{thinking} --yes" if (attach || resume) && thinking
|
|
94
95
|
first_command = commands.size > 1 ? commands : commands.first
|
|
95
96
|
surface = open_surface
|
|
@@ -728,6 +728,9 @@ module Samagotchi
|
|
|
728
728
|
# The files whose text is in the prompt (SystemPrompt#prompt_files).
|
|
729
729
|
files = prompt_files_text(snapshot[:prompt_files])
|
|
730
730
|
lines << "prompt files: #{files}" if files
|
|
731
|
+
# The plugins' prompt sections it holds (chi.prompt_section): name (bundle).
|
|
732
|
+
sections = Array(snapshot[:prompt_sections]).map { |item| "#{item[:name]} (#{item[:bundle]})" }
|
|
733
|
+
lines << "prompt sections: #{sections.join(", ")}" unless sections.empty?
|
|
731
734
|
# The toolkit's prompt text (Engine#stats_snapshot's kit_prompt).
|
|
732
735
|
if (kit = snapshot[:kit_prompt])
|
|
733
736
|
lines << "kit prompt: #{kit[:kit]} (~#{kit[:tokens]} tokens)"
|
|
@@ -101,7 +101,9 @@ module Samagotchi
|
|
|
101
101
|
"Samagotchi::ProcessGroup.stop" => "stops a server's process group: needs a ctx process API",
|
|
102
102
|
'require "samagotchi/token_usage"' => "for CHARS_PER_TOKEN",
|
|
103
103
|
"Samagotchi::TokenUsage::CHARS_PER_TOKEN" => "estimates a tool's tokens: could be a stable constant",
|
|
104
|
-
'require "samagotchi/tool_declarations"' => "loaded for Api.tool_spec's schemas"
|
|
104
|
+
'require "samagotchi/tool_declarations"' => "loaded for Api.tool_spec's schemas",
|
|
105
|
+
"Samagotchi::ProjectEnv" => "checks the worker has a project env (an older chi doesn't)",
|
|
106
|
+
"Samagotchi::ProjectEnv.current" => "starts a server in the session's project env: needs a ctx tools-env API"
|
|
105
107
|
},
|
|
106
108
|
"slow-commands" => {
|
|
107
109
|
'require "samagotchi/guardrails/shell_lex"' => "for ShellLex",
|
|
@@ -101,7 +101,7 @@ module Samagotchi
|
|
|
101
101
|
Plugin::Registries.new(
|
|
102
102
|
commands: SessionCommands.register_builtins(Commands::Registry.new), tools: Tools::Builtins.registry,
|
|
103
103
|
services: Plugin::Services.new, stage_tools: ->(*) {}, toolkits: [], tool_index: Tools::ToolIndex.new,
|
|
104
|
-
peer_sources: PeerSources.new, **fields
|
|
104
|
+
peer_sources: PeerSources.new, prompt_sections: Plugin::PromptSections.new, **fields
|
|
105
105
|
)
|
|
106
106
|
end
|
|
107
107
|
end
|
|
@@ -138,6 +138,9 @@ module Samagotchi
|
|
|
138
138
|
attr_reader :errors
|
|
139
139
|
# @return [Integer] how often chi was told the tools changed
|
|
140
140
|
attr_reader :tools_changes
|
|
141
|
+
# @return [Integer] how often chi was told the prompt sections changed
|
|
142
|
+
# (chi.prompt_changed!)
|
|
143
|
+
attr_reader :prompt_changes
|
|
141
144
|
|
|
142
145
|
def initialize(dir, settings: {}, host: {}, session_settings: {}, sandbox: nil)
|
|
143
146
|
@dir = File.expand_path(dir)
|
|
@@ -163,6 +166,7 @@ module Samagotchi
|
|
|
163
166
|
@errors = []
|
|
164
167
|
@lenient = false
|
|
165
168
|
@tools_changes = 0
|
|
169
|
+
@prompt_changes = 0
|
|
166
170
|
@contexts = {}
|
|
167
171
|
@registries = build_registries
|
|
168
172
|
end
|
|
@@ -261,12 +265,16 @@ module Samagotchi
|
|
|
261
265
|
# Run a command line ("/name args") as chi does: the first matching
|
|
262
266
|
# command, given the text after its name. Returns the handler's value
|
|
263
267
|
# (a raise propagates: chi would show it as an error result).
|
|
264
|
-
|
|
268
|
+
# @param confirm [Boolean] sent by a person at a web tab or a TUI, who
|
|
269
|
+
# can answer ctx.cache_confirm (false: chi send, a delegate); a line
|
|
270
|
+
# with --yes never asks, as in chi
|
|
271
|
+
def command(line, confirm: false)
|
|
265
272
|
entry = commands.lookup(line)
|
|
266
273
|
raise ArgumentError, "no command matches #{line.inspect}" unless entry
|
|
267
274
|
raise ArgumentError, "#{entry.name} is chi's own command, not the bundle's" if entry.source == "core"
|
|
268
275
|
|
|
269
|
-
|
|
276
|
+
args = line.to_s.strip.delete_prefix(entry.name).strip
|
|
277
|
+
failing { CacheConfirm.asking(confirm && !CacheConfirm.split(args).last) { entry.handler.call(args) } }
|
|
270
278
|
end
|
|
271
279
|
|
|
272
280
|
# Call a registered tool as chi's dispatch does: its handler gets the
|
|
@@ -324,6 +332,16 @@ module Samagotchi
|
|
|
324
332
|
# @return [PeerSources]
|
|
325
333
|
def peer_sources = @registries.peer_sources
|
|
326
334
|
|
|
335
|
+
# The prompt sections as chi's next prompt build runs them, for the
|
|
336
|
+
# session now (its id: a failure keeps that session's last good text).
|
|
337
|
+
# @return [Array<Plugin::PromptSections::Built>]
|
|
338
|
+
def prompt_sections = @registries.prompt_sections.render(@recorder[:session_id])
|
|
339
|
+
|
|
340
|
+
# The text they add to the system prompt, as chi puts it there; nil
|
|
341
|
+
# with none.
|
|
342
|
+
# @return [String, nil]
|
|
343
|
+
def prompt_text = Plugin::PromptSections.text(prompt_sections)
|
|
344
|
+
|
|
327
345
|
# The bundle's own command entries.
|
|
328
346
|
def bundle_commands = commands.entries.select { |entry| entry.source == @name }
|
|
329
347
|
# The bundle's own tool entries.
|
|
@@ -401,6 +419,7 @@ module Samagotchi
|
|
|
401
419
|
env: ENV, sessions: @sessions, http_transport: @http)
|
|
402
420
|
},
|
|
403
421
|
tools_changed: -> { @tools_changes += 1 },
|
|
422
|
+
prompt_changed: -> { @prompt_changes += 1 },
|
|
404
423
|
stage_tools: ->(_bundle, specs, context) { @staged_tools << StagedTools.new(specs: specs, context: context) },
|
|
405
424
|
init: lambda { |_bundle, label, plugin_label, provides_tools:, quiet:, timeout:, failed: nil, &block|
|
|
406
425
|
@inits << Init.new(label: label, plugin_label: plugin_label, provides_tools: provides_tools, quiet: quiet,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../cancellation_controller"
|
|
4
|
+
require_relative "../cache_confirm"
|
|
4
5
|
require_relative "../plugin/context"
|
|
5
6
|
require_relative "../session_metrics/cost"
|
|
6
7
|
require_relative "../session_metrics/usage"
|
|
@@ -12,6 +13,14 @@ module Samagotchi
|
|
|
12
13
|
# with every member set, its values read from the Recorder at each call
|
|
13
14
|
# and its actions landing in it.
|
|
14
15
|
module Host
|
|
16
|
+
# What CacheConfirm.ask reads of an Engine, from the Recorder's
|
|
17
|
+
# history: and context_tokens:.
|
|
18
|
+
CacheEngine = Struct.new(:history, :tokens) do
|
|
19
|
+
def history? = history ? true : false
|
|
20
|
+
def cache_restart_tokens = tokens
|
|
21
|
+
def cache_restart_anthropic? = false
|
|
22
|
+
end
|
|
23
|
+
|
|
15
24
|
module_function
|
|
16
25
|
|
|
17
26
|
# @param recorder [Recorder]
|
|
@@ -37,6 +46,7 @@ module Samagotchi
|
|
|
37
46
|
},
|
|
38
47
|
model_name: -> { recorder[:model_name] },
|
|
39
48
|
model_key: -> { recorder[:model_key] },
|
|
49
|
+
model_names: -> { recorder[:model_names] || model_names(recorder[:model_name]) },
|
|
40
50
|
state_dir: -> { recorder[:state_dir] || Session.default_state_dir },
|
|
41
51
|
scratch: -> { recorder[:scratch] },
|
|
42
52
|
setup: -> { recorder[:setup] },
|
|
@@ -52,10 +62,21 @@ module Samagotchi
|
|
|
52
62
|
cancel_controller: ->(handler: false) { recorder[:cancelled] ? stopped : nil },
|
|
53
63
|
cost: -> { cost(recorder[:cost]) },
|
|
54
64
|
usage: -> { usage(recorder[:usage]) },
|
|
55
|
-
turn_author: -> { recorder[:turn_author] }
|
|
65
|
+
turn_author: -> { recorder[:turn_author] },
|
|
66
|
+
cache_confirm: lambda { |line, changed|
|
|
67
|
+
CacheConfirm.ask(line: line, cost: :full, changed: changed,
|
|
68
|
+
engine: CacheEngine.new(recorder[:history], recorder[:context_tokens]))
|
|
69
|
+
}
|
|
56
70
|
)
|
|
57
71
|
end
|
|
58
72
|
|
|
73
|
+
# ctx.model_names for a model ref when the test sets none: the ref,
|
|
74
|
+
# then its id without the host ("test:model" → test:model, model).
|
|
75
|
+
def model_names(ref)
|
|
76
|
+
ref = ref.to_s
|
|
77
|
+
[ref, ref.split(":", 2).last].reject(&:empty?).uniq
|
|
78
|
+
end
|
|
79
|
+
|
|
59
80
|
# The parts a test may give bundle.set(cost: {...}).
|
|
60
81
|
COST_PARTS = %i[reported estimated side known].freeze
|
|
61
82
|
|
|
@@ -43,7 +43,12 @@ module Samagotchi
|
|
|
43
43
|
# callable giving one.
|
|
44
44
|
usage: nil,
|
|
45
45
|
# ctx.turn_author and the hooks' author: (a participant's "p:<id>"), nil for the owner.
|
|
46
|
-
turn_author: nil
|
|
46
|
+
turn_author: nil,
|
|
47
|
+
# ctx.cache_confirm: whether the session has a model reply (a cache
|
|
48
|
+
# to restart), and the context size its confirm names (nil: none).
|
|
49
|
+
history: false, context_tokens: nil,
|
|
50
|
+
# ctx.model_names: nil gives model_name and its id without the host.
|
|
51
|
+
model_names: nil
|
|
47
52
|
}.freeze
|
|
48
53
|
|
|
49
54
|
attr_reader :notices, :cards, :steers, :stops, :questions, :model_requests, :toolkit_uses, :tool_loads, :answers,
|
|
@@ -11,7 +11,7 @@ module Samagotchi
|
|
|
11
11
|
# The real Plugin::Api, also recording each declaration in order
|
|
12
12
|
# (#declarations), for a spec of what #register declares as written.
|
|
13
13
|
class RecordingApi < Plugin::Api
|
|
14
|
-
DECLARATIONS = %i[command annotation tool on service init toolkit tool_source peer_source].freeze
|
|
14
|
+
DECLARATIONS = %i[command annotation tool on service init toolkit tool_source peer_source prompt_section].freeze
|
|
15
15
|
|
|
16
16
|
# @return [Array<Declaration>]
|
|
17
17
|
def declarations = (@declarations ||= [])
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "registry"
|
|
4
4
|
require_relative "../tool_declarations"
|
|
5
5
|
require_relative "../paths"
|
|
6
|
+
require_relative "../project_env"
|
|
6
7
|
require_relative "execute"
|
|
7
8
|
require_relative "read"
|
|
8
9
|
require_relative "write"
|
|
@@ -91,7 +92,7 @@ module Samagotchi
|
|
|
91
92
|
end,
|
|
92
93
|
Execute::NAME => lambda do |call, kctx|
|
|
93
94
|
Execute.call(call[:content], cwd: call[:cwd], cancelled: cancelled_proc(kctx), stop_reason: stop_reason_proc(kctx),
|
|
94
|
-
**participant_execute(kctx, parent_env(kctx)))
|
|
95
|
+
**participant_execute(kctx, project_env(kctx).merge(parent_env(kctx))))
|
|
95
96
|
end,
|
|
96
97
|
RegisterReminder::NAME => lambda do |call, kctx|
|
|
97
98
|
RegisterReminder.call(call[:content], reminder_store: kctx.reminder_store, description: call[:description],
|
|
@@ -145,6 +146,16 @@ module Samagotchi
|
|
|
145
146
|
SESSION_REASONING_HISTORY_ENV => history }.merge(cli_model_unset(env)).merge(xdg_absolute(env))
|
|
146
147
|
end
|
|
147
148
|
|
|
149
|
+
# The session's project env (ProjectEnv.current: the worker's mise env,
|
|
150
|
+
# the chi shim first on PATH) for the commands the owner's turns run;
|
|
151
|
+
# less what every tool runs without (TaskRuntime::SANITIZED_ENV_KEYS).
|
|
152
|
+
# A participant's turn gets none of it: a project's [env] may load its
|
|
153
|
+
# .env files.
|
|
154
|
+
def project_env(kctx)
|
|
155
|
+
mode = kctx.respond_to?(:participant_mode) ? kctx.participant_mode : nil
|
|
156
|
+
mode ? {} : ProjectEnv.tools_env
|
|
157
|
+
end
|
|
158
|
+
|
|
148
159
|
# execute's env: and on_timeout: for the running turn. A participant's
|
|
149
160
|
# turn runs without the variables whose names hold a secret
|
|
150
161
|
# (Guardrails::PairBoundary.scrubbed_env), and a command still running
|
|
@@ -179,7 +190,7 @@ module Samagotchi
|
|
|
179
190
|
# markers are reserved, the model's env can't set them).
|
|
180
191
|
def task_marker_env(kctx, env)
|
|
181
192
|
given = env.is_a?(Hash) ? env.keys.map(&:to_s) : []
|
|
182
|
-
parent_env(kctx).reject { |key, _| given.include?(key) }
|
|
193
|
+
project_env(kctx).merge(parent_env(kctx)).reject { |key, _| given.include?(key) }
|
|
183
194
|
end
|
|
184
195
|
|
|
185
196
|
# Stop flips the turn's controller, seen through the Engine's PeerView
|