samagotchi 0.141.0 → 0.142.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 +46 -1
- data/README.md +1 -1
- data/bin/chi +2 -0
- data/docs/architecture.md +12 -283
- data/docs/bundle-authoring.md +1 -0
- data/docs/cli.md +15 -7
- data/docs/configuration.md +57 -18
- data/docs/context.md +2 -2
- data/docs/guardrails.md +20 -11
- data/docs/hooks.md +9 -7
- data/docs/internals/attached-context.md +226 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +380 -0
- data/docs/internals/bundles.md +1 -0
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +24 -18
- data/docs/internals/participants.md +13 -12
- data/docs/internals/remote-sessions.md +1 -1
- data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
- data/docs/memory.md +31 -1
- data/docs/plugins.md +122 -1
- data/docs/sessions.md +50 -26
- data/docs/system-one.md +122 -0
- data/docs/testing.md +3 -1
- data/lib/samagotchi/bridge.rb +2 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +6 -3
- data/lib/samagotchi/bundles/check-in/plugin.rb +101 -4
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +74 -14
- data/lib/samagotchi/context_candidate_choice.rb +8 -0
- data/lib/samagotchi/context_candidates_log.rb +25 -1
- data/lib/samagotchi/context_judge.rb +6 -2
- data/lib/samagotchi/engine.rb +68 -7
- data/lib/samagotchi/extension_load.rb +3 -2
- data/lib/samagotchi/guardrail_wiring.rb +36 -31
- data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
- data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
- data/lib/samagotchi/guardrails_command.rb +3 -3
- data/lib/samagotchi/judgments_command.rb +155 -0
- data/lib/samagotchi/kernel_loop.rb +13 -1
- data/lib/samagotchi/llm/chat_loop.rb +74 -10
- data/lib/samagotchi/llm/errors.rb +7 -5
- data/lib/samagotchi/llm/stray_tool_call.rb +61 -0
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
- data/lib/samagotchi/plugin/api.rb +33 -2
- data/lib/samagotchi/plugin/context.rb +49 -1
- data/lib/samagotchi/plugin/judgments.rb +72 -0
- data/lib/samagotchi/plugin/loader.rb +3 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +36 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +3 -0
- data/lib/samagotchi/session_manager.rb +13 -2
- data/lib/samagotchi/session_retention.rb +10 -0
- data/lib/samagotchi/side_judgments.rb +12 -1
- data/lib/samagotchi/system_one/answer.rb +63 -11
- data/lib/samagotchi/system_one/approval_label.rb +56 -0
- data/lib/samagotchi/system_one/approval_question.rb +2 -0
- data/lib/samagotchi/system_one/client.rb +43 -17
- data/lib/samagotchi/system_one/fake_backend.rb +1 -1
- data/lib/samagotchi/system_one/http_backend.rb +12 -4
- data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
- data/lib/samagotchi/system_one/judgments.rb +260 -0
- data/lib/samagotchi/system_one/log.rb +4 -2
- data/lib/samagotchi/system_one/recording.rb +117 -0
- data/lib/samagotchi/system_one/runs.rb +179 -0
- data/lib/samagotchi/system_one/runs_store.rb +230 -0
- data/lib/samagotchi/system_one.rb +9 -0
- data/lib/samagotchi/testing/bundle.rb +9 -2
- data/lib/samagotchi/testing/host.rb +2 -1
- data/lib/samagotchi/testing/judgments.rb +185 -0
- data/lib/samagotchi/testing/recorder.rb +5 -0
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/tool_output_cut.rb +26 -6
- data/lib/samagotchi/tool_response.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +63 -4
- data/lib/samagotchi/toolkit_suggest.rb +11 -2
- data/lib/samagotchi/tools/memory.rb +28 -0
- data/lib/samagotchi/turn_note.rb +9 -0
- data/lib/samagotchi/version.rb +1 -1
- metadata +16 -2
|
@@ -11,6 +11,7 @@ require_relative "anthropic_wire"
|
|
|
11
11
|
require_relative "native_tool_normalizer"
|
|
12
12
|
require_relative "refusal_memo"
|
|
13
13
|
require_relative "stream_drops"
|
|
14
|
+
require_relative "stray_tool_call"
|
|
14
15
|
require_relative "../kernel_loop"
|
|
15
16
|
require_relative "../answer_display"
|
|
16
17
|
require_relative "../answer_refs"
|
|
@@ -374,7 +375,7 @@ module Samagotchi
|
|
|
374
375
|
def strip_model_thought(text) = @kernel.strip_model_thought(text)
|
|
375
376
|
|
|
376
377
|
def tool_runner
|
|
377
|
-
@tool_runner ||= ToolRunner.new(@kernel)
|
|
378
|
+
@tool_runner ||= ToolRunner.new(@kernel, window: -> { context_window(@kernel.turn_settings&.model_name) })
|
|
378
379
|
end
|
|
379
380
|
|
|
380
381
|
# The window for +model+: the kernel's client probes the server it
|
|
@@ -584,6 +585,10 @@ module Samagotchi
|
|
|
584
585
|
@reasoning_start = ReasoningHistory.run_start(conversation, start || conversation.length)
|
|
585
586
|
@tool_activity = []
|
|
586
587
|
@empty_retry = EmptyAnswerRetry.new
|
|
588
|
+
# A reply that was only a tool call the host could not read is asked
|
|
589
|
+
# for once, whatever retry.empty_answer says (a bug fix, not a
|
|
590
|
+
# preference), at the same retry temperature.
|
|
591
|
+
@stray_retry = EmptyAnswerRetry.new(limit: 1)
|
|
587
592
|
# The empty generations, never saved: the UIs draw the turn's
|
|
588
593
|
# steps from them (ModelResult#empty_steps).
|
|
589
594
|
@empty_steps = []
|
|
@@ -629,6 +634,10 @@ module Samagotchi
|
|
|
629
634
|
# tool calls that came with it are not run.
|
|
630
635
|
@refusal = refusal_of(response, last_text)
|
|
631
636
|
stopped = @refusal || response.finish_reason.to_s == FILTERED
|
|
637
|
+
# The budget is for two in a row: a real tool call in between means
|
|
638
|
+
# the model recovered, and a slip later in a long turn gets its own retry.
|
|
639
|
+
@stray_retry = EmptyAnswerRetry.new(limit: 1) unless response.tool_calls.empty?
|
|
640
|
+
|
|
632
641
|
if response.tool_calls.empty? || stopped
|
|
633
642
|
# A refusal is no answer, never asked again; text it streamed
|
|
634
643
|
# before is not kept as one. A filtered answer's text stays,
|
|
@@ -637,24 +646,56 @@ module Samagotchi
|
|
|
637
646
|
last_text = "#{last_text}\n#{FILTERED_MARK}" if !@refusal && response.finish_reason.to_s == FILTERED
|
|
638
647
|
# Only whitespace is no answer either (as in the native loop).
|
|
639
648
|
empty = last_text.strip.empty?
|
|
649
|
+
# A reply that is nothing but a tool call the host could not
|
|
650
|
+
# read (LLM::StrayToolCall) is no answer either. Checked here,
|
|
651
|
+
# before the append, so the bad text never enters the history:
|
|
652
|
+
# the retry's prompt is the same conversation plus one note, and
|
|
653
|
+
# nothing is guessed from the block. (:generation_completed and
|
|
654
|
+
# :after_generation have already seen the text, as they do for
|
|
655
|
+
# every step; the UIs lose the streamed text with the
|
|
656
|
+
# :generation_retrying below.)
|
|
657
|
+
stray = empty || stopped ? nil : LLM::StrayToolCall.shape(last_text)
|
|
658
|
+
retry_stray = stray && @stray_retry.left? && !@cancel_controller&.cancelled?
|
|
659
|
+
if stray
|
|
660
|
+
Log.warn(:model, "generation_malformed", iteration: iteration, shape: stray,
|
|
661
|
+
finish_reason: response.finish_reason,
|
|
662
|
+
served_model: response.model,
|
|
663
|
+
retried: retry_stray,
|
|
664
|
+
payload: LLM::StrayToolCall.tail(last_text))
|
|
665
|
+
unless retry_stray
|
|
666
|
+
# The second one in a row is no answer and runs no tool: the
|
|
667
|
+
# turn fails, and its earlier steps stay (FailedTurn's
|
|
668
|
+
# partial conversation, as for any provider error here).
|
|
669
|
+
@stray_retry.drop_nudge!(@conversation)
|
|
670
|
+
host = host_label
|
|
671
|
+
raise LLM::MalformedGeneration.new("#{host}: the reply came back as a #{stray} block the host " \
|
|
672
|
+
"could not read, twice in a row; nothing was run", host: host)
|
|
673
|
+
end
|
|
674
|
+
end
|
|
640
675
|
# Kept before a merge too: the model answers the merged line
|
|
641
676
|
# knowing what it just said.
|
|
642
|
-
@conversation << with_thinking({ role: "model", content: last_text }, response) unless empty
|
|
677
|
+
@conversation << with_thinking({ role: "model", content: last_text }, response) unless empty || stray
|
|
643
678
|
retry_empty = empty && !stopped &&
|
|
644
679
|
@empty_retry.retry_empty?(iteration: iteration, cancelled: @cancel_controller&.cancelled?,
|
|
645
680
|
finish_reason: response.finish_reason,
|
|
646
681
|
used_tokens: response.usage&.total_tokens, window_tokens: @window&.tokens)
|
|
647
|
-
# An empty answer that will be retried is no
|
|
648
|
-
# plugin's steer joins the retry instead of being
|
|
649
|
-
# queued input (a user's line, a steer) goes in
|
|
650
|
-
|
|
682
|
+
# An empty answer or a stray call that will be retried is no
|
|
683
|
+
# answer site: a plugin's steer joins the retry instead of being
|
|
684
|
+
# dropped, and queued input (a user's line, a steer) goes in
|
|
685
|
+
# place of the nudge.
|
|
686
|
+
next if inject_pending_input(iteration, answer: retry_empty || retry_stray ? nil : last_text)
|
|
651
687
|
|
|
652
688
|
# An empty answer (content "" + stop, seen from a remote host)
|
|
653
|
-
# ends the turn with no text: the UIs show their notice.
|
|
654
|
-
|
|
689
|
+
# ends the turn with no text: the UIs show their notice. A stray
|
|
690
|
+
# reply's text goes to the steps too, so a reload of the turn
|
|
691
|
+
# can show what the model wrote and did not get run.
|
|
692
|
+
@empty_steps << with_thinking({ role: "model", content: empty ? "" : last_text }, response) if empty || retry_stray
|
|
693
|
+
# Nothing of this reply is the turn's answer: a run that runs
|
|
694
|
+
# out of steps at the retry reports no answer, not the block.
|
|
695
|
+
last_text = "" if empty || retry_stray
|
|
655
696
|
next if retry_empty && nudge_empty_answer(iteration, response)
|
|
697
|
+
next if retry_stray && nudge_stray_call(iteration, response, stray)
|
|
656
698
|
|
|
657
|
-
last_text = "" if empty
|
|
658
699
|
exhausted = false
|
|
659
700
|
break
|
|
660
701
|
end
|
|
@@ -692,6 +733,27 @@ module Samagotchi
|
|
|
692
733
|
thinking_chars: response.reasoning.to_s.length)
|
|
693
734
|
end
|
|
694
735
|
|
|
736
|
+
# The hidden nudge before the retry of a reply that was only a tool
|
|
737
|
+
# call (LLM::StrayToolCall). The step's streamed text is void, so the
|
|
738
|
+
# stream events say so first (:generation_retrying with restarted,
|
|
739
|
+
# what TurnAccumulator and the web already honour), then the note goes
|
|
740
|
+
# on the tail (:empty_answer_retry with malformed, the "malformed
|
|
741
|
+
# answer" line the UIs know) and the next request runs at the retry
|
|
742
|
+
# sampling. Returns true.
|
|
743
|
+
def nudge_stray_call(iteration, response, shape)
|
|
744
|
+
emit(type: :generation_retrying, iteration: iteration, restarted: true, shape: shape)
|
|
745
|
+
@stray_retry.nudge!(@conversation, TurnNote.malformed_retry,
|
|
746
|
+
emit: method(:emit), iteration: iteration, malformed: true, shape: shape,
|
|
747
|
+
finish_reason: response.finish_reason, thinking_chars: response.reasoning.to_s.length)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# The host a failure is blamed on: the adapter's own name, the model
|
|
751
|
+
# when the adapter has none.
|
|
752
|
+
def host_label
|
|
753
|
+
name = @loop.adapter.host_name if @loop.adapter.respond_to?(:host_name)
|
|
754
|
+
name.to_s.strip.empty? ? @model_name.to_s : name.to_s
|
|
755
|
+
end
|
|
756
|
+
|
|
695
757
|
# A cut generation (CutPolicy): a Stop after it ends the turn as
|
|
696
758
|
# any cancel, the turn going on is :retry, the hook ending is the
|
|
697
759
|
# turn's result.
|
|
@@ -737,7 +799,9 @@ module Samagotchi
|
|
|
737
799
|
@loop.fire_hook(:before_generation, { type: :before_generation, iteration: iteration })
|
|
738
800
|
return [@cancel_controller.reason, "", ""] if @cancel_controller&.cancelled?
|
|
739
801
|
|
|
740
|
-
|
|
802
|
+
# The retry of an empty answer, or of a reply that was only a tool
|
|
803
|
+
# call, runs at the retry temperature.
|
|
804
|
+
retry_generation = @empty_retry.take_sampling! || @stray_retry.take_sampling!
|
|
741
805
|
emit(type: :generation_started, iteration: iteration, context_window_tokens: window&.tokens,
|
|
742
806
|
context_window_source: window&.source, price: @loop.price&.to_h)
|
|
743
807
|
streamed = +""
|
|
@@ -244,11 +244,13 @@ module Samagotchi
|
|
|
244
244
|
def summary = "unexpected response from host #{host}: #{detail}"
|
|
245
245
|
end
|
|
246
246
|
|
|
247
|
-
# A
|
|
248
|
-
# call never closed, or a fresh thought
|
|
249
|
-
# second time without the server's
|
|
250
|
-
# prompt cache
|
|
251
|
-
#
|
|
247
|
+
# A generation that came out corrupt twice, and is no answer. KernelLoop:
|
|
248
|
+
# a native generation with a tool call never closed, or a fresh thought
|
|
249
|
+
# header after the answer, the second time asked without the server's
|
|
250
|
+
# prompt cache (a poisoned llama.cpp prompt cache does this,
|
|
251
|
+
# ggml-org/llama.cpp#27148). ChatLoop: a reply that is nothing but a
|
|
252
|
+
# tool-call block the host could not read (LLM::StrayToolCall), asked for
|
|
253
|
+
# once more with a nudge.
|
|
252
254
|
class MalformedGeneration < ProtocolError
|
|
253
255
|
def summary = "malformed generation from host #{host}: #{detail}"
|
|
254
256
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module LLM
|
|
5
|
+
# A reply that is nothing but a tool call written as text. The host's
|
|
6
|
+
# parser could not read the block, so it came back as plain content with
|
|
7
|
+
# no tool calls: a Qwen flavour that writes `<parameter=execute>` where
|
|
8
|
+
# `<function=execute>` belongs, seen twice in 14 days of chat-loop
|
|
9
|
+
# sessions (docs/internals/engine.md, "A reply that is only a tool call").
|
|
10
|
+
# It is not an answer, and nothing is guessed from it: ChatLoop asks for
|
|
11
|
+
# the reply once more, and a second one fails the turn.
|
|
12
|
+
#
|
|
13
|
+
# The rule is the narrow one the data supports, and it needs no model
|
|
14
|
+
# profile (ChatLoop has no ToolCallParser): the whole reply is one closed
|
|
15
|
+
# block that opens with the call. A reply opening with prose, a fence or
|
|
16
|
+
# a quote is an answer, and so is one that mentions, quotes or fences the
|
|
17
|
+
# syntax. KernelLoop has its own path for a corrupt generation
|
|
18
|
+
# (ToolCallParser#malformed, per profile); this predicate is ChatLoop's.
|
|
19
|
+
module StrayToolCall
|
|
20
|
+
# A stray call can only open with the block itself.
|
|
21
|
+
OPENS = ["<tool_call>", "<function=", "<parameter="].freeze
|
|
22
|
+
# ... and end with the close of that block: nothing else in the reply.
|
|
23
|
+
CLOSES = ["</function>", "</parameter>", "</tool_call>"].freeze
|
|
24
|
+
# A block carries its own tags; an answer that merely starts with a
|
|
25
|
+
# "<" is not one.
|
|
26
|
+
INNER_TAG = /<[a-z_]+=/i
|
|
27
|
+
# How much of the reply the warning keeps (KernelLoop's own cap).
|
|
28
|
+
TAIL_CHARS = 400
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @return [String, nil] the shape of the reply ("tool_call", "function"
|
|
33
|
+
# or "parameter") when all of it is one closed tool-call block, nil
|
|
34
|
+
# when it reads as an answer.
|
|
35
|
+
# +text+ is the reply with its thought already stripped (ChatLoop's
|
|
36
|
+
# strip_model_thought).
|
|
37
|
+
def shape(text)
|
|
38
|
+
body = text.to_s.strip
|
|
39
|
+
# The cheap gate: a whole block has to open with the block. This is
|
|
40
|
+
# the whole cost of the check on a normal answer.
|
|
41
|
+
return nil unless body.start_with?("<")
|
|
42
|
+
|
|
43
|
+
opener = OPENS.find { |open| body.start_with?(open) }
|
|
44
|
+
return nil if opener.nil?
|
|
45
|
+
return nil unless CLOSES.any? { |close| body.end_with?(close) }
|
|
46
|
+
|
|
47
|
+
# The tags inside it: a lone opening tag in a sentence is not a call
|
|
48
|
+
# the host tried to read.
|
|
49
|
+
return nil unless body[1..-2].match?(INNER_TAG)
|
|
50
|
+
|
|
51
|
+
opener[/[a-z_]+/]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The tail of a stray reply the log keeps, to see what the host sent.
|
|
55
|
+
def tail(text)
|
|
56
|
+
text = text.to_s
|
|
57
|
+
text.length > TAIL_CHARS ? text[-TAIL_CHARS..] : text
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/samagotchi/log_line.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Samagotchi
|
|
|
16
16
|
# (debug dumps) are indented by four spaces and belong to the record above.
|
|
17
17
|
module LogLine
|
|
18
18
|
LEVELS = %w[DEBUG INFO WARN ERROR].freeze
|
|
19
|
-
TAGS = %w[turn http worker bridge web attached repl idle recap hooks plugins guardrails config memory model context broadcast toolkit].freeze
|
|
19
|
+
TAGS = %w[turn http worker bridge web attached repl idle recap hooks plugins guardrails config memory model context broadcast toolkit system_one].freeze
|
|
20
20
|
INDENT = " "
|
|
21
21
|
|
|
22
22
|
Record = Struct.new(:ts, :level, :tag, :pid, :sid, :event, :fields, :payload, keyword_init: true)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../config"
|
|
4
|
+
require_relative "index_size"
|
|
5
|
+
require_relative "index_sync"
|
|
6
|
+
require_relative "index_updater"
|
|
7
|
+
require_relative "../model_overlay"
|
|
8
|
+
require_relative "../tools/memory"
|
|
9
|
+
|
|
10
|
+
module Samagotchi
|
|
11
|
+
module MemoryBundle
|
|
12
|
+
# The size of one memory's text, which each memory_read of it pays
|
|
13
|
+
# (memory_read is not cut by max_tool_output_chars). memory_write and
|
|
14
|
+
# write/edit on a memory compare the size before and after a write
|
|
15
|
+
# against memory.entry_warn_tokens (#crossing_note); `chi self` lists
|
|
16
|
+
# the memories over it (SelfReport). tokens is the estimate IndexSize
|
|
17
|
+
# uses (characters / context.chars_per_token, rounded up).
|
|
18
|
+
#
|
|
19
|
+
# A memory that didn't exist before a write counts as 0 tokens. Not
|
|
20
|
+
# counted: the index, a model overlay, a model_notes_* memory (it has its
|
|
21
|
+
# own warning).
|
|
22
|
+
EntrySize = Data.define(:name, :scope, :tokens) do
|
|
23
|
+
# @param text [String, nil] the memory's text
|
|
24
|
+
# @return [EntrySize]
|
|
25
|
+
def self.of_text(name, scope, text, chars_per_token: IndexSize.chars_per_token)
|
|
26
|
+
new(name: name.to_s, scope: scope.to_s, tokens: (text.to_s.length / chars_per_token).ceil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Whether a memory of this name is sized at all (not the index, not a
|
|
30
|
+
# model note).
|
|
31
|
+
def self.counted?(name)
|
|
32
|
+
name = name.to_s
|
|
33
|
+
!name.casecmp?(IndexUpdater::MEMORY_INDEX) && !name.start_with?(Tools::MODEL_NOTES_PREFIX)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The memory file at +path+ as it is now (0 tokens when it isn't
|
|
37
|
+
# there), nil when it isn't a counted memory of a scope or
|
|
38
|
+
# memory.entry_warn_tokens is off, or when it can't be measured (a
|
|
39
|
+
# write's measure must never fail the write).
|
|
40
|
+
# @return [EntrySize, nil]
|
|
41
|
+
def self.measure_file(path)
|
|
42
|
+
return nil unless warn_limit.positive?
|
|
43
|
+
|
|
44
|
+
path = File.expand_path(path.to_s)
|
|
45
|
+
scope = IndexSync.memory_scope(path)
|
|
46
|
+
name = File.basename(path, ".md")
|
|
47
|
+
return nil unless scope && counted?(name) && !ModelOverlay.overlay_file?(path)
|
|
48
|
+
|
|
49
|
+
of_text(name, scope, File.file?(path) ? File.read(path, encoding: "UTF-8") : "")
|
|
50
|
+
rescue StandardError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The memories of +scope+ over +limit+ tokens, largest first: each
|
|
55
|
+
# top-level *.md of its dir but the index, the overlays and the
|
|
56
|
+
# model notes (hidden files, .bundles/ and archived/ are not in a
|
|
57
|
+
# top-level glob). Nothing for a limit that isn't positive.
|
|
58
|
+
# @param env [Hash] the XDG env the memories dir resolves from
|
|
59
|
+
# @return [Array<EntrySize>]
|
|
60
|
+
def self.over_limit(scope, limit: warn_limit, env: ENV)
|
|
61
|
+
return [] unless limit.to_i.positive?
|
|
62
|
+
|
|
63
|
+
Dir.glob(File.join(Tools::MemoryRead.memories_dir(scope, env: env), "*.md")).filter_map do |path|
|
|
64
|
+
name = File.basename(path, ".md")
|
|
65
|
+
next unless counted?(name) && File.file?(path) && !ModelOverlay.overlay_file?(path)
|
|
66
|
+
|
|
67
|
+
size = of_text(name, scope, File.read(path, encoding: "UTF-8"))
|
|
68
|
+
size if size.over?(limit)
|
|
69
|
+
end.sort_by { |size| [-size.tokens, size.name] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# memory.entry_warn_tokens: the tokens one memory may hold before a
|
|
73
|
+
# write that crosses it gets a note; 0 turns the note off.
|
|
74
|
+
def self.warn_limit
|
|
75
|
+
Config.get("memory.entry_warn_tokens").to_i
|
|
76
|
+
rescue StandardError
|
|
77
|
+
0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The note a write gets when it took the memory from at or under
|
|
81
|
+
# +limit+ to over it, nil otherwise: once per crossing, as the
|
|
82
|
+
# index's. Independent of memory.index_warn_tokens.
|
|
83
|
+
# @param before [EntrySize, nil]
|
|
84
|
+
# @param after [EntrySize, nil]
|
|
85
|
+
# @return [String, nil]
|
|
86
|
+
def self.crossing_note(before, after, limit = warn_limit)
|
|
87
|
+
return nil unless before && after && after.over?(limit) && !before.over?(limit)
|
|
88
|
+
|
|
89
|
+
"Note: memory '#{after.name}' is now ~#{after.tokens} tokens (over memory.entry_warn_tokens #{limit}), paid " \
|
|
90
|
+
"on each read. When you next have a moment, tighten it; split out only parts that are read separately, " \
|
|
91
|
+
"since each new memory adds an index line to every prompt. Don't remove or merge memories unless the " \
|
|
92
|
+
"user asks."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether the memory holds more than +limit+ tokens; a limit that
|
|
96
|
+
# isn't positive is off.
|
|
97
|
+
def over?(limit) = limit.to_i.positive? && tokens > limit.to_i
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -4,6 +4,7 @@ require_relative "../toolkit"
|
|
|
4
4
|
require_relative "../log"
|
|
5
5
|
require_relative "../hooks/registry"
|
|
6
6
|
require_relative "../commands/registry"
|
|
7
|
+
require_relative "../system_one/judgments"
|
|
7
8
|
require_relative "service"
|
|
8
9
|
require_relative "tool_result"
|
|
9
10
|
require_relative "tool_spec"
|
|
@@ -48,7 +49,8 @@ module Samagotchi
|
|
|
48
49
|
"ctx.sessions.review" => "0.107.0", "ctx.sessions.send(delivery:)" => "0.64.0",
|
|
49
50
|
"chi.toolkit(prompt:)" => "0.72.0", "ctx.ask_model(model:)" => "0.80.0", "chi.toolkit(groups:)" => "0.121.0",
|
|
50
51
|
"chi.prompt_section" => "0.132.0", "chi.prompt_changed" => "0.132.0", "ctx.cache_confirm" => "0.132.0",
|
|
51
|
-
"ctx.model_names" => "0.132.0"
|
|
52
|
+
"ctx.model_names" => "0.132.0", "chi.judgment" => "0.142.0", "ctx.judge" => "0.142.0",
|
|
53
|
+
"ctx.judge_later" => "0.142.0", "ctx.judgments" => "0.142.0"
|
|
52
54
|
# Unreleased: "ctx.context.propose", "ctx.context.candidates", "ctx.context.withdraw", "ctx.review".
|
|
53
55
|
}.freeze
|
|
54
56
|
|
|
@@ -71,6 +73,7 @@ module Samagotchi
|
|
|
71
73
|
@toolkits = []
|
|
72
74
|
@prompt_sections = []
|
|
73
75
|
@peer_sources = []
|
|
76
|
+
@judgments = []
|
|
74
77
|
@tool_source = nil
|
|
75
78
|
@committed = false
|
|
76
79
|
end
|
|
@@ -399,6 +402,33 @@ module Samagotchi
|
|
|
399
402
|
nil
|
|
400
403
|
end
|
|
401
404
|
|
|
405
|
+
# A cheap question this bundle asks System 1 (docs/plugins.md,
|
|
406
|
+
# Judgments): ctx.judge and ctx.judge_later ask it, and each ask saves a
|
|
407
|
+
# run. Off until the user sets its mode in config.yml's
|
|
408
|
+
# system_one.judgments, under its full name "<bundle>.<name>".
|
|
409
|
+
# @param name [String] dot-separated words of a-z, 0-9, _ and -
|
|
410
|
+
# @param questions [Hash, #call] id => {type:, instructions:, criteria:}
|
|
411
|
+
# ("noul", "choice" or "score"), checked now; or a callable given the
|
|
412
|
+
# ctx, returning that at each ask, where they vary per session
|
|
413
|
+
# @param positive [String, Array<String>, nil] the label values that
|
|
414
|
+
# say the answer was right, for a later comparison
|
|
415
|
+
# @param negative [String, Array<String>, nil] and wrong
|
|
416
|
+
# @param shared_host [Symbol] what to do while a turn runs on the same
|
|
417
|
+
# local server, where an ask evicts its cached prompt: :skip saves
|
|
418
|
+
# the run unasked (asked: false), :ask asks anyway
|
|
419
|
+
def judgment(name, questions:, positive: nil, negative: nil, shared_host: :skip)
|
|
420
|
+
raise ArgumentError, "this chi has no judgments (plugins: false)" unless @registries.judgments
|
|
421
|
+
|
|
422
|
+
declaration = SystemOne::Judgments::Declaration.build(bundle: @bundle, name: name, questions: questions, positive: positive,
|
|
423
|
+
negative: negative, shared_host: shared_host)
|
|
424
|
+
if @judgments.any? { |item| item.name == declaration.name } || @registries.judgments.declared?(declaration.name)
|
|
425
|
+
raise ArgumentError, "judgment #{declaration.name} is declared twice"
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
@judgments << declaration
|
|
429
|
+
nil
|
|
430
|
+
end
|
|
431
|
+
|
|
402
432
|
# Register what was staged. Called by Loader after #register. What
|
|
403
433
|
# can only be checked now (an annotation's command) is checked before
|
|
404
434
|
# anything is registered: a raise here leaves the registries as they were.
|
|
@@ -439,6 +469,7 @@ module Samagotchi
|
|
|
439
469
|
end
|
|
440
470
|
@registries.tool_index.add(@tool_source) if @tool_source
|
|
441
471
|
@peer_sources.each { |item| @registries.peer_sources.declare(item[:name], label: item[:label], bundle: @bundle) }
|
|
472
|
+
@judgments.each { |declaration| @registries.judgments.declare(declaration) }
|
|
442
473
|
@committed = true
|
|
443
474
|
end
|
|
444
475
|
|
|
@@ -446,7 +477,7 @@ module Samagotchi
|
|
|
446
477
|
def counts = { commands: @commands.size, annotations: @annotations.size, tools: @tools.size, hooks: @hooks.size,
|
|
447
478
|
services: @services.size, inits: @inits.size, toolkits: @toolkits.size,
|
|
448
479
|
prompt_sections: @prompt_sections.size,
|
|
449
|
-
tool_sources: @tool_source ? 1 : 0, peer_sources: @peer_sources.size }
|
|
480
|
+
tool_sources: @tool_source ? 1 : 0, peer_sources: @peer_sources.size, judgments: @judgments.size }
|
|
450
481
|
|
|
451
482
|
# Each annotation's command is one #command staged.
|
|
452
483
|
def check_annotations!
|
|
@@ -28,6 +28,7 @@ module Samagotchi
|
|
|
28
28
|
autoload :Http, File.expand_path("http", __dir__)
|
|
29
29
|
autoload :HttpError, File.expand_path("http", __dir__)
|
|
30
30
|
autoload :HttpCancelled, File.expand_path("http", __dir__)
|
|
31
|
+
autoload :Judgments, File.expand_path("judgments", __dir__)
|
|
31
32
|
|
|
32
33
|
# The Engine's side of a Context: callables, so the Engine itself is
|
|
33
34
|
# never handed out. +messages+ returns the conversation, +notify+ takes
|
|
@@ -65,11 +66,13 @@ module Samagotchi
|
|
|
65
66
|
# session's model is looked up under (EffectiveModel#names).
|
|
66
67
|
# +candidate_proposed+ takes (candidate): a late proposal no owner
|
|
67
68
|
# prompt mentions, for the candidate judge (Engine#judge_late_candidates).
|
|
69
|
+
# +judgments+ returns the SystemOne::Judgments ctx.judge, ctx.judge_later
|
|
70
|
+
# and ctx.judgments ask and label through.
|
|
68
71
|
Host = Struct.new(:session_id, :cwd, :messages, :messages_partial, :notify, :ask_user, :cancelled, :card,
|
|
69
72
|
:ask_model, :model_name, :model_key, :state_dir, :scratch, :setup, :prompt_files,
|
|
70
73
|
:steer, :stop_turn, :stop_generation, :frontend, :toolkit, :declared_tools, :use_toolkit, :load_tools,
|
|
71
74
|
:cancel_controller, :cost, :usage, :turn_author, :cache_confirm, :model_names,
|
|
72
|
-
:candidate_proposed, keyword_init: true)
|
|
75
|
+
:candidate_proposed, :judgments, keyword_init: true)
|
|
73
76
|
|
|
74
77
|
# ctx.ask_model failed: the model couldn't be reached, timed out, or
|
|
75
78
|
# sent nothing usable. The message says why, for the user.
|
|
@@ -407,6 +410,45 @@ module Samagotchi
|
|
|
407
410
|
# ctx.ask_model's answer limit when none is given.
|
|
408
411
|
ASK_MAX_TOKENS = 1024
|
|
409
412
|
|
|
413
|
+
# Ask System 1 this bundle's judgment (chi.judgment, docs/plugins.md,
|
|
414
|
+
# Judgments) and wait, at most system_one.timeout_ms. A Stop of the
|
|
415
|
+
# turn cuts an ask made for it. Off, no host, a failed backend or a
|
|
416
|
+
# skip is a Result with no answers, never a raise.
|
|
417
|
+
# @param name [String] as declared, without the bundle
|
|
418
|
+
# @param state [String, Hash, Array] what the question is about; it
|
|
419
|
+
# goes to the System 1 host and is saved in the run as sent
|
|
420
|
+
# @return [SystemOne::Judgments::Result] #answers, #p(id), #run_id,
|
|
421
|
+
# #audit?, #no_judgment?, #reason
|
|
422
|
+
# @raise [ArgumentError] a judgment this bundle didn't declare, a bad
|
|
423
|
+
# state, or called in a :generation_progress handler, whose stream it
|
|
424
|
+
# would hold (use #judge_later)
|
|
425
|
+
def judge(name, state:)
|
|
426
|
+
event = Thread.current[CURRENT_EVENTS]&.[](self)
|
|
427
|
+
if event.is_a?(Hash) && event[:type] == :generation_progress
|
|
428
|
+
raise ArgumentError, "ctx.judge would hold the stream in a :generation_progress handler; use ctx.judge_later"
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
judgment_layer.judge(judgment_name(name), state: state, context: self, cancel_controller: http_cancel_controller)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# #judge off this thread: the run's id comes back at once and the block
|
|
435
|
+
# gets the Result later, on another thread, always once. So its ctx.*
|
|
436
|
+
# act on the session as it is then, not this handler's event. While
|
|
437
|
+
# this judgment's last judge_later still runs, nothing is asked: a
|
|
438
|
+
# skipped run, and the block gets a Result with no answers.
|
|
439
|
+
# @return [String, nil] the run's id; nil when no run will be saved (off,
|
|
440
|
+
# no host, runs off, a scratch session, a participant's turn)
|
|
441
|
+
# @raise [ArgumentError] a judgment this bundle didn't declare, a bad state
|
|
442
|
+
def judge_later(name, state:, &)
|
|
443
|
+
judgment_layer.later(judgment_name(name), state: state, context: self, &)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# What came of this bundle's judgments' runs: #label, #acted.
|
|
447
|
+
# @return [Judgments]
|
|
448
|
+
def judgments
|
|
449
|
+
@judgments ||= Judgments.new(-> { judgment_layer }, bundle: @bundle)
|
|
450
|
+
end
|
|
451
|
+
|
|
410
452
|
# Other sessions: fork one from a conversation, send one a message,
|
|
411
453
|
# read one (Sessions).
|
|
412
454
|
# @return [Sessions]
|
|
@@ -493,6 +535,12 @@ module Samagotchi
|
|
|
493
535
|
|
|
494
536
|
private
|
|
495
537
|
|
|
538
|
+
def judgment_layer
|
|
539
|
+
@host.judgments&.call or raise ArgumentError, "this chi has no judgments (plugins: false)"
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def judgment_name(name) = "#{@bundle}.#{name}"
|
|
543
|
+
|
|
496
544
|
# Whether this plugin's handler runs on this thread (#with_event).
|
|
497
545
|
def in_handler? = !!Thread.current[CURRENT_EVENTS]&.key?(self)
|
|
498
546
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../system_one/runs"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
module Plugin
|
|
7
|
+
# ctx.judgments: what came of a bundle's own runs (docs/plugins.md,
|
|
8
|
+
# Judgments), saved as later lines on them. A bundle's lines say it is
|
|
9
|
+
# theirs (+by+ is its name), and it reaches no one else's runs.
|
|
10
|
+
class Judgments
|
|
11
|
+
MAX_TEXT = 200
|
|
12
|
+
STRENGTHS = %w[strong weak].freeze
|
|
13
|
+
|
|
14
|
+
# @param layer [#call] → the SystemOne::Judgments to save through
|
|
15
|
+
# @param bundle [String]
|
|
16
|
+
def initialize(layer, bundle:)
|
|
17
|
+
@layer = layer
|
|
18
|
+
@bundle = bundle
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The bundle's own word for its run: a raw value now, mapped at analysis.
|
|
22
|
+
# @param run_id [String, nil] ctx.judge's Result#run_id, or what
|
|
23
|
+
# ctx.judge_later returned; nil (no run was saved) saves nothing
|
|
24
|
+
# @param value [String] up to 200 characters
|
|
25
|
+
# @param by [String, nil] what said it, after the bundle's name ("card":
|
|
26
|
+
# "check-in:card")
|
|
27
|
+
# @param strength [String, nil] "strong" or "weak"
|
|
28
|
+
# @param question [String, nil] the question it answers
|
|
29
|
+
# @return [Boolean] whether it was saved
|
|
30
|
+
# @raise [ArgumentError] a run of another bundle's, a bad value
|
|
31
|
+
def label(run_id, value, by: nil, strength: nil, question: nil)
|
|
32
|
+
value = text("label", value)
|
|
33
|
+
raise ArgumentError, "strength: is #{STRENGTHS.join(" or ")}, not #{strength.inspect}" unless strength.nil? || STRENGTHS.include?(strength.to_s)
|
|
34
|
+
return false unless own?(run_id)
|
|
35
|
+
|
|
36
|
+
@layer.call.label(run_id, value, by: by.to_s.strip.empty? ? @bundle : "#{@bundle}:#{text("by", by)}",
|
|
37
|
+
strength: strength&.to_s, question: question && text("question", question.to_s))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# What the bundle did with the answer: never known when the run is saved.
|
|
41
|
+
# @param what [String] up to 200 characters
|
|
42
|
+
# @return [Boolean] whether it was saved
|
|
43
|
+
# @raise [ArgumentError] a run of another bundle's, a bad value
|
|
44
|
+
def acted(run_id, what)
|
|
45
|
+
what = text("acted", what)
|
|
46
|
+
return false unless own?(run_id)
|
|
47
|
+
|
|
48
|
+
@layer.call.acted(run_id, what)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# False for nil; a run of anything but a judgment this bundle declared
|
|
54
|
+
# raises (a name it can't declare has no run of its).
|
|
55
|
+
def own?(run_id)
|
|
56
|
+
return false if run_id.nil?
|
|
57
|
+
|
|
58
|
+
judgment = SystemOne::Runs.judgment_of(run_id)
|
|
59
|
+
return true if judgment&.start_with?("#{@bundle}.") && @layer.call.declared?(judgment)
|
|
60
|
+
|
|
61
|
+
raise ArgumentError, "#{run_id.inspect} is not a run of bundle #{@bundle}'s"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def text(what, value)
|
|
65
|
+
word = value.to_s.strip
|
|
66
|
+
return word if value.is_a?(String) && word.length.between?(1, MAX_TEXT)
|
|
67
|
+
|
|
68
|
+
raise ArgumentError, "#{what} takes a String of 1 to #{MAX_TEXT} characters, not #{value.inspect[0, 40]}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -22,9 +22,10 @@ module Samagotchi
|
|
|
22
22
|
# +tool_index+ the Engine's Tools::ToolIndex (chi.tool_source);
|
|
23
23
|
# +peer_sources+ the Engine's PeerSources (chi.peer_source);
|
|
24
24
|
# +prompt_sections+ the Engine's PromptSections (chi.prompt_section),
|
|
25
|
-
# +prompt_changed+ rebuilds its system prompts (chi.prompt_changed!)
|
|
25
|
+
# +prompt_changed+ rebuilds its system prompts (chi.prompt_changed!),
|
|
26
|
+
# +judgments+ the Engine's SystemOne::Judgments (chi.judgment).
|
|
26
27
|
Registries = Struct.new(:commands, :tools, :hooks, :context_for, :tools_changed, :services, :stage_tools, :init,
|
|
27
|
-
:toolkits, :tool_index, :peer_sources, :prompt_sections, :prompt_changed,
|
|
28
|
+
:toolkits, :tool_index, :peer_sources, :prompt_sections, :prompt_changed, :judgments,
|
|
28
29
|
keyword_init: true)
|
|
29
30
|
|
|
30
31
|
# Loads installed bundles' plugins (manifest plugin: {file:, sha256:})
|
|
@@ -207,6 +207,7 @@ module Samagotchi
|
|
|
207
207
|
chi memory <ls|rm> [--scope project|system] the memories; rm moves one to the bundle trash
|
|
208
208
|
chi bundle <install|upgrade|uninstall|status|diff|list|build|trash|new|test|check|try>
|
|
209
209
|
chi guardrails replay [--dry-run] [--threshold T] [--model M] seed System 1's record from answered approvals
|
|
210
|
+
chi judgments ls | report NAME System 1's saved asks on this machine
|
|
210
211
|
chi toolkit report [--days N] grade toolkit suggestions against the tools sessions used
|
|
211
212
|
chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
|
|
212
213
|
chi models [--format json] [TEXT] the models --model takes, from every host
|
|
@@ -21,6 +21,7 @@ require_relative "memory_bundle/provenance"
|
|
|
21
21
|
require_relative "memory_bundle/manifest"
|
|
22
22
|
require_relative "memory_bundle/system_bundle"
|
|
23
23
|
require_relative "memory_bundle/index_size"
|
|
24
|
+
require_relative "memory_bundle/entry_size"
|
|
24
25
|
require_relative "desktop"
|
|
25
26
|
require_relative "live_versions"
|
|
26
27
|
require_relative "web/lan"
|
|
@@ -60,6 +61,7 @@ module Samagotchi
|
|
|
60
61
|
["memories", Tools::MemoryRead.memories_dir("system", env: env)],
|
|
61
62
|
["project memories", Tools::MemoryRead.memories_dir("project", env: env)],
|
|
62
63
|
["memory index", memory_index_summary(env)],
|
|
64
|
+
["big memories", big_memories_summary(env)],
|
|
63
65
|
["sessions", Session.default_state_dir(env: env)],
|
|
64
66
|
["log", log_path(env)],
|
|
65
67
|
["model", model ? model_label(model, default, env) : "(not configured)"],
|
|
@@ -122,6 +124,40 @@ module Samagotchi
|
|
|
122
124
|
"(unreadable: #{e.message})"
|
|
123
125
|
end
|
|
124
126
|
|
|
127
|
+
# How many big memories "big memories" names before "+N more".
|
|
128
|
+
BIG_MEMORIES_SHOWN = 5
|
|
129
|
+
|
|
130
|
+
# The memories over memory.entry_warn_tokens in both scopes, largest
|
|
131
|
+
# first, "name ~5.6k tokens (system, from samagotchi-system)": each
|
|
132
|
+
# memory_read of one pays its size, and a memory already over gets no
|
|
133
|
+
# crossing note. A bundle's memory says whose it is (upstream's to
|
|
134
|
+
# tighten). "none" when there are none, "off" with the limit 0. A scope
|
|
135
|
+
# that can't be read is named instead.
|
|
136
|
+
def big_memories_summary(env)
|
|
137
|
+
limit = MemoryBundle::EntrySize.warn_limit
|
|
138
|
+
return "off (memory.entry_warn_tokens 0)" unless limit.positive?
|
|
139
|
+
|
|
140
|
+
big = []
|
|
141
|
+
unreadable = []
|
|
142
|
+
%w[system project].each do |scope|
|
|
143
|
+
big.concat(MemoryBundle::EntrySize.over_limit(scope, limit: limit, env: env))
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
unreadable << "#{scope} memories unreadable: #{e.message}"
|
|
146
|
+
end
|
|
147
|
+
shown = big.sort_by { |size| [-size.tokens, size.scope, size.name] }.first(BIG_MEMORIES_SHOWN).map { |size| big_memory(size, env) }
|
|
148
|
+
shown << "+#{big.size - BIG_MEMORIES_SHOWN} more" if big.size > BIG_MEMORIES_SHOWN
|
|
149
|
+
parts = shown + unreadable.map { |text| "(#{text})" }
|
|
150
|
+
parts.empty? ? "none" : parts.join(", ")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def big_memory(size, env)
|
|
154
|
+
owner = MemoryBundle::Provenance.claimants("#{size.name}.md", scope: size.scope, dir: MemoryPaths.bundles_dir(env: env)).first
|
|
155
|
+
where = [size.scope, ("from #{owner}" if owner)].compact.join(", ")
|
|
156
|
+
"#{size.name} ~#{MemoryBundle::IndexSize.count_text(size.tokens)} tokens (#{where})"
|
|
157
|
+
rescue StandardError
|
|
158
|
+
"#{size.name} ~#{MemoryBundle::IndexSize.count_text(size.tokens)} tokens (#{size.scope})"
|
|
159
|
+
end
|
|
160
|
+
|
|
125
161
|
def with_presence(path)
|
|
126
162
|
File.exist?(path) ? path : "#{path} (missing)"
|
|
127
163
|
end
|
|
@@ -26,7 +26,7 @@ module Samagotchi
|
|
|
26
26
|
# bin/chi subcommands that act on this machine: never sessions. chi
|
|
27
27
|
# remote (this machine's list of remotes) runs here under a remote too
|
|
28
28
|
# (Dispatch); the others are refused there.
|
|
29
|
-
MACHINE = %w[bootstrap update models desktop memory bundle guardrails toolkit remote home].freeze
|
|
29
|
+
MACHINE = %w[bootstrap update models desktop memory bundle guardrails toolkit judgments remote home].freeze
|
|
30
30
|
|
|
31
31
|
# "command", "command sub" or either with a flag (its long name) →
|
|
32
32
|
# SUPPORTED or LOCAL_ONLY. A LOCAL_ONLY command or subcommand lists
|