samagotchi 0.57.0 → 0.59.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 +56 -1
- data/docs/architecture.md +3 -3
- data/docs/cli.md +5 -4
- data/docs/configuration.md +40 -23
- data/docs/hooks.md +2 -2
- data/docs/internals/background-tasks.md +7 -0
- data/docs/internals/engine.md +5 -5
- data/docs/internals/prompt-caching.md +2 -2
- data/docs/internals/tool-guardrails.md +2 -1
- data/docs/plugins.md +26 -6
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
- data/lib/samagotchi/bridge.rb +6 -2
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +5 -4
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +169 -16
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/config.rb +11 -5
- data/lib/samagotchi/engine.rb +28 -3
- data/lib/samagotchi/host_config.rb +3 -1
- data/lib/samagotchi/host_registry.rb +2 -1
- data/lib/samagotchi/model_profile.rb +14 -3
- data/lib/samagotchi/model_settings.rb +4 -2
- data/lib/samagotchi/process_group.rb +76 -3
- data/lib/samagotchi/redacted_inspect.rb +21 -6
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session_commands.rb +19 -6
- data/lib/samagotchi/steer_cut.rb +12 -8
- data/lib/samagotchi/steer_cut_after.rb +69 -0
- data/lib/samagotchi/system_prompt.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -4
- data/lib/samagotchi/terminal_ui/formatting.rb +9 -5
- data/lib/samagotchi/terminal_ui.rb +15 -6
- data/lib/samagotchi/tool_activity.rb +18 -1
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tools/execute.rb +318 -70
- data/lib/samagotchi/tools/task_get.rb +2 -0
- data/lib/samagotchi/tools/task_runtime.rb +96 -13
- data/lib/samagotchi/tools/task_wait.rb +23 -5
- data/lib/samagotchi/utf8_default.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +8 -2
- data/lib/samagotchi/web/public/app.js +7 -6
- data/lib/samagotchi/web/public/index.html +20 -7
- data/lib/samagotchi/web/public/read_view.js +3 -1
- data/lib/samagotchi/web/public/ref_menu.js +188 -0
- data/lib/samagotchi/web/public/ref_scan.js +11 -1
- data/lib/samagotchi/web/public/refs.js +271 -51
- data/lib/samagotchi/web/public/stage_view.js +21 -0
- data/lib/samagotchi/web/public/task_stop.js +25 -7
- data/lib/samagotchi/worker.rb +2 -2
- metadata +3 -1
|
@@ -2,21 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
# A single redacted #inspect for the classes that hold the process
|
|
5
|
-
# environment (@env, every API key) or the web token (@token
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
5
|
+
# environment (@env, every API key) or the web token (@token, or a :token
|
|
6
|
+
# key inside a Hash ivar, as Web::App's @lan): instead of dumping every
|
|
7
|
+
# instance variable (which would print the keys), it shows the rest with
|
|
8
|
+
# each value's own inspect and prints the others as [redacted]. A failing
|
|
9
|
+
# spec, a debugger or an error message that inspects one of them used to
|
|
10
|
+
# leak every key.
|
|
10
11
|
module RedactedInspect
|
|
11
12
|
# The instance variables never printed.
|
|
12
13
|
REDACTED = %i[@env @token].freeze
|
|
14
|
+
# The keys of a Hash ivar whose values are never printed; the rest of the
|
|
15
|
+
# Hash is, so Web::App's @lan still shows its :ip.
|
|
16
|
+
REDACTED_KEYS = %i[token].freeze
|
|
17
|
+
|
|
18
|
+
# Stands in for a redacted Hash value: inspects as [redacted], unquoted.
|
|
19
|
+
HIDDEN = Object.new.tap { |hidden| hidden.define_singleton_method(:inspect) { "[redacted]" } }.freeze
|
|
13
20
|
|
|
14
21
|
def inspect
|
|
15
22
|
body = instance_variables.map do |ivar|
|
|
16
23
|
if REDACTED.include?(ivar)
|
|
17
24
|
"#{ivar}=[redacted]"
|
|
18
25
|
else
|
|
19
|
-
"#{ivar}=#{instance_variable_get(ivar).inspect}"
|
|
26
|
+
"#{ivar}=#{redacted_value(instance_variable_get(ivar)).inspect}"
|
|
20
27
|
end
|
|
21
28
|
end.join(", ")
|
|
22
29
|
"#<#{self.class.name} #{body}>"
|
|
@@ -24,5 +31,13 @@ module Samagotchi
|
|
|
24
31
|
|
|
25
32
|
# pp otherwise ignores inspect.
|
|
26
33
|
def pretty_print(printer) = printer.text(inspect)
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def redacted_value(value)
|
|
38
|
+
return value unless value.is_a?(Hash) && REDACTED_KEYS.any? { |key| value.key?(key) }
|
|
39
|
+
|
|
40
|
+
value.to_h { |key, item| [key, REDACTED_KEYS.include?(key) ? HIDDEN : item] }
|
|
41
|
+
end
|
|
27
42
|
end
|
|
28
43
|
end
|
|
@@ -586,7 +586,7 @@ module Samagotchi
|
|
|
586
586
|
outcome = Delivery.cut?(delivery) ? ack["cut"].to_s : ""
|
|
587
587
|
return " (goes in at the running turn's next step)" unless CUT_OUTCOMES.include?(outcome)
|
|
588
588
|
|
|
589
|
-
" #{TerminalUI::Formatting.steer_cut_note(outcome.to_sym)}"
|
|
589
|
+
" #{TerminalUI::Formatting.steer_cut_note(outcome.to_sym, ack["cut_after"])}"
|
|
590
590
|
end
|
|
591
591
|
|
|
592
592
|
# More keys for --format json, with --cut only (the line is unchanged
|
|
@@ -188,12 +188,12 @@ module Samagotchi
|
|
|
188
188
|
# @return [String, nil]
|
|
189
189
|
def self.builtin_name(line) = builtin_registry.lookup(line)&.name
|
|
190
190
|
|
|
191
|
-
# @return [String] the model /model clear goes back to
|
|
191
|
+
# @return [String, nil] the model /model clear goes back to; nil when none
|
|
192
192
|
attr_reader :default_model
|
|
193
193
|
|
|
194
|
-
# @param default_model [String] the config default, which /model clear
|
|
194
|
+
# @param default_model [String, nil] the config default, which /model clear
|
|
195
195
|
# restores and /model names (the Engine may have started elsewhere, e.g.
|
|
196
|
-
# on a resumed session's model)
|
|
196
|
+
# on a resumed session's model); nil when the config names no default
|
|
197
197
|
# @param save [#call] saves a session (a worker passes its state dir)
|
|
198
198
|
# @param registry [Commands::Registry] the commands #run looks lines up in
|
|
199
199
|
def initialize(engine:, turn_flow:, default_model:, save: ->(session) { session.save },
|
|
@@ -328,7 +328,9 @@ module Samagotchi
|
|
|
328
328
|
command = text.delete_prefix(SHELL_BANG_PREFIX).strip
|
|
329
329
|
return reply("!: please provide a shell command after '!'", status: :error) if command.empty?
|
|
330
330
|
|
|
331
|
-
|
|
331
|
+
# Killed at the limit, never handed over: the output goes in a user
|
|
332
|
+
# message, which no one reads a task id from (created_ids_in).
|
|
333
|
+
output = Samagotchi::Tools::Execute.call(command, env: Samagotchi::Tools::Builtins.cli_model_unset, on_timeout: :kill)
|
|
332
334
|
@engine.append_messages([{ role: "user", content: "!(#{command})\n#{output}" }])
|
|
333
335
|
# Rolling back past this would silently drop the command output.
|
|
334
336
|
@turn_flow.note_conversation_changed
|
|
@@ -484,9 +486,9 @@ module Samagotchi
|
|
|
484
486
|
def model_command(input)
|
|
485
487
|
suffix = input.delete_prefix(MODEL_COMMAND).strip
|
|
486
488
|
if suffix.empty?
|
|
487
|
-
return ["runtime model: #{model_name}#{model_note}#{served_note}#{sampling_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", false] if model_name == @engine.model_ref_for(@default_model)
|
|
489
|
+
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)
|
|
488
490
|
|
|
489
|
-
return ["runtime model: #{model_name}#{model_note("default: #{@default_model}")}#{served_note}#{sampling_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", false]
|
|
491
|
+
return ["runtime model: #{model_name}#{model_note("default: #{@default_model}")}#{served_note}#{sampling_note}#{steer_cut_after_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", false]
|
|
490
492
|
end
|
|
491
493
|
|
|
492
494
|
# Parse flags: --default and --alias <name> / --alias=<name> (tolerant order)
|
|
@@ -554,6 +556,7 @@ module Samagotchi
|
|
|
554
556
|
|
|
555
557
|
if ConfigFile::RESERVED_MODEL_ALIASES.include?(arg.downcase)
|
|
556
558
|
return ["--default cannot be combined with clear/default/none/off", false] if persist_default
|
|
559
|
+
raise ModelProfile::MissingModel, "no default model is configured (set default.model in config.yml, or /model <name> --default)" if @default_model.nil?
|
|
557
560
|
|
|
558
561
|
switch_model(@default_model)
|
|
559
562
|
return ["runtime model reset to #{model_name}#{model_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", true]
|
|
@@ -605,6 +608,16 @@ module Samagotchi
|
|
|
605
608
|
""
|
|
606
609
|
end
|
|
607
610
|
|
|
611
|
+
# "; steer cut: after 60 s (models: qwen)", or "; steer cut: never
|
|
612
|
+
# (hosts.splash)" for 0 or less, when the model has one configured;
|
|
613
|
+
# "" for the default.
|
|
614
|
+
def steer_cut_after_note
|
|
615
|
+
summary = @engine.steer_cut_summary
|
|
616
|
+
summary ? "; steer cut: #{summary}" : ""
|
|
617
|
+
rescue StandardError
|
|
618
|
+
""
|
|
619
|
+
end
|
|
620
|
+
|
|
608
621
|
# "; notes: model_notes_deepseek (system, 612 chars)": the model notes
|
|
609
622
|
# the session's prompt carries (after a switch, the ones the new
|
|
610
623
|
# model's prompt loads); "" without any.
|
data/lib/samagotchi/steer_cut.rb
CHANGED
|
@@ -30,19 +30,23 @@ module Samagotchi
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# +clock+: monotonic seconds; +controller+: the running turn's cancel
|
|
33
|
-
# controller (nil between turns), read only when a cut is due
|
|
34
|
-
|
|
33
|
+
# controller (nil between turns), read only when a cut is due;
|
|
34
|
+
# +cut_after+: seconds a thinking-only generation may stream before a
|
|
35
|
+
# cut, called on every check (the Engine passes the model's own
|
|
36
|
+
# SteerCutAfter; the global steer.cut_after here).
|
|
37
|
+
def initialize(clock:, controller:, cut_after: -> { self.class.cut_after })
|
|
35
38
|
# What the running generation streams: whether a steer may cut it.
|
|
36
39
|
@phase = GenerationPhase.new(clock: clock)
|
|
37
40
|
# A message that may cut it but came too early: it cuts later.
|
|
38
41
|
@waiting = WaitingSteer.new
|
|
42
|
+
@cut_after = cut_after
|
|
39
43
|
@controller = controller
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
# A message for the running turn from +source+ (Steer.source_for_client:
|
|
43
47
|
# nil for the user, "chi_send", "parent_agent") cuts the streaming
|
|
44
|
-
# generation when it has streamed only thinking for
|
|
45
|
-
#
|
|
48
|
+
# generation when it has streamed only thinking for cut_after seconds
|
|
49
|
+
# (GenerationPhase): the loops' cut path (CutPolicy) then starts
|
|
46
50
|
# the step again with the message, no nudge, no retry spent. Too early,
|
|
47
51
|
# the message waits (WaitingSteer) and cuts once the thinking passes it
|
|
48
52
|
# (#recheck, on each thinking chunk), unless a boundary hands it to the
|
|
@@ -52,13 +56,13 @@ module Samagotchi
|
|
|
52
56
|
# before queueing it. Outside any lock; a cut that lands just after the
|
|
53
57
|
# generation ended is harmless (the loop re-asks).
|
|
54
58
|
# @return [Symbol] :now (a generation was cut now), :waits (the message
|
|
55
|
-
# waits for the thinking to pass
|
|
59
|
+
# waits for the thinking to pass cut_after), or :off (cutting
|
|
56
60
|
# off for this source: a plugin, no turn, cut_after 0, a generation
|
|
57
61
|
# that isn't thinking-only, or a drain already took the message)
|
|
58
62
|
def cut_for_steer(source, epoch: nil)
|
|
59
63
|
return :off unless Steer.cuts?(source)
|
|
60
64
|
|
|
61
|
-
after =
|
|
65
|
+
after = @cut_after.call.to_i
|
|
62
66
|
return :off unless after.positive?
|
|
63
67
|
|
|
64
68
|
ctrl = @controller.call
|
|
@@ -107,11 +111,11 @@ module Samagotchi
|
|
|
107
111
|
private
|
|
108
112
|
|
|
109
113
|
# A thinking chunk streamed: a waiting message cuts once the thinking
|
|
110
|
-
# passes
|
|
114
|
+
# passes cut_after.
|
|
111
115
|
def recheck
|
|
112
116
|
return unless @waiting.waiting?
|
|
113
117
|
|
|
114
|
-
after =
|
|
118
|
+
after = @cut_after.call.to_i
|
|
115
119
|
return unless after.positive? && @phase.cuttable?(after)
|
|
116
120
|
|
|
117
121
|
source = @waiting.take
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# How long a message sent as a cut waits for the generation it came during:
|
|
7
|
+
# the seconds a generation must have streamed only thinking before a cut may
|
|
8
|
+
# land (SteerCut), one per model, host or run, like Thinking for the level.
|
|
9
|
+
#
|
|
10
|
+
# Order (#resolve): the env (SAMAGOTCHI_STEER_CUT_AFTER), then
|
|
11
|
+
# models.<key>.steer_cut_after, hosts.<name>.steer_cut_after,
|
|
12
|
+
# steer.cut_after in config.yml, then 20. 0 = never cut (a slow local
|
|
13
|
+
# thinker is left alone); it is a value, not an unset.
|
|
14
|
+
module SteerCutAfter
|
|
15
|
+
KEY = "steer_cut_after"
|
|
16
|
+
ENV_KEY = "SAMAGOTCHI_STEER_CUT_AFTER"
|
|
17
|
+
DEFAULT = 20
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# A `steer_cut_after:` setting (hosts: entry or models: entry): whole
|
|
22
|
+
# seconds ≥ 0 (0 = never), nil when unset; anything else warns once,
|
|
23
|
+
# naming +where+, and counts as unset.
|
|
24
|
+
# @return [Integer, nil]
|
|
25
|
+
def seconds(value, where)
|
|
26
|
+
return nil if value.nil?
|
|
27
|
+
|
|
28
|
+
text = value.to_s.strip
|
|
29
|
+
return text.to_i if text.match?(/\A\d+\z/)
|
|
30
|
+
|
|
31
|
+
ConfigFile.warn_once "Warning: #{where}: steer_cut_after must be whole seconds (0 = never); ignored"
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The seconds for +target+ and where they came from
|
|
36
|
+
# ("SAMAGOTCHI_STEER_CUT_AFTER", "models: qwen", "hosts.splash",
|
|
37
|
+
# "steer.cut_after"; nil for the default).
|
|
38
|
+
# @param target [HostRegistry::ModelTarget]
|
|
39
|
+
# @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
|
|
40
|
+
# @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
|
|
41
|
+
# @return [Array(Integer, String|nil)]
|
|
42
|
+
def resolve(target, names:, models: nil)
|
|
43
|
+
value, origin = Config.get_with_origin("steer.cut_after")
|
|
44
|
+
return [value.to_i, ENV_KEY] if origin == :env
|
|
45
|
+
|
|
46
|
+
models ||= begin
|
|
47
|
+
ConfigFile.model_settings
|
|
48
|
+
rescue StandardError
|
|
49
|
+
{}
|
|
50
|
+
end
|
|
51
|
+
setting = ConfigFile.model_setting(names, :steer_cut_after, models: models)
|
|
52
|
+
return [setting.value, "models: #{setting.key}"] if setting
|
|
53
|
+
|
|
54
|
+
host = target.entry.steer_cut_after
|
|
55
|
+
return [host, "hosts.#{target.entry.name}"] unless host.nil?
|
|
56
|
+
|
|
57
|
+
return [value.to_i, "steer.cut_after"] if origin == :file
|
|
58
|
+
|
|
59
|
+
[DEFAULT, nil]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# "after 60 s (models: qwen)"; 0 or less is "never (hosts.splash)".
|
|
63
|
+
# No source: just the seconds.
|
|
64
|
+
def label(seconds, source)
|
|
65
|
+
text = seconds.to_i <= 0 ? "never" : "after #{seconds} s"
|
|
66
|
+
source ? "#{text} (#{source})" : text
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -16,7 +16,7 @@ module Samagotchi
|
|
|
16
16
|
# The system prompt an Engine gives its model: the base prompt (tool
|
|
17
17
|
# declarations and call syntax for the raw-prompt loop, none for the chat
|
|
18
18
|
# loop, the shared guidance) and around it the thinking token, rg
|
|
19
|
-
# guidance, the identity memory, the model notes, the preloaded memories,
|
|
19
|
+
# guidance, the identity memory, the model notes, the preloaded memories, AGENTS.md, the
|
|
20
20
|
# memory indexes and, last, the model, where the session runs and the
|
|
21
21
|
# session (#system_prompt_with_index has the order and why).
|
|
22
22
|
#
|
|
@@ -24,7 +24,7 @@ module Samagotchi
|
|
|
24
24
|
# tools a plugin replaces, the attached session, the thinking level) is
|
|
25
25
|
# read at build time through the lookups; the memory lists are fixed.
|
|
26
26
|
class SystemPrompt
|
|
27
|
-
AGENT_DESCRIPTION_FILE = "
|
|
27
|
+
AGENT_DESCRIPTION_FILE = "AGENTS.md"
|
|
28
28
|
|
|
29
29
|
# The model a session runs on, as the prompt names it: the resolved
|
|
30
30
|
# ref, its host ("name, host:port" or url), the memory overlay key, and
|
|
@@ -282,7 +282,7 @@ module Samagotchi
|
|
|
282
282
|
# Ordered for the servers' prompt caches (they reuse only an exact token
|
|
283
283
|
# prefix): what every session shares first, what changes least before
|
|
284
284
|
# what changes more, and the per-session lines last. The global parts
|
|
285
|
-
# (identity, preloads) come before the project's
|
|
285
|
+
# (identity, preloads) come before the project's AGENTS.md, the memory
|
|
286
286
|
# indexes (a memory write changes them) after both; the model, the
|
|
287
287
|
# location and the session close the prompt (Gemma's tool declarations
|
|
288
288
|
# still follow them).
|
|
@@ -464,7 +464,7 @@ module Samagotchi
|
|
|
464
464
|
|
|
465
465
|
# ── Project / rg helpers ───────────────────────────────────────────────────
|
|
466
466
|
|
|
467
|
-
#
|
|
467
|
+
# AGENTS.md in the cwd, else at the top of its git work tree.
|
|
468
468
|
def project_specific_description
|
|
469
469
|
return nil if skip_agent_description?
|
|
470
470
|
|
|
@@ -566,7 +566,7 @@ module Samagotchi
|
|
|
566
566
|
end
|
|
567
567
|
|
|
568
568
|
def skip_agent_description?
|
|
569
|
-
Config.get("
|
|
569
|
+
Config.get("skip_agents_md") == true
|
|
570
570
|
end
|
|
571
571
|
|
|
572
572
|
def rg_available?
|
|
@@ -712,7 +712,7 @@ module Samagotchi
|
|
|
712
712
|
|
|
713
713
|
return NEXT_STEP_NOTE unless delivery == Delivery::CUT
|
|
714
714
|
|
|
715
|
-
steer_cut_note(ack["cut"].to_s.to_sym)
|
|
715
|
+
steer_cut_note(ack["cut"].to_s.to_sym, ack["cut_after"])
|
|
716
716
|
end
|
|
717
717
|
|
|
718
718
|
def send_prompt(text, delivery: nil)
|
|
@@ -1456,9 +1456,7 @@ module Samagotchi
|
|
|
1456
1456
|
|
|
1457
1457
|
# The config's default model, as the worker's /model names it.
|
|
1458
1458
|
def default_model_name
|
|
1459
|
-
@default_model_name ||= ModelProfile.
|
|
1460
|
-
rescue StandardError
|
|
1461
|
-
nil
|
|
1459
|
+
@default_model_name ||= ModelProfile.default_model_name
|
|
1462
1460
|
end
|
|
1463
1461
|
|
|
1464
1462
|
def reminder_origin?(origin) = origin[:client_id].to_s.start_with?(ClientId::SYSTEM_PREFIX)
|
|
@@ -99,8 +99,9 @@ module Samagotchi
|
|
|
99
99
|
# What a line submitted at the open prompt says became of it: a plain
|
|
100
100
|
# line goes in at the running turn's next step boundary (nothing cuts
|
|
101
101
|
# unless the message asked for it), /cut's cutting now or once the
|
|
102
|
-
# thinking passes
|
|
103
|
-
#
|
|
102
|
+
# thinking passes the model's cut wait (the run's cut_after; the global
|
|
103
|
+
# steer.cut_after when the worker didn't say), and /queue's waiting for
|
|
104
|
+
# the end. Both TUIs and chi send --cut print the same words
|
|
104
105
|
# (SteerCut#cut_for_steer's answer).
|
|
105
106
|
NEXT_STEP_NOTE = "(goes in at the next step)"
|
|
106
107
|
CUT_NOW_NOTE = "(cut in now)"
|
|
@@ -116,17 +117,20 @@ module Samagotchi
|
|
|
116
117
|
STEER_CHOICE = /\A(#{CUT_COMMAND}|#{QUEUE_COMMAND})(?:\s+(.*))?\z/m
|
|
117
118
|
|
|
118
119
|
# @param outcome [Symbol] SteerCut#cut_for_steer's answer: :now, :waits or :off
|
|
120
|
+
# @param after [Integer, nil] the run's cut wait (the model's own
|
|
121
|
+
# SteerCutAfter): the seconds a :waits waits for; the global
|
|
122
|
+
# steer.cut_after when not given.
|
|
119
123
|
# @return [String] the dim line to show for a /cut
|
|
120
|
-
def self.steer_cut_note(outcome)
|
|
124
|
+
def self.steer_cut_note(outcome, after = nil)
|
|
121
125
|
case outcome
|
|
122
126
|
when :now then CUT_NOW_NOTE
|
|
123
|
-
when :waits then format(CUT_WAITS_NOTE, SteerCut.cut_after)
|
|
127
|
+
when :waits then format(CUT_WAITS_NOTE, after || SteerCut.cut_after)
|
|
124
128
|
else CUT_OFF_NOTE
|
|
125
129
|
end
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
# The same words, for a UI that includes the module.
|
|
129
|
-
def steer_cut_note(outcome) = Formatting.steer_cut_note(outcome)
|
|
133
|
+
def steer_cut_note(outcome, after = nil) = Formatting.steer_cut_note(outcome, after)
|
|
130
134
|
|
|
131
135
|
# The choice a mid-turn line carries, as [:cut, "TEXT"] or
|
|
132
136
|
# [:queue, "TEXT"]; nil for any other line. The text is "" when the
|
|
@@ -98,7 +98,7 @@ module Samagotchi
|
|
|
98
98
|
@thinking_word = thinking
|
|
99
99
|
@prompt = prompt
|
|
100
100
|
@llm_context_words = llm_context
|
|
101
|
-
@default_model_name = ModelProfile.
|
|
101
|
+
@default_model_name = ModelProfile.default_model_name
|
|
102
102
|
# As typed: the Engine applies an alias where it resolves the model.
|
|
103
103
|
flag_model_name = model_name.to_s.strip.empty? ? nil : ModelProfile.required_model_name(model_name)
|
|
104
104
|
@effective_model_name = flag_model_name || @default_model_name
|
|
@@ -127,6 +127,12 @@ module Samagotchi
|
|
|
127
127
|
# Re-resolve client after resume may change effective model
|
|
128
128
|
@client = @host_registry.resolve(@effective_model_name).client
|
|
129
129
|
end
|
|
130
|
+
# No --model, no session model and no default: nothing to run.
|
|
131
|
+
raise ModelProfile::MissingModel, ModelProfile.missing_model_message if @effective_model_name.to_s.strip.empty?
|
|
132
|
+
|
|
133
|
+
# With no config default the Engine starts on the model it runs.
|
|
134
|
+
no_default = @default_model_name.nil?
|
|
135
|
+
|
|
130
136
|
# The model it starts on names a configured host (a bad default.model
|
|
131
137
|
# doesn't matter when --model or the session picks another).
|
|
132
138
|
ModelProfile.check_host!(@effective_model_name, hosts: @host_registry.entries)
|
|
@@ -166,8 +172,10 @@ module Samagotchi
|
|
|
166
172
|
session_id: session_id,
|
|
167
173
|
no_interrupt: no_interrupt,
|
|
168
174
|
# nil: the config default, unchecked here (the model the REPL starts
|
|
169
|
-
# on is checked above, and --model may make the default moot).
|
|
170
|
-
|
|
175
|
+
# on is checked above, and --model may make the default moot). With
|
|
176
|
+
# no default, the model it runs (the switch below is then skipped).
|
|
177
|
+
model_name: no_default ? @effective_model_name : nil,
|
|
178
|
+
model_typed: no_default ? @resumed_model_typed : nil,
|
|
171
179
|
memories: @requested_memories,
|
|
172
180
|
muted_memories: @muted_memory_names,
|
|
173
181
|
recap: scratch ? nil : recap_config,
|
|
@@ -199,8 +207,9 @@ module Samagotchi
|
|
|
199
207
|
@commands = SessionCommands.new(engine: @engine, turn_flow: @turn_flow, default_model: @default_model_name,
|
|
200
208
|
registry: @engine.command_registry)
|
|
201
209
|
# Runtime --model flag or resumed session: switch the Engine (client,
|
|
202
|
-
# kernel profile) without persisting the default.
|
|
203
|
-
|
|
210
|
+
# kernel profile) without persisting the default. Skipped when there's
|
|
211
|
+
# no config default (the Engine was built on the session model).
|
|
212
|
+
@engine.switch_model!(@effective_model_name, typed: @resumed_model_typed) if !no_default && @effective_model_name != @default_model_name
|
|
204
213
|
# Render an idle session-recap via the cursor-safe background writer; the
|
|
205
214
|
# detector itself is Engine-owned (see Engine#recap) and opt-in.
|
|
206
215
|
@recap_handle = @engine.subscribe(observer: ->(event) { @between_turns.take_recap(event) })
|
|
@@ -1190,7 +1199,7 @@ module Samagotchi
|
|
|
1190
1199
|
@pending_input_queue.push(text)
|
|
1191
1200
|
# With no session running the cut is :off; the text still waits for
|
|
1192
1201
|
# the running turn's next step.
|
|
1193
|
-
@surface.commit(steer_cut_note(@engine.cut_for_steer(nil, epoch: epoch)))
|
|
1202
|
+
@surface.commit(steer_cut_note(@engine.cut_for_steer(nil, epoch: epoch), @engine.steer_cut_after))
|
|
1194
1203
|
true
|
|
1195
1204
|
end
|
|
1196
1205
|
|
|
@@ -46,7 +46,11 @@ module Samagotchi
|
|
|
46
46
|
status: tool_activity_status(result, tool_name, command: call.is_a?(Hash) ? call[:content] : nil)
|
|
47
47
|
}
|
|
48
48
|
# A grep that found nothing: ok, and the web row says "no match".
|
|
49
|
-
|
|
49
|
+
# Not a hand-over: an "exit: 1" in its output tail is the command's.
|
|
50
|
+
if event[:status] == "ok" && tool_name == Tools::Execute::NAME && !handed_over?(tool_name, result) &&
|
|
51
|
+
execute_failed?(result.to_s)
|
|
52
|
+
event[:no_match] = true
|
|
53
|
+
end
|
|
50
54
|
title = tool_title(tool_name, call, cwd: cwd)
|
|
51
55
|
event[:title] = title if title
|
|
52
56
|
# The name the model used when an alias ran a built-in (called_as: "bash"
|
|
@@ -214,6 +218,8 @@ module Samagotchi
|
|
|
214
218
|
return "stopped" if text == Tools::Execute::NOT_RUN_ON_STOP
|
|
215
219
|
return "stopped" if tool_name == Tools::Execute::NAME && text.start_with?(Tools::Execute::STOPPED_BY_USER)
|
|
216
220
|
return "error" if text.start_with?("Error:")
|
|
221
|
+
# Still running, as a task: its output tail may hold any "exit: N".
|
|
222
|
+
return "ok" if handed_over?(tool_name, text)
|
|
217
223
|
if tool_name == Tools::Execute::NAME && execute_failed?(text)
|
|
218
224
|
return no_match?(text, command) ? "ok" : "error"
|
|
219
225
|
end
|
|
@@ -222,6 +228,17 @@ module Samagotchi
|
|
|
222
228
|
"ok"
|
|
223
229
|
end
|
|
224
230
|
|
|
231
|
+
# An execute still running at its limit, handed over as a task
|
|
232
|
+
# (Tools::Execute::HANDED_OVER).
|
|
233
|
+
def handed_over?(tool_name, text)
|
|
234
|
+
return false unless tool_name == Tools::Execute::NAME && text.to_s.start_with?(Tools::Execute::HANDED_OVER)
|
|
235
|
+
|
|
236
|
+
# The first line only, scrubbed: a regexp raises on bytes that aren't UTF-8.
|
|
237
|
+
text.to_s.each_line.first.to_s.scrub.match?(HANDED_OVER_START)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
HANDED_OVER_START = /\A#{Regexp.escape(Tools::Execute::HANDED_OVER)} \d+s limit:/
|
|
241
|
+
|
|
225
242
|
def execute_failed?(text)
|
|
226
243
|
code = execute_exit_code(text)
|
|
227
244
|
!code.nil? && code != "0"
|
|
@@ -20,7 +20,7 @@ module Samagotchi
|
|
|
20
20
|
TOOL_SCHEMAS = [
|
|
21
21
|
{
|
|
22
22
|
name: "execute",
|
|
23
|
-
description: "Run any shell command in a working directory (defaults to the project root) and see stdout, stderr, and exit code. Large output may be truncated to a head+tail preview with metadata. Don't background with &; use task_create.",
|
|
23
|
+
description: "Run any shell command in a working directory (defaults to the project root) and see stdout, stderr, and exit code. Large output may be truncated to a head+tail preview with metadata. A command still running at the time limit keeps running as a background task (by default): the result gives its task_id, and task_wait waits for it. Don't background with &; use task_create.",
|
|
24
24
|
parameters: {
|
|
25
25
|
type: "object",
|
|
26
26
|
properties: {
|