samagotchi 0.140.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 +64 -1
- data/README.md +1 -1
- data/bin/chi +2 -0
- data/docs/architecture.md +14 -285
- data/docs/bundle-authoring.md +1 -0
- data/docs/cli.md +15 -7
- data/docs/configuration.md +57 -18
- data/docs/context.md +68 -6
- 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 +5 -2
- data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
- data/docs/memory.md +31 -1
- data/docs/plugins.md +162 -2
- data/docs/sessions.md +53 -28
- 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/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +14 -0
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +15 -1
- 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_command.rb +32 -9
- data/lib/samagotchi/context_judge.rb +6 -2
- data/lib/samagotchi/context_move.rb +31 -0
- data/lib/samagotchi/context_sources.rb +248 -10
- 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/attached_context.rb +14 -2
- data/lib/samagotchi/plugin/context.rb +54 -1
- data/lib/samagotchi/plugin/judgments.rb +72 -0
- data/lib/samagotchi/plugin/loader.rb +3 -2
- data/lib/samagotchi/plugin/peer.rb +53 -0
- data/lib/samagotchi/plugin/sessions.rb +9 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +36 -0
- data/lib/samagotchi/send_command.rb +3 -1
- data/lib/samagotchi/session.rb +4 -0
- data/lib/samagotchi/session_access/contract.rb +3 -1
- data/lib/samagotchi/session_access/local.rb +24 -3
- data/lib/samagotchi/session_access/remote.rb +19 -8
- data/lib/samagotchi/session_access.rb +26 -6
- data/lib/samagotchi/session_commands.rb +3 -0
- data/lib/samagotchi/session_continue.rb +9 -2
- 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/boundary.rb +5 -1
- 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
- data/lib/samagotchi/web/app.rb +14 -8
- data/lib/samagotchi/web/public/chains.js +10 -2
- data/lib/samagotchi/web/session_access_routes.rb +7 -2
- data/lib/samagotchi/web/session_create.rb +1 -0
- metadata +17 -2
data/lib/samagotchi/config.rb
CHANGED
|
@@ -179,17 +179,21 @@ module Samagotchi
|
|
|
179
179
|
# mode GuardrailWiring also reads rules, threshold and min_record for the auto-allow (docs/guardrails.md).
|
|
180
180
|
# off (the default) | audit (judge every ask, log it, never decide) | on. An unknown value warns and is off.
|
|
181
181
|
Entry.new(key: "guardrails.system_one.mode", yaml_path: %w[guardrails system_one mode], type: :enum, default: "off", expose: %i[config], enum_values: %w[off audit on], lifecycle: :next_tool_or_approval),
|
|
182
|
-
# The hosts: entry serving /v1/systemone (Splash, or the hosted Jev API); unset: no judgment.
|
|
183
|
-
Entry.new(key: "guardrails.system_one.host", yaml_path: %w[guardrails system_one host], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
184
|
-
# The judgment model; unset: the host's first model, and no judgment if it names none.
|
|
185
|
-
Entry.new(key: "guardrails.system_one.model", yaml_path: %w[guardrails system_one model], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
186
182
|
# The rule ids System 1 may answer for ("a|b", or a YAML list, parsed like guardrails.small_models).
|
|
187
183
|
Entry.new(key: "guardrails.system_one.rules", yaml_path: %w[guardrails system_one rules], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
188
184
|
# The probability an auto-allow needs, and how many clean records a rule needs before it is trusted.
|
|
189
185
|
Entry.new(key: "guardrails.system_one.threshold", yaml_path: %w[guardrails system_one threshold], type: :float, default: 0.8, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
190
186
|
Entry.new(key: "guardrails.system_one.min_record", yaml_path: %w[guardrails system_one min_record], type: :integer, default: 5, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
|
|
188
|
+
# What every use of System 1 shares: approvals, toolkit suggestions, context candidates and bundles' judgments.
|
|
189
|
+
# The hosts: entry serving /v1/systemone (Splash, or the hosted Jev API); unset: no judgment, so nothing auto-allows.
|
|
190
|
+
Entry.new(key: "system_one.host", yaml_path: %w[system_one host], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
191
|
+
# The judgment model; unset: the host's first model, and no judgment if it names none.
|
|
192
|
+
Entry.new(key: "system_one.model", yaml_path: %w[system_one model], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
193
|
+
# How long one judgment may take, in milliseconds; a judgment that doesn't arrive is no judgment.
|
|
194
|
+
Entry.new(key: "system_one.timeout_ms", yaml_path: %w[system_one timeout_ms], type: :integer, default: 2000, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
195
|
+
# Save each System 1 ask and what came of it (docs/system-one.md): on (the default) | off.
|
|
196
|
+
Entry.new(key: "system_one.runs", yaml_path: %w[system_one runs], type: :enum, default: "on", expose: %i[config], enum_values: %w[on off], lifecycle: :next_tool_or_approval),
|
|
193
197
|
|
|
194
198
|
Entry.new(key: "log.file", yaml_path: %w[log file], type: :string, default: nil, expose: %i[env config cli], lifecycle: :process_start),
|
|
195
199
|
Entry.new(key: "log.disable", yaml_path: %w[log disable], type: :bool, default: false, expose: %i[env config cli], lifecycle: :process_start),
|
|
@@ -204,6 +208,12 @@ module Samagotchi
|
|
|
204
208
|
# The tokens one scope's memory index (sent with every prompt) may hold before a write that crosses it gets a note
|
|
205
209
|
# (MemoryBundle::IndexSize); 0: no note. `memory:` is its own section, not the `memories:` preload list.
|
|
206
210
|
Entry.new(key: "memory.index_warn_tokens", yaml_path: %w[memory index_warn_tokens], type: :integer, default: 2500, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
211
|
+
# The most characters one memory_read result holds (ToolOutputCut cuts it with a line naming the memory file); 0: no
|
|
212
|
+
# fixed cap. memory_read is not cut by max_tool_output_chars (ToolRunner).
|
|
213
|
+
Entry.new(key: "memory.read_max_chars", yaml_path: %w[memory read_max_chars], type: :integer, default: 0, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
214
|
+
# The tokens one memory may hold (each read of it pays them) before a write that crosses it gets a note
|
|
215
|
+
# (MemoryBundle::EntrySize); 0: no note.
|
|
216
|
+
Entry.new(key: "memory.entry_warn_tokens", yaml_path: %w[memory entry_warn_tokens], type: :integer, default: 5000, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
207
217
|
Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds], type: :string, default: "20,40,60,80", expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
208
218
|
# Attached context (ContextSources): how often a source's command runs when it names no --every (seconds; less
|
|
209
219
|
# than 30 counts as 30).
|
|
@@ -219,7 +229,7 @@ module Samagotchi
|
|
|
219
229
|
Entry.new(key: "context.peer_turns", yaml_path: %w[context peer_turns], type: :integer, default: 3, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
220
230
|
# The candidate judge (ContextJudge): off (no conversation text goes to a System 1 host); audit (asks and logs
|
|
221
231
|
# context_candidates.jsonl, attaches nothing); on (ContextCandidateChoice: p ≥ threshold attaches quietly,
|
|
222
|
-
# offer_threshold ≤ p < threshold shows one card). Its host and model are
|
|
232
|
+
# offer_threshold ≤ p < threshold shows one card). Its host and model are system_one.host and .model.
|
|
223
233
|
# reask_after_turns: the owner prompts after a judged ask before its one re-ask.
|
|
224
234
|
Entry.new(key: "context.candidates.mode", yaml_path: %w[context candidates mode], type: :enum, default: "off", expose: %i[config], enum_values: %w[off audit on], lifecycle: :next_matching_turn_display_poll_write),
|
|
225
235
|
Entry.new(key: "context.candidates.threshold", yaml_path: %w[context candidates threshold], type: :float, default: 0.8, expose: %i[config], lifecycle: :next_matching_turn_display_poll_write),
|
|
@@ -260,7 +270,7 @@ module Samagotchi
|
|
|
260
270
|
Entry.new(key: "toolkit.default", yaml_path: %w[toolkit default], type: :string, default: nil, expose: %i[env config], lifecycle: :session_creation),
|
|
261
271
|
Entry.new(key: "toolkit.child", yaml_path: %w[toolkit child], type: :string, default: nil, expose: %i[env config], lifecycle: :session_creation),
|
|
262
272
|
# Toolkit suggestion (ToolkitSuggest): off; audit (logs, never blocks or changes a session); on (the first eligible
|
|
263
|
-
# turn waits up to
|
|
273
|
+
# turn waits up to system_one.timeout_ms and may start on the suggested kit).
|
|
264
274
|
Entry.new(key: "toolkit.suggest.mode", yaml_path: %w[toolkit suggest mode], type: :enum, default: "off", expose: %i[config], enum_values: %w[off audit on], lifecycle: :worker_start),
|
|
265
275
|
Entry.new(key: "toolkit.suggest.threshold", yaml_path: %w[toolkit suggest threshold], type: :float, default: 0.5, expose: %i[config], lifecycle: :worker_start),
|
|
266
276
|
Entry.new(key: "toolkit.suggest.defer_turns", yaml_path: %w[toolkit suggest defer_turns], type: :integer, default: 3, expose: %i[config], lifecycle: :worker_start),
|
|
@@ -390,8 +400,18 @@ module Samagotchi
|
|
|
390
400
|
# The keys a hosts.<name>.models entry may hold (HostModel.parse_map).
|
|
391
401
|
HOST_MODEL_KEYS = %w[price served].freeze
|
|
392
402
|
# Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
|
|
393
|
-
# And toolkit's kits (Toolkit.kits).
|
|
394
|
-
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze, "toolkit" => %w[kits].freeze }.freeze
|
|
403
|
+
# And toolkit's kits (Toolkit.kits), and bundles' judgments' settings (SystemOne::JudgmentSettings).
|
|
404
|
+
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze, "toolkit" => %w[kits].freeze, "system_one" => %w[judgments].freeze }.freeze
|
|
405
|
+
|
|
406
|
+
# guardrails.system_one's keys that moved to system_one (SystemOne::Client.from_config).
|
|
407
|
+
SYSTEM_ONE_MOVED_KEYS = %w[host model timeout_ms].freeze
|
|
408
|
+
# What a system_one.judgments entry may hold. Core's judgments take none (RESERVED_JUDGMENTS).
|
|
409
|
+
JUDGMENT_ENTRY_KEYS = %w[mode host model].freeze
|
|
410
|
+
JUDGMENT_MODES = %w[off audit on].freeze
|
|
411
|
+
# Names core keeps, as a judgment's first word: a bundle can't declare one and
|
|
412
|
+
# system_one.judgments can't set one (Record and Similar are model-blind, so a
|
|
413
|
+
# swapped model would inherit a clean record and could auto-allow).
|
|
414
|
+
RESERVED_JUDGMENTS = %w[approvals context guardrails toolkit].freeze
|
|
395
415
|
|
|
396
416
|
# What a :bool value reads as (Config.coerce!), YAML 1.1's words and 1/0.
|
|
397
417
|
BOOL_WORDS = { "1" => true, "true" => true, "yes" => true, "on" => true,
|
|
@@ -464,6 +484,14 @@ module Samagotchi
|
|
|
464
484
|
word && entry.enum_values.include?(word) ? word : nil
|
|
465
485
|
end
|
|
466
486
|
|
|
487
|
+
# A system_one.judgments entry's mode as written, nil for one it can't
|
|
488
|
+
# be: an unquoted on/off is a boolean there too.
|
|
489
|
+
# @return [String, nil]
|
|
490
|
+
def judgment_mode(raw)
|
|
491
|
+
word = BOOL_ENUM_WORDS.fetch(raw) { raw.to_s.strip.downcase }
|
|
492
|
+
word if JUDGMENT_MODES.include?(word)
|
|
493
|
+
end
|
|
494
|
+
|
|
467
495
|
# What +entry+'s type takes, as a refusal names it.
|
|
468
496
|
def takes(entry)
|
|
469
497
|
case entry.type
|
|
@@ -692,6 +720,8 @@ module Samagotchi
|
|
|
692
720
|
# A section's leaf: a key the registry knows, or a mapping of a
|
|
693
721
|
# third level (web.participants.cap) whose keys it knows.
|
|
694
722
|
def leaf_problems(path, value)
|
|
723
|
+
return judgment_problems(value) if path == "system_one.judgments"
|
|
724
|
+
|
|
695
725
|
if value.is_a?(Hash) && ENTRIES.any? { |e| e.key.start_with?("#{path}.") }
|
|
696
726
|
value.keys.filter_map { |leaf| key_problem("#{path}.#{leaf}") }
|
|
697
727
|
else
|
|
@@ -699,6 +729,29 @@ module Samagotchi
|
|
|
699
729
|
end
|
|
700
730
|
end
|
|
701
731
|
|
|
732
|
+
# system_one.judgments' entries: a map entry bypasses the registry, so
|
|
733
|
+
# its keys and mode are checked here (an unquoted on/off is a boolean).
|
|
734
|
+
# An undeclared name can only be told once the plugins have loaded
|
|
735
|
+
# (SystemOne::Judgments#warn_undeclared).
|
|
736
|
+
def judgment_problems(value)
|
|
737
|
+
return value.nil? ? [] : ["config: 'system_one.judgments' must be a mapping of judgments; ignored"] unless value.is_a?(Hash)
|
|
738
|
+
|
|
739
|
+
value.flat_map do |name, entry|
|
|
740
|
+
where = "system_one.judgments.#{name}"
|
|
741
|
+
if RESERVED_JUDGMENTS.include?(name.to_s.split(".").first)
|
|
742
|
+
next ["config: #{where}: core's judgments take no settings of their own; ignored (see docs/system-one.md)"]
|
|
743
|
+
end
|
|
744
|
+
next ["config: #{where} must be a mapping of #{JUDGMENT_ENTRY_KEYS.join(", ")}; ignored"] unless entry.is_a?(Hash)
|
|
745
|
+
|
|
746
|
+
unknown = (entry.keys.map(&:to_s) - JUDGMENT_ENTRY_KEYS).map do |k|
|
|
747
|
+
unknown_key_message("#{where}.#{k}", k, JUDGMENT_ENTRY_KEYS, prefix: "#{where}.")
|
|
748
|
+
end
|
|
749
|
+
mode = entry.key?("mode") && judgment_mode(entry["mode"]).nil? ? ["config: #{where}.mode must be off, audit or on; it is off"] : []
|
|
750
|
+
text = %w[host model].reject { |k| entry[k].nil? || entry[k].is_a?(String) }.map { |k| "config: #{where}.#{k} must be text" }
|
|
751
|
+
unknown + mode + text
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
702
755
|
# What a model ref in config.yml won't do as written: an alias whose
|
|
703
756
|
# target is another alias (aliases apply once, ModelRef), a ref that
|
|
704
757
|
# starts with "<host>/" (only ':' names a host).
|
|
@@ -752,10 +805,17 @@ module Samagotchi
|
|
|
752
805
|
@known_sections ||= known_config_keys.filter_map { |k| k.split(".").first if k.include?(".") }.to_set.freeze
|
|
753
806
|
end
|
|
754
807
|
|
|
755
|
-
# Keys replaced by another
|
|
808
|
+
# Keys replaced by another, and what the warning adds: it names the new
|
|
809
|
+
# one (no alias).
|
|
810
|
+
PARENT_ANSWERS_HINT = "hard, standard or soft; see docs/guardrails.md"
|
|
811
|
+
SYSTEM_ONE_MOVED_HINT = "it has moved: until all of host, model and timeout_ms have, nothing is judged or " \
|
|
812
|
+
"auto-allowed, and every ask goes to you; see docs/configuration.md"
|
|
756
813
|
REPLACED_KEYS = {
|
|
757
|
-
"guardrails.parent_approvals" => "guardrails.parent_answers",
|
|
758
|
-
"turn.parent_continue" => "guardrails.parent_answers"
|
|
814
|
+
"guardrails.parent_approvals" => ["guardrails.parent_answers", PARENT_ANSWERS_HINT],
|
|
815
|
+
"turn.parent_continue" => ["guardrails.parent_answers", PARENT_ANSWERS_HINT],
|
|
816
|
+
"guardrails.system_one.host" => ["system_one.host", SYSTEM_ONE_MOVED_HINT],
|
|
817
|
+
"guardrails.system_one.model" => ["system_one.model", SYSTEM_ONE_MOVED_HINT],
|
|
818
|
+
"guardrails.system_one.timeout_ms" => ["system_one.timeout_ms", SYSTEM_ONE_MOVED_HINT]
|
|
759
819
|
}.freeze
|
|
760
820
|
|
|
761
821
|
# nil when +dotted+ is a key the file may hold.
|
|
@@ -763,7 +823,7 @@ module Samagotchi
|
|
|
763
823
|
return nil if known_config_keys.include?(dotted)
|
|
764
824
|
return "config: #{ConfigFile.removed_transport_problem("'#{dotted}'")}" if dotted == "server.transport"
|
|
765
825
|
if (replaced = REPLACED_KEYS[dotted])
|
|
766
|
-
return "config: unknown key '#{dotted}': replaced by '#{replaced}' (
|
|
826
|
+
return "config: unknown key '#{dotted}': replaced by '#{replaced.first}' (#{replaced.last})"
|
|
767
827
|
end
|
|
768
828
|
|
|
769
829
|
entry = find_by_key(dotted)
|
|
@@ -133,12 +133,19 @@ module Samagotchi
|
|
|
133
133
|
own.add(ContextCandidates.source_for(candidate, why: CHOSEN_WHY))
|
|
134
134
|
Log.info(:context, "candidate_attached", name: candidate.name, trigger: "command")
|
|
135
135
|
ContextCandidatesLog.event("attached", session_id: session.id, name: candidate.name, state_dir: state_dir, by: "command")
|
|
136
|
+
label_run(session, candidate, "attached", state_dir)
|
|
136
137
|
show_card.call(**closed_card(candidate, "Attached."))
|
|
137
138
|
[:ok, "attached #{candidate.name}: its text comes with your next message"]
|
|
138
139
|
rescue ContextSources::Invalid, ContextProviders::Invalid => e
|
|
139
140
|
[:error, "#{candidate.name} not attached: #{e.message}"]
|
|
140
141
|
end
|
|
141
142
|
|
|
143
|
+
# /context attach|decline (the card's buttons), on the latest ask's run.
|
|
144
|
+
def label_run(session, candidate, value, state_dir)
|
|
145
|
+
records = ContextCandidatesLog.records_for(ContextCandidatesLog.log_for(state_dir), session.id)
|
|
146
|
+
ContextCandidatesLog.label_run(records, candidate.name, value, by: "command", sessions_dir: state_dir)
|
|
147
|
+
end
|
|
148
|
+
|
|
142
149
|
def decline(candidate, status, session, state_dir, show_card)
|
|
143
150
|
if status == "attached"
|
|
144
151
|
return [:error, "#{candidate.name} is attached; chi context rm #{candidate.name} removes it"]
|
|
@@ -147,6 +154,7 @@ module Samagotchi
|
|
|
147
154
|
|
|
148
155
|
ContextSources.session_location(session.id, state_dir: state_dir).decline(candidate.name, candidate.url.to_s)
|
|
149
156
|
ContextCandidatesLog.event("declined", session_id: session.id, name: candidate.name, state_dir: state_dir, by: "command")
|
|
157
|
+
label_run(session, candidate, "declined", state_dir)
|
|
150
158
|
show_card.call(**closed_card(candidate, "Not attached here."))
|
|
151
159
|
[:ok, "declined #{candidate.name}: not proposed again in this session"]
|
|
152
160
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "log"
|
|
4
4
|
require_relative "session"
|
|
5
5
|
require_relative "side_judgments"
|
|
6
|
+
require_relative "system_one/runs"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# The candidates' record, `context_candidates.jsonl` in the state dir
|
|
@@ -15,7 +16,8 @@ module Samagotchi
|
|
|
15
16
|
# Kinds (every line has session_id, name and at):
|
|
16
17
|
# - proposed: a plugin's ctx.context.propose recorded it (title, by)
|
|
17
18
|
# - mentioned: an owner prompt named it and it was attached (mention)
|
|
18
|
-
# - judged: System 1's ask (ContextJudge.ask: p, task_p, stated,
|
|
19
|
+
# - judged: System 1's ask (ContextJudge.ask: run_id, p, task_p, stated,
|
|
20
|
+
# would, …)
|
|
19
21
|
# - judge_attached: on mode attached it (p ≥ threshold; p)
|
|
20
22
|
# - carded: on mode showed its card (the middle band; p): once per
|
|
21
23
|
# candidate, read back by ContextCandidateChoice.carded?
|
|
@@ -34,6 +36,10 @@ module Samagotchi
|
|
|
34
36
|
STATUS_OF = { "proposed" => "proposed", "mentioned" => "attached", "judge_attached" => "attached", "attached" => "attached",
|
|
35
37
|
"declined" => "declined", "detached" => "proposed", "withdrawn" => nil }.freeze
|
|
36
38
|
|
|
39
|
+
# The kinds #look sees that are a removal: a chip's ×, chi context rm. One
|
|
40
|
+
# removed after the session's last turn is never seen.
|
|
41
|
+
REMOVED = %w[declined detached].freeze
|
|
42
|
+
|
|
37
43
|
module_function
|
|
38
44
|
|
|
39
45
|
# @param state_dir [String, nil] the sessions folder (Session.default_state_dir)
|
|
@@ -77,10 +83,28 @@ module Samagotchi
|
|
|
77
83
|
|
|
78
84
|
kind = status == "proposed" ? "detached" : status
|
|
79
85
|
event(kind, session_id: session_id, name: candidate.name, log: log, by: "user")
|
|
86
|
+
label_run(records, candidate.name, kind, by: "user", root: log.dir) if REMOVED.include?(kind)
|
|
80
87
|
kind
|
|
81
88
|
end
|
|
82
89
|
end
|
|
83
90
|
|
|
91
|
+
# The user's say on a candidate, as a label on the latest ask about it
|
|
92
|
+
# (the judged line's run, question "about"). No ask with a run: nothing.
|
|
93
|
+
# Never raises.
|
|
94
|
+
# @param records [Array<Hash>] the session's lines (#records_for)
|
|
95
|
+
# @param root [String, nil] the state dir; nil: from +sessions_dir+
|
|
96
|
+
# @return [Boolean] whether a label was written
|
|
97
|
+
def label_run(records, name, value, by:, sessions_dir: nil, root: nil)
|
|
98
|
+
judged = records.reverse_each.find { |line| line["kind"] == "judged" && line["name"] == name && line["run_id"] }
|
|
99
|
+
return false if judged.nil?
|
|
100
|
+
|
|
101
|
+
runs = SystemOne::Runs.in(root || File.dirname(sessions_dir || Session.default_state_dir))
|
|
102
|
+
runs.label(judged["run_id"], value, by: by, question: "about")
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
Log.warn(:context, "context_candidates_log_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
|
|
84
108
|
# +text+ as a log line keeps it: one line, at most PROMPT_MAX chars.
|
|
85
109
|
def short(text)
|
|
86
110
|
line = text.to_s.gsub(/\s+/, " ").strip
|
|
@@ -25,12 +25,14 @@ module Samagotchi
|
|
|
25
25
|
|
|
26
26
|
USAGE = <<~TEXT
|
|
27
27
|
Usage: chi context <add|push|ls|show|refresh|rm|mute|unmute|report> [options] [TARGET]
|
|
28
|
-
add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] TARGET
|
|
29
|
-
add URL [--why TEXT] [--hint TEXT] TARGET a URL an installed bundle's provider knows (github-pr: a PR)
|
|
28
|
+
add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] [--[no-]movable] TARGET
|
|
29
|
+
add URL [--why TEXT] [--hint TEXT] [--[no-]movable] TARGET a URL an installed bundle's provider knows (github-pr: a PR)
|
|
30
30
|
a source: CMD prints its text (plain, or JSON {"text", "summary", "wake", "hint"});
|
|
31
31
|
--push: text comes from chi context push. --every: how often CMD runs
|
|
32
32
|
(seconds, at least 30; default context.every_seconds). --why: why it's attached;
|
|
33
33
|
--hint: one line, often its URL. NAME: a-z, 0-9 and -, up to 40.
|
|
34
|
+
--movable / --no-movable: whether a continue or a fork takes it to the new
|
|
35
|
+
session (default: a --cmd or URL source moves, a --push one stays).
|
|
34
36
|
push NAME [-m TEXT] [TARGET] new text for a --push source (stdin without -m; text or JSON)
|
|
35
37
|
ls [TARGET] [--format json] the sources, their age and state; a session's candidates
|
|
36
38
|
show NAME [--json] [TARGET] the text (--json: the source and its snapshot)
|
|
@@ -52,6 +54,8 @@ module Samagotchi
|
|
|
52
54
|
f.value "--every"
|
|
53
55
|
f.value "--why"
|
|
54
56
|
f.value "--hint"
|
|
57
|
+
f.switch "--movable"
|
|
58
|
+
f.switch "--no-movable", key: :movable, set: false
|
|
55
59
|
f.switch "--project"
|
|
56
60
|
end,
|
|
57
61
|
"push" => CLI::Flags.new(help: HELP_WORDS) do |f|
|
|
@@ -142,15 +146,27 @@ module Samagotchi
|
|
|
142
146
|
name: name, cmd: options[:cmd], every_seconds: ContextSources.check_every!(options[:every]),
|
|
143
147
|
why: ContextSources.one_line(options[:why], ContextSources::LINE_MAX_CHARS),
|
|
144
148
|
hint: ContextSources.one_line(options[:hint], ContextSources::LINE_MAX_CHARS),
|
|
145
|
-
scope: nil, added_by: added_by, created_at: Time.now.utc.iso8601
|
|
149
|
+
scope: nil, added_by: added_by, created_at: Time.now.utc.iso8601, movable: options[:movable]
|
|
146
150
|
)
|
|
147
151
|
each_target(options, args) do |target|
|
|
148
|
-
|
|
152
|
+
add_to(target, source)
|
|
149
153
|
@stdout.puts("#{target.label} attached #{name}")
|
|
150
154
|
true
|
|
151
155
|
end
|
|
152
156
|
end
|
|
153
157
|
|
|
158
|
+
# Attaches +source+ to +target+; a movable flag an older chi web
|
|
159
|
+
# dropped (its answer has no movable) is a warning: the source is
|
|
160
|
+
# attached with the default.
|
|
161
|
+
def add_to(target, source)
|
|
162
|
+
warn_movable_dropped(target, source.name) if @access.context_add(target, source).movable_dropped
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def warn_movable_dropped(target, name)
|
|
166
|
+
error_line("#{command_name}: warning: the remote's chi web is too old for --movable/--no-movable: " \
|
|
167
|
+
"#{name} on #{target.label} has the default (update chi there)")
|
|
168
|
+
end
|
|
169
|
+
|
|
154
170
|
# A URL through the installed bundles' providers (ContextProviders):
|
|
155
171
|
# the provider gives the name, command, hint and interval. Not gated
|
|
156
172
|
# like --cmd (chi-context-cmd): the command is the bundle's.
|
|
@@ -167,10 +183,11 @@ module Samagotchi
|
|
|
167
183
|
name: resolved.name, cmd: resolved.cmd, every_seconds: resolved.every_seconds,
|
|
168
184
|
why: ContextSources.one_line(options[:why] || resolved.why, ContextSources::LINE_MAX_CHARS),
|
|
169
185
|
hint: ContextSources.one_line(options[:hint] || resolved.hint, ContextSources::LINE_MAX_CHARS),
|
|
170
|
-
scope: nil, added_by: added_by, created_at: Time.now.utc.iso8601, provider: resolved.bundle
|
|
186
|
+
scope: nil, added_by: added_by, created_at: Time.now.utc.iso8601, provider: resolved.bundle,
|
|
187
|
+
movable: options[:movable]
|
|
171
188
|
)
|
|
172
189
|
each_target(options, args) do |target|
|
|
173
|
-
|
|
190
|
+
add_to(target, source)
|
|
174
191
|
@stdout.puts("#{target.label} attached #{source.name}")
|
|
175
192
|
true
|
|
176
193
|
end
|
|
@@ -183,8 +200,9 @@ module Samagotchi
|
|
|
183
200
|
why = ContextSources.one_line(options[:why], ContextSources::LINE_MAX_CHARS)
|
|
184
201
|
hint = ContextSources.one_line(options[:hint], ContextSources::LINE_MAX_CHARS)
|
|
185
202
|
each_target(options, args) do |target|
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
added = @access.context_add_url(target, url, why: why, hint: hint, added_by: added_by, movable: options[:movable])
|
|
204
|
+
warn_movable_dropped(target, added.name) if added.movable_dropped
|
|
205
|
+
@stdout.puts("#{target.label} attached #{added.name}")
|
|
188
206
|
true
|
|
189
207
|
end
|
|
190
208
|
rescue ContextProviders::Invalid => e
|
|
@@ -210,6 +228,9 @@ module Samagotchi
|
|
|
210
228
|
# two fetches would read as a change and then vanish.
|
|
211
229
|
error_line("#{command_name}: #{SessionAccess::ContextPush.not_push_message(name)}")
|
|
212
230
|
next false
|
|
231
|
+
when :continued
|
|
232
|
+
error_line("#{command_name}: #{SessionAccess::ContextPush.continued_message(name, target.session_id, pushed.next_id)}")
|
|
233
|
+
next false
|
|
213
234
|
end
|
|
214
235
|
|
|
215
236
|
@stdout.puts("#{target.label} #{name}: #{pushed.revision == pushed.before ? "unchanged" : "new text"} " \
|
|
@@ -452,7 +473,8 @@ module Samagotchi
|
|
|
452
473
|
"cmd" => source.cmd, "every_seconds" => source.every_seconds, "why" => source.why, "hint" => source.hint,
|
|
453
474
|
"added_by" => source.added_by, "created_at" => source.created_at, "fetched_at" => snapshot.fetched_at,
|
|
454
475
|
"revision" => snapshot.revision, "summary" => snapshot.summary, "error" => snapshot.error,
|
|
455
|
-
"muted" => entry.muted, "shadowed" => entry.shadowed, "unread" => entry.unread
|
|
476
|
+
"muted" => entry.muted, "shadowed" => entry.shadowed, "unread" => entry.unread,
|
|
477
|
+
"movable" => (source.movable? if source.scope == "session") }.compact
|
|
456
478
|
end
|
|
457
479
|
|
|
458
480
|
def print_rows(rows, labels:)
|
|
@@ -465,6 +487,7 @@ module Samagotchi
|
|
|
465
487
|
else
|
|
466
488
|
"ok"
|
|
467
489
|
end
|
|
490
|
+
state = "#{state} · stays" if r["movable"] == false
|
|
468
491
|
[*(labels ? [r["target"]] : []), r["name"], r["scope"], kind, age(r["fetched_at"]), state, r["why"].to_s]
|
|
469
492
|
end
|
|
470
493
|
widths = lines.transpose.map { |column| column.map(&:length).max }
|
|
@@ -4,6 +4,7 @@ require_relative "config"
|
|
|
4
4
|
require_relative "context_candidates_log"
|
|
5
5
|
require_relative "log"
|
|
6
6
|
require_relative "system_one/question"
|
|
7
|
+
require_relative "system_one/runs"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
10
|
# System 1's judgment of a session's candidate context
|
|
@@ -26,6 +27,8 @@ module Samagotchi
|
|
|
26
27
|
# (attach, card, or nothing); audit only logs.
|
|
27
28
|
module ContextJudge
|
|
28
29
|
MODES = %w[off audit on].freeze
|
|
30
|
+
# Its saved asks' name (SystemOne::Runs).
|
|
31
|
+
JUDGMENT = "context.candidate"
|
|
29
32
|
ABOUT_ID = "about"
|
|
30
33
|
TASK_ID = "task"
|
|
31
34
|
# The bound on each prompt in the state.
|
|
@@ -168,14 +171,15 @@ module Samagotchi
|
|
|
168
171
|
# candidate is asked again at the next owner prompt.
|
|
169
172
|
# @return [Hash, nil] the line's fields
|
|
170
173
|
def ask(client:, job:, log:, settings:, cancel_controller: nil)
|
|
174
|
+
run_id = SystemOne::Runs.new_id(JUDGMENT)
|
|
171
175
|
answers = client.ask(state: state(job), questions: questions(child: !job.parent_attached.nil?),
|
|
172
|
-
cancel_controller: cancel_controller)
|
|
176
|
+
cancel_controller: cancel_controller, run_id: run_id)
|
|
173
177
|
about = answers&.dig(ABOUT_ID)&.noul
|
|
174
178
|
task = answers&.dig(TASK_ID)&.noul
|
|
175
179
|
return nil if about.nil? || task.nil?
|
|
176
180
|
|
|
177
181
|
stated = task >= TASK_MIN
|
|
178
|
-
fields = { "title" => job.title, "phase" => job.phase, "owner_prompts" => job.owner_prompts,
|
|
182
|
+
fields = { "run_id" => run_id, "title" => job.title, "phase" => job.phase, "owner_prompts" => job.owner_prompts,
|
|
179
183
|
"p" => about.round(4), "task_p" => task.round(4), "stated" => stated,
|
|
180
184
|
"would" => stated ? would(about, settings) : nil, "mode" => settings.mode,
|
|
181
185
|
"threshold" => settings.threshold, "offer_threshold" => settings.offer_threshold,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "context_sources"
|
|
4
|
+
require_relative "session_inbox"
|
|
5
|
+
require_relative "log"
|
|
6
|
+
|
|
7
|
+
module Samagotchi
|
|
8
|
+
# Movable context on its two paths, a continue (SessionContinue) and a
|
|
9
|
+
# plugin fork (ctx.sessions.fork): from SessionManager.spawn_session's
|
|
10
|
+
# before_spawn, once the new session and its first note are saved and
|
|
11
|
+
# before its worker starts, so its first absorb sees the sources.
|
|
12
|
+
module ContextMove
|
|
13
|
+
# What the new session's note says it is from.
|
|
14
|
+
NOTE_SOURCE = "attached context"
|
|
15
|
+
|
|
16
|
+
# ContextSources.move, then a second note in the new session saying what
|
|
17
|
+
# moved and what stayed (named by time, it sorts after the first). Best
|
|
18
|
+
# effort: a failure is logged and the session starts without (its start
|
|
19
|
+
# must not be undone for it).
|
|
20
|
+
# @return [ContextSources::Moved]
|
|
21
|
+
def self.call(from:, to:, mode:, state_dir:, seed: nil)
|
|
22
|
+
moved = ContextSources.move(from: from, to: to, mode: mode, seed: seed, state_dir: state_dir)
|
|
23
|
+
text = moved.note_text(from)
|
|
24
|
+
SessionInbox.write_note(to, text: text, source: NOTE_SOURCE, state_dir: state_dir) if text
|
|
25
|
+
moved
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
Log.warn(:context, "context_move_failed", sid: to, from: from, error: e.class.name, msg: e.message.to_s[0, 200])
|
|
28
|
+
ContextSources::Moved.none
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|